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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user