farming-code 2.2.6

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 (453) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +287 -0
  3. package/README.zh_cn.md +329 -0
  4. package/SECURITY.md +29 -0
  5. package/THIRD_PARTY_NOTICES.md +106 -0
  6. package/backend/acp/client-services.js +312 -0
  7. package/backend/acp/patch-decisions.js +137 -0
  8. package/backend/acp/permission-security.js +108 -0
  9. package/backend/acp-runtime.js +1144 -0
  10. package/backend/acp-session-state.js +448 -0
  11. package/backend/acp-transcript.js +169 -0
  12. package/backend/agent-activity.js +186 -0
  13. package/backend/agent-env.js +250 -0
  14. package/backend/agent-json-stream.js +268 -0
  15. package/backend/agent-manager.js +4259 -0
  16. package/backend/agent-memory-report.js +583 -0
  17. package/backend/agent-order.js +162 -0
  18. package/backend/agent-provider-session.js +287 -0
  19. package/backend/agent-session-history.js +888 -0
  20. package/backend/app-server-api.js +917 -0
  21. package/backend/async-cache.js +94 -0
  22. package/backend/auth.js +178 -0
  23. package/backend/claude-settings.js +121 -0
  24. package/backend/cli-agents.js +484 -0
  25. package/backend/codex-app-server-home.js +39 -0
  26. package/backend/codex-app-server-runtime.js +901 -0
  27. package/backend/codex-context-window.js +189 -0
  28. package/backend/codex-models.js +229 -0
  29. package/backend/codex-rollout-follower.js +243 -0
  30. package/backend/codex-session-history.js +553 -0
  31. package/backend/codex-transcript-sanitizer.js +103 -0
  32. package/backend/codex-transcript.js +2341 -0
  33. package/backend/command-runner-child.js +67 -0
  34. package/backend/config-manager.js +815 -0
  35. package/backend/control-api.js +151 -0
  36. package/backend/data/chinese-poetic-words.json +8223 -0
  37. package/backend/data/poetic-word-sources/china.json +49209 -0
  38. package/backend/data/poetic-word-sources/india.json +9763 -0
  39. package/backend/data/poetic-word-sources/japan.json +8455 -0
  40. package/backend/executable-discovery.js +207 -0
  41. package/backend/farming-app-cli.js +790 -0
  42. package/backend/farming-cli.js +400 -0
  43. package/backend/farming-session-store.js +373 -0
  44. package/backend/haiku-token.js +772 -0
  45. package/backend/index-html.js +49 -0
  46. package/backend/input-parts.js +54 -0
  47. package/backend/input-routing.js +24 -0
  48. package/backend/json-cli-runtime.js +161 -0
  49. package/backend/local-session-engine.js +620 -0
  50. package/backend/main-agent-skills.js +292 -0
  51. package/backend/main-page-session.js +119 -0
  52. package/backend/native-pty-host-client.js +516 -0
  53. package/backend/native-pty-host-path.js +22 -0
  54. package/backend/native-pty-host.js +816 -0
  55. package/backend/native-session-engine.js +196 -0
  56. package/backend/network.js +23 -0
  57. package/backend/npm-update-helper.js +196 -0
  58. package/backend/packaged-node-pty.js +79 -0
  59. package/backend/provider-session-id.js +47 -0
  60. package/backend/qr-share-tickets.js +100 -0
  61. package/backend/review-diff-router.js +180 -0
  62. package/backend/review-diff-service.js +1270 -0
  63. package/backend/review-session-router.js +74 -0
  64. package/backend/review-session-service.js +432 -0
  65. package/backend/review-session-store.js +144 -0
  66. package/backend/review-state-router.js +116 -0
  67. package/backend/review-state-store.js +287 -0
  68. package/backend/run-history-store.js +70 -0
  69. package/backend/server.js +2446 -0
  70. package/backend/session-engine-bridge.js +110 -0
  71. package/backend/session-engine-router.js +66 -0
  72. package/backend/session-engine.js +39 -0
  73. package/backend/shell-busy-integration.js +472 -0
  74. package/backend/slash-command-discovery.js +296 -0
  75. package/backend/storage-layout.js +101 -0
  76. package/backend/system-monitor.js +77 -0
  77. package/backend/terminal-runtime-cleanup.js +80 -0
  78. package/backend/terminal-screen-state.js +240 -0
  79. package/backend/terminal-screen-worker-pool.js +165 -0
  80. package/backend/terminal-screen-worker-thread.js +161 -0
  81. package/backend/terminal-screen-worker.js +255 -0
  82. package/backend/terminal-status.js +356 -0
  83. package/backend/theme-manager.js +145 -0
  84. package/backend/update-service.js +1183 -0
  85. package/backend/usage-forecast.js +86 -0
  86. package/backend/usage-monitor.js +1357 -0
  87. package/backend/workspace-discovery.js +311 -0
  88. package/backend/workspace-file-router.js +431 -0
  89. package/backend/workspace-file-service.js +2765 -0
  90. package/bin/farming +5 -0
  91. package/config/farming.deploy.env.example +13 -0
  92. package/config/farming.install.env.example +17 -0
  93. package/dist/assets/App-BrP-ENHg.css +1 -0
  94. package/dist/assets/App-Dza5yEDe.js +124 -0
  95. package/dist/assets/FileEditorMarkdownPreview-DRDNQ8mX.css +1 -0
  96. package/dist/assets/FileEditorMarkdownPreview-elKWc8Im.js +404 -0
  97. package/dist/assets/FileEditorPane-BQLocXOp.js +5 -0
  98. package/dist/assets/IconGlyphs-RkMAdBXF.js +1 -0
  99. package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  100. package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  101. package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  102. package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  103. package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  104. package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  105. package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  106. package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  107. package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  108. package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  109. package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  110. package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  111. package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  112. package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  113. package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  114. package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  115. package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  116. package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  117. package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  118. package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  119. package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  120. package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  121. package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  122. package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  123. package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  124. package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  125. package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  126. package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  127. package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  128. package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  129. package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  130. package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  131. package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  132. package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  133. package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  134. package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  135. package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  136. package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  137. package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  138. package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  139. package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  140. package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  141. package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  142. package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  143. package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  144. package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  145. package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  146. package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  147. package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  148. package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  149. package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  150. package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  151. package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  152. package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  153. package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  154. package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  155. package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  156. package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  157. package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  158. package/dist/assets/ProjectFilesSection-lS1azbgj.js +12 -0
  159. package/dist/assets/ReviewPage-DgnmgU2T.js +3 -0
  160. package/dist/assets/abap-Cc5D5h5i.js +1 -0
  161. package/dist/assets/abnfDiagram-VRR7QNED-BgawzDQT.js +1 -0
  162. package/dist/assets/apex-BKopwLmx.js +1 -0
  163. package/dist/assets/api-D1lyBYIQ.js +1 -0
  164. package/dist/assets/arc-CWoVlxJw.js +1 -0
  165. package/dist/assets/architecture-TIHT7OUA-CbJBFh3w.js +1 -0
  166. package/dist/assets/architectureDiagram-ZJ3FMSHR-DUShwPg0.js +36 -0
  167. package/dist/assets/array-HeX70jSN.js +1 -0
  168. package/dist/assets/azcli-DI-C34WA.js +1 -0
  169. package/dist/assets/bat-Cbp6u42z.js +1 -0
  170. package/dist/assets/bicep-4QA1jTCM.js +2 -0
  171. package/dist/assets/blockDiagram-677ZJIJ3-CeGZviXR.js +132 -0
  172. package/dist/assets/c4Diagram-LMCZKHZV-CLhYht6S.js +10 -0
  173. package/dist/assets/cameligo-Dda_adPs.js +1 -0
  174. package/dist/assets/channel-Uw90AO0b.js +1 -0
  175. package/dist/assets/chunk-2Q5K7J3B-d5WWh8jo.js +1 -0
  176. package/dist/assets/chunk-32BRIVSS-LFYpT-Cu.js +1 -0
  177. package/dist/assets/chunk-52WLFC77-BH6jDw9J.js +10 -0
  178. package/dist/assets/chunk-5FCAYU7R-C7AmIhYv.js +1 -0
  179. package/dist/assets/chunk-5HE753X5-C8cXaouP.js +1 -0
  180. package/dist/assets/chunk-5JV3BV7I-C1PtimcA.js +1 -0
  181. package/dist/assets/chunk-5TONJI2A-_81zUME8.js +2 -0
  182. package/dist/assets/chunk-5VM5RSS4-Dlw7AXWp.js +15 -0
  183. package/dist/assets/chunk-7BUUIJ7U-ZBRhjLOh.js +1 -0
  184. package/dist/assets/chunk-BIQX33UG-CWmzxeAw.js +1 -0
  185. package/dist/assets/chunk-C7G6YPKG-B_mIUlb7.js +1 -0
  186. package/dist/assets/chunk-CQNSW5MT-CMayolwI.js +1 -0
  187. package/dist/assets/chunk-CYSBUYHQ-lSPbrL3Z.js +1 -0
  188. package/dist/assets/chunk-DECur_0Z.js +1 -0
  189. package/dist/assets/chunk-EMLP6XTP-D9-jtCnM.js +1 -0
  190. package/dist/assets/chunk-EX3LRPZG-C63hVuAJ.js +231 -0
  191. package/dist/assets/chunk-FWX5IMBZ-CboX-WF_.js +2 -0
  192. package/dist/assets/chunk-HOUHSVGY-BOgSd6Un.js +1 -0
  193. package/dist/assets/chunk-ICXQ74PX-C0K-pgZt.js +2 -0
  194. package/dist/assets/chunk-JG7HCLWE-Cib606u_.js +2 -0
  195. package/dist/assets/chunk-JWPE2WC7-DSyBRaRG.js +1 -0
  196. package/dist/assets/chunk-KEIR6QF5-CNOHik0T.js +161 -0
  197. package/dist/assets/chunk-MOJQB5TN-CVMfwN_7.js +88 -0
  198. package/dist/assets/chunk-MOZMSUNE-D6JeZqmE.js +1 -0
  199. package/dist/assets/chunk-OGEWGWER-DLT3pxyw.js +1 -0
  200. package/dist/assets/chunk-OSBZ3O6U-C0W8KSeg.js +1 -0
  201. package/dist/assets/chunk-PUDLZKDR-Cd079yIQ.js +156 -0
  202. package/dist/assets/chunk-Q4XR5HBZ-D0jHa6SO.js +70 -0
  203. package/dist/assets/chunk-QBLGF6JB-p4omp7uO.js +1 -0
  204. package/dist/assets/chunk-R7FJI6CG-CZ_p9hSr.js +1 -0
  205. package/dist/assets/chunk-RYQCIY6F-CZA9pvFX.js +1 -0
  206. package/dist/assets/chunk-U6XO7XAA-CcXKsWxp.js +2 -0
  207. package/dist/assets/chunk-V7JOEXUC-CVDtR75H.js +206 -0
  208. package/dist/assets/chunk-VAUOI2AC-B-W2_9Ot.js +1 -0
  209. package/dist/assets/chunk-VR4S4FIN-FD3kWh32.js +1 -0
  210. package/dist/assets/chunk-WYO6CB5R-C2bGaS5r.js +127 -0
  211. package/dist/assets/chunk-XXDRQBXY-BBXwjhUx.js +1 -0
  212. package/dist/assets/chunk-Y2CYZVJY-Di0sYzi1.js +1 -0
  213. package/dist/assets/chunk-YOTPTUD7-BXHmEk89.js +1 -0
  214. package/dist/assets/chunk-ZGVPDNZ5-ymgts0zo.js +62 -0
  215. package/dist/assets/chunk-ZIRB5QZD-DncwlQQc.js +32 -0
  216. package/dist/assets/classDiagram-OUVF2IWQ-DKW_dtku.js +1 -0
  217. package/dist/assets/classDiagram-v2-EOCWNBFH-C8Fi7-yv.js +1 -0
  218. package/dist/assets/clojure-CvY8A83L.js +1 -0
  219. package/dist/assets/code-dark-Cjb1QGhu.css +1 -0
  220. package/dist/assets/code-mobile-3C2Rr-CN.css +1 -0
  221. package/dist/assets/codicon-ngg6Pgfi.ttf +0 -0
  222. package/dist/assets/coffee-4LX2Sv8A.js +1 -0
  223. package/dist/assets/core-ZlAPicox.js +3 -0
  224. package/dist/assets/cose-bilkent-JH36ORCC-BNIAz0nN.js +1 -0
  225. package/dist/assets/cpp-D4AzSV28.js +1 -0
  226. package/dist/assets/csharp-C4nWN4Yd.js +1 -0
  227. package/dist/assets/csp-CiEaaS5Z.js +1 -0
  228. package/dist/assets/css-DXxBRsbk.js +3 -0
  229. package/dist/assets/css.worker-DyDu5ynT.js +89 -0
  230. package/dist/assets/cssMode-DDWxTPm_.js +1 -0
  231. package/dist/assets/cynefin-VYW2F7L2-DgXGHlRt.js +1 -0
  232. package/dist/assets/cynefinDiagram-TSTJHNR4-CwpobXIl.js +62 -0
  233. package/dist/assets/cypher-DOMzqyFA.js +1 -0
  234. package/dist/assets/cytoscape.esm-Cl8iC3D7.js +321 -0
  235. package/dist/assets/dagre-DXlcHgr3.js +1 -0
  236. package/dist/assets/dagre-VKFMJZFB-Dkhlfjna.js +4 -0
  237. package/dist/assets/dart-BWS7MXt_.js +1 -0
  238. package/dist/assets/defaultLocale-GJwWH1Jr.js +1 -0
  239. package/dist/assets/diagram-FQU43EPY-hA8dicp6.js +3 -0
  240. package/dist/assets/diagram-G47NLZAW-BTY6JTzO.js +24 -0
  241. package/dist/assets/diagram-NH7WQ7WH-xkaHHX9M.js +24 -0
  242. package/dist/assets/diagram-OA4YK3LP-gk_fg9UU.js +30 -0
  243. package/dist/assets/diagram-WEI45ONY-BvC8xd6T.js +41 -0
  244. package/dist/assets/dist-ClAL-Tv5.js +1 -0
  245. package/dist/assets/dockerfile-Baf4U3xT.js +1 -0
  246. package/dist/assets/ebnfDiagram-CCIWWBDH-CR7RaNat.js +1 -0
  247. package/dist/assets/ecl-CT7udlkQ.js +1 -0
  248. package/dist/assets/editor-CtbFZlk0.css +1 -0
  249. package/dist/assets/editor.api2-B9h6iFPK.js +872 -0
  250. package/dist/assets/editor.worker-Dpt_xPrs.js +26 -0
  251. package/dist/assets/elixir-CIbAZw5l.js +1 -0
  252. package/dist/assets/erDiagram-Q63AITRT-_M-JFPbj.js +85 -0
  253. package/dist/assets/eventmodeling-45OFAUF4-DXYTQ2nt.js +1 -0
  254. package/dist/assets/file-icons-EFUGSSwf.js +1 -0
  255. package/dist/assets/flow9-Dqiw45WV.js +1 -0
  256. package/dist/assets/flowDiagram-23GEKE2U-DAiS5Y7G.js +1 -0
  257. package/dist/assets/freemarker2-QBsTERH-.js +3 -0
  258. package/dist/assets/fsharp-E8E2L9jU.js +1 -0
  259. package/dist/assets/ganttDiagram-NO4QXBWP-Bl8P1yf_.js +292 -0
  260. package/dist/assets/gitGraph-TEB2WS4Q-CzTBjEvV.js +1 -0
  261. package/dist/assets/gitGraphDiagram-IHSO6WYX-DoHa5ysz.js +106 -0
  262. package/dist/assets/go-CouVSN4D.js +1 -0
  263. package/dist/assets/graphlib-CbSvZcZU.js +1 -0
  264. package/dist/assets/graphql-a6h6h5-t.js +1 -0
  265. package/dist/assets/handlebars-CuoxPo5A.js +1 -0
  266. package/dist/assets/hcl-vso3e6N-.js +1 -0
  267. package/dist/assets/html-gX5PEu0Q.js +1 -0
  268. package/dist/assets/html.worker-IWNXhnIC.js +502 -0
  269. package/dist/assets/htmlMode-DIWUbUxd.js +1 -0
  270. package/dist/assets/index-BpHPeJf0.js +10 -0
  271. package/dist/assets/info-DKCQHKI2-DznP7j3s.js +1 -0
  272. package/dist/assets/infoDiagram-FWYZ7A6U-DSFcSuUs.js +2 -0
  273. package/dist/assets/ini-SwZZkotR.js +1 -0
  274. package/dist/assets/init-BPLPMQ3Y.js +1 -0
  275. package/dist/assets/ishikawaDiagram-FXEZZL3T-DdkqullI.js +70 -0
  276. package/dist/assets/java-CtRLC4_3.js +1 -0
  277. package/dist/assets/javascript-BYKGIUGs.js +1 -0
  278. package/dist/assets/journeyDiagram-5HDEW3XC-B4hRnQJr.js +139 -0
  279. package/dist/assets/json.worker-c4h5s80L.js +58 -0
  280. package/dist/assets/jsonMode-AFmQQP6u.js +7 -0
  281. package/dist/assets/jsx-runtime-BthL93pI.js +1 -0
  282. package/dist/assets/julia-CuJWFKQH.js +1 -0
  283. package/dist/assets/kanban-definition-HUTT4EX6-tm2FZB2E.js +89 -0
  284. package/dist/assets/katex-C8DPyI6s.js +257 -0
  285. package/dist/assets/kotlin-B_kBhsre.js +1 -0
  286. package/dist/assets/less-CtrHIfz3.js +2 -0
  287. package/dist/assets/lexon-yVt1pJLu.js +1 -0
  288. package/dist/assets/line-CmsjK4kn.js +1 -0
  289. package/dist/assets/linear-Dld3_ERu.js +1 -0
  290. package/dist/assets/liquid-DBIl-f0z.js +1 -0
  291. package/dist/assets/lspLanguageFeatures-DK6tqicV.js +4 -0
  292. package/dist/assets/lua-prOwgJ4U.js +1 -0
  293. package/dist/assets/m3-DhrdTQ84.js +1 -0
  294. package/dist/assets/main-DTJwH51M.css +1 -0
  295. package/dist/assets/map-LZWWoeWa.js +1 -0
  296. package/dist/assets/markdown-DoI5HwSM.js +1 -0
  297. package/dist/assets/mdx-DS-bVtZN.js +1 -0
  298. package/dist/assets/mermaid-parser.core-BQtepoLd.js +4 -0
  299. package/dist/assets/mermaid.core-C7UT_EEL.js +11 -0
  300. package/dist/assets/mindmap-definition-LN4V7U3C-DplfjGms.js +96 -0
  301. package/dist/assets/mips-Dye5jsBU.js +1 -0
  302. package/dist/assets/monaco.contribution-CyLosfZI.js +2 -0
  303. package/dist/assets/msdax-TkNOyIL8.js +1 -0
  304. package/dist/assets/mysql-CEc-RKZF.js +1 -0
  305. package/dist/assets/objective-c-CqqCsQz0.js +1 -0
  306. package/dist/assets/ordinal-BLrOss5K.js +1 -0
  307. package/dist/assets/packet-7NZHBO7P-BDEPOPha.js +1 -0
  308. package/dist/assets/pascal-DO_pj8yg.js +1 -0
  309. package/dist/assets/pascaligo-1cbdZdF3.js +1 -0
  310. package/dist/assets/path-YdFzr2W6.js +1 -0
  311. package/dist/assets/pegDiagram-2B236MQR-lu3SdkQs.js +1 -0
  312. package/dist/assets/perl-B_N3PYfC.js +1 -0
  313. package/dist/assets/pgsql-9iYk1FQ7.js +1 -0
  314. package/dist/assets/php-rO2sgoUM.js +1 -0
  315. package/dist/assets/pie-RZYD4A2V-DWIrwPJQ.js +1 -0
  316. package/dist/assets/pieDiagram-ENE6RG2P-3fHbmDf9.js +39 -0
  317. package/dist/assets/pla-Cbgv60kq.js +1 -0
  318. package/dist/assets/postiats-y6xrhy_M.js +1 -0
  319. package/dist/assets/powerquery-BaD8gWNE.js +1 -0
  320. package/dist/assets/powershell-BMhG4JVe.js +1 -0
  321. package/dist/assets/preload-helper-mJowh_hw.js +1 -0
  322. package/dist/assets/protobuf-CdOP5kN3.js +2 -0
  323. package/dist/assets/pug-Ck60hNF9.js +1 -0
  324. package/dist/assets/python-C93q3xXz.js +1 -0
  325. package/dist/assets/qoder-C9LmmOSf.svg +1 -0
  326. package/dist/assets/qoder-Cf9gl0Y5.svg +1 -0
  327. package/dist/assets/qoder-gHCinseV.svg +1 -0
  328. package/dist/assets/qrcode-DV-_rjU8.js +3 -0
  329. package/dist/assets/qsharp-BcaKFfP-.js +1 -0
  330. package/dist/assets/quadrantDiagram-ABIIQ3AL-a5qoWz-X.js +7 -0
  331. package/dist/assets/r-rFNes0iK.js +1 -0
  332. package/dist/assets/radar-I7S5WNFK-B7OaD8vp.js +1 -0
  333. package/dist/assets/railroad-3IZDKUUU-BXW2nuCA.js +1 -0
  334. package/dist/assets/railroad-abnf-AHOZXSZD-Buw9QG2r.js +1 -0
  335. package/dist/assets/railroad-ebnf-EBAXGLYW-BjSi4sWJ.js +1 -0
  336. package/dist/assets/railroad-peg-LSFZ7HO6-DN1wIVhS.js +1 -0
  337. package/dist/assets/railroadDiagram-RFXS5EU6-D2TOuXW9.js +1 -0
  338. package/dist/assets/razor-CBifNGXO.js +1 -0
  339. package/dist/assets/react-dom-gtU6X-Hj.js +1 -0
  340. package/dist/assets/redis-DkpQ3K6G.js +1 -0
  341. package/dist/assets/redshift-Di9N5TI3.js +1 -0
  342. package/dist/assets/requirementDiagram-TGXJPOKE-LQBWpUvd.js +84 -0
  343. package/dist/assets/restructuredtext-By5ZzLt6.js +1 -0
  344. package/dist/assets/review-BcnwQtJv.css +1 -0
  345. package/dist/assets/rough.esm-c4PR5shF.js +1 -0
  346. package/dist/assets/ruby-fF5zGiLp.js +1 -0
  347. package/dist/assets/rust-C5b3AYY6.js +1 -0
  348. package/dist/assets/sankeyDiagram-HTMAVEWB-B1ohYoyK.js +40 -0
  349. package/dist/assets/sb-CD07UNRs.js +1 -0
  350. package/dist/assets/scala-jtn1ip9G.js +1 -0
  351. package/dist/assets/scheme-C4vqMkn_.js +1 -0
  352. package/dist/assets/scss-I4Glxjbz.js +3 -0
  353. package/dist/assets/sequenceDiagram-DBY2YBRQ-C3vM-hMq.js +162 -0
  354. package/dist/assets/shell-DubUcuns.js +1 -0
  355. package/dist/assets/sizeCapture-X5ZJPWSS-daTB6OcF.js +1 -0
  356. package/dist/assets/solidity-CNy2KSl2.js +1 -0
  357. package/dist/assets/sophia-D9_URt84.js +1 -0
  358. package/dist/assets/sparql-DviXpm4C.js +1 -0
  359. package/dist/assets/sql-vDRbAZgK.js +1 -0
  360. package/dist/assets/src-WBW1QxJt.js +1 -0
  361. package/dist/assets/st-CnpnRqA7.js +1 -0
  362. package/dist/assets/stateDiagram-2N3HPSRC-BvFhOryI.js +1 -0
  363. package/dist/assets/stateDiagram-v2-6OUMAXLB-WMrz4PNo.js +1 -0
  364. package/dist/assets/swift-D_g8Ganz.js +1 -0
  365. package/dist/assets/swimlanes-5IMT3BWC-DKF_b-2s.js +2 -0
  366. package/dist/assets/swimlanesDiagram-G3AALYLV-BGswC9oC.js +8 -0
  367. package/dist/assets/systemverilog-OuZa5h1M.js +1 -0
  368. package/dist/assets/tcl-DBm9ucsV.js +1 -0
  369. package/dist/assets/timeline-definition-FHXFAJF6-vAs11ux_.js +120 -0
  370. package/dist/assets/tokens-BwenNn5Q.css +1 -0
  371. package/dist/assets/treeView-QDETBFTQ-BVfICaZQ.js +1 -0
  372. package/dist/assets/treemap-6X3UGDF4-oWHPqo17.js +1 -0
  373. package/dist/assets/ts.worker-BwM5Ha3L.js +67719 -0
  374. package/dist/assets/tsMode-DGZTlTj8.js +11 -0
  375. package/dist/assets/twig-BRzEqX5Y.js +1 -0
  376. package/dist/assets/typescript-KJKfYO7K.js +1 -0
  377. package/dist/assets/typespec-D4ZgGeX8.js +1 -0
  378. package/dist/assets/useWorkspaceMenuKeyboard-CneKAZUJ.js +1 -0
  379. package/dist/assets/vb-DcP4s6WS.js +1 -0
  380. package/dist/assets/vennDiagram-L72KCM5P-BybTX_9B.js +34 -0
  381. package/dist/assets/wardley-OPB4EBWU-Dg8t5y3s.js +1 -0
  382. package/dist/assets/wardleyDiagram-EHGQE667-CicUmMBR.js +78 -0
  383. package/dist/assets/wgsl-CnRJ_QUf.js +298 -0
  384. package/dist/assets/workers-BSQRIkqa.js +1 -0
  385. package/dist/assets/workspace-view-state-CTyDzk2D.js +1 -0
  386. package/dist/assets/xml-C8FAcN6j.js +1 -0
  387. package/dist/assets/xychartDiagram-FW5EYKEG-BhWDyCX-.js +7 -0
  388. package/dist/assets/yaml-BQ4NrRhG.js +1 -0
  389. package/dist/assets/zsh-CLpveKlF.svg +1 -0
  390. package/dist/assets/zsh-FxSpMPbz.svg +1 -0
  391. package/dist/farming-2/app-icon-v2-180.png +0 -0
  392. package/dist/farming-2/app-icon-v2-192.png +0 -0
  393. package/dist/farming-2/app-icon-v2-512.png +0 -0
  394. package/dist/farming-2/app-icon-v2-maskable-512.png +0 -0
  395. package/dist/farming-2/app-icon-v2-master.png +0 -0
  396. package/dist/farming-2/favicon-v2-16.png +0 -0
  397. package/dist/farming-2/favicon-v2-32.png +0 -0
  398. package/dist/farming-2/favicon-v2-48.png +0 -0
  399. package/dist/farming-2/favicon-v2-64.png +0 -0
  400. package/dist/farming-2/favicon-v2.ico +0 -0
  401. package/dist/farming-2/favicon.ico +0 -0
  402. package/dist/farming-2/github-social-preview.png +0 -0
  403. package/dist/farming-2/images/avatar-watercolor-v1-bee-garden.png +0 -0
  404. package/dist/farming-2/images/avatar-watercolor-v2.png +0 -0
  405. package/dist/farming-2/images/avatar-watercolor-v3.png +0 -0
  406. package/dist/farming-2/legacy-icons-v1/favicon.ico +1 -0
  407. package/dist/farming-2/legacy-icons-v1/logo-16.png +0 -0
  408. package/dist/farming-2/legacy-icons-v1/logo-180.png +0 -0
  409. package/dist/farming-2/legacy-icons-v1/logo-192.png +0 -0
  410. package/dist/farming-2/legacy-icons-v1/logo-32.png +0 -0
  411. package/dist/farming-2/legacy-icons-v1/logo-48.png +0 -0
  412. package/dist/farming-2/legacy-icons-v1/logo-512.png +0 -0
  413. package/dist/farming-2/legacy-icons-v1/logo-64.png +0 -0
  414. package/dist/farming-2/logo-16.png +0 -0
  415. package/dist/farming-2/logo-180.png +0 -0
  416. package/dist/farming-2/logo-192.png +0 -0
  417. package/dist/farming-2/logo-32.png +0 -0
  418. package/dist/farming-2/logo-48.png +0 -0
  419. package/dist/farming-2/logo-512.png +0 -0
  420. package/dist/farming-2/logo-64.png +0 -0
  421. package/dist/farming-2/site.webmanifest +27 -0
  422. package/dist/farming-2/style_capsules/watercolor-pastoral.json +94 -0
  423. package/dist/index.html +27 -0
  424. package/frontend/ghostty-loader.js +24 -0
  425. package/frontend/runtime-paths.js +28 -0
  426. package/frontend/session-bridge.js +82 -0
  427. package/frontend/session-modal-bridge.js +331 -0
  428. package/frontend/skin-bridge.js +25 -0
  429. package/frontend/skins/crt/app.js +6940 -0
  430. package/frontend/skins/crt/assets/branding/farming-crt-icon.svg +11 -0
  431. package/frontend/skins/crt/assets/branding/farming-crt-logo-v1.png +0 -0
  432. package/frontend/skins/crt/assets/effects/phosphor-noise.svg +11 -0
  433. package/frontend/skins/crt/assets/fonts/departure-mono/DepartureMonoNerdFontMono-Regular.otf +0 -0
  434. package/frontend/skins/crt/assets/fonts/departure-mono/LICENSE +93 -0
  435. package/frontend/skins/crt/effects.js +8 -0
  436. package/frontend/skins/crt/index.html +1712 -0
  437. package/frontend/skins/crt/styles/billing.css +1003 -0
  438. package/frontend/skins/crt/styles/effects.css +171 -0
  439. package/frontend/skins/crt/styles/history.css +215 -0
  440. package/frontend/skins/crt/styles/monochrome-green.css +509 -0
  441. package/frontend/skins/crt/styles/search.css +271 -0
  442. package/frontend/terminal-bridge.js +192 -0
  443. package/frontend/theme-loader.js +84 -0
  444. package/frontend/themes/terminal/theme.json +24 -0
  445. package/frontend/vendor/ghostty-web/LICENSE +21 -0
  446. package/frontend/vendor/ghostty-web/__vite-browser-external-2447137e.js +4 -0
  447. package/frontend/vendor/ghostty-web/ghostty-vt.wasm +0 -0
  448. package/frontend/vendor/ghostty-web/ghostty-web.js +2963 -0
  449. package/frontend/vendor/ghostty-web/ghostty-web.umd.cjs +13 -0
  450. package/frontend/vendor/ghostty-web/index.d.ts +1842 -0
  451. package/index.html +23 -0
  452. package/package.json +144 -0
  453. package/scripts/sync-ghostty-vendor.js +66 -0
