@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
@@ -0,0 +1,1571 @@
1
+ import { chatControlActions, chatGenerationEvents, chatGenerations, chatGenerationTerminalOutbox, chatMessages, chatQueuedMessages, } from "@rudderhq/db";
2
+ import { and, asc, desc, eq, inArray, isNotNull, isNull, lte, or, sql } from "drizzle-orm";
3
+ import { createHash, randomUUID } from "node:crypto";
4
+ import { conflict, notFound, unprocessable } from "../errors.js";
5
+ import { withPersistedTranscript } from "./chats.helpers.js";
6
+ import { normalizeLocalLibraryPathMarkdown } from "./library-path-markdown.js";
7
+ const OUTPUT_ADMITTING_GENERATION_STATUSES = [
8
+ "starting",
9
+ "active",
10
+ "running",
11
+ "tool_busy",
12
+ "closing",
13
+ ];
14
+ const CONTROL_ACTIVE_GENERATION_STATUSES = [
15
+ ...OUTPUT_ADMITTING_GENERATION_STATUSES,
16
+ ];
17
+ const RECOVERABLE_CONTROL_GENERATION_STATUSES = [
18
+ ...CONTROL_ACTIVE_GENERATION_STATUSES,
19
+ "stop_requested",
20
+ "stopping",
21
+ ];
22
+ const PROJECTABLE_TERMINAL_STATUSES = [
23
+ "completed",
24
+ "failed",
25
+ "stopped",
26
+ "aborted",
27
+ "interrupted_unverified",
28
+ "control_lost",
29
+ ];
30
+ const EMPTY_BODY_SHA256 = createHash("sha256").update("").digest("hex");
31
+ const SHA256_PATTERN = /^[a-f0-9]{64}$/i;
32
+ export function hashChatGenerationBody(body) {
33
+ return createHash("sha256").update(body).digest("hex");
34
+ }
35
+ function normalizeBodyHash(value) {
36
+ if (!SHA256_PATTERN.test(value)) {
37
+ throw unprocessable("Chat generation body hash must be a SHA-256 hex digest");
38
+ }
39
+ return value.toLowerCase();
40
+ }
41
+ function assertGenerationFence(generation, input) {
42
+ if (input.conversationId && generation.conversationId !== input.conversationId) {
43
+ throw notFound("Chat generation not found");
44
+ }
45
+ if (generation.attemptEpoch !== input.expectedAttemptEpoch) {
46
+ throw conflict("Chat generation runtime attempt changed");
47
+ }
48
+ if (input.expectedOwnerToken !== undefined
49
+ && generation.controlOwnerToken !== input.expectedOwnerToken) {
50
+ throw conflict("Chat generation control owner changed");
51
+ }
52
+ }
53
+ async function lockGeneration(tx, input) {
54
+ await tx.execute(sql `
55
+ select ${chatGenerations.id}
56
+ from ${chatGenerations}
57
+ where ${chatGenerations.id} = ${input.generationId}
58
+ and ${chatGenerations.orgId} = ${input.orgId}
59
+ for update
60
+ `);
61
+ const generation = await tx
62
+ .select()
63
+ .from(chatGenerations)
64
+ .where(and(eq(chatGenerations.id, input.generationId), eq(chatGenerations.orgId, input.orgId)))
65
+ .limit(1)
66
+ .then((rows) => rows[0] ?? null);
67
+ if (!generation || (input.conversationId && generation.conversationId !== input.conversationId)) {
68
+ throw notFound("Chat generation not found");
69
+ }
70
+ return generation;
71
+ }
72
+ async function nextGenerationSeq(tx, generationId) {
73
+ return tx
74
+ .select({ value: sql `coalesce(max(${chatGenerationEvents.generationSeq}), 0) + 1` })
75
+ .from(chatGenerationEvents)
76
+ .where(eq(chatGenerationEvents.generationId, generationId))
77
+ .then((rows) => Number(rows[0]?.value ?? 1));
78
+ }
79
+ async function appendEventLocked(tx, input) {
80
+ const generationSeq = await nextGenerationSeq(tx, input.generationId);
81
+ const [event] = await tx
82
+ .insert(chatGenerationEvents)
83
+ .values({
84
+ orgId: input.orgId,
85
+ generationId: input.generationId,
86
+ generationSeq,
87
+ attemptEpoch: input.attemptEpoch,
88
+ eventKind: input.eventKind,
89
+ payload: input.payload ?? {},
90
+ bodyOffset: input.bodyOffset ?? null,
91
+ bodyLength: input.bodyLength ?? null,
92
+ assistantMessageId: input.assistantMessageId ?? null,
93
+ runId: input.runId ?? null,
94
+ controlActionId: input.controlActionId ?? null,
95
+ queueItemId: input.queueItemId ?? null,
96
+ emittedAt: input.emittedAt ?? null,
97
+ })
98
+ .returning();
99
+ if (!event)
100
+ throw new Error("Failed to append chat generation event");
101
+ return event;
102
+ }
103
+ async function bodyHashAtSeq(tx, generationId, generationSeq) {
104
+ if (generationSeq === 0)
105
+ return EMPTY_BODY_SHA256;
106
+ const event = await tx
107
+ .select({ payload: chatGenerationEvents.payload })
108
+ .from(chatGenerationEvents)
109
+ .where(and(eq(chatGenerationEvents.generationId, generationId), eq(chatGenerationEvents.generationSeq, generationSeq)))
110
+ .limit(1)
111
+ .then((rows) => rows[0] ?? null);
112
+ const value = event?.payload?.bodyHash;
113
+ return typeof value === "string" && SHA256_PATTERN.test(value)
114
+ ? value.toLowerCase()
115
+ : null;
116
+ }
117
+ async function runtimeOutputAdmission(tx, generationId) {
118
+ return tx
119
+ .select()
120
+ .from(chatGenerationEvents)
121
+ .where(and(eq(chatGenerationEvents.generationId, generationId), eq(chatGenerationEvents.eventKind, "runtime_output")))
122
+ .orderBy(asc(chatGenerationEvents.generationSeq))
123
+ .limit(1)
124
+ .then((rows) => rows[0] ?? null);
125
+ }
126
+ async function latestVisibleBodyCheckpoint(tx, generationId) {
127
+ const events = await tx
128
+ .select({
129
+ generationSeq: chatGenerationEvents.generationSeq,
130
+ payload: chatGenerationEvents.payload,
131
+ })
132
+ .from(chatGenerationEvents)
133
+ .where(eq(chatGenerationEvents.generationId, generationId))
134
+ .orderBy(desc(chatGenerationEvents.generationSeq))
135
+ .limit(100);
136
+ for (const event of events) {
137
+ const bodyHash = event.payload?.bodyHash;
138
+ if (typeof bodyHash === "string" && SHA256_PATTERN.test(bodyHash)) {
139
+ return { generationSeq: event.generationSeq, bodyHash: bodyHash.toLowerCase() };
140
+ }
141
+ }
142
+ return { generationSeq: 0, bodyHash: EMPTY_BODY_SHA256 };
143
+ }
144
+ async function visibleGenerationProjectionThrough(tx, generationId, generationSeq) {
145
+ const events = generationSeq <= 0
146
+ ? []
147
+ : await tx
148
+ .select()
149
+ .from(chatGenerationEvents)
150
+ .where(and(eq(chatGenerationEvents.generationId, generationId), lte(chatGenerationEvents.generationSeq, generationSeq)))
151
+ .orderBy(asc(chatGenerationEvents.generationSeq));
152
+ let body = "";
153
+ const transcript = [];
154
+ let assistantMessageId = null;
155
+ let runId = null;
156
+ for (const event of events) {
157
+ if (event.assistantMessageId)
158
+ assistantMessageId = event.assistantMessageId;
159
+ if (event.runId)
160
+ runId = event.runId;
161
+ if (event.eventKind === "assistant_delta" && typeof event.payload.delta === "string") {
162
+ body += event.payload.delta;
163
+ }
164
+ else if (event.eventKind === "runtime_output" && typeof event.payload.body === "string") {
165
+ body = event.payload.body;
166
+ }
167
+ else if (event.eventKind === "transcript"
168
+ && event.payload.entry
169
+ && typeof event.payload.entry === "object"
170
+ && !Array.isArray(event.payload.entry)) {
171
+ transcript.push(event.payload.entry);
172
+ }
173
+ }
174
+ return { body, transcript, assistantMessageId, runId };
175
+ }
176
+ async function freezeAssistantMessageProjection(tx, generation, acceptedThroughSeq) {
177
+ const projection = await visibleGenerationProjectionThrough(tx, generation.id, acceptedThroughSeq);
178
+ if (!projection.assistantMessageId)
179
+ return projection;
180
+ const existing = await tx
181
+ .select()
182
+ .from(chatMessages)
183
+ .where(and(eq(chatMessages.id, projection.assistantMessageId), eq(chatMessages.orgId, generation.orgId), eq(chatMessages.conversationId, generation.conversationId), eq(chatMessages.role, "assistant")))
184
+ .limit(1)
185
+ .then((rows) => rows[0] ?? null);
186
+ if (!existing)
187
+ return projection;
188
+ const durableBody = await normalizeLocalLibraryPathMarkdown(projection.body, generation.orgId);
189
+ await tx
190
+ .update(chatMessages)
191
+ .set({
192
+ status: "stopped",
193
+ body: durableBody,
194
+ structuredPayload: withPersistedTranscript(existing.structuredPayload, projection.transcript),
195
+ ...(projection.runId ? { runId: projection.runId } : {}),
196
+ updatedAt: new Date(),
197
+ })
198
+ .where(eq(chatMessages.id, existing.id));
199
+ return projection;
200
+ }
201
+ function terminalStatusFromPayload(payload) {
202
+ const status = payload.finalStatus;
203
+ return typeof status === "string"
204
+ && PROJECTABLE_TERMINAL_STATUSES.includes(status)
205
+ ? status
206
+ : null;
207
+ }
208
+ export function chatGenerationProtocolService(db) {
209
+ async function getLatestVisibleCheckpoint(input) {
210
+ const generation = await db
211
+ .select()
212
+ .from(chatGenerations)
213
+ .where(and(eq(chatGenerations.id, input.generationId), eq(chatGenerations.orgId, input.orgId), eq(chatGenerations.conversationId, input.conversationId)))
214
+ .limit(1)
215
+ .then((rows) => rows[0] ?? null);
216
+ if (!generation)
217
+ throw notFound("Chat generation not found");
218
+ const events = await db
219
+ .select({
220
+ generationSeq: chatGenerationEvents.generationSeq,
221
+ payload: chatGenerationEvents.payload,
222
+ })
223
+ .from(chatGenerationEvents)
224
+ .where(eq(chatGenerationEvents.generationId, input.generationId))
225
+ .orderBy(desc(chatGenerationEvents.generationSeq))
226
+ .limit(100);
227
+ for (const event of events) {
228
+ const bodyHash = event.payload?.bodyHash;
229
+ if (typeof bodyHash === "string" && SHA256_PATTERN.test(bodyHash)) {
230
+ return {
231
+ generation,
232
+ generationSeq: event.generationSeq,
233
+ bodyHash: bodyHash.toLowerCase(),
234
+ };
235
+ }
236
+ }
237
+ return { generation, generationSeq: 0, bodyHash: EMPTY_BODY_SHA256 };
238
+ }
239
+ async function appendGenerationEvent(input) {
240
+ return db.transaction(async (tx) => {
241
+ const generation = await lockGeneration(tx, input);
242
+ assertGenerationFence(generation, {
243
+ conversationId: input.conversationId,
244
+ expectedAttemptEpoch: input.attemptEpoch,
245
+ expectedOwnerToken: input.expectedOwnerToken,
246
+ });
247
+ if (input.admission === "visible") {
248
+ if (await runtimeOutputAdmission(tx, generation.id)
249
+ || !OUTPUT_ADMITTING_GENERATION_STATUSES.includes(generation.status)) {
250
+ throw conflict("Chat-visible output admission is closed for this generation");
251
+ }
252
+ }
253
+ const payload = { ...(input.payload ?? {}) };
254
+ if (input.admission === "visible") {
255
+ if (!input.bodyHash) {
256
+ throw unprocessable("Visible chat generation events require the projected body hash");
257
+ }
258
+ const bodyHash = normalizeBodyHash(input.bodyHash);
259
+ if (typeof payload.bodyHash === "string" && payload.bodyHash.toLowerCase() !== bodyHash) {
260
+ throw conflict("Chat generation event body hash disagrees with its payload");
261
+ }
262
+ payload.bodyHash = bodyHash;
263
+ }
264
+ const event = await appendEventLocked(tx, { ...input, payload });
265
+ let updatedGeneration = generation;
266
+ if (input.admission === "diagnostic") {
267
+ const [updated] = await tx
268
+ .update(chatGenerations)
269
+ .set({
270
+ lateEventsDropped: sql `${chatGenerations.lateEventsDropped} + 1`,
271
+ lateBytes: sql `${chatGenerations.lateBytes} + ${Math.max(0, input.lateBytes ?? 0)}`,
272
+ updatedAt: new Date(),
273
+ })
274
+ .where(eq(chatGenerations.id, generation.id))
275
+ .returning();
276
+ updatedGeneration = updated ?? generation;
277
+ }
278
+ return { event, generation: updatedGeneration };
279
+ });
280
+ }
281
+ async function appendVisibleEventAndProject(input) {
282
+ const durableBody = await normalizeLocalLibraryPathMarkdown(input.body, input.orgId);
283
+ const bodyHash = normalizeBodyHash(input.bodyHash);
284
+ return db.transaction(async (tx) => {
285
+ const generation = await lockGeneration(tx, {
286
+ orgId: input.orgId,
287
+ conversationId: input.conversationId,
288
+ generationId: input.generationId,
289
+ });
290
+ assertGenerationFence(generation, {
291
+ conversationId: input.conversationId,
292
+ expectedAttemptEpoch: input.expectedAttemptEpoch,
293
+ expectedOwnerToken: input.expectedOwnerToken,
294
+ });
295
+ if (await runtimeOutputAdmission(tx, generation.id)) {
296
+ throw conflict("Chat-visible output admission is closed for this generation");
297
+ }
298
+ if (!OUTPUT_ADMITTING_GENERATION_STATUSES.includes(generation.status)) {
299
+ throw conflict("Chat-visible output admission is closed for this generation");
300
+ }
301
+ const payload = { ...(input.payload ?? {}) };
302
+ if (typeof payload.bodyHash === "string" && payload.bodyHash.toLowerCase() !== bodyHash) {
303
+ throw conflict("Chat generation event body hash disagrees with its payload");
304
+ }
305
+ payload.bodyHash = bodyHash;
306
+ const existing = input.messageId ? await tx
307
+ .select()
308
+ .from(chatMessages)
309
+ .where(and(eq(chatMessages.id, input.messageId), eq(chatMessages.orgId, input.orgId), eq(chatMessages.conversationId, input.conversationId)))
310
+ .limit(1)
311
+ .then((rows) => rows[0] ?? null) : null;
312
+ if (input.messageId && !existing) {
313
+ throw conflict("Visible generation message projection no longer exists");
314
+ }
315
+ const structuredPayload = withPersistedTranscript(existing?.structuredPayload ?? null, input.transcript);
316
+ const [message] = existing
317
+ ? await tx
318
+ .update(chatMessages)
319
+ .set({
320
+ status: "streaming",
321
+ body: durableBody,
322
+ structuredPayload,
323
+ ...(input.runId !== undefined ? { runId: input.runId } : {}),
324
+ ...(input.replyingAgentId !== undefined ? { replyingAgentId: input.replyingAgentId } : {}),
325
+ updatedAt: new Date(),
326
+ })
327
+ .where(eq(chatMessages.id, existing.id))
328
+ .returning()
329
+ : await tx
330
+ .insert(chatMessages)
331
+ .values({
332
+ orgId: input.orgId,
333
+ conversationId: input.conversationId,
334
+ role: "assistant",
335
+ kind: "message",
336
+ status: "streaming",
337
+ body: durableBody,
338
+ structuredPayload,
339
+ runId: input.runId ?? null,
340
+ replyingAgentId: input.replyingAgentId ?? null,
341
+ chatTurnId: input.chatTurnId,
342
+ turnVariant: input.turnVariant,
343
+ })
344
+ .returning();
345
+ if (!message)
346
+ throw new Error("Failed to project visible chat generation event");
347
+ const event = await appendEventLocked(tx, {
348
+ ...input,
349
+ attemptEpoch: input.expectedAttemptEpoch,
350
+ payload,
351
+ assistantMessageId: message.id,
352
+ });
353
+ return { generation, message, event };
354
+ });
355
+ }
356
+ async function getFrozenVisibleProjection(input) {
357
+ return db.transaction(async (tx) => {
358
+ const generation = await tx
359
+ .select()
360
+ .from(chatGenerations)
361
+ .where(and(eq(chatGenerations.id, input.generationId), eq(chatGenerations.orgId, input.orgId), eq(chatGenerations.conversationId, input.conversationId)))
362
+ .limit(1)
363
+ .then((rows) => rows[0] ?? null);
364
+ if (!generation)
365
+ throw notFound("Chat generation not found");
366
+ const checkpoint = generation.acceptedThroughSeq === null
367
+ ? await latestVisibleBodyCheckpoint(tx, generation.id)
368
+ : { generationSeq: generation.acceptedThroughSeq, bodyHash: generation.frozenBodyHash ?? EMPTY_BODY_SHA256 };
369
+ const projection = await visibleGenerationProjectionThrough(tx, generation.id, checkpoint.generationSeq);
370
+ return { generation, checkpoint, projection };
371
+ });
372
+ }
373
+ async function recordClientCheckpoint(input) {
374
+ const renderedBodyHash = normalizeBodyHash(input.renderedBodyHash);
375
+ return db.transaction(async (tx) => {
376
+ const generation = await lockGeneration(tx, input);
377
+ assertGenerationFence(generation, input);
378
+ if (!OUTPUT_ADMITTING_GENERATION_STATUSES.includes(generation.status)) {
379
+ throw conflict("Chat generation checkpoint arrived after output admission closed");
380
+ }
381
+ const currentSeq = generation.lastClientCheckpointSeq ?? -1;
382
+ if (input.generationSeq < currentSeq) {
383
+ return { generation, event: null, advanced: false };
384
+ }
385
+ if (input.generationSeq === currentSeq) {
386
+ if (generation.lastClientCheckpointHash !== renderedBodyHash) {
387
+ throw conflict("Chat generation checkpoint hash changed at the same sequence");
388
+ }
389
+ return { generation, event: null, advanced: false };
390
+ }
391
+ const durableHash = await bodyHashAtSeq(tx, generation.id, input.generationSeq);
392
+ if (!durableHash) {
393
+ throw conflict("Chat generation checkpoint does not reference a durable visible event");
394
+ }
395
+ if (durableHash !== renderedBodyHash) {
396
+ throw conflict("Chat generation checkpoint hash does not match the durable projection");
397
+ }
398
+ const now = input.now ?? new Date();
399
+ const [updatedGeneration] = await tx
400
+ .update(chatGenerations)
401
+ .set({
402
+ lastClientCheckpointSeq: input.generationSeq,
403
+ lastClientCheckpointHash: renderedBodyHash,
404
+ updatedAt: now,
405
+ })
406
+ .where(and(eq(chatGenerations.id, generation.id), eq(chatGenerations.attemptEpoch, input.expectedAttemptEpoch)))
407
+ .returning();
408
+ if (!updatedGeneration)
409
+ throw conflict("Chat generation changed while checkpointing");
410
+ const event = await appendEventLocked(tx, {
411
+ orgId: input.orgId,
412
+ generationId: generation.id,
413
+ attemptEpoch: input.expectedAttemptEpoch,
414
+ eventKind: "client_checkpoint",
415
+ payload: {
416
+ generationSeq: input.generationSeq,
417
+ renderedBodyHash,
418
+ },
419
+ });
420
+ return { generation: updatedGeneration, event, advanced: true };
421
+ });
422
+ }
423
+ async function beginStopAction(input) {
424
+ const requestedBodyHash = normalizeBodyHash(input.requestedBodyHash);
425
+ return db.transaction(async (tx) => {
426
+ const generation = await lockGeneration(tx, {
427
+ orgId: input.orgId,
428
+ conversationId: input.conversationId,
429
+ generationId: input.expectedGenerationId,
430
+ });
431
+ const existingAction = await tx
432
+ .select()
433
+ .from(chatControlActions)
434
+ .where(eq(chatControlActions.id, input.controlActionId))
435
+ .limit(1)
436
+ .then((rows) => rows[0] ?? null);
437
+ if (existingAction) {
438
+ if (existingAction.orgId !== input.orgId
439
+ || existingAction.actionKind !== "stop"
440
+ || existingAction.expectedGenerationId !== input.expectedGenerationId
441
+ || existingAction.expectedAttemptEpoch !== input.expectedAttemptEpoch
442
+ || existingAction.requestedRenderSeq !== input.requestedRenderSeq
443
+ || existingAction.requestedBodyHash !== requestedBodyHash) {
444
+ throw conflict("Control action id was already used for a different Stop request");
445
+ }
446
+ return {
447
+ action: existingAction,
448
+ generation,
449
+ stopRequestedEvent: null,
450
+ outputCutoffEvent: null,
451
+ outcome: existingAction.lastError === "generation_result_already_committed"
452
+ ? "completion_committed"
453
+ : "stop_applied",
454
+ idempotent: true,
455
+ };
456
+ }
457
+ assertGenerationFence(generation, {
458
+ conversationId: input.conversationId,
459
+ expectedAttemptEpoch: input.expectedAttemptEpoch,
460
+ });
461
+ const completionAdmission = await runtimeOutputAdmission(tx, generation.id);
462
+ if (completionAdmission) {
463
+ const now = input.now ?? new Date();
464
+ const [action] = await tx
465
+ .insert(chatControlActions)
466
+ .values({
467
+ id: input.controlActionId,
468
+ orgId: input.orgId,
469
+ expectedGenerationId: generation.id,
470
+ expectedAttemptEpoch: input.expectedAttemptEpoch,
471
+ expectedControlVersion: input.expectedControlVersion,
472
+ actionKind: "stop",
473
+ localDisposition: "cancelled",
474
+ providerDisposition: "not_sent",
475
+ controlOwnerToken: generation.controlOwnerToken,
476
+ providerEvidence: {
477
+ completionEventId: completionAdmission.id,
478
+ completionGenerationSeq: completionAdmission.generationSeq,
479
+ },
480
+ requestedRenderSeq: input.requestedRenderSeq,
481
+ requestedBodyHash,
482
+ lastError: "generation_result_already_committed",
483
+ requestedAt: now,
484
+ resolvedAt: now,
485
+ createdAt: now,
486
+ updatedAt: now,
487
+ })
488
+ .returning();
489
+ if (!action)
490
+ throw new Error("Failed to persist completed-generation Stop outcome");
491
+ return {
492
+ action,
493
+ generation,
494
+ stopRequestedEvent: null,
495
+ outputCutoffEvent: null,
496
+ outcome: "completion_committed",
497
+ idempotent: false,
498
+ };
499
+ }
500
+ if (generation.runtimeTerminalAt) {
501
+ throw conflict("Chat generation runtime is already terminal");
502
+ }
503
+ if (!CONTROL_ACTIVE_GENERATION_STATUSES.includes(generation.status)) {
504
+ throw conflict("Chat generation is no longer stoppable");
505
+ }
506
+ if (generation.controlVersion !== input.expectedControlVersion) {
507
+ throw conflict("Chat generation control version changed");
508
+ }
509
+ const durableCheckpointIsNewer = generation.lastClientCheckpointSeq !== null
510
+ && generation.lastClientCheckpointHash
511
+ && generation.lastClientCheckpointSeq > input.requestedRenderSeq;
512
+ const requestedDurableHash = durableCheckpointIsNewer
513
+ ? null
514
+ : await bodyHashAtSeq(tx, generation.id, input.requestedRenderSeq);
515
+ let acceptedThroughSeq;
516
+ let frozenBodyHash;
517
+ if (durableCheckpointIsNewer) {
518
+ acceptedThroughSeq = generation.lastClientCheckpointSeq;
519
+ frozenBodyHash = generation.lastClientCheckpointHash;
520
+ }
521
+ else if (requestedDurableHash) {
522
+ if (requestedDurableHash !== requestedBodyHash) {
523
+ throw conflict("Stop body hash does not match the durable visible projection");
524
+ }
525
+ acceptedThroughSeq = input.requestedRenderSeq;
526
+ frozenBodyHash = requestedBodyHash;
527
+ }
528
+ else if (generation.lastClientCheckpointSeq !== null
529
+ && generation.lastClientCheckpointHash) {
530
+ acceptedThroughSeq = generation.lastClientCheckpointSeq;
531
+ frozenBodyHash = generation.lastClientCheckpointHash;
532
+ }
533
+ else {
534
+ throw conflict("Stop could not establish a durable rendered checkpoint");
535
+ }
536
+ const now = input.now ?? new Date();
537
+ const appliedControlVersion = generation.controlVersion + 1;
538
+ const [action] = await tx
539
+ .insert(chatControlActions)
540
+ .values({
541
+ id: input.controlActionId,
542
+ orgId: input.orgId,
543
+ expectedGenerationId: generation.id,
544
+ expectedAttemptEpoch: input.expectedAttemptEpoch,
545
+ expectedControlVersion: input.expectedControlVersion,
546
+ appliedControlVersion,
547
+ actionKind: "stop",
548
+ localDisposition: "stop_requested",
549
+ providerDisposition: "not_sent",
550
+ controlOwnerToken: generation.controlOwnerToken,
551
+ requestedRenderSeq: input.requestedRenderSeq,
552
+ requestedBodyHash,
553
+ acceptedThroughSeq,
554
+ frozenBodyHash,
555
+ requestedAt: now,
556
+ createdAt: now,
557
+ updatedAt: now,
558
+ })
559
+ .returning();
560
+ if (!action)
561
+ throw new Error("Failed to persist Stop control action");
562
+ const stopRequestedEvent = await appendEventLocked(tx, {
563
+ orgId: input.orgId,
564
+ generationId: generation.id,
565
+ attemptEpoch: input.expectedAttemptEpoch,
566
+ eventKind: "stop_requested",
567
+ controlActionId: action.id,
568
+ payload: {
569
+ controlActionId: action.id,
570
+ requestedRenderSeq: input.requestedRenderSeq,
571
+ requestedBodyHash,
572
+ acceptedThroughSeq,
573
+ frozenBodyHash,
574
+ appliedControlVersion,
575
+ },
576
+ });
577
+ const outputCutoffEvent = await appendEventLocked(tx, {
578
+ orgId: input.orgId,
579
+ generationId: generation.id,
580
+ attemptEpoch: input.expectedAttemptEpoch,
581
+ eventKind: "output_cutoff",
582
+ controlActionId: action.id,
583
+ payload: {
584
+ controlActionId: action.id,
585
+ acceptedThroughSeq,
586
+ frozenBodyHash,
587
+ },
588
+ });
589
+ await freezeAssistantMessageProjection(tx, generation, acceptedThroughSeq);
590
+ const [updatedGeneration] = await tx
591
+ .update(chatGenerations)
592
+ .set({
593
+ status: "stop_requested",
594
+ terminalReason: "operator_stop",
595
+ controlState: "stopping",
596
+ controlVersion: appliedControlVersion,
597
+ acceptedThroughSeq,
598
+ frozenBodyHash,
599
+ stopRequestedAt: now,
600
+ updatedAt: now,
601
+ })
602
+ .where(and(eq(chatGenerations.id, generation.id), eq(chatGenerations.attemptEpoch, input.expectedAttemptEpoch), eq(chatGenerations.controlVersion, input.expectedControlVersion)))
603
+ .returning();
604
+ if (!updatedGeneration)
605
+ throw conflict("Chat generation changed while applying Stop");
606
+ return {
607
+ action,
608
+ generation: updatedGeneration,
609
+ stopRequestedEvent,
610
+ outputCutoffEvent,
611
+ outcome: "stop_applied",
612
+ idempotent: false,
613
+ };
614
+ });
615
+ }
616
+ async function beginSteerFallbackCutoff(input) {
617
+ const requestedBodyHash = input.requestedBodyHash === undefined
618
+ ? undefined
619
+ : normalizeBodyHash(input.requestedBodyHash);
620
+ if ((input.requestedRenderSeq === undefined) !== (requestedBodyHash === undefined)) {
621
+ throw unprocessable("Steer fallback render sequence and body hash must be supplied together");
622
+ }
623
+ return db.transaction(async (tx) => {
624
+ const generation = await lockGeneration(tx, {
625
+ orgId: input.orgId,
626
+ conversationId: input.conversationId,
627
+ generationId: input.generationId,
628
+ });
629
+ assertGenerationFence(generation, {
630
+ conversationId: input.conversationId,
631
+ expectedAttemptEpoch: input.expectedAttemptEpoch,
632
+ });
633
+ await tx.execute(sql `
634
+ select ${chatControlActions.id}
635
+ from ${chatControlActions}
636
+ where ${chatControlActions.id} = ${input.controlActionId}
637
+ and ${chatControlActions.orgId} = ${input.orgId}
638
+ for update
639
+ `);
640
+ await tx.execute(sql `
641
+ select ${chatQueuedMessages.id}
642
+ from ${chatQueuedMessages}
643
+ where ${chatQueuedMessages.id} = ${input.queueItemId}
644
+ and ${chatQueuedMessages.orgId} = ${input.orgId}
645
+ and ${chatQueuedMessages.conversationId} = ${input.conversationId}
646
+ for update
647
+ `);
648
+ const action = await tx
649
+ .select()
650
+ .from(chatControlActions)
651
+ .where(and(eq(chatControlActions.id, input.controlActionId), eq(chatControlActions.orgId, input.orgId), eq(chatControlActions.actionKind, "steer")))
652
+ .limit(1)
653
+ .then((rows) => rows[0] ?? null);
654
+ const item = await tx
655
+ .select()
656
+ .from(chatQueuedMessages)
657
+ .where(and(eq(chatQueuedMessages.id, input.queueItemId), eq(chatQueuedMessages.orgId, input.orgId), eq(chatQueuedMessages.conversationId, input.conversationId), eq(chatQueuedMessages.controlActionId, input.controlActionId)))
658
+ .limit(1)
659
+ .then((rows) => rows[0] ?? null);
660
+ if (!action
661
+ || action.expectedGenerationId !== generation.id
662
+ || action.expectedAttemptEpoch !== generation.attemptEpoch) {
663
+ throw conflict("Steer fallback control action no longer targets this generation");
664
+ }
665
+ if (!item)
666
+ throw notFound("Queued Steer feedback not found");
667
+ if (action.acceptedThroughSeq !== null && action.frozenBodyHash) {
668
+ return {
669
+ action,
670
+ generation,
671
+ item,
672
+ outputCutoffEvent: null,
673
+ continuationEvent: null,
674
+ outcome: "cutoff_applied",
675
+ idempotent: true,
676
+ };
677
+ }
678
+ const completionAdmission = await runtimeOutputAdmission(tx, generation.id);
679
+ if (completionAdmission) {
680
+ const existingContinuationEvent = await tx
681
+ .select()
682
+ .from(chatGenerationEvents)
683
+ .where(and(eq(chatGenerationEvents.generationId, generation.id), eq(chatGenerationEvents.eventKind, "continuation_scheduled"), eq(chatGenerationEvents.controlActionId, action.id)))
684
+ .orderBy(desc(chatGenerationEvents.generationSeq))
685
+ .limit(1)
686
+ .then((rows) => rows[0] ?? null);
687
+ if (action.localDisposition === "continuation_pending"
688
+ && existingContinuationEvent?.payload.reason === "target_generation_completion_committed") {
689
+ return {
690
+ action,
691
+ generation,
692
+ item,
693
+ outputCutoffEvent: null,
694
+ continuationEvent: existingContinuationEvent,
695
+ outcome: "completion_committed",
696
+ idempotent: true,
697
+ };
698
+ }
699
+ const now = input.now ?? new Date();
700
+ const [updatedAction] = await tx
701
+ .update(chatControlActions)
702
+ .set({
703
+ localDisposition: "continuation_pending",
704
+ providerDisposition: "not_sent",
705
+ providerSentAt: null,
706
+ requestedRenderSeq: input.requestedRenderSeq ?? null,
707
+ requestedBodyHash: requestedBodyHash ?? null,
708
+ acceptedThroughSeq: null,
709
+ frozenBodyHash: null,
710
+ providerEvidence: {
711
+ ...(action.providerEvidence ?? {}),
712
+ completionEventId: completionAdmission.id,
713
+ completionGenerationSeq: completionAdmission.generationSeq,
714
+ },
715
+ lastError: "target_generation_completion_committed",
716
+ resolvedAt: now,
717
+ updatedAt: now,
718
+ })
719
+ .where(and(eq(chatControlActions.id, action.id), eq(chatControlActions.orgId, input.orgId), eq(chatControlActions.actionKind, "steer")))
720
+ .returning();
721
+ if (!updatedAction)
722
+ throw conflict("Steer continuation action changed after generation completion");
723
+ const [updatedItem] = await tx
724
+ .update(chatQueuedMessages)
725
+ .set({
726
+ status: "continuation_pending",
727
+ deliveryIntent: "steer",
728
+ deliveryDisposition: "continuation_pending",
729
+ reconciliationReason: "target_generation_completion_committed",
730
+ lastDeliveryReason: null,
731
+ version: sql `${chatQueuedMessages.version} + 1`,
732
+ updatedAt: now,
733
+ })
734
+ .where(and(eq(chatQueuedMessages.id, item.id), eq(chatQueuedMessages.version, item.version)))
735
+ .returning();
736
+ if (!updatedItem)
737
+ throw conflict("Queued Steer feedback changed after generation completion");
738
+ const continuationEvent = await appendEventLocked(tx, {
739
+ orgId: input.orgId,
740
+ generationId: generation.id,
741
+ attemptEpoch: generation.attemptEpoch,
742
+ eventKind: "continuation_scheduled",
743
+ controlActionId: action.id,
744
+ queueItemId: item.id,
745
+ payload: {
746
+ controlActionId: action.id,
747
+ reason: "target_generation_completion_committed",
748
+ completionEventId: completionAdmission.id,
749
+ completionGenerationSeq: completionAdmission.generationSeq,
750
+ },
751
+ });
752
+ return {
753
+ action: updatedAction,
754
+ generation,
755
+ item: updatedItem,
756
+ outputCutoffEvent: null,
757
+ continuationEvent,
758
+ outcome: "completion_committed",
759
+ idempotent: false,
760
+ };
761
+ }
762
+ if (generation.runtimeTerminalAt) {
763
+ throw conflict("Chat generation runtime is already terminal");
764
+ }
765
+ const priorCutoffExists = generation.acceptedThroughSeq !== null
766
+ && Boolean(generation.frozenBodyHash)
767
+ && generation.stopRequestedAt !== null;
768
+ if (!priorCutoffExists
769
+ && action.appliedControlVersion !== generation.controlVersion) {
770
+ throw conflict("Chat generation control version changed before Steer fallback cutoff");
771
+ }
772
+ let acceptedThroughSeq;
773
+ let frozenBodyHash;
774
+ if (priorCutoffExists) {
775
+ acceptedThroughSeq = generation.acceptedThroughSeq;
776
+ frozenBodyHash = generation.frozenBodyHash;
777
+ }
778
+ else {
779
+ const requestedDurableHash = input.requestedRenderSeq === undefined
780
+ ? null
781
+ : await bodyHashAtSeq(tx, generation.id, input.requestedRenderSeq);
782
+ const clientCheckpointIsNewer = generation.lastClientCheckpointSeq !== null
783
+ && generation.lastClientCheckpointHash
784
+ && (input.requestedRenderSeq === undefined
785
+ || generation.lastClientCheckpointSeq > input.requestedRenderSeq);
786
+ if (clientCheckpointIsNewer) {
787
+ acceptedThroughSeq = generation.lastClientCheckpointSeq;
788
+ frozenBodyHash = generation.lastClientCheckpointHash;
789
+ }
790
+ else if (requestedDurableHash && requestedBodyHash) {
791
+ if (requestedDurableHash !== requestedBodyHash) {
792
+ throw conflict("Steer fallback body hash does not match the durable visible projection");
793
+ }
794
+ acceptedThroughSeq = input.requestedRenderSeq;
795
+ frozenBodyHash = requestedBodyHash;
796
+ }
797
+ else if (generation.lastClientCheckpointSeq !== null && generation.lastClientCheckpointHash) {
798
+ acceptedThroughSeq = generation.lastClientCheckpointSeq;
799
+ frozenBodyHash = generation.lastClientCheckpointHash;
800
+ }
801
+ else {
802
+ const checkpoint = await latestVisibleBodyCheckpoint(tx, generation.id);
803
+ acceptedThroughSeq = checkpoint.generationSeq;
804
+ frozenBodyHash = checkpoint.bodyHash;
805
+ }
806
+ }
807
+ const now = input.now ?? new Date();
808
+ const [updatedAction] = await tx
809
+ .update(chatControlActions)
810
+ .set({
811
+ localDisposition: "continuation_pending",
812
+ providerDisposition: "not_sent",
813
+ providerSentAt: null,
814
+ requestedRenderSeq: input.requestedRenderSeq ?? acceptedThroughSeq,
815
+ requestedBodyHash: requestedBodyHash ?? frozenBodyHash,
816
+ acceptedThroughSeq,
817
+ frozenBodyHash,
818
+ lastError: "runtime_requires_continuation",
819
+ resolvedAt: now,
820
+ updatedAt: now,
821
+ })
822
+ .where(and(eq(chatControlActions.id, action.id), eq(chatControlActions.orgId, input.orgId), eq(chatControlActions.actionKind, "steer")))
823
+ .returning();
824
+ if (!updatedAction)
825
+ throw conflict("Steer fallback control action changed");
826
+ const [updatedItem] = await tx
827
+ .update(chatQueuedMessages)
828
+ .set({
829
+ status: "continuation_pending",
830
+ deliveryIntent: "steer",
831
+ deliveryDisposition: "continuation_pending",
832
+ reconciliationReason: "runtime_requires_continuation",
833
+ lastDeliveryReason: null,
834
+ version: sql `${chatQueuedMessages.version} + 1`,
835
+ updatedAt: now,
836
+ })
837
+ .where(and(eq(chatQueuedMessages.id, item.id), eq(chatQueuedMessages.version, item.version)))
838
+ .returning();
839
+ if (!updatedItem)
840
+ throw conflict("Queued Steer feedback changed before fallback cutoff");
841
+ let outputCutoffEvent = null;
842
+ if (!priorCutoffExists) {
843
+ outputCutoffEvent = await appendEventLocked(tx, {
844
+ orgId: input.orgId,
845
+ generationId: generation.id,
846
+ attemptEpoch: generation.attemptEpoch,
847
+ eventKind: "output_cutoff",
848
+ controlActionId: action.id,
849
+ queueItemId: item.id,
850
+ payload: {
851
+ controlActionId: action.id,
852
+ reason: "steer_fallback",
853
+ acceptedThroughSeq,
854
+ frozenBodyHash,
855
+ },
856
+ });
857
+ }
858
+ const continuationEvent = await appendEventLocked(tx, {
859
+ orgId: input.orgId,
860
+ generationId: generation.id,
861
+ attemptEpoch: generation.attemptEpoch,
862
+ eventKind: "continuation_scheduled",
863
+ controlActionId: action.id,
864
+ queueItemId: item.id,
865
+ payload: {
866
+ controlActionId: action.id,
867
+ reason: priorCutoffExists ? "existing_output_cutoff" : "runtime_requires_continuation",
868
+ acceptedThroughSeq,
869
+ frozenBodyHash,
870
+ },
871
+ });
872
+ await freezeAssistantMessageProjection(tx, generation, acceptedThroughSeq);
873
+ let updatedGeneration = generation;
874
+ if (!priorCutoffExists) {
875
+ const [cutoffGeneration] = await tx
876
+ .update(chatGenerations)
877
+ .set({
878
+ status: "stop_requested",
879
+ terminalReason: "steer_fallback",
880
+ controlState: "stopping",
881
+ acceptedThroughSeq,
882
+ frozenBodyHash,
883
+ stopRequestedAt: now,
884
+ updatedAt: now,
885
+ })
886
+ .where(and(eq(chatGenerations.id, generation.id), eq(chatGenerations.controlVersion, generation.controlVersion), eq(chatGenerations.attemptEpoch, generation.attemptEpoch)))
887
+ .returning();
888
+ if (!cutoffGeneration)
889
+ throw conflict("Chat generation changed while applying Steer fallback cutoff");
890
+ updatedGeneration = cutoffGeneration;
891
+ }
892
+ return {
893
+ action: updatedAction,
894
+ generation: updatedGeneration,
895
+ item: updatedItem,
896
+ outputCutoffEvent,
897
+ continuationEvent,
898
+ outcome: "cutoff_applied",
899
+ idempotent: false,
900
+ };
901
+ });
902
+ }
903
+ async function recordRuntimeTerminal(input) {
904
+ return db.transaction(async (tx) => {
905
+ const generation = await lockGeneration(tx, input);
906
+ assertGenerationFence(generation, input);
907
+ const cutoffWon = generation.status === "stop_requested"
908
+ || generation.status === "stopping"
909
+ || generation.stopRequestedAt !== null;
910
+ const runtimeTerminationUnverified = input.finalStatus === "interrupted_unverified";
911
+ const finalStatus = cutoffWon
912
+ ? runtimeTerminationUnverified ? "interrupted_unverified" : "stopped"
913
+ : input.finalStatus;
914
+ const cutoffReason = generation.terminalReason === "steer_fallback"
915
+ ? "steer_fallback"
916
+ : "operator_stop";
917
+ const terminalReason = cutoffWon
918
+ ? runtimeTerminationUnverified ? `${cutoffReason}_unverified` : cutoffReason
919
+ : input.terminalReason;
920
+ const projectionVersion = generation.controlVersion;
921
+ const existingOutbox = await tx
922
+ .select()
923
+ .from(chatGenerationTerminalOutbox)
924
+ .where(and(eq(chatGenerationTerminalOutbox.generationId, input.generationId), eq(chatGenerationTerminalOutbox.projectionVersion, projectionVersion)))
925
+ .limit(1)
926
+ .then((rows) => rows[0] ?? null);
927
+ if (existingOutbox) {
928
+ if (terminalStatusFromPayload(existingOutbox.payload) !== finalStatus
929
+ || existingOutbox.payload.terminalReason !== terminalReason) {
930
+ throw conflict("Terminal projection version was reused with different evidence");
931
+ }
932
+ return { generation, event: null, outbox: existingOutbox, idempotent: true };
933
+ }
934
+ if (!RECOVERABLE_CONTROL_GENERATION_STATUSES.includes(generation.status)) {
935
+ throw conflict("Chat generation is already terminal");
936
+ }
937
+ const now = input.now ?? new Date();
938
+ const inferredControlAction = input.controlActionId
939
+ ? await tx
940
+ .select({ id: chatControlActions.id, actionKind: chatControlActions.actionKind })
941
+ .from(chatControlActions)
942
+ .where(and(eq(chatControlActions.id, input.controlActionId), eq(chatControlActions.orgId, input.orgId)))
943
+ .limit(1)
944
+ .then((rows) => rows[0] ?? null)
945
+ : (finalStatus === "stopped" || finalStatus === "interrupted_unverified"
946
+ ? await tx
947
+ .select({ id: chatControlActions.id, actionKind: chatControlActions.actionKind })
948
+ .from(chatControlActions)
949
+ .where(and(eq(chatControlActions.orgId, input.orgId), eq(chatControlActions.expectedGenerationId, input.generationId), isNotNull(chatControlActions.acceptedThroughSeq)))
950
+ .orderBy(desc(chatControlActions.requestedAt))
951
+ .limit(1)
952
+ .then((rows) => rows[0] ?? null)
953
+ : null);
954
+ const inferredControlActionId = inferredControlAction?.id ?? null;
955
+ const payload = {
956
+ ...(input.payload ?? {}),
957
+ attemptEpoch: generation.attemptEpoch,
958
+ finalStatus,
959
+ terminalReason,
960
+ controlActionId: inferredControlActionId,
961
+ controlActionKind: inferredControlAction?.actionKind ?? null,
962
+ runtimeTerminationVerified: finalStatus !== "interrupted_unverified",
963
+ };
964
+ const event = await appendEventLocked(tx, {
965
+ orgId: input.orgId,
966
+ generationId: input.generationId,
967
+ attemptEpoch: input.expectedAttemptEpoch,
968
+ eventKind: "runtime_terminal",
969
+ controlActionId: inferredControlActionId,
970
+ payload,
971
+ });
972
+ const [outbox] = await tx
973
+ .insert(chatGenerationTerminalOutbox)
974
+ .values({
975
+ orgId: input.orgId,
976
+ generationId: input.generationId,
977
+ sourceEventId: event.id,
978
+ projectionVersion,
979
+ expectedControlVersion: generation.controlVersion,
980
+ payload,
981
+ availableAt: now,
982
+ createdAt: now,
983
+ updatedAt: now,
984
+ })
985
+ .returning();
986
+ if (!outbox)
987
+ throw new Error("Failed to enqueue chat terminal projection");
988
+ const stagingStatus = finalStatus === "stopped"
989
+ || finalStatus === "interrupted_unverified"
990
+ ? "stopping"
991
+ : "closing";
992
+ const [updatedGeneration] = await tx
993
+ .update(chatGenerations)
994
+ .set({
995
+ status: stagingStatus,
996
+ controlState: stagingStatus === "stopping" ? "stopping" : generation.controlState,
997
+ terminalReason,
998
+ runtimeTerminalAt: now,
999
+ updatedAt: now,
1000
+ })
1001
+ .where(eq(chatGenerations.id, generation.id))
1002
+ .returning();
1003
+ return {
1004
+ generation: updatedGeneration ?? generation,
1005
+ event,
1006
+ outbox,
1007
+ idempotent: false,
1008
+ };
1009
+ });
1010
+ }
1011
+ async function claimTerminalProjection(input) {
1012
+ const now = input.now ?? new Date();
1013
+ const leaseExpiresAt = new Date(now.getTime() + input.leaseMs);
1014
+ return db.transaction(async (tx) => {
1015
+ const conditions = [
1016
+ lte(chatGenerationTerminalOutbox.availableAt, now),
1017
+ or(inArray(chatGenerationTerminalOutbox.status, ["pending", "retry_wait"]), and(eq(chatGenerationTerminalOutbox.status, "claimed"), or(sql `${chatGenerationTerminalOutbox.leaseExpiresAt} is null`, lte(chatGenerationTerminalOutbox.leaseExpiresAt, now)))),
1018
+ ];
1019
+ if (input.orgId)
1020
+ conditions.push(eq(chatGenerationTerminalOutbox.orgId, input.orgId));
1021
+ const candidate = await tx
1022
+ .select()
1023
+ .from(chatGenerationTerminalOutbox)
1024
+ .where(and(...conditions))
1025
+ .orderBy(asc(chatGenerationTerminalOutbox.availableAt), asc(chatGenerationTerminalOutbox.createdAt))
1026
+ .limit(1)
1027
+ .for("update", { skipLocked: true })
1028
+ .then((rows) => rows[0] ?? null);
1029
+ if (!candidate)
1030
+ return null;
1031
+ const claimToken = randomUUID();
1032
+ const claimEpoch = candidate.claimEpoch + 1;
1033
+ const [claimed] = await tx
1034
+ .update(chatGenerationTerminalOutbox)
1035
+ .set({
1036
+ status: "claimed",
1037
+ claimToken,
1038
+ claimEpoch,
1039
+ claimOwner: input.workerId,
1040
+ leaseExpiresAt,
1041
+ attemptCount: sql `${chatGenerationTerminalOutbox.attemptCount} + 1`,
1042
+ replayCount: candidate.status === "claimed"
1043
+ ? sql `${chatGenerationTerminalOutbox.replayCount} + 1`
1044
+ : chatGenerationTerminalOutbox.replayCount,
1045
+ lastAttemptAt: now,
1046
+ updatedAt: now,
1047
+ })
1048
+ .where(eq(chatGenerationTerminalOutbox.id, candidate.id))
1049
+ .returning();
1050
+ return claimed ?? null;
1051
+ });
1052
+ }
1053
+ async function getNextTerminalProjectionWakeAt(input = {}) {
1054
+ const conditions = [
1055
+ inArray(chatGenerationTerminalOutbox.status, ["pending", "retry_wait", "claimed"]),
1056
+ ];
1057
+ if (input.orgId)
1058
+ conditions.push(eq(chatGenerationTerminalOutbox.orgId, input.orgId));
1059
+ const next = await db
1060
+ .select({
1061
+ status: chatGenerationTerminalOutbox.status,
1062
+ availableAt: chatGenerationTerminalOutbox.availableAt,
1063
+ leaseExpiresAt: chatGenerationTerminalOutbox.leaseExpiresAt,
1064
+ })
1065
+ .from(chatGenerationTerminalOutbox)
1066
+ .where(and(...conditions))
1067
+ .orderBy(asc(sql `
1068
+ case
1069
+ when ${chatGenerationTerminalOutbox.status} = 'claimed'
1070
+ and ${chatGenerationTerminalOutbox.leaseExpiresAt} is not null
1071
+ and ${chatGenerationTerminalOutbox.leaseExpiresAt} > ${chatGenerationTerminalOutbox.availableAt}
1072
+ then ${chatGenerationTerminalOutbox.leaseExpiresAt}
1073
+ else ${chatGenerationTerminalOutbox.availableAt}
1074
+ end
1075
+ `))
1076
+ .limit(1)
1077
+ .then((rows) => rows[0] ?? null);
1078
+ if (!next)
1079
+ return null;
1080
+ if (next.status === "claimed"
1081
+ && next.leaseExpiresAt
1082
+ && next.leaseExpiresAt > next.availableAt) {
1083
+ return next.leaseExpiresAt;
1084
+ }
1085
+ return next.availableAt;
1086
+ }
1087
+ async function completeTerminalProjection(input) {
1088
+ const now = input.now ?? new Date();
1089
+ return db.transaction(async (tx) => {
1090
+ const observedClaim = await tx
1091
+ .select({
1092
+ id: chatGenerationTerminalOutbox.id,
1093
+ orgId: chatGenerationTerminalOutbox.orgId,
1094
+ generationId: chatGenerationTerminalOutbox.generationId,
1095
+ })
1096
+ .from(chatGenerationTerminalOutbox)
1097
+ .where(eq(chatGenerationTerminalOutbox.id, input.outboxId))
1098
+ .limit(1)
1099
+ .then((rows) => rows[0] ?? null);
1100
+ if (!observedClaim)
1101
+ return null;
1102
+ const generation = await lockGeneration(tx, {
1103
+ orgId: observedClaim.orgId,
1104
+ generationId: observedClaim.generationId,
1105
+ });
1106
+ await tx.execute(sql `
1107
+ select ${chatGenerationTerminalOutbox.id}
1108
+ from ${chatGenerationTerminalOutbox}
1109
+ where ${chatGenerationTerminalOutbox.id} = ${input.outboxId}
1110
+ for update
1111
+ `);
1112
+ const claim = await tx
1113
+ .select()
1114
+ .from(chatGenerationTerminalOutbox)
1115
+ .where(eq(chatGenerationTerminalOutbox.id, input.outboxId))
1116
+ .limit(1)
1117
+ .then((rows) => rows[0] ?? null);
1118
+ if (!claim
1119
+ || claim.status !== "claimed"
1120
+ || claim.claimToken !== input.claimToken
1121
+ || claim.claimEpoch !== input.claimEpoch
1122
+ || !claim.leaseExpiresAt
1123
+ || claim.leaseExpiresAt <= now) {
1124
+ return null;
1125
+ }
1126
+ if (generation.controlVersion !== claim.expectedControlVersion) {
1127
+ throw conflict("Terminal projector claim has a stale control version");
1128
+ }
1129
+ const sourceAttemptEpoch = Number(claim.payload.attemptEpoch);
1130
+ if (!Number.isInteger(sourceAttemptEpoch) || sourceAttemptEpoch !== generation.attemptEpoch) {
1131
+ const [superseded] = await tx
1132
+ .update(chatGenerationTerminalOutbox)
1133
+ .set({
1134
+ status: "superseded",
1135
+ claimToken: null,
1136
+ claimOwner: null,
1137
+ leaseExpiresAt: null,
1138
+ projectedAt: now,
1139
+ lastError: "terminal_projection_attempt_superseded",
1140
+ updatedAt: now,
1141
+ })
1142
+ .where(and(eq(chatGenerationTerminalOutbox.id, claim.id), eq(chatGenerationTerminalOutbox.status, "claimed"), eq(chatGenerationTerminalOutbox.claimToken, input.claimToken), eq(chatGenerationTerminalOutbox.claimEpoch, input.claimEpoch)))
1143
+ .returning();
1144
+ return superseded ? {
1145
+ outbox: superseded,
1146
+ generation,
1147
+ event: null,
1148
+ superseded: true,
1149
+ } : null;
1150
+ }
1151
+ const finalStatus = input.finalStatus ?? terminalStatusFromPayload(claim.payload);
1152
+ if (!finalStatus || !PROJECTABLE_TERMINAL_STATUSES.includes(finalStatus)) {
1153
+ throw unprocessable("Terminal projector claim does not name a supported final status");
1154
+ }
1155
+ const terminalReason = input.terminalReason
1156
+ ?? (typeof claim.payload.terminalReason === "string" ? claim.payload.terminalReason : finalStatus);
1157
+ await input.project?.(tx, claim);
1158
+ const deliveredSteers = await tx
1159
+ .update(chatQueuedMessages)
1160
+ .set({
1161
+ status: "delivered",
1162
+ deliveryDisposition: "delivered",
1163
+ reconciliationReason: null,
1164
+ lastDeliveryReason: null,
1165
+ version: sql `${chatQueuedMessages.version} + 1`,
1166
+ updatedAt: now,
1167
+ })
1168
+ .where(and(or(eq(chatQueuedMessages.status, "accepted_current"), and(eq(chatQueuedMessages.status, "steer_pending"), inArray(chatQueuedMessages.controlActionId, tx
1169
+ .select({ id: chatControlActions.id })
1170
+ .from(chatControlActions)
1171
+ .where(eq(chatControlActions.providerDisposition, "acknowledged"))))), or(eq(chatQueuedMessages.expectedGenerationId, claim.generationId), eq(chatQueuedMessages.activeGenerationId, claim.generationId))))
1172
+ .returning({ controlActionId: chatQueuedMessages.controlActionId });
1173
+ const deliveredControlActionIds = deliveredSteers
1174
+ .map((item) => item.controlActionId)
1175
+ .filter((id) => Boolean(id));
1176
+ if (deliveredControlActionIds.length > 0) {
1177
+ await tx
1178
+ .update(chatControlActions)
1179
+ .set({ localDisposition: "delivered", resolvedAt: now, updatedAt: now })
1180
+ .where(inArray(chatControlActions.id, deliveredControlActionIds));
1181
+ }
1182
+ const uncertainSteers = await tx
1183
+ .update(chatQueuedMessages)
1184
+ .set({
1185
+ status: "acceptance_unknown",
1186
+ deliveryDisposition: "acceptance_unknown",
1187
+ reconciliationReason: `provider_receipt_unknown_at_${finalStatus}`,
1188
+ lastDeliveryReason: `provider_receipt_unknown_at_${finalStatus}`,
1189
+ version: sql `${chatQueuedMessages.version} + 1`,
1190
+ updatedAt: now,
1191
+ })
1192
+ .where(and(eq(chatQueuedMessages.status, "steer_pending"), inArray(chatQueuedMessages.controlActionId, tx
1193
+ .select({ id: chatControlActions.id })
1194
+ .from(chatControlActions)
1195
+ .where(inArray(chatControlActions.providerDisposition, [
1196
+ "sent",
1197
+ "timed_out",
1198
+ "connection_lost",
1199
+ "waiting_safe_boundary",
1200
+ "unverified",
1201
+ ]))), or(eq(chatQueuedMessages.expectedGenerationId, claim.generationId), eq(chatQueuedMessages.activeGenerationId, claim.generationId))))
1202
+ .returning({ controlActionId: chatQueuedMessages.controlActionId });
1203
+ const uncertainControlActionIds = uncertainSteers
1204
+ .map((item) => item.controlActionId)
1205
+ .filter((id) => Boolean(id));
1206
+ if (uncertainControlActionIds.length > 0) {
1207
+ await tx
1208
+ .update(chatControlActions)
1209
+ .set({
1210
+ localDisposition: "acceptance_unknown",
1211
+ lastError: `provider_receipt_unknown_at_${finalStatus}`,
1212
+ resolvedAt: now,
1213
+ updatedAt: now,
1214
+ })
1215
+ .where(inArray(chatControlActions.id, uncertainControlActionIds));
1216
+ }
1217
+ const continuationSteers = await tx
1218
+ .update(chatQueuedMessages)
1219
+ .set({
1220
+ status: "continuation_pending",
1221
+ deliveryDisposition: "continuation_pending",
1222
+ reconciliationReason: `target_generation_${finalStatus}`,
1223
+ lastDeliveryReason: null,
1224
+ version: sql `${chatQueuedMessages.version} + 1`,
1225
+ updatedAt: now,
1226
+ })
1227
+ .where(and(eq(chatQueuedMessages.status, "steer_pending"), or(isNull(chatQueuedMessages.controlActionId), inArray(chatQueuedMessages.controlActionId, tx
1228
+ .select({ id: chatControlActions.id })
1229
+ .from(chatControlActions)
1230
+ .where(or(eq(chatControlActions.providerDisposition, "not_sent"), eq(chatControlActions.providerDisposition, "rejected"), isNull(chatControlActions.providerDisposition))))), or(eq(chatQueuedMessages.expectedGenerationId, claim.generationId), eq(chatQueuedMessages.activeGenerationId, claim.generationId))))
1231
+ .returning({ controlActionId: chatQueuedMessages.controlActionId });
1232
+ const continuationControlActionIds = continuationSteers
1233
+ .map((item) => item.controlActionId)
1234
+ .filter((id) => Boolean(id));
1235
+ if (continuationControlActionIds.length > 0) {
1236
+ await tx
1237
+ .update(chatControlActions)
1238
+ .set({
1239
+ localDisposition: "continuation_pending",
1240
+ providerDisposition: "not_sent",
1241
+ lastError: `target_generation_${finalStatus}`,
1242
+ resolvedAt: now,
1243
+ updatedAt: now,
1244
+ })
1245
+ .where(inArray(chatControlActions.id, continuationControlActionIds));
1246
+ }
1247
+ const terminalProjectedEvent = await appendEventLocked(tx, {
1248
+ orgId: claim.orgId,
1249
+ generationId: claim.generationId,
1250
+ attemptEpoch: generation.attemptEpoch,
1251
+ eventKind: "terminal_projected",
1252
+ controlActionId: typeof claim.payload.controlActionId === "string"
1253
+ ? claim.payload.controlActionId
1254
+ : null,
1255
+ payload: {
1256
+ outboxId: claim.id,
1257
+ projectionVersion: claim.projectionVersion,
1258
+ finalStatus,
1259
+ terminalReason,
1260
+ },
1261
+ });
1262
+ const [updatedGeneration] = await tx
1263
+ .update(chatGenerations)
1264
+ .set({
1265
+ status: finalStatus,
1266
+ terminalReason,
1267
+ controlState: finalStatus === "control_lost" ? "control_lost" : "terminal",
1268
+ completedAt: now,
1269
+ updatedAt: now,
1270
+ })
1271
+ .where(eq(chatGenerations.id, claim.generationId))
1272
+ .returning();
1273
+ const controlActionId = typeof claim.payload.controlActionId === "string"
1274
+ ? claim.payload.controlActionId
1275
+ : null;
1276
+ if (controlActionId && input.controlDisposition) {
1277
+ await tx
1278
+ .update(chatControlActions)
1279
+ .set({
1280
+ localDisposition: input.controlDisposition,
1281
+ resolvedAt: now,
1282
+ updatedAt: now,
1283
+ })
1284
+ .where(and(eq(chatControlActions.id, controlActionId), eq(chatControlActions.orgId, claim.orgId)));
1285
+ }
1286
+ const [completed] = await tx
1287
+ .update(chatGenerationTerminalOutbox)
1288
+ .set({
1289
+ status: "projected",
1290
+ claimToken: null,
1291
+ claimOwner: null,
1292
+ leaseExpiresAt: null,
1293
+ projectedAt: now,
1294
+ lastError: null,
1295
+ updatedAt: now,
1296
+ })
1297
+ .where(and(eq(chatGenerationTerminalOutbox.id, claim.id), eq(chatGenerationTerminalOutbox.status, "claimed"), eq(chatGenerationTerminalOutbox.claimToken, input.claimToken), eq(chatGenerationTerminalOutbox.claimEpoch, input.claimEpoch)))
1298
+ .returning();
1299
+ if (!completed || !updatedGeneration) {
1300
+ throw conflict("Terminal projector claim changed while completing");
1301
+ }
1302
+ return {
1303
+ outbox: completed,
1304
+ generation: updatedGeneration,
1305
+ event: terminalProjectedEvent,
1306
+ };
1307
+ });
1308
+ }
1309
+ async function retryTerminalProjection(input) {
1310
+ const now = input.now ?? new Date();
1311
+ return db.transaction(async (tx) => {
1312
+ const observedClaim = await tx
1313
+ .select({
1314
+ orgId: chatGenerationTerminalOutbox.orgId,
1315
+ generationId: chatGenerationTerminalOutbox.generationId,
1316
+ })
1317
+ .from(chatGenerationTerminalOutbox)
1318
+ .where(eq(chatGenerationTerminalOutbox.id, input.outboxId))
1319
+ .limit(1)
1320
+ .then((rows) => rows[0] ?? null);
1321
+ if (!observedClaim)
1322
+ return null;
1323
+ const generation = await lockGeneration(tx, observedClaim);
1324
+ await tx.execute(sql `
1325
+ select ${chatGenerationTerminalOutbox.id}
1326
+ from ${chatGenerationTerminalOutbox}
1327
+ where ${chatGenerationTerminalOutbox.id} = ${input.outboxId}
1328
+ for update
1329
+ `);
1330
+ const claim = await tx
1331
+ .select()
1332
+ .from(chatGenerationTerminalOutbox)
1333
+ .where(eq(chatGenerationTerminalOutbox.id, input.outboxId))
1334
+ .limit(1)
1335
+ .then((rows) => rows[0] ?? null);
1336
+ if (!claim
1337
+ || claim.status !== "claimed"
1338
+ || claim.claimToken !== input.claimToken
1339
+ || claim.claimEpoch !== input.claimEpoch) {
1340
+ return null;
1341
+ }
1342
+ const failedActionable = claim.attemptCount >= input.maxAttempts;
1343
+ const [updated] = await tx
1344
+ .update(chatGenerationTerminalOutbox)
1345
+ .set({
1346
+ status: failedActionable ? "failed_actionable" : "retry_wait",
1347
+ claimToken: null,
1348
+ claimOwner: null,
1349
+ leaseExpiresAt: null,
1350
+ availableAt: failedActionable ? now : input.retryAt,
1351
+ lastError: input.error,
1352
+ updatedAt: now,
1353
+ })
1354
+ .where(and(eq(chatGenerationTerminalOutbox.id, input.outboxId), eq(chatGenerationTerminalOutbox.status, "claimed"), eq(chatGenerationTerminalOutbox.claimToken, input.claimToken), eq(chatGenerationTerminalOutbox.claimEpoch, input.claimEpoch)))
1355
+ .returning();
1356
+ if (!updated || !failedActionable)
1357
+ return updated ?? null;
1358
+ await tx
1359
+ .update(chatGenerations)
1360
+ .set({
1361
+ status: "failed",
1362
+ terminalReason: "terminal_projection_failed_actionable",
1363
+ controlState: "terminal",
1364
+ controlOwnerToken: null,
1365
+ controlLeaseExpiresAt: null,
1366
+ completedAt: now,
1367
+ updatedAt: now,
1368
+ })
1369
+ .where(and(eq(chatGenerations.id, generation.id), inArray(chatGenerations.status, ["closing", "stopping"])));
1370
+ const deliveredSteers = await tx
1371
+ .update(chatQueuedMessages)
1372
+ .set({
1373
+ status: "delivered",
1374
+ deliveryDisposition: "delivered",
1375
+ reconciliationReason: null,
1376
+ lastDeliveryReason: null,
1377
+ version: sql `${chatQueuedMessages.version} + 1`,
1378
+ updatedAt: now,
1379
+ })
1380
+ .where(and(or(eq(chatQueuedMessages.status, "accepted_current"), and(eq(chatQueuedMessages.status, "steer_pending"), inArray(chatQueuedMessages.controlActionId, tx
1381
+ .select({ id: chatControlActions.id })
1382
+ .from(chatControlActions)
1383
+ .where(eq(chatControlActions.providerDisposition, "acknowledged"))))), or(eq(chatQueuedMessages.expectedGenerationId, claim.generationId), eq(chatQueuedMessages.activeGenerationId, claim.generationId))))
1384
+ .returning({ controlActionId: chatQueuedMessages.controlActionId });
1385
+ const deliveredControlActionIds = deliveredSteers
1386
+ .map((item) => item.controlActionId)
1387
+ .filter((id) => Boolean(id));
1388
+ if (deliveredControlActionIds.length > 0) {
1389
+ await tx
1390
+ .update(chatControlActions)
1391
+ .set({
1392
+ localDisposition: "delivered",
1393
+ lastError: null,
1394
+ resolvedAt: now,
1395
+ updatedAt: now,
1396
+ })
1397
+ .where(inArray(chatControlActions.id, deliveredControlActionIds));
1398
+ }
1399
+ const uncertainSteers = await tx
1400
+ .update(chatQueuedMessages)
1401
+ .set({
1402
+ status: "acceptance_unknown",
1403
+ deliveryDisposition: "acceptance_unknown",
1404
+ reconciliationReason: "terminal_projection_failed_provider_receipt_unknown",
1405
+ lastDeliveryReason: "terminal_projection_failed_provider_receipt_unknown",
1406
+ version: sql `${chatQueuedMessages.version} + 1`,
1407
+ updatedAt: now,
1408
+ })
1409
+ .where(and(eq(chatQueuedMessages.status, "steer_pending"), inArray(chatQueuedMessages.controlActionId, tx
1410
+ .select({ id: chatControlActions.id })
1411
+ .from(chatControlActions)
1412
+ .where(inArray(chatControlActions.providerDisposition, [
1413
+ "sent",
1414
+ "timed_out",
1415
+ "connection_lost",
1416
+ "waiting_safe_boundary",
1417
+ "unverified",
1418
+ ]))), or(eq(chatQueuedMessages.expectedGenerationId, claim.generationId), eq(chatQueuedMessages.activeGenerationId, claim.generationId))))
1419
+ .returning({ controlActionId: chatQueuedMessages.controlActionId });
1420
+ const uncertainControlActionIds = uncertainSteers
1421
+ .map((item) => item.controlActionId)
1422
+ .filter((id) => Boolean(id));
1423
+ if (uncertainControlActionIds.length > 0) {
1424
+ await tx
1425
+ .update(chatControlActions)
1426
+ .set({
1427
+ localDisposition: "acceptance_unknown",
1428
+ lastError: "terminal_projection_failed_provider_receipt_unknown",
1429
+ resolvedAt: now,
1430
+ updatedAt: now,
1431
+ })
1432
+ .where(inArray(chatControlActions.id, uncertainControlActionIds));
1433
+ }
1434
+ const failedSteers = await tx
1435
+ .update(chatQueuedMessages)
1436
+ .set({
1437
+ status: "failed_actionable",
1438
+ deliveryDisposition: "failed_actionable",
1439
+ reconciliationReason: "terminal_projection_failed_actionable",
1440
+ lastDeliveryReason: "terminal_projection_failed_actionable",
1441
+ version: sql `${chatQueuedMessages.version} + 1`,
1442
+ updatedAt: now,
1443
+ })
1444
+ .where(and(inArray(chatQueuedMessages.status, ["steer_pending", "continuation_pending"]), or(eq(chatQueuedMessages.expectedGenerationId, claim.generationId), eq(chatQueuedMessages.activeGenerationId, claim.generationId))))
1445
+ .returning({ controlActionId: chatQueuedMessages.controlActionId });
1446
+ const failedControlActionIds = failedSteers
1447
+ .map((item) => item.controlActionId)
1448
+ .filter((id) => Boolean(id));
1449
+ if (failedControlActionIds.length > 0) {
1450
+ await tx
1451
+ .update(chatControlActions)
1452
+ .set({
1453
+ localDisposition: "failed_actionable",
1454
+ lastError: "terminal_projection_failed_actionable",
1455
+ resolvedAt: now,
1456
+ updatedAt: now,
1457
+ })
1458
+ .where(inArray(chatControlActions.id, failedControlActionIds));
1459
+ }
1460
+ const controlActionId = typeof claim.payload.controlActionId === "string"
1461
+ ? claim.payload.controlActionId
1462
+ : null;
1463
+ if (controlActionId) {
1464
+ await tx
1465
+ .update(chatControlActions)
1466
+ .set({
1467
+ localDisposition: "failed_actionable",
1468
+ lastError: input.error,
1469
+ resolvedAt: now,
1470
+ updatedAt: now,
1471
+ })
1472
+ .where(and(eq(chatControlActions.id, controlActionId), eq(chatControlActions.orgId, claim.orgId), eq(chatControlActions.actionKind, "stop")));
1473
+ }
1474
+ return updated;
1475
+ });
1476
+ }
1477
+ async function recoverStaleControlOwners(input) {
1478
+ const now = input.now ?? new Date();
1479
+ const candidates = await db
1480
+ .select({ id: chatGenerations.id, orgId: chatGenerations.orgId })
1481
+ .from(chatGenerations)
1482
+ .where(and(inArray(chatGenerations.status, RECOVERABLE_CONTROL_GENERATION_STATUSES), isNotNull(chatGenerations.controlOwnerToken), input.assumeAllOwnersStale
1483
+ ? sql `true`
1484
+ : and(isNotNull(chatGenerations.controlLeaseExpiresAt), lte(chatGenerations.controlLeaseExpiresAt, now))))
1485
+ .orderBy(asc(chatGenerations.updatedAt))
1486
+ .limit(Math.max(1, input.limit ?? 100));
1487
+ const recovered = [];
1488
+ for (const candidate of candidates) {
1489
+ const result = await db.transaction(async (tx) => {
1490
+ const generation = await lockGeneration(tx, {
1491
+ orgId: candidate.orgId,
1492
+ generationId: candidate.id,
1493
+ });
1494
+ if (!RECOVERABLE_CONTROL_GENERATION_STATUSES.includes(generation.status)
1495
+ || !generation.controlOwnerToken
1496
+ || (!input.assumeAllOwnersStale
1497
+ && (!generation.controlLeaseExpiresAt || generation.controlLeaseExpiresAt > now))) {
1498
+ return null;
1499
+ }
1500
+ const nextControlVersion = generation.controlVersion + 1;
1501
+ const payload = {
1502
+ attemptEpoch: generation.attemptEpoch,
1503
+ finalStatus: "control_lost",
1504
+ terminalReason: "control_owner_stale",
1505
+ runtimeTerminationVerified: false,
1506
+ staleOwnerToken: generation.controlOwnerToken,
1507
+ staleAttemptEpoch: generation.attemptEpoch,
1508
+ };
1509
+ const event = await appendEventLocked(tx, {
1510
+ orgId: generation.orgId,
1511
+ generationId: generation.id,
1512
+ attemptEpoch: generation.attemptEpoch,
1513
+ eventKind: "terminal_projection_requested",
1514
+ payload,
1515
+ });
1516
+ const [outbox] = await tx
1517
+ .insert(chatGenerationTerminalOutbox)
1518
+ .values({
1519
+ orgId: generation.orgId,
1520
+ generationId: generation.id,
1521
+ sourceEventId: event.id,
1522
+ projectionVersion: nextControlVersion,
1523
+ expectedControlVersion: nextControlVersion,
1524
+ payload,
1525
+ availableAt: now,
1526
+ createdAt: now,
1527
+ updatedAt: now,
1528
+ })
1529
+ .returning();
1530
+ if (!outbox)
1531
+ throw new Error("Failed to enqueue stale chat control recovery");
1532
+ const [updatedGeneration] = await tx
1533
+ .update(chatGenerations)
1534
+ .set({
1535
+ status: "control_lost",
1536
+ terminalReason: "control_owner_stale",
1537
+ controlState: "control_lost",
1538
+ controlVersion: nextControlVersion,
1539
+ controlOwnerToken: null,
1540
+ controlLeaseExpiresAt: null,
1541
+ completedAt: now,
1542
+ updatedAt: now,
1543
+ })
1544
+ .where(and(eq(chatGenerations.id, generation.id), eq(chatGenerations.controlVersion, generation.controlVersion)))
1545
+ .returning();
1546
+ if (!updatedGeneration)
1547
+ throw conflict("Stale chat control owner changed during recovery");
1548
+ return { generation: updatedGeneration, event, outbox };
1549
+ });
1550
+ if (result)
1551
+ recovered.push(result);
1552
+ }
1553
+ return recovered;
1554
+ }
1555
+ return {
1556
+ getLatestVisibleCheckpoint,
1557
+ appendGenerationEvent,
1558
+ appendVisibleEventAndProject,
1559
+ getFrozenVisibleProjection,
1560
+ recordClientCheckpoint,
1561
+ beginStopAction,
1562
+ beginSteerFallbackCutoff,
1563
+ recordRuntimeTerminal,
1564
+ claimTerminalProjection,
1565
+ getNextTerminalProjectionWakeAt,
1566
+ completeTerminalProjection,
1567
+ retryTerminalProjection,
1568
+ recoverStaleControlOwners,
1569
+ };
1570
+ }
1571
+ //# sourceMappingURL=chat-generation-protocol.js.map