@cuongtran001/kanna 0.39.2

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 (473) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +355 -0
  3. package/bin/kanna +9 -0
  4. package/dist/client/apple-touch-icon.png +0 -0
  5. package/dist/client/assets/abap-BdImnpbu.js +1 -0
  6. package/dist/client/assets/actionscript-3-CoDkCxhg.js +1 -0
  7. package/dist/client/assets/ada-bCR0ucgS.js +1 -0
  8. package/dist/client/assets/andromeeda-C4gqWexZ.js +1 -0
  9. package/dist/client/assets/angular-html-CU67Zn6k.js +1 -0
  10. package/dist/client/assets/angular-ts-BwZT4LLn.js +1 -0
  11. package/dist/client/assets/apache-Pmp26Uib.js +1 -0
  12. package/dist/client/assets/apex-D8_7TLub.js +1 -0
  13. package/dist/client/assets/apl-dKokRX4l.js +1 -0
  14. package/dist/client/assets/applescript-Co6uUVPk.js +1 -0
  15. package/dist/client/assets/ara-BRHolxvo.js +1 -0
  16. package/dist/client/assets/asciidoc-Ve4PFQV2.js +1 -0
  17. package/dist/client/assets/asm-D_Q5rh1f.js +1 -0
  18. package/dist/client/assets/astro-CbQHKStN.js +1 -0
  19. package/dist/client/assets/aurora-x-D-2ljcwZ.js +1 -0
  20. package/dist/client/assets/awk-DMzUqQB5.js +1 -0
  21. package/dist/client/assets/ayu-dark-DYE7WIF3.js +1 -0
  22. package/dist/client/assets/ayu-light-BA47KaF1.js +1 -0
  23. package/dist/client/assets/ayu-mirage-32ctXXKs.js +1 -0
  24. package/dist/client/assets/ballerina-BFfxhgS-.js +1 -0
  25. package/dist/client/assets/bat-BkioyH1T.js +1 -0
  26. package/dist/client/assets/beancount-k_qm7-4y.js +1 -0
  27. package/dist/client/assets/berry-uYugtg8r.js +1 -0
  28. package/dist/client/assets/bibtex-CHM0blh-.js +1 -0
  29. package/dist/client/assets/bicep-Bmn6On1c.js +1 -0
  30. package/dist/client/assets/bird2-DPOp833l.js +1 -0
  31. package/dist/client/assets/blade-D4QpJJKB.js +1 -0
  32. package/dist/client/assets/bricolage-grotesque-latin-ext-wght-normal-CcLUaPy7.woff2 +0 -0
  33. package/dist/client/assets/bricolage-grotesque-latin-wght-normal-DLoelf7F.woff2 +0 -0
  34. package/dist/client/assets/bricolage-grotesque-vietnamese-wght-normal-BUzh504Q.woff2 +0 -0
  35. package/dist/client/assets/bsl-BO_Y6i37.js +1 -0
  36. package/dist/client/assets/c-BIGW1oBm.js +1 -0
  37. package/dist/client/assets/c3-eo99z4R2.js +1 -0
  38. package/dist/client/assets/cadence-Bv_4Rxtq.js +1 -0
  39. package/dist/client/assets/cairo-KRGpt6FW.js +1 -0
  40. package/dist/client/assets/catppuccin-frappe-DFWUc33u.js +1 -0
  41. package/dist/client/assets/catppuccin-latte-C9dUb6Cb.js +1 -0
  42. package/dist/client/assets/catppuccin-macchiato-DQyhUUbL.js +1 -0
  43. package/dist/client/assets/catppuccin-mocha-D87Tk5Gz.js +1 -0
  44. package/dist/client/assets/clarity-D53aC0YG.js +1 -0
  45. package/dist/client/assets/clojure-P80f7IUj.js +1 -0
  46. package/dist/client/assets/cmake-D1j8_8rp.js +1 -0
  47. package/dist/client/assets/cobol-nwyudZeR.js +1 -0
  48. package/dist/client/assets/codeowners-Bp6g37R7.js +1 -0
  49. package/dist/client/assets/codeql-DsOJ9woJ.js +1 -0
  50. package/dist/client/assets/coffee-Ch7k5sss.js +1 -0
  51. package/dist/client/assets/common-lisp-Cg-RD9OK.js +1 -0
  52. package/dist/client/assets/coq-DkFqJrB1.js +1 -0
  53. package/dist/client/assets/cpp-CofmeUqb.js +1 -0
  54. package/dist/client/assets/crystal-tKQVLTB8.js +1 -0
  55. package/dist/client/assets/csharp-COcwbKMJ.js +1 -0
  56. package/dist/client/assets/css-DPfMkruS.js +1 -0
  57. package/dist/client/assets/csv-fuZLfV_i.js +1 -0
  58. package/dist/client/assets/cue-D82EKSYY.js +1 -0
  59. package/dist/client/assets/cypher-COkxafJQ.js +1 -0
  60. package/dist/client/assets/d-85-TOEBH.js +1 -0
  61. package/dist/client/assets/dark-plus-C3mMm8J8.js +1 -0
  62. package/dist/client/assets/dart-CF10PKvl.js +1 -0
  63. package/dist/client/assets/dax-CEL-wOlO.js +1 -0
  64. package/dist/client/assets/desktop-BmXAJ9_W.js +1 -0
  65. package/dist/client/assets/diff-D97Zzqfu.js +1 -0
  66. package/dist/client/assets/docker-BcOcwvcX.js +1 -0
  67. package/dist/client/assets/dotenv-Da5cRb03.js +1 -0
  68. package/dist/client/assets/dracula-BzJJZx-M.js +1 -0
  69. package/dist/client/assets/dracula-soft-BXkSAIEj.js +1 -0
  70. package/dist/client/assets/dream-maker-BtqSS_iP.js +1 -0
  71. package/dist/client/assets/edge-BkV0erSs.js +1 -0
  72. package/dist/client/assets/elixir-CDX3lj18.js +1 -0
  73. package/dist/client/assets/elm-DbKCFpqz.js +1 -0
  74. package/dist/client/assets/emacs-lisp-C9XAeP06.js +1 -0
  75. package/dist/client/assets/erb-B12qg9BL.js +1 -0
  76. package/dist/client/assets/erlang-DsQrWhSR.js +1 -0
  77. package/dist/client/assets/everforest-dark-BgDCqdQA.js +1 -0
  78. package/dist/client/assets/everforest-light-C8M2exoo.js +1 -0
  79. package/dist/client/assets/fennel-BYunw83y.js +1 -0
  80. package/dist/client/assets/fish-BvzEVeQv.js +1 -0
  81. package/dist/client/assets/fluent-C4IJs8-o.js +1 -0
  82. package/dist/client/assets/fortran-fixed-form-CkoXwp7k.js +1 -0
  83. package/dist/client/assets/fortran-free-form-BxgE0vQu.js +1 -0
  84. package/dist/client/assets/fsharp-CXgrBDvD.js +1 -0
  85. package/dist/client/assets/gdresource-BOOCDP_w.js +1 -0
  86. package/dist/client/assets/gdscript-C5YyOfLZ.js +1 -0
  87. package/dist/client/assets/gdshader-DkwncUOv.js +1 -0
  88. package/dist/client/assets/genie-D0YGMca9.js +1 -0
  89. package/dist/client/assets/gherkin-DyxjwDmM.js +1 -0
  90. package/dist/client/assets/git-commit-F4YmCXRG.js +1 -0
  91. package/dist/client/assets/git-rebase-r7XF79zn.js +1 -0
  92. package/dist/client/assets/github-dark-DHJKELXO.js +1 -0
  93. package/dist/client/assets/github-dark-default-Cuk6v7N8.js +1 -0
  94. package/dist/client/assets/github-dark-dimmed-DH5Ifo-i.js +1 -0
  95. package/dist/client/assets/github-dark-high-contrast-E3gJ1_iC.js +1 -0
  96. package/dist/client/assets/github-light-DAi9KRSo.js +1 -0
  97. package/dist/client/assets/github-light-default-D7oLnXFd.js +1 -0
  98. package/dist/client/assets/github-light-high-contrast-BfjtVDDH.js +1 -0
  99. package/dist/client/assets/gleam-BspZqrRM.js +1 -0
  100. package/dist/client/assets/glimmer-js-Rg0-pVw9.js +1 -0
  101. package/dist/client/assets/glimmer-ts-U6CK756n.js +1 -0
  102. package/dist/client/assets/glsl-DplSGwfg.js +1 -0
  103. package/dist/client/assets/gn-n2N0HUVH.js +1 -0
  104. package/dist/client/assets/gnuplot-DdkO51Og.js +1 -0
  105. package/dist/client/assets/go-CxLEBnE3.js +1 -0
  106. package/dist/client/assets/graphql-ChdNCCLP.js +1 -0
  107. package/dist/client/assets/groovy-gcz8RCvz.js +1 -0
  108. package/dist/client/assets/gruvbox-dark-hard-CFHQjOhq.js +1 -0
  109. package/dist/client/assets/gruvbox-dark-medium-GsRaNv29.js +1 -0
  110. package/dist/client/assets/gruvbox-dark-soft-CVdnzihN.js +1 -0
  111. package/dist/client/assets/gruvbox-light-hard-CH1njM8p.js +1 -0
  112. package/dist/client/assets/gruvbox-light-medium-DRw_LuNl.js +1 -0
  113. package/dist/client/assets/gruvbox-light-soft-hJgmCMqR.js +1 -0
  114. package/dist/client/assets/hack-CaT9iCJl.js +1 -0
  115. package/dist/client/assets/haml-B8DHNrY2.js +1 -0
  116. package/dist/client/assets/handlebars-BL8al0AC.js +1 -0
  117. package/dist/client/assets/haskell-Df6bDoY_.js +1 -0
  118. package/dist/client/assets/haxe-CzTSHFRz.js +1 -0
  119. package/dist/client/assets/hcl-BWvSN4gD.js +1 -0
  120. package/dist/client/assets/hjson-D5-asLiD.js +1 -0
  121. package/dist/client/assets/hlsl-D3lLCCz7.js +1 -0
  122. package/dist/client/assets/horizon-BUw7H-hv.js +1 -0
  123. package/dist/client/assets/horizon-bright-Cn-bp-IR.js +1 -0
  124. package/dist/client/assets/houston-DnULxvSX.js +1 -0
  125. package/dist/client/assets/html-GMplVEZG.js +1 -0
  126. package/dist/client/assets/html-derivative-BFtXZ54Q.js +1 -0
  127. package/dist/client/assets/http-jrhK8wxY.js +1 -0
  128. package/dist/client/assets/hurl-irOxFIW8.js +1 -0
  129. package/dist/client/assets/hxml-Bvhsp5Yf.js +1 -0
  130. package/dist/client/assets/hy-DFXneXwc.js +1 -0
  131. package/dist/client/assets/imba-DGztddWO.js +1 -0
  132. package/dist/client/assets/index-Do7324M0.css +32 -0
  133. package/dist/client/assets/index-ktE9DLCD.js +2620 -0
  134. package/dist/client/assets/ini-BEwlwnbL.js +1 -0
  135. package/dist/client/assets/java-CylS5w8V.js +1 -0
  136. package/dist/client/assets/javascript-wDzz0qaB.js +1 -0
  137. package/dist/client/assets/jinja-4LBKfQ-Z.js +1 -0
  138. package/dist/client/assets/jison-wvAkD_A8.js +1 -0
  139. package/dist/client/assets/json-Cp-IABpG.js +1 -0
  140. package/dist/client/assets/json5-C9tS-k6U.js +1 -0
  141. package/dist/client/assets/jsonc-Des-eS-w.js +1 -0
  142. package/dist/client/assets/jsonl-DcaNXYhu.js +1 -0
  143. package/dist/client/assets/jsonnet-DFQXde-d.js +1 -0
  144. package/dist/client/assets/jssm-C2t-YnRu.js +1 -0
  145. package/dist/client/assets/jsx-g9-lgVsj.js +1 -0
  146. package/dist/client/assets/julia-CxzCAyBv.js +1 -0
  147. package/dist/client/assets/just-Cw27pwNe.js +1 -0
  148. package/dist/client/assets/kanagawa-dragon-CkXjmgJE.js +1 -0
  149. package/dist/client/assets/kanagawa-lotus-CfQXZHmo.js +1 -0
  150. package/dist/client/assets/kanagawa-wave-DWedfzmr.js +1 -0
  151. package/dist/client/assets/kdl-DV7GczEv.js +1 -0
  152. package/dist/client/assets/kotlin-BdnUsdx6.js +1 -0
  153. package/dist/client/assets/kusto-DZf3V79B.js +1 -0
  154. package/dist/client/assets/laserwave-DUszq2jm.js +1 -0
  155. package/dist/client/assets/latex-CWtU0Tv5.js +1 -0
  156. package/dist/client/assets/lean-BZvkOJ9d.js +1 -0
  157. package/dist/client/assets/less-B1dDrJ26.js +1 -0
  158. package/dist/client/assets/light-plus-B7mTdjB0.js +1 -0
  159. package/dist/client/assets/liquid-DYVedYrR.js +1 -0
  160. package/dist/client/assets/llvm-DjAJT7YJ.js +1 -0
  161. package/dist/client/assets/log-2UxHyX5q.js +1 -0
  162. package/dist/client/assets/logo-BtOb2qkB.js +1 -0
  163. package/dist/client/assets/lua-BaeVxFsk.js +1 -0
  164. package/dist/client/assets/luau-C-HG3fhB.js +1 -0
  165. package/dist/client/assets/make-CHLpvVh8.js +1 -0
  166. package/dist/client/assets/markdown-Cvjx9yec.js +1 -0
  167. package/dist/client/assets/marko-CnJfTvn9.js +1 -0
  168. package/dist/client/assets/material-theme-D5KoaKCx.js +1 -0
  169. package/dist/client/assets/material-theme-darker-BfHTSMKl.js +1 -0
  170. package/dist/client/assets/material-theme-lighter-B0m2ddpp.js +1 -0
  171. package/dist/client/assets/material-theme-ocean-CyktbL80.js +1 -0
  172. package/dist/client/assets/material-theme-palenight-Csfq5Kiy.js +1 -0
  173. package/dist/client/assets/matlab-D7o27uSR.js +1 -0
  174. package/dist/client/assets/mdc-BMNejdWA.js +1 -0
  175. package/dist/client/assets/mdx-Cmh6b_Ma.js +1 -0
  176. package/dist/client/assets/mermaid-mWjccvbQ.js +1 -0
  177. package/dist/client/assets/min-dark-CafNBF8u.js +1 -0
  178. package/dist/client/assets/min-light-CTRr51gU.js +1 -0
  179. package/dist/client/assets/mipsasm-CKIfxQSi.js +1 -0
  180. package/dist/client/assets/mojo-rZm6bMo-.js +1 -0
  181. package/dist/client/assets/monokai-D4h5O-jR.js +1 -0
  182. package/dist/client/assets/moonbit-_H4v1dQx.js +1 -0
  183. package/dist/client/assets/move-IF9eRakj.js +1 -0
  184. package/dist/client/assets/narrat-DRg8JJMk.js +1 -0
  185. package/dist/client/assets/nextflow-Zz6hmt5N.js +1 -0
  186. package/dist/client/assets/nextflow-groovy-BeH2EWoN.js +1 -0
  187. package/dist/client/assets/nginx-BpAMiNFr.js +1 -0
  188. package/dist/client/assets/night-owl-C39BiMTA.js +1 -0
  189. package/dist/client/assets/night-owl-light-CMTm3GFP.js +1 -0
  190. package/dist/client/assets/nim-CVrawwO9.js +1 -0
  191. package/dist/client/assets/nix-CwoSXNpI.js +1 -0
  192. package/dist/client/assets/nord-Ddv68eIx.js +1 -0
  193. package/dist/client/assets/nushell-Cz2AlsmD.js +1 -0
  194. package/dist/client/assets/objective-c-DXmwc3jG.js +1 -0
  195. package/dist/client/assets/objective-cpp-CLxacb5B.js +1 -0
  196. package/dist/client/assets/ocaml-C0hk2d4L.js +1 -0
  197. package/dist/client/assets/odin-BBf5iR-q.js +1 -0
  198. package/dist/client/assets/one-dark-pro-DVMEJ2y_.js +1 -0
  199. package/dist/client/assets/one-light-C3Wv6jpd.js +1 -0
  200. package/dist/client/assets/openscad-C4EeE6gA.js +1 -0
  201. package/dist/client/assets/pascal-D93ZcfNL.js +1 -0
  202. package/dist/client/assets/perl-C0TMdlhV.js +1 -0
  203. package/dist/client/assets/php-Dhbhpdrm.js +1 -0
  204. package/dist/client/assets/pierre-dark-DF2SEV7i.js +1 -0
  205. package/dist/client/assets/pierre-light-DOlZxES8.js +1 -0
  206. package/dist/client/assets/pkl-u5AG7uiY.js +1 -0
  207. package/dist/client/assets/plastic-3e1v2bzS.js +1 -0
  208. package/dist/client/assets/plsql-ChMvpjG-.js +1 -0
  209. package/dist/client/assets/po-BTJTHyun.js +1 -0
  210. package/dist/client/assets/poimandres-CS3Unz2-.js +1 -0
  211. package/dist/client/assets/polar-C0HS_06l.js +1 -0
  212. package/dist/client/assets/postcss-CXtECtnM.js +1 -0
  213. package/dist/client/assets/powerquery-CEu0bR-o.js +1 -0
  214. package/dist/client/assets/powershell-Dpen1YoG.js +1 -0
  215. package/dist/client/assets/prisma-Dd19v3D-.js +1 -0
  216. package/dist/client/assets/prolog-CbFg5uaA.js +1 -0
  217. package/dist/client/assets/proto-C7zT0LnQ.js +1 -0
  218. package/dist/client/assets/pug-CGlum2m_.js +1 -0
  219. package/dist/client/assets/puppet-BMWR74SV.js +1 -0
  220. package/dist/client/assets/purescript-CklMAg4u.js +1 -0
  221. package/dist/client/assets/python-B6aJPvgy.js +1 -0
  222. package/dist/client/assets/qml-3beO22l8.js +1 -0
  223. package/dist/client/assets/qmldir-C8lEn-DE.js +1 -0
  224. package/dist/client/assets/qss-IeuSbFQv.js +1 -0
  225. package/dist/client/assets/r-Dspwwk_N.js +1 -0
  226. package/dist/client/assets/racket-BqYA7rlc.js +1 -0
  227. package/dist/client/assets/raku-DXvB9xmW.js +1 -0
  228. package/dist/client/assets/razor-Uh8Bk_45.js +1 -0
  229. package/dist/client/assets/red-bN70gL4F.js +1 -0
  230. package/dist/client/assets/reg-C-SQnVFl.js +1 -0
  231. package/dist/client/assets/regexp-CDVJQ6XC.js +1 -0
  232. package/dist/client/assets/rel-C3B-1QV4.js +1 -0
  233. package/dist/client/assets/riscv-BM1_JUlF.js +1 -0
  234. package/dist/client/assets/ron-D8l8udqQ.js +1 -0
  235. package/dist/client/assets/rose-pine-dawn-DHQR4-dF.js +1 -0
  236. package/dist/client/assets/rose-pine-moon-D4_iv3hh.js +1 -0
  237. package/dist/client/assets/rose-pine-qdsjHGoJ.js +1 -0
  238. package/dist/client/assets/rosmsg-BJDFO7_C.js +1 -0
  239. package/dist/client/assets/rst-BrH8l1NY.js +1 -0
  240. package/dist/client/assets/ruby-Dw2BHqvy.js +1 -0
  241. package/dist/client/assets/rust-B1yitclQ.js +1 -0
  242. package/dist/client/assets/sas-cz2c8ADy.js +1 -0
  243. package/dist/client/assets/sass-Cj5Yp3dK.js +1 -0
  244. package/dist/client/assets/scala-C151Ov-r.js +1 -0
  245. package/dist/client/assets/scheme-C98Dy4si.js +1 -0
  246. package/dist/client/assets/scss-OYdSNvt2.js +1 -0
  247. package/dist/client/assets/sdbl-DVxCFoDh.js +1 -0
  248. package/dist/client/assets/shaderlab-Dg9Lc6iA.js +1 -0
  249. package/dist/client/assets/shellscript-Yzrsuije.js +1 -0
  250. package/dist/client/assets/shellsession-BADoaaVG.js +1 -0
  251. package/dist/client/assets/slack-dark-BthQWCQV.js +1 -0
  252. package/dist/client/assets/slack-ochin-DqwNpetd.js +1 -0
  253. package/dist/client/assets/smalltalk-BERRCDM3.js +1 -0
  254. package/dist/client/assets/snazzy-light-Bw305WKR.js +1 -0
  255. package/dist/client/assets/solarized-dark-DXbdFlpD.js +1 -0
  256. package/dist/client/assets/solarized-light-L9t79GZl.js +1 -0
  257. package/dist/client/assets/solidity-rGO070M0.js +1 -0
  258. package/dist/client/assets/soy-Brmx7dQM.js +1 -0
  259. package/dist/client/assets/sparql-rVzFXLq3.js +1 -0
  260. package/dist/client/assets/splunk-BtCnVYZw.js +1 -0
  261. package/dist/client/assets/sql-BLtJtn59.js +1 -0
  262. package/dist/client/assets/ssh-config-_ykCGR6B.js +1 -0
  263. package/dist/client/assets/stata-BH5u7GGu.js +1 -0
  264. package/dist/client/assets/stylus-BEDo0Tqx.js +1 -0
  265. package/dist/client/assets/surrealql-Bq5Q-fJD.js +1 -0
  266. package/dist/client/assets/svelte-C_ipcX3V.js +1 -0
  267. package/dist/client/assets/swift-D82vCrfD.js +1 -0
  268. package/dist/client/assets/synthwave-84-CbfX1IO0.js +1 -0
  269. package/dist/client/assets/system-verilog-CnnmHF94.js +1 -0
  270. package/dist/client/assets/systemd-4A_iFExJ.js +1 -0
  271. package/dist/client/assets/talonscript-CkByrt1z.js +1 -0
  272. package/dist/client/assets/tasl-QIJgUcNo.js +1 -0
  273. package/dist/client/assets/tcl-dwOrl1Do.js +1 -0
  274. package/dist/client/assets/templ-P3uqSqPl.js +1 -0
  275. package/dist/client/assets/terraform-BETggiCN.js +1 -0
  276. package/dist/client/assets/tex-idrVyKtj.js +1 -0
  277. package/dist/client/assets/tokyo-night-hegEt444.js +1 -0
  278. package/dist/client/assets/toml-vGWfd6FD.js +1 -0
  279. package/dist/client/assets/ts-tags-zn1MmPIZ.js +1 -0
  280. package/dist/client/assets/tsv-B_m7g4N7.js +1 -0
  281. package/dist/client/assets/tsx-COt5Ahok.js +1 -0
  282. package/dist/client/assets/turtle-BsS91CYL.js +1 -0
  283. package/dist/client/assets/twig-DNn4PbVi.js +1 -0
  284. package/dist/client/assets/typescript-BPQ3VLAy.js +1 -0
  285. package/dist/client/assets/typespec-BGHnOYBU.js +1 -0
  286. package/dist/client/assets/typst-DHCkPAjA.js +1 -0
  287. package/dist/client/assets/v-BcVCzyr7.js +1 -0
  288. package/dist/client/assets/vala-CsfeWuGM.js +1 -0
  289. package/dist/client/assets/vb-D17OF-Vu.js +1 -0
  290. package/dist/client/assets/verilog-BQ8w6xss.js +1 -0
  291. package/dist/client/assets/vesper-DU1UobuO.js +1 -0
  292. package/dist/client/assets/vhdl-CeAyd5Ju.js +1 -0
  293. package/dist/client/assets/viml-CJc9bBzg.js +1 -0
  294. package/dist/client/assets/vitesse-black-Bkuqu6BP.js +1 -0
  295. package/dist/client/assets/vitesse-dark-D0r3Knsf.js +1 -0
  296. package/dist/client/assets/vitesse-light-CVO1_9PV.js +1 -0
  297. package/dist/client/assets/vue-DN_0RTcg.js +1 -0
  298. package/dist/client/assets/vue-html-AaS7Mt5G.js +1 -0
  299. package/dist/client/assets/vue-vine-CQOfvN7w.js +1 -0
  300. package/dist/client/assets/vyper-CDx5xZoG.js +1 -0
  301. package/dist/client/assets/wasm-CG6Dc4jp.js +1 -0
  302. package/dist/client/assets/wasm-MzD3tlZU.js +1 -0
  303. package/dist/client/assets/wenyan-BV7otONQ.js +1 -0
  304. package/dist/client/assets/wgsl-Dx-B1_4e.js +1 -0
  305. package/dist/client/assets/wikitext-BhOHFoWU.js +1 -0
  306. package/dist/client/assets/wit-5i3qLPDT.js +1 -0
  307. package/dist/client/assets/wolfram-lXgVvXCa.js +1 -0
  308. package/dist/client/assets/xml-sdJ4AIDG.js +1 -0
  309. package/dist/client/assets/xsl-CtQFsRM5.js +1 -0
  310. package/dist/client/assets/yaml-Buea-lGh.js +1 -0
  311. package/dist/client/assets/zenscript-DVFEvuxE.js +1 -0
  312. package/dist/client/assets/zig-VOosw3JB.js +1 -0
  313. package/dist/client/chat-sounds/Blow.mp3 +0 -0
  314. package/dist/client/chat-sounds/Bottle.mp3 +0 -0
  315. package/dist/client/chat-sounds/Frog.mp3 +0 -0
  316. package/dist/client/chat-sounds/Funk.mp3 +0 -0
  317. package/dist/client/chat-sounds/Glass.mp3 +0 -0
  318. package/dist/client/chat-sounds/Ping.mp3 +0 -0
  319. package/dist/client/chat-sounds/Pop.mp3 +0 -0
  320. package/dist/client/chat-sounds/Purr.mp3 +0 -0
  321. package/dist/client/chat-sounds/Tink.mp3 +0 -0
  322. package/dist/client/editor-icons/cursor.png +0 -0
  323. package/dist/client/editor-icons/custom.png +0 -0
  324. package/dist/client/editor-icons/default-app.png +0 -0
  325. package/dist/client/editor-icons/finder.png +0 -0
  326. package/dist/client/editor-icons/preview.png +0 -0
  327. package/dist/client/editor-icons/terminal.png +0 -0
  328. package/dist/client/editor-icons/windsurf.png +0 -0
  329. package/dist/client/editor-icons/xcode.png +0 -0
  330. package/dist/client/favicon.png +0 -0
  331. package/dist/client/fonts/body-medium.woff2 +0 -0
  332. package/dist/client/fonts/body-regular-italic.woff2 +0 -0
  333. package/dist/client/fonts/body-regular.woff2 +0 -0
  334. package/dist/client/fonts/body-semibold.woff2 +0 -0
  335. package/dist/client/icon-192.png +0 -0
  336. package/dist/client/icon-512.png +0 -0
  337. package/dist/client/icon-maskable-512.png +0 -0
  338. package/dist/client/icon.svg +4 -0
  339. package/dist/client/index.html +34 -0
  340. package/dist/client/manifest.webmanifest +46 -0
  341. package/dist/client/screenshot-light.png +0 -0
  342. package/dist/client/screenshot.png +0 -0
  343. package/dist/export-viewer/assets/bricolage-grotesque-latin-ext-wght-normal-CcLUaPy7.woff2 +0 -0
  344. package/dist/export-viewer/assets/bricolage-grotesque-latin-wght-normal-DLoelf7F.woff2 +0 -0
  345. package/dist/export-viewer/assets/bricolage-grotesque-vietnamese-wght-normal-BUzh504Q.woff2 +0 -0
  346. package/dist/export-viewer/assets/index-D1qUumZR.js +410 -0
  347. package/dist/export-viewer/assets/index-gG2nMW51.css +1 -0
  348. package/dist/export-viewer/editor-icons/cursor.png +0 -0
  349. package/dist/export-viewer/editor-icons/custom.png +0 -0
  350. package/dist/export-viewer/editor-icons/default-app.png +0 -0
  351. package/dist/export-viewer/editor-icons/finder.png +0 -0
  352. package/dist/export-viewer/editor-icons/preview.png +0 -0
  353. package/dist/export-viewer/editor-icons/terminal.png +0 -0
  354. package/dist/export-viewer/editor-icons/windsurf.png +0 -0
  355. package/dist/export-viewer/editor-icons/xcode.png +0 -0
  356. package/dist/export-viewer/fonts/body-medium.woff2 +0 -0
  357. package/dist/export-viewer/fonts/body-regular-italic.woff2 +0 -0
  358. package/dist/export-viewer/fonts/body-regular.woff2 +0 -0
  359. package/dist/export-viewer/fonts/body-semibold.woff2 +0 -0
  360. package/dist/export-viewer/index.html +14 -0
  361. package/package.json +99 -0
  362. package/src/server/__fixtures__/claude-session-empty.jsonl +0 -0
  363. package/src/server/__fixtures__/claude-session-malformed.jsonl +3 -0
  364. package/src/server/__fixtures__/claude-session-valid.jsonl +6 -0
  365. package/src/server/agent.test.ts +2369 -0
  366. package/src/server/agent.ts +1927 -0
  367. package/src/server/analytics.test.ts +313 -0
  368. package/src/server/analytics.ts +131 -0
  369. package/src/server/app-settings.test.ts +233 -0
  370. package/src/server/app-settings.ts +548 -0
  371. package/src/server/auth.test.ts +329 -0
  372. package/src/server/auth.ts +204 -0
  373. package/src/server/auto-continue/e2e.test.ts +215 -0
  374. package/src/server/auto-continue/events.test.ts +30 -0
  375. package/src/server/auto-continue/events.ts +35 -0
  376. package/src/server/auto-continue/limit-detector.test.ts +153 -0
  377. package/src/server/auto-continue/limit-detector.ts +159 -0
  378. package/src/server/auto-continue/read-model.test.ts +109 -0
  379. package/src/server/auto-continue/read-model.ts +83 -0
  380. package/src/server/auto-continue/schedule-manager.test.ts +155 -0
  381. package/src/server/auto-continue/schedule-manager.ts +116 -0
  382. package/src/server/claude-session-importer.test.ts +214 -0
  383. package/src/server/claude-session-importer.ts +187 -0
  384. package/src/server/claude-session-mapper.test.ts +88 -0
  385. package/src/server/claude-session-mapper.ts +106 -0
  386. package/src/server/claude-session-parser.test.ts +38 -0
  387. package/src/server/claude-session-parser.ts +67 -0
  388. package/src/server/claude-session-scanner.test.ts +49 -0
  389. package/src/server/claude-session-scanner.ts +24 -0
  390. package/src/server/claude-session-types.ts +61 -0
  391. package/src/server/cli-runtime.test.ts +523 -0
  392. package/src/server/cli-runtime.ts +405 -0
  393. package/src/server/cli-supervisor.ts +102 -0
  394. package/src/server/cli.ts +64 -0
  395. package/src/server/cloudflare-tunnel/agent-integration.test.ts +76 -0
  396. package/src/server/cloudflare-tunnel/agent-integration.ts +55 -0
  397. package/src/server/cloudflare-tunnel/detector.test.ts +72 -0
  398. package/src/server/cloudflare-tunnel/detector.ts +44 -0
  399. package/src/server/cloudflare-tunnel/e2e.test.ts +194 -0
  400. package/src/server/cloudflare-tunnel/events.test.ts +43 -0
  401. package/src/server/cloudflare-tunnel/events.ts +31 -0
  402. package/src/server/cloudflare-tunnel/gateway.ts +143 -0
  403. package/src/server/cloudflare-tunnel/lifecycle.test.ts +48 -0
  404. package/src/server/cloudflare-tunnel/lifecycle.ts +62 -0
  405. package/src/server/cloudflare-tunnel/read-model.test.ts +69 -0
  406. package/src/server/cloudflare-tunnel/read-model.ts +80 -0
  407. package/src/server/cloudflare-tunnel/tunnel-manager.test.ts +116 -0
  408. package/src/server/cloudflare-tunnel/tunnel-manager.ts +165 -0
  409. package/src/server/codex-app-server-protocol.ts +487 -0
  410. package/src/server/codex-app-server.test.ts +1816 -0
  411. package/src/server/codex-app-server.ts +1475 -0
  412. package/src/server/diff-store.test.ts +737 -0
  413. package/src/server/diff-store.ts +2199 -0
  414. package/src/server/discovery.test.ts +211 -0
  415. package/src/server/discovery.ts +301 -0
  416. package/src/server/event-store.test.ts +797 -0
  417. package/src/server/event-store.ts +1421 -0
  418. package/src/server/events.ts +217 -0
  419. package/src/server/external-open.test.ts +112 -0
  420. package/src/server/external-open.ts +345 -0
  421. package/src/server/generate-commit-message.test.ts +79 -0
  422. package/src/server/generate-commit-message.ts +126 -0
  423. package/src/server/generate-title.ts +76 -0
  424. package/src/server/harness-types.ts +19 -0
  425. package/src/server/keybindings.test.ts +144 -0
  426. package/src/server/keybindings.ts +178 -0
  427. package/src/server/llm-provider.test.ts +134 -0
  428. package/src/server/llm-provider.ts +207 -0
  429. package/src/server/machine-name.ts +22 -0
  430. package/src/server/paths-route.test.ts +64 -0
  431. package/src/server/paths.ts +35 -0
  432. package/src/server/process-utils.test.ts +12 -0
  433. package/src/server/process-utils.ts +47 -0
  434. package/src/server/project-paths.test.ts +95 -0
  435. package/src/server/project-paths.ts +191 -0
  436. package/src/server/provider-catalog.test.ts +69 -0
  437. package/src/server/provider-catalog.ts +87 -0
  438. package/src/server/quick-response.test.ts +440 -0
  439. package/src/server/quick-response.ts +300 -0
  440. package/src/server/read-models.test.ts +509 -0
  441. package/src/server/read-models.ts +230 -0
  442. package/src/server/restart.test.ts +27 -0
  443. package/src/server/restart.ts +30 -0
  444. package/src/server/server.ts +616 -0
  445. package/src/server/share.test.ts +180 -0
  446. package/src/server/share.ts +150 -0
  447. package/src/server/standalone-export.test.ts +224 -0
  448. package/src/server/standalone-export.ts +419 -0
  449. package/src/server/terminal-manager.test.ts +315 -0
  450. package/src/server/terminal-manager.ts +350 -0
  451. package/src/server/test-helpers/async-event-queue.ts +52 -0
  452. package/src/server/test-helpers/wait-for.ts +14 -0
  453. package/src/server/title-generation.live.test.ts +44 -0
  454. package/src/server/update-manager.test.ts +158 -0
  455. package/src/server/update-manager.ts +222 -0
  456. package/src/server/update-strategy.test.ts +237 -0
  457. package/src/server/update-strategy.ts +241 -0
  458. package/src/server/uploads.test.ts +292 -0
  459. package/src/server/uploads.ts +131 -0
  460. package/src/server/ws-router.test.ts +2292 -0
  461. package/src/server/ws-router.ts +1465 -0
  462. package/src/shared/analytics.ts +30 -0
  463. package/src/shared/branding.test.ts +31 -0
  464. package/src/shared/branding.ts +77 -0
  465. package/src/shared/dev-ports.test.ts +113 -0
  466. package/src/shared/dev-ports.ts +134 -0
  467. package/src/shared/ports.ts +2 -0
  468. package/src/shared/protocol.ts +257 -0
  469. package/src/shared/share.ts +27 -0
  470. package/src/shared/tools.test.ts +164 -0
  471. package/src/shared/tools.ts +327 -0
  472. package/src/shared/types.test.ts +25 -0
  473. package/src/shared/types.ts +1088 -0
