@desplega.ai/agent-swarm 1.157.0 → 1.158.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. package/dist/{acp-adapter-qhqrk4q0.js → acp-adapter-xaq4n6pc.js} +1 -1
  2. package/dist/{actions-9rrg0zxm.js → actions-p9hz5yat.js} +15 -15
  3. package/dist/{app-enysy1zd.js → app-g26wdcz6.js} +4 -4
  4. package/dist/{artifact-td4q4dch.js → artifact-3v5t9jtg.js} +1 -1
  5. package/dist/{assistant-zegamy48.js → assistant-wydfqkb3.js} +16 -16
  6. package/dist/{boot-reembed-r6shx5qx.js → boot-reembed-g5ckg9yb.js} +6 -6
  7. package/dist/{boot-reembed-zfxb937k.js → boot-reembed-se8zmdcp.js} +5 -5
  8. package/dist/{boot-scrub-logs-k5y0se6r.js → boot-scrub-logs-wfxqan61.js} +3 -3
  9. package/dist/{seed-pricing-vqxa06qf.js → boot-seeds-s3q01xj3.js} +10 -10
  10. package/dist/{claude-adapter-5yc7hxwv.js → claude-adapter-tv2x4f36.js} +7 -7
  11. package/dist/{claude-managed-adapter-ea2aga6w.js → claude-managed-adapter-41b2s7fd.js} +2 -2
  12. package/dist/{claude-sdk-session-j4ywn3xf.js → claude-sdk-session-t442qz6f.js} +8 -8
  13. package/dist/{cli-e781c6f9.js → cli-0xjkj1j7.js} +1 -0
  14. package/dist/{cli-58rgvkt4.js → cli-1hkrch3k.js} +2 -2
  15. package/dist/{cli-yscm6cv8.js → cli-20cj37vt.js} +1866 -9
  16. package/dist/{cli-h0wdadsx.js → cli-4198qc9v.js} +7 -3
  17. package/dist/{cli-awwphhcs.js → cli-47m6dkd3.js} +362 -126
  18. package/dist/{cli-wb2zvbfg.js → cli-53sz8gkz.js} +1 -1
  19. package/dist/{cli-005pze6s.js → cli-5d10vd2m.js} +1 -1
  20. package/dist/{cli-v53rbqp3.js → cli-65hjmev5.js} +57 -17
  21. package/dist/{cli-scwq51gn.js → cli-68w09sjw.js} +7 -1
  22. package/dist/{cli-s98skd5r.js → cli-7gtvvpbh.js} +4 -4
  23. package/dist/{cli-y3nxwx0t.js → cli-7s3syk22.js} +18 -11
  24. package/dist/{cli-74h6f2bp.js → cli-8qmme6vd.js} +2 -2
  25. package/dist/{cli-wsed24mk.js → cli-987hg0aa.js} +2 -2
  26. package/dist/{cli-9n76jszt.js → cli-9httdpyq.js} +1 -1
  27. package/dist/{cli-f3va018d.js → cli-aky8tctm.js} +26 -93
  28. package/dist/{cli-sgyqyyxe.js → cli-c07r9a2m.js} +2 -1
  29. package/dist/{cli-c41wjx0r.js → cli-fgqxwrpv.js} +291 -12
  30. package/dist/{cli-01vfmq8c.js → cli-ftfvggzx.js} +607 -164
  31. package/dist/{cli-ytxs7mn1.js → cli-g8djp6qe.js} +17 -0
  32. package/dist/{cli-avkd6mqv.js → cli-h967nb6r.js} +551 -0
  33. package/dist/{cli-r9ee9c4w.js → cli-j55n64b6.js} +23 -22
  34. package/dist/{cli-9p33cz9h.js → cli-j57qkrpq.js} +86 -28
  35. package/dist/{cli-tjmghmm5.js → cli-q67y4wzm.js} +6 -3
  36. package/dist/{cli-n6zq3kbd.js → cli-rj19hc34.js} +1 -1
  37. package/dist/{cli-jytrw2na.js → cli-rjehvrts.js} +4 -4
  38. package/dist/{cli-tpv97m71.js → cli-rqz8qbw7.js} +245 -75
  39. package/dist/{cli-p37sg0a9.js → cli-s8eet254.js} +1 -1
  40. package/dist/cli-t1ze64h3.js +811 -0
  41. package/dist/{cli-6jjzxv3b.js → cli-ws6rqycm.js} +3 -2
  42. package/dist/{cli-xf5b7y1f.js → cli-wscdvcqz.js} +13 -5
  43. package/dist/{cli-5j7bm4tb.js → cli-x24pshtq.js} +5 -5
  44. package/dist/{cli-nbp6m6gp.js → cli-xberxa1r.js} +51 -26
  45. package/dist/{cli-vx9p3qdt.js → cli-xhaeg2nc.js} +1 -1
  46. package/dist/cli.js +29 -15
  47. package/dist/{codex-adapter-gns3s6c3.js → codex-adapter-cnm8wk11.js} +5 -5
  48. package/dist/{codex-session-runner-gqnxys5w.js → codex-session-runner-gfg547zj.js} +5 -5
  49. package/dist/{commands-hxy2y2sp.js → commands-e3bn0p75.js} +3 -3
  50. package/dist/{db-w8ghhsgf.js → db-03v9kzr2.js} +17 -3
  51. package/dist/{dispatcher-cp1rknjz.js → dispatcher-nrmg8yht.js} +14 -14
  52. package/dist/{dsh-adapter-1dwg97zv.js → dsh-adapter-k03nfqa6.js} +2 -2
  53. package/dist/{handlers-egfhr351.js → handlers-aqcedbyb.js} +17 -17
  54. package/dist/{hook-x4kftwzt.js → hook-7vkskkre.js} +19 -11
  55. package/dist/{hook-fbxjqfjz.js → hook-ybvmrmhq.js} +4 -4
  56. package/dist/{http-c7jfx990.js → http-2eqa8t24.js} +432 -195
  57. package/dist/{index-4k9hct21.js → index-1yks3qcr.js} +14 -14
  58. package/dist/{index-jtvpgb8f.js → index-a3vn96n8.js} +30 -27
  59. package/dist/{index-dc2ek6bw.js → index-c6zsv1bd.js} +5 -5
  60. package/dist/{index-yy07ef2s.js → index-chgdkq36.js} +14 -14
  61. package/dist/{index-r6bty6tb.js → index-x71bvq0p.js} +15 -15
  62. package/dist/{keepalive-f7ekefyc.js → keepalive-vtkf5n99.js} +5 -5
  63. package/dist/{lead-h053syts.js → lead-h3sp357x.js} +26 -26
  64. package/dist/{maintenance-a3w6zs54.js → maintenance-jzeqtk3q.js} +6 -6
  65. package/dist/{oauth-refresh-sweep-vra1dhkd.js → oauth-refresh-sweep-qp3dzmdp.js} +4 -4
  66. package/dist/{onboard-mfgzeqxs.js → onboard-tmsp0qbz.js} +278 -81
  67. package/dist/{opencode-adapter-z64xjzec.js → opencode-adapter-x6x2da1z.js} +3 -3
  68. package/dist/{otel-impl-2ez6xk57.js → otel-impl-9mz73q65.js} +416 -301
  69. package/dist/{pi-mono-adapter-2b7mw16r.js → pi-mono-adapter-m6dvv0dz.js} +2 -2
  70. package/dist/{pricing-refresh-6sw65tck.js → pricing-refresh-v1cbr9te.js} +5 -5
  71. package/dist/{rbac-roles-nm6924sy.js → rbac-roles-c9wqz83x.js} +4 -4
  72. package/dist/{render-v2-f026j6c4.js → render-v2-gryrh9h8.js} +4 -4
  73. package/dist/{setup-w606fr1q.js → setup-q9720bt6.js} +2 -2
  74. package/dist/{virtual-modules-8qheft18.js → virtual-modules-3htpgn8x.js} +1 -1
  75. package/dist/{work-objects-ezmggdne.js → work-objects-b76x7deb.js} +17 -17
  76. package/dist/{worker-1mbyv29k.js → worker-6vrxbv41.js} +26 -26
  77. package/openapi.json +1626 -629
  78. package/package.json +2 -1
  79. package/src/apps/catalog.generated.json +229 -0
  80. package/src/apps/page-validator.ts +71 -16
  81. package/src/apps/row-store.ts +66 -11
  82. package/src/apps/sync.ts +59 -17
  83. package/src/artifact-sdk/browser-sdk.ts +436 -2
  84. package/src/be/approval-sweeps.ts +109 -0
  85. package/src/be/boot-seeds.ts +40 -0
  86. package/src/be/chunking.ts +1 -1
  87. package/src/be/db/agents.ts +20 -0
  88. package/src/be/db/approvals.ts +78 -0
  89. package/src/be/db/tasks/read.ts +197 -39
  90. package/src/be/db/tasks/write.ts +41 -9
  91. package/src/be/db-client.ts +107 -48
  92. package/src/be/db.ts +219 -76
  93. package/src/be/events.ts +33 -1
  94. package/src/be/memory/graph-expansion.ts +10 -3
  95. package/src/be/memory/providers/sqlite-store.ts +55 -13
  96. package/src/be/memory/raters/retrieval.ts +5 -2
  97. package/src/be/memory/reranker.ts +26 -2
  98. package/src/be/memory/types.ts +2 -0
  99. package/src/be/migrations/166_approval_request_auto_cancellation.sql +9 -0
  100. package/src/be/migrations/171_memory_retrieval_relevance.sql +8 -0
  101. package/src/be/migrations/175_workflow_runs_list_index.sql +11 -0
  102. package/src/be/migrations/177_events_event_agent_created_index.sql +11 -0
  103. package/src/be/models-catalog.ts +2 -0
  104. package/src/be/modelsdev-cache.json +1857 -0
  105. package/src/be/modelsdev-cache.ts +14 -1
  106. package/src/be/pricing-normalize.ts +2 -0
  107. package/src/be/scripts/typecheck.ts +1 -0
  108. package/src/be/seed-pricing.ts +3 -1
  109. package/src/be/seed-skills/bundled-files.generated.json +1 -1
  110. package/src/be/steering.ts +53 -3
  111. package/src/be/swarm-config-guard.ts +3 -0
  112. package/src/be/task-citations.ts +29 -0
  113. package/src/cli.tsx +14 -1
  114. package/src/commands/onboard/compose-generator.ts +8 -3
  115. package/src/commands/onboard/container-engine.ts +220 -0
  116. package/src/commands/onboard/manifest.ts +1 -0
  117. package/src/commands/onboard/steps/health-check.tsx +4 -1
  118. package/src/commands/onboard/steps/prereq-check.tsx +50 -54
  119. package/src/commands/onboard/steps/start.tsx +31 -16
  120. package/src/commands/onboard/types.ts +7 -2
  121. package/src/commands/onboard.tsx +39 -4
  122. package/src/commands/profile-sync.ts +79 -26
  123. package/src/commands/runner.ts +8 -1
  124. package/src/extensions/catalog.generated.json +17 -0
  125. package/src/extensions/contract-types.generated.ts +1 -1
  126. package/src/heartbeat/heartbeat.ts +263 -103
  127. package/src/hooks/hook.ts +21 -12
  128. package/src/http/agents.ts +35 -0
  129. package/src/http/approval-requests.ts +136 -64
  130. package/src/http/apps.ts +14 -3
  131. package/src/http/config.ts +15 -1
  132. package/src/http/events.ts +62 -2
  133. package/src/http/index.ts +6 -4
  134. package/src/http/memory.ts +21 -7
  135. package/src/http/schedules.ts +1 -1
  136. package/src/http/session-data.ts +9 -4
  137. package/src/http/sessions.ts +25 -18
  138. package/src/http/stats.ts +19 -7
  139. package/src/http/status.ts +11 -1
  140. package/src/http/tasks.ts +40 -8
  141. package/src/http/webhooks.ts +67 -46
  142. package/src/http/workflows.ts +15 -20
  143. package/src/linear/outbound.ts +37 -15
  144. package/src/otel-impl.ts +151 -1
  145. package/src/otel.ts +14 -3
  146. package/src/prompts/memories.ts +19 -2
  147. package/src/providers/claude-managed-models.ts +8 -0
  148. package/src/providers/modelsdev-reasoning.json +551 -0
  149. package/src/rbac/legacy-policy.ts +16 -0
  150. package/src/rbac/permissions.ts +4 -0
  151. package/src/scheduler/schedule-task.ts +3 -0
  152. package/src/scripts-runtime/sdk-allowlist.ts +1 -0
  153. package/src/scripts-runtime/swarm-sdk.ts +9 -0
  154. package/src/scripts-runtime/types/stdlib.d.ts +1 -0
  155. package/src/scripts-runtime/types/swarm-extension.d.ts +1 -0
  156. package/src/scripts-runtime/types/swarm-sdk.d.ts +1 -0
  157. package/src/server.ts +12 -7
  158. package/src/slack/app.ts +35 -4
  159. package/src/slack/connection-state.ts +17 -0
  160. package/src/tasks/context-key.ts +1 -0
  161. package/src/tasks/worker-follow-up.ts +6 -2
  162. package/src/tests/agent-task-activity.test.ts +74 -0
  163. package/src/tests/agents-list-model-display.test.ts +6 -1
  164. package/src/tests/approval-auto-cancellation-migration.test.ts +104 -0
  165. package/src/tests/approval-requests.test.ts +395 -0
  166. package/src/tests/approval-sweeps-config.test.ts +47 -0
  167. package/src/tests/approval-sweeps.test.ts +508 -0
  168. package/src/tests/apps-kibo-blocks.test.ts +77 -0
  169. package/src/tests/apps-spike.test.ts +56 -1
  170. package/src/tests/apps-sync-engine.test.ts +20 -9
  171. package/src/tests/boot-seeds.test.ts +170 -0
  172. package/src/tests/cancel-approval-request-tool.test.ts +115 -0
  173. package/src/tests/claude-managed-adapter.test.ts +6 -0
  174. package/src/tests/config-key-filter.test.ts +80 -0
  175. package/src/tests/context-window.test.ts +1 -0
  176. package/src/tests/db-client-busy-retry.test.ts +96 -0
  177. package/src/tests/db-client.test.ts +60 -1
  178. package/src/tests/db-task-read-contract.test.ts +10 -0
  179. package/src/tests/db-task-write-contract.test.ts +9 -0
  180. package/src/tests/docs-index-footprint.test.js +3 -3
  181. package/src/tests/events-http.test.ts +103 -0
  182. package/src/tests/extensions-catalog.test.ts +22 -17
  183. package/src/tests/extensions-pre-task.test.ts +107 -0
  184. package/src/tests/fixtures/kibo-blocks-definition.json.txt +105 -0
  185. package/src/tests/follow-up-config-inheritance.test.ts +260 -0
  186. package/src/tests/heartbeat-orphan-resume.test.ts +84 -0
  187. package/src/tests/heartbeat-reboot-live.test.ts +95 -0
  188. package/src/tests/heartbeat-stall-cas.test.ts +80 -0
  189. package/src/tests/heartbeat.test.ts +99 -3
  190. package/src/tests/hook-mcp-config.test.ts +29 -0
  191. package/src/tests/http-api-integration.test.ts +16 -2
  192. package/src/tests/kv-storage.test.ts +62 -0
  193. package/src/tests/linear-outbound-sync.test.ts +120 -5
  194. package/src/tests/list-endpoint-slimming.test.ts +91 -0
  195. package/src/tests/memory-graph-organic-route.test.ts +155 -0
  196. package/src/tests/memory-http-recall-gating.test.ts +14 -4
  197. package/src/tests/memory-hybrid.test.ts +161 -1
  198. package/src/tests/memory-prompt-recall-crowdout.test.ts +297 -0
  199. package/src/tests/memory.test.ts +22 -0
  200. package/src/tests/onboard-container-engine.test.ts +240 -0
  201. package/src/tests/otel-event-loop-and-logs.test.ts +99 -0
  202. package/src/tests/pricing-routes.test.ts +14 -0
  203. package/src/tests/profile-sync-warning.test.ts +8 -4
  204. package/src/tests/promote-draft-task-route.test.ts +19 -0
  205. package/src/tests/provider-registration.test.ts +296 -42
  206. package/src/tests/rbac-engine.test.ts +30 -0
  207. package/src/tests/reasoning-effort.test.ts +8 -0
  208. package/src/tests/script-connections.test.ts +7 -0
  209. package/src/tests/sdk-allowlist.test.ts +39 -0
  210. package/src/tests/session-costs-model-key-normalize.test.ts +1 -0
  211. package/src/tests/sessions-detail-batch.test.ts +250 -0
  212. package/src/tests/sessions.test.ts +118 -0
  213. package/src/tests/slack-config.test.ts +22 -0
  214. package/src/tests/sonnet-5-5-registry.test.ts +44 -0
  215. package/src/tests/status.test.ts +36 -1
  216. package/src/tests/swarm-diff.test.ts +316 -10
  217. package/src/tests/task-completion-idempotency.test.ts +2 -1
  218. package/src/tests/tasks-requested-by-filter.test.ts +3 -1
  219. package/src/tests/tool-annotations.test.ts +1 -1
  220. package/src/tests/ui-list-slim-projections.test.ts +158 -0
  221. package/src/tests/webhooks-task-blocked.test.ts +127 -0
  222. package/src/tests/workflow-approval-recovery.test.ts +203 -0
  223. package/src/tests/workflow-hitl-routing.test.ts +22 -1
  224. package/src/tests/workflow-hitl-slack-card.test.ts +52 -0
  225. package/src/tests/workflow-http-v2.test.ts +99 -8
  226. package/src/tests/workflow-retry-validation.test.ts +56 -0
  227. package/src/tests/workflow-tla-races.test.ts +389 -0
  228. package/src/tests/workflow-validation-port-routing.test.ts +113 -1
  229. package/src/tools/cancel-approval-request.ts +57 -0
  230. package/src/tools/memory-search.ts +1 -0
  231. package/src/tools/request-human-input.ts +4 -2
  232. package/src/tools/send-task.ts +4 -0
  233. package/src/tools/templates.ts +19 -0
  234. package/src/tools/tool-config.ts +2 -1
  235. package/src/tools/workflows/list-workflow-runs.ts +1 -0
  236. package/src/types.ts +38 -0
  237. package/src/utils/context-window.ts +1 -0
  238. package/src/workflows/approval-cancel.ts +152 -0
  239. package/src/workflows/approval-notifications.ts +84 -0
  240. package/src/workflows/checkpoint.ts +10 -5
  241. package/src/workflows/definition.ts +21 -0
  242. package/src/workflows/engine.ts +152 -21
  243. package/src/workflows/executors/human-in-the-loop.ts +1 -1
  244. package/src/workflows/recovery.ts +100 -45
  245. package/src/workflows/resume.ts +49 -18
  246. package/src/workflows/retry-poller.ts +71 -21
  247. package/templates/extensions/README.md +1 -0
  248. package/templates/extensions/github-sender-allowlist/README.md +18 -0
  249. package/templates/extensions/github-sender-allowlist/hooks.ts +55 -0
  250. package/templates/extensions/github-sender-allowlist/manifest.json +8 -0
  251. package/templates/skills/apps/content.md +20 -1
  252. package/templates/skills/code-quality/content.md +26 -1
  253. package/templates/skills/pages/content.md +16 -4
  254. package/templates/skills/pages/files/examples/multiplayer-board.html +3 -12
  255. package/dist/cli-2daksqps.js +0 -378
  256. package/dist/rbac-roles-3gxmnb20.js +0 -37
