@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,408 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import type { SubagentRunSnapshot } from "../shared/types"
3
+ import type { SubagentRunEvent } from "./events"
4
+ import {
5
+ applySubagentEvent,
6
+ getSubagentRuns,
7
+ runningSubagentRuns,
8
+ type SubagentRunMap,
9
+ } from "./event-store-subagent"
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Helpers
13
+ // ---------------------------------------------------------------------------
14
+
15
+ const TS = 1_700_000_000_000
16
+
17
+ function makeRunsByChatId(
18
+ chatId: string,
19
+ initial: SubagentRunSnapshot[] = [],
20
+ ): Map<string, SubagentRunMap> {
21
+ const inner: SubagentRunMap = new Map()
22
+ for (const run of initial) inner.set(run.runId, run)
23
+ return new Map([[chatId, inner]])
24
+ }
25
+
26
+ function baseRun(overrides: Partial<SubagentRunSnapshot> = {}): SubagentRunSnapshot {
27
+ return {
28
+ runId: "run-1",
29
+ chatId: "chat-1",
30
+ subagentId: "agent-1",
31
+ subagentName: "Test Agent",
32
+ label: null,
33
+ provider: "claude",
34
+ model: "claude-opus-4-5",
35
+ status: "running",
36
+ parentUserMessageId: "msg-1",
37
+ parentRunId: null,
38
+ depth: 0,
39
+ startedAt: TS,
40
+ finishedAt: null,
41
+ finalText: null,
42
+ error: null,
43
+ usage: null,
44
+ entries: [],
45
+ pendingTool: null,
46
+ ...overrides,
47
+ }
48
+ }
49
+
50
+ function startedEvent(
51
+ overrides: Partial<Extract<SubagentRunEvent, { type: "subagent_run_started" }>> = {},
52
+ ): Extract<SubagentRunEvent, { type: "subagent_run_started" }> {
53
+ return {
54
+ v: 3,
55
+ type: "subagent_run_started",
56
+ timestamp: TS,
57
+ chatId: "chat-1",
58
+ runId: "run-1",
59
+ subagentId: "agent-1",
60
+ subagentName: "Test Agent",
61
+ label: "My label",
62
+ provider: "claude",
63
+ model: "claude-opus-4-5",
64
+ parentUserMessageId: "msg-1",
65
+ parentRunId: null,
66
+ depth: 0,
67
+ ...overrides,
68
+ }
69
+ }
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // applySubagentEvent — subagent_run_started
73
+ // ---------------------------------------------------------------------------
74
+
75
+ describe("applySubagentEvent / subagent_run_started", () => {
76
+ test("creates a new run snapshot in the inner map", () => {
77
+ const map = new Map<string, SubagentRunMap>([["chat-1", new Map()]])
78
+ applySubagentEvent(map, startedEvent())
79
+ const run = map.get("chat-1")?.get("run-1")
80
+ expect(run).toBeDefined()
81
+ expect(run?.status).toBe("running")
82
+ expect(run?.subagentId).toBe("agent-1")
83
+ expect(run?.label).toBe("My label")
84
+ expect(run?.entries).toEqual([])
85
+ expect(run?.pendingTool).toBeNull()
86
+ })
87
+
88
+ test("defaults label to null when event.label is absent", () => {
89
+ const event = startedEvent()
90
+ const { label: _removed, ...rest } = event
91
+ const map = new Map<string, SubagentRunMap>([["chat-1", new Map()]])
92
+ applySubagentEvent(map, rest as typeof event)
93
+ expect(map.get("chat-1")?.get("run-1")?.label).toBeNull()
94
+ })
95
+
96
+ test("no-ops when chatId not in outer map", () => {
97
+ const map = new Map<string, SubagentRunMap>()
98
+ applySubagentEvent(map, startedEvent())
99
+ expect(map.size).toBe(0)
100
+ })
101
+ })
102
+
103
+ // ---------------------------------------------------------------------------
104
+ // applySubagentEvent — subagent_message_delta
105
+ // ---------------------------------------------------------------------------
106
+
107
+ describe("applySubagentEvent / subagent_message_delta", () => {
108
+ test("appends content to finalText", () => {
109
+ const run = baseRun({ finalText: "hello " })
110
+ const outer = makeRunsByChatId("chat-1", [run])
111
+ applySubagentEvent(outer, {
112
+ v: 3,
113
+ type: "subagent_message_delta",
114
+ timestamp: TS,
115
+ chatId: "chat-1",
116
+ runId: "run-1",
117
+ content: "world",
118
+ })
119
+ expect(outer.get("chat-1")?.get("run-1")?.finalText).toBe("hello world")
120
+ })
121
+
122
+ test("initialises finalText from null", () => {
123
+ const run = baseRun({ finalText: null })
124
+ const outer = makeRunsByChatId("chat-1", [run])
125
+ applySubagentEvent(outer, {
126
+ v: 3,
127
+ type: "subagent_message_delta",
128
+ timestamp: TS,
129
+ chatId: "chat-1",
130
+ runId: "run-1",
131
+ content: "first",
132
+ })
133
+ expect(outer.get("chat-1")?.get("run-1")?.finalText).toBe("first")
134
+ })
135
+
136
+ test("no-ops for unknown runId", () => {
137
+ const run = baseRun()
138
+ const outer = makeRunsByChatId("chat-1", [run])
139
+ applySubagentEvent(outer, {
140
+ v: 3,
141
+ type: "subagent_message_delta",
142
+ timestamp: TS,
143
+ chatId: "chat-1",
144
+ runId: "run-UNKNOWN",
145
+ content: "x",
146
+ })
147
+ expect(outer.get("chat-1")?.get("run-1")?.finalText).toBeNull()
148
+ })
149
+ })
150
+
151
+ // ---------------------------------------------------------------------------
152
+ // applySubagentEvent — subagent_entry_appended
153
+ // ---------------------------------------------------------------------------
154
+
155
+ describe("applySubagentEvent / subagent_entry_appended", () => {
156
+ test("pushes a non-result entry without touching usage", () => {
157
+ const run = baseRun()
158
+ const outer = makeRunsByChatId("chat-1", [run])
159
+ const entry = {
160
+ kind: "assistant_text" as const,
161
+ _id: "at-1",
162
+ createdAt: TS,
163
+ text: "hello",
164
+ }
165
+ applySubagentEvent(outer, {
166
+ v: 3,
167
+ type: "subagent_entry_appended",
168
+ timestamp: TS,
169
+ chatId: "chat-1",
170
+ runId: "run-1",
171
+ entry,
172
+ })
173
+ const updated = outer.get("chat-1")?.get("run-1")
174
+ expect(updated?.entries).toHaveLength(1)
175
+ expect(updated?.entries[0]).toEqual(entry)
176
+ expect(updated?.usage).toBeNull()
177
+ })
178
+
179
+ test("mirrors usage from a result entry", () => {
180
+ const run = baseRun()
181
+ const outer = makeRunsByChatId("chat-1", [run])
182
+ const resultEntry = {
183
+ kind: "result" as const,
184
+ _id: "res-1",
185
+ createdAt: TS,
186
+ subtype: "success" as const,
187
+ isError: false,
188
+ durationMs: 100,
189
+ result: "done",
190
+ costUsd: 0.05,
191
+ usage: { inputTokens: 10, outputTokens: 20 },
192
+ }
193
+ applySubagentEvent(outer, {
194
+ v: 3,
195
+ type: "subagent_entry_appended",
196
+ timestamp: TS,
197
+ chatId: "chat-1",
198
+ runId: "run-1",
199
+ entry: resultEntry,
200
+ })
201
+ const updated = outer.get("chat-1")?.get("run-1")
202
+ expect(updated?.usage).toEqual({
203
+ inputTokens: 10,
204
+ outputTokens: 20,
205
+ cachedInputTokens: undefined,
206
+ costUsd: 0.05,
207
+ })
208
+ })
209
+ })
210
+
211
+ // ---------------------------------------------------------------------------
212
+ // applySubagentEvent — subagent_run_completed
213
+ // ---------------------------------------------------------------------------
214
+
215
+ describe("applySubagentEvent / subagent_run_completed", () => {
216
+ test("sets status completed, finishedAt, finalText", () => {
217
+ const run = baseRun()
218
+ const outer = makeRunsByChatId("chat-1", [run])
219
+ applySubagentEvent(outer, {
220
+ v: 3,
221
+ type: "subagent_run_completed",
222
+ timestamp: TS + 1000,
223
+ chatId: "chat-1",
224
+ runId: "run-1",
225
+ finalContent: "result text",
226
+ usage: { inputTokens: 5, outputTokens: 8 },
227
+ })
228
+ const updated = outer.get("chat-1")?.get("run-1")
229
+ expect(updated?.status).toBe("completed")
230
+ expect(updated?.finishedAt).toBe(TS + 1000)
231
+ expect(updated?.finalText).toBe("result text")
232
+ expect(updated?.usage?.inputTokens).toBe(5)
233
+ })
234
+
235
+ test("preserves existing mirrored usage when event omits it", () => {
236
+ const run = baseRun({ usage: { inputTokens: 99 } })
237
+ const outer = makeRunsByChatId("chat-1", [run])
238
+ applySubagentEvent(outer, {
239
+ v: 3,
240
+ type: "subagent_run_completed",
241
+ timestamp: TS + 1000,
242
+ chatId: "chat-1",
243
+ runId: "run-1",
244
+ finalContent: "done",
245
+ // no usage field
246
+ })
247
+ expect(outer.get("chat-1")?.get("run-1")?.usage?.inputTokens).toBe(99)
248
+ })
249
+ })
250
+
251
+ // ---------------------------------------------------------------------------
252
+ // applySubagentEvent — subagent_run_failed
253
+ // ---------------------------------------------------------------------------
254
+
255
+ describe("applySubagentEvent / subagent_run_failed", () => {
256
+ test("sets status failed and clears pendingTool", () => {
257
+ const run = baseRun({
258
+ pendingTool: { toolUseId: "t1", toolKind: "ask_user_question", input: {}, requestedAt: TS },
259
+ })
260
+ const outer = makeRunsByChatId("chat-1", [run])
261
+ applySubagentEvent(outer, {
262
+ v: 3,
263
+ type: "subagent_run_failed",
264
+ timestamp: TS + 500,
265
+ chatId: "chat-1",
266
+ runId: "run-1",
267
+ error: { code: "TIMEOUT", message: "timed out" },
268
+ })
269
+ const updated = outer.get("chat-1")?.get("run-1")
270
+ expect(updated?.status).toBe("failed")
271
+ expect(updated?.finishedAt).toBe(TS + 500)
272
+ expect(updated?.error?.code).toBe("TIMEOUT")
273
+ expect(updated?.pendingTool).toBeNull()
274
+ })
275
+ })
276
+
277
+ // ---------------------------------------------------------------------------
278
+ // applySubagentEvent — subagent_run_cancelled
279
+ // ---------------------------------------------------------------------------
280
+
281
+ describe("applySubagentEvent / subagent_run_cancelled", () => {
282
+ test("sets status cancelled and clears pendingTool", () => {
283
+ const run = baseRun({
284
+ pendingTool: { toolUseId: "t1", toolKind: "exit_plan_mode", input: {}, requestedAt: TS },
285
+ })
286
+ const outer = makeRunsByChatId("chat-1", [run])
287
+ applySubagentEvent(outer, {
288
+ v: 3,
289
+ type: "subagent_run_cancelled",
290
+ timestamp: TS + 200,
291
+ chatId: "chat-1",
292
+ runId: "run-1",
293
+ })
294
+ const updated = outer.get("chat-1")?.get("run-1")
295
+ expect(updated?.status).toBe("cancelled")
296
+ expect(updated?.pendingTool).toBeNull()
297
+ })
298
+ })
299
+
300
+ // ---------------------------------------------------------------------------
301
+ // applySubagentEvent — subagent_tool_pending
302
+ // ---------------------------------------------------------------------------
303
+
304
+ describe("applySubagentEvent / subagent_tool_pending", () => {
305
+ test("sets pendingTool on the run", () => {
306
+ const run = baseRun()
307
+ const outer = makeRunsByChatId("chat-1", [run])
308
+ applySubagentEvent(outer, {
309
+ v: 3,
310
+ type: "subagent_tool_pending",
311
+ timestamp: TS + 100,
312
+ chatId: "chat-1",
313
+ runId: "run-1",
314
+ toolUseId: "tu-42",
315
+ toolKind: "ask_user_question",
316
+ input: { question: "What?" },
317
+ })
318
+ const updated = outer.get("chat-1")?.get("run-1")
319
+ expect(updated?.pendingTool?.toolUseId).toBe("tu-42")
320
+ expect(updated?.pendingTool?.toolKind).toBe("ask_user_question")
321
+ expect(updated?.pendingTool?.requestedAt).toBe(TS + 100)
322
+ })
323
+ })
324
+
325
+ // ---------------------------------------------------------------------------
326
+ // applySubagentEvent — subagent_tool_resolved
327
+ // ---------------------------------------------------------------------------
328
+
329
+ describe("applySubagentEvent / subagent_tool_resolved", () => {
330
+ test("clears pendingTool and appends synthetic tool_result entry", () => {
331
+ const run = baseRun({
332
+ pendingTool: { toolUseId: "tu-42", toolKind: "ask_user_question", input: {}, requestedAt: TS },
333
+ })
334
+ const outer = makeRunsByChatId("chat-1", [run])
335
+ applySubagentEvent(outer, {
336
+ v: 3,
337
+ type: "subagent_tool_resolved",
338
+ timestamp: TS + 300,
339
+ chatId: "chat-1",
340
+ runId: "run-1",
341
+ toolUseId: "tu-42",
342
+ result: "User said yes",
343
+ resolution: "user",
344
+ })
345
+ const updated = outer.get("chat-1")?.get("run-1")
346
+ expect(updated?.pendingTool).toBeNull()
347
+ expect(updated?.entries).toHaveLength(1)
348
+ const entry = updated?.entries[0]
349
+ expect(entry?.kind).toBe("tool_result")
350
+ if (entry?.kind === "tool_result") {
351
+ expect(entry._id).toBe("run-1:tu-42:resolved")
352
+ expect(entry.toolId).toBe("tu-42")
353
+ expect(entry.content).toBe("User said yes")
354
+ }
355
+ })
356
+ })
357
+
358
+ // ---------------------------------------------------------------------------
359
+ // getSubagentRuns
360
+ // ---------------------------------------------------------------------------
361
+
362
+ describe("getSubagentRuns", () => {
363
+ test("returns runs keyed by runId", () => {
364
+ const run1 = baseRun({ runId: "r1" })
365
+ const run2 = baseRun({ runId: "r2", status: "completed" })
366
+ const outer = makeRunsByChatId("chat-1", [run1, run2])
367
+ const result = getSubagentRuns(outer, "chat-1")
368
+ expect(Object.keys(result).sort()).toEqual(["r1", "r2"])
369
+ expect(result.r1?.status).toBe("running")
370
+ expect(result.r2?.status).toBe("completed")
371
+ })
372
+
373
+ test("returns empty object for unknown chatId", () => {
374
+ const outer = new Map<string, SubagentRunMap>()
375
+ expect(getSubagentRuns(outer, "no-such-chat")).toEqual({})
376
+ })
377
+ })
378
+
379
+ // ---------------------------------------------------------------------------
380
+ // runningSubagentRuns
381
+ // ---------------------------------------------------------------------------
382
+
383
+ describe("runningSubagentRuns", () => {
384
+ test("yields only running runs across all chats", () => {
385
+ const r1 = baseRun({ runId: "r1", chatId: "c1", status: "running" })
386
+ const r2 = baseRun({ runId: "r2", chatId: "c1", status: "completed" })
387
+ const r3 = baseRun({ runId: "r3", chatId: "c2", status: "running" })
388
+ const r4 = baseRun({ runId: "r4", chatId: "c2", status: "failed" })
389
+
390
+ const outer: Map<string, SubagentRunMap> = new Map([
391
+ ["c1", new Map([["r1", r1], ["r2", r2]])],
392
+ ["c2", new Map([["r3", r3], ["r4", r4]])],
393
+ ])
394
+
395
+ const runIds = [...runningSubagentRuns(outer)].map((r) => r.runId).sort()
396
+ expect(runIds).toEqual(["r1", "r3"])
397
+ })
398
+
399
+ test("returns empty iterable when nothing is running", () => {
400
+ const run = baseRun({ status: "cancelled" })
401
+ const outer = makeRunsByChatId("chat-1", [run])
402
+ expect([...runningSubagentRuns(outer)]).toHaveLength(0)
403
+ })
404
+
405
+ test("returns empty iterable for empty map", () => {
406
+ expect([...runningSubagentRuns(new Map())]).toHaveLength(0)
407
+ })
408
+ })
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Subagent run read-model and write-path layers extracted from event-store.ts.
3
+ *
4
+ * Read functions are pure folds over the in-memory `subagentRunsByChatId` map.
5
+ * `appendSubagentEvent` has IO via injected deps (enqueueDiskAppend) and
6
+ * calls capTranscriptEntry for tool_result capping.
7
+ */
8
+ import type { SubagentRunSnapshot, TranscriptEntry } from "../shared/types"
9
+ import type { ChatRecord, SubagentRunEvent } from "./events"
10
+ import { capTranscriptEntry } from "./subagent-entry-cap.adapter"
11
+
12
+ // ─── Write-path deps ──────────────────────────────────────────────────────
13
+
14
+ export interface AppendSubagentDeps {
15
+ readonly chatsById: Map<string, ChatRecord>
16
+ readonly turnsLogPath: string
17
+ readonly dataDir: string
18
+ applyEvent: (event: SubagentRunEvent) => void
19
+ enqueueDiskAppend: (filePath: string, payload: string) => void
20
+ }
21
+
22
+ /** Alias for the per-chat inner map type used throughout this module. */
23
+ export type SubagentRunMap = Map<string, SubagentRunSnapshot>
24
+
25
+ /**
26
+ * Apply a single `SubagentRunEvent` to the in-memory
27
+ * `subagentRunsByChatId` map (mutates in place).
28
+ *
29
+ * Mirrors the eight `case "subagent_*"` branches that previously lived
30
+ * inside `EventStore.applyEvent`.
31
+ */
32
+ export function applySubagentEvent(
33
+ subagentRunsByChatId: Map<string, SubagentRunMap>,
34
+ event: SubagentRunEvent,
35
+ ): void {
36
+ switch (event.type) {
37
+ case "subagent_run_started": {
38
+ const map = subagentRunsByChatId.get(event.chatId)
39
+ if (!map) break
40
+ map.set(event.runId, {
41
+ runId: event.runId,
42
+ chatId: event.chatId,
43
+ subagentId: event.subagentId,
44
+ subagentName: event.subagentName,
45
+ label: event.label ?? null,
46
+ provider: event.provider,
47
+ model: event.model,
48
+ status: "running",
49
+ parentUserMessageId: event.parentUserMessageId,
50
+ parentRunId: event.parentRunId,
51
+ depth: event.depth,
52
+ startedAt: event.timestamp,
53
+ finishedAt: null,
54
+ finalText: null,
55
+ error: null,
56
+ usage: null,
57
+ entries: [],
58
+ pendingTool: null,
59
+ })
60
+ break
61
+ }
62
+ case "subagent_message_delta": {
63
+ const run = subagentRunsByChatId.get(event.chatId)?.get(event.runId)
64
+ if (!run) break
65
+ run.finalText = (run.finalText ?? "") + event.content
66
+ break
67
+ }
68
+ case "subagent_entry_appended": {
69
+ const run = subagentRunsByChatId.get(event.chatId)?.get(event.runId)
70
+ if (!run) break
71
+ run.entries.push(event.entry)
72
+ // If the entry carries usage (the SDK's terminal "result" message), mirror
73
+ // it onto run.usage so callers can read it without scanning entries.
74
+ if (event.entry.kind === "result") {
75
+ const usage = event.entry.usage
76
+ const cost = event.entry.costUsd
77
+ run.usage = {
78
+ inputTokens: usage?.inputTokens,
79
+ outputTokens: usage?.outputTokens,
80
+ cachedInputTokens: usage?.cachedInputTokens,
81
+ costUsd: cost,
82
+ }
83
+ }
84
+ break
85
+ }
86
+ case "subagent_run_completed": {
87
+ const run = subagentRunsByChatId.get(event.chatId)?.get(event.runId)
88
+ if (!run) break
89
+ run.status = "completed"
90
+ run.finishedAt = event.timestamp
91
+ run.finalText = event.finalContent
92
+ // Merge: prefer event.usage if present, otherwise keep what subagent_entry_appended
93
+ // already mirrored. Otherwise null. Without this guard a streaming run
94
+ // whose completion event omits usage would silently erase it.
95
+ run.usage = event.usage ?? run.usage ?? null
96
+ break
97
+ }
98
+ case "subagent_run_failed": {
99
+ const run = subagentRunsByChatId.get(event.chatId)?.get(event.runId)
100
+ if (!run) break
101
+ run.status = "failed"
102
+ run.finishedAt = event.timestamp
103
+ run.error = event.error
104
+ run.pendingTool = null
105
+ break
106
+ }
107
+ case "subagent_run_cancelled": {
108
+ const run = subagentRunsByChatId.get(event.chatId)?.get(event.runId)
109
+ if (!run) break
110
+ run.status = "cancelled"
111
+ run.finishedAt = event.timestamp
112
+ run.pendingTool = null
113
+ break
114
+ }
115
+ case "subagent_tool_pending": {
116
+ const run = subagentRunsByChatId.get(event.chatId)?.get(event.runId)
117
+ if (!run) break
118
+ run.pendingTool = {
119
+ toolUseId: event.toolUseId,
120
+ toolKind: event.toolKind,
121
+ input: event.input,
122
+ requestedAt: event.timestamp,
123
+ }
124
+ break
125
+ }
126
+ case "subagent_tool_resolved": {
127
+ const run = subagentRunsByChatId.get(event.chatId)?.get(event.runId)
128
+ if (!run) break
129
+ run.pendingTool = null
130
+ const syntheticEntry: TranscriptEntry = {
131
+ kind: "tool_result",
132
+ _id: `${event.runId}:${event.toolUseId}:resolved`,
133
+ createdAt: event.timestamp,
134
+ toolId: event.toolUseId,
135
+ content: event.result,
136
+ }
137
+ run.entries.push(syntheticEntry)
138
+ break
139
+ }
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Return all subagent runs for a chat as a plain record keyed by runId.
145
+ * Returns `{}` if the chat has no subagent map (e.g. was never started).
146
+ */
147
+ export function getSubagentRuns(
148
+ subagentRunsByChatId: Map<string, SubagentRunMap>,
149
+ chatId: string,
150
+ ): Record<string, SubagentRunSnapshot> {
151
+ const map = subagentRunsByChatId.get(chatId)
152
+ if (!map) return {}
153
+ return Object.fromEntries(map.entries())
154
+ }
155
+
156
+ /**
157
+ * Yield every subagent run whose status is `"running"`, across all chats.
158
+ */
159
+ export function* runningSubagentRuns(
160
+ subagentRunsByChatId: Map<string, SubagentRunMap>,
161
+ ): Iterable<SubagentRunSnapshot> {
162
+ for (const map of subagentRunsByChatId.values()) {
163
+ for (const run of map.values()) {
164
+ if (run.status === "running") yield run
165
+ }
166
+ }
167
+ }
168
+
169
+ // ─── Write-path ────────────────────────────────────────────────────────────
170
+
171
+ /**
172
+ * Cap tool_result entries, apply in-memory synchronously (so the UI sees the
173
+ * update immediately), then enqueue the disk append on the write-chain.
174
+ * Mirrors the ephemeral-event optimisation in the original EventStore method.
175
+ */
176
+ export async function appendSubagentEvent(
177
+ deps: AppendSubagentDeps,
178
+ event: SubagentRunEvent,
179
+ ): Promise<void> {
180
+ let effectiveEvent = event
181
+ if (event.type === "subagent_entry_appended" && event.entry.kind === "tool_result") {
182
+ const chat = deps.chatsById.get(event.chatId)
183
+ if (chat) {
184
+ effectiveEvent = {
185
+ ...event,
186
+ entry: await capTranscriptEntry({
187
+ entry: event.entry,
188
+ chatId: event.chatId,
189
+ runId: event.runId,
190
+ projectId: chat.projectId,
191
+ kannaRoot: deps.dataDir,
192
+ }),
193
+ }
194
+ }
195
+ }
196
+ deps.applyEvent(effectiveEvent)
197
+ deps.enqueueDiskAppend(deps.turnsLogPath, `${JSON.stringify(effectiveEvent)}\n`)
198
+ }