@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,356 @@
1
+ /**
2
+ * Project, stack, chat entity write operations and session/turn write ops
3
+ * extracted from event-store.ts.
4
+ *
5
+ * All functions follow the pattern: build an event using a builder from
6
+ * event-store-write-ops, then delegate to `deps.append`. This file is pure
7
+ * (no direct disk IO), so it does NOT carry the .adapter.ts suffix.
8
+ *
9
+ * Must NOT import from event-store.ts (no circular deps).
10
+ */
11
+ import type { AgentProvider, QueuedChatMessage, SlashCommand, StackBinding } from "../shared/types"
12
+ import type { ChatPermissionPolicyOverride } from "../shared/permission-policy"
13
+ import type { AutoContinueEvent } from "./auto-continue/events"
14
+ import type { ChatRecord, StackRecord, StoreEvent, StoreState, TurnRunConfig } from "./events"
15
+ import type { StorageBackend } from "./storage/backend"
16
+ import {
17
+ buildAddProjectToStackEvent,
18
+ buildChatPolicyOverrideEvent,
19
+ buildChatProviderEvent,
20
+ buildChatReadStateEvent,
21
+ buildChatSourceHashEvent,
22
+ buildCompactFailuresEvent,
23
+ buildCreateChatEvent,
24
+ buildCreateStackEvent,
25
+ buildEnqueueMessageResult,
26
+ buildOpenProjectResult,
27
+ buildPendingForkSessionTokenEvent,
28
+ buildPlanModeEvent,
29
+ buildRemoveProjectEvent,
30
+ buildRemoveProjectFromStackEvent,
31
+ buildRemoveQueuedMessageEvent,
32
+ buildRemoveStackEvent,
33
+ buildRenameStackEvent,
34
+ buildRenameChatEvent,
35
+ buildSessionCommandsEvent,
36
+ buildSessionTokenEvent,
37
+ buildSetProjectStarEvent,
38
+ buildTurnCancelledEvent,
39
+ buildTurnFailedEvent,
40
+ buildTurnFinishedEvent,
41
+ buildTurnStartedEvent,
42
+ computeNewSidebarOrder,
43
+ } from "./event-store-write-ops"
44
+ import { writeSidebarOrderFile } from "./event-store-snapshot"
45
+
46
+ // ─── Entity write deps ────────────────────────────────────────────────────
47
+
48
+ export interface EntityWriteDeps {
49
+ readonly storage: StorageBackend
50
+ readonly dataDir: string
51
+ readonly sidebarProjectOrderPath: string
52
+ readonly projectsLogPath: string
53
+ readonly chatsLogPath: string
54
+ readonly queuedMessagesLogPath: string
55
+ readonly stacksLogPath: string
56
+ readonly projectsById: StoreState["projectsById"]
57
+ readonly projectIdsByPath: StoreState["projectIdsByPath"]
58
+ readonly chatsById: Map<string, ChatRecord>
59
+ readonly queuedMessagesByChatId: StoreState["queuedMessagesByChatId"]
60
+ readonly stacksById: StoreState["stacksById"]
61
+ /** Mutable ref for the sidebar project order. */
62
+ readonly sidebarProjectOrderRef: { value: string[] }
63
+ /** Read the current write-chain promise. */
64
+ getWriteChain: () => Promise<void>
65
+ /** Replace the write-chain promise. */
66
+ setWriteChain: (p: Promise<void>) => void
67
+ /** Core append: writes event to disk and applies to in-memory state. */
68
+ append: <T extends StoreEvent>(filePath: string, event: T) => Promise<void>
69
+ }
70
+
71
+ // ─── Session / turn write deps ────────────────────────────────────────────
72
+
73
+ export interface SessionWriteDeps {
74
+ readonly chatsById: Map<string, ChatRecord>
75
+ readonly turnsLogPath: string
76
+ readonly schedulesLogPath: string
77
+ append: <T extends StoreEvent>(filePath: string, event: T) => Promise<void>
78
+ }
79
+
80
+ // ─── Project ops ───────────────────────────────────────────────────────────
81
+
82
+ export async function openProject(
83
+ deps: EntityWriteDeps,
84
+ localPath: string,
85
+ title?: string,
86
+ ) {
87
+ const result = buildOpenProjectResult(
88
+ { projectsById: deps.projectsById, projectIdsByPath: deps.projectIdsByPath },
89
+ localPath,
90
+ title,
91
+ )
92
+ if (result.kind === "existing") return result.project
93
+ await deps.append(deps.projectsLogPath, result.event)
94
+ return deps.projectsById.get(result.event.projectId)!
95
+ }
96
+
97
+ export async function removeProject(deps: EntityWriteDeps, projectId: string) {
98
+ const event = buildRemoveProjectEvent(deps.projectsById, projectId)
99
+ await deps.append(deps.projectsLogPath, event)
100
+ }
101
+
102
+ export async function setProjectStar(
103
+ deps: EntityWriteDeps,
104
+ projectId: string,
105
+ starred: boolean,
106
+ ) {
107
+ const event = buildSetProjectStarEvent(deps.projectsById, projectId, starred)
108
+ await deps.append(deps.projectsLogPath, event)
109
+ }
110
+
111
+ // ─── Stack ops ─────────────────────────────────────────────────────────────
112
+
113
+ export async function createStack(
114
+ deps: EntityWriteDeps,
115
+ title: string,
116
+ projectIds: string[],
117
+ ): Promise<StackRecord> {
118
+ const event = buildCreateStackEvent(
119
+ { projectsById: deps.projectsById, stacksById: deps.stacksById },
120
+ title,
121
+ projectIds,
122
+ )
123
+ await deps.append(deps.stacksLogPath, event)
124
+ return deps.stacksById.get(event.stackId)!
125
+ }
126
+
127
+ export async function renameStack(
128
+ deps: EntityWriteDeps,
129
+ stackId: string,
130
+ title: string,
131
+ ) {
132
+ const event = buildRenameStackEvent(deps.stacksById, stackId, title)
133
+ if (event) await deps.append(deps.stacksLogPath, event)
134
+ }
135
+
136
+ export async function removeStack(deps: EntityWriteDeps, stackId: string) {
137
+ const event = buildRemoveStackEvent(deps.stacksById, stackId)
138
+ if (event) await deps.append(deps.stacksLogPath, event)
139
+ }
140
+
141
+ export async function addProjectToStack(
142
+ deps: EntityWriteDeps,
143
+ stackId: string,
144
+ projectId: string,
145
+ ) {
146
+ const event = buildAddProjectToStackEvent(
147
+ { projectsById: deps.projectsById, stacksById: deps.stacksById },
148
+ stackId,
149
+ projectId,
150
+ )
151
+ if (event) await deps.append(deps.stacksLogPath, event)
152
+ }
153
+
154
+ export async function removeProjectFromStack(
155
+ deps: EntityWriteDeps,
156
+ stackId: string,
157
+ projectId: string,
158
+ ) {
159
+ const event = buildRemoveProjectFromStackEvent(deps.stacksById, stackId, projectId)
160
+ if (event) await deps.append(deps.stacksLogPath, event)
161
+ }
162
+
163
+ // ─── Sidebar ───────────────────────────────────────────────────────────────
164
+
165
+ export async function setSidebarProjectOrder(
166
+ deps: EntityWriteDeps,
167
+ projectIds: string[],
168
+ ): Promise<void> {
169
+ const newOrder = computeNewSidebarOrder(
170
+ deps.projectsById,
171
+ deps.sidebarProjectOrderRef.value,
172
+ projectIds,
173
+ )
174
+ if (!newOrder) return
175
+ const newChain = deps.getWriteChain().then(async () => {
176
+ await writeSidebarOrderFile(deps.storage, deps.dataDir, deps.sidebarProjectOrderPath, newOrder)
177
+ deps.sidebarProjectOrderRef.value = [...newOrder]
178
+ })
179
+ deps.setWriteChain(newChain)
180
+ await newChain
181
+ }
182
+
183
+ // ─── Chat lifecycle ops ────────────────────────────────────────────────────
184
+
185
+ export async function createChat(
186
+ deps: EntityWriteDeps,
187
+ projectId: string,
188
+ options?: { stackId?: string; stackBindings?: StackBinding[] },
189
+ ): Promise<ChatRecord> {
190
+ const event = buildCreateChatEvent(
191
+ { projectsById: deps.projectsById, stacksById: deps.stacksById },
192
+ projectId,
193
+ options,
194
+ )
195
+ await deps.append(deps.chatsLogPath, event)
196
+ return deps.chatsById.get(event.chatId)!
197
+ }
198
+
199
+ export async function renameChat(
200
+ deps: EntityWriteDeps,
201
+ chatId: string,
202
+ title: string,
203
+ ) {
204
+ const event = buildRenameChatEvent(deps.chatsById, chatId, title)
205
+ if (event) await deps.append(deps.chatsLogPath, event)
206
+ }
207
+
208
+ export async function setChatProvider(
209
+ deps: EntityWriteDeps,
210
+ chatId: string,
211
+ provider: AgentProvider,
212
+ ) {
213
+ const ev = buildChatProviderEvent(deps.chatsById, chatId, provider)
214
+ if (ev) await deps.append(deps.chatsLogPath, ev)
215
+ }
216
+
217
+ export async function setPlanMode(
218
+ deps: EntityWriteDeps,
219
+ chatId: string,
220
+ planMode: boolean,
221
+ ) {
222
+ const ev = buildPlanModeEvent(deps.chatsById, chatId, planMode)
223
+ if (ev) await deps.append(deps.chatsLogPath, ev)
224
+ }
225
+
226
+ export async function setCompactFailureCount(
227
+ deps: EntityWriteDeps,
228
+ chatId: string,
229
+ compactFailureCount: number,
230
+ ) {
231
+ const ev = buildCompactFailuresEvent(deps.chatsById, chatId, compactFailureCount)
232
+ if (ev) await deps.append(deps.chatsLogPath, ev)
233
+ }
234
+
235
+ export async function setChatReadState(
236
+ deps: EntityWriteDeps,
237
+ chatId: string,
238
+ unread: boolean,
239
+ ) {
240
+ const ev = buildChatReadStateEvent(deps.chatsById, chatId, unread)
241
+ if (ev) await deps.append(deps.chatsLogPath, ev)
242
+ }
243
+
244
+ export async function setChatPolicyOverride(
245
+ deps: EntityWriteDeps,
246
+ chatId: string,
247
+ policyOverride: ChatPermissionPolicyOverride | null,
248
+ ) {
249
+ await deps.append(
250
+ deps.chatsLogPath,
251
+ buildChatPolicyOverrideEvent(deps.chatsById, chatId, policyOverride),
252
+ )
253
+ }
254
+
255
+ export async function setSourceHash(
256
+ deps: EntityWriteDeps,
257
+ chatId: string,
258
+ sourceHash: string | null,
259
+ ) {
260
+ const ev = buildChatSourceHashEvent(deps.chatsById, chatId, sourceHash)
261
+ if (ev) await deps.append(deps.chatsLogPath, ev)
262
+ }
263
+
264
+ // ─── Queued message ops ────────────────────────────────────────────────────
265
+
266
+ export async function enqueueMessage(
267
+ deps: EntityWriteDeps,
268
+ chatId: string,
269
+ message: Omit<QueuedChatMessage, "id" | "createdAt"> & Partial<Pick<QueuedChatMessage, "id" | "createdAt">>,
270
+ ) {
271
+ const { event, queuedMessage } = buildEnqueueMessageResult(deps.chatsById, chatId, message)
272
+ await deps.append(deps.queuedMessagesLogPath, event)
273
+ return queuedMessage
274
+ }
275
+
276
+ export async function removeQueuedMessage(
277
+ deps: EntityWriteDeps,
278
+ chatId: string,
279
+ queuedMessageId: string,
280
+ ) {
281
+ const event = buildRemoveQueuedMessageEvent(
282
+ deps.chatsById, deps.queuedMessagesByChatId, chatId, queuedMessageId,
283
+ )
284
+ await deps.append(deps.queuedMessagesLogPath, event)
285
+ }
286
+
287
+ // ─── Session / turn write ops ─────────────────────────────────────────────
288
+
289
+ export async function recordTurnStarted(
290
+ deps: SessionWriteDeps,
291
+ chatId: string,
292
+ runConfig?: TurnRunConfig,
293
+ ) {
294
+ await deps.append(deps.turnsLogPath, buildTurnStartedEvent(deps.chatsById, chatId, runConfig))
295
+ }
296
+
297
+ export async function recordTurnFinished(deps: SessionWriteDeps, chatId: string) {
298
+ await deps.append(deps.turnsLogPath, buildTurnFinishedEvent(deps.chatsById, chatId))
299
+ }
300
+
301
+ export async function recordTurnFailed(
302
+ deps: SessionWriteDeps,
303
+ chatId: string,
304
+ error: string,
305
+ ) {
306
+ await deps.append(deps.turnsLogPath, buildTurnFailedEvent(deps.chatsById, chatId, error))
307
+ }
308
+
309
+ export async function recordTurnCancelled(deps: SessionWriteDeps, chatId: string) {
310
+ await deps.append(deps.turnsLogPath, buildTurnCancelledEvent(deps.chatsById, chatId))
311
+ }
312
+
313
+ export async function setSessionTokenForProvider(
314
+ deps: SessionWriteDeps,
315
+ chatId: string,
316
+ provider: AgentProvider,
317
+ sessionToken: string | null,
318
+ ) {
319
+ const ev = buildSessionTokenEvent(deps.chatsById, chatId, provider, sessionToken)
320
+ if (ev) await deps.append(deps.turnsLogPath, ev)
321
+ }
322
+
323
+ export async function recordSessionCommandsLoaded(
324
+ deps: SessionWriteDeps,
325
+ chatId: string,
326
+ commands: SlashCommand[],
327
+ ) {
328
+ const ev = buildSessionCommandsEvent(deps.chatsById, chatId, commands)
329
+ if (ev) await deps.append(deps.turnsLogPath, ev)
330
+ }
331
+
332
+ export async function setPendingForkSessionToken(
333
+ deps: SessionWriteDeps,
334
+ chatId: string,
335
+ value: { provider: AgentProvider; token: string } | null,
336
+ ) {
337
+ const ev = buildPendingForkSessionToken(deps.chatsById, chatId, value)
338
+ if (ev) await deps.append(deps.turnsLogPath, ev)
339
+ }
340
+
341
+ export async function appendAutoContinueEvent(
342
+ deps: SessionWriteDeps,
343
+ event: AutoContinueEvent,
344
+ ) {
345
+ return deps.append(deps.schedulesLogPath, event)
346
+ }
347
+
348
+ // ─── Private helper ───────────────────────────────────────────────────────
349
+
350
+ function buildPendingForkSessionToken(
351
+ chatsById: Map<string, ChatRecord>,
352
+ chatId: string,
353
+ value: { provider: AgentProvider; token: string } | null,
354
+ ) {
355
+ return buildPendingForkSessionTokenEvent(chatsById, chatId, value)
356
+ }
@@ -0,0 +1,230 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import type { TranscriptEntry } from "../shared/types"
3
+ import type { StoreEvent } from "./events"
4
+ import {
5
+ coalesceContextWindowUpdates,
6
+ decodeCursor,
7
+ encodeHistoryCursor,
8
+ getForkedChatTitle,
9
+ getHistorySnapshot,
10
+ getReplayEventPriority,
11
+ normalizeSidebarProjectOrder,
12
+ slashCommandsEqual,
13
+ type TranscriptPageResult,
14
+ } from "./event-store-helpers"
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // normalizeSidebarProjectOrder
18
+ // ---------------------------------------------------------------------------
19
+ describe("normalizeSidebarProjectOrder", () => {
20
+ test("returns empty array for non-array input", () => {
21
+ expect(normalizeSidebarProjectOrder(null)).toEqual([])
22
+ expect(normalizeSidebarProjectOrder(42)).toEqual([])
23
+ expect(normalizeSidebarProjectOrder({ a: 1 })).toEqual([])
24
+ })
25
+
26
+ test("filters out non-string entries", () => {
27
+ expect(normalizeSidebarProjectOrder(["a", 2, null, "b"])).toEqual(["a", "b"])
28
+ })
29
+
30
+ test("trims whitespace and skips blank strings", () => {
31
+ expect(normalizeSidebarProjectOrder([" a ", " ", "b"])).toEqual(["a", "b"])
32
+ })
33
+
34
+ test("deduplicates preserving first occurrence", () => {
35
+ expect(normalizeSidebarProjectOrder(["x", "y", "x", "z"])).toEqual(["x", "y", "z"])
36
+ })
37
+
38
+ test("returns ordered array for clean input", () => {
39
+ expect(normalizeSidebarProjectOrder(["proj-1", "proj-2"])).toEqual(["proj-1", "proj-2"])
40
+ })
41
+ })
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // encodeHistoryCursor / decodeCursor
45
+ // ---------------------------------------------------------------------------
46
+ describe("encodeHistoryCursor", () => {
47
+ test("encodes index as idx: prefix string", () => {
48
+ expect(encodeHistoryCursor(0)).toBe("idx:0")
49
+ expect(encodeHistoryCursor(42)).toBe("idx:42")
50
+ expect(encodeHistoryCursor(9999)).toBe("idx:9999")
51
+ })
52
+ })
53
+
54
+ describe("decodeCursor", () => {
55
+ test("round-trips with encodeHistoryCursor", () => {
56
+ expect(decodeCursor(encodeHistoryCursor(0))).toBe(0)
57
+ expect(decodeCursor(encodeHistoryCursor(100))).toBe(100)
58
+ })
59
+
60
+ test("throws on invalid cursor format", () => {
61
+ expect(() => decodeCursor("cursor:0")).toThrow("Invalid history cursor")
62
+ expect(() => decodeCursor("")).toThrow("Invalid history cursor")
63
+ expect(() => decodeCursor("idx:-1")).toThrow("Invalid history cursor")
64
+ expect(() => decodeCursor("idx:abc")).toThrow("Invalid history cursor")
65
+ })
66
+ })
67
+
68
+ // ---------------------------------------------------------------------------
69
+ // slashCommandsEqual
70
+ // ---------------------------------------------------------------------------
71
+ describe("slashCommandsEqual", () => {
72
+ test("returns true for two empty arrays", () => {
73
+ expect(slashCommandsEqual([], [])).toBe(true)
74
+ })
75
+
76
+ test("returns false when lengths differ", () => {
77
+ expect(slashCommandsEqual(
78
+ [{ name: "a", description: "d", argumentHint: "" }],
79
+ [],
80
+ )).toBe(false)
81
+ })
82
+
83
+ test("returns true for identical arrays", () => {
84
+ const cmds = [{ name: "foo", description: "bar", argumentHint: "<hint>" }]
85
+ expect(slashCommandsEqual(cmds, cmds)).toBe(true)
86
+ })
87
+
88
+ test("returns false when a field differs", () => {
89
+ const a = [{ name: "foo", description: "bar", argumentHint: "" }]
90
+ const b = [{ name: "foo", description: "baz", argumentHint: "" }]
91
+ expect(slashCommandsEqual(a, b)).toBe(false)
92
+ })
93
+ })
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // coalesceContextWindowUpdates
97
+ // ---------------------------------------------------------------------------
98
+
99
+ function makeEntry(kind: TranscriptEntry["kind"], id: string): TranscriptEntry {
100
+ return { _id: id, createdAt: 0, kind } as TranscriptEntry
101
+ }
102
+
103
+ describe("coalesceContextWindowUpdates", () => {
104
+ test("returns empty array unchanged", () => {
105
+ expect(coalesceContextWindowUpdates([])).toEqual([])
106
+ })
107
+
108
+ test("passes through entries without context_window_updated", () => {
109
+ const entries = [
110
+ makeEntry("user_prompt", "1"),
111
+ makeEntry("assistant_text", "2"),
112
+ ]
113
+ expect(coalesceContextWindowUpdates(entries)).toEqual(entries)
114
+ })
115
+
116
+ test("collapses consecutive context_window_updated to last", () => {
117
+ const entries = [
118
+ makeEntry("context_window_updated", "cwu-1"),
119
+ makeEntry("context_window_updated", "cwu-2"),
120
+ makeEntry("context_window_updated", "cwu-3"),
121
+ ]
122
+ const result = coalesceContextWindowUpdates(entries)
123
+ expect(result).toHaveLength(1)
124
+ expect(result[0]!._id).toBe("cwu-3")
125
+ })
126
+
127
+ test("preserves non-cwu entries between cwu runs", () => {
128
+ const entries = [
129
+ makeEntry("context_window_updated", "cwu-1"),
130
+ makeEntry("context_window_updated", "cwu-2"),
131
+ makeEntry("user_prompt", "up-1"),
132
+ makeEntry("context_window_updated", "cwu-3"),
133
+ makeEntry("context_window_updated", "cwu-4"),
134
+ ]
135
+ const result = coalesceContextWindowUpdates(entries)
136
+ expect(result.map((e) => e._id)).toEqual(["cwu-2", "up-1", "cwu-4"])
137
+ })
138
+
139
+ test("does not drop a single context_window_updated", () => {
140
+ const entries = [makeEntry("context_window_updated", "solo")]
141
+ expect(coalesceContextWindowUpdates(entries)).toEqual(entries)
142
+ })
143
+ })
144
+
145
+ // ---------------------------------------------------------------------------
146
+ // getHistorySnapshot
147
+ // ---------------------------------------------------------------------------
148
+ describe("getHistorySnapshot", () => {
149
+ test("maps page fields and recentLimit into ChatHistorySnapshot", () => {
150
+ const page: TranscriptPageResult = {
151
+ entries: [],
152
+ hasOlder: true,
153
+ olderCursor: "idx:5",
154
+ }
155
+ expect(getHistorySnapshot(page, 50)).toEqual({
156
+ hasOlder: true,
157
+ olderCursor: "idx:5",
158
+ recentLimit: 50,
159
+ })
160
+ })
161
+
162
+ test("passes null olderCursor through", () => {
163
+ const page: TranscriptPageResult = { entries: [], hasOlder: false, olderCursor: null }
164
+ expect(getHistorySnapshot(page, 20)).toEqual({
165
+ hasOlder: false,
166
+ olderCursor: null,
167
+ recentLimit: 20,
168
+ })
169
+ })
170
+ })
171
+
172
+ // ---------------------------------------------------------------------------
173
+ // getForkedChatTitle
174
+ // ---------------------------------------------------------------------------
175
+ describe("getForkedChatTitle", () => {
176
+ test("prefixes non-fork titles", () => {
177
+ expect(getForkedChatTitle("My Chat")).toBe("Fork: My Chat")
178
+ })
179
+
180
+ test("does not double-prefix already-forked titles", () => {
181
+ expect(getForkedChatTitle("Fork: My Chat")).toBe("Fork: My Chat")
182
+ })
183
+
184
+ test("returns default for empty title", () => {
185
+ expect(getForkedChatTitle("")).toBe("Fork: New Chat")
186
+ })
187
+
188
+ test("returns default for whitespace-only title", () => {
189
+ expect(getForkedChatTitle(" ")).toBe("Fork: New Chat")
190
+ })
191
+
192
+ test("trims whitespace before prefixing", () => {
193
+ expect(getForkedChatTitle(" Hello ")).toBe("Fork: Hello")
194
+ })
195
+ })
196
+
197
+ // ---------------------------------------------------------------------------
198
+ // getReplayEventPriority — spot-check priority buckets
199
+ // ---------------------------------------------------------------------------
200
+ describe("getReplayEventPriority", () => {
201
+ function makeEvent(type: string): StoreEvent {
202
+ return { type } as StoreEvent
203
+ }
204
+
205
+ test("project events have priority 0", () => {
206
+ expect(getReplayEventPriority(makeEvent("project_opened"))).toBe(0)
207
+ expect(getReplayEventPriority(makeEvent("sidebar_project_order_set"))).toBe(0)
208
+ })
209
+
210
+ test("chat_created has priority 1", () => {
211
+ expect(getReplayEventPriority(makeEvent("chat_created"))).toBe(1)
212
+ })
213
+
214
+ test("turn_started has priority 5", () => {
215
+ expect(getReplayEventPriority(makeEvent("turn_started"))).toBe(5)
216
+ })
217
+
218
+ test("turn_finished has priority 8", () => {
219
+ expect(getReplayEventPriority(makeEvent("turn_finished"))).toBe(8)
220
+ })
221
+
222
+ test("loop_armed has priority 11", () => {
223
+ expect(getReplayEventPriority(makeEvent("loop_armed"))).toBe(11)
224
+ })
225
+
226
+ test("orch events have priority 5", () => {
227
+ expect(getReplayEventPriority(makeEvent("orch_run_created"))).toBe(5)
228
+ expect(getReplayEventPriority(makeEvent("orch_task_committed"))).toBe(5)
229
+ })
230
+ })