@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
@@ -1,9 +1,45 @@
1
1
  import express from 'express';
2
+ import type { Response } from 'express';
2
3
 
3
4
  import { browserUseService } from '@/modules/browser-use/browser-use.service.js';
4
5
 
5
6
  const router = express.Router();
6
7
 
8
+ /**
9
+ * Stable machine-readable error codes for the browser-use API.
10
+ *
11
+ * The server always replies with the structured envelope used by AppError
12
+ * (`{ success: false, error: { code, message, details } }`). Clients map
13
+ * `code` to localized copy; `message` is an English fallback and `details`
14
+ * may carry the underlying error text for diagnostics.
15
+ */
16
+ export const BROWSER_USE_ERROR_CODES = {
17
+ STATUS_LOAD_FAILED: 'BROWSER_USE_STATUS_LOAD_FAILED',
18
+ SETTINGS_LOAD_FAILED: 'BROWSER_USE_SETTINGS_LOAD_FAILED',
19
+ SETTINGS_SAVE_FAILED: 'BROWSER_USE_SETTINGS_SAVE_FAILED',
20
+ RUNTIME_INSTALL_FAILED: 'BROWSER_USE_RUNTIME_INSTALL_FAILED',
21
+ SESSIONS_LOAD_FAILED: 'BROWSER_USE_SESSIONS_LOAD_FAILED',
22
+ SESSION_STOP_FAILED: 'BROWSER_USE_SESSION_STOP_FAILED',
23
+ SESSION_DELETE_FAILED: 'BROWSER_USE_SESSION_DELETE_FAILED',
24
+ } as const;
25
+
26
+ function sendError(
27
+ res: Response,
28
+ statusCode: number,
29
+ code: string,
30
+ message: string,
31
+ details?: unknown,
32
+ ): void {
33
+ res.status(statusCode).json({
34
+ success: false,
35
+ error: {
36
+ code,
37
+ message,
38
+ details,
39
+ },
40
+ });
41
+ }
42
+
7
43
  function readParam(value: string | string[] | undefined): string {
8
44
  return Array.isArray(value) ? value[0] || '' : value || '';
9
45
  }
@@ -12,10 +48,13 @@ router.get('/status', async (_req, res) => {
12
48
  try {
13
49
  res.json({ success: true, data: await browserUseService.getStatus() });
14
50
  } catch (error) {
15
- res.status(500).json({
16
- success: false,
17
- error: error instanceof Error ? error.message : 'Failed to load Browser status.',
18
- });
51
+ sendError(
52
+ res,
53
+ 500,
54
+ BROWSER_USE_ERROR_CODES.STATUS_LOAD_FAILED,
55
+ 'Failed to load Browser status.',
56
+ error instanceof Error ? error.message : undefined,
57
+ );
19
58
  }
20
59
  });
21
60
 
@@ -23,10 +62,13 @@ router.get('/settings', async (_req, res) => {
23
62
  try {
24
63
  res.json({ success: true, data: { settings: await browserUseService.getSettings() } });
25
64
  } catch (error) {
26
- res.status(500).json({
27
- success: false,
28
- error: error instanceof Error ? error.message : 'Failed to load Browser settings.',
29
- });
65
+ sendError(
66
+ res,
67
+ 500,
68
+ BROWSER_USE_ERROR_CODES.SETTINGS_LOAD_FAILED,
69
+ 'Failed to load Browser settings.',
70
+ error instanceof Error ? error.message : undefined,
71
+ );
30
72
  }
31
73
  });
32
74
 
@@ -35,26 +77,38 @@ router.put('/settings', async (req, res) => {
35
77
  const settings = await browserUseService.updateSettings(req.body || {});
36
78
  res.json({ success: true, data: { settings } });
37
79
  } catch (error) {
38
- res.status(400).json({
39
- success: false,
40
- error: error instanceof Error ? error.message : 'Failed to save Browser settings.',
41
- });
80
+ sendError(
81
+ res,
82
+ 400,
83
+ BROWSER_USE_ERROR_CODES.SETTINGS_SAVE_FAILED,
84
+ 'Failed to save Browser settings.',
85
+ error instanceof Error ? error.message : undefined,
86
+ );
42
87
  }
43
88
  });
44
89
 
