@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.
Files changed (453) hide show
  1. package/README.md +269 -0
  2. package/dist/assets/{abnfDiagram-N423BO3Z-C4UAoHJb.js → abnfDiagram-N423BO3Z-Dmkwx38H.js} +1 -1
  3. package/dist/assets/{arc-B8jF-ja9.js → arc-DLe9AYuO.js} +1 -1
  4. package/dist/assets/{architectureDiagram-T3A2C74G-C_Wur_CU.js → architectureDiagram-T3A2C74G-CBbbbNUW.js} +1 -1
  5. package/dist/assets/{blockDiagram-VBNYF7ZC-Cq4P1i8t.js → blockDiagram-VBNYF7ZC-CaMEaWu4.js} +1 -1
  6. package/dist/assets/{c4Diagram-5PPSVZJV-C1wG4eUj.js → c4Diagram-5PPSVZJV-DhjMgNnR.js} +1 -1
  7. package/dist/assets/channel-CWrmwsir.js +1 -0
  8. package/dist/assets/{chunk-2GRJ4B5K-B8fnBG6E.js → chunk-2GRJ4B5K-yjl3-F-i.js} +1 -1
  9. package/dist/assets/{chunk-2Q5K7J3B-WdfPooPJ.js → chunk-2Q5K7J3B-DsAYaWEP.js} +1 -1
  10. package/dist/assets/{chunk-5RXB4S5H-DhZJuG2E.js → chunk-5RXB4S5H-Bt5qmqOX.js} +1 -1
  11. package/dist/assets/{chunk-5VM5RSS4-B0rjslal.js → chunk-5VM5RSS4-yWrWVvUt.js} +1 -1
  12. package/dist/assets/{chunk-6Q2QTUOP-CG2Oj4jL.js → chunk-6Q2QTUOP-Dt66K5xb.js} +1 -1
  13. package/dist/assets/{chunk-GF5L2VYU-rrl2lRxY.js → chunk-GF5L2VYU-DA0vzaj4.js} +1 -1
  14. package/dist/assets/{chunk-JWPE2WC7-CjVbrVMA.js → chunk-JWPE2WC7-CUkbx96V.js} +1 -1
  15. package/dist/assets/{chunk-KBJHAD2P-CRzKgU8S.js → chunk-KBJHAD2P-DWKxx1WH.js} +1 -1
  16. package/dist/assets/{chunk-RYQCIY6F-DBz4GTZF.js → chunk-RYQCIY6F-BqluplN0.js} +1 -1
  17. package/dist/assets/{chunk-XXDRQBXY-wHxalsXk.js → chunk-XXDRQBXY-DelKg3Xf.js} +1 -1
  18. package/dist/assets/classDiagram-JCYQIIEL-COAGCC6w.js +1 -0
  19. package/dist/assets/classDiagram-v2-OCEON4UE-COAGCC6w.js +1 -0
  20. package/dist/assets/{cose-bilkent-JH36ORCC-s1hygY8e.js → cose-bilkent-JH36ORCC-DHV93-Em.js} +1 -1
  21. package/dist/assets/{cynefin-VYW2F7L2-C4LGV15i.js → cynefin-VYW2F7L2-B9HZgoGX.js} +1 -1
  22. package/dist/assets/{cynefinDiagram-MW4NZA55-C5uhu7B3.js → cynefinDiagram-MW4NZA55-hFjjEQg8.js} +1 -1
  23. package/dist/assets/{dagre-VZM6K2ZE-B-OBTewm.js → dagre-VZM6K2ZE-CpeVOC7V.js} +1 -1
  24. package/dist/assets/{diagram-7IWD3JNH-BjGKQxih.js → diagram-7IWD3JNH-D0H1UoRA.js} +1 -1
  25. package/dist/assets/{diagram-B4RE2ZJO-BMKz1X2L.js → diagram-B4RE2ZJO-6Y7BQUup.js} +1 -1
  26. package/dist/assets/{diagram-LBJQPF4R-DNh-GhKC.js → diagram-LBJQPF4R-C0nNOwht.js} +1 -1
  27. package/dist/assets/{diagram-Q27KOJAE-BaTG9jPT.js → diagram-Q27KOJAE-En6QlOVX.js} +1 -1
  28. package/dist/assets/{diagram-UB23O5K3-BUwo2OvJ.js → diagram-UB23O5K3-CmPiNeGD.js} +1 -1
  29. package/dist/assets/{ebnfDiagram-BXEA7PRR-DAZgfzX2.js → ebnfDiagram-BXEA7PRR-Bygzp1Qt.js} +1 -1
  30. package/dist/assets/{erDiagram-JOGREHBK-1w1CcGEb.js → erDiagram-JOGREHBK-CwScRfzl.js} +1 -1
  31. package/dist/assets/{flowDiagram-UKHOOZJN-D4EII8Jj.js → flowDiagram-UKHOOZJN-bKX-0xP6.js} +1 -1
  32. package/dist/assets/{ganttDiagram-PKOTCBZU-ChvJZAgB.js → ganttDiagram-PKOTCBZU-DtZPKXGd.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-DS77QQ5N-DtOn41S8.js → gitGraphDiagram-DS77QQ5N-CUOtdL1o.js} +1 -1
  34. package/dist/assets/index-CdjIFxM2.js +5004 -0
  35. package/dist/assets/index-DgafhGTF.css +32 -0
  36. package/dist/assets/{infoDiagram-6WML65LV-DN_PVEJZ.js → infoDiagram-6WML65LV-ciRQTS0h.js} +1 -1
  37. package/dist/assets/{ishikawaDiagram-WSZJBQD7-DbkF1kXb.js → ishikawaDiagram-WSZJBQD7-CgjhQZ84.js} +1 -1
  38. package/dist/assets/{journeyDiagram-NVQOT4AX-pJwmhddE.js → journeyDiagram-NVQOT4AX-BsbQSeIW.js} +1 -1
  39. package/dist/assets/{kanban-definition-27J2QSJJ-PPAPgNp5.js → kanban-definition-27J2QSJJ-CrwzY9Jw.js} +1 -1
  40. package/dist/assets/{linear-iPR-J9f7.js → linear-BUaCDr6l.js} +1 -1
  41. package/dist/assets/{mermaid.core-BEUL86Z6.js → mermaid.core-ClgdUU0j.js} +5 -5
  42. package/dist/assets/{mindmap-definition-FAOFIHXS-BvZoBib7.js → mindmap-definition-FAOFIHXS-BictljY1.js} +1 -1
  43. package/dist/assets/{pegDiagram-VL7TDLO6-DhYreaUd.js → pegDiagram-VL7TDLO6-BXdByy_U.js} +1 -1
  44. package/dist/assets/{pieDiagram-7S7Q4E2Y-CrOHlyD0.js → pieDiagram-7S7Q4E2Y-uZsMfkRX.js} +1 -1
  45. package/dist/assets/{quadrantDiagram-CIZ2JOQS-D4g5yH3t.js → quadrantDiagram-CIZ2JOQS-7lCv4QIj.js} +1 -1
  46. package/dist/assets/{railroadDiagram-AXF67PYL-Dj39lHlf.js → railroadDiagram-AXF67PYL-07dxIWbi.js} +1 -1
  47. package/dist/assets/{requirementDiagram-LRYGKXZP-1AS4Zgm_.js → requirementDiagram-LRYGKXZP-Cb31Yv1_.js} +1 -1
  48. package/dist/assets/{sankeyDiagram-W5VNT64P-CKlMn19P.js → sankeyDiagram-W5VNT64P-X0YPSyF9.js} +1 -1
  49. package/dist/assets/{sequenceDiagram-SI44F4Z6-B_j3qyvo.js → sequenceDiagram-SI44F4Z6-BdBxSIlP.js} +1 -1
  50. package/dist/assets/server.browser-CtX3Hdl5.js +27 -0
  51. package/dist/assets/{sizeCapture-X5ZJPWSS-Bp2Op16v.js → sizeCapture-X5ZJPWSS-DQM5xAyD.js} +1 -1
  52. package/dist/assets/{stateDiagram-OKZ733FA-C0cN6DoY.js → stateDiagram-OKZ733FA-DJ3xkLUF.js} +1 -1
  53. package/dist/assets/stateDiagram-v2-UEYNNEHI-vAaE4zNo.js +1 -0
  54. package/dist/assets/{swimlanes-SLNWSIFB-BEvXOV9b.js → swimlanes-SLNWSIFB-rjfYdW0w.js} +2 -2
  55. package/dist/assets/swimlanesDiagram-ULZ7WXOC-TV2IF4Nq.js +8 -0
  56. package/dist/assets/{timeline-definition-Z64GVDOM-zdJQinvZ.js → timeline-definition-Z64GVDOM-DZMra5do.js} +1 -1
  57. package/dist/assets/{vendor-codemirror-OwyKSvPE.js → vendor-codemirror-CYrSt7-U.js} +18 -18
  58. package/dist/assets/{vennDiagram-T6HMQDX7-DuF1uZMY.js → vennDiagram-T6HMQDX7-BevD8uu0.js} +1 -1
  59. package/dist/assets/{wardleyDiagram-T6FBY63Y-B9AlLQup.js → wardleyDiagram-T6FBY63Y-DsIyXB7z.js} +1 -1
  60. package/dist/assets/{xychartDiagram-ELKLHX3M-DTJX48Qg.js → xychartDiagram-ELKLHX3M-G3PlF3ko.js} +1 -1
  61. package/dist/index.html +3 -3
  62. package/dist-server/server/index.js +11 -4
  63. package/dist-server/server/index.js.map +1 -1
  64. package/dist-server/server/modules/agent/agent.module.js +9 -2
  65. package/dist-server/server/modules/agent/agent.module.js.map +1 -1
  66. package/dist-server/server/modules/agent/agent.routes.js +177 -130
  67. package/dist-server/server/modules/agent/agent.routes.js.map +1 -1
  68. package/dist-server/server/modules/agent/tests/agent-run-registration.test.js +306 -0
  69. package/dist-server/server/modules/agent/tests/agent-run-registration.test.js.map +1 -0
  70. package/dist-server/server/modules/agent/tests/agent.routes.test.js +48 -2
  71. package/dist-server/server/modules/agent/tests/agent.routes.test.js.map +1 -1
  72. package/dist-server/server/modules/browser-use/browser-use.routes.js +39 -33
  73. package/dist-server/server/modules/browser-use/browser-use.routes.js.map +1 -1
  74. package/dist-server/server/modules/database/connection.js +1 -1
  75. package/dist-server/server/modules/database/connection.js.map +1 -1
  76. package/dist-server/server/modules/database/index.js +5 -0
  77. package/dist-server/server/modules/database/index.js.map +1 -1
  78. package/dist-server/server/modules/database/migrations.js +45 -1
  79. package/dist-server/server/modules/database/migrations.js.map +1 -1
  80. package/dist-server/server/modules/database/repositories/provider-models.js +60 -6
  81. package/dist-server/server/modules/database/repositories/provider-models.js.map +1 -1
  82. package/dist-server/server/modules/database/repositories/scheduled-messages.db.js +71 -0
  83. package/dist-server/server/modules/database/repositories/scheduled-messages.db.js.map +1 -0
  84. package/dist-server/server/modules/database/repositories/session-drafts.db.js +105 -0
  85. package/dist-server/server/modules/database/repositories/session-drafts.db.js.map +1 -0
  86. package/dist-server/server/modules/database/repositories/sessions.db.js +144 -5
  87. package/dist-server/server/modules/database/repositories/sessions.db.js.map +1 -1
  88. package/dist-server/server/modules/database/repositories/user-preferences.db.js +65 -0
  89. package/dist-server/server/modules/database/repositories/user-preferences.db.js.map +1 -0
  90. package/dist-server/server/modules/database/schema.js +99 -0
  91. package/dist-server/server/modules/database/schema.js.map +1 -1
  92. package/dist-server/server/modules/database/tests/provider-models.db.integration.test.js +99 -0
  93. package/dist-server/server/modules/database/tests/provider-models.db.integration.test.js.map +1 -1
  94. package/dist-server/server/modules/database/tests/sessions.db.integration.test.js +39 -0
  95. package/dist-server/server/modules/database/tests/sessions.db.integration.test.js.map +1 -1
  96. package/dist-server/server/modules/database/tests/user-preferences.db.integration.test.js +105 -0
  97. package/dist-server/server/modules/database/tests/user-preferences.db.integration.test.js.map +1 -0
  98. package/dist-server/server/modules/file-tree/file-tree.module.js +2 -1
  99. package/dist-server/server/modules/file-tree/file-tree.module.js.map +1 -1
  100. package/dist-server/server/modules/file-tree/file-tree.service.js +44 -7
  101. package/dist-server/server/modules/file-tree/file-tree.service.js.map +1 -1
  102. package/dist-server/server/modules/file-tree/tests/file-tree.service.test.js +126 -1
  103. package/dist-server/server/modules/file-tree/tests/file-tree.service.test.js.map +1 -1
  104. package/dist-server/server/modules/git/git-branch-diff.service.js +142 -0
  105. package/dist-server/server/modules/git/git-branch-diff.service.js.map +1 -0
  106. package/dist-server/server/modules/git/git-branch.service.js.map +1 -1
  107. package/dist-server/server/modules/git/git.routes.js +92 -5
  108. package/dist-server/server/modules/git/git.routes.js.map +1 -1
  109. package/dist-server/server/modules/git/tests/git-branch-diff.service.test.js +160 -0
  110. package/dist-server/server/modules/git/tests/git-branch-diff.service.test.js.map +1 -0
  111. package/dist-server/server/modules/projects/project-clone.routes.js +108 -0
  112. package/dist-server/server/modules/projects/project-clone.routes.js.map +1 -0
  113. package/dist-server/server/modules/projects/projects.routes.js +13 -66
  114. package/dist-server/server/modules/projects/projects.routes.js.map +1 -1
  115. package/dist-server/server/modules/projects/services/project-clone-request.service.js +36 -0
  116. package/dist-server/server/modules/projects/services/project-clone-request.service.js.map +1 -0
  117. package/dist-server/server/modules/projects/services/project-clone.service.js +86 -32
  118. package/dist-server/server/modules/projects/services/project-clone.service.js.map +1 -1
  119. package/dist-server/server/modules/projects/services/project-delete.service.js +12 -1
  120. package/dist-server/server/modules/projects/services/project-delete.service.js.map +1 -1
  121. package/dist-server/server/modules/projects/services/project-star.service.js +2 -2
  122. package/dist-server/server/modules/projects/services/project-star.service.js.map +1 -1
  123. package/dist-server/server/modules/projects/tests/project-clone.routes.test.js +191 -0
  124. package/dist-server/server/modules/projects/tests/project-clone.routes.test.js.map +1 -0
  125. package/dist-server/server/modules/projects/tests/project-clone.service.test.js +275 -0
  126. package/dist-server/server/modules/projects/tests/project-clone.service.test.js.map +1 -1
  127. package/dist-server/server/modules/providers/index.js +3 -0
  128. package/dist-server/server/modules/providers/index.js.map +1 -1
  129. package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js +62 -9
  130. package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js.map +1 -1
  131. package/dist-server/server/modules/providers/list/claude/claude-fork.provider.js +46 -0
  132. package/dist-server/server/modules/providers/list/claude/claude-fork.provider.js.map +1 -0
  133. package/dist-server/server/modules/providers/list/claude/claude-models.provider.js +31 -12
  134. package/dist-server/server/modules/providers/list/claude/claude-models.provider.js.map +1 -1
  135. package/dist-server/server/modules/providers/list/claude/claude-runtime.provider.js +472 -53
  136. package/dist-server/server/modules/providers/list/claude/claude-runtime.provider.js.map +1 -1
  137. package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js +38 -11
  138. package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js.map +1 -1
  139. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js +1007 -60
  140. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js.map +1 -1
  141. package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js +4 -1
  142. package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js.map +1 -1
  143. package/dist-server/server/modules/providers/list/claude/claude.provider.js +2 -0
  144. package/dist-server/server/modules/providers/list/claude/claude.provider.js.map +1 -1
  145. package/dist-server/server/modules/providers/list/codex/codex-app-server.client.js +204 -0
  146. package/dist-server/server/modules/providers/list/codex/codex-app-server.client.js.map +1 -0
  147. package/dist-server/server/modules/providers/list/codex/codex-fork.provider.js +32 -0
  148. package/dist-server/server/modules/providers/list/codex/codex-fork.provider.js.map +1 -0
  149. package/dist-server/server/modules/providers/list/codex/codex-models.provider.js +47 -0
  150. package/dist-server/server/modules/providers/list/codex/codex-models.provider.js.map +1 -1
  151. package/dist-server/server/modules/providers/list/codex/codex-runtime.provider.js +71 -53
  152. package/dist-server/server/modules/providers/list/codex/codex-runtime.provider.js.map +1 -1
  153. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js +8 -0
  154. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js.map +1 -1
  155. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +1621 -431
  156. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -1
  157. package/dist-server/server/modules/providers/list/codex/codex.provider.js +2 -0
  158. package/dist-server/server/modules/providers/list/codex/codex.provider.js.map +1 -1
  159. package/dist-server/server/modules/providers/list/cursor/cursor-auth.provider.js +9 -2
  160. package/dist-server/server/modules/providers/list/cursor/cursor-auth.provider.js.map +1 -1
  161. package/dist-server/server/modules/providers/list/cursor/cursor-runtime.provider.js +16 -8
  162. package/dist-server/server/modules/providers/list/cursor/cursor-runtime.provider.js.map +1 -1
  163. package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js.map +1 -1
  164. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js +274 -9
  165. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js.map +1 -1
  166. package/dist-server/server/modules/providers/list/opencode/opencode-runtime.provider.js +5 -2
  167. package/dist-server/server/modules/providers/list/opencode/opencode-runtime.provider.js.map +1 -1
  168. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js +19 -2
  169. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js.map +1 -1
  170. package/dist-server/server/modules/providers/provider.routes.js +134 -2
  171. package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
  172. package/dist-server/server/modules/providers/services/claude-cli-liveness.service.js +100 -0
  173. package/dist-server/server/modules/providers/services/claude-cli-liveness.service.js.map +1 -0
  174. package/dist-server/server/modules/providers/services/claude-settings-permissions.service.js +97 -0
  175. package/dist-server/server/modules/providers/services/claude-settings-permissions.service.js.map +1 -0
  176. package/dist-server/server/modules/providers/services/provider-capabilities.service.js +23 -1
  177. package/dist-server/server/modules/providers/services/provider-capabilities.service.js.map +1 -1
  178. package/dist-server/server/modules/providers/services/provider-models.service.js +59 -5
  179. package/dist-server/server/modules/providers/services/provider-models.service.js.map +1 -1
  180. package/dist-server/server/modules/providers/services/provider-runtime.service.js +8 -0
  181. package/dist-server/server/modules/providers/services/provider-runtime.service.js.map +1 -1
  182. package/dist-server/server/modules/providers/services/provider-token-usage.service.js +139 -35
  183. package/dist-server/server/modules/providers/services/provider-token-usage.service.js.map +1 -1
  184. package/dist-server/server/modules/providers/services/session-conversations-search.service.js +2 -4
  185. package/dist-server/server/modules/providers/services/session-conversations-search.service.js.map +1 -1
  186. package/dist-server/server/modules/providers/services/session-history-cache.service.js +91 -0
  187. package/dist-server/server/modules/providers/services/session-history-cache.service.js.map +1 -0
  188. package/dist-server/server/modules/providers/services/session-synchronizer.service.js +105 -32
  189. package/dist-server/server/modules/providers/services/session-synchronizer.service.js.map +1 -1
  190. package/dist-server/server/modules/providers/services/sessions-watcher.service.js +3 -60
  191. package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -1
  192. package/dist-server/server/modules/providers/services/sessions.service.js +252 -12
  193. package/dist-server/server/modules/providers/services/sessions.service.js.map +1 -1
  194. package/dist-server/server/modules/providers/tests/claude-auth.test.js +183 -0
  195. package/dist-server/server/modules/providers/tests/claude-auth.test.js.map +1 -1
  196. package/dist-server/server/modules/providers/tests/claude-background-work.test.js +172 -0
  197. package/dist-server/server/modules/providers/tests/claude-background-work.test.js.map +1 -0
  198. package/dist-server/server/modules/providers/tests/claude-compaction.test.js +84 -0
  199. package/dist-server/server/modules/providers/tests/claude-compaction.test.js.map +1 -0
  200. package/dist-server/server/modules/providers/tests/claude-message-model.test.js +123 -0
  201. package/dist-server/server/modules/providers/tests/claude-message-model.test.js.map +1 -0
  202. package/dist-server/server/modules/providers/tests/claude-models.test.js +39 -0
  203. package/dist-server/server/modules/providers/tests/claude-models.test.js.map +1 -0
  204. package/dist-server/server/modules/providers/tests/claude-runtime-hold.test.js +265 -0
  205. package/dist-server/server/modules/providers/tests/claude-runtime-hold.test.js.map +1 -0
  206. package/dist-server/server/modules/providers/tests/claude-sessions.test.js +1815 -33
  207. package/dist-server/server/modules/providers/tests/claude-sessions.test.js.map +1 -1
  208. package/dist-server/server/modules/providers/tests/claude-settings-permissions.test.js +228 -0
  209. package/dist-server/server/modules/providers/tests/claude-settings-permissions.test.js.map +1 -0
  210. package/dist-server/server/modules/providers/tests/claude-subagent-echo.test.js +16 -0
  211. package/dist-server/server/modules/providers/tests/claude-subagent-echo.test.js.map +1 -0
  212. package/dist-server/server/modules/providers/tests/claude-token-budget.test.js +85 -0
  213. package/dist-server/server/modules/providers/tests/claude-token-budget.test.js.map +1 -0
  214. package/dist-server/server/modules/providers/tests/codex-message-editing.test.js +338 -0
  215. package/dist-server/server/modules/providers/tests/codex-message-editing.test.js.map +1 -0
  216. package/dist-server/server/modules/providers/tests/codex-models.test.js +25 -0
  217. package/dist-server/server/modules/providers/tests/codex-models.test.js.map +1 -0
  218. package/dist-server/server/modules/providers/tests/codex-runtime.test.js +83 -0
  219. package/dist-server/server/modules/providers/tests/codex-runtime.test.js.map +1 -0
  220. package/dist-server/server/modules/providers/tests/codex-session-fork.test.js +54 -0
  221. package/dist-server/server/modules/providers/tests/codex-session-fork.test.js.map +1 -0
  222. package/dist-server/server/modules/providers/tests/codex-sessions.test.js +354 -15
  223. package/dist-server/server/modules/providers/tests/codex-sessions.test.js.map +1 -1
  224. package/dist-server/server/modules/providers/tests/cursor-auth.test.js +167 -0
  225. package/dist-server/server/modules/providers/tests/cursor-auth.test.js.map +1 -0
  226. package/dist-server/server/modules/providers/tests/opencode-models.test.js +196 -4
  227. package/dist-server/server/modules/providers/tests/opencode-models.test.js.map +1 -1
  228. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js +19 -1
  229. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js.map +1 -1
  230. package/dist-server/server/modules/providers/tests/provider-capabilities.service.test.js +30 -0
  231. package/dist-server/server/modules/providers/tests/provider-capabilities.service.test.js.map +1 -0
  232. package/dist-server/server/modules/providers/tests/provider-models.service.test.js +79 -1
  233. package/dist-server/server/modules/providers/tests/provider-models.service.test.js.map +1 -1
  234. package/dist-server/server/modules/providers/tests/provider-token-usage.service.test.js +164 -1
  235. package/dist-server/server/modules/providers/tests/provider-token-usage.service.test.js.map +1 -1
  236. package/dist-server/server/modules/providers/tests/provider.routes.test.js +190 -1
  237. package/dist-server/server/modules/providers/tests/provider.routes.test.js.map +1 -1
  238. package/dist-server/server/modules/providers/tests/session-fork.test.js +113 -0
  239. package/dist-server/server/modules/providers/tests/session-fork.test.js.map +1 -0
  240. package/dist-server/server/modules/providers/tests/session-history-cache.service.test.js +139 -0
  241. package/dist-server/server/modules/providers/tests/session-history-cache.service.test.js.map +1 -0
  242. package/dist-server/server/modules/providers/tests/session-orphan-prune.test.js +90 -0
  243. package/dist-server/server/modules/providers/tests/session-orphan-prune.test.js.map +1 -0
  244. package/dist-server/server/modules/providers/tests/session-synchronizer-coalescing.test.js +122 -0
  245. package/dist-server/server/modules/providers/tests/session-synchronizer-coalescing.test.js.map +1 -0
  246. package/dist-server/server/modules/providers/tests/sessions.service.test.js +219 -1
  247. package/dist-server/server/modules/providers/tests/sessions.service.test.js.map +1 -1
  248. package/dist-server/server/modules/providers/tests/skills.test.js +15 -3
  249. package/dist-server/server/modules/providers/tests/skills.test.js.map +1 -1
  250. package/dist-server/server/modules/scheduled-messages/index.js +5 -0
  251. package/dist-server/server/modules/scheduled-messages/index.js.map +1 -0
  252. package/dist-server/server/modules/scheduled-messages/scheduled-messages.routes.js +47 -0
  253. package/dist-server/server/modules/scheduled-messages/scheduled-messages.routes.js.map +1 -0
  254. package/dist-server/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.js +175 -0
  255. package/dist-server/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.js.map +1 -0
  256. package/dist-server/server/modules/scheduled-messages/services/scheduled-messages.service.js +95 -0
  257. package/dist-server/server/modules/scheduled-messages/services/scheduled-messages.service.js.map +1 -0
  258. package/dist-server/server/modules/scheduled-messages/tests/scheduled-messages.test.js +290 -0
  259. package/dist-server/server/modules/scheduled-messages/tests/scheduled-messages.test.js.map +1 -0
  260. package/dist-server/server/modules/taskmaster/taskmaster.routes.js +23 -23
  261. package/dist-server/server/modules/taskmaster/taskmaster.routes.js.map +1 -1
  262. package/dist-server/server/modules/taskmaster/tests/taskmaster.routes.test.js +48 -0
  263. package/dist-server/server/modules/taskmaster/tests/taskmaster.routes.test.js.map +1 -1
  264. package/dist-server/server/modules/user/tests/user.service.test.js +70 -0
  265. package/dist-server/server/modules/user/tests/user.service.test.js.map +1 -1
  266. package/dist-server/server/modules/user/user.module.js +10 -1
  267. package/dist-server/server/modules/user/user.module.js.map +1 -1
  268. package/dist-server/server/modules/user/user.routes.js +44 -0
  269. package/dist-server/server/modules/user/user.routes.js.map +1 -1
  270. package/dist-server/server/modules/user/user.service.js +75 -0
  271. package/dist-server/server/modules/user/user.service.js.map +1 -1
  272. package/dist-server/server/modules/voice/tests/voice.service.test.js +3 -2
  273. package/dist-server/server/modules/voice/tests/voice.service.test.js.map +1 -1
  274. package/dist-server/server/modules/websocket/index.js +7 -0
  275. package/dist-server/server/modules/websocket/index.js.map +1 -1
  276. package/dist-server/server/modules/websocket/services/chat-run-registry.service.js +37 -49
  277. package/dist-server/server/modules/websocket/services/chat-run-registry.service.js.map +1 -1
  278. package/dist-server/server/modules/websocket/services/chat-session-writer.service.js +35 -5
  279. package/dist-server/server/modules/websocket/services/chat-session-writer.service.js.map +1 -1
  280. package/dist-server/server/modules/websocket/services/chat-websocket.service.js +204 -10
  281. package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
  282. package/dist-server/server/modules/websocket/services/session-upsert-broadcast.service.js +94 -0
  283. package/dist-server/server/modules/websocket/services/session-upsert-broadcast.service.js.map +1 -0
  284. package/dist-server/server/modules/websocket/services/shell-websocket.service.js +236 -20
  285. package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -1
  286. package/dist-server/server/modules/websocket/tests/chat-background-subscribe.test.js +137 -0
  287. package/dist-server/server/modules/websocket/tests/chat-background-subscribe.test.js.map +1 -0
  288. package/dist-server/server/modules/websocket/tests/chat-edit-send.test.js +263 -0
  289. package/dist-server/server/modules/websocket/tests/chat-edit-send.test.js.map +1 -0
  290. package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js +29 -3
  291. package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js.map +1 -1
  292. package/dist-server/server/modules/websocket/tests/chat-stop-task.test.js +96 -0
  293. package/dist-server/server/modules/websocket/tests/chat-stop-task.test.js.map +1 -0
  294. package/dist-server/server/modules/websocket/tests/session-upsert-broadcast.test.js +106 -0
  295. package/dist-server/server/modules/websocket/tests/session-upsert-broadcast.test.js.map +1 -0
  296. package/dist-server/server/modules/websocket/tests/shell-websocket.service.test.js +500 -0
  297. package/dist-server/server/modules/websocket/tests/shell-websocket.service.test.js.map +1 -1
  298. package/dist-server/server/shared/claude-cli-path.js +22 -5
  299. package/dist-server/server/shared/claude-cli-path.js.map +1 -1
  300. package/dist-server/server/shared/index.js +3 -0
  301. package/dist-server/server/shared/index.js.map +1 -0
  302. package/dist-server/server/shared/message-unification.js +376 -0
  303. package/dist-server/server/shared/message-unification.js.map +1 -0
  304. package/dist-server/server/shared/tests/claude-cli-path.test.js +45 -1
  305. package/dist-server/server/shared/tests/claude-cli-path.test.js.map +1 -1
  306. package/dist-server/server/shared/tests/message-unification.test.js +174 -0
  307. package/dist-server/server/shared/tests/message-unification.test.js.map +1 -0
  308. package/dist-server/server/shared/tests/read-only-roots.test.js +124 -0
  309. package/dist-server/server/shared/tests/read-only-roots.test.js.map +1 -0
  310. package/dist-server/server/shared/tests/strip-ansi-sequences.test.js +31 -0
  311. package/dist-server/server/shared/tests/strip-ansi-sequences.test.js.map +1 -0
  312. package/dist-server/server/shared/utils.js +160 -0
  313. package/dist-server/server/shared/utils.js.map +1 -1
  314. package/electron/desktopWindow.js +6 -0
  315. package/electron/viewHost.js +25 -2
  316. package/package.json +20 -17
  317. package/server/index.ts +16 -5
  318. package/server/modules/agent/agent.module.ts +11 -1
  319. package/server/modules/agent/agent.routes.ts +200 -142
  320. package/server/modules/agent/tests/agent-run-registration.test.ts +348 -0
  321. package/server/modules/agent/tests/agent.routes.test.ts +51 -2
  322. package/server/modules/browser-use/browser-use.routes.ts +96 -33
  323. package/server/modules/database/connection.ts +2 -1
  324. package/server/modules/database/index.ts +10 -0
  325. package/server/modules/database/migrations.ts +51 -0
  326. package/server/modules/database/repositories/provider-models.ts +79 -6
  327. package/server/modules/database/repositories/scheduled-messages.db.ts +127 -0
  328. package/server/modules/database/repositories/session-drafts.db.ts +171 -0
  329. package/server/modules/database/repositories/sessions.db.ts +204 -3
  330. package/server/modules/database/repositories/user-preferences.db.ts +80 -0
  331. package/server/modules/database/schema.ts +103 -0
  332. package/server/modules/database/tests/provider-models.db.integration.test.ts +113 -0
  333. package/server/modules/database/tests/sessions.db.integration.test.ts +52 -0
  334. package/server/modules/database/tests/user-preferences.db.integration.test.ts +135 -0
  335. package/server/modules/file-tree/file-tree.module.ts +2 -1
  336. package/server/modules/file-tree/file-tree.service.ts +49 -7
  337. package/server/modules/file-tree/tests/file-tree.service.test.ts +163 -1
  338. package/server/modules/git/git-branch-diff.service.ts +204 -0
  339. package/server/modules/git/git-branch.service.ts +2 -12
  340. package/server/modules/git/git.routes.ts +100 -6
  341. package/server/modules/git/tests/git-branch-diff.service.test.ts +210 -0
  342. package/server/modules/projects/project-clone.routes.ts +142 -0
  343. package/server/modules/projects/projects.routes.ts +16 -81
  344. package/server/modules/projects/services/project-clone-request.service.ts +56 -0
  345. package/server/modules/projects/services/project-clone.service.ts +102 -34
  346. package/server/modules/projects/services/project-delete.service.ts +13 -1
  347. package/server/modules/projects/services/project-star.service.ts +2 -2
  348. package/server/modules/projects/tests/project-clone.routes.test.ts +246 -0
  349. package/server/modules/projects/tests/project-clone.service.test.ts +376 -0
  350. package/server/modules/providers/index.ts +4 -0
  351. package/server/modules/providers/list/claude/claude-auth.provider.ts +81 -11
  352. package/server/modules/providers/list/claude/claude-fork.provider.ts +57 -0
  353. package/server/modules/providers/list/claude/claude-models.provider.ts +37 -17
  354. package/server/modules/providers/list/claude/claude-runtime.provider.js +510 -54
  355. package/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts +40 -14
  356. package/server/modules/providers/list/claude/claude-sessions.provider.ts +1195 -63
  357. package/server/modules/providers/list/claude/claude-skills.provider.ts +4 -1
  358. package/server/modules/providers/list/claude/claude.provider.ts +3 -0
  359. package/server/modules/providers/list/codex/codex-app-server.client.ts +243 -0
  360. package/server/modules/providers/list/codex/codex-fork.provider.ts +40 -0
  361. package/server/modules/providers/list/codex/codex-models.provider.ts +47 -0
  362. package/server/modules/providers/list/codex/{codex-runtime.provider.js → codex-runtime.provider.ts} +105 -70
  363. package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +9 -0
  364. package/server/modules/providers/list/codex/codex-sessions.provider.ts +1847 -455
  365. package/server/modules/providers/list/codex/codex.provider.ts +3 -0
  366. package/server/modules/providers/list/cursor/cursor-auth.provider.ts +10 -2
  367. package/server/modules/providers/list/cursor/cursor-runtime.provider.js +17 -8
  368. package/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.ts +0 -1
  369. package/server/modules/providers/list/opencode/opencode-models.provider.ts +297 -9
  370. package/server/modules/providers/list/opencode/opencode-runtime.provider.js +5 -2
  371. package/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.ts +30 -2
  372. package/server/modules/providers/provider.routes.ts +162 -2
  373. package/server/modules/providers/services/claude-cli-liveness.service.ts +121 -0
  374. package/server/modules/providers/services/claude-settings-permissions.service.ts +127 -0
  375. package/server/modules/providers/services/provider-capabilities.service.ts +40 -1
  376. package/server/modules/providers/services/provider-models.service.ts +71 -4
  377. package/server/modules/providers/services/provider-runtime.service.ts +11 -0
  378. package/server/modules/providers/services/provider-token-usage.service.ts +166 -39
  379. package/server/modules/providers/services/session-conversations-search.service.ts +2 -5
  380. package/server/modules/providers/services/session-history-cache.service.ts +150 -0
  381. package/server/modules/providers/services/session-synchronizer.service.ts +125 -38
  382. package/server/modules/providers/services/sessions-watcher.service.ts +3 -64
  383. package/server/modules/providers/services/sessions.service.ts +309 -17
  384. package/server/modules/providers/tests/claude-auth.test.ts +208 -0
  385. package/server/modules/providers/tests/claude-background-work.test.ts +213 -0
  386. package/server/modules/providers/tests/claude-compaction.test.ts +101 -0
  387. package/server/modules/providers/tests/claude-message-model.test.ts +160 -0
  388. package/server/modules/providers/tests/claude-models.test.ts +87 -0
  389. package/server/modules/providers/tests/claude-runtime-hold.test.ts +309 -0
  390. package/server/modules/providers/tests/claude-sessions.test.ts +2167 -37
  391. package/server/modules/providers/tests/claude-settings-permissions.test.ts +271 -0
  392. package/server/modules/providers/tests/claude-subagent-echo.test.ts +25 -0
  393. package/server/modules/providers/tests/claude-token-budget.test.ts +104 -0
  394. package/server/modules/providers/tests/codex-message-editing.test.ts +419 -0
  395. package/server/modules/providers/tests/codex-models.test.ts +37 -0
  396. package/server/modules/providers/tests/codex-runtime.test.ts +94 -0
  397. package/server/modules/providers/tests/codex-session-fork.test.ts +59 -0
  398. package/server/modules/providers/tests/codex-sessions.test.ts +396 -15
  399. package/server/modules/providers/tests/cursor-auth.test.ts +187 -0
  400. package/server/modules/providers/tests/opencode-models.test.ts +266 -8
  401. package/server/modules/providers/tests/opencode-sessions.test.ts +40 -1
  402. package/server/modules/providers/tests/provider-capabilities.service.test.ts +37 -0
  403. package/server/modules/providers/tests/provider-models.service.test.ts +99 -1
  404. package/server/modules/providers/tests/provider-token-usage.service.test.ts +188 -1
  405. package/server/modules/providers/tests/provider.routes.test.ts +230 -1
  406. package/server/modules/providers/tests/session-fork.test.ts +173 -0
  407. package/server/modules/providers/tests/session-history-cache.service.test.ts +165 -0
  408. package/server/modules/providers/tests/session-orphan-prune.test.ts +107 -0
  409. package/server/modules/providers/tests/session-synchronizer-coalescing.test.ts +136 -0
  410. package/server/modules/providers/tests/sessions.service.test.ts +303 -1
  411. package/server/modules/providers/tests/skills.test.ts +21 -5
  412. package/server/modules/scheduled-messages/index.ts +8 -0
  413. package/server/modules/scheduled-messages/scheduled-messages.routes.ts +68 -0
  414. package/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.ts +216 -0
  415. package/server/modules/scheduled-messages/services/scheduled-messages.service.ts +121 -0
  416. package/server/modules/scheduled-messages/tests/scheduled-messages.test.ts +354 -0
  417. package/server/modules/taskmaster/taskmaster.routes.ts +23 -33
  418. package/server/modules/taskmaster/tests/taskmaster.routes.test.ts +57 -0
  419. package/server/modules/user/tests/user.service.test.ts +90 -0
  420. package/server/modules/user/user.module.ts +10 -1
  421. package/server/modules/user/user.routes.ts +44 -0
  422. package/server/modules/user/user.service.ts +105 -0
  423. package/server/modules/voice/tests/voice.service.test.ts +3 -2
  424. package/server/modules/websocket/README.md +4 -2
  425. package/server/modules/websocket/index.ts +8 -0
  426. package/server/modules/websocket/services/chat-run-registry.service.ts +45 -54
  427. package/server/modules/websocket/services/chat-session-writer.service.ts +36 -6
  428. package/server/modules/websocket/services/chat-websocket.service.ts +302 -16
  429. package/server/modules/websocket/services/session-upsert-broadcast.service.ts +108 -0
  430. package/server/modules/websocket/services/shell-websocket.service.ts +276 -22
  431. package/server/modules/websocket/tests/chat-background-subscribe.test.ts +167 -0
  432. package/server/modules/websocket/tests/chat-edit-send.test.ts +309 -0
  433. package/server/modules/websocket/tests/chat-run-registry.test.ts +34 -3
  434. package/server/modules/websocket/tests/chat-stop-task.test.ts +127 -0
  435. package/server/modules/websocket/tests/session-upsert-broadcast.test.ts +138 -0
  436. package/server/modules/websocket/tests/shell-websocket.service.test.ts +629 -0
  437. package/server/shared/claude-cli-path.ts +24 -6
  438. package/server/shared/index.ts +3 -0
  439. package/server/shared/interfaces.ts +91 -0
  440. package/server/shared/message-unification.ts +449 -0
  441. package/server/shared/tests/claude-cli-path.test.ts +58 -1
  442. package/server/shared/tests/message-unification.test.ts +218 -0
  443. package/server/shared/tests/read-only-roots.test.ts +139 -0
  444. package/server/shared/tests/strip-ansi-sequences.test.ts +42 -0
  445. package/server/shared/types.ts +382 -7
  446. package/server/shared/utils.ts +176 -0
  447. package/dist/assets/channel-_falYbU1.js +0 -1
  448. package/dist/assets/classDiagram-JCYQIIEL-Dn-3CfpR.js +0 -1
  449. package/dist/assets/classDiagram-v2-OCEON4UE-Dn-3CfpR.js +0 -1
  450. package/dist/assets/index-CzPLBvXT.css +0 -32
  451. package/dist/assets/index-DFkBBuSA.js +0 -1549
  452. package/dist/assets/stateDiagram-v2-UEYNNEHI-BvBxUUrZ.js +0 -1
  453. package/dist/assets/swimlanesDiagram-ULZ7WXOC-CsE-x4YK.js +0 -8
