@cloudcli-ai/cloudcli 1.37.3 → 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 +1 -1
- package/dist/assets/{abnfDiagram-N423BO3Z-CJntskwA.js → abnfDiagram-N423BO3Z-Dmkwx38H.js} +1 -1
- package/dist/assets/{arc-CIfH4uMN.js → arc-DLe9AYuO.js} +1 -1
- package/dist/assets/{architectureDiagram-T3A2C74G-_IpEkukg.js → architectureDiagram-T3A2C74G-CBbbbNUW.js} +1 -1
- package/dist/assets/{blockDiagram-VBNYF7ZC-BttxrpgZ.js → blockDiagram-VBNYF7ZC-CaMEaWu4.js} +1 -1
- package/dist/assets/{c4Diagram-5PPSVZJV-g51dSRr1.js → c4Diagram-5PPSVZJV-DhjMgNnR.js} +1 -1
- package/dist/assets/channel-CWrmwsir.js +1 -0
- package/dist/assets/{chunk-2GRJ4B5K-D_-LbQUa.js → chunk-2GRJ4B5K-yjl3-F-i.js} +1 -1
- package/dist/assets/{chunk-2Q5K7J3B-BtlHzwaO.js → chunk-2Q5K7J3B-DsAYaWEP.js} +1 -1
- package/dist/assets/{chunk-5RXB4S5H-UzkpvpAT.js → chunk-5RXB4S5H-Bt5qmqOX.js} +1 -1
- package/dist/assets/{chunk-5VM5RSS4-BKwx5Gag.js → chunk-5VM5RSS4-yWrWVvUt.js} +1 -1
- package/dist/assets/{chunk-6Q2QTUOP-D_KEBH5f.js → chunk-6Q2QTUOP-Dt66K5xb.js} +1 -1
- package/dist/assets/{chunk-GF5L2VYU-rK2DGyto.js → chunk-GF5L2VYU-DA0vzaj4.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-CeBl_N9i.js → chunk-JWPE2WC7-CUkbx96V.js} +1 -1
- package/dist/assets/{chunk-KBJHAD2P-BiQLWVIr.js → chunk-KBJHAD2P-DWKxx1WH.js} +1 -1
- package/dist/assets/{chunk-RYQCIY6F-C1knVGV5.js → chunk-RYQCIY6F-BqluplN0.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-CtKbJkvF.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-DjX8y40Z.js → cose-bilkent-JH36ORCC-DHV93-Em.js} +1 -1
- package/dist/assets/{cynefin-VYW2F7L2-DM6YeRba.js → cynefin-VYW2F7L2-B9HZgoGX.js} +1 -1
- package/dist/assets/{cynefinDiagram-MW4NZA55-wZJ-vy5B.js → cynefinDiagram-MW4NZA55-hFjjEQg8.js} +1 -1
- package/dist/assets/{dagre-VZM6K2ZE-BzP0ejkH.js → dagre-VZM6K2ZE-CpeVOC7V.js} +1 -1
- package/dist/assets/{diagram-7IWD3JNH-BTL_j23J.js → diagram-7IWD3JNH-D0H1UoRA.js} +1 -1
- package/dist/assets/{diagram-B4RE2ZJO-Cf1Norgu.js → diagram-B4RE2ZJO-6Y7BQUup.js} +1 -1
- package/dist/assets/{diagram-LBJQPF4R-DPiUEKr-.js → diagram-LBJQPF4R-C0nNOwht.js} +1 -1
- package/dist/assets/{diagram-Q27KOJAE-CTcTpiz5.js → diagram-Q27KOJAE-En6QlOVX.js} +1 -1
- package/dist/assets/{diagram-UB23O5K3-DPMSdYm0.js → diagram-UB23O5K3-CmPiNeGD.js} +1 -1
- package/dist/assets/{ebnfDiagram-BXEA7PRR-7yvD6uiF.js → ebnfDiagram-BXEA7PRR-Bygzp1Qt.js} +1 -1
- package/dist/assets/{erDiagram-JOGREHBK-CfgeDoV6.js → erDiagram-JOGREHBK-CwScRfzl.js} +1 -1
- package/dist/assets/{flowDiagram-UKHOOZJN-B27ng5zu.js → flowDiagram-UKHOOZJN-bKX-0xP6.js} +1 -1
- package/dist/assets/{ganttDiagram-PKOTCBZU-CcC6b_7t.js → ganttDiagram-PKOTCBZU-DtZPKXGd.js} +1 -1
- package/dist/assets/{gitGraphDiagram-DS77QQ5N-BX5kPGrO.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-CUP-SlN4.js → infoDiagram-6WML65LV-ciRQTS0h.js} +1 -1
- package/dist/assets/{ishikawaDiagram-WSZJBQD7-BTTDhOpX.js → ishikawaDiagram-WSZJBQD7-CgjhQZ84.js} +1 -1
- package/dist/assets/{journeyDiagram-NVQOT4AX-ARXcc61m.js → journeyDiagram-NVQOT4AX-BsbQSeIW.js} +1 -1
- package/dist/assets/{kanban-definition-27J2QSJJ-BieMpqMA.js → kanban-definition-27J2QSJJ-CrwzY9Jw.js} +1 -1
- package/dist/assets/{linear-61vxjRnP.js → linear-BUaCDr6l.js} +1 -1
- package/dist/assets/{mermaid.core-nhnwkfQx.js → mermaid.core-ClgdUU0j.js} +5 -5
- package/dist/assets/{mindmap-definition-FAOFIHXS-Cm6x5vT1.js → mindmap-definition-FAOFIHXS-BictljY1.js} +1 -1
- package/dist/assets/{pegDiagram-VL7TDLO6-CcYBq_9H.js → pegDiagram-VL7TDLO6-BXdByy_U.js} +1 -1
- package/dist/assets/{pieDiagram-7S7Q4E2Y-CfvIjfzo.js → pieDiagram-7S7Q4E2Y-uZsMfkRX.js} +1 -1
- package/dist/assets/{quadrantDiagram-CIZ2JOQS-SgYoWPNb.js → quadrantDiagram-CIZ2JOQS-7lCv4QIj.js} +1 -1
- package/dist/assets/{railroadDiagram-AXF67PYL-D2Mu5E_f.js → railroadDiagram-AXF67PYL-07dxIWbi.js} +1 -1
- package/dist/assets/{requirementDiagram-LRYGKXZP-Azwe_k4Q.js → requirementDiagram-LRYGKXZP-Cb31Yv1_.js} +1 -1
- package/dist/assets/{sankeyDiagram-W5VNT64P-DN1Ju3PA.js → sankeyDiagram-W5VNT64P-X0YPSyF9.js} +1 -1
- package/dist/assets/{sequenceDiagram-SI44F4Z6-Bj4qC34S.js → sequenceDiagram-SI44F4Z6-BdBxSIlP.js} +1 -1
- package/dist/assets/{sizeCapture-X5ZJPWSS-Co4PdklG.js → sizeCapture-X5ZJPWSS-DQM5xAyD.js} +1 -1
- package/dist/assets/{stateDiagram-OKZ733FA-nOvL9dRC.js → stateDiagram-OKZ733FA-DJ3xkLUF.js} +1 -1
- package/dist/assets/{stateDiagram-v2-UEYNNEHI-DNv8ppw7.js → stateDiagram-v2-UEYNNEHI-vAaE4zNo.js} +1 -1
- package/dist/assets/{swimlanes-SLNWSIFB-CCK3wMid.js → swimlanes-SLNWSIFB-rjfYdW0w.js} +2 -2
- package/dist/assets/swimlanesDiagram-ULZ7WXOC-TV2IF4Nq.js +8 -0
- package/dist/assets/{timeline-definition-Z64GVDOM-BC29RNFm.js → timeline-definition-Z64GVDOM-DZMra5do.js} +1 -1
- package/dist/assets/{vennDiagram-T6HMQDX7-pZlZCtEo.js → vennDiagram-T6HMQDX7-BevD8uu0.js} +1 -1
- package/dist/assets/{wardleyDiagram-T6FBY63Y-De8k2-zM.js → wardleyDiagram-T6FBY63Y-DsIyXB7z.js} +1 -1
- package/dist/assets/{xychartDiagram-ELKLHX3M-Bk8ykH0w.js → xychartDiagram-ELKLHX3M-G3PlF3ko.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/server/index.js +4 -1
- 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 +175 -128
- 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 +18 -1
- package/dist-server/server/modules/agent/tests/agent.routes.test.js.map +1 -1
- package/dist-server/server/modules/database/migrations.js +13 -0
- 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/sessions.db.js +7 -0
- package/dist-server/server/modules/database/repositories/sessions.db.js.map +1 -1
- package/dist-server/server/modules/database/schema.js +7 -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/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/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/list/claude/claude-auth.provider.js +42 -8
- package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-models.provider.js +6 -16
- 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 +305 -16
- 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 +686 -77
- package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-models.provider.js +31 -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 +11 -4
- package/dist-server/server/modules/providers/list/codex/codex-runtime.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +57 -0
- package/dist-server/server/modules/providers/list/codex/codex-sessions.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 +8 -2
- package/dist-server/server/modules/providers/list/cursor/cursor-runtime.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js +16 -3
- 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 +110 -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 +9 -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/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 +4 -0
- package/dist-server/server/modules/providers/services/session-history-cache.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/sessions.service.js +73 -3
- package/dist-server/server/modules/providers/services/sessions.service.js.map +1 -1
- package/dist-server/server/modules/providers/tests/claude-auth.test.js +131 -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-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 +1368 -12
- 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/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 +32 -0
- package/dist-server/server/modules/providers/tests/codex-runtime.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/codex-sessions.test.js +50 -0
- 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 +79 -0
- 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.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-history-cache.service.test.js +19 -0
- package/dist-server/server/modules/providers/tests/session-history-cache.service.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/sessions.service.test.js +175 -1
- package/dist-server/server/modules/providers/tests/sessions.service.test.js.map +1 -1
- package/dist-server/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.js +13 -1
- package/dist-server/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.js.map +1 -1
- package/dist-server/server/modules/scheduled-messages/tests/scheduled-messages.test.js +36 -1
- package/dist-server/server/modules/scheduled-messages/tests/scheduled-messages.test.js.map +1 -1
- package/dist-server/server/modules/websocket/services/chat-run-registry.service.js +27 -5
- package/dist-server/server/modules/websocket/services/chat-run-registry.service.js.map +1 -1
- package/dist-server/server/modules/websocket/services/chat-websocket.service.js +38 -1
- package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
- package/dist-server/server/modules/websocket/services/shell-websocket.service.js +230 -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-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/shell-websocket.service.test.js +420 -0
- package/dist-server/server/modules/websocket/tests/shell-websocket.service.test.js.map +1 -1
- package/dist-server/server/shared/message-unification.js +1 -1
- package/dist-server/server/shared/message-unification.js.map +1 -1
- 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 +126 -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 +4 -4
- package/server/index.ts +5 -1
- package/server/modules/agent/agent.module.ts +11 -1
- package/server/modules/agent/agent.routes.ts +198 -140
- package/server/modules/agent/tests/agent-run-registration.test.ts +348 -0
- package/server/modules/agent/tests/agent.routes.test.ts +18 -1
- package/server/modules/database/migrations.ts +14 -0
- package/server/modules/database/repositories/provider-models.ts +79 -6
- package/server/modules/database/repositories/sessions.db.ts +8 -0
- package/server/modules/database/schema.ts +7 -0
- package/server/modules/database/tests/provider-models.db.integration.test.ts +113 -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/tests/project-clone.routes.test.ts +246 -0
- package/server/modules/projects/tests/project-clone.service.test.ts +376 -0
- package/server/modules/providers/list/claude/claude-auth.provider.ts +60 -10
- package/server/modules/providers/list/claude/claude-models.provider.ts +10 -22
- package/server/modules/providers/list/claude/claude-runtime.provider.js +322 -15
- package/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts +40 -14
- package/server/modules/providers/list/claude/claude-sessions.provider.ts +796 -88
- package/server/modules/providers/list/codex/codex-models.provider.ts +31 -0
- package/server/modules/providers/list/codex/codex-runtime.provider.ts +12 -4
- package/server/modules/providers/list/codex/codex-sessions.provider.ts +63 -0
- package/server/modules/providers/list/cursor/cursor-auth.provider.ts +10 -2
- package/server/modules/providers/list/cursor/cursor-runtime.provider.js +9 -2
- package/server/modules/providers/list/opencode/opencode-models.provider.ts +18 -3
- 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 +131 -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 +17 -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/session-conversations-search.service.ts +2 -5
- package/server/modules/providers/services/session-history-cache.service.ts +13 -2
- package/server/modules/providers/services/sessions.service.ts +108 -8
- package/server/modules/providers/tests/claude-auth.test.ts +150 -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-runtime-hold.test.ts +309 -0
- package/server/modules/providers/tests/claude-sessions.test.ts +1630 -12
- package/server/modules/providers/tests/claude-settings-permissions.test.ts +271 -0
- package/server/modules/providers/tests/codex-models.test.ts +37 -0
- package/server/modules/providers/tests/codex-runtime.test.ts +36 -0
- package/server/modules/providers/tests/codex-sessions.test.ts +59 -0
- package/server/modules/providers/tests/cursor-auth.test.ts +187 -0
- package/server/modules/providers/tests/opencode-models.test.ts +100 -0
- 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.routes.test.ts +230 -1
- package/server/modules/providers/tests/session-history-cache.service.test.ts +22 -0
- package/server/modules/providers/tests/sessions.service.test.ts +237 -1
- package/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.ts +14 -1
- package/server/modules/scheduled-messages/tests/scheduled-messages.test.ts +45 -1
- package/server/modules/websocket/README.md +4 -2
- package/server/modules/websocket/services/chat-run-registry.service.ts +29 -5
- package/server/modules/websocket/services/chat-websocket.service.ts +58 -1
- package/server/modules/websocket/services/shell-websocket.service.ts +269 -22
- package/server/modules/websocket/tests/chat-background-subscribe.test.ts +167 -0
- package/server/modules/websocket/tests/chat-stop-task.test.ts +127 -0
- package/server/modules/websocket/tests/shell-websocket.service.test.ts +521 -0
- package/server/shared/interfaces.ts +29 -0
- package/server/shared/message-unification.ts +1 -1
- 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 +261 -7
- package/server/shared/utils.ts +136 -0
- package/dist/assets/channel-C5xU9jMw.js +0 -1
- package/dist/assets/classDiagram-JCYQIIEL-CRrL3vp7.js +0 -1
- package/dist/assets/classDiagram-v2-OCEON4UE-CRrL3vp7.js +0 -1
- package/dist/assets/index-BTDHQ6Id.css +0 -32
- package/dist/assets/index-sFfz8iBR.js +0 -4931
- package/dist/assets/swimlanesDiagram-ULZ7WXOC-DnfbNQEp.js +0 -8
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
|
-
import { appendFile, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { appendFile, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import test from 'node:test';
|
|
6
6
|
|
|
7
7
|
import { closeConnection, initializeDatabase, projectsDb, sessionsDb } from '@/modules/database/index.js';
|
|
8
|
+
import { providerRegistry } from '@/modules/providers/provider.registry.js';
|
|
8
9
|
import { sessionsService } from '@/modules/providers/services/sessions.service.js';
|
|
10
|
+
import { chatRunRegistry } from '@/modules/websocket/index.js';
|
|
11
|
+
import type { IProvider } from '@/shared/interfaces.js';
|
|
12
|
+
import type { BackgroundTaskSummary } from '@/shared/types.js';
|
|
9
13
|
|
|
10
14
|
async function withIsolatedDatabase(runTest: () => void | Promise<void>): Promise<void> {
|
|
11
15
|
const previousDatabasePath = process.env.DATABASE_PATH;
|
|
@@ -191,3 +195,235 @@ test('history pages are sliced from the cached full transcript and see appended
|
|
|
191
195
|
await rm(transcriptDirectory, { recursive: true, force: true });
|
|
192
196
|
}
|
|
193
197
|
});
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Stands in for the provider registry with one provider per id, each with the
|
|
201
|
+
* runtime given — the seam through which a runtime's background work reaches
|
|
202
|
+
* the running-sessions list without an SDK run behind it.
|
|
203
|
+
*/
|
|
204
|
+
async function withProviders(
|
|
205
|
+
runtimes: Partial<Record<'claude' | 'codex', IProvider['runtime']>>,
|
|
206
|
+
runTest: () => void | Promise<void>,
|
|
207
|
+
): Promise<void> {
|
|
208
|
+
const realListProviders = providerRegistry.listProviders;
|
|
209
|
+
providerRegistry.listProviders = () =>
|
|
210
|
+
Object.entries(runtimes).map(([id, runtime]) => ({ id, runtime }) as IProvider);
|
|
211
|
+
try {
|
|
212
|
+
await runTest();
|
|
213
|
+
} finally {
|
|
214
|
+
providerRegistry.listProviders = realListProviders;
|
|
215
|
+
chatRunRegistry.clearAll();
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Runs `runTest` against a Claude CLI process registry containing exactly
|
|
221
|
+
* `records`, so the running-sessions tests never read the developer's real
|
|
222
|
+
* `~/.claude/sessions`.
|
|
223
|
+
*/
|
|
224
|
+
async function withClaudeCliRegistry(
|
|
225
|
+
records: Array<Record<string, unknown>>,
|
|
226
|
+
runTest: () => void | Promise<void>,
|
|
227
|
+
): Promise<void> {
|
|
228
|
+
const homeDirectory = await mkdtemp(path.join(os.tmpdir(), 'claude-cli-registry-'));
|
|
229
|
+
await mkdir(path.join(homeDirectory, '.claude', 'sessions'), { recursive: true });
|
|
230
|
+
for (const record of records) {
|
|
231
|
+
await writeFile(
|
|
232
|
+
path.join(homeDirectory, '.claude', 'sessions', `${record.pid}.json`),
|
|
233
|
+
JSON.stringify(record),
|
|
234
|
+
'utf8',
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const realHomedir = os.homedir;
|
|
239
|
+
(os as unknown as { homedir: () => string }).homedir = () => homeDirectory;
|
|
240
|
+
try {
|
|
241
|
+
await runTest();
|
|
242
|
+
} finally {
|
|
243
|
+
(os as unknown as { homedir: () => string }).homedir = realHomedir;
|
|
244
|
+
await rm(homeDirectory, { recursive: true, force: true });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const task = (taskId: string, startedAt: number): BackgroundTaskSummary => ({
|
|
249
|
+
taskId,
|
|
250
|
+
toolUseId: `toolu_${taskId}`,
|
|
251
|
+
taskType: 'local_agent',
|
|
252
|
+
description: `Task ${taskId}`,
|
|
253
|
+
startedAt,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test('running sessions list a session whose background work outlived its turn', { concurrency: false }, async () => {
|
|
257
|
+
await withIsolatedDatabase(async () => {
|
|
258
|
+
sessionsDb.createAppSession('held-session', 'claude', '/tmp/running-project');
|
|
259
|
+
// The turn ran and completed: the registry still holds the finished run.
|
|
260
|
+
const run = chatRunRegistry.startRun({
|
|
261
|
+
appSessionId: 'held-session', provider: 'claude', providerSessionId: null, connection: null, userId: null,
|
|
262
|
+
});
|
|
263
|
+
assert.ok(run);
|
|
264
|
+
run.writer.send({ kind: 'text', content: 'launched', sessionId: 'held-session', provider: 'claude' });
|
|
265
|
+
run.writer.sendComplete({ exitCode: 0 });
|
|
266
|
+
assert.equal(run.status, 'completed');
|
|
267
|
+
|
|
268
|
+
const tasks = [task('late', 2_000), task('early', 1_000)];
|
|
269
|
+
await withClaudeCliRegistry([], async () => await withProviders(
|
|
270
|
+
{ claude: { run: async () => undefined, abort: () => false, listBackgroundWork: () => [{ sessionId: 'held-session', tasks }] } },
|
|
271
|
+
async () => {
|
|
272
|
+
assert.deepEqual(await sessionsService.listRunningSessions(), [{
|
|
273
|
+
sessionId: 'held-session',
|
|
274
|
+
provider: 'claude',
|
|
275
|
+
startedAt: 1_000,
|
|
276
|
+
lastSeq: run.lastSeq,
|
|
277
|
+
background: true,
|
|
278
|
+
canInterrupt: false,
|
|
279
|
+
tasks,
|
|
280
|
+
}]);
|
|
281
|
+
assert.ok(run.lastSeq > 0, 'the finished run still reports its sequence for replay');
|
|
282
|
+
},
|
|
283
|
+
));
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test('running sessions carry their tasks on a chat run that is still going', { concurrency: false }, async () => {
|
|
288
|
+
await withIsolatedDatabase(async () => {
|
|
289
|
+
sessionsDb.createAppSession('busy-session', 'claude', '/tmp/running-project');
|
|
290
|
+
sessionsDb.createAppSession('idle-session', 'codex', '/tmp/running-project');
|
|
291
|
+
const run = chatRunRegistry.startRun({
|
|
292
|
+
appSessionId: 'busy-session', provider: 'claude', providerSessionId: null, connection: null, userId: null,
|
|
293
|
+
});
|
|
294
|
+
assert.ok(run);
|
|
295
|
+
|
|
296
|
+
const tasks = [task('agent', 5_000)];
|
|
297
|
+
await withClaudeCliRegistry([], async () => await withProviders(
|
|
298
|
+
{
|
|
299
|
+
claude: { run: async () => undefined, abort: () => false, listBackgroundWork: () => [{ sessionId: 'busy-session', tasks }] },
|
|
300
|
+
// A runtime without background work contributes chat runs alone.
|
|
301
|
+
codex: { run: async () => undefined, abort: () => false },
|
|
302
|
+
},
|
|
303
|
+
async () => {
|
|
304
|
+
const sessions = await sessionsService.listRunningSessions();
|
|
305
|
+
assert.equal(sessions.length, 1, 'a session with a running chat run is listed once');
|
|
306
|
+
assert.deepEqual(sessions[0], {
|
|
307
|
+
sessionId: 'busy-session',
|
|
308
|
+
provider: 'claude',
|
|
309
|
+
startedAt: run.startedAt,
|
|
310
|
+
lastSeq: 0,
|
|
311
|
+
tasks,
|
|
312
|
+
});
|
|
313
|
+
},
|
|
314
|
+
));
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test('running sessions with no background work are the chat runs alone', { concurrency: false }, async () => {
|
|
319
|
+
await withIsolatedDatabase(async () => {
|
|
320
|
+
sessionsDb.createAppSession('plain-session', 'codex', '/tmp/running-project');
|
|
321
|
+
const run = chatRunRegistry.startRun({
|
|
322
|
+
appSessionId: 'plain-session', provider: 'codex', providerSessionId: null, connection: null, userId: null,
|
|
323
|
+
});
|
|
324
|
+
assert.ok(run);
|
|
325
|
+
|
|
326
|
+
await withClaudeCliRegistry([], async () => await withProviders(
|
|
327
|
+
{ codex: { run: async () => undefined, abort: () => false } },
|
|
328
|
+
async () => {
|
|
329
|
+
assert.deepEqual(await sessionsService.listRunningSessions(), [{
|
|
330
|
+
sessionId: 'plain-session', provider: 'codex', startedAt: run.startedAt, lastSeq: 0,
|
|
331
|
+
}]);
|
|
332
|
+
},
|
|
333
|
+
));
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test('running sessions report a turn driven by the Claude CLI', { concurrency: false }, async () => {
|
|
338
|
+
await withIsolatedDatabase(async () => {
|
|
339
|
+
sessionsDb.createAppSession('cli-session', 'claude', '/tmp/running-project');
|
|
340
|
+
sessionsDb.assignProviderSessionId('cli-session', 'claude-native-1');
|
|
341
|
+
|
|
342
|
+
// `process.pid` is the one pid guaranteed to be alive; omitting procStart
|
|
343
|
+
// keeps the fixture portable, since the start-time cross-check is Linux-only.
|
|
344
|
+
await withClaudeCliRegistry(
|
|
345
|
+
[{ pid: process.pid, sessionId: 'claude-native-1', status: 'busy', entrypoint: 'cli', startedAt: 4_000 }],
|
|
346
|
+
async () => await withProviders(
|
|
347
|
+
{ claude: { run: async () => undefined, abort: () => false } },
|
|
348
|
+
async () => {
|
|
349
|
+
assert.deepEqual(await sessionsService.listRunningSessions(), [{
|
|
350
|
+
sessionId: 'cli-session',
|
|
351
|
+
provider: 'claude',
|
|
352
|
+
startedAt: 4_000,
|
|
353
|
+
lastSeq: 0,
|
|
354
|
+
canInterrupt: false,
|
|
355
|
+
statusText: 'Running in the Claude CLI',
|
|
356
|
+
}]);
|
|
357
|
+
},
|
|
358
|
+
),
|
|
359
|
+
);
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
test('running sessions ignore registry files a crash left behind', { concurrency: false }, async () => {
|
|
364
|
+
await withIsolatedDatabase(async () => {
|
|
365
|
+
sessionsDb.createAppSession('stale-session', 'claude', '/tmp/running-project');
|
|
366
|
+
sessionsDb.assignProviderSessionId('stale-session', 'claude-native-stale');
|
|
367
|
+
|
|
368
|
+
await withClaudeCliRegistry(
|
|
369
|
+
[
|
|
370
|
+
// The process is gone: signal 0 cannot find this pid.
|
|
371
|
+
{ pid: 4_194_302, sessionId: 'claude-native-stale', status: 'busy', startedAt: 1_000 },
|
|
372
|
+
// Alive, but waiting for input rather than producing a response.
|
|
373
|
+
{ pid: process.pid, sessionId: 'claude-native-idle', status: 'idle', startedAt: 2_000 },
|
|
374
|
+
],
|
|
375
|
+
async () => await withProviders(
|
|
376
|
+
{ claude: { run: async () => undefined, abort: () => false } },
|
|
377
|
+
async () => {
|
|
378
|
+
assert.deepEqual(await sessionsService.listRunningSessions(), []);
|
|
379
|
+
},
|
|
380
|
+
),
|
|
381
|
+
);
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
test('a CLI turn does not duplicate a session already running a chat run', { concurrency: false }, async () => {
|
|
386
|
+
await withIsolatedDatabase(async () => {
|
|
387
|
+
sessionsDb.createAppSession('both-session', 'claude', '/tmp/running-project');
|
|
388
|
+
sessionsDb.assignProviderSessionId('both-session', 'claude-native-both');
|
|
389
|
+
const run = chatRunRegistry.startRun({
|
|
390
|
+
appSessionId: 'both-session', provider: 'claude', providerSessionId: 'claude-native-both', connection: null, userId: null,
|
|
391
|
+
});
|
|
392
|
+
assert.ok(run);
|
|
393
|
+
|
|
394
|
+
await withClaudeCliRegistry(
|
|
395
|
+
[{ pid: process.pid, sessionId: 'claude-native-both', status: 'busy', startedAt: 3_000 }],
|
|
396
|
+
async () => await withProviders(
|
|
397
|
+
{ claude: { run: async () => undefined, abort: () => false } },
|
|
398
|
+
async () => {
|
|
399
|
+
const sessions = await sessionsService.listRunningSessions();
|
|
400
|
+
assert.equal(sessions.length, 1, 'the chat run already covers this session');
|
|
401
|
+
assert.equal(sessions[0].canInterrupt, undefined, 'the interruptible chat run entry wins');
|
|
402
|
+
},
|
|
403
|
+
),
|
|
404
|
+
);
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
test('an unreadable registry never fails the running-sessions poll', { concurrency: false }, async () => {
|
|
409
|
+
await withIsolatedDatabase(async () => {
|
|
410
|
+
sessionsDb.createAppSession('safe-session', 'claude', '/tmp/running-project');
|
|
411
|
+
sessionsDb.assignProviderSessionId('safe-session', 'claude-native-safe');
|
|
412
|
+
|
|
413
|
+
await withClaudeCliRegistry(
|
|
414
|
+
[{ pid: process.pid, sessionId: 'claude-native-safe', status: 'busy', startedAt: 5_000 }],
|
|
415
|
+
async () => {
|
|
416
|
+
// A half-written file sits alongside the good one; it must be skipped
|
|
417
|
+
// rather than abort the whole poll.
|
|
418
|
+
await writeFile(path.join(os.homedir(), '.claude', 'sessions', 'broken.json'), '{"pid":', 'utf8');
|
|
419
|
+
await withProviders(
|
|
420
|
+
{ claude: { run: async () => undefined, abort: () => false } },
|
|
421
|
+
async () => {
|
|
422
|
+
const sessions = await sessionsService.listRunningSessions();
|
|
423
|
+
assert.deepEqual(sessions.map((session) => session.sessionId), ['safe-session']);
|
|
424
|
+
},
|
|
425
|
+
);
|
|
426
|
+
},
|
|
427
|
+
);
|
|
428
|
+
});
|
|
429
|
+
});
|
|
@@ -81,13 +81,26 @@ async function sendClaimedQueuedMessage(
|
|
|
81
81
|
sessionDraftsDb.deleteEmptyDraft(candidate.userId, candidate.sessionId);
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Whether a queued turn has to keep waiting for the session.
|
|
86
|
+
*
|
|
87
|
+
* A turn that ends with background work still going (a backgrounded command,
|
|
88
|
+
* agent or workflow) is marked completed, but its CLI process stays open so
|
|
89
|
+
* that work can report back. A new turn replaces that process and stops the
|
|
90
|
+
* work, which is why the composer asks before an interactive send. A queued
|
|
91
|
+
* turn was promised to go "when this finishes", so it waits for that work too.
|
|
92
|
+
*/
|
|
93
|
+
function isSessionBusy(sessionId: string, runtime: ProviderRuntimeGateway): boolean {
|
|
94
|
+
return chatRunRegistry.isProcessing(sessionId) || runtime.hasBackgroundWork(sessionId);
|
|
95
|
+
}
|
|
96
|
+
|
|
84
97
|
/** Sends every persisted queued turn whose session is currently idle. */
|
|
85
98
|
export async function dispatchQueuedMessages(runtime: ProviderRuntimeGateway): Promise<number> {
|
|
86
99
|
const candidates = sessionDraftsDb.listQueuedMessages();
|
|
87
100
|
let claimed = 0;
|
|
88
101
|
|
|
89
102
|
await Promise.all(candidates.map(async (candidate) => {
|
|
90
|
-
if (
|
|
103
|
+
if (isSessionBusy(candidate.sessionId, runtime)) {
|
|
91
104
|
return;
|
|
92
105
|
}
|
|
93
106
|
if (!sessionDraftsDb.claimQueuedMessage(candidate)) {
|
|
@@ -37,9 +37,15 @@ async function withIsolatedDatabase(runTest: (userId: number) => void | Promise<
|
|
|
37
37
|
|
|
38
38
|
type RunCall = { provider: string; command: string; options: Record<string, unknown> };
|
|
39
39
|
|
|
40
|
-
function createRuntime(
|
|
40
|
+
function createRuntime(
|
|
41
|
+
runs: RunCall[],
|
|
42
|
+
behaviour: 'ok' | 'throw' = 'ok',
|
|
43
|
+
aborts: string[] = [],
|
|
44
|
+
hasBackgroundWork: (sessionId: string) => boolean = () => false,
|
|
45
|
+
) {
|
|
41
46
|
return {
|
|
42
47
|
hasRuntime: () => true,
|
|
48
|
+
hasBackgroundWork,
|
|
43
49
|
run: async (provider: string, command: string, options: Record<string, unknown>) => {
|
|
44
50
|
if (behaviour === 'throw') {
|
|
45
51
|
throw new Error('provider exploded');
|
|
@@ -117,6 +123,44 @@ test('a queued message stays pending while its session is busy', async () => {
|
|
|
117
123
|
});
|
|
118
124
|
});
|
|
119
125
|
|
|
126
|
+
test('a queued message waits for background work its session\'s last turn left running', async () => {
|
|
127
|
+
await withIsolatedDatabase(async (userId) => {
|
|
128
|
+
sessionDraftsDb.saveDraft(userId, SESSION_ID, {
|
|
129
|
+
text: '',
|
|
130
|
+
queuedMessage: { content: 'send after the background job' },
|
|
131
|
+
});
|
|
132
|
+
// Turn A ended, so its run is completed, but it backgrounded a command
|
|
133
|
+
// that is still running under A's CLI process.
|
|
134
|
+
const runA = chatRunRegistry.startRun({
|
|
135
|
+
appSessionId: SESSION_ID,
|
|
136
|
+
provider: 'claude',
|
|
137
|
+
providerSessionId: null,
|
|
138
|
+
connection: null,
|
|
139
|
+
userId,
|
|
140
|
+
});
|
|
141
|
+
runA!.writer.sendComplete({ exitCode: 0 });
|
|
142
|
+
assert.equal(chatRunRegistry.isProcessing(SESSION_ID), false);
|
|
143
|
+
|
|
144
|
+
let backgroundWork = true;
|
|
145
|
+
const runs: RunCall[] = [];
|
|
146
|
+
const runtime = createRuntime(runs, 'ok', [], (sessionId) => sessionId === SESSION_ID && backgroundWork);
|
|
147
|
+
|
|
148
|
+
// Starting B now would replace that process and stop the command.
|
|
149
|
+
assert.equal(await dispatchQueuedMessages(runtime), 0);
|
|
150
|
+
assert.equal(runs.length, 0);
|
|
151
|
+
assert.deepEqual(sessionDraftsDb.getDrafts(userId)[0]?.queuedMessage, {
|
|
152
|
+
content: 'send after the background job',
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Once the work has reported back, B goes on the next pass.
|
|
156
|
+
backgroundWork = false;
|
|
157
|
+
assert.equal(await dispatchQueuedMessages(runtime), 1);
|
|
158
|
+
assert.equal(runs.length, 1);
|
|
159
|
+
assert.equal(runs[0].command, 'send after the background job');
|
|
160
|
+
assert.equal(sessionDraftsDb.getDrafts(userId).length, 0);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
120
164
|
test('a due message interrupts a run in progress instead of failing', async () => {
|
|
121
165
|
await withIsolatedDatabase(async (userId) => {
|
|
122
166
|
scheduledMessagesService.schedule({
|
|
@@ -33,7 +33,7 @@ Benefits:
|
|
|
33
33
|
|---|---|
|
|
34
34
|
| `services/websocket-server.service.ts` | Creates `WebSocketServer`, binds `verifyClient`, routes connection by pathname |
|
|
35
35
|
| `services/websocket-auth.service.ts` | Authenticates upgrade requests and attaches `request.user` |
|
|
36
|
-
| `services/chat-websocket.service.ts` | Handles the `/ws` chat protocol (`chat.send` / `chat.abort` / `chat.subscribe` / `chat.permission-response`) |
|
|
36
|
+
| `services/chat-websocket.service.ts` | Handles the `/ws` chat protocol (`chat.send` / `chat.abort` / `chat.stop-task` / `chat.subscribe` / `chat.permission-response`) |
|
|
37
37
|
| `services/chat-run-registry.service.ts` | Tracks live provider runs per app session id: seq numbering, event replay buffer, provider-id mapping, completion state |
|
|
38
38
|
| `services/chat-session-writer.service.ts` | Gateway writer handed to provider runtimes: remaps provider session ids to app ids, swallows `session_created`, assigns `seq` |
|
|
39
39
|
| `services/shell-websocket.service.ts` | Handles `/shell` PTY lifecycle, reconnect buffering, auth URL detection |
|
|
@@ -108,7 +108,7 @@ When a chat socket connects:
|
|
|
108
108
|
|
|
109
109
|
1. Add socket to `connectedClients`.
|
|
110
110
|
2. Parse each incoming message with `parseIncomingJsonObject`.
|
|
111
|
-
3. Dispatch by `data.type` (
|
|
111
|
+
3. Dispatch by `data.type` (six message types, none provider-specific).
|
|
112
112
|
4. On close, remove socket from `connectedClients`.
|
|
113
113
|
|
|
114
114
|
### Session identity model
|
|
@@ -130,7 +130,9 @@ flowchart TD
|
|
|
130
130
|
B -->|ok| D{data.type}
|
|
131
131
|
|
|
132
132
|
D -->|chat.send| E[resolve session row -> startRun -> providerRuntimeService.run]
|
|
133
|
+
D -->|chat.edit-send| E2[history_truncated -> rewind or resume at the anchor -> same as chat.send]
|
|
133
134
|
D -->|chat.abort| F[providerRuntimeService.abort + synthetic complete]
|
|
135
|
+
D -->|chat.stop-task| F2[providerRuntimeService.stopBackgroundTask]
|
|
134
136
|
D -->|chat.subscribe| G[chat_subscribed ack + attach socket + replay events seq > lastSeq]
|
|
135
137
|
D -->|chat.permission-response| H[providerRuntimeService.resolveToolApproval]
|
|
136
138
|
D -->|other| I[send kind:protocol_error]
|
|
@@ -59,12 +59,31 @@ const MAX_BUFFERED_EVENTS_PER_RUN = 5000;
|
|
|
59
59
|
*/
|
|
60
60
|
const runs = new Map<string, ChatRun>();
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Answers whether a completed run must stay registered a while longer. Set by
|
|
64
|
+
* the composition root to the provider runtimes' background-work check: a
|
|
65
|
+
* session whose turn ended but whose agents, workflows or commands are still
|
|
66
|
+
* running keeps sending live events through this run's writer, and a tab that
|
|
67
|
+
* subscribes meanwhile needs the run to attach to.
|
|
68
|
+
*/
|
|
69
|
+
let retainCompletedRun: (appSessionId: string) => boolean = () => false;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Schedules one run's eviction. The timer is bound to the run it was armed
|
|
73
|
+
* for: a later run can take the session's slot while this one's retention —
|
|
74
|
+
* re-armed for as long as the guard holds — is still pending, and firing on
|
|
75
|
+
* the slot alone would evict that newer run early.
|
|
76
|
+
*/
|
|
77
|
+
function evictRunLater(run: ChatRun): void {
|
|
63
78
|
const timer = setTimeout(() => {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
79
|
+
if (runs.get(run.appSessionId) !== run || run.status !== 'completed') {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (retainCompletedRun(run.appSessionId)) {
|
|
83
|
+
evictRunLater(run);
|
|
84
|
+
return;
|
|
67
85
|
}
|
|
86
|
+
runs.delete(run.appSessionId);
|
|
68
87
|
}, COMPLETED_RUN_RETENTION_MS);
|
|
69
88
|
|
|
70
89
|
// Never keep the process alive just to evict a buffered run.
|
|
@@ -104,7 +123,7 @@ function decorateAndRecordEvent(run: ChatRun, message: NormalizedMessage): Norma
|
|
|
104
123
|
outbound.actualSessionId = run.appSessionId;
|
|
105
124
|
run.status = 'completed';
|
|
106
125
|
run.completedAt = Date.now();
|
|
107
|
-
evictRunLater(run
|
|
126
|
+
evictRunLater(run);
|
|
108
127
|
}
|
|
109
128
|
|
|
110
129
|
run.events.push(outbound);
|
|
@@ -160,6 +179,11 @@ function recordProviderSessionId(run: ChatRun, providerSessionId: string): void
|
|
|
160
179
|
* regardless of which provider runtime produced them.
|
|
161
180
|
*/
|
|
162
181
|
export const chatRunRegistry = {
|
|
182
|
+
/** Installs the check that keeps a completed run registered while its session still has background work. */
|
|
183
|
+
setRetentionGuard(guard: (appSessionId: string) => boolean): void {
|
|
184
|
+
retainCompletedRun = guard;
|
|
185
|
+
},
|
|
186
|
+
|
|
163
187
|
/**
|
|
164
188
|
* Starts tracking a run and returns it, or `null` when a run is already in
|
|
165
189
|
* progress for the session (callers must reject the duplicate send).
|
|
@@ -73,6 +73,9 @@ export type ProviderRuntimeGateway = {
|
|
|
73
73
|
writer: ProviderRuntimeWriter,
|
|
74
74
|
): Promise<unknown>;
|
|
75
75
|
abort(provider: LLMProvider, sessionId: string): Promise<boolean>;
|
|
76
|
+
stopBackgroundTask(provider: LLMProvider, sessionId: string, taskId: string): Promise<boolean>;
|
|
77
|
+
/** Whether a provider runtime still holds background work for the session after its turn ended. */
|
|
78
|
+
hasBackgroundWork(sessionId: string): boolean;
|
|
76
79
|
resolveToolApproval(requestId: string, payload: ProviderPermissionDecision): void;
|
|
77
80
|
getPendingApprovalsForSession(sessionId: string): unknown[];
|
|
78
81
|
};
|
|
@@ -437,6 +440,52 @@ async function handleChatAbort(
|
|
|
437
440
|
});
|
|
438
441
|
}
|
|
439
442
|
|
|
443
|
+
/**
|
|
444
|
+
* Handles `chat.stop-task`: stops one background task of a session — an
|
|
445
|
+
* agent, a workflow or a backgrounded command that is still going after its
|
|
446
|
+
* turn ended. Unlike `chat.abort` there is no run to consult: the task lives
|
|
447
|
+
* in the provider's held process, which is the only thing that can stop it.
|
|
448
|
+
* The provider then reports the task as stopped on the session's stream, so
|
|
449
|
+
* nothing is echoed back here on success.
|
|
450
|
+
*/
|
|
451
|
+
async function handleChatStopTask(
|
|
452
|
+
ws: WebSocket,
|
|
453
|
+
data: AnyRecord,
|
|
454
|
+
dependencies: ChatWebSocketDependencies
|
|
455
|
+
): Promise<void> {
|
|
456
|
+
const sessionId = readRequiredSessionId(data);
|
|
457
|
+
if (!sessionId) {
|
|
458
|
+
sendProtocolError(ws, 'SESSION_ID_REQUIRED', 'chat.stop-task requires a sessionId.');
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const taskId = typeof data.taskId === 'string' ? data.taskId.trim() : '';
|
|
463
|
+
if (!taskId) {
|
|
464
|
+
sendProtocolError(ws, 'TASK_ID_REQUIRED', 'chat.stop-task requires a taskId.', sessionId);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const session = sessionsDb.getSessionById(sessionId);
|
|
469
|
+
if (!session) {
|
|
470
|
+
sendProtocolError(ws, 'SESSION_NOT_FOUND', `Session "${sessionId}" was not found.`, sessionId);
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const stopped = await dependencies.runtime.stopBackgroundTask(
|
|
475
|
+
session.provider as LLMProvider,
|
|
476
|
+
sessionId,
|
|
477
|
+
taskId,
|
|
478
|
+
);
|
|
479
|
+
if (!stopped) {
|
|
480
|
+
sendProtocolError(
|
|
481
|
+
ws,
|
|
482
|
+
'NO_SUCH_TASK',
|
|
483
|
+
`Session "${sessionId}" has no running background task "${taskId}".`,
|
|
484
|
+
sessionId,
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
440
489
|
/**
|
|
441
490
|
* Handles `chat.subscribe`: for each requested session, reports whether a run
|
|
442
491
|
* is processing, re-attaches the live stream to this socket, replays missed
|
|
@@ -474,7 +523,11 @@ function handleChatSubscribe(
|
|
|
474
523
|
|
|
475
524
|
// Future live events for this run should land on the socket that asked —
|
|
476
525
|
// this is what makes mid-stream page refreshes work for all providers.
|
|
477
|
-
|
|
526
|
+
// A session whose turn ended but whose background work is still going
|
|
527
|
+
// keeps producing events through the same writer (task progress, the
|
|
528
|
+
// turn the CLI pushes when a task reports), so a tab opened during that
|
|
529
|
+
// work attaches too; the registry keeps the run while the work lasts.
|
|
530
|
+
if (isProcessing || dependencies.runtime.hasBackgroundWork(sessionId)) {
|
|
478
531
|
chatRunRegistry.attachConnection(sessionId, ws);
|
|
479
532
|
}
|
|
480
533
|
|
|
@@ -527,6 +580,7 @@ function handlePermissionResponse(data: AnyRecord, dependencies: ChatWebSocketDe
|
|
|
527
580
|
* Inbound protocol (client to server):
|
|
528
581
|
* - `chat.send` { sessionId, content, options? }
|
|
529
582
|
* - `chat.abort` { sessionId }
|
|
583
|
+
* - `chat.stop-task` { sessionId, taskId }
|
|
530
584
|
* - `chat.subscribe` { sessions: [{ sessionId, lastSeq? }] }
|
|
531
585
|
* - `chat.permission-response` { requestId, allow, updatedInput?, message?, rememberEntry? }
|
|
532
586
|
*
|
|
@@ -630,6 +684,9 @@ export function handleChatConnection(
|
|
|
630
684
|
case 'chat.abort':
|
|
631
685
|
await handleChatAbort(ws, data, dependencies);
|
|
632
686
|
return;
|
|
687
|
+
case 'chat.stop-task':
|
|
688
|
+
await handleChatStopTask(ws, data, dependencies);
|
|
689
|
+
return;
|
|
633
690
|
case 'chat.subscribe':
|
|
634
691
|
handleChatSubscribe(ws, data, dependencies);
|
|
635
692
|
return;
|