airaknit 1.1.2-rc.9

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 (440) hide show
  1. package/LICENSE +84 -0
  2. package/README.md +202 -0
  3. package/bin/airaknit +9 -0
  4. package/bin/airaknit-project +14 -0
  5. package/bin/kanna +9 -0
  6. package/dist/client/assets/CompactSummaryMessage-Yw0BDWEJ.js +1 -0
  7. package/dist/client/assets/ExitPlanModeMessage-DIdkQ4uF.js +1 -0
  8. package/dist/client/assets/LocalFilePreviewDialog-DQx2eiCc.js +3 -0
  9. package/dist/client/assets/LocalProjectsSection-C4xlWkgS.js +1 -0
  10. package/dist/client/assets/TextMessage-B5G39DEJ.js +1 -0
  11. package/dist/client/assets/UserMessage-CIkWk-0L.js +1 -0
  12. package/dist/client/assets/_basePickBy-CVrAFfnZ.js +1 -0
  13. package/dist/client/assets/_baseUniq-JL-aaF4P.js +1 -0
  14. package/dist/client/assets/arc-B07zg7ol.js +1 -0
  15. package/dist/client/assets/architecture-YZFGNWBL-PSLVJL3p.js +1 -0
  16. package/dist/client/assets/architectureDiagram-Q4EWVU46-DfWIF1G_.js +36 -0
  17. package/dist/client/assets/array-BGFCBI0e.js +1 -0
  18. package/dist/client/assets/blockDiagram-DXYQGD6D-CzwIeo_B.js +132 -0
  19. package/dist/client/assets/bundle-mjs-BDE2gWbQ.js +1 -0
  20. package/dist/client/assets/button-DO50qOGv.js +1 -0
  21. package/dist/client/assets/c4Diagram-AHTNJAMY-CR8DCQRE.js +10 -0
  22. package/dist/client/assets/channel-Dj-UUfaF.js +1 -0
  23. package/dist/client/assets/chunk-2KRD3SAO-dznP-cn8.js +1 -0
  24. package/dist/client/assets/chunk-336JU56O-Dqss5Vu6.js +2 -0
  25. package/dist/client/assets/chunk-426QAEUC-CEKis_0_.js +1 -0
  26. package/dist/client/assets/chunk-4BX2VUAB-BYOv0Gm1.js +1 -0
  27. package/dist/client/assets/chunk-4TB4RGXK-BxzubH5S.js +206 -0
  28. package/dist/client/assets/chunk-55IACEB6-BSlTj03a.js +1 -0
  29. package/dist/client/assets/chunk-5FUZZQ4R-9Au93Bi1.js +62 -0
  30. package/dist/client/assets/chunk-5PVQY5BW-BhksHFEZ.js +2 -0
  31. package/dist/client/assets/chunk-67CJDMHE-BFwhz-8t.js +1 -0
  32. package/dist/client/assets/chunk-7N4EOEYR-BDUPds87.js +1 -0
  33. package/dist/client/assets/chunk-AA7GKIK3-CEWTdyXO.js +1 -0
  34. package/dist/client/assets/chunk-BO2N2NFS-D0LvxnhU.js +103 -0
  35. package/dist/client/assets/chunk-BSJP7CBP-BNJnK6sq.js +1 -0
  36. package/dist/client/assets/chunk-Bj-mKKzh.js +1 -0
  37. package/dist/client/assets/chunk-CIAEETIT-CYhfoCeN.js +1 -0
  38. package/dist/client/assets/chunk-EDXVE4YY-C5ovJLc0.js +1 -0
  39. package/dist/client/assets/chunk-ENJZ2VHE-HOhYaeGr.js +10 -0
  40. package/dist/client/assets/chunk-FMBD7UC4-BLCiKcAQ.js +15 -0
  41. package/dist/client/assets/chunk-FOC6F5B3-B6GtY2ek.js +1 -0
  42. package/dist/client/assets/chunk-ICPOFSXX-DPoIZoC5.js +122 -0
  43. package/dist/client/assets/chunk-K5T4RW27-BsKN63rv.js +94 -0
  44. package/dist/client/assets/chunk-KGLVRYIC-BUGn9uuY.js +1 -0
  45. package/dist/client/assets/chunk-LIHQZDEY-DhaZyo03.js +1 -0
  46. package/dist/client/assets/chunk-ORNJ4GCN-DlpeeJyi.js +1 -0
  47. package/dist/client/assets/chunk-OYMX7WX6-Dc9q7aYA.js +231 -0
  48. package/dist/client/assets/chunk-QZHKN3VN-BEdrPoSb.js +1 -0
  49. package/dist/client/assets/chunk-U2HBQHQK-CIB3Bjjd.js +70 -0
  50. package/dist/client/assets/chunk-X2U36JSP-CtB-o8Yp.js +1 -0
  51. package/dist/client/assets/chunk-XPW4576I-C6iHhX_8.js +32 -0
  52. package/dist/client/assets/chunk-YZCP3GAM-CTmKr6ZH.js +1 -0
  53. package/dist/client/assets/chunk-ZZ45TVLE-BgU8A2RF.js +1 -0
  54. package/dist/client/assets/classDiagram-6PBFFD2Q-Bqk5e679.js +1 -0
  55. package/dist/client/assets/classDiagram-v2-HSJHXN6E-6pSaZOkC.js +1 -0
  56. package/dist/client/assets/client-BrKWI4CM.js +1 -0
  57. package/dist/client/assets/client-CGgNRU9w.js +1 -0
  58. package/dist/client/assets/client-DMSLRzg9.js +6 -0
  59. package/dist/client/assets/clone-DWcL7whJ.js +1 -0
  60. package/dist/client/assets/cose-bilkent-S5V4N54A-CrV5wsV_.js +1 -0
  61. package/dist/client/assets/cytoscape.esm--aLzKuep.js +321 -0
  62. package/dist/client/assets/dagre-CuRxWcrj.js +1 -0
  63. package/dist/client/assets/dagre-KV5264BT-BIDiVnkA.js +4 -0
  64. package/dist/client/assets/defaultLocale-CRZydyG6.js +1 -0
  65. package/dist/client/assets/diagram-5BDNPKRD-i1kjKRCB.js +10 -0
  66. package/dist/client/assets/diagram-G4DWMVQ6-9ZSLuhbl.js +24 -0
  67. package/dist/client/assets/diagram-MMDJMWI5-B4_CUjgv.js +43 -0
  68. package/dist/client/assets/diagram-TYMM5635-Ct5eTGS8.js +24 -0
  69. package/dist/client/assets/dist-CuB4kiSK.js +1 -0
  70. package/dist/client/assets/erDiagram-SMLLAGMA-Cy38ercc.js +85 -0
  71. package/dist/client/assets/flowDiagram-DWJPFMVM-CZKuYl0V.js +162 -0
  72. package/dist/client/assets/ganttDiagram-T4ZO3ILL-DLPjCh7a.js +292 -0
  73. package/dist/client/assets/gitGraph-7Q5UKJZL-DqbrtEp9.js +1 -0
  74. package/dist/client/assets/gitGraphDiagram-UUTBAWPF-BoRBkDhQ.js +106 -0
  75. package/dist/client/assets/graphlib-BcQ6qlQh.js +1 -0
  76. package/dist/client/assets/highlighted-body-OFNGDK62-BEpBVDTX.js +1 -0
  77. package/dist/client/assets/index-CetCiuqP.js +105 -0
  78. package/dist/client/assets/index-N29Mip7A.css +1 -0
  79. package/dist/client/assets/info-OMHHGYJF-D98DRBJX.js +1 -0
  80. package/dist/client/assets/infoDiagram-42DDH7IO-BAcdTWbt.js +2 -0
  81. package/dist/client/assets/init-B8gtcn7T.js +1 -0
  82. package/dist/client/assets/isArrayLikeObject-D8SJFmkN.js +1 -0
  83. package/dist/client/assets/isEmpty-BF3YX5Jk.js +1 -0
  84. package/dist/client/assets/ishikawaDiagram-UXIWVN3A-Ynu2VKdC.js +70 -0
  85. package/dist/client/assets/journeyDiagram-VCZTEJTY-BjfhQaN3.js +139 -0
  86. package/dist/client/assets/jsx-runtime-CyI9ICYU.js +1 -0
  87. package/dist/client/assets/kanban-definition-6JOO6SKY-JLXH9zUJ.js +89 -0
  88. package/dist/client/assets/katex-B94qP8b6.js +265 -0
  89. package/dist/client/assets/lib--QVjyxmL.js +29 -0
  90. package/dist/client/assets/lib-B6rgJiZ9.js +1 -0
  91. package/dist/client/assets/line-DCrYfLBn.js +1 -0
  92. package/dist/client/assets/linear-_4upLmeo.js +1 -0
  93. package/dist/client/assets/mermaid-GHXKKRXX-rwJHYUmW.js +1 -0
  94. package/dist/client/assets/mermaid-parser.core-KZinfW8o.js +4 -0
  95. package/dist/client/assets/mermaid.core-QqY9gSNe.js +11 -0
  96. package/dist/client/assets/mindmap-definition-QFDTVHPH-TWgHDAzp.js +96 -0
  97. package/dist/client/assets/ordinal-CCj7PWgZ.js +1 -0
  98. package/dist/client/assets/packet-4T2RLAQJ-DEvfkn3F.js +1 -0
  99. package/dist/client/assets/path-DZF-JdEe.js +1 -0
  100. package/dist/client/assets/pie-ZZUOXDRM-72e6WVjb.js +1 -0
  101. package/dist/client/assets/pieDiagram-DEJITSTG-Cl8PCsoj.js +30 -0
  102. package/dist/client/assets/preload-helper-rov5CBGT.js +1 -0
  103. package/dist/client/assets/pty-client-DZ27IS00.js +1 -0
  104. package/dist/client/assets/ptyInstancesStore-D9ag7SYd.js +1 -0
  105. package/dist/client/assets/quadrantDiagram-34T5L4WZ-CHyVGp9E.js +7 -0
  106. package/dist/client/assets/radar-PYXPWWZC-Cp7xd_EY.js +1 -0
  107. package/dist/client/assets/react-CClhXMB2.js +1 -0
  108. package/dist/client/assets/react-Dd6D81m0.js +1 -0
  109. package/dist/client/assets/react-dom--G6_6fQ_.js +1 -0
  110. package/dist/client/assets/requirementDiagram-MS252O5E-DaanG2iM.js +84 -0
  111. package/dist/client/assets/rough.esm-BsmKo2S5.js +1 -0
  112. package/dist/client/assets/sankeyDiagram-XADWPNL6-B_fhLY36.js +10 -0
  113. package/dist/client/assets/sequenceDiagram-FGHM5R23-C5FNrveI.js +157 -0
  114. package/dist/client/assets/src-DeTlMJAU.js +1 -0
  115. package/dist/client/assets/stateDiagram-FHFEXIEX-nTTcdjjQ.js +1 -0
  116. package/dist/client/assets/stateDiagram-v2-QKLJ7IA2-Dw0632j_.js +1 -0
  117. package/dist/client/assets/timeline-definition-GMOUNBTQ-DkQV1yP8.js +120 -0
  118. package/dist/client/assets/treeView-SZITEDCU-ZLIgC7_K.js +1 -0
  119. package/dist/client/assets/treemap-W4RFUUIX-BqaMbB6N.js +1 -0
  120. package/dist/client/assets/uiIdentityOverlay-Ba7GNj7m.js +1 -0
  121. package/dist/client/assets/vennDiagram-DHZGUBPP-DbZ2xgs6.js +34 -0
  122. package/dist/client/assets/wardley-RL74JXVD-DXQS8zf4.js +1 -0
  123. package/dist/client/assets/wardleyDiagram-NUSXRM2D-BzCJ6MAu.js +20 -0
  124. package/dist/client/assets/xychartDiagram-5P7HB3ND-BSlFecop.js +7 -0
  125. package/dist/client/favicon.png +0 -0
  126. package/dist/client/favicon.svg +15 -0
  127. package/dist/client/fonts/body-medium.woff2 +0 -0
  128. package/dist/client/fonts/body-regular-italic.woff2 +0 -0
  129. package/dist/client/fonts/body-regular.woff2 +0 -0
  130. package/dist/client/fonts/body-semibold.woff2 +0 -0
  131. package/dist/client/index.html +31 -0
  132. package/dist/client/manifest.webmanifest +12 -0
  133. package/dist/client/sw.js +32 -0
  134. package/package.json +122 -0
  135. package/src/nats/auth-callout/callout-config.test.ts +93 -0
  136. package/src/nats/auth-callout/callout-config.ts +109 -0
  137. package/src/nats/auth-callout/callout.integration.test.ts +332 -0
  138. package/src/nats/auth-callout/keys.ts +103 -0
  139. package/src/nats/auth-callout/responder.ts +241 -0
  140. package/src/nats/auth-callout/scope-policy.test.ts +159 -0
  141. package/src/nats/auth-callout/scope-policy.ts +210 -0
  142. package/src/nats/auth-callout/token.test.ts +163 -0
  143. package/src/nats/auth-callout/token.ts +157 -0
  144. package/src/nats/nats-daemon-callout.ts +194 -0
  145. package/src/nats/nats-daemon.test.ts +77 -0
  146. package/src/nats/nats-daemon.ts +50 -0
  147. package/src/nats/nats-token.test.ts +61 -0
  148. package/src/nats/nats-token.ts +59 -0
  149. package/src/runner/coordination-mcp-integration.test.ts +134 -0
  150. package/src/runner/nats-coordination-client.test.ts +49 -0
  151. package/src/runner/nats-coordination-client.ts +94 -0
  152. package/src/runner/runner-agent.test.ts +469 -0
  153. package/src/runner/runner-agent.ts +453 -0
  154. package/src/runner/runner-credential.test.ts +93 -0
  155. package/src/runner/runner-credential.ts +82 -0
  156. package/src/runner/runner-nats.test.ts +495 -0
  157. package/src/runner/runner-nats.ts +323 -0
  158. package/src/runner/runner-pair.test.ts +107 -0
  159. package/src/runner/runner-pair.ts +81 -0
  160. package/src/runner/runner.test.ts +135 -0
  161. package/src/runner/runner.ts +212 -0
  162. package/src/runner/turn-factories.test.ts +97 -0
  163. package/src/runner/turn-factories.ts +475 -0
  164. package/src/server/agent-config-journey.test.ts +106 -0
  165. package/src/server/agent.ts +8 -0
  166. package/src/server/auto-continue/auth-error-detector.ts +66 -0
  167. package/src/server/auto-continue/limit-detector.ts +194 -0
  168. package/src/server/bm25.test.ts +92 -0
  169. package/src/server/bm25.ts +101 -0
  170. package/src/server/chat-events-jetstream.test.ts +135 -0
  171. package/src/server/claude-harness.ts +360 -0
  172. package/src/server/claude-pty/agent-normalizers.ts +309 -0
  173. package/src/server/claude-pty/auth.test.ts +38 -0
  174. package/src/server/claude-pty/auth.ts +32 -0
  175. package/src/server/claude-pty/claude-session-registry.adapter.ts +81 -0
  176. package/src/server/claude-pty/claude-session-registry.test.ts +149 -0
  177. package/src/server/claude-pty/driver.test.ts +902 -0
  178. package/src/server/claude-pty/driver.ts +807 -0
  179. package/src/server/claude-pty/jsonl-path.adapter.ts +57 -0
  180. package/src/server/claude-pty/jsonl-path.test.ts +114 -0
  181. package/src/server/claude-pty/jsonl-to-event.test.ts +241 -0
  182. package/src/server/claude-pty/jsonl-to-event.ts +174 -0
  183. package/src/server/claude-pty/output-ring.test.ts +35 -0
  184. package/src/server/claude-pty/output-ring.ts +25 -0
  185. package/src/server/claude-pty/parity-matrix.test.ts +227 -0
  186. package/src/server/claude-pty/pid-registry.adapter.ts +135 -0
  187. package/src/server/claude-pty/pid-registry.test.ts +122 -0
  188. package/src/server/claude-pty/preflight/binary-fingerprint.adapter.ts +20 -0
  189. package/src/server/claude-pty/preflight/binary-fingerprint.test.ts +32 -0
  190. package/src/server/claude-pty/pty-instance-registry.test.ts +177 -0
  191. package/src/server/claude-pty/pty-instance-registry.ts +166 -0
  192. package/src/server/claude-pty/pty-memory-sampler.adapter.test.ts +103 -0
  193. package/src/server/claude-pty/pty-memory-sampler.adapter.ts +85 -0
  194. package/src/server/claude-pty/pty-process.adapter.ts +66 -0
  195. package/src/server/claude-pty/pty-process.test.ts +49 -0
  196. package/src/server/claude-pty/resolve-binary.adapter.ts +106 -0
  197. package/src/server/claude-pty/resolve-binary.test.ts +118 -0
  198. package/src/server/claude-pty/runtime-dir.adapter.ts +19 -0
  199. package/src/server/claude-pty/settings-writer.adapter.ts +27 -0
  200. package/src/server/claude-pty/settings-writer.test.ts +22 -0
  201. package/src/server/claude-pty/smoke-test-io.adapter.ts +28 -0
  202. package/src/server/claude-pty/smoke-test.test.ts +191 -0
  203. package/src/server/claude-pty/smoke-test.ts +185 -0
  204. package/src/server/claude-pty/subagent-orchestrator.ts +887 -0
  205. package/src/server/claude-pty/tool-callback.ts +274 -0
  206. package/src/server/claude-pty/tui-control.test.ts +272 -0
  207. package/src/server/claude-pty/tui-control.ts +182 -0
  208. package/src/server/claude-pty/tui-source.adapter.test.ts +360 -0
  209. package/src/server/claude-pty/tui-source.adapter.ts +343 -0
  210. package/src/server/claude-pty/tunnel-gateway.ts +12 -0
  211. package/src/server/claude-pty-mcp/canonical-args.ts +15 -0
  212. package/src/server/claude-pty-mcp/fs-stat.adapter.ts +8 -0
  213. package/src/server/claude-pty-mcp/history-primer.ts +90 -0
  214. package/src/server/claude-pty-mcp/http-server.adapter.ts +33 -0
  215. package/src/server/claude-pty-mcp/mcp-http.ts +177 -0
  216. package/src/server/claude-pty-mcp/mcp.ts +412 -0
  217. package/src/server/claude-pty-mcp/mention-parser.ts +25 -0
  218. package/src/server/claude-pty-mcp/paths.ts +24 -0
  219. package/src/server/claude-pty-mcp/permission-gate.ts +243 -0
  220. package/src/server/claude-pty-mcp/terminal-pid-registry.adapter.ts +107 -0
  221. package/src/server/claude-pty-mcp/tools/ask-user-question.test.ts +119 -0
  222. package/src/server/claude-pty-mcp/tools/ask-user-question.ts +61 -0
  223. package/src/server/claude-pty-mcp/tools/bash.adapter.ts +76 -0
  224. package/src/server/claude-pty-mcp/tools/bash.test.ts +56 -0
  225. package/src/server/claude-pty-mcp/tools/delegate-subagent.test.ts +155 -0
  226. package/src/server/claude-pty-mcp/tools/delegate-subagent.ts +111 -0
  227. package/src/server/claude-pty-mcp/tools/edit.adapter.ts +95 -0
  228. package/src/server/claude-pty-mcp/tools/edit.test.ts +93 -0
  229. package/src/server/claude-pty-mcp/tools/exit-plan-mode.test.ts +61 -0
  230. package/src/server/claude-pty-mcp/tools/exit-plan-mode.ts +50 -0
  231. package/src/server/claude-pty-mcp/tools/glob.adapter.ts +86 -0
  232. package/src/server/claude-pty-mcp/tools/glob.test.ts +61 -0
  233. package/src/server/claude-pty-mcp/tools/grep.adapter.ts +126 -0
  234. package/src/server/claude-pty-mcp/tools/grep.test.ts +62 -0
  235. package/src/server/claude-pty-mcp/tools/read.adapter.ts +58 -0
  236. package/src/server/claude-pty-mcp/tools/read.test.ts +62 -0
  237. package/src/server/claude-pty-mcp/tools/tool-callback-shim.ts +42 -0
  238. package/src/server/claude-pty-mcp/tools/webfetch.test.ts +81 -0
  239. package/src/server/claude-pty-mcp/tools/webfetch.ts +82 -0
  240. package/src/server/claude-pty-mcp/tools/websearch.test.ts +40 -0
  241. package/src/server/claude-pty-mcp/tools/websearch.ts +42 -0
  242. package/src/server/claude-pty-mcp/tools/write.adapter.ts +60 -0
  243. package/src/server/claude-pty-mcp/tools/write.test.ts +52 -0
  244. package/src/server/claude-pty-mcp/uploads.adapter.ts +98 -0
  245. package/src/server/claude-pty-mcp/uploads.ts +38 -0
  246. package/src/server/claude-turn.test.ts +176 -0
  247. package/src/server/cli-runtime.test.ts +456 -0
  248. package/src/server/cli-runtime.ts +374 -0
  249. package/src/server/cli-supervisor.ts +81 -0
  250. package/src/server/cli.ts +78 -0
  251. package/src/server/client-log-forwarder.test.ts +74 -0
  252. package/src/server/client-log-forwarder.ts +75 -0
  253. package/src/server/codex-app-server-protocol.ts +449 -0
  254. package/src/server/codex-app-server.test.ts +2990 -0
  255. package/src/server/codex-app-server.ts +1713 -0
  256. package/src/server/coordination-integration.test.ts +63 -0
  257. package/src/server/coordination-mcp.test.ts +149 -0
  258. package/src/server/coordination-mcp.ts +197 -0
  259. package/src/server/delegation-coordinator.test.ts +675 -0
  260. package/src/server/delegation-coordinator.ts +454 -0
  261. package/src/server/discovery.test.ts +211 -0
  262. package/src/server/discovery.ts +301 -0
  263. package/src/server/event-store-agent-config.test.ts +124 -0
  264. package/src/server/event-store-coordination.test.ts +149 -0
  265. package/src/server/event-store-profile.test.ts +132 -0
  266. package/src/server/event-store-repo.test.ts +154 -0
  267. package/src/server/event-store-runner-team.test.ts +104 -0
  268. package/src/server/event-store.test.ts +342 -0
  269. package/src/server/event-store.ts +2208 -0
  270. package/src/server/events.ts +379 -0
  271. package/src/server/extension-router.test.ts +183 -0
  272. package/src/server/extension-router.ts +114 -0
  273. package/src/server/extensions/agents/server.test.ts +191 -0
  274. package/src/server/extensions/agents/server.ts +108 -0
  275. package/src/server/extensions/c3/server.test.ts +284 -0
  276. package/src/server/extensions/c3/server.ts +212 -0
  277. package/src/server/extensions/code/server.test.ts +200 -0
  278. package/src/server/extensions/code/server.ts +150 -0
  279. package/src/server/extensions.config.ts +10 -0
  280. package/src/server/external-open.ts +69 -0
  281. package/src/server/generate-fork-context.ts +58 -0
  282. package/src/server/generate-merge-context.test.ts +290 -0
  283. package/src/server/generate-merge-context.ts +141 -0
  284. package/src/server/generate-title.ts +36 -0
  285. package/src/server/git-clone-policy.test.ts +138 -0
  286. package/src/server/git-clone-policy.ts +27 -0
  287. package/src/server/harness-types.ts +1 -0
  288. package/src/server/journey-verification.test.ts +640 -0
  289. package/src/server/journey-verification.ts +195 -0
  290. package/src/server/machine-name.ts +22 -0
  291. package/src/server/nats-auth.test.ts +92 -0
  292. package/src/server/nats-auth.ts +6 -0
  293. package/src/server/nats-bind-guard.test.ts +71 -0
  294. package/src/server/nats-bind-guard.ts +42 -0
  295. package/src/server/nats-bridge.test.ts +141 -0
  296. package/src/server/nats-bridge.ts +111 -0
  297. package/src/server/nats-connector.test.ts +56 -0
  298. package/src/server/nats-connector.ts +71 -0
  299. package/src/server/nats-daemon-manager.test.ts +76 -0
  300. package/src/server/nats-daemon-manager.ts +174 -0
  301. package/src/server/nats-publisher.test.ts +356 -0
  302. package/src/server/nats-publisher.ts +271 -0
  303. package/src/server/nats-responders.test.ts +1018 -0
  304. package/src/server/nats-responders.ts +925 -0
  305. package/src/server/nats-streams.test.ts +112 -0
  306. package/src/server/nats-streams.ts +129 -0
  307. package/src/server/oauth-pool/oauth-responders.ts +185 -0
  308. package/src/server/oauth-pool/oauth-settings-store.ts +173 -0
  309. package/src/server/oauth-pool/oauth-token-pool.ts +303 -0
  310. package/src/server/orchestration.test.ts +1063 -0
  311. package/src/server/orchestration.ts +716 -0
  312. package/src/server/pairing-endpoints.test.ts +171 -0
  313. package/src/server/pairing-store.test.ts +154 -0
  314. package/src/server/pairing-store.ts +124 -0
  315. package/src/server/paths.ts +27 -0
  316. package/src/server/pr3-liveness.test.ts +252 -0
  317. package/src/server/process-utils.ts +10 -0
  318. package/src/server/project-cli.ts +180 -0
  319. package/src/server/provider-catalog.test.ts +177 -0
  320. package/src/server/provider-catalog.ts +146 -0
  321. package/src/server/pty-responders.ts +345 -0
  322. package/src/server/push-notifications.test.ts +234 -0
  323. package/src/server/push-notifications.ts +188 -0
  324. package/src/server/quick-response.test.ts +196 -0
  325. package/src/server/quick-response.ts +154 -0
  326. package/src/server/read-models-agent-config.test.ts +59 -0
  327. package/src/server/read-models-coordination.test.ts +69 -0
  328. package/src/server/read-models.test.ts +332 -0
  329. package/src/server/read-models.ts +258 -0
  330. package/src/server/repo-journey.test.ts +96 -0
  331. package/src/server/repo-manager.test.ts +118 -0
  332. package/src/server/repo-manager.ts +97 -0
  333. package/src/server/repo-status.test.ts +54 -0
  334. package/src/server/repo-status.ts +82 -0
  335. package/src/server/restart.test.ts +27 -0
  336. package/src/server/restart.ts +30 -0
  337. package/src/server/runner-incompatible-gate.test.ts +383 -0
  338. package/src/server/runner-manager.test.ts +72 -0
  339. package/src/server/runner-manager.ts +312 -0
  340. package/src/server/runner-pairing-urls.test.ts +59 -0
  341. package/src/server/runner-pairing-urls.ts +67 -0
  342. package/src/server/runner-proxy.test.ts +456 -0
  343. package/src/server/runner-proxy.ts +494 -0
  344. package/src/server/runner-router.test.ts +429 -0
  345. package/src/server/runner-router.ts +212 -0
  346. package/src/server/runner-routing.test.ts +584 -0
  347. package/src/server/runtime-registry.test.ts +436 -0
  348. package/src/server/runtime-registry.ts +307 -0
  349. package/src/server/sandbox-health.test.ts +127 -0
  350. package/src/server/sandbox-health.ts +94 -0
  351. package/src/server/sandbox-journey.test.ts +232 -0
  352. package/src/server/sandbox-manager.test.ts +146 -0
  353. package/src/server/sandbox-manager.ts +159 -0
  354. package/src/server/server.test.ts +287 -0
  355. package/src/server/server.ts +1108 -0
  356. package/src/server/session-discovery.test.ts +129 -0
  357. package/src/server/session-discovery.ts +475 -0
  358. package/src/server/session-index.test.ts +362 -0
  359. package/src/server/session-index.ts +119 -0
  360. package/src/server/session-seed.ts +288 -0
  361. package/src/server/share.test.ts +108 -0
  362. package/src/server/share.ts +113 -0
  363. package/src/server/skill-discovery.test.ts +201 -0
  364. package/src/server/skill-discovery.ts +77 -0
  365. package/src/server/storage/test-helpers.ts +67 -0
  366. package/src/server/terminal-manager.test.ts +309 -0
  367. package/src/server/terminal-manager.ts +354 -0
  368. package/src/server/transcript-consumer.test.ts +339 -0
  369. package/src/server/transcript-consumer.ts +162 -0
  370. package/src/server/transcript-search.test.ts +193 -0
  371. package/src/server/transcript-search.ts +83 -0
  372. package/src/server/transcript-utils.ts +52 -0
  373. package/src/server/update-manager.test.ts +107 -0
  374. package/src/server/update-manager.ts +230 -0
  375. package/src/server/workflow-engine.test.ts +251 -0
  376. package/src/server/workflow-engine.ts +169 -0
  377. package/src/server/workflow-mcp.ts +49 -0
  378. package/src/server/workflow-store.test.ts +155 -0
  379. package/src/server/workflow-store.ts +139 -0
  380. package/src/server/workspace-agent-integration.test.ts +167 -0
  381. package/src/server/workspace-agent-routes.test.ts +127 -0
  382. package/src/server/workspace-agent-routes.ts +89 -0
  383. package/src/server/workspace-agent.test.ts +103 -0
  384. package/src/server/workspace-agent.ts +102 -0
  385. package/src/server/workspace-cli.test.ts +79 -0
  386. package/src/server/workspace-config-manager.test.ts +109 -0
  387. package/src/server/workspace-config-manager.ts +83 -0
  388. package/src/server/workspace-directory-policy.test.ts +109 -0
  389. package/src/server/workspace-directory-policy.ts +56 -0
  390. package/src/shared/agent-config-types.ts +25 -0
  391. package/src/shared/branding.test.ts +42 -0
  392. package/src/shared/branding.ts +54 -0
  393. package/src/shared/compression.test.ts +85 -0
  394. package/src/shared/compression.ts +42 -0
  395. package/src/shared/coordination-store.test.ts +24 -0
  396. package/src/shared/coordination-store.ts +26 -0
  397. package/src/shared/dev-ports.test.ts +84 -0
  398. package/src/shared/dev-ports.ts +100 -0
  399. package/src/shared/extension-types.ts +45 -0
  400. package/src/shared/fork-presets.ts +54 -0
  401. package/src/shared/harness-types.test.ts +15 -0
  402. package/src/shared/harness-types.ts +21 -0
  403. package/src/shared/log-sink.test.ts +112 -0
  404. package/src/shared/log-sink.ts +185 -0
  405. package/src/shared/mention-pattern.ts +7 -0
  406. package/src/shared/merge-presets.ts +41 -0
  407. package/src/shared/nats-subjects.test.ts +61 -0
  408. package/src/shared/nats-subjects.ts +131 -0
  409. package/src/shared/permission-policy.ts +136 -0
  410. package/src/shared/ports.ts +3 -0
  411. package/src/shared/preset-types.ts +15 -0
  412. package/src/shared/profile-types.ts +49 -0
  413. package/src/shared/projectFileUrl.ts +36 -0
  414. package/src/shared/protocol.ts +153 -0
  415. package/src/shared/pty-instance.ts +43 -0
  416. package/src/shared/puggy/diagnostics/result.ts +18 -0
  417. package/src/shared/puggy/expressions/evaluate.ts +292 -0
  418. package/src/shared/puggy/index.test.ts +101 -0
  419. package/src/shared/puggy/index.ts +69 -0
  420. package/src/shared/puggy/parser/ast.ts +110 -0
  421. package/src/shared/puggy/parser/parser.ts +624 -0
  422. package/src/shared/puggy/renderer/html.ts +447 -0
  423. package/src/shared/runner-protocol.test.ts +277 -0
  424. package/src/shared/runner-protocol.ts +210 -0
  425. package/src/shared/runner-team-types.ts +73 -0
  426. package/src/shared/runtime-types.ts +48 -0
  427. package/src/shared/sandbox-types.ts +53 -0
  428. package/src/shared/tailwind-build.test.ts +12 -0
  429. package/src/shared/tinkaria-system-prompt.ts +101 -0
  430. package/src/shared/tools.test.ts +335 -0
  431. package/src/shared/tools.ts +397 -0
  432. package/src/shared/transcript-entries.ts +27 -0
  433. package/src/shared/transcript-render.test.ts +225 -0
  434. package/src/shared/transcript-render.ts +467 -0
  435. package/src/shared/types.ts +1031 -0
  436. package/src/shared/vite-config.test.ts +47 -0
  437. package/src/shared/web-context.test.ts +110 -0
  438. package/src/shared/web-context.ts +76 -0
  439. package/src/shared/workflow-types.ts +51 -0
  440. package/src/shared/workspace-types.ts +100 -0
