@cloudcli-ai/cloudcli 1.37.3 → 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 (295) hide show
  1. package/README.md +1 -1
  2. package/dist/assets/{abnfDiagram-N423BO3Z-CJntskwA.js → abnfDiagram-N423BO3Z-Dmkwx38H.js} +1 -1
  3. package/dist/assets/{arc-CIfH4uMN.js → arc-DLe9AYuO.js} +1 -1
  4. package/dist/assets/{architectureDiagram-T3A2C74G-_IpEkukg.js → architectureDiagram-T3A2C74G-CBbbbNUW.js} +1 -1
  5. package/dist/assets/{blockDiagram-VBNYF7ZC-BttxrpgZ.js → blockDiagram-VBNYF7ZC-CaMEaWu4.js} +1 -1
  6. package/dist/assets/{c4Diagram-5PPSVZJV-g51dSRr1.js → c4Diagram-5PPSVZJV-DhjMgNnR.js} +1 -1
  7. package/dist/assets/channel-CWrmwsir.js +1 -0
  8. package/dist/assets/{chunk-2GRJ4B5K-D_-LbQUa.js → chunk-2GRJ4B5K-yjl3-F-i.js} +1 -1
  9. package/dist/assets/{chunk-2Q5K7J3B-BtlHzwaO.js → chunk-2Q5K7J3B-DsAYaWEP.js} +1 -1
  10. package/dist/assets/{chunk-5RXB4S5H-UzkpvpAT.js → chunk-5RXB4S5H-Bt5qmqOX.js} +1 -1
  11. package/dist/assets/{chunk-5VM5RSS4-BKwx5Gag.js → chunk-5VM5RSS4-yWrWVvUt.js} +1 -1
  12. package/dist/assets/{chunk-6Q2QTUOP-D_KEBH5f.js → chunk-6Q2QTUOP-Dt66K5xb.js} +1 -1
  13. package/dist/assets/{chunk-GF5L2VYU-rK2DGyto.js → chunk-GF5L2VYU-DA0vzaj4.js} +1 -1
  14. package/dist/assets/{chunk-JWPE2WC7-CeBl_N9i.js → chunk-JWPE2WC7-CUkbx96V.js} +1 -1
  15. package/dist/assets/{chunk-KBJHAD2P-BiQLWVIr.js → chunk-KBJHAD2P-DWKxx1WH.js} +1 -1
  16. package/dist/assets/{chunk-RYQCIY6F-C1knVGV5.js → chunk-RYQCIY6F-BqluplN0.js} +1 -1
  17. package/dist/assets/{chunk-XXDRQBXY-CtKbJkvF.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-DjX8y40Z.js → cose-bilkent-JH36ORCC-DHV93-Em.js} +1 -1
  21. package/dist/assets/{cynefin-VYW2F7L2-DM6YeRba.js → cynefin-VYW2F7L2-B9HZgoGX.js} +1 -1
  22. package/dist/assets/{cynefinDiagram-MW4NZA55-wZJ-vy5B.js → cynefinDiagram-MW4NZA55-hFjjEQg8.js} +1 -1
  23. package/dist/assets/{dagre-VZM6K2ZE-BzP0ejkH.js → dagre-VZM6K2ZE-CpeVOC7V.js} +1 -1
  24. package/dist/assets/{diagram-7IWD3JNH-BTL_j23J.js → diagram-7IWD3JNH-D0H1UoRA.js} +1 -1
  25. package/dist/assets/{diagram-B4RE2ZJO-Cf1Norgu.js → diagram-B4RE2ZJO-6Y7BQUup.js} +1 -1
  26. package/dist/assets/{diagram-LBJQPF4R-DPiUEKr-.js → diagram-LBJQPF4R-C0nNOwht.js} +1 -1
  27. package/dist/assets/{diagram-Q27KOJAE-CTcTpiz5.js → diagram-Q27KOJAE-En6QlOVX.js} +1 -1
  28. package/dist/assets/{diagram-UB23O5K3-DPMSdYm0.js → diagram-UB23O5K3-CmPiNeGD.js} +1 -1
  29. package/dist/assets/{ebnfDiagram-BXEA7PRR-7yvD6uiF.js → ebnfDiagram-BXEA7PRR-Bygzp1Qt.js} +1 -1
  30. package/dist/assets/{erDiagram-JOGREHBK-CfgeDoV6.js → erDiagram-JOGREHBK-CwScRfzl.js} +1 -1
  31. package/dist/assets/{flowDiagram-UKHOOZJN-B27ng5zu.js → flowDiagram-UKHOOZJN-bKX-0xP6.js} +1 -1
  32. package/dist/assets/{ganttDiagram-PKOTCBZU-CcC6b_7t.js → ganttDiagram-PKOTCBZU-DtZPKXGd.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-DS77QQ5N-BX5kPGrO.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-CUP-SlN4.js → infoDiagram-6WML65LV-ciRQTS0h.js} +1 -1
  37. package/dist/assets/{ishikawaDiagram-WSZJBQD7-BTTDhOpX.js → ishikawaDiagram-WSZJBQD7-CgjhQZ84.js} +1 -1
  38. package/dist/assets/{journeyDiagram-NVQOT4AX-ARXcc61m.js → journeyDiagram-NVQOT4AX-BsbQSeIW.js} +1 -1
  39. package/dist/assets/{kanban-definition-27J2QSJJ-BieMpqMA.js → kanban-definition-27J2QSJJ-CrwzY9Jw.js} +1 -1
  40. package/dist/assets/{linear-61vxjRnP.js → linear-BUaCDr6l.js} +1 -1
  41. package/dist/assets/{mermaid.core-nhnwkfQx.js → mermaid.core-ClgdUU0j.js} +5 -5
  42. package/dist/assets/{mindmap-definition-FAOFIHXS-Cm6x5vT1.js → mindmap-definition-FAOFIHXS-BictljY1.js} +1 -1
  43. package/dist/assets/{pegDiagram-VL7TDLO6-CcYBq_9H.js → pegDiagram-VL7TDLO6-BXdByy_U.js} +1 -1
  44. package/dist/assets/{pieDiagram-7S7Q4E2Y-CfvIjfzo.js → pieDiagram-7S7Q4E2Y-uZsMfkRX.js} +1 -1
  45. package/dist/assets/{quadrantDiagram-CIZ2JOQS-SgYoWPNb.js → quadrantDiagram-CIZ2JOQS-7lCv4QIj.js} +1 -1
  46. package/dist/assets/{railroadDiagram-AXF67PYL-D2Mu5E_f.js → railroadDiagram-AXF67PYL-07dxIWbi.js} +1 -1
  47. package/dist/assets/{requirementDiagram-LRYGKXZP-Azwe_k4Q.js → requirementDiagram-LRYGKXZP-Cb31Yv1_.js} +1 -1
  48. package/dist/assets/{sankeyDiagram-W5VNT64P-DN1Ju3PA.js → sankeyDiagram-W5VNT64P-X0YPSyF9.js} +1 -1
  49. package/dist/assets/{sequenceDiagram-SI44F4Z6-Bj4qC34S.js → sequenceDiagram-SI44F4Z6-BdBxSIlP.js} +1 -1
  50. package/dist/assets/{sizeCapture-X5ZJPWSS-Co4PdklG.js → sizeCapture-X5ZJPWSS-DQM5xAyD.js} +1 -1
  51. package/dist/assets/{stateDiagram-OKZ733FA-nOvL9dRC.js → stateDiagram-OKZ733FA-DJ3xkLUF.js} +1 -1
  52. package/dist/assets/{stateDiagram-v2-UEYNNEHI-DNv8ppw7.js → stateDiagram-v2-UEYNNEHI-vAaE4zNo.js} +1 -1
  53. package/dist/assets/{swimlanes-SLNWSIFB-CCK3wMid.js → swimlanes-SLNWSIFB-rjfYdW0w.js} +2 -2
  54. package/dist/assets/swimlanesDiagram-ULZ7WXOC-TV2IF4Nq.js +8 -0
  55. package/dist/assets/{timeline-definition-Z64GVDOM-BC29RNFm.js → timeline-definition-Z64GVDOM-DZMra5do.js} +1 -1
  56. package/dist/assets/{vennDiagram-T6HMQDX7-pZlZCtEo.js → vennDiagram-T6HMQDX7-BevD8uu0.js} +1 -1
  57. package/dist/assets/{wardleyDiagram-T6FBY63Y-De8k2-zM.js → wardleyDiagram-T6FBY63Y-DsIyXB7z.js} +1 -1
  58. package/dist/assets/{xychartDiagram-ELKLHX3M-Bk8ykH0w.js → xychartDiagram-ELKLHX3M-G3PlF3ko.js} +1 -1
  59. package/dist/index.html +2 -2
  60. package/dist-server/server/index.js +4 -1
  61. package/dist-server/server/index.js.map +1 -1
  62. package/dist-server/server/modules/agent/agent.module.js +9 -2
  63. package/dist-server/server/modules/agent/agent.module.js.map +1 -1
  64. package/dist-server/server/modules/agent/agent.routes.js +175 -128
  65. package/dist-server/server/modules/agent/agent.routes.js.map +1 -1
  66. package/dist-server/server/modules/agent/tests/agent-run-registration.test.js +306 -0
  67. package/dist-server/server/modules/agent/tests/agent-run-registration.test.js.map +1 -0
  68. package/dist-server/server/modules/agent/tests/agent.routes.test.js +18 -1
  69. package/dist-server/server/modules/agent/tests/agent.routes.test.js.map +1 -1
  70. package/dist-server/server/modules/database/migrations.js +13 -0
  71. package/dist-server/server/modules/database/migrations.js.map +1 -1
  72. package/dist-server/server/modules/database/repositories/provider-models.js +60 -6
  73. package/dist-server/server/modules/database/repositories/provider-models.js.map +1 -1
  74. package/dist-server/server/modules/database/repositories/sessions.db.js +7 -0
  75. package/dist-server/server/modules/database/repositories/sessions.db.js.map +1 -1
  76. package/dist-server/server/modules/database/schema.js +7 -0
  77. package/dist-server/server/modules/database/schema.js.map +1 -1
  78. package/dist-server/server/modules/database/tests/provider-models.db.integration.test.js +99 -0
  79. package/dist-server/server/modules/database/tests/provider-models.db.integration.test.js.map +1 -1
  80. package/dist-server/server/modules/file-tree/file-tree.module.js +2 -1
  81. package/dist-server/server/modules/file-tree/file-tree.module.js.map +1 -1
  82. package/dist-server/server/modules/file-tree/file-tree.service.js +44 -7
  83. package/dist-server/server/modules/file-tree/file-tree.service.js.map +1 -1
  84. package/dist-server/server/modules/file-tree/tests/file-tree.service.test.js +126 -1
  85. package/dist-server/server/modules/file-tree/tests/file-tree.service.test.js.map +1 -1
  86. package/dist-server/server/modules/git/git-branch-diff.service.js +142 -0
  87. package/dist-server/server/modules/git/git-branch-diff.service.js.map +1 -0
  88. package/dist-server/server/modules/git/git-branch.service.js.map +1 -1
  89. package/dist-server/server/modules/git/git.routes.js +92 -5
  90. package/dist-server/server/modules/git/git.routes.js.map +1 -1
  91. package/dist-server/server/modules/git/tests/git-branch-diff.service.test.js +160 -0
  92. package/dist-server/server/modules/git/tests/git-branch-diff.service.test.js.map +1 -0
  93. package/dist-server/server/modules/projects/project-clone.routes.js +108 -0
  94. package/dist-server/server/modules/projects/project-clone.routes.js.map +1 -0
  95. package/dist-server/server/modules/projects/projects.routes.js +13 -66
  96. package/dist-server/server/modules/projects/projects.routes.js.map +1 -1
  97. package/dist-server/server/modules/projects/services/project-clone-request.service.js +36 -0
  98. package/dist-server/server/modules/projects/services/project-clone-request.service.js.map +1 -0
  99. package/dist-server/server/modules/projects/services/project-clone.service.js +86 -32
  100. package/dist-server/server/modules/projects/services/project-clone.service.js.map +1 -1
  101. package/dist-server/server/modules/projects/tests/project-clone.routes.test.js +191 -0
  102. package/dist-server/server/modules/projects/tests/project-clone.routes.test.js.map +1 -0
  103. package/dist-server/server/modules/projects/tests/project-clone.service.test.js +275 -0
  104. package/dist-server/server/modules/projects/tests/project-clone.service.test.js.map +1 -1
  105. package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js +42 -8
  106. package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js.map +1 -1
  107. package/dist-server/server/modules/providers/list/claude/claude-models.provider.js +6 -16
  108. package/dist-server/server/modules/providers/list/claude/claude-models.provider.js.map +1 -1
  109. package/dist-server/server/modules/providers/list/claude/claude-runtime.provider.js +305 -16
  110. package/dist-server/server/modules/providers/list/claude/claude-runtime.provider.js.map +1 -1
  111. package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js +38 -11
  112. package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js.map +1 -1
  113. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js +686 -77
  114. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js.map +1 -1
  115. package/dist-server/server/modules/providers/list/codex/codex-models.provider.js +31 -0
  116. package/dist-server/server/modules/providers/list/codex/codex-models.provider.js.map +1 -1
  117. package/dist-server/server/modules/providers/list/codex/codex-runtime.provider.js +11 -4
  118. package/dist-server/server/modules/providers/list/codex/codex-runtime.provider.js.map +1 -1
  119. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +57 -0
  120. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -1
  121. package/dist-server/server/modules/providers/list/cursor/cursor-auth.provider.js +9 -2
  122. package/dist-server/server/modules/providers/list/cursor/cursor-auth.provider.js.map +1 -1
  123. package/dist-server/server/modules/providers/list/cursor/cursor-runtime.provider.js +8 -2
  124. package/dist-server/server/modules/providers/list/cursor/cursor-runtime.provider.js.map +1 -1
  125. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js +16 -3
  126. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js.map +1 -1
  127. package/dist-server/server/modules/providers/list/opencode/opencode-runtime.provider.js +5 -2
  128. package/dist-server/server/modules/providers/list/opencode/opencode-runtime.provider.js.map +1 -1
  129. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js +19 -2
  130. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js.map +1 -1
  131. package/dist-server/server/modules/providers/provider.routes.js +110 -2
  132. package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
  133. package/dist-server/server/modules/providers/services/claude-cli-liveness.service.js +100 -0
  134. package/dist-server/server/modules/providers/services/claude-cli-liveness.service.js.map +1 -0
  135. package/dist-server/server/modules/providers/services/claude-settings-permissions.service.js +97 -0
  136. package/dist-server/server/modules/providers/services/claude-settings-permissions.service.js.map +1 -0
  137. package/dist-server/server/modules/providers/services/provider-capabilities.service.js +9 -1
  138. package/dist-server/server/modules/providers/services/provider-capabilities.service.js.map +1 -1
  139. package/dist-server/server/modules/providers/services/provider-models.service.js +59 -5
  140. package/dist-server/server/modules/providers/services/provider-models.service.js.map +1 -1
  141. package/dist-server/server/modules/providers/services/provider-runtime.service.js +8 -0
  142. package/dist-server/server/modules/providers/services/provider-runtime.service.js.map +1 -1
  143. package/dist-server/server/modules/providers/services/session-conversations-search.service.js +2 -4
  144. package/dist-server/server/modules/providers/services/session-conversations-search.service.js.map +1 -1
  145. package/dist-server/server/modules/providers/services/session-history-cache.service.js +4 -0
  146. package/dist-server/server/modules/providers/services/session-history-cache.service.js.map +1 -1
  147. package/dist-server/server/modules/providers/services/sessions.service.js +73 -3
  148. package/dist-server/server/modules/providers/services/sessions.service.js.map +1 -1
  149. package/dist-server/server/modules/providers/tests/claude-auth.test.js +131 -0
  150. package/dist-server/server/modules/providers/tests/claude-auth.test.js.map +1 -1
  151. package/dist-server/server/modules/providers/tests/claude-background-work.test.js +172 -0
  152. package/dist-server/server/modules/providers/tests/claude-background-work.test.js.map +1 -0
  153. package/dist-server/server/modules/providers/tests/claude-compaction.test.js +84 -0
  154. package/dist-server/server/modules/providers/tests/claude-compaction.test.js.map +1 -0
  155. package/dist-server/server/modules/providers/tests/claude-message-model.test.js +123 -0
  156. package/dist-server/server/modules/providers/tests/claude-message-model.test.js.map +1 -0
  157. package/dist-server/server/modules/providers/tests/claude-runtime-hold.test.js +265 -0
  158. package/dist-server/server/modules/providers/tests/claude-runtime-hold.test.js.map +1 -0
  159. package/dist-server/server/modules/providers/tests/claude-sessions.test.js +1368 -12
  160. package/dist-server/server/modules/providers/tests/claude-sessions.test.js.map +1 -1
  161. package/dist-server/server/modules/providers/tests/claude-settings-permissions.test.js +228 -0
  162. package/dist-server/server/modules/providers/tests/claude-settings-permissions.test.js.map +1 -0
  163. package/dist-server/server/modules/providers/tests/codex-models.test.js +25 -0
  164. package/dist-server/server/modules/providers/tests/codex-models.test.js.map +1 -0
  165. package/dist-server/server/modules/providers/tests/codex-runtime.test.js +32 -0
  166. package/dist-server/server/modules/providers/tests/codex-runtime.test.js.map +1 -1
  167. package/dist-server/server/modules/providers/tests/codex-sessions.test.js +50 -0
  168. package/dist-server/server/modules/providers/tests/codex-sessions.test.js.map +1 -1
  169. package/dist-server/server/modules/providers/tests/cursor-auth.test.js +167 -0
  170. package/dist-server/server/modules/providers/tests/cursor-auth.test.js.map +1 -0
  171. package/dist-server/server/modules/providers/tests/opencode-models.test.js +79 -0
  172. package/dist-server/server/modules/providers/tests/opencode-models.test.js.map +1 -1
  173. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js +19 -1
  174. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js.map +1 -1
  175. package/dist-server/server/modules/providers/tests/provider-capabilities.service.test.js +30 -0
  176. package/dist-server/server/modules/providers/tests/provider-capabilities.service.test.js.map +1 -0
  177. package/dist-server/server/modules/providers/tests/provider-models.service.test.js +79 -1
  178. package/dist-server/server/modules/providers/tests/provider-models.service.test.js.map +1 -1
  179. package/dist-server/server/modules/providers/tests/provider.routes.test.js +190 -1
  180. package/dist-server/server/modules/providers/tests/provider.routes.test.js.map +1 -1
  181. package/dist-server/server/modules/providers/tests/session-history-cache.service.test.js +19 -0
  182. package/dist-server/server/modules/providers/tests/session-history-cache.service.test.js.map +1 -1
  183. package/dist-server/server/modules/providers/tests/sessions.service.test.js +175 -1
  184. package/dist-server/server/modules/providers/tests/sessions.service.test.js.map +1 -1
  185. package/dist-server/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.js +13 -1
  186. package/dist-server/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.js.map +1 -1
  187. package/dist-server/server/modules/scheduled-messages/tests/scheduled-messages.test.js +36 -1
  188. package/dist-server/server/modules/scheduled-messages/tests/scheduled-messages.test.js.map +1 -1
  189. package/dist-server/server/modules/websocket/services/chat-run-registry.service.js +27 -5
  190. package/dist-server/server/modules/websocket/services/chat-run-registry.service.js.map +1 -1
  191. package/dist-server/server/modules/websocket/services/chat-websocket.service.js +38 -1
  192. package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
  193. package/dist-server/server/modules/websocket/services/shell-websocket.service.js +230 -20
  194. package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -1
  195. package/dist-server/server/modules/websocket/tests/chat-background-subscribe.test.js +137 -0
  196. package/dist-server/server/modules/websocket/tests/chat-background-subscribe.test.js.map +1 -0
  197. package/dist-server/server/modules/websocket/tests/chat-stop-task.test.js +96 -0
  198. package/dist-server/server/modules/websocket/tests/chat-stop-task.test.js.map +1 -0
  199. package/dist-server/server/modules/websocket/tests/shell-websocket.service.test.js +420 -0
  200. package/dist-server/server/modules/websocket/tests/shell-websocket.service.test.js.map +1 -1
  201. package/dist-server/server/shared/message-unification.js +1 -1
  202. package/dist-server/server/shared/message-unification.js.map +1 -1
  203. package/dist-server/server/shared/tests/read-only-roots.test.js +124 -0
  204. package/dist-server/server/shared/tests/read-only-roots.test.js.map +1 -0
  205. package/dist-server/server/shared/tests/strip-ansi-sequences.test.js +31 -0
  206. package/dist-server/server/shared/tests/strip-ansi-sequences.test.js.map +1 -0
  207. package/dist-server/server/shared/utils.js +126 -0
  208. package/dist-server/server/shared/utils.js.map +1 -1
  209. package/electron/desktopWindow.js +6 -0
  210. package/electron/viewHost.js +25 -2
  211. package/package.json +4 -4
  212. package/server/index.ts +5 -1
  213. package/server/modules/agent/agent.module.ts +11 -1
  214. package/server/modules/agent/agent.routes.ts +198 -140
  215. package/server/modules/agent/tests/agent-run-registration.test.ts +348 -0
  216. package/server/modules/agent/tests/agent.routes.test.ts +18 -1
  217. package/server/modules/database/migrations.ts +14 -0
  218. package/server/modules/database/repositories/provider-models.ts +79 -6
  219. package/server/modules/database/repositories/sessions.db.ts +8 -0
  220. package/server/modules/database/schema.ts +7 -0
  221. package/server/modules/database/tests/provider-models.db.integration.test.ts +113 -0
  222. package/server/modules/file-tree/file-tree.module.ts +2 -1
  223. package/server/modules/file-tree/file-tree.service.ts +49 -7
  224. package/server/modules/file-tree/tests/file-tree.service.test.ts +163 -1
  225. package/server/modules/git/git-branch-diff.service.ts +204 -0
  226. package/server/modules/git/git-branch.service.ts +2 -12
  227. package/server/modules/git/git.routes.ts +100 -6
  228. package/server/modules/git/tests/git-branch-diff.service.test.ts +210 -0
  229. package/server/modules/projects/project-clone.routes.ts +142 -0
  230. package/server/modules/projects/projects.routes.ts +16 -81
  231. package/server/modules/projects/services/project-clone-request.service.ts +56 -0
  232. package/server/modules/projects/services/project-clone.service.ts +102 -34
  233. package/server/modules/projects/tests/project-clone.routes.test.ts +246 -0
  234. package/server/modules/projects/tests/project-clone.service.test.ts +376 -0
  235. package/server/modules/providers/list/claude/claude-auth.provider.ts +60 -10
  236. package/server/modules/providers/list/claude/claude-models.provider.ts +10 -22
  237. package/server/modules/providers/list/claude/claude-runtime.provider.js +322 -15
  238. package/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts +40 -14
  239. package/server/modules/providers/list/claude/claude-sessions.provider.ts +796 -88
  240. package/server/modules/providers/list/codex/codex-models.provider.ts +31 -0
  241. package/server/modules/providers/list/codex/codex-runtime.provider.ts +12 -4
  242. package/server/modules/providers/list/codex/codex-sessions.provider.ts +63 -0
  243. package/server/modules/providers/list/cursor/cursor-auth.provider.ts +10 -2
  244. package/server/modules/providers/list/cursor/cursor-runtime.provider.js +9 -2
  245. package/server/modules/providers/list/opencode/opencode-models.provider.ts +18 -3
  246. package/server/modules/providers/list/opencode/opencode-runtime.provider.js +5 -2
  247. package/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.ts +30 -2
  248. package/server/modules/providers/provider.routes.ts +131 -2
  249. package/server/modules/providers/services/claude-cli-liveness.service.ts +121 -0
  250. package/server/modules/providers/services/claude-settings-permissions.service.ts +127 -0
  251. package/server/modules/providers/services/provider-capabilities.service.ts +17 -1
  252. package/server/modules/providers/services/provider-models.service.ts +71 -4
  253. package/server/modules/providers/services/provider-runtime.service.ts +11 -0
  254. package/server/modules/providers/services/session-conversations-search.service.ts +2 -5
  255. package/server/modules/providers/services/session-history-cache.service.ts +13 -2
  256. package/server/modules/providers/services/sessions.service.ts +108 -8
  257. package/server/modules/providers/tests/claude-auth.test.ts +150 -0
  258. package/server/modules/providers/tests/claude-background-work.test.ts +213 -0
  259. package/server/modules/providers/tests/claude-compaction.test.ts +101 -0
  260. package/server/modules/providers/tests/claude-message-model.test.ts +160 -0
  261. package/server/modules/providers/tests/claude-runtime-hold.test.ts +309 -0
  262. package/server/modules/providers/tests/claude-sessions.test.ts +1630 -12
  263. package/server/modules/providers/tests/claude-settings-permissions.test.ts +271 -0
  264. package/server/modules/providers/tests/codex-models.test.ts +37 -0
  265. package/server/modules/providers/tests/codex-runtime.test.ts +36 -0
  266. package/server/modules/providers/tests/codex-sessions.test.ts +59 -0
  267. package/server/modules/providers/tests/cursor-auth.test.ts +187 -0
  268. package/server/modules/providers/tests/opencode-models.test.ts +100 -0
  269. package/server/modules/providers/tests/opencode-sessions.test.ts +40 -1
  270. package/server/modules/providers/tests/provider-capabilities.service.test.ts +37 -0
  271. package/server/modules/providers/tests/provider-models.service.test.ts +99 -1
  272. package/server/modules/providers/tests/provider.routes.test.ts +230 -1
  273. package/server/modules/providers/tests/session-history-cache.service.test.ts +22 -0
  274. package/server/modules/providers/tests/sessions.service.test.ts +237 -1
  275. package/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.ts +14 -1
  276. package/server/modules/scheduled-messages/tests/scheduled-messages.test.ts +45 -1
  277. package/server/modules/websocket/README.md +4 -2
  278. package/server/modules/websocket/services/chat-run-registry.service.ts +29 -5
  279. package/server/modules/websocket/services/chat-websocket.service.ts +58 -1
  280. package/server/modules/websocket/services/shell-websocket.service.ts +269 -22
  281. package/server/modules/websocket/tests/chat-background-subscribe.test.ts +167 -0
  282. package/server/modules/websocket/tests/chat-stop-task.test.ts +127 -0
  283. package/server/modules/websocket/tests/shell-websocket.service.test.ts +521 -0
  284. package/server/shared/interfaces.ts +29 -0
  285. package/server/shared/message-unification.ts +1 -1
  286. package/server/shared/tests/read-only-roots.test.ts +139 -0
  287. package/server/shared/tests/strip-ansi-sequences.test.ts +42 -0
  288. package/server/shared/types.ts +261 -7
  289. package/server/shared/utils.ts +136 -0
  290. package/dist/assets/channel-C5xU9jMw.js +0 -1
  291. package/dist/assets/classDiagram-JCYQIIEL-CRrL3vp7.js +0 -1
  292. package/dist/assets/classDiagram-v2-OCEON4UE-CRrL3vp7.js +0 -1
  293. package/dist/assets/index-BTDHQ6Id.css +0 -32
  294. package/dist/assets/index-sFfz8iBR.js +0 -4931
  295. package/dist/assets/swimlanesDiagram-ULZ7WXOC-DnfbNQEp.js +0 -8