@@ -1,4 +1,4 @@
1
1
  // Generated by scripts/bundle-extension-types.ts. Do not edit.
2
2
  // biome-ignore-all lint/suspicious/noTemplateCurlyInString: Type declarations contain literal template placeholders.
3
3
  export const EXTENSION_TYPE_DEFINITIONS =
4
- '// biome-ignore-all lint/suspicious/noConfusingVoidType: Extension handlers may explicitly return no result.\ndeclare module "swarm-extension" {\n export type CreateTaskOptions = {\n key?: string | undefined;\n agentId?: string | null | undefined;\n creatorAgentId?: string | undefined;\n source?:\n | "workflow"\n | "schedule"\n | "slack"\n | "agentmail"\n | "mcp"\n | "api"\n | "ui"\n | "github"\n | "gitlab"\n | "system"\n | "linear"\n | "jira"\n | undefined;\n routingReason?:\n | "skill"\n | "continuity"\n | "overflow"\n | "human_pinned"\n | "reroute_fault"\n | undefined;\n routingSource?: "declared" | "engine_default" | undefined;\n routingNote?: string | undefined;\n taskType?: string | undefined;\n tags?: string[] | undefined;\n priority?: number | undefined;\n dependsOn?: string[] | undefined;\n offeredTo?: string | undefined;\n status?: "draft" | "backlog" | "unassigned" | undefined;\n slackChannelId?: string | undefined;\n slackThreadTs?: string | undefined;\n slackTriggerMessageTs?: string | undefined;\n slackUserId?: string | undefined;\n overrideSlackContext?: boolean | undefined;\n vcsProvider?: "github" | "gitlab" | undefined;\n vcsRepo?: string | undefined;\n vcsEventType?: string | undefined;\n vcsNumber?: number | undefined;\n vcsCommentId?: number | undefined;\n vcsAuthor?: string | undefined;\n vcsUrl?: string | undefined;\n vcsInstallationId?: number | undefined;\n vcsNodeId?: string | undefined;\n agentmailInboxId?: string | undefined;\n agentmailMessageId?: string | undefined;\n agentmailThreadId?: string | undefined;\n mentionMessageId?: string | undefined;\n mentionChannelId?: string | undefined;\n dir?: string | undefined;\n parentTaskId?: string | undefined;\n model?: string | undefined;\n modelTier?: "smol" | "regular" | "smart" | "ultra" | undefined;\n effort?: "off" | "low" | "medium" | "high" | "xhigh" | "max" | undefined;\n scheduleId?: string | undefined;\n workflowRunId?: string | undefined;\n workflowRunStepId?: string | undefined;\n sourceTaskId?: string | undefined;\n outputSchema?: Record<string, unknown> | undefined;\n inheritParentOutputSchema?: boolean | undefined;\n inheritParentRoutingAffinity?: boolean | undefined;\n followUpConfig?:\n | {\n disabled?: boolean | undefined;\n onCompleted?: string | undefined;\n onFailed?: string | undefined;\n }\n | undefined;\n requestedByUserId?: string | undefined;\n contextKey?: string | undefined;\n bypassTrackerContextDedup?: boolean | undefined;\n routingAffinity?:\n | {\n capabilities: string[];\n sourceAgentId?: string | undefined;\n role?: string | undefined;\n harnessProvider?:\n | "claude"\n | "codex"\n | "pi"\n | "devin"\n | "claude-managed"\n | "opencode"\n | "acp"\n | "dsh"\n | undefined;\n leadOnly?: boolean | undefined;\n }\n | undefined;\n };\n\n export type AgentTask = {\n id: string;\n key: string;\n agentId: string | null;\n task: string;\n status:\n | "draft"\n | "backlog"\n | "unassigned"\n | "offered"\n | "reviewing"\n | "pending"\n | "in_progress"\n | "paused"\n | "completed"\n | "failed"\n | "cancelled"\n | "superseded";\n source:\n | "workflow"\n | "schedule"\n | "slack"\n | "agentmail"\n | "mcp"\n | "api"\n | "ui"\n | "github"\n | "gitlab"\n | "system"\n | "linear"\n | "jira";\n tags: string[];\n priority: number;\n dependsOn: string[];\n createdAt: string;\n lastUpdatedAt: string;\n slackReplySent: boolean;\n wasPaused: boolean;\n creatorAgentId?: string | undefined;\n title?: string | undefined;\n routingReason?:\n | "skill"\n | "continuity"\n | "overflow"\n | "human_pinned"\n | "reroute_fault"\n | undefined;\n routingSource?: "declared" | "engine_default" | undefined;\n routingNote?: string | undefined;\n taskType?: string | undefined;\n offeredTo?: string | undefined;\n offeredAt?: string | undefined;\n acceptedAt?: string | undefined;\n rejectionReason?: string | undefined;\n finishedAt?: string | undefined;\n notifiedAt?: string | undefined;\n failureReason?: string | undefined;\n output?: string | undefined;\n progress?: string | undefined;\n slackChannelId?: string | undefined;\n slackThreadTs?: string | undefined;\n slackTriggerMessageTs?: string | undefined;\n slackUserId?: string | undefined;\n slackProgressMessageTs?: string | undefined;\n slackTreeRootMessageTs?: string | undefined;\n vcsProvider?: "github" | "gitlab" | undefined;\n vcsRepo?: string | undefined;\n vcsEventType?: string | undefined;\n vcsNumber?: number | undefined;\n vcsCommentId?: number | undefined;\n vcsAuthor?: string | undefined;\n vcsUrl?: string | undefined;\n vcsInstallationId?: number | undefined;\n vcsNodeId?: string | undefined;\n agentmailInboxId?: string | undefined;\n agentmailMessageId?: string | undefined;\n agentmailThreadId?: string | undefined;\n mentionMessageId?: string | undefined;\n mentionChannelId?: string | undefined;\n dir?: string | undefined;\n parentTaskId?: string | undefined;\n claudeSessionId?: string | undefined;\n model?: string | undefined;\n modelTier?: "smol" | "regular" | "smart" | "ultra" | undefined;\n effort?: "off" | "low" | "medium" | "high" | "xhigh" | "max" | undefined;\n scheduleId?: string | undefined;\n workflowRunId?: string | null | undefined;\n workflowRunStepId?: string | null | undefined;\n contextKey?: string | undefined;\n outputSchema?: Record<string, unknown> | undefined;\n followUpConfig?:\n | {\n disabled?: boolean | undefined;\n onCompleted?: string | undefined;\n onFailed?: string | undefined;\n }\n | undefined;\n deferredAt?: string | undefined;\n compactionCount?: number | undefined;\n peakContextPercent?: number | undefined;\n peakContextTokens?: number | undefined;\n contextWindowSize?: number | undefined;\n credentialKeySuffix?: string | undefined;\n credentialKeyType?: string | undefined;\n requestedByUserId?: string | undefined;\n swarmVersion?: string | undefined;\n provider?:\n | "claude"\n | "codex"\n | "pi"\n | "devin"\n | "claude-managed"\n | "opencode"\n | "acp"\n | "dsh"\n | undefined;\n providerMeta?: Record<string, unknown> | undefined;\n harnessVariant?: string | undefined;\n harnessVariantMeta?: Record<string, unknown> | undefined;\n totalCostUsd?: number | undefined;\n routingAffinity?:\n | {\n capabilities: string[];\n sourceAgentId?: string | undefined;\n role?: string | undefined;\n harnessProvider?:\n | "claude"\n | "codex"\n | "pi"\n | "devin"\n | "claude-managed"\n | "opencode"\n | "acp"\n | "dsh"\n | undefined;\n leadOnly?: boolean | undefined;\n }\n | undefined;\n routingAffinityInvalid?: boolean | undefined;\n };\n\n export type ActiveSession = {\n id: string;\n agentId: string;\n taskId: string | null;\n triggerType: string;\n inboxMessageId: string | null;\n taskDescription: string | null;\n runnerSessionId: string | null;\n providerSessionId: string | null;\n startedAt: string;\n lastHeartbeatAt: string;\n runtimeInstanceId?: string | null | undefined;\n };\n\n export type RequestInfo = {\n sessionId: string | undefined;\n agentId: string | undefined;\n runtimeInstanceId: string | undefined;\n sourceTaskId: string | undefined;\n contextKey: string | undefined;\n callOrigin: "mcp" | "extension" | "script-sdk";\n };\n\n import type { SwarmSdk } from "swarm-sdk";\n import type { z } from "zod";\n export type Runtime = "api" | "worker";\n export interface ExtensionScriptAsset {\n name: string;\n file: string;\n description: string;\n intent?: string;\n }\n export interface ExtensionScheduleAsset {\n name: string;\n description?: string;\n script: string;\n cronExpression?: string;\n intervalMs?: number;\n timezone?: string;\n args?: Record<string, unknown>;\n }\n export interface ExtensionWorkflowAsset {\n file: string;\n }\n export interface ExtensionSkillAsset {\n dir: string;\n }\n export interface ExtensionManifest {\n $schema?: string;\n name: string;\n description: string;\n version: string;\n runtime: Runtime;\n assets: {\n hooks: string;\n scripts?: readonly ExtensionScriptAsset[];\n schedules?: readonly ExtensionScheduleAsset[];\n workflows?: readonly ExtensionWorkflowAsset[];\n skills?: readonly ExtensionSkillAsset[];\n };\n homepage?: string;\n author?: string;\n }\n export interface HooksModule {\n default: SwarmExtension;\n config?: z.ZodTypeAny;\n }\n export interface ExtensionState {\n get<T = unknown>(key: string): Promise<T | null>;\n set<T>(key: string, value: T): Promise<void>;\n incr(key: string, by?: number): Promise<number>;\n del(key: string): Promise<void>;\n }\n export interface ExtensionLogger {\n debug(message: string, data?: unknown): void;\n info(message: string, data?: unknown): void;\n warn(message: string, data?: unknown): void;\n error(message: string, data?: unknown): void;\n }\n type ConfigFor<M extends ExtensionManifest> = M extends {\n config: infer C extends z.ZodTypeAny;\n }\n ? z.infer<C>\n : Record<string, unknown>;\n export interface ApiCtx<Config = Record<string, unknown>> {\n swarm: SwarmSdk;\n state: ExtensionState;\n config: Config;\n log: ExtensionLogger;\n signal: AbortSignal;\n event: {\n name: keyof SwarmEventMap;\n at: string;\n extension: {\n id: string;\n version: number;\n };\n };\n }\n export interface WorkerCtx<Config = Record<string, unknown>>\n extends Omit<ApiCtx<Config>, "swarm"> {\n swarm: SwarmSdk;\n worker: {\n agentId: string;\n taskId: string;\n harness: string;\n };\n }\n export type CtxFor<M extends ExtensionManifest> = M["runtime"] extends "api"\n ? ApiCtx<ConfigFor<M>>\n : WorkerCtx<ConfigFor<M>>;\n export type PreResult<Modify> =\n | void\n | {\n action: "continue";\n }\n | {\n action: "modify";\n data: Modify;\n }\n | {\n action: "block";\n reason: string;\n };\n export type TaskCreateOrigin =\n | "rest"\n | "app"\n | "mcp"\n | "slack"\n | "schedule"\n | "workflow"\n | "webhook"\n | "followUp"\n | `extension:${string}`;\n export interface TaskCreateEvent {\n options: CreateTaskOptions;\n description: string;\n origin: TaskCreateOrigin;\n requestInfo?: RequestInfo;\n }\n export type TaskCreateModify = Partial<\n Pick<\n CreateTaskOptions,\n | "agentId"\n | "creatorAgentId"\n | "source"\n | "taskType"\n | "tags"\n | "priority"\n | "dependsOn"\n | "offeredTo"\n | "vcsProvider"\n | "vcsRepo"\n | "vcsEventType"\n | "vcsNumber"\n | "vcsCommentId"\n | "vcsAuthor"\n | "vcsUrl"\n | "vcsInstallationId"\n | "vcsNodeId"\n | "agentmailInboxId"\n | "agentmailMessageId"\n | "agentmailThreadId"\n | "mentionMessageId"\n | "mentionChannelId"\n | "dir"\n | "model"\n | "modelTier"\n | "effort"\n | "outputSchema"\n | "followUpConfig"\n | "bypassTrackerContextDedup"\n >\n > & {\n description?: string;\n };\n export interface TaskFollowUpEvent {\n completedTask: AgentTask;\n status: "completed" | "failed";\n output?: string;\n failureReason?: string;\n workerAgentId: string;\n leadAgentId: string;\n summary: string;\n }\n export type TaskFollowUpModify = Partial<\n Pick<CreateTaskOptions, "agentId" | "priority" | "followUpConfig">\n > & {\n description?: string;\n };\n export interface SlackRouteEvent {\n channelId: string;\n userId: string;\n text: string;\n threadTs?: string;\n botMentioned: boolean;\n threadContext?: {\n channelId: string;\n threadTs: string;\n };\n }\n export interface SlackRouteModify {\n target:\n | {\n kind: "agent";\n agentId: string;\n }\n | {\n kind: "lead";\n }\n | {\n kind: "broadcast";\n };\n }\n export type HeartbeatClassification = "no-session" | "stale-session" | "fresh-stalled";\n export type HeartbeatAction = "supersede-resume" | "fail" | "record";\n export interface HeartbeatRemediateEvent {\n task: AgentTask;\n session?: ActiveSession;\n classification: HeartbeatClassification;\n proposedAction: HeartbeatAction;\n reason: string;\n taskAgeMs: number;\n sessionHeartbeatAgeMs?: number;\n }\n export interface HeartbeatRemediateModify {\n proposedAction: HeartbeatAction;\n }\n export interface ToolCallEvent {\n tool: string;\n args: unknown;\n requestInfo: RequestInfo;\n }\n export interface ToolCallModify {\n args: unknown;\n }\n export interface TaskEvent {\n task: AgentTask;\n }\n export interface TaskCompletedEvent extends TaskEvent {\n output: string;\n }\n export interface TaskFailedEvent extends TaskEvent {\n failureReason: string;\n }\n export interface TaskSupersededEvent extends TaskEvent {\n supersededBy: string;\n }\n export interface TaskProgressEvent extends TaskEvent {\n progress: string;\n }\n export interface SlackMessageEvent {\n channelId: string;\n userId: string;\n text: string;\n threadTs?: string;\n taskId?: string;\n }\n export interface ToolCallCompletedEvent extends ToolCallEvent {\n result: unknown;\n durationMs: number;\n }\n export interface SwarmEventMap {\n "pre.task.create": {\n event: TaskCreateEvent;\n modify: TaskCreateModify;\n result: PreResult<TaskCreateModify>;\n };\n "pre.task.followUp": {\n event: TaskFollowUpEvent;\n modify: TaskFollowUpModify;\n result: PreResult<TaskFollowUpModify>;\n };\n "pre.slack.route": {\n event: SlackRouteEvent;\n modify: SlackRouteModify;\n result: PreResult<SlackRouteModify>;\n };\n "pre.heartbeat.remediate": {\n event: HeartbeatRemediateEvent;\n modify: HeartbeatRemediateModify;\n result: PreResult<HeartbeatRemediateModify>;\n };\n "pre.tool.call": {\n event: ToolCallEvent;\n modify: ToolCallModify;\n result: PreResult<ToolCallModify>;\n };\n "post.task.created": {\n event: TaskEvent;\n modify: never;\n result: void;\n };\n "post.task.completed": {\n event: TaskCompletedEvent;\n modify: never;\n result: void;\n };\n "post.task.failed": {\n event: TaskFailedEvent;\n modify: never;\n result: void;\n };\n "post.task.cancelled": {\n event: TaskEvent;\n modify: never;\n result: void;\n };\n "post.task.superseded": {\n event: TaskSupersededEvent;\n modify: never;\n result: void;\n };\n "post.task.progress": {\n event: TaskProgressEvent;\n modify: never;\n result: void;\n };\n "post.slack.message": {\n event: SlackMessageEvent;\n modify: never;\n result: void;\n };\n "post.tool.call": {\n event: ToolCallCompletedEvent;\n modify: never;\n result: void;\n };\n }\n export interface ExtensionApi<\n M extends ExtensionManifest = ExtensionManifest & {\n runtime: "api";\n },\n > {\n on<E extends keyof SwarmEventMap>(\n event: E,\n handler: (\n event: SwarmEventMap[E]["event"],\n ctx: CtxFor<M>,\n ) => Promise<SwarmEventMap[E]["result"]> | SwarmEventMap[E]["result"],\n opts?: {\n priority?: number;\n },\n ): void;\n }\n export type SwarmExtension<\n M extends ExtensionManifest = ExtensionManifest & {\n runtime: "api";\n },\n > = (api: ExtensionApi<M>) => void;\n export { block, modify } from "./contract-runtime";\n}\n';
4
+ '// biome-ignore-all lint/suspicious/noConfusingVoidType: Extension handlers may explicitly return no result.\ndeclare module "swarm-extension" {\n export type CreateTaskOptions = {\n key?: string | undefined;\n agentId?: string | null | undefined;\n creatorAgentId?: string | undefined;\n source?:\n | "workflow"\n | "schedule"\n | "slack"\n | "agentmail"\n | "mcp"\n | "api"\n | "ui"\n | "github"\n | "gitlab"\n | "system"\n | "linear"\n | "jira"\n | undefined;\n routingReason?:\n | "skill"\n | "continuity"\n | "overflow"\n | "human_pinned"\n | "reroute_fault"\n | undefined;\n routingSource?: "declared" | "engine_default" | undefined;\n routingNote?: string | undefined;\n taskType?: string | undefined;\n tags?: string[] | undefined;\n priority?: number | undefined;\n dependsOn?: string[] | undefined;\n offeredTo?: string | undefined;\n status?: "draft" | "backlog" | "unassigned" | undefined;\n slackChannelId?: string | undefined;\n slackThreadTs?: string | undefined;\n slackTriggerMessageTs?: string | undefined;\n slackUserId?: string | undefined;\n overrideSlackContext?: boolean | undefined;\n vcsProvider?: "github" | "gitlab" | undefined;\n vcsRepo?: string | undefined;\n vcsEventType?: string | undefined;\n vcsNumber?: number | undefined;\n vcsCommentId?: number | undefined;\n vcsAuthor?: string | undefined;\n vcsUrl?: string | undefined;\n vcsInstallationId?: number | undefined;\n vcsNodeId?: string | undefined;\n agentmailInboxId?: string | undefined;\n agentmailMessageId?: string | undefined;\n agentmailThreadId?: string | undefined;\n mentionMessageId?: string | undefined;\n mentionChannelId?: string | undefined;\n dir?: string | undefined;\n parentTaskId?: string | undefined;\n model?: string | undefined;\n modelTier?: "smol" | "regular" | "smart" | "ultra" | undefined;\n effort?: "off" | "low" | "medium" | "high" | "xhigh" | "max" | undefined;\n scheduleId?: string | undefined;\n workflowRunId?: string | undefined;\n workflowRunStepId?: string | undefined;\n sourceTaskId?: string | undefined;\n outputSchema?: Record<string, unknown> | undefined;\n inheritParentOutputSchema?: boolean | undefined;\n inheritParentRoutingAffinity?: boolean | undefined;\n inheritParentFollowUpConfig?: boolean | undefined;\n followUpConfig?:\n | {\n disabled?: boolean | undefined;\n onCompleted?: string | undefined;\n onFailed?: string | undefined;\n }\n | undefined;\n requestedByUserId?: string | undefined;\n contextKey?: string | undefined;\n bypassTrackerContextDedup?: boolean | undefined;\n routingAffinity?:\n | {\n capabilities: string[];\n sourceAgentId?: string | undefined;\n role?: string | undefined;\n harnessProvider?:\n | "claude"\n | "codex"\n | "pi"\n | "devin"\n | "claude-managed"\n | "opencode"\n | "acp"\n | "dsh"\n | undefined;\n leadOnly?: boolean | undefined;\n }\n | undefined;\n };\n\n export type AgentTask = {\n id: string;\n key: string;\n agentId: string | null;\n task: string;\n status:\n | "draft"\n | "backlog"\n | "unassigned"\n | "offered"\n | "reviewing"\n | "pending"\n | "in_progress"\n | "paused"\n | "completed"\n | "failed"\n | "cancelled"\n | "superseded";\n source:\n | "workflow"\n | "schedule"\n | "slack"\n | "agentmail"\n | "mcp"\n | "api"\n | "ui"\n | "github"\n | "gitlab"\n | "system"\n | "linear"\n | "jira";\n tags: string[];\n priority: number;\n dependsOn: string[];\n createdAt: string;\n lastUpdatedAt: string;\n slackReplySent: boolean;\n wasPaused: boolean;\n creatorAgentId?: string | undefined;\n title?: string | undefined;\n routingReason?:\n | "skill"\n | "continuity"\n | "overflow"\n | "human_pinned"\n | "reroute_fault"\n | undefined;\n routingSource?: "declared" | "engine_default" | undefined;\n routingNote?: string | undefined;\n taskType?: string | undefined;\n offeredTo?: string | undefined;\n offeredAt?: string | undefined;\n acceptedAt?: string | undefined;\n rejectionReason?: string | undefined;\n finishedAt?: string | undefined;\n notifiedAt?: string | undefined;\n failureReason?: string | undefined;\n output?: string | undefined;\n progress?: string | undefined;\n slackChannelId?: string | undefined;\n slackThreadTs?: string | undefined;\n slackTriggerMessageTs?: string | undefined;\n slackUserId?: string | undefined;\n slackProgressMessageTs?: string | undefined;\n slackTreeRootMessageTs?: string | undefined;\n vcsProvider?: "github" | "gitlab" | undefined;\n vcsRepo?: string | undefined;\n vcsEventType?: string | undefined;\n vcsNumber?: number | undefined;\n vcsCommentId?: number | undefined;\n vcsAuthor?: string | undefined;\n vcsUrl?: string | undefined;\n vcsInstallationId?: number | undefined;\n vcsNodeId?: string | undefined;\n agentmailInboxId?: string | undefined;\n agentmailMessageId?: string | undefined;\n agentmailThreadId?: string | undefined;\n mentionMessageId?: string | undefined;\n mentionChannelId?: string | undefined;\n dir?: string | undefined;\n parentTaskId?: string | undefined;\n claudeSessionId?: string | undefined;\n model?: string | undefined;\n modelTier?: "smol" | "regular" | "smart" | "ultra" | undefined;\n effort?: "off" | "low" | "medium" | "high" | "xhigh" | "max" | undefined;\n scheduleId?: string | undefined;\n workflowRunId?: string | null | undefined;\n workflowRunStepId?: string | null | undefined;\n contextKey?: string | undefined;\n outputSchema?: Record<string, unknown> | undefined;\n followUpConfig?:\n | {\n disabled?: boolean | undefined;\n onCompleted?: string | undefined;\n onFailed?: string | undefined;\n }\n | undefined;\n deferredAt?: string | undefined;\n compactionCount?: number | undefined;\n peakContextPercent?: number | undefined;\n peakContextTokens?: number | undefined;\n contextWindowSize?: number | undefined;\n credentialKeySuffix?: string | undefined;\n credentialKeyType?: string | undefined;\n requestedByUserId?: string | undefined;\n swarmVersion?: string | undefined;\n provider?:\n | "claude"\n | "codex"\n | "pi"\n | "devin"\n | "claude-managed"\n | "opencode"\n | "acp"\n | "dsh"\n | undefined;\n providerMeta?: Record<string, unknown> | undefined;\n harnessVariant?: string | undefined;\n harnessVariantMeta?: Record<string, unknown> | undefined;\n totalCostUsd?: number | undefined;\n routingAffinity?:\n | {\n capabilities: string[];\n sourceAgentId?: string | undefined;\n role?: string | undefined;\n harnessProvider?:\n | "claude"\n | "codex"\n | "pi"\n | "devin"\n | "claude-managed"\n | "opencode"\n | "acp"\n | "dsh"\n | undefined;\n leadOnly?: boolean | undefined;\n }\n | undefined;\n routingAffinityInvalid?: boolean | undefined;\n };\n\n export type ActiveSession = {\n id: string;\n agentId: string;\n taskId: string | null;\n triggerType: string;\n inboxMessageId: string | null;\n taskDescription: string | null;\n runnerSessionId: string | null;\n providerSessionId: string | null;\n startedAt: string;\n lastHeartbeatAt: string;\n runtimeInstanceId?: string | null | undefined;\n };\n\n export type RequestInfo = {\n sessionId: string | undefined;\n agentId: string | undefined;\n runtimeInstanceId: string | undefined;\n sourceTaskId: string | undefined;\n contextKey: string | undefined;\n callOrigin: "mcp" | "extension" | "script-sdk";\n };\n\n import type { SwarmSdk } from "swarm-sdk";\n import type { z } from "zod";\n export type Runtime = "api" | "worker";\n export interface ExtensionScriptAsset {\n name: string;\n file: string;\n description: string;\n intent?: string;\n }\n export interface ExtensionScheduleAsset {\n name: string;\n description?: string;\n script: string;\n cronExpression?: string;\n intervalMs?: number;\n timezone?: string;\n args?: Record<string, unknown>;\n }\n export interface ExtensionWorkflowAsset {\n file: string;\n }\n export interface ExtensionSkillAsset {\n dir: string;\n }\n export interface ExtensionManifest {\n $schema?: string;\n name: string;\n description: string;\n version: string;\n runtime: Runtime;\n assets: {\n hooks: string;\n scripts?: readonly ExtensionScriptAsset[];\n schedules?: readonly ExtensionScheduleAsset[];\n workflows?: readonly ExtensionWorkflowAsset[];\n skills?: readonly ExtensionSkillAsset[];\n };\n homepage?: string;\n author?: string;\n }\n export interface HooksModule {\n default: SwarmExtension;\n config?: z.ZodTypeAny;\n }\n export interface ExtensionState {\n get<T = unknown>(key: string): Promise<T | null>;\n set<T>(key: string, value: T): Promise<void>;\n incr(key: string, by?: number): Promise<number>;\n del(key: string): Promise<void>;\n }\n export interface ExtensionLogger {\n debug(message: string, data?: unknown): void;\n info(message: string, data?: unknown): void;\n warn(message: string, data?: unknown): void;\n error(message: string, data?: unknown): void;\n }\n type ConfigFor<M extends ExtensionManifest> = M extends {\n config: infer C extends z.ZodTypeAny;\n }\n ? z.infer<C>\n : Record<string, unknown>;\n export interface ApiCtx<Config = Record<string, unknown>> {\n swarm: SwarmSdk;\n state: ExtensionState;\n config: Config;\n log: ExtensionLogger;\n signal: AbortSignal;\n event: {\n name: keyof SwarmEventMap;\n at: string;\n extension: {\n id: string;\n version: number;\n };\n };\n }\n export interface WorkerCtx<Config = Record<string, unknown>>\n extends Omit<ApiCtx<Config>, "swarm"> {\n swarm: SwarmSdk;\n worker: {\n agentId: string;\n taskId: string;\n harness: string;\n };\n }\n export type CtxFor<M extends ExtensionManifest> = M["runtime"] extends "api"\n ? ApiCtx<ConfigFor<M>>\n : WorkerCtx<ConfigFor<M>>;\n export type PreResult<Modify> =\n | void\n | {\n action: "continue";\n }\n | {\n action: "modify";\n data: Modify;\n }\n | {\n action: "block";\n reason: string;\n };\n export type TaskCreateOrigin =\n | "rest"\n | "app"\n | "mcp"\n | "slack"\n | "schedule"\n | "workflow"\n | "webhook"\n | "followUp"\n | `extension:${string}`;\n export interface TaskCreateEvent {\n options: CreateTaskOptions;\n description: string;\n origin: TaskCreateOrigin;\n requestInfo?: RequestInfo;\n }\n export type TaskCreateModify = Partial<\n Pick<\n CreateTaskOptions,\n | "agentId"\n | "creatorAgentId"\n | "source"\n | "taskType"\n | "tags"\n | "priority"\n | "dependsOn"\n | "offeredTo"\n | "vcsProvider"\n | "vcsRepo"\n | "vcsEventType"\n | "vcsNumber"\n | "vcsCommentId"\n | "vcsAuthor"\n | "vcsUrl"\n | "vcsInstallationId"\n | "vcsNodeId"\n | "agentmailInboxId"\n | "agentmailMessageId"\n | "agentmailThreadId"\n | "mentionMessageId"\n | "mentionChannelId"\n | "dir"\n | "model"\n | "modelTier"\n | "effort"\n | "outputSchema"\n | "followUpConfig"\n | "bypassTrackerContextDedup"\n >\n > & {\n description?: string;\n };\n export interface TaskFollowUpEvent {\n completedTask: AgentTask;\n status: "completed" | "failed";\n output?: string;\n failureReason?: string;\n workerAgentId: string;\n leadAgentId: string;\n summary: string;\n }\n export type TaskFollowUpModify = Partial<\n Pick<CreateTaskOptions, "agentId" | "priority" | "followUpConfig">\n > & {\n description?: string;\n };\n export interface SlackRouteEvent {\n channelId: string;\n userId: string;\n text: string;\n threadTs?: string;\n botMentioned: boolean;\n threadContext?: {\n channelId: string;\n threadTs: string;\n };\n }\n export interface SlackRouteModify {\n target:\n | {\n kind: "agent";\n agentId: string;\n }\n | {\n kind: "lead";\n }\n | {\n kind: "broadcast";\n };\n }\n export type HeartbeatClassification = "no-session" | "stale-session" | "fresh-stalled";\n export type HeartbeatAction = "supersede-resume" | "fail" | "record";\n export interface HeartbeatRemediateEvent {\n task: AgentTask;\n session?: ActiveSession;\n classification: HeartbeatClassification;\n proposedAction: HeartbeatAction;\n reason: string;\n taskAgeMs: number;\n sessionHeartbeatAgeMs?: number;\n }\n export interface HeartbeatRemediateModify {\n proposedAction: HeartbeatAction;\n }\n export interface ToolCallEvent {\n tool: string;\n args: unknown;\n requestInfo: RequestInfo;\n }\n export interface ToolCallModify {\n args: unknown;\n }\n export interface TaskEvent {\n task: AgentTask;\n }\n export interface TaskCompletedEvent extends TaskEvent {\n output: string;\n }\n export interface TaskFailedEvent extends TaskEvent {\n failureReason: string;\n }\n export interface TaskSupersededEvent extends TaskEvent {\n supersededBy: string;\n }\n export interface TaskProgressEvent extends TaskEvent {\n progress: string;\n }\n export interface SlackMessageEvent {\n channelId: string;\n userId: string;\n text: string;\n threadTs?: string;\n taskId?: string;\n }\n export interface ToolCallCompletedEvent extends ToolCallEvent {\n result: unknown;\n durationMs: number;\n }\n export interface SwarmEventMap {\n "pre.task.create": {\n event: TaskCreateEvent;\n modify: TaskCreateModify;\n result: PreResult<TaskCreateModify>;\n };\n "pre.task.followUp": {\n event: TaskFollowUpEvent;\n modify: TaskFollowUpModify;\n result: PreResult<TaskFollowUpModify>;\n };\n "pre.slack.route": {\n event: SlackRouteEvent;\n modify: SlackRouteModify;\n result: PreResult<SlackRouteModify>;\n };\n "pre.heartbeat.remediate": {\n event: HeartbeatRemediateEvent;\n modify: HeartbeatRemediateModify;\n result: PreResult<HeartbeatRemediateModify>;\n };\n "pre.tool.call": {\n event: ToolCallEvent;\n modify: ToolCallModify;\n result: PreResult<ToolCallModify>;\n };\n "post.task.created": {\n event: TaskEvent;\n modify: never;\n result: void;\n };\n "post.task.completed": {\n event: TaskCompletedEvent;\n modify: never;\n result: void;\n };\n "post.task.failed": {\n event: TaskFailedEvent;\n modify: never;\n result: void;\n };\n "post.task.cancelled": {\n event: TaskEvent;\n modify: never;\n result: void;\n };\n "post.task.superseded": {\n event: TaskSupersededEvent;\n modify: never;\n result: void;\n };\n "post.task.progress": {\n event: TaskProgressEvent;\n modify: never;\n result: void;\n };\n "post.slack.message": {\n event: SlackMessageEvent;\n modify: never;\n result: void;\n };\n "post.tool.call": {\n event: ToolCallCompletedEvent;\n modify: never;\n result: void;\n };\n }\n export interface ExtensionApi<\n M extends ExtensionManifest = ExtensionManifest & {\n runtime: "api";\n },\n > {\n on<E extends keyof SwarmEventMap>(\n event: E,\n handler: (\n event: SwarmEventMap[E]["event"],\n ctx: CtxFor<M>,\n ) => Promise<SwarmEventMap[E]["result"]> | SwarmEventMap[E]["result"],\n opts?: {\n priority?: number;\n },\n ): void;\n }\n export type SwarmExtension<\n M extends ExtensionManifest = ExtensionManifest & {\n runtime: "api";\n },\n > = (api: ExtensionApi<M>) => void;\n export { block, modify } from "./contract-runtime";\n}\n';
@@ -1,7 +1,12 @@
1
+ import {
2
+ autoCancelStaleApprovalRequests,
3
+ timeoutExpiredApprovalRequests,
4
+ } from "../be/approval-sweeps";
1
5
  import {
2
6
  assignUnassignedTaskPending,
3
7
  backfillSupersedeTaskResumeTaskId,
4
8
  buildRoutingAffinityFromAgent,
9
+ cascadeFailDependents,
5
10
  cleanupStaleSessions,
6
11
  createLogEntry,
7
12
  createTaskExtended,
@@ -12,6 +17,7 @@ import {
12
17
  getActiveTaskCount,
13
18
  getAllAgents,
14
19
  getDbClient,
20
+ getDependentTasks,
15
21
  getIdleWorkersWithCapacity,
16
22
  getLeadAgent,
17
23
  getPendingSteeringForTask,
@@ -21,6 +27,7 @@ import {
21
27
  getStalePinnedResumes,
22
28
  getStaleUnassignedAffinityTasks,
23
29
  getStalledInProgressTasks,
30
+ getSupersededTasksWithoutResume,
24
31
  getTaskById,
25
32
  getTaskStats,
26
33
  getTasksByStatus,
@@ -257,6 +264,8 @@ export interface HeartbeatFindings {
257
264
  staleRuntimes: number;
258
265
  staleOfferedTasks: number;
259
266
  abandonedDraftTasks: number;
267
+ approvalAutoCancelled: number;
268
+ approvalTimedOut: number;
260
269
  };
261
270
  }
262
271
 
@@ -334,6 +343,8 @@ export async function codeLevelTriage(): Promise<HeartbeatFindings> {
334
343
  staleRuntimes: 0,
335
344
  staleOfferedTasks: 0,
336
345
  abandonedDraftTasks: 0,
346
+ approvalAutoCancelled: 0,
347
+ approvalTimedOut: 0,
337
348
  },
338
349
  };
339
350
 
@@ -352,6 +363,10 @@ export async function codeLevelTriage(): Promise<HeartbeatFindings> {
352
363
  // 1. Detect and remediate stalled tasks (tiered: auto-fail dead workers)
353
364
  await detectAndRemediateStalledTasks(findings);
354
365
 
366
+ // 1.5. Give a resume to any task superseded without one (crash between
367
+ // the supersede write and the resume write).
368
+ await repairSupersededWithoutResume(findings);
369
+
355
370
  // 2. Check and fix worker health
356
371
  await checkWorkerHealth(findings);
357
372
 
@@ -498,6 +513,40 @@ async function detectAndRemediateStalledTasks(findings: HeartbeatFindings): Prom
498
513
  }
499
514
  }