45
90
  router.post('/runtime/install', async (_req, res) => {
46
91
  try {
47
92
  const result = await browserUseService.installRuntime();
48
- res.status(result.success ? 200 : 500).json({
49
- success: result.success,
50
- data: result,
51
- error: result.success ? undefined : result.message,
52
- });
93
+ if (result.success) {
94
+ res.json({ success: true, data: result });
95
+ return;
96
+ }
97
+ sendError(
98
+ res,
99
+ 500,
100
+ BROWSER_USE_ERROR_CODES.RUNTIME_INSTALL_FAILED,
101
+ 'Failed to install Browser runtime.',
102
+ result.message,
103
+ );
53
104
  } catch (error) {
54
- res.status(500).json({
55
- success: false,
56
- error: error instanceof Error ? error.message : 'Failed to install Browser runtime.',
57
- });
105
+ sendError(
106
+ res,
107
+ 500,
108
+ BROWSER_USE_ERROR_CODES.RUNTIME_INSTALL_FAILED,
109
+ 'Failed to install Browser runtime.',
110
+ error instanceof Error ? error.message : undefined,
111
+ );
58
112
  }
59
113
  });
60
114
 
@@ -62,10 +116,13 @@ router.get('/sessions', async (_req, res) => {
62
116
  try {
63
117
  res.json({ success: true, data: { sessions: await browserUseService.listSessions() } });
64
118
  } catch (error) {
65
- res.status(401).json({
66
- success: false,
67
- error: error instanceof Error ? error.message : 'Failed to list browser sessions.',
68
- });
119
+ sendError(
120
+ res,
121
+ 401,
122
+ BROWSER_USE_ERROR_CODES.SESSIONS_LOAD_FAILED,
123
+ 'Failed to list browser sessions.',
124
+ error instanceof Error ? error.message : undefined,
125
+ );
69
126
  }
70
127
  });
71
128
 
@@ -74,10 +131,13 @@ router.post('/sessions/:sessionId/stop', async (req, res) => {
74
131
  const result = await browserUseService.stopSession(readParam(req.params.sessionId));
75
132
  res.json({ success: true, data: result });
76
133
  } catch (error) {
77
- res.status(400).json({
78
- success: false,
79
- error: error instanceof Error ? error.message : 'Failed to stop browser session.',
80
- });
134
+ sendError(
135
+ res,
136
+ 400,
137
+ BROWSER_USE_ERROR_CODES.SESSION_STOP_FAILED,
138
+ 'Failed to stop browser session.',
139
+ error instanceof Error ? error.message : undefined,
140
+ );
81
141
  }
82
142
  });
83
143
 
@@ -86,10 +146,13 @@ router.delete('/sessions/:sessionId', async (req, res) => {
86
146
  const result = await browserUseService.deleteSession(readParam(req.params.sessionId));
87
147
  res.json({ success: true, data: result });
88
148
  } catch (error) {
89
- res.status(400).json({
90
- success: false,
91
- error: error instanceof Error ? error.message : 'Failed to delete browser session.',
92
- });
149
+ sendError(
150
+ res,
151
+ 400,
152
+ BROWSER_USE_ERROR_CODES.SESSION_DELETE_FAILED,
153
+ 'Failed to delete browser session.',
154
+ error instanceof Error ? error.message : undefined,
155
+ );
93
156
  }
94
157
  });
95
158
 
@@ -10,11 +10,12 @@
10
10
  * `getConnection()` to obtain the shared singleton.
11
11
  */
12
12
 
13
- import Database from 'better-sqlite3';
14
13
  import fs from 'fs';
15
14
  import path from 'path';
16
15
  import { fileURLToPath } from 'url';
17
16
 
17
+ import Database from 'better-sqlite3';
18
+
18
19
  import { APP_CONFIG_TABLE_SCHEMA_SQL } from '@/modules/database/schema.js';
19
20
 
20
21
  const __filename = fileURLToPath(import.meta.url);
@@ -12,6 +12,16 @@ export { providerModelsDb } from '@/modules/database/repositories/provider-model
12
12
  export { projectsDb } from '@/modules/database/repositories/projects.db.js';
13
13
  export { pushSubscriptionsDb } from '@/modules/database/repositories/push-subscriptions.js';
14
14
  export { scanStateDb } from '@/modules/database/repositories/scan-state.db.js';
