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.
- package/LICENSE +21 -0
- package/README.md +287 -0
- package/README.zh_cn.md +329 -0
- package/SECURITY.md +29 -0
- package/THIRD_PARTY_NOTICES.md +106 -0
- package/backend/acp/client-services.js +312 -0
- package/backend/acp/patch-decisions.js +137 -0
- package/backend/acp/permission-security.js +108 -0
- package/backend/acp-runtime.js +1144 -0
- package/backend/acp-session-state.js +448 -0
- package/backend/acp-transcript.js +169 -0
- package/backend/agent-activity.js +186 -0
- package/backend/agent-env.js +250 -0
- package/backend/agent-json-stream.js +268 -0
- package/backend/agent-manager.js +4259 -0
- package/backend/agent-memory-report.js +583 -0
- package/backend/agent-order.js +162 -0
- package/backend/agent-provider-session.js +287 -0
- package/backend/agent-session-history.js +888 -0
- package/backend/app-server-api.js +917 -0
- package/backend/async-cache.js +94 -0
- package/backend/auth.js +178 -0
- package/backend/claude-settings.js +121 -0
- package/backend/cli-agents.js +484 -0
- package/backend/codex-app-server-home.js +39 -0
- package/backend/codex-app-server-runtime.js +901 -0
- package/backend/codex-context-window.js +189 -0
- package/backend/codex-models.js +229 -0
- package/backend/codex-rollout-follower.js +243 -0
- package/backend/codex-session-history.js +553 -0
- package/backend/codex-transcript-sanitizer.js +103 -0
- package/backend/codex-transcript.js +2341 -0
- package/backend/command-runner-child.js +67 -0
- package/backend/config-manager.js +815 -0
- package/backend/control-api.js +151 -0
- package/backend/data/chinese-poetic-words.json +8223 -0
- package/backend/data/poetic-word-sources/china.json +49209 -0
- package/backend/data/poetic-word-sources/india.json +9763 -0
- package/backend/data/poetic-word-sources/japan.json +8455 -0
- package/backend/executable-discovery.js +207 -0
- package/backend/farming-app-cli.js +790 -0
- package/backend/farming-cli.js +400 -0
- package/backend/farming-session-store.js +373 -0
- package/backend/haiku-token.js +772 -0
- package/backend/index-html.js +49 -0
- package/backend/input-parts.js +54 -0
- package/backend/input-routing.js +24 -0
- package/backend/json-cli-runtime.js +161 -0
- package/backend/local-session-engine.js +620 -0
- package/backend/main-agent-skills.js +292 -0
- package/backend/main-page-session.js +119 -0
- package/backend/native-pty-host-client.js +516 -0
- package/backend/native-pty-host-path.js +22 -0
- package/backend/native-pty-host.js +816 -0
- package/backend/native-session-engine.js +196 -0
- package/backend/network.js +23 -0
- package/backend/npm-update-helper.js +196 -0
- package/backend/packaged-node-pty.js +79 -0
- package/backend/provider-session-id.js +47 -0
- package/backend/qr-share-tickets.js +100 -0
- package/backend/review-diff-router.js +180 -0
- package/backend/review-diff-service.js +1270 -0
- package/backend/review-session-router.js +74 -0
- package/backend/review-session-service.js +432 -0
- package/backend/review-session-store.js +144 -0
- package/backend/review-state-router.js +116 -0
- package/backend/review-state-store.js +287 -0
- package/backend/run-history-store.js +70 -0
- package/backend/server.js +2446 -0
- package/backend/session-engine-bridge.js +110 -0
- package/backend/session-engine-router.js +66 -0
- package/backend/session-engine.js +39 -0
- package/backend/shell-busy-integration.js +472 -0
- package/backend/slash-command-discovery.js +296 -0
- package/backend/storage-layout.js +101 -0
- package/backend/system-monitor.js +77 -0
- package/backend/terminal-runtime-cleanup.js +80 -0
- package/backend/terminal-screen-state.js +240 -0
- package/backend/terminal-screen-worker-pool.js +165 -0
- package/backend/terminal-screen-worker-thread.js +161 -0
- package/backend/terminal-screen-worker.js +255 -0
- package/backend/terminal-status.js +356 -0
- package/backend/theme-manager.js +145 -0
- package/backend/update-service.js +1183 -0
- package/backend/usage-forecast.js +86 -0
- package/backend/usage-monitor.js +1357 -0
- package/backend/workspace-discovery.js +311 -0
- package/backend/workspace-file-router.js +431 -0
- package/backend/workspace-file-service.js +2765 -0
- package/bin/farming +5 -0
- package/config/farming.deploy.env.example +13 -0
- package/config/farming.install.env.example +17 -0
- package/dist/assets/App-BrP-ENHg.css +1 -0
- package/dist/assets/App-Dza5yEDe.js +124 -0
- package/dist/assets/FileEditorMarkdownPreview-DRDNQ8mX.css +1 -0
- package/dist/assets/FileEditorMarkdownPreview-elKWc8Im.js +404 -0
- package/dist/assets/FileEditorPane-BQLocXOp.js +5 -0
- package/dist/assets/IconGlyphs-RkMAdBXF.js +1 -0
- package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/assets/ProjectFilesSection-lS1azbgj.js +12 -0
- package/dist/assets/ReviewPage-DgnmgU2T.js +3 -0
- package/dist/assets/abap-Cc5D5h5i.js +1 -0
- package/dist/assets/abnfDiagram-VRR7QNED-BgawzDQT.js +1 -0
- package/dist/assets/apex-BKopwLmx.js +1 -0
- package/dist/assets/api-D1lyBYIQ.js +1 -0
- package/dist/assets/arc-CWoVlxJw.js +1 -0
- package/dist/assets/architecture-TIHT7OUA-CbJBFh3w.js +1 -0
- package/dist/assets/architectureDiagram-ZJ3FMSHR-DUShwPg0.js +36 -0
- package/dist/assets/array-HeX70jSN.js +1 -0
- package/dist/assets/azcli-DI-C34WA.js +1 -0
- package/dist/assets/bat-Cbp6u42z.js +1 -0
- package/dist/assets/bicep-4QA1jTCM.js +2 -0
- package/dist/assets/blockDiagram-677ZJIJ3-CeGZviXR.js +132 -0
- package/dist/assets/c4Diagram-LMCZKHZV-CLhYht6S.js +10 -0
- package/dist/assets/cameligo-Dda_adPs.js +1 -0
- package/dist/assets/channel-Uw90AO0b.js +1 -0
- package/dist/assets/chunk-2Q5K7J3B-d5WWh8jo.js +1 -0
- package/dist/assets/chunk-32BRIVSS-LFYpT-Cu.js +1 -0
- package/dist/assets/chunk-52WLFC77-BH6jDw9J.js +10 -0
- package/dist/assets/chunk-5FCAYU7R-C7AmIhYv.js +1 -0
- package/dist/assets/chunk-5HE753X5-C8cXaouP.js +1 -0
- package/dist/assets/chunk-5JV3BV7I-C1PtimcA.js +1 -0
- package/dist/assets/chunk-5TONJI2A-_81zUME8.js +2 -0
- package/dist/assets/chunk-5VM5RSS4-Dlw7AXWp.js +15 -0
- package/dist/assets/chunk-7BUUIJ7U-ZBRhjLOh.js +1 -0
- package/dist/assets/chunk-BIQX33UG-CWmzxeAw.js +1 -0
- package/dist/assets/chunk-C7G6YPKG-B_mIUlb7.js +1 -0
- package/dist/assets/chunk-CQNSW5MT-CMayolwI.js +1 -0
- package/dist/assets/chunk-CYSBUYHQ-lSPbrL3Z.js +1 -0
- package/dist/assets/chunk-DECur_0Z.js +1 -0
- package/dist/assets/chunk-EMLP6XTP-D9-jtCnM.js +1 -0
- package/dist/assets/chunk-EX3LRPZG-C63hVuAJ.js +231 -0
- package/dist/assets/chunk-FWX5IMBZ-CboX-WF_.js +2 -0
- package/dist/assets/chunk-HOUHSVGY-BOgSd6Un.js +1 -0
- package/dist/assets/chunk-ICXQ74PX-C0K-pgZt.js +2 -0
- package/dist/assets/chunk-JG7HCLWE-Cib606u_.js +2 -0
- package/dist/assets/chunk-JWPE2WC7-DSyBRaRG.js +1 -0
- package/dist/assets/chunk-KEIR6QF5-CNOHik0T.js +161 -0
- package/dist/assets/chunk-MOJQB5TN-CVMfwN_7.js +88 -0
- package/dist/assets/chunk-MOZMSUNE-D6JeZqmE.js +1 -0
- package/dist/assets/chunk-OGEWGWER-DLT3pxyw.js +1 -0
- package/dist/assets/chunk-OSBZ3O6U-C0W8KSeg.js +1 -0
- package/dist/assets/chunk-PUDLZKDR-Cd079yIQ.js +156 -0
- package/dist/assets/chunk-Q4XR5HBZ-D0jHa6SO.js +70 -0
- package/dist/assets/chunk-QBLGF6JB-p4omp7uO.js +1 -0
- package/dist/assets/chunk-R7FJI6CG-CZ_p9hSr.js +1 -0
- package/dist/assets/chunk-RYQCIY6F-CZA9pvFX.js +1 -0
- package/dist/assets/chunk-U6XO7XAA-CcXKsWxp.js +2 -0
- package/dist/assets/chunk-V7JOEXUC-CVDtR75H.js +206 -0
- package/dist/assets/chunk-VAUOI2AC-B-W2_9Ot.js +1 -0
- package/dist/assets/chunk-VR4S4FIN-FD3kWh32.js +1 -0
- package/dist/assets/chunk-WYO6CB5R-C2bGaS5r.js +127 -0
- package/dist/assets/chunk-XXDRQBXY-BBXwjhUx.js +1 -0
- package/dist/assets/chunk-Y2CYZVJY-Di0sYzi1.js +1 -0
- package/dist/assets/chunk-YOTPTUD7-BXHmEk89.js +1 -0
- package/dist/assets/chunk-ZGVPDNZ5-ymgts0zo.js +62 -0
- package/dist/assets/chunk-ZIRB5QZD-DncwlQQc.js +32 -0
- package/dist/assets/classDiagram-OUVF2IWQ-DKW_dtku.js +1 -0
- package/dist/assets/classDiagram-v2-EOCWNBFH-C8Fi7-yv.js +1 -0
- package/dist/assets/clojure-CvY8A83L.js +1 -0
- package/dist/assets/code-dark-Cjb1QGhu.css +1 -0
- package/dist/assets/code-mobile-3C2Rr-CN.css +1 -0
- package/dist/assets/codicon-ngg6Pgfi.ttf +0 -0
- package/dist/assets/coffee-4LX2Sv8A.js +1 -0
- package/dist/assets/core-ZlAPicox.js +3 -0
- package/dist/assets/cose-bilkent-JH36ORCC-BNIAz0nN.js +1 -0
- package/dist/assets/cpp-D4AzSV28.js +1 -0
- package/dist/assets/csharp-C4nWN4Yd.js +1 -0
- package/dist/assets/csp-CiEaaS5Z.js +1 -0
- package/dist/assets/css-DXxBRsbk.js +3 -0
- package/dist/assets/css.worker-DyDu5ynT.js +89 -0
- package/dist/assets/cssMode-DDWxTPm_.js +1 -0
- package/dist/assets/cynefin-VYW2F7L2-DgXGHlRt.js +1 -0
- package/dist/assets/cynefinDiagram-TSTJHNR4-CwpobXIl.js +62 -0
- package/dist/assets/cypher-DOMzqyFA.js +1 -0
- package/dist/assets/cytoscape.esm-Cl8iC3D7.js +321 -0
- package/dist/assets/dagre-DXlcHgr3.js +1 -0
- package/dist/assets/dagre-VKFMJZFB-Dkhlfjna.js +4 -0
- package/dist/assets/dart-BWS7MXt_.js +1 -0
- package/dist/assets/defaultLocale-GJwWH1Jr.js +1 -0
- package/dist/assets/diagram-FQU43EPY-hA8dicp6.js +3 -0
- package/dist/assets/diagram-G47NLZAW-BTY6JTzO.js +24 -0
- package/dist/assets/diagram-NH7WQ7WH-xkaHHX9M.js +24 -0
- package/dist/assets/diagram-OA4YK3LP-gk_fg9UU.js +30 -0
- package/dist/assets/diagram-WEI45ONY-BvC8xd6T.js +41 -0
- package/dist/assets/dist-ClAL-Tv5.js +1 -0
- package/dist/assets/dockerfile-Baf4U3xT.js +1 -0
- package/dist/assets/ebnfDiagram-CCIWWBDH-CR7RaNat.js +1 -0
- package/dist/assets/ecl-CT7udlkQ.js +1 -0
- package/dist/assets/editor-CtbFZlk0.css +1 -0
- package/dist/assets/editor.api2-B9h6iFPK.js +872 -0
- package/dist/assets/editor.worker-Dpt_xPrs.js +26 -0
- package/dist/assets/elixir-CIbAZw5l.js +1 -0
- package/dist/assets/erDiagram-Q63AITRT-_M-JFPbj.js +85 -0
- package/dist/assets/eventmodeling-45OFAUF4-DXYTQ2nt.js +1 -0
- package/dist/assets/file-icons-EFUGSSwf.js +1 -0
- package/dist/assets/flow9-Dqiw45WV.js +1 -0
- package/dist/assets/flowDiagram-23GEKE2U-DAiS5Y7G.js +1 -0
- package/dist/assets/freemarker2-QBsTERH-.js +3 -0
- package/dist/assets/fsharp-E8E2L9jU.js +1 -0
- package/dist/assets/ganttDiagram-NO4QXBWP-Bl8P1yf_.js +292 -0
- package/dist/assets/gitGraph-TEB2WS4Q-CzTBjEvV.js +1 -0
- package/dist/assets/gitGraphDiagram-IHSO6WYX-DoHa5ysz.js +106 -0
- package/dist/assets/go-CouVSN4D.js +1 -0
- package/dist/assets/graphlib-CbSvZcZU.js +1 -0
- package/dist/assets/graphql-a6h6h5-t.js +1 -0
- package/dist/assets/handlebars-CuoxPo5A.js +1 -0
- package/dist/assets/hcl-vso3e6N-.js +1 -0
- package/dist/assets/html-gX5PEu0Q.js +1 -0
- package/dist/assets/html.worker-IWNXhnIC.js +502 -0
- package/dist/assets/htmlMode-DIWUbUxd.js +1 -0
- package/dist/assets/index-BpHPeJf0.js +10 -0
- package/dist/assets/info-DKCQHKI2-DznP7j3s.js +1 -0
- package/dist/assets/infoDiagram-FWYZ7A6U-DSFcSuUs.js +2 -0
- package/dist/assets/ini-SwZZkotR.js +1 -0
- package/dist/assets/init-BPLPMQ3Y.js +1 -0
- package/dist/assets/ishikawaDiagram-FXEZZL3T-DdkqullI.js +70 -0
- package/dist/assets/java-CtRLC4_3.js +1 -0
- package/dist/assets/javascript-BYKGIUGs.js +1 -0
- package/dist/assets/journeyDiagram-5HDEW3XC-B4hRnQJr.js +139 -0
- package/dist/assets/json.worker-c4h5s80L.js +58 -0
- package/dist/assets/jsonMode-AFmQQP6u.js +7 -0
- package/dist/assets/jsx-runtime-BthL93pI.js +1 -0
- package/dist/assets/julia-CuJWFKQH.js +1 -0
- package/dist/assets/kanban-definition-HUTT4EX6-tm2FZB2E.js +89 -0
- package/dist/assets/katex-C8DPyI6s.js +257 -0
- package/dist/assets/kotlin-B_kBhsre.js +1 -0
- package/dist/assets/less-CtrHIfz3.js +2 -0
- package/dist/assets/lexon-yVt1pJLu.js +1 -0
- package/dist/assets/line-CmsjK4kn.js +1 -0
- package/dist/assets/linear-Dld3_ERu.js +1 -0
- package/dist/assets/liquid-DBIl-f0z.js +1 -0
- package/dist/assets/lspLanguageFeatures-DK6tqicV.js +4 -0
- package/dist/assets/lua-prOwgJ4U.js +1 -0
- package/dist/assets/m3-DhrdTQ84.js +1 -0
- package/dist/assets/main-DTJwH51M.css +1 -0
- package/dist/assets/map-LZWWoeWa.js +1 -0
- package/dist/assets/markdown-DoI5HwSM.js +1 -0
- package/dist/assets/mdx-DS-bVtZN.js +1 -0
- package/dist/assets/mermaid-parser.core-BQtepoLd.js +4 -0
- package/dist/assets/mermaid.core-C7UT_EEL.js +11 -0
- package/dist/assets/mindmap-definition-LN4V7U3C-DplfjGms.js +96 -0
- package/dist/assets/mips-Dye5jsBU.js +1 -0
- package/dist/assets/monaco.contribution-CyLosfZI.js +2 -0
- package/dist/assets/msdax-TkNOyIL8.js +1 -0
- package/dist/assets/mysql-CEc-RKZF.js +1 -0
- package/dist/assets/objective-c-CqqCsQz0.js +1 -0
- package/dist/assets/ordinal-BLrOss5K.js +1 -0
- package/dist/assets/packet-7NZHBO7P-BDEPOPha.js +1 -0
- package/dist/assets/pascal-DO_pj8yg.js +1 -0
- package/dist/assets/pascaligo-1cbdZdF3.js +1 -0
- package/dist/assets/path-YdFzr2W6.js +1 -0
- package/dist/assets/pegDiagram-2B236MQR-lu3SdkQs.js +1 -0
- package/dist/assets/perl-B_N3PYfC.js +1 -0
- package/dist/assets/pgsql-9iYk1FQ7.js +1 -0
- package/dist/assets/php-rO2sgoUM.js +1 -0
- package/dist/assets/pie-RZYD4A2V-DWIrwPJQ.js +1 -0
- package/dist/assets/pieDiagram-ENE6RG2P-3fHbmDf9.js +39 -0
- package/dist/assets/pla-Cbgv60kq.js +1 -0
- package/dist/assets/postiats-y6xrhy_M.js +1 -0
- package/dist/assets/powerquery-BaD8gWNE.js +1 -0
- package/dist/assets/powershell-BMhG4JVe.js +1 -0
- package/dist/assets/preload-helper-mJowh_hw.js +1 -0
- package/dist/assets/protobuf-CdOP5kN3.js +2 -0
- package/dist/assets/pug-Ck60hNF9.js +1 -0
- package/dist/assets/python-C93q3xXz.js +1 -0
- package/dist/assets/qoder-C9LmmOSf.svg +1 -0
- package/dist/assets/qoder-Cf9gl0Y5.svg +1 -0
- package/dist/assets/qoder-gHCinseV.svg +1 -0
- package/dist/assets/qrcode-DV-_rjU8.js +3 -0
- package/dist/assets/qsharp-BcaKFfP-.js +1 -0
- package/dist/assets/quadrantDiagram-ABIIQ3AL-a5qoWz-X.js +7 -0
- package/dist/assets/r-rFNes0iK.js +1 -0
- package/dist/assets/radar-I7S5WNFK-B7OaD8vp.js +1 -0
- package/dist/assets/railroad-3IZDKUUU-BXW2nuCA.js +1 -0
- package/dist/assets/railroad-abnf-AHOZXSZD-Buw9QG2r.js +1 -0
- package/dist/assets/railroad-ebnf-EBAXGLYW-BjSi4sWJ.js +1 -0
- package/dist/assets/railroad-peg-LSFZ7HO6-DN1wIVhS.js +1 -0
- package/dist/assets/railroadDiagram-RFXS5EU6-D2TOuXW9.js +1 -0
- package/dist/assets/razor-CBifNGXO.js +1 -0
- package/dist/assets/react-dom-gtU6X-Hj.js +1 -0
- package/dist/assets/redis-DkpQ3K6G.js +1 -0
- package/dist/assets/redshift-Di9N5TI3.js +1 -0
- package/dist/assets/requirementDiagram-TGXJPOKE-LQBWpUvd.js +84 -0
- package/dist/assets/restructuredtext-By5ZzLt6.js +1 -0
- package/dist/assets/review-BcnwQtJv.css +1 -0
- package/dist/assets/rough.esm-c4PR5shF.js +1 -0
- package/dist/assets/ruby-fF5zGiLp.js +1 -0
- package/dist/assets/rust-C5b3AYY6.js +1 -0
- package/dist/assets/sankeyDiagram-HTMAVEWB-B1ohYoyK.js +40 -0
- package/dist/assets/sb-CD07UNRs.js +1 -0
- package/dist/assets/scala-jtn1ip9G.js +1 -0
- package/dist/assets/scheme-C4vqMkn_.js +1 -0
- package/dist/assets/scss-I4Glxjbz.js +3 -0
- package/dist/assets/sequenceDiagram-DBY2YBRQ-C3vM-hMq.js +162 -0
- package/dist/assets/shell-DubUcuns.js +1 -0
- package/dist/assets/sizeCapture-X5ZJPWSS-daTB6OcF.js +1 -0
- package/dist/assets/solidity-CNy2KSl2.js +1 -0
- package/dist/assets/sophia-D9_URt84.js +1 -0
- package/dist/assets/sparql-DviXpm4C.js +1 -0
- package/dist/assets/sql-vDRbAZgK.js +1 -0
- package/dist/assets/src-WBW1QxJt.js +1 -0
- package/dist/assets/st-CnpnRqA7.js +1 -0
- package/dist/assets/stateDiagram-2N3HPSRC-BvFhOryI.js +1 -0
- package/dist/assets/stateDiagram-v2-6OUMAXLB-WMrz4PNo.js +1 -0
- package/dist/assets/swift-D_g8Ganz.js +1 -0
- package/dist/assets/swimlanes-5IMT3BWC-DKF_b-2s.js +2 -0
- package/dist/assets/swimlanesDiagram-G3AALYLV-BGswC9oC.js +8 -0
- package/dist/assets/systemverilog-OuZa5h1M.js +1 -0
- package/dist/assets/tcl-DBm9ucsV.js +1 -0
- package/dist/assets/timeline-definition-FHXFAJF6-vAs11ux_.js +120 -0
- package/dist/assets/tokens-BwenNn5Q.css +1 -0
- package/dist/assets/treeView-QDETBFTQ-BVfICaZQ.js +1 -0
- package/dist/assets/treemap-6X3UGDF4-oWHPqo17.js +1 -0
- package/dist/assets/ts.worker-BwM5Ha3L.js +67719 -0
- package/dist/assets/tsMode-DGZTlTj8.js +11 -0
- package/dist/assets/twig-BRzEqX5Y.js +1 -0
- package/dist/assets/typescript-KJKfYO7K.js +1 -0
- package/dist/assets/typespec-D4ZgGeX8.js +1 -0
- package/dist/assets/useWorkspaceMenuKeyboard-CneKAZUJ.js +1 -0
- package/dist/assets/vb-DcP4s6WS.js +1 -0
- package/dist/assets/vennDiagram-L72KCM5P-BybTX_9B.js +34 -0
- package/dist/assets/wardley-OPB4EBWU-Dg8t5y3s.js +1 -0
- package/dist/assets/wardleyDiagram-EHGQE667-CicUmMBR.js +78 -0
- package/dist/assets/wgsl-CnRJ_QUf.js +298 -0
- package/dist/assets/workers-BSQRIkqa.js +1 -0
- package/dist/assets/workspace-view-state-CTyDzk2D.js +1 -0
- package/dist/assets/xml-C8FAcN6j.js +1 -0
- package/dist/assets/xychartDiagram-FW5EYKEG-BhWDyCX-.js +7 -0
- package/dist/assets/yaml-BQ4NrRhG.js +1 -0
- package/dist/assets/zsh-CLpveKlF.svg +1 -0
- package/dist/assets/zsh-FxSpMPbz.svg +1 -0
- package/dist/farming-2/app-icon-v2-180.png +0 -0
- package/dist/farming-2/app-icon-v2-192.png +0 -0
- package/dist/farming-2/app-icon-v2-512.png +0 -0
- package/dist/farming-2/app-icon-v2-maskable-512.png +0 -0
- package/dist/farming-2/app-icon-v2-master.png +0 -0
- package/dist/farming-2/favicon-v2-16.png +0 -0
- package/dist/farming-2/favicon-v2-32.png +0 -0
- package/dist/farming-2/favicon-v2-48.png +0 -0
- package/dist/farming-2/favicon-v2-64.png +0 -0
- package/dist/farming-2/favicon-v2.ico +0 -0
- package/dist/farming-2/favicon.ico +0 -0
- package/dist/farming-2/github-social-preview.png +0 -0
- package/dist/farming-2/images/avatar-watercolor-v1-bee-garden.png +0 -0
- package/dist/farming-2/images/avatar-watercolor-v2.png +0 -0
- package/dist/farming-2/images/avatar-watercolor-v3.png +0 -0
- package/dist/farming-2/legacy-icons-v1/favicon.ico +1 -0
- package/dist/farming-2/legacy-icons-v1/logo-16.png +0 -0
- package/dist/farming-2/legacy-icons-v1/logo-180.png +0 -0
- package/dist/farming-2/legacy-icons-v1/logo-192.png +0 -0
- package/dist/farming-2/legacy-icons-v1/logo-32.png +0 -0
- package/dist/farming-2/legacy-icons-v1/logo-48.png +0 -0
- package/dist/farming-2/legacy-icons-v1/logo-512.png +0 -0
- package/dist/farming-2/legacy-icons-v1/logo-64.png +0 -0
- package/dist/farming-2/logo-16.png +0 -0
- package/dist/farming-2/logo-180.png +0 -0
- package/dist/farming-2/logo-192.png +0 -0
- package/dist/farming-2/logo-32.png +0 -0
- package/dist/farming-2/logo-48.png +0 -0
- package/dist/farming-2/logo-512.png +0 -0
- package/dist/farming-2/logo-64.png +0 -0
- package/dist/farming-2/site.webmanifest +27 -0
- package/dist/farming-2/style_capsules/watercolor-pastoral.json +94 -0
- package/dist/index.html +27 -0
- package/frontend/ghostty-loader.js +24 -0
- package/frontend/runtime-paths.js +28 -0
- package/frontend/session-bridge.js +82 -0
- package/frontend/session-modal-bridge.js +331 -0
- package/frontend/skin-bridge.js +25 -0
- package/frontend/skins/crt/app.js +6940 -0
- package/frontend/skins/crt/assets/branding/farming-crt-icon.svg +11 -0
- package/frontend/skins/crt/assets/branding/farming-crt-logo-v1.png +0 -0
- package/frontend/skins/crt/assets/effects/phosphor-noise.svg +11 -0
- package/frontend/skins/crt/assets/fonts/departure-mono/DepartureMonoNerdFontMono-Regular.otf +0 -0
- package/frontend/skins/crt/assets/fonts/departure-mono/LICENSE +93 -0
- package/frontend/skins/crt/effects.js +8 -0
- package/frontend/skins/crt/index.html +1712 -0
- package/frontend/skins/crt/styles/billing.css +1003 -0
- package/frontend/skins/crt/styles/effects.css +171 -0
- package/frontend/skins/crt/styles/history.css +215 -0
- package/frontend/skins/crt/styles/monochrome-green.css +509 -0
- package/frontend/skins/crt/styles/search.css +271 -0
- package/frontend/terminal-bridge.js +192 -0
- package/frontend/theme-loader.js +84 -0
- package/frontend/themes/terminal/theme.json +24 -0
- package/frontend/vendor/ghostty-web/LICENSE +21 -0
- package/frontend/vendor/ghostty-web/__vite-browser-external-2447137e.js +4 -0
- package/frontend/vendor/ghostty-web/ghostty-vt.wasm +0 -0
- package/frontend/vendor/ghostty-web/ghostty-web.js +2963 -0
- package/frontend/vendor/ghostty-web/ghostty-web.umd.cjs +13 -0
- package/frontend/vendor/ghostty-web/index.d.ts +1842 -0
- package/index.html +23 -0
- package/package.json +144 -0
- 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;
|