@desplega.ai/agent-swarm 1.131.0 → 1.133.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (265) hide show
  1. package/README.md +2 -2
  2. package/dist/{actions-w53mbns9.js → actions-cskpayd7.js} +11 -9
  3. package/dist/{anthropic-messages-rjmd2e7n.js → anthropic-messages-tpxtbx7b.js} +28 -12
  4. package/dist/{app-m9gwqsqv.js → app-ang77nbb.js} +8 -6
  5. package/dist/{assistant-bp65m6f3.js → assistant-b4exebdw.js} +16 -13
  6. package/dist/{azure-openai-responses-c0yjmbfd.js → azure-openai-responses-m3921t4y.js} +8 -8
  7. package/dist/{boot-reembed-62p0at2k.js → boot-reembed-ps2aftpb.js} +9 -7
  8. package/dist/{boot-reembed-xmfp6wdh.js → boot-reembed-znyk3nxb.js} +10 -8
  9. package/dist/{boot-scrub-logs-rr8q2dms.js → boot-scrub-logs-2ax3xgz1.js} +7 -5
  10. package/dist/{claude-adapter-2y47mrkd.js → claude-adapter-3ct8kgxw.js} +13 -9
  11. package/dist/{claude-managed-adapter-gv19tdew.js → claude-managed-adapter-akx4ntf2.js} +2 -2
  12. package/dist/{cli-t1xze5qh.js → cli-13w2hwkh.js} +1 -1
  13. package/dist/{cli-efxfnq6q.js → cli-1532szb1.js} +8 -6
  14. package/dist/{cli-d2tfkcga.js → cli-23d3rseb.js} +33 -17
  15. package/dist/{cli-nm5gtp1x.js → cli-2a75z9c5.js} +196 -7
  16. package/dist/{cli-fywms69x.js → cli-2t30rj0d.js} +1 -1
  17. package/dist/{cli-cvgjwnh1.js → cli-3c3tdgab.js} +10 -10
  18. package/dist/{cli-pm6xbh84.js → cli-3zzxa9jz.js} +46 -16
  19. package/dist/{cli-cdeg08wx.js → cli-4k6233gc.js} +1 -1
  20. package/dist/{cli-t55yw3a1.js → cli-5p2tz05r.js} +2 -2
  21. package/dist/{cli-0rcyq6d9.js → cli-5z2yhzvd.js} +125 -47
  22. package/dist/{cli-3kf51j15.js → cli-6zfq31nj.js} +9 -3
  23. package/dist/{cli-er3a4bb0.js → cli-8m9de10a.js} +6 -1
  24. package/dist/{cli-gm9cbhbq.js → cli-8t14vg9w.js} +27 -8
  25. package/dist/{cli-72q8m8gk.js → cli-9nfc2es8.js} +6 -6
  26. package/dist/{cli-5ze9rv97.js → cli-9ta3pwms.js} +11 -4
  27. package/dist/{cli-78xwvkhj.js → cli-avr0ns7z.js} +2 -2
  28. package/dist/{cli-n6h8qt8n.js → cli-c897wyhw.js} +12 -7
  29. package/dist/{cli-2q72qz2s.js → cli-dgk20ft7.js} +15 -6
  30. package/dist/cli-et1bbwqf.js +13 -0
  31. package/dist/{cli-bqw0mt1n.js → cli-f5w27pj3.js} +3 -3
  32. package/dist/{cli-2wadx7p2.js → cli-f8dre4bz.js} +2 -2
  33. package/dist/{cli-e7ckcg4v.js → cli-ftwpv451.js} +170 -387
  34. package/dist/{cli-g6b8x1h8.js → cli-g854s2dw.js} +1 -1
  35. package/dist/{cli-5kx36evn.js → cli-ggmdxeqa.js} +13 -8
  36. package/dist/cli-hkwfe3qj.js +530 -0
  37. package/dist/{cli-5dsbcjjw.js → cli-jqwnqx9m.js} +1 -1
  38. package/dist/{cli-9dr5qzc0.js → cli-ny122vh4.js} +21 -10
  39. package/dist/{cli-5v4rw6yd.js → cli-pvzqhbs9.js} +2 -2
  40. package/dist/cli-pzq5rjym.js +15 -0
  41. package/dist/{cli-xpxde7rm.js → cli-qes8ef67.js} +48 -30
  42. package/dist/{cli-sajgpkqg.js → cli-qs9hhsvb.js} +1723 -325
  43. package/dist/cli-qys3rhhz.js +27 -0
  44. package/dist/{cli-rzbpmxdr.js → cli-r6tpm37h.js} +2 -2
  45. package/dist/{cli-qv61hp37.js → cli-rch80c76.js} +2 -2
  46. package/dist/{cli-0258203w.js → cli-rpp0x4zy.js} +137 -17
  47. package/dist/{cli-5n6e5yfn.js → cli-s1k0wxhe.js} +12 -7
  48. package/dist/{cli-psh3c4va.js → cli-sfd9v7sj.js} +1 -1
  49. package/dist/{cli-70xqj0z3.js → cli-sh3ytsyj.js} +49 -39
  50. package/dist/{cli-zsspvvm2.js → cli-w1mp9jy7.js} +1 -1
  51. package/dist/{cli-d3x3x1c9.js → cli-xdb4mpnp.js} +2 -2
  52. package/dist/{cli-57khth04.js → cli-xmyqv00z.js} +120 -4
  53. package/dist/{cli-yw8vghx2.js → cli-xv8t8p3c.js} +65 -32
  54. package/dist/{cli-f1am9szv.js → cli-xxv5pn4f.js} +1 -1
  55. package/dist/{cli-t76vw5db.js → cli-y08jvxzx.js} +601 -140
  56. package/dist/{cli-a4jzdpa3.js → cli-zfbf3srj.js} +650 -245
  57. package/dist/{cli-a40rnf71.js → cli-zmq89k34.js} +6 -6
  58. package/dist/{cli-qmdnhpej.js → cli-zr9q011v.js} +23 -13
  59. package/dist/cli.js +22 -22
  60. package/dist/{codex-adapter-d24q75wk.js → codex-adapter-bef93pq4.js} +9 -7
  61. package/dist/{codex-hook-hq54t9ft.js → codex-hook-e8dba65p.js} +3 -2
  62. package/dist/{codex-session-runner-g50vcsex.js → codex-session-runner-5pkxys7x.js} +9 -7
  63. package/dist/{commands-c3nrc5hg.js → commands-e23s1bff.js} +9 -7
  64. package/dist/{db-nt7fg3tw.js → db-m3nabatn.js} +17 -5
  65. package/dist/{google-generative-ai-hysrd8qx.js → google-generative-ai-8maw55xh.js} +11 -8
  66. package/dist/{google-vertex-p5arrga0.js → google-vertex-8vjqmx8g.js} +11 -8
  67. package/dist/{handlers-fkr6swat.js → handlers-akazedvr.js} +16 -13
  68. package/dist/{hook-77wpvnjc.js → hook-ze4cpf0v.js} +55 -41
  69. package/dist/{hook-g019yehy.js → hook-zfqyyenw.js} +10 -7
  70. package/dist/{http-jewj0syk.js → http-sja0n6kw.js} +730 -150
  71. package/dist/index-0gr8vd6k.js +39 -0
  72. package/dist/{index-7n2xpbd4.js → index-354vkhdv.js} +154 -74
  73. package/dist/{index-070v4eyb.js → index-5qthek5t.js} +2 -3
  74. package/dist/{index-hfvggd0z.js → index-95t3cqq5.js} +16 -14
  75. package/dist/{index-9znhar3x.js → index-sea66619.js} +3 -4
  76. package/dist/{index-6gm5he8h.js → index-vpn36mma.js} +15 -13
  77. package/dist/{keepalive-j0fjvqfd.js → keepalive-3sp3meqj.js} +16 -10
  78. package/dist/{lead-1n8x30xe.js → lead-mefeewkf.js} +39 -36
  79. package/dist/{maintenance-gvev11sa.js → maintenance-8tbrbjfg.js} +10 -8
  80. package/dist/mistral-conversations-00zrp7g4.js +717 -0
  81. package/dist/{oauth-refresh-sweep-s9ecsvh0.js → oauth-refresh-sweep-srjwjz6m.js} +8 -6
  82. package/dist/{onboard-pvm5b38w.js → onboard-mt2tgyjg.js} +7 -7
  83. package/dist/{openai-codex-responses-5g27pd6f.js → openai-codex-responses-map32h2s.js} +18 -18
  84. package/dist/{openai-completions-3vkmfz2a.js → openai-completions-w871b1cq.js} +12 -11
  85. package/dist/{openai-responses-dg4t7j7a.js → openai-responses-b0j7xnw8.js} +15 -12
  86. package/dist/{opencode-adapter-1gm38aba.js → opencode-adapter-b39jefcj.js} +5 -3
  87. package/dist/{openrouter-images-g3gm8931.js → openrouter-images-srfkywy0.js} +4 -4
  88. package/dist/{otel-impl-p6jvgd9c.js → otel-impl-z6pxt3j9.js} +224 -9
  89. package/dist/{pi-mono-adapter-t091s27g.js → pi-mono-adapter-btvqmj58.js} +1138 -408
  90. package/dist/{pricing-refresh-k49rcyed.js → pricing-refresh-kyq8cezh.js} +9 -7
  91. package/dist/{rbac-roles-cr80g96t.js → rbac-roles-3ryfrfna.js} +8 -6
  92. package/dist/{rbac-roles-30pspvy7.js → rbac-roles-6qfvbee1.js} +9 -7
  93. package/dist/{render-v2-xm457ng1.js → render-v2-p61ppjey.js} +8 -6
  94. package/dist/{seed-pricing-yhpv35fr.js → seed-pricing-j56bbj4y.js} +8 -6
  95. package/dist/{setup-nytnmwwr.js → setup-tpr6c1kn.js} +2 -2
  96. package/dist/{worker-msytr2cw.js → worker-9w1v0mbb.js} +39 -36
  97. package/openapi.json +542 -16
  98. package/package.json +6 -6
  99. package/src/be/budget-refusal-notify.ts +14 -7
  100. package/src/be/db-queries/task-artifacts.ts +182 -0
  101. package/src/be/db.ts +783 -169
  102. package/src/be/events.ts +5 -0
  103. package/src/be/migrations/130_scratch_script_retention_grace.sql +6 -0
  104. package/src/be/migrations/131_session_logs_created_at_index.sql +2 -0
  105. package/src/be/migrations/132_multi_runtime_instances.sql +44 -0
  106. package/src/be/migrations/133_workflow_run_trigger_type.sql +12 -0
  107. package/src/be/migrations/135_backfill_task_pull_request_attachments.sql +228 -0
  108. package/src/be/migrations/136_task_requester_provenance.sql +37 -0
  109. package/src/be/migrations/137_memory_retrieval_composite_index.sql +1 -0
  110. package/src/be/multi-runtime.ts +430 -0
  111. package/src/be/scripts/db.ts +43 -1
  112. package/src/be/scripts/extract-schema.ts +3 -1
  113. package/src/be/scripts/retention.ts +164 -0
  114. package/src/be/scripts/run-saved.ts +16 -1
  115. package/src/be/scripts/typecheck.ts +3 -2
  116. package/src/be/seed-skills/index.ts +7 -0
  117. package/src/be/steering.ts +1 -1
  118. package/src/be/swarm-config-guard.ts +18 -0
  119. package/src/commands/credential-wait.ts +54 -0
  120. package/src/commands/onboard/steps/core-credentials.tsx +1 -1
  121. package/src/commands/onboard/steps/custom-templates.tsx +1 -1
  122. package/src/commands/onboard/steps/harness-credentials.tsx +1 -1
  123. package/src/commands/onboard/steps/health-check.tsx +1 -1
  124. package/src/commands/onboard/steps/start.tsx +1 -1
  125. package/src/commands/profile-sync-audit.ts +56 -0
  126. package/src/commands/profile-sync.ts +287 -15
  127. package/src/commands/provider-credentials.ts +46 -19
  128. package/src/commands/runner.ts +196 -33
  129. package/src/commands/templates.ts +22 -0
  130. package/src/github/handlers.ts +55 -9
  131. package/src/heartbeat/heartbeat.ts +71 -12
  132. package/src/hooks/hook.ts +49 -33
  133. package/src/http/active-sessions.ts +11 -1
  134. package/src/http/agents.ts +261 -30
  135. package/src/http/all-routes.ts +1 -0
  136. package/src/http/config.ts +13 -2
  137. package/src/http/core.ts +102 -4
  138. package/src/http/db-query-bounded.ts +313 -0
  139. package/src/http/db-query-shared.ts +249 -0
  140. package/src/http/db-query.ts +54 -42
  141. package/src/http/events.ts +2 -0
  142. package/src/http/index.ts +27 -2
  143. package/src/http/mcp-bridge.ts +37 -3
  144. package/src/http/mcp.ts +12 -0
  145. package/src/http/memory.ts +13 -2
  146. package/src/http/openapi.ts +1 -0
  147. package/src/http/poll.ts +70 -27
  148. package/src/http/route-def.ts +43 -9
  149. package/src/http/schedules.ts +2 -2
  150. package/src/http/scripts.ts +31 -4
  151. package/src/http/session-data.ts +44 -0
  152. package/src/http/tasks.ts +2 -2
  153. package/src/http/users.ts +74 -52
  154. package/src/http/webhooks.ts +5 -2
  155. package/src/http/workflows.ts +1 -4
  156. package/src/http/x.ts +14 -0
  157. package/src/integrations/kapso/inbound.ts +1 -1
  158. package/src/jira/outbound.ts +39 -8
  159. package/src/jira/webhook-lifecycle.ts +13 -2
  160. package/src/linear/outbound.ts +43 -10
  161. package/src/oauth/keepalive.ts +13 -2
  162. package/src/prompts/base-prompt.ts +1 -1
  163. package/src/prompts/session-templates.ts +45 -7
  164. package/src/providers/claude-adapter.ts +6 -0
  165. package/src/providers/claude-managed-models.ts +34 -10
  166. package/src/providers/codex-adapter.ts +6 -0
  167. package/src/providers/opencode-adapter.ts +6 -0
  168. package/src/providers/pi-mono-adapter.ts +8 -0
  169. package/src/providers/pricing-sources.md +24 -0
  170. package/src/queue-stall-alarm.ts +216 -0
  171. package/src/scheduler/scheduler.ts +7 -0
  172. package/src/script-workflows/executor.ts +2 -1
  173. package/src/scripts-runtime/executors/native.ts +2 -1
  174. package/src/scripts-runtime/executors/types.ts +5 -0
  175. package/src/scripts-runtime/loader.ts +5 -0
  176. package/src/scripts-runtime/swarm-config.ts +8 -0
  177. package/src/scripts-runtime/swarm-sdk.ts +7 -0
  178. package/src/scripts-runtime/types/stdlib.d.ts +1 -0
  179. package/src/scripts-runtime/types/swarm-sdk.d.ts +1 -0
  180. package/src/tests/budget-claim-gate.test.ts +20 -0
  181. package/src/tests/claude-adapter.test.ts +32 -0
  182. package/src/tests/claude-managed-adapter.test.ts +38 -7
  183. package/src/tests/codex-adapter.test.ts +30 -0
  184. package/src/tests/create-task-validation.test.ts +128 -0
  185. package/src/tests/credential-wait.test.ts +67 -1
  186. package/src/tests/db-query.test.ts +711 -2
  187. package/src/tests/http-api-integration.test.ts +81 -0
  188. package/src/tests/http-users.test.ts +166 -1
  189. package/src/tests/identity-field-budget.test.ts +176 -0
  190. package/src/tests/mcp-tools-user.test.ts +34 -0
  191. package/src/tests/mcp-user-route.test.ts +17 -0
  192. package/src/tests/metrics-http.test.ts +44 -0
  193. package/src/tests/multi-runtime-registration.test.ts +2703 -0
  194. package/src/tests/openapi-response-contract.test.ts +24 -0
  195. package/src/tests/openapi-route-headers.test.ts +83 -0
  196. package/src/tests/opencode-adapter.test.ts +40 -0
  197. package/src/tests/outbound-listener-rejection.test.ts +138 -0
  198. package/src/tests/pi-mono-adapter.test.ts +98 -0
  199. package/src/tests/profile-sync-warning.test.ts +277 -0
  200. package/src/tests/profile-sync.test.ts +232 -6
  201. package/src/tests/prompt-template-session.test.ts +32 -3
  202. package/src/tests/queue-stall-alarm.test.ts +226 -0
  203. package/src/tests/requester-comms.test.ts +32 -0
  204. package/src/tests/runner-requester-profile.test.ts +38 -0
  205. package/src/tests/sandboxed-process.test.ts +45 -0
  206. package/src/tests/schedule-target-type.test.ts +38 -0
  207. package/src/tests/script-connections.test.ts +16 -3
  208. package/src/tests/scripts-external-api.test.ts +8 -3
  209. package/src/tests/scripts-http.test.ts +12 -0
  210. package/src/tests/scripts-retention.test.ts +384 -0
  211. package/src/tests/scripts-runtime-identity.test.ts +375 -0
  212. package/src/tests/scripts-runtime.test.ts +4 -3
  213. package/src/tests/sdk-allowlist.test.ts +47 -0
  214. package/src/tests/send-task-requested-by.test.ts +9 -0
  215. package/src/tests/session-costs.test.ts +506 -1
  216. package/src/tests/shutdown-error-scrubbing.test.ts +92 -0
  217. package/src/tests/steering-core.test.ts +28 -0
  218. package/src/tests/task-artifacts.test.ts +386 -0
  219. package/src/tests/task-attachments-schema.test.ts +17 -1
  220. package/src/tests/task-completion-idempotency.test.ts +57 -0
  221. package/src/tests/task-pull-request-backfill-migration.test.ts +321 -0
  222. package/src/tests/update-profile-api.test.ts +67 -39
  223. package/src/tests/update-profile-auth.test.ts +48 -4
  224. package/src/tests/user-attribution-migration.test.ts +54 -0
  225. package/src/tests/user-identity.test.ts +72 -0
  226. package/src/tests/workflow-async-v2.test.ts +39 -1
  227. package/src/tests/workflow-http-v2.test.ts +3 -2
  228. package/src/tests/workflow-mcp-trigger-schema.test.ts +52 -3
  229. package/src/tests/workflow-retry-v2.test.ts +45 -1
  230. package/src/tests/workflow-swarm-script.test.ts +14 -5
  231. package/src/tools/db-query.ts +18 -8
  232. package/src/tools/get-task-details.ts +24 -3
  233. package/src/tools/manage-user.ts +36 -3
  234. package/src/tools/poll-task.ts +93 -14
  235. package/src/tools/script-common.ts +5 -0
  236. package/src/tools/send-task.ts +1 -4
  237. package/src/tools/store-progress.ts +13 -2
  238. package/src/tools/swarm-config/delete-config.ts +9 -0
  239. package/src/tools/swarm-config/set-config.ts +3 -2
  240. package/src/tools/task-action.ts +30 -0
  241. package/src/tools/task-tool-ctx.ts +8 -1
  242. package/src/tools/update-profile.ts +16 -21
  243. package/src/tools/utils.ts +5 -0
  244. package/src/tools/workflows/trigger-workflow.ts +7 -1
  245. package/src/types.ts +165 -1
  246. package/src/utils/github-pull-request.ts +76 -0
  247. package/src/utils/identity-field-budget.ts +73 -0
  248. package/src/utils/multi-runtime.ts +20 -0
  249. package/src/utils/requester-comms.ts +24 -0
  250. package/src/utils/sandboxed-process.ts +40 -14
  251. package/src/workflows/engine.ts +22 -2
  252. package/src/workflows/executors/swarm-script.ts +24 -1
  253. package/src/workflows/resume.ts +19 -11
  254. package/src/workflows/retry-poller.ts +1 -0
  255. package/src/workflows/triggers.ts +4 -0
  256. package/templates/schedules/daily-compounding-reflection/content.md +12 -3
  257. package/templates/skills/db-query-guidance/config.json +14 -0
  258. package/templates/skills/db-query-guidance/content.md +57 -0
  259. package/dist/cli-3te4jdkm.js +0 -225
  260. package/dist/cli-bmze986d.js +0 -7
  261. package/dist/cli-kh3ba2zf.js +0 -607
  262. package/dist/index-hp1kdbjj.js +0 -36
  263. package/dist/mistral-conversations-81fwav0j.js +0 -30578
  264. package/dist/otel-nqa12wf3.js +0 -3149
  265. package/dist/{pi-messages-tqj8q6b9.js → pi-messages-sssy32x4.js} +3 -3
