@rudderhq/server 0.4.6-canary.9 → 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 (350) 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 +348 -172
  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 +31 -54
  39. package/dist/index.js.map +1 -1
  40. package/dist/routes/access-onboarding.helpers.js +3 -3
  41. package/dist/routes/access-onboarding.helpers.js.map +1 -1
  42. package/dist/routes/access.helpers.d.ts.map +1 -1
  43. package/dist/routes/access.helpers.js +3 -2
  44. package/dist/routes/access.helpers.js.map +1 -1
  45. package/dist/routes/access.js +1 -1
  46. package/dist/routes/access.js.map +1 -1
  47. package/dist/routes/activity.d.ts.map +1 -1
  48. package/dist/routes/activity.js +5 -1
  49. package/dist/routes/activity.js.map +1 -1
  50. package/dist/routes/agents.management-routes.d.ts.map +1 -1
  51. package/dist/routes/agents.management-routes.js +13 -13
  52. package/dist/routes/agents.management-routes.js.map +1 -1
  53. package/dist/routes/assets.d.ts.map +1 -1
  54. package/dist/routes/assets.js +9 -2
  55. package/dist/routes/assets.js.map +1 -1
  56. package/dist/routes/chat-background-runtime.d.ts +19 -0
  57. package/dist/routes/chat-background-runtime.d.ts.map +1 -0
  58. package/dist/routes/chat-background-runtime.js +129 -0
  59. package/dist/routes/chat-background-runtime.js.map +1 -0
  60. package/dist/routes/chats.d.ts +2 -1
  61. package/dist/routes/chats.d.ts.map +1 -1
  62. package/dist/routes/chats.js +944 -24
  63. package/dist/routes/chats.js.map +1 -1
  64. package/dist/routes/chats.stream-routes.d.ts.map +1 -1
  65. package/dist/routes/chats.stream-routes.js +552 -76
  66. package/dist/routes/chats.stream-routes.js.map +1 -1
  67. package/dist/routes/issues.comments-attachments.d.ts.map +1 -1
  68. package/dist/routes/issues.comments-attachments.js +14 -31
  69. package/dist/routes/issues.comments-attachments.js.map +1 -1
  70. package/dist/routes/onboarding.js +1 -1
  71. package/dist/routes/onboarding.js.map +1 -1
  72. package/dist/routes/orgs.d.ts +2 -1
  73. package/dist/routes/orgs.d.ts.map +1 -1
  74. package/dist/routes/orgs.js +31 -2
  75. package/dist/routes/orgs.js.map +1 -1
  76. package/dist/routes/run-intelligence.d.ts.map +1 -1
  77. package/dist/routes/run-intelligence.js +76 -16
  78. package/dist/routes/run-intelligence.js.map +1 -1
  79. package/dist/services/activity-log.d.ts +4 -1
  80. package/dist/services/activity-log.d.ts.map +1 -1
  81. package/dist/services/activity-log.js +12 -4
  82. package/dist/services/activity-log.js.map +1 -1
  83. package/dist/services/activity.d.ts +11 -394
  84. package/dist/services/activity.d.ts.map +1 -1
  85. package/dist/services/activity.js +24 -18
  86. package/dist/services/activity.js.map +1 -1
  87. package/dist/services/agents.d.ts +741 -118
  88. package/dist/services/agents.d.ts.map +1 -1
  89. package/dist/services/approvals.d.ts +3 -3
  90. package/dist/services/automation-chat-output.d.ts.map +1 -1
  91. package/dist/services/automation-chat-output.js +115 -94
  92. package/dist/services/automation-chat-output.js.map +1 -1
  93. package/dist/services/budgets.d.ts.map +1 -1
  94. package/dist/services/budgets.js +38 -36
  95. package/dist/services/budgets.js.map +1 -1
  96. package/dist/services/chat-agent-runs.d.ts +3 -1
  97. package/dist/services/chat-agent-runs.d.ts.map +1 -1
  98. package/dist/services/chat-agent-runs.js +95 -34
  99. package/dist/services/chat-agent-runs.js.map +1 -1
  100. package/dist/services/chat-assistant.d.ts.map +1 -1
  101. package/dist/services/chat-assistant.helpers.d.ts +27 -4
  102. package/dist/services/chat-assistant.helpers.d.ts.map +1 -1
  103. package/dist/services/chat-assistant.helpers.js +66 -2
  104. package/dist/services/chat-assistant.helpers.js.map +1 -1
  105. package/dist/services/chat-assistant.inline-visuals.test.d.ts +2 -0
  106. package/dist/services/chat-assistant.inline-visuals.test.d.ts.map +1 -0
  107. package/dist/services/chat-assistant.inline-visuals.test.js +86 -0
  108. package/dist/services/chat-assistant.inline-visuals.test.js.map +1 -0
  109. package/dist/services/chat-assistant.js +115 -22
  110. package/dist/services/chat-assistant.js.map +1 -1
  111. package/dist/services/chat-generation-locks.d.ts +85 -0
  112. package/dist/services/chat-generation-locks.d.ts.map +1 -1
  113. package/dist/services/chat-generation-locks.js +371 -9
  114. package/dist/services/chat-generation-locks.js.map +1 -1
  115. package/dist/services/chat-generation-locks.test.d.ts +2 -0
  116. package/dist/services/chat-generation-locks.test.d.ts.map +1 -0
  117. package/dist/services/chat-generation-locks.test.js +324 -0
  118. package/dist/services/chat-generation-locks.test.js.map +1 -0
  119. package/dist/services/chat-generation-protocol.d.ts +674 -0
  120. package/dist/services/chat-generation-protocol.d.ts.map +1 -0
  121. package/dist/services/chat-generation-protocol.js +1571 -0
  122. package/dist/services/chat-generation-protocol.js.map +1 -0
  123. package/dist/services/chat-generation-protocol.test.d.ts +2 -0
  124. package/dist/services/chat-generation-protocol.test.d.ts.map +1 -0
  125. package/dist/services/chat-generation-protocol.test.js +1156 -0
  126. package/dist/services/chat-generation-protocol.test.js.map +1 -0
  127. package/dist/services/chat-title-generation.d.ts +0 -3
  128. package/dist/services/chat-title-generation.d.ts.map +1 -1
  129. package/dist/services/chat-title-generation.js +2 -32
  130. package/dist/services/chat-title-generation.js.map +1 -1
  131. package/dist/services/chat-work-manifest.d.ts.map +1 -1
  132. package/dist/services/chat-work-manifest.js +5 -1
  133. package/dist/services/chat-work-manifest.js.map +1 -1
  134. package/dist/services/chats.d.ts +1451 -272
  135. package/dist/services/chats.d.ts.map +1 -1
  136. package/dist/services/chats.js +1372 -35
  137. package/dist/services/chats.js.map +1 -1
  138. package/dist/services/costs.d.ts +34 -7
  139. package/dist/services/costs.d.ts.map +1 -1
  140. package/dist/services/costs.js +129 -61
  141. package/dist/services/costs.js.map +1 -1
  142. package/dist/services/finance.d.ts +3 -3
  143. package/dist/services/goals.d.ts +2 -2
  144. package/dist/services/heartbeat.d.ts +1 -1
  145. package/dist/services/heartbeat.d.ts.map +1 -1
  146. package/dist/services/heartbeat.js +1 -1
  147. package/dist/services/heartbeat.js.map +1 -1
  148. package/dist/services/index.d.ts +1 -0
  149. package/dist/services/index.d.ts.map +1 -1
  150. package/dist/services/index.js +1 -0
  151. package/dist/services/index.js.map +1 -1
  152. package/dist/services/integrations/agent-integrations.d.ts +1 -1
  153. package/dist/services/integrations/custom-integrations.d.ts +1 -1
  154. package/dist/services/integrations/feishu/runtime.d.ts.map +1 -1
  155. package/dist/services/integrations/feishu/runtime.js +16 -0
  156. package/dist/services/integrations/feishu/runtime.js.map +1 -1
  157. package/dist/services/issues.comments-attachments.d.ts +111 -0
  158. package/dist/services/issues.comments-attachments.d.ts.map +1 -1
  159. package/dist/services/issues.comments-attachments.js +138 -66
  160. package/dist/services/issues.comments-attachments.js.map +1 -1
  161. package/dist/services/issues.d.ts +56 -0
  162. package/dist/services/issues.d.ts.map +1 -1
  163. package/dist/services/issues.helpers.d.ts.map +1 -1
  164. package/dist/services/issues.helpers.js +2 -2
  165. package/dist/services/issues.helpers.js.map +1 -1
  166. package/dist/services/issues.js +5 -2
  167. package/dist/services/issues.js.map +1 -1
  168. package/dist/services/knowledge-portability/organization-skills.catalog.d.ts.map +1 -1
  169. package/dist/services/knowledge-portability/organization-skills.catalog.js +7 -1
  170. package/dist/services/knowledge-portability/organization-skills.catalog.js.map +1 -1
  171. package/dist/services/messenger.d.ts.map +1 -1
  172. package/dist/services/messenger.js +11 -9
  173. package/dist/services/messenger.js.map +1 -1
  174. package/dist/services/organization-workspace-browser.d.ts.map +1 -1
  175. package/dist/services/organization-workspace-browser.js +19 -3
  176. package/dist/services/organization-workspace-browser.js.map +1 -1
  177. package/dist/services/orgs.d.ts.map +1 -1
  178. package/dist/services/orgs.js +36 -12
  179. package/dist/services/orgs.js.map +1 -1
  180. package/dist/services/plugin-dev-watcher.d.ts +19 -2
  181. package/dist/services/plugin-dev-watcher.d.ts.map +1 -1
  182. package/dist/services/plugin-dev-watcher.js +45 -11
  183. package/dist/services/plugin-dev-watcher.js.map +1 -1
  184. package/dist/services/plugin-registry.d.ts +28 -28
  185. package/dist/services/projects.d.ts +1 -1
  186. package/dist/services/run-events.d.ts +20 -0
  187. package/dist/services/run-events.d.ts.map +1 -0
  188. package/dist/services/run-events.js +34 -0
  189. package/dist/services/run-events.js.map +1 -0
  190. package/dist/services/run-intelligence.d.ts +16 -4
  191. package/dist/services/run-intelligence.d.ts.map +1 -1
  192. package/dist/services/run-intelligence.js +90 -15
  193. package/dist/services/run-intelligence.js.map +1 -1
  194. package/dist/services/runtime-kernel/heartbeat.core.d.ts +23 -0
  195. package/dist/services/runtime-kernel/heartbeat.core.d.ts.map +1 -1
  196. package/dist/services/runtime-kernel/heartbeat.core.js +1 -0
  197. package/dist/services/runtime-kernel/heartbeat.core.js.map +1 -1
  198. package/dist/services/runtime-kernel/heartbeat.d.ts +95 -37
  199. package/dist/services/runtime-kernel/heartbeat.d.ts.map +1 -1
  200. package/dist/services/runtime-kernel/heartbeat.execute.d.ts +3 -1
  201. package/dist/services/runtime-kernel/heartbeat.execute.d.ts.map +1 -1
  202. package/dist/services/runtime-kernel/heartbeat.execute.js +389 -219
  203. package/dist/services/runtime-kernel/heartbeat.execute.js.map +1 -1
  204. package/dist/services/runtime-kernel/heartbeat.js +594 -204
  205. package/dist/services/runtime-kernel/heartbeat.js.map +1 -1
  206. package/dist/services/runtime-kernel/heartbeat.misc.d.ts.map +1 -1
  207. package/dist/services/runtime-kernel/heartbeat.misc.js +34 -24
  208. package/dist/services/runtime-kernel/heartbeat.misc.js.map +1 -1
  209. package/dist/services/runtime-kernel/heartbeat.recovery.d.ts.map +1 -1
  210. package/dist/services/runtime-kernel/heartbeat.recovery.js +56 -6
  211. package/dist/services/runtime-kernel/heartbeat.recovery.js.map +1 -1
  212. package/dist/services/runtime-kernel/heartbeat.release.d.ts +3 -1
  213. package/dist/services/runtime-kernel/heartbeat.release.d.ts.map +1 -1
  214. package/dist/services/runtime-kernel/heartbeat.release.js +276 -44
  215. package/dist/services/runtime-kernel/heartbeat.release.js.map +1 -1
  216. package/dist/services/runtime-kernel/heartbeat.sessions.d.ts +42 -1
  217. package/dist/services/runtime-kernel/heartbeat.sessions.d.ts.map +1 -1
  218. package/dist/services/runtime-kernel/heartbeat.sessions.js +110 -8
  219. package/dist/services/runtime-kernel/heartbeat.sessions.js.map +1 -1
  220. package/dist/services/runtime-kernel/heartbeat.terminal.d.ts +575 -0
  221. package/dist/services/runtime-kernel/heartbeat.terminal.d.ts.map +1 -0
  222. package/dist/services/runtime-kernel/heartbeat.terminal.js +589 -0
  223. package/dist/services/runtime-kernel/heartbeat.terminal.js.map +1 -0
  224. package/dist/services/runtime-kernel/heartbeat.wakeup.d.ts.map +1 -1
  225. package/dist/services/runtime-kernel/heartbeat.wakeup.js +238 -92
  226. package/dist/services/runtime-kernel/heartbeat.wakeup.js.map +1 -1
  227. package/dist/services/runtime-kernel/model-fallback.d.ts.map +1 -1
  228. package/dist/services/runtime-kernel/model-fallback.js +11 -0
  229. package/dist/services/runtime-kernel/model-fallback.js.map +1 -1
  230. package/dist/services/side-chats.d.ts +212 -0
  231. package/dist/services/side-chats.d.ts.map +1 -0
  232. package/dist/services/side-chats.js +379 -0
  233. package/dist/services/side-chats.js.map +1 -0
  234. package/dist/services/workspace-runtime.services.d.ts +1 -1
  235. package/dist/services/workspace-web-preview.d.ts +36 -0
  236. package/dist/services/workspace-web-preview.d.ts.map +1 -0
  237. package/dist/services/workspace-web-preview.js +566 -0
  238. package/dist/services/workspace-web-preview.js.map +1 -0
  239. package/package.json +13 -13
  240. package/resources/bundled-skills/rudder-docs/SKILL.md +161 -0
  241. package/resources/bundled-skills/rudder-docs/evals/trigger-evals.json +82 -0
  242. package/{skills/rudder → resources/bundled-skills/rudder-docs}/references/api-reference.md +17 -1
  243. package/{skills/rudder → resources/bundled-skills/rudder-docs}/references/cli-reference.md +49 -94
  244. package/resources/bundled-skills/rudder-docs/references/control-plane-practices.md +231 -0
  245. package/{skills/rudder → resources/bundled-skills/rudder-docs}/references/organization-skills.md +15 -1
  246. package/resources/bundled-skills/rudder-docs/references/source-map.md +138 -0
  247. package/resources/bundled-skills/visualize/SKILL.md +132 -0
  248. package/resources/bundled-skills/visualize/agents/openai.yaml +7 -0
  249. package/resources/bundled-skills/visualize/assets/example-chart.html +89 -0
  250. package/resources/bundled-skills/visualize/references/runtime-contract.md +114 -0
  251. package/skills/rudder-docs/SKILL.md +161 -0
  252. package/skills/rudder-docs/evals/trigger-evals.json +82 -0
  253. package/{resources/bundled-skills/rudder → skills/rudder-docs}/references/api-reference.md +17 -1
  254. package/{resources/bundled-skills/rudder → skills/rudder-docs}/references/cli-reference.md +49 -94
  255. package/skills/rudder-docs/references/control-plane-practices.md +231 -0
  256. package/{resources/bundled-skills/rudder → skills/rudder-docs}/references/organization-skills.md +15 -1
  257. package/skills/rudder-docs/references/source-map.md +138 -0
  258. package/skills/visualize/SKILL.md +132 -0
  259. package/skills/visualize/agents/openai.yaml +7 -0
  260. package/skills/visualize/assets/example-chart.html +89 -0
  261. package/skills/visualize/references/runtime-contract.md +114 -0
  262. package/ui-dist/assets/{abnfDiagram-VRR7QNED-CKLAbEpH.js → abnfDiagram-VRR7QNED-Dpnx79fp.js} +1 -1
  263. package/ui-dist/assets/{arc-CS20WjB1.js → arc-Cpvh_rCf.js} +1 -1
  264. package/ui-dist/assets/{architectureDiagram-ZJ3FMSHR-BY4lounK.js → architectureDiagram-ZJ3FMSHR-sDD_mBAI.js} +1 -1
  265. package/ui-dist/assets/{blockDiagram-677ZJIJ3-BHtHl-hm.js → blockDiagram-677ZJIJ3-4O4wWti1.js} +1 -1
  266. package/ui-dist/assets/{c4Diagram-LMCZKHZV-DgOJp7yn.js → c4Diagram-LMCZKHZV-BkGODE4P.js} +1 -1
  267. package/ui-dist/assets/channel-CM4Myg_6.js +1 -0
  268. package/ui-dist/assets/{chunk-2Q5K7J3B-Uin6HTPH.js → chunk-2Q5K7J3B-DsDQQpID.js} +1 -1
  269. package/ui-dist/assets/{chunk-32BRIVSS-BxAI7yMx.js → chunk-32BRIVSS-Bzx_VnIw.js} +1 -1
  270. package/ui-dist/assets/{chunk-5VM5RSS4-Ce6AR6Sg.js → chunk-5VM5RSS4-D4LoPCni.js} +1 -1
  271. package/ui-dist/assets/{chunk-EX3LRPZG-DgxPO59e.js → chunk-EX3LRPZG-BO0R-mS4.js} +3 -3
  272. package/ui-dist/assets/{chunk-JWPE2WC7-Bn3BYmxA.js → chunk-JWPE2WC7-B4Nr8KRL.js} +1 -1
  273. package/ui-dist/assets/{chunk-MOJQB5TN-BlTdGU58.js → chunk-MOJQB5TN--uaDDrYM.js} +1 -1
  274. package/ui-dist/assets/{chunk-RYQCIY6F-C0V6evNH.js → chunk-RYQCIY6F-CyBY2w_j.js} +1 -1
  275. package/ui-dist/assets/{chunk-V7JOEXUC-DjvZ40fp.js → chunk-V7JOEXUC-BMid_x8S.js} +3 -3
  276. package/ui-dist/assets/{chunk-VR4S4FIN-CejmQVxi.js → chunk-VR4S4FIN-DVqdPKuG.js} +1 -1
  277. package/ui-dist/assets/{chunk-XXDRQBXY-tj-vHLHN.js → chunk-XXDRQBXY-vy03lLSS.js} +1 -1
  278. package/ui-dist/assets/classDiagram-OUVF2IWQ-CtlIEzTt.js +1 -0
  279. package/ui-dist/assets/classDiagram-v2-EOCWNBFH-CtlIEzTt.js +1 -0
  280. package/ui-dist/assets/{cose-bilkent-JH36ORCC-Ci7DAkTj.js → cose-bilkent-JH36ORCC-jQDZmrG3.js} +1 -1
  281. package/ui-dist/assets/{cynefin-VYW2F7L2-CHU2plsQ.js → cynefin-VYW2F7L2-BxBj6aea.js} +1 -1
  282. package/ui-dist/assets/{cynefinDiagram-TSTJHNR4-pqqNoczb.js → cynefinDiagram-TSTJHNR4-BuUubF5I.js} +1 -1
  283. package/ui-dist/assets/{dagre-VKFMJZFB-Bk8Y_kGf.js → dagre-VKFMJZFB-B8HipJ3a.js} +1 -1
  284. package/ui-dist/assets/{diagram-FQU43EPY-CiSxIS2M.js → diagram-FQU43EPY-r7gkbKSt.js} +2 -2
  285. package/ui-dist/assets/{diagram-G47NLZAW-qpVsOLnz.js → diagram-G47NLZAW-CxClRsxD.js} +1 -1
  286. package/ui-dist/assets/{diagram-NH7WQ7WH-Cda5JA3K.js → diagram-NH7WQ7WH-C8HYTLlq.js} +1 -1
  287. package/ui-dist/assets/{diagram-OA4YK3LP-Dh_DdE2d.js → diagram-OA4YK3LP-CjwqqLKz.js} +1 -1
  288. package/ui-dist/assets/{diagram-WEI45ONY-r5ouUhNZ.js → diagram-WEI45ONY-2IWfCqZb.js} +1 -1
  289. package/ui-dist/assets/{ebnfDiagram-CCIWWBDH-C6lDAmmi.js → ebnfDiagram-CCIWWBDH-DVGZkgjy.js} +1 -1
  290. package/ui-dist/assets/{erDiagram-Q63AITRT-BznPfnRu.js → erDiagram-Q63AITRT-C_-u3Gmw.js} +1 -1
  291. package/ui-dist/assets/{flowDiagram-23GEKE2U-bCaogpDj.js → flowDiagram-23GEKE2U-BvGg0NCq.js} +4 -4
  292. package/ui-dist/assets/{ganttDiagram-NO4QXBWP-Bn-e5kP6.js → ganttDiagram-NO4QXBWP-U3CZUUUR.js} +1 -1
  293. package/ui-dist/assets/{gitGraphDiagram-IHSO6WYX-D7E-RLhl.js → gitGraphDiagram-IHSO6WYX-CIWuEBIo.js} +1 -1
  294. package/ui-dist/assets/{graph-3haZvvEy.js → graph-D3P5q-7m.js} +1 -1
  295. package/ui-dist/assets/index-B1rLBtZ1.js +1875 -0
  296. package/ui-dist/assets/{index-BUneV0z1.js → index-BAMXpiI5.js} +1 -1
  297. package/ui-dist/assets/{index-CheuLaX8.js → index-BAgtKYR1.js} +1 -1
  298. package/ui-dist/assets/{index-BDji4WI2.js → index-CBOr9F6w.js} +1 -1
  299. package/ui-dist/assets/{index-DlcNrNku.js → index-CbHqnWUe.js} +1 -1
  300. package/ui-dist/assets/{index-C1NcizE3.js → index-CjztBLJi.js} +1 -1
  301. package/ui-dist/assets/{index-JqztCnnf.js → index-Cu1iMhjl.js} +1 -1
  302. package/ui-dist/assets/{index-BFnwTV9t.js → index-D7ycf1qU.js} +1 -1
  303. package/ui-dist/assets/{index-JLGi6sfB.js → index-DAYlwxsg.js} +1 -1
  304. package/ui-dist/assets/{index-Bje1DqHh.js → index-DCFf9dps.js} +4 -4
  305. package/ui-dist/assets/{index-zyCQr__a.js → index-DOPIV8vd.js} +1 -1
  306. package/ui-dist/assets/{index-JEy5ZJbE.js → index-DRxLJD40.js} +1 -1
  307. package/ui-dist/assets/{index-Doc88zxN.js → index-DYMtCDKT.js} +1 -1
  308. package/ui-dist/assets/index-DekHKOsL.css +1 -0
  309. package/ui-dist/assets/{index-BN3GMGqN.js → index-DidCCw78.js} +1 -1
  310. package/ui-dist/assets/{index-DKRH0hjR.js → index-Ds_qma4-.js} +1 -1
  311. package/ui-dist/assets/{index-B2bHoMTu.js → index-HkQRx1D6.js} +1 -1
  312. package/ui-dist/assets/{index-DLxzQvPB.js → index-ikpL--zj.js} +1 -1
  313. package/ui-dist/assets/{index-mE0kmjy0.js → index-mnl55Stn.js} +1 -1
  314. package/ui-dist/assets/{infoDiagram-FWYZ7A6U-NSQ6saR1.js → infoDiagram-FWYZ7A6U-Cx8Fa-I4.js} +1 -1
  315. package/ui-dist/assets/{ishikawaDiagram-FXEZZL3T-XhGk3DMM.js → ishikawaDiagram-FXEZZL3T-Cmuz1QUN.js} +1 -1
  316. package/ui-dist/assets/{journeyDiagram-5HDEW3XC-CUyh1IKu.js → journeyDiagram-5HDEW3XC-CJgkiVXG.js} +1 -1
  317. package/ui-dist/assets/{kanban-definition-HUTT4EX6-P_0zllUc.js → kanban-definition-HUTT4EX6-RAWpefOB.js} +1 -1
  318. package/ui-dist/assets/layout-DyWQv0Rr.js +1 -0
  319. package/ui-dist/assets/{linear-DWu-hp7y.js → linear-D5pdgIf9.js} +1 -1
  320. package/ui-dist/assets/{map-B2IiHu-a.js → map-bVSzNLl2.js} +1 -1
  321. package/ui-dist/assets/mermaid.core-CeaXGD6B.js +312 -0
  322. package/ui-dist/assets/{mindmap-definition-LN4V7U3C-DpKo7DKc.js → mindmap-definition-LN4V7U3C-B8B8AX5C.js} +1 -1
  323. package/ui-dist/assets/{pegDiagram-2B236MQR-DzDDMRRX.js → pegDiagram-2B236MQR-C4Sv-ua6.js} +1 -1
  324. package/ui-dist/assets/{pieDiagram-ENE6RG2P-Dr7ZCi_a.js → pieDiagram-ENE6RG2P-CLARlOHx.js} +1 -1
  325. package/ui-dist/assets/{quadrantDiagram-ABIIQ3AL-BB_gv9s9.js → quadrantDiagram-ABIIQ3AL-DPCXQmeT.js} +1 -1
  326. package/ui-dist/assets/{railroadDiagram-RFXS5EU6-C2ji_jMN.js → railroadDiagram-RFXS5EU6-BpqRuKUQ.js} +1 -1
  327. package/ui-dist/assets/{requirementDiagram-TGXJPOKE-BJg8vYwc.js → requirementDiagram-TGXJPOKE-Dd-h62HZ.js} +1 -1
  328. package/ui-dist/assets/{sankeyDiagram-HTMAVEWB-msvrlvO_.js → sankeyDiagram-HTMAVEWB-DylT_pqq.js} +1 -1
  329. package/ui-dist/assets/{sequenceDiagram-DBY2YBRQ-B_B68qK5.js → sequenceDiagram-DBY2YBRQ-RiCF1REj.js} +1 -1
  330. package/ui-dist/assets/{sizeCapture-X5ZJPWSS-D0VdKiQi.js → sizeCapture-X5ZJPWSS-DXW4JQz2.js} +1 -1
  331. package/ui-dist/assets/{stateDiagram-2N3HPSRC-B4V-owEQ.js → stateDiagram-2N3HPSRC-6U3ESIGT.js} +1 -1
  332. package/ui-dist/assets/stateDiagram-v2-6OUMAXLB-KQTrcZvp.js +1 -0
  333. package/ui-dist/assets/{swimlanes-5IMT3BWC-B1JTwGb7.js → swimlanes-5IMT3BWC-CeNHVNCi.js} +2 -2
  334. package/ui-dist/assets/swimlanesDiagram-G3AALYLV-CcxS1c6r.js +8 -0
  335. package/ui-dist/assets/{timeline-definition-FHXFAJF6-C-8YlQA9.js → timeline-definition-FHXFAJF6-Vlwoe_1b.js} +1 -1
  336. package/ui-dist/assets/{vennDiagram-L72KCM5P-DP13xuPX.js → vennDiagram-L72KCM5P-DBP7D3HE.js} +6 -6
  337. package/ui-dist/assets/{wardleyDiagram-EHGQE667-D6BNPJwb.js → wardleyDiagram-EHGQE667-DG-wzqpy.js} +1 -1
  338. package/ui-dist/assets/{xychartDiagram-FW5EYKEG-my7CwP2H.js → xychartDiagram-FW5EYKEG-BAq3mbha.js} +1 -1
  339. package/ui-dist/index.html +2 -2
  340. package/resources/bundled-skills/rudder/SKILL.md +0 -314
  341. package/skills/rudder/SKILL.md +0 -314
  342. package/ui-dist/assets/channel-D-nDuwao.js +0 -1
  343. package/ui-dist/assets/classDiagram-OUVF2IWQ-Ds2nJ2Kt.js +0 -1
  344. package/ui-dist/assets/classDiagram-v2-EOCWNBFH-Ds2nJ2Kt.js +0 -1
  345. package/ui-dist/assets/index-BA7XbHmf.css +0 -1
  346. package/ui-dist/assets/index-hE0ZX3Ud.js +0 -1820
  347. package/ui-dist/assets/layout-C-tMX-j8.js +0 -1
  348. package/ui-dist/assets/mermaid.core-WA5rvr0F.js +0 -314
  349. package/ui-dist/assets/stateDiagram-v2-6OUMAXLB-DAB9Vdyb.js +0 -1
  350. package/ui-dist/assets/swimlanesDiagram-G3AALYLV-DRt36f68.js +0 -8