500
515
 
516
+ /** Repair window: skip in-flight supersedes, stop at a day of history. */
517
+ const ORPHAN_SUPERSEDE_MIN_AGE_MS = 60 * 1000;
518
+ const ORPHAN_SUPERSEDE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
519
+
520
+ /**
521
+ * Supersede and resume creation are separate writes. If the API process dies
522
+ * between them, the task stays `superseded` with no resume and nothing else
523
+ * picks it up (the stall classifier only reads `in_progress`). This creates
524
+ * the missing resume. Found by the TLA+ model (specs/tla/heartbeat/).
525
+ */
526
+ async function repairSupersededWithoutResume(findings: HeartbeatFindings): Promise<void> {
527
+ const now = Date.now();
528
+ const orphans = await getSupersededTasksWithoutResume(
529
+ new Date(now - ORPHAN_SUPERSEDE_MIN_AGE_MS).toISOString(),
530
+ new Date(now - ORPHAN_SUPERSEDE_MAX_AGE_MS).toISOString(),
531
+ );
532
+ for (const task of orphans) {
533
+ if (!task.agentId) continue;
534
+ if (getNextResumeGeneration(task) > maxResumeGenerations()) continue;
535
+ const resume = await createResumeFollowUp({ parentId: task.id, reason: "crash_recovery" });
536
+ if (resume.kind !== "created") continue;
537
+ await backfillSupersedeTaskResumeTaskId(task.id, resume.task.id);
538
+ findings.autoResumedTasks.push({
539
+ taskId: task.id,
540
+ resumeTaskId: resume.task.id,
541
+ agentId: task.agentId,
542
+ reason: "repaired superseded task without resume",
543
+ });
544
+ console.log(
545
+ `[Heartbeat] Created missing resume ${resume.task.id.slice(0, 8)} for superseded task ${task.id.slice(0, 8)}`,
546
+ );
547
+ }
548
+ }
549
+
501
550
  function isRemediationAction(value: unknown): value is RemediationAction {
502
551
  return typeof value === "string" && REMEDIATION_ACTIONS.has(value as RemediationAction);
503
552
  }
