@cloudcli-ai/cloudcli 1.37.2 → 1.37.3

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 (328) hide show
  1. package/README.md +269 -0
  2. package/dist/assets/{abnfDiagram-N423BO3Z-C4UAoHJb.js → abnfDiagram-N423BO3Z-CJntskwA.js} +1 -1
  3. package/dist/assets/{arc-B8jF-ja9.js → arc-CIfH4uMN.js} +1 -1
  4. package/dist/assets/{architectureDiagram-T3A2C74G-C_Wur_CU.js → architectureDiagram-T3A2C74G-_IpEkukg.js} +1 -1
  5. package/dist/assets/{blockDiagram-VBNYF7ZC-Cq4P1i8t.js → blockDiagram-VBNYF7ZC-BttxrpgZ.js} +1 -1
  6. package/dist/assets/{c4Diagram-5PPSVZJV-C1wG4eUj.js → c4Diagram-5PPSVZJV-g51dSRr1.js} +1 -1
  7. package/dist/assets/channel-C5xU9jMw.js +1 -0
  8. package/dist/assets/{chunk-2GRJ4B5K-B8fnBG6E.js → chunk-2GRJ4B5K-D_-LbQUa.js} +1 -1
  9. package/dist/assets/{chunk-2Q5K7J3B-WdfPooPJ.js → chunk-2Q5K7J3B-BtlHzwaO.js} +1 -1
  10. package/dist/assets/{chunk-5RXB4S5H-DhZJuG2E.js → chunk-5RXB4S5H-UzkpvpAT.js} +1 -1
  11. package/dist/assets/{chunk-5VM5RSS4-B0rjslal.js → chunk-5VM5RSS4-BKwx5Gag.js} +1 -1
  12. package/dist/assets/{chunk-6Q2QTUOP-CG2Oj4jL.js → chunk-6Q2QTUOP-D_KEBH5f.js} +1 -1
  13. package/dist/assets/{chunk-GF5L2VYU-rrl2lRxY.js → chunk-GF5L2VYU-rK2DGyto.js} +1 -1
  14. package/dist/assets/{chunk-JWPE2WC7-CjVbrVMA.js → chunk-JWPE2WC7-CeBl_N9i.js} +1 -1
  15. package/dist/assets/{chunk-KBJHAD2P-CRzKgU8S.js → chunk-KBJHAD2P-BiQLWVIr.js} +1 -1
  16. package/dist/assets/{chunk-RYQCIY6F-DBz4GTZF.js → chunk-RYQCIY6F-C1knVGV5.js} +1 -1
  17. package/dist/assets/{chunk-XXDRQBXY-wHxalsXk.js → chunk-XXDRQBXY-CtKbJkvF.js} +1 -1
  18. package/dist/assets/classDiagram-JCYQIIEL-CRrL3vp7.js +1 -0
  19. package/dist/assets/classDiagram-v2-OCEON4UE-CRrL3vp7.js +1 -0
  20. package/dist/assets/{cose-bilkent-JH36ORCC-s1hygY8e.js → cose-bilkent-JH36ORCC-DjX8y40Z.js} +1 -1
  21. package/dist/assets/{cynefin-VYW2F7L2-C4LGV15i.js → cynefin-VYW2F7L2-DM6YeRba.js} +1 -1
  22. package/dist/assets/{cynefinDiagram-MW4NZA55-C5uhu7B3.js → cynefinDiagram-MW4NZA55-wZJ-vy5B.js} +1 -1
  23. package/dist/assets/{dagre-VZM6K2ZE-B-OBTewm.js → dagre-VZM6K2ZE-BzP0ejkH.js} +1 -1
  24. package/dist/assets/{diagram-7IWD3JNH-BjGKQxih.js → diagram-7IWD3JNH-BTL_j23J.js} +1 -1
  25. package/dist/assets/{diagram-B4RE2ZJO-BMKz1X2L.js → diagram-B4RE2ZJO-Cf1Norgu.js} +1 -1
  26. package/dist/assets/{diagram-LBJQPF4R-DNh-GhKC.js → diagram-LBJQPF4R-DPiUEKr-.js} +1 -1
  27. package/dist/assets/{diagram-Q27KOJAE-BaTG9jPT.js → diagram-Q27KOJAE-CTcTpiz5.js} +1 -1
  28. package/dist/assets/{diagram-UB23O5K3-BUwo2OvJ.js → diagram-UB23O5K3-DPMSdYm0.js} +1 -1
  29. package/dist/assets/{ebnfDiagram-BXEA7PRR-DAZgfzX2.js → ebnfDiagram-BXEA7PRR-7yvD6uiF.js} +1 -1
  30. package/dist/assets/{erDiagram-JOGREHBK-1w1CcGEb.js → erDiagram-JOGREHBK-CfgeDoV6.js} +1 -1
  31. package/dist/assets/{flowDiagram-UKHOOZJN-D4EII8Jj.js → flowDiagram-UKHOOZJN-B27ng5zu.js} +1 -1
  32. package/dist/assets/{ganttDiagram-PKOTCBZU-ChvJZAgB.js → ganttDiagram-PKOTCBZU-CcC6b_7t.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-DS77QQ5N-DtOn41S8.js → gitGraphDiagram-DS77QQ5N-BX5kPGrO.js} +1 -1
  34. package/dist/assets/index-BTDHQ6Id.css +32 -0
  35. package/dist/assets/index-sFfz8iBR.js +4931 -0
  36. package/dist/assets/{infoDiagram-6WML65LV-DN_PVEJZ.js → infoDiagram-6WML65LV-CUP-SlN4.js} +1 -1
  37. package/dist/assets/{ishikawaDiagram-WSZJBQD7-DbkF1kXb.js → ishikawaDiagram-WSZJBQD7-BTTDhOpX.js} +1 -1
  38. package/dist/assets/{journeyDiagram-NVQOT4AX-pJwmhddE.js → journeyDiagram-NVQOT4AX-ARXcc61m.js} +1 -1
  39. package/dist/assets/{kanban-definition-27J2QSJJ-PPAPgNp5.js → kanban-definition-27J2QSJJ-BieMpqMA.js} +1 -1
  40. package/dist/assets/{linear-iPR-J9f7.js → linear-61vxjRnP.js} +1 -1
  41. package/dist/assets/{mermaid.core-BEUL86Z6.js → mermaid.core-nhnwkfQx.js} +5 -5
  42. package/dist/assets/{mindmap-definition-FAOFIHXS-BvZoBib7.js → mindmap-definition-FAOFIHXS-Cm6x5vT1.js} +1 -1
  43. package/dist/assets/{pegDiagram-VL7TDLO6-DhYreaUd.js → pegDiagram-VL7TDLO6-CcYBq_9H.js} +1 -1
  44. package/dist/assets/{pieDiagram-7S7Q4E2Y-CrOHlyD0.js → pieDiagram-7S7Q4E2Y-CfvIjfzo.js} +1 -1
  45. package/dist/assets/{quadrantDiagram-CIZ2JOQS-D4g5yH3t.js → quadrantDiagram-CIZ2JOQS-SgYoWPNb.js} +1 -1
  46. package/dist/assets/{railroadDiagram-AXF67PYL-Dj39lHlf.js → railroadDiagram-AXF67PYL-D2Mu5E_f.js} +1 -1
  47. package/dist/assets/{requirementDiagram-LRYGKXZP-1AS4Zgm_.js → requirementDiagram-LRYGKXZP-Azwe_k4Q.js} +1 -1
  48. package/dist/assets/{sankeyDiagram-W5VNT64P-CKlMn19P.js → sankeyDiagram-W5VNT64P-DN1Ju3PA.js} +1 -1
  49. package/dist/assets/{sequenceDiagram-SI44F4Z6-B_j3qyvo.js → sequenceDiagram-SI44F4Z6-Bj4qC34S.js} +1 -1
  50. package/dist/assets/server.browser-CtX3Hdl5.js +27 -0
  51. package/dist/assets/{sizeCapture-X5ZJPWSS-Bp2Op16v.js → sizeCapture-X5ZJPWSS-Co4PdklG.js} +1 -1
  52. package/dist/assets/{stateDiagram-OKZ733FA-C0cN6DoY.js → stateDiagram-OKZ733FA-nOvL9dRC.js} +1 -1
  53. package/dist/assets/stateDiagram-v2-UEYNNEHI-DNv8ppw7.js +1 -0
  54. package/dist/assets/{swimlanes-SLNWSIFB-BEvXOV9b.js → swimlanes-SLNWSIFB-CCK3wMid.js} +2 -2
  55. package/dist/assets/swimlanesDiagram-ULZ7WXOC-DnfbNQEp.js +8 -0
  56. package/dist/assets/{timeline-definition-Z64GVDOM-zdJQinvZ.js → timeline-definition-Z64GVDOM-BC29RNFm.js} +1 -1
  57. package/dist/assets/{vendor-codemirror-OwyKSvPE.js → vendor-codemirror-CYrSt7-U.js} +18 -18
  58. package/dist/assets/{vennDiagram-T6HMQDX7-DuF1uZMY.js → vennDiagram-T6HMQDX7-pZlZCtEo.js} +1 -1
  59. package/dist/assets/{wardleyDiagram-T6FBY63Y-B9AlLQup.js → wardleyDiagram-T6FBY63Y-De8k2-zM.js} +1 -1
  60. package/dist/assets/{xychartDiagram-ELKLHX3M-DTJX48Qg.js → xychartDiagram-ELKLHX3M-Bk8ykH0w.js} +1 -1
  61. package/dist/index.html +3 -3
  62. package/dist-server/server/index.js +7 -3
  63. package/dist-server/server/index.js.map +1 -1
  64. package/dist-server/server/modules/agent/agent.routes.js +2 -2
  65. package/dist-server/server/modules/agent/agent.routes.js.map +1 -1
  66. package/dist-server/server/modules/agent/tests/agent.routes.test.js +30 -1
  67. package/dist-server/server/modules/agent/tests/agent.routes.test.js.map +1 -1
  68. package/dist-server/server/modules/browser-use/browser-use.routes.js +39 -33
  69. package/dist-server/server/modules/browser-use/browser-use.routes.js.map +1 -1
  70. package/dist-server/server/modules/database/connection.js +1 -1
  71. package/dist-server/server/modules/database/connection.js.map +1 -1
  72. package/dist-server/server/modules/database/index.js +5 -0
  73. package/dist-server/server/modules/database/index.js.map +1 -1
  74. package/dist-server/server/modules/database/migrations.js +32 -1
  75. package/dist-server/server/modules/database/migrations.js.map +1 -1
  76. package/dist-server/server/modules/database/repositories/scheduled-messages.db.js +71 -0
  77. package/dist-server/server/modules/database/repositories/scheduled-messages.db.js.map +1 -0
  78. package/dist-server/server/modules/database/repositories/session-drafts.db.js +105 -0
  79. package/dist-server/server/modules/database/repositories/session-drafts.db.js.map +1 -0
  80. package/dist-server/server/modules/database/repositories/sessions.db.js +137 -5
  81. package/dist-server/server/modules/database/repositories/sessions.db.js.map +1 -1
  82. package/dist-server/server/modules/database/repositories/user-preferences.db.js +65 -0
  83. package/dist-server/server/modules/database/repositories/user-preferences.db.js.map +1 -0
  84. package/dist-server/server/modules/database/schema.js +92 -0
  85. package/dist-server/server/modules/database/schema.js.map +1 -1
  86. package/dist-server/server/modules/database/tests/sessions.db.integration.test.js +39 -0
  87. package/dist-server/server/modules/database/tests/sessions.db.integration.test.js.map +1 -1
  88. package/dist-server/server/modules/database/tests/user-preferences.db.integration.test.js +105 -0
  89. package/dist-server/server/modules/database/tests/user-preferences.db.integration.test.js.map +1 -0
  90. package/dist-server/server/modules/projects/services/project-delete.service.js +12 -1
  91. package/dist-server/server/modules/projects/services/project-delete.service.js.map +1 -1
  92. package/dist-server/server/modules/projects/services/project-star.service.js +2 -2
  93. package/dist-server/server/modules/projects/services/project-star.service.js.map +1 -1
  94. package/dist-server/server/modules/providers/index.js +3 -0
  95. package/dist-server/server/modules/providers/index.js.map +1 -1
  96. package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js +20 -1
  97. package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js.map +1 -1
  98. package/dist-server/server/modules/providers/list/claude/claude-fork.provider.js +46 -0
  99. package/dist-server/server/modules/providers/list/claude/claude-fork.provider.js.map +1 -0
  100. package/dist-server/server/modules/providers/list/claude/claude-models.provider.js +35 -6
  101. package/dist-server/server/modules/providers/list/claude/claude-models.provider.js.map +1 -1
  102. package/dist-server/server/modules/providers/list/claude/claude-runtime.provider.js +168 -38
  103. package/dist-server/server/modules/providers/list/claude/claude-runtime.provider.js.map +1 -1
  104. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js +388 -50
  105. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js.map +1 -1
  106. package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js +4 -1
  107. package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js.map +1 -1
  108. package/dist-server/server/modules/providers/list/claude/claude.provider.js +2 -0
  109. package/dist-server/server/modules/providers/list/claude/claude.provider.js.map +1 -1
  110. package/dist-server/server/modules/providers/list/codex/codex-app-server.client.js +204 -0
  111. package/dist-server/server/modules/providers/list/codex/codex-app-server.client.js.map +1 -0
  112. package/dist-server/server/modules/providers/list/codex/codex-fork.provider.js +32 -0
  113. package/dist-server/server/modules/providers/list/codex/codex-fork.provider.js.map +1 -0
  114. package/dist-server/server/modules/providers/list/codex/codex-models.provider.js +16 -0
  115. package/dist-server/server/modules/providers/list/codex/codex-models.provider.js.map +1 -1
  116. package/dist-server/server/modules/providers/list/codex/codex-runtime.provider.js +60 -49
  117. package/dist-server/server/modules/providers/list/codex/codex-runtime.provider.js.map +1 -1
  118. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js +8 -0
  119. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js.map +1 -1
  120. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +1565 -432
  121. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -1
  122. package/dist-server/server/modules/providers/list/codex/codex.provider.js +2 -0
  123. package/dist-server/server/modules/providers/list/codex/codex.provider.js.map +1 -1
  124. package/dist-server/server/modules/providers/list/cursor/cursor-runtime.provider.js +8 -6
  125. package/dist-server/server/modules/providers/list/cursor/cursor-runtime.provider.js.map +1 -1
  126. package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js.map +1 -1
  127. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js +258 -6
  128. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js.map +1 -1
  129. package/dist-server/server/modules/providers/provider.routes.js +24 -0
  130. package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
  131. package/dist-server/server/modules/providers/services/provider-capabilities.service.js +14 -0
  132. package/dist-server/server/modules/providers/services/provider-capabilities.service.js.map +1 -1
  133. package/dist-server/server/modules/providers/services/provider-token-usage.service.js +139 -35
  134. package/dist-server/server/modules/providers/services/provider-token-usage.service.js.map +1 -1
  135. package/dist-server/server/modules/providers/services/session-history-cache.service.js +87 -0
  136. package/dist-server/server/modules/providers/services/session-history-cache.service.js.map +1 -0
  137. package/dist-server/server/modules/providers/services/session-synchronizer.service.js +105 -32
  138. package/dist-server/server/modules/providers/services/session-synchronizer.service.js.map +1 -1
  139. package/dist-server/server/modules/providers/services/sessions-watcher.service.js +3 -60
  140. package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -1
  141. package/dist-server/server/modules/providers/services/sessions.service.js +179 -9
  142. package/dist-server/server/modules/providers/services/sessions.service.js.map +1 -1
  143. package/dist-server/server/modules/providers/tests/claude-auth.test.js +52 -0
  144. package/dist-server/server/modules/providers/tests/claude-auth.test.js.map +1 -1
  145. package/dist-server/server/modules/providers/tests/claude-models.test.js +39 -0
  146. package/dist-server/server/modules/providers/tests/claude-models.test.js.map +1 -0
  147. package/dist-server/server/modules/providers/tests/claude-sessions.test.js +465 -39
  148. package/dist-server/server/modules/providers/tests/claude-sessions.test.js.map +1 -1
  149. package/dist-server/server/modules/providers/tests/claude-subagent-echo.test.js +16 -0
  150. package/dist-server/server/modules/providers/tests/claude-subagent-echo.test.js.map +1 -0
  151. package/dist-server/server/modules/providers/tests/claude-token-budget.test.js +85 -0
  152. package/dist-server/server/modules/providers/tests/claude-token-budget.test.js.map +1 -0
  153. package/dist-server/server/modules/providers/tests/codex-message-editing.test.js +338 -0
  154. package/dist-server/server/modules/providers/tests/codex-message-editing.test.js.map +1 -0
  155. package/dist-server/server/modules/providers/tests/codex-runtime.test.js +51 -0
  156. package/dist-server/server/modules/providers/tests/codex-runtime.test.js.map +1 -0
  157. package/dist-server/server/modules/providers/tests/codex-session-fork.test.js +54 -0
  158. package/dist-server/server/modules/providers/tests/codex-session-fork.test.js.map +1 -0
  159. package/dist-server/server/modules/providers/tests/codex-sessions.test.js +304 -15
  160. package/dist-server/server/modules/providers/tests/codex-sessions.test.js.map +1 -1
  161. package/dist-server/server/modules/providers/tests/opencode-models.test.js +117 -4
  162. package/dist-server/server/modules/providers/tests/opencode-models.test.js.map +1 -1
  163. package/dist-server/server/modules/providers/tests/provider-token-usage.service.test.js +164 -1
  164. package/dist-server/server/modules/providers/tests/provider-token-usage.service.test.js.map +1 -1
  165. package/dist-server/server/modules/providers/tests/session-fork.test.js +113 -0
  166. package/dist-server/server/modules/providers/tests/session-fork.test.js.map +1 -0
  167. package/dist-server/server/modules/providers/tests/session-history-cache.service.test.js +120 -0
  168. package/dist-server/server/modules/providers/tests/session-history-cache.service.test.js.map +1 -0
  169. package/dist-server/server/modules/providers/tests/session-orphan-prune.test.js +90 -0
  170. package/dist-server/server/modules/providers/tests/session-orphan-prune.test.js.map +1 -0
  171. package/dist-server/server/modules/providers/tests/session-synchronizer-coalescing.test.js +122 -0
  172. package/dist-server/server/modules/providers/tests/session-synchronizer-coalescing.test.js.map +1 -0
  173. package/dist-server/server/modules/providers/tests/sessions.service.test.js +45 -1
  174. package/dist-server/server/modules/providers/tests/sessions.service.test.js.map +1 -1
  175. package/dist-server/server/modules/providers/tests/skills.test.js +15 -3
  176. package/dist-server/server/modules/providers/tests/skills.test.js.map +1 -1
  177. package/dist-server/server/modules/scheduled-messages/index.js +5 -0
  178. package/dist-server/server/modules/scheduled-messages/index.js.map +1 -0
  179. package/dist-server/server/modules/scheduled-messages/scheduled-messages.routes.js +47 -0
  180. package/dist-server/server/modules/scheduled-messages/scheduled-messages.routes.js.map +1 -0
  181. package/dist-server/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.js +163 -0
  182. package/dist-server/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.js.map +1 -0
  183. package/dist-server/server/modules/scheduled-messages/services/scheduled-messages.service.js +95 -0
  184. package/dist-server/server/modules/scheduled-messages/services/scheduled-messages.service.js.map +1 -0
  185. package/dist-server/server/modules/scheduled-messages/tests/scheduled-messages.test.js +255 -0
  186. package/dist-server/server/modules/scheduled-messages/tests/scheduled-messages.test.js.map +1 -0
  187. package/dist-server/server/modules/taskmaster/taskmaster.routes.js +23 -23
  188. package/dist-server/server/modules/taskmaster/taskmaster.routes.js.map +1 -1
  189. package/dist-server/server/modules/taskmaster/tests/taskmaster.routes.test.js +48 -0
  190. package/dist-server/server/modules/taskmaster/tests/taskmaster.routes.test.js.map +1 -1
  191. package/dist-server/server/modules/user/tests/user.service.test.js +70 -0
  192. package/dist-server/server/modules/user/tests/user.service.test.js.map +1 -1
  193. package/dist-server/server/modules/user/user.module.js +10 -1
  194. package/dist-server/server/modules/user/user.module.js.map +1 -1
  195. package/dist-server/server/modules/user/user.routes.js +44 -0
  196. package/dist-server/server/modules/user/user.routes.js.map +1 -1
  197. package/dist-server/server/modules/user/user.service.js +75 -0
  198. package/dist-server/server/modules/user/user.service.js.map +1 -1
  199. package/dist-server/server/modules/voice/tests/voice.service.test.js +3 -2
  200. package/dist-server/server/modules/voice/tests/voice.service.test.js.map +1 -1
  201. package/dist-server/server/modules/websocket/index.js +7 -0
  202. package/dist-server/server/modules/websocket/index.js.map +1 -1
  203. package/dist-server/server/modules/websocket/services/chat-run-registry.service.js +10 -44
  204. package/dist-server/server/modules/websocket/services/chat-run-registry.service.js.map +1 -1
  205. package/dist-server/server/modules/websocket/services/chat-session-writer.service.js +35 -5
  206. package/dist-server/server/modules/websocket/services/chat-session-writer.service.js.map +1 -1
  207. package/dist-server/server/modules/websocket/services/chat-websocket.service.js +166 -9
  208. package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
  209. package/dist-server/server/modules/websocket/services/session-upsert-broadcast.service.js +94 -0
  210. package/dist-server/server/modules/websocket/services/session-upsert-broadcast.service.js.map +1 -0
  211. package/dist-server/server/modules/websocket/services/shell-websocket.service.js +9 -3
  212. package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -1
  213. package/dist-server/server/modules/websocket/tests/chat-edit-send.test.js +263 -0
  214. package/dist-server/server/modules/websocket/tests/chat-edit-send.test.js.map +1 -0
  215. package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js +29 -3
  216. package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js.map +1 -1
  217. package/dist-server/server/modules/websocket/tests/session-upsert-broadcast.test.js +106 -0
  218. package/dist-server/server/modules/websocket/tests/session-upsert-broadcast.test.js.map +1 -0
  219. package/dist-server/server/modules/websocket/tests/shell-websocket.service.test.js +80 -0
  220. package/dist-server/server/modules/websocket/tests/shell-websocket.service.test.js.map +1 -1
  221. package/dist-server/server/shared/claude-cli-path.js +22 -5
  222. package/dist-server/server/shared/claude-cli-path.js.map +1 -1
  223. package/dist-server/server/shared/index.js +3 -0
  224. package/dist-server/server/shared/index.js.map +1 -0
  225. package/dist-server/server/shared/message-unification.js +376 -0
  226. package/dist-server/server/shared/message-unification.js.map +1 -0
  227. package/dist-server/server/shared/tests/claude-cli-path.test.js +45 -1
  228. package/dist-server/server/shared/tests/claude-cli-path.test.js.map +1 -1
  229. package/dist-server/server/shared/tests/message-unification.test.js +174 -0
  230. package/dist-server/server/shared/tests/message-unification.test.js.map +1 -0
  231. package/dist-server/server/shared/utils.js +34 -0
  232. package/dist-server/server/shared/utils.js.map +1 -1
  233. package/package.json +19 -16
  234. package/server/index.ts +11 -4
  235. package/server/modules/agent/agent.routes.ts +2 -2
  236. package/server/modules/agent/tests/agent.routes.test.ts +33 -1
  237. package/server/modules/browser-use/browser-use.routes.ts +96 -33
  238. package/server/modules/database/connection.ts +2 -1
  239. package/server/modules/database/index.ts +10 -0
  240. package/server/modules/database/migrations.ts +37 -0
  241. package/server/modules/database/repositories/scheduled-messages.db.ts +127 -0
  242. package/server/modules/database/repositories/session-drafts.db.ts +171 -0
  243. package/server/modules/database/repositories/sessions.db.ts +196 -3
  244. package/server/modules/database/repositories/user-preferences.db.ts +80 -0
  245. package/server/modules/database/schema.ts +96 -0
  246. package/server/modules/database/tests/sessions.db.integration.test.ts +52 -0
  247. package/server/modules/database/tests/user-preferences.db.integration.test.ts +135 -0
  248. package/server/modules/projects/services/project-delete.service.ts +13 -1
  249. package/server/modules/projects/services/project-star.service.ts +2 -2
  250. package/server/modules/providers/index.ts +4 -0
  251. package/server/modules/providers/list/claude/claude-auth.provider.ts +21 -1
  252. package/server/modules/providers/list/claude/claude-fork.provider.ts +57 -0
  253. package/server/modules/providers/list/claude/claude-models.provider.ts +38 -6
  254. package/server/modules/providers/list/claude/claude-runtime.provider.js +188 -39
  255. package/server/modules/providers/list/claude/claude-sessions.provider.ts +478 -54
  256. package/server/modules/providers/list/claude/claude-skills.provider.ts +4 -1
  257. package/server/modules/providers/list/claude/claude.provider.ts +3 -0
  258. package/server/modules/providers/list/codex/codex-app-server.client.ts +243 -0
  259. package/server/modules/providers/list/codex/codex-fork.provider.ts +40 -0
  260. package/server/modules/providers/list/codex/codex-models.provider.ts +16 -0
  261. package/server/modules/providers/list/codex/{codex-runtime.provider.js → codex-runtime.provider.ts} +93 -66
  262. package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +9 -0
  263. package/server/modules/providers/list/codex/codex-sessions.provider.ts +1806 -477
  264. package/server/modules/providers/list/codex/codex.provider.ts +3 -0
  265. package/server/modules/providers/list/cursor/cursor-runtime.provider.js +8 -6
  266. package/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.ts +0 -1
  267. package/server/modules/providers/list/opencode/opencode-models.provider.ts +279 -6
  268. package/server/modules/providers/provider.routes.ts +31 -0
  269. package/server/modules/providers/services/provider-capabilities.service.ts +23 -0
  270. package/server/modules/providers/services/provider-token-usage.service.ts +166 -39
  271. package/server/modules/providers/services/session-history-cache.service.ts +139 -0
  272. package/server/modules/providers/services/session-synchronizer.service.ts +125 -38
  273. package/server/modules/providers/services/sessions-watcher.service.ts +3 -64
  274. package/server/modules/providers/services/sessions.service.ts +201 -9
  275. package/server/modules/providers/tests/claude-auth.test.ts +58 -0
  276. package/server/modules/providers/tests/claude-models.test.ts +87 -0
  277. package/server/modules/providers/tests/claude-sessions.test.ts +552 -40
  278. package/server/modules/providers/tests/claude-subagent-echo.test.ts +25 -0
  279. package/server/modules/providers/tests/claude-token-budget.test.ts +104 -0
  280. package/server/modules/providers/tests/codex-message-editing.test.ts +419 -0
  281. package/server/modules/providers/tests/codex-runtime.test.ts +58 -0
  282. package/server/modules/providers/tests/codex-session-fork.test.ts +59 -0
  283. package/server/modules/providers/tests/codex-sessions.test.ts +337 -15
  284. package/server/modules/providers/tests/opencode-models.test.ts +166 -8
  285. package/server/modules/providers/tests/provider-token-usage.service.test.ts +188 -1
  286. package/server/modules/providers/tests/session-fork.test.ts +173 -0
  287. package/server/modules/providers/tests/session-history-cache.service.test.ts +143 -0
  288. package/server/modules/providers/tests/session-orphan-prune.test.ts +107 -0
  289. package/server/modules/providers/tests/session-synchronizer-coalescing.test.ts +136 -0
  290. package/server/modules/providers/tests/sessions.service.test.ts +67 -1
  291. package/server/modules/providers/tests/skills.test.ts +21 -5
  292. package/server/modules/scheduled-messages/index.ts +8 -0
  293. package/server/modules/scheduled-messages/scheduled-messages.routes.ts +68 -0
  294. package/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.ts +203 -0
  295. package/server/modules/scheduled-messages/services/scheduled-messages.service.ts +121 -0
  296. package/server/modules/scheduled-messages/tests/scheduled-messages.test.ts +310 -0
  297. package/server/modules/taskmaster/taskmaster.routes.ts +23 -33
  298. package/server/modules/taskmaster/tests/taskmaster.routes.test.ts +57 -0
  299. package/server/modules/user/tests/user.service.test.ts +90 -0
  300. package/server/modules/user/user.module.ts +10 -1
  301. package/server/modules/user/user.routes.ts +44 -0
  302. package/server/modules/user/user.service.ts +105 -0
  303. package/server/modules/voice/tests/voice.service.test.ts +3 -2
  304. package/server/modules/websocket/index.ts +8 -0
  305. package/server/modules/websocket/services/chat-run-registry.service.ts +17 -50
  306. package/server/modules/websocket/services/chat-session-writer.service.ts +36 -6
  307. package/server/modules/websocket/services/chat-websocket.service.ts +244 -15
  308. package/server/modules/websocket/services/session-upsert-broadcast.service.ts +108 -0
  309. package/server/modules/websocket/services/shell-websocket.service.ts +10 -3
  310. package/server/modules/websocket/tests/chat-edit-send.test.ts +309 -0
  311. package/server/modules/websocket/tests/chat-run-registry.test.ts +34 -3
  312. package/server/modules/websocket/tests/session-upsert-broadcast.test.ts +138 -0
  313. package/server/modules/websocket/tests/shell-websocket.service.test.ts +108 -0
  314. package/server/shared/claude-cli-path.ts +24 -6
  315. package/server/shared/index.ts +3 -0
  316. package/server/shared/interfaces.ts +62 -0
  317. package/server/shared/message-unification.ts +449 -0
  318. package/server/shared/tests/claude-cli-path.test.ts +58 -1
  319. package/server/shared/tests/message-unification.test.ts +218 -0
  320. package/server/shared/types.ts +123 -2
  321. package/server/shared/utils.ts +40 -0
  322. package/dist/assets/channel-_falYbU1.js +0 -1
  323. package/dist/assets/classDiagram-JCYQIIEL-Dn-3CfpR.js +0 -1
  324. package/dist/assets/classDiagram-v2-OCEON4UE-Dn-3CfpR.js +0 -1
  325. package/dist/assets/index-CzPLBvXT.css +0 -32
  326. package/dist/assets/index-DFkBBuSA.js +0 -1549
  327. package/dist/assets/stateDiagram-v2-UEYNNEHI-BvBxUUrZ.js +0 -1
  328. package/dist/assets/swimlanesDiagram-ULZ7WXOC-CsE-x4YK.js +0 -8