@@ -4,6 +4,9 @@ import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import test from 'node:test';
6
6
 
7
+ import Database from 'better-sqlite3';
8
+
9
+ import { closeConnection, initializeDatabase } from '@/modules/database/index.js';
7
10
  import {
8
11
  OpenCodeProviderModels,
9
12
  OPENCODE_PREDEFINED_MODELS,
@@ -197,3 +200,100 @@ test('OpenCode offers only models the install can route to', async () => {
197
200
  },
198
201
  );
199
202
  });
203
+
204
+ const writeOpenCodeSessionDatabase = async (homeDir: string, rows: Array<Record<string, unknown>>) => {
205
+ const dbPath = path.join(homeDir, '.local', 'share', 'opencode', 'opencode.db');
206
+ await mkdir(path.dirname(dbPath), { recursive: true });
207
+ const db = new Database(dbPath);
208
+ try {
209
+ db.exec(`
210
+ CREATE TABLE session (
211
+ id text PRIMARY KEY,
212
+ model text,
213
+ agent text,
214
+ directory text,
215
+ time_created integer NOT NULL,
216
+ time_updated integer NOT NULL
217
+ );
218
+ `);
219
+ const insert = db.prepare(
220
+ 'INSERT INTO session (id, model, agent, directory, time_created, time_updated) VALUES (?, ?, ?, ?, ?, ?)',
221
+ );
222
+ for (const [index, row] of rows.entries()) {
223
+ insert.run(
224
+ row.id,
225
+ row.model,
226
+ row.agent ?? null,
227
+ row.directory ?? '/tmp/project',
228
+ row.timeCreated ?? 1700000000000,
229
+ row.timeUpdated ?? 1700000001000 + index,
230
+ );
231
+ }
232
+ } finally {
233
+ db.close();
234
+ }
235
+ };
236
+
237
+ test('OpenCode composes the provider prefix into session model ids', async () => {
238
+ // The lookup translates the app session id through the sessions database,
239
+ // so point it at a throwaway file rather than the developer's real one.
240
+ const tempSessionsDirectory = await mkdtemp(path.join(os.tmpdir(), 'opencode-session-model-'));
241
+ const previousDatabasePath = process.env.DATABASE_PATH;
242
+ try {
243
+ closeConnection();
244
+ process.env.DATABASE_PATH = path.join(tempSessionsDirectory, 'auth.db');
245
+ await initializeDatabase();
246
+ // OpenCode stores the provider and model id in separate fields of the
247
+ // session row ({"id":"z-ai/glm-5.3-flash","providerID":"openrouter"}), but
248
+ // the CLI's `--model` flag only resolves the composed
249
+ // `<providerID>/<modelID>` form. Resuming such a session without the prefix
250
+ // fails with an unknown-model server error, which hits OpenRouter users
251
+ // hardest because their model ids carry a slash themselves.
252
+ await withOpenCodeHome(
253
+ (homeDir) => writeOpenCodeSessionDatabase(homeDir, [
254
+ {
255
+ id: 'ses_openrouter',
256
+ model: JSON.stringify({
257
+ id: 'z-ai/glm-5.3-flash',
258
+ providerID: 'openrouter',
259
+ variant: 'default',
260
+ }),
261
+ },
262
+ {
263
+ id: 'ses_qualified',
264
+ // An id that already carries the provider prefix must survive as-is.
265
+ model: JSON.stringify({
266
+ id: 'openrouter/z-ai/glm-5.3-flash',
267
+ providerID: 'openrouter',
268
+ }),
269
+ },
270
+ {
271
+ id: 'ses_plain_string',
272
+ model: 'opencode/gpt-5.6-terra',
273
+ },
274
+ ]),
275
+ async (adapter) => {
276
+ assert.equal(
277
+ (await adapter.getCurrentActiveModel('ses_openrouter')).model,
278
+ 'openrouter/z-ai/glm-5.3-flash',
279
+ );
280
+ assert.equal(
281
+ (await adapter.getCurrentActiveModel('ses_qualified')).model,
282
+ 'openrouter/z-ai/glm-5.3-flash',
283
+ );
284
+ assert.equal(
285
+ (await adapter.getCurrentActiveModel('ses_plain_string')).model,
286
+ 'opencode/gpt-5.6-terra',
287
+ );
288
+ },
289
+ );
290
+ } finally {
291
+ closeConnection();
292
+ if (previousDatabasePath === undefined) {
293
+ delete process.env.DATABASE_PATH;
294
+ } else {
295
+ process.env.DATABASE_PATH = previousDatabasePath;
296
+ }
297
+ await rm(tempSessionsDirectory, { recursive: true, force: true });
298
+ }
299
+ });
@@ -151,6 +151,30 @@ const createOpenCodeDatabase = async (homeDir: string, workspacePath: string): P
151
151
  3,
