diff --git a/web/src/sessionReducer.test.ts b/web/src/sessionReducer.test.ts index 1871ebc..5e29730 100644 --- a/web/src/sessionReducer.test.ts +++ b/web/src/sessionReducer.test.ts @@ -296,18 +296,17 @@ describe("history boundary replacement", () => { expect(next.replacementEvents.get(4)).toEqual(replacement); }); - it("rejects invalid replacement targets", () => { + it("skips replacements whose target seq is absent", () => { const state = makeState(); - expect(() => - replaceHistoryBoundary( - state, - { - type: "turn/stop", - history_boundary: { anchor: "a", kind: "agent_turn" }, - }, - 4, - ), - ).toThrow("matched 0"); + const next = replaceHistoryBoundary( + state, + { + type: "turn/stop", + history_boundary: { anchor: "a", kind: "agent_turn" }, + }, + 4, + ); + expect(next).toBe(state); }); it("rejects mismatched canonical identity", () => { diff --git a/web/src/sessionReducer.ts b/web/src/sessionReducer.ts index a3cb9f9..24b271a 100644 --- a/web/src/sessionReducer.ts +++ b/web/src/sessionReducer.ts @@ -858,6 +858,16 @@ export function replaceHistoryBoundary( (message.seq === seq || (Array.isArray(message.seq) && message.seq.includes(seq))), ); + if (matches.length === 0) { + // A replacement can reference a message this client doesn't have: a live + // boundary racing a still-streaming history load, or a boundary persisted + // under an older seq numbering. Dropping it degrades one message's + // replacement; throwing would wedge the whole session view on every load. + console.error( + `History replacement matched 0 messages at seq ${seq}; skipping`, + ); + return s; + } if (matches.length !== 1) throw new Error( `History replacement matched ${matches.length} messages at seq ${seq}`,