fix: pump wake turns double-appended their steer (Wave B regression)

A pump-fired wake turn carries its steer in the input appends, but the
pump only shifts its queue entry AFTER the turn registers - so the
handler's claimSteers popped that very steer too. The record-side
steerDelivered filter checks the PRE-write-ahead snapshot and cannot
see the carried copy: the marker persisted twice with the same
steer_<jobId> id, two view messages shared one React key, and the
client crashed on delivery (live: steer_job_muhlokbf_84e9373c).

This exact bug was known and fixed 2026-09-18 (the old claimSteers
received the incoming body messages and finalized-without-appending
anything the request already carried). Wave B's turn-root rewrite kept
only the record-side filter and lost the carried-side half.

The fix restores the carried filter: a steer whose id the input already
carries is claimed-and-finalized but never appended a second time.

Regression pin in chat-flow.test.ts drives the REAL pump end to end
(deliverJobResult with a real job record -> waitForIdle -> scripted
wake turn) and asserts exactly one copy + the wake response. Two test
lessons baked into comments: chatIdOf resolves a job steer's chat
through the JOB STORE (a synthetic jobId with no record silently
no-ops the pump), and deliverJobResult resolves at CLAIM time, not at
turn completion (the record assertion waits for the wake response).

The live-corrupted record (chat_muhlcltz) was deduped by hand.
This commit is contained in:
amosblomqvist
2026-09-26 03:14:40 +03:00
parent 8e097752da
commit 43ef004ab0
2 changed files with 67 additions and 2 deletions
+11 -1
View File
@@ -215,7 +215,17 @@ export async function handleChatInput(
// this turn owns the registry and the write-ahead persist below makes the
// claim durable. Steers already in the transcript (a wake raced in) are
// finalized by the claim itself — the append filter keeps exactly one copy.
const steers = claimSteers(chatId).filter((s) => !steerDelivered(record.messages, s.id));
// THE CARRIED FILTER (restored 2026-09-26, the Wave B regression of the
// 2026-09-18 duplicate-key bug): a pump-fired wake turn ALREADY CARRIES
// its steer in `appends` — and the pump only shifts its queue entry AFTER
// the turn registers, so the claim above pops that very steer too. The
// old record-side-only filter couldn't see the carried copy (it checks
// the PRE-write-ahead snapshot), and the marker persisted twice with the
// same `steer_<jobId>` id — two view messages, one React key, crash.
const carried = input.type === "steer" ? input.steer.id : undefined;
const steers = claimSteers(chatId).filter(
(s) => s.id !== carried && !steerDelivered(record.messages, s.id),
);
// Write-ahead persistence: the input's native appends go on disk BEFORE
// the turn starts — the learner's answer is durable the instant the