@@ -556,7 +605,10 @@ async function remediateCrashedWorkerTask(
556
605
  if (!task.agentId) return; // Type guard — caller already checked.
557
606
 
558
607
  if (decision.action === "fail") {
559
- const failed = await failTask(task.id, decision.reason);
608
+ // CAS on the lastUpdatedAt this sweep read: progress since then wins.
609
+ const failed = await failTask(task.id, decision.reason, {
610
+ expectedLastUpdatedAt: task.lastUpdatedAt,
611
+ });
560
612
  if (failed) {
561
613
  findings.autoFailedTasks.push({
562
614
  taskId: task.id,
@@ -578,6 +630,8 @@ async function remediateCrashedWorkerTask(
578
630
  const superseded = await supersedeTask(task.id, {
579
631
  reason: decision.reason,
580
632
  resumeTaskId: null,
633
+ // CAS on the lastUpdatedAt this sweep read: progress since then wins.
634
+ expectedLastUpdatedAt: task.lastUpdatedAt,
581
635
  });
582
636
  if (!superseded) {
583
637
  return;
@@ -737,128 +791,145 @@ export async function runRebootSweep(): Promise<void> {
737
791
  // Heartbeated after (or within skew of) this boot → genuinely live, skip
738
792
  continue;
739
793
  }
740
- // Pre-boot stale session → fall through to auto-fail + reboot-retry child
794
+ // Workers run in their own containers and outlive an API restart, and
795
+ // sessions heartbeat on tool calls only. A pre-boot heartbeat is
796
+ // therefore not evidence of a dead worker: a live one inside a long
797
+ // model call has none in the first seconds after boot. Only a session
798
+ // stale by the classifier's own threshold counts as dead; anything
799
+ // fresher is left to the regular stalled-task sweep.
800
+ if (Date.now() - sessionLastSeen < stallThresholdStaleHeartbeatMin() * 60 * 1000) {
801
+ continue;
802
+ }
803
+ // Stale session → fall through to auto-fail + reboot-retry child
741
804
  }
742
805
 
743
806
  // Clean up pre-boot stale session before failing (if it existed)
744
807
  if (session) await deleteActiveSession(task.id);
745
808
 
746
- // Auto-fail the task
747
- const failed = await failTask(task.id, reason);
809
+ // Auto-fail the task. Its dependents are settled in the `finally` below
810
+ // (re-pointed to the retry child, or cascade-failed when there is none)
811
+ // instead of by failTask's unconditional cascade.
812
+ const failed = await failTask(task.id, reason, { cascadeDependents: false });
748
813
  if (!failed) continue;
749
814
 
750
- // Fix agent status
751
- if ((await getActiveTaskCount(task.agentId)) === 0) {
752
- await restoreAgentIdleAfterRemediation(task.agentId);
753
- }
754
-
755
- // Don't retry system-generated heartbeat tasks
756
- if (SKIP_AUTO_RESUME_TYPES.has(task.taskType ?? "")) {
757
- rebootAffectedTasks.push({ original: failed, retryTaskId: null });
758
- continue;
759
- }
760
-
761
- // Auto-retry: create a replacement task with parentTaskId
762
815
  let retryTaskId: string | null = null;
816
+ try {
817
+ // Fix agent status
818
+ if ((await getActiveTaskCount(task.agentId)) === 0) {
819
+ await restoreAgentIdleAfterRemediation(task.agentId);
820
+ }
763
821
 
764
- // A corrupt persisted affinity is never converted into an untagged
765
- // retry by this independent recovery path.
766
- if (task.routingAffinityInvalid) {
767
- console.error(
768
- `[Heartbeat] Reboot retry suppressed for ${task.id.slice(0, 8)}: invalid routing affinity`,
769
- );
770
- rebootAffectedTasks.push({ original: failed, retryTaskId: null });
771
- continue;
772
- }
822
+ // Don't retry system-generated heartbeat tasks
823
+ if (SKIP_AUTO_RESUME_TYPES.has(task.taskType ?? "")) {
824
+ rebootAffectedTasks.push({ original: failed, retryTaskId: null });
825
+ continue;
826
+ }
773
827
 
774
- // Guard: only retry if parent doesn't already have a retry child
775
- const existingRetry = await getDbClient().get<{ id: string }>(
776
- `SELECT id FROM agent_tasks
828
+ // Auto-retry: create a replacement task with parentTaskId
829
+ // A corrupt persisted affinity is never converted into an untagged
830
+ // retry by this independent recovery path.
831
+ if (task.routingAffinityInvalid) {
832
+ console.error(
833
+ `[Heartbeat] Reboot retry suppressed for ${task.id.slice(0, 8)}: invalid routing affinity`,
834
+ );
835
+ rebootAffectedTasks.push({ original: failed, retryTaskId: null });
836
+ continue;
837
+ }
838
+
839
+ // Guard: only retry if parent doesn't already have a retry child
840
+ const existingRetry = await getDbClient().get<{ id: string }>(
841
+ `SELECT id FROM agent_tasks
777
842
  WHERE parentTaskId = ?
778
843
  AND status NOT IN ('completed', 'failed', 'cancelled')
779
844
  LIMIT 1`,
780
- [task.id],
781
- );
845
+ [task.id],
846
+ );
782
847
 
783
- if (!existingRetry) {
784
- try {
785
- // Routing affinity (Phase 3): pin the retry child to the original
786
- // agent when it still looks recoverable (row exists, not offline,
787
- // has capacity) — the same gate `createResumeFollowUp` uses for its
788
- // same-agent pin. This keeps the retry off the role-blind pool
789
- // whenever the original worker is merely restarting. Always stamp
790
- // a `routingAffinity` snapshot from the original agent — even on
791
- // the pool-fallback leg (agent gone/offline/at-capacity) — so that
792
- // leg is still role/capability-gated instead of role-blind.
793
- let preferredAgentId: string | undefined;
794
- let sourceCandidate = await getPinCandidateAgent(task.agentId);
795
- if (sourceCandidate) {
796
- const activeCount = await getActiveTaskCount(sourceCandidate.id);
797
- const maxTasks = sourceCandidate.maxTasks ?? 1;
798
- const hasCap = activeCount < maxTasks;
799
- if (hasCap) {
800
- preferredAgentId = sourceCandidate.id;
801
- } else {
802
- sourceCandidate = null;
803
- console.warn(
804
- `[Heartbeat] Reboot retry for task ${task.id.slice(0, 8)} NOT pinned: agent ${task.agentId.slice(0, 8)} at capacity (${activeCount}/${maxTasks}); falling back to affinity-gated pool`,
805
- );
848
+ if (!existingRetry) {
849
+ try {
850
+ // Routing affinity (Phase 3): pin the retry child to the original
851
+ // agent when it still looks recoverable (row exists, not offline,
852
+ // has capacity) — the same gate `createResumeFollowUp` uses for its
853
+ // same-agent pin. This keeps the retry off the role-blind pool
854
+ // whenever the original worker is merely restarting. Always stamp
855
+ // a `routingAffinity` snapshot from the original agent — even on
856
+ // the pool-fallback leg (agent gone/offline/at-capacity) — so that
857
+ // leg is still role/capability-gated instead of role-blind.
858
+ let preferredAgentId: string | undefined;
859
+ let sourceCandidate = await getPinCandidateAgent(task.agentId);
860
+ if (sourceCandidate) {
861
+ const activeCount = await getActiveTaskCount(sourceCandidate.id);
862
+ const maxTasks = sourceCandidate.maxTasks ?? 1;
863
+ const hasCap = activeCount < maxTasks;
864
+ if (hasCap) {
865
+ preferredAgentId = sourceCandidate.id;
866
+ } else {
867
+ sourceCandidate = null;
868
+ console.warn(
869
+ `[Heartbeat] Reboot retry for task ${task.id.slice(0, 8)} NOT pinned: agent ${task.agentId.slice(0, 8)} at capacity (${activeCount}/${maxTasks}); falling back to affinity-gated pool`,
870
+ );
871
+ }
806
872
  }
807
- }
808
873
 
809
- const leadOnly = task.routingAffinity?.leadOnly === true;
810
- const authorization = await resolveLeadOnlyRecoveryAssignment(task, sourceCandidate);
811
- if (leadOnly) preferredAgentId = authorization.agentId;
812
-
813
- const tags = ["reboot-retry", "auto-generated"];
814
- if (preferredAgentId !== undefined) tags.push(REBOOT_RETRY_PIN_TAG);
815
- const sourceAffinity = (await buildRoutingAffinityFromAgent(task.agentId)) ?? undefined;
816
- const routingAffinity = leadOnly
817
- ? {
818
- ...(sourceAffinity ?? task.routingAffinity),
819
- capabilities:
820
- task.routingAffinity?.capabilities ?? sourceAffinity?.capabilities ?? [],
821
- leadOnly: true,
822
- }
823
- : sourceAffinity;
824
-
825
- const retryTask = await createTaskExtended(task.task, {
826
- parentTaskId: task.id,
827
- agentId: preferredAgentId,
828
- routingReason:
829
- preferredAgentId === undefined
830
- ? undefined
831
- : preferredAgentId === task.agentId
832
- ? "continuity"
833
- : "reroute_fault",
834
- routingSource: preferredAgentId === undefined ? undefined : "engine_default",
835
- tags,
836
- priority: task.priority,
837
- source: task.source,
838
- taskType: task.taskType ?? undefined,
839
- routingAffinity,
840
- });
841
- if (authorization.decision) {
842
- await createLogEntry({
843
- eventType: "task_recovery_authorization",
844
- taskId: retryTask.id,
874
+ const leadOnly = task.routingAffinity?.leadOnly === true;
875
+ const authorization = await resolveLeadOnlyRecoveryAssignment(task, sourceCandidate);
876
+ if (leadOnly) preferredAgentId = authorization.agentId;
877
+
878
+ const tags = ["reboot-retry", "auto-generated"];
879
+ if (preferredAgentId !== undefined) tags.push(REBOOT_RETRY_PIN_TAG);
880
+ const sourceAffinity = (await buildRoutingAffinityFromAgent(task.agentId)) ?? undefined;
881
+ const routingAffinity = leadOnly
882
+ ? {
883
+ ...(sourceAffinity ?? task.routingAffinity),
884
+ capabilities:
885
+ task.routingAffinity?.capabilities ?? sourceAffinity?.capabilities ?? [],
886
+ leadOnly: true,
887
+ }
888
+ : sourceAffinity;
889
+
890
+ const retryTask = await createTaskExtended(task.task, {
891
+ parentTaskId: task.id,
845
892
  agentId: preferredAgentId,
846
- metadata: {
847
- leadOnly: true,
848
- parentTaskId: task.id,
849
- sourceAgentId: task.agentId,
850
- decision: authorization.decision,
851
- },
893
+ routingReason:
894
+ preferredAgentId === undefined
895
+ ? undefined
896
+ : preferredAgentId === task.agentId
897
+ ? "continuity"
898
+ : "reroute_fault",
899
+ routingSource: preferredAgentId === undefined ? undefined : "engine_default",
900
+ tags,
901
+ priority: task.priority,
902
+ source: task.source,
903
+ taskType: task.taskType ?? undefined,
904
+ // The retry re-runs the same work, so it keeps the creator's
905
+ // onCompleted/onFailed.
906
+ inheritParentFollowUpConfig: true,
907
+ routingAffinity,
852
908
  });
909
+ if (authorization.decision) {
910
+ await createLogEntry({
911
+ eventType: "task_recovery_authorization",
912
+ taskId: retryTask.id,
913
+ agentId: preferredAgentId,
914
+ metadata: {
915
+ leadOnly: true,
916
+ parentTaskId: task.id,
917
+ sourceAgentId: task.agentId,
918
+ decision: authorization.decision,
919
+ },
920
+ });
921
+ }
922
+ retryTaskId = retryTask.id;
923
+ console.log(`[Heartbeat] Reboot retry created: ${retryTaskId} (parent: ${task.id})`);
924
+ } catch (err) {
925
+ console.error(`[Heartbeat] Failed to create retry task for ${task.id}:`, err);
853
926
  }
854
- retryTaskId = retryTask.id;
855
- console.log(`[Heartbeat] Reboot retry created: ${retryTaskId} (parent: ${task.id})`);
856
- } catch (err) {
857
- console.error(`[Heartbeat] Failed to create retry task for ${task.id}:`, err);
858
927
  }
859
- }
860
928
 
861
- rebootAffectedTasks.push({ original: failed, retryTaskId });
929
+ rebootAffectedTasks.push({ original: failed, retryTaskId });
930
+ } finally {
931
+ await settleRebootSweptDependents(task.id, retryTaskId);
932
+ }
862
933
  }
863
934
 
864
935
  console.log(
@@ -869,6 +940,67 @@ export async function runRebootSweep(): Promise<void> {
869
940
  }
870
941
  }
871
942
 
943
+ /** Statuses a dependent can hold before it ever started running. */
944
+ const NEVER_STARTED_STATUSES = new Set([
945
+ "draft",
946
+ "backlog",
947
+ "unassigned",
948
+ "offered",
949
+ "reviewing",
950
+ "pending",
951
+ ]);
952
+
953
+ /**
954
+ * Settle the dependents of a reboot-swept task. With a retry child, every
955
+ * never-started dependent has the swept id in `dependsOn` replaced by the
956
+ * retry id, so it waits on the retry instead of dying with `Blocked dependency
957
+ * … was failed`. The row keeps its id, agent, Slack fields, follow-up config,
958
+ * priority and parent. Anything still depending on the swept task afterwards
959
+ * (no retry, or a dependent that already started) cascade-fails as before.
960
+ */
961
+ async function settleRebootSweptDependents(
962
+ sweptId: string,
963
+ retryTaskId: string | null,
964
+ ): Promise<void> {
965
+ try {
966
+ if (retryTaskId) {
967
+ for (const dep of await getDependentTasks(sweptId)) {
968
+ if (!NEVER_STARTED_STATUSES.has(dep.status)) continue;
969
+ const dependsOn = [
970
+ ...new Set(dep.dependsOn.map((id) => (id === sweptId ? retryTaskId : id))),
971
+ ];
972
+ // Status + membership predicates make the re-point a no-op if the
973
+ // dependent was claimed or re-pointed since it was read.
974
+ const row = await getDbClient().get<{ id: string }>(
975
+ `UPDATE agent_tasks
976
+ SET dependsOn = ?, lastUpdatedAt = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
977
+ WHERE id = ? AND status = ?
978
+ AND EXISTS (SELECT 1 FROM json_each(agent_tasks.dependsOn) WHERE value = ?)
979
+ RETURNING id`,
980
+ [JSON.stringify(dependsOn), dep.id, dep.status, sweptId],
981
+ );
982
+ if (!row) continue;
983
+ try {
984
+ await createLogEntry({
985
+ eventType: "task_dependency_repointed",
986
+ taskId: dep.id,
987
+ agentId: dep.agentId ?? undefined,
988
+ oldValue: sweptId,
989
+ newValue: retryTaskId,
990
+ metadata: { reason: "reboot_retry" },
991
+ });
992
+ } catch {}
993
+ console.log(
994
+ `[Heartbeat] Reboot sweep: dependent ${dep.id.slice(0, 8)} re-pointed from ${sweptId.slice(0, 8)} to retry ${retryTaskId.slice(0, 8)}`,
995
+ );
996
+ }
997
+ }
998
+ await cascadeFailDependents(sweptId, "failed");
999
+ } catch (err) {
1000
+ console.error(`[Heartbeat] Reboot sweep: settling dependents of ${sweptId} failed:`, err);
1001
+ }
1002
+ }
1003
+
872
1004
  /** Get tasks affected by the most recent reboot sweep */
873
1005
  export function getRebootAffectedTasks() {
874
1006
  return rebootAffectedTasks;
@@ -1187,6 +1319,24 @@ async function cleanupStaleResources(findings: HeartbeatFindings): Promise<void>
1187
1319
  // Workflow engine may not be initialized yet — skip recovery
1188
1320
  findings.staleCleanup.workflowRuns = 0;
1189
1321
  }
1322
+ // Approval sweeps run after the recovery pass, so a waiting run past its
1323
+ // expiresAt is routed on its timeout port before the timeout sweep reads it.
1324
+ try {
1325
+ findings.staleCleanup.approvalTimedOut = (
1326
+ await timeoutExpiredApprovalRequests()
1327
+ ).timedOut.length;
1328
+ } catch (err) {
1329
+ console.error("[heartbeat] approval timeout sweep failed:", err);
1330
+ findings.staleCleanup.approvalTimedOut = 0;
1331
+ }
1332
+ try {
1333
+ findings.staleCleanup.approvalAutoCancelled = (
1334
+ await autoCancelStaleApprovalRequests()
1335
+ ).cancelled.length;
1336
+ } catch (err) {
1337
+ console.error("[heartbeat] approval auto-cancel sweep failed:", err);
1338
+ findings.staleCleanup.approvalAutoCancelled = 0;
1339
+ }
1190
1340
  }
1191
1341
 
1192
1342
  // ============================================================================
@@ -1484,6 +1634,8 @@ export async function runHeartbeatSweep(): Promise<void> {
1484
1634
  staleRuntimes: 0,
1485
1635
  staleOfferedTasks: 0,
1486
1636
  abandonedDraftTasks: 0,
1637
+ approvalAutoCancelled: 0,
1638
+ approvalTimedOut: 0,
1487
1639
  },
1488
1640
  };
1489
1641
  // Expiry runs even on a cleanup-only tick: an idle agent whose runtime
@@ -1553,9 +1705,17 @@ function logFindings(findings: HeartbeatFindings): void {
1553
1705
  workflowRuns,
1554
1706
  staleOfferedTasks,
1555
1707
  abandonedDraftTasks,
1708
+ approvalAutoCancelled,
1709
+ approvalTimedOut,
1556
1710
  } = findings.staleCleanup;
1557
1711
  const totalCleanup =
1558
- sessions + reviewingTasks + mentionProcessing + inboxProcessing + workflowRuns;
1712
+ sessions +
1713
+ reviewingTasks +
1714
+ mentionProcessing +
1715
+ inboxProcessing +
1716
+ workflowRuns +
1717
+ approvalAutoCancelled +
1718
+ approvalTimedOut;
1559
1719
  if (totalCleanup > 0) {
1560
1720
  parts.push(`stale_cleanup=${totalCleanup}`);
1561
1721
  }
package/src/hooks/hook.ts CHANGED
@@ -42,6 +42,26 @@ type McpServerConfig = {
42
42
  };
43
43
  };
44
44
 
45
+ export async function loadHookMcpConfig(
46
+ projectDir: string,
47
+ workspaceDir = "/workspace",
48
+ ): Promise<McpServerConfig | undefined> {
49
+ for (const configDir of new Set([projectDir, workspaceDir])) {
50
+ try {
51
+ const mcpFile = Bun.file(`${configDir}/.mcp.json`);
52
+ if (!(await mcpFile.exists())) continue;
53
+
54
+ const config = await mcpFile.json();
55
+ const serverConfig = config?.mcpServers?.[SERVER_NAME] as McpServerConfig | undefined;
56
+ if (serverConfig) return serverConfig;
57
+ } catch {
58
+ // Try the next config location.
59
+ }
60
+ }
61
+
62
+ return undefined;
63
+ }
64
+
45
65
  interface HookMessage {
46
66
  hook_event_name: string;
47
67
  session_id?: string;
@@ -493,18 +513,7 @@ export async function runSessionSummaryFromStdin(): Promise<void> {
493
513
  */
494
514
  export async function handleHook(): Promise<void> {
495
515
  const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
496
-
497
- let mcpConfig: McpServerConfig | undefined;
498
-
499
- try {
500
- const mcpFile = Bun.file(`${projectDir}/.mcp.json`);
501
- if (await mcpFile.exists()) {
502
- const config = await mcpFile.json();
503
- mcpConfig = config?.mcpServers?.[SERVER_NAME] as McpServerConfig;
504
- }
505
- } catch {
506
- // No config found, proceed without MCP
507
- }
516
+ const mcpConfig = await loadHookMcpConfig(projectDir);
508
517
 
509
518
  let msg: HookMessage;
510
519
  try {