From cc3615b535d05e0301ee83a5f2e207432e58a595 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Mon, 20 Jul 2026 12:20:41 +0000 Subject: [PATCH] fix(app): tolerate missing message parents (#37918) Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com> --- .../app/src/context/server-session.test.ts | 43 +++++++++++++++++++ packages/app/src/context/server-session.ts | 13 +++++- 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/packages/app/src/context/server-session.test.ts b/packages/app/src/context/server-session.test.ts index 38f7ff71e5..5554928075 100644 --- a/packages/app/src/context/server-session.test.ts +++ b/packages/app/src/context/server-session.test.ts @@ -200,6 +200,49 @@ describe("server session", () => { expect(store.history.more("child")).toBe(true) }) + test("keeps assistant history when its deleted parent cannot be backfilled", async () => { + const missing = Promise.withResolvers() + const assistant = assistantMessage("message-2", "message-missing") + const client = rootMessageClient([response([{ info: assistant, parts: [] }], "older")], [missing.promise]) + const store = createServerSession(client) + const loading = store.sync("child") + await client.rootRequested(1) + + missing.reject(new Error("Message not found: message-missing", { cause: { status: 404 } })) + await loading + + expect(client.rootRequests).toEqual([{ sessionID: "child", messageID: "message-missing" }]) + expect(store.data.message.child).toEqual([assistant]) + expect(store.history.more("child")).toBe(true) + }) + + test("drops a cached parent when a forced refresh confirms it was deleted", async () => { + const missing = Promise.withResolvers() + const parent = userMessage("message-1") + const part = textPart(parent.id) + const assistant = assistantMessage("message-2", parent.id) + const client = rootMessageClient( + [ + response([ + { info: parent, parts: [part] }, + { info: assistant, parts: [] }, + ]), + response([{ info: assistant, parts: [] }], "older"), + ], + [missing.promise], + ) + const store = createServerSession(client) + await store.sync("child") + const loading = store.sync("child", { force: true }) + await client.rootRequested(1) + + missing.reject(new Error(`Message not found: ${parent.id}`, { cause: { status: 404 } })) + await loading + + expect(store.data.message.child).toEqual([assistant]) + expect(store.data.part[parent.id]).toBeUndefined() + }) + test("does not let an optimistic user suppress initial root backfill", async () => { const user = userMessage("message-1") const part = textPart(user.id) diff --git a/packages/app/src/context/server-session.ts b/packages/app/src/context/server-session.ts index a440aab38f..5a892f7915 100644 --- a/packages/app/src/context/server-session.ts +++ b/packages/app/src/context/server-session.ts @@ -110,6 +110,7 @@ function reconcileFetched( options: { touched?: ReadonlySet retained?: ReadonlySet + removed?: ReadonlySet preserveUnfetched?: boolean | ((item: T) => boolean) } = {}, ) { @@ -132,6 +133,7 @@ function reconcileFetched( if (item) result.set(id, item) if (!item) result.delete(id) } + for (const id of options.removed ?? emptyIDs) result.delete(id) return [...result.values()].sort((a, b) => cmp(a.id, b.id)) } @@ -577,6 +579,7 @@ export function createServerSession(client: OpencodeClient, options?: { retry?: const messages = reconcileFetched(merged.session, data.message[sessionID] ?? [], { touched: touchedMessages, retained: load?.retainedMessages, + removed: load?.removedMessages, preserveUnfetched, }) batch(() => { @@ -645,7 +648,15 @@ export function createServerSession(client: OpencodeClient, options?: { retry?: if (generations.get(sessionID) !== active) break const parent = await fetchMessage(sessionID, parentID, () => resetMessageLoad(sessionID, load, messageLoadBaseline(load, parentID)), - ) + ).catch((error) => { + const cause = error instanceof Error && typeof error.cause === "object" ? error.cause : undefined + if (cause && "status" in cause && cause.status === 404) { + load.removedMessages.add(parentID) + return + } + throw error + }) + if (!parent) continue if (parent.message.role !== "user") throw new Error(`Assistant parent is not a user message: ${parentID}`) parents.push(parent) }