@cloudcli-ai/cloudcli 1.37.2 → 1.37.3
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-CJntskwA.js} +1 -1
- package/dist/assets/{arc-B8jF-ja9.js → arc-CIfH4uMN.js} +1 -1
- package/dist/assets/{architectureDiagram-T3A2C74G-C_Wur_CU.js → architectureDiagram-T3A2C74G-_IpEkukg.js} +1 -1
- package/dist/assets/{blockDiagram-VBNYF7ZC-Cq4P1i8t.js → blockDiagram-VBNYF7ZC-BttxrpgZ.js} +1 -1
- package/dist/assets/{c4Diagram-5PPSVZJV-C1wG4eUj.js → c4Diagram-5PPSVZJV-g51dSRr1.js} +1 -1
- package/dist/assets/channel-C5xU9jMw.js +1 -0
- package/dist/assets/{chunk-2GRJ4B5K-B8fnBG6E.js → chunk-2GRJ4B5K-D_-LbQUa.js} +1 -1
- package/dist/assets/{chunk-2Q5K7J3B-WdfPooPJ.js → chunk-2Q5K7J3B-BtlHzwaO.js} +1 -1
- package/dist/assets/{chunk-5RXB4S5H-DhZJuG2E.js → chunk-5RXB4S5H-UzkpvpAT.js} +1 -1
- package/dist/assets/{chunk-5VM5RSS4-B0rjslal.js → chunk-5VM5RSS4-BKwx5Gag.js} +1 -1
- package/dist/assets/{chunk-6Q2QTUOP-CG2Oj4jL.js → chunk-6Q2QTUOP-D_KEBH5f.js} +1 -1
- package/dist/assets/{chunk-GF5L2VYU-rrl2lRxY.js → chunk-GF5L2VYU-rK2DGyto.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-CjVbrVMA.js → chunk-JWPE2WC7-CeBl_N9i.js} +1 -1
- package/dist/assets/{chunk-KBJHAD2P-CRzKgU8S.js → chunk-KBJHAD2P-BiQLWVIr.js} +1 -1
- package/dist/assets/{chunk-RYQCIY6F-DBz4GTZF.js → chunk-RYQCIY6F-C1knVGV5.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-wHxalsXk.js → chunk-XXDRQBXY-CtKbJkvF.js} +1 -1
- package/dist/assets/classDiagram-JCYQIIEL-CRrL3vp7.js +1 -0
- package/dist/assets/classDiagram-v2-OCEON4UE-CRrL3vp7.js +1 -0
- package/dist/assets/{cose-bilkent-JH36ORCC-s1hygY8e.js → cose-bilkent-JH36ORCC-DjX8y40Z.js} +1 -1
- package/dist/assets/{cynefin-VYW2F7L2-C4LGV15i.js → cynefin-VYW2F7L2-DM6YeRba.js} +1 -1
- package/dist/assets/{cynefinDiagram-MW4NZA55-C5uhu7B3.js → cynefinDiagram-MW4NZA55-wZJ-vy5B.js} +1 -1
- package/dist/assets/{dagre-VZM6K2ZE-B-OBTewm.js → dagre-VZM6K2ZE-BzP0ejkH.js} +1 -1
- package/dist/assets/{diagram-7IWD3JNH-BjGKQxih.js → diagram-7IWD3JNH-BTL_j23J.js} +1 -1
- package/dist/assets/{diagram-B4RE2ZJO-BMKz1X2L.js → diagram-B4RE2ZJO-Cf1Norgu.js} +1 -1
- package/dist/assets/{diagram-LBJQPF4R-DNh-GhKC.js → diagram-LBJQPF4R-DPiUEKr-.js} +1 -1
- package/dist/assets/{diagram-Q27KOJAE-BaTG9jPT.js → diagram-Q27KOJAE-CTcTpiz5.js} +1 -1
- package/dist/assets/{diagram-UB23O5K3-BUwo2OvJ.js → diagram-UB23O5K3-DPMSdYm0.js} +1 -1
- package/dist/assets/{ebnfDiagram-BXEA7PRR-DAZgfzX2.js → ebnfDiagram-BXEA7PRR-7yvD6uiF.js} +1 -1
- package/dist/assets/{erDiagram-JOGREHBK-1w1CcGEb.js → erDiagram-JOGREHBK-CfgeDoV6.js} +1 -1
- package/dist/assets/{flowDiagram-UKHOOZJN-D4EII8Jj.js → flowDiagram-UKHOOZJN-B27ng5zu.js} +1 -1
- package/dist/assets/{ganttDiagram-PKOTCBZU-ChvJZAgB.js → ganttDiagram-PKOTCBZU-CcC6b_7t.js} +1 -1
- package/dist/assets/{gitGraphDiagram-DS77QQ5N-DtOn41S8.js → gitGraphDiagram-DS77QQ5N-BX5kPGrO.js} +1 -1
- package/dist/assets/index-BTDHQ6Id.css +32 -0
- package/dist/assets/index-sFfz8iBR.js +4931 -0
- package/dist/assets/{infoDiagram-6WML65LV-DN_PVEJZ.js → infoDiagram-6WML65LV-CUP-SlN4.js} +1 -1
- package/dist/assets/{ishikawaDiagram-WSZJBQD7-DbkF1kXb.js → ishikawaDiagram-WSZJBQD7-BTTDhOpX.js} +1 -1
- package/dist/assets/{journeyDiagram-NVQOT4AX-pJwmhddE.js → journeyDiagram-NVQOT4AX-ARXcc61m.js} +1 -1
- package/dist/assets/{kanban-definition-27J2QSJJ-PPAPgNp5.js → kanban-definition-27J2QSJJ-BieMpqMA.js} +1 -1
- package/dist/assets/{linear-iPR-J9f7.js → linear-61vxjRnP.js} +1 -1
- package/dist/assets/{mermaid.core-BEUL86Z6.js → mermaid.core-nhnwkfQx.js} +5 -5
- package/dist/assets/{mindmap-definition-FAOFIHXS-BvZoBib7.js → mindmap-definition-FAOFIHXS-Cm6x5vT1.js} +1 -1
- package/dist/assets/{pegDiagram-VL7TDLO6-DhYreaUd.js → pegDiagram-VL7TDLO6-CcYBq_9H.js} +1 -1
- package/dist/assets/{pieDiagram-7S7Q4E2Y-CrOHlyD0.js → pieDiagram-7S7Q4E2Y-CfvIjfzo.js} +1 -1
- package/dist/assets/{quadrantDiagram-CIZ2JOQS-D4g5yH3t.js → quadrantDiagram-CIZ2JOQS-SgYoWPNb.js} +1 -1
- package/dist/assets/{railroadDiagram-AXF67PYL-Dj39lHlf.js → railroadDiagram-AXF67PYL-D2Mu5E_f.js} +1 -1
- package/dist/assets/{requirementDiagram-LRYGKXZP-1AS4Zgm_.js → requirementDiagram-LRYGKXZP-Azwe_k4Q.js} +1 -1
- package/dist/assets/{sankeyDiagram-W5VNT64P-CKlMn19P.js → sankeyDiagram-W5VNT64P-DN1Ju3PA.js} +1 -1
- package/dist/assets/{sequenceDiagram-SI44F4Z6-B_j3qyvo.js → sequenceDiagram-SI44F4Z6-Bj4qC34S.js} +1 -1
- package/dist/assets/server.browser-CtX3Hdl5.js +27 -0
- package/dist/assets/{sizeCapture-X5ZJPWSS-Bp2Op16v.js → sizeCapture-X5ZJPWSS-Co4PdklG.js} +1 -1
- package/dist/assets/{stateDiagram-OKZ733FA-C0cN6DoY.js → stateDiagram-OKZ733FA-nOvL9dRC.js} +1 -1
- package/dist/assets/stateDiagram-v2-UEYNNEHI-DNv8ppw7.js +1 -0
- package/dist/assets/{swimlanes-SLNWSIFB-BEvXOV9b.js → swimlanes-SLNWSIFB-CCK3wMid.js} +2 -2
- package/dist/assets/swimlanesDiagram-ULZ7WXOC-DnfbNQEp.js +8 -0
- package/dist/assets/{timeline-definition-Z64GVDOM-zdJQinvZ.js → timeline-definition-Z64GVDOM-BC29RNFm.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-pZlZCtEo.js} +1 -1
- package/dist/assets/{wardleyDiagram-T6FBY63Y-B9AlLQup.js → wardleyDiagram-T6FBY63Y-De8k2-zM.js} +1 -1
- package/dist/assets/{xychartDiagram-ELKLHX3M-DTJX48Qg.js → xychartDiagram-ELKLHX3M-Bk8ykH0w.js} +1 -1
- package/dist/index.html +3 -3
- package/dist-server/server/index.js +7 -3
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/modules/agent/agent.routes.js +2 -2
- package/dist-server/server/modules/agent/agent.routes.js.map +1 -1
- package/dist-server/server/modules/agent/tests/agent.routes.test.js +30 -1
- 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 +32 -1
- package/dist-server/server/modules/database/migrations.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 +137 -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 +92 -0
- package/dist-server/server/modules/database/schema.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/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/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 +20 -1
- 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 +35 -6
- 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 +168 -38
- package/dist-server/server/modules/providers/list/claude/claude-runtime.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js +388 -50
- 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 +16 -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 +60 -49
- 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 +1565 -432
- 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-runtime.provider.js +8 -6
- 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 +258 -6
- package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js.map +1 -1
- package/dist-server/server/modules/providers/provider.routes.js +24 -0
- package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
- package/dist-server/server/modules/providers/services/provider-capabilities.service.js +14 -0
- package/dist-server/server/modules/providers/services/provider-capabilities.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-history-cache.service.js +87 -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 +179 -9
- package/dist-server/server/modules/providers/services/sessions.service.js.map +1 -1
- package/dist-server/server/modules/providers/tests/claude-auth.test.js +52 -0
- package/dist-server/server/modules/providers/tests/claude-auth.test.js.map +1 -1
- 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-sessions.test.js +465 -39
- package/dist-server/server/modules/providers/tests/claude-sessions.test.js.map +1 -1
- 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-runtime.test.js +51 -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 +304 -15
- package/dist-server/server/modules/providers/tests/codex-sessions.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/opencode-models.test.js +117 -4
- package/dist-server/server/modules/providers/tests/opencode-models.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/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 +120 -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 +45 -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 +163 -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 +255 -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 +10 -44
- 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 +166 -9
- 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 +9 -3
- package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -1
- 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/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 +80 -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/utils.js +34 -0
- package/dist-server/server/shared/utils.js.map +1 -1
- package/package.json +19 -16
- package/server/index.ts +11 -4
- package/server/modules/agent/agent.routes.ts +2 -2
- package/server/modules/agent/tests/agent.routes.test.ts +33 -1
- 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 +37 -0
- 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 +196 -3
- package/server/modules/database/repositories/user-preferences.db.ts +80 -0
- package/server/modules/database/schema.ts +96 -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/projects/services/project-delete.service.ts +13 -1
- package/server/modules/projects/services/project-star.service.ts +2 -2
- package/server/modules/providers/index.ts +4 -0
- package/server/modules/providers/list/claude/claude-auth.provider.ts +21 -1
- package/server/modules/providers/list/claude/claude-fork.provider.ts +57 -0
- package/server/modules/providers/list/claude/claude-models.provider.ts +38 -6
- package/server/modules/providers/list/claude/claude-runtime.provider.js +188 -39
- package/server/modules/providers/list/claude/claude-sessions.provider.ts +478 -54
- 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 +16 -0
- package/server/modules/providers/list/codex/{codex-runtime.provider.js → codex-runtime.provider.ts} +93 -66
- package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +9 -0
- package/server/modules/providers/list/codex/codex-sessions.provider.ts +1806 -477
- package/server/modules/providers/list/codex/codex.provider.ts +3 -0
- package/server/modules/providers/list/cursor/cursor-runtime.provider.js +8 -6
- package/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.ts +0 -1
- package/server/modules/providers/list/opencode/opencode-models.provider.ts +279 -6
- package/server/modules/providers/provider.routes.ts +31 -0
- package/server/modules/providers/services/provider-capabilities.service.ts +23 -0
- package/server/modules/providers/services/provider-token-usage.service.ts +166 -39
- package/server/modules/providers/services/session-history-cache.service.ts +139 -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 +201 -9
- package/server/modules/providers/tests/claude-auth.test.ts +58 -0
- package/server/modules/providers/tests/claude-models.test.ts +87 -0
- package/server/modules/providers/tests/claude-sessions.test.ts +552 -40
- 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-runtime.test.ts +58 -0
- package/server/modules/providers/tests/codex-session-fork.test.ts +59 -0
- package/server/modules/providers/tests/codex-sessions.test.ts +337 -15
- package/server/modules/providers/tests/opencode-models.test.ts +166 -8
- package/server/modules/providers/tests/provider-token-usage.service.test.ts +188 -1
- package/server/modules/providers/tests/session-fork.test.ts +173 -0
- package/server/modules/providers/tests/session-history-cache.service.test.ts +143 -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 +67 -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 +203 -0
- package/server/modules/scheduled-messages/services/scheduled-messages.service.ts +121 -0
- package/server/modules/scheduled-messages/tests/scheduled-messages.test.ts +310 -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/index.ts +8 -0
- package/server/modules/websocket/services/chat-run-registry.service.ts +17 -50
- package/server/modules/websocket/services/chat-session-writer.service.ts +36 -6
- package/server/modules/websocket/services/chat-websocket.service.ts +244 -15
- package/server/modules/websocket/services/session-upsert-broadcast.service.ts +108 -0
- package/server/modules/websocket/services/shell-websocket.service.ts +10 -3
- 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/session-upsert-broadcast.test.ts +138 -0
- package/server/modules/websocket/tests/shell-websocket.service.test.ts +108 -0
- package/server/shared/claude-cli-path.ts +24 -6
- package/server/shared/index.ts +3 -0
- package/server/shared/interfaces.ts +62 -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/types.ts +123 -2
- package/server/shared/utils.ts +40 -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
|
@@ -5,6 +5,14 @@ type GitConfig = {
|
|
|
5
5
|
git_email: string | null;
|
|
6
6
|
};
|
|
7
7
|
|
|
8
|
+
/** One chat scope's unsent composer text and queued message. */
|
|
9
|
+
type DraftRecord = {
|
|
10
|
+
scope: string;
|
|
11
|
+
text: string;
|
|
12
|
+
queuedMessage: unknown | null;
|
|
13
|
+
updatedAt: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
8
16
|
type UserDependencies = {
|
|
9
17
|
users: {
|
|
10
18
|
getGitConfig(userId: number): GitConfig | undefined;
|
|
@@ -12,12 +20,65 @@ type UserDependencies = {
|
|
|
12
20
|
completeOnboarding(userId: number): void;
|
|
13
21
|
hasCompletedOnboarding(userId: number): boolean;
|
|
14
22
|
};
|
|
23
|
+
preferences: {
|
|
24
|
+
getPreferences(userId: number): Record<string, unknown>;
|
|
25
|
+
savePreferences(userId: number, updates: Record<string, unknown>): void;
|
|
26
|
+
};
|
|
27
|
+
drafts: {
|
|
28
|
+
getDrafts(userId: number): DraftRecord[];
|
|
29
|
+
saveDraft(userId: number, scope: string, draft: { text: string; queuedMessage: unknown | null }): void;
|
|
30
|
+
deleteDraft(userId: number, scope: string): void;
|
|
31
|
+
};
|
|
15
32
|
readSystemGitConfig(): Promise<GitConfig>;
|
|
16
33
|
applyGlobalGitConfig(gitName: string, gitEmail: string): Promise<void>;
|
|
17
34
|
logInfo(message: string): void;
|
|
18
35
|
logError(message: string, error: unknown): void;
|
|
19
36
|
};
|
|
20
37
|
|
|
38
|
+
/**
|
|
39
|
+
* The longest a draft scope or preference key may be.
|
|
40
|
+
*
|
|
41
|
+
* Scopes are session UUIDs or `project:<id>`, and preference keys are fixed
|
|
42
|
+
* identifiers, so anything longer is a client bug or an attempt to use the
|
|
43
|
+
* table as general storage.
|
|
44
|
+
*/
|
|
45
|
+
const MAX_KEY_LENGTH = 200;
|
|
46
|
+
|
|
47
|
+
/** Guards against a runaway composer filling the database with one row. */
|
|
48
|
+
const MAX_DRAFT_TEXT_LENGTH = 100_000;
|
|
49
|
+
|
|
50
|
+
const readDraftScope = (value: unknown): string => {
|
|
51
|
+
const scope = typeof value === 'string' ? value.trim() : '';
|
|
52
|
+
if (!scope || scope.length > MAX_KEY_LENGTH) {
|
|
53
|
+
throw new AppError('A draft scope of 1-200 characters is required', {
|
|
54
|
+
code: 'INVALID_DRAFT_SCOPE',
|
|
55
|
+
statusCode: 400,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return scope;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const readPreferenceUpdates = (body: unknown): Record<string, unknown> => {
|
|
62
|
+
if (!body || typeof body !== 'object' || Array.isArray(body)) {
|
|
63
|
+
throw new AppError('Preferences must be sent as an object', {
|
|
64
|
+
code: 'INVALID_PREFERENCES',
|
|
65
|
+
statusCode: 400,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const updates = body as Record<string, unknown>;
|
|
70
|
+
for (const key of Object.keys(updates)) {
|
|
71
|
+
if (!key || key.length > MAX_KEY_LENGTH) {
|
|
72
|
+
throw new AppError('Preference keys must be 1-200 characters', {
|
|
73
|
+
code: 'INVALID_PREFERENCE_KEY',
|
|
74
|
+
statusCode: 400,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return updates;
|
|
80
|
+
};
|
|
81
|
+
|
|
21
82
|
/** Creates user-profile workflows with explicit repository and Git adapters. */
|
|
22
83
|
export function createUserService(dependencies: UserDependencies) {
|
|
23
84
|
return {
|
|
@@ -81,5 +142,49 @@ export function createUserService(dependencies: UserDependencies) {
|
|
|
81
142
|
hasCompletedOnboarding: dependencies.users.hasCompletedOnboarding(userId),
|
|
82
143
|
};
|
|
83
144
|
},
|
|
145
|
+
|
|
146
|
+
/** Every stored preference at once; the client fills gaps with its defaults. */
|
|
147
|
+
getPreferences(userId: number) {
|
|
148
|
+
return { success: true, preferences: dependencies.preferences.getPreferences(userId) };
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Merge-patch: only the keys in the request body are touched, so two
|
|
153
|
+
* independent settings screens can save concurrently without one erasing
|
|
154
|
+
* the other's work.
|
|
155
|
+
*/
|
|
156
|
+
savePreferences(userId: number, body: unknown) {
|
|
157
|
+
const updates = readPreferenceUpdates(body);
|
|
158
|
+
dependencies.preferences.savePreferences(userId, updates);
|
|
159
|
+
return { success: true, preferences: dependencies.preferences.getPreferences(userId) };
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
getDrafts(userId: number) {
|
|
163
|
+
return { success: true, drafts: dependencies.drafts.getDrafts(userId) };
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
saveDraft(userId: number, scopeInput: unknown, body: unknown) {
|
|
167
|
+
const scope = readDraftScope(scopeInput);
|
|
168
|
+
const payload = (body ?? {}) as { text?: unknown; queuedMessage?: unknown };
|
|
169
|
+
const text = typeof payload.text === 'string' ? payload.text : '';
|
|
170
|
+
|
|
171
|
+
if (text.length > MAX_DRAFT_TEXT_LENGTH) {
|
|
172
|
+
throw new AppError('Draft text is too long to store', {
|
|
173
|
+
code: 'DRAFT_TEXT_TOO_LONG',
|
|
174
|
+
statusCode: 413,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
dependencies.drafts.saveDraft(userId, scope, {
|
|
179
|
+
text,
|
|
180
|
+
queuedMessage: payload.queuedMessage ?? null,
|
|
181
|
+
});
|
|
182
|
+
return { success: true };
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
deleteDraft(userId: number, scopeInput: unknown) {
|
|
186
|
+
dependencies.drafts.deleteDraft(userId, readDraftScope(scopeInput));
|
|
187
|
+
return { success: true };
|
|
188
|
+
},
|
|
84
189
|
};
|
|
85
190
|
}
|
|
@@ -47,8 +47,9 @@ test('transcribes with injected fetch and request-level credential/model overrid
|
|
|
47
47
|
|
|
48
48
|
assert.deepEqual(result, { ok: true, value: { text: 'hello' } });
|
|
49
49
|
assert.equal(requestedUrl, 'https://voice.example/v1/audio/transcriptions');
|
|
50
|
-
assert.
|
|
51
|
-
assert.equal((requestedOptions
|
|
50
|
+
assert.ok(requestedOptions, 'fetchBackend should receive request options');
|
|
51
|
+
assert.equal((requestedOptions.headers as Record<string, string>).Authorization, 'Bearer request-key');
|
|
52
|
+
assert.equal((requestedOptions.body as FormData).get('model'), 'custom-whisper');
|
|
52
53
|
});
|
|
53
54
|
|
|
54
55
|
test('forwards the explicit TTS format and maps backend authentication failures', async () => {
|
|
@@ -1,3 +1,11 @@
|
|
|
1
1
|
export { WS_OPEN_STATE, connectedClients } from './services/websocket-state.service.js';
|
|
2
2
|
export { createWebSocketServer } from './services/websocket-server.service.js';
|
|
3
3
|
export { chatRunRegistry } from './services/chat-run-registry.service.js';
|
|
4
|
+
// Consumed by the providers module's sessions watcher, which announces the
|
|
5
|
+
// sessions it (re)indexed from disk through the same builder the chat gateway
|
|
6
|
+
// uses, so both paths put the identical delta on the wire.
|
|
7
|
+
export { broadcastSessionUpserted, broadcastSessionUpsertedBatch } from './services/session-upsert-broadcast.service.js';
|
|
8
|
+
// runDetachedChatTurn: used by the scheduled-messages module to run a turn
|
|
9
|
+
// from a timer, with no socket to stream to or report errors on.
|
|
10
|
+
export { runDetachedChatTurn } from './services/chat-websocket.service.js';
|
|
11
|
+
export type { ProviderRuntimeGateway } from './services/chat-websocket.service.js';
|
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
import { projectsDb, sessionsDb } from '@/modules/database/index.js';
|
|
4
|
-
import { generateDisplayName } from '@/modules/projects/index.js';
|
|
1
|
+
import { sessionsDb } from '@/modules/database/index.js';
|
|
5
2
|
import { ChatSessionWriter } from '@/modules/websocket/services/chat-session-writer.service.js';
|
|
6
|
-
import {
|
|
3
|
+
import { broadcastSessionUpserted } from '@/modules/websocket/services/session-upsert-broadcast.service.js';
|
|
7
4
|
import type {
|
|
8
5
|
LLMProvider,
|
|
9
6
|
NormalizedMessage,
|
|
@@ -62,48 +59,6 @@ const MAX_BUFFERED_EVENTS_PER_RUN = 5000;
|
|
|
62
59
|
*/
|
|
63
60
|
const runs = new Map<string, ChatRun>();
|
|
64
61
|
|
|
65
|
-
async function broadcastCanonicalSessionUpsert(appSessionId: string): Promise<void> {
|
|
66
|
-
const row = sessionsDb.getSessionById(appSessionId);
|
|
67
|
-
if (!row || row.isArchived) {
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const projectPath = row.project_path;
|
|
72
|
-
const project = projectPath ? projectsDb.getProjectPath(projectPath) : null;
|
|
73
|
-
const displayName = project?.custom_project_name?.trim()
|
|
74
|
-
? project.custom_project_name
|
|
75
|
-
: await generateDisplayName(path.basename(projectPath ?? '') || (projectPath ?? ''), projectPath);
|
|
76
|
-
|
|
77
|
-
const payload = JSON.stringify({
|
|
78
|
-
kind: 'session_upserted',
|
|
79
|
-
sessionId: row.session_id,
|
|
80
|
-
providerSessionId: row.provider_session_id,
|
|
81
|
-
provider: row.provider,
|
|
82
|
-
session: {
|
|
83
|
-
id: row.session_id,
|
|
84
|
-
summary: row.custom_name || '',
|
|
85
|
-
messageCount: 0,
|
|
86
|
-
lastActivity: row.updated_at ?? row.created_at ?? new Date().toISOString(),
|
|
87
|
-
},
|
|
88
|
-
project: project
|
|
89
|
-
? {
|
|
90
|
-
projectId: project.project_id,
|
|
91
|
-
path: project.project_path,
|
|
92
|
-
fullPath: project.project_path,
|
|
93
|
-
displayName,
|
|
94
|
-
isStarred: Boolean(project.isStarred),
|
|
95
|
-
}
|
|
96
|
-
: null,
|
|
97
|
-
timestamp: new Date().toISOString(),
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
connectedClients.forEach((client) => {
|
|
101
|
-
if (client.readyState === WS_OPEN_STATE) {
|
|
102
|
-
client.send(payload);
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
|
|
107
62
|
function evictRunLater(appSessionId: string): void {
|
|
108
63
|
const timer = setTimeout(() => {
|
|
109
64
|
const run = runs.get(appSessionId);
|
|
@@ -178,7 +133,7 @@ function recordProviderSessionId(run: ChatRun, providerSessionId: string): void
|
|
|
178
133
|
|
|
179
134
|
try {
|
|
180
135
|
sessionsDb.assignProviderSessionId(run.appSessionId, providerSessionId);
|
|
181
|
-
void
|
|
136
|
+
void broadcastSessionUpserted(run.appSessionId).catch((error) => {
|
|
182
137
|
const message = error instanceof Error ? error.message : String(error);
|
|
183
138
|
console.error('[ChatRunRegistry] Failed to broadcast canonical session mapping', {
|
|
184
139
|
appSessionId: run.appSessionId,
|
|
@@ -213,7 +168,13 @@ export const chatRunRegistry = {
|
|
|
213
168
|
appSessionId: string;
|
|
214
169
|
provider: LLMProvider;
|
|
215
170
|
providerSessionId: string | null;
|
|
216
|
-
|
|
171
|
+
/**
|
|
172
|
+
* The socket that asked for this run, or `null` for one nobody is watching
|
|
173
|
+
* — a scheduled message fires with no browser attached. The writer's event
|
|
174
|
+
* buffer still records everything, so a client that subscribes later
|
|
175
|
+
* replays the run from its start.
|
|
176
|
+
*/
|
|
177
|
+
connection: RealtimeClientConnection | null;
|
|
217
178
|
userId: string | number | null;
|
|
218
179
|
}): ChatRun | null {
|
|
219
180
|
const existing = runs.get(input.appSessionId);
|
|
@@ -273,11 +234,17 @@ export const chatRunRegistry = {
|
|
|
273
234
|
},
|
|
274
235
|
|
|
275
236
|
/**
|
|
276
|
-
*
|
|
237
|
+
* Adds a websocket connection to a run's live audience.
|
|
277
238
|
*
|
|
278
239
|
* This is the generic replacement for the Claude-only writer reconnect:
|
|
279
240
|
* after a page refresh the new socket subscribes and immediately starts
|
|
280
241
|
* receiving the still-running stream, for every provider.
|
|
242
|
+
*
|
|
243
|
+
* Subscribing does not take the stream away from sockets that were already
|
|
244
|
+
* watching — a session open in two places stays live in both, and the
|
|
245
|
+
* refreshed tab's abandoned socket is dropped when the next event finds it
|
|
246
|
+
* closed. Replay stays per-connection because each client sends its own
|
|
247
|
+
* `lastSeq` with `chat.subscribe`.
|
|
281
248
|
*/
|
|
282
249
|
attachConnection(appSessionId: string, connection: RealtimeClientConnection): boolean {
|
|
283
250
|
const run = runs.get(appSessionId);
|
|
@@ -7,7 +7,7 @@ import type {
|
|
|
7
7
|
import { createCompleteMessage, readObjectRecord } from '@/shared/utils.js';
|
|
8
8
|
|
|
9
9
|
type ChatSessionWriterOptions = {
|
|
10
|
-
connection: RealtimeClientConnection;
|
|
10
|
+
connection: RealtimeClientConnection | null;
|
|
11
11
|
userId: string | number | null;
|
|
12
12
|
provider: LLMProvider;
|
|
13
13
|
/** Provider-native id when resuming an existing session, otherwise null. */
|
|
@@ -44,7 +44,6 @@ type ChatSessionWriterOptions = {
|
|
|
44
44
|
* intercepted and recorded as the provider-id mapping as well.
|
|
45
45
|
*/
|
|
46
46
|
export class ChatSessionWriter {
|
|
47
|
-
ws: RealtimeClientConnection;
|
|
48
47
|
userId: string | number | null;
|
|
49
48
|
/**
|
|
50
49
|
* Some runtimes feature-detect their writer with this flag; keep it so the
|
|
@@ -53,6 +52,17 @@ export class ChatSessionWriter {
|
|
|
53
52
|
isWebSocketWriter = true;
|
|
54
53
|
|
|
55
54
|
private readonly options: ChatSessionWriterOptions;
|
|
55
|
+
/**
|
|
56
|
+
* Every socket currently watching this run, not just the one that started
|
|
57
|
+
* it. A session can legitimately be open in more than one place — a second
|
|
58
|
+
* browser tab, a phone alongside a laptop, the desktop app beside the web
|
|
59
|
+
* app — and each of those subscribes through `attachConnection`.
|
|
60
|
+
*
|
|
61
|
+
* Holding a single connection here meant the newest subscriber took the
|
|
62
|
+
* stream away from everyone before it, so a second tab silently froze
|
|
63
|
+
* mid-run and only caught up on the next REST history refresh.
|
|
64
|
+
*/
|
|
65
|
+
private readonly connections = new Set<RealtimeClientConnection>();
|
|
56
66
|
/**
|
|
57
67
|
* The provider-native session id as the runtime knows it. Kept locally
|
|
58
68
|
* (besides the registry) because runtimes read it back via `getSessionId()`
|
|
@@ -63,7 +73,12 @@ export class ChatSessionWriter {
|
|
|
63
73
|
|
|
64
74
|
constructor(options: ChatSessionWriterOptions) {
|
|
65
75
|
this.options = options;
|
|
66
|
-
|
|
76
|
+
// A run started by a timer has no audience yet. Everything still goes
|
|
77
|
+
// through `decorateOutboundEvent` into the run's replay buffer, so a
|
|
78
|
+
// client that subscribes mid-run catches up from the beginning.
|
|
79
|
+
if (options.connection) {
|
|
80
|
+
this.connections.add(options.connection);
|
|
81
|
+
}
|
|
67
82
|
this.userId = options.userId;
|
|
68
83
|
this.providerSessionId = options.providerSessionId;
|
|
69
84
|
}
|
|
@@ -116,8 +131,13 @@ export class ChatSessionWriter {
|
|
|
116
131
|
}
|
|
117
132
|
}
|
|
118
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Adds a socket to the run's live audience. Named for the `WebSocketWriter`
|
|
136
|
+
* method it stands in for, so runtime adapters keep working unchanged —
|
|
137
|
+
* but it adds rather than replaces.
|
|
138
|
+
*/
|
|
119
139
|
updateWebSocket(newConnection: RealtimeClientConnection): void {
|
|
120
|
-
this.
|
|
140
|
+
this.connections.add(newConnection);
|
|
121
141
|
}
|
|
122
142
|
|
|
123
143
|
setSessionId(sessionId: string): void {
|
|
@@ -138,8 +158,18 @@ export class ChatSessionWriter {
|
|
|
138
158
|
}
|
|
139
159
|
|
|
140
160
|
private forward(message: NormalizedMessage): void {
|
|
141
|
-
|
|
142
|
-
|
|
161
|
+
const payload = JSON.stringify(message);
|
|
162
|
+
|
|
163
|
+
// Sending is also when dead sockets get collected: a refreshed tab leaves
|
|
164
|
+
// its old connection behind, and without dropping it here the set would
|
|
165
|
+
// grow for the whole life of the run. Deleting during iteration is safe
|
|
166
|
+
// for a Set — the removed entry is simply not revisited.
|
|
167
|
+
for (const connection of this.connections) {
|
|
168
|
+
if (connection.readyState === WS_OPEN_STATE) {
|
|
169
|
+
connection.send(payload);
|
|
170
|
+
} else {
|
|
171
|
+
this.connections.delete(connection);
|
|
172
|
+
}
|
|
143
173
|
}
|
|
144
174
|
}
|
|
145
175
|
}
|
|
@@ -3,7 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import type { WebSocket } from 'ws';
|
|
4
4
|
|
|
5
5
|
import { sessionsDb } from '@/modules/database/index.js';
|
|
6
|
-
import { providerModelsService } from '@/modules/providers/index.js';
|
|
6
|
+
import { providerModelsService, sessionsService } from '@/modules/providers/index.js';
|
|
7
7
|
import { chatRunRegistry } from '@/modules/websocket/services/chat-run-registry.service.js';
|
|
8
8
|
import { connectedClients, WS_OPEN_STATE } from '@/modules/websocket/services/websocket-state.service.js';
|
|
9
9
|
import {
|
|
@@ -64,7 +64,7 @@ export function filterImagesToUploadStore(
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
/** Application boundary for dispatching provider runs and approvals. */
|
|
67
|
-
type ProviderRuntimeGateway = {
|
|
67
|
+
export type ProviderRuntimeGateway = {
|
|
68
68
|
hasRuntime(provider: string): boolean;
|
|
69
69
|
run(
|
|
70
70
|
provider: LLMProvider,
|
|
@@ -149,10 +149,34 @@ async function handleChatSend(
|
|
|
149
149
|
data: AnyRecord,
|
|
150
150
|
dependencies: ChatWebSocketDependencies
|
|
151
151
|
): Promise<void> {
|
|
152
|
+
const resolved = resolveSendTarget(ws, data, dependencies, 'chat.send');
|
|
153
|
+
if (!resolved) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
await dispatchRun(ws, userId, resolved.sessionId, resolved.session, data, dependencies);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
type ResolvedSendTarget = {
|
|
161
|
+
sessionId: string;
|
|
162
|
+
session: NonNullable<ReturnType<typeof sessionsDb.getSessionById>>;
|
|
163
|
+
provider: LLMProvider;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Shared front half of `chat.send` and `chat.edit-send`: the session row and
|
|
168
|
+
* provider come from the database, never from the client.
|
|
169
|
+
*/
|
|
170
|
+
function resolveSendTarget(
|
|
171
|
+
ws: WebSocket,
|
|
172
|
+
data: AnyRecord,
|
|
173
|
+
dependencies: ChatWebSocketDependencies,
|
|
174
|
+
frameName: string,
|
|
175
|
+
): ResolvedSendTarget | null {
|
|
152
176
|
const sessionId = readRequiredSessionId(data);
|
|
153
177
|
if (!sessionId) {
|
|
154
|
-
sendProtocolError(ws, 'SESSION_ID_REQUIRED',
|
|
155
|
-
return;
|
|
178
|
+
sendProtocolError(ws, 'SESSION_ID_REQUIRED', `${frameName} requires a sessionId.`);
|
|
179
|
+
return null;
|
|
156
180
|
}
|
|
157
181
|
|
|
158
182
|
const session = sessionsDb.getSessionById(sessionId);
|
|
@@ -163,15 +187,36 @@ async function handleChatSend(
|
|
|
163
187
|
`Session "${sessionId}" was not found. Create it via POST /api/providers/sessions first.`,
|
|
164
188
|
sessionId
|
|
165
189
|
);
|
|
166
|
-
return;
|
|
190
|
+
return null;
|
|
167
191
|
}
|
|
168
192
|
|
|
169
193
|
const provider = session.provider as LLMProvider;
|
|
170
194
|
if (!dependencies.runtime.hasRuntime(provider)) {
|
|
171
195
|
sendProtocolError(ws, 'UNSUPPORTED_PROVIDER', `Provider "${provider}" is not available.`, sessionId);
|
|
172
|
-
return;
|
|
196
|
+
return null;
|
|
173
197
|
}
|
|
174
198
|
|
|
199
|
+
return { sessionId, session, provider };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Registers the run and hands the turn to the provider runtime.
|
|
204
|
+
*
|
|
205
|
+
* `extraRuntimeOptions` is how an edited message asks the provider to resume
|
|
206
|
+
* partway instead of continuing from the tip; a normal send passes nothing.
|
|
207
|
+
*/
|
|
208
|
+
async function dispatchRun(
|
|
209
|
+
ws: WebSocket | null,
|
|
210
|
+
userId: string | number | null,
|
|
211
|
+
sessionId: string,
|
|
212
|
+
session: NonNullable<ReturnType<typeof sessionsDb.getSessionById>>,
|
|
213
|
+
data: AnyRecord,
|
|
214
|
+
dependencies: ChatWebSocketDependencies,
|
|
215
|
+
extraRuntimeOptions: AnyRecord = {},
|
|
216
|
+
beforeRun?: (run: NonNullable<ReturnType<typeof chatRunRegistry.startRun>>) => void | Promise<void>,
|
|
217
|
+
): Promise<{ started: boolean; error: string | null }> {
|
|
218
|
+
const provider = session.provider as LLMProvider;
|
|
219
|
+
|
|
175
220
|
const run = chatRunRegistry.startRun({
|
|
176
221
|
appSessionId: sessionId,
|
|
177
222
|
provider,
|
|
@@ -181,13 +226,15 @@ async function handleChatSend(
|
|
|
181
226
|
});
|
|
182
227
|
|
|
183
228
|
if (!run) {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
229
|
+
if (ws) {
|
|
230
|
+
sendProtocolError(
|
|
231
|
+
ws,
|
|
232
|
+
'RUN_IN_PROGRESS',
|
|
233
|
+
`Session "${sessionId}" already has a run in progress.`,
|
|
234
|
+
sessionId
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
return { started: false, error: 'A run is already in progress for this session.' };
|
|
191
238
|
}
|
|
192
239
|
|
|
193
240
|
const clientOptions = (data.options ?? {}) as AnyRecord;
|
|
@@ -221,6 +268,7 @@ async function handleChatSend(
|
|
|
221
268
|
// app session id.
|
|
222
269
|
const runtimeOptions: AnyRecord = {
|
|
223
270
|
...clientOptions,
|
|
271
|
+
...extraRuntimeOptions,
|
|
224
272
|
// Attachments are re-validated server-side: only direct children of the
|
|
225
273
|
// global upload store may reach provider runtimes or their file tools.
|
|
226
274
|
attachments: uniqueAttachments,
|
|
@@ -231,11 +279,17 @@ async function handleChatSend(
|
|
|
231
279
|
projectPath: session.project_path ?? clientOptions.projectPath,
|
|
232
280
|
};
|
|
233
281
|
|
|
282
|
+
let failure: string | null = null;
|
|
234
283
|
try {
|
|
284
|
+
// Runs only now that the session is reserved, because an edit rewinds the
|
|
285
|
+
// conversation here and a rewind for a run that was never admitted cannot
|
|
286
|
+
// be taken back. Inside the try so a rewind that throws still releases the
|
|
287
|
+
// run instead of leaving the session processing forever.
|
|
288
|
+
await beforeRun?.(run);
|
|
235
289
|
await dependencies.runtime.run(provider, command, runtimeOptions, run.writer);
|
|
236
290
|
} catch (error) {
|
|
237
|
-
|
|
238
|
-
console.error(`[Chat] Provider runtime "${provider}" failed`, { sessionId, error:
|
|
291
|
+
failure = error instanceof Error ? error.message : String(error);
|
|
292
|
+
console.error(`[Chat] Provider runtime "${provider}" failed`, { sessionId, error: failure });
|
|
239
293
|
} finally {
|
|
240
294
|
// Safety net: a runtime that crashed (or resolved) without emitting its
|
|
241
295
|
// terminal `complete` would otherwise leave the session stuck in
|
|
@@ -244,6 +298,113 @@ async function handleChatSend(
|
|
|
244
298
|
// settles, and the session-keyed completeRun would kill that new run.
|
|
245
299
|
chatRunRegistry.completeRunIfCurrent(run, { exitCode: 1 });
|
|
246
300
|
}
|
|
301
|
+
|
|
302
|
+
return { started: true, error: failure };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Handles `chat.edit-send`: replaces an already-sent message and everything
|
|
307
|
+
* after it with a new turn.
|
|
308
|
+
*
|
|
309
|
+
* Nothing is deleted. The provider resumes the conversation partway and
|
|
310
|
+
* appends the replacement, so the abandoned attempt stays in the transcript
|
|
311
|
+
* file and is simply no longer part of the live conversation — the same shape
|
|
312
|
+
* Claude Code's rewind and Codex's fork-with-cut-point produce.
|
|
313
|
+
*/
|
|
314
|
+
async function handleChatEditSend(
|
|
315
|
+
ws: WebSocket,
|
|
316
|
+
userId: string | number | null,
|
|
317
|
+
data: AnyRecord,
|
|
318
|
+
dependencies: ChatWebSocketDependencies
|
|
319
|
+
): Promise<void> {
|
|
320
|
+
const resolved = resolveSendTarget(ws, data, dependencies, 'chat.edit-send');
|
|
321
|
+
if (!resolved) {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const { sessionId, session, provider } = resolved;
|
|
326
|
+
const anchorId = typeof data.anchorId === 'string' ? data.anchorId.trim() : '';
|
|
327
|
+
if (!anchorId) {
|
|
328
|
+
sendProtocolError(ws, 'ANCHOR_REQUIRED', 'chat.edit-send requires the anchorId of the message being replaced.', sessionId);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
let resumeThroughId: string | null;
|
|
333
|
+
try {
|
|
334
|
+
const anchor = await sessionsService.resolveEditAnchor(sessionId, anchorId);
|
|
335
|
+
if (!anchor) {
|
|
336
|
+
sendProtocolError(
|
|
337
|
+
ws,
|
|
338
|
+
'EDIT_NOT_SUPPORTED',
|
|
339
|
+
`Provider "${provider}" cannot replace an already-sent message.`,
|
|
340
|
+
sessionId
|
|
341
|
+
);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (!anchor.found) {
|
|
345
|
+
sendProtocolError(ws, 'ANCHOR_NOT_FOUND', 'That message is no longer in the transcript.', sessionId);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
resumeThroughId = anchor.resumeThroughId;
|
|
349
|
+
} catch (error) {
|
|
350
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
351
|
+
sendProtocolError(ws, 'ANCHOR_LOOKUP_FAILED', `Could not read the transcript: ${message}`, sessionId);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Providers split here on what their runtime can do. Claude resumes its
|
|
356
|
+
// transcript partway, so the anchor rides along as a run option. Codex
|
|
357
|
+
// cannot — a thread only grows — so the conversation is rewound on disk and
|
|
358
|
+
// the run that follows is an ordinary resume of whatever the session then
|
|
359
|
+
// points at. Which of the two applies is decided here; the rewind itself
|
|
360
|
+
// waits until the run has actually been admitted.
|
|
361
|
+
const rewinds = sessionsService.providerRewindsForEdit(sessionId);
|
|
362
|
+
|
|
363
|
+
await dispatchRun(
|
|
364
|
+
ws,
|
|
365
|
+
userId,
|
|
366
|
+
sessionId,
|
|
367
|
+
session,
|
|
368
|
+
data,
|
|
369
|
+
dependencies,
|
|
370
|
+
// `null` is meaningful: the edited turn was the first prompt, so the
|
|
371
|
+
// conversation starts over instead of resuming.
|
|
372
|
+
rewinds
|
|
373
|
+
? {}
|
|
374
|
+
: { resumeAnchorId: resumeThroughId ?? undefined, resumeFromScratch: resumeThroughId === null },
|
|
375
|
+
async (run) => {
|
|
376
|
+
// Emitted through the run's writer so it is sequenced and replayed like
|
|
377
|
+
// any other event — a second tab watching this session has to truncate
|
|
378
|
+
// too.
|
|
379
|
+
//
|
|
380
|
+
// Before the rewind, not after it. A rewind that has to branch spawns a
|
|
381
|
+
// process and waits on a JSON-RPC round trip, and holding the frame
|
|
382
|
+
// until that came back left the message the user had just edited away
|
|
383
|
+
// sitting on screen for about a second — the very flicker this feature
|
|
384
|
+
// exists to avoid. Announcing first is safe because a rewind that fails
|
|
385
|
+
// still ends the run, and the terminal `complete` makes every client
|
|
386
|
+
// re-read the transcript, which puts back anything that turned out not
|
|
387
|
+
// to have been replaced after all.
|
|
388
|
+
run.writer.send({
|
|
389
|
+
kind: 'history_truncated',
|
|
390
|
+
provider,
|
|
391
|
+
sessionId,
|
|
392
|
+
anchorId,
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
if (rewinds) {
|
|
396
|
+
try {
|
|
397
|
+
await sessionsService.rewindSessionForEdit(sessionId, resumeThroughId);
|
|
398
|
+
} catch (error) {
|
|
399
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
400
|
+
sendProtocolError(ws, 'EDIT_REWIND_FAILED', `Could not rewind the conversation: ${message}`, sessionId);
|
|
401
|
+
// Ends the run before the provider is asked to continue a
|
|
402
|
+
// conversation that was not rewound after all.
|
|
403
|
+
throw error;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
},
|
|
407
|
+
);
|
|
247
408
|
}
|
|
248
409
|
|
|
249
410
|
/**
|
|
@@ -374,6 +535,71 @@ function handlePermissionResponse(data: AnyRecord, dependencies: ChatWebSocketDe
|
|
|
374
535
|
* (`chat_subscribed`, `session_upserted`, `loading_progress`,
|
|
375
536
|
* `protocol_error`).
|
|
376
537
|
*/
|
|
538
|
+
/**
|
|
539
|
+
* Runs a turn for a session with no client attached.
|
|
540
|
+
*
|
|
541
|
+
* Used by scheduled messages, which fire from a timer: there is no socket to
|
|
542
|
+
* report errors to and no audience to stream to. The run is registered exactly
|
|
543
|
+
* like an interactive one, so anyone who opens the session while it is going
|
|
544
|
+
* subscribes and replays it from the start, and the session shows as busy
|
|
545
|
+
* everywhere in the meantime.
|
|
546
|
+
*
|
|
547
|
+
* Resolves when the provider run settles. Returns false when the session has
|
|
548
|
+
* gone away or is busy without `interruptActiveRun`, which the caller reports
|
|
549
|
+
* on the schedule.
|
|
550
|
+
*/
|
|
551
|
+
export async function runDetachedChatTurn(
|
|
552
|
+
input: {
|
|
553
|
+
sessionId: string;
|
|
554
|
+
userId: string | number | null;
|
|
555
|
+
content: string;
|
|
556
|
+
options?: AnyRecord;
|
|
557
|
+
/**
|
|
558
|
+
* Aborts a run already in progress instead of refusing to start. A
|
|
559
|
+
* scheduled message sets this: the user picked the time knowing it might
|
|
560
|
+
* land mid-run, so the timer outranks whatever is running.
|
|
561
|
+
*/
|
|
562
|
+
interruptActiveRun?: boolean;
|
|
563
|
+
},
|
|
564
|
+
dependencies: ChatWebSocketDependencies,
|
|
565
|
+
): Promise<{ started: boolean; error: string | null }> {
|
|
566
|
+
const session = sessionsDb.getSessionById(input.sessionId);
|
|
567
|
+
if (!session) {
|
|
568
|
+
return { started: false, error: 'The session no longer exists.' };
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const provider = session.provider as LLMProvider;
|
|
572
|
+
if (!dependencies.runtime.hasRuntime(provider)) {
|
|
573
|
+
return { started: false, error: `Provider "${provider}" is not available.` };
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const activeRun = chatRunRegistry.getRun(input.sessionId);
|
|
577
|
+
if (activeRun && activeRun.status === 'running') {
|
|
578
|
+
if (!input.interruptActiveRun) {
|
|
579
|
+
return { started: false, error: 'A run was already in progress for this session.' };
|
|
580
|
+
}
|
|
581
|
+
// Same shape as `chat.abort`: cancel the provider run and emit the
|
|
582
|
+
// terminal `complete` on its behalf, so every watching client sees the
|
|
583
|
+
// interrupted run end before this turn's stream begins. The interrupted
|
|
584
|
+
// run's own dispatch settles later through completeRunIfCurrent, which is
|
|
585
|
+
// scoped to that run and cannot touch the one started here.
|
|
586
|
+
const aborted = await dependencies.runtime.abort(activeRun.provider, input.sessionId);
|
|
587
|
+
chatRunRegistry.completeRun(input.sessionId, {
|
|
588
|
+
exitCode: aborted ? 0 : 1,
|
|
589
|
+
aborted: true,
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
return dispatchRun(
|
|
594
|
+
null,
|
|
595
|
+
input.userId,
|
|
596
|
+
input.sessionId,
|
|
597
|
+
session,
|
|
598
|
+
{ sessionId: input.sessionId, content: input.content, options: input.options ?? {} },
|
|
599
|
+
dependencies,
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
|
|
377
603
|
export function handleChatConnection(
|
|
378
604
|
ws: WebSocket,
|
|
379
605
|
request: AuthenticatedWebSocketRequest,
|
|
@@ -395,6 +621,9 @@ export function handleChatConnection(
|
|
|
395
621
|
const messageType = typeof data.type === 'string' ? data.type : '';
|
|
396
622
|
|
|
397
623
|
switch (messageType) {
|
|
624
|
+
case 'chat.edit-send':
|
|
625
|
+
await handleChatEditSend(ws, userId, data, dependencies);
|
|
626
|
+
return;
|
|
398
627
|
case 'chat.send':
|
|
399
628
|
await handleChatSend(ws, userId, data, dependencies);
|
|
400
629
|
return;
|