// Phase 3 多进程执行:worker 进程【入口】。 // // node worker.js (或 tsx worker.ts ) // // daemon 在 spawn 前写好 runs//job.json,本进程读它即可执行,【完全不碰 DB】。 // 一切进度/结果只经 protocol 的文件协议汇报: // - 起手 emit started(pid/worktree/branch/model) // - 周期 touchHeartbeat(daemon 据 mtime 判活) // - runPipeline 内部 emit failed|result + done(终态) // - 意外异常 / 收到 SIGTERM → 兜底 emit failed + done,然后退出 // // 顶层【不 import 任何 store/db】——worker 与 DB 完全隔离。 import { readJobSpec, appendOutbox, touchHeartbeat, HEARTBEAT_INTERVAL_MS, type OutboxPayload, } from './protocol.js'; import { pickModel } from './models.js'; import { runPipeline, realDeps } from './pipeline.js'; async function main(): Promise { const runId = process.argv[2]; if (!runId) { // 没 runId 没法定位 job.json / outbox,无处汇报——只能直接退出(非 0 让父进程可感知)。 process.stderr.write('worker: 缺少 runId 参数(用法:worker )\n'); process.exit(2); } const emit = (p: OutboxPayload): void => { appendOutbox(runId, p); }; const job = readJobSpec(runId); // 起手汇报:pid / worktree / branch / 实际执行模型 emit({ type: 'started', pid: process.pid, worktree: job.worktreeDir, branch: job.branch, model: pickModel(job.task, job.project, 'executor'), }); // 心跳:立即一次 + 周期刷(结束时清掉) touchHeartbeat(runId); const hb = setInterval(() => touchHeartbeat(runId), HEARTBEAT_INTERVAL_MS); // 心跳定时器不应拖住事件循环退出(正常路径我们显式 exit,这里只是兜底) hb.unref?.(); // SIGTERM=daemon 取消/超时:兜底报失败终态后干净退出(exit 0:已自报终态,不算崩溃) let signalled = false; process.on('SIGTERM', () => { if (signalled) return; signalled = true; clearInterval(hb); try { emit({ type: 'failed', error: 'worker 收到 SIGTERM(取消/超时)', transcriptRef: null, sessionId: null }); emit({ type: 'done' }); } catch { /* 汇报失败也要退出,别卡死 */ } process.exit(0); }); try { // runPipeline 内部已 emit failed|result + done;正常路径这里不再补发终态 await runPipeline(job, realDeps, emit); } catch (e) { // 意外异常(pipeline 之外或 emit 抛错等):兜底补一条 failed + done if (!signalled) { try { emit({ type: 'failed', error: (e as Error).message, transcriptRef: null, sessionId: null }); emit({ type: 'done' }); } catch { /* 已尽力汇报 */ } } } finally { clearInterval(hb); } if (!signalled) process.exit(0); } main().catch((e) => { // main 自身(如 readJobSpec 抛错)兜底:尽量写一条 failed,但若连 runId 都没有就只能干退。 const runId = process.argv[2]; if (runId) { try { appendOutbox(runId, { type: 'failed', error: (e as Error).message, transcriptRef: null, sessionId: null }); appendOutbox(runId, { type: 'done' }); } catch { /* 无处可报 */ } } else { process.stderr.write(`worker 致命错误:${(e as Error).message}\n`); } process.exit(1); });