@@ -0,0 +1,4259 @@
1
+ const EventEmitter = require('events');
2
+ const { execFile } = require('child_process');
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+ const { promisify } = require('util');
7
+ const SystemMonitor = require('./system-monitor');
8
+ const SessionEngineBridge = require('./session-engine-bridge');
9
+ const { isSupportedHistoryAgent, resolveLaunchCommand } = require('./cli-agents');
10
+ const { buildAgentSessionResumeCommand, findAgentSession } = require('./agent-session-history');
11
+ const { listCodexSessions } = require('./codex-session-history');
12
+ const { buildAgentProviderSessionPlan, sessionFromExactResumeSource } = require('./agent-provider-session');
13
+ const { resolveAgentExecutable, resolveCompatibleCodexExecutable } = require('./executable-discovery');
14
+ const { ensureMainAgentSkillFiles, renderMainAgentBootstrap } = require('./main-agent-skills');
15
+ const { mainPageAgentSessionKey, resumedAgentSource } = require('./main-page-session');
16
+ const { isSafeProviderSessionId, isTemporaryProviderSessionId } = require('./provider-session-id');
17
+ const { deriveTerminalStatus } = require('./terminal-status');
18
+ const { CodexAppServerRuntime, normalizeCodexRuntimeMode } = require('./codex-app-server-runtime');
19
+ const { JsonCliRuntime } = require('./json-cli-runtime');
20
+ const { AcpRuntime } = require('./acp-runtime');
21
+ const { acpToolChanges, acpToolDetail, acpToolReviewChanges } = require('./acp-transcript');
22
+ const { ensureCodexAppServerHome } = require('./codex-app-server-home');
23
+ const {
24
+ ensureAgentOrders,
25
+ finiteOrder,
26
+ nextPinnedOrder,
27
+ reorderedPinnedAgentOrders,
28
+ reorderedProjectAgentOrders,
29
+ } = require('./agent-order');
30
+ const {
31
+ buildInteractiveAgentBaseEnv,
32
+ normalizeInteractiveTerminalEnv,
33
+ resolveUserShellEnvSync,
34
+ } = require('./agent-env');
35
+
36
+ const SESSION_OUTPUT_LIMIT = 10000;
37
+ const AGENT_USAGE_RATE_WINDOW_MS = 5 * 60 * 1000;
38
+ const ACTIVITY_UPDATE_INTERVAL_MS = 1000;
39
+ const ACTIVITY_HOT_SEC = 30 * 60;
40
+ const ACTIVITY_WARM_SEC = 3 * 60 * 60;
41
+ const ACTIVITY_COOL_SEC = 12 * 60 * 60;
42
+ const ZOMBIE_IDLE_MS = 72 * 60 * 60 * 1000;
43
+ const ZOMBIE_SWEEP_INTERVAL_MS = 60 * 1000;
44
+ const INPUT_SESSION_RETRY_DELAYS_MS = [25, 50, 100, 180, 300, 500];
45
+ const MISSING_ENGINE_SESSION_STARTUP_GRACE_MS = 5000;
46
+ const MIN_TERMINAL_RESIZE_COLS = 40;
47
+ const MIN_TERMINAL_RESIZE_ROWS = 10;
48
+ const CODEX_PROVIDER_SESSION_RESOLVE_COOLDOWN_MS = 1000;
49
+ const CODEX_PROVIDER_SESSION_MATCH_GRACE_MS = 30 * 1000;
50
+ const PROVIDER_SESSION_TITLE_RESOLVE_COOLDOWN_MS = 30 * 1000;
51
+ const AGENT_DISCOVERY_CACHE_MAX_AGE_MS = 3_000;
52
+ const SHELL_PROMPT_ENV_KEYS = [
53
+ 'PS1',
54
+ 'PS2',
55
+ 'PS3',
56
+ 'PS4',
57
+ 'PROMPT',
58
+ 'RPROMPT',
59
+ 'RPS1',
60
+ 'PROMPT_COMMAND',
61
+ ];
62
+ const execFileAsync = promisify(execFile);
63
+
64
+ function trimSessionOutput(output) {
65
+ const text = typeof output === 'string' ? output : '';
66
+ return text.length > SESSION_OUTPUT_LIMIT ? text.slice(-SESSION_OUTPUT_LIMIT) : text;
67
+ }
68
+
69
+ function hasResumeArg(args) {
70
+ return Array.isArray(args)
71
+ ? args.some(arg => arg === '--resume' || (typeof arg === 'string' && arg.startsWith('--resume=')))
72
+ : false;
73
+ }
74
+
75
+ function isSameOrDescendantPath(root, target) {
76
+ const relative = path.relative(root, target);
77
+ return relative === '' || Boolean(relative) && !relative.startsWith('..') && !path.isAbsolute(relative);
78
+ }
79
+
80
+ function resumedSessionFromSource(source) {
81
+ return sessionFromExactResumeSource(source);
82
+ }
83
+
84
+ function agentProgramName(command) {
85
+ const executable = String(command || '')
86
+ .trim()
87
+ .split(/\s+/)
88
+ .find(token => token !== 'env' && !/^[A-Za-z_][A-Za-z0-9_]*=/.test(token));
89
+ return path.basename(executable || '');
90
+ }
91
+
92
+ function agentHomeProviderForProgram(command) {
93
+ const program = agentProgramName(command).toLowerCase();
94
+ if (program === 'qodercli') return 'qoder';
95
+ return ['codex', 'claude', 'opencode', 'qoder'].includes(program) ? program : '';
96
+ }
97
+
98
+ function hasUsableCodexAppServerHome(metadata) {
99
+ const homePath = String(metadata?.codexAppServerHomePath || '').trim();
100
+ return Boolean(homePath) && fs.existsSync(homePath);
101
+ }
102
+
103
+ function shouldRecoverAsCodexAppServer(metadata) {
104
+ const provider = metadata?.providerSessionProvider || metadata?.provider || '';
105
+ return provider === 'codex'
106
+ && metadata?.codexRuntimeMode === 'app-server'
107
+ && hasUsableCodexAppServerHome(metadata);
108
+ }
109
+
110
+ function isJsonCliAgent(agent) {
111
+ return agent && agent.agentRuntimeMode === 'json';
112
+ }
113
+
114
+ function isAcpAgent(agent) {
115
+ return agent && agent.agentRuntimeMode === 'acp';
116
+ }
117
+
118
+ function isShellProgram(command) {
119
+ return ['bash', 'zsh', 'sh', 'fish'].includes(agentProgramName(command).toLowerCase());
120
+ }
121
+
122
+ function isEphemeralShellAgent(agent) {
123
+ return agent && isShellProgram(agent.forkCommand || agent.command || '');
124
+ }
125
+
126
+ function terminalRuntimeStatus(agentStatus) {
127
+ return agentStatus === 'stopped' || agentStatus === 'dead' ? 'exited' : agentStatus;
128
+ }
129
+
130
+ function finiteNumberOrNull(value) {
131
+ return typeof value === 'number' && Number.isFinite(value) ? value : null;
132
+ }
133
+
134
+ function finiteNonNegativeInteger(value) {
135
+ return typeof value === 'number' && Number.isFinite(value) && value >= 0
136
+ ? Math.floor(value)
137
+ : 0;
138
+ }
139
+
140
+ function deriveAgentTerminalStatus(agent, overrides = {}) {
141
+ const terminalBusy = Object.prototype.hasOwnProperty.call(overrides, 'terminalBusy')
142
+ ? overrides.terminalBusy
143
+ : agent.terminalBusy;
144
+ return deriveTerminalStatus({
145
+ command: agent.forkCommand || agent.command,
146
+ cwd: overrides.cwd || agent.shellCwd || agent.cwd,
147
+ status: overrides.status || terminalRuntimeStatus(agent.status),
148
+ title: Object.prototype.hasOwnProperty.call(overrides, 'title')
149
+ ? overrides.title
150
+ : (agent.sessionTitle || ''),
151
+ previewText: Object.prototype.hasOwnProperty.call(overrides, 'previewText')
152
+ ? overrides.previewText
153
+ : (agent.previewText || agent.output || ''),
154
+ terminalBusy: typeof terminalBusy === 'boolean' ? terminalBusy : null,
155
+ shellLastExitCode: typeof agent.shellLastExitCode === 'number' ? agent.shellLastExitCode : null,
156
+ shellLastEvent: agent.shellLastEvent || '',
157
+ shellCommand: Object.prototype.hasOwnProperty.call(overrides, 'shellCommand')
158
+ ? overrides.shellCommand
159
+ : (agent.shellCommand || ''),
160
+ shellLastCommand: Object.prototype.hasOwnProperty.call(overrides, 'shellLastCommand')
161
+ ? overrides.shellLastCommand
162
+ : (agent.shellLastCommand || ''),
163
+ shellCommandStartedAt: Object.prototype.hasOwnProperty.call(overrides, 'shellCommandStartedAt')
164
+ ? overrides.shellCommandStartedAt
165
+ : finiteNumberOrNull(agent.shellCommandStartedAt),
166
+ shellLastCommandStartedAt: Object.prototype.hasOwnProperty.call(overrides, 'shellLastCommandStartedAt')
167
+ ? overrides.shellLastCommandStartedAt
168
+ : finiteNumberOrNull(agent.shellLastCommandStartedAt),
169
+ shellLastCommandFinishedAt: Object.prototype.hasOwnProperty.call(overrides, 'shellLastCommandFinishedAt')
170
+ ? overrides.shellLastCommandFinishedAt
171
+ : finiteNumberOrNull(agent.shellLastCommandFinishedAt),
172
+ shellLastCommandDurationMs: Object.prototype.hasOwnProperty.call(overrides, 'shellLastCommandDurationMs')
173
+ ? overrides.shellLastCommandDurationMs
174
+ : finiteNumberOrNull(agent.shellLastCommandDurationMs),
175
+ });
176
+ }
177
+
178
+ function agentAttentionUnread(agent) {
179
+ return finiteNonNegativeInteger(agent && agent.attentionSeq) > finiteNonNegativeInteger(agent && agent.readAttentionSeq);
180
+ }
181
+
182
+ function hasAgentOutputAfterAttentionBaseline(agent) {
183
+ if (!agent || agent.attentionRequiresNewOutput !== true) return true;
184
+ const baselineSeq = finiteNumberOrNull(agent.attentionBaselineOutputSeq);
185
+ const lastOutputSeq = finiteNumberOrNull(agent.lastOutputSeq);
186
+ if (baselineSeq !== null && lastOutputSeq !== null) {
187
+ return lastOutputSeq > baselineSeq;
188
+ }
189
+
190
+ const baselineAt = finiteNumberOrNull(agent.attentionBaselineOutputAt);
191
+ const lastOutputAt = finiteNumberOrNull(agent.lastEngineOutputAt);
192
+ if (baselineAt !== null && lastOutputAt !== null) {
193
+ return lastOutputAt > baselineAt;
194
+ }
195
+
196
+ return false;
197
+ }
198
+
199
+ function shouldRecoverEngineSession(metadata) {
200
+ if (!metadata) return false;
201
+ if (metadata.category === 'shell') return false;
202
+ return !isShellProgram(metadata.forkCommand || metadata.command || '');
203
+ }
204
+
205
+ function recoveredEngineSessionId(entry, metadata = {}) {
206
+ return entry && (entry.sessionId || entry.agentId || metadata.agentId) || '';
207
+ }
208
+
209
+ function agentDisplayName(command) {
210
+ const program = agentProgramName(command).toLowerCase();
211
+ if (program === 'codex') return 'codex';
212
+ if (program === 'claude') return 'claude code';
213
+ return program;
214
+ }
215
+
216
+ function titleComparisonKey(title) {
217
+ return String(title || '')
218
+ .trim()
219
+ .replace(/^[\s**✳✱✲✶·•:.\u2800-\u28FF]+/u, '')
220
+ .replace(/\s+/g, ' ')
221
+ .toLowerCase();
222
+ }
223
+
224
+ function agentWorkspaceTitleKeys(agent) {
225
+ return [agent && agent.cwd, agent && agent.projectWorkspace]
226
+ .filter(value => typeof value === 'string' && value.trim().length > 0)
227
+ .map(value => path.basename(String(value).replace(/[\\/]+$/, '')))
228
+ .filter(Boolean)
229
+ .map(titleComparisonKey);
230
+ }
231
+
232
+ function isGenericSessionTitle(agent, title) {
233
+ const normalizedTitle = titleComparisonKey(title);
234
+ if (!normalizedTitle) return true;
235
+
236
+ const program = agentProgramName(agent && agent.command).toLowerCase();
237
+ const displayName = agentDisplayName(agent && agent.command);
238
+ const genericTitles = new Set([
239
+ program,
240
+ displayName,
241
+ `${program} session`,
242
+ `${displayName} session`,
243
+ 'main agent',
244
+ 'farming',
245
+ ].filter(Boolean));
246
+
247
+ if (genericTitles.has(normalizedTitle)) return true;
248
+ return agentWorkspaceTitleKeys(agent).includes(normalizedTitle);
249
+ }
250
+
251
+ function interruptInputForAgent(agent) {
252
+ const program = agentProgramName(agent && agent.command);
253
+ if (program === 'codex' || program === 'claude' || program === 'qodercli') {
254
+ return '\x1b';
255
+ }
256
+ return '\x03';
257
+ }
258
+
259
+ function sleep(ms) {
260
+ return new Promise(resolve => setTimeout(resolve, ms));
261
+ }
262
+
263
+ function normalizePathValue(value) {
264
+ if (typeof value !== 'string') return '';
265
+ const trimmed = value.trim();
266
+ if (!trimmed || trimmed === path.sep) return trimmed;
267
+ return trimmed.replace(/[\\/]+$/, '');
268
+ }
269
+
270
+ function timestampMs(value) {
271
+ const parsed = Date.parse(value || '');
272
+ return Number.isFinite(parsed) ? parsed : 0;
273
+ }
274
+
275
+ function normalizePositiveInteger(value, fallback, min, max) {
276
+ const parsed = Math.floor(Number(value));
277
+ if (!Number.isFinite(parsed)) return fallback;
278
+ return Math.max(min, Math.min(max, parsed));
279
+ }
280
+
281
+ function isSessionNotAvailableError(error) {
282
+ const message = String(error && (error.message || error));
283
+ return /Session not available/i.test(message) ||
284
+ /Native PTY host (?:failed to start or connect|is not reachable)/i.test(message);
285
+ }
286
+
287
+ function isRunningAgentRuntimeStatus(status) {
288
+ return String(status || '').toLowerCase() === 'running';
289
+ }
290
+
291
+ function isCodexAppServerAgent(agent) {
292
+ return agent && agent.codexRuntimeMode === 'app-server';
293
+ }
294
+
295
+ function isLiveEngineSessionState(sessionState) {
296
+ return Boolean(sessionState && sessionState.status && sessionState.status !== 'exited');
297
+ }
298
+
299
+ function timestampSlug(now = new Date()) {
300
+ const pad = (value) => String(value).padStart(2, '0');
301
+ return [
302
+ now.getFullYear(),
303
+ pad(now.getMonth() + 1),
304
+ pad(now.getDate()),
305
+ '-',
306
+ pad(now.getHours()),
307
+ pad(now.getMinutes()),
308
+ pad(now.getSeconds()),
309
+ ].join('');
310
+ }
311
+
312
+ function isFarmingForkWorktreePath(workspace) {
313
+ const basename = path.basename(String(workspace || '').replace(/[\\/]+$/, ''));
314
+ return /-farming-fork-\d{8}-\d{6}(?:-\d+)?$/.test(basename);
315
+ }
316
+
317
+ function statusEntriesFromPorcelain(output) {
318
+ return String(output || '')
319
+ .split(/\r?\n/)
320
+ .map(line => line.trimEnd())
321
+ .filter(Boolean);
322
+ }
323
+
324
+ class AgentManager extends EventEmitter {
325
+ constructor(configManager, options = {}) {
326
+ super();
327
+ this.configManager = configManager;
328
+ this.controlUrl = options.controlUrl || '';
329
+ this.tokenFile = options.tokenFile || '';
330
+ this.authDisabled = options.authDisabled === true;
331
+ this.cliBinDir = options.cliBinDir || path.join(__dirname, '..', 'bin');
332
+ this.agentShellEnvProvider = typeof options.agentShellEnvProvider === 'function'
333
+ ? options.agentShellEnvProvider
334
+ : shell => resolveUserShellEnvSync({ processEnv: process.env, shell });
335
+ this.agentShellEnvCache = new Map();
336
+ this.agentShellEnvCacheMs = normalizePositiveInteger(
337
+ process.env.FARMING_AGENT_SHELL_ENV_CACHE_MS,
338
+ 5 * 60 * 1000,
339
+ 0,
340
+ 60 * 60 * 1000
341
+ );
342
+ this.agents = new Map();
343
+ this.mainAgentId = null;
344
+ this.lastActivity = new Map();
345
+ this.lastActivityUpdate = new Map();
346
+ this.outputEvents = new Map(); // Map<agentId, Array<{timestamp, bytes}>> for rate tracking
347
+ this.lastResizeByAgent = new Map();
348
+ this.inputQueues = new Map();
349
+ this.codexSessionResolveInFlight = new Map();
350
+ this.codexSessionResolveLastAttemptAt = new Map();
351
+ this.providerSessionTitleResolveInFlight = new Map();
352
+ this.providerSessionTitleResolveLastAttemptAt = new Map();
353
+ this.permissionRestartInFlight = new Map();
354
+ this.runtimeRestartInFlight = new Map();
355
+ this.permissionRestartSuppressedAgentIds = new Set();
356
+ this.codexAppServerRuntime = options.codexAppServerRuntime || new CodexAppServerRuntime();
357
+ this.jsonCliRuntime = options.jsonCliRuntime || new JsonCliRuntime();
358
+ this.acpRuntime = options.acpRuntime || new AcpRuntime(
359
+ process.env.FARMING_E2E_FAKE_ACP_AGENT === '1'
360
+ ? {
361
+ resolveLaunch: () => ({
362
+ command: process.execPath,
363
+ args: [path.join(__dirname, 'tests', 'fixtures', 'fake-acp-agent.mjs')],
364
+ version: 'e2e',
365
+ }),
366
+ }
367
+ : undefined
368
+ );
369
+ this.heartbeatInterval = null;
370
+ this.disposed = false;
371
+ this.systemMonitor = new SystemMonitor();
372
+ this.startTime = Date.now();
373
+ this.engineBridge = new SessionEngineBridge(configManager);
374
+ this.recoveryPromise = Promise.resolve();
375
+ this.taskHistory = (this.configManager && this.configManager.getTaskHistory)
376
+ ? [...this.configManager.getTaskHistory()]
377
+ : [];
378
+ this.lastZombieSweepAt = 0;
379
+ this.startHeartbeat();
380
+ this.bindEngineEvents();
381
+ this.bindCodexAppServerRuntimeEvents();
382
+ this.bindJsonCliRuntimeEvents();
383
+ this.bindAcpRuntimeEvents();
384
+ if (this.configManager && this.configManager.farmingDir) {
385
+ this.recoveryPromise = this.recoverEngineSessions().catch((error) => {
386
+ console.warn('Failed to recover engine sessions:', error && (error.message || error));
387
+ });
388
+ }
389
+ }
390
+
391
+ bindJsonCliRuntimeEvents() {
392
+ if (!this.jsonCliRuntime || typeof this.jsonCliRuntime.on !== 'function') return;
393
+ this.jsonCliRuntime.on('agent-runtime', ({ agentId, state, error, sessionId }) => {
394
+ const agent = this.agents.get(agentId);
395
+ if (!agent) return;
396
+ agent.jsonCliState = state || '';
397
+ agent.jsonCliError = error || '';
398
+ if (sessionId) {
399
+ agent.providerSessionId = sessionId;
400
+ agent.providerSessionTemporary = false;
401
+ agent.providerSessionKey = this.providerSessionKey(
402
+ agent.providerSessionProvider,
403
+ sessionId,
404
+ agent.providerHomeId || 'default'
405
+ );
406
+ this.ensurePersistentAgentSession(agent);
407
+ }
408
+ this.lastActivity.set(agentId, Date.now());
409
+ this.emit('update');
410
+ });
411
+ this.jsonCliRuntime.on('transcript', ({ agentId }) => {
412
+ const agent = this.agents.get(agentId);
413
+ if (!agent) return;
414
+ agent.jsonCliEvents = this.jsonCliRuntime.getEvents(agentId);
415
+ agent.jsonCliTranscriptUpdatedAt = new Date().toISOString();
416
+ this.emit('update');
417
+ });
418
+ }
419
+
420
+ bindAcpRuntimeEvents() {
421
+ if (!this.acpRuntime || typeof this.acpRuntime.on !== 'function') return;
422
+ this.acpRuntime.on('agent-runtime', ({ agentId, state, error, sessionId, stopReason, pendingPermission, pendingPermissions, pendingElicitation, pendingElicitations, activeElicitations, updatedAt }) => {
423
+ const agent = this.agents.get(agentId);
424
+ if (!agent) return;
425
+ agent.acpState = state || '';
426
+ agent.acpError = error || '';
427
+ agent.acpStopReason = stopReason || '';
428
+ agent.acpPendingPermission = pendingPermission || null;
429
+ agent.acpPendingPermissions = Array.isArray(pendingPermissions) ? pendingPermissions : [];
430
+ agent.acpPendingElicitation = pendingElicitation || null;
431
+ agent.acpPendingElicitations = Array.isArray(pendingElicitations) ? pendingElicitations : [];
432
+ agent.acpActiveElicitations = Array.isArray(activeElicitations) ? activeElicitations : [];
433
+ agent.acpSessionUpdatedAt = updatedAt || '';
434
+ if (sessionId) {
435
+ agent.providerSessionId = sessionId;
436
+ agent.providerSessionTemporary = false;
437
+ agent.providerSessionKey = this.providerSessionKey(
438
+ agent.providerSessionProvider,
439
+ sessionId,
440
+ agent.providerHomeId || 'default'
441
+ );
442
+ this.ensurePersistentAgentSession(agent);
443
+ }
444
+ if (state === 'working' || state === 'waiting-for-permission' || state === 'waiting-for-input') this.lastActivity.set(agentId, Date.now());
445
+ this.emit('update');
446
+ });
447
+ this.acpRuntime.on('session', ({ agentId, revision }) => {
448
+ const agent = this.agents.get(agentId);
449
+ if (!agent) return;
450
+ agent.acpSessionUpdatedAt = new Date().toISOString();
451
+ agent.acpSessionRevision = Number.isFinite(Number(revision)) ? Number(revision) : (Number(agent.acpSessionRevision) || 0) + 1;
452
+ this.emit('update');
453
+ });
454
+ }
455
+
456
+ bindCodexAppServerRuntimeEvents() {
457
+ if (!this.codexAppServerRuntime || typeof this.codexAppServerRuntime.on !== 'function') return;
458
+ this.codexAppServerRuntime.on('agent-runtime', ({ agentId, ...patch }) => {
459
+ const agent = this.agents.get(agentId);
460
+ if (!agent) return;
461
+
462
+ if (typeof patch.threadId === 'string' && patch.threadId) {
463
+ agent.codexAppServerThreadId = patch.threadId;
464
+ agent.providerSessionId = patch.threadId;
465
+ agent.providerSessionTemporary = false;
466
+ agent.providerSessionKey = this.providerSessionKey(
467
+ agent.providerSessionProvider || 'codex',
468
+ patch.threadId,
469
+ agent.providerHomeId || 'default'
470
+ );
471
+ }
472
+ if (typeof patch.turnId === 'string') agent.codexAppServerTurnId = patch.turnId;
473
+ if (typeof patch.state === 'string' && patch.state) agent.codexAppServerState = patch.state;
474
+ if (typeof patch.error === 'string') agent.codexAppServerError = patch.error;
475
+ if (typeof patch.pendingRequestId === 'string') agent.codexAppServerPendingRequestId = patch.pendingRequestId;
476
+ if (typeof patch.pendingRequestMethod === 'string') agent.codexAppServerPendingRequestMethod = patch.pendingRequestMethod;
477
+ if (Object.prototype.hasOwnProperty.call(patch, 'pendingRequest')) {
478
+ agent.codexAppServerPendingRequest = patch.pendingRequest || null;
479
+ }
480
+ if (Object.prototype.hasOwnProperty.call(patch, 'notice')) {
481
+ agent.codexAppServerNotice = patch.notice || null;
482
+ }
483
+ if (Object.prototype.hasOwnProperty.call(patch, 'goal')) {
484
+ agent.codexAppServerGoal = patch.goal || null;
485
+ }
486
+ if (typeof patch.transcriptUpdatedAt === 'string') {
487
+ agent.codexAppServerTranscriptUpdatedAt = patch.transcriptUpdatedAt;
488
+ }
489
+
490
+ if (patch.state === 'working' || patch.state === 'waiting-for-input') {
491
+ this.lastActivity.set(agentId, Date.now());
492
+ }
493
+ this.emit('update');
494
+ });
495
+ }
496
+
497
+ bindEngineEvents() {
498
+ this.engineBridge.on('session-started', ({ sessionId, status, startedAt }) => {
499
+ const agent = this.agents.get(sessionId);
500
+ if (!agent) return;
501
+
502
+ agent.engineStarted = true;
503
+ agent.engineStatus = status || 'running';
504
+ agent.startedAt = startedAt || Date.now();
505
+ this.observeAgentAttentionState(sessionId);
506
+ this.observeAgentStateChange(sessionId, { force: true });
507
+ this.emit('update');
508
+ });
509
+
510
+ this.engineBridge.on('session-output', ({ sessionId, data, engineName, outputSeq }) => {
511
+ const agent = this.agents.get(sessionId);
512
+ if (!agent) return;
513
+
514
+ this.reviveAgentRuntime(agent);
515
+ agent.output = trimSessionOutput(agent.output + data);
516
+ agent.lastEngineOutputAt = Date.now();
517
+ if (Number.isFinite(outputSeq)) {
518
+ agent.lastOutputSeq = outputSeq;
519
+ }
520
+
521
+ this.lastActivity.set(sessionId, Date.now());
522
+
523
+ // Track output events for rate calculation
524
+ const events = this.outputEvents.get(sessionId) || [];
525
+ events.push({ timestamp: Date.now(), bytes: Buffer.byteLength(String(data), 'utf8') });
526
+ const cutoff = Date.now() - AGENT_USAGE_RATE_WINDOW_MS;
527
+ this.outputEvents.set(sessionId, events.filter(e => e.timestamp > cutoff));
528
+
529
+ this.observeAgentAttentionState(sessionId);
530
+ this.observeAgentStateChange(sessionId);
531
+ const sessionSource = this.getEngineSessionSource(engineName);
532
+ const stream = {
533
+ agentId: sessionId,
534
+ data,
535
+ sessionSource,
536
+ };
537
+ if (Number.isFinite(outputSeq)) {
538
+ stream.outputSeq = outputSeq;
539
+ }
540
+ this.emit('session-stream', stream);
541
+ });
542
+
543
+ this.engineBridge.on('session-sync', ({ sessionId, output, engineName, replaceLive = true, outputSeq }) => {
544
+ const agent = this.agents.get(sessionId);
545
+ if (!agent) return;
546
+
547
+ this.reviveAgentRuntime(agent);
548
+ agent.output = trimSessionOutput(output);
549
+ agent.previewText = agent.output.slice(-2000);
550
+ if (Number.isFinite(outputSeq)) {
551
+ agent.lastOutputSeq = outputSeq;
552
+ }
553
+ this.lastActivity.set(sessionId, Date.now());
554
+
555
+ if (replaceLive) {
556
+ const sessionSource = this.getEngineSessionSource(engineName);
557
+ const stream = {
558
+ agentId: sessionId,
559
+ data: agent.output,
560
+ sessionSource,
561
+ replace: true,
562
+ };
563
+ if (Number.isFinite(outputSeq)) {
564
+ stream.outputSeq = outputSeq;
565
+ }
566
+ this.emit('session-stream', stream);
567
+ }
568
+ this.observeAgentAttentionState(sessionId);
569
+ this.observeAgentStateChange(sessionId);
570
+ this.emit('update');
571
+ });
572
+
573
+ this.engineBridge.on('session-preview', ({ sessionId, previewText, cols, rows, previewSnapshot, title }) => {
574
+ const agent = this.agents.get(sessionId);
575
+ if (!agent) return;
576
+
577
+ const revived = this.reviveAgentRuntime(agent);
578
+ const titleChanged = typeof title === 'string'
579
+ ? this.updateAgentSessionTitle(agent, title)
580
+ : false;
581
+ agent.previewText = previewText || '';
582
+ agent.previewSnapshot = previewSnapshot || null;
583
+ if (Number.isFinite(cols) && cols > 0) {
584
+ agent.previewCols = cols;
585
+ }
586
+ if (Number.isFinite(rows) && rows > 0) {
587
+ agent.previewRows = rows;
588
+ }
589
+ this.emit('session-preview-update', {
590
+ agentId: sessionId,
591
+ previewText: agent.previewText,
592
+ cols: agent.previewCols || 80,
593
+ rows: agent.previewRows || 30,
594
+ previewSnapshot: agent.previewSnapshot,
595
+ terminalStatus: deriveAgentTerminalStatus(agent, {
596
+ previewText: agent.previewText,
597
+ title: agent.sessionTitle || '',
598
+ terminalBusy: typeof agent.terminalBusy === 'boolean' ? agent.terminalBusy : null,
599
+ }),
600
+ });
601
+ this.observeAgentAttentionState(sessionId);
602
+ this.observeAgentStateChange(sessionId);
603
+ if (titleChanged || revived) {
604
+ this.emit('update');
605
+ }
606
+ });
607
+
608
+ this.engineBridge.on('session-title', ({ sessionId, title }) => {
609
+ const agent = this.agents.get(sessionId);
610
+ if (!agent) return;
611
+
612
+ if (this.updateAgentSessionTitle(agent, title)) {
613
+ this.observeAgentAttentionState(sessionId);
614
+ this.observeAgentStateChange(sessionId);
615
+ this.emit('update');
616
+ }
617
+ });
618
+
619
+ this.engineBridge.on('session-activity', ({ sessionId, lastActivityAt }) => {
620
+ const agent = this.agents.get(sessionId);
621
+ const revived = agent ? this.reviveAgentRuntime(agent) : false;
622
+ this.lastActivity.set(sessionId, lastActivityAt || Date.now());
623
+ this.observeAgentAttentionState(sessionId);
624
+ this.observeAgentStateChange(sessionId);
625
+ this.emitActivityUpdate(sessionId, lastActivityAt || Date.now());
626
+ if (revived) this.emit('update');
627
+ });
628
+
629
+ this.engineBridge.on('session-busy-state', (payload = {}) => {
630
+ const {
631
+ sessionId,
632
+ terminalBusy,
633
+ cwd,
634
+ lastExitCode,
635
+ shellEvent,
636
+ shellCommand,
637
+ shellLastCommand,
638
+ shellCommandStartedAt,
639
+ shellLastCommandStartedAt,
640
+ shellLastCommandFinishedAt,
641
+ shellLastCommandDurationMs,
642
+ statusMarkerSeen,
643
+ busyMarkerSeen,
644
+ } = payload;
645
+ const agent = this.agents.get(sessionId);
646
+ if (!agent) return;
647
+
648
+ const previousState = JSON.stringify({
649
+ terminalBusy: agent.terminalBusy,
650
+ shellCwd: agent.shellCwd || '',
651
+ shellLastExitCode: agent.shellLastExitCode ?? null,
652
+ shellLastEvent: agent.shellLastEvent || '',
653
+ shellCommand: agent.shellCommand || '',
654
+ shellLastCommand: agent.shellLastCommand || '',
655
+ shellCommandStartedAt: agent.shellCommandStartedAt ?? null,
656
+ shellLastCommandStartedAt: agent.shellLastCommandStartedAt ?? null,
657
+ shellLastCommandFinishedAt: agent.shellLastCommandFinishedAt ?? null,
658
+ shellLastCommandDurationMs: agent.shellLastCommandDurationMs ?? null,
659
+ shellStatusMarkerSeen: agent.shellStatusMarkerSeen === true,
660
+ shellBusyMarkerSeen: agent.shellBusyMarkerSeen === true,
661
+ });
662
+ if (typeof terminalBusy === 'boolean') {
663
+ agent.terminalBusy = terminalBusy;
664
+ }
665
+ if (typeof cwd === 'string' && cwd) {
666
+ agent.shellCwd = cwd;
667
+ }
668
+ if (Object.prototype.hasOwnProperty.call(payload, 'lastExitCode')) {
669
+ agent.shellLastExitCode = typeof lastExitCode === 'number' ? lastExitCode : null;
670
+ }
671
+ if (shellEvent === 'start' || shellEvent === 'finish') {
672
+ agent.shellLastEvent = shellEvent;
673
+ }
674
+ if (Object.prototype.hasOwnProperty.call(payload, 'shellCommand')) {
675
+ agent.shellCommand = typeof shellCommand === 'string' ? shellCommand : '';
676
+ }
677
+ if (Object.prototype.hasOwnProperty.call(payload, 'shellLastCommand')) {
678
+ agent.shellLastCommand = typeof shellLastCommand === 'string' ? shellLastCommand : '';
679
+ } else if (shellEvent === 'finish' && agent.shellCommand) {
680
+ agent.shellLastCommand = agent.shellCommand;
681
+ agent.shellCommand = '';
682
+ }
683
+ if (Object.prototype.hasOwnProperty.call(payload, 'shellCommandStartedAt')) {
684
+ agent.shellCommandStartedAt = finiteNumberOrNull(shellCommandStartedAt);
685
+ }
686
+ if (Object.prototype.hasOwnProperty.call(payload, 'shellLastCommandStartedAt')) {
687
+ agent.shellLastCommandStartedAt = finiteNumberOrNull(shellLastCommandStartedAt);
688
+ }
689
+ if (Object.prototype.hasOwnProperty.call(payload, 'shellLastCommandFinishedAt')) {
690
+ agent.shellLastCommandFinishedAt = finiteNumberOrNull(shellLastCommandFinishedAt);
691
+ }
692
+ if (Object.prototype.hasOwnProperty.call(payload, 'shellLastCommandDurationMs')) {
693
+ agent.shellLastCommandDurationMs = finiteNumberOrNull(shellLastCommandDurationMs);
694
+ }
695
+ if (statusMarkerSeen === true) {
696
+ agent.shellStatusMarkerSeen = true;
697
+ }
698
+ if (busyMarkerSeen === true) {
699
+ agent.shellBusyMarkerSeen = true;
700
+ }
701
+ const nextState = JSON.stringify({
702
+ terminalBusy: agent.terminalBusy,
703
+ shellCwd: agent.shellCwd || '',
704
+ shellLastExitCode: agent.shellLastExitCode ?? null,
705
+ shellLastEvent: agent.shellLastEvent || '',
706
+ shellCommand: agent.shellCommand || '',
707
+ shellLastCommand: agent.shellLastCommand || '',
708
+ shellCommandStartedAt: agent.shellCommandStartedAt ?? null,
709
+ shellLastCommandStartedAt: agent.shellLastCommandStartedAt ?? null,
710
+ shellLastCommandFinishedAt: agent.shellLastCommandFinishedAt ?? null,
711
+ shellLastCommandDurationMs: agent.shellLastCommandDurationMs ?? null,
712
+ shellStatusMarkerSeen: agent.shellStatusMarkerSeen === true,
713
+ shellBusyMarkerSeen: agent.shellBusyMarkerSeen === true,
714
+ });
715
+ if (previousState === nextState) return;
716
+ this.observeAgentAttentionState(sessionId);
717
+ this.observeAgentStateChange(sessionId);
718
+ this.emit('update');
719
+ });
720
+
721
+ this.engineBridge.on('session-exited', ({ sessionId, code, exitedAt }) => {
722
+ const agent = this.agents.get(sessionId);
723
+ if (!agent) return;
724
+ if (this.permissionRestartSuppressedAgentIds.has(sessionId)) return;
725
+
726
+ // A fresh App Server thread deliberately has no CLI/PTy observer until
727
+ // its first turn has completed and become resumable. Native host
728
+ // cleanup can still report an old/missing session id while that gap is
729
+ // open; it is not an exit of the App Server Agent.
730
+ if (isCodexAppServerAgent(agent)) return;
731
+
732
+ if (!agent.validated) {
733
+ this.stopCodexProviderSessionResolver(sessionId);
734
+ this.stopProviderSessionTitleResolver(sessionId);
735
+ this.agents.delete(sessionId);
736
+ this.lastActivity.delete(sessionId);
737
+ this.lastActivityUpdate.delete(sessionId);
738
+ this.outputEvents.delete(sessionId);
739
+ this.lastResizeByAgent.delete(sessionId);
740
+
741
+ if (this.mainAgentId === sessionId) {
742
+ this.mainAgentId = null;
743
+ }
744
+
745
+ this.emit('update');
746
+ return;
747
+ }
748
+
749
+ this.stopCodexProviderSessionResolver(sessionId);
750
+ this.stopProviderSessionTitleResolver(sessionId);
751
+ agent.status = sessionId === this.mainAgentId ? 'dead' : 'stopped';
752
+ agent.exitedAt = exitedAt || Date.now();
753
+ agent.output = trimSessionOutput(`${agent.output}\nProcess exited with code ${code}`);
754
+ this.observeAgentAttentionState(sessionId);
755
+ this.observeAgentStateChange(sessionId, { force: true });
756
+ if (sessionId !== this.mainAgentId) {
757
+ this.recordTaskHistory(agent, {
758
+ reason: 'process-exit',
759
+ archivedAt: Date.now(),
760
+ });
761
+ }
762
+ this.emit('update');
763
+ });
764
+
765
+ this.engineBridge.on('session-error', ({ sessionId, error, fatal = true }) => {
766
+ const agent = this.agents.get(sessionId);
767
+ if (!agent) return;
768
+ if (this.permissionRestartSuppressedAgentIds.has(sessionId)) return;
769
+
770
+ if (fatal === false) {
771
+ return;
772
+ }
773
+ if (isSessionNotAvailableError(error) && this.shouldDeferMissingEngineSession(agent)) {
774
+ return;
775
+ }
776
+
777
+ this.markAgentSessionDead(sessionId, error);
778
+ });
779
+ }
780
+
781
+ async recoverEngineSessions() {
782
+ if (!this.engineBridge || typeof this.engineBridge.recoverSessions !== 'function') {
783
+ return;
784
+ }
785
+
786
+ const recovered = await this.engineBridge.recoverSessions();
787
+ const persistedRecords = this.configManager && typeof this.configManager.listAgentSessionRecords === 'function'
788
+ ? this.configManager.listAgentSessionRecords()
789
+ : [];
790
+ const persistedByRuntimeAgentId = new Map(persistedRecords
791
+ .filter(record => record && record.runtimeAgentId)
792
+ .map(record => [record.runtimeAgentId, record]));
793
+ let changed = false;
794
+
795
+ for (const entry of recovered || []) {
796
+ const engineMetadata = entry.metadata || {};
797
+ const state = entry.state || {};
798
+ const agentId = recoveredEngineSessionId(entry, engineMetadata);
799
+ const persisted = persistedByRuntimeAgentId.get(agentId);
800
+ // The persisted runtime mode is authoritative. A PTY can outlive the
801
+ // server long enough to appear in native-host recovery after the Agent
802
+ // has already switched to ACP. Recovering that stale PTY first would
803
+ // overwrite the persisted record back to `terminal` before
804
+ // recoverAcpSessions() gets a chance to read it.
805
+ if (persisted?.agentRuntimeMode === 'acp') {
806
+ await this.killRecoveredEngineSession(entry, engineMetadata, agentId);
807
+ continue;
808
+ }
809
+ const metadata = persisted ? {
810
+ ...engineMetadata,
811
+ pinned: persisted.pinned === true,
812
+ projectOrder: finiteOrder(persisted.projectOrder) ?? finiteOrder(engineMetadata.projectOrder),
813
+ pinnedOrder: finiteOrder(persisted.pinnedOrder) ?? finiteOrder(engineMetadata.pinnedOrder),
814
+ } : engineMetadata;
815
+ if (!agentId || this.agents.has(agentId)) continue;
816
+ if (!shouldRecoverEngineSession(metadata)) {
817
+ await this.killRecoveredEngineSession(entry, metadata, agentId);
818
+ continue;
819
+ }
820
+
821
+ const agentRecord = this.recoveredAgentRecord(agentId, entry.engineName || metadata.engineName || 'native', metadata, state);
822
+ ensureAgentOrders(agentRecord, Array.from(this.agents.values()));
823
+ agentRecord.lastObservedTurnActive = this.isAgentAttentionTurnActive(agentRecord);
824
+ this.ensurePersistentAgentSession(agentRecord);
825
+ this.agents.set(agentId, agentRecord);
826
+ this.lastActivity.set(agentId, state.lastActivityAt || metadata.lastActivityAt || Date.now());
827
+ if (agentRecord.wantsMain && !this.mainAgentId) {
828
+ this.mainAgentId = agentId;
829
+ }
830
+ this.activateProviderSessionTracking(agentId);
831
+ changed = true;
832
+ }
833
+
834
+ if (changed) {
835
+ this.emit('update');
836
+ }
837
+
838
+ await this.recoverCodexAppServerSessions();
839
+ await this.recoverAcpSessions();
840
+ }
841
+
842
+ async recoverAcpSessions() {
843
+ if (!this.acpRuntime || !this.configManager || typeof this.configManager.listAgentSessionRecords !== 'function') return;
844
+ for (const record of this.configManager.listAgentSessionRecords()) {
845
+ if (!record || record.archived === true || record.agentRuntimeMode !== 'acp') continue;
846
+ const agentId = String(record.runtimeAgentId || '').trim();
847
+ const provider = String(record.providerSessionProvider || record.provider || '').trim();
848
+ const sessionId = String(record.providerSessionId || '').trim();
849
+ if (!agentId || !sessionId || !['codex', 'claude', 'opencode', 'qoder'].includes(provider)) continue;
850
+ let agent = this.agents.get(agentId);
851
+ if (!agent) {
852
+ agent = this.recoveredAgentRecord(agentId, record.engine || 'native', record, { status: 'running' });
853
+ ensureAgentOrders(agent, Array.from(this.agents.values()));
854
+ agent.persistentSessionId = record.id || '';
855
+ agent.engineStarted = false;
856
+ this.agents.set(agentId, agent);
857
+ this.lastActivity.set(agentId, Date.now());
858
+ }
859
+ try {
860
+ const executableName = provider === 'qoder' ? 'qodercli' : provider;
861
+ const executable = resolveAgentExecutable(executableName) || executableName;
862
+ const approvalMode = agent.launchPermissionMode || (
863
+ provider === 'codex' && this.configManager.getCodexApprovalMode
864
+ ? this.configManager.getCodexApprovalMode()
865
+ : 'approve'
866
+ );
867
+ const prepared = await this.acpRuntime.prepareAgent({
868
+ agentId,
869
+ provider,
870
+ executable,
871
+ env: this.buildAgentEnv(agentId, agent),
872
+ cwd: agent.cwd,
873
+ sessionId,
874
+ historyMode: 'load',
875
+ approvalMode,
876
+ model: this.configManager.getCodexModel
877
+ ? this.configManager.getCodexModel()
878
+ : '',
879
+ reasoningEffort: this.configManager.getCodexReasoningEffort
880
+ ? this.configManager.getCodexReasoningEffort()
881
+ : '',
882
+ serviceTier: this.configManager.getCodexServiceTier
883
+ ? this.configManager.getCodexServiceTier()
884
+ : '',
885
+ });
886
+ agent.providerSessionId = prepared.sessionId;
887
+ agent.providerSessionTemporary = false;
888
+ agent.providerSessionSource = `acp-${prepared.historyMode}`;
889
+ agent.agentRuntimeMode = 'acp';
890
+ agent.acpState = 'idle';
891
+ agent.acpError = '';
892
+ agent.status = 'running';
893
+ agent.engineStatus = 'running';
894
+ agent.engineStarted = false;
895
+ this.ensurePersistentAgentSession(agent);
896
+ } catch (error) {
897
+ agent.acpState = 'error';
898
+ agent.acpError = `ACP recovery failed: ${error && (error.message || error)}`;
899
+ agent.status = 'stopped';
900
+ agent.engineStatus = 'stopped';
901
+ agent.engineStarted = false;
902
+ agent.exitedAt = Date.now();
903
+ this.ensurePersistentAgentSession(agent);
904
+ }
905
+ }
906
+ this.emit('update');
907
+ }
908
+
909
+ async recoverCodexAppServerSessions() {
910
+ if (!this.codexAppServerRuntime || !this.configManager || typeof this.configManager.listAgentSessionRecords !== 'function') return;
911
+ const records = this.configManager.listAgentSessionRecords();
912
+ for (const record of records) {
913
+ if (!record || record.archived === true || record.codexRuntimeMode !== 'app-server') continue;
914
+ const agentId = String(record.runtimeAgentId || '').trim();
915
+ const threadId = String(record.codexAppServerThreadId || record.providerSessionId || '').trim();
916
+ const home = String(record.codexAppServerHomePath || '').trim();
917
+ if (!agentId || !threadId || !home || !fs.existsSync(home)) {
918
+ if (record.provider === 'codex' || record.providerSessionProvider === 'codex') {
919
+ let agent = this.agents.get(agentId);
920
+ if (!agent && agentId) {
921
+ agent = this.recoveredAgentRecord(agentId, record.engine || 'native', record, { status: 'running' });
922
+ agent.persistentSessionId = record.id || '';
923
+ }
924
+ if (agent) {
925
+ ensureAgentOrders(agent, Array.from(this.agents.values()));
926
+ agent.codexRuntimeMode = 'cli';
927
+ agent.codexAppServerHomePath = '';
928
+ agent.codexAppServerState = '';
929
+ agent.codexAppServerEndpoint = '';
930
+ agent.codexAppServerTurnId = '';
931
+ agent.codexAppServerError = '';
932
+ agent.codexAppServerPendingRequestId = '';
933
+ agent.codexAppServerPendingRequestMethod = '';
934
+ agent.codexAppServerPendingRequest = null;
935
+ agent.codexCliObserverDeferred = false;
936
+ this.ensurePersistentAgentSession(agent);
937
+ this.updateEngineProviderSessionMetadata(agent);
938
+ }
939
+ }
940
+ continue;
941
+ }
942
+ let agent = this.agents.get(agentId);
943
+ if (!agent) {
944
+ agent = this.recoveredAgentRecord(agentId, record.engine || 'native', record, { status: 'running' });
945
+ ensureAgentOrders(agent, Array.from(this.agents.values()));
946
+ agent.persistentSessionId = record.id || '';
947
+ agent.engineStarted = false;
948
+ this.agents.set(agentId, agent);
949
+ this.lastActivity.set(agentId, Date.now());
950
+ }
951
+ try {
952
+ // Remove a CLI observer recovered from pre-split metadata before the
953
+ // structured runtime reconnects to this App Server Agent.
954
+ await this.killRecoveredEngineSession({ engineName: agent.engineName }, record, agentId);
955
+ const executable = resolveCompatibleCodexExecutable('').path || resolveAgentExecutable('codex') || 'codex';
956
+ const binding = await this.codexAppServerRuntime.reattachAgent({
957
+ agentId,
958
+ threadId,
959
+ codexHome: home,
960
+ executable,
961
+ env: this.buildAgentEnv(agentId, agent),
962
+ cwd: agent.cwd,
963
+ workspaceRoot: agent.projectWorkspace || agent.cwd,
964
+ approvalMode: agent.launchPermissionMode || 'approve',
965
+ });
966
+ agent.codexAppServerThreadId = binding.threadId;
967
+ agent.codexAppServerState = 'idle';
968
+ agent.codexAppServerError = '';
969
+ // App Server owns this runtime. It deliberately has no CLI/PTy
970
+ // observer, so recovery cannot create a second reader or writer.
971
+ agent.status = 'running';
972
+ agent.engineStatus = 'running';
973
+ agent.engineStarted = false;
974
+ agent.codexCliObserverDeferred = false;
975
+ agent.exitedAt = null;
976
+ this.activateProviderSessionTracking(agentId);
977
+ this.ensurePersistentAgentSession(agent);
978
+ } catch (error) {
979
+ agent.codexAppServerError = `Codex App Server recovery failed: ${error && (error.message || error)}`;
980
+ agent.codexAppServerState = 'error';
981
+ agent.status = 'stopped';
982
+ agent.engineStatus = 'stopped';
983
+ agent.engineStarted = false;
984
+ agent.exitedAt = Date.now();
985
+ this.ensurePersistentAgentSession(agent);
986
+ }
987
+ }
988
+ this.emit('update');
989
+ }
990
+
991
+ async killRecoveredEngineSession(entry, metadata, agentId) {
992
+ if (!this.engineBridge || typeof this.engineBridge.killSession !== 'function') return;
993
+ const engineName = entry.engineName || metadata.engineName || 'native';
994
+ try {
995
+ await this.engineBridge.killSession(engineName, agentId);
996
+ } catch (error) {
997
+ console.warn('Failed to kill unrecovered engine session:', agentId, error && (error.message || error));
998
+ }
999
+ }
1000
+
1001
+ async whenRecovered() {
1002
+ await this.recoveryPromise;
1003
+ }
1004
+
1005
+ recoveredAgentRecord(agentId, engineName, metadata, state) {
1006
+ const wantsMain = metadata.wantsMain === true;
1007
+ const providerSessionProvider = metadata.providerSessionProvider || metadata.provider || '';
1008
+ const recoverCodexAppServer = shouldRecoverAsCodexAppServer(metadata);
1009
+ return {
1010
+ id: agentId,
1011
+ command: metadata.forkCommand || metadata.command || '',
1012
+ forkCommand: metadata.forkCommand || metadata.command || '',
1013
+ cwd: metadata.cwd || '',
1014
+ output: typeof state.output === 'string' ? trimSessionOutput(state.output) : '',
1015
+ previewText: typeof state.previewText === 'string' ? state.previewText : '',
1016
+ previewSnapshot: state.previewSnapshot || null,
1017
+ previewCols: state.previewCols || 80,
1018
+ previewRows: state.previewRows || 30,
1019
+ sessionTitle: state.title || metadata.sessionTitle || '',
1020
+ status: state.status === 'exited' ? 'stopped' : 'running',
1021
+ engineName,
1022
+ wantsMain,
1023
+ mainWorkspace: metadata.mainWorkspace || '',
1024
+ projectWorkspace: metadata.projectWorkspace || metadata.cwd || '',
1025
+ category: metadata.category || 'coding',
1026
+ launchPermissionMode: metadata.launchPermissionMode || '',
1027
+ parentAgentId: metadata.parentAgentId || '',
1028
+ task: metadata.task || '',
1029
+ workflowTemplate: metadata.workflowTemplate || '',
1030
+ source: metadata.source || 'recovered',
1031
+ providerSessionProvider,
1032
+ providerHomeId: metadata.providerHomeId || '',
1033
+ providerHomePath: metadata.providerHomePath || '',
1034
+ providerSessionId: metadata.providerSessionId || '',
1035
+ providerSessionKey: metadata.providerSessionKey || (
1036
+ providerSessionProvider && metadata.providerSessionId
1037
+ ? mainPageAgentSessionKey(providerSessionProvider, metadata.providerSessionId, metadata.providerHomeId || '')
1038
+ : ''
1039
+ ),
1040
+ providerSessionTemporary: metadata.providerSessionTemporary === true || isTemporaryProviderSessionId(metadata.providerSessionId),
1041
+ providerSessionSource: metadata.providerSessionSource || '',
1042
+ providerSessionResolvedAt: metadata.providerSessionResolvedAt || null,
1043
+ providerSessionTitle: metadata.providerSessionTitle || '',
1044
+ // Older persisted sessions predate App Server mode. Also, a Codex
1045
+ // App Server record without its isolated runtime home is not actually
1046
+ // attachable; recover it as terminal-owned CLI instead of leaving the
1047
+ // UI in a split state where the pane is terminal but Composer sends to
1048
+ // App Server.
1049
+ codexRuntimeMode: recoverCodexAppServer ? 'app-server' : 'cli',
1050
+ agentRuntimeMode: metadata.agentRuntimeMode === 'acp' ? 'acp' : (metadata.agentRuntimeMode === 'json' ? 'json' : 'terminal'),
1051
+ acpState: metadata.agentRuntimeMode === 'acp' ? (metadata.acpState || '') : '',
1052
+ acpError: metadata.agentRuntimeMode === 'acp' ? (metadata.acpError || '') : '',
1053
+ acpStopReason: metadata.agentRuntimeMode === 'acp' ? (metadata.acpStopReason || '') : '',
1054
+ acpPendingPermission: null,
1055
+ acpPendingPermissions: [],
1056
+ acpPendingElicitation: null,
1057
+ acpPendingElicitations: [],
1058
+ acpActiveElicitations: [],
1059
+ acpSessionUpdatedAt: metadata.agentRuntimeMode === 'acp' ? (metadata.acpSessionUpdatedAt || '') : '',
1060
+ acpSessionRevision: metadata.agentRuntimeMode === 'acp' ? (Number(metadata.acpSessionRevision) || 0) : 0,
1061
+ codexAppServerHomePath: recoverCodexAppServer ? metadata.codexAppServerHomePath : '',
1062
+ codexAppServerState: recoverCodexAppServer ? (metadata.codexAppServerState || '') : '',
1063
+ codexAppServerEndpoint: recoverCodexAppServer ? (metadata.codexAppServerEndpoint || '') : '',
1064
+ codexAppServerThreadId: metadata.codexAppServerThreadId || metadata.providerSessionId || '',
1065
+ codexAppServerTurnId: recoverCodexAppServer ? (metadata.codexAppServerTurnId || '') : '',
1066
+ codexAppServerError: recoverCodexAppServer ? (metadata.codexAppServerError || '') : '',
1067
+ codexAppServerPendingRequestId: recoverCodexAppServer ? (metadata.codexAppServerPendingRequestId || '') : '',
1068
+ codexAppServerPendingRequestMethod: recoverCodexAppServer ? (metadata.codexAppServerPendingRequestMethod || '') : '',
1069
+ codexAppServerPendingRequest: recoverCodexAppServer ? (metadata.codexAppServerPendingRequest || null) : null,
1070
+ codexCliObserverDeferred: false,
1071
+ forkedFromProviderSessionId: metadata.forkedFromProviderSessionId || '',
1072
+ restartedFromAgentId: metadata.restartedFromAgentId || '',
1073
+ restartedFromAgentIds: Array.isArray(metadata.restartedFromAgentIds)
1074
+ ? metadata.restartedFromAgentIds.filter(id => typeof id === 'string' && id)
1075
+ : [],
1076
+ persistentSessionId: metadata.persistentSessionId || '',
1077
+ customTitle: metadata.customTitle || '',
1078
+ terminalBusy: typeof state.terminalBusy === 'boolean' ? state.terminalBusy : null,
1079
+ shellCwd: state.shellCwd || metadata.cwd || '',
1080
+ shellLastExitCode: typeof state.shellLastExitCode === 'number' ? state.shellLastExitCode : null,
1081
+ shellLastEvent: state.shellLastEvent || '',
1082
+ shellCommand: typeof state.shellCommand === 'string' ? state.shellCommand : '',
1083
+ shellLastCommand: typeof state.shellLastCommand === 'string' ? state.shellLastCommand : '',
1084
+ shellCommandStartedAt: finiteNumberOrNull(state.shellCommandStartedAt),
1085
+ shellLastCommandStartedAt: finiteNumberOrNull(state.shellLastCommandStartedAt),
1086
+ shellLastCommandFinishedAt: finiteNumberOrNull(state.shellLastCommandFinishedAt),
1087
+ shellLastCommandDurationMs: finiteNumberOrNull(state.shellLastCommandDurationMs),
1088
+ pinned: metadata.pinned === true,
1089
+ projectOrder: finiteOrder(metadata.projectOrder),
1090
+ pinnedOrder: finiteOrder(metadata.pinnedOrder),
1091
+ attentionSeq: finiteNonNegativeInteger(metadata.attentionSeq),
1092
+ readAttentionSeq: finiteNonNegativeInteger(metadata.readAttentionSeq),
1093
+ attentionUpdatedAt: finiteNumberOrNull(metadata.attentionUpdatedAt),
1094
+ readAttentionAt: finiteNumberOrNull(metadata.readAttentionAt),
1095
+ attentionReason: metadata.attentionReason || '',
1096
+ attentionOutputSeq: finiteNumberOrNull(metadata.attentionOutputSeq),
1097
+ unread: finiteNonNegativeInteger(metadata.attentionSeq) > finiteNonNegativeInteger(metadata.readAttentionSeq),
1098
+ archived: false,
1099
+ archivedAt: null,
1100
+ canForkNewWorktree: this.canCreateForkWorktree(metadata.projectWorkspace || metadata.cwd || ''),
1101
+ validated: true,
1102
+ engineStarted: true,
1103
+ engineStatus: state.status || 'running',
1104
+ startedAt: state.startedAt || metadata.startedAt || Date.now(),
1105
+ lastEngineOutputAt: Date.now(),
1106
+ lastOutputSeq: finiteNumberOrNull(state.outputSeq),
1107
+ attentionRequiresNewOutput: true,
1108
+ attentionBaselineOutputSeq: finiteNumberOrNull(state.outputSeq),
1109
+ attentionBaselineOutputAt: Date.now(),
1110
+ attentionTrackingReady: true,
1111
+ lastObservedTurnActive: false,
1112
+ attentionSuppressUntil: 0,
1113
+ };
1114
+ }
1115
+
1116
+ reviveAgentRuntime(agent, sessionState = null) {
1117
+ if (!agent) return false;
1118
+ if (sessionState && !isLiveEngineSessionState(sessionState)) return false;
1119
+ if (!['dead', 'stopped', 'pending'].includes(agent.status)) {
1120
+ if (sessionState && sessionState.status) {
1121
+ agent.engineStatus = sessionState.status;
1122
+ }
1123
+ return false;
1124
+ }
1125
+
1126
+ agent.status = 'running';
1127
+ agent.engineStatus = sessionState && sessionState.status ? sessionState.status : 'running';
1128
+ agent.exitedAt = null;
1129
+ agent.terminalBusy = typeof agent.terminalBusy === 'boolean' ? agent.terminalBusy : null;
1130
+ return true;
1131
+ }
1132
+
1133
+ shouldDeferMissingEngineSession(agent) {
1134
+ if (!agent || !isRunningAgentRuntimeStatus(agent.status)) return false;
1135
+ if (isCodexAppServerAgent(agent)) return true;
1136
+ if (agent.engineStarted === false) return true;
1137
+ const startedAt = Number(agent.startedAt);
1138
+ return Number.isFinite(startedAt) && Date.now() - startedAt < MISSING_ENGINE_SESSION_STARTUP_GRACE_MS;
1139
+ }
1140
+
1141
+ providerSessionKey(provider, sessionId, providerHomeId = '') {
1142
+ return provider && sessionId ? mainPageAgentSessionKey(provider, sessionId, providerHomeId) : '';
1143
+ }
1144
+
1145
+ getMainPageSessionKeys() {
1146
+ if (this.configManager && typeof this.configManager.getMainPageSessionKeys === 'function') {
1147
+ return this.configManager.getMainPageSessionKeys();
1148
+ }
1149
+ if (this.configManager && typeof this.configManager.getSettings === 'function') {
1150
+ const settings = this.configManager.getSettings();
1151
+ return Array.isArray(settings.mainPageSessionKeys) ? settings.mainPageSessionKeys : [];
1152
+ }
1153
+ return [];
1154
+ }
1155
+
1156
+ setMainPageSessionKeys(keys) {
1157
+ if (this.configManager && typeof this.configManager.setMainPageSessionKeys === 'function') {
1158
+ return this.configManager.setMainPageSessionKeys(keys);
1159
+ }
1160
+ if (this.configManager && typeof this.configManager.updateSettings === 'function') {
1161
+ this.configManager.updateSettings({ mainPageSessionKeys: keys });
1162
+ return keys;
1163
+ }
1164
+ return [];
1165
+ }
1166
+
1167
+ ensurePersistentAgentSession(agent, patch = {}) {
1168
+ if (!agent || !this.configManager || typeof this.configManager.ensureAgentSessionRecord !== 'function') {
1169
+ return '';
1170
+ }
1171
+ const persistentSessionId = this.configManager.ensureAgentSessionRecord(agent, patch);
1172
+ if (persistentSessionId && !agent.persistentSessionId) {
1173
+ agent.persistentSessionId = persistentSessionId;
1174
+ }
1175
+ return persistentSessionId;
1176
+ }
1177
+
1178
+ currentProviderSessionIds(provider, excludedAgentId = '', providerHomeId = 'default') {
1179
+ const ids = new Set();
1180
+ const normalizedHomeId = String(providerHomeId || 'default').trim() || 'default';
1181
+ for (const agent of this.agents.values()) {
1182
+ if (!agent || agent.id === excludedAgentId) continue;
1183
+ if (agent.providerSessionProvider !== provider) continue;
1184
+ if ((String(agent.providerHomeId || 'default').trim() || 'default') !== normalizedHomeId) continue;
1185
+ if (!agent.providerSessionId || agent.providerSessionTemporary === true) continue;
1186
+ ids.add(agent.providerSessionId);
1187
+ }
1188
+ return ids;
1189
+ }
1190
+
1191
+ rememberMainPageProviderSession(agent) {
1192
+ if (!agent || agent.wantsMain) return;
1193
+ if (!agent.providerSessionProvider || !agent.providerSessionId || agent.providerSessionTemporary === true) return;
1194
+ if (!this.configManager) {
1195
+ return;
1196
+ }
1197
+
1198
+ const sessionKey = this.providerSessionKey(agent.providerSessionProvider, agent.providerSessionId, agent.providerHomeId || '');
1199
+ if (!sessionKey) return;
1200
+ const currentKeys = this.getMainPageSessionKeys();
1201
+ if (currentKeys[0] === sessionKey) {
1202
+ this.ensurePersistentAgentSession(agent, { visibleOnMainPage: true, archived: false });
1203
+ return;
1204
+ }
1205
+ if (typeof this.configManager.rememberAgentSessionRecord === 'function') {
1206
+ const persistentSessionId = this.configManager.rememberAgentSessionRecord(agent);
1207
+ if (persistentSessionId) agent.persistentSessionId = persistentSessionId;
1208
+ return;
1209
+ }
1210
+ this.setMainPageSessionKeys([
1211
+ sessionKey,
1212
+ ...currentKeys.filter(key => key !== sessionKey),
1213
+ ]);
1214
+ }
1215
+
1216
+ updateEngineProviderSessionMetadata(agent) {
1217
+ if (!agent || !agent.engineName || agent.engineStarted !== true) return;
1218
+ const engine = this.engineBridge.getEngine(agent.engineName);
1219
+ if (!engine || typeof engine.updateSessionMetadata !== 'function') return;
1220
+ Promise.resolve(engine.updateSessionMetadata(agent.id, {
1221
+ providerSessionProvider: agent.providerSessionProvider || '',
1222
+ providerHomeId: agent.providerHomeId || '',
1223
+ providerHomePath: agent.providerHomePath || '',
1224
+ providerSessionId: agent.providerSessionId || '',
1225
+ providerSessionKey: agent.providerSessionKey || '',
1226
+ providerSessionTemporary: agent.providerSessionTemporary === true,
1227
+ providerSessionSource: agent.providerSessionSource || '',
1228
+ providerSessionResolvedAt: agent.providerSessionResolvedAt || null,
1229
+ providerSessionTitle: agent.providerSessionTitle || '',
1230
+ codexRuntimeMode: agent.codexRuntimeMode || '',
1231
+ agentRuntimeMode: agent.agentRuntimeMode || 'terminal',
1232
+ acpState: agent.acpState || '',
1233
+ acpError: agent.acpError || '',
1234
+ acpStopReason: agent.acpStopReason || '',
1235
+ acpSessionUpdatedAt: agent.acpSessionUpdatedAt || '',
1236
+ acpSessionRevision: Number(agent.acpSessionRevision) || 0,
1237
+ codexAppServerHomePath: agent.codexAppServerHomePath || '',
1238
+ codexAppServerState: agent.codexAppServerState || '',
1239
+ codexAppServerEndpoint: agent.codexAppServerEndpoint || '',
1240
+ codexAppServerThreadId: agent.codexAppServerThreadId || '',
1241
+ codexAppServerTurnId: agent.codexAppServerTurnId || '',
1242
+ codexAppServerError: agent.codexAppServerError || '',
1243
+ codexAppServerPendingRequestId: agent.codexAppServerPendingRequestId || '',
1244
+ codexAppServerPendingRequestMethod: agent.codexAppServerPendingRequestMethod || '',
1245
+ codexAppServerPendingRequest: agent.codexAppServerPendingRequest || null,
1246
+ codexCliObserverDeferred: agent.codexCliObserverDeferred === true,
1247
+ forkedFromProviderSessionId: agent.forkedFromProviderSessionId || '',
1248
+ launchPermissionMode: agent.launchPermissionMode || '',
1249
+ attentionSeq: finiteNonNegativeInteger(agent.attentionSeq),
1250
+ readAttentionSeq: finiteNonNegativeInteger(agent.readAttentionSeq),
1251
+ attentionUpdatedAt: finiteNumberOrNull(agent.attentionUpdatedAt),
1252
+ readAttentionAt: finiteNumberOrNull(agent.readAttentionAt),
1253
+ attentionReason: agent.attentionReason || '',
1254
+ attentionOutputSeq: finiteNumberOrNull(agent.attentionOutputSeq),
1255
+ projectOrder: finiteOrder(agent.projectOrder),
1256
+ pinnedOrder: finiteOrder(agent.pinnedOrder),
1257
+ })).catch((error) => {
1258
+ console.warn('Failed to update provider session metadata:', error && (error.message || error));
1259
+ });
1260
+ }
1261
+
1262
+ activateProviderSessionTracking(agentId) {
1263
+ const agent = this.agents.get(agentId);
1264
+ if (!agent || !agent.providerSessionProvider || !agent.providerSessionId) return;
1265
+
1266
+ if (agent.providerSessionProvider === 'codex' && agent.providerSessionTemporary === true) {
1267
+ this.observeAgentStateChange(agentId, { force: true });
1268
+ return;
1269
+ }
1270
+
1271
+ this.stopCodexProviderSessionResolver(agentId);
1272
+ this.updateEngineProviderSessionMetadata(agent);
1273
+ this.rememberMainPageProviderSession(agent);
1274
+ this.attemptProviderSessionTitleResolution(agentId, { force: true }).catch((error) => {
1275
+ console.warn('Failed to resolve provider session title:', error && (error.message || error));
1276
+ });
1277
+ }
1278
+
1279
+ stopCodexProviderSessionResolver(agentId) {
1280
+ this.codexSessionResolveInFlight.delete(agentId);
1281
+ this.codexSessionResolveLastAttemptAt.delete(agentId);
1282
+ }
1283
+
1284
+ stopProviderSessionTitleResolver(agentId) {
1285
+ this.providerSessionTitleResolveInFlight.delete(agentId);
1286
+ this.providerSessionTitleResolveLastAttemptAt.delete(agentId);
1287
+ }
1288
+
1289
+ isAgentAttentionTurnActive(agent) {
1290
+ if (!agent) return false;
1291
+ if (agent.status === 'pending') return true;
1292
+ if (agent.status !== 'running') return false;
1293
+ const terminalStatus = deriveAgentTerminalStatus(agent);
1294
+ return terminalStatus.activity === 'busy';
1295
+ }
1296
+
1297
+ observeAgentAttentionState(agentId) {
1298
+ const agent = this.agents.get(agentId);
1299
+ if (!agent) return false;
1300
+
1301
+ const turnActive = this.isAgentAttentionTurnActive(agent);
1302
+ if (agent.attentionTrackingReady !== true) {
1303
+ agent.lastObservedTurnActive = turnActive;
1304
+ agent.attentionTrackingReady = true;
1305
+ return false;
1306
+ }
1307
+
1308
+ const wasTurnActive = agent.lastObservedTurnActive === true;
1309
+ agent.lastObservedTurnActive = turnActive;
1310
+
1311
+ if (wasTurnActive && !turnActive) {
1312
+ if (!hasAgentOutputAfterAttentionBaseline(agent)) {
1313
+ return false;
1314
+ }
1315
+ agent.attentionRequiresNewOutput = false;
1316
+ const reason = agent.status === 'stopped' || agent.status === 'dead'
1317
+ ? 'process-exit'
1318
+ : 'turn-complete';
1319
+ this.recordAgentAttentionEvent(agent, reason);
1320
+ return true;
1321
+ }
1322
+
1323
+ return false;
1324
+ }
1325
+
1326
+ recordAgentAttentionEvent(agent, reason = 'turn-complete') {
1327
+ if (!agent || this.isMainAgentRecord(agent.id, agent)) return null;
1328
+ const now = Date.now();
1329
+ const nextSeq = finiteNonNegativeInteger(agent.attentionSeq) + 1;
1330
+ agent.attentionSeq = nextSeq;
1331
+ agent.attentionUpdatedAt = now;
1332
+ agent.attentionReason = reason;
1333
+ agent.attentionOutputSeq = Number.isFinite(agent.lastOutputSeq) ? agent.lastOutputSeq : null;
1334
+ if (agent.attentionAutoReadNext === true) {
1335
+ agent.attentionAutoReadNext = false;
1336
+ agent.readAttentionSeq = nextSeq;
1337
+ agent.readAttentionAt = now;
1338
+ }
1339
+ agent.unread = agentAttentionUnread(agent);
1340
+ this.ensurePersistentAgentSession(agent);
1341
+ this.updateEngineProviderSessionMetadata(agent);
1342
+ this.emit('update');
1343
+ return {
1344
+ agentId: agent.id,
1345
+ attentionSeq: agent.attentionSeq,
1346
+ readAttentionSeq: finiteNonNegativeInteger(agent.readAttentionSeq),
1347
+ unread: agent.unread,
1348
+ };
1349
+ }
1350
+
1351
+ markAgentReadCursor(agentId, readAttentionSeq) {
1352
+ const agent = this.agents.get(agentId);
1353
+ if (!agent) {
1354
+ return { error: 'Agent not found' };
1355
+ }
1356
+
1357
+ const attentionSeq = finiteNonNegativeInteger(agent.attentionSeq);
1358
+ const requestedSeq = Number.isFinite(readAttentionSeq)
1359
+ ? finiteNonNegativeInteger(readAttentionSeq)
1360
+ : attentionSeq;
1361
+ const nextReadSeq = Math.min(attentionSeq, Math.max(finiteNonNegativeInteger(agent.readAttentionSeq), requestedSeq));
1362
+ const changed = finiteNonNegativeInteger(agent.readAttentionSeq) !== nextReadSeq || agent.unread === true;
1363
+
1364
+ agent.readAttentionSeq = nextReadSeq;
1365
+ agent.readAttentionAt = Date.now();
1366
+ agent.unread = agentAttentionUnread(agent);
1367
+ if (changed) {
1368
+ this.ensurePersistentAgentSession(agent);
1369
+ this.updateEngineProviderSessionMetadata(agent);
1370
+ this.emit('update');
1371
+ }
1372
+ return {
1373
+ agentId,
1374
+ attentionSeq,
1375
+ readAttentionSeq: agent.readAttentionSeq,
1376
+ unread: agent.unread,
1377
+ changed,
1378
+ };
1379
+ }
1380
+
1381
+ markAgentUnreadCursor(agentId) {
1382
+ const agent = this.agents.get(agentId);
1383
+ if (!agent) {
1384
+ return { error: 'Agent not found' };
1385
+ }
1386
+
1387
+ let changed = false;
1388
+ if (finiteNonNegativeInteger(agent.attentionSeq) === 0) {
1389
+ this.recordAgentAttentionEvent(agent, 'manual-unread');
1390
+ changed = true;
1391
+ }
1392
+ const attentionSeq = finiteNonNegativeInteger(agent.attentionSeq);
1393
+ const nextReadAttentionSeq = Math.max(0, attentionSeq - 1);
1394
+ changed = changed ||
1395
+ finiteNonNegativeInteger(agent.readAttentionSeq) !== nextReadAttentionSeq ||
1396
+ agent.unread !== true;
1397
+ if (changed) {
1398
+ agent.readAttentionSeq = nextReadAttentionSeq;
1399
+ agent.readAttentionAt = Date.now();
1400
+ agent.unread = agentAttentionUnread(agent);
1401
+ this.ensurePersistentAgentSession(agent);
1402
+ this.updateEngineProviderSessionMetadata(agent);
1403
+ this.emit('update');
1404
+ }
1405
+ return {
1406
+ agentId,
1407
+ attentionSeq: agent.attentionSeq,
1408
+ readAttentionSeq: agent.readAttentionSeq,
1409
+ unread: agent.unread,
1410
+ changed,
1411
+ };
1412
+ }
1413
+
1414
+ observeAgentStateChange(agentId, options = {}) {
1415
+ this.attemptCodexProviderSessionResolution(agentId, options).catch((error) => {
1416
+ console.warn('Failed to resolve Codex provider session:', error && (error.message || error));
1417
+ });
1418
+ this.attemptProviderSessionTitleResolution(agentId, options).catch((error) => {
1419
+ console.warn('Failed to resolve provider session title:', error && (error.message || error));
1420
+ });
1421
+ }
1422
+
1423
+ attemptCodexProviderSessionResolution(agentId, options = {}) {
1424
+ const agent = this.agents.get(agentId);
1425
+ if (!agent || agent.providerSessionProvider !== 'codex' || agent.providerSessionTemporary !== true) {
1426
+ this.stopCodexProviderSessionResolver(agentId);
1427
+ return Promise.resolve(false);
1428
+ }
1429
+
1430
+ const inFlight = this.codexSessionResolveInFlight.get(agentId);
1431
+ if (inFlight) return inFlight;
1432
+
1433
+ const now = Date.now();
1434
+ const lastAttemptAt = this.codexSessionResolveLastAttemptAt.get(agentId) || 0;
1435
+ if (options.force !== true && now - lastAttemptAt < CODEX_PROVIDER_SESSION_RESOLVE_COOLDOWN_MS) {
1436
+ return Promise.resolve(false);
1437
+ }
1438
+ this.codexSessionResolveLastAttemptAt.set(agentId, now);
1439
+
1440
+ const attempt = this.findCodexSessionForTemporaryAgent(agent)
1441
+ .then((session) => {
1442
+ if (!session || !session.id) return false;
1443
+ return this.resolveProviderSession(agentId, {
1444
+ provider: 'codex',
1445
+ sessionId: session.id,
1446
+ source: 'codex-rollout',
1447
+ title: session.title || '',
1448
+ });
1449
+ })
1450
+ .catch(() => false)
1451
+ .finally(() => {
1452
+ if (this.codexSessionResolveInFlight.get(agentId) === attempt) {
1453
+ this.codexSessionResolveInFlight.delete(agentId);
1454
+ }
1455
+ });
1456
+ this.codexSessionResolveInFlight.set(agentId, attempt);
1457
+ return attempt;
1458
+ }
1459
+
1460
+ attemptProviderSessionTitleResolution(agentId, options = {}) {
1461
+ const agent = this.agents.get(agentId);
1462
+ if (
1463
+ !agent
1464
+ || !agent.providerSessionProvider
1465
+ || !agent.providerSessionId
1466
+ || agent.providerSessionTemporary === true
1467
+ || isTemporaryProviderSessionId(agent.providerSessionId)
1468
+ || String(agent.providerSessionTitle || '').trim()
1469
+ ) {
1470
+ this.stopProviderSessionTitleResolver(agentId);
1471
+ return Promise.resolve(false);
1472
+ }
1473
+
1474
+ const inFlight = this.providerSessionTitleResolveInFlight.get(agentId);
1475
+ if (inFlight) return inFlight;
1476
+
1477
+ const now = Date.now();
1478
+ const lastAttemptAt = this.providerSessionTitleResolveLastAttemptAt.get(agentId) || 0;
1479
+ if (options.force !== true && now - lastAttemptAt < PROVIDER_SESSION_TITLE_RESOLVE_COOLDOWN_MS) {
1480
+ return Promise.resolve(false);
1481
+ }
1482
+ this.providerSessionTitleResolveLastAttemptAt.set(agentId, now);
1483
+
1484
+ const provider = agent.providerSessionProvider;
1485
+ const sessionId = agent.providerSessionId;
1486
+ const attempt = findAgentSession(provider, sessionId, { limit: 200, providerLimit: 200, providerHomeId: agent.providerHomeId || 'default', providerHomes: this.configManager && this.configManager.getSettings ? this.configManager.getSettings().agentHomes : undefined })
1487
+ .then((session) => {
1488
+ const title = String(session && session.title || '').trim().slice(0, 160);
1489
+ if (!title) return false;
1490
+
1491
+ const current = this.agents.get(agentId);
1492
+ if (
1493
+ !current
1494
+ || current.providerSessionProvider !== provider
1495
+ || current.providerSessionId !== sessionId
1496
+ || current.providerSessionTemporary === true
1497
+ || String(current.providerSessionTitle || '').trim()
1498
+ ) {
1499
+ return false;
1500
+ }
1501
+
1502
+ current.providerSessionTitle = title;
1503
+ this.ensurePersistentAgentSession(current);
1504
+ this.updateEngineProviderSessionMetadata(current);
1505
+ this.rememberMainPageProviderSession(current);
1506
+ this.emit('provider-session-updated', {
1507
+ agentId,
1508
+ provider,
1509
+ sessionId,
1510
+ title,
1511
+ temporary: false,
1512
+ });
1513
+ this.emit('update');
1514
+ return true;
1515
+ })
1516
+ .catch(() => false)
1517
+ .finally(() => {
1518
+ if (this.providerSessionTitleResolveInFlight.get(agentId) === attempt) {
1519
+ this.providerSessionTitleResolveInFlight.delete(agentId);
1520
+ }
1521
+ });
1522
+ this.providerSessionTitleResolveInFlight.set(agentId, attempt);
1523
+ return attempt;
1524
+ }
1525
+
1526
+ async findCodexSessionForTemporaryAgent(agent) {
1527
+ const sessions = await listCodexSessions({ codexHome: agent.providerHomePath || undefined, limit: 100, scanLimit: 1000 });
1528
+ const workspace = normalizePathValue(agent.projectWorkspace || agent.cwd);
1529
+ const startedAt = Number(agent.startedAt) || 0;
1530
+ const claimedSessionIds = this.currentProviderSessionIds('codex', agent.id, agent.providerHomeId || 'default');
1531
+ const candidates = sessions
1532
+ .filter(session => {
1533
+ if (!session || !session.id || claimedSessionIds.has(session.id)) return false;
1534
+ const sessionWorkspace = normalizePathValue(session.workspace || session.cwd);
1535
+ if (workspace && !sessionWorkspace) return false;
1536
+ if (workspace && workspace !== sessionWorkspace) return false;
1537
+ const sessionTime = timestampMs(session.createdAt || session.updatedAt);
1538
+ if (!sessionTime || !startedAt) return true;
1539
+ return sessionTime >= startedAt - CODEX_PROVIDER_SESSION_MATCH_GRACE_MS;
1540
+ })
1541
+ .sort((a, b) => {
1542
+ const aTime = timestampMs(a.createdAt || a.updatedAt);
1543
+ const bTime = timestampMs(b.createdAt || b.updatedAt);
1544
+ const aDistance = startedAt && aTime ? Math.abs(aTime - startedAt) : Number.MAX_SAFE_INTEGER;
1545
+ const bDistance = startedAt && bTime ? Math.abs(bTime - startedAt) : Number.MAX_SAFE_INTEGER;
1546
+ if (aDistance !== bDistance) return aDistance - bDistance;
1547
+ return bTime - aTime;
1548
+ });
1549
+
1550
+ return candidates[0] || null;
1551
+ }
1552
+
1553
+ resolveProviderSession(agentId, { provider, sessionId, source, title }) {
1554
+ const agent = this.agents.get(agentId);
1555
+ if (!agent || !provider || !sessionId || isTemporaryProviderSessionId(sessionId)) return false;
1556
+
1557
+ const previousSessionId = agent.providerSessionId || '';
1558
+ const providerSessionTitle = String(title || '').trim().slice(0, 160);
1559
+ agent.providerSessionProvider = provider;
1560
+ agent.providerSessionId = sessionId;
1561
+ agent.providerSessionKey = this.providerSessionKey(provider, sessionId, agent.providerHomeId || '');
1562
+ agent.providerSessionTemporary = false;
1563
+ agent.providerSessionSource = source || agent.providerSessionSource || '';
1564
+ agent.providerSessionResolvedAt = Date.now();
1565
+ if (providerSessionTitle) {
1566
+ agent.providerSessionTitle = providerSessionTitle;
1567
+ }
1568
+
1569
+ this.stopCodexProviderSessionResolver(agentId);
1570
+ this.stopProviderSessionTitleResolver(agentId);
1571
+ this.ensurePersistentAgentSession(agent);
1572
+ this.updateEngineProviderSessionMetadata(agent);
1573
+ this.rememberMainPageProviderSession(agent);
1574
+ this.emit('provider-session-updated', {
1575
+ agentId,
1576
+ provider,
1577
+ sessionId,
1578
+ previousSessionId,
1579
+ temporary: false,
1580
+ });
1581
+ this.emit('update');
1582
+ return true;
1583
+ }
1584
+
1585
+ emitActivityUpdate(sessionId, activityAt) {
1586
+ const now = Number.isFinite(activityAt) ? activityAt : Date.now();
1587
+ const lastEmittedAt = this.lastActivityUpdate.get(sessionId) || 0;
1588
+ if (now - lastEmittedAt < ACTIVITY_UPDATE_INTERVAL_MS) {
1589
+ return;
1590
+ }
1591
+
1592
+ this.lastActivityUpdate.set(sessionId, now);
1593
+ this.emit('update');
1594
+ }
1595
+
1596
+ updateAgentSessionTitle(agent, title) {
1597
+ const sessionTitle = String(title || '').trim().slice(0, 160);
1598
+ if ((agent.task || resumedSessionFromSource(agent.source)) && isGenericSessionTitle(agent, sessionTitle)) {
1599
+ if (agent.sessionTitle && isGenericSessionTitle(agent, agent.sessionTitle)) {
1600
+ agent.sessionTitle = '';
1601
+ return true;
1602
+ }
1603
+ return false;
1604
+ }
1605
+ if (agent.sessionTitle === sessionTitle) {
1606
+ return false;
1607
+ }
1608
+
1609
+ agent.sessionTitle = sessionTitle;
1610
+ return true;
1611
+ }
1612
+
1613
+ getEngineSessionSource(engineName) {
1614
+ const engine = this.engineBridge.getEngine(engineName);
1615
+ if (engine && typeof engine.getSessionSource === 'function') {
1616
+ return engine.getSessionSource();
1617
+ }
1618
+ return 'buffer';
1619
+ }
1620
+
1621
+ resolveAgentShellEnv(shell = '', options = {}) {
1622
+ const now = Date.now();
1623
+ const cacheKey = String(shell || '').trim() || '__default__';
1624
+ const cached = this.agentShellEnvCache.get(cacheKey);
1625
+ const hasMaxAgeOverride = Number.isFinite(options.maxAgeMs);
1626
+ const maxAgeMs = hasMaxAgeOverride
1627
+ ? Math.max(0, options.maxAgeMs)
1628
+ : this.agentShellEnvCacheMs;
1629
+ if (
1630
+ options.force !== true &&
1631
+ cached &&
1632
+ ((!hasMaxAgeOverride && maxAgeMs === 0) || now - cached.resolvedAt < maxAgeMs)
1633
+ ) {
1634
+ return cached.env;
1635
+ }
1636
+
1637
+ let shellEnv = null;
1638
+ try {
1639
+ shellEnv = this.agentShellEnvProvider(shell) || null;
1640
+ } catch (error) {
1641
+ console.warn('Failed to resolve user shell environment for agent:', error && (error.message || error));
1642
+ }
1643
+
1644
+ this.agentShellEnvCache.set(cacheKey, {
1645
+ initialized: true,
1646
+ resolvedAt: now,
1647
+ env: shellEnv,
1648
+ });
1649
+ return shellEnv;
1650
+ }
1651
+
1652
+ buildAgentBaseEnv(agent) {
1653
+ const command = agent?.forkCommand || agent?.command || '';
1654
+ const shell = agent?.category === 'other' && isShellProgram(command)
1655
+ ? (resolveAgentExecutable(command) || command)
1656
+ : '';
1657
+ return buildInteractiveAgentBaseEnv({
1658
+ processEnv: process.env,
1659
+ shellEnv: this.resolveAgentShellEnv(shell),
1660
+ });
1661
+ }
1662
+
1663
+ buildAgentEnv(agentId, agent) {
1664
+ const env = this.buildAgentBaseEnv(agent);
1665
+ if (agent.category === 'coding') {
1666
+ // Prompt policy is meaningful only for shell sessions. Never pass a
1667
+ // shell presentation toggle into a directly launched coding CLI.
1668
+ delete env.FARMING_ANONYMIZE_SHELL_PROMPT;
1669
+ delete env.FARMING_SHELL_CONTROLLED_PROMPT;
1670
+ delete env.FARMING_PRESERVE_SHELL_PROMPT;
1671
+ }
1672
+ if (agent.category === 'other' && isShellProgram(agent.forkCommand || agent.command || '')) {
1673
+ // Like VS Code, the launched shell's own startup files own its prompt.
1674
+ // Never let a different shell's captured prompt leak into this process.
1675
+ for (const key of SHELL_PROMPT_ENV_KEYS) delete env[key];
1676
+ }
1677
+ const pathEntries = [this.cliBinDir, env.PATH || ''].filter(Boolean);
1678
+
1679
+ env.PATH = pathEntries.join(path.delimiter);
1680
+ normalizeInteractiveTerminalEnv(env, {
1681
+ stripRuntimeShims: process.env.FARMING_STRIP_AGENT_LD_LIBRARY_PATH !== '0',
1682
+ stripNodeOptions: process.env.FARMING_STRIP_AGENT_NODE_OPTIONS !== '0',
1683
+ });
1684
+ env.FARMING_AGENT_ID = agentId;
1685
+ env.FARMING_IS_MAIN_AGENT = agent.wantsMain ? '1' : '0';
1686
+ env.FARMING_SKILLS_COMMAND = 'farming skills';
1687
+ env.FARMING_MAIN_WORKSPACE = agent.mainWorkspace || '';
1688
+ env.FARMING_PROJECT_WORKSPACE = agent.projectWorkspace || '';
1689
+
1690
+ if (agent.parentAgentId) {
1691
+ env.FARMING_PARENT_AGENT_ID = agent.parentAgentId;
1692
+ }
1693
+ if (this.controlUrl) {
1694
+ env.FARMING_CONTROL_URL = this.controlUrl;
1695
+ }
1696
+ if (this.tokenFile) {
1697
+ env.FARMING_TOKEN_FILE = this.tokenFile;
1698
+ }
1699
+ if (this.authDisabled) {
1700
+ env.FARMING_DISABLE_AUTH = '1';
1701
+ }
1702
+ if (this.configManager && this.configManager.farmingDir) {
1703
+ env.FARMING_CONFIG_DIR = this.configManager.farmingDir;
1704
+ }
1705
+ if (agent.mainWorkspace) {
1706
+ env.FARMING_SKILLS_FILE = path.join(agent.mainWorkspace, 'FARMING_MAIN_AGENT_SKILLS.md');
1707
+ }
1708
+ if (agent.providerHomePath) {
1709
+ const provider = agent.providerSessionProvider || agentHomeProviderForProgram(agent.forkCommand || agent.command);
1710
+ if (provider === 'codex') env.CODEX_HOME = agent.codexAppServerHomePath || agent.providerHomePath;
1711
+ if (provider === 'claude') env.CLAUDE_CONFIG_DIR = agent.providerHomePath;
1712
+ if (provider === 'opencode') env.OPENCODE_CONFIG_DIR = agent.providerHomePath;
1713
+ if (provider === 'qoder') env.QODER_CONFIG_DIR = agent.providerHomePath;
1714
+ }
1715
+
1716
+ return env;
1717
+ }
1718
+
1719
+ expandWorkspacePath(workspace) {
1720
+ if (typeof workspace !== 'string') return '';
1721
+ const value = workspace.trim();
1722
+ if (!value) return '';
1723
+ if (value === '~') return process.env.HOME || os.homedir();
1724
+ if (value.startsWith('~/')) return path.join(process.env.HOME || os.homedir(), value.slice(2));
1725
+ return value;
1726
+ }
1727
+
1728
+ canCreateForkWorktree(workspace) {
1729
+ const sourceWorkspace = this.expandWorkspacePath(workspace);
1730
+ if (!sourceWorkspace) return false;
1731
+ let current = path.resolve(sourceWorkspace);
1732
+ while (true) {
1733
+ if (fs.existsSync(path.join(current, '.git'))) return true;
1734
+ const parent = path.dirname(current);
1735
+ if (parent === current) return false;
1736
+ current = parent;
1737
+ }
1738
+ }
1739
+
1740
+ resolveMainAgentWorkspace(requestedWorkspace) {
1741
+ const expanded = this.expandWorkspacePath(requestedWorkspace);
1742
+ const baseWorkspace = expanded || (this.configManager ? this.configManager.getWorkspace() : process.env.HOME);
1743
+ const resolvedBase = path.resolve(baseWorkspace);
1744
+ const mainWorkspace = path.basename(resolvedBase) === '.farming'
1745
+ ? resolvedBase
1746
+ : path.join(resolvedBase, '.farming');
1747
+ const projectWorkspace = path.basename(resolvedBase) === '.farming'
1748
+ ? (expanded ? path.dirname(resolvedBase) : resolvedBase)
1749
+ : resolvedBase;
1750
+
1751
+ return {
1752
+ workspace: mainWorkspace,
1753
+ projectWorkspace,
1754
+ selectedWorkspace: resolvedBase,
1755
+ };
1756
+ }
1757
+
1758
+ findActiveMainAgentStart() {
1759
+ const isActive = (agent) => agent && !['dead', 'stopped'].includes(agent.status);
1760
+ const currentMain = this.mainAgentId ? this.agents.get(this.mainAgentId) : null;
1761
+ if (isActive(currentMain)) {
1762
+ return currentMain;
1763
+ }
1764
+
1765
+ for (const agent of this.agents.values()) {
1766
+ if (agent.wantsMain && isActive(agent)) {
1767
+ return agent;
1768
+ }
1769
+ }
1770
+
1771
+ return null;
1772
+ }
1773
+
1774
+ isMainAgentRecord(agentId, agent) {
1775
+ if (agentId === this.mainAgentId) {
1776
+ return true;
1777
+ }
1778
+
1779
+ if (agent.wantsMain !== true || ['dead', 'stopped'].includes(agent.status)) {
1780
+ return false;
1781
+ }
1782
+
1783
+ const currentMain = this.mainAgentId ? this.agents.get(this.mainAgentId) : null;
1784
+ const hasDifferentActiveMain = currentMain
1785
+ && currentMain.id !== agentId
1786
+ && !['dead', 'stopped'].includes(currentMain.status);
1787
+ return !hasDifferentActiveMain;
1788
+ }
1789
+
1790
+ startHeartbeat() {
1791
+ const interval = this.configManager ? this.configManager.getHeartbeatInterval() : 1000;
1792
+ console.log('Starting heartbeat with interval:', interval, 'ms');
1793
+
1794
+ this.heartbeatInterval = setInterval(async () => {
1795
+ if (this.disposed) return;
1796
+ const now = Date.now();
1797
+ if (now - this.lastZombieSweepAt >= ZOMBIE_SWEEP_INTERVAL_MS) {
1798
+ this.lastZombieSweepAt = now;
1799
+ await this.cleanupZombieAgents();
1800
+ }
1801
+
1802
+ if (this.mainAgentId) {
1803
+ const mainAgent = this.agents.get(this.mainAgentId);
1804
+ if (mainAgent && mainAgent.status === 'dead') {
1805
+ this.emit('update');
1806
+ }
1807
+ }
1808
+
1809
+ try {
1810
+ const systemStats = await this.systemMonitor.getSystemStats();
1811
+ this.emit('system-stats', systemStats);
1812
+ } catch (error) {
1813
+ console.error('Failed to get system stats:', error);
1814
+ }
1815
+ }, interval);
1816
+ }
1817
+
1818
+ async dispose(options = {}) {
1819
+ if (this.disposed) return;
1820
+ this.disposed = true;
1821
+ if (this.heartbeatInterval) {
1822
+ clearInterval(this.heartbeatInterval);
1823
+ this.heartbeatInterval = null;
1824
+ }
1825
+ this.codexSessionResolveInFlight.clear();
1826
+ this.codexSessionResolveLastAttemptAt.clear();
1827
+ this.providerSessionTitleResolveInFlight.clear();
1828
+ this.providerSessionTitleResolveLastAttemptAt.clear();
1829
+ this.permissionRestartInFlight.clear();
1830
+ this.runtimeRestartInFlight.clear();
1831
+ this.permissionRestartSuppressedAgentIds.clear();
1832
+ this.inputQueues.clear();
1833
+ if (this.codexAppServerRuntime && typeof this.codexAppServerRuntime.dispose === 'function') {
1834
+ this.codexAppServerRuntime.dispose();
1835
+ }
1836
+ if (this.jsonCliRuntime) {
1837
+ for (const agentId of this.agents.keys()) this.jsonCliRuntime.unregisterAgent(agentId);
1838
+ }
1839
+ if (this.acpRuntime && typeof this.acpRuntime.dispose === 'function') this.acpRuntime.dispose();
1840
+ if (this.engineBridge && typeof this.engineBridge.dispose === 'function') {
1841
+ await this.engineBridge.dispose({
1842
+ preserveHost: options.preserveTerminalHost === true,
1843
+ });
1844
+ }
1845
+ }
1846
+
1847
+ async cleanupZombieAgents() {
1848
+ const now = Date.now();
1849
+ const zombieIds = [];
1850
+ for (const [agentId] of this.agents) {
1851
+ if (this.isZombie(agentId, now)) {
1852
+ zombieIds.push(agentId);
1853
+ }
1854
+ }
1855
+ for (const zombieId of zombieIds) {
1856
+ await this.killAgent(zombieId, { reason: 'zombie-cleanup' });
1857
+ }
1858
+ }
1859
+
1860
+ engineSessionMetadata(agent) {
1861
+ return {
1862
+ agentId: agent.id,
1863
+ command: agent.command,
1864
+ forkCommand: agent.forkCommand,
1865
+ cwd: agent.cwd,
1866
+ projectWorkspace: agent.projectWorkspace || '',
1867
+ mainWorkspace: agent.mainWorkspace || '',
1868
+ wantsMain: agent.wantsMain === true,
1869
+ category: agent.category,
1870
+ launchPermissionMode: agent.launchPermissionMode,
1871
+ parentAgentId: agent.parentAgentId || '',
1872
+ task: agent.task,
1873
+ workflowTemplate: agent.workflowTemplate,
1874
+ source: agent.source,
1875
+ providerSessionProvider: agent.providerSessionProvider,
1876
+ providerHomeId: agent.providerHomeId || '',
1877
+ providerHomePath: agent.providerHomePath || '',
1878
+ providerSessionId: agent.providerSessionId,
1879
+ providerSessionKey: agent.providerSessionKey,
1880
+ providerSessionTemporary: agent.providerSessionTemporary,
1881
+ providerSessionSource: agent.providerSessionSource,
1882
+ providerSessionResolvedAt: agent.providerSessionResolvedAt,
1883
+ providerSessionTitle: agent.providerSessionTitle,
1884
+ codexRuntimeMode: agent.codexRuntimeMode,
1885
+ agentRuntimeMode: agent.agentRuntimeMode || 'terminal',
1886
+ jsonCliState: agent.jsonCliState || '',
1887
+ jsonCliError: agent.jsonCliError || '',
1888
+ acpState: agent.acpState || '',
1889
+ acpError: agent.acpError || '',
1890
+ acpStopReason: agent.acpStopReason || '',
1891
+ acpPendingPermission: agent.acpPendingPermission || null,
1892
+ acpPendingPermissions: Array.isArray(agent.acpPendingPermissions) ? agent.acpPendingPermissions : [],
1893
+ acpPendingElicitation: agent.acpPendingElicitation || null,
1894
+ acpPendingElicitations: Array.isArray(agent.acpPendingElicitations) ? agent.acpPendingElicitations : [],
1895
+ acpActiveElicitations: Array.isArray(agent.acpActiveElicitations) ? agent.acpActiveElicitations : [],
1896
+ acpSessionUpdatedAt: agent.acpSessionUpdatedAt || '',
1897
+ acpSessionRevision: Number(agent.acpSessionRevision) || 0,
1898
+ codexAppServerHomePath: agent.codexAppServerHomePath || '',
1899
+ codexAppServerState: agent.codexAppServerState,
1900
+ codexAppServerEndpoint: agent.codexAppServerEndpoint,
1901
+ codexAppServerThreadId: agent.codexAppServerThreadId,
1902
+ codexAppServerTurnId: agent.codexAppServerTurnId,
1903
+ codexAppServerError: agent.codexAppServerError,
1904
+ codexAppServerPendingRequestId: agent.codexAppServerPendingRequestId,
1905
+ codexAppServerPendingRequestMethod: agent.codexAppServerPendingRequestMethod,
1906
+ codexAppServerPendingRequest: agent.codexAppServerPendingRequest || null,
1907
+ codexCliObserverDeferred: agent.codexCliObserverDeferred === true,
1908
+ forkedFromProviderSessionId: agent.forkedFromProviderSessionId,
1909
+ restartedFromAgentId: agent.restartedFromAgentId,
1910
+ restartedFromAgentIds: agent.restartedFromAgentIds,
1911
+ persistentSessionId: agent.persistentSessionId,
1912
+ customTitle: agent.customTitle,
1913
+ pinned: agent.pinned,
1914
+ projectOrder: finiteOrder(agent.projectOrder),
1915
+ pinnedOrder: finiteOrder(agent.pinnedOrder),
1916
+ startedAt: agent.startedAt,
1917
+ attentionSeq: agent.attentionSeq,
1918
+ readAttentionSeq: agent.readAttentionSeq,
1919
+ attentionUpdatedAt: agent.attentionUpdatedAt,
1920
+ readAttentionAt: agent.readAttentionAt,
1921
+ attentionReason: agent.attentionReason,
1922
+ attentionOutputSeq: agent.attentionOutputSeq,
1923
+ };
1924
+ }
1925
+
1926
+ async createAgentEngineSession(agent, engine, launch) {
1927
+ await engine.createSession({
1928
+ agentId: agent.id,
1929
+ command: launch.command,
1930
+ args: launch.args,
1931
+ cwd: launch.cwd,
1932
+ env: this.buildAgentEnv(agent.id, agent),
1933
+ category: launch.category,
1934
+ metadata: this.engineSessionMetadata(agent),
1935
+ });
1936
+ }
1937
+
1938
+ async startAgent(command, customWorkspace, callback, options = {}) {
1939
+ if (options.wantsMain !== false) {
1940
+ await this.whenRecovered();
1941
+ }
1942
+
1943
+ const wantsMain = options.wantsMain === true || (options.wantsMain !== false && !this.mainAgentId);
1944
+ if (wantsMain) {
1945
+ const existingMainStart = this.findActiveMainAgentStart();
1946
+ if (existingMainStart) {
1947
+ console.log('Main Agent already starting or running:', existingMainStart.id);
1948
+ if (callback) callback(existingMainStart.id);
1949
+ return existingMainStart.id;
1950
+ }
1951
+ }
1952
+
1953
+ const dangerouslySkipPermissions = options.dangerouslySkipPermissions === true
1954
+ || (
1955
+ options.dangerouslySkipPermissions !== false
1956
+ && this.configManager
1957
+ && this.configManager.getDangerouslySkipAgentPermissionsByDefault()
1958
+ );
1959
+ const launch = resolveLaunchCommand(command, {
1960
+ dangerouslySkipPermissions,
1961
+ agentLaunchProfiles: this.configManager && this.configManager.getAgentLaunchProfiles
1962
+ ? this.configManager.getAgentLaunchProfiles()
1963
+ : undefined,
1964
+ codexApprovalMode: options.codexApprovalMode || (
1965
+ dangerouslySkipPermissions
1966
+ ? undefined
1967
+ : (this.configManager && this.configManager.getCodexApprovalMode ? this.configManager.getCodexApprovalMode() : 'approve')
1968
+ ),
1969
+ claudePermissionMode: typeof options.claudePermissionMode === 'string' ? options.claudePermissionMode : undefined,
1970
+ codexModelPreset: this.configManager && this.configManager.getCodexModelPreset
1971
+ ? this.configManager.getCodexModelPreset()
1972
+ : 'gpt-5.5:xhigh',
1973
+ codexModel: this.configManager && this.configManager.getCodexModel
1974
+ ? this.configManager.getCodexModel()
1975
+ : 'gpt-5.5',
1976
+ codexReasoningEffort: this.configManager && this.configManager.getCodexReasoningEffort
1977
+ ? this.configManager.getCodexReasoningEffort()
1978
+ : 'xhigh',
1979
+ codexServiceTier: this.configManager && this.configManager.getCodexServiceTier
1980
+ ? this.configManager.getCodexServiceTier()
1981
+ : 'default',
1982
+ mainAgentSystemPrompt: wantsMain ? renderMainAgentBootstrap() : '',
1983
+ });
1984
+ const program = launch.program;
1985
+ const resolvedSource = typeof options.source === 'string' ? options.source : 'ui';
1986
+ let providerSessionPlan = buildAgentProviderSessionPlan({
1987
+ command,
1988
+ program,
1989
+ args: launch.args,
1990
+ source: resolvedSource,
1991
+ });
1992
+
1993
+ const hasResumeSource = Boolean(resumedSessionFromSource(resolvedSource));
1994
+ if (providerSessionPlan.source === 'resume-source' && hasResumeSource && !hasResumeArg(launch.args)) {
1995
+ providerSessionPlan = buildAgentProviderSessionPlan({
1996
+ command,
1997
+ program,
1998
+ args: launch.args,
1999
+ source: 'ui',
2000
+ });
2001
+ }
2002
+
2003
+ let args = providerSessionPlan.args;
2004
+ const userShellEnv = this.resolveAgentShellEnv('', { maxAgeMs: AGENT_DISCOVERY_CACHE_MAX_AGE_MS });
2005
+ const launchPathEnv = typeof userShellEnv?.PATH === 'string' && userShellEnv.PATH.trim()
2006
+ ? userShellEnv.PATH
2007
+ : (process.env.PATH || '');
2008
+ let spawnProgram = resolveAgentExecutable(program, launchPathEnv) || program;
2009
+ if (path.basename(program) === 'codex') {
2010
+ const codexResolution = resolveCompatibleCodexExecutable(options.requiredCliVersion || '', launchPathEnv);
2011
+ if (!codexResolution.compatible) {
2012
+ if (callback) callback(null, codexResolution.error || 'Codex CLI is not compatible with this session');
2013
+ return null;
2014
+ }
2015
+ spawnProgram = codexResolution.path || spawnProgram;
2016
+ }
2017
+
2018
+ const parentAgentId = typeof options.parentAgentId === 'string' ? options.parentAgentId : '';
2019
+ const parentAgent = parentAgentId ? this.agents.get(parentAgentId) : null;
2020
+ const defaultWorkspace = wantsMain
2021
+ ? (this.configManager ? this.configManager.getWorkspace() : process.env.HOME)
2022
+ : ((parentAgent && (parentAgent.projectWorkspace || parentAgent.cwd)) || process.env.PWD || process.cwd() || process.env.HOME);
2023
+ let workspace = this.expandWorkspacePath(customWorkspace || defaultWorkspace);
2024
+ const explicitProjectWorkspace = !wantsMain && typeof options.projectWorkspace === 'string' && options.projectWorkspace.trim()
2025
+ ? this.expandWorkspacePath(options.projectWorkspace)
2026
+ : '';
2027
+ let projectWorkspace = '';
2028
+
2029
+ if (wantsMain) {
2030
+ const resolvedMain = this.resolveMainAgentWorkspace(customWorkspace || '');
2031
+ const selectedParent = path.basename(resolvedMain.selectedWorkspace) === '.farming'
2032
+ ? path.dirname(resolvedMain.selectedWorkspace)
2033
+ : resolvedMain.selectedWorkspace;
2034
+ let selectedParentExists = false;
2035
+ try {
2036
+ selectedParentExists = fs.statSync(selectedParent).isDirectory();
2037
+ } catch {
2038
+ selectedParentExists = false;
2039
+ }
2040
+ if (!selectedParentExists) {
2041
+ console.log('Workspace does not exist:', selectedParent);
2042
+ if (callback) callback(null, `Workspace does not exist: ${selectedParent}`);
2043
+ return null;
2044
+ }
2045
+ workspace = resolvedMain.workspace;
2046
+ projectWorkspace = resolvedMain.projectWorkspace;
2047
+ fs.mkdirSync(workspace, { recursive: true });
2048
+ ensureMainAgentSkillFiles(workspace);
2049
+ } else {
2050
+ projectWorkspace = workspace;
2051
+ if (explicitProjectWorkspace) {
2052
+ const resolvedProjectWorkspace = path.resolve(explicitProjectWorkspace);
2053
+ const resolvedWorkspace = path.resolve(workspace);
2054
+ try {
2055
+ if (fs.statSync(resolvedProjectWorkspace).isDirectory() && isSameOrDescendantPath(resolvedProjectWorkspace, resolvedWorkspace)) {
2056
+ projectWorkspace = explicitProjectWorkspace;
2057
+ }
2058
+ } catch {
2059
+ projectWorkspace = workspace;
2060
+ }
2061
+ }
2062
+ }
2063
+
2064
+ const logArgs = args.map((arg, index) => (
2065
+ index > 0 && args[index - 1] === '--append-system-prompt'
2066
+ ? '<farming-main-agent-bootstrap>'
2067
+ : arg
2068
+ ));
2069
+ console.log('Starting agent:', program, logArgs, 'workspace:', workspace, spawnProgram !== program ? `resolved: ${spawnProgram}` : '');
2070
+
2071
+ if (!fs.existsSync(workspace)) {
2072
+ console.log('Workspace does not exist:', workspace);
2073
+ if (callback) callback(null, `Workspace does not exist: ${workspace}`);
2074
+ return null;
2075
+ }
2076
+
2077
+ let resolution;
2078
+ try {
2079
+ resolution = this.engineBridge.resolve(command);
2080
+ } catch (error) {
2081
+ if (callback) callback(null, error.message);
2082
+ return null;
2083
+ }
2084
+
2085
+ const agentId = `agent-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
2086
+ const homeProvider = providerSessionPlan.provider || agentHomeProviderForProgram(program);
2087
+ const providerHomeId = typeof options.providerHomeId === 'string' && options.providerHomeId.trim()
2088
+ ? options.providerHomeId.trim()
2089
+ : (providerSessionPlan.providerHomeId || '');
2090
+ const providerHome = homeProvider && providerHomeId && this.configManager && this.configManager.getAgentHome
2091
+ ? this.configManager.getAgentHome(homeProvider, providerHomeId)
2092
+ : null;
2093
+ if (
2094
+ homeProvider
2095
+ && providerHomeId
2096
+ && !providerHome
2097
+ && !options.providerHomePath
2098
+ && this.configManager
2099
+ && typeof this.configManager.getAgentHome === 'function'
2100
+ ) {
2101
+ if (callback) callback(null, `Unknown ${homeProvider} agent home: ${providerHomeId}`);
2102
+ return null;
2103
+ }
2104
+ let providerHomePath = typeof options.providerHomePath === 'string' && options.providerHomePath.trim()
2105
+ ? this.expandWorkspacePath(options.providerHomePath)
2106
+ : (providerHome ? providerHome.path : '');
2107
+ let resolvedProviderHomeId = providerHome ? providerHome.id : (providerHomeId || '');
2108
+ if (
2109
+ homeProvider === 'codex'
2110
+ && !providerHomePath
2111
+ && this.configManager
2112
+ && typeof this.configManager.getAgentHome === 'function'
2113
+ ) {
2114
+ const defaultCodexHome = this.configManager.getAgentHome('codex', 'default');
2115
+ if (defaultCodexHome) {
2116
+ providerHomePath = defaultCodexHome.path;
2117
+ resolvedProviderHomeId = defaultCodexHome.id || 'default';
2118
+ }
2119
+ }
2120
+ const requestedCodexRuntimeMode = options.codexRuntimeMode === 'app-server' || options.codexRuntimeMode === 'cli'
2121
+ ? options.codexRuntimeMode
2122
+ : (this.configManager && typeof this.configManager.getCodexRuntimeMode === 'function'
2123
+ ? this.configManager.getCodexRuntimeMode()
2124
+ : 'cli');
2125
+ const useCodexAppServer = providerSessionPlan.provider === 'codex'
2126
+ && this.codexAppServerRuntime
2127
+ && options.agentRuntimeMode !== 'acp'
2128
+ // The deterministic browser/server fixtures are terminal-only shims,
2129
+ // not an implementation of the Codex App Server protocol. App Server
2130
+ // behavior has its own mock runtime test; keep these legacy PTY tests
2131
+ // on the path they are designed to exercise.
2132
+ && process.env.FARMING_E2E_FAKE_EXECUTABLES !== '1'
2133
+ && normalizeCodexRuntimeMode(requestedCodexRuntimeMode) === 'app-server';
2134
+ const requestedAgentRuntimeMode = ['json', 'acp'].includes(options.agentRuntimeMode)
2135
+ ? options.agentRuntimeMode
2136
+ : 'terminal';
2137
+ const useJsonCli = requestedAgentRuntimeMode === 'json'
2138
+ && ['codex', 'opencode'].includes(providerSessionPlan.provider)
2139
+ && !useCodexAppServer
2140
+ && process.env.FARMING_E2E_FAKE_EXECUTABLES !== '1';
2141
+ const useAcp = requestedAgentRuntimeMode === 'acp'
2142
+ && ['codex', 'claude', 'opencode', 'qoder'].includes(providerSessionPlan.provider)
2143
+ && !useCodexAppServer
2144
+ && (
2145
+ process.env.FARMING_E2E_FAKE_EXECUTABLES !== '1'
2146
+ || process.env.FARMING_E2E_FAKE_ACP_AGENT === '1'
2147
+ );
2148
+ let codexAppServerHomePath = '';
2149
+ if (useCodexAppServer) {
2150
+ try {
2151
+ codexAppServerHomePath = ensureCodexAppServerHome({
2152
+ configDir: this.configManager && this.configManager.farmingDir,
2153
+ agentId,
2154
+ sourceHome: providerHomePath || path.join(os.homedir(), '.codex'),
2155
+ });
2156
+ } catch (error) {
2157
+ if (callback) callback(null, error.message);
2158
+ return null;
2159
+ }
2160
+ }
2161
+ const agentRecord = {
2162
+ id: agentId,
2163
+ command: launch.program,
2164
+ forkCommand: String(command || '').trim() || launch.program,
2165
+ cwd: workspace,
2166
+ output: '',
2167
+ previewText: '',
2168
+ previewSnapshot: null,
2169
+ previewCols: 80,
2170
+ previewRows: 30,
2171
+ sessionTitle: '',
2172
+ status: 'pending',
2173
+ engineName: resolution.engineName,
2174
+ wantsMain,
2175
+ mainWorkspace: wantsMain ? workspace : '',
2176
+ projectWorkspace,
2177
+ category: resolution.spec ? resolution.spec.category : 'other',
2178
+ launchPermissionMode: launch.permissionMode || '',
2179
+ parentAgentId,
2180
+ task: typeof options.task === 'string' ? options.task : '',
2181
+ workflowTemplate: typeof options.workflowTemplate === 'string' ? options.workflowTemplate : '',
2182
+ source: typeof options.source === 'string' ? options.source : 'ui',
2183
+ providerSessionProvider: providerSessionPlan.provider || '',
2184
+ providerHomeId: resolvedProviderHomeId,
2185
+ providerHomePath,
2186
+ providerSessionId: providerSessionPlan.id || '',
2187
+ providerSessionKey: this.providerSessionKey(providerSessionPlan.provider, providerSessionPlan.id, providerHome ? providerHome.id : providerHomeId),
2188
+ providerSessionTemporary: providerSessionPlan.temporary === true,
2189
+ providerSessionSource: providerSessionPlan.source || '',
2190
+ providerSessionResolvedAt: providerSessionPlan.temporary === true ? null : Date.now(),
2191
+ providerSessionTitle: typeof options.providerSessionTitle === 'string' ? options.providerSessionTitle.trim().slice(0, 160) : '',
2192
+ codexRuntimeMode: providerSessionPlan.provider === 'codex'
2193
+ ? (useCodexAppServer ? 'app-server' : 'cli')
2194
+ : '',
2195
+ agentRuntimeMode: useAcp ? 'acp' : (useJsonCli ? 'json' : 'terminal'),
2196
+ jsonCliState: useJsonCli ? 'idle' : '',
2197
+ jsonCliError: '',
2198
+ jsonCliTranscriptUpdatedAt: '',
2199
+ jsonCliEvents: Array.isArray(options.jsonCliEvents) ? options.jsonCliEvents : [],
2200
+ acpState: useAcp ? 'connecting' : '',
2201
+ acpError: '',
2202
+ acpStopReason: '',
2203
+ acpPendingPermission: null,
2204
+ acpPendingPermissions: [],
2205
+ acpPendingElicitation: null,
2206
+ acpPendingElicitations: [],
2207
+ acpActiveElicitations: [],
2208
+ acpSessionUpdatedAt: '',
2209
+ acpSessionRevision: 0,
2210
+ codexAppServerHomePath,
2211
+ codexAppServerState: useCodexAppServer ? 'connecting' : '',
2212
+ codexAppServerEndpoint: '',
2213
+ codexAppServerThreadId: '',
2214
+ codexAppServerTurnId: '',
2215
+ codexAppServerError: '',
2216
+ codexAppServerPendingRequestId: '',
2217
+ codexAppServerPendingRequestMethod: '',
2218
+ codexAppServerPendingRequest: null,
2219
+ codexCliObserverDeferred: false,
2220
+ forkedFromProviderSessionId: providerSessionPlan.forkedFromProviderSessionId || '',
2221
+ restartedFromAgentId: typeof options.restartedFromAgentId === 'string' ? options.restartedFromAgentId : '',
2222
+ restartedFromAgentIds: Array.isArray(options.restartedFromAgentIds)
2223
+ ? Array.from(new Set(options.restartedFromAgentIds.filter(id => typeof id === 'string' && id)))
2224
+ : [],
2225
+ runtimeSwitchVerifiedSessionId: typeof options.runtimeSwitchVerifiedSessionId === 'string'
2226
+ ? options.runtimeSwitchVerifiedSessionId
2227
+ : '',
2228
+ persistentSessionId: '',
2229
+ customTitle: typeof options.customTitle === 'string' ? options.customTitle.trim().slice(0, 80) : '',
2230
+ terminalBusy: null,
2231
+ shellCwd: '',
2232
+ shellLastExitCode: null,
2233
+ shellLastEvent: '',
2234
+ shellCommand: '',
2235
+ shellLastCommand: '',
2236
+ shellCommandStartedAt: null,
2237
+ shellLastCommandStartedAt: null,
2238
+ shellLastCommandFinishedAt: null,
2239
+ shellLastCommandDurationMs: null,
2240
+ pinned: false,
2241
+ projectOrder: finiteOrder(options.projectOrder),
2242
+ pinnedOrder: finiteOrder(options.pinnedOrder),
2243
+ attentionSeq: 0,
2244
+ readAttentionSeq: 0,
2245
+ attentionUpdatedAt: null,
2246
+ readAttentionAt: null,
2247
+ attentionReason: '',
2248
+ attentionOutputSeq: null,
2249
+ unread: false,
2250
+ archived: false,
2251
+ archivedAt: null,
2252
+ canForkNewWorktree: this.canCreateForkWorktree(projectWorkspace || workspace),
2253
+ validated: true,
2254
+ engineStarted: false,
2255
+ startedAt: Date.now(),
2256
+ lastOutputSeq: null,
2257
+ attentionAutoReadNext: options.autoReadInitialAttention === true,
2258
+ attentionTrackingReady: false,
2259
+ lastObservedTurnActive: false,
2260
+ attentionSuppressUntil: 0
2261
+ };
2262
+
2263
+ ensureAgentOrders(agentRecord, Array.from(this.agents.values()));
2264
+ this.agents.set(agentId, agentRecord);
2265
+
2266
+ this.lastActivity.set(agentId, Date.now());
2267
+
2268
+ this.emit('update');
2269
+
2270
+ try {
2271
+ if (useCodexAppServer) {
2272
+ try {
2273
+ const prepared = await this.codexAppServerRuntime.prepareAgent({
2274
+ agentId,
2275
+ codexHome: agentRecord.codexAppServerHomePath,
2276
+ executable: spawnProgram,
2277
+ env: this.buildAgentEnv(agentId, agentRecord),
2278
+ cwd: workspace,
2279
+ workspaceRoot: projectWorkspace || workspace,
2280
+ resumeThreadId: agentRecord.providerSessionTemporary ? '' : agentRecord.providerSessionId,
2281
+ temporaryThreadId: agentRecord.providerSessionTemporary ? agentRecord.providerSessionId : '',
2282
+ approvalMode: agentRecord.launchPermissionMode || (
2283
+ this.configManager && this.configManager.getCodexApprovalMode
2284
+ ? this.configManager.getCodexApprovalMode()
2285
+ : 'approve'
2286
+ ),
2287
+ model: this.configManager && this.configManager.getCodexModel
2288
+ ? this.configManager.getCodexModel()
2289
+ : 'gpt-5.5',
2290
+ reasoningEffort: this.configManager && this.configManager.getCodexReasoningEffort
2291
+ ? this.configManager.getCodexReasoningEffort()
2292
+ : 'xhigh',
2293
+ serviceTier: this.configManager && this.configManager.getCodexServiceTier
2294
+ ? this.configManager.getCodexServiceTier()
2295
+ : 'default',
2296
+ developerInstructions: wantsMain ? renderMainAgentBootstrap() : '',
2297
+ });
2298
+ agentRecord.providerSessionId = prepared.threadId;
2299
+ agentRecord.providerSessionKey = this.providerSessionKey(
2300
+ 'codex',
2301
+ prepared.threadId,
2302
+ agentRecord.providerHomeId || 'default'
2303
+ );
2304
+ agentRecord.providerSessionTemporary = false;
2305
+ agentRecord.providerSessionSource = prepared.resumed ? 'app-server-resume' : 'app-server-start';
2306
+ agentRecord.providerSessionResolvedAt = Date.now();
2307
+ agentRecord.codexAppServerState = 'idle';
2308
+ agentRecord.codexAppServerEndpoint = prepared.endpoint;
2309
+ agentRecord.codexAppServerThreadId = prepared.threadId;
2310
+ agentRecord.codexAppServerError = '';
2311
+ // App Server mode has no embedded CLI observer. Its chat and
2312
+ // lifecycle both come from the structured App Server connection.
2313
+ agentRecord.codexCliObserverDeferred = false;
2314
+ } catch (error) {
2315
+ const reason = error && error.message ? error.message : String(error);
2316
+ throw new Error(`Codex App Server failed to start: ${reason}`, { cause: error });
2317
+ }
2318
+ }
2319
+
2320
+ if (useJsonCli) {
2321
+ this.jsonCliRuntime.registerAgent({
2322
+ agentId,
2323
+ provider: providerSessionPlan.provider,
2324
+ executable: spawnProgram,
2325
+ env: this.buildAgentEnv(agentId, agentRecord),
2326
+ cwd: workspace,
2327
+ sessionId: agentRecord.providerSessionTemporary ? '' : agentRecord.providerSessionId,
2328
+ approvalMode: agentRecord.launchPermissionMode || 'approve',
2329
+ autoApprove: options.dangerouslySkipPermissions === true,
2330
+ initialEvents: agentRecord.jsonCliEvents,
2331
+ });
2332
+ }
2333
+
2334
+ if (useAcp) {
2335
+ const prepared = await this.acpRuntime.prepareAgent({
2336
+ agentId,
2337
+ provider: providerSessionPlan.provider,
2338
+ executable: spawnProgram,
2339
+ env: this.buildAgentEnv(agentId, agentRecord),
2340
+ cwd: workspace,
2341
+ sessionId: agentRecord.providerSessionTemporary ? '' : agentRecord.providerSessionId,
2342
+ historyMode: options.acpHistoryMode === 'resume' ? 'resume' : 'load',
2343
+ approvalMode: agentRecord.launchPermissionMode || 'approve',
2344
+ model: this.configManager && this.configManager.getCodexModel
2345
+ ? this.configManager.getCodexModel()
2346
+ : '',
2347
+ reasoningEffort: this.configManager && this.configManager.getCodexReasoningEffort
2348
+ ? this.configManager.getCodexReasoningEffort()
2349
+ : '',
2350
+ serviceTier: this.configManager && this.configManager.getCodexServiceTier
2351
+ ? this.configManager.getCodexServiceTier()
2352
+ : '',
2353
+ });
2354
+ agentRecord.providerSessionId = prepared.sessionId;
2355
+ agentRecord.providerSessionKey = this.providerSessionKey(
2356
+ providerSessionPlan.provider,
2357
+ prepared.sessionId,
2358
+ agentRecord.providerHomeId || 'default'
2359
+ );
2360
+ agentRecord.providerSessionTemporary = false;
2361
+ agentRecord.providerSessionSource = `acp-${prepared.historyMode}`;
2362
+ agentRecord.providerSessionResolvedAt = Date.now();
2363
+ agentRecord.acpState = 'idle';
2364
+ agentRecord.acpError = '';
2365
+ }
2366
+
2367
+ agentRecord.persistentSessionId = this.ensurePersistentAgentSession(agentRecord);
2368
+ if (!useCodexAppServer && !useJsonCli && !useAcp) {
2369
+ const engineLaunch = {
2370
+ command: spawnProgram,
2371
+ args,
2372
+ cwd: workspace,
2373
+ category: resolution.spec ? resolution.spec.category : 'shell',
2374
+ };
2375
+ await this.createAgentEngineSession(agentRecord, resolution.engine, engineLaunch);
2376
+ }
2377
+
2378
+ const agent = this.agents.get(agentId);
2379
+ if (agent && agent.status === 'pending') {
2380
+ agent.status = 'running';
2381
+
2382
+ const currentMainAgent = this.mainAgentId ? this.agents.get(this.mainAgentId) : null;
2383
+ const canBecomeMain = !this.mainAgentId || !currentMainAgent || currentMainAgent.status === 'dead';
2384
+ if (agent.wantsMain && canBecomeMain) {
2385
+ this.mainAgentId = agentId;
2386
+ }
2387
+ }
2388
+
2389
+ this.activateProviderSessionTracking(agentId);
2390
+ if (callback) callback(agentId);
2391
+ this.emit('update');
2392
+ return agentId;
2393
+ } catch (error) {
2394
+ console.error('Failed to start agent:', error);
2395
+ this.agents.delete(agentId);
2396
+ this.lastActivity.delete(agentId);
2397
+ this.lastActivityUpdate.delete(agentId);
2398
+ this.outputEvents.delete(agentId);
2399
+ this.lastResizeByAgent.delete(agentId);
2400
+ this.stopCodexProviderSessionResolver(agentId);
2401
+ if (this.codexAppServerRuntime && typeof this.codexAppServerRuntime.unregisterAgent === 'function') {
2402
+ this.codexAppServerRuntime.unregisterAgent(agentId);
2403
+ }
2404
+ if (this.acpRuntime) this.acpRuntime.unregisterAgent(agentId);
2405
+
2406
+ if (this.mainAgentId === agentId) {
2407
+ this.mainAgentId = null;
2408
+ }
2409
+
2410
+ this.emit('update');
2411
+ if (callback) callback(null, error.message);
2412
+ return null;
2413
+ }
2414
+ }
2415
+
2416
+ async sendInput(agentId, input) {
2417
+ const previous = this.inputQueues.get(agentId) || Promise.resolve();
2418
+ const next = previous
2419
+ .catch(() => {})
2420
+ .then(() => this.sendInputNow(agentId, input));
2421
+
2422
+ this.inputQueues.set(agentId, next);
2423
+ try {
2424
+ await next;
2425
+ } finally {
2426
+ if (this.inputQueues.get(agentId) === next) {
2427
+ this.inputQueues.delete(agentId);
2428
+ }
2429
+ }
2430
+ }
2431
+
2432
+ codexAppServerOptionsForAgent(agent, message = '') {
2433
+ const codexResolution = resolveCompatibleCodexExecutable(agent.requiredCliVersion || '');
2434
+ if (!codexResolution.compatible) {
2435
+ throw new Error(codexResolution.error || 'Codex CLI is not compatible with App Server mode');
2436
+ }
2437
+
2438
+ return {
2439
+ agentId: agent.id,
2440
+ codexHome: agent.codexAppServerHomePath || '',
2441
+ executable: codexResolution.path || resolveAgentExecutable(agent.command) || agent.command,
2442
+ env: this.buildAgentEnv(agent.id, agent),
2443
+ cwd: agent.cwd,
2444
+ workspaceRoot: agent.projectWorkspace || agent.cwd,
2445
+ threadId: agent.codexAppServerThreadId || agent.providerSessionId || '',
2446
+ approvalMode: agent.launchPermissionMode || (
2447
+ this.configManager && this.configManager.getCodexApprovalMode
2448
+ ? this.configManager.getCodexApprovalMode()
2449
+ : 'approve'
2450
+ ),
2451
+ model: this.configManager && this.configManager.getCodexModel
2452
+ ? this.configManager.getCodexModel()
2453
+ : 'gpt-5.5',
2454
+ reasoningEffort: this.configManager && this.configManager.getCodexReasoningEffort
2455
+ ? this.configManager.getCodexReasoningEffort()
2456
+ : 'xhigh',
2457
+ serviceTier: this.configManager && this.configManager.getCodexServiceTier
2458
+ ? this.configManager.getCodexServiceTier()
2459
+ : 'default',
2460
+ message,
2461
+ };
2462
+ }
2463
+
2464
+ async sendComposerMessage(agentId, message) {
2465
+ const previous = this.inputQueues.get(agentId) || Promise.resolve();
2466
+ const next = previous
2467
+ .catch(() => {})
2468
+ .then(() => this.sendComposerMessageNow(agentId, message));
2469
+
2470
+ this.inputQueues.set(agentId, next);
2471
+ try {
2472
+ return await next;
2473
+ } finally {
2474
+ if (this.inputQueues.get(agentId) === next) {
2475
+ this.inputQueues.delete(agentId);
2476
+ }
2477
+ }
2478
+ }
2479
+
2480
+ async sendComposerMessageNow(agentId, message) {
2481
+ const agent = this.agents.get(agentId);
2482
+ if (!agent) throw new Error('Agent not found');
2483
+ const prompt = Array.isArray(message) ? message : [{ type: 'text', text: String(message || '') }];
2484
+ const text = prompt
2485
+ .filter(content => content?.type === 'text')
2486
+ .map(content => String(content.text || ''))
2487
+ .join('')
2488
+ .trim();
2489
+ if (prompt.length === 0 || (!text && !prompt.some(content => content?.type !== 'text'))) {
2490
+ throw new Error('Composer message is empty');
2491
+ }
2492
+
2493
+ if (
2494
+ agent.providerSessionProvider === 'codex'
2495
+ && agent.codexRuntimeMode === 'app-server'
2496
+ && this.codexAppServerRuntime
2497
+ && typeof this.codexAppServerRuntime.submitComposerMessage === 'function'
2498
+ ) {
2499
+ const result = await this.codexAppServerRuntime.submitComposerMessage(
2500
+ this.codexAppServerOptionsForAgent(agent, text)
2501
+ );
2502
+ agent.codexAppServerThreadId = result.threadId;
2503
+ agent.codexAppServerTurnId = result.turnId;
2504
+ agent.codexAppServerState = 'working';
2505
+ agent.codexAppServerError = '';
2506
+ this.lastActivity.set(agentId, Date.now());
2507
+ this.emit('update');
2508
+ return result;
2509
+ }
2510
+
2511
+ if (isJsonCliAgent(agent)) {
2512
+ const result = await this.jsonCliRuntime.submitComposerMessage(agentId, text, {
2513
+ approvalMode: agent.launchPermissionMode || 'approve',
2514
+ });
2515
+ agent.providerSessionId = result.sessionId || agent.providerSessionId;
2516
+ agent.providerSessionTemporary = !agent.providerSessionId;
2517
+ this.ensurePersistentAgentSession(agent);
2518
+ return { kind: 'json', sessionId: agent.providerSessionId };
2519
+ }
2520
+
2521
+ if (isAcpAgent(agent)) {
2522
+ const result = await this.acpRuntime.prompt(agentId, prompt);
2523
+ agent.acpState = 'idle';
2524
+ agent.acpStopReason = result.stopReason || '';
2525
+ this.ensurePersistentAgentSession(agent);
2526
+ return { kind: 'acp', ...result };
2527
+ }
2528
+
2529
+ await this.sendInputNow(agentId, [{ type: 'paste', text }, '\r']);
2530
+ return { kind: 'terminal' };
2531
+ }
2532
+
2533
+ respondToCodexAppServerRequest(agentId, requestId, result, options = {}) {
2534
+ const agent = this.agents.get(agentId);
2535
+ if (!agent) throw new Error('Agent not found');
2536
+ if (agent.providerSessionProvider !== 'codex' || agent.codexRuntimeMode !== 'app-server') {
2537
+ throw new Error('This Agent is not using the Codex App Server runtime');
2538
+ }
2539
+ const resolvedRequestId = String(requestId || '');
2540
+ if (!resolvedRequestId || resolvedRequestId !== agent.codexAppServerPendingRequestId) {
2541
+ throw new Error('Codex App Server request is no longer pending');
2542
+ }
2543
+ if (!this.codexAppServerRuntime) throw new Error('Codex App Server runtime is unavailable');
2544
+
2545
+ const response = options.reject === true
2546
+ ? this.codexAppServerRuntime.rejectAgentServerRequest(
2547
+ agentId,
2548
+ resolvedRequestId,
2549
+ { code: -32000, message: String(options.reason || 'Rejected by Farming') }
2550
+ )
2551
+ : this.codexAppServerRuntime.resolveAgentServerRequest(agentId, resolvedRequestId, result);
2552
+ agent.codexAppServerPendingRequestId = '';
2553
+ agent.codexAppServerPendingRequestMethod = '';
2554
+ agent.codexAppServerPendingRequest = null;
2555
+ agent.codexAppServerState = agent.codexAppServerTurnId ? 'working' : 'idle';
2556
+ this.emit('update');
2557
+ return response;
2558
+ }
2559
+
2560
+ assertCodexAppServerGoalAgent(agentId) {
2561
+ const agent = this.agents.get(agentId);
2562
+ if (!agent) throw new Error('Agent not found');
2563
+ if (agent.providerSessionProvider !== 'codex' || agent.codexRuntimeMode !== 'app-server') {
2564
+ throw new Error('This Agent is not using the Codex App Server runtime');
2565
+ }
2566
+ if (!this.codexAppServerRuntime) throw new Error('Codex App Server runtime is unavailable');
2567
+ if (!(agent.codexAppServerThreadId || agent.providerSessionId)) {
2568
+ throw new Error('Codex App Server thread id is not available yet');
2569
+ }
2570
+ return agent;
2571
+ }
2572
+
2573
+ async getCodexAppServerGoal(agentId) {
2574
+ const agent = this.assertCodexAppServerGoalAgent(agentId);
2575
+ const goal = await this.codexAppServerRuntime.getAgentGoal(this.codexAppServerOptionsForAgent(agent));
2576
+ agent.codexAppServerGoal = goal || null;
2577
+ this.emit('update');
2578
+ return goal || null;
2579
+ }
2580
+
2581
+ getCodexAppServerTranscript(agentId, options = {}) {
2582
+ const agent = this.assertCodexAppServerGoalAgent(agentId);
2583
+ if (!this.codexAppServerRuntime || typeof this.codexAppServerRuntime.getAgentTranscript !== 'function') {
2584
+ throw new Error('Codex App Server transcript is unavailable');
2585
+ }
2586
+ return this.codexAppServerRuntime.getAgentTranscript(agent.id, options);
2587
+ }
2588
+
2589
+ getJsonCliTranscript(agentId, options = {}) {
2590
+ const agent = this.agents.get(agentId);
2591
+ if (!agent) throw new Error('Agent not found');
2592
+ if (!isJsonCliAgent(agent)) throw new Error('Agent is not using the JSON CLI runtime');
2593
+ return this.jsonCliRuntime.getTranscript(agentId, options);
2594
+ }
2595
+
2596
+ getAcpSession(agentId, options = {}) {
2597
+ const agent = this.agents.get(agentId);
2598
+ if (!agent) throw new Error('Agent not found');
2599
+ if (!isAcpAgent(agent)) throw new Error('Agent is not using the ACP runtime');
2600
+ return this.acpRuntime.getSession(agentId, options);
2601
+ }
2602
+
2603
+ getAcpTranscript(agentId, options = {}) {
2604
+ const agent = this.agents.get(agentId);
2605
+ if (!agent) throw new Error('Agent not found');
2606
+ if (!isAcpAgent(agent)) throw new Error('Agent is not using the ACP runtime');
2607
+ return this.acpRuntime.getTranscriptSession(agentId, options);
2608
+ }
2609
+
2610
+ getAcpToolDetail(agentId, toolCallId) {
2611
+ const agent = this.agents.get(agentId);
2612
+ if (!agent) throw new Error('Agent not found');
2613
+ if (!isAcpAgent(agent)) throw new Error('Agent is not using the ACP runtime');
2614
+ const entry = this.acpRuntime.getToolEntry(agentId, toolCallId);
2615
+ if (!entry) throw new Error('ACP tool call not found');
2616
+ const subagentSessionId = String(entry?._meta?.subagent_session_info?.session_id || '');
2617
+ const subagentSession = subagentSessionId
2618
+ ? this.acpRuntime.getSubagentTranscriptSession(agentId, subagentSessionId, { maxTurns: 12 })
2619
+ : null;
2620
+ return {
2621
+ toolCallId: String(toolCallId || ''),
2622
+ detail: acpToolDetail(entry),
2623
+ changes: acpToolChanges(entry),
2624
+ ...(subagentSession ? { subagentSession } : {}),
2625
+ terminals: (Array.isArray(entry.content) ? entry.content : [])
2626
+ .filter(block => block?.type === 'terminal')
2627
+ .map(block => ({ terminalId: String(block.terminalId || ''), ...(block.terminal ? { terminal: block.terminal } : {}) })),
2628
+ };
2629
+ }
2630
+
2631
+ killAcpTerminal(agentId, terminalId) {
2632
+ this.getAcpSession(agentId);
2633
+ return this.acpRuntime.killTerminal(agentId, terminalId);
2634
+ }
2635
+
2636
+ inputAcpTerminal(agentId, terminalId, input) {
2637
+ this.getAcpSession(agentId);
2638
+ return this.acpRuntime.inputTerminal(agentId, terminalId, input);
2639
+ }
2640
+
2641
+ resizeAcpTerminal(agentId, terminalId, cols, rows) {
2642
+ this.getAcpSession(agentId);
2643
+ return this.acpRuntime.resizeTerminal(agentId, terminalId, cols, rows);
2644
+ }
2645
+
2646
+ cancelAcpSubagent(agentId, sessionId) {
2647
+ this.getAcpSession(agentId);
2648
+ return this.acpRuntime.cancelSubagent(agentId, sessionId);
2649
+ }
2650
+
2651
+ decideAcpPatch(agentId, toolCallId, requestedPath, decision) {
2652
+ this.getAcpSession(agentId);
2653
+ return this.acpRuntime.decidePatch(agentId, toolCallId, requestedPath, decision);
2654
+ }
2655
+
2656
+ getAcpReviewChanges(agentId, toolCallIds) {
2657
+ const agent = this.agents.get(agentId);
2658
+ if (!agent) throw new Error('Agent not found');
2659
+ if (!isAcpAgent(agent)) throw new Error('Agent is not using the ACP runtime');
2660
+ if (!Array.isArray(toolCallIds) || toolCallIds.length === 0 || toolCallIds.length > 256) {
2661
+ throw new Error('ACP review tool calls are invalid');
2662
+ }
2663
+ return toolCallIds.flatMap(toolCallId => {
2664
+ if (typeof toolCallId !== 'string' || !toolCallId.trim()) {
2665
+ throw new Error('ACP review tool calls are invalid');
2666
+ }
2667
+ const entry = this.acpRuntime.getToolEntry(agentId, toolCallId.trim());
2668
+ if (!entry) throw new Error('ACP tool call not found');
2669
+ return acpToolReviewChanges(entry);
2670
+ });
2671
+ }
2672
+
2673
+ listAcpSessions(agentId, options = {}) {
2674
+ const agent = this.agents.get(agentId);
2675
+ if (!agent) throw new Error('Agent not found');
2676
+ if (!isAcpAgent(agent)) throw new Error('Agent is not using the ACP runtime');
2677
+ return this.acpRuntime.listSessions(agentId, options);
2678
+ }
2679
+
2680
+ respondToAcpPermission(agentId, requestId, optionId, cancelled = false) {
2681
+ const agent = this.agents.get(agentId);
2682
+ if (!agent) throw new Error('Agent not found');
2683
+ if (!isAcpAgent(agent)) throw new Error('Agent is not using the ACP runtime');
2684
+ return this.acpRuntime.respondPermission(agentId, requestId, optionId, cancelled);
2685
+ }
2686
+
2687
+ respondToAcpElicitation(agentId, requestId, action, content) {
2688
+ const agent = this.agents.get(agentId);
2689
+ if (!agent) throw new Error('Agent not found');
2690
+ if (!isAcpAgent(agent)) throw new Error('Agent is not using the ACP runtime');
2691
+ return this.acpRuntime.respondElicitation(agentId, requestId, action, content);
2692
+ }
2693
+
2694
+ authenticateAcpAgent(agentId, methodId) {
2695
+ this.getAcpSession(agentId);
2696
+ return this.acpRuntime.authenticate(agentId, methodId);
2697
+ }
2698
+
2699
+ forkAcpSession(agentId, options = {}) {
2700
+ this.getAcpSession(agentId);
2701
+ return this.acpRuntime.forkSession(agentId, options);
2702
+ }
2703
+
2704
+ deleteAcpSession(agentId, sessionId) {
2705
+ this.getAcpSession(agentId);
2706
+ return this.acpRuntime.deleteSession(agentId, sessionId);
2707
+ }
2708
+
2709
+ closeAcpSession(agentId) {
2710
+ this.getAcpSession(agentId);
2711
+ return this.acpRuntime.closeSession(agentId);
2712
+ }
2713
+
2714
+ setAcpSessionMode(agentId, modeId) {
2715
+ this.getAcpSession(agentId);
2716
+ return this.acpRuntime.setSessionMode(agentId, modeId);
2717
+ }
2718
+
2719
+ setAcpSessionConfigOption(agentId, configId, value) {
2720
+ this.getAcpSession(agentId);
2721
+ return this.acpRuntime.setSessionConfigOption(agentId, configId, value);
2722
+ }
2723
+
2724
+ async setCodexAppServerGoal(agentId, patch = {}) {
2725
+ const agent = this.assertCodexAppServerGoalAgent(agentId);
2726
+ const objective = typeof patch.objective === 'string' ? patch.objective.trim().slice(0, 4000) : undefined;
2727
+ const status = typeof patch.status === 'string' ? patch.status : undefined;
2728
+ let tokenBudget;
2729
+ if (Object.prototype.hasOwnProperty.call(patch, 'tokenBudget')) {
2730
+ tokenBudget = Number.isFinite(Number(patch.tokenBudget)) && Number(patch.tokenBudget) > 0
2731
+ ? Math.floor(Number(patch.tokenBudget))
2732
+ : null;
2733
+ }
2734
+ if (objective !== undefined && !objective) {
2735
+ throw new Error('Codex goal objective is required');
2736
+ }
2737
+ const goal = await this.codexAppServerRuntime.setAgentGoal({
2738
+ ...this.codexAppServerOptionsForAgent(agent),
2739
+ ...(objective !== undefined ? { objective } : {}),
2740
+ ...(status !== undefined ? { status } : {}),
2741
+ ...(Object.prototype.hasOwnProperty.call(patch, 'tokenBudget') ? { tokenBudget } : {}),
2742
+ });
2743
+ agent.codexAppServerGoal = goal || null;
2744
+ this.emit('update');
2745
+ return goal || null;
2746
+ }
2747
+
2748
+ async clearCodexAppServerGoal(agentId) {
2749
+ const agent = this.assertCodexAppServerGoalAgent(agentId);
2750
+ await this.codexAppServerRuntime.clearAgentGoal(this.codexAppServerOptionsForAgent(agent));
2751
+ agent.codexAppServerGoal = null;
2752
+ this.emit('update');
2753
+ return null;
2754
+ }
2755
+
2756
+ async sendInputNow(agentId, input) {
2757
+ const agent = this.agents.get(agentId);
2758
+ if (!agent) return;
2759
+ // The observer PTY is optional telemetry for App Server Agents. Its
2760
+ // disappearance must never redefine the App Server Agent as dead.
2761
+ if (isCodexAppServerAgent(agent) || isJsonCliAgent(agent) || isAcpAgent(agent)) return;
2762
+
2763
+ const engine = this.engineBridge.getEngine(agent.engineName);
2764
+ if (!engine) return;
2765
+
2766
+ for (let attempt = 0; ; attempt += 1) {
2767
+ try {
2768
+ await engine.sendInput(agentId, input);
2769
+ this.observeAgentStateChange(agentId, { force: true });
2770
+ return;
2771
+ } catch (error) {
2772
+ const delay = INPUT_SESSION_RETRY_DELAYS_MS[attempt];
2773
+ if (!isSessionNotAvailableError(error) || delay === undefined) {
2774
+ console.error('Failed to send input:', error);
2775
+ if (isSessionNotAvailableError(error)) {
2776
+ this.markAgentSessionDead(agentId, error);
2777
+ }
2778
+ return;
2779
+ }
2780
+ await sleep(delay);
2781
+ }
2782
+ }
2783
+ }
2784
+
2785
+ markAgentSessionDead(agentId, error) {
2786
+ const agent = this.agents.get(agentId);
2787
+ if (!agent || agent.status === 'dead') return;
2788
+ if (isCodexAppServerAgent(agent)) return;
2789
+
2790
+ const message = error && error.message ? error.message : String(error || 'Session not available');
2791
+ agent.status = 'dead';
2792
+ agent.engineStatus = 'dead';
2793
+ agent.terminalBusy = false;
2794
+ agent.exitedAt = Date.now();
2795
+ agent.output = trimSessionOutput(`${agent.output || ''}\n${message}`);
2796
+ this.observeAgentStateChange(agentId, { force: true });
2797
+ this.emit('update');
2798
+ }
2799
+
2800
+ async interruptAgent(agentId) {
2801
+ const agent = this.agents.get(agentId);
2802
+ if (!agent) return;
2803
+
2804
+ try {
2805
+ if (
2806
+ agent.providerSessionProvider === 'codex'
2807
+ && agent.codexRuntimeMode === 'app-server'
2808
+ && this.codexAppServerRuntime
2809
+ && typeof this.codexAppServerRuntime.interruptAgent === 'function'
2810
+ ) {
2811
+ try {
2812
+ const interrupted = await this.codexAppServerRuntime.interruptAgent(agentId);
2813
+ if (interrupted) {
2814
+ agent.codexAppServerState = 'interrupting';
2815
+ this.emit('update');
2816
+ return;
2817
+ }
2818
+ agent.codexAppServerError = 'Codex App Server interrupt is not available';
2819
+ this.emit('update');
2820
+ return;
2821
+ } catch (error) {
2822
+ agent.codexAppServerError = `Codex App Server interrupt failed: ${error && (error.message || error)}`;
2823
+ this.emit('update');
2824
+ return;
2825
+ }
2826
+ }
2827
+ if (isJsonCliAgent(agent)) {
2828
+ this.jsonCliRuntime.interruptAgent(agentId);
2829
+ return;
2830
+ }
2831
+ if (isAcpAgent(agent)) {
2832
+ await this.acpRuntime.cancel(agentId);
2833
+ return;
2834
+ }
2835
+ const engine = this.engineBridge.getEngine(agent.engineName);
2836
+ if (!engine) return;
2837
+
2838
+ const input = interruptInputForAgent(agent);
2839
+ if (engine.interruptSession) {
2840
+ await engine.interruptSession(agentId, input);
2841
+ } else {
2842
+ await engine.sendInput(agentId, input);
2843
+ }
2844
+ } catch (error) {
2845
+ console.error('Failed to interrupt agent:', error);
2846
+ if (isSessionNotAvailableError(error)) {
2847
+ this.markAgentSessionDead(agentId, error);
2848
+ }
2849
+ }
2850
+ }
2851
+
2852
+ async resizeAgentSession(agentId, cols, rows) {
2853
+ const agent = this.agents.get(agentId);
2854
+ if (!agent) return;
2855
+ if (isCodexAppServerAgent(agent) || isAcpAgent(agent) || isJsonCliAgent(agent)) return;
2856
+
2857
+ const nextCols = Math.floor(Number(cols));
2858
+ const nextRows = Math.floor(Number(rows));
2859
+ if (
2860
+ !Number.isFinite(nextCols) ||
2861
+ !Number.isFinite(nextRows) ||
2862
+ nextCols < MIN_TERMINAL_RESIZE_COLS ||
2863
+ nextRows < MIN_TERMINAL_RESIZE_ROWS
2864
+ ) {
2865
+ return;
2866
+ }
2867
+
2868
+ try {
2869
+ const previousSize = this.lastResizeByAgent.get(agentId);
2870
+ if (previousSize && previousSize.cols === nextCols && previousSize.rows === nextRows) {
2871
+ return;
2872
+ }
2873
+
2874
+ const engine = this.engineBridge.getEngine(agent.engineName);
2875
+ if (!engine || !engine.resizeSession) return;
2876
+
2877
+ const result = await engine.resizeSession(agentId, nextCols, nextRows);
2878
+ if (result && result.resized === false) {
2879
+ this.markAgentSessionDead(agentId, 'Session not available');
2880
+ return;
2881
+ }
2882
+ this.lastResizeByAgent.set(agentId, { cols: nextCols, rows: nextRows });
2883
+ } catch (error) {
2884
+ console.error('Failed to resize agent session:', error);
2885
+ }
2886
+ }
2887
+
2888
+ async clearAgentSessionBuffer(agentId) {
2889
+ const agent = this.agents.get(agentId);
2890
+ if (!agent) return { cleared: false };
2891
+ if (isCodexAppServerAgent(agent) || isAcpAgent(agent) || isJsonCliAgent(agent)) return { cleared: false };
2892
+
2893
+ try {
2894
+ const engine = this.engineBridge.getEngine(agent.engineName);
2895
+ if (!engine || !engine.clearBuffer) return { cleared: false };
2896
+ const result = await engine.clearBuffer(agentId);
2897
+ if (result && result.cleared === false) {
2898
+ this.markAgentSessionDead(agentId, 'Session not available');
2899
+ return result;
2900
+ }
2901
+ agent.output = '';
2902
+ agent.previewText = '';
2903
+ agent.previewSnapshot = null;
2904
+ this.outputEvents.delete(agentId);
2905
+ this.lastActivity.set(agentId, Date.now());
2906
+ this.observeAgentStateChange(agentId, { force: true });
2907
+ this.emit('update');
2908
+ return result || { cleared: true };
2909
+ } catch (error) {
2910
+ console.error('Failed to clear agent session buffer:', error);
2911
+ if (isSessionNotAvailableError(error)) {
2912
+ this.markAgentSessionDead(agentId, error);
2913
+ }
2914
+ return { cleared: false, error: error && error.message ? error.message : String(error) };
2915
+ }
2916
+ }
2917
+
2918
+ renameAgent(agentId, title) {
2919
+ const agent = this.agents.get(agentId);
2920
+ if (!agent) {
2921
+ return { error: 'Agent not found' };
2922
+ }
2923
+
2924
+ const customTitle = String(title || '').trim().slice(0, 80);
2925
+ agent.customTitle = customTitle;
2926
+ this.emit('update');
2927
+ return { agentId, customTitle };
2928
+ }
2929
+
2930
+ setAgentTask(agentId, task) {
2931
+ const agent = this.agents.get(agentId);
2932
+ if (!agent) {
2933
+ return { error: 'Agent not found' };
2934
+ }
2935
+
2936
+ const nextTask = String(task || '').trim().slice(0, 240);
2937
+ agent.task = nextTask;
2938
+ this.emit('update');
2939
+ return { agentId, task: nextTask };
2940
+ }
2941
+
2942
+ updateAgentFlags(agentId, flags) {
2943
+ const agent = this.agents.get(agentId);
2944
+ if (!agent) {
2945
+ return { error: 'Agent not found' };
2946
+ }
2947
+
2948
+ const updates = {};
2949
+ if (typeof flags.pinned === 'boolean') {
2950
+ const wasPinned = agent.pinned === true;
2951
+ agent.pinned = flags.pinned;
2952
+ updates.pinned = agent.pinned;
2953
+ if (!wasPinned && agent.pinned) {
2954
+ agent.pinnedOrder = nextPinnedOrder(Array.from(this.agents.values()));
2955
+ }
2956
+ updates.pinnedOrder = finiteOrder(agent.pinnedOrder);
2957
+ }
2958
+
2959
+ if (typeof flags.unread === 'boolean') {
2960
+ const result = flags.unread
2961
+ ? this.markAgentUnreadCursor(agentId)
2962
+ : this.markAgentReadCursor(agentId);
2963
+ if (result.error) return result;
2964
+ updates.unread = result.unread;
2965
+ updates.attentionSeq = result.attentionSeq;
2966
+ updates.readAttentionSeq = result.readAttentionSeq;
2967
+ }
2968
+
2969
+ if (typeof flags.readAttentionSeq === 'number' && Number.isFinite(flags.readAttentionSeq)) {
2970
+ const result = this.markAgentReadCursor(agentId, flags.readAttentionSeq);
2971
+ if (result.error) return result;
2972
+ updates.unread = result.unread;
2973
+ updates.attentionSeq = result.attentionSeq;
2974
+ updates.readAttentionSeq = result.readAttentionSeq;
2975
+ }
2976
+
2977
+ if (flags.archived === true) {
2978
+ if (agent.id === this.mainAgentId) {
2979
+ return { error: 'Main Agent cannot be archived' };
2980
+ }
2981
+ return { error: 'Use archiveAgent to archive live agents' };
2982
+ }
2983
+
2984
+ if (flags.archived === false) {
2985
+ agent.archived = false;
2986
+ agent.archivedAt = null;
2987
+ updates.archived = agent.archived;
2988
+ updates.archivedAt = agent.archivedAt;
2989
+ }
2990
+
2991
+ if (typeof flags.pinned === 'boolean') {
2992
+ this.ensurePersistentAgentSession(agent);
2993
+ this.updateEngineProviderSessionMetadata(agent);
2994
+ }
2995
+ this.emit('update');
2996
+ return { agentId, ...updates };
2997
+ }
2998
+
2999
+ reorderProjectAgent(agentId, { beforeAgentId = '', afterAgentId = '' } = {}) {
3000
+ const result = reorderedProjectAgentOrders(
3001
+ Array.from(this.agents.values()),
3002
+ agentId,
3003
+ String(beforeAgentId || ''),
3004
+ String(afterAgentId || ''),
3005
+ );
3006
+ if (result.error) return result;
3007
+
3008
+ const updates = [];
3009
+ for (const [updatedAgentId, projectOrder] of result.updates) {
3010
+ const agent = this.agents.get(updatedAgentId);
3011
+ if (!agent) continue;
3012
+ agent.projectOrder = projectOrder;
3013
+ this.ensurePersistentAgentSession(agent);
3014
+ this.updateEngineProviderSessionMetadata(agent);
3015
+ updates.push({ agentId: updatedAgentId, projectOrder });
3016
+ }
3017
+ this.emit('update');
3018
+ return { agentId, projectOrder: finiteOrder(this.agents.get(agentId)?.projectOrder), updates };
3019
+ }
3020
+
3021
+ reorderPinnedAgent(agentId, { beforeAgentId = '', afterAgentId = '' } = {}) {
3022
+ const result = reorderedPinnedAgentOrders(
3023
+ Array.from(this.agents.values()),
3024
+ agentId,
3025
+ String(beforeAgentId || ''),
3026
+ String(afterAgentId || ''),
3027
+ );
3028
+ if (result.error) return result;
3029
+
3030
+ const updates = [];
3031
+ for (const [updatedAgentId, pinnedOrder] of result.updates) {
3032
+ const agent = this.agents.get(updatedAgentId);
3033
+ if (!agent) continue;
3034
+ agent.pinnedOrder = pinnedOrder;
3035
+ this.ensurePersistentAgentSession(agent);
3036
+ this.updateEngineProviderSessionMetadata(agent);
3037
+ updates.push({ agentId: updatedAgentId, pinnedOrder });
3038
+ }
3039
+ this.emit('update');
3040
+ return { agentId, pinnedOrder: finiteOrder(this.agents.get(agentId)?.pinnedOrder), updates };
3041
+ }
3042
+
3043
+ reorderAgent(agentId, neighbors = {}) {
3044
+ const agent = this.agents.get(agentId);
3045
+ if (!agent) return { error: 'Agent not found' };
3046
+ return agent.pinned === true
3047
+ ? this.reorderPinnedAgent(agentId, neighbors)
3048
+ : this.reorderProjectAgent(agentId, neighbors);
3049
+ }
3050
+
3051
+ async syncCodexTerminalPermissionMode(agentId, mode) {
3052
+ const agent = this.agents.get(agentId);
3053
+ if (!agent) return { error: 'Agent not found' };
3054
+
3055
+ if (
3056
+ agent.providerSessionProvider === 'codex'
3057
+ && agent.codexRuntimeMode === 'app-server'
3058
+ ) {
3059
+ return this.updateCodexAppServerPermissionMode(agentId, mode);
3060
+ }
3061
+
3062
+ return this.restartAgentWithPermissionMode(agentId, mode);
3063
+ }
3064
+
3065
+ async updateCodexAppServerPermissionMode(agentId, mode) {
3066
+ const agent = this.agents.get(agentId);
3067
+ if (!agent) return { error: 'Agent not found' };
3068
+ if (agent.providerSessionProvider !== 'codex' || agent.codexRuntimeMode !== 'app-server') {
3069
+ return { error: 'Agent is not using the Codex App Server runtime' };
3070
+ }
3071
+
3072
+ const nextMode = ['ask', 'approve', 'full', 'custom'].includes(mode) ? mode : '';
3073
+ if (!nextMode) return { error: 'Unsupported Codex permission mode' };
3074
+ if (!this.codexAppServerRuntime || typeof this.codexAppServerRuntime.updateAgentPermissionMode !== 'function') {
3075
+ return { error: 'Codex App Server permission updates are unavailable' };
3076
+ }
3077
+
3078
+ try {
3079
+ await this.codexAppServerRuntime.updateAgentPermissionMode({
3080
+ ...this.codexAppServerOptionsForAgent(agent),
3081
+ approvalMode: nextMode,
3082
+ });
3083
+ } catch (error) {
3084
+ return { error: error && error.message ? error.message : 'Failed to update Codex App Server permissions' };
3085
+ }
3086
+
3087
+ agent.launchPermissionMode = nextMode;
3088
+ agent.codexAppServerError = '';
3089
+ this.updateEngineProviderSessionMetadata(agent);
3090
+ this.ensurePersistentAgentSession(agent, { launchPermissionMode: nextMode });
3091
+ this.emit('update');
3092
+ return {
3093
+ agentId,
3094
+ updated: true,
3095
+ launchPermissionMode: nextMode,
3096
+ };
3097
+ }
3098
+
3099
+ async restartAgentWithPermissionMode(agentId, mode) {
3100
+ const inFlight = this.permissionRestartInFlight.get(agentId);
3101
+ if (inFlight) {
3102
+ return inFlight.mode === mode
3103
+ ? inFlight.promise
3104
+ : { error: 'Permission change already in progress' };
3105
+ }
3106
+
3107
+ const restart = this.performAgentPermissionRestart(agentId, mode);
3108
+ const entry = { mode, promise: restart };
3109
+ this.permissionRestartInFlight.set(agentId, entry);
3110
+ try {
3111
+ return await restart;
3112
+ } finally {
3113
+ if (this.permissionRestartInFlight.get(agentId) === entry) {
3114
+ this.permissionRestartInFlight.delete(agentId);
3115
+ }
3116
+ }
3117
+ }
3118
+
3119
+ async restartAgentRuntimeMode(agentId, mode) {
3120
+ const inFlight = this.runtimeRestartInFlight.get(agentId);
3121
+ if (inFlight) {
3122
+ return inFlight.mode === mode
3123
+ ? inFlight.promise
3124
+ : { error: 'Agent runtime switch already in progress' };
3125
+ }
3126
+ const restart = this.performAgentRuntimeModeRestart(agentId, mode);
3127
+ const entry = { mode, promise: restart };
3128
+ this.runtimeRestartInFlight.set(agentId, entry);
3129
+ try {
3130
+ return await restart;
3131
+ } finally {
3132
+ if (this.runtimeRestartInFlight.get(agentId) === entry) {
3133
+ this.runtimeRestartInFlight.delete(agentId);
3134
+ }
3135
+ }
3136
+ }
3137
+
3138
+ async performAgentRuntimeModeRestart(agentId, mode) {
3139
+ const agent = this.agents.get(agentId);
3140
+ if (!agent) return { error: 'Agent not found' };
3141
+ const nextMode = ['terminal', 'json', 'acp'].includes(mode) ? mode : '';
3142
+ if (!nextMode) return { error: 'Unsupported Agent runtime mode' };
3143
+ const provider = agent.providerSessionProvider || '';
3144
+ const leavesCodexAppServer = provider === 'codex'
3145
+ && agent.codexRuntimeMode === 'app-server'
3146
+ && nextMode === 'terminal';
3147
+ if (agent.agentRuntimeMode === nextMode && !leavesCodexAppServer) {
3148
+ return { agentId, agentRuntimeMode: nextMode };
3149
+ }
3150
+ const turnActive = agent.agentRuntimeMode === 'acp'
3151
+ ? ['working', 'waiting-for-permission', 'interrupting'].includes(agent.acpState || '')
3152
+ : this.isAgentAttentionTurnActive(agent);
3153
+ if (turnActive) {
3154
+ return { error: 'Interrupt the active Agent turn before switching Chat and Terminal.' };
3155
+ }
3156
+ const supportedProviders = nextMode === 'json' ? ['codex', 'opencode'] : ['codex', 'claude', 'opencode', 'qoder'];
3157
+ if (!supportedProviders.includes(provider)) {
3158
+ return { error: `Agent does not support the ${nextMode.toUpperCase()} runtime` };
3159
+ }
3160
+ const sessionId = String(agent.providerSessionId || '').trim();
3161
+ if (!isSafeProviderSessionId(sessionId)) {
3162
+ return { error: 'Runtime switching requires a resumable provider session. Send the first message and try again.' };
3163
+ }
3164
+ // A live ACP binding is the authoritative owner of a newly-created
3165
+ // provider session. Provider history indexes can lag behind that binding
3166
+ // (and some adapters persist only while closing), so requiring an
3167
+ // immediate filesystem/history hit makes Chat -> Terminal spuriously
3168
+ // fail. Historical/terminal sessions still use the provider lookup as a
3169
+ // stale-session guard.
3170
+ let liveAcpSession = false;
3171
+ if (agent.agentRuntimeMode === 'acp' && this.acpRuntime && typeof this.acpRuntime.getSession === 'function') {
3172
+ try {
3173
+ const snapshot = this.acpRuntime.getSession(agentId, { maxEntries: 0 });
3174
+ liveAcpSession = String(snapshot?.sessionId || '') === sessionId;
3175
+ } catch {
3176
+ liveAcpSession = false;
3177
+ }
3178
+ }
3179
+ const previouslyVerifiedSession = String(agent.runtimeSwitchVerifiedSessionId || '') === sessionId;
3180
+ if (!liveAcpSession && !previouslyVerifiedSession) {
3181
+ const providerSession = await this.findRuntimeSwitchSession(agent);
3182
+ if (!providerSession) {
3183
+ return { error: 'The saved Agent session is no longer available in the selected Agent Home.' };
3184
+ }
3185
+ }
3186
+ const command = buildAgentSessionResumeCommand(provider, sessionId, {
3187
+ cwd: agent.cwd || agent.projectWorkspace || '',
3188
+ });
3189
+ if (!command) return { error: 'Failed to build provider resume command' };
3190
+ const preserved = {
3191
+ pinned: agent.pinned === true,
3192
+ projectOrder: finiteOrder(agent.projectOrder),
3193
+ pinnedOrder: finiteOrder(agent.pinnedOrder),
3194
+ customTitle: agent.customTitle || '',
3195
+ unread: agent.unread === true,
3196
+ jsonCliEvents: isJsonCliAgent(agent)
3197
+ ? this.jsonCliRuntime.getEvents(agentId)
3198
+ : (Array.isArray(agent.jsonCliEvents) ? agent.jsonCliEvents : []),
3199
+ };
3200
+ const restartOptions = {
3201
+ wantsMain: agent.wantsMain === true,
3202
+ task: agent.task || agent.providerSessionTitle || '',
3203
+ workflowTemplate: agent.workflowTemplate || '',
3204
+ projectWorkspace: agent.projectWorkspace || agent.cwd || '',
3205
+ source: resumedAgentSource(provider, sessionId, agent.providerHomeId || ''),
3206
+ providerHomeId: agent.providerHomeId || '',
3207
+ providerHomePath: agent.providerHomePath || '',
3208
+ providerSessionTitle: agent.providerSessionTitle || '',
3209
+ restartedFromAgentId: agentId,
3210
+ restartedFromAgentIds: Array.from(new Set([
3211
+ ...(Array.isArray(agent.restartedFromAgentIds) ? agent.restartedFromAgentIds : []),
3212
+ ...(agent.restartedFromAgentId ? [agent.restartedFromAgentId] : []),
3213
+ agentId,
3214
+ ])),
3215
+ projectOrder: preserved.projectOrder,
3216
+ pinnedOrder: preserved.pinnedOrder,
3217
+ agentRuntimeMode: nextMode,
3218
+ codexRuntimeMode: 'cli',
3219
+ codexApprovalMode: agent.launchPermissionMode || undefined,
3220
+ jsonCliEvents: preserved.jsonCliEvents,
3221
+ runtimeSwitchVerifiedSessionId: sessionId,
3222
+ };
3223
+ const originalMode = agent.agentRuntimeMode || 'terminal';
3224
+ const originalOptions = {
3225
+ ...restartOptions,
3226
+ agentRuntimeMode: originalMode,
3227
+ codexRuntimeMode: agent.codexRuntimeMode || 'cli',
3228
+ };
3229
+ const startReplacement = options => new Promise(resolve => {
3230
+ let settled = false;
3231
+ const finish = (restartedAgentId, error) => {
3232
+ if (settled) return;
3233
+ settled = true;
3234
+ resolve({ restartedAgentId: restartedAgentId || '', error: error || '' });
3235
+ };
3236
+ try {
3237
+ const started = this.startAgent(
3238
+ command,
3239
+ agent.cwd || agent.projectWorkspace || null,
3240
+ (restartedAgentId, error) => finish(restartedAgentId, error),
3241
+ options
3242
+ );
3243
+ Promise.resolve(started).catch(error => finish('', error?.message || 'Failed to start Agent'));
3244
+ } catch (error) {
3245
+ finish('', error?.message || 'Failed to start Agent');
3246
+ }
3247
+ });
3248
+ const restorePreservedState = restartedAgentId => {
3249
+ const replacement = this.agents.get(restartedAgentId);
3250
+ if (!replacement) return;
3251
+ Object.assign(replacement, preserved);
3252
+ this.ensurePersistentAgentSession(replacement);
3253
+ };
3254
+ await this.killAgent(agentId, { reason: 'runtime-switch', recordHistory: false, emitUpdate: false });
3255
+ const switched = await startReplacement(restartOptions);
3256
+ if (switched.restartedAgentId && !switched.error) {
3257
+ restorePreservedState(switched.restartedAgentId);
3258
+ this.emit('update');
3259
+ return {
3260
+ agentId,
3261
+ restarted: true,
3262
+ restartedAgentId: switched.restartedAgentId,
3263
+ agentRuntimeMode: nextMode,
3264
+ };
3265
+ }
3266
+ if (switched.restartedAgentId && this.agents.has(switched.restartedAgentId)) {
3267
+ await this.killAgent(switched.restartedAgentId, {
3268
+ reason: 'runtime-switch-start-failed',
3269
+ recordHistory: false,
3270
+ emitUpdate: false,
3271
+ });
3272
+ }
3273
+
3274
+ const restored = await startReplacement(originalOptions);
3275
+ if (restored.restartedAgentId && !restored.error) {
3276
+ restorePreservedState(restored.restartedAgentId);
3277
+ this.emit('update');
3278
+ return {
3279
+ agentId,
3280
+ restarted: true,
3281
+ restartedAgentId: restored.restartedAgentId,
3282
+ agentRuntimeMode: originalMode,
3283
+ switchFailed: true,
3284
+ warning: `${switched.error || 'Failed to switch Agent runtime'} Original runtime restored.`,
3285
+ };
3286
+ }
3287
+ if (restored.restartedAgentId && this.agents.has(restored.restartedAgentId)) {
3288
+ await this.killAgent(restored.restartedAgentId, {
3289
+ reason: 'runtime-switch-restore-failed',
3290
+ recordHistory: false,
3291
+ emitUpdate: false,
3292
+ });
3293
+ }
3294
+ this.emit('update');
3295
+ return {
3296
+ error: `${switched.error || 'Failed to switch Agent runtime'} Restore also failed: ${restored.error || 'unknown error'}`,
3297
+ };
3298
+ }
3299
+
3300
+ findRuntimeSwitchSession(agent) {
3301
+ const provider = agent.providerSessionProvider;
3302
+ const providerHomeId = agent.providerHomeId || 'default';
3303
+ const providerHomePath = agent.providerHomePath || '';
3304
+ return findAgentSession(agent.providerSessionProvider, agent.providerSessionId, {
3305
+ limit: 1000,
3306
+ providerLimit: 1000,
3307
+ scanLimit: 5000,
3308
+ providerHomeId,
3309
+ providerHomes: providerHomePath
3310
+ ? { [provider]: [{ id: providerHomeId, path: providerHomePath }] }
3311
+ : undefined,
3312
+ });
3313
+ }
3314
+
3315
+ async performAgentPermissionRestart(agentId, mode) {
3316
+ const agent = this.agents.get(agentId);
3317
+ if (!agent) {
3318
+ return { error: 'Agent not found' };
3319
+ }
3320
+
3321
+ const sourceSession = resumedSessionFromSource(agent.source);
3322
+ const provider = agent.providerSessionProvider || (sourceSession && sourceSession.provider) || '';
3323
+ const providerHomeId = agent.providerHomeId || (sourceSession && sourceSession.providerHomeId) || '';
3324
+ const sessionId = agent.providerSessionTemporary === true
3325
+ ? (sourceSession && sourceSession.sessionId)
3326
+ : (agent.providerSessionId || (sourceSession && sourceSession.sessionId) || '');
3327
+
3328
+ if (!['codex', 'claude'].includes(provider)) {
3329
+ return { error: 'Agent does not support permission restart' };
3330
+ }
3331
+
3332
+ const nextMode = provider === 'codex'
3333
+ ? (['ask', 'approve', 'full', 'custom'].includes(mode) ? mode : '')
3334
+ : (['acceptEdits', 'auto', 'bypassPermissions', 'default', 'dontAsk', 'plan'].includes(mode) ? mode : '');
3335
+ if (!nextMode) {
3336
+ return { error: `Unsupported ${provider === 'codex' ? 'Codex' : 'Claude'} permission mode` };
3337
+ }
3338
+
3339
+ const hasResumableSession = isSafeProviderSessionId(sessionId);
3340
+ const startsFreshCodexSession = provider === 'codex'
3341
+ && !hasResumableSession
3342
+ && (agent.providerSessionTemporary === true || !String(agent.providerSessionId || '').trim());
3343
+ if (!hasResumableSession && !startsFreshCodexSession) {
3344
+ return { error: 'Permission changes require a resumable provider session. Try again after the session id is available.' };
3345
+ }
3346
+
3347
+ const command = startsFreshCodexSession
3348
+ ? 'codex'
3349
+ : buildAgentSessionResumeCommand(provider, sessionId, {
3350
+ cwd: agent.cwd || agent.projectWorkspace || '',
3351
+ });
3352
+ if (!command) {
3353
+ return { error: 'Failed to build provider resume command' };
3354
+ }
3355
+
3356
+ const restartOptions = {
3357
+ wantsMain: agent.wantsMain === true,
3358
+ task: agent.task || agent.providerSessionTitle || '',
3359
+ workflowTemplate: agent.workflowTemplate || '',
3360
+ requiredCliVersion: provider === 'codex' ? (agent.requiredCliVersion || '') : '',
3361
+ projectWorkspace: agent.projectWorkspace || agent.cwd || '',
3362
+ source: startsFreshCodexSession
3363
+ ? 'ui'
3364
+ : (resumedSessionFromSource(agent.source)
3365
+ ? agent.source
3366
+ : resumedAgentSource(provider, sessionId, providerHomeId)),
3367
+ providerHomeId,
3368
+ providerHomePath: agent.providerHomePath || '',
3369
+ providerSessionTitle: agent.providerSessionTitle || '',
3370
+ restartedFromAgentId: agentId,
3371
+ restartedFromAgentIds: Array.from(new Set([
3372
+ ...(Array.isArray(agent.restartedFromAgentIds) ? agent.restartedFromAgentIds : []),
3373
+ ...(agent.restartedFromAgentId ? [agent.restartedFromAgentId] : []),
3374
+ agentId,
3375
+ ])),
3376
+ projectOrder: finiteOrder(agent.projectOrder),
3377
+ pinnedOrder: finiteOrder(agent.pinnedOrder),
3378
+ ...(provider === 'codex' ? { codexApprovalMode: nextMode } : { claudePermissionMode: nextMode }),
3379
+ };
3380
+ const preserved = {
3381
+ pinned: agent.pinned === true,
3382
+ projectOrder: finiteOrder(agent.projectOrder),
3383
+ pinnedOrder: finiteOrder(agent.pinnedOrder),
3384
+ customTitle: agent.customTitle || '',
3385
+ unread: agent.unread === true,
3386
+ attentionSeq: finiteNonNegativeInteger(agent.attentionSeq),
3387
+ readAttentionSeq: finiteNonNegativeInteger(agent.readAttentionSeq),
3388
+ };
3389
+
3390
+ this.permissionRestartSuppressedAgentIds.add(agentId);
3391
+ try {
3392
+ await this.killAgent(agentId, {
3393
+ reason: 'permission-restart',
3394
+ recordHistory: false,
3395
+ emitUpdate: false,
3396
+ });
3397
+ } finally {
3398
+ this.permissionRestartSuppressedAgentIds.delete(agentId);
3399
+ }
3400
+
3401
+ return new Promise((resolve) => {
3402
+ const startResult = this.startAgent(command, agent.cwd || agent.projectWorkspace || null, (restartedAgentId, error) => {
3403
+ if (error) {
3404
+ this.emit('update');
3405
+ resolve({ error });
3406
+ return;
3407
+ }
3408
+ if (!restartedAgentId) {
3409
+ this.emit('update');
3410
+ resolve({ error: 'Failed to restart agent with updated permissions' });
3411
+ return;
3412
+ }
3413
+
3414
+ const restartedAgent = this.agents.get(restartedAgentId);
3415
+ if (restartedAgent) {
3416
+ restartedAgent.pinned = preserved.pinned;
3417
+ restartedAgent.projectOrder = preserved.projectOrder;
3418
+ restartedAgent.pinnedOrder = preserved.pinnedOrder;
3419
+ restartedAgent.customTitle = preserved.customTitle;
3420
+ restartedAgent.unread = preserved.unread;
3421
+ restartedAgent.attentionSeq = preserved.attentionSeq;
3422
+ restartedAgent.readAttentionSeq = preserved.readAttentionSeq;
3423
+ restartedAgent.launchPermissionMode = nextMode;
3424
+ this.updateEngineProviderSessionMetadata(restartedAgent);
3425
+ this.ensurePersistentAgentSession(restartedAgent, {
3426
+ pinned: restartedAgent.pinned,
3427
+ projectOrder: restartedAgent.projectOrder,
3428
+ pinnedOrder: restartedAgent.pinnedOrder,
3429
+ customTitle: restartedAgent.customTitle,
3430
+ unread: restartedAgent.unread,
3431
+ attentionSeq: restartedAgent.attentionSeq,
3432
+ readAttentionSeq: restartedAgent.readAttentionSeq,
3433
+ launchPermissionMode: nextMode,
3434
+ });
3435
+ }
3436
+ this.emit('update');
3437
+ resolve({
3438
+ agentId,
3439
+ restarted: true,
3440
+ restartedAgentId,
3441
+ launchPermissionMode: nextMode,
3442
+ });
3443
+ }, restartOptions);
3444
+ Promise.resolve(startResult).catch((error) => {
3445
+ this.emit('update');
3446
+ resolve({ error: error.message || 'Failed to restart agent with updated permissions' });
3447
+ });
3448
+ });
3449
+ }
3450
+
3451
+ setAgentUnread(agentId, unread) {
3452
+ return unread === true
3453
+ ? this.markAgentUnreadCursor(agentId)
3454
+ : this.markAgentReadCursor(agentId);
3455
+ }
3456
+
3457
+ async createForkWorktree(workspace) {
3458
+ const sourceWorkspace = this.expandWorkspacePath(workspace);
3459
+ if (!sourceWorkspace) {
3460
+ throw new Error('Source workspace is empty');
3461
+ }
3462
+
3463
+ let root;
3464
+ try {
3465
+ const { stdout } = await execFileAsync('git', ['-C', sourceWorkspace, 'rev-parse', '--show-toplevel'], {
3466
+ timeout: 15000,
3467
+ maxBuffer: 1024 * 1024,
3468
+ });
3469
+ root = stdout.trim();
3470
+ } catch (error) {
3471
+ const message = error && error.stderr ? String(error.stderr).trim() : '';
3472
+ throw new Error(message || 'Source workspace is not inside a git repository', { cause: error });
3473
+ }
3474
+
3475
+ const parentDir = path.dirname(root);
3476
+ const baseName = path.basename(root);
3477
+ const slug = timestampSlug();
3478
+ let target = path.join(parentDir, `${baseName}-farming-fork-${slug}`);
3479
+ let suffix = 2;
3480
+ while (fs.existsSync(target)) {
3481
+ target = path.join(parentDir, `${baseName}-farming-fork-${slug}-${suffix}`);
3482
+ suffix += 1;
3483
+ }
3484
+
3485
+ try {
3486
+ await execFileAsync('git', ['-C', root, 'worktree', 'add', target, 'HEAD'], {
3487
+ timeout: 60000,
3488
+ maxBuffer: 1024 * 1024 * 4,
3489
+ });
3490
+ } catch (error) {
3491
+ const message = error && error.stderr ? String(error.stderr).trim() : '';
3492
+ throw new Error(message || 'Failed to create git worktree', { cause: error });
3493
+ }
3494
+
3495
+ return target;
3496
+ }
3497
+
3498
+ async inspectForkWorktreeProject(workspace) {
3499
+ const expanded = this.expandWorkspacePath(workspace);
3500
+ const resolvedWorkspace = expanded ? path.resolve(expanded) : '';
3501
+ if (!resolvedWorkspace) {
3502
+ return { error: 'Workspace is required' };
3503
+ }
3504
+ if (!isFarmingForkWorktreePath(resolvedWorkspace)) {
3505
+ return { error: 'Only Farming fork worktrees can be deleted' };
3506
+ }
3507
+ try {
3508
+ if (!fs.statSync(resolvedWorkspace).isDirectory()) {
3509
+ return { error: 'Workspace is not a directory' };
3510
+ }
3511
+ } catch {
3512
+ return { error: 'Workspace not found' };
3513
+ }
3514
+
3515
+ let topLevel = '';
3516
+ try {
3517
+ const { stdout } = await execFileAsync('git', ['-C', resolvedWorkspace, 'rev-parse', '--show-toplevel'], {
3518
+ timeout: 15000,
3519
+ maxBuffer: 1024 * 1024,
3520
+ });
3521
+ topLevel = path.resolve(stdout.trim());
3522
+ } catch (error) {
3523
+ const message = error && error.stderr ? String(error.stderr).trim() : '';
3524
+ return { error: message || 'Workspace is not a git worktree' };
3525
+ }
3526
+
3527
+ if (topLevel !== resolvedWorkspace) {
3528
+ return { error: 'Workspace must be the root of a Farming fork worktree' };
3529
+ }
3530
+
3531
+ try {
3532
+ const { stdout } = await execFileAsync('git', ['-C', resolvedWorkspace, 'status', '--porcelain', '--untracked-files=all'], {
3533
+ timeout: 30000,
3534
+ maxBuffer: 1024 * 1024 * 4,
3535
+ });
3536
+ const dirtyEntries = statusEntriesFromPorcelain(stdout);
3537
+ return {
3538
+ workspace: resolvedWorkspace,
3539
+ dirtyEntries,
3540
+ requiresForce: dirtyEntries.length > 0,
3541
+ };
3542
+ } catch (error) {
3543
+ const message = error && error.stderr ? String(error.stderr).trim() : '';
3544
+ return { error: message || 'Failed to inspect worktree status' };
3545
+ }
3546
+ }
3547
+
3548
+ agentsForProjectWorkspace(workspace) {
3549
+ const resolvedWorkspace = path.resolve(workspace);
3550
+ return Array.from(this.agents.values()).filter(agent => {
3551
+ if (!agent || agent.isMain) return false;
3552
+ const agentWorkspace = this.expandWorkspacePath(agent.projectWorkspace || agent.cwd || '');
3553
+ if (!agentWorkspace) return false;
3554
+ return path.resolve(agentWorkspace) === resolvedWorkspace;
3555
+ });
3556
+ }
3557
+
3558
+ removeMainPageProviderSessionsForAgents(agents) {
3559
+ if (!this.configManager) {
3560
+ return [];
3561
+ }
3562
+
3563
+ const keysToRemove = new Set();
3564
+ agents.forEach(agent => {
3565
+ const providerSessionKey = agent.providerSessionKey || this.providerSessionKey(
3566
+ agent.providerSessionProvider,
3567
+ agent.providerSessionId,
3568
+ agent.providerHomeId || ''
3569
+ );
3570
+ if (providerSessionKey) keysToRemove.add(providerSessionKey);
3571
+ });
3572
+ if (keysToRemove.size === 0) return [];
3573
+
3574
+ const currentKeys = this.getMainPageSessionKeys();
3575
+ const removedKeys = currentKeys.filter(key => keysToRemove.has(key));
3576
+ if (removedKeys.length === 0) return [];
3577
+ if (typeof this.configManager.removeMainPageSessionKeys === 'function') {
3578
+ this.configManager.removeMainPageSessionKeys(removedKeys);
3579
+ } else {
3580
+ const nextKeys = currentKeys.filter(key => !keysToRemove.has(key));
3581
+ this.setMainPageSessionKeys(nextKeys);
3582
+ }
3583
+ return removedKeys;
3584
+ }
3585
+
3586
+ async deleteForkWorktreeProject(workspace, options = {}) {
3587
+ const inspected = await this.inspectForkWorktreeProject(workspace);
3588
+ if (inspected.error) return inspected;
3589
+ if (inspected.requiresForce && options.force !== true) {
3590
+ return {
3591
+ ...inspected,
3592
+ error: 'Worktree has uncommitted or untracked files',
3593
+ };
3594
+ }
3595
+
3596
+ const projectAgents = this.agentsForProjectWorkspace(inspected.workspace);
3597
+ const removedMainPageSessionKeys = this.removeMainPageProviderSessionsForAgents(projectAgents);
3598
+ const archivedAgentIds = [];
3599
+ for (const agent of projectAgents) {
3600
+ const result = await this.archiveAgent(agent.id);
3601
+ if (result && !result.error) archivedAgentIds.push(agent.id);
3602
+ }
3603
+
3604
+ const args = ['-C', inspected.workspace, 'worktree', 'remove'];
3605
+ if (options.force === true) args.push('--force');
3606
+ args.push(inspected.workspace);
3607
+
3608
+ try {
3609
+ await execFileAsync('git', args, {
3610
+ timeout: 60000,
3611
+ maxBuffer: 1024 * 1024 * 4,
3612
+ });
3613
+ } catch (error) {
3614
+ const message = error && error.stderr ? String(error.stderr).trim() : '';
3615
+ return {
3616
+ ...inspected,
3617
+ archivedAgentIds,
3618
+ removedMainPageSessionKeys,
3619
+ error: message || 'Failed to delete git worktree',
3620
+ };
3621
+ }
3622
+
3623
+ return {
3624
+ workspace: inspected.workspace,
3625
+ deleted: true,
3626
+ forced: options.force === true,
3627
+ archivedAgentIds,
3628
+ removedMainPageSessionKeys,
3629
+ };
3630
+ }
3631
+
3632
+ async forkAgent(agentId, mode = 'same-worktree') {
3633
+ const agent = this.agents.get(agentId);
3634
+ if (!agent) {
3635
+ return { error: 'Agent not found' };
3636
+ }
3637
+
3638
+ const sourceWorkspace = agent.projectWorkspace || agent.cwd;
3639
+ let targetWorkspace = sourceWorkspace;
3640
+ if (mode === 'new-worktree') {
3641
+ try {
3642
+ targetWorkspace = await this.createForkWorktree(sourceWorkspace);
3643
+ } catch (error) {
3644
+ return { error: error.message || 'Failed to create git worktree' };
3645
+ }
3646
+ } else if (mode !== 'same-worktree') {
3647
+ return { error: 'Unsupported fork mode' };
3648
+ }
3649
+
3650
+ const resumedSession = agent.providerSessionProvider
3651
+ && agent.providerSessionId
3652
+ && agent.providerSessionTemporary !== true
3653
+ ? { provider: agent.providerSessionProvider, providerHomeId: agent.providerHomeId || 'default', sessionId: agent.providerSessionId }
3654
+ : resumedSessionFromSource(agent.source);
3655
+ const forkCommand = resumedSession
3656
+ ? buildAgentSessionResumeCommand(resumedSession.provider, resumedSession.sessionId, {
3657
+ fork: true,
3658
+ cwd: targetWorkspace,
3659
+ })
3660
+ : (agent.forkCommand || agent.command);
3661
+
3662
+ return new Promise((resolve) => {
3663
+ this.startAgent(forkCommand, targetWorkspace, (forkedAgentId, error) => {
3664
+ if (error) {
3665
+ resolve({ error });
3666
+ return;
3667
+ }
3668
+ if (!forkedAgentId) {
3669
+ resolve({ error: 'Failed to start forked agent' });
3670
+ return;
3671
+ }
3672
+ resolve({
3673
+ agentId: forkedAgentId,
3674
+ workspace: targetWorkspace,
3675
+ mode,
3676
+ });
3677
+ }, {
3678
+ wantsMain: false,
3679
+ parentAgentId: agent.id,
3680
+ task: agent.task ? `Fork: ${agent.task}` : `Fork of ${agent.command}`,
3681
+ workflowTemplate: agent.workflowTemplate || '',
3682
+ source: mode === 'new-worktree' ? 'ui-fork-new-worktree' : 'ui-fork-same-worktree',
3683
+ providerHomeId: agent.providerHomeId || (resumedSession && resumedSession.providerHomeId) || '',
3684
+ providerHomePath: agent.providerHomePath || '',
3685
+ });
3686
+ });
3687
+ }
3688
+
3689
+ recordTaskHistory(agent, options = {}) {
3690
+ if (!agent || agent.id === this.mainAgentId) return;
3691
+ if (!isSupportedHistoryAgent(agent.forkCommand || agent.command || '')) return;
3692
+ const providerHistorySource = agent.providerSessionProvider
3693
+ && agent.providerSessionId
3694
+ && agent.providerSessionTemporary !== true
3695
+ ? resumedAgentSource(agent.providerSessionProvider, agent.providerSessionId, agent.providerHomeId || '')
3696
+ : '';
3697
+ const entry = {
3698
+ id: `history-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
3699
+ agentId: agent.id,
3700
+ command: agent.command || '',
3701
+ cwd: agent.cwd || '',
3702
+ projectWorkspace: agent.projectWorkspace || agent.cwd || '',
3703
+ title: agent.customTitle || agent.sessionTitle || agent.task || '',
3704
+ customTitle: agent.customTitle || '',
3705
+ task: agent.task || '',
3706
+ workflowTemplate: agent.workflowTemplate || '',
3707
+ source: providerHistorySource || agent.source || 'ui',
3708
+ reason: options.reason || 'manual-kill',
3709
+ status: agent.status || 'stopped',
3710
+ startedAt: agent.startedAt || null,
3711
+ lastActivity: this.lastActivity.get(agent.id) || null,
3712
+ archivedAt: options.archivedAt || Date.now(),
3713
+ };
3714
+ this.taskHistory = [entry, ...this.taskHistory].slice(0, 200);
3715
+ if (this.configManager && this.configManager.appendTaskHistory) {
3716
+ this.configManager.appendTaskHistory(entry);
3717
+ }
3718
+ }
3719
+
3720
+ async archiveAgent(agentId) {
3721
+ const agent = this.agents.get(agentId);
3722
+ if (!agent) {
3723
+ return { error: 'Agent not found' };
3724
+ }
3725
+ if (agent.id === this.mainAgentId) {
3726
+ return { error: 'Main Agent cannot be archived' };
3727
+ }
3728
+
3729
+ const removedMainPageSessionKeys = this.removeMainPageProviderSessionsForAgents([agent]);
3730
+ await this.killAgent(agentId, {
3731
+ reason: 'manual-archive',
3732
+ recordHistory: !isEphemeralShellAgent(agent),
3733
+ });
3734
+ return { agentId, archived: true, removed: true, removedMainPageSessionKeys };
3735
+ }
3736
+
3737
+ async killAgent(agentId, options = {}) {
3738
+ const agent = this.agents.get(agentId);
3739
+ if (!agent) return;
3740
+
3741
+ try {
3742
+ const engine = this.engineBridge.getEngine(agent.engineName);
3743
+ if (engine) {
3744
+ await engine.killSession(agentId);
3745
+ }
3746
+ } catch (error) {
3747
+ console.error('Failed to kill agent:', error);
3748
+ }
3749
+
3750
+ if (options.recordHistory !== false && !isEphemeralShellAgent(agent)) {
3751
+ this.recordTaskHistory(agent, {
3752
+ reason: options.reason || 'manual-kill',
3753
+ archivedAt: Date.now(),
3754
+ });
3755
+ }
3756
+
3757
+ this.agents.delete(agentId);
3758
+ this.lastActivity.delete(agentId);
3759
+ this.lastActivityUpdate.delete(agentId);
3760
+ this.outputEvents.delete(agentId);
3761
+ this.lastResizeByAgent.delete(agentId);
3762
+ this.stopCodexProviderSessionResolver(agentId);
3763
+ this.stopProviderSessionTitleResolver(agentId);
3764
+ if (this.codexAppServerRuntime && typeof this.codexAppServerRuntime.unregisterAgent === 'function') {
3765
+ this.codexAppServerRuntime.unregisterAgent(agentId);
3766
+ }
3767
+ if (this.jsonCliRuntime) this.jsonCliRuntime.unregisterAgent(agentId);
3768
+ if (this.acpRuntime) this.acpRuntime.unregisterAgent(agentId);
3769
+
3770
+ if (this.mainAgentId === agentId) {
3771
+ this.mainAgentId = null;
3772
+ }
3773
+
3774
+ if (options.emitUpdate !== false) {
3775
+ this.emit('update');
3776
+ }
3777
+ }
3778
+
3779
+ async getAgentSessionText(agentId) {
3780
+ const agent = this.agents.get(agentId);
3781
+ if (!agent) {
3782
+ return null;
3783
+ }
3784
+
3785
+ const engine = this.engineBridge.getEngine(agent.engineName);
3786
+ if (!engine) {
3787
+ return agent.output;
3788
+ }
3789
+
3790
+ try {
3791
+ const sessionState = await engine.getSessionState(agentId);
3792
+ if (isLiveEngineSessionState(sessionState) && this.reviveAgentRuntime(agent, sessionState)) {
3793
+ this.emit('update');
3794
+ }
3795
+ if (sessionState && typeof sessionState.output === 'string') {
3796
+ return sessionState.output;
3797
+ }
3798
+ if (!sessionState && isRunningAgentRuntimeStatus(agent.status) && !this.shouldDeferMissingEngineSession(agent)) {
3799
+ this.markAgentSessionDead(agentId, 'Session not available');
3800
+ }
3801
+ } catch (error) {
3802
+ console.error('Failed to read session text:', error);
3803
+ if (isSessionNotAvailableError(error) && !this.shouldDeferMissingEngineSession(agent)) {
3804
+ this.markAgentSessionDead(agentId, error);
3805
+ }
3806
+ }
3807
+
3808
+ return agent.output;
3809
+ }
3810
+
3811
+ getAgentWorkspaceRoot(agentId) {
3812
+ const agent = this.agents.get(agentId);
3813
+ if (!agent) {
3814
+ return null;
3815
+ }
3816
+
3817
+ return agent.projectWorkspace || agent.cwd;
3818
+ }
3819
+
3820
+ getAgentProviderSession(agentId) {
3821
+ const agent = this.agents.get(agentId);
3822
+ if (!agent) return null;
3823
+ return {
3824
+ provider: agent.providerSessionProvider || '',
3825
+ sessionId: agent.providerSessionId || '',
3826
+ providerHomeId: agent.providerHomeId || '',
3827
+ providerHomePath: agent.providerHomePath || '',
3828
+ codexAppServerHomePath: agent.codexAppServerHomePath || '',
3829
+ codexRuntimeMode: agent.codexRuntimeMode || '',
3830
+ temporary: agent.providerSessionTemporary === true,
3831
+ title: agent.providerSessionTitle || '',
3832
+ };
3833
+ }
3834
+
3835
+ async getAgentSessionView(agentId) {
3836
+ const agent = this.agents.get(agentId);
3837
+ if (!agent) {
3838
+ return null;
3839
+ }
3840
+
3841
+ const engine = this.engineBridge.getEngine(agent.engineName);
3842
+ let sessionState = null;
3843
+
3844
+ if (engine && engine.getSessionState) {
3845
+ try {
3846
+ sessionState = await engine.getSessionState(agentId);
3847
+ if (isLiveEngineSessionState(sessionState) && this.reviveAgentRuntime(agent, sessionState)) {
3848
+ this.emit('update');
3849
+ }
3850
+ if (!sessionState && isRunningAgentRuntimeStatus(agent.status) && !this.shouldDeferMissingEngineSession(agent)) {
3851
+ this.markAgentSessionDead(agentId, 'Session not available');
3852
+ }
3853
+ } catch (error) {
3854
+ console.error('Failed to read session state:', error);
3855
+ if (isSessionNotAvailableError(error) && !this.shouldDeferMissingEngineSession(agent)) {
3856
+ this.markAgentSessionDead(agentId, error);
3857
+ }
3858
+ }
3859
+ }
3860
+
3861
+ const fallbackOutput = agent.output || '';
3862
+ const fallbackPreview = agent.previewText || fallbackOutput.slice(-2000);
3863
+ const lastActivity = this.lastActivity.get(agentId) || Date.now();
3864
+ const terminalBusy = sessionState && typeof sessionState.terminalBusy === 'boolean'
3865
+ ? sessionState.terminalBusy
3866
+ : (typeof agent.terminalBusy === 'boolean' ? agent.terminalBusy : null);
3867
+ const shellCommand = sessionState && typeof sessionState.shellCommand === 'string'
3868
+ ? sessionState.shellCommand
3869
+ : (agent.shellCommand || '');
3870
+ const shellLastCommand = sessionState && typeof sessionState.shellLastCommand === 'string'
3871
+ ? sessionState.shellLastCommand
3872
+ : (agent.shellLastCommand || '');
3873
+ const shellCommandStartedAt = sessionState && Object.prototype.hasOwnProperty.call(sessionState, 'shellCommandStartedAt')
3874
+ ? finiteNumberOrNull(sessionState.shellCommandStartedAt)
3875
+ : finiteNumberOrNull(agent.shellCommandStartedAt);
3876
+ const shellLastCommandStartedAt = sessionState && Object.prototype.hasOwnProperty.call(sessionState, 'shellLastCommandStartedAt')
3877
+ ? finiteNumberOrNull(sessionState.shellLastCommandStartedAt)
3878
+ : finiteNumberOrNull(agent.shellLastCommandStartedAt);
3879
+ const shellLastCommandFinishedAt = sessionState && Object.prototype.hasOwnProperty.call(sessionState, 'shellLastCommandFinishedAt')
3880
+ ? finiteNumberOrNull(sessionState.shellLastCommandFinishedAt)
3881
+ : finiteNumberOrNull(agent.shellLastCommandFinishedAt);
3882
+ const shellLastCommandDurationMs = sessionState && Object.prototype.hasOwnProperty.call(sessionState, 'shellLastCommandDurationMs')
3883
+ ? finiteNumberOrNull(sessionState.shellLastCommandDurationMs)
3884
+ : finiteNumberOrNull(agent.shellLastCommandDurationMs);
3885
+ const previewText = (sessionState && typeof sessionState.previewText === 'string') ? sessionState.previewText : fallbackPreview;
3886
+ const sessionTitle = (sessionState && typeof sessionState.title === 'string' && sessionState.title) || agent.sessionTitle || '';
3887
+ const terminalStatus = (sessionState && sessionState.terminalStatus) || deriveAgentTerminalStatus(agent, {
3888
+ terminalBusy,
3889
+ status: sessionState && sessionState.status ? sessionState.status : terminalRuntimeStatus(agent.status),
3890
+ title: sessionTitle,
3891
+ previewText,
3892
+ cwd: (sessionState && sessionState.terminalStatus && sessionState.terminalStatus.cwd) || agent.shellCwd || agent.cwd,
3893
+ shellCommand,
3894
+ shellLastCommand,
3895
+ shellCommandStartedAt,
3896
+ shellLastCommandStartedAt,
3897
+ shellLastCommandFinishedAt,
3898
+ shellLastCommandDurationMs,
3899
+ });
3900
+
3901
+ const now = Date.now();
3902
+ const isMain = this.isMainAgentRecord(agent.id, agent);
3903
+ return {
3904
+ agentId: agent.id,
3905
+ command: agent.command,
3906
+ engineName: agent.engineName || '',
3907
+ cwd: agent.cwd,
3908
+ projectWorkspace: agent.projectWorkspace || '',
3909
+ status: sessionState && sessionState.status === 'exited'
3910
+ ? agent.status
3911
+ : (isLiveEngineSessionState(sessionState) ? 'running' : agent.status),
3912
+ terminalBusy,
3913
+ terminalStatus,
3914
+ shellCommand,
3915
+ shellLastCommand,
3916
+ shellCommandStartedAt,
3917
+ shellLastCommandStartedAt,
3918
+ shellLastCommandFinishedAt,
3919
+ shellLastCommandDurationMs,
3920
+ parentAgentId: agent.parentAgentId || '',
3921
+ task: agent.task || '',
3922
+ workflowTemplate: agent.workflowTemplate || '',
3923
+ source: agent.source || '',
3924
+ providerSessionProvider: agent.providerSessionProvider || '',
3925
+ providerHomeId: agent.providerHomeId || '',
3926
+ providerHomePath: agent.providerHomePath || '',
3927
+ providerSessionId: agent.providerSessionId || '',
3928
+ providerSessionKey: agent.providerSessionKey || '',
3929
+ providerSessionTemporary: agent.providerSessionTemporary === true,
3930
+ providerSessionSource: agent.providerSessionSource || '',
3931
+ providerSessionResolvedAt: agent.providerSessionResolvedAt || null,
3932
+ providerSessionTitle: agent.providerSessionTitle || '',
3933
+ codexRuntimeMode: agent.codexRuntimeMode || '',
3934
+ agentRuntimeMode: agent.agentRuntimeMode || 'terminal',
3935
+ acpState: agent.acpState || '',
3936
+ acpError: agent.acpError || '',
3937
+ acpStopReason: agent.acpStopReason || '',
3938
+ acpPendingPermission: agent.acpPendingPermission || null,
3939
+ acpPendingPermissions: Array.isArray(agent.acpPendingPermissions) ? agent.acpPendingPermissions : [],
3940
+ acpPendingElicitation: agent.acpPendingElicitation || null,
3941
+ acpPendingElicitations: Array.isArray(agent.acpPendingElicitations) ? agent.acpPendingElicitations : [],
3942
+ acpActiveElicitations: Array.isArray(agent.acpActiveElicitations) ? agent.acpActiveElicitations : [],
3943
+ acpSessionUpdatedAt: agent.acpSessionUpdatedAt || '',
3944
+ acpSessionRevision: Number(agent.acpSessionRevision) || 0,
3945
+ codexAppServerState: agent.codexAppServerState || '',
3946
+ codexAppServerEndpoint: agent.codexAppServerEndpoint || '',
3947
+ codexAppServerThreadId: agent.codexAppServerThreadId || '',
3948
+ codexAppServerTurnId: agent.codexAppServerTurnId || '',
3949
+ codexAppServerError: agent.codexAppServerError || '',
3950
+ codexAppServerPendingRequestId: agent.codexAppServerPendingRequestId || '',
3951
+ codexAppServerPendingRequestMethod: agent.codexAppServerPendingRequestMethod || '',
3952
+ codexAppServerPendingRequest: agent.codexAppServerPendingRequest || null,
3953
+ forkedFromProviderSessionId: agent.forkedFromProviderSessionId || '',
3954
+ customTitle: agent.customTitle || '',
3955
+ pinned: agent.pinned === true,
3956
+ projectOrder: finiteOrder(agent.projectOrder),
3957
+ pinnedOrder: finiteOrder(agent.pinnedOrder),
3958
+ attentionSeq: finiteNonNegativeInteger(agent.attentionSeq),
3959
+ readAttentionSeq: finiteNonNegativeInteger(agent.readAttentionSeq),
3960
+ attentionUpdatedAt: finiteNumberOrNull(agent.attentionUpdatedAt),
3961
+ readAttentionAt: finiteNumberOrNull(agent.readAttentionAt),
3962
+ attentionReason: agent.attentionReason || '',
3963
+ attentionOutputSeq: finiteNumberOrNull(agent.attentionOutputSeq),
3964
+ unread: agentAttentionUnread(agent),
3965
+ archived: agent.archived === true,
3966
+ archivedAt: agent.archivedAt || null,
3967
+ sessionSource: this.getEngineSessionSource(agent.engineName),
3968
+ outputSeq: sessionState && Number.isFinite(sessionState.outputSeq) ? sessionState.outputSeq : null,
3969
+ isMain,
3970
+ activityLevel: isMain ? 'warm' : this.calculateActivityLevel(lastActivity, now),
3971
+ lastActivity,
3972
+ attentionScore: isMain ? 0 : this.calculateAttentionScore(agentId, now),
3973
+ isZombie: isMain ? false : this.isZombie(agentId, now),
3974
+ startedAt: (sessionState && sessionState.startedAt) || agent.startedAt || null,
3975
+ exitedAt: (sessionState && sessionState.exitedAt) || agent.exitedAt || null,
3976
+ sessionTitle,
3977
+ output: (sessionState && typeof sessionState.output === 'string') ? sessionState.output : fallbackOutput,
3978
+ renderOutput: (sessionState && typeof sessionState.renderOutput === 'string') ? sessionState.renderOutput : fallbackOutput,
3979
+ previewText,
3980
+ previewSnapshot: (sessionState && sessionState.previewSnapshot) || agent.previewSnapshot || null,
3981
+ previewCols: (sessionState && Number.isFinite(sessionState.previewCols) && sessionState.previewCols > 0)
3982
+ ? sessionState.previewCols
3983
+ : (agent.previewCols || 80),
3984
+ previewRows: (sessionState && Number.isFinite(sessionState.previewRows) && sessionState.previewRows > 0)
3985
+ ? sessionState.previewRows
3986
+ : (agent.previewRows || 30),
3987
+ usageRate: this.calculateAgentUsageRate(agent.id),
3988
+ };
3989
+ }
3990
+
3991
+ calculateAgentUsageRate(agentId, options = {}) {
3992
+ const now = options.now || Date.now();
3993
+ const windowMs = options.windowMs || AGENT_USAGE_RATE_WINDOW_MS;
3994
+ const cutoff = now - windowMs;
3995
+ const events = (this.outputEvents.get(agentId) || []).filter(event => (
3996
+ event.timestamp >= cutoff && event.timestamp <= now + 1000
3997
+ ));
3998
+ if (events.length > 0) {
3999
+ this.outputEvents.set(agentId, events);
4000
+ } else {
4001
+ this.outputEvents.delete(agentId);
4002
+ }
4003
+ const outputBytes = events.reduce((sum, event) => sum + Math.max(0, event.bytes || 0), 0);
4004
+ const estimatedOutputTokens = Math.ceil(outputBytes / 4);
4005
+ const windowMinutes = Math.max(1, windowMs / 60_000);
4006
+
4007
+ return {
4008
+ windowMs,
4009
+ outputBytes,
4010
+ estimatedOutputTokens,
4011
+ estimatedTokensPerMinute: Math.round((estimatedOutputTokens / windowMinutes) * 10) / 10,
4012
+ eventCount: events.length,
4013
+ sampledAt: now,
4014
+ source: 'terminal-output-estimate',
4015
+ };
4016
+ }
4017
+
4018
+ getAgentUsageSnapshots(options = {}) {
4019
+ const now = options.now || Date.now();
4020
+ const windowMs = options.windowMs || AGENT_USAGE_RATE_WINDOW_MS;
4021
+ const agents = Array.from(this.agents.values()).map(agent => ({
4022
+ agentId: agent.id,
4023
+ command: agent.command,
4024
+ cwd: agent.cwd,
4025
+ isMain: this.isMainAgentRecord(agent.id, agent),
4026
+ status: agent.status,
4027
+ usageRate: this.calculateAgentUsageRate(agent.id, { now, windowMs }),
4028
+ }));
4029
+ const totalOutputBytes = agents.reduce((sum, agent) => sum + agent.usageRate.outputBytes, 0);
4030
+ const estimatedOutputTokens = agents.reduce((sum, agent) => sum + agent.usageRate.estimatedOutputTokens, 0);
4031
+ const windowMinutes = Math.max(1, windowMs / 60_000);
4032
+
4033
+ return {
4034
+ windowMs,
4035
+ sampledAt: now,
4036
+ source: 'terminal-output-estimate',
4037
+ totalOutputBytes,
4038
+ estimatedOutputTokens,
4039
+ estimatedTokensPerMinute: Math.round((estimatedOutputTokens / windowMinutes) * 10) / 10,
4040
+ agents,
4041
+ };
4042
+ }
4043
+
4044
+ getState() {
4045
+ const state = {
4046
+ mainAgentId: this.mainAgentId,
4047
+ agents: [],
4048
+ taskHistory: this.taskHistory
4049
+ };
4050
+
4051
+ for (const [id, agent] of this.agents) {
4052
+ const now = Date.now();
4053
+ const lastActivity = this.lastActivity.get(id) || now;
4054
+ const isMain = this.isMainAgentRecord(id, agent);
4055
+ const terminalBusy = typeof agent.terminalBusy === 'boolean' ? agent.terminalBusy : null;
4056
+ const terminalStatus = deriveAgentTerminalStatus(agent, {
4057
+ terminalBusy,
4058
+ status: terminalRuntimeStatus(agent.status),
4059
+ title: agent.sessionTitle || '',
4060
+ previewText: agent.previewText || '',
4061
+ });
4062
+
4063
+ state.agents.push({
4064
+ id: agent.id,
4065
+ command: agent.command,
4066
+ engineName: agent.engineName || '',
4067
+ cwd: agent.cwd,
4068
+ projectWorkspace: agent.projectWorkspace || '',
4069
+ output: agent.output.slice(-2000),
4070
+ previewText: agent.previewText || '',
4071
+ previewCols: agent.previewCols || 80,
4072
+ previewRows: agent.previewRows || 30,
4073
+ sessionTitle: agent.sessionTitle || '',
4074
+ sessionSource: this.getEngineSessionSource(agent.engineName),
4075
+ status: agent.status,
4076
+ terminalBusy,
4077
+ terminalStatus,
4078
+ shellCommand: agent.shellCommand || '',
4079
+ shellLastCommand: agent.shellLastCommand || '',
4080
+ shellCommandStartedAt: finiteNumberOrNull(agent.shellCommandStartedAt),
4081
+ shellLastCommandStartedAt: finiteNumberOrNull(agent.shellLastCommandStartedAt),
4082
+ shellLastCommandFinishedAt: finiteNumberOrNull(agent.shellLastCommandFinishedAt),
4083
+ shellLastCommandDurationMs: finiteNumberOrNull(agent.shellLastCommandDurationMs),
4084
+ isMain,
4085
+ parentAgentId: agent.parentAgentId || '',
4086
+ task: agent.task || '',
4087
+ workflowTemplate: agent.workflowTemplate || '',
4088
+ source: agent.source || '',
4089
+ providerSessionProvider: agent.providerSessionProvider || '',
4090
+ providerHomeId: agent.providerHomeId || '',
4091
+ providerHomePath: agent.providerHomePath || '',
4092
+ providerSessionId: agent.providerSessionId || '',
4093
+ providerSessionKey: agent.providerSessionKey || '',
4094
+ providerSessionTemporary: agent.providerSessionTemporary === true,
4095
+ providerSessionSource: agent.providerSessionSource || '',
4096
+ providerSessionResolvedAt: agent.providerSessionResolvedAt || null,
4097
+ providerSessionTitle: agent.providerSessionTitle || '',
4098
+ codexRuntimeMode: agent.codexRuntimeMode || '',
4099
+ agentRuntimeMode: agent.agentRuntimeMode || 'terminal',
4100
+ jsonCliState: agent.jsonCliState || '',
4101
+ jsonCliError: agent.jsonCliError || '',
4102
+ jsonCliTranscriptUpdatedAt: agent.jsonCliTranscriptUpdatedAt || '',
4103
+ acpState: agent.acpState || '',
4104
+ acpError: agent.acpError || '',
4105
+ acpStopReason: agent.acpStopReason || '',
4106
+ acpPendingPermission: agent.acpPendingPermission || null,
4107
+ acpPendingPermissions: Array.isArray(agent.acpPendingPermissions) ? agent.acpPendingPermissions : [],
4108
+ acpPendingElicitation: agent.acpPendingElicitation || null,
4109
+ acpPendingElicitations: Array.isArray(agent.acpPendingElicitations) ? agent.acpPendingElicitations : [],
4110
+ acpActiveElicitations: Array.isArray(agent.acpActiveElicitations) ? agent.acpActiveElicitations : [],
4111
+ acpSessionUpdatedAt: agent.acpSessionUpdatedAt || '',
4112
+ acpSessionRevision: Number(agent.acpSessionRevision) || 0,
4113
+ codexAppServerState: agent.codexAppServerState || '',
4114
+ codexAppServerEndpoint: agent.codexAppServerEndpoint || '',
4115
+ codexAppServerThreadId: agent.codexAppServerThreadId || '',
4116
+ codexAppServerTurnId: agent.codexAppServerTurnId || '',
4117
+ codexAppServerError: agent.codexAppServerError || '',
4118
+ codexAppServerPendingRequestId: agent.codexAppServerPendingRequestId || '',
4119
+ codexAppServerPendingRequestMethod: agent.codexAppServerPendingRequestMethod || '',
4120
+ codexAppServerPendingRequest: agent.codexAppServerPendingRequest || null,
4121
+ codexAppServerNotice: agent.codexAppServerNotice || null,
4122
+ codexAppServerGoal: agent.codexAppServerGoal || null,
4123
+ codexCliObserverDeferred: agent.codexCliObserverDeferred === true,
4124
+ forkedFromProviderSessionId: agent.forkedFromProviderSessionId || '',
4125
+ restartedFromAgentId: agent.restartedFromAgentId || '',
4126
+ restartedFromAgentIds: Array.isArray(agent.restartedFromAgentIds) ? agent.restartedFromAgentIds : [],
4127
+ launchPermissionMode: agent.launchPermissionMode || '',
4128
+ customTitle: agent.customTitle || '',
4129
+ pinned: agent.pinned === true,
4130
+ projectOrder: finiteOrder(agent.projectOrder),
4131
+ pinnedOrder: finiteOrder(agent.pinnedOrder),
4132
+ attentionSeq: finiteNonNegativeInteger(agent.attentionSeq),
4133
+ readAttentionSeq: finiteNonNegativeInteger(agent.readAttentionSeq),
4134
+ attentionUpdatedAt: finiteNumberOrNull(agent.attentionUpdatedAt),
4135
+ readAttentionAt: finiteNumberOrNull(agent.readAttentionAt),
4136
+ attentionReason: agent.attentionReason || '',
4137
+ attentionOutputSeq: finiteNumberOrNull(agent.attentionOutputSeq),
4138
+ unread: agentAttentionUnread(agent),
4139
+ archived: agent.archived === true,
4140
+ archivedAt: agent.archivedAt || null,
4141
+ canForkNewWorktree: this.canCreateForkWorktree(agent.projectWorkspace || agent.cwd || ''),
4142
+ startedAt: agent.startedAt || null,
4143
+ exitedAt: agent.exitedAt || null,
4144
+ // Main agent is exempt from activity/attention/zombie scoring
4145
+ activityLevel: isMain ? 'warm' : this.calculateActivityLevel(lastActivity, now),
4146
+ lastActivity,
4147
+ attentionScore: isMain ? 0 : this.calculateAttentionScore(id, now),
4148
+ isZombie: isMain ? false : this.isZombie(id, now),
4149
+ usageRate: this.calculateAgentUsageRate(id, { now })
4150
+ });
4151
+ }
4152
+
4153
+ return state;
4154
+ }
4155
+
4156
+ calculateActivityLevel(lastActivity, now) {
4157
+ const secondsSinceActivity = (now - lastActivity) / 1000;
4158
+
4159
+ if (secondsSinceActivity < ACTIVITY_HOT_SEC) return 'hot';
4160
+ if (secondsSinceActivity < ACTIVITY_WARM_SEC) return 'warm';
4161
+ if (secondsSinceActivity < ACTIVITY_COOL_SEC) return 'cool';
4162
+ return 'cold';
4163
+ }
4164
+
4165
+ isZombie(agentId, now) {
4166
+ const agent = this.agents.get(agentId);
4167
+ if (!agent || agent.status !== 'running') return false;
4168
+ if (this.isMainAgentRecord(agentId, agent)) return false;
4169
+ const lastAct = this.lastActivity.get(agentId) || now;
4170
+ return now - lastAct > ZOMBIE_IDLE_MS;
4171
+ }
4172
+
4173
+ calculateAttentionScore(agentId, now) {
4174
+ const agent = this.agents.get(agentId);
4175
+ if (!agent) return 0;
4176
+ if (this.isMainAgentRecord(agentId, agent)) return 0;
4177
+
4178
+ let score = 0;
4179
+ const lastAct = this.lastActivity.get(agentId) || now;
4180
+ const secsSinceActivity = (now - lastAct) / 1000;
4181
+
4182
+ // Status weight (0-20)
4183
+ if (agent.status === 'running') score += 20;
4184
+ else if (agent.status === 'pending') score += 15;
4185
+ else if (agent.status === 'stopped') score += 5;
4186
+
4187
+ // Recency (0-40)
4188
+ if (secsSinceActivity < ACTIVITY_HOT_SEC) score += 40;
4189
+ else if (secsSinceActivity < ACTIVITY_WARM_SEC) score += 30;
4190
+ else if (secsSinceActivity < ACTIVITY_COOL_SEC) score += 15;
4191
+
4192
+ // Output rate (0-30) — based on events in last 30s
4193
+ const events = this.outputEvents.get(agentId) || [];
4194
+ const recentEvents = events.filter(e => (now - e.timestamp) < 30000);
4195
+ if (recentEvents.length > 0) {
4196
+ const eventsPerSec = recentEvents.length / 30;
4197
+ const totalBytes = recentEvents.reduce((sum, e) => sum + e.bytes, 0);
4198
+ const bytesPerSec = totalBytes / 30;
4199
+ score += Math.min(30, Math.round(eventsPerSec * 6 + bytesPerSec / 50));
4200
+ }
4201
+
4202
+ // Zombie penalty
4203
+ if (this.isZombie(agentId, now)) {
4204
+ score = Math.max(0, score - 10);
4205
+ }
4206
+
4207
+ return Math.min(100, Math.max(0, score));
4208
+ }
4209
+
4210
+ getUptime() {
4211
+ return Math.floor((Date.now() - this.startTime) / 1000);
4212
+ }
4213
+
4214
+ onSystemStats(callback) {
4215
+ this.on('system-stats', callback);
4216
+ }
4217
+
4218
+ onUpdate(callback) {
4219
+ this.on('update', callback);
4220
+ }
4221
+
4222
+ onSessionStream(callback) {
4223
+ this.on('session-stream', callback);
4224
+ }
4225
+
4226
+ onSessionPreview(callback) {
4227
+ this.on('session-preview-update', callback);
4228
+ }
4229
+
4230
+ getPreviewPayloads() {
4231
+ const previews = [];
4232
+ for (const agent of this.agents.values()) {
4233
+ if (!agent.previewText && !agent.previewSnapshot) {
4234
+ continue;
4235
+ }
4236
+
4237
+ previews.push({
4238
+ agentId: agent.id,
4239
+ previewText: agent.previewText || '',
4240
+ cols: agent.previewCols || 80,
4241
+ rows: agent.previewRows || 30,
4242
+ previewSnapshot: agent.previewSnapshot || null,
4243
+ terminalStatus: deriveAgentTerminalStatus(agent, {
4244
+ previewText: agent.previewText || '',
4245
+ title: agent.sessionTitle || '',
4246
+ terminalBusy: typeof agent.terminalBusy === 'boolean' ? agent.terminalBusy : null,
4247
+ }),
4248
+ });
4249
+ }
4250
+
4251
+ return previews;
4252
+ }
4253
+ }
4254
+
4255
+ module.exports = AgentManager;
4256
+ module.exports.SESSION_OUTPUT_LIMIT = SESSION_OUTPUT_LIMIT;
4257
+ module.exports.AGENT_USAGE_RATE_WINDOW_MS = AGENT_USAGE_RATE_WINDOW_MS;
4258
+ module.exports.ZOMBIE_IDLE_MS = ZOMBIE_IDLE_MS;
4259
+ module.exports.trimSessionOutput = trimSessionOutput;