@rudderhq/server 0.4.6-canary.8 → 0.5.0

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 (365) hide show
  1. package/dist/agent-runtimes/http/execute.d.ts.map +1 -1
  2. package/dist/agent-runtimes/http/execute.js +8 -2
  3. package/dist/agent-runtimes/http/execute.js.map +1 -1
  4. package/dist/agent-runtimes/process/execute.d.ts.map +1 -1
  5. package/dist/agent-runtimes/process/execute.js +3 -1
  6. package/dist/agent-runtimes/process/execute.js.map +1 -1
  7. package/dist/agent-runtimes/utils.d.ts +6 -1
  8. package/dist/agent-runtimes/utils.d.ts.map +1 -1
  9. package/dist/agent-runtimes/utils.js +1 -1
  10. package/dist/agent-runtimes/utils.js.map +1 -1
  11. package/dist/app.d.ts +3 -3
  12. package/dist/app.d.ts.map +1 -1
  13. package/dist/app.js +17 -8
  14. package/dist/app.js.map +1 -1
  15. package/dist/bootstrap/create-http-app.d.ts +6 -1
  16. package/dist/bootstrap/create-http-app.d.ts.map +1 -1
  17. package/dist/bootstrap/create-http-app.js +122 -46
  18. package/dist/bootstrap/create-http-app.js.map +1 -1
  19. package/dist/bootstrap/plugin-host-runtime.d.ts.map +1 -1
  20. package/dist/bootstrap/plugin-host-runtime.js +89 -31
  21. package/dist/bootstrap/plugin-host-runtime.js.map +1 -1
  22. package/dist/bootstrap/register-api-routes.d.ts +3 -1
  23. package/dist/bootstrap/register-api-routes.d.ts.map +1 -1
  24. package/dist/bootstrap/register-api-routes.js +3 -3
  25. package/dist/bootstrap/register-api-routes.js.map +1 -1
  26. package/dist/bootstrap/types.d.ts +1 -0
  27. package/dist/bootstrap/types.d.ts.map +1 -1
  28. package/dist/bundled-plugins/plugin-linear/dist/worker.js +351 -174
  29. package/dist/bundled-plugins/plugin-linear/dist/worker.js.map +3 -3
  30. package/dist/config.d.ts +2 -0
  31. package/dist/config.d.ts.map +1 -1
  32. package/dist/config.js +23 -0
  33. package/dist/config.js.map +1 -1
  34. package/dist/home-paths.d.ts.map +1 -1
  35. package/dist/home-paths.js +79 -7
  36. package/dist/home-paths.js.map +1 -1
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +112 -119
  39. package/dist/index.js.map +1 -1
  40. package/dist/realtime/live-events-ws.d.ts +4 -20
  41. package/dist/realtime/live-events-ws.d.ts.map +1 -1
  42. package/dist/realtime/live-events-ws.js +68 -8
  43. package/dist/realtime/live-events-ws.js.map +1 -1
  44. package/dist/routes/access-onboarding.helpers.js +3 -3
  45. package/dist/routes/access-onboarding.helpers.js.map +1 -1
  46. package/dist/routes/access.helpers.d.ts.map +1 -1
  47. package/dist/routes/access.helpers.js +3 -2
  48. package/dist/routes/access.helpers.js.map +1 -1
  49. package/dist/routes/access.js +1 -1
  50. package/dist/routes/access.js.map +1 -1
  51. package/dist/routes/activity.d.ts.map +1 -1
  52. package/dist/routes/activity.js +5 -1
  53. package/dist/routes/activity.js.map +1 -1
  54. package/dist/routes/agents.management-routes.d.ts.map +1 -1
  55. package/dist/routes/agents.management-routes.js +15 -15
  56. package/dist/routes/agents.management-routes.js.map +1 -1
  57. package/dist/routes/assets.d.ts.map +1 -1
  58. package/dist/routes/assets.js +9 -2
  59. package/dist/routes/assets.js.map +1 -1
  60. package/dist/routes/chat-background-runtime.d.ts +19 -0
  61. package/dist/routes/chat-background-runtime.d.ts.map +1 -0
  62. package/dist/routes/chat-background-runtime.js +129 -0
  63. package/dist/routes/chat-background-runtime.js.map +1 -0
  64. package/dist/routes/chats.d.ts +2 -1
  65. package/dist/routes/chats.d.ts.map +1 -1
  66. package/dist/routes/chats.js +944 -24
  67. package/dist/routes/chats.js.map +1 -1
  68. package/dist/routes/chats.stream-routes.d.ts.map +1 -1
  69. package/dist/routes/chats.stream-routes.js +552 -76
  70. package/dist/routes/chats.stream-routes.js.map +1 -1
  71. package/dist/routes/issues.comments-attachments.d.ts.map +1 -1
  72. package/dist/routes/issues.comments-attachments.js +14 -31
  73. package/dist/routes/issues.comments-attachments.js.map +1 -1
  74. package/dist/routes/onboarding.js +1 -1
  75. package/dist/routes/onboarding.js.map +1 -1
  76. package/dist/routes/orgs.d.ts +2 -1
  77. package/dist/routes/orgs.d.ts.map +1 -1
  78. package/dist/routes/orgs.js +32 -3
  79. package/dist/routes/orgs.js.map +1 -1
  80. package/dist/routes/run-intelligence.d.ts.map +1 -1
  81. package/dist/routes/run-intelligence.js +109 -25
  82. package/dist/routes/run-intelligence.js.map +1 -1
  83. package/dist/runtime/runtime-supervisor.d.ts +21 -0
  84. package/dist/runtime/runtime-supervisor.d.ts.map +1 -0
  85. package/dist/runtime/runtime-supervisor.js +66 -0
  86. package/dist/runtime/runtime-supervisor.js.map +1 -0
  87. package/dist/services/activity-log.d.ts +4 -1
  88. package/dist/services/activity-log.d.ts.map +1 -1
  89. package/dist/services/activity-log.js +12 -4
  90. package/dist/services/activity-log.js.map +1 -1
  91. package/dist/services/activity.d.ts +11 -394
  92. package/dist/services/activity.d.ts.map +1 -1
  93. package/dist/services/activity.js +24 -18
  94. package/dist/services/activity.js.map +1 -1
  95. package/dist/services/agents.d.ts +799 -137
  96. package/dist/services/agents.d.ts.map +1 -1
  97. package/dist/services/approvals.d.ts +3 -3
  98. package/dist/services/automation-chat-output.d.ts.map +1 -1
  99. package/dist/services/automation-chat-output.js +115 -94
  100. package/dist/services/automation-chat-output.js.map +1 -1
  101. package/dist/services/budgets.d.ts.map +1 -1
  102. package/dist/services/budgets.js +38 -36
  103. package/dist/services/budgets.js.map +1 -1
  104. package/dist/services/chat-agent-runs.d.ts +3 -1
  105. package/dist/services/chat-agent-runs.d.ts.map +1 -1
  106. package/dist/services/chat-agent-runs.js +97 -34
  107. package/dist/services/chat-agent-runs.js.map +1 -1
  108. package/dist/services/chat-assistant.d.ts.map +1 -1
  109. package/dist/services/chat-assistant.helpers.d.ts +27 -4
  110. package/dist/services/chat-assistant.helpers.d.ts.map +1 -1
  111. package/dist/services/chat-assistant.helpers.js +66 -2
  112. package/dist/services/chat-assistant.helpers.js.map +1 -1
  113. package/dist/services/chat-assistant.inline-visuals.test.d.ts +2 -0
  114. package/dist/services/chat-assistant.inline-visuals.test.d.ts.map +1 -0
  115. package/dist/services/chat-assistant.inline-visuals.test.js +86 -0
  116. package/dist/services/chat-assistant.inline-visuals.test.js.map +1 -0
  117. package/dist/services/chat-assistant.js +115 -22
  118. package/dist/services/chat-assistant.js.map +1 -1
  119. package/dist/services/chat-generation-locks.d.ts +85 -0
  120. package/dist/services/chat-generation-locks.d.ts.map +1 -1
  121. package/dist/services/chat-generation-locks.js +371 -9
  122. package/dist/services/chat-generation-locks.js.map +1 -1
  123. package/dist/services/chat-generation-locks.test.d.ts +2 -0
  124. package/dist/services/chat-generation-locks.test.d.ts.map +1 -0
  125. package/dist/services/chat-generation-locks.test.js +324 -0
  126. package/dist/services/chat-generation-locks.test.js.map +1 -0
  127. package/dist/services/chat-generation-protocol.d.ts +674 -0
  128. package/dist/services/chat-generation-protocol.d.ts.map +1 -0
  129. package/dist/services/chat-generation-protocol.js +1571 -0
  130. package/dist/services/chat-generation-protocol.js.map +1 -0
  131. package/dist/services/chat-generation-protocol.test.d.ts +2 -0
  132. package/dist/services/chat-generation-protocol.test.d.ts.map +1 -0
  133. package/dist/services/chat-generation-protocol.test.js +1156 -0
  134. package/dist/services/chat-generation-protocol.test.js.map +1 -0
  135. package/dist/services/chat-title-generation.d.ts +0 -3
  136. package/dist/services/chat-title-generation.d.ts.map +1 -1
  137. package/dist/services/chat-title-generation.js +2 -32
  138. package/dist/services/chat-title-generation.js.map +1 -1
  139. package/dist/services/chat-work-manifest.d.ts.map +1 -1
  140. package/dist/services/chat-work-manifest.js +5 -1
  141. package/dist/services/chat-work-manifest.js.map +1 -1
  142. package/dist/services/chats.d.ts +1451 -272
  143. package/dist/services/chats.d.ts.map +1 -1
  144. package/dist/services/chats.js +1372 -35
  145. package/dist/services/chats.js.map +1 -1
  146. package/dist/services/costs.d.ts +34 -7
  147. package/dist/services/costs.d.ts.map +1 -1
  148. package/dist/services/costs.js +129 -61
  149. package/dist/services/costs.js.map +1 -1
  150. package/dist/services/finance.d.ts +3 -3
  151. package/dist/services/goals.d.ts +2 -2
  152. package/dist/services/heartbeat-run-summary.d.ts.map +1 -1
  153. package/dist/services/heartbeat-run-summary.js +16 -1
  154. package/dist/services/heartbeat-run-summary.js.map +1 -1
  155. package/dist/services/heartbeat.d.ts +1 -1
  156. package/dist/services/heartbeat.d.ts.map +1 -1
  157. package/dist/services/heartbeat.js +1 -1
  158. package/dist/services/heartbeat.js.map +1 -1
  159. package/dist/services/index.d.ts +1 -0
  160. package/dist/services/index.d.ts.map +1 -1
  161. package/dist/services/index.js +1 -0
  162. package/dist/services/index.js.map +1 -1
  163. package/dist/services/integrations/agent-integrations.d.ts +1 -1
  164. package/dist/services/integrations/custom-integrations.d.ts +1 -1
  165. package/dist/services/integrations/feishu/runtime.d.ts.map +1 -1
  166. package/dist/services/integrations/feishu/runtime.js +16 -0
  167. package/dist/services/integrations/feishu/runtime.js.map +1 -1
  168. package/dist/services/issues.comments-attachments.d.ts +111 -0
  169. package/dist/services/issues.comments-attachments.d.ts.map +1 -1
  170. package/dist/services/issues.comments-attachments.js +138 -66
  171. package/dist/services/issues.comments-attachments.js.map +1 -1
  172. package/dist/services/issues.d.ts +56 -0
  173. package/dist/services/issues.d.ts.map +1 -1
  174. package/dist/services/issues.helpers.d.ts.map +1 -1
  175. package/dist/services/issues.helpers.js +2 -2
  176. package/dist/services/issues.helpers.js.map +1 -1
  177. package/dist/services/issues.js +5 -2
  178. package/dist/services/issues.js.map +1 -1
  179. package/dist/services/knowledge-portability/organization-skills.catalog.d.ts.map +1 -1
  180. package/dist/services/knowledge-portability/organization-skills.catalog.js +7 -1
  181. package/dist/services/knowledge-portability/organization-skills.catalog.js.map +1 -1
  182. package/dist/services/messenger.d.ts.map +1 -1
  183. package/dist/services/messenger.js +11 -9
  184. package/dist/services/messenger.js.map +1 -1
  185. package/dist/services/organization-workspace-browser.d.ts +1 -1
  186. package/dist/services/organization-workspace-browser.d.ts.map +1 -1
  187. package/dist/services/organization-workspace-browser.js +49 -5
  188. package/dist/services/organization-workspace-browser.js.map +1 -1
  189. package/dist/services/orgs.d.ts.map +1 -1
  190. package/dist/services/orgs.js +36 -12
  191. package/dist/services/orgs.js.map +1 -1
  192. package/dist/services/plugin-dev-watcher.d.ts +19 -2
  193. package/dist/services/plugin-dev-watcher.d.ts.map +1 -1
  194. package/dist/services/plugin-dev-watcher.js +45 -11
  195. package/dist/services/plugin-dev-watcher.js.map +1 -1
  196. package/dist/services/plugin-registry.d.ts +28 -28
  197. package/dist/services/projects.d.ts +1 -1
  198. package/dist/services/run-events.d.ts +20 -0
  199. package/dist/services/run-events.d.ts.map +1 -0
  200. package/dist/services/run-events.js +34 -0
  201. package/dist/services/run-events.js.map +1 -0
  202. package/dist/services/run-intelligence.d.ts +66 -4
  203. package/dist/services/run-intelligence.d.ts.map +1 -1
  204. package/dist/services/run-intelligence.js +391 -42
  205. package/dist/services/run-intelligence.js.map +1 -1
  206. package/dist/services/run-log-store.d.ts.map +1 -1
  207. package/dist/services/run-log-store.js +26 -3
  208. package/dist/services/run-log-store.js.map +1 -1
  209. package/dist/services/runtime-kernel/heartbeat.core.d.ts +24 -1
  210. package/dist/services/runtime-kernel/heartbeat.core.d.ts.map +1 -1
  211. package/dist/services/runtime-kernel/heartbeat.core.js +2 -1
  212. package/dist/services/runtime-kernel/heartbeat.core.js.map +1 -1
  213. package/dist/services/runtime-kernel/heartbeat.d.ts +98 -37
  214. package/dist/services/runtime-kernel/heartbeat.d.ts.map +1 -1
  215. package/dist/services/runtime-kernel/heartbeat.execute.d.ts +3 -1
  216. package/dist/services/runtime-kernel/heartbeat.execute.d.ts.map +1 -1
  217. package/dist/services/runtime-kernel/heartbeat.execute.js +394 -219
  218. package/dist/services/runtime-kernel/heartbeat.execute.js.map +1 -1
  219. package/dist/services/runtime-kernel/heartbeat.js +603 -205
  220. package/dist/services/runtime-kernel/heartbeat.js.map +1 -1
  221. package/dist/services/runtime-kernel/heartbeat.misc.d.ts.map +1 -1
  222. package/dist/services/runtime-kernel/heartbeat.misc.js +34 -24
  223. package/dist/services/runtime-kernel/heartbeat.misc.js.map +1 -1
  224. package/dist/services/runtime-kernel/heartbeat.recovery.d.ts.map +1 -1
  225. package/dist/services/runtime-kernel/heartbeat.recovery.js +56 -6
  226. package/dist/services/runtime-kernel/heartbeat.recovery.js.map +1 -1
  227. package/dist/services/runtime-kernel/heartbeat.release.d.ts +3 -1
  228. package/dist/services/runtime-kernel/heartbeat.release.d.ts.map +1 -1
  229. package/dist/services/runtime-kernel/heartbeat.release.js +276 -44
  230. package/dist/services/runtime-kernel/heartbeat.release.js.map +1 -1
  231. package/dist/services/runtime-kernel/heartbeat.sessions.d.ts +42 -1
  232. package/dist/services/runtime-kernel/heartbeat.sessions.d.ts.map +1 -1
  233. package/dist/services/runtime-kernel/heartbeat.sessions.js +110 -8
  234. package/dist/services/runtime-kernel/heartbeat.sessions.js.map +1 -1
  235. package/dist/services/runtime-kernel/heartbeat.terminal.d.ts +575 -0
  236. package/dist/services/runtime-kernel/heartbeat.terminal.d.ts.map +1 -0
  237. package/dist/services/runtime-kernel/heartbeat.terminal.js +589 -0
  238. package/dist/services/runtime-kernel/heartbeat.terminal.js.map +1 -0
  239. package/dist/services/runtime-kernel/heartbeat.wakeup.d.ts.map +1 -1
  240. package/dist/services/runtime-kernel/heartbeat.wakeup.js +238 -92
  241. package/dist/services/runtime-kernel/heartbeat.wakeup.js.map +1 -1
  242. package/dist/services/runtime-kernel/model-fallback.d.ts.map +1 -1
  243. package/dist/services/runtime-kernel/model-fallback.js +11 -0
  244. package/dist/services/runtime-kernel/model-fallback.js.map +1 -1
  245. package/dist/services/side-chats.d.ts +212 -0
  246. package/dist/services/side-chats.d.ts.map +1 -0
  247. package/dist/services/side-chats.js +379 -0
  248. package/dist/services/side-chats.js.map +1 -0
  249. package/dist/services/workspace-runtime.services.d.ts +1 -1
  250. package/dist/services/workspace-web-preview.d.ts +36 -0
  251. package/dist/services/workspace-web-preview.d.ts.map +1 -0
  252. package/dist/services/workspace-web-preview.js +566 -0
  253. package/dist/services/workspace-web-preview.js.map +1 -0
  254. package/package.json +13 -13
  255. package/resources/bundled-skills/rudder-docs/SKILL.md +161 -0
  256. package/resources/bundled-skills/rudder-docs/evals/trigger-evals.json +82 -0
  257. package/{skills/rudder → resources/bundled-skills/rudder-docs}/references/api-reference.md +17 -1
  258. package/{skills/rudder → resources/bundled-skills/rudder-docs}/references/cli-reference.md +52 -97
  259. package/resources/bundled-skills/rudder-docs/references/control-plane-practices.md +231 -0
  260. package/{skills/rudder → resources/bundled-skills/rudder-docs}/references/organization-skills.md +15 -1
  261. package/resources/bundled-skills/rudder-docs/references/source-map.md +138 -0
  262. package/resources/bundled-skills/visualize/SKILL.md +132 -0
  263. package/resources/bundled-skills/visualize/agents/openai.yaml +7 -0
  264. package/resources/bundled-skills/visualize/assets/example-chart.html +89 -0
  265. package/resources/bundled-skills/visualize/references/runtime-contract.md +114 -0
  266. package/skills/rudder-docs/SKILL.md +161 -0
  267. package/skills/rudder-docs/evals/trigger-evals.json +82 -0
  268. package/{resources/bundled-skills/rudder → skills/rudder-docs}/references/api-reference.md +17 -1
  269. package/{resources/bundled-skills/rudder → skills/rudder-docs}/references/cli-reference.md +52 -97
  270. package/skills/rudder-docs/references/control-plane-practices.md +231 -0
  271. package/{resources/bundled-skills/rudder → skills/rudder-docs}/references/organization-skills.md +15 -1
  272. package/skills/rudder-docs/references/source-map.md +138 -0
  273. package/skills/visualize/SKILL.md +132 -0
  274. package/skills/visualize/agents/openai.yaml +7 -0
  275. package/skills/visualize/assets/example-chart.html +89 -0
  276. package/skills/visualize/references/runtime-contract.md +114 -0
  277. package/ui-dist/assets/{abnfDiagram-VRR7QNED-C4e_AuJm.js → abnfDiagram-VRR7QNED-Dpnx79fp.js} +1 -1
  278. package/ui-dist/assets/{arc-3OG5_qjR.js → arc-Cpvh_rCf.js} +1 -1
  279. package/ui-dist/assets/{architectureDiagram-ZJ3FMSHR-CJz3inSs.js → architectureDiagram-ZJ3FMSHR-sDD_mBAI.js} +1 -1
  280. package/ui-dist/assets/{blockDiagram-677ZJIJ3-CpBIok39.js → blockDiagram-677ZJIJ3-4O4wWti1.js} +1 -1
  281. package/ui-dist/assets/{c4Diagram-LMCZKHZV-BbQrD6Fx.js → c4Diagram-LMCZKHZV-BkGODE4P.js} +1 -1
  282. package/ui-dist/assets/channel-CM4Myg_6.js +1 -0
  283. package/ui-dist/assets/{chunk-2Q5K7J3B-BkoT6fZL.js → chunk-2Q5K7J3B-DsDQQpID.js} +1 -1
  284. package/ui-dist/assets/{chunk-32BRIVSS-DecBtqTH.js → chunk-32BRIVSS-Bzx_VnIw.js} +1 -1
  285. package/ui-dist/assets/{chunk-5VM5RSS4-SFFAgh5q.js → chunk-5VM5RSS4-D4LoPCni.js} +1 -1
  286. package/ui-dist/assets/{chunk-EX3LRPZG-CxUndf0S.js → chunk-EX3LRPZG-BO0R-mS4.js} +3 -3
  287. package/ui-dist/assets/{chunk-JWPE2WC7-BbEXuDLm.js → chunk-JWPE2WC7-B4Nr8KRL.js} +1 -1
  288. package/ui-dist/assets/{chunk-MOJQB5TN-MOCGQpRH.js → chunk-MOJQB5TN--uaDDrYM.js} +1 -1
  289. package/ui-dist/assets/{chunk-RYQCIY6F-CUuwDB4L.js → chunk-RYQCIY6F-CyBY2w_j.js} +1 -1
  290. package/ui-dist/assets/{chunk-V7JOEXUC-D5H80ZpL.js → chunk-V7JOEXUC-BMid_x8S.js} +3 -3
  291. package/ui-dist/assets/{chunk-VR4S4FIN-Cg5d82yb.js → chunk-VR4S4FIN-DVqdPKuG.js} +1 -1
  292. package/ui-dist/assets/{chunk-XXDRQBXY-Bu7DXS_3.js → chunk-XXDRQBXY-vy03lLSS.js} +1 -1
  293. package/ui-dist/assets/classDiagram-OUVF2IWQ-CtlIEzTt.js +1 -0
  294. package/ui-dist/assets/classDiagram-v2-EOCWNBFH-CtlIEzTt.js +1 -0
  295. package/ui-dist/assets/{cose-bilkent-JH36ORCC-LCobsJkA.js → cose-bilkent-JH36ORCC-jQDZmrG3.js} +1 -1
  296. package/ui-dist/assets/{cynefin-VYW2F7L2-CxDfvhGJ.js → cynefin-VYW2F7L2-BxBj6aea.js} +1 -1
  297. package/ui-dist/assets/{cynefinDiagram-TSTJHNR4-BbebJ96a.js → cynefinDiagram-TSTJHNR4-BuUubF5I.js} +1 -1
  298. package/ui-dist/assets/{dagre-VKFMJZFB-BNQ0fvpl.js → dagre-VKFMJZFB-B8HipJ3a.js} +1 -1
  299. package/ui-dist/assets/{diagram-FQU43EPY-B9vCEyLD.js → diagram-FQU43EPY-r7gkbKSt.js} +2 -2
  300. package/ui-dist/assets/{diagram-G47NLZAW-Z7Hg_sXF.js → diagram-G47NLZAW-CxClRsxD.js} +1 -1
  301. package/ui-dist/assets/{diagram-NH7WQ7WH-B_TZ5-gD.js → diagram-NH7WQ7WH-C8HYTLlq.js} +1 -1
  302. package/ui-dist/assets/{diagram-OA4YK3LP-qe-twEnv.js → diagram-OA4YK3LP-CjwqqLKz.js} +1 -1
  303. package/ui-dist/assets/{diagram-WEI45ONY-D8ufoJVE.js → diagram-WEI45ONY-2IWfCqZb.js} +1 -1
  304. package/ui-dist/assets/{ebnfDiagram-CCIWWBDH-BXXAFqU6.js → ebnfDiagram-CCIWWBDH-DVGZkgjy.js} +1 -1
  305. package/ui-dist/assets/{erDiagram-Q63AITRT-uXmIG2c_.js → erDiagram-Q63AITRT-C_-u3Gmw.js} +1 -1
  306. package/ui-dist/assets/{flowDiagram-23GEKE2U-3eiT6gWx.js → flowDiagram-23GEKE2U-BvGg0NCq.js} +4 -4
  307. package/ui-dist/assets/{ganttDiagram-NO4QXBWP-CiVmqV02.js → ganttDiagram-NO4QXBWP-U3CZUUUR.js} +1 -1
  308. package/ui-dist/assets/{gitGraphDiagram-IHSO6WYX-DmIrlFHc.js → gitGraphDiagram-IHSO6WYX-CIWuEBIo.js} +1 -1
  309. package/ui-dist/assets/{graph-BpS6eOIF.js → graph-D3P5q-7m.js} +1 -1
  310. package/ui-dist/assets/index-B1rLBtZ1.js +1875 -0
  311. package/ui-dist/assets/{index-Dlx6uy7_.js → index-BAMXpiI5.js} +1 -1
  312. package/ui-dist/assets/{index-CUHGsSRd.js → index-BAgtKYR1.js} +1 -1
  313. package/ui-dist/assets/{index--YRaLrzl.js → index-CBOr9F6w.js} +1 -1
  314. package/ui-dist/assets/{index-cHC60zXl.js → index-CbHqnWUe.js} +1 -1
  315. package/ui-dist/assets/{index-DeWKDYpk.js → index-CjztBLJi.js} +1 -1
  316. package/ui-dist/assets/{index-T2PpfhXU.js → index-Cu1iMhjl.js} +1 -1
  317. package/ui-dist/assets/{index-Dgj1-Gzi.js → index-D7ycf1qU.js} +1 -1
  318. package/ui-dist/assets/{index-Dpvu2aoc.js → index-DAYlwxsg.js} +1 -1
  319. package/ui-dist/assets/{index-BAU4CU-l.js → index-DCFf9dps.js} +4 -4
  320. package/ui-dist/assets/{index-CWaZ_rS8.js → index-DOPIV8vd.js} +1 -1
  321. package/ui-dist/assets/{index-C-TZf854.js → index-DRxLJD40.js} +1 -1
  322. package/ui-dist/assets/{index-CUI9Uyvz.js → index-DYMtCDKT.js} +1 -1
  323. package/ui-dist/assets/index-DekHKOsL.css +1 -0
  324. package/ui-dist/assets/{index-BncN7e-4.js → index-DidCCw78.js} +1 -1
  325. package/ui-dist/assets/{index-Cl27pPic.js → index-Ds_qma4-.js} +1 -1
  326. package/ui-dist/assets/{index-CF3Fjm4r.js → index-HkQRx1D6.js} +1 -1
  327. package/ui-dist/assets/{index-Cp12taja.js → index-ikpL--zj.js} +1 -1
  328. package/ui-dist/assets/{index-BzDB5XYN.js → index-mnl55Stn.js} +1 -1
  329. package/ui-dist/assets/{infoDiagram-FWYZ7A6U-B_e34a6A.js → infoDiagram-FWYZ7A6U-Cx8Fa-I4.js} +1 -1
  330. package/ui-dist/assets/{ishikawaDiagram-FXEZZL3T-C_h8nqRS.js → ishikawaDiagram-FXEZZL3T-Cmuz1QUN.js} +1 -1
  331. package/ui-dist/assets/{journeyDiagram-5HDEW3XC-CrxAvEMf.js → journeyDiagram-5HDEW3XC-CJgkiVXG.js} +1 -1
  332. package/ui-dist/assets/{kanban-definition-HUTT4EX6-Zp6LAKss.js → kanban-definition-HUTT4EX6-RAWpefOB.js} +1 -1
  333. package/ui-dist/assets/layout-DyWQv0Rr.js +1 -0
  334. package/ui-dist/assets/{linear-_N4Odhfh.js → linear-D5pdgIf9.js} +1 -1
  335. package/ui-dist/assets/{map-CYGuJtD3.js → map-bVSzNLl2.js} +1 -1
  336. package/ui-dist/assets/mermaid.core-CeaXGD6B.js +312 -0
  337. package/ui-dist/assets/{mindmap-definition-LN4V7U3C-C-J9N_o_.js → mindmap-definition-LN4V7U3C-B8B8AX5C.js} +1 -1
  338. package/ui-dist/assets/{pegDiagram-2B236MQR-B9Mlsk91.js → pegDiagram-2B236MQR-C4Sv-ua6.js} +1 -1
  339. package/ui-dist/assets/{pieDiagram-ENE6RG2P-BY7DU2f8.js → pieDiagram-ENE6RG2P-CLARlOHx.js} +1 -1
  340. package/ui-dist/assets/{quadrantDiagram-ABIIQ3AL-CdCl_yc0.js → quadrantDiagram-ABIIQ3AL-DPCXQmeT.js} +1 -1
  341. package/ui-dist/assets/{railroadDiagram-RFXS5EU6-C5w2LKAD.js → railroadDiagram-RFXS5EU6-BpqRuKUQ.js} +1 -1
  342. package/ui-dist/assets/{requirementDiagram-TGXJPOKE-fRb_n-yH.js → requirementDiagram-TGXJPOKE-Dd-h62HZ.js} +1 -1
  343. package/ui-dist/assets/{sankeyDiagram-HTMAVEWB-CYjb1HUH.js → sankeyDiagram-HTMAVEWB-DylT_pqq.js} +1 -1
  344. package/ui-dist/assets/{sequenceDiagram-DBY2YBRQ-CS95ySsI.js → sequenceDiagram-DBY2YBRQ-RiCF1REj.js} +1 -1
  345. package/ui-dist/assets/{sizeCapture-X5ZJPWSS-DPXTC0TI.js → sizeCapture-X5ZJPWSS-DXW4JQz2.js} +1 -1
  346. package/ui-dist/assets/{stateDiagram-2N3HPSRC-C4VFIMBR.js → stateDiagram-2N3HPSRC-6U3ESIGT.js} +1 -1
  347. package/ui-dist/assets/stateDiagram-v2-6OUMAXLB-KQTrcZvp.js +1 -0
  348. package/ui-dist/assets/{swimlanes-5IMT3BWC-UvxkQ8RD.js → swimlanes-5IMT3BWC-CeNHVNCi.js} +2 -2
  349. package/ui-dist/assets/swimlanesDiagram-G3AALYLV-CcxS1c6r.js +8 -0
  350. package/ui-dist/assets/{timeline-definition-FHXFAJF6-BKotSwXW.js → timeline-definition-FHXFAJF6-Vlwoe_1b.js} +1 -1
  351. package/ui-dist/assets/{vennDiagram-L72KCM5P-TmXc-aFs.js → vennDiagram-L72KCM5P-DBP7D3HE.js} +6 -6
  352. package/ui-dist/assets/{wardleyDiagram-EHGQE667-aTINQKt9.js → wardleyDiagram-EHGQE667-DG-wzqpy.js} +1 -1
  353. package/ui-dist/assets/{xychartDiagram-FW5EYKEG-CZ86hWNQ.js → xychartDiagram-FW5EYKEG-BAq3mbha.js} +1 -1
  354. package/ui-dist/index.html +2 -2
  355. package/resources/bundled-skills/rudder/SKILL.md +0 -314
  356. package/skills/rudder/SKILL.md +0 -314
  357. package/ui-dist/assets/channel-roHkdL27.js +0 -1
  358. package/ui-dist/assets/classDiagram-OUVF2IWQ-CKZLREY1.js +0 -1
  359. package/ui-dist/assets/classDiagram-v2-EOCWNBFH-CKZLREY1.js +0 -1
  360. package/ui-dist/assets/index-BTW19O7e.js +0 -1808
  361. package/ui-dist/assets/index-DOlmbVZk.css +0 -1
  362. package/ui-dist/assets/layout-DlHAdVX4.js +0 -1
  363. package/ui-dist/assets/mermaid.core-B8dXcSSK.js +0 -314
  364. package/ui-dist/assets/stateDiagram-v2-6OUMAXLB-CF5IY4p0.js +0 -1
  365. package/ui-dist/assets/swimlanesDiagram-G3AALYLV-BVLEg2fG.js +0 -8
