@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,235 @@
1
+ /**
2
+ * Pure helper functions extracted from event-store.ts.
3
+ * No EventStore class dependencies. No IO side effects.
4
+ */
5
+ import { log } from "../shared/log"
6
+ import type { ChatHistorySnapshot, SlashCommand, TranscriptEntry } from "../shared/types"
7
+ import { cloneTranscriptEntries } from "./events"
8
+ import type { StoreEvent } from "./events"
9
+
10
+ export interface TranscriptPageResult {
11
+ entries: TranscriptEntry[]
12
+ hasOlder: boolean
13
+ olderCursor: string | null
14
+ }
15
+
16
+ export function normalizeSidebarProjectOrder<T>(value: T): string[] {
17
+ if (!Array.isArray(value)) {
18
+ return []
19
+ }
20
+
21
+ const seen = new Set<string>()
22
+ const projectIds: string[] = []
23
+ for (const entry of value) {
24
+ if (typeof entry !== "string") continue
25
+ const projectId = entry.trim()
26
+ if (!projectId || seen.has(projectId)) continue
27
+ seen.add(projectId)
28
+ projectIds.push(projectId)
29
+ }
30
+
31
+ return projectIds
32
+ }
33
+
34
+ export function isSendToStartingProfilingEnabled(): boolean {
35
+ return process.env.KANNA_PROFILE_SEND_TO_STARTING === "1"
36
+ }
37
+
38
+ export function logSendToStartingProfile(stage: string, details?: Record<string, unknown>): void {
39
+ if (!isSendToStartingProfilingEnabled()) {
40
+ return
41
+ }
42
+
43
+ log.info("[kanna/send->starting][server]", JSON.stringify({
44
+ stage,
45
+ ...details,
46
+ }))
47
+ }
48
+
49
+ export function getReplayEventPriority(event: StoreEvent): number {
50
+ const discriminator = "type" in event ? event.type : event.kind
51
+ switch (discriminator) {
52
+ case "project_opened":
53
+ case "project_removed":
54
+ case "sidebar_project_order_set":
55
+ case "project_star_set":
56
+ return 0
57
+ case "chat_created":
58
+ return 1
59
+ case "chat_renamed":
60
+ case "chat_provider_set":
61
+ case "chat_plan_mode_set":
62
+ return 2
63
+ case "message_appended":
64
+ return 3
65
+ case "queued_message_enqueued":
66
+ case "queued_message_removed":
67
+ return 4
68
+ case "turn_started":
69
+ return 5
70
+ case "session_token_set":
71
+ case "session_commands_loaded":
72
+ return 6
73
+ case "pending_fork_session_token_set":
74
+ return 6
75
+ case "turn_cancelled":
76
+ return 7
77
+ case "turn_finished":
78
+ case "turn_failed":
79
+ return 8
80
+ case "chat_read_state_set":
81
+ case "chat_source_hash_set":
82
+ case "chat_policy_override_set":
83
+ case "chat_compact_failures_set":
84
+ return 9
85
+ case "chat_deleted":
86
+ case "chat_archived":
87
+ case "chat_unarchived":
88
+ return 10
89
+ case "auto_continue_proposed":
90
+ case "auto_continue_accepted":
91
+ case "auto_continue_rescheduled":
92
+ case "auto_continue_cancelled":
93
+ case "auto_continue_fired":
94
+ case "loop_armed":
95
+ case "loop_disarmed":
96
+ return 11
97
+ case "stack_added":
98
+ case "stack_removed":
99
+ case "stack_renamed":
100
+ case "stack_project_added":
101
+ case "stack_project_removed":
102
+ return 0
103
+ case "subagent_run_started":
104
+ case "subagent_message_delta":
105
+ case "subagent_entry_appended":
106
+ case "subagent_run_completed":
107
+ case "subagent_run_failed":
108
+ case "subagent_run_cancelled":
109
+ case "subagent_tool_pending":
110
+ case "subagent_tool_resolved":
111
+ return 5
112
+ // tool_request_put shares priority 5 with subagent_* events; sourceIndex
113
+ // tie-break orders them (tool-requests has sourceIndex 7, turns has 5).
114
+ case "tool_request_put":
115
+ return 5
116
+ case "tool_request_resolved":
117
+ return 6
118
+ case "orch_run_created":
119
+ case "orch_worktree_provisioned":
120
+ case "orch_worktree_init_started":
121
+ case "orch_worktree_init_completed":
122
+ case "orch_task_claimed":
123
+ case "orch_phase_started":
124
+ case "orch_phase_completed":
125
+ case "orch_gate_opened":
126
+ case "orch_gate_resolved":
127
+ case "orch_scope_overlap_flagged":
128
+ case "orch_config_warning":
129
+ case "orch_verify_started":
130
+ case "orch_verify_completed":
131
+ case "orch_task_committed":
132
+ case "orch_task_failed":
133
+ case "orch_task_requeued":
134
+ case "orch_run_completed":
135
+ case "orch_run_cancelled":
136
+ return 5
137
+ default: {
138
+ const _exhaustive: never = discriminator
139
+ throw new Error(`Unhandled replay event type: ${String(_exhaustive)}`)
140
+ }
141
+ }
142
+ }
143
+
144
+ export function encodeHistoryCursor(index: number): string {
145
+ return `idx:${index}`
146
+ }
147
+
148
+ export function decodeCursor(cursor: string): number {
149
+ if (cursor.startsWith("idx:")) {
150
+ const value = Number.parseInt(cursor.slice("idx:".length), 10)
151
+ if (!Number.isInteger(value) || value < 0) {
152
+ throw new Error("Invalid history cursor")
153
+ }
154
+ return value
155
+ }
156
+
157
+ throw new Error("Invalid history cursor")
158
+ }
159
+
160
+ export function slashCommandsEqual(a: SlashCommand[], b: SlashCommand[]): boolean {
161
+ if (a.length !== b.length) return false
162
+ for (let i = 0; i < a.length; i += 1) {
163
+ const ai = a[i]
164
+ const bi = b[i]
165
+ if (ai.name !== bi.name || ai.description !== bi.description || ai.argumentHint !== bi.argumentHint) {
166
+ return false
167
+ }
168
+ }
169
+ return true
170
+ }
171
+
172
+ // `context_window_updated` entries are token-readout noise emitted once per
173
+ // stream tick — only the latest value matters. A single turn can emit hundreds
174
+ // of them, and the bounded live window (getMessagesPageFromEntries) counts each
175
+ // 1:1 against the limit, so a flood evicts real turns (e.g. the user_prompt)
176
+ // out of the snapshot the client renders. Collapsing each maximal run of
177
+ // consecutive cwu entries to its last entry keeps the latest readout while
178
+ // freeing window budget for real turns. Applied only on the live-window read
179
+ // path (not getMessages), so getLatestContextWindowUsage / full-transcript
180
+ // export / importer still observe every persisted cwu.
181
+ export function coalesceContextWindowUpdates(entries: TranscriptEntry[]): TranscriptEntry[] {
182
+ const result: TranscriptEntry[] = []
183
+ for (let index = 0; index < entries.length; index += 1) {
184
+ const entry = entries[index]!
185
+ const next = entries[index + 1]
186
+ if (entry.kind === "context_window_updated" && next?.kind === "context_window_updated") {
187
+ // Drop this cwu; the last of the run survives.
188
+ continue
189
+ }
190
+ result.push(entry)
191
+ }
192
+ return result
193
+ }
194
+
195
+ export function getHistorySnapshot(page: TranscriptPageResult, recentLimit: number): ChatHistorySnapshot {
196
+ return {
197
+ hasOlder: page.hasOlder,
198
+ olderCursor: page.olderCursor,
199
+ recentLimit,
200
+ }
201
+ }
202
+
203
+ export function getForkedChatTitle(title: string): string {
204
+ const trimmed = title.trim()
205
+ if (!trimmed) return "Fork: New Chat"
206
+ return trimmed.startsWith("Fork: ") ? trimmed : `Fork: ${trimmed}`
207
+ }
208
+
209
+ /**
210
+ * Slice a page of transcript entries from a flat array.
211
+ *
212
+ * - `limit` controls page size; `beforeIndex` (from a cursor) is the
213
+ * exclusive upper bound of the window (undefined = latest page).
214
+ * - Returns stable `TranscriptPageResult` with cloned entries so
215
+ * callers cannot mutate the in-memory transcript.
216
+ */
217
+ export function getMessagesPageFromEntries(
218
+ entries: TranscriptEntry[],
219
+ limit: number,
220
+ beforeIndex?: number,
221
+ ): TranscriptPageResult {
222
+ if (entries.length === 0) {
223
+ return { entries: [], hasOlder: false, olderCursor: null }
224
+ }
225
+ const endIndex =
226
+ beforeIndex === undefined
227
+ ? entries.length
228
+ : Math.max(0, Math.min(beforeIndex, entries.length))
229
+ const startIndex = Math.max(0, endIndex - limit)
230
+ return {
231
+ entries: cloneTranscriptEntries(entries.slice(startIndex, endIndex)),
232
+ hasOlder: startIndex > 0,
233
+ olderCursor: startIndex > 0 ? encodeHistoryCursor(startIndex) : null,
234
+ }
235
+ }
@@ -0,0 +1,276 @@
1
+ /**
2
+ * EventStore initialization and snapshot/migration helpers extracted from
3
+ * event-store.ts.
4
+ *
5
+ * Contains initialize, ensureFile, clearStorage, loadSnapshot, resetState,
6
+ * clearLegacyTranscriptState, loadSidebarProjectOrder, replayLogs,
7
+ * shouldSnapshotLogs, snapshotAndTruncateLogs, getLegacyTranscriptStats,
8
+ * hasLegacyTranscriptData, and migrateLegacyTranscripts.
9
+ *
10
+ * This file does direct disk IO and must be suffixed .ts (not .adapter.ts)
11
+ * because the side-effect seal only applies to files in src/server/** production
12
+ * code that are NOT in an exempt glob. However, since this file performs IO and
13
+ * is NOT an adapter, callers (EventStore) must be careful to only call these
14
+ * from the class constructor / initialize flow.
15
+ *
16
+ * Must NOT import from event-store.ts (no circular deps).
17
+ */
18
+ import path from "node:path"
19
+ import type { AgentProvider, TranscriptEntry } from "../shared/types"
20
+ import type { StoreState } from "./events"
21
+ import type { StorageBackend } from "./storage/backend"
22
+ import type { CachedTranscriptRef } from "./event-store-messages.adapter"
23
+ import type { CloudflareTunnelEvent } from "./cloudflare-tunnel/events"
24
+ import {
25
+ buildSnapshotFile,
26
+ calcShouldTruncateLogs,
27
+ computeLegacyTranscriptStats,
28
+ loadAndReplayLogs,
29
+ loadSidebarOrder,
30
+ loadSnapshotIntoState,
31
+ migrateLegacyTranscripts as migrateLegacyTranscriptsImpl,
32
+ truncateLogsAfterSnapshot,
33
+ type LegacyTranscriptStats,
34
+ type SnapshotLogPaths,
35
+ } from "./event-store-snapshot"
36
+
37
+ export type { LegacyTranscriptStats }
38
+
39
+ // ─── Deps interface ────────────────────────────────────────────────────────
40
+
41
+ export interface EventStoreInitDeps {
42
+ readonly storage: StorageBackend
43
+ readonly dataDir: string
44
+ readonly snapshotPath: string
45
+ readonly projectsLogPath: string
46
+ readonly chatsLogPath: string
47
+ readonly messagesLogPath: string
48
+ readonly queuedMessagesLogPath: string
49
+ readonly turnsLogPath: string
50
+ readonly schedulesLogPath: string
51
+ readonly tunnelLogPath: string
52
+ readonly sharesLogPath: string
53
+ readonly pushLogPath: string
54
+ readonly stacksLogPath: string
55
+ readonly toolRequestsLogPath: string
56
+ readonly orchLogPath: string
57
+ readonly transcriptsDir: string
58
+ readonly sidebarProjectOrderPath: string
59
+ readonly state: StoreState
60
+ readonly legacyMessagesByChatId: Map<string, TranscriptEntry[]>
61
+ readonly tunnelEventsByChatId: Map<string, CloudflareTunnelEvent[]>
62
+ readonly cachedTranscriptRef: CachedTranscriptRef
63
+ readonly sidebarProjectOrderRef: { value: string[] }
64
+ /** Gets the current legacySidebarProjectOrder array reference. */
65
+ getLegacySidebarProjectOrder: () => string[]
66
+ /** Sets legacySidebarProjectOrder. */
67
+ setLegacySidebarProjectOrder: (v: string[]) => void
68
+ /** Gets the current snapshotHasLegacyMessages value. */
69
+ getSnapshotHasLegacyMessages: () => boolean
70
+ /** Sets snapshotHasLegacyMessages. */
71
+ setSnapshotHasLegacyMessages: (v: boolean) => void
72
+ /** Gets the current storageReset flag. */
73
+ getStorageReset: () => boolean
74
+ /** Sets the storageReset flag. */
75
+ setStorageReset: (v: boolean) => void
76
+ /** The replayChatProvider map for log replay. */
77
+ replayChatProvider: Map<string, AgentProvider | null>
78
+ /** Applies a store event (routes to applyStoreEvent). */
79
+ applyEvent: (event: Parameters<typeof import("./event-store-apply").applyStoreEvent>[0]) => void
80
+ }
81
+
82
+ // ─── Internal helpers ──────────────────────────────────────────────────────
83
+
84
+ function getLogPaths(deps: EventStoreInitDeps): SnapshotLogPaths {
85
+ return {
86
+ snapshotPath: deps.snapshotPath,
87
+ projectsLogPath: deps.projectsLogPath,
88
+ chatsLogPath: deps.chatsLogPath,
89
+ messagesLogPath: deps.messagesLogPath,
90
+ queuedMessagesLogPath: deps.queuedMessagesLogPath,
91
+ turnsLogPath: deps.turnsLogPath,
92
+ schedulesLogPath: deps.schedulesLogPath,
93
+ stacksLogPath: deps.stacksLogPath,
94
+ toolRequestsLogPath: deps.toolRequestsLogPath,
95
+ orchLogPath: deps.orchLogPath,
96
+ }
97
+ }
98
+
99
+ async function ensureFile(deps: EventStoreInitDeps, filePath: string): Promise<void> {
100
+ if (!(await deps.storage.exists(filePath))) {
101
+ await deps.storage.writeText(filePath, "")
102
+ }
103
+ }
104
+
105
+ function resetState(deps: EventStoreInitDeps): void {
106
+ deps.state.projectsById.clear()
107
+ deps.state.projectIdsByPath.clear()
108
+ deps.state.chatsById.clear()
109
+ deps.state.queuedMessagesByChatId.clear()
110
+ deps.state.sidebarProjectOrder = []
111
+ deps.state.autoContinueEventsByChatId.clear()
112
+ deps.state.stacksById.clear()
113
+ deps.tunnelEventsByChatId.clear()
114
+ deps.sidebarProjectOrderRef.value = []
115
+ deps.setLegacySidebarProjectOrder([])
116
+ deps.cachedTranscriptRef.value = null
117
+ }
118
+
119
+ function clearLegacyTranscriptState(deps: EventStoreInitDeps): void {
120
+ deps.legacyMessagesByChatId.clear()
121
+ deps.setSnapshotHasLegacyMessages(false)
122
+ }
123
+
124
+ export async function clearStorage(deps: EventStoreInitDeps): Promise<void> {
125
+ if (deps.getStorageReset()) return
126
+ deps.setStorageReset(true)
127
+ resetState(deps)
128
+ clearLegacyTranscriptState(deps)
129
+ await Promise.all([
130
+ deps.storage.writeText(deps.snapshotPath, ""),
131
+ deps.storage.writeText(deps.projectsLogPath, ""),
132
+ deps.storage.writeText(deps.chatsLogPath, ""),
133
+ deps.storage.writeText(deps.messagesLogPath, ""),
134
+ deps.storage.writeText(deps.queuedMessagesLogPath, ""),
135
+ deps.storage.writeText(deps.turnsLogPath, ""),
136
+ deps.storage.writeText(deps.schedulesLogPath, ""),
137
+ deps.storage.writeText(deps.tunnelLogPath, ""),
138
+ deps.storage.writeText(deps.sharesLogPath, ""),
139
+ deps.storage.writeText(deps.stacksLogPath, ""),
140
+ deps.storage.writeText(deps.toolRequestsLogPath, ""),
141
+ deps.storage.writeText(deps.orchLogPath, ""),
142
+ ])
143
+ }
144
+
145
+ async function loadSnapshot(deps: EventStoreInitDeps): Promise<void> {
146
+ const result = await loadSnapshotIntoState(
147
+ deps.storage,
148
+ deps.snapshotPath,
149
+ deps.state,
150
+ deps.legacyMessagesByChatId,
151
+ () => clearStorage(deps),
152
+ )
153
+ deps.setSnapshotHasLegacyMessages(result.snapshotHasLegacyMessages)
154
+ deps.setLegacySidebarProjectOrder(result.legacySidebarProjectOrder)
155
+ }
156
+
157
+ async function loadSidebarProjectOrder(deps: EventStoreInitDeps): Promise<void> {
158
+ deps.sidebarProjectOrderRef.value = await loadSidebarOrder(
159
+ deps.storage,
160
+ deps.sidebarProjectOrderPath,
161
+ deps.projectsLogPath,
162
+ deps.dataDir,
163
+ deps.getLegacySidebarProjectOrder(),
164
+ )
165
+ }
166
+
167
+ async function replayLogs(deps: EventStoreInitDeps): Promise<void> {
168
+ await loadAndReplayLogs(
169
+ deps.storage,
170
+ getLogPaths(deps),
171
+ () => deps.getStorageReset(),
172
+ (event) => { deps.applyEvent(event) },
173
+ () => clearStorage(deps),
174
+ () => { deps.replayChatProvider.clear() },
175
+ )
176
+ }
177
+
178
+ export async function shouldSnapshotLogs(deps: EventStoreInitDeps): Promise<boolean> {
179
+ return calcShouldTruncateLogs(deps.storage, getLogPaths(deps))
180
+ }
181
+
182
+ // ─── Public API ────────────────────────────────────────────────────────────
183
+
184
+ export async function initializeEventStore(
185
+ deps: EventStoreInitDeps,
186
+ callbacks: {
187
+ loadTunnelEvents: () => Promise<void>
188
+ loadShareEvents: () => Promise<void>
189
+ hasLegacyTranscriptData: () => Promise<boolean>
190
+ snapshotAndTruncateLogs: () => Promise<void>
191
+ },
192
+ ): Promise<void> {
193
+ await deps.storage.mkdir(deps.dataDir)
194
+ await deps.storage.mkdir(deps.transcriptsDir)
195
+ await ensureFile(deps, deps.projectsLogPath)
196
+ await ensureFile(deps, deps.chatsLogPath)
197
+ await ensureFile(deps, deps.messagesLogPath)
198
+ await ensureFile(deps, deps.queuedMessagesLogPath)
199
+ await ensureFile(deps, deps.turnsLogPath)
200
+ await ensureFile(deps, deps.schedulesLogPath)
201
+ await ensureFile(deps, deps.tunnelLogPath)
202
+ await ensureFile(deps, deps.sharesLogPath)
203
+ await ensureFile(deps, deps.pushLogPath)
204
+ await ensureFile(deps, deps.stacksLogPath)
205
+ await ensureFile(deps, deps.toolRequestsLogPath)
206
+ await ensureFile(deps, deps.orchLogPath)
207
+ await loadSnapshot(deps)
208
+ await replayLogs(deps)
209
+ await callbacks.loadTunnelEvents()
210
+ await callbacks.loadShareEvents()
211
+ await loadSidebarProjectOrder(deps)
212
+ if (!(await callbacks.hasLegacyTranscriptData()) && await shouldSnapshotLogs(deps)) {
213
+ await callbacks.snapshotAndTruncateLogs()
214
+ }
215
+ }
216
+
217
+ export function resetEventStoreState(deps: EventStoreInitDeps): void {
218
+ resetState(deps)
219
+ }
220
+
221
+ export function clearEventStoreLegacyTranscriptState(deps: EventStoreInitDeps): void {
222
+ clearLegacyTranscriptState(deps)
223
+ }
224
+
225
+ // ─── Snapshot / legacy migration ops ─────────────────────────────────────────
226
+
227
+ export async function getLegacyTranscriptStats(
228
+ deps: EventStoreInitDeps,
229
+ ): Promise<LegacyTranscriptStats> {
230
+ return computeLegacyTranscriptStats(
231
+ deps.storage,
232
+ deps.messagesLogPath,
233
+ deps.getSnapshotHasLegacyMessages(),
234
+ deps.legacyMessagesByChatId,
235
+ )
236
+ }
237
+
238
+ export async function hasLegacyTranscriptData(deps: EventStoreInitDeps): Promise<boolean> {
239
+ return (await getLegacyTranscriptStats(deps)).hasLegacyData
240
+ }
241
+
242
+ export async function snapshotAndTruncateLogs(deps: EventStoreInitDeps): Promise<void> {
243
+ const projects = [...deps.state.projectsById.values()].filter((p) => !p.deletedAt)
244
+ const snapshot = buildSnapshotFile(deps.state, projects)
245
+ const logPaths: SnapshotLogPaths = {
246
+ snapshotPath: deps.snapshotPath,
247
+ projectsLogPath: deps.projectsLogPath,
248
+ chatsLogPath: deps.chatsLogPath,
249
+ messagesLogPath: deps.messagesLogPath,
250
+ queuedMessagesLogPath: deps.queuedMessagesLogPath,
251
+ turnsLogPath: deps.turnsLogPath,
252
+ schedulesLogPath: deps.schedulesLogPath,
253
+ stacksLogPath: deps.stacksLogPath,
254
+ toolRequestsLogPath: deps.toolRequestsLogPath,
255
+ orchLogPath: deps.orchLogPath,
256
+ }
257
+ await truncateLogsAfterSnapshot(deps.storage, logPaths, JSON.stringify(snapshot, null, 2))
258
+ }
259
+
260
+ export async function migrateLegacyTranscripts(
261
+ deps: EventStoreInitDeps,
262
+ onProgress?: (message: string) => void,
263
+ ): Promise<boolean> {
264
+ const stats = await getLegacyTranscriptStats(deps)
265
+ return migrateLegacyTranscriptsImpl(
266
+ deps.storage,
267
+ deps.transcriptsDir,
268
+ stats,
269
+ deps.legacyMessagesByChatId,
270
+ (chatId) => path.join(deps.transcriptsDir, `${chatId}.jsonl`),
271
+ () => { clearEventStoreLegacyTranscriptState(deps) },
272
+ () => snapshotAndTruncateLogs(deps),
273
+ () => { deps.cachedTranscriptRef.value = null },
274
+ onProgress,
275
+ )
276
+ }