@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,441 @@
1
+ /**
2
+ * Pure read-model functions for project, stack, chat-lifecycle, turn, queued-
3
+ * message, and auto-continue event application.
4
+ *
5
+ * Extracted from event-store.ts to reduce file size. All functions are pure
6
+ * state mutations — no IO, no side effects (side-effect seal: IO lives in
7
+ * *.adapter.ts files).
8
+ */
9
+
10
+ import type { AgentProvider, SubagentRunSnapshot, TranscriptEntry } from "../shared/types"
11
+ import type {
12
+ ChatEvent,
13
+ ChatRecord,
14
+ ChatTimingState,
15
+ ProjectEvent,
16
+ ProjectRecord,
17
+ QueuedMessageEvent,
18
+ StackEvent,
19
+ StackRecord,
20
+ StoreState,
21
+ TurnEvent,
22
+ } from "./events"
23
+ import type { AutoContinueEvent } from "./auto-continue/events"
24
+ import { ACTIVE_SESSION_IDLE_GAP_MS } from "./read-models"
25
+ import { resolveLocalPath } from "./paths"
26
+
27
+ // ─── Project read-model ────────────────────────────────────────────────────
28
+
29
+ type ProjectLifecycleState = Pick<StoreState, "projectsById" | "projectIdsByPath" | "sidebarProjectOrder">
30
+
31
+ /**
32
+ * Apply a single ProjectEvent to the in-memory project maps. Mutates the
33
+ * maps in-place; reassigns `state.sidebarProjectOrder` for the
34
+ * `sidebar_project_order_set` case.
35
+ */
36
+ export function applyProjectEvent(state: ProjectLifecycleState, event: ProjectEvent): void {
37
+ switch (event.type) {
38
+ case "project_opened": {
39
+ const localPath = resolveLocalPath(event.localPath)
40
+ const project: ProjectRecord = {
41
+ id: event.projectId,
42
+ localPath,
43
+ title: event.title,
44
+ createdAt: event.timestamp,
45
+ updatedAt: event.timestamp,
46
+ }
47
+ state.projectsById.set(project.id, project)
48
+ state.projectIdsByPath.set(localPath, project.id)
49
+ break
50
+ }
51
+ case "project_removed": {
52
+ const project = state.projectsById.get(event.projectId)
53
+ if (!project) break
54
+ project.deletedAt = event.timestamp
55
+ project.updatedAt = event.timestamp
56
+ state.projectIdsByPath.delete(project.localPath)
57
+ break
58
+ }
59
+ case "sidebar_project_order_set": {
60
+ state.sidebarProjectOrder = [...event.projectIds]
61
+ break
62
+ }
63
+ case "project_star_set": {
64
+ const project = state.projectsById.get(event.projectId)
65
+ if (!project) break
66
+ if (event.starredAt == null) {
67
+ delete project.starredAt
68
+ } else {
69
+ project.starredAt = event.starredAt
70
+ }
71
+ project.updatedAt = event.timestamp
72
+ break
73
+ }
74
+ }
75
+ }
76
+
77
+ // ─── Stack read-model ──────────────────────────────────────────────────────
78
+
79
+ /**
80
+ * Apply a single StackEvent to the in-memory stacks map. Mutates in-place.
81
+ */
82
+ export function applyStackEvent(stacksById: Map<string, StackRecord>, event: StackEvent): void {
83
+ switch (event.type) {
84
+ case "stack_added": {
85
+ const record: StackRecord = {
86
+ id: event.stackId,
87
+ title: event.title,
88
+ projectIds: [...event.projectIds],
89
+ createdAt: event.timestamp,
90
+ updatedAt: event.timestamp,
91
+ }
92
+ stacksById.set(record.id, record)
93
+ break
94
+ }
95
+ case "stack_removed": {
96
+ const stack = stacksById.get(event.stackId)
97
+ if (!stack || stack.deletedAt) break
98
+ stack.deletedAt = event.timestamp
99
+ stack.updatedAt = event.timestamp
100
+ break
101
+ }
102
+ case "stack_renamed": {
103
+ const stack = stacksById.get(event.stackId)
104
+ if (!stack || stack.deletedAt) break
105
+ stack.title = event.title
106
+ stack.updatedAt = event.timestamp
107
+ break
108
+ }
109
+ case "stack_project_added": {
110
+ const stack = stacksById.get(event.stackId)
111
+ if (!stack || stack.deletedAt) break
112
+ if (stack.projectIds.includes(event.projectId)) break
113
+ stack.projectIds = [...stack.projectIds, event.projectId]
114
+ stack.updatedAt = event.timestamp
115
+ break
116
+ }
117
+ case "stack_project_removed": {
118
+ const stack = stacksById.get(event.stackId)
119
+ if (!stack || stack.deletedAt) break
120
+ stack.projectIds = stack.projectIds.filter((id) => id !== event.projectId)
121
+ stack.updatedAt = event.timestamp
122
+ break
123
+ }
124
+ }
125
+ }
126
+
127
+ // ─── Chat timing ───────────────────────────────────────────────────────────
128
+
129
+ /**
130
+ * Update the ChatTimingState for a given chat in response to a status
131
+ * transition. Seeds the entry on first call (chat_created path).
132
+ *
133
+ * @param chatTimingsByChatId - mutable timing map from StoreState
134
+ * @param chatId - target chat
135
+ * @param eventTs - event timestamp (ms epoch)
136
+ * @param nextStatus - status to transition into
137
+ * @param onTurnStart - true when this event marks turn start
138
+ * @param onTurnFinish - true when this event marks turn end
139
+ */
140
+ export function updateChatTiming(
141
+ chatTimingsByChatId: Map<string, ChatTimingState>,
142
+ chatId: string,
143
+ eventTs: number,
144
+ nextStatus: ChatTimingState["status"],
145
+ onTurnStart?: boolean,
146
+ onTurnFinish?: boolean,
147
+ ): void {
148
+ const prev = chatTimingsByChatId.get(chatId)
149
+ if (!prev) {
150
+ // chat_created path: seed initial timing entry
151
+ chatTimingsByChatId.set(chatId, {
152
+ status: nextStatus,
153
+ stateEnteredAt: eventTs,
154
+ activeSessionStartedAt: eventTs,
155
+ lastTurnStartedAt: null,
156
+ lastTurnDurationMs: null,
157
+ cumulativeMs: { idle: 0, starting: 0, running: 0, failed: 0 },
158
+ })
159
+ return
160
+ }
161
+
162
+ const segmentMs = Math.max(0, eventTs - prev.stateEnteredAt)
163
+ let activeSessionStartedAt = prev.activeSessionStartedAt
164
+ let cumulativeMs = { ...prev.cumulativeMs }
165
+
166
+ // Detect long idle gap when leaving idle → something else
167
+ if (prev.status === "idle" && nextStatus !== "idle" && segmentMs > ACTIVE_SESSION_IDLE_GAP_MS) {
168
+ activeSessionStartedAt = eventTs
169
+ cumulativeMs = { idle: 0, starting: 0, running: 0, failed: 0 }
170
+ } else {
171
+ cumulativeMs[prev.status] += segmentMs
172
+ }
173
+
174
+ let lastTurnStartedAt = prev.lastTurnStartedAt
175
+ let lastTurnDurationMs = prev.lastTurnDurationMs
176
+ if (onTurnStart) lastTurnStartedAt = eventTs
177
+ if (onTurnFinish && lastTurnStartedAt != null) lastTurnDurationMs = Math.max(0, eventTs - lastTurnStartedAt)
178
+
179
+ chatTimingsByChatId.set(chatId, {
180
+ status: nextStatus,
181
+ stateEnteredAt: eventTs,
182
+ activeSessionStartedAt,
183
+ lastTurnStartedAt,
184
+ lastTurnDurationMs,
185
+ cumulativeMs,
186
+ })
187
+ }
188
+
189
+ // ─── Chat / Turn / QueuedMessage lifecycle ─────────────────────────────────
190
+
191
+ type ChatLifecycleState = Pick<
192
+ StoreState,
193
+ "chatsById" | "queuedMessagesByChatId" | "autoContinueEventsByChatId" | "chatTimingsByChatId" | "subagentRunsByChatId"
194
+ >
195
+
196
+ /**
197
+ * Apply a single ChatEvent, TurnEvent, or QueuedMessageEvent to the in-memory
198
+ * chat state. Mutates maps in-place.
199
+ *
200
+ * NOTE: `MessageEvent` (`message_appended`) is intentionally excluded because
201
+ * it also mutates `legacyMessagesByChatId` — a class field not in StoreState.
202
+ * That case remains inline in EventStore.applyEvent().
203
+ *
204
+ * @param state - chat-related maps from StoreState
205
+ * @param replayChatProvider - class-level provider-replay map (not in StoreState)
206
+ * @param event - the event to apply
207
+ */
208
+ export function applyChatLifecycleEvent(
209
+ state: ChatLifecycleState,
210
+ replayChatProvider: Map<string, AgentProvider | null>,
211
+ event: ChatEvent | TurnEvent | QueuedMessageEvent,
212
+ ): void {
213
+ switch (event.type) {
214
+ case "chat_created": {
215
+ const chat: ChatRecord = {
216
+ id: event.chatId,
217
+ projectId: event.projectId,
218
+ title: event.title,
219
+ createdAt: event.timestamp,
220
+ updatedAt: event.timestamp,
221
+ unread: false,
222
+ provider: null,
223
+ planMode: false,
224
+ sessionTokensByProvider: {},
225
+ sourceHash: null,
226
+ pendingForkSessionToken: null,
227
+ hasMessages: false,
228
+ lastTurnOutcome: null,
229
+ }
230
+ if (event.stackId !== undefined) chat.stackId = event.stackId
231
+ if (event.stackBindings !== undefined) chat.stackBindings = event.stackBindings.map((b) => ({ ...b }))
232
+ state.chatsById.set(chat.id, chat)
233
+ replayChatProvider.set(event.chatId, null)
234
+ state.subagentRunsByChatId.set(event.chatId, new Map<string, SubagentRunSnapshot>())
235
+ updateChatTiming(state.chatTimingsByChatId, event.chatId, event.timestamp, "idle")
236
+ break
237
+ }
238
+ case "chat_renamed": {
239
+ const chat = state.chatsById.get(event.chatId)
240
+ if (!chat) break
241
+ chat.title = event.title
242
+ chat.updatedAt = event.timestamp
243
+ break
244
+ }
245
+ case "chat_deleted": {
246
+ const chat = state.chatsById.get(event.chatId)
247
+ if (!chat) break
248
+ chat.deletedAt = event.timestamp
249
+ chat.updatedAt = event.timestamp
250
+ state.queuedMessagesByChatId.delete(event.chatId)
251
+ state.autoContinueEventsByChatId.delete(event.chatId)
252
+ state.chatTimingsByChatId.delete(event.chatId)
253
+ state.subagentRunsByChatId.delete(event.chatId)
254
+ break
255
+ }
256
+ case "chat_archived": {
257
+ const chat = state.chatsById.get(event.chatId)
258
+ if (!chat) break
259
+ chat.archivedAt = event.timestamp
260
+ chat.updatedAt = event.timestamp
261
+ break
262
+ }
263
+ case "chat_unarchived": {
264
+ const chat = state.chatsById.get(event.chatId)
265
+ if (!chat) break
266
+ delete chat.archivedAt
267
+ chat.updatedAt = event.timestamp
268
+ break
269
+ }
270
+ case "chat_provider_set": {
271
+ const chat = state.chatsById.get(event.chatId)
272
+ if (!chat) break
273
+ chat.provider = event.provider
274
+ chat.updatedAt = event.timestamp
275
+ replayChatProvider.set(event.chatId, event.provider)
276
+ break
277
+ }
278
+ case "chat_plan_mode_set": {
279
+ const chat = state.chatsById.get(event.chatId)
280
+ if (!chat) break
281
+ chat.planMode = event.planMode
282
+ chat.updatedAt = event.timestamp
283
+ break
284
+ }
285
+ case "chat_read_state_set": {
286
+ const chat = state.chatsById.get(event.chatId)
287
+ if (!chat) break
288
+ chat.unread = event.unread
289
+ chat.updatedAt = event.timestamp
290
+ break
291
+ }
292
+ case "chat_source_hash_set": {
293
+ const chat = state.chatsById.get(event.chatId)
294
+ if (!chat) break
295
+ chat.sourceHash = event.sourceHash
296
+ chat.updatedAt = event.timestamp
297
+ break
298
+ }
299
+ case "chat_policy_override_set": {
300
+ const chat = state.chatsById.get(event.chatId)
301
+ if (!chat) break
302
+ chat.policyOverride = event.policyOverride
303
+ chat.updatedAt = event.timestamp
304
+ break
305
+ }
306
+ case "chat_compact_failures_set": {
307
+ const chat = state.chatsById.get(event.chatId)
308
+ if (!chat) break
309
+ chat.compactFailureCount = event.compactFailureCount
310
+ chat.updatedAt = event.timestamp
311
+ break
312
+ }
313
+ case "queued_message_enqueued": {
314
+ const existing = state.queuedMessagesByChatId.get(event.chatId) ?? []
315
+ existing.push({
316
+ ...event.message,
317
+ attachments: [...event.message.attachments],
318
+ })
319
+ state.queuedMessagesByChatId.set(event.chatId, existing)
320
+ const chat = state.chatsById.get(event.chatId)
321
+ if (chat) {
322
+ chat.updatedAt = event.timestamp
323
+ }
324
+ break
325
+ }
326
+ case "queued_message_removed": {
327
+ const existing = state.queuedMessagesByChatId.get(event.chatId) ?? []
328
+ const next = existing.filter((entry) => entry.id !== event.queuedMessageId)
329
+ if (next.length > 0) {
330
+ state.queuedMessagesByChatId.set(event.chatId, next)
331
+ } else {
332
+ state.queuedMessagesByChatId.delete(event.chatId)
333
+ }
334
+ const chat = state.chatsById.get(event.chatId)
335
+ if (chat) {
336
+ chat.updatedAt = event.timestamp
337
+ }
338
+ break
339
+ }
340
+ case "turn_started": {
341
+ const chat = state.chatsById.get(event.chatId)
342
+ if (!chat) break
343
+ chat.updatedAt = event.timestamp
344
+ updateChatTiming(state.chatTimingsByChatId, event.chatId, event.timestamp, "running", true, false)
345
+ break
346
+ }
347
+ case "turn_finished": {
348
+ const chat = state.chatsById.get(event.chatId)
349
+ if (!chat) break
350
+ chat.updatedAt = event.timestamp
351
+ chat.unread = true
352
+ chat.lastTurnOutcome = "success"
353
+ updateChatTiming(state.chatTimingsByChatId, event.chatId, event.timestamp, "idle", false, true)
354
+ break
355
+ }
356
+ case "turn_failed": {
357
+ const chat = state.chatsById.get(event.chatId)
358
+ if (!chat) break
359
+ chat.updatedAt = event.timestamp
360
+ chat.unread = true
361
+ chat.lastTurnOutcome = "failed"
362
+ updateChatTiming(state.chatTimingsByChatId, event.chatId, event.timestamp, "failed", false, true)
363
+ break
364
+ }
365
+ case "turn_cancelled": {
366
+ const chat = state.chatsById.get(event.chatId)
367
+ if (!chat) break
368
+ chat.updatedAt = event.timestamp
369
+ chat.lastTurnOutcome = "cancelled"
370
+ updateChatTiming(state.chatTimingsByChatId, event.chatId, event.timestamp, "idle", false, true)
371
+ break
372
+ }
373
+ case "session_token_set": {
374
+ const chat = state.chatsById.get(event.chatId)
375
+ if (!chat) break
376
+ const provider = event.provider ?? replayChatProvider.get(event.chatId) ?? chat.provider
377
+ if (!provider) break
378
+ chat.sessionTokensByProvider = {
379
+ ...chat.sessionTokensByProvider,
380
+ [provider]: event.sessionToken,
381
+ }
382
+ chat.updatedAt = event.timestamp
383
+ break
384
+ }
385
+ case "session_commands_loaded": {
386
+ const chat = state.chatsById.get(event.chatId)
387
+ if (!chat) break
388
+ chat.slashCommands = event.commands.map((c) => ({ ...c }))
389
+ chat.updatedAt = event.timestamp
390
+ break
391
+ }
392
+ case "pending_fork_session_token_set": {
393
+ const chat = state.chatsById.get(event.chatId)
394
+ if (!chat) break
395
+ if (event.pendingForkSessionToken == null) {
396
+ chat.pendingForkSessionToken = null
397
+ } else {
398
+ const provider = event.provider ?? replayChatProvider.get(event.chatId) ?? chat.provider
399
+ if (!provider) break
400
+ chat.pendingForkSessionToken = { provider, token: event.pendingForkSessionToken }
401
+ }
402
+ chat.updatedAt = event.timestamp
403
+ break
404
+ }
405
+ }
406
+ }
407
+
408
+ // ─── Auto-continue read-model ──────────────────────────────────────────────
409
+
410
+ /**
411
+ * Push an AutoContinueEvent onto the per-chat event list. Pure map mutation.
412
+ */
413
+ export function applyAutoContinueToState(
414
+ autoContinueEventsByChatId: Map<string, AutoContinueEvent[]>,
415
+ event: AutoContinueEvent,
416
+ ): void {
417
+ const existing = autoContinueEventsByChatId.get(event.chatId) ?? []
418
+ existing.push(event)
419
+ autoContinueEventsByChatId.set(event.chatId, existing)
420
+ }
421
+
422
+ // ─── Transcript message metadata ───────────────────────────────────────────
423
+
424
+ /**
425
+ * Update chat metadata fields derived from a newly appended transcript entry.
426
+ * Called for `message_appended` events (which also mutate legacyMessagesByChatId
427
+ * and therefore stay partially inline in EventStore).
428
+ */
429
+ export function applyChatMessageMetadata(
430
+ chatsById: Map<string, ChatRecord>,
431
+ chatId: string,
432
+ entry: TranscriptEntry,
433
+ ): void {
434
+ const chat = chatsById.get(chatId)
435
+ if (!chat) return
436
+ chat.hasMessages = true
437
+ if (entry.kind === "user_prompt") {
438
+ chat.lastMessageAt = entry.createdAt
439
+ }
440
+ chat.updatedAt = Math.max(chat.updatedAt, entry.createdAt)
441
+ }