@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
@@ -1,13 +1,15 @@
1
1
  import { hasSessionCompactionThresholds } from "@rudderhq/agent-runtime-utils";
2
2
  import { agentRuntimeState, agents, agentTaskSessions, agentWakeupRequests, heartbeatRunEvents, heartbeatRuns, issues, organizations } from "@rudderhq/db";
3
- import { summarizeTokenUsage } from "@rudderhq/shared";
4
- import { and, asc, desc, eq, gt, gte, inArray, lte, sql } from "drizzle-orm";
3
+ import { summarizeTokenUsage, toHeartbeatRun } from "@rudderhq/shared";
4
+ import { and, asc, desc, eq, gt, gte, inArray, lte, or, sql } from "drizzle-orm";
5
+ import { randomUUID } from "node:crypto";
5
6
  import { runningProcesses } from "../../agent-runtimes/index.js";
6
- import { parseObject } from "../../agent-runtimes/utils.js";
7
+ import { killChildProcessTree, parseObject } from "../../agent-runtimes/utils.js";
7
8
  import { notFound } from "../../errors.js";
8
9
  import { redactCurrentUserText, redactCurrentUserValue } from "../../log-redaction.js";
9
10
  import { logger } from "../../middleware/logger.js";
10
11
  import { agentRunContextService } from "../agent-run-context.js";
12
+ import { publishAutomationRunOutputToChat } from "../automation-chat-output.js";
11
13
  import { budgetService } from "../budgets.js";
12
14
  import { costService } from "../costs.js";
13
15
  import { runWorkspaceService } from "../execution-workspaces.js";
@@ -15,6 +17,7 @@ import { summarizeHeartbeatRunResultJson } from "../heartbeat-run-summary.js";
15
17
  import { instanceSettingsService } from "../instance-settings.js";
16
18
  import { issueService } from "../issues.js";
17
19
  import { publishLiveEvent } from "../live-events.js";
20
+ import { appendHeartbeatRunEvent } from "../run-events.js";
18
21
  import { getRunLogStore } from "../run-log-store.js";
19
22
  import { workspaceOperationService } from "../workspace-operations.js";
20
23
  export { prioritizeProjectWorkspaceCandidatesForRun } from "../agent-run-context.js";
@@ -26,9 +29,16 @@ import { createHeartbeatExecuteHandlers } from "./heartbeat.execute.js";
26
29
  import { createHeartbeatMiscHandlers } from "./heartbeat.misc.js";
27
30
  import { createHeartbeatRecoveryHandlers } from "./heartbeat.recovery.js";
28
31
  import { createHeartbeatReleaseHandlers } from "./heartbeat.release.js";
32
+ import { checkpointHeartbeatRunTerminalEffect, claimExpiredHeartbeatRunExecution, claimHeartbeatRunTerminalEffects, completeHeartbeatRunTerminalEffects, failHeartbeatRunTerminalEffect, markHeartbeatRunProcessExited, reconcileHeartbeatRunEvidence, reconcileHeartbeatRunTerminalEffectsIntent, releaseHeartbeatRunTerminalEffectsClaim, renewHeartbeatRunExecutionLease, renewHeartbeatRunTerminalEffectsClaim, RUN_EXECUTION_LEASE_MS, setWakeupStatusMonotonic, terminalEffectNames, transitionHeartbeatRunToTerminal, } from "./heartbeat.terminal.js";
29
33
  import { createHeartbeatWakeupHandlers } from "./heartbeat.wakeup.js";
30
34
  const DEFAULT_HEARTBEAT_RUN_TIMEOUT_MS = 12 * 60 * 60 * 1000;
31
35
  const DEFAULT_HEARTBEAT_RUN_INACTIVITY_TIMEOUT_MS = 30 * 60 * 1000;
