@cloudcli-ai/cloudcli 1.37.1 → 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 +1 -1
- package/dist/assets/abnfDiagram-N423BO3Z-CJntskwA.js +1 -0
- package/dist/assets/arc-CIfH4uMN.js +1 -0
- package/dist/assets/architectureDiagram-T3A2C74G-_IpEkukg.js +36 -0
- package/dist/assets/blockDiagram-VBNYF7ZC-BttxrpgZ.js +132 -0
- package/dist/assets/c4Diagram-5PPSVZJV-g51dSRr1.js +10 -0
- package/dist/assets/channel-C5xU9jMw.js +1 -0
- package/dist/assets/chunk-2GRJ4B5K-D_-LbQUa.js +1 -0
- package/dist/assets/chunk-2Q5K7J3B-BtlHzwaO.js +1 -0
- package/dist/assets/chunk-5RXB4S5H-UzkpvpAT.js +231 -0
- package/dist/assets/chunk-5VM5RSS4-BKwx5Gag.js +15 -0
- package/dist/assets/chunk-6Q2QTUOP-D_KEBH5f.js +88 -0
- package/dist/assets/chunk-GF5L2VYU-rK2DGyto.js +206 -0
- package/dist/assets/chunk-JWPE2WC7-CeBl_N9i.js +1 -0
- package/dist/assets/chunk-KBJHAD2P-BiQLWVIr.js +1 -0
- package/dist/assets/chunk-RYQCIY6F-C1knVGV5.js +1 -0
- package/dist/assets/chunk-XXDRQBXY-CtKbJkvF.js +1 -0
- 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-DjX8y40Z.js +1 -0
- package/dist/assets/cynefin-VYW2F7L2-DM6YeRba.js +178 -0
- package/dist/assets/cynefinDiagram-MW4NZA55-wZJ-vy5B.js +62 -0
- package/dist/assets/cytoscape.esm-C10L9Icf.js +331 -0
- package/dist/assets/dagre-VZM6K2ZE-BzP0ejkH.js +4 -0
- package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/dist/assets/diagram-7IWD3JNH-BTL_j23J.js +30 -0
- package/dist/assets/diagram-B4RE2ZJO-Cf1Norgu.js +3 -0
- package/dist/assets/diagram-LBJQPF4R-DPiUEKr-.js +24 -0
- package/dist/assets/diagram-Q27KOJAE-CTcTpiz5.js +24 -0
- package/dist/assets/diagram-UB23O5K3-DPMSdYm0.js +41 -0
- package/dist/assets/ebnfDiagram-BXEA7PRR-7yvD6uiF.js +1 -0
- package/dist/assets/erDiagram-JOGREHBK-CfgeDoV6.js +85 -0
- package/dist/assets/flowDiagram-UKHOOZJN-B27ng5zu.js +156 -0
- package/dist/assets/ganttDiagram-PKOTCBZU-CcC6b_7t.js +292 -0
- package/dist/assets/gitGraphDiagram-DS77QQ5N-BX5kPGrO.js +106 -0
- package/dist/assets/graph-Cx-EqisW.js +1 -0
- package/dist/assets/index-BTDHQ6Id.css +32 -0
- package/dist/assets/index-sFfz8iBR.js +4931 -0
- package/dist/assets/infoDiagram-6WML65LV-CUP-SlN4.js +2 -0
- package/dist/assets/init-Gi6I4Gst.js +1 -0
- package/dist/assets/ishikawaDiagram-WSZJBQD7-BTTDhOpX.js +70 -0
- package/dist/assets/journeyDiagram-NVQOT4AX-ARXcc61m.js +139 -0
- package/dist/assets/kanban-definition-27J2QSJJ-BieMpqMA.js +89 -0
- package/dist/assets/layout-D6BstSzF.js +1 -0
- package/dist/assets/linear-61vxjRnP.js +1 -0
- package/dist/assets/map-CV9uif0g.js +1 -0
- package/dist/assets/mermaid.core-nhnwkfQx.js +312 -0
- package/dist/assets/mindmap-definition-FAOFIHXS-Cm6x5vT1.js +96 -0
- package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/dist/assets/pegDiagram-VL7TDLO6-CcYBq_9H.js +1 -0
- package/dist/assets/pieDiagram-7S7Q4E2Y-CfvIjfzo.js +39 -0
- package/dist/assets/quadrantDiagram-CIZ2JOQS-SgYoWPNb.js +7 -0
- package/dist/assets/railroadDiagram-AXF67PYL-D2Mu5E_f.js +1 -0
- package/dist/assets/requirementDiagram-LRYGKXZP-Azwe_k4Q.js +84 -0
- package/dist/assets/sankeyDiagram-W5VNT64P-DN1Ju3PA.js +40 -0
- package/dist/assets/sequenceDiagram-SI44F4Z6-Bj4qC34S.js +162 -0
- package/dist/assets/server.browser-CtX3Hdl5.js +27 -0
- package/dist/assets/sizeCapture-X5ZJPWSS-Co4PdklG.js +1 -0
- package/dist/assets/stateDiagram-OKZ733FA-nOvL9dRC.js +1 -0
- package/dist/assets/stateDiagram-v2-UEYNNEHI-DNv8ppw7.js +1 -0
- package/dist/assets/swimlanes-SLNWSIFB-CCK3wMid.js +2 -0
- package/dist/assets/swimlanesDiagram-ULZ7WXOC-DnfbNQEp.js +8 -0
- package/dist/assets/timeline-definition-Z64GVDOM-BC29RNFm.js +120 -0
- package/dist/assets/{vendor-codemirror-OwyKSvPE.js → vendor-codemirror-CYrSt7-U.js} +18 -18
- package/dist/assets/vennDiagram-T6HMQDX7-pZlZCtEo.js +34 -0
- package/dist/assets/wardleyDiagram-T6FBY63Y-De8k2-zM.js +78 -0
- package/dist/assets/xychartDiagram-ELKLHX3M-Bk8ykH0w.js +7 -0
- package/dist/index.html +3 -3
- package/dist-server/server/index.js +8 -5
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/modules/agent/agent.module.js +2 -1
- package/dist-server/server/modules/agent/agent.module.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/auth/auth.middleware.js +1 -1
- package/dist-server/server/modules/auth/auth.middleware.js.map +1 -1
- package/dist-server/server/modules/browser-use/browser-use-runtime.js +10 -0
- package/dist-server/server/modules/browser-use/browser-use-runtime.js.map +1 -0
- 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/browser-use/browser-use.service.js +3 -6
- package/dist-server/server/modules/browser-use/browser-use.service.js.map +1 -1
- package/dist-server/server/modules/browser-use/index.js +2 -0
- package/dist-server/server/modules/browser-use/index.js.map +1 -1
- package/dist-server/server/modules/cli/cli.js +9 -3
- package/dist-server/server/modules/cli/cli.js.map +1 -1
- package/dist-server/server/modules/cli/cli.module.js +0 -1
- package/dist-server/server/modules/cli/cli.module.js.map +1 -1
- package/dist-server/server/modules/cli/cli.service.js +0 -18
- package/dist-server/server/modules/cli/cli.service.js.map +1 -1
- package/dist-server/server/modules/cli/tests/cli-environment-bootstrap.test.js +154 -0
- package/dist-server/server/modules/cli/tests/cli-environment-bootstrap.test.js.map +1 -0
- package/dist-server/server/modules/cli/tests/cli.service.test.js +0 -3
- package/dist-server/server/modules/cli/tests/cli.service.test.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/file-tree/file-tree.service.js +18 -10
- package/dist-server/server/modules/file-tree/file-tree.service.js.map +1 -1
- package/dist-server/server/modules/file-tree/tests/file-tree.service.test.js +62 -11
- package/dist-server/server/modules/file-tree/tests/file-tree.service.test.js.map +1 -1
- package/dist-server/server/modules/git/git-branch.service.js +15 -0
- package/dist-server/server/modules/git/git-branch.service.js.map +1 -0
- package/dist-server/server/modules/git/git.routes.js +15 -7
- package/dist-server/server/modules/git/git.routes.js.map +1 -1
- package/dist-server/server/modules/git/tests/git-branch.service.test.js +52 -0
- package/dist-server/server/modules/git/tests/git-branch.service.test.js.map +1 -0
- package/dist-server/server/modules/git/tests/git-init.routes.test.js +84 -0
- package/dist-server/server/modules/git/tests/git-init.routes.test.js.map +1 -1
- package/dist-server/server/modules/notifications/index.js +3 -1
- package/dist-server/server/modules/notifications/index.js.map +1 -1
- package/dist-server/server/modules/notifications/services/notification-orchestrator.service.js +22 -1
- package/dist-server/server/modules/notifications/services/notification-orchestrator.service.js.map +1 -1
- package/dist-server/server/modules/projects/services/project-delete.service.js +12 -1
- package/dist-server/server/modules/projects/services/project-delete.service.js.map +1 -1
- package/dist-server/server/modules/projects/services/project-star.service.js +2 -2
- package/dist-server/server/modules/projects/services/project-star.service.js.map +1 -1
- package/dist-server/server/modules/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 +422 -86
- 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 +29 -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-conversations-search.service.js +63 -7
- package/dist-server/server/modules/providers/services/session-conversations-search.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/session-history-cache.service.js +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/provider.routes.test.js +35 -0
- package/dist-server/server/modules/providers/tests/provider.routes.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/session-fork.test.js +113 -0
- package/dist-server/server/modules/providers/tests/session-fork.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/session-history-cache.service.test.js +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 +42 -0
- package/dist-server/server/shared/utils.js.map +1 -1
- package/package.json +23 -16
- package/scripts/promote-dist-server.mjs +50 -0
- package/server/index.ts +12 -6
- package/server/modules/agent/agent.module.ts +2 -1
- package/server/modules/agent/agent.routes.ts +2 -2
- package/server/modules/agent/tests/agent.routes.test.ts +33 -1
- package/server/modules/auth/auth.middleware.ts +2 -2
- package/server/modules/browser-use/browser-use-runtime.ts +10 -0
- package/server/modules/browser-use/browser-use.routes.ts +96 -33
- package/server/modules/browser-use/browser-use.service.ts +5 -8
- package/server/modules/browser-use/index.ts +3 -0
- package/server/modules/cli/cli.module.ts +0 -1
- package/server/modules/cli/cli.service.ts +0 -20
- package/server/modules/cli/cli.ts +9 -3
- package/server/modules/cli/tests/cli-environment-bootstrap.test.ts +186 -0
- package/server/modules/cli/tests/cli.service.test.ts +0 -3
- 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/file-tree/file-tree.service.ts +21 -10
- package/server/modules/file-tree/tests/file-tree.service.test.ts +65 -11
- package/server/modules/git/git-branch.service.ts +43 -0
- package/server/modules/git/git.routes.ts +16 -9
- package/server/modules/git/tests/git-branch.service.test.ts +60 -0
- package/server/modules/git/tests/git-init.routes.test.ts +83 -0
- package/server/modules/notifications/index.ts +2 -0
- package/server/modules/notifications/services/notification-orchestrator.service.js +24 -1
- 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 +463 -93
- 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 +36 -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-conversations-search.service.ts +94 -7
- 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/provider.routes.test.ts +61 -0
- 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 -3
- package/server/shared/utils.ts +50 -0
- package/server/tsconfig.json +6 -2
- package/README.de.md +0 -257
- package/README.ja.md +0 -249
- package/README.ko.md +0 -249
- package/README.ru.md +0 -257
- package/README.tr.md +0 -258
- package/README.zh-CN.md +0 -249
- package/README.zh-TW.md +0 -249
- package/dist/assets/index-D3jfzuKN.css +0 -32
- package/dist/assets/index-DTMWjJj8.js +0 -1550
- package/dist-server/server/constants/config.js +0 -6
- package/dist-server/server/constants/config.js.map +0 -1
- package/server/constants/config.ts +0 -5
|
@@ -6,7 +6,10 @@ import test from 'node:test';
|
|
|
6
6
|
|
|
7
7
|
import Database from 'better-sqlite3';
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
createProviderTokenUsageService,
|
|
11
|
+
summarizeClaudeTokenUsage,
|
|
12
|
+
} from '@/modules/providers/services/provider-token-usage.service.js';
|
|
10
13
|
import { AppError } from '@/shared/utils.js';
|
|
11
14
|
|
|
12
15
|
function createSessionRow(overrides: Record<string, unknown> = {}) {
|
|
@@ -19,6 +22,7 @@ function createSessionRow(overrides: Record<string, unknown> = {}) {
|
|
|
19
22
|
custom_name: null,
|
|
20
23
|
model: null,
|
|
21
24
|
effort: null,
|
|
25
|
+
forked_from_session_id: null,
|
|
22
26
|
isArchived: 0,
|
|
23
27
|
created_at: '2026-01-01T00:00:00.000Z',
|
|
24
28
|
updated_at: '2026-01-01T00:00:00.000Z',
|
|
@@ -184,3 +188,186 @@ test('token usage reports SESSION_NOT_FOUND for an unknown app session id', asyn
|
|
|
184
188
|
),
|
|
185
189
|
);
|
|
186
190
|
});
|
|
191
|
+
|
|
192
|
+
test('the Claude summarizer reads the newest assistant turn, not the whole conversation', () => {
|
|
193
|
+
const entries = [
|
|
194
|
+
{ type: 'assistant', message: { usage: { input_tokens: 5, cache_read_input_tokens: 1000, output_tokens: 50 } } },
|
|
195
|
+
{ type: 'user', message: { role: 'user', content: 'next' } },
|
|
196
|
+
// The newest turn's prompt is the whole context, so its cache_read already
|
|
197
|
+
// includes everything before it. Summing turns would double-count.
|
|
198
|
+
{ type: 'assistant', message: { usage: { input_tokens: 3, cache_read_input_tokens: 4000, cache_creation_input_tokens: 100, output_tokens: 80 } } },
|
|
199
|
+
];
|
|
200
|
+
|
|
201
|
+
assert.deepEqual(summarizeClaudeTokenUsage(entries, '200000'), {
|
|
202
|
+
used: 4183,
|
|
203
|
+
total: 200_000,
|
|
204
|
+
inputTokens: 4103,
|
|
205
|
+
outputTokens: 80,
|
|
206
|
+
cacheReadTokens: 4000,
|
|
207
|
+
cacheCreationTokens: 100,
|
|
208
|
+
cacheTokens: 4100,
|
|
209
|
+
breakdown: { input: 4103, output: 80 },
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test('the Claude summarizer skips synthetic rows that carry an all-zero usage block', () => {
|
|
214
|
+
// Interrupts, API errors and "No response requested." are written as
|
|
215
|
+
// assistant rows with a fully zeroed usage block. Reading one as the newest
|
|
216
|
+
// turn dropped the composer counter to 0 until the next turn pushed it back.
|
|
217
|
+
const entries = [
|
|
218
|
+
{ type: 'assistant', message: { usage: { input_tokens: 3, cache_read_input_tokens: 4000, output_tokens: 80 } } },
|
|
219
|
+
{
|
|
220
|
+
type: 'assistant',
|
|
221
|
+
message: {
|
|
222
|
+
model: '<synthetic>',
|
|
223
|
+
usage: {
|
|
224
|
+
input_tokens: 0,
|
|
225
|
+
output_tokens: 0,
|
|
226
|
+
cache_creation_input_tokens: 0,
|
|
227
|
+
cache_read_input_tokens: 0,
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
];
|
|
232
|
+
|
|
233
|
+
assert.equal(summarizeClaudeTokenUsage(entries, '200000').used, 4083);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test('the Claude summarizer skips a subagent sidechain turn', () => {
|
|
237
|
+
// A sidechain turn reports the subagent's own context window. Reading it
|
|
238
|
+
// made the counter drop to the subagent's number mid-run.
|
|
239
|
+
const entries = [
|
|
240
|
+
{ type: 'assistant', message: { usage: { input_tokens: 3, cache_read_input_tokens: 4000, output_tokens: 80 } } },
|
|
241
|
+
{
|
|
242
|
+
type: 'assistant',
|
|
243
|
+
isSidechain: true,
|
|
244
|
+
message: { usage: { input_tokens: 10, cache_read_input_tokens: 900, output_tokens: 5 } },
|
|
245
|
+
},
|
|
246
|
+
];
|
|
247
|
+
|
|
248
|
+
assert.equal(summarizeClaudeTokenUsage(entries, '200000').used, 4083);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test('the Claude summarizer reports zero for a transcript with no assistant turn yet', () => {
|
|
252
|
+
const usage = summarizeClaudeTokenUsage([{ type: 'user', message: { role: 'user', content: 'hi' } }], '200000');
|
|
253
|
+
|
|
254
|
+
assert.equal(usage.used, 0);
|
|
255
|
+
assert.equal(usage.total, 200_000);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
/** Padding rows large enough to push earlier rows out of the 4MB tail window. */
|
|
259
|
+
function paddingLines(totalBytes: number): string {
|
|
260
|
+
const line = JSON.stringify({ type: 'attachment', filler: 'x'.repeat(4096) });
|
|
261
|
+
return Array.from({ length: Math.ceil(totalBytes / line.length) }, () => line).join('\n');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
test('Claude token usage reads only the tail of a large transcript', async () => {
|
|
265
|
+
const tempDirectory = await mkdtemp(path.join(tmpdir(), 'provider-token-usage-claude-tail-'));
|
|
266
|
+
const sessionFilePath = path.join(tempDirectory, 'provider-session.jsonl');
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
await writeFile(sessionFilePath, [
|
|
270
|
+
paddingLines(5 * 1024 * 1024),
|
|
271
|
+
JSON.stringify({ type: 'assistant', message: { usage: { input_tokens: 7, output_tokens: 2 } } }),
|
|
272
|
+
].join('\n'));
|
|
273
|
+
|
|
274
|
+
const service = createProviderTokenUsageService({
|
|
275
|
+
getSessionById: () => createSessionRow({ jsonl_path: sessionFilePath }),
|
|
276
|
+
getClaudeContextWindow: () => '180000',
|
|
277
|
+
// Reading the whole file here would defeat the tail read; fail loudly.
|
|
278
|
+
readTextFile: () => { throw new Error('full read must not happen when the tail has usage'); },
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
const usage = await service.getSessionTokenUsage('app-session');
|
|
282
|
+
assert.equal(usage.inputTokens, 7);
|
|
283
|
+
assert.equal(usage.outputTokens, 2);
|
|
284
|
+
} finally {
|
|
285
|
+
await rm(tempDirectory, { recursive: true, force: true });
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test('Claude token usage falls back to the whole file when the tail has no usage row', async () => {
|
|
290
|
+
const tempDirectory = await mkdtemp(path.join(tmpdir(), 'provider-token-usage-claude-fallback-'));
|
|
291
|
+
const sessionFilePath = path.join(tempDirectory, 'provider-session.jsonl');
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
await writeFile(sessionFilePath, [
|
|
295
|
+
JSON.stringify({ type: 'assistant', message: { usage: { input_tokens: 11, output_tokens: 3 } } }),
|
|
296
|
+
paddingLines(5 * 1024 * 1024),
|
|
297
|
+
].join('\n'));
|
|
298
|
+
|
|
299
|
+
const service = createProviderTokenUsageService({
|
|
300
|
+
getSessionById: () => createSessionRow({ jsonl_path: sessionFilePath }),
|
|
301
|
+
getClaudeContextWindow: () => '180000',
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
const usage = await service.getSessionTokenUsage('app-session');
|
|
305
|
+
assert.equal(usage.inputTokens, 11);
|
|
306
|
+
assert.equal(usage.outputTokens, 3);
|
|
307
|
+
} finally {
|
|
308
|
+
await rm(tempDirectory, { recursive: true, force: true });
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
test('Codex token usage reads only the tail of a large rollout', async () => {
|
|
313
|
+
const tempDirectory = await mkdtemp(path.join(tmpdir(), 'provider-token-usage-codex-tail-'));
|
|
314
|
+
const sessionFilePath = path.join(tempDirectory, 'rollout-provider-session.jsonl');
|
|
315
|
+
|
|
316
|
+
try {
|
|
317
|
+
await writeFile(sessionFilePath, [
|
|
318
|
+
paddingLines(5 * 1024 * 1024),
|
|
319
|
+
JSON.stringify({
|
|
320
|
+
type: 'event_msg',
|
|
321
|
+
payload: {
|
|
322
|
+
type: 'token_count',
|
|
323
|
+
info: {
|
|
324
|
+
total_token_usage: { input_tokens: 21, output_tokens: 8, total_tokens: 29 },
|
|
325
|
+
model_context_window: 150_000,
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
}),
|
|
329
|
+
].join('\n'));
|
|
330
|
+
|
|
331
|
+
const service = createProviderTokenUsageService({
|
|
332
|
+
getSessionById: () => createSessionRow({ provider: 'codex', jsonl_path: sessionFilePath }),
|
|
333
|
+
readTextFile: () => { throw new Error('full read must not happen when the tail has usage'); },
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
assert.deepEqual(await service.getSessionTokenUsage('app-session'), {
|
|
337
|
+
used: 29,
|
|
338
|
+
total: 150_000,
|
|
339
|
+
inputTokens: 21,
|
|
340
|
+
outputTokens: 8,
|
|
341
|
+
breakdown: { input: 21, output: 8 },
|
|
342
|
+
});
|
|
343
|
+
} finally {
|
|
344
|
+
await rm(tempDirectory, { recursive: true, force: true });
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test('Codex token usage falls back to the whole file when the tail has no token_count row', async () => {
|
|
349
|
+
const tempDirectory = await mkdtemp(path.join(tmpdir(), 'provider-token-usage-codex-fallback-'));
|
|
350
|
+
const sessionFilePath = path.join(tempDirectory, 'rollout-provider-session.jsonl');
|
|
351
|
+
|
|
352
|
+
try {
|
|
353
|
+
await writeFile(sessionFilePath, [
|
|
354
|
+
JSON.stringify({
|
|
355
|
+
type: 'event_msg',
|
|
356
|
+
payload: {
|
|
357
|
+
type: 'token_count',
|
|
358
|
+
info: { total_token_usage: { input_tokens: 5, output_tokens: 1, total_tokens: 6 } },
|
|
359
|
+
},
|
|
360
|
+
}),
|
|
361
|
+
paddingLines(5 * 1024 * 1024),
|
|
362
|
+
].join('\n'));
|
|
363
|
+
|
|
364
|
+
const service = createProviderTokenUsageService({
|
|
365
|
+
getSessionById: () => createSessionRow({ provider: 'codex', jsonl_path: sessionFilePath }),
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
const usage = await service.getSessionTokenUsage('app-session');
|
|
369
|
+
assert.equal(usage.used, 6);
|
|
370
|
+
} finally {
|
|
371
|
+
await rm(tempDirectory, { recursive: true, force: true });
|
|
372
|
+
}
|
|
373
|
+
});
|
|
@@ -78,6 +78,67 @@ test('session creation route names a CloudCLI session from the initial message',
|
|
|
78
78
|
});
|
|
79
79
|
});
|
|
80
80
|
|
|
81
|
+
test('conversation search streams title matches before transcript results', async () => {
|
|
82
|
+
await withProviderServer(async (baseUrl, workspacePath) => {
|
|
83
|
+
sessionsDb.createAppSession(
|
|
84
|
+
'title-only-session',
|
|
85
|
+
'codex',
|
|
86
|
+
workspacePath,
|
|
87
|
+
'Release planning notes',
|
|
88
|
+
);
|
|
89
|
+
const transcriptPath = path.join(path.dirname(workspacePath), 'codex-search.jsonl');
|
|
90
|
+
await writeFile(transcriptPath, `${JSON.stringify({
|
|
91
|
+
type: 'event_msg',
|
|
92
|
+
timestamp: '2026-08-12T09:00:00.000Z',
|
|
93
|
+
payload: {
|
|
94
|
+
type: 'user_message',
|
|
95
|
+
kind: 'plain',
|
|
96
|
+
message: 'Release planning also appears in this conversation.',
|
|
97
|
+
},
|
|
98
|
+
})}\n`);
|
|
99
|
+
sessionsDb.createSession(
|
|
100
|
+
'transcript-session',
|
|
101
|
+
'codex',
|
|
102
|
+
workspacePath,
|
|
103
|
+
'Unrelated session',
|
|
104
|
+
undefined,
|
|
105
|
+
undefined,
|
|
106
|
+
transcriptPath,
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const response = await fetch(
|
|
110
|
+
`${baseUrl}/api/providers/search/sessions?q=release%20planning&limit=50`,
|
|
111
|
+
);
|
|
112
|
+
const eventStream = await response.text();
|
|
113
|
+
const titleEventIndex = eventStream.indexOf('event: title-results');
|
|
114
|
+
const conversationEventIndex = eventStream.indexOf('event: result');
|
|
115
|
+
const doneEventIndex = eventStream.indexOf('event: done');
|
|
116
|
+
|
|
117
|
+
assert.equal(response.status, 200);
|
|
118
|
+
assert.ok(titleEventIndex >= 0);
|
|
119
|
+
assert.ok(conversationEventIndex > titleEventIndex);
|
|
120
|
+
assert.ok(doneEventIndex > titleEventIndex);
|
|
121
|
+
|
|
122
|
+
const titleDataLine = eventStream
|
|
123
|
+
.slice(titleEventIndex, conversationEventIndex)
|
|
124
|
+
.split('\n')
|
|
125
|
+
.find((line) => line.startsWith('data: '));
|
|
126
|
+
assert.ok(titleDataLine);
|
|
127
|
+
|
|
128
|
+
const titlePayload = JSON.parse(titleDataLine.slice('data: '.length)) as {
|
|
129
|
+
titleResults: Array<{
|
|
130
|
+
sessionId: string;
|
|
131
|
+
sessionTitle: string;
|
|
132
|
+
provider: string;
|
|
133
|
+
}>;
|
|
134
|
+
};
|
|
135
|
+
assert.equal(titlePayload.titleResults.length, 1);
|
|
136
|
+
assert.equal(titlePayload.titleResults[0]?.sessionId, 'title-only-session');
|
|
137
|
+
assert.equal(titlePayload.titleResults[0]?.sessionTitle, 'Release planning notes');
|
|
138
|
+
assert.equal(titlePayload.titleResults[0]?.provider, 'codex');
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
81
142
|
test('reasoning effort is persisted and returned with the active session model', async () => {
|
|
82
143
|
await withProviderServer(async (baseUrl, workspacePath) => {
|
|
83
144
|
sessionsDb.createAppSession('effort-session', 'codex', workspacePath);
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import os 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 { providerRegistry } from '@/modules/providers/provider.registry.js';
|
|
9
|
+
import { sessionsService } from '@/modules/providers/services/sessions.service.js';
|
|
10
|
+
import type { IProviderFork } from '@/shared/interfaces.js';
|
|
11
|
+
|
|
12
|
+
const SOURCE_ID = 'fork-source';
|
|
13
|
+
|
|
14
|
+
type ForkCall = {
|
|
15
|
+
providerSessionId: string;
|
|
16
|
+
jsonlPath: string;
|
|
17
|
+
upToAnchorId?: string;
|
|
18
|
+
title?: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
async function withForkableClaude(
|
|
22
|
+
runTest: (context: { calls: ForkCall[]; directory: string }) => Promise<void>,
|
|
23
|
+
options: { disableFork?: boolean } = {},
|
|
24
|
+
): Promise<void> {
|
|
25
|
+
const previousDatabasePath = process.env.DATABASE_PATH;
|
|
26
|
+
const directory = await mkdtemp(path.join(os.tmpdir(), 'session-fork-'));
|
|
27
|
+
const sourcePath = path.join(directory, 'native-source.jsonl');
|
|
28
|
+
const forkedPath = path.join(directory, 'native-fork.jsonl');
|
|
29
|
+
await writeFile(sourcePath, '{}\n', 'utf8');
|
|
30
|
+
await writeFile(forkedPath, '{}\n', 'utf8');
|
|
31
|
+
|
|
32
|
+
closeConnection();
|
|
33
|
+
process.env.DATABASE_PATH = path.join(directory, 'auth.db');
|
|
34
|
+
await initializeDatabase();
|
|
35
|
+
|
|
36
|
+
const calls: ForkCall[] = [];
|
|
37
|
+
const claude = providerRegistry.resolveProvider('claude') as { fork?: IProviderFork };
|
|
38
|
+
const realFork = claude.fork;
|
|
39
|
+
const replacement = options.disableFork
|
|
40
|
+
? undefined
|
|
41
|
+
: {
|
|
42
|
+
forkSession: async (input: ForkCall) => {
|
|
43
|
+
calls.push(input);
|
|
44
|
+
return { providerSessionId: 'native-fork', jsonlPath: forkedPath };
|
|
45
|
+
},
|
|
46
|
+
} as IProviderFork;
|
|
47
|
+
|
|
48
|
+
Object.defineProperty(claude, 'fork', { value: replacement, configurable: true, writable: true });
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
await runTest({ calls, directory });
|
|
52
|
+
} finally {
|
|
53
|
+
Object.defineProperty(claude, 'fork', { value: realFork, configurable: true, writable: true });
|
|
54
|
+
closeConnection();
|
|
55
|
+
if (previousDatabasePath === undefined) {
|
|
56
|
+
delete process.env.DATABASE_PATH;
|
|
57
|
+
} else {
|
|
58
|
+
process.env.DATABASE_PATH = previousDatabasePath;
|
|
59
|
+
}
|
|
60
|
+
await rm(directory, { recursive: true, force: true });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function seedSource(directory: string): void {
|
|
65
|
+
const now = new Date().toISOString();
|
|
66
|
+
sessionsDb.createSession(
|
|
67
|
+
SOURCE_ID,
|
|
68
|
+
'claude',
|
|
69
|
+
directory,
|
|
70
|
+
'Original session',
|
|
71
|
+
now,
|
|
72
|
+
now,
|
|
73
|
+
path.join(directory, 'native-source.jsonl'),
|
|
74
|
+
);
|
|
75
|
+
sessionsDb.assignProviderSessionId(SOURCE_ID, 'native-source');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
test('a fork becomes an independent session that points back at its source', async () => {
|
|
79
|
+
await withForkableClaude(async ({ calls, directory }) => {
|
|
80
|
+
seedSource(directory);
|
|
81
|
+
|
|
82
|
+
const result = await sessionsService.forkSessionById(SOURCE_ID, { upToAnchorId: 'uuid-3' });
|
|
83
|
+
|
|
84
|
+
assert.equal(calls.length, 1);
|
|
85
|
+
assert.equal(calls[0].providerSessionId, 'native-source');
|
|
86
|
+
assert.equal(calls[0].upToAnchorId, 'uuid-3');
|
|
87
|
+
|
|
88
|
+
const forked = sessionsDb.getSessionById(result.sessionId);
|
|
89
|
+
assert.ok(forked);
|
|
90
|
+
// Written before the watcher can see the new file, so it is never indexed
|
|
91
|
+
// a second time under its provider-native id.
|
|
92
|
+
assert.equal(forked?.provider_session_id, 'native-fork');
|
|
93
|
+
assert.equal(forked?.jsonl_path, path.join(directory, 'native-fork.jsonl'));
|
|
94
|
+
assert.equal(forked?.forked_from_session_id, SOURCE_ID);
|
|
95
|
+
assert.equal(forked?.custom_name, 'Original session (fork)');
|
|
96
|
+
|
|
97
|
+
// The source is untouched: this is "try two approaches", not a move.
|
|
98
|
+
const source = sessionsDb.getSessionById(SOURCE_ID);
|
|
99
|
+
assert.equal(source?.provider_session_id, 'native-source');
|
|
100
|
+
assert.equal(source?.custom_name, 'Original session');
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('a fork inherits the model and effort of the conversation it branched from', async () => {
|
|
105
|
+
await withForkableClaude(async ({ directory }) => {
|
|
106
|
+
seedSource(directory);
|
|
107
|
+
sessionsDb.setSessionModel(SOURCE_ID, 'claude-opus-5');
|
|
108
|
+
sessionsDb.setSessionEffort(SOURCE_ID, 'xhigh');
|
|
109
|
+
|
|
110
|
+
const result = await sessionsService.forkSessionById(SOURCE_ID);
|
|
111
|
+
|
|
112
|
+
const forked = sessionsDb.getSessionById(result.sessionId);
|
|
113
|
+
assert.equal(forked?.model, 'claude-opus-5');
|
|
114
|
+
assert.equal(forked?.effort, 'xhigh');
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('forking replaces a row the watcher already made for the new transcript', async () => {
|
|
119
|
+
await withForkableClaude(async ({ directory }) => {
|
|
120
|
+
seedSource(directory);
|
|
121
|
+
// The watcher wins the race and indexes the fork under its native id.
|
|
122
|
+
const now = new Date().toISOString();
|
|
123
|
+
sessionsDb.createSession(
|
|
124
|
+
'native-fork',
|
|
125
|
+
'claude',
|
|
126
|
+
directory,
|
|
127
|
+
'Indexed by the watcher',
|
|
128
|
+
now,
|
|
129
|
+
now,
|
|
130
|
+
path.join(directory, 'native-fork.jsonl'),
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
const result = await sessionsService.forkSessionById(SOURCE_ID);
|
|
134
|
+
|
|
135
|
+
assert.equal(sessionsDb.getSessionById('native-fork'), null);
|
|
136
|
+
assert.equal(sessionsDb.getSessionById(result.sessionId)?.provider_session_id, 'native-fork');
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('a session with no transcript yet cannot be forked', async () => {
|
|
141
|
+
await withForkableClaude(async ({ directory }) => {
|
|
142
|
+
// An app-created session that has never run: no provider id, no transcript.
|
|
143
|
+
sessionsDb.createAppSession('never-ran', 'claude', directory, 'Never ran');
|
|
144
|
+
|
|
145
|
+
await assert.rejects(
|
|
146
|
+
() => sessionsService.forkSessionById('never-ran'),
|
|
147
|
+
(error: Error & { code?: string }) => error.code === 'FORK_SOURCE_NOT_READY',
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('a provider without the capability is refused rather than silently ignored', async () => {
|
|
153
|
+
await withForkableClaude(
|
|
154
|
+
async ({ directory }) => {
|
|
155
|
+
seedSource(directory);
|
|
156
|
+
|
|
157
|
+
await assert.rejects(
|
|
158
|
+
() => sessionsService.forkSessionById(SOURCE_ID),
|
|
159
|
+
(error: Error & { code?: string }) => error.code === 'FORK_NOT_SUPPORTED',
|
|
160
|
+
);
|
|
161
|
+
},
|
|
162
|
+
{ disableFork: true },
|
|
163
|
+
);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('forking a session that does not exist is a 404', async () => {
|
|
167
|
+
await withForkableClaude(async () => {
|
|
168
|
+
await assert.rejects(
|
|
169
|
+
() => sessionsService.forkSessionById('no-such-session'),
|
|
170
|
+
(error: Error & { code?: string }) => error.code === 'SESSION_NOT_FOUND',
|
|
171
|
+
);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { appendFile, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import test from 'node:test';
|
|
6
|
+
|
|
7
|
+
import { createSessionHistoryCache } from '@/modules/providers/services/session-history-cache.service.js';
|
|
8
|
+
import type { FetchHistoryResult, NormalizedMessage } from '@/shared/types.js';
|
|
9
|
+
|
|
10
|
+
function historyResult(marker: string): FetchHistoryResult {
|
|
11
|
+
const message = {
|
|
12
|
+
id: marker,
|
|
13
|
+
sessionId: 'session',
|
|
14
|
+
timestamp: '2026-01-01T00:00:00.000Z',
|
|
15
|
+
provider: 'claude',
|
|
16
|
+
kind: 'text',
|
|
17
|
+
role: 'user',
|
|
18
|
+
content: marker,
|
|
19
|
+
} as NormalizedMessage;
|
|
20
|
+
|
|
21
|
+
return { messages: [message], total: 1, hasMore: false, offset: 0, limit: null };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function withTranscriptFile(
|
|
25
|
+
runTest: (transcriptPath: string) => Promise<void>,
|
|
26
|
+
): Promise<void> {
|
|
27
|
+
const tempDirectory = await mkdtemp(path.join(os.tmpdir(), 'session-history-cache-'));
|
|
28
|
+
const transcriptPath = path.join(tempDirectory, 'session.jsonl');
|
|
29
|
+
await writeFile(transcriptPath, '{"type":"user"}\n', 'utf8');
|
|
30
|
+
try {
|
|
31
|
+
await runTest(transcriptPath);
|
|
32
|
+
} finally {
|
|
33
|
+
await rm(tempDirectory, { recursive: true, force: true });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
test('an unchanged transcript file is loaded once and then served from cache', async () => {
|
|
38
|
+
await withTranscriptFile(async (transcriptPath) => {
|
|
39
|
+
const cache = createSessionHistoryCache();
|
|
40
|
+
let loads = 0;
|
|
41
|
+
const loadFull = async () => {
|
|
42
|
+
loads += 1;
|
|
43
|
+
return historyResult(`load-${loads}`);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const first = await cache.getFullHistory({ sessionId: 's1', transcriptPath, loadFull });
|
|
47
|
+
const second = await cache.getFullHistory({ sessionId: 's1', transcriptPath, loadFull });
|
|
48
|
+
|
|
49
|
+
assert.equal(loads, 1);
|
|
50
|
+
assert.equal(second, first);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('growing the transcript file invalidates the cached load', async () => {
|
|
55
|
+
await withTranscriptFile(async (transcriptPath) => {
|
|
56
|
+
const cache = createSessionHistoryCache();
|
|
57
|
+
let loads = 0;
|
|
58
|
+
const loadFull = async () => {
|
|
59
|
+
loads += 1;
|
|
60
|
+
return historyResult(`load-${loads}`);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
await cache.getFullHistory({ sessionId: 's1', transcriptPath, loadFull });
|
|
64
|
+
await appendFile(transcriptPath, '{"type":"assistant"}\n', 'utf8');
|
|
65
|
+
const afterAppend = await cache.getFullHistory({ sessionId: 's1', transcriptPath, loadFull });
|
|
66
|
+
|
|
67
|
+
assert.equal(loads, 2);
|
|
68
|
+
assert.equal(afterAppend?.messages[0]?.id, 'load-2');
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('a missing transcript path or file bypasses the cache', async () => {
|
|
73
|
+
const cache = createSessionHistoryCache();
|
|
74
|
+
let loads = 0;
|
|
75
|
+
const loadFull = async () => {
|
|
76
|
+
loads += 1;
|
|
77
|
+
return historyResult('unused');
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
assert.equal(await cache.getFullHistory({ sessionId: 's1', transcriptPath: null, loadFull }), null);
|
|
81
|
+
assert.equal(
|
|
82
|
+
await cache.getFullHistory({
|
|
83
|
+
sessionId: 's1',
|
|
84
|
+
transcriptPath: path.join(os.tmpdir(), 'session-history-cache-does-not-exist.jsonl'),
|
|
85
|
+
loadFull,
|
|
86
|
+
}),
|
|
87
|
+
null,
|
|
88
|
+
);
|
|
89
|
+
assert.equal(loads, 0);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('concurrent misses share a single load', async () => {
|
|
93
|
+
await withTranscriptFile(async (transcriptPath) => {
|
|
94
|
+
const cache = createSessionHistoryCache();
|
|
95
|
+
let loads = 0;
|
|
96
|
+
let release: (() => void) | undefined;
|
|
97
|
+
const gate = new Promise<void>((resolve) => { release = resolve; });
|
|
98
|
+
const loadFull = async () => {
|
|
99
|
+
loads += 1;
|
|
100
|
+
await gate;
|
|
101
|
+
return historyResult(`load-${loads}`);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const firstRequest = cache.getFullHistory({ sessionId: 's1', transcriptPath, loadFull });
|
|
105
|
+
const secondRequest = cache.getFullHistory({ sessionId: 's1', transcriptPath, loadFull });
|
|
106
|
+
release!();
|
|
107
|
+
const [first, second] = await Promise.all([firstRequest, secondRequest]);
|
|
108
|
+
|
|
109
|
+
assert.equal(loads, 1);
|
|
110
|
+
assert.equal(second, first);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('the oldest entries are evicted over budget, but the newest survives alone', async () => {
|
|
115
|
+
const tempDirectory = await mkdtemp(path.join(os.tmpdir(), 'session-history-cache-evict-'));
|
|
116
|
+
try {
|
|
117
|
+
const firstPath = path.join(tempDirectory, 'first.jsonl');
|
|
118
|
+
const secondPath = path.join(tempDirectory, 'second.jsonl');
|
|
119
|
+
// Each file is 100 bytes, so a 150-byte budget holds exactly one entry —
|
|
120
|
+
// and the newest entry stays cached even though it exceeds nothing alone.
|
|
121
|
+
await writeFile(firstPath, 'x'.repeat(100), 'utf8');
|
|
122
|
+
await writeFile(secondPath, 'y'.repeat(100), 'utf8');
|
|
123
|
+
|
|
124
|
+
const cache = createSessionHistoryCache(150);
|
|
125
|
+
const loadsBySession = new Map<string, number>();
|
|
126
|
+
const loaderFor = (sessionId: string) => async () => {
|
|
127
|
+
loadsBySession.set(sessionId, (loadsBySession.get(sessionId) ?? 0) + 1);
|
|
128
|
+
return historyResult(sessionId);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
await cache.getFullHistory({ sessionId: 's1', transcriptPath: firstPath, loadFull: loaderFor('s1') });
|
|
132
|
+
await cache.getFullHistory({ sessionId: 's2', transcriptPath: secondPath, loadFull: loaderFor('s2') });
|
|
133
|
+
|
|
134
|
+
// s2 is still cached; s1 was evicted to fit the budget.
|
|
135
|
+
await cache.getFullHistory({ sessionId: 's2', transcriptPath: secondPath, loadFull: loaderFor('s2') });
|
|
136
|
+
await cache.getFullHistory({ sessionId: 's1', transcriptPath: firstPath, loadFull: loaderFor('s1') });
|
|
137
|
+
|
|
138
|
+
assert.equal(loadsBySession.get('s2'), 1);
|
|
139
|
+
assert.equal(loadsBySession.get('s1'), 2);
|
|
140
|
+
} finally {
|
|
141
|
+
await rm(tempDirectory, { recursive: true, force: true });
|
|
142
|
+
}
|
|
143
|
+
});
|