package/src/types.ts CHANGED
@@ -633,6 +633,102 @@ export const AgentTaskSchema = z
633
633
  })
634
634
  .openapi("AgentTask");
635
635
 
636
+ // ─── Create-task input contract ──────────────────────────────────────────────
637
+ //
638
+ // Runtime schema for `createTaskExtended` options (src/be/db.ts). Every task
639
+ // write — REST POST /api/tasks, MCP send-task, the scripts bridge, webhooks,
640
+ // the scheduler, and internal callers — funnels through that function, so
641
+ // this schema is the single enforcement point for task-input shapes. It
642
+ // rejects, never coerces: a type mismatch or out-of-range value throws, and
643
+ // only an ABSENT field falls back to the `??` defaults at the INSERT bind
644
+ // site. Deliberately NOT an `.openapi()` component: it is an internal write
645
+ // contract, not a response shape.
646
+ export const CreateTaskOptionsSchema = z.object({
647
+ key: z.string().min(1).optional(),
648
+ agentId: z.string().nullable().optional(),
649
+ creatorAgentId: z.string().optional(),
650
+ source: AgentTaskSourceSchema.optional(),
651
+ taskType: z.string().max(50).optional(),
652
+ tags: z.array(z.string()).optional(),
653
+ priority: z.number().int().min(0).max(100).optional(),
654
+ dependsOn: z.array(z.string()).optional(),
655
+ offeredTo: z.string().optional(),
656
+ /** Explicitly set initial status. */
657
+ status: z.enum(["backlog", "unassigned"]).optional(),
658
+ slackChannelId: z.string().optional(),
659
+ slackThreadTs: z.string().optional(),
660
+ /** Exact Slack message that directly triggered this task; never inherited. */
661
+ slackTriggerMessageTs: z.string().optional(),
662
+ slackUserId: z.string().optional(),
663
+ /**
664
+ * Opt out of the residual Slack/contextKey normalization (see the
665
+ * "Residual-mismatch guard" comment near the INSERT in src/be/db.ts): a
666
+ * deliberate cross-channel/thread dispatch (e.g. `send-task`'s
667
+ * `overrideSlackContext: true`) sets this so its explicit slackChannelId/
668
+ * slackThreadTs survive even when they disagree with a slack-family
669
+ * `contextKey`/parent. Trusted callers that don't set this get normalized —
670
+ * this boundary must not let a caller silently persist a mismatch.
671
+ */
672
+ overrideSlackContext: z.boolean().optional(),
673
+ vcsProvider: z.enum(["github", "gitlab"]).optional(),
674
+ vcsRepo: z.string().optional(),
675
+ vcsEventType: z.string().optional(),
676
+ vcsNumber: z.number().int().optional(),
677
+ vcsCommentId: z.number().int().optional(),
678
+ vcsAuthor: z.string().optional(),
679
+ vcsUrl: z.string().optional(),
680
+ vcsInstallationId: z.number().int().optional(),
681
+ vcsNodeId: z.string().optional(),
682
+ agentmailInboxId: z.string().optional(),
683
+ agentmailMessageId: z.string().optional(),
684
+ agentmailThreadId: z.string().optional(),
685
+ mentionMessageId: z.string().optional(),
686
+ mentionChannelId: z.string().optional(),
687
+ dir: z.string().optional(),
688
+ parentTaskId: z.string().optional(),
689
+ model: z.string().optional(),
690
+ modelTier: ModelTierSchema.optional(),
691
+ effort: ReasoningEffortSchema.optional(),
692
+ scheduleId: z.string().optional(),
693
+ workflowRunId: z.string().optional(),
694
+ workflowRunStepId: z.string().optional(),
695
+ sourceTaskId: z.string().optional(),
696
+ /**
697
+ * Optional JSON Schema the agent's final output must conform to.
698
+ *
699
+ * Enforced via the MCP `store-progress` tool (validated in
700
+ * `src/tools/store-progress.ts`). NOT enforced when the task runs on
701
+ * default-mode Devin (no MCP) — see runbooks/harness-providers.md
702
+ * ("Per-task outputSchema support"). Callers reading `task.output` for
703
+ * a schema'd task should be defensive about JSON parsing.
704
+ */
705
+ outputSchema: z.record(z.string(), z.unknown()).optional(),
706
+ /**
707
+ * When a `parentTaskId` is set, the child inherits the parent's `outputSchema`
708
+ * by default. Set this to `false` to opt out — used by control-plane children
709
+ * (e.g. the Lead `reroute-decision` task) that must inherit Slack/VCS context
710
+ * from the parent but must NOT be forced to satisfy the original work's output
711
+ * contract on completion (which would block the control task — DES-523).
712
+ */
713
+ inheritParentOutputSchema: z.boolean().optional(),
714
+ followUpConfig: FollowUpConfigSchema.optional(),
715
+ requestedByUserId: z.string().optional(),
716
+ contextKey: z.string().optional(),
717
+ /**
718
+ * Internal control-plane escape hatch for continuations that MUST coexist
719
+ * with their active Linear parent (for example promoted steering). General
720
+ * task creation keeps tracker-context dedup enabled.
721
+ */
722
+ bypassTrackerContextDedup: z.boolean().optional(),
723
+ /**
724
+ * Routing-affinity snapshot gating pool eligibility (see
725
+ * `isAgentEligibleForTask`). Inherited from the parent (via `parentTaskId`)
726
+ * when not explicitly set — same treatment as `vcsRepo`/`contextKey`.
727
+ */
728
+ routingAffinity: RoutingAffinitySchema.optional(),
729
+ });
730
+ export type CreateTaskOptions = z.infer<typeof CreateTaskOptionsSchema>;
731
+
636
732
  // ============================================================================