@@ -0,0 +1,139 @@
1
+ import assert from 'node:assert/strict';
2
+ import { appendFile, mkdtemp, rm, writeFile } from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import test from 'node:test';
6
+ import { createSessionHistoryCache } from '../../../modules/providers/services/session-history-cache.service.js';
7
+ function historyResult(marker) {
8
+ const message = {
9
+ id: marker,
10
+ sessionId: 'session',
11
+ timestamp: '2026-01-01T00:00:00.000Z',
12
+ provider: 'claude',
13
+ kind: 'text',
14
+ role: 'user',
15
+ content: marker,
16
+ };
17
+ return { messages: [message], total: 1, hasMore: false, offset: 0, limit: null };
18
+ }
19
+ async function withTranscriptFile(runTest) {
20
+ const tempDirectory = await mkdtemp(path.join(os.tmpdir(), 'session-history-cache-'));
21
+ const transcriptPath = path.join(tempDirectory, 'session.jsonl');
22
+ await writeFile(transcriptPath, '{"type":"user"}\n', 'utf8');
23
+ try {
24
+ await runTest(transcriptPath);
25
+ }
26
+ finally {
27
+ await rm(tempDirectory, { recursive: true, force: true });
28
+ }
29
+ }
30
+ test('an unchanged transcript file is loaded once and then served from cache', async () => {
31
+ await withTranscriptFile(async (transcriptPath) => {
32
+ const cache = createSessionHistoryCache();
33
+ let loads = 0;
34
+ const loadFull = async () => {
35
+ loads += 1;
36
+ return historyResult(`load-${loads}`);
37
+ };
38
+ const first = await cache.getFullHistory({ sessionId: 's1', transcriptPath, loadFull });
39
+ const second = await cache.getFullHistory({ sessionId: 's1', transcriptPath, loadFull });
40
+ assert.equal(loads, 1);
41
+ assert.equal(second, first);
42
+ });
43
+ });
44
+ test('growing the transcript file invalidates the cached load', async () => {
45
+ await withTranscriptFile(async (transcriptPath) => {
46
+ const cache = createSessionHistoryCache();
47
+ let loads = 0;
48
+ const loadFull = async () => {
49
+ loads += 1;
50
+ return historyResult(`load-${loads}`);
51
+ };
52
+ await cache.getFullHistory({ sessionId: 's1', transcriptPath, loadFull });
53
+ await appendFile(transcriptPath, '{"type":"assistant"}\n', 'utf8');
54
+ const afterAppend = await cache.getFullHistory({ sessionId: 's1', transcriptPath, loadFull });
55
+ assert.equal(loads, 2);
56
+ assert.equal(afterAppend?.messages[0]?.id, 'load-2');
57
+ });
58
+ });
59
+ test('a missing transcript path or file bypasses the cache', async () => {
60
+ const cache = createSessionHistoryCache();
61
+ let loads = 0;
62
+ const loadFull = async () => {
63
+ loads += 1;
64
+ return historyResult('unused');
65
+ };
66
+ assert.equal(await cache.getFullHistory({ sessionId: 's1', transcriptPath: null, loadFull }), null);
67
+ assert.equal(await cache.getFullHistory({
68
+ sessionId: 's1',
69
+ transcriptPath: path.join(os.tmpdir(), 'session-history-cache-does-not-exist.jsonl'),
70
+ loadFull,
71
+ }), null);
72
+ assert.equal(loads, 0);
73
+ });
74
+ test('concurrent misses share a single load', async () => {
75
+ await withTranscriptFile(async (transcriptPath) => {
76
+ const cache = createSessionHistoryCache();
77
+ let loads = 0;
78
+ let release;
79
+ const gate = new Promise((resolve) => { release = resolve; });
80
+ const loadFull = async () => {
81
+ loads += 1;
82
+ await gate;
83
+ return historyResult(`load-${loads}`);
84
+ };
85
+ const firstRequest = cache.getFullHistory({ sessionId: 's1', transcriptPath, loadFull });
86
+ const secondRequest = cache.getFullHistory({ sessionId: 's1', transcriptPath, loadFull });
87
+ release();
88
+ const [first, second] = await Promise.all([firstRequest, secondRequest]);
89
+ assert.equal(loads, 1);
90
+ assert.equal(second, first);
91
+ });
92
+ });
93
+ test('the oldest entries are evicted over budget, but the newest survives alone', async () => {
94
+ const tempDirectory = await mkdtemp(path.join(os.tmpdir(), 'session-history-cache-evict-'));
95
+ try {
96
+ const firstPath = path.join(tempDirectory, 'first.jsonl');
97
+ const secondPath = path.join(tempDirectory, 'second.jsonl');
98
+ // Each file is 100 bytes, so a 150-byte budget holds exactly one entry —
99
+ // and the newest entry stays cached even though it exceeds nothing alone.
100
+ await writeFile(firstPath, 'x'.repeat(100), 'utf8');
101
+ await writeFile(secondPath, 'y'.repeat(100), 'utf8');
102
+ const cache = createSessionHistoryCache(150);
103
+ const loadsBySession = new Map();
104
+ const loaderFor = (sessionId) => async () => {
105
+ loadsBySession.set(sessionId, (loadsBySession.get(sessionId) ?? 0) + 1);
106
+ return historyResult(sessionId);
107
+ };
108
+ await cache.getFullHistory({ sessionId: 's1', transcriptPath: firstPath, loadFull: loaderFor('s1') });
109
+ await cache.getFullHistory({ sessionId: 's2', transcriptPath: secondPath, loadFull: loaderFor('s2') });
110
+ // s2 is still cached; s1 was evicted to fit the budget.
111
+ await cache.getFullHistory({ sessionId: 's2', transcriptPath: secondPath, loadFull: loaderFor('s2') });
112
+ await cache.getFullHistory({ sessionId: 's1', transcriptPath: firstPath, loadFull: loaderFor('s1') });
113
+ assert.equal(loadsBySession.get('s2'), 1);
114
+ assert.equal(loadsBySession.get('s1'), 2);
115
+ }
116
+ finally {
117
+ await rm(tempDirectory, { recursive: true, force: true });
118
+ }
119
+ });
120
+ test('invalidating a session re-parses its unchanged transcript on the next read', async () => {
121
+ await withTranscriptFile(async (transcriptPath) => {
122
+ const cache = createSessionHistoryCache();
123
+ let loads = 0;
124
+ const loadFull = async () => {
125
+ loads += 1;
126
+ return historyResult(`load-${loads}`);
127
+ };
128
+ // Claude's history reader decides a background agent's status from whether
129
+ // the session's CLI process is up — nothing the file's stat can reflect —
130
+ // so the runtime drops the entry when that process starts or ends.
131
+ const first = await cache.getFullHistory({ sessionId: 's1', transcriptPath, loadFull });
132
+ cache.invalidate('s1');
133
+ const second = await cache.getFullHistory({ sessionId: 's1', transcriptPath, loadFull });
134
+ assert.equal(loads, 2);
135
+ assert.notEqual(second, first);
136
+ assert.equal(second?.messages[0]?.content, 'load-2');
137
+ });
138
+ });
139
+ //# sourceMappingURL=session-history-cache.service.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-history-cache.service.test.js","sourceRoot":"","sources":["../../../../../server/modules/providers/tests/session-history-cache.service.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,90 @@
1
+ import assert from 'node:assert/strict';
2
+ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import test from 'node:test';
6
+ import { closeConnection, initializeDatabase, sessionsDb } from '../../../modules/database/index.js';
7
+ // Each provider synchronizer resolves `os.homedir()` when the registry module is
8
+ // first imported, so HOME has to point at an empty fixture home *before* that
9
+ // import runs. Otherwise the sync pass walks the developer's real ~/.claude.
10
+ const fixtureHome = await mkdtemp(path.join(os.tmpdir(), 'session-prune-home-'));
11
+ const previousHome = process.env.HOME;
12
+ const previousUserProfile = process.env.USERPROFILE;
13
+ process.env.HOME = fixtureHome;
14
+ process.env.USERPROFILE = fixtureHome;
15
+ const { sessionSynchronizerService } = await import('../../../modules/providers/services/session-synchronizer.service.js');
16
+ process.on('exit', () => {
17
+ if (previousHome === undefined) {
18
+ delete process.env.HOME;
19
+ }
20
+ else {
21
+ process.env.HOME = previousHome;
22
+ }
23
+ if (previousUserProfile === undefined) {
24
+ delete process.env.USERPROFILE;
25
+ }
26
+ else {
27
+ process.env.USERPROFILE = previousUserProfile;
28
+ }
29
+ });
30
+ async function withIsolatedDatabase(runTest) {
31
+ const previousDatabasePath = process.env.DATABASE_PATH;
32
+ const tempDirectory = await mkdtemp(path.join(os.tmpdir(), 'session-prune-db-'));
33
+ closeConnection();
34
+ process.env.DATABASE_PATH = path.join(tempDirectory, 'auth.db');
35
+ await initializeDatabase();
36
+ try {
37
+ await runTest(tempDirectory);
38
+ }
39
+ finally {
40
+ closeConnection();
41
+ if (previousDatabasePath === undefined) {
42
+ delete process.env.DATABASE_PATH;
43
+ }
44
+ else {
45
+ process.env.DATABASE_PATH = previousDatabasePath;
46
+ }
47
+ await rm(tempDirectory, { recursive: true, force: true });
48
+ }
49
+ }
50
+ const PROJECT_PATH = '/tmp/session-prune-project';
51
+ test('synchronizeSessions drops indexed sessions whose transcript file was deleted', async () => {
52
+ await withIsolatedDatabase(async (workspace) => {
53
+ const transcriptDirectory = path.join(workspace, 'transcripts');
54
+ await mkdir(transcriptDirectory, { recursive: true });
55
+ const livePath = path.join(transcriptDirectory, 'live.jsonl');
56
+ await writeFile(livePath, '{}\n');
57
+ const deletedPath = path.join(transcriptDirectory, 'deleted.jsonl');
58
+ sessionsDb.createSession('live-session', 'claude', PROJECT_PATH, 'Live', undefined, undefined, livePath);
59
+ sessionsDb.createSession('orphan-session', 'claude', PROJECT_PATH, 'Untitled Claude Session', undefined, undefined, deletedPath);
60
+ const result = await sessionSynchronizerService.synchronizeSessions();
61
+ assert.deepEqual(result.failures, []);
62
+ assert.equal(result.prunedOrphans, 1);
63
+ assert.equal(sessionsDb.getSessionById('orphan-session'), null);
64
+ assert.ok(sessionsDb.getSessionById('live-session'), 'a session whose transcript still exists must survive');
65
+ });
66
+ });
67
+ test('synchronizeSessions keeps sessions whose whole transcript directory is missing', async () => {
68
+ await withIsolatedDatabase(async (workspace) => {
69
+ // Stands in for an unmounted or not-yet-created home: every transcript
70
+ // "looks" deleted, so pruning here would wipe the entire index.
71
+ const unmountedPath = path.join(workspace, 'not-mounted', 'session.jsonl');
72
+ sessionsDb.createSession('unmounted-session', 'claude', PROJECT_PATH, 'Unmounted', undefined, undefined, unmountedPath);
73
+ const result = await sessionSynchronizerService.synchronizeSessions();
74
+ assert.equal(result.prunedOrphans, 0);
75
+ assert.ok(sessionsDb.getSessionById('unmounted-session'));
76
+ });
77
+ });
78
+ test('synchronizeSessions keeps sessions that have no transcript path yet', async () => {
79
+ await withIsolatedDatabase(async () => {
80
+ // App-created rows (jsonl_path NULL until the first provider write) and
81
+ // OpenCode rows (one shared sqlite file, so jsonl_path stays NULL).
82
+ sessionsDb.createAppSession('pending-app-session', 'claude', PROJECT_PATH, 'Pending');
83
+ sessionsDb.createSession('opencode-session', 'opencode', PROJECT_PATH, 'OpenCode', undefined, undefined, null);
84
+ const result = await sessionSynchronizerService.synchronizeSessions();
85
+ assert.equal(result.prunedOrphans, 0);
86
+ assert.ok(sessionsDb.getSessionById('pending-app-session'));
87
+ assert.ok(sessionsDb.getSessionById('opencode-session'));
88
+ });
89
+ });
90
+ //# sourceMappingURL=session-orphan-prune.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-orphan-prune.test.js","sourceRoot":"","sources":["../../../../../server/modules/providers/tests/session-orphan-prune.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,122 @@
1
+ import assert from 'node:assert/strict';
2
+ import { mkdtemp, rm } from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import test from 'node:test';
6
+ import { closeConnection, initializeDatabase } from '../../../modules/database/index.js';
7
+ // Each provider synchronizer resolves `os.homedir()` when the registry module is
8
+ // first imported, so HOME has to point at an empty fixture home *before* that
9
+ // import runs. The registry is stubbed per test, but the import still happens.
10
+ const fixtureHome = await mkdtemp(path.join(os.tmpdir(), 'session-sync-home-'));
11
+ const previousHome = process.env.HOME;
12
+ const previousUserProfile = process.env.USERPROFILE;
13
+ process.env.HOME = fixtureHome;
14
+ process.env.USERPROFILE = fixtureHome;
15
+ const { providerRegistry } = await import('../../../modules/providers/provider.registry.js');
16
+ const { sessionSynchronizerService } = await import('../../../modules/providers/services/session-synchronizer.service.js');
17
+ process.on('exit', () => {
18
+ if (previousHome === undefined) {
19
+ delete process.env.HOME;
20
+ }
21
+ else {
22
+ process.env.HOME = previousHome;
23
+ }
24
+ if (previousUserProfile === undefined) {
25
+ delete process.env.USERPROFILE;
26
+ }
27
+ else {
28
+ process.env.USERPROFILE = previousUserProfile;
29
+ }
30
+ });
31
+ async function withIsolatedDatabase(runTest) {
32
+ const previousDatabasePath = process.env.DATABASE_PATH;
33
+ const tempDirectory = await mkdtemp(path.join(os.tmpdir(), 'session-sync-db-'));
34
+ closeConnection();
35
+ process.env.DATABASE_PATH = path.join(tempDirectory, 'auth.db');
36
+ await initializeDatabase();
37
+ try {
38
+ await runTest();
39
+ }
40
+ finally {
41
+ closeConnection();
42
+ if (previousDatabasePath === undefined) {
43
+ delete process.env.DATABASE_PATH;
44
+ }
45
+ else {
46
+ process.env.DATABASE_PATH = previousDatabasePath;
47
+ }
48
+ await rm(tempDirectory, { recursive: true, force: true });
49
+ }
50
+ }
51
+ /**
52
+ * Installs a single fake provider whose scan only settles when the returned
53
+ * `release` is called, so a second caller can be observed arriving mid-scan.
54
+ */
55
+ function stubSlowProvider() {
56
+ const originalListProviders = providerRegistry.listProviders;
57
+ let synchronizeCalls = 0;
58
+ let releaseScan = () => { };
59
+ const scanStarted = new Promise((resolveStarted) => {
60
+ const fakeProvider = {
61
+ id: 'claude',
62
+ sessionSynchronizer: {
63
+ async synchronize() {
64
+ synchronizeCalls += 1;
65
+ resolveStarted();
66
+ await new Promise((resolveScan) => {
67
+ releaseScan = resolveScan;
68
+ });
69
+ return 7;
70
+ },
71
+ },
72
+ };
73
+ providerRegistry.listProviders = () => [fakeProvider];
74
+ });
75
+ return {
76
+ scanStarted,
77
+ release: () => releaseScan(),
78
+ getSynchronizeCalls: () => synchronizeCalls,
79
+ restore: () => {
80
+ providerRegistry.listProviders = originalListProviders;
81
+ },
82
+ };
83
+ }
84
+ test('concurrent synchronizeSessions callers share one provider scan', async () => {
85
+ await withIsolatedDatabase(async () => {
86
+ const providerStub = stubSlowProvider();
87
+ try {
88
+ const first = sessionSynchronizerService.synchronizeSessions();
89
+ await providerStub.scanStarted;
90
+ // Opening the UI fires /api/projects and /api/projects/archived at once;
91
+ // the second must join the running scan instead of starting its own.
92
+ const second = sessionSynchronizerService.synchronizeSessions();
93
+ providerStub.release();
94
+ const [firstResult, secondResult] = await Promise.all([first, second]);
95
+ assert.equal(providerStub.getSynchronizeCalls(), 1);
96
+ assert.equal(firstResult.processedByProvider.claude, 7);
97
+ assert.deepEqual(secondResult, firstResult);
98
+ }
99
+ finally {
100
+ providerStub.restore();
101
+ }
102
+ });
103
+ });
104
+ test('a synchronizeSessions call after the shared scan settles starts a new scan', async () => {
105
+ await withIsolatedDatabase(async () => {
106
+ const providerStub = stubSlowProvider();
107
+ try {
108
+ const first = sessionSynchronizerService.synchronizeSessions();
109
+ await providerStub.scanStarted;
110
+ providerStub.release();
111
+ await first;
112
+ const second = sessionSynchronizerService.synchronizeSessions();
113
+ providerStub.release();
114
+ await second;
115
+ assert.equal(providerStub.getSynchronizeCalls(), 2);
116
+ }
117
+ finally {
118
+ providerStub.restore();
119
+ }
120
+ });
121
+ });
122
+ //# sourceMappingURL=session-synchronizer-coalescing.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-synchronizer-coalescing.test.js","sourceRoot":"","sources":["../../../../../server/modules/providers/tests/session-synchronizer-coalescing.test.ts"],"names":[],"mappings":"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"}
@@ -1,10 +1,12 @@
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
  import { closeConnection, initializeDatabase, projectsDb, sessionsDb } from '../../../modules/database/index.js';