36
+ const TERMINAL_EFFECT_CLAIM_RENEW_INTERVAL_MS = 60_000;
37
+ // heartbeatService is instantiated by routes and the scheduler. Execution
38
+ // ownership must therefore be shared across every service instance in this
39
+ // server process.
40
+ const activeRunExecutions = new Set();
41
+ const runAbortControllers = new Map();
32
42
  function formatDurationMs(ms) {
33
43
  const totalSeconds = Math.max(0, Math.round(ms / 1000));
34
44
  const hours = Math.floor(totalSeconds / 3600);
@@ -40,7 +50,7 @@ function formatDurationMs(ms) {
40
50
  return `${minutes}m ${seconds}s`;
41
51
  return `${seconds}s`;
42
52
  }
43
- export function heartbeatService(db) {
53
+ export function heartbeatService(db, testHooks) {
44
54
  const instanceSettings = instanceSettingsService(db);
45
55
  const getCurrentUserRedactionOptions = async () => ({
46
56
  enabled: (await instanceSettings.getGeneral()).censorUsernameInLogs,
@@ -50,7 +60,6 @@ export function heartbeatService(db) {
50
60
  const issuesSvc = issueService(db);
51
61
  const executionWorkspacesSvc = runWorkspaceService(db);
52
62
  const workspaceOperationsSvc = workspaceOperationService(db);
53
- const activeRunExecutions = new Set();
54
63
  const budgetHooks = {
55
64
  cancelWorkForScope: (scope) => cancelBudgetScopeWork(scope),
56
65
  };
@@ -229,8 +238,7 @@ export function heartbeatService(db) {
229
238
  (codec.getDisplayId ? codec.getDisplayId(parsedParams) : null) ??
230
239
  readNonEmptyString(parsedParams?.sessionId));
231
240
  }
232
- const runtimeForRun = await getRuntimeState(agent.id);
233
- return runtimeForRun?.sessionId ?? null;
241
+ return null;
234
242
  }
235
243
  async function resolveExplicitResumeSessionOverride(agent, payload, taskKey) {
236
244
  const resumeFromRunId = readNonEmptyString(payload?.resumeFromRunId);
@@ -242,6 +250,8 @@ export function heartbeatService(db) {
242
250
  contextSnapshot: heartbeatRuns.contextSnapshot,
243
251
  sessionIdBefore: heartbeatRuns.sessionIdBefore,
244
252
  sessionIdAfter: heartbeatRuns.sessionIdAfter,
253
+ sessionParamsBeforeJson: heartbeatRuns.sessionParamsBeforeJson,
254
+ sessionParamsAfterJson: heartbeatRuns.sessionParamsAfterJson,
245
255
  })
246
256
  .from(heartbeatRuns)
247
257
  .where(and(eq(heartbeatRuns.id, resumeFromRunId), eq(heartbeatRuns.orgId, agent.orgId), eq(heartbeatRuns.agentId, agent.id)))
@@ -254,10 +264,21 @@ export function heartbeatService(db) {
254
264
  ? await getTaskSession(agent.orgId, agent.id, agent.agentRuntimeType, resumeTaskKey)
255
265
  : null;
256
266
  const sessionCodec = getAgentRuntimeSessionCodec(agent.agentRuntimeType);
267
+ const persistedSessionSuppression = heartbeatSessions.readSessionReuseSuppression(resumeContext);
268
+ const resumeRunHasSessionAfter = Boolean(readNonEmptyString(resumeRun.sessionIdAfter)) ||
269
+ Boolean(resumeRun.sessionParamsAfterJson && Object.keys(resumeRun.sessionParamsAfterJson).length > 0);
257
270
  const sessionOverride = buildExplicitResumeSessionOverride({
258
271
  resumeFromRunId,
259
272
  resumeRunSessionIdBefore: resumeRun.sessionIdBefore,
260
273
  resumeRunSessionIdAfter: resumeRun.sessionIdAfter,
274
+ resumeRunSessionParamsBefore: resumeRun.sessionParamsBeforeJson,
275
+ resumeRunSessionParamsAfter: resumeRun.sessionParamsAfterJson,
276
+ resumeRunSessionCleared: resumeRun.sessionIdAfter == null &&
277
+ resumeRun.sessionParamsAfterJson != null &&
278
+ Object.keys(resumeRun.sessionParamsAfterJson).length === 0,
279
+ resumeRunSessionSuppression: resumeRunHasSessionAfter ? null : persistedSessionSuppression,
280
+ resumeContextSessionParams: parseObject(resumeContext.resumeSessionParams),
281
+ resumeContextSessionDisplayId: readNonEmptyString(resumeContext.resumeSessionDisplayId),
261
282
  taskSession: resumeTaskSession,
262
283
  sessionCodec,
263
284
  });
@@ -270,38 +291,64 @@ export function heartbeatService(db) {
270
291
  taskId: readNonEmptyString(resumeContext.taskId) ?? readNonEmptyString(resumeContext.issueId),
271
292
  sessionDisplayId: sessionOverride.sessionDisplayId,
272
293
  sessionParams: sessionOverride.sessionParams,
294
+ sessionCleared: "sessionCleared" in sessionOverride && sessionOverride.sessionCleared === true,
295
+ sessionReuseSuppression: "sessionReuseSuppression" in sessionOverride
296
+ ? sessionOverride.sessionReuseSuppression
297
+ : null,
273
298
  };
274
299
  }
275
300
  async function upsertTaskSession(input) {
276
- const existing = await getTaskSession(input.orgId, input.agentId, input.agentRuntimeType, input.taskKey);
277
- if (existing) {
278
- return db
279
- .update(agentTaskSessions)
280
- .set({
301
+ return db.transaction(async (tx) => {
302
+ await tx.execute(sql `select pg_advisory_xact_lock(hashtext(${`task-session:${input.orgId}:${input.agentId}:${input.agentRuntimeType}:${input.taskKey}`}))`);
303
+ const existing = await tx
304
+ .select()
305
+ .from(agentTaskSessions)
306
+ .where(and(eq(agentTaskSessions.orgId, input.orgId), eq(agentTaskSessions.agentId, input.agentId), eq(agentTaskSessions.agentRuntimeType, input.agentRuntimeType), eq(agentTaskSessions.taskKey, input.taskKey)))
307
+ .then((rows) => rows[0] ?? null);
308
+ if (existing) {
309
+ if (existing.lastRunId && input.lastRunId && existing.lastRunId !== input.lastRunId) {
310
+ const runDates = await tx
311
+ .select({ id: heartbeatRuns.id, createdAt: heartbeatRuns.createdAt })
312
+ .from(heartbeatRuns)
313
+ .where(inArray(heartbeatRuns.id, [existing.lastRunId, input.lastRunId]));
314
+ const existingDate = runDates.find((row) => row.id === existing.lastRunId)?.createdAt;
315
+ const incomingDate = runDates.find((row) => row.id === input.lastRunId)?.createdAt;
316
+ if (existingDate
317
+ && incomingDate
318
+ && (existingDate.getTime() > incomingDate.getTime()
319
+ || (existingDate.getTime() === incomingDate.getTime()
320
+ && existing.lastRunId.localeCompare(input.lastRunId) > 0))) {
321
+ return existing;
322
+ }
323
+ }
324
+ return tx
325
+ .update(agentTaskSessions)
326
+ .set({
327
+ sessionParamsJson: input.sessionParamsJson,
328
+ sessionDisplayId: input.sessionDisplayId,
329
+ lastRunId: input.lastRunId,
330
+ lastError: input.lastError,
331
+ updatedAt: new Date(),
332
+ })
333
+ .where(eq(agentTaskSessions.id, existing.id))
334
+ .returning()
335
+ .then((rows) => rows[0] ?? null);
336
+ }
337
+ return tx
338
+ .insert(agentTaskSessions)
339
+ .values({
340
+ orgId: input.orgId,
341
+ agentId: input.agentId,
342
+ agentRuntimeType: input.agentRuntimeType,
343
+ taskKey: input.taskKey,
281
344
  sessionParamsJson: input.sessionParamsJson,
282
345
  sessionDisplayId: input.sessionDisplayId,
283
346
  lastRunId: input.lastRunId,
284
347
  lastError: input.lastError,
285
- updatedAt: new Date(),
286
348
  })
287
- .where(eq(agentTaskSessions.id, existing.id))
288
349
  .returning()
289
350
  .then((rows) => rows[0] ?? null);
290
- }
291
- return db
292
- .insert(agentTaskSessions)
293
- .values({
294
- orgId: input.orgId,
295
- agentId: input.agentId,
296
- agentRuntimeType: input.agentRuntimeType,
297
- taskKey: input.taskKey,
298
- sessionParamsJson: input.sessionParamsJson,
299
- sessionDisplayId: input.sessionDisplayId,
300
- lastRunId: input.lastRunId,
301
- lastError: input.lastError,
302
- })
303
- .returning()
304
- .then((rows) => rows[0] ?? null);
351
+ });
305
352
  }
306
353
  async function clearTaskSessions(orgId, agentId, opts) {
307
354
  const conditions = [
@@ -314,6 +361,27 @@ export function heartbeatService(db) {
314
361
  if (opts?.agentRuntimeType) {
315
362
  conditions.push(eq(agentTaskSessions.agentRuntimeType, opts.agentRuntimeType));
316
363
  }
364
+ if (opts?.sourceRunId) {
365
+ const sourceRun = await getRun(opts.sourceRunId);
366
+ if (sourceRun) {
367
+ conditions.push(sql `(
368
+ ${agentTaskSessions.lastRunId} is null
369
+ or ${agentTaskSessions.lastRunId} = ${opts.sourceRunId}
370
+ or not exists (
371
+ select 1
372
+ from heartbeat_runs previous_run
373
+ where previous_run.id = ${agentTaskSessions.lastRunId}
374
+ and (
375
+ previous_run.created_at > ${sourceRun.createdAt.toISOString()}::timestamptz
376
+ or (
377
+ previous_run.created_at = ${sourceRun.createdAt.toISOString()}::timestamptz
378
+ and previous_run.id > ${sourceRun.id}
379
+ )
380
+ )
381
+ )
382
+ )`);
383
+ }
384
+ }
317
385
  return db
318
386
  .delete(agentTaskSessions)
319
387
  .where(and(...conditions))
@@ -346,39 +414,61 @@ export function heartbeatService(db) {
346
414
  .returning()
347
415
  .then((rows) => rows[0]);
348
416
  }
417
+ function publishRunStatus(updated) {
418
+ publishLiveEvent({
419
+ orgId: updated.orgId,
420
+ type: "heartbeat.run.status",
421
+ payload: {
422
+ runId: updated.id,
423
+ agentId: updated.agentId,
424
+ status: updated.status,
425
+ invocationSource: updated.invocationSource,
426
+ triggerDetail: updated.triggerDetail,
427
+ error: updated.error ?? null,
428
+ errorCode: updated.errorCode ?? null,
429
+ startedAt: updated.startedAt ? new Date(updated.startedAt).toISOString() : null,
430
+ finishedAt: updated.finishedAt ? new Date(updated.finishedAt).toISOString() : null,
431
+ },
432
+ });
433
+ }
349
434
  async function setRunStatus(runId, status, patch) {
435
+ if (["succeeded", "failed", "cancelled", "timed_out"].includes(status)) {
436
+ throw new Error("Terminal heartbeat run updates must use transitionRunToTerminal");
437
+ }
350
438
  const updated = await db
351
439
  .update(heartbeatRuns)
352
440
  .set({ status, ...patch, updatedAt: new Date() })
353
441
  .where(eq(heartbeatRuns.id, runId))
354
442
  .returning()
355
443
  .then((rows) => rows[0] ?? null);
356
- if (updated) {
357
- publishLiveEvent({
358
- orgId: updated.orgId,
359
- type: "heartbeat.run.status",
360
- payload: {
361
- runId: updated.id,
362
- agentId: updated.agentId,
363
- status: updated.status,
364
- invocationSource: updated.invocationSource,
365
- triggerDetail: updated.triggerDetail,
366
- error: updated.error ?? null,
367
- errorCode: updated.errorCode ?? null,
368
- startedAt: updated.startedAt ? new Date(updated.startedAt).toISOString() : null,
369
- finishedAt: updated.finishedAt ? new Date(updated.finishedAt).toISOString() : null,
370
- },
371
- });
372
- }
444
+ if (updated)
445
+ publishRunStatus(updated);
446
+ return updated;
447
+ }
448
+ async function transitionRunToTerminal(runId, status, patch, opts) {
449
+ const updated = await transitionHeartbeatRunToTerminal(db, {
450
+ runId,
451
+ status,
452
+ patch,
453
+ expectedStatuses: opts?.expectedStatuses,
454
+ activityWatermark: opts?.activityWatermark,
455
+ terminalEffectsPending: opts?.terminalEffectsPending,
456
+ terminalEffectsIntent: opts?.terminalEffectsIntent,
457
+ processExitedAt: opts?.processExitedAt,
458
+ expectedExecutionOwnerToken: opts?.expectedExecutionOwnerToken,
459
+ });
460
+ if (updated)
461
+ publishRunStatus(updated);
373
462
  return updated;
374
463
  }
464
+ async function reconcileRunEvidence(runId, patch) {
465
+ return reconcileHeartbeatRunEvidence(db, runId, patch);
466
+ }
467
+ async function reconcileTerminalEffectsIntent(runId, intent) {
468
+ return reconcileHeartbeatRunTerminalEffectsIntent(db, runId, intent);
469
+ }
375
470
  async function setWakeupStatus(wakeupRequestId, status, patch) {
376
- if (!wakeupRequestId)
377
- return;
378
- await db
379
- .update(agentWakeupRequests)
380
- .set({ status, ...patch, updatedAt: new Date() })
381
- .where(eq(agentWakeupRequests.id, wakeupRequestId));
471
+ return setWakeupStatusMonotonic(db, wakeupRequestId, status, patch);
382
472
  }
383
473
  async function updateWakeupRequestRecord(tx, wakeupRequestId, patch) {
384
474
  return tx
@@ -389,13 +479,21 @@ export function heartbeatService(db) {
389
479
  .then((rows) => rows[0] ?? null);
390
480
  }
391
481
  async function insertWakeupRequestRecord(tx, values) {
392
- return tx
482
+ const inserted = await tx
393
483
  .insert(agentWakeupRequests)
394
484
  .values(values)
485
+ .onConflictDoNothing()
395
486
  .returning()
396
487
  .then((rows) => rows[0] ?? null);
488
+ if (inserted || !values.idempotencyKey)
489
+ return inserted;
490
+ return tx
491
+ .select()
492
+ .from(agentWakeupRequests)
493
+ .where(and(eq(agentWakeupRequests.orgId, values.orgId), eq(agentWakeupRequests.agentId, values.agentId), eq(agentWakeupRequests.idempotencyKey, values.idempotencyKey)))
494
+ .then((rows) => rows[0] ?? null);
397
495
  }
398
- async function appendRunEvent(run, seq, event) {
496
+ async function appendRunEvent(run, event) {
399
497
  const currentUserRedactionOptions = await getCurrentUserRedactionOptions();
400
498
  const sanitizedMessage = event.message
401
499
  ? redactCurrentUserText(event.message, currentUserRedactionOptions)
@@ -403,17 +501,17 @@ export function heartbeatService(db) {
403
501
  const sanitizedPayload = event.payload
404
502
  ? redactCurrentUserValue(event.payload, currentUserRedactionOptions)
405
503
  : event.payload;
406
- await db.insert(heartbeatRunEvents).values({
504
+ const inserted = await appendHeartbeatRunEvent(db, {
407
505
  orgId: run.orgId,
408
506
  runId: run.id,
409
507
  agentId: run.agentId,
410
- seq,
411
508
  eventType: event.eventType,
412
509
  stream: event.stream,
413
510
  level: event.level,
414
511
  color: event.color,
415
512
  message: sanitizedMessage,
416
513
  payload: sanitizedPayload,
514
+ idempotencyKey: event.idempotencyKey ?? null,
417
515
  });
418
516
  publishLiveEvent({
419
517
  orgId: run.orgId,
@@ -421,7 +519,7 @@ export function heartbeatService(db) {
421
519
  payload: {
422
520
  runId: run.id,
423
521
  agentId: run.agentId,
424
- seq,
522
+ seq: inserted.seq,
425
523
  eventType: event.eventType,
426
524
  stream: event.stream ?? null,
427
525
  level: event.level ?? null,
@@ -431,13 +529,6 @@ export function heartbeatService(db) {
431
529
  },
432
530
  });
433
531
  }
434
- async function nextRunEventSeq(runId) {
435
- const [row] = await db
436
- .select({ maxSeq: sql `max(${heartbeatRunEvents.seq})` })
437
- .from(heartbeatRunEvents)
438
- .where(eq(heartbeatRunEvents.runId, runId));
439
- return Number(row?.maxSeq ?? 0) + 1;
440
- }
441
532
  async function persistRunProcessMetadata(runId, meta) {
442
533
  const startedAt = new Date(meta.startedAt);
443
534
  const updated = await db
@@ -447,7 +538,7 @@ export function heartbeatService(db) {
447
538
  processStartedAt: Number.isNaN(startedAt.getTime()) ? new Date() : startedAt,
448
539
  updatedAt: new Date(),
449
540
  })
450
- .where(eq(heartbeatRuns.id, runId))
541
+ .where(and(eq(heartbeatRuns.id, runId), eq(heartbeatRuns.status, "running")))
451
542
  .returning()
452
543
  .then((rows) => rows[0] ?? null);
453
544
  return updated;
@@ -465,7 +556,7 @@ export function heartbeatService(db) {
465
556
  .then((rows) => rows[0] ?? null);
466
557
  if (!updated)
467
558
  return null;
468
- await appendRunEvent(updated, await nextRunEventSeq(updated.id), {
559
+ await appendRunEvent(updated, {
469
560
  eventType: "lifecycle",
470
561
  stream: "system",
471
562
  level: "info",
@@ -473,11 +564,39 @@ export function heartbeatService(db) {
473
564
  });
474
565
  return updated;
475
566
  }
476
- async function countRunningRunsForAgent(agentId) {
567
+ async function terminateRunProcessAndWait(run, agentRuntimeType) {
568
+ if (run.processExitedAt)
569
+ return true;
570
+ runAbortControllers.get(run.id)?.abort();
571
+ const tracked = runningProcesses.get(run.id);
572
+ if (tracked) {
573
+ const pid = tracked.child.pid;
574
+ killChildProcessTree(tracked.child, false);
575
+ if (typeof pid !== "number" || pid <= 0)
576
+ return true;
577
+ const graceMs = Math.max(1, tracked.graceSec) * 1000;
578
+ if (await waitForProcessExit(pid, graceMs))
579
+ return true;
580
+ killChildProcessTree(tracked.child, true);
581
+ return waitForProcessExit(pid, ORPHANED_PROCESS_KILL_WAIT_MS);
582
+ }
583
+ if (isTrackedLocalChildProcessAdapter(agentRuntimeType) && run.processPid && isProcessAlive(run.processPid)) {
584
+ return !(await terminateOrphanedProcess(run.processPid)).stillAlive;
585
+ }
586
+ return !activeRunExecutions.has(run.id);
587
+ }
588
+ async function acknowledgeRunProcessExit(runId) {
589
+ return markHeartbeatRunProcessExited(db, runId);
590
+ }
591
+ async function renewRunExecutionLease(runId, ownerToken) {
592
+ return renewHeartbeatRunExecutionLease(db, runId, ownerToken);
593
+ }
594
+ async function countRunningRunsForAgent(agentId, excludeRunId) {
595
+ const pendingExecutionIds = [...activeRunExecutions].filter((runId) => runId !== excludeRunId);
477
596
  const [{ count }] = await db
478
597
  .select({ count: sql `count(*)` })
479
598
  .from(heartbeatRuns)
480
- .where(and(eq(heartbeatRuns.agentId, agentId), eq(heartbeatRuns.status, "running")));
599
+ .where(and(eq(heartbeatRuns.agentId, agentId), or(eq(heartbeatRuns.status, "running"), eq(heartbeatRuns.terminalEffectsPending, true), ...(pendingExecutionIds.length > 0 ? [inArray(heartbeatRuns.id, pendingExecutionIds)] : [])), ...(excludeRunId ? [sql `${heartbeatRuns.id} <> ${excludeRunId}`] : [])));
481
600
  return Number(count ?? 0);
482
601
  }
483
602
  async function claimQueuedRun(run) {
@@ -499,25 +618,26 @@ export function heartbeatService(db) {
499
618
  }
500
619
  }
501
620
  async function cancelQueuedRunDuringClaim(reason) {
502
- const cancelled = await setRunStatus(run.id, "cancelled", {
621
+ const cancelled = await transitionRunToTerminal(run.id, "cancelled", {
503
622
  finishedAt: new Date(),
504
623
  error: reason,
505
624
  errorCode: "cancelled",
506
- });
625
+ }, { expectedStatuses: ["queued"], processExitedAt: new Date() });
626
+ if (!cancelled)
627
+ return null;
507
628
  await setWakeupStatus(run.wakeupRequestId, "cancelled", {
508
629
  finishedAt: new Date(),
509
630
  error: reason,
510
631
  });
511
632
  if (cancelled) {
512
- await appendRunEvent(cancelled, 1, {
633
+ await appendRunEvent(cancelled, {
513
634
  eventType: "lifecycle",
514
635
  stream: "system",
515
636
  level: "warn",
516
637
  message: "run cancelled",
517
638
  });
518
- await releaseIssueExecutionAndPromote(cancelled);
639
+ await completeTerminalControlEffects(cancelled, { startNext: false });
519
640
  }
520
- await finalizeAgentStatus(run.agentId, "cancelled");
521
641
  return null;
522
642
  }
523
643
  const agent = await getAgent(run.agentId);
@@ -554,16 +674,82 @@ export function heartbeatService(db) {
554
674
  return await cancelQueuedRunDuringClaim(budgetBlock.reason);
555
675
  }
556
676
  const claimedAt = new Date();
557
- const claimed = await db
558
- .update(heartbeatRuns)
559
- .set({
560
- status: "running",
561
- startedAt: run.startedAt ?? claimedAt,
562
- updatedAt: claimedAt,
563
- })
564
- .where(and(eq(heartbeatRuns.id, run.id), eq(heartbeatRuns.status, "queued")))
565
- .returning()
566
- .then((rows) => rows[0] ?? null);
677
+ const claimed = await db.transaction(async (tx) => {
678
+ await tx.execute(sql `select pg_advisory_xact_lock(hashtext(${`agent-run-state:${run.agentId}`}))`);
679
+ await tx.execute(sql `select id from agents where id = ${run.agentId} for update`);
680
+ const currentAgent = await tx
681
+ .select({ status: agents.status })
682
+ .from(agents)
683
+ .where(eq(agents.id, run.agentId))
684
+ .then((rows) => rows[0] ?? null);
685
+ if (!currentAgent || ["paused", "terminated", "pending_approval"].includes(currentAgent.status)) {
686
+ return null;
687
+ }
688
+ const pendingTerminalEffects = await tx
689
+ .select({ id: heartbeatRuns.id })
690
+ .from(heartbeatRuns)
691
+ .where(and(eq(heartbeatRuns.agentId, run.agentId), eq(heartbeatRuns.terminalEffectsPending, true)))
692
+ .limit(1)
693
+ .then((rows) => rows[0] ?? null);
694
+ if (pendingTerminalEffects)
695
+ return null;
696
+ await tx.execute(sql `select id from heartbeat_runs where id = ${run.id} for update`);
697
+ const currentRun = await tx
698
+ .select()
699
+ .from(heartbeatRuns)
700
+ .where(eq(heartbeatRuns.id, run.id))
701
+ .then((rows) => rows[0] ?? null);
702
+ if (!currentRun || currentRun.status !== "queued")
703
+ return null;
704
+ const currentContext = parseObject(currentRun.contextSnapshot);
705
+ const currentTaskKey = deriveTaskKey(currentContext, null);
706
+ const resetTaskSession = shouldResetTaskSessionForWake(currentContext);
707
+ const sessionCodec = getAgentRuntimeSessionCodec(agent.agentRuntimeType);
708
+ const explicitSessionParams = normalizeSessionParams(sessionCodec.deserialize(parseObject(currentContext.resumeSessionParams)));
709
+ const explicitSessionDisplayId = truncateDisplayId(readNonEmptyString(currentContext.resumeSessionDisplayId) ??
710
+ (sessionCodec.getDisplayId ? sessionCodec.getDisplayId(explicitSessionParams) : null) ??
711
+ readNonEmptyString(explicitSessionParams?.sessionId));
712
+ const taskSession = currentTaskKey && !resetTaskSession
713
+ ? await tx
714
+ .select()
715
+ .from(agentTaskSessions)
716
+ .where(and(eq(agentTaskSessions.orgId, currentRun.orgId), eq(agentTaskSessions.agentId, currentRun.agentId), eq(agentTaskSessions.agentRuntimeType, agent.agentRuntimeType), eq(agentTaskSessions.taskKey, currentTaskKey)))
717
+ .then((rows) => rows[0] ?? null)
718
+ : null;
719
+ const taskSessionParams = normalizeSessionParams(sessionCodec.deserialize(taskSession?.sessionParamsJson ?? null));
720
+ const taskSessionDisplayId = truncateDisplayId(taskSession?.sessionDisplayId ??
721
+ (sessionCodec.getDisplayId ? sessionCodec.getDisplayId(taskSessionParams) : null) ??
722
+ readNonEmptyString(taskSessionParams?.sessionId));
723
+ const sessionSelection = heartbeatSessions.selectRunSessionLineage({
724
+ forceFresh: Boolean(heartbeatSessions.readSessionReuseSuppression(currentContext)),
725
+ explicitSessionParams,
726
+ explicitSessionDisplayId,
727
+ taskSessionParams,
728
+ taskSessionDisplayId,
729
+ });
730
+ const claimedRun = await tx
731
+ .update(heartbeatRuns)
732
+ .set({
733
+ status: "running",
734
+ startedAt: currentRun.startedAt ?? claimedAt,
735
+ sessionIdBefore: sessionSelection.sessionDisplayId ?? readNonEmptyString(sessionSelection.sessionParams?.sessionId),
736
+ sessionParamsBeforeJson: sessionSelection.sessionParams,
737
+ sessionReuseScope: sessionSelection.reuseScope,
738
+ executionOwnerToken: randomUUID(),
739
+ executionLeaseExpiresAt: new Date(claimedAt.getTime() + RUN_EXECUTION_LEASE_MS),
740
+ updatedAt: claimedAt,
741
+ })
742
+ .where(and(eq(heartbeatRuns.id, run.id), eq(heartbeatRuns.status, "queued")))
743
+ .returning()
744
+ .then((rows) => rows[0] ?? null);
745
+ if (!claimedRun)
746
+ return null;
747
+ await tx
748
+ .update(agents)
749
+ .set({ status: "running", updatedAt: claimedAt })
750
+ .where(eq(agents.id, run.agentId));
751
+ return claimedRun;
752
+ });
567
753
  if (!claimed)
568
754
  return null;
569
755
  publishLiveEvent({
@@ -584,14 +770,14 @@ export function heartbeatService(db) {
584
770
  await setWakeupStatus(claimed.wakeupRequestId, "claimed", { claimedAt });
585
771
  return claimed;
586
772
  }
587
- async function finalizeAgentStatus(agentId, outcome) {
773
+ async function finalizeAgentStatus(agentId, outcome, runId) {
588
774
  const existing = await getAgent(agentId);
589
775
  if (!existing)
590
776
  return;
591
777
  if (existing.status === "paused" || existing.status === "terminated") {
592
778
  return;
593
779
  }
594
- const runningCount = await countRunningRunsForAgent(agentId);
780
+ const runningCount = await countRunningRunsForAgent(agentId, runId);
595
781
  const nextStatus = runningCount > 0
596
782
  ? "running"
597
783
  : outcome === "succeeded" || outcome === "cancelled"
@@ -624,7 +810,8 @@ export function heartbeatService(db) {
624
810
  }
625
811
  async function reapOrphanedRuns(opts) {
626
812
  const staleThresholdMs = opts?.staleThresholdMs ?? 0;
627
- const now = new Date();
813
+ const now = opts?.now ?? new Date();
814
+ const recoveryCutoff = opts?.recoveryCutoff ?? now;
628
815
  // Find all runs stuck in "running" state (queued runs are legitimately waiting; resumeQueuedRuns handles them)
629
816
  const activeRuns = await db
630
817
  .select({
@@ -633,100 +820,107 @@ export function heartbeatService(db) {
633
820
  })
634
821
  .from(heartbeatRuns)
635
822
  .innerJoin(agents, eq(heartbeatRuns.agentId, agents.id))
636
- .where(eq(heartbeatRuns.status, "running"));
823
+ .where(or(eq(heartbeatRuns.status, "running"), eq(heartbeatRuns.terminalEffectsPending, true)));
637
824
  const reaped = [];
638
825
  for (const { run, agentRuntimeType } of activeRuns) {
639
- if (runningProcesses.has(run.id) || activeRunExecutions.has(run.id))
826
+ if (run.terminalEffectsPending) {
827
+ if (!run.processExitedAt) {
828
+ const exited = await terminateRunProcessAndWait(run, agentRuntimeType);
829
+ if (!exited || activeRunExecutions.has(run.id))
830
+ continue;
831
+ await acknowledgeRunProcessExit(run.id);
832
+ }
833
+ const completed = await completeTerminalControlEffects(run);
834
+ if (!completed)
835
+ continue;
836
+ reaped.push(run.id);
640
837
  continue;
838
+ }
641
839
  // Apply staleness threshold to avoid false positives
642
840
  if (staleThresholdMs > 0) {
643
841
  const refTime = run.updatedAt ? new Date(run.updatedAt).getTime() : 0;
644
842
  if (now.getTime() - refTime < staleThresholdMs)
645
843
  continue;
646
844
  }
845
+ const recoveryClaim = await claimExpiredHeartbeatRunExecution(db, run.id, { now, recoveryCutoff });
846
+ if (!recoveryClaim)
847
+ continue;
848
+ const claimedRun = recoveryClaim.run;
647
849
  const tracksLocalChild = isTrackedLocalChildProcessAdapter(agentRuntimeType);
648
850
  let detachedTerminationMessage = null;
649
- if (tracksLocalChild && run.processPid && isProcessAlive(run.processPid)) {
650
- const termination = await terminateOrphanedProcess(run.processPid);
851
+ if (!claimedRun.processExitedAt && tracksLocalChild && claimedRun.processPid && isProcessAlive(claimedRun.processPid)) {
852
+ const termination = await terminateOrphanedProcess(claimedRun.processPid);
651
853
  if (termination.stillAlive) {
652
854
  const detachedMessage = termination.error
653
- ? `Lost in-memory process handle, child pid ${run.processPid} is still alive, and Rudder could not terminate it: ${termination.error}`
654
- : `Lost in-memory process handle, but child pid ${run.processPid} is still alive`;
655
- const detachedRun = await setRunStatus(run.id, "running", {
855
+ ? `Lost in-memory process handle, child pid ${claimedRun.processPid} is still alive, and Rudder could not terminate it: ${termination.error}`
856
+ : `Lost in-memory process handle, but child pid ${claimedRun.processPid} is still alive`;
857
+ const detachedRun = await setRunStatus(claimedRun.id, "running", {
656
858
  error: detachedMessage,
657
859
  errorCode: DETACHED_PROCESS_ERROR_CODE,
658
860
  });
659
861
  if (detachedRun) {
660
- await appendRunEvent(detachedRun, await nextRunEventSeq(detachedRun.id), {
862
+ await appendRunEvent(detachedRun, {
661
863
  eventType: "lifecycle",
662
864
  stream: "system",
663
865
  level: "warn",
664
866
  message: detachedMessage,
665
867
  payload: {
666
- processPid: run.processPid,
868
+ processPid: claimedRun.processPid,
667
869
  },
668
870
  });
669
871
  }
670
872
  continue;
671
873
  }
672
874
  detachedTerminationMessage = termination.terminationSignal
673
- ? `Terminated detached child pid ${run.processPid} with ${termination.terminationSignal} after Rudder lost its process handle`
674
- : `Detached child pid ${run.processPid} exited before Rudder could terminate it`;
875
+ ? `Terminated detached child pid ${claimedRun.processPid} with ${termination.terminationSignal} after Rudder lost its process handle`
876
+ : `Detached child pid ${claimedRun.processPid} exited before Rudder could terminate it`;
675
877
  }
676
- const shouldRetry = tracksLocalChild && !!run.processPid && (run.processLossRetryCount ?? 0) < 1;
677
- const baseMessage = run.processPid
678
- ? `Process lost -- child pid ${run.processPid} is no longer running`
878
+ const shouldRetry = tracksLocalChild && !!claimedRun.processPid && (claimedRun.processLossRetryCount ?? 0) < 1;
879
+ const baseMessage = claimedRun.processPid
880
+ ? `Process lost -- child pid ${claimedRun.processPid} is no longer running`
679
881
  : "Process lost -- server may have restarted";
680
- let finalizedRun = await setRunStatus(run.id, "failed", {
882
+ let finalizedRun = await transitionRunToTerminal(claimedRun.id, "failed", {
681
883
  error: shouldRetry ? `${baseMessage}; retrying once` : baseMessage,
682
884
  errorCode: "process_lost",
683
885
  finishedAt: now,
886
+ }, {
887
+ processExitedAt: now,
888
+ terminalEffectsIntent: shouldRetry ? { version: 1, processLossRetry: true } : { version: 1 },
889
+ expectedExecutionOwnerToken: recoveryClaim.ownerToken,
684
890
  });
685
- await setWakeupStatus(run.wakeupRequestId, "failed", {
891
+ if (!finalizedRun)
892
+ continue;
893
+ await setWakeupStatus(claimedRun.wakeupRequestId, "failed", {
686
894
  finishedAt: now,
687
895
  error: shouldRetry ? `${baseMessage}; retrying once` : baseMessage,
688
896
  });
689
- if (!finalizedRun)
690
- finalizedRun = await getRun(run.id);
691
- if (!finalizedRun)
692
- continue;
693
897
  if (detachedTerminationMessage) {
694
- await appendRunEvent(finalizedRun, await nextRunEventSeq(finalizedRun.id), {
898
+ await appendRunEvent(finalizedRun, {
695
899
  eventType: "lifecycle",
696
900
  stream: "system",
697
901
  level: "warn",
698
902
  message: detachedTerminationMessage,
699
903
  payload: {
700
- ...(run.processPid ? { processPid: run.processPid } : {}),
904
+ ...(claimedRun.processPid ? { processPid: claimedRun.processPid } : {}),
701
905
  },
702
906
  });
703
907
  }
704
- let retriedRun = null;
705
- if (shouldRetry) {
706
- const agent = await getAgent(run.agentId);
707
- if (agent) {
708
- retriedRun = await enqueueProcessLossRetry(finalizedRun, agent, now);
709
- }
710
- }
711
- else {
712
- await releaseIssueExecutionAndPromote(finalizedRun);
713
- }
714
- await appendRunEvent(finalizedRun, await nextRunEventSeq(finalizedRun.id), {
908
+ await appendRunEvent(finalizedRun, {
715
909
  eventType: "lifecycle",
716
910
  stream: "system",
717
911
  level: "error",
718
912
  message: shouldRetry
719
- ? `${baseMessage}; queued retry ${retriedRun?.id ?? ""}`.trim()
913
+ ? `${baseMessage}; retry will be queued after terminal effects complete`
720
914
  : baseMessage,
721
915
  payload: {
722
- ...(run.processPid ? { processPid: run.processPid } : {}),
723
- ...(retriedRun ? { retryRunId: retriedRun.id } : {}),
916
+ ...(claimedRun.processPid ? { processPid: claimedRun.processPid } : {}),
724
917
  },
725
918
  });
726
- await finalizeAgentStatus(run.agentId, "failed");
727
- await startNextQueuedRunForAgent(run.agentId);
728
- runningProcesses.delete(run.id);
729
- reaped.push(run.id);
919
+ const completed = await completeTerminalControlEffects(finalizedRun);
920
+ runningProcesses.delete(claimedRun.id);
921
+ if (!completed)
922
+ continue;
923
+ reaped.push(claimedRun.id);
730
924
  }
731
925
  if (reaped.length > 0) {
732
926
  logger.warn({ reapedCount: reaped.length, runIds: reaped }, "reaped orphaned heartbeat runs");
@@ -744,6 +938,7 @@ export function heartbeatService(db) {
744
938
  run: heartbeatRuns,
745
939
  agentRuntimeType: agents.agentRuntimeType,
746
940
  lastEventAt: sql `max(${heartbeatRunEvents.createdAt})`,
941
+ eventCount: sql `count(${heartbeatRunEvents.id})::int`,
747
942
  })
748
943
  .from(heartbeatRuns)
749
944
  .innerJoin(agents, eq(heartbeatRuns.agentId, agents.id))
@@ -751,7 +946,9 @@ export function heartbeatService(db) {
751
946
  .where(eq(heartbeatRuns.status, "running"))
752
947
  .groupBy(heartbeatRuns.id, agents.agentRuntimeType);
753
948
  const timedOut = [];
754
- for (const { run, agentRuntimeType, lastEventAt } of activeRuns) {
949
+ for (const { run, agentRuntimeType, lastEventAt, eventCount } of activeRuns) {
950
+ if (opts?.recoveryCutoff && new Date(run.createdAt).getTime() >= opts.recoveryCutoff.getTime())
951
+ continue;
755
952
  const activityTimes = [
756
953
  run.updatedAt,
757
954
  lastEventAt,
@@ -768,33 +965,29 @@ export function heartbeatService(db) {
768
965
  if (inactiveMs < maxInactivityMs)
769
966
  continue;
770
967
  const message = `Run had no recorded activity for ${formatDurationMs(maxInactivityMs)}`;
771
- const running = runningProcesses.get(run.id);
772
- if (running) {
773
- const pid = running.child.pid;
774
- running.child.kill("SIGTERM");
775
- const graceMs = Math.max(1, running.graceSec) * 1000;
776
- setTimeout(() => {
777
- if (typeof pid === "number" && isProcessAlive(pid)) {
778
- running.child.kill("SIGKILL");
779
- }
780
- }, graceMs);
781
- }
782
- else if (isTrackedLocalChildProcessAdapter(agentRuntimeType) && run.processPid && isProcessAlive(run.processPid)) {
783
- await terminateOrphanedProcess(run.processPid);
784
- }
785
- const finalizedRun = await setRunStatus(run.id, "timed_out", {
968
+ const processExitPending = activeRunExecutions.has(run.id)
969
+ || runningProcesses.has(run.id)
970
+ || (!run.processExitedAt && isTrackedLocalChildProcessAdapter(agentRuntimeType) && !!run.processPid && isProcessAlive(run.processPid));
971
+ const finalizedRun = await transitionRunToTerminal(run.id, "timed_out", {
786
972
  finishedAt: now,
787
973
  error: message,
788
974
  errorCode: "inactivity_timeout",
975
+ terminalEffectsPending: true,
976
+ }, {
977
+ activityWatermark: {
978
+ updatedAt: run.updatedAt,
979
+ eventCount: Number(eventCount ?? 0),
980
+ },
981
+ processExitedAt: processExitPending ? null : now,
982
+ expectedExecutionOwnerToken: run.executionOwnerToken,
789
983
  });
984
+ if (!finalizedRun)
985
+ continue;
790
986
  await setWakeupStatus(run.wakeupRequestId, "timed_out", {
791
987
  finishedAt: now,
792
988
  error: message,
793
989
  });
794
- const terminalRun = finalizedRun ?? await getRun(run.id);
795
- if (!terminalRun)
796
- continue;
797
- await appendRunEvent(terminalRun, await nextRunEventSeq(terminalRun.id), {
990
+ await appendRunEvent(finalizedRun, {
798
991
  eventType: "lifecycle",
799
992
  stream: "system",
800
993
  level: "error",
@@ -807,10 +1000,12 @@ export function heartbeatService(db) {
807
1000
  ...(run.processPid ? { processPid: run.processPid } : {}),
808
1001
  },
809
1002
  });
810
- await releaseIssueExecutionAndPromote(terminalRun);
811
- await finalizeAgentStatus(run.agentId, "timed_out");
812
- await startNextQueuedRunForAgent(run.agentId);
813
- runningProcesses.delete(run.id);
1003
+ const processExited = !processExitPending || await terminateRunProcessAndWait(finalizedRun, agentRuntimeType);
1004
+ if (processExited && !activeRunExecutions.has(finalizedRun.id)) {
1005
+ await acknowledgeRunProcessExit(finalizedRun.id);
1006
+ await completeTerminalControlEffects(finalizedRun);
1007
+ runningProcesses.delete(run.id);
1008
+ }
814
1009
  timedOut.push(run.id);
815
1010
  }
816
1011
  if (timedOut.length > 0) {
@@ -834,6 +1029,8 @@ export function heartbeatService(db) {
834
1029
  .where(eq(heartbeatRuns.status, "running"));
835
1030
  const timedOut = [];
836
1031
  for (const { run, agentRuntimeType } of activeRuns) {
1032
+ if (opts?.recoveryCutoff && new Date(run.createdAt).getTime() >= opts.recoveryCutoff.getTime())
1033
+ continue;
837
1034
  const startedAt = run.startedAt ? new Date(run.startedAt).getTime() : null;
838
1035
  if (!startedAt || !Number.isFinite(startedAt))
839
1036
  continue;
@@ -841,33 +1038,25 @@ export function heartbeatService(db) {
841
1038
  if (runtimeMs < maxRuntimeMs)
842
1039
  continue;
843
1040
  const message = `Run exceeded maximum duration of ${formatDurationMs(maxRuntimeMs)}`;
844
- const running = runningProcesses.get(run.id);
845
- if (running) {
846
- const pid = running.child.pid;
847
- running.child.kill("SIGTERM");
848
- const graceMs = Math.max(1, running.graceSec) * 1000;
849
- setTimeout(() => {
850
- if (typeof pid === "number" && isProcessAlive(pid)) {
851
- running.child.kill("SIGKILL");
852
- }
853
- }, graceMs);
854
- }
855
- else if (isTrackedLocalChildProcessAdapter(agentRuntimeType) && run.processPid && isProcessAlive(run.processPid)) {
856
- await terminateOrphanedProcess(run.processPid);
857
- }
858
- const finalizedRun = await setRunStatus(run.id, "timed_out", {
1041
+ const processExitPending = activeRunExecutions.has(run.id)
1042
+ || runningProcesses.has(run.id)
1043
+ || (!run.processExitedAt && isTrackedLocalChildProcessAdapter(agentRuntimeType) && !!run.processPid && isProcessAlive(run.processPid));
1044
+ const finalizedRun = await transitionRunToTerminal(run.id, "timed_out", {
859
1045
  finishedAt: now,
860
1046
  error: message,
861
1047
  errorCode: "timeout",
1048
+ terminalEffectsPending: true,
1049
+ }, {
1050
+ processExitedAt: processExitPending ? null : now,
1051
+ expectedExecutionOwnerToken: run.executionOwnerToken,
862
1052
  });
1053
+ if (!finalizedRun)
1054
+ continue;
863
1055
  await setWakeupStatus(run.wakeupRequestId, "timed_out", {
864
1056
  finishedAt: now,
865
1057
  error: message,
866
1058
  });
867
- const terminalRun = finalizedRun ?? await getRun(run.id);
868
- if (!terminalRun)
869
- continue;
870
- await appendRunEvent(terminalRun, await nextRunEventSeq(terminalRun.id), {
1059
+ await appendRunEvent(finalizedRun, {
871
1060
  eventType: "lifecycle",
872
1061
  stream: "system",
873
1062
  level: "error",
@@ -880,10 +1069,12 @@ export function heartbeatService(db) {
880
1069
  ...(run.processPid ? { processPid: run.processPid } : {}),
881
1070
  },
882
1071
  });
883
- await releaseIssueExecutionAndPromote(terminalRun);
884
- await finalizeAgentStatus(run.agentId, "timed_out");
885
- await startNextQueuedRunForAgent(run.agentId);
886
- runningProcesses.delete(run.id);
1072
+ const processExited = !processExitPending || await terminateRunProcessAndWait(finalizedRun, agentRuntimeType);
1073
+ if (processExited && !activeRunExecutions.has(finalizedRun.id)) {
1074
+ await acknowledgeRunProcessExit(finalizedRun.id);
1075
+ await completeTerminalControlEffects(finalizedRun);
1076
+ runningProcesses.delete(run.id);
1077
+ }
887
1078
  timedOut.push(run.id);
888
1079
  }
889
1080
  if (timedOut.length > 0) {
@@ -901,7 +1092,7 @@ export function heartbeatService(db) {
901
1092
  await startNextQueuedRunForAgent(agentId);
902
1093
  }
903
1094
  }
904
- async function updateRuntimeState(agent, run, result, session, normalizedUsage) {
1095
+ async function updateRuntimeState(agent, run, result, session, normalizedUsage, opts) {
905
1096
  await ensureRuntimeState(agent);
906
1097
  const usage = normalizedUsage ?? normalizeUsageTotals(result.usage);
907
1098
  const rawInputTokens = usage?.inputTokens ?? 0;
@@ -919,24 +1110,37 @@ export function heartbeatService(db) {
919
1110
  });
920
1111
  const biller = resolveLedgerBiller(result);
921
1112
  const ledgerScope = await resolveLedgerScopeForRun(db, agent.orgId, run);
922
- await db
923
- .update(agentRuntimeState)
924
- .set({
925
- agentRuntimeType: agent.agentRuntimeType,
926
- sessionId: session.legacySessionId,
927
- lastRunId: run.id,
928
- lastRunStatus: run.status,
929
- lastError: result.errorMessage ?? null,
930
- totalInputTokens: sql `${agentRuntimeState.totalInputTokens} + ${tokenSummary.promptTokens}`,
931
- totalOutputTokens: sql `${agentRuntimeState.totalOutputTokens} + ${outputTokens}`,
932
- totalCachedInputTokens: sql `${agentRuntimeState.totalCachedInputTokens} + ${cachedInputTokens}`,
933
- totalCostCents: sql `${agentRuntimeState.totalCostCents} + ${additionalCostCents}`,
934
- updatedAt: new Date(),
935
- })
936
- .where(eq(agentRuntimeState.agentId, agent.id));
1113
+ if (opts?.ownsTerminal) {
1114
+ await db
1115
+ .update(agentRuntimeState)
1116
+ .set({
1117
+ agentRuntimeType: agent.agentRuntimeType,
1118
+ sessionId: session.legacySessionId,
1119
+ lastRunId: run.id,
1120
+ lastRunStatus: run.status,
1121
+ lastError: result.errorMessage ?? null,
1122
+ updatedAt: new Date(),
1123
+ })
1124
+ .where(and(eq(agentRuntimeState.agentId, agent.id), sql `(
1125
+ ${agentRuntimeState.lastRunId} is null
1126
+ or ${agentRuntimeState.lastRunId} = ${run.id}
1127
+ or not exists (
1128
+ select 1
1129
+ from heartbeat_runs previous_run
1130
+ where previous_run.id = ${agentRuntimeState.lastRunId}
1131
+ and (
1132
+ previous_run.created_at > ${run.createdAt.toISOString()}::timestamptz
1133
+ or (
1134
+ previous_run.created_at = ${run.createdAt.toISOString()}::timestamptz
1135
+ and previous_run.id > ${run.id}
1136
+ )
1137
+ )
1138
+ )
1139
+ )`));
1140
+ }
937
1141
  if (additionalCostCents > 0 || hasTokenUsage) {
938
1142
  const costs = costService(db, budgetHooks);
939
- await costs.createEvent(agent.orgId, {
1143
+ await costs.createHeartbeatRunEventOnce(agent.orgId, {
940
1144
  heartbeatRunId: run.id,
941
1145
  agentId: agent.id,
942
1146
  issueId: ledgerScope.issueId,
@@ -950,9 +1154,163 @@ export function heartbeatService(db) {
950
1154
  outputTokens,
951
1155
  costCents: additionalCostCents,
952
1156
  occurredAt: new Date(),
1157
+ }, async (tx) => {
1158
+ await tx
1159
+ .update(agentRuntimeState)
1160
+ .set({
1161
+ totalInputTokens: sql `${agentRuntimeState.totalInputTokens} + ${tokenSummary.promptTokens}`,
1162
+ totalOutputTokens: sql `${agentRuntimeState.totalOutputTokens} + ${outputTokens}`,
1163
+ totalCachedInputTokens: sql `${agentRuntimeState.totalCachedInputTokens} + ${cachedInputTokens}`,
1164
+ totalCostCents: sql `${agentRuntimeState.totalCostCents} + ${additionalCostCents}`,
1165
+ updatedAt: new Date(),
1166
+ })
1167
+ .where(eq(agentRuntimeState.agentId, agent.id));
953
1168
  });
954
1169
  }
955
1170
  }
1171
+ async function completeTerminalControlEffects(run, opts) {
1172
+ const claim = await claimHeartbeatRunTerminalEffects(db, run.id);
1173
+ if (!claim)
1174
+ return;
1175
+ const current = claim.run;
1176
+ let claimReleased = false;
1177
+ try {
1178
+ const runContext = parseObject(current.contextSnapshot);
1179
+ const intent = parseObject(current.terminalEffectsJson);
1180
+ const completedEffects = new Set(current.terminalEffectsCompletedJson ?? []);
1181
+ const deadLetteredEffects = new Set(current.terminalEffectsDeadLetteredJson ?? []);
1182
+ const runClaimedEffect = async (effectName, effect) => {
1183
+ if (completedEffects.has(effectName) || deadLetteredEffects.has(effectName))
1184
+ return undefined;
1185
+ const renewClaim = async () => {
1186
+ if (!await renewHeartbeatRunTerminalEffectsClaim(db, current.id, claim.claimToken)) {
1187
+ throw new Error("Heartbeat terminal effects claim was lost");
1188
+ }
1189
+ };
1190
+ await renewClaim();
1191
+ let renewalError = null;
1192
+ let renewal = Promise.resolve();
1193
+ const renewalTimer = setInterval(() => {
1194
+ renewal = renewal
1195
+ .then(renewClaim)
1196
+ .catch((error) => {
1197
+ renewalError ??= error;
1198
+ });
1199
+ }, TERMINAL_EFFECT_CLAIM_RENEW_INTERVAL_MS);
1200
+ renewalTimer.unref?.();
1201
+ try {
1202
+ const result = await effect();
1203
+ await renewal;
1204
+ if (renewalError)
1205
+ throw renewalError;
1206
+ await renewClaim();
1207
+ if (!await checkpointHeartbeatRunTerminalEffect(db, current.id, claim.claimToken, effectName)) {
1208
+ throw new Error(`Heartbeat terminal effect ${effectName} checkpoint was lost`);
1209
+ }
1210
+ completedEffects.add(effectName);
1211
+ return result;
1212
+ }
1213
+ catch (error) {
1214
+ const failure = await failHeartbeatRunTerminalEffect(db, current.id, claim.claimToken, effectName, error);
1215
+ if (!failure)
1216
+ throw error;
1217
+ if (failure.deadLettered) {
1218
+ deadLetteredEffects.add(effectName);
1219
+ return undefined;
1220
+ }
1221
+ claimReleased = true;
1222
+ throw error;
1223
+ }
1224
+ finally {
1225
+ clearInterval(renewalTimer);
1226
+ }
1227
+ };
1228
+ if (intent.automation) {
1229
+ await runClaimedEffect("automation_chat", () => publishAutomationRunOutputToChat(db, {
1230
+ issueId: readNonEmptyString(runContext.issueId),
1231
+ output: opts?.automationOutput ?? intent.automation?.output ?? current.error,
1232
+ status: current.status,
1233
+ transcript: opts?.automationTranscript ?? [],
1234
+ }));
1235
+ }
1236
+ const agent = await getAgent(current.agentId);
1237
+ const runtimeIntent = intent.runtime;
1238
+ if (runtimeIntent) {
1239
+ await runClaimedEffect("runtime_cost", () => {
1240
+ if (!agent)
1241
+ throw new Error("Agent not found while replaying runtime terminal effect");
1242
+ return updateRuntimeState(agent, current, {
1243
+ exitCode: null,
1244
+ signal: null,
1245
+ timedOut: false,
1246
+ errorMessage: runtimeIntent.errorMessage,
1247
+ provider: runtimeIntent.provider,
1248
+ biller: runtimeIntent.biller,
1249
+ model: runtimeIntent.model,
1250
+ billingType: runtimeIntent.billingType,
1251
+ costUsd: runtimeIntent.costUsd,
1252
+ }, { legacySessionId: runtimeIntent.legacySessionId }, runtimeIntent.normalizedUsage, { ownsTerminal: runtimeIntent.ownsTerminal !== false });
1253
+ });
1254
+ }
1255
+ const taskSessionIntent = intent.taskSession;
1256
+ if (taskSessionIntent?.operation === "clear") {
1257
+ await runClaimedEffect("task_session", () => clearTaskSessions(taskSessionIntent.orgId, taskSessionIntent.agentId, {
1258
+ taskKey: taskSessionIntent.taskKey,
1259
+ agentRuntimeType: taskSessionIntent.agentRuntimeType,
1260
+ sourceRunId: taskSessionIntent.lastRunId ?? current.id,
1261
+ }));
1262
+ }
1263
+ else if (taskSessionIntent?.operation === "upsert") {
1264
+ await runClaimedEffect("task_session", () => upsertTaskSession({
1265
+ orgId: taskSessionIntent.orgId,
1266
+ agentId: taskSessionIntent.agentId,
1267
+ agentRuntimeType: taskSessionIntent.agentRuntimeType,
1268
+ taskKey: taskSessionIntent.taskKey,
1269
+ sessionParamsJson: taskSessionIntent.sessionParamsJson ?? null,
1270
+ sessionDisplayId: taskSessionIntent.sessionDisplayId ?? null,
1271
+ lastRunId: taskSessionIntent.lastRunId ?? current.id,
1272
+ lastError: taskSessionIntent.lastError ?? null,
1273
+ }));
1274
+ }
1275
+ if (intent.processLossRetry) {
1276
+ await runClaimedEffect("process_loss_retry", () => {
1277
+ if (!agent)
1278
+ throw new Error("Agent not found while replaying process-loss retry");
1279
+ return enqueueProcessLossRetry(current, agent, current.finishedAt ?? new Date());
1280
+ });
1281
+ }
1282
+ await runClaimedEffect("issue_release", () => releaseIssueExecutionAndPromote(current, { startNext: false }));
1283
+ const requiredEffects = terminalEffectNames(intent);
1284
+ if (!requiredEffects.every((effect) => completedEffects.has(effect) || deadLetteredEffects.has(effect))) {
1285
+ throw new Error("Heartbeat terminal effects did not converge");
1286
+ }
1287
+ const completed = await completeHeartbeatRunTerminalEffects(db, current.id, claim.claimToken);
1288
+ if (!completed)
1289
+ return;
1290
+ if (completed.agent) {
1291
+ publishLiveEvent({
1292
+ orgId: completed.agent.orgId,
1293
+ type: "agent.status",
1294
+ payload: {
1295
+ agentId: completed.agent.id,
1296
+ status: completed.agent.status,
1297
+ lastHeartbeatAt: completed.agent.lastHeartbeatAt
1298
+ ? new Date(completed.agent.lastHeartbeatAt).toISOString()
1299
+ : null,
1300
+ },
1301
+ });
1302
+ }
1303
+ if (opts?.startNext !== false)
1304
+ await resumeQueuedRuns();
1305
+ return completed;
1306
+ }
1307
+ catch (error) {
1308
+ if (!claimReleased) {
1309
+ await releaseHeartbeatRunTerminalEffectsClaim(db, current.id, claim.claimToken, error);
1310
+ }
1311
+ throw error;
1312
+ }
1313
+ }
956
1314
  async function startNextQueuedRunForAgent(agentId) {
957
1315
  return withAgentStartLock(agentId, async () => {
958
1316
  const agent = await getAgent(agentId);
@@ -961,6 +1319,14 @@ export function heartbeatService(db) {
961
1319
  if (agent.status === "paused" || agent.status === "terminated" || agent.status === "pending_approval") {
962
1320
  return [];
963
1321
  }
1322
+ const pendingTerminalEffects = await db
1323
+ .select({ id: heartbeatRuns.id })
1324
+ .from(heartbeatRuns)
1325
+ .where(and(eq(heartbeatRuns.agentId, agentId), eq(heartbeatRuns.terminalEffectsPending, true)))
1326
+ .limit(1)
1327
+ .then((rows) => rows[0] ?? null);
1328
+ if (pendingTerminalEffects)
1329
+ return [];
964
1330
  const policy = parseHeartbeatPolicy(agent);
965
1331
  const runningCount = await countRunningRunsForAgent(agentId);
966
1332
  const availableSlots = Math.max(0, policy.maxConcurrentRuns - runningCount);
@@ -984,27 +1350,51 @@ export function heartbeatService(db) {
984
1350
  return [];
985
1351
  const claimedRuns = [];
986
1352
  for (const queuedRun of queuedRuns) {
987
- const claimed = await claimQueuedRun(queuedRun);
988
- if (claimed)
989
- claimedRuns.push(claimed);
1353
+ if (activeRunExecutions.has(queuedRun.id))
1354
+ continue;
1355
+ activeRunExecutions.add(queuedRun.id);
1356
+ runAbortControllers.set(queuedRun.id, new AbortController());
1357
+ try {
1358
+ const claimed = await claimQueuedRun(queuedRun);
1359
+ if (claimed) {
1360
+ claimedRuns.push(claimed);
1361
+ void executeRun(claimed.id, { executionReserved: true }).catch((err) => {
1362
+ logger.error({ err, runId: claimed.id }, "queued heartbeat execution failed");
1363
+ });
1364
+ }
1365
+ else {
1366
+ runAbortControllers.delete(queuedRun.id);
1367
+ activeRunExecutions.delete(queuedRun.id);
1368
+ }
1369
+ }
1370
+ catch (error) {
1371
+ runAbortControllers.delete(queuedRun.id);
1372
+ activeRunExecutions.delete(queuedRun.id);
1373
+ throw error;
1374
+ }
990
1375
  }
991
1376
  if (claimedRuns.length === 0)
992
1377
  return [];
993
- for (const claimedRun of claimedRuns) {
994
- void executeRun(claimedRun.id).catch((err) => {
995
- logger.error({ err, runId: claimedRun.id }, "queued heartbeat execution failed");
996
- });
997
- }
998
1378
  return claimedRuns;
999
1379
  });
1000
1380
  }
1001
1381
  const baseContext = {
1002
- db, instanceSettings, getCurrentUserRedactionOptions, runLogStore, runContextSvc, issuesSvc, executionWorkspacesSvc, workspaceOperationsSvc, activeRunExecutions, budgetHooks, budgets,
1003
- getAgent, getRun, getRuntimeState, getTaskSession, getLatestRunForSession, getOldestRunForSession, resolveNormalizedUsageForSession, evaluateSessionCompaction, resolveSessionBeforeForWakeup, resolveExplicitResumeSessionOverride, upsertTaskSession, clearTaskSessions, ensureRuntimeState, setRunStatus, setWakeupStatus, updateWakeupRequestRecord, insertWakeupRequestRecord, appendRunEvent, nextRunEventSeq, persistRunProcessMetadata, clearDetachedRunWarning, countRunningRunsForAgent, claimQueuedRun, finalizeAgentStatus, reapOrphanedRuns, reapInactiveRuns, reapTimedOutRuns, resumeQueuedRuns, updateRuntimeState, startNextQueuedRunForAgent,
1382
+ db, instanceSettings, getCurrentUserRedactionOptions, runLogStore, runContextSvc, issuesSvc, executionWorkspacesSvc, workspaceOperationsSvc, activeRunExecutions, runAbortControllers, budgetHooks, budgets,
1383
+ getAgent, getRun, getRuntimeState, getTaskSession, getLatestRunForSession, getOldestRunForSession, resolveNormalizedUsageForSession, evaluateSessionCompaction, resolveSessionBeforeForWakeup, resolveExplicitResumeSessionOverride, upsertTaskSession, clearTaskSessions, ensureRuntimeState, setRunStatus, transitionRunToTerminal, reconcileRunEvidence, reconcileTerminalEffectsIntent, setWakeupStatus, updateWakeupRequestRecord, insertWakeupRequestRecord, appendRunEvent, persistRunProcessMetadata, clearDetachedRunWarning, terminateRunProcessAndWait, acknowledgeRunProcessExit, renewRunExecutionLease, countRunningRunsForAgent, claimQueuedRun, finalizeAgentStatus, completeTerminalControlEffects, reapOrphanedRuns, reapInactiveRuns, reapTimedOutRuns, resumeQueuedRuns, updateRuntimeState, startNextQueuedRunForAgent,
1004
1384
  };
1005
1385
  const recoveryHandlers = createHeartbeatRecoveryHandlers({ ...baseContext, startNextQueuedRunForAgent });
1006
- const wakeupHandlers = createHeartbeatWakeupHandlers({ ...baseContext, ...recoveryHandlers, startNextQueuedRunForAgent });
1007
- const releaseHandlers = createHeartbeatReleaseHandlers({ ...baseContext, ...recoveryHandlers, ...wakeupHandlers });
1386
+ const wakeupHandlers = createHeartbeatWakeupHandlers({
1387
+ ...baseContext,
1388
+ ...recoveryHandlers,
1389
+ startNextQueuedRunForAgent,
1390
+ afterIssuePromotionCommitted: testHooks?.afterIssuePromotionCommitted,
1391
+ });
1392
+ const releaseHandlers = createHeartbeatReleaseHandlers({
1393
+ ...baseContext,
1394
+ ...recoveryHandlers,
1395
+ ...wakeupHandlers,
1396
+ afterIssuePromotionCommitted: testHooks?.afterIssuePromotionCommitted,
1397
+ });
1008
1398
  const executeHandlers = createHeartbeatExecuteHandlers({ ...baseContext, ...recoveryHandlers, ...releaseHandlers, ...wakeupHandlers });
1009
1399
  const miscHandlers = createHeartbeatMiscHandlers({ ...baseContext, ...recoveryHandlers, ...releaseHandlers, ...wakeupHandlers, ...executeHandlers });
1010
1400
  const { enqueueRecoveryRun, enqueueProcessLossRetry, evaluatePassiveIssueClosureForLockedIssue, parseHeartbeatPolicy } = recoveryHandlers;
@@ -1058,7 +1448,7 @@ export function heartbeatService(db) {
1058
1448
  usedSkillsByRun.set(event.runId, runSkills);
1059
1449
  }
1060
1450
  }
1061
- return rows.map((row) => ({
1451
+ return rows.map((row) => toHeartbeatRun({
1062
1452
  ...row,
1063
1453
  resultJson: (() => {
1064
1454
  const summary = summarizeHeartbeatRunResultJson(row.resultJson);
@@ -1249,7 +1639,7 @@ export function heartbeatService(db) {
1249
1639
  const [run] = await db
1250
1640
  .select()
1251
1641
  .from(heartbeatRuns)
1252
- .where(and(eq(heartbeatRuns.agentId, agentId), eq(heartbeatRuns.status, "running")))
1642
+ .where(and(eq(heartbeatRuns.agentId, agentId), or(eq(heartbeatRuns.status, "running"), eq(heartbeatRuns.terminalEffectsPending, true))))
1253
1643
  .orderBy(desc(heartbeatRuns.startedAt))
1254
1644
  .limit(1);
1255
1645
  return run ?? null;