104 lines
3.6 KiB
TypeScript
104 lines
3.6 KiB
TypeScript
/**
|
|
* 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}`);
|
|
}
|
|
}
|