@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
@@ -6,7 +6,10 @@ import test from 'node:test';
6
6
 
7
7
  import Database from 'better-sqlite3';
8
8
 
9
- import { createProviderTokenUsageService } from '@/modules/providers/services/provider-token-usage.service.js';
9
+ import {
10
+ createProviderTokenUsageService,
11
+ summarizeClaudeTokenUsage,
12
+ } from '@/modules/providers/services/provider-token-usage.service.js';
10
13
  import { AppError } from '@/shared/utils.js';
11
14
 
12
15
  function createSessionRow(overrides: Record<string, unknown> = {}) {
@@ -19,6 +22,7 @@ function createSessionRow(overrides: Record<string, unknown> = {}) {
19
22
  custom_name: null,
20
23
  model: null,
21
24
  effort: null,
25
+ forked_from_session_id: null,
22
26
  isArchived: 0,
23
27
  created_at: '2026-01-01T00:00:00.000Z',
24
28
  updated_at: '2026-01-01T00:00:00.000Z',
@@ -184,3 +188,186 @@ test('token usage reports SESSION_NOT_FOUND for an unknown app session id', asyn
184
188
  ),
185
189
  );
186
190
  });
191
+
192
+ test('the Claude summarizer reads the newest assistant turn, not the whole conversation', () => {
193
+ const entries = [
194
+ { type: 'assistant', message: { usage: { input_tokens: 5, cache_read_input_tokens: 1000, output_tokens: 50 } } },
195
+ { type: 'user', message: { role: 'user', content: 'next' } },
196
+ // The newest turn's prompt is the whole context, so its cache_read already
197
+ // includes everything before it. Summing turns would double-count.
198
+ { type: 'assistant', message: { usage: { input_tokens: 3, cache_read_input_tokens: 4000, cache_creation_input_tokens: 100, output_tokens: 80 } } },
199
+ ];
200
+
201
+ assert.deepEqual(summarizeClaudeTokenUsage(entries, '200000'), {
202
+ used: 4183,
203
+ total: 200_000,
204
+ inputTokens: 4103,
205
+ outputTokens: 80,
206
+ cacheReadTokens: 4000,
207
+ cacheCreationTokens: 100,
208
+ cacheTokens: 4100,
209
+ breakdown: { input: 4103, output: 80 },
210
+ });
211
+ });
212
+
213
+ test('the Claude summarizer skips synthetic rows that carry an all-zero usage block', () => {
214
+ // Interrupts, API errors and "No response requested." are written as
215
+ // assistant rows with a fully zeroed usage block. Reading one as the newest
216
+ // turn dropped the composer counter to 0 until the next turn pushed it back.
217
+ const entries = [
218
+ { type: 'assistant', message: { usage: { input_tokens: 3, cache_read_input_tokens: 4000, output_tokens: 80 } } },
219
+ {
220
+ type: 'assistant',
221
+ message: {
222
+ model: '<synthetic>',
223
+ usage: {
224
+ input_tokens: 0,
225
+ output_tokens: 0,
226
+ cache_creation_input_tokens: 0,
227
+ cache_read_input_tokens: 0,
228
+ },
229
+ },
230
+ },
231
+ ];
232
+
233
+ assert.equal(summarizeClaudeTokenUsage(entries, '200000').used, 4083);
234
+ });
235
+
236
+ test('the Claude summarizer skips a subagent sidechain turn', () => {
237
+ // A sidechain turn reports the subagent's own context window. Reading it
238
+ // made the counter drop to the subagent's number mid-run.
239
+ const entries = [
240
+ { type: 'assistant', message: { usage: { input_tokens: 3, cache_read_input_tokens: 4000, output_tokens: 80 } } },
241
+ {
242
+ type: 'assistant',
243
+ isSidechain: true,
244
+ message: { usage: { input_tokens: 10, cache_read_input_tokens: 900, output_tokens: 5 } },
245
+ },
246
+ ];
247
+
248
+ assert.equal(summarizeClaudeTokenUsage(entries, '200000').used, 4083);
249
+ });
250
+
251
+ test('the Claude summarizer reports zero for a transcript with no assistant turn yet', () => {
252
+ const usage = summarizeClaudeTokenUsage([{ type: 'user', message: { role: 'user', content: 'hi' } }], '200000');
253
+
254
+ assert.equal(usage.used, 0);
255
+ assert.equal(usage.total, 200_000);
256
+ });
257
+
258
+ /** Padding rows large enough to push earlier rows out of the 4MB tail window. */
259
+ function paddingLines(totalBytes: number): string {
260
+ const line = JSON.stringify({ type: 'attachment', filler: 'x'.repeat(4096) });
261
+ return Array.from({ length: Math.ceil(totalBytes / line.length) }, () => line).join('\n');
262
+ }
263
+
264
+ test('Claude token usage reads only the tail of a large transcript', async () => {
265
+ const tempDirectory = await mkdtemp(path.join(tmpdir(), 'provider-token-usage-claude-tail-'));
266
+ const sessionFilePath = path.join(tempDirectory, 'provider-session.jsonl');
267
+
268
+ try {
269
+ await writeFile(sessionFilePath, [
270
+ paddingLines(5 * 1024 * 1024),
271
+ JSON.stringify({ type: 'assistant', message: { usage: { input_tokens: 7, output_tokens: 2 } } }),
272
+ ].join('\n'));
273
+
274
+ const service = createProviderTokenUsageService({
275
+ getSessionById: () => createSessionRow({ jsonl_path: sessionFilePath }),
276
+ getClaudeContextWindow: () => '180000',
277
+ // Reading the whole file here would defeat the tail read; fail loudly.
278
+ readTextFile: () => { throw new Error('full read must not happen when the tail has usage'); },
279
+ });
280
+
281
+ const usage = await service.getSessionTokenUsage('app-session');
282
+ assert.equal(usage.inputTokens, 7);
283
+ assert.equal(usage.outputTokens, 2);
284
+ } finally {
285
+ await rm(tempDirectory, { recursive: true, force: true });
286
+ }
287
+ });
288
+
289
+ test('Claude token usage falls back to the whole file when the tail has no usage row', async () => {
290
+ const tempDirectory = await mkdtemp(path.join(tmpdir(), 'provider-token-usage-claude-fallback-'));
291
+ const sessionFilePath = path.join(tempDirectory, 'provider-session.jsonl');
292
+
293
+ try {
294
+ await writeFile(sessionFilePath, [
295
+ JSON.stringify({ type: 'assistant', message: { usage: { input_tokens: 11, output_tokens: 3 } } }),
296
+ paddingLines(5 * 1024 * 1024),
297
+ ].join('\n'));
298
+
299
+ const service = createProviderTokenUsageService({
300
+ getSessionById: () => createSessionRow({ jsonl_path: sessionFilePath }),
301
+ getClaudeContextWindow: () => '180000',
302
+ });
303
+
304
+ const usage = await service.getSessionTokenUsage('app-session');
305
+ assert.equal(usage.inputTokens, 11);
306
+ assert.equal(usage.outputTokens, 3);
307
+ } finally {
308
+ await rm(tempDirectory, { recursive: true, force: true });
309
+ }
310
+ });
311
+
312
+ test('Codex token usage reads only the tail of a large rollout', async () => {
313
+ const tempDirectory = await mkdtemp(path.join(tmpdir(), 'provider-token-usage-codex-tail-'));
314
+ const sessionFilePath = path.join(tempDirectory, 'rollout-provider-session.jsonl');
315
+
316
+ try {
317
+ await writeFile(sessionFilePath, [
318
+ paddingLines(5 * 1024 * 1024),
319
+ JSON.stringify({
320
+ type: 'event_msg',
321
+ payload: {
322
+ type: 'token_count',
323
+ info: {
324
+ total_token_usage: { input_tokens: 21, output_tokens: 8, total_tokens: 29 },
325
+ model_context_window: 150_000,
326
+ },
327
+ },
328
+ }),
329
+ ].join('\n'));
330
+
331
+ const service = createProviderTokenUsageService({
332
+ getSessionById: () => createSessionRow({ provider: 'codex', jsonl_path: sessionFilePath }),
333
+ readTextFile: () => { throw new Error('full read must not happen when the tail has usage'); },
334
+ });
335
+
336
+ assert.deepEqual(await service.getSessionTokenUsage('app-session'), {
337
+ used: 29,
338
+ total: 150_000,
339
+ inputTokens: 21,
340
+ outputTokens: 8,
341
+ breakdown: { input: 21, output: 8 },
342
+ });
343
+ } finally {
344
+ await rm(tempDirectory, { recursive: true, force: true });
345
+ }
346
+ });
347
+
348
+ test('Codex token usage falls back to the whole file when the tail has no token_count row', async () => {
349
+ const tempDirectory = await mkdtemp(path.join(tmpdir(), 'provider-token-usage-codex-fallback-'));
350
+ const sessionFilePath = path.join(tempDirectory, 'rollout-provider-session.jsonl');
351
+
352
+ try {
353
+ await writeFile(sessionFilePath, [
354
+ JSON.stringify({
355
+ type: 'event_msg',
356
+ payload: {
357
+ type: 'token_count',
358
+ info: { total_token_usage: { input_tokens: 5, output_tokens: 1, total_tokens: 6 } },
359
+ },
360
+ }),
361
+ paddingLines(5 * 1024 * 1024),
362
+ ].join('\n'));
363
+
364
+ const service = createProviderTokenUsageService({
365
+ getSessionById: () => createSessionRow({ provider: 'codex', jsonl_path: sessionFilePath }),
366
+ });
367
+
368
+ const usage = await service.getSessionTokenUsage('app-session');
369
+ assert.equal(usage.used, 6);
370
+ } finally {
371
+ await rm(tempDirectory, { recursive: true, force: true });
372
+ }
373
+ });
@@ -1,6 +1,6 @@
1
1
  import assert from 'node:assert/strict';