@@ -1,16 +1,65 @@
1
- import { agentIntegrationChatBindings, agentIntegrations, approvals, assets, chatAttachments, chatContextLinks, chatConversations, chatConversationUserStates, chatGenerations, chatMessages, chatQueuedMessages, messengerCustomGroupEntries, messengerCustomGroups } from "@rudderhq/db";
2
- import { MESSENGER_FORK_GROUP_DEFAULT_ICON, sanitizeChatStructuredPayload } from "@rudderhq/shared";
1
+ import { agentIntegrationChatBindings, agentIntegrations, approvals, assets, chatAttachments, chatContextLinks, chatControlActions, chatConversations, chatConversationUserStates, chatGenerationEvents, chatGenerations, chatMessages, chatQueuedMessages, messengerCustomGroupEntries, messengerCustomGroups } from "@rudderhq/db";
2
+ import { chatInlineVisualMappingsFromStructuredPayload, MESSENGER_FORK_GROUP_DEFAULT_ICON, sanitizeChatStructuredPayload } from "@rudderhq/shared";
3
3
  import { and, asc, desc, eq, gt, gte, inArray, isNull, lt, or, sql } from "drizzle-orm";
4
4
  import { randomUUID } from "node:crypto";
5
5
  import { conflict, notFound, unprocessable } from "../errors.js";
