fix: recording session ID routing, IIFE for timestamps, default speed 5x

cdp-relay.ts:
- Fix pre-existing bug: recording HTTP routes (/recording/start, stop,
  status, cancel) were treating CDP session IDs (pw-tab-*) as executor
  session IDs, causing 'Session not found' errors. Now skip executor
  lookup when sessionId is a CDP session ID and pass it through directly
  to the extension for tab routing.

executor.ts:
- Use async IIFE instead of let+try for VM execution timestamp tracking.
  Cleaner pattern: const result = await (async () => { try/finally })()

ffmpeg.ts + skill.md:
- Change default idle speed from 4x to 5x
This commit is contained in:
Tommy D. Rossi
2026-02-25 13:12:41 +01:00
parent d84cdba528
commit 1cad56f1a2
4 changed files with 36 additions and 26 deletions
+16 -7
View File
@@ -1729,9 +1729,15 @@ export async function startPlayWriterCDPRelayServer({
} }
const sessionId = normalizeSessionId(body.sessionId) const sessionId = normalizeSessionId(body.sessionId)
const { sessionId: _sessionId, ...recordingOptions } = body const { sessionId: _sessionId, ...recordingOptions } = body
// sessionId serves two purposes here:
// 1. CDP session ID (pw-tab-*) — routes to the correct tab in the extension
// 2. Executor session ID (numeric) — used to find which extension to route to
// Only look up the executor when sessionId is NOT a CDP session ID.
const isCdpSessionId = sessionId?.startsWith('pw-tab-')
const manager = await getExecutorManager() const manager = await getExecutorManager()
const executor = sessionId ? manager.getSession(sessionId) : null const executor = sessionId && !isCdpSessionId ? manager.getSession(sessionId) : null
if (sessionId && !executor) { if (sessionId && !isCdpSessionId && !executor) {
return c.json({ success: false, error: `Session ${sessionId} not found` }, 404) return c.json({ success: false, error: `Session ${sessionId} not found` }, 404)
} }
const extensionId = executor?.getSessionMetadata().extensionId || null const extensionId = executor?.getSessionMetadata().extensionId || null
@@ -1748,9 +1754,10 @@ export async function startPlayWriterCDPRelayServer({
app.post('/recording/stop', async (c) => { app.post('/recording/stop', async (c) => {
const body = (await c.req.json()) as { sessionId?: string | number } const body = (await c.req.json()) as { sessionId?: string | number }
const sessionId = normalizeSessionId(body.sessionId) const sessionId = normalizeSessionId(body.sessionId)
const isCdpSessionId = sessionId?.startsWith('pw-tab-')
const manager = await getExecutorManager() const manager = await getExecutorManager()
const executor = sessionId ? manager.getSession(sessionId) : null const executor = sessionId && !isCdpSessionId ? manager.getSession(sessionId) : null
if (sessionId && !executor) { if (sessionId && !isCdpSessionId && !executor) {
return c.json({ success: false, error: `Session ${sessionId} not found` }, 404) return c.json({ success: false, error: `Session ${sessionId} not found` }, 404)
} }
const extensionId = executor?.getSessionMetadata().extensionId || null const extensionId = executor?.getSessionMetadata().extensionId || null
@@ -1767,8 +1774,9 @@ export async function startPlayWriterCDPRelayServer({
app.get('/recording/status', async (c) => { app.get('/recording/status', async (c) => {
const sessionId = normalizeSessionId(c.req.query('sessionId')) const sessionId = normalizeSessionId(c.req.query('sessionId'))
const normalizedSessionId = sessionId || undefined const normalizedSessionId = sessionId || undefined
const isCdpSessionId = normalizedSessionId?.startsWith('pw-tab-')
const manager = await getExecutorManager() const manager = await getExecutorManager()
const executor = normalizedSessionId ? manager.getSession(normalizedSessionId) : null const executor = normalizedSessionId && !isCdpSessionId ? manager.getSession(normalizedSessionId) : null
const extensionId = executor?.getSessionMetadata().extensionId || null const extensionId = executor?.getSessionMetadata().extensionId || null
const relay = getRecordingRelay(extensionId) const relay = getRecordingRelay(extensionId)
if (!relay) { if (!relay) {
@@ -1782,9 +1790,10 @@ export async function startPlayWriterCDPRelayServer({
app.post('/recording/cancel', async (c) => { app.post('/recording/cancel', async (c) => {
const body = (await c.req.json()) as { sessionId?: string | number } const body = (await c.req.json()) as { sessionId?: string | number }
const sessionId = normalizeSessionId(body.sessionId) const sessionId = normalizeSessionId(body.sessionId)
const isCdpSessionId = sessionId?.startsWith('pw-tab-')
const manager = await getExecutorManager() const manager = await getExecutorManager()
const executor = sessionId ? manager.getSession(sessionId) : null const executor = sessionId && !isCdpSessionId ? manager.getSession(sessionId) : null
if (sessionId && !executor) { if (sessionId && !isCdpSessionId && !executor) {
return c.json({ success: false, error: `Session ${sessionId} not found` }, 404) return c.json({ success: false, error: `Session ${sessionId} not found` }, 404)
} }
const extensionId = executor?.getSessionMetadata().extensionId || null const extensionId = executor?.getSessionMetadata().extensionId || null
+15 -14
View File
@@ -1035,26 +1035,27 @@ export class PlaywrightExecutor {
// Track execution timestamps relative to recording start (seconds). // Track execution timestamps relative to recording start (seconds).
// Used to identify idle gaps that can be sped up in demo videos. // Used to identify idle gaps that can be sped up in demo videos.
// Captured before try so we can record timing even if execution throws. // Captured before execution so we can record timing even if it throws.
const recordingStartSnapshot = this.recordingStartedAt const recordingStartSnapshot = this.recordingStartedAt
const execStartSec = recordingStartSnapshot !== null const execStartSec = recordingStartSnapshot !== null
? (Date.now() - recordingStartSnapshot) / 1000 ? (Date.now() - recordingStartSnapshot) / 1000
: -1 : -1
let result: unknown const result = await (async () => {
try { try {
result = await Promise.race([ return await Promise.race([
vm.runInContext(wrappedCode, vmContext, { timeout, displayErrors: true }), vm.runInContext(wrappedCode, vmContext, { timeout, displayErrors: true }),
new Promise((_, reject) => setTimeout(() => reject(new CodeExecutionTimeoutError(timeout)), timeout)), new Promise((_, reject) => setTimeout(() => reject(new CodeExecutionTimeoutError(timeout)), timeout)),
]) ])
} finally { } finally {
// Record timestamp even on error — the execution still occupied real time // Record timestamp even on error — the execution still occupied real time
// that should not be sped up in the demo video. // that should not be sped up in the demo video.
if (recordingStartSnapshot !== null && execStartSec >= 0 && this.recordingStartedAt !== null) { if (recordingStartSnapshot !== null && execStartSec >= 0 && this.recordingStartedAt !== null) {
const execEndSec = (Date.now() - recordingStartSnapshot) / 1000 const execEndSec = (Date.now() - recordingStartSnapshot) / 1000
this.executionTimestamps.push({ start: execStartSec, end: execEndSec }) this.executionTimestamps.push({ start: execStartSec, end: execEndSec })
}
} }
} })()
let responseText = formatConsoleLogs(consoleLogs) let responseText = formatConsoleLogs(consoleLogs)
+4 -4
View File
@@ -420,13 +420,13 @@ export interface ExecutionTimestamp {
export function computeIdleSections({ export function computeIdleSections({
executionTimestamps, executionTimestamps,
totalDurationMs, totalDurationMs,
speed = 4, speed = 5,
bufferSeconds = INTERACTION_BUFFER_SECONDS, bufferSeconds = INTERACTION_BUFFER_SECONDS,
}: { }: {
executionTimestamps: ExecutionTimestamp[] executionTimestamps: ExecutionTimestamp[]
/** Total recording duration in milliseconds (from stopRecording result) */ /** Total recording duration in milliseconds (from stopRecording result) */
totalDurationMs: number totalDurationMs: number
/** Speed multiplier for idle sections (default 4) */ /** Speed multiplier for idle sections (default 5) */
speed?: number speed?: number
/** Override the default buffer around each execution (seconds) */ /** Override the default buffer around each execution (seconds) */
bufferSeconds?: number bufferSeconds?: number
@@ -497,7 +497,7 @@ export interface CreateDemoVideoOptions {
durationMs: number durationMs: number
/** Execution timestamps (from stopRecording result) */ /** Execution timestamps (from stopRecording result) */
executionTimestamps: ExecutionTimestamp[] executionTimestamps: ExecutionTimestamp[]
/** Speed multiplier for idle sections (default 4) */ /** Speed multiplier for idle sections (default 5) */
speed?: number speed?: number
/** Output file path (defaults to recordingPath with `-demo` suffix) */ /** Output file path (defaults to recordingPath with `-demo` suffix) */
outputFile?: string outputFile?: string
@@ -522,7 +522,7 @@ export async function createDemoVideo(
recordingPath, recordingPath,
durationMs, durationMs,
executionTimestamps, executionTimestamps,
speed = 4, speed = 5,
signal, signal,
} = options } = options
+1 -1
View File
@@ -924,7 +924,7 @@ const demoPath = await createDemoVideo({
recordingPath: recording.path, recordingPath: recording.path,
durationMs: recording.duration, durationMs: recording.duration,
executionTimestamps: recording.executionTimestamps, executionTimestamps: recording.executionTimestamps,
speed: 4, // optional, default 4x for idle sections speed: 5, // optional, default 5x for idle sections
// outputFile: './demo.mp4', // optional, defaults to recording-demo.mp4 // outputFile: './demo.mp4', // optional, defaults to recording-demo.mp4
}) })
console.log('Demo video:', demoPath) console.log('Demo video:', demoPath)