fix(app): tolerate missing message parents (#37918)
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
This commit is contained in:
parent
5542415b64
commit
cc3615b535
2 changed files with 55 additions and 1 deletions
|
|
@ -200,6 +200,49 @@ describe("server session", () => {
|
||||||
expect(store.history.more("child")).toBe(true)
|
expect(store.history.more("child")).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("keeps assistant history when its deleted parent cannot be backfilled", async () => {
|
||||||
|
const missing = Promise.withResolvers<SingleMessageResponse>()
|
||||||
|
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<SingleMessageResponse>()
|
||||||
|
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 () => {
|
test("does not let an optimistic user suppress initial root backfill", async () => {
|
||||||
const user = userMessage("message-1")
|
const user = userMessage("message-1")
|
||||||
const part = textPart(user.id)
|
const part = textPart(user.id)
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,7 @@ function reconcileFetched<T extends { id: string }>(
|
||||||
options: {
|
options: {
|
||||||
touched?: ReadonlySet<string>
|
touched?: ReadonlySet<string>
|
||||||
retained?: ReadonlySet<string>
|
retained?: ReadonlySet<string>
|
||||||
|
removed?: ReadonlySet<string>
|
||||||
preserveUnfetched?: boolean | ((item: T) => boolean)
|
preserveUnfetched?: boolean | ((item: T) => boolean)
|
||||||
} = {},
|
} = {},
|
||||||
) {
|
) {
|
||||||
|
|
@ -132,6 +133,7 @@ function reconcileFetched<T extends { id: string }>(
|
||||||
if (item) result.set(id, item)
|
if (item) result.set(id, item)
|
||||||
if (!item) result.delete(id)
|
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))
|
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] ?? [], {
|
const messages = reconcileFetched(merged.session, data.message[sessionID] ?? [], {
|
||||||
touched: touchedMessages,
|
touched: touchedMessages,
|
||||||
retained: load?.retainedMessages,
|
retained: load?.retainedMessages,
|
||||||
|
removed: load?.removedMessages,
|
||||||
preserveUnfetched,
|
preserveUnfetched,
|
||||||
})
|
})
|
||||||
batch(() => {
|
batch(() => {
|
||||||
|
|
@ -645,7 +648,15 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
|
||||||
if (generations.get(sessionID) !== active) break
|
if (generations.get(sessionID) !== active) break
|
||||||
const parent = await fetchMessage(sessionID, parentID, () =>
|
const parent = await fetchMessage(sessionID, parentID, () =>
|
||||||
resetMessageLoad(sessionID, load, messageLoadBaseline(load, 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}`)
|
if (parent.message.role !== "user") throw new Error(`Assistant parent is not a user message: ${parentID}`)
|
||||||
parents.push(parent)
|
parents.push(parent)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue