@cloudcli-ai/cloudcli 1.37.0 → 1.37.2

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 (242) hide show
  1. package/dist/assets/abnfDiagram-N423BO3Z-C4UAoHJb.js +1 -0
  2. package/dist/assets/arc-B8jF-ja9.js +1 -0
  3. package/dist/assets/architectureDiagram-T3A2C74G-C_Wur_CU.js +36 -0
  4. package/dist/assets/blockDiagram-VBNYF7ZC-Cq4P1i8t.js +132 -0
  5. package/dist/assets/c4Diagram-5PPSVZJV-C1wG4eUj.js +10 -0
  6. package/dist/assets/channel-_falYbU1.js +1 -0
  7. package/dist/assets/chunk-2GRJ4B5K-B8fnBG6E.js +1 -0
  8. package/dist/assets/chunk-2Q5K7J3B-WdfPooPJ.js +1 -0
  9. package/dist/assets/chunk-5RXB4S5H-DhZJuG2E.js +231 -0
  10. package/dist/assets/chunk-5VM5RSS4-B0rjslal.js +15 -0
  11. package/dist/assets/chunk-6Q2QTUOP-CG2Oj4jL.js +88 -0
  12. package/dist/assets/chunk-GF5L2VYU-rrl2lRxY.js +206 -0
  13. package/dist/assets/chunk-JWPE2WC7-CjVbrVMA.js +1 -0
  14. package/dist/assets/chunk-KBJHAD2P-CRzKgU8S.js +1 -0
  15. package/dist/assets/chunk-RYQCIY6F-DBz4GTZF.js +1 -0
  16. package/dist/assets/chunk-XXDRQBXY-wHxalsXk.js +1 -0
  17. package/dist/assets/classDiagram-JCYQIIEL-Dn-3CfpR.js +1 -0
  18. package/dist/assets/classDiagram-v2-OCEON4UE-Dn-3CfpR.js +1 -0
  19. package/dist/assets/cose-bilkent-JH36ORCC-s1hygY8e.js +1 -0
  20. package/dist/assets/cynefin-VYW2F7L2-C4LGV15i.js +178 -0
  21. package/dist/assets/cynefinDiagram-MW4NZA55-C5uhu7B3.js +62 -0
  22. package/dist/assets/cytoscape.esm-C10L9Icf.js +331 -0
  23. package/dist/assets/dagre-VZM6K2ZE-B-OBTewm.js +4 -0
  24. package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  25. package/dist/assets/diagram-7IWD3JNH-BjGKQxih.js +30 -0
  26. package/dist/assets/diagram-B4RE2ZJO-BMKz1X2L.js +3 -0
  27. package/dist/assets/diagram-LBJQPF4R-DNh-GhKC.js +24 -0
  28. package/dist/assets/diagram-Q27KOJAE-BaTG9jPT.js +24 -0
  29. package/dist/assets/diagram-UB23O5K3-BUwo2OvJ.js +41 -0
  30. package/dist/assets/ebnfDiagram-BXEA7PRR-DAZgfzX2.js +1 -0
  31. package/dist/assets/erDiagram-JOGREHBK-1w1CcGEb.js +85 -0
  32. package/dist/assets/flowDiagram-UKHOOZJN-D4EII8Jj.js +156 -0
  33. package/dist/assets/ganttDiagram-PKOTCBZU-ChvJZAgB.js +292 -0
  34. package/dist/assets/gitGraphDiagram-DS77QQ5N-DtOn41S8.js +106 -0
  35. package/dist/assets/graph-Cx-EqisW.js +1 -0
  36. package/dist/assets/index-CzPLBvXT.css +32 -0
  37. package/dist/assets/index-DFkBBuSA.js +1549 -0
  38. package/dist/assets/infoDiagram-6WML65LV-DN_PVEJZ.js +2 -0
  39. package/dist/assets/init-Gi6I4Gst.js +1 -0
  40. package/dist/assets/ishikawaDiagram-WSZJBQD7-DbkF1kXb.js +70 -0
  41. package/dist/assets/journeyDiagram-NVQOT4AX-pJwmhddE.js +139 -0
  42. package/dist/assets/kanban-definition-27J2QSJJ-PPAPgNp5.js +89 -0
  43. package/dist/assets/layout-D6BstSzF.js +1 -0
  44. package/dist/assets/linear-iPR-J9f7.js +1 -0
  45. package/dist/assets/map-CV9uif0g.js +1 -0
  46. package/dist/assets/mermaid.core-BEUL86Z6.js +312 -0
  47. package/dist/assets/mindmap-definition-FAOFIHXS-BvZoBib7.js +96 -0
  48. package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  49. package/dist/assets/pegDiagram-VL7TDLO6-DhYreaUd.js +1 -0
  50. package/dist/assets/pieDiagram-7S7Q4E2Y-CrOHlyD0.js +39 -0
  51. package/dist/assets/quadrantDiagram-CIZ2JOQS-D4g5yH3t.js +7 -0
  52. package/dist/assets/railroadDiagram-AXF67PYL-Dj39lHlf.js +1 -0
  53. package/dist/assets/requirementDiagram-LRYGKXZP-1AS4Zgm_.js +84 -0
  54. package/dist/assets/sankeyDiagram-W5VNT64P-CKlMn19P.js +40 -0
  55. package/dist/assets/sequenceDiagram-SI44F4Z6-B_j3qyvo.js +162 -0
  56. package/dist/assets/sizeCapture-X5ZJPWSS-Bp2Op16v.js +1 -0
  57. package/dist/assets/stateDiagram-OKZ733FA-C0cN6DoY.js +1 -0
  58. package/dist/assets/stateDiagram-v2-UEYNNEHI-BvBxUUrZ.js +1 -0
  59. package/dist/assets/swimlanes-SLNWSIFB-BEvXOV9b.js +2 -0
  60. package/dist/assets/swimlanesDiagram-ULZ7WXOC-CsE-x4YK.js +8 -0
  61. package/dist/assets/timeline-definition-Z64GVDOM-zdJQinvZ.js +120 -0
  62. package/dist/assets/vennDiagram-T6HMQDX7-DuF1uZMY.js +34 -0
  63. package/dist/assets/wardleyDiagram-T6FBY63Y-B9AlLQup.js +78 -0
  64. package/dist/assets/xychartDiagram-ELKLHX3M-DTJX48Qg.js +7 -0
  65. package/dist/index.html +2 -2
  66. package/dist-server/server/index.js +1 -2
  67. package/dist-server/server/index.js.map +1 -1
  68. package/dist-server/server/modules/agent/agent.module.js +2 -1
  69. package/dist-server/server/modules/agent/agent.module.js.map +1 -1
  70. package/dist-server/server/modules/auth/auth.middleware.js +1 -1
  71. package/dist-server/server/modules/auth/auth.middleware.js.map +1 -1
  72. package/dist-server/server/modules/browser-use/browser-use-runtime.js +10 -0
  73. package/dist-server/server/modules/browser-use/browser-use-runtime.js.map +1 -0
  74. package/dist-server/server/modules/browser-use/browser-use.service.js +3 -6
  75. package/dist-server/server/modules/browser-use/browser-use.service.js.map +1 -1
  76. package/dist-server/server/modules/browser-use/index.js +2 -0
  77. package/dist-server/server/modules/browser-use/index.js.map +1 -1
  78. package/dist-server/server/modules/cli/cli.js +9 -3
  79. package/dist-server/server/modules/cli/cli.js.map +1 -1
  80. package/dist-server/server/modules/cli/cli.module.js +0 -1
  81. package/dist-server/server/modules/cli/cli.module.js.map +1 -1
  82. package/dist-server/server/modules/cli/cli.service.js +0 -18
  83. package/dist-server/server/modules/cli/cli.service.js.map +1 -1
  84. package/dist-server/server/modules/cli/tests/cli-environment-bootstrap.test.js +154 -0
  85. package/dist-server/server/modules/cli/tests/cli-environment-bootstrap.test.js.map +1 -0
  86. package/dist-server/server/modules/cli/tests/cli.service.test.js +0 -3
  87. package/dist-server/server/modules/cli/tests/cli.service.test.js.map +1 -1
  88. package/dist-server/server/modules/commands/commands.routes.js +3 -3
  89. package/dist-server/server/modules/commands/commands.routes.js.map +1 -1
  90. package/dist-server/server/modules/commands/tests/commands.test.js +2 -7
  91. package/dist-server/server/modules/commands/tests/commands.test.js.map +1 -1
  92. package/dist-server/server/modules/database/index.js +2 -0
  93. package/dist-server/server/modules/database/index.js.map +1 -1
  94. package/dist-server/server/modules/database/migrations.js +18 -1
  95. package/dist-server/server/modules/database/migrations.js.map +1 -1
  96. package/dist-server/server/modules/database/repositories/provider-models.js +112 -0
  97. package/dist-server/server/modules/database/repositories/provider-models.js.map +1 -0
  98. package/dist-server/server/modules/database/repositories/sessions.db.js +63 -7
  99. package/dist-server/server/modules/database/repositories/sessions.db.js.map +1 -1
  100. package/dist-server/server/modules/database/schema.js +28 -3
  101. package/dist-server/server/modules/database/schema.js.map +1 -1
  102. package/dist-server/server/modules/database/tests/provider-models.db.integration.test.js +102 -0
  103. package/dist-server/server/modules/database/tests/provider-models.db.integration.test.js.map +1 -0
  104. package/dist-server/server/modules/database/tests/sessions-provider-mapping.test.js +2 -1
  105. package/dist-server/server/modules/database/tests/sessions-provider-mapping.test.js.map +1 -1
  106. package/dist-server/server/modules/database/tests/sessions.db.integration.test.js +22 -0
  107. package/dist-server/server/modules/database/tests/sessions.db.integration.test.js.map +1 -1
  108. package/dist-server/server/modules/file-tree/file-tree.module.js +5 -1
  109. package/dist-server/server/modules/file-tree/file-tree.module.js.map +1 -1
  110. package/dist-server/server/modules/file-tree/file-tree.service.js +60 -20
  111. package/dist-server/server/modules/file-tree/file-tree.service.js.map +1 -1
  112. package/dist-server/server/modules/file-tree/tests/file-tree.service.test.js +143 -16
  113. package/dist-server/server/modules/file-tree/tests/file-tree.service.test.js.map +1 -1
  114. package/dist-server/server/modules/git/git-branch.service.js +15 -0
  115. package/dist-server/server/modules/git/git-branch.service.js.map +1 -0
  116. package/dist-server/server/modules/git/git.routes.js +15 -7
  117. package/dist-server/server/modules/git/git.routes.js.map +1 -1
  118. package/dist-server/server/modules/git/tests/git-branch.service.test.js +52 -0
  119. package/dist-server/server/modules/git/tests/git-branch.service.test.js.map +1 -0
  120. package/dist-server/server/modules/git/tests/git-init.routes.test.js +84 -0
  121. package/dist-server/server/modules/git/tests/git-init.routes.test.js.map +1 -1
  122. package/dist-server/server/modules/notifications/index.js +3 -1
  123. package/dist-server/server/modules/notifications/index.js.map +1 -1
  124. package/dist-server/server/modules/notifications/services/notification-orchestrator.service.js +22 -1
  125. package/dist-server/server/modules/notifications/services/notification-orchestrator.service.js.map +1 -1
  126. package/dist-server/server/modules/providers/list/claude/claude-models.provider.js +46 -14
  127. package/dist-server/server/modules/providers/list/claude/claude-models.provider.js.map +1 -1
  128. package/dist-server/server/modules/providers/list/claude/claude-runtime.provider.js +259 -60
  129. package/dist-server/server/modules/providers/list/claude/claude-runtime.provider.js.map +1 -1
  130. package/dist-server/server/modules/providers/list/codex/codex-models.provider.js +58 -71
  131. package/dist-server/server/modules/providers/list/codex/codex-models.provider.js.map +1 -1
  132. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js +1 -54
  133. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js.map +1 -1
  134. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +7 -7
  135. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -1
  136. package/dist-server/server/modules/providers/list/cursor/cursor-models.provider.js +32 -669
  137. package/dist-server/server/modules/providers/list/cursor/cursor-models.provider.js.map +1 -1
  138. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js +105 -328
  139. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js.map +1 -1
  140. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js +0 -12
  141. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js.map +1 -1
  142. package/dist-server/server/modules/providers/provider.routes.js +144 -31
  143. package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
  144. package/dist-server/server/modules/providers/services/provider-models.service.js +167 -199
  145. package/dist-server/server/modules/providers/services/provider-models.service.js.map +1 -1
  146. package/dist-server/server/modules/providers/services/provider-runtime.service.js +2 -2
  147. package/dist-server/server/modules/providers/services/provider-runtime.service.js.map +1 -1
  148. package/dist-server/server/modules/providers/services/session-conversations-search.service.js +75 -11
  149. package/dist-server/server/modules/providers/services/session-conversations-search.service.js.map +1 -1
  150. package/dist-server/server/modules/providers/services/sessions.service.js +62 -3
  151. package/dist-server/server/modules/providers/services/sessions.service.js.map +1 -1
  152. package/dist-server/server/modules/providers/tests/codex-sessions.test.js +53 -17
  153. package/dist-server/server/modules/providers/tests/codex-sessions.test.js.map +1 -1
  154. package/dist-server/server/modules/providers/tests/opencode-models.test.js +15 -126
  155. package/dist-server/server/modules/providers/tests/opencode-models.test.js.map +1 -1
  156. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js +5 -5
  157. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js.map +1 -1
  158. package/dist-server/server/modules/providers/tests/provider-models.service.test.js +175 -292
  159. package/dist-server/server/modules/providers/tests/provider-models.service.test.js.map +1 -1
  160. package/dist-server/server/modules/providers/tests/provider-runtime.service.test.js +2 -6
  161. package/dist-server/server/modules/providers/tests/provider-runtime.service.test.js.map +1 -1
  162. package/dist-server/server/modules/providers/tests/provider-token-usage.service.test.js +1 -0
  163. package/dist-server/server/modules/providers/tests/provider-token-usage.service.test.js.map +1 -1
  164. package/dist-server/server/modules/providers/tests/provider.routes.test.js +161 -0
  165. package/dist-server/server/modules/providers/tests/provider.routes.test.js.map +1 -0
  166. package/dist-server/server/modules/providers/tests/sessions.service.test.js +87 -0
  167. package/dist-server/server/modules/providers/tests/sessions.service.test.js.map +1 -0
  168. package/dist-server/server/modules/websocket/services/chat-websocket.service.js +5 -1
  169. package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
  170. package/dist-server/server/shared/utils.js +8 -0
  171. package/dist-server/server/shared/utils.js.map +1 -1
  172. package/package.json +8 -4
  173. package/scripts/promote-dist-server.mjs +50 -0
  174. package/server/index.ts +1 -2
  175. package/server/modules/agent/agent.module.ts +2 -1
  176. package/server/modules/auth/auth.middleware.ts +2 -2
  177. package/server/modules/browser-use/browser-use-runtime.ts +10 -0
  178. package/server/modules/browser-use/browser-use.service.ts +5 -8
  179. package/server/modules/browser-use/index.ts +3 -0
  180. package/server/modules/cli/cli.module.ts +0 -1
  181. package/server/modules/cli/cli.service.ts +0 -20
  182. package/server/modules/cli/cli.ts +9 -3
  183. package/server/modules/cli/tests/cli-environment-bootstrap.test.ts +186 -0
  184. package/server/modules/cli/tests/cli.service.test.ts +0 -3
  185. package/server/modules/commands/commands.routes.ts +3 -3
  186. package/server/modules/commands/tests/commands.test.ts +2 -7
  187. package/server/modules/database/index.ts +2 -0
  188. package/server/modules/database/migrations.ts +20 -0
  189. package/server/modules/database/repositories/provider-models.ts +168 -0
  190. package/server/modules/database/repositories/sessions.db.ts +85 -8
  191. package/server/modules/database/schema.ts +29 -3
  192. package/server/modules/database/tests/provider-models.db.integration.test.ts +125 -0
  193. package/server/modules/database/tests/sessions-provider-mapping.test.ts +2 -1
  194. package/server/modules/database/tests/sessions.db.integration.test.ts +32 -0
  195. package/server/modules/file-tree/file-tree.module.ts +5 -1
  196. package/server/modules/file-tree/file-tree.service.ts +81 -22
  197. package/server/modules/file-tree/tests/file-tree.service.test.ts +167 -16
  198. package/server/modules/git/git-branch.service.ts +43 -0
  199. package/server/modules/git/git.routes.ts +16 -9
  200. package/server/modules/git/tests/git-branch.service.test.ts +60 -0
  201. package/server/modules/git/tests/git-init.routes.test.ts +83 -0
  202. package/server/modules/notifications/index.ts +2 -0
  203. package/server/modules/notifications/services/notification-orchestrator.service.js +24 -1
  204. package/server/modules/providers/list/claude/claude-models.provider.ts +46 -14
  205. package/server/modules/providers/list/claude/claude-runtime.provider.js +280 -67
  206. package/server/modules/providers/list/codex/codex-models.provider.ts +58 -99
  207. package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +1 -60
  208. package/server/modules/providers/list/codex/codex-sessions.provider.ts +7 -7
  209. package/server/modules/providers/list/cursor/cursor-models.provider.ts +32 -710
  210. package/server/modules/providers/list/opencode/opencode-models.provider.ts +105 -413
  211. package/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.ts +0 -13
  212. package/server/modules/providers/provider.routes.ts +190 -33
  213. package/server/modules/providers/services/provider-models.service.ts +223 -288
  214. package/server/modules/providers/services/provider-runtime.service.ts +2 -3
  215. package/server/modules/providers/services/session-conversations-search.service.ts +106 -11
  216. package/server/modules/providers/services/sessions.service.ts +87 -3
  217. package/server/modules/providers/tests/codex-sessions.test.ts +57 -18
  218. package/server/modules/providers/tests/opencode-models.test.ts +34 -135
  219. package/server/modules/providers/tests/opencode-sessions.test.ts +5 -5
  220. package/server/modules/providers/tests/provider-models.service.test.ts +353 -450
  221. package/server/modules/providers/tests/provider-runtime.service.test.ts +2 -6
  222. package/server/modules/providers/tests/provider-token-usage.service.test.ts +1 -0
  223. package/server/modules/providers/tests/provider.routes.test.ts +244 -0
  224. package/server/modules/providers/tests/sessions.service.test.ts +127 -0
  225. package/server/modules/websocket/services/chat-websocket.service.ts +5 -1
  226. package/server/shared/interfaces.ts +4 -6
  227. package/server/shared/types.ts +29 -18
  228. package/server/shared/utils.ts +10 -0
  229. package/server/tsconfig.json +6 -2
  230. package/README.de.md +0 -257
  231. package/README.ja.md +0 -249
  232. package/README.ko.md +0 -249
  233. package/README.md +0 -269
  234. package/README.ru.md +0 -257
  235. package/README.tr.md +0 -258
  236. package/README.zh-CN.md +0 -249
  237. package/README.zh-TW.md +0 -249
  238. package/dist/assets/index-DSlds633.css +0 -32
  239. package/dist/assets/index-eGqPeIEu.js +0 -1531
  240. package/dist-server/server/constants/config.js +0 -6
  241. package/dist-server/server/constants/config.js.map +0 -1
  242. package/server/constants/config.ts +0 -5
