@paperclipai/server 0.3.1 → 2026.3.17-canary.2

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 (335) hide show
  1. package/dist/adapters/registry.d.ts.map +1 -1
  2. package/dist/adapters/registry.js +23 -2
  3. package/dist/adapters/registry.js.map +1 -1
  4. package/dist/adapters/types.d.ts +1 -1
  5. package/dist/adapters/types.d.ts.map +1 -1
  6. package/dist/app.d.ts +4 -0
  7. package/dist/app.d.ts.map +1 -1
  8. package/dist/app.js +115 -3
  9. package/dist/app.js.map +1 -1
  10. package/dist/attachment-types.d.ts.map +1 -1
  11. package/dist/attachment-types.js +6 -0
  12. package/dist/attachment-types.js.map +1 -1
  13. package/dist/config.d.ts.map +1 -1
  14. package/dist/config.js +9 -1
  15. package/dist/config.js.map +1 -1
  16. package/dist/home-paths.d.ts +5 -0
  17. package/dist/home-paths.d.ts.map +1 -1
  18. package/dist/home-paths.js +18 -0
  19. package/dist/home-paths.js.map +1 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +61 -42
  22. package/dist/index.js.map +1 -1
  23. package/dist/routes/access.d.ts.map +1 -1
  24. package/dist/routes/access.js +90 -1
  25. package/dist/routes/access.js.map +1 -1
  26. package/dist/routes/agents.d.ts.map +1 -1
  27. package/dist/routes/agents.js +64 -1
  28. package/dist/routes/agents.js.map +1 -1
  29. package/dist/routes/assets.d.ts.map +1 -1
  30. package/dist/routes/assets.js +191 -13
  31. package/dist/routes/assets.js.map +1 -1
  32. package/dist/routes/companies.d.ts.map +1 -1
  33. package/dist/routes/companies.js +10 -1
  34. package/dist/routes/companies.js.map +1 -1
  35. package/dist/routes/costs.d.ts.map +1 -1
  36. package/dist/routes/costs.js +160 -5
  37. package/dist/routes/costs.js.map +1 -1
  38. package/dist/routes/execution-workspaces.d.ts +3 -0
  39. package/dist/routes/execution-workspaces.d.ts.map +1 -0
  40. package/dist/routes/execution-workspaces.js +165 -0
  41. package/dist/routes/execution-workspaces.js.map +1 -0
  42. package/dist/routes/health.d.ts.map +1 -1
  43. package/dist/routes/health.js +3 -1
  44. package/dist/routes/health.js.map +1 -1
  45. package/dist/routes/index.d.ts +1 -0
  46. package/dist/routes/index.d.ts.map +1 -1
  47. package/dist/routes/index.js +1 -0
  48. package/dist/routes/index.js.map +1 -1
  49. package/dist/routes/instance-settings.d.ts +3 -0
  50. package/dist/routes/instance-settings.d.ts.map +1 -0
  51. package/dist/routes/instance-settings.js +46 -0
  52. package/dist/routes/instance-settings.js.map +1 -0
  53. package/dist/routes/issues.d.ts.map +1 -1
  54. package/dist/routes/issues.js +339 -4
  55. package/dist/routes/issues.js.map +1 -1
  56. package/dist/routes/plugin-ui-static.d.ts +69 -0
  57. package/dist/routes/plugin-ui-static.d.ts.map +1 -0
  58. package/dist/routes/plugin-ui-static.js +411 -0
  59. package/dist/routes/plugin-ui-static.js.map +1 -0
  60. package/dist/routes/plugins.d.ts +120 -0
  61. package/dist/routes/plugins.d.ts.map +1 -0
  62. package/dist/routes/plugins.js +1784 -0
  63. package/dist/routes/plugins.js.map +1 -0
  64. package/dist/routes/projects.d.ts.map +1 -1
  65. package/dist/routes/projects.js +5 -1
  66. package/dist/routes/projects.js.map +1 -1
  67. package/dist/services/access.d.ts +2 -2
  68. package/dist/services/activity-log.d.ts +3 -0
  69. package/dist/services/activity-log.d.ts.map +1 -1
  70. package/dist/services/activity-log.js +34 -0
  71. package/dist/services/activity-log.js.map +1 -1
  72. package/dist/services/agents.d.ts +39 -15
  73. package/dist/services/agents.d.ts.map +1 -1
  74. package/dist/services/agents.js +60 -8
  75. package/dist/services/agents.js.map +1 -1
  76. package/dist/services/approvals.d.ts +5 -5
  77. package/dist/services/approvals.d.ts.map +1 -1
  78. package/dist/services/approvals.js +11 -0
  79. package/dist/services/approvals.js.map +1 -1
  80. package/dist/services/assets.d.ts +4 -4
  81. package/dist/services/budgets.d.ts +38 -0
  82. package/dist/services/budgets.d.ts.map +1 -0
  83. package/dist/services/budgets.js +784 -0
  84. package/dist/services/budgets.js.map +1 -0
  85. package/dist/services/companies.d.ts +39 -418
  86. package/dist/services/companies.d.ts.map +1 -1
  87. package/dist/services/companies.js +157 -21
  88. package/dist/services/companies.js.map +1 -1
  89. package/dist/services/costs.d.ts +67 -3
  90. package/dist/services/costs.d.ts.map +1 -1
  91. package/dist/services/costs.js +185 -57
  92. package/dist/services/costs.js.map +1 -1
  93. package/dist/services/cron.d.ts +80 -0
  94. package/dist/services/cron.d.ts.map +1 -0
  95. package/dist/services/cron.js +300 -0
  96. package/dist/services/cron.js.map +1 -0
  97. package/dist/services/dashboard.d.ts +6 -0
  98. package/dist/services/dashboard.d.ts.map +1 -1
  99. package/dist/services/dashboard.js +9 -0
  100. package/dist/services/dashboard.js.map +1 -1
  101. package/dist/services/documents.d.ts +164 -0
  102. package/dist/services/documents.d.ts.map +1 -0
  103. package/dist/services/documents.js +382 -0
  104. package/dist/services/documents.js.map +1 -0
  105. package/dist/services/execution-workspace-policy.d.ts +2 -1
  106. package/dist/services/execution-workspace-policy.d.ts.map +1 -1
  107. package/dist/services/execution-workspace-policy.js +63 -15
  108. package/dist/services/execution-workspace-policy.js.map +1 -1
  109. package/dist/services/execution-workspaces.d.ts +19 -0
  110. package/dist/services/execution-workspaces.d.ts.map +1 -0
  111. package/dist/services/execution-workspaces.js +87 -0
  112. package/dist/services/execution-workspaces.js.map +1 -0
  113. package/dist/services/finance.d.ts +93 -0
  114. package/dist/services/finance.d.ts.map +1 -0
  115. package/dist/services/finance.js +120 -0
  116. package/dist/services/finance.js.map +1 -0
  117. package/dist/services/heartbeat.d.ts +11 -1
  118. package/dist/services/heartbeat.d.ts.map +1 -1
  119. package/dist/services/heartbeat.js +1292 -559
  120. package/dist/services/heartbeat.js.map +1 -1
  121. package/dist/services/index.d.ts +7 -0
  122. package/dist/services/index.d.ts.map +1 -1
  123. package/dist/services/index.js +7 -0
  124. package/dist/services/index.js.map +1 -1
  125. package/dist/services/instance-settings.d.ts +9 -0
  126. package/dist/services/instance-settings.d.ts.map +1 -0
  127. package/dist/services/instance-settings.js +80 -0
  128. package/dist/services/instance-settings.js.map +1 -0
  129. package/dist/services/issue-approvals.d.ts +1 -1
  130. package/dist/services/issues.d.ts +14 -2
  131. package/dist/services/issues.d.ts.map +1 -1
  132. package/dist/services/issues.js +161 -9
  133. package/dist/services/issues.js.map +1 -1
  134. package/dist/services/live-events.d.ts +5 -0
  135. package/dist/services/live-events.d.ts.map +1 -1
  136. package/dist/services/live-events.js +9 -0
  137. package/dist/services/live-events.js.map +1 -1
  138. package/dist/services/plugin-capability-validator.d.ts +108 -0
  139. package/dist/services/plugin-capability-validator.d.ts.map +1 -0
  140. package/dist/services/plugin-capability-validator.js +268 -0
  141. package/dist/services/plugin-capability-validator.js.map +1 -0
  142. package/dist/services/plugin-config-validator.d.ts +26 -0
  143. package/dist/services/plugin-config-validator.d.ts.map +1 -0
  144. package/dist/services/plugin-config-validator.js +41 -0
  145. package/dist/services/plugin-config-validator.js.map +1 -0
  146. package/dist/services/plugin-dev-watcher.d.ts +30 -0
  147. package/dist/services/plugin-dev-watcher.d.ts.map +1 -0
  148. package/dist/services/plugin-dev-watcher.js +241 -0
  149. package/dist/services/plugin-dev-watcher.js.map +1 -0
  150. package/dist/services/plugin-event-bus.d.ts +149 -0
  151. package/dist/services/plugin-event-bus.d.ts.map +1 -0
  152. package/dist/services/plugin-event-bus.js +258 -0
  153. package/dist/services/plugin-event-bus.js.map +1 -0
  154. package/dist/services/plugin-host-service-cleanup.d.ts +14 -0
  155. package/dist/services/plugin-host-service-cleanup.d.ts.map +1 -0
  156. package/dist/services/plugin-host-service-cleanup.js +37 -0
  157. package/dist/services/plugin-host-service-cleanup.js.map +1 -0
  158. package/dist/services/plugin-host-services.d.ts +13 -0
  159. package/dist/services/plugin-host-services.d.ts.map +1 -0
  160. package/dist/services/plugin-host-services.js +969 -0
  161. package/dist/services/plugin-host-services.js.map +1 -0
  162. package/dist/services/plugin-job-coordinator.d.ts +81 -0
  163. package/dist/services/plugin-job-coordinator.d.ts.map +1 -0
  164. package/dist/services/plugin-job-coordinator.js +172 -0
  165. package/dist/services/plugin-job-coordinator.js.map +1 -0
  166. package/dist/services/plugin-job-scheduler.d.ts +163 -0
  167. package/dist/services/plugin-job-scheduler.d.ts.map +1 -0
  168. package/dist/services/plugin-job-scheduler.js +454 -0
  169. package/dist/services/plugin-job-scheduler.js.map +1 -0
  170. package/dist/services/plugin-job-store.d.ts +208 -0
  171. package/dist/services/plugin-job-store.d.ts.map +1 -0
  172. package/dist/services/plugin-job-store.js +350 -0
  173. package/dist/services/plugin-job-store.js.map +1 -0
  174. package/dist/services/plugin-lifecycle.d.ts +203 -0
  175. package/dist/services/plugin-lifecycle.d.ts.map +1 -0
  176. package/dist/services/plugin-lifecycle.js +476 -0
  177. package/dist/services/plugin-lifecycle.js.map +1 -0
  178. package/dist/services/plugin-loader.d.ts +441 -0
  179. package/dist/services/plugin-loader.d.ts.map +1 -0
  180. package/dist/services/plugin-loader.js +1192 -0
  181. package/dist/services/plugin-loader.js.map +1 -0
  182. package/dist/services/plugin-log-retention.d.ts +20 -0
  183. package/dist/services/plugin-log-retention.d.ts.map +1 -0
  184. package/dist/services/plugin-log-retention.js +63 -0
  185. package/dist/services/plugin-log-retention.js.map +1 -0
  186. package/dist/services/plugin-manifest-validator.d.ts +79 -0
  187. package/dist/services/plugin-manifest-validator.d.ts.map +1 -0
  188. package/dist/services/plugin-manifest-validator.js +84 -0
  189. package/dist/services/plugin-manifest-validator.js.map +1 -0
  190. package/dist/services/plugin-registry.d.ts +2542 -0
  191. package/dist/services/plugin-registry.d.ts.map +1 -0
  192. package/dist/services/plugin-registry.js +539 -0
  193. package/dist/services/plugin-registry.js.map +1 -0
  194. package/dist/services/plugin-runtime-sandbox.d.ts +40 -0
  195. package/dist/services/plugin-runtime-sandbox.d.ts.map +1 -0
  196. package/dist/services/plugin-runtime-sandbox.js +154 -0
  197. package/dist/services/plugin-runtime-sandbox.js.map +1 -0
  198. package/dist/services/plugin-secrets-handler.d.ts +81 -0
  199. package/dist/services/plugin-secrets-handler.d.ts.map +1 -0
  200. package/dist/services/plugin-secrets-handler.js +275 -0
  201. package/dist/services/plugin-secrets-handler.js.map +1 -0
  202. package/dist/services/plugin-state-store.d.ts +92 -0
  203. package/dist/services/plugin-state-store.d.ts.map +1 -0
  204. package/dist/services/plugin-state-store.js +190 -0
  205. package/dist/services/plugin-state-store.js.map +1 -0
  206. package/dist/services/plugin-stream-bus.d.ts +29 -0
  207. package/dist/services/plugin-stream-bus.d.ts.map +1 -0
  208. package/dist/services/plugin-stream-bus.js +48 -0
  209. package/dist/services/plugin-stream-bus.js.map +1 -0
  210. package/dist/services/plugin-tool-dispatcher.d.ts +180 -0
  211. package/dist/services/plugin-tool-dispatcher.d.ts.map +1 -0
  212. package/dist/services/plugin-tool-dispatcher.js +224 -0
  213. package/dist/services/plugin-tool-dispatcher.js.map +1 -0
  214. package/dist/services/plugin-tool-registry.d.ts +192 -0
  215. package/dist/services/plugin-tool-registry.d.ts.map +1 -0
  216. package/dist/services/plugin-tool-registry.js +224 -0
  217. package/dist/services/plugin-tool-registry.js.map +1 -0
  218. package/dist/services/plugin-worker-manager.d.ts +260 -0
  219. package/dist/services/plugin-worker-manager.d.ts.map +1 -0
  220. package/dist/services/plugin-worker-manager.js +835 -0
  221. package/dist/services/plugin-worker-manager.js.map +1 -0
  222. package/dist/services/projects.d.ts +12 -1
  223. package/dist/services/projects.d.ts.map +1 -1
  224. package/dist/services/projects.js +100 -4
  225. package/dist/services/projects.js.map +1 -1
  226. package/dist/services/quota-windows.d.ts +9 -0
  227. package/dist/services/quota-windows.d.ts.map +1 -0
  228. package/dist/services/quota-windows.js +56 -0
  229. package/dist/services/quota-windows.js.map +1 -0
  230. package/dist/services/secrets.d.ts +5 -5
  231. package/dist/services/work-products.d.ts +14 -0
  232. package/dist/services/work-products.d.ts.map +1 -0
  233. package/dist/services/work-products.js +100 -0
  234. package/dist/services/work-products.js.map +1 -0
  235. package/dist/services/workspace-operation-log-store.d.ts +33 -0
  236. package/dist/services/workspace-operation-log-store.d.ts.map +1 -0
  237. package/dist/services/workspace-operation-log-store.js +110 -0
  238. package/dist/services/workspace-operation-log-store.js.map +1 -0
  239. package/dist/services/workspace-operations.d.ts +44 -0
  240. package/dist/services/workspace-operations.d.ts.map +1 -0
  241. package/dist/services/workspace-operations.js +204 -0
  242. package/dist/services/workspace-operations.js.map +1 -0
  243. package/dist/services/workspace-runtime.d.ts +43 -5
  244. package/dist/services/workspace-runtime.d.ts.map +1 -1
  245. package/dist/services/workspace-runtime.js +413 -30
  246. package/dist/services/workspace-runtime.js.map +1 -1
  247. package/dist/ui-branding.d.ts +10 -1
  248. package/dist/ui-branding.d.ts.map +1 -1
  249. package/dist/ui-branding.js +164 -14
  250. package/dist/ui-branding.js.map +1 -1
  251. package/dist/version.d.ts +2 -0
  252. package/dist/version.d.ts.map +1 -0
  253. package/dist/version.js +5 -0
  254. package/dist/version.js.map +1 -0
  255. package/package.json +32 -14
  256. package/skills/paperclip/SKILL.md +34 -20
  257. package/skills/paperclip-create-plugin/SKILL.md +101 -0
  258. package/ui-dist/assets/{_basePickBy-B0xbZITw.js → _basePickBy-Chytvk8p.js} +1 -1
  259. package/ui-dist/assets/{_baseUniq-Cfd5u3qc.js → _baseUniq-BVL8b2zA.js} +1 -1
  260. package/ui-dist/assets/{arc-7Xbu8tBF.js → arc-D5hJS7gD.js} +1 -1
  261. package/ui-dist/assets/{architectureDiagram-VXUJARFQ-C8F7ZRYc.js → architectureDiagram-VXUJARFQ-BFq3iN58.js} +1 -1
  262. package/ui-dist/assets/{blockDiagram-VD42YOAC-Dp08a65A.js → blockDiagram-VD42YOAC-B69ok7vL.js} +1 -1
  263. package/ui-dist/assets/{c4Diagram-YG6GDRKO-BJfslTgZ.js → c4Diagram-YG6GDRKO-FE0D9aiA.js} +1 -1
  264. package/ui-dist/assets/channel-CiKAknwU.js +1 -0
  265. package/ui-dist/assets/{chunk-4BX2VUAB-BhaYWH7e.js → chunk-4BX2VUAB-SAhF6g-l.js} +1 -1
  266. package/ui-dist/assets/{chunk-55IACEB6-BOzJUsYW.js → chunk-55IACEB6-yxKD70bX.js} +1 -1
  267. package/ui-dist/assets/{chunk-B4BG7PRW-CbVcziyE.js → chunk-B4BG7PRW-qB4VaAIx.js} +1 -1
  268. package/ui-dist/assets/{chunk-DI55MBZ5-DghPtP3y.js → chunk-DI55MBZ5-B4NIq1Ee.js} +1 -1
  269. package/ui-dist/assets/{chunk-FMBD7UC4-CB095Kfn.js → chunk-FMBD7UC4-B5kLoRe2.js} +1 -1
  270. package/ui-dist/assets/{chunk-QN33PNHL-DfpzfDqJ.js → chunk-QN33PNHL-BVVG2n_X.js} +1 -1
  271. package/ui-dist/assets/{chunk-QZHKN3VN-Df3d5z1y.js → chunk-QZHKN3VN-ncMubclp.js} +1 -1
  272. package/ui-dist/assets/{chunk-TZMSLE5B-B6tX6bZI.js → chunk-TZMSLE5B-DSyjQk8C.js} +1 -1
  273. package/ui-dist/assets/classDiagram-2ON5EDUG-B93oQX02.js +1 -0
  274. package/ui-dist/assets/classDiagram-v2-WZHVMYZB-B93oQX02.js +1 -0
  275. package/ui-dist/assets/clone-Dld-3yq0.js +1 -0
  276. package/ui-dist/assets/{cose-bilkent-S5V4N54A-38wg_s9V.js → cose-bilkent-S5V4N54A-BAikNewC.js} +1 -1
  277. package/ui-dist/assets/{dagre-6UL2VRFP-DPVj3XLS.js → dagre-6UL2VRFP-j223UI8_.js} +1 -1
  278. package/ui-dist/assets/{diagram-PSM6KHXK-BJh89zUA.js → diagram-PSM6KHXK-Bhg27ZsN.js} +1 -1
  279. package/ui-dist/assets/{diagram-QEK2KX5R-Bw0m_j10.js → diagram-QEK2KX5R-BmtcrTHq.js} +1 -1
  280. package/ui-dist/assets/{diagram-S2PKOQOG-D7_M2F3u.js → diagram-S2PKOQOG-x8XuP7dH.js} +1 -1
  281. package/ui-dist/assets/{erDiagram-Q2GNP2WA-CLsjqTWP.js → erDiagram-Q2GNP2WA-Dc8mHvB-.js} +1 -1
  282. package/ui-dist/assets/{flowDiagram-NV44I4VS-BZZ7ezVB.js → flowDiagram-NV44I4VS-Br08v6xs.js} +1 -1
  283. package/ui-dist/assets/{ganttDiagram-JELNMOA3-KzsFuwBt.js → ganttDiagram-JELNMOA3-CksKhiV8.js} +1 -1
  284. package/ui-dist/assets/{gitGraphDiagram-V2S2FVAM-DSHtY7Vu.js → gitGraphDiagram-V2S2FVAM-DmitztAD.js} +1 -1
  285. package/ui-dist/assets/{graph-luIG1UAS.js → graph-Xzh7bNfz.js} +1 -1
  286. package/ui-dist/assets/{index-nFjLambq.js → index-B-MPJNeS.js} +1 -1
  287. package/ui-dist/assets/{index-RUBFVv6t.js → index-BAOC35TS.js} +1 -1
  288. package/ui-dist/assets/{index-CaR9XM4h.js → index-BCtrpKdJ.js} +1 -1
  289. package/ui-dist/assets/{index-itc7BfMy.js → index-B_mlwDCG.js} +1 -1
  290. package/ui-dist/assets/{index-1gX09-Fl.js → index-Bp8oJMwx.js} +1 -1
  291. package/ui-dist/assets/{index-B_3g3Rie.js → index-BqOfMG1d.js} +1 -1
  292. package/ui-dist/assets/{index-uR3zjYaD.js → index-C0hw7cTu.js} +1 -1
  293. package/ui-dist/assets/{index-Ds7vLTSK.js → index-C1ENEd6Y.js} +1 -1
  294. package/ui-dist/assets/{index-CeDCs_2i.js → index-CGB1OHQi.js} +1 -1
  295. package/ui-dist/assets/{index-DBy0vJy3.js → index-CZlfAt7E.js} +1 -1
  296. package/ui-dist/assets/{index-00kuG4sI.js → index-CeL-QouZ.js} +1 -1
  297. package/ui-dist/assets/{index-Bv4xCjxl.js → index-Cz0TxJbw.js} +1 -1
  298. package/ui-dist/assets/{index-DJf8diAA.js → index-DBxUCwDG.js} +1 -1
  299. package/ui-dist/assets/{index-DnIPDZLp.js → index-DGzQUMWC.js} +1 -1
  300. package/ui-dist/assets/{index-CbGtsjW7.js → index-DI9dat7c.js} +1 -1
  301. package/ui-dist/assets/{index-BfB4lKJN.js → index-DMpo-QSZ.js} +1 -1
  302. package/ui-dist/assets/{index-DcfLFstG.js → index-DQ6xyFaN.js} +1 -1
  303. package/ui-dist/assets/{index-B1ZMzzs0.js → index-DWMQ1Fyj.js} +1 -1
  304. package/ui-dist/assets/index-DbrFgvLd.css +1 -0
  305. package/ui-dist/assets/{index-C8XyGAr9.js → index-Er4i9aLN.js} +1 -1
  306. package/ui-dist/assets/{index-C2SZYIDA.js → index-I5K-yQx5.js} +1 -1
  307. package/ui-dist/assets/index-MuodIbDJ.js +1120 -0
  308. package/ui-dist/assets/{index-BeeKMqNU.js → index-auLN8BHe.js} +1 -1
  309. package/ui-dist/assets/{index-DoTq-BeR.js → index-xw01-Pl4.js} +1 -1
  310. package/ui-dist/assets/{infoDiagram-HS3SLOUP-CDEfWpme.js → infoDiagram-HS3SLOUP-DHb4TS1W.js} +1 -1
  311. package/ui-dist/assets/{journeyDiagram-XKPGCS4Q-i7nsbg_Y.js → journeyDiagram-XKPGCS4Q-CLRBeXVU.js} +1 -1
  312. package/ui-dist/assets/{kanban-definition-3W4ZIXB7-DDlx1qVE.js → kanban-definition-3W4ZIXB7-CsQp6woE.js} +1 -1
  313. package/ui-dist/assets/{layout-C7AtmJzX.js → layout-DwD9tZ5r.js} +1 -1
  314. package/ui-dist/assets/{linear-C5jHT-WP.js → linear-DF9n_1d3.js} +1 -1
  315. package/ui-dist/assets/{mermaid.core-C8YQ4fcY.js → mermaid.core-CDm4Crkq.js} +4 -4
  316. package/ui-dist/assets/{mindmap-definition-VGOIOE7T-B9m9PuUg.js → mindmap-definition-VGOIOE7T-CVzU1x4i.js} +1 -1
  317. package/ui-dist/assets/{pieDiagram-ADFJNKIX-SvKywCSE.js → pieDiagram-ADFJNKIX-CWgl0Up8.js} +1 -1
  318. package/ui-dist/assets/{quadrantDiagram-AYHSOK5B-QfBPm7Y1.js → quadrantDiagram-AYHSOK5B-CcedyN6Q.js} +1 -1
  319. package/ui-dist/assets/{requirementDiagram-UZGBJVZJ-DnfQQuwi.js → requirementDiagram-UZGBJVZJ-DZ4BVz3s.js} +1 -1
  320. package/ui-dist/assets/{sankeyDiagram-TZEHDZUN-DvLkjzIW.js → sankeyDiagram-TZEHDZUN-CLHEx34B.js} +1 -1
  321. package/ui-dist/assets/{sequenceDiagram-WL72ISMW-5Lq2rWBc.js → sequenceDiagram-WL72ISMW-nmTJbPyy.js} +1 -1
  322. package/ui-dist/assets/{stateDiagram-FKZM4ZOC-DcYPHDyi.js → stateDiagram-FKZM4ZOC-_UGwFEqN.js} +1 -1
  323. package/ui-dist/assets/stateDiagram-v2-4FDKWEC3-CvFVoOUd.js +1 -0
  324. package/ui-dist/assets/{timeline-definition-IT6M3QCI-lrN4JmWa.js → timeline-definition-IT6M3QCI-Bibk8Daj.js} +1 -1
  325. package/ui-dist/assets/{treemap-GDKQZRPO-DFJKIBTQ.js → treemap-GDKQZRPO-Ch3rlaqT.js} +1 -1
  326. package/ui-dist/assets/{xychartDiagram-PRI3JC2R-CT47vtdm.js → xychartDiagram-PRI3JC2R-DBUudr-E.js} +1 -1
  327. package/ui-dist/index.html +4 -2
  328. package/skills/create-agent-adapter/SKILL.md +0 -718
  329. package/ui-dist/assets/channel-BViQDbSq.js +0 -1
  330. package/ui-dist/assets/classDiagram-2ON5EDUG-CjMCmxMT.js +0 -1
  331. package/ui-dist/assets/classDiagram-v2-WZHVMYZB-CjMCmxMT.js +0 -1
  332. package/ui-dist/assets/clone-Dbn9wtE1.js +0 -1
  333. package/ui-dist/assets/index-BHP9dico.js +0 -1006
  334. package/ui-dist/assets/index-BfNaDZnn.css +0 -1
  335. package/ui-dist/assets/stateDiagram-v2-4FDKWEC3-nv50YYrl.js +0 -1