@@ -0,0 +1,2208 @@
1
+ import { appendFile, mkdir, rename, writeFile } from "node:fs/promises"
2
+ import { homedir } from "node:os"
3
+ import path from "node:path"
4
+ import { getDataDir, LOG_PREFIX } from "../shared/branding"
5
+ import type { AgentConfig, AgentConfigRecord } from "../shared/agent-config-types"
6
+ import type { ProviderProfile, WorkspaceProfileOverride } from "../shared/profile-types"
7
+ import type { TeamMember } from "../shared/runner-team-types"
8
+ import type { AgentProvider, TranscriptEntry } from "../shared/types"
9
+ import { STORE_VERSION, compareIndependentWorkspaces } from "../shared/types"
10
+ import {
11
+ type ChatEvent,
12
+ type AgentConfigEvent,
13
+ type CoordinationEvent,
14
+ type MessageEvent,
15
+ type WorkspaceEvent,
16
+ type ProviderProfileEvent,
17
+ type ExtensionPreferenceEvent,
18
+ type RunnerTeamEvent,
19
+ type SandboxEvent,
20
+ type WorkflowEvent,
21
+ type SnapshotFile,
22
+ type StoreEvent,
23
+ type StoreState,
24
+ type RepoEvent,
25
+ type RepoRecord,
26
+ type TurnEvent,
27
+ type QueuedChatTurnRecord,
28
+ cloneTranscriptEntries,
29
+ createEmptyCoordinationState,
30
+ createEmptyState,
31
+ } from "./events"
32
+ import type { WorkflowRunState } from "../shared/workflow-types"
33
+ import type { SandboxRecord } from "../shared/sandbox-types"
34
+ import { resolveLocalPath } from "./paths"
35
+
36
+ const COMPACTION_THRESHOLD_BYTES = 2 * 1024 * 1024
37
+
38
+ interface LegacyTranscriptStats {
39
+ hasLegacyData: boolean
40
+ sources: Array<"snapshot" | "messages_log">
41
+ chatCount: number
42
+ entryCount: number
43
+ }
44
+
45
+ export class EventStore {
46
+ readonly dataDir: string
47
+ readonly state: StoreState = createEmptyState()
48
+ private writeChain = Promise.resolve()
49
+ private storageReset = false
50
+ private readonly snapshotPath: string
51
+ private readonly projectsLogPath: string
52
+ private readonly chatsLogPath: string
53
+ private readonly messagesLogPath: string
54
+ private readonly turnsLogPath: string
55
+ private readonly transcriptsDir: string
56
+ private legacyMessagesByChatId = new Map<string, TranscriptEntry[]>()
57
+ private snapshotHasLegacyMessages = false
58
+ private transcriptCache = new Map<string, TranscriptEntry[]>()
59
+ private static readonly TRANSCRIPT_CACHE_MAX = 5
60
+ private readonly coordinationLogPath: string
61
+ private readonly agentConfigsLogPath: string
62
+ private readonly reposLogPath: string
63
+ private readonly workflowsLogPath: string
64
+ private readonly sandboxLogPath: string
65
+ private readonly profilesLogPath: string
66
+ private readonly extensionPrefsLogPath: string
67
+ private readonly runnerTeamsLogPath: string
68
+ private readonly ptySubagentLogPath: string
69
+ private readonly ptyToolRequestsLogPath: string
70
+ private readonly ptySessionTokensLogPath: string
71
+ private readonly _ptySessionTokensByChatId = new Map<string, Map<string, string>>()
72
+
73
+ constructor(dataDir = getDataDir(homedir())) {
74
+ this.dataDir = dataDir
75
+ this.snapshotPath = path.join(this.dataDir, "snapshot.json")
76
+ this.projectsLogPath = path.join(this.dataDir, "projects.jsonl")
77
+ this.chatsLogPath = path.join(this.dataDir, "chats.jsonl")
78
+ this.messagesLogPath = path.join(this.dataDir, "messages.jsonl")
79
+ this.turnsLogPath = path.join(this.dataDir, "turns.jsonl")
80
+ this.transcriptsDir = path.join(this.dataDir, "transcripts")
81
+ this.coordinationLogPath = path.join(this.dataDir, "coordination.jsonl")
82
+ this.agentConfigsLogPath = path.join(this.dataDir, "agent-configs.jsonl")
83
+ this.reposLogPath = path.join(this.dataDir, "repos.jsonl")
84
+ this.workflowsLogPath = path.join(this.dataDir, "workflows.jsonl")
85
+ this.sandboxLogPath = path.join(this.dataDir, "sandbox.jsonl")
86
+ this.profilesLogPath = path.join(this.dataDir, "profiles.jsonl")
87
+ this.extensionPrefsLogPath = path.join(this.dataDir, "extension-prefs.jsonl")
88
+ this.runnerTeamsLogPath = path.join(this.dataDir, "runner-teams.jsonl")
89
+ this.ptySubagentLogPath = path.join(this.dataDir, "pty-subagent.jsonl")
90
+ this.ptyToolRequestsLogPath = path.join(this.dataDir, "pty-tool-requests.jsonl")
91
+ this.ptySessionTokensLogPath = path.join(this.dataDir, "pty-session-tokens.jsonl")
92
+ }
93
+
94
+ async initialize() {
95
+ await mkdir(this.dataDir, { recursive: true })
96
+ await mkdir(this.transcriptsDir, { recursive: true })
97
+ await this.ensureFile(this.projectsLogPath)
98
+ await this.ensureFile(this.chatsLogPath)
99
+ await this.ensureFile(this.messagesLogPath)
100
+ await this.ensureFile(this.turnsLogPath)
101
+ await this.ensureFile(this.coordinationLogPath)
102
+ await this.ensureFile(this.agentConfigsLogPath)
103
+ await this.ensureFile(this.reposLogPath)
104
+ await this.ensureFile(this.workflowsLogPath)
105
+ await this.ensureFile(this.sandboxLogPath)
106
+ await this.ensureFile(this.profilesLogPath)
107
+ await this.ensureFile(this.extensionPrefsLogPath)
108
+ await this.ensureFile(this.runnerTeamsLogPath)
109
+ await this.ensureFile(this.ptySubagentLogPath)
110
+ await this.ensureFile(this.ptyToolRequestsLogPath)
111
+ await this.ensureFile(this.ptySessionTokensLogPath)
112
+ await this.replayPtyLogs()
113
+ await this.loadSnapshot()
114
+ await this.replayLogs()
115
+ if (!(await this.hasLegacyTranscriptData()) && await this.shouldCompact()) {
116
+ await this.compact()
117
+ }
118
+ }
119
+
120
+ private async ensureFile(filePath: string) {
121
+ const file = Bun.file(filePath)
122
+ if (!(await file.exists())) {
123
+ await Bun.write(filePath, "")
124
+ }
125
+ }
126
+
127
+ private async clearStorage() {
128
+ if (this.storageReset) return
129
+ this.storageReset = true
130
+ this.resetState()
131
+ this.clearLegacyTranscriptState()
132
+ await Promise.all([
133
+ Bun.write(this.snapshotPath, ""),
134
+ Bun.write(this.projectsLogPath, ""),
135
+ Bun.write(this.chatsLogPath, ""),
136
+ Bun.write(this.messagesLogPath, ""),
137
+ Bun.write(this.turnsLogPath, ""),
138
+ Bun.write(this.coordinationLogPath, ""),
139
+ Bun.write(this.agentConfigsLogPath, ""),
140
+ Bun.write(this.reposLogPath, ""),
141
+ Bun.write(this.workflowsLogPath, ""),
142
+ Bun.write(this.sandboxLogPath, ""),
143
+ Bun.write(this.profilesLogPath, ""),
144
+ Bun.write(this.extensionPrefsLogPath, ""),
145
+ Bun.write(this.runnerTeamsLogPath, ""),
146
+ ])
147
+ }
148
+
149
+ private async loadSnapshot() {
150
+ const file = Bun.file(this.snapshotPath)
151
+ if (!(await file.exists())) return
152
+
153
+ try {
154
+ const text = await file.text()
155
+ if (!text.trim()) return
156
+ const parsed = JSON.parse(text) as SnapshotFile
157
+ if (parsed.v !== STORE_VERSION) {
158
+ console.warn(`${LOG_PREFIX} Resetting local chat history for store version ${STORE_VERSION}`)
159
+ await this.clearStorage()
160
+ return
161
+ }
162
+ for (const project of parsed.workspaces) {
163
+ this.state.workspacesById.set(project.id, { ...project })
164
+ this.state.workspaceIdsByPath.set(project.localPath, project.id)
165
+ }
166
+ if (parsed.independentWorkspaces?.length) {
167
+ for (const ws of parsed.independentWorkspaces) {
168
+ this.state.independentWorkspacesById.set(ws.id, { ...ws })
169
+ }
170
+ }
171
+ for (const chat of parsed.chats) {
172
+ this.state.chatsById.set(chat.id, { ...chat, unread: chat.unread ?? false, model: chat.model ?? null })
173
+ }
174
+ if (parsed.queuedTurns?.length) {
175
+ for (const queued of parsed.queuedTurns) {
176
+ this.state.queuedTurnsByChat.set(queued.chatId, { ...queued })
177
+ }
178
+ }
179
+ if (parsed.coordination?.length) {
180
+ for (const entry of parsed.coordination) {
181
+ const coord = createEmptyCoordinationState()
182
+ for (const todo of entry.todos) coord.todos.set(todo.id, todo)
183
+ for (const claim of entry.claims) coord.claims.set(claim.id, claim)
184
+ for (const wt of entry.worktrees) coord.worktrees.set(wt.id, wt)
185
+ for (const rule of entry.rules) coord.rules.set(rule.id, rule)
186
+ this.state.coordinationByWorkspace.set(entry.workspaceId, coord)
187
+ }
188
+ }
189
+ if (parsed.agentConfigs?.length) {
190
+ for (const entry of parsed.agentConfigs) {
191
+ const configMap = new Map<string, AgentConfigRecord>()
192
+ for (const record of entry.records) configMap.set(record.id, record)
193
+ this.state.agentConfigsByWorkspace.set(entry.workspaceId, configMap)
194
+ }
195
+ }
196
+ if (parsed.repos?.length) {
197
+ for (const repo of parsed.repos) {
198
+ this.state.reposById.set(repo.id, { ...repo })
199
+ if (repo.localPath) this.state.reposByPath.set(repo.localPath, repo.id)
200
+ }
201
+ }
202
+ if (parsed.workflowRuns?.length) {
203
+ for (const entry of parsed.workflowRuns) {
204
+ const runsMap = new Map<string, import("../shared/workflow-types").WorkflowRunState>()
205
+ for (const run of entry.runs) runsMap.set(run.runId, run)
206
+ this.state.workflowRunsByWorkspace.set(entry.workspaceId, runsMap)
207
+ }
208
+ }
209
+ if (parsed.sandboxes?.length) {
210
+ for (const sandbox of parsed.sandboxes) {
211
+ this.state.sandboxByWorkspace.set(sandbox.workspaceId, sandbox)
212
+ }
213
+ }
214
+ if (parsed.providerProfiles?.length) {
215
+ for (const record of parsed.providerProfiles) {
216
+ this.state.providerProfiles.set(record.id, { ...record })
217
+ }
218
+ }
219
+ if (parsed.workspaceProfileOverrides?.length) {
220
+ for (const override of parsed.workspaceProfileOverrides) {
221
+ const wsMap = this.state.workspaceProfileOverrides.get(override.workspaceId) ?? new Map<string, WorkspaceProfileOverride>()
222
+ wsMap.set(override.profileId, { ...override })
223
+ this.state.workspaceProfileOverrides.set(override.workspaceId, wsMap)
224
+ }
225
+ }
226
+ if (parsed.extensionPreferences?.length) {
227
+ for (const pref of parsed.extensionPreferences) {
228
+ this.state.extensionPreferences.set(pref.extensionId, { ...pref })
229
+ }
230
+ }
231
+ if (parsed.teamMembers?.length) {
232
+ for (const member of parsed.teamMembers) {
233
+ this.state.teamMembers.set(member.id, { ...member })
234
+ }
235
+ }
236
+ if (parsed.runnerLabels?.length) {
237
+ for (const label of parsed.runnerLabels) {
238
+ this.state.runnerLabels.set(label.runnerId, { ...label })
239
+ }
240
+ }
241
+ if (parsed.messages?.length) {
242
+ this.snapshotHasLegacyMessages = true
243
+ for (const messageSet of parsed.messages) {
244
+ this.legacyMessagesByChatId.set(messageSet.chatId, cloneTranscriptEntries(messageSet.entries))
245
+ }
246
+ }
247
+ } catch (error) {
248
+ console.warn(`${LOG_PREFIX} Failed to load snapshot, resetting local history:`, error)
249
+ await this.clearStorage()
250
+ }
251
+ }
252
+
253
+ private resetState() {
254
+ this.state.workspacesById.clear()
255
+ this.state.workspaceIdsByPath.clear()
256
+ this.state.chatsById.clear()
257
+ this.state.queuedTurnsByChat.clear()
258
+ this.state.coordinationByWorkspace.clear()
259
+ this.state.agentConfigsByWorkspace.clear()
260
+ this.state.reposById.clear()
261
+ this.state.reposByPath.clear()
262
+ this.state.workflowRunsByWorkspace.clear()
263
+ this.state.sandboxByWorkspace.clear()
264
+ this.state.providerProfiles.clear()
265
+ this.state.workspaceProfileOverrides.clear()
266
+ this.state.extensionPreferences.clear()
267
+ this.state.teamMembers.clear()
268
+ this.state.runnerLabels.clear()
269
+ this.transcriptCache.clear()
270
+ }
271
+
272
+ private clearLegacyTranscriptState() {
273
+ this.legacyMessagesByChatId.clear()
274
+ this.snapshotHasLegacyMessages = false
275
+ }
276
+
277
+ private async replayLogs() {
278
+ if (this.storageReset) return
279
+ await this.replayLog<WorkspaceEvent>(this.projectsLogPath)
280
+ if (this.storageReset) return
281
+ await this.replayLog<ChatEvent>(this.chatsLogPath)
282
+ if (this.storageReset) return
283
+ await this.replayLog<MessageEvent>(this.messagesLogPath)
284
+ if (this.storageReset) return
285
+ await this.replayLog<TurnEvent>(this.turnsLogPath)
286
+ if (this.storageReset) return
287
+ await this.replayLog<CoordinationEvent>(this.coordinationLogPath)
288
+ if (this.storageReset) return
289
+ await this.replayLog<AgentConfigEvent>(this.agentConfigsLogPath)
290
+ if (this.storageReset) return
291
+ await this.replayLog<RepoEvent>(this.reposLogPath)
292
+ if (this.storageReset) return
293
+ await this.replayLog<WorkflowEvent>(this.workflowsLogPath)
294
+ if (this.storageReset) return
295
+ await this.replayLog<SandboxEvent>(this.sandboxLogPath)
296
+ if (this.storageReset) return
297
+ await this.replayLog<ProviderProfileEvent>(this.profilesLogPath)
298
+ if (this.storageReset) return
299
+ await this.replayLog<ExtensionPreferenceEvent>(this.extensionPrefsLogPath)
300
+ if (this.storageReset) return
301
+ await this.replayLog<RunnerTeamEvent>(this.runnerTeamsLogPath)
302
+ }
303
+
304
+ private async replayLog<TEvent extends StoreEvent>(filePath: string) {
305
+ const file = Bun.file(filePath)
306
+ if (!(await file.exists())) return
307
+ const text = await file.text()
308
+ if (!text.trim()) return
309
+
310
+ const lines = text.split("\n")
311
+ let lastNonEmpty = -1
312
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
313
+ if (lines[index].trim()) {
314
+ lastNonEmpty = index
315
+ break
316
+ }
317
+ }
318
+
319
+ for (let index = 0; index < lines.length; index += 1) {
320
+ const line = lines[index].trim()
321
+ if (!line) continue
322
+ try {
323
+ const event = JSON.parse(line) as Partial<StoreEvent>
324
+ if (event.v !== STORE_VERSION) {
325
+ console.warn(`${LOG_PREFIX} Resetting local history from incompatible event log`)
326
+ await this.clearStorage()
327
+ return
328
+ }
329
+ this.applyEvent(event as StoreEvent)
330
+ } catch (error) {
331
+ if (index === lastNonEmpty) {
332
+ console.warn(`${LOG_PREFIX} Ignoring corrupt trailing line in ${path.basename(filePath)}`)
333
+ return
334
+ }
335
+ console.warn(`${LOG_PREFIX} Failed to replay ${path.basename(filePath)}, resetting local history:`, error)
336
+ await this.clearStorage()
337
+ return
338
+ }
339
+ }
340
+ }
341
+
342
+ private applyEvent(event: StoreEvent) {
343
+ switch (event.type) {
344
+ case "workspace_opened": {
345
+ const localPath = resolveLocalPath(event.localPath)
346
+ const project = {
347
+ id: event.workspaceId,
348
+ localPath,
349
+ title: event.title,
350
+ createdAt: event.timestamp,
351
+ updatedAt: event.timestamp,
352
+ }
353
+ this.state.workspacesById.set(project.id, project)
354
+ this.state.workspaceIdsByPath.set(localPath, project.id)
355
+ break
356
+ }
357
+ case "workspace_removed": {
358
+ const project = this.state.workspacesById.get(event.workspaceId)
359
+ if (!project) break
360
+ project.deletedAt = event.timestamp
361
+ project.updatedAt = event.timestamp
362
+ this.state.workspaceIdsByPath.delete(project.localPath)
363
+ break
364
+ }
365
+ case "independent_workspace_created": {
366
+ this.state.independentWorkspacesById.set(event.workspaceId, {
367
+ id: event.workspaceId,
368
+ name: event.name,
369
+ createdAt: event.timestamp,
370
+ updatedAt: event.timestamp,
371
+ })
372
+ break
373
+ }
374
+ case "independent_workspace_deleted": {
375
+ this.state.independentWorkspacesById.delete(event.workspaceId)
376
+ break
377
+ }
378
+ case "independent_workspace_renamed": {
379
+ const ws = this.state.independentWorkspacesById.get(event.workspaceId)
380
+ if (ws) {
381
+ ws.name = event.name
382
+ ws.updatedAt = event.timestamp
383
+ }
384
+ break
385
+ }
386
+ case "independent_workspace_pin_toggled": {
387
+ const ws = this.state.independentWorkspacesById.get(event.workspaceId)
388
+ if (ws) {
389
+ ws.pinned = event.pinned
390
+ ws.updatedAt = event.timestamp
391
+ }
392
+ break
393
+ }
394
+ case "independent_workspaces_reordered": {
395
+ event.orderedWorkspaceIds.forEach((id, index) => {
396
+ const ws = this.state.independentWorkspacesById.get(id)
397
+ if (ws) {
398
+ ws.sortOrder = index
399
+ ws.updatedAt = event.timestamp
400
+ }
401
+ })
402
+ break
403
+ }
404
+ case "chat_created": {
405
+ const chat = {
406
+ id: event.chatId,
407
+ workspaceId: event.workspaceId,
408
+ repoId: event.repoId ?? null,
409
+ title: event.title,
410
+ createdAt: event.timestamp,
411
+ updatedAt: event.timestamp,
412
+ unread: false,
413
+ provider: null,
414
+ model: null,
415
+ planMode: false,
416
+ sessionToken: null,
417
+ lastTurnOutcome: null,
418
+ }
419
+ this.state.chatsById.set(chat.id, chat)
420
+ break
421
+ }
422
+ case "chat_renamed": {
423
+ const chat = this.state.chatsById.get(event.chatId)
424
+ if (!chat) break
425
+ chat.title = event.title
426
+ chat.updatedAt = event.timestamp
427
+ break
428
+ }
429
+ case "chat_deleted": {
430
+ const chat = this.state.chatsById.get(event.chatId)
431
+ if (!chat) break
432
+ chat.deletedAt = event.timestamp
433
+ chat.updatedAt = event.timestamp
434
+ this.state.queuedTurnsByChat.delete(event.chatId)
435
+ break
436
+ }
437
+ case "chat_provider_set": {
438
+ const chat = this.state.chatsById.get(event.chatId)
439
+ if (!chat) break
440
+ chat.provider = event.provider
441
+ chat.updatedAt = event.timestamp
442
+ break
443
+ }
444
+ case "chat_model_set": {
445
+ const chat = this.state.chatsById.get(event.chatId)
446
+ if (!chat) break
447
+ chat.model = event.model
448
+ chat.updatedAt = event.timestamp
449
+ break
450
+ }
451
+ case "chat_plan_mode_set": {
452
+ const chat = this.state.chatsById.get(event.chatId)
453
+ if (!chat) break
454
+ chat.planMode = event.planMode
455
+ chat.updatedAt = event.timestamp
456
+ break
457
+ }
458
+ case "chat_read_state_set": {
459
+ const chat = this.state.chatsById.get(event.chatId)
460
+ if (!chat) break
461
+ chat.unread = event.unread
462
+ chat.updatedAt = event.timestamp
463
+ break
464
+ }
465
+ case "chat_runner_set": {
466
+ const chat = this.state.chatsById.get(event.chatId)
467
+ if (!chat) break
468
+ chat.runnerId = event.runnerId
469
+ chat.updatedAt = event.timestamp
470
+ break
471
+ }
472
+ case "message_appended": {
473
+ this.applyMessageMetadata(event.chatId, event.entry)
474
+ const existing = this.legacyMessagesByChatId.get(event.chatId) ?? []
475
+ existing.push({ ...event.entry })
476
+ this.legacyMessagesByChatId.set(event.chatId, existing)
477
+ break
478
+ }
479
+ case "turn_started": {
480
+ const chat = this.state.chatsById.get(event.chatId)
481
+ if (!chat) break
482
+ chat.updatedAt = event.timestamp
483
+ break
484
+ }
485
+ case "turn_finished": {
486
+ const chat = this.state.chatsById.get(event.chatId)
487
+ if (!chat) break
488
+ chat.updatedAt = event.timestamp
489
+ chat.lastTurnOutcome = "success"
490
+ chat.unread = true
491
+ break
492
+ }
493
+ case "turn_failed": {
494
+ const chat = this.state.chatsById.get(event.chatId)
495
+ if (!chat) break
496
+ chat.updatedAt = event.timestamp
497
+ chat.lastTurnOutcome = "failed"
498
+ chat.unread = true
499
+ break
500
+ }
501
+ case "turn_cancelled": {
502
+ const chat = this.state.chatsById.get(event.chatId)
503
+ if (!chat) break
504
+ chat.updatedAt = event.timestamp
505
+ chat.lastTurnOutcome = "cancelled"
506
+ break
507
+ }
508
+ case "session_token_set": {
509
+ const chat = this.state.chatsById.get(event.chatId)
510
+ if (!chat) break
511
+ chat.sessionToken = event.sessionToken
512
+ chat.updatedAt = event.timestamp
513
+ break
514
+ }
515
+ case "chat_turn_queued": {
516
+ const existing = this.state.queuedTurnsByChat.get(event.chatId)
517
+ const current = existing?.content.trim() ?? ""
518
+ const next = event.content.trim()
519
+ const content = current && next ? `${current}\n\n${next}` : next || current
520
+ if (!content) break
521
+ this.state.queuedTurnsByChat.set(event.chatId, {
522
+ chatId: event.chatId,
523
+ provider: event.provider ?? existing?.provider,
524
+ content,
525
+ model: event.model ?? existing?.model,
526
+ modelOptions: event.modelOptions ?? existing?.modelOptions,
527
+ effort: event.effort ?? existing?.effort,
528
+ planMode: event.planMode ?? existing?.planMode,
529
+ updatedAt: event.timestamp,
530
+ })
531
+ break
532
+ }
533
+ case "chat_queued_turn_cleared": {
534
+ this.state.queuedTurnsByChat.delete(event.chatId)
535
+ break
536
+ }
537
+ case "todo_added": {
538
+ const coord = this.getOrCreateCoordination(event.workspaceId)
539
+ coord.todos.set(event.todoId, {
540
+ id: event.todoId,
541
+ description: event.description,
542
+ priority: event.priority,
543
+ status: "open",
544
+ claimedBy: null,
545
+ outputs: [],
546
+ createdBy: event.createdBy,
547
+ createdAt: new Date(event.timestamp).toISOString(),
548
+ updatedAt: new Date(event.timestamp).toISOString(),
549
+ })
550
+ coord.lastUpdated = new Date(event.timestamp).toISOString()
551
+ break
552
+ }
553
+ case "todo_claimed": {
554
+ const coord = this.getOrCreateCoordination(event.workspaceId)
555
+ const todo = coord.todos.get(event.todoId)
556
+ if (!todo) break
557
+ todo.status = "claimed"
558
+ todo.claimedBy = event.claimedBy
559
+ todo.updatedAt = new Date(event.timestamp).toISOString()
560
+ coord.lastUpdated = new Date(event.timestamp).toISOString()
561
+ break
562
+ }
563
+ case "todo_completed": {
564
+ const coord = this.getOrCreateCoordination(event.workspaceId)
565
+ const todo = coord.todos.get(event.todoId)
566
+ if (!todo) break
567
+ todo.status = "complete"
568
+ todo.outputs = event.outputs
569
+ todo.updatedAt = new Date(event.timestamp).toISOString()
570
+ coord.lastUpdated = new Date(event.timestamp).toISOString()
571
+ break
572
+ }
573
+ case "todo_abandoned": {
574
+ const coord = this.getOrCreateCoordination(event.workspaceId)
575
+ const todo = coord.todos.get(event.todoId)
576
+ if (!todo) break
577
+ todo.status = "abandoned"
578
+ todo.updatedAt = new Date(event.timestamp).toISOString()
579
+ coord.lastUpdated = new Date(event.timestamp).toISOString()
580
+ break
581
+ }
582
+ case "claim_created": {
583
+ const coord = this.getOrCreateCoordination(event.workspaceId)
584
+ coord.claims.set(event.claimId, {
585
+ id: event.claimId,
586
+ intent: event.intent,
587
+ files: event.files,
588
+ sessionId: event.sessionId,
589
+ status: "active",
590
+ conflictsWith: null,
591
+ createdAt: new Date(event.timestamp).toISOString(),
592
+ })
593
+ coord.lastUpdated = new Date(event.timestamp).toISOString()
594
+ break
595
+ }
596
+ case "claim_released": {
597
+ const coord = this.getOrCreateCoordination(event.workspaceId)
598
+ const claim = coord.claims.get(event.claimId)
599
+ if (!claim) break
600
+ claim.status = "released"
601
+ coord.lastUpdated = new Date(event.timestamp).toISOString()
602
+ break
603
+ }
604
+ case "claim_conflict_detected": {
605
+ const coord = this.getOrCreateCoordination(event.workspaceId)
606
+ const claim = coord.claims.get(event.claimId)
607
+ if (!claim) break
608
+ claim.status = "conflict"
609
+ claim.conflictsWith = event.conflictsWith
610
+ coord.lastUpdated = new Date(event.timestamp).toISOString()
611
+ break
612
+ }
613
+ case "worktree_created": {
614
+ const coord = this.getOrCreateCoordination(event.workspaceId)
615
+ coord.worktrees.set(event.worktreeId, {
616
+ id: event.worktreeId,
617
+ branch: event.branch,
618
+ baseBranch: event.baseBranch,
619
+ path: event.path,
620
+ assignedTo: null,
621
+ status: "ready",
622
+ createdAt: new Date(event.timestamp).toISOString(),
623
+ })
624
+ coord.lastUpdated = new Date(event.timestamp).toISOString()
625
+ break
626
+ }
627
+ case "worktree_assigned": {
628
+ const coord = this.getOrCreateCoordination(event.workspaceId)
629
+ const wt = coord.worktrees.get(event.worktreeId)
630
+ if (!wt) break
631
+ wt.assignedTo = event.sessionId
632
+ wt.status = "assigned"
633
+ coord.lastUpdated = new Date(event.timestamp).toISOString()
634
+ break
635
+ }
636
+ case "worktree_removed": {
637
+ const coord = this.getOrCreateCoordination(event.workspaceId)
638
+ const wt = coord.worktrees.get(event.worktreeId)
639
+ if (!wt) break
640
+ wt.status = "removed"
641
+ wt.assignedTo = null
642
+ coord.lastUpdated = new Date(event.timestamp).toISOString()
643
+ break
644
+ }
645
+ case "rule_set": {
646
+ const coord = this.getOrCreateCoordination(event.workspaceId)
647
+ coord.rules.set(event.ruleId, {
648
+ id: event.ruleId,
649
+ content: event.content,
650
+ setBy: event.setBy,
651
+ updatedAt: new Date(event.timestamp).toISOString(),
652
+ })
653
+ coord.lastUpdated = new Date(event.timestamp).toISOString()
654
+ break
655
+ }
656
+ case "rule_removed": {
657
+ const coord = this.getOrCreateCoordination(event.workspaceId)
658
+ coord.rules.delete(event.ruleId)
659
+ coord.lastUpdated = new Date(event.timestamp).toISOString()
660
+ break
661
+ }
662
+ case "agent_config_saved": {
663
+ const workspaceMap = this.state.agentConfigsByWorkspace.get(event.workspaceId) ?? new Map<string, AgentConfigRecord>()
664
+ const existing = workspaceMap.get(event.agentId)
665
+ workspaceMap.set(event.agentId, {
666
+ id: event.agentId,
667
+ workspaceId: event.workspaceId,
668
+ config: event.config,
669
+ createdAt: existing?.createdAt ?? event.timestamp,
670
+ updatedAt: event.timestamp,
671
+ lastCommitHash: existing?.lastCommitHash,
672
+ })
673
+ this.state.agentConfigsByWorkspace.set(event.workspaceId, workspaceMap)
674
+ break
675
+ }
676
+ case "agent_config_committed": {
677
+ const record = this.state.agentConfigsByWorkspace.get(event.workspaceId)?.get(event.agentId)
678
+ if (record) record.lastCommitHash = event.commitHash
679
+ break
680
+ }
681
+ case "agent_config_removed": {
682
+ this.state.agentConfigsByWorkspace.get(event.workspaceId)?.delete(event.agentId)
683
+ break
684
+ }
685
+ case "repo_added": {
686
+ const repo: RepoRecord = {
687
+ id: event.id,
688
+ workspaceId: event.workspaceId,
689
+ origin: event.origin,
690
+ localPath: event.localPath,
691
+ label: event.label,
692
+ status: "cloned",
693
+ branch: event.branch,
694
+ createdAt: event.timestamp,
695
+ updatedAt: event.timestamp,
696
+ }
697
+ this.state.reposById.set(event.id, repo)
698
+ if (event.localPath) this.state.reposByPath.set(event.localPath, event.id)
699
+ break
700
+ }
701
+ case "repo_clone_started": {
702
+ const repo: RepoRecord = {
703
+ id: event.id,
704
+ workspaceId: event.workspaceId,
705
+ origin: event.origin,
706
+ localPath: event.targetPath,
707
+ label: event.label,
708
+ status: "pending",
709
+ branch: null,
710
+ createdAt: event.timestamp,
711
+ updatedAt: event.timestamp,
712
+ }
713
+ this.state.reposById.set(event.id, repo)
714
+ this.state.reposByPath.set(event.targetPath, event.id)
715
+ break
716
+ }
717
+ case "repo_cloned": {
718
+ const repo = this.state.reposById.get(event.id)
719
+ if (!repo) break
720
+ repo.status = "cloned"
721
+ repo.localPath = event.localPath
722
+ repo.branch = event.branch
723
+ repo.updatedAt = event.timestamp
724
+ this.state.reposByPath.set(event.localPath, event.id)
725
+ break
726
+ }
727
+ case "repo_clone_failed": {
728
+ const repo = this.state.reposById.get(event.id)
729
+ if (!repo) break
730
+ repo.status = "error"
731
+ repo.updatedAt = event.timestamp
732
+ break
733
+ }
734
+ case "repo_removed": {
735
+ const repo = this.state.reposById.get(event.id)
736
+ if (repo) {
737
+ this.state.reposByPath.delete(repo.localPath)
738
+ this.state.reposById.delete(event.id)
739
+ // Re-parent orphaned chats
740
+ for (const chat of this.state.chatsById.values()) {
741
+ if (chat.repoId === event.id) {
742
+ chat.repoId = null
743
+ }
744
+ }
745
+ }
746
+ break
747
+ }
748
+ case "repo_label_updated": {
749
+ const repo = this.state.reposById.get(event.id)
750
+ if (!repo) break
751
+ repo.label = event.label
752
+ repo.updatedAt = event.timestamp
753
+ break
754
+ }
755
+ case "workflow_started": {
756
+ const runsMap = this.state.workflowRunsByWorkspace.get(event.workspaceId) ?? new Map<string, WorkflowRunState>()
757
+ runsMap.set(event.runId, {
758
+ runId: event.runId,
759
+ workflowId: event.workflowId,
760
+ workspaceId: event.workspaceId,
761
+ targetRepoIds: event.targetRepoIds,
762
+ status: "running",
763
+ steps: [],
764
+ startedAt: event.timestamp,
765
+ triggeredBy: event.triggeredBy,
766
+ })
767
+ this.state.workflowRunsByWorkspace.set(event.workspaceId, runsMap)
768
+ break
769
+ }
770
+ case "workflow_step_started": {
771
+ const run = this.state.workflowRunsByWorkspace.get(event.workspaceId)?.get(event.runId)
772
+ if (!run) break
773
+ run.steps.push({
774
+ stepIndex: event.stepIndex,
775
+ mcp_tool: event.mcp_tool,
776
+ repoId: event.repoId,
777
+ status: "running",
778
+ startedAt: event.timestamp,
779
+ })
780
+ break
781
+ }
782
+ case "workflow_step_completed": {
783
+ const run = this.state.workflowRunsByWorkspace.get(event.workspaceId)?.get(event.runId)
784
+ if (!run) break
785
+ const step = run.steps.find((s) => s.stepIndex === event.stepIndex && s.repoId === event.repoId)
786
+ if (step) {
787
+ step.status = "completed"
788
+ step.output = event.output
789
+ step.completedAt = event.timestamp
790
+ }
791
+ break
792
+ }
793
+ case "workflow_step_failed": {
794
+ const run = this.state.workflowRunsByWorkspace.get(event.workspaceId)?.get(event.runId)
795
+ if (!run) break
796
+ const step = run.steps.find((s) => s.stepIndex === event.stepIndex && s.repoId === event.repoId)
797
+ if (step) {
798
+ step.status = "failed"
799
+ step.error = event.error
800
+ step.completedAt = event.timestamp
801
+ }
802
+ break
803
+ }
804
+ case "workflow_completed": {
805
+ const run = this.state.workflowRunsByWorkspace.get(event.workspaceId)?.get(event.runId)
806
+ if (!run) break
807
+ run.status = "completed"
808
+ run.completedAt = event.timestamp
809
+ break
810
+ }
811
+ case "workflow_failed": {
812
+ const run = this.state.workflowRunsByWorkspace.get(event.workspaceId)?.get(event.runId)
813
+ if (!run) break
814
+ run.status = "failed"
815
+ run.error = event.error
816
+ run.failedStep = event.failedStep
817
+ run.completedAt = event.timestamp
818
+ break
819
+ }
820
+ case "workflow_cancelled": {
821
+ const run = this.state.workflowRunsByWorkspace.get(event.workspaceId)?.get(event.runId)
822
+ if (!run) break
823
+ run.status = "cancelled"
824
+ run.completedAt = event.timestamp
825
+ break
826
+ }
827
+ case "sandbox_created": {
828
+ const record: SandboxRecord = {
829
+ id: event.id,
830
+ workspaceId: event.workspaceId,
831
+ containerId: null,
832
+ status: "creating",
833
+ resourceLimits: event.resourceLimits,
834
+ natsUrl: "",
835
+ createdAt: event.timestamp,
836
+ updatedAt: event.timestamp,
837
+ lastHealthCheck: null,
838
+ error: null,
839
+ }
840
+ this.state.sandboxByWorkspace.set(event.workspaceId, record)
841
+ break
842
+ }
843
+ case "sandbox_started": {
844
+ const existing = this.findSandboxById(event.id)
845
+ if (existing) {
846
+ existing.containerId = event.containerId
847
+ existing.natsUrl = event.natsUrl
848
+ existing.status = "running"
849
+ existing.updatedAt = event.timestamp
850
+ }
851
+ break
852
+ }
853
+ case "sandbox_stopped": {
854
+ const existing = this.findSandboxById(event.id)
855
+ if (existing) {
856
+ existing.status = "stopped"
857
+ existing.updatedAt = event.timestamp
858
+ }
859
+ break
860
+ }
861
+ case "sandbox_destroyed": {
862
+ for (const [key, sb] of this.state.sandboxByWorkspace) {
863
+ if (sb.id === event.id) { this.state.sandboxByWorkspace.delete(key); break }
864
+ }
865
+ break
866
+ }
867
+ case "sandbox_error": {
868
+ const existing = this.findSandboxById(event.id)
869
+ if (existing) {
870
+ existing.status = "error"
871
+ existing.error = event.error
872
+ existing.updatedAt = event.timestamp
873
+ }
874
+ break
875
+ }
876
+ case "sandbox_health_updated": {
877
+ const existing = this.findSandboxById(event.id)
878
+ if (existing) {
879
+ existing.lastHealthCheck = event.timestamp
880
+ existing.updatedAt = event.timestamp
881
+ }
882
+ break
883
+ }
884
+ case "provider_profile_saved": {
885
+ const existingProfile = this.state.providerProfiles.get(event.profileId)
886
+ this.state.providerProfiles.set(event.profileId, {
887
+ id: event.profileId,
888
+ profile: event.profile,
889
+ createdAt: existingProfile?.createdAt ?? event.timestamp,
890
+ updatedAt: event.timestamp,
891
+ })
892
+ break
893
+ }
894
+ case "provider_profile_removed": {
895
+ this.state.providerProfiles.delete(event.profileId)
896
+ for (const [, overrides] of this.state.workspaceProfileOverrides) {
897
+ overrides.delete(event.profileId)
898
+ }
899
+ break
900
+ }
901
+ case "team_member_saved": {
902
+ this.state.teamMembers.set(event.memberId, event.member)
903
+ break
904
+ }
905
+ case "team_member_removed": {
906
+ this.state.teamMembers.delete(event.memberId)
907
+ // Cascade: unassign any runner that pointed at the removed member.
908
+ for (const [runnerId, label] of this.state.runnerLabels) {
909
+ if (label.memberId === event.memberId) {
910
+ this.state.runnerLabels.set(runnerId, { ...label, memberId: null, updatedAt: event.timestamp })
911
+ }
912
+ }
913
+ break
914
+ }
915
+ case "runner_label_set": {
916
+ // Upsert by runnerId. A label with neither a name nor a member is still
917
+ // a valid record (the reducer never prunes — removal is explicit).
918
+ this.state.runnerLabels.set(event.runnerId, {
919
+ runnerId: event.runnerId,
920
+ name: event.name,
921
+ memberId: event.memberId,
922
+ updatedAt: event.timestamp,
923
+ })
924
+ break
925
+ }
926
+ case "runner_label_removed": {
927
+ this.state.runnerLabels.delete(event.runnerId)
928
+ break
929
+ }
930
+ case "workspace_profile_override_set": {
931
+ const wsOverrides = this.state.workspaceProfileOverrides.get(event.workspaceId) ?? new Map<string, WorkspaceProfileOverride>()
932
+ wsOverrides.set(event.profileId, {
933
+ profileId: event.profileId,
934
+ workspaceId: event.workspaceId,
935
+ overrides: event.overrides,
936
+ updatedAt: event.timestamp,
937
+ })
938
+ this.state.workspaceProfileOverrides.set(event.workspaceId, wsOverrides)
939
+ break
940
+ }
941
+ case "workspace_profile_override_removed": {
942
+ this.state.workspaceProfileOverrides.get(event.workspaceId)?.delete(event.profileId)
943
+ break
944
+ }
945
+ case "extension_preference_set": {
946
+ this.state.extensionPreferences.set(event.extensionId, {
947
+ extensionId: event.extensionId,
948
+ enabled: event.enabled,
949
+ updatedAt: event.timestamp,
950
+ })
951
+ break
952
+ }
953
+ case "delegation_initiated":
954
+ case "delegation_completed":
955
+ // Audit-only — KV is the source of truth for delegation state
956
+ break
957
+ }
958
+ }
959
+
960
+ private findSandboxById(id: string): SandboxRecord | undefined {
961
+ for (const sb of this.state.sandboxByWorkspace.values()) {
962
+ if (sb.id === id) return sb
963
+ }
964
+ return undefined
965
+ }
966
+
967
+ private getOrCreateCoordination(workspaceId: string) {
968
+ let coord = this.state.coordinationByWorkspace.get(workspaceId)
969
+ if (!coord) {
970
+ coord = createEmptyCoordinationState()
971
+ this.state.coordinationByWorkspace.set(workspaceId, coord)
972
+ }
973
+ return coord
974
+ }
975
+
976
+ private applyMessageMetadata(chatId: string, entry: TranscriptEntry) {
977
+ const chat = this.state.chatsById.get(chatId)
978
+ if (!chat) return
979
+ if (entry.kind === "user_prompt") {
980
+ chat.lastMessageAt = entry.createdAt
981
+ }
982
+ chat.updatedAt = Math.max(chat.updatedAt, entry.createdAt)
983
+ }
984
+
985
+ private append<TEvent extends StoreEvent>(filePath: string, event: TEvent) {
986
+ const payload = `${JSON.stringify(event)}\n`
987
+ this.writeChain = this.writeChain.then(async () => {
988
+ await appendFile(filePath, payload, "utf8")
989
+ this.applyEvent(event)
990
+ })
991
+ return this.writeChain
992
+ }
993
+
994
+ private transcriptPath(chatId: string) {
995
+ return path.join(this.transcriptsDir, `${chatId}.jsonl`)
996
+ }
997
+
998
+ private async loadTranscriptFromDisk(chatId: string): Promise<TranscriptEntry[]> {
999
+ const transcriptPath = this.transcriptPath(chatId)
1000
+ const file = Bun.file(transcriptPath)
1001
+ if (!await file.exists()) {
1002
+ return []
1003
+ }
1004
+
1005
+ const text = await file.text()
1006
+ if (!text.trim()) return []
1007
+
1008
+ const entries: TranscriptEntry[] = []
1009
+ for (const rawLine of text.split("\n")) {
1010
+ const line = rawLine.trim()
1011
+ if (!line) continue
1012
+ try {
1013
+ entries.push(JSON.parse(line) as TranscriptEntry)
1014
+ } catch (_err: unknown) {
1015
+ // Skip malformed JSONL lines — one bad line must not crash transcript loading
1016
+ }
1017
+ }
1018
+ return entries
1019
+ }
1020
+
1021
+ private setTranscriptCache(chatId: string, entries: TranscriptEntry[]) {
1022
+ this.transcriptCache.delete(chatId) // Remove to refresh insertion order
1023
+ this.transcriptCache.set(chatId, entries)
1024
+ if (this.transcriptCache.size > EventStore.TRANSCRIPT_CACHE_MAX) {
1025
+ const oldest = this.transcriptCache.keys().next().value
1026
+ if (oldest) this.transcriptCache.delete(oldest)
1027
+ }
1028
+ }
1029
+
1030
+ async openProject(localPath: string, title?: string) {
1031
+ const normalized = resolveLocalPath(localPath)
1032
+ const existingId = this.state.workspaceIdsByPath.get(normalized)
1033
+ if (existingId) {
1034
+ const existing = this.state.workspacesById.get(existingId)
1035
+ if (existing && !existing.deletedAt) {
1036
+ return existing
1037
+ }
1038
+ }
1039
+
1040
+ const workspaceId = crypto.randomUUID()
1041
+ const event: WorkspaceEvent = {
1042
+ v: STORE_VERSION,
1043
+ type: "workspace_opened",
1044
+ timestamp: Date.now(),
1045
+ workspaceId,
1046
+ localPath: normalized,
1047
+ title: title?.trim() || path.basename(normalized) || normalized,
1048
+ }
1049
+ await this.append(this.projectsLogPath, event)
1050
+ return this.state.workspacesById.get(workspaceId)!
1051
+ }
1052
+
1053
+ async removeProject(workspaceId: string) {
1054
+ const project = this.getProject(workspaceId)
1055
+ if (!project) {
1056
+ throw new Error("Project not found")
1057
+ }
1058
+
1059
+ const event: WorkspaceEvent = {
1060
+ v: STORE_VERSION,
1061
+ type: "workspace_removed",
1062
+ timestamp: Date.now(),
1063
+ workspaceId,
1064
+ }
1065
+ await this.append(this.projectsLogPath, event)
1066
+ }
1067
+
1068
+ async createIndependentWorkspace(name: string) {
1069
+ const workspaceId = crypto.randomUUID()
1070
+ const event: WorkspaceEvent = {
1071
+ v: STORE_VERSION,
1072
+ type: "independent_workspace_created",
1073
+ timestamp: Date.now(),
1074
+ workspaceId,
1075
+ name: name.trim(),
1076
+ }
1077
+ await this.append(this.projectsLogPath, event)
1078
+ return this.state.independentWorkspacesById.get(workspaceId)!
1079
+ }
1080
+
1081
+ async deleteIndependentWorkspace(workspaceId: string) {
1082
+ const workspace = this.state.independentWorkspacesById.get(workspaceId)
1083
+ if (!workspace) {
1084
+ throw new Error("Independent workspace not found")
1085
+ }
1086
+ const event: WorkspaceEvent = {
1087
+ v: STORE_VERSION,
1088
+ type: "independent_workspace_deleted",
1089
+ timestamp: Date.now(),
1090
+ workspaceId,
1091
+ }
1092
+ await this.append(this.projectsLogPath, event)
1093
+ }
1094
+
1095
+ async renameIndependentWorkspace(workspaceId: string, name: string) {
1096
+ if (!this.state.independentWorkspacesById.has(workspaceId)) {
1097
+ throw new Error("Independent workspace not found")
1098
+ }
1099
+ const event: WorkspaceEvent = {
1100
+ v: STORE_VERSION,
1101
+ type: "independent_workspace_renamed",
1102
+ timestamp: Date.now(),
1103
+ workspaceId,
1104
+ name: name.trim(),
1105
+ }
1106
+ await this.append(this.projectsLogPath, event)
1107
+ }
1108
+
1109
+ async setIndependentWorkspacePinned(workspaceId: string, pinned: boolean) {
1110
+ if (!this.state.independentWorkspacesById.has(workspaceId)) {
1111
+ throw new Error("Independent workspace not found")
1112
+ }
1113
+ const event: WorkspaceEvent = {
1114
+ v: STORE_VERSION,
1115
+ type: "independent_workspace_pin_toggled",
1116
+ timestamp: Date.now(),
1117
+ workspaceId,
1118
+ pinned,
1119
+ }
1120
+ await this.append(this.projectsLogPath, event)
1121
+ }
1122
+
1123
+ async reorderIndependentWorkspaces(orderedWorkspaceIds: string[]) {
1124
+ for (const id of orderedWorkspaceIds) {
1125
+ if (!this.state.independentWorkspacesById.has(id)) {
1126
+ throw new Error("Independent workspace not found")
1127
+ }
1128
+ }
1129
+ const event: WorkspaceEvent = {
1130
+ v: STORE_VERSION,
1131
+ type: "independent_workspaces_reordered",
1132
+ timestamp: Date.now(),
1133
+ orderedWorkspaceIds,
1134
+ }
1135
+ await this.append(this.projectsLogPath, event)
1136
+ }
1137
+
1138
+ listIndependentWorkspaces() {
1139
+ return [...this.state.independentWorkspacesById.values()].sort(compareIndependentWorkspaces)
1140
+ }
1141
+
1142
+ async createChat(workspaceId: string, repoId?: string, chatId?: string) {
1143
+ const project = this.state.workspacesById.get(workspaceId)
1144
+ if (!project || project.deletedAt) {
1145
+ throw new Error("Project not found")
1146
+ }
1147
+ const nextChatId = chatId ?? crypto.randomUUID()
1148
+ const existing = this.state.chatsById.get(nextChatId)
1149
+ if (existing && !existing.deletedAt) {
1150
+ if (existing.workspaceId !== workspaceId) {
1151
+ throw new Error("Chat already exists for another project")
1152
+ }
1153
+ return existing
1154
+ }
1155
+ const event: ChatEvent = {
1156
+ v: STORE_VERSION,
1157
+ type: "chat_created",
1158
+ timestamp: Date.now(),
1159
+ chatId: nextChatId,
1160
+ workspaceId,
1161
+ title: "New Chat",
1162
+ ...(repoId ? { repoId } : {}),
1163
+ }
1164
+ await this.append(this.chatsLogPath, event)
1165
+ return this.state.chatsById.get(nextChatId)!
1166
+ }
1167
+
1168
+ async renameChat(chatId: string, title: string) {
1169
+ const trimmed = title.trim()
1170
+ if (!trimmed) return
1171
+ const chat = this.requireChat(chatId)
1172
+ if (chat.title === trimmed) return
1173
+ const event: ChatEvent = {
1174
+ v: STORE_VERSION,
1175
+ type: "chat_renamed",
1176
+ timestamp: Date.now(),
1177
+ chatId,
1178
+ title: trimmed,
1179
+ }
1180
+ await this.append(this.chatsLogPath, event)
1181
+ }
1182
+
1183
+ async deleteChat(chatId: string) {
1184
+ this.requireChat(chatId)
1185
+ const event: ChatEvent = {
1186
+ v: STORE_VERSION,
1187
+ type: "chat_deleted",
1188
+ timestamp: Date.now(),
1189
+ chatId,
1190
+ }
1191
+ await this.append(this.chatsLogPath, event)
1192
+ }
1193
+
1194
+ async setChatProvider(chatId: string, provider: AgentProvider) {
1195
+ const chat = this.requireChat(chatId)
1196
+ if (chat.provider === provider) return
1197
+ const event: ChatEvent = {
1198
+ v: STORE_VERSION,
1199
+ type: "chat_provider_set",
1200
+ timestamp: Date.now(),
1201
+ chatId,
1202
+ provider,
1203
+ }
1204
+ await this.append(this.chatsLogPath, event)
1205
+ }
1206
+
1207
+ async setChatRunner(chatId: string, runnerId: string | null) {
1208
+ const chat = this.requireChat(chatId)
1209
+ if ((chat.runnerId ?? null) === runnerId) return
1210
+ const event: ChatEvent = {
1211
+ v: STORE_VERSION,
1212
+ type: "chat_runner_set",
1213
+ timestamp: Date.now(),
1214
+ chatId,
1215
+ runnerId,
1216
+ }
1217
+ await this.append(this.chatsLogPath, event)
1218
+ }
1219
+
1220
+ async setPlanMode(chatId: string, planMode: boolean) {
1221
+ const chat = this.requireChat(chatId)
1222
+ if (chat.planMode === planMode) return
1223
+ const event: ChatEvent = {
1224
+ v: STORE_VERSION,
1225
+ type: "chat_plan_mode_set",
1226
+ timestamp: Date.now(),
1227
+ chatId,
1228
+ planMode,
1229
+ }
1230
+ await this.append(this.chatsLogPath, event)
1231
+ }
1232
+
1233
+ async setChatModel(chatId: string, model: string | null) {
1234
+ const chat = this.requireChat(chatId)
1235
+ if ((chat.model ?? null) === model) return
1236
+ const event: ChatEvent = {
1237
+ v: STORE_VERSION,
1238
+ type: "chat_model_set",
1239
+ timestamp: Date.now(),
1240
+ chatId,
1241
+ model,
1242
+ }
1243
+ await this.append(this.chatsLogPath, event)
1244
+ }
1245
+
1246
+ async setChatReadState(chatId: string, unread: boolean) {
1247
+ const chat = this.requireChat(chatId)
1248
+ if (chat.unread === unread) return
1249
+ const event: ChatEvent = {
1250
+ v: STORE_VERSION,
1251
+ type: "chat_read_state_set",
1252
+ timestamp: Date.now(),
1253
+ chatId,
1254
+ unread,
1255
+ }
1256
+ await this.append(this.chatsLogPath, event)
1257
+ }
1258
+
1259
+ appendMessage(chatId: string, entry: TranscriptEntry) {
1260
+ this.requireChat(chatId)
1261
+ // In-memory first so NATS publish fires without waiting for disk I/O
1262
+ this.applyMessageMetadata(chatId, entry)
1263
+ const cached = this.transcriptCache.get(chatId)
1264
+ if (cached) {
1265
+ cached.push({ ...entry })
1266
+ }
1267
+ const payload = `${JSON.stringify(entry)}\n`
1268
+ const transcriptPath = this.transcriptPath(chatId)
1269
+ this.writeChain = this.writeChain.then(() => appendFile(transcriptPath, payload, "utf8"))
1270
+ }
1271
+
1272
+ async recordTurnStarted(chatId: string) {
1273
+ this.requireChat(chatId)
1274
+ const event: TurnEvent = {
1275
+ v: STORE_VERSION,
1276
+ type: "turn_started",
1277
+ timestamp: Date.now(),
1278
+ chatId,
1279
+ }
1280
+ await this.append(this.turnsLogPath, event)
1281
+ }
1282
+
1283
+ async recordTurnFinished(chatId: string) {
1284
+ this.requireChat(chatId)
1285
+ const event: TurnEvent = {
1286
+ v: STORE_VERSION,
1287
+ type: "turn_finished",
1288
+ timestamp: Date.now(),
1289
+ chatId,
1290
+ }
1291
+ await this.append(this.turnsLogPath, event)
1292
+ }
1293
+
1294
+ async recordTurnFailed(chatId: string, error: string) {
1295
+ this.requireChat(chatId)
1296
+ const event: TurnEvent = {
1297
+ v: STORE_VERSION,
1298
+ type: "turn_failed",
1299
+ timestamp: Date.now(),
1300
+ chatId,
1301
+ error,
1302
+ }
1303
+ await this.append(this.turnsLogPath, event)
1304
+ }
1305
+
1306
+ async recordTurnCancelled(chatId: string) {
1307
+ this.requireChat(chatId)
1308
+ const event: TurnEvent = {
1309
+ v: STORE_VERSION,
1310
+ type: "turn_cancelled",
1311
+ timestamp: Date.now(),
1312
+ chatId,
1313
+ }
1314
+ await this.append(this.turnsLogPath, event)
1315
+ }
1316
+
1317
+ async enqueueQueuedTurn(args: {
1318
+ chatId: string
1319
+ provider?: AgentProvider
1320
+ content: string
1321
+ model?: string
1322
+ modelOptions?: import("../shared/types").ModelOptions
1323
+ effort?: string
1324
+ planMode?: boolean
1325
+ }) {
1326
+ this.requireChat(args.chatId)
1327
+ const content = args.content.trim()
1328
+ if (!content) return
1329
+ const event: TurnEvent = {
1330
+ v: STORE_VERSION,
1331
+ type: "chat_turn_queued",
1332
+ timestamp: Date.now(),
1333
+ chatId: args.chatId,
1334
+ provider: args.provider,
1335
+ content,
1336
+ model: args.model,
1337
+ modelOptions: args.modelOptions,
1338
+ effort: args.effort,
1339
+ planMode: args.planMode,
1340
+ }
1341
+ await this.append(this.turnsLogPath, event)
1342
+ }
1343
+
1344
+ getQueuedTurn(chatId: string): QueuedChatTurnRecord | null {
1345
+ const queued = this.state.queuedTurnsByChat.get(chatId)
1346
+ return queued ? { ...queued } : null
1347
+ }
1348
+
1349
+ async clearQueuedTurn(chatId: string) {
1350
+ if (!this.state.queuedTurnsByChat.has(chatId)) return
1351
+ const event: TurnEvent = {
1352
+ v: STORE_VERSION,
1353
+ type: "chat_queued_turn_cleared",
1354
+ timestamp: Date.now(),
1355
+ chatId,
1356
+ }
1357
+ await this.append(this.turnsLogPath, event)
1358
+ }
1359
+
1360
+ async recordDelegationInitiated(workspaceId: string, args: { delegationId: string; parentChatId: string; childChatId: string; mode: "blocking" | "background"; resume: "immediate" | "gate" }) {
1361
+ const event: TurnEvent = {
1362
+ v: STORE_VERSION,
1363
+ type: "delegation_initiated",
1364
+ timestamp: Date.now(),
1365
+ delegationId: args.delegationId,
1366
+ parentChatId: args.parentChatId,
1367
+ childChatId: args.childChatId,
1368
+ workspaceId,
1369
+ mode: args.mode,
1370
+ resume: args.resume,
1371
+ }
1372
+ await this.append(this.turnsLogPath, event)
1373
+ }
1374
+
1375
+ async recordDelegationCompleted(workspaceId: string, args: { delegationId: string; parentChatId: string; childChatId: string; outcome: "completed" | "failed" | "orphaned" | "stale" }) {
1376
+ const event: TurnEvent = {
1377
+ v: STORE_VERSION,
1378
+ type: "delegation_completed",
1379
+ timestamp: Date.now(),
1380
+ delegationId: args.delegationId,
1381
+ parentChatId: args.parentChatId,
1382
+ childChatId: args.childChatId,
1383
+ workspaceId,
1384
+ outcome: args.outcome,
1385
+ }
1386
+ await this.append(this.turnsLogPath, event)
1387
+ }
1388
+
1389
+ async setSessionToken(chatId: string, sessionToken: string | null) {
1390
+ const chat = this.requireChat(chatId)
1391
+ if (chat.sessionToken === sessionToken) return
1392
+ const event: TurnEvent = {
1393
+ v: STORE_VERSION,
1394
+ type: "session_token_set",
1395
+ timestamp: Date.now(),
1396
+ chatId,
1397
+ sessionToken,
1398
+ }
1399
+ await this.append(this.turnsLogPath, event)
1400
+ }
1401
+
1402
+ // --- Coordination mutation methods ---
1403
+
1404
+ async addTodo(workspaceId: string, todoId: string, description: string, priority: "high" | "normal" | "low", createdBy: string) {
1405
+ const event: CoordinationEvent = { v: STORE_VERSION, type: "todo_added", timestamp: Date.now(), workspaceId, todoId, description, priority, createdBy }
1406
+ await this.append<CoordinationEvent>(this.coordinationLogPath, event)
1407
+ }
1408
+
1409
+ async claimTodo(workspaceId: string, todoId: string, claimedBy: string) {
1410
+ const event: CoordinationEvent = { v: STORE_VERSION, type: "todo_claimed", timestamp: Date.now(), workspaceId, todoId, claimedBy }
1411
+ await this.append<CoordinationEvent>(this.coordinationLogPath, event)
1412
+ }
1413
+
1414
+ async completeTodo(workspaceId: string, todoId: string, outputs: string[]) {
1415
+ const event: CoordinationEvent = { v: STORE_VERSION, type: "todo_completed", timestamp: Date.now(), workspaceId, todoId, outputs }
1416
+ await this.append<CoordinationEvent>(this.coordinationLogPath, event)
1417
+ }
1418
+
1419
+ async abandonTodo(workspaceId: string, todoId: string) {
1420
+ const event: CoordinationEvent = { v: STORE_VERSION, type: "todo_abandoned", timestamp: Date.now(), workspaceId, todoId }
1421
+ await this.append<CoordinationEvent>(this.coordinationLogPath, event)
1422
+ }
1423
+
1424
+ async createClaim(workspaceId: string, claimId: string, intent: string, files: string[], sessionId: string) {
1425
+ const event: CoordinationEvent = { v: STORE_VERSION, type: "claim_created", timestamp: Date.now(), workspaceId, claimId, intent, files, sessionId }
1426
+ await this.append<CoordinationEvent>(this.coordinationLogPath, event)
1427
+
1428
+ // Auto-detect file overlap with existing active claims.
1429
+ // INVARIANT: append() updates in-memory state synchronously via applyEvent(),
1430
+ // so coordinationByWorkspace is already current when we read it here.
1431
+ // Only the first overlapping claim triggers a conflict event (intentional —
1432
+ // downstream can trace the full conflict chain via claim_conflict_detected events).
1433
+ const coord = this.state.coordinationByWorkspace.get(workspaceId)
1434
+ if (coord) {
1435
+ const fileSet = new Set(files)
1436
+ for (const [existingId, existing] of coord.claims) {
1437
+ if (existingId === claimId || existing.status !== "active") continue
1438
+ const overlapping = existing.files.filter((f) => fileSet.has(f))
1439
+ if (overlapping.length > 0) {
1440
+ const conflictEvent: CoordinationEvent = {
1441
+ v: STORE_VERSION, type: "claim_conflict_detected", timestamp: Date.now(),
1442
+ workspaceId, claimId, conflictsWith: existingId, overlappingFiles: overlapping,
1443
+ }
1444
+ await this.append<CoordinationEvent>(this.coordinationLogPath, conflictEvent)
1445
+ break
1446
+ }
1447
+ }
1448
+ }
1449
+ }
1450
+
1451
+ async releaseClaim(workspaceId: string, claimId: string) {
1452
+ const event: CoordinationEvent = { v: STORE_VERSION, type: "claim_released", timestamp: Date.now(), workspaceId, claimId }
1453
+ await this.append<CoordinationEvent>(this.coordinationLogPath, event)
1454
+ }
1455
+
1456
+ async createWorktree(workspaceId: string, worktreeId: string, branch: string, baseBranch: string, wtPath: string) {
1457
+ const event: CoordinationEvent = { v: STORE_VERSION, type: "worktree_created", timestamp: Date.now(), workspaceId, worktreeId, branch, baseBranch, path: wtPath }
1458
+ await this.append<CoordinationEvent>(this.coordinationLogPath, event)
1459
+ }
1460
+
1461
+ async assignWorktree(workspaceId: string, worktreeId: string, sessionId: string) {
1462
+ const event: CoordinationEvent = { v: STORE_VERSION, type: "worktree_assigned", timestamp: Date.now(), workspaceId, worktreeId, sessionId }
1463
+ await this.append<CoordinationEvent>(this.coordinationLogPath, event)
1464
+ }
1465
+
1466
+ async removeWorktree(workspaceId: string, worktreeId: string) {
1467
+ const event: CoordinationEvent = { v: STORE_VERSION, type: "worktree_removed", timestamp: Date.now(), workspaceId, worktreeId }
1468
+ await this.append<CoordinationEvent>(this.coordinationLogPath, event)
1469
+ }
1470
+
1471
+ async setRule(workspaceId: string, ruleId: string, content: string, setBy: string) {
1472
+ const event: CoordinationEvent = { v: STORE_VERSION, type: "rule_set", timestamp: Date.now(), workspaceId, ruleId, content, setBy }
1473
+ await this.append<CoordinationEvent>(this.coordinationLogPath, event)
1474
+ }
1475
+
1476
+ async removeRule(workspaceId: string, ruleId: string) {
1477
+ const event: CoordinationEvent = { v: STORE_VERSION, type: "rule_removed", timestamp: Date.now(), workspaceId, ruleId }
1478
+ await this.append<CoordinationEvent>(this.coordinationLogPath, event)
1479
+ }
1480
+
1481
+ // --- Agent config mutation methods ---
1482
+
1483
+ async saveAgentConfig(workspaceId: string, agentId: string, config: AgentConfig) {
1484
+ const event: AgentConfigEvent = { v: STORE_VERSION, type: "agent_config_saved", timestamp: Date.now(), workspaceId, agentId, config }
1485
+ await this.append<AgentConfigEvent>(this.agentConfigsLogPath, event)
1486
+ }
1487
+
1488
+ async commitAgentConfig(workspaceId: string, agentId: string, commitHash: string) {
1489
+ const event: AgentConfigEvent = { v: STORE_VERSION, type: "agent_config_committed", timestamp: Date.now(), workspaceId, agentId, commitHash }
1490
+ await this.append<AgentConfigEvent>(this.agentConfigsLogPath, event)
1491
+ }
1492
+
1493
+ async removeAgentConfig(workspaceId: string, agentId: string) {
1494
+ const event: AgentConfigEvent = { v: STORE_VERSION, type: "agent_config_removed", timestamp: Date.now(), workspaceId, agentId }
1495
+ await this.append<AgentConfigEvent>(this.agentConfigsLogPath, event)
1496
+ }
1497
+
1498
+ // --- Provider profile mutation methods ---
1499
+
1500
+ async saveProviderProfile(profileId: string, profile: ProviderProfile) {
1501
+ const event: ProviderProfileEvent = { v: STORE_VERSION, type: "provider_profile_saved", timestamp: Date.now(), profileId, profile }
1502
+ await this.append<ProviderProfileEvent>(this.profilesLogPath, event)
1503
+ }
1504
+
1505
+ async removeProviderProfile(profileId: string) {
1506
+ const event: ProviderProfileEvent = { v: STORE_VERSION, type: "provider_profile_removed", timestamp: Date.now(), profileId }
1507
+ await this.append<ProviderProfileEvent>(this.profilesLogPath, event)
1508
+ }
1509
+
1510
+ async setWorkspaceProfileOverride(workspaceId: string, profileId: string, overrides: Partial<Omit<ProviderProfile, "id" | "provider">>) {
1511
+ const event: ProviderProfileEvent = { v: STORE_VERSION, type: "workspace_profile_override_set", timestamp: Date.now(), workspaceId, profileId, overrides }
1512
+ await this.append<ProviderProfileEvent>(this.profilesLogPath, event)
1513
+ }
1514
+
1515
+ async removeWorkspaceProfileOverride(workspaceId: string, profileId: string) {
1516
+ const event: ProviderProfileEvent = { v: STORE_VERSION, type: "workspace_profile_override_removed", timestamp: Date.now(), workspaceId, profileId }
1517
+ await this.append<ProviderProfileEvent>(this.profilesLogPath, event)
1518
+ }
1519
+
1520
+ // --- Extension preference mutation methods ---
1521
+
1522
+ async setExtensionPreference(extensionId: string, enabled: boolean) {
1523
+ const event: ExtensionPreferenceEvent = { v: STORE_VERSION, type: "extension_preference_set", timestamp: Date.now(), extensionId, enabled }
1524
+ await this.append<ExtensionPreferenceEvent>(this.extensionPrefsLogPath, event)
1525
+ }
1526
+
1527
+ // --- Runner team mutation methods (US-RTN) ---
1528
+
1529
+ async saveTeamMember(member: TeamMember) {
1530
+ const event: RunnerTeamEvent = { v: STORE_VERSION, type: "team_member_saved", timestamp: Date.now(), memberId: member.id, member }
1531
+ await this.append<RunnerTeamEvent>(this.runnerTeamsLogPath, event)
1532
+ }
1533
+
1534
+ async removeTeamMember(memberId: string) {
1535
+ const event: RunnerTeamEvent = { v: STORE_VERSION, type: "team_member_removed", timestamp: Date.now(), memberId }
1536
+ await this.append<RunnerTeamEvent>(this.runnerTeamsLogPath, event)
1537
+ }
1538
+
1539
+ async setRunnerLabel(runnerId: string, name: string | null, memberId: string | null) {
1540
+ const event: RunnerTeamEvent = { v: STORE_VERSION, type: "runner_label_set", timestamp: Date.now(), runnerId, name, memberId }
1541
+ await this.append<RunnerTeamEvent>(this.runnerTeamsLogPath, event)
1542
+ }
1543
+
1544
+ async removeRunnerLabel(runnerId: string) {
1545
+ const event: RunnerTeamEvent = { v: STORE_VERSION, type: "runner_label_removed", timestamp: Date.now(), runnerId }
1546
+ await this.append<RunnerTeamEvent>(this.runnerTeamsLogPath, event)
1547
+ }
1548
+
1549
+ // --- Repo mutation methods ---
1550
+
1551
+ async addRepo(id: string, workspaceId: string, localPath: string, origin: string | null, label: string | null, branch: string | null) {
1552
+ const event: RepoEvent = { v: STORE_VERSION, type: "repo_added", timestamp: Date.now(), id, workspaceId, localPath, origin, label, branch }
1553
+ await this.append<RepoEvent>(this.reposLogPath, event)
1554
+ }
1555
+
1556
+ async startRepoClone(id: string, workspaceId: string, origin: string, targetPath: string, label: string | null) {
1557
+ const event: RepoEvent = { v: STORE_VERSION, type: "repo_clone_started", timestamp: Date.now(), id, workspaceId, origin, targetPath, label }
1558
+ await this.append<RepoEvent>(this.reposLogPath, event)
1559
+ }
1560
+
1561
+ async markRepoCloned(id: string, localPath: string, branch: string | null) {
1562
+ const event: RepoEvent = { v: STORE_VERSION, type: "repo_cloned", timestamp: Date.now(), id, localPath, branch }
1563
+ await this.append<RepoEvent>(this.reposLogPath, event)
1564
+ }
1565
+
1566
+ async markRepoCloneFailed(id: string, error: string) {
1567
+ const event: RepoEvent = { v: STORE_VERSION, type: "repo_clone_failed", timestamp: Date.now(), id, error }
1568
+ await this.append<RepoEvent>(this.reposLogPath, event)
1569
+ }
1570
+
1571
+ async removeRepo(id: string, workspaceId: string) {
1572
+ const event: RepoEvent = { v: STORE_VERSION, type: "repo_removed", timestamp: Date.now(), id, workspaceId }
1573
+ await this.append<RepoEvent>(this.reposLogPath, event)
1574
+ }
1575
+
1576
+ async updateRepoLabel(id: string, label: string) {
1577
+ const event: RepoEvent = { v: STORE_VERSION, type: "repo_label_updated", timestamp: Date.now(), id, label }
1578
+ await this.append<RepoEvent>(this.reposLogPath, event)
1579
+ }
1580
+
1581
+ // --- Workflow mutation methods ---
1582
+
1583
+ async emitWorkflowStarted(runId: string, workflowId: string, workspaceId: string, targetRepoIds: string[], triggeredBy: string) {
1584
+ const event: WorkflowEvent = { v: STORE_VERSION, type: "workflow_started", timestamp: Date.now(), runId, workflowId, workspaceId, targetRepoIds, triggeredBy }
1585
+ await this.append<WorkflowEvent>(this.workflowsLogPath, event)
1586
+ }
1587
+
1588
+ async emitWorkflowStepStarted(runId: string, workspaceId: string, stepIndex: number, mcpTool: string, repoId?: string) {
1589
+ const event: WorkflowEvent = { v: STORE_VERSION, type: "workflow_step_started", timestamp: Date.now(), runId, workspaceId, stepIndex, mcp_tool: mcpTool, ...(repoId !== undefined ? { repoId } : {}) }
1590
+ await this.append<WorkflowEvent>(this.workflowsLogPath, event)
1591
+ }
1592
+
1593
+ async emitWorkflowStepCompleted(runId: string, workspaceId: string, stepIndex: number, output: string, repoId?: string) {
1594
+ const event: WorkflowEvent = { v: STORE_VERSION, type: "workflow_step_completed", timestamp: Date.now(), runId, workspaceId, stepIndex, output, ...(repoId !== undefined ? { repoId } : {}) }
1595
+ await this.append<WorkflowEvent>(this.workflowsLogPath, event)
1596
+ }
1597
+
1598
+ async emitWorkflowStepFailed(runId: string, workspaceId: string, stepIndex: number, error: string, repoId?: string) {
1599
+ const event: WorkflowEvent = { v: STORE_VERSION, type: "workflow_step_failed", timestamp: Date.now(), runId, workspaceId, stepIndex, error, ...(repoId !== undefined ? { repoId } : {}) }
1600
+ await this.append<WorkflowEvent>(this.workflowsLogPath, event)
1601
+ }
1602
+
1603
+ async emitWorkflowCompleted(runId: string, workspaceId: string) {
1604
+ const event: WorkflowEvent = { v: STORE_VERSION, type: "workflow_completed", timestamp: Date.now(), runId, workspaceId }
1605
+ await this.append<WorkflowEvent>(this.workflowsLogPath, event)
1606
+ }
1607
+
1608
+ async emitWorkflowFailed(runId: string, workspaceId: string, error: string, failedStep: number) {
1609
+ const event: WorkflowEvent = { v: STORE_VERSION, type: "workflow_failed", timestamp: Date.now(), runId, workspaceId, error, failedStep }
1610
+ await this.append<WorkflowEvent>(this.workflowsLogPath, event)
1611
+ }
1612
+
1613
+ async emitWorkflowCancelled(runId: string, workspaceId: string) {
1614
+ const event: WorkflowEvent = { v: STORE_VERSION, type: "workflow_cancelled", timestamp: Date.now(), runId, workspaceId }
1615
+ await this.append<WorkflowEvent>(this.workflowsLogPath, event)
1616
+ }
1617
+
1618
+ // --- Sandbox mutation methods ---
1619
+
1620
+ async emitSandboxCreated(id: string, workspaceId: string, resourceLimits: import("../shared/sandbox-types").ResourceLimits) {
1621
+ await this.append<SandboxEvent>(this.sandboxLogPath, { v: 3, type: "sandbox_created", timestamp: Date.now(), id, workspaceId, resourceLimits })
1622
+ }
1623
+
1624
+ async emitSandboxStarted(id: string, containerId: string, natsUrl: string) {
1625
+ await this.append<SandboxEvent>(this.sandboxLogPath, { v: 3, type: "sandbox_started", timestamp: Date.now(), id, containerId, natsUrl })
1626
+ }
1627
+
1628
+ async emitSandboxStopped(id: string, reason: string) {
1629
+ await this.append<SandboxEvent>(this.sandboxLogPath, { v: 3, type: "sandbox_stopped", timestamp: Date.now(), id, reason })
1630
+ }
1631
+
1632
+ async emitSandboxDestroyed(id: string) {
1633
+ await this.append<SandboxEvent>(this.sandboxLogPath, { v: 3, type: "sandbox_destroyed", timestamp: Date.now(), id })
1634
+ }
1635
+
1636
+ async emitSandboxError(id: string, error: string) {
1637
+ await this.append<SandboxEvent>(this.sandboxLogPath, { v: 3, type: "sandbox_error", timestamp: Date.now(), id, error })
1638
+ }
1639
+
1640
+ async emitSandboxHealthUpdated(id: string, health: import("../shared/sandbox-types").SandboxHealthReport) {
1641
+ await this.append<SandboxEvent>(this.sandboxLogPath, { v: 3, type: "sandbox_health_updated", timestamp: Date.now(), id, health })
1642
+ }
1643
+
1644
+ getProject(workspaceId: string) {
1645
+ const project = this.state.workspacesById.get(workspaceId)
1646
+ if (!project || project.deletedAt) return null
1647
+ return project
1648
+ }
1649
+
1650
+ requireChat(chatId: string) {
1651
+ const chat = this.state.chatsById.get(chatId)
1652
+ if (!chat || chat.deletedAt) {
1653
+ throw new Error("Chat not found")
1654
+ }
1655
+ return chat
1656
+ }
1657
+
1658
+ getChat(chatId: string) {
1659
+ const chat = this.state.chatsById.get(chatId)
1660
+ if (!chat || chat.deletedAt) return null
1661
+ return chat
1662
+ }
1663
+
1664
+ async getMessages(chatId: string, options?: { offset?: number; limit?: number }) {
1665
+ let entries: TranscriptEntry[]
1666
+
1667
+ if (this.transcriptCache.has(chatId)) {
1668
+ entries = this.transcriptCache.get(chatId)!
1669
+ } else {
1670
+ const legacyEntries = this.legacyMessagesByChatId.get(chatId)
1671
+ if (legacyEntries) {
1672
+ this.setTranscriptCache(chatId, cloneTranscriptEntries(legacyEntries))
1673
+ entries = this.transcriptCache.get(chatId)!
1674
+ } else {
1675
+ // Drain pending writes before reading from disk to ensure consistency
1676
+ await this.writeChain
1677
+ entries = await this.loadTranscriptFromDisk(chatId)
1678
+ this.setTranscriptCache(chatId, entries)
1679
+ }
1680
+ }
1681
+
1682
+ if (options?.offset !== undefined || options?.limit !== undefined) {
1683
+ const start = options.offset ?? 0
1684
+ const end = options.limit !== undefined ? start + options.limit : undefined
1685
+ return cloneTranscriptEntries(entries.slice(start, end))
1686
+ }
1687
+
1688
+ return cloneTranscriptEntries(entries)
1689
+ }
1690
+
1691
+ async getMessageCount(chatId: string): Promise<number> {
1692
+ if (this.transcriptCache.has(chatId)) {
1693
+ return this.transcriptCache.get(chatId)!.length
1694
+ }
1695
+ const legacyEntries = this.legacyMessagesByChatId.get(chatId)
1696
+ if (legacyEntries) {
1697
+ return legacyEntries.length
1698
+ }
1699
+ await this.writeChain
1700
+ const entries = await this.loadTranscriptFromDisk(chatId)
1701
+ this.setTranscriptCache(chatId, entries)
1702
+ return entries.length
1703
+ }
1704
+
1705
+ listProjects() {
1706
+ return [...this.state.workspacesById.values()].filter((project) => !project.deletedAt)
1707
+ }
1708
+
1709
+ listChatsByProject(workspaceId: string) {
1710
+ return [...this.state.chatsById.values()]
1711
+ .filter((chat) => chat.workspaceId === workspaceId && !chat.deletedAt)
1712
+ .sort((a, b) => (b.lastMessageAt ?? b.updatedAt) - (a.lastMessageAt ?? a.updatedAt))
1713
+ }
1714
+
1715
+ getChatCount(workspaceId: string) {
1716
+ return this.listChatsByProject(workspaceId).length
1717
+ }
1718
+
1719
+ async getLegacyTranscriptStats(): Promise<LegacyTranscriptStats> {
1720
+ const messagesLogSize = await Bun.file(this.messagesLogPath).size
1721
+ const sources: LegacyTranscriptStats["sources"] = []
1722
+ if (this.snapshotHasLegacyMessages) {
1723
+ sources.push("snapshot")
1724
+ }
1725
+ if (messagesLogSize > 0) {
1726
+ sources.push("messages_log")
1727
+ }
1728
+
1729
+ let entryCount = 0
1730
+ for (const entries of this.legacyMessagesByChatId.values()) {
1731
+ entryCount += entries.length
1732
+ }
1733
+
1734
+ return {
1735
+ hasLegacyData: sources.length > 0 || this.legacyMessagesByChatId.size > 0,
1736
+ sources,
1737
+ chatCount: this.legacyMessagesByChatId.size,
1738
+ entryCount,
1739
+ }
1740
+ }
1741
+
1742
+ async hasLegacyTranscriptData() {
1743
+ return (await this.getLegacyTranscriptStats()).hasLegacyData
1744
+ }
1745
+
1746
+ private createSnapshot(): SnapshotFile {
1747
+ const coordination: SnapshotFile["coordination"] = []
1748
+ for (const [workspaceId, coord] of this.state.coordinationByWorkspace) {
1749
+ coordination.push({
1750
+ workspaceId,
1751
+ todos: [...coord.todos.values()],
1752
+ claims: [...coord.claims.values()],
1753
+ worktrees: [...coord.worktrees.values()],
1754
+ rules: [...coord.rules.values()],
1755
+ })
1756
+ }
1757
+ const agentConfigs: SnapshotFile["agentConfigs"] = []
1758
+ for (const [workspaceId, configMap] of this.state.agentConfigsByWorkspace) {
1759
+ if (configMap.size > 0) {
1760
+ agentConfigs.push({ workspaceId, records: [...configMap.values()] })
1761
+ }
1762
+ }
1763
+ return {
1764
+ v: STORE_VERSION,
1765
+ generatedAt: Date.now(),
1766
+ workspaces: this.listProjects().map((project) => ({ ...project })),
1767
+ ...(this.state.independentWorkspacesById.size > 0 ? { independentWorkspaces: this.listIndependentWorkspaces() } : {}),
1768
+ chats: [...this.state.chatsById.values()]
1769
+ .filter((chat) => !chat.deletedAt)
1770
+ .map((chat) => ({ ...chat })),
1771
+ ...(this.state.queuedTurnsByChat.size > 0 ? {
1772
+ queuedTurns: [...this.state.queuedTurnsByChat.values()].map((queued) => ({ ...queued })),
1773
+ } : {}),
1774
+ ...(coordination.length > 0 ? { coordination } : {}),
1775
+ ...(agentConfigs.length > 0 ? { agentConfigs } : {}),
1776
+ ...(this.state.reposById.size > 0 ? { repos: [...this.state.reposById.values()] } : {}),
1777
+ ...(this.state.workflowRunsByWorkspace.size > 0 ? {
1778
+ workflowRuns: [...this.state.workflowRunsByWorkspace.entries()].map(([workspaceId, runsMap]) => ({
1779
+ workspaceId,
1780
+ runs: [...runsMap.values()],
1781
+ })),
1782
+ } : {}),
1783
+ ...(this.state.sandboxByWorkspace.size > 0 ? {
1784
+ sandboxes: [...this.state.sandboxByWorkspace.values()],
1785
+ } : {}),
1786
+ ...(this.state.providerProfiles.size > 0 ? {
1787
+ providerProfiles: [...this.state.providerProfiles.values()],
1788
+ } : {}),
1789
+ ...(this.state.workspaceProfileOverrides.size > 0 ? {
1790
+ workspaceProfileOverrides: [...this.state.workspaceProfileOverrides.values()].flatMap(
1791
+ (wsMap) => [...wsMap.values()],
1792
+ ),
1793
+ } : {}),
1794
+ ...(this.state.extensionPreferences.size > 0 ? {
1795
+ extensionPreferences: [...this.state.extensionPreferences.values()],
1796
+ } : {}),
1797
+ ...(this.state.teamMembers.size > 0 ? {
1798
+ teamMembers: [...this.state.teamMembers.values()],
1799
+ } : {}),
1800
+ ...(this.state.runnerLabels.size > 0 ? {
1801
+ runnerLabels: [...this.state.runnerLabels.values()],
1802
+ } : {}),
1803
+ }
1804
+ }
1805
+
1806
+ async compact() {
1807
+ const snapshot = this.createSnapshot()
1808
+ await Bun.write(this.snapshotPath, JSON.stringify(snapshot, null, 2))
1809
+ await Promise.all([
1810
+ Bun.write(this.projectsLogPath, ""),
1811
+ Bun.write(this.chatsLogPath, ""),
1812
+ Bun.write(this.messagesLogPath, ""),
1813
+ Bun.write(this.turnsLogPath, ""),
1814
+ Bun.write(this.coordinationLogPath, ""),
1815
+ Bun.write(this.agentConfigsLogPath, ""),
1816
+ Bun.write(this.reposLogPath, ""),
1817
+ Bun.write(this.workflowsLogPath, ""),
1818
+ Bun.write(this.sandboxLogPath, ""),
1819
+ Bun.write(this.profilesLogPath, ""),
1820
+ Bun.write(this.extensionPrefsLogPath, ""),
1821
+ Bun.write(this.runnerTeamsLogPath, ""),
1822
+ ])
1823
+ }
1824
+
1825
+ async migrateLegacyTranscripts(onProgress?: (message: string) => void) {
1826
+ const stats = await this.getLegacyTranscriptStats()
1827
+ if (!stats.hasLegacyData) return false
1828
+
1829
+ const sourceSummary = stats.sources.map((source) => source === "messages_log" ? "messages.jsonl" : "snapshot.json").join(", ")
1830
+ onProgress?.(`${LOG_PREFIX} transcript migration detected: ${stats.chatCount} chats, ${stats.entryCount} entries from ${sourceSummary}`)
1831
+
1832
+ const messageSets = [...this.legacyMessagesByChatId.entries()]
1833
+ onProgress?.(`${LOG_PREFIX} transcript migration: writing ${messageSets.length} per-chat transcript files`)
1834
+
1835
+ await mkdir(this.transcriptsDir, { recursive: true })
1836
+ const logEveryChat = messageSets.length <= 10
1837
+ for (let index = 0; index < messageSets.length; index += 1) {
1838
+ const [chatId, entries] = messageSets[index]
1839
+ const transcriptPath = this.transcriptPath(chatId)
1840
+ const tempPath = `${transcriptPath}.tmp`
1841
+ const payload = entries.map((entry) => JSON.stringify(entry)).join("\n")
1842
+ await writeFile(tempPath, payload ? `${payload}\n` : "", "utf8")
1843
+ await rename(tempPath, transcriptPath)
1844
+ if (logEveryChat || (index + 1) % 25 === 0 || index === messageSets.length - 1) {
1845
+ onProgress?.(`${LOG_PREFIX} transcript migration: ${index + 1}/${messageSets.length} chats`)
1846
+ }
1847
+ }
1848
+
1849
+ this.clearLegacyTranscriptState()
1850
+ await this.compact()
1851
+ this.transcriptCache.clear()
1852
+ onProgress?.(`${LOG_PREFIX} transcript migration complete`)
1853
+ return true
1854
+ }
1855
+
1856
+ private async shouldCompact() {
1857
+ const sizes = await Promise.all([
1858
+ Bun.file(this.projectsLogPath).size,
1859
+ Bun.file(this.chatsLogPath).size,
1860
+ Bun.file(this.messagesLogPath).size,
1861
+ Bun.file(this.turnsLogPath).size,
1862
+ Bun.file(this.coordinationLogPath).size,
1863
+ Bun.file(this.agentConfigsLogPath).size,
1864
+ Bun.file(this.reposLogPath).size,
1865
+ Bun.file(this.workflowsLogPath).size,
1866
+ Bun.file(this.sandboxLogPath).size,
1867
+ Bun.file(this.profilesLogPath).size,
1868
+ Bun.file(this.extensionPrefsLogPath).size,
1869
+ Bun.file(this.runnerTeamsLogPath).size,
1870
+ ])
1871
+ return sizes.reduce((total, size) => total + size, 0) >= COMPACTION_THRESHOLD_BYTES
1872
+ }
1873
+
1874
+ // ===== claude-pty additions (in-memory only — PORT-TODO durable persistence) =====
1875
+
1876
+ private readonly _ptySubagentRuns = new Map<string, Map<string, PtySubagentRunSnapshot>>()
1877
+ private readonly _ptyToolRequests = new Map<string, PtyToolRequest>()
1878
+
1879
+ private async replayPtyLogs(): Promise<void> {
1880
+ // Subagent log replay
1881
+ const subagentFile = Bun.file(this.ptySubagentLogPath)
1882
+ if (await subagentFile.exists()) {
1883
+ const text = await subagentFile.text()
1884
+ for (const line of text.split("\n")) {
1885
+ if (!line.trim()) continue
1886
+ try {
1887
+ const event = JSON.parse(line) as PtySubagentRunEvent
1888
+ this.applyPtySubagentEvent(event)
1889
+ } catch {
1890
+ // skip malformed
1891
+ }
1892
+ }
1893
+ }
1894
+ // Session token replay
1895
+ const tokenFile = Bun.file(this.ptySessionTokensLogPath)
1896
+ if (await tokenFile.exists()) {
1897
+ const text = await tokenFile.text()
1898
+ for (const line of text.split("\n")) {
1899
+ if (!line.trim()) continue
1900
+ try {
1901
+ const rec = JSON.parse(line) as { chatId: string; providerId: string; sessionToken: string | null }
1902
+ let map = this._ptySessionTokensByChatId.get(rec.chatId)
1903
+ if (!map) {
1904
+ map = new Map<string, string>()
1905
+ this._ptySessionTokensByChatId.set(rec.chatId, map)
1906
+ }
1907
+ if (rec.sessionToken === null) {
1908
+ map.delete(rec.providerId)
1909
+ } else {
1910
+ map.set(rec.providerId, rec.sessionToken)
1911
+ }
1912
+ } catch {
1913
+ // skip malformed
1914
+ }
1915
+ }
1916
+ }
1917
+ // Tool request log replay
1918
+ const toolFile = Bun.file(this.ptyToolRequestsLogPath)
1919
+ if (await toolFile.exists()) {
1920
+ const text = await toolFile.text()
1921
+ for (const line of text.split("\n")) {
1922
+ if (!line.trim()) continue
1923
+ try {
1924
+ const rec = JSON.parse(line) as { type: "put" | "resolved"; request?: PtyToolRequest; id?: string; status?: import("../shared/permission-policy").ToolRequestStatus; decision?: import("../shared/permission-policy").ToolRequestDecision; resolvedAt?: number; mismatchReason?: string }
1925
+ if (rec.type === "put" && rec.request) {
1926
+ this._ptyToolRequests.set(rec.request.id, { ...rec.request })
1927
+ } else if (rec.type === "resolved" && rec.id) {
1928
+ const existing = this._ptyToolRequests.get(rec.id)
1929
+ if (existing) {
1930
+ this._ptyToolRequests.set(rec.id, {
1931
+ ...existing,
1932
+ status: rec.status ?? existing.status,
1933
+ decision: rec.decision ?? existing.decision,
1934
+ resolvedAt: rec.resolvedAt ?? existing.resolvedAt,
1935
+ mismatchReason: rec.mismatchReason,
1936
+ })
1937
+ }
1938
+ }
1939
+ } catch {
1940
+ // skip malformed
1941
+ }
1942
+ }
1943
+ }
1944
+ }
1945
+
1946
+ private applyPtySubagentEvent(event: PtySubagentRunEvent): void {
1947
+ const chatMap = this._ptySubagentRuns.get(event.chatId) ?? new Map<string, PtySubagentRunSnapshot>()
1948
+ this._ptySubagentRuns.set(event.chatId, chatMap)
1949
+ const existing = chatMap.get(event.runId)
1950
+ switch (event.type) {
1951
+ case "subagent_run_started":
1952
+ chatMap.set(event.runId, {
1953
+ runId: event.runId,
1954
+ chatId: event.chatId,
1955
+ subagentId: event.subagentId,
1956
+ subagentName: event.subagentName,
1957
+ provider: event.provider,
1958
+ model: event.model,
1959
+ status: "running",
1960
+ parentUserMessageId: event.parentUserMessageId,
1961
+ parentRunId: event.parentRunId,
1962
+ depth: event.depth,
1963
+ startedAt: event.timestamp,
1964
+ finishedAt: null,
1965
+ finalText: null,
1966
+ error: null,
1967
+ usage: null,
1968
+ entries: [],
1969
+ pendingTool: null,
1970
+ })
1971
+ return
1972
+ case "subagent_message_delta":
1973
+ if (existing) existing.finalText = (existing.finalText ?? "") + event.content
1974
+ return
1975
+ case "subagent_entry_appended":
1976
+ if (existing) existing.entries.push(event.entry)
1977
+ return
1978
+ case "subagent_run_completed":
1979
+ if (existing) {
1980
+ existing.status = "completed"
1981
+ existing.finishedAt = event.timestamp
1982
+ existing.finalText = event.finalContent
1983
+ existing.usage = event.usage ?? null
1984
+ }
1985
+ return
1986
+ case "subagent_run_failed":
1987
+ if (existing) {
1988
+ existing.status = "failed"
1989
+ existing.finishedAt = event.timestamp
1990
+ existing.error = event.error
1991
+ }
1992
+ return
1993
+ case "subagent_run_cancelled":
1994
+ if (existing) {
1995
+ existing.status = "cancelled"
1996
+ existing.finishedAt = event.timestamp
1997
+ }
1998
+ return
1999
+ case "subagent_tool_pending":
2000
+ if (existing) existing.pendingTool = event.pendingTool
2001
+ return
2002
+ case "subagent_tool_resolved":
2003
+ if (existing) existing.pendingTool = null
2004
+ return
2005
+ }
2006
+ }
2007
+
2008
+ async appendSubagentEvent(event: PtySubagentRunEvent): Promise<void> {
2009
+ this.applyPtySubagentEvent(event)
2010
+ const payload = `${JSON.stringify(event)}\n`
2011
+ this.writeChain = this.writeChain.then(() => appendFile(this.ptySubagentLogPath, payload, "utf8"))
2012
+ await this.writeChain
2013
+ }
2014
+
2015
+ getSubagentRuns(chatId: string): Record<string, PtySubagentRunSnapshot> {
2016
+ const map = this._ptySubagentRuns.get(chatId)
2017
+ if (!map) return {}
2018
+ return Object.fromEntries(map.entries())
2019
+ }
2020
+
2021
+ *runningSubagentRuns(): Iterable<PtySubagentRunSnapshot> {
2022
+ for (const map of this._ptySubagentRuns.values()) {
2023
+ for (const run of map.values()) {
2024
+ if (run.status === "running") yield run
2025
+ }
2026
+ }
2027
+ }
2028
+
2029
+ async putToolRequest(req: PtyToolRequest): Promise<void> {
2030
+ this._ptyToolRequests.set(req.id, { ...req })
2031
+ const payload = `${JSON.stringify({ type: "put", request: req })}\n`
2032
+ this.writeChain = this.writeChain.then(() => appendFile(this.ptyToolRequestsLogPath, payload, "utf8"))
2033
+ await this.writeChain
2034
+ }
2035
+
2036
+ getToolRequest(id: string): PtyToolRequest | null {
2037
+ const req = this._ptyToolRequests.get(id)
2038
+ return req ? { ...req } : null
2039
+ }
2040
+
2041
+ listPendingToolRequests(chatId: string): PtyToolRequest[] {
2042
+ const out: PtyToolRequest[] = []
2043
+ for (const req of this._ptyToolRequests.values()) {
2044
+ if (req.chatId !== chatId) continue
2045
+ if (req.status !== "pending") continue
2046
+ out.push({ ...req })
2047
+ }
2048
+ return out
2049
+ }
2050
+
2051
+ async resolveToolRequest(
2052
+ id: string,
2053
+ args: {
2054
+ status: import("../shared/permission-policy").ToolRequestStatus
2055
+ decision?: import("../shared/permission-policy").ToolRequestDecision
2056
+ resolvedAt: number
2057
+ mismatchReason?: string
2058
+ },
2059
+ ): Promise<void> {
2060
+ const existing = this._ptyToolRequests.get(id)
2061
+ if (!existing) throw new Error(`resolveToolRequest: unknown id ${id}`)
2062
+ this._ptyToolRequests.set(id, {
2063
+ ...existing,
2064
+ status: args.status,
2065
+ decision: args.decision ?? existing.decision,
2066
+ resolvedAt: args.resolvedAt,
2067
+ mismatchReason: args.mismatchReason,
2068
+ })
2069
+ const payload = `${JSON.stringify({ type: "resolved", id, status: args.status, decision: args.decision, resolvedAt: args.resolvedAt, mismatchReason: args.mismatchReason })}\n`
2070
+ this.writeChain = this.writeChain.then(() => appendFile(this.ptyToolRequestsLogPath, payload, "utf8"))
2071
+ await this.writeChain
2072
+ }
2073
+
2074
+ scanAllToolRequests(): PtyToolRequest[] {
2075
+ return [...this._ptyToolRequests.values()].map((req) => ({ ...req }))
2076
+ }
2077
+
2078
+ /**
2079
+ * Generic per-provider session-token setter for claude-pty (and future
2080
+ * non-AgentProvider-union backends). `providerId` is a string so PTY can
2081
+ * use "claude-pty" without forcing the canonical AgentProvider union to
2082
+ * widen.
2083
+ */
2084
+ async setPtySessionToken(chatId: string, providerId: string, sessionToken: string | null): Promise<void> {
2085
+ let map = this._ptySessionTokensByChatId.get(chatId)
2086
+ if (!map) {
2087
+ map = new Map<string, string>()
2088
+ this._ptySessionTokensByChatId.set(chatId, map)
2089
+ }
2090
+ if (sessionToken === null) {
2091
+ map.delete(providerId)
2092
+ } else {
2093
+ map.set(providerId, sessionToken)
2094
+ }
2095
+ const payload = `${JSON.stringify({ chatId, providerId, sessionToken, timestamp: Date.now() })}\n`
2096
+ this.writeChain = this.writeChain.then(() => appendFile(this.ptySessionTokensLogPath, payload, "utf8"))
2097
+ await this.writeChain
2098
+ }
2099
+
2100
+ getPtySessionToken(chatId: string, providerId: string): string | null {
2101
+ return this._ptySessionTokensByChatId.get(chatId)?.get(providerId) ?? null
2102
+ }
2103
+
2104
+ getPtySessionTokensForChat(chatId: string): Record<string, string> {
2105
+ const map = this._ptySessionTokensByChatId.get(chatId)
2106
+ if (!map) return {}
2107
+ return Object.fromEntries(map.entries())
2108
+ }
2109
+ }
2110
+
2111
+ // ===== claude-pty shared shapes =====
2112
+
2113
+ export interface PtySubagentRunSnapshot {
2114
+ runId: string
2115
+ chatId: string
2116
+ subagentId: string | null
2117
+ subagentName: string
2118
+ provider: import("../shared/types").AgentProvider
2119
+ model: string
2120
+ status: "running" | "completed" | "failed" | "cancelled"
2121
+ parentUserMessageId: string
2122
+ parentRunId: string | null
2123
+ depth: number
2124
+ startedAt: number
2125
+ finishedAt: number | null
2126
+ finalText: string | null
2127
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2128
+ error: { code: any; message: string } | null
2129
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2130
+ usage: any | null
2131
+ entries: import("../shared/types").TranscriptEntry[]
2132
+ pendingTool: import("../shared/types").PendingToolSnapshot | null
2133
+ }
2134
+
2135
+ export type PtySubagentRunEvent =
2136
+ | {
2137
+ v: 3
2138
+ type: "subagent_run_started"
2139
+ timestamp: number
2140
+ chatId: string
2141
+ runId: string
2142
+ subagentId: string | null
2143
+ subagentName: string
2144
+ provider: import("../shared/types").AgentProvider
2145
+ model: string
2146
+ parentUserMessageId: string
2147
+ parentRunId: string | null
2148
+ depth: number
2149
+ }
2150
+ | {
2151
+ v: 3
2152
+ type: "subagent_message_delta"
2153
+ timestamp: number
2154
+ chatId: string
2155
+ runId: string
2156
+ content: string
2157
+ }
2158
+ | {
2159
+ v: 3
2160
+ type: "subagent_run_completed"
2161
+ timestamp: number
2162
+ chatId: string
2163
+ runId: string
2164
+ finalContent: string
2165
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2166
+ usage?: any
2167
+ }
2168
+ | {
2169
+ v: 3
2170
+ type: "subagent_run_failed"
2171
+ timestamp: number
2172
+ chatId: string
2173
+ runId: string
2174
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2175
+ error: { code: any; message: string }
2176
+ }
2177
+ | {
2178
+ v: 3
2179
+ type: "subagent_run_cancelled"
2180
+ timestamp: number
2181
+ chatId: string
2182
+ runId: string
2183
+ }
2184
+ | {
2185
+ v: 3
2186
+ type: "subagent_entry_appended"
2187
+ timestamp: number
2188
+ chatId: string
2189
+ runId: string
2190
+ entry: import("../shared/types").TranscriptEntry
2191
+ }
2192
+ | {
2193
+ v: 3
2194
+ type: "subagent_tool_pending"
2195
+ timestamp: number
2196
+ chatId: string
2197
+ runId: string
2198
+ pendingTool: import("../shared/types").PendingToolSnapshot
2199
+ }
2200
+ | {
2201
+ v: 3
2202
+ type: "subagent_tool_resolved"
2203
+ timestamp: number
2204
+ chatId: string
2205
+ runId: string
2206
+ }
2207
+
2208
+ export type PtyToolRequest = import("../shared/permission-policy").ToolRequest