152
152
  2,
153
153
  );
154
+ db.prepare(`
155
+ INSERT INTO session (
156
+ id, project_id, parent_id, slug, directory, title, version, time_created, time_updated,
157
+ time_archived, tokens_input, tokens_output, tokens_reasoning, tokens_cache_read,
158
+ tokens_cache_write
159
+ )
160
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
161
+ `).run(
162
+ 'open-child-session-1',
163
+ 'project-1',
164
+ 'open-session-1',
165
+ 'open-child-session-1',
166
+ workspacePath,
167
+ 'OpenCode child session',
168
+ '0.0.0',
169
+ 1_700_000_002_000,
170
+ 1_700_000_005_000,
171
+ null,
172
+ 5,
173
+ 10,
174
+ 2,
175
+ 1,
176
+ 1,
177
+ );
154
178
 
155
179
  const userMessageData = JSON.stringify({
156
180
  role: 'user',
@@ -255,16 +279,29 @@ test('OpenCode session synchronizer indexes sqlite sessions without deletable tr
255
279
  try {
256
280
  await createOpenCodeDatabase(tempRoot, workspacePath);
257
281
  await withIsolatedDatabase(() => {
282
+ sessionsDb.createAppSession('app-child-session-1', 'opencode', workspacePath);
283
+ sessionsDb.assignProviderSessionId('app-child-session-1', 'open-child-session-1');
284
+
258
285
  const synchronizer = new OpenCodeSessionSynchronizer();
259
286
  const processed = synchronizer.synchronize();
260
287
 
261
- return Promise.resolve(processed).then((count) => {
288
+ return Promise.resolve(processed).then(async (count) => {
262
289
  assert.equal(count, 1);
263
290
  const indexed = sessionsDb.getSessionById('open-session-1');
264
291
  assert.equal(indexed?.provider, 'opencode');
265
292
  assert.equal(indexed?.project_path, workspacePath);
266
293
  assert.equal(indexed?.custom_name, 'OpenCode indexed title');
267
294
  assert.equal(indexed?.jsonl_path, null);
295
+ assert.equal(sessionsDb.getSessionById('app-child-session-1'), null);
296
+ assert.equal(sessionsDb.getSessionByProviderSessionId('open-child-session-1'), null);
297
+
298
+ sessionsDb.createAppSession('app-child-session-2', 'opencode', workspacePath);
299
+ sessionsDb.assignProviderSessionId('app-child-session-2', 'open-child-session-1');
300
+ await synchronizer.synchronize(new Date(1_700_000_004_500));
301
+ assert.equal(
302
+ sessionsDb.getSessionById('app-child-session-2')?.provider_session_id,
303
+ 'open-child-session-1',
304
+ );
268
305
  });
269
306
  });
270
307
  } finally {
@@ -290,6 +327,7 @@ test('OpenCode session synchronizer returns the app session id once provider map
290
327
  assert.equal(sessionId, 'app-session-1');
291
328
  assert.equal(sessionsDb.getAllSessions().length, 1);
292
329
  assert.equal(sessionsDb.getSessionById('app-session-1')?.provider_session_id, 'open-session-1');
330
+ assert.equal(sessionsDb.getSessionById('open-child-session-1'), null);
293
331
  });
294
332
  });
295
333
  } finally {
@@ -438,6 +476,7 @@ const seedOpenCodeSession = async (
438
476
  CREATE TABLE session (
439
477
  id TEXT PRIMARY KEY,
440
478
  project_id TEXT,
479
+ parent_id TEXT,
441
480
  directory TEXT,
442
481
  title TEXT,
443
482
  time_created INTEGER,
@@ -0,0 +1,37 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+
4
+ import { CLAUDE_PREDEFINED_MODELS } from '@/modules/providers/list/claude/claude-models.provider.js';
5
+ import { CODEX_PREDEFINED_MODELS } from '@/modules/providers/list/codex/codex-models.provider.js';
6
+ import { CURSOR_PREDEFINED_MODELS } from '@/modules/providers/list/cursor/cursor-models.provider.js';
7
+ import { OPENCODE_PREDEFINED_MODELS } from '@/modules/providers/list/opencode/opencode-models.provider.js';
8
+ import { providerCapabilitiesService } from '@/modules/providers/services/provider-capabilities.service.js';
9
+ import type { LLMProvider, ProviderModelsDefinition } from '@/shared/types.js';
10
+
11
+ /**
12
+ * The full curated catalogs, before OpenCode narrows its list to the upstream
13
+ * providers a machine has connected.
14
+ */
15
+ const CURATED_CATALOGS: Record<LLMProvider, ProviderModelsDefinition> = {
16
+ claude: CLAUDE_PREDEFINED_MODELS,
17
+ codex: CODEX_PREDEFINED_MODELS,
18
+ cursor: CURSOR_PREDEFINED_MODELS,
19
+ opencode: OPENCODE_PREDEFINED_MODELS,
20
+ };
21
+
22
+ test('capability effort levels cover every level a curated model declares', () => {
23
+ // Custom models may declare only these levels, so a level added to a curated
24
+ // model has to be added here too, or users could not give it to their own.
25
+ for (const [provider, catalog] of Object.entries(CURATED_CATALOGS) as Array<[LLMProvider, ProviderModelsDefinition]>) {
26
+ const { effortLevels, supportsEffort } = providerCapabilitiesService.getProviderCapabilities(provider);
27
+
28
+ assert.equal(supportsEffort, effortLevels.length > 0, `${provider}: supportsEffort disagrees with effortLevels`);
29
+ assert.equal(new Set(effortLevels).size, effortLevels.length, `${provider}: effortLevels lists a level twice`);
30
+
31
+ const declaredLevels = new Set(
32
+ catalog.OPTIONS.flatMap((option) => option.effort?.values.map((level) => level.value) ?? []),
33
+ );
34
+ const missingLevels = [...declaredLevels].filter((level) => !effortLevels.includes(level));
35
+ assert.deepEqual(missingLevels, [], `${provider}: effortLevels misses curated levels`);
36
+ }
37
+ });
@@ -69,6 +69,7 @@ const createCatalogStore = () => {
69
69
  modelId: input.id,
70
70
  model: input.model,
71
71
  sortOrder: readRows(provider).length,
72
+ effort: input.effort ?? null,
72
73
  };
73
74
  rows.set(provider, [...readRows(provider), record]);
74
75
  return record;
@@ -82,7 +83,13 @@ const createCatalogStore = () => {
82
83
  if (!existing) {
83
84
  return null;
84
85
  }
85
- const updated = { ...existing, modelId: input.id, model: input.model };
86
+ const updated = {
87
+ ...existing,
88
+ modelId: input.id,
89
+ model: input.model,
90
+ // Mirrors the repository: an omitted effort keeps the stored levels.
91
+ effort: input.effort === undefined ? existing.effort : input.effort,
92
+ };
86
93
  rows.set(provider, readRows(provider).map((record) => (
87
94
  record.recordId === recordId ? updated : record
88
95
  )));
@@ -165,6 +172,97 @@ test('custom models can be created, edited, and deleted', async () => {
165
172
  assert.equal(removed.models.OPTIONS.some((option) => option.recordId === recordId), false);
166
173
  });
167
174
 
175
+ test('custom models carry declared effort levels into the merged catalog', async () => {
176
+ const { service } = createTestService();
177
+
178
+ const created = await service.createCustomModel('claude', {
179
+ model: 'My Custom Model',
180
+ id: 'my-custom-model',
181
+ effort: { values: ['low', 'max'], default: 'max' },
182
+ });
183
+ const expectedEffort = { default: 'max', values: [{ value: 'low' }, { value: 'max' }] };
184
+ assert.deepEqual(created.model.effort, expectedEffort);
185
+
186
+ const catalog = await service.getProviderModels('claude');
187
+ const merged = catalog.OPTIONS.find((option) => option.value === 'my-custom-model');
188
+ assert.deepEqual(merged?.effort, expectedEffort);
189
+
190
+ // A rename that does not mention effort (an older client) keeps the levels.
191
+ const renamed = await service.updateCustomModel('claude', created.model.recordId as number, {
192
+ model: 'Renamed',
193
+ id: 'my-custom-model',
194
+ });
195
+ assert.deepEqual(renamed.model.effort, expectedEffort);
196
+
197
+ const cleared = await service.updateCustomModel('claude', created.model.recordId as number, {
198
+ model: 'Renamed',
199
+ id: 'my-custom-model',
200
+ effort: null,
201
+ });
202
+ assert.equal('effort' in cleared.model, false);
203
+ });
204
+
205
+ test('custom models without declared effort levels expose no effort', async () => {
206
+ const { service } = createTestService();
207
+
208
+ const created = await service.createCustomModel('codex', { model: 'Plain', id: 'plain-model' });
209
+
210
+ assert.equal('effort' in created.model, false);
211
+ });
212
+
213
+ test('custom effort levels must be levels the provider supports', async () => {
214
+ const { service } = createTestService();
215
+
216
+ await assert.rejects(
217
+ () => service.createCustomModel('codex', {
218
+ model: 'Unknown level',
219
+ id: 'unknown-level',
220
+ effort: { values: ['low', 'turbo'] },
221
+ }),
222
+ (error) => error instanceof AppError
223
+ && error.code === 'INVALID_MODEL_EFFORT'
224
+ && error.statusCode === 400,
225
+ );
226
+
227
+ await assert.rejects(
228
+ () => service.createCustomModel('cursor', {
229
+ model: 'Cursor effort',
230
+ id: 'cursor-effort',
231
+ effort: { values: ['low'] },
232
+ }),
233
+ (error) => error instanceof AppError
234
+ && error.code === 'MODEL_EFFORT_NOT_SUPPORTED'
235
+ && error.statusCode === 400,
236
+ );
237
+ });
238
+
239
+ test('allowed effort levels come from the provider, not from the built-in models this machine sees', async () => {
240
+ // The fake adapters' built-in models declare no effort at all, which is what
241
+ // the real OpenCode adapter returns once it narrows its catalog to OpenCode
242
+ // Zen, Anthropic, or OpenAI: only its OpenCode Go models declare effort.
243
+ const { service } = createTestService();
244
+
245
+ const created = await service.createCustomModel('opencode', {
246
+ model: 'Router model',
247
+ id: 'openrouter/router-model',
248
+ effort: { values: ['none', 'high'] },
249
+ });
250
+ assert.deepEqual(created.model.effort, { values: [{ value: 'none' }, { value: 'high' }] });
251
+
252
+ // Every merged catalog carries the provider's levels for the model library,
253
+ // weakest first, and an empty list where effort is unsupported.
254
+ const expectedLevels: Record<LLMProvider, string[]> = {
255
+ claude: ['low', 'medium', 'high', 'xhigh', 'max', 'ultracode'],
256
+ codex: ['low', 'medium', 'high', 'xhigh', 'max', 'ultra'],
257
+ cursor: [],
258
+ opencode: ['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'thinking'],
259
+ };
260
+ for (const [provider, levels] of Object.entries(expectedLevels) as Array<[LLMProvider, string[]]>) {
261
+ assert.deepEqual((await service.getProviderModels(provider)).EFFORT_LEVELS, levels, provider);
262
+ }
263
+ assert.deepEqual(created.models.EFFORT_LEVELS, expectedLevels.opencode);
264
+ });
265
+
168
266
  test('duplicate model ids are rejected within one provider', async () => {
169
267
  const { service } = createTestService();
170
268
  await service.createCustomModel('cursor', { model: 'First', id: 'custom-id' });
@@ -1,6 +1,6 @@
1
1
  import assert from 'node:assert/strict';
2
2
  import { once } from 'node:events';
3
- import { mkdtemp, rm, writeFile } from 'node:fs/promises';
3
+ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
4
4
  import type { AddressInfo } from 'node:net';
5
5
  import os from 'node:os';
6
6
  import path from 'node:path';
@@ -10,6 +10,9 @@ import express, { type NextFunction, type Request, type Response } from 'express
10
10
 
11
11
  import { closeConnection, initializeDatabase, sessionsDb } from '@/modules/database/index.js';
12
12
  import providerRouter from '@/modules/providers/provider.routes.js';
13
+ import { providerRegistry } from '@/modules/providers/provider.registry.js';
14
+ import type { IProvider } from '@/shared/interfaces.js';
15
+ import type { BackgroundTaskSummary, WorkflowAgentActivity } from '@/shared/types.js';
13
16
  import { AppError } from '@/shared/utils.js';
14
17
 
15
18
  async function withProviderServer(
@@ -242,3 +245,229 @@ test('model routes expose immutable defaults and full custom model CRUD', async
242
245
  );
243
246
  });
244
247
  });
248
+
249
+ test('custom model routes accept, validate, and return reasoning-effort levels', async () => {
250
+ await withProviderServer(async (baseUrl) => {
251
+ const postModel = async (provider: string, body: unknown) => {
252
+ const response = await fetch(`${baseUrl}/api/providers/${provider}/models`, {
253
+ method: 'POST',
254
+ headers: { 'content-type': 'application/json' },
255
+ body: JSON.stringify(body),
256
+ });
257
+ return {
258
+ status: response.status,
259
+ payload: await response.json() as {
260
+ data?: {
261
+ model: { recordId: number; effort?: unknown };
262
+ models: { OPTIONS: Array<{ value: string; effort?: unknown }> };
263
+ };
264
+ error?: { code: string; message?: string };
265
+ },
266
+ };
267
+ };
268
+
269
+ const created = await postModel('claude', {
270
+ model: 'My Custom Model',
271
+ id: 'my-custom-model',
272
+ effort: { values: [' low ', 'high'], default: 'high' },
273
+ });
274
+ const expectedEffort = { default: 'high', values: [{ value: 'low' }, { value: 'high' }] };
275
+ assert.equal(created.status, 201);
276
+ assert.deepEqual(created.payload.data?.model.effort, expectedEffort);
277
+
278
+ const catalogResponse = await fetch(`${baseUrl}/api/providers/claude/models`);
279
+ const catalogPayload = await catalogResponse.json() as {
280
+ data: { models: { OPTIONS: Array<{ value: string; effort?: unknown }>; EFFORT_LEVELS?: string[] } };
281
+ };
282
+ assert.deepEqual(
283
+ catalogPayload.data.models.OPTIONS.find((option) => option.value === 'my-custom-model')?.effort,
284
+ expectedEffort,
285
+ );
286
+ // The model library's toggles come from here, not from the built-in models.
287
+ assert.deepEqual(
288
+ catalogPayload.data.models.EFFORT_LEVELS,
289
+ ['low', 'medium', 'high', 'xhigh', 'max', 'ultracode'],
290
+ );
291
+
292
+ // OpenCode's allowed levels do not depend on which upstream providers the
293
+ // machine running the tests has connected.
294
+ const openCodeModel = await postModel('opencode', {
295
+ model: 'Router model',
296
+ id: 'openrouter/router-model',
297
+ effort: { values: ['none', 'high'] },
298
+ });
299
+ assert.equal(openCodeModel.status, 201, JSON.stringify(openCodeModel.payload));
300
+ assert.deepEqual(openCodeModel.payload.data?.model.effort, { values: [{ value: 'none' }, { value: 'high' }] });
301
+
302
+ // A PATCH from a client that predates effort metadata keeps the levels.
303
+ const renameResponse = await fetch(
304
+ `${baseUrl}/api/providers/claude/models/${created.payload.data?.model.recordId}`,
305
+ {
306
+ method: 'PATCH',
307
+ headers: { 'content-type': 'application/json' },
308
+ body: JSON.stringify({ model: 'Renamed', id: 'my-custom-model' }),
309
+ },
310
+ );
311
+ const renamePayload = await renameResponse.json() as { data: { model: { effort?: unknown } } };
312
+ assert.equal(renameResponse.status, 200);
313
+ assert.deepEqual(renamePayload.data.model.effort, expectedEffort);
314
+
315
+ const invalidPayloads: Array<{ provider: string; effort: unknown; code: string }> = [
316
+ { provider: 'claude', effort: { values: ['low', 'low'] }, code: 'INVALID_MODEL_EFFORT' },
317
+ { provider: 'claude', effort: { values: ['low', ''] }, code: 'INVALID_MODEL_EFFORT' },
318
+ { provider: 'claude', effort: { values: ['low'], default: 'high' }, code: 'INVALID_MODEL_EFFORT' },
319
+ { provider: 'claude', effort: { values: ['turbo'] }, code: 'INVALID_MODEL_EFFORT' },
320
+ { provider: 'claude', effort: 'high', code: 'INVALID_MODEL_EFFORT' },
321
+ { provider: 'cursor', effort: { values: ['low'] }, code: 'MODEL_EFFORT_NOT_SUPPORTED' },
322
+ ];
323
+ for (const [index, invalid] of invalidPayloads.entries()) {
324
+ const rejected = await postModel(invalid.provider, {
325
+ model: `Invalid ${index}`,
326
+ id: `invalid-${index}`,
327
+ effort: invalid.effort,
328
+ });
329
+ assert.equal(rejected.status, 400, JSON.stringify(invalid));
330
+ assert.equal(rejected.payload.error?.code, invalid.code, JSON.stringify(invalid));
331
+ }
332
+
333
+ // An oversized list is refused by its length, before any entry is walked:
334
+ // the JSON parser accepts bodies of up to 50 MB.
335
+ const oversized = await postModel('claude', {
336
+ model: 'Oversized',
337
+ id: 'oversized',
338
+ effort: { values: Array.from({ length: 33 }, (_, index) => `level-${index}`) },
339
+ });
340
+ assert.equal(oversized.status, 400);
341
+ assert.equal(oversized.payload.error?.code, 'INVALID_MODEL_EFFORT');
342
+ assert.match(oversized.payload.error?.message ?? '', /at most 32 effort levels/);
343
+ });
344
+ });
345
+
346
+ test('the running-sessions route reports a session held open for background work', async () => {
347
+ // The client polls this route to keep every tab's activity map in step, so
348
+ // a session whose turn ended but whose agent is still running has to be in
349
+ // the payload — flagged as background work, with the tasks to show and stop.
350
+ const tasks: BackgroundTaskSummary[] = [{
351
+ taskId: 'a73dc7a6442f7c415',
352
+ toolUseId: 'toolu_013sUz4iWcNjN6BskVkHUCAr',
353
+ taskType: 'local_agent',
354
+ description: 'Investigate the flaky test',
355
+ startedAt: 1_700_000_000_000,
356
+ }];
357
+ const realListProviders = providerRegistry.listProviders;
358
+ providerRegistry.listProviders = () => [{
359
+ id: 'claude',
360
+ runtime: { listBackgroundWork: () => [{ sessionId: 'held-session', tasks }] },
361
+ } as unknown as IProvider];
362
+
363
+ try {
364
+ await withProviderServer(async (baseUrl) => {
365
+ const response = await fetch(`${baseUrl}/api/providers/sessions/running`);
366
+ const payload = await response.json() as { data: { sessions: unknown[] } };
367
+
368
+ assert.equal(response.status, 200);
369
+ assert.deepEqual(payload.data.sessions, [{
370
+ sessionId: 'held-session',
371
+ provider: 'claude',
372
+ startedAt: 1_700_000_000_000,
373
+ lastSeq: 0,
374
+ background: true,
375
+ canInterrupt: false,
376
+ tasks,
377
+ }]);
378
+ });
379
+ } finally {
380
+ providerRegistry.listProviders = realListProviders;
381
+ }
382
+ });
383
+
384
+ const WORKFLOW_SESSION_ID = 'claude-workflow-routes-session';
385
+ const WORKFLOW_RUN_ID = 'wf_16fbf852-274';
386
+ const FINISHED_AGENT_ID = 'aa1e064cf8bd159d6';
387
+ const UNREPORTED_AGENT_ID = 'ab89f2cde612a51b1';
388
+
389
+ /**
390
+ * Writes a workflow run's directory the way a real run leaves it under the
391
+ * session's transcript directory: one `agent-<id>.jsonl` per agent, in the
392
+ * row shape an Agent subagent's transcript has, and the journal beside them.
393
+ */
394
+ async function writeWorkflowRun(projectDirectory: string): Promise<string> {
395
+ const sessionPath = path.join(projectDirectory, `${WORKFLOW_SESSION_ID}.jsonl`);
396
+ await mkdir(projectDirectory, { recursive: true });
397
+ await writeFile(sessionPath, `${JSON.stringify({ type: 'user', uuid: 'u1', sessionId: WORKFLOW_SESSION_ID, timestamp: '2026-08-21T10:32:10.000Z', message: { role: 'user', content: 'audit the frontend' } })}\n`, 'utf8');
398
+
399
+ const runDir = path.join(projectDirectory, WORKFLOW_SESSION_ID, 'subagents', 'workflows', WORKFLOW_RUN_ID);
400
+ await mkdir(runDir, { recursive: true });
401
+
402
+ const agentRows = (agentId: string) => [
403
+ { parentUuid: null, isSidechain: true, agentId, type: 'user', uuid: `${agentId}-u1`, timestamp: '2026-08-21T10:32:20.000Z', message: { role: 'user', content: 'You are the analysis step of a code-quality audit.' } },
404
+ { parentUuid: `${agentId}-u1`, isSidechain: true, agentId, type: 'assistant', uuid: `${agentId}-a1`, timestamp: '2026-08-21T10:32:25.000Z', message: { role: 'assistant', model: 'claude-opus-4-1', content: [{ type: 'tool_use', id: `toolu_${agentId}_1`, name: 'Grep', input: { pattern: 'useSidebar' } }] } },
405
+ { parentUuid: `${agentId}-a1`, isSidechain: true, agentId, type: 'user', uuid: `${agentId}-u2`, timestamp: '2026-08-21T10:32:26.000Z', message: { role: 'user', content: [{ type: 'tool_result', tool_use_id: `toolu_${agentId}_1`, content: 'src/modules/sidebar/useSidebar.ts' }] } },
406
+ { parentUuid: `${agentId}-u2`, isSidechain: true, agentId, type: 'assistant', uuid: `${agentId}-a2`, timestamp: '2026-08-21T10:32:40.000Z', message: { role: 'assistant', model: 'claude-opus-4-1', content: [{ type: 'text', text: 'One hook owns the sidebar.' }] } },
407
+ ];
408
+ for (const agentId of [FINISHED_AGENT_ID, UNREPORTED_AGENT_ID]) {
409
+ await writeFile(path.join(runDir, `agent-${agentId}.jsonl`), `${agentRows(agentId).map((row) => JSON.stringify(row)).join('\n')}\n`, 'utf8');
410
+ await writeFile(path.join(runDir, `agent-${agentId}.meta.json`), JSON.stringify({ agentType: 'workflow-subagent', spawnDepth: 1 }), 'utf8');
411
+ }
412
+ const journal = [
413
+ { type: 'started', key: 'v2:one', agentId: FINISHED_AGENT_ID, label: 'audit:sidebar', phase: 'Audit' },
414
+ { type: 'started', key: 'v2:two', agentId: UNREPORTED_AGENT_ID, label: 'synthesize', phase: 'Synthesize' },
415
+ { type: 'result', key: 'v2:one', agentId: FINISHED_AGENT_ID, result: { area: 'sidebar' } },
416
+ ];
417
+ await writeFile(path.join(runDir, 'journal.jsonl'), `${journal.map((row) => JSON.stringify(row)).join('\n')}\n`, 'utf8');
418
+
419
+ return sessionPath;
420
+ }
421
+
422
+ test('the workflow agent route reads an agent\'s timeline and status from its run directory', async () => {
423
+ await withProviderServer(async (baseUrl, workspacePath) => {
424
+ const sessionPath = await writeWorkflowRun(workspacePath);
425
+ const now = new Date().toISOString();
426
+ sessionsDb.createSession(WORKFLOW_SESSION_ID, 'claude', workspacePath, 'Workflow session', now, now, sessionPath);
427
+
428
+ const agentUrl = (runId: string, agentId: string) =>
429
+ `${baseUrl}/api/providers/sessions/${WORKFLOW_SESSION_ID}/workflows/${runId}/agents/${agentId}`;
430
+
431
+ // The SDK streams none of an agent's rows to the parent, so its transcript
432
+ // is the only record of what it did — read on demand, capped like an
433
+ // Agent card's, and settled by the journal beside it.
434
+ const finishedResponse = await fetch(agentUrl(WORKFLOW_RUN_ID, FINISHED_AGENT_ID));
435
+ const finished = (await finishedResponse.json() as { data: WorkflowAgentActivity }).data;
436
+ assert.equal(finishedResponse.status, 200);
437
+ assert.deepEqual(finished.agent, { id: FINISHED_AGENT_ID, label: 'audit:sidebar', model: 'claude-opus-4-1', status: 'completed' });
438
+ assert.equal(finished.activityCount, 2);
439
+ assert.deepEqual(finished.activity.map((entry) => entry.kind), ['tool', 'text']);
440
+ assert.equal(finished.activity[0]?.toolName, 'Grep');
441
+ assert.deepEqual(finished.activity[0]?.toolInput, { pattern: 'useSidebar' });
442
+ assert.deepEqual(finished.activity[0]?.toolResult, { content: 'src/modules/sidebar/useSidebar.ts', isError: false });
443
+ assert.equal(finished.activity[1]?.content, 'One hook owns the sidebar.');
444
+
445
+ // A `started` with no `result` or `failed`, and no live run of the session
446
+ // to still be working in: nothing can report for it any more.
447
+ const unreportedResponse = await fetch(agentUrl(WORKFLOW_RUN_ID, UNREPORTED_AGENT_ID));
448
+ const unreported = (await unreportedResponse.json() as { data: WorkflowAgentActivity }).data;
449
+ assert.equal(unreportedResponse.status, 200);
450
+ assert.equal(unreported.agent.status, 'stopped');
451
+ assert.equal(unreported.agent.label, 'synthesize');
452
+
453
+ // Both ids name a file under the session directory, so neither may carry
454
+ // anything but the harness's own shapes — not even a well-formed
455
+ // directory that happens to exist.
456
+ for (const [runId, agentId, code] of [
457
+ [`${WORKFLOW_RUN_ID}%2F..`, FINISHED_AGENT_ID, 'INVALID_WORKFLOW_RUN_ID'],
458
+ ['subagents', FINISHED_AGENT_ID, 'INVALID_WORKFLOW_RUN_ID'],
459
+ [WORKFLOW_RUN_ID, '..%2Fjournal', 'INVALID_WORKFLOW_AGENT_ID'],
460
+ [WORKFLOW_RUN_ID, 'aa1e064cf8bd159d', 'INVALID_WORKFLOW_AGENT_ID'],
461
+ ] as const) {
462
+ const response = await fetch(agentUrl(runId, agentId));
463
+ const payload = await response.json() as { error: { code: string } };
464
+ assert.equal(response.status, 400, `${runId}/${agentId}`);
465
+ assert.equal(payload.error.code, code);
466
+ }
467
+
468
+ const missingResponse = await fetch(agentUrl(WORKFLOW_RUN_ID, 'a0000000000000000'));
469
+ const missing = await missingResponse.json() as { error: { code: string } };
470
+ assert.equal(missingResponse.status, 404);
471
+ assert.equal(missing.error.code, 'WORKFLOW_AGENT_NOT_FOUND');
472
+ });
473
+ });
@@ -141,3 +141,25 @@ test('the oldest entries are evicted over budget, but the newest survives alone'
141
141
  await rm(tempDirectory, { recursive: true, force: true });
142
142
  }
143
143
  });
144
+
145
+ test('invalidating a session re-parses its unchanged transcript on the next read', async () => {
146
+ await withTranscriptFile(async (transcriptPath) => {
147
+ const cache = createSessionHistoryCache();
148
+ let loads = 0;
149
+ const loadFull = async () => {
150
+ loads += 1;
151
+ return historyResult(`load-${loads}`);
152
+ };
153
+
154
+ // Claude's history reader decides a background agent's status from whether
155
+ // the session's CLI process is up — nothing the file's stat can reflect —
156
+ // so the runtime drops the entry when that process starts or ends.
157
+ const first = await cache.getFullHistory({ sessionId: 's1', transcriptPath, loadFull });
158
+ cache.invalidate('s1');
159
+ const second = await cache.getFullHistory({ sessionId: 's1', transcriptPath, loadFull });
160
+
161
+ assert.equal(loads, 2);
162
+ assert.notEqual(second, first);
163
+ assert.equal(second?.messages[0]?.content, 'load-2');
164
+ });
165
+ });