factory-factory 0.3.3 → 0.3.5
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/dist/client/assets/{_basePickBy-0_GqMukI.js → _basePickBy-DOa44aLB.js} +1 -1
- package/dist/client/assets/{_baseUniq-BkDCIs_N.js → _baseUniq-DuVP6l01.js} +1 -1
- package/dist/client/assets/{arc-BYBi6T58.js → arc-BRTVE1a5.js} +1 -1
- package/dist/client/assets/{architectureDiagram-VXUJARFQ-utRnH0EI.js → architectureDiagram-VXUJARFQ-DphOng2K.js} +1 -1
- package/dist/client/assets/{blockDiagram-VD42YOAC-ke2uPDpN.js → blockDiagram-VD42YOAC-DvVRHTty.js} +1 -1
- package/dist/client/assets/{c4Diagram-YG6GDRKO-DAt0t6eK.js → c4Diagram-YG6GDRKO-BZaLUCEP.js} +1 -1
- package/dist/client/assets/channel-v55vPUhK.js +1 -0
- package/dist/client/assets/{chunk-4BX2VUAB-D1vNGBo6.js → chunk-4BX2VUAB-n69YHsoN.js} +1 -1
- package/dist/client/assets/{chunk-55IACEB6-BDwpH7UI.js → chunk-55IACEB6-C67Ak1Qy.js} +1 -1
- package/dist/client/assets/{chunk-B4BG7PRW-DAM5zmSO.js → chunk-B4BG7PRW-_Ja9GXA4.js} +1 -1
- package/dist/client/assets/{chunk-DI55MBZ5-CgmMeVp9.js → chunk-DI55MBZ5-C1TAOocX.js} +1 -1
- package/dist/client/assets/{chunk-FMBD7UC4-nijXNm9V.js → chunk-FMBD7UC4-Co8OmD7c.js} +1 -1
- package/dist/client/assets/{chunk-QN33PNHL-GM5iQLPS.js → chunk-QN33PNHL-ESdvapXF.js} +1 -1
- package/dist/client/assets/{chunk-QZHKN3VN-CbOwrlLz.js → chunk-QZHKN3VN-DQTGFT4I.js} +1 -1
- package/dist/client/assets/{chunk-TZMSLE5B-D0eFaU6T.js → chunk-TZMSLE5B-C7a0BTNr.js} +1 -1
- package/dist/client/assets/classDiagram-2ON5EDUG-DV-E9odE.js +1 -0
- package/dist/client/assets/classDiagram-v2-WZHVMYZB-DV-E9odE.js +1 -0
- package/dist/client/assets/clone-DQgzPMba.js +1 -0
- package/dist/client/assets/{cose-bilkent-S5V4N54A-BAHIgVQk.js → cose-bilkent-S5V4N54A-Dg_Elpr2.js} +1 -1
- package/dist/client/assets/{dagre-6UL2VRFP-CNvHuwOI.js → dagre-6UL2VRFP-DC_BntL_.js} +1 -1
- package/dist/client/assets/{diagram-PSM6KHXK-726LBpxe.js → diagram-PSM6KHXK-D4EnEadG.js} +1 -1
- package/dist/client/assets/{diagram-QEK2KX5R-DQsnVN2A.js → diagram-QEK2KX5R-DeDgxkkd.js} +1 -1
- package/dist/client/assets/{diagram-S2PKOQOG-D_T0ifdw.js → diagram-S2PKOQOG-DqCrW2Mz.js} +1 -1
- package/dist/client/assets/{erDiagram-Q2GNP2WA-BmVDGu8M.js → erDiagram-Q2GNP2WA-CoIzOJMx.js} +1 -1
- package/dist/client/assets/{flowDiagram-NV44I4VS-Z65JFfSc.js → flowDiagram-NV44I4VS-H-hzyVxN.js} +1 -1
- package/dist/client/assets/{ganttDiagram-JELNMOA3-BZ3yfDKP.js → ganttDiagram-JELNMOA3-BZbu1Bvf.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-NY62KEGX-faBi9oTt.js → gitGraphDiagram-NY62KEGX-yHNzOwte.js} +1 -1
- package/dist/client/assets/{graph-DMDSMqaB.js → graph-0fFVLhuM.js} +1 -1
- package/dist/client/assets/index-CzQJ6qnR.css +1 -0
- package/dist/client/assets/{index-D9ddlgEa.js → index-D2gG7VAV.js} +170 -170
- package/dist/client/assets/{infoDiagram-WHAUD3N6-CJU-Jxtx.js → infoDiagram-WHAUD3N6-BdYcSIzA.js} +1 -1
- package/dist/client/assets/{journeyDiagram-XKPGCS4Q-CIFki-iq.js → journeyDiagram-XKPGCS4Q-BgD0iboh.js} +1 -1
- package/dist/client/assets/{kanban-definition-3W4ZIXB7-DDuM9VeF.js → kanban-definition-3W4ZIXB7-2cCK2J0j.js} +1 -1
- package/dist/client/assets/{layout-CnYzyA25.js → layout-UQByNmge.js} +1 -1
- package/dist/client/assets/{linear-BoRrKhGP.js → linear-COsW-oY4.js} +1 -1
- package/dist/client/assets/{mindmap-definition-VGOIOE7T-QhVqNLyz.js → mindmap-definition-VGOIOE7T-CnhVGcBr.js} +1 -1
- package/dist/client/assets/{pieDiagram-ADFJNKIX-BkK0cKA8.js → pieDiagram-ADFJNKIX-DX0wt7fJ.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-AYHSOK5B-CeFigc3p.js → quadrantDiagram-AYHSOK5B-nfJepDyE.js} +1 -1
- package/dist/client/assets/{requirementDiagram-UZGBJVZJ-KowHp3BW.js → requirementDiagram-UZGBJVZJ-I_tnp42L.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-TZEHDZUN-D7CevVS2.js → sankeyDiagram-TZEHDZUN-4r_mPLW_.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-WL72ISMW-3mHDMyaP.js → sequenceDiagram-WL72ISMW-DWymCFZa.js} +1 -1
- package/dist/client/assets/{stateDiagram-FKZM4ZOC-h-G1xS2P.js → stateDiagram-FKZM4ZOC-CCHlcmzO.js} +1 -1
- package/dist/client/assets/stateDiagram-v2-4FDKWEC3-CuSoER-j.js +1 -0
- package/dist/client/assets/{terminal-instance-BbEVZkgm.js → terminal-instance-DKydBe-u.js} +1 -1
- package/dist/client/assets/{timeline-definition-IT6M3QCI-Cc2H2fvU.js → timeline-definition-IT6M3QCI-EkYjHzU_.js} +1 -1
- package/dist/client/assets/{treemap-KMMF4GRG-BAjv-2_R.js → treemap-KMMF4GRG-BH9wTzrS.js} +1 -1
- package/dist/client/assets/{xychartDiagram-PRI3JC2R-CqgFJhi0.js → xychartDiagram-PRI3JC2R-DwOnTNwE.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/prisma/generated/commonInputTypes.d.ts +0 -30
- package/dist/prisma/generated/commonInputTypes.d.ts.map +1 -1
- package/dist/prisma/generated/internal/class.js +2 -2
- package/dist/prisma/generated/internal/class.js.map +1 -1
- package/dist/prisma/generated/internal/prismaNamespace.d.ts +0 -2
- package/dist/prisma/generated/internal/prismaNamespace.d.ts.map +1 -1
- package/dist/prisma/generated/internal/prismaNamespace.js +0 -2
- package/dist/prisma/generated/internal/prismaNamespace.js.map +1 -1
- package/dist/prisma/generated/internal/prismaNamespaceBrowser.d.ts +0 -2
- package/dist/prisma/generated/internal/prismaNamespaceBrowser.d.ts.map +1 -1
- package/dist/prisma/generated/internal/prismaNamespaceBrowser.js +0 -2
- package/dist/prisma/generated/internal/prismaNamespaceBrowser.js.map +1 -1
- package/dist/prisma/generated/models/Workspace.d.ts +1 -90
- package/dist/prisma/generated/models/Workspace.d.ts.map +1 -1
- package/dist/prompts/quick-actions/screenshots.md +12 -6
- package/dist/prompts/ratchet/dispatch.md +4 -0
- package/dist/src/backend/domains/github/bridges.d.ts +0 -15
- package/dist/src/backend/domains/github/bridges.d.ts.map +1 -1
- package/dist/src/backend/domains/github/github-domain-exports.test.js +1 -7
- package/dist/src/backend/domains/github/github-domain-exports.test.js.map +1 -1
- package/dist/src/backend/domains/github/index.d.ts +1 -3
- package/dist/src/backend/domains/github/index.d.ts.map +1 -1
- package/dist/src/backend/domains/github/index.js +0 -2
- package/dist/src/backend/domains/github/index.js.map +1 -1
- package/dist/src/backend/domains/ratchet/bridges.d.ts +6 -1
- package/dist/src/backend/domains/ratchet/bridges.d.ts.map +1 -1
- package/dist/src/backend/domains/ratchet/fixer-session.service.d.ts.map +1 -1
- package/dist/src/backend/domains/ratchet/fixer-session.service.js +2 -0
- package/dist/src/backend/domains/ratchet/fixer-session.service.js.map +1 -1
- package/dist/src/backend/domains/ratchet/fixer-session.service.test.js +1 -0
- package/dist/src/backend/domains/ratchet/fixer-session.service.test.js.map +1 -1
- package/dist/src/backend/domains/ratchet/index.d.ts +0 -3
- package/dist/src/backend/domains/ratchet/index.d.ts.map +1 -1
- package/dist/src/backend/domains/ratchet/index.js +0 -2
- package/dist/src/backend/domains/ratchet/index.js.map +1 -1
- package/dist/src/backend/domains/ratchet/ratchet-domain-exports.test.js +1 -7
- package/dist/src/backend/domains/ratchet/ratchet-domain-exports.test.js.map +1 -1
- package/dist/src/backend/domains/ratchet/ratchet.service.d.ts.map +1 -1
- package/dist/src/backend/domains/ratchet/ratchet.service.js +27 -51
- package/dist/src/backend/domains/ratchet/ratchet.service.js.map +1 -1
- package/dist/src/backend/domains/ratchet/ratchet.service.test.js +18 -18
- package/dist/src/backend/domains/ratchet/ratchet.service.test.js.map +1 -1
- package/dist/src/backend/domains/session/acp/acp-event-translator.d.ts +3 -0
- package/dist/src/backend/domains/session/acp/acp-event-translator.d.ts.map +1 -1
- package/dist/src/backend/domains/session/acp/acp-event-translator.js +47 -56
- package/dist/src/backend/domains/session/acp/acp-event-translator.js.map +1 -1
- package/dist/src/backend/domains/session/acp/acp-permission-bridge.d.ts +3 -1
- package/dist/src/backend/domains/session/acp/acp-permission-bridge.d.ts.map +1 -1
- package/dist/src/backend/domains/session/acp/acp-permission-bridge.js +10 -1
- package/dist/src/backend/domains/session/acp/acp-permission-bridge.js.map +1 -1
- package/dist/src/backend/domains/session/acp/acp-permission-bridge.test.js +24 -0
- package/dist/src/backend/domains/session/acp/acp-permission-bridge.test.js.map +1 -1
- package/dist/src/backend/domains/session/acp/acp-runtime-manager.d.ts.map +1 -1
- package/dist/src/backend/domains/session/acp/acp-runtime-manager.js +180 -18
- package/dist/src/backend/domains/session/acp/acp-runtime-manager.js.map +1 -1
- package/dist/src/backend/domains/session/acp/acp-runtime-manager.manual.integration.test.d.ts +2 -0
- package/dist/src/backend/domains/session/acp/acp-runtime-manager.manual.integration.test.d.ts.map +1 -0
- package/dist/src/backend/domains/session/acp/acp-runtime-manager.manual.integration.test.js +40 -0
- package/dist/src/backend/domains/session/acp/acp-runtime-manager.manual.integration.test.js.map +1 -0
- package/dist/src/backend/domains/session/acp/acp-runtime-manager.test.js +88 -1
- package/dist/src/backend/domains/session/acp/acp-runtime-manager.test.js.map +1 -1
- package/dist/src/backend/domains/session/acp/acp-session-negotiation.integration.test.js +13 -1
- package/dist/src/backend/domains/session/acp/acp-session-negotiation.integration.test.js.map +1 -1
- package/dist/src/backend/domains/session/acp/codex-app-server-adapter/codex-app-server-acp-adapter.d.ts +96 -0
- package/dist/src/backend/domains/session/acp/codex-app-server-adapter/codex-app-server-acp-adapter.d.ts.map +1 -0
- package/dist/src/backend/domains/session/acp/codex-app-server-adapter/codex-app-server-acp-adapter.js +2496 -0
- package/dist/src/backend/domains/session/acp/codex-app-server-adapter/codex-app-server-acp-adapter.js.map +1 -0
- package/dist/src/backend/domains/session/acp/codex-app-server-adapter/codex-app-server-acp-adapter.manual.integration.test.d.ts +2 -0
- package/dist/src/backend/domains/session/acp/codex-app-server-adapter/codex-app-server-acp-adapter.manual.integration.test.d.ts.map +1 -0
- package/dist/src/backend/domains/session/acp/codex-app-server-adapter/codex-app-server-acp-adapter.manual.integration.test.js +92 -0
- package/dist/src/backend/domains/session/acp/codex-app-server-adapter/codex-app-server-acp-adapter.manual.integration.test.js.map +1 -0
- package/dist/src/backend/domains/session/acp/codex-app-server-adapter/codex-app-server-acp-adapter.test.d.ts +2 -0
- package/dist/src/backend/domains/session/acp/codex-app-server-adapter/codex-app-server-acp-adapter.test.d.ts.map +1 -0
- package/dist/src/backend/domains/session/acp/codex-app-server-adapter/codex-app-server-acp-adapter.test.js +2026 -0
- package/dist/src/backend/domains/session/acp/codex-app-server-adapter/codex-app-server-acp-adapter.test.js.map +1 -0
- package/dist/src/backend/domains/session/acp/codex-app-server-adapter/codex-rpc-client.d.ts +46 -0
- package/dist/src/backend/domains/session/acp/codex-app-server-adapter/codex-rpc-client.d.ts.map +1 -0
- package/dist/src/backend/domains/session/acp/codex-app-server-adapter/codex-rpc-client.js +238 -0
- package/dist/src/backend/domains/session/acp/codex-app-server-adapter/codex-rpc-client.js.map +1 -0
- package/dist/src/backend/domains/session/acp/codex-app-server-adapter/codex-rpc-client.test.d.ts +2 -0
- package/dist/src/backend/domains/session/acp/codex-app-server-adapter/codex-rpc-client.test.d.ts.map +1 -0
- package/dist/src/backend/domains/session/acp/codex-app-server-adapter/codex-rpc-client.test.js +140 -0
- package/dist/src/backend/domains/session/acp/codex-app-server-adapter/codex-rpc-client.test.js.map +1 -0
- package/dist/src/backend/domains/session/acp/codex-app-server-adapter/codex-schema-snapshot.test.d.ts +2 -0
- package/dist/src/backend/domains/session/acp/codex-app-server-adapter/codex-schema-snapshot.test.d.ts.map +1 -0
- package/dist/src/backend/domains/session/acp/codex-app-server-adapter/codex-schema-snapshot.test.js +35 -0
- package/dist/src/backend/domains/session/acp/codex-app-server-adapter/codex-schema-snapshot.test.js.map +1 -0
- package/dist/src/backend/domains/session/acp/codex-app-server-adapter/codex-zod.d.ts +334 -0
- package/dist/src/backend/domains/session/acp/codex-app-server-adapter/codex-zod.d.ts.map +1 -0
- package/dist/src/backend/domains/session/acp/codex-app-server-adapter/codex-zod.js +324 -0
- package/dist/src/backend/domains/session/acp/codex-app-server-adapter/codex-zod.js.map +1 -0
- package/dist/src/backend/domains/session/acp/codex-app-server-adapter/codex-zod.test.d.ts +2 -0
- package/dist/src/backend/domains/session/acp/codex-app-server-adapter/codex-zod.test.d.ts.map +1 -0
- package/dist/src/backend/domains/session/acp/codex-app-server-adapter/codex-zod.test.js +162 -0
- package/dist/src/backend/domains/session/acp/codex-app-server-adapter/codex-zod.test.js.map +1 -0
- package/dist/src/backend/domains/session/acp/codex-app-server-adapter/index.d.ts +2 -0
- package/dist/src/backend/domains/session/acp/codex-app-server-adapter/index.d.ts.map +1 -0
- package/dist/src/backend/domains/session/acp/codex-app-server-adapter/index.js +2 -0
- package/dist/src/backend/domains/session/acp/codex-app-server-adapter/index.js.map +1 -0
- package/dist/src/backend/domains/session/acp/codex-cli-import-resolution.integration.test.d.ts +2 -0
- package/dist/src/backend/domains/session/acp/codex-cli-import-resolution.integration.test.d.ts.map +1 -0
- package/dist/src/backend/domains/session/acp/codex-cli-import-resolution.integration.test.js +64 -0
- package/dist/src/backend/domains/session/acp/codex-cli-import-resolution.integration.test.js.map +1 -0
- package/dist/src/backend/domains/session/acp/index.d.ts +1 -0
- package/dist/src/backend/domains/session/acp/index.d.ts.map +1 -1
- package/dist/src/backend/domains/session/acp/index.js +1 -0
- package/dist/src/backend/domains/session/acp/index.js.map +1 -1
- package/dist/src/backend/domains/session/chat/chat-message-handlers/handlers/load-session.handler.d.ts +2 -2
- package/dist/src/backend/domains/session/chat/chat-message-handlers/handlers/load-session.handler.d.ts.map +1 -1
- package/dist/src/backend/domains/session/chat/chat-message-handlers/handlers/load-session.handler.js +32 -1
- package/dist/src/backend/domains/session/chat/chat-message-handlers/handlers/load-session.handler.js.map +1 -1
- package/dist/src/backend/domains/session/chat/chat-message-handlers/handlers/load-session.handler.test.js +79 -9
- package/dist/src/backend/domains/session/chat/chat-message-handlers/handlers/load-session.handler.test.js.map +1 -1
- package/dist/src/backend/domains/session/chat/chat-message-handlers/handlers/permission-response.handler.d.ts.map +1 -1
- package/dist/src/backend/domains/session/chat/chat-message-handlers/handlers/permission-response.handler.js +3 -2
- package/dist/src/backend/domains/session/chat/chat-message-handlers/handlers/permission-response.handler.js.map +1 -1
- package/dist/src/backend/domains/session/chat/chat-message-handlers/handlers/stop.handler.d.ts +2 -2
- package/dist/src/backend/domains/session/chat/chat-message-handlers/handlers/stop.handler.d.ts.map +1 -1
- package/dist/src/backend/domains/session/chat/chat-message-handlers/handlers/stop.handler.js +2 -1
- package/dist/src/backend/domains/session/chat/chat-message-handlers/handlers/stop.handler.js.map +1 -1
- package/dist/src/backend/domains/session/chat/chat-message-handlers/handlers/stop.handler.test.js +7 -1
- package/dist/src/backend/domains/session/chat/chat-message-handlers/handlers/stop.handler.test.js.map +1 -1
- package/dist/src/backend/domains/session/chat/chat-message-handlers/registry.js +2 -2
- package/dist/src/backend/domains/session/chat/chat-message-handlers/registry.js.map +1 -1
- package/dist/src/backend/domains/session/chat/chat-message-handlers.service.d.ts +1 -1
- package/dist/src/backend/domains/session/chat/chat-message-handlers.service.d.ts.map +1 -1
- package/dist/src/backend/domains/session/chat/chat-message-handlers.service.js +32 -21
- package/dist/src/backend/domains/session/chat/chat-message-handlers.service.js.map +1 -1
- package/dist/src/backend/domains/session/chat/chat-message-handlers.service.test.js +52 -0
- package/dist/src/backend/domains/session/chat/chat-message-handlers.service.test.js.map +1 -1
- package/dist/src/backend/domains/session/data/claude-session-history-loader.service.d.ts.map +1 -1
- package/dist/src/backend/domains/session/data/claude-session-history-loader.service.js +17 -29
- package/dist/src/backend/domains/session/data/claude-session-history-loader.service.js.map +1 -1
- package/dist/src/backend/domains/session/data/codex-session-history-loader.service.d.ts +1 -0
- package/dist/src/backend/domains/session/data/codex-session-history-loader.service.d.ts.map +1 -1
- package/dist/src/backend/domains/session/data/codex-session-history-loader.service.js +121 -79
- package/dist/src/backend/domains/session/data/codex-session-history-loader.service.js.map +1 -1
- package/dist/src/backend/domains/session/data/codex-session-history-loader.service.test.js +84 -1
- package/dist/src/backend/domains/session/data/codex-session-history-loader.service.test.js.map +1 -1
- package/dist/src/backend/domains/session/data/session-data.service.d.ts +6 -9
- package/dist/src/backend/domains/session/data/session-data.service.d.ts.map +1 -1
- package/dist/src/backend/domains/session/data/session-data.service.js +3 -0
- package/dist/src/backend/domains/session/data/session-data.service.js.map +1 -1
- package/dist/src/backend/domains/session/data/session-history-jsonl-reader.d.ts +6 -0
- package/dist/src/backend/domains/session/data/session-history-jsonl-reader.d.ts.map +1 -0
- package/dist/src/backend/domains/session/data/session-history-jsonl-reader.js +25 -0
- package/dist/src/backend/domains/session/data/session-history-jsonl-reader.js.map +1 -0
- package/dist/src/backend/domains/session/data/session-provider-resolver.service.test.js +0 -2
- package/dist/src/backend/domains/session/data/session-provider-resolver.service.test.js.map +1 -1
- package/dist/src/backend/domains/session/index.d.ts +1 -1
- package/dist/src/backend/domains/session/index.d.ts.map +1 -1
- package/dist/src/backend/domains/session/index.js +1 -1
- package/dist/src/backend/domains/session/index.js.map +1 -1
- package/dist/src/backend/domains/session/lifecycle/session.repository.d.ts +2 -2
- package/dist/src/backend/domains/session/lifecycle/session.repository.d.ts.map +1 -1
- package/dist/src/backend/domains/session/lifecycle/session.repository.js.map +1 -1
- package/dist/src/backend/domains/session/lifecycle/session.service.d.ts +29 -3
- package/dist/src/backend/domains/session/lifecycle/session.service.d.ts.map +1 -1
- package/dist/src/backend/domains/session/lifecycle/session.service.js +384 -56
- package/dist/src/backend/domains/session/lifecycle/session.service.js.map +1 -1
- package/dist/src/backend/domains/session/lifecycle/session.service.test.js +531 -2
- package/dist/src/backend/domains/session/lifecycle/session.service.test.js.map +1 -1
- package/dist/src/backend/domains/session/session-domain.service.d.ts +6 -0
- package/dist/src/backend/domains/session/session-domain.service.d.ts.map +1 -1
- package/dist/src/backend/domains/session/session-domain.service.js +26 -1
- package/dist/src/backend/domains/session/session-domain.service.js.map +1 -1
- package/dist/src/backend/domains/session/session-domain.service.test.js +21 -0
- package/dist/src/backend/domains/session/session-domain.service.test.js.map +1 -1
- package/dist/src/backend/domains/session/store/session-replay-builder.d.ts.map +1 -1
- package/dist/src/backend/domains/session/store/session-replay-builder.js +2 -1
- package/dist/src/backend/domains/session/store/session-replay-builder.js.map +1 -1
- package/dist/src/backend/domains/session/store/session-replay-builder.test.js +26 -0
- package/dist/src/backend/domains/session/store/session-replay-builder.test.js.map +1 -1
- package/dist/src/backend/domains/session/store/session-store.types.d.ts +1 -1
- package/dist/src/backend/domains/session/store/session-store.types.d.ts.map +1 -1
- package/dist/src/backend/domains/session/store/session-transcript.d.ts +1 -0
- package/dist/src/backend/domains/session/store/session-transcript.d.ts.map +1 -1
- package/dist/src/backend/domains/session/store/session-transcript.js +8 -0
- package/dist/src/backend/domains/session/store/session-transcript.js.map +1 -1
- package/dist/src/backend/domains/workspace/bridges.d.ts +1 -0
- package/dist/src/backend/domains/workspace/bridges.d.ts.map +1 -1
- package/dist/src/backend/domains/workspace/index.d.ts +1 -0
- package/dist/src/backend/domains/workspace/index.d.ts.map +1 -1
- package/dist/src/backend/domains/workspace/index.js +1 -0
- package/dist/src/backend/domains/workspace/index.js.map +1 -1
- package/dist/src/backend/domains/workspace/lifecycle/creation.service.test.js +0 -2
- package/dist/src/backend/domains/workspace/lifecycle/creation.service.test.js.map +1 -1
- package/dist/src/backend/domains/workspace/lifecycle/data.service.d.ts +10 -24
- package/dist/src/backend/domains/workspace/lifecycle/data.service.d.ts.map +1 -1
- package/dist/src/backend/domains/workspace/query/project-management.service.d.ts +2 -6
- package/dist/src/backend/domains/workspace/query/project-management.service.d.ts.map +1 -1
- package/dist/src/backend/domains/workspace/query/workspace-query.service.d.ts +82 -6
- package/dist/src/backend/domains/workspace/query/workspace-query.service.d.ts.map +1 -1
- package/dist/src/backend/domains/workspace/query/workspace-query.service.js +15 -15
- package/dist/src/backend/domains/workspace/query/workspace-query.service.js.map +1 -1
- package/dist/src/backend/domains/workspace/query/workspace-query.service.test.d.ts +2 -0
- package/dist/src/backend/domains/workspace/query/workspace-query.service.test.d.ts.map +1 -0
- package/dist/src/backend/domains/workspace/query/workspace-query.service.test.js +165 -0
- package/dist/src/backend/domains/workspace/query/workspace-query.service.test.js.map +1 -0
- package/dist/src/backend/domains/workspace/state/pending-request-type.d.ts +6 -0
- package/dist/src/backend/domains/workspace/state/pending-request-type.d.ts.map +1 -0
- package/dist/src/backend/domains/workspace/state/pending-request-type.js +27 -0
- package/dist/src/backend/domains/workspace/state/pending-request-type.js.map +1 -0
- package/dist/src/backend/domains/workspace/state/pending-request-type.test.d.ts +2 -0
- package/dist/src/backend/domains/workspace/state/pending-request-type.test.d.ts.map +1 -0
- package/dist/src/backend/domains/workspace/state/pending-request-type.test.js +44 -0
- package/dist/src/backend/domains/workspace/state/pending-request-type.test.js.map +1 -0
- package/dist/src/backend/domains/workspace/worktree/worktree-init.test.js +9 -2
- package/dist/src/backend/domains/workspace/worktree/worktree-init.test.js.map +1 -1
- package/dist/src/backend/interceptors/pr-detection.interceptor.d.ts.map +1 -1
- package/dist/src/backend/interceptors/pr-detection.interceptor.js +57 -8
- package/dist/src/backend/interceptors/pr-detection.interceptor.js.map +1 -1
- package/dist/src/backend/interceptors/pr-detection.interceptor.test.d.ts +2 -0
- package/dist/src/backend/interceptors/pr-detection.interceptor.test.d.ts.map +1 -0
- package/dist/src/backend/interceptors/pr-detection.interceptor.test.js +120 -0
- package/dist/src/backend/interceptors/pr-detection.interceptor.test.js.map +1 -0
- package/dist/src/backend/lib/session-model.js +1 -1
- package/dist/src/backend/lib/session-model.js.map +1 -1
- package/dist/src/backend/lib/session-model.test.js +1 -1
- package/dist/src/backend/lib/session-model.test.js.map +1 -1
- package/dist/src/backend/orchestration/cli-health.service.d.ts +1 -0
- package/dist/src/backend/orchestration/cli-health.service.d.ts.map +1 -1
- package/dist/src/backend/orchestration/cli-health.service.js +16 -1
- package/dist/src/backend/orchestration/cli-health.service.js.map +1 -1
- package/dist/src/backend/orchestration/cli-health.service.test.js +23 -0
- package/dist/src/backend/orchestration/cli-health.service.test.js.map +1 -1
- package/dist/src/backend/orchestration/data-backup.service.d.ts.map +1 -1
- package/dist/src/backend/orchestration/data-backup.service.js +0 -4
- package/dist/src/backend/orchestration/data-backup.service.js.map +1 -1
- package/dist/src/backend/orchestration/data-backup.service.test.js +0 -4
- package/dist/src/backend/orchestration/data-backup.service.test.js.map +1 -1
- package/dist/src/backend/orchestration/domain-bridges.orchestrator.d.ts.map +1 -1
- package/dist/src/backend/orchestration/domain-bridges.orchestrator.js +3 -19
- package/dist/src/backend/orchestration/domain-bridges.orchestrator.js.map +1 -1
- package/dist/src/backend/orchestration/domain-bridges.orchestrator.test.js +13 -27
- package/dist/src/backend/orchestration/domain-bridges.orchestrator.test.js.map +1 -1
- package/dist/src/backend/orchestration/snapshot-reconciliation.orchestrator.d.ts +1 -0
- package/dist/src/backend/orchestration/snapshot-reconciliation.orchestrator.d.ts.map +1 -1
- package/dist/src/backend/orchestration/snapshot-reconciliation.orchestrator.js +1 -15
- package/dist/src/backend/orchestration/snapshot-reconciliation.orchestrator.js.map +1 -1
- package/dist/src/backend/orchestration/snapshot-reconciliation.orchestrator.test.js +97 -0
- package/dist/src/backend/orchestration/snapshot-reconciliation.orchestrator.test.js.map +1 -1
- package/dist/src/backend/orchestration/workspace-archive.orchestrator.d.ts +1 -3
- package/dist/src/backend/orchestration/workspace-archive.orchestrator.d.ts.map +1 -1
- package/dist/src/backend/orchestration/workspace-init.orchestrator.d.ts.map +1 -1
- package/dist/src/backend/orchestration/workspace-init.orchestrator.js +4 -1
- package/dist/src/backend/orchestration/workspace-init.orchestrator.js.map +1 -1
- package/dist/src/backend/orchestration/workspace-init.orchestrator.test.js +1 -0
- package/dist/src/backend/orchestration/workspace-init.orchestrator.test.js.map +1 -1
- package/dist/src/backend/prompts/ratchet-dispatch.d.ts +8 -1
- package/dist/src/backend/prompts/ratchet-dispatch.d.ts.map +1 -1
- package/dist/src/backend/prompts/ratchet-dispatch.js +20 -3
- package/dist/src/backend/prompts/ratchet-dispatch.js.map +1 -1
- package/dist/src/backend/resource_accessors/agent-session.accessor.d.ts +1 -0
- package/dist/src/backend/resource_accessors/agent-session.accessor.d.ts.map +1 -1
- package/dist/src/backend/resource_accessors/agent-session.accessor.js +10 -0
- package/dist/src/backend/resource_accessors/agent-session.accessor.js.map +1 -1
- package/dist/src/backend/resource_accessors/agent-session.accessor.test.d.ts +2 -0
- package/dist/src/backend/resource_accessors/agent-session.accessor.test.d.ts.map +1 -0
- package/dist/src/backend/resource_accessors/agent-session.accessor.test.js +148 -0
- package/dist/src/backend/resource_accessors/agent-session.accessor.test.js.map +1 -0
- package/dist/src/backend/resource_accessors/workspace.accessor.d.ts +0 -15
- package/dist/src/backend/resource_accessors/workspace.accessor.d.ts.map +1 -1
- package/dist/src/backend/resource_accessors/workspace.accessor.js +0 -33
- package/dist/src/backend/resource_accessors/workspace.accessor.js.map +1 -1
- package/dist/src/backend/schemas/tool-inputs.schema.d.ts +2 -0
- package/dist/src/backend/schemas/tool-inputs.schema.d.ts.map +1 -1
- package/dist/src/backend/schemas/tool-inputs.schema.js +1 -0
- package/dist/src/backend/schemas/tool-inputs.schema.js.map +1 -1
- package/dist/src/backend/services/constants.d.ts +1 -5
- package/dist/src/backend/services/constants.d.ts.map +1 -1
- package/dist/src/backend/services/constants.js +1 -5
- package/dist/src/backend/services/constants.js.map +1 -1
- package/dist/src/backend/services/workspace-snapshot-store.service.d.ts +2 -2
- package/dist/src/backend/services/workspace-snapshot-store.service.d.ts.map +1 -1
- package/dist/src/backend/trpc/admin-active-processes.d.ts +55 -0
- package/dist/src/backend/trpc/admin-active-processes.d.ts.map +1 -0
- package/dist/src/backend/trpc/admin-active-processes.js +103 -0
- package/dist/src/backend/trpc/admin-active-processes.js.map +1 -0
- package/dist/src/backend/trpc/admin-active-processes.test.d.ts +2 -0
- package/dist/src/backend/trpc/admin-active-processes.test.d.ts.map +1 -0
- package/dist/src/backend/trpc/admin-active-processes.test.js +105 -0
- package/dist/src/backend/trpc/admin-active-processes.test.js.map +1 -0
- package/dist/src/backend/trpc/admin.trpc.d.ts +5 -32
- package/dist/src/backend/trpc/admin.trpc.d.ts.map +1 -1
- package/dist/src/backend/trpc/admin.trpc.js +29 -128
- package/dist/src/backend/trpc/admin.trpc.js.map +1 -1
- package/dist/src/backend/trpc/decision-log.trpc.d.ts +3 -3
- package/dist/src/backend/trpc/index.d.ts +137 -104
- package/dist/src/backend/trpc/index.d.ts.map +1 -1
- package/dist/src/backend/trpc/log-file-reader.d.ts +26 -0
- package/dist/src/backend/trpc/log-file-reader.d.ts.map +1 -0
- package/dist/src/backend/trpc/log-file-reader.js +160 -0
- package/dist/src/backend/trpc/log-file-reader.js.map +1 -0
- package/dist/src/backend/trpc/log-file-reader.test.d.ts +2 -0
- package/dist/src/backend/trpc/log-file-reader.test.d.ts.map +1 -0
- package/dist/src/backend/trpc/log-file-reader.test.js +162 -0
- package/dist/src/backend/trpc/log-file-reader.test.js.map +1 -0
- package/dist/src/backend/trpc/project.trpc.d.ts +2 -6
- package/dist/src/backend/trpc/project.trpc.d.ts.map +1 -1
- package/dist/src/backend/trpc/session.trpc.d.ts +24 -31
- package/dist/src/backend/trpc/session.trpc.d.ts.map +1 -1
- package/dist/src/backend/trpc/session.trpc.js +5 -1
- package/dist/src/backend/trpc/session.trpc.js.map +1 -1
- package/dist/src/backend/trpc/workspace/init.trpc.d.ts +4 -6
- package/dist/src/backend/trpc/workspace/init.trpc.d.ts.map +1 -1
- package/dist/src/backend/trpc/workspace/workspace-helpers.d.ts +5 -9
- package/dist/src/backend/trpc/workspace/workspace-helpers.d.ts.map +1 -1
- package/dist/src/backend/trpc/workspace.trpc.d.ts +103 -39
- package/dist/src/backend/trpc/workspace.trpc.d.ts.map +1 -1
- package/dist/src/backend/trpc/workspace.trpc.js +8 -0
- package/dist/src/backend/trpc/workspace.trpc.js.map +1 -1
- package/dist/src/cli/index.js +7 -2
- package/dist/src/cli/index.js.map +1 -1
- package/dist/src/shared/acp-protocol/protocol/interaction.d.ts +2 -0
- package/dist/src/shared/acp-protocol/protocol/interaction.d.ts.map +1 -1
- package/dist/src/shared/acp-protocol/protocol/websocket.d.ts +1 -0
- package/dist/src/shared/acp-protocol/protocol/websocket.d.ts.map +1 -1
- package/dist/src/shared/acp-protocol/protocol/websocket.js.map +1 -1
- package/dist/src/shared/pending-request-types.d.ts +4 -0
- package/dist/src/shared/pending-request-types.d.ts.map +1 -1
- package/dist/src/shared/pending-request-types.js +10 -0
- package/dist/src/shared/pending-request-types.js.map +1 -1
- package/dist/src/shared/schemas/export-data.schema.d.ts +0 -18
- package/dist/src/shared/schemas/export-data.schema.d.ts.map +1 -1
- package/dist/src/shared/schemas/export-data.schema.js +0 -2
- package/dist/src/shared/schemas/export-data.schema.js.map +1 -1
- package/dist/src/shared/websocket/chat-message.schema.d.ts +1 -0
- package/dist/src/shared/websocket/chat-message.schema.d.ts.map +1 -1
- package/dist/src/shared/websocket/chat-message.schema.js +1 -0
- package/dist/src/shared/websocket/chat-message.schema.js.map +1 -1
- package/dist/tsconfig.backend.tsbuildinfo +1 -1
- package/package.json +4 -2
- package/prisma/migrations/20260214232046_remove_ratchet_last_modified/migration.sql +68 -0
- package/prisma/schema.prisma +0 -2
- package/prompts/quick-actions/screenshots.md +12 -6
- package/prompts/ratchet/dispatch.md +4 -0
- package/dist/client/assets/channel-BA4I5_zc.js +0 -1
- package/dist/client/assets/classDiagram-2ON5EDUG-DHiY0J8v.js +0 -1
- package/dist/client/assets/classDiagram-v2-WZHVMYZB-DHiY0J8v.js +0 -1
- package/dist/client/assets/clone-C7QOkM1k.js +0 -1
- package/dist/client/assets/index-D_8rf7lP.css +0 -1
- package/dist/client/assets/stateDiagram-v2-4FDKWEC3-BGGNxhyR.js +0 -1
- package/dist/src/backend/domains/github/pr-review-fixer.service.d.ts +0 -63
- package/dist/src/backend/domains/github/pr-review-fixer.service.d.ts.map +0 -1
- package/dist/src/backend/domains/github/pr-review-fixer.service.js +0 -141
- package/dist/src/backend/domains/github/pr-review-fixer.service.js.map +0 -1
- package/dist/src/backend/domains/github/pr-review-fixer.service.test.d.ts +0 -2
- package/dist/src/backend/domains/github/pr-review-fixer.service.test.d.ts.map +0 -1
- package/dist/src/backend/domains/github/pr-review-fixer.service.test.js +0 -28
- package/dist/src/backend/domains/github/pr-review-fixer.service.test.js.map +0 -1
- package/dist/src/backend/domains/github/pr-review-monitor.service.d.ts +0 -19
- package/dist/src/backend/domains/github/pr-review-monitor.service.d.ts.map +0 -1
- package/dist/src/backend/domains/github/pr-review-monitor.service.js +0 -215
- package/dist/src/backend/domains/github/pr-review-monitor.service.js.map +0 -1
- package/dist/src/backend/domains/ratchet/ci-fixer.service.d.ts +0 -47
- package/dist/src/backend/domains/ratchet/ci-fixer.service.d.ts.map +0 -1
- package/dist/src/backend/domains/ratchet/ci-fixer.service.js +0 -95
- package/dist/src/backend/domains/ratchet/ci-fixer.service.js.map +0 -1
- package/dist/src/backend/domains/ratchet/ci-fixer.service.test.d.ts +0 -2
- package/dist/src/backend/domains/ratchet/ci-fixer.service.test.d.ts.map +0 -1
- package/dist/src/backend/domains/ratchet/ci-fixer.service.test.js +0 -100
- package/dist/src/backend/domains/ratchet/ci-fixer.service.test.js.map +0 -1
- package/dist/src/backend/domains/ratchet/ci-monitor.service.d.ts +0 -36
- package/dist/src/backend/domains/ratchet/ci-monitor.service.d.ts.map +0 -1
- package/dist/src/backend/domains/ratchet/ci-monitor.service.js +0 -278
- package/dist/src/backend/domains/ratchet/ci-monitor.service.js.map +0 -1
- package/dist/src/backend/services/fixer-workflow.service.d.ts +0 -83
- package/dist/src/backend/services/fixer-workflow.service.d.ts.map +0 -1
- package/dist/src/backend/services/fixer-workflow.service.js +0 -77
- package/dist/src/backend/services/fixer-workflow.service.js.map +0 -1
- package/dist/src/backend/services/fixer-workflow.service.test.d.ts +0 -2
- package/dist/src/backend/services/fixer-workflow.service.test.d.ts.map +0 -1
- package/dist/src/backend/services/fixer-workflow.service.test.js +0 -55
- package/dist/src/backend/services/fixer-workflow.service.test.js.map +0 -1
|
@@ -0,0 +1,2496 @@
|
|
|
1
|
+
import { Readable, Writable } from 'node:stream';
|
|
2
|
+
import { AgentSideConnection, ndJsonStream, PROTOCOL_VERSION, RequestError, } from '@agentclientprotocol/sdk';
|
|
3
|
+
import { CodexRequestError, CodexRpcClient } from './codex-rpc-client.js';
|
|
4
|
+
import { collaborationModeListResponseSchema, configRequirementsReadResponseSchema, knownCodexNotificationSchema, knownCodexServerRequestSchema, modelListResponseSchema, threadReadResponseSchema, threadResumeResponseSchema, threadStartResponseSchema, turnStartResponseSchema, } from './codex-zod.js';
|
|
5
|
+
const PENDING_TURN_ID = '__pending_turn__';
|
|
6
|
+
const MAX_CLOSE_WATCHER_ATTACH_RETRIES = 50;
|
|
7
|
+
const DEFAULT_APPROVAL_POLICIES = ['on-failure', 'on-request', 'never'];
|
|
8
|
+
const DEFAULT_SANDBOX_MODES = ['read-only', 'workspace-write', 'danger-full-access'];
|
|
9
|
+
const SHAPE_DRIFT_DETAILS_LIMIT = 700;
|
|
10
|
+
function toTurnItemKey(turnId, itemId) {
|
|
11
|
+
return `${turnId}:${itemId}`;
|
|
12
|
+
}
|
|
13
|
+
function toShapeDriftDetails(value) {
|
|
14
|
+
try {
|
|
15
|
+
const text = JSON.stringify(value);
|
|
16
|
+
if (typeof text !== 'string') {
|
|
17
|
+
return '[unserializable]';
|
|
18
|
+
}
|
|
19
|
+
return text.length > SHAPE_DRIFT_DETAILS_LIMIT
|
|
20
|
+
? `${text.slice(0, SHAPE_DRIFT_DETAILS_LIMIT)}...`
|
|
21
|
+
: text;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return '[unserializable]';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function isNonEmptyString(value) {
|
|
28
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
29
|
+
}
|
|
30
|
+
function dedupeStrings(values) {
|
|
31
|
+
return Array.from(new Set(values));
|
|
32
|
+
}
|
|
33
|
+
function humanizeToken(value) {
|
|
34
|
+
return value
|
|
35
|
+
.split(/[-_]/)
|
|
36
|
+
.filter((part) => part.length > 0)
|
|
37
|
+
.map((part) => part.slice(0, 1).toUpperCase() + part.slice(1))
|
|
38
|
+
.join(' ');
|
|
39
|
+
}
|
|
40
|
+
function sanitizeModeName(mode) {
|
|
41
|
+
return mode
|
|
42
|
+
.split('_')
|
|
43
|
+
.map((part) => part.slice(0, 1).toUpperCase() + part.slice(1))
|
|
44
|
+
.join(' ');
|
|
45
|
+
}
|
|
46
|
+
function formatApprovalPolicyLabel(policy) {
|
|
47
|
+
if (policy === 'on-request') {
|
|
48
|
+
return 'On Request';
|
|
49
|
+
}
|
|
50
|
+
if (policy === 'on-failure') {
|
|
51
|
+
return 'On Failure';
|
|
52
|
+
}
|
|
53
|
+
if (policy === 'never') {
|
|
54
|
+
return 'Never Ask';
|
|
55
|
+
}
|
|
56
|
+
if (policy === 'untrusted') {
|
|
57
|
+
return 'Untrusted';
|
|
58
|
+
}
|
|
59
|
+
return humanizeToken(policy);
|
|
60
|
+
}
|
|
61
|
+
function formatSandboxModeLabel(mode) {
|
|
62
|
+
if (mode === 'workspace-write') {
|
|
63
|
+
return 'Workspace Write';
|
|
64
|
+
}
|
|
65
|
+
if (mode === 'read-only') {
|
|
66
|
+
return 'Read-only';
|
|
67
|
+
}
|
|
68
|
+
if (mode === 'danger-full-access') {
|
|
69
|
+
return 'Full Access';
|
|
70
|
+
}
|
|
71
|
+
return humanizeToken(mode);
|
|
72
|
+
}
|
|
73
|
+
function formatExecutionPresetName(approvalPolicy, sandboxMode) {
|
|
74
|
+
if (approvalPolicy === 'never' && sandboxMode === 'danger-full-access') {
|
|
75
|
+
return 'YOLO (Full Access)';
|
|
76
|
+
}
|
|
77
|
+
return `${formatApprovalPolicyLabel(approvalPolicy)} (${formatSandboxModeLabel(sandboxMode)})`;
|
|
78
|
+
}
|
|
79
|
+
function createWorkspaceWriteSandboxPolicy(cwd) {
|
|
80
|
+
return {
|
|
81
|
+
type: 'workspaceWrite',
|
|
82
|
+
writableRoots: [cwd],
|
|
83
|
+
readOnlyAccess: { type: 'fullAccess' },
|
|
84
|
+
networkAccess: true,
|
|
85
|
+
excludeTmpdirEnvVar: false,
|
|
86
|
+
excludeSlashTmp: false,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function createDangerFullAccessSandboxPolicy() {
|
|
90
|
+
return {
|
|
91
|
+
type: 'dangerFullAccess',
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function parseSandboxModeFromPolicy(policy) {
|
|
95
|
+
if (!isRecord(policy)) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
const policyType = asString(policy.type);
|
|
99
|
+
if (policyType === 'workspaceWrite') {
|
|
100
|
+
return 'workspace-write';
|
|
101
|
+
}
|
|
102
|
+
if (policyType === 'readOnly') {
|
|
103
|
+
return 'read-only';
|
|
104
|
+
}
|
|
105
|
+
if (policyType === 'dangerFullAccess') {
|
|
106
|
+
return 'danger-full-access';
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
function createSandboxPolicyFromMode(mode, cwd) {
|
|
111
|
+
if (mode === 'danger-full-access') {
|
|
112
|
+
return createDangerFullAccessSandboxPolicy();
|
|
113
|
+
}
|
|
114
|
+
if (mode === 'read-only') {
|
|
115
|
+
return {
|
|
116
|
+
type: 'readOnly',
|
|
117
|
+
access: { type: 'fullAccess' },
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
return createWorkspaceWriteSandboxPolicy(cwd);
|
|
121
|
+
}
|
|
122
|
+
function toSessionId(threadId) {
|
|
123
|
+
return `sess_${threadId}`;
|
|
124
|
+
}
|
|
125
|
+
function toThreadId(sessionId) {
|
|
126
|
+
return sessionId.startsWith('sess_') ? sessionId.slice('sess_'.length) : sessionId;
|
|
127
|
+
}
|
|
128
|
+
function extractToolCallIdFromUnknown(value) {
|
|
129
|
+
if (!isRecord(value)) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
return asString(value.callId) ?? asString(value.call_id) ?? null;
|
|
133
|
+
}
|
|
134
|
+
function resolveToolCallId(params) {
|
|
135
|
+
return extractToolCallIdFromUnknown(params.source) ?? params.itemId;
|
|
136
|
+
}
|
|
137
|
+
function isPlanLikeMode(mode) {
|
|
138
|
+
return /plan/i.test(mode);
|
|
139
|
+
}
|
|
140
|
+
const PLAN_EXIT_MODE_PREFERENCE = ['default', 'code', 'acceptEdits', 'ask'];
|
|
141
|
+
const PLAN_TEXT_MAX_DEPTH = 8;
|
|
142
|
+
const PLAN_TEXT_PREFERRED_KEYS = [
|
|
143
|
+
'plan',
|
|
144
|
+
'text',
|
|
145
|
+
'content',
|
|
146
|
+
'markdown',
|
|
147
|
+
'value',
|
|
148
|
+
'message',
|
|
149
|
+
];
|
|
150
|
+
const REASONING_TEXT_MAX_DEPTH = 8;
|
|
151
|
+
const REASONING_TEXT_PREFERRED_KEYS = [
|
|
152
|
+
'summary',
|
|
153
|
+
'summaryText',
|
|
154
|
+
'text',
|
|
155
|
+
'delta',
|
|
156
|
+
'message',
|
|
157
|
+
'content',
|
|
158
|
+
'reasoning',
|
|
159
|
+
];
|
|
160
|
+
function extractFirstPlanText(values, depth) {
|
|
161
|
+
for (const entry of values) {
|
|
162
|
+
const extracted = extractPlanTextLocal(entry, depth + 1);
|
|
163
|
+
if (extracted) {
|
|
164
|
+
return extracted;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
function extractPlanTextFromRecord(value, depth) {
|
|
170
|
+
const preferred = extractFirstPlanText(PLAN_TEXT_PREFERRED_KEYS.map((key) => value[key]), depth);
|
|
171
|
+
if (preferred) {
|
|
172
|
+
return preferred;
|
|
173
|
+
}
|
|
174
|
+
return extractFirstPlanText(Object.values(value), depth);
|
|
175
|
+
}
|
|
176
|
+
function extractPlanTextLocal(value, depth = 0) {
|
|
177
|
+
if (depth > PLAN_TEXT_MAX_DEPTH) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
if (typeof value === 'string') {
|
|
181
|
+
const trimmed = value.trim();
|
|
182
|
+
return trimmed.length > 0 ? value : null;
|
|
183
|
+
}
|
|
184
|
+
if (Array.isArray(value)) {
|
|
185
|
+
return extractFirstPlanText(value, depth);
|
|
186
|
+
}
|
|
187
|
+
if (!isRecord(value)) {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
return extractPlanTextFromRecord(value, depth);
|
|
191
|
+
}
|
|
192
|
+
function collectReasoningText(values, value, depth = 0) {
|
|
193
|
+
if (depth > REASONING_TEXT_MAX_DEPTH) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (typeof value === 'string') {
|
|
197
|
+
if (value.trim().length > 0) {
|
|
198
|
+
values.push(value);
|
|
199
|
+
}
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (Array.isArray(value)) {
|
|
203
|
+
for (const entry of value) {
|
|
204
|
+
collectReasoningText(values, entry, depth + 1);
|
|
205
|
+
}
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (!isRecord(value)) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
for (const key of REASONING_TEXT_PREFERRED_KEYS) {
|
|
212
|
+
collectReasoningText(values, value[key], depth + 1);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function extractReasoningTextLocal(value) {
|
|
216
|
+
const collected = [];
|
|
217
|
+
collectReasoningText(collected, value);
|
|
218
|
+
if (collected.length === 0) {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
return dedupeStrings(collected).join('\n\n');
|
|
222
|
+
}
|
|
223
|
+
function toMcpEnvRecord(envVars) {
|
|
224
|
+
const env = {};
|
|
225
|
+
for (const entry of envVars) {
|
|
226
|
+
env[entry.name] = entry.value;
|
|
227
|
+
}
|
|
228
|
+
return env;
|
|
229
|
+
}
|
|
230
|
+
function toMcpHeadersRecord(headers) {
|
|
231
|
+
const result = {};
|
|
232
|
+
for (const header of headers) {
|
|
233
|
+
result[header.name] = header.value;
|
|
234
|
+
}
|
|
235
|
+
return result;
|
|
236
|
+
}
|
|
237
|
+
function sanitizeMcpServerName(name, index) {
|
|
238
|
+
const trimmed = name.trim();
|
|
239
|
+
return trimmed.length > 0 ? trimmed : `mcp_server_${index + 1}`;
|
|
240
|
+
}
|
|
241
|
+
function toCodexMcpServerConfig(server) {
|
|
242
|
+
if ('command' in server) {
|
|
243
|
+
const env = toMcpEnvRecord(server.env);
|
|
244
|
+
return {
|
|
245
|
+
enabled: true,
|
|
246
|
+
command: server.command,
|
|
247
|
+
args: [...server.args],
|
|
248
|
+
...(Object.keys(env).length > 0 ? { env } : {}),
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
const httpHeaders = toMcpHeadersRecord(server.headers);
|
|
252
|
+
return {
|
|
253
|
+
enabled: true,
|
|
254
|
+
url: server.url,
|
|
255
|
+
...(Object.keys(httpHeaders).length > 0 ? { http_headers: httpHeaders } : {}),
|
|
256
|
+
...(server.type === 'sse' ? { transport: 'sse' } : {}),
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
function toCodexMcpConfigMap(mcpServers) {
|
|
260
|
+
const mcpServersByName = {};
|
|
261
|
+
const usedNames = new Set();
|
|
262
|
+
for (const [index, server] of mcpServers.entries()) {
|
|
263
|
+
const baseName = sanitizeMcpServerName(server.name, index);
|
|
264
|
+
let nextName = baseName;
|
|
265
|
+
let suffix = 2;
|
|
266
|
+
while (usedNames.has(nextName)) {
|
|
267
|
+
nextName = `${baseName}_${suffix}`;
|
|
268
|
+
suffix += 1;
|
|
269
|
+
}
|
|
270
|
+
usedNames.add(nextName);
|
|
271
|
+
mcpServersByName[nextName] = toCodexMcpServerConfig(server);
|
|
272
|
+
}
|
|
273
|
+
return mcpServersByName;
|
|
274
|
+
}
|
|
275
|
+
function buildToolUserInputPermissionOptions(questions) {
|
|
276
|
+
if (questions.length !== 1) {
|
|
277
|
+
return [
|
|
278
|
+
{ optionId: 'allow_once', name: 'Submit', kind: 'allow_once' },
|
|
279
|
+
{ optionId: 'reject_once', name: 'Cancel', kind: 'reject_once' },
|
|
280
|
+
];
|
|
281
|
+
}
|
|
282
|
+
const firstQuestionWithOptions = questions.find((question) => Array.isArray(question.options) && question.options.length > 0);
|
|
283
|
+
if (!firstQuestionWithOptions?.options) {
|
|
284
|
+
return [
|
|
285
|
+
{ optionId: 'allow_once', name: 'Submit', kind: 'allow_once' },
|
|
286
|
+
{ optionId: 'reject_once', name: 'Cancel', kind: 'reject_once' },
|
|
287
|
+
];
|
|
288
|
+
}
|
|
289
|
+
const mappedOptions = firstQuestionWithOptions.options.slice(0, 6).map((option, index) => ({
|
|
290
|
+
optionId: `answer_${index}`,
|
|
291
|
+
name: option.label,
|
|
292
|
+
kind: 'allow_once',
|
|
293
|
+
}));
|
|
294
|
+
mappedOptions.push({ optionId: 'reject_once', name: 'Cancel', kind: 'reject_once' });
|
|
295
|
+
return mappedOptions;
|
|
296
|
+
}
|
|
297
|
+
function buildToolUserInputAnswers(params) {
|
|
298
|
+
if (params.selectedOptionId === null || params.selectedOptionId === 'reject_once') {
|
|
299
|
+
return {};
|
|
300
|
+
}
|
|
301
|
+
const parsedAnswers = parseToolUserInputAnswersFromPermissionMeta({
|
|
302
|
+
questions: params.questions,
|
|
303
|
+
permission: params.permission,
|
|
304
|
+
});
|
|
305
|
+
if (parsedAnswers) {
|
|
306
|
+
return parsedAnswers;
|
|
307
|
+
}
|
|
308
|
+
if (params.questions.length !== 1) {
|
|
309
|
+
throw new Error('Missing structured answers for multi-question requestUserInput');
|
|
310
|
+
}
|
|
311
|
+
const selectedIndex = params.selectedOptionId.startsWith('answer_')
|
|
312
|
+
? Number.parseInt(params.selectedOptionId.slice('answer_'.length), 10)
|
|
313
|
+
: Number.NaN;
|
|
314
|
+
const answers = {};
|
|
315
|
+
const [question] = params.questions;
|
|
316
|
+
if (!(question && Array.isArray(question.options)) || question.options.length === 0) {
|
|
317
|
+
return answers;
|
|
318
|
+
}
|
|
319
|
+
const selectedOption = Number.isNaN(selectedIndex)
|
|
320
|
+
? question.options[0]
|
|
321
|
+
: (question.options[selectedIndex] ?? question.options[0]);
|
|
322
|
+
if (!selectedOption) {
|
|
323
|
+
return answers;
|
|
324
|
+
}
|
|
325
|
+
answers[question.id] = { answers: [selectedOption.label] };
|
|
326
|
+
return answers;
|
|
327
|
+
}
|
|
328
|
+
function isRecord(value) {
|
|
329
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
330
|
+
}
|
|
331
|
+
function parseToolUserInputAnswersFromPermissionMeta(params) {
|
|
332
|
+
const meta = params.permission._meta;
|
|
333
|
+
if (!isRecord(meta)) {
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
const factoryFactoryMeta = isRecord(meta.factoryFactory) ? meta.factoryFactory : null;
|
|
337
|
+
const answersRaw = (factoryFactoryMeta?.toolUserInputAnswers ?? meta.toolUserInputAnswers);
|
|
338
|
+
if (!isRecord(answersRaw)) {
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
const knownQuestionIds = new Set(params.questions.map((question) => question.id));
|
|
342
|
+
const answers = {};
|
|
343
|
+
for (const [questionId, value] of Object.entries(answersRaw)) {
|
|
344
|
+
if (!knownQuestionIds.has(questionId)) {
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
const values = (Array.isArray(value) ? value : [value])
|
|
348
|
+
.filter((entry) => typeof entry === 'string')
|
|
349
|
+
.map((entry) => entry.trim())
|
|
350
|
+
.filter((entry) => entry.length > 0);
|
|
351
|
+
if (values.length === 0) {
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
answers[questionId] = { answers: values };
|
|
355
|
+
}
|
|
356
|
+
return answers;
|
|
357
|
+
}
|
|
358
|
+
function asString(value) {
|
|
359
|
+
return typeof value === 'string' ? value : null;
|
|
360
|
+
}
|
|
361
|
+
function trimOptionalQuotes(token) {
|
|
362
|
+
if (token.length >= 2 && token.startsWith('"') && token.endsWith('"')) {
|
|
363
|
+
return { text: token.slice(1, -1), quote: '"' };
|
|
364
|
+
}
|
|
365
|
+
if (token.length >= 2 && token.startsWith("'") && token.endsWith("'")) {
|
|
366
|
+
return { text: token.slice(1, -1), quote: "'" };
|
|
367
|
+
}
|
|
368
|
+
return { text: token, quote: null };
|
|
369
|
+
}
|
|
370
|
+
function unescapeCommandToken(token, quote) {
|
|
371
|
+
if (quote === "'") {
|
|
372
|
+
return token.replace(/\\'/g, "'").replace(/\\\\/g, '\\');
|
|
373
|
+
}
|
|
374
|
+
return token.replace(/\\(["'\\\s|&;])/g, '$1');
|
|
375
|
+
}
|
|
376
|
+
function sanitizeCommandToken(token) {
|
|
377
|
+
const trimmed = token.trim();
|
|
378
|
+
const { text, quote } = trimOptionalQuotes(trimmed);
|
|
379
|
+
return unescapeCommandToken(text, quote).trim();
|
|
380
|
+
}
|
|
381
|
+
function tokenizeCommand(command) {
|
|
382
|
+
return (command.match(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|\S+/g) ?? [])
|
|
383
|
+
.map((token) => sanitizeCommandToken(token))
|
|
384
|
+
.filter((token) => token.length > 0);
|
|
385
|
+
}
|
|
386
|
+
function commandName(token) {
|
|
387
|
+
const unix = token.split('/').at(-1) ?? token;
|
|
388
|
+
return (unix.split('\\').at(-1) ?? unix).toLowerCase();
|
|
389
|
+
}
|
|
390
|
+
function isChainSeparatorChar(char) {
|
|
391
|
+
return char === ';' || char === '\n' || char === '|' || char === '&';
|
|
392
|
+
}
|
|
393
|
+
function readChainSeparatorLength(command, index) {
|
|
394
|
+
const char = command[index];
|
|
395
|
+
if (!(char && isChainSeparatorChar(char))) {
|
|
396
|
+
return 0;
|
|
397
|
+
}
|
|
398
|
+
const next = command[index + 1];
|
|
399
|
+
if ((char === '|' || char === '&') && next === char) {
|
|
400
|
+
return 2;
|
|
401
|
+
}
|
|
402
|
+
return 1;
|
|
403
|
+
}
|
|
404
|
+
function isLikelyPathToken(token) {
|
|
405
|
+
if (!token || token.startsWith('-')) {
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
if (token.startsWith('./') ||
|
|
409
|
+
token.startsWith('../') ||
|
|
410
|
+
token.startsWith('/') ||
|
|
411
|
+
token.startsWith('~')) {
|
|
412
|
+
return true;
|
|
413
|
+
}
|
|
414
|
+
return (token.includes('/') ||
|
|
415
|
+
token.includes('\\') ||
|
|
416
|
+
token.endsWith('.') ||
|
|
417
|
+
/\.[a-z0-9]+$/i.test(token));
|
|
418
|
+
}
|
|
419
|
+
function normalizeLocationPath(pathToken, cwd) {
|
|
420
|
+
if (pathToken.startsWith('/')) {
|
|
421
|
+
return pathToken;
|
|
422
|
+
}
|
|
423
|
+
if (pathToken.startsWith('~')) {
|
|
424
|
+
return pathToken;
|
|
425
|
+
}
|
|
426
|
+
const base = cwd.endsWith('/') ? cwd.slice(0, -1) : cwd;
|
|
427
|
+
return `${base}/${pathToken}`;
|
|
428
|
+
}
|
|
429
|
+
const READ_COMMANDS = new Set(['cat', 'head', 'tail', 'less', 'more']);
|
|
430
|
+
const LIST_OR_FIND_COMMANDS = new Set(['ls', 'tree', 'find', 'fd']);
|
|
431
|
+
const GREP_LIKE_COMMANDS = new Set(['rg', 'ripgrep', 'grep', 'ag']);
|
|
432
|
+
const SHELL_META_COMMANDS = new Set(['cd', 'export', 'set', 'unset', 'alias', 'unalias', 'source']);
|
|
433
|
+
function pushCommandChainPart(state) {
|
|
434
|
+
const trimmed = state.current.trim();
|
|
435
|
+
if (trimmed.length > 0) {
|
|
436
|
+
state.parts.push(trimmed);
|
|
437
|
+
}
|
|
438
|
+
state.current = '';
|
|
439
|
+
}
|
|
440
|
+
function consumeEscapedCommandChar(state, char) {
|
|
441
|
+
if (!state.escaped) {
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
state.current += char;
|
|
445
|
+
state.escaped = false;
|
|
446
|
+
return true;
|
|
447
|
+
}
|
|
448
|
+
function consumeEscapeInitiator(state, char) {
|
|
449
|
+
if (!(char === '\\' && state.quote !== "'")) {
|
|
450
|
+
return false;
|
|
451
|
+
}
|
|
452
|
+
state.current += char;
|
|
453
|
+
state.escaped = true;
|
|
454
|
+
return true;
|
|
455
|
+
}
|
|
456
|
+
function hasOddTrailingBackslashes(value) {
|
|
457
|
+
let count = 0;
|
|
458
|
+
for (let index = value.length - 1; index >= 0; index -= 1) {
|
|
459
|
+
if (value[index] !== '\\') {
|
|
460
|
+
break;
|
|
461
|
+
}
|
|
462
|
+
count += 1;
|
|
463
|
+
}
|
|
464
|
+
return count % 2 === 1;
|
|
465
|
+
}
|
|
466
|
+
function consumeQuotedCommandChar(state, char) {
|
|
467
|
+
if (!state.quote) {
|
|
468
|
+
return false;
|
|
469
|
+
}
|
|
470
|
+
const escapedSingleQuote = state.quote === "'" && char === "'" && hasOddTrailingBackslashes(state.current);
|
|
471
|
+
state.current += char;
|
|
472
|
+
if (char === state.quote && !escapedSingleQuote) {
|
|
473
|
+
state.quote = null;
|
|
474
|
+
}
|
|
475
|
+
return true;
|
|
476
|
+
}
|
|
477
|
+
function consumeQuoteStart(state, char) {
|
|
478
|
+
if (!(char === '"' || char === "'")) {
|
|
479
|
+
return false;
|
|
480
|
+
}
|
|
481
|
+
state.quote = char;
|
|
482
|
+
state.current += char;
|
|
483
|
+
return true;
|
|
484
|
+
}
|
|
485
|
+
function consumeCommandChainSeparator(state, command, index) {
|
|
486
|
+
const separatorLength = readChainSeparatorLength(command, index);
|
|
487
|
+
if (separatorLength <= 0) {
|
|
488
|
+
return 0;
|
|
489
|
+
}
|
|
490
|
+
pushCommandChainPart(state);
|
|
491
|
+
return separatorLength;
|
|
492
|
+
}
|
|
493
|
+
function splitCommandChain(command) {
|
|
494
|
+
const state = {
|
|
495
|
+
parts: [],
|
|
496
|
+
current: '',
|
|
497
|
+
quote: null,
|
|
498
|
+
escaped: false,
|
|
499
|
+
};
|
|
500
|
+
for (let index = 0; index < command.length; index += 1) {
|
|
501
|
+
const char = command[index];
|
|
502
|
+
if (!char) {
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
if (consumeEscapedCommandChar(state, char)) {
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
if (consumeEscapeInitiator(state, char)) {
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
if (consumeQuotedCommandChar(state, char)) {
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
if (consumeQuoteStart(state, char)) {
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
const consumedSeparatorLength = consumeCommandChainSeparator(state, command, index);
|
|
518
|
+
if (consumedSeparatorLength > 0) {
|
|
519
|
+
index += consumedSeparatorLength - 1;
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
state.current += char;
|
|
523
|
+
}
|
|
524
|
+
pushCommandChainPart(state);
|
|
525
|
+
return state.parts;
|
|
526
|
+
}
|
|
527
|
+
function buildCommandDisplayContexts(rawCommand, cwd) {
|
|
528
|
+
const command = rawCommand?.trim();
|
|
529
|
+
if (!command) {
|
|
530
|
+
return [];
|
|
531
|
+
}
|
|
532
|
+
const contexts = [];
|
|
533
|
+
const segments = splitCommandChain(command);
|
|
534
|
+
for (const segment of segments) {
|
|
535
|
+
const tokens = tokenizeCommand(segment);
|
|
536
|
+
if (tokens.length === 0) {
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
const [firstToken] = tokens;
|
|
540
|
+
if (!firstToken) {
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
const firstCommand = commandName(firstToken);
|
|
544
|
+
const nonFlagArgs = tokens.slice(1).filter((token) => !token.startsWith('-'));
|
|
545
|
+
const pathArg = nonFlagArgs.find(isLikelyPathToken) ?? null;
|
|
546
|
+
const commandPath = pathArg ? normalizeLocationPath(pathArg, cwd) : null;
|
|
547
|
+
const locations = commandPath ? [{ path: commandPath }] : [];
|
|
548
|
+
contexts.push({
|
|
549
|
+
command: segment,
|
|
550
|
+
firstCommand,
|
|
551
|
+
nonFlagArgs,
|
|
552
|
+
pathArg,
|
|
553
|
+
cwd,
|
|
554
|
+
locations,
|
|
555
|
+
isShellMeta: SHELL_META_COMMANDS.has(firstCommand),
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
return contexts;
|
|
559
|
+
}
|
|
560
|
+
function chooseActionableCommandContexts(contexts) {
|
|
561
|
+
const actionable = contexts.filter((context) => !context.isShellMeta);
|
|
562
|
+
return actionable.length > 0 ? actionable : contexts;
|
|
563
|
+
}
|
|
564
|
+
function selectCombinedCommandKind(displays) {
|
|
565
|
+
const firstNonExecute = displays.find((display) => display.kind !== 'execute');
|
|
566
|
+
return firstNonExecute?.kind ?? 'execute';
|
|
567
|
+
}
|
|
568
|
+
function resolveCommandDisplay(params) {
|
|
569
|
+
const contexts = chooseActionableCommandContexts(buildCommandDisplayContexts(params.command, params.cwd));
|
|
570
|
+
if (contexts.length === 0) {
|
|
571
|
+
return { title: 'commandExecution', kind: 'execute', locations: [] };
|
|
572
|
+
}
|
|
573
|
+
const [singleContext] = contexts;
|
|
574
|
+
if (singleContext && contexts.length === 1) {
|
|
575
|
+
return resolveCommandDisplayFromContext(singleContext);
|
|
576
|
+
}
|
|
577
|
+
const displays = contexts.map((context) => resolveCommandDisplayFromContext(context));
|
|
578
|
+
const title = dedupeStrings(displays.map((display) => display.title)).join(', ');
|
|
579
|
+
const kind = selectCombinedCommandKind(displays);
|
|
580
|
+
const locations = dedupeLocations(displays.flatMap((display) => display.locations)).map((location) => ({ path: location.path }));
|
|
581
|
+
return { title, kind, locations };
|
|
582
|
+
}
|
|
583
|
+
function resolveCommandDisplayFromContext(context) {
|
|
584
|
+
if (READ_COMMANDS.has(context.firstCommand)) {
|
|
585
|
+
const label = context.pathArg ?? context.command;
|
|
586
|
+
return { title: `Read ${label}`, kind: 'read', locations: context.locations };
|
|
587
|
+
}
|
|
588
|
+
if (LIST_OR_FIND_COMMANDS.has(context.firstCommand)) {
|
|
589
|
+
const target = context.pathArg
|
|
590
|
+
? normalizeLocationPath(context.pathArg, context.cwd)
|
|
591
|
+
: context.cwd;
|
|
592
|
+
const title = context.firstCommand === 'find' || context.firstCommand === 'fd'
|
|
593
|
+
? `Search ${target}`
|
|
594
|
+
: `List ${target}`;
|
|
595
|
+
return { title, kind: 'search', locations: context.locations };
|
|
596
|
+
}
|
|
597
|
+
if (GREP_LIKE_COMMANDS.has(context.firstCommand)) {
|
|
598
|
+
const query = context.nonFlagArgs.find((token) => !isLikelyPathToken(token));
|
|
599
|
+
const target = context.pathArg ? ` in ${context.pathArg}` : '';
|
|
600
|
+
const title = query ? `Search ${query}${target}` : `Search ${context.command}`;
|
|
601
|
+
return { title, kind: 'search', locations: context.locations };
|
|
602
|
+
}
|
|
603
|
+
return { title: context.command, kind: 'execute', locations: context.locations };
|
|
604
|
+
}
|
|
605
|
+
function normalizeCwdForScope(cwd) {
|
|
606
|
+
if (cwd.length > 1 && cwd.endsWith('/')) {
|
|
607
|
+
return cwd.slice(0, -1);
|
|
608
|
+
}
|
|
609
|
+
return cwd;
|
|
610
|
+
}
|
|
611
|
+
function normalizeScopeToken(token) {
|
|
612
|
+
return token.replace(/\s+/g, ' ').trim();
|
|
613
|
+
}
|
|
614
|
+
function buildCommandApprovalScopeKey(params) {
|
|
615
|
+
const contexts = buildCommandDisplayContexts(params.command, params.cwd);
|
|
616
|
+
if (contexts.length === 0) {
|
|
617
|
+
return null;
|
|
618
|
+
}
|
|
619
|
+
let scopeCwd = normalizeCwdForScope(params.cwd);
|
|
620
|
+
const segments = [];
|
|
621
|
+
for (const context of contexts) {
|
|
622
|
+
if (context.firstCommand === 'cd') {
|
|
623
|
+
const rawTarget = context.nonFlagArgs[0];
|
|
624
|
+
const target = rawTarget ? normalizeScopeToken(rawTarget) : '';
|
|
625
|
+
if (target.length > 0) {
|
|
626
|
+
scopeCwd = normalizeCwdForScope(normalizeLocationPath(target, scopeCwd));
|
|
627
|
+
segments.push(`cd ${target} -> ${scopeCwd}`);
|
|
628
|
+
}
|
|
629
|
+
else {
|
|
630
|
+
segments.push('cd');
|
|
631
|
+
}
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
const args = context.nonFlagArgs.map(normalizeScopeToken).filter((arg) => arg.length > 0);
|
|
635
|
+
const normalized = normalizeScopeToken([context.firstCommand, ...args].join(' '));
|
|
636
|
+
if (normalized.length === 0) {
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
segments.push(`[cwd=${scopeCwd}] ${normalized}`);
|
|
640
|
+
}
|
|
641
|
+
const normalizedSegments = segments
|
|
642
|
+
.map(normalizeScopeToken)
|
|
643
|
+
.filter((segment) => segment.length > 0);
|
|
644
|
+
if (normalizedSegments.length === 0) {
|
|
645
|
+
return null;
|
|
646
|
+
}
|
|
647
|
+
return `cwd=${normalizeCwdForScope(params.cwd)}|${normalizedSegments.join(' && ')}`;
|
|
648
|
+
}
|
|
649
|
+
function dedupeLocations(locations) {
|
|
650
|
+
const seen = new Set();
|
|
651
|
+
const result = [];
|
|
652
|
+
for (const location of locations) {
|
|
653
|
+
const key = `${location.path}:${location.line ?? ''}`;
|
|
654
|
+
if (seen.has(key)) {
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
seen.add(key);
|
|
658
|
+
result.push(location);
|
|
659
|
+
}
|
|
660
|
+
return result;
|
|
661
|
+
}
|
|
662
|
+
function parseTextFromPromptBlock(block) {
|
|
663
|
+
if (block.type === 'text') {
|
|
664
|
+
return block.text;
|
|
665
|
+
}
|
|
666
|
+
if (block.type === 'resource_link') {
|
|
667
|
+
return `[ACP_RESOURCE_LINK uri="${block.uri}" name="${block.name}"]\n[/ACP_RESOURCE_LINK]`;
|
|
668
|
+
}
|
|
669
|
+
if (block.type === 'resource') {
|
|
670
|
+
const resource = block.resource;
|
|
671
|
+
const mime = resource.mimeType ?? 'unknown';
|
|
672
|
+
const payload = 'text' in resource && typeof resource.text === 'string'
|
|
673
|
+
? resource.text
|
|
674
|
+
: 'blob' in resource && typeof resource.blob === 'string'
|
|
675
|
+
? resource.blob
|
|
676
|
+
: JSON.stringify(resource);
|
|
677
|
+
return `[ACP_RESOURCE uri="${resource.uri}" mime="${mime}"]\n${payload}\n[/ACP_RESOURCE]`;
|
|
678
|
+
}
|
|
679
|
+
if (block.type === 'image') {
|
|
680
|
+
const mime = block.mimeType ?? 'application/octet-stream';
|
|
681
|
+
return `[ACP_IMAGE mime="${mime}" bytes=${block.data.length}]`;
|
|
682
|
+
}
|
|
683
|
+
return JSON.stringify(block);
|
|
684
|
+
}
|
|
685
|
+
function toToolStatus(status) {
|
|
686
|
+
if (status === 'inProgress') {
|
|
687
|
+
return 'in_progress';
|
|
688
|
+
}
|
|
689
|
+
if (status === 'completed') {
|
|
690
|
+
return 'completed';
|
|
691
|
+
}
|
|
692
|
+
if (status === 'failed' || status === 'declined') {
|
|
693
|
+
return 'failed';
|
|
694
|
+
}
|
|
695
|
+
return undefined;
|
|
696
|
+
}
|
|
697
|
+
function createCollaborationModeConfigOption(currentMode, collaborationModes) {
|
|
698
|
+
const options = collaborationModes
|
|
699
|
+
.filter((entry) => isNonEmptyString(entry.mode))
|
|
700
|
+
.map((entry) => ({
|
|
701
|
+
value: entry.mode,
|
|
702
|
+
name: entry.name,
|
|
703
|
+
}));
|
|
704
|
+
if (!options.some((option) => option.value === currentMode)) {
|
|
705
|
+
options.unshift({ value: currentMode, name: sanitizeModeName(currentMode) });
|
|
706
|
+
}
|
|
707
|
+
return {
|
|
708
|
+
id: 'mode',
|
|
709
|
+
category: 'mode',
|
|
710
|
+
name: 'Collaboration Mode',
|
|
711
|
+
type: 'select',
|
|
712
|
+
currentValue: currentMode,
|
|
713
|
+
options,
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
function createModelConfigOption(currentModel, modelCatalog) {
|
|
717
|
+
const options = modelCatalog.map((model) => ({
|
|
718
|
+
value: model.id,
|
|
719
|
+
name: model.displayName,
|
|
720
|
+
description: model.description,
|
|
721
|
+
}));
|
|
722
|
+
if (!options.some((option) => option.value === currentModel)) {
|
|
723
|
+
options.unshift({ value: currentModel, name: currentModel });
|
|
724
|
+
}
|
|
725
|
+
return {
|
|
726
|
+
id: 'model',
|
|
727
|
+
category: 'model',
|
|
728
|
+
name: 'Model',
|
|
729
|
+
type: 'select',
|
|
730
|
+
currentValue: currentModel,
|
|
731
|
+
options,
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
function createReasoningEffortConfigOption(currentReasoningEffort, modelCatalog, currentModel) {
|
|
735
|
+
const modelEntry = modelCatalog.find((model) => model.id === currentModel);
|
|
736
|
+
if (!modelEntry) {
|
|
737
|
+
return null;
|
|
738
|
+
}
|
|
739
|
+
const supportedEntries = modelEntry.supportedReasoningEfforts.filter((entry) => isNonEmptyString(entry.reasoningEffort));
|
|
740
|
+
if (supportedEntries.length === 0) {
|
|
741
|
+
return null;
|
|
742
|
+
}
|
|
743
|
+
const supportedByValue = new Map();
|
|
744
|
+
for (const entry of supportedEntries) {
|
|
745
|
+
if (!supportedByValue.has(entry.reasoningEffort)) {
|
|
746
|
+
supportedByValue.set(entry.reasoningEffort, entry.description);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
const supportedValues = [...supportedByValue.keys()];
|
|
750
|
+
const resolvedCurrent = currentReasoningEffort && supportedByValue.has(currentReasoningEffort)
|
|
751
|
+
? currentReasoningEffort
|
|
752
|
+
: supportedByValue.has(modelEntry.defaultReasoningEffort)
|
|
753
|
+
? modelEntry.defaultReasoningEffort
|
|
754
|
+
: supportedValues[0];
|
|
755
|
+
if (!resolvedCurrent) {
|
|
756
|
+
return null;
|
|
757
|
+
}
|
|
758
|
+
return {
|
|
759
|
+
id: 'reasoning_effort',
|
|
760
|
+
category: 'thought_level',
|
|
761
|
+
name: 'Reasoning Effort',
|
|
762
|
+
type: 'select',
|
|
763
|
+
currentValue: resolvedCurrent,
|
|
764
|
+
options: supportedValues.map((value) => ({
|
|
765
|
+
value,
|
|
766
|
+
name: value,
|
|
767
|
+
...(supportedByValue.get(value) ? { description: supportedByValue.get(value) } : {}),
|
|
768
|
+
})),
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
function createExecutionModeConfigOption(currentPresetId, presets) {
|
|
772
|
+
return {
|
|
773
|
+
id: 'execution_mode',
|
|
774
|
+
category: 'permission',
|
|
775
|
+
name: 'Execution Mode',
|
|
776
|
+
type: 'select',
|
|
777
|
+
currentValue: currentPresetId,
|
|
778
|
+
options: presets.map((preset) => ({
|
|
779
|
+
value: preset.id,
|
|
780
|
+
name: preset.name,
|
|
781
|
+
...(preset.description ? { description: preset.description } : {}),
|
|
782
|
+
})),
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
function toExecutionPresetId(approvalPolicy, sandboxMode) {
|
|
786
|
+
return JSON.stringify([approvalPolicy, sandboxMode]);
|
|
787
|
+
}
|
|
788
|
+
export class CodexAppServerAcpAdapter {
|
|
789
|
+
connection;
|
|
790
|
+
codex;
|
|
791
|
+
sessions = new Map();
|
|
792
|
+
sessionIdByThreadId = new Map();
|
|
793
|
+
modelCatalog = [];
|
|
794
|
+
allowedApprovalPolicies = [];
|
|
795
|
+
allowedSandboxModes = [];
|
|
796
|
+
collaborationModes = [];
|
|
797
|
+
mcpServersByThreadId = new Map();
|
|
798
|
+
appliedMcpServerConfigJson = '{}';
|
|
799
|
+
shapeDriftCounts = new Map();
|
|
800
|
+
constructor(connection, codexClient) {
|
|
801
|
+
this.connection = connection;
|
|
802
|
+
this.codex =
|
|
803
|
+
codexClient ??
|
|
804
|
+
new CodexRpcClient({
|
|
805
|
+
cwd: process.cwd(),
|
|
806
|
+
env: { ...process.env },
|
|
807
|
+
onStderr: (line) => {
|
|
808
|
+
process.stderr.write(line);
|
|
809
|
+
},
|
|
810
|
+
onNotification: (notification) => {
|
|
811
|
+
void this.handleCodexNotification(notification.method, notification.params);
|
|
812
|
+
},
|
|
813
|
+
onRequest: (request) => {
|
|
814
|
+
void this.handleCodexServerRequest(request).catch((error) => {
|
|
815
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
816
|
+
this.codex.respondError(request.id, {
|
|
817
|
+
code: -32_600,
|
|
818
|
+
message: 'Failed to process codex approval request',
|
|
819
|
+
data: { error: message },
|
|
820
|
+
});
|
|
821
|
+
});
|
|
822
|
+
},
|
|
823
|
+
onProtocolError: (error) => {
|
|
824
|
+
process.stderr.write(`[codex-app-server-acp] protocol-error: ${error.reason}\n`);
|
|
825
|
+
},
|
|
826
|
+
});
|
|
827
|
+
this.monitorConnectionClose();
|
|
828
|
+
}
|
|
829
|
+
monitorConnectionClose() {
|
|
830
|
+
let attachAttempts = 0;
|
|
831
|
+
const attachCloseWatcher = () => {
|
|
832
|
+
try {
|
|
833
|
+
void this.connection.closed
|
|
834
|
+
.finally(async () => {
|
|
835
|
+
await this.codex.stop();
|
|
836
|
+
})
|
|
837
|
+
.catch(() => {
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
catch {
|
|
841
|
+
attachAttempts += 1;
|
|
842
|
+
if (attachAttempts >= MAX_CLOSE_WATCHER_ATTACH_RETRIES) {
|
|
843
|
+
process.stderr.write(`[codex-app-server-acp] failed to attach close watcher after ${MAX_CLOSE_WATCHER_ATTACH_RETRIES} attempts\n`);
|
|
844
|
+
void this.codex.stop();
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
const retryTimer = setTimeout(attachCloseWatcher, 0);
|
|
848
|
+
retryTimer.unref?.();
|
|
849
|
+
}
|
|
850
|
+
};
|
|
851
|
+
queueMicrotask(attachCloseWatcher);
|
|
852
|
+
}
|
|
853
|
+
async initialize(_params) {
|
|
854
|
+
this.codex.start();
|
|
855
|
+
await this.codex.request('initialize', {
|
|
856
|
+
clientInfo: {
|
|
857
|
+
name: 'factory-factory-codex-app-server-acp',
|
|
858
|
+
version: '0.1.0',
|
|
859
|
+
},
|
|
860
|
+
capabilities: {
|
|
861
|
+
experimentalApi: true,
|
|
862
|
+
},
|
|
863
|
+
});
|
|
864
|
+
this.codex.notify('initialized');
|
|
865
|
+
const configRequirements = await this.loadConfigRequirements();
|
|
866
|
+
this.allowedApprovalPolicies = configRequirements.allowedApprovalPolicies;
|
|
867
|
+
this.allowedSandboxModes = configRequirements.allowedSandboxModes;
|
|
868
|
+
this.collaborationModes = await this.loadCollaborationModes();
|
|
869
|
+
this.modelCatalog = await this.loadModelCatalog();
|
|
870
|
+
return {
|
|
871
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
872
|
+
agentInfo: {
|
|
873
|
+
name: 'factory-factory-codex-app-server-acp',
|
|
874
|
+
version: '0.1.0',
|
|
875
|
+
},
|
|
876
|
+
agentCapabilities: {
|
|
877
|
+
loadSession: true,
|
|
878
|
+
promptCapabilities: {
|
|
879
|
+
image: false,
|
|
880
|
+
embeddedContext: true,
|
|
881
|
+
},
|
|
882
|
+
},
|
|
883
|
+
authMethods: [],
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
async newSession(params) {
|
|
887
|
+
const defaultModel = this.resolveDefaultModel();
|
|
888
|
+
const responseRaw = await this.codex.request('thread/start', {
|
|
889
|
+
cwd: params.cwd,
|
|
890
|
+
model: defaultModel,
|
|
891
|
+
});
|
|
892
|
+
const response = threadStartResponseSchema.parse(responseRaw);
|
|
893
|
+
const sessionId = toSessionId(response.thread.id);
|
|
894
|
+
const approvalPolicy = this.requireApprovalPolicy(response.approvalPolicy, 'thread/start');
|
|
895
|
+
const model = this.resolveSessionModel(response.model, defaultModel);
|
|
896
|
+
const sandboxPolicy = this.resolveSandboxPolicy(response.sandbox, params.cwd);
|
|
897
|
+
const reasoningEffort = this.resolveReasoningEffortForModel(model, response.reasoningEffort);
|
|
898
|
+
const collaborationMode = this.resolveDefaultCollaborationMode();
|
|
899
|
+
const session = {
|
|
900
|
+
sessionId,
|
|
901
|
+
threadId: response.thread.id,
|
|
902
|
+
cwd: params.cwd,
|
|
903
|
+
defaults: {
|
|
904
|
+
model,
|
|
905
|
+
approvalPolicy,
|
|
906
|
+
sandboxPolicy,
|
|
907
|
+
reasoningEffort,
|
|
908
|
+
collaborationMode,
|
|
909
|
+
},
|
|
910
|
+
activeTurn: null,
|
|
911
|
+
toolCallsByItemId: new Map(),
|
|
912
|
+
reasoningDeltaItemIds: new Set(),
|
|
913
|
+
planTextByItemId: new Map(),
|
|
914
|
+
planApprovalRequestedByTurnId: new Set(),
|
|
915
|
+
pendingPlanApprovalsByTurnId: new Map(),
|
|
916
|
+
pendingTurnCompletionsByTurnId: new Map(),
|
|
917
|
+
commandApprovalScopes: new Set(),
|
|
918
|
+
replayedTurnItemKeys: new Set(),
|
|
919
|
+
};
|
|
920
|
+
this.sessions.set(sessionId, session);
|
|
921
|
+
this.sessionIdByThreadId.set(session.threadId, sessionId);
|
|
922
|
+
try {
|
|
923
|
+
await this.applyMcpServers(session.threadId, params.mcpServers);
|
|
924
|
+
}
|
|
925
|
+
catch (error) {
|
|
926
|
+
this.sessions.delete(sessionId);
|
|
927
|
+
this.sessionIdByThreadId.delete(session.threadId);
|
|
928
|
+
throw error;
|
|
929
|
+
}
|
|
930
|
+
return {
|
|
931
|
+
sessionId,
|
|
932
|
+
configOptions: this.buildConfigOptions(session),
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
async loadSession(params) {
|
|
936
|
+
const threadId = toThreadId(params.sessionId);
|
|
937
|
+
const responseRaw = await this.codex.request('thread/resume', {
|
|
938
|
+
threadId,
|
|
939
|
+
cwd: params.cwd,
|
|
940
|
+
});
|
|
941
|
+
const response = threadResumeResponseSchema.parse(responseRaw);
|
|
942
|
+
const approvalPolicy = this.requireApprovalPolicy(response.approvalPolicy, 'thread/resume');
|
|
943
|
+
const model = this.resolveSessionModel(response.model, this.resolveDefaultModel());
|
|
944
|
+
const sandboxPolicy = this.resolveSandboxPolicy(response.sandbox, params.cwd);
|
|
945
|
+
const reasoningEffort = this.resolveReasoningEffortForModel(model, response.reasoningEffort);
|
|
946
|
+
const collaborationMode = this.resolveDefaultCollaborationMode();
|
|
947
|
+
const session = {
|
|
948
|
+
sessionId: params.sessionId,
|
|
949
|
+
threadId: response.thread.id,
|
|
950
|
+
cwd: params.cwd,
|
|
951
|
+
defaults: {
|
|
952
|
+
model,
|
|
953
|
+
approvalPolicy,
|
|
954
|
+
sandboxPolicy,
|
|
955
|
+
reasoningEffort,
|
|
956
|
+
collaborationMode,
|
|
957
|
+
},
|
|
958
|
+
activeTurn: null,
|
|
959
|
+
toolCallsByItemId: new Map(),
|
|
960
|
+
reasoningDeltaItemIds: new Set(),
|
|
961
|
+
planTextByItemId: new Map(),
|
|
962
|
+
planApprovalRequestedByTurnId: new Set(),
|
|
963
|
+
pendingPlanApprovalsByTurnId: new Map(),
|
|
964
|
+
pendingTurnCompletionsByTurnId: new Map(),
|
|
965
|
+
commandApprovalScopes: new Set(),
|
|
966
|
+
replayedTurnItemKeys: new Set(),
|
|
967
|
+
};
|
|
968
|
+
this.sessions.set(session.sessionId, session);
|
|
969
|
+
this.sessionIdByThreadId.set(session.threadId, session.sessionId);
|
|
970
|
+
try {
|
|
971
|
+
await this.applyMcpServers(session.threadId, params.mcpServers);
|
|
972
|
+
await this.replayThreadHistory(session.sessionId, session.threadId);
|
|
973
|
+
}
|
|
974
|
+
catch (error) {
|
|
975
|
+
try {
|
|
976
|
+
await this.removeMcpServersForThread(session.threadId);
|
|
977
|
+
}
|
|
978
|
+
catch {
|
|
979
|
+
}
|
|
980
|
+
this.sessions.delete(session.sessionId);
|
|
981
|
+
this.sessionIdByThreadId.delete(session.threadId);
|
|
982
|
+
throw error;
|
|
983
|
+
}
|
|
984
|
+
return {
|
|
985
|
+
configOptions: this.buildConfigOptions(session),
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
async authenticate(_params) {
|
|
989
|
+
await Promise.resolve();
|
|
990
|
+
return {};
|
|
991
|
+
}
|
|
992
|
+
async setSessionMode(params) {
|
|
993
|
+
await Promise.resolve();
|
|
994
|
+
const session = this.requireSession(params.sessionId);
|
|
995
|
+
const availableModes = this.getCollaborationModeValues(session.defaults.collaborationMode);
|
|
996
|
+
if (!availableModes.includes(params.modeId)) {
|
|
997
|
+
throw RequestError.invalidParams({
|
|
998
|
+
modeId: params.modeId,
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
session.defaults.collaborationMode = params.modeId;
|
|
1002
|
+
return {};
|
|
1003
|
+
}
|
|
1004
|
+
async setSessionConfigOption(params) {
|
|
1005
|
+
await Promise.resolve();
|
|
1006
|
+
const session = this.requireSession(params.sessionId);
|
|
1007
|
+
switch (params.configId) {
|
|
1008
|
+
case 'mode': {
|
|
1009
|
+
const availableModes = this.getCollaborationModeValues(session.defaults.collaborationMode);
|
|
1010
|
+
if (!availableModes.includes(params.value)) {
|
|
1011
|
+
throw RequestError.invalidParams({
|
|
1012
|
+
configId: params.configId,
|
|
1013
|
+
value: params.value,
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
session.defaults.collaborationMode = params.value;
|
|
1017
|
+
break;
|
|
1018
|
+
}
|
|
1019
|
+
case 'execution_mode': {
|
|
1020
|
+
const presets = this.getExecutionPresets(session);
|
|
1021
|
+
const selectedPreset = presets.find((preset) => preset.id === params.value);
|
|
1022
|
+
if (!selectedPreset) {
|
|
1023
|
+
throw RequestError.invalidParams({
|
|
1024
|
+
configId: params.configId,
|
|
1025
|
+
value: params.value,
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
session.defaults.approvalPolicy = selectedPreset.approvalPolicy;
|
|
1029
|
+
session.defaults.sandboxPolicy = createSandboxPolicyFromMode(selectedPreset.sandboxMode, session.cwd);
|
|
1030
|
+
break;
|
|
1031
|
+
}
|
|
1032
|
+
case 'model': {
|
|
1033
|
+
if (!this.modelCatalog.some((model) => model.id === params.value)) {
|
|
1034
|
+
throw RequestError.invalidParams({
|
|
1035
|
+
configId: params.configId,
|
|
1036
|
+
value: params.value,
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
session.defaults.model = params.value;
|
|
1040
|
+
session.defaults.reasoningEffort = this.resolveReasoningEffortForModel(session.defaults.model, session.defaults.reasoningEffort);
|
|
1041
|
+
break;
|
|
1042
|
+
}
|
|
1043
|
+
case 'reasoning_effort':
|
|
1044
|
+
case 'thought_level': {
|
|
1045
|
+
const modelEntry = this.modelCatalog.find((model) => model.id === session.defaults.model);
|
|
1046
|
+
const supportedReasoningEfforts = modelEntry?.supportedReasoningEfforts ?? [];
|
|
1047
|
+
const supportedValues = new Set(supportedReasoningEfforts
|
|
1048
|
+
.map((entry) => entry.reasoningEffort)
|
|
1049
|
+
.filter((effort) => isNonEmptyString(effort)));
|
|
1050
|
+
if (!supportedValues.has(params.value)) {
|
|
1051
|
+
throw RequestError.invalidParams({
|
|
1052
|
+
configId: params.configId,
|
|
1053
|
+
value: params.value,
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
session.defaults.reasoningEffort = params.value;
|
|
1057
|
+
break;
|
|
1058
|
+
}
|
|
1059
|
+
default:
|
|
1060
|
+
throw RequestError.invalidParams({ configId: params.configId });
|
|
1061
|
+
}
|
|
1062
|
+
return {
|
|
1063
|
+
configOptions: this.buildConfigOptions(session),
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
async prompt(params) {
|
|
1067
|
+
const session = this.requireSession(params.sessionId);
|
|
1068
|
+
if (session.activeTurn) {
|
|
1069
|
+
throw RequestError.invalidRequest({
|
|
1070
|
+
reason: 'A turn is already in progress for this session',
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
const input = params.prompt
|
|
1074
|
+
.map((block) => parseTextFromPromptBlock(block))
|
|
1075
|
+
.filter((text) => text.trim().length > 0)
|
|
1076
|
+
.map((text) => ({ type: 'text', text, text_elements: [] }));
|
|
1077
|
+
if (input.length === 0) {
|
|
1078
|
+
throw RequestError.invalidParams({
|
|
1079
|
+
reason: 'Prompt must include at least one non-empty content block',
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
const stopReasonPromise = this.createPendingTurnPromise(session);
|
|
1083
|
+
try {
|
|
1084
|
+
const turnStartParams = {
|
|
1085
|
+
threadId: session.threadId,
|
|
1086
|
+
input,
|
|
1087
|
+
cwd: session.cwd,
|
|
1088
|
+
approvalPolicy: session.defaults.approvalPolicy,
|
|
1089
|
+
sandboxPolicy: session.defaults.sandboxPolicy,
|
|
1090
|
+
model: session.defaults.model,
|
|
1091
|
+
effort: session.defaults.reasoningEffort,
|
|
1092
|
+
};
|
|
1093
|
+
const collaborationModeParams = this.resolveTurnCollaborationMode(session);
|
|
1094
|
+
if (collaborationModeParams) {
|
|
1095
|
+
turnStartParams.collaborationMode = collaborationModeParams;
|
|
1096
|
+
}
|
|
1097
|
+
const turnStartRaw = await this.codex.request('turn/start', turnStartParams);
|
|
1098
|
+
const turnStart = turnStartResponseSchema.parse(turnStartRaw);
|
|
1099
|
+
await this.bindTurnToActivePrompt(session, turnStart.turn.id);
|
|
1100
|
+
const immediateStopReason = await this.resolveImmediateTurnStopReason(session, turnStart);
|
|
1101
|
+
if (immediateStopReason) {
|
|
1102
|
+
this.settleTurn(session, immediateStopReason);
|
|
1103
|
+
return { stopReason: await stopReasonPromise };
|
|
1104
|
+
}
|
|
1105
|
+
if (this.isActiveTurnCancelRequested(session)) {
|
|
1106
|
+
await this.requestTurnInterrupt(session);
|
|
1107
|
+
}
|
|
1108
|
+
return { stopReason: await stopReasonPromise };
|
|
1109
|
+
}
|
|
1110
|
+
catch (error) {
|
|
1111
|
+
if (error instanceof CodexRequestError && error.code === -32_001) {
|
|
1112
|
+
await this.emitTurnFailureMessage(session.sessionId, error.message || 'Codex app-server request failed with overload response.');
|
|
1113
|
+
this.settleTurn(session, 'end_turn');
|
|
1114
|
+
return { stopReason: await stopReasonPromise };
|
|
1115
|
+
}
|
|
1116
|
+
this.settleTurn(session, 'end_turn');
|
|
1117
|
+
throw error;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
async resolveImmediateTurnStopReason(session, turnStart) {
|
|
1121
|
+
const status = turnStart.turn.status;
|
|
1122
|
+
if (status === 'interrupted') {
|
|
1123
|
+
return 'cancelled';
|
|
1124
|
+
}
|
|
1125
|
+
if (status === 'failed') {
|
|
1126
|
+
const failureMessage = asString(isRecord(turnStart.turn.error) ? turnStart.turn.error.message : null);
|
|
1127
|
+
if (failureMessage) {
|
|
1128
|
+
await this.emitTurnFailureMessage(session.sessionId, failureMessage);
|
|
1129
|
+
}
|
|
1130
|
+
return 'end_turn';
|
|
1131
|
+
}
|
|
1132
|
+
if (status === 'completed') {
|
|
1133
|
+
return 'end_turn';
|
|
1134
|
+
}
|
|
1135
|
+
return null;
|
|
1136
|
+
}
|
|
1137
|
+
createPendingTurnPromise(session) {
|
|
1138
|
+
return new Promise((resolve) => {
|
|
1139
|
+
session.activeTurn = {
|
|
1140
|
+
turnId: PENDING_TURN_ID,
|
|
1141
|
+
cancelRequested: false,
|
|
1142
|
+
settled: false,
|
|
1143
|
+
resolve,
|
|
1144
|
+
};
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
1147
|
+
async bindTurnToActivePrompt(session, turnId) {
|
|
1148
|
+
if (!session.activeTurn || session.activeTurn.settled) {
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
session.activeTurn.turnId = turnId;
|
|
1152
|
+
const pendingCompletion = session.pendingTurnCompletionsByTurnId.get(turnId);
|
|
1153
|
+
if (!pendingCompletion) {
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
session.pendingTurnCompletionsByTurnId.delete(turnId);
|
|
1157
|
+
if (pendingCompletion.errorMessage) {
|
|
1158
|
+
await this.emitTurnFailureMessage(session.sessionId, pendingCompletion.errorMessage);
|
|
1159
|
+
}
|
|
1160
|
+
this.settleTurn(session, pendingCompletion.stopReason);
|
|
1161
|
+
}
|
|
1162
|
+
async requestTurnInterrupt(session) {
|
|
1163
|
+
if (!session.activeTurn || session.activeTurn.turnId === PENDING_TURN_ID) {
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
await this.codex.request('turn/interrupt', {
|
|
1167
|
+
threadId: session.threadId,
|
|
1168
|
+
turnId: session.activeTurn.turnId,
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
isActiveTurnCancelRequested(session) {
|
|
1172
|
+
return Boolean(session.activeTurn?.cancelRequested);
|
|
1173
|
+
}
|
|
1174
|
+
async emitTurnFailureMessage(sessionId, errorMessage) {
|
|
1175
|
+
await this.emitSessionUpdate(sessionId, {
|
|
1176
|
+
sessionUpdate: 'agent_message_chunk',
|
|
1177
|
+
content: {
|
|
1178
|
+
type: 'text',
|
|
1179
|
+
text: errorMessage,
|
|
1180
|
+
},
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
async cancel(params) {
|
|
1184
|
+
const session = this.sessions.get(params.sessionId);
|
|
1185
|
+
if (!session?.activeTurn) {
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
session.activeTurn.cancelRequested = true;
|
|
1189
|
+
if (session.activeTurn.turnId === PENDING_TURN_ID) {
|
|
1190
|
+
return;
|
|
1191
|
+
}
|
|
1192
|
+
try {
|
|
1193
|
+
await this.requestTurnInterrupt(session);
|
|
1194
|
+
}
|
|
1195
|
+
catch {
|
|
1196
|
+
this.settleTurn(session, 'cancelled');
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
resolveDefaultModel() {
|
|
1200
|
+
const preferred = this.modelCatalog.find((model) => model.isDefault);
|
|
1201
|
+
const fallback = this.modelCatalog[0]?.id;
|
|
1202
|
+
if (preferred?.id) {
|
|
1203
|
+
return preferred.id;
|
|
1204
|
+
}
|
|
1205
|
+
if (fallback) {
|
|
1206
|
+
return fallback;
|
|
1207
|
+
}
|
|
1208
|
+
throw new Error('Codex app-server model/list returned no models');
|
|
1209
|
+
}
|
|
1210
|
+
buildMergedMcpServersConfig() {
|
|
1211
|
+
const merged = {};
|
|
1212
|
+
const threadIds = [...this.mcpServersByThreadId.keys()].sort();
|
|
1213
|
+
for (const threadId of threadIds) {
|
|
1214
|
+
const threadConfig = this.mcpServersByThreadId.get(threadId);
|
|
1215
|
+
if (!threadConfig) {
|
|
1216
|
+
continue;
|
|
1217
|
+
}
|
|
1218
|
+
for (const serverName of Object.keys(threadConfig).sort()) {
|
|
1219
|
+
const nextServerConfig = threadConfig[serverName];
|
|
1220
|
+
if (!nextServerConfig) {
|
|
1221
|
+
continue;
|
|
1222
|
+
}
|
|
1223
|
+
const existingConfig = merged[serverName];
|
|
1224
|
+
if (!existingConfig) {
|
|
1225
|
+
merged[serverName] = nextServerConfig;
|
|
1226
|
+
continue;
|
|
1227
|
+
}
|
|
1228
|
+
if (JSON.stringify(existingConfig) === JSON.stringify(nextServerConfig)) {
|
|
1229
|
+
continue;
|
|
1230
|
+
}
|
|
1231
|
+
merged[`${serverName}__${threadId}`] = nextServerConfig;
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
return merged;
|
|
1235
|
+
}
|
|
1236
|
+
async writeMergedMcpServersConfig() {
|
|
1237
|
+
const mergedConfig = this.buildMergedMcpServersConfig();
|
|
1238
|
+
const nextConfigJson = JSON.stringify(mergedConfig);
|
|
1239
|
+
if (nextConfigJson === this.appliedMcpServerConfigJson) {
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
await this.codex.request('config/value/write', {
|
|
1243
|
+
keyPath: 'mcp_servers',
|
|
1244
|
+
value: mergedConfig,
|
|
1245
|
+
mergeStrategy: 'replace',
|
|
1246
|
+
});
|
|
1247
|
+
await this.codex.request('config/mcpServer/reload', {});
|
|
1248
|
+
this.appliedMcpServerConfigJson = nextConfigJson;
|
|
1249
|
+
}
|
|
1250
|
+
async removeMcpServersForThread(threadId) {
|
|
1251
|
+
if (!this.mcpServersByThreadId.has(threadId)) {
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
this.mcpServersByThreadId.delete(threadId);
|
|
1255
|
+
await this.writeMergedMcpServersConfig();
|
|
1256
|
+
}
|
|
1257
|
+
async applyMcpServers(threadId, mcpServers) {
|
|
1258
|
+
const previousConfig = this.mcpServersByThreadId.get(threadId);
|
|
1259
|
+
const hadPreviousConfig = this.mcpServersByThreadId.has(threadId);
|
|
1260
|
+
const threadConfig = toCodexMcpConfigMap(mcpServers);
|
|
1261
|
+
if (Object.keys(threadConfig).length === 0) {
|
|
1262
|
+
this.mcpServersByThreadId.delete(threadId);
|
|
1263
|
+
}
|
|
1264
|
+
else {
|
|
1265
|
+
this.mcpServersByThreadId.set(threadId, threadConfig);
|
|
1266
|
+
}
|
|
1267
|
+
try {
|
|
1268
|
+
await this.writeMergedMcpServersConfig();
|
|
1269
|
+
}
|
|
1270
|
+
catch (error) {
|
|
1271
|
+
if (hadPreviousConfig && previousConfig) {
|
|
1272
|
+
this.mcpServersByThreadId.set(threadId, previousConfig);
|
|
1273
|
+
}
|
|
1274
|
+
else {
|
|
1275
|
+
this.mcpServersByThreadId.delete(threadId);
|
|
1276
|
+
}
|
|
1277
|
+
throw error;
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
buildConfigOptions(session) {
|
|
1281
|
+
const executionPresets = this.getExecutionPresets(session);
|
|
1282
|
+
const currentExecutionPreset = this.resolveExecutionPresetId(session, executionPresets);
|
|
1283
|
+
const configOptions = [
|
|
1284
|
+
createModelConfigOption(session.defaults.model, this.modelCatalog),
|
|
1285
|
+
createCollaborationModeConfigOption(session.defaults.collaborationMode, this.collaborationModes),
|
|
1286
|
+
createExecutionModeConfigOption(currentExecutionPreset, executionPresets),
|
|
1287
|
+
];
|
|
1288
|
+
const reasoningOption = createReasoningEffortConfigOption(session.defaults.reasoningEffort, this.modelCatalog, session.defaults.model);
|
|
1289
|
+
if (reasoningOption) {
|
|
1290
|
+
configOptions.push(reasoningOption);
|
|
1291
|
+
}
|
|
1292
|
+
return configOptions;
|
|
1293
|
+
}
|
|
1294
|
+
requireApprovalPolicy(approvalPolicy, source) {
|
|
1295
|
+
if (!isNonEmptyString(approvalPolicy)) {
|
|
1296
|
+
throw new Error(`Codex ${source} response did not include approvalPolicy`);
|
|
1297
|
+
}
|
|
1298
|
+
return approvalPolicy;
|
|
1299
|
+
}
|
|
1300
|
+
resolveSessionModel(model, fallbackModel) {
|
|
1301
|
+
return isNonEmptyString(model) ? model : fallbackModel;
|
|
1302
|
+
}
|
|
1303
|
+
resolveSandboxPolicy(sandbox, cwd) {
|
|
1304
|
+
return isRecord(sandbox) ? sandbox : createWorkspaceWriteSandboxPolicy(cwd);
|
|
1305
|
+
}
|
|
1306
|
+
resolveDefaultCollaborationMode() {
|
|
1307
|
+
if (this.collaborationModes.length === 0) {
|
|
1308
|
+
throw new Error('Codex collaborationMode/list returned no modes');
|
|
1309
|
+
}
|
|
1310
|
+
const preferredDefault = this.collaborationModes.find((entry) => entry.mode === 'default');
|
|
1311
|
+
const firstMode = this.collaborationModes[0]?.mode;
|
|
1312
|
+
if (!firstMode) {
|
|
1313
|
+
throw new Error('Codex collaborationMode/list returned an invalid mode entry');
|
|
1314
|
+
}
|
|
1315
|
+
return preferredDefault?.mode ?? firstMode;
|
|
1316
|
+
}
|
|
1317
|
+
resolveTurnCollaborationMode(session) {
|
|
1318
|
+
const modeEntry = this.collaborationModes.find((entry) => entry.mode === session.defaults.collaborationMode);
|
|
1319
|
+
return {
|
|
1320
|
+
mode: modeEntry?.mode ?? session.defaults.collaborationMode,
|
|
1321
|
+
settings: {
|
|
1322
|
+
model: modeEntry?.model ?? session.defaults.model,
|
|
1323
|
+
reasoning_effort: modeEntry?.reasoningEffort ?? session.defaults.reasoningEffort,
|
|
1324
|
+
developer_instructions: modeEntry?.developerInstructions ?? null,
|
|
1325
|
+
},
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
resolveReasoningEffortForModel(modelId, candidateReasoningEffort) {
|
|
1329
|
+
const modelEntry = this.modelCatalog.find((entry) => entry.id === modelId);
|
|
1330
|
+
if (!modelEntry) {
|
|
1331
|
+
return null;
|
|
1332
|
+
}
|
|
1333
|
+
const supportedEntries = modelEntry.supportedReasoningEfforts.filter((entry) => isNonEmptyString(entry.reasoningEffort));
|
|
1334
|
+
if (supportedEntries.length === 0) {
|
|
1335
|
+
return null;
|
|
1336
|
+
}
|
|
1337
|
+
const supportedValues = dedupeStrings(supportedEntries.map((entry) => entry.reasoningEffort));
|
|
1338
|
+
if (isNonEmptyString(candidateReasoningEffort) &&
|
|
1339
|
+
supportedValues.includes(candidateReasoningEffort)) {
|
|
1340
|
+
return candidateReasoningEffort;
|
|
1341
|
+
}
|
|
1342
|
+
if (supportedValues.includes(modelEntry.defaultReasoningEffort)) {
|
|
1343
|
+
return modelEntry.defaultReasoningEffort;
|
|
1344
|
+
}
|
|
1345
|
+
return supportedValues[0] ?? null;
|
|
1346
|
+
}
|
|
1347
|
+
getCollaborationModeValues(currentMode) {
|
|
1348
|
+
const values = this.collaborationModes
|
|
1349
|
+
.map((entry) => entry.mode)
|
|
1350
|
+
.filter((mode) => isNonEmptyString(mode));
|
|
1351
|
+
if (!values.includes(currentMode)) {
|
|
1352
|
+
values.unshift(currentMode);
|
|
1353
|
+
}
|
|
1354
|
+
return dedupeStrings(values);
|
|
1355
|
+
}
|
|
1356
|
+
resolveCurrentSandboxMode(session) {
|
|
1357
|
+
const currentSandboxMode = parseSandboxModeFromPolicy(session.defaults.sandboxPolicy);
|
|
1358
|
+
if (currentSandboxMode) {
|
|
1359
|
+
return currentSandboxMode;
|
|
1360
|
+
}
|
|
1361
|
+
const allowedSandboxMode = this.allowedSandboxModes[0];
|
|
1362
|
+
if (allowedSandboxMode) {
|
|
1363
|
+
return allowedSandboxMode;
|
|
1364
|
+
}
|
|
1365
|
+
throw new Error('Unable to resolve current sandbox mode for execution-mode options');
|
|
1366
|
+
}
|
|
1367
|
+
getExecutionPresets(session) {
|
|
1368
|
+
const currentSandboxMode = this.resolveCurrentSandboxMode(session);
|
|
1369
|
+
const policies = this.allowedApprovalPolicies.length > 0
|
|
1370
|
+
? this.allowedApprovalPolicies
|
|
1371
|
+
: [session.defaults.approvalPolicy];
|
|
1372
|
+
const sandboxModes = this.allowedSandboxModes.length > 0 ? this.allowedSandboxModes : [currentSandboxMode];
|
|
1373
|
+
const presets = [];
|
|
1374
|
+
const seen = new Set();
|
|
1375
|
+
const addPreset = (approvalPolicy, sandboxMode, description) => {
|
|
1376
|
+
const id = toExecutionPresetId(approvalPolicy, sandboxMode);
|
|
1377
|
+
if (seen.has(id)) {
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
seen.add(id);
|
|
1381
|
+
presets.push({
|
|
1382
|
+
id,
|
|
1383
|
+
name: formatExecutionPresetName(approvalPolicy, sandboxMode),
|
|
1384
|
+
...(description ? { description } : {}),
|
|
1385
|
+
approvalPolicy,
|
|
1386
|
+
sandboxMode,
|
|
1387
|
+
});
|
|
1388
|
+
};
|
|
1389
|
+
addPreset(session.defaults.approvalPolicy, currentSandboxMode, `Current session: ${formatExecutionPresetName(session.defaults.approvalPolicy, currentSandboxMode)}`);
|
|
1390
|
+
for (const policy of policies) {
|
|
1391
|
+
for (const sandboxMode of sandboxModes) {
|
|
1392
|
+
addPreset(policy, sandboxMode);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
return presets;
|
|
1396
|
+
}
|
|
1397
|
+
resolveExecutionPresetId(session, presets) {
|
|
1398
|
+
const currentSandboxMode = this.resolveCurrentSandboxMode(session);
|
|
1399
|
+
const currentPresetId = toExecutionPresetId(session.defaults.approvalPolicy, currentSandboxMode);
|
|
1400
|
+
const fallbackPresetId = presets[0]?.id;
|
|
1401
|
+
if (!fallbackPresetId) {
|
|
1402
|
+
throw new Error('Execution-mode presets are unavailable for this session');
|
|
1403
|
+
}
|
|
1404
|
+
return presets.some((preset) => preset.id === currentPresetId)
|
|
1405
|
+
? currentPresetId
|
|
1406
|
+
: fallbackPresetId;
|
|
1407
|
+
}
|
|
1408
|
+
requireSession(sessionId) {
|
|
1409
|
+
const session = this.sessions.get(sessionId);
|
|
1410
|
+
if (!session) {
|
|
1411
|
+
throw RequestError.invalidParams({ sessionId });
|
|
1412
|
+
}
|
|
1413
|
+
return session;
|
|
1414
|
+
}
|
|
1415
|
+
async loadConfigRequirements() {
|
|
1416
|
+
const raw = await this.codex.request('configRequirements/read', undefined);
|
|
1417
|
+
const parsed = configRequirementsReadResponseSchema.parse(raw);
|
|
1418
|
+
const requirements = parsed.requirements;
|
|
1419
|
+
const allowedApprovalPolicies = dedupeStrings((requirements?.allowedApprovalPolicies ?? []).filter(isNonEmptyString));
|
|
1420
|
+
const allowedSandboxModes = dedupeStrings((requirements?.allowedSandboxModes ?? []).filter((mode) => mode === 'read-only' || mode === 'workspace-write' || mode === 'danger-full-access'));
|
|
1421
|
+
const hasExplicitApprovalPolicies = Array.isArray(requirements?.allowedApprovalPolicies);
|
|
1422
|
+
const hasExplicitSandboxModes = Array.isArray(requirements?.allowedSandboxModes);
|
|
1423
|
+
return {
|
|
1424
|
+
allowedApprovalPolicies: allowedApprovalPolicies.length > 0
|
|
1425
|
+
? allowedApprovalPolicies
|
|
1426
|
+
: hasExplicitApprovalPolicies
|
|
1427
|
+
? []
|
|
1428
|
+
: DEFAULT_APPROVAL_POLICIES,
|
|
1429
|
+
allowedSandboxModes: allowedSandboxModes.length > 0
|
|
1430
|
+
? allowedSandboxModes
|
|
1431
|
+
: hasExplicitSandboxModes
|
|
1432
|
+
? []
|
|
1433
|
+
: DEFAULT_SANDBOX_MODES,
|
|
1434
|
+
};
|
|
1435
|
+
}
|
|
1436
|
+
async loadCollaborationModes() {
|
|
1437
|
+
const entries = [];
|
|
1438
|
+
let cursor = null;
|
|
1439
|
+
for (;;) {
|
|
1440
|
+
const response = await this.requestCollaborationModeListWithRetry(cursor);
|
|
1441
|
+
entries.push(...response.data.flatMap((entry) => isNonEmptyString(entry.mode)
|
|
1442
|
+
? [
|
|
1443
|
+
{
|
|
1444
|
+
mode: entry.mode,
|
|
1445
|
+
name: entry.name,
|
|
1446
|
+
model: entry.model ?? null,
|
|
1447
|
+
reasoningEffort: entry.reasoning_effort ?? null,
|
|
1448
|
+
developerInstructions: entry.developer_instructions ?? null,
|
|
1449
|
+
},
|
|
1450
|
+
]
|
|
1451
|
+
: []));
|
|
1452
|
+
cursor = response.nextCursor ?? null;
|
|
1453
|
+
if (!cursor) {
|
|
1454
|
+
break;
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
if (entries.length === 0) {
|
|
1458
|
+
throw new Error('Codex collaborationMode/list returned no modes');
|
|
1459
|
+
}
|
|
1460
|
+
return entries;
|
|
1461
|
+
}
|
|
1462
|
+
async requestCollaborationModeListWithRetry(cursor, maxAttempts = 3) {
|
|
1463
|
+
let attempt = 0;
|
|
1464
|
+
let lastError;
|
|
1465
|
+
while (attempt < maxAttempts) {
|
|
1466
|
+
attempt += 1;
|
|
1467
|
+
try {
|
|
1468
|
+
const raw = await this.codex.request('collaborationMode/list', cursor ? { cursor } : {});
|
|
1469
|
+
return collaborationModeListResponseSchema.parse(raw);
|
|
1470
|
+
}
|
|
1471
|
+
catch (error) {
|
|
1472
|
+
lastError = error;
|
|
1473
|
+
if (!(error instanceof CodexRequestError) ||
|
|
1474
|
+
error.code !== -32_001 ||
|
|
1475
|
+
attempt >= maxAttempts) {
|
|
1476
|
+
throw error;
|
|
1477
|
+
}
|
|
1478
|
+
const delayMs = Math.round(2 ** attempt * 100 + Math.random() * 120);
|
|
1479
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
throw lastError;
|
|
1483
|
+
}
|
|
1484
|
+
async loadModelCatalog() {
|
|
1485
|
+
const models = [];
|
|
1486
|
+
let cursor = null;
|
|
1487
|
+
for (;;) {
|
|
1488
|
+
const response = await this.requestModelListWithRetry(cursor);
|
|
1489
|
+
models.push(...response.data.map((model) => ({
|
|
1490
|
+
id: model.id,
|
|
1491
|
+
displayName: model.displayName,
|
|
1492
|
+
description: model.description,
|
|
1493
|
+
defaultReasoningEffort: model.defaultReasoningEffort,
|
|
1494
|
+
supportedReasoningEfforts: (model.supportedReasoningEfforts ?? [])
|
|
1495
|
+
.filter((entry) => isNonEmptyString(entry.reasoningEffort))
|
|
1496
|
+
.map((entry) => ({
|
|
1497
|
+
reasoningEffort: entry.reasoningEffort,
|
|
1498
|
+
description: entry.description,
|
|
1499
|
+
})),
|
|
1500
|
+
inputModalities: model.inputModalities ?? [],
|
|
1501
|
+
isDefault: model.isDefault ?? false,
|
|
1502
|
+
})));
|
|
1503
|
+
cursor = response.nextCursor ?? null;
|
|
1504
|
+
if (!cursor) {
|
|
1505
|
+
return models;
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
async requestModelListWithRetry(cursor, maxAttempts = 3) {
|
|
1510
|
+
let attempt = 0;
|
|
1511
|
+
let lastError;
|
|
1512
|
+
while (attempt < maxAttempts) {
|
|
1513
|
+
attempt += 1;
|
|
1514
|
+
try {
|
|
1515
|
+
const raw = await this.codex.request('model/list', cursor ? { cursor } : {});
|
|
1516
|
+
return modelListResponseSchema.parse(raw);
|
|
1517
|
+
}
|
|
1518
|
+
catch (error) {
|
|
1519
|
+
lastError = error;
|
|
1520
|
+
if (!(error instanceof CodexRequestError) ||
|
|
1521
|
+
error.code !== -32_001 ||
|
|
1522
|
+
attempt >= maxAttempts) {
|
|
1523
|
+
throw error;
|
|
1524
|
+
}
|
|
1525
|
+
const delayMs = Math.round(2 ** attempt * 100 + Math.random() * 120);
|
|
1526
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
throw lastError;
|
|
1530
|
+
}
|
|
1531
|
+
async replayThreadHistory(sessionId, threadId) {
|
|
1532
|
+
const session = this.requireSession(sessionId);
|
|
1533
|
+
const threadReadRaw = await this.codex.request('thread/read', {
|
|
1534
|
+
threadId,
|
|
1535
|
+
includeTurns: true,
|
|
1536
|
+
});
|
|
1537
|
+
const threadRead = threadReadResponseSchema.parse(threadReadRaw);
|
|
1538
|
+
for (const turn of threadRead.thread.turns) {
|
|
1539
|
+
for (const item of turn.items) {
|
|
1540
|
+
await this.replayThreadHistoryItem(session, sessionId, turn.id, item);
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
async replayThreadHistoryItem(session, sessionId, turnId, item) {
|
|
1545
|
+
if (item.type === 'userMessage') {
|
|
1546
|
+
await this.replayUserMessageHistoryItem(sessionId, item);
|
|
1547
|
+
return;
|
|
1548
|
+
}
|
|
1549
|
+
if (item.type === 'agentMessage') {
|
|
1550
|
+
await this.replayAgentMessageHistoryItem(sessionId, item);
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
await this.replayToolLikeHistoryItem(session, sessionId, turnId, item);
|
|
1554
|
+
}
|
|
1555
|
+
async replayUserMessageHistoryItem(sessionId, item) {
|
|
1556
|
+
const contentBlocks = Array.isArray(item.content) ? item.content : [];
|
|
1557
|
+
for (const content of contentBlocks) {
|
|
1558
|
+
if (!isRecord(content) || content.type !== 'text' || typeof content.text !== 'string') {
|
|
1559
|
+
continue;
|
|
1560
|
+
}
|
|
1561
|
+
await this.emitSessionUpdate(sessionId, {
|
|
1562
|
+
sessionUpdate: 'user_message_chunk',
|
|
1563
|
+
content: { type: 'text', text: content.text },
|
|
1564
|
+
});
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
async replayAgentMessageHistoryItem(sessionId, item) {
|
|
1568
|
+
const text = asString(item.text);
|
|
1569
|
+
if (!text) {
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1572
|
+
await this.emitSessionUpdate(sessionId, {
|
|
1573
|
+
sessionUpdate: 'agent_message_chunk',
|
|
1574
|
+
content: { type: 'text', text },
|
|
1575
|
+
});
|
|
1576
|
+
}
|
|
1577
|
+
async replayToolLikeHistoryItem(session, sessionId, turnId, item) {
|
|
1578
|
+
if (item.type === 'reasoning') {
|
|
1579
|
+
await this.emitSessionUpdate(sessionId, {
|
|
1580
|
+
sessionUpdate: 'tool_call',
|
|
1581
|
+
toolCallId: resolveToolCallId({ itemId: item.id, source: item }),
|
|
1582
|
+
title: 'reasoning',
|
|
1583
|
+
kind: 'think',
|
|
1584
|
+
status: 'completed',
|
|
1585
|
+
rawInput: item,
|
|
1586
|
+
rawOutput: item,
|
|
1587
|
+
});
|
|
1588
|
+
await this.emitReasoningThoughtChunkFromItem(sessionId, item);
|
|
1589
|
+
session.replayedTurnItemKeys.add(toTurnItemKey(turnId, item.id));
|
|
1590
|
+
return;
|
|
1591
|
+
}
|
|
1592
|
+
const toolInfo = this.buildToolCallState(session, item, turnId);
|
|
1593
|
+
if (!toolInfo) {
|
|
1594
|
+
this.reportShapeDrift('unhandled_replay_item', {
|
|
1595
|
+
turnId,
|
|
1596
|
+
itemType: item.type,
|
|
1597
|
+
itemId: item.id,
|
|
1598
|
+
});
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1601
|
+
await this.emitSessionUpdate(sessionId, {
|
|
1602
|
+
sessionUpdate: 'tool_call',
|
|
1603
|
+
toolCallId: toolInfo.toolCallId,
|
|
1604
|
+
title: toolInfo.title,
|
|
1605
|
+
kind: toolInfo.kind,
|
|
1606
|
+
status: 'completed',
|
|
1607
|
+
rawInput: item,
|
|
1608
|
+
rawOutput: item,
|
|
1609
|
+
});
|
|
1610
|
+
session.replayedTurnItemKeys.add(toTurnItemKey(turnId, item.id));
|
|
1611
|
+
}
|
|
1612
|
+
async handleCodexNotification(method, params) {
|
|
1613
|
+
const parsed = knownCodexNotificationSchema.safeParse({ method, params });
|
|
1614
|
+
if (!parsed.success) {
|
|
1615
|
+
this.reportShapeDrift('malformed_notification', {
|
|
1616
|
+
method,
|
|
1617
|
+
issues: parsed.error.issues.slice(0, 3).map((issue) => issue.message),
|
|
1618
|
+
});
|
|
1619
|
+
return;
|
|
1620
|
+
}
|
|
1621
|
+
const notification = parsed.data;
|
|
1622
|
+
if (notification.method === 'error' || notification.method === 'turn/started') {
|
|
1623
|
+
return;
|
|
1624
|
+
}
|
|
1625
|
+
const sessionId = this.sessionIdByThreadId.get(notification.params.threadId);
|
|
1626
|
+
if (!sessionId) {
|
|
1627
|
+
return;
|
|
1628
|
+
}
|
|
1629
|
+
const session = this.sessions.get(sessionId);
|
|
1630
|
+
if (!session) {
|
|
1631
|
+
return;
|
|
1632
|
+
}
|
|
1633
|
+
if (notification.method === 'item/agentMessage/delta') {
|
|
1634
|
+
await this.emitSessionUpdate(sessionId, {
|
|
1635
|
+
sessionUpdate: 'agent_message_chunk',
|
|
1636
|
+
content: {
|
|
1637
|
+
type: 'text',
|
|
1638
|
+
text: notification.params.delta,
|
|
1639
|
+
},
|
|
1640
|
+
});
|
|
1641
|
+
return;
|
|
1642
|
+
}
|
|
1643
|
+
if (notification.method === 'item/plan/delta') {
|
|
1644
|
+
await this.handlePlanDelta(sessionId, session, notification.params.itemId, notification.params.delta);
|
|
1645
|
+
return;
|
|
1646
|
+
}
|
|
1647
|
+
if (notification.method === 'item/reasoning/summaryTextDelta') {
|
|
1648
|
+
await this.emitReasoningThoughtDelta(sessionId, session, notification.params.turnId, notification.params.itemId, notification.params.delta);
|
|
1649
|
+
return;
|
|
1650
|
+
}
|
|
1651
|
+
if (notification.method === 'item/commandExecution/outputDelta') {
|
|
1652
|
+
await this.emitToolCallProgress(sessionId, session, notification.params.turnId, notification.params.itemId, notification.params.delta);
|
|
1653
|
+
return;
|
|
1654
|
+
}
|
|
1655
|
+
if (notification.method === 'item/fileChange/outputDelta') {
|
|
1656
|
+
await this.emitToolCallProgress(sessionId, session, notification.params.turnId, notification.params.itemId, notification.params.delta);
|
|
1657
|
+
return;
|
|
1658
|
+
}
|
|
1659
|
+
if (notification.method === 'item/mcpToolCall/progress') {
|
|
1660
|
+
await this.emitToolCallProgress(sessionId, session, notification.params.turnId, notification.params.itemId, notification.params.message);
|
|
1661
|
+
return;
|
|
1662
|
+
}
|
|
1663
|
+
if (notification.method === 'item/started') {
|
|
1664
|
+
await this.handleItemStarted(session, notification.params.item, notification.params.turnId);
|
|
1665
|
+
return;
|
|
1666
|
+
}
|
|
1667
|
+
if (notification.method === 'item/completed') {
|
|
1668
|
+
await this.handleItemCompleted(session, notification.params.item, notification.params.turnId);
|
|
1669
|
+
return;
|
|
1670
|
+
}
|
|
1671
|
+
if (notification.method === 'turn/completed') {
|
|
1672
|
+
await this.handleTurnCompletedNotification(session, notification.params.turn.id, notification.params.turn.status, notification.params.turn.error?.message);
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
async handlePlanDelta(sessionId, session, itemId, delta) {
|
|
1676
|
+
const previous = session.planTextByItemId.get(itemId) ?? '';
|
|
1677
|
+
const next = `${previous}${delta}`;
|
|
1678
|
+
session.planTextByItemId.set(itemId, next);
|
|
1679
|
+
await this.emitSessionUpdate(sessionId, {
|
|
1680
|
+
sessionUpdate: 'plan',
|
|
1681
|
+
entries: [
|
|
1682
|
+
{
|
|
1683
|
+
content: next,
|
|
1684
|
+
priority: 'medium',
|
|
1685
|
+
status: 'in_progress',
|
|
1686
|
+
},
|
|
1687
|
+
],
|
|
1688
|
+
});
|
|
1689
|
+
}
|
|
1690
|
+
async emitReasoningThoughtChunkFromItem(sessionId, item) {
|
|
1691
|
+
const text = extractReasoningTextLocal(item);
|
|
1692
|
+
if (!text) {
|
|
1693
|
+
return;
|
|
1694
|
+
}
|
|
1695
|
+
await this.emitSessionUpdate(sessionId, {
|
|
1696
|
+
sessionUpdate: 'agent_thought_chunk',
|
|
1697
|
+
content: { type: 'text', text },
|
|
1698
|
+
});
|
|
1699
|
+
}
|
|
1700
|
+
isReplayedTurnItem(session, turnId, itemId) {
|
|
1701
|
+
return session.replayedTurnItemKeys.has(toTurnItemKey(turnId, itemId));
|
|
1702
|
+
}
|
|
1703
|
+
async emitReasoningThoughtDelta(sessionId, session, turnId, itemId, delta) {
|
|
1704
|
+
if (this.isReplayedTurnItem(session, turnId, itemId)) {
|
|
1705
|
+
return;
|
|
1706
|
+
}
|
|
1707
|
+
if (delta.length === 0) {
|
|
1708
|
+
return;
|
|
1709
|
+
}
|
|
1710
|
+
const existing = session.toolCallsByItemId.get(itemId);
|
|
1711
|
+
if (!existing) {
|
|
1712
|
+
const toolCallId = resolveToolCallId({
|
|
1713
|
+
itemId,
|
|
1714
|
+
});
|
|
1715
|
+
session.toolCallsByItemId.set(itemId, {
|
|
1716
|
+
toolCallId,
|
|
1717
|
+
title: 'reasoning',
|
|
1718
|
+
kind: 'think',
|
|
1719
|
+
locations: [],
|
|
1720
|
+
});
|
|
1721
|
+
await this.emitSessionUpdate(sessionId, {
|
|
1722
|
+
sessionUpdate: 'tool_call',
|
|
1723
|
+
toolCallId,
|
|
1724
|
+
title: 'reasoning',
|
|
1725
|
+
kind: 'think',
|
|
1726
|
+
status: 'pending',
|
|
1727
|
+
rawInput: {
|
|
1728
|
+
type: 'reasoning',
|
|
1729
|
+
id: itemId,
|
|
1730
|
+
},
|
|
1731
|
+
});
|
|
1732
|
+
await this.emitSessionUpdate(sessionId, {
|
|
1733
|
+
sessionUpdate: 'tool_call_update',
|
|
1734
|
+
toolCallId,
|
|
1735
|
+
title: 'reasoning',
|
|
1736
|
+
kind: 'think',
|
|
1737
|
+
status: 'in_progress',
|
|
1738
|
+
});
|
|
1739
|
+
}
|
|
1740
|
+
session.reasoningDeltaItemIds.add(itemId);
|
|
1741
|
+
await this.emitSessionUpdate(sessionId, {
|
|
1742
|
+
sessionUpdate: 'agent_thought_chunk',
|
|
1743
|
+
content: { type: 'text', text: delta },
|
|
1744
|
+
});
|
|
1745
|
+
}
|
|
1746
|
+
async emitToolCallProgress(sessionId, session, turnId, itemId, output) {
|
|
1747
|
+
if (this.isReplayedTurnItem(session, turnId, itemId)) {
|
|
1748
|
+
return;
|
|
1749
|
+
}
|
|
1750
|
+
const toolCall = session.toolCallsByItemId.get(itemId);
|
|
1751
|
+
if (!toolCall) {
|
|
1752
|
+
this.reportShapeDrift('tool_progress_without_tool_call', { turnId, itemId });
|
|
1753
|
+
return;
|
|
1754
|
+
}
|
|
1755
|
+
await this.emitSessionUpdate(sessionId, {
|
|
1756
|
+
sessionUpdate: 'tool_call_update',
|
|
1757
|
+
toolCallId: toolCall.toolCallId,
|
|
1758
|
+
status: 'in_progress',
|
|
1759
|
+
...(toolCall.locations.length > 0 ? { locations: toolCall.locations } : {}),
|
|
1760
|
+
rawOutput: output,
|
|
1761
|
+
});
|
|
1762
|
+
}
|
|
1763
|
+
async handleTurnCompletedNotification(session, turnId, status, errorMessage) {
|
|
1764
|
+
if (status === 'inProgress') {
|
|
1765
|
+
return;
|
|
1766
|
+
}
|
|
1767
|
+
const completionStatus = status;
|
|
1768
|
+
if (this.shouldDeferTurnCompletion(session, turnId, completionStatus)) {
|
|
1769
|
+
session.pendingTurnCompletionsByTurnId.set(turnId, this.buildPendingTurnCompletion(completionStatus, errorMessage));
|
|
1770
|
+
return;
|
|
1771
|
+
}
|
|
1772
|
+
await this.finalizeTurnCompletion(session, completionStatus, errorMessage);
|
|
1773
|
+
}
|
|
1774
|
+
shouldDeferTurnCompletion(session, turnId, status) {
|
|
1775
|
+
if (!session.activeTurn || session.activeTurn.turnId !== turnId) {
|
|
1776
|
+
return true;
|
|
1777
|
+
}
|
|
1778
|
+
return status !== 'interrupted' && this.hasPendingPlanApprovals(session, turnId);
|
|
1779
|
+
}
|
|
1780
|
+
buildPendingTurnCompletion(status, errorMessage) {
|
|
1781
|
+
const stopReason = status === 'interrupted' ? 'cancelled' : 'end_turn';
|
|
1782
|
+
if (status === 'failed' && errorMessage) {
|
|
1783
|
+
return { stopReason, errorMessage };
|
|
1784
|
+
}
|
|
1785
|
+
return { stopReason };
|
|
1786
|
+
}
|
|
1787
|
+
async finalizeTurnCompletion(session, status, errorMessage) {
|
|
1788
|
+
if (status === 'interrupted') {
|
|
1789
|
+
this.settleTurn(session, 'cancelled');
|
|
1790
|
+
return;
|
|
1791
|
+
}
|
|
1792
|
+
if (status === 'failed' && errorMessage) {
|
|
1793
|
+
await this.emitTurnFailureMessage(session.sessionId, errorMessage);
|
|
1794
|
+
}
|
|
1795
|
+
if (status === 'failed') {
|
|
1796
|
+
this.settleTurn(session, 'end_turn');
|
|
1797
|
+
return;
|
|
1798
|
+
}
|
|
1799
|
+
this.settleTurn(session, session.activeTurn?.cancelRequested ? 'cancelled' : 'end_turn');
|
|
1800
|
+
}
|
|
1801
|
+
async handleItemStarted(session, item, turnId) {
|
|
1802
|
+
if (this.isReplayedTurnItem(session, turnId, item.id)) {
|
|
1803
|
+
return;
|
|
1804
|
+
}
|
|
1805
|
+
if (item.type === 'reasoning') {
|
|
1806
|
+
const existing = session.toolCallsByItemId.get(item.id);
|
|
1807
|
+
const toolInfo = existing ??
|
|
1808
|
+
{
|
|
1809
|
+
toolCallId: resolveToolCallId({
|
|
1810
|
+
itemId: item.id,
|
|
1811
|
+
source: item,
|
|
1812
|
+
}),
|
|
1813
|
+
title: 'reasoning',
|
|
1814
|
+
kind: 'think',
|
|
1815
|
+
locations: [],
|
|
1816
|
+
};
|
|
1817
|
+
if (!existing) {
|
|
1818
|
+
session.toolCallsByItemId.set(item.id, toolInfo);
|
|
1819
|
+
await this.emitSessionUpdate(session.sessionId, {
|
|
1820
|
+
sessionUpdate: 'tool_call',
|
|
1821
|
+
toolCallId: toolInfo.toolCallId,
|
|
1822
|
+
title: toolInfo.title,
|
|
1823
|
+
kind: toolInfo.kind,
|
|
1824
|
+
status: 'pending',
|
|
1825
|
+
rawInput: item,
|
|
1826
|
+
});
|
|
1827
|
+
}
|
|
1828
|
+
await this.emitSessionUpdate(session.sessionId, {
|
|
1829
|
+
sessionUpdate: 'tool_call_update',
|
|
1830
|
+
toolCallId: toolInfo.toolCallId,
|
|
1831
|
+
title: toolInfo.title,
|
|
1832
|
+
kind: toolInfo.kind,
|
|
1833
|
+
status: 'in_progress',
|
|
1834
|
+
});
|
|
1835
|
+
return;
|
|
1836
|
+
}
|
|
1837
|
+
const toolInfo = this.buildToolCallState(session, item, turnId);
|
|
1838
|
+
if (!toolInfo) {
|
|
1839
|
+
this.reportShapeDrift('unhandled_item_started_type', {
|
|
1840
|
+
turnId,
|
|
1841
|
+
itemType: item.type,
|
|
1842
|
+
itemId: item.id,
|
|
1843
|
+
});
|
|
1844
|
+
return;
|
|
1845
|
+
}
|
|
1846
|
+
session.toolCallsByItemId.set(item.id, toolInfo);
|
|
1847
|
+
await this.emitSessionUpdate(session.sessionId, {
|
|
1848
|
+
sessionUpdate: 'tool_call',
|
|
1849
|
+
toolCallId: toolInfo.toolCallId,
|
|
1850
|
+
title: toolInfo.title,
|
|
1851
|
+
kind: toolInfo.kind,
|
|
1852
|
+
status: 'pending',
|
|
1853
|
+
...(toolInfo.locations.length > 0 ? { locations: toolInfo.locations } : {}),
|
|
1854
|
+
rawInput: item,
|
|
1855
|
+
});
|
|
1856
|
+
const itemStatus = toToolStatus(item.status);
|
|
1857
|
+
if (itemStatus === 'in_progress') {
|
|
1858
|
+
await this.emitSessionUpdate(session.sessionId, {
|
|
1859
|
+
sessionUpdate: 'tool_call_update',
|
|
1860
|
+
toolCallId: toolInfo.toolCallId,
|
|
1861
|
+
status: 'in_progress',
|
|
1862
|
+
});
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
async handleItemCompleted(session, item, turnId) {
|
|
1866
|
+
if (this.isReplayedTurnItem(session, turnId, item.id)) {
|
|
1867
|
+
return;
|
|
1868
|
+
}
|
|
1869
|
+
if (item.type === 'reasoning') {
|
|
1870
|
+
const existing = session.toolCallsByItemId.get(item.id);
|
|
1871
|
+
const reasoningToolCallId = existing?.toolCallId ??
|
|
1872
|
+
resolveToolCallId({
|
|
1873
|
+
itemId: item.id,
|
|
1874
|
+
source: item,
|
|
1875
|
+
});
|
|
1876
|
+
if (existing) {
|
|
1877
|
+
await this.emitSessionUpdate(session.sessionId, {
|
|
1878
|
+
sessionUpdate: 'tool_call_update',
|
|
1879
|
+
toolCallId: reasoningToolCallId,
|
|
1880
|
+
title: existing.title,
|
|
1881
|
+
kind: existing.kind,
|
|
1882
|
+
status: toToolStatus(item.status) ?? 'completed',
|
|
1883
|
+
rawOutput: item,
|
|
1884
|
+
});
|
|
1885
|
+
}
|
|
1886
|
+
else {
|
|
1887
|
+
await this.emitSessionUpdate(session.sessionId, {
|
|
1888
|
+
sessionUpdate: 'tool_call',
|
|
1889
|
+
toolCallId: reasoningToolCallId,
|
|
1890
|
+
title: 'reasoning',
|
|
1891
|
+
kind: 'think',
|
|
1892
|
+
status: toToolStatus(item.status) ?? 'completed',
|
|
1893
|
+
rawInput: item,
|
|
1894
|
+
rawOutput: item,
|
|
1895
|
+
});
|
|
1896
|
+
}
|
|
1897
|
+
session.toolCallsByItemId.delete(item.id);
|
|
1898
|
+
const sawDelta = session.reasoningDeltaItemIds.has(item.id);
|
|
1899
|
+
session.reasoningDeltaItemIds.delete(item.id);
|
|
1900
|
+
if (!sawDelta) {
|
|
1901
|
+
await this.emitReasoningThoughtChunkFromItem(session.sessionId, item);
|
|
1902
|
+
}
|
|
1903
|
+
return;
|
|
1904
|
+
}
|
|
1905
|
+
const existing = session.toolCallsByItemId.get(item.id);
|
|
1906
|
+
if (!existing) {
|
|
1907
|
+
this.reportShapeDrift('item_completed_without_started_state', {
|
|
1908
|
+
turnId,
|
|
1909
|
+
itemType: item.type,
|
|
1910
|
+
itemId: item.id,
|
|
1911
|
+
});
|
|
1912
|
+
return;
|
|
1913
|
+
}
|
|
1914
|
+
const shouldHoldTurnForPlanApproval = item.type === 'plan' &&
|
|
1915
|
+
isPlanLikeMode(session.defaults.collaborationMode) &&
|
|
1916
|
+
!session.planApprovalRequestedByTurnId.has(turnId) &&
|
|
1917
|
+
this.extractPlanApprovalText(session, item) !== null;
|
|
1918
|
+
if (shouldHoldTurnForPlanApproval) {
|
|
1919
|
+
this.holdTurnUntilPlanApprovalResolves(session, turnId);
|
|
1920
|
+
}
|
|
1921
|
+
const statusFromItem = toToolStatus(item.status);
|
|
1922
|
+
const status = statusFromItem ?? 'completed';
|
|
1923
|
+
const completionLocations = this.extractLocations(item);
|
|
1924
|
+
const locations = dedupeLocations([...existing.locations, ...completionLocations]);
|
|
1925
|
+
await this.emitSessionUpdate(session.sessionId, {
|
|
1926
|
+
sessionUpdate: 'tool_call_update',
|
|
1927
|
+
toolCallId: existing.toolCallId,
|
|
1928
|
+
status,
|
|
1929
|
+
kind: existing.kind,
|
|
1930
|
+
title: existing.title,
|
|
1931
|
+
...(locations.length > 0 ? { locations } : {}),
|
|
1932
|
+
rawOutput: item,
|
|
1933
|
+
});
|
|
1934
|
+
session.toolCallsByItemId.delete(item.id);
|
|
1935
|
+
await this.maybeRequestPlanApproval(session, item, turnId, existing);
|
|
1936
|
+
}
|
|
1937
|
+
buildPlanApprovalInput(planText, sourceItemId) {
|
|
1938
|
+
return {
|
|
1939
|
+
type: 'ExitPlanMode',
|
|
1940
|
+
plan: { type: 'text', text: planText },
|
|
1941
|
+
reason: 'Plan proposed. Approve to exit plan mode and continue implementation.',
|
|
1942
|
+
source: 'codex_plan_completion',
|
|
1943
|
+
sourceItemId,
|
|
1944
|
+
};
|
|
1945
|
+
}
|
|
1946
|
+
extractPlanApprovalText(session, item) {
|
|
1947
|
+
const bufferedText = session.planTextByItemId.get(item.id);
|
|
1948
|
+
if (bufferedText && bufferedText.trim().length > 0) {
|
|
1949
|
+
return bufferedText;
|
|
1950
|
+
}
|
|
1951
|
+
const fromPlanField = extractPlanTextLocal(item.plan);
|
|
1952
|
+
if (fromPlanField && fromPlanField.trim().length > 0) {
|
|
1953
|
+
return fromPlanField;
|
|
1954
|
+
}
|
|
1955
|
+
const fromTextField = extractPlanTextLocal(item.text);
|
|
1956
|
+
if (fromTextField && fromTextField.trim().length > 0) {
|
|
1957
|
+
return fromTextField;
|
|
1958
|
+
}
|
|
1959
|
+
return null;
|
|
1960
|
+
}
|
|
1961
|
+
getPlanExitModePriority(modeId) {
|
|
1962
|
+
const normalized = modeId.toLowerCase();
|
|
1963
|
+
const index = PLAN_EXIT_MODE_PREFERENCE.findIndex((preferred) => preferred.toLowerCase() === normalized);
|
|
1964
|
+
return index >= 0 ? index : PLAN_EXIT_MODE_PREFERENCE.length;
|
|
1965
|
+
}
|
|
1966
|
+
comparePlanExitModePreference(left, right) {
|
|
1967
|
+
const priorityDiff = this.getPlanExitModePriority(left) - this.getPlanExitModePriority(right);
|
|
1968
|
+
if (priorityDiff !== 0) {
|
|
1969
|
+
return priorityDiff;
|
|
1970
|
+
}
|
|
1971
|
+
return left.localeCompare(right);
|
|
1972
|
+
}
|
|
1973
|
+
resolveCollaborationModeLabel(modeId) {
|
|
1974
|
+
const entry = this.collaborationModes.find((candidate) => candidate.mode === modeId);
|
|
1975
|
+
if (entry?.name) {
|
|
1976
|
+
return entry.name;
|
|
1977
|
+
}
|
|
1978
|
+
return sanitizeModeName(modeId);
|
|
1979
|
+
}
|
|
1980
|
+
buildPlanApprovalOptions(session) {
|
|
1981
|
+
const currentMode = session.defaults.collaborationMode;
|
|
1982
|
+
const availableModes = this.getCollaborationModeValues(currentMode);
|
|
1983
|
+
const nonPlanModes = availableModes
|
|
1984
|
+
.filter((modeId) => !isPlanLikeMode(modeId))
|
|
1985
|
+
.sort((left, right) => this.comparePlanExitModePreference(left, right));
|
|
1986
|
+
const approvableModeIds = new Set(nonPlanModes);
|
|
1987
|
+
const options = nonPlanModes.map((modeId) => ({
|
|
1988
|
+
optionId: modeId,
|
|
1989
|
+
name: `Approve and switch to ${this.resolveCollaborationModeLabel(modeId)}`,
|
|
1990
|
+
kind: 'allow_once',
|
|
1991
|
+
}));
|
|
1992
|
+
options.push({
|
|
1993
|
+
optionId: currentMode,
|
|
1994
|
+
name: 'Keep planning',
|
|
1995
|
+
kind: 'reject_once',
|
|
1996
|
+
});
|
|
1997
|
+
return { options, approvableModeIds };
|
|
1998
|
+
}
|
|
1999
|
+
holdTurnUntilPlanApprovalResolves(session, turnId) {
|
|
2000
|
+
if (this.hasPendingPlanApprovals(session, turnId)) {
|
|
2001
|
+
return;
|
|
2002
|
+
}
|
|
2003
|
+
session.pendingPlanApprovalsByTurnId.set(turnId, 1);
|
|
2004
|
+
}
|
|
2005
|
+
hasPendingPlanApprovals(session, turnId) {
|
|
2006
|
+
return (session.pendingPlanApprovalsByTurnId.get(turnId) ?? 0) > 0;
|
|
2007
|
+
}
|
|
2008
|
+
async releaseTurnHoldForPlanApproval(session, turnId) {
|
|
2009
|
+
const pendingCount = session.pendingPlanApprovalsByTurnId.get(turnId) ?? 0;
|
|
2010
|
+
if (pendingCount <= 1) {
|
|
2011
|
+
session.pendingPlanApprovalsByTurnId.delete(turnId);
|
|
2012
|
+
}
|
|
2013
|
+
else {
|
|
2014
|
+
session.pendingPlanApprovalsByTurnId.set(turnId, pendingCount - 1);
|
|
2015
|
+
}
|
|
2016
|
+
if (this.hasPendingPlanApprovals(session, turnId)) {
|
|
2017
|
+
return;
|
|
2018
|
+
}
|
|
2019
|
+
if (!session.activeTurn || session.activeTurn.settled || session.activeTurn.turnId !== turnId) {
|
|
2020
|
+
return;
|
|
2021
|
+
}
|
|
2022
|
+
const deferredCompletion = session.pendingTurnCompletionsByTurnId.get(turnId);
|
|
2023
|
+
if (!deferredCompletion) {
|
|
2024
|
+
return;
|
|
2025
|
+
}
|
|
2026
|
+
session.pendingTurnCompletionsByTurnId.delete(turnId);
|
|
2027
|
+
if (deferredCompletion.errorMessage) {
|
|
2028
|
+
await this.emitTurnFailureMessage(session.sessionId, deferredCompletion.errorMessage);
|
|
2029
|
+
}
|
|
2030
|
+
this.settleTurn(session, deferredCompletion.stopReason);
|
|
2031
|
+
}
|
|
2032
|
+
async maybeRequestPlanApproval(session, item, turnId, completedPlanToolCall) {
|
|
2033
|
+
if (item.type !== 'plan') {
|
|
2034
|
+
return;
|
|
2035
|
+
}
|
|
2036
|
+
if (!isPlanLikeMode(session.defaults.collaborationMode)) {
|
|
2037
|
+
return;
|
|
2038
|
+
}
|
|
2039
|
+
if (session.planApprovalRequestedByTurnId.has(turnId)) {
|
|
2040
|
+
return;
|
|
2041
|
+
}
|
|
2042
|
+
const planText = this.extractPlanApprovalText(session, item);
|
|
2043
|
+
if (!planText) {
|
|
2044
|
+
return;
|
|
2045
|
+
}
|
|
2046
|
+
session.planApprovalRequestedByTurnId.add(turnId);
|
|
2047
|
+
this.holdTurnUntilPlanApprovalResolves(session, turnId);
|
|
2048
|
+
const approvalToolCallId = `${completedPlanToolCall.toolCallId}:exit-plan`;
|
|
2049
|
+
const approvalInput = this.buildPlanApprovalInput(planText, item.id);
|
|
2050
|
+
await this.emitSessionUpdate(session.sessionId, {
|
|
2051
|
+
sessionUpdate: 'tool_call',
|
|
2052
|
+
toolCallId: approvalToolCallId,
|
|
2053
|
+
title: 'ExitPlanMode',
|
|
2054
|
+
kind: 'switch_mode',
|
|
2055
|
+
status: 'pending',
|
|
2056
|
+
rawInput: approvalInput,
|
|
2057
|
+
});
|
|
2058
|
+
try {
|
|
2059
|
+
const { options: planApprovalOptions, approvableModeIds } = this.buildPlanApprovalOptions(session);
|
|
2060
|
+
if (approvableModeIds.size === 0) {
|
|
2061
|
+
await this.emitSessionUpdate(session.sessionId, {
|
|
2062
|
+
sessionUpdate: 'tool_call_update',
|
|
2063
|
+
toolCallId: approvalToolCallId,
|
|
2064
|
+
kind: 'switch_mode',
|
|
2065
|
+
title: 'ExitPlanMode',
|
|
2066
|
+
status: 'failed',
|
|
2067
|
+
rawOutput: 'Plan proposed, but no non-plan collaboration mode is available.',
|
|
2068
|
+
});
|
|
2069
|
+
return;
|
|
2070
|
+
}
|
|
2071
|
+
const permissionResult = await this.connection.requestPermission({
|
|
2072
|
+
sessionId: session.sessionId,
|
|
2073
|
+
toolCall: {
|
|
2074
|
+
toolCallId: approvalToolCallId,
|
|
2075
|
+
title: 'ExitPlanMode',
|
|
2076
|
+
kind: 'switch_mode',
|
|
2077
|
+
status: 'pending',
|
|
2078
|
+
rawInput: approvalInput,
|
|
2079
|
+
},
|
|
2080
|
+
options: planApprovalOptions,
|
|
2081
|
+
});
|
|
2082
|
+
const selectedMode = permissionResult.outcome.outcome === 'selected' &&
|
|
2083
|
+
approvableModeIds.has(permissionResult.outcome.optionId)
|
|
2084
|
+
? permissionResult.outcome.optionId
|
|
2085
|
+
: null;
|
|
2086
|
+
const approved = selectedMode !== null;
|
|
2087
|
+
if (selectedMode && session.defaults.collaborationMode !== selectedMode) {
|
|
2088
|
+
session.defaults.collaborationMode = selectedMode;
|
|
2089
|
+
await this.emitSessionUpdate(session.sessionId, {
|
|
2090
|
+
sessionUpdate: 'config_option_update',
|
|
2091
|
+
configOptions: this.buildConfigOptions(session),
|
|
2092
|
+
});
|
|
2093
|
+
}
|
|
2094
|
+
await this.emitSessionUpdate(session.sessionId, {
|
|
2095
|
+
sessionUpdate: 'tool_call_update',
|
|
2096
|
+
toolCallId: approvalToolCallId,
|
|
2097
|
+
kind: 'switch_mode',
|
|
2098
|
+
title: 'ExitPlanMode',
|
|
2099
|
+
status: approved ? 'completed' : 'failed',
|
|
2100
|
+
rawOutput: approved ? 'Plan approved' : 'Plan approval rejected',
|
|
2101
|
+
});
|
|
2102
|
+
}
|
|
2103
|
+
catch (error) {
|
|
2104
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2105
|
+
await this.emitSessionUpdate(session.sessionId, {
|
|
2106
|
+
sessionUpdate: 'tool_call_update',
|
|
2107
|
+
toolCallId: approvalToolCallId,
|
|
2108
|
+
kind: 'switch_mode',
|
|
2109
|
+
title: 'ExitPlanMode',
|
|
2110
|
+
status: 'failed',
|
|
2111
|
+
rawOutput: `Plan approval failed: ${message}`,
|
|
2112
|
+
});
|
|
2113
|
+
}
|
|
2114
|
+
finally {
|
|
2115
|
+
await this.releaseTurnHoldForPlanApproval(session, turnId);
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
extractLocations(item) {
|
|
2119
|
+
if (!isRecord(item) || item.type !== 'fileChange') {
|
|
2120
|
+
return [];
|
|
2121
|
+
}
|
|
2122
|
+
const changes = item.changes;
|
|
2123
|
+
if (!Array.isArray(changes)) {
|
|
2124
|
+
return [];
|
|
2125
|
+
}
|
|
2126
|
+
const locations = [];
|
|
2127
|
+
for (const change of changes) {
|
|
2128
|
+
if (!isRecord(change)) {
|
|
2129
|
+
continue;
|
|
2130
|
+
}
|
|
2131
|
+
const path = asString(change.path);
|
|
2132
|
+
if (!path) {
|
|
2133
|
+
continue;
|
|
2134
|
+
}
|
|
2135
|
+
locations.push({ path });
|
|
2136
|
+
}
|
|
2137
|
+
return locations;
|
|
2138
|
+
}
|
|
2139
|
+
buildToolCallState(session, item, _turnId) {
|
|
2140
|
+
const kindByType = {
|
|
2141
|
+
commandExecution: 'execute',
|
|
2142
|
+
fileChange: 'edit',
|
|
2143
|
+
mcpToolCall: 'fetch',
|
|
2144
|
+
webSearch: 'search',
|
|
2145
|
+
plan: 'think',
|
|
2146
|
+
};
|
|
2147
|
+
const kind = kindByType[item.type];
|
|
2148
|
+
if (!kind) {
|
|
2149
|
+
return null;
|
|
2150
|
+
}
|
|
2151
|
+
let title = item.type;
|
|
2152
|
+
let kindToEmit = kind;
|
|
2153
|
+
let locations = [];
|
|
2154
|
+
if (item.type === 'commandExecution') {
|
|
2155
|
+
const command = asString(item.command);
|
|
2156
|
+
const cwd = asString(item.cwd) ?? session.cwd;
|
|
2157
|
+
const parsed = resolveCommandDisplay({ command, cwd });
|
|
2158
|
+
title = parsed.title;
|
|
2159
|
+
kindToEmit = parsed.kind;
|
|
2160
|
+
locations = parsed.locations;
|
|
2161
|
+
}
|
|
2162
|
+
else if (item.type === 'fileChange') {
|
|
2163
|
+
title = 'fileChange';
|
|
2164
|
+
locations = this.extractLocations(item);
|
|
2165
|
+
}
|
|
2166
|
+
else if (item.type === 'mcpToolCall') {
|
|
2167
|
+
const server = asString(item.server) ?? 'mcp';
|
|
2168
|
+
const tool = asString(item.tool) ?? 'tool';
|
|
2169
|
+
title = `mcpToolCall:${server}/${tool}`;
|
|
2170
|
+
}
|
|
2171
|
+
else if (item.type === 'webSearch') {
|
|
2172
|
+
const query = asString(item.query);
|
|
2173
|
+
title = query ? `webSearch:${query}` : 'webSearch';
|
|
2174
|
+
}
|
|
2175
|
+
return {
|
|
2176
|
+
toolCallId: resolveToolCallId({
|
|
2177
|
+
itemId: item.id,
|
|
2178
|
+
source: item,
|
|
2179
|
+
}),
|
|
2180
|
+
kind: kindToEmit,
|
|
2181
|
+
title,
|
|
2182
|
+
locations,
|
|
2183
|
+
};
|
|
2184
|
+
}
|
|
2185
|
+
buildPermissionOptions(params) {
|
|
2186
|
+
if (params.method === 'item/tool/requestUserInput') {
|
|
2187
|
+
return buildToolUserInputPermissionOptions(params.questions);
|
|
2188
|
+
}
|
|
2189
|
+
if (params.method === 'item/commandExecution/requestApproval') {
|
|
2190
|
+
return [
|
|
2191
|
+
{
|
|
2192
|
+
optionId: 'allow_always',
|
|
2193
|
+
name: 'Allow for session',
|
|
2194
|
+
kind: 'allow_always',
|
|
2195
|
+
},
|
|
2196
|
+
{
|
|
2197
|
+
optionId: 'allow_once',
|
|
2198
|
+
name: 'Allow once',
|
|
2199
|
+
kind: 'allow_once',
|
|
2200
|
+
},
|
|
2201
|
+
{
|
|
2202
|
+
optionId: 'reject_once',
|
|
2203
|
+
name: 'Reject',
|
|
2204
|
+
kind: 'reject_once',
|
|
2205
|
+
},
|
|
2206
|
+
];
|
|
2207
|
+
}
|
|
2208
|
+
return [
|
|
2209
|
+
{
|
|
2210
|
+
optionId: 'allow_once',
|
|
2211
|
+
name: 'Allow once',
|
|
2212
|
+
kind: 'allow_once',
|
|
2213
|
+
},
|
|
2214
|
+
{
|
|
2215
|
+
optionId: 'reject_once',
|
|
2216
|
+
name: 'Reject',
|
|
2217
|
+
kind: 'reject_once',
|
|
2218
|
+
},
|
|
2219
|
+
];
|
|
2220
|
+
}
|
|
2221
|
+
async maybeAutoApproveCommandRequest(params) {
|
|
2222
|
+
if (params.request.method !== 'item/commandExecution/requestApproval') {
|
|
2223
|
+
return false;
|
|
2224
|
+
}
|
|
2225
|
+
const scopeKey = buildCommandApprovalScopeKey({
|
|
2226
|
+
command: asString(params.request.params.command),
|
|
2227
|
+
cwd: asString(params.request.params.cwd) ?? params.session.cwd,
|
|
2228
|
+
});
|
|
2229
|
+
if (!(scopeKey && params.session.commandApprovalScopes.has(scopeKey))) {
|
|
2230
|
+
return false;
|
|
2231
|
+
}
|
|
2232
|
+
await this.respondToCodexPermissionRequest({
|
|
2233
|
+
request: params.request,
|
|
2234
|
+
permission: {
|
|
2235
|
+
outcome: { outcome: 'selected', optionId: 'allow_always' },
|
|
2236
|
+
},
|
|
2237
|
+
session: params.session,
|
|
2238
|
+
toolCallId: params.toolCallId,
|
|
2239
|
+
questions: [],
|
|
2240
|
+
});
|
|
2241
|
+
return true;
|
|
2242
|
+
}
|
|
2243
|
+
async handleCodexServerRequest(request) {
|
|
2244
|
+
try {
|
|
2245
|
+
const parsed = knownCodexServerRequestSchema.safeParse(request);
|
|
2246
|
+
if (!parsed.success) {
|
|
2247
|
+
this.reportShapeDrift('malformed_server_request', {
|
|
2248
|
+
method: request.method,
|
|
2249
|
+
issues: parsed.error.issues.slice(0, 3).map((issue) => issue.message),
|
|
2250
|
+
});
|
|
2251
|
+
this.codex.respondError(request.id, {
|
|
2252
|
+
code: -32_602,
|
|
2253
|
+
message: 'Unsupported codex server request payload',
|
|
2254
|
+
});
|
|
2255
|
+
return;
|
|
2256
|
+
}
|
|
2257
|
+
const typed = parsed.data;
|
|
2258
|
+
const sessionId = this.sessionIdByThreadId.get(typed.params.threadId);
|
|
2259
|
+
if (!sessionId) {
|
|
2260
|
+
this.reportShapeDrift('request_for_unknown_thread', {
|
|
2261
|
+
method: typed.method,
|
|
2262
|
+
threadId: typed.params.threadId,
|
|
2263
|
+
itemId: typed.params.itemId,
|
|
2264
|
+
});
|
|
2265
|
+
this.codex.respondError(typed.id, {
|
|
2266
|
+
code: -32_603,
|
|
2267
|
+
message: 'No ACP session mapped for this thread',
|
|
2268
|
+
});
|
|
2269
|
+
return;
|
|
2270
|
+
}
|
|
2271
|
+
const session = this.sessions.get(sessionId);
|
|
2272
|
+
if (!session) {
|
|
2273
|
+
this.reportShapeDrift('request_for_missing_session', {
|
|
2274
|
+
method: typed.method,
|
|
2275
|
+
sessionId,
|
|
2276
|
+
itemId: typed.params.itemId,
|
|
2277
|
+
});
|
|
2278
|
+
this.codex.respondError(typed.id, {
|
|
2279
|
+
code: -32_603,
|
|
2280
|
+
message: 'No ACP session mapped for this thread',
|
|
2281
|
+
});
|
|
2282
|
+
return;
|
|
2283
|
+
}
|
|
2284
|
+
const existingTool = session.toolCallsByItemId.get(typed.params.itemId);
|
|
2285
|
+
const toolCall = existingTool ??
|
|
2286
|
+
this.buildApprovalToolCallState(session, typed.method, typed.params.itemId, typed.params.turnId, typed.params);
|
|
2287
|
+
session.toolCallsByItemId.set(typed.params.itemId, toolCall);
|
|
2288
|
+
if (!existingTool) {
|
|
2289
|
+
await this.emitSessionUpdate(sessionId, {
|
|
2290
|
+
sessionUpdate: 'tool_call',
|
|
2291
|
+
toolCallId: toolCall.toolCallId,
|
|
2292
|
+
title: toolCall.title,
|
|
2293
|
+
kind: toolCall.kind,
|
|
2294
|
+
status: 'pending',
|
|
2295
|
+
...(toolCall.locations.length > 0 ? { locations: toolCall.locations } : {}),
|
|
2296
|
+
rawInput: typed.params,
|
|
2297
|
+
});
|
|
2298
|
+
}
|
|
2299
|
+
if (await this.maybeAutoApproveCommandRequest({
|
|
2300
|
+
request: typed,
|
|
2301
|
+
session,
|
|
2302
|
+
toolCallId: toolCall.toolCallId,
|
|
2303
|
+
})) {
|
|
2304
|
+
return;
|
|
2305
|
+
}
|
|
2306
|
+
const questions = typed.method === 'item/tool/requestUserInput' ? typed.params.questions : [];
|
|
2307
|
+
const permissionOptions = this.buildPermissionOptions({
|
|
2308
|
+
method: typed.method,
|
|
2309
|
+
questions,
|
|
2310
|
+
});
|
|
2311
|
+
await this.emitSessionUpdate(sessionId, {
|
|
2312
|
+
sessionUpdate: 'tool_call_update',
|
|
2313
|
+
toolCallId: toolCall.toolCallId,
|
|
2314
|
+
title: toolCall.title,
|
|
2315
|
+
kind: toolCall.kind,
|
|
2316
|
+
status: 'pending',
|
|
2317
|
+
...(toolCall.locations.length > 0 ? { locations: toolCall.locations } : {}),
|
|
2318
|
+
});
|
|
2319
|
+
const permissionResult = await this.connection.requestPermission({
|
|
2320
|
+
sessionId,
|
|
2321
|
+
toolCall: {
|
|
2322
|
+
toolCallId: toolCall.toolCallId,
|
|
2323
|
+
title: toolCall.title,
|
|
2324
|
+
kind: toolCall.kind,
|
|
2325
|
+
status: 'pending',
|
|
2326
|
+
rawInput: typed.params,
|
|
2327
|
+
},
|
|
2328
|
+
options: permissionOptions,
|
|
2329
|
+
});
|
|
2330
|
+
await this.respondToCodexPermissionRequest({
|
|
2331
|
+
request: typed,
|
|
2332
|
+
permission: permissionResult,
|
|
2333
|
+
session,
|
|
2334
|
+
toolCallId: toolCall.toolCallId,
|
|
2335
|
+
questions,
|
|
2336
|
+
});
|
|
2337
|
+
}
|
|
2338
|
+
catch (error) {
|
|
2339
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2340
|
+
this.codex.respondError(request.id, {
|
|
2341
|
+
code: -32_600,
|
|
2342
|
+
message: 'Failed to process codex approval request',
|
|
2343
|
+
data: { error: message },
|
|
2344
|
+
});
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
buildApprovalToolCallState(session, method, itemId, _turnId, params) {
|
|
2348
|
+
if (method === 'item/commandExecution/requestApproval') {
|
|
2349
|
+
const command = asString(params.command);
|
|
2350
|
+
const cwd = asString(params.cwd) ?? session.cwd;
|
|
2351
|
+
const parsed = resolveCommandDisplay({ command, cwd });
|
|
2352
|
+
return {
|
|
2353
|
+
toolCallId: resolveToolCallId({
|
|
2354
|
+
itemId,
|
|
2355
|
+
source: params,
|
|
2356
|
+
}),
|
|
2357
|
+
title: parsed.title,
|
|
2358
|
+
kind: parsed.kind,
|
|
2359
|
+
locations: parsed.locations,
|
|
2360
|
+
};
|
|
2361
|
+
}
|
|
2362
|
+
if (method === 'item/fileChange/requestApproval') {
|
|
2363
|
+
const grantRoot = asString(params.grantRoot);
|
|
2364
|
+
return {
|
|
2365
|
+
toolCallId: resolveToolCallId({
|
|
2366
|
+
itemId,
|
|
2367
|
+
source: params,
|
|
2368
|
+
}),
|
|
2369
|
+
title: 'fileChange',
|
|
2370
|
+
kind: 'edit',
|
|
2371
|
+
locations: grantRoot ? [{ path: grantRoot }] : [],
|
|
2372
|
+
};
|
|
2373
|
+
}
|
|
2374
|
+
return {
|
|
2375
|
+
toolCallId: resolveToolCallId({
|
|
2376
|
+
itemId,
|
|
2377
|
+
source: params,
|
|
2378
|
+
}),
|
|
2379
|
+
title: 'item/tool/requestUserInput',
|
|
2380
|
+
kind: 'other',
|
|
2381
|
+
locations: [],
|
|
2382
|
+
};
|
|
2383
|
+
}
|
|
2384
|
+
isAllowedPermissionSelection(selectedOptionId) {
|
|
2385
|
+
return selectedOptionId === 'allow_once' || selectedOptionId === 'allow_always';
|
|
2386
|
+
}
|
|
2387
|
+
async handleToolUserInputPermissionResponse(params) {
|
|
2388
|
+
if (params.request.method !== 'item/tool/requestUserInput') {
|
|
2389
|
+
return false;
|
|
2390
|
+
}
|
|
2391
|
+
const rejected = params.selectedOptionId === null || params.selectedOptionId === 'reject_once';
|
|
2392
|
+
let answers;
|
|
2393
|
+
try {
|
|
2394
|
+
answers = buildToolUserInputAnswers({
|
|
2395
|
+
questions: params.questions,
|
|
2396
|
+
permission: params.permission,
|
|
2397
|
+
selectedOptionId: params.selectedOptionId,
|
|
2398
|
+
});
|
|
2399
|
+
}
|
|
2400
|
+
catch (error) {
|
|
2401
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2402
|
+
this.codex.respondError(params.request.id, {
|
|
2403
|
+
code: -32_602,
|
|
2404
|
+
message: 'Failed to map requestUserInput answers',
|
|
2405
|
+
data: { error: message },
|
|
2406
|
+
});
|
|
2407
|
+
await this.emitSessionUpdate(params.session.sessionId, {
|
|
2408
|
+
sessionUpdate: 'tool_call_update',
|
|
2409
|
+
toolCallId: params.toolCallId,
|
|
2410
|
+
status: 'failed',
|
|
2411
|
+
rawOutput: { error: message },
|
|
2412
|
+
});
|
|
2413
|
+
return true;
|
|
2414
|
+
}
|
|
2415
|
+
this.codex.respondSuccess(params.request.id, {
|
|
2416
|
+
answers,
|
|
2417
|
+
});
|
|
2418
|
+
await this.emitSessionUpdate(params.session.sessionId, {
|
|
2419
|
+
sessionUpdate: 'tool_call_update',
|
|
2420
|
+
toolCallId: params.toolCallId,
|
|
2421
|
+
status: rejected ? 'failed' : 'completed',
|
|
2422
|
+
rawOutput: rejected ? { outcome: 'rejected' } : { answers },
|
|
2423
|
+
});
|
|
2424
|
+
return true;
|
|
2425
|
+
}
|
|
2426
|
+
async respondToCodexPermissionRequest(params) {
|
|
2427
|
+
const selected = params.permission.outcome.outcome === 'selected' ? params.permission.outcome.optionId : null;
|
|
2428
|
+
if (await this.handleToolUserInputPermissionResponse({
|
|
2429
|
+
...params,
|
|
2430
|
+
selectedOptionId: selected,
|
|
2431
|
+
})) {
|
|
2432
|
+
return;
|
|
2433
|
+
}
|
|
2434
|
+
if (params.request.method === 'item/commandExecution/requestApproval' &&
|
|
2435
|
+
selected === 'allow_always') {
|
|
2436
|
+
const scopeKey = buildCommandApprovalScopeKey({
|
|
2437
|
+
command: asString(params.request.params.command),
|
|
2438
|
+
cwd: asString(params.request.params.cwd) ?? params.session.cwd,
|
|
2439
|
+
});
|
|
2440
|
+
if (scopeKey) {
|
|
2441
|
+
params.session.commandApprovalScopes.add(scopeKey);
|
|
2442
|
+
}
|
|
2443
|
+
else {
|
|
2444
|
+
this.reportShapeDrift('allow_always_without_scope_key', {
|
|
2445
|
+
itemId: params.request.params.itemId,
|
|
2446
|
+
command: params.request.params.command,
|
|
2447
|
+
});
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
const allow = this.isAllowedPermissionSelection(selected);
|
|
2451
|
+
this.codex.respondSuccess(params.request.id, {
|
|
2452
|
+
decision: allow ? 'accept' : 'decline',
|
|
2453
|
+
});
|
|
2454
|
+
await this.emitSessionUpdate(params.session.sessionId, {
|
|
2455
|
+
sessionUpdate: 'tool_call_update',
|
|
2456
|
+
toolCallId: params.toolCallId,
|
|
2457
|
+
status: allow ? 'in_progress' : 'failed',
|
|
2458
|
+
rawOutput: { decision: allow ? 'accept' : 'decline' },
|
|
2459
|
+
});
|
|
2460
|
+
}
|
|
2461
|
+
settleTurn(session, stopReason) {
|
|
2462
|
+
if (!session.activeTurn || session.activeTurn.settled) {
|
|
2463
|
+
return;
|
|
2464
|
+
}
|
|
2465
|
+
session.activeTurn.settled = true;
|
|
2466
|
+
session.activeTurn.resolve(stopReason);
|
|
2467
|
+
session.activeTurn = null;
|
|
2468
|
+
session.planTextByItemId.clear();
|
|
2469
|
+
session.planApprovalRequestedByTurnId.clear();
|
|
2470
|
+
session.pendingPlanApprovalsByTurnId.clear();
|
|
2471
|
+
session.toolCallsByItemId.clear();
|
|
2472
|
+
session.reasoningDeltaItemIds.clear();
|
|
2473
|
+
session.pendingTurnCompletionsByTurnId.clear();
|
|
2474
|
+
}
|
|
2475
|
+
reportShapeDrift(event, details) {
|
|
2476
|
+
const count = (this.shapeDriftCounts.get(event) ?? 0) + 1;
|
|
2477
|
+
this.shapeDriftCounts.set(event, count);
|
|
2478
|
+
const includeDetails = details !== undefined && (count <= 5 || count % 50 === 0);
|
|
2479
|
+
const detailsSuffix = includeDetails ? ` details=${toShapeDriftDetails(details)}` : '';
|
|
2480
|
+
process.stderr.write(`[codex-app-server-acp] shape-drift event=${event} count=${count}${detailsSuffix}\n`);
|
|
2481
|
+
}
|
|
2482
|
+
async emitSessionUpdate(sessionId, update) {
|
|
2483
|
+
try {
|
|
2484
|
+
await this.connection.sessionUpdate({ sessionId, update });
|
|
2485
|
+
}
|
|
2486
|
+
catch {
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
export function runCodexAppServerAcpAdapter() {
|
|
2491
|
+
const output = Writable.toWeb(process.stdout);
|
|
2492
|
+
const input = Readable.toWeb(process.stdin);
|
|
2493
|
+
const stream = ndJsonStream(output, input);
|
|
2494
|
+
new AgentSideConnection((connection) => new CodexAppServerAcpAdapter(connection), stream);
|
|
2495
|
+
}
|
|
2496
|
+
//# sourceMappingURL=codex-app-server-acp-adapter.js.map
|