phase3(worker): pipeline.ts 纯执行管线 + worker.ts 进程入口(不碰 DB,经 outbox 文件汇报)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-13 12:56:50 +08:00
parent 007dffac39
commit d8f8b5da1e
3 changed files with 396 additions and 0 deletions
+93
View File
@@ -0,0 +1,93 @@
// Phase 3 多进程执行:worker 进程【入口】。
//
// node worker.js <runId> (或 tsx worker.ts <runId>
//
// daemon 在 spawn 前写好 runs/<runId>/job.json,本进程读它即可执行,【完全不碰 DB】。
// 一切进度/结果只经 protocol 的文件协议汇报:
// - 起手 emit startedpid/worktree/branch/model
// - 周期 touchHeartbeatdaemon 据 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<void> {
const runId = process.argv[2];
if (!runId) {
// 没 runId 没法定位 job.json / outbox,无处汇报——只能直接退出(非 0 让父进程可感知)。
process.stderr.write('worker: 缺少 runId 参数(用法:worker <runId>\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?.();
// SIGTERMdaemon 取消/超时:兜底报失败终态后干净退出(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);
});