@@ -0,0 +1,1816 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { EventEmitter } from "node:events"
3
+ import { PassThrough } from "node:stream"
4
+ import { CodexAppServerManager } from "./codex-app-server"
5
+
6
+ class FakeCodexProcess extends EventEmitter {
7
+ readonly stdin = new PassThrough()
8
+ readonly stdout = new PassThrough()
9
+ readonly stderr = new PassThrough()
10
+ readonly messages: unknown[] = []
11
+ killed = false
12
+
13
+ constructor(
14
+ private readonly onMessage?: (message: any, process: FakeCodexProcess) => void
15
+ ) {
16
+ super()
17
+ let buffer = ""
18
+ this.stdin.on("data", (chunk) => {
19
+ buffer += chunk.toString()
20
+ const lines = buffer.split("\n")
21
+ buffer = lines.pop() ?? ""
22
+ for (const line of lines) {
23
+ if (!line.trim()) continue
24
+ const message = JSON.parse(line)
25
+ this.messages.push(message)
26
+ this.onMessage?.(message, this)
27
+ }
28
+ })
29
+ }
30
+
31
+ kill() {
32
+ this.killed = true
33
+ this.emit("close", 0)
34
+ }
35
+
36
+ writeServerMessage(message: unknown) {
37
+ this.stdout.write(`${JSON.stringify(message)}\n`)
38
+ }
39
+
40
+ writeStderr(message: string) {
41
+ this.stderr.write(`${message}\n`)
42
+ }
43
+
44
+ closeWithCode(code: number) {
45
+ this.emit("close", code)
46
+ }
47
+ }
48
+
49
+ async function collectStream(stream: AsyncIterable<any>) {
50
+ const items: any[] = []
51
+ for await (const item of stream) {
52
+ items.push(item)
53
+ }
54
+ return items
55
+ }
56
+
57
+ describe("CodexAppServerManager", () => {
58
+ test("initializes app-server and starts a fresh thread", async () => {
59
+ const process = new FakeCodexProcess((message, child) => {
60
+ if (message.method === "initialize") {
61
+ child.writeServerMessage({ id: message.id, result: { userAgent: "codex-test" } })
62
+ } else if (message.method === "thread/start") {
63
+ child.writeServerMessage({
64
+ id: message.id,
65
+ result: { thread: { id: "thread-1" }, model: "gpt-5.4", reasoningEffort: "high" },
66
+ })
67
+ }
68
+ })
69
+
70
+ const manager = new CodexAppServerManager({
71
+ spawnProcess: () => process as never,
72
+ })
73
+
74
+ await manager.startSession({
75
+ chatId: "chat-1",
76
+ cwd: "/tmp/project",
77
+ model: "gpt-5.4",
78
+ sessionToken: null,
79
+ })
80
+
81
+ expect(process.messages).toHaveLength(3)
82
+ expect((process.messages[0] as any).method).toBe("initialize")
83
+ expect((process.messages[1] as any).method).toBe("initialized")
84
+ expect((process.messages[2] as any).method).toBe("thread/start")
85
+ })
86
+
87
+ test("falls back to thread/start when thread/resume is recoverably missing", async () => {
88
+ const process = new FakeCodexProcess((message, child) => {
89
+ if (message.method === "initialize") {
90
+ child.writeServerMessage({ id: message.id, result: { userAgent: "codex-test" } })
91
+ } else if (message.method === "thread/resume") {
92
+ child.writeServerMessage({
93
+ id: message.id,
94
+ error: { message: "thread/resume failed: thread not found" },
95
+ })
96
+ } else if (message.method === "thread/start") {
97
+ child.writeServerMessage({
98
+ id: message.id,
99
+ result: { thread: { id: "thread-2" }, model: "gpt-5.4", reasoningEffort: "high" },
100
+ })
101
+ }
102
+ })
103
+
104
+ const manager = new CodexAppServerManager({
105
+ spawnProcess: () => process as never,
106
+ })
107
+
108
+ await manager.startSession({
109
+ chatId: "chat-1",
110
+ cwd: "/tmp/project",
111
+ model: "gpt-5.4",
112
+ sessionToken: "missing-thread",
113
+ })
114
+
115
+ expect(process.messages.map((message: any) => message.method)).toEqual([
116
+ "initialize",
117
+ "initialized",
118
+ "thread/resume",
119
+ "thread/start",
120
+ ])
121
+ })
122
+
123
+ test("forks a thread when a pending fork session token is provided", async () => {
124
+ const process = new FakeCodexProcess((message, child) => {
125
+ if (message.method === "initialize") {
126
+ child.writeServerMessage({ id: message.id, result: { userAgent: "codex-test" } })
127
+ } else if (message.method === "thread/fork") {
128
+ child.writeServerMessage({
129
+ id: message.id,
130
+ result: { thread: { id: "thread-fork-1" }, model: "gpt-5.4", reasoningEffort: "high" },
131
+ })
132
+ }
133
+ })
134
+
135
+ const manager = new CodexAppServerManager({
136
+ spawnProcess: () => process as never,
137
+ })
138
+
139
+ const sessionToken = await manager.startSession({
140
+ chatId: "chat-1",
141
+ cwd: "/tmp/project",
142
+ model: "gpt-5.4",
143
+ sessionToken: null,
144
+ pendingForkSessionToken: "thread-source",
145
+ })
146
+
147
+ expect(sessionToken).toBe("thread-fork-1")
148
+ expect(process.messages.map((message: any) => message.method)).toEqual([
149
+ "initialize",
150
+ "initialized",
151
+ "thread/fork",
152
+ ])
153
+ })
154
+
155
+ test("maps fast mode and reasoning into app-server params", async () => {
156
+ const process = new FakeCodexProcess((message, child) => {
157
+ if (message.method === "initialize") {
158
+ child.writeServerMessage({ id: message.id, result: { userAgent: "codex-test" } })
159
+ } else if (message.method === "thread/start") {
160
+ child.writeServerMessage({
161
+ id: message.id,
162
+ result: { thread: { id: "thread-1" }, model: "gpt-5.4", reasoningEffort: "high" },
163
+ })
164
+ } else if (message.method === "turn/start") {
165
+ child.writeServerMessage({
166
+ id: message.id,
167
+ result: { turn: { id: "turn-1", status: "completed", error: null } },
168
+ })
169
+ child.writeServerMessage({
170
+ method: "turn/completed",
171
+ params: {
172
+ threadId: "thread-1",
173
+ turn: { id: "turn-1", status: "completed", error: null },
174
+ },
175
+ })
176
+ }
177
+ })
178
+
179
+ const manager = new CodexAppServerManager({
180
+ spawnProcess: () => process as never,
181
+ })
182
+
183
+ await manager.startSession({
184
+ chatId: "chat-1",
185
+ cwd: "/tmp/project",
186
+ model: "gpt-5.4",
187
+ serviceTier: "fast",
188
+ sessionToken: null,
189
+ })
190
+
191
+ const turn = await manager.startTurn({
192
+ chatId: "chat-1",
193
+ model: "gpt-5.4",
194
+ effort: "xhigh",
195
+ serviceTier: "fast",
196
+ content: "Run pwd",
197
+ planMode: false,
198
+ onToolRequest: async () => ({}),
199
+ })
200
+
201
+ await collectStream(turn.stream)
202
+
203
+ const threadStart = process.messages.find((message: any) => message.method === "thread/start") as
204
+ | { method: "thread/start"; params: { serviceTier?: string } }
205
+ | undefined
206
+ const turnStart = process.messages.find((message: any) => message.method === "turn/start") as
207
+ | { method: "turn/start"; params: { effort?: string; serviceTier?: string; collaborationMode?: { settings?: { reasoning_effort?: string | null } } } }
208
+ | undefined
209
+
210
+ expect(threadStart?.params.serviceTier).toBe("fast")
211
+ expect(turnStart?.params.effort).toBe("xhigh")
212
+ expect(turnStart?.params.serviceTier).toBe("fast")
213
+ expect(turnStart?.params.collaborationMode?.settings?.reasoning_effort).toBeNull()
214
+ })
215
+
216
+ test("maps thread token usage updates into context window transcript entries", async () => {
217
+ const process = new FakeCodexProcess((message, child) => {
218
+ if (message.method === "initialize") {
219
+ child.writeServerMessage({ id: message.id, result: { userAgent: "codex-test" } })
220
+ } else if (message.method === "thread/start") {
221
+ child.writeServerMessage({
222
+ id: message.id,
223
+ result: { thread: { id: "thread-usage" }, model: "gpt-5.4", reasoningEffort: "high" },
224
+ })
225
+ } else if (message.method === "turn/start") {
226
+ child.writeServerMessage({
227
+ id: message.id,
228
+ result: { turn: { id: "turn-usage", status: "completed", error: null } },
229
+ })
230
+ child.writeServerMessage({
231
+ method: "thread/tokenUsage/updated",
232
+ params: {
233
+ threadId: "thread-usage",
234
+ turnId: "turn-usage",
235
+ tokenUsage: {
236
+ total: {
237
+ inputTokens: 11_833,
238
+ cachedInputTokens: 3456,
239
+ outputTokens: 6,
240
+ reasoningOutputTokens: 0,
241
+ totalTokens: 11_839,
242
+ },
243
+ last: {
244
+ inputTokens: 120,
245
+ cachedInputTokens: 0,
246
+ outputTokens: 6,
247
+ reasoningOutputTokens: 0,
248
+ totalTokens: 126,
249
+ },
250
+ modelContextWindow: 258_400,
251
+ },
252
+ },
253
+ })
254
+ child.writeServerMessage({
255
+ method: "turn/completed",
256
+ params: {
257
+ threadId: "thread-usage",
258
+ turn: { id: "turn-usage", status: "completed", error: null },
259
+ },
260
+ })
261
+ }
262
+ })
263
+
264
+ const manager = new CodexAppServerManager({
265
+ spawnProcess: () => process as never,
266
+ })
267
+
268
+ await manager.startSession({
269
+ chatId: "chat-1",
270
+ cwd: "/tmp/project",
271
+ model: "gpt-5.4",
272
+ sessionToken: null,
273
+ })
274
+
275
+ const turn = await manager.startTurn({
276
+ chatId: "chat-1",
277
+ model: "gpt-5.4",
278
+ content: "Hello",
279
+ planMode: false,
280
+ onToolRequest: async () => ({}),
281
+ })
282
+
283
+ const events = await collectStream(turn.stream)
284
+ const usageEvent = events.find((event) => event.type === "transcript" && event.entry.kind === "context_window_updated")
285
+
286
+ expect(usageEvent).toBeDefined()
287
+ if (!usageEvent || usageEvent.type !== "transcript" || usageEvent.entry.kind !== "context_window_updated") {
288
+ throw new Error("missing usage event")
289
+ }
290
+
291
+ expect(usageEvent.entry.usage).toMatchObject({
292
+ usedTokens: 126,
293
+ totalProcessedTokens: 11_839,
294
+ maxTokens: 258_400,
295
+ inputTokens: 120,
296
+ cachedInputTokens: 0,
297
+ outputTokens: 6,
298
+ reasoningOutputTokens: 0,
299
+ lastUsedTokens: 126,
300
+ compactsAutomatically: true,
301
+ })
302
+ })
303
+
304
+ test("generateStructured returns the final assistant JSON and stops the transient session", async () => {
305
+ const process = new FakeCodexProcess((message, child) => {
306
+ if (message.method === "initialize") {
307
+ child.writeServerMessage({ id: message.id, result: { userAgent: "codex-test" } })
308
+ } else if (message.method === "thread/start") {
309
+ child.writeServerMessage({
310
+ id: message.id,
311
+ result: { thread: { id: "thread-structured" }, model: "gpt-5.5", reasoningEffort: "high" },
312
+ })
313
+ } else if (message.method === "turn/start") {
314
+ child.writeServerMessage({
315
+ id: message.id,
316
+ result: { turn: { id: "turn-structured", status: "completed", error: null } },
317
+ })
318
+ child.writeServerMessage({
319
+ method: "item/completed",
320
+ params: {
321
+ threadId: "thread-structured",
322
+ turnId: "turn-structured",
323
+ item: {
324
+ type: "agentMessage",
325
+ id: "msg-structured",
326
+ text: "{\"title\":\"Codex title\"}",
327
+ phase: "final_answer",
328
+ },
329
+ },
330
+ })
331
+ child.writeServerMessage({
332
+ method: "turn/completed",
333
+ params: {
334
+ threadId: "thread-structured",
335
+ turn: { id: "turn-structured", status: "completed", error: null },
336
+ },
337
+ })
338
+ }
339
+ })
340
+
341
+ const manager = new CodexAppServerManager({
342
+ spawnProcess: () => process as never,
343
+ })
344
+
345
+ const result = await manager.generateStructured({
346
+ cwd: "/tmp/project",
347
+ prompt: "Return JSON",
348
+ })
349
+
350
+ expect(result).toBe("{\"title\":\"Codex title\"}")
351
+ expect(process.killed).toBe(true)
352
+ expect((process.messages.find((message: any) => message.method === "thread/start") as any)?.params.model).toBe("gpt-5.5")
353
+ expect((process.messages.find((message: any) => message.method === "turn/start") as any)?.params.model).toBe("gpt-5.5")
354
+ })
355
+
356
+ test("maps command execution and agent output into the shared transcript stream", async () => {
357
+ const process = new FakeCodexProcess((message, child) => {
358
+ if (message.method === "initialize") {
359
+ child.writeServerMessage({ id: message.id, result: { userAgent: "codex-test" } })
360
+ } else if (message.method === "thread/start") {
361
+ child.writeServerMessage({
362
+ id: message.id,
363
+ result: { thread: { id: "thread-1" }, model: "gpt-5.4", reasoningEffort: "high" },
364
+ })
365
+ } else if (message.method === "turn/start") {
366
+ child.writeServerMessage({
367
+ id: message.id,
368
+ result: { turn: { id: "turn-1", status: "inProgress", error: null } },
369
+ })
370
+ child.writeServerMessage({
371
+ method: "item/started",
372
+ params: {
373
+ threadId: "thread-1",
374
+ turnId: "turn-1",
375
+ item: {
376
+ type: "commandExecution",
377
+ id: "call-1",
378
+ command: "/bin/zsh -lc pwd",
379
+ status: "inProgress",
380
+ },
381
+ },
382
+ })
383
+ child.writeServerMessage({
384
+ method: "item/completed",
385
+ params: {
386
+ threadId: "thread-1",
387
+ turnId: "turn-1",
388
+ item: {
389
+ type: "commandExecution",
390
+ id: "call-1",
391
+ command: "/bin/zsh -lc pwd",
392
+ status: "completed",
393
+ aggregatedOutput: "/tmp/project\n",
394
+ exitCode: 0,
395
+ },
396
+ },
397
+ })
398
+ child.writeServerMessage({
399
+ method: "item/completed",
400
+ params: {
401
+ threadId: "thread-1",
402
+ turnId: "turn-1",
403
+ item: {
404
+ type: "agentMessage",
405
+ id: "msg-1",
406
+ text: "/tmp/project",
407
+ phase: "final_answer",
408
+ },
409
+ },
410
+ })
411
+ child.writeServerMessage({
412
+ method: "turn/completed",
413
+ params: {
414
+ threadId: "thread-1",
415
+ turn: { id: "turn-1", status: "completed", error: null },
416
+ },
417
+ })
418
+ }
419
+ })
420
+
421
+ const manager = new CodexAppServerManager({
422
+ spawnProcess: () => process as never,
423
+ })
424
+
425
+ await manager.startSession({
426
+ chatId: "chat-1",
427
+ cwd: "/tmp/project",
428
+ model: "gpt-5.4",
429
+ sessionToken: null,
430
+ })
431
+
432
+ const turn = await manager.startTurn({
433
+ chatId: "chat-1",
434
+ model: "gpt-5.4",
435
+ content: "Run pwd",
436
+ planMode: false,
437
+ onToolRequest: async () => ({}),
438
+ })
439
+
440
+ const events = await collectStream(turn.stream)
441
+ const transcriptKinds = events
442
+ .filter((event) => event.type === "transcript")
443
+ .map((event) => event.entry.kind)
444
+
445
+ expect(events[0]).toEqual({ type: "session_token", sessionToken: "thread-1" })
446
+ expect(transcriptKinds).toEqual(["system_init", "tool_call", "tool_result", "assistant_text", "result"])
447
+ })
448
+
449
+ test("emits only a compact boundary when Codex reports thread compaction", async () => {
450
+ const process = new FakeCodexProcess((message, child) => {
451
+ if (message.method === "initialize") {
452
+ child.writeServerMessage({ id: message.id, result: { userAgent: "codex-test" } })
453
+ } else if (message.method === "thread/start") {
454
+ child.writeServerMessage({
455
+ id: message.id,
456
+ result: { thread: { id: "thread-1" }, model: "gpt-5.4", reasoningEffort: "high" },
457
+ })
458
+ } else if (message.method === "turn/start") {
459
+ child.writeServerMessage({
460
+ id: message.id,
461
+ result: { turn: { id: "turn-1", status: "inProgress", error: null } },
462
+ })
463
+ child.writeServerMessage({
464
+ method: "thread/compacted",
465
+ params: {
466
+ threadId: "thread-1",
467
+ turnId: "turn-1",
468
+ },
469
+ })
470
+ child.writeServerMessage({
471
+ method: "turn/completed",
472
+ params: {
473
+ threadId: "thread-1",
474
+ turn: { id: "turn-1", status: "completed", error: null },
475
+ },
476
+ })
477
+ }
478
+ })
479
+
480
+ const manager = new CodexAppServerManager({
481
+ spawnProcess: () => process as never,
482
+ })
483
+
484
+ await manager.startSession({
485
+ chatId: "chat-1",
486
+ cwd: "/tmp/project",
487
+ model: "gpt-5.4",
488
+ sessionToken: null,
489
+ })
490
+
491
+ const turn = await manager.startTurn({
492
+ chatId: "chat-1",
493
+ model: "gpt-5.4",
494
+ content: "/compact",
495
+ planMode: false,
496
+ onToolRequest: async () => ({}),
497
+ })
498
+
499
+ const events = await collectStream(turn.stream)
500
+ const transcriptKinds = events
501
+ .filter((event) => event.type === "transcript")
502
+ .map((event) => event.entry.kind)
503
+
504
+ expect(transcriptKinds).toEqual(["system_init", "compact_boundary", "result"])
505
+ expect(transcriptKinds).not.toContain("context_cleared")
506
+ })
507
+
508
+ test("maps fileChange updates into edit_file tool calls", async () => {
509
+ const process = new FakeCodexProcess((message, child) => {
510
+ if (message.method === "initialize") {
511
+ child.writeServerMessage({ id: message.id, result: { userAgent: "codex-test" } })
512
+ } else if (message.method === "thread/start") {
513
+ child.writeServerMessage({
514
+ id: message.id,
515
+ result: { thread: { id: "thread-1" }, model: "gpt-5.4", reasoningEffort: "high" },
516
+ })
517
+ } else if (message.method === "turn/start") {
518
+ child.writeServerMessage({
519
+ id: message.id,
520
+ result: { turn: { id: "turn-1", status: "inProgress", error: null } },
521
+ })
522
+ child.writeServerMessage({
523
+ method: "item/started",
524
+ params: {
525
+ threadId: "thread-1",
526
+ turnId: "turn-1",
527
+ item: {
528
+ type: "fileChange",
529
+ id: "call-1",
530
+ changes: [
531
+ {
532
+ path: "/tmp/project/test.md",
533
+ kind: {
534
+ type: "update",
535
+ move_path: null,
536
+ },
537
+ diff: "@@ -1,2 +1,2 @@\n-old line\n+new line",
538
+ },
539
+ ],
540
+ status: "inProgress",
541
+ },
542
+ },
543
+ })
544
+ child.writeServerMessage({
545
+ method: "item/completed",
546
+ params: {
547
+ threadId: "thread-1",
548
+ turnId: "turn-1",
549
+ item: {
550
+ type: "fileChange",
551
+ id: "call-1",
552
+ changes: [
553
+ {
554
+ path: "/tmp/project/test.md",
555
+ kind: {
556
+ type: "update",
557
+ move_path: null,
558
+ },
559
+ diff: "@@ -1,2 +1,2 @@\n-old line\n+new line",
560
+ },
561
+ ],
562
+ status: "completed",
563
+ },
564
+ },
565
+ })
566
+ child.writeServerMessage({
567
+ method: "turn/completed",
568
+ params: {
569
+ threadId: "thread-1",
570
+ turn: { id: "turn-1", status: "completed", error: null },
571
+ },
572
+ })
573
+ }
574
+ })
575
+
576
+ const manager = new CodexAppServerManager({
577
+ spawnProcess: () => process as never,
578
+ })
579
+
580
+ await manager.startSession({
581
+ chatId: "chat-1",
582
+ cwd: "/tmp/project",
583
+ model: "gpt-5.4",
584
+ sessionToken: null,
585
+ })
586
+
587
+ const turn = await manager.startTurn({
588
+ chatId: "chat-1",
589
+ model: "gpt-5.4",
590
+ content: "edit a file",
591
+ planMode: false,
592
+ onToolRequest: async () => ({}),
593
+ })
594
+
595
+ const events = await collectStream(turn.stream)
596
+ const toolCall = events.find((event) => event.type === "transcript" && event.entry.kind === "tool_call")
597
+
598
+ expect(toolCall?.entry.kind).toBe("tool_call")
599
+ if (!toolCall || toolCall.entry.kind !== "tool_call") throw new Error("missing tool call")
600
+ expect(toolCall.entry.tool.toolKind).toBe("edit_file")
601
+ expect(toolCall.entry.tool.toolName).toBe("Edit")
602
+ expect(toolCall.entry.tool.input).toEqual({
603
+ filePath: "/tmp/project/test.md",
604
+ oldString: "old line",
605
+ newString: "new line",
606
+ })
607
+ })
608
+
609
+ test("maps fileChange adds into write_file tool calls", async () => {
610
+ const process = new FakeCodexProcess((message, child) => {
611
+ if (message.method === "initialize") {
612
+ child.writeServerMessage({ id: message.id, result: { userAgent: "codex-test" } })
613
+ } else if (message.method === "thread/start") {
614
+ child.writeServerMessage({
615
+ id: message.id,
616
+ result: { thread: { id: "thread-1" }, model: "gpt-5.4", reasoningEffort: "high" },
617
+ })
618
+ } else if (message.method === "turn/start") {
619
+ child.writeServerMessage({
620
+ id: message.id,
621
+ result: { turn: { id: "turn-1", status: "inProgress", error: null } },
622
+ })
623
+ child.writeServerMessage({
624
+ method: "item/started",
625
+ params: {
626
+ threadId: "thread-1",
627
+ turnId: "turn-1",
628
+ item: {
629
+ type: "fileChange",
630
+ id: "call-1",
631
+ changes: [
632
+ {
633
+ path: "/tmp/project/test.md",
634
+ kind: {
635
+ type: "add",
636
+ move_path: null,
637
+ },
638
+ diff: "@@ -0,0 +1,2 @@\n+hello\n+world",
639
+ },
640
+ ],
641
+ status: "inProgress",
642
+ },
643
+ },
644
+ })
645
+ child.writeServerMessage({
646
+ method: "item/completed",
647
+ params: {
648
+ threadId: "thread-1",
649
+ turnId: "turn-1",
650
+ item: {
651
+ type: "fileChange",
652
+ id: "call-1",
653
+ changes: [
654
+ {
655
+ path: "/tmp/project/test.md",
656
+ kind: {
657
+ type: "add",
658
+ move_path: null,
659
+ },
660
+ diff: "@@ -0,0 +1,2 @@\n+hello\n+world",
661
+ },
662
+ ],
663
+ status: "completed",
664
+ },
665
+ },
666
+ })
667
+ child.writeServerMessage({
668
+ method: "turn/completed",
669
+ params: {
670
+ threadId: "thread-1",
671
+ turn: { id: "turn-1", status: "completed", error: null },
672
+ },
673
+ })
674
+ }
675
+ })
676
+
677
+ const manager = new CodexAppServerManager({
678
+ spawnProcess: () => process as never,
679
+ })
680
+
681
+ await manager.startSession({
682
+ chatId: "chat-1",
683
+ cwd: "/tmp/project",
684
+ model: "gpt-5.4",
685
+ sessionToken: null,
686
+ })
687
+
688
+ const turn = await manager.startTurn({
689
+ chatId: "chat-1",
690
+ model: "gpt-5.4",
691
+ content: "write a file",
692
+ planMode: false,
693
+ onToolRequest: async () => ({}),
694
+ })
695
+
696
+ const events = await collectStream(turn.stream)
697
+ const toolCall = events.find((event) => event.type === "transcript" && event.entry.kind === "tool_call")
698
+
699
+ expect(toolCall?.entry.kind).toBe("tool_call")
700
+ if (!toolCall || toolCall.entry.kind !== "tool_call") throw new Error("missing tool call")
701
+ expect(toolCall.entry.tool.toolKind).toBe("write_file")
702
+ expect(toolCall.entry.tool.toolName).toBe("Write")
703
+ expect(toolCall.entry.tool.input).toEqual({
704
+ filePath: "/tmp/project/test.md",
705
+ content: "hello\nworld",
706
+ })
707
+ })
708
+
709
+ test("maps plain-text fileChange adds into write_file tool calls", async () => {
710
+ const process = new FakeCodexProcess((message, child) => {
711
+ if (message.method === "initialize") {
712
+ child.writeServerMessage({ id: message.id, result: { userAgent: "codex-test" } })
713
+ } else if (message.method === "thread/start") {
714
+ child.writeServerMessage({
715
+ id: message.id,
716
+ result: { thread: { id: "thread-1" }, model: "gpt-5.4", reasoningEffort: "high" },
717
+ })
718
+ } else if (message.method === "turn/start") {
719
+ child.writeServerMessage({
720
+ id: message.id,
721
+ result: { turn: { id: "turn-1", status: "inProgress", error: null } },
722
+ })
723
+ child.writeServerMessage({
724
+ method: "item/completed",
725
+ params: {
726
+ threadId: "thread-1",
727
+ turnId: "turn-1",
728
+ item: {
729
+ type: "fileChange",
730
+ id: "call-1",
731
+ changes: [
732
+ {
733
+ path: "/tmp/project/test.md",
734
+ kind: {
735
+ type: "add",
736
+ move_path: null,
737
+ },
738
+ diff: "hello\nworld\n",
739
+ },
740
+ ],
741
+ status: "completed",
742
+ },
743
+ },
744
+ })
745
+ child.writeServerMessage({
746
+ method: "turn/completed",
747
+ params: {
748
+ threadId: "thread-1",
749
+ turn: { id: "turn-1", status: "completed", error: null },
750
+ },
751
+ })
752
+ }
753
+ })
754
+
755
+ const manager = new CodexAppServerManager({
756
+ spawnProcess: () => process as never,
757
+ })
758
+
759
+ await manager.startSession({
760
+ chatId: "chat-1",
761
+ cwd: "/tmp/project",
762
+ model: "gpt-5.4",
763
+ sessionToken: null,
764
+ })
765
+
766
+ const turn = await manager.startTurn({
767
+ chatId: "chat-1",
768
+ model: "gpt-5.4",
769
+ content: "write a file",
770
+ planMode: false,
771
+ onToolRequest: async () => ({}),
772
+ })
773
+
774
+ const events = await collectStream(turn.stream)
775
+ const toolCall = events.find((event) => event.type === "transcript" && event.entry.kind === "tool_call")
776
+
777
+ expect(toolCall?.entry.kind).toBe("tool_call")
778
+ if (!toolCall || toolCall.entry.kind !== "tool_call") throw new Error("missing tool call")
779
+ expect(toolCall.entry.tool.toolKind).toBe("write_file")
780
+ expect(toolCall.entry.tool.input).toEqual({
781
+ filePath: "/tmp/project/test.md",
782
+ content: "hello\nworld\n",
783
+ })
784
+ })
785
+
786
+ test("maps plain-text fileChange deletes into delete_file tool calls", async () => {
787
+ const process = new FakeCodexProcess((message, child) => {
788
+ if (message.method === "initialize") {
789
+ child.writeServerMessage({ id: message.id, result: { userAgent: "codex-test" } })
790
+ } else if (message.method === "thread/start") {
791
+ child.writeServerMessage({
792
+ id: message.id,
793
+ result: { thread: { id: "thread-1" }, model: "gpt-5.4", reasoningEffort: "high" },
794
+ })
795
+ } else if (message.method === "turn/start") {
796
+ child.writeServerMessage({
797
+ id: message.id,
798
+ result: { turn: { id: "turn-1", status: "inProgress", error: null } },
799
+ })
800
+ child.writeServerMessage({
801
+ method: "item/completed",
802
+ params: {
803
+ threadId: "thread-1",
804
+ turnId: "turn-1",
805
+ item: {
806
+ type: "fileChange",
807
+ id: "call-1",
808
+ changes: [
809
+ {
810
+ path: "/tmp/project/test.md",
811
+ kind: {
812
+ type: "delete",
813
+ move_path: null,
814
+ },
815
+ diff: "hello\nworld\n",
816
+ },
817
+ ],
818
+ status: "completed",
819
+ },
820
+ },
821
+ })
822
+ child.writeServerMessage({
823
+ method: "turn/completed",
824
+ params: {
825
+ threadId: "thread-1",
826
+ turn: { id: "turn-1", status: "completed", error: null },
827
+ },
828
+ })
829
+ }
830
+ })
831
+
832
+ const manager = new CodexAppServerManager({
833
+ spawnProcess: () => process as never,
834
+ })
835
+
836
+ await manager.startSession({
837
+ chatId: "chat-1",
838
+ cwd: "/tmp/project",
839
+ model: "gpt-5.4",
840
+ sessionToken: null,
841
+ })
842
+
843
+ const turn = await manager.startTurn({
844
+ chatId: "chat-1",
845
+ model: "gpt-5.4",
846
+ content: "delete a file",
847
+ planMode: false,
848
+ onToolRequest: async () => ({}),
849
+ })
850
+
851
+ const events = await collectStream(turn.stream)
852
+ const toolCall = events.find((event) => event.type === "transcript" && event.entry.kind === "tool_call")
853
+
854
+ expect(toolCall?.entry.kind).toBe("tool_call")
855
+ if (!toolCall || toolCall.entry.kind !== "tool_call") throw new Error("missing tool call")
856
+ expect(toolCall.entry.tool.toolKind).toBe("delete_file")
857
+ expect(toolCall.entry.tool.toolName).toBe("Delete")
858
+ expect(toolCall.entry.tool.input).toEqual({
859
+ filePath: "/tmp/project/test.md",
860
+ content: "hello\nworld\n",
861
+ })
862
+ })
863
+
864
+ test("splits multi-change fileChange items into multiple tool calls and results", async () => {
865
+ const process = new FakeCodexProcess((message, child) => {
866
+ if (message.method === "initialize") {
867
+ child.writeServerMessage({ id: message.id, result: { userAgent: "codex-test" } })
868
+ } else if (message.method === "thread/start") {
869
+ child.writeServerMessage({
870
+ id: message.id,
871
+ result: { thread: { id: "thread-1" }, model: "gpt-5.4", reasoningEffort: "high" },
872
+ })
873
+ } else if (message.method === "turn/start") {
874
+ child.writeServerMessage({
875
+ id: message.id,
876
+ result: { turn: { id: "turn-1", status: "inProgress", error: null } },
877
+ })
878
+ child.writeServerMessage({
879
+ method: "item/completed",
880
+ params: {
881
+ threadId: "thread-1",
882
+ turnId: "turn-1",
883
+ item: {
884
+ type: "fileChange",
885
+ id: "call-1",
886
+ changes: [
887
+ {
888
+ path: "/tmp/project/one.md",
889
+ kind: {
890
+ type: "add",
891
+ move_path: null,
892
+ },
893
+ diff: "@@ -0,0 +1,2 @@\n+hello\n+world",
894
+ },
895
+ {
896
+ path: "/tmp/project/two.md",
897
+ kind: {
898
+ type: "update",
899
+ move_path: null,
900
+ },
901
+ diff: "@@ -1,2 +1,2 @@\n-old line\n+new line",
902
+ },
903
+ ],
904
+ status: "completed",
905
+ },
906
+ },
907
+ })
908
+ child.writeServerMessage({
909
+ method: "turn/completed",
910
+ params: {
911
+ threadId: "thread-1",
912
+ turn: { id: "turn-1", status: "completed", error: null },
913
+ },
914
+ })
915
+ }
916
+ })
917
+
918
+ const manager = new CodexAppServerManager({
919
+ spawnProcess: () => process as never,
920
+ })
921
+
922
+ await manager.startSession({
923
+ chatId: "chat-1",
924
+ cwd: "/tmp/project",
925
+ model: "gpt-5.4",
926
+ sessionToken: null,
927
+ })
928
+
929
+ const turn = await manager.startTurn({
930
+ chatId: "chat-1",
931
+ model: "gpt-5.4",
932
+ content: "change multiple files",
933
+ planMode: false,
934
+ onToolRequest: async () => ({}),
935
+ })
936
+
937
+ const events = await collectStream(turn.stream)
938
+ const toolCalls = events.filter((event) => event.type === "transcript" && event.entry.kind === "tool_call")
939
+ const toolResults = events.filter((event) => event.type === "transcript" && event.entry.kind === "tool_result")
940
+
941
+ expect(toolCalls).toHaveLength(2)
942
+ expect(toolResults).toHaveLength(2)
943
+
944
+ expect(toolCalls[0]?.entry.kind).toBe("tool_call")
945
+ expect(toolCalls[1]?.entry.kind).toBe("tool_call")
946
+ if (toolCalls[0]?.entry.kind !== "tool_call" || toolCalls[1]?.entry.kind !== "tool_call") {
947
+ throw new Error("missing tool calls")
948
+ }
949
+
950
+ expect(toolCalls[0].entry.tool.toolKind).toBe("write_file")
951
+ expect(toolCalls[0].entry.tool.toolId).toBe("call-1:change:0")
952
+ expect(toolCalls[0].entry.tool.input).toEqual({
953
+ filePath: "/tmp/project/one.md",
954
+ content: "hello\nworld",
955
+ })
956
+
957
+ expect(toolCalls[1].entry.tool.toolKind).toBe("edit_file")
958
+ expect(toolCalls[1].entry.tool.toolId).toBe("call-1:change:1")
959
+ expect(toolCalls[1].entry.tool.input).toEqual({
960
+ filePath: "/tmp/project/two.md",
961
+ oldString: "old line",
962
+ newString: "new line",
963
+ })
964
+
965
+ expect(toolResults[0]?.entry.kind).toBe("tool_result")
966
+ expect(toolResults[1]?.entry.kind).toBe("tool_result")
967
+ if (toolResults[0]?.entry.kind !== "tool_result" || toolResults[1]?.entry.kind !== "tool_result") {
968
+ throw new Error("missing tool results")
969
+ }
970
+
971
+ expect(toolResults[0].entry.toolId).toBe("call-1:change:0")
972
+ expect(toolResults[1].entry.toolId).toBe("call-1:change:1")
973
+ })
974
+
975
+ test("maps plan updates into TodoWrite and synthesizes ExitPlanMode on successful plan turns", async () => {
976
+ const process = new FakeCodexProcess((message, child) => {
977
+ if (message.method === "initialize") {
978
+ child.writeServerMessage({ id: message.id, result: { userAgent: "codex-test" } })
979
+ } else if (message.method === "thread/start") {
980
+ child.writeServerMessage({
981
+ id: message.id,
982
+ result: { thread: { id: "thread-1" }, model: "gpt-5.4", reasoningEffort: "high" },
983
+ })
984
+ } else if (message.method === "turn/start") {
985
+ child.writeServerMessage({
986
+ id: message.id,
987
+ result: { turn: { id: "turn-1", status: "inProgress", error: null } },
988
+ })
989
+ child.writeServerMessage({
990
+ method: "turn/plan/updated",
991
+ params: {
992
+ threadId: "thread-1",
993
+ turnId: "turn-1",
994
+ explanation: "Plan the work",
995
+ plan: [
996
+ { step: "Inspect repo", status: "completed" },
997
+ { step: "Implement changes", status: "inProgress" },
998
+ ],
999
+ },
1000
+ })
1001
+ child.writeServerMessage({
1002
+ method: "item/started",
1003
+ params: {
1004
+ threadId: "thread-1",
1005
+ turnId: "turn-1",
1006
+ item: {
1007
+ type: "plan",
1008
+ id: "plan-1",
1009
+ text: "",
1010
+ },
1011
+ },
1012
+ })
1013
+ child.writeServerMessage({
1014
+ method: "item/plan/delta",
1015
+ params: {
1016
+ threadId: "thread-1",
1017
+ turnId: "turn-1",
1018
+ itemId: "plan-1",
1019
+ delta: "## Plan\n\n- [x] Inspect repo\n- [ ] Implement changes",
1020
+ },
1021
+ })
1022
+ child.writeServerMessage({
1023
+ method: "turn/completed",
1024
+ params: {
1025
+ threadId: "thread-1",
1026
+ turn: { id: "turn-1", status: "completed", error: null },
1027
+ },
1028
+ })
1029
+ }
1030
+ })
1031
+
1032
+ const manager = new CodexAppServerManager({
1033
+ spawnProcess: () => process as never,
1034
+ })
1035
+
1036
+ await manager.startSession({
1037
+ chatId: "chat-1",
1038
+ cwd: "/tmp/project",
1039
+ model: "gpt-5.4",
1040
+ sessionToken: null,
1041
+ })
1042
+
1043
+ const turn = await manager.startTurn({
1044
+ chatId: "chat-1",
1045
+ model: "gpt-5.4",
1046
+ content: "make a plan",
1047
+ planMode: true,
1048
+ onToolRequest: async () => ({ confirmed: true }),
1049
+ })
1050
+
1051
+ const events = await collectStream(turn.stream)
1052
+ const toolCalls = events
1053
+ .filter((event) => event.type === "transcript" && event.entry.kind === "tool_call")
1054
+ .map((event) => event.entry.tool)
1055
+
1056
+ expect(toolCalls[0]?.toolKind).toBe("todo_write")
1057
+ expect(toolCalls[1]?.toolKind).toBe("exit_plan_mode")
1058
+ if (!toolCalls[1] || toolCalls[1].toolKind !== "exit_plan_mode") {
1059
+ throw new Error("missing ExitPlanMode tool")
1060
+ }
1061
+ expect(toolCalls[1].input.summary).toBe("Plan the work")
1062
+ expect(toolCalls[1].input.plan).toContain("## Plan")
1063
+ })
1064
+
1065
+ test("maps collab agent tool calls into subagent_task", async () => {
1066
+ const process = new FakeCodexProcess((message, child) => {
1067
+ if (message.method === "initialize") {
1068
+ child.writeServerMessage({ id: message.id, result: { userAgent: "codex-test" } })
1069
+ } else if (message.method === "thread/start") {
1070
+ child.writeServerMessage({
1071
+ id: message.id,
1072
+ result: { thread: { id: "thread-1" }, model: "gpt-5.4", reasoningEffort: "high" },
1073
+ })
1074
+ } else if (message.method === "turn/start") {
1075
+ child.writeServerMessage({
1076
+ id: message.id,
1077
+ result: { turn: { id: "turn-1", status: "inProgress", error: null } },
1078
+ })
1079
+ child.writeServerMessage({
1080
+ method: "item/completed",
1081
+ params: {
1082
+ threadId: "thread-1",
1083
+ turnId: "turn-1",
1084
+ item: {
1085
+ type: "collabAgentToolCall",
1086
+ id: "agent-1",
1087
+ tool: "spawnAgent",
1088
+ status: "completed",
1089
+ senderThreadId: "thread-1",
1090
+ receiverThreadIds: ["thread-2"],
1091
+ prompt: "Inspect tests",
1092
+ agentsStates: {
1093
+ "thread-2": { status: "running", message: "Inspecting" },
1094
+ },
1095
+ },
1096
+ },
1097
+ })
1098
+ child.writeServerMessage({
1099
+ method: "turn/completed",
1100
+ params: {
1101
+ threadId: "thread-1",
1102
+ turn: { id: "turn-1", status: "completed", error: null },
1103
+ },
1104
+ })
1105
+ }
1106
+ })
1107
+
1108
+ const manager = new CodexAppServerManager({
1109
+ spawnProcess: () => process as never,
1110
+ })
1111
+
1112
+ await manager.startSession({
1113
+ chatId: "chat-1",
1114
+ cwd: "/tmp/project",
1115
+ model: "gpt-5.4",
1116
+ sessionToken: null,
1117
+ })
1118
+
1119
+ const turn = await manager.startTurn({
1120
+ chatId: "chat-1",
1121
+ model: "gpt-5.4",
1122
+ content: "spawn an agent",
1123
+ planMode: false,
1124
+ onToolRequest: async () => ({}),
1125
+ })
1126
+
1127
+ const events = await collectStream(turn.stream)
1128
+ const toolCall = events.find((event) => event.type === "transcript" && event.entry.kind === "tool_call")
1129
+
1130
+ expect(toolCall?.entry.kind).toBe("tool_call")
1131
+ if (!toolCall || toolCall.entry.kind !== "tool_call") throw new Error("missing tool call")
1132
+ expect(toolCall.entry.tool.toolKind).toBe("subagent_task")
1133
+ expect(toolCall.entry.tool.input).toEqual({ subagentType: "spawnAgent" })
1134
+ })
1135
+
1136
+ test("uses the completed webSearch query when the started item is empty", async () => {
1137
+ const process = new FakeCodexProcess((message, child) => {
1138
+ if (message.method === "initialize") {
1139
+ child.writeServerMessage({ id: message.id, result: { userAgent: "codex-test" } })
1140
+ } else if (message.method === "thread/start") {
1141
+ child.writeServerMessage({
1142
+ id: message.id,
1143
+ result: { thread: { id: "thread-1" }, model: "gpt-5.4", reasoningEffort: "high" },
1144
+ })
1145
+ } else if (message.method === "turn/start") {
1146
+ child.writeServerMessage({
1147
+ id: message.id,
1148
+ result: { turn: { id: "turn-1", status: "inProgress", error: null } },
1149
+ })
1150
+ child.writeServerMessage({
1151
+ method: "item/started",
1152
+ params: {
1153
+ threadId: "thread-1",
1154
+ turnId: "turn-1",
1155
+ item: {
1156
+ type: "webSearch",
1157
+ id: "ws-1",
1158
+ query: "",
1159
+ },
1160
+ },
1161
+ })
1162
+ child.writeServerMessage({
1163
+ method: "item/completed",
1164
+ params: {
1165
+ threadId: "thread-1",
1166
+ turnId: "turn-1",
1167
+ item: {
1168
+ type: "webSearch",
1169
+ id: "ws-1",
1170
+ query: "jake mor",
1171
+ action: {
1172
+ type: "search",
1173
+ query: "jake mor",
1174
+ queries: ["jake mor"],
1175
+ },
1176
+ },
1177
+ },
1178
+ })
1179
+ child.writeServerMessage({
1180
+ method: "turn/completed",
1181
+ params: {
1182
+ threadId: "thread-1",
1183
+ turn: { id: "turn-1", status: "completed", error: null },
1184
+ },
1185
+ })
1186
+ }
1187
+ })
1188
+
1189
+ const manager = new CodexAppServerManager({
1190
+ spawnProcess: () => process as never,
1191
+ })
1192
+
1193
+ await manager.startSession({
1194
+ chatId: "chat-1",
1195
+ cwd: "/tmp/project",
1196
+ model: "gpt-5.4",
1197
+ sessionToken: null,
1198
+ })
1199
+
1200
+ const turn = await manager.startTurn({
1201
+ chatId: "chat-1",
1202
+ model: "gpt-5.4",
1203
+ content: "search",
1204
+ planMode: false,
1205
+ onToolRequest: async () => ({}),
1206
+ })
1207
+
1208
+ const events = await collectStream(turn.stream)
1209
+ const toolCalls = events.filter((event) => event.type === "transcript" && event.entry.kind === "tool_call")
1210
+
1211
+ expect(toolCalls).toHaveLength(1)
1212
+ const toolCall = toolCalls[0]
1213
+ if (toolCall.entry.kind !== "tool_call") throw new Error("missing tool call")
1214
+ expect(toolCall.entry.tool.toolKind).toBe("web_search")
1215
+ expect(toolCall.entry.tool.input).toEqual({ query: "jake mor" })
1216
+ })
1217
+
1218
+ test("responds to unsupported dynamic tool requests with a generic tool error", async () => {
1219
+ const process = new FakeCodexProcess((message, child) => {
1220
+ if (message.method === "initialize") {
1221
+ child.writeServerMessage({ id: message.id, result: { userAgent: "codex-test" } })
1222
+ } else if (message.method === "thread/start") {
1223
+ child.writeServerMessage({
1224
+ id: message.id,
1225
+ result: { thread: { id: "thread-1" }, model: "gpt-5.4", reasoningEffort: "high" },
1226
+ })
1227
+ } else if (message.method === "turn/start") {
1228
+ child.writeServerMessage({
1229
+ id: message.id,
1230
+ result: { turn: { id: "turn-1", status: "inProgress", error: null } },
1231
+ })
1232
+ child.writeServerMessage({
1233
+ id: "dyn-1",
1234
+ method: "item/tool/call",
1235
+ params: {
1236
+ threadId: "thread-1",
1237
+ turnId: "turn-1",
1238
+ callId: "call-1",
1239
+ tool: "custom_tool",
1240
+ arguments: { value: 1 },
1241
+ },
1242
+ })
1243
+ child.writeServerMessage({
1244
+ method: "turn/completed",
1245
+ params: {
1246
+ threadId: "thread-1",
1247
+ turn: { id: "turn-1", status: "completed", error: null },
1248
+ },
1249
+ })
1250
+ }
1251
+ })
1252
+
1253
+ const manager = new CodexAppServerManager({
1254
+ spawnProcess: () => process as never,
1255
+ })
1256
+
1257
+ await manager.startSession({
1258
+ chatId: "chat-1",
1259
+ cwd: "/tmp/project",
1260
+ model: "gpt-5.4",
1261
+ sessionToken: null,
1262
+ })
1263
+
1264
+ const turn = await manager.startTurn({
1265
+ chatId: "chat-1",
1266
+ model: "gpt-5.4",
1267
+ content: "call tool",
1268
+ planMode: false,
1269
+ onToolRequest: async () => ({}),
1270
+ })
1271
+
1272
+ const events = await collectStream(turn.stream)
1273
+ const toolCall = events.find((event) => event.type === "transcript" && event.entry.kind === "tool_call")
1274
+ const toolResult = events.find((event) => event.type === "transcript" && event.entry.kind === "tool_result")
1275
+ const response = process.messages.find((message: any) => message.id === "dyn-1")
1276
+
1277
+ expect(toolCall?.entry.kind).toBe("tool_call")
1278
+ if (!toolCall || toolCall.entry.kind !== "tool_call") throw new Error("missing tool call")
1279
+ expect(toolCall.entry.tool.toolKind).toBe("unknown_tool")
1280
+ expect(toolCall.entry.tool.toolName).toBe("custom_tool")
1281
+ expect(toolResult?.entry.kind).toBe("tool_result")
1282
+ expect(response).toEqual({
1283
+ id: "dyn-1",
1284
+ result: {
1285
+ contentItems: [{ type: "inputText", text: "Unsupported dynamic tool call: custom_tool" }],
1286
+ success: false,
1287
+ },
1288
+ })
1289
+ })
1290
+
1291
+ test("answers requestUserInput requests with the official JSON-RPC result payload", async () => {
1292
+ const process = new FakeCodexProcess((message, child) => {
1293
+ if (message.method === "initialize") {
1294
+ child.writeServerMessage({ id: message.id, result: { userAgent: "codex-test" } })
1295
+ } else if (message.method === "thread/start") {
1296
+ child.writeServerMessage({
1297
+ id: message.id,
1298
+ result: { thread: { id: "thread-1" }, model: "gpt-5.4", reasoningEffort: "high" },
1299
+ })
1300
+ } else if (message.method === "turn/start") {
1301
+ child.writeServerMessage({
1302
+ id: message.id,
1303
+ result: { turn: { id: "turn-1", status: "inProgress", error: null } },
1304
+ })
1305
+ child.writeServerMessage({
1306
+ id: "req-1",
1307
+ method: "item/tool/requestUserInput",
1308
+ params: {
1309
+ threadId: "thread-1",
1310
+ turnId: "turn-1",
1311
+ itemId: "ask-1",
1312
+ questions: [
1313
+ {
1314
+ id: "runtime",
1315
+ header: "Runtime",
1316
+ question: "Which runtime?",
1317
+ isOther: false,
1318
+ isSecret: false,
1319
+ options: null,
1320
+ },
1321
+ ],
1322
+ },
1323
+ })
1324
+ child.writeServerMessage({
1325
+ method: "turn/completed",
1326
+ params: {
1327
+ threadId: "thread-1",
1328
+ turn: { id: "turn-1", status: "completed", error: null },
1329
+ },
1330
+ })
1331
+ }
1332
+ })
1333
+
1334
+ const manager = new CodexAppServerManager({
1335
+ spawnProcess: () => process as never,
1336
+ })
1337
+
1338
+ await manager.startSession({
1339
+ chatId: "chat-1",
1340
+ cwd: "/tmp/project",
1341
+ model: "gpt-5.4",
1342
+ sessionToken: null,
1343
+ })
1344
+
1345
+ const turn = await manager.startTurn({
1346
+ chatId: "chat-1",
1347
+ model: "gpt-5.4",
1348
+ content: "ask me",
1349
+ planMode: false,
1350
+ onToolRequest: async () => ({
1351
+ questions: [{
1352
+ id: "runtime",
1353
+ question: "Which runtime?",
1354
+ }],
1355
+ answers: {
1356
+ runtime: "bun",
1357
+ },
1358
+ }),
1359
+ })
1360
+
1361
+ const events = await collectStream(turn.stream)
1362
+ const askEntry = events.find((event) => event.type === "transcript" && event.entry.kind === "tool_call")
1363
+ expect(askEntry?.entry.tool.toolKind).toBe("ask_user_question")
1364
+
1365
+ const response = process.messages.find((message: any) => message.id === "req-1")
1366
+ expect(response).toEqual({
1367
+ id: "req-1",
1368
+ result: {
1369
+ answers: {
1370
+ runtime: {
1371
+ answers: ["bun"],
1372
+ },
1373
+ },
1374
+ },
1375
+ })
1376
+ })
1377
+
1378
+ test("falls back to question text when requestUserInput answers are keyed by prompt text", async () => {
1379
+ const process = new FakeCodexProcess((message, child) => {
1380
+ if (message.method === "initialize") {
1381
+ child.writeServerMessage({ id: message.id, result: { userAgent: "codex-test" } })
1382
+ } else if (message.method === "thread/start") {
1383
+ child.writeServerMessage({
1384
+ id: message.id,
1385
+ result: { thread: { id: "thread-1" }, model: "gpt-5.4", reasoningEffort: "high" },
1386
+ })
1387
+ } else if (message.method === "turn/start") {
1388
+ child.writeServerMessage({
1389
+ id: message.id,
1390
+ result: { turn: { id: "turn-1", status: "inProgress", error: null } },
1391
+ })
1392
+ child.writeServerMessage({
1393
+ id: "req-1",
1394
+ method: "item/tool/requestUserInput",
1395
+ params: {
1396
+ threadId: "thread-1",
1397
+ turnId: "turn-1",
1398
+ itemId: "ask-1",
1399
+ questions: [
1400
+ {
1401
+ id: "favorite_color",
1402
+ header: "Color",
1403
+ question: "What is your favorite color right now?",
1404
+ isOther: true,
1405
+ isSecret: false,
1406
+ options: [
1407
+ { label: "Red", description: null },
1408
+ { label: "Blue", description: null },
1409
+ ],
1410
+ },
1411
+ ],
1412
+ },
1413
+ })
1414
+ child.writeServerMessage({
1415
+ method: "turn/completed",
1416
+ params: {
1417
+ threadId: "thread-1",
1418
+ turn: { id: "turn-1", status: "completed", error: null },
1419
+ },
1420
+ })
1421
+ }
1422
+ })
1423
+
1424
+ const manager = new CodexAppServerManager({
1425
+ spawnProcess: () => process as never,
1426
+ })
1427
+
1428
+ await manager.startSession({
1429
+ chatId: "chat-1",
1430
+ cwd: "/tmp/project",
1431
+ model: "gpt-5.4",
1432
+ sessionToken: null,
1433
+ })
1434
+
1435
+ const turn = await manager.startTurn({
1436
+ chatId: "chat-1",
1437
+ model: "gpt-5.4",
1438
+ content: "ask me",
1439
+ planMode: false,
1440
+ onToolRequest: async () => ({
1441
+ questions: [{
1442
+ id: "favorite_color",
1443
+ question: "What is your favorite color right now?",
1444
+ }],
1445
+ answers: {
1446
+ "What is your favorite color right now?": "Red",
1447
+ },
1448
+ }),
1449
+ })
1450
+
1451
+ await collectStream(turn.stream)
1452
+
1453
+ const response = process.messages.find((message: any) => message.id === "req-1")
1454
+ expect(response).toEqual({
1455
+ id: "req-1",
1456
+ result: {
1457
+ answers: {
1458
+ favorite_color: {
1459
+ answers: ["Red"],
1460
+ },
1461
+ },
1462
+ },
1463
+ })
1464
+ })
1465
+
1466
+ test("infers multi-select Codex questions from prompt text and returns multiple answers", async () => {
1467
+ const process = new FakeCodexProcess((message, child) => {
1468
+ if (message.method === "initialize") {
1469
+ child.writeServerMessage({ id: message.id, result: { userAgent: "codex-test" } })
1470
+ } else if (message.method === "thread/start") {
1471
+ child.writeServerMessage({
1472
+ id: message.id,
1473
+ result: { thread: { id: "thread-1" }, model: "gpt-5.4", reasoningEffort: "high" },
1474
+ })
1475
+ } else if (message.method === "turn/start") {
1476
+ child.writeServerMessage({
1477
+ id: message.id,
1478
+ result: { turn: { id: "turn-1", status: "inProgress", error: null } },
1479
+ })
1480
+ child.writeServerMessage({
1481
+ id: "req-1",
1482
+ method: "item/tool/requestUserInput",
1483
+ params: {
1484
+ threadId: "thread-1",
1485
+ turnId: "turn-1",
1486
+ itemId: "ask-1",
1487
+ questions: [
1488
+ {
1489
+ id: "runtimes",
1490
+ header: "Runtime",
1491
+ question: "Select all runtimes that apply",
1492
+ isOther: true,
1493
+ isSecret: false,
1494
+ options: [
1495
+ { label: "bun", description: null },
1496
+ { label: "node", description: null },
1497
+ ],
1498
+ },
1499
+ ],
1500
+ },
1501
+ })
1502
+ child.writeServerMessage({
1503
+ method: "turn/completed",
1504
+ params: {
1505
+ threadId: "thread-1",
1506
+ turn: { id: "turn-1", status: "completed", error: null },
1507
+ },
1508
+ })
1509
+ }
1510
+ })
1511
+
1512
+ const manager = new CodexAppServerManager({
1513
+ spawnProcess: () => process as never,
1514
+ })
1515
+
1516
+ await manager.startSession({
1517
+ chatId: "chat-1",
1518
+ cwd: "/tmp/project",
1519
+ model: "gpt-5.4",
1520
+ sessionToken: null,
1521
+ })
1522
+
1523
+ const turn = await manager.startTurn({
1524
+ chatId: "chat-1",
1525
+ model: "gpt-5.4",
1526
+ content: "ask me",
1527
+ planMode: false,
1528
+ onToolRequest: async ({ tool }) => {
1529
+ expect(tool.toolKind).toBe("ask_user_question")
1530
+ if (tool.toolKind !== "ask_user_question") {
1531
+ return {}
1532
+ }
1533
+
1534
+ expect(tool.input.questions[0]?.multiSelect).toBe(true)
1535
+
1536
+ return {
1537
+ questions: [{
1538
+ id: "runtimes",
1539
+ question: "Select all runtimes that apply",
1540
+ multiSelect: true,
1541
+ }],
1542
+ answers: {
1543
+ runtimes: ["bun", "node"],
1544
+ },
1545
+ }
1546
+ },
1547
+ })
1548
+
1549
+ await collectStream(turn.stream)
1550
+
1551
+ const response = process.messages.find((message: any) => message.id === "req-1")
1552
+ expect(response).toEqual({
1553
+ id: "req-1",
1554
+ result: {
1555
+ answers: {
1556
+ runtimes: {
1557
+ answers: ["bun", "node"],
1558
+ },
1559
+ },
1560
+ },
1561
+ })
1562
+ })
1563
+
1564
+ test("sends approval decisions back to the app-server", async () => {
1565
+ const process = new FakeCodexProcess((message, child) => {
1566
+ if (message.method === "initialize") {
1567
+ child.writeServerMessage({ id: message.id, result: { userAgent: "codex-test" } })
1568
+ } else if (message.method === "thread/start") {
1569
+ child.writeServerMessage({
1570
+ id: message.id,
1571
+ result: { thread: { id: "thread-1" }, model: "gpt-5.4", reasoningEffort: "high" },
1572
+ })
1573
+ } else if (message.method === "turn/start") {
1574
+ child.writeServerMessage({
1575
+ id: message.id,
1576
+ result: { turn: { id: "turn-1", status: "inProgress", error: null } },
1577
+ })
1578
+ child.writeServerMessage({
1579
+ id: "approval-1",
1580
+ method: "item/commandExecution/requestApproval",
1581
+ params: {
1582
+ threadId: "thread-1",
1583
+ turnId: "turn-1",
1584
+ itemId: "call-1",
1585
+ command: "rm -rf .",
1586
+ cwd: "/tmp/project",
1587
+ },
1588
+ })
1589
+ child.writeServerMessage({
1590
+ method: "turn/completed",
1591
+ params: {
1592
+ threadId: "thread-1",
1593
+ turn: { id: "turn-1", status: "completed", error: null },
1594
+ },
1595
+ })
1596
+ }
1597
+ })
1598
+
1599
+ const manager = new CodexAppServerManager({
1600
+ spawnProcess: () => process as never,
1601
+ })
1602
+
1603
+ await manager.startSession({
1604
+ chatId: "chat-1",
1605
+ cwd: "/tmp/project",
1606
+ model: "gpt-5.4",
1607
+ sessionToken: null,
1608
+ })
1609
+
1610
+ const turn = await manager.startTurn({
1611
+ chatId: "chat-1",
1612
+ model: "gpt-5.4",
1613
+ content: "approve something",
1614
+ planMode: false,
1615
+ onToolRequest: async () => ({}),
1616
+ onApprovalRequest: async () => "accept",
1617
+ })
1618
+
1619
+ await collectStream(turn.stream)
1620
+
1621
+ const response = process.messages.find((message: any) => message.id === "approval-1")
1622
+ expect(response).toEqual({
1623
+ id: "approval-1",
1624
+ result: {
1625
+ decision: "accept",
1626
+ },
1627
+ })
1628
+ })
1629
+
1630
+ test("interrupt sends turn/interrupt for the active turn", async () => {
1631
+ const process = new FakeCodexProcess((message, child) => {
1632
+ if (message.method === "initialize") {
1633
+ child.writeServerMessage({ id: message.id, result: { userAgent: "codex-test" } })
1634
+ } else if (message.method === "thread/start") {
1635
+ child.writeServerMessage({
1636
+ id: message.id,
1637
+ result: { thread: { id: "thread-1" }, model: "gpt-5.4", reasoningEffort: "high" },
1638
+ })
1639
+ } else if (message.method === "turn/start") {
1640
+ child.writeServerMessage({
1641
+ id: message.id,
1642
+ result: { turn: { id: "turn-1", status: "inProgress", error: null } },
1643
+ })
1644
+ } else if (message.method === "turn/interrupt") {
1645
+ child.writeServerMessage({ id: message.id, result: {} })
1646
+ }
1647
+ })
1648
+
1649
+ const manager = new CodexAppServerManager({
1650
+ spawnProcess: () => process as never,
1651
+ })
1652
+
1653
+ await manager.startSession({
1654
+ chatId: "chat-1",
1655
+ cwd: "/tmp/project",
1656
+ model: "gpt-5.4",
1657
+ sessionToken: null,
1658
+ })
1659
+
1660
+ const turn = await manager.startTurn({
1661
+ chatId: "chat-1",
1662
+ model: "gpt-5.4",
1663
+ content: "wait",
1664
+ planMode: false,
1665
+ onToolRequest: async () => ({}),
1666
+ })
1667
+
1668
+ await turn.interrupt()
1669
+
1670
+ const interruptRequest = process.messages.find((message: any) => message.method === "turn/interrupt") as
1671
+ | { id: string; method: "turn/interrupt"; params: { threadId: string; turnId: string } }
1672
+ | undefined
1673
+ expect(interruptRequest).toBeDefined()
1674
+ if (!interruptRequest) throw new Error("missing interrupt request")
1675
+ expect(interruptRequest).toEqual({
1676
+ id: interruptRequest.id,
1677
+ method: "turn/interrupt",
1678
+ params: {
1679
+ threadId: "thread-1",
1680
+ turnId: "turn-1",
1681
+ },
1682
+ })
1683
+ })
1684
+
1685
+ test("interrupt clears a pending exit-plan wait so a new turn can start immediately", async () => {
1686
+ let resolveToolRequest!: (value: unknown) => void
1687
+
1688
+ const process = new FakeCodexProcess((message, child) => {
1689
+ if (message.method === "initialize") {
1690
+ child.writeServerMessage({ id: message.id, result: { userAgent: "codex-test" } })
1691
+ } else if (message.method === "thread/start") {
1692
+ child.writeServerMessage({
1693
+ id: message.id,
1694
+ result: { thread: { id: "thread-1" }, model: "gpt-5.4", reasoningEffort: "high" },
1695
+ })
1696
+ } else if (message.method === "turn/start") {
1697
+ if (message.params.input[0]?.text === "make a plan") {
1698
+ child.writeServerMessage({
1699
+ id: message.id,
1700
+ result: { turn: { id: "turn-plan", status: "completed", error: null } },
1701
+ })
1702
+ child.writeServerMessage({
1703
+ method: "turn/plan/updated",
1704
+ params: {
1705
+ threadId: "thread-1",
1706
+ turnId: "turn-plan",
1707
+ explanation: "Plan the work",
1708
+ plan: [{ step: "Inspect repo", status: "completed" }],
1709
+ },
1710
+ })
1711
+ child.writeServerMessage({
1712
+ method: "turn/completed",
1713
+ params: {
1714
+ threadId: "thread-1",
1715
+ turn: { id: "turn-plan", status: "completed", error: null },
1716
+ },
1717
+ })
1718
+ } else {
1719
+ child.writeServerMessage({
1720
+ id: message.id,
1721
+ result: { turn: { id: "turn-next", status: "completed", error: null } },
1722
+ })
1723
+ child.writeServerMessage({
1724
+ method: "turn/completed",
1725
+ params: {
1726
+ threadId: "thread-1",
1727
+ turn: { id: "turn-next", status: "completed", error: null },
1728
+ },
1729
+ })
1730
+ }
1731
+ }
1732
+ })
1733
+
1734
+ const manager = new CodexAppServerManager({
1735
+ spawnProcess: () => process as never,
1736
+ })
1737
+
1738
+ await manager.startSession({
1739
+ chatId: "chat-1",
1740
+ cwd: "/tmp/project",
1741
+ model: "gpt-5.4",
1742
+ sessionToken: null,
1743
+ })
1744
+
1745
+ const turn = await manager.startTurn({
1746
+ chatId: "chat-1",
1747
+ model: "gpt-5.4",
1748
+ content: "make a plan",
1749
+ planMode: true,
1750
+ onToolRequest: async () => await new Promise((resolve) => {
1751
+ resolveToolRequest = resolve
1752
+ }),
1753
+ })
1754
+
1755
+ const iterator = turn.stream[Symbol.asyncIterator]()
1756
+ await iterator.next()
1757
+ await iterator.next()
1758
+ await iterator.next()
1759
+ await turn.interrupt()
1760
+
1761
+ const nextTurn = await manager.startTurn({
1762
+ chatId: "chat-1",
1763
+ model: "gpt-5.4",
1764
+ content: "continue",
1765
+ planMode: false,
1766
+ onToolRequest: async () => ({}),
1767
+ })
1768
+
1769
+ await collectStream(nextTurn.stream)
1770
+ resolveToolRequest({})
1771
+ })
1772
+
1773
+ test("emits an error result when the app-server exits mid-turn", async () => {
1774
+ const process = new FakeCodexProcess((message, child) => {
1775
+ if (message.method === "initialize") {
1776
+ child.writeServerMessage({ id: message.id, result: { userAgent: "codex-test" } })
1777
+ } else if (message.method === "thread/start") {
1778
+ child.writeServerMessage({
1779
+ id: message.id,
1780
+ result: { thread: { id: "thread-1" }, model: "gpt-5.4", reasoningEffort: "high" },
1781
+ })
1782
+ } else if (message.method === "turn/start") {
1783
+ child.writeServerMessage({
1784
+ id: message.id,
1785
+ result: { turn: { id: "turn-1", status: "inProgress", error: null } },
1786
+ })
1787
+ child.writeStderr("fatal: app-server crashed")
1788
+ child.closeWithCode(1)
1789
+ }
1790
+ })
1791
+
1792
+ const manager = new CodexAppServerManager({
1793
+ spawnProcess: () => process as never,
1794
+ })
1795
+
1796
+ await manager.startSession({
1797
+ chatId: "chat-1",
1798
+ cwd: "/tmp/project",
1799
+ model: "gpt-5.4",
1800
+ sessionToken: null,
1801
+ })
1802
+
1803
+ const turn = await manager.startTurn({
1804
+ chatId: "chat-1",
1805
+ model: "gpt-5.4",
1806
+ content: "crash",
1807
+ planMode: false,
1808
+ onToolRequest: async () => ({}),
1809
+ })
1810
+
1811
+ const events = await collectStream(turn.stream)
1812
+ const resultEvent = events.find((event) => event.type === "transcript" && event.entry.kind === "result")
1813
+ expect(resultEvent?.entry.subtype).toBe("error")
1814
+ expect(resultEvent?.entry.result).toContain("fatal: app-server crashed")
1815
+ })
1816
+ })