@cuongtran001/kanna 1.2.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/dist/client/assets/{arc-BLk1CZKB.js → arc-DXtN3EWJ.js} +1 -1
  2. package/dist/client/assets/{architectureDiagram-3BPJPVTR-B01ZQYte.js → architectureDiagram-3BPJPVTR-CIGqW1z-.js} +1 -1
  3. package/dist/client/assets/{blockDiagram-GPEHLZMM-diL6a3cq.js → blockDiagram-GPEHLZMM-BafNS_H6.js} +1 -1
  4. package/dist/client/assets/{c4Diagram-AAUBKEIU-BY1verL8.js → c4Diagram-AAUBKEIU-YdAgRSis.js} +1 -1
  5. package/dist/client/assets/channel-BEYTrUAm.js +1 -0
  6. package/dist/client/assets/{chunk-2J33WTMH-psdZuajw.js → chunk-2J33WTMH-eiP5y5G-.js} +1 -1
  7. package/dist/client/assets/{chunk-4BX2VUAB-CrwQVBc5.js → chunk-4BX2VUAB-CvLGzPMF.js} +1 -1
  8. package/dist/client/assets/{chunk-55IACEB6-9-aj4jKO.js → chunk-55IACEB6-Y-S9Fb8A.js} +1 -1
  9. package/dist/client/assets/{chunk-727SXJPM-1FNK9_nB.js → chunk-727SXJPM-6sJH1RI9.js} +1 -1
  10. package/dist/client/assets/{chunk-AQP2D5EJ-DFhvd_Ek.js → chunk-AQP2D5EJ-hV1r_IXO.js} +1 -1
  11. package/dist/client/assets/{chunk-FMBD7UC4-B7MLrDa7.js → chunk-FMBD7UC4-CC3L28S5.js} +1 -1
  12. package/dist/client/assets/{chunk-ND2GUHAM-D4Df05QE.js → chunk-ND2GUHAM-BZ3czhdH.js} +1 -1
  13. package/dist/client/assets/{chunk-QZHKN3VN-Iq17Dznm.js → chunk-QZHKN3VN-Bjyd6Yr-.js} +1 -1
  14. package/dist/client/assets/classDiagram-4FO5ZUOK-CvnF5Ud6.js +1 -0
  15. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-CvnF5Ud6.js +1 -0
  16. package/dist/client/assets/{cose-bilkent-S5V4N54A-BmaZhtXy.js → cose-bilkent-S5V4N54A-CfxdDghE.js} +1 -1
  17. package/dist/client/assets/{dagre-BM42HDAG-BCmpfnfP.js → dagre-BM42HDAG-B8CddsbB.js} +1 -1
  18. package/dist/client/assets/{diagram-2AECGRRQ-BIEcKowS.js → diagram-2AECGRRQ-F0yS0fVB.js} +1 -1
  19. package/dist/client/assets/{diagram-5GNKFQAL-CRBABBfv.js → diagram-5GNKFQAL-cW0i0qlh.js} +1 -1
  20. package/dist/client/assets/{diagram-KO2AKTUF-BRxUk16N.js → diagram-KO2AKTUF-BzAKMeJT.js} +1 -1
  21. package/dist/client/assets/{diagram-LMA3HP47-DPBs4xl1.js → diagram-LMA3HP47-qdC4XqIl.js} +1 -1
  22. package/dist/client/assets/{diagram-OG6HWLK6-BE9rgVv8.js → diagram-OG6HWLK6-CGcpi6Dh.js} +1 -1
  23. package/dist/client/assets/{erDiagram-TEJ5UH35-D3vgA1xJ.js → erDiagram-TEJ5UH35-CxNTDicM.js} +1 -1
  24. package/dist/client/assets/{flowDiagram-I6XJVG4X-Cgu-8XXC.js → flowDiagram-I6XJVG4X-qkGa5TqY.js} +1 -1
  25. package/dist/client/assets/{ganttDiagram-6RSMTGT7-CgjXVyTR.js → ganttDiagram-6RSMTGT7-D9mVPoQi.js} +1 -1
  26. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-zD5D8B-i.js → gitGraphDiagram-PVQCEYII-DA0grAXe.js} +1 -1
  27. package/dist/client/assets/{index-DBTdhMbJ.js → index-BqUY1Bm4.js} +1 -1
  28. package/dist/client/assets/index-CLsOWXff.css +32 -0
  29. package/dist/client/assets/{index-zQLLeAfl.js → index-TU1k6ov6.js} +253 -243
  30. package/dist/client/assets/{infoDiagram-5YYISTIA-BaFHqT4u.js → infoDiagram-5YYISTIA-CGljtLs3.js} +1 -1
  31. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-2yAC1br2.js → ishikawaDiagram-YF4QCWOH-CtC0xrmG.js} +1 -1
  32. package/dist/client/assets/{journeyDiagram-JHISSGLW-DacQ-ND5.js → journeyDiagram-JHISSGLW-URLnVZeN.js} +1 -1
  33. package/dist/client/assets/{kanban-definition-UN3LZRKU-Qlbwm3z4.js → kanban-definition-UN3LZRKU-C2Q6aqQh.js} +1 -1
  34. package/dist/client/assets/{linear-DeafqpgO.js → linear-ByKHtsIx.js} +1 -1
  35. package/dist/client/assets/{mermaid.core-KYiCPJDj.js → mermaid.core-Dl_f2KLc.js} +4 -4
  36. package/dist/client/assets/{mindmap-definition-RKZ34NQL-B1V-T6d3.js → mindmap-definition-RKZ34NQL-CaJ9IDLI.js} +1 -1
  37. package/dist/client/assets/{pieDiagram-4H26LBE5-DOF9OFRu.js → pieDiagram-4H26LBE5-mmYaf2Oa.js} +1 -1
  38. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-lH1mB7ko.js → quadrantDiagram-W4KKPZXB-B_PO6OCC.js} +1 -1
  39. package/dist/client/assets/{requirementDiagram-4Y6WPE33-BaTsJ_sS.js → requirementDiagram-4Y6WPE33-M4huwJt1.js} +1 -1
  40. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-Pfxib5iI.js → sankeyDiagram-5OEKKPKP-B0hb1sWh.js} +1 -1
  41. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-BQMEKkBC.js → sequenceDiagram-3UESZ5HK-BAqSptnA.js} +1 -1
  42. package/dist/client/assets/{stateDiagram-AJRCARHV-CuldNnzp.js → stateDiagram-AJRCARHV-DTNkvPi5.js} +1 -1
  43. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-CVN3oGTF.js +1 -0
  44. package/dist/client/assets/{timeline-definition-PNZ67QCA-DP7u0YAg.js → timeline-definition-PNZ67QCA-MgAjHz8Q.js} +1 -1
  45. package/dist/client/assets/{vennDiagram-CIIHVFJN-QlrjpNeQ.js → vennDiagram-CIIHVFJN-DmKoO51W.js} +1 -1
  46. package/dist/client/assets/{wardley-L42UT6IY-BqdJ_sAM.js → wardley-L42UT6IY-CWcyHnYs.js} +1 -1
  47. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-Wi3kaQ5X.js → wardleyDiagram-YWT4CUSO-BGIGmFP9.js} +1 -1
  48. package/dist/client/assets/{xychartDiagram-2RQKCTM6-D-KXMyCG.js → xychartDiagram-2RQKCTM6-D20YIvEl.js} +1 -1
  49. package/dist/client/index.html +2 -2
  50. package/package.json +1 -1
  51. package/src/server/agent-coordinator-types.ts +158 -0
  52. package/src/server/agent-coordinator.ts +919 -0
  53. package/src/server/agent-deps-builders.ts +425 -0
  54. package/src/server/agent.oauth-rotation.test.ts +101 -0
  55. package/src/server/agent.test.ts +24 -8
  56. package/src/server/agent.ts +35 -4963
  57. package/src/server/auto-continue/read-model.test.ts +2 -2
  58. package/src/server/auto-continue/read-model.ts +4 -1
  59. package/src/server/claude-autocontinue-commands.test.ts +494 -0
  60. package/src/server/claude-autocontinue-commands.ts +256 -0
  61. package/src/server/claude-cancel-handler.test.ts +472 -0
  62. package/src/server/claude-cancel-handler.ts +241 -0
  63. package/src/server/claude-chat-management.test.ts +395 -0
  64. package/src/server/claude-chat-management.ts +251 -0
  65. package/src/server/claude-harness-stream.test.ts +273 -0
  66. package/src/server/claude-harness-stream.ts +158 -0
  67. package/src/server/claude-loop-orch-commands.test.ts +579 -0
  68. package/src/server/claude-loop-orch-commands.ts +466 -0
  69. package/src/server/claude-message-normalizer.test.ts +213 -0
  70. package/src/server/claude-message-normalizer.ts +436 -0
  71. package/src/server/claude-prompt-helpers.test.ts +248 -0
  72. package/src/server/claude-prompt-helpers.ts +139 -0
  73. package/src/server/claude-sdk-queue.test.ts +169 -0
  74. package/src/server/claude-sdk-queue.ts +82 -0
  75. package/src/server/claude-send-command.test.ts +414 -0
  76. package/src/server/claude-send-command.ts +420 -0
  77. package/src/server/claude-session-config-helpers.test.ts +279 -0
  78. package/src/server/claude-session-config-helpers.ts +151 -0
  79. package/src/server/claude-session-config.test.ts +250 -0
  80. package/src/server/claude-session-config.ts +159 -0
  81. package/src/server/claude-session-error-handler.test.ts +498 -0
  82. package/src/server/claude-session-error-handler.ts +361 -0
  83. package/src/server/claude-session-lifecycle.test.ts +669 -0
  84. package/src/server/claude-session-lifecycle.ts +283 -0
  85. package/src/server/claude-session-rebuild.test.ts +224 -0
  86. package/src/server/claude-session-rebuild.ts +101 -0
  87. package/src/server/claude-session-runner.test.ts +523 -0
  88. package/src/server/claude-session-runner.ts +521 -0
  89. package/src/server/claude-session-spawner.test.ts +401 -0
  90. package/src/server/claude-session-spawner.ts +401 -0
  91. package/src/server/claude-session-start.test.ts +205 -0
  92. package/src/server/claude-session-start.ts +277 -0
  93. package/src/server/claude-session-state-queries.test.ts +344 -0
  94. package/src/server/claude-session-state-queries.ts +160 -0
  95. package/src/server/claude-session-state.ts +104 -0
  96. package/src/server/claude-slash-commands.test.ts +277 -0
  97. package/src/server/claude-slash-commands.ts +234 -0
  98. package/src/server/claude-spawn-helpers.test.ts +221 -0
  99. package/src/server/claude-spawn-helpers.ts +179 -0
  100. package/src/server/claude-steer-log.test.ts +243 -0
  101. package/src/server/claude-steer-log.ts +58 -0
  102. package/src/server/claude-subagent-tool-response.test.ts +213 -0
  103. package/src/server/claude-subagent-tool-response.ts +165 -0
  104. package/src/server/claude-subagent-wiring.test.ts +263 -0
  105. package/src/server/claude-subagent-wiring.ts +324 -0
  106. package/src/server/claude-tool-respond.test.ts +321 -0
  107. package/src/server/claude-tool-respond.ts +142 -0
  108. package/src/server/claude-turn-runner.test.ts +310 -0
  109. package/src/server/claude-turn-runner.ts +228 -0
  110. package/src/server/claude-turn-starter.test.ts +266 -0
  111. package/src/server/claude-turn-starter.ts +566 -0
  112. package/src/server/claude-usage-math.test.ts +169 -0
  113. package/src/server/claude-usage-math.ts +132 -0
  114. package/src/server/diff-store-branch-ops.adapter.test.ts +332 -0
  115. package/src/server/diff-store-branch-ops.adapter.ts +602 -0
  116. package/src/server/diff-store-commit-ops.adapter.test.ts +358 -0
  117. package/src/server/diff-store-commit-ops.adapter.ts +258 -0
  118. package/src/server/diff-store-errors.ts +85 -0
  119. package/src/server/diff-store-file-ops.adapter.ts +212 -0
  120. package/src/server/diff-store-git-branch.adapter.ts +686 -0
  121. package/src/server/diff-store-io.adapter.ts +27 -0
  122. package/src/server/diff-store-parse.test.ts +212 -0
  123. package/src/server/diff-store-parse.ts +122 -0
  124. package/src/server/diff-store-state.ts +82 -0
  125. package/src/server/diff-store.ts +76 -1854
  126. package/src/server/event-store-apply.test.ts +280 -0
  127. package/src/server/event-store-apply.ts +132 -0
  128. package/src/server/event-store-chat-lifecycle.test.ts +408 -0
  129. package/src/server/event-store-chat-lifecycle.ts +441 -0
  130. package/src/server/event-store-entity-write.ts +356 -0
  131. package/src/server/event-store-helpers.test.ts +230 -0
  132. package/src/server/event-store-helpers.ts +235 -0
  133. package/src/server/event-store-init.ts +276 -0
  134. package/src/server/event-store-messages.adapter.test.ts +365 -0
  135. package/src/server/event-store-messages.adapter.ts +213 -0
  136. package/src/server/event-store-orch-subscription.ts +124 -0
  137. package/src/server/event-store-orch.test.ts +462 -0
  138. package/src/server/event-store-orch.ts +312 -0
  139. package/src/server/event-store-peripheral-events.adapter.test.ts +254 -0
  140. package/src/server/event-store-peripheral-events.adapter.ts +105 -0
  141. package/src/server/event-store-snapshot.test.ts +597 -0
  142. package/src/server/event-store-snapshot.ts +658 -0
  143. package/src/server/event-store-subagent.test.ts +408 -0
  144. package/src/server/event-store-subagent.ts +198 -0
  145. package/src/server/event-store-tool-requests.test.ts +210 -0
  146. package/src/server/event-store-tool-requests.ts +131 -0
  147. package/src/server/event-store-transcript-write.adapter.ts +285 -0
  148. package/src/server/event-store-write-ops.test.ts +554 -0
  149. package/src/server/event-store-write-ops.ts +529 -0
  150. package/src/server/event-store.test.ts +35 -0
  151. package/src/server/event-store.ts +251 -2284
  152. package/src/server/events.ts +3 -0
  153. package/src/server/harness-types.ts +15 -1
  154. package/src/server/oauth-errors.ts +19 -0
  155. package/src/server/read-models.ts +26 -1
  156. package/src/server/subagent-orchestrator.ts +3 -0
  157. package/src/server/ws-router-agent-ctrl.test.ts +187 -0
  158. package/src/server/ws-router-agent-ctrl.ts +137 -0
  159. package/src/server/ws-router-broadcast.ts +488 -0
  160. package/src/server/ws-router-chat.test.ts +461 -0
  161. package/src/server/ws-router-chat.ts +279 -0
  162. package/src/server/ws-router-defaults.test.ts +283 -0
  163. package/src/server/ws-router-defaults.ts +434 -0
  164. package/src/server/ws-router-diff.test.ts +167 -0
  165. package/src/server/ws-router-diff.ts +250 -0
  166. package/src/server/ws-router-envelope.ts +329 -0
  167. package/src/server/ws-router-misc.test.ts +422 -0
  168. package/src/server/ws-router-misc.ts +265 -0
  169. package/src/server/ws-router-orch.test.ts +190 -0
  170. package/src/server/ws-router-orch.ts +93 -0
  171. package/src/server/ws-router-project.test.ts +403 -0
  172. package/src/server/ws-router-project.ts +256 -0
  173. package/src/server/ws-router-push.test.ts +193 -0
  174. package/src/server/ws-router-push.ts +120 -0
  175. package/src/server/ws-router-settings.test.ts +173 -0
  176. package/src/server/ws-router-settings.ts +416 -0
  177. package/src/server/ws-router-skills.test.ts +194 -0
  178. package/src/server/ws-router-skills.ts +204 -0
  179. package/src/server/ws-router-utils.ts +224 -0
  180. package/src/server/ws-router.ts +339 -2254
  181. package/src/shared/app-settings-types.ts +365 -0
  182. package/src/shared/core-types.ts +27 -0
  183. package/src/shared/git-diff-types.ts +165 -0
  184. package/src/shared/loop-progress.test.ts +106 -0
  185. package/src/shared/loop-progress.ts +88 -0
  186. package/src/shared/mcp-types.ts +105 -0
  187. package/src/shared/provider-model-types.ts +437 -0
  188. package/src/shared/subagent-types.ts +198 -0
  189. package/src/shared/tool-call-types.ts +314 -0
  190. package/src/shared/transcript-types.ts +270 -0
  191. package/src/shared/types.ts +27 -1668
  192. package/dist/client/assets/channel-BMkhjiLI.js +0 -1
  193. package/dist/client/assets/classDiagram-4FO5ZUOK-C5Kg1XOD.js +0 -1
  194. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-C5Kg1XOD.js +0 -1
  195. package/dist/client/assets/index-H6C-f2Vp.css +0 -32
  196. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-Ca2U11af.js +0 -1
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Pure prompt-manipulation helpers — no IO, no side effects.
3
+ * Extracted from agent.ts to keep it lean.
4
+ */
5
+
6
+ import type { ChatAttachment } from "../shared/types"
7
+ import { isRecord } from "../shared/errors"
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // XML helpers
11
+ // ---------------------------------------------------------------------------
12
+
13
+ function escapeXmlAttribute(value: string) {
14
+ return value
15
+ .replaceAll("&", "&")
16
+ .replaceAll("\"", """)
17
+ .replaceAll("<", "&lt;")
18
+ .replaceAll(">", "&gt;")
19
+ }
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Attachment hint
23
+ // ---------------------------------------------------------------------------
24
+
25
+ export function buildAttachmentHintText(attachments: ChatAttachment[]) {
26
+ if (attachments.length === 0) return ""
27
+
28
+ const lines = attachments.map((attachment) => (
29
+ `<attachment kind="${escapeXmlAttribute(attachment.kind)}" mime_type="${escapeXmlAttribute(attachment.mimeType)}" path="${escapeXmlAttribute(attachment.absolutePath)}" project_path="${escapeXmlAttribute(attachment.relativePath)}" size_bytes="${attachment.size}" display_name="${escapeXmlAttribute(attachment.displayName)}" />`
30
+ ))
31
+
32
+ return [
33
+ "<kanna-attachments>",
34
+ ...lines,
35
+ "</kanna-attachments>",
36
+ ].join("\n")
37
+ }
38
+
39
+ export function buildPromptText(content: string, attachments: ChatAttachment[]) {
40
+ const attachmentHint = buildAttachmentHintText(attachments)
41
+ if (!attachmentHint) {
42
+ return content.trim()
43
+ }
44
+
45
+ const trimmed = content.trim()
46
+ return [
47
+ trimmed || "Please inspect the attached files.",
48
+ attachmentHint,
49
+ ].join("\n\n").trim()
50
+ }
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Steered message
54
+ // ---------------------------------------------------------------------------
55
+
56
+ const STEERED_MESSAGE_PREFIX = `<system-message>
57
+ The user would like to inform you of something while you continue to work. Acknowledge receipt immediately with a text response, then continue with the task at hand, incorporating the user's feedback if needed.
58
+ </system-message>`
59
+
60
+ export function buildSteeredMessageContent(content: string) {
61
+ return content.trim().length > 0
62
+ ? `${STEERED_MESSAGE_PREFIX}\n\n${content}`
63
+ : STEERED_MESSAGE_PREFIX
64
+ }
65
+
66
+ // ---------------------------------------------------------------------------
67
+ // Error-message classifiers
68
+ // ---------------------------------------------------------------------------
69
+
70
+ export function isPromptTooLongMessage(message: string): boolean {
71
+ return /\bprompt\b.*\btoo\s+long\b/i.test(message)
72
+ || /\bprompt\b.*\btoo\s+large\b/i.test(message)
73
+ }
74
+
75
+ // The stored session token points at a conversation the Claude CLI never
76
+ // persisted (e.g. a spawn interrupted before its first write). Every resume
77
+ // then fails instantly — and the doomed spawn mints yet another unpersisted
78
+ // session id, so without clearing the token the chat is wedged forever. The
79
+ // message rides in result.errors (debugRaw); result text is empty.
80
+ export function isNoConversationFoundMessage(message: string): boolean {
81
+ return /No conversation found with session ID/i.test(message)
82
+ }
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // SDK effort normaliser
86
+ // ---------------------------------------------------------------------------
87
+
88
+ /** Narrows a free-form effort string to the SDK-accepted union without a cast. */
89
+ export function toSdkEffort(effort: string | undefined): "low" | "medium" | "high" | "xhigh" | "max" | undefined {
90
+ if (effort === "low" || effort === "medium" || effort === "high" || effort === "xhigh" || effort === "max") {
91
+ return effort
92
+ }
93
+ return undefined
94
+ }
95
+
96
+ // ---------------------------------------------------------------------------
97
+ // Background-task ID extraction
98
+ // ---------------------------------------------------------------------------
99
+
100
+ // Claude Code's BashTool emits this exact line in the tool_result when a command
101
+ // is launched with `run_in_background: true`. It is the only observable launch
102
+ // signal in Kanna's entry stream (the later `<task-notification>` line produces
103
+ // no transcript entry). The id is alphanumeric. Global flag: one result may
104
+ // report multiple launches in theory; capture every id.
105
+ const BACKGROUND_TASK_LAUNCH_RE = /Command running in background with ID:\s*(\w+)/g
106
+
107
+ /** Extract background-task ids from a tool_result entry's content (string or content blocks). */
108
+ export function backgroundTaskIdsFromToolResult<T>(content: T): string[] {
109
+ let text = ""
110
+ if (typeof content === "string") {
111
+ text = content
112
+ } else if (Array.isArray(content)) {
113
+ for (const block of content) {
114
+ if (isRecord(block)) {
115
+ const blockText = block.text
116
+ if (typeof blockText === "string") {
117
+ text += `${blockText}\n`
118
+ }
119
+ }
120
+ }
121
+ } else {
122
+ return []
123
+ }
124
+ const ids: string[] = []
125
+ for (const match of text.matchAll(BACKGROUND_TASK_LAUNCH_RE)) {
126
+ if (match[1]) ids.push(match[1])
127
+ }
128
+ return ids
129
+ }
130
+
131
+ // ---------------------------------------------------------------------------
132
+ // Env-var helpers
133
+ // ---------------------------------------------------------------------------
134
+
135
+ export function positiveIntegerFromEnv(value: string | undefined, fallback: number): number {
136
+ if (value === undefined || value.trim() === "") return fallback
137
+ const parsed = Number(value)
138
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback
139
+ }
@@ -0,0 +1,169 @@
1
+ import { expect, test, describe } from "bun:test"
2
+ import { AsyncMessageQueue, discardedToolResult, toClaudeMessageStream } from "./claude-sdk-queue"
3
+ import type { ClaudeRawSdkMessage } from "./claude-message-normalizer"
4
+ import type { NormalizedToolCall } from "../shared/types"
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Helpers
8
+ // ---------------------------------------------------------------------------
9
+
10
+ /** Build a fake Query (AsyncGenerator) from an array of objects */
11
+ async function* fakeQuery(
12
+ items: Record<string, unknown>[]
13
+ ): AsyncGenerator<Record<string, unknown>, void> {
14
+ for (const item of items) {
15
+ yield item
16
+ }
17
+ }
18
+
19
+ /** Collect all values from an async iterable into an array */
20
+ async function collect<T>(iter: AsyncIterable<T>): Promise<T[]> {
21
+ const out: T[] = []
22
+ for await (const v of iter) {
23
+ out.push(v)
24
+ }
25
+ return out
26
+ }
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // AsyncMessageQueue
30
+ // ---------------------------------------------------------------------------
31
+
32
+ describe("AsyncMessageQueue", () => {
33
+ test("push + async iteration yields values in order", async () => {
34
+ const q = new AsyncMessageQueue<number>()
35
+ q.push(1)
36
+ q.push(2)
37
+ q.push(3)
38
+ q.close()
39
+ const result = await collect(q)
40
+ expect(result).toEqual([1, 2, 3])
41
+ })
42
+
43
+ test("close() ends iteration", async () => {
44
+ const q = new AsyncMessageQueue<string>()
45
+ q.push("a")
46
+ q.close()
47
+ const result = await collect(q)
48
+ expect(result).toEqual(["a"])
49
+ })
50
+
51
+ test("push after close throws", () => {
52
+ const q = new AsyncMessageQueue<number>()
53
+ q.close()
54
+ expect(() => q.push(42)).toThrow("Cannot push to a closed queue")
55
+ })
56
+
57
+ test("waiter is resolved immediately when queue had a pending value", async () => {
58
+ const q = new AsyncMessageQueue<string>()
59
+ // Start consuming before any push
60
+ const iter = q[Symbol.asyncIterator]()
61
+ const nextPromise = iter.next()
62
+ // Push while a waiter is registered
63
+ q.push("hello")
64
+ const result = await nextPromise
65
+ expect(result).toEqual({ done: false, value: "hello" })
66
+ })
67
+
68
+ test("multiple pushes before iteration are delivered in order", async () => {
69
+ const q = new AsyncMessageQueue<number>()
70
+ for (let i = 0; i < 5; i++) q.push(i)
71
+ q.close()
72
+ const result = await collect(q)
73
+ expect(result).toEqual([0, 1, 2, 3, 4])
74
+ })
75
+
76
+ test("close with pending waiters resolves them as done", async () => {
77
+ const q = new AsyncMessageQueue<number>()
78
+ const iter = q[Symbol.asyncIterator]()
79
+ const nextPromise = iter.next()
80
+ // Close while a waiter is pending
81
+ q.close()
82
+ const result = await nextPromise
83
+ expect(result).toEqual({ done: true, value: undefined })
84
+ })
85
+
86
+ test("close() is idempotent — calling twice does not throw", () => {
87
+ const q = new AsyncMessageQueue<number>()
88
+ q.push(1)
89
+ q.close()
90
+ expect(() => q.close()).not.toThrow()
91
+ })
92
+ })
93
+
94
+ // ---------------------------------------------------------------------------
95
+ // discardedToolResult
96
+ // ---------------------------------------------------------------------------
97
+
98
+ /** Build a minimal mock NormalizedToolCall with the given toolKind. */
99
+ function makeTool<TKind extends "ask_user_question" | "exit_plan_mode">(
100
+ toolKind: TKind
101
+ ): NormalizedToolCall & { toolKind: TKind } {
102
+ return {
103
+ kind: "tool",
104
+ toolKind,
105
+ toolName: toolKind,
106
+ toolId: "t1",
107
+ input: {} as never,
108
+ } as unknown as NormalizedToolCall & { toolKind: TKind }
109
+ }
110
+
111
+ describe("discardedToolResult", () => {
112
+ test("ask_user_question variant returns answers: {}", () => {
113
+ const result = discardedToolResult(makeTool("ask_user_question"))
114
+ expect(result).toHaveProperty("answers")
115
+ expect((result as { answers: Record<string, string> }).answers).toEqual({})
116
+ })
117
+
118
+ test("exit_plan_mode variant does not have an answers field", () => {
119
+ const result = discardedToolResult(makeTool("exit_plan_mode"))
120
+ expect(result).not.toHaveProperty("answers")
121
+ })
122
+
123
+ test("ask_user_question result carries discarded: true", () => {
124
+ expect(discardedToolResult(makeTool("ask_user_question")).discarded).toBe(true)
125
+ })
126
+
127
+ test("exit_plan_mode result carries discarded: true", () => {
128
+ expect(discardedToolResult(makeTool("exit_plan_mode")).discarded).toBe(true)
129
+ })
130
+ })
131
+
132
+ // ---------------------------------------------------------------------------
133
+ // toClaudeMessageStream
134
+ // ---------------------------------------------------------------------------
135
+
136
+ describe("toClaudeMessageStream", () => {
137
+ test("passes through all SDK messages unchanged", async () => {
138
+ const messages: ClaudeRawSdkMessage[] = [
139
+ { type: "assistant", uuid: "a" },
140
+ { type: "system", uuid: "b" },
141
+ ]
142
+ // fakeQuery produces objects; cast to satisfy Query's AsyncGenerator shape
143
+ const q = fakeQuery(messages as Record<string, unknown>[])
144
+ const result = await collect(toClaudeMessageStream(q as never))
145
+ expect(result).toHaveLength(2)
146
+ expect(result[0]).toEqual(messages[0])
147
+ expect(result[1]).toEqual(messages[1])
148
+ })
149
+
150
+ test("yields empty sequence for an empty query", async () => {
151
+ const q = fakeQuery([])
152
+ const result = await collect(toClaudeMessageStream(q as never))
153
+ expect(result).toHaveLength(0)
154
+ })
155
+
156
+ test("yielded messages preserve their properties", async () => {
157
+ const msg: ClaudeRawSdkMessage = {
158
+ type: "result",
159
+ result: "DONE",
160
+ total_cost_usd: 0.001,
161
+ session_id: "sess-1",
162
+ }
163
+ const q = fakeQuery([msg as Record<string, unknown>])
164
+ const [yielded] = await collect(toClaudeMessageStream(q as never))
165
+ expect(yielded?.result).toBe("DONE")
166
+ expect(yielded?.total_cost_usd).toBe(0.001)
167
+ expect(yielded?.session_id).toBe("sess-1")
168
+ })
169
+ })
@@ -0,0 +1,82 @@
1
+ import { type Query } from "@anthropic-ai/claude-agent-sdk"
2
+ import { type ClaudeRawSdkMessage, isSdkToClaudeMessage } from "./claude-message-normalizer"
3
+ import type { NormalizedToolCall } from "../shared/types"
4
+
5
+ /**
6
+ * Generic async iterable queue used to buffer SDK user-message turns.
7
+ * Pure data structure — no IO.
8
+ */
9
+ export class AsyncMessageQueue<T> implements AsyncIterable<T> {
10
+ private readonly values: T[] = []
11
+ private readonly waiters: Array<(result: IteratorResult<T, undefined>) => void> = []
12
+ private closed = false
13
+
14
+ push(value: T) {
15
+ if (this.closed) {
16
+ throw new Error("Cannot push to a closed queue")
17
+ }
18
+
19
+ const waiter = this.waiters.shift()
20
+ if (waiter) {
21
+ waiter({ done: false, value })
22
+ return
23
+ }
24
+
25
+ this.values.push(value)
26
+ }
27
+
28
+ close() {
29
+ if (this.closed) return
30
+ this.closed = true
31
+ while (this.waiters.length > 0) {
32
+ const waiter = this.waiters.shift()
33
+ waiter?.({ done: true, value: undefined })
34
+ }
35
+ }
36
+
37
+ [Symbol.asyncIterator](): AsyncIterator<T, undefined> {
38
+ return {
39
+ next: async (): Promise<IteratorResult<T, undefined>> => {
40
+ if (this.values.length > 0) {
41
+ return { done: false, value: this.values.shift()! }
42
+ }
43
+
44
+ if (this.closed) {
45
+ return { done: true, value: undefined }
46
+ }
47
+
48
+ return await new Promise<IteratorResult<T, undefined>>((resolve) => {
49
+ this.waiters.push(resolve)
50
+ })
51
+ },
52
+ }
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Builds the discard payload for ask_user_question / exit_plan_mode tools
58
+ * when a pending tool call is discarded (e.g. session closed mid-flight).
59
+ */
60
+ export function discardedToolResult(
61
+ tool: NormalizedToolCall & { toolKind: "ask_user_question" | "exit_plan_mode" }
62
+ ) {
63
+ if (tool.toolKind === "ask_user_question") {
64
+ return {
65
+ discarded: true,
66
+ answers: {},
67
+ }
68
+ }
69
+
70
+ return {
71
+ discarded: true,
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Filters the raw SDK stream to Claude-only messages.
77
+ */
78
+ export async function* toClaudeMessageStream(q: Query): AsyncGenerator<ClaudeRawSdkMessage> {
79
+ for await (const m of q) {
80
+ if (isSdkToClaudeMessage(m)) yield m
81
+ }
82
+ }