phase3(worker): pipeline.ts 纯执行管线 + worker.ts 进程入口(不碰 DB,经 outbox 文件汇报)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
// Phase 3 多进程执行:worker 进程【入口】。
|
||||
//
|
||||
// node worker.js <runId> (或 tsx worker.ts <runId>)
|
||||
//
|
||||
// daemon 在 spawn 前写好 runs/<runId>/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<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?.();
|
||||
|
||||
// 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);
|
||||
});
|
||||
Reference in New Issue
Block a user