637
733
  // Task Attachments (Phase 1 — pointer-based artifacts)
638
734
  // ============================================================================
@@ -645,11 +741,19 @@ export const AgentTaskSchema = z
645
741
  export const TaskAttachmentKindSchema = z.enum(["agent-fs", "url", "shared-fs", "page"]);
646
742
  export type TaskAttachmentKind = z.infer<typeof TaskAttachmentKindSchema>;
647
743
 
744
+ export const SERVER_GENERATED_ATTACHMENT_CAPABILITY = "_agentSwarmGeneratedBy";
745
+
746
+ const attachmentCapabilitiesSchema = z
747
+ .record(z.string(), z.unknown())
748
+ .refine((capabilities) => !(SERVER_GENERATED_ATTACHMENT_CAPABILITY in capabilities), {
749
+ message: `${SERVER_GENERATED_ATTACHMENT_CAPABILITY} is reserved for server-generated attachments`,
750
+ });
751
+
648
752
  const attachmentCommonFields = {
649
753
  name: z.string().min(1).describe("Display name for the attachment."),
650
754
  providerId: z.string().min(1).optional(),
651
755
  providerKey: z.string().min(1).optional(),
652
- capabilities: z.record(z.string(), z.unknown()).optional(),
756
+ capabilities: attachmentCapabilitiesSchema.optional(),
653
757
  mimeType: z.string().optional(),
654
758
  sizeBytes: z.number().int().min(0).optional(),
655
759
  sha256: z.string().optional(),
@@ -755,6 +859,22 @@ export const UserSchema = z
755
859
 
756
860
  export type User = z.infer<typeof UserSchema>;
757
861
 
862
+ /**
863
+ * Structured communication preferences read from `users.metadata.comms`.
864
+ * All fields are free-form strings; values render verbatim into the requester
865
+ * profile prompt. Written merge-safely via the `comms` field on
866
+ * PATCH /api/users/{id} and the manage-user tool.
867
+ */
868
+ export const UserCommsPrefsSchema = z
869
+ .object({
870
+ tone: z.string().optional(),
871
+ language: z.string().optional(),
872
+ verbosity: z.string().optional(),
873
+ })
874
+ .openapi("UserCommsPrefs");
875
+
876
+ export type UserCommsPrefs = z.infer<typeof UserCommsPrefsSchema>;
877
+
758
878
  /**
759
879
  * Identity event types — mirrored in lockstep with the CHECK constraint on
760
880
  * `user_identity_events.eventType` in migration 064. Drift breaks helper
@@ -1338,6 +1458,8 @@ export const EventNameSchema = z.enum([
1338
1458
  "system.boot",
1339
1459
  "system.migration",
1340
1460
  "system.error",
1461
+ "system.profile_sync_rejected",
1462
+ "system.profile_sync_reconciled",
1341
1463
  // Script catalog events
1342
1464
  "script.global_upsert",
1343
1465
  // Schedule events
@@ -1563,6 +1685,8 @@ export const ActiveSessionSchema = z
1563
1685
  taskDescription: z.string().nullable(),
1564
1686
  runnerSessionId: z.string().nullable(),
1565
1687
  providerSessionId: z.string().nullable(),
1688
+ /** Runtime process that owns this session; null for single-runtime rows. */
1689
+ runtimeInstanceId: z.string().nullable().optional(),
1566
1690
  startedAt: z.iso.datetime(),
1567
1691
  lastHeartbeatAt: z.iso.datetime(),
1568
1692
  })
