fix(discord): engine tests stop in finally; a turn pi never started stops pi instead of guessing (#1509)

Every engine test stops its engine in finally, and commands() tolerates a
log that doesn't exist yet, so a failed assertion no longer leaks a fake pi
and hangs the six-package union. A turn that failed client-side stays at
the front of the queue and holds the next prompt. If pi has sent no
agent_start ABORT_GRACE_MS (30 s) after the failure, the engine marks
itself wedged, fails held prompts with engine-wedged, refuses new ones with
engine-down, and stops pi. The exit reaches onExit, the connector exits 1,
and the unit restarts it. Pi's events carry no prompt id, so R1's approach,
dropping the turn and sending on, let a late run answer the next prompt.
Rocko rejected R1 and approved R2.

Tests: engine 17/17 (R1 fails 4, HEAD fails 5). Union 408/408 and the eight
suites green on the committed index. Record:
agents/darkwing/work/discord-engine-busy/.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-09-26 15:50:37 -05:00
co-authored by Claude Opus 5.5
parent f55b94e866
commit 3edb15eb96
11 changed files with 1843 additions and 102 deletions
+21 -3
View File
@@ -8,9 +8,13 @@
// then a second turn that answers "read <n> file(s)"
// "toolonly" a run whose only turn calls a tool and never answers
// "retry" an agent_end with willRetry, then the real answer
// "mute" accept the prompt and emit nothing, staying idle
// "late <ms>" ignore abort; after <ms> emit a tool pair and a tool turn,
// then answer "late reply", like a run that outlives its
// client-side timeout
// "stall <ms>" accept the prompt, then read nothing for <ms> (pi stuck
// before agent_start); then run it, answering "echo:
// stalled", and only then read what came in meanwhile
// anything else answer "echo: <text>" immediately
// A prompt received while busy without streamingBehavior is refused, as pi
// does. A prompt with streamingBehavior followUp is folded into the running
@@ -30,6 +34,7 @@ function assistant(text, stopReason = "stop") {
}
function run(text) {
if (text === "mute") return;
busy = true;
out({ type: "agent_start" });
out({ type: "turn_start" });
@@ -109,11 +114,16 @@ function run(text) {
let current = null;
let buffer = "";
let stalled = false;
process.stdin.setEncoding("utf8");
process.stdin.on("data", (chunk) => {
buffer += chunk;
drain();
});
function drain() {
let idx;
while ((idx = buffer.indexOf("\n")) !== -1) {
while (!stalled && (idx = buffer.indexOf("\n")) !== -1) {
const line = buffer.slice(0, idx);
buffer = buffer.slice(idx + 1);
if (!line) continue;
@@ -125,7 +135,15 @@ process.stdin.on("data", (chunk) => {
continue;
}
out({ id: cmd.id, type: "response", command: "prompt", success: true });
if (busy) queue.push(cmd.message);
const sm = /^stall (\d+)$/.exec(cmd.message);
if (sm) {
stalled = true;
setTimeout(() => {
run("stalled");
stalled = false;
drain();
}, Number(sm[1]));
} else if (busy) queue.push(cmd.message);
else run(cmd.message);
} else if (cmd.type === "abort") {
out({ id: cmd.id, type: "response", command: "abort", success: true });
@@ -139,5 +157,5 @@ process.stdin.on("data", (chunk) => {
out({ id: cmd.id, type: "response", command: cmd.type, success: true, data: {} });
}
}
});
}
process.stdin.on("end", () => process.exit(0));