Files
stack/agents/darkwing/work/cohort-k1/build.patch
T
jason.woltjeandClaude Opus 5.5 7478ee2c02 docs(conversation): row 46 cohort K1/K3/K10 diagnosis, receipts and candidate packet (darkwing)
The ignoreTerm tool child was TERMed before Node installed its handler
when the claim store sits on a fast disk. Candidate fixes the fixture;
the patch itself is not committed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-09 08:11:48 -05:00

68 lines
3.7 KiB
Diff

diff --git a/packages/conversation/tests/fake-pi.mjs b/packages/conversation/tests/fake-pi.mjs
index b3014f86..a78dfebd 100644
--- a/packages/conversation/tests/fake-pi.mjs
+++ b/packages/conversation/tests/fake-pi.mjs
@@ -615,14 +615,34 @@ const children = [];
// K2); `forkLoop` forks every 5 ms (K12); `ignoreTerm` survives SIGTERM, so
// only the kill phase ends it (K3, K10, K11). With `pidLog`, the fork loop
// appends each child's pid and a `term` line when it gets SIGTERM (K12).
+//
+// It resolves only once the child writes its ready byte, which it does after
+// installing its TERM handler. Node takes 15 to 80 ms to get there, and a
+// force stop can send TERM sooner (12 ms when the claim store is on a fast
+// disk). A TERM before the handler kills a child that is meant to ignore it
+// (#1528).
function spawnChild({ setsid = false, forkLoop = false, ignoreTerm = false, pidLog = null } = {}) {
const note = pidLog ? `const note=(s)=>require('node:fs').appendFileSync(${JSON.stringify(pidLog)},s+'\\n');` : "const note=()=>{};";
- const code = note + (ignoreTerm ? "process.on('SIGTERM',()=>note('term'));" : "") + (forkLoop
+ const code = note + (ignoreTerm ? "process.on('SIGTERM',()=>note('term'));" : "") + "process.stdout.write('r');" + (forkLoop
? "const {spawn}=require('node:child_process');setInterval(()=>{try{const c=spawn('sleep',['1000'],{stdio:'ignore'});if(c.pid)note(String(c.pid))}catch{}},5);setInterval(()=>{},1e9)"
: "setInterval(()=>{},1e9)");
- const child = spawn(process.execPath, ["-e", code], { stdio: "ignore", detached: setsid });
+ const child = spawn(process.execPath, ["-e", code], { stdio: ["ignore", "pipe", "ignore"], detached: setsid });
children.push(child.pid);
- return child.pid;
+ return new Promise((resolve, reject) => {
+ const fail = (why) => {
+ clearTimeout(timer);
+ reject(new Error(`tool child ${child.pid} ${why} before it was ready`));
+ };
+ const timer = setTimeout(() => fail("took 10 s"), 10000);
+ child.once("error", (err) => fail(err.code ?? err.message));
+ child.once("exit", (code, signal) => fail(`exited (${signal ?? code})`));
+ child.stdout.once("data", () => {
+ clearTimeout(timer);
+ child.removeAllListeners("exit");
+ child.stdout.destroy();
+ resolve(child.pid);
+ });
+ });
}
// K13: a member writes its own pid to another cgroup's cgroup.procs.
@@ -671,7 +691,7 @@ async function main() {
drop: () => fake.dropResponse(req.type, req.n ?? 1),
extension: () => void fake.extensionPrompt(req.args ?? {}),
state: () => ({ streaming: fake.streaming, runs: fake.runs.length, commands: fake.commands, pid: process.pid, children, appends: fake.appends }),
- child: () => ({ pid: spawnChild(req.args ?? {}) }),
+ child: () => spawnChild(req.args ?? {}).then((pid) => ({ pid })),
escape: () => escape(req.target),
cgroup: () => readFileSync(`/proc/${req.pid ?? process.pid}/cgroup`, "utf8"),
waitPaused: () => fake.waitPaused(req.point),
@@ -679,12 +699,13 @@ async function main() {
stall: () => void process.stdin.pause(),
};
if (!ops[req.op]) return sock.write(encodeLine({ id: req.id, ok: false, error: `unknown op ${req.op}` }));
+ const failed = (err) => sock.write(encodeLine({ id: req.id, ok: false, error: String(err.message) }));
try {
const out = ops[req.op]();
- if (out && typeof out.then === "function") out.then(reply);
+ if (out && typeof out.then === "function") out.then(reply, failed);
else reply(out ?? null);
} catch (err) {
- sock.write(encodeLine({ id: req.id, ok: false, error: String(err.message) }));
+ failed(err);
}
});
sock.on("data", (c) => splitter.push(c));