@cloudcli-ai/cloudcli 1.37.2 → 1.37.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +269 -0
- package/dist/assets/{abnfDiagram-N423BO3Z-C4UAoHJb.js → abnfDiagram-N423BO3Z-Dmkwx38H.js} +1 -1
- package/dist/assets/{arc-B8jF-ja9.js → arc-DLe9AYuO.js} +1 -1
- package/dist/assets/{architectureDiagram-T3A2C74G-C_Wur_CU.js → architectureDiagram-T3A2C74G-CBbbbNUW.js} +1 -1
- package/dist/assets/{blockDiagram-VBNYF7ZC-Cq4P1i8t.js → blockDiagram-VBNYF7ZC-CaMEaWu4.js} +1 -1
- package/dist/assets/{c4Diagram-5PPSVZJV-C1wG4eUj.js → c4Diagram-5PPSVZJV-DhjMgNnR.js} +1 -1
- package/dist/assets/channel-CWrmwsir.js +1 -0
- package/dist/assets/{chunk-2GRJ4B5K-B8fnBG6E.js → chunk-2GRJ4B5K-yjl3-F-i.js} +1 -1
- package/dist/assets/{chunk-2Q5K7J3B-WdfPooPJ.js → chunk-2Q5K7J3B-DsAYaWEP.js} +1 -1
- package/dist/assets/{chunk-5RXB4S5H-DhZJuG2E.js → chunk-5RXB4S5H-Bt5qmqOX.js} +1 -1
- package/dist/assets/{chunk-5VM5RSS4-B0rjslal.js → chunk-5VM5RSS4-yWrWVvUt.js} +1 -1
- package/dist/assets/{chunk-6Q2QTUOP-CG2Oj4jL.js → chunk-6Q2QTUOP-Dt66K5xb.js} +1 -1
- package/dist/assets/{chunk-GF5L2VYU-rrl2lRxY.js → chunk-GF5L2VYU-DA0vzaj4.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-CjVbrVMA.js → chunk-JWPE2WC7-CUkbx96V.js} +1 -1
- package/dist/assets/{chunk-KBJHAD2P-CRzKgU8S.js → chunk-KBJHAD2P-DWKxx1WH.js} +1 -1
- package/dist/assets/{chunk-RYQCIY6F-DBz4GTZF.js → chunk-RYQCIY6F-BqluplN0.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-wHxalsXk.js → chunk-XXDRQBXY-DelKg3Xf.js} +1 -1
- package/dist/assets/classDiagram-JCYQIIEL-COAGCC6w.js +1 -0
- package/dist/assets/classDiagram-v2-OCEON4UE-COAGCC6w.js +1 -0
- package/dist/assets/{cose-bilkent-JH36ORCC-s1hygY8e.js → cose-bilkent-JH36ORCC-DHV93-Em.js} +1 -1
- package/dist/assets/{cynefin-VYW2F7L2-C4LGV15i.js → cynefin-VYW2F7L2-B9HZgoGX.js} +1 -1
- package/dist/assets/{cynefinDiagram-MW4NZA55-C5uhu7B3.js → cynefinDiagram-MW4NZA55-hFjjEQg8.js} +1 -1
- package/dist/assets/{dagre-VZM6K2ZE-B-OBTewm.js → dagre-VZM6K2ZE-CpeVOC7V.js} +1 -1
- package/dist/assets/{diagram-7IWD3JNH-BjGKQxih.js → diagram-7IWD3JNH-D0H1UoRA.js} +1 -1
- package/dist/assets/{diagram-B4RE2ZJO-BMKz1X2L.js → diagram-B4RE2ZJO-6Y7BQUup.js} +1 -1
- package/dist/assets/{diagram-LBJQPF4R-DNh-GhKC.js → diagram-LBJQPF4R-C0nNOwht.js} +1 -1
- package/dist/assets/{diagram-Q27KOJAE-BaTG9jPT.js → diagram-Q27KOJAE-En6QlOVX.js} +1 -1
- package/dist/assets/{diagram-UB23O5K3-BUwo2OvJ.js → diagram-UB23O5K3-CmPiNeGD.js} +1 -1
- package/dist/assets/{ebnfDiagram-BXEA7PRR-DAZgfzX2.js → ebnfDiagram-BXEA7PRR-Bygzp1Qt.js} +1 -1
- package/dist/assets/{erDiagram-JOGREHBK-1w1CcGEb.js → erDiagram-JOGREHBK-CwScRfzl.js} +1 -1
- package/dist/assets/{flowDiagram-UKHOOZJN-D4EII8Jj.js → flowDiagram-UKHOOZJN-bKX-0xP6.js} +1 -1
- package/dist/assets/{ganttDiagram-PKOTCBZU-ChvJZAgB.js → ganttDiagram-PKOTCBZU-DtZPKXGd.js} +1 -1
- package/dist/assets/{gitGraphDiagram-DS77QQ5N-DtOn41S8.js → gitGraphDiagram-DS77QQ5N-CUOtdL1o.js} +1 -1
- package/dist/assets/index-CdjIFxM2.js +5004 -0
- package/dist/assets/index-DgafhGTF.css +32 -0
- package/dist/assets/{infoDiagram-6WML65LV-DN_PVEJZ.js → infoDiagram-6WML65LV-ciRQTS0h.js} +1 -1
- package/dist/assets/{ishikawaDiagram-WSZJBQD7-DbkF1kXb.js → ishikawaDiagram-WSZJBQD7-CgjhQZ84.js} +1 -1
- package/dist/assets/{journeyDiagram-NVQOT4AX-pJwmhddE.js → journeyDiagram-NVQOT4AX-BsbQSeIW.js} +1 -1
- package/dist/assets/{kanban-definition-27J2QSJJ-PPAPgNp5.js → kanban-definition-27J2QSJJ-CrwzY9Jw.js} +1 -1
- package/dist/assets/{linear-iPR-J9f7.js → linear-BUaCDr6l.js} +1 -1
- package/dist/assets/{mermaid.core-BEUL86Z6.js → mermaid.core-ClgdUU0j.js} +5 -5
- package/dist/assets/{mindmap-definition-FAOFIHXS-BvZoBib7.js → mindmap-definition-FAOFIHXS-BictljY1.js} +1 -1
- package/dist/assets/{pegDiagram-VL7TDLO6-DhYreaUd.js → pegDiagram-VL7TDLO6-BXdByy_U.js} +1 -1
- package/dist/assets/{pieDiagram-7S7Q4E2Y-CrOHlyD0.js → pieDiagram-7S7Q4E2Y-uZsMfkRX.js} +1 -1
- package/dist/assets/{quadrantDiagram-CIZ2JOQS-D4g5yH3t.js → quadrantDiagram-CIZ2JOQS-7lCv4QIj.js} +1 -1
- package/dist/assets/{railroadDiagram-AXF67PYL-Dj39lHlf.js → railroadDiagram-AXF67PYL-07dxIWbi.js} +1 -1
- package/dist/assets/{requirementDiagram-LRYGKXZP-1AS4Zgm_.js → requirementDiagram-LRYGKXZP-Cb31Yv1_.js} +1 -1
- package/dist/assets/{sankeyDiagram-W5VNT64P-CKlMn19P.js → sankeyDiagram-W5VNT64P-X0YPSyF9.js} +1 -1
- package/dist/assets/{sequenceDiagram-SI44F4Z6-B_j3qyvo.js → sequenceDiagram-SI44F4Z6-BdBxSIlP.js} +1 -1
- package/dist/assets/server.browser-CtX3Hdl5.js +27 -0
- package/dist/assets/{sizeCapture-X5ZJPWSS-Bp2Op16v.js → sizeCapture-X5ZJPWSS-DQM5xAyD.js} +1 -1
- package/dist/assets/{stateDiagram-OKZ733FA-C0cN6DoY.js → stateDiagram-OKZ733FA-DJ3xkLUF.js} +1 -1
- package/dist/assets/stateDiagram-v2-UEYNNEHI-vAaE4zNo.js +1 -0
- package/dist/assets/{swimlanes-SLNWSIFB-BEvXOV9b.js → swimlanes-SLNWSIFB-rjfYdW0w.js} +2 -2
- package/dist/assets/swimlanesDiagram-ULZ7WXOC-TV2IF4Nq.js +8 -0
- package/dist/assets/{timeline-definition-Z64GVDOM-zdJQinvZ.js → timeline-definition-Z64GVDOM-DZMra5do.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-BevD8uu0.js} +1 -1
- package/dist/assets/{wardleyDiagram-T6FBY63Y-B9AlLQup.js → wardleyDiagram-T6FBY63Y-DsIyXB7z.js} +1 -1
- package/dist/assets/{xychartDiagram-ELKLHX3M-DTJX48Qg.js → xychartDiagram-ELKLHX3M-G3PlF3ko.js} +1 -1
- package/dist/index.html +3 -3
- package/dist-server/server/index.js +11 -4
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/modules/agent/agent.module.js +9 -2
- package/dist-server/server/modules/agent/agent.module.js.map +1 -1
- package/dist-server/server/modules/agent/agent.routes.js +177 -130
- package/dist-server/server/modules/agent/agent.routes.js.map +1 -1
- package/dist-server/server/modules/agent/tests/agent-run-registration.test.js +306 -0
- package/dist-server/server/modules/agent/tests/agent-run-registration.test.js.map +1 -0
- package/dist-server/server/modules/agent/tests/agent.routes.test.js +48 -2
- 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 +45 -1
- package/dist-server/server/modules/database/migrations.js.map +1 -1
- package/dist-server/server/modules/database/repositories/provider-models.js +60 -6
- package/dist-server/server/modules/database/repositories/provider-models.js.map +1 -1
- package/dist-server/server/modules/database/repositories/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 +144 -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 +99 -0
- package/dist-server/server/modules/database/schema.js.map +1 -1
- package/dist-server/server/modules/database/tests/provider-models.db.integration.test.js +99 -0
- package/dist-server/server/modules/database/tests/provider-models.db.integration.test.js.map +1 -1
- package/dist-server/server/modules/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.module.js +2 -1
- package/dist-server/server/modules/file-tree/file-tree.module.js.map +1 -1
- package/dist-server/server/modules/file-tree/file-tree.service.js +44 -7
- package/dist-server/server/modules/file-tree/file-tree.service.js.map +1 -1
- package/dist-server/server/modules/file-tree/tests/file-tree.service.test.js +126 -1
- package/dist-server/server/modules/file-tree/tests/file-tree.service.test.js.map +1 -1
- package/dist-server/server/modules/git/git-branch-diff.service.js +142 -0
- package/dist-server/server/modules/git/git-branch-diff.service.js.map +1 -0
- package/dist-server/server/modules/git/git-branch.service.js.map +1 -1
- package/dist-server/server/modules/git/git.routes.js +92 -5
- package/dist-server/server/modules/git/git.routes.js.map +1 -1
- package/dist-server/server/modules/git/tests/git-branch-diff.service.test.js +160 -0
- package/dist-server/server/modules/git/tests/git-branch-diff.service.test.js.map +1 -0
- package/dist-server/server/modules/projects/project-clone.routes.js +108 -0
- package/dist-server/server/modules/projects/project-clone.routes.js.map +1 -0
- package/dist-server/server/modules/projects/projects.routes.js +13 -66
- package/dist-server/server/modules/projects/projects.routes.js.map +1 -1
- package/dist-server/server/modules/projects/services/project-clone-request.service.js +36 -0
- package/dist-server/server/modules/projects/services/project-clone-request.service.js.map +1 -0
- package/dist-server/server/modules/projects/services/project-clone.service.js +86 -32
- package/dist-server/server/modules/projects/services/project-clone.service.js.map +1 -1
- package/dist-server/server/modules/projects/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/projects/tests/project-clone.routes.test.js +191 -0
- package/dist-server/server/modules/projects/tests/project-clone.routes.test.js.map +1 -0
- package/dist-server/server/modules/projects/tests/project-clone.service.test.js +275 -0
- package/dist-server/server/modules/projects/tests/project-clone.service.test.js.map +1 -1
- package/dist-server/server/modules/providers/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 +62 -9
- 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 +31 -12
- 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 +472 -53
- package/dist-server/server/modules/providers/list/claude/claude-runtime.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js +38 -11
- package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js +1007 -60
- 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 +47 -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 +71 -53
- 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 +1621 -431
- 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-auth.provider.js +9 -2
- package/dist-server/server/modules/providers/list/cursor/cursor-auth.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/cursor/cursor-runtime.provider.js +16 -8
- 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 +274 -9
- package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/opencode/opencode-runtime.provider.js +5 -2
- package/dist-server/server/modules/providers/list/opencode/opencode-runtime.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js +19 -2
- package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js.map +1 -1
- package/dist-server/server/modules/providers/provider.routes.js +134 -2
- package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
- package/dist-server/server/modules/providers/services/claude-cli-liveness.service.js +100 -0
- package/dist-server/server/modules/providers/services/claude-cli-liveness.service.js.map +1 -0
- package/dist-server/server/modules/providers/services/claude-settings-permissions.service.js +97 -0
- package/dist-server/server/modules/providers/services/claude-settings-permissions.service.js.map +1 -0
- package/dist-server/server/modules/providers/services/provider-capabilities.service.js +23 -1
- package/dist-server/server/modules/providers/services/provider-capabilities.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/provider-models.service.js +59 -5
- package/dist-server/server/modules/providers/services/provider-models.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/provider-runtime.service.js +8 -0
- package/dist-server/server/modules/providers/services/provider-runtime.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/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 +2 -4
- package/dist-server/server/modules/providers/services/session-conversations-search.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/session-history-cache.service.js +91 -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 +252 -12
- package/dist-server/server/modules/providers/services/sessions.service.js.map +1 -1
- package/dist-server/server/modules/providers/tests/claude-auth.test.js +183 -0
- package/dist-server/server/modules/providers/tests/claude-auth.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/claude-background-work.test.js +172 -0
- package/dist-server/server/modules/providers/tests/claude-background-work.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/claude-compaction.test.js +84 -0
- package/dist-server/server/modules/providers/tests/claude-compaction.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/claude-message-model.test.js +123 -0
- package/dist-server/server/modules/providers/tests/claude-message-model.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/claude-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-runtime-hold.test.js +265 -0
- package/dist-server/server/modules/providers/tests/claude-runtime-hold.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/claude-sessions.test.js +1815 -33
- package/dist-server/server/modules/providers/tests/claude-sessions.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/claude-settings-permissions.test.js +228 -0
- package/dist-server/server/modules/providers/tests/claude-settings-permissions.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/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-models.test.js +25 -0
- package/dist-server/server/modules/providers/tests/codex-models.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/codex-runtime.test.js +83 -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 +354 -15
- package/dist-server/server/modules/providers/tests/codex-sessions.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/cursor-auth.test.js +167 -0
- package/dist-server/server/modules/providers/tests/cursor-auth.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/opencode-models.test.js +196 -4
- package/dist-server/server/modules/providers/tests/opencode-models.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/opencode-sessions.test.js +19 -1
- package/dist-server/server/modules/providers/tests/opencode-sessions.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/provider-capabilities.service.test.js +30 -0
- package/dist-server/server/modules/providers/tests/provider-capabilities.service.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/provider-models.service.test.js +79 -1
- package/dist-server/server/modules/providers/tests/provider-models.service.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/provider-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 +190 -1
- 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 +139 -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 +219 -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 +175 -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 +290 -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 +37 -49
- 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 +204 -10
- 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 +236 -20
- package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -1
- package/dist-server/server/modules/websocket/tests/chat-background-subscribe.test.js +137 -0
- package/dist-server/server/modules/websocket/tests/chat-background-subscribe.test.js.map +1 -0
- package/dist-server/server/modules/websocket/tests/chat-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/chat-stop-task.test.js +96 -0
- package/dist-server/server/modules/websocket/tests/chat-stop-task.test.js.map +1 -0
- package/dist-server/server/modules/websocket/tests/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 +500 -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/tests/read-only-roots.test.js +124 -0
- package/dist-server/server/shared/tests/read-only-roots.test.js.map +1 -0
- package/dist-server/server/shared/tests/strip-ansi-sequences.test.js +31 -0
- package/dist-server/server/shared/tests/strip-ansi-sequences.test.js.map +1 -0
- package/dist-server/server/shared/utils.js +160 -0
- package/dist-server/server/shared/utils.js.map +1 -1
- package/electron/desktopWindow.js +6 -0
- package/electron/viewHost.js +25 -2
- package/package.json +20 -17
- package/server/index.ts +16 -5
- package/server/modules/agent/agent.module.ts +11 -1
- package/server/modules/agent/agent.routes.ts +200 -142
- package/server/modules/agent/tests/agent-run-registration.test.ts +348 -0
- package/server/modules/agent/tests/agent.routes.test.ts +51 -2
- 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 +51 -0
- package/server/modules/database/repositories/provider-models.ts +79 -6
- 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 +204 -3
- package/server/modules/database/repositories/user-preferences.db.ts +80 -0
- package/server/modules/database/schema.ts +103 -0
- package/server/modules/database/tests/provider-models.db.integration.test.ts +113 -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.module.ts +2 -1
- package/server/modules/file-tree/file-tree.service.ts +49 -7
- package/server/modules/file-tree/tests/file-tree.service.test.ts +163 -1
- package/server/modules/git/git-branch-diff.service.ts +204 -0
- package/server/modules/git/git-branch.service.ts +2 -12
- package/server/modules/git/git.routes.ts +100 -6
- package/server/modules/git/tests/git-branch-diff.service.test.ts +210 -0
- package/server/modules/projects/project-clone.routes.ts +142 -0
- package/server/modules/projects/projects.routes.ts +16 -81
- package/server/modules/projects/services/project-clone-request.service.ts +56 -0
- package/server/modules/projects/services/project-clone.service.ts +102 -34
- package/server/modules/projects/services/project-delete.service.ts +13 -1
- package/server/modules/projects/services/project-star.service.ts +2 -2
- package/server/modules/projects/tests/project-clone.routes.test.ts +246 -0
- package/server/modules/projects/tests/project-clone.service.test.ts +376 -0
- package/server/modules/providers/index.ts +4 -0
- package/server/modules/providers/list/claude/claude-auth.provider.ts +81 -11
- package/server/modules/providers/list/claude/claude-fork.provider.ts +57 -0
- package/server/modules/providers/list/claude/claude-models.provider.ts +37 -17
- package/server/modules/providers/list/claude/claude-runtime.provider.js +510 -54
- package/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts +40 -14
- package/server/modules/providers/list/claude/claude-sessions.provider.ts +1195 -63
- 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 +47 -0
- package/server/modules/providers/list/codex/{codex-runtime.provider.js → codex-runtime.provider.ts} +105 -70
- package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +9 -0
- package/server/modules/providers/list/codex/codex-sessions.provider.ts +1847 -455
- package/server/modules/providers/list/codex/codex.provider.ts +3 -0
- package/server/modules/providers/list/cursor/cursor-auth.provider.ts +10 -2
- package/server/modules/providers/list/cursor/cursor-runtime.provider.js +17 -8
- package/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.ts +0 -1
- package/server/modules/providers/list/opencode/opencode-models.provider.ts +297 -9
- package/server/modules/providers/list/opencode/opencode-runtime.provider.js +5 -2
- package/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.ts +30 -2
- package/server/modules/providers/provider.routes.ts +162 -2
- package/server/modules/providers/services/claude-cli-liveness.service.ts +121 -0
- package/server/modules/providers/services/claude-settings-permissions.service.ts +127 -0
- package/server/modules/providers/services/provider-capabilities.service.ts +40 -1
- package/server/modules/providers/services/provider-models.service.ts +71 -4
- package/server/modules/providers/services/provider-runtime.service.ts +11 -0
- package/server/modules/providers/services/provider-token-usage.service.ts +166 -39
- package/server/modules/providers/services/session-conversations-search.service.ts +2 -5
- package/server/modules/providers/services/session-history-cache.service.ts +150 -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 +309 -17
- package/server/modules/providers/tests/claude-auth.test.ts +208 -0
- package/server/modules/providers/tests/claude-background-work.test.ts +213 -0
- package/server/modules/providers/tests/claude-compaction.test.ts +101 -0
- package/server/modules/providers/tests/claude-message-model.test.ts +160 -0
- package/server/modules/providers/tests/claude-models.test.ts +87 -0
- package/server/modules/providers/tests/claude-runtime-hold.test.ts +309 -0
- package/server/modules/providers/tests/claude-sessions.test.ts +2167 -37
- package/server/modules/providers/tests/claude-settings-permissions.test.ts +271 -0
- 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-models.test.ts +37 -0
- package/server/modules/providers/tests/codex-runtime.test.ts +94 -0
- package/server/modules/providers/tests/codex-session-fork.test.ts +59 -0
- package/server/modules/providers/tests/codex-sessions.test.ts +396 -15
- package/server/modules/providers/tests/cursor-auth.test.ts +187 -0
- package/server/modules/providers/tests/opencode-models.test.ts +266 -8
- package/server/modules/providers/tests/opencode-sessions.test.ts +40 -1
- package/server/modules/providers/tests/provider-capabilities.service.test.ts +37 -0
- package/server/modules/providers/tests/provider-models.service.test.ts +99 -1
- package/server/modules/providers/tests/provider-token-usage.service.test.ts +188 -1
- package/server/modules/providers/tests/provider.routes.test.ts +230 -1
- package/server/modules/providers/tests/session-fork.test.ts +173 -0
- package/server/modules/providers/tests/session-history-cache.service.test.ts +165 -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 +303 -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 +216 -0
- package/server/modules/scheduled-messages/services/scheduled-messages.service.ts +121 -0
- package/server/modules/scheduled-messages/tests/scheduled-messages.test.ts +354 -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/README.md +4 -2
- package/server/modules/websocket/index.ts +8 -0
- package/server/modules/websocket/services/chat-run-registry.service.ts +45 -54
- package/server/modules/websocket/services/chat-session-writer.service.ts +36 -6
- package/server/modules/websocket/services/chat-websocket.service.ts +302 -16
- package/server/modules/websocket/services/session-upsert-broadcast.service.ts +108 -0
- package/server/modules/websocket/services/shell-websocket.service.ts +276 -22
- package/server/modules/websocket/tests/chat-background-subscribe.test.ts +167 -0
- 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/chat-stop-task.test.ts +127 -0
- package/server/modules/websocket/tests/session-upsert-broadcast.test.ts +138 -0
- package/server/modules/websocket/tests/shell-websocket.service.test.ts +629 -0
- package/server/shared/claude-cli-path.ts +24 -6
- package/server/shared/index.ts +3 -0
- package/server/shared/interfaces.ts +91 -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/tests/read-only-roots.test.ts +139 -0
- package/server/shared/tests/strip-ansi-sequences.test.ts +42 -0
- package/server/shared/types.ts +382 -7
- package/server/shared/utils.ts +176 -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
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
|
-
import { mkdtemp, rm } from 'node:fs/promises';
|
|
2
|
+
import { appendFile, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import test from 'node:test';
|
|
6
6
|
|
|
7
7
|
import { closeConnection, initializeDatabase, projectsDb, sessionsDb } from '@/modules/database/index.js';
|
|
8
|
+
import { providerRegistry } from '@/modules/providers/provider.registry.js';
|
|
8
9
|
import { sessionsService } from '@/modules/providers/services/sessions.service.js';
|
|
10
|
+
import { chatRunRegistry } from '@/modules/websocket/index.js';
|
|
11
|
+
import type { IProvider } from '@/shared/interfaces.js';
|
|
12
|
+
import type { BackgroundTaskSummary } from '@/shared/types.js';
|
|
9
13
|
|
|
10
14
|
async function withIsolatedDatabase(runTest: () => void | Promise<void>): Promise<void> {
|
|
11
15
|
const previousDatabasePath = process.env.DATABASE_PATH;
|
|
@@ -125,3 +129,301 @@ test('recent sessions map project metadata and preserve database pagination', {
|
|
|
125
129
|
});
|
|
126
130
|
});
|
|
127
131
|
});
|
|
132
|
+
|
|
133
|
+
/** One Claude transcript row of user or assistant text, linked by uuid chain. */
|
|
134
|
+
function claudeTextRow(
|
|
135
|
+
sessionId: string,
|
|
136
|
+
role: 'user' | 'assistant',
|
|
137
|
+
text: string,
|
|
138
|
+
ordinal: number,
|
|
139
|
+
): Record<string, unknown> {
|
|
140
|
+
return {
|
|
141
|
+
type: role,
|
|
142
|
+
uuid: `row-${ordinal}`,
|
|
143
|
+
parentUuid: ordinal === 0 ? null : `row-${ordinal - 1}`,
|
|
144
|
+
timestamp: new Date(Date.UTC(2026, 0, 1, 0, 0, ordinal)).toISOString(),
|
|
145
|
+
sessionId,
|
|
146
|
+
message: { role, content: [{ type: 'text', text }] },
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
test('history pages are sliced from the cached full transcript and see appended rows', { concurrency: false }, async () => {
|
|
151
|
+
const transcriptDirectory = await mkdtemp(path.join(os.tmpdir(), 'sessions-service-history-'));
|
|
152
|
+
const sessionId = 'claude-history-cache-session';
|
|
153
|
+
const transcriptPath = path.join(transcriptDirectory, `${sessionId}.jsonl`);
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
await withIsolatedDatabase(async () => {
|
|
157
|
+
const rows = [
|
|
158
|
+
claudeTextRow(sessionId, 'user', 'one', 0),
|
|
159
|
+
claudeTextRow(sessionId, 'assistant', 'reply one', 1),
|
|
160
|
+
claudeTextRow(sessionId, 'user', 'two', 2),
|
|
161
|
+
claudeTextRow(sessionId, 'assistant', 'reply two', 3),
|
|
162
|
+
];
|
|
163
|
+
await writeFile(transcriptPath, `${rows.map((row) => JSON.stringify(row)).join('\n')}\n`, 'utf8');
|
|
164
|
+
sessionsDb.createSession(
|
|
165
|
+
sessionId,
|
|
166
|
+
'claude',
|
|
167
|
+
'/tmp/history-cache-project',
|
|
168
|
+
'History cache conversation',
|
|
169
|
+
'2026-01-01T00:00:00.000Z',
|
|
170
|
+
'2026-01-01T00:00:10.000Z',
|
|
171
|
+
transcriptPath,
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
const page = await sessionsService.fetchHistory(sessionId, { limit: 2, offset: 0 });
|
|
175
|
+
assert.equal(page.total, 4);
|
|
176
|
+
assert.equal(page.hasMore, true);
|
|
177
|
+
assert.deepEqual(page.messages.map((message) => message.content), ['two', 'reply two']);
|
|
178
|
+
|
|
179
|
+
// A row appended after the page was cached must appear on the next read.
|
|
180
|
+
await appendFile(
|
|
181
|
+
transcriptPath,
|
|
182
|
+
`${JSON.stringify(claudeTextRow(sessionId, 'user', 'three', 4))}\n`,
|
|
183
|
+
'utf8',
|
|
184
|
+
);
|
|
185
|
+
const refreshed = await sessionsService.fetchHistory(sessionId, { limit: 2, offset: 0 });
|
|
186
|
+
assert.equal(refreshed.total, 5);
|
|
187
|
+
assert.deepEqual(refreshed.messages.map((message) => message.content), ['reply two', 'three']);
|
|
188
|
+
|
|
189
|
+
// An older page keeps the tail-offset contract while served from cache.
|
|
190
|
+
const older = await sessionsService.fetchHistory(sessionId, { limit: 2, offset: 2 });
|
|
191
|
+
assert.deepEqual(older.messages.map((message) => message.content), ['reply one', 'two']);
|
|
192
|
+
assert.equal(older.hasMore, true);
|
|
193
|
+
});
|
|
194
|
+
} finally {
|
|
195
|
+
await rm(transcriptDirectory, { recursive: true, force: true });
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Stands in for the provider registry with one provider per id, each with the
|
|
201
|
+
* runtime given — the seam through which a runtime's background work reaches
|
|
202
|
+
* the running-sessions list without an SDK run behind it.
|
|
203
|
+
*/
|
|
204
|
+
async function withProviders(
|
|
205
|
+
runtimes: Partial<Record<'claude' | 'codex', IProvider['runtime']>>,
|
|
206
|
+
runTest: () => void | Promise<void>,
|
|
207
|
+
): Promise<void> {
|
|
208
|
+
const realListProviders = providerRegistry.listProviders;
|
|
209
|
+
providerRegistry.listProviders = () =>
|
|
210
|
+
Object.entries(runtimes).map(([id, runtime]) => ({ id, runtime }) as IProvider);
|
|
211
|
+
try {
|
|
212
|
+
await runTest();
|
|
213
|
+
} finally {
|
|
214
|
+
providerRegistry.listProviders = realListProviders;
|
|
215
|
+
chatRunRegistry.clearAll();
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Runs `runTest` against a Claude CLI process registry containing exactly
|
|
221
|
+
* `records`, so the running-sessions tests never read the developer's real
|
|
222
|
+
* `~/.claude/sessions`.
|
|
223
|
+
*/
|
|
224
|
+
async function withClaudeCliRegistry(
|
|
225
|
+
records: Array<Record<string, unknown>>,
|
|
226
|
+
runTest: () => void | Promise<void>,
|
|
227
|
+
): Promise<void> {
|
|
228
|
+
const homeDirectory = await mkdtemp(path.join(os.tmpdir(), 'claude-cli-registry-'));
|
|
229
|
+
await mkdir(path.join(homeDirectory, '.claude', 'sessions'), { recursive: true });
|
|
230
|
+
for (const record of records) {
|
|
231
|
+
await writeFile(
|
|
232
|
+
path.join(homeDirectory, '.claude', 'sessions', `${record.pid}.json`),
|
|
233
|
+
JSON.stringify(record),
|
|
234
|
+
'utf8',
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const realHomedir = os.homedir;
|
|
239
|
+
(os as unknown as { homedir: () => string }).homedir = () => homeDirectory;
|
|
240
|
+
try {
|
|
241
|
+
await runTest();
|
|
242
|
+
} finally {
|
|
243
|
+
(os as unknown as { homedir: () => string }).homedir = realHomedir;
|
|
244
|
+
await rm(homeDirectory, { recursive: true, force: true });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const task = (taskId: string, startedAt: number): BackgroundTaskSummary => ({
|
|
249
|
+
taskId,
|
|
250
|
+
toolUseId: `toolu_${taskId}`,
|
|
251
|
+
taskType: 'local_agent',
|
|
252
|
+
description: `Task ${taskId}`,
|
|
253
|
+
startedAt,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test('running sessions list a session whose background work outlived its turn', { concurrency: false }, async () => {
|
|
257
|
+
await withIsolatedDatabase(async () => {
|
|
258
|
+
sessionsDb.createAppSession('held-session', 'claude', '/tmp/running-project');
|
|
259
|
+
// The turn ran and completed: the registry still holds the finished run.
|
|
260
|
+
const run = chatRunRegistry.startRun({
|
|
261
|
+
appSessionId: 'held-session', provider: 'claude', providerSessionId: null, connection: null, userId: null,
|
|
262
|
+
});
|
|
263
|
+
assert.ok(run);
|
|
264
|
+
run.writer.send({ kind: 'text', content: 'launched', sessionId: 'held-session', provider: 'claude' });
|
|
265
|
+
run.writer.sendComplete({ exitCode: 0 });
|
|
266
|
+
assert.equal(run.status, 'completed');
|
|
267
|
+
|
|
268
|
+
const tasks = [task('late', 2_000), task('early', 1_000)];
|
|
269
|
+
await withClaudeCliRegistry([], async () => await withProviders(
|
|
270
|
+
{ claude: { run: async () => undefined, abort: () => false, listBackgroundWork: () => [{ sessionId: 'held-session', tasks }] } },
|
|
271
|
+
async () => {
|
|
272
|
+
assert.deepEqual(await sessionsService.listRunningSessions(), [{
|
|
273
|
+
sessionId: 'held-session',
|
|
274
|
+
provider: 'claude',
|
|
275
|
+
startedAt: 1_000,
|
|
276
|
+
lastSeq: run.lastSeq,
|
|
277
|
+
background: true,
|
|
278
|
+
canInterrupt: false,
|
|
279
|
+
tasks,
|
|
280
|
+
}]);
|
|
281
|
+
assert.ok(run.lastSeq > 0, 'the finished run still reports its sequence for replay');
|
|
282
|
+
},
|
|
283
|
+
));
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test('running sessions carry their tasks on a chat run that is still going', { concurrency: false }, async () => {
|
|
288
|
+
await withIsolatedDatabase(async () => {
|
|
289
|
+
sessionsDb.createAppSession('busy-session', 'claude', '/tmp/running-project');
|
|
290
|
+
sessionsDb.createAppSession('idle-session', 'codex', '/tmp/running-project');
|
|
291
|
+
const run = chatRunRegistry.startRun({
|
|
292
|
+
appSessionId: 'busy-session', provider: 'claude', providerSessionId: null, connection: null, userId: null,
|
|
293
|
+
});
|
|
294
|
+
assert.ok(run);
|
|
295
|
+
|
|
296
|
+
const tasks = [task('agent', 5_000)];
|
|
297
|
+
await withClaudeCliRegistry([], async () => await withProviders(
|
|
298
|
+
{
|
|
299
|
+
claude: { run: async () => undefined, abort: () => false, listBackgroundWork: () => [{ sessionId: 'busy-session', tasks }] },
|
|
300
|
+
// A runtime without background work contributes chat runs alone.
|
|
301
|
+
codex: { run: async () => undefined, abort: () => false },
|
|
302
|
+
},
|
|
303
|
+
async () => {
|
|
304
|
+
const sessions = await sessionsService.listRunningSessions();
|
|
305
|
+
assert.equal(sessions.length, 1, 'a session with a running chat run is listed once');
|
|
306
|
+
assert.deepEqual(sessions[0], {
|
|
307
|
+
sessionId: 'busy-session',
|
|
308
|
+
provider: 'claude',
|
|
309
|
+
startedAt: run.startedAt,
|
|
310
|
+
lastSeq: 0,
|
|
311
|
+
tasks,
|
|
312
|
+
});
|
|
313
|
+
},
|
|
314
|
+
));
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test('running sessions with no background work are the chat runs alone', { concurrency: false }, async () => {
|
|
319
|
+
await withIsolatedDatabase(async () => {
|
|
320
|
+
sessionsDb.createAppSession('plain-session', 'codex', '/tmp/running-project');
|
|
321
|
+
const run = chatRunRegistry.startRun({
|
|
322
|
+
appSessionId: 'plain-session', provider: 'codex', providerSessionId: null, connection: null, userId: null,
|
|
323
|
+
});
|
|
324
|
+
assert.ok(run);
|
|
325
|
+
|
|
326
|
+
await withClaudeCliRegistry([], async () => await withProviders(
|
|
327
|
+
{ codex: { run: async () => undefined, abort: () => false } },
|
|
328
|
+
async () => {
|
|
329
|
+
assert.deepEqual(await sessionsService.listRunningSessions(), [{
|
|
330
|
+
sessionId: 'plain-session', provider: 'codex', startedAt: run.startedAt, lastSeq: 0,
|
|
331
|
+
}]);
|
|
332
|
+
},
|
|
333
|
+
));
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test('running sessions report a turn driven by the Claude CLI', { concurrency: false }, async () => {
|
|
338
|
+
await withIsolatedDatabase(async () => {
|
|
339
|
+
sessionsDb.createAppSession('cli-session', 'claude', '/tmp/running-project');
|
|
340
|
+
sessionsDb.assignProviderSessionId('cli-session', 'claude-native-1');
|
|
341
|
+
|
|
342
|
+
// `process.pid` is the one pid guaranteed to be alive; omitting procStart
|
|
343
|
+
// keeps the fixture portable, since the start-time cross-check is Linux-only.
|
|
344
|
+
await withClaudeCliRegistry(
|
|
345
|
+
[{ pid: process.pid, sessionId: 'claude-native-1', status: 'busy', entrypoint: 'cli', startedAt: 4_000 }],
|
|
346
|
+
async () => await withProviders(
|
|
347
|
+
{ claude: { run: async () => undefined, abort: () => false } },
|
|
348
|
+
async () => {
|
|
349
|
+
assert.deepEqual(await sessionsService.listRunningSessions(), [{
|
|
350
|
+
sessionId: 'cli-session',
|
|
351
|
+
provider: 'claude',
|
|
352
|
+
startedAt: 4_000,
|
|
353
|
+
lastSeq: 0,
|
|
354
|
+
canInterrupt: false,
|
|
355
|
+
statusText: 'Running in the Claude CLI',
|
|
356
|
+
}]);
|
|
357
|
+
},
|
|
358
|
+
),
|
|
359
|
+
);
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
test('running sessions ignore registry files a crash left behind', { concurrency: false }, async () => {
|
|
364
|
+
await withIsolatedDatabase(async () => {
|
|
365
|
+
sessionsDb.createAppSession('stale-session', 'claude', '/tmp/running-project');
|
|
366
|
+
sessionsDb.assignProviderSessionId('stale-session', 'claude-native-stale');
|
|
367
|
+
|
|
368
|
+
await withClaudeCliRegistry(
|
|
369
|
+
[
|
|
370
|
+
// The process is gone: signal 0 cannot find this pid.
|
|
371
|
+
{ pid: 4_194_302, sessionId: 'claude-native-stale', status: 'busy', startedAt: 1_000 },
|
|
372
|
+
// Alive, but waiting for input rather than producing a response.
|
|
373
|
+
{ pid: process.pid, sessionId: 'claude-native-idle', status: 'idle', startedAt: 2_000 },
|
|
374
|
+
],
|
|
375
|
+
async () => await withProviders(
|
|
376
|
+
{ claude: { run: async () => undefined, abort: () => false } },
|
|
377
|
+
async () => {
|
|
378
|
+
assert.deepEqual(await sessionsService.listRunningSessions(), []);
|
|
379
|
+
},
|
|
380
|
+
),
|
|
381
|
+
);
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
test('a CLI turn does not duplicate a session already running a chat run', { concurrency: false }, async () => {
|
|
386
|
+
await withIsolatedDatabase(async () => {
|
|
387
|
+
sessionsDb.createAppSession('both-session', 'claude', '/tmp/running-project');
|
|
388
|
+
sessionsDb.assignProviderSessionId('both-session', 'claude-native-both');
|
|
389
|
+
const run = chatRunRegistry.startRun({
|
|
390
|
+
appSessionId: 'both-session', provider: 'claude', providerSessionId: 'claude-native-both', connection: null, userId: null,
|
|
391
|
+
});
|
|
392
|
+
assert.ok(run);
|
|
393
|
+
|
|
394
|
+
await withClaudeCliRegistry(
|
|
395
|
+
[{ pid: process.pid, sessionId: 'claude-native-both', status: 'busy', startedAt: 3_000 }],
|
|
396
|
+
async () => await withProviders(
|
|
397
|
+
{ claude: { run: async () => undefined, abort: () => false } },
|
|
398
|
+
async () => {
|
|
399
|
+
const sessions = await sessionsService.listRunningSessions();
|
|
400
|
+
assert.equal(sessions.length, 1, 'the chat run already covers this session');
|
|
401
|
+
assert.equal(sessions[0].canInterrupt, undefined, 'the interruptible chat run entry wins');
|
|
402
|
+
},
|
|
403
|
+
),
|
|
404
|
+
);
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
test('an unreadable registry never fails the running-sessions poll', { concurrency: false }, async () => {
|
|
409
|
+
await withIsolatedDatabase(async () => {
|
|
410
|
+
sessionsDb.createAppSession('safe-session', 'claude', '/tmp/running-project');
|
|
411
|
+
sessionsDb.assignProviderSessionId('safe-session', 'claude-native-safe');
|
|
412
|
+
|
|
413
|
+
await withClaudeCliRegistry(
|
|
414
|
+
[{ pid: process.pid, sessionId: 'claude-native-safe', status: 'busy', startedAt: 5_000 }],
|
|
415
|
+
async () => {
|
|
416
|
+
// A half-written file sits alongside the good one; it must be skipped
|
|
417
|
+
// rather than abort the whole poll.
|
|
418
|
+
await writeFile(path.join(os.homedir(), '.claude', 'sessions', 'broken.json'), '{"pid":', 'utf8');
|
|
419
|
+
await withProviders(
|
|
420
|
+
{ claude: { run: async () => undefined, abort: () => false } },
|
|
421
|
+
async () => {
|
|
422
|
+
const sessions = await sessionsService.listRunningSessions();
|
|
423
|
+
assert.deepEqual(sessions.map((session) => session.sessionId), ['safe-session']);
|
|
424
|
+
},
|
|
425
|
+
);
|
|
426
|
+
},
|
|
427
|
+
);
|
|
428
|
+
});
|
|
429
|
+
});
|
|
@@ -69,7 +69,8 @@ const writeClaudePluginCommand = async (
|
|
|
69
69
|
|
|
70
70
|
/**
|
|
71
71
|
* This test covers Claude user/project skill folders plus plugin discovery from
|
|
72
|
-
* installed plugin command files and
|
|
72
|
+
* installed plugin command files and plugin skill files, including a plugin that
|
|
73
|
+
* ships both.
|
|
73
74
|
*/
|
|
74
75
|
test('providerSkillsService lists claude user, project, and enabled plugin skills', { concurrency: false }, async () => {
|
|
75
76
|
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'llm-skills-claude-'));
|
|
@@ -144,11 +145,19 @@ test('providerSkillsService lists claude user, project, and enabled plugin skill
|
|
|
144
145
|
);
|
|
145
146
|
await writeSkill(
|
|
146
147
|
path.join(commandPluginInstallPath, 'skills'),
|
|
147
|
-
'
|
|
148
|
-
'
|
|
149
|
-
'
|
|
148
|
+
'command-plugin-skill-dir',
|
|
149
|
+
'command-plugin-skill',
|
|
150
|
+
'Skill shipped beside plugin commands',
|
|
150
151
|
);
|
|
151
152
|
await writeClaudePluginManifest(skillPluginInstallPath, 'ExampleSkills');
|
|
153
|
+
// A commands folder holding nothing this reader takes must not hide the
|
|
154
|
+
// skills beside it: plugins do ship commands in other agents' formats.
|
|
155
|
+
await fs.mkdir(path.join(skillPluginInstallPath, 'commands'), { recursive: true });
|
|
156
|
+
await fs.writeFile(
|
|
157
|
+
path.join(skillPluginInstallPath, 'commands', 'other-agent.toml'),
|
|
158
|
+
'description = "Command for another agent"\n',
|
|
159
|
+
'utf8',
|
|
160
|
+
);
|
|
152
161
|
await writeSkill(
|
|
153
162
|
path.join(skillPluginInstallPath, 'skills'),
|
|
154
163
|
'claude-plugin-dir',
|
|
@@ -280,7 +289,14 @@ test('providerSkillsService lists claude user, project, and enabled plugin skill
|
|
|
280
289
|
assert.equal(pluginCommand?.command, '/Notion:insert-row');
|
|
281
290
|
assert.equal(pluginCommand?.description, 'Insert a Notion database row');
|
|
282
291
|
assert.match(pluginCommand?.sourcePath ?? '', /commands[\\/]insert-row\.md$/);
|
|
283
|
-
|
|
292
|
+
// A plugin that ships commands keeps its skills: the CLI offers both, so
|
|
293
|
+
// the menu lists both rather than letting the commands folder hide them.
|
|
294
|
+
const commandPluginSkill = byName.get('command-plugin-skill');
|
|
295
|
+
assert.equal(commandPluginSkill?.scope, 'plugin');
|
|
296
|
+
assert.equal(commandPluginSkill?.pluginName, 'Notion');
|
|
297
|
+
assert.equal(commandPluginSkill?.pluginId, 'notion@notion-marketplace');
|
|
298
|
+
assert.equal(commandPluginSkill?.command, '/Notion:command-plugin-skill');
|
|
299
|
+
assert.equal(commandPluginSkill?.description, 'Skill shipped beside plugin commands');
|
|
284
300
|
|
|
285
301
|
const pluginSkill = byName.get('claude-plugin');
|
|
286
302
|
assert.equal(pluginSkill?.scope, 'plugin');
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// The HTTP surface for scheduling a message to a session, mounted by the app.
|
|
2
|
+
export { default as scheduledMessagesRoutes } from './scheduled-messages.routes.js';
|
|
3
|
+
|
|
4
|
+
// The timer that sends them, started and stopped with the server.
|
|
5
|
+
export {
|
|
6
|
+
initializeScheduledMessageDispatcher,
|
|
7
|
+
closeScheduledMessageDispatcher,
|
|
8
|
+
} from './services/scheduled-message-dispatcher.service.js';
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import type { Request, Response } from 'express';
|
|
3
|
+
|
|
4
|
+
import { scheduledMessagesService } from '@/modules/scheduled-messages/services/scheduled-messages.service.js';
|
|
5
|
+
import { AppError, asyncHandler, createApiSuccessResponse } from '@/shared/utils.js';
|
|
6
|
+
|
|
7
|
+
type AuthenticatedRequest = Request & { user?: { id?: number | string } };
|
|
8
|
+
|
|
9
|
+
function readUserId(request: Request): number {
|
|
10
|
+
const userId = Number((request as AuthenticatedRequest).user?.id);
|
|
11
|
+
if (!Number.isInteger(userId)) {
|
|
12
|
+
throw new AppError('Authenticated user is required.', {
|
|
13
|
+
code: 'USER_REQUIRED',
|
|
14
|
+
statusCode: 401,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return userId;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function readString(value: unknown, field: string): string {
|
|
21
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
22
|
+
throw new AppError(`${field} is required.`, {
|
|
23
|
+
code: 'INVALID_REQUEST_BODY',
|
|
24
|
+
statusCode: 400,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const router = express.Router();
|
|
31
|
+
|
|
32
|
+
router.get(
|
|
33
|
+
'/',
|
|
34
|
+
asyncHandler(async (req: Request, res: Response) => {
|
|
35
|
+
const sessionId = typeof req.query.sessionId === 'string' ? req.query.sessionId : '';
|
|
36
|
+
const userId = readUserId(req);
|
|
37
|
+
res.json(createApiSuccessResponse(
|
|
38
|
+
sessionId
|
|
39
|
+
? scheduledMessagesService.listForSession(userId, sessionId)
|
|
40
|
+
: scheduledMessagesService.listPending(userId),
|
|
41
|
+
));
|
|
42
|
+
}),
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
router.post(
|
|
46
|
+
'/',
|
|
47
|
+
asyncHandler(async (req: Request, res: Response) => {
|
|
48
|
+
const body = (req.body ?? {}) as Record<string, unknown>;
|
|
49
|
+
const result = scheduledMessagesService.schedule({
|
|
50
|
+
userId: readUserId(req),
|
|
51
|
+
sessionId: readString(body.sessionId, 'sessionId'),
|
|
52
|
+
content: readString(body.content, 'content'),
|
|
53
|
+
options: body.options,
|
|
54
|
+
scheduledFor: readString(body.scheduledFor, 'scheduledFor'),
|
|
55
|
+
});
|
|
56
|
+
res.status(201).json(createApiSuccessResponse(result));
|
|
57
|
+
}),
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
router.delete(
|
|
61
|
+
'/:id',
|
|
62
|
+
asyncHandler(async (req: Request, res: Response) => {
|
|
63
|
+
scheduledMessagesService.cancel(readUserId(req), readString(req.params.id, 'id'));
|
|
64
|
+
res.json(createApiSuccessResponse({ cancelled: true }));
|
|
65
|
+
}),
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
export default router;
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { scheduledMessagesDb, sessionDraftsDb } from '@/modules/database/index.js';
|
|
2
|
+
import type { QueuedSessionMessageRecord, ScheduledMessageRow } from '@/modules/database/index.js';
|
|
3
|
+
import { chatRunRegistry, runDetachedChatTurn } from '@/modules/websocket/index.js';
|
|
4
|
+
import type { ProviderRuntimeGateway } from '@/modules/websocket/index.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* How often due messages are looked for.
|
|
8
|
+
*
|
|
9
|
+
* A minute is the granularity the composer offers, and a claim is indexed on
|
|
10
|
+
* `(status, scheduled_for)`, so the poll is one cheap query. Anything finer
|
|
11
|
+
* would buy precision nobody asked for.
|
|
12
|
+
*/
|
|
13
|
+
const POLL_INTERVAL_MS = 30_000;
|
|
14
|
+
|
|
15
|
+
let pollTimer: ReturnType<typeof setInterval> | null = null;
|
|
16
|
+
let dispatchInFlight = false;
|
|
17
|
+
|
|
18
|
+
type StoredQueuedMessage = {
|
|
19
|
+
content: string;
|
|
20
|
+
options: Record<string, unknown>;
|
|
21
|
+
attachments: unknown[];
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function readOptions(raw: string): Record<string, unknown> {
|
|
25
|
+
try {
|
|
26
|
+
const parsed: unknown = JSON.parse(raw);
|
|
27
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
28
|
+
? parsed as Record<string, unknown>
|
|
29
|
+
: {};
|
|
30
|
+
} catch {
|
|
31
|
+
return {};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function readQueuedMessage(value: unknown): StoredQueuedMessage | null {
|
|
36
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const record = value as Record<string, unknown>;
|
|
40
|
+
const content = typeof record.content === 'string' ? record.content : '';
|
|
41
|
+
const attachments = Array.isArray(record.attachments)
|
|
42
|
+
? record.attachments
|
|
43
|
+
: Array.isArray(record.images)
|
|
44
|
+
? record.images
|
|
45
|
+
: [];
|
|
46
|
+
if (!content.trim() && attachments.length === 0) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
const options = record.options && typeof record.options === 'object' && !Array.isArray(record.options)
|
|
50
|
+
? record.options as Record<string, unknown>
|
|
51
|
+
: {};
|
|
52
|
+
return { content, options, attachments };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function sendClaimedQueuedMessage(
|
|
56
|
+
candidate: QueuedSessionMessageRecord,
|
|
57
|
+
runtime: ProviderRuntimeGateway,
|
|
58
|
+
): Promise<void> {
|
|
59
|
+
const message = readQueuedMessage(candidate.queuedMessage);
|
|
60
|
+
if (!message) {
|
|
61
|
+
sessionDraftsDb.deleteEmptyDraft(candidate.userId, candidate.sessionId);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const result = await runDetachedChatTurn(
|
|
66
|
+
{
|
|
67
|
+
sessionId: candidate.sessionId,
|
|
68
|
+
userId: candidate.userId,
|
|
69
|
+
content: message.content,
|
|
70
|
+
options: { ...message.options, attachments: message.attachments },
|
|
71
|
+
},
|
|
72
|
+
{ runtime },
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
// The registry check and run reservation are separate operations. If a run
|
|
76
|
+
// wins that tiny race, put the turn back so the next poll tries again.
|
|
77
|
+
if (!result.started && result.error === 'A run was already in progress for this session.') {
|
|
78
|
+
sessionDraftsDb.restoreQueuedMessage(candidate);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
sessionDraftsDb.deleteEmptyDraft(candidate.userId, candidate.sessionId);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Whether a queued turn has to keep waiting for the session.
|
|
86
|
+
*
|
|
87
|
+
* A turn that ends with background work still going (a backgrounded command,
|
|
88
|
+
* agent or workflow) is marked completed, but its CLI process stays open so
|
|
89
|
+
* that work can report back. A new turn replaces that process and stops the
|
|
90
|
+
* work, which is why the composer asks before an interactive send. A queued
|
|
91
|
+
* turn was promised to go "when this finishes", so it waits for that work too.
|
|
92
|
+
*/
|
|
93
|
+
function isSessionBusy(sessionId: string, runtime: ProviderRuntimeGateway): boolean {
|
|
94
|
+
return chatRunRegistry.isProcessing(sessionId) || runtime.hasBackgroundWork(sessionId);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Sends every persisted queued turn whose session is currently idle. */
|
|
98
|
+
export async function dispatchQueuedMessages(runtime: ProviderRuntimeGateway): Promise<number> {
|
|
99
|
+
const candidates = sessionDraftsDb.listQueuedMessages();
|
|
100
|
+
let claimed = 0;
|
|
101
|
+
|
|
102
|
+
await Promise.all(candidates.map(async (candidate) => {
|
|
103
|
+
if (isSessionBusy(candidate.sessionId, runtime)) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (!sessionDraftsDb.claimQueuedMessage(candidate)) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
claimed += 1;
|
|
110
|
+
await sendClaimedQueuedMessage(candidate, runtime);
|
|
111
|
+
}));
|
|
112
|
+
|
|
113
|
+
return claimed;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function sendClaimedMessage(
|
|
117
|
+
row: ScheduledMessageRow,
|
|
118
|
+
runtime: ProviderRuntimeGateway,
|
|
119
|
+
): Promise<void> {
|
|
120
|
+
try {
|
|
121
|
+
const result = await runDetachedChatTurn(
|
|
122
|
+
{
|
|
123
|
+
sessionId: row.session_id,
|
|
124
|
+
userId: row.user_id,
|
|
125
|
+
content: row.content,
|
|
126
|
+
options: readOptions(row.options),
|
|
127
|
+
// The user picked this time on purpose; a run that happens to be going
|
|
128
|
+
// is aborted so the scheduled message lands when it was due, instead
|
|
129
|
+
// of being recorded as "not sent — session was busy".
|
|
130
|
+
interruptActiveRun: true,
|
|
131
|
+
},
|
|
132
|
+
{ runtime },
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
// Recorded rather than retried, and recorded whether the run never started
|
|
136
|
+
// (deleted session, unavailable provider) or started and then failed.
|
|
137
|
+
// Silently dropping a message the user scheduled is worse than telling
|
|
138
|
+
// them it did not go.
|
|
139
|
+
if (!result.started || result.error) {
|
|
140
|
+
scheduledMessagesDb.markFailed(row.id, result.error ?? 'The session was unavailable when this was due.');
|
|
141
|
+
}
|
|
142
|
+
} catch (error) {
|
|
143
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
144
|
+
scheduledMessagesDb.markFailed(row.id, message);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Sends every message whose time has come.
|
|
150
|
+
*
|
|
151
|
+
* Exported so a test can drive one pass without waiting on the timer.
|
|
152
|
+
*/
|
|
153
|
+
export async function dispatchDueScheduledMessages(
|
|
154
|
+
runtime: ProviderRuntimeGateway,
|
|
155
|
+
now: Date = new Date(),
|
|
156
|
+
): Promise<number> {
|
|
157
|
+
// Claimed before any of them runs, so a long turn cannot let the next poll
|
|
158
|
+
// pick the same message up again.
|
|
159
|
+
const due = scheduledMessagesDb.claimDue(now);
|
|
160
|
+
if (due.length === 0) {
|
|
161
|
+
return 0;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Sequentially: a session can only have one run at a time, and two due
|
|
165
|
+
// messages for the same session must not race each other into it.
|
|
166
|
+
for (const row of due) {
|
|
167
|
+
await sendClaimedMessage(row, runtime);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return due.length;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Starts the poll that sends scheduled messages.
|
|
175
|
+
*
|
|
176
|
+
* The schedule lives in the database, so a message stays scheduled across a
|
|
177
|
+
* restart and one that came due while the server was down is sent on the first
|
|
178
|
+
* poll after it comes back, rather than being skipped.
|
|
179
|
+
*/
|
|
180
|
+
export function initializeScheduledMessageDispatcher(runtime: ProviderRuntimeGateway): void {
|
|
181
|
+
if (pollTimer) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const poll = () => {
|
|
186
|
+
// A pass that overruns the interval must not be started again underneath
|
|
187
|
+
// itself; the claim is transactional but the runs are not.
|
|
188
|
+
if (dispatchInFlight) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
dispatchInFlight = true;
|
|
192
|
+
void dispatchDueScheduledMessages(runtime)
|
|
193
|
+
.then(() => dispatchQueuedMessages(runtime))
|
|
194
|
+
.catch((error: unknown) => {
|
|
195
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
196
|
+
console.error('[ScheduledMessages] Dispatch pass failed', { error: message });
|
|
197
|
+
})
|
|
198
|
+
.finally(() => {
|
|
199
|
+
dispatchInFlight = false;
|
|
200
|
+
});
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
pollTimer = setInterval(poll, POLL_INTERVAL_MS);
|
|
204
|
+
// Never keep the process alive just to poll for scheduled messages.
|
|
205
|
+
pollTimer.unref?.();
|
|
206
|
+
|
|
207
|
+
// Catch up on anything that came due while the server was not running.
|
|
208
|
+
poll();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function closeScheduledMessageDispatcher(): void {
|
|
212
|
+
if (pollTimer) {
|
|
213
|
+
clearInterval(pollTimer);
|
|
214
|
+
pollTimer = null;
|
|
215
|
+
}
|
|
216
|
+
}
|