@@ -9,12 +9,12 @@ import type {
9
9
  } from '@/shared/types.js';
10
10
  import { buildDefaultProviderCurrentActiveModel } from '@/shared/utils.js';
11
11
 
12
- export const CLAUDE_FALLBACK_MODELS: ProviderModelsDefinition = {
12
+ export const CLAUDE_PREDEFINED_MODELS: ProviderModelsDefinition = {
13
13
  OPTIONS: [
14
14
  {
15
15
  value: 'default',
16
16
  label: 'Default (recommended)',
17
- description: 'Use the Claude Code default model (currently Sonnet 4.6)',
17
+ description: 'Use the recommended model for your Claude account and deployment.',
18
18
  effort: {
19
19
  default: 'high',
20
20
  values: [
@@ -25,10 +25,25 @@ export const CLAUDE_FALLBACK_MODELS: ProviderModelsDefinition = {
25
25
  ],
26
26
  },
27
27
  },
28
+ {
29
+ value: 'best',
30
+ label: 'Best available',
31
+ description: 'Use Fable 5 when available, otherwise the latest Opus model.',
32
+ effort: {
33
+ default: 'high',
34
+ values: [
35
+ { value: 'low' },
36
+ { value: 'medium' },
37
+ { value: 'high' },
38
+ { value: 'xhigh' },
39
+ { value: 'max' },
40
+ ],
41
+ },
42
+ },
28
43
  {
29
44
  value: 'fable',
30
- label: 'Fable',
31
- description: 'Fable 5 · Most capable for your hardest and longest-running tasks · Uses your limits ~2× faster than Opus',
45
+ label: 'Fable 5',
46
+ description: 'Most capable Claude model for the hardest, longest-running tasks.',
32
47
  effort: {
33
48
  default: 'high',
34
49
  values: [
@@ -41,15 +56,16 @@ export const CLAUDE_FALLBACK_MODELS: ProviderModelsDefinition = {
41
56
  },
42
57
  },
43
58
  {
44
- value: "sonnet",
45
- label: "Sonnet",
46
- description: "Sonnet 4.6 · Best for everyday tasks · $3/$15 per Mtok",
59
+ value: 'sonnet',
60
+ label: 'Sonnet',
61
+ description: 'Latest Sonnet model for everyday coding tasks.',
47
62
  effort: {
48
63
  default: 'high',
49
64
  values: [
50
65
  { value: 'low' },
51
66
  { value: 'medium' },
52
67
  { value: 'high' },
68
+ { value: 'xhigh' },
53
69
  { value: 'max' },
54
70
  ],
55
71
  },
@@ -57,13 +73,14 @@ export const CLAUDE_FALLBACK_MODELS: ProviderModelsDefinition = {
57
73
  {
58
74
  value: 'sonnet[1m]',
59
75
  label: 'Sonnet (1M context)',
60
- description: 'Sonnet 4.6 for long sessions · $3/$15 per Mtok',
76
+ description: 'Latest Sonnet model with a 1M context window.',
61
77
  effort: {
62
78
  default: 'high',
63
79
  values: [
64
80
  { value: 'low' },
65
81
  { value: 'medium' },
66
82
  { value: 'high' },
83
+ { value: 'xhigh' },
67
84
  { value: 'max' },
68
85
  ],
69
86
  },
@@ -71,7 +88,7 @@ export const CLAUDE_FALLBACK_MODELS: ProviderModelsDefinition = {
71
88
  {
72
89
  value: 'opus',
73
90
  label: 'Opus',
74
- description: 'Opus 4.8 · Best for everyday, complex tasks · ~2× usage vs Sonnet',
91
+ description: 'Latest Opus model for complex reasoning and coding tasks.',
75
92
  effort: {
76
93
  default: 'high',
77
94
  values: [
@@ -85,8 +102,8 @@ export const CLAUDE_FALLBACK_MODELS: ProviderModelsDefinition = {
85
102
  },
86
103
  {
87
104
  value: 'opus[1m]',
88
- label: 'Opus 4.8 (1M context)',
89
- description: 'Opus 4.8 with 1M context · Most capable for complex work · $5/$25 per Mtok',
105
+ label: 'Opus (1M context)',
106
+ description: 'Latest Opus model with a 1M context window.',
90
107
  effort: {
91
108
  default: 'high',
92
109
  values: [
@@ -101,7 +118,22 @@ export const CLAUDE_FALLBACK_MODELS: ProviderModelsDefinition = {
101
118
  {
102
119
  value: 'haiku',
103
120
  label: 'Haiku',
104
- description: 'Haiku 4.5 · Fastest for quick answers · $1/$5 per Mtok',
121
+ description: 'Fast and efficient Claude model for simple tasks.',
122
+ },
123
+ {
124
+ value: 'opusplan',
125
+ label: 'Opus Plan',
126
+ description: 'Use Opus while planning, then switch to Sonnet for execution.',
127
+ effort: {
128
+ default: 'high',
129
+ values: [
130
+ { value: 'low' },
131
+ { value: 'medium' },
132
+ { value: 'high' },
133
+ { value: 'xhigh' },
134
+ { value: 'max' },
135
+ ],
136
+ },
105
137
  },
106
138
  ],
107
139
  DEFAULT: 'default',
@@ -113,7 +145,7 @@ export const findClaudeModelOption = (model: string | undefined | null): Provide
113
145
  return null;
114
146
  }
115
147
 
116
- return CLAUDE_FALLBACK_MODELS.OPTIONS.find((option) => option.value === normalizedModel) ?? null;
148
+ return CLAUDE_PREDEFINED_MODELS.OPTIONS.find((option) => option.value === normalizedModel) ?? null;
117
149
  };
118
150
  type ClaudeInitEvent = {
119
151
  sessionId?: string;
@@ -237,7 +269,7 @@ export class ClaudeProviderModels implements IProviderModels {
237
269
  // const supportedModels = await queryInstance.supportedModels();
238
270
  // queryInstance.close();
239
271
  // return buildClaudeModelsDefinition(supportedModels);
240
- return CLAUDE_FALLBACK_MODELS;
272
+ return CLAUDE_PREDEFINED_MODELS;
241
273
  }
242
274
 
243
275
  async getCurrentActiveModel(sessionId?: string): Promise<ProviderCurrentActiveModel> {
@@ -24,10 +24,11 @@ import {
24
24
  buildClaudeUserContent,
25
25
  normalizeImageDescriptors
26
26
  } from '@/shared/image-attachments.js';
27
- import { CLAUDE_FALLBACK_MODELS } from '@/modules/providers/list/claude/claude-models.provider.js';
27
+ import { CLAUDE_PREDEFINED_MODELS } from '@/modules/providers/list/claude/claude-models.provider.js';
28
28
  import { resolveClaudeCodeExecutablePath } from '@/shared/claude-cli-path.js';
29
29
  import {
30
30
  createNotificationEvent,
31
+ notifyBackgroundWorkCompleted,
31
32
  notifyRunFailed,
32
33
  notifyRunStopped,
33
34
  notifyUserIfEnabled
@@ -40,12 +41,35 @@ const pendingToolApprovals = new Map();
40
41
  // terminal `complete` (aborted: true) to the client, so the run loop must not
41
42
  // emit a second one when its generator winds down.
42
43
  const abortedSessionIds = new Set();
44
+ // Query instances interrupted because a newer run took over their session id
45
+ // (see addSession). Their run loops must stay silent on wind-down: the map
46
+ // entry, the abort flag, and all client-facing events belong to the new run.
47
+ const supersededInstances = new WeakSet();
43
48
 
44
49
  const TOOL_APPROVAL_TIMEOUT_MS = parseInt(process.env.CLAUDE_TOOL_APPROVAL_TIMEOUT_MS, 10) || 55000;
45
50
 
51
+ // How long background work is allowed to keep running after a turn ends. This drives
52
+ // two halves of the same behaviour:
53
+ //
54
+ // 1. Passed to the spawned CLI as CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS, which is how
55
+ // long it waits for still-running background *agents* before killing them.
56
+ // 2. A backstop on how long we hold the SDK's stdin open after a turn's `result`.
57
+ // The SDK closes stdin as soon as a turn ends, and the CLI reads that EOF as
58
+ // "print wind-down" — killing background *shells* after a short grace period,
59
+ // which the ceiling above does not cover. Holding stdin open also lets the CLI
60
+ // push follow-up turns (background-task completions, Monitor notifications,
61
+ // scheduled wake-ups).
62
+ //
63
+ // The hold normally ends long before this: a turn with nothing outstanding closes
64
+ // stdin immediately, background work releases it as soon as it reports back, and a
65
+ // new turn supersedes the previous hold. This ceiling only catches background work
66
+ // that never reports at all, so an abandoned session cannot leak a CLI process
67
+ // forever. The timer resets on every message, so it measures silence, not total time.
68
+ const BG_WAIT_CEILING_MS = 30 * 60 * 1000;
69
+
46
70
  const TOOLS_REQUIRING_INTERACTION = new Set(['AskUserQuestion', 'ExitPlanMode']);
47
71
 
48
- function resolveClaudeEffort(model, effort, modelsDefinition = CLAUDE_FALLBACK_MODELS) {
72
+ function resolveClaudeEffort(model, effort, modelsDefinition = CLAUDE_PREDEFINED_MODELS) {
49
73
  const selectedModel = modelsDefinition?.OPTIONS?.find((option) => option.value === model) || null;
50
74
  const allowedEfforts = selectedModel?.effort?.values
51
75
  ?.map((value) => value.value) || [];
@@ -165,7 +189,7 @@ function mapCliOptionsToSDK(options = {}) {
165
189
 
166
190
  // Forward all host env vars (e.g. ANTHROPIC_BASE_URL) to the subprocess.
167
191
  // Since SDK 0.2.113, options.env replaces process.env instead of overlaying it.
168
- sdkOptions.env = { ...process.env };
192
+ sdkOptions.env = { ...process.env, CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS: String(BG_WAIT_CEILING_MS) };
169
193
 
170
194
  // Resolve the executable eagerly on Windows because the SDK uses raw child_process.spawn,
171
195
  // which does not reliably follow npm's shell wrappers like cross-spawn does.
@@ -209,12 +233,12 @@ function mapCliOptionsToSDK(options = {}) {
209
233
 
210
234
  sdkOptions.disallowedTools = settings.disallowedTools || [];
211
235
 
212
- sdkOptions.model = options.model || CLAUDE_FALLBACK_MODELS.DEFAULT;
236
+ sdkOptions.model = options.model || CLAUDE_PREDEFINED_MODELS.DEFAULT;
213
237
 
214
238
  const resolvedEffort = resolveClaudeEffort(
215
239
  sdkOptions.model,
216
240
  effort,
217
- options.effortModels || CLAUDE_FALLBACK_MODELS,
241
+ options.effortModels || CLAUDE_PREDEFINED_MODELS,
218
242
  );
219
243
  if (resolvedEffort) {
220
244
  sdkOptions.effort = resolvedEffort;
@@ -240,13 +264,37 @@ function mapCliOptionsToSDK(options = {}) {
240
264
  * @param {string} sessionId - Session identifier
241
265
  * @param {Object} queryInstance - SDK query instance
242
266
  * @param {Object} writer - WebSocket writer for reconnect support
267
+ * @param {Function} releaseInput - Closes the held stdin stream so the CLI can exit
243
268
  */
244
- function addSession(sessionId, queryInstance, writer = null) {
269
+ function addSession(sessionId, queryInstance, writer = null, releaseInput = null) {
270
+ const existing = activeSessions.get(sessionId);
271
+ // A different live instance under the same key means an earlier run was
272
+ // superseded without being stopped (e.g. an abort that raced run setup and
273
+ // found nothing to interrupt). Overwriting it here would strand its
274
+ // generator forever — this map entry is the only handle for interrupting
275
+ // it. Stop it directly rather than via abortClaudeSDKSession, whose
276
+ // session-keyed abortedSessionIds flag would be consumed by the new run
277
+ // and suppress its terminal `complete`.
278
+ const superseding = Boolean(
279
+ existing && existing.status === 'active' && existing.instance && existing.instance !== queryInstance
280
+ );
281
+ if (superseding) {
282
+ supersededInstances.add(existing.instance);
283
+ Promise.resolve()
284
+ .then(() => existing.instance.interrupt())
285
+ .catch((error) => {
286
+ console.error(`Error interrupting superseded run for session ${sessionId}:`, error?.message || error);
287
+ });
288
+ existing.releaseInput?.();
289
+ }
290
+ const carried = superseding ? null : existing;
245
291
  activeSessions.set(sessionId, {
246
292
  instance: queryInstance,
247
- startTime: Date.now(),
293
+ startTime: carried?.startTime || Date.now(),
248
294
  status: 'active',
249
- writer
295
+ writer,
296
+ // Re-registered mid-run once the provider session id lands; keep the closer.
297
+ releaseInput: releaseInput || carried?.releaseInput || null
250
298
  });
251
299
  }
252
300
 
@@ -363,38 +411,93 @@ function extractTokenBudget(sdkMessage) {
363
411
  };
364
412
  }
365
413
 
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']);
417
+
366
418
  /**
367
- * Builds the SDK `prompt` payload for one turn.
419
+ * Detects tool calls that keep working after the turn's `result` arrives.
368
420
  *
369
- * Plain text turns pass the string through unchanged. Turns with image
370
- * attachments use the SDK's streaming-input mode: a single SDKUserMessage
371
- * whose content carries the prompt text plus one base64 `image` block per
372
- * attachment (read from the global `~/.cloudcli/assets` folder).
421
+ * Only turns that start background work need their CLI process held open; every
422
+ * other turn can let it exit immediately, as it did before the hold existed.
423
+ *
424
+ * @param {Object} sdkMessage - SDK stream message
425
+ * @returns {boolean} True when the message launches work that outlives the turn
426
+ */
427
+ function startsBackgroundWork(sdkMessage) {
428
+ const content = sdkMessage?.message?.content;
429
+ if (!Array.isArray(content)) {
430
+ return false;
431
+ }
432
+
433
+ return content.some((block) => {
434
+ if (block?.type !== 'tool_use') {
435
+ return false;
436
+ }
437
+ if (block.name === 'Bash') {
438
+ return block.input?.run_in_background === true;
439
+ }
440
+ return DEFERRED_WORK_TOOLS.has(block.name);
441
+ });
442
+ }
443
+
444
+ /**
445
+ * Builds the SDK user messages for one turn.
446
+ *
447
+ * Always returns SDKUserMessage records rather than a bare string: a string
448
+ * prompt makes the SDK flag the query as single-turn and close stdin the moment
449
+ * the turn's `result` arrives, which kills the CLI's background tasks. Plain
450
+ * text turns carry string content; turns with image attachments carry the
451
+ * prompt text plus one base64 `image` block per attachment (read from the
452
+ * global `~/.cloudcli/assets` folder).
373
453
  *
374
454
  * @param {string} command - User prompt
375
455
  * @param {Array} images - Image descriptors ({ path, name?, mimeType? })
376
456
  * @param {Array} files - Non-image attachment descriptors
377
457
  * @param {string} cwd - Project working directory attachment paths resolve against
378
- * @returns {Promise<string|AsyncIterable>} SDK prompt payload
458
+ * @returns {Promise<Array<Object>>} SDKUserMessage records for the turn
379
459
  */
380
- async function buildPromptPayload(command, images, files, cwd) {
460
+ async function buildPromptMessages(command, images, files, cwd) {
381
461
  const promptWithFiles = appendFilesInputTag(command, files);
382
- if (normalizeImageDescriptors(images).length === 0) {
383
- return promptWithFiles;
384
- }
462
+ const content = normalizeImageDescriptors(images).length === 0
463
+ ? promptWithFiles
464
+ : await buildClaudeUserContent(promptWithFiles, images, cwd);
465
+
466
+ return [{
467
+ type: 'user',
468
+ message: {
469
+ role: 'user',
470
+ content
471
+ },
472
+ parent_tool_use_id: null,
473
+ timestamp: new Date().toISOString()
474
+ }];
475
+ }
385
476
 
386
- const content = await buildClaudeUserContent(promptWithFiles, images, cwd);
387
- return (async function* () {
388
- yield {
389
- type: 'user',
390
- message: {
391
- role: 'user',
392
- content
393
- },
394
- parent_tool_use_id: null,
395
- timestamp: new Date().toISOString()
396
- };
477
+ /**
478
+ * Wraps prompt messages in an async iterable that yields them and then parks.
479
+ *
480
+ * The SDK closes the CLI's stdin as soon as its input iterable is exhausted (and
481
+ * immediately on `result` for string prompts). The CLI reads that EOF as the end
482
+ * of the run and kills anything still going in the background, so the iterable
483
+ * has to stay pending until we actually want the process gone.
484
+ *
485
+ * @param {Array<Object>} messages - SDKUserMessage records to send
486
+ * @returns {{ stream: AsyncIterable, release: () => void }} Stream plus its closer
487
+ */
488
+ function createHeldPromptStream(messages) {
489
+ let release;
490
+ const held = new Promise((resolve) => { release = resolve; });
491
+
492
+ const stream = (async function* () {
493
+ for (const message of messages) {
494
+ yield message;
495
+ }
496
+ // Keeps stdin open — the CLI stays alive until release() is called.
497
+ await held;
397
498
  })();
499
+
500
+ return { stream, release };
398
501
  }
399
502
 
400
503
  /**
@@ -483,9 +586,47 @@ async function queryClaudeSDK(command, options = {}, ws, context) {
483
586
  });
484
587
  };
485
588
 
589
+ // Closes the held stdin stream so the CLI can wind down. Replaced once the
590
+ // stream exists; the finally block calls it no matter how the run ends.
591
+ let releasePromptStream = () => {};
592
+ let idleReleaseTimer = null;
593
+ // The client is told the turn is over as soon as `result` lands, even though
594
+ // the process lingers, so the UI never waits out the idle hold.
595
+ let turnCompleteSent = false;
596
+ // Set when a turn starts background work, cleared when the next `result`
597
+ // arrives — only turns with work still outstanding hold their process open.
598
+ let backgroundWorkPending = false;
599
+ // True while the process is being held open for background work, so a later
600
+ // `result` can be recognised as that work reporting back.
601
+ let heldForBackgroundWork = false;
602
+
603
+ // A new turn supersedes any earlier one still holding this session's process
604
+ // open, so held runs cannot stack up across a conversation.
605
+ if (sessionKey()) {
606
+ getSession(sessionKey())?.releaseInput?.();
607
+ }
608
+
609
+ // Arms (or re-arms) the idle countdown that eventually closes stdin.
610
+ const scheduleRelease = () => {
611
+ if (idleReleaseTimer) {
612
+ clearTimeout(idleReleaseTimer);
613
+ idleReleaseTimer = null;
614
+ }
615
+ idleReleaseTimer = setTimeout(() => {
616
+ idleReleaseTimer = null;
617
+ releasePromptStream();
618
+ }, BG_WAIT_CEILING_MS);
619
+ // Never let the hold keep the server process alive on its own.
620
+ idleReleaseTimer.unref?.();
621
+ };
622
+
623
+ // Hoisted above the try so the catch's cleanup can tell whether this run
624
+ // still owns the activeSessions entry (or was superseded by a newer run).
625
+ let queryInstance = null;
626
+
486
627
  try {
487
628
  const resolvedModel = await context.resolveResumeModel(sessionId, options.model);
488
- let effortModels = CLAUDE_FALLBACK_MODELS;
629
+ let effortModels = CLAUDE_PREDEFINED_MODELS;
489
630
  try {
490
631
  effortModels = await context.getProviderModels();
491
632
  } catch (error) {
@@ -504,10 +645,10 @@ async function queryClaudeSDK(command, options = {}, ws, context) {
504
645
  sdkOptions.mcpServers = mcpServers;
505
646
  }
506
647
 
507
- // Turns with image attachments switch to streaming input so the images
508
- // ride along as real content blocks. Built per query attempt because an
509
- // async generator cannot be replayed once consumed.
510
- const createPrompt = () => buildPromptPayload(command, options.images, options.files, options.cwd);
648
+ // Every turn uses streaming input so stdin stays open past the turn's
649
+ // `result`. The message list is reusable, but each query attempt needs its
650
+ // own stream because an async generator cannot be replayed once consumed.
651
+ const promptMessages = await buildPromptMessages(command, options.images, options.files, options.cwd);
511
652
 
512
653
  sdkOptions.hooks = {
513
654
  Notification: [{
@@ -610,14 +751,11 @@ async function queryClaudeSDK(command, options = {}, ws, context) {
610
751
  return { behavior: 'deny', message: decision.message ?? 'User denied tool use' };
611
752
  };
612
753
 
613
- // Query constructor reads this synchronously.
614
- const prevStreamTimeout = process.env.CLAUDE_CODE_STREAM_CLOSE_TIMEOUT;
615
- process.env.CLAUDE_CODE_STREAM_CLOSE_TIMEOUT = '300000';
616
-
617
- let queryInstance;
754
+ let heldPrompt = createHeldPromptStream(promptMessages);
755
+ releasePromptStream = heldPrompt.release;
618
756
  try {
619
757
  queryInstance = query({
620
- prompt: await createPrompt(),
758
+ prompt: heldPrompt.stream,
621
759
  options: sdkOptions
622
760
  });
623
761
  } catch (hookError) {
@@ -625,22 +763,19 @@ async function queryClaudeSDK(command, options = {}, ws, context) {
625
763
  // Keep notification behavior operational via runtime events even if hook registration fails.
626
764
  console.warn('Failed to initialize Claude query with hooks, retrying without hooks:', hookError?.message || hookError);
627
765
  delete sdkOptions.hooks;
766
+ // Discard the abandoned stream and build a fresh one for the retry.
767
+ heldPrompt.release();
768
+ heldPrompt = createHeldPromptStream(promptMessages);
769
+ releasePromptStream = heldPrompt.release;
628
770
  queryInstance = query({
629
- prompt: await createPrompt(),
771
+ prompt: heldPrompt.stream,
630
772
  options: sdkOptions
631
773
  });
632
774
  }
633
775
 
634
- // Restore immediately — Query constructor already captured the value
635
- if (prevStreamTimeout !== undefined) {
636
- process.env.CLAUDE_CODE_STREAM_CLOSE_TIMEOUT = prevStreamTimeout;
637
- } else {
638
- delete process.env.CLAUDE_CODE_STREAM_CLOSE_TIMEOUT;
639
- }
640
-
641
776
  // Track the query instance for abort capability
642
777
  if (sessionKey()) {
643
- addSession(sessionKey(), queryInstance, ws);
778
+ addSession(sessionKey(), queryInstance, ws, releasePromptStream);
644
779
  }
645
780
 
646
781
  // Process streaming messages
@@ -650,7 +785,7 @@ async function queryClaudeSDK(command, options = {}, ws, context) {
650
785
  if (message.session_id && !capturedSessionId) {
651
786
 
652
787
  capturedSessionId = message.session_id;
653
- addSession(sessionKey(), queryInstance, ws);
788
+ addSession(sessionKey(), queryInstance, ws, releasePromptStream);
654
789
 
655
790
  // Set session ID on writer
656
791
  if (ws.setSessionId && typeof ws.setSessionId === 'function') {
@@ -685,36 +820,98 @@ async function queryClaudeSDK(command, options = {}, ws, context) {
685
820
  if (tokenBudgetData) {
686
821
  ws.send(createNormalizedMessage({ kind: 'status', text: 'token_budget', tokenBudget: tokenBudgetData, sessionId: capturedSessionId || sessionId || null, provider: 'claude' }));
687
822
  }
823
+
824
+ if (startsBackgroundWork(message)) {
825
+ backgroundWorkPending = true;
826
+ }
827
+
828
+ if (message.type === 'result') {
829
+ // The turn is done as far as the client is concerned.
830
+ const abortPending = sessionKey() ? abortedSessionIds.has(sessionKey()) : false;
831
+ if (!turnCompleteSent && !abortPending) {
832
+ turnCompleteSent = true;
833
+ ws.send(createCompleteMessage({ provider: 'claude', sessionId: capturedSessionId || sessionId || null, exitCode: 0 }));
834
+ notifyRunStopped({
835
+ userId: ws?.userId || null,
836
+ provider: 'claude',
837
+ sessionId: sessionId || capturedSessionId || null,
838
+ sessionName: sessionSummary,
839
+ stopReason: 'completed'
840
+ });
841
+ } else if (heldForBackgroundWork && !abortPending) {
842
+ // 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.
844
+ notifyBackgroundWorkCompleted({
845
+ userId: ws?.userId || null,
846
+ provider: 'claude',
847
+ sessionId: sessionId || capturedSessionId || null,
848
+ sessionName: sessionSummary
849
+ });
850
+ }
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;
856
+ heldForBackgroundWork = true;
857
+ scheduleRelease();
858
+ } else {
859
+ // Either nothing was backgrounded, or the background work just
860
+ // reported in — let the CLI exit now, as it always has.
861
+ heldForBackgroundWork = false;
862
+ releasePromptStream();
863
+ }
864
+ } else if (idleReleaseTimer) {
865
+ // Background activity after the turn — push the countdown back out.
866
+ scheduleRelease();
867
+ }
688
868
  }
689
869
 
690
- // Clean up session on completion
691
- if (sessionKey()) {
870
+ // Clean up session on completion — only while this run still owns the map
871
+ // entry. A superseding run may have replaced it, and deleting here would
872
+ // strand that run.
873
+ if (sessionKey() && getSession(sessionKey())?.instance === queryInstance) {
692
874
  removeSession(sessionKey());
693
875
  }
694
876
 
877
+ // A superseded run winds down silently: the map entry, the abort flag,
878
+ // and all client-facing events belong to the run that replaced it.
879
+ const superseded = supersededInstances.has(queryInstance);
880
+
695
881
  // Send the terminal completion event — skipped for aborted runs, whose
696
- // terminal `complete` (aborted: true) was already sent by abort-session.
697
- const wasAborted = sessionKey() ? abortedSessionIds.delete(sessionKey()) : false;
698
- if (!wasAborted) {
699
- ws.send(createCompleteMessage({ provider: 'claude', sessionId: capturedSessionId || sessionId || null, exitCode: 0 }));
882
+ // terminal `complete` (aborted: true) was already sent by abort-session, and
883
+ // for runs that already reported completion when their `result` arrived.
884
+ const wasAborted = !superseded && sessionKey() ? abortedSessionIds.delete(sessionKey()) : false;
885
+ if (!turnCompleteSent && !superseded) {
886
+ turnCompleteSent = true;
887
+ if (!wasAborted) {
888
+ ws.send(createCompleteMessage({ provider: 'claude', sessionId: capturedSessionId || sessionId || null, exitCode: 0 }));
889
+ }
890
+ notifyRunStopped({
891
+ userId: ws?.userId || null,
892
+ provider: 'claude',
893
+ sessionId: sessionId || capturedSessionId || null,
894
+ sessionName: sessionSummary,
895
+ stopReason: wasAborted ? 'aborted' : 'completed'
896
+ });
700
897
  }
701
- notifyRunStopped({
702
- userId: ws?.userId || null,
703
- provider: 'claude',
704
- sessionId: sessionId || capturedSessionId || null,
705
- sessionName: sessionSummary,
706
- stopReason: wasAborted ? 'aborted' : 'completed'
707
- });
708
898
  // Complete
709
899
 
710
900
  } catch (error) {
711
901
  console.error('SDK query error:', error);
712
902
 
713
- // Clean up session on error
714
- if (sessionKey()) {
903
+ // Clean up session on error — only while this run still owns the map entry
904
+ // (a superseding run may have replaced it).
905
+ if (sessionKey() && getSession(sessionKey())?.instance === queryInstance) {
715
906
  removeSession(sessionKey());
716
907
  }
717
908
 
909
+ if (supersededInstances.has(queryInstance)) {
910
+ // Interrupted because a newer run took over this session id; that run
911
+ // owns the abort flag and all further client-facing events.
912
+ return;
913
+ }
914
+
718
915
  const wasAborted = sessionKey() ? abortedSessionIds.delete(sessionKey()) : false;
719
916
  if (wasAborted) {
720
917
  // The abort already produced the terminal complete; a generator throw
@@ -728,9 +925,13 @@ async function queryClaudeSDK(command, options = {}, ws, context) {
728
925
  ? 'Claude Code is not installed. Please install it first: https://docs.anthropic.com/en/docs/claude-code'
729
926
  : error.message;
730
927
 
731
- // Send error to WebSocket, then the terminal complete
928
+ // Send error to WebSocket, then the terminal complete. A run that already
929
+ // reported completion and then failed during its post-turn hold still
930
+ // surfaces the error, but must not emit a second terminal complete.
732
931
  ws.send(createNormalizedMessage({ kind: 'error', content: errorContent, sessionId: capturedSessionId || sessionId || null, provider: 'claude' }));
733
- ws.send(createCompleteMessage({ provider: 'claude', sessionId: capturedSessionId || sessionId || null, exitCode: 1 }));
932
+ if (!turnCompleteSent) {
933
+ ws.send(createCompleteMessage({ provider: 'claude', sessionId: capturedSessionId || sessionId || null, exitCode: 1 }));
934
+ }
734
935
  notifyRunFailed({
735
936
  userId: ws?.userId || null,
736
937
  provider: 'claude',
@@ -738,6 +939,14 @@ async function queryClaudeSDK(command, options = {}, ws, context) {
738
939
  sessionName: sessionSummary,
739
940
  error
740
941
  });
942
+ } finally {
943
+ // Always close stdin — otherwise an aborted or failed run leaves the CLI
944
+ // process (and its MCP servers) alive until the server exits.
945
+ if (idleReleaseTimer) {
946
+ clearTimeout(idleReleaseTimer);
947
+ idleReleaseTimer = null;
948
+ }
949
+ releasePromptStream();
741
950
  }
742
951
  }
743
952
 
@@ -764,6 +973,10 @@ async function abortClaudeSDKSession(sessionId) {
764
973
  // Call interrupt() on the query instance
765
974
  await session.instance.interrupt();
766
975
 
976
+ // Release the held stdin stream; without this the CLI stays up for the rest
977
+ // of the post-turn hold even though the user cancelled.
978
+ session.releaseInput?.();
979
+
767
980
  // Update session status
768
981
  session.status = 'aborted';
769
982