@@ -0,0 +1,108 @@
1
+ import path from 'node:path';
2
+
3
+ import { projectsDb, sessionsDb } from '@/modules/database/index.js';
4
+ import { generateDisplayName } from '@/modules/projects/index.js';
5
+ import { connectedClients, WS_OPEN_STATE } from '@/modules/websocket/services/websocket-state.service.js';
6
+ import type { SessionUpsertedEvent } from '@/shared/types.js';
7
+
8
+ /**
9
+ * The single producer of the `session_upserted` delta.
10
+ *
11
+ * The event carries everything a sidebar needs to upsert a session in place
12
+ * (its summary plus the owning project's metadata), so clients never refetch
13
+ * the whole project list because one transcript changed on disk.
14
+ *
15
+ * It used to be built in two places — the on-disk sessions watcher and the
16
+ * chat run registry — and only the registry's copy set `providerSessionId`.
17
+ * That field is how the client notices a row it is showing has been merged
18
+ * into its canonical app-session row, so a merge announced by any other path
19
+ * left a stale duplicate in the sidebar. One builder, one shape.
20
+ */
21
+ async function buildSessionUpsertedEvent(
22
+ sessionIdOrProviderSessionId: string,
23
+ ): Promise<SessionUpsertedEvent | null> {
24
+ // Resolving by provider id first covers the watcher, which only ever sees
25
+ // the id written in the transcript. For a row where the two ids are equal
26
+ // — legacy rows, and any session indexed from disk before its mapping
27
+ // existed — both lookups land on the same row anyway.
28
+ const row = sessionsDb.getSessionByProviderSessionId(sessionIdOrProviderSessionId)
29
+ ?? sessionsDb.getSessionById(sessionIdOrProviderSessionId);
30
+ if (!row || row.isArchived) {
31
+ return null;
32
+ }
33
+
34
+ const projectPath = row.project_path;
35
+ const project = projectPath ? projectsDb.getProjectPath(projectPath) : null;
36
+ const displayName = project?.custom_project_name?.trim()
37
+ ? project.custom_project_name
38
+ : await generateDisplayName(path.basename(projectPath ?? '') || (projectPath ?? ''), projectPath);
39
+
40
+ return {
41
+ kind: 'session_upserted',
42
+ sessionId: row.session_id,
43
+ providerSessionId: row.provider_session_id ?? null,
44
+ // The column is a plain TEXT and typed as `string` on the row, but only
45
+ // provider adapters ever write it.
46
+ provider: row.provider as SessionUpsertedEvent['provider'],
47
+ session: {
48
+ id: row.session_id,
49
+ summary: row.custom_name || '',
50
+ messageCount: 0,
51
+ lastActivity: row.updated_at ?? row.created_at ?? new Date().toISOString(),
52
+ },
53
+ project: project
54
+ ? {
55
+ projectId: project.project_id,
56
+ path: project.project_path,
57
+ fullPath: project.project_path,
58
+ displayName,
59
+ isStarred: Boolean(project.isStarred),
60
+ }
61
+ : null,
62
+ timestamp: new Date().toISOString(),
63
+ };
64
+ }
65
+
66
+ function sendToConnectedClients(payloads: string[]): void {
67
+ if (payloads.length === 0) {
68
+ return;
69
+ }
70
+
71
+ connectedClients.forEach((client) => {
72
+ if (client.readyState === WS_OPEN_STATE) {
73
+ for (const payload of payloads) {
74
+ client.send(payload);
75
+ }
76
+ }
77
+ });
78
+ }
79
+
80
+ /** Announces one session. Used by the chat run registry when a run reports its provider-native id. */
81
+ export async function broadcastSessionUpserted(sessionIdOrProviderSessionId: string): Promise<void> {
82
+ const event = await buildSessionUpsertedEvent(sessionIdOrProviderSessionId);
83
+ if (event) {
84
+ sendToConnectedClients([JSON.stringify(event)]);
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Announces a batch of sessions. Used by the providers module's sessions
90
+ * watcher, whose debounced flush can carry dozens of ids at once — the client
91
+ * set is walked once for the whole batch rather than once per session.
92
+ */
93
+ export async function broadcastSessionUpsertedBatch(
94
+ sessionIds: Iterable<string>,
95
+ ): Promise<void> {
96
+ const payloads: string[] = [];
97
+ for (const sessionId of sessionIds) {
98
+ const event = await buildSessionUpsertedEvent(sessionId);
99
+ if (event) {
100
+ payloads.push(JSON.stringify(event));
101
+ }
102
+ }
103
+
104
+ sendToConnectedClients(payloads);
105
+ }
106
+
107
+ /** @internal Exported for the broadcast tests, which assert the payload shape directly. */
108
+ export { buildSessionUpsertedEvent };
@@ -19,6 +19,7 @@ type ShellIncomingMessage = {
19
19
  initialCommand?: string;
20
20
  isPlainShell?: boolean;
21
21
  forceRestart?: boolean;
22
+ bypassPermissions?: boolean;
22
23
  };
23
24
 
24
25
  type PtySessionEntry = {
@@ -216,12 +217,18 @@ function buildShellCommand(
216
217
  return initialCommand || 'opencode';
217
218
  }
218
219
 
219
- const command = initialCommand || 'claude';
220
+ // Launching with the flag is what unlocks "bypass permissions" in the CLI's
221
+ // shift+tab permission-mode cycle; it cannot be enabled from inside a
222
+ // session started without it.
223
+ const bypassFlag = readBoolean(message.bypassPermissions)
224
+ ? ' --dangerously-skip-permissions'
225
+ : '';
226
+ const command = initialCommand || `claude${bypassFlag}`;
220
227
  if (resumeSessionId) {
221
228
  if (os.platform() === 'win32') {
222
- return `claude --resume "${resumeSessionId}"; if ($LASTEXITCODE -ne 0) { claude }`;
229
+ return `claude --resume "${resumeSessionId}"${bypassFlag}; if ($LASTEXITCODE -ne 0) { claude${bypassFlag} }`;
223
230
  }
224
- return `claude --resume "${resumeSessionId}" || claude`;
231
+ return `claude --resume "${resumeSessionId}"${bypassFlag} || claude${bypassFlag}`;
225
232
  }
226
233
  return command;
227
234
  }
@@ -0,0 +1,309 @@
1
+ import assert from 'node:assert/strict';
2
+ import { EventEmitter } from 'node:events';
3
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import test from 'node:test';
7
+
8
+ import { closeConnection, initializeDatabase, sessionsDb } from '@/modules/database/index.js';
9
+ import { sessionsService } from '@/modules/providers/index.js';
10
+ import { handleChatConnection } from '@/modules/websocket/services/chat-websocket.service.js';
11
+ import { chatRunRegistry } from '@/modules/websocket/services/chat-run-registry.service.js';
12
+ import { connectedClients } from '@/modules/websocket/services/websocket-state.service.js';
13
+
14
+ const SESSION_ID = 'edit-session';
15
+
16
+ function createFakeSocket() {
17
+ const socket = new EventEmitter() as EventEmitter & {
18
+ readyState: number;
19
+ frames: Array<Record<string, unknown>>;
20
+ send: (data: string) => void;
21
+ };
22
+ socket.readyState = 1;
23
+ socket.frames = [];
24
+ socket.send = (data: string) => socket.frames.push(JSON.parse(data) as Record<string, unknown>);
25
+ return socket;
26
+ }
27
+
28
+ /** Two turns and an edit of the second, which is the transcript the gateway reads. */
29
+ const TRANSCRIPT_ROWS = [
30
+ {
31
+ type: 'user', uuid: 'e-u1', parentUuid: null, sessionId: SESSION_ID,
32
+ timestamp: '2026-08-23T10:00:00.000Z',
33
+ message: { role: 'user', content: [{ type: 'text', text: 'first' }] },
34
+ },
35
+ {
36
+ type: 'assistant', uuid: 'e-a1', parentUuid: 'e-u1', sessionId: SESSION_ID,
37
+ timestamp: '2026-08-23T10:00:01.000Z',
38
+ message: { role: 'assistant', model: 'claude-opus-5', content: [{ type: 'text', text: 'reply' }] },
39
+ },
40
+ {
41
+ type: 'user', uuid: 'e-u2', parentUuid: 'e-a1', sessionId: SESSION_ID,
42
+ timestamp: '2026-08-23T10:00:02.000Z',
43
+ message: { role: 'user', content: [{ type: 'text', text: 'second' }] },
44
+ },
45
+ ];
46
+
47
+ /**
48
+ * The same two turns as a Codex rollout. Codex rows carry no id of their own,
49
+ * so the edit anchor is the enclosing turn — which is also the unit its fork
50
+ * endpoint cuts at.
51
+ */
52
+ const CODEX_TRANSCRIPT_ROWS = [
53
+ { type: 'event_msg', payload: { type: 'task_started', turn_id: 'turn-a' } },
54
+ { type: 'event_msg', payload: { type: 'user_message', message: 'first' } },
55
+ { type: 'event_msg', payload: { type: 'task_complete', turn_id: 'turn-a' } },
56
+ { type: 'event_msg', payload: { type: 'task_started', turn_id: 'turn-b' } },
57
+ { type: 'event_msg', payload: { type: 'user_message', message: 'second' } },
58
+ { type: 'event_msg', payload: { type: 'task_complete', turn_id: 'turn-b' } },
59
+ ];
60
+
61
+ type RunCall = { provider: string; command: string; options: Record<string, unknown> };
62
+
63
+ /**
64
+ * Set by a test that needs a run to still be in flight when the next frame
65
+ * arrives; the stub runtime returns immediately otherwise. Released when the
66
+ * test ends, so no stub run is left pending for the rest of the file.
67
+ */
68
+ let holdRun: Promise<void> | null = null;
69
+ let releaseHeldRun: (() => void) | null = null;
70
+
71
+ function holdTheNextRun(): void {
72
+ holdRun = new Promise<void>((resolve) => { releaseHeldRun = resolve; });
73
+ }
74
+
75
+ async function withGateway(
76
+ provider: string,
77
+ runTest: (context: {
78
+ socket: ReturnType<typeof createFakeSocket>;
79
+ runs: RunCall[];
80
+ }) => Promise<void>,
81
+ rows: unknown[] = TRANSCRIPT_ROWS,
82
+ ): Promise<void> {
83
+ const previousDatabasePath = process.env.DATABASE_PATH;
84
+ const tempDirectory = await mkdtemp(path.join(os.tmpdir(), 'chat-edit-send-'));
85
+ const transcriptPath = path.join(tempDirectory, `${SESSION_ID}.jsonl`);
86
+ await writeFile(transcriptPath, `${rows.map((row) => JSON.stringify(row)).join('\n')}\n`, 'utf8');
87
+
88
+ closeConnection();
89
+ process.env.DATABASE_PATH = path.join(tempDirectory, 'auth.db');
90
+ await initializeDatabase();
91
+
92
+ const runs: RunCall[] = [];
93
+ const socket = createFakeSocket();
94
+
95
+ try {
96
+ const now = new Date().toISOString();
97
+ sessionsDb.createSession(SESSION_ID, provider, tempDirectory, 'Edit session', now, now, transcriptPath);
98
+
99
+ handleChatConnection(
100
+ socket as never,
101
+ { user: { id: 1 } } as never,
102
+ {
103
+ runtime: {
104
+ hasRuntime: () => true,
105
+ run: async (runProvider: string, command: string, options: Record<string, unknown>) => {
106
+ runs.push({ provider: runProvider, command, options });
107
+ if (holdRun) {
108
+ await holdRun;
109
+ }
110
+ },
111
+ } as never,
112
+ },
113
+ );
114
+
115
+ await runTest({ socket, runs });
116
+ } finally {
117
+ releaseHeldRun?.();
118
+ releaseHeldRun = null;
119
+ holdRun = null;
120
+ connectedClients.clear();
121
+ chatRunRegistry.clearAll();
122
+ closeConnection();
123
+ if (previousDatabasePath === undefined) {
124
+ delete process.env.DATABASE_PATH;
125
+ } else {
126
+ process.env.DATABASE_PATH = previousDatabasePath;
127
+ }
128
+ await rm(tempDirectory, { recursive: true, force: true });
129
+ }
130
+ }
131
+
132
+ /** The handler is async and the socket listener does not await it. */
133
+ const settle = () => new Promise((resolve) => { setTimeout(resolve, 30); });
134
+
135
+ test('an edit resumes through the turn before the one being replaced', async () => {
136
+ await withGateway('claude', async ({ socket, runs }) => {
137
+ socket.emit('message', JSON.stringify({
138
+ type: 'chat.edit-send',
139
+ sessionId: SESSION_ID,
140
+ anchorId: 'e-u2',
141
+ content: 'a better second prompt',
142
+ }));
143
+ await settle();
144
+
145
+ assert.equal(runs.length, 1);
146
+ assert.equal(runs[0].command, 'a better second prompt');
147
+ // Inclusive of the row it names, so this is the last turn KEPT.
148
+ assert.equal(runs[0].options.resumeAnchorId, 'e-a1');
149
+ assert.equal(runs[0].options.resumeFromScratch, false);
150
+ });
151
+ });
152
+
153
+ test('editing the first prompt starts the conversation over', async () => {
154
+ await withGateway('claude', async ({ socket, runs }) => {
155
+ socket.emit('message', JSON.stringify({
156
+ type: 'chat.edit-send',
157
+ sessionId: SESSION_ID,
158
+ anchorId: 'e-u1',
159
+ content: 'a better first prompt',
160
+ }));
161
+ await settle();
162
+
163
+ assert.equal(runs.length, 1);
164
+ assert.equal(runs[0].options.resumeAnchorId, undefined);
165
+ assert.equal(runs[0].options.resumeFromScratch, true);
166
+ });
167
+ });
168
+
169
+ test('every subscribed client is told to drop the superseded turns', async () => {
170
+ await withGateway('claude', async ({ socket }) => {
171
+ socket.emit('message', JSON.stringify({
172
+ type: 'chat.edit-send',
173
+ sessionId: SESSION_ID,
174
+ anchorId: 'e-u2',
175
+ content: 'replacement',
176
+ }));
177
+ await settle();
178
+
179
+ const truncation = socket.frames.find((frame) => frame.kind === 'history_truncated');
180
+ assert.ok(truncation, 'a history_truncated frame is emitted');
181
+ assert.equal(truncation?.anchorId, 'e-u2');
182
+ assert.equal(truncation?.sessionId, SESSION_ID);
183
+ // Sequenced like every other run event, so a reconnecting tab replays it.
184
+ assert.equal(typeof truncation?.seq, 'number');
185
+ });
186
+ });
187
+
188
+ test('an edit without an anchor is refused', async () => {
189
+ await withGateway('claude', async ({ socket, runs }) => {
190
+ socket.emit('message', JSON.stringify({
191
+ type: 'chat.edit-send',
192
+ sessionId: SESSION_ID,
193
+ content: 'no anchor',
194
+ }));
195
+ await settle();
196
+
197
+ assert.equal(runs.length, 0);
198
+ assert.equal(socket.frames.at(-1)?.code, 'ANCHOR_REQUIRED');
199
+ });
200
+ });
201
+
202
+ test('an anchor the transcript does not hold is refused', async () => {
203
+ await withGateway('claude', async ({ socket, runs }) => {
204
+ socket.emit('message', JSON.stringify({
205
+ type: 'chat.edit-send',
206
+ sessionId: SESSION_ID,
207
+ anchorId: 'not-in-transcript',
208
+ content: 'replacement',
209
+ }));
210
+ await settle();
211
+
212
+ assert.equal(runs.length, 0);
213
+ assert.equal(socket.frames.at(-1)?.code, 'ANCHOR_NOT_FOUND');
214
+ });
215
+ });
216
+
217
+ test('a provider that cannot re-run from a point is refused rather than sending a new message', async () => {
218
+ await withGateway('cursor', async ({ socket, runs }) => {
219
+ socket.emit('message', JSON.stringify({
220
+ type: 'chat.edit-send',
221
+ sessionId: SESSION_ID,
222
+ anchorId: 'e-u2',
223
+ content: 'replacement',
224
+ }));
225
+ await settle();
226
+
227
+ assert.equal(runs.length, 0);
228
+ assert.equal(socket.frames.at(-1)?.code, 'EDIT_NOT_SUPPORTED');
229
+ });
230
+ });
231
+
232
+ test('a refused send never rewinds the conversation', async () => {
233
+ await withGateway('codex', async ({ socket, runs }) => {
234
+ // The rewind moves the session onto a different provider transcript and
235
+ // cannot be undone, so a send the gateway is about to refuse must not
236
+ // reach it. A run already in flight is the realistic way that happens: a
237
+ // scheduled message, or another device.
238
+ const realRewind = sessionsService.rewindSessionForEdit;
239
+ let rewound = false;
240
+ sessionsService.rewindSessionForEdit = async () => { rewound = true; };
241
+ holdTheNextRun();
242
+
243
+ try {
244
+ socket.emit('message', JSON.stringify({
245
+ type: 'chat.send',
246
+ sessionId: SESSION_ID,
247
+ content: 'a turn that is already running',
248
+ }));
249
+ await settle();
250
+ assert.equal(runs.length, 1);
251
+
252
+ socket.emit('message', JSON.stringify({
253
+ type: 'chat.edit-send',
254
+ sessionId: SESSION_ID,
255
+ anchorId: 'turn-b',
256
+ content: 'an edit that arrives too late',
257
+ }));
258
+ await settle();
259
+ } finally {
260
+ sessionsService.rewindSessionForEdit = realRewind;
261
+ }
262
+
263
+ assert.equal(rewound, false);
264
+ assert.equal(runs.length, 1);
265
+ assert.equal(socket.frames.at(-1)?.code, 'RUN_IN_PROGRESS');
266
+ }, CODEX_TRANSCRIPT_ROWS);
267
+ });
268
+
269
+ test('a provider that has to branch to rewind is rewound before the run, not during it', async () => {
270
+ await withGateway('codex', async ({ socket, runs }) => {
271
+ // The rewind itself belongs to the provider and is covered there; what
272
+ // this asserts is the gateway's half — that a provider which reports it
273
+ // rewound gets an ordinary run instead of one carrying a resume anchor its
274
+ // runtime would not know what to do with.
275
+ const realRewind = sessionsService.rewindSessionForEdit;
276
+ const rewindCalls: unknown[] = [];
277
+ let truncatedBeforeRewind = false;
278
+ sessionsService.rewindSessionForEdit = async (sessionId: string, keepThroughId: string | null) => {
279
+ truncatedBeforeRewind = socket.frames.some((frame) => frame.kind === 'history_truncated');
280
+ rewindCalls.push({ sessionId, keepThroughId });
281
+ };
282
+
283
+ try {
284
+ socket.emit('message', JSON.stringify({
285
+ type: 'chat.edit-send',
286
+ sessionId: SESSION_ID,
287
+ anchorId: 'turn-b',
288
+ content: 'a better second prompt',
289
+ }));
290
+ await settle();
291
+ } finally {
292
+ sessionsService.rewindSessionForEdit = realRewind;
293
+ }
294
+
295
+ // Resolved from the real Codex rollout: the turn before the edited one.
296
+ assert.deepEqual(rewindCalls, [{ sessionId: SESSION_ID, keepThroughId: 'turn-a' }]);
297
+ assert.equal(runs.length, 1);
298
+ assert.equal(runs[0].command, 'a better second prompt');
299
+ assert.equal(runs[0].options.resumeAnchorId, undefined);
300
+ assert.equal(runs[0].options.resumeFromScratch, undefined);
301
+
302
+ // Clients still hear about the cut; how it was made is the server's
303
+ // business. And they hear about it first: a rewind that branches waits on
304
+ // a spawned process, and holding the frame until it returned left the
305
+ // replaced message on screen for about a second.
306
+ assert.ok(socket.frames.some((frame) => frame.kind === 'history_truncated'));
307
+ assert.equal(truncatedBeforeRewind, true);
308
+ }, CODEX_TRANSCRIPT_ROWS);
309
+ });
@@ -70,7 +70,7 @@ test('live events are remapped to the app session id and sequenced', async () =>
70
70
  });
71
71
 
72
72
  test('session_created is swallowed and persisted as the provider-id mapping', async () => {
73
- await withIsolatedDatabase(() => {
73
+ await withIsolatedDatabase(async () => {
74
74
  sessionsDb.createAppSession('app-run-2', 'cursor', '/workspace/demo');
75
75
  const connection = new FakeConnection();
76
76
  connectedClients.add(connection as never);
@@ -90,6 +90,10 @@ test('session_created is swallowed and persisted as the provider-id mapping', as
90
90
  newSessionId: 'cursor-native-7',
91
91
  });
92
92
 
93
+ // The upsert is broadcast without blocking the run: resolving the owning
94
+ // project's display name is async, so let that settle before asserting.
95
+ await new Promise((resolve) => { setTimeout(resolve, 0); });
96
+
93
97
  // The provider-native event itself is never forwarded...
94
98
  const sessionUpserts = connection.frames.filter((frame) => frame.kind === 'session_upserted');
95
99
  assert.equal(sessionUpserts.length, 1);
@@ -222,7 +226,7 @@ test('replayEvents returns only events after the requested seq', async () => {
222
226
  });
223
227
  });
224
228
 
225
- test('attachConnection reroutes the live stream to a new socket', async () => {
229
+ test('attachConnection adds a socket without cutting off the ones already watching', async () => {
226
230
  await withIsolatedDatabase(() => {
227
231
  sessionsDb.createAppSession('app-run-5', 'opencode', '/workspace/demo');
228
232
  const firstConnection = new FakeConnection();
@@ -237,15 +241,42 @@ test('attachConnection reroutes the live stream to a new socket', async () => {
237
241
 
238
242
  run.writer.send({ kind: 'stream_delta', provider: 'opencode', sessionId: 'o', content: 'before' });
239
243
 
244
+ // A second tab on the same session subscribes mid-run.
240
245
  const secondConnection = new FakeConnection();
241
246
  assert.equal(chatRunRegistry.attachConnection('app-run-5', secondConnection), true);
242
247
  run.writer.send({ kind: 'stream_delta', provider: 'opencode', sessionId: 'o', content: 'after' });
243
248
 
244
- assert.deepEqual(firstConnection.frames.map((frame) => frame.content), ['before']);
249
+ assert.deepEqual(firstConnection.frames.map((frame) => frame.content), ['before', 'after']);
245
250
  assert.deepEqual(secondConnection.frames.map((frame) => frame.content), ['after']);
246
251
  });
247
252
  });
248
253
 
254
+ test('a refreshed tab stops receiving once its old socket is closed', async () => {
255
+ await withIsolatedDatabase(() => {
256
+ sessionsDb.createAppSession('app-run-5b', 'opencode', '/workspace/demo');
257
+ const staleConnection = new FakeConnection();
258
+ const run = chatRunRegistry.startRun({
259
+ appSessionId: 'app-run-5b',
260
+ provider: 'opencode',
261
+ providerSessionId: null,
262
+ connection: staleConnection,
263
+ userId: null,
264
+ });
265
+ assert.ok(run);
266
+
267
+ // The page reloads: the original socket closes and the fresh one subscribes.
268
+ staleConnection.readyState = 3;
269
+ const reloadedConnection = new FakeConnection();
270
+ assert.equal(chatRunRegistry.attachConnection('app-run-5b', reloadedConnection), true);
271
+
272
+ run.writer.send({ kind: 'stream_delta', provider: 'opencode', sessionId: 'o', content: 'after' });
273
+ run.writer.send({ kind: 'stream_delta', provider: 'opencode', sessionId: 'o', content: 'later' });
274
+
275
+ assert.deepEqual(staleConnection.frames, []);
276
+ assert.deepEqual(reloadedConnection.frames.map((frame) => frame.content), ['after', 'later']);
277
+ });
278
+ });
279
+
249
280
  test('startRun rejects a second concurrent run for the same session', async () => {
250
281
  await withIsolatedDatabase(() => {
251
282
  sessionsDb.createAppSession('app-run-6', 'opencode', '/workspace/demo');
@@ -0,0 +1,138 @@
1
+ import assert from 'node:assert/strict';
2
+ import { mkdtemp, rm } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import path from 'node:path';
5
+ import test from 'node:test';
6
+
7
+ import { closeConnection, initializeDatabase, sessionsDb } from '@/modules/database/index.js';
8
+ import {
9
+ broadcastSessionUpserted,
10
+ broadcastSessionUpsertedBatch,
11
+ } from '@/modules/websocket/services/session-upsert-broadcast.service.js';
12
+ import { connectedClients } from '@/modules/websocket/services/websocket-state.service.js';
13
+
14
+ class FakeConnection {
15
+ readyState = 1; // WS_OPEN_STATE
16
+ frames: Array<Record<string, unknown>> = [];
17
+
18
+ send(data: string): void {
19
+ this.frames.push(JSON.parse(data) as Record<string, unknown>);
20
+ }
21
+ }
22
+
23
+ async function withIsolatedDatabase(runTest: () => void | Promise<void>): Promise<void> {
24
+ const previousDatabasePath = process.env.DATABASE_PATH;
25
+ const tempDirectory = await mkdtemp(path.join(tmpdir(), 'session-upsert-broadcast-'));
26
+
27
+ closeConnection();
28
+ process.env.DATABASE_PATH = path.join(tempDirectory, 'auth.db');
29
+ await initializeDatabase();
30
+
31
+ try {
32
+ await runTest();
33
+ } finally {
34
+ connectedClients.clear();
35
+ closeConnection();
36
+ if (previousDatabasePath === undefined) {
37
+ delete process.env.DATABASE_PATH;
38
+ } else {
39
+ process.env.DATABASE_PATH = previousDatabasePath;
40
+ }
41
+ await rm(tempDirectory, { recursive: true, force: true });
42
+ }
43
+ }
44
+
45
+ test('an upsert always carries the provider session id', async () => {
46
+ await withIsolatedDatabase(async () => {
47
+ sessionsDb.createAppSession('app-1', 'opencode', '/workspace/demo');
48
+ sessionsDb.assignProviderSessionId('app-1', 'native-1');
49
+
50
+ const connection = new FakeConnection();
51
+ connectedClients.add(connection as never);
52
+
53
+ await broadcastSessionUpserted('app-1');
54
+
55
+ assert.equal(connection.frames.length, 1);
56
+ assert.equal(connection.frames[0].kind, 'session_upserted');
57
+ assert.equal(connection.frames[0].sessionId, 'app-1');
58
+ assert.equal(connection.frames[0].providerSessionId, 'native-1');
59
+ });
60
+ });
61
+
62
+ test('the watcher path resolves a provider-native id to the same canonical event', async () => {
63
+ await withIsolatedDatabase(async () => {
64
+ sessionsDb.createAppSession('app-2', 'opencode', '/workspace/demo');
65
+ sessionsDb.assignProviderSessionId('app-2', 'native-2');
66
+
67
+ const connection = new FakeConnection();
68
+ connectedClients.add(connection as never);
69
+
70
+ // The sessions watcher only ever knows the id written in the transcript.
71
+ await broadcastSessionUpsertedBatch(['native-2']);
72
+
73
+ assert.equal(connection.frames.length, 1);
74
+ assert.equal(connection.frames[0].sessionId, 'app-2');
75
+ assert.equal(connection.frames[0].providerSessionId, 'native-2');
76
+ });
77
+ });
78
+
79
+ test('a session with no provider id yet reports null rather than omitting the field', async () => {
80
+ await withIsolatedDatabase(async () => {
81
+ sessionsDb.createAppSession('app-3', 'claude', '/workspace/demo');
82
+
83
+ const connection = new FakeConnection();
84
+ connectedClients.add(connection as never);
85
+
86
+ await broadcastSessionUpserted('app-3');
87
+
88
+ assert.equal(connection.frames.length, 1);
89
+ assert.ok('providerSessionId' in connection.frames[0]);
90
+ assert.equal(connection.frames[0].providerSessionId, null);
91
+ });
92
+ });
93
+
94
+ test('an unresolvable id broadcasts nothing', async () => {
95
+ await withIsolatedDatabase(async () => {
96
+ const connection = new FakeConnection();
97
+ connectedClients.add(connection as never);
98
+
99
+ await broadcastSessionUpserted('does-not-exist');
100
+ await broadcastSessionUpsertedBatch(['also-missing']);
101
+
102
+ assert.deepEqual(connection.frames, []);
103
+ });
104
+ });
105
+
106
+ test('a batch delivers every resolvable session and skips the rest', async () => {
107
+ await withIsolatedDatabase(async () => {
108
+ sessionsDb.createAppSession('app-4', 'claude', '/workspace/demo');
109
+ sessionsDb.createAppSession('app-5', 'claude', '/workspace/demo');
110
+
111
+ const connection = new FakeConnection();
112
+ connectedClients.add(connection as never);
113
+
114
+ await broadcastSessionUpsertedBatch(['app-4', 'missing', 'app-5']);
115
+
116
+ assert.deepEqual(
117
+ connection.frames.map((frame) => frame.sessionId),
118
+ ['app-4', 'app-5'],
119
+ );
120
+ });
121
+ });
122
+
123
+ test('a closed socket is skipped', async () => {
124
+ await withIsolatedDatabase(async () => {
125
+ sessionsDb.createAppSession('app-6', 'claude', '/workspace/demo');
126
+
127
+ const open = new FakeConnection();
128
+ const closing = new FakeConnection();
129
+ closing.readyState = 3;
130
+ connectedClients.add(open as never);
131
+ connectedClients.add(closing as never);
132
+
133
+ await broadcastSessionUpserted('app-6');
134
+
135
+ assert.equal(open.frames.length, 1);
136
+ assert.deepEqual(closing.frames, []);
137
+ });
138
+ });