Files
maestro/src/executor/worker.ts
T

94 lines
3.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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);
});