@@ -1570,6 +1694,45 @@ export const ActiveSessionSchema = z
1570
1694
 
1571
1695
  export type ActiveSession = z.infer<typeof ActiveSessionSchema>;
1572
1696
 
1697
+ // ============================================================================
1698
+ // Runtime Instance Types (multi-runtime worker tracking)
1699
+ // ============================================================================
1700
+
1701
+ /**
1702
+ * Lifecycle of a runtime instance. Zod is the source of truth for the allowed
1703
+ * values — the SQL column has no CHECK constraint (migration 132) so adding a
1704
+ * state doesn't require a table rebuild.
1705
+ */
1706
+ export const RuntimeInstanceStatusSchema = z.enum(["active", "offline"]);
1707
+
1708
+ /**
1709
+ * One worker process serving a logical agent. The `agents` row (AGENT_ID /
1710
+ * X-Agent-ID) keeps durable identity and the logical maxTasks policy; a
1711
+ * runtime instance carries only process-scoped state: liveness and its own
1712
+ * reported execution capacity. A logical agent may be served by 0..N runtime
1713
+ * instances; rows exist only for multi-runtime registrations
1714
+ * (MULTI_RUNTIME_ENABLED).
1715
+ */
1716
+ export const RuntimeInstanceSchema = z
1717
+ .object({
1718
+ id: z.string(),
1719
+ agentId: z.string(),
1720
+ status: RuntimeInstanceStatusSchema,
1721
+ /** Runtime-local concurrent task capacity, self-reported at registration. */
1722
+ reportedSlots: z.number().int(),
1723
+ /** Process-local credential readiness; null when never reported. */
1724
+ credentialReady: z.boolean().nullable().optional(),
1725
+ /** Provider-neutral metadata; unused in the initial slice. */
1726
+ metadata: z.record(z.string(), z.unknown()).nullable(),
1727
+ lastSeenAt: z.iso.datetime(),
1728
+ createdAt: z.iso.datetime(),
1729
+ updatedAt: z.iso.datetime(),
1730
+ })
1731
+ .openapi("RuntimeInstance");
1732
+
1733
+ export type RuntimeInstanceStatus = z.infer<typeof RuntimeInstanceStatusSchema>;
1734
+ export type RuntimeInstance = z.infer<typeof RuntimeInstanceSchema>;
1735
+
1573
1736
  // ============================================================================