@@ -1,5 +1,7 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { execFile as execFileCallback } from "node:child_process";
4
+ import { promisify } from "node:util";
3
5
  import { and, asc, desc, eq, gt, inArray, sql } from "drizzle-orm";
4
6
  import { agents, agentRuntimeState, agentTaskSessions, agentWakeupRequests, heartbeatRunEvents, heartbeatRuns, issues, projects, projectWorkspaces, } from "@paperclipai/db";
5
7
  import { conflict, notFound } from "../errors.js";
@@ -10,19 +12,91 @@ import { getServerAdapter, runningProcesses } from "../adapters/index.js";
10
12
  import { createLocalAgentJwt } from "../agent-auth-jwt.js";
11
13
  import { parseObject, asBoolean, asNumber, appendWithCap, MAX_EXCERPT_BYTES } from "../adapters/utils.js";
12
14
  import { costService } from "./costs.js";
15
+ import { budgetService } from "./budgets.js";
13
16
  import { secretService } from "./secrets.js";
14
- import { resolveDefaultAgentWorkspaceDir } from "../home-paths.js";
17
+ import { resolveDefaultAgentWorkspaceDir, resolveManagedProjectWorkspaceDir } from "../home-paths.js";
15
18
  import { summarizeHeartbeatRunResultJson } from "./heartbeat-run-summary.js";
16
- import { buildWorkspaceReadyComment, ensureRuntimeServicesForRun, persistAdapterManagedRuntimeServices, realizeExecutionWorkspace, releaseRuntimeServicesForRun, } from "./workspace-runtime.js";
19
+ import { buildWorkspaceReadyComment, cleanupExecutionWorkspaceArtifacts, ensureRuntimeServicesForRun, persistAdapterManagedRuntimeServices, realizeExecutionWorkspace, releaseRuntimeServicesForRun, sanitizeRuntimeServiceBaseEnv, } from "./workspace-runtime.js";
17
20
  import { issueService } from "./issues.js";
18
- import { buildExecutionWorkspaceAdapterConfig, parseIssueExecutionWorkspaceSettings, parseProjectExecutionWorkspacePolicy, resolveExecutionWorkspaceMode, } from "./execution-workspace-policy.js";
21
+ import { executionWorkspaceService } from "./execution-workspaces.js";
22
+ import { workspaceOperationService } from "./workspace-operations.js";
23
+ import { buildExecutionWorkspaceAdapterConfig, gateProjectExecutionWorkspacePolicy, parseIssueExecutionWorkspaceSettings, parseProjectExecutionWorkspacePolicy, resolveExecutionWorkspaceMode, } from "./execution-workspace-policy.js";
24
+ import { instanceSettingsService } from "./instance-settings.js";
19
25
  import { redactCurrentUserText, redactCurrentUserValue } from "../log-redaction.js";
26
+ import { hasSessionCompactionThresholds, resolveSessionCompactionPolicy, } from "@paperclipai/adapter-utils";
20
27
  const MAX_LIVE_LOG_CHUNK_BYTES = 8 * 1024;
21
28
  const HEARTBEAT_MAX_CONCURRENT_RUNS_DEFAULT = 1;
22
29
  const HEARTBEAT_MAX_CONCURRENT_RUNS_MAX = 10;
23
30
  const DEFERRED_WAKE_CONTEXT_KEY = "_paperclipWakeContext";
24
31
  const startLocksByAgent = new Map();
25
32
  const REPO_ONLY_CWD_SENTINEL = "/__paperclip_repo_only__";
33
+ const MANAGED_WORKSPACE_GIT_CLONE_TIMEOUT_MS = 10 * 60 * 1000;
34
+ const execFile = promisify(execFileCallback);
35
+ const SESSIONED_LOCAL_ADAPTERS = new Set([
36
+ "claude_local",
37
+ "codex_local",
38
+ "cursor",
39
+ "gemini_local",
40
+ "opencode_local",
41
+ "pi_local",
42
+ ]);
43
+ function deriveRepoNameFromRepoUrl(repoUrl) {
44
+ const trimmed = repoUrl?.trim() ?? "";
45
+ if (!trimmed)
46
+ return null;
47
+ try {
48
+ const parsed = new URL(trimmed);
49
+ const cleanedPath = parsed.pathname.replace(/\/+$/, "");
50
+ const repoName = cleanedPath.split("/").filter(Boolean).pop()?.replace(/\.git$/i, "") ?? "";
51
+ return repoName || null;
52
+ }
53
+ catch {
54
+ return null;
55
+ }
56
+ }
57
+ async function ensureManagedProjectWorkspace(input) {
58
+ const cwd = resolveManagedProjectWorkspaceDir({
59
+ companyId: input.companyId,
60
+ projectId: input.projectId,
61
+ repoName: deriveRepoNameFromRepoUrl(input.repoUrl),
62
+ });
63
+ await fs.mkdir(path.dirname(cwd), { recursive: true });
64
+ const stats = await fs.stat(cwd).catch(() => null);
65
+ if (!input.repoUrl) {
66
+ if (!stats) {
67
+ await fs.mkdir(cwd, { recursive: true });
68
+ }
69
+ return { cwd, warning: null };
70
+ }
71
+ const gitDirExists = await fs
72
+ .stat(path.resolve(cwd, ".git"))
73
+ .then((entry) => entry.isDirectory())
74
+ .catch(() => false);
75
+ if (gitDirExists) {
76
+ return { cwd, warning: null };
77
+ }
78
+ if (stats) {
79
+ const entries = await fs.readdir(cwd).catch(() => []);
80
+ if (entries.length > 0) {
81
+ return {
82
+ cwd,
83
+ warning: `Managed workspace path "${cwd}" already exists but is not a git checkout. Using it as-is.`,
84
+ };
85
+ }
86
+ await fs.rm(cwd, { recursive: true, force: true });
87
+ }
88
+ try {
89
+ await execFile("git", ["clone", input.repoUrl, cwd], {
90
+ env: sanitizeRuntimeServiceBaseEnv(process.env),
91
+ timeout: MANAGED_WORKSPACE_GIT_CLONE_TIMEOUT_MS,
92
+ });
93
+ return { cwd, warning: null };
94
+ }
95
+ catch (error) {
96
+ const reason = error instanceof Error ? error.message : String(error);
97
+ throw new Error(`Failed to prepare managed checkout for "${input.repoUrl}" at "${cwd}": ${reason}`);
98
+ }
99
+ }
26
100
  const heartbeatRunListColumns = {
27
101
  id: heartbeatRuns.id,
28
102
  companyId: heartbeatRuns.companyId,
@@ -76,9 +150,122 @@ async function withAgentStartLock(agentId, fn) {
76
150
  }
77
151
  }
