@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
|
@@ -11,6 +11,12 @@ import { AppError, getOpenCodeDatabasePath } from '@/shared/utils.js';
|
|
|
11
11
|
|
|
12
12
|
type SessionRow = NonNullable<ReturnType<typeof sessionsDb.getSessionById>>;
|
|
13
13
|
|
|
14
|
+
type FileTail = {
|
|
15
|
+
content: string;
|
|
16
|
+
/** True when `content` is the whole file, not just its trailing bytes. */
|
|
17
|
+
isComplete: boolean;
|
|
18
|
+
};
|
|
19
|
+
|
|
14
20
|
type ProviderTokenUsageServiceDependencies = {
|
|
15
21
|
getSessionById: (sessionId: string) => SessionRow | null | undefined;
|
|
16
22
|
getHomeDirectory: () => string;
|
|
@@ -18,7 +24,9 @@ type ProviderTokenUsageServiceDependencies = {
|
|
|
18
24
|
fileExists: (filePath: string) => boolean;
|
|
19
25
|
readDirectory: (directoryPath: string) => Promise<Dirent[]>;
|
|
20
26
|
readTextFile: (filePath: string) => Promise<string>;
|
|
27
|
+
readTextFileTail: (filePath: string, maxBytes: number) => Promise<FileTail>;
|
|
21
28
|
getClaudeContextWindow: () => string | undefined;
|
|
29
|
+
isProviderSessionSuperseded: (providerSessionId: string, provider: string) => boolean;
|
|
22
30
|
};
|
|
23
31
|
|
|
24
32
|
type TokenUsageResult = {
|
|
@@ -45,6 +53,14 @@ type OpenCodeTokenRow = {
|
|
|
45
53
|
cacheWriteTokens: number | null;
|
|
46
54
|
};
|
|
47
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Both JSONL usage readers below only need the newest usage row, which sits at
|
|
58
|
+
* or near the end of the transcript. Reading just this much of the tail keeps
|
|
59
|
+
* a session-open usage lookup O(1) in the transcript's size; the rare tail
|
|
60
|
+
* with no usage row at all falls back to the full read.
|
|
61
|
+
*/
|
|
62
|
+
const TOKEN_USAGE_TAIL_BYTES = 4 * 1024 * 1024;
|
|
63
|
+
|
|
48
64
|
const defaultDependencies: ProviderTokenUsageServiceDependencies = {
|
|
49
65
|
getSessionById: (sessionId) => sessionsDb.getSessionById(sessionId),
|
|
50
66
|
getHomeDirectory: () => os.homedir(),
|
|
@@ -52,7 +68,32 @@ const defaultDependencies: ProviderTokenUsageServiceDependencies = {
|
|
|
52
68
|
fileExists: (filePath) => fsSync.existsSync(filePath),
|
|
53
69
|
readDirectory: (directoryPath) => fsp.readdir(directoryPath, { withFileTypes: true }),
|
|
54
70
|
readTextFile: (filePath) => fsp.readFile(filePath, 'utf8'),
|
|
71
|
+
readTextFileTail: async (filePath, maxBytes) => {
|
|
72
|
+
const handle = await fsp.open(filePath, 'r');
|
|
73
|
+
try {
|
|
74
|
+
const { size } = await handle.stat();
|
|
75
|
+
if (size <= maxBytes) {
|
|
76
|
+
return { content: await handle.readFile({ encoding: 'utf8' }), isComplete: true };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const buffer = Buffer.alloc(maxBytes);
|
|
80
|
+
await handle.read(buffer, 0, maxBytes, size - maxBytes);
|
|
81
|
+
const content = buffer.toString('utf8');
|
|
82
|
+
// The window almost never starts on a row boundary; dropping everything
|
|
83
|
+
// up to the first newline discards the partial row (and any split
|
|
84
|
+
// multi-byte character with it).
|
|
85
|
+
const firstNewline = content.indexOf('\n');
|
|
86
|
+
return {
|
|
87
|
+
content: firstNewline === -1 ? '' : content.slice(firstNewline + 1),
|
|
88
|
+
isComplete: false,
|
|
89
|
+
};
|
|
90
|
+
} finally {
|
|
91
|
+
await handle.close();
|
|
92
|
+
}
|
|
93
|
+
},
|
|
55
94
|
getClaudeContextWindow: () => process.env.CONTEXT_WINDOW,
|
|
95
|
+
isProviderSessionSuperseded: (providerSessionId, provider) =>
|
|
96
|
+
sessionsDb.isProviderSessionSuperseded(providerSessionId, provider),
|
|
56
97
|
};
|
|
57
98
|
|
|
58
99
|
function readUsageNumber(value: unknown): number {
|
|
@@ -92,11 +133,8 @@ async function findCodexSessionFile(
|
|
|
92
133
|
return null;
|
|
93
134
|
}
|
|
94
135
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
let outputTokens = 0;
|
|
98
|
-
let totalTokens = 0;
|
|
99
|
-
let contextWindow = 200_000;
|
|
136
|
+
/** Newest `token_count` snapshot in the given JSONL text, or null when it has none. */
|
|
137
|
+
function findCodexTokenUsage(fileContent: string): TokenUsageResult | null {
|
|
100
138
|
const lines = fileContent.trim().split('\n');
|
|
101
139
|
|
|
102
140
|
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
@@ -109,58 +147,101 @@ function readCodexTokenUsage(fileContent: string): TokenUsageResult {
|
|
|
109
147
|
continue;
|
|
110
148
|
}
|
|
111
149
|
|
|
150
|
+
let inputTokens = 0;
|
|
151
|
+
let outputTokens = 0;
|
|
152
|
+
let totalTokens = 0;
|
|
112
153
|
if (tokenInfo.total_token_usage) {
|
|
113
154
|
inputTokens = readUsageNumber(tokenInfo.total_token_usage.input_tokens);
|
|
114
155
|
outputTokens = readUsageNumber(tokenInfo.total_token_usage.output_tokens);
|
|
115
156
|
totalTokens = readUsageNumber(tokenInfo.total_token_usage.total_tokens)
|
|
116
157
|
|| inputTokens + outputTokens;
|
|
117
158
|
}
|
|
118
|
-
|
|
119
|
-
|
|
159
|
+
return {
|
|
160
|
+
used: totalTokens,
|
|
161
|
+
total: readUsageNumber(tokenInfo.model_context_window) || 200_000,
|
|
162
|
+
inputTokens,
|
|
163
|
+
outputTokens,
|
|
164
|
+
breakdown: { input: inputTokens, output: outputTokens },
|
|
165
|
+
};
|
|
120
166
|
} catch {
|
|
121
167
|
// A provider may be writing the last JSONL line while this read happens.
|
|
122
168
|
}
|
|
123
169
|
}
|
|
124
170
|
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function emptyCodexTokenUsage(): TokenUsageResult {
|
|
125
175
|
return {
|
|
126
|
-
used:
|
|
127
|
-
total:
|
|
128
|
-
inputTokens,
|
|
129
|
-
outputTokens,
|
|
130
|
-
breakdown: { input:
|
|
176
|
+
used: 0,
|
|
177
|
+
total: 200_000,
|
|
178
|
+
inputTokens: 0,
|
|
179
|
+
outputTokens: 0,
|
|
180
|
+
breakdown: { input: 0, output: 0 },
|
|
131
181
|
};
|
|
132
182
|
}
|
|
133
183
|
|
|
134
|
-
|
|
184
|
+
/**
|
|
185
|
+
* Latest context-window usage from a Claude transcript's already-parsed rows.
|
|
186
|
+
*
|
|
187
|
+
* Exported because the session-messages reader hands the same usage back on
|
|
188
|
+
* every history page, the way the Codex and OpenCode readers do. Without that,
|
|
189
|
+
* a Claude session's counter only moved when the session was reselected, and
|
|
190
|
+
* the store's "this provider reports no usage" path overwrote it with zero.
|
|
191
|
+
*
|
|
192
|
+
* Reads the newest assistant turn only: `input_tokens + cache_read +
|
|
193
|
+
* cache_creation` is that one request's whole prompt, i.e. what the context
|
|
194
|
+
* window currently holds. Summing turns would count the same cached prefix
|
|
195
|
+
* once per turn.
|
|
196
|
+
*/
|
|
197
|
+
export function summarizeClaudeTokenUsage(
|
|
198
|
+
entries: AnyRecord[],
|
|
199
|
+
configuredContextWindow: string | undefined = process.env.CONTEXT_WINDOW,
|
|
200
|
+
): TokenUsageResult {
|
|
135
201
|
let inputTokens = 0;
|
|
136
202
|
let outputTokens = 0;
|
|
137
203
|
let cacheReadTokens = 0;
|
|
138
204
|
let cacheCreationTokens = 0;
|
|
139
|
-
const lines = fileContent.trim().split('\n');
|
|
140
205
|
|
|
141
|
-
for (let index =
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
206
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
207
|
+
const entry = entries[index];
|
|
208
|
+
// A subagent's turns report the subagent's context window, not this
|
|
209
|
+
// conversation's; reading one makes the counter drop to the subagent's
|
|
210
|
+
// number and bounce back on the next main-thread turn.
|
|
211
|
+
if (entry?.isSidechain === true) {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
148
214
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
215
|
+
const usage = entry?.type === 'assistant' ? entry.message?.usage : null;
|
|
216
|
+
if (!usage) {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const directInputTokens = readUsageNumber(usage.input_tokens ?? usage.inputTokens);
|
|
221
|
+
const rowCacheReadTokens = readUsageNumber(
|
|
222
|
+
usage.cache_read_input_tokens ?? usage.cacheReadInputTokens ?? usage.cacheReadTokens,
|
|
223
|
+
);
|
|
224
|
+
const rowCacheCreationTokens = readUsageNumber(
|
|
225
|
+
usage.cache_creation_input_tokens
|
|
226
|
+
?? usage.cacheCreationInputTokens
|
|
227
|
+
?? usage.cacheCreationTokens,
|
|
228
|
+
);
|
|
229
|
+
const rowInputTokens = directInputTokens + rowCacheReadTokens + rowCacheCreationTokens;
|
|
230
|
+
const rowOutputTokens = readUsageNumber(usage.output_tokens ?? usage.outputTokens);
|
|
231
|
+
|
|
232
|
+
// `<synthetic>` rows — interrupts, API errors, "No response requested" —
|
|
233
|
+
// are written with an all-zero usage block rather than none at all. They
|
|
234
|
+
// never carried a prompt, so treating one as the newest turn zeroed a
|
|
235
|
+
// counter that a live event had just set correctly.
|
|
236
|
+
if (rowInputTokens === 0 && rowOutputTokens === 0) {
|
|
237
|
+
continue;
|
|
163
238
|
}
|
|
239
|
+
|
|
240
|
+
cacheReadTokens = rowCacheReadTokens;
|
|
241
|
+
cacheCreationTokens = rowCacheCreationTokens;
|
|
242
|
+
inputTokens = rowInputTokens;
|
|
243
|
+
outputTokens = rowOutputTokens;
|
|
244
|
+
break;
|
|
164
245
|
}
|
|
165
246
|
|
|
166
247
|
const parsedContextWindow = Number.parseInt(configuredContextWindow ?? '', 10);
|
|
@@ -179,6 +260,22 @@ function readClaudeTokenUsage(fileContent: string, configuredContextWindow: stri
|
|
|
179
260
|
};
|
|
180
261
|
}
|
|
181
262
|
|
|
263
|
+
function parseClaudeUsageEntries(fileContent: string): AnyRecord[] {
|
|
264
|
+
const entries: AnyRecord[] = [];
|
|
265
|
+
for (const line of fileContent.trim().split('\n')) {
|
|
266
|
+
try {
|
|
267
|
+
entries.push(JSON.parse(line) as AnyRecord);
|
|
268
|
+
} catch {
|
|
269
|
+
// Skip malformed lines without discarding usage from earlier messages.
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return entries;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function claudeEntriesHaveUsage(entries: AnyRecord[]): boolean {
|
|
276
|
+
return entries.some((entry) => entry?.type === 'assistant' && entry.message?.usage);
|
|
277
|
+
}
|
|
278
|
+
|
|
182
279
|
function readOpenCodeTokenUsage(databasePath: string, providerSessionId: string): TokenUsageResult {
|
|
183
280
|
const database = new Database(databasePath, { readonly: true, fileMustExist: true });
|
|
184
281
|
try {
|
|
@@ -264,6 +361,24 @@ export function createProviderTokenUsageService(
|
|
|
264
361
|
});
|
|
265
362
|
}
|
|
266
363
|
|
|
364
|
+
// The fallback covers rows whose provider id was never recorded, and for
|
|
365
|
+
// a session discovered from disk the app id *is* its provider id. That
|
|
366
|
+
// stops being true the moment an edit rewinds the conversation off a
|
|
367
|
+
// thread: until the replacement run announces its own id, the fallback
|
|
368
|
+
// would resolve the retired transcript and report the discarded
|
|
369
|
+
// conversation's usage against an empty one.
|
|
370
|
+
if (
|
|
371
|
+
!session.provider_session_id
|
|
372
|
+
&& dependencies.isProviderSessionSuperseded(sessionId, session.provider)
|
|
373
|
+
) {
|
|
374
|
+
return {
|
|
375
|
+
used: 0,
|
|
376
|
+
inputTokens: 0,
|
|
377
|
+
outputTokens: 0,
|
|
378
|
+
breakdown: { input: 0, output: 0 },
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
267
382
|
const providerSessionId = session.provider_session_id || sessionId;
|
|
268
383
|
|
|
269
384
|
if (session.provider === 'cursor') {
|
|
@@ -307,8 +422,16 @@ export function createProviderTokenUsageService(
|
|
|
307
422
|
});
|
|
308
423
|
}
|
|
309
424
|
|
|
310
|
-
const
|
|
311
|
-
|
|
425
|
+
const tail = await dependencies.readTextFileTail(sessionFilePath, TOKEN_USAGE_TAIL_BYTES);
|
|
426
|
+
const tailUsage = findCodexTokenUsage(tail.content);
|
|
427
|
+
if (tailUsage || tail.isComplete) {
|
|
428
|
+
return tailUsage ?? emptyCodexTokenUsage();
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// A tail this large with no token_count row is pathological, but the
|
|
432
|
+
// whole file is still authoritative when it happens.
|
|
433
|
+
return findCodexTokenUsage(await dependencies.readTextFile(sessionFilePath))
|
|
434
|
+
?? emptyCodexTokenUsage();
|
|
312
435
|
}
|
|
313
436
|
|
|
314
437
|
let sessionFilePath = session.jsonl_path;
|
|
@@ -345,8 +468,12 @@ export function createProviderTokenUsageService(
|
|
|
345
468
|
});
|
|
346
469
|
}
|
|
347
470
|
|
|
348
|
-
const
|
|
349
|
-
|
|
471
|
+
const tail = await dependencies.readTextFileTail(sessionFilePath, TOKEN_USAGE_TAIL_BYTES);
|
|
472
|
+
let entries = parseClaudeUsageEntries(tail.content);
|
|
473
|
+
if (!claudeEntriesHaveUsage(entries) && !tail.isComplete) {
|
|
474
|
+
entries = parseClaudeUsageEntries(await dependencies.readTextFile(sessionFilePath));
|
|
475
|
+
}
|
|
476
|
+
return summarizeClaudeTokenUsage(entries, dependencies.getClaudeContextWindow());
|
|
350
477
|
},
|
|
351
478
|
};
|
|
352
479
|
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import fsp from 'node:fs/promises';
|
|
2
|
+
|
|
3
|
+
import type { FetchHistoryResult } from '@/shared/types.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Full-transcript cache for session history reads.
|
|
7
|
+
*
|
|
8
|
+
* Every provider history reader materializes the complete normalized
|
|
9
|
+
* transcript and then slices out the requested page, so serving a 20-row page
|
|
10
|
+
* of a large session re-read and re-parsed the whole transcript file on every
|
|
11
|
+
* request — opening a session, each older page while scrolling up, and every
|
|
12
|
+
* post-turn refresh. For a multi-megabyte JSONL that is most of a second of
|
|
13
|
+
* CPU per request.
|
|
14
|
+
*
|
|
15
|
+
* Entries are keyed by app session id and validated with one `stat` per
|
|
16
|
+
* request against the transcript file's identity (path + mtime + size), so
|
|
17
|
+
* only the first read after the file changes pays the parse. Anything that
|
|
18
|
+
* rewrites history (a new turn, an edit, a rewind, a fork) touches the file
|
|
19
|
+
* and invalidates naturally; no explicit invalidation hooks exist or are
|
|
20
|
+
* needed.
|
|
21
|
+
*
|
|
22
|
+
* Only history readers that read `jsonl_path` itself may use this cache —
|
|
23
|
+
* callers pass `transcriptPath: null` for providers whose messages live
|
|
24
|
+
* elsewhere (Cursor's store.db, OpenCode's shared SQLite), which bypasses
|
|
25
|
+
* caching entirely.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
type CacheEntry = {
|
|
29
|
+
transcriptPath: string;
|
|
30
|
+
mtimeMs: number;
|
|
31
|
+
/** File size in bytes; doubles as the entry's cost against the byte budget. */
|
|
32
|
+
size: number;
|
|
33
|
+
full: FetchHistoryResult;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type GetFullHistoryArgs = {
|
|
37
|
+
sessionId: string;
|
|
38
|
+
/** Path of the file the provider's history reader actually parses, or null to bypass. */
|
|
39
|
+
transcriptPath: string | null | undefined;
|
|
40
|
+
/** Loads the complete transcript (`limit: null, offset: 0`) from the provider. */
|
|
41
|
+
loadFull: () => Promise<FetchHistoryResult>;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A transcript entry's heap cost is roughly the file it was parsed from, so
|
|
46
|
+
* the budget is expressed in file bytes. The newest entry is always retained
|
|
47
|
+
* even when it alone exceeds the budget — evicting it would just re-parse the
|
|
48
|
+
* same file on the next request.
|
|
49
|
+
*/
|
|
50
|
+
const MAX_CACHED_TRANSCRIPT_FILE_BYTES = 256 * 1024 * 1024;
|
|
51
|
+
const MAX_CACHE_ENTRIES = 8;
|
|
52
|
+
|
|
53
|
+
export function createSessionHistoryCache(
|
|
54
|
+
maxTotalFileBytes = MAX_CACHED_TRANSCRIPT_FILE_BYTES,
|
|
55
|
+
maxEntries = MAX_CACHE_ENTRIES,
|
|
56
|
+
) {
|
|
57
|
+
const entries = new Map<string, CacheEntry>();
|
|
58
|
+
const pendingLoads = new Map<string, Promise<FetchHistoryResult>>();
|
|
59
|
+
|
|
60
|
+
function evictOverBudget(): void {
|
|
61
|
+
let totalBytes = 0;
|
|
62
|
+
for (const entry of entries.values()) {
|
|
63
|
+
totalBytes += entry.size;
|
|
64
|
+
}
|
|
65
|
+
for (const key of entries.keys()) {
|
|
66
|
+
if (entries.size <= 1 || (totalBytes <= maxTotalFileBytes && entries.size <= maxEntries)) {
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
totalBytes -= entries.get(key)!.size;
|
|
70
|
+
entries.delete(key);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
/**
|
|
76
|
+
* Returns the session's full transcript through the cache, or null when
|
|
77
|
+
* the session is not cacheable (no transcript path, or the file cannot be
|
|
78
|
+
* stat'ed) — the caller then falls back to a plain provider read.
|
|
79
|
+
*/
|
|
80
|
+
async getFullHistory({ sessionId, transcriptPath, loadFull }: GetFullHistoryArgs): Promise<FetchHistoryResult | null> {
|
|
81
|
+
if (!transcriptPath) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let stat;
|
|
86
|
+
try {
|
|
87
|
+
stat = await fsp.stat(transcriptPath);
|
|
88
|
+
} catch {
|
|
89
|
+
entries.delete(sessionId);
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
if (!stat.isFile()) {
|
|
93
|
+
entries.delete(sessionId);
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const cached = entries.get(sessionId);
|
|
98
|
+
if (
|
|
99
|
+
cached
|
|
100
|
+
&& cached.transcriptPath === transcriptPath
|
|
101
|
+
&& cached.mtimeMs === stat.mtimeMs
|
|
102
|
+
&& cached.size === stat.size
|
|
103
|
+
) {
|
|
104
|
+
// Re-insert to mark as most recently used.
|
|
105
|
+
entries.delete(sessionId);
|
|
106
|
+
entries.set(sessionId, cached);
|
|
107
|
+
return cached.full;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Concurrent requests for the same session share one parse. The file may
|
|
111
|
+
// gain rows while the load runs; the pre-load stat is what the entry is
|
|
112
|
+
// keyed by, so the next request would see a changed stat and re-read.
|
|
113
|
+
const pending = pendingLoads.get(sessionId);
|
|
114
|
+
if (pending) {
|
|
115
|
+
return pending;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const load = loadFull().then((full) => {
|
|
119
|
+
entries.delete(sessionId);
|
|
120
|
+
entries.set(sessionId, {
|
|
121
|
+
transcriptPath,
|
|
122
|
+
mtimeMs: stat.mtimeMs,
|
|
123
|
+
size: stat.size,
|
|
124
|
+
full,
|
|
125
|
+
});
|
|
126
|
+
evictOverBudget();
|
|
127
|
+
return full;
|
|
128
|
+
});
|
|
129
|
+
pendingLoads.set(sessionId, load);
|
|
130
|
+
try {
|
|
131
|
+
return await load;
|
|
132
|
+
} finally {
|
|
133
|
+
pendingLoads.delete(sessionId);
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export const sessionHistoryCache = createSessionHistoryCache();
|
|
@@ -1,59 +1,146 @@
|
|
|
1
|
-
import
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { access } from 'node:fs/promises';
|
|
3
|
+
|
|
4
|
+
import { scanStateDb, sessionsDb } from '@/modules/database/index.js';
|
|
2
5
|
import { providerRegistry } from '@/modules/providers/provider.registry.js';
|
|
3
6
|
import type { LLMProvider } from '@/shared/types.js';
|
|
4
7
|
|
|
5
8
|
type SessionSynchronizeResult = {
|
|
6
9
|
processedByProvider: Record<LLMProvider, number>;
|
|
10
|
+
/** Indexed sessions dropped because their transcript file no longer exists. */
|
|
11
|
+
prunedOrphans: number;
|
|
7
12
|
failures: string[];
|
|
8
13
|
};
|
|
9
14
|
|
|
15
|
+
const pathExists = async (target: string): Promise<boolean> => {
|
|
16
|
+
try {
|
|
17
|
+
await access(target);
|
|
18
|
+
return true;
|
|
19
|
+
} catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Removes indexed sessions whose transcript file has disappeared from disk.
|
|
26
|
+
*
|
|
27
|
+
* Nothing else deletes these rows: the synchronizers only ever upsert, and the
|
|
28
|
+
* watcher reacts to `add`/`change` but not `unlink`. A transcript removed by
|
|
29
|
+
* hand — or written by a test run that pointed at the real `~/.claude` — left a
|
|
30
|
+
* permanent sidebar entry that opened an empty "Untitled" session.
|
|
31
|
+
*
|
|
32
|
+
* A row is only dropped when its *containing directory* still exists. That
|
|
33
|
+
* keeps an unmounted or not-yet-created home from being read as "every
|
|
34
|
+
* transcript was deleted" and wiping the whole index.
|
|
35
|
+
*/
|
|
36
|
+
const pruneOrphanedSessions = async (): Promise<number> => {
|
|
37
|
+
const knownDirectoryExists = new Map<string, boolean>();
|
|
38
|
+
let pruned = 0;
|
|
39
|
+
|
|
40
|
+
for (const { session_id: sessionId, jsonl_path: jsonlPath } of sessionsDb.getSessionsWithTranscriptPath()) {
|
|
41
|
+
if (await pathExists(jsonlPath)) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const directory = path.dirname(jsonlPath);
|
|
46
|
+
let directoryExists = knownDirectoryExists.get(directory);
|
|
47
|
+
if (directoryExists === undefined) {
|
|
48
|
+
directoryExists = await pathExists(directory);
|
|
49
|
+
knownDirectoryExists.set(directory, directoryExists);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!directoryExists) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (sessionsDb.deleteSessionById(sessionId)) {
|
|
57
|
+
pruned += 1;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return pruned;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The scan that every `synchronizeSessions()` caller shares while it runs.
|
|
66
|
+
*
|
|
67
|
+
* Opening the UI fires `/api/projects` and `/api/projects/archived` at once,
|
|
68
|
+
* and each used to start its own full provider scan from the same
|
|
69
|
+
* `last_scanned_at` cursor: identical work over identical transcripts,
|
|
70
|
+
* contending for the same synchronous SQLite writes and doubling how long the
|
|
71
|
+
* sidebar sits on its loading screen. Callers that arrive while a scan is
|
|
72
|
+
* already running now await that scan instead of starting another.
|
|
73
|
+
*/
|
|
74
|
+
let inFlightSynchronization: Promise<SessionSynchronizeResult> | null = null;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Runs all provider synchronizers and updates scan_state.last_scanned_at.
|
|
78
|
+
*/
|
|
79
|
+
async function runSessionSynchronization(): Promise<SessionSynchronizeResult> {
|
|
80
|
+
const lastScanAt = scanStateDb.getLastScannedAt();
|
|
81
|
+
const scanBoundary = new Date();
|
|
82
|
+
const processedByProvider: Record<LLMProvider, number> = {
|
|
83
|
+
claude: 0,
|
|
84
|
+
codex: 0,
|
|
85
|
+
cursor: 0,
|
|
86
|
+
opencode: 0,
|
|
87
|
+
};
|
|
88
|
+
const failures: string[] = [];
|
|
89
|
+
|
|
90
|
+
const results = await Promise.allSettled(
|
|
91
|
+
providerRegistry.listProviders().map(async (provider) => ({
|
|
92
|
+
provider: provider.id,
|
|
93
|
+
processed: await provider.sessionSynchronizer.synchronize(lastScanAt ?? undefined),
|
|
94
|
+
}))
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
for (const result of results) {
|
|
98
|
+
if (result.status === 'fulfilled') {
|
|
99
|
+
processedByProvider[result.value.provider] = result.value.processed;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const reason = result.reason instanceof Error ? result.reason.message : String(result.reason);
|
|
104
|
+
failures.push(reason);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Pruning is skipped after a partial sync: a provider that just failed may
|
|
108
|
+
// not have re-indexed transcripts it would otherwise have re-created.
|
|
109
|
+
const prunedOrphans = failures.length === 0 ? await pruneOrphanedSessions() : 0;
|
|
110
|
+
|
|
111
|
+
if (failures.length === 0) {
|
|
112
|
+
scanStateDb.updateLastScannedAt(scanBoundary);
|
|
113
|
+
} else {
|
|
114
|
+
console.warn(
|
|
115
|
+
`[Sessions] Skipping scan_state cursor advance because ${failures.length} provider sync(s) failed.`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
processedByProvider,
|
|
121
|
+
prunedOrphans,
|
|
122
|
+
failures,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
10
126
|
/**
|
|
11
127
|
* Orchestrates provider-specific session indexers and indexed-session lifecycle operations.
|
|
12
128
|
*/
|
|
13
129
|
export const sessionSynchronizerService = {
|
|
14
130
|
/**
|
|
15
|
-
*
|
|
131
|
+
* Scans every provider for new or changed sessions, coalescing concurrent
|
|
132
|
+
* callers onto a single scan.
|
|
16
133
|
*/
|
|
17
134
|
async synchronizeSessions(): Promise<SessionSynchronizeResult> {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const processedByProvider: Record<LLMProvider, number> = {
|
|
21
|
-
claude: 0,
|
|
22
|
-
codex: 0,
|
|
23
|
-
cursor: 0,
|
|
24
|
-
opencode: 0,
|
|
25
|
-
};
|
|
26
|
-
const failures: string[] = [];
|
|
27
|
-
|
|
28
|
-
const results = await Promise.allSettled(
|
|
29
|
-
providerRegistry.listProviders().map(async (provider) => ({
|
|
30
|
-
provider: provider.id,
|
|
31
|
-
processed: await provider.sessionSynchronizer.synchronize(lastScanAt ?? undefined),
|
|
32
|
-
}))
|
|
33
|
-
);
|
|
34
|
-
|
|
35
|
-
for (const result of results) {
|
|
36
|
-
if (result.status === 'fulfilled') {
|
|
37
|
-
processedByProvider[result.value.provider] = result.value.processed;
|
|
38
|
-
continue;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const reason = result.reason instanceof Error ? result.reason.message : String(result.reason);
|
|
42
|
-
failures.push(reason);
|
|
135
|
+
if (inFlightSynchronization) {
|
|
136
|
+
return inFlightSynchronization;
|
|
43
137
|
}
|
|
44
138
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
console.warn(
|
|
49
|
-
`[Sessions] Skipping scan_state cursor advance because ${failures.length} provider sync(s) failed.`,
|
|
50
|
-
);
|
|
51
|
-
}
|
|
139
|
+
inFlightSynchronization = runSessionSynchronization().finally(() => {
|
|
140
|
+
inFlightSynchronization = null;
|
|
141
|
+
});
|
|
52
142
|
|
|
53
|
-
return
|
|
54
|
-
processedByProvider,
|
|
55
|
-
failures,
|
|
56
|
-
};
|
|
143
|
+
return inFlightSynchronization;
|
|
57
144
|
},
|
|
58
145
|
|
|
59
146
|
/**
|