@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
@@ -24,7 +24,10 @@ import {
24
24
  buildClaudeUserContent,
25
25
  normalizeImageDescriptors
26
26
  } from '@/shared/image-attachments.js';
27
- import { CLAUDE_PREDEFINED_MODELS } from '@/modules/providers/list/claude/claude-models.provider.js';
27
+ import {
28
+ CLAUDE_PREDEFINED_MODELS,
29
+ CLAUDE_ULTRACODE_EFFORT
30
+ } from '@/modules/providers/list/claude/claude-models.provider.js';
28
31
  import { resolveClaudeCodeExecutablePath } from '@/shared/claude-cli-path.js';
29
32
  import {
30
33
  createNotificationEvent,
@@ -33,9 +36,14 @@ import {
33
36
  notifyRunStopped,
34
37
  notifyUserIfEnabled
35
38
  } from '@/modules/notifications/index.js';
39
+ import { sessionHistoryCache } from '@/modules/providers/services/session-history-cache.service.js';
36
40
  import { createCompleteMessage, createNormalizedMessage } from '@/shared/utils.js';
37
41
 
38
42
  const activeSessions = new Map();
43
+ // Outstanding background tasks per live session, keyed like activeSessions. An
44
+ // entry lives exactly as long as the map entry it shadows: cleared when the
45
+ // session is removed, and reset when a newer run takes the key over.
46
+ const backgroundWork = createBackgroundWorkTracker();
39
47
  const pendingToolApprovals = new Map();
40
48
  // Sessions cancelled via abort-session. The abort handler already sent the
41
49
  // terminal `complete` (aborted: true) to the client, so the run loop must not
@@ -69,6 +77,12 @@ const BG_WAIT_CEILING_MS = 30 * 60 * 1000;
69
77
 
70
78
  const TOOLS_REQUIRING_INTERACTION = new Set(['AskUserQuestion', 'ExitPlanMode']);
71
79
 
80
+ // Ultracode is a session-scoped setting rather than an SDK effort level: it pairs xhigh
81
+ // effort with standing dynamic-workflow orchestration, and the CLI only honours it when
82
+ // Workflows are enabled. The catalog offers it as an effort choice for the picker, so the
83
+ // selection is translated back into the two options the SDK actually understands here.
84
+ const ULTRACODE_SDK_EFFORT = 'xhigh';
85
+
72
86
  function resolveClaudeEffort(model, effort, modelsDefinition = CLAUDE_PREDEFINED_MODELS) {
73
87
  const selectedModel = modelsDefinition?.OPTIONS?.find((option) => option.value === model) || null;
74
88
  const allowedEfforts = selectedModel?.effort?.values
@@ -78,6 +92,30 @@ function resolveClaudeEffort(model, effort, modelsDefinition = CLAUDE_PREDEFINED
78
92
  : undefined;
79
93
  }
80
94
 
95
+ /**
96
+ * Writes the resolved effort choice onto the SDK options, expanding `ultracode` into the
97
+ * xhigh effort level plus the session-scoped settings it requires.
98
+ * @param {Object} sdkOptions - SDK options being built
99
+ * @param {string|undefined} resolvedEffort - Catalog-validated effort selection
100
+ */
101
+ function applyClaudeEffort(sdkOptions, resolvedEffort) {
102
+ if (!resolvedEffort) {
103
+ return;
104
+ }
105
+
106
+ if (resolvedEffort !== CLAUDE_ULTRACODE_EFFORT) {
107
+ sdkOptions.effort = resolvedEffort;
108
+ return;
109
+ }
110
+
111
+ sdkOptions.effort = ULTRACODE_SDK_EFFORT;
112
+ sdkOptions.settings = {
113
+ ...(sdkOptions.settings || {}),
114
+ ultracode: true,
115
+ enableWorkflows: true
116
+ };
117
+ }
118
+
81
119
  function createRequestId() {
82
120
  if (typeof crypto.randomUUID === 'function') {
83
121
  return crypto.randomUUID();
@@ -183,7 +221,7 @@ function matchesToolPermission(entry, toolName, input) {
183
221
  }
184
222
 
185
223
  function mapCliOptionsToSDK(options = {}) {
186
- const { providerSessionId, cwd, toolsSettings, permissionMode, effort } = options;
224
+ const { providerSessionId, cwd, toolsSettings, permissionMode, effort, resumeAnchorId, resumeFromScratch } = options;
187
225
 
188
226
  const sdkOptions = {};
189
227
 
@@ -193,7 +231,12 @@ function mapCliOptionsToSDK(options = {}) {
193
231
 
194
232
  // Resolve the executable eagerly on Windows because the SDK uses raw child_process.spawn,
195
233
  // which does not reliably follow npm's shell wrappers like cross-spawn does.
196
- sdkOptions.pathToClaudeCodeExecutable = resolveClaudeCodeExecutablePath(process.env.CLAUDE_CLI_PATH);
234
+ // When nothing resolves the option stays unset on purpose: the SDK then falls back to the
235
+ // binary it ships, which beats handing it a bare `claude` that raw spawn can never launch.
236
+ const claudeExecutablePath = resolveClaudeCodeExecutablePath(process.env.CLAUDE_CLI_PATH);
237
+ if (claudeExecutablePath) {
238
+ sdkOptions.pathToClaudeCodeExecutable = claudeExecutablePath;
239
+ }
197
240
 
198
241
  if (cwd) {
199
242
  sdkOptions.cwd = cwd;
@@ -235,14 +278,11 @@ function mapCliOptionsToSDK(options = {}) {
235
278
 
236
279
  sdkOptions.model = options.model || CLAUDE_PREDEFINED_MODELS.DEFAULT;
237
280
 
238
- const resolvedEffort = resolveClaudeEffort(
281
+ applyClaudeEffort(sdkOptions, resolveClaudeEffort(
239
282
  sdkOptions.model,
240
283
  effort,
241
284
  options.effortModels || CLAUDE_PREDEFINED_MODELS,
242
- );
243
- if (resolvedEffort) {
244
- sdkOptions.effort = resolvedEffort;
245
- }
285
+ ));
246
286
 
247
287
  sdkOptions.systemPrompt = {
248
288
  type: 'preset',
@@ -252,8 +292,19 @@ function mapCliOptionsToSDK(options = {}) {
252
292
  sdkOptions.settingSources = ['project', 'user', 'local'];
253
293
 
254
294
  // The SDK resumes with the provider-native session id, never the app id.
255
- if (providerSessionId) {
295
+ // `resumeFromScratch` is set when the very first prompt of a conversation was
296
+ // edited: there is nothing before it to resume through, so the turn has to
297
+ // start the conversation over instead.
298
+ if (providerSessionId && !resumeFromScratch) {
256
299
  sdkOptions.resume = providerSessionId;
300
+
301
+ // Editing an already-sent message re-runs the conversation truncated just
302
+ // before it. `resumeSessionAt` is inclusive of the uuid it names, so the
303
+ // caller resolves the last row to KEEP and passes that — never the edited
304
+ // turn itself, which would leave the original prompt in context.
305
+ if (resumeAnchorId) {
306
+ sdkOptions.resumeSessionAt = resumeAnchorId;
307
+ }
257
308
  }
258
309
 
259
310
  return sdkOptions;
@@ -286,6 +337,9 @@ function addSession(sessionId, queryInstance, writer = null, releaseInput = null
286
337
  console.error(`Error interrupting superseded run for session ${sessionId}:`, error?.message || error);
287
338
  });
288
339
  existing.releaseInput?.();
340
+ // Whatever the superseded process had outstanding dies with it and will
341
+ // never report, so the new run starts from an empty task set.
342
+ backgroundWork.clear(sessionId);
289
343
  }
290
344
  const carried = superseding ? null : existing;
291
345
  activeSessions.set(sessionId, {
@@ -296,6 +350,44 @@ function addSession(sessionId, queryInstance, writer = null, releaseInput = null
296
350
  // Re-registered mid-run once the provider session id lands; keep the closer.
297
351
  releaseInput: releaseInput || carried?.releaseInput || null
298
352
  });
353
+ // The history reader reports a background agent as running or stopped by
354
+ // whether this entry exists, and the cached history does not see this map.
355
+ sessionHistoryCache.invalidate(sessionId);
356
+ }
357
+
358
+ /**
359
+ * Lets go of the process an earlier run left holding a session open, so the
360
+ * next turn can take the session over.
361
+ *
362
+ * Closing stdin is not enough on its own: the CLI's wind-down waits for the
363
+ * background agents the process still has going — for up to the ceiling it
364
+ * was started with (BG_WAIT_CEILING_MS) — so an abandoned process with an
365
+ * agent outstanding would run next to the process that replaced it for as
366
+ * long as the agent does, both appending to the same session file. Stop the
367
+ * tasks first, while stdin is still open: the SDK drops control requests
368
+ * written after stdin has ended, which is also why the interrupt a later
369
+ * supersede sends to this process cannot do the job.
370
+ *
371
+ * The stops are not awaited — each request is written to the CLI before this
372
+ * returns, and a reply is only missing when the process has already gone.
373
+ * @param {string} sessionId - Session identifier
374
+ */
375
+ function releaseHeldSession(sessionId) {
376
+ const held = getSession(sessionId);
377
+ if (!held) {
378
+ return;
379
+ }
380
+ for (const task of backgroundWork.outstanding(sessionId)) {
381
+ // A stop that cannot be delivered changes nothing about what happens
382
+ // next: the process is on its way out either way. Guard the synchronous
383
+ // call too, so a failing stop can never skip the release below.
384
+ try {
385
+ Promise.resolve(held.instance?.stopTask?.(task.taskId)).catch(() => {});
386
+ } catch {
387
+ // Same as a rejected stop.
388
+ }
389
+ }
390
+ held.releaseInput?.();
299
391
  }
300
392
 
301
393
  /**
@@ -304,6 +396,11 @@ function addSession(sessionId, queryInstance, writer = null, releaseInput = null
304
396
  */
305
397
  function removeSession(sessionId) {
306
398
  activeSessions.delete(sessionId);
399
+ // No process, no background work: anything still tracked was killed with it.
400
+ backgroundWork.clear(sessionId);
401
+ // See addSession: a page cached while the process was up still says
402
+ // `running` for any agent that never reported back.
403
+ sessionHistoryCache.invalidate(sessionId);
307
404
  }
308
405
 
309
406
  /**
@@ -339,47 +436,129 @@ function transformMessage(sdkMessage) {
339
436
  return sdkMessage;
340
437
  }
341
438
 
439
+ /**
440
+ * True for the user bubble the SDK echoes for a subagent's own prompt.
441
+ *
442
+ * Subagent traffic carries `parent_tool_use_id`, so this echo lands in the main
443
+ * thread and stacks a second copy of the prompt right below the Agent tool card
444
+ * that already displays it. It also disappears on reload, because the transcript
445
+ * keeps that turn in the subagent's sidechain rather than the session file.
446
+ * @param {Object} message - Normalized message about to be sent to the client
447
+ * @returns {boolean}
448
+ */
449
+ export function isSubagentPromptEcho(message) {
450
+ return Boolean(message?.parentToolUseId) && message.role === 'user' && message.kind === 'text';
451
+ }
452
+
342
453
  function readNumber(value) {
343
454
  const parsed = Number(value);
344
455
  return Number.isFinite(parsed) ? parsed : 0;
345
456
  }
346
457
 
347
458
  /**
348
- * Extracts token usage from SDK messages.
349
- * Prefers per-step `message.usage` (Claude message payload), then falls back
350
- * to result-level usage/modelUsage for compatibility across SDK versions.
459
+ * @typedef {Object} TokenBudget
460
+ * @property {number} used
461
+ * @property {number} total
462
+ * @property {number} inputTokens
463
+ * @property {number} outputTokens
464
+ * @property {number} [cacheReadTokens]
465
+ * @property {number} [cacheCreationTokens]
466
+ * @property {number} [cacheTokens]
467
+ * @property {{ input: number, output: number }} breakdown
468
+ */
469
+
470
+ /**
471
+ * Builds a context-window budget from an Anthropic-shaped usage payload.
472
+ *
473
+ * `input_tokens + cache_read + cache_creation` is one request's whole prompt,
474
+ * which is exactly what the context window holds at that moment.
475
+ * @param {Object} messageUsage - Anthropic usage payload
476
+ * @returns {TokenBudget} Token budget object
477
+ */
478
+ function buildTokenBudget(messageUsage) {
479
+ const directInputTokens = readNumber(messageUsage.input_tokens ?? messageUsage.inputTokens);
480
+ const cacheCreationTokens = readNumber(messageUsage.cache_creation_input_tokens ?? messageUsage.cacheCreationInputTokens ?? messageUsage.cacheCreationTokens);
481
+ const cacheReadTokens = readNumber(messageUsage.cache_read_input_tokens ?? messageUsage.cacheReadInputTokens ?? messageUsage.cacheReadTokens);
482
+ const cacheTokens = cacheCreationTokens + cacheReadTokens;
483
+ const inputTokens = directInputTokens + cacheTokens;
484
+ const outputTokens = readNumber(messageUsage.output_tokens ?? messageUsage.outputTokens);
485
+ const contextWindow = parseInt(process.env.CONTEXT_WINDOW, 10) || 160000;
486
+
487
+ return {
488
+ used: inputTokens + outputTokens,
489
+ total: contextWindow,
490
+ inputTokens,
491
+ outputTokens,
492
+ cacheReadTokens,
493
+ cacheCreationTokens,
494
+ cacheTokens,
495
+ breakdown: {
496
+ input: inputTokens,
497
+ output: outputTokens,
498
+ },
499
+ };
500
+ }
501
+
502
+ /**
503
+ * Extracts the session's context-window usage from an SDK stream message.
504
+ *
505
+ * Only assistant messages describe the context window: each one reports the
506
+ * prompt its own request carried. The turn-ending `result` is deliberately not
507
+ * a source here — see `extractCumulativeTokenBudget`.
351
508
  * @param {Object} sdkMessage - SDK stream message
352
- * @returns {Object|null} Token budget object or null
509
+ * @returns {TokenBudget|null} Token budget object or null
353
510
  */
354
511
  function extractTokenBudget(sdkMessage) {
355
512
  if (!sdkMessage || typeof sdkMessage !== 'object') {
356
513
  return null;
357
514
  }
358
515
 
359
- const messageUsage = sdkMessage.message?.usage || sdkMessage.usage;
360
- if (messageUsage && typeof messageUsage === 'object') {
361
- const directInputTokens = readNumber(messageUsage.input_tokens ?? messageUsage.inputTokens);
362
- const cacheCreationTokens = readNumber(messageUsage.cache_creation_input_tokens ?? messageUsage.cacheCreationInputTokens ?? messageUsage.cacheCreationTokens);
363
- const cacheReadTokens = readNumber(messageUsage.cache_read_input_tokens ?? messageUsage.cacheReadInputTokens ?? messageUsage.cacheReadTokens);
364
- const cacheTokens = cacheCreationTokens + cacheReadTokens;
365
- const inputTokens = directInputTokens + cacheTokens;
366
- const outputTokens = readNumber(messageUsage.output_tokens ?? messageUsage.outputTokens);
367
- const totalUsed = inputTokens + outputTokens;
368
- const contextWindow = parseInt(process.env.CONTEXT_WINDOW, 10) || 160000;
516
+ // Subagent traffic (parent_tool_use_id set) reports the subagent's own
517
+ // context window, not this session's — surfacing it makes the counter drop
518
+ // to the subagent's number and bounce back on the next main-thread event.
519
+ if (sdkMessage.parent_tool_use_id) {
520
+ return null;
521
+ }
522
+
523
+ // Only assistant messages carry Anthropic-shaped usage. System
524
+ // task_progress/task_notification events have a top-level `usage` too, but
525
+ // shaped {total_tokens, tool_uses, duration_ms} — reading Anthropic keys
526
+ // off it yields an all-zero budget that flashes "0" in the composer.
527
+ if (sdkMessage.type !== 'assistant') {
528
+ return null;
529
+ }
369
530
 
370
- return {
371
- used: totalUsed,
372
- total: contextWindow,
373
- inputTokens,
374
- outputTokens,
375
- cacheReadTokens,
376
- cacheCreationTokens,
377
- cacheTokens,
378
- breakdown: {
379
- input: inputTokens,
380
- output: outputTokens,
381
- },
382
- };
531
+ const messageUsage = sdkMessage.message?.usage;
532
+ if (!messageUsage || typeof messageUsage !== 'object') {
533
+ return null;
534
+ }
535
+
536
+ return buildTokenBudget(messageUsage);
537
+ }
538
+
539
+ /**
540
+ * Last-resort budget read from a turn's `result` message.
541
+ *
542
+ * `result.usage` and `result.modelUsage` are the turn's *bill*: every request
543
+ * the turn made, summed, including each subagent's. A turn that made four
544
+ * requests therefore reports roughly four times the context the conversation
545
+ * actually holds, so publishing it made the counter leap at the end of a turn
546
+ * and fall back on the next assistant message — worst with subagents running,
547
+ * whose requests inflate the sum without ever entering this session's context.
548
+ *
549
+ * It is still the only usage an SDK build that reports none per assistant
550
+ * message ever emits, so it stays available for the caller to use when a turn
551
+ * produced no assistant budget at all.
552
+ * @param {Object} sdkMessage - SDK stream message
553
+ * @returns {TokenBudget|null} Token budget object or null
554
+ */
555
+ function extractCumulativeTokenBudget(sdkMessage) {
556
+ if (!sdkMessage || typeof sdkMessage !== 'object' || sdkMessage.type !== 'result') {
557
+ return null;
558
+ }
559
+
560
+ if (sdkMessage.usage && typeof sdkMessage.usage === 'object') {
561
+ return buildTokenBudget(sdkMessage.usage);
383
562
  }
384
563
 
385
564
  if (!sdkMessage.modelUsage || typeof sdkMessage.modelUsage !== 'object') {
@@ -411,9 +590,12 @@ function extractTokenBudget(sdkMessage) {
411
590
  };
412
591
  }
413
592
 
414
- // Tool calls that leave work running past the end of a turn. Bash only counts
415
- // when it is explicitly backgrounded; the rest defer or watch work by nature.
416
- const DEFERRED_WORK_TOOLS = new Set(['Monitor', 'ScheduleWakeup', 'CronCreate', 'TaskCreate']);
593
+ // Tool calls that leave work running past the end of a turn. Bash and Agent only
594
+ // count when they are backgrounded; the rest defer or watch work by nature.
595
+ // Workflow belongs here rather than in a branch of its own: its input schema has
596
+ // no foreground option at all, so every call returns a task id immediately and
597
+ // reports back in a later turn.
598
+ const DEFERRED_WORK_TOOLS = new Set(['Monitor', 'ScheduleWakeup', 'CronCreate', 'TaskCreate', 'Workflow']);
417
599
 
418
600
  /**
419
601
  * Detects tool calls that keep working after the turn's `result` arrives.
@@ -421,10 +603,14 @@ const DEFERRED_WORK_TOOLS = new Set(['Monitor', 'ScheduleWakeup', 'CronCreate',
421
603
  * Only turns that start background work need their CLI process held open; every
422
604
  * other turn can let it exit immediately, as it did before the hold existed.
423
605
  *
606
+ * Used by the providers module's tests, which pin the tool matching directly:
607
+ * the alternative is driving a whole SDK run to observe whether stdin was held,
608
+ * and the cost of getting this wrong is silently killed background work.
609
+ *
424
610
  * @param {Object} sdkMessage - SDK stream message
425
611
  * @returns {boolean} True when the message launches work that outlives the turn
426
612
  */
427
- function startsBackgroundWork(sdkMessage) {
613
+ export function startsBackgroundWork(sdkMessage) {
428
614
  const content = sdkMessage?.message?.content;
429
615
  if (!Array.isArray(content)) {
430
616
  return false;
@@ -437,10 +623,162 @@ function startsBackgroundWork(sdkMessage) {
437
623
  if (block.name === 'Bash') {
438
624
  return block.input?.run_in_background === true;
439
625
  }
626
+ // A backgrounded subagent outlives the turn exactly like a backgrounded
627
+ // Bash does, so the process has to be held open for it to report back.
628
+ // Agents background by default — `run_in_background` is optional and only
629
+ // an explicit `false` opts out — hence `!== false` rather than `=== true`.
630
+ // A foreground agent must stay out of DEFERRED_WORK_TOOLS: it never pushes
631
+ // a follow-up turn, so it would pin the process for the full ceiling.
632
+ if (block.name === 'Agent') {
633
+ return block.input?.run_in_background !== false;
634
+ }
440
635
  return DEFERRED_WORK_TOOLS.has(block.name);
441
636
  });
442
637
  }
443
638
 
639
+ // `task_updated` patch statuses after which a task is gone for good. `pending`,
640
+ // `running` and `paused` are still outstanding; `killed` is what the CLI
641
+ // writes when it stops a task itself (the task notification spells it
642
+ // `stopped`).
643
+ const TERMINAL_TASK_STATUSES = new Set(['completed', 'failed', 'killed']);
644
+
645
+ /**
646
+ * Tracks the background tasks each live session still has outstanding, folded
647
+ * from the `system` task events the SDK stream already carries.
648
+ *
649
+ * `startsBackgroundWork` above only knows that a turn *launched* something
650
+ * lasting; this knows what is still running and which task ids it answers to,
651
+ * which is what the running-sessions list and a stop request need once the
652
+ * turn's `result` has gone out and nothing else remembers the session is busy.
653
+ *
654
+ * Verified against a real query (SDK 0.3.165): `task_started` carries
655
+ * `task_id`, `tool_use_id`, `task_type` and `description` for every agent,
656
+ * workflow and backgrounded command (a foreground Bash emits nothing);
657
+ * `task_notification` settles a task with any status; `task_updated` carries
658
+ * only `task_id` and a patch, and is terminal when the patch's `status` is.
659
+ * Housekeeping tasks the CLI starts on its own have no `tool_use_id` and are
660
+ * not tracked — nothing in the transcript could show them.
661
+ *
662
+ * Exported so the folding can be driven with the four event shapes directly;
663
+ * the runtime keeps one instance keyed like `activeSessions`.
664
+ *
665
+ * @returns {{
666
+ * apply: (sessionKey: string, message: Object) => void,
667
+ * hasOutstanding: (sessionKey: string) => boolean,
668
+ * has: (sessionKey: string, taskId: string) => boolean,
669
+ * outstanding: (sessionKey: string) => Array<import('@/shared/types.js').BackgroundTaskSummary>,
670
+ * clear: (sessionKey: string) => void,
671
+ * list: () => Array<{ sessionId: string, tasks: Array<import('@/shared/types.js').BackgroundTaskSummary> }>
672
+ * }}
673
+ */
674
+ export function createBackgroundWorkTracker() {
675
+ /** @type {Map<string, Map<string, import('@/shared/types.js').BackgroundTaskSummary>>} */
676
+ const sessions = new Map();
677
+ /**
678
+ * Tool-use ids the session's own turns issued. A task started for a call an
679
+ * agent made inside its own transcript — a workflow agent's backgrounded
680
+ * command, say — reaches this stream too, and nothing in the parent
681
+ * transcript could show it; it is kept for stopping but flagged `nested`.
682
+ * @type {Map<string, Set<string>>}
683
+ */
684
+ const ownToolUseIds = new Map();
685
+
686
+ const remove = (sessionKey, taskId) => {
687
+ const tasks = sessions.get(sessionKey);
688
+ if (!tasks) {
689
+ return;
690
+ }
691
+ tasks.delete(taskId);
692
+ if (tasks.size === 0) {
693
+ sessions.delete(sessionKey);
694
+ }
695
+ };
696
+
697
+ return {
698
+ apply(sessionKey, message) {
699
+ if (message?.type === 'assistant' && !message.parent_tool_use_id) {
700
+ const content = message.message?.content;
701
+ if (Array.isArray(content)) {
702
+ for (const block of content) {
703
+ if (block?.type === 'tool_use' && typeof block.id === 'string') {
704
+ let ids = ownToolUseIds.get(sessionKey);
705
+ if (!ids) {
706
+ ids = new Set();
707
+ ownToolUseIds.set(sessionKey, ids);
708
+ }
709
+ ids.add(block.id);
710
+ }
711
+ }
712
+ }
713
+ return;
714
+ }
715
+ if (message?.type !== 'system' || typeof message.task_id !== 'string') {
716
+ return;
717
+ }
718
+ switch (message.subtype) {
719
+ case 'task_started': {
720
+ if (typeof message.tool_use_id !== 'string') {
721
+ return;
722
+ }
723
+ const task = {
724
+ taskId: message.task_id,
725
+ toolUseId: message.tool_use_id,
726
+ taskType: message.task_type,
727
+ description: message.description,
728
+ startedAt: Date.now()
729
+ };
730
+ if (typeof message.workflow_name === 'string') {
731
+ task.workflowName = message.workflow_name;
732
+ }
733
+ if (!ownToolUseIds.get(sessionKey)?.has(message.tool_use_id)) {
734
+ task.nested = true;
735
+ }
736
+ let tasks = sessions.get(sessionKey);
737
+ if (!tasks) {
738
+ tasks = new Map();
739
+ sessions.set(sessionKey, tasks);
740
+ }
741
+ tasks.set(task.taskId, task);
742
+ return;
743
+ }
744
+ case 'task_notification':
745
+ remove(sessionKey, message.task_id);
746
+ return;
747
+ case 'task_updated':
748
+ if (TERMINAL_TASK_STATUSES.has(message.patch?.status)) {
749
+ remove(sessionKey, message.task_id);
750
+ }
751
+ return;
752
+ default:
753
+ }
754
+ },
755
+
756
+ hasOutstanding(sessionKey) {
757
+ return sessions.has(sessionKey);
758
+ },
759
+
760
+ has(sessionKey, taskId) {
761
+ return Boolean(sessions.get(sessionKey)?.has(taskId));
762
+ },
763
+
764
+ outstanding(sessionKey) {
765
+ return Array.from(sessions.get(sessionKey)?.values() ?? []);
766
+ },
767
+
768
+ clear(sessionKey) {
769
+ sessions.delete(sessionKey);
770
+ ownToolUseIds.delete(sessionKey);
771
+ },
772
+
773
+ list() {
774
+ return Array.from(sessions, ([sessionId, tasks]) => ({
775
+ sessionId,
776
+ tasks: Array.from(tasks.values())
777
+ }));
778
+ }
779
+ };
780
+ }
781
+
444
782
  /**
445
783
  * Builds the SDK user messages for one turn.
446
784
  *
@@ -596,14 +934,23 @@ async function queryClaudeSDK(command, options = {}, ws, context) {
596
934
  // Set when a turn starts background work, cleared when the next `result`
597
935
  // arrives — only turns with work still outstanding hold their process open.
598
936
  let backgroundWorkPending = false;
937
+ // Set when the stream reports a task starting during this turn. Task events
938
+ // are the exact word on what is still running, so when the turn produced
939
+ // any, the tracker decides the hold; `startsBackgroundWork` is the fallback
940
+ // for tools that emit none (Monitor, ScheduleWakeup, CronCreate, TaskCreate)
941
+ // and for an SDK that does not report tasks at all.
942
+ let sawTaskEventThisTurn = false;
599
943
  // True while the process is being held open for background work, so a later
600
944
  // `result` can be recognised as that work reporting back.
601
945
  let heldForBackgroundWork = false;
946
+ // Set once a turn publishes a budget read from an assistant message, so the
947
+ // turn-ending `result` is only mined for usage when nothing better arrived.
948
+ let assistantBudgetSent = false;
602
949
 
603
950
  // A new turn supersedes any earlier one still holding this session's process
604
951
  // open, so held runs cannot stack up across a conversation.
605
952
  if (sessionKey()) {
606
- getSession(sessionKey())?.releaseInput?.();
953
+ releaseHeldSession(sessionKey());
607
954
  }
608
955
 
609
956
  // Arms (or re-arms) the idle countdown that eventually closes stdin.
@@ -736,6 +1083,13 @@ async function queryClaudeSDK(command, options = {}, ws, context) {
736
1083
  return { behavior: 'deny', message: 'Permission request cancelled' };
737
1084
  }
738
1085
 
1086
+ // A client answered. Announce it on the run stream so the replay buffer
1087
+ // and every other attached tab drop the prompt — resolving happens over
1088
+ // the inbound socket only, so without this a mid-run page refresh
1089
+ // replays the `permission_request` with nothing to retract it and the
1090
+ // already-answered prompt resurrects.
1091
+ ws.send(createNormalizedMessage({ kind: 'permission_resolved', requestId, sessionId: capturedSessionId || sessionId || null, provider: 'claude' }));
1092
+
739
1093
  if (decision.allow) {
740
1094
  if (decision.rememberEntry && typeof decision.rememberEntry === 'string') {
741
1095
  if (!sdkOptions.allowedTools.includes(decision.rememberEntry)) {
@@ -751,10 +1105,13 @@ async function queryClaudeSDK(command, options = {}, ws, context) {
751
1105
  return { behavior: 'deny', message: decision.message ?? 'User denied tool use' };
752
1106
  };
753
1107
 
1108
+ // The SDK's own `query`, unless the caller supplies one (tests script the
1109
+ // stream to drive the hold logic below without a CLI process).
1110
+ const createQuery = context.createQuery ?? query;
754
1111
  let heldPrompt = createHeldPromptStream(promptMessages);
755
1112
  releasePromptStream = heldPrompt.release;
756
1113
  try {
757
- queryInstance = query({
1114
+ queryInstance = createQuery({
758
1115
  prompt: heldPrompt.stream,
759
1116
  options: sdkOptions
760
1117
  });
@@ -767,7 +1124,7 @@ async function queryClaudeSDK(command, options = {}, ws, context) {
767
1124
  heldPrompt.release();
768
1125
  heldPrompt = createHeldPromptStream(promptMessages);
769
1126
  releasePromptStream = heldPrompt.release;
770
- queryInstance = query({
1127
+ queryInstance = createQuery({
771
1128
  prompt: heldPrompt.stream,
772
1129
  options: sdkOptions
773
1130
  });
@@ -812,22 +1169,53 @@ async function queryClaudeSDK(command, options = {}, ws, context) {
812
1169
  if (transformedMessage.parentToolUseId && !msg.parentToolUseId) {
813
1170
  msg.parentToolUseId = transformedMessage.parentToolUseId;
814
1171
  }
1172
+ if (isSubagentPromptEcho(msg)) {
1173
+ continue;
1174
+ }
815
1175
  ws.send(msg);
816
1176
  }
817
1177
 
818
- // Extract and send token budget updates from assistant/result usage payloads
819
- const tokenBudgetData = extractTokenBudget(message);
1178
+ // Extract and send token budget updates from assistant usage payloads,
1179
+ // falling back to the turn's cumulative bill only for SDK builds that
1180
+ // report no per-assistant usage at all.
1181
+ const tokenBudgetData = extractTokenBudget(message)
1182
+ || (assistantBudgetSent ? null : extractCumulativeTokenBudget(message));
820
1183
  if (tokenBudgetData) {
1184
+ if (message.type === 'assistant') {
1185
+ assistantBudgetSent = true;
1186
+ }
821
1187
  ws.send(createNormalizedMessage({ kind: 'status', text: 'token_budget', tokenBudget: tokenBudgetData, sessionId: capturedSessionId || sessionId || null, provider: 'claude' }));
822
1188
  }
823
1189
 
824
1190
  if (startsBackgroundWork(message)) {
825
1191
  backgroundWorkPending = true;
826
1192
  }
1193
+ if (message.type === 'system' && message.subtype === 'task_started') {
1194
+ sawTaskEventThisTurn = true;
1195
+ }
1196
+ backgroundWork.apply(sessionKey(), message);
1197
+
1198
+ // A task the user stopped gets no follow-up turn from the CLI — only its
1199
+ // `stopped` notification — so when that was the last outstanding task
1200
+ // nothing will ever push the `result` the release below waits for, and
1201
+ // the process would sit until the idle ceiling. Release it here. A
1202
+ // completed task is different: the CLI relays its result in a turn of
1203
+ // its own, which closing stdin now would cut short.
1204
+ if (
1205
+ heldForBackgroundWork
1206
+ && message.type === 'system'
1207
+ && message.subtype === 'task_notification'
1208
+ && message.status === 'stopped'
1209
+ && !backgroundWork.hasOutstanding(sessionKey())
1210
+ ) {
1211
+ heldForBackgroundWork = false;
1212
+ releasePromptStream();
1213
+ }
827
1214
 
828
1215
  if (message.type === 'result') {
829
1216
  // The turn is done as far as the client is concerned.
830
1217
  const abortPending = sessionKey() ? abortedSessionIds.has(sessionKey()) : false;
1218
+ const stillOutstanding = backgroundWork.hasOutstanding(sessionKey());
831
1219
  if (!turnCompleteSent && !abortPending) {
832
1220
  turnCompleteSent = true;
833
1221
  ws.send(createCompleteMessage({ provider: 'claude', sessionId: capturedSessionId || sessionId || null, exitCode: 0 }));
@@ -838,9 +1226,10 @@ async function queryClaudeSDK(command, options = {}, ws, context) {
838
1226
  sessionName: sessionSummary,
839
1227
  stopReason: 'completed'
840
1228
  });
841
- } else if (heldForBackgroundWork && !abortPending) {
1229
+ } else if (heldForBackgroundWork && !abortPending && !stillOutstanding) {
842
1230
  // A result after the turn already reported complete means the work we
843
- // held the process open for has finished and pushed a follow-up turn.
1231
+ // held the process open for has finished and pushed a follow-up turn
1232
+ // — the last of it, when nothing else is still running.
844
1233
  notifyBackgroundWorkCompleted({
845
1234
  userId: ws?.userId || null,
846
1235
  provider: 'claude',
@@ -848,11 +1237,26 @@ async function queryClaudeSDK(command, options = {}, ws, context) {
848
1237
  sessionName: sessionSummary
849
1238
  });
850
1239
  }
851
- if (backgroundWorkPending) {
852
- // Work started during this turn is still running. Hold the process
853
- // open so it can finish and report back in a follow-up turn; the
854
- // ceiling is only a backstop for work that never reports.
855
- backgroundWorkPending = false;
1240
+ // Work started during this turn, or work from an earlier turn that
1241
+ // has not settled yet (a follow-up turn reports one task in while
1242
+ // another is still going), is still running. Hold the process open
1243
+ // so it can finish and report back in a follow-up turn; the ceiling
1244
+ // is only a backstop for work that never reports.
1245
+ //
1246
+ // The release when the last task settles is this same branch on the
1247
+ // follow-up turn the CLI pushes for it, not the settling event
1248
+ // itself: closing stdin at that moment would cut the turn that
1249
+ // relays the task's result.
1250
+ //
1251
+ // When the turn reported its tasks, the tracker is the whole truth: an
1252
+ // Agent call without `run_in_background` is scored as background by
1253
+ // `startsBackgroundWork`, but the CLI runs it in the foreground and
1254
+ // it has settled before this `result` — holding for it kept a process
1255
+ // alive for the full ceiling with nothing outstanding.
1256
+ const holdForTurn = sawTaskEventThisTurn ? stillOutstanding : backgroundWorkPending || stillOutstanding;
1257
+ backgroundWorkPending = false;
1258
+ sawTaskEventThisTurn = false;
1259
+ if (holdForTurn) {
856
1260
  heldForBackgroundWork = true;
857
1261
  scheduleRelease();
858
1262
  } else {
@@ -992,6 +1396,36 @@ async function abortClaudeSDKSession(sessionId) {
992
1396
  }
993
1397
  }
994
1398
 
1399
+ /**
1400
+ * Sessions whose background tasks are still outstanding, with the tasks.
1401
+ *
1402
+ * A session stays here after its turn's `result` for as long as the process
1403
+ * is held open for the work — which is exactly the window in which nothing
1404
+ * else (the chat run registry marks the run completed at `result`) knows the
1405
+ * session is still busy.
1406
+ * @returns {Array<{ sessionId: string, tasks: Array<import('@/shared/types.js').BackgroundTaskSummary> }>}
1407
+ */
1408
+ function listClaudeSDKBackgroundWork() {
1409
+ return backgroundWork.list();
1410
+ }
1411
+
1412
+ /**
1413
+ * Stops one outstanding background task through the SDK, which then emits a
1414
+ * `task_notification` with status `stopped` — the same event that drops the
1415
+ * task from the tracker and settles its card.
1416
+ * @param {string} sessionId - Session identifier
1417
+ * @param {string} taskId - The task's `task_id` as reported on `task_started`
1418
+ * @returns {Promise<boolean>} False when no live process is tracking the task
1419
+ */
1420
+ async function stopClaudeSDKTask(sessionId, taskId) {
1421
+ const session = getSession(sessionId);
1422
+ if (!session || !backgroundWork.has(sessionId, taskId)) {
1423
+ return false;
1424
+ }
1425
+ await session.instance.stopTask(taskId);
1426
+ return true;
1427
+ }
1428
+
995
1429
  /**
996
1430
  * Checks if an SDK session is currently active
997
1431
  * @param {string} sessionId - Session identifier
@@ -999,7 +1433,22 @@ async function abortClaudeSDKSession(sessionId) {
999
1433
  */
1000
1434
  function isClaudeSDKSessionActive(sessionId) {
1001
1435
  const session = getSession(sessionId);
1002
- return session && session.status === 'active';
1436
+ return Boolean(session && session.status === 'active');
1437
+ }
1438
+
1439
+ /**
1440
+ * When the run behind a session started, or null when no run is up.
1441
+ *
1442
+ * The history reader uses this to tell a background agent launched by the
1443
+ * live process (still able to report back) from one launched by an earlier
1444
+ * process that has since exited (never will): a launch row older than the
1445
+ * live run cannot belong to it.
1446
+ * @param {string} sessionId - Session identifier
1447
+ * @returns {number|null} Epoch milliseconds the live run started, or null
1448
+ */
1449
+ function getClaudeSDKSessionStartTime(sessionId) {
1450
+ const session = getSession(sessionId);
1451
+ return session && session.status === 'active' ? session.startTime : null;
1003
1452
  }
1004
1453
 
1005
1454
  /**
@@ -1054,15 +1503,22 @@ export const claudeRuntime = {
1054
1503
  resolve: resolveToolApproval,
1055
1504
  listPending: getPendingApprovalsForSession,
1056
1505
  },
1506
+ listBackgroundWork: listClaudeSDKBackgroundWork,
1507
+ stopBackgroundTask: stopClaudeSDKTask,
1057
1508
  };
1058
1509
 
1059
1510
  // Export public API
1060
1511
  export {
1061
1512
  queryClaudeSDK,
1062
1513
  abortClaudeSDKSession,
1514
+ listClaudeSDKBackgroundWork,
1515
+ stopClaudeSDKTask,
1063
1516
  isClaudeSDKSessionActive,
1517
+ getClaudeSDKSessionStartTime,
1064
1518
  getActiveClaudeSDKSessions,
1065
1519
  resolveToolApproval,
1066
1520
  getPendingApprovalsForSession,
1067
- reconnectSessionWriter
1521
+ reconnectSessionWriter,
1522
+ extractTokenBudget,
1523
+ extractCumulativeTokenBudget
1068
1524
  };