78
152
  }
153
+ export function prioritizeProjectWorkspaceCandidatesForRun(rows, preferredWorkspaceId) {
154
+ if (!preferredWorkspaceId)
155
+ return rows;
156
+ const preferredIndex = rows.findIndex((row) => row.id === preferredWorkspaceId);
157
+ if (preferredIndex <= 0)
158
+ return rows;
159
+ return [rows[preferredIndex], ...rows.slice(0, preferredIndex), ...rows.slice(preferredIndex + 1)];
160
+ }
79
161
  function readNonEmptyString(value) {
80
162
  return typeof value === "string" && value.trim().length > 0 ? value : null;
81
163
  }
164
+ function normalizeLedgerBillingType(value) {
165
+ const raw = readNonEmptyString(value);
166
+ switch (raw) {
167
+ case "api":
168
+ case "metered_api":
169
+ return "metered_api";
170
+ case "subscription":
171
+ case "subscription_included":
172
+ return "subscription_included";
173
+ case "subscription_overage":
174
+ return "subscription_overage";
175
+ case "credits":
176
+ return "credits";
177
+ case "fixed":
178
+ return "fixed";
179
+ default:
180
+ return "unknown";
181
+ }
182
+ }
183
+ function resolveLedgerBiller(result) {
184
+ return readNonEmptyString(result.biller) ?? readNonEmptyString(result.provider) ?? "unknown";
185
+ }
186
+ function normalizeBilledCostCents(costUsd, billingType) {
187
+ if (billingType === "subscription_included")
188
+ return 0;
189
+ if (typeof costUsd !== "number" || !Number.isFinite(costUsd))
190
+ return 0;
191
+ return Math.max(0, Math.round(costUsd * 100));
192
+ }
193
+ async function resolveLedgerScopeForRun(db, companyId, run) {
194
+ const context = parseObject(run.contextSnapshot);
195
+ const contextIssueId = readNonEmptyString(context.issueId);
196
+ const contextProjectId = readNonEmptyString(context.projectId);
197
+ if (!contextIssueId) {
198
+ return {
199
+ issueId: null,
200
+ projectId: contextProjectId,
201
+ };
202
+ }
203
+ const issue = await db
204
+ .select({
205
+ id: issues.id,
206
+ projectId: issues.projectId,
207
+ })
208
+ .from(issues)
209
+ .where(and(eq(issues.id, contextIssueId), eq(issues.companyId, companyId)))
210
+ .then((rows) => rows[0] ?? null);
211
+ return {
212
+ issueId: issue?.id ?? null,
213
+ projectId: issue?.projectId ?? contextProjectId,
214
+ };
215
+ }
216
+ function normalizeUsageTotals(usage) {
217
+ if (!usage)
218
+ return null;
219
+ return {
220
+ inputTokens: Math.max(0, Math.floor(asNumber(usage.inputTokens, 0))),
221
+ cachedInputTokens: Math.max(0, Math.floor(asNumber(usage.cachedInputTokens, 0))),
222
+ outputTokens: Math.max(0, Math.floor(asNumber(usage.outputTokens, 0))),
223
+ };
224
+ }
225
+ function readRawUsageTotals(usageJson) {
226
+ const parsed = parseObject(usageJson);
227
+ if (Object.keys(parsed).length === 0)
228
+ return null;
229
+ const inputTokens = Math.max(0, Math.floor(asNumber(parsed.rawInputTokens, asNumber(parsed.inputTokens, 0))));
230
+ const cachedInputTokens = Math.max(0, Math.floor(asNumber(parsed.rawCachedInputTokens, asNumber(parsed.cachedInputTokens, 0))));
231
+ const outputTokens = Math.max(0, Math.floor(asNumber(parsed.rawOutputTokens, asNumber(parsed.outputTokens, 0))));
232
+ if (inputTokens <= 0 && cachedInputTokens <= 0 && outputTokens <= 0) {
233
+ return null;
234
+ }
235
+ return {
236
+ inputTokens,
237
+ cachedInputTokens,
238
+ outputTokens,
239
+ };
240
+ }
241
+ function deriveNormalizedUsageDelta(current, previous) {
242
+ if (!current)
243
+ return null;
244
+ if (!previous)
245
+ return { ...current };
246
+ const inputTokens = current.inputTokens >= previous.inputTokens
247
+ ? current.inputTokens - previous.inputTokens
248
+ : current.inputTokens;
249
+ const cachedInputTokens = current.cachedInputTokens >= previous.cachedInputTokens
250
+ ? current.cachedInputTokens - previous.cachedInputTokens
251
+ : current.cachedInputTokens;
252
+ const outputTokens = current.outputTokens >= previous.outputTokens
253
+ ? current.outputTokens - previous.outputTokens
254
+ : current.outputTokens;
255
+ return {
256
+ inputTokens: Math.max(0, inputTokens),
257
+ cachedInputTokens: Math.max(0, cachedInputTokens),
258
+ outputTokens: Math.max(0, outputTokens),
259
+ };
260
+ }
261
+ function formatCount(value) {
262
+ if (typeof value !== "number" || !Number.isFinite(value))
263
+ return "0";
264
+ return value.toLocaleString("en-US");
265
+ }
266
+ export function parseSessionCompactionPolicy(agent) {
267
+ return resolveSessionCompactionPolicy(agent.adapterType, agent.runtimeConfig).policy;
268
+ }
82
269
  export function resolveRuntimeSessionParamsForWorkspace(input) {
83
270
  const { agentId, previousSessionParams, resolvedWorkspace } = input;
84
271
  const previousSessionId = readNonEmptyString(previousSessionParams?.sessionId);
@@ -164,26 +351,19 @@ function deriveTaskKey(contextSnapshot, payload) {
164
351
  null);
165
352
  }
166
353
  export function shouldResetTaskSessionForWake(contextSnapshot) {
354
+ if (contextSnapshot?.forceFreshSession === true)
355
+ return true;
167
356
  const wakeReason = readNonEmptyString(contextSnapshot?.wakeReason);
168
357
  if (wakeReason === "issue_assigned")
169
358
  return true;
170
- const wakeSource = readNonEmptyString(contextSnapshot?.wakeSource);
171
- if (wakeSource === "timer")
172
- return true;
173
- const wakeTriggerDetail = readNonEmptyString(contextSnapshot?.wakeTriggerDetail);
174
- return wakeSource === "on_demand" && wakeTriggerDetail === "manual";
359
+ return false;
175
360
  }
176
361
  function describeSessionResetReason(contextSnapshot) {
362
+ if (contextSnapshot?.forceFreshSession === true)
363
+ return "forceFreshSession was requested";
177
364
  const wakeReason = readNonEmptyString(contextSnapshot?.wakeReason);
178
365
  if (wakeReason === "issue_assigned")
179
366
  return "wake reason is issue_assigned";
180
- const wakeSource = readNonEmptyString(contextSnapshot?.wakeSource);
181
- if (wakeSource === "timer")
182
- return "wake source is timer";
183
- const wakeTriggerDetail = readNonEmptyString(contextSnapshot?.wakeTriggerDetail);
184
- if (wakeSource === "on_demand" && wakeTriggerDetail === "manual") {
185
- return "this is a manual invoke";
186
- }
187
367
  return null;
188
368
  }
189
369
  function deriveCommentId(contextSnapshot, payload) {
@@ -328,9 +508,17 @@ function resolveNextSessionState(input) {
328
508
  };
329
509
  }