1574
1737
  // Workflow Engine Types
1575
1738
  // ============================================================================
@@ -2238,6 +2401,7 @@ export const WorkflowRunSchema = z
2238
2401
  triggerData: z.unknown().optional(),
2239
2402
  context: z.record(z.string(), z.unknown()).optional(),
2240
2403
  error: z.string().optional(),
2404
+ createdBy: z.string().optional(),
2241
2405
  startedAt: z.string(),
2242
2406
  lastUpdatedAt: z.string(),
2243
2407
  finishedAt: z.string().optional(),
@@ -0,0 +1,76 @@
1
+ export interface GitHubPullRequestUrl {
2
+ url: string;
3
+ owner: string;
4
+ repo: string;
5
+ number: number;
6
+ }
7
+
8
+ const CANDIDATE_TOKEN_RE = /[^\s()[\]{}<>"'`]+/g;
9
+ const TRAILING_PROSE_PUNCTUATION_RE = /[)\]},.;:!]+$/;
10
+ const GITHUB_PATH_SEGMENT_RE = /^[\w.-]+$/;
11
+
12
+ function parseGitHubPullRequestToken(token: string): GitHubPullRequestUrl | null {
13
+ const candidate = token.replace(TRAILING_PROSE_PUNCTUATION_RE, "");
14
+ const lowerCandidate = candidate.toLowerCase();
15
+ if (
16
+ !lowerCandidate.startsWith("https://github.com/") &&
17
+ !lowerCandidate.startsWith("http://github.com/") &&
18
+ !lowerCandidate.startsWith("github.com/")
19
+ ) {
20
+ return null;
21
+ }
22
+ const withScheme = /^https?:\/\//i.test(candidate) ? candidate : `https://${candidate}`;
23
+
24
+ let parsed: URL;
25
+ try {
26
+ parsed = new URL(withScheme);
27
+ } catch {
28
+ return null;
29
+ }
30
+ if (
31
+ parsed.host.toLowerCase() !== "github.com" ||
32
+ parsed.username.length > 0 ||
33
+ parsed.password.length > 0
34
+ ) {
35
+ return null;
36
+ }
37
+
38
+ const [owner, repo, pull, numberText] = parsed.pathname.split("/").filter(Boolean);
39
+ if (
40
+ !owner ||
41
+ !repo ||
42
+ pull?.toLowerCase() !== "pull" ||
43
+ !numberText ||
44
+ !GITHUB_PATH_SEGMENT_RE.test(owner) ||
45
+ !GITHUB_PATH_SEGMENT_RE.test(repo) ||
46
+ !/^\d+$/.test(numberText)
47
+ ) {
48
+ return null;
49
+ }
50
+
51
+ return {
52
+ url: `https://github.com/${owner}/${repo}/pull/${numberText}`,
53
+ owner,
54
+ repo,
55
+ number: Number(numberText),
56
+ };
57
+ }
58
+
59
+ /** Extract distinct canonical GitHub pull-request URLs from free text. */
60
+ export function extractGitHubPullRequestUrls(
61
+ text: string | null | undefined,
62
+ ): GitHubPullRequestUrl[] {
63
+ if (!text) return [];
64
+
65
+ const results: GitHubPullRequestUrl[] = [];
66
+ const seen = new Set<string>();
67
+ for (const match of text.matchAll(CANDIDATE_TOKEN_RE)) {
68
+ const pullRequest = parseGitHubPullRequestToken(match[0]);
69
+ if (!pullRequest) continue;
70
+ const dedupeKey = pullRequest.url.toLowerCase();
71
+ if (seen.has(dedupeKey)) continue;
72
+ seen.add(dedupeKey);
73
+ results.push(pullRequest);
74
+ }
75
+ return results;
76
+ }
@@ -0,0 +1,73 @@
1
+ import { BOOTSTRAP_MAX_CHARS } from "../prompts/base-prompt";
2
+
3
+ export const SOUL_MD_MAX_CHARS = 10_000;
4
+ export const IDENTITY_MD_MAX_CHARS = 10_000;
5
+
6
+ export const IDENTITY_FIELD_BUDGETS = {
7
+ soulMd: SOUL_MD_MAX_CHARS,
8
+ identityMd: IDENTITY_MD_MAX_CHARS,
9
+ claudeMd: BOOTSTRAP_MAX_CHARS,
10
+ toolsMd: BOOTSTRAP_MAX_CHARS,
11
+ } as const;
12
+
13
+ export type BudgetedIdentityField = keyof typeof IDENTITY_FIELD_BUDGETS;
14
+
15
+ export interface IdentityFieldBudgetRejection {
16
+ field: BudgetedIdentityField;
17
+ dbSize: number;
18
+ diskSize: number;
19
+ budget: number;
20
+ delta: number;
21
+ reason: string;
22
+ }
23
+
24
+ export type IdentityFieldBudgetResult =
25
+ | { ok: true }
26
+ | ({ ok: false } & IdentityFieldBudgetRejection);
27
+
28
+ export function checkIdentityFieldBudget({
29
+ field,
30
+ currentValue,
31
+ nextValue,
32
+ }: {
33
+ field: BudgetedIdentityField;
34
+ currentValue: string;
35
+ nextValue: string;
36
+ }): IdentityFieldBudgetResult {
37
+ const budget = IDENTITY_FIELD_BUDGETS[field];
38
+ if (nextValue.length <= budget || nextValue.length <= currentValue.length) {
39
+ return { ok: true };
40
+ }
41
+
42
+ const delta = nextValue.length - currentValue.length;
43
+ const remediation =
44
+ field === "toolsMd"
45
+ ? ` Content past the ${budget}-character cap is already silently dropped at read time, so shrinking that tail loses nothing sessions currently receive. Move durable content into memories and keep pointers to it in this field.`
46
+ : field === "claudeMd"
47
+ ? ` The tail past the ${budget}-character cap is dropped from the base prompt and only reaches harnesses with a native CLAUDE.md loader. Move durable content into memories and keep pointers to it in this field.`
48
+ : " Move durable content into memories and keep pointers to it in this field.";
49
+
50
+ return {
51
+ ok: false,
52
+ field,
53
+ dbSize: currentValue.length,
54
+ diskSize: nextValue.length,
55
+ budget,
56
+ delta,
57
+ reason:
58
+ `Update rejected for ${field}: current size ${currentValue.length} characters, ` +
59
+ `budget ${budget} characters, delta ${delta >= 0 ? "+" : ""}${delta} characters.` +
60
+ remediation,
61
+ };
62
+ }
63
+
64
+ export class IdentityFieldBudgetError extends Error {
65
+ constructor(
66
+ readonly rejection: IdentityFieldBudgetRejection,
67
+ readonly dbHash: string,
68
+ readonly diskHash: string,
69
+ ) {
70
+ super(rejection.reason);
71
+ this.name = "IdentityFieldBudgetError";
72
+ }
73
+ }
@@ -0,0 +1,20 @@
1
+ import { parseEnvFlag } from "./env-flag";
2
+
3
+ /**
4
+ * Opt-in, default off: allows one logical agent to be served by several
5
+ * worker processes. Lives outside `src/be` so worker-side code can import it
6
+ * without crossing the DB boundary.
7
+ */
8
+ export function isMultiRuntimeEnabled(env: NodeJS.ProcessEnv = process.env): boolean {
9
+ return parseEnvFlag(env.MULTI_RUNTIME_ENABLED, false);
10
+ }
11
+
12
+ /**
13
+ * Per-boot runtime identity the runner stamps into the process environment
14
+ * before any provider session exists. Adapters read it here to send the
15
+ * X-Runtime-Instance-ID header on the swarm MCP session — runtime identity is
16
+ * process/auth context, never a tool argument.
17
+ */
18
+ export function swarmRuntimeInstanceId(env: NodeJS.ProcessEnv = process.env): string | undefined {
19
+ return env.SWARM_RUNTIME_INSTANCE_ID || undefined;
20
+ }
@@ -0,0 +1,24 @@
1
+ import type { User, UserCommsPrefs } from "../types";
2
+
3
+ function pickString(value: unknown): string | undefined {
4
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
5
+ }
6
+
7
+ /**
8
+ * Extract structured communication preferences from `users.metadata.comms`.
9
+ * `metadata` is a free-form JSON blob with no write-side validation, so only
10
+ * non-empty string fields survive; returns undefined when nothing usable is set.
11
+ */
12
+ export function getUserCommsPrefs(
13
+ user: Pick<User, "metadata"> | null | undefined,
14
+ ): UserCommsPrefs | undefined {
15
+ const comms = user?.metadata?.comms;
16
+ if (!comms || typeof comms !== "object" || Array.isArray(comms)) return undefined;
17
+ const record = comms as Record<string, unknown>;
18
+ const prefs: UserCommsPrefs = {
19
+ tone: pickString(record.tone),
20
+ language: pickString(record.language),
21
+ verbosity: pickString(record.verbosity),
22
+ };
23
+ return prefs.tone || prefs.language || prefs.verbosity ? prefs : undefined;
24
+ }
@@ -35,20 +35,36 @@ export const DEFAULT_SANDBOX_LIMITS: SandboxResourceLimits = {
35
35
  };
