diff options
| author | Adam Malczewski <[email protected]> | 2026-10-21 15:36:54 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-10-21 15:36:54 +0900 |
| commit | ac43e92d174ed1f081c387944e9044643ee9efc2 (patch) | |
| tree | 3110ac7c1fa722ad6c092200ea02aead5384b5d3 /src/app | |
| parent | e9786bf1dbf258b68f793870d02378c390939d3d (diff) | |
| download | dispatch-web-ac43e92d174ed1f081c387944e9044643ee9efc2.tar.gz dispatch-web-ac43e92d174ed1f081c387944e9044643ee9efc2.zip | |
fix(review-followup-fee): recover omitted chat payloads through history
Diffstat (limited to 'src/app')
| -rw-r--r-- | src/app/store.test.ts | 53 | ||||
| -rw-r--r-- | src/app/views/ChatPane.svelte | 1 | ||||
| -rw-r--r-- | src/app/ws-handlers.ts | 7 |
3 files changed, 59 insertions, 2 deletions
diff --git a/src/app/store.test.ts b/src/app/store.test.ts index 40a0fa7..33f93bb 100644 --- a/src/app/store.test.ts +++ b/src/app/store.test.ts @@ -3076,3 +3076,56 @@ describe("compaction queued acknowledgement routing", () => { store.dispose(); }); }); + +it("routes omitted replay only to the matching watch/tab without WS resend or an error banner", async () => { + const ws = fakeSocket(); + const store = createAppStore({ + socketFactory: () => ws, + fetchImpl: fakeFetchImpl(), + indexedDB: new IDBFactory(), + localStorage: createFakeStorage(), + chatFlushScheduler: null, + }); + try { + ws.resolveOpen(); + const tab = store.activeChat; + const watch = store.watchConversation("omitted-watch"); + const sentBefore = ws.sent.length; + ws.feedServerMessage({ + type: "chat.resync-required", + conversationId: "unknown", + turnId: "unknown-turn", + reason: "replay-limit", + }); + expect(watch.recovering).toBe(false); + expect(tab.recovering).toBe(false); + ws.feedServerMessage({ + type: "chat.resync-required", + conversationId: "omitted-watch", + turnId: "t1", + reason: "replay-limit", + }); + expect(watch.recovering).toBe(true); + expect(watch.generating).toBe(true); + expect(tab.recovering).toBe(false); + expect(watch.error).toBeNull(); + expect(tab.error).toBeNull(); + expect(store.fatalError).toBeNull(); + expect(ws.sent).toHaveLength(sentBefore); + ws.feedServerMessage({ + type: "chat.delta", + event: { + type: "turn-ended", + conversationId: "omitted-watch", + turnId: "t1", + reason: "aborted", + }, + }); + await vi.waitFor(() => expect(watch.pendingSync).toBe(false)); + expect(watch.recovering).toBe(false); + expect(watch.generating).toBe(false); + expect(ws.sent).toHaveLength(sentBefore); + } finally { + store.dispose(); + } +}); diff --git a/src/app/views/ChatPane.svelte b/src/app/views/ChatPane.svelte index d1630af..62b9344 100644 --- a/src/app/views/ChatPane.svelte +++ b/src/app/views/ChatPane.svelte @@ -80,6 +80,7 @@ {#key store.activeConversationId} <ChatView chunks={store.activeChat.chunks} + recovering={store.activeChat.recovering} turnMetrics={store.activeChat.turnMetrics} hasEarlier={store.activeChat.hasEarlier} onShowEarlier={handleShowEarlier} diff --git a/src/app/ws-handlers.ts b/src/app/ws-handlers.ts index aa1e03e..b9adb30 100644 --- a/src/app/ws-handlers.ts +++ b/src/app/ws-handlers.ts @@ -2,6 +2,7 @@ import type { ChatDeltaMessage, ChatErrorMessage, ChatQueuedMessage, + ChatResyncRequiredMessage, ConversationCompactedMessage, ConversationOpenMessage, ConversationStatusChangedMessage, @@ -53,7 +54,9 @@ export function createWsHandlers(deps: { layoutStore, tileLayoutStore, } = deps; - function handleChatMessage(msg: ChatDeltaMessage | ChatErrorMessage | ChatQueuedMessage): void { + function handleChatMessage( + msg: ChatDeltaMessage | ChatErrorMessage | ChatQueuedMessage | ChatResyncRequiredMessage, + ): void { let targetId: string | undefined; if (msg.type === "chat.delta") { targetId = msg.event.conversationId; @@ -69,7 +72,7 @@ export function createWsHandlers(deps: { } } - if (msg.type === "chat.queued") return; + if (msg.type === "chat.queued" || msg.type === "chat.resync-required") return; // fallback: try all stores (chat.error without conversationId) for (const store of registry.chatStores.values()) { |
