@cuongtran001/kanna 1.3.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 (186) hide show
  1. package/dist/client/assets/{arc-CAysfmX5.js → arc-DXtN3EWJ.js} +1 -1
  2. package/dist/client/assets/{architectureDiagram-3BPJPVTR-BKqRYsm4.js → architectureDiagram-3BPJPVTR-CIGqW1z-.js} +1 -1
  3. package/dist/client/assets/{blockDiagram-GPEHLZMM-Ce7Eorec.js → blockDiagram-GPEHLZMM-BafNS_H6.js} +1 -1
  4. package/dist/client/assets/{c4Diagram-AAUBKEIU-CJ77zauH.js → c4Diagram-AAUBKEIU-YdAgRSis.js} +1 -1
  5. package/dist/client/assets/channel-BEYTrUAm.js +1 -0
  6. package/dist/client/assets/{chunk-2J33WTMH-Rk_3zLxJ.js → chunk-2J33WTMH-eiP5y5G-.js} +1 -1
  7. package/dist/client/assets/{chunk-4BX2VUAB-DPIz6Nvf.js → chunk-4BX2VUAB-CvLGzPMF.js} +1 -1
  8. package/dist/client/assets/{chunk-55IACEB6-D-TkbGh2.js → chunk-55IACEB6-Y-S9Fb8A.js} +1 -1
  9. package/dist/client/assets/{chunk-727SXJPM-B4NOSIZV.js → chunk-727SXJPM-6sJH1RI9.js} +1 -1
  10. package/dist/client/assets/{chunk-AQP2D5EJ-Cm_TGLYm.js → chunk-AQP2D5EJ-hV1r_IXO.js} +1 -1
  11. package/dist/client/assets/{chunk-FMBD7UC4-Bu45BUVh.js → chunk-FMBD7UC4-CC3L28S5.js} +1 -1
  12. package/dist/client/assets/{chunk-ND2GUHAM-C1Bw8_gg.js → chunk-ND2GUHAM-BZ3czhdH.js} +1 -1
  13. package/dist/client/assets/{chunk-QZHKN3VN-DueG15o0.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-YgpMurXf.js → cose-bilkent-S5V4N54A-CfxdDghE.js} +1 -1
  17. package/dist/client/assets/{dagre-BM42HDAG-Dop7Oq67.js → dagre-BM42HDAG-B8CddsbB.js} +1 -1
  18. package/dist/client/assets/{diagram-2AECGRRQ-unMfBEJH.js → diagram-2AECGRRQ-F0yS0fVB.js} +1 -1
  19. package/dist/client/assets/{diagram-5GNKFQAL-D9EJ9qMI.js → diagram-5GNKFQAL-cW0i0qlh.js} +1 -1
  20. package/dist/client/assets/{diagram-KO2AKTUF-C5IykOIR.js → diagram-KO2AKTUF-BzAKMeJT.js} +1 -1
  21. package/dist/client/assets/{diagram-LMA3HP47-DJAuPngz.js → diagram-LMA3HP47-qdC4XqIl.js} +1 -1
  22. package/dist/client/assets/{diagram-OG6HWLK6-B3wHFtS-.js → diagram-OG6HWLK6-CGcpi6Dh.js} +1 -1
  23. package/dist/client/assets/{erDiagram-TEJ5UH35-Dm7kt9o5.js → erDiagram-TEJ5UH35-CxNTDicM.js} +1 -1
  24. package/dist/client/assets/{flowDiagram-I6XJVG4X-s9Lds3ny.js → flowDiagram-I6XJVG4X-qkGa5TqY.js} +1 -1
  25. package/dist/client/assets/{ganttDiagram-6RSMTGT7-CRQNYdAf.js → ganttDiagram-6RSMTGT7-D9mVPoQi.js} +1 -1
  26. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-DhYjK4Jb.js → gitGraphDiagram-PVQCEYII-DA0grAXe.js} +1 -1
  27. package/dist/client/assets/{index-CbSY9HfS.js → index-BqUY1Bm4.js} +1 -1
  28. package/dist/client/assets/{index-C0U2kwuB.js → index-TU1k6ov6.js} +5 -5
  29. package/dist/client/assets/{infoDiagram-5YYISTIA-CRKJ0PSH.js → infoDiagram-5YYISTIA-CGljtLs3.js} +1 -1
  30. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-CEAkB0IY.js → ishikawaDiagram-YF4QCWOH-CtC0xrmG.js} +1 -1
  31. package/dist/client/assets/{journeyDiagram-JHISSGLW-CyOK5_DG.js → journeyDiagram-JHISSGLW-URLnVZeN.js} +1 -1
  32. package/dist/client/assets/{kanban-definition-UN3LZRKU-BfKjiJSo.js → kanban-definition-UN3LZRKU-C2Q6aqQh.js} +1 -1
  33. package/dist/client/assets/{linear-DLR6miy3.js → linear-ByKHtsIx.js} +1 -1
  34. package/dist/client/assets/{mermaid.core-D7AdC_lE.js → mermaid.core-Dl_f2KLc.js} +4 -4
  35. package/dist/client/assets/{mindmap-definition-RKZ34NQL-zYyJtDSa.js → mindmap-definition-RKZ34NQL-CaJ9IDLI.js} +1 -1
  36. package/dist/client/assets/{pieDiagram-4H26LBE5-BgP8PFlo.js → pieDiagram-4H26LBE5-mmYaf2Oa.js} +1 -1
  37. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-Di8h7J96.js → quadrantDiagram-W4KKPZXB-B_PO6OCC.js} +1 -1
  38. package/dist/client/assets/{requirementDiagram-4Y6WPE33-DuTzFl3R.js → requirementDiagram-4Y6WPE33-M4huwJt1.js} +1 -1
  39. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-DBRoMm_T.js → sankeyDiagram-5OEKKPKP-B0hb1sWh.js} +1 -1
  40. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-Bmb-1HUN.js → sequenceDiagram-3UESZ5HK-BAqSptnA.js} +1 -1
  41. package/dist/client/assets/{stateDiagram-AJRCARHV-fnv5ayn-.js → stateDiagram-AJRCARHV-DTNkvPi5.js} +1 -1
  42. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-CVN3oGTF.js +1 -0
  43. package/dist/client/assets/{timeline-definition-PNZ67QCA-ByWRdpQX.js → timeline-definition-PNZ67QCA-MgAjHz8Q.js} +1 -1
  44. package/dist/client/assets/{vennDiagram-CIIHVFJN--2WGpVXW.js → vennDiagram-CIIHVFJN-DmKoO51W.js} +1 -1
  45. package/dist/client/assets/{wardley-L42UT6IY-24zlnzxK.js → wardley-L42UT6IY-CWcyHnYs.js} +1 -1
  46. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-Cllvqb3K.js → wardleyDiagram-YWT4CUSO-BGIGmFP9.js} +1 -1
  47. package/dist/client/assets/{xychartDiagram-2RQKCTM6-ZcdC6nPE.js → xychartDiagram-2RQKCTM6-D20YIvEl.js} +1 -1
  48. package/dist/client/index.html +1 -1
  49. package/package.json +1 -1
  50. package/src/server/agent-coordinator-types.ts +158 -0
  51. package/src/server/agent-coordinator.ts +919 -0
  52. package/src/server/agent-deps-builders.ts +425 -0
  53. package/src/server/agent.test.ts +22 -6
  54. package/src/server/agent.ts +35 -4987
  55. package/src/server/claude-autocontinue-commands.test.ts +494 -0
  56. package/src/server/claude-autocontinue-commands.ts +256 -0
  57. package/src/server/claude-cancel-handler.test.ts +472 -0
  58. package/src/server/claude-cancel-handler.ts +241 -0
  59. package/src/server/claude-chat-management.test.ts +395 -0
  60. package/src/server/claude-chat-management.ts +251 -0
  61. package/src/server/claude-harness-stream.test.ts +273 -0
  62. package/src/server/claude-harness-stream.ts +158 -0
  63. package/src/server/claude-loop-orch-commands.test.ts +579 -0
  64. package/src/server/claude-loop-orch-commands.ts +466 -0
  65. package/src/server/claude-message-normalizer.test.ts +213 -0
  66. package/src/server/claude-message-normalizer.ts +436 -0
  67. package/src/server/claude-prompt-helpers.test.ts +248 -0
  68. package/src/server/claude-prompt-helpers.ts +139 -0
  69. package/src/server/claude-sdk-queue.test.ts +169 -0
  70. package/src/server/claude-sdk-queue.ts +82 -0
  71. package/src/server/claude-send-command.test.ts +414 -0
  72. package/src/server/claude-send-command.ts +420 -0
  73. package/src/server/claude-session-config-helpers.test.ts +279 -0
  74. package/src/server/claude-session-config-helpers.ts +151 -0
  75. package/src/server/claude-session-config.test.ts +250 -0
  76. package/src/server/claude-session-config.ts +159 -0
  77. package/src/server/claude-session-error-handler.test.ts +498 -0
  78. package/src/server/claude-session-error-handler.ts +361 -0
  79. package/src/server/claude-session-lifecycle.test.ts +669 -0
  80. package/src/server/claude-session-lifecycle.ts +283 -0
  81. package/src/server/claude-session-rebuild.test.ts +224 -0
  82. package/src/server/claude-session-rebuild.ts +101 -0
  83. package/src/server/claude-session-runner.test.ts +523 -0
  84. package/src/server/claude-session-runner.ts +521 -0
  85. package/src/server/claude-session-spawner.test.ts +401 -0
  86. package/src/server/claude-session-spawner.ts +401 -0
  87. package/src/server/claude-session-start.test.ts +205 -0
  88. package/src/server/claude-session-start.ts +277 -0
  89. package/src/server/claude-session-state-queries.test.ts +344 -0
  90. package/src/server/claude-session-state-queries.ts +160 -0
  91. package/src/server/claude-session-state.ts +104 -0
  92. package/src/server/claude-slash-commands.test.ts +277 -0
  93. package/src/server/claude-slash-commands.ts +234 -0
  94. package/src/server/claude-spawn-helpers.test.ts +221 -0
  95. package/src/server/claude-spawn-helpers.ts +179 -0
  96. package/src/server/claude-steer-log.test.ts +243 -0
  97. package/src/server/claude-steer-log.ts +58 -0
  98. package/src/server/claude-subagent-tool-response.test.ts +213 -0
  99. package/src/server/claude-subagent-tool-response.ts +165 -0
  100. package/src/server/claude-subagent-wiring.test.ts +263 -0
  101. package/src/server/claude-subagent-wiring.ts +324 -0
  102. package/src/server/claude-tool-respond.test.ts +321 -0
  103. package/src/server/claude-tool-respond.ts +142 -0
  104. package/src/server/claude-turn-runner.test.ts +310 -0
  105. package/src/server/claude-turn-runner.ts +228 -0
  106. package/src/server/claude-turn-starter.test.ts +266 -0
  107. package/src/server/claude-turn-starter.ts +566 -0
  108. package/src/server/claude-usage-math.test.ts +169 -0
  109. package/src/server/claude-usage-math.ts +132 -0
  110. package/src/server/diff-store-branch-ops.adapter.test.ts +332 -0
  111. package/src/server/diff-store-branch-ops.adapter.ts +602 -0
  112. package/src/server/diff-store-commit-ops.adapter.test.ts +358 -0
  113. package/src/server/diff-store-commit-ops.adapter.ts +258 -0
  114. package/src/server/diff-store-errors.ts +85 -0
  115. package/src/server/diff-store-file-ops.adapter.ts +212 -0
  116. package/src/server/diff-store-git-branch.adapter.ts +686 -0
  117. package/src/server/diff-store-io.adapter.ts +27 -0
  118. package/src/server/diff-store-parse.test.ts +212 -0
  119. package/src/server/diff-store-parse.ts +122 -0
  120. package/src/server/diff-store-state.ts +82 -0
  121. package/src/server/diff-store.ts +76 -1854
  122. package/src/server/event-store-apply.test.ts +280 -0
  123. package/src/server/event-store-apply.ts +132 -0
  124. package/src/server/event-store-chat-lifecycle.test.ts +408 -0
  125. package/src/server/event-store-chat-lifecycle.ts +441 -0
  126. package/src/server/event-store-entity-write.ts +356 -0
  127. package/src/server/event-store-helpers.test.ts +230 -0
  128. package/src/server/event-store-helpers.ts +235 -0
  129. package/src/server/event-store-init.ts +276 -0
  130. package/src/server/event-store-messages.adapter.test.ts +365 -0
  131. package/src/server/event-store-messages.adapter.ts +213 -0
  132. package/src/server/event-store-orch-subscription.ts +124 -0
  133. package/src/server/event-store-orch.test.ts +462 -0
  134. package/src/server/event-store-orch.ts +312 -0
  135. package/src/server/event-store-peripheral-events.adapter.test.ts +254 -0
  136. package/src/server/event-store-peripheral-events.adapter.ts +105 -0
  137. package/src/server/event-store-snapshot.test.ts +597 -0
  138. package/src/server/event-store-snapshot.ts +658 -0
  139. package/src/server/event-store-subagent.test.ts +408 -0
  140. package/src/server/event-store-subagent.ts +198 -0
  141. package/src/server/event-store-tool-requests.test.ts +210 -0
  142. package/src/server/event-store-tool-requests.ts +131 -0
  143. package/src/server/event-store-transcript-write.adapter.ts +285 -0
  144. package/src/server/event-store-write-ops.test.ts +554 -0
  145. package/src/server/event-store-write-ops.ts +529 -0
  146. package/src/server/event-store.test.ts +35 -0
  147. package/src/server/event-store.ts +251 -2285
  148. package/src/server/harness-types.ts +15 -1
  149. package/src/server/oauth-errors.ts +19 -0
  150. package/src/server/ws-router-agent-ctrl.test.ts +187 -0
  151. package/src/server/ws-router-agent-ctrl.ts +137 -0
  152. package/src/server/ws-router-broadcast.ts +488 -0
  153. package/src/server/ws-router-chat.test.ts +461 -0
  154. package/src/server/ws-router-chat.ts +279 -0
  155. package/src/server/ws-router-defaults.test.ts +283 -0
  156. package/src/server/ws-router-defaults.ts +434 -0
  157. package/src/server/ws-router-diff.test.ts +167 -0
  158. package/src/server/ws-router-diff.ts +250 -0
  159. package/src/server/ws-router-envelope.ts +329 -0
  160. package/src/server/ws-router-misc.test.ts +422 -0
  161. package/src/server/ws-router-misc.ts +265 -0
  162. package/src/server/ws-router-orch.test.ts +190 -0
  163. package/src/server/ws-router-orch.ts +93 -0
  164. package/src/server/ws-router-project.test.ts +403 -0
  165. package/src/server/ws-router-project.ts +256 -0
  166. package/src/server/ws-router-push.test.ts +193 -0
  167. package/src/server/ws-router-push.ts +120 -0
  168. package/src/server/ws-router-settings.test.ts +173 -0
  169. package/src/server/ws-router-settings.ts +416 -0
  170. package/src/server/ws-router-skills.test.ts +194 -0
  171. package/src/server/ws-router-skills.ts +204 -0
  172. package/src/server/ws-router-utils.ts +224 -0
  173. package/src/server/ws-router.ts +339 -2254
  174. package/src/shared/app-settings-types.ts +365 -0
  175. package/src/shared/core-types.ts +27 -0
  176. package/src/shared/git-diff-types.ts +165 -0
  177. package/src/shared/mcp-types.ts +105 -0
  178. package/src/shared/provider-model-types.ts +437 -0
  179. package/src/shared/subagent-types.ts +198 -0
  180. package/src/shared/tool-call-types.ts +314 -0
  181. package/src/shared/transcript-types.ts +270 -0
  182. package/src/shared/types.ts +25 -1711
  183. package/dist/client/assets/channel-KQhRwDzJ.js +0 -1
  184. package/dist/client/assets/classDiagram-4FO5ZUOK-ChxT1GNq.js +0 -1
  185. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-ChxT1GNq.js +0 -1
  186. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-D4X-ZnLV.js +0 -1
