@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.
- package/dist/adapters/registry.d.ts.map +1 -1
- package/dist/adapters/registry.js +23 -2
- package/dist/adapters/registry.js.map +1 -1
- package/dist/adapters/types.d.ts +1 -1
- package/dist/adapters/types.d.ts.map +1 -1
- package/dist/app.d.ts +4 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +115 -3
- package/dist/app.js.map +1 -1
- package/dist/attachment-types.d.ts.map +1 -1
- package/dist/attachment-types.js +6 -0
- package/dist/attachment-types.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +9 -1
- package/dist/config.js.map +1 -1
- package/dist/home-paths.d.ts +5 -0
- package/dist/home-paths.d.ts.map +1 -1
- package/dist/home-paths.js +18 -0
- package/dist/home-paths.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +61 -42
- package/dist/index.js.map +1 -1
- package/dist/routes/access.d.ts.map +1 -1
- package/dist/routes/access.js +90 -1
- package/dist/routes/access.js.map +1 -1
- package/dist/routes/agents.d.ts.map +1 -1
- package/dist/routes/agents.js +64 -1
- package/dist/routes/agents.js.map +1 -1
- package/dist/routes/assets.d.ts.map +1 -1
- package/dist/routes/assets.js +191 -13
- package/dist/routes/assets.js.map +1 -1
- package/dist/routes/companies.d.ts.map +1 -1
- package/dist/routes/companies.js +10 -1
- package/dist/routes/companies.js.map +1 -1
- package/dist/routes/costs.d.ts.map +1 -1
- package/dist/routes/costs.js +160 -5
- package/dist/routes/costs.js.map +1 -1
- package/dist/routes/execution-workspaces.d.ts +3 -0
- package/dist/routes/execution-workspaces.d.ts.map +1 -0
- package/dist/routes/execution-workspaces.js +165 -0
- package/dist/routes/execution-workspaces.js.map +1 -0
- package/dist/routes/health.d.ts.map +1 -1
- package/dist/routes/health.js +3 -1
- package/dist/routes/health.js.map +1 -1
- package/dist/routes/index.d.ts +1 -0
- package/dist/routes/index.d.ts.map +1 -1
- package/dist/routes/index.js +1 -0
- package/dist/routes/index.js.map +1 -1
- package/dist/routes/instance-settings.d.ts +3 -0
- package/dist/routes/instance-settings.d.ts.map +1 -0
- package/dist/routes/instance-settings.js +46 -0
- package/dist/routes/instance-settings.js.map +1 -0
- package/dist/routes/issues.d.ts.map +1 -1
- package/dist/routes/issues.js +339 -4
- package/dist/routes/issues.js.map +1 -1
- package/dist/routes/plugin-ui-static.d.ts +69 -0
- package/dist/routes/plugin-ui-static.d.ts.map +1 -0
- package/dist/routes/plugin-ui-static.js +411 -0
- package/dist/routes/plugin-ui-static.js.map +1 -0
- package/dist/routes/plugins.d.ts +120 -0
- package/dist/routes/plugins.d.ts.map +1 -0
- package/dist/routes/plugins.js +1784 -0
- package/dist/routes/plugins.js.map +1 -0
- package/dist/routes/projects.d.ts.map +1 -1
- package/dist/routes/projects.js +5 -1
- package/dist/routes/projects.js.map +1 -1
- package/dist/services/access.d.ts +2 -2
- package/dist/services/activity-log.d.ts +3 -0
- package/dist/services/activity-log.d.ts.map +1 -1
- package/dist/services/activity-log.js +34 -0
- package/dist/services/activity-log.js.map +1 -1
- package/dist/services/agents.d.ts +39 -15
- package/dist/services/agents.d.ts.map +1 -1
- package/dist/services/agents.js +60 -8
- package/dist/services/agents.js.map +1 -1
- package/dist/services/approvals.d.ts +5 -5
- package/dist/services/approvals.d.ts.map +1 -1
- package/dist/services/approvals.js +11 -0
- package/dist/services/approvals.js.map +1 -1
- package/dist/services/assets.d.ts +4 -4
- package/dist/services/budgets.d.ts +38 -0
- package/dist/services/budgets.d.ts.map +1 -0
- package/dist/services/budgets.js +784 -0
- package/dist/services/budgets.js.map +1 -0
- package/dist/services/companies.d.ts +39 -418
- package/dist/services/companies.d.ts.map +1 -1
- package/dist/services/companies.js +157 -21
- package/dist/services/companies.js.map +1 -1
- package/dist/services/costs.d.ts +67 -3
- package/dist/services/costs.d.ts.map +1 -1
- package/dist/services/costs.js +185 -57
- package/dist/services/costs.js.map +1 -1
- package/dist/services/cron.d.ts +80 -0
- package/dist/services/cron.d.ts.map +1 -0
- package/dist/services/cron.js +300 -0
- package/dist/services/cron.js.map +1 -0
- package/dist/services/dashboard.d.ts +6 -0
- package/dist/services/dashboard.d.ts.map +1 -1
- package/dist/services/dashboard.js +9 -0
- package/dist/services/dashboard.js.map +1 -1
- package/dist/services/documents.d.ts +164 -0
- package/dist/services/documents.d.ts.map +1 -0
- package/dist/services/documents.js +382 -0
- package/dist/services/documents.js.map +1 -0
- package/dist/services/execution-workspace-policy.d.ts +2 -1
- package/dist/services/execution-workspace-policy.d.ts.map +1 -1
- package/dist/services/execution-workspace-policy.js +63 -15
- package/dist/services/execution-workspace-policy.js.map +1 -1
- package/dist/services/execution-workspaces.d.ts +19 -0
- package/dist/services/execution-workspaces.d.ts.map +1 -0
- package/dist/services/execution-workspaces.js +87 -0
- package/dist/services/execution-workspaces.js.map +1 -0
- package/dist/services/finance.d.ts +93 -0
- package/dist/services/finance.d.ts.map +1 -0
- package/dist/services/finance.js +120 -0
- package/dist/services/finance.js.map +1 -0
- package/dist/services/heartbeat.d.ts +11 -1
- package/dist/services/heartbeat.d.ts.map +1 -1
- package/dist/services/heartbeat.js +1292 -559
- package/dist/services/heartbeat.js.map +1 -1
- package/dist/services/index.d.ts +7 -0
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +7 -0
- package/dist/services/index.js.map +1 -1
- package/dist/services/instance-settings.d.ts +9 -0
- package/dist/services/instance-settings.d.ts.map +1 -0
- package/dist/services/instance-settings.js +80 -0
- package/dist/services/instance-settings.js.map +1 -0
- package/dist/services/issue-approvals.d.ts +1 -1
- package/dist/services/issues.d.ts +14 -2
- package/dist/services/issues.d.ts.map +1 -1
- package/dist/services/issues.js +161 -9
- package/dist/services/issues.js.map +1 -1
- package/dist/services/live-events.d.ts +5 -0
- package/dist/services/live-events.d.ts.map +1 -1
- package/dist/services/live-events.js +9 -0
- package/dist/services/live-events.js.map +1 -1
- package/dist/services/plugin-capability-validator.d.ts +108 -0
- package/dist/services/plugin-capability-validator.d.ts.map +1 -0
- package/dist/services/plugin-capability-validator.js +268 -0
- package/dist/services/plugin-capability-validator.js.map +1 -0
- package/dist/services/plugin-config-validator.d.ts +26 -0
- package/dist/services/plugin-config-validator.d.ts.map +1 -0
- package/dist/services/plugin-config-validator.js +41 -0
- package/dist/services/plugin-config-validator.js.map +1 -0
- package/dist/services/plugin-dev-watcher.d.ts +30 -0
- package/dist/services/plugin-dev-watcher.d.ts.map +1 -0
- package/dist/services/plugin-dev-watcher.js +241 -0
- package/dist/services/plugin-dev-watcher.js.map +1 -0
- package/dist/services/plugin-event-bus.d.ts +149 -0
- package/dist/services/plugin-event-bus.d.ts.map +1 -0
- package/dist/services/plugin-event-bus.js +258 -0
- package/dist/services/plugin-event-bus.js.map +1 -0
- package/dist/services/plugin-host-service-cleanup.d.ts +14 -0
- package/dist/services/plugin-host-service-cleanup.d.ts.map +1 -0
- package/dist/services/plugin-host-service-cleanup.js +37 -0
- package/dist/services/plugin-host-service-cleanup.js.map +1 -0
- package/dist/services/plugin-host-services.d.ts +13 -0
- package/dist/services/plugin-host-services.d.ts.map +1 -0
- package/dist/services/plugin-host-services.js +969 -0
- package/dist/services/plugin-host-services.js.map +1 -0
- package/dist/services/plugin-job-coordinator.d.ts +81 -0
- package/dist/services/plugin-job-coordinator.d.ts.map +1 -0
- package/dist/services/plugin-job-coordinator.js +172 -0
- package/dist/services/plugin-job-coordinator.js.map +1 -0
- package/dist/services/plugin-job-scheduler.d.ts +163 -0
- package/dist/services/plugin-job-scheduler.d.ts.map +1 -0
- package/dist/services/plugin-job-scheduler.js +454 -0
- package/dist/services/plugin-job-scheduler.js.map +1 -0
- package/dist/services/plugin-job-store.d.ts +208 -0
- package/dist/services/plugin-job-store.d.ts.map +1 -0
- package/dist/services/plugin-job-store.js +350 -0
- package/dist/services/plugin-job-store.js.map +1 -0
- package/dist/services/plugin-lifecycle.d.ts +203 -0
- package/dist/services/plugin-lifecycle.d.ts.map +1 -0
- package/dist/services/plugin-lifecycle.js +476 -0
- package/dist/services/plugin-lifecycle.js.map +1 -0
- package/dist/services/plugin-loader.d.ts +441 -0
- package/dist/services/plugin-loader.d.ts.map +1 -0
- package/dist/services/plugin-loader.js +1192 -0
- package/dist/services/plugin-loader.js.map +1 -0
- package/dist/services/plugin-log-retention.d.ts +20 -0
- package/dist/services/plugin-log-retention.d.ts.map +1 -0
- package/dist/services/plugin-log-retention.js +63 -0
- package/dist/services/plugin-log-retention.js.map +1 -0
- package/dist/services/plugin-manifest-validator.d.ts +79 -0
- package/dist/services/plugin-manifest-validator.d.ts.map +1 -0
- package/dist/services/plugin-manifest-validator.js +84 -0
- package/dist/services/plugin-manifest-validator.js.map +1 -0
- package/dist/services/plugin-registry.d.ts +2542 -0
- package/dist/services/plugin-registry.d.ts.map +1 -0
- package/dist/services/plugin-registry.js +539 -0
- package/dist/services/plugin-registry.js.map +1 -0
- package/dist/services/plugin-runtime-sandbox.d.ts +40 -0
- package/dist/services/plugin-runtime-sandbox.d.ts.map +1 -0
- package/dist/services/plugin-runtime-sandbox.js +154 -0
- package/dist/services/plugin-runtime-sandbox.js.map +1 -0
- package/dist/services/plugin-secrets-handler.d.ts +81 -0
- package/dist/services/plugin-secrets-handler.d.ts.map +1 -0
- package/dist/services/plugin-secrets-handler.js +275 -0
- package/dist/services/plugin-secrets-handler.js.map +1 -0
- package/dist/services/plugin-state-store.d.ts +92 -0
- package/dist/services/plugin-state-store.d.ts.map +1 -0
- package/dist/services/plugin-state-store.js +190 -0
- package/dist/services/plugin-state-store.js.map +1 -0
- package/dist/services/plugin-stream-bus.d.ts +29 -0
- package/dist/services/plugin-stream-bus.d.ts.map +1 -0
- package/dist/services/plugin-stream-bus.js +48 -0
- package/dist/services/plugin-stream-bus.js.map +1 -0
- package/dist/services/plugin-tool-dispatcher.d.ts +180 -0
- package/dist/services/plugin-tool-dispatcher.d.ts.map +1 -0
- package/dist/services/plugin-tool-dispatcher.js +224 -0
- package/dist/services/plugin-tool-dispatcher.js.map +1 -0
- package/dist/services/plugin-tool-registry.d.ts +192 -0
- package/dist/services/plugin-tool-registry.d.ts.map +1 -0
- package/dist/services/plugin-tool-registry.js +224 -0
- package/dist/services/plugin-tool-registry.js.map +1 -0
- package/dist/services/plugin-worker-manager.d.ts +260 -0
- package/dist/services/plugin-worker-manager.d.ts.map +1 -0
- package/dist/services/plugin-worker-manager.js +835 -0
- package/dist/services/plugin-worker-manager.js.map +1 -0
- package/dist/services/projects.d.ts +12 -1
- package/dist/services/projects.d.ts.map +1 -1
- package/dist/services/projects.js +100 -4
- package/dist/services/projects.js.map +1 -1
- package/dist/services/quota-windows.d.ts +9 -0
- package/dist/services/quota-windows.d.ts.map +1 -0
- package/dist/services/quota-windows.js +56 -0
- package/dist/services/quota-windows.js.map +1 -0
- package/dist/services/secrets.d.ts +5 -5
- package/dist/services/work-products.d.ts +14 -0
- package/dist/services/work-products.d.ts.map +1 -0
- package/dist/services/work-products.js +100 -0
- package/dist/services/work-products.js.map +1 -0
- package/dist/services/workspace-operation-log-store.d.ts +33 -0
- package/dist/services/workspace-operation-log-store.d.ts.map +1 -0
- package/dist/services/workspace-operation-log-store.js +110 -0
- package/dist/services/workspace-operation-log-store.js.map +1 -0
- package/dist/services/workspace-operations.d.ts +44 -0
- package/dist/services/workspace-operations.d.ts.map +1 -0
- package/dist/services/workspace-operations.js +204 -0
- package/dist/services/workspace-operations.js.map +1 -0
- package/dist/services/workspace-runtime.d.ts +43 -5
- package/dist/services/workspace-runtime.d.ts.map +1 -1
- package/dist/services/workspace-runtime.js +413 -30
- package/dist/services/workspace-runtime.js.map +1 -1
- package/dist/ui-branding.d.ts +10 -1
- package/dist/ui-branding.d.ts.map +1 -1
- package/dist/ui-branding.js +164 -14
- package/dist/ui-branding.js.map +1 -1
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +5 -0
- package/dist/version.js.map +1 -0
- package/package.json +32 -14
- package/skills/paperclip/SKILL.md +34 -20
- package/skills/paperclip-create-plugin/SKILL.md +101 -0
- package/ui-dist/assets/{_basePickBy-B0xbZITw.js → _basePickBy-Chytvk8p.js} +1 -1
- package/ui-dist/assets/{_baseUniq-Cfd5u3qc.js → _baseUniq-BVL8b2zA.js} +1 -1
- package/ui-dist/assets/{arc-7Xbu8tBF.js → arc-D5hJS7gD.js} +1 -1
- package/ui-dist/assets/{architectureDiagram-VXUJARFQ-C8F7ZRYc.js → architectureDiagram-VXUJARFQ-BFq3iN58.js} +1 -1
- package/ui-dist/assets/{blockDiagram-VD42YOAC-Dp08a65A.js → blockDiagram-VD42YOAC-B69ok7vL.js} +1 -1
- package/ui-dist/assets/{c4Diagram-YG6GDRKO-BJfslTgZ.js → c4Diagram-YG6GDRKO-FE0D9aiA.js} +1 -1
- package/ui-dist/assets/channel-CiKAknwU.js +1 -0
- package/ui-dist/assets/{chunk-4BX2VUAB-BhaYWH7e.js → chunk-4BX2VUAB-SAhF6g-l.js} +1 -1
- package/ui-dist/assets/{chunk-55IACEB6-BOzJUsYW.js → chunk-55IACEB6-yxKD70bX.js} +1 -1
- package/ui-dist/assets/{chunk-B4BG7PRW-CbVcziyE.js → chunk-B4BG7PRW-qB4VaAIx.js} +1 -1
- package/ui-dist/assets/{chunk-DI55MBZ5-DghPtP3y.js → chunk-DI55MBZ5-B4NIq1Ee.js} +1 -1
- package/ui-dist/assets/{chunk-FMBD7UC4-CB095Kfn.js → chunk-FMBD7UC4-B5kLoRe2.js} +1 -1
- package/ui-dist/assets/{chunk-QN33PNHL-DfpzfDqJ.js → chunk-QN33PNHL-BVVG2n_X.js} +1 -1
- package/ui-dist/assets/{chunk-QZHKN3VN-Df3d5z1y.js → chunk-QZHKN3VN-ncMubclp.js} +1 -1
- package/ui-dist/assets/{chunk-TZMSLE5B-B6tX6bZI.js → chunk-TZMSLE5B-DSyjQk8C.js} +1 -1
- package/ui-dist/assets/classDiagram-2ON5EDUG-B93oQX02.js +1 -0
- package/ui-dist/assets/classDiagram-v2-WZHVMYZB-B93oQX02.js +1 -0
- package/ui-dist/assets/clone-Dld-3yq0.js +1 -0
- package/ui-dist/assets/{cose-bilkent-S5V4N54A-38wg_s9V.js → cose-bilkent-S5V4N54A-BAikNewC.js} +1 -1
- package/ui-dist/assets/{dagre-6UL2VRFP-DPVj3XLS.js → dagre-6UL2VRFP-j223UI8_.js} +1 -1
- package/ui-dist/assets/{diagram-PSM6KHXK-BJh89zUA.js → diagram-PSM6KHXK-Bhg27ZsN.js} +1 -1
- package/ui-dist/assets/{diagram-QEK2KX5R-Bw0m_j10.js → diagram-QEK2KX5R-BmtcrTHq.js} +1 -1
- package/ui-dist/assets/{diagram-S2PKOQOG-D7_M2F3u.js → diagram-S2PKOQOG-x8XuP7dH.js} +1 -1
- package/ui-dist/assets/{erDiagram-Q2GNP2WA-CLsjqTWP.js → erDiagram-Q2GNP2WA-Dc8mHvB-.js} +1 -1
- package/ui-dist/assets/{flowDiagram-NV44I4VS-BZZ7ezVB.js → flowDiagram-NV44I4VS-Br08v6xs.js} +1 -1
- package/ui-dist/assets/{ganttDiagram-JELNMOA3-KzsFuwBt.js → ganttDiagram-JELNMOA3-CksKhiV8.js} +1 -1
- package/ui-dist/assets/{gitGraphDiagram-V2S2FVAM-DSHtY7Vu.js → gitGraphDiagram-V2S2FVAM-DmitztAD.js} +1 -1
- package/ui-dist/assets/{graph-luIG1UAS.js → graph-Xzh7bNfz.js} +1 -1
- package/ui-dist/assets/{index-nFjLambq.js → index-B-MPJNeS.js} +1 -1
- package/ui-dist/assets/{index-RUBFVv6t.js → index-BAOC35TS.js} +1 -1
- package/ui-dist/assets/{index-CaR9XM4h.js → index-BCtrpKdJ.js} +1 -1
- package/ui-dist/assets/{index-itc7BfMy.js → index-B_mlwDCG.js} +1 -1
- package/ui-dist/assets/{index-1gX09-Fl.js → index-Bp8oJMwx.js} +1 -1
- package/ui-dist/assets/{index-B_3g3Rie.js → index-BqOfMG1d.js} +1 -1
- package/ui-dist/assets/{index-uR3zjYaD.js → index-C0hw7cTu.js} +1 -1
- package/ui-dist/assets/{index-Ds7vLTSK.js → index-C1ENEd6Y.js} +1 -1
- package/ui-dist/assets/{index-CeDCs_2i.js → index-CGB1OHQi.js} +1 -1
- package/ui-dist/assets/{index-DBy0vJy3.js → index-CZlfAt7E.js} +1 -1
- package/ui-dist/assets/{index-00kuG4sI.js → index-CeL-QouZ.js} +1 -1
- package/ui-dist/assets/{index-Bv4xCjxl.js → index-Cz0TxJbw.js} +1 -1
- package/ui-dist/assets/{index-DJf8diAA.js → index-DBxUCwDG.js} +1 -1
- package/ui-dist/assets/{index-DnIPDZLp.js → index-DGzQUMWC.js} +1 -1
- package/ui-dist/assets/{index-CbGtsjW7.js → index-DI9dat7c.js} +1 -1
- package/ui-dist/assets/{index-BfB4lKJN.js → index-DMpo-QSZ.js} +1 -1
- package/ui-dist/assets/{index-DcfLFstG.js → index-DQ6xyFaN.js} +1 -1
- package/ui-dist/assets/{index-B1ZMzzs0.js → index-DWMQ1Fyj.js} +1 -1
- package/ui-dist/assets/index-DbrFgvLd.css +1 -0
- package/ui-dist/assets/{index-C8XyGAr9.js → index-Er4i9aLN.js} +1 -1
- package/ui-dist/assets/{index-C2SZYIDA.js → index-I5K-yQx5.js} +1 -1
- package/ui-dist/assets/index-MuodIbDJ.js +1120 -0
- package/ui-dist/assets/{index-BeeKMqNU.js → index-auLN8BHe.js} +1 -1
- package/ui-dist/assets/{index-DoTq-BeR.js → index-xw01-Pl4.js} +1 -1
- package/ui-dist/assets/{infoDiagram-HS3SLOUP-CDEfWpme.js → infoDiagram-HS3SLOUP-DHb4TS1W.js} +1 -1
- package/ui-dist/assets/{journeyDiagram-XKPGCS4Q-i7nsbg_Y.js → journeyDiagram-XKPGCS4Q-CLRBeXVU.js} +1 -1
- package/ui-dist/assets/{kanban-definition-3W4ZIXB7-DDlx1qVE.js → kanban-definition-3W4ZIXB7-CsQp6woE.js} +1 -1
- package/ui-dist/assets/{layout-C7AtmJzX.js → layout-DwD9tZ5r.js} +1 -1
- package/ui-dist/assets/{linear-C5jHT-WP.js → linear-DF9n_1d3.js} +1 -1
- package/ui-dist/assets/{mermaid.core-C8YQ4fcY.js → mermaid.core-CDm4Crkq.js} +4 -4
- package/ui-dist/assets/{mindmap-definition-VGOIOE7T-B9m9PuUg.js → mindmap-definition-VGOIOE7T-CVzU1x4i.js} +1 -1
- package/ui-dist/assets/{pieDiagram-ADFJNKIX-SvKywCSE.js → pieDiagram-ADFJNKIX-CWgl0Up8.js} +1 -1
- package/ui-dist/assets/{quadrantDiagram-AYHSOK5B-QfBPm7Y1.js → quadrantDiagram-AYHSOK5B-CcedyN6Q.js} +1 -1
- package/ui-dist/assets/{requirementDiagram-UZGBJVZJ-DnfQQuwi.js → requirementDiagram-UZGBJVZJ-DZ4BVz3s.js} +1 -1
- package/ui-dist/assets/{sankeyDiagram-TZEHDZUN-DvLkjzIW.js → sankeyDiagram-TZEHDZUN-CLHEx34B.js} +1 -1
- package/ui-dist/assets/{sequenceDiagram-WL72ISMW-5Lq2rWBc.js → sequenceDiagram-WL72ISMW-nmTJbPyy.js} +1 -1
- package/ui-dist/assets/{stateDiagram-FKZM4ZOC-DcYPHDyi.js → stateDiagram-FKZM4ZOC-_UGwFEqN.js} +1 -1
- package/ui-dist/assets/stateDiagram-v2-4FDKWEC3-CvFVoOUd.js +1 -0
- package/ui-dist/assets/{timeline-definition-IT6M3QCI-lrN4JmWa.js → timeline-definition-IT6M3QCI-Bibk8Daj.js} +1 -1
- package/ui-dist/assets/{treemap-GDKQZRPO-DFJKIBTQ.js → treemap-GDKQZRPO-Ch3rlaqT.js} +1 -1
- package/ui-dist/assets/{xychartDiagram-PRI3JC2R-CT47vtdm.js → xychartDiagram-PRI3JC2R-DBUudr-E.js} +1 -1
- package/ui-dist/index.html +4 -2
- package/skills/create-agent-adapter/SKILL.md +0 -718
- package/ui-dist/assets/channel-BViQDbSq.js +0 -1
- package/ui-dist/assets/classDiagram-2ON5EDUG-CjMCmxMT.js +0 -1
- package/ui-dist/assets/classDiagram-v2-WZHVMYZB-CjMCmxMT.js +0 -1
- package/ui-dist/assets/clone-Dbn9wtE1.js +0 -1
- package/ui-dist/assets/index-BHP9dico.js +0 -1006
- package/ui-dist/assets/index-BfNaDZnn.css +0 -1
- 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 {
|
|
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
|
-
|
|
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
|
|
702
|
+
const contextProjectWorkspaceId = readNonEmptyString(context.projectWorkspaceId);
|
|
703
|
+
const issueProjectRef = issueId
|
|
378
704
|
? await db
|
|
379
|
-
.select({
|
|
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]
|
|
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
|
|
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
|
-
|
|
744
|
+
let projectCwd = readNonEmptyString(workspace.cwd);
|
|
745
|
+
let managedWorkspaceWarning = null;
|
|
405
746
|
if (!projectCwd || projectCwd === REPO_ONLY_CWD_SENTINEL) {
|
|
406
|
-
|
|
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 "
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
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
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
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
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
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
|
-
|
|
1049
|
-
.
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
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
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
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
|
-
|
|
1099
|
-
workspace: executionWorkspace,
|
|
1100
|
-
config: resolvedConfig,
|
|
1101
|
-
adapterEnv,
|
|
1102
|
-
onLog,
|
|
1376
|
+
recorder: workspaceOperationRecorder,
|
|
1103
1377
|
});
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
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
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1124
|
-
|
|
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
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
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: "
|
|
1619
|
+
eventType: "lifecycle",
|
|
1136
1620
|
stream: "system",
|
|
1137
1621
|
level: "info",
|
|
1138
|
-
message: "
|
|
1139
|
-
payload: meta,
|
|
1622
|
+
message: "run started",
|
|
1140
1623
|
});
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
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
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
}
|
|
1193
|
-
|
|
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
|
|
1702
|
+
runtimeServices,
|
|
1199
1703
|
}), { agentId: agent.id });
|
|
1200
1704
|
}
|
|
1201
1705
|
catch (err) {
|
|
1202
|
-
await onLog("stderr", `[paperclip] Failed to post
|
|
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
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
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
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
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 === "
|
|
1255
|
-
?
|
|
1256
|
-
:
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
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
|
|
1285
|
-
|
|
1286
|
-
|
|
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
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
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
|
-
|
|
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:
|
|
1304
|
-
sessionDisplayId:
|
|
1305
|
-
lastRunId:
|
|
1306
|
-
lastError:
|
|
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 (
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
2011
|
-
|
|
2012
|
-
|
|
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()
|