@@ -0,0 +1,1156 @@
1
+ import { applyPendingMigrations, chatControlActions, chatConversations, chatGenerationEvents, chatGenerations, chatGenerationTerminalOutbox, chatMessages, chatQueuedMessages, createDb, ensurePostgresDatabase, organizations, } from "@rudderhq/db";
2
+ import { deriveOrganizationUrlKey } from "@rudderhq/shared";
3
+ import { asc, eq } from "drizzle-orm";
4
+ import { randomUUID } from "node:crypto";
5
+ import fs from "node:fs";
6
+ import net from "node:net";
7
+ import os from "node:os";
8
+ import path from "node:path";
9
+ import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
10
+ import { chatGenerationProtocolService, hashChatGenerationBody, } from "./chat-generation-protocol.js";
11
+ async function getAvailablePort() {
12
+ return new Promise((resolve, reject) => {
13
+ const server = net.createServer();
14
+ server.unref();
15
+ server.on("error", reject);
16
+ server.listen(0, "127.0.0.1", () => {
17
+ const address = server.address();
18
+ if (!address || typeof address === "string") {
19
+ server.close(() => reject(new Error("Failed to allocate test port")));
20
+ return;
21
+ }
22
+ server.close((error) => error ? reject(error) : resolve(address.port));
23
+ });
24
+ });
25
+ }
26
+ async function startTempDatabase() {
27
+ const externalUrl = process.env.RUDDER_CHAT_GENERATION_PROTOCOL_TEST_DATABASE_URL?.trim();
28
+ if (externalUrl) {
29
+ await applyPendingMigrations(externalUrl);
30
+ return { connectionString: externalUrl, dataDir: "", instance: null };
31
+ }
32
+ const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "rudder-chat-generation-protocol-"));
33
+ const port = await getAvailablePort();
34
+ const mod = await import("embedded-postgres");
35
+ const EmbeddedPostgres = mod.default;
36
+ const instance = new EmbeddedPostgres({
37
+ databaseDir: dataDir,
38
+ user: "rudder",
39
+ password: "rudder",
40
+ port,
41
+ persistent: true,
42
+ initdbFlags: ["--encoding=UTF8", "--locale=C"],
43
+ onLog: () => { },
44
+ onError: () => { },
45
+ });
46
+ await instance.initialise();
47
+ await instance.start();
48
+ const adminUrl = `postgres://rudder:rudder@127.0.0.1:${port}/postgres`;
49
+ await ensurePostgresDatabase(adminUrl, "rudder");
50
+ const connectionString = `postgres://rudder:rudder@127.0.0.1:${port}/rudder`;
51
+ await applyPendingMigrations(connectionString);
52
+ return { connectionString, dataDir, instance };
53
+ }
54
+ describe("chatGenerationProtocolService", () => {
55
+ let db;
56
+ let protocol;
57
+ let instance = null;
58
+ let dataDir = "";
59
+ beforeAll(async () => {
60
+ const started = await startTempDatabase();
61
+ db = createDb(started.connectionString);
62
+ protocol = chatGenerationProtocolService(db);
63
+ instance = started.instance;
64
+ dataDir = started.dataDir;
65
+ }, 30_000);
66
+ afterEach(async () => {
67
+ await db.delete(chatGenerationTerminalOutbox);
68
+ await db.delete(chatGenerationEvents);
69
+ await db.delete(chatControlActions);
70
+ await db.delete(chatGenerations);
71
+ await db.delete(chatConversations);
72
+ await db.delete(organizations);
73
+ });
74
+ afterAll(async () => {
75
+ await instance?.stop();
76
+ if (dataDir)
77
+ fs.rmSync(dataDir, { recursive: true, force: true });
78
+ });
79
+ async function seedGeneration(input = {}) {
80
+ const orgId = input.orgId ?? randomUUID();
81
+ const conversationId = input.conversationId ?? randomUUID();
82
+ const generationId = input.generationId ?? randomUUID();
83
+ const existingOrg = await db
84
+ .select({ id: organizations.id })
85
+ .from(organizations)
86
+ .where(eq(organizations.id, orgId))
87
+ .limit(1);
88
+ if (existingOrg.length === 0) {
89
+ await db.insert(organizations).values({
90
+ id: orgId,
91
+ name: `Protocol ${orgId.slice(0, 6)}`,
92
+ urlKey: deriveOrganizationUrlKey(`protocol-${orgId}`),
93
+ issuePrefix: `P${orgId.replaceAll("-", "").slice(0, 5).toUpperCase()}`,
94
+ requireBoardApprovalForNewAgents: false,
95
+ });
96
+ }
97
+ await db.insert(chatConversations).values({
98
+ id: conversationId,
99
+ orgId,
100
+ title: "Protocol test",
101
+ });
102
+ const [generation] = await db.insert(chatGenerations).values({
103
+ id: generationId,
104
+ orgId,
105
+ conversationId,
106
+ status: input.status ?? "running",
107
+ attemptEpoch: input.attemptEpoch ?? 3,
108
+ controlVersion: input.controlVersion ?? 0,
109
+ controlState: input.ownerToken === null ? "unregistered" : "ready",
110
+ controlOwnerToken: input.ownerToken === undefined ? "owner-1" : input.ownerToken,
111
+ controlLeaseExpiresAt: input.leaseExpiresAt ?? new Date(Date.now() + 60_000),
112
+ }).returning();
113
+ if (!generation)
114
+ throw new Error("Failed to seed generation");
115
+ return generation;
116
+ }
117
+ it("serializes visible events and applies an idempotent Stop cutoff", async () => {
118
+ const generation = await seedGeneration();
119
+ const bodies = ["First", "First second"];
120
+ const appended = await Promise.all(bodies.map((body, index) => protocol.appendGenerationEvent({
121
+ orgId: generation.orgId,
122
+ conversationId: generation.conversationId,
123
+ generationId: generation.id,
124
+ attemptEpoch: generation.attemptEpoch,
125
+ expectedOwnerToken: generation.controlOwnerToken,
126
+ admission: "visible",
127
+ eventKind: "assistant_delta",
128
+ payload: { delta: index === 0 ? "First" : " second" },
129
+ bodyOffset: index === 0 ? 0 : 5,
130
+ bodyLength: index === 0 ? 5 : 7,
131
+ bodyHash: hashChatGenerationBody(body),
132
+ })));
133
+ expect(appended.map(({ event }) => event.generationSeq).sort((a, b) => a - b)).toEqual([1, 2]);
134
+ const latest = appended.reduce((left, right) => left.event.generationSeq > right.event.generationSeq ? left : right);
135
+ const latestHash = String(latest.event.payload.bodyHash);
136
+ const checkpoint = await protocol.recordClientCheckpoint({
137
+ orgId: generation.orgId,
138
+ conversationId: generation.conversationId,
139
+ generationId: generation.id,
140
+ expectedAttemptEpoch: generation.attemptEpoch,
141
+ expectedOwnerToken: generation.controlOwnerToken,
142
+ generationSeq: latest.event.generationSeq,
143
+ renderedBodyHash: latestHash,
144
+ });
145
+ expect(checkpoint.advanced).toBe(true);
146
+ await expect(protocol.beginStopAction({
147
+ orgId: generation.orgId,
148
+ conversationId: generation.conversationId,
149
+ controlActionId: randomUUID(),
150
+ expectedGenerationId: generation.id,
151
+ expectedAttemptEpoch: generation.attemptEpoch,
152
+ expectedControlVersion: 0,
153
+ requestedRenderSeq: latest.event.generationSeq,
154
+ requestedBodyHash: hashChatGenerationBody("different body"),
155
+ })).rejects.toMatchObject({ status: 409 });
156
+ expect(await db.select().from(chatControlActions)).toHaveLength(0);
157
+ expect(await db.select().from(chatGenerationEvents)).toHaveLength(3);
158
+ const controlActionId = randomUUID();
159
+ const stop = await protocol.beginStopAction({
160
+ orgId: generation.orgId,
161
+ conversationId: generation.conversationId,
162
+ controlActionId,
163
+ expectedGenerationId: generation.id,
164
+ expectedAttemptEpoch: generation.attemptEpoch,
165
+ expectedControlVersion: 0,
166
+ requestedRenderSeq: latest.event.generationSeq,
167
+ requestedBodyHash: latestHash,
168
+ });
169
+ expect(stop.idempotent).toBe(false);
170
+ expect(stop.generation).toMatchObject({
171
+ status: "stop_requested",
172
+ controlState: "stopping",
173
+ controlVersion: 1,
174
+ acceptedThroughSeq: latest.event.generationSeq,
175
+ frozenBodyHash: latestHash,
176
+ });
177
+ expect(stop.outputCutoffEvent?.generationSeq).toBe((stop.stopRequestedEvent?.generationSeq ?? 0) + 1);
178
+ const repeated = await protocol.beginStopAction({
179
+ orgId: generation.orgId,
180
+ conversationId: generation.conversationId,
181
+ controlActionId,
182
+ expectedGenerationId: generation.id,
183
+ expectedAttemptEpoch: generation.attemptEpoch,
184
+ expectedControlVersion: 1,
185
+ requestedRenderSeq: latest.event.generationSeq,
186
+ requestedBodyHash: latestHash,
187
+ });
188
+ expect(repeated.idempotent).toBe(true);
189
+ expect(await db.select().from(chatControlActions)).toHaveLength(1);
190
+ expect(await db.select().from(chatGenerationEvents)).toHaveLength(5);
191
+ await expect(protocol.appendGenerationEvent({
192
+ orgId: generation.orgId,
193
+ generationId: generation.id,
194
+ attemptEpoch: generation.attemptEpoch,
195
+ expectedOwnerToken: generation.controlOwnerToken,
196
+ admission: "visible",
197
+ eventKind: "assistant_delta",
198
+ payload: { delta: " late" },
199
+ bodyHash: hashChatGenerationBody("late"),
200
+ })).rejects.toMatchObject({ status: 409 });
201
+ });
202
+ it("never regresses a Stop cutoff behind the latest acknowledged client checkpoint", async () => {
203
+ const generation = await seedGeneration();
204
+ const first = await protocol.appendGenerationEvent({
205
+ orgId: generation.orgId,
206
+ conversationId: generation.conversationId,
207
+ generationId: generation.id,
208
+ attemptEpoch: generation.attemptEpoch,
209
+ expectedOwnerToken: generation.controlOwnerToken,
210
+ admission: "visible",
211
+ eventKind: "assistant_delta",
212
+ payload: { delta: "Visible reply" },
213
+ bodyOffset: 0,
214
+ bodyLength: 13,
215
+ bodyHash: hashChatGenerationBody("Visible reply"),
216
+ });
217
+ await protocol.recordClientCheckpoint({
218
+ orgId: generation.orgId,
219
+ conversationId: generation.conversationId,
220
+ generationId: generation.id,
221
+ expectedAttemptEpoch: generation.attemptEpoch,
222
+ expectedOwnerToken: generation.controlOwnerToken,
223
+ generationSeq: first.event.generationSeq,
224
+ renderedBodyHash: hashChatGenerationBody("Visible reply"),
225
+ });
226
+ const stop = await protocol.beginStopAction({
227
+ orgId: generation.orgId,
228
+ conversationId: generation.conversationId,
229
+ controlActionId: randomUUID(),
230
+ expectedGenerationId: generation.id,
231
+ expectedAttemptEpoch: generation.attemptEpoch,
232
+ expectedControlVersion: 0,
233
+ requestedRenderSeq: 0,
234
+ requestedBodyHash: hashChatGenerationBody(""),
235
+ });
236
+ expect(stop.action).toMatchObject({
237
+ acceptedThroughSeq: first.event.generationSeq,
238
+ frozenBodyHash: hashChatGenerationBody("Visible reply"),
239
+ });
240
+ });
241
+ it("persists a completion outcome when final output admission wins ahead of Stop", async () => {
242
+ const generation = await seedGeneration({ status: "closing" });
243
+ const finalBody = "Committed final reply";
244
+ const controlActionId = randomUUID();
245
+ const completion = await protocol.appendVisibleEventAndProject({
246
+ orgId: generation.orgId,
247
+ conversationId: generation.conversationId,
248
+ generationId: generation.id,
249
+ expectedAttemptEpoch: generation.attemptEpoch,
250
+ eventKind: "runtime_output",
251
+ payload: { resultKind: "message", body: finalBody },
252
+ bodyOffset: 0,
253
+ bodyLength: finalBody.length,
254
+ bodyHash: hashChatGenerationBody(finalBody),
255
+ body: finalBody,
256
+ transcript: [],
257
+ chatTurnId: randomUUID(),
258
+ turnVariant: 0,
259
+ });
260
+ const stop = await protocol.beginStopAction({
261
+ orgId: generation.orgId,
262
+ conversationId: generation.conversationId,
263
+ controlActionId,
264
+ expectedGenerationId: generation.id,
265
+ expectedAttemptEpoch: generation.attemptEpoch,
266
+ expectedControlVersion: generation.controlVersion,
267
+ requestedRenderSeq: completion.event.generationSeq,
268
+ requestedBodyHash: hashChatGenerationBody(finalBody),
269
+ });
270
+ expect(stop).toMatchObject({
271
+ outcome: "completion_committed",
272
+ idempotent: false,
273
+ action: {
274
+ id: controlActionId,
275
+ localDisposition: "cancelled",
276
+ providerDisposition: "not_sent",
277
+ lastError: "generation_result_already_committed",
278
+ },
279
+ });
280
+ expect(stop.stopRequestedEvent).toBeNull();
281
+ expect(stop.outputCutoffEvent).toBeNull();
282
+ expect(stop.action.providerEvidence).toMatchObject({
283
+ completionEventId: completion.event.id,
284
+ completionGenerationSeq: completion.event.generationSeq,
285
+ });
286
+ expect(await db.select().from(chatControlActions)).toHaveLength(1);
287
+ expect(completion.message).toMatchObject({
288
+ body: finalBody,
289
+ status: "streaming",
290
+ });
291
+ const repeated = await protocol.beginStopAction({
292
+ orgId: generation.orgId,
293
+ conversationId: generation.conversationId,
294
+ controlActionId,
295
+ expectedGenerationId: generation.id,
296
+ expectedAttemptEpoch: generation.attemptEpoch,
297
+ expectedControlVersion: generation.controlVersion,
298
+ requestedRenderSeq: completion.event.generationSeq,
299
+ requestedBodyHash: hashChatGenerationBody(finalBody),
300
+ });
301
+ expect(repeated).toMatchObject({
302
+ outcome: "completion_committed",
303
+ idempotent: true,
304
+ action: { id: controlActionId },
305
+ });
306
+ await expect(protocol.appendVisibleEventAndProject({
307
+ orgId: generation.orgId,
308
+ conversationId: generation.conversationId,
309
+ generationId: generation.id,
310
+ expectedAttemptEpoch: generation.attemptEpoch,
311
+ eventKind: "assistant_delta",
312
+ payload: { delta: " late" },
313
+ messageId: completion.message.id,
314
+ bodyHash: hashChatGenerationBody(`${finalBody} late`),
315
+ body: `${finalBody} late`,
316
+ transcript: [],
317
+ chatTurnId: completion.message.chatTurnId,
318
+ turnVariant: completion.message.turnVariant,
319
+ })).rejects.toMatchObject({
320
+ status: 409,
321
+ message: "Chat-visible output admission is closed for this generation",
322
+ });
323
+ await expect(protocol.appendGenerationEvent({
324
+ orgId: generation.orgId,
325
+ conversationId: generation.conversationId,
326
+ generationId: generation.id,
327
+ attemptEpoch: generation.attemptEpoch,
328
+ admission: "visible",
329
+ eventKind: "assistant_delta",
330
+ payload: { delta: " legacy late" },
331
+ bodyHash: hashChatGenerationBody(`${finalBody} legacy late`),
332
+ })).rejects.toMatchObject({
333
+ status: 409,
334
+ message: "Chat-visible output admission is closed for this generation",
335
+ });
336
+ });
337
+ it("schedules fallback Steer without a cutoff when final output admission already won", async () => {
338
+ const generation = await seedGeneration({ status: "closing", controlVersion: 1 });
339
+ const finalBody = "Committed before fallback";
340
+ const completion = await protocol.appendVisibleEventAndProject({
341
+ orgId: generation.orgId,
342
+ conversationId: generation.conversationId,
343
+ generationId: generation.id,
344
+ expectedAttemptEpoch: generation.attemptEpoch,
345
+ eventKind: "runtime_output",
346
+ payload: { resultKind: "message", body: finalBody },
347
+ bodyHash: hashChatGenerationBody(finalBody),
348
+ body: finalBody,
349
+ transcript: [],
350
+ chatTurnId: randomUUID(),
351
+ turnVariant: 0,
352
+ });
353
+ const controlActionId = randomUUID();
354
+ const queueItemId = randomUUID();
355
+ await db.insert(chatControlActions).values({
356
+ id: controlActionId,
357
+ orgId: generation.orgId,
358
+ expectedGenerationId: generation.id,
359
+ expectedAttemptEpoch: generation.attemptEpoch,
360
+ expectedControlVersion: 0,
361
+ appliedControlVersion: 1,
362
+ actionKind: "steer",
363
+ localDisposition: "pending",
364
+ providerDisposition: "sent",
365
+ providerSentAt: new Date(),
366
+ });
367
+ await db.insert(chatQueuedMessages).values({
368
+ id: queueItemId,
369
+ orgId: generation.orgId,
370
+ conversationId: generation.conversationId,
371
+ position: 1,
372
+ clientMutationId: randomUUID(),
373
+ status: "steer_pending",
374
+ deliveryIntent: "steer",
375
+ deliveryDisposition: "pending",
376
+ controlActionId,
377
+ expectedGenerationId: generation.id,
378
+ activeGenerationId: generation.id,
379
+ attemptEpoch: generation.attemptEpoch,
380
+ payload: { body: "Use the public API" },
381
+ });
382
+ const fallback = await protocol.beginSteerFallbackCutoff({
383
+ orgId: generation.orgId,
384
+ conversationId: generation.conversationId,
385
+ generationId: generation.id,
386
+ expectedAttemptEpoch: generation.attemptEpoch,
387
+ controlActionId,
388
+ queueItemId,
389
+ requestedRenderSeq: completion.event.generationSeq,
390
+ requestedBodyHash: hashChatGenerationBody(finalBody),
391
+ });
392
+ expect(fallback).toMatchObject({
393
+ outcome: "completion_committed",
394
+ idempotent: false,
395
+ generation: { status: "closing", acceptedThroughSeq: null },
396
+ action: {
397
+ localDisposition: "continuation_pending",
398
+ providerDisposition: "not_sent",
399
+ acceptedThroughSeq: null,
400
+ frozenBodyHash: null,
401
+ lastError: "target_generation_completion_committed",
402
+ },
403
+ item: {
404
+ status: "continuation_pending",
405
+ deliveryDisposition: "continuation_pending",
406
+ reconciliationReason: "target_generation_completion_committed",
407
+ },
408
+ });
409
+ expect(fallback.outputCutoffEvent).toBeNull();
410
+ expect(fallback.continuationEvent).toMatchObject({
411
+ eventKind: "continuation_scheduled",
412
+ payload: {
413
+ reason: "target_generation_completion_committed",
414
+ completionEventId: completion.event.id,
415
+ },
416
+ });
417
+ const generationEvents = await db
418
+ .select()
419
+ .from(chatGenerationEvents)
420
+ .where(eq(chatGenerationEvents.generationId, generation.id));
421
+ expect(generationEvents.filter((event) => event.eventKind === "output_cutoff")).toHaveLength(0);
422
+ const [persistedMessage] = await db
423
+ .select()
424
+ .from(chatMessages)
425
+ .where(eq(chatMessages.id, completion.message.id));
426
+ expect(persistedMessage).toMatchObject({ body: finalBody, status: "streaming" });
427
+ const repeated = await protocol.beginSteerFallbackCutoff({
428
+ orgId: generation.orgId,
429
+ conversationId: generation.conversationId,
430
+ generationId: generation.id,
431
+ expectedAttemptEpoch: generation.attemptEpoch,
432
+ controlActionId,
433
+ queueItemId,
434
+ requestedRenderSeq: completion.event.generationSeq,
435
+ requestedBodyHash: hashChatGenerationBody(finalBody),
436
+ });
437
+ expect(repeated).toMatchObject({
438
+ outcome: "completion_committed",
439
+ idempotent: true,
440
+ continuationEvent: { id: fallback.continuationEvent?.id },
441
+ });
442
+ });
443
+ it("rewinds an atomically projected event beyond the client Stop checkpoint", async () => {
444
+ const generation = await seedGeneration();
445
+ const chatTurnId = randomUUID();
446
+ const first = await protocol.appendVisibleEventAndProject({
447
+ orgId: generation.orgId,
448
+ conversationId: generation.conversationId,
449
+ generationId: generation.id,
450
+ expectedAttemptEpoch: generation.attemptEpoch,
451
+ eventKind: "assistant_delta",
452
+ payload: { delta: "Visible prefix" },
453
+ bodyOffset: 0,
454
+ bodyLength: 14,
455
+ bodyHash: hashChatGenerationBody("Visible prefix"),
456
+ body: "Visible prefix",
457
+ transcript: [],
458
+ chatTurnId,
459
+ turnVariant: 0,
460
+ });
461
+ await protocol.appendVisibleEventAndProject({
462
+ orgId: generation.orgId,
463
+ conversationId: generation.conversationId,
464
+ generationId: generation.id,
465
+ expectedAttemptEpoch: generation.attemptEpoch,
466
+ eventKind: "assistant_delta",
467
+ payload: { delta: " hidden tail" },
468
+ messageId: first.message.id,
469
+ bodyOffset: 14,
470
+ bodyLength: 12,
471
+ bodyHash: hashChatGenerationBody("Visible prefix hidden tail"),
472
+ body: "Visible prefix hidden tail",
473
+ transcript: [],
474
+ chatTurnId,
475
+ turnVariant: 0,
476
+ });
477
+ await protocol.beginStopAction({
478
+ orgId: generation.orgId,
479
+ conversationId: generation.conversationId,
480
+ controlActionId: randomUUID(),
481
+ expectedGenerationId: generation.id,
482
+ expectedAttemptEpoch: generation.attemptEpoch,
483
+ expectedControlVersion: generation.controlVersion,
484
+ requestedRenderSeq: first.event.generationSeq,
485
+ requestedBodyHash: hashChatGenerationBody("Visible prefix"),
486
+ });
487
+ const [message] = await db
488
+ .select()
489
+ .from(chatMessages)
490
+ .where(eq(chatMessages.id, first.message.id));
491
+ expect(message).toMatchObject({
492
+ body: "Visible prefix",
493
+ status: "stopped",
494
+ });
495
+ await expect(protocol.appendVisibleEventAndProject({
496
+ orgId: generation.orgId,
497
+ conversationId: generation.conversationId,
498
+ generationId: generation.id,
499
+ expectedAttemptEpoch: generation.attemptEpoch,
500
+ eventKind: "runtime_output",
501
+ payload: { resultKind: "message", body: "Completed too late" },
502
+ messageId: first.message.id,
503
+ bodyHash: hashChatGenerationBody("Completed too late"),
504
+ body: "Completed too late",
505
+ transcript: [],
506
+ chatTurnId,
507
+ turnVariant: 0,
508
+ })).rejects.toMatchObject({
509
+ status: 409,
510
+ message: "Chat-visible output admission is closed for this generation",
511
+ });
512
+ });
513
+ it("freezes visible output before scheduling an unsupported-runtime Steer continuation", async () => {
514
+ const generation = await seedGeneration({ controlVersion: 1 });
515
+ const visible = await protocol.appendGenerationEvent({
516
+ orgId: generation.orgId,
517
+ conversationId: generation.conversationId,
518
+ generationId: generation.id,
519
+ attemptEpoch: generation.attemptEpoch,
520
+ expectedOwnerToken: generation.controlOwnerToken,
521
+ admission: "visible",
522
+ eventKind: "assistant_delta",
523
+ payload: { delta: "Before fallback" },
524
+ bodyOffset: 0,
525
+ bodyLength: 15,
526
+ bodyHash: hashChatGenerationBody("Before fallback"),
527
+ });
528
+ const controlActionId = randomUUID();
529
+ const queueItemId = randomUUID();
530
+ await db.insert(chatControlActions).values({
531
+ id: controlActionId,
532
+ orgId: generation.orgId,
533
+ expectedGenerationId: generation.id,
534
+ expectedAttemptEpoch: generation.attemptEpoch,
535
+ expectedControlVersion: 0,
536
+ appliedControlVersion: 1,
537
+ actionKind: "steer",
538
+ localDisposition: "pending",
539
+ providerDisposition: "sent",
540
+ providerSentAt: new Date(),
541
+ });
542
+ await db.insert(chatQueuedMessages).values({
543
+ id: queueItemId,
544
+ orgId: generation.orgId,
545
+ conversationId: generation.conversationId,
546
+ position: 1,
547
+ clientMutationId: randomUUID(),
548
+ status: "steer_pending",
549
+ deliveryIntent: "steer",
550
+ deliveryDisposition: "pending",
551
+ controlActionId,
552
+ expectedGenerationId: generation.id,
553
+ activeGenerationId: generation.id,
554
+ attemptEpoch: generation.attemptEpoch,
555
+ payload: { body: "Use the public API" },
556
+ });
557
+ const fallback = await protocol.beginSteerFallbackCutoff({
558
+ orgId: generation.orgId,
559
+ conversationId: generation.conversationId,
560
+ generationId: generation.id,
561
+ expectedAttemptEpoch: generation.attemptEpoch,
562
+ controlActionId,
563
+ queueItemId,
564
+ requestedRenderSeq: visible.event.generationSeq,
565
+ requestedBodyHash: hashChatGenerationBody("Before fallback"),
566
+ });
567
+ expect(fallback.idempotent).toBe(false);
568
+ expect(fallback.generation).toMatchObject({
569
+ status: "stop_requested",
570
+ terminalReason: "steer_fallback",
571
+ acceptedThroughSeq: visible.event.generationSeq,
572
+ frozenBodyHash: hashChatGenerationBody("Before fallback"),
573
+ });
574
+ expect(fallback.action).toMatchObject({
575
+ localDisposition: "continuation_pending",
576
+ providerDisposition: "not_sent",
577
+ acceptedThroughSeq: visible.event.generationSeq,
578
+ frozenBodyHash: hashChatGenerationBody("Before fallback"),
579
+ });
580
+ expect(fallback.item).toMatchObject({
581
+ status: "continuation_pending",
582
+ deliveryDisposition: "continuation_pending",
583
+ });
584
+ expect(fallback.outputCutoffEvent?.eventKind).toBe("output_cutoff");
585
+ expect(fallback.continuationEvent?.eventKind).toBe("continuation_scheduled");
586
+ await expect(protocol.appendGenerationEvent({
587
+ orgId: generation.orgId,
588
+ conversationId: generation.conversationId,
589
+ generationId: generation.id,
590
+ attemptEpoch: generation.attemptEpoch,
591
+ admission: "visible",
592
+ eventKind: "assistant_delta",
593
+ payload: { delta: " after" },
594
+ bodyHash: hashChatGenerationBody("Before fallback after"),
595
+ })).rejects.toMatchObject({ status: 409 });
596
+ const repeated = await protocol.beginSteerFallbackCutoff({
597
+ orgId: generation.orgId,
598
+ conversationId: generation.conversationId,
599
+ generationId: generation.id,
600
+ expectedAttemptEpoch: generation.attemptEpoch,
601
+ controlActionId,
602
+ queueItemId,
603
+ requestedRenderSeq: visible.event.generationSeq,
604
+ requestedBodyHash: hashChatGenerationBody("Before fallback"),
605
+ });
606
+ expect(repeated.idempotent).toBe(true);
607
+ const terminal = await protocol.recordRuntimeTerminal({
608
+ orgId: generation.orgId,
609
+ conversationId: generation.conversationId,
610
+ generationId: generation.id,
611
+ expectedAttemptEpoch: generation.attemptEpoch,
612
+ finalStatus: "completed",
613
+ terminalReason: "completed",
614
+ });
615
+ expect(terminal.outbox.payload).toMatchObject({
616
+ finalStatus: "stopped",
617
+ terminalReason: "steer_fallback",
618
+ controlActionId,
619
+ controlActionKind: "steer",
620
+ });
621
+ });
622
+ it("reuses a Stop cutoff when Stop wins after Steer provider admission", async () => {
623
+ const generation = await seedGeneration({ controlVersion: 1 });
624
+ const steerActionId = randomUUID();
625
+ const queueItemId = randomUUID();
626
+ await db.insert(chatControlActions).values({
627
+ id: steerActionId,
628
+ orgId: generation.orgId,
629
+ expectedGenerationId: generation.id,
630
+ expectedAttemptEpoch: generation.attemptEpoch,
631
+ expectedControlVersion: 0,
632
+ appliedControlVersion: 1,
633
+ actionKind: "steer",
634
+ localDisposition: "pending",
635
+ providerDisposition: "sent",
636
+ });
637
+ await db.insert(chatQueuedMessages).values({
638
+ id: queueItemId,
639
+ orgId: generation.orgId,
640
+ conversationId: generation.conversationId,
641
+ position: 1,
642
+ clientMutationId: randomUUID(),
643
+ status: "steer_pending",
644
+ deliveryIntent: "steer",
645
+ deliveryDisposition: "pending",
646
+ controlActionId: steerActionId,
647
+ expectedGenerationId: generation.id,
648
+ activeGenerationId: generation.id,
649
+ attemptEpoch: generation.attemptEpoch,
650
+ payload: { body: "Continue with this" },
651
+ });
652
+ const stop = await protocol.beginStopAction({
653
+ orgId: generation.orgId,
654
+ conversationId: generation.conversationId,
655
+ controlActionId: randomUUID(),
656
+ expectedGenerationId: generation.id,
657
+ expectedAttemptEpoch: generation.attemptEpoch,
658
+ expectedControlVersion: 1,
659
+ requestedRenderSeq: 0,
660
+ requestedBodyHash: hashChatGenerationBody(""),
661
+ });
662
+ const fallback = await protocol.beginSteerFallbackCutoff({
663
+ orgId: generation.orgId,
664
+ conversationId: generation.conversationId,
665
+ generationId: generation.id,
666
+ expectedAttemptEpoch: generation.attemptEpoch,
667
+ controlActionId: steerActionId,
668
+ queueItemId,
669
+ requestedRenderSeq: 0,
670
+ requestedBodyHash: hashChatGenerationBody(""),
671
+ });
672
+ expect(fallback.generation).toMatchObject({
673
+ terminalReason: "operator_stop",
674
+ acceptedThroughSeq: stop.action.acceptedThroughSeq,
675
+ frozenBodyHash: stop.action.frozenBodyHash,
676
+ });
677
+ expect(fallback.outputCutoffEvent).toBeNull();
678
+ expect(fallback.action.localDisposition).toBe("continuation_pending");
679
+ });
680
+ it("linearizes runtime terminal evidence on either side of Stop", async () => {
681
+ const terminalFirst = await seedGeneration();
682
+ const terminal = await protocol.recordRuntimeTerminal({
683
+ orgId: terminalFirst.orgId,
684
+ conversationId: terminalFirst.conversationId,
685
+ generationId: terminalFirst.id,
686
+ expectedAttemptEpoch: terminalFirst.attemptEpoch,
687
+ expectedOwnerToken: terminalFirst.controlOwnerToken,
688
+ finalStatus: "completed",
689
+ terminalReason: "completed",
690
+ });
691
+ await expect(protocol.beginStopAction({
692
+ orgId: terminalFirst.orgId,
693
+ conversationId: terminalFirst.conversationId,
694
+ controlActionId: randomUUID(),
695
+ expectedGenerationId: terminalFirst.id,
696
+ expectedAttemptEpoch: terminalFirst.attemptEpoch,
697
+ expectedControlVersion: terminalFirst.controlVersion,
698
+ requestedRenderSeq: 0,
699
+ requestedBodyHash: hashChatGenerationBody(""),
700
+ })).rejects.toMatchObject({ status: 409 });
701
+ const terminalClaim = await protocol.claimTerminalProjection({ workerId: "terminal-first", leaseMs: 1_000 });
702
+ const terminalProjected = await protocol.completeTerminalProjection({
703
+ outboxId: terminalClaim.id,
704
+ claimToken: terminalClaim.claimToken,
705
+ claimEpoch: terminalClaim.claimEpoch,
706
+ });
707
+ expect(terminalProjected?.generation.status).toBe("completed");
708
+ const stopFirst = await seedGeneration();
709
+ const stop = await protocol.beginStopAction({
710
+ orgId: stopFirst.orgId,
711
+ conversationId: stopFirst.conversationId,
712
+ controlActionId: randomUUID(),
713
+ expectedGenerationId: stopFirst.id,
714
+ expectedAttemptEpoch: stopFirst.attemptEpoch,
715
+ expectedControlVersion: stopFirst.controlVersion,
716
+ requestedRenderSeq: 0,
717
+ requestedBodyHash: hashChatGenerationBody(""),
718
+ });
719
+ const stoppedTerminal = await protocol.recordRuntimeTerminal({
720
+ orgId: stopFirst.orgId,
721
+ conversationId: stopFirst.conversationId,
722
+ generationId: stopFirst.id,
723
+ expectedAttemptEpoch: stopFirst.attemptEpoch,
724
+ expectedOwnerToken: stopFirst.controlOwnerToken,
725
+ finalStatus: "completed",
726
+ terminalReason: "completed",
727
+ });
728
+ expect(stoppedTerminal.outbox).toMatchObject({
729
+ expectedControlVersion: stop.generation.controlVersion,
730
+ payload: expect.objectContaining({ finalStatus: "stopped", terminalReason: "operator_stop" }),
731
+ });
732
+ const stoppedClaim = await protocol.claimTerminalProjection({ workerId: "stop-first", leaseMs: 1_000 });
733
+ const stoppedProjected = await protocol.completeTerminalProjection({
734
+ outboxId: stoppedClaim.id,
735
+ claimToken: stoppedClaim.claimToken,
736
+ claimEpoch: stoppedClaim.claimEpoch,
737
+ controlDisposition: "stopped",
738
+ });
739
+ expect(stoppedProjected?.generation.status).toBe("stopped");
740
+ });
741
+ it("never replays Steer when terminal projection cannot prove provider rejection", async () => {
742
+ const generation = await seedGeneration({ controlVersion: 1 });
743
+ const controlActionId = randomUUID();
744
+ const queueItemId = randomUUID();
745
+ await db.insert(chatControlActions).values({
746
+ id: controlActionId,
747
+ orgId: generation.orgId,
748
+ expectedGenerationId: generation.id,
749
+ expectedAttemptEpoch: generation.attemptEpoch,
750
+ expectedControlVersion: 0,
751
+ appliedControlVersion: 1,
752
+ actionKind: "steer",
753
+ localDisposition: "pending",
754
+ providerDisposition: "sent",
755
+ });
756
+ await db.insert(chatQueuedMessages).values({
757
+ id: queueItemId,
758
+ orgId: generation.orgId,
759
+ conversationId: generation.conversationId,
760
+ position: 1,
761
+ status: "steer_pending",
762
+ clientMutationId: randomUUID(),
763
+ payload: { body: "Do not duplicate this feedback" },
764
+ deliveryIntent: "steer",
765
+ deliveryDisposition: "pending",
766
+ controlActionId,
767
+ expectedGenerationId: generation.id,
768
+ activeGenerationId: generation.id,
769
+ attemptEpoch: generation.attemptEpoch,
770
+ });
771
+ await protocol.recordRuntimeTerminal({
772
+ orgId: generation.orgId,
773
+ conversationId: generation.conversationId,
774
+ generationId: generation.id,
775
+ expectedAttemptEpoch: generation.attemptEpoch,
776
+ expectedOwnerToken: generation.controlOwnerToken,
777
+ finalStatus: "completed",
778
+ terminalReason: "completed",
779
+ });
780
+ const claim = await protocol.claimTerminalProjection({ workerId: "unknown-receipt", leaseMs: 1_000 });
781
+ await protocol.completeTerminalProjection({
782
+ outboxId: claim.id,
783
+ claimToken: claim.claimToken,
784
+ claimEpoch: claim.claimEpoch,
785
+ });
786
+ const [item] = await db.select().from(chatQueuedMessages).where(eq(chatQueuedMessages.id, queueItemId));
787
+ const [action] = await db.select().from(chatControlActions).where(eq(chatControlActions.id, controlActionId));
788
+ expect(item).toMatchObject({ status: "acceptance_unknown", deliveryDisposition: "acceptance_unknown" });
789
+ expect(action).toMatchObject({ localDisposition: "acceptance_unknown", providerDisposition: "sent" });
790
+ });
791
+ it("fences terminal projector retries by claim token and epoch", async () => {
792
+ const generation = await seedGeneration();
793
+ const emptyHash = hashChatGenerationBody("");
794
+ const controlActionId = randomUUID();
795
+ await protocol.beginStopAction({
796
+ orgId: generation.orgId,
797
+ conversationId: generation.conversationId,
798
+ controlActionId,
799
+ expectedGenerationId: generation.id,
800
+ expectedAttemptEpoch: generation.attemptEpoch,
801
+ expectedControlVersion: 0,
802
+ requestedRenderSeq: 0,
803
+ requestedBodyHash: emptyHash,
804
+ });
805
+ const terminal = await protocol.recordRuntimeTerminal({
806
+ orgId: generation.orgId,
807
+ conversationId: generation.conversationId,
808
+ generationId: generation.id,
809
+ expectedAttemptEpoch: generation.attemptEpoch,
810
+ expectedOwnerToken: generation.controlOwnerToken,
811
+ finalStatus: "stopped",
812
+ terminalReason: "operator_stop",
813
+ controlActionId,
814
+ now: new Date("2026-07-16T00:00:00.000Z"),
815
+ });
816
+ expect(terminal.outbox.status).toBe("pending");
817
+ const firstClaim = await protocol.claimTerminalProjection({
818
+ workerId: "projector-a",
819
+ leaseMs: 1_000,
820
+ now: new Date("2026-07-16T00:00:00.000Z"),
821
+ });
822
+ expect(firstClaim).toMatchObject({ status: "claimed", claimEpoch: 1, attemptCount: 1 });
823
+ expect(await protocol.getNextTerminalProjectionWakeAt()).toEqual(new Date("2026-07-16T00:00:01.000Z"));
824
+ const retryAt = new Date("2026-07-16T00:00:02.000Z");
825
+ const retry = await protocol.retryTerminalProjection({
826
+ outboxId: firstClaim.id,
827
+ claimToken: firstClaim.claimToken,
828
+ claimEpoch: firstClaim.claimEpoch,
829
+ error: "projection unavailable",
830
+ retryAt,
831
+ maxAttempts: 3,
832
+ now: new Date("2026-07-16T00:00:00.500Z"),
833
+ });
834
+ expect(retry?.status).toBe("retry_wait");
835
+ expect(await protocol.getNextTerminalProjectionWakeAt()).toEqual(retryAt);
836
+ const secondClaim = await protocol.claimTerminalProjection({
837
+ workerId: "projector-b",
838
+ leaseMs: 1_000,
839
+ now: retryAt,
840
+ });
841
+ expect(secondClaim).toMatchObject({ status: "claimed", claimEpoch: 2, attemptCount: 2 });
842
+ expect(await protocol.completeTerminalProjection({
843
+ outboxId: firstClaim.id,
844
+ claimToken: firstClaim.claimToken,
845
+ claimEpoch: firstClaim.claimEpoch,
846
+ now: retryAt,
847
+ })).toBeNull();
848
+ let projected = false;
849
+ const completed = await protocol.completeTerminalProjection({
850
+ outboxId: secondClaim.id,
851
+ claimToken: secondClaim.claimToken,
852
+ claimEpoch: secondClaim.claimEpoch,
853
+ controlDisposition: "stopped",
854
+ now: new Date("2026-07-16T00:00:02.500Z"),
855
+ project: async () => {
856
+ projected = true;
857
+ },
858
+ });
859
+ expect(projected).toBe(true);
860
+ expect(completed?.outbox.status).toBe("projected");
861
+ expect(completed?.generation.status).toBe("stopped");
862
+ const [action] = await db.select().from(chatControlActions);
863
+ expect(action?.localDisposition).toBe("stopped");
864
+ });
865
+ it("releases generation ownership as an actionable failure when terminal projection exhausts", async () => {
866
+ const generation = await seedGeneration();
867
+ const controlActionId = randomUUID();
868
+ await protocol.beginStopAction({
869
+ orgId: generation.orgId,
870
+ conversationId: generation.conversationId,
871
+ controlActionId,
872
+ expectedGenerationId: generation.id,
873
+ expectedAttemptEpoch: generation.attemptEpoch,
874
+ expectedControlVersion: generation.controlVersion,
875
+ requestedRenderSeq: 0,
876
+ requestedBodyHash: hashChatGenerationBody(""),
877
+ });
878
+ const terminal = await protocol.recordRuntimeTerminal({
879
+ orgId: generation.orgId,
880
+ conversationId: generation.conversationId,
881
+ generationId: generation.id,
882
+ expectedAttemptEpoch: generation.attemptEpoch,
883
+ expectedOwnerToken: generation.controlOwnerToken,
884
+ finalStatus: "stopped",
885
+ terminalReason: "operator_stop",
886
+ controlActionId,
887
+ now: new Date("2026-07-16T01:00:00.000Z"),
888
+ });
889
+ expect(terminal.generation.status).toBe("stopping");
890
+ const claim = await protocol.claimTerminalProjection({
891
+ workerId: "exhausting-projector",
892
+ leaseMs: 1_000,
893
+ now: new Date("2026-07-16T01:00:00.000Z"),
894
+ });
895
+ const exhaustedAt = new Date("2026-07-16T01:00:00.500Z");
896
+ const exhausted = await protocol.retryTerminalProjection({
897
+ outboxId: claim.id,
898
+ claimToken: claim.claimToken,
899
+ claimEpoch: claim.claimEpoch,
900
+ error: "assistant projection failed",
901
+ retryAt: new Date("2026-07-16T01:00:02.000Z"),
902
+ maxAttempts: 1,
903
+ now: exhaustedAt,
904
+ });
905
+ expect(exhausted).toMatchObject({
906
+ status: "failed_actionable",
907
+ lastError: "assistant projection failed",
908
+ projectedAt: null,
909
+ payload: expect.objectContaining({
910
+ finalStatus: "stopped",
911
+ terminalReason: "operator_stop",
912
+ }),
913
+ });
914
+ const [failedGeneration] = await db
915
+ .select()
916
+ .from(chatGenerations)
917
+ .where(eq(chatGenerations.id, generation.id));
918
+ expect(failedGeneration).toMatchObject({
919
+ status: "failed",
920
+ terminalReason: "terminal_projection_failed_actionable",
921
+ controlState: "terminal",
922
+ controlOwnerToken: null,
923
+ controlLeaseExpiresAt: null,
924
+ completedAt: exhaustedAt,
925
+ });
926
+ const [failedAction] = await db
927
+ .select()
928
+ .from(chatControlActions)
929
+ .where(eq(chatControlActions.id, controlActionId));
930
+ expect(failedAction).toMatchObject({
931
+ localDisposition: "failed_actionable",
932
+ lastError: "assistant projection failed",
933
+ resolvedAt: exhaustedAt,
934
+ });
935
+ expect(await protocol.getNextTerminalProjectionWakeAt()).toBeNull();
936
+ expect(await protocol.completeTerminalProjection({
937
+ outboxId: claim.id,
938
+ claimToken: claim.claimToken,
939
+ claimEpoch: claim.claimEpoch,
940
+ now: exhaustedAt,
941
+ })).toBeNull();
942
+ const events = await db
943
+ .select()
944
+ .from(chatGenerationEvents)
945
+ .where(eq(chatGenerationEvents.generationId, generation.id))
946
+ .orderBy(asc(chatGenerationEvents.generationSeq));
947
+ expect(events.map(({ eventKind }) => eventKind)).toEqual([
948
+ "stop_requested",
949
+ "output_cutoff",
950
+ "runtime_terminal",
951
+ ]);
952
+ const [nextGeneration] = await db
953
+ .insert(chatGenerations)
954
+ .values({
955
+ orgId: generation.orgId,
956
+ conversationId: generation.conversationId,
957
+ status: "active",
958
+ })
959
+ .returning();
960
+ expect(nextGeneration?.status).toBe("active");
961
+ });
962
+ it("releases a closing generation when terminal projection exhausts", async () => {
963
+ const generation = await seedGeneration();
964
+ const terminal = await protocol.recordRuntimeTerminal({
965
+ orgId: generation.orgId,
966
+ conversationId: generation.conversationId,
967
+ generationId: generation.id,
968
+ expectedAttemptEpoch: generation.attemptEpoch,
969
+ expectedOwnerToken: generation.controlOwnerToken,
970
+ finalStatus: "completed",
971
+ terminalReason: "completed",
972
+ now: new Date("2026-07-16T02:00:00.000Z"),
973
+ });
974
+ expect(terminal.generation.status).toBe("closing");
975
+ const claim = await protocol.claimTerminalProjection({
976
+ workerId: "closing-projector",
977
+ leaseMs: 1_000,
978
+ now: new Date("2026-07-16T02:00:00.000Z"),
979
+ });
980
+ await protocol.retryTerminalProjection({
981
+ outboxId: claim.id,
982
+ claimToken: claim.claimToken,
983
+ claimEpoch: claim.claimEpoch,
984
+ error: "run projection failed",
985
+ retryAt: new Date("2026-07-16T02:00:02.000Z"),
986
+ maxAttempts: 1,
987
+ now: new Date("2026-07-16T02:00:00.500Z"),
988
+ });
989
+ const [failedGeneration] = await db
990
+ .select()
991
+ .from(chatGenerations)
992
+ .where(eq(chatGenerations.id, generation.id));
993
+ expect(failedGeneration).toMatchObject({
994
+ status: "failed",
995
+ terminalReason: "terminal_projection_failed_actionable",
996
+ controlState: "terminal",
997
+ });
998
+ const [nextGeneration] = await db
999
+ .insert(chatGenerations)
1000
+ .values({
1001
+ orgId: generation.orgId,
1002
+ conversationId: generation.conversationId,
1003
+ status: "active",
1004
+ })
1005
+ .returning();
1006
+ expect(nextGeneration?.status).toBe("active");
1007
+ });
1008
+ it("reconciles generation-linked Steers when terminal projection exhausts", async () => {
1009
+ const generation = await seedGeneration();
1010
+ const states = [
1011
+ {
1012
+ itemId: randomUUID(),
1013
+ actionId: randomUUID(),
1014
+ queueStatus: "accepted_current",
1015
+ localDisposition: "accepted_current",
1016
+ providerDisposition: "acknowledged",
1017
+ },
1018
+ {
1019
+ itemId: randomUUID(),
1020
+ actionId: randomUUID(),
1021
+ queueStatus: "steer_pending",
1022
+ localDisposition: "pending",
1023
+ providerDisposition: "sent",
1024
+ },
1025
+ {
1026
+ itemId: randomUUID(),
1027
+ actionId: randomUUID(),
1028
+ queueStatus: "continuation_pending",
1029
+ localDisposition: "continuation_pending",
1030
+ providerDisposition: "not_sent",
1031
+ },
1032
+ ];
1033
+ await db.insert(chatControlActions).values(states.map((state) => ({
1034
+ id: state.actionId,
1035
+ orgId: generation.orgId,
1036
+ expectedGenerationId: generation.id,
1037
+ expectedAttemptEpoch: generation.attemptEpoch,
1038
+ expectedControlVersion: generation.controlVersion,
1039
+ actionKind: "steer",
1040
+ localDisposition: state.localDisposition,
1041
+ providerDisposition: state.providerDisposition,
1042
+ })));
1043
+ await db.insert(chatQueuedMessages).values(states.map((state, index) => ({
1044
+ id: state.itemId,
1045
+ orgId: generation.orgId,
1046
+ conversationId: generation.conversationId,
1047
+ position: index + 1,
1048
+ status: state.queueStatus,
1049
+ clientMutationId: randomUUID(),
1050
+ payload: { body: `Steer ${index + 1}` },
1051
+ deliveryIntent: "steer",
1052
+ deliveryDisposition: state.localDisposition,
1053
+ controlActionId: state.actionId,
1054
+ expectedGenerationId: generation.id,
1055
+ activeGenerationId: generation.id,
1056
+ attemptEpoch: generation.attemptEpoch,
1057
+ })));
1058
+ await protocol.recordRuntimeTerminal({
1059
+ orgId: generation.orgId,
1060
+ conversationId: generation.conversationId,
1061
+ generationId: generation.id,
1062
+ expectedAttemptEpoch: generation.attemptEpoch,
1063
+ expectedOwnerToken: generation.controlOwnerToken,
1064
+ finalStatus: "completed",
1065
+ terminalReason: "completed",
1066
+ now: new Date("2026-07-16T03:00:00.000Z"),
1067
+ });
1068
+ const claim = await protocol.claimTerminalProjection({
1069
+ workerId: "steer-reconciliation-projector",
1070
+ leaseMs: 1_000,
1071
+ now: new Date("2026-07-16T03:00:00.000Z"),
1072
+ });
1073
+ await protocol.retryTerminalProjection({
1074
+ outboxId: claim.id,
1075
+ claimToken: claim.claimToken,
1076
+ claimEpoch: claim.claimEpoch,
1077
+ error: "terminal projection exhausted",
1078
+ retryAt: new Date("2026-07-16T03:00:02.000Z"),
1079
+ maxAttempts: 1,
1080
+ now: new Date("2026-07-16T03:00:00.500Z"),
1081
+ });
1082
+ const items = await db
1083
+ .select()
1084
+ .from(chatQueuedMessages)
1085
+ .orderBy(asc(chatQueuedMessages.position));
1086
+ expect(items.map(({ status, deliveryDisposition, reconciliationReason }) => ({
1087
+ status,
1088
+ deliveryDisposition,
1089
+ reconciliationReason,
1090
+ }))).toEqual([
1091
+ { status: "delivered", deliveryDisposition: "delivered", reconciliationReason: null },
1092
+ {
1093
+ status: "acceptance_unknown",
1094
+ deliveryDisposition: "acceptance_unknown",
1095
+ reconciliationReason: "terminal_projection_failed_provider_receipt_unknown",
1096
+ },
1097
+ {
1098
+ status: "failed_actionable",
1099
+ deliveryDisposition: "failed_actionable",
1100
+ reconciliationReason: "terminal_projection_failed_actionable",
1101
+ },
1102
+ ]);
1103
+ const actions = await db
1104
+ .select()
1105
+ .from(chatControlActions)
1106
+ .orderBy(asc(chatControlActions.requestedAt), asc(chatControlActions.id));
1107
+ const actionById = new Map(actions.map((action) => [action.id, action]));
1108
+ expect(actionById.get(states[0].actionId)?.localDisposition).toBe("delivered");
1109
+ expect(actionById.get(states[1].actionId)?.localDisposition).toBe("acceptance_unknown");
1110
+ expect(actionById.get(states[2].actionId)?.localDisposition).toBe("failed_actionable");
1111
+ });
1112
+ it("recovers stale control owners without claiming runtime termination", async () => {
1113
+ const orgId = randomUUID();
1114
+ const expired = await seedGeneration({
1115
+ orgId,
1116
+ leaseExpiresAt: new Date("2026-07-15T23:59:00.000Z"),
1117
+ });
1118
+ const future = await seedGeneration({
1119
+ orgId,
1120
+ leaseExpiresAt: new Date("2026-07-16T01:00:00.000Z"),
1121
+ });
1122
+ const recovered = await protocol.recoverStaleControlOwners({
1123
+ now: new Date("2026-07-16T00:00:00.000Z"),
1124
+ });
1125
+ expect(recovered).toHaveLength(1);
1126
+ expect(recovered[0]?.generation).toMatchObject({
1127
+ id: expired.id,
1128
+ status: "control_lost",
1129
+ controlState: "control_lost",
1130
+ controlOwnerToken: null,
1131
+ runtimeTerminalAt: null,
1132
+ });
1133
+ expect(recovered[0]?.outbox.payload).toMatchObject({
1134
+ finalStatus: "control_lost",
1135
+ runtimeTerminationVerified: false,
1136
+ });
1137
+ const [futureAfter] = await db
1138
+ .select()
1139
+ .from(chatGenerations)
1140
+ .where(eq(chatGenerations.id, future.id));
1141
+ expect(futureAfter?.status).toBe("running");
1142
+ const startupRecovered = await protocol.recoverStaleControlOwners({
1143
+ now: new Date("2026-07-16T00:00:01.000Z"),
1144
+ assumeAllOwnersStale: true,
1145
+ });
1146
+ expect(startupRecovered.map(({ generation }) => generation.id)).toContain(future.id);
1147
+ const events = await db
1148
+ .select()
1149
+ .from(chatGenerationEvents)
1150
+ .where(eq(chatGenerationEvents.generationId, future.id))
1151
+ .orderBy(asc(chatGenerationEvents.generationSeq));
1152
+ expect(events).toHaveLength(1);
1153
+ expect(events[0]?.eventKind).toBe("terminal_projection_requested");
1154
+ });
1155
+ });
1156
+ //# sourceMappingURL=chat-generation-protocol.test.js.map