docs(web): add process-level continuation supervisor
This commit is contained in:
29
nextTask.md
29
nextTask.md
@@ -64,6 +64,34 @@ node tools/web/run-wbv2-continuous.mjs --max-iterations 0 --commit --push
|
||||
入口,任务未推进、治理失败、构建失败或推送失败时立即停在原任务并保留日志。
|
||||
凭据必须使用已配置的 Git 凭据,不得写入命令行历史、URL、日志或仓库文件。
|
||||
|
||||
## 进程级不间断执行
|
||||
|
||||
聊天消息只能启动一次代理执行;要跨越终端、网络或会话边界持续运行,必须
|
||||
启动仓库内 supervisor。supervisor 每次调用现有 runner 一个任务迭代,成功
|
||||
交接后立即启动下一迭代,并在重新启动时从 `docs/EXECUTION_QUEUE.md` 恢复,
|
||||
不会依赖上一轮聊天上下文:
|
||||
|
||||
```bash
|
||||
node tools/web/run-wbv2-supervisor.mjs --max-iterations 0 --commit --push
|
||||
```
|
||||
|
||||
需要脱离终端运行时使用:
|
||||
|
||||
```bash
|
||||
nohup node tools/web/run-wbv2-supervisor.mjs --max-iterations 0 --commit --push \
|
||||
> tests/golden/WBV2/continuous-supervisor.stdout.log 2>&1 < /dev/null &
|
||||
```
|
||||
|
||||
supervisor 的连续契约:
|
||||
|
||||
- `--max-iterations 0` 表示不限任务轮数;每轮只执行当前 manifest 的一个 `nextTask`。
|
||||
- 每轮先运行 `print-task-context` 和 `check-task-context`,再调用 runner;成功后立即重读队列。
|
||||
- 每轮状态写入 `tests/golden/WBV2/continuous-supervisor.json`,详细输出写入 `continuous-supervisor.log`。
|
||||
- 使用锁文件防止多个 supervisor 同时改写队列;正常退出会清理锁文件。
|
||||
- 子进程失败、队列未前进、队列在失败后已经前进、推送失败或上下文不一致时立即保存 `blocked` checkpoint;不得盲目重试未知的状态变更。
|
||||
- supervisor 进程退出后,再次执行同一命令即可从当前队列恢复;不复制旧日志、不重放未知命令。
|
||||
- supervisor 只负责有效 handoff 的连续执行,不能绕过真实阻塞、治理门禁、源码编译失败或 provenance 规则。
|
||||
|
||||
## Copyable Resume Prompt
|
||||
|
||||
Use the following prompt when continuous execution is needed. It is the
|
||||
@@ -92,6 +120,7 @@ and (when authorized) repository push:
|
||||
13. 可使用仓库内 unattended runner 保持进程级连续执行:`node tools/web/run-wbv2-continuous.mjs --max-iterations 0 --commit --push`。它只执行任务卡声明的入口;`0` 表示不限轮数,`--max-iterations N` 用于受控运行,`--commit --push` 才允许自动提交/推送。任何入口失败、退出 0 但队列未前进、治理失败或 push 失败都立即停止并保留 `tests/golden/<task>/continuous-runner.log`,不得重试未知结果。
|
||||
14. 6900_IMPLEMENTATION_IRON_RULE:只按 capability family 和唯一行为批次实现全部 Blender 功能;不启动 Chromium/E2E/receipt/parity/save-reopen/determinism 验证,不写 completion overlay,不把维护 smoke 当作能力完成。若任务卡与本铁律冲突,以源码实现和本铁律为准,并将验证留到后续独立验证波次。
|
||||
15. UNIFIED_WASM_JSON_ABI_IRON_RULE:Worker 与 WASM 的新调用统一使用单一 C ABI `web_engine_call_json(handle, requestJson, resultOut)`;请求必须是 `{"cmd":"...","params":{...}}`,返回必须是 `{"ok":true,"cmd":"...","result":...}` 或包含 `code/error` 的失败 JSON,再由 Worker 解析为领域结果。`applyCommand`、快照、增量、撤销/重做、Depsgraph 等状态路径不得新增独立公开 WASM 函数;open/append/save/geometry 等仍未迁移的二进制专用 ABI 仅作为兼容边界保留,并不得扩散到新的状态调用。
|
||||
16. PROCESS_CONTINUITY_IRON_RULE:要实现跨会话不间断执行,必须使用 `run-wbv2-supervisor.mjs --max-iterations 0`;单次聊天执行完成后不得假定代理会在下一轮自动复活。supervisor 必须按任务原子迭代、锁队列、写 checkpoint、成功后立即重读 manifest;失败或状态未知时只能停在精确 checkpoint,不得自动重试或伪造队列前进。
|
||||
```
|
||||
|
||||
## Resume Command
|
||||
|
||||
148
tools/web/run-wbv2-supervisor.mjs
Normal file
148
tools/web/run-wbv2-supervisor.mjs
Normal file
@@ -0,0 +1,148 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const abs = (file) => path.join(root, file);
|
||||
const supervisorRoot = "tests/golden/WBV2";
|
||||
const supervisorLog = `${supervisorRoot}/continuous-supervisor.log`;
|
||||
const checkpointPath = `${supervisorRoot}/continuous-supervisor.json`;
|
||||
const lockPath = `${supervisorRoot}/continuous-supervisor.lock`;
|
||||
|
||||
const valueArg = (name, fallback) => {
|
||||
const index = process.argv.indexOf(name);
|
||||
return index >= 0 ? process.argv[index + 1] : fallback;
|
||||
};
|
||||
const maxIterations = Number(valueArg("--max-iterations", "0"));
|
||||
const pollMs = Number(valueArg("--poll-ms", "1000"));
|
||||
const commitEnabled = process.argv.includes("--commit");
|
||||
const pushEnabled = process.argv.includes("--push");
|
||||
if (!Number.isSafeInteger(maxIterations) || maxIterations < 0) throw new Error("--max-iterations must be a non-negative integer; 0 means unlimited");
|
||||
if (!Number.isSafeInteger(pollMs) || pollMs < 0) throw new Error("--poll-ms must be a non-negative integer");
|
||||
if (pushEnabled && !commitEnabled) throw new Error("--push requires --commit");
|
||||
|
||||
fs.mkdirSync(abs(supervisorRoot), { recursive: true });
|
||||
let lockHandle;
|
||||
const acquireLock = () => {
|
||||
try {
|
||||
return fs.openSync(abs(lockPath), "wx");
|
||||
}
|
||||
catch (error) {
|
||||
if (error?.code !== "EEXIST") throw error;
|
||||
try {
|
||||
const lock = JSON.parse(fs.readFileSync(abs(lockPath), "utf8"));
|
||||
if (Number.isSafeInteger(lock.pid)) {
|
||||
try { process.kill(lock.pid, 0); }
|
||||
catch (probeError) {
|
||||
if (probeError?.code === "ESRCH") {
|
||||
fs.unlinkSync(abs(lockPath));
|
||||
return fs.openSync(abs(lockPath), "wx");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
// A live or unreadable lock is treated as owned to avoid concurrent writes.
|
||||
}
|
||||
throw new Error(`another WBV2 supervisor appears to be running; lock=${lockPath}`);
|
||||
}
|
||||
};
|
||||
lockHandle = acquireLock();
|
||||
fs.writeFileSync(lockHandle, `${JSON.stringify({ pid: process.pid, startedAt: new Date().toISOString() })}\n`);
|
||||
|
||||
let lockReleased = false;
|
||||
const releaseLock = () => {
|
||||
if (lockReleased || lockHandle === undefined) return;
|
||||
lockReleased = true;
|
||||
fs.closeSync(lockHandle);
|
||||
try { fs.unlinkSync(abs(lockPath)); }
|
||||
catch (error) { if (error?.code !== "ENOENT") throw error; }
|
||||
};
|
||||
const stopFromSignal = (signal) => {
|
||||
try {
|
||||
writeCheckpoint({ state: "stopped", iteration: 0, reason: signal });
|
||||
appendLog(`stopped signal=${signal}`);
|
||||
releaseLock();
|
||||
}
|
||||
finally {
|
||||
process.exitCode = signal === "SIGINT" ? 130 : 143;
|
||||
}
|
||||
};
|
||||
process.once("SIGINT", () => stopFromSignal("SIGINT"));
|
||||
process.once("SIGTERM", () => stopFromSignal("SIGTERM"));
|
||||
|
||||
if (lockHandle === undefined) {
|
||||
throw new Error(`unable to acquire supervisor lock=${lockPath}`);
|
||||
}
|
||||
|
||||
function appendLog(text) {
|
||||
fs.appendFileSync(abs(supervisorLog), `[${new Date().toISOString()}] ${text}\n`);
|
||||
}
|
||||
function writeCheckpoint(payload) {
|
||||
fs.writeFileSync(abs(checkpointPath), `${JSON.stringify({ schemaVersion: 1, pid: process.pid, updatedAt: new Date().toISOString(), ...payload }, null, 2)}\n`);
|
||||
}
|
||||
const run = (command, args = []) => {
|
||||
const result = spawnSync(command, args, { cwd: root, encoding: "utf8", maxBuffer: 16 * 1024 * 1024 });
|
||||
return { code: result.status ?? 1, output: `${result.stdout ?? ""}${result.stderr ?? ""}` };
|
||||
};
|
||||
const currentTask = () => {
|
||||
const queue = fs.readFileSync(abs("docs/EXECUTION_QUEUE.md"), "utf8");
|
||||
const task = queue.match(/\| 当前任务 \| `(WBV2-P\d+-\d+)` \|/u)?.[1];
|
||||
const parentManifest = queue.match(/\| parent manifest \| `([^`]+)` \|/u)?.[1];
|
||||
if (!task || !parentManifest) throw new Error("queue pointer is incomplete");
|
||||
return { task, parentManifest };
|
||||
};
|
||||
const childArgs = ["tools/web/run-wbv2-continuous.mjs", "--max-iterations", "1"];
|
||||
if (commitEnabled) childArgs.push("--commit");
|
||||
if (pushEnabled) childArgs.push("--push");
|
||||
|
||||
appendLog(`started pid=${process.pid} maxIterations=${maxIterations} child=${childArgs.join(" ")}`);
|
||||
writeCheckpoint({ state: "started", iteration: 0, current: currentTask(), child: childArgs.join(" ") });
|
||||
|
||||
let iteration = 0;
|
||||
let exitCode = 0;
|
||||
try {
|
||||
while (maxIterations === 0 || iteration < maxIterations) {
|
||||
const before = currentTask();
|
||||
const print = run(process.execPath, ["tools/web/print-task-context.mjs"]);
|
||||
const check = run(process.execPath, ["tools/web/check-task-context.mjs"]);
|
||||
appendLog(`preflight task=${before.task} printExit=${print.code} checkExit=${check.code}`);
|
||||
if (print.code !== 0 || check.code !== 0) {
|
||||
writeCheckpoint({ state: "blocked", iteration, current: before, reason: "preflight_failed", printExit: print.code, checkExit: check.code });
|
||||
exitCode = 2;
|
||||
break;
|
||||
}
|
||||
writeCheckpoint({ state: "running", iteration: iteration + 1, before, child: childArgs.join(" ") });
|
||||
const child = run(process.execPath, childArgs);
|
||||
appendLog(`child task=${before.task} exit=${child.code}\n${child.output}`);
|
||||
const after = currentTask();
|
||||
if (child.code !== 0) {
|
||||
const reason = after.task === before.task ? "child_failed_same_task" : "child_failed_after_queue_advance";
|
||||
writeCheckpoint({ state: "blocked", iteration: iteration + 1, before, after, reason, childExit: child.code });
|
||||
exitCode = 3;
|
||||
break;
|
||||
}
|
||||
if (after.task === before.task) {
|
||||
writeCheckpoint({ state: "blocked", iteration: iteration + 1, before, after, reason: "child_exited_without_queue_advance", childExit: child.code });
|
||||
exitCode = 4;
|
||||
break;
|
||||
}
|
||||
iteration += 1;
|
||||
writeCheckpoint({ state: "handed_off", iteration, before, after });
|
||||
appendLog(`handoff iteration=${iteration} before=${before.task} after=${after.task}`);
|
||||
if (pollMs > 0) {
|
||||
const wait = new Int32Array(new SharedArrayBuffer(4));
|
||||
Atomics.wait(wait, 0, 0, pollMs);
|
||||
}
|
||||
}
|
||||
if (exitCode === 0) {
|
||||
const current = currentTask();
|
||||
writeCheckpoint({ state: maxIterations === 0 ? "running" : "complete", iteration, current });
|
||||
appendLog(`stopped state=${maxIterations === 0 ? "running" : "complete"} iteration=${iteration} current=${current.task}`);
|
||||
}
|
||||
}
|
||||
finally {
|
||||
releaseLock();
|
||||
}
|
||||
process.exitCode = exitCode;
|
||||
Reference in New Issue
Block a user