15
+ // sessionDraftsDb: used by User for drafts and Scheduled Messages for server-owned queued turns.
16
+ export { sessionDraftsDb } from '@/modules/database/repositories/session-drafts.db.js';
17
+ export type {
18
+ QueuedSessionMessageRecord,
19
+ SessionDraftRecord,
20
+ } from '@/modules/database/repositories/session-drafts.db.js';
15
21
  export { sessionsDb } from '@/modules/database/repositories/sessions.db.js';
16
22
  export { userDb } from '@/modules/database/repositories/users.js';
23
+ // userPreferencesDb: used by the User module to persist the settings that used to live in browser localStorage.
24
+ export { userPreferencesDb } from '@/modules/database/repositories/user-preferences.db.js';
17
25
  export { vapidKeysDb } from '@/modules/database/repositories/vapid-keys.js';
26
+ export { scheduledMessagesDb } from './repositories/scheduled-messages.db.js';
27
+ export type { ScheduledMessageRow, ScheduledMessageStatus } from './repositories/scheduled-messages.db.js';
@@ -7,7 +7,11 @@ import {
7
7
  PROJECTS_TABLE_SCHEMA_SQL,
8
8
  PROVIDER_MODELS_TABLE_SCHEMA_SQL,
9
9
  PUSH_SUBSCRIPTIONS_TABLE_SCHEMA_SQL,
10
+ SESSION_DRAFTS_TABLE_SCHEMA_SQL,
11
+ SUPERSEDED_PROVIDER_SESSIONS_TABLE_SCHEMA_SQL,
12
+ SCHEDULED_MESSAGES_TABLE_SCHEMA_SQL,
10
13
  SESSIONS_TABLE_SCHEMA_SQL,
14
+ USER_PREFERENCES_TABLE_SCHEMA_SQL,
11
15
  USER_NOTIFICATION_PREFERENCES_TABLE_SCHEMA_SQL,
12
16
  VAPID_KEYS_TABLE_SCHEMA_SQL,
13
17
  } from '@/modules/database/schema.js';
@@ -403,6 +407,29 @@ const addProviderSessionIdMapping = (db: Database): void => {
403
407
  `);
404
408
  };
405
409
 
410
+ /**
411
+ * Adds the `forked_from_session_id` column recording where a branched session
412
+ * came from.
413
+ *
414
+ * Nothing is backfilled: a session that predates forking was not forked.
415
+ */
416
+ /**
417
+ * Adds the transcript path to the superseded-session record.
418
+ *
419
+ * Only rows written before this column existed lack it, and there is nothing
420
+ * to backfill from — the session stopped pointing at that file when the row
421
+ * was created — so they keep a NULL and the delete path skips them.
422
+ */
423
+ const addSupersededTranscriptPathColumn = (db: Database): void => {
424
+ const columnNames = getTableInfo(db, 'superseded_provider_sessions').map((column) => column.name);
425
+ addColumnToTableIfNotExists(db, 'superseded_provider_sessions', columnNames, 'jsonl_path', 'TEXT');
426
+ };
427
+
428
+ const addForkedFromSessionIdColumn = (db: Database): void => {
429
+ const columnNames = getTableInfo(db, 'sessions').map((column) => column.name);
430
+ addColumnToTableIfNotExists(db, 'sessions', columnNames, 'forked_from_session_id', 'TEXT');
431
+ };
432
+
406
433
  /**
407
434
  * Adds the `model` column that records which model each session runs with.
408
435
  *
@@ -477,6 +504,10 @@ export const runMigrations = (db: Database) => {
477
504
  CREATE INDEX IF NOT EXISTS idx_provider_models_provider_order
478
505
  ON provider_models(provider, sort_order, id)
479
506
  `);
507
+ db.exec(USER_PREFERENCES_TABLE_SCHEMA_SQL);
508
+ db.exec(SESSION_DRAFTS_TABLE_SCHEMA_SQL);
509
+ db.exec(SUPERSEDED_PROVIDER_SESSIONS_TABLE_SCHEMA_SQL);
510
+ addSupersededTranscriptPathColumn(db);
480
511
 
481
512
  db.exec(PROJECTS_TABLE_SCHEMA_SQL);
482
513
  rebuildProjectsTableWithPrimaryKeySchema(db);
