Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 10 additions & 11 deletions web/src/sessionReducer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
10 changes: 10 additions & 0 deletions web/src/sessionReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`,
Expand Down