6
6
  import { logActivity } from "./activity-log.js";
7
7
  import { agentService } from "./agents.js";
8
8
  import { approvalService } from "./approvals.js";
9
+ import { chatGenerationProtocolService } from "./chat-generation-protocol.js";
9
10
  import { issueApprovalService } from "./issue-approvals.js";
10
11
  import { issueService } from "./issues.js";
11
12
  import { normalizeLocalLibraryPathMarkdown } from "./library-path-markdown.js";
12
13
  import { organizationService } from "./orgs.js";
13
14
  import { isPostgresError } from "./postgres-errors.js";
15
+ const CHAT_TITLE_MAX_LENGTH = 200;
16
+ const FORK_TITLE_SUFFIX_PATTERN = / \(([1-9]\d*)\)$/;
17
+ const ACTIVE_CHAT_GENERATION_STATUSES = [
18
+ "starting",
19
+ "active",
20
+ "running",
21
+ "tool_busy",
22
+ "closing",
23
+ "stop_requested",
24
+ "stopping",
25
+ ];
26
+ const NATIVE_STEER_GENERATION_STATUSES = ["starting", "active", "running", "tool_busy"];
27
+ const SERVER_QUEUE_RUNNING_STATUSES = ["dequeue_claimed", "running_next"];
28
+ const CHAT_GENERATION_CONTROL_LEASE_MS = 30_000;
29
+ function baseForkTitle(title, sourceIsFork, familyTitles) {
30
+ const trimmed = title.trim() || "Forked conversation";
31
+ if (!sourceIsFork)
32
+ return trimmed;
33
+ const match = trimmed.match(FORK_TITLE_SUFFIX_PATTERN);
34
+ const index = Number(match?.[1]);
35
+ if (!match || index < 2)
36
+ return trimmed;
37
+ for (const familyTitle of familyTitles) {
38
+ const candidateBase = familyTitle.trim();
39
+ if (!candidateBase || candidateBase === trimmed)
40
+ continue;
41
+ if (numberedForkTitle(candidateBase, index) !== trimmed)
42
+ continue;
43
+ if (index === 2 || familyTitles.has(numberedForkTitle(candidateBase, index - 1))) {
44
+ return candidateBase;
45
+ }
46
+ }
47
+ return trimmed;
48
+ }
49
+ function numberedForkTitle(baseTitle, index) {
50
+ const suffix = ` (${index})`;
51
+ const availableBaseLength = CHAT_TITLE_MAX_LENGTH - suffix.length;
52
+ const truncatedBase = baseTitle.slice(0, availableBaseLength).trimEnd() || "Forked conversation";
53
+ return `${truncatedBase}${suffix}`;
54
+ }
55
+ function nextForkTitle(source, familyTitles) {
56
+ const existingTitles = new Set(familyTitles);
57
+ const baseTitle = baseForkTitle(source.title, Boolean(source.forkedFromConversationId), existingTitles);
58
+ let index = 2;
59
+ while (existingTitles.has(numberedForkTitle(baseTitle, index)))
60
+ index += 1;
61
+ return numberedForkTitle(baseTitle, index);
62
+ }
14
63
  function conversationMutability(row, sourceMetadata, sourceMetadataByConversationId) {
15
64
  if (sourceMetadata)
16
65
  return "external_bound_chat";
@@ -22,6 +71,7 @@ function conversationMutability(row, sourceMetadata, sourceMetadataByConversatio
22
71
  }
23
72
  import { buildSearchSnippet, CHAT_TRANSCRIPT_KEY, chatTranscriptFromPayload, chatTranscriptSummaryFromEntries, contentPath, escapeLikePattern, incomingMessagePreviewSql, issueProposalFromPayload, isVisibleIncomingChatMessage, listContextLinksForConversationIds, listPrimaryIssues, operationProposalDecisionStatusFromPayload, operationProposalFromPayload, resolveContextEntities, safeTrim, stripChatMetadataFromPayload, textContains, truncatePreview, visibleIncomingMessageSql, withOperationProposalDecisionState, withPersistedTranscript, } from "./chats.helpers.js";
24
73
  export function chatService(db) {
74
+ const generationProtocol = chatGenerationProtocolService(db);
25
75
  const QUEUED_MESSAGE_CLAIM_LEASE_MS = 2 * 60 * 1000;
26
76
  const issuesSvc = issueService(db);
27
77
  const approvalsSvc = approvalService(db);
@@ -388,8 +438,11 @@ export function chatService(db) {
388
438
  .orderBy(chatAttachments.createdAt);
389
439
  const map = new Map();
390
440
  for (const row of rows) {
441
+ const safeRow = row.contentType === "text/html" && row.createdByAgentId
442
+ ? (({ provider: _provider, objectKey: _objectKey, ...safe }) => safe)(row)
443
+ : row;
391
444
  const attachment = {
392
- ...row,
445
+ ...safeRow,
393
446
  contentPath: contentPath(row.assetId),
394
447
  };
395
448
  const list = map.get(row.messageId);
@@ -438,12 +491,18 @@ export function chatService(db) {
438
491
  };
439
492
  }
440
493
  async function createGeneration(orgId, conversationId) {
494
+ const now = new Date();
441
495
  const [row] = await db
442
496
  .insert(chatGenerations)
443
497
  .values({
444
498
  orgId,
445
499
  conversationId,
446
500
  status: "active",
501
+ controlOwnerToken: randomUUID(),
502
+ controlLeaseExpiresAt: new Date(now.getTime() + CHAT_GENERATION_CONTROL_LEASE_MS),
503
+ startedAt: now,
504
+ createdAt: now,
505
+ updatedAt: now,
447
506
  })
448
507
  .returning();
449
508
  if (!row)
@@ -454,23 +513,81 @@ export function chatService(db) {
454
513
  if (!generationId)
455
514
  return null;
456
515
  const now = new Date();
457
- const [row] = await db
458
- .update(chatGenerations)
459
- .set({
460
- status,
461
- terminalReason: status,
462
- completedAt: now,
463
- updatedAt: now,
464
- })
465
- .where(and(eq(chatGenerations.id, generationId), inArray(chatGenerations.status, ["active", "tool_busy", "closing"])))
466
- .returning();
467
- return row ?? null;
516
+ return db.transaction(async (tx) => {
517
+ const [row] = await tx
518
+ .update(chatGenerations)
519
+ .set({
520
+ status,
521
+ terminalReason: status,
522
+ controlState: "terminal",
523
+ runtimeTerminalAt: now,
524
+ completedAt: now,
525
+ updatedAt: now,
526
+ })
527
+ .where(and(eq(chatGenerations.id, generationId), inArray(chatGenerations.status, ACTIVE_CHAT_GENERATION_STATUSES)))
528
+ .returning();
529
+ if (!row)
530
+ return null;
531
+ const deliveredSteers = await tx
532
+ .update(chatQueuedMessages)
533
+ .set({
534
+ status: "delivered",
535
+ deliveryDisposition: "delivered",
536
+ reconciliationReason: null,
537
+ lastDeliveryReason: null,
538
+ version: sql `${chatQueuedMessages.version} + 1`,
539
+ updatedAt: now,
540
+ })
541
+ .where(and(eq(chatQueuedMessages.status, "accepted_current"), or(eq(chatQueuedMessages.expectedGenerationId, generationId), eq(chatQueuedMessages.activeGenerationId, generationId))))
542
+ .returning({ controlActionId: chatQueuedMessages.controlActionId });
543
+ const controlActionIds = deliveredSteers
544
+ .map((item) => item.controlActionId)
545
+ .filter((id) => Boolean(id));
546
+ if (controlActionIds.length > 0) {
547
+ await tx
548
+ .update(chatControlActions)
549
+ .set({
550
+ localDisposition: "delivered",
551
+ resolvedAt: now,
552
+ updatedAt: now,
553
+ })
554
+ .where(and(inArray(chatControlActions.id, controlActionIds), eq(chatControlActions.localDisposition, "accepted_current")));
555
+ }
556
+ const continuedSteers = await tx
557
+ .update(chatQueuedMessages)
558
+ .set({
559
+ status: "continuation_pending",
560
+ deliveryDisposition: "continuation_pending",
561
+ reconciliationReason: `target_generation_${status}`,
562
+ lastDeliveryReason: null,
563
+ version: sql `${chatQueuedMessages.version} + 1`,
564
+ updatedAt: now,
565
+ })
566
+ .where(and(eq(chatQueuedMessages.status, "steer_pending"), or(eq(chatQueuedMessages.expectedGenerationId, generationId), eq(chatQueuedMessages.activeGenerationId, generationId))))
567
+ .returning({ controlActionId: chatQueuedMessages.controlActionId });
568
+ const continuedControlActionIds = continuedSteers
569
+ .map((item) => item.controlActionId)
570
+ .filter((id) => Boolean(id));
571
+ if (continuedControlActionIds.length > 0) {
572
+ await tx
573
+ .update(chatControlActions)
574
+ .set({
575
+ localDisposition: "continuation_pending",
576
+ providerDisposition: "not_sent",
577
+ lastError: `target_generation_${status}`,
578
+ resolvedAt: now,
579
+ updatedAt: now,
580
+ })
581
+ .where(and(inArray(chatControlActions.id, continuedControlActionIds), eq(chatControlActions.localDisposition, "pending")));
582
+ }
583
+ return row;
584
+ });
468
585
  }
469
586
  async function getLatestActiveGeneration(conversationId) {
470
587
  return db
471
588
  .select()
472
589
  .from(chatGenerations)
473
- .where(and(eq(chatGenerations.conversationId, conversationId), inArray(chatGenerations.status, ["active", "tool_busy", "closing"])))
590
+ .where(and(eq(chatGenerations.conversationId, conversationId), inArray(chatGenerations.status, ACTIVE_CHAT_GENERATION_STATUSES)))
474
591
  .orderBy(desc(chatGenerations.startedAt), desc(chatGenerations.createdAt))
475
592
  .limit(1)
476
593
  .then((rows) => rows[0] ?? null);
@@ -489,7 +606,16 @@ export function chatService(db) {
489
606
  const rows = await db
490
607
  .select()
491
608
  .from(chatQueuedMessages)
492
- .where(and(eq(chatQueuedMessages.conversationId, conversationId), inArray(chatQueuedMessages.status, ["queued", "steer_pending", "dequeue_claimed", "running"])))
609
+ .where(and(eq(chatQueuedMessages.conversationId, conversationId), inArray(chatQueuedMessages.status, [
610
+ "queued",
611
+ "steer_pending",
612
+ "acceptance_unknown",
613
+ "continuation_pending",
614
+ "dequeue_claimed",
615
+ "running",
616
+ "running_next",
617
+ "failed_actionable",
618
+ ])))
493
619
  .orderBy(asc(chatQueuedMessages.position), asc(chatQueuedMessages.createdAt));
494
620
  return rows.map(hydrateQueuedMessage);
495
621
  }
@@ -497,13 +623,673 @@ export function chatService(db) {
497
623
  const activeGeneration = activeGenerationId === undefined
498
624
  ? await getLatestActiveGeneration(conversationId)
499
625
  : activeGenerationId
500
- ? { id: activeGenerationId }
626
+ ? await db
627
+ .select()
628
+ .from(chatGenerations)
629
+ .where(and(eq(chatGenerations.id, activeGenerationId), eq(chatGenerations.conversationId, conversationId)))
630
+ .limit(1)
631
+ .then((rows) => rows[0] ?? null)
501
632
  : null;
502
633
  return {
503
634
  activeGenerationId: activeGeneration?.id ?? null,
635
+ activeAttemptEpoch: activeGeneration?.attemptEpoch ?? null,
636
+ activeControlVersion: activeGeneration?.controlVersion ?? null,
637
+ activeGenerationStatus: activeGeneration?.status ?? null,
504
638
  items: await listQueuedMessages(conversationId),
505
639
  };
506
640
  }
641
+ async function beginGenerationControlAttempt(input) {
642
+ const now = new Date();
643
+ const leaseExpiresAt = new Date(now.getTime() + CHAT_GENERATION_CONTROL_LEASE_MS);
644
+ const [row] = await db
645
+ .update(chatGenerations)
646
+ .set({
647
+ attemptEpoch: input.attemptEpoch,
648
+ controlState: "unregistered",
649
+ controlRuntimeType: input.runtimeType,
650
+ controlOwnerToken: input.ownerToken,
651
+ controlLeaseExpiresAt: leaseExpiresAt,
652
+ providerThreadId: null,
653
+ providerTurnId: null,
654
+ status: "starting",
655
+ updatedAt: now,
656
+ })
657
+ .where(and(eq(chatGenerations.id, input.generationId), eq(chatGenerations.orgId, input.orgId), eq(chatGenerations.conversationId, input.conversationId), isNull(chatGenerations.runtimeTerminalAt), inArray(chatGenerations.status, ["active", "starting", "running", "tool_busy", "closing"]), sql `${chatGenerations.attemptEpoch} <= ${input.attemptEpoch}`))
658
+ .returning();
659
+ if (!row)
660
+ throw conflict("Chat generation control ownership changed before runtime startup");
661
+ return row;
662
+ }
663
+ async function markGenerationControlReady(input) {
664
+ const now = new Date();
665
+ const [row] = await db
666
+ .update(chatGenerations)
667
+ .set({
668
+ controlState: "ready",
669
+ controlRuntimeType: input.runtimeType,
670
+ providerThreadId: input.providerThreadId ?? null,
671
+ providerTurnId: input.providerTurnId ?? null,
672
+ controlLeaseExpiresAt: new Date(now.getTime() + CHAT_GENERATION_CONTROL_LEASE_MS),
673
+ status: "running",
674
+ updatedAt: now,
675
+ })
676
+ .where(and(eq(chatGenerations.id, input.generationId), eq(chatGenerations.attemptEpoch, input.attemptEpoch), eq(chatGenerations.controlOwnerToken, input.ownerToken), isNull(chatGenerations.runtimeTerminalAt), inArray(chatGenerations.status, ["active", "starting", "running", "tool_busy"])))
677
+ .returning();
678
+ if (!row)
679
+ throw conflict("Chat runtime control handle lost its generation lease");
680
+ return row;
681
+ }
682
+ async function renewGenerationControlLease(input) {
683
+ const now = input.now ?? new Date();
684
+ const [row] = await db
685
+ .update(chatGenerations)
686
+ .set({
687
+ controlLeaseExpiresAt: new Date(now.getTime() + CHAT_GENERATION_CONTROL_LEASE_MS),
688
+ updatedAt: now,
689
+ })
690
+ .where(and(eq(chatGenerations.id, input.generationId), eq(chatGenerations.attemptEpoch, input.attemptEpoch), eq(chatGenerations.controlOwnerToken, input.ownerToken), isNull(chatGenerations.runtimeTerminalAt), inArray(chatGenerations.status, ["active", "starting", "running", "tool_busy", "closing"])))
691
+ .returning({ id: chatGenerations.id });
692
+ if (!row)
693
+ throw conflict("Chat generation control lease is no longer current");
694
+ return true;
695
+ }
696
+ async function markGenerationControlAttemptCompleted(input) {
697
+ const now = new Date();
698
+ return db.transaction(async (tx) => {
699
+ const [row] = await tx
700
+ .update(chatGenerations)
701
+ .set({
702
+ controlState: "unregistered",
703
+ status: "closing",
704
+ updatedAt: now,
705
+ })
706
+ .where(and(eq(chatGenerations.id, input.generationId), eq(chatGenerations.attemptEpoch, input.attemptEpoch), eq(chatGenerations.controlOwnerToken, input.ownerToken), inArray(chatGenerations.status, ["active", "starting", "running", "tool_busy", "closing"])))
707
+ .returning();
708
+ if (!row)
709
+ return null;
710
+ const pendingSteers = await tx
711
+ .select({
712
+ id: chatQueuedMessages.id,
713
+ controlActionId: chatQueuedMessages.controlActionId,
714
+ })
715
+ .from(chatQueuedMessages)
716
+ .where(and(eq(chatQueuedMessages.status, "steer_pending"), eq(chatQueuedMessages.activeGenerationId, input.generationId), eq(chatQueuedMessages.attemptEpoch, input.attemptEpoch)));
717
+ const pendingItemIds = pendingSteers.map((item) => item.id).sort();
718
+ const controlActionIds = pendingSteers
719
+ .map((item) => item.controlActionId)
720
+ .filter((id) => Boolean(id))
721
+ .sort();
722
+ if (controlActionIds.length > 0) {
723
+ await tx
724
+ .select({ id: chatControlActions.id })
725
+ .from(chatControlActions)
726
+ .where(inArray(chatControlActions.id, controlActionIds))
727
+ .orderBy(asc(chatControlActions.id))
728
+ .for("update");
729
+ }
730
+ if (pendingItemIds.length > 0) {
731
+ await tx
732
+ .select({ id: chatQueuedMessages.id })
733
+ .from(chatQueuedMessages)
734
+ .where(inArray(chatQueuedMessages.id, pendingItemIds))
735
+ .orderBy(asc(chatQueuedMessages.id))
736
+ .for("update");
737
+ }
738
+ const uncertainSteers = pendingItemIds.length === 0
739
+ ? []
740
+ : await tx
741
+ .update(chatQueuedMessages)
742
+ .set({
743
+ status: "acceptance_unknown",
744
+ deliveryDisposition: "acceptance_unknown",
745
+ reconciliationReason: "runtime_attempt_completed_after_steer_send",
746
+ lastDeliveryReason: "runtime_attempt_completed_after_steer_send",
747
+ version: sql `${chatQueuedMessages.version} + 1`,
748
+ updatedAt: now,
749
+ })
750
+ .where(and(inArray(chatQueuedMessages.id, pendingItemIds), eq(chatQueuedMessages.status, "steer_pending"), inArray(chatQueuedMessages.controlActionId, tx
751
+ .select({ id: chatControlActions.id })
752
+ .from(chatControlActions)
753
+ .where(inArray(chatControlActions.providerDisposition, [
754
+ "sent",
755
+ "timed_out",
756
+ "connection_lost",
757
+ "waiting_safe_boundary",
758
+ "unverified",
759
+ ])))))
760
+ .returning({ controlActionId: chatQueuedMessages.controlActionId });
761
+ const uncertainControlActionIds = uncertainSteers
762
+ .map((item) => item.controlActionId)
763
+ .filter((id) => Boolean(id));
764
+ if (uncertainControlActionIds.length > 0) {
765
+ await tx
766
+ .update(chatControlActions)
767
+ .set({
768
+ localDisposition: "acceptance_unknown",
769
+ providerDisposition: "unverified",
770
+ lastError: "runtime_attempt_completed_after_steer_send",
771
+ resolvedAt: now,
772
+ updatedAt: now,
773
+ })
774
+ .where(and(inArray(chatControlActions.id, uncertainControlActionIds), eq(chatControlActions.localDisposition, "pending")));
775
+ }
776
+ const continuedSteers = pendingItemIds.length === 0
777
+ ? []
778
+ : await tx
779
+ .update(chatQueuedMessages)
780
+ .set({
781
+ status: "continuation_pending",
782
+ deliveryDisposition: "continuation_pending",
783
+ reconciliationReason: "runtime_attempt_completed_without_steer_acceptance",
784
+ lastDeliveryReason: null,
785
+ version: sql `${chatQueuedMessages.version} + 1`,
786
+ updatedAt: now,
787
+ })
788
+ .where(and(inArray(chatQueuedMessages.id, pendingItemIds), eq(chatQueuedMessages.status, "steer_pending"), or(isNull(chatQueuedMessages.controlActionId), inArray(chatQueuedMessages.controlActionId, tx
789
+ .select({ id: chatControlActions.id })
790
+ .from(chatControlActions)
791
+ .where(or(eq(chatControlActions.providerDisposition, "not_sent"), eq(chatControlActions.providerDisposition, "rejected"), isNull(chatControlActions.providerDisposition)))))))
792
+ .returning({ controlActionId: chatQueuedMessages.controlActionId });
793
+ const continuedControlActionIds = continuedSteers
794
+ .map((item) => item.controlActionId)
795
+ .filter((id) => Boolean(id));
796
+ if (continuedControlActionIds.length > 0) {
797
+ await tx
798
+ .update(chatControlActions)
799
+ .set({
800
+ localDisposition: "continuation_pending",
801
+ providerDisposition: "not_sent",
802
+ lastError: "runtime_attempt_completed_without_steer_acceptance",
803
+ resolvedAt: now,
804
+ updatedAt: now,
805
+ })
806
+ .where(and(inArray(chatControlActions.id, continuedControlActionIds), eq(chatControlActions.localDisposition, "pending")));
807
+ }
808
+ return row;
809
+ });
810
+ }
811
+ async function appendGenerationEvent(input) {
812
+ return db.transaction(async (tx) => {
813
+ await tx.execute(sql `select id from ${chatGenerations} where ${chatGenerations.id} = ${input.generationId} and ${chatGenerations.orgId} = ${input.orgId} for update`);
814
+ const generation = await tx
815
+ .select({ id: chatGenerations.id })
816
+ .from(chatGenerations)
817
+ .where(and(eq(chatGenerations.id, input.generationId), eq(chatGenerations.orgId, input.orgId)))
818
+ .limit(1)
819
+ .then((rows) => rows[0] ?? null);
820
+ if (!generation)
821
+ throw notFound("Chat generation not found");
822
+ const nextSeq = await tx
823
+ .select({ value: sql `coalesce(max(${chatGenerationEvents.generationSeq}), 0) + 1` })
824
+ .from(chatGenerationEvents)
825
+ .where(eq(chatGenerationEvents.generationId, input.generationId))
826
+ .then((rows) => Number(rows[0]?.value ?? 1));
827
+ const [event] = await tx
828
+ .insert(chatGenerationEvents)
829
+ .values({
830
+ orgId: input.orgId,
831
+ generationId: input.generationId,
832
+ generationSeq: nextSeq,
833
+ attemptEpoch: input.attemptEpoch,
834
+ eventKind: input.eventKind,
835
+ payload: input.payload ?? {},
836
+ controlActionId: input.controlActionId ?? null,
837
+ queueItemId: input.queueItemId ?? null,
838
+ emittedAt: input.emittedAt ?? null,
839
+ })
840
+ .returning();
841
+ if (!event)
842
+ throw new Error("Failed to append chat generation event");
843
+ return event;
844
+ });
845
+ }
846
+ async function beginSteerControlAction(input) {
847
+ return db.transaction(async (tx) => {
848
+ const existingAction = await tx
849
+ .select()
850
+ .from(chatControlActions)
851
+ .where(and(eq(chatControlActions.id, input.controlActionId), eq(chatControlActions.orgId, input.orgId)))
852
+ .limit(1)
853
+ .then((rows) => rows[0] ?? null);
854
+ if (existingAction) {
855
+ const existingItem = await tx
856
+ .select()
857
+ .from(chatQueuedMessages)
858
+ .where(and(eq(chatQueuedMessages.id, input.itemId), eq(chatQueuedMessages.conversationId, input.conversationId), eq(chatQueuedMessages.controlActionId, existingAction.id)))
859
+ .limit(1)
860
+ .then((rows) => rows[0] ?? null);
861
+ const existingGeneration = existingAction.expectedGenerationId
862
+ ? await tx
863
+ .select()
864
+ .from(chatGenerations)
865
+ .where(eq(chatGenerations.id, existingAction.expectedGenerationId))
866
+ .limit(1)
867
+ .then((rows) => rows[0] ?? null)
868
+ : null;
869
+ if (!existingItem || existingAction.actionKind !== "steer") {
870
+ throw conflict("Control action id was already used for a different operation");
871
+ }
872
+ return {
873
+ action: existingAction,
874
+ item: hydrateQueuedMessage(existingItem),
875
+ generation: existingGeneration,
876
+ idempotent: true,
877
+ };
878
+ }
879
+ await tx.execute(sql `select id from ${chatGenerations} where ${chatGenerations.id} = ${input.expectedGenerationId} for update`);
880
+ await tx.execute(sql `select id from ${chatQueuedMessages} where ${chatQueuedMessages.id} = ${input.itemId} for update`);
881
+ const generation = await tx
882
+ .select()
883
+ .from(chatGenerations)
884
+ .where(and(eq(chatGenerations.id, input.expectedGenerationId), eq(chatGenerations.orgId, input.orgId), eq(chatGenerations.conversationId, input.conversationId)))
885
+ .limit(1)
886
+ .then((rows) => rows[0] ?? null);
887
+ const generationAcceptsNativeSteer = Boolean(generation && NATIVE_STEER_GENERATION_STATUSES.includes(generation.status));
888
+ const item = await tx
889
+ .select()
890
+ .from(chatQueuedMessages)
891
+ .where(and(eq(chatQueuedMessages.id, input.itemId), eq(chatQueuedMessages.orgId, input.orgId), eq(chatQueuedMessages.conversationId, input.conversationId), inArray(chatQueuedMessages.status, ["queued", "steer_pending"])))
892
+ .limit(1)
893
+ .then((rows) => rows[0] ?? null);
894
+ if (!item)
895
+ throw conflict("Queued feedback is no longer steerable");
896
+ if (item.status === "steer_pending") {
897
+ const boundAction = item.controlActionId
898
+ ? await tx
899
+ .select()
900
+ .from(chatControlActions)
901
+ .where(and(eq(chatControlActions.id, item.controlActionId), eq(chatControlActions.orgId, input.orgId), eq(chatControlActions.actionKind, "steer")))
902
+ .limit(1)
903
+ .then((rows) => rows[0] ?? null)
904
+ : null;
905
+ if (!boundAction) {
906
+ throw conflict("Queued feedback has an unresolved Steer action");
907
+ }
908
+ const boundGeneration = boundAction.expectedGenerationId
909
+ ? await tx
910
+ .select()
911
+ .from(chatGenerations)
912
+ .where(and(eq(chatGenerations.id, boundAction.expectedGenerationId), eq(chatGenerations.orgId, input.orgId), eq(chatGenerations.conversationId, input.conversationId)))
913
+ .limit(1)
914
+ .then((rows) => rows[0] ?? null)
915
+ : null;
916
+ return {
917
+ action: boundAction,
918
+ item: hydrateQueuedMessage(item),
919
+ generation: boundGeneration,
920
+ idempotent: true,
921
+ };
922
+ }
923
+ if (generationAcceptsNativeSteer && generation && (generation.attemptEpoch !== input.expectedAttemptEpoch
924
+ || generation.controlVersion !== input.expectedControlVersion)) {
925
+ throw conflict("The targeted chat generation control version changed");
926
+ }
927
+ const now = new Date();
928
+ if (!generationAcceptsNativeSteer) {
929
+ const [action] = await tx
930
+ .insert(chatControlActions)
931
+ .values({
932
+ id: input.controlActionId,
933
+ orgId: input.orgId,
934
+ expectedGenerationId: generation?.id ?? null,
935
+ expectedAttemptEpoch: generation?.attemptEpoch ?? null,
936
+ expectedControlVersion: generation?.controlVersion ?? null,
937
+ actionKind: "steer",
938
+ localDisposition: "continuation_pending",
939
+ providerDisposition: "not_sent",
940
+ providerClientMessageId: input.controlActionId,
941
+ resolvedAt: now,
942
+ })
943
+ .returning();
944
+ if (!action)
945
+ throw new Error("Failed to persist Steer continuation action");
946
+ const [updatedItem] = await tx
947
+ .update(chatQueuedMessages)
948
+ .set({
949
+ status: "continuation_pending",
950
+ deliveryIntent: "steer",
951
+ deliveryDisposition: "continuation_pending",
952
+ controlActionId: action.id,
953
+ requestActor: item.requestActor ?? input.requestActor ?? null,
954
+ activeGenerationId: generation?.id ?? item.activeGenerationId,
955
+ attemptEpoch: generation?.attemptEpoch ?? item.attemptEpoch,
956
+ providerClientMessageId: action.providerClientMessageId,
957
+ reconciliationReason: generation ? "target_generation_terminal" : "target_generation_missing",
958
+ lastDeliveryReason: null,
959
+ version: sql `${chatQueuedMessages.version} + 1`,
960
+ updatedAt: now,
961
+ })
962
+ .where(and(eq(chatQueuedMessages.id, item.id), eq(chatQueuedMessages.version, item.version)))
963
+ .returning();
964
+ if (!updatedItem)
965
+ throw conflict("Queued feedback changed while continuation was being scheduled");
966
+ return {
967
+ action,
968
+ item: hydrateQueuedMessage(updatedItem),
969
+ generation,
970
+ idempotent: false,
971
+ };
972
+ }
973
+ if (!generation)
974
+ throw new Error("Expected a native Steer generation");
975
+ const appliedControlVersion = generation.controlVersion + 1;
976
+ const [action] = await tx
977
+ .insert(chatControlActions)
978
+ .values({
979
+ id: input.controlActionId,
980
+ orgId: input.orgId,
981
+ expectedGenerationId: generation.id,
982
+ expectedAttemptEpoch: generation.attemptEpoch,
983
+ expectedControlVersion: generation.controlVersion,
984
+ appliedControlVersion,
985
+ actionKind: "steer",
986
+ localDisposition: "pending",
987
+ providerDisposition: "not_sent",
988
+ controlOwnerToken: generation.controlOwnerToken,
989
+ providerClientMessageId: input.controlActionId,
990
+ })
991
+ .returning();
992
+ if (!action)
993
+ throw new Error("Failed to create chat Steer control action");
994
+ const [updatedGeneration] = await tx
995
+ .update(chatGenerations)
996
+ .set({ controlVersion: appliedControlVersion, updatedAt: now })
997
+ .where(and(eq(chatGenerations.id, generation.id), eq(chatGenerations.controlVersion, generation.controlVersion), eq(chatGenerations.attemptEpoch, generation.attemptEpoch)))
998
+ .returning();
999
+ if (!updatedGeneration)
1000
+ throw conflict("The targeted chat generation control version changed");
1001
+ const [updatedItem] = await tx
1002
+ .update(chatQueuedMessages)
1003
+ .set({
1004
+ status: "steer_pending",
1005
+ deliveryIntent: "steer",
1006
+ deliveryDisposition: "pending",
1007
+ controlActionId: action.id,
1008
+ requestActor: item.requestActor ?? input.requestActor ?? null,
1009
+ activeGenerationId: generation.id,
1010
+ attemptEpoch: generation.attemptEpoch,
1011
+ providerClientMessageId: action.providerClientMessageId,
1012
+ deliveryAttempts: sql `${chatQueuedMessages.deliveryAttempts} + 1`,
1013
+ lastAttemptAt: now,
1014
+ lastDeliveryReason: null,
1015
+ version: sql `${chatQueuedMessages.version} + 1`,
1016
+ updatedAt: now,
1017
+ })
1018
+ .where(and(eq(chatQueuedMessages.id, item.id), eq(chatQueuedMessages.version, item.version)))
1019
+ .returning();
1020
+ if (!updatedItem)
1021
+ throw conflict("Queued feedback changed while Steer was being accepted");
1022
+ return {
1023
+ action,
1024
+ item: hydrateQueuedMessage(updatedItem),
1025
+ generation: updatedGeneration,
1026
+ idempotent: false,
1027
+ };
1028
+ });
1029
+ }
1030
+ async function resolveSteerControlAction(input) {
1031
+ const now = new Date();
1032
+ return db.transaction(async (tx) => {
1033
+ await tx.execute(sql `
1034
+ select ${chatControlActions.id}
1035
+ from ${chatControlActions}
1036
+ where ${chatControlActions.id} = ${input.controlActionId}
1037
+ and ${chatControlActions.orgId} = ${input.orgId}
1038
+ for update
1039
+ `);
1040
+ await tx.execute(sql `
1041
+ select ${chatQueuedMessages.id}
1042
+ from ${chatQueuedMessages}
1043
+ where ${chatQueuedMessages.id} = ${input.itemId}
1044
+ and ${chatQueuedMessages.orgId} = ${input.orgId}
1045
+ and ${chatQueuedMessages.conversationId} = ${input.conversationId}
1046
+ for update
1047
+ `);
1048
+ const existingAction = await tx
1049
+ .select()
1050
+ .from(chatControlActions)
1051
+ .where(and(eq(chatControlActions.id, input.controlActionId), eq(chatControlActions.orgId, input.orgId), eq(chatControlActions.actionKind, "steer")))
1052
+ .limit(1)
1053
+ .then((rows) => rows[0] ?? null);
1054
+ const existingItem = await tx
1055
+ .select()
1056
+ .from(chatQueuedMessages)
1057
+ .where(and(eq(chatQueuedMessages.id, input.itemId), eq(chatQueuedMessages.orgId, input.orgId), eq(chatQueuedMessages.conversationId, input.conversationId), eq(chatQueuedMessages.controlActionId, input.controlActionId)))
1058
+ .limit(1)
1059
+ .then((rows) => rows[0] ?? null);
1060
+ if (!existingAction)
1061
+ throw notFound("Chat Steer control action not found");
1062
+ if (!existingItem)
1063
+ throw notFound("Queued Steer feedback not found");
1064
+ const allowedCurrentStatuses = input.status === "accepted_current"
1065
+ ? ["steer_pending", "acceptance_unknown", "accepted_current"]
1066
+ : input.status === "acceptance_unknown"
1067
+ ? ["steer_pending", "acceptance_unknown"]
1068
+ : input.status === "continuation_pending"
1069
+ ? ["steer_pending", "acceptance_unknown", "continuation_pending"]
1070
+ : input.status === "steer_pending"
1071
+ ? ["steer_pending"]
1072
+ : ["steer_pending", "acceptance_unknown", "continuation_pending", "failed_actionable"];
1073
+ const transitionAllowed = allowedCurrentStatuses.includes(existingItem.status);
1074
+ const preserveAcknowledgement = existingAction.providerDisposition === "acknowledged"
1075
+ && input.providerDisposition !== "acknowledged";
1076
+ const providerDisposition = preserveAcknowledgement
1077
+ ? existingAction.providerDisposition
1078
+ : input.providerDisposition;
1079
+ const [action] = await tx
1080
+ .update(chatControlActions)
1081
+ .set({
1082
+ localDisposition: transitionAllowed ? input.disposition : existingAction.localDisposition,
1083
+ providerDisposition,
1084
+ providerThreadId: input.providerThreadId ?? existingAction.providerThreadId,
1085
+ providerTurnId: input.providerTurnId ?? existingAction.providerTurnId,
1086
+ providerEvidence: input.providerEvidence ?? existingAction.providerEvidence,
1087
+ lastError: transitionAllowed ? input.reason ?? null : existingAction.lastError,
1088
+ providerSentAt: providerDisposition === "not_sent"
1089
+ ? null
1090
+ : existingAction.providerSentAt ?? now,
1091
+ providerAcknowledgedAt: providerDisposition === "acknowledged"
1092
+ ? existingAction.providerAcknowledgedAt ?? now
1093
+ : existingAction.providerAcknowledgedAt,
1094
+ resolvedAt: transitionAllowed
1095
+ ? input.disposition === "pending" ? null : now
1096
+ : existingAction.resolvedAt,
1097
+ updatedAt: now,
1098
+ })
1099
+ .where(and(eq(chatControlActions.id, input.controlActionId), eq(chatControlActions.orgId, input.orgId), eq(chatControlActions.actionKind, "steer")))
1100
+ .returning();
1101
+ if (!action)
1102
+ throw notFound("Chat Steer control action not found");
1103
+ if (!transitionAllowed) {
1104
+ return { action, item: hydrateQueuedMessage(existingItem), applied: false };
1105
+ }
1106
+ const [item] = await tx
1107
+ .update(chatQueuedMessages)
1108
+ .set({
1109
+ status: input.status,
1110
+ deliveryDisposition: input.disposition,
1111
+ providerThreadId: input.providerThreadId ?? existingItem.providerThreadId,
1112
+ providerTurnId: input.providerTurnId ?? existingItem.providerTurnId,
1113
+ providerEvidence: input.providerEvidence ?? existingItem.providerEvidence,
1114
+ reconciliationReason: input.reason ?? null,
1115
+ lastDeliveryReason: input.reason ?? null,
1116
+ steeredAt: input.status === "accepted_current" ? now : existingItem.steeredAt,
1117
+ version: sql `${chatQueuedMessages.version} + 1`,
1118
+ updatedAt: now,
1119
+ })
1120
+ .where(and(eq(chatQueuedMessages.id, existingItem.id), eq(chatQueuedMessages.status, existingItem.status), eq(chatQueuedMessages.version, existingItem.version)))
1121
+ .returning();
1122
+ if (!item)
1123
+ throw conflict("Queued Steer feedback changed while provider evidence was resolving");
1124
+ return { action, item: hydrateQueuedMessage(item), applied: true };
1125
+ });
1126
+ }
1127
+ async function claimSteerProviderSend(input) {
1128
+ return db.transaction(async (tx) => {
1129
+ const existingAction = await tx
1130
+ .select()
1131
+ .from(chatControlActions)
1132
+ .where(and(eq(chatControlActions.id, input.controlActionId), eq(chatControlActions.orgId, input.orgId), eq(chatControlActions.actionKind, "steer")))
1133
+ .limit(1)
1134
+ .then((rows) => rows[0] ?? null);
1135
+ if (!existingAction
1136
+ || existingAction.localDisposition !== "pending"
1137
+ || existingAction.providerDisposition !== "not_sent") {
1138
+ return null;
1139
+ }
1140
+ const generation = existingAction.expectedGenerationId
1141
+ ? await tx
1142
+ .select()
1143
+ .from(chatGenerations)
1144
+ .where(and(eq(chatGenerations.id, existingAction.expectedGenerationId), eq(chatGenerations.orgId, input.orgId)))
1145
+ .limit(1)
1146
+ .for("update")
1147
+ .then((rows) => rows[0] ?? null)
1148
+ : null;
1149
+ const leaseIsCurrent = generation
1150
+ ? await tx
1151
+ .select({
1152
+ value: sql `${chatGenerations.controlLeaseExpiresAt} > clock_timestamp()`,
1153
+ })
1154
+ .from(chatGenerations)
1155
+ .where(eq(chatGenerations.id, generation.id))
1156
+ .limit(1)
1157
+ .then((rows) => rows[0]?.value === true)
1158
+ : false;
1159
+ const now = new Date();
1160
+ const generationFenceMatches = Boolean(generation
1161
+ && NATIVE_STEER_GENERATION_STATUSES.includes(generation.status)
1162
+ && generation.controlState === "ready"
1163
+ && generation.runtimeTerminalAt === null
1164
+ && Boolean(generation.controlOwnerToken)
1165
+ && generation.attemptEpoch === existingAction.expectedAttemptEpoch
1166
+ && generation.controlVersion === existingAction.appliedControlVersion
1167
+ && generation.controlOwnerToken === existingAction.controlOwnerToken
1168
+ && leaseIsCurrent);
1169
+ if (!generationFenceMatches) {
1170
+ await tx.execute(sql `
1171
+ select ${chatControlActions.id}
1172
+ from ${chatControlActions}
1173
+ where ${chatControlActions.id} = ${input.controlActionId}
1174
+ and ${chatControlActions.orgId} = ${input.orgId}
1175
+ for update
1176
+ `);
1177
+ const queueItem = await tx
1178
+ .select()
1179
+ .from(chatQueuedMessages)
1180
+ .where(and(eq(chatQueuedMessages.controlActionId, input.controlActionId), eq(chatQueuedMessages.orgId, input.orgId)))
1181
+ .limit(1)
1182
+ .for("update")
1183
+ .then((rows) => rows[0] ?? null);
1184
+ if (!queueItem)
1185
+ throw conflict("Queued Steer feedback disappeared before provider send");
1186
+ const reason = generation?.stopRequestedAt
1187
+ || generation?.status === "stop_requested"
1188
+ || generation?.status === "stopping"
1189
+ ? "stop_cutoff_won_before_provider_send"
1190
+ : "generation_fence_changed_before_provider_send";
1191
+ const [action] = await tx
1192
+ .update(chatControlActions)
1193
+ .set({
1194
+ localDisposition: "continuation_pending",
1195
+ providerDisposition: "not_sent",
1196
+ providerSentAt: null,
1197
+ lastError: reason,
1198
+ resolvedAt: now,
1199
+ updatedAt: now,
1200
+ })
1201
+ .where(and(eq(chatControlActions.id, input.controlActionId), eq(chatControlActions.orgId, input.orgId), eq(chatControlActions.actionKind, "steer"), eq(chatControlActions.localDisposition, "pending"), eq(chatControlActions.providerDisposition, "not_sent")))
1202
+ .returning();
1203
+ if (!action)
1204
+ return null;
1205
+ const [item] = await tx
1206
+ .update(chatQueuedMessages)
1207
+ .set({
1208
+ status: "continuation_pending",
1209
+ deliveryDisposition: "continuation_pending",
1210
+ reconciliationReason: reason,
1211
+ lastDeliveryReason: null,
1212
+ version: sql `${chatQueuedMessages.version} + 1`,
1213
+ updatedAt: now,
1214
+ })
1215
+ .where(and(eq(chatQueuedMessages.id, queueItem.id), eq(chatQueuedMessages.status, "steer_pending"), eq(chatQueuedMessages.version, queueItem.version)))
1216
+ .returning();
1217
+ if (!item)
1218
+ throw conflict("Queued Steer feedback changed before provider send fallback");
1219
+ return {
1220
+ sendDenied: true,
1221
+ reason,
1222
+ action,
1223
+ item: hydrateQueuedMessage(item),
1224
+ };
1225
+ }
1226
+ const [action] = await tx
1227
+ .update(chatControlActions)
1228
+ .set({
1229
+ providerDisposition: "sent",
1230
+ providerSentAt: now,
1231
+ updatedAt: now,
1232
+ })
1233
+ .where(and(eq(chatControlActions.id, input.controlActionId), eq(chatControlActions.orgId, input.orgId), eq(chatControlActions.actionKind, "steer"), eq(chatControlActions.localDisposition, "pending"), eq(chatControlActions.providerDisposition, "not_sent"), eq(chatControlActions.expectedGenerationId, generation.id), eq(chatControlActions.expectedAttemptEpoch, generation.attemptEpoch), eq(chatControlActions.appliedControlVersion, generation.controlVersion), eq(chatControlActions.controlOwnerToken, generation.controlOwnerToken)))
1234
+ .returning();
1235
+ return action ?? null;
1236
+ });
1237
+ }
1238
+ async function releaseSteerProviderSendClaim(input) {
1239
+ const now = new Date();
1240
+ return db.transaction(async (tx) => {
1241
+ await tx.execute(sql `
1242
+ select ${chatControlActions.id}
1243
+ from ${chatControlActions}
1244
+ where ${chatControlActions.id} = ${input.controlActionId}
1245
+ and ${chatControlActions.orgId} = ${input.orgId}
1246
+ for update
1247
+ `);
1248
+ const existingAction = await tx
1249
+ .select()
1250
+ .from(chatControlActions)
1251
+ .where(and(eq(chatControlActions.id, input.controlActionId), eq(chatControlActions.orgId, input.orgId), eq(chatControlActions.actionKind, "steer")))
1252
+ .limit(1)
1253
+ .then((rows) => rows[0] ?? null);
1254
+ if (!existingAction
1255
+ || existingAction.providerDisposition !== "sent"
1256
+ || existingAction.providerAcknowledgedAt) {
1257
+ return null;
1258
+ }
1259
+ await tx.execute(sql `
1260
+ select ${chatQueuedMessages.id}
1261
+ from ${chatQueuedMessages}
1262
+ where ${chatQueuedMessages.controlActionId} = ${input.controlActionId}
1263
+ for update
1264
+ `);
1265
+ const [action] = await tx
1266
+ .update(chatControlActions)
1267
+ .set({
1268
+ localDisposition: "pending",
1269
+ providerDisposition: "not_sent",
1270
+ providerSentAt: null,
1271
+ lastError: input.reason,
1272
+ resolvedAt: null,
1273
+ updatedAt: now,
1274
+ })
1275
+ .where(and(eq(chatControlActions.id, input.controlActionId), eq(chatControlActions.orgId, input.orgId), eq(chatControlActions.actionKind, "steer"), eq(chatControlActions.providerDisposition, "sent"), isNull(chatControlActions.providerAcknowledgedAt)))
1276
+ .returning();
1277
+ if (!action)
1278
+ return null;
1279
+ await tx
1280
+ .update(chatQueuedMessages)
1281
+ .set({
1282
+ status: "steer_pending",
1283
+ deliveryDisposition: "pending",
1284
+ reconciliationReason: input.reason,
1285
+ lastDeliveryReason: input.reason,
1286
+ version: sql `${chatQueuedMessages.version} + 1`,
1287
+ updatedAt: now,
1288
+ })
1289
+ .where(and(eq(chatQueuedMessages.controlActionId, input.controlActionId), inArray(chatQueuedMessages.status, ["steer_pending", "acceptance_unknown"])));
1290
+ return action;
1291
+ });
1292
+ }
507
1293
  async function createQueuedMessage(input) {
508
1294
  const payload = {
509
1295
  ...input.payload,
@@ -545,6 +1331,7 @@ export function chatService(db) {
545
1331
  clientMutationId: input.clientMutationId,
546
1332
  position: Number(positionRow?.nextPosition ?? 1),
547
1333
  payload,
1334
+ requestActor: input.requestActor ?? null,
548
1335
  expectedGenerationId: input.expectedGenerationId ?? null,
549
1336
  })
550
1337
  .returning();
@@ -610,6 +1397,99 @@ export function chatService(db) {
610
1397
  throw conflict("Queued message was changed or is no longer cancellable");
611
1398
  return hydrateQueuedMessage(row);
612
1399
  }
1400
+ async function scheduleSteerContinuation(input) {
1401
+ return db.transaction(async (tx) => {
1402
+ const existingAction = await tx
1403
+ .select()
1404
+ .from(chatControlActions)
1405
+ .where(and(eq(chatControlActions.id, input.controlActionId), eq(chatControlActions.orgId, input.orgId)))
1406
+ .limit(1)
1407
+ .then((rows) => rows[0] ?? null);
1408
+ if (existingAction) {
1409
+ const existingItem = await tx
1410
+ .select()
1411
+ .from(chatQueuedMessages)
1412
+ .where(and(eq(chatQueuedMessages.id, input.itemId), eq(chatQueuedMessages.orgId, input.orgId), eq(chatQueuedMessages.conversationId, input.conversationId), eq(chatQueuedMessages.controlActionId, existingAction.id)))
1413
+ .limit(1)
1414
+ .then((rows) => rows[0] ?? null);
1415
+ if (!existingItem || existingAction.actionKind !== "steer") {
1416
+ throw conflict("Control action id was already used for a different operation");
1417
+ }
1418
+ return {
1419
+ action: existingAction,
1420
+ item: hydrateQueuedMessage(existingItem),
1421
+ idempotent: true,
1422
+ };
1423
+ }
1424
+ await tx.execute(sql `select id from ${chatQueuedMessages} where ${chatQueuedMessages.id} = ${input.itemId} for update`);
1425
+ const item = await tx
1426
+ .select()
1427
+ .from(chatQueuedMessages)
1428
+ .where(and(eq(chatQueuedMessages.id, input.itemId), eq(chatQueuedMessages.orgId, input.orgId), eq(chatQueuedMessages.conversationId, input.conversationId), inArray(chatQueuedMessages.status, ["queued", "steer_pending", "continuation_pending"])))
1429
+ .limit(1)
1430
+ .then((rows) => rows[0] ?? null);
1431
+ if (!item)
1432
+ throw conflict("Queued feedback is no longer schedulable");
1433
+ const targetGenerationId = item.expectedGenerationId ?? item.activeGenerationId;
1434
+ const targetGeneration = targetGenerationId
1435
+ ? await tx
1436
+ .select()
1437
+ .from(chatGenerations)
1438
+ .where(and(eq(chatGenerations.id, targetGenerationId), eq(chatGenerations.orgId, input.orgId), eq(chatGenerations.conversationId, input.conversationId)))
1439
+ .limit(1)
1440
+ .then((rows) => rows[0] ?? null)
1441
+ : await tx
1442
+ .select()
1443
+ .from(chatGenerations)
1444
+ .where(and(eq(chatGenerations.orgId, input.orgId), eq(chatGenerations.conversationId, input.conversationId)))
1445
+ .orderBy(desc(chatGenerations.startedAt), desc(chatGenerations.createdAt))
1446
+ .limit(1)
1447
+ .then((rows) => rows[0] ?? null);
1448
+ const now = new Date();
1449
+ const [action] = await tx
1450
+ .insert(chatControlActions)
1451
+ .values({
1452
+ id: input.controlActionId,
1453
+ orgId: input.orgId,
1454
+ expectedGenerationId: targetGeneration?.id ?? null,
1455
+ expectedAttemptEpoch: targetGeneration?.attemptEpoch ?? null,
1456
+ expectedControlVersion: targetGeneration?.controlVersion ?? null,
1457
+ actionKind: "steer",
1458
+ localDisposition: "continuation_pending",
1459
+ providerDisposition: "not_sent",
1460
+ providerClientMessageId: input.controlActionId,
1461
+ resolvedAt: now,
1462
+ })
1463
+ .returning();
1464
+ if (!action)
1465
+ throw new Error("Failed to persist Steer continuation action");
1466
+ const [updatedItem] = await tx
1467
+ .update(chatQueuedMessages)
1468
+ .set({
1469
+ status: "continuation_pending",
1470
+ deliveryIntent: "steer",
1471
+ deliveryDisposition: "continuation_pending",
1472
+ controlActionId: action.id,
1473
+ requestActor: item.requestActor ?? input.requestActor ?? null,
1474
+ activeGenerationId: targetGeneration?.id ?? item.activeGenerationId,
1475
+ attemptEpoch: targetGeneration?.attemptEpoch ?? item.attemptEpoch,
1476
+ providerClientMessageId: action.providerClientMessageId,
1477
+ reconciliationReason: targetGeneration ? "target_generation_terminal" : "no_active_generation",
1478
+ lastDeliveryReason: null,
1479
+ version: sql `${chatQueuedMessages.version} + 1`,
1480
+ updatedAt: now,
1481
+ })
1482
+ .where(and(eq(chatQueuedMessages.id, item.id), eq(chatQueuedMessages.version, item.version)))
1483
+ .returning();
1484
+ if (!updatedItem)
1485
+ throw conflict("Queued feedback changed while continuation was being scheduled");
1486
+ return {
1487
+ action,
1488
+ item: hydrateQueuedMessage(updatedItem),
1489
+ idempotent: false,
1490
+ };
1491
+ });
1492
+ }
613
1493
  async function markQueuedMessageSteerFallback(input) {
614
1494
  const now = new Date();
615
1495
  const [row] = await db
@@ -629,6 +1509,342 @@ export function chatService(db) {
629
1509
  throw conflict("Queued message was changed or is no longer steerable");
630
1510
  return hydrateQueuedMessage(row);
631
1511
  }
1512
+ async function claimNextServerQueuedMessage(input) {
1513
+ const now = input.now ?? new Date();
1514
+ const leaseExpiresAt = new Date(now.getTime() + input.leaseMs);
1515
+ return db.transaction(async (tx) => {
1516
+ const batchSize = 25;
1517
+ let scanOffset = 0;
1518
+ while (true) {
1519
+ const candidates = await tx
1520
+ .select()
1521
+ .from(chatQueuedMessages)
1522
+ .where(and(inArray(chatQueuedMessages.status, ["continuation_pending", "steer_pending", "queued"]), sql `jsonb_typeof(${chatQueuedMessages.requestActor}) = 'object'`, or(isNull(chatQueuedMessages.deliveryLeaseExpiresAt), lt(chatQueuedMessages.deliveryLeaseExpiresAt, now)), sql `${chatQueuedMessages.status} <> 'steer_pending' or coalesce((
1523
+ select steer_action.provider_disposition
1524
+ from ${chatControlActions} as steer_action
1525
+ where steer_action.id = ${chatQueuedMessages.controlActionId}
1526
+ limit 1
1527
+ ), 'not_sent') in ('not_sent', 'rejected')`))
1528
+ .orderBy(sql `case
1529
+ when ${chatQueuedMessages.status} = 'continuation_pending' then 0
1530
+ when ${chatQueuedMessages.status} = 'steer_pending' then 1
1531
+ else 2
1532
+ end`, asc(chatQueuedMessages.position), asc(chatQueuedMessages.createdAt))
1533
+ .limit(batchSize)
1534
+ .offset(scanOffset)
1535
+ .for("update", { skipLocked: true });
1536
+ for (const candidate of candidates) {
1537
+ if (!candidate.requestActor || typeof candidate.requestActor !== "object" || Array.isArray(candidate.requestActor)) {
1538
+ continue;
1539
+ }
1540
+ const latestGeneration = await tx
1541
+ .select()
1542
+ .from(chatGenerations)
1543
+ .where(and(eq(chatGenerations.orgId, candidate.orgId), eq(chatGenerations.conversationId, candidate.conversationId)))
1544
+ .orderBy(desc(chatGenerations.startedAt), desc(chatGenerations.createdAt))
1545
+ .limit(1)
1546
+ .then((rows) => rows[0] ?? null);
1547
+ const isSteer = candidate.deliveryIntent === "steer"
1548
+ || candidate.status === "steer_pending"
1549
+ || candidate.status === "continuation_pending";
1550
+ if (latestGeneration
1551
+ && ACTIVE_CHAT_GENERATION_STATUSES.some((status) => status === latestGeneration.status)) {
1552
+ continue;
1553
+ }
1554
+ if (candidate.status === "queued"
1555
+ && latestGeneration
1556
+ && latestGeneration.status !== "completed") {
1557
+ continue;
1558
+ }
1559
+ if (isSteer
1560
+ && latestGeneration
1561
+ && ["failed", "control_lost", "interrupted_unverified"].includes(latestGeneration.status)) {
1562
+ await tx
1563
+ .update(chatQueuedMessages)
1564
+ .set({
1565
+ status: "failed_actionable",
1566
+ deliveryDisposition: "failed_actionable",
1567
+ reconciliationReason: `prior_generation_${latestGeneration.status}`,
1568
+ lastDeliveryReason: `prior_generation_${latestGeneration.status}`,
1569
+ updatedAt: now,
1570
+ version: sql `${chatQueuedMessages.version} + 1`,
1571
+ })
1572
+ .where(eq(chatQueuedMessages.id, candidate.id));
1573
+ if (candidate.controlActionId) {
1574
+ await tx
1575
+ .update(chatControlActions)
1576
+ .set({
1577
+ localDisposition: "failed_actionable",
1578
+ lastError: `prior_generation_${latestGeneration.status}`,
1579
+ resolvedAt: now,
1580
+ updatedAt: now,
1581
+ })
1582
+ .where(eq(chatControlActions.id, candidate.controlActionId));
1583
+ }
1584
+ continue;
1585
+ }
1586
+ const generationId = randomUUID();
1587
+ const leaseToken = randomUUID();
1588
+ const leaseEpoch = candidate.deliveryLeaseEpoch + 1;
1589
+ await tx.insert(chatGenerations).values({
1590
+ id: generationId,
1591
+ orgId: candidate.orgId,
1592
+ conversationId: candidate.conversationId,
1593
+ status: "active",
1594
+ attemptEpoch: 0,
1595
+ controlVersion: 0,
1596
+ controlState: "unregistered",
1597
+ controlOwnerToken: leaseToken,
1598
+ controlLeaseExpiresAt: new Date(now.getTime() + CHAT_GENERATION_CONTROL_LEASE_MS),
1599
+ startedAt: now,
1600
+ createdAt: now,
1601
+ updatedAt: now,
1602
+ });
1603
+ let userMessageId = candidate.continuationMessageId;
1604
+ if (userMessageId) {
1605
+ const existingMessage = await tx
1606
+ .select({ id: chatMessages.id })
1607
+ .from(chatMessages)
1608
+ .where(and(eq(chatMessages.id, userMessageId), eq(chatMessages.orgId, candidate.orgId), eq(chatMessages.conversationId, candidate.conversationId), eq(chatMessages.role, "user")))
1609
+ .limit(1)
1610
+ .then((rows) => rows[0] ?? null);
1611
+ if (!existingMessage)
1612
+ userMessageId = null;
1613
+ }
1614
+ if (!userMessageId) {
1615
+ userMessageId = randomUUID();
1616
+ await tx.insert(chatMessages).values({
1617
+ id: userMessageId,
1618
+ orgId: candidate.orgId,
1619
+ conversationId: candidate.conversationId,
1620
+ role: "user",
1621
+ kind: "message",
1622
+ status: "completed",
1623
+ body: normalizeQueuedPayload(candidate.payload).body,
1624
+ structuredPayload: null,
1625
+ chatTurnId: randomUUID(),
1626
+ turnVariant: 0,
1627
+ createdAt: now,
1628
+ updatedAt: now,
1629
+ });
1630
+ await tx
1631
+ .update(chatConversations)
1632
+ .set({ lastMessageAt: now, updatedAt: now })
1633
+ .where(and(eq(chatConversations.id, candidate.conversationId), eq(chatConversations.orgId, candidate.orgId)));
1634
+ }
1635
+ const [claimed] = await tx
1636
+ .update(chatQueuedMessages)
1637
+ .set({
1638
+ status: isSteer ? "running_next" : "dequeue_claimed",
1639
+ deliveryDisposition: isSteer ? "running_next" : null,
1640
+ continuationGenerationId: generationId,
1641
+ continuationMessageId: userMessageId,
1642
+ sourceMessageId: userMessageId,
1643
+ deliveredMessageId: userMessageId,
1644
+ deliveryLeaseToken: leaseToken,
1645
+ deliveryLeaseEpoch: leaseEpoch,
1646
+ deliveryLeaseOwner: input.workerId,
1647
+ deliveryLeaseExpiresAt: leaseExpiresAt,
1648
+ dequeuedAt: now,
1649
+ deliveryAttempts: sql `${chatQueuedMessages.deliveryAttempts} + 1`,
1650
+ lastAttemptAt: now,
1651
+ lastDeliveryReason: null,
1652
+ reconciliationReason: "server_claimed",
1653
+ version: sql `${chatQueuedMessages.version} + 1`,
1654
+ updatedAt: now,
1655
+ })
1656
+ .where(and(eq(chatQueuedMessages.id, candidate.id), eq(chatQueuedMessages.version, candidate.version)))
1657
+ .returning();
1658
+ if (!claimed)
1659
+ throw conflict("Queued continuation changed while being claimed");
1660
+ if (candidate.controlActionId) {
1661
+ await tx
1662
+ .update(chatControlActions)
1663
+ .set({
1664
+ localDisposition: "running_next",
1665
+ resolvedAt: null,
1666
+ updatedAt: now,
1667
+ })
1668
+ .where(and(eq(chatControlActions.id, candidate.controlActionId), eq(chatControlActions.orgId, candidate.orgId)));
1669
+ }
1670
+ return {
1671
+ item: hydrateQueuedMessage(claimed),
1672
+ generationId,
1673
+ userMessageId,
1674
+ leaseToken,
1675
+ leaseEpoch,
1676
+ };
1677
+ }
1678
+ if (candidates.length < batchSize)
1679
+ return null;
1680
+ scanOffset += candidates.length;
1681
+ }
1682
+ });
1683
+ }
1684
+ async function renewServerQueuedMessageClaim(input) {
1685
+ const now = input.now ?? new Date();
1686
+ const [row] = await db
1687
+ .update(chatQueuedMessages)
1688
+ .set({
1689
+ deliveryLeaseExpiresAt: new Date(now.getTime() + input.leaseMs),
1690
+ updatedAt: now,
1691
+ })
1692
+ .where(and(eq(chatQueuedMessages.id, input.itemId), eq(chatQueuedMessages.continuationGenerationId, input.generationId), eq(chatQueuedMessages.deliveryLeaseToken, input.leaseToken), eq(chatQueuedMessages.deliveryLeaseEpoch, input.leaseEpoch), inArray(chatQueuedMessages.status, SERVER_QUEUE_RUNNING_STATUSES)))
1693
+ .returning({ id: chatQueuedMessages.id });
1694
+ return Boolean(row);
1695
+ }
1696
+ async function completeServerQueuedMessageDelivery(input) {
1697
+ const now = new Date();
1698
+ return db.transaction(async (tx) => {
1699
+ const item = await tx
1700
+ .select()
1701
+ .from(chatQueuedMessages)
1702
+ .where(and(eq(chatQueuedMessages.id, input.itemId), eq(chatQueuedMessages.continuationGenerationId, input.generationId), eq(chatQueuedMessages.deliveryLeaseToken, input.leaseToken), eq(chatQueuedMessages.deliveryLeaseEpoch, input.leaseEpoch), inArray(chatQueuedMessages.status, SERVER_QUEUE_RUNNING_STATUSES)))
1703
+ .limit(1)
1704
+ .then((rows) => rows[0] ?? null);
1705
+ if (!item)
1706
+ return null;
1707
+ const isSteer = item.deliveryIntent === "steer" || item.status === "running_next";
1708
+ const succeeded = input.status === "completed";
1709
+ const nextStatus = succeeded
1710
+ ? (isSteer ? "delivered" : "completed")
1711
+ : "failed_actionable";
1712
+ const nextDisposition = succeeded
1713
+ ? (isSteer ? "delivered" : null)
1714
+ : "failed_actionable";
1715
+ const [updated] = await tx
1716
+ .update(chatQueuedMessages)
1717
+ .set({
1718
+ status: nextStatus,
1719
+ deliveryDisposition: nextDisposition,
1720
+ deliveryLeaseToken: null,
1721
+ deliveryLeaseOwner: null,
1722
+ deliveryLeaseExpiresAt: null,
1723
+ lastDeliveryReason: succeeded ? null : (input.reason ?? input.status),
1724
+ reconciliationReason: succeeded ? null : (input.reason ?? input.status),
1725
+ version: sql `${chatQueuedMessages.version} + 1`,
1726
+ updatedAt: now,
1727
+ })
1728
+ .where(and(eq(chatQueuedMessages.id, item.id), eq(chatQueuedMessages.version, item.version)))
1729
+ .returning();
1730
+ if (item.controlActionId) {
1731
+ await tx
1732
+ .update(chatControlActions)
1733
+ .set({
1734
+ localDisposition: nextDisposition ?? (succeeded ? "delivered" : "failed_actionable"),
1735
+ lastError: succeeded ? null : (input.reason ?? input.status),
1736
+ resolvedAt: now,
1737
+ updatedAt: now,
1738
+ })
1739
+ .where(eq(chatControlActions.id, item.controlActionId));
1740
+ }
1741
+ return updated ? hydrateQueuedMessage(updated) : null;
1742
+ });
1743
+ }
1744
+ async function releaseServerQueuedMessageClaim(input) {
1745
+ const now = new Date();
1746
+ return db.transaction(async (tx) => {
1747
+ const item = await tx
1748
+ .select()
1749
+ .from(chatQueuedMessages)
1750
+ .where(and(eq(chatQueuedMessages.id, input.itemId), eq(chatQueuedMessages.continuationGenerationId, input.generationId), eq(chatQueuedMessages.deliveryLeaseToken, input.leaseToken), eq(chatQueuedMessages.deliveryLeaseEpoch, input.leaseEpoch), inArray(chatQueuedMessages.status, SERVER_QUEUE_RUNNING_STATUSES)))
1751
+ .limit(1)
1752
+ .then((rows) => rows[0] ?? null);
1753
+ if (!item)
1754
+ return null;
1755
+ const generation = await tx
1756
+ .select()
1757
+ .from(chatGenerations)
1758
+ .where(eq(chatGenerations.id, input.generationId))
1759
+ .limit(1)
1760
+ .then((rows) => rows[0] ?? null);
1761
+ const providerMayHaveStarted = Boolean(generation && (generation.attemptEpoch > 0
1762
+ || generation.status === "starting"
1763
+ || generation.status === "running"
1764
+ || generation.status === "tool_busy"
1765
+ || generation.status === "closing"));
1766
+ const isSteer = item.deliveryIntent === "steer" || item.status === "running_next";
1767
+ const nextStatus = providerMayHaveStarted
1768
+ ? (isSteer ? "acceptance_unknown" : "failed_actionable")
1769
+ : (isSteer ? "continuation_pending" : "queued");
1770
+ const nextDisposition = providerMayHaveStarted
1771
+ ? (isSteer ? "acceptance_unknown" : "failed_actionable")
1772
+ : (isSteer ? "continuation_pending" : null);
1773
+ const [updated] = await tx
1774
+ .update(chatQueuedMessages)
1775
+ .set({
1776
+ status: nextStatus,
1777
+ deliveryDisposition: nextDisposition,
1778
+ continuationGenerationId: null,
1779
+ deliveryLeaseToken: null,
1780
+ deliveryLeaseOwner: null,
1781
+ deliveryLeaseExpiresAt: null,
1782
+ lastDeliveryReason: input.reason,
1783
+ reconciliationReason: input.reason,
1784
+ version: sql `${chatQueuedMessages.version} + 1`,
1785
+ updatedAt: now,
1786
+ })
1787
+ .where(and(eq(chatQueuedMessages.id, item.id), eq(chatQueuedMessages.version, item.version)))
1788
+ .returning();
1789
+ if (generation) {
1790
+ await tx
1791
+ .update(chatGenerations)
1792
+ .set({
1793
+ status: "aborted",
1794
+ terminalReason: input.reason,
1795
+ controlState: providerMayHaveStarted ? "control_lost" : "terminal",
1796
+ runtimeTerminalAt: now,
1797
+ completedAt: now,
1798
+ updatedAt: now,
1799
+ })
1800
+ .where(eq(chatGenerations.id, generation.id));
1801
+ }
1802
+ if (item.controlActionId) {
1803
+ await tx
1804
+ .update(chatControlActions)
1805
+ .set({
1806
+ localDisposition: nextDisposition ?? "pending",
1807
+ providerDisposition: providerMayHaveStarted ? "unverified" : "not_sent",
1808
+ lastError: input.reason,
1809
+ resolvedAt: providerMayHaveStarted ? now : null,
1810
+ updatedAt: now,
1811
+ })
1812
+ .where(eq(chatControlActions.id, item.controlActionId));
1813
+ }
1814
+ return updated ? hydrateQueuedMessage(updated) : null;
1815
+ });
1816
+ }
1817
+ async function recoverExpiredServerQueueClaims(now = new Date()) {
1818
+ const expired = await db
1819
+ .select({
1820
+ id: chatQueuedMessages.id,
1821
+ generationId: chatQueuedMessages.continuationGenerationId,
1822
+ leaseToken: chatQueuedMessages.deliveryLeaseToken,
1823
+ leaseEpoch: chatQueuedMessages.deliveryLeaseEpoch,
1824
+ })
1825
+ .from(chatQueuedMessages)
1826
+ .where(and(inArray(chatQueuedMessages.status, SERVER_QUEUE_RUNNING_STATUSES), lt(chatQueuedMessages.deliveryLeaseExpiresAt, now)));
1827
+ let requeued = 0;
1828
+ let ambiguous = 0;
1829
+ for (const row of expired) {
1830
+ if (!row.generationId || !row.leaseToken)
1831
+ continue;
1832
+ const recovered = await releaseServerQueuedMessageClaim({
1833
+ itemId: row.id,
1834
+ generationId: row.generationId,
1835
+ leaseToken: row.leaseToken,
1836
+ leaseEpoch: row.leaseEpoch,
1837
+ reason: "server_continuation_lease_expired",
1838
+ });
1839
+ if (!recovered)
1840
+ continue;
1841
+ if (recovered.status === "continuation_pending" || recovered.status === "queued")
1842
+ requeued += 1;
1843
+ else
1844
+ ambiguous += 1;
1845
+ }
1846
+ return { inspected: expired.length, requeued, ambiguous };
1847
+ }
632
1848
  async function claimNextQueuedMessage(conversationId) {
633
1849
  await reclaimStaleQueuedMessageClaims(conversationId);
634
1850
  const now = new Date();
@@ -770,6 +1986,16 @@ export function chatService(db) {
770
1986
  if (status !== "all") {
771
1987
  conditions.push(eq(chatConversations.status, status));
772
1988
  }
1989
+ if (options?.projectId) {
1990
+ conditions.push(sql `EXISTS (
1991
+ SELECT 1
1992
+ FROM ${chatContextLinks}
1993
+ WHERE ${chatContextLinks.conversationId} = ${chatConversations.id}
1994
+ AND ${chatContextLinks.orgId} = ${orgId}
1995
+ AND ${chatContextLinks.entityType} = 'project'
1996
+ AND ${chatContextLinks.entityId} = ${options.projectId}
1997
+ )`);
1998
+ }
773
1999
  if (hasSearch) {
774
2000
  conditions.push(sql `(
775
2001
  ${chatConversations.title} ILIKE ${containsPattern} ESCAPE '\\'
@@ -990,6 +2216,28 @@ export function chatService(db) {
990
2216
  }
991
2217
  async function forkConversation(input) {
992
2218
  const created = await db.transaction(async (tx) => {
2219
+ const initialSource = await tx
2220
+ .select()
2221
+ .from(chatConversations)
2222
+ .where(and(eq(chatConversations.id, input.sourceConversationId), eq(chatConversations.orgId, input.orgId)))
2223
+ .then((rows) => rows[0] ?? null);
2224
+ if (!initialSource)
2225
+ throw notFound("Chat conversation not found");
2226
+ const initialRootConversationId = initialSource.forkRootConversationId ?? initialSource.id;
2227
+ await tx.execute(sql `
2228
+ SELECT ${chatConversations.id}
2229
+ FROM ${chatConversations}
2230
+ WHERE ${chatConversations.id} = ${initialRootConversationId}
2231
+ FOR UPDATE
2232
+ `);
2233
+ if (initialSource.id !== initialRootConversationId) {
2234
+ await tx.execute(sql `
2235
+ SELECT ${chatConversations.id}
2236
+ FROM ${chatConversations}
2237
+ WHERE ${chatConversations.id} = ${initialSource.id}
2238
+ FOR UPDATE
2239
+ `);
2240
+ }
993
2241
  const source = await tx
994
2242
  .select()
995
2243
  .from(chatConversations)
@@ -998,6 +2246,12 @@ export function chatService(db) {
998
2246
  if (!source)
999
2247
  throw notFound("Chat conversation not found");
1000
2248
  const rootConversationId = source.forkRootConversationId ?? source.id;
2249
+ const familyTitles = await tx
2250
+ .select({ title: chatConversations.title })
2251
+ .from(chatConversations)
2252
+ .where(and(eq(chatConversations.orgId, input.orgId), or(eq(chatConversations.id, rootConversationId), eq(chatConversations.forkRootConversationId, rootConversationId))))
2253
+ .then((rows) => rows.map((row) => row.title));
2254
+ const childTitle = input.title?.trim() || nextForkTitle(source, familyTitles);
1001
2255
  const messageConditions = [
1002
2256
  eq(chatMessages.conversationId, source.id),
1003
2257
  eq(chatMessages.orgId, input.orgId),
@@ -1028,7 +2282,7 @@ export function chatService(db) {
1028
2282
  .values({
1029
2283
  orgId: input.orgId,
1030
2284
  status: "active",
1031
- title: input.title?.trim() || source.title,
2285
+ title: childTitle,
1032
2286
  summary: source.summary,
1033
2287
  preferredAgentId: source.preferredAgentId,
1034
2288
  routedAgentId: source.routedAgentId,
@@ -1073,22 +2327,77 @@ export function chatService(db) {
1073
2327
  ? messagesToCopy.slice(0, messagesToCopy.findIndex((message) => message.id === input.sourceMessageId) + 1)
1074
2328
  : messagesToCopy;
1075
2329
  if (forkMessages.length > 0) {
1076
- await tx.insert(chatMessages).values(forkMessages.map((message) => ({
1077
- orgId: input.orgId,
1078
- conversationId: child.id,
1079
- role: message.role,
1080
- kind: message.kind,
1081
- status: message.status === "streaming" ? "interrupted" : message.status,
1082
- body: message.body,
1083
- structuredPayload: null,
1084
- approvalId: null,
1085
- runId: null,
1086
- replyingAgentId: message.replyingAgentId,
1087
- chatTurnId: null,
1088
- turnVariant: 0,
1089
- createdAt: message.createdAt,
1090
- updatedAt: message.updatedAt,
1091
- })));
2330
+ for (const message of forkMessages) {
2331
+ const copiedMessageId = randomUUID();
2332
+ const visualMappings = chatInlineVisualMappingsFromStructuredPayload(message.structuredPayload);
2333
+ const readyAttachmentIds = visualMappings
2334
+ .filter((mapping) => mapping.status === "ready")
2335
+ .map((mapping) => mapping.attachmentId);
2336
+ const copiedAttachmentIdBySourceId = new Map();
2337
+ const pendingVisualAttachments = [];
2338
+ if (readyAttachmentIds.length > 0) {
2339
+ const sourceVisualAttachments = await tx
2340
+ .select({
2341
+ id: chatAttachments.id,
2342
+ assetId: chatAttachments.assetId,
2343
+ contentType: assets.contentType,
2344
+ createdByAgentId: assets.createdByAgentId,
2345
+ createdByUserId: assets.createdByUserId,
2346
+ })
2347
+ .from(chatAttachments)
2348
+ .innerJoin(assets, eq(chatAttachments.assetId, assets.id))
2349
+ .where(and(eq(chatAttachments.conversationId, source.id), eq(chatAttachments.messageId, message.id), inArray(chatAttachments.id, readyAttachmentIds)));
2350
+ const safeVisualAttachments = sourceVisualAttachments.filter((attachment) => attachment.contentType === "text/html"
2351
+ && Boolean(attachment.createdByAgentId)
2352
+ && !attachment.createdByUserId);
2353
+ if (safeVisualAttachments.length > 0) {
2354
+ pendingVisualAttachments.push(...safeVisualAttachments.map((attachment) => {
2355
+ const copiedAttachmentId = randomUUID();
2356
+ copiedAttachmentIdBySourceId.set(attachment.id, copiedAttachmentId);
2357
+ return {
2358
+ id: copiedAttachmentId,
2359
+ orgId: input.orgId,
2360
+ conversationId: child.id,
2361
+ messageId: copiedMessageId,
2362
+ assetId: attachment.assetId,
2363
+ };
2364
+ }));
2365
+ }
2366
+ }
2367
+ const copiedVisualMappings = visualMappings.map((mapping) => {
2368
+ if (mapping.status === "unavailable")
2369
+ return mapping;
2370
+ const copiedAttachmentId = copiedAttachmentIdBySourceId.get(mapping.attachmentId);
2371
+ return copiedAttachmentId
2372
+ ? { ...mapping, attachmentId: copiedAttachmentId }
2373
+ : {
2374
+ directiveIndex: mapping.directiveIndex,
2375
+ file: mapping.file,
2376
+ status: "unavailable",
2377
+ reason: "fork_source_missing",
2378
+ };
2379
+ });
2380
+ await tx.insert(chatMessages).values({
2381
+ id: copiedMessageId,
2382
+ orgId: input.orgId,
2383
+ conversationId: child.id,
2384
+ role: message.role,
2385
+ kind: message.kind,
2386
+ status: message.status === "streaming" ? "interrupted" : message.status,
2387
+ body: message.body,
2388
+ structuredPayload: copiedVisualMappings.length > 0 ? { inlineVisuals: copiedVisualMappings } : null,
2389
+ approvalId: null,
2390
+ runId: null,
2391
+ replyingAgentId: message.replyingAgentId,
2392
+ chatTurnId: null,
2393
+ turnVariant: 0,
2394
+ createdAt: message.createdAt,
2395
+ updatedAt: message.updatedAt,
2396
+ });
2397
+ if (pendingVisualAttachments.length > 0) {
2398
+ await tx.insert(chatAttachments).values(pendingVisualAttachments);
2399
+ }
2400
+ }
1092
2401
  }
1093
2402
  const [systemEvent] = await tx
1094
2403
  .insert(chatMessages)
@@ -1175,6 +2484,14 @@ export function chatService(db) {
1175
2484
  .where(eq(chatAttachments.conversationId, conversationId));
1176
2485
  return rows;
1177
2486
  }
2487
+ async function assetHasAttachments(assetId) {
2488
+ const [row] = await db
2489
+ .select({ id: chatAttachments.id })
2490
+ .from(chatAttachments)
2491
+ .where(eq(chatAttachments.assetId, assetId))
2492
+ .limit(1);
2493
+ return Boolean(row);
2494
+ }
1178
2495
  async function remove(id) {
1179
2496
  return db.transaction(async (tx) => {
1180
2497
  const attachmentRows = await tx
@@ -1189,7 +2506,10 @@ export function chatService(db) {
1189
2506
  return null;
1190
2507
  const assetIds = [...new Set(attachmentRows.map((row) => row.assetId))];
1191
2508
  if (assetIds.length > 0) {
1192
- await tx.delete(assets).where(inArray(assets.id, assetIds));
2509
+ await tx.delete(assets).where(and(inArray(assets.id, assetIds), sql `not exists (
2510
+ select 1 from ${chatAttachments}
2511
+ where ${chatAttachments.assetId} = ${assets.id}
2512
+ )`));
1193
2513
  }
1194
2514
  return deleted;
1195
2515
  });
@@ -2081,6 +3401,7 @@ export function chatService(db) {
2081
3401
  });
2082
3402
  }
2083
3403
  return {
3404
+ generationProtocol,
2084
3405
  list,
2085
3406
  listSummaries,
2086
3407
  listPinnedSummaries,
@@ -2091,6 +3412,7 @@ export function chatService(db) {
2091
3412
  updateDefaultTitle,
2092
3413
  replaceSystemGeneratedTitle,
2093
3414
  listAttachmentsForConversation,
3415
+ assetHasAttachments,
2094
3416
  remove,
2095
3417
  forkConversation,
2096
3418
  resolve,
@@ -2098,6 +3420,11 @@ export function chatService(db) {
2098
3420
  markUnread,
2099
3421
  setPinned,
2100
3422
  createGeneration,
3423
+ beginGenerationControlAttempt,
3424
+ markGenerationControlReady,
3425
+ renewGenerationControlLease,
3426
+ markGenerationControlAttemptCompleted,
3427
+ appendGenerationEvent,
2101
3428
  markGenerationTerminal,
2102
3429
  getLatestActiveGeneration,
2103
3430
  getLatestGeneration,
@@ -2106,7 +3433,17 @@ export function chatService(db) {
2106
3433
  createQueuedMessage,
2107
3434
  updateQueuedMessage,
2108
3435
  cancelQueuedMessage,
3436
+ scheduleSteerContinuation,
2109
3437
  markQueuedMessageSteerFallback,
3438
+ beginSteerControlAction,
3439
+ claimSteerProviderSend,
3440
+ releaseSteerProviderSendClaim,
3441
+ resolveSteerControlAction,
3442
+ claimNextServerQueuedMessage,
3443
+ renewServerQueuedMessageClaim,
3444
+ completeServerQueuedMessageDelivery,
3445
+ releaseServerQueuedMessageClaim,
3446
+ recoverExpiredServerQueueClaims,
2110
3447
  claimNextQueuedMessage,
2111
3448
  releaseQueuedMessageClaim,
2112
3449
  assertQueuedMessageClaimedForDelivery,