36
36
 
37
37
  /**
38
- * Bun's Linux runtime reserves several GB of virtual address space at
39
- * startup, so any sandboxed command whose argv[0] is `bun` needs a much
40
- * higher RLIMIT_AS floor than a plain shell/python process or the harness is
41
- * killed before user code ever runs. `buildSandboxedCommand` applies this
42
- * automatically — callers don't need to special-case it.
38
+ * Bun and Node reserve several GB of virtual address space and start runtime
39
+ * threads before user code runs. Give their process trees enough RLIMIT_AS
40
+ * and RLIMIT_NPROC headroom to boot while retaining the stricter defaults for
41
+ * direct non-interpreter commands.
43
42
  */
44
43
  export const BUN_SANDBOX_VIRTUAL_MEMORY_MB = 4096;
44
+ export const JAVASCRIPT_RUNTIME_SANDBOX_MAX_PROCS = 512;
45
+
46
+ const JAVASCRIPT_RUNTIMES = new Set(["bun", "bunx", "node", "nodejs", "npx"]);
47
+ const SHELL_RUNTIMES = new Set(["bash", "dash", "sh"]);
48
+
49
+ function commandBasename(command: string): string {
50
+ return command.slice(command.lastIndexOf("/") + 1);
51
+ }
52
+
53
+ function usesInterpreterSandboxProfile(innerCommand: readonly string[]): boolean {
54
+ const runtime = commandBasename(innerCommand[0] ?? "");
55
+ // A shell can invoke another interpreter through arbitrary valid shell
56
+ // grammar (substitutions, functions, variables, heredocs, and more). Treat
57
+ // the shell itself as the runtime boundary instead of letting untrusted
58
+ // source text select its own resource profile through brittle parsing.
59
+ return JAVASCRIPT_RUNTIMES.has(runtime) || SHELL_RUNTIMES.has(runtime);
60
+ }
45
61
 
