summaryrefslogtreecommitdiffhomepage
path: root/src/adapters
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/adapters
parent0bdb04b0aa0c0500e68e2e985b1a2880730ad7e8 (diff)
downloaddispatch-web-e9786bf1dbf258b68f793870d02378c390939d3d.tar.gz
dispatch-web-e9786bf1dbf258b68f793870d02378c390939d3d.zip
fix(review-followup-fed): reconcile compaction-queued send acknowledgements
Diffstat (limited to 'src/adapters')
-rw-r--r--src/adapters/ws/index.test.ts23
-rw-r--r--src/adapters/ws/index.ts7
-rw-r--r--src/adapters/ws/logic.test.ts32
-rw-r--r--src/adapters/ws/logic.ts16
4 files changed, 75 insertions, 3 deletions
diff --git a/src/adapters/ws/index.test.ts b/src/adapters/ws/index.test.ts
index 6f5e4ef..6461493 100644
--- a/src/adapters/ws/index.test.ts
+++ b/src/adapters/ws/index.test.ts
@@ -476,3 +476,26 @@ describe("reconnect backoff after open", () => {
}
});
});
+
+it("routes chat.queued only through the typed chat callback, never surface/error handling", () => {
+ const ws = fakeSocket();
+ const onChat = vi.fn();
+ const onMessage = vi.fn();
+ const handle = createSurfaceSocket({
+ url: "ws://test",
+ socketFactory: () => ws,
+ onChat,
+ onMessage,
+ });
+ const ack = {
+ type: "chat.queued",
+ conversationId: "c1",
+ reason: "compaction",
+ messageId: "q1",
+ requestId: "r1",
+ };
+ ws.invokeMessage(JSON.stringify(ack));
+ expect(onChat).toHaveBeenCalledExactlyOnceWith(ack);
+ expect(onMessage).not.toHaveBeenCalled();
+ handle.close();
+});
diff --git a/src/adapters/ws/index.ts b/src/adapters/ws/index.ts
index eeb7901..25e9911 100644
--- a/src/adapters/ws/index.ts
+++ b/src/adapters/ws/index.ts
@@ -1,6 +1,7 @@
import type {
ChatDeltaMessage,
ChatErrorMessage,
+ ChatQueuedMessage,
ConversationCompactedMessage,
ConversationOpenMessage,
ConversationStatusChangedMessage,
@@ -24,7 +25,7 @@ export interface WebSocketLike {
export interface SurfaceSocketOptions {
url: string;
onMessage: (msg: SurfaceServerMessage) => void;
- onChat?: (msg: ChatDeltaMessage | ChatErrorMessage) => void;
+ onChat?: (msg: ChatDeltaMessage | ChatErrorMessage | ChatQueuedMessage) => void;
/** Broadcast when a conversation is "opened" (e.g. CLI `--open` flag). */
onConversationOpen?: (msg: ConversationOpenMessage) => void;
/** Broadcast when a conversation's lifecycle status changes (active/idle/closed). */
@@ -100,8 +101,8 @@ export function createSurfaceSocket(opts: SurfaceSocketOptions): SurfaceSocketHa
if (disposed) return;
const msg = parseServerMessage(ev.data);
if (msg !== null) {
- if (msg.type === "chat.delta" || msg.type === "chat.error") {
- opts.onChat?.(msg as ChatDeltaMessage | ChatErrorMessage);
+ if (msg.type === "chat.delta" || msg.type === "chat.error" || msg.type === "chat.queued") {
+ opts.onChat?.(msg as ChatDeltaMessage | ChatErrorMessage | ChatQueuedMessage);
} else if (msg.type === "conversation.open") {
opts.onConversationOpen?.(msg as ConversationOpenMessage);
} else if (msg.type === "conversation.statusChanged") {
diff --git a/src/adapters/ws/logic.test.ts b/src/adapters/ws/logic.test.ts
index 4be2cd6..db6444a 100644
--- a/src/adapters/ws/logic.test.ts
+++ b/src/adapters/ws/logic.test.ts
@@ -577,3 +577,35 @@ it("preserves a compaction epoch and rejects absent or invalid epochs", () => {
expect(parseServerMessage(JSON.stringify({ ...msg, historyEpoch }))).toBeNull();
}
});
+
+describe("chat.queued admission acknowledgement", () => {
+ const ack = {
+ type: "chat.queued",
+ conversationId: "c1",
+ reason: "compaction",
+ messageId: "q1",
+ };
+
+ it.each([undefined, "request-1"])("preserves optional requestId %s", (requestId) => {
+ const msg = { ...ack, ...(requestId !== undefined ? { requestId } : {}) };
+ expect(parseServerMessage(JSON.stringify(msg))).toEqual(msg);
+ });
+
+ it.each([
+ { conversationId: undefined },
+ { conversationId: 12 },
+ { messageId: undefined },
+ { messageId: null },
+ { reason: undefined },
+ { reason: "other" },
+ { requestId: null },
+ { requestId: 12 },
+ ])("rejects malformed acknowledgement %j", (bad) => {
+ expect(parseServerMessage(JSON.stringify({ ...ack, ...bad }))).toBeNull();
+ });
+
+ it("serializes a correlated send", () => {
+ const msg = { type: "chat.send" as const, message: "hello", requestId: "r1" };
+ expect(JSON.parse(serialize(msg))).toEqual(msg);
+ });
+});
diff --git a/src/adapters/ws/logic.ts b/src/adapters/ws/logic.ts
index 858deaa..0efcd90 100644
--- a/src/adapters/ws/logic.ts
+++ b/src/adapters/ws/logic.ts
@@ -1,6 +1,7 @@
import type {
ChatDeltaMessage,
ChatErrorMessage,
+ ChatQueuedMessage,
ConversationCompactedMessage,
ConversationOpenMessage,
ConversationStatusChangedMessage,
@@ -25,6 +26,7 @@ const VALID_SERVER_TYPES = new Set([
"error",
"chat.delta",
"chat.error",
+ "chat.queued",
"conversation.open",
"conversation.statusChanged",
"conversation.compacted",
@@ -111,6 +113,20 @@ export function parseServerMessage(data: string): WsServerMessage | null {
if (typeof event.type !== "string") return null;
return { type: "chat.delta", event: event as unknown as ChatDeltaMessage["event"] };
}
+ case "chat.queued": {
+ if (typeof parsed.conversationId !== "string") return null;
+ if (typeof parsed.messageId !== "string") return null;
+ if (parsed.reason !== "compaction") return null;
+ if (parsed.requestId !== undefined && typeof parsed.requestId !== "string") return null;
+ const msg: ChatQueuedMessage = {
+ type: "chat.queued",
+ conversationId: parsed.conversationId,
+ messageId: parsed.messageId,
+ reason: "compaction",
+ ...(parsed.requestId !== undefined ? { requestId: parsed.requestId } : {}),
+ };
+ return msg;
+ }
case "chat.error": {
if (typeof parsed.message !== "string") return null;
const conversationId = parsed.conversationId;