@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
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
import { projectsDb, sessionsDb } from '@/modules/database/index.js';
|
|
4
|
+
import { generateDisplayName } from '@/modules/projects/index.js';
|
|
5
|
+
import { connectedClients, WS_OPEN_STATE } from '@/modules/websocket/services/websocket-state.service.js';
|
|
6
|
+
import type { SessionUpsertedEvent } from '@/shared/types.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The single producer of the `session_upserted` delta.
|
|
10
|
+
*
|
|
11
|
+
* The event carries everything a sidebar needs to upsert a session in place
|
|
12
|
+
* (its summary plus the owning project's metadata), so clients never refetch
|
|
13
|
+
* the whole project list because one transcript changed on disk.
|
|
14
|
+
*
|
|
15
|
+
* It used to be built in two places — the on-disk sessions watcher and the
|
|
16
|
+
* chat run registry — and only the registry's copy set `providerSessionId`.
|
|
17
|
+
* That field is how the client notices a row it is showing has been merged
|
|
18
|
+
* into its canonical app-session row, so a merge announced by any other path
|
|
19
|
+
* left a stale duplicate in the sidebar. One builder, one shape.
|
|
20
|
+
*/
|
|
21
|
+
async function buildSessionUpsertedEvent(
|
|
22
|
+
sessionIdOrProviderSessionId: string,
|
|
23
|
+
): Promise<SessionUpsertedEvent | null> {
|
|
24
|
+
// Resolving by provider id first covers the watcher, which only ever sees
|
|
25
|
+
// the id written in the transcript. For a row where the two ids are equal
|
|
26
|
+
// — legacy rows, and any session indexed from disk before its mapping
|
|
27
|
+
// existed — both lookups land on the same row anyway.
|
|
28
|
+
const row = sessionsDb.getSessionByProviderSessionId(sessionIdOrProviderSessionId)
|
|
29
|
+
?? sessionsDb.getSessionById(sessionIdOrProviderSessionId);
|
|
30
|
+
if (!row || row.isArchived) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const projectPath = row.project_path;
|
|
35
|
+
const project = projectPath ? projectsDb.getProjectPath(projectPath) : null;
|
|
36
|
+
const displayName = project?.custom_project_name?.trim()
|
|
37
|
+
? project.custom_project_name
|
|
38
|
+
: await generateDisplayName(path.basename(projectPath ?? '') || (projectPath ?? ''), projectPath);
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
kind: 'session_upserted',
|
|
42
|
+
sessionId: row.session_id,
|
|
43
|
+
providerSessionId: row.provider_session_id ?? null,
|
|
44
|
+
// The column is a plain TEXT and typed as `string` on the row, but only
|
|
45
|
+
// provider adapters ever write it.
|
|
46
|
+
provider: row.provider as SessionUpsertedEvent['provider'],
|
|
47
|
+
session: {
|
|
48
|
+
id: row.session_id,
|
|
49
|
+
summary: row.custom_name || '',
|
|
50
|
+
messageCount: 0,
|
|
51
|
+
lastActivity: row.updated_at ?? row.created_at ?? new Date().toISOString(),
|
|
52
|
+
},
|
|
53
|
+
project: project
|
|
54
|
+
? {
|
|
55
|
+
projectId: project.project_id,
|
|
56
|
+
path: project.project_path,
|
|
57
|
+
fullPath: project.project_path,
|
|
58
|
+
displayName,
|
|
59
|
+
isStarred: Boolean(project.isStarred),
|
|
60
|
+
}
|
|
61
|
+
: null,
|
|
62
|
+
timestamp: new Date().toISOString(),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function sendToConnectedClients(payloads: string[]): void {
|
|
67
|
+
if (payloads.length === 0) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
connectedClients.forEach((client) => {
|
|
72
|
+
if (client.readyState === WS_OPEN_STATE) {
|
|
73
|
+
for (const payload of payloads) {
|
|
74
|
+
client.send(payload);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Announces one session. Used by the chat run registry when a run reports its provider-native id. */
|
|
81
|
+
export async function broadcastSessionUpserted(sessionIdOrProviderSessionId: string): Promise<void> {
|
|
82
|
+
const event = await buildSessionUpsertedEvent(sessionIdOrProviderSessionId);
|
|
83
|
+
if (event) {
|
|
84
|
+
sendToConnectedClients([JSON.stringify(event)]);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Announces a batch of sessions. Used by the providers module's sessions
|
|
90
|
+
* watcher, whose debounced flush can carry dozens of ids at once — the client
|
|
91
|
+
* set is walked once for the whole batch rather than once per session.
|
|
92
|
+
*/
|
|
93
|
+
export async function broadcastSessionUpsertedBatch(
|
|
94
|
+
sessionIds: Iterable<string>,
|
|
95
|
+
): Promise<void> {
|
|
96
|
+
const payloads: string[] = [];
|
|
97
|
+
for (const sessionId of sessionIds) {
|
|
98
|
+
const event = await buildSessionUpsertedEvent(sessionId);
|
|
99
|
+
if (event) {
|
|
100
|
+
payloads.push(JSON.stringify(event));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
sendToConnectedClients(payloads);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** @internal Exported for the broadcast tests, which assert the payload shape directly. */
|
|
108
|
+
export { buildSessionUpsertedEvent };
|
|
@@ -19,6 +19,7 @@ type ShellIncomingMessage = {
|
|
|
19
19
|
initialCommand?: string;
|
|
20
20
|
isPlainShell?: boolean;
|
|
21
21
|
forceRestart?: boolean;
|
|
22
|
+
bypassPermissions?: boolean;
|
|
22
23
|
};
|
|
23
24
|
|
|
24
25
|
type PtySessionEntry = {
|
|
@@ -216,12 +217,18 @@ function buildShellCommand(
|
|
|
216
217
|
return initialCommand || 'opencode';
|
|
217
218
|
}
|
|
218
219
|
|
|
219
|
-
|
|
220
|
+
// Launching with the flag is what unlocks "bypass permissions" in the CLI's
|
|
221
|
+
// shift+tab permission-mode cycle; it cannot be enabled from inside a
|
|
222
|
+
// session started without it.
|
|
223
|
+
const bypassFlag = readBoolean(message.bypassPermissions)
|
|
224
|
+
? ' --dangerously-skip-permissions'
|
|
225
|
+
: '';
|
|
226
|
+
const command = initialCommand || `claude${bypassFlag}`;
|
|
220
227
|
if (resumeSessionId) {
|
|
221
228
|
if (os.platform() === 'win32') {
|
|
222
|
-
return `claude --resume "${resumeSessionId}"; if ($LASTEXITCODE -ne 0) { claude }`;
|
|
229
|
+
return `claude --resume "${resumeSessionId}"${bypassFlag}; if ($LASTEXITCODE -ne 0) { claude${bypassFlag} }`;
|
|
223
230
|
}
|
|
224
|
-
return `claude --resume "${resumeSessionId}" || claude`;
|
|
231
|
+
return `claude --resume "${resumeSessionId}"${bypassFlag} || claude${bypassFlag}`;
|
|
225
232
|
}
|
|
226
233
|
return command;
|
|
227
234
|
}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { EventEmitter } from 'node:events';
|
|
3
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import test from 'node:test';
|
|
7
|
+
|
|
8
|
+
import { closeConnection, initializeDatabase, sessionsDb } from '@/modules/database/index.js';
|
|
9
|
+
import { sessionsService } from '@/modules/providers/index.js';
|
|
10
|
+
import { handleChatConnection } from '@/modules/websocket/services/chat-websocket.service.js';
|
|
11
|
+
import { chatRunRegistry } from '@/modules/websocket/services/chat-run-registry.service.js';
|
|
12
|
+
import { connectedClients } from '@/modules/websocket/services/websocket-state.service.js';
|
|
13
|
+
|
|
14
|
+
const SESSION_ID = 'edit-session';
|
|
15
|
+
|
|
16
|
+
function createFakeSocket() {
|
|
17
|
+
const socket = new EventEmitter() as EventEmitter & {
|
|
18
|
+
readyState: number;
|
|
19
|
+
frames: Array<Record<string, unknown>>;
|
|
20
|
+
send: (data: string) => void;
|
|
21
|
+
};
|
|
22
|
+
socket.readyState = 1;
|
|
23
|
+
socket.frames = [];
|
|
24
|
+
socket.send = (data: string) => socket.frames.push(JSON.parse(data) as Record<string, unknown>);
|
|
25
|
+
return socket;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Two turns and an edit of the second, which is the transcript the gateway reads. */
|
|
29
|
+
const TRANSCRIPT_ROWS = [
|
|
30
|
+
{
|
|
31
|
+
type: 'user', uuid: 'e-u1', parentUuid: null, sessionId: SESSION_ID,
|
|
32
|
+
timestamp: '2026-08-23T10:00:00.000Z',
|
|
33
|
+
message: { role: 'user', content: [{ type: 'text', text: 'first' }] },
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
type: 'assistant', uuid: 'e-a1', parentUuid: 'e-u1', sessionId: SESSION_ID,
|
|
37
|
+
timestamp: '2026-08-23T10:00:01.000Z',
|
|
38
|
+
message: { role: 'assistant', model: 'claude-opus-5', content: [{ type: 'text', text: 'reply' }] },
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
type: 'user', uuid: 'e-u2', parentUuid: 'e-a1', sessionId: SESSION_ID,
|
|
42
|
+
timestamp: '2026-08-23T10:00:02.000Z',
|
|
43
|
+
message: { role: 'user', content: [{ type: 'text', text: 'second' }] },
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The same two turns as a Codex rollout. Codex rows carry no id of their own,
|
|
49
|
+
* so the edit anchor is the enclosing turn — which is also the unit its fork
|
|
50
|
+
* endpoint cuts at.
|
|
51
|
+
*/
|
|
52
|
+
const CODEX_TRANSCRIPT_ROWS = [
|
|
53
|
+
{ type: 'event_msg', payload: { type: 'task_started', turn_id: 'turn-a' } },
|
|
54
|
+
{ type: 'event_msg', payload: { type: 'user_message', message: 'first' } },
|
|
55
|
+
{ type: 'event_msg', payload: { type: 'task_complete', turn_id: 'turn-a' } },
|
|
56
|
+
{ type: 'event_msg', payload: { type: 'task_started', turn_id: 'turn-b' } },
|
|
57
|
+
{ type: 'event_msg', payload: { type: 'user_message', message: 'second' } },
|
|
58
|
+
{ type: 'event_msg', payload: { type: 'task_complete', turn_id: 'turn-b' } },
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
type RunCall = { provider: string; command: string; options: Record<string, unknown> };
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Set by a test that needs a run to still be in flight when the next frame
|
|
65
|
+
* arrives; the stub runtime returns immediately otherwise. Released when the
|
|
66
|
+
* test ends, so no stub run is left pending for the rest of the file.
|
|
67
|
+
*/
|
|
68
|
+
let holdRun: Promise<void> | null = null;
|
|
69
|
+
let releaseHeldRun: (() => void) | null = null;
|
|
70
|
+
|
|
71
|
+
function holdTheNextRun(): void {
|
|
72
|
+
holdRun = new Promise<void>((resolve) => { releaseHeldRun = resolve; });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function withGateway(
|
|
76
|
+
provider: string,
|
|
77
|
+
runTest: (context: {
|
|
78
|
+
socket: ReturnType<typeof createFakeSocket>;
|
|
79
|
+
runs: RunCall[];
|
|
80
|
+
}) => Promise<void>,
|
|
81
|
+
rows: unknown[] = TRANSCRIPT_ROWS,
|
|
82
|
+
): Promise<void> {
|
|
83
|
+
const previousDatabasePath = process.env.DATABASE_PATH;
|
|
84
|
+
const tempDirectory = await mkdtemp(path.join(os.tmpdir(), 'chat-edit-send-'));
|
|
85
|
+
const transcriptPath = path.join(tempDirectory, `${SESSION_ID}.jsonl`);
|
|
86
|
+
await writeFile(transcriptPath, `${rows.map((row) => JSON.stringify(row)).join('\n')}\n`, 'utf8');
|
|
87
|
+
|
|
88
|
+
closeConnection();
|
|
89
|
+
process.env.DATABASE_PATH = path.join(tempDirectory, 'auth.db');
|
|
90
|
+
await initializeDatabase();
|
|
91
|
+
|
|
92
|
+
const runs: RunCall[] = [];
|
|
93
|
+
const socket = createFakeSocket();
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const now = new Date().toISOString();
|
|
97
|
+
sessionsDb.createSession(SESSION_ID, provider, tempDirectory, 'Edit session', now, now, transcriptPath);
|
|
98
|
+
|
|
99
|
+
handleChatConnection(
|
|
100
|
+
socket as never,
|
|
101
|
+
{ user: { id: 1 } } as never,
|
|
102
|
+
{
|
|
103
|
+
runtime: {
|
|
104
|
+
hasRuntime: () => true,
|
|
105
|
+
run: async (runProvider: string, command: string, options: Record<string, unknown>) => {
|
|
106
|
+
runs.push({ provider: runProvider, command, options });
|
|
107
|
+
if (holdRun) {
|
|
108
|
+
await holdRun;
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
} as never,
|
|
112
|
+
},
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
await runTest({ socket, runs });
|
|
116
|
+
} finally {
|
|
117
|
+
releaseHeldRun?.();
|
|
118
|
+
releaseHeldRun = null;
|
|
119
|
+
holdRun = null;
|
|
120
|
+
connectedClients.clear();
|
|
121
|
+
chatRunRegistry.clearAll();
|
|
122
|
+
closeConnection();
|
|
123
|
+
if (previousDatabasePath === undefined) {
|
|
124
|
+
delete process.env.DATABASE_PATH;
|
|
125
|
+
} else {
|
|
126
|
+
process.env.DATABASE_PATH = previousDatabasePath;
|
|
127
|
+
}
|
|
128
|
+
await rm(tempDirectory, { recursive: true, force: true });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** The handler is async and the socket listener does not await it. */
|
|
133
|
+
const settle = () => new Promise((resolve) => { setTimeout(resolve, 30); });
|
|
134
|
+
|
|
135
|
+
test('an edit resumes through the turn before the one being replaced', async () => {
|
|
136
|
+
await withGateway('claude', async ({ socket, runs }) => {
|
|
137
|
+
socket.emit('message', JSON.stringify({
|
|
138
|
+
type: 'chat.edit-send',
|
|
139
|
+
sessionId: SESSION_ID,
|
|
140
|
+
anchorId: 'e-u2',
|
|
141
|
+
content: 'a better second prompt',
|
|
142
|
+
}));
|
|
143
|
+
await settle();
|
|
144
|
+
|
|
145
|
+
assert.equal(runs.length, 1);
|
|
146
|
+
assert.equal(runs[0].command, 'a better second prompt');
|
|
147
|
+
// Inclusive of the row it names, so this is the last turn KEPT.
|
|
148
|
+
assert.equal(runs[0].options.resumeAnchorId, 'e-a1');
|
|
149
|
+
assert.equal(runs[0].options.resumeFromScratch, false);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('editing the first prompt starts the conversation over', async () => {
|
|
154
|
+
await withGateway('claude', async ({ socket, runs }) => {
|
|
155
|
+
socket.emit('message', JSON.stringify({
|
|
156
|
+
type: 'chat.edit-send',
|
|
157
|
+
sessionId: SESSION_ID,
|
|
158
|
+
anchorId: 'e-u1',
|
|
159
|
+
content: 'a better first prompt',
|
|
160
|
+
}));
|
|
161
|
+
await settle();
|
|
162
|
+
|
|
163
|
+
assert.equal(runs.length, 1);
|
|
164
|
+
assert.equal(runs[0].options.resumeAnchorId, undefined);
|
|
165
|
+
assert.equal(runs[0].options.resumeFromScratch, true);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('every subscribed client is told to drop the superseded turns', async () => {
|
|
170
|
+
await withGateway('claude', async ({ socket }) => {
|
|
171
|
+
socket.emit('message', JSON.stringify({
|
|
172
|
+
type: 'chat.edit-send',
|
|
173
|
+
sessionId: SESSION_ID,
|
|
174
|
+
anchorId: 'e-u2',
|
|
175
|
+
content: 'replacement',
|
|
176
|
+
}));
|
|
177
|
+
await settle();
|
|
178
|
+
|
|
179
|
+
const truncation = socket.frames.find((frame) => frame.kind === 'history_truncated');
|
|
180
|
+
assert.ok(truncation, 'a history_truncated frame is emitted');
|
|
181
|
+
assert.equal(truncation?.anchorId, 'e-u2');
|
|
182
|
+
assert.equal(truncation?.sessionId, SESSION_ID);
|
|
183
|
+
// Sequenced like every other run event, so a reconnecting tab replays it.
|
|
184
|
+
assert.equal(typeof truncation?.seq, 'number');
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test('an edit without an anchor is refused', async () => {
|
|
189
|
+
await withGateway('claude', async ({ socket, runs }) => {
|
|
190
|
+
socket.emit('message', JSON.stringify({
|
|
191
|
+
type: 'chat.edit-send',
|
|
192
|
+
sessionId: SESSION_ID,
|
|
193
|
+
content: 'no anchor',
|
|
194
|
+
}));
|
|
195
|
+
await settle();
|
|
196
|
+
|
|
197
|
+
assert.equal(runs.length, 0);
|
|
198
|
+
assert.equal(socket.frames.at(-1)?.code, 'ANCHOR_REQUIRED');
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test('an anchor the transcript does not hold is refused', async () => {
|
|
203
|
+
await withGateway('claude', async ({ socket, runs }) => {
|
|
204
|
+
socket.emit('message', JSON.stringify({
|
|
205
|
+
type: 'chat.edit-send',
|
|
206
|
+
sessionId: SESSION_ID,
|
|
207
|
+
anchorId: 'not-in-transcript',
|
|
208
|
+
content: 'replacement',
|
|
209
|
+
}));
|
|
210
|
+
await settle();
|
|
211
|
+
|
|
212
|
+
assert.equal(runs.length, 0);
|
|
213
|
+
assert.equal(socket.frames.at(-1)?.code, 'ANCHOR_NOT_FOUND');
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test('a provider that cannot re-run from a point is refused rather than sending a new message', async () => {
|
|
218
|
+
await withGateway('cursor', async ({ socket, runs }) => {
|
|
219
|
+
socket.emit('message', JSON.stringify({
|
|
220
|
+
type: 'chat.edit-send',
|
|
221
|
+
sessionId: SESSION_ID,
|
|
222
|
+
anchorId: 'e-u2',
|
|
223
|
+
content: 'replacement',
|
|
224
|
+
}));
|
|
225
|
+
await settle();
|
|
226
|
+
|
|
227
|
+
assert.equal(runs.length, 0);
|
|
228
|
+
assert.equal(socket.frames.at(-1)?.code, 'EDIT_NOT_SUPPORTED');
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test('a refused send never rewinds the conversation', async () => {
|
|
233
|
+
await withGateway('codex', async ({ socket, runs }) => {
|
|
234
|
+
// The rewind moves the session onto a different provider transcript and
|
|
235
|
+
// cannot be undone, so a send the gateway is about to refuse must not
|
|
236
|
+
// reach it. A run already in flight is the realistic way that happens: a
|
|
237
|
+
// scheduled message, or another device.
|
|
238
|
+
const realRewind = sessionsService.rewindSessionForEdit;
|
|
239
|
+
let rewound = false;
|
|
240
|
+
sessionsService.rewindSessionForEdit = async () => { rewound = true; };
|
|
241
|
+
holdTheNextRun();
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
socket.emit('message', JSON.stringify({
|
|
245
|
+
type: 'chat.send',
|
|
246
|
+
sessionId: SESSION_ID,
|
|
247
|
+
content: 'a turn that is already running',
|
|
248
|
+
}));
|
|
249
|
+
await settle();
|
|
250
|
+
assert.equal(runs.length, 1);
|
|
251
|
+
|
|
252
|
+
socket.emit('message', JSON.stringify({
|
|
253
|
+
type: 'chat.edit-send',
|
|
254
|
+
sessionId: SESSION_ID,
|
|
255
|
+
anchorId: 'turn-b',
|
|
256
|
+
content: 'an edit that arrives too late',
|
|
257
|
+
}));
|
|
258
|
+
await settle();
|
|
259
|
+
} finally {
|
|
260
|
+
sessionsService.rewindSessionForEdit = realRewind;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
assert.equal(rewound, false);
|
|
264
|
+
assert.equal(runs.length, 1);
|
|
265
|
+
assert.equal(socket.frames.at(-1)?.code, 'RUN_IN_PROGRESS');
|
|
266
|
+
}, CODEX_TRANSCRIPT_ROWS);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test('a provider that has to branch to rewind is rewound before the run, not during it', async () => {
|
|
270
|
+
await withGateway('codex', async ({ socket, runs }) => {
|
|
271
|
+
// The rewind itself belongs to the provider and is covered there; what
|
|
272
|
+
// this asserts is the gateway's half — that a provider which reports it
|
|
273
|
+
// rewound gets an ordinary run instead of one carrying a resume anchor its
|
|
274
|
+
// runtime would not know what to do with.
|
|
275
|
+
const realRewind = sessionsService.rewindSessionForEdit;
|
|
276
|
+
const rewindCalls: unknown[] = [];
|
|
277
|
+
let truncatedBeforeRewind = false;
|
|
278
|
+
sessionsService.rewindSessionForEdit = async (sessionId: string, keepThroughId: string | null) => {
|
|
279
|
+
truncatedBeforeRewind = socket.frames.some((frame) => frame.kind === 'history_truncated');
|
|
280
|
+
rewindCalls.push({ sessionId, keepThroughId });
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
socket.emit('message', JSON.stringify({
|
|
285
|
+
type: 'chat.edit-send',
|
|
286
|
+
sessionId: SESSION_ID,
|
|
287
|
+
anchorId: 'turn-b',
|
|
288
|
+
content: 'a better second prompt',
|
|
289
|
+
}));
|
|
290
|
+
await settle();
|
|
291
|
+
} finally {
|
|
292
|
+
sessionsService.rewindSessionForEdit = realRewind;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Resolved from the real Codex rollout: the turn before the edited one.
|
|
296
|
+
assert.deepEqual(rewindCalls, [{ sessionId: SESSION_ID, keepThroughId: 'turn-a' }]);
|
|
297
|
+
assert.equal(runs.length, 1);
|
|
298
|
+
assert.equal(runs[0].command, 'a better second prompt');
|
|
299
|
+
assert.equal(runs[0].options.resumeAnchorId, undefined);
|
|
300
|
+
assert.equal(runs[0].options.resumeFromScratch, undefined);
|
|
301
|
+
|
|
302
|
+
// Clients still hear about the cut; how it was made is the server's
|
|
303
|
+
// business. And they hear about it first: a rewind that branches waits on
|
|
304
|
+
// a spawned process, and holding the frame until it returned left the
|
|
305
|
+
// replaced message on screen for about a second.
|
|
306
|
+
assert.ok(socket.frames.some((frame) => frame.kind === 'history_truncated'));
|
|
307
|
+
assert.equal(truncatedBeforeRewind, true);
|
|
308
|
+
}, CODEX_TRANSCRIPT_ROWS);
|
|
309
|
+
});
|
|
@@ -70,7 +70,7 @@ test('live events are remapped to the app session id and sequenced', async () =>
|
|
|
70
70
|
});
|
|
71
71
|
|
|
72
72
|
test('session_created is swallowed and persisted as the provider-id mapping', async () => {
|
|
73
|
-
await withIsolatedDatabase(() => {
|
|
73
|
+
await withIsolatedDatabase(async () => {
|
|
74
74
|
sessionsDb.createAppSession('app-run-2', 'cursor', '/workspace/demo');
|
|
75
75
|
const connection = new FakeConnection();
|
|
76
76
|
connectedClients.add(connection as never);
|
|
@@ -90,6 +90,10 @@ test('session_created is swallowed and persisted as the provider-id mapping', as
|
|
|
90
90
|
newSessionId: 'cursor-native-7',
|
|
91
91
|
});
|
|
92
92
|
|
|
93
|
+
// The upsert is broadcast without blocking the run: resolving the owning
|
|
94
|
+
// project's display name is async, so let that settle before asserting.
|
|
95
|
+
await new Promise((resolve) => { setTimeout(resolve, 0); });
|
|
96
|
+
|
|
93
97
|
// The provider-native event itself is never forwarded...
|
|
94
98
|
const sessionUpserts = connection.frames.filter((frame) => frame.kind === 'session_upserted');
|
|
95
99
|
assert.equal(sessionUpserts.length, 1);
|
|
@@ -222,7 +226,7 @@ test('replayEvents returns only events after the requested seq', async () => {
|
|
|
222
226
|
});
|
|
223
227
|
});
|
|
224
228
|
|
|
225
|
-
test('attachConnection
|
|
229
|
+
test('attachConnection adds a socket without cutting off the ones already watching', async () => {
|
|
226
230
|
await withIsolatedDatabase(() => {
|
|
227
231
|
sessionsDb.createAppSession('app-run-5', 'opencode', '/workspace/demo');
|
|
228
232
|
const firstConnection = new FakeConnection();
|
|
@@ -237,15 +241,42 @@ test('attachConnection reroutes the live stream to a new socket', async () => {
|
|
|
237
241
|
|
|
238
242
|
run.writer.send({ kind: 'stream_delta', provider: 'opencode', sessionId: 'o', content: 'before' });
|
|
239
243
|
|
|
244
|
+
// A second tab on the same session subscribes mid-run.
|
|
240
245
|
const secondConnection = new FakeConnection();
|
|
241
246
|
assert.equal(chatRunRegistry.attachConnection('app-run-5', secondConnection), true);
|
|
242
247
|
run.writer.send({ kind: 'stream_delta', provider: 'opencode', sessionId: 'o', content: 'after' });
|
|
243
248
|
|
|
244
|
-
assert.deepEqual(firstConnection.frames.map((frame) => frame.content), ['before']);
|
|
249
|
+
assert.deepEqual(firstConnection.frames.map((frame) => frame.content), ['before', 'after']);
|
|
245
250
|
assert.deepEqual(secondConnection.frames.map((frame) => frame.content), ['after']);
|
|
246
251
|
});
|
|
247
252
|
});
|
|
248
253
|
|
|
254
|
+
test('a refreshed tab stops receiving once its old socket is closed', async () => {
|
|
255
|
+
await withIsolatedDatabase(() => {
|
|
256
|
+
sessionsDb.createAppSession('app-run-5b', 'opencode', '/workspace/demo');
|
|
257
|
+
const staleConnection = new FakeConnection();
|
|
258
|
+
const run = chatRunRegistry.startRun({
|
|
259
|
+
appSessionId: 'app-run-5b',
|
|
260
|
+
provider: 'opencode',
|
|
261
|
+
providerSessionId: null,
|
|
262
|
+
connection: staleConnection,
|
|
263
|
+
userId: null,
|
|
264
|
+
});
|
|
265
|
+
assert.ok(run);
|
|
266
|
+
|
|
267
|
+
// The page reloads: the original socket closes and the fresh one subscribes.
|
|
268
|
+
staleConnection.readyState = 3;
|
|
269
|
+
const reloadedConnection = new FakeConnection();
|
|
270
|
+
assert.equal(chatRunRegistry.attachConnection('app-run-5b', reloadedConnection), true);
|
|
271
|
+
|
|
272
|
+
run.writer.send({ kind: 'stream_delta', provider: 'opencode', sessionId: 'o', content: 'after' });
|
|
273
|
+
run.writer.send({ kind: 'stream_delta', provider: 'opencode', sessionId: 'o', content: 'later' });
|
|
274
|
+
|
|
275
|
+
assert.deepEqual(staleConnection.frames, []);
|
|
276
|
+
assert.deepEqual(reloadedConnection.frames.map((frame) => frame.content), ['after', 'later']);
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
249
280
|
test('startRun rejects a second concurrent run for the same session', async () => {
|
|
250
281
|
await withIsolatedDatabase(() => {
|
|
251
282
|
sessionsDb.createAppSession('app-run-6', 'opencode', '/workspace/demo');
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import test from 'node:test';
|
|
6
|
+
|
|
7
|
+
import { closeConnection, initializeDatabase, sessionsDb } from '@/modules/database/index.js';
|
|
8
|
+
import {
|
|
9
|
+
broadcastSessionUpserted,
|
|
10
|
+
broadcastSessionUpsertedBatch,
|
|
11
|
+
} from '@/modules/websocket/services/session-upsert-broadcast.service.js';
|
|
12
|
+
import { connectedClients } from '@/modules/websocket/services/websocket-state.service.js';
|
|
13
|
+
|
|
14
|
+
class FakeConnection {
|
|
15
|
+
readyState = 1; // WS_OPEN_STATE
|
|
16
|
+
frames: Array<Record<string, unknown>> = [];
|
|
17
|
+
|
|
18
|
+
send(data: string): void {
|
|
19
|
+
this.frames.push(JSON.parse(data) as Record<string, unknown>);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function withIsolatedDatabase(runTest: () => void | Promise<void>): Promise<void> {
|
|
24
|
+
const previousDatabasePath = process.env.DATABASE_PATH;
|
|
25
|
+
const tempDirectory = await mkdtemp(path.join(tmpdir(), 'session-upsert-broadcast-'));
|
|
26
|
+
|
|
27
|
+
closeConnection();
|
|
28
|
+
process.env.DATABASE_PATH = path.join(tempDirectory, 'auth.db');
|
|
29
|
+
await initializeDatabase();
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
await runTest();
|
|
33
|
+
} finally {
|
|
34
|
+
connectedClients.clear();
|
|
35
|
+
closeConnection();
|
|
36
|
+
if (previousDatabasePath === undefined) {
|
|
37
|
+
delete process.env.DATABASE_PATH;
|
|
38
|
+
} else {
|
|
39
|
+
process.env.DATABASE_PATH = previousDatabasePath;
|
|
40
|
+
}
|
|
41
|
+
await rm(tempDirectory, { recursive: true, force: true });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
test('an upsert always carries the provider session id', async () => {
|
|
46
|
+
await withIsolatedDatabase(async () => {
|
|
47
|
+
sessionsDb.createAppSession('app-1', 'opencode', '/workspace/demo');
|
|
48
|
+
sessionsDb.assignProviderSessionId('app-1', 'native-1');
|
|
49
|
+
|
|
50
|
+
const connection = new FakeConnection();
|
|
51
|
+
connectedClients.add(connection as never);
|
|
52
|
+
|
|
53
|
+
await broadcastSessionUpserted('app-1');
|
|
54
|
+
|
|
55
|
+
assert.equal(connection.frames.length, 1);
|
|
56
|
+
assert.equal(connection.frames[0].kind, 'session_upserted');
|
|
57
|
+
assert.equal(connection.frames[0].sessionId, 'app-1');
|
|
58
|
+
assert.equal(connection.frames[0].providerSessionId, 'native-1');
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('the watcher path resolves a provider-native id to the same canonical event', async () => {
|
|
63
|
+
await withIsolatedDatabase(async () => {
|
|
64
|
+
sessionsDb.createAppSession('app-2', 'opencode', '/workspace/demo');
|
|
65
|
+
sessionsDb.assignProviderSessionId('app-2', 'native-2');
|
|
66
|
+
|
|
67
|
+
const connection = new FakeConnection();
|
|
68
|
+
connectedClients.add(connection as never);
|
|
69
|
+
|
|
70
|
+
// The sessions watcher only ever knows the id written in the transcript.
|
|
71
|
+
await broadcastSessionUpsertedBatch(['native-2']);
|
|
72
|
+
|
|
73
|
+
assert.equal(connection.frames.length, 1);
|
|
74
|
+
assert.equal(connection.frames[0].sessionId, 'app-2');
|
|
75
|
+
assert.equal(connection.frames[0].providerSessionId, 'native-2');
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('a session with no provider id yet reports null rather than omitting the field', async () => {
|
|
80
|
+
await withIsolatedDatabase(async () => {
|
|
81
|
+
sessionsDb.createAppSession('app-3', 'claude', '/workspace/demo');
|
|
82
|
+
|
|
83
|
+
const connection = new FakeConnection();
|
|
84
|
+
connectedClients.add(connection as never);
|
|
85
|
+
|
|
86
|
+
await broadcastSessionUpserted('app-3');
|
|
87
|
+
|
|
88
|
+
assert.equal(connection.frames.length, 1);
|
|
89
|
+
assert.ok('providerSessionId' in connection.frames[0]);
|
|
90
|
+
assert.equal(connection.frames[0].providerSessionId, null);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('an unresolvable id broadcasts nothing', async () => {
|
|
95
|
+
await withIsolatedDatabase(async () => {
|
|
96
|
+
const connection = new FakeConnection();
|
|
97
|
+
connectedClients.add(connection as never);
|
|
98
|
+
|
|
99
|
+
await broadcastSessionUpserted('does-not-exist');
|
|
100
|
+
await broadcastSessionUpsertedBatch(['also-missing']);
|
|
101
|
+
|
|
102
|
+
assert.deepEqual(connection.frames, []);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('a batch delivers every resolvable session and skips the rest', async () => {
|
|
107
|
+
await withIsolatedDatabase(async () => {
|
|
108
|
+
sessionsDb.createAppSession('app-4', 'claude', '/workspace/demo');
|
|
109
|
+
sessionsDb.createAppSession('app-5', 'claude', '/workspace/demo');
|
|
110
|
+
|
|
111
|
+
const connection = new FakeConnection();
|
|
112
|
+
connectedClients.add(connection as never);
|
|
113
|
+
|
|
114
|
+
await broadcastSessionUpsertedBatch(['app-4', 'missing', 'app-5']);
|
|
115
|
+
|
|
116
|
+
assert.deepEqual(
|
|
117
|
+
connection.frames.map((frame) => frame.sessionId),
|
|
118
|
+
['app-4', 'app-5'],
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('a closed socket is skipped', async () => {
|
|
124
|
+
await withIsolatedDatabase(async () => {
|
|
125
|
+
sessionsDb.createAppSession('app-6', 'claude', '/workspace/demo');
|
|
126
|
+
|
|
127
|
+
const open = new FakeConnection();
|
|
128
|
+
const closing = new FakeConnection();
|
|
129
|
+
closing.readyState = 3;
|
|
130
|
+
connectedClients.add(open as never);
|
|
131
|
+
connectedClients.add(closing as never);
|
|
132
|
+
|
|
133
|
+
await broadcastSessionUpserted('app-6');
|
|
134
|
+
|
|
135
|
+
assert.equal(open.frames.length, 1);
|
|
136
|
+
assert.deepEqual(closing.frames, []);
|
|
137
|
+
});
|
|
138
|
+
});
|