46
62
  function shellQuote(value: string): string {
47
63
  return `'${value.replaceAll("'", "'\\''")}'`;
48
64
  }
49
65
 
50
66
  /**
51
- * Wrap `innerCommand` in a POSIX `sh -c` prelude that applies the resource
67
+ * Wrap `innerCommand` in a shell prelude that applies the resource
52
68
  * ulimits above, then `exec`s into a completely clean environment (`env -i`)
53
69
  * containing ONLY the entries in `env`. This is the enforcement point for
54
70
  * two invariants every spawn-user-code path must uphold:
@@ -60,7 +76,10 @@ function shellQuote(value: string): string {
60
76
  * token / API key in `env`; pass secrets over stdin instead so they
61
77
  * never appear in `/proc/<pid>/environ` or a child's `process.env`.
62
78
  *
63
- * No-ops (returns `innerCommand` unchanged) on win32, where `ulimit`/`env -i`
79
+ * JavaScript-capable runtime trees use Bash intentionally: Ubuntu's `/bin/sh` is
80
+ * dash, whose `ulimit` does not implement `-u` (RLIMIT_NPROC). Other commands
81
+ * retain the existing POSIX-sh sandbox behavior and its strict profile. No-ops
82
+ * (returns `innerCommand` unchanged) on win32, where `ulimit`/`env -i`
64
83
  * don't exist — matches the existing native.ts behavior. Because this no-op
65
84
  * means `env` is never applied on win32, callers MUST pass `env` itself
66
85
  * (not just `{ PATH }`) to `Bun.spawn` on that platform — use
@@ -74,15 +93,18 @@ export function buildSandboxedCommand(
74
93
  ): string[] {
75
94
  if (process.platform === "win32") return [...innerCommand];
76
95
 
77
- const virtualMemoryMb =
78
- innerCommand[0] === "bun"
79
- ? Math.max(limits.virtualMemoryMb, BUN_SANDBOX_VIRTUAL_MEMORY_MB)
80
- : limits.virtualMemoryMb;
96
+ const usesInterpreterProfile = usesInterpreterSandboxProfile(innerCommand);
97
+ const virtualMemoryMb = usesInterpreterProfile
98
+ ? Math.max(limits.virtualMemoryMb, BUN_SANDBOX_VIRTUAL_MEMORY_MB)
99
+ : limits.virtualMemoryMb;
100
+ const maxProcs = usesInterpreterProfile
101
+ ? Math.max(limits.maxProcs, JAVASCRIPT_RUNTIME_SANDBOX_MAX_PROCS)
102
+ : limits.maxProcs;
81
103
 
82
104
  const ulimits = [
83
105
  `ulimit -v ${Math.floor(virtualMemoryMb * 1024)} 2>/dev/null || true`,
84
106
  `ulimit -t ${limits.cpuTimeSec} 2>/dev/null || true`,
85
- `ulimit -u ${limits.maxProcs} 2>/dev/null || true`,
107
+ `ulimit -u ${maxProcs} 2>/dev/null || true`,
86
108
  `ulimit -f ${limits.maxFileKb} 2>/dev/null || true`,
87
109
  `ulimit -n ${limits.maxFdCount} 2>/dev/null || true`,
88
110
  ].join("; ");
@@ -92,7 +114,11 @@ export function buildSandboxedCommand(
92
114
  .join(" ");
93
115
  const quotedInner = innerCommand.map(shellQuote).join(" ");
94
116
 
95
- return ["sh", "-c", `${ulimits}; exec env -i ${envAssignments} ${quotedInner}`];
117
+ return [
118
+ usesInterpreterProfile ? "bash" : "sh",
119
+ "-c",
120
+ `${ulimits}; exec env -i ${envAssignments} ${quotedInner}`,
121
+ ];
96
122
  }
97
123
 
98
124
  /**
@@ -101,7 +127,7 @@ export function buildSandboxedCommand(
101
127
  *
102
128
  * On POSIX, `buildSandboxedCommand` wraps the command in an `env -i` prelude
103
129
  * that injects `env` itself into the child — Bun.spawn only needs PATH to
104
- * locate the `sh` binary that runs the prelude.
130
+ * locate the shell binary that runs the prelude.
105
131
  *
106
132
  * On win32, `buildSandboxedCommand` is a no-op (no `ulimit`/`env -i`), so
107
133
  * there is no prelude to inject `env` — it must go through Bun.spawn's own
@@ -76,7 +76,13 @@ export async function startWorkflowExecution(
76
76
  // Cooldown check
77
77
  if (workflow.cooldown && shouldSkipCooldown(workflow.id, workflow.cooldown)) {
78
78
  const runId = crypto.randomUUID();
79
- createWorkflowRun({ id: runId, workflowId: workflow.id, triggerData });
79
+ createWorkflowRun({
80
+ id: runId,
81
+ workflowId: workflow.id,
82
+ triggerType: options.triggerType ?? "manual",
83
+ triggerData,
84
+ createdBy: options.requestedByUserId,
85
+ });
80
86
  updateWorkflowRun(runId, {
81
87
  status: "skipped",
82
88
  error: "cooldown",
@@ -86,7 +92,13 @@ export async function startWorkflowExecution(
86
92
  }
87
93
 
88
94
  const runId = crypto.randomUUID();
89
- createWorkflowRun({ id: runId, workflowId: workflow.id, triggerData });
95
+ createWorkflowRun({
96
+ id: runId,
97
+ workflowId: workflow.id,
98
+ triggerType: options.triggerType ?? "manual",
99
+ triggerData,
100
+ createdBy: options.requestedByUserId,
101
+ });
90
102
  telemetry.workflow("started", {
91
103
  workflowId: workflow.id,
92
104
  nodeCount: workflow.definition.nodes.length,
@@ -160,6 +172,14 @@ export async function walkGraph(
160
172
  secretKeys: Set<string> = new Set(),
161
173
  options: WorkflowExecutionOptions = {},
162
174
  ): Promise<void> {
175
+ // Initial triggers carry the requester in options. Resumes, retries, and
176
+ // recovery start from persisted state, so rehydrate it from the run.
177
+ if (!options.requestedByUserId) {
178
+ options = {
179
+ ...options,
180
+ requestedByUserId: getWorkflowRun(runId)?.createdBy,
181
+ };
182
+ }
163
183
  let nodeExecutionCount = 0;
164
184
  // `run.id` is a context builtin (like `trigger` / `input`) so nodes can reference
165
185
  // their own run — e.g. an audit/receipt node correlating its output with the run
@@ -4,7 +4,12 @@ import {
4
4
  getScriptMcpConnectionDescriptors,
5
5
  } from "../../be/script-connections";
6
6
  import { buildScriptCredentialBindingsWithFailures } from "../../be/script-credential-broker";
7
- import { getScript, getScriptVersion } from "../../be/scripts/db";
7
+ import {
8
+ getScript,
9
+ getScriptVersion,
10
+ restoreScratchScriptLastUsedIfUnchanged,
11
+ touchScratchScriptLastUsed,
12
+ } from "../../be/scripts/db";
8
13
  import {
9
14
  DEFAULT_SCRIPT_RESOURCES,
10
15
  MAX_SCRIPT_WALL_CLOCK_MS,
@@ -104,6 +109,12 @@ export class SwarmScriptExecutor extends BaseExecutor<
104
109
  };
105
110
  }
106
111
 
112
+ // Touch before executing so an already-stale scratch script isn't reaped
113
+ // by the retention sweep while this run is still in flight.
114
+ const runStartTouch = resolved.script.isScratch
115
+ ? touchScratchScriptLastUsed(resolved.script.id)
116
+ : null;
117
+
107
118
  const credentials = await buildScriptCredentialBindingsWithFailures({
108
119
  agentId: agentId ?? undefined,
109
120
  });
@@ -118,6 +129,18 @@ export class SwarmScriptExecutor extends BaseExecutor<
118
129
  mcpConnections: getScriptMcpConnectionDescriptors({ agentId: agentId ?? undefined }),
119
130
  timeoutMs: config.timeoutMs,
120
131
  });
132
+ if (output.exitCode === 0 && !output.error && !output.runtimeError) {
133
+ touchScratchScriptLastUsed(resolved.script.id);
134
+ } else if (resolved.script.isScratch && runStartTouch) {
135
+ // Failed run — restore the pre-run timestamp so it doesn't buy the
136
+ // script another retention window, unless a concurrent run already
137
+ // touched it since.
138
+ restoreScratchScriptLastUsedIfUnchanged(
139
+ resolved.script.id,
140
+ resolved.script.updatedAt,
141
+ runStartTouch,
142
+ );
143
+ }
121
144
 
122
145
  const workflowOutput = {
123
146
  result: output.result,
@@ -13,6 +13,7 @@ import {
13
13
  updateWorkflowRun,
14
14
  updateWorkflowRunStep,
15
15
  } from "../be/db";
16
+ import { scrubSecrets } from "../utils/secret-scrubber";
16
17
  import { checkpointStep } from "./checkpoint";
17
18
  import { FAILED_TASK_OUTPUT_PREFIX } from "./constants";
18
19
  import { getSuccessors } from "./definition";
@@ -51,9 +52,9 @@ export function setupWorkflowResumeListener(
51
52
  registry: ExecutorRegistry,
52
53
  ): void {
53
54
  eventBus.on("task.completed", async (data: unknown) => {
54
- const event = data as TaskEvent;
55
- if (!event.workflowRunId || !event.workflowRunStepId) return;
56
55
  try {
56
+ const event = data as TaskEvent;
57
+ if (!event.workflowRunId || !event.workflowRunStepId) return;
57
58
  await resumeFromTaskCompletion(event, registry);
58
59
  } catch (err) {
59
60
  console.error("[workflows] Resume from task completion failed:", err);
@@ -61,9 +62,9 @@ export function setupWorkflowResumeListener(
61
62
  });
62
63
 
63
64
  eventBus.on("task.failed", async (data: unknown) => {
64
- const event = data as TaskEvent;
65
- if (!event.workflowRunId || !event.workflowRunStepId) return;
66
65
  try {
66
+ const event = data as TaskEvent;
67
+ if (!event.workflowRunId || !event.workflowRunStepId) return;
67
68
  await handleTaskFailure(event, event.failureReason ?? "Task failed", registry);
68
69
  } catch (err) {
69
70
  console.error("[workflows] Handle task failure error:", err);
@@ -71,9 +72,9 @@ export function setupWorkflowResumeListener(
71
72
  });
72
73
 
73
74
  eventBus.on("task.cancelled", async (data: unknown) => {
74
- const event = data as TaskEvent;
75
- if (!event.workflowRunId || !event.workflowRunStepId) return;
76
75
  try {
76
+ const event = data as TaskEvent;
77
+ if (!event.workflowRunId || !event.workflowRunStepId) return;
77
78
  await handleTaskFailure(event, "Task was cancelled", registry);
78
79
  } catch (err) {
79
80
  console.error("[workflows] Handle task cancellation error:", err);
@@ -81,9 +82,9 @@ export function setupWorkflowResumeListener(
81
82
  });
82
83
 
83
84
  eventBus.on("approval.resolved", async (data: unknown) => {
84
- const event = data as ApprovalEvent;
85
- if (!event.workflowRunId || !event.workflowRunStepId) return;
86
85
  try {
86
+ const event = data as ApprovalEvent;
87
+ if (!event.workflowRunId || !event.workflowRunStepId) return;
87
88
  await resumeFromApprovalResolution(event, registry);
88
89
  } catch (err) {
89
90
  console.error("[workflows] Resume from approval resolution failed:", err);
@@ -589,9 +590,16 @@ function registerWait(waitId: string, eventName: string): void {
589
590
 
590
591
  if (!listenersByEvent.has(eventName)) {
591
592
  const listener = (data: unknown) => {
592
- // Fire-and-forget: don't block the bus thread. Errors are logged
593
- // per-wait inside processBusEvent.
594
- void processBusEvent(eventName, data);
593
+ // Fire-and-forget: don't block the bus thread. Per-wait errors are
594
+ // logged inside processBusEvent's loop, but the code around that loop
595
+ // is not covered by it — and the emitter cannot observe this promise,
596
+ // since EventEmitter discards whatever a listener returns.
597
+ processBusEvent(eventName, data).catch((err) => {
598
+ console.error(
599
+ `[workflows] Wait bus listener failed for event=${eventName}:`,
600
+ scrubSecrets(err instanceof Error ? err.message : String(err)),
601
+ );
602
+ });
595
603
  };
596
604
  listenersByEvent.set(eventName, listener);
597
605
  workflowEventBus.on(eventName, listener);