/** * recording-flush.ts * * Called at task end to: * 1. Flush any buffered recorder actions to recordings/{recordTo}.json. * 2. If recordTo ends with ".next", compile a candidate patch script at * scripts/{baseName}.next.js using the source script's description and * params as hints (self-healing patch staging). * * This helper is intentionally non-throwing — all errors are logged and * swallowed so a flush bug cannot crash the agent loop. */ import { readFileSync, writeFileSync, renameSync, existsSync, mkdirSync } from 'fs'; import { join, dirname } from 'path'; import { recorder } from '../engine/browser-recorder.js'; import { parseScript, serializeScript } from './frontmatter.js'; import { compileScriptBody } from './script-compiler.js'; import { resolveUserSubdir } from './paths.js'; import { logger } from '../logger.js'; import type { RecordedAction } from '../engine/browser-recorder.js'; export function flushAndStageRecording( taskId: string, ownerId: string | undefined, userFolderRoot: string, ): void { if (!ownerId) return; const recordTo = recorder.recordTo(taskId); if (!recordTo) return; const flushedPath = recorder.flush(taskId, userFolderRoot, ownerId); if (!flushedPath) return; if (!recordTo.endsWith('.next')) { logger.debug(`[recording] flushed regular recording: ${recordTo}.json`); return; } // Self-healing patch: compile a candidate .next.js from the flushed trace. const baseName = recordTo.slice(0, -'.next'.length); try { const sourceScriptPath = resolveUserSubdir( userFolderRoot, ownerId, 'browser-macros', `${baseName}.js`, ); if (!existsSync(sourceScriptPath)) { logger.warn( `[recording] cannot stage patch — source script ${baseName}.js missing`, ); return; } const sourceText = readFileSync(sourceScriptPath, 'utf-8'); const sourceParsed = parseScript(sourceText); const recordingPayload = JSON.parse(readFileSync(flushedPath, 'utf-8')) as { actions: RecordedAction[]; }; // Compile just the body (we build frontmatter ourselves to control timestamps) const { body, paramSpecs } = compileScriptBody({ recording: recordingPayload.actions, description: sourceParsed.frontmatter.description, sessionProfileId: sourceParsed.frontmatter.sessionProfileId, // We don't know which fill values map to which params after a recovery session. // Pass no paramHints so the patch contains literal values; the user will // manually re-parameterize during diff review. paramHints: [], recordingSource: `${recordTo}.json`, }); const now = new Date().toISOString(); const patched = serializeScript({ frontmatter: { description: sourceParsed.frontmatter.description, params: paramSpecs, sessionProfileId: sourceParsed.frontmatter.sessionProfileId, recordingSource: `${recordTo}.json`, // Preserve original createdAt; stamp fresh updatedAt. createdAt: sourceParsed.frontmatter.createdAt ?? now, updatedAt: now, }, body, }); const targetPath = resolveUserSubdir( userFolderRoot, ownerId, 'browser-macros', `${baseName}.next.js`, ); const tmpPath = `${targetPath}.tmp-${process.pid}-${Date.now()}`; mkdirSync(dirname(targetPath), { recursive: true }); writeFileSync(tmpPath, patched, { encoding: 'utf-8', mode: 0o600 }); renameSync(tmpPath, targetPath); logger.info(`[user-folder] staged self-healing patch: browser-macros/${baseName}.next.js`); } catch (e) { logger.warn(`[recording] failed to stage patch: ${(e as Error).message}`); } }