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,887 @@
1
+ import crypto from "node:crypto"
2
+ import { LOG_PREFIX } from "../../shared/branding"
3
+ import type {
4
+ AgentProvider,
5
+ Subagent,
6
+ TranscriptEntry,
7
+ } from "../../shared/types"
8
+ import type { EventStore } from "../event-store"
9
+ // PORT-TODO: kanna's ProviderUsage / SubagentErrorCode not yet ported as concrete shapes.
10
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
+ type ProviderUsage = any
12
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
13
+ type SubagentErrorCode = any
14
+ import { buildHistoryPrimer, extractPreviousAssistantReply } from "../claude-pty-mcp/history-primer"
15
+ import { parseMentions, type ParsedMention } from "../claude-pty-mcp/mention-parser"
16
+
17
+ class PausableTimeout {
18
+ private remainingMs: number
19
+ private deadline: number | null = null
20
+ private handle: ReturnType<typeof setTimeout> | null = null
21
+ private onFire: () => void
22
+
23
+ constructor(totalMs: number, onFire: () => void) {
24
+ this.remainingMs = totalMs
25
+ this.onFire = onFire
26
+ }
27
+
28
+ start(now: number = Date.now()): void {
29
+ this.deadline = now + this.remainingMs
30
+ this.handle = setTimeout(this.onFire, this.remainingMs)
31
+ }
32
+
33
+ pause(now: number = Date.now()): void {
34
+ if (this.handle == null || this.deadline == null) return
35
+ clearTimeout(this.handle)
36
+ this.handle = null
37
+ this.remainingMs = Math.max(0, this.deadline - now)
38
+ this.deadline = null
39
+ }
40
+
41
+ resume(now: number = Date.now()): void {
42
+ if (this.handle != null) return
43
+ this.start(now)
44
+ }
45
+
46
+ clear(): void {
47
+ if (this.handle != null) clearTimeout(this.handle)
48
+ this.handle = null
49
+ this.deadline = null
50
+ }
51
+ }
52
+
53
+ interface Deferred<T> {
54
+ promise: Promise<T>
55
+ resolve: (value: T) => void
56
+ reject: (err: Error) => void
57
+ }
58
+
59
+ function createDeferred<T>(): Deferred<T> {
60
+ let resolve!: (value: T) => void
61
+ let reject!: (err: Error) => void
62
+ const promise = new Promise<T>((res, rej) => { resolve = res; reject = rej })
63
+ return { promise, resolve, reject }
64
+ }
65
+
66
+ export interface ProviderRunStart {
67
+ provider: AgentProvider
68
+ model: string
69
+ systemPrompt: string
70
+ preamble: string | null
71
+ /**
72
+ * Run the subagent against its provider.
73
+ * - `onChunk(text)`: every assistant_text fragment, in order. Used to
74
+ * persist `subagent_message_delta` events for streaming UI.
75
+ * - `onEntry(entry)`: every TranscriptEntry — including the assistant_text
76
+ * entries forwarded to onChunk, plus tool_call / tool_result / result.
77
+ * Used to persist `subagent_entry_appended` events.
78
+ * Returns the final accumulated text + usage for the run_completed event.
79
+ */
80
+ start: (
81
+ onChunk: (chunk: string) => void,
82
+ onEntry: (entry: TranscriptEntry) => void,
83
+ ) => Promise<{ text: string; usage?: ProviderUsage }>
84
+ authReady: () => Promise<boolean>
85
+ }
86
+
87
+ export interface OrchestratorAppSettings {
88
+ getSnapshot(): { subagents: Subagent[] }
89
+ }
90
+
91
+ export interface SubagentOrchestratorDeps {
92
+ store: EventStore
93
+ appSettings: OrchestratorAppSettings
94
+ startProviderRun: (args: {
95
+ subagent: Subagent
96
+ chatId: string
97
+ primer: string | null
98
+ /**
99
+ * Instruction text shown to the subagent above the primer — user's own
100
+ * message for direct mentions, parent agent's reply for chained mentions,
101
+ * or null when unavailable. Used by composeInitialPrompt to ensure the
102
+ * subagent sees the request, not only the prior context.
103
+ */
104
+ userInstruction: string | null
105
+ runId: string
106
+ abortSignal: AbortSignal
107
+ /** Depth of THIS run in the chain (top-level user delegation = 0). */
108
+ depth: number
109
+ /** Ancestor chain of subagent ids leading to this run, oldest first. */
110
+ ancestorSubagentIds: string[]
111
+ /** User message id the originating chat turn is responding to. */
112
+ parentUserMessageId: string
113
+ }) => ProviderRunStart
114
+ /**
115
+ * Called when a subagent run enters a terminal state (failed / completed /
116
+ * interrupted) so external resources keyed on (chatId, runId) — e.g. the
117
+ * `subagentPendingResolvers` map on AgentCoordinator — can be released.
118
+ * The SDK's `canUseTool` Promise must be rejected when the run dies, or it
119
+ * hangs forever and leaks. Optional for tests.
120
+ */
121
+ onRunTerminal?: (chatId: string, runId: string, reason: "failed" | "completed") => void
122
+ /**
123
+ * Called on every non-terminal subagent state change — run start and each
124
+ * persisted transcript entry. Wired by AgentCoordinator to
125
+ * `emitStateChange(chatId)` so the ws-router pushes a fresh chat snapshot
126
+ * (carrying `subagentRuns`) to connected clients WHILE the run is in
127
+ * flight. Without this the client only ever sees the run at terminal
128
+ * (the sole `onRunTerminal` broadcast), so a delegated run renders as
129
+ * blank/absent until it finishes. ws-router coalesces these at 16ms and
130
+ * signature-dedups, so high-frequency entry fan-out is cheap. Optional
131
+ * for tests.
132
+ */
133
+ onRunProgress?: (chatId: string, runId: string) => void
134
+ now?: () => number
135
+ maxParallel?: number
136
+ maxChainDepth?: number
137
+ runTimeoutMs?: number
138
+ }
139
+
140
+ const DEFAULT_MAX_PARALLEL = 4
141
+ const DEFAULT_MAX_CHAIN_DEPTH = 1
142
+
143
+ /**
144
+ * Terminal outcome of a single subagent run, surfaced to callers that
145
+ * need the final reply text — e.g. `mcp__kanna__delegate_subagent` so
146
+ * the main agent can synthesize the subagent's answer into its own reply.
147
+ */
148
+ export type DelegationOutcome =
149
+ | { status: "completed"; runId: string; text: string }
150
+ | { status: "failed"; runId: string; errorCode: SubagentErrorCode; errorMessage: string }
151
+ // Subagents now run with full toolset (Bash, Read, etc) so single turns may
152
+ // take minutes. 600s matches the default Bash tool wall-clock cap. Tests still
153
+ // override via SubagentOrchestratorDeps.runTimeoutMs.
154
+ const DEFAULT_RUN_TIMEOUT_MS = 600_000
155
+
156
+ interface RunState {
157
+ chatId: string
158
+ parentRunId: string | null
159
+ childRunIds: Set<string>
160
+ abortController: AbortController
161
+ timeout: PausableTimeout | null
162
+ cancelled: boolean
163
+ pendingAcquire: boolean
164
+ permitWaiter: { resolve: () => void; reject: (e: Error) => void } | null
165
+ }
166
+
167
+ export class SubagentOrchestrator {
168
+ private permits: number
169
+ private readonly waiters: Array<{ chatId: string; resolve: () => void; reject: (err: Error) => void }> = []
170
+ private readonly cancelledChats = new Set<string>()
171
+ private readonly runStateByRunId = new Map<string, RunState>()
172
+
173
+ private readonly recoveryPromise: Promise<void>
174
+
175
+ constructor(private readonly deps: SubagentOrchestratorDeps) {
176
+ this.permits = this.maxParallel()
177
+ this.recoveryPromise = this.recoverInterruptedRuns()
178
+ }
179
+
180
+ /**
181
+ * Caller must `await` this before spawning new runs to ensure orphan
182
+ * `running` runs from a previous server lifetime have been failed first.
183
+ */
184
+ whenRecovered(): Promise<void> {
185
+ return this.recoveryPromise
186
+ }
187
+
188
+ private async recoverInterruptedRuns(): Promise<void> {
189
+ // Recover ALL `running` runs from the previous server lifetime, not just
190
+ // those mid-tool. A subagent crashed mid-bash (or mid-streaming) leaves
191
+ // its run in `running` forever otherwise, blocking the UI and leaking a
192
+ // permit until the server is restarted again with a fix.
193
+ for (const run of this.deps.store.runningSubagentRuns()) {
194
+ try {
195
+ await this.deps.store.appendSubagentEvent({
196
+ v: 3,
197
+ type: "subagent_run_failed",
198
+ timestamp: this.now(),
199
+ chatId: run.chatId,
200
+ runId: run.runId,
201
+ error: {
202
+ code: "INTERRUPTED",
203
+ message: run.pendingTool
204
+ ? "Server restart while subagent awaited tool response"
205
+ : "Server restart while subagent run was in progress",
206
+ },
207
+ })
208
+ } catch (err) {
209
+ console.warn(`${LOG_PREFIX} interrupted-run recovery failed`, {
210
+ chatId: run.chatId, runId: run.runId, err,
211
+ })
212
+ }
213
+ }
214
+ }
215
+
216
+ private maxParallel() { return this.deps.maxParallel ?? DEFAULT_MAX_PARALLEL }
217
+ private maxDepth() { return this.deps.maxChainDepth ?? DEFAULT_MAX_CHAIN_DEPTH }
218
+ private timeoutMs() { return this.deps.runTimeoutMs ?? DEFAULT_RUN_TIMEOUT_MS }
219
+ private now() { return this.deps.now?.() ?? Date.now() }
220
+
221
+ activePermitCount() {
222
+ return this.maxParallel() - this.permits
223
+ }
224
+
225
+ notifySubagentToolPending(runId: string): void {
226
+ this.runStateByRunId.get(runId)?.timeout?.pause()
227
+ }
228
+
229
+ notifySubagentToolResolved(runId: string): void {
230
+ this.runStateByRunId.get(runId)?.timeout?.resume()
231
+ }
232
+
233
+ private async acquire(chatId: string, runId: string): Promise<void> {
234
+ if (this.cancelledChats.has(chatId)) {
235
+ throw new Error("CHAT_CANCELLED")
236
+ }
237
+ if (this.permits > 0) {
238
+ this.permits -= 1
239
+ const state = this.runStateByRunId.get(runId)
240
+ if (state) state.pendingAcquire = false
241
+ return
242
+ }
243
+ const { promise, resolve, reject } = Promise.withResolvers<void>()
244
+ const state = this.runStateByRunId.get(runId)
245
+ if (state) {
246
+ state.permitWaiter = { resolve, reject }
247
+ }
248
+ this.waiters.push({ chatId, resolve, reject })
249
+ try {
250
+ // `release()` hands a permit to the next waiter by resolving its
251
+ // promise without incrementing this.permits — the permit transfers
252
+ // in-place. Decrementing here would double-charge the handoff and
253
+ // permanently leak one parallel slot per waiter (B1).
254
+ await promise
255
+ } finally {
256
+ if (state) {
257
+ state.permitWaiter = null
258
+ state.pendingAcquire = false
259
+ }
260
+ }
261
+ }
262
+
263
+ private release(): void {
264
+ const next = this.waiters.shift()
265
+ if (next) {
266
+ next.resolve()
267
+ return
268
+ }
269
+ this.permits += 1
270
+ }
271
+
272
+ /**
273
+ * Clear the sticky cancel marker for a chat. Call this at the start of
274
+ * each new user turn so a previous cancelChat() does not poison every
275
+ * subsequent `delegateRun` with "Chat cancelled before run started"
276
+ * forever (B3 + delegate_subagent path).
277
+ */
278
+ clearChatCancellation(chatId: string): void {
279
+ this.cancelledChats.delete(chatId)
280
+ }
281
+
282
+ cancelChat(chatId: string): void {
283
+ this.cancelledChats.add(chatId)
284
+ for (let i = this.waiters.length - 1; i >= 0; i -= 1) {
285
+ const w = this.waiters[i]
286
+ if (w.chatId !== chatId) continue
287
+ this.waiters.splice(i, 1)
288
+ w.reject(new Error("CHAT_CANCELLED"))
289
+ }
290
+ // Cancel every acquired/queued run in this chat. cancelRun is idempotent
291
+ // (re-cancellation no-op) and handles both queued and running states.
292
+ // Snapshot runIds first because cancelRun may mutate the map.
293
+ const runIds: string[] = []
294
+ for (const [runId, state] of this.runStateByRunId) {
295
+ if (state.chatId === chatId) runIds.push(runId)
296
+ }
297
+ for (const runId of runIds) this.cancelRun(chatId, runId)
298
+ }
299
+
300
+ cancelRun(chatId: string, runId: string): void {
301
+ const state = this.runStateByRunId.get(runId)
302
+ if (!state) return
303
+ if (state.cancelled) return
304
+ if (state.chatId !== chatId) return
305
+ state.cancelled = true
306
+ // Cascade to running descendants. With current DEFAULT_MAX_CHAIN_DEPTH=1
307
+ // this is a no-op in practice (children spawn only after parent
308
+ // completes) but guards forward-compat with higher chain depths.
309
+ for (const childRunId of [...state.childRunIds]) {
310
+ this.cancelRun(chatId, childRunId)
311
+ }
312
+ if (state.pendingAcquire && state.permitWaiter) {
313
+ // Queued: splice waiter out of this.waiters FIRST so release() cannot
314
+ // grant us a permit we will never use, then reject the Promise.
315
+ const idx = this.waiters.findIndex((w) => w.resolve === state.permitWaiter!.resolve)
316
+ if (idx >= 0) this.waiters.splice(idx, 1)
317
+ const reject = state.permitWaiter.reject
318
+ state.permitWaiter = null
319
+ reject(new Error("USER_CANCELLED"))
320
+ } else {
321
+ state.abortController.abort()
322
+ }
323
+ }
324
+
325
+ async runMentionsForUserMessage(args: {
326
+ chatId: string
327
+ userMessageId: string
328
+ mentions: ParsedMention[]
329
+ /**
330
+ * The text accompanying the @agent mention. For user-triggered runs this
331
+ * is the user's typed message. For main-Claude-triggered runs this is the
332
+ * assistant's reply text. Passed through to the subagent's initial prompt
333
+ * so the run sees the request, not just the prior context primer. Default
334
+ * "" preserves prior call-site semantics (primer-only) in tests that
335
+ * haven't been migrated.
336
+ */
337
+ userContent?: string
338
+ }): Promise<void> {
339
+ const userContent = args.userContent ?? ""
340
+ // A new mention batch from this chat means the user is asking for fresh
341
+ // work — clear any "cancelled" marker left over from a prior cancelChat
342
+ // call (B3). Without this, every subagent in a chat that has ever been
343
+ // cancelled would fail before start until process restart.
344
+ this.cancelledChats.delete(args.chatId)
345
+ await this.recoveryPromise
346
+ const subagents = this.deps.appSettings.getSnapshot().subagents
347
+ const resolved: { mention: Extract<ParsedMention, { kind: "subagent" }>; subagent: Subagent }[] = []
348
+
349
+ for (const mention of args.mentions) {
350
+ if (mention.kind === "unknown-subagent") {
351
+ const runId = crypto.randomUUID()
352
+ await this.deps.store.appendSubagentEvent({
353
+ v: 3,
354
+ type: "subagent_run_started",
355
+ timestamp: this.now(),
356
+ chatId: args.chatId,
357
+ runId,
358
+ subagentId: null,
359
+ subagentName: mention.name,
360
+ provider: "claude",
361
+ model: "",
362
+ parentUserMessageId: args.userMessageId,
363
+ parentRunId: null,
364
+ depth: 0,
365
+ })
366
+ await this.failRun(args.chatId, runId, "UNKNOWN_SUBAGENT", `Unknown subagent '${mention.name}'`)
367
+ continue
368
+ }
369
+ const subagent = subagents.find((s) => s.id === mention.subagentId)
370
+ if (!subagent) {
371
+ const runId = crypto.randomUUID()
372
+ await this.deps.store.appendSubagentEvent({
373
+ v: 3,
374
+ type: "subagent_run_started",
375
+ timestamp: this.now(),
376
+ chatId: args.chatId,
377
+ runId,
378
+ subagentId: mention.subagentId,
379
+ subagentName: mention.subagentId,
380
+ provider: "claude",
381
+ model: "",
382
+ parentUserMessageId: args.userMessageId,
383
+ parentRunId: null,
384
+ depth: 0,
385
+ })
386
+ await this.failRun(args.chatId, runId, "UNKNOWN_SUBAGENT", `Subagent ${mention.subagentId} was deleted`)
387
+ continue
388
+ }
389
+ resolved.push({ mention, subagent })
390
+ }
391
+
392
+ await Promise.all(resolved.map(({ subagent }) =>
393
+ this.spawnRun({
394
+ subagent,
395
+ chatId: args.chatId,
396
+ parentUserMessageId: args.userMessageId,
397
+ parentRunId: null,
398
+ depth: 0,
399
+ ancestorSubagentIds: [],
400
+ userInstruction: userContent,
401
+ })
402
+ ))
403
+ }
404
+
405
+ /**
406
+ * Public entry point for `mcp__kanna__delegate_subagent`. The main agent
407
+ * (or a parent subagent, when sub-spawning-sub is enabled) calls this with
408
+ * a subagent id and a prompt; the orchestrator runs the subagent and the
409
+ * caller awaits the terminal {@link DelegationOutcome}.
410
+ *
411
+ * Cycle / depth guards mirror the chained-mention path in `spawnRun`: a
412
+ * parent cannot delegate to a subagent already in its ancestor chain, and
413
+ * `depth > maxChainDepth` fails fast with `DEPTH_EXCEEDED`.
414
+ */
415
+ async delegateRun(args: {
416
+ chatId: string
417
+ parentUserMessageId: string
418
+ parentRunId: string | null
419
+ parentSubagentId: string | null
420
+ ancestorSubagentIds: string[]
421
+ depth: number
422
+ subagentId: string
423
+ prompt: string
424
+ /**
425
+ * Optional per-entry sink. Called once for each persisted
426
+ * `subagent_entry_appended` event while the run is in flight. Used by
427
+ * the MCP `delegate_subagent` tool to emit `notifications/progress`
428
+ * so the CLI's transport-error watchdog cannot declare the call lost
429
+ * on long-running subagent runs.
430
+ */
431
+ onEntry?: (entry: TranscriptEntry) => void
432
+ }): Promise<DelegationOutcome> {
433
+ await this.recoveryPromise
434
+ const subagent = this.deps.appSettings
435
+ .getSnapshot()
436
+ .subagents.find((s) => s.id === args.subagentId)
437
+ if (!subagent) {
438
+ const runId = crypto.randomUUID()
439
+ await this.deps.store.appendSubagentEvent({
440
+ v: 3,
441
+ type: "subagent_run_started",
442
+ timestamp: this.now(),
443
+ chatId: args.chatId,
444
+ runId,
445
+ subagentId: args.subagentId,
446
+ subagentName: args.subagentId,
447
+ provider: "claude",
448
+ model: "",
449
+ parentUserMessageId: args.parentUserMessageId,
450
+ parentRunId: args.parentRunId,
451
+ depth: args.depth,
452
+ })
453
+ return await this.failRun(args.chatId, runId, "UNKNOWN_SUBAGENT", `Subagent ${args.subagentId} not found`)
454
+ }
455
+ if (args.depth > this.maxDepth()) {
456
+ const runId = crypto.randomUUID()
457
+ await this.deps.store.appendSubagentEvent({
458
+ v: 3,
459
+ type: "subagent_run_started",
460
+ timestamp: this.now(),
461
+ chatId: args.chatId,
462
+ runId,
463
+ subagentId: subagent.id,
464
+ subagentName: subagent.name,
465
+ provider: subagent.provider,
466
+ model: subagent.model,
467
+ parentUserMessageId: args.parentUserMessageId,
468
+ parentRunId: args.parentRunId,
469
+ depth: args.depth,
470
+ })
471
+ return await this.failRun(
472
+ args.chatId,
473
+ runId,
474
+ "DEPTH_EXCEEDED",
475
+ `Chain depth ${args.depth} exceeds limit ${this.maxDepth()}`,
476
+ )
477
+ }
478
+ if (args.ancestorSubagentIds.includes(subagent.id)) {
479
+ const runId = crypto.randomUUID()
480
+ await this.deps.store.appendSubagentEvent({
481
+ v: 3,
482
+ type: "subagent_run_started",
483
+ timestamp: this.now(),
484
+ chatId: args.chatId,
485
+ runId,
486
+ subagentId: subagent.id,
487
+ subagentName: subagent.name,
488
+ provider: subagent.provider,
489
+ model: subagent.model,
490
+ parentUserMessageId: args.parentUserMessageId,
491
+ parentRunId: args.parentRunId,
492
+ depth: args.depth,
493
+ })
494
+ return await this.failRun(
495
+ args.chatId,
496
+ runId,
497
+ "LOOP_DETECTED",
498
+ `Subagent ${subagent.name} already in ancestor chain`,
499
+ )
500
+ }
501
+ const outcome = await this.spawnRun({
502
+ subagent,
503
+ chatId: args.chatId,
504
+ parentUserMessageId: args.parentUserMessageId,
505
+ parentRunId: args.parentRunId,
506
+ depth: args.depth,
507
+ ancestorSubagentIds: args.ancestorSubagentIds,
508
+ userInstruction: args.prompt,
509
+ onEntry: args.onEntry,
510
+ })
511
+ // Trace point: this is the return that flows back through the MCP
512
+ // `delegate_subagent` tool to the parent claude as its tool_result.
513
+ // If the parent appears to hang after this point, the bug is on the
514
+ // MCP shim or the parent PTY side, not in the orchestrator.
515
+ console.log("[kanna/subagent] delegateRun outcome", {
516
+ chatId: args.chatId,
517
+ subagentId: args.subagentId,
518
+ parentRunId: args.parentRunId,
519
+ depth: args.depth,
520
+ status: outcome.status,
521
+ errorCode: outcome.status === "failed" ? outcome.errorCode : undefined,
522
+ textChars: outcome.status === "completed" ? outcome.text.length : undefined,
523
+ })
524
+ return outcome
525
+ }
526
+
527
+ private async spawnRun(args: {
528
+ subagent: Subagent
529
+ chatId: string
530
+ parentUserMessageId: string
531
+ parentRunId: string | null
532
+ depth: number
533
+ ancestorSubagentIds: string[]
534
+ /**
535
+ * Instruction the spawn was triggered by — user's typed text for top-level
536
+ * runs, parent agent's full reply for chained runs. Forwarded to the
537
+ * provider run so composeInitialPrompt can render it above the primer.
538
+ */
539
+ userInstruction: string
540
+ /** External per-entry sink (see {@link delegateRun}). */
541
+ onEntry?: (entry: TranscriptEntry) => void
542
+ }): Promise<DelegationOutcome> {
543
+ const runId = crypto.randomUUID()
544
+ await this.deps.store.appendSubagentEvent({
545
+ v: 3,
546
+ type: "subagent_run_started",
547
+ timestamp: this.now(),
548
+ chatId: args.chatId,
549
+ runId,
550
+ subagentId: args.subagent.id,
551
+ subagentName: args.subagent.name,
552
+ provider: args.subagent.provider,
553
+ model: args.subagent.model,
554
+ parentUserMessageId: args.parentUserMessageId,
555
+ parentRunId: args.parentRunId,
556
+ depth: args.depth,
557
+ })
558
+ this.deps.onRunProgress?.(args.chatId, runId)
559
+
560
+ // Register RunState BEFORE acquire so cancelRun can find a queued run.
561
+ // The reducer marks the run as `status: "running"` from this event on,
562
+ // which is what the UI uses to show the X button.
563
+ const runState: RunState = {
564
+ chatId: args.chatId,
565
+ parentRunId: args.parentRunId,
566
+ childRunIds: new Set(),
567
+ abortController: new AbortController(),
568
+ timeout: null,
569
+ cancelled: false,
570
+ pendingAcquire: true,
571
+ permitWaiter: null,
572
+ }
573
+ this.runStateByRunId.set(runId, runState)
574
+ if (args.parentRunId != null) {
575
+ this.runStateByRunId.get(args.parentRunId)?.childRunIds.add(runId)
576
+ }
577
+
578
+ try {
579
+ await this.acquire(args.chatId, runId)
580
+ } catch (err) {
581
+ const msg = err instanceof Error ? err.message : String(err)
582
+ const code: SubagentErrorCode = msg === "USER_CANCELLED" ? "USER_CANCELLED" : "PROVIDER_ERROR"
583
+ const message = msg === "USER_CANCELLED"
584
+ ? "Cancelled before run started"
585
+ : "Chat cancelled before run started"
586
+ const outcome = await this.failRun(args.chatId, runId, code, message)
587
+ this.cleanupRunState(runId)
588
+ return outcome
589
+ }
590
+ let released = false
591
+ const releaseSlot = () => {
592
+ if (released) return
593
+ released = true
594
+ this.release()
595
+ }
596
+
597
+ if (this.cancelledChats.has(args.chatId)) {
598
+ releaseSlot()
599
+ const outcome = await this.failRun(args.chatId, runId, "PROVIDER_ERROR", "Chat cancelled before run started")
600
+ this.cleanupRunState(runId)
601
+ return outcome
602
+ }
603
+
604
+ try {
605
+ const transcript = (await this.deps.store.getMessages(args.chatId)) as TranscriptEntry[]
606
+ let primer: string | null
607
+ if (args.subagent.contextScope === "full-transcript") {
608
+ primer = buildHistoryPrimer(transcript, args.subagent.provider, "")
609
+ } else {
610
+ const reply = extractPreviousAssistantReply(transcript)
611
+ primer = reply == null ? null : `Previous assistant reply:\n${reply}`
612
+ }
613
+
614
+ let runStart: ProviderRunStart
615
+ try {
616
+ runStart = this.deps.startProviderRun({
617
+ subagent: args.subagent,
618
+ chatId: args.chatId,
619
+ primer,
620
+ userInstruction: args.userInstruction.length > 0 ? args.userInstruction : null,
621
+ runId,
622
+ abortSignal: runState.abortController.signal,
623
+ depth: args.depth,
624
+ ancestorSubagentIds: args.ancestorSubagentIds,
625
+ parentUserMessageId: args.parentUserMessageId,
626
+ })
627
+ } catch (err) {
628
+ // Defensive: startProviderRun is a synchronous factory but a real impl
629
+ // (buildSubagentProviderRunForChat in agent.ts) can throw if e.g. the
630
+ // chat's project lookup fails. Without this guard the run would leak
631
+ // as `running` forever (no failed/completed event ever appended).
632
+ const msg = err instanceof Error ? err.message : String(err)
633
+ const outcome = await this.failRun(args.chatId, runId, "PROVIDER_ERROR", msg)
634
+ // releaseSlot — outer `finally` would re-release if we called raw
635
+ // `this.release()` here (B2). releaseSlot is idempotent via the
636
+ // `released` flag so the finally is a no-op.
637
+ releaseSlot()
638
+ this.cleanupRunState(runId)
639
+ return outcome
640
+ }
641
+
642
+ if (!(await runStart.authReady())) {
643
+ const outcome = await this.failRun(args.chatId, runId, "AUTH_REQUIRED", `Authentication required for ${args.subagent.provider}`)
644
+ releaseSlot()
645
+ this.cleanupRunState(runId)
646
+ return outcome
647
+ }
648
+
649
+ let finalText = ""
650
+ let usage: ProviderUsage | undefined
651
+ // Trailing-edge throttle handle for chunk-driven progress broadcasts.
652
+ let chunkProgressTimer: ReturnType<typeof setTimeout> | null = null
653
+ const CHUNK_PROGRESS_THROTTLE_MS = 100
654
+
655
+ const onChunk = (chunk: string) => {
656
+ if (!chunk) return
657
+ this.deps.store
658
+ .appendSubagentEvent({
659
+ v: 3,
660
+ type: "subagent_message_delta",
661
+ timestamp: this.now(),
662
+ chatId: args.chatId,
663
+ runId,
664
+ content: chunk,
665
+ })
666
+ .catch((err: unknown) => {
667
+ console.warn(`${LOG_PREFIX} subagent delta append failed`, { chatId: args.chatId, runId, err })
668
+ })
669
+ // Trailing-edge throttle: fire progress after a quiet window so
670
+ // streamed assistant text becomes visible incrementally in the UI.
671
+ if (chunkProgressTimer !== null) clearTimeout(chunkProgressTimer)
672
+ chunkProgressTimer = setTimeout(() => {
673
+ chunkProgressTimer = null
674
+ this.deps.onRunProgress?.(args.chatId, runId)
675
+ }, CHUNK_PROGRESS_THROTTLE_MS)
676
+ }
677
+ const externalOnEntry = args.onEntry
678
+ const onEntry = (entry: TranscriptEntry) => {
679
+ this.deps.store
680
+ .appendSubagentEvent({
681
+ v: 3,
682
+ type: "subagent_entry_appended",
683
+ timestamp: this.now(),
684
+ chatId: args.chatId,
685
+ runId,
686
+ entry,
687
+ })
688
+ .catch((err: unknown) => {
689
+ console.warn(`${LOG_PREFIX} subagent entry append failed`, { chatId: args.chatId, runId, err })
690
+ })
691
+ // Fire immediately — applyEvent now runs synchronously in appendSubagentEvent
692
+ // so the broadcast snapshot already includes this entry.
693
+ this.deps.onRunProgress?.(args.chatId, runId)
694
+ if (externalOnEntry) {
695
+ try {
696
+ externalOnEntry(entry)
697
+ } catch (err) {
698
+ console.warn(`${LOG_PREFIX} external onEntry threw`, { chatId: args.chatId, runId, err })
699
+ }
700
+ }
701
+ }
702
+ const timeoutRejection = createDeferred<never>()
703
+ const pausable = new PausableTimeout(this.timeoutMs(), () => {
704
+ // Race-rejection ORDER MATTERS. Reject TIMEOUT before aborting so
705
+ // `Promise.race` resolves with the TIMEOUT error and the catch
706
+ // branch records `failRun TIMEOUT` instead of `USER_CANCELLED`.
707
+ // Then abort the controller to tear down the underlying provider
708
+ // session — `buildSubagentProviderRun` wires
709
+ // `session.interrupt()` / `codexManager.stopSession()` to this
710
+ // signal, without which a timed-out run keeps streaming and
711
+ // pollutes the event log (B5).
712
+ timeoutRejection.reject(new Error("TIMEOUT"))
713
+ runState.abortController.abort()
714
+ })
715
+ runState.timeout = pausable
716
+ pausable.start()
717
+ try {
718
+ const abortRejection = createDeferred<never>()
719
+ const abortListener = () => abortRejection.reject(new Error("USER_CANCELLED"))
720
+ runState.abortController.signal.addEventListener("abort", abortListener, { once: true })
721
+ let result: { text: string; usage?: ProviderUsage }
722
+ try {
723
+ // Fast-path: if already aborted, fire listener synchronously so the
724
+ // race rejects on the next microtask. Doing this AFTER abortRejection.promise
725
+ // is passed to Promise.race ensures the rejection always has a handler.
726
+ if (runState.abortController.signal.aborted) {
727
+ abortListener()
728
+ }
729
+ result = await Promise.race([
730
+ runStart.start(onChunk, onEntry),
731
+ timeoutRejection.promise,
732
+ abortRejection.promise,
733
+ ])
734
+ } finally {
735
+ runState.abortController.signal.removeEventListener("abort", abortListener)
736
+ }
737
+ finalText = result.text
738
+ usage = result.usage
739
+ } catch (error) {
740
+ const message = error instanceof Error ? error.message : String(error)
741
+ let outcome: DelegationOutcome
742
+ if (message === "TIMEOUT") {
743
+ outcome = await this.failRun(args.chatId, runId, "TIMEOUT", `Run exceeded ${this.timeoutMs()}ms`)
744
+ } else if (message === "USER_CANCELLED" || runState.cancelled) {
745
+ outcome = await this.failRun(args.chatId, runId, "USER_CANCELLED", "Cancelled by user")
746
+ } else {
747
+ outcome = await this.failRun(args.chatId, runId, "PROVIDER_ERROR", message)
748
+ }
749
+ return outcome
750
+ } finally {
751
+ pausable.clear()
752
+ runState.timeout = null
753
+ if (chunkProgressTimer !== null) {
754
+ // Flush any pending chunk-driven progress immediately at run end
755
+ // so the final streamed text is always broadcast before completion.
756
+ clearTimeout(chunkProgressTimer)
757
+ chunkProgressTimer = null
758
+ this.deps.onRunProgress?.(args.chatId, runId)
759
+ }
760
+ }
761
+
762
+ // Codex `stopSession` finishes the pending stream queue rather than
763
+ // rejecting — without this guard, a cancelled run can reach the
764
+ // success path.
765
+ if (runState.cancelled) {
766
+ return await this.failRun(args.chatId, runId, "USER_CANCELLED", "Cancelled by user")
767
+ }
768
+
769
+ await this.deps.store.appendSubagentEvent({
770
+ v: 3,
771
+ type: "subagent_run_completed",
772
+ timestamp: this.now(),
773
+ chatId: args.chatId,
774
+ runId,
775
+ finalContent: finalText,
776
+ usage,
777
+ })
778
+ try {
779
+ this.deps.onRunTerminal?.(args.chatId, runId, "completed")
780
+ } catch (err) {
781
+ console.warn(`${LOG_PREFIX} onRunTerminal(completed) threw`, { chatId: args.chatId, runId, err })
782
+ }
783
+
784
+ releaseSlot()
785
+
786
+ const chainedMentions = parseMentions(finalText, this.deps.appSettings.getSnapshot().subagents)
787
+ for (const mention of chainedMentions) {
788
+ if (mention.kind !== "subagent") continue
789
+ const chainSubagent = this.deps.appSettings.getSnapshot().subagents.find((s) => s.id === mention.subagentId)
790
+ if (!chainSubagent) continue
791
+ const childDepth = args.depth + 1
792
+ if (childDepth > this.maxDepth()) {
793
+ const childRunId = crypto.randomUUID()
794
+ await this.deps.store.appendSubagentEvent({
795
+ v: 3,
796
+ type: "subagent_run_started",
797
+ timestamp: this.now(),
798
+ chatId: args.chatId,
799
+ runId: childRunId,
800
+ subagentId: chainSubagent.id,
801
+ subagentName: chainSubagent.name,
802
+ provider: chainSubagent.provider,
803
+ model: chainSubagent.model,
804
+ parentUserMessageId: args.parentUserMessageId,
805
+ parentRunId: runId,
806
+ depth: childDepth,
807
+ })
808
+ await this.failRun(args.chatId, childRunId, "DEPTH_EXCEEDED", `Chain depth ${childDepth} exceeds limit ${this.maxDepth()}`)
809
+ continue
810
+ }
811
+ if ([...args.ancestorSubagentIds, args.subagent.id].includes(chainSubagent.id)) {
812
+ const childRunId = crypto.randomUUID()
813
+ await this.deps.store.appendSubagentEvent({
814
+ v: 3,
815
+ type: "subagent_run_started",
816
+ timestamp: this.now(),
817
+ chatId: args.chatId,
818
+ runId: childRunId,
819
+ subagentId: chainSubagent.id,
820
+ subagentName: chainSubagent.name,
821
+ provider: chainSubagent.provider,
822
+ model: chainSubagent.model,
823
+ parentUserMessageId: args.parentUserMessageId,
824
+ parentRunId: runId,
825
+ depth: childDepth,
826
+ })
827
+ await this.failRun(args.chatId, childRunId, "LOOP_DETECTED", `Subagent ${chainSubagent.name} already in ancestor chain`)
828
+ continue
829
+ }
830
+ await this.spawnRun({
831
+ subagent: chainSubagent,
832
+ chatId: args.chatId,
833
+ parentUserMessageId: args.parentUserMessageId,
834
+ parentRunId: runId,
835
+ depth: childDepth,
836
+ ancestorSubagentIds: [...args.ancestorSubagentIds, args.subagent.id],
837
+ userInstruction: finalText,
838
+ })
839
+ }
840
+ return { status: "completed", runId, text: finalText }
841
+ } finally {
842
+ releaseSlot()
843
+ this.cleanupRunState(runId)
844
+ }
845
+ }
846
+
847
+ private cleanupRunState(runId: string) {
848
+ const state = this.runStateByRunId.get(runId)
849
+ if (!state) return
850
+ state.timeout?.clear()
851
+ if (state.parentRunId != null) {
852
+ this.runStateByRunId.get(state.parentRunId)?.childRunIds.delete(runId)
853
+ }
854
+ this.runStateByRunId.delete(runId)
855
+ }
856
+
857
+ private async failRun(
858
+ chatId: string,
859
+ runId: string,
860
+ code: SubagentErrorCode,
861
+ message: string,
862
+ ): Promise<DelegationOutcome> {
863
+ console.warn(`${LOG_PREFIX} subagent run failed`, { chatId, runId, code, message })
864
+ try {
865
+ await this.deps.store.appendSubagentEvent({
866
+ v: 3,
867
+ type: "subagent_run_failed",
868
+ timestamp: this.now(),
869
+ chatId,
870
+ runId,
871
+ error: { code, message },
872
+ })
873
+ } catch (err) {
874
+ // Persisting the failure event must never throw out of failRun — it's
875
+ // called from `catch` and `finally` blocks where an unhandled rejection
876
+ // would leak the permit. Log and continue; the orchestrator will still
877
+ // notify the terminal callback below so the resolver map is cleaned up.
878
+ console.warn(`${LOG_PREFIX} failRun appendSubagentEvent threw`, { chatId, runId, code, err })
879
+ }
880
+ try {
881
+ this.deps.onRunTerminal?.(chatId, runId, "failed")
882
+ } catch (err) {
883
+ console.warn(`${LOG_PREFIX} onRunTerminal(failed) threw`, { chatId, runId, err })
884
+ }
885
+ return { status: "failed", runId, errorCode: code, errorMessage: message }
886
+ }
887
+ }