330
510
  export function heartbeatService(db) {
511
+ const instanceSettings = instanceSettingsService(db);
331
512
  const runLogStore = getRunLogStore();
332
513
  const secretsSvc = secretService(db);
333
514
  const issuesSvc = issueService(db);
515
+ const executionWorkspacesSvc = executionWorkspaceService(db);
516
+ const workspaceOperationsSvc = workspaceOperationService(db);
517
+ const activeRunExecutions = new Set();
518
+ const budgetHooks = {
519
+ cancelWorkForScope: cancelBudgetScopeWork,
520
+ };
521
+ const budgets = budgetService(db, budgetHooks);
334
522
  async function getAgent(agentId) {
335
523
  return db
336
524
  .select()
@@ -359,6 +547,143 @@ export function heartbeatService(db) {
359
547
  .where(and(eq(agentTaskSessions.companyId, companyId), eq(agentTaskSessions.agentId, agentId), eq(agentTaskSessions.adapterType, adapterType), eq(agentTaskSessions.taskKey, taskKey)))
360
548
  .then((rows) => rows[0] ?? null);
361
549
  }
550
+ async function getLatestRunForSession(agentId, sessionId, opts) {
551
+ const conditions = [
552
+ eq(heartbeatRuns.agentId, agentId),
553
+ eq(heartbeatRuns.sessionIdAfter, sessionId),
554
+ ];
555
+ if (opts?.excludeRunId) {
556
+ conditions.push(sql `${heartbeatRuns.id} <> ${opts.excludeRunId}`);
557
+ }
558
+ return db
559
+ .select()
560
+ .from(heartbeatRuns)
561
+ .where(and(...conditions))
562
+ .orderBy(desc(heartbeatRuns.createdAt))
563
+ .limit(1)
564
+ .then((rows) => rows[0] ?? null);
565
+ }
566
+ async function getOldestRunForSession(agentId, sessionId) {
567
+ return db
568
+ .select({
569
+ id: heartbeatRuns.id,
570
+ createdAt: heartbeatRuns.createdAt,
571
+ })
572
+ .from(heartbeatRuns)
573
+ .where(and(eq(heartbeatRuns.agentId, agentId), eq(heartbeatRuns.sessionIdAfter, sessionId)))
574
+ .orderBy(asc(heartbeatRuns.createdAt), asc(heartbeatRuns.id))
575
+ .limit(1)
576
+ .then((rows) => rows[0] ?? null);
577
+ }
578
+ async function resolveNormalizedUsageForSession(input) {
579
+ const { agentId, runId, sessionId, rawUsage } = input;
580
+ if (!sessionId || !rawUsage) {
581
+ return {
582
+ normalizedUsage: rawUsage,
583
+ previousRawUsage: null,
584
+ derivedFromSessionTotals: false,
585
+ };
586
+ }
587
+ const previousRun = await getLatestRunForSession(agentId, sessionId, { excludeRunId: runId });
588
+ const previousRawUsage = readRawUsageTotals(previousRun?.usageJson);
589
+ return {
590
+ normalizedUsage: deriveNormalizedUsageDelta(rawUsage, previousRawUsage),
591
+ previousRawUsage,
592
+ derivedFromSessionTotals: previousRawUsage !== null,
593
+ };
594
+ }
595
+ async function evaluateSessionCompaction(input) {
596
+ const { agent, sessionId, issueId } = input;
597
+ if (!sessionId) {
598
+ return {
599
+ rotate: false,
600
+ reason: null,
601
+ handoffMarkdown: null,
602
+ previousRunId: null,
603
+ };
604
+ }
605
+ const policy = parseSessionCompactionPolicy(agent);
606
+ if (!policy.enabled || !hasSessionCompactionThresholds(policy)) {
607
+ return {
608
+ rotate: false,
609
+ reason: null,
610
+ handoffMarkdown: null,
611
+ previousRunId: null,
612
+ };
613
+ }
614
+ const fetchLimit = Math.max(policy.maxSessionRuns > 0 ? policy.maxSessionRuns + 1 : 0, 4);
615
+ const runs = await db
616
+ .select({
617
+ id: heartbeatRuns.id,
618
+ createdAt: heartbeatRuns.createdAt,
619
+ usageJson: heartbeatRuns.usageJson,
620
+ resultJson: heartbeatRuns.resultJson,
621
+ error: heartbeatRuns.error,
622
+ })
623
+ .from(heartbeatRuns)
624
+ .where(and(eq(heartbeatRuns.agentId, agent.id), eq(heartbeatRuns.sessionIdAfter, sessionId)))
625
+ .orderBy(desc(heartbeatRuns.createdAt))
626
+ .limit(fetchLimit);
627
+ if (runs.length === 0) {
628
+ return {
629
+ rotate: false,
630
+ reason: null,
631
+ handoffMarkdown: null,
632
+ previousRunId: null,
633
+ };
634
+ }
635
+ const latestRun = runs[0] ?? null;
636
+ const oldestRun = policy.maxSessionAgeHours > 0
637
+ ? await getOldestRunForSession(agent.id, sessionId)
638
+ : runs[runs.length - 1] ?? latestRun;
639
+ const latestRawUsage = readRawUsageTotals(latestRun?.usageJson);
640
+ const sessionAgeHours = latestRun && oldestRun
641
+ ? Math.max(0, (new Date(latestRun.createdAt).getTime() - new Date(oldestRun.createdAt).getTime()) / (1000 * 60 * 60))
642
+ : 0;
643
+ let reason = null;
644
+ if (policy.maxSessionRuns > 0 && runs.length > policy.maxSessionRuns) {
645
+ reason = `session exceeded ${policy.maxSessionRuns} runs`;
646
+ }
647
+ else if (policy.maxRawInputTokens > 0 &&
648
+ latestRawUsage &&
649
+ latestRawUsage.inputTokens >= policy.maxRawInputTokens) {
650
+ reason =
651
+ `session raw input reached ${formatCount(latestRawUsage.inputTokens)} tokens ` +
652
+ `(threshold ${formatCount(policy.maxRawInputTokens)})`;
653
+ }
654
+ else if (policy.maxSessionAgeHours > 0 && sessionAgeHours >= policy.maxSessionAgeHours) {
655
+ reason = `session age reached ${Math.floor(sessionAgeHours)} hours`;
656
+ }
657
+ if (!reason || !latestRun) {
658
+ return {
659
+ rotate: false,
660
+ reason: null,
661
+ handoffMarkdown: null,
662
+ previousRunId: latestRun?.id ?? null,
663
+ };
664
+ }
665
+ const latestSummary = summarizeHeartbeatRunResultJson(latestRun.resultJson);
666
+ const latestTextSummary = readNonEmptyString(latestSummary?.summary) ??
667
+ readNonEmptyString(latestSummary?.result) ??
668
+ readNonEmptyString(latestSummary?.message) ??
669
+ readNonEmptyString(latestRun.error);
670
+ const handoffMarkdown = [
671
+ "Paperclip session handoff:",
672
+ `- Previous session: ${sessionId}`,
673
+ issueId ? `- Issue: ${issueId}` : "",
674
+ `- Rotation reason: ${reason}`,
675
+ latestTextSummary ? `- Last run summary: ${latestTextSummary}` : "",
676
+ "Continue from the current task state. Rebuild only the minimum context you need.",
677
+ ]
678
+ .filter(Boolean)
679
+ .join("\n");
680
+ return {
681
+ rotate: true,
682
+ reason,
683
+ handoffMarkdown,
684
+ previousRunId: latestRun.id,
685
+ };
686
+ }
362
687
  async function resolveSessionBeforeForWakeup(agent, taskKey) {
363
688
  if (taskKey) {
364
689
  const codec = getAdapterSessionCodec(agent.adapterType);
@@ -374,23 +699,30 @@ export function heartbeatService(db) {
374
699
  async function resolveWorkspaceForRun(agent, context, previousSessionParams, opts) {
375
700
  const issueId = readNonEmptyString(context.issueId);
376
701
  const contextProjectId = readNonEmptyString(context.projectId);
377
- const issueProjectId = issueId
702
+ const contextProjectWorkspaceId = readNonEmptyString(context.projectWorkspaceId);
703
+ const issueProjectRef = issueId
378
704
  ? await db
379
- .select({ projectId: issues.projectId })
705
+ .select({
706
+ projectId: issues.projectId,
707
+ projectWorkspaceId: issues.projectWorkspaceId,
708
+ })
380
709
  .from(issues)
381
710
  .where(and(eq(issues.id, issueId), eq(issues.companyId, agent.companyId)))
382
- .then((rows) => rows[0]?.projectId ?? null)
711
+ .then((rows) => rows[0] ?? null)
383
712
  : null;
713
+ const issueProjectId = issueProjectRef?.projectId ?? null;
714
+ const preferredProjectWorkspaceId = issueProjectRef?.projectWorkspaceId ?? contextProjectWorkspaceId ?? null;
384
715
  const resolvedProjectId = issueProjectId ?? contextProjectId;
385
716
  const useProjectWorkspace = opts?.useProjectWorkspace !== false;
386
717
  const workspaceProjectId = useProjectWorkspace ? resolvedProjectId : null;
387
- const projectWorkspaceRows = workspaceProjectId
718
+ const unorderedProjectWorkspaceRows = workspaceProjectId
388
719
  ? await db
389
720
  .select()
390
721
  .from(projectWorkspaces)
391
722
  .where(and(eq(projectWorkspaces.companyId, agent.companyId), eq(projectWorkspaces.projectId, workspaceProjectId)))
392
723
  .orderBy(asc(projectWorkspaces.createdAt), asc(projectWorkspaces.id))
393
724
  : [];
725
+ const projectWorkspaceRows = prioritizeProjectWorkspaceCandidatesForRun(unorderedProjectWorkspaceRows, preferredProjectWorkspaceId);
394
726
  const workspaceHints = projectWorkspaceRows.map((workspace) => ({
395
727
  workspaceId: workspace.id,
396
728
  cwd: readNonEmptyString(workspace.cwd),
@@ -398,12 +730,35 @@ export function heartbeatService(db) {
398
730
  repoRef: readNonEmptyString(workspace.repoRef),
399
731
  }));
400
732
  if (projectWorkspaceRows.length > 0) {
733
+ const preferredWorkspace = preferredProjectWorkspaceId
734
+ ? projectWorkspaceRows.find((workspace) => workspace.id === preferredProjectWorkspaceId) ?? null
735
+ : null;
401
736
  const missingProjectCwds = [];
402
737
  let hasConfiguredProjectCwd = false;
738
+ let preferredWorkspaceWarning = null;
739
+ if (preferredProjectWorkspaceId && !preferredWorkspace) {
740
+ preferredWorkspaceWarning =
741
+ `Selected project workspace "${preferredProjectWorkspaceId}" is not available on this project.`;
742
+ }
403
743
  for (const workspace of projectWorkspaceRows) {
404
- const projectCwd = readNonEmptyString(workspace.cwd);
744
+ let projectCwd = readNonEmptyString(workspace.cwd);
745
+ let managedWorkspaceWarning = null;
405
746
  if (!projectCwd || projectCwd === REPO_ONLY_CWD_SENTINEL) {
406
- continue;
747
+ try {
748
+ const managedWorkspace = await ensureManagedProjectWorkspace({
749
+ companyId: agent.companyId,
750
+ projectId: workspaceProjectId ?? resolvedProjectId ?? workspace.projectId,
751
+ repoUrl: readNonEmptyString(workspace.repoUrl),
752
+ });
753
+ projectCwd = managedWorkspace.cwd;
754
+ managedWorkspaceWarning = managedWorkspace.warning;
755
+ }
756
+ catch (error) {
757
+ if (preferredWorkspace?.id === workspace.id) {
758
+ preferredWorkspaceWarning = error instanceof Error ? error.message : String(error);
759
+ }
760
+ continue;
761
+ }
407
762
  }
408
763
  hasConfiguredProjectCwd = true;
409
764
  const projectCwdExists = await fs
@@ -419,14 +774,21 @@ export function heartbeatService(db) {
419
774
  repoUrl: workspace.repoUrl,
420
775
  repoRef: workspace.repoRef,
421
776
  workspaceHints,
422
- warnings: [],
777
+ warnings: [preferredWorkspaceWarning, managedWorkspaceWarning].filter((value) => Boolean(value)),
423
778
  };
424
779
  }
780
+ if (preferredWorkspace?.id === workspace.id) {
781
+ preferredWorkspaceWarning =
782
+ `Selected project workspace path "${projectCwd}" is not available yet.`;
783
+ }
425
784
  missingProjectCwds.push(projectCwd);
426
785
  }
427
786
  const fallbackCwd = resolveDefaultAgentWorkspaceDir(agent.id);
428
787
  await fs.mkdir(fallbackCwd, { recursive: true });
429
788
  const warnings = [];
789
+ if (preferredWorkspaceWarning) {
790
+ warnings.push(preferredWorkspaceWarning);
791
+ }
430
792
  if (missingProjectCwds.length > 0) {
431
793
  const firstMissing = missingProjectCwds[0];
432
794
  const extraMissingCount = Math.max(0, missingProjectCwds.length - 1);
@@ -448,6 +810,23 @@ export function heartbeatService(db) {
448
810
  warnings,
449
811
  };
450
812
  }
813
+ if (workspaceProjectId) {
814
+ const managedWorkspace = await ensureManagedProjectWorkspace({
815
+ companyId: agent.companyId,
816
+ projectId: workspaceProjectId,
817
+ repoUrl: null,
818
+ });
819
+ return {
820
+ cwd: managedWorkspace.cwd,
821
+ source: "project_primary",
822
+ projectId: resolvedProjectId,
823
+ workspaceId: null,
824
+ repoUrl: null,
825
+ repoRef: null,
826
+ workspaceHints,
827
+ warnings: managedWorkspace.warning ? [managedWorkspace.warning] : [],
828
+ };
829
+ }
451
830
  const sessionCwd = readNonEmptyString(previousSessionParams?.cwd);
452
831
  if (sessionCwd) {
453
832
  const sessionCwdExists = await fs
@@ -638,6 +1017,24 @@ export function heartbeatService(db) {
638
1017
  async function claimQueuedRun(run) {
639
1018
  if (run.status !== "queued")
640
1019
  return run;
1020
+ const agent = await getAgent(run.agentId);
1021
+ if (!agent) {
1022
+ await cancelRunInternal(run.id, "Cancelled because the agent no longer exists");
1023
+ return null;
1024
+ }
1025
+ if (agent.status === "paused" || agent.status === "terminated" || agent.status === "pending_approval") {
1026
+ await cancelRunInternal(run.id, "Cancelled because the agent is not invokable");
1027
+ return null;
1028
+ }
1029
+ const context = parseObject(run.contextSnapshot);
1030
+ const budgetBlock = await budgets.getInvocationBlock(run.companyId, run.agentId, {
1031
+ issueId: readNonEmptyString(context.issueId),
1032
+ projectId: readNonEmptyString(context.projectId),
1033
+ });
1034
+ if (budgetBlock) {
1035
+ await cancelRunInternal(run.id, budgetBlock.reason);
1036
+ return null;
1037
+ }
641
1038
  const claimedAt = new Date();
642
1039
  const claimed = await db
643
1040
  .update(heartbeatRuns)
@@ -710,14 +1107,14 @@ export function heartbeatService(db) {
710
1107
  async function reapOrphanedRuns(opts) {
711
1108
  const staleThresholdMs = opts?.staleThresholdMs ?? 0;
712
1109
  const now = new Date();
713
- // Find all runs in "queued" or "running" state
1110
+ // Find all runs stuck in "running" state (queued runs are legitimately waiting; resumeQueuedRuns handles them)
714
1111
  const activeRuns = await db
715
1112
  .select()
716
1113
  .from(heartbeatRuns)
717
- .where(inArray(heartbeatRuns.status, ["queued", "running"]));
1114
+ .where(eq(heartbeatRuns.status, "running"));
718
1115
  const reaped = [];
719
1116
  for (const run of activeRuns) {
720
- if (runningProcesses.has(run.id))
1117
+ if (runningProcesses.has(run.id) || activeRunExecutions.has(run.id))
721
1118
  continue;
722
1119
  // Apply staleness threshold to avoid false positives
723
1120
  if (staleThresholdMs > 0) {
@@ -754,14 +1151,28 @@ export function heartbeatService(db) {
754
1151
  }
755
1152
  return { reaped: reaped.length, runIds: reaped };
756
1153
  }
757
- async function updateRuntimeState(agent, run, result, session) {
1154
+ async function resumeQueuedRuns() {
1155
+ const queuedRuns = await db
1156
+ .select({ agentId: heartbeatRuns.agentId })
1157
+ .from(heartbeatRuns)
1158
+ .where(eq(heartbeatRuns.status, "queued"));
1159
+ const agentIds = [...new Set(queuedRuns.map((r) => r.agentId))];
1160
+ for (const agentId of agentIds) {
1161
+ await startNextQueuedRunForAgent(agentId);
1162
+ }
1163
+ }
1164
+ async function updateRuntimeState(agent, run, result, session, normalizedUsage) {
758
1165
  await ensureRuntimeState(agent);
759
- const usage = result.usage;
1166
+ const usage = normalizedUsage ?? normalizeUsageTotals(result.usage);
760
1167
  const inputTokens = usage?.inputTokens ?? 0;
761
1168
  const outputTokens = usage?.outputTokens ?? 0;
762
1169
  const cachedInputTokens = usage?.cachedInputTokens ?? 0;
763
- const additionalCostCents = Math.max(0, Math.round((result.costUsd ?? 0) * 100));
1170
+ const billingType = normalizeLedgerBillingType(result.billingType);
1171
+ const additionalCostCents = normalizeBilledCostCents(result.costUsd, billingType);
764
1172
  const hasTokenUsage = inputTokens > 0 || outputTokens > 0 || cachedInputTokens > 0;
1173
+ const provider = result.provider ?? "unknown";
1174
+ const biller = resolveLedgerBiller(result);
1175
+ const ledgerScope = await resolveLedgerScopeForRun(db, agent.companyId, run);
765
1176
  await db
766
1177
  .update(agentRuntimeState)
767
1178
  .set({
@@ -778,12 +1189,18 @@ export function heartbeatService(db) {
778
1189
  })
779
1190
  .where(eq(agentRuntimeState.agentId, agent.id));
780
1191
  if (additionalCostCents > 0 || hasTokenUsage) {
781
- const costs = costService(db);
1192
+ const costs = costService(db, budgetHooks);
782
1193
  await costs.createEvent(agent.companyId, {
1194
+ heartbeatRunId: run.id,
783
1195
  agentId: agent.id,
784
- provider: result.provider ?? "unknown",
1196
+ issueId: ledgerScope.issueId,
1197
+ projectId: ledgerScope.projectId,
1198
+ provider,
1199
+ biller,
1200
+ billingType,
785
1201
  model: result.model ?? "unknown",
786
1202
  inputTokens,
1203
+ cachedInputTokens,
787
1204
  outputTokens,
788
1205
  costCents: additionalCostCents,
789
1206
  occurredAt: new Date(),
@@ -795,6 +1212,9 @@ export function heartbeatService(db) {
795
1212
  const agent = await getAgent(agentId);
796
1213
  if (!agent)
797
1214
  return [];
1215
+ if (agent.status === "paused" || agent.status === "terminated" || agent.status === "pending_approval") {
1216
+ return [];
1217
+ }
798
1218
  const policy = parseHeartbeatPolicy(agent);
799
1219
  const runningCount = await countRunningRunsForAgent(agentId);
800
1220
  const availableSlots = Math.max(0, policy.maxConcurrentRuns - runningCount);
@@ -838,273 +1258,234 @@ export function heartbeatService(db) {
838
1258
  }
839
1259
  run = claimed;
840
1260
  }
841
- const agent = await getAgent(run.agentId);
842
- if (!agent) {
843
- await setRunStatus(runId, "failed", {
844
- error: "Agent not found",
845
- errorCode: "agent_not_found",
846
- finishedAt: new Date(),
847
- });
848
- await setWakeupStatus(run.wakeupRequestId, "failed", {
849
- finishedAt: new Date(),
850
- error: "Agent not found",
851
- });
852
- const failedRun = await getRun(runId);
853
- if (failedRun)
854
- await releaseIssueExecutionAndPromote(failedRun);
855
- return;
856
- }
857
- const runtime = await ensureRuntimeState(agent);
858
- const context = parseObject(run.contextSnapshot);
859
- const taskKey = deriveTaskKey(context, null);
860
- const sessionCodec = getAdapterSessionCodec(agent.adapterType);
861
- const issueId = readNonEmptyString(context.issueId);
862
- const issueAssigneeConfig = issueId
863
- ? await db
864
- .select({
865
- projectId: issues.projectId,
866
- assigneeAgentId: issues.assigneeAgentId,
867
- assigneeAdapterOverrides: issues.assigneeAdapterOverrides,
868
- executionWorkspaceSettings: issues.executionWorkspaceSettings,
869
- })
870
- .from(issues)
871
- .where(and(eq(issues.id, issueId), eq(issues.companyId, agent.companyId)))
872
- .then((rows) => rows[0] ?? null)
873
- : null;
874
- const issueAssigneeOverrides = issueAssigneeConfig && issueAssigneeConfig.assigneeAgentId === agent.id
875
- ? parseIssueAssigneeAdapterOverrides(issueAssigneeConfig.assigneeAdapterOverrides)
876
- : null;
877
- const issueExecutionWorkspaceSettings = parseIssueExecutionWorkspaceSettings(issueAssigneeConfig?.executionWorkspaceSettings);
878
- const contextProjectId = readNonEmptyString(context.projectId);
879
- const executionProjectId = issueAssigneeConfig?.projectId ?? contextProjectId;
880
- const projectExecutionWorkspacePolicy = executionProjectId
881
- ? await db
882
- .select({ executionWorkspacePolicy: projects.executionWorkspacePolicy })
883
- .from(projects)
884
- .where(and(eq(projects.id, executionProjectId), eq(projects.companyId, agent.companyId)))
885
- .then((rows) => parseProjectExecutionWorkspacePolicy(rows[0]?.executionWorkspacePolicy))
886
- : null;
887
- const taskSession = taskKey
888
- ? await getTaskSession(agent.companyId, agent.id, agent.adapterType, taskKey)
889
- : null;
890
- const resetTaskSession = shouldResetTaskSessionForWake(context);
891
- const sessionResetReason = describeSessionResetReason(context);
892
- const taskSessionForRun = resetTaskSession ? null : taskSession;
893
- const previousSessionParams = normalizeSessionParams(sessionCodec.deserialize(taskSessionForRun?.sessionParamsJson ?? null));
894
- const config = parseObject(agent.adapterConfig);
895
- const executionWorkspaceMode = resolveExecutionWorkspaceMode({
896
- projectPolicy: projectExecutionWorkspacePolicy,
897
- issueSettings: issueExecutionWorkspaceSettings,
898
- legacyUseProjectWorkspace: issueAssigneeOverrides?.useProjectWorkspace ?? null,
899
- });
900
- const resolvedWorkspace = await resolveWorkspaceForRun(agent, context, previousSessionParams, { useProjectWorkspace: executionWorkspaceMode !== "agent_default" });
901
- const workspaceManagedConfig = buildExecutionWorkspaceAdapterConfig({
902
- agentConfig: config,
903
- projectPolicy: projectExecutionWorkspacePolicy,
904
- issueSettings: issueExecutionWorkspaceSettings,
905
- mode: executionWorkspaceMode,
906
- legacyUseProjectWorkspace: issueAssigneeOverrides?.useProjectWorkspace ?? null,
907
- });
908
- const mergedConfig = issueAssigneeOverrides?.adapterConfig
909
- ? { ...workspaceManagedConfig, ...issueAssigneeOverrides.adapterConfig }
910
- : workspaceManagedConfig;
911
- const { config: resolvedConfig, secretKeys } = await secretsSvc.resolveAdapterConfigForRuntime(agent.companyId, mergedConfig);
912
- const issueRef = issueId
913
- ? await db
914
- .select({
915
- id: issues.id,
916
- identifier: issues.identifier,
917
- title: issues.title,
918
- })
919
- .from(issues)
920
- .where(and(eq(issues.id, issueId), eq(issues.companyId, agent.companyId)))
921
- .then((rows) => rows[0] ?? null)
922
- : null;
923
- const executionWorkspace = await realizeExecutionWorkspace({
924
- base: {
925
- baseCwd: resolvedWorkspace.cwd,
926
- source: resolvedWorkspace.source,
927
- projectId: resolvedWorkspace.projectId,
928
- workspaceId: resolvedWorkspace.workspaceId,
929
- repoUrl: resolvedWorkspace.repoUrl,
930
- repoRef: resolvedWorkspace.repoRef,
931
- },
932
- config: resolvedConfig,
933
- issue: issueRef,
934
- agent: {
935
- id: agent.id,
936
- name: agent.name,
937
- companyId: agent.companyId,
938
- },
939
- });
940
- const runtimeSessionResolution = resolveRuntimeSessionParamsForWorkspace({
941
- agentId: agent.id,
942
- previousSessionParams,
943
- resolvedWorkspace: {
944
- ...resolvedWorkspace,
945
- cwd: executionWorkspace.cwd,
946
- },
947
- });
948
- const runtimeSessionParams = runtimeSessionResolution.sessionParams;
949
- const runtimeWorkspaceWarnings = [
950
- ...resolvedWorkspace.warnings,
951
- ...executionWorkspace.warnings,
952
- ...(runtimeSessionResolution.warning ? [runtimeSessionResolution.warning] : []),
953
- ...(resetTaskSession && sessionResetReason
954
- ? [
955
- taskKey
956
- ? `Skipping saved session resume for task "${taskKey}" because ${sessionResetReason}.`
957
- : `Skipping saved session resume because ${sessionResetReason}.`,
958
- ]
959
- : []),
960
- ];
961
- context.paperclipWorkspace = {
962
- cwd: executionWorkspace.cwd,
963
- source: executionWorkspace.source,
964
- mode: executionWorkspaceMode,
965
- strategy: executionWorkspace.strategy,
966
- projectId: executionWorkspace.projectId,
967
- workspaceId: executionWorkspace.workspaceId,
968
- repoUrl: executionWorkspace.repoUrl,
969
- repoRef: executionWorkspace.repoRef,
970
- branchName: executionWorkspace.branchName,
971
- worktreePath: executionWorkspace.worktreePath,
972
- };
973
- context.paperclipWorkspaces = resolvedWorkspace.workspaceHints;
974
- const runtimeServiceIntents = (() => {
975
- const runtimeConfig = parseObject(resolvedConfig.workspaceRuntime);
976
- return Array.isArray(runtimeConfig.services)
977
- ? runtimeConfig.services.filter((value) => typeof value === "object" && value !== null)
978
- : [];
979
- })();
980
- if (runtimeServiceIntents.length > 0) {
981
- context.paperclipRuntimeServiceIntents = runtimeServiceIntents;
982
- }
983
- else {
984
- delete context.paperclipRuntimeServiceIntents;
985
- }
986
- if (executionWorkspace.projectId && !readNonEmptyString(context.projectId)) {
987
- context.projectId = executionWorkspace.projectId;
988
- }
989
- const runtimeSessionFallback = taskKey || resetTaskSession ? null : runtime.sessionId;
990
- const previousSessionDisplayId = truncateDisplayId(taskSessionForRun?.sessionDisplayId ??
991
- (sessionCodec.getDisplayId ? sessionCodec.getDisplayId(runtimeSessionParams) : null) ??
992
- readNonEmptyString(runtimeSessionParams?.sessionId) ??
993
- runtimeSessionFallback);
994
- const runtimeForAdapter = {
995
- sessionId: readNonEmptyString(runtimeSessionParams?.sessionId) ?? runtimeSessionFallback,
996
- sessionParams: runtimeSessionParams,
997
- sessionDisplayId: previousSessionDisplayId,
998
- taskKey,
999
- };
1000
- let seq = 1;
1001
- let handle = null;
1002
- let stdoutExcerpt = "";
1003
- let stderrExcerpt = "";
1261
+ activeRunExecutions.add(run.id);
1004
1262
  try {
1005
- const startedAt = run.startedAt ?? new Date();
1006
- const runningWithSession = await db
1007
- .update(heartbeatRuns)
1008
- .set({
1009
- startedAt,
1010
- sessionIdBefore: runtimeForAdapter.sessionDisplayId ?? runtimeForAdapter.sessionId,
1011
- contextSnapshot: context,
1012
- updatedAt: new Date(),
1013
- })
1014
- .where(eq(heartbeatRuns.id, run.id))
1015
- .returning()
1016
- .then((rows) => rows[0] ?? null);
1017
- if (runningWithSession)
1018
- run = runningWithSession;
1019
- const runningAgent = await db
1020
- .update(agents)
1021
- .set({ status: "running", updatedAt: new Date() })
1022
- .where(eq(agents.id, agent.id))
1023
- .returning()
1024
- .then((rows) => rows[0] ?? null);
1025
- if (runningAgent) {
1026
- publishLiveEvent({
1027
- companyId: runningAgent.companyId,
1028
- type: "agent.status",
1029
- payload: {
1030
- agentId: runningAgent.id,
1031
- status: runningAgent.status,
1032
- outcome: "running",
1033
- },
1263
+ const agent = await getAgent(run.agentId);
1264
+ if (!agent) {
1265
+ await setRunStatus(runId, "failed", {
1266
+ error: "Agent not found",
1267
+ errorCode: "agent_not_found",
1268
+ finishedAt: new Date(),
1269
+ });
1270
+ await setWakeupStatus(run.wakeupRequestId, "failed", {
1271
+ finishedAt: new Date(),
1272
+ error: "Agent not found",
1034
1273
  });
1274
+ const failedRun = await getRun(runId);
1275
+ if (failedRun)
1276
+ await releaseIssueExecutionAndPromote(failedRun);
1277
+ return;
1035
1278
  }
1036
- const currentRun = run;
1037
- await appendRunEvent(currentRun, seq++, {
1038
- eventType: "lifecycle",
1039
- stream: "system",
1040
- level: "info",
1041
- message: "run started",
1279
+ const runtime = await ensureRuntimeState(agent);
1280
+ const context = parseObject(run.contextSnapshot);
1281
+ const taskKey = deriveTaskKey(context, null);
1282
+ const sessionCodec = getAdapterSessionCodec(agent.adapterType);
1283
+ const issueId = readNonEmptyString(context.issueId);
1284
+ const issueContext = issueId
1285
+ ? await db
1286
+ .select({
1287
+ id: issues.id,
1288
+ identifier: issues.identifier,
1289
+ title: issues.title,
1290
+ projectId: issues.projectId,
1291
+ projectWorkspaceId: issues.projectWorkspaceId,
1292
+ executionWorkspaceId: issues.executionWorkspaceId,
1293
+ executionWorkspacePreference: issues.executionWorkspacePreference,
1294
+ assigneeAgentId: issues.assigneeAgentId,
1295
+ assigneeAdapterOverrides: issues.assigneeAdapterOverrides,
1296
+ executionWorkspaceSettings: issues.executionWorkspaceSettings,
1297
+ })
1298
+ .from(issues)
1299
+ .where(and(eq(issues.id, issueId), eq(issues.companyId, agent.companyId)))
1300
+ .then((rows) => rows[0] ?? null)
1301
+ : null;
1302
+ const issueAssigneeOverrides = issueContext && issueContext.assigneeAgentId === agent.id
1303
+ ? parseIssueAssigneeAdapterOverrides(issueContext.assigneeAdapterOverrides)
1304
+ : null;
1305
+ const isolatedWorkspacesEnabled = (await instanceSettings.getExperimental()).enableIsolatedWorkspaces;
1306
+ const issueExecutionWorkspaceSettings = isolatedWorkspacesEnabled
1307
+ ? parseIssueExecutionWorkspaceSettings(issueContext?.executionWorkspaceSettings)
1308
+ : null;
1309
+ const contextProjectId = readNonEmptyString(context.projectId);
1310
+ const executionProjectId = issueContext?.projectId ?? contextProjectId;
1311
+ const projectExecutionWorkspacePolicy = executionProjectId
1312
+ ? await db
1313
+ .select({ executionWorkspacePolicy: projects.executionWorkspacePolicy })
1314
+ .from(projects)
1315
+ .where(and(eq(projects.id, executionProjectId), eq(projects.companyId, agent.companyId)))
1316
+ .then((rows) => gateProjectExecutionWorkspacePolicy(parseProjectExecutionWorkspacePolicy(rows[0]?.executionWorkspacePolicy), isolatedWorkspacesEnabled))
1317
+ : null;
1318
+ const taskSession = taskKey
1319
+ ? await getTaskSession(agent.companyId, agent.id, agent.adapterType, taskKey)
1320
+ : null;
1321
+ const resetTaskSession = shouldResetTaskSessionForWake(context);
1322
+ const sessionResetReason = describeSessionResetReason(context);
1323
+ const taskSessionForRun = resetTaskSession ? null : taskSession;
1324
+ const previousSessionParams = normalizeSessionParams(sessionCodec.deserialize(taskSessionForRun?.sessionParamsJson ?? null));
1325
+ const config = parseObject(agent.adapterConfig);
1326
+ const executionWorkspaceMode = resolveExecutionWorkspaceMode({
1327
+ projectPolicy: projectExecutionWorkspacePolicy,
1328
+ issueSettings: issueExecutionWorkspaceSettings,
1329
+ legacyUseProjectWorkspace: issueAssigneeOverrides?.useProjectWorkspace ?? null,
1042
1330
  });
1043
- handle = await runLogStore.begin({
1044
- companyId: run.companyId,
1045
- agentId: run.agentId,
1046
- runId,
1331
+ const resolvedWorkspace = await resolveWorkspaceForRun(agent, context, previousSessionParams, { useProjectWorkspace: executionWorkspaceMode !== "agent_default" });
1332
+ const workspaceManagedConfig = buildExecutionWorkspaceAdapterConfig({
1333
+ agentConfig: config,
1334
+ projectPolicy: projectExecutionWorkspacePolicy,
1335
+ issueSettings: issueExecutionWorkspaceSettings,
1336
+ mode: executionWorkspaceMode,
1337
+ legacyUseProjectWorkspace: issueAssigneeOverrides?.useProjectWorkspace ?? null,
1047
1338
  });
1048
- await db
1049
- .update(heartbeatRuns)
1050
- .set({
1051
- logStore: handle.store,
1052
- logRef: handle.logRef,
1053
- updatedAt: new Date(),
1054
- })
1055
- .where(eq(heartbeatRuns.id, runId));
1056
- const onLog = async (stream, chunk) => {
1057
- const sanitizedChunk = redactCurrentUserText(chunk);
1058
- if (stream === "stdout")
1059
- stdoutExcerpt = appendExcerpt(stdoutExcerpt, sanitizedChunk);
1060
- if (stream === "stderr")
1061
- stderrExcerpt = appendExcerpt(stderrExcerpt, sanitizedChunk);
1062
- const ts = new Date().toISOString();
1063
- if (handle) {
1064
- await runLogStore.append(handle, {
1065
- stream,
1066
- chunk: sanitizedChunk,
1067
- ts,
1068
- });
1339
+ const mergedConfig = issueAssigneeOverrides?.adapterConfig
1340
+ ? { ...workspaceManagedConfig, ...issueAssigneeOverrides.adapterConfig }
1341
+ : workspaceManagedConfig;
1342
+ const { config: resolvedConfig, secretKeys } = await secretsSvc.resolveAdapterConfigForRuntime(agent.companyId, mergedConfig);
1343
+ const issueRef = issueContext
1344
+ ? {
1345
+ id: issueContext.id,
1346
+ identifier: issueContext.identifier,
1347
+ title: issueContext.title,
1348
+ projectId: issueContext.projectId,
1349
+ projectWorkspaceId: issueContext.projectWorkspaceId,
1350
+ executionWorkspaceId: issueContext.executionWorkspaceId,
1351
+ executionWorkspacePreference: issueContext.executionWorkspacePreference,
1069
1352
  }
1070
- const payloadChunk = sanitizedChunk.length > MAX_LIVE_LOG_CHUNK_BYTES
1071
- ? sanitizedChunk.slice(sanitizedChunk.length - MAX_LIVE_LOG_CHUNK_BYTES)
1072
- : sanitizedChunk;
1073
- publishLiveEvent({
1074
- companyId: run.companyId,
1075
- type: "heartbeat.run.log",
1076
- payload: {
1077
- runId: run.id,
1078
- agentId: run.agentId,
1079
- ts,
1080
- stream,
1081
- chunk: payloadChunk,
1082
- truncated: payloadChunk.length !== sanitizedChunk.length,
1083
- },
1084
- });
1085
- };
1086
- for (const warning of runtimeWorkspaceWarnings) {
1087
- await onLog("stderr", `[paperclip] ${warning}\n`);
1088
- }
1089
- const adapterEnv = Object.fromEntries(Object.entries(parseObject(resolvedConfig.env)).filter((entry) => typeof entry[0] === "string" && typeof entry[1] === "string"));
1090
- const runtimeServices = await ensureRuntimeServicesForRun({
1091
- db,
1092
- runId: run.id,
1353
+ : null;
1354
+ const existingExecutionWorkspace = issueRef?.executionWorkspaceId ? await executionWorkspacesSvc.getById(issueRef.executionWorkspaceId) : null;
1355
+ const workspaceOperationRecorder = workspaceOperationsSvc.createRecorder({
1356
+ companyId: agent.companyId,
1357
+ heartbeatRunId: run.id,
1358
+ executionWorkspaceId: existingExecutionWorkspace?.id ?? null,
1359
+ });
1360
+ const executionWorkspace = await realizeExecutionWorkspace({
1361
+ base: {
1362
+ baseCwd: resolvedWorkspace.cwd,
1363
+ source: resolvedWorkspace.source,
1364
+ projectId: resolvedWorkspace.projectId,
1365
+ workspaceId: resolvedWorkspace.workspaceId,
1366
+ repoUrl: resolvedWorkspace.repoUrl,
1367
+ repoRef: resolvedWorkspace.repoRef,
1368
+ },
1369
+ config: resolvedConfig,
1370
+ issue: issueRef,
1093
1371
  agent: {
1094
1372
  id: agent.id,
1095
1373
  name: agent.name,
1096
1374
  companyId: agent.companyId,
1097
1375
  },
1098
- issue: issueRef,
1099
- workspace: executionWorkspace,
1100
- config: resolvedConfig,
1101
- adapterEnv,
1102
- onLog,
1376
+ recorder: workspaceOperationRecorder,
1103
1377
  });
1104
- if (runtimeServices.length > 0) {
1105
- context.paperclipRuntimeServices = runtimeServices;
1106
- context.paperclipRuntimePrimaryUrl =
1107
- runtimeServices.find((service) => readNonEmptyString(service.url))?.url ?? null;
1378
+ const resolvedProjectId = executionWorkspace.projectId ?? issueRef?.projectId ?? executionProjectId ?? null;
1379
+ const resolvedProjectWorkspaceId = issueRef?.projectWorkspaceId ?? resolvedWorkspace.workspaceId ?? null;
1380
+ const shouldReuseExisting = issueRef?.executionWorkspacePreference === "reuse_existing" &&
1381
+ existingExecutionWorkspace &&
1382
+ existingExecutionWorkspace.status !== "archived";
1383
+ let persistedExecutionWorkspace = null;
1384
+ try {
1385
+ persistedExecutionWorkspace = shouldReuseExisting && existingExecutionWorkspace
1386
+ ? await executionWorkspacesSvc.update(existingExecutionWorkspace.id, {
1387
+ cwd: executionWorkspace.cwd,
1388
+ repoUrl: executionWorkspace.repoUrl,
1389
+ baseRef: executionWorkspace.repoRef,
1390
+ branchName: executionWorkspace.branchName,
1391
+ providerType: executionWorkspace.strategy === "git_worktree" ? "git_worktree" : "local_fs",
1392
+ providerRef: executionWorkspace.worktreePath,
1393
+ status: "active",
1394
+ lastUsedAt: new Date(),
1395
+ metadata: {
1396
+ ...(existingExecutionWorkspace.metadata ?? {}),
1397
+ source: executionWorkspace.source,
1398
+ createdByRuntime: executionWorkspace.created,
1399
+ },
1400
+ })
1401
+ : resolvedProjectId
1402
+ ? await executionWorkspacesSvc.create({
1403
+ companyId: agent.companyId,
1404
+ projectId: resolvedProjectId,
1405
+ projectWorkspaceId: resolvedProjectWorkspaceId,
1406
+ sourceIssueId: issueRef?.id ?? null,
1407
+ mode: executionWorkspaceMode === "isolated_workspace"
1408
+ ? "isolated_workspace"
1409
+ : executionWorkspaceMode === "operator_branch"
1410
+ ? "operator_branch"
1411
+ : executionWorkspaceMode === "agent_default"
1412
+ ? "adapter_managed"
1413
+ : "shared_workspace",
1414
+ strategyType: executionWorkspace.strategy === "git_worktree" ? "git_worktree" : "project_primary",
1415
+ name: executionWorkspace.branchName ?? issueRef?.identifier ?? `workspace-${agent.id.slice(0, 8)}`,
1416
+ status: "active",
1417
+ cwd: executionWorkspace.cwd,
1418
+ repoUrl: executionWorkspace.repoUrl,
1419
+ baseRef: executionWorkspace.repoRef,
1420
+ branchName: executionWorkspace.branchName,
1421
+ providerType: executionWorkspace.strategy === "git_worktree" ? "git_worktree" : "local_fs",
1422
+ providerRef: executionWorkspace.worktreePath,
1423
+ lastUsedAt: new Date(),
1424
+ openedAt: new Date(),
1425
+ metadata: {
1426
+ source: executionWorkspace.source,
1427
+ createdByRuntime: executionWorkspace.created,
1428
+ },
1429
+ })
1430
+ : null;
1431
+ }
1432
+ catch (error) {
1433
+ if (executionWorkspace.created) {
1434
+ try {
1435
+ await cleanupExecutionWorkspaceArtifacts({
1436
+ workspace: {
1437
+ id: existingExecutionWorkspace?.id ?? `transient-${run.id}`,
1438
+ cwd: executionWorkspace.cwd,
1439
+ providerType: executionWorkspace.strategy === "git_worktree" ? "git_worktree" : "local_fs",
1440
+ providerRef: executionWorkspace.worktreePath,
1441
+ branchName: executionWorkspace.branchName,
1442
+ repoUrl: executionWorkspace.repoUrl,
1443
+ baseRef: executionWorkspace.repoRef,
1444
+ projectId: resolvedProjectId,
1445
+ projectWorkspaceId: resolvedProjectWorkspaceId,
1446
+ sourceIssueId: issueRef?.id ?? null,
1447
+ metadata: {
1448
+ createdByRuntime: true,
1449
+ source: executionWorkspace.source,
1450
+ },
1451
+ },
1452
+ projectWorkspace: {
1453
+ cwd: resolvedWorkspace.cwd,
1454
+ cleanupCommand: null,
1455
+ },
1456
+ teardownCommand: projectExecutionWorkspacePolicy?.workspaceStrategy?.teardownCommand ?? null,
1457
+ recorder: workspaceOperationRecorder,
1458
+ });
1459
+ }
1460
+ catch (cleanupError) {
1461
+ logger.warn({
1462
+ runId: run.id,
1463
+ issueId,
1464
+ executionWorkspaceCwd: executionWorkspace.cwd,
1465
+ cleanupError: cleanupError instanceof Error ? cleanupError.message : String(cleanupError),
1466
+ }, "Failed to cleanup realized execution workspace after persistence failure");
1467
+ }
1468
+ }
1469
+ throw error;
1470
+ }
1471
+ await workspaceOperationRecorder.attachExecutionWorkspaceId(persistedExecutionWorkspace?.id ?? null);
1472
+ if (existingExecutionWorkspace &&
1473
+ persistedExecutionWorkspace &&
1474
+ existingExecutionWorkspace.id !== persistedExecutionWorkspace.id &&
1475
+ existingExecutionWorkspace.status === "active") {
1476
+ await executionWorkspacesSvc.update(existingExecutionWorkspace.id, {
1477
+ status: "idle",
1478
+ cleanupReason: null,
1479
+ });
1480
+ }
1481
+ if (issueId && persistedExecutionWorkspace && issueRef?.executionWorkspaceId !== persistedExecutionWorkspace.id) {
1482
+ await issuesSvc.update(issueId, {
1483
+ executionWorkspaceId: persistedExecutionWorkspace.id,
1484
+ ...(resolvedProjectWorkspaceId ? { projectWorkspaceId: resolvedProjectWorkspaceId } : {}),
1485
+ });
1486
+ }
1487
+ if (persistedExecutionWorkspace) {
1488
+ context.executionWorkspaceId = persistedExecutionWorkspace.id;
1108
1489
  await db
1109
1490
  .update(heartbeatRuns)
1110
1491
  .set({
@@ -1113,58 +1494,182 @@ export function heartbeatService(db) {
1113
1494
  })
1114
1495
  .where(eq(heartbeatRuns.id, run.id));
1115
1496
  }
1116
- if (issueId && (executionWorkspace.created || runtimeServices.some((service) => !service.reused))) {
1117
- try {
1118
- await issuesSvc.addComment(issueId, buildWorkspaceReadyComment({
1119
- workspace: executionWorkspace,
1120
- runtimeServices,
1121
- }), { agentId: agent.id });
1122
- }
1123
- catch (err) {
1124
- await onLog("stderr", `[paperclip] Failed to post workspace-ready comment: ${err instanceof Error ? err.message : String(err)}\n`);
1497
+ const runtimeSessionResolution = resolveRuntimeSessionParamsForWorkspace({
1498
+ agentId: agent.id,
1499
+ previousSessionParams,
1500
+ resolvedWorkspace: {
1501
+ ...resolvedWorkspace,
1502
+ cwd: executionWorkspace.cwd,
1503
+ },
1504
+ });
1505
+ const runtimeSessionParams = runtimeSessionResolution.sessionParams;
1506
+ const runtimeWorkspaceWarnings = [
1507
+ ...resolvedWorkspace.warnings,
1508
+ ...executionWorkspace.warnings,
1509
+ ...(runtimeSessionResolution.warning ? [runtimeSessionResolution.warning] : []),
1510
+ ...(resetTaskSession && sessionResetReason
1511
+ ? [
1512
+ taskKey
1513
+ ? `Skipping saved session resume for task "${taskKey}" because ${sessionResetReason}.`
1514
+ : `Skipping saved session resume because ${sessionResetReason}.`,
1515
+ ]
1516
+ : []),
1517
+ ];
1518
+ context.paperclipWorkspace = {
1519
+ cwd: executionWorkspace.cwd,
1520
+ source: executionWorkspace.source,
1521
+ mode: executionWorkspaceMode,
1522
+ strategy: executionWorkspace.strategy,
1523
+ projectId: executionWorkspace.projectId,
1524
+ workspaceId: executionWorkspace.workspaceId,
1525
+ repoUrl: executionWorkspace.repoUrl,
1526
+ repoRef: executionWorkspace.repoRef,
1527
+ branchName: executionWorkspace.branchName,
1528
+ worktreePath: executionWorkspace.worktreePath,
1529
+ agentHome: resolveDefaultAgentWorkspaceDir(agent.id),
1530
+ };
1531
+ context.paperclipWorkspaces = resolvedWorkspace.workspaceHints;
1532
+ const runtimeServiceIntents = (() => {
1533
+ const runtimeConfig = parseObject(resolvedConfig.workspaceRuntime);
1534
+ return Array.isArray(runtimeConfig.services)
1535
+ ? runtimeConfig.services.filter((value) => typeof value === "object" && value !== null)
1536
+ : [];
1537
+ })();
1538
+ if (runtimeServiceIntents.length > 0) {
1539
+ context.paperclipRuntimeServiceIntents = runtimeServiceIntents;
1540
+ }
1541
+ else {
1542
+ delete context.paperclipRuntimeServiceIntents;
1543
+ }
1544
+ if (executionWorkspace.projectId && !readNonEmptyString(context.projectId)) {
1545
+ context.projectId = executionWorkspace.projectId;
1546
+ }
1547
+ const runtimeSessionFallback = taskKey || resetTaskSession ? null : runtime.sessionId;
1548
+ let previousSessionDisplayId = truncateDisplayId(taskSessionForRun?.sessionDisplayId ??
1549
+ (sessionCodec.getDisplayId ? sessionCodec.getDisplayId(runtimeSessionParams) : null) ??
1550
+ readNonEmptyString(runtimeSessionParams?.sessionId) ??
1551
+ runtimeSessionFallback);
1552
+ let runtimeSessionIdForAdapter = readNonEmptyString(runtimeSessionParams?.sessionId) ?? runtimeSessionFallback;
1553
+ let runtimeSessionParamsForAdapter = runtimeSessionParams;
1554
+ const sessionCompaction = await evaluateSessionCompaction({
1555
+ agent,
1556
+ sessionId: previousSessionDisplayId ?? runtimeSessionIdForAdapter,
1557
+ issueId,
1558
+ });
1559
+ if (sessionCompaction.rotate) {
1560
+ context.paperclipSessionHandoffMarkdown = sessionCompaction.handoffMarkdown;
1561
+ context.paperclipSessionRotationReason = sessionCompaction.reason;
1562
+ context.paperclipPreviousSessionId = previousSessionDisplayId ?? runtimeSessionIdForAdapter;
1563
+ runtimeSessionIdForAdapter = null;
1564
+ runtimeSessionParamsForAdapter = null;
1565
+ previousSessionDisplayId = null;
1566
+ if (sessionCompaction.reason) {
1567
+ runtimeWorkspaceWarnings.push(`Starting a fresh session because ${sessionCompaction.reason}.`);
1125
1568
  }
1126
1569
  }
1127
- const onAdapterMeta = async (meta) => {
1128
- if (meta.env && secretKeys.size > 0) {
1129
- for (const key of secretKeys) {
1130
- if (key in meta.env)
1131
- meta.env[key] = "***REDACTED***";
1132
- }
1570
+ else {
1571
+ delete context.paperclipSessionHandoffMarkdown;
1572
+ delete context.paperclipSessionRotationReason;
1573
+ delete context.paperclipPreviousSessionId;
1574
+ }
1575
+ const runtimeForAdapter = {
1576
+ sessionId: runtimeSessionIdForAdapter,
1577
+ sessionParams: runtimeSessionParamsForAdapter,
1578
+ sessionDisplayId: previousSessionDisplayId,
1579
+ taskKey,
1580
+ };
1581
+ let seq = 1;
1582
+ let handle = null;
1583
+ let stdoutExcerpt = "";
1584
+ let stderrExcerpt = "";
1585
+ try {
1586
+ const startedAt = run.startedAt ?? new Date();
1587
+ const runningWithSession = await db
1588
+ .update(heartbeatRuns)
1589
+ .set({
1590
+ startedAt,
1591
+ sessionIdBefore: runtimeForAdapter.sessionDisplayId ?? runtimeForAdapter.sessionId,
1592
+ contextSnapshot: context,
1593
+ updatedAt: new Date(),
1594
+ })
1595
+ .where(eq(heartbeatRuns.id, run.id))
1596
+ .returning()
1597
+ .then((rows) => rows[0] ?? null);
1598
+ if (runningWithSession)
1599
+ run = runningWithSession;
1600
+ const runningAgent = await db
1601
+ .update(agents)
1602
+ .set({ status: "running", updatedAt: new Date() })
1603
+ .where(eq(agents.id, agent.id))
1604
+ .returning()
1605
+ .then((rows) => rows[0] ?? null);
1606
+ if (runningAgent) {
1607
+ publishLiveEvent({
1608
+ companyId: runningAgent.companyId,
1609
+ type: "agent.status",
1610
+ payload: {
1611
+ agentId: runningAgent.id,
1612
+ status: runningAgent.status,
1613
+ outcome: "running",
1614
+ },
1615
+ });
1133
1616
  }
1617
+ const currentRun = run;
1134
1618
  await appendRunEvent(currentRun, seq++, {
1135
- eventType: "adapter.invoke",
1619
+ eventType: "lifecycle",
1136
1620
  stream: "system",
1137
1621
  level: "info",
1138
- message: "adapter invocation",
1139
- payload: meta,
1622
+ message: "run started",
1140
1623
  });
1141
- };
1142
- const adapter = getServerAdapter(agent.adapterType);
1143
- const authToken = adapter.supportsLocalAgentJwt
1144
- ? createLocalAgentJwt(agent.id, agent.companyId, agent.adapterType, run.id)
1145
- : null;
1146
- if (adapter.supportsLocalAgentJwt && !authToken) {
1147
- logger.warn({
1148
- companyId: agent.companyId,
1149
- agentId: agent.id,
1150
- runId: run.id,
1151
- adapterType: agent.adapterType,
1152
- }, "local agent jwt secret missing or invalid; running without injected PAPERCLIP_API_KEY");
1153
- }
1154
- const adapterResult = await adapter.execute({
1155
- runId: run.id,
1156
- agent,
1157
- runtime: runtimeForAdapter,
1158
- config: resolvedConfig,
1159
- context,
1160
- onLog,
1161
- onMeta: onAdapterMeta,
1162
- authToken: authToken ?? undefined,
1163
- });
1164
- const adapterManagedRuntimeServices = adapterResult.runtimeServices
1165
- ? await persistAdapterManagedRuntimeServices({
1624
+ handle = await runLogStore.begin({
1625
+ companyId: run.companyId,
1626
+ agentId: run.agentId,
1627
+ runId,
1628
+ });
1629
+ await db
1630
+ .update(heartbeatRuns)
1631
+ .set({
1632
+ logStore: handle.store,
1633
+ logRef: handle.logRef,
1634
+ updatedAt: new Date(),
1635
+ })
1636
+ .where(eq(heartbeatRuns.id, runId));
1637
+ const onLog = async (stream, chunk) => {
1638
+ const sanitizedChunk = redactCurrentUserText(chunk);
1639
+ if (stream === "stdout")
1640
+ stdoutExcerpt = appendExcerpt(stdoutExcerpt, sanitizedChunk);
1641
+ if (stream === "stderr")
1642
+ stderrExcerpt = appendExcerpt(stderrExcerpt, sanitizedChunk);
1643
+ const ts = new Date().toISOString();
1644
+ if (handle) {
1645
+ await runLogStore.append(handle, {
1646
+ stream,
1647
+ chunk: sanitizedChunk,
1648
+ ts,
1649
+ });
1650
+ }
1651
+ const payloadChunk = sanitizedChunk.length > MAX_LIVE_LOG_CHUNK_BYTES
1652
+ ? sanitizedChunk.slice(sanitizedChunk.length - MAX_LIVE_LOG_CHUNK_BYTES)
1653
+ : sanitizedChunk;
1654
+ publishLiveEvent({
1655
+ companyId: run.companyId,
1656
+ type: "heartbeat.run.log",
1657
+ payload: {
1658
+ runId: run.id,
1659
+ agentId: run.agentId,
1660
+ ts,
1661
+ stream,
1662
+ chunk: payloadChunk,
1663
+ truncated: payloadChunk.length !== sanitizedChunk.length,
1664
+ },
1665
+ });
1666
+ };
1667
+ for (const warning of runtimeWorkspaceWarnings) {
1668
+ await onLog("stderr", `[paperclip] ${warning}\n`);
1669
+ }
1670
+ const adapterEnv = Object.fromEntries(Object.entries(parseObject(resolvedConfig.env)).filter((entry) => typeof entry[0] === "string" && typeof entry[1] === "string"));
1671
+ const runtimeServices = await ensureRuntimeServicesForRun({
1166
1672
  db,
1167
- adapterType: agent.adapterType,
1168
1673
  runId: run.id,
1169
1674
  agent: {
1170
1675
  id: agent.id,
@@ -1173,203 +1678,337 @@ export function heartbeatService(db) {
1173
1678
  },
1174
1679
  issue: issueRef,
1175
1680
  workspace: executionWorkspace,
1176
- reports: adapterResult.runtimeServices,
1177
- })
1178
- : [];
1179
- if (adapterManagedRuntimeServices.length > 0) {
1180
- const combinedRuntimeServices = [
1181
- ...runtimeServices,
1182
- ...adapterManagedRuntimeServices,
1183
- ];
1184
- context.paperclipRuntimeServices = combinedRuntimeServices;
1185
- context.paperclipRuntimePrimaryUrl =
1186
- combinedRuntimeServices.find((service) => readNonEmptyString(service.url))?.url ?? null;
1187
- await db
1188
- .update(heartbeatRuns)
1189
- .set({
1190
- contextSnapshot: context,
1191
- updatedAt: new Date(),
1192
- })
1193
- .where(eq(heartbeatRuns.id, run.id));
1194
- if (issueId) {
1681
+ executionWorkspaceId: persistedExecutionWorkspace?.id ?? issueRef?.executionWorkspaceId ?? null,
1682
+ config: resolvedConfig,
1683
+ adapterEnv,
1684
+ onLog,
1685
+ });
1686
+ if (runtimeServices.length > 0) {
1687
+ context.paperclipRuntimeServices = runtimeServices;
1688
+ context.paperclipRuntimePrimaryUrl =
1689
+ runtimeServices.find((service) => readNonEmptyString(service.url))?.url ?? null;
1690
+ await db
1691
+ .update(heartbeatRuns)
1692
+ .set({
1693
+ contextSnapshot: context,
1694
+ updatedAt: new Date(),
1695
+ })
1696
+ .where(eq(heartbeatRuns.id, run.id));
1697
+ }
1698
+ if (issueId && (executionWorkspace.created || runtimeServices.some((service) => !service.reused))) {
1195
1699
  try {
1196
1700
  await issuesSvc.addComment(issueId, buildWorkspaceReadyComment({
1197
1701
  workspace: executionWorkspace,
1198
- runtimeServices: adapterManagedRuntimeServices,
1702
+ runtimeServices,
1199
1703
  }), { agentId: agent.id });
1200
1704
  }
1201
1705
  catch (err) {
1202
- await onLog("stderr", `[paperclip] Failed to post adapter-managed runtime comment: ${err instanceof Error ? err.message : String(err)}\n`);
1706
+ await onLog("stderr", `[paperclip] Failed to post workspace-ready comment: ${err instanceof Error ? err.message : String(err)}\n`);
1203
1707
  }
1204
1708
  }
1205
- }
1206
- const nextSessionState = resolveNextSessionState({
1207
- codec: sessionCodec,
1208
- adapterResult,
1209
- previousParams: previousSessionParams,
1210
- previousDisplayId: runtimeForAdapter.sessionDisplayId,
1211
- previousLegacySessionId: runtimeForAdapter.sessionId,
1212
- });
1213
- let outcome;
1214
- const latestRun = await getRun(run.id);
1215
- if (latestRun?.status === "cancelled") {
1216
- outcome = "cancelled";
1217
- }
1218
- else if (adapterResult.timedOut) {
1219
- outcome = "timed_out";
1220
- }
1221
- else if ((adapterResult.exitCode ?? 0) === 0 && !adapterResult.errorMessage) {
1222
- outcome = "succeeded";
1223
- }
1224
- else {
1225
- outcome = "failed";
1226
- }
1227
- let logSummary = null;
1228
- if (handle) {
1229
- logSummary = await runLogStore.finalize(handle);
1230
- }
1231
- const status = outcome === "succeeded"
1232
- ? "succeeded"
1233
- : outcome === "cancelled"
1234
- ? "cancelled"
1235
- : outcome === "timed_out"
1236
- ? "timed_out"
1237
- : "failed";
1238
- const usageJson = adapterResult.usage || adapterResult.costUsd != null
1239
- ? {
1240
- ...(adapterResult.usage ?? {}),
1241
- ...(adapterResult.costUsd != null ? { costUsd: adapterResult.costUsd } : {}),
1242
- ...(adapterResult.billingType ? { billingType: adapterResult.billingType } : {}),
1709
+ const onAdapterMeta = async (meta) => {
1710
+ if (meta.env && secretKeys.size > 0) {
1711
+ for (const key of secretKeys) {
1712
+ if (key in meta.env)
1713
+ meta.env[key] = "***REDACTED***";
1714
+ }
1715
+ }
1716
+ await appendRunEvent(currentRun, seq++, {
1717
+ eventType: "adapter.invoke",
1718
+ stream: "system",
1719
+ level: "info",
1720
+ message: "adapter invocation",
1721
+ payload: meta,
1722
+ });
1723
+ };
1724
+ const adapter = getServerAdapter(agent.adapterType);
1725
+ const authToken = adapter.supportsLocalAgentJwt
1726
+ ? createLocalAgentJwt(agent.id, agent.companyId, agent.adapterType, run.id)
1727
+ : null;
1728
+ if (adapter.supportsLocalAgentJwt && !authToken) {
1729
+ logger.warn({
1730
+ companyId: agent.companyId,
1731
+ agentId: agent.id,
1732
+ runId: run.id,
1733
+ adapterType: agent.adapterType,
1734
+ }, "local agent jwt secret missing or invalid; running without injected PAPERCLIP_API_KEY");
1243
1735
  }
1244
- : null;
1245
- await setRunStatus(run.id, status, {
1246
- finishedAt: new Date(),
1247
- error: outcome === "succeeded"
1248
- ? null
1249
- : redactCurrentUserText(adapterResult.errorMessage ?? (outcome === "timed_out" ? "Timed out" : "Adapter failed")),
1250
- errorCode: outcome === "timed_out"
1251
- ? "timeout"
1736
+ const adapterResult = await adapter.execute({
1737
+ runId: run.id,
1738
+ agent,
1739
+ runtime: runtimeForAdapter,
1740
+ config: resolvedConfig,
1741
+ context,
1742
+ onLog,
1743
+ onMeta: onAdapterMeta,
1744
+ authToken: authToken ?? undefined,
1745
+ });
1746
+ const adapterManagedRuntimeServices = adapterResult.runtimeServices
1747
+ ? await persistAdapterManagedRuntimeServices({
1748
+ db,
1749
+ adapterType: agent.adapterType,
1750
+ runId: run.id,
1751
+ agent: {
1752
+ id: agent.id,
1753
+ name: agent.name,
1754
+ companyId: agent.companyId,
1755
+ },
1756
+ issue: issueRef,
1757
+ workspace: executionWorkspace,
1758
+ reports: adapterResult.runtimeServices,
1759
+ })
1760
+ : [];
1761
+ if (adapterManagedRuntimeServices.length > 0) {
1762
+ const combinedRuntimeServices = [
1763
+ ...runtimeServices,
1764
+ ...adapterManagedRuntimeServices,
1765
+ ];
1766
+ context.paperclipRuntimeServices = combinedRuntimeServices;
1767
+ context.paperclipRuntimePrimaryUrl =
1768
+ combinedRuntimeServices.find((service) => readNonEmptyString(service.url))?.url ?? null;
1769
+ await db
1770
+ .update(heartbeatRuns)
1771
+ .set({
1772
+ contextSnapshot: context,
1773
+ updatedAt: new Date(),
1774
+ })
1775
+ .where(eq(heartbeatRuns.id, run.id));
1776
+ if (issueId) {
1777
+ try {
1778
+ await issuesSvc.addComment(issueId, buildWorkspaceReadyComment({
1779
+ workspace: executionWorkspace,
1780
+ runtimeServices: adapterManagedRuntimeServices,
1781
+ }), { agentId: agent.id });
1782
+ }
1783
+ catch (err) {
1784
+ await onLog("stderr", `[paperclip] Failed to post adapter-managed runtime comment: ${err instanceof Error ? err.message : String(err)}\n`);
1785
+ }
1786
+ }
1787
+ }
1788
+ const nextSessionState = resolveNextSessionState({
1789
+ codec: sessionCodec,
1790
+ adapterResult,
1791
+ previousParams: previousSessionParams,
1792
+ previousDisplayId: runtimeForAdapter.sessionDisplayId,
1793
+ previousLegacySessionId: runtimeForAdapter.sessionId,
1794
+ });
1795
+ const rawUsage = normalizeUsageTotals(adapterResult.usage);
1796
+ const sessionUsageResolution = await resolveNormalizedUsageForSession({
1797
+ agentId: agent.id,
1798
+ runId: run.id,
1799
+ sessionId: nextSessionState.displayId ?? nextSessionState.legacySessionId,
1800
+ rawUsage,
1801
+ });
1802
+ const normalizedUsage = sessionUsageResolution.normalizedUsage;
1803
+ let outcome;
1804
+ const latestRun = await getRun(run.id);
1805
+ if (latestRun?.status === "cancelled") {
1806
+ outcome = "cancelled";
1807
+ }
1808
+ else if (adapterResult.timedOut) {
1809
+ outcome = "timed_out";
1810
+ }
1811
+ else if ((adapterResult.exitCode ?? 0) === 0 && !adapterResult.errorMessage) {
1812
+ outcome = "succeeded";
1813
+ }
1814
+ else {
1815
+ outcome = "failed";
1816
+ }
1817
+ let logSummary = null;
1818
+ if (handle) {
1819
+ logSummary = await runLogStore.finalize(handle);
1820
+ }
1821
+ const status = outcome === "succeeded"
1822
+ ? "succeeded"
1252
1823
  : outcome === "cancelled"
1253
1824
  ? "cancelled"
1254
- : outcome === "failed"
1255
- ? (adapterResult.errorCode ?? "adapter_failed")
1256
- : null,
1257
- exitCode: adapterResult.exitCode,
1258
- signal: adapterResult.signal,
1259
- usageJson,
1260
- resultJson: adapterResult.resultJson ?? null,
1261
- sessionIdAfter: nextSessionState.displayId ?? nextSessionState.legacySessionId,
1262
- stdoutExcerpt,
1263
- stderrExcerpt,
1264
- logBytes: logSummary?.bytes,
1265
- logSha256: logSummary?.sha256,
1266
- logCompressed: logSummary?.compressed ?? false,
1267
- });
1268
- await setWakeupStatus(run.wakeupRequestId, outcome === "succeeded" ? "completed" : status, {
1269
- finishedAt: new Date(),
1270
- error: adapterResult.errorMessage ?? null,
1271
- });
1272
- const finalizedRun = await getRun(run.id);
1273
- if (finalizedRun) {
1274
- await appendRunEvent(finalizedRun, seq++, {
1275
- eventType: "lifecycle",
1276
- stream: "system",
1277
- level: outcome === "succeeded" ? "info" : "error",
1278
- message: `run ${outcome}`,
1279
- payload: {
1280
- status,
1281
- exitCode: adapterResult.exitCode,
1282
- },
1825
+ : outcome === "timed_out"
1826
+ ? "timed_out"
1827
+ : "failed";
1828
+ const usageJson = normalizedUsage || adapterResult.costUsd != null
1829
+ ? {
1830
+ ...(normalizedUsage ?? {}),
1831
+ ...(rawUsage ? {
1832
+ rawInputTokens: rawUsage.inputTokens,
1833
+ rawCachedInputTokens: rawUsage.cachedInputTokens,
1834
+ rawOutputTokens: rawUsage.outputTokens,
1835
+ } : {}),
1836
+ ...(sessionUsageResolution.derivedFromSessionTotals ? { usageSource: "session_delta" } : {}),
1837
+ ...((nextSessionState.displayId ?? nextSessionState.legacySessionId)
1838
+ ? { persistedSessionId: nextSessionState.displayId ?? nextSessionState.legacySessionId }
1839
+ : {}),
1840
+ sessionReused: runtimeForAdapter.sessionId != null || runtimeForAdapter.sessionDisplayId != null,
1841
+ taskSessionReused: taskSessionForRun != null,
1842
+ freshSession: runtimeForAdapter.sessionId == null && runtimeForAdapter.sessionDisplayId == null,
1843
+ sessionRotated: sessionCompaction.rotate,
1844
+ sessionRotationReason: sessionCompaction.reason,
1845
+ provider: readNonEmptyString(adapterResult.provider) ?? "unknown",
1846
+ biller: resolveLedgerBiller(adapterResult),
1847
+ model: readNonEmptyString(adapterResult.model) ?? "unknown",
1848
+ ...(adapterResult.costUsd != null ? { costUsd: adapterResult.costUsd } : {}),
1849
+ billingType: normalizeLedgerBillingType(adapterResult.billingType),
1850
+ }
1851
+ : null;
1852
+ await setRunStatus(run.id, status, {
1853
+ finishedAt: new Date(),
1854
+ error: outcome === "succeeded"
1855
+ ? null
1856
+ : redactCurrentUserText(adapterResult.errorMessage ?? (outcome === "timed_out" ? "Timed out" : "Adapter failed")),
1857
+ errorCode: outcome === "timed_out"
1858
+ ? "timeout"
1859
+ : outcome === "cancelled"
1860
+ ? "cancelled"
1861
+ : outcome === "failed"
1862
+ ? (adapterResult.errorCode ?? "adapter_failed")
1863
+ : null,
1864
+ exitCode: adapterResult.exitCode,
1865
+ signal: adapterResult.signal,
1866
+ usageJson,
1867
+ resultJson: adapterResult.resultJson ?? null,
1868
+ sessionIdAfter: nextSessionState.displayId ?? nextSessionState.legacySessionId,
1869
+ stdoutExcerpt,
1870
+ stderrExcerpt,
1871
+ logBytes: logSummary?.bytes,
1872
+ logSha256: logSummary?.sha256,
1873
+ logCompressed: logSummary?.compressed ?? false,
1283
1874
  });
1284
- await releaseIssueExecutionAndPromote(finalizedRun);
1285
- }
1286
- if (finalizedRun) {
1287
- await updateRuntimeState(agent, finalizedRun, adapterResult, {
1288
- legacySessionId: nextSessionState.legacySessionId,
1875
+ await setWakeupStatus(run.wakeupRequestId, outcome === "succeeded" ? "completed" : status, {
1876
+ finishedAt: new Date(),
1877
+ error: adapterResult.errorMessage ?? null,
1289
1878
  });
1290
- if (taskKey) {
1291
- if (adapterResult.clearSession || (!nextSessionState.params && !nextSessionState.displayId)) {
1292
- await clearTaskSessions(agent.companyId, agent.id, {
1293
- taskKey,
1294
- adapterType: agent.adapterType,
1295
- });
1879
+ const finalizedRun = await getRun(run.id);
1880
+ if (finalizedRun) {
1881
+ await appendRunEvent(finalizedRun, seq++, {
1882
+ eventType: "lifecycle",
1883
+ stream: "system",
1884
+ level: outcome === "succeeded" ? "info" : "error",
1885
+ message: `run ${outcome}`,
1886
+ payload: {
1887
+ status,
1888
+ exitCode: adapterResult.exitCode,
1889
+ },
1890
+ });
1891
+ await releaseIssueExecutionAndPromote(finalizedRun);
1892
+ }
1893
+ if (finalizedRun) {
1894
+ await updateRuntimeState(agent, finalizedRun, adapterResult, {
1895
+ legacySessionId: nextSessionState.legacySessionId,
1896
+ }, normalizedUsage);
1897
+ if (taskKey) {
1898
+ if (adapterResult.clearSession || (!nextSessionState.params && !nextSessionState.displayId)) {
1899
+ await clearTaskSessions(agent.companyId, agent.id, {
1900
+ taskKey,
1901
+ adapterType: agent.adapterType,
1902
+ });
1903
+ }
1904
+ else {
1905
+ await upsertTaskSession({
1906
+ companyId: agent.companyId,
1907
+ agentId: agent.id,
1908
+ adapterType: agent.adapterType,
1909
+ taskKey,
1910
+ sessionParamsJson: nextSessionState.params,
1911
+ sessionDisplayId: nextSessionState.displayId,
1912
+ lastRunId: finalizedRun.id,
1913
+ lastError: outcome === "succeeded" ? null : (adapterResult.errorMessage ?? "run_failed"),
1914
+ });
1915
+ }
1296
1916
  }
1297
- else {
1917
+ }
1918
+ await finalizeAgentStatus(agent.id, outcome);
1919
+ }
1920
+ catch (err) {
1921
+ const message = redactCurrentUserText(err instanceof Error ? err.message : "Unknown adapter failure");
1922
+ logger.error({ err, runId }, "heartbeat execution failed");
1923
+ let logSummary = null;
1924
+ if (handle) {
1925
+ try {
1926
+ logSummary = await runLogStore.finalize(handle);
1927
+ }
1928
+ catch (finalizeErr) {
1929
+ logger.warn({ err: finalizeErr, runId }, "failed to finalize run log after error");
1930
+ }
1931
+ }
1932
+ const failedRun = await setRunStatus(run.id, "failed", {
1933
+ error: message,
1934
+ errorCode: "adapter_failed",
1935
+ finishedAt: new Date(),
1936
+ stdoutExcerpt,
1937
+ stderrExcerpt,
1938
+ logBytes: logSummary?.bytes,
1939
+ logSha256: logSummary?.sha256,
1940
+ logCompressed: logSummary?.compressed ?? false,
1941
+ });
1942
+ await setWakeupStatus(run.wakeupRequestId, "failed", {
1943
+ finishedAt: new Date(),
1944
+ error: message,
1945
+ });
1946
+ if (failedRun) {
1947
+ await appendRunEvent(failedRun, seq++, {
1948
+ eventType: "error",
1949
+ stream: "system",
1950
+ level: "error",
1951
+ message,
1952
+ });
1953
+ await releaseIssueExecutionAndPromote(failedRun);
1954
+ await updateRuntimeState(agent, failedRun, {
1955
+ exitCode: null,
1956
+ signal: null,
1957
+ timedOut: false,
1958
+ errorMessage: message,
1959
+ }, {
1960
+ legacySessionId: runtimeForAdapter.sessionId,
1961
+ });
1962
+ if (taskKey && (previousSessionParams || previousSessionDisplayId || taskSession)) {
1298
1963
  await upsertTaskSession({
1299
1964
  companyId: agent.companyId,
1300
1965
  agentId: agent.id,
1301
1966
  adapterType: agent.adapterType,
1302
1967
  taskKey,
1303
- sessionParamsJson: nextSessionState.params,
1304
- sessionDisplayId: nextSessionState.displayId,
1305
- lastRunId: finalizedRun.id,
1306
- lastError: outcome === "succeeded" ? null : (adapterResult.errorMessage ?? "run_failed"),
1968
+ sessionParamsJson: previousSessionParams,
1969
+ sessionDisplayId: previousSessionDisplayId,
1970
+ lastRunId: failedRun.id,
1971
+ lastError: message,
1307
1972
  });
1308
1973
  }
1309
1974
  }
1975
+ await finalizeAgentStatus(agent.id, "failed");
1310
1976
  }
1311
- await finalizeAgentStatus(agent.id, outcome);
1312
1977
  }
1313
- catch (err) {
1314
- const message = redactCurrentUserText(err instanceof Error ? err.message : "Unknown adapter failure");
1315
- logger.error({ err, runId }, "heartbeat execution failed");
1316
- let logSummary = null;
1317
- if (handle) {
1318
- try {
1319
- logSummary = await runLogStore.finalize(handle);
1320
- }
1321
- catch (finalizeErr) {
1322
- logger.warn({ err: finalizeErr, runId }, "failed to finalize run log after error");
1323
- }
1324
- }
1325
- const failedRun = await setRunStatus(run.id, "failed", {
1978
+ catch (outerErr) {
1979
+ // Setup code before adapter.execute threw (e.g. ensureRuntimeState, resolveWorkspaceForRun).
1980
+ // The inner catch did not fire, so we must record the failure here.
1981
+ const message = outerErr instanceof Error ? outerErr.message : "Unknown setup failure";
1982
+ logger.error({ err: outerErr, runId }, "heartbeat execution setup failed");
1983
+ await setRunStatus(runId, "failed", {
1326
1984
  error: message,
1327
1985
  errorCode: "adapter_failed",
1328
1986
  finishedAt: new Date(),
1329
- stdoutExcerpt,
1330
- stderrExcerpt,
1331
- logBytes: logSummary?.bytes,
1332
- logSha256: logSummary?.sha256,
1333
- logCompressed: logSummary?.compressed ?? false,
1334
- });
1987
+ }).catch(() => undefined);
1335
1988
  await setWakeupStatus(run.wakeupRequestId, "failed", {
1336
1989
  finishedAt: new Date(),
1337
1990
  error: message,
1338
- });
1991
+ }).catch(() => undefined);
1992
+ const failedRun = await getRun(runId).catch(() => null);
1339
1993
  if (failedRun) {
1340
- await appendRunEvent(failedRun, seq++, {
1994
+ // Emit a run-log event so the failure is visible in the run timeline,
1995
+ // consistent with what the inner catch block does for adapter failures.
1996
+ await appendRunEvent(failedRun, 1, {
1341
1997
  eventType: "error",
1342
1998
  stream: "system",
1343
1999
  level: "error",
1344
2000
  message,
1345
- });
1346
- await releaseIssueExecutionAndPromote(failedRun);
1347
- await updateRuntimeState(agent, failedRun, {
1348
- exitCode: null,
1349
- signal: null,
1350
- timedOut: false,
1351
- errorMessage: message,
1352
- }, {
1353
- legacySessionId: runtimeForAdapter.sessionId,
1354
- });
1355
- if (taskKey && (previousSessionParams || previousSessionDisplayId || taskSession)) {
1356
- await upsertTaskSession({
1357
- companyId: agent.companyId,
1358
- agentId: agent.id,
1359
- adapterType: agent.adapterType,
1360
- taskKey,
1361
- sessionParamsJson: previousSessionParams,
1362
- sessionDisplayId: previousSessionDisplayId,
1363
- lastRunId: failedRun.id,
1364
- lastError: message,
1365
- });
1366
- }
2001
+ }).catch(() => undefined);
2002
+ await releaseIssueExecutionAndPromote(failedRun).catch(() => undefined);
1367
2003
  }
1368
- await finalizeAgentStatus(agent.id, "failed");
2004
+ // Ensure the agent is not left stuck in "running" if the inner catch handler's
2005
+ // DB calls threw (e.g. a transient DB error in finalizeAgentStatus).
2006
+ await finalizeAgentStatus(run.agentId, "failed").catch(() => undefined);
1369
2007
  }
1370
2008
  finally {
1371
- await releaseRuntimeServicesForRun(run.id);
1372
- await startNextQueuedRunForAgent(agent.id);
2009
+ await releaseRuntimeServicesForRun(run.id).catch(() => undefined);
2010
+ activeRunExecutions.delete(run.id);
2011
+ await startNextQueuedRunForAgent(run.agentId);
1373
2012
  }
1374
2013
  }
1375
2014
  async function releaseIssueExecutionAndPromote(run) {
@@ -1512,19 +2151,13 @@ export function heartbeatService(db) {
1512
2151
  const agent = await getAgent(agentId);
1513
2152
  if (!agent)
1514
2153
  throw notFound("Agent not found");
1515
- if (agent.status === "paused" ||
1516
- agent.status === "terminated" ||
1517
- agent.status === "pending_approval") {
1518
- throw conflict("Agent is not invokable in its current state", { status: agent.status });
1519
- }
1520
- const policy = parseHeartbeatPolicy(agent);
1521
- const writeSkippedRequest = async (reason) => {
2154
+ const writeSkippedRequest = async (skipReason) => {
1522
2155
  await db.insert(agentWakeupRequests).values({
1523
2156
  companyId: agent.companyId,
1524
2157
  agentId,
1525
2158
  source,
1526
2159
  triggerDetail,
1527
- reason,
2160
+ reason: skipReason,
1528
2161
  payload,
1529
2162
  status: "skipped",
1530
2163
  requestedByActorType: opts.requestedByActorType ?? null,
@@ -1533,6 +2166,31 @@ export function heartbeatService(db) {
1533
2166
  finishedAt: new Date(),
1534
2167
  });
1535
2168
  };
2169
+ let projectId = readNonEmptyString(enrichedContextSnapshot.projectId);
2170
+ if (!projectId && issueId) {
2171
+ projectId = await db
2172
+ .select({ projectId: issues.projectId })
2173
+ .from(issues)
2174
+ .where(and(eq(issues.id, issueId), eq(issues.companyId, agent.companyId)))
2175
+ .then((rows) => rows[0]?.projectId ?? null);
2176
+ }
2177
+ const budgetBlock = await budgets.getInvocationBlock(agent.companyId, agentId, {
2178
+ issueId,
2179
+ projectId,
2180
+ });
2181
+ if (budgetBlock) {
2182
+ await writeSkippedRequest("budget.blocked");
2183
+ throw conflict(budgetBlock.reason, {
2184
+ scopeType: budgetBlock.scopeType,
2185
+ scopeId: budgetBlock.scopeId,
2186
+ });
2187
+ }
2188
+ if (agent.status === "paused" ||
2189
+ agent.status === "terminated" ||
2190
+ agent.status === "pending_approval") {
2191
+ throw conflict("Agent is not invokable in its current state", { status: agent.status });
2192
+ }
2193
+ const policy = parseHeartbeatPolicy(agent);
1536
2194
  if (source === "timer" && !policy.enabled) {
1537
2195
  await writeSkippedRequest("heartbeat.disabled");
1538
2196
  return null;
@@ -1864,6 +2522,140 @@ export function heartbeatService(db) {
1864
2522
  await startNextQueuedRunForAgent(agent.id);
1865
2523
  return newRun;
1866
2524
  }
2525
+ async function listProjectScopedRunIds(companyId, projectId) {
2526
+ const runIssueId = sql `${heartbeatRuns.contextSnapshot} ->> 'issueId'`;
2527
+ const effectiveProjectId = sql `coalesce(${heartbeatRuns.contextSnapshot} ->> 'projectId', ${issues.projectId}::text)`;
2528
+ const rows = await db
2529
+ .selectDistinctOn([heartbeatRuns.id], { id: heartbeatRuns.id })
2530
+ .from(heartbeatRuns)
2531
+ .leftJoin(issues, and(eq(issues.companyId, companyId), sql `${issues.id}::text = ${runIssueId}`))
2532
+ .where(and(eq(heartbeatRuns.companyId, companyId), inArray(heartbeatRuns.status, ["queued", "running"]), sql `${effectiveProjectId} = ${projectId}`));
2533
+ return rows.map((row) => row.id);
2534
+ }
2535
+ async function listProjectScopedWakeupIds(companyId, projectId) {
2536
+ const wakeIssueId = sql `${agentWakeupRequests.payload} ->> 'issueId'`;
2537
+ const effectiveProjectId = sql `coalesce(${agentWakeupRequests.payload} ->> 'projectId', ${issues.projectId}::text)`;
2538
+ const rows = await db
2539
+ .selectDistinctOn([agentWakeupRequests.id], { id: agentWakeupRequests.id })
2540
+ .from(agentWakeupRequests)
2541
+ .leftJoin(issues, and(eq(issues.companyId, companyId), sql `${issues.id}::text = ${wakeIssueId}`))
2542
+ .where(and(eq(agentWakeupRequests.companyId, companyId), inArray(agentWakeupRequests.status, ["queued", "deferred_issue_execution"]), sql `${agentWakeupRequests.runId} is null`, sql `${effectiveProjectId} = ${projectId}`));
2543
+ return rows.map((row) => row.id);
2544
+ }
2545
+ async function cancelPendingWakeupsForBudgetScope(scope) {
2546
+ const now = new Date();
2547
+ let wakeupIds = [];
2548
+ if (scope.scopeType === "company") {
2549
+ wakeupIds = await db
2550
+ .select({ id: agentWakeupRequests.id })
2551
+ .from(agentWakeupRequests)
2552
+ .where(and(eq(agentWakeupRequests.companyId, scope.companyId), inArray(agentWakeupRequests.status, ["queued", "deferred_issue_execution"]), sql `${agentWakeupRequests.runId} is null`))
2553
+ .then((rows) => rows.map((row) => row.id));
2554
+ }
2555
+ else if (scope.scopeType === "agent") {
2556
+ wakeupIds = await db
2557
+ .select({ id: agentWakeupRequests.id })
2558
+ .from(agentWakeupRequests)
2559
+ .where(and(eq(agentWakeupRequests.companyId, scope.companyId), eq(agentWakeupRequests.agentId, scope.scopeId), inArray(agentWakeupRequests.status, ["queued", "deferred_issue_execution"]), sql `${agentWakeupRequests.runId} is null`))
2560
+ .then((rows) => rows.map((row) => row.id));
2561
+ }
2562
+ else {
2563
+ wakeupIds = await listProjectScopedWakeupIds(scope.companyId, scope.scopeId);
2564
+ }
2565
+ if (wakeupIds.length === 0)
2566
+ return 0;
2567
+ await db
2568
+ .update(agentWakeupRequests)
2569
+ .set({
2570
+ status: "cancelled",
2571
+ finishedAt: now,
2572
+ error: "Cancelled due to budget pause",
2573
+ updatedAt: now,
2574
+ })
2575
+ .where(inArray(agentWakeupRequests.id, wakeupIds));
2576
+ return wakeupIds.length;
2577
+ }
2578
+ async function cancelRunInternal(runId, reason = "Cancelled by control plane") {
2579
+ const run = await getRun(runId);
2580
+ if (!run)
2581
+ throw notFound("Heartbeat run not found");
2582
+ if (run.status !== "running" && run.status !== "queued")
2583
+ return run;
2584
+ const running = runningProcesses.get(run.id);
2585
+ if (running) {
2586
+ running.child.kill("SIGTERM");
2587
+ const graceMs = Math.max(1, running.graceSec) * 1000;
2588
+ setTimeout(() => {
2589
+ if (!running.child.killed) {
2590
+ running.child.kill("SIGKILL");
2591
+ }
2592
+ }, graceMs);
2593
+ }
2594
+ const cancelled = await setRunStatus(run.id, "cancelled", {
2595
+ finishedAt: new Date(),
2596
+ error: reason,
2597
+ errorCode: "cancelled",
2598
+ });
2599
+ await setWakeupStatus(run.wakeupRequestId, "cancelled", {
2600
+ finishedAt: new Date(),
2601
+ error: reason,
2602
+ });
2603
+ if (cancelled) {
2604
+ await appendRunEvent(cancelled, 1, {
2605
+ eventType: "lifecycle",
2606
+ stream: "system",
2607
+ level: "warn",
2608
+ message: "run cancelled",
2609
+ });
2610
+ await releaseIssueExecutionAndPromote(cancelled);
2611
+ }
2612
+ runningProcesses.delete(run.id);
2613
+ await finalizeAgentStatus(run.agentId, "cancelled");
2614
+ await startNextQueuedRunForAgent(run.agentId);
2615
+ return cancelled;
2616
+ }
2617
+ async function cancelActiveForAgentInternal(agentId, reason = "Cancelled due to agent pause") {
2618
+ const runs = await db
2619
+ .select()
2620
+ .from(heartbeatRuns)
2621
+ .where(and(eq(heartbeatRuns.agentId, agentId), inArray(heartbeatRuns.status, ["queued", "running"])));
2622
+ for (const run of runs) {
2623
+ await setRunStatus(run.id, "cancelled", {
2624
+ finishedAt: new Date(),
2625
+ error: reason,
2626
+ errorCode: "cancelled",
2627
+ });
2628
+ await setWakeupStatus(run.wakeupRequestId, "cancelled", {
2629
+ finishedAt: new Date(),
2630
+ error: reason,
2631
+ });
2632
+ const running = runningProcesses.get(run.id);
2633
+ if (running) {
2634
+ running.child.kill("SIGTERM");
2635
+ runningProcesses.delete(run.id);
2636
+ }
2637
+ await releaseIssueExecutionAndPromote(run);
2638
+ }
2639
+ return runs.length;
2640
+ }
2641
+ async function cancelBudgetScopeWork(scope) {
2642
+ if (scope.scopeType === "agent") {
2643
+ await cancelActiveForAgentInternal(scope.scopeId, "Cancelled due to budget pause");
2644
+ await cancelPendingWakeupsForBudgetScope(scope);
2645
+ return;
2646
+ }
2647
+ const runIds = scope.scopeType === "company"
2648
+ ? await db
2649
+ .select({ id: heartbeatRuns.id })
2650
+ .from(heartbeatRuns)
2651
+ .where(and(eq(heartbeatRuns.companyId, scope.companyId), inArray(heartbeatRuns.status, ["queued", "running"])))
2652
+ .then((rows) => rows.map((row) => row.id))
2653
+ : await listProjectScopedRunIds(scope.companyId, scope.scopeId);
2654
+ for (const runId of runIds) {
2655
+ await cancelRunInternal(runId, "Cancelled due to budget pause");
2656
+ }
2657
+ await cancelPendingWakeupsForBudgetScope(scope);
2658
+ }
1867
2659
  return {
1868
2660
  list: async (companyId, agentId, limit) => {
1869
2661
  const query = db
@@ -1972,6 +2764,7 @@ export function heartbeatService(db) {
1972
2764
  }),
1973
2765
  wakeup: enqueueWakeup,
1974
2766
  reapOrphanedRuns,
2767
+ resumeQueuedRuns,
1975
2768
  tickTimers: async (now = new Date()) => {
1976
2769
  const allAgents = await db.select().from(agents);
1977
2770
  let checked = 0;
@@ -2007,69 +2800,9 @@ export function heartbeatService(db) {
2007
2800
  }
2008
2801
  return { checked, enqueued, skipped };
2009
2802
  },
2010
- cancelRun: async (runId) => {
2011
- const run = await getRun(runId);
2012
- if (!run)
2013
- throw notFound("Heartbeat run not found");
2014
- if (run.status !== "running" && run.status !== "queued")
2015
- return run;
2016
- const running = runningProcesses.get(run.id);
2017
- if (running) {
2018
- running.child.kill("SIGTERM");
2019
- const graceMs = Math.max(1, running.graceSec) * 1000;
2020
- setTimeout(() => {
2021
- if (!running.child.killed) {
2022
- running.child.kill("SIGKILL");
2023
- }
2024
- }, graceMs);
2025
- }
2026
- const cancelled = await setRunStatus(run.id, "cancelled", {
2027
- finishedAt: new Date(),
2028
- error: "Cancelled by control plane",
2029
- errorCode: "cancelled",
2030
- });
2031
- await setWakeupStatus(run.wakeupRequestId, "cancelled", {
2032
- finishedAt: new Date(),
2033
- error: "Cancelled by control plane",
2034
- });
2035
- if (cancelled) {
2036
- await appendRunEvent(cancelled, 1, {
2037
- eventType: "lifecycle",
2038
- stream: "system",
2039
- level: "warn",
2040
- message: "run cancelled",
2041
- });
2042
- await releaseIssueExecutionAndPromote(cancelled);
2043
- }
2044
- runningProcesses.delete(run.id);
2045
- await finalizeAgentStatus(run.agentId, "cancelled");
2046
- await startNextQueuedRunForAgent(run.agentId);
2047
- return cancelled;
2048
- },
2049
- cancelActiveForAgent: async (agentId) => {
2050
- const runs = await db
2051
- .select()
2052
- .from(heartbeatRuns)
2053
- .where(and(eq(heartbeatRuns.agentId, agentId), inArray(heartbeatRuns.status, ["queued", "running"])));
2054
- for (const run of runs) {
2055
- await setRunStatus(run.id, "cancelled", {
2056
- finishedAt: new Date(),
2057
- error: "Cancelled due to agent pause",
2058
- errorCode: "cancelled",
2059
- });
2060
- await setWakeupStatus(run.wakeupRequestId, "cancelled", {
2061
- finishedAt: new Date(),
2062
- error: "Cancelled due to agent pause",
2063
- });
2064
- const running = runningProcesses.get(run.id);
2065
- if (running) {
2066
- running.child.kill("SIGTERM");
2067
- runningProcesses.delete(run.id);
2068
- }
2069
- await releaseIssueExecutionAndPromote(run);
2070
- }
2071
- return runs.length;
2072
- },
2803
+ cancelRun: (runId) => cancelRunInternal(runId),
2804
+ cancelActiveForAgent: (agentId) => cancelActiveForAgentInternal(agentId),
2805
+ cancelBudgetScopeWork,
2073
2806
  getActiveRunForAgent: async (agentId) => {
2074
2807
  const [run] = await db
2075
2808
  .select()