@@ -487,11 +518,17 @@ export const runMigrations = (db: Database) => {
487
518
  addProviderSessionIdMapping(db);
488
519
  addSessionModelColumn(db);
489
520
  addSessionEffortColumn(db);
521
+ addForkedFromSessionIdColumn(db);
490
522
  ensureProjectsForSessionPaths(db);
523
+ db.exec(SCHEDULED_MESSAGES_TABLE_SCHEMA_SQL);
491
524
 
492
525
  db.exec('CREATE INDEX IF NOT EXISTS idx_session_ids_lookup ON sessions(session_id)');
493
526
  db.exec('CREATE INDEX IF NOT EXISTS idx_sessions_provider_session_id ON sessions(provider_session_id)');
494
527
  db.exec('CREATE INDEX IF NOT EXISTS idx_sessions_project_path ON sessions(project_path)');
528
+ db.exec('CREATE INDEX IF NOT EXISTS idx_sessions_forked_from ON sessions(forked_from_session_id)');
529
+ // The due-message poll runs on a timer; without this it table-scans.
530
+ db.exec('CREATE INDEX IF NOT EXISTS idx_scheduled_messages_due ON scheduled_messages(status, scheduled_for)');
531
+ db.exec('CREATE INDEX IF NOT EXISTS idx_scheduled_messages_session ON scheduled_messages(session_id)');
495
532
  db.exec('CREATE INDEX IF NOT EXISTS idx_sessions_is_archived ON sessions(isArchived)');
496
533
  db.exec('CREATE INDEX IF NOT EXISTS idx_projects_is_starred ON projects(isStarred)');
497
534
  db.exec('CREATE INDEX IF NOT EXISTS idx_projects_is_archived ON projects(isArchived)');
@@ -0,0 +1,127 @@
1
+ import { randomUUID } from 'node:crypto';
2
+
3
+ import { getConnection } from '@/modules/database/connection.js';
4
+
5
+ export type ScheduledMessageStatus = 'pending' | 'sent' | 'failed' | 'cancelled';
6
+
7
+ export type ScheduledMessageRow = {
8
+ id: string;
9
+ user_id: number;
10
+ session_id: string;
11
+ content: string;
12
+ options: string;
13
+ scheduled_for: string;
14
+ status: ScheduledMessageStatus;
15
+ failure_reason: string | null;
16
+ created_at: string;
17
+ updated_at: string;
18
+ };
19
+
20
+ const COLUMNS =
21
+ 'id, user_id, session_id, content, options, scheduled_for, status, failure_reason, created_at, updated_at';
22
+
23
+ export const scheduledMessagesDb = {
24
+ create(input: {
25
+ userId: number;
26
+ sessionId: string;
27
+ content: string;
28
+ options: unknown;
29
+ scheduledFor: Date;
30
+ }): ScheduledMessageRow {
31
+ const db = getConnection();
32
+ const id = randomUUID();
33
+
34
+ db.prepare(
35
+ `INSERT INTO scheduled_messages (id, user_id, session_id, content, options, scheduled_for, status)
36
+ VALUES (?, ?, ?, ?, ?, ?, 'pending')`
37
+ ).run(
38
+ id,
39
+ input.userId,
40
+ input.sessionId,
41
+ input.content,
42
+ JSON.stringify(input.options ?? {}),
43
+ input.scheduledFor.toISOString(),
44
+ );
45
+
46
+ return db.prepare(`SELECT ${COLUMNS} FROM scheduled_messages WHERE id = ?`).get(id) as ScheduledMessageRow;
47
+ },
48
+
49
+ /** Everything still to come or recently resolved, newest schedule first. */
50
+ listForSession(userId: number, sessionId: string): ScheduledMessageRow[] {
51
+ return getConnection()
52
+ .prepare(
53
+ `SELECT ${COLUMNS} FROM scheduled_messages
54
+ WHERE user_id = ? AND session_id = ?
55
+ ORDER BY scheduled_for ASC`
56
+ )
57
+ .all(userId, sessionId) as ScheduledMessageRow[];
58
+ },
59
+
60
+ listPendingForUser(userId: number): ScheduledMessageRow[] {
61
+ return getConnection()
62
+ .prepare(
63
+ `SELECT ${COLUMNS} FROM scheduled_messages
64
+ WHERE user_id = ? AND status = 'pending'
65
+ ORDER BY scheduled_for ASC`
66
+ )
67
+ .all(userId) as ScheduledMessageRow[];
68
+ },
69
+
70
+ /**
71
+ * Claims every message whose time has passed, marking them in the same
72
+ * statement that selects them.
73
+ *
74
+ * Claiming is what makes a missed schedule work: the server can be down at
75
+ * the moment a message was due, and the next poll after it starts picks the
76
+ * message up instead of skipping it. Doing it in one transaction is what
77
+ * stops two overlapping polls from sending the same message twice.
78
+ */
79
+ claimDue(now: Date): ScheduledMessageRow[] {
80
+ const db = getConnection();
81
+ const nowIso = now.toISOString();
82
+
83
+ return db.transaction(() => {
84
+ const due = db
85
+ .prepare(
86
+ `SELECT ${COLUMNS} FROM scheduled_messages
87
+ WHERE status = 'pending' AND scheduled_for <= ?
88
+ ORDER BY scheduled_for ASC`
89
+ )
90
+ .all(nowIso) as ScheduledMessageRow[];
91
+
92
+ for (const row of due) {
93
+ db.prepare(
94
+ `UPDATE scheduled_messages SET status = 'sent', updated_at = CURRENT_TIMESTAMP WHERE id = ?`
95
+ ).run(row.id);
96
+ }
97
+
98
+ return due;
99
+ })();
100
+ },
101
+
102
+ markFailed(id: string, reason: string): void {
103
+ getConnection()
104
+ .prepare(
105
+ `UPDATE scheduled_messages
106
+ SET status = 'failed', failure_reason = ?, updated_at = CURRENT_TIMESTAMP
107
+ WHERE id = ?`
108
+ )
109
+ .run(reason.slice(0, 500), id);
110
+ },
111
+
112
+ /**
113
+ * Cancels a pending message, or dismisses a failed one so its banner goes
114
+ * away. Returns false when it had already fired successfully.
115
+ */
116
+ cancel(userId: number, id: string): boolean {
117
+ const result = getConnection()
118
+ .prepare(
119
+ `UPDATE scheduled_messages
120
+ SET status = 'cancelled', updated_at = CURRENT_TIMESTAMP
121
+ WHERE id = ? AND user_id = ? AND status IN ('pending', 'failed')`
122
+ )
123
+ .run(id, userId);
124
+
125
+ return result.changes > 0;
126
+ },
127
+ };
@@ -0,0 +1,171 @@
1
+ import { getConnection } from '@/modules/database/connection.js';
2
+
3
+ /**
4
+ * One chat scope's unsent state: the text still in the composer, plus the
5
+ * message queued behind an in-flight turn. Both are optional — a scope can
6
+ * hold only a draft, only a queued message, or both.
7
+ */
8
+ export type SessionDraftRecord = {
9
+ scope: string;
10
+ text: string;
11
+ queuedMessage: unknown | null;
12
+ updatedAt: string;
13
+ };
14
+
15
+ type DraftRow = {
16
+ draft_scope: string;
17
+ draft_text: string;
18
+ queued_message: string | null;
19
+ updated_at: string;
20
+ };
21
+
22
+ /** A server-owned queued turn together with the exact stored value used to claim it once. */
23
+ export type QueuedSessionMessageRecord = {
24
+ userId: number;
25
+ sessionId: string;
26
+ queuedMessage: unknown;
27
+ claimToken: string;
28
+ };
29
+
30
+ type QueuedMessageRow = {
31
+ user_id: number;
32
+ draft_scope: string;
33
+ queued_message: string;
34
+ };
35
+
36
+ /** A queued message that no longer parses is treated as absent, not fatal. */
37
+ function parseQueuedMessage(raw: string | null): unknown | null {
38
+ if (!raw) {
39
+ return null;
40
+ }
41
+
42
+ try {
43
+ return JSON.parse(raw) as unknown;
44
+ } catch {
45
+ return null;
46
+ }
47
+ }
48
+
49
+ function toRecord(row: DraftRow): SessionDraftRecord {
50
+ return {
51
+ scope: row.draft_scope,
52
+ text: row.draft_text,
53
+ queuedMessage: parseQueuedMessage(row.queued_message),
54
+ updatedAt: row.updated_at,
55
+ };
56
+ }
57
+
58
+ export const sessionDraftsDb = {
59
+ /**
60
+ * Returns every draft the user has, newest first.
61
+ *
62
+ * The client pulls the whole set once per load: drafts are short strings, and
63
+ * having them all up front means switching sessions restores a draft written
64
+ * on another device without a round trip.
65
+ */
66
+ getDrafts(userId: number): SessionDraftRecord[] {
67
+ const db = getConnection();
68
+ const rows = db
69
+ .prepare(
70
+ `SELECT draft_scope, draft_text, queued_message, updated_at
71
+ FROM session_drafts
72
+ WHERE user_id = ?
73
+ ORDER BY datetime(updated_at) DESC`
74
+ )
75
+ .all(userId) as DraftRow[];
76
+
77
+ return rows.map(toRecord);
78
+ },
79
+
80
+ /** Lists persisted queued turns whose scopes are real chat sessions. */
81
+ listQueuedMessages(): QueuedSessionMessageRecord[] {
82
+ const rows = getConnection()
83
+ .prepare(
84
+ `SELECT drafts.user_id, drafts.draft_scope, drafts.queued_message
85
+ FROM session_drafts AS drafts
86
+ INNER JOIN sessions ON sessions.session_id = drafts.draft_scope
87
+ WHERE drafts.queued_message IS NOT NULL`
88
+ )
89
+ .all() as QueuedMessageRow[];
90
+
91
+ return rows.map((row) => ({
92
+ userId: row.user_id,
93
+ sessionId: row.draft_scope,
94
+ queuedMessage: parseQueuedMessage(row.queued_message),
95
+ claimToken: row.queued_message,
96
+ }));
97
+ },
98
+
99
+ /** Atomically removes a queued turn only if it has not been edited since listing. */
100
+ claimQueuedMessage(candidate: QueuedSessionMessageRecord): boolean {
101
+ const result = getConnection()
102
+ .prepare(
103
+ `UPDATE session_drafts
104
+ SET queued_message = NULL, updated_at = CURRENT_TIMESTAMP
105
+ WHERE user_id = ? AND draft_scope = ? AND queued_message = ?`
106
+ )
107
+ .run(candidate.userId, candidate.sessionId, candidate.claimToken);
108
+ return result.changes > 0;
109
+ },
110
+
111
+ /** Restores a claim lost to the narrow race where another run starts first. */
112
+ restoreQueuedMessage(candidate: QueuedSessionMessageRecord): void {
113
+ getConnection()
114
+ .prepare(
115
+ `UPDATE session_drafts
116
+ SET queued_message = ?, updated_at = CURRENT_TIMESTAMP
117
+ WHERE user_id = ? AND draft_scope = ? AND queued_message IS NULL`
118
+ )
119
+ .run(candidate.claimToken, candidate.userId, candidate.sessionId);
120
+ },
121
+
122
+ /** Removes the placeholder row left after its last queued turn is claimed. */
123
+ deleteEmptyDraft(userId: number, scope: string): void {
124
+ getConnection()
125
+ .prepare(
126
+ `DELETE FROM session_drafts
127
+ WHERE user_id = ? AND draft_scope = ? AND draft_text = '' AND queued_message IS NULL`
128
+ )
129
+ .run(userId, scope);
130
+ },
131
+
132
+ /**
133
+ * Writes one scope's draft, or deletes the row when nothing is left to keep.
134
+ *
135
+ * Deleting on empty is what stops the table growing a permanent row for every
136
+ * session the user ever opened and typed a character into.
137
+ */
138
+ saveDraft(
139
+ userId: number,
140
+ scope: string,
141
+ draft: { text: string; queuedMessage: unknown | null }
142
+ ): void {
143
+ const db = getConnection();
144
+
145
+ if (!draft.text && draft.queuedMessage === null) {
146
+ db.prepare('DELETE FROM session_drafts WHERE user_id = ? AND draft_scope = ?')
147
+ .run(userId, scope);
148
+ return;
149
+ }
150
+
151
+ db.prepare(
152
+ `INSERT INTO session_drafts (user_id, draft_scope, draft_text, queued_message, updated_at)
153
+ VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)
154
+ ON CONFLICT(user_id, draft_scope) DO UPDATE SET
155
+ draft_text = excluded.draft_text,
156
+ queued_message = excluded.queued_message,
157
+ updated_at = CURRENT_TIMESTAMP`
158
+ ).run(
159
+ userId,
160
+ scope,
161
+ draft.text,
162
+ draft.queuedMessage === null ? null : JSON.stringify(draft.queuedMessage)
163
+ );
164
+ },
165
+
166
+ deleteDraft(userId: number, scope: string): void {
167
+ const db = getConnection();
168
+ db.prepare('DELETE FROM session_drafts WHERE user_id = ? AND draft_scope = ?')
169
+ .run(userId, scope);
170
+ },
171
+ };