@@ -0,0 +1,312 @@
1
+ /**
2
+ * Orchestration read-model layer — pure functions that fold OrchestrationEvents
3
+ * into the `orchRunsById` map and project snapshots from it.
4
+ *
5
+ * Extracted from event-store.ts; EventStore delegates every orch* method here.
6
+ * All functions are pure with respect to IO: they mutate only the map they
7
+ * receive, which is the same Map instance held in StoreState.
8
+ */
9
+
10
+ import { LOG_PREFIX } from "../shared/branding"
11
+ import { log } from "../shared/log"
12
+ import type { OrchRunSnapshot, OrchTaskSnapshot } from "../shared/orchestration-types"
13
+ import type { OrchestrationEvent, OrchRunRecord, OrchTaskRecord } from "./events"
14
+ import { isTaskEventType, nextRunStatus, nextTaskState } from "./orchestration-state-machine"
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Event application
18
+ // ---------------------------------------------------------------------------
19
+
20
+ /**
21
+ * Fold one OrchestrationEvent into the orchRunsById map in place.
22
+ * Mirrors the private `applyOrchestrationEvent` that lived in EventStore.
23
+ */
24
+ export function applyOrchEvent(
25
+ orchRunsById: Map<string, OrchRunRecord>,
26
+ event: OrchestrationEvent,
27
+ ): void {
28
+ if (event.type === "orch_run_created") {
29
+ const tasksById = new Map<string, OrchTaskRecord>()
30
+ for (const spec of event.tasks) {
31
+ tasksById.set(spec.id, {
32
+ taskId: spec.id,
33
+ title: spec.title,
34
+ prompt: spec.prompt,
35
+ scopePaths: spec.scopePaths ?? [],
36
+ state: "queued",
37
+ ownerWorkerId: null,
38
+ worktreePath: null,
39
+ branch: null,
40
+ baseSha: null,
41
+ phaseIndex: -1,
42
+ attempts: 0,
43
+ error: null,
44
+ commitSha: null,
45
+ lastPhaseOutput: null,
46
+ verifying: false,
47
+ updatedAt: event.timestamp,
48
+ })
49
+ }
50
+ orchRunsById.set(event.runId, {
51
+ runId: event.runId,
52
+ status: "running",
53
+ config: event.config,
54
+ tasksById,
55
+ taskOrder: event.tasks.map((t) => t.id),
56
+ worktrees: [],
57
+ eventLog: [event],
58
+ createdAt: event.timestamp,
59
+ updatedAt: event.timestamp,
60
+ })
61
+ return
62
+ }
63
+
64
+ const run = orchRunsById.get(event.runId)
65
+ if (!run) return
66
+
67
+ run.eventLog.push(event)
68
+ run.updatedAt = event.timestamp
69
+
70
+ if (event.type === "orch_run_completed" || event.type === "orch_run_cancelled") {
71
+ const trans = nextRunStatus(run.status, event.type)
72
+ if (!trans.ok) {
73
+ log.warn(`${LOG_PREFIX} dropped illegal orch run transition`, {
74
+ runId: event.runId,
75
+ from: run.status,
76
+ event: event.type,
77
+ })
78
+ return
79
+ }
80
+ run.status = trans.next
81
+ return
82
+ }
83
+
84
+ if (event.type === "orch_scope_overlap_flagged") return // observability-only, no state fold
85
+ if (event.type === "orch_config_warning") return // observability-only, no state fold
86
+
87
+ // Worktree pool fold (F13)
88
+ if (event.type === "orch_worktree_provisioned") {
89
+ run.worktrees.push({
90
+ index: event.index,
91
+ path: event.path,
92
+ branch: event.branch,
93
+ heldByTaskId: null,
94
+ initialized: false,
95
+ })
96
+ return
97
+ }
98
+ if (event.type === "orch_worktree_init_started") return // timeline-only
99
+ if (event.type === "orch_worktree_init_completed") {
100
+ const slot = run.worktrees.find((w) => w.index === event.index)
101
+ if (slot) slot.initialized = event.ok
102
+ return
103
+ }
104
+
105
+ const task = run.tasksById.get(event.taskId)
106
+ if (!task) return
107
+
108
+ // FSM guard: drop an illegal task transition rather than corrupt the read
109
+ // model. The engine only emits legal transitions, so this is a defensive
110
+ // invariant (also protects replay of an older/foreign log).
111
+ if (isTaskEventType(event.type)) {
112
+ const trans = nextTaskState(task.state, event.type)
113
+ if (!trans.ok) {
114
+ log.warn(`${LOG_PREFIX} dropped illegal orch task transition`, {
115
+ runId: event.runId,
116
+ taskId: event.taskId,
117
+ from: task.state,
118
+ event: event.type,
119
+ })
120
+ return
121
+ }
122
+ }
123
+
124
+ task.updatedAt = event.timestamp
125
+ const slotOf = (t: OrchTaskRecord) => run.worktrees.find((w) => w.path === t.worktreePath)
126
+
127
+ switch (event.type) {
128
+ case "orch_task_claimed":
129
+ task.state = "claimed"
130
+ task.ownerWorkerId = event.workerId
131
+ task.worktreePath = event.worktreePath
132
+ task.branch = event.branch
133
+ task.baseSha = event.baseSha
134
+ task.attempts += 1
135
+ {
136
+ const slot = slotOf(task)
137
+ if (slot) slot.heldByTaskId = task.taskId
138
+ }
139
+ break
140
+ case "orch_phase_started":
141
+ task.state = "running"
142
+ task.phaseIndex = event.phaseIndex
143
+ task.verifying = false
144
+ break
145
+ case "orch_phase_completed":
146
+ task.lastPhaseOutput = event.output
147
+ break
148
+ case "orch_gate_opened":
149
+ task.state = "gated"
150
+ break
151
+ case "orch_gate_resolved":
152
+ if (event.decision === "approve") task.state = "running"
153
+ // reject: state stays gated; the engine appends orch_task_failed next
154
+ break
155
+ case "orch_verify_started":
156
+ task.verifying = true // observable stage; task stays "running"
157
+ break
158
+ case "orch_verify_completed":
159
+ task.verifying = false
160
+ break
161
+ case "orch_task_committed":
162
+ task.state = "committed"
163
+ task.ownerWorkerId = null
164
+ task.commitSha = event.commitSha
165
+ task.verifying = false
166
+ {
167
+ const slot = slotOf(task)
168
+ if (slot?.heldByTaskId === task.taskId) slot.heldByTaskId = null
169
+ }
170
+ break
171
+ case "orch_task_failed":
172
+ task.state = "failed"
173
+ task.ownerWorkerId = null
174
+ task.error = event.error
175
+ task.verifying = false
176
+ {
177
+ const slot = slotOf(task)
178
+ if (slot?.heldByTaskId === task.taskId) slot.heldByTaskId = null
179
+ }
180
+ break
181
+ case "orch_task_requeued":
182
+ // Slot hold deliberately KEPT (F13/F2): the task's uncommitted progress
183
+ // lives in its worktree — re-claim resumes the SAME slot.
184
+ task.state = "queued"
185
+ task.ownerWorkerId = null
186
+ task.verifying = false
187
+ break
188
+ }
189
+ }
190
+
191
+ // ---------------------------------------------------------------------------
192
+ // Snapshot projection
193
+ // ---------------------------------------------------------------------------
194
+
195
+ /** Project a stored OrchRunRecord into the wire-safe OrchRunSnapshot shape. */
196
+ export function toOrchRunSnapshot(run: OrchRunRecord): OrchRunSnapshot {
197
+ const tasks: OrchTaskSnapshot[] = run.taskOrder.flatMap((taskId) => {
198
+ const t = run.tasksById.get(taskId)
199
+ if (!t) return []
200
+ return [
201
+ {
202
+ taskId: t.taskId,
203
+ title: t.title,
204
+ state: t.state,
205
+ ownerWorkerId: t.ownerWorkerId,
206
+ worktreePath: t.worktreePath,
207
+ branch: t.branch,
208
+ baseSha: t.baseSha,
209
+ phaseIndex: t.phaseIndex,
210
+ attempts: t.attempts,
211
+ error: t.error,
212
+ commitSha: t.commitSha,
213
+ verifying: t.verifying,
214
+ updatedAt: t.updatedAt,
215
+ },
216
+ ]
217
+ })
218
+ return {
219
+ runId: run.runId,
220
+ status: run.status,
221
+ config: run.config,
222
+ tasks,
223
+ worktrees: run.worktrees.map((w) => ({ ...w })),
224
+ createdAt: run.createdAt,
225
+ updatedAt: run.updatedAt,
226
+ }
227
+ }
228
+
229
+ // ---------------------------------------------------------------------------
230
+ // Query helpers
231
+ // ---------------------------------------------------------------------------
232
+
233
+ /** Look up one run by id and project it to a snapshot. */
234
+ export function getOrchRunSnapshot(
235
+ orchRunsById: Map<string, OrchRunRecord>,
236
+ runId: string,
237
+ ): OrchRunSnapshot | null {
238
+ const run = orchRunsById.get(runId)
239
+ if (!run) return null
240
+ return toOrchRunSnapshot(run)
241
+ }
242
+
243
+ /** Project all runs to snapshots. */
244
+ export function getAllOrchRunSnapshots(
245
+ orchRunsById: Map<string, OrchRunRecord>,
246
+ ): OrchRunSnapshot[] {
247
+ return [...orchRunsById.values()].map(toOrchRunSnapshot)
248
+ }
249
+
250
+ /** Task spec lookup for the engine (records keep prompt/scope; snapshots do not). */
251
+ export function getOrchTaskSpec(
252
+ orchRunsById: Map<string, OrchRunRecord>,
253
+ runId: string,
254
+ taskId: string,
255
+ ): { prompt: string; scopePaths: string[] } | null {
256
+ const task = orchRunsById.get(runId)?.tasksById.get(taskId)
257
+ if (!task) return null
258
+ return { prompt: task.prompt, scopePaths: task.scopePaths }
259
+ }
260
+
261
+ /** Last completed phase's output — {{PRIOR}} context when resuming a gated/recovered task. */
262
+ export function getOrchLastPhaseOutput(
263
+ orchRunsById: Map<string, OrchRunRecord>,
264
+ runId: string,
265
+ taskId: string,
266
+ ): string | null {
267
+ return orchRunsById.get(runId)?.tasksById.get(taskId)?.lastPhaseOutput ?? null
268
+ }
269
+
270
+ /** Full ordered event timeline for one run — the rich drill-in source (F8). */
271
+ export function getOrchRunEvents(
272
+ orchRunsById: Map<string, OrchRunRecord>,
273
+ runId: string,
274
+ ): OrchestrationEvent[] {
275
+ return [...(orchRunsById.get(runId)?.eventLog ?? [])]
276
+ }
277
+
278
+ // ---------------------------------------------------------------------------
279
+ // Recovery iterators
280
+ // ---------------------------------------------------------------------------
281
+
282
+ /**
283
+ * Tasks a restart must RE-QUEUE.
284
+ * `gated` is deliberately excluded — a gated task is re-armed in place
285
+ * (gate re-notified), never requeued.
286
+ */
287
+ export function* nonTerminalOrchTasks(
288
+ orchRunsById: Map<string, OrchRunRecord>,
289
+ ): Generator<{ runId: string; taskId: string; state: "claimed" | "running" }> {
290
+ for (const run of orchRunsById.values()) {
291
+ if (run.status !== "running") continue
292
+ for (const task of run.tasksById.values()) {
293
+ if (task.state === "claimed" || task.state === "running") {
294
+ yield { runId: run.runId, taskId: task.taskId, state: task.state }
295
+ }
296
+ }
297
+ }
298
+ }
299
+
300
+ /** Tasks paused at a hard gate — re-armed (not requeued) by recoverOnStartup. */
301
+ export function* gatedOrchTasks(
302
+ orchRunsById: Map<string, OrchRunRecord>,
303
+ ): Generator<{ runId: string; taskId: string; phaseIndex: number }> {
304
+ for (const run of orchRunsById.values()) {
305
+ if (run.status !== "running") continue
306
+ for (const task of run.tasksById.values()) {
307
+ if (task.state === "gated") {
308
+ yield { runId: run.runId, taskId: task.taskId, phaseIndex: task.phaseIndex }
309
+ }
310
+ }
311
+ }
312
+ }
@@ -0,0 +1,254 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import type { CloudflareTunnelEvent } from "./cloudflare-tunnel/events"
3
+ import type { PushEvent } from "./push/events"
4
+ import type { ShareEvent } from "./session-share/share-projection"
5
+ import type { StorageBackend } from "./storage/backend"
6
+ import {
7
+ appendPushEvent,
8
+ appendShareEvent,
9
+ appendTunnelEvent,
10
+ getShareEvents,
11
+ getTunnelEvents,
12
+ listTunnelChats,
13
+ loadPushEvents,
14
+ loadShareEvents,
15
+ loadTunnelEvents,
16
+ type PeripheralEventsDeps,
17
+ } from "./event-store-peripheral-events.adapter"
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Helpers
21
+ // ---------------------------------------------------------------------------
22
+
23
+ /** Minimal in-memory StorageBackend stub. */
24
+ function makeStorage(files: Map<string, string> = new Map()): StorageBackend {
25
+ return {
26
+ mkdir: async () => {},
27
+ exists: async (p) => files.has(p),
28
+ existsSync: (p) => files.has(p),
29
+ size: async (p) => files.get(p)?.length ?? 0,
30
+ readText: async (p) => files.get(p) ?? "",
31
+ readTextSync: (p) => files.get(p) ?? "",
32
+ writeText: async (p, v) => { files.set(p, v) },
33
+ appendText: async (p, v) => { files.set(p, (files.get(p) ?? "") + v) },
34
+ rename: async () => {},
35
+ remove: async () => {},
36
+ }
37
+ }
38
+
39
+ function makeWriteChainRef() {
40
+ let chain = Promise.resolve()
41
+ return {
42
+ getWriteChain: () => chain,
43
+ setWriteChain: (p: Promise<void>) => { chain = p },
44
+ }
45
+ }
46
+
47
+ function makeDeps(overrides: Partial<PeripheralEventsDeps> = {}): PeripheralEventsDeps {
48
+ const wc = makeWriteChainRef()
49
+ return {
50
+ storage: makeStorage(),
51
+ tunnelLogPath: "/data/tunnels.jsonl",
52
+ sharesLogPath: "/data/shares.jsonl",
53
+ pushLogPath: "/data/push.jsonl",
54
+ tunnelEventsByChatId: new Map(),
55
+ shareEventsAll: [],
56
+ getWriteChain: wc.getWriteChain,
57
+ setWriteChain: wc.setWriteChain,
58
+ ...overrides,
59
+ }
60
+ }
61
+
62
+ function makeTunnelEvent(chatId = "chat-1"): CloudflareTunnelEvent {
63
+ return { type: "tunnel_started", chatId, tunnelId: "t-1", url: "https://ex.trycloudflare.com", timestamp: 1000 } as unknown as CloudflareTunnelEvent
64
+ }
65
+
66
+ function makeShareEvent(chatId = "chat-1"): ShareEvent {
67
+ return { type: "share_minted", chatId, shareId: "s-1", token: "tok", expiresAt: null, timestamp: 1000 } as unknown as ShareEvent
68
+ }
69
+
70
+ function makePushEvent(): PushEvent {
71
+ return { type: "push_device_registered", deviceId: "d-1", pushToken: "pt-1", platform: "web", timestamp: 1000 } as unknown as PushEvent
72
+ }
73
+
74
+ // ---------------------------------------------------------------------------
75
+ // Tunnel tests
76
+ // ---------------------------------------------------------------------------
77
+
78
+ describe("getTunnelEvents", () => {
79
+ test("returns empty array for unknown chatId", () => {
80
+ const deps = makeDeps()
81
+ expect(getTunnelEvents(deps, "no-chat")).toEqual([])
82
+ })
83
+
84
+ test("returns list for known chatId", () => {
85
+ const ev = makeTunnelEvent("chat-a")
86
+ const tunnelEventsByChatId = new Map([["chat-a", [ev]]])
87
+ const deps = makeDeps({ tunnelEventsByChatId })
88
+ expect(getTunnelEvents(deps, "chat-a")).toEqual([ev])
89
+ })
90
+
91
+ test("returns a copy (not the original array)", () => {
92
+ const ev = makeTunnelEvent("chat-a")
93
+ const inner: CloudflareTunnelEvent[] = [ev]
94
+ const tunnelEventsByChatId = new Map([["chat-a", inner]])
95
+ const deps = makeDeps({ tunnelEventsByChatId })
96
+ const result = getTunnelEvents(deps, "chat-a")
97
+ expect(result).toEqual([ev])
98
+ expect(result).not.toBe(inner)
99
+ })
100
+ })
101
+
102
+ describe("listTunnelChats", () => {
103
+ test("returns empty array when no chats", () => {
104
+ const deps = makeDeps()
105
+ expect(listTunnelChats(deps)).toEqual([])
106
+ })
107
+
108
+ test("returns all chatIds with tunnel events", () => {
109
+ const tunnelEventsByChatId = new Map([
110
+ ["c1", [makeTunnelEvent("c1")]],
111
+ ["c2", [makeTunnelEvent("c2")]],
112
+ ])
113
+ const deps = makeDeps({ tunnelEventsByChatId })
114
+ expect(listTunnelChats(deps)).toEqual(expect.arrayContaining(["c1", "c2"]))
115
+ })
116
+ })
117
+
118
+ describe("appendTunnelEvent", () => {
119
+ test("writes to tunnelLogPath and updates in-memory map", async () => {
120
+ const files = new Map([[ "/data/tunnels.jsonl", "" ]])
121
+ const storage = makeStorage(files)
122
+ const deps = makeDeps({ storage })
123
+ const ev = makeTunnelEvent("chat-1")
124
+
125
+ await appendTunnelEvent(deps, ev)
126
+
127
+ // Disk
128
+ const content = files.get("/data/tunnels.jsonl") ?? ""
129
+ expect(content.trim()).toBe(JSON.stringify(ev))
130
+
131
+ // In-memory map
132
+ expect(getTunnelEvents(deps, "chat-1")).toEqual([ev])
133
+ })
134
+
135
+ test("chains multiple appends in order", async () => {
136
+ const files = new Map([["/data/tunnels.jsonl", ""]])
137
+ const storage = makeStorage(files)
138
+ const deps = makeDeps({ storage })
139
+ const ev1 = { ...makeTunnelEvent("chat-1"), tunnelId: "t-1" }
140
+ const ev2 = { ...makeTunnelEvent("chat-1"), tunnelId: "t-2" }
141
+
142
+ await Promise.all([appendTunnelEvent(deps, ev1), appendTunnelEvent(deps, ev2)])
143
+
144
+ const lines = (files.get("/data/tunnels.jsonl") ?? "").trim().split("\n")
145
+ expect(lines.length).toBe(2)
146
+ expect(JSON.parse(lines[0]!).tunnelId).toBe("t-1")
147
+ expect(JSON.parse(lines[1]!).tunnelId).toBe("t-2")
148
+ })
149
+ })
150
+
151
+ describe("loadTunnelEvents", () => {
152
+ test("populates in-memory map from disk", async () => {
153
+ const ev = makeTunnelEvent("chat-x")
154
+ const files = new Map([["/data/tunnels.jsonl", `${JSON.stringify(ev)}\n`]])
155
+ const storage = makeStorage(files)
156
+ const tunnelEventsByChatId = new Map<string, CloudflareTunnelEvent[]>()
157
+ const deps = makeDeps({ storage, tunnelEventsByChatId })
158
+
159
+ await loadTunnelEvents(deps)
160
+
161
+ expect(getTunnelEvents(deps, "chat-x")).toEqual([ev])
162
+ })
163
+
164
+ test("handles empty log file without errors", async () => {
165
+ const files = new Map([["/data/tunnels.jsonl", ""]])
166
+ const deps = makeDeps({ storage: makeStorage(files) })
167
+ await expect(loadTunnelEvents(deps)).resolves.toBeUndefined()
168
+ })
169
+ })
170
+
171
+ // ---------------------------------------------------------------------------
172
+ // Share tests
173
+ // ---------------------------------------------------------------------------
174
+
175
+ describe("getShareEvents", () => {
176
+ test("returns empty array by default", () => {
177
+ const deps = makeDeps()
178
+ expect(getShareEvents(deps)).toEqual([])
179
+ })
180
+
181
+ test("returns a copy of shareEventsAll", () => {
182
+ const ev = makeShareEvent()
183
+ const shareEventsAll: ShareEvent[] = [ev]
184
+ const deps = makeDeps({ shareEventsAll })
185
+ const result = getShareEvents(deps)
186
+ expect(result).toEqual([ev])
187
+ expect(result).not.toBe(shareEventsAll)
188
+ })
189
+ })
190
+
191
+ describe("appendShareEvent", () => {
192
+ test("writes to sharesLogPath and pushes into shareEventsAll", async () => {
193
+ const files = new Map([["/data/shares.jsonl", ""]])
194
+ const storage = makeStorage(files)
195
+ const shareEventsAll: ShareEvent[] = []
196
+ const deps = makeDeps({ storage, shareEventsAll })
197
+ const ev = makeShareEvent("chat-2")
198
+
199
+ await appendShareEvent(deps, ev)
200
+
201
+ expect(files.get("/data/shares.jsonl")?.trim()).toBe(JSON.stringify(ev))
202
+ expect(shareEventsAll).toEqual([ev])
203
+ })
204
+ })
205
+
206
+ describe("loadShareEvents", () => {
207
+ test("fills shareEventsAll from disk", async () => {
208
+ const ev = makeShareEvent("chat-y")
209
+ const files = new Map([["/data/shares.jsonl", `${JSON.stringify(ev)}\n`]])
210
+ const storage = makeStorage(files)
211
+ const shareEventsAll: ShareEvent[] = []
212
+ const deps = makeDeps({ storage, shareEventsAll })
213
+
214
+ await loadShareEvents(deps)
215
+
216
+ expect(shareEventsAll).toEqual([ev])
217
+ })
218
+ })
219
+
220
+ // ---------------------------------------------------------------------------
221
+ // Push tests
222
+ // ---------------------------------------------------------------------------
223
+
224
+ describe("appendPushEvent", () => {
225
+ test("writes to pushLogPath (no in-memory state)", async () => {
226
+ const files = new Map([["/data/push.jsonl", ""]])
227
+ const storage = makeStorage(files)
228
+ const deps = makeDeps({ storage })
229
+ const ev = makePushEvent()
230
+
231
+ await appendPushEvent(deps, ev)
232
+
233
+ expect(files.get("/data/push.jsonl")?.trim()).toBe(JSON.stringify(ev))
234
+ })
235
+ })
236
+
237
+ describe("loadPushEvents", () => {
238
+ test("returns events from push log", async () => {
239
+ const ev = makePushEvent()
240
+ const files = new Map([["/data/push.jsonl", `${JSON.stringify(ev)}\n`]])
241
+ const deps = makeDeps({ storage: makeStorage(files) })
242
+
243
+ const result = await loadPushEvents(deps)
244
+
245
+ expect(result).toEqual([ev])
246
+ })
247
+
248
+ test("returns empty array for empty log", async () => {
249
+ const files = new Map([["/data/push.jsonl", ""]])
250
+ const deps = makeDeps({ storage: makeStorage(files) })
251
+ const result = await loadPushEvents(deps)
252
+ expect(result).toEqual([])
253
+ })
254
+ })
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Tunnel, Share, and Push peripheral event operations extracted from
3
+ * event-store.ts. All IO is performed through the injected StorageBackend.
4
+ *
5
+ * This module is an adapter (`.adapter.ts`) because it performs disk IO.
6
+ * It must NOT import from event-store.ts (no circular deps).
7
+ */
8
+ import type { StorageBackend } from "./storage/backend"
9
+ import type { CloudflareTunnelEvent } from "./cloudflare-tunnel/events"
10
+ import type { PushEvent } from "./push/events"
11
+ import type { ShareEvent } from "./session-share/share-projection"
12
+ import {
13
+ applyTunnelEventToMap,
14
+ loadPushEventsFromLog,
15
+ loadShareEventsFromLog,
16
+ loadTunnelEventsFromLog,
17
+ } from "./event-store-snapshot"
18
+
19
+ // ─── Deps interface ────────────────────────────────────────────────────────
20
+
21
+ export interface PeripheralEventsDeps {
22
+ readonly storage: StorageBackend
23
+ readonly tunnelLogPath: string
24
+ readonly sharesLogPath: string
25
+ readonly pushLogPath: string
26
+ readonly tunnelEventsByChatId: Map<string, CloudflareTunnelEvent[]>
27
+ /** Mutable share events array — methods push into this directly. */
28
+ readonly shareEventsAll: ShareEvent[]
29
+ /** Read the current write-chain promise. */
30
+ getWriteChain: () => Promise<void>
31
+ /** Replace the write-chain promise (called inside async chain links). */
32
+ setWriteChain: (p: Promise<void>) => void
33
+ }
34
+
35
+ // ─── Tunnel ────────────────────────────────────────────────────────────────
36
+
37
+ export async function appendTunnelEvent(
38
+ deps: PeripheralEventsDeps,
39
+ event: CloudflareTunnelEvent,
40
+ ): Promise<void> {
41
+ const payload = `${JSON.stringify(event)}\n`
42
+ const chain = deps.getWriteChain().then(async () => {
43
+ await deps.storage.appendText(deps.tunnelLogPath, payload)
44
+ applyTunnelEventToMap(deps.tunnelEventsByChatId, event)
45
+ })
46
+ deps.setWriteChain(chain)
47
+ await chain
48
+ }
49
+
50
+ export function getTunnelEvents(
51
+ deps: PeripheralEventsDeps,
52
+ chatId: string,
53
+ ): CloudflareTunnelEvent[] {
54
+ const list = deps.tunnelEventsByChatId.get(chatId)
55
+ return list ? [...list] : []
56
+ }
57
+
58
+ export function listTunnelChats(deps: PeripheralEventsDeps): string[] {
59
+ return [...deps.tunnelEventsByChatId.keys()]
60
+ }
61
+
62
+ export async function loadTunnelEvents(deps: PeripheralEventsDeps): Promise<void> {
63
+ await loadTunnelEventsFromLog(deps.storage, deps.tunnelLogPath, deps.tunnelEventsByChatId)
64
+ }
65
+
66
+ // ─── Share ─────────────────────────────────────────────────────────────────
67
+
68
+ export async function appendShareEvent(
69
+ deps: PeripheralEventsDeps,
70
+ event: ShareEvent,
71
+ ): Promise<void> {
72
+ const payload = `${JSON.stringify(event)}\n`
73
+ const chain = deps.getWriteChain().then(async () => {
74
+ await deps.storage.appendText(deps.sharesLogPath, payload)
75
+ deps.shareEventsAll.push(event)
76
+ })
77
+ deps.setWriteChain(chain)
78
+ await chain
79
+ }
80
+
81
+ export function getShareEvents(deps: PeripheralEventsDeps): ShareEvent[] {
82
+ return [...deps.shareEventsAll]
83
+ }
84
+
85
+ export async function loadShareEvents(deps: PeripheralEventsDeps): Promise<void> {
86
+ await loadShareEventsFromLog(deps.storage, deps.sharesLogPath, deps.shareEventsAll)
87
+ }
88
+
89
+ // ─── Push ──────────────────────────────────────────────────────────────────
90
+
91
+ export async function appendPushEvent(
92
+ deps: PeripheralEventsDeps,
93
+ event: PushEvent,
94
+ ): Promise<void> {
95
+ const payload = `${JSON.stringify(event)}\n`
96
+ const chain = deps.getWriteChain().then(async () => {
97
+ await deps.storage.appendText(deps.pushLogPath, payload)
98
+ })
99
+ deps.setWriteChain(chain)
100
+ await chain
101
+ }
102
+
103
+ export async function loadPushEvents(deps: PeripheralEventsDeps): Promise<PushEvent[]> {
104
+ return loadPushEventsFromLog(deps.storage, deps.pushLogPath)
105
+ }