2
2
  import { once } from 'node:events';
3
- import { mkdtemp, rm, writeFile } from 'node:fs/promises';
3
+ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
4
4
  import type { AddressInfo } from 'node:net';
5
5
  import os from 'node:os';
6
6
  import path from 'node:path';
@@ -10,6 +10,9 @@ import express, { type NextFunction, type Request, type Response } from 'express
10
10
 
11
11
  import { closeConnection, initializeDatabase, sessionsDb } from '@/modules/database/index.js';
12
12
  import providerRouter from '@/modules/providers/provider.routes.js';
13
+ import { providerRegistry } from '@/modules/providers/provider.registry.js';
14
+ import type { IProvider } from '@/shared/interfaces.js';
15
+ import type { BackgroundTaskSummary, WorkflowAgentActivity } from '@/shared/types.js';
13
16
  import { AppError } from '@/shared/utils.js';
14
17
 
15
18
  async function withProviderServer(
@@ -242,3 +245,229 @@ test('model routes expose immutable defaults and full custom model CRUD', async
242
245
  );
243
246
  });
244
247
  });
248
+
249
+ test('custom model routes accept, validate, and return reasoning-effort levels', async () => {
250
+ await withProviderServer(async (baseUrl) => {
251
+ const postModel = async (provider: string, body: unknown) => {
252
+ const response = await fetch(`${baseUrl}/api/providers/${provider}/models`, {
253
+ method: 'POST',
254
+ headers: { 'content-type': 'application/json' },
255
+ body: JSON.stringify(body),
256
+ });
257
+ return {
258
+ status: response.status,
259
+ payload: await response.json() as {
260
+ data?: {
261
+ model: { recordId: number; effort?: unknown };
262
+ models: { OPTIONS: Array<{ value: string; effort?: unknown }> };
263
+ };
264
+ error?: { code: string; message?: string };
265
+ },
266
+ };
267
+ };
268
+
269
+ const created = await postModel('claude', {
270
+ model: 'My Custom Model',
271
+ id: 'my-custom-model',
272
+ effort: { values: [' low ', 'high'], default: 'high' },
273
+ });
274
+ const expectedEffort = { default: 'high', values: [{ value: 'low' }, { value: 'high' }] };
275
+ assert.equal(created.status, 201);
276
+ assert.deepEqual(created.payload.data?.model.effort, expectedEffort);
277
+
278
+ const catalogResponse = await fetch(`${baseUrl}/api/providers/claude/models`);
279
+ const catalogPayload = await catalogResponse.json() as {
280
+ data: { models: { OPTIONS: Array<{ value: string; effort?: unknown }>; EFFORT_LEVELS?: string[] } };
281
+ };
282
+ assert.deepEqual(
283
+ catalogPayload.data.models.OPTIONS.find((option) => option.value === 'my-custom-model')?.effort,
284
+ expectedEffort,
285
+ );
286
+ // The model library's toggles come from here, not from the built-in models.
287
+ assert.deepEqual(
288
+ catalogPayload.data.models.EFFORT_LEVELS,
289
+ ['low', 'medium', 'high', 'xhigh', 'max', 'ultracode'],
290
+ );
291
+
292
+ // OpenCode's allowed levels do not depend on which upstream providers the
293
+ // machine running the tests has connected.
294
+ const openCodeModel = await postModel('opencode', {
295
+ model: 'Router model',
296
+ id: 'openrouter/router-model',
297
+ effort: { values: ['none', 'high'] },
298
+ });
299
+ assert.equal(openCodeModel.status, 201, JSON.stringify(openCodeModel.payload));
300
+ assert.deepEqual(openCodeModel.payload.data?.model.effort, { values: [{ value: 'none' }, { value: 'high' }] });
301
+
302
+ // A PATCH from a client that predates effort metadata keeps the levels.
303
+ const renameResponse = await fetch(
304
+ `${baseUrl}/api/providers/claude/models/${created.payload.data?.model.recordId}`,
305
+ {
306
+ method: 'PATCH',
307
+ headers: { 'content-type': 'application/json' },
308
+ body: JSON.stringify({ model: 'Renamed', id: 'my-custom-model' }),
309
+ },
310
+ );
311
+ const renamePayload = await renameResponse.json() as { data: { model: { effort?: unknown } } };
312
+ assert.equal(renameResponse.status, 200);
313
+ assert.deepEqual(renamePayload.data.model.effort, expectedEffort);
314
+
315
+ const invalidPayloads: Array<{ provider: string; effort: unknown; code: string }> = [
316
+ { provider: 'claude', effort: { values: ['low', 'low'] }, code: 'INVALID_MODEL_EFFORT' },
317
+ { provider: 'claude', effort: { values: ['low', ''] }, code: 'INVALID_MODEL_EFFORT' },
318
+ { provider: 'claude', effort: { values: ['low'], default: 'high' }, code: 'INVALID_MODEL_EFFORT' },
319
+ { provider: 'claude', effort: { values: ['turbo'] }, code: 'INVALID_MODEL_EFFORT' },
320
+ { provider: 'claude', effort: 'high', code: 'INVALID_MODEL_EFFORT' },
321
+ { provider: 'cursor', effort: { values: ['low'] }, code: 'MODEL_EFFORT_NOT_SUPPORTED' },
322
+ ];
323
+ for (const [index, invalid] of invalidPayloads.entries()) {
324
+ const rejected = await postModel(invalid.provider, {
325
+ model: `Invalid ${index}`,
326
+ id: `invalid-${index}`,
327
+ effort: invalid.effort,
328
+ });
329
+ assert.equal(rejected.status, 400, JSON.stringify(invalid));
330
+ assert.equal(rejected.payload.error?.code, invalid.code, JSON.stringify(invalid));
331
+ }
332
+
333
+ // An oversized list is refused by its length, before any entry is walked:
334
+ // the JSON parser accepts bodies of up to 50 MB.
335
+ const oversized = await postModel('claude', {
336
+ model: 'Oversized',
337
+ id: 'oversized',
338
+ effort: { values: Array.from({ length: 33 }, (_, index) => `level-${index}`) },
339
+ });
340
+ assert.equal(oversized.status, 400);
341
+ assert.equal(oversized.payload.error?.code, 'INVALID_MODEL_EFFORT');
342
+ assert.match(oversized.payload.error?.message ?? '', /at most 32 effort levels/);
343
+ });
344
+ });
345
+
346
+ test('the running-sessions route reports a session held open for background work', async () => {
347
+ // The client polls this route to keep every tab's activity map in step, so
348
+ // a session whose turn ended but whose agent is still running has to be in
349
+ // the payload — flagged as background work, with the tasks to show and stop.
350
+ const tasks: BackgroundTaskSummary[] = [{
351
+ taskId: 'a73dc7a6442f7c415',
352
+ toolUseId: 'toolu_013sUz4iWcNjN6BskVkHUCAr',
353
+ taskType: 'local_agent',
354
+ description: 'Investigate the flaky test',
355
+ startedAt: 1_700_000_000_000,
356
+ }];
357
+ const realListProviders = providerRegistry.listProviders;
358
+ providerRegistry.listProviders = () => [{
359
+ id: 'claude',
360
+ runtime: { listBackgroundWork: () => [{ sessionId: 'held-session', tasks }] },
361
+ } as unknown as IProvider];
362
+
363
+ try {
364
+ await withProviderServer(async (baseUrl) => {
365
+ const response = await fetch(`${baseUrl}/api/providers/sessions/running`);
366
+ const payload = await response.json() as { data: { sessions: unknown[] } };
367
+
368
+ assert.equal(response.status, 200);
369
+ assert.deepEqual(payload.data.sessions, [{
370
+ sessionId: 'held-session',
371
+ provider: 'claude',
372
+ startedAt: 1_700_000_000_000,
373
+ lastSeq: 0,
374
+ background: true,
375
+ canInterrupt: false,
376
+ tasks,
377
+ }]);
378
+ });
379
+ } finally {
380
+ providerRegistry.listProviders = realListProviders;
381
+ }
382
+ });
383
+
384
+ const WORKFLOW_SESSION_ID = 'claude-workflow-routes-session';
385
+ const WORKFLOW_RUN_ID = 'wf_16fbf852-274';
386
+ const FINISHED_AGENT_ID = 'aa1e064cf8bd159d6';
387
+ const UNREPORTED_AGENT_ID = 'ab89f2cde612a51b1';
388
+
389
+ /**
390
+ * Writes a workflow run's directory the way a real run leaves it under the
391
+ * session's transcript directory: one `agent-<id>.jsonl` per agent, in the
392
+ * row shape an Agent subagent's transcript has, and the journal beside them.
393
+ */
394
+ async function writeWorkflowRun(projectDirectory: string): Promise<string> {
395
+ const sessionPath = path.join(projectDirectory, `${WORKFLOW_SESSION_ID}.jsonl`);
396
+ await mkdir(projectDirectory, { recursive: true });
397
+ await writeFile(sessionPath, `${JSON.stringify({ type: 'user', uuid: 'u1', sessionId: WORKFLOW_SESSION_ID, timestamp: '2026-08-21T10:32:10.000Z', message: { role: 'user', content: 'audit the frontend' } })}\n`, 'utf8');
398
+
399
+ const runDir = path.join(projectDirectory, WORKFLOW_SESSION_ID, 'subagents', 'workflows', WORKFLOW_RUN_ID);
400
+ await mkdir(runDir, { recursive: true });
401
+
402
+ const agentRows = (agentId: string) => [
403
+ { parentUuid: null, isSidechain: true, agentId, type: 'user', uuid: `${agentId}-u1`, timestamp: '2026-08-21T10:32:20.000Z', message: { role: 'user', content: 'You are the analysis step of a code-quality audit.' } },
404
+ { parentUuid: `${agentId}-u1`, isSidechain: true, agentId, type: 'assistant', uuid: `${agentId}-a1`, timestamp: '2026-08-21T10:32:25.000Z', message: { role: 'assistant', model: 'claude-opus-4-1', content: [{ type: 'tool_use', id: `toolu_${agentId}_1`, name: 'Grep', input: { pattern: 'useSidebar' } }] } },
405
+ { parentUuid: `${agentId}-a1`, isSidechain: true, agentId, type: 'user', uuid: `${agentId}-u2`, timestamp: '2026-08-21T10:32:26.000Z', message: { role: 'user', content: [{ type: 'tool_result', tool_use_id: `toolu_${agentId}_1`, content: 'src/modules/sidebar/useSidebar.ts' }] } },
406
+ { parentUuid: `${agentId}-u2`, isSidechain: true, agentId, type: 'assistant', uuid: `${agentId}-a2`, timestamp: '2026-08-21T10:32:40.000Z', message: { role: 'assistant', model: 'claude-opus-4-1', content: [{ type: 'text', text: 'One hook owns the sidebar.' }] } },
407
+ ];
408
+ for (const agentId of [FINISHED_AGENT_ID, UNREPORTED_AGENT_ID]) {
409
+ await writeFile(path.join(runDir, `agent-${agentId}.jsonl`), `${agentRows(agentId).map((row) => JSON.stringify(row)).join('\n')}\n`, 'utf8');
410
+ await writeFile(path.join(runDir, `agent-${agentId}.meta.json`), JSON.stringify({ agentType: 'workflow-subagent', spawnDepth: 1 }), 'utf8');
411
+ }
412
+ const journal = [
413
+ { type: 'started', key: 'v2:one', agentId: FINISHED_AGENT_ID, label: 'audit:sidebar', phase: 'Audit' },
414
+ { type: 'started', key: 'v2:two', agentId: UNREPORTED_AGENT_ID, label: 'synthesize', phase: 'Synthesize' },
415
+ { type: 'result', key: 'v2:one', agentId: FINISHED_AGENT_ID, result: { area: 'sidebar' } },
416
+ ];
417
+ await writeFile(path.join(runDir, 'journal.jsonl'), `${journal.map((row) => JSON.stringify(row)).join('\n')}\n`, 'utf8');
418
+
419
+ return sessionPath;
420
+ }
421
+
422
+ test('the workflow agent route reads an agent\'s timeline and status from its run directory', async () => {
423
+ await withProviderServer(async (baseUrl, workspacePath) => {
424
+ const sessionPath = await writeWorkflowRun(workspacePath);
425
+ const now = new Date().toISOString();
426
+ sessionsDb.createSession(WORKFLOW_SESSION_ID, 'claude', workspacePath, 'Workflow session', now, now, sessionPath);
427
+
428
+ const agentUrl = (runId: string, agentId: string) =>
429
+ `${baseUrl}/api/providers/sessions/${WORKFLOW_SESSION_ID}/workflows/${runId}/agents/${agentId}`;
430
+
431
+ // The SDK streams none of an agent's rows to the parent, so its transcript
432
+ // is the only record of what it did — read on demand, capped like an
433
+ // Agent card's, and settled by the journal beside it.
434
+ const finishedResponse = await fetch(agentUrl(WORKFLOW_RUN_ID, FINISHED_AGENT_ID));
435
+ const finished = (await finishedResponse.json() as { data: WorkflowAgentActivity }).data;
436
+ assert.equal(finishedResponse.status, 200);
437
+ assert.deepEqual(finished.agent, { id: FINISHED_AGENT_ID, label: 'audit:sidebar', model: 'claude-opus-4-1', status: 'completed' });
438
+ assert.equal(finished.activityCount, 2);
439
+ assert.deepEqual(finished.activity.map((entry) => entry.kind), ['tool', 'text']);
440
+ assert.equal(finished.activity[0]?.toolName, 'Grep');
441
+ assert.deepEqual(finished.activity[0]?.toolInput, { pattern: 'useSidebar' });
442
+ assert.deepEqual(finished.activity[0]?.toolResult, { content: 'src/modules/sidebar/useSidebar.ts', isError: false });
443
+ assert.equal(finished.activity[1]?.content, 'One hook owns the sidebar.');
444
+
445
+ // A `started` with no `result` or `failed`, and no live run of the session
446
+ // to still be working in: nothing can report for it any more.
447
+ const unreportedResponse = await fetch(agentUrl(WORKFLOW_RUN_ID, UNREPORTED_AGENT_ID));
448
+ const unreported = (await unreportedResponse.json() as { data: WorkflowAgentActivity }).data;
449
+ assert.equal(unreportedResponse.status, 200);
450
+ assert.equal(unreported.agent.status, 'stopped');
451
+ assert.equal(unreported.agent.label, 'synthesize');
452
+
453
+ // Both ids name a file under the session directory, so neither may carry
454
+ // anything but the harness's own shapes — not even a well-formed
455
+ // directory that happens to exist.
456
+ for (const [runId, agentId, code] of [
457
+ [`${WORKFLOW_RUN_ID}%2F..`, FINISHED_AGENT_ID, 'INVALID_WORKFLOW_RUN_ID'],
458
+ ['subagents', FINISHED_AGENT_ID, 'INVALID_WORKFLOW_RUN_ID'],
459
+ [WORKFLOW_RUN_ID, '..%2Fjournal', 'INVALID_WORKFLOW_AGENT_ID'],
460
+ [WORKFLOW_RUN_ID, 'aa1e064cf8bd159d', 'INVALID_WORKFLOW_AGENT_ID'],
461
+ ] as const) {
462
+ const response = await fetch(agentUrl(runId, agentId));
463
+ const payload = await response.json() as { error: { code: string } };
464
+ assert.equal(response.status, 400, `${runId}/${agentId}`);
465
+ assert.equal(payload.error.code, code);
466
+ }
467
+
468
+ const missingResponse = await fetch(agentUrl(WORKFLOW_RUN_ID, 'a0000000000000000'));
469
+ const missing = await missingResponse.json() as { error: { code: string } };
470
+ assert.equal(missingResponse.status, 404);
471
+ assert.equal(missing.error.code, 'WORKFLOW_AGENT_NOT_FOUND');
472
+ });
473
+ });
@@ -0,0 +1,173 @@
1
+ import assert from 'node:assert/strict';
2
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import test from 'node:test';
6
+
7
+ import { closeConnection, initializeDatabase, sessionsDb } from '@/modules/database/index.js';
8
+ import { providerRegistry } from '@/modules/providers/provider.registry.js';
9
+ import { sessionsService } from '@/modules/providers/services/sessions.service.js';
10
+ import type { IProviderFork } from '@/shared/interfaces.js';
11
+
12
+ const SOURCE_ID = 'fork-source';
13
+
14
+ type ForkCall = {
15
+ providerSessionId: string;
16
+ jsonlPath: string;
17
+ upToAnchorId?: string;
18
+ title?: string;
19
+ };
20
+
21
+ async function withForkableClaude(
22
+ runTest: (context: { calls: ForkCall[]; directory: string }) => Promise<void>,
23
+ options: { disableFork?: boolean } = {},
24
+ ): Promise<void> {
25
+ const previousDatabasePath = process.env.DATABASE_PATH;
26
+ const directory = await mkdtemp(path.join(os.tmpdir(), 'session-fork-'));
27
+ const sourcePath = path.join(directory, 'native-source.jsonl');
28
+ const forkedPath = path.join(directory, 'native-fork.jsonl');
29
+ await writeFile(sourcePath, '{}\n', 'utf8');
30
+ await writeFile(forkedPath, '{}\n', 'utf8');
31
+
32
+ closeConnection();
33
+ process.env.DATABASE_PATH = path.join(directory, 'auth.db');
34
+ await initializeDatabase();
35
+
36
+ const calls: ForkCall[] = [];
37
+ const claude = providerRegistry.resolveProvider('claude') as { fork?: IProviderFork };
38
+ const realFork = claude.fork;
39
+ const replacement = options.disableFork
40
+ ? undefined
41
+ : {
42
+ forkSession: async (input: ForkCall) => {
43
+ calls.push(input);
44
+ return { providerSessionId: 'native-fork', jsonlPath: forkedPath };
45
+ },
46
+ } as IProviderFork;
47
+
48
+ Object.defineProperty(claude, 'fork', { value: replacement, configurable: true, writable: true });
49
+
50
+ try {
51
+ await runTest({ calls, directory });
52
+ } finally {
53
+ Object.defineProperty(claude, 'fork', { value: realFork, configurable: true, writable: true });
54
+ closeConnection();
55
+ if (previousDatabasePath === undefined) {
56
+ delete process.env.DATABASE_PATH;
57
+ } else {
58
+ process.env.DATABASE_PATH = previousDatabasePath;
59
+ }
60
+ await rm(directory, { recursive: true, force: true });
61
+ }
62
+ }
63
+
64
+ function seedSource(directory: string): void {
65
+ const now = new Date().toISOString();
66
+ sessionsDb.createSession(
67
+ SOURCE_ID,
68
+ 'claude',
69
+ directory,
70
+ 'Original session',
71
+ now,
72
+ now,
73
+ path.join(directory, 'native-source.jsonl'),
74
+ );
75
+ sessionsDb.assignProviderSessionId(SOURCE_ID, 'native-source');
76
+ }
77
+
78
+ test('a fork becomes an independent session that points back at its source', async () => {
79
+ await withForkableClaude(async ({ calls, directory }) => {
80
+ seedSource(directory);
81
+
82
+ const result = await sessionsService.forkSessionById(SOURCE_ID, { upToAnchorId: 'uuid-3' });
83
+
84
+ assert.equal(calls.length, 1);
85
+ assert.equal(calls[0].providerSessionId, 'native-source');
86
+ assert.equal(calls[0].upToAnchorId, 'uuid-3');
87
+
88
+ const forked = sessionsDb.getSessionById(result.sessionId);
89
+ assert.ok(forked);
90
+ // Written before the watcher can see the new file, so it is never indexed
91
+ // a second time under its provider-native id.
92
+ assert.equal(forked?.provider_session_id, 'native-fork');
93
+ assert.equal(forked?.jsonl_path, path.join(directory, 'native-fork.jsonl'));
94
+ assert.equal(forked?.forked_from_session_id, SOURCE_ID);
95
+ assert.equal(forked?.custom_name, 'Original session (fork)');
96
+
97
+ // The source is untouched: this is "try two approaches", not a move.
98
+ const source = sessionsDb.getSessionById(SOURCE_ID);
99
+ assert.equal(source?.provider_session_id, 'native-source');
100
+ assert.equal(source?.custom_name, 'Original session');
101
+ });
102
+ });
103
+
104
+ test('a fork inherits the model and effort of the conversation it branched from', async () => {
105
+ await withForkableClaude(async ({ directory }) => {
106
+ seedSource(directory);
107
+ sessionsDb.setSessionModel(SOURCE_ID, 'claude-opus-5');
108
+ sessionsDb.setSessionEffort(SOURCE_ID, 'xhigh');
109
+
110
+ const result = await sessionsService.forkSessionById(SOURCE_ID);
111
+
112
+ const forked = sessionsDb.getSessionById(result.sessionId);
113
+ assert.equal(forked?.model, 'claude-opus-5');
114
+ assert.equal(forked?.effort, 'xhigh');
115
+ });
116
+ });
117
+
118
+ test('forking replaces a row the watcher already made for the new transcript', async () => {
119
+ await withForkableClaude(async ({ directory }) => {
120
+ seedSource(directory);
121
+ // The watcher wins the race and indexes the fork under its native id.
122
+ const now = new Date().toISOString();
123
+ sessionsDb.createSession(
124
+ 'native-fork',
125
+ 'claude',
126
+ directory,
127
+ 'Indexed by the watcher',
128
+ now,
129
+ now,
130
+ path.join(directory, 'native-fork.jsonl'),
131
+ );
132
+
133
+ const result = await sessionsService.forkSessionById(SOURCE_ID);
134
+
135
+ assert.equal(sessionsDb.getSessionById('native-fork'), null);
136
+ assert.equal(sessionsDb.getSessionById(result.sessionId)?.provider_session_id, 'native-fork');
137
+ });
138
+ });
139
+
140
+ test('a session with no transcript yet cannot be forked', async () => {
141
+ await withForkableClaude(async ({ directory }) => {
142
+ // An app-created session that has never run: no provider id, no transcript.
143
+ sessionsDb.createAppSession('never-ran', 'claude', directory, 'Never ran');
144
+
145
+ await assert.rejects(
146
+ () => sessionsService.forkSessionById('never-ran'),
147
+ (error: Error & { code?: string }) => error.code === 'FORK_SOURCE_NOT_READY',
148
+ );
149
+ });
150
+ });
151
+
152
+ test('a provider without the capability is refused rather than silently ignored', async () => {
153
+ await withForkableClaude(
154
+ async ({ directory }) => {
155
+ seedSource(directory);
156
+
157
+ await assert.rejects(
158
+ () => sessionsService.forkSessionById(SOURCE_ID),
159
+ (error: Error & { code?: string }) => error.code === 'FORK_NOT_SUPPORTED',
160
+ );
161
+ },
162
+ { disableFork: true },
163
+ );
164
+ });
165
+
166
+ test('forking a session that does not exist is a 404', async () => {
167
+ await withForkableClaude(async () => {
168
+ await assert.rejects(
169
+ () => sessionsService.forkSessionById('no-such-session'),
170
+ (error: Error & { code?: string }) => error.code === 'SESSION_NOT_FOUND',
171
+ );
172
+ });
173
+ });