b7be524cdc
新增单份串行启动入口、首个正向运行杀进程恢复证据、逐候选审阅导出与账目统计;参考只读副本分数对照。456项免费测试通过,尚未启动三份真实验收。同步结构说明与当前工作状态。
260 lines
13 KiB
TypeScript
260 lines
13 KiB
TypeScript
import { execFileSync } from "node:child_process";
|
|
import { appendFileSync, existsSync, mkdirSync } from "node:fs";
|
|
import { join, resolve } from "node:path";
|
|
import { forwardAccounts, frozenConfig, jsonFile } from "../lib/forward.ts";
|
|
import { ofKind, readLedger } from "../lib/ledger.ts";
|
|
import { launch, sleep, waitForLedger, writeConfig } from "../lib/study.ts";
|
|
import { watchTimeline } from "../lib/timeline.ts";
|
|
|
|
// 任务书16:一次只启动一份;三次由父会话逐个执行。kill 只打首个正向运行,随后恢复同一研究一次。
|
|
// node eval/forward/run.ts <new|kill|resume> <研究编号> <绝对输出目录>
|
|
const [mode, name, output, ...extra] = process.argv.slice(2);
|
|
if (
|
|
!mode ||
|
|
!name ||
|
|
!output ||
|
|
extra.length ||
|
|
!["new", "kill", "resume"].includes(mode) ||
|
|
output !== resolve(output)
|
|
) {
|
|
throw new Error(
|
|
"用法:node eval/forward/run.ts <new|kill|resume> <研究编号> <绝对输出目录>",
|
|
);
|
|
}
|
|
const out = output;
|
|
const studyDir = join(out, "study");
|
|
if (mode === "resume") {
|
|
if (frozenConfig(readLedger(studyDir)).studyId !== name)
|
|
throw new Error("研究编号与账本不符");
|
|
} else {
|
|
if (existsSync(out))
|
|
throw new Error(`输出目录已存在,不覆盖现场:${out}`);
|
|
mkdirSync(out, { recursive: true });
|
|
}
|
|
const config =
|
|
mode === "resume"
|
|
? null
|
|
: writeConfig(
|
|
"eval/round-zero/research.json",
|
|
join(out, "research.json"),
|
|
name,
|
|
null,
|
|
);
|
|
const inspect = (container: string) =>
|
|
JSON.parse(
|
|
execFileSync("docker", ["inspect", container], {
|
|
encoding: "utf8",
|
|
}),
|
|
)[0] as {
|
|
Id: string;
|
|
Name: string;
|
|
Created: string;
|
|
State: {
|
|
Running: boolean;
|
|
StartedAt: string;
|
|
FinishedAt: string;
|
|
Status: string;
|
|
};
|
|
};
|
|
function observe(label: string) {
|
|
appendFileSync(
|
|
join(out, "machine.jsonl"),
|
|
`${JSON.stringify({
|
|
at: new Date().toISOString(),
|
|
label,
|
|
processes: execFileSync(
|
|
"ps",
|
|
["-eo", "pid,ppid,lstart,args"],
|
|
{ encoding: "utf8" },
|
|
),
|
|
containers: execFileSync(
|
|
"docker",
|
|
["ps", "--no-trunc"],
|
|
{ encoding: "utf8" },
|
|
),
|
|
})}\n`,
|
|
);
|
|
}
|
|
function start(path: string | null) {
|
|
observe(path === null ? "resume" : "start");
|
|
const study = launch(path, studyDir, {});
|
|
appendFileSync(
|
|
join(out, "launches.jsonl"),
|
|
`${JSON.stringify({ event: "start", pid: study.child.pid, at: new Date().toISOString(), config: path })}\n`,
|
|
);
|
|
const interrupt = () => study.child.kill("SIGINT");
|
|
process.on("SIGINT", interrupt);
|
|
study.done.then((end) => {
|
|
process.off("SIGINT", interrupt);
|
|
appendFileSync(
|
|
join(out, "launches.jsonl"),
|
|
`${JSON.stringify({ event: "end", pid: study.child.pid, at: new Date().toISOString(), ...end })}\n`,
|
|
);
|
|
});
|
|
return study;
|
|
}
|
|
let active = start(config);
|
|
try {
|
|
await waitForLedger(studyDir, active.child);
|
|
let watching = watchTimeline(
|
|
studyDir,
|
|
active.child,
|
|
join(out, "timeline.jsonl"),
|
|
);
|
|
if (mode === "kill") {
|
|
let hit = false;
|
|
while (
|
|
active.child.exitCode === null &&
|
|
active.child.signalCode === null
|
|
) {
|
|
const records = readLedger(studyDir);
|
|
const plan = ofKind(records, "runPlanned").find(
|
|
(r) =>
|
|
r.content.purpose === "forward" &&
|
|
r.content.round === 1,
|
|
);
|
|
if (plan !== undefined) {
|
|
const run = plan.content.run;
|
|
if (
|
|
ofKind(records, "runResult").some(
|
|
(r) => r.content.run === run,
|
|
)
|
|
)
|
|
throw new Error(
|
|
"首个正向运行已经结束,未打中;保留现场,不改杀别的候选",
|
|
);
|
|
const started = ofKind(
|
|
records,
|
|
"runStarted",
|
|
).find((r) => r.content.run === run);
|
|
if (started !== undefined) {
|
|
// 容器名是正式入口记录在结构文档里的约定,不 import flow 内部文件。
|
|
const container = `inforsi-${name}-${run}`;
|
|
const before = inspect(container);
|
|
jsonFile(
|
|
join(
|
|
out,
|
|
"container-before.json",
|
|
),
|
|
before,
|
|
);
|
|
if (!before.State.Running)
|
|
throw new Error(
|
|
"首个正向容器已不在运行,未打中",
|
|
);
|
|
if (
|
|
ofKind(
|
|
readLedger(studyDir),
|
|
"runResult",
|
|
).some(
|
|
(r) =>
|
|
r.content
|
|
.run ===
|
|
run,
|
|
)
|
|
)
|
|
throw new Error(
|
|
"杀前已有结果,未打中",
|
|
);
|
|
jsonFile(join(out, "kill.json"), {
|
|
run,
|
|
candidate: plan.content.object,
|
|
planSeq: plan.seq,
|
|
startSeq: started.seq,
|
|
beforeKillSeq:
|
|
records.at(-1)!.seq,
|
|
pid: active.child.pid,
|
|
at: new Date().toISOString(),
|
|
container,
|
|
});
|
|
if (!active.child.kill("SIGKILL"))
|
|
throw new Error(
|
|
"未发出 SIGKILL",
|
|
);
|
|
const killed = await active.done;
|
|
await watching;
|
|
if (killed.signal !== "SIGKILL")
|
|
throw new Error(
|
|
"控制进程不是以 SIGKILL 结束",
|
|
);
|
|
const atKill = readLedger(studyDir);
|
|
jsonFile(
|
|
join(
|
|
out,
|
|
"ledger-at-kill.json",
|
|
),
|
|
atKill,
|
|
);
|
|
if (
|
|
ofKind(
|
|
atKill,
|
|
"runResult",
|
|
).some(
|
|
(r) =>
|
|
r.content
|
|
.run ===
|
|
run,
|
|
)
|
|
)
|
|
throw new Error(
|
|
"杀后核对发现目标运行已经写结果,未打中;保留现场",
|
|
);
|
|
const afterKill = inspect(container);
|
|
jsonFile(
|
|
join(
|
|
out,
|
|
"container-after-kill.json",
|
|
),
|
|
afterKill,
|
|
);
|
|
observe("after-kill");
|
|
active = start(null);
|
|
const afterResume = inspect(container);
|
|
jsonFile(
|
|
join(
|
|
out,
|
|
"container-after-resume.json",
|
|
),
|
|
afterResume,
|
|
);
|
|
watching = watchTimeline(
|
|
studyDir,
|
|
active.child,
|
|
join(out, "timeline.jsonl"),
|
|
);
|
|
hit = true;
|
|
break;
|
|
}
|
|
}
|
|
await sleep(100);
|
|
}
|
|
if (!hit)
|
|
throw new Error(
|
|
`没有打中首个正向运行:${JSON.stringify(await active.done)}`,
|
|
);
|
|
}
|
|
await watching;
|
|
const end = await active.done;
|
|
jsonFile(join(out, "outcome.json"), end);
|
|
observe("end");
|
|
const accounts = forwardAccounts(readLedger(studyDir));
|
|
jsonFile(join(out, "accounts.json"), accounts);
|
|
// 杀进程报告由 stats 读原始容器观察和账本重算;即使外部服务失败后人工恢复,也不覆盖原杀进程证据。
|
|
if (end.code !== 0 || !Object.values(accounts.checks).every(Boolean)) {
|
|
throw new Error(
|
|
`没有完成采纳验收,保留现场;查明原因后才能手动 resume:${JSON.stringify(end)}`,
|
|
);
|
|
}
|
|
console.log(
|
|
`已到采纳后停点:${studyDir}。仍需 export、stats 和独立质量审阅。`,
|
|
);
|
|
} finally {
|
|
if (
|
|
active.child.exitCode === null &&
|
|
active.child.signalCode === null
|
|
) {
|
|
active.child.kill("SIGINT");
|
|
await active.done;
|
|
}
|
|
}
|