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,1713 @@
1
+ import { spawn } from "node:child_process"
2
+ import { randomUUID } from "node:crypto"
3
+ import { createInterface } from "node:readline"
4
+ import type { Readable, Writable } from "node:stream"
5
+ import type {
6
+ AskUserQuestionItem,
7
+ CodexReasoningEffort,
8
+ PresentContentSchemaValidationError,
9
+ PresentContentSuccessToolResult,
10
+ PresentContentValidationIssue,
11
+ ServiceTier,
12
+ TodoItem,
13
+ TranscriptEntry,
14
+ } from "../shared/types"
15
+ import { getWebContextPrompt } from "../shared/web-context"
16
+ import type { HarnessEvent, HarnessToolRequest, HarnessTurn } from "./harness-types"
17
+ import { normalizeToolCall } from "../shared/tools"
18
+ import { z, type ZodIssue } from "zod"
19
+ import {
20
+ type CollabAgentToolCallItem,
21
+ type ContextCompactedNotification,
22
+ type CodexRequestId,
23
+ type CommandExecutionApprovalDecision,
24
+ type CommandExecutionRequestApprovalParams,
25
+ type CommandExecutionRequestApprovalResponse,
26
+ type DynamicToolCallOutputContentItem,
27
+ type DynamicToolCallResponse,
28
+ type FileChangeApprovalDecision,
29
+ type FileChangeRequestApprovalParams,
30
+ type FileChangeRequestApprovalResponse,
31
+ type InitializeParams,
32
+ type ItemCompletedNotification,
33
+ type ItemStartedNotification,
34
+ type JsonRpcResponse,
35
+ type McpToolCallItem,
36
+ type PlanDeltaNotification,
37
+ type ServerNotification,
38
+ type ServerRequest,
39
+ type ThreadItem,
40
+ type ThreadResumeParams,
41
+ type ThreadResumeResponse,
42
+ type ThreadStartParams,
43
+ type ThreadStartResponse,
44
+ type ToolRequestUserInputParams,
45
+ type ToolRequestUserInputQuestion,
46
+ type ToolRequestUserInputResponse,
47
+ type TurnPlanStep,
48
+ type TurnPlanUpdatedNotification,
49
+ type TurnCompletedNotification,
50
+ type TurnInterruptParams,
51
+ type TurnStartParams,
52
+ type TurnStartResponse,
53
+ isJsonRpcResponse,
54
+ isServerNotification,
55
+ isServerRequest,
56
+ } from "./codex-app-server-protocol"
57
+
58
+ interface CodexAppServerProcess {
59
+ stdin: Writable
60
+ stdout: Readable
61
+ stderr: Readable
62
+ killed?: boolean
63
+ kill(signal?: NodeJS.Signals | number): void
64
+ on(event: "close", listener: (code: number | null) => void): this
65
+ on(event: "error", listener: (error: Error) => void): this
66
+ once(event: "close", listener: (code: number | null) => void): this
67
+ once(event: "error", listener: (error: Error) => void): this
68
+ }
69
+
70
+ type SpawnCodexAppServer = (cwd: string) => CodexAppServerProcess
71
+
72
+ interface PendingRequest<TResult> {
73
+ method: string
74
+ resolve: (value: TResult) => void
75
+ reject: (error: Error) => void
76
+ }
77
+
78
+ interface PendingTurn {
79
+ turnId: string | null
80
+ model: string
81
+ planMode: boolean
82
+ queue: AsyncQueue<HarnessEvent>
83
+ startedToolIds: Set<string>
84
+ handledDynamicToolIds: Set<string>
85
+ latestPlanExplanation: string | null
86
+ latestPlanSteps: TurnPlanStep[]
87
+ latestPlanText: string | null
88
+ planTextByItemId: Map<string, string>
89
+ todoSequence: number
90
+ pendingWebSearchResultToolId: string | null
91
+ resolved: boolean
92
+ orchestration:
93
+ | {
94
+ callerChatId: string
95
+ tools: CodexOrchestrationToolHost
96
+ }
97
+ | null
98
+ onToolRequest: (request: HarnessToolRequest) => Promise<unknown>
99
+ onApprovalRequest?: (
100
+ request:
101
+ | {
102
+ requestId: CodexRequestId
103
+ kind: "command_execution"
104
+ params: CommandExecutionRequestApprovalParams
105
+ }
106
+ | {
107
+ requestId: CodexRequestId
108
+ kind: "file_change"
109
+ params: FileChangeRequestApprovalParams
110
+ }
111
+ ) => Promise<CommandExecutionApprovalDecision | FileChangeApprovalDecision>
112
+ }
113
+
114
+ interface CodexOrchestrationToolHost {
115
+ spawnAgent(
116
+ callerChatId: string,
117
+ args: {
118
+ instruction: string
119
+ provider?: "claude" | "codex"
120
+ model?: string
121
+ forkContext?: boolean
122
+ mode?: "blocking" | "background"
123
+ resume?: "immediate" | "gate"
124
+ },
125
+ ): Promise<{ chatId: string; delegationId?: string }>
126
+ listAgents(chatId: string): unknown
127
+ sendInput(
128
+ callerChatId: string,
129
+ args: { targetChatId: string; content: string; model?: string },
130
+ ): Promise<void>
131
+ waitForResult(
132
+ callerChatId: string,
133
+ args: { targetChatId: string; timeoutMs?: number },
134
+ ): Promise<{ result: string; isError: boolean }>
135
+ closeAgent(
136
+ callerChatId: string,
137
+ args: { targetChatId: string },
138
+ ): Promise<void>
139
+ }
140
+
141
+ interface SessionContext {
142
+ chatId: string
143
+ cwd: string
144
+ child: CodexAppServerProcess
145
+ pendingRequests: Map<CodexRequestId, PendingRequest<unknown>>
146
+ pendingTurn: PendingTurn | null
147
+ sessionToken: string | null
148
+ stderrLines: string[]
149
+ closed: boolean
150
+ }
151
+
152
+ export interface StartCodexSessionArgs {
153
+ chatId: string
154
+ cwd: string
155
+ model: string
156
+ serviceTier?: ServiceTier
157
+ sessionToken: string | null
158
+ }
159
+
160
+ export interface StartCodexTurnArgs {
161
+ chatId: string
162
+ model: string
163
+ effort?: CodexReasoningEffort
164
+ serviceTier?: ServiceTier
165
+ content: string
166
+ planMode: boolean
167
+ skills?: string[]
168
+ orchestrator?: CodexOrchestrationToolHost
169
+ orchestrationChatId?: string
170
+ onToolRequest: (request: HarnessToolRequest) => Promise<unknown>
171
+ onApprovalRequest?: PendingTurn["onApprovalRequest"]
172
+ }
173
+
174
+ export interface GenerateStructuredArgs {
175
+ cwd: string
176
+ prompt: string
177
+ model?: string
178
+ effort?: CodexReasoningEffort
179
+ serviceTier?: ServiceTier
180
+ }
181
+
182
+ function timestamped<T extends Omit<TranscriptEntry, "_id" | "createdAt">>(
183
+ entry: T,
184
+ createdAt = Date.now()
185
+ ): TranscriptEntry {
186
+ return {
187
+ _id: randomUUID(),
188
+ createdAt,
189
+ ...entry,
190
+ } as TranscriptEntry
191
+ }
192
+
193
+ function codexSystemInitEntry(model: string, skills?: string[]): TranscriptEntry {
194
+ return timestamped({
195
+ kind: "system_init",
196
+ provider: "codex",
197
+ model,
198
+ tools: ["Bash", "Write", "Edit", "WebSearch", "TodoWrite", "AskUserQuestion", "ExitPlanMode"],
199
+ agents: ["spawnAgent", "sendInput", "resumeAgent", "wait", "closeAgent"],
200
+ slashCommands: skills ?? [],
201
+ mcpServers: [],
202
+ })
203
+ }
204
+
205
+ function errorMessage(value: unknown): string {
206
+ if (value instanceof Error) return value.message
207
+ return String(value)
208
+ }
209
+
210
+ function parseJsonLine(line: string): unknown | null {
211
+ try {
212
+ return JSON.parse(line)
213
+ } catch (error: unknown) {
214
+ void error
215
+ return null
216
+ }
217
+ }
218
+
219
+ function isRecoverableResumeError(error: unknown): boolean {
220
+ const message = errorMessage(error).toLowerCase()
221
+ return message.includes("thread/resume")
222
+ }
223
+
224
+ const MULTI_SELECT_HINT_PATTERN = /\b(all that apply|select all|choose all|pick all|select multiple|choose multiple|pick multiple|multiple selections?|multiple choice|more than one|one or more)\b/i
225
+
226
+ function inferQuestionAllowsMultiple(question: ToolRequestUserInputQuestion): boolean {
227
+ const combinedText = [question.header, question.question].filter(Boolean).join(" ")
228
+ return MULTI_SELECT_HINT_PATTERN.test(combinedText)
229
+ }
230
+
231
+ function toAskUserQuestionItems(params: ToolRequestUserInputParams): AskUserQuestionItem[] {
232
+ return params.questions.map((question) => ({
233
+ id: question.id,
234
+ question: question.question,
235
+ header: question.header || undefined,
236
+ options: question.options?.map((option) => ({
237
+ label: option.label,
238
+ description: option.description ?? undefined,
239
+ })),
240
+ multiSelect: inferQuestionAllowsMultiple(question),
241
+ }))
242
+ }
243
+
244
+ function toToolRequestUserInputResponse(raw: unknown, questions: ToolRequestUserInputParams["questions"]): ToolRequestUserInputResponse {
245
+ const record = raw && typeof raw === "object" ? raw as Record<string, unknown> : {}
246
+ const answersValue = record.answers
247
+ const value = answersValue && typeof answersValue === "object" && !Array.isArray(answersValue)
248
+ ? answersValue as Record<string, unknown>
249
+ : record
250
+ const answers = Object.fromEntries(
251
+ questions.map((question) => {
252
+ const rawAnswer = value[question.id] ?? value[question.question]
253
+ if (Array.isArray(rawAnswer)) {
254
+ return [question.id, { answers: rawAnswer.map((entry) => String(entry)) }]
255
+ }
256
+ if (typeof rawAnswer === "string") {
257
+ return [question.id, { answers: [rawAnswer] }]
258
+ }
259
+ if (rawAnswer && typeof rawAnswer === "object" && Array.isArray((rawAnswer as { answers?: unknown }).answers)) {
260
+ return [question.id, { answers: ((rawAnswer as { answers: unknown[] }).answers).map((entry) => String(entry)) }]
261
+ }
262
+ return [question.id, { answers: [] }]
263
+ })
264
+ )
265
+ return { answers }
266
+ }
267
+
268
+ function contentFromMcpResult(item: McpToolCallItem): unknown {
269
+ if (item.error?.message) {
270
+ return { error: item.error.message }
271
+ }
272
+ return item.result?.structuredContent ?? item.result?.content ?? null
273
+ }
274
+
275
+ function asRecord(value: unknown): Record<string, unknown> | null {
276
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null
277
+ return value as Record<string, unknown>
278
+ }
279
+
280
+ function todoStatus(status: TurnPlanStep["status"]): TodoItem["status"] {
281
+ if (status === "completed") return "completed"
282
+ if (status === "inProgress") return "in_progress"
283
+ return "pending"
284
+ }
285
+
286
+ function planStepsToTodos(steps: TurnPlanStep[]): TodoItem[] {
287
+ return steps.map((step) => ({
288
+ content: step.step,
289
+ status: todoStatus(step.status),
290
+ activeForm: step.step,
291
+ }))
292
+ }
293
+
294
+ function renderPlanMarkdownFromSteps(steps: TurnPlanStep[]): string {
295
+ return steps.map((step) => {
296
+ const checkbox = step.status === "completed" ? "[x]" : "[ ]"
297
+ return `- ${checkbox} ${step.step}`
298
+ }).join("\n")
299
+ }
300
+
301
+ function dynamicContentToText(contentItems: DynamicToolCallOutputContentItem[] | null | undefined): string {
302
+ if (!contentItems?.length) return ""
303
+ return contentItems
304
+ .map((item) => item.type === "inputText" ? item.text ?? "" : item.imageUrl ?? "")
305
+ .filter(Boolean)
306
+ .join("\n")
307
+ }
308
+
309
+ function dynamicToolPayload(value: Record<string, unknown> | unknown[] | string | number | boolean | null | undefined): Record<string, unknown> {
310
+ const record = asRecord(value)
311
+ if (record) return record
312
+ return { value }
313
+ }
314
+
315
+ const presentContentSchema = z.object({
316
+ title: z.string(),
317
+ kind: z.enum(["markdown", "code", "diagram"]),
318
+ format: z.string(),
319
+ source: z.string(),
320
+ summary: z.string().optional(),
321
+ collapsed: z.boolean().optional(),
322
+ }).strict()
323
+
324
+ const askUserQuestionSchema = z.object({
325
+ questions: z.array(z.object({
326
+ id: z.string().optional(),
327
+ question: z.string(),
328
+ header: z.string().optional(),
329
+ options: z.array(z.object({
330
+ label: z.string(),
331
+ description: z.string().optional(),
332
+ })).optional(),
333
+ multiSelect: z.boolean().optional(),
334
+ })).min(1),
335
+ }).strict()
336
+
337
+ function presentContentValidationError(issues: ZodIssue[]): { error: PresentContentSchemaValidationError } {
338
+ return {
339
+ error: {
340
+ source: "schema_validation",
341
+ schema: "present_content",
342
+ issues: issues.map((issue) => ({
343
+ path: issue.path.map(String),
344
+ code: issue.code,
345
+ message: issue.message,
346
+ }) satisfies PresentContentValidationIssue),
347
+ },
348
+ }
349
+ }
350
+
351
+ function presentContentToolCall(toolId: string, input: Record<string, unknown>): TranscriptEntry {
352
+ return timestamped({
353
+ kind: "tool_call",
354
+ tool: normalizeToolCall({
355
+ toolName: "present_content",
356
+ toolId,
357
+ input,
358
+ }),
359
+ })
360
+ }
361
+
362
+ function askUserQuestionToolCall(toolId: string, questions: AskUserQuestionItem[]): TranscriptEntry {
363
+ return timestamped({
364
+ kind: "tool_call",
365
+ tool: normalizeToolCall({
366
+ toolName: "AskUserQuestion",
367
+ toolId,
368
+ input: { questions },
369
+ }),
370
+ })
371
+ }
372
+
373
+ function enrichPresentContentResult(
374
+ value: z.infer<typeof presentContentSchema>
375
+ ): PresentContentSuccessToolResult {
376
+ return {
377
+ accepted: true,
378
+ ...value,
379
+ }
380
+ }
381
+
382
+ function orchestrationToolCall(toolId: string, toolName: string, input: Record<string, unknown>): TranscriptEntry {
383
+ return timestamped({
384
+ kind: "tool_call",
385
+ tool: normalizeToolCall({
386
+ toolName,
387
+ toolId,
388
+ input,
389
+ }),
390
+ })
391
+ }
392
+
393
+ function webSearchQuery(item: Extract<ThreadItem, { type: "webSearch" }>): string {
394
+ return item.query || item.action?.query || item.action?.queries?.find((query) => typeof query === "string") || ""
395
+ }
396
+
397
+ function genericDynamicToolCall(toolId: string, toolName: string, input: Record<string, unknown>): TranscriptEntry {
398
+ return timestamped({
399
+ kind: "tool_call",
400
+ tool: {
401
+ kind: "tool",
402
+ toolKind: "unknown_tool",
403
+ toolName,
404
+ toolId,
405
+ input: {
406
+ payload: input,
407
+ },
408
+ rawInput: input,
409
+ },
410
+ })
411
+ }
412
+
413
+ function collabToolCall(item: CollabAgentToolCallItem): TranscriptEntry {
414
+ return timestamped({
415
+ kind: "tool_call",
416
+ tool: {
417
+ kind: "tool",
418
+ toolKind: "subagent_task",
419
+ toolName: "Task",
420
+ toolId: item.id,
421
+ input: {
422
+ subagentType: item.tool,
423
+ },
424
+ rawInput: item as unknown as Record<string, unknown>,
425
+ },
426
+ })
427
+ }
428
+
429
+ function todoToolCall(toolId: string, steps: TurnPlanStep[]): TranscriptEntry {
430
+ return timestamped({
431
+ kind: "tool_call",
432
+ tool: {
433
+ kind: "tool",
434
+ toolKind: "todo_write",
435
+ toolName: "TodoWrite",
436
+ toolId,
437
+ input: {
438
+ todos: planStepsToTodos(steps),
439
+ },
440
+ rawInput: {
441
+ plan: steps,
442
+ },
443
+ },
444
+ })
445
+ }
446
+
447
+ function fileChangeKind(
448
+ kind: "add" | "delete" | "update" | { type: "add" | "delete" | "update"; move_path?: string | null }
449
+ ): { type: "add" | "delete" | "update"; movePath?: string | null } {
450
+ if (typeof kind === "string") {
451
+ return { type: kind }
452
+ }
453
+ return {
454
+ type: kind.type,
455
+ movePath: kind.move_path ?? null,
456
+ }
457
+ }
458
+
459
+ function fileChangeToolId(itemId: string, index: number, totalChanges: number): string {
460
+ if (totalChanges === 1) {
461
+ return itemId
462
+ }
463
+ return `${itemId}:change:${index}`
464
+ }
465
+
466
+ function fileChangePayload(
467
+ item: Extract<ThreadItem, { type: "fileChange" }>,
468
+ change: Extract<ThreadItem, { type: "fileChange" }>["changes"][number]
469
+ ): Record<string, unknown> {
470
+ return {
471
+ ...item,
472
+ changes: [change],
473
+ } as unknown as Record<string, unknown>
474
+ }
475
+
476
+ function parseUnifiedDiff(diff: string): { oldString: string; newString: string } {
477
+ const oldLines: string[] = []
478
+ const newLines: string[] = []
479
+
480
+ for (const line of diff.split(/\r?\n/)) {
481
+ if (!line) continue
482
+ if (line.startsWith("@@") || line.startsWith("---") || line.startsWith("+++")) continue
483
+ if (line === "\") continue
484
+
485
+ const prefix = line[0]
486
+ const content = line.slice(1)
487
+
488
+ if (prefix === " ") {
489
+ oldLines.push(content)
490
+ newLines.push(content)
491
+ continue
492
+ }
493
+ if (prefix === "-") {
494
+ oldLines.push(content)
495
+ continue
496
+ }
497
+ if (prefix === "+") {
498
+ newLines.push(content)
499
+ }
500
+ }
501
+
502
+ return {
503
+ oldString: oldLines.join("\n"),
504
+ newString: newLines.join("\n"),
505
+ }
506
+ }
507
+
508
+ function fileChangeToToolCalls(item: Extract<ThreadItem, { type: "fileChange" }>): TranscriptEntry[] {
509
+ return item.changes.map((change, index) => {
510
+ const payload = fileChangePayload(item, change)
511
+ const toolId = fileChangeToolId(item.id, index, item.changes.length)
512
+ const normalizedKind = fileChangeKind(change.kind)
513
+
514
+ if (normalizedKind.movePath) {
515
+ return timestamped({
516
+ kind: "tool_call",
517
+ tool: {
518
+ kind: "tool",
519
+ toolKind: "unknown_tool",
520
+ toolName: "FileChange",
521
+ toolId,
522
+ input: {
523
+ payload,
524
+ },
525
+ rawInput: payload,
526
+ },
527
+ })
528
+ }
529
+
530
+ if (typeof change.diff === "string") {
531
+ const { oldString, newString } = parseUnifiedDiff(change.diff)
532
+
533
+ if (normalizedKind.type === "add") {
534
+ return timestamped({
535
+ kind: "tool_call",
536
+ tool: {
537
+ kind: "tool",
538
+ toolKind: "write_file",
539
+ toolName: "Write",
540
+ toolId,
541
+ input: {
542
+ filePath: change.path,
543
+ content: newString,
544
+ },
545
+ rawInput: payload,
546
+ },
547
+ })
548
+ }
549
+
550
+ if (normalizedKind.type === "update") {
551
+ return timestamped({
552
+ kind: "tool_call",
553
+ tool: {
554
+ kind: "tool",
555
+ toolKind: "edit_file",
556
+ toolName: "Edit",
557
+ toolId,
558
+ input: {
559
+ filePath: change.path,
560
+ oldString,
561
+ newString,
562
+ },
563
+ rawInput: payload,
564
+ },
565
+ })
566
+ }
567
+ }
568
+
569
+ return timestamped({
570
+ kind: "tool_call",
571
+ tool: {
572
+ kind: "tool",
573
+ toolKind: "unknown_tool",
574
+ toolName: "FileChange",
575
+ toolId,
576
+ input: {
577
+ payload,
578
+ },
579
+ rawInput: payload,
580
+ },
581
+ })
582
+ })
583
+ }
584
+
585
+ function fileChangeToToolResults(item: Extract<ThreadItem, { type: "fileChange" }>): TranscriptEntry[] {
586
+ return item.changes.map((change, index) => timestamped({
587
+ kind: "tool_result",
588
+ toolId: fileChangeToolId(item.id, index, item.changes.length),
589
+ content: fileChangePayload(item, change),
590
+ isError: item.status === "failed" || item.status === "declined",
591
+ }))
592
+ }
593
+
594
+ function itemToToolCalls(item: ThreadItem): TranscriptEntry[] {
595
+ switch (item.type) {
596
+ case "dynamicToolCall":
597
+ return [genericDynamicToolCall(item.id, item.tool, dynamicToolPayload(item.arguments))]
598
+ case "collabAgentToolCall":
599
+ return [collabToolCall(item)]
600
+ case "commandExecution":
601
+ return [timestamped({
602
+ kind: "tool_call",
603
+ tool: {
604
+ kind: "tool",
605
+ toolKind: "bash",
606
+ toolName: "Bash",
607
+ toolId: item.id,
608
+ input: {
609
+ command: item.command,
610
+ },
611
+ rawInput: item,
612
+ },
613
+ })]
614
+ case "webSearch":
615
+ return [timestamped({
616
+ kind: "tool_call",
617
+ tool: {
618
+ kind: "tool",
619
+ toolKind: "web_search",
620
+ toolName: "WebSearch",
621
+ toolId: item.id,
622
+ input: {
623
+ query: webSearchQuery(item),
624
+ },
625
+ rawInput: item,
626
+ },
627
+ })]
628
+ case "mcpToolCall":
629
+ return [timestamped({
630
+ kind: "tool_call",
631
+ tool: {
632
+ kind: "tool",
633
+ toolKind: "mcp_generic",
634
+ toolName: `mcp__${item.server}__${item.tool}`,
635
+ toolId: item.id,
636
+ input: {
637
+ server: item.server,
638
+ tool: item.tool,
639
+ payload: item.arguments ?? {},
640
+ },
641
+ rawInput: item.arguments ?? {},
642
+ },
643
+ })]
644
+ case "fileChange":
645
+ return fileChangeToToolCalls(item)
646
+ case "plan":
647
+ return []
648
+ case "error":
649
+ return [timestamped({
650
+ kind: "tool_call",
651
+ tool: {
652
+ kind: "tool",
653
+ toolKind: "unknown_tool",
654
+ toolName: "Error",
655
+ toolId: item.id,
656
+ input: {
657
+ payload: item as unknown as Record<string, unknown>,
658
+ },
659
+ rawInput: item as unknown as Record<string, unknown>,
660
+ },
661
+ })]
662
+ default:
663
+ return []
664
+ }
665
+ }
666
+
667
+ function itemToToolResults(item: ThreadItem): TranscriptEntry[] {
668
+ switch (item.type) {
669
+ case "dynamicToolCall":
670
+ return [timestamped({
671
+ kind: "tool_result",
672
+ toolId: item.id,
673
+ content: dynamicContentToText(item.contentItems) || item,
674
+ isError: item.status === "failed" || item.success === false,
675
+ })]
676
+ case "collabAgentToolCall":
677
+ return [timestamped({
678
+ kind: "tool_result",
679
+ toolId: item.id,
680
+ content: item,
681
+ isError: item.status === "failed",
682
+ })]
683
+ case "commandExecution":
684
+ return [timestamped({
685
+ kind: "tool_result",
686
+ toolId: item.id,
687
+ content: item.aggregatedOutput ?? item,
688
+ isError: (typeof item.exitCode === "number" && item.exitCode !== 0) || item.status === "failed" || item.status === "declined",
689
+ })]
690
+ case "webSearch":
691
+ return [timestamped({
692
+ kind: "tool_result",
693
+ toolId: item.id,
694
+ content: item,
695
+ })]
696
+ case "mcpToolCall":
697
+ return [timestamped({
698
+ kind: "tool_result",
699
+ toolId: item.id,
700
+ content: contentFromMcpResult(item),
701
+ isError: item.status === "failed",
702
+ })]
703
+ case "fileChange":
704
+ return fileChangeToToolResults(item)
705
+ case "plan":
706
+ return []
707
+ case "error":
708
+ return [timestamped({
709
+ kind: "tool_result",
710
+ toolId: item.id,
711
+ content: item.message,
712
+ isError: true,
713
+ })]
714
+ default:
715
+ return []
716
+ }
717
+ }
718
+
719
+ class AsyncQueue<T> implements AsyncIterable<T> {
720
+ private values: T[] = []
721
+ private resolvers: Array<(value: IteratorResult<T>) => void> = []
722
+ private done = false
723
+
724
+ push(value: T) {
725
+ if (this.done) return
726
+ const resolver = this.resolvers.shift()
727
+ if (resolver) {
728
+ resolver({ value, done: false })
729
+ return
730
+ }
731
+ this.values.push(value)
732
+ }
733
+
734
+ finish() {
735
+ if (this.done) return
736
+ this.done = true
737
+ while (this.resolvers.length > 0) {
738
+ const resolver = this.resolvers.shift()
739
+ resolver?.({ value: undefined as T, done: true })
740
+ }
741
+ }
742
+
743
+ [Symbol.asyncIterator](): AsyncIterator<T> {
744
+ return {
745
+ next: () => {
746
+ if (this.values.length > 0) {
747
+ return Promise.resolve({ value: this.values.shift() as T, done: false })
748
+ }
749
+ if (this.done) {
750
+ return Promise.resolve({ value: undefined as T, done: true })
751
+ }
752
+ return new Promise<IteratorResult<T>>((resolve) => {
753
+ this.resolvers.push(resolve)
754
+ })
755
+ },
756
+ }
757
+ }
758
+ }
759
+
760
+ export class CodexAppServerManager {
761
+ private readonly sessions = new Map<string, SessionContext>()
762
+ private readonly spawnProcess: SpawnCodexAppServer
763
+
764
+ constructor(args: { spawnProcess?: SpawnCodexAppServer; binaryPath?: string; extraEnv?: Record<string, string> } = {}) {
765
+ this.spawnProcess = args.spawnProcess ?? ((cwd) =>
766
+ spawn(args.binaryPath ?? "codex", ["app-server"], {
767
+ cwd,
768
+ stdio: ["pipe", "pipe", "pipe"],
769
+ env: { ...process.env, ...args.extraEnv },
770
+ }) as unknown as CodexAppServerProcess)
771
+ }
772
+
773
+ async startSession(args: StartCodexSessionArgs) {
774
+ const existing = this.sessions.get(args.chatId)
775
+ if (existing && !existing.closed && existing.cwd === args.cwd) {
776
+ return
777
+ }
778
+
779
+ if (existing) {
780
+ this.stopSession(args.chatId)
781
+ }
782
+
783
+ const child = this.spawnProcess(args.cwd)
784
+ const context: SessionContext = {
785
+ chatId: args.chatId,
786
+ cwd: args.cwd,
787
+ child,
788
+ pendingRequests: new Map(),
789
+ pendingTurn: null,
790
+ sessionToken: null,
791
+ stderrLines: [],
792
+ closed: false,
793
+ }
794
+ this.sessions.set(args.chatId, context)
795
+ this.attachListeners(context)
796
+
797
+ await this.sendRequest(context, "initialize", {
798
+ clientInfo: {
799
+ name: "tinkaria_desktop",
800
+ title: "Tinkaria",
801
+ version: "0.1.0",
802
+ },
803
+ capabilities: {
804
+ experimentalApi: true,
805
+ },
806
+ } satisfies InitializeParams)
807
+ this.writeMessage(context, {
808
+ method: "initialized",
809
+ })
810
+
811
+ const threadParams = {
812
+ model: args.model,
813
+ cwd: args.cwd,
814
+ serviceTier: args.serviceTier,
815
+ approvalPolicy: "never",
816
+ sandbox: "danger-full-access",
817
+ experimentalRawEvents: false,
818
+ persistExtendedHistory: false,
819
+ } satisfies ThreadStartParams
820
+
821
+ let response: ThreadStartResponse | ThreadResumeResponse
822
+ if (args.sessionToken) {
823
+ try {
824
+ response = await this.sendRequest<ThreadResumeResponse>(context, "thread/resume", {
825
+ threadId: args.sessionToken,
826
+ model: args.model,
827
+ cwd: args.cwd,
828
+ serviceTier: args.serviceTier,
829
+ approvalPolicy: "never",
830
+ sandbox: "danger-full-access",
831
+ persistExtendedHistory: false,
832
+ } satisfies ThreadResumeParams)
833
+ } catch (error) {
834
+ if (!isRecoverableResumeError(error)) {
835
+ this.stopSession(args.chatId)
836
+ throw error
837
+ }
838
+ response = await this.sendRequest<ThreadStartResponse>(context, "thread/start", threadParams)
839
+ }
840
+ } else {
841
+ response = await this.sendRequest<ThreadStartResponse>(context, "thread/start", threadParams)
842
+ }
843
+
844
+ context.sessionToken = response.thread.id
845
+ }
846
+
847
+ async startTurn(args: StartCodexTurnArgs): Promise<HarnessTurn> {
848
+ const context = this.requireSession(args.chatId)
849
+ if (context.pendingTurn) {
850
+ throw new Error("Codex turn is already running")
851
+ }
852
+
853
+ const queue = new AsyncQueue<HarnessEvent>()
854
+ if (context.sessionToken) {
855
+ queue.push({ type: "session_token", sessionToken: context.sessionToken })
856
+ }
857
+ queue.push({ type: "transcript", entry: codexSystemInitEntry(args.model, args.skills) })
858
+
859
+ const pendingTurn: PendingTurn = {
860
+ turnId: null,
861
+ model: args.model,
862
+ planMode: args.planMode,
863
+ queue,
864
+ startedToolIds: new Set(),
865
+ handledDynamicToolIds: new Set(),
866
+ latestPlanExplanation: null,
867
+ latestPlanSteps: [],
868
+ latestPlanText: null,
869
+ planTextByItemId: new Map(),
870
+ todoSequence: 0,
871
+ pendingWebSearchResultToolId: null,
872
+ resolved: false,
873
+ orchestration: args.orchestrator && args.orchestrationChatId
874
+ ? {
875
+ callerChatId: args.orchestrationChatId,
876
+ tools: args.orchestrator,
877
+ }
878
+ : null,
879
+ onToolRequest: args.onToolRequest,
880
+ onApprovalRequest: args.onApprovalRequest,
881
+ }
882
+ context.pendingTurn = pendingTurn
883
+
884
+ try {
885
+ const response = await this.sendRequest<TurnStartResponse>(context, "turn/start", {
886
+ threadId: context.sessionToken ?? "",
887
+ input: [
888
+ {
889
+ type: "text",
890
+ text: args.content,
891
+ text_elements: [],
892
+ },
893
+ ],
894
+ approvalPolicy: "never",
895
+ model: args.model,
896
+ effort: args.effort,
897
+ serviceTier: args.serviceTier,
898
+ collaborationMode: {
899
+ mode: args.planMode ? "plan" : "default",
900
+ settings: {
901
+ model: args.model,
902
+ reasoning_effort: null,
903
+ developer_instructions: getWebContextPrompt("codex", {
904
+ codexNativeSubagentsEnabled: Boolean(args.orchestrator && args.orchestrationChatId),
905
+ }),
906
+ },
907
+ },
908
+ } satisfies TurnStartParams)
909
+ if (context.pendingTurn) {
910
+ context.pendingTurn.turnId = response.turn.id
911
+ } else {
912
+ pendingTurn.turnId = response.turn.id
913
+ }
914
+ } catch (error) {
915
+ context.pendingTurn = null
916
+ queue.finish()
917
+ throw error
918
+ }
919
+
920
+ return {
921
+ provider: "codex",
922
+ stream: queue,
923
+ interrupt: async () => {
924
+ const pendingTurn = context.pendingTurn
925
+ if (!pendingTurn) return
926
+
927
+ context.pendingTurn = null
928
+ pendingTurn.resolved = true
929
+ pendingTurn.queue.finish()
930
+
931
+ if (!pendingTurn.turnId || !context.sessionToken) return
932
+
933
+ await this.sendRequest(context, "turn/interrupt", {
934
+ threadId: context.sessionToken,
935
+ turnId: pendingTurn.turnId,
936
+ } satisfies TurnInterruptParams)
937
+ },
938
+ close: () => {
939
+ this.stopSession(context.chatId)
940
+ },
941
+ }
942
+ }
943
+
944
+ async generateStructured(args: GenerateStructuredArgs): Promise<string | null> {
945
+ const chatId = `quick-${randomUUID()}`
946
+ let turn: HarnessTurn | null = null
947
+ let assistantText = ""
948
+ let resultText = ""
949
+
950
+ try {
951
+ await this.startSession({
952
+ chatId,
953
+ cwd: args.cwd,
954
+ model: args.model ?? "gpt-5.4",
955
+ serviceTier: args.serviceTier ?? "fast",
956
+ sessionToken: null,
957
+ })
958
+
959
+ turn = await this.startTurn({
960
+ chatId,
961
+ model: args.model ?? "gpt-5.4",
962
+ effort: args.effort,
963
+ serviceTier: args.serviceTier ?? "fast",
964
+ content: args.prompt,
965
+ planMode: false,
966
+ onToolRequest: async () => ({}),
967
+ })
968
+
969
+ for await (const event of turn.stream) {
970
+ if (event.type !== "transcript" || !event.entry) continue
971
+ if (event.entry.kind === "assistant_text") {
972
+ assistantText += assistantText ? `\n${event.entry.text}` : event.entry.text
973
+ }
974
+ if (event.entry.kind === "result" && !event.entry.isError && event.entry.result.trim()) {
975
+ resultText = event.entry.result
976
+ }
977
+ }
978
+
979
+ const candidate = assistantText.trim() || resultText.trim()
980
+ return candidate || null
981
+ } finally {
982
+ if (turn) {
983
+ turn.close() // kills session via stopSession
984
+ } else {
985
+ this.stopSession(chatId) // fallback when turn was never created
986
+ }
987
+ }
988
+ }
989
+
990
+ stopSession(chatId: string) {
991
+ const context = this.sessions.get(chatId)
992
+ if (!context) return
993
+ context.closed = true
994
+ // Reject pending RPC promises before killing — prevents leaked awaits
995
+ for (const [, pending] of context.pendingRequests) {
996
+ pending.reject(new Error("Session stopped"))
997
+ }
998
+ context.pendingRequests.clear()
999
+ context.pendingTurn?.queue.finish()
1000
+ this.sessions.delete(chatId)
1001
+ try {
1002
+ context.child.kill("SIGKILL")
1003
+ } catch (error: unknown) {
1004
+ void error
1005
+ // ignore kill failures
1006
+ }
1007
+ }
1008
+
1009
+ stopAll() {
1010
+ for (const chatId of this.sessions.keys()) {
1011
+ this.stopSession(chatId)
1012
+ }
1013
+ }
1014
+
1015
+ private requireSession(chatId: string) {
1016
+ const context = this.sessions.get(chatId)
1017
+ if (!context || context.closed) {
1018
+ throw new Error("Codex session not started")
1019
+ }
1020
+ return context
1021
+ }
1022
+
1023
+ private attachListeners(context: SessionContext) {
1024
+ const lines = createInterface({ input: context.child.stdout })
1025
+ void (async () => {
1026
+ for await (const line of lines) {
1027
+ const parsed = parseJsonLine(line)
1028
+ if (!parsed) continue
1029
+
1030
+ if (isJsonRpcResponse(parsed)) {
1031
+ this.handleResponse(context, parsed)
1032
+ continue
1033
+ }
1034
+
1035
+ if (isServerRequest(parsed)) {
1036
+ this.handleServerRequest(context, parsed).catch(() => {
1037
+ // Swallow — failContext already handles cleanup
1038
+ })
1039
+ continue
1040
+ }
1041
+
1042
+ if (isServerNotification(parsed)) {
1043
+ this.handleNotification(context, parsed).catch(() => {
1044
+ // Swallow — failContext already handles cleanup
1045
+ })
1046
+ }
1047
+ }
1048
+ })()
1049
+
1050
+ const stderr = createInterface({ input: context.child.stderr })
1051
+ void (async () => {
1052
+ for await (const line of stderr) {
1053
+ if (line.trim()) {
1054
+ context.stderrLines.push(line.trim())
1055
+ }
1056
+ }
1057
+ })()
1058
+
1059
+ context.child.on("error", (error) => {
1060
+ this.failContext(context, error.message)
1061
+ })
1062
+
1063
+ context.child.on("close", (code) => {
1064
+ if (context.closed) return
1065
+ queueMicrotask(() => {
1066
+ if (context.closed) return
1067
+ const message = context.stderrLines.at(-1) || `Codex app-server exited with code ${code ?? 1}`
1068
+ this.failContext(context, message)
1069
+ })
1070
+ })
1071
+ }
1072
+
1073
+ private handleResponse(context: SessionContext, response: JsonRpcResponse) {
1074
+ const pending = context.pendingRequests.get(response.id)
1075
+ if (!pending) return
1076
+ context.pendingRequests.delete(response.id)
1077
+ if (response.error) {
1078
+ pending.reject(new Error(`${pending.method} failed: ${response.error.message ?? "Unknown error"}`))
1079
+ return
1080
+ }
1081
+ pending.resolve(response.result)
1082
+ }
1083
+
1084
+ private async handleServerRequest(context: SessionContext, request: ServerRequest) {
1085
+ const pendingTurn = context.pendingTurn
1086
+ if (!pendingTurn) {
1087
+ this.writeMessage(context, {
1088
+ id: request.id,
1089
+ error: {
1090
+ message: "No active turn",
1091
+ },
1092
+ })
1093
+ return
1094
+ }
1095
+
1096
+ if (request.method === "item/tool/requestUserInput") {
1097
+ const questions = toAskUserQuestionItems(request.params)
1098
+ const toolId = request.params.itemId
1099
+ const toolRequest: HarnessToolRequest = {
1100
+ tool: {
1101
+ kind: "tool",
1102
+ toolKind: "ask_user_question",
1103
+ toolName: "AskUserQuestion",
1104
+ toolId,
1105
+ input: { questions },
1106
+ rawInput: {
1107
+ questions: request.params.questions,
1108
+ },
1109
+ },
1110
+ }
1111
+ pendingTurn.queue.push({
1112
+ type: "transcript",
1113
+ entry: timestamped({
1114
+ kind: "tool_call",
1115
+ tool: toolRequest.tool,
1116
+ }),
1117
+ })
1118
+
1119
+ const result = await pendingTurn.onToolRequest(toolRequest)
1120
+ this.writeMessage(context, {
1121
+ id: request.id,
1122
+ result: toToolRequestUserInputResponse(result, request.params.questions),
1123
+ })
1124
+ return
1125
+ }
1126
+
1127
+ if (request.method === "item/tool/call") {
1128
+ pendingTurn.handledDynamicToolIds.add(request.params.callId)
1129
+ if (request.params.tool === "update_plan") {
1130
+ const args = asRecord(request.params.arguments)
1131
+ const plan = Array.isArray(args?.plan) ? args.plan : []
1132
+ const steps: TurnPlanStep[] = plan
1133
+ .map((entry) => asRecord(entry))
1134
+ .filter((entry): entry is Record<string, unknown> => Boolean(entry))
1135
+ .map((entry) => {
1136
+ const status: TurnPlanStep["status"] =
1137
+ entry.status === "completed"
1138
+ ? "completed"
1139
+ : entry.status === "inProgress" || entry.status === "in_progress"
1140
+ ? "inProgress"
1141
+ : "pending"
1142
+ return {
1143
+ step: typeof entry.step === "string" ? entry.step : "",
1144
+ status,
1145
+ }
1146
+ })
1147
+ .filter((step) => step.step.length > 0)
1148
+
1149
+ if (steps.length > 0) {
1150
+ pendingTurn.latestPlanSteps = steps
1151
+ pendingTurn.latestPlanExplanation = typeof args?.explanation === "string" ? args.explanation : pendingTurn.latestPlanExplanation
1152
+ pendingTurn.queue.push({
1153
+ type: "transcript",
1154
+ entry: todoToolCall(request.params.callId, steps),
1155
+ })
1156
+ pendingTurn.queue.push({
1157
+ type: "transcript",
1158
+ entry: timestamped({
1159
+ kind: "tool_result",
1160
+ toolId: request.params.callId,
1161
+ content: "",
1162
+ }),
1163
+ })
1164
+ }
1165
+
1166
+ this.writeMessage(context, {
1167
+ id: request.id,
1168
+ result: {
1169
+ contentItems: [],
1170
+ success: true,
1171
+ } satisfies DynamicToolCallResponse,
1172
+ })
1173
+ return
1174
+ }
1175
+
1176
+ if (request.params.tool === "present_content") {
1177
+ const payload = dynamicToolPayload(request.params.arguments)
1178
+ const parsed = presentContentSchema.safeParse(payload)
1179
+
1180
+ pendingTurn.queue.push({
1181
+ type: "transcript",
1182
+ entry: presentContentToolCall(request.params.callId, payload),
1183
+ })
1184
+
1185
+ if (!parsed.success) {
1186
+ pendingTurn.queue.push({
1187
+ type: "transcript",
1188
+ entry: timestamped({
1189
+ kind: "tool_result",
1190
+ toolId: request.params.callId,
1191
+ content: presentContentValidationError(parsed.error.issues),
1192
+ isError: true,
1193
+ }),
1194
+ })
1195
+ this.writeMessage(context, {
1196
+ id: request.id,
1197
+ result: {
1198
+ contentItems: [{ type: "inputText", text: "Invalid present_content payload" }],
1199
+ success: false,
1200
+ } satisfies DynamicToolCallResponse,
1201
+ })
1202
+ return
1203
+ }
1204
+
1205
+ const normalized = enrichPresentContentResult(parsed.data)
1206
+
1207
+ pendingTurn.queue.push({
1208
+ type: "transcript",
1209
+ entry: timestamped({
1210
+ kind: "tool_result",
1211
+ toolId: request.params.callId,
1212
+ content: normalized,
1213
+ }),
1214
+ })
1215
+ this.writeMessage(context, {
1216
+ id: request.id,
1217
+ result: {
1218
+ contentItems: [{ type: "inputText", text: "presented" }],
1219
+ success: true,
1220
+ } satisfies DynamicToolCallResponse,
1221
+ })
1222
+ return
1223
+ }
1224
+
1225
+ if (request.params.tool === "ask_user_question") {
1226
+ const payload = dynamicToolPayload(request.params.arguments)
1227
+ const parsed = askUserQuestionSchema.safeParse(payload)
1228
+ const questions = parsed.success
1229
+ ? parsed.data.questions.map((question, index) => ({
1230
+ ...question,
1231
+ id: question.id ?? `q-${index}`,
1232
+ }))
1233
+ : []
1234
+
1235
+ pendingTurn.queue.push({
1236
+ type: "transcript",
1237
+ entry: askUserQuestionToolCall(request.params.callId, questions),
1238
+ })
1239
+
1240
+ if (!parsed.success) {
1241
+ pendingTurn.queue.push({
1242
+ type: "transcript",
1243
+ entry: timestamped({
1244
+ kind: "tool_result",
1245
+ toolId: request.params.callId,
1246
+ content: "Invalid ask_user_question payload",
1247
+ isError: true,
1248
+ }),
1249
+ })
1250
+ this.writeMessage(context, {
1251
+ id: request.id,
1252
+ result: {
1253
+ contentItems: [{ type: "inputText", text: "Invalid ask_user_question payload" }],
1254
+ success: false,
1255
+ } satisfies DynamicToolCallResponse,
1256
+ })
1257
+ return
1258
+ }
1259
+
1260
+ const toolRequest: HarnessToolRequest = {
1261
+ tool: {
1262
+ kind: "tool",
1263
+ toolKind: "ask_user_question",
1264
+ toolName: "AskUserQuestion",
1265
+ toolId: request.params.callId,
1266
+ input: { questions },
1267
+ rawInput: payload,
1268
+ },
1269
+ }
1270
+
1271
+ const result = await pendingTurn.onToolRequest(toolRequest)
1272
+ this.writeMessage(context, {
1273
+ id: request.id,
1274
+ result: {
1275
+ contentItems: [{ type: "inputText", text: JSON.stringify(result) }],
1276
+ success: true,
1277
+ } satisfies DynamicToolCallResponse,
1278
+ })
1279
+ return
1280
+ }
1281
+
1282
+ if (await this.handleOrchestrationToolCall(context, pendingTurn, request)) {
1283
+ return
1284
+ }
1285
+
1286
+ const payload = dynamicToolPayload(request.params.arguments)
1287
+ pendingTurn.queue.push({
1288
+ type: "transcript",
1289
+ entry: genericDynamicToolCall(request.params.callId, request.params.tool, payload),
1290
+ })
1291
+ const errorMessage = `Unsupported dynamic tool call: ${request.params.tool}`
1292
+ pendingTurn.queue.push({
1293
+ type: "transcript",
1294
+ entry: timestamped({
1295
+ kind: "tool_result",
1296
+ toolId: request.params.callId,
1297
+ content: errorMessage,
1298
+ isError: true,
1299
+ }),
1300
+ })
1301
+ this.writeMessage(context, {
1302
+ id: request.id,
1303
+ result: {
1304
+ contentItems: [{ type: "inputText", text: errorMessage }],
1305
+ success: false,
1306
+ } satisfies DynamicToolCallResponse,
1307
+ })
1308
+ return
1309
+ }
1310
+
1311
+ if (request.method === "item/commandExecution/requestApproval") {
1312
+ const decision = await pendingTurn.onApprovalRequest?.({
1313
+ requestId: request.id,
1314
+ kind: "command_execution",
1315
+ params: request.params,
1316
+ }) ?? "decline"
1317
+ this.writeMessage(context, {
1318
+ id: request.id,
1319
+ result: {
1320
+ decision,
1321
+ } satisfies CommandExecutionRequestApprovalResponse,
1322
+ })
1323
+ return
1324
+ }
1325
+
1326
+ const decision = await pendingTurn.onApprovalRequest?.({
1327
+ requestId: request.id,
1328
+ kind: "file_change",
1329
+ params: request.params,
1330
+ }) ?? "decline"
1331
+ this.writeMessage(context, {
1332
+ id: request.id,
1333
+ result: {
1334
+ decision,
1335
+ } satisfies FileChangeRequestApprovalResponse,
1336
+ })
1337
+ }
1338
+
1339
+ private async handleOrchestrationToolCall(
1340
+ context: SessionContext,
1341
+ pendingTurn: PendingTurn,
1342
+ request: Extract<ServerRequest, { method: "item/tool/call" }>,
1343
+ ): Promise<boolean> {
1344
+ const orchestration = pendingTurn.orchestration
1345
+ if (!orchestration) return false
1346
+
1347
+ const payload = dynamicToolPayload(request.params.arguments)
1348
+ const toolName = request.params.tool
1349
+ if (
1350
+ toolName !== "spawn_agent"
1351
+ && toolName !== "list_agents"
1352
+ && toolName !== "send_input"
1353
+ && toolName !== "wait_agent"
1354
+ && toolName !== "close_agent"
1355
+ ) {
1356
+ return false
1357
+ }
1358
+
1359
+ pendingTurn.queue.push({
1360
+ type: "transcript",
1361
+ entry: orchestrationToolCall(request.params.callId, toolName, payload),
1362
+ })
1363
+
1364
+ try {
1365
+ let result: unknown
1366
+ let isError = false
1367
+
1368
+ switch (toolName) {
1369
+ case "spawn_agent":
1370
+ result = await orchestration.tools.spawnAgent(orchestration.callerChatId, {
1371
+ instruction: typeof payload.instruction === "string" ? payload.instruction : "",
1372
+ provider: payload.provider === "claude" || payload.provider === "codex" ? payload.provider : undefined,
1373
+ model: typeof payload.model === "string" ? payload.model : undefined,
1374
+ forkContext: typeof payload.fork_context === "boolean" ? payload.fork_context : undefined,
1375
+ mode: payload.mode === "blocking" || payload.mode === "background" ? payload.mode : undefined,
1376
+ resume: payload.resume === "immediate" || payload.resume === "gate" ? payload.resume : undefined,
1377
+ })
1378
+ break
1379
+ case "list_agents":
1380
+ result = orchestration.tools.listAgents(orchestration.callerChatId)
1381
+ break
1382
+ case "send_input":
1383
+ await orchestration.tools.sendInput(orchestration.callerChatId, {
1384
+ targetChatId: typeof payload.targetChatId === "string" ? payload.targetChatId : "",
1385
+ content: typeof payload.content === "string" ? payload.content : "",
1386
+ model: typeof payload.model === "string" ? payload.model : undefined,
1387
+ })
1388
+ result = "Input sent"
1389
+ break
1390
+ case "wait_agent": {
1391
+ const waitResult = await orchestration.tools.waitForResult(orchestration.callerChatId, {
1392
+ targetChatId: typeof payload.targetChatId === "string" ? payload.targetChatId : "",
1393
+ timeoutMs: typeof payload.timeoutMs === "number" ? payload.timeoutMs : undefined,
1394
+ })
1395
+ result = waitResult
1396
+ isError = waitResult.isError
1397
+ break
1398
+ }
1399
+ case "close_agent":
1400
+ await orchestration.tools.closeAgent(orchestration.callerChatId, {
1401
+ targetChatId: typeof payload.targetChatId === "string" ? payload.targetChatId : "",
1402
+ })
1403
+ result = "Agent closed"
1404
+ break
1405
+ }
1406
+
1407
+ pendingTurn.queue.push({
1408
+ type: "transcript",
1409
+ entry: timestamped({
1410
+ kind: "tool_result",
1411
+ toolId: request.params.callId,
1412
+ content: result,
1413
+ isError,
1414
+ }),
1415
+ })
1416
+ this.writeMessage(context, {
1417
+ id: request.id,
1418
+ result: {
1419
+ contentItems: [{ type: "inputText", text: typeof result === "string" ? result : JSON.stringify(result) }],
1420
+ success: !isError,
1421
+ } satisfies DynamicToolCallResponse,
1422
+ })
1423
+ } catch (error) {
1424
+ const message = errorMessage(error)
1425
+ pendingTurn.queue.push({
1426
+ type: "transcript",
1427
+ entry: timestamped({
1428
+ kind: "tool_result",
1429
+ toolId: request.params.callId,
1430
+ content: message,
1431
+ isError: true,
1432
+ }),
1433
+ })
1434
+ this.writeMessage(context, {
1435
+ id: request.id,
1436
+ result: {
1437
+ contentItems: [{ type: "inputText", text: message }],
1438
+ success: false,
1439
+ } satisfies DynamicToolCallResponse,
1440
+ })
1441
+ }
1442
+
1443
+ return true
1444
+ }
1445
+
1446
+ private async handleNotification(context: SessionContext, notification: ServerNotification) {
1447
+ if (notification.method === "thread/started") {
1448
+ context.sessionToken = notification.params.thread.id
1449
+ if (context.pendingTurn) {
1450
+ context.pendingTurn.queue.push({
1451
+ type: "session_token",
1452
+ sessionToken: notification.params.thread.id,
1453
+ })
1454
+ }
1455
+ return
1456
+ }
1457
+
1458
+ const pendingTurn = context.pendingTurn
1459
+ if (!pendingTurn) return
1460
+
1461
+ switch (notification.method) {
1462
+ case "turn/plan/updated":
1463
+ this.handlePlanUpdated(pendingTurn, notification.params)
1464
+ return
1465
+ case "item/started":
1466
+ this.handleItemStarted(pendingTurn, notification.params)
1467
+ return
1468
+ case "item/completed":
1469
+ this.handleItemCompleted(pendingTurn, notification.params)
1470
+ return
1471
+ case "item/plan/delta":
1472
+ this.handlePlanDelta(pendingTurn, notification.params)
1473
+ return
1474
+ case "turn/completed":
1475
+ await this.handleTurnCompleted(context, notification.params)
1476
+ return
1477
+ case "thread/compacted":
1478
+ this.handleContextCompacted(pendingTurn, notification.params)
1479
+ return
1480
+ case "error":
1481
+ this.failContext(context, notification.params.error.message)
1482
+ return
1483
+ default:
1484
+ return
1485
+ }
1486
+ }
1487
+
1488
+ private handleItemStarted(pendingTurn: PendingTurn, notification: ItemStartedNotification) {
1489
+ if (notification.item.type === "plan") {
1490
+ pendingTurn.planTextByItemId.set(notification.item.id, notification.item.text)
1491
+ pendingTurn.latestPlanText = notification.item.text
1492
+ return
1493
+ }
1494
+
1495
+ if (
1496
+ notification.item.type === "commandExecution"
1497
+ || notification.item.type === "webSearch"
1498
+ || notification.item.type === "mcpToolCall"
1499
+ || notification.item.type === "dynamicToolCall"
1500
+ || notification.item.type === "collabAgentToolCall"
1501
+ || notification.item.type === "fileChange"
1502
+ || notification.item.type === "error"
1503
+ ) {
1504
+ if (pendingTurn.handledDynamicToolIds.has(notification.item.id)) {
1505
+ return
1506
+ }
1507
+ if (notification.item.type === "webSearch" && !webSearchQuery(notification.item)) {
1508
+ return
1509
+ }
1510
+ }
1511
+
1512
+ const entries = itemToToolCalls(notification.item)
1513
+ for (const entry of entries) {
1514
+ if (entry.kind === "tool_call") {
1515
+ pendingTurn.startedToolIds.add(entry.tool.toolId)
1516
+ }
1517
+ pendingTurn.queue.push({ type: "transcript", entry })
1518
+ }
1519
+ }
1520
+
1521
+ private handleItemCompleted(pendingTurn: PendingTurn, notification: ItemCompletedNotification) {
1522
+ if (notification.item.type === "agentMessage") {
1523
+ pendingTurn.queue.push({
1524
+ type: "transcript",
1525
+ entry: timestamped({
1526
+ kind: "assistant_text",
1527
+ text: notification.item.text,
1528
+ }),
1529
+ })
1530
+ if (pendingTurn.pendingWebSearchResultToolId && notification.item.text.trim()) {
1531
+ pendingTurn.queue.push({
1532
+ type: "transcript",
1533
+ entry: timestamped({
1534
+ kind: "tool_result",
1535
+ toolId: pendingTurn.pendingWebSearchResultToolId,
1536
+ content: notification.item.text,
1537
+ }),
1538
+ })
1539
+ pendingTurn.pendingWebSearchResultToolId = null
1540
+ }
1541
+ return
1542
+ }
1543
+
1544
+ if (notification.item.type === "plan") {
1545
+ pendingTurn.planTextByItemId.set(notification.item.id, notification.item.text)
1546
+ pendingTurn.latestPlanText = notification.item.text
1547
+ return
1548
+ }
1549
+
1550
+ if (pendingTurn.handledDynamicToolIds.has(notification.item.id)) {
1551
+ return
1552
+ }
1553
+
1554
+ const startedEntries = itemToToolCalls(notification.item)
1555
+ for (const entry of startedEntries) {
1556
+ if (entry.kind !== "tool_call") {
1557
+ continue
1558
+ }
1559
+ if (pendingTurn.startedToolIds.has(entry.tool.toolId)) {
1560
+ continue
1561
+ }
1562
+ pendingTurn.startedToolIds.add(entry.tool.toolId)
1563
+ pendingTurn.queue.push({ type: "transcript", entry })
1564
+ }
1565
+
1566
+ const resultEntries = itemToToolResults(notification.item)
1567
+ for (const entry of resultEntries) {
1568
+ pendingTurn.queue.push({ type: "transcript", entry })
1569
+ if (notification.item.type === "webSearch" && entry.kind === "tool_result" && !entry.isError) {
1570
+ pendingTurn.pendingWebSearchResultToolId = notification.item.id
1571
+ }
1572
+ }
1573
+ }
1574
+
1575
+ private handlePlanUpdated(pendingTurn: PendingTurn, notification: TurnPlanUpdatedNotification) {
1576
+ pendingTurn.latestPlanExplanation = notification.explanation ?? null
1577
+ pendingTurn.latestPlanSteps = notification.plan
1578
+ if (notification.plan.length === 0) {
1579
+ return
1580
+ }
1581
+ pendingTurn.todoSequence += 1
1582
+ pendingTurn.queue.push({
1583
+ type: "transcript",
1584
+ entry: todoToolCall(
1585
+ `${notification.turnId}:todo-${pendingTurn.todoSequence}`,
1586
+ notification.plan
1587
+ ),
1588
+ })
1589
+ }
1590
+
1591
+ private handlePlanDelta(pendingTurn: PendingTurn, notification: PlanDeltaNotification) {
1592
+ const current = pendingTurn.planTextByItemId.get(notification.itemId) ?? ""
1593
+ const next = `${current}${notification.delta}`
1594
+ pendingTurn.planTextByItemId.set(notification.itemId, next)
1595
+ pendingTurn.latestPlanText = next
1596
+ }
1597
+
1598
+ private handleContextCompacted(pendingTurn: PendingTurn, _notification: ContextCompactedNotification) {
1599
+ pendingTurn.queue.push({
1600
+ type: "transcript",
1601
+ entry: timestamped({ kind: "compact_boundary" }),
1602
+ })
1603
+ }
1604
+
1605
+ private async handleTurnCompleted(context: SessionContext, notification: TurnCompletedNotification) {
1606
+ const pendingTurn = context.pendingTurn
1607
+ if (!pendingTurn) return
1608
+ const status = notification.turn.status
1609
+ const isCancelled = status === "interrupted"
1610
+ const isError = status === "failed"
1611
+ pendingTurn.pendingWebSearchResultToolId = null
1612
+
1613
+ if (!isCancelled && !isError && pendingTurn.planMode) {
1614
+ const planText = pendingTurn.latestPlanText?.trim()
1615
+ || renderPlanMarkdownFromSteps(pendingTurn.latestPlanSteps).trim()
1616
+
1617
+ if (planText) {
1618
+ pendingTurn.turnId = null
1619
+ const tool = {
1620
+ kind: "tool" as const,
1621
+ toolKind: "exit_plan_mode" as const,
1622
+ toolName: "ExitPlanMode",
1623
+ toolId: `${notification.turn.id}:exit-plan`,
1624
+ input: {
1625
+ plan: planText,
1626
+ summary: pendingTurn.latestPlanExplanation ?? undefined,
1627
+ },
1628
+ rawInput: {
1629
+ plan: planText,
1630
+ summary: pendingTurn.latestPlanExplanation ?? undefined,
1631
+ },
1632
+ }
1633
+ pendingTurn.queue.push({
1634
+ type: "transcript",
1635
+ entry: timestamped({
1636
+ kind: "tool_call",
1637
+ tool,
1638
+ }),
1639
+ })
1640
+ await pendingTurn.onToolRequest({ tool })
1641
+ pendingTurn.resolved = true
1642
+ pendingTurn.queue.finish()
1643
+ context.pendingTurn = null
1644
+ return
1645
+ }
1646
+ }
1647
+
1648
+ pendingTurn.resolved = true
1649
+ pendingTurn.queue.push({
1650
+ type: "transcript",
1651
+ entry: timestamped({
1652
+ kind: "result",
1653
+ subtype: isCancelled ? "cancelled" : isError ? "error" : "success",
1654
+ isError,
1655
+ durationMs: 0,
1656
+ result: notification.turn.error?.message ?? "",
1657
+ }),
1658
+ })
1659
+ pendingTurn.queue.finish()
1660
+ context.pendingTurn = null
1661
+ }
1662
+
1663
+ private failContext(context: SessionContext, message: string) {
1664
+ const pendingTurn = context.pendingTurn
1665
+ if (pendingTurn && !pendingTurn.resolved) {
1666
+ pendingTurn.queue.push({
1667
+ type: "transcript",
1668
+ entry: timestamped({
1669
+ kind: "result",
1670
+ subtype: "error",
1671
+ isError: true,
1672
+ durationMs: 0,
1673
+ result: message,
1674
+ }),
1675
+ })
1676
+ pendingTurn.queue.finish()
1677
+ context.pendingTurn = null
1678
+ }
1679
+
1680
+ for (const pending of context.pendingRequests.values()) {
1681
+ pending.reject(new Error(message))
1682
+ }
1683
+ context.pendingRequests.clear()
1684
+ context.closed = true
1685
+ }
1686
+
1687
+ private async sendRequest<TResult>(context: SessionContext, method: string, params: unknown): Promise<TResult> {
1688
+ const id = randomUUID()
1689
+ const promise = new Promise<TResult>((resolve, reject) => {
1690
+ context.pendingRequests.set(id, {
1691
+ method,
1692
+ resolve: resolve as (value: unknown) => void,
1693
+ reject,
1694
+ })
1695
+ })
1696
+ this.writeMessage(context, {
1697
+ id,
1698
+ method,
1699
+ params,
1700
+ })
1701
+ return await promise
1702
+ }
1703
+
1704
+ private writeMessage(context: SessionContext, message: Record<string, unknown>) {
1705
+ if (context.closed) return
1706
+ try {
1707
+ context.child.stdin.write(`${JSON.stringify(message)}\n`)
1708
+ } catch (error: unknown) {
1709
+ void error
1710
+ // Child process already dead — ignore write failures (EPIPE etc.)
1711
+ }
1712
+ }
1713
+ }