@cloudcli-ai/cloudcli 1.37.2 → 1.37.4
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/README.md +269 -0
- package/dist/assets/{abnfDiagram-N423BO3Z-C4UAoHJb.js → abnfDiagram-N423BO3Z-Dmkwx38H.js} +1 -1
- package/dist/assets/{arc-B8jF-ja9.js → arc-DLe9AYuO.js} +1 -1
- package/dist/assets/{architectureDiagram-T3A2C74G-C_Wur_CU.js → architectureDiagram-T3A2C74G-CBbbbNUW.js} +1 -1
- package/dist/assets/{blockDiagram-VBNYF7ZC-Cq4P1i8t.js → blockDiagram-VBNYF7ZC-CaMEaWu4.js} +1 -1
- package/dist/assets/{c4Diagram-5PPSVZJV-C1wG4eUj.js → c4Diagram-5PPSVZJV-DhjMgNnR.js} +1 -1
- package/dist/assets/channel-CWrmwsir.js +1 -0
- package/dist/assets/{chunk-2GRJ4B5K-B8fnBG6E.js → chunk-2GRJ4B5K-yjl3-F-i.js} +1 -1
- package/dist/assets/{chunk-2Q5K7J3B-WdfPooPJ.js → chunk-2Q5K7J3B-DsAYaWEP.js} +1 -1
- package/dist/assets/{chunk-5RXB4S5H-DhZJuG2E.js → chunk-5RXB4S5H-Bt5qmqOX.js} +1 -1
- package/dist/assets/{chunk-5VM5RSS4-B0rjslal.js → chunk-5VM5RSS4-yWrWVvUt.js} +1 -1
- package/dist/assets/{chunk-6Q2QTUOP-CG2Oj4jL.js → chunk-6Q2QTUOP-Dt66K5xb.js} +1 -1
- package/dist/assets/{chunk-GF5L2VYU-rrl2lRxY.js → chunk-GF5L2VYU-DA0vzaj4.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-CjVbrVMA.js → chunk-JWPE2WC7-CUkbx96V.js} +1 -1
- package/dist/assets/{chunk-KBJHAD2P-CRzKgU8S.js → chunk-KBJHAD2P-DWKxx1WH.js} +1 -1
- package/dist/assets/{chunk-RYQCIY6F-DBz4GTZF.js → chunk-RYQCIY6F-BqluplN0.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-wHxalsXk.js → chunk-XXDRQBXY-DelKg3Xf.js} +1 -1
- package/dist/assets/classDiagram-JCYQIIEL-COAGCC6w.js +1 -0
- package/dist/assets/classDiagram-v2-OCEON4UE-COAGCC6w.js +1 -0
- package/dist/assets/{cose-bilkent-JH36ORCC-s1hygY8e.js → cose-bilkent-JH36ORCC-DHV93-Em.js} +1 -1
- package/dist/assets/{cynefin-VYW2F7L2-C4LGV15i.js → cynefin-VYW2F7L2-B9HZgoGX.js} +1 -1
- package/dist/assets/{cynefinDiagram-MW4NZA55-C5uhu7B3.js → cynefinDiagram-MW4NZA55-hFjjEQg8.js} +1 -1
- package/dist/assets/{dagre-VZM6K2ZE-B-OBTewm.js → dagre-VZM6K2ZE-CpeVOC7V.js} +1 -1
- package/dist/assets/{diagram-7IWD3JNH-BjGKQxih.js → diagram-7IWD3JNH-D0H1UoRA.js} +1 -1
- package/dist/assets/{diagram-B4RE2ZJO-BMKz1X2L.js → diagram-B4RE2ZJO-6Y7BQUup.js} +1 -1
- package/dist/assets/{diagram-LBJQPF4R-DNh-GhKC.js → diagram-LBJQPF4R-C0nNOwht.js} +1 -1
- package/dist/assets/{diagram-Q27KOJAE-BaTG9jPT.js → diagram-Q27KOJAE-En6QlOVX.js} +1 -1
- package/dist/assets/{diagram-UB23O5K3-BUwo2OvJ.js → diagram-UB23O5K3-CmPiNeGD.js} +1 -1
- package/dist/assets/{ebnfDiagram-BXEA7PRR-DAZgfzX2.js → ebnfDiagram-BXEA7PRR-Bygzp1Qt.js} +1 -1
- package/dist/assets/{erDiagram-JOGREHBK-1w1CcGEb.js → erDiagram-JOGREHBK-CwScRfzl.js} +1 -1
- package/dist/assets/{flowDiagram-UKHOOZJN-D4EII8Jj.js → flowDiagram-UKHOOZJN-bKX-0xP6.js} +1 -1
- package/dist/assets/{ganttDiagram-PKOTCBZU-ChvJZAgB.js → ganttDiagram-PKOTCBZU-DtZPKXGd.js} +1 -1
- package/dist/assets/{gitGraphDiagram-DS77QQ5N-DtOn41S8.js → gitGraphDiagram-DS77QQ5N-CUOtdL1o.js} +1 -1
- package/dist/assets/index-CdjIFxM2.js +5004 -0
- package/dist/assets/index-DgafhGTF.css +32 -0
- package/dist/assets/{infoDiagram-6WML65LV-DN_PVEJZ.js → infoDiagram-6WML65LV-ciRQTS0h.js} +1 -1
- package/dist/assets/{ishikawaDiagram-WSZJBQD7-DbkF1kXb.js → ishikawaDiagram-WSZJBQD7-CgjhQZ84.js} +1 -1
- package/dist/assets/{journeyDiagram-NVQOT4AX-pJwmhddE.js → journeyDiagram-NVQOT4AX-BsbQSeIW.js} +1 -1
- package/dist/assets/{kanban-definition-27J2QSJJ-PPAPgNp5.js → kanban-definition-27J2QSJJ-CrwzY9Jw.js} +1 -1
- package/dist/assets/{linear-iPR-J9f7.js → linear-BUaCDr6l.js} +1 -1
- package/dist/assets/{mermaid.core-BEUL86Z6.js → mermaid.core-ClgdUU0j.js} +5 -5
- package/dist/assets/{mindmap-definition-FAOFIHXS-BvZoBib7.js → mindmap-definition-FAOFIHXS-BictljY1.js} +1 -1
- package/dist/assets/{pegDiagram-VL7TDLO6-DhYreaUd.js → pegDiagram-VL7TDLO6-BXdByy_U.js} +1 -1
- package/dist/assets/{pieDiagram-7S7Q4E2Y-CrOHlyD0.js → pieDiagram-7S7Q4E2Y-uZsMfkRX.js} +1 -1
- package/dist/assets/{quadrantDiagram-CIZ2JOQS-D4g5yH3t.js → quadrantDiagram-CIZ2JOQS-7lCv4QIj.js} +1 -1
- package/dist/assets/{railroadDiagram-AXF67PYL-Dj39lHlf.js → railroadDiagram-AXF67PYL-07dxIWbi.js} +1 -1
- package/dist/assets/{requirementDiagram-LRYGKXZP-1AS4Zgm_.js → requirementDiagram-LRYGKXZP-Cb31Yv1_.js} +1 -1
- package/dist/assets/{sankeyDiagram-W5VNT64P-CKlMn19P.js → sankeyDiagram-W5VNT64P-X0YPSyF9.js} +1 -1
- package/dist/assets/{sequenceDiagram-SI44F4Z6-B_j3qyvo.js → sequenceDiagram-SI44F4Z6-BdBxSIlP.js} +1 -1
- package/dist/assets/server.browser-CtX3Hdl5.js +27 -0
- package/dist/assets/{sizeCapture-X5ZJPWSS-Bp2Op16v.js → sizeCapture-X5ZJPWSS-DQM5xAyD.js} +1 -1
- package/dist/assets/{stateDiagram-OKZ733FA-C0cN6DoY.js → stateDiagram-OKZ733FA-DJ3xkLUF.js} +1 -1
- package/dist/assets/stateDiagram-v2-UEYNNEHI-vAaE4zNo.js +1 -0
- package/dist/assets/{swimlanes-SLNWSIFB-BEvXOV9b.js → swimlanes-SLNWSIFB-rjfYdW0w.js} +2 -2
- package/dist/assets/swimlanesDiagram-ULZ7WXOC-TV2IF4Nq.js +8 -0
- package/dist/assets/{timeline-definition-Z64GVDOM-zdJQinvZ.js → timeline-definition-Z64GVDOM-DZMra5do.js} +1 -1
- package/dist/assets/{vendor-codemirror-OwyKSvPE.js → vendor-codemirror-CYrSt7-U.js} +18 -18
- package/dist/assets/{vennDiagram-T6HMQDX7-DuF1uZMY.js → vennDiagram-T6HMQDX7-BevD8uu0.js} +1 -1
- package/dist/assets/{wardleyDiagram-T6FBY63Y-B9AlLQup.js → wardleyDiagram-T6FBY63Y-DsIyXB7z.js} +1 -1
- package/dist/assets/{xychartDiagram-ELKLHX3M-DTJX48Qg.js → xychartDiagram-ELKLHX3M-G3PlF3ko.js} +1 -1
- package/dist/index.html +3 -3
- package/dist-server/server/index.js +11 -4
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/modules/agent/agent.module.js +9 -2
- package/dist-server/server/modules/agent/agent.module.js.map +1 -1
- package/dist-server/server/modules/agent/agent.routes.js +177 -130
- package/dist-server/server/modules/agent/agent.routes.js.map +1 -1
- package/dist-server/server/modules/agent/tests/agent-run-registration.test.js +306 -0
- package/dist-server/server/modules/agent/tests/agent-run-registration.test.js.map +1 -0
- package/dist-server/server/modules/agent/tests/agent.routes.test.js +48 -2
- package/dist-server/server/modules/agent/tests/agent.routes.test.js.map +1 -1
- package/dist-server/server/modules/browser-use/browser-use.routes.js +39 -33
- package/dist-server/server/modules/browser-use/browser-use.routes.js.map +1 -1
- package/dist-server/server/modules/database/connection.js +1 -1
- package/dist-server/server/modules/database/connection.js.map +1 -1
- package/dist-server/server/modules/database/index.js +5 -0
- package/dist-server/server/modules/database/index.js.map +1 -1
- package/dist-server/server/modules/database/migrations.js +45 -1
- package/dist-server/server/modules/database/migrations.js.map +1 -1
- package/dist-server/server/modules/database/repositories/provider-models.js +60 -6
- package/dist-server/server/modules/database/repositories/provider-models.js.map +1 -1
- package/dist-server/server/modules/database/repositories/scheduled-messages.db.js +71 -0
- package/dist-server/server/modules/database/repositories/scheduled-messages.db.js.map +1 -0
- package/dist-server/server/modules/database/repositories/session-drafts.db.js +105 -0
- package/dist-server/server/modules/database/repositories/session-drafts.db.js.map +1 -0
- package/dist-server/server/modules/database/repositories/sessions.db.js +144 -5
- package/dist-server/server/modules/database/repositories/sessions.db.js.map +1 -1
- package/dist-server/server/modules/database/repositories/user-preferences.db.js +65 -0
- package/dist-server/server/modules/database/repositories/user-preferences.db.js.map +1 -0
- package/dist-server/server/modules/database/schema.js +99 -0
- package/dist-server/server/modules/database/schema.js.map +1 -1
- package/dist-server/server/modules/database/tests/provider-models.db.integration.test.js +99 -0
- package/dist-server/server/modules/database/tests/provider-models.db.integration.test.js.map +1 -1
- package/dist-server/server/modules/database/tests/sessions.db.integration.test.js +39 -0
- package/dist-server/server/modules/database/tests/sessions.db.integration.test.js.map +1 -1
- package/dist-server/server/modules/database/tests/user-preferences.db.integration.test.js +105 -0
- package/dist-server/server/modules/database/tests/user-preferences.db.integration.test.js.map +1 -0
- package/dist-server/server/modules/file-tree/file-tree.module.js +2 -1
- package/dist-server/server/modules/file-tree/file-tree.module.js.map +1 -1
- package/dist-server/server/modules/file-tree/file-tree.service.js +44 -7
- package/dist-server/server/modules/file-tree/file-tree.service.js.map +1 -1
- package/dist-server/server/modules/file-tree/tests/file-tree.service.test.js +126 -1
- package/dist-server/server/modules/file-tree/tests/file-tree.service.test.js.map +1 -1
- package/dist-server/server/modules/git/git-branch-diff.service.js +142 -0
- package/dist-server/server/modules/git/git-branch-diff.service.js.map +1 -0
- package/dist-server/server/modules/git/git-branch.service.js.map +1 -1
- package/dist-server/server/modules/git/git.routes.js +92 -5
- package/dist-server/server/modules/git/git.routes.js.map +1 -1
- package/dist-server/server/modules/git/tests/git-branch-diff.service.test.js +160 -0
- package/dist-server/server/modules/git/tests/git-branch-diff.service.test.js.map +1 -0
- package/dist-server/server/modules/projects/project-clone.routes.js +108 -0
- package/dist-server/server/modules/projects/project-clone.routes.js.map +1 -0
- package/dist-server/server/modules/projects/projects.routes.js +13 -66
- package/dist-server/server/modules/projects/projects.routes.js.map +1 -1
- package/dist-server/server/modules/projects/services/project-clone-request.service.js +36 -0
- package/dist-server/server/modules/projects/services/project-clone-request.service.js.map +1 -0
- package/dist-server/server/modules/projects/services/project-clone.service.js +86 -32
- package/dist-server/server/modules/projects/services/project-clone.service.js.map +1 -1
- package/dist-server/server/modules/projects/services/project-delete.service.js +12 -1
- package/dist-server/server/modules/projects/services/project-delete.service.js.map +1 -1
- package/dist-server/server/modules/projects/services/project-star.service.js +2 -2
- package/dist-server/server/modules/projects/services/project-star.service.js.map +1 -1
- package/dist-server/server/modules/projects/tests/project-clone.routes.test.js +191 -0
- package/dist-server/server/modules/projects/tests/project-clone.routes.test.js.map +1 -0
- package/dist-server/server/modules/projects/tests/project-clone.service.test.js +275 -0
- package/dist-server/server/modules/projects/tests/project-clone.service.test.js.map +1 -1
- package/dist-server/server/modules/providers/index.js +3 -0
- package/dist-server/server/modules/providers/index.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js +62 -9
- package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-fork.provider.js +46 -0
- package/dist-server/server/modules/providers/list/claude/claude-fork.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/claude/claude-models.provider.js +31 -12
- package/dist-server/server/modules/providers/list/claude/claude-models.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-runtime.provider.js +472 -53
- package/dist-server/server/modules/providers/list/claude/claude-runtime.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js +38 -11
- package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js +1007 -60
- package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js +4 -1
- package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude.provider.js +2 -0
- package/dist-server/server/modules/providers/list/claude/claude.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-app-server.client.js +204 -0
- package/dist-server/server/modules/providers/list/codex/codex-app-server.client.js.map +1 -0
- package/dist-server/server/modules/providers/list/codex/codex-fork.provider.js +32 -0
- package/dist-server/server/modules/providers/list/codex/codex-fork.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/codex/codex-models.provider.js +47 -0
- package/dist-server/server/modules/providers/list/codex/codex-models.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-runtime.provider.js +71 -53
- package/dist-server/server/modules/providers/list/codex/codex-runtime.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js +8 -0
- package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +1621 -431
- package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex.provider.js +2 -0
- package/dist-server/server/modules/providers/list/codex/codex.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/cursor/cursor-auth.provider.js +9 -2
- package/dist-server/server/modules/providers/list/cursor/cursor-auth.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/cursor/cursor-runtime.provider.js +16 -8
- package/dist-server/server/modules/providers/list/cursor/cursor-runtime.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js +274 -9
- package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/opencode/opencode-runtime.provider.js +5 -2
- package/dist-server/server/modules/providers/list/opencode/opencode-runtime.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js +19 -2
- package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js.map +1 -1
- package/dist-server/server/modules/providers/provider.routes.js +134 -2
- package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
- package/dist-server/server/modules/providers/services/claude-cli-liveness.service.js +100 -0
- package/dist-server/server/modules/providers/services/claude-cli-liveness.service.js.map +1 -0
- package/dist-server/server/modules/providers/services/claude-settings-permissions.service.js +97 -0
- package/dist-server/server/modules/providers/services/claude-settings-permissions.service.js.map +1 -0
- package/dist-server/server/modules/providers/services/provider-capabilities.service.js +23 -1
- package/dist-server/server/modules/providers/services/provider-capabilities.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/provider-models.service.js +59 -5
- package/dist-server/server/modules/providers/services/provider-models.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/provider-runtime.service.js +8 -0
- package/dist-server/server/modules/providers/services/provider-runtime.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/provider-token-usage.service.js +139 -35
- package/dist-server/server/modules/providers/services/provider-token-usage.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/session-conversations-search.service.js +2 -4
- package/dist-server/server/modules/providers/services/session-conversations-search.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/session-history-cache.service.js +91 -0
- package/dist-server/server/modules/providers/services/session-history-cache.service.js.map +1 -0
- package/dist-server/server/modules/providers/services/session-synchronizer.service.js +105 -32
- package/dist-server/server/modules/providers/services/session-synchronizer.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/sessions-watcher.service.js +3 -60
- package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/sessions.service.js +252 -12
- package/dist-server/server/modules/providers/services/sessions.service.js.map +1 -1
- package/dist-server/server/modules/providers/tests/claude-auth.test.js +183 -0
- package/dist-server/server/modules/providers/tests/claude-auth.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/claude-background-work.test.js +172 -0
- package/dist-server/server/modules/providers/tests/claude-background-work.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/claude-compaction.test.js +84 -0
- package/dist-server/server/modules/providers/tests/claude-compaction.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/claude-message-model.test.js +123 -0
- package/dist-server/server/modules/providers/tests/claude-message-model.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/claude-models.test.js +39 -0
- package/dist-server/server/modules/providers/tests/claude-models.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/claude-runtime-hold.test.js +265 -0
- package/dist-server/server/modules/providers/tests/claude-runtime-hold.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/claude-sessions.test.js +1815 -33
- package/dist-server/server/modules/providers/tests/claude-sessions.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/claude-settings-permissions.test.js +228 -0
- package/dist-server/server/modules/providers/tests/claude-settings-permissions.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/claude-subagent-echo.test.js +16 -0
- package/dist-server/server/modules/providers/tests/claude-subagent-echo.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/claude-token-budget.test.js +85 -0
- package/dist-server/server/modules/providers/tests/claude-token-budget.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/codex-message-editing.test.js +338 -0
- package/dist-server/server/modules/providers/tests/codex-message-editing.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/codex-models.test.js +25 -0
- package/dist-server/server/modules/providers/tests/codex-models.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/codex-runtime.test.js +83 -0
- package/dist-server/server/modules/providers/tests/codex-runtime.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/codex-session-fork.test.js +54 -0
- package/dist-server/server/modules/providers/tests/codex-session-fork.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/codex-sessions.test.js +354 -15
- package/dist-server/server/modules/providers/tests/codex-sessions.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/cursor-auth.test.js +167 -0
- package/dist-server/server/modules/providers/tests/cursor-auth.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/opencode-models.test.js +196 -4
- package/dist-server/server/modules/providers/tests/opencode-models.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/opencode-sessions.test.js +19 -1
- package/dist-server/server/modules/providers/tests/opencode-sessions.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/provider-capabilities.service.test.js +30 -0
- package/dist-server/server/modules/providers/tests/provider-capabilities.service.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/provider-models.service.test.js +79 -1
- package/dist-server/server/modules/providers/tests/provider-models.service.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/provider-token-usage.service.test.js +164 -1
- package/dist-server/server/modules/providers/tests/provider-token-usage.service.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/provider.routes.test.js +190 -1
- package/dist-server/server/modules/providers/tests/provider.routes.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/session-fork.test.js +113 -0
- package/dist-server/server/modules/providers/tests/session-fork.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/session-history-cache.service.test.js +139 -0
- package/dist-server/server/modules/providers/tests/session-history-cache.service.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/session-orphan-prune.test.js +90 -0
- package/dist-server/server/modules/providers/tests/session-orphan-prune.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/session-synchronizer-coalescing.test.js +122 -0
- package/dist-server/server/modules/providers/tests/session-synchronizer-coalescing.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/sessions.service.test.js +219 -1
- package/dist-server/server/modules/providers/tests/sessions.service.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/skills.test.js +15 -3
- package/dist-server/server/modules/providers/tests/skills.test.js.map +1 -1
- package/dist-server/server/modules/scheduled-messages/index.js +5 -0
- package/dist-server/server/modules/scheduled-messages/index.js.map +1 -0
- package/dist-server/server/modules/scheduled-messages/scheduled-messages.routes.js +47 -0
- package/dist-server/server/modules/scheduled-messages/scheduled-messages.routes.js.map +1 -0
- package/dist-server/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.js +175 -0
- package/dist-server/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.js.map +1 -0
- package/dist-server/server/modules/scheduled-messages/services/scheduled-messages.service.js +95 -0
- package/dist-server/server/modules/scheduled-messages/services/scheduled-messages.service.js.map +1 -0
- package/dist-server/server/modules/scheduled-messages/tests/scheduled-messages.test.js +290 -0
- package/dist-server/server/modules/scheduled-messages/tests/scheduled-messages.test.js.map +1 -0
- package/dist-server/server/modules/taskmaster/taskmaster.routes.js +23 -23
- package/dist-server/server/modules/taskmaster/taskmaster.routes.js.map +1 -1
- package/dist-server/server/modules/taskmaster/tests/taskmaster.routes.test.js +48 -0
- package/dist-server/server/modules/taskmaster/tests/taskmaster.routes.test.js.map +1 -1
- package/dist-server/server/modules/user/tests/user.service.test.js +70 -0
- package/dist-server/server/modules/user/tests/user.service.test.js.map +1 -1
- package/dist-server/server/modules/user/user.module.js +10 -1
- package/dist-server/server/modules/user/user.module.js.map +1 -1
- package/dist-server/server/modules/user/user.routes.js +44 -0
- package/dist-server/server/modules/user/user.routes.js.map +1 -1
- package/dist-server/server/modules/user/user.service.js +75 -0
- package/dist-server/server/modules/user/user.service.js.map +1 -1
- package/dist-server/server/modules/voice/tests/voice.service.test.js +3 -2
- package/dist-server/server/modules/voice/tests/voice.service.test.js.map +1 -1
- package/dist-server/server/modules/websocket/index.js +7 -0
- package/dist-server/server/modules/websocket/index.js.map +1 -1
- package/dist-server/server/modules/websocket/services/chat-run-registry.service.js +37 -49
- package/dist-server/server/modules/websocket/services/chat-run-registry.service.js.map +1 -1
- package/dist-server/server/modules/websocket/services/chat-session-writer.service.js +35 -5
- package/dist-server/server/modules/websocket/services/chat-session-writer.service.js.map +1 -1
- package/dist-server/server/modules/websocket/services/chat-websocket.service.js +204 -10
- package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
- package/dist-server/server/modules/websocket/services/session-upsert-broadcast.service.js +94 -0
- package/dist-server/server/modules/websocket/services/session-upsert-broadcast.service.js.map +1 -0
- package/dist-server/server/modules/websocket/services/shell-websocket.service.js +236 -20
- package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -1
- package/dist-server/server/modules/websocket/tests/chat-background-subscribe.test.js +137 -0
- package/dist-server/server/modules/websocket/tests/chat-background-subscribe.test.js.map +1 -0
- package/dist-server/server/modules/websocket/tests/chat-edit-send.test.js +263 -0
- package/dist-server/server/modules/websocket/tests/chat-edit-send.test.js.map +1 -0
- package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js +29 -3
- package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js.map +1 -1
- package/dist-server/server/modules/websocket/tests/chat-stop-task.test.js +96 -0
- package/dist-server/server/modules/websocket/tests/chat-stop-task.test.js.map +1 -0
- package/dist-server/server/modules/websocket/tests/session-upsert-broadcast.test.js +106 -0
- package/dist-server/server/modules/websocket/tests/session-upsert-broadcast.test.js.map +1 -0
- package/dist-server/server/modules/websocket/tests/shell-websocket.service.test.js +500 -0
- package/dist-server/server/modules/websocket/tests/shell-websocket.service.test.js.map +1 -1
- package/dist-server/server/shared/claude-cli-path.js +22 -5
- package/dist-server/server/shared/claude-cli-path.js.map +1 -1
- package/dist-server/server/shared/index.js +3 -0
- package/dist-server/server/shared/index.js.map +1 -0
- package/dist-server/server/shared/message-unification.js +376 -0
- package/dist-server/server/shared/message-unification.js.map +1 -0
- package/dist-server/server/shared/tests/claude-cli-path.test.js +45 -1
- package/dist-server/server/shared/tests/claude-cli-path.test.js.map +1 -1
- package/dist-server/server/shared/tests/message-unification.test.js +174 -0
- package/dist-server/server/shared/tests/message-unification.test.js.map +1 -0
- package/dist-server/server/shared/tests/read-only-roots.test.js +124 -0
- package/dist-server/server/shared/tests/read-only-roots.test.js.map +1 -0
- package/dist-server/server/shared/tests/strip-ansi-sequences.test.js +31 -0
- package/dist-server/server/shared/tests/strip-ansi-sequences.test.js.map +1 -0
- package/dist-server/server/shared/utils.js +160 -0
- package/dist-server/server/shared/utils.js.map +1 -1
- package/electron/desktopWindow.js +6 -0
- package/electron/viewHost.js +25 -2
- package/package.json +20 -17
- package/server/index.ts +16 -5
- package/server/modules/agent/agent.module.ts +11 -1
- package/server/modules/agent/agent.routes.ts +200 -142
- package/server/modules/agent/tests/agent-run-registration.test.ts +348 -0
- package/server/modules/agent/tests/agent.routes.test.ts +51 -2
- package/server/modules/browser-use/browser-use.routes.ts +96 -33
- package/server/modules/database/connection.ts +2 -1
- package/server/modules/database/index.ts +10 -0
- package/server/modules/database/migrations.ts +51 -0
- package/server/modules/database/repositories/provider-models.ts +79 -6
- package/server/modules/database/repositories/scheduled-messages.db.ts +127 -0
- package/server/modules/database/repositories/session-drafts.db.ts +171 -0
- package/server/modules/database/repositories/sessions.db.ts +204 -3
- package/server/modules/database/repositories/user-preferences.db.ts +80 -0
- package/server/modules/database/schema.ts +103 -0
- package/server/modules/database/tests/provider-models.db.integration.test.ts +113 -0
- package/server/modules/database/tests/sessions.db.integration.test.ts +52 -0
- package/server/modules/database/tests/user-preferences.db.integration.test.ts +135 -0
- package/server/modules/file-tree/file-tree.module.ts +2 -1
- package/server/modules/file-tree/file-tree.service.ts +49 -7
- package/server/modules/file-tree/tests/file-tree.service.test.ts +163 -1
- package/server/modules/git/git-branch-diff.service.ts +204 -0
- package/server/modules/git/git-branch.service.ts +2 -12
- package/server/modules/git/git.routes.ts +100 -6
- package/server/modules/git/tests/git-branch-diff.service.test.ts +210 -0
- package/server/modules/projects/project-clone.routes.ts +142 -0
- package/server/modules/projects/projects.routes.ts +16 -81
- package/server/modules/projects/services/project-clone-request.service.ts +56 -0
- package/server/modules/projects/services/project-clone.service.ts +102 -34
- package/server/modules/projects/services/project-delete.service.ts +13 -1
- package/server/modules/projects/services/project-star.service.ts +2 -2
- package/server/modules/projects/tests/project-clone.routes.test.ts +246 -0
- package/server/modules/projects/tests/project-clone.service.test.ts +376 -0
- package/server/modules/providers/index.ts +4 -0
- package/server/modules/providers/list/claude/claude-auth.provider.ts +81 -11
- package/server/modules/providers/list/claude/claude-fork.provider.ts +57 -0
- package/server/modules/providers/list/claude/claude-models.provider.ts +37 -17
- package/server/modules/providers/list/claude/claude-runtime.provider.js +510 -54
- package/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts +40 -14
- package/server/modules/providers/list/claude/claude-sessions.provider.ts +1195 -63
- package/server/modules/providers/list/claude/claude-skills.provider.ts +4 -1
- package/server/modules/providers/list/claude/claude.provider.ts +3 -0
- package/server/modules/providers/list/codex/codex-app-server.client.ts +243 -0
- package/server/modules/providers/list/codex/codex-fork.provider.ts +40 -0
- package/server/modules/providers/list/codex/codex-models.provider.ts +47 -0
- package/server/modules/providers/list/codex/{codex-runtime.provider.js → codex-runtime.provider.ts} +105 -70
- package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +9 -0
- package/server/modules/providers/list/codex/codex-sessions.provider.ts +1847 -455
- package/server/modules/providers/list/codex/codex.provider.ts +3 -0
- package/server/modules/providers/list/cursor/cursor-auth.provider.ts +10 -2
- package/server/modules/providers/list/cursor/cursor-runtime.provider.js +17 -8
- package/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.ts +0 -1
- package/server/modules/providers/list/opencode/opencode-models.provider.ts +297 -9
- package/server/modules/providers/list/opencode/opencode-runtime.provider.js +5 -2
- package/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.ts +30 -2
- package/server/modules/providers/provider.routes.ts +162 -2
- package/server/modules/providers/services/claude-cli-liveness.service.ts +121 -0
- package/server/modules/providers/services/claude-settings-permissions.service.ts +127 -0
- package/server/modules/providers/services/provider-capabilities.service.ts +40 -1
- package/server/modules/providers/services/provider-models.service.ts +71 -4
- package/server/modules/providers/services/provider-runtime.service.ts +11 -0
- package/server/modules/providers/services/provider-token-usage.service.ts +166 -39
- package/server/modules/providers/services/session-conversations-search.service.ts +2 -5
- package/server/modules/providers/services/session-history-cache.service.ts +150 -0
- package/server/modules/providers/services/session-synchronizer.service.ts +125 -38
- package/server/modules/providers/services/sessions-watcher.service.ts +3 -64
- package/server/modules/providers/services/sessions.service.ts +309 -17
- package/server/modules/providers/tests/claude-auth.test.ts +208 -0
- package/server/modules/providers/tests/claude-background-work.test.ts +213 -0
- package/server/modules/providers/tests/claude-compaction.test.ts +101 -0
- package/server/modules/providers/tests/claude-message-model.test.ts +160 -0
- package/server/modules/providers/tests/claude-models.test.ts +87 -0
- package/server/modules/providers/tests/claude-runtime-hold.test.ts +309 -0
- package/server/modules/providers/tests/claude-sessions.test.ts +2167 -37
- package/server/modules/providers/tests/claude-settings-permissions.test.ts +271 -0
- package/server/modules/providers/tests/claude-subagent-echo.test.ts +25 -0
- package/server/modules/providers/tests/claude-token-budget.test.ts +104 -0
- package/server/modules/providers/tests/codex-message-editing.test.ts +419 -0
- package/server/modules/providers/tests/codex-models.test.ts +37 -0
- package/server/modules/providers/tests/codex-runtime.test.ts +94 -0
- package/server/modules/providers/tests/codex-session-fork.test.ts +59 -0
- package/server/modules/providers/tests/codex-sessions.test.ts +396 -15
- package/server/modules/providers/tests/cursor-auth.test.ts +187 -0
- package/server/modules/providers/tests/opencode-models.test.ts +266 -8
- package/server/modules/providers/tests/opencode-sessions.test.ts +40 -1
- package/server/modules/providers/tests/provider-capabilities.service.test.ts +37 -0
- package/server/modules/providers/tests/provider-models.service.test.ts +99 -1
- package/server/modules/providers/tests/provider-token-usage.service.test.ts +188 -1
- package/server/modules/providers/tests/provider.routes.test.ts +230 -1
- package/server/modules/providers/tests/session-fork.test.ts +173 -0
- package/server/modules/providers/tests/session-history-cache.service.test.ts +165 -0
- package/server/modules/providers/tests/session-orphan-prune.test.ts +107 -0
- package/server/modules/providers/tests/session-synchronizer-coalescing.test.ts +136 -0
- package/server/modules/providers/tests/sessions.service.test.ts +303 -1
- package/server/modules/providers/tests/skills.test.ts +21 -5
- package/server/modules/scheduled-messages/index.ts +8 -0
- package/server/modules/scheduled-messages/scheduled-messages.routes.ts +68 -0
- package/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.ts +216 -0
- package/server/modules/scheduled-messages/services/scheduled-messages.service.ts +121 -0
- package/server/modules/scheduled-messages/tests/scheduled-messages.test.ts +354 -0
- package/server/modules/taskmaster/taskmaster.routes.ts +23 -33
- package/server/modules/taskmaster/tests/taskmaster.routes.test.ts +57 -0
- package/server/modules/user/tests/user.service.test.ts +90 -0
- package/server/modules/user/user.module.ts +10 -1
- package/server/modules/user/user.routes.ts +44 -0
- package/server/modules/user/user.service.ts +105 -0
- package/server/modules/voice/tests/voice.service.test.ts +3 -2
- package/server/modules/websocket/README.md +4 -2
- package/server/modules/websocket/index.ts +8 -0
- package/server/modules/websocket/services/chat-run-registry.service.ts +45 -54
- package/server/modules/websocket/services/chat-session-writer.service.ts +36 -6
- package/server/modules/websocket/services/chat-websocket.service.ts +302 -16
- package/server/modules/websocket/services/session-upsert-broadcast.service.ts +108 -0
- package/server/modules/websocket/services/shell-websocket.service.ts +276 -22
- package/server/modules/websocket/tests/chat-background-subscribe.test.ts +167 -0
- package/server/modules/websocket/tests/chat-edit-send.test.ts +309 -0
- package/server/modules/websocket/tests/chat-run-registry.test.ts +34 -3
- package/server/modules/websocket/tests/chat-stop-task.test.ts +127 -0
- package/server/modules/websocket/tests/session-upsert-broadcast.test.ts +138 -0
- package/server/modules/websocket/tests/shell-websocket.service.test.ts +629 -0
- package/server/shared/claude-cli-path.ts +24 -6
- package/server/shared/index.ts +3 -0
- package/server/shared/interfaces.ts +91 -0
- package/server/shared/message-unification.ts +449 -0
- package/server/shared/tests/claude-cli-path.test.ts +58 -1
- package/server/shared/tests/message-unification.test.ts +218 -0
- package/server/shared/tests/read-only-roots.test.ts +139 -0
- package/server/shared/tests/strip-ansi-sequences.test.ts +42 -0
- package/server/shared/types.ts +382 -7
- package/server/shared/utils.ts +176 -0
- package/dist/assets/channel-_falYbU1.js +0 -1
- package/dist/assets/classDiagram-JCYQIIEL-Dn-3CfpR.js +0 -1
- package/dist/assets/classDiagram-v2-OCEON4UE-Dn-3CfpR.js +0 -1
- package/dist/assets/index-CzPLBvXT.css +0 -32
- package/dist/assets/index-DFkBBuSA.js +0 -1549
- package/dist/assets/stateDiagram-v2-UEYNNEHI-BvBxUUrZ.js +0 -1
- package/dist/assets/swimlanesDiagram-ULZ7WXOC-CsE-x4YK.js +0 -8
|
@@ -1,24 +1,61 @@
|
|
|
1
1
|
import fsSync from 'node:fs';
|
|
2
|
+
import fsp from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
2
4
|
import readline from 'node:readline';
|
|
3
5
|
|
|
4
6
|
import { sessionsDb } from '@/modules/database/index.js';
|
|
7
|
+
import { codexAppServer } from '@/modules/providers/list/codex/codex-app-server.client.js';
|
|
5
8
|
import { parseFilesInputTag, toImageAttachments } from '@/shared/image-attachments.js';
|
|
6
9
|
import type { IProviderSessions } from '@/shared/interfaces.js';
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
10
|
+
import { prepareTranscriptMessages } from '@/shared/message-unification.js';
|
|
11
|
+
import type {
|
|
12
|
+
AnyRecord,
|
|
13
|
+
FetchHistoryOptions,
|
|
14
|
+
FetchHistoryResult,
|
|
15
|
+
MemoryCitation,
|
|
16
|
+
NormalizedMessage,
|
|
17
|
+
SubagentActivity,
|
|
18
|
+
SubagentInfo,
|
|
19
|
+
} from '@/shared/types.js';
|
|
20
|
+
import {
|
|
21
|
+
AppError,
|
|
22
|
+
createNormalizedMessage,
|
|
23
|
+
generateMessageId,
|
|
24
|
+
readObjectRecord,
|
|
25
|
+
sliceTailPage,
|
|
26
|
+
truncateSubagentActivity,
|
|
27
|
+
} from '@/shared/utils.js';
|
|
9
28
|
|
|
10
29
|
const PROVIDER = 'codex';
|
|
11
30
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
31
|
+
/**
|
|
32
|
+
* How far up the `~/.codex/sessions/<year>/<month>/<day>` tree a subagent
|
|
33
|
+
* rollout is searched for when it is not next to its parent. Spawned agents
|
|
34
|
+
* are written seconds after the parent, so one directory level up (the month)
|
|
35
|
+
* already covers a run that crosses midnight.
|
|
36
|
+
*/
|
|
37
|
+
const SUBAGENT_LOOKUP_PARENT_LEVELS = 2;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Upper bound on how much of a subagent's timeline is sent to the client. A
|
|
41
|
+
* long-running agent can record hundreds of tool calls, and the transcript only
|
|
42
|
+
* ever shows them behind a collapsed header, so shipping the whole history on
|
|
43
|
+
* every load costs far more than it shows.
|
|
44
|
+
*/
|
|
45
|
+
const MAX_TRANSMITTED_SUBAGENT_ACTIVITIES = 200;
|
|
46
|
+
|
|
47
|
+
type CodexHistoryResult = {
|
|
48
|
+
messages: AnyRecord[];
|
|
49
|
+
total?: number;
|
|
50
|
+
hasMore?: boolean;
|
|
51
|
+
offset?: number;
|
|
52
|
+
limit?: number | null;
|
|
53
|
+
tokenUsage?: unknown;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
function readNonEmptyString(value: unknown): string | undefined {
|
|
57
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
58
|
+
}
|
|
22
59
|
|
|
23
60
|
function isVisibleCodexUserMessage(payload: AnyRecord | null | undefined): boolean {
|
|
24
61
|
if (!payload || payload.type !== 'user_message') {
|
|
@@ -32,6 +69,96 @@ function isVisibleCodexUserMessage(payload: AnyRecord | null | undefined): boole
|
|
|
32
69
|
return typeof payload.message === 'string' && payload.message.trim().length > 0;
|
|
33
70
|
}
|
|
34
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Follows which turn a Codex rollout is inside as its rows stream past.
|
|
74
|
+
*
|
|
75
|
+
* Codex writes no per-row id — every line is `{timestamp, type, payload}` and
|
|
76
|
+
* the `user_message` event that carries a prompt has no identity of its own —
|
|
77
|
+
* so a turn id is the only durable address a user message has. Turns are
|
|
78
|
+
* bracketed by rows that do carry `turn_id`: `task_started` opens one,
|
|
79
|
+
* `turn_context` restates it, `task_complete` closes it, and the prompt is
|
|
80
|
+
* written between them. That id is also exactly what `thread/fork` cuts at,
|
|
81
|
+
* which is what makes "edit this message" and "fork from here" addressable
|
|
82
|
+
* for Codex at all.
|
|
83
|
+
*
|
|
84
|
+
* `thread_rolled_back` retires the last N turns in place: the rows stay in the
|
|
85
|
+
* file, but the turns are no longer part of the thread and the fork endpoint
|
|
86
|
+
* refuses to cut at one. They are tracked here so a retired turn is never
|
|
87
|
+
* offered as an anchor, rather than discovered when the fork fails.
|
|
88
|
+
*/
|
|
89
|
+
function createCodexTurnTracker() {
|
|
90
|
+
/** Turn ids still part of the conversation, oldest first. */
|
|
91
|
+
const liveTurnIds: string[] = [];
|
|
92
|
+
const seenTurnIds = new Set<string>();
|
|
93
|
+
const rolledBackTurnIds = new Set<string>();
|
|
94
|
+
let currentTurnId: string | undefined;
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
/** Feeds one rollout row in. */
|
|
98
|
+
observe(entryType: unknown, payload: AnyRecord): void {
|
|
99
|
+
if (entryType === 'event_msg' && payload.type === 'thread_rolled_back') {
|
|
100
|
+
const retiredTurns = Number(payload.num_turns);
|
|
101
|
+
for (let retired = 0; retired < retiredTurns && liveTurnIds.length > 0; retired++) {
|
|
102
|
+
rolledBackTurnIds.add(liveTurnIds.pop() as string);
|
|
103
|
+
}
|
|
104
|
+
currentTurnId = undefined;
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const turnId = readNonEmptyString(payload.turn_id);
|
|
109
|
+
if (!turnId) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
currentTurnId = turnId;
|
|
113
|
+
if (!seenTurnIds.has(turnId)) {
|
|
114
|
+
seenTurnIds.add(turnId);
|
|
115
|
+
liveTurnIds.push(turnId);
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
/** The turn the rows being read belong to, or undefined outside one. */
|
|
119
|
+
getCurrentTurnId(): string | undefined {
|
|
120
|
+
return currentTurnId;
|
|
121
|
+
},
|
|
122
|
+
getLiveTurnIds(): string[] {
|
|
123
|
+
return liveTurnIds;
|
|
124
|
+
},
|
|
125
|
+
isRolledBack(turnId: string): boolean {
|
|
126
|
+
return rolledBackTurnIds.has(turnId);
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Reads the turns a Codex rollout still contains, oldest first.
|
|
133
|
+
*
|
|
134
|
+
* A separate pass rather than a by-product of the transcript reader: this runs
|
|
135
|
+
* once when a message is edited, while the reader runs on every history fetch,
|
|
136
|
+
* and both share the one rule for what a live turn is.
|
|
137
|
+
*/
|
|
138
|
+
async function readCodexLiveTurnIds(filePath: string): Promise<string[]> {
|
|
139
|
+
const turns = createCodexTurnTracker();
|
|
140
|
+
const stream = fsSync.createReadStream(filePath);
|
|
141
|
+
const lines = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
142
|
+
|
|
143
|
+
for await (const line of lines) {
|
|
144
|
+
if (!line.trim()) {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
let entry: AnyRecord;
|
|
148
|
+
try {
|
|
149
|
+
entry = JSON.parse(line) as AnyRecord;
|
|
150
|
+
} catch {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const payload = readObjectRecord(entry.payload);
|
|
154
|
+
if (payload) {
|
|
155
|
+
turns.observe(entry.type, payload);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return turns.getLiveTurnIds();
|
|
160
|
+
}
|
|
161
|
+
|
|
35
162
|
/**
|
|
36
163
|
* Reads the image attachments Codex records on `user_message` events.
|
|
37
164
|
* Turns sent with `local_image` input items land in `local_images` as file
|
|
@@ -93,6 +220,111 @@ function extractCodexTextContent(content: unknown): string {
|
|
|
93
220
|
.join('\n');
|
|
94
221
|
}
|
|
95
222
|
|
|
223
|
+
/** Reads image inputs from a typed UserMessage item. */
|
|
224
|
+
function extractCodexTypedUserImages(
|
|
225
|
+
content: unknown,
|
|
226
|
+
): Array<{ path?: string; data?: string }> | undefined {
|
|
227
|
+
if (!Array.isArray(content)) {
|
|
228
|
+
return undefined;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const attachments: Array<{ path?: string; data?: string }> = [];
|
|
232
|
+
for (const rawEntry of content) {
|
|
233
|
+
const entry = readObjectRecord(rawEntry);
|
|
234
|
+
if (!entry) {
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const value = entry.type === 'local_image'
|
|
239
|
+
? readNonEmptyString(entry.path)
|
|
240
|
+
: entry.type === 'image'
|
|
241
|
+
? readNonEmptyString(entry.image_url)
|
|
242
|
+
: undefined;
|
|
243
|
+
if (!value) {
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (value.startsWith('data:')) {
|
|
247
|
+
attachments.push({ data: value });
|
|
248
|
+
} else {
|
|
249
|
+
attachments.push(...toImageAttachments([value]));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return attachments.length > 0 ? attachments : undefined;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Reads the markdown out of Codex's `<proposed_plan>` envelope.
|
|
258
|
+
*
|
|
259
|
+
* Codex delivers a plan as a wrapped assistant message while Claude delivers
|
|
260
|
+
* one as an `ExitPlanMode` call. Recognizing the envelope here lets both end up
|
|
261
|
+
* on the same plan card instead of one provider showing a card and the other
|
|
262
|
+
* showing prose with a stray tag in it. The closing tag is optional because a
|
|
263
|
+
* streamed plan exposes the opening tag first.
|
|
264
|
+
*
|
|
265
|
+
* Exported for tests.
|
|
266
|
+
*/
|
|
267
|
+
export function readCodexProposedPlan(text: string): string | null {
|
|
268
|
+
const openingTag = /^\s*<proposed_plan>[ \t]*(?:\r?\n)?/i;
|
|
269
|
+
if (!openingTag.test(text)) {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const body = text
|
|
274
|
+
.replace(openingTag, '')
|
|
275
|
+
.replace(/(?:\r?\n)?[ \t]*<\/proposed_plan>\s*$/i, '')
|
|
276
|
+
.trim();
|
|
277
|
+
|
|
278
|
+
return body || null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* The block Codex appends to a reply that leaned on its memory files. It is
|
|
283
|
+
* always the last thing in the message and is meant for programmatic parsing,
|
|
284
|
+
* so it reads as raw XML when left in the prose.
|
|
285
|
+
*/
|
|
286
|
+
const CODEX_MEMORY_CITATION_BLOCK = /<oai-mem-citation>([\s\S]*?)<\/oai-mem-citation>\s*$/i;
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Lifts Codex's memory citations out of an assistant reply.
|
|
290
|
+
*
|
|
291
|
+
* The block names the memory files and line ranges the answer drew on, which is
|
|
292
|
+
* worth showing — but as a footnote under the reply, not as markup inside it.
|
|
293
|
+
* The trailing `<rollout_ids>` list is dropped: those are handles to earlier
|
|
294
|
+
* rollouts that the transcript view has no way to open.
|
|
295
|
+
*
|
|
296
|
+
* Exported for tests.
|
|
297
|
+
*/
|
|
298
|
+
export function readCodexMemoryCitations(text: string): { text: string; memoryCitations?: MemoryCitation[] } {
|
|
299
|
+
const block = CODEX_MEMORY_CITATION_BLOCK.exec(text);
|
|
300
|
+
if (!block) {
|
|
301
|
+
return { text };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const entries = /<citation_entries>([\s\S]*?)<\/citation_entries>/i.exec(block[1])?.[1] ?? '';
|
|
305
|
+
const memoryCitations: MemoryCitation[] = [];
|
|
306
|
+
for (const line of entries.split('\n')) {
|
|
307
|
+
const entry = line.trim();
|
|
308
|
+
if (!entry) {
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const noteMarker = entry.indexOf('|note=');
|
|
313
|
+
const source = (noteMarker === -1 ? entry : entry.slice(0, noteMarker)).trim();
|
|
314
|
+
if (!source) {
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const note = noteMarker === -1
|
|
319
|
+
? ''
|
|
320
|
+
: entry.slice(noteMarker + '|note='.length).trim().replace(/^\[/, '').replace(/\]$/, '').trim();
|
|
321
|
+
memoryCitations.push(note ? { source, note } : { source });
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const prose = text.slice(0, block.index).trimEnd();
|
|
325
|
+
return memoryCitations.length > 0 ? { text: prose, memoryCitations } : { text: prose };
|
|
326
|
+
}
|
|
327
|
+
|
|
96
328
|
function extractCodexToolOutput(output: unknown): string {
|
|
97
329
|
if (typeof output === 'string') {
|
|
98
330
|
return output;
|
|
@@ -111,19 +343,30 @@ function extractCodexToolOutput(output: unknown): string {
|
|
|
111
343
|
.join('');
|
|
112
344
|
}
|
|
113
345
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
346
|
+
// ─── Codex "code mode" exec scripts ─────────────────────────────────────────
|
|
347
|
+
// Newer Codex builds (and every Sol-family model) stop calling shell/patch
|
|
348
|
+
// tools directly. Instead they emit one `exec` custom tool call whose input is
|
|
349
|
+
// a small JavaScript program that awaits `tools.<name>({...})`. Persisting that
|
|
350
|
+
// program verbatim is what makes the transcript read as raw machinery, so it is
|
|
351
|
+
// parsed back into the operations it performs.
|
|
119
352
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
}
|
|
353
|
+
type CodexExecOperation =
|
|
354
|
+
| { kind: 'shell'; command: string; justification?: string }
|
|
355
|
+
| { kind: 'stdin'; chars: string }
|
|
356
|
+
| { kind: 'patch'; patch: string }
|
|
357
|
+
| { kind: 'search'; queries: string[] }
|
|
358
|
+
| { kind: 'plan'; todos: CodexPlanTodo[] };
|
|
126
359
|
|
|
360
|
+
/** One step of a checklist, in the shape the shared todo renderer consumes. */
|
|
361
|
+
type CodexPlanTodo = { content: string; status: string };
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Decodes one JavaScript string literal (single, double or backtick quoted).
|
|
365
|
+
*
|
|
366
|
+
* Template literals with `${}` interpolation are returned with the expression
|
|
367
|
+
* text intact — the transcript has no bindings to evaluate, and showing the
|
|
368
|
+
* raw template is still far more readable than the surrounding program.
|
|
369
|
+
*/
|
|
127
370
|
function decodeJavaScriptStringLiteral(literal: string): string {
|
|
128
371
|
if (literal.startsWith('"')) {
|
|
129
372
|
try {
|
|
@@ -141,62 +384,498 @@ function decodeJavaScriptStringLiteral(literal: string): string {
|
|
|
141
384
|
.replace(/\\([\\'`])/g, '$1');
|
|
142
385
|
}
|
|
143
386
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
387
|
+
/**
|
|
388
|
+
* Returns the source of the argument list of the call that opens at
|
|
389
|
+
* `openParenIndex`, skipping over parens that appear inside string literals.
|
|
390
|
+
*
|
|
391
|
+
* A plain regex cannot be used here: shell commands routinely embed `(`/`)`
|
|
392
|
+
* and quotes, so the argument object has to be found by balancing.
|
|
393
|
+
*/
|
|
394
|
+
function readCallArguments(source: string, openParenIndex: number): string | null {
|
|
395
|
+
let depth = 0;
|
|
396
|
+
let quote: string | null = null;
|
|
150
397
|
|
|
151
|
-
|
|
152
|
-
const
|
|
153
|
-
const stringPattern = /("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`)/g;
|
|
398
|
+
for (let index = openParenIndex; index < source.length; index += 1) {
|
|
399
|
+
const character = source[index];
|
|
154
400
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
401
|
+
if (quote) {
|
|
402
|
+
if (character === '\\') {
|
|
403
|
+
index += 1;
|
|
158
404
|
continue;
|
|
159
405
|
}
|
|
160
|
-
|
|
161
|
-
|
|
406
|
+
if (character === quote) {
|
|
407
|
+
quote = null;
|
|
408
|
+
}
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (character === '"' || character === "'" || character === '`') {
|
|
413
|
+
quote = character;
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (character === '(') {
|
|
418
|
+
depth += 1;
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (character === ')') {
|
|
423
|
+
depth -= 1;
|
|
424
|
+
if (depth === 0) {
|
|
425
|
+
return source.slice(openParenIndex + 1, index);
|
|
162
426
|
}
|
|
163
427
|
}
|
|
164
428
|
}
|
|
165
429
|
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/** Reads `key: "value"` / `"key": "value"` out of an object-literal source. */
|
|
434
|
+
function readStringProperty(objectSource: string, key: string): string | undefined {
|
|
435
|
+
const pattern = new RegExp(
|
|
436
|
+
`(?:["']${key}["']|\\b${key})\\s*:\\s*("(?:\\\\.|[^"\\\\])*"|'(?:\\\\.|[^'\\\\])*'|\`(?:\\\\.|[^\`\\\\])*\`)`,
|
|
437
|
+
's',
|
|
438
|
+
);
|
|
439
|
+
const match = pattern.exec(objectSource);
|
|
440
|
+
return match ? decodeJavaScriptStringLiteral(match[1]) : undefined;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** Reads every `q: "..."` entry out of a `web__run` search-query array. */
|
|
444
|
+
function readSearchQueries(objectSource: string): string[] {
|
|
445
|
+
const pattern = /(?:["']q["']|\bq)\s*:\s*("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`)/gs;
|
|
446
|
+
return Array.from(objectSource.matchAll(pattern), (match) => decodeJavaScriptStringLiteral(match[1]));
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Finds the patch body an `apply_patch` call is given.
|
|
451
|
+
*
|
|
452
|
+
* The program almost always assigns it to a local first
|
|
453
|
+
* (`const patch = "*** Begin Patch..."; tools.apply_patch(patch)`), so the
|
|
454
|
+
* argument itself is usually an identifier. Scanning the whole script for the
|
|
455
|
+
* literal that carries the patch header covers both forms.
|
|
456
|
+
*/
|
|
457
|
+
function readPatchLiteral(source: string): string | undefined {
|
|
458
|
+
const pattern = /("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`)/gs;
|
|
459
|
+
for (const match of source.matchAll(pattern)) {
|
|
460
|
+
const decoded = decodeJavaScriptStringLiteral(match[1]);
|
|
461
|
+
if (decoded.includes('*** Begin Patch')) {
|
|
462
|
+
return decoded;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return undefined;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Reads the steps of an `update_plan` call made from an exec script.
|
|
470
|
+
*
|
|
471
|
+
* The script writes its argument as a JavaScript object literal with unquoted
|
|
472
|
+
* keys, so it is not JSON — each `{step, status}` entry is matched directly.
|
|
473
|
+
*/
|
|
474
|
+
function readPlanSteps(objectSource: string): CodexPlanTodo[] {
|
|
475
|
+
const todos: CodexPlanTodo[] = [];
|
|
476
|
+
for (const entry of objectSource.matchAll(/\{([^{}]*)\}/g)) {
|
|
477
|
+
const content = readStringProperty(entry[1], 'step') ?? readStringProperty(entry[1], 'content');
|
|
478
|
+
if (content) {
|
|
479
|
+
todos.push({ content, status: readStringProperty(entry[1], 'status') ?? 'pending' });
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return todos;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Recovers commands a script keeps in a local array and maps over, which is how
|
|
488
|
+
* Codex batches several shell calls:
|
|
489
|
+
* `const cmds = ["a", "b"]; await Promise.all(cmds.map(command => tools.shell_command({ command })))`
|
|
490
|
+
* The call site then passes `{ command }` shorthand, so there is no literal to
|
|
491
|
+
* read at the call itself.
|
|
492
|
+
*/
|
|
493
|
+
function readMappedCommandCandidates(source: string): string[] {
|
|
494
|
+
const commands: string[] = [];
|
|
495
|
+
const arrayPattern = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*\[([\s\S]*?)\]\s*;/g;
|
|
496
|
+
const stringPattern = /("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`)/gs;
|
|
497
|
+
|
|
498
|
+
for (const arrayMatch of source.matchAll(arrayPattern)) {
|
|
499
|
+
if (!new RegExp(`\\b${arrayMatch[1]}\\.map\\s*\\(`).test(source)) {
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
for (const stringMatch of arrayMatch[2].matchAll(stringPattern)) {
|
|
503
|
+
commands.push(decodeJavaScriptStringLiteral(stringMatch[1]));
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
166
507
|
return commands;
|
|
167
508
|
}
|
|
168
509
|
|
|
169
510
|
/**
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
511
|
+
* Turns one Codex `exec` script into the operations it performs, in source
|
|
512
|
+
* order. An empty result means the script did nothing recognizable and the
|
|
513
|
+
* caller should fall back to showing it as code.
|
|
514
|
+
*
|
|
515
|
+
* Exported for tests.
|
|
173
516
|
*/
|
|
174
|
-
function
|
|
175
|
-
const source = typeof input === 'string' ? input : String(input
|
|
176
|
-
if (
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
517
|
+
export function parseCodexExecScript(input: unknown): CodexExecOperation[] {
|
|
518
|
+
const source = typeof input === 'string' ? input : String(input ?? '');
|
|
519
|
+
if (!source.includes('tools.')) {
|
|
520
|
+
return [];
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const operations: CodexExecOperation[] = [];
|
|
524
|
+
const callPattern = /tools\.([A-Za-z_0-9]+)\s*\(/g;
|
|
525
|
+
/** Filled in lazily, and only once, for shorthand-argument shell calls. */
|
|
526
|
+
let mappedCommands: string[] | null = null;
|
|
527
|
+
|
|
528
|
+
for (const match of source.matchAll(callPattern)) {
|
|
529
|
+
const toolName = match[1];
|
|
530
|
+
const openParenIndex = match.index + match[0].length - 1;
|
|
531
|
+
const argumentSource = readCallArguments(source, openParenIndex) ?? '';
|
|
532
|
+
|
|
533
|
+
switch (toolName) {
|
|
534
|
+
case 'shell_command':
|
|
535
|
+
case 'exec_command': {
|
|
536
|
+
const command = readStringProperty(argumentSource, 'command')
|
|
537
|
+
?? readStringProperty(argumentSource, 'cmd');
|
|
538
|
+
if (command) {
|
|
539
|
+
operations.push({
|
|
540
|
+
kind: 'shell',
|
|
541
|
+
command,
|
|
542
|
+
justification: readStringProperty(argumentSource, 'justification'),
|
|
543
|
+
});
|
|
544
|
+
break;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (mappedCommands === null) {
|
|
548
|
+
mappedCommands = readMappedCommandCandidates(source);
|
|
549
|
+
for (const mappedCommand of mappedCommands) {
|
|
550
|
+
operations.push({ kind: 'shell', command: mappedCommand });
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
break;
|
|
554
|
+
}
|
|
555
|
+
case 'write_stdin': {
|
|
556
|
+
operations.push({ kind: 'stdin', chars: readStringProperty(argumentSource, 'chars') ?? '' });
|
|
557
|
+
break;
|
|
558
|
+
}
|
|
559
|
+
case 'apply_patch': {
|
|
560
|
+
const patch = readPatchLiteral(argumentSource) ?? readPatchLiteral(source);
|
|
561
|
+
if (patch) {
|
|
562
|
+
operations.push({ kind: 'patch', patch });
|
|
563
|
+
}
|
|
564
|
+
break;
|
|
565
|
+
}
|
|
566
|
+
case 'web__run':
|
|
567
|
+
case 'web_search': {
|
|
568
|
+
const queries = readSearchQueries(argumentSource);
|
|
569
|
+
if (queries.length > 0) {
|
|
570
|
+
operations.push({ kind: 'search', queries });
|
|
571
|
+
}
|
|
572
|
+
break;
|
|
573
|
+
}
|
|
574
|
+
case 'update_plan': {
|
|
575
|
+
const todos = readPlanSteps(argumentSource);
|
|
576
|
+
if (todos.length > 0) {
|
|
577
|
+
operations.push({ kind: 'plan', todos });
|
|
578
|
+
}
|
|
579
|
+
break;
|
|
580
|
+
}
|
|
581
|
+
default:
|
|
582
|
+
break;
|
|
183
583
|
}
|
|
184
584
|
}
|
|
185
585
|
|
|
186
|
-
return
|
|
586
|
+
return operations;
|
|
187
587
|
}
|
|
188
588
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
589
|
+
// ─── Codex shell output ─────────────────────────────────────────────────────
|
|
590
|
+
|
|
591
|
+
type CodexShellOutput = {
|
|
592
|
+
output: string;
|
|
593
|
+
/** Set when the command has exited; `undefined` while it is still running. */
|
|
594
|
+
exitCode?: number;
|
|
595
|
+
/** Present while the command runs in the background under this cell id. */
|
|
596
|
+
runningCellId?: string;
|
|
597
|
+
truncated: boolean;
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Strips the two nested envelopes Codex wraps shell output in.
|
|
602
|
+
*
|
|
603
|
+
* The outer one reports on the sandbox *script* (`Script completed`, its wall
|
|
604
|
+
* time, and `Output:`); the inner one reports on the *command* (`Exit code`,
|
|
605
|
+
* its own wall time, an optional line count). Neither is useful in a chat
|
|
606
|
+
* transcript, but the exit code decides whether the row is shown as failed, so
|
|
607
|
+
* it is lifted out before the text is discarded.
|
|
608
|
+
*
|
|
609
|
+
* Exported for tests.
|
|
610
|
+
*/
|
|
611
|
+
export function cleanCodexShellOutput(rawOutput: string): CodexShellOutput {
|
|
612
|
+
let output = rawOutput;
|
|
613
|
+
let exitCode: number | undefined;
|
|
614
|
+
let runningCellId: string | undefined;
|
|
615
|
+
let truncated = false;
|
|
616
|
+
|
|
617
|
+
const runningMatch = /^Script running with cell ID\s+(\S+)[^\n]*\n?/i.exec(output);
|
|
618
|
+
if (runningMatch) {
|
|
619
|
+
runningCellId = runningMatch[1];
|
|
620
|
+
output = output.slice(runningMatch[0].length);
|
|
621
|
+
} else {
|
|
622
|
+
output = output.replace(/^Script (?:completed|failed)[^\n]*\n?/i, '');
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
output = output.replace(/^Wall time[^\n]*\n?/i, '');
|
|
626
|
+
output = output.replace(/^Output:\s*\n?/i, '');
|
|
627
|
+
output = output.replace(/^Script error:\s*\n?/i, '');
|
|
628
|
+
|
|
629
|
+
const truncationMatch = /^Warning: truncated output[^\n]*\n(?:Total output lines:[^\n]*\n)?\n?/i.exec(output);
|
|
630
|
+
if (truncationMatch) {
|
|
631
|
+
truncated = true;
|
|
632
|
+
output = output.slice(truncationMatch[0].length);
|
|
633
|
+
output = output.replace(/^Script error:\s*\n?/i, '');
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const exitCodeMatch = /^Exit code:\s*(-?\d+)[^\n]*\n?/i.exec(output);
|
|
637
|
+
if (exitCodeMatch) {
|
|
638
|
+
exitCode = Number.parseInt(exitCodeMatch[1], 10);
|
|
639
|
+
output = output.slice(exitCodeMatch[0].length);
|
|
640
|
+
output = output.replace(/^Wall time:[^\n]*\n?/i, '');
|
|
641
|
+
output = output.replace(/^Total output lines:[^\n]*\n?/i, '');
|
|
642
|
+
output = output.replace(/^Output:\s*\n?/i, '');
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
return { output, exitCode, runningCellId, truncated };
|
|
194
646
|
}
|
|
195
647
|
|
|
648
|
+
/**
|
|
649
|
+
* `write_stdin` answers with a JSON envelope rather than plain text. Only its
|
|
650
|
+
* `output` and `exit_code` matter to a reader.
|
|
651
|
+
*/
|
|
652
|
+
function readCodexStdinOutput(rawOutput: string): CodexShellOutput | null {
|
|
653
|
+
const trimmed = rawOutput.trim();
|
|
654
|
+
const jsonStart = trimmed.indexOf('{');
|
|
655
|
+
if (jsonStart === -1) {
|
|
656
|
+
return null;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
try {
|
|
660
|
+
const parsed = JSON.parse(trimmed.slice(jsonStart)) as AnyRecord;
|
|
661
|
+
if (typeof parsed.output !== 'string') {
|
|
662
|
+
return null;
|
|
663
|
+
}
|
|
664
|
+
return {
|
|
665
|
+
output: parsed.output,
|
|
666
|
+
exitCode: typeof parsed.exit_code === 'number' ? parsed.exit_code : undefined,
|
|
667
|
+
truncated: false,
|
|
668
|
+
};
|
|
669
|
+
} catch {
|
|
670
|
+
return null;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// ─── Unified patches ────────────────────────────────────────────────────────
|
|
675
|
+
|
|
676
|
+
type CodexFileChange = {
|
|
677
|
+
filePath: string;
|
|
678
|
+
/** `add` and `delete` are rendered as a whole-file write/removal. */
|
|
679
|
+
changeType: 'add' | 'update' | 'delete';
|
|
680
|
+
oldText: string;
|
|
681
|
+
newText: string;
|
|
682
|
+
};
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* Rebuilds the before/after text of one unified diff.
|
|
686
|
+
*
|
|
687
|
+
* Only the hunks are available — never the whole file — so the reconstruction
|
|
688
|
+
* keeps context lines in both sides and drops hunk headers. Feeding that pair
|
|
689
|
+
* to the frontend's line differ reproduces exactly the additions, removals and
|
|
690
|
+
* surrounding context the patch described, which is what Claude's `Edit` rows
|
|
691
|
+
* already show.
|
|
692
|
+
*
|
|
693
|
+
* Exported for tests.
|
|
694
|
+
*/
|
|
695
|
+
export function unifiedDiffToTexts(unifiedDiff: string): { oldText: string; newText: string } {
|
|
696
|
+
const oldLines: string[] = [];
|
|
697
|
+
const newLines: string[] = [];
|
|
698
|
+
|
|
699
|
+
for (const line of unifiedDiff.split('\n')) {
|
|
700
|
+
if (line.startsWith('@@') || line.startsWith('\\ No newline')) {
|
|
701
|
+
continue;
|
|
702
|
+
}
|
|
703
|
+
if (line.startsWith('+')) {
|
|
704
|
+
newLines.push(line.slice(1));
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
if (line.startsWith('-')) {
|
|
708
|
+
oldLines.push(line.slice(1));
|
|
709
|
+
continue;
|
|
710
|
+
}
|
|
711
|
+
const contextLine = line.startsWith(' ') ? line.slice(1) : line;
|
|
712
|
+
oldLines.push(contextLine);
|
|
713
|
+
newLines.push(contextLine);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// A unified diff ends with a newline, which would otherwise reconstruct as a
|
|
717
|
+
// phantom trailing blank line on both sides.
|
|
718
|
+
if (oldLines[oldLines.length - 1] === '' && newLines[newLines.length - 1] === '') {
|
|
719
|
+
oldLines.pop();
|
|
720
|
+
newLines.pop();
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
return { oldText: oldLines.join('\n'), newText: newLines.join('\n') };
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Splits a `*** Begin Patch` body into one change per file.
|
|
728
|
+
*
|
|
729
|
+
* Exported for tests.
|
|
730
|
+
*/
|
|
731
|
+
export function parseCodexPatch(patch: string): CodexFileChange[] {
|
|
732
|
+
const changes: CodexFileChange[] = [];
|
|
733
|
+
const lines = patch.split('\n');
|
|
734
|
+
let current: { filePath: string; changeType: CodexFileChange['changeType']; body: string[] } | null = null;
|
|
735
|
+
|
|
736
|
+
const flush = () => {
|
|
737
|
+
if (!current) {
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
const { oldText, newText } = unifiedDiffToTexts(current.body.join('\n'));
|
|
741
|
+
changes.push({
|
|
742
|
+
filePath: current.filePath,
|
|
743
|
+
changeType: current.changeType,
|
|
744
|
+
oldText: current.changeType === 'add' ? '' : oldText,
|
|
745
|
+
newText: current.changeType === 'delete' ? '' : newText,
|
|
746
|
+
});
|
|
747
|
+
current = null;
|
|
748
|
+
};
|
|
749
|
+
|
|
750
|
+
for (const line of lines) {
|
|
751
|
+
const fileHeader = /^\*\*\* (Add|Update|Delete) File:\s*(.+)$/.exec(line);
|
|
752
|
+
if (fileHeader) {
|
|
753
|
+
flush();
|
|
754
|
+
const changeType = fileHeader[1].toLowerCase() as CodexFileChange['changeType'];
|
|
755
|
+
current = { filePath: fileHeader[2].trim(), changeType, body: [] };
|
|
756
|
+
continue;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
if (line.startsWith('*** ')) {
|
|
760
|
+
// `*** Begin Patch`, `*** End Patch`, `*** Move to:` — never diff content.
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
current?.body.push(line);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
flush();
|
|
768
|
+
return changes;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/** Converts a `patch_apply_end` change map into the same per-file shape. */
|
|
772
|
+
function readPatchApplyChanges(changes: unknown): CodexFileChange[] {
|
|
773
|
+
const record = readObjectRecord(changes);
|
|
774
|
+
if (!record) {
|
|
775
|
+
return [];
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
const parsed: CodexFileChange[] = [];
|
|
779
|
+
for (const [filePath, rawChange] of Object.entries(record)) {
|
|
780
|
+
const change = readObjectRecord(rawChange);
|
|
781
|
+
if (!change) {
|
|
782
|
+
continue;
|
|
783
|
+
}
|
|
784
|
+
const changeType = change.type === 'add' || change.type === 'delete' ? change.type : 'update';
|
|
785
|
+
const { oldText, newText } = unifiedDiffToTexts(
|
|
786
|
+
typeof change.unified_diff === 'string' ? change.unified_diff : '',
|
|
787
|
+
);
|
|
788
|
+
parsed.push({
|
|
789
|
+
filePath,
|
|
790
|
+
changeType,
|
|
791
|
+
oldText: changeType === 'add' ? '' : oldText,
|
|
792
|
+
newText: changeType === 'delete' ? '' : newText,
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
return parsed;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/** Maps a file change onto the tool the frontend already renders diffs for. */
|
|
800
|
+
function fileChangeToToolMessage(change: CodexFileChange, timestamp: string, toolCallId: string): AnyRecord {
|
|
801
|
+
const toolName = change.changeType === 'add' ? 'Write' : 'Edit';
|
|
802
|
+
return {
|
|
803
|
+
type: 'tool_use',
|
|
804
|
+
timestamp,
|
|
805
|
+
toolName,
|
|
806
|
+
toolInput: JSON.stringify({
|
|
807
|
+
file_path: change.filePath,
|
|
808
|
+
old_string: change.oldText,
|
|
809
|
+
new_string: change.newText,
|
|
810
|
+
...(change.changeType === 'delete' ? { deleted: true } : {}),
|
|
811
|
+
}),
|
|
812
|
+
toolCallId,
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/** Reads the `file_path` back out of a rendered file-change row. */
|
|
817
|
+
function readRowFilePath(row: AnyRecord): string {
|
|
818
|
+
try {
|
|
819
|
+
return String((JSON.parse(String(row.toolInput ?? '{}')) as AnyRecord).file_path ?? '');
|
|
820
|
+
} catch {
|
|
821
|
+
return '';
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/**
|
|
826
|
+
* Rewrites the rows reconstructed from a patch call's input with the
|
|
827
|
+
* authoritative diffs Codex reports once the patch has been applied.
|
|
828
|
+
*
|
|
829
|
+
* Rows are matched by path, never by position: `patch_apply_end` reports the
|
|
830
|
+
* touched files in map order, which does not follow the order they appeared in
|
|
831
|
+
* the patch body. Rows whose file the report does not mention are dropped
|
|
832
|
+
* rather than left behind claiming an edit that never happened.
|
|
833
|
+
*/
|
|
834
|
+
function rewritePatchRows(
|
|
835
|
+
rows: AnyRecord[],
|
|
836
|
+
changes: CodexFileChange[],
|
|
837
|
+
callId: string,
|
|
838
|
+
messages: AnyRecord[],
|
|
839
|
+
): void {
|
|
840
|
+
const rowsByPath = new Map<string, AnyRecord>();
|
|
841
|
+
for (const row of rows) {
|
|
842
|
+
rowsByPath.set(readRowFilePath(row), row);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
const keptRows: AnyRecord[] = [];
|
|
846
|
+
for (const [index, change] of changes.entries()) {
|
|
847
|
+
const replacement = fileChangeToToolMessage(change, String(rows[0]?.timestamp ?? ''), `${callId}#${index}`);
|
|
848
|
+
const existing = rowsByPath.get(change.filePath);
|
|
849
|
+
if (existing) {
|
|
850
|
+
existing.toolName = replacement.toolName;
|
|
851
|
+
existing.toolInput = replacement.toolInput;
|
|
852
|
+
keptRows.push(existing);
|
|
853
|
+
rowsByPath.delete(change.filePath);
|
|
854
|
+
continue;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
replacement.timestamp = rows[0]?.timestamp ?? replacement.timestamp;
|
|
858
|
+
keptRows.push(replacement);
|
|
859
|
+
messages.push(replacement);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
for (const surplus of rowsByPath.values()) {
|
|
863
|
+
const position = messages.indexOf(surplus);
|
|
864
|
+
if (position !== -1) {
|
|
865
|
+
messages.splice(position, 1);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
rows.splice(0, rows.length, ...keptRows);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// ─── Subagents ──────────────────────────────────────────────────────────────
|
|
873
|
+
|
|
196
874
|
type CodexSubagentRecord = {
|
|
197
875
|
toolCallId: string;
|
|
198
876
|
message: AnyRecord;
|
|
199
877
|
agentPath?: string;
|
|
878
|
+
agentThreadId?: string;
|
|
200
879
|
isComplete: boolean;
|
|
201
880
|
};
|
|
202
881
|
|
|
@@ -212,11 +891,289 @@ function parseCodexSubagentMessage(payload: AnyRecord): {
|
|
|
212
891
|
return null;
|
|
213
892
|
}
|
|
214
893
|
|
|
215
|
-
return {
|
|
216
|
-
author,
|
|
217
|
-
messageType: header?.[1]?.trim().toUpperCase() || 'MESSAGE',
|
|
218
|
-
result: header?.[3]?.trim() || '',
|
|
219
|
-
};
|
|
894
|
+
return {
|
|
895
|
+
author,
|
|
896
|
+
messageType: header?.[1]?.trim().toUpperCase() || 'MESSAGE',
|
|
897
|
+
result: header?.[3]?.trim() || '',
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
/**
|
|
902
|
+
* Codex names a spawned agent's rollout file `rollout-<timestamp>-<threadId>`
|
|
903
|
+
* and writes it next to the parent's. Look there first, then widen to the
|
|
904
|
+
* enclosing day/month directories so a run that crosses midnight still
|
|
905
|
+
* resolves, and give up rather than walking the whole archive.
|
|
906
|
+
*/
|
|
907
|
+
async function findCodexSubagentRollout(
|
|
908
|
+
parentFilePath: string,
|
|
909
|
+
agentThreadId: string,
|
|
910
|
+
): Promise<string | null> {
|
|
911
|
+
const suffix = `-${agentThreadId}.jsonl`;
|
|
912
|
+
let directory = path.dirname(parentFilePath);
|
|
913
|
+
|
|
914
|
+
for (let level = 0; level <= SUBAGENT_LOOKUP_PARENT_LEVELS; level += 1) {
|
|
915
|
+
const match = await findFileWithSuffix(directory, suffix, level === 0 ? 0 : level);
|
|
916
|
+
if (match) {
|
|
917
|
+
return match;
|
|
918
|
+
}
|
|
919
|
+
const parent = path.dirname(directory);
|
|
920
|
+
if (parent === directory) {
|
|
921
|
+
break;
|
|
922
|
+
}
|
|
923
|
+
directory = parent;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
return null;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/** Depth-limited search for a file whose name ends with `suffix`. */
|
|
930
|
+
async function findFileWithSuffix(directory: string, suffix: string, depth: number): Promise<string | null> {
|
|
931
|
+
let entries;
|
|
932
|
+
try {
|
|
933
|
+
entries = await fsp.readdir(directory, { withFileTypes: true });
|
|
934
|
+
} catch {
|
|
935
|
+
return null;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
const subdirectories: string[] = [];
|
|
939
|
+
for (const entry of entries) {
|
|
940
|
+
if (entry.isDirectory()) {
|
|
941
|
+
subdirectories.push(path.join(directory, entry.name));
|
|
942
|
+
continue;
|
|
943
|
+
}
|
|
944
|
+
if (entry.name.endsWith(suffix)) {
|
|
945
|
+
return path.join(directory, entry.name);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
if (depth <= 0) {
|
|
950
|
+
return null;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
for (const subdirectory of subdirectories) {
|
|
954
|
+
const match = await findFileWithSuffix(subdirectory, suffix, depth - 1);
|
|
955
|
+
if (match) {
|
|
956
|
+
return match;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
return null;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
type CodexSubagentTranscript = {
|
|
964
|
+
activity: SubagentActivity[];
|
|
965
|
+
nickname?: string;
|
|
966
|
+
agentPath?: string;
|
|
967
|
+
model?: string;
|
|
968
|
+
};
|
|
969
|
+
|
|
970
|
+
/**
|
|
971
|
+
* Flattens a spawned agent's own rollout into the shared activity timeline.
|
|
972
|
+
*
|
|
973
|
+
* The subagent's transcript is the same format as the parent's, so its shell
|
|
974
|
+
* and patch calls go through the same translation and end up rendered by the
|
|
975
|
+
* same components as the main thread's.
|
|
976
|
+
*/
|
|
977
|
+
async function readCodexSubagentTranscript(filePath: string): Promise<CodexSubagentTranscript> {
|
|
978
|
+
const activity: SubagentActivity[] = [];
|
|
979
|
+
const transcript: CodexSubagentTranscript = { activity };
|
|
980
|
+
const pendingResults = new Map<string, SubagentActivity>();
|
|
981
|
+
|
|
982
|
+
let fileStream;
|
|
983
|
+
try {
|
|
984
|
+
fileStream = fsSync.createReadStream(filePath);
|
|
985
|
+
} catch {
|
|
986
|
+
return transcript;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity });
|
|
990
|
+
|
|
991
|
+
for await (const line of rl) {
|
|
992
|
+
if (!line.trim()) {
|
|
993
|
+
continue;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
let entry: AnyRecord;
|
|
997
|
+
try {
|
|
998
|
+
entry = JSON.parse(line) as AnyRecord;
|
|
999
|
+
} catch {
|
|
1000
|
+
continue;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
const payload = readObjectRecord(entry.payload);
|
|
1004
|
+
if (!payload) {
|
|
1005
|
+
continue;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
if (entry.type === 'session_meta') {
|
|
1009
|
+
// A resumed rollout writes a second, sparser session_meta; assigning
|
|
1010
|
+
// unconditionally would erase the identity the first one carried.
|
|
1011
|
+
transcript.nickname = readNonEmptyString(payload.agent_nickname) ?? transcript.nickname;
|
|
1012
|
+
transcript.agentPath = readNonEmptyString(payload.agent_path) ?? transcript.agentPath;
|
|
1013
|
+
continue;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
if (entry.type === 'turn_context') {
|
|
1017
|
+
transcript.model = readNonEmptyString(payload.model) ?? transcript.model;
|
|
1018
|
+
continue;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
if (entry.type !== 'response_item') {
|
|
1022
|
+
continue;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
const timestamp = typeof entry.timestamp === 'string' ? entry.timestamp : undefined;
|
|
1026
|
+
|
|
1027
|
+
if (payload.type === 'message' && payload.role === 'assistant') {
|
|
1028
|
+
const content = extractCodexTextContent(payload.content).trim();
|
|
1029
|
+
if (content) {
|
|
1030
|
+
activity.push({ kind: 'text', content, timestamp });
|
|
1031
|
+
}
|
|
1032
|
+
continue;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
if (payload.type === 'reasoning') {
|
|
1036
|
+
const summary = Array.isArray(payload.summary)
|
|
1037
|
+
? payload.summary.map((item: AnyRecord) => item?.text).filter(Boolean).join('\n')
|
|
1038
|
+
: '';
|
|
1039
|
+
if (summary.trim()) {
|
|
1040
|
+
activity.push({ kind: 'thinking', content: summary, timestamp });
|
|
1041
|
+
}
|
|
1042
|
+
continue;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
if (payload.type === 'function_call' || payload.type === 'custom_tool_call') {
|
|
1046
|
+
const callId = readNonEmptyString(payload.call_id) ?? generateMessageId('codex-subagent-call');
|
|
1047
|
+
for (const child of translateCodexToolCall(payload, callId)) {
|
|
1048
|
+
const record: SubagentActivity = { ...child, timestamp };
|
|
1049
|
+
activity.push(record);
|
|
1050
|
+
pendingResults.set(callId, record);
|
|
1051
|
+
}
|
|
1052
|
+
continue;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
if (payload.type === 'function_call_output' || payload.type === 'custom_tool_call_output') {
|
|
1056
|
+
const callId = readNonEmptyString(payload.call_id);
|
|
1057
|
+
const target = callId ? pendingResults.get(callId) : undefined;
|
|
1058
|
+
if (!target) {
|
|
1059
|
+
continue;
|
|
1060
|
+
}
|
|
1061
|
+
const cleaned = cleanCodexShellOutput(extractCodexToolOutput(payload.output));
|
|
1062
|
+
target.toolResult = {
|
|
1063
|
+
content: cleaned.output,
|
|
1064
|
+
isError: cleaned.exitCode !== undefined && cleaned.exitCode !== 0,
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
return transcript;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
/**
|
|
1073
|
+
* Translates one persisted Codex tool call into the canonical tool rows the
|
|
1074
|
+
* transcript renders. Most calls produce exactly one row; a patch produces one
|
|
1075
|
+
* per file it touched.
|
|
1076
|
+
*
|
|
1077
|
+
* Shared by the main thread and by subagent transcripts so a subagent's shell
|
|
1078
|
+
* command looks identical to the parent's.
|
|
1079
|
+
*/
|
|
1080
|
+
function translateCodexToolCall(payload: AnyRecord, callId: string): SubagentActivity[] {
|
|
1081
|
+
const name = readNonEmptyString(payload.name) ?? 'tool';
|
|
1082
|
+
const rawInput = payload.type === 'custom_tool_call' ? payload.input : payload.arguments;
|
|
1083
|
+
|
|
1084
|
+
if (name === 'exec') {
|
|
1085
|
+
const operations = parseCodexExecScript(rawInput);
|
|
1086
|
+
const rows: SubagentActivity[] = [];
|
|
1087
|
+
for (const operation of operations) {
|
|
1088
|
+
if (operation.kind === 'shell') {
|
|
1089
|
+
rows.push({
|
|
1090
|
+
kind: 'tool',
|
|
1091
|
+
toolId: callId,
|
|
1092
|
+
toolName: 'Bash',
|
|
1093
|
+
toolInput: { command: operation.command, description: operation.justification },
|
|
1094
|
+
});
|
|
1095
|
+
} else if (operation.kind === 'patch') {
|
|
1096
|
+
for (const change of parseCodexPatch(operation.patch)) {
|
|
1097
|
+
rows.push({
|
|
1098
|
+
kind: 'tool',
|
|
1099
|
+
toolId: callId,
|
|
1100
|
+
toolName: change.changeType === 'add' ? 'Write' : 'Edit',
|
|
1101
|
+
toolInput: {
|
|
1102
|
+
file_path: change.filePath,
|
|
1103
|
+
old_string: change.oldText,
|
|
1104
|
+
new_string: change.newText,
|
|
1105
|
+
},
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
} else if (operation.kind === 'search') {
|
|
1109
|
+
rows.push({
|
|
1110
|
+
kind: 'tool',
|
|
1111
|
+
toolId: callId,
|
|
1112
|
+
toolName: 'WebSearch',
|
|
1113
|
+
toolInput: { query: operation.queries.join(' | ') },
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
if (rows.length > 0) {
|
|
1118
|
+
return rows;
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
if (name === 'shell_command' || name === 'exec_command') {
|
|
1123
|
+
let command = '';
|
|
1124
|
+
try {
|
|
1125
|
+
const args = JSON.parse(String(rawInput || '{}')) as AnyRecord;
|
|
1126
|
+
command = String(args.command ?? args.cmd ?? '');
|
|
1127
|
+
} catch {
|
|
1128
|
+
command = String(rawInput ?? '');
|
|
1129
|
+
}
|
|
1130
|
+
return [{ kind: 'tool', toolId: callId, toolName: 'Bash', toolInput: { command } }];
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
if (name === 'apply_patch') {
|
|
1134
|
+
return parseCodexPatch(String(rawInput ?? '')).map((change) => ({
|
|
1135
|
+
kind: 'tool' as const,
|
|
1136
|
+
toolId: callId,
|
|
1137
|
+
toolName: change.changeType === 'add' ? 'Write' : 'Edit',
|
|
1138
|
+
toolInput: {
|
|
1139
|
+
file_path: change.filePath,
|
|
1140
|
+
old_string: change.oldText,
|
|
1141
|
+
new_string: change.newText,
|
|
1142
|
+
},
|
|
1143
|
+
}));
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
if (name === 'update_plan') {
|
|
1147
|
+
return [{ kind: 'tool', toolId: callId, toolName: 'TodoWrite', toolInput: readCodexPlanInput(rawInput) }];
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
return [{ kind: 'tool', toolId: callId, toolName: humanizeCodexToolName(name), toolInput: rawInput }];
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
/**
|
|
1154
|
+
* Converts Codex's `update_plan` steps into the todo shape Claude's
|
|
1155
|
+
* `TodoWrite` already uses, so one renderer covers both providers.
|
|
1156
|
+
*/
|
|
1157
|
+
function readCodexPlanInput(rawArguments: unknown): AnyRecord {
|
|
1158
|
+
try {
|
|
1159
|
+
const parsed = JSON.parse(String(rawArguments || '{}')) as AnyRecord;
|
|
1160
|
+
const steps = Array.isArray(parsed.plan) ? parsed.plan : [];
|
|
1161
|
+
return {
|
|
1162
|
+
todos: steps.map((step: AnyRecord) => ({
|
|
1163
|
+
content: typeof step?.step === 'string' ? step.step : String(step?.content ?? ''),
|
|
1164
|
+
status: typeof step?.status === 'string' ? step.status : 'pending',
|
|
1165
|
+
})),
|
|
1166
|
+
};
|
|
1167
|
+
} catch {
|
|
1168
|
+
return { todos: [] };
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
function humanizeCodexToolName(toolName: string): string {
|
|
1173
|
+
return toolName
|
|
1174
|
+
.replace(/__/g, ' ')
|
|
1175
|
+
.replace(/_/g, ' ')
|
|
1176
|
+
.replace(/\b\w/g, (character) => character.toUpperCase());
|
|
220
1177
|
}
|
|
221
1178
|
|
|
222
1179
|
const CODEX_COLLABORATION_CONTROL_TOOLS = new Set([
|
|
@@ -227,412 +1184,781 @@ const CODEX_COLLABORATION_CONTROL_TOOLS = new Set([
|
|
|
227
1184
|
'wait_agent',
|
|
228
1185
|
]);
|
|
229
1186
|
|
|
230
|
-
|
|
231
|
-
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
232
|
-
}
|
|
1187
|
+
// ─── Transcript reader ──────────────────────────────────────────────────────
|
|
233
1188
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
): Promise<CodexHistoryResult> {
|
|
239
|
-
|
|
240
|
-
const sessionFilePath = sessionsDb.getSessionById(sessionId)?.jsonl_path;
|
|
241
|
-
|
|
242
|
-
if (!sessionFilePath) {
|
|
243
|
-
console.warn(`Codex session file not found for session ${sessionId}`);
|
|
244
|
-
return { messages: [], total: 0, hasMore: false };
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
const messages: AnyRecord[] = [];
|
|
248
|
-
let tokenUsage: AnyRecord | null = null;
|
|
249
|
-
const ignoredToolCallIds = new Set<string>();
|
|
250
|
-
const execToolCallIds = new Set<string>();
|
|
251
|
-
const execCallByCellId = new Map<string, string>();
|
|
252
|
-
const waitCallToExecCall = new Map<string, string>();
|
|
253
|
-
const pendingExecOutput = new Map<string, string>();
|
|
254
|
-
const completedExecCalls = new Set<string>();
|
|
255
|
-
const subagentsByCallId = new Map<string, CodexSubagentRecord>();
|
|
256
|
-
const subagentsByPath = new Map<string, CodexSubagentRecord>();
|
|
257
|
-
const fileStream = fsSync.createReadStream(sessionFilePath);
|
|
258
|
-
const rl = readline.createInterface({
|
|
259
|
-
input: fileStream,
|
|
260
|
-
crlfDelay: Infinity,
|
|
261
|
-
});
|
|
1189
|
+
/**
|
|
1190
|
+
* Reads one Codex rollout file and produces the compact per-message records
|
|
1191
|
+
* `normalizeHistoryEntry` turns into `NormalizedMessage`s.
|
|
1192
|
+
*/
|
|
1193
|
+
async function getCodexSessionMessages(sessionId: string): Promise<CodexHistoryResult> {
|
|
1194
|
+
const sessionFilePath = sessionsDb.getSessionById(sessionId)?.jsonl_path;
|
|
262
1195
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
1196
|
+
if (!sessionFilePath) {
|
|
1197
|
+
console.warn(`Codex session file not found for session ${sessionId}`);
|
|
1198
|
+
return { messages: [], total: 0, hasMore: false };
|
|
1199
|
+
}
|
|
267
1200
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
1201
|
+
const messages: AnyRecord[] = [];
|
|
1202
|
+
let tokenUsage: AnyRecord | null = null;
|
|
1203
|
+
|
|
1204
|
+
/** Call ids whose orchestration wrapper must never reach the transcript. */
|
|
1205
|
+
const ignoredToolCallIds = new Set<string>();
|
|
1206
|
+
/** Exec calls that own a shell row, so their output can be routed back. */
|
|
1207
|
+
const shellCallMessages = new Map<string, AnyRecord>();
|
|
1208
|
+
/**
|
|
1209
|
+
* File rows produced by each patch call, so the authoritative
|
|
1210
|
+
* `patch_apply_end` diffs can replace what was reconstructed from the call
|
|
1211
|
+
* input, and so every row gets exactly one result.
|
|
1212
|
+
*/
|
|
1213
|
+
const patchGroups = new Map<string, {
|
|
1214
|
+
callId: string;
|
|
1215
|
+
rows: AnyRecord[];
|
|
1216
|
+
timestamp: string;
|
|
1217
|
+
success?: boolean;
|
|
1218
|
+
failure?: string;
|
|
1219
|
+
}>();
|
|
1220
|
+
/** Patch call awaiting its out-of-band `patch_apply_end` report. */
|
|
1221
|
+
let pendingPatchCallId: string | null = null;
|
|
1222
|
+
const execCallByCellId = new Map<string, string>();
|
|
1223
|
+
const waitCallToExecCall = new Map<string, string>();
|
|
1224
|
+
const pendingExecOutput = new Map<string, string>();
|
|
1225
|
+
const completedExecCalls = new Set<string>();
|
|
1226
|
+
/** Newest still-running exec call, for `write_stdin` polls that carry no cell id. */
|
|
1227
|
+
let runningExecCallId: string | null = null;
|
|
1228
|
+
|
|
1229
|
+
const subagentsByCallId = new Map<string, CodexSubagentRecord>();
|
|
1230
|
+
const subagentsByPath = new Map<string, CodexSubagentRecord>();
|
|
1231
|
+
const turns = createCodexTurnTracker();
|
|
1232
|
+
/** Turns whose prompt already carries the anchor, so only the first does. */
|
|
1233
|
+
const anchoredTurnIds = new Set<string>();
|
|
1234
|
+
|
|
1235
|
+
const fileStream = fsSync.createReadStream(sessionFilePath);
|
|
1236
|
+
const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity });
|
|
1237
|
+
|
|
1238
|
+
/** Emits a tool_result row unless the call already produced one. */
|
|
1239
|
+
const pushToolResult = (callId: string, timestamp: string, output: string, isError: boolean) => {
|
|
1240
|
+
if (completedExecCalls.has(callId)) {
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1243
|
+
completedExecCalls.add(callId);
|
|
1244
|
+
messages.push({ type: 'tool_result', timestamp, toolCallId: callId, output, isError });
|
|
1245
|
+
};
|
|
1246
|
+
|
|
1247
|
+
for await (const line of rl) {
|
|
1248
|
+
if (!line.trim()) {
|
|
1249
|
+
continue;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
let entry: AnyRecord;
|
|
1253
|
+
try {
|
|
1254
|
+
entry = JSON.parse(line) as AnyRecord;
|
|
1255
|
+
} catch {
|
|
1256
|
+
continue;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
const payload = readObjectRecord(entry.payload);
|
|
1260
|
+
if (!payload) {
|
|
1261
|
+
continue;
|
|
1262
|
+
}
|
|
1263
|
+
const timestamp = typeof entry.timestamp === 'string' ? entry.timestamp : new Date().toISOString();
|
|
1264
|
+
// Before the type-specific branches: a turn is opened and closed by rows
|
|
1265
|
+
// that produce no transcript entry of their own, and each of those
|
|
1266
|
+
// branches ends in a `continue`.
|
|
1267
|
+
turns.observe(entry.type, payload);
|
|
1268
|
+
|
|
1269
|
+
// ── event_msg ──────────────────────────────────────────────────────────
|
|
1270
|
+
if (entry.type === 'event_msg') {
|
|
1271
|
+
if (payload.type === 'token_count' && payload.info) {
|
|
1272
|
+
const info = payload.info as AnyRecord;
|
|
1273
|
+
if (info.total_token_usage) {
|
|
1274
|
+
const usage = info.total_token_usage as AnyRecord;
|
|
1275
|
+
tokenUsage = { used: usage.total_tokens || 0, total: info.model_context_window || 200000 };
|
|
279
1276
|
}
|
|
1277
|
+
continue;
|
|
1278
|
+
}
|
|
280
1279
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
)
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
if (subagent && agentPath) {
|
|
1280
|
+
if (payload.type === 'sub_agent_activity' && payload.kind === 'started') {
|
|
1281
|
+
const eventId = readNonEmptyString(payload.event_id);
|
|
1282
|
+
const agentPath = readNonEmptyString(payload.agent_path);
|
|
1283
|
+
const agentThreadId = readNonEmptyString(payload.agent_thread_id);
|
|
1284
|
+
const subagent = eventId ? subagentsByCallId.get(eventId) : undefined;
|
|
1285
|
+
if (subagent) {
|
|
1286
|
+
subagent.agentThreadId = agentThreadId ?? subagent.agentThreadId;
|
|
1287
|
+
if (agentPath) {
|
|
290
1288
|
subagent.agentPath = agentPath;
|
|
291
1289
|
subagentsByPath.set(agentPath, subagent);
|
|
292
1290
|
}
|
|
293
1291
|
}
|
|
1292
|
+
continue;
|
|
1293
|
+
}
|
|
294
1294
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
1295
|
+
// Codex reports patch results out of band. They carry the real unified
|
|
1296
|
+
// diffs, so they replace whatever was reconstructed from the call input.
|
|
1297
|
+
// Codex reports the applied patch out of band, and it is the only place
|
|
1298
|
+
// the real unified diffs appear. It always arrives before the call's own
|
|
1299
|
+
// output, so the rows reconstructed from the call input are rewritten
|
|
1300
|
+
// here rather than duplicated.
|
|
1301
|
+
if (payload.type === 'patch_apply_end') {
|
|
1302
|
+
const callId = readNonEmptyString(payload.call_id);
|
|
1303
|
+
if (!callId) {
|
|
1304
|
+
continue;
|
|
1305
|
+
}
|
|
1306
|
+
const changes = readPatchApplyChanges(payload.changes);
|
|
1307
|
+
// A patch applied from inside an `exec` script is reported under a
|
|
1308
|
+
// synthetic id that does not match the script's call id, so fall back
|
|
1309
|
+
// to the patch call that is still in flight.
|
|
1310
|
+
const group = patchGroups.get(callId)
|
|
1311
|
+
?? (pendingPatchCallId ? patchGroups.get(pendingPatchCallId) : undefined);
|
|
1312
|
+
if (group) {
|
|
1313
|
+
rewritePatchRows(group.rows, changes, group.callId, messages);
|
|
1314
|
+
group.success = payload.success !== false;
|
|
1315
|
+
group.failure = String(payload.stderr || payload.stdout || '');
|
|
1316
|
+
pendingPatchCallId = null;
|
|
1317
|
+
} else if (changes.length > 0) {
|
|
1318
|
+
// A resumed thread can replay the result of a patch whose call is
|
|
1319
|
+
// not in this file; the rows still belong in the transcript.
|
|
1320
|
+
const rows = changes.map((change, index) =>
|
|
1321
|
+
fileChangeToToolMessage(change, timestamp, `${callId}#${index}`));
|
|
1322
|
+
rows.forEach((row) => messages.push(row));
|
|
1323
|
+
patchGroups.set(callId, {
|
|
1324
|
+
callId,
|
|
1325
|
+
rows,
|
|
1326
|
+
timestamp,
|
|
1327
|
+
success: payload.success !== false,
|
|
1328
|
+
failure: String(payload.stderr || payload.stdout || ''),
|
|
304
1329
|
});
|
|
305
1330
|
}
|
|
1331
|
+
continue;
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
if (payload.type === 'turn_aborted') {
|
|
1335
|
+
messages.push({
|
|
1336
|
+
type: 'status_note',
|
|
1337
|
+
timestamp,
|
|
1338
|
+
content: payload.reason === 'interrupted' ? 'Turn interrupted' : `Turn aborted (${String(payload.reason ?? 'unknown')})`,
|
|
1339
|
+
});
|
|
1340
|
+
continue;
|
|
1341
|
+
}
|
|
306
1342
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
1343
|
+
if (payload.type === 'context_compacted') {
|
|
1344
|
+
messages.push({ type: 'status_note', timestamp, content: 'Context compacted' });
|
|
1345
|
+
continue;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
// Paginated rollouts carry user prompts as `item_completed` /
|
|
1349
|
+
// `UserMessage`; wire `response_item` user rows are internal, not prompts.
|
|
1350
|
+
if (payload.type === 'item_completed') {
|
|
1351
|
+
const item = readObjectRecord(payload.item);
|
|
1352
|
+
if (item?.type === 'UserMessage') {
|
|
1353
|
+
const content = extractCodexTextContent(item.content);
|
|
1354
|
+
const images = extractCodexTypedUserImages(item.content);
|
|
1355
|
+
if (content.trim() || images?.length) {
|
|
1356
|
+
// Only the first prompt of a turn is anchored, matching the legacy
|
|
1357
|
+
// `user_message` branch below: a turn can hold several prompts and
|
|
1358
|
+
// the edit cut is per turn.
|
|
1359
|
+
const turnId = readNonEmptyString(payload.turn_id)
|
|
1360
|
+
?? turns.getCurrentTurnId();
|
|
1361
|
+
const isFirstPromptOfTurn = Boolean(turnId) && !anchoredTurnIds.has(turnId as string);
|
|
1362
|
+
if (isFirstPromptOfTurn) {
|
|
1363
|
+
anchoredTurnIds.add(turnId as string);
|
|
1364
|
+
}
|
|
1365
|
+
const row: AnyRecord = {
|
|
1366
|
+
type: 'user',
|
|
1367
|
+
timestamp,
|
|
1368
|
+
message: { role: 'user', content },
|
|
1369
|
+
...(images ? { images } : {}),
|
|
1370
|
+
...(isFirstPromptOfTurn ? { turnId } : {}),
|
|
1371
|
+
};
|
|
1372
|
+
messages.push(row);
|
|
322
1373
|
}
|
|
323
1374
|
}
|
|
1375
|
+
continue;
|
|
1376
|
+
}
|
|
324
1377
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
messages.push({
|
|
335
|
-
type: 'thinking',
|
|
336
|
-
timestamp: entry.timestamp,
|
|
337
|
-
message: {
|
|
338
|
-
role: 'assistant',
|
|
339
|
-
content: summaryText,
|
|
340
|
-
},
|
|
341
|
-
});
|
|
342
|
-
}
|
|
1378
|
+
if (isVisibleCodexUserMessage(payload)) {
|
|
1379
|
+
// Only the first prompt of a turn is anchored. A turn can hold more
|
|
1380
|
+
// than one — a follow-up queued while the turn was running is written
|
|
1381
|
+
// into it — and the cut is per turn, so anchoring the second would
|
|
1382
|
+
// quietly take the first with it when the user edited only the second.
|
|
1383
|
+
const turnId = turns.getCurrentTurnId();
|
|
1384
|
+
const isFirstPromptOfTurn = Boolean(turnId) && !anchoredTurnIds.has(turnId as string);
|
|
1385
|
+
if (isFirstPromptOfTurn) {
|
|
1386
|
+
anchoredTurnIds.add(turnId as string);
|
|
343
1387
|
}
|
|
1388
|
+
messages.push({
|
|
1389
|
+
type: 'user',
|
|
1390
|
+
timestamp,
|
|
1391
|
+
message: { role: 'user', content: payload.message },
|
|
1392
|
+
images: extractCodexUserImages(payload),
|
|
1393
|
+
...(isFirstPromptOfTurn ? { turnId } : {}),
|
|
1394
|
+
});
|
|
1395
|
+
}
|
|
1396
|
+
continue;
|
|
1397
|
+
}
|
|
344
1398
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
let subagent = subagentsByPath.get(agentMessage.author);
|
|
349
|
-
if (!subagent) {
|
|
350
|
-
const fallbackCallId = entry.payload.id || generateMessageId('codex-subagent');
|
|
351
|
-
const taskName = agentMessage.author.split('/').filter(Boolean).pop() || 'agent';
|
|
352
|
-
const taskMessage: AnyRecord = {
|
|
353
|
-
uuid: fallbackCallId,
|
|
354
|
-
type: 'tool_use',
|
|
355
|
-
timestamp: entry.timestamp,
|
|
356
|
-
toolName: 'Task',
|
|
357
|
-
toolInput: JSON.stringify({
|
|
358
|
-
subagent_type: 'Codex',
|
|
359
|
-
description: humanizeCodexToolName(taskName),
|
|
360
|
-
}),
|
|
361
|
-
toolCallId: fallbackCallId,
|
|
362
|
-
};
|
|
363
|
-
messages.push(taskMessage);
|
|
364
|
-
subagent = {
|
|
365
|
-
toolCallId: fallbackCallId,
|
|
366
|
-
message: taskMessage,
|
|
367
|
-
agentPath: agentMessage.author,
|
|
368
|
-
isComplete: false,
|
|
369
|
-
};
|
|
370
|
-
subagentsByCallId.set(fallbackCallId, subagent);
|
|
371
|
-
subagentsByPath.set(agentMessage.author, subagent);
|
|
372
|
-
}
|
|
1399
|
+
if (entry.type !== 'response_item') {
|
|
1400
|
+
continue;
|
|
1401
|
+
}
|
|
373
1402
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
subagent.isComplete = true;
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
}
|
|
1403
|
+
// ── response_item ──────────────────────────────────────────────────────
|
|
1404
|
+
if (payload.type === 'message' && payload.role === 'assistant') {
|
|
1405
|
+
const cited = readCodexMemoryCitations(extractCodexTextContent(payload.content));
|
|
1406
|
+
const textContent = cited.text;
|
|
1407
|
+
if (!textContent.trim()) {
|
|
1408
|
+
continue;
|
|
1409
|
+
}
|
|
385
1410
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
1411
|
+
const proposedPlan = readCodexProposedPlan(textContent);
|
|
1412
|
+
if (proposedPlan) {
|
|
1413
|
+
const planCallId = readNonEmptyString(payload.id) ?? generateMessageId('codex-plan');
|
|
1414
|
+
messages.push({
|
|
1415
|
+
type: 'tool_use',
|
|
1416
|
+
timestamp,
|
|
1417
|
+
toolName: 'ExitPlanMode',
|
|
1418
|
+
toolInput: JSON.stringify({ plan: proposedPlan }),
|
|
1419
|
+
toolCallId: planCallId,
|
|
1420
|
+
memoryCitations: cited.memoryCitations,
|
|
1421
|
+
});
|
|
1422
|
+
messages.push({ type: 'tool_result', timestamp, toolCallId: planCallId, output: '' });
|
|
1423
|
+
continue;
|
|
1424
|
+
}
|
|
389
1425
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
1426
|
+
messages.push({
|
|
1427
|
+
type: 'assistant',
|
|
1428
|
+
timestamp,
|
|
1429
|
+
message: { role: 'assistant', content: textContent },
|
|
1430
|
+
memoryCitations: cited.memoryCitations,
|
|
1431
|
+
});
|
|
1432
|
+
continue;
|
|
1433
|
+
}
|
|
398
1434
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
toolCallId: entry.payload.call_id,
|
|
413
|
-
message: taskMessage,
|
|
414
|
-
isComplete: false,
|
|
415
|
-
});
|
|
416
|
-
ignoredToolCallIds.add(entry.payload.call_id);
|
|
417
|
-
continue;
|
|
418
|
-
}
|
|
1435
|
+
if (payload.type === 'reasoning') {
|
|
1436
|
+
const summaryText = Array.isArray(payload.summary)
|
|
1437
|
+
? payload.summary.map((item: AnyRecord) => item?.text).filter(Boolean).join('\n')
|
|
1438
|
+
: '';
|
|
1439
|
+
if (summaryText.trim()) {
|
|
1440
|
+
messages.push({
|
|
1441
|
+
type: 'thinking',
|
|
1442
|
+
timestamp,
|
|
1443
|
+
message: { role: 'assistant', content: summaryText },
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
continue;
|
|
1447
|
+
}
|
|
419
1448
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
1449
|
+
if (payload.type === 'web_search_call') {
|
|
1450
|
+
const action = readObjectRecord(payload.action);
|
|
1451
|
+
const query = readNonEmptyString(action?.query as string | undefined);
|
|
1452
|
+
if (query) {
|
|
1453
|
+
const callId = readNonEmptyString(payload.id) ?? generateMessageId('codex-search');
|
|
1454
|
+
messages.push({
|
|
1455
|
+
type: 'tool_use',
|
|
1456
|
+
timestamp,
|
|
1457
|
+
toolName: 'WebSearch',
|
|
1458
|
+
toolInput: JSON.stringify({ query }),
|
|
1459
|
+
toolCallId: callId,
|
|
1460
|
+
});
|
|
1461
|
+
}
|
|
1462
|
+
continue;
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
if (payload.type === 'agent_message') {
|
|
1466
|
+
const agentMessage = parseCodexSubagentMessage(payload);
|
|
1467
|
+
if (!agentMessage || agentMessage.messageType !== 'FINAL_ANSWER' || !agentMessage.result) {
|
|
1468
|
+
continue;
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
let subagent = subagentsByPath.get(agentMessage.author);
|
|
1472
|
+
if (!subagent) {
|
|
1473
|
+
// A resumed thread can replay an answer whose spawn call is not in this
|
|
1474
|
+
// file. Synthesize the container so the answer still has a home.
|
|
1475
|
+
const fallbackCallId = readNonEmptyString(payload.id) ?? generateMessageId('codex-subagent');
|
|
1476
|
+
const taskName = agentMessage.author.split('/').filter(Boolean).pop() || 'agent';
|
|
1477
|
+
const taskMessage: AnyRecord = {
|
|
1478
|
+
uuid: fallbackCallId,
|
|
1479
|
+
type: 'tool_use',
|
|
1480
|
+
timestamp,
|
|
1481
|
+
toolName: 'Task',
|
|
1482
|
+
toolInput: JSON.stringify({ description: humanizeCodexToolName(taskName) }),
|
|
1483
|
+
toolCallId: fallbackCallId,
|
|
1484
|
+
};
|
|
1485
|
+
messages.push(taskMessage);
|
|
1486
|
+
subagent = { toolCallId: fallbackCallId, message: taskMessage, agentPath: agentMessage.author, isComplete: false };
|
|
1487
|
+
subagentsByCallId.set(fallbackCallId, subagent);
|
|
1488
|
+
subagentsByPath.set(agentMessage.author, subagent);
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
if (!subagent.isComplete) {
|
|
1492
|
+
messages.push({
|
|
1493
|
+
type: 'tool_result',
|
|
1494
|
+
timestamp,
|
|
1495
|
+
toolCallId: subagent.toolCallId,
|
|
1496
|
+
output: agentMessage.result,
|
|
1497
|
+
});
|
|
1498
|
+
subagent.isComplete = true;
|
|
1499
|
+
}
|
|
1500
|
+
continue;
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
if (payload.type === 'function_call' || payload.type === 'custom_tool_call') {
|
|
1504
|
+
const callId = readNonEmptyString(payload.call_id) ?? generateMessageId('codex-call');
|
|
1505
|
+
const name = readNonEmptyString(payload.name) ?? 'tool';
|
|
1506
|
+
const rawInput = payload.type === 'custom_tool_call' ? payload.input : payload.arguments;
|
|
1507
|
+
|
|
1508
|
+
if (name === 'spawn_agent') {
|
|
1509
|
+
let taskName = 'agent';
|
|
1510
|
+
let prompt: string | undefined;
|
|
1511
|
+
try {
|
|
1512
|
+
const args = JSON.parse(String(rawInput || '{}')) as AnyRecord;
|
|
1513
|
+
taskName = readNonEmptyString(args.task_name) || taskName;
|
|
1514
|
+
// `message` is an encrypted transport blob on collaboration spawns —
|
|
1515
|
+
// only surface it when it is readable prompt text.
|
|
1516
|
+
const message = readNonEmptyString(args.message);
|
|
1517
|
+
prompt = message && !/^gAAAAA/.test(message) ? message : undefined;
|
|
1518
|
+
} catch {
|
|
1519
|
+
// The activity event still supplies the canonical agent path.
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
const taskMessage: AnyRecord = {
|
|
1523
|
+
uuid: callId,
|
|
1524
|
+
type: 'tool_use',
|
|
1525
|
+
timestamp,
|
|
1526
|
+
toolName: 'Task',
|
|
1527
|
+
// No `subagent_type`: Codex has no agent presets, and naming the
|
|
1528
|
+
// provider there would render as "Codex / Codex" in the transcript.
|
|
1529
|
+
toolInput: JSON.stringify({
|
|
1530
|
+
description: humanizeCodexToolName(taskName),
|
|
1531
|
+
...(prompt ? { prompt } : {}),
|
|
1532
|
+
}),
|
|
1533
|
+
toolCallId: callId,
|
|
1534
|
+
};
|
|
1535
|
+
messages.push(taskMessage);
|
|
1536
|
+
subagentsByCallId.set(callId, { toolCallId: callId, message: taskMessage, isComplete: false });
|
|
1537
|
+
ignoredToolCallIds.add(callId);
|
|
1538
|
+
continue;
|
|
1539
|
+
}
|
|
434
1540
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
1541
|
+
if (name === 'wait') {
|
|
1542
|
+
try {
|
|
1543
|
+
const args = JSON.parse(String(rawInput || '{}')) as AnyRecord;
|
|
1544
|
+
const execCallId = execCallByCellId.get(String(args.cell_id || ''));
|
|
1545
|
+
if (execCallId) {
|
|
1546
|
+
waitCallToExecCall.set(callId, execCallId);
|
|
438
1547
|
}
|
|
1548
|
+
} catch {
|
|
1549
|
+
// Suppress the orchestration wait even when its payload is malformed.
|
|
1550
|
+
}
|
|
1551
|
+
ignoredToolCallIds.add(callId);
|
|
1552
|
+
continue;
|
|
1553
|
+
}
|
|
439
1554
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
1555
|
+
if (CODEX_COLLABORATION_CONTROL_TOOLS.has(name)) {
|
|
1556
|
+
ignoredToolCallIds.add(callId);
|
|
1557
|
+
continue;
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
if (name === 'update_plan') {
|
|
1561
|
+
messages.push({
|
|
1562
|
+
type: 'tool_use',
|
|
1563
|
+
timestamp,
|
|
1564
|
+
toolName: 'TodoWrite',
|
|
1565
|
+
toolInput: JSON.stringify(readCodexPlanInput(rawInput)),
|
|
1566
|
+
toolCallId: callId,
|
|
1567
|
+
});
|
|
1568
|
+
continue;
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
if (name === 'exec') {
|
|
1572
|
+
const operations = parseCodexExecScript(rawInput);
|
|
1573
|
+
const shellCommands = operations.filter((operation) => operation.kind === 'shell');
|
|
1574
|
+
const stdinWrites = operations.filter((operation) => operation.kind === 'stdin');
|
|
1575
|
+
const patches = operations.filter((operation) => operation.kind === 'patch');
|
|
1576
|
+
const searches = operations.filter((operation) => operation.kind === 'search');
|
|
1577
|
+
const plans = operations.filter((operation) => operation.kind === 'plan');
|
|
1578
|
+
|
|
1579
|
+
// A bare stdin poll continues whatever command is still running; it is
|
|
1580
|
+
// a continuation, not an action of its own.
|
|
1581
|
+
if (operations.length > 0 && shellCommands.length === 0 && stdinWrites.length > 0) {
|
|
1582
|
+
ignoredToolCallIds.add(callId);
|
|
1583
|
+
if (runningExecCallId) {
|
|
1584
|
+
waitCallToExecCall.set(callId, runningExecCallId);
|
|
448
1585
|
}
|
|
1586
|
+
continue;
|
|
1587
|
+
}
|
|
449
1588
|
|
|
450
|
-
|
|
1589
|
+
if (operations.length === 0) {
|
|
1590
|
+
// An unrecognized wrapper is shown as-is rather than mislabelled as a
|
|
1591
|
+
// shell command, so a new Codex tool degrades to a readable code
|
|
1592
|
+
// block instead of a wrong one.
|
|
1593
|
+
const fallbackMessage: AnyRecord = {
|
|
451
1594
|
type: 'tool_use',
|
|
452
|
-
timestamp
|
|
453
|
-
toolName,
|
|
454
|
-
toolInput,
|
|
455
|
-
toolCallId:
|
|
456
|
-
}
|
|
1595
|
+
timestamp,
|
|
1596
|
+
toolName: 'exec',
|
|
1597
|
+
toolInput: rawInput,
|
|
1598
|
+
toolCallId: callId,
|
|
1599
|
+
};
|
|
1600
|
+
messages.push(fallbackMessage);
|
|
1601
|
+
continue;
|
|
457
1602
|
}
|
|
458
1603
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
if (!runningOutput && !completedExecCalls.has(waitExecCallId)) {
|
|
468
|
-
messages.push({
|
|
469
|
-
type: 'tool_result',
|
|
470
|
-
timestamp: entry.timestamp,
|
|
471
|
-
toolCallId: waitExecCallId,
|
|
472
|
-
output: accumulatedOutput,
|
|
473
|
-
});
|
|
474
|
-
completedExecCalls.add(waitExecCallId);
|
|
475
|
-
}
|
|
476
|
-
continue;
|
|
1604
|
+
// The call has exactly one output, so whichever row represents the work
|
|
1605
|
+
// it did keeps the call id and receives that output. Extra rows get
|
|
1606
|
+
// suffixed ids and are resolved by their own bookkeeping.
|
|
1607
|
+
let outputOwnerAssigned = false;
|
|
1608
|
+
const nextRowId = () => {
|
|
1609
|
+
if (!outputOwnerAssigned) {
|
|
1610
|
+
outputOwnerAssigned = true;
|
|
1611
|
+
return callId;
|
|
477
1612
|
}
|
|
1613
|
+
return `${callId}~${messages.length}`;
|
|
1614
|
+
};
|
|
478
1615
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
1616
|
+
if (shellCommands.length > 0) {
|
|
1617
|
+
const shellMessage: AnyRecord = {
|
|
1618
|
+
type: 'tool_use',
|
|
1619
|
+
timestamp,
|
|
1620
|
+
toolName: 'Bash',
|
|
1621
|
+
toolInput: JSON.stringify({
|
|
1622
|
+
command: shellCommands.map((operation) => operation.command).join('\n'),
|
|
1623
|
+
description: shellCommands.find((operation) => operation.justification)?.justification,
|
|
1624
|
+
}),
|
|
1625
|
+
toolCallId: nextRowId(),
|
|
1626
|
+
};
|
|
1627
|
+
messages.push(shellMessage);
|
|
1628
|
+
shellCallMessages.set(callId, shellMessage);
|
|
1629
|
+
}
|
|
493
1630
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
1631
|
+
for (const search of searches) {
|
|
1632
|
+
messages.push({
|
|
1633
|
+
type: 'tool_use',
|
|
1634
|
+
timestamp,
|
|
1635
|
+
toolName: 'WebSearch',
|
|
1636
|
+
toolInput: JSON.stringify({ query: search.queries.join(' | ') }),
|
|
1637
|
+
toolCallId: nextRowId(),
|
|
1638
|
+
});
|
|
1639
|
+
}
|
|
497
1640
|
|
|
1641
|
+
for (const plan of plans) {
|
|
1642
|
+
// The script's own result covers whatever else it did, so the plan
|
|
1643
|
+
// row is closed here — without one it would read as still running.
|
|
1644
|
+
const planCallId = nextRowId();
|
|
498
1645
|
messages.push({
|
|
499
|
-
type: '
|
|
500
|
-
timestamp
|
|
501
|
-
|
|
502
|
-
|
|
1646
|
+
type: 'tool_use',
|
|
1647
|
+
timestamp,
|
|
1648
|
+
toolName: 'TodoWrite',
|
|
1649
|
+
toolInput: JSON.stringify({ todos: plan.todos }),
|
|
1650
|
+
toolCallId: planCallId,
|
|
503
1651
|
});
|
|
1652
|
+
messages.push({ type: 'tool_result', timestamp, toolCallId: planCallId, output: '' });
|
|
504
1653
|
}
|
|
505
1654
|
|
|
506
|
-
if (
|
|
507
|
-
|
|
508
|
-
const
|
|
509
|
-
|
|
1655
|
+
if (patches.length > 0) {
|
|
1656
|
+
const changes = patches.flatMap((patch) => parseCodexPatch(patch.patch));
|
|
1657
|
+
const rows = changes.map((change, index) =>
|
|
1658
|
+
fileChangeToToolMessage(change, timestamp, index === 0 ? nextRowId() : `${callId}#${index}`));
|
|
1659
|
+
rows.forEach((row) => messages.push(row));
|
|
1660
|
+
patchGroups.set(callId, { callId, rows, timestamp });
|
|
1661
|
+
pendingPatchCallId = callId;
|
|
1662
|
+
}
|
|
510
1663
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
type: 'tool_use',
|
|
519
|
-
timestamp: entry.timestamp,
|
|
520
|
-
toolName,
|
|
521
|
-
toolInput,
|
|
522
|
-
toolCallId: entry.payload.call_id,
|
|
523
|
-
});
|
|
524
|
-
execToolCallIds.add(entry.payload.call_id);
|
|
525
|
-
continue;
|
|
526
|
-
}
|
|
1664
|
+
if (!outputOwnerAssigned) {
|
|
1665
|
+
// Every recognized operation produced no row (an empty patch, say).
|
|
1666
|
+
// Nothing owns the output, so drop it rather than leaving it loose.
|
|
1667
|
+
ignoredToolCallIds.add(callId);
|
|
1668
|
+
}
|
|
1669
|
+
continue;
|
|
1670
|
+
}
|
|
527
1671
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
} else if (lineContent.startsWith('+') && !lineContent.startsWith('+++')) {
|
|
539
|
-
newLines.push(lineContent.slice(1));
|
|
540
|
-
}
|
|
541
|
-
}
|
|
1672
|
+
if (name === 'apply_patch') {
|
|
1673
|
+
const rows = parseCodexPatch(String(rawInput ?? ''))
|
|
1674
|
+
.map((change, index) => fileChangeToToolMessage(change, timestamp, `${callId}#${index}`));
|
|
1675
|
+
rows.forEach((row) => messages.push(row));
|
|
1676
|
+
patchGroups.set(callId, { callId, rows, timestamp });
|
|
1677
|
+
pendingPatchCallId = callId;
|
|
1678
|
+
// The call's own output is a bare `{}`; the per-file rows carry the result.
|
|
1679
|
+
ignoredToolCallIds.add(callId);
|
|
1680
|
+
continue;
|
|
1681
|
+
}
|
|
542
1682
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
new_string: newLines.join('\n'),
|
|
551
|
-
}),
|
|
552
|
-
toolCallId: entry.payload.call_id,
|
|
553
|
-
});
|
|
554
|
-
} else {
|
|
555
|
-
messages.push({
|
|
556
|
-
type: 'tool_use',
|
|
557
|
-
timestamp: entry.timestamp,
|
|
558
|
-
toolName,
|
|
559
|
-
toolInput: input,
|
|
560
|
-
toolCallId: entry.payload.call_id,
|
|
561
|
-
});
|
|
562
|
-
}
|
|
1683
|
+
if (name === 'shell_command' || name === 'exec_command') {
|
|
1684
|
+
let command = '';
|
|
1685
|
+
try {
|
|
1686
|
+
const args = JSON.parse(String(rawInput || '{}')) as AnyRecord;
|
|
1687
|
+
command = String(args.command ?? args.cmd ?? '');
|
|
1688
|
+
} catch {
|
|
1689
|
+
command = String(rawInput ?? '');
|
|
563
1690
|
}
|
|
1691
|
+
const shellMessage: AnyRecord = {
|
|
1692
|
+
type: 'tool_use',
|
|
1693
|
+
timestamp,
|
|
1694
|
+
toolName: 'Bash',
|
|
1695
|
+
toolInput: JSON.stringify({ command }),
|
|
1696
|
+
toolCallId: callId,
|
|
1697
|
+
};
|
|
1698
|
+
messages.push(shellMessage);
|
|
1699
|
+
shellCallMessages.set(callId, shellMessage);
|
|
1700
|
+
continue;
|
|
1701
|
+
}
|
|
564
1702
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
1703
|
+
messages.push({
|
|
1704
|
+
type: 'tool_use',
|
|
1705
|
+
timestamp,
|
|
1706
|
+
toolName: name,
|
|
1707
|
+
toolInput: rawInput,
|
|
1708
|
+
toolCallId: callId,
|
|
1709
|
+
});
|
|
1710
|
+
continue;
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
if (payload.type === 'function_call_output' || payload.type === 'custom_tool_call_output') {
|
|
1714
|
+
const callId = readNonEmptyString(payload.call_id) ?? '';
|
|
1715
|
+
const rawOutput = extractCodexToolOutput(payload.output);
|
|
1716
|
+
|
|
1717
|
+
// A wait / stdin poll delivers the tail of an earlier command's output.
|
|
1718
|
+
const continuedCallId = waitCallToExecCall.get(callId);
|
|
1719
|
+
if (continuedCallId) {
|
|
1720
|
+
const cleaned = readCodexStdinOutput(rawOutput) ?? cleanCodexShellOutput(rawOutput);
|
|
1721
|
+
const accumulated = `${pendingExecOutput.get(continuedCallId) || ''}${cleaned.output}`;
|
|
1722
|
+
pendingExecOutput.set(continuedCallId, accumulated);
|
|
1723
|
+
if (!cleaned.runningCellId) {
|
|
1724
|
+
if (runningExecCallId === continuedCallId) {
|
|
1725
|
+
runningExecCallId = null;
|
|
568
1726
|
}
|
|
1727
|
+
pushToolResult(
|
|
1728
|
+
continuedCallId,
|
|
1729
|
+
timestamp,
|
|
1730
|
+
accumulated,
|
|
1731
|
+
cleaned.exitCode !== undefined && cleaned.exitCode !== 0,
|
|
1732
|
+
);
|
|
1733
|
+
}
|
|
1734
|
+
continue;
|
|
1735
|
+
}
|
|
569
1736
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
}
|
|
578
|
-
completedExecCalls.add(entry.payload.call_id);
|
|
1737
|
+
const subagent = subagentsByCallId.get(callId);
|
|
1738
|
+
if (subagent) {
|
|
1739
|
+
try {
|
|
1740
|
+
const taskPath = readNonEmptyString((JSON.parse(rawOutput) as AnyRecord).task_name);
|
|
1741
|
+
if (taskPath) {
|
|
1742
|
+
subagent.agentPath = taskPath;
|
|
1743
|
+
subagentsByPath.set(taskPath, subagent);
|
|
579
1744
|
}
|
|
1745
|
+
} catch {
|
|
1746
|
+
// The sub_agent_activity event normally supplies the path.
|
|
1747
|
+
}
|
|
1748
|
+
continue;
|
|
1749
|
+
}
|
|
580
1750
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
1751
|
+
if (ignoredToolCallIds.has(callId)) {
|
|
1752
|
+
continue;
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
if (shellCallMessages.has(callId)) {
|
|
1756
|
+
const cleaned = cleanCodexShellOutput(rawOutput);
|
|
1757
|
+
if (cleaned.runningCellId) {
|
|
1758
|
+
execCallByCellId.set(cleaned.runningCellId, callId);
|
|
1759
|
+
pendingExecOutput.set(callId, cleaned.output);
|
|
1760
|
+
runningExecCallId = callId;
|
|
1761
|
+
continue;
|
|
587
1762
|
}
|
|
588
|
-
|
|
589
|
-
|
|
1763
|
+
const accumulated = `${pendingExecOutput.get(callId) || ''}${cleaned.output}`;
|
|
1764
|
+
pushToolResult(
|
|
1765
|
+
callId,
|
|
1766
|
+
timestamp,
|
|
1767
|
+
accumulated,
|
|
1768
|
+
cleaned.exitCode !== undefined && cleaned.exitCode !== 0,
|
|
1769
|
+
);
|
|
1770
|
+
continue;
|
|
590
1771
|
}
|
|
1772
|
+
|
|
1773
|
+
pushToolResult(callId, timestamp, rawOutput, false);
|
|
591
1774
|
}
|
|
1775
|
+
}
|
|
592
1776
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
total,
|
|
607
|
-
hasMore,
|
|
608
|
-
offset,
|
|
609
|
-
limit,
|
|
610
|
-
tokenUsage,
|
|
611
|
-
};
|
|
1777
|
+
// Every file row needs a result or the UI shows it as still running. The
|
|
1778
|
+
// patch outcome is known only after `patch_apply_end` (which may never
|
|
1779
|
+
// arrive), so results are emitted once the whole file has been read.
|
|
1780
|
+
for (const group of patchGroups.values()) {
|
|
1781
|
+
const failed = group.success === false;
|
|
1782
|
+
for (const row of group.rows) {
|
|
1783
|
+
messages.push({
|
|
1784
|
+
type: 'tool_result',
|
|
1785
|
+
timestamp: group.timestamp,
|
|
1786
|
+
toolCallId: row.toolCallId,
|
|
1787
|
+
output: failed ? (group.failure || 'Patch failed') : '',
|
|
1788
|
+
isError: failed,
|
|
1789
|
+
});
|
|
612
1790
|
}
|
|
1791
|
+
}
|
|
613
1792
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
1793
|
+
await attachCodexSubagentTranscripts(sessionFilePath, subagentsByCallId);
|
|
1794
|
+
|
|
1795
|
+
// A rollback is recorded after the turns it retires, so a prompt can be
|
|
1796
|
+
// anchored and then retired later in the same file. Its rows still render —
|
|
1797
|
+
// they are what the conversation looked like — but the turn is gone from the
|
|
1798
|
+
// thread, so the anchor goes with it and the message loses its pencil.
|
|
1799
|
+
for (const message of messages) {
|
|
1800
|
+
if (typeof message.turnId === 'string' && turns.isRolledBack(message.turnId)) {
|
|
1801
|
+
delete message.turnId;
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
messages.sort((a, b) => new Date(a.timestamp || 0).getTime() - new Date(b.timestamp || 0).getTime());
|
|
1806
|
+
return { messages, tokenUsage: tokenUsage ?? undefined };
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
/**
|
|
1810
|
+
* Loads each spawned agent's own rollout and hangs its timeline off the
|
|
1811
|
+
* `Task` row that started it.
|
|
1812
|
+
*/
|
|
1813
|
+
async function attachCodexSubagentTranscripts(
|
|
1814
|
+
parentFilePath: string,
|
|
1815
|
+
subagentsByCallId: Map<string, CodexSubagentRecord>,
|
|
1816
|
+
): Promise<void> {
|
|
1817
|
+
for (const record of subagentsByCallId.values()) {
|
|
1818
|
+
let parsedInput: AnyRecord = {};
|
|
1819
|
+
try {
|
|
1820
|
+
parsedInput = JSON.parse(String(record.message.toolInput || '{}')) as AnyRecord;
|
|
1821
|
+
} catch {
|
|
1822
|
+
parsedInput = {};
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
const agentName = record.agentPath?.split('/').filter(Boolean).pop();
|
|
1826
|
+
// Codex has no agent-type concept the way Claude does, so `type` is left
|
|
1827
|
+
// unset rather than filled with the provider's own name; the panel falls
|
|
1828
|
+
// back to a neutral label.
|
|
1829
|
+
const subagent: SubagentInfo = {
|
|
1830
|
+
id: record.agentThreadId ?? record.toolCallId,
|
|
1831
|
+
description: readNonEmptyString(parsedInput.description as string | undefined) ?? agentName,
|
|
1832
|
+
status: record.isComplete ? 'completed' : 'running',
|
|
1833
|
+
};
|
|
1834
|
+
|
|
1835
|
+
if (record.agentThreadId) {
|
|
1836
|
+
const rolloutPath = await findCodexSubagentRollout(parentFilePath, record.agentThreadId);
|
|
1837
|
+
if (rolloutPath) {
|
|
1838
|
+
const transcript = await readCodexSubagentTranscript(rolloutPath);
|
|
1839
|
+
if (transcript.activity.length > 0) {
|
|
1840
|
+
record.message.subagentTools = transcript.activity
|
|
1841
|
+
.slice(0, MAX_TRANSMITTED_SUBAGENT_ACTIVITIES)
|
|
1842
|
+
.map(truncateSubagentActivity);
|
|
1843
|
+
subagent.activityCount = transcript.activity.length;
|
|
1844
|
+
}
|
|
1845
|
+
subagent.name = transcript.nickname ?? agentName;
|
|
1846
|
+
subagent.model = transcript.model;
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
subagent.name = subagent.name ?? agentName;
|
|
1851
|
+
record.message.subagent = subagent;
|
|
618
1852
|
}
|
|
619
1853
|
}
|
|
620
1854
|
|
|
621
1855
|
export class CodexSessionsProvider implements IProviderSessions {
|
|
1856
|
+
/**
|
|
1857
|
+
* Resolves the last turn to keep when the turn `anchorId` names is replaced.
|
|
1858
|
+
*
|
|
1859
|
+
* `thread/fork`'s `lastTurnId` is inclusive of the turn it names, which is
|
|
1860
|
+
* the same thing this contract's `resumeThroughId` means, so the answer is
|
|
1861
|
+
* simply the turn before the edited one. Editing the first prompt leaves
|
|
1862
|
+
* nothing to keep, and `null` is how that is reported.
|
|
1863
|
+
*/
|
|
1864
|
+
async resolveEditAnchor(
|
|
1865
|
+
sessionId: string,
|
|
1866
|
+
anchorId: string,
|
|
1867
|
+
): Promise<{ found: boolean; resumeThroughId: string | null }> {
|
|
1868
|
+
const session = sessionsDb.getSessionById(sessionId);
|
|
1869
|
+
const jsonlPath = session?.jsonl_path;
|
|
1870
|
+
if (!jsonlPath || !session?.provider_session_id) {
|
|
1871
|
+
return { found: false, resumeThroughId: null };
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
const liveTurnIds = await readCodexLiveTurnIds(jsonlPath);
|
|
1875
|
+
const anchorIndex = liveTurnIds.indexOf(anchorId);
|
|
1876
|
+
if (anchorIndex < 0) {
|
|
1877
|
+
return { found: false, resumeThroughId: null };
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
return {
|
|
1881
|
+
found: true,
|
|
1882
|
+
resumeThroughId: anchorIndex === 0 ? null : liveTurnIds[anchorIndex - 1],
|
|
1883
|
+
};
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
/**
|
|
1887
|
+
* Branches the conversation at `keepThroughId` and moves the session onto
|
|
1888
|
+
* the branch.
|
|
1889
|
+
*
|
|
1890
|
+
* Codex threads are append-only — the SDK resumes one at its tip and nothing
|
|
1891
|
+
* shortens it — so rewinding means copying the part being kept into a new
|
|
1892
|
+
* thread. The pre-edit thread stays on disk in full and is recorded as
|
|
1893
|
+
* superseded so the session indexer does not hand it back later.
|
|
1894
|
+
*/
|
|
1895
|
+
async rewindSession(sessionId: string, keepThroughId: string | null): Promise<void> {
|
|
1896
|
+
const session = sessionsDb.getSessionById(sessionId);
|
|
1897
|
+
const supersededThreadId = session?.provider_session_id;
|
|
1898
|
+
if (!session || !supersededThreadId) {
|
|
1899
|
+
throw new AppError('This session has not produced a transcript yet.', {
|
|
1900
|
+
code: 'EDIT_SOURCE_NOT_READY',
|
|
1901
|
+
statusCode: 409,
|
|
1902
|
+
});
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
// Nothing of the conversation survives an edit to its first prompt, and
|
|
1906
|
+
// there is no such thing as a fork of no turns, so the session is simply
|
|
1907
|
+
// detached and the next run opens a new thread.
|
|
1908
|
+
if (keepThroughId === null) {
|
|
1909
|
+
sessionsDb.markProviderSessionSuperseded({
|
|
1910
|
+
providerSessionId: supersededThreadId,
|
|
1911
|
+
provider: PROVIDER,
|
|
1912
|
+
sessionId,
|
|
1913
|
+
jsonlPath: session.jsonl_path ?? null,
|
|
1914
|
+
});
|
|
1915
|
+
sessionsDb.detachProviderSession(sessionId);
|
|
1916
|
+
return;
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
const fork = await codexAppServer.forkThread({
|
|
1920
|
+
threadId: supersededThreadId,
|
|
1921
|
+
lastTurnId: keepThroughId,
|
|
1922
|
+
cwd: session.project_path ?? '',
|
|
1923
|
+
});
|
|
1924
|
+
|
|
1925
|
+
sessionsDb.markProviderSessionSuperseded({
|
|
1926
|
+
providerSessionId: supersededThreadId,
|
|
1927
|
+
provider: PROVIDER,
|
|
1928
|
+
sessionId,
|
|
1929
|
+
jsonlPath: session.jsonl_path ?? null,
|
|
1930
|
+
});
|
|
1931
|
+
sessionsDb.repointSessionToProviderSession(sessionId, {
|
|
1932
|
+
providerSessionId: fork.threadId,
|
|
1933
|
+
jsonlPath: fork.path,
|
|
1934
|
+
});
|
|
1935
|
+
}
|
|
1936
|
+
|
|
622
1937
|
/**
|
|
623
1938
|
* Normalizes a persisted Codex JSONL entry.
|
|
624
1939
|
*
|
|
625
1940
|
* Live Codex SDK events are transformed before they reach normalizeMessage(),
|
|
626
|
-
* while history entries already use
|
|
1941
|
+
* while history entries already use the compact message/tool shape produced
|
|
1942
|
+
* by getCodexSessionMessages().
|
|
627
1943
|
*/
|
|
628
1944
|
private normalizeHistoryEntry(raw: AnyRecord, sessionId: string | null): NormalizedMessage[] {
|
|
629
1945
|
const ts = raw.timestamp || new Date().toISOString();
|
|
630
1946
|
const baseId = raw.uuid || generateMessageId('codex');
|
|
631
1947
|
|
|
1948
|
+
if (raw.type === 'status_note' && typeof raw.content === 'string') {
|
|
1949
|
+
return [createNormalizedMessage({
|
|
1950
|
+
id: baseId,
|
|
1951
|
+
sessionId,
|
|
1952
|
+
timestamp: ts,
|
|
1953
|
+
provider: PROVIDER,
|
|
1954
|
+
kind: 'task_notification',
|
|
1955
|
+
summary: raw.content,
|
|
1956
|
+
status: 'info',
|
|
1957
|
+
})];
|
|
1958
|
+
}
|
|
1959
|
+
|
|
632
1960
|
if (raw.type === 'thinking' || raw.isReasoning) {
|
|
633
|
-
const thinkingContent = typeof raw.message?.content === 'string'
|
|
634
|
-
? raw.message.content
|
|
635
|
-
: '';
|
|
1961
|
+
const thinkingContent = typeof raw.message?.content === 'string' ? raw.message.content : '';
|
|
636
1962
|
if (!thinkingContent.trim()) {
|
|
637
1963
|
return [];
|
|
638
1964
|
}
|
|
@@ -651,9 +1977,9 @@ export class CodexSessionsProvider implements IProviderSessions {
|
|
|
651
1977
|
? raw.message.content
|
|
652
1978
|
: Array.isArray(raw.message.content)
|
|
653
1979
|
? raw.message.content
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
1980
|
+
.map((part: string | AnyRecord) => typeof part === 'string' ? part : part?.text || '')
|
|
1981
|
+
.filter(Boolean)
|
|
1982
|
+
.join('\n')
|
|
657
1983
|
: String(raw.message.content || '');
|
|
658
1984
|
const parsedFiles = parseFilesInputTag(content);
|
|
659
1985
|
const rawImages = Array.isArray(raw.images) && raw.images.length > 0 ? raw.images : undefined;
|
|
@@ -671,6 +1997,10 @@ export class CodexSessionsProvider implements IProviderSessions {
|
|
|
671
1997
|
content: parsedFiles.text,
|
|
672
1998
|
images: rawImages,
|
|
673
1999
|
files,
|
|
2000
|
+
// The enclosing turn, when the reader could name one. `baseId` is not
|
|
2001
|
+
// usable for this: Codex rows carry no id, so it is synthesized per
|
|
2002
|
+
// read and would address a different message every time.
|
|
2003
|
+
transcriptAnchorId: readNonEmptyString(raw.turnId),
|
|
674
2004
|
})];
|
|
675
2005
|
}
|
|
676
2006
|
|
|
@@ -679,9 +2009,9 @@ export class CodexSessionsProvider implements IProviderSessions {
|
|
|
679
2009
|
? raw.message.content
|
|
680
2010
|
: Array.isArray(raw.message.content)
|
|
681
2011
|
? raw.message.content
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
2012
|
+
.map((part: string | AnyRecord) => typeof part === 'string' ? part : part?.text || '')
|
|
2013
|
+
.filter(Boolean)
|
|
2014
|
+
.join('\n')
|
|
685
2015
|
: '';
|
|
686
2016
|
if (!content.trim()) {
|
|
687
2017
|
return [];
|
|
@@ -694,6 +2024,7 @@ export class CodexSessionsProvider implements IProviderSessions {
|
|
|
694
2024
|
kind: 'text',
|
|
695
2025
|
role: 'assistant',
|
|
696
2026
|
content,
|
|
2027
|
+
memoryCitations: raw.memoryCitations,
|
|
697
2028
|
})];
|
|
698
2029
|
}
|
|
699
2030
|
|
|
@@ -707,6 +2038,9 @@ export class CodexSessionsProvider implements IProviderSessions {
|
|
|
707
2038
|
toolName: raw.toolName || 'Unknown',
|
|
708
2039
|
toolInput: raw.toolInput,
|
|
709
2040
|
toolId: raw.toolCallId || baseId,
|
|
2041
|
+
subagentTools: raw.subagentTools,
|
|
2042
|
+
subagent: raw.subagent,
|
|
2043
|
+
memoryCitations: raw.memoryCitations,
|
|
710
2044
|
})];
|
|
711
2045
|
}
|
|
712
2046
|
|
|
@@ -743,92 +2077,157 @@ export class CodexSessionsProvider implements IProviderSessions {
|
|
|
743
2077
|
const baseId = raw.uuid || generateMessageId('codex');
|
|
744
2078
|
|
|
745
2079
|
if (raw.type === 'item') {
|
|
2080
|
+
// Live items carry a stable SDK id, so an in-progress row and its later
|
|
2081
|
+
// completion collapse onto the same transcript entry instead of stacking.
|
|
2082
|
+
const itemId = readNonEmptyString(raw.itemId as string | undefined) ?? baseId;
|
|
2083
|
+
|
|
746
2084
|
switch (raw.itemType) {
|
|
747
|
-
case 'agent_message':
|
|
2085
|
+
case 'agent_message': {
|
|
2086
|
+
const cited = readCodexMemoryCitations(String(raw.message?.content || ''));
|
|
2087
|
+
const text = cited.text;
|
|
2088
|
+
const proposedPlan = readCodexProposedPlan(text);
|
|
2089
|
+
if (proposedPlan) {
|
|
2090
|
+
// Same unification as history: a proposed plan is a plan card, not
|
|
2091
|
+
// an assistant paragraph that happens to start with a tag.
|
|
2092
|
+
return [createNormalizedMessage({
|
|
2093
|
+
id: itemId,
|
|
2094
|
+
sessionId,
|
|
2095
|
+
timestamp: ts,
|
|
2096
|
+
provider: PROVIDER,
|
|
2097
|
+
kind: 'tool_use',
|
|
2098
|
+
toolName: 'ExitPlanMode',
|
|
2099
|
+
toolInput: { plan: proposedPlan },
|
|
2100
|
+
toolId: itemId,
|
|
2101
|
+
memoryCitations: cited.memoryCitations,
|
|
2102
|
+
})];
|
|
2103
|
+
}
|
|
748
2104
|
return [createNormalizedMessage({
|
|
749
|
-
id:
|
|
2105
|
+
id: itemId,
|
|
750
2106
|
sessionId,
|
|
751
2107
|
timestamp: ts,
|
|
752
2108
|
provider: PROVIDER,
|
|
753
2109
|
kind: 'text',
|
|
754
2110
|
role: 'assistant',
|
|
755
|
-
content:
|
|
2111
|
+
content: text,
|
|
2112
|
+
memoryCitations: cited.memoryCitations,
|
|
756
2113
|
})];
|
|
2114
|
+
}
|
|
757
2115
|
case 'reasoning':
|
|
758
2116
|
return [createNormalizedMessage({
|
|
759
|
-
id:
|
|
2117
|
+
id: itemId,
|
|
760
2118
|
sessionId,
|
|
761
2119
|
timestamp: ts,
|
|
762
2120
|
provider: PROVIDER,
|
|
763
2121
|
kind: 'thinking',
|
|
764
2122
|
content: raw.message?.content || '',
|
|
765
2123
|
})];
|
|
766
|
-
case 'command_execution':
|
|
767
|
-
|
|
768
|
-
id:
|
|
2124
|
+
case 'command_execution': {
|
|
2125
|
+
const toolUse = createNormalizedMessage({
|
|
2126
|
+
id: itemId,
|
|
769
2127
|
sessionId,
|
|
770
2128
|
timestamp: ts,
|
|
771
2129
|
provider: PROVIDER,
|
|
772
2130
|
kind: 'tool_use',
|
|
773
2131
|
toolName: 'Bash',
|
|
774
2132
|
toolInput: { command: raw.command },
|
|
775
|
-
toolId:
|
|
776
|
-
output: raw.output,
|
|
777
|
-
exitCode: raw.exitCode,
|
|
2133
|
+
toolId: itemId,
|
|
778
2134
|
status: raw.status,
|
|
2135
|
+
});
|
|
2136
|
+
const partialOutput = typeof raw.output === 'string' ? raw.output : '';
|
|
2137
|
+
if (raw.status === 'in_progress' && !partialOutput) {
|
|
2138
|
+
return [toolUse];
|
|
2139
|
+
}
|
|
2140
|
+
// The SDK reports a command's output on the item itself; the UI reads
|
|
2141
|
+
// tool output from a paired result, so one is emitted here. Emitting
|
|
2142
|
+
// it while the command is still running is what lets its output
|
|
2143
|
+
// stream into the row instead of appearing only at the end.
|
|
2144
|
+
return [toolUse, createNormalizedMessage({
|
|
2145
|
+
id: `${itemId}_result`,
|
|
2146
|
+
sessionId,
|
|
2147
|
+
timestamp: ts,
|
|
2148
|
+
provider: PROVIDER,
|
|
2149
|
+
kind: 'tool_result',
|
|
2150
|
+
toolId: itemId,
|
|
2151
|
+
content: partialOutput,
|
|
2152
|
+
isError: raw.status === 'failed' || (typeof raw.exitCode === 'number' && raw.exitCode !== 0),
|
|
779
2153
|
})];
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
2154
|
+
}
|
|
2155
|
+
case 'file_change': {
|
|
2156
|
+
// One row per file so each change gets the same diff view as an Edit.
|
|
2157
|
+
const changes = Array.isArray(raw.changes) ? raw.changes : [];
|
|
2158
|
+
return changes.map((change: AnyRecord, index: number) => createNormalizedMessage({
|
|
2159
|
+
id: `${itemId}_${index}`,
|
|
783
2160
|
sessionId,
|
|
784
2161
|
timestamp: ts,
|
|
785
2162
|
provider: PROVIDER,
|
|
786
2163
|
kind: 'tool_use',
|
|
787
|
-
toolName: '
|
|
788
|
-
toolInput:
|
|
789
|
-
toolId:
|
|
2164
|
+
toolName: change?.kind === 'add' ? 'Write' : 'Edit',
|
|
2165
|
+
toolInput: { file_path: change?.path, old_string: '', new_string: '' },
|
|
2166
|
+
toolId: `${itemId}_${index}`,
|
|
790
2167
|
status: raw.status,
|
|
791
|
-
})
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
2168
|
+
}));
|
|
2169
|
+
}
|
|
2170
|
+
case 'mcp_tool_call': {
|
|
2171
|
+
const toolName = raw.server ? `mcp__${String(raw.server)}__${String(raw.tool ?? 'tool')}` : String(raw.tool || 'MCP');
|
|
2172
|
+
const toolUse = createNormalizedMessage({
|
|
2173
|
+
id: itemId,
|
|
795
2174
|
sessionId,
|
|
796
2175
|
timestamp: ts,
|
|
797
2176
|
provider: PROVIDER,
|
|
798
2177
|
kind: 'tool_use',
|
|
799
|
-
toolName
|
|
2178
|
+
toolName,
|
|
800
2179
|
toolInput: raw.arguments,
|
|
801
|
-
toolId:
|
|
802
|
-
server: raw.server,
|
|
803
|
-
result: raw.result,
|
|
804
|
-
error: raw.error,
|
|
2180
|
+
toolId: itemId,
|
|
805
2181
|
status: raw.status,
|
|
2182
|
+
});
|
|
2183
|
+
if (raw.status === 'in_progress') {
|
|
2184
|
+
return [toolUse];
|
|
2185
|
+
}
|
|
2186
|
+
return [toolUse, createNormalizedMessage({
|
|
2187
|
+
id: `${itemId}_result`,
|
|
2188
|
+
sessionId,
|
|
2189
|
+
timestamp: ts,
|
|
2190
|
+
provider: PROVIDER,
|
|
2191
|
+
kind: 'tool_result',
|
|
2192
|
+
toolId: itemId,
|
|
2193
|
+
content: raw.error
|
|
2194
|
+
? String((raw.error as AnyRecord)?.message ?? raw.error)
|
|
2195
|
+
: JSON.stringify(raw.result ?? ''),
|
|
2196
|
+
isError: Boolean(raw.error) || raw.status === 'failed',
|
|
806
2197
|
})];
|
|
2198
|
+
}
|
|
807
2199
|
case 'web_search':
|
|
808
2200
|
return [createNormalizedMessage({
|
|
809
|
-
id:
|
|
2201
|
+
id: itemId,
|
|
810
2202
|
sessionId,
|
|
811
2203
|
timestamp: ts,
|
|
812
2204
|
provider: PROVIDER,
|
|
813
2205
|
kind: 'tool_use',
|
|
814
2206
|
toolName: 'WebSearch',
|
|
815
2207
|
toolInput: { query: raw.query },
|
|
816
|
-
toolId:
|
|
2208
|
+
toolId: itemId,
|
|
817
2209
|
})];
|
|
818
2210
|
case 'todo_list':
|
|
2211
|
+
// Codex's live plan uses `{text, completed}`; Claude's TodoWrite uses
|
|
2212
|
+
// `{content, status}`. Normalizing here means one renderer, not two.
|
|
819
2213
|
return [createNormalizedMessage({
|
|
820
|
-
id:
|
|
2214
|
+
id: itemId,
|
|
821
2215
|
sessionId,
|
|
822
2216
|
timestamp: ts,
|
|
823
2217
|
provider: PROVIDER,
|
|
824
2218
|
kind: 'tool_use',
|
|
825
|
-
toolName: '
|
|
826
|
-
toolInput: {
|
|
827
|
-
|
|
2219
|
+
toolName: 'TodoWrite',
|
|
2220
|
+
toolInput: {
|
|
2221
|
+
todos: (Array.isArray(raw.items) ? raw.items : []).map((item: AnyRecord) => ({
|
|
2222
|
+
content: String(item?.text ?? ''),
|
|
2223
|
+
status: item?.completed ? 'completed' : 'pending',
|
|
2224
|
+
})),
|
|
2225
|
+
},
|
|
2226
|
+
toolId: itemId,
|
|
828
2227
|
})];
|
|
829
2228
|
case 'error':
|
|
830
2229
|
return [createNormalizedMessage({
|
|
831
|
-
id:
|
|
2230
|
+
id: itemId,
|
|
832
2231
|
sessionId,
|
|
833
2232
|
timestamp: ts,
|
|
834
2233
|
provider: PROVIDER,
|
|
@@ -837,14 +2236,14 @@ export class CodexSessionsProvider implements IProviderSessions {
|
|
|
837
2236
|
})];
|
|
838
2237
|
default:
|
|
839
2238
|
return [createNormalizedMessage({
|
|
840
|
-
id:
|
|
2239
|
+
id: itemId,
|
|
841
2240
|
sessionId,
|
|
842
2241
|
timestamp: ts,
|
|
843
2242
|
provider: PROVIDER,
|
|
844
2243
|
kind: 'tool_use',
|
|
845
2244
|
toolName: raw.itemType || 'Unknown',
|
|
846
2245
|
toolInput: raw.item || raw,
|
|
847
|
-
toolId:
|
|
2246
|
+
toolId: itemId,
|
|
848
2247
|
})];
|
|
849
2248
|
}
|
|
850
2249
|
}
|
|
@@ -873,8 +2272,8 @@ export class CodexSessionsProvider implements IProviderSessions {
|
|
|
873
2272
|
}
|
|
874
2273
|
|
|
875
2274
|
/**
|
|
876
|
-
* Loads Codex JSONL history and keeps token usage metadata when
|
|
877
|
-
*
|
|
2275
|
+
* Loads Codex JSONL history and keeps token usage metadata when the
|
|
2276
|
+
* transcript reported it.
|
|
878
2277
|
*/
|
|
879
2278
|
async fetchHistory(
|
|
880
2279
|
sessionId: string,
|
|
@@ -884,20 +2283,15 @@ export class CodexSessionsProvider implements IProviderSessions {
|
|
|
884
2283
|
|
|
885
2284
|
let result: CodexHistoryResult;
|
|
886
2285
|
try {
|
|
887
|
-
|
|
888
|
-
// not raw JSONL records.
|
|
889
|
-
result = await getCodexSessionMessages(sessionId, null, 0);
|
|
2286
|
+
result = await getCodexSessionMessages(sessionId);
|
|
890
2287
|
} catch (error) {
|
|
891
2288
|
const message = error instanceof Error ? error.message : String(error);
|
|
892
2289
|
console.warn(`[CodexProvider] Failed to load session ${sessionId}:`, message);
|
|
893
2290
|
return { messages: [], total: 0, hasMore: false, offset: 0, limit: null };
|
|
894
2291
|
}
|
|
895
2292
|
|
|
896
|
-
const rawMessages = Array.isArray(result) ? result : (result.messages || []);
|
|
897
|
-
const tokenUsage = Array.isArray(result) ? undefined : result.tokenUsage;
|
|
898
|
-
|
|
899
2293
|
const normalized: NormalizedMessage[] = [];
|
|
900
|
-
for (const raw of
|
|
2294
|
+
for (const raw of result.messages) {
|
|
901
2295
|
normalized.push(...this.normalizeHistoryEntry(raw, sessionId));
|
|
902
2296
|
}
|
|
903
2297
|
|
|
@@ -916,15 +2310,13 @@ export class CodexSessionsProvider implements IProviderSessions {
|
|
|
916
2310
|
}
|
|
917
2311
|
}
|
|
918
2312
|
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
}
|
|
924
|
-
}
|
|
2313
|
+
// Everything the transcript draws, and nothing else — so a page of N rows
|
|
2314
|
+
// is N rows the user sees, and `total` counts the same thing.
|
|
2315
|
+
const transcript = prepareTranscriptMessages(normalized);
|
|
2316
|
+
const total = transcript.length;
|
|
925
2317
|
const normalizedOffset = Math.max(0, offset);
|
|
926
2318
|
const normalizedLimit = limit === null ? null : Math.max(0, limit);
|
|
927
|
-
const { page, hasMore } = sliceTailPage(
|
|
2319
|
+
const { page, hasMore } = sliceTailPage(transcript, normalizedLimit, normalizedOffset);
|
|
928
2320
|
|
|
929
2321
|
return {
|
|
930
2322
|
messages: page,
|
|
@@ -932,7 +2324,7 @@ export class CodexSessionsProvider implements IProviderSessions {
|
|
|
932
2324
|
hasMore,
|
|
933
2325
|
offset: normalizedOffset,
|
|
934
2326
|
limit: normalizedLimit,
|
|
935
|
-
tokenUsage,
|
|
2327
|
+
tokenUsage: result.tokenUsage,
|
|
936
2328
|
};
|
|
937
2329
|
}
|
|
938
2330
|
}
|