summaryrefslogtreecommitdiffhomepage
path: root/src/app
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-10-21 14:52:42 +0900
committerAdam Malczewski <[email protected]>2026-10-21 14:52:42 +0900
commite9786bf1dbf258b68f793870d02378c390939d3d (patch)
tree4cecca2c569ce374aabf071cfd33976c936f850e /src/app
parent0bdb04b0aa0c0500e68e2e985b1a2880730ad7e8 (diff)
downloaddispatch-web-e9786bf1dbf258b68f793870d02378c390939d3d.tar.gz
dispatch-web-e9786bf1dbf258b68f793870d02378c390939d3d.zip
fix(review-followup-fed): reconcile compaction-queued send acknowledgements
Diffstat (limited to 'src/app')
-rw-r--r--src/app/chat-registry.svelte.ts1
-rw-r--r--src/app/store.test.ts85
-rw-r--r--src/app/ws-handlers.ts5
3 files changed, 90 insertions, 1 deletions
diff --git a/src/app/chat-registry.svelte.ts b/src/app/chat-registry.svelte.ts
index 8e97af1..a9b28ff 100644
--- a/src/app/chat-registry.svelte.ts
+++ b/src/app/chat-registry.svelte.ts
@@ -78,6 +78,7 @@ export function createChatRegistry(deps: {
function createChatFor(conversationId: string, model: string, workspaceId: string): ChatStore {
return createChatStore({
conversationId,
+ createRequestId: randomId,
model,
workspaceId,
transport: {
diff --git a/src/app/store.test.ts b/src/app/store.test.ts
index f257f3a..40a0fa7 100644
--- a/src/app/store.test.ts
+++ b/src/app/store.test.ts
@@ -2991,3 +2991,88 @@ it.each([
store.dispose();
}
});
+
+describe("compaction queued acknowledgement routing", () => {
+ it.each([
+ "surface-first",
+ "ack-first",
+ "drained-before-ack",
+ ])("uses only authoritative queue snapshots: %s", (order) => {
+ const ws = fakeSocket();
+ const store = createAppStore({
+ socketFactory: () => ws,
+ fetchImpl: fakeFetchImpl(),
+ localStorage: createFakeStorage(),
+ chatFlushScheduler: null,
+ });
+ ws.resolveOpen();
+ store.send("queued text", [{ url: "data:image/png;base64,AAAA" }]);
+ const cid = activeConversationId(store);
+ const send = parseSent(ws).find((m) => (m as { type: string }).type === "chat.send") as {
+ requestId: string;
+ };
+ expect(send.requestId).toEqual(expect.any(String));
+ const sourceChat = store.activeChat;
+ const watch = store.watchConversation("other-conversation");
+ watch.send("unrelated local echo");
+ ws.feedSurfaceMessage({
+ type: "catalog",
+ catalog: [{ id: "message-queue", region: "inline", title: "Queue", scope: "conversation" }],
+ });
+ const spec = (messages: { id: string; text: string; timestamp: number }[]) => ({
+ id: "message-queue",
+ region: "inline",
+ title: "Queue",
+ fields: [{ kind: "custom" as const, rendererId: "message-queue", payload: { messages } }],
+ });
+ const queued = spec([{ id: "q1", text: "queued text", timestamp: 1 }]);
+ const drained = spec([]);
+ const ack = () =>
+ ws.feedServerMessage({
+ type: "chat.queued",
+ conversationId: cid,
+ reason: "compaction",
+ messageId: "q1",
+ requestId: send.requestId,
+ });
+ if (order === "ack-first") {
+ ack();
+ expect(store.surface("message-queue", cid)).toBeNull();
+ }
+ ws.feedSurfaceMessage({ type: "surface", conversationId: cid, spec: queued });
+ expect(store.surface("message-queue", cid)).toEqual(queued);
+ if (order === "drained-before-ack") {
+ ws.feedSurfaceMessage({
+ type: "update",
+ update: { surfaceId: "message-queue", conversationId: cid, spec: drained },
+ });
+ ws.feedServerMessage({
+ type: "chat.delta",
+ event: { type: "turn-start", conversationId: cid, turnId: "t1" },
+ });
+ ws.feedServerMessage({
+ type: "chat.delta",
+ event: { type: "user-message", conversationId: cid, turnId: "t1", text: "queued text" },
+ });
+ }
+ ack();
+ ack();
+ expect(store.surface("message-queue", cid)).toEqual(
+ order === "drained-before-ack" ? drained : queued,
+ );
+ expect(sourceChat.chunks.map((c) => c.chunk)).toEqual(
+ order === "drained-before-ack" ? [{ type: "text", text: "queued text" }] : [],
+ );
+ expect(sourceChat.error).toBeNull();
+ expect(watch.chunks.map((c) => c.chunk)).toEqual([
+ { type: "text", text: "unrelated local echo" },
+ ]);
+ expect(parseSent(ws).filter((m) => (m as { type: string }).type === "chat.send")).toHaveLength(
+ 2,
+ );
+ expect(parseSent(ws).filter((m) => (m as { type: string }).type === "chat.queue")).toHaveLength(
+ 0,
+ );
+ store.dispose();
+ });
+});
diff --git a/src/app/ws-handlers.ts b/src/app/ws-handlers.ts
index c76c9c1..aa1e03e 100644
--- a/src/app/ws-handlers.ts
+++ b/src/app/ws-handlers.ts
@@ -1,6 +1,7 @@
import type {
ChatDeltaMessage,
ChatErrorMessage,
+ ChatQueuedMessage,
ConversationCompactedMessage,
ConversationOpenMessage,
ConversationStatusChangedMessage,
@@ -52,7 +53,7 @@ export function createWsHandlers(deps: {
layoutStore,
tileLayoutStore,
} = deps;
- function handleChatMessage(msg: ChatDeltaMessage | ChatErrorMessage): void {
+ function handleChatMessage(msg: ChatDeltaMessage | ChatErrorMessage | ChatQueuedMessage): void {
let targetId: string | undefined;
if (msg.type === "chat.delta") {
targetId = msg.event.conversationId;
@@ -68,6 +69,8 @@ export function createWsHandlers(deps: {
}
}
+ if (msg.type === "chat.queued") return;
+
// fallback: try all stores (chat.error without conversationId)
for (const store of registry.chatStores.values()) {
store.handleDelta(msg);