7
+ import { providerRegistry } from '../../../modules/providers/provider.registry.js';
7
8
  import { sessionsService } from '../../../modules/providers/services/sessions.service.js';
9
+ import { chatRunRegistry } from '../../../modules/websocket/index.js';
8
10
  async function withIsolatedDatabase(runTest) {
9
11
  const previousDatabasePath = process.env.DATABASE_PATH;
10
12
  const tempDirectory = await mkdtemp(path.join(os.tmpdir(), 'sessions-service-db-'));
@@ -84,4 +86,220 @@ test('recent sessions map project metadata and preserve database pagination', {
84
86
  });
85
87
  });
86
88
  });
89
+ /** One Claude transcript row of user or assistant text, linked by uuid chain. */
90
+ function claudeTextRow(sessionId, role, text, ordinal) {
91
+ return {
92
+ type: role,
93
+ uuid: `row-${ordinal}`,
94
+ parentUuid: ordinal === 0 ? null : `row-${ordinal - 1}`,
95
+ timestamp: new Date(Date.UTC(2026, 0, 1, 0, 0, ordinal)).toISOString(),
96
+ sessionId,
97
+ message: { role, content: [{ type: 'text', text }] },
98
+ };
99
+ }
100
+ test('history pages are sliced from the cached full transcript and see appended rows', { concurrency: false }, async () => {
101
+ const transcriptDirectory = await mkdtemp(path.join(os.tmpdir(), 'sessions-service-history-'));
102
+ const sessionId = 'claude-history-cache-session';
103
+ const transcriptPath = path.join(transcriptDirectory, `${sessionId}.jsonl`);
104
+ try {
105
+ await withIsolatedDatabase(async () => {
106
+ const rows = [
107
+ claudeTextRow(sessionId, 'user', 'one', 0),
108
+ claudeTextRow(sessionId, 'assistant', 'reply one', 1),
109
+ claudeTextRow(sessionId, 'user', 'two', 2),
110
+ claudeTextRow(sessionId, 'assistant', 'reply two', 3),
111
+ ];
112
+ await writeFile(transcriptPath, `${rows.map((row) => JSON.stringify(row)).join('\n')}\n`, 'utf8');
113
+ sessionsDb.createSession(sessionId, 'claude', '/tmp/history-cache-project', 'History cache conversation', '2026-01-01T00:00:00.000Z', '2026-01-01T00:00:10.000Z', transcriptPath);
114
+ const page = await sessionsService.fetchHistory(sessionId, { limit: 2, offset: 0 });
115
+ assert.equal(page.total, 4);
116
+ assert.equal(page.hasMore, true);
117
+ assert.deepEqual(page.messages.map((message) => message.content), ['two', 'reply two']);
118
+ // A row appended after the page was cached must appear on the next read.
119
+ await appendFile(transcriptPath, `${JSON.stringify(claudeTextRow(sessionId, 'user', 'three', 4))}\n`, 'utf8');
120
+ const refreshed = await sessionsService.fetchHistory(sessionId, { limit: 2, offset: 0 });
121
+ assert.equal(refreshed.total, 5);
122
+ assert.deepEqual(refreshed.messages.map((message) => message.content), ['reply two', 'three']);
123
+ // An older page keeps the tail-offset contract while served from cache.
124
+ const older = await sessionsService.fetchHistory(sessionId, { limit: 2, offset: 2 });
125
+ assert.deepEqual(older.messages.map((message) => message.content), ['reply one', 'two']);
126
+ assert.equal(older.hasMore, true);
127
+ });
128
+ }
129
+ finally {
130
+ await rm(transcriptDirectory, { recursive: true, force: true });
131
+ }
132
+ });
133
+ /**
134
+ * Stands in for the provider registry with one provider per id, each with the
135
+ * runtime given — the seam through which a runtime's background work reaches
136
+ * the running-sessions list without an SDK run behind it.
137
+ */
138
+ async function withProviders(runtimes, runTest) {
139
+ const realListProviders = providerRegistry.listProviders;
140
+ providerRegistry.listProviders = () => Object.entries(runtimes).map(([id, runtime]) => ({ id, runtime }));
141
+ try {
142
+ await runTest();
143
+ }
144
+ finally {
145
+ providerRegistry.listProviders = realListProviders;
146
+ chatRunRegistry.clearAll();
147
+ }
148
+ }
149
+ /**
150
+ * Runs `runTest` against a Claude CLI process registry containing exactly
151
+ * `records`, so the running-sessions tests never read the developer's real
152
+ * `~/.claude/sessions`.
153
+ */
154
+ async function withClaudeCliRegistry(records, runTest) {
155
+ const homeDirectory = await mkdtemp(path.join(os.tmpdir(), 'claude-cli-registry-'));
156
+ await mkdir(path.join(homeDirectory, '.claude', 'sessions'), { recursive: true });
157
+ for (const record of records) {
158
+ await writeFile(path.join(homeDirectory, '.claude', 'sessions', `${record.pid}.json`), JSON.stringify(record), 'utf8');
159
+ }
160
+ const realHomedir = os.homedir;
161
+ os.homedir = () => homeDirectory;
162
+ try {
163
+ await runTest();
164
+ }
165
+ finally {
166
+ os.homedir = realHomedir;
167
+ await rm(homeDirectory, { recursive: true, force: true });
168
+ }
169
+ }
170
+ const task = (taskId, startedAt) => ({
171
+ taskId,
172
+ toolUseId: `toolu_${taskId}`,
173
+ taskType: 'local_agent',
174
+ description: `Task ${taskId}`,
175
+ startedAt,
176
+ });
177
+ test('running sessions list a session whose background work outlived its turn', { concurrency: false }, async () => {
178
+ await withIsolatedDatabase(async () => {
179
+ sessionsDb.createAppSession('held-session', 'claude', '/tmp/running-project');
180
+ // The turn ran and completed: the registry still holds the finished run.
181
+ const run = chatRunRegistry.startRun({
182
+ appSessionId: 'held-session', provider: 'claude', providerSessionId: null, connection: null, userId: null,
183
+ });
184
+ assert.ok(run);
185
+ run.writer.send({ kind: 'text', content: 'launched', sessionId: 'held-session', provider: 'claude' });
186
+ run.writer.sendComplete({ exitCode: 0 });
187
+ assert.equal(run.status, 'completed');
188
+ const tasks = [task('late', 2_000), task('early', 1_000)];
189
+ await withClaudeCliRegistry([], async () => await withProviders({ claude: { run: async () => undefined, abort: () => false, listBackgroundWork: () => [{ sessionId: 'held-session', tasks }] } }, async () => {
190
+ assert.deepEqual(await sessionsService.listRunningSessions(), [{
191
+ sessionId: 'held-session',
192
+ provider: 'claude',
193
+ startedAt: 1_000,
194
+ lastSeq: run.lastSeq,
195
+ background: true,
196
+ canInterrupt: false,
197
+ tasks,
198
+ }]);
199
+ assert.ok(run.lastSeq > 0, 'the finished run still reports its sequence for replay');
200
+ }));
201
+ });
202
+ });
203
+ test('running sessions carry their tasks on a chat run that is still going', { concurrency: false }, async () => {
204
+ await withIsolatedDatabase(async () => {
205
+ sessionsDb.createAppSession('busy-session', 'claude', '/tmp/running-project');
206
+ sessionsDb.createAppSession('idle-session', 'codex', '/tmp/running-project');
207
+ const run = chatRunRegistry.startRun({
208
+ appSessionId: 'busy-session', provider: 'claude', providerSessionId: null, connection: null, userId: null,
209
+ });
210
+ assert.ok(run);
211
+ const tasks = [task('agent', 5_000)];
212
+ await withClaudeCliRegistry([], async () => await withProviders({
213
+ claude: { run: async () => undefined, abort: () => false, listBackgroundWork: () => [{ sessionId: 'busy-session', tasks }] },
214
+ // A runtime without background work contributes chat runs alone.
215
+ codex: { run: async () => undefined, abort: () => false },
216
+ }, async () => {
217
+ const sessions = await sessionsService.listRunningSessions();
218
+ assert.equal(sessions.length, 1, 'a session with a running chat run is listed once');
219
+ assert.deepEqual(sessions[0], {
220
+ sessionId: 'busy-session',
221
+ provider: 'claude',
222
+ startedAt: run.startedAt,
223
+ lastSeq: 0,
224
+ tasks,
225
+ });
226
+ }));
227
+ });
228
+ });
229
+ test('running sessions with no background work are the chat runs alone', { concurrency: false }, async () => {
230
+ await withIsolatedDatabase(async () => {
231
+ sessionsDb.createAppSession('plain-session', 'codex', '/tmp/running-project');
232
+ const run = chatRunRegistry.startRun({
233
+ appSessionId: 'plain-session', provider: 'codex', providerSessionId: null, connection: null, userId: null,
234
+ });
235
+ assert.ok(run);
236
+ await withClaudeCliRegistry([], async () => await withProviders({ codex: { run: async () => undefined, abort: () => false } }, async () => {
237
+ assert.deepEqual(await sessionsService.listRunningSessions(), [{
238
+ sessionId: 'plain-session', provider: 'codex', startedAt: run.startedAt, lastSeq: 0,
239
+ }]);
240
+ }));
241
+ });
242
+ });
243
+ test('running sessions report a turn driven by the Claude CLI', { concurrency: false }, async () => {
244
+ await withIsolatedDatabase(async () => {
245
+ sessionsDb.createAppSession('cli-session', 'claude', '/tmp/running-project');
246
+ sessionsDb.assignProviderSessionId('cli-session', 'claude-native-1');
247
+ // `process.pid` is the one pid guaranteed to be alive; omitting procStart
248
+ // keeps the fixture portable, since the start-time cross-check is Linux-only.
249
+ await withClaudeCliRegistry([{ pid: process.pid, sessionId: 'claude-native-1', status: 'busy', entrypoint: 'cli', startedAt: 4_000 }], async () => await withProviders({ claude: { run: async () => undefined, abort: () => false } }, async () => {
250
+ assert.deepEqual(await sessionsService.listRunningSessions(), [{
251
+ sessionId: 'cli-session',
252
+ provider: 'claude',
253
+ startedAt: 4_000,
254
+ lastSeq: 0,
255
+ canInterrupt: false,
256
+ statusText: 'Running in the Claude CLI',
257
+ }]);
258
+ }));
259
+ });
260
+ });
261
+ test('running sessions ignore registry files a crash left behind', { concurrency: false }, async () => {
262
+ await withIsolatedDatabase(async () => {
263
+ sessionsDb.createAppSession('stale-session', 'claude', '/tmp/running-project');
264
+ sessionsDb.assignProviderSessionId('stale-session', 'claude-native-stale');
265
+ await withClaudeCliRegistry([
266
+ // The process is gone: signal 0 cannot find this pid.
267
+ { pid: 4_194_302, sessionId: 'claude-native-stale', status: 'busy', startedAt: 1_000 },
268
+ // Alive, but waiting for input rather than producing a response.
269
+ { pid: process.pid, sessionId: 'claude-native-idle', status: 'idle', startedAt: 2_000 },
270
+ ], async () => await withProviders({ claude: { run: async () => undefined, abort: () => false } }, async () => {
271
+ assert.deepEqual(await sessionsService.listRunningSessions(), []);
272
+ }));
273
+ });
274
+ });
275
+ test('a CLI turn does not duplicate a session already running a chat run', { concurrency: false }, async () => {
276
+ await withIsolatedDatabase(async () => {
277
+ sessionsDb.createAppSession('both-session', 'claude', '/tmp/running-project');
278
+ sessionsDb.assignProviderSessionId('both-session', 'claude-native-both');
279
+ const run = chatRunRegistry.startRun({
280
+ appSessionId: 'both-session', provider: 'claude', providerSessionId: 'claude-native-both', connection: null, userId: null,
281
+ });
282
+ assert.ok(run);
283
+ await withClaudeCliRegistry([{ pid: process.pid, sessionId: 'claude-native-both', status: 'busy', startedAt: 3_000 }], async () => await withProviders({ claude: { run: async () => undefined, abort: () => false } }, async () => {
284
+ const sessions = await sessionsService.listRunningSessions();
285
+ assert.equal(sessions.length, 1, 'the chat run already covers this session');
286
+ assert.equal(sessions[0].canInterrupt, undefined, 'the interruptible chat run entry wins');
287
+ }));
288
+ });
289
+ });
290
+ test('an unreadable registry never fails the running-sessions poll', { concurrency: false }, async () => {
291
+ await withIsolatedDatabase(async () => {
292
+ sessionsDb.createAppSession('safe-session', 'claude', '/tmp/running-project');
293
+ sessionsDb.assignProviderSessionId('safe-session', 'claude-native-safe');
294
+ await withClaudeCliRegistry([{ pid: process.pid, sessionId: 'claude-native-safe', status: 'busy', startedAt: 5_000 }], async () => {
295
+ // A half-written file sits alongside the good one; it must be skipped
296
+ // rather than abort the whole poll.
297
+ await writeFile(path.join(os.homedir(), '.claude', 'sessions', 'broken.json'), '{"pid":', 'utf8');
298
+ await withProviders({ claude: { run: async () => undefined, abort: () => false } }, async () => {
299
+ const sessions = await sessionsService.listRunningSessions();
300
+ assert.deepEqual(sessions.map((session) => session.sessionId), ['safe-session']);
301
+ });
302
+ });
303
+ });
304
+ });
87
305
  //# sourceMappingURL=sessions.service.test.js.map