@desplega.ai/agent-swarm 1.158.0 → 1.159.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 (272) hide show
  1. package/dist/{acp-adapter-xaq4n6pc.js → acp-adapter-qhch0n2v.js} +1 -1
  2. package/dist/{actions-p9hz5yat.js → actions-dtsh1mfb.js} +33 -29
  3. package/dist/{anthropic-messages-51pz26qe.js → anthropic-messages-f2h06k64.js} +8 -3
  4. package/dist/{app-g26wdcz6.js → app-y1b9srva.js} +10 -9
  5. package/dist/{assistant-wydfqkb3.js → assistant-31hwy5xj.js} +34 -30
  6. package/dist/{azure-openai-responses-8gksqtmz.js → azure-openai-responses-0k1mfax5.js} +12 -13
  7. package/dist/{boot-reembed-se8zmdcp.js → boot-reembed-48d2qpkt.js} +11 -11
  8. package/dist/{boot-reembed-g5ckg9yb.js → boot-reembed-w0bryhkr.js} +12 -12
  9. package/dist/{boot-scrub-logs-wfxqan61.js → boot-scrub-logs-t65wbrn7.js} +9 -8
  10. package/dist/{boot-seeds-s3q01xj3.js → boot-seeds-h7c32hvm.js} +12 -11
  11. package/dist/{claude-adapter-tv2x4f36.js → claude-adapter-1t6611ne.js} +26 -21
  12. package/dist/{claude-managed-adapter-41b2s7fd.js → claude-managed-adapter-werp659j.js} +4 -2
  13. package/dist/{claude-managed-setup-60g8aazx.js → claude-managed-setup-y6rdgbc1.js} +11 -2
  14. package/dist/claude-sdk-session-zhkyk8yw.js +29319 -0
  15. package/dist/{cli-vj6h9kc2.js → cli-0m7wct0d.js} +59 -1
  16. package/dist/{cli-wscdvcqz.js → cli-0n2e4x49.js} +1 -1
  17. package/dist/cli-0xqde53b.js +176 -0
  18. package/dist/{cli-xberxa1r.js → cli-10ym38qc.js} +2 -2
  19. package/dist/{cli-5b6b62tz.js → cli-13hw4z91.js} +1 -1
  20. package/dist/{cli-rqz8qbw7.js → cli-20035trh.js} +50 -18
  21. package/dist/{cli-k0g1sjzm.js → cli-31nkw3g3.js} +3 -3
  22. package/dist/cli-3gvrr8vj.js +1422 -0
  23. package/dist/{cli-c07r9a2m.js → cli-48527fpw.js} +9 -8
  24. package/dist/{cli-24k806sc.js → cli-4aj25szq.js} +2 -2
  25. package/dist/{cli-7gtvvpbh.js → cli-4tvgz99m.js} +7 -7
  26. package/dist/cli-503ya656.js +455 -0
  27. package/dist/{cli-cw9zck8j.js → cli-5f2hq128.js} +8 -4
  28. package/dist/{cli-s8eet254.js → cli-68bmme5p.js} +11078 -27833
  29. package/dist/{cli-ftfvggzx.js → cli-79aq9cgb.js} +794 -563
  30. package/dist/cli-7cxw8b60.js +14910 -0
  31. package/dist/cli-7kyk9jr3.js +22 -0
  32. package/dist/cli-7zttzetj.js +324 -0
  33. package/dist/{cli-y95wp3tg.js → cli-96qd5psa.js} +1 -1
  34. package/dist/{cli-g8djp6qe.js → cli-9yg65c1j.js} +22 -0
  35. package/dist/{cli-3gmcwfyx.js → cli-ardpzhjm.js} +5 -5
  36. package/dist/{cli-7s3syk22.js → cli-b6thtzac.js} +5 -5
  37. package/dist/{cli-00w2c66p.js → cli-bgv70kym.js} +13 -2
  38. package/dist/{cli-j55n64b6.js → cli-bv0a38v9.js} +163 -44
  39. package/dist/cli-ca2w7zv7.js +115 -0
  40. package/dist/cli-cpvsaqsw.js +333 -0
  41. package/dist/cli-cygyqj3b.js +754 -0
  42. package/dist/{cli-x24pshtq.js → cli-dvjpkkjd.js} +6 -6
  43. package/dist/{cli-5d10vd2m.js → cli-e0ndxhtk.js} +4 -4
  44. package/dist/{cli-4198qc9v.js → cli-e4xnc0qp.js} +15 -3
  45. package/dist/{cli-rjehvrts.js → cli-eg7cdg68.js} +8 -8
  46. package/dist/cli-ejpygnqz.js +3815 -0
  47. package/dist/{cli-fgqxwrpv.js → cli-fjtp3463.js} +1631 -127
  48. package/dist/{cli-zt1227k1.js → cli-fkqhbg8g.js} +5 -3
  49. package/dist/{cli-47m6dkd3.js → cli-g9df2m37.js} +409 -168
  50. package/dist/cli-gzyh0j0g.js +8455 -0
  51. package/dist/cli-hgqfttfc.js +183 -0
  52. package/dist/cli-hj65wsts.js +17755 -0
  53. package/dist/cli-j9z8mfx3.js +476 -0
  54. package/dist/{cli-aehm1rbq.js → cli-kc3dp84d.js} +1135 -61
  55. package/dist/{cli-h38x0qhg.js → cli-kdnehe0t.js} +4 -3
  56. package/dist/{cli-20cj37vt.js → cli-m4excttz.js} +136 -2216
  57. package/dist/{cli-8qmme6vd.js → cli-mg24hkt4.js} +2 -2
  58. package/dist/{cli-xhaeg2nc.js → cli-mzkqpxee.js} +1 -1
  59. package/dist/{cli-q67y4wzm.js → cli-phsaj4xq.js} +24 -2
  60. package/dist/{cli-pw9rj5t9.js → cli-pqetb83f.js} +1 -176
  61. package/dist/{cli-rj19hc34.js → cli-qjxy52w7.js} +1 -1
  62. package/dist/{cli-53sz8gkz.js → cli-qv4jv8bx.js} +1 -1
  63. package/dist/cli-qvnnx4ct.js +1051 -0
  64. package/dist/cli-rb9m6rpe.js +4980 -0
  65. package/dist/{cli-tt9k8r35.js → cli-rfezr056.js} +476 -909
  66. package/dist/{cli-ggm92wcn.js → cli-rgjprct3.js} +1 -0
  67. package/dist/{cli-9httdpyq.js → cli-rrqmqr1s.js} +1 -1
  68. package/dist/cli-rykfmgwg.js +102 -0
  69. package/dist/{cli-eetrvv91.js → cli-t3p77qk1.js} +1 -1
  70. package/dist/{cli-68w09sjw.js → cli-vn0qv9cj.js} +25 -88
  71. package/dist/{cli-987hg0aa.js → cli-vwbbaecp.js} +2 -2
  72. package/dist/cli-wd9hz40d.js +69 -0
  73. package/dist/{cli-j57qkrpq.js → cli-wgjx0q33.js} +13 -13
  74. package/dist/cli-xgpf00sk.js +2149 -0
  75. package/dist/{cli-mcnhgnn1.js → cli-ykk5jaax.js} +184 -26
  76. package/dist/{cli-aky8tctm.js → cli-z9x0sh1t.js} +3 -3
  77. package/dist/{cli-1hkrch3k.js → cli-zw65mab6.js} +5 -5
  78. package/dist/cli.js +38 -29
  79. package/dist/cloudflare-workers-ai-system-one-1sg586qv.js +46 -0
  80. package/dist/{codex-adapter-cnm8wk11.js → codex-adapter-jqfqqa21.js} +21 -14
  81. package/dist/{codex-hook-9hptmdhj.js → codex-hook-5eys3dya.js} +2 -2
  82. package/dist/{codex-session-runner-gfg547zj.js → codex-session-runner-zd341dsm.js} +21 -14
  83. package/dist/{commands-e3bn0p75.js → commands-vfsysz4s.js} +9 -8
  84. package/dist/{db-03v9kzr2.js → db-s3znhvbn.js} +15 -10
  85. package/dist/{dispatcher-nrmg8yht.js → dispatcher-5xn0c7rf.js} +32 -28
  86. package/dist/{dsh-adapter-k03nfqa6.js → dsh-adapter-894mgfvs.js} +3 -3
  87. package/dist/{e2b-93x1k0j5.js → e2b-ez07znam.js} +4 -3
  88. package/dist/execute-71ysgv35.js +965 -0
  89. package/dist/{google-generative-ai-bxmv50nc.js → google-generative-ai-1nfcdphp.js} +5 -4
  90. package/dist/{google-vertex-809af514.js → google-vertex-1tqcd7b1.js} +5 -4
  91. package/dist/{handlers-aqcedbyb.js → handlers-5w4jwycr.js} +35 -31
  92. package/dist/{hook-7vkskkre.js → hook-853bxrr6.js} +16 -13
  93. package/dist/{hook-ybvmrmhq.js → hook-z0hbxj85.js} +16 -13
  94. package/dist/{http-2eqa8t24.js → http-akc3wy3m.js} +600 -123
  95. package/dist/{index-f6cryxwe.js → index-4a5njnm1.js} +2 -59
  96. package/dist/{index-a3vn96n8.js → index-5sz16ksh.js} +187 -47
  97. package/dist/{index-c6zsv1bd.js → index-bjmjh9h4.js} +11 -11
  98. package/dist/{index-chgdkq36.js → index-cd151qkq.js} +32 -28
  99. package/dist/{index-x71bvq0p.js → index-k661dzzr.js} +33 -29
  100. package/dist/{index-1yks3qcr.js → index-ttpbhnjg.js} +32 -28
  101. package/dist/{keepalive-vtkf5n99.js → keepalive-8p9rpvz5.js} +11 -10
  102. package/dist/{lead-h3sp357x.js → lead-jp69z5h3.js} +49 -45
  103. package/dist/llama-cpp-classify-t2tccams.js +335 -0
  104. package/dist/{maintenance-jzeqtk3q.js → maintenance-fwx4ev0k.js} +12 -12
  105. package/dist/{mistral-conversations-zx0kn554.js → mistral-conversations-bhvwqcnk.js} +14 -17
  106. package/dist/{oauth-refresh-sweep-qp3dzmdp.js → oauth-refresh-sweep-cwcq1ctx.js} +10 -9
  107. package/dist/{onboard-tmsp0qbz.js → onboard-2b4q0t84.js} +6 -5
  108. package/dist/{openai-codex-responses-4vjqzvjp.js → openai-codex-responses-nekthgks.js} +21 -8
  109. package/dist/{openai-completions-9hjrqbw8.js → openai-completions-hxtzwj0s.js} +8 -11
  110. package/dist/{openai-responses-7b1pn0xt.js → openai-responses-w4r118w7.js} +22 -17
  111. package/dist/{opencode-adapter-x6x2da1z.js → opencode-adapter-zz7xvjj6.js} +6 -4
  112. package/dist/{openrouter-images-9f9ek4rw.js → openrouter-images-b6sqqjgn.js} +4 -6
  113. package/dist/{otel-impl-9mz73q65.js → otel-impl-g8rs4vwr.js} +1 -1
  114. package/dist/{pi-messages-txpjj39q.js → pi-messages-vxdt8v7j.js} +2 -1
  115. package/dist/{pi-mono-adapter-m6dvv0dz.js → pi-mono-adapter-n0me28wt.js} +29 -17
  116. package/dist/pricing-refresh-0v77385v.js +39 -0
  117. package/dist/{rbac-roles-c9wqz83x.js → rbac-roles-krjyazbj.js} +10 -9
  118. package/dist/{render-v2-gryrh9h8.js → render-v2-3wjsp9aw.js} +10 -9
  119. package/dist/runtime-ktjysacx.js +24 -0
  120. package/dist/{setup-q9720bt6.js → setup-yeqryke6.js} +6 -5
  121. package/dist/typesafe-system-one-96960jxp.js +27 -0
  122. package/dist/{virtual-modules-3htpgn8x.js → virtual-modules-dzdeq1sf.js} +21 -13
  123. package/dist/{work-objects-b76x7deb.js → work-objects-3pakpkw0.js} +35 -31
  124. package/dist/{worker-6vrxbv41.js → worker-85testgj.js} +49 -45
  125. package/openapi.json +863 -14
  126. package/package.json +9 -8
  127. package/src/be/db/agents.ts +20 -0
  128. package/src/be/db/tasks/human-free.ts +442 -0
  129. package/src/be/db/tasks/read.ts +56 -1
  130. package/src/be/db/tasks/write.ts +107 -11
  131. package/src/be/db.ts +89 -165
  132. package/src/be/harness-model-support.ts +182 -0
  133. package/src/be/human-free-drain.ts +67 -0
  134. package/src/be/migrations/182_task_human_free_flag.sql +63 -0
  135. package/src/be/migrations/183_human_free_reclassify_queue.sql +25 -0
  136. package/src/be/migrations/184_model_catalog.sql +120 -0
  137. package/src/be/model-catalog-store.ts +644 -0
  138. package/src/be/model-tier-keys.ts +31 -0
  139. package/src/be/model-tier-resolution.ts +489 -0
  140. package/src/be/model-validation.ts +112 -0
  141. package/src/be/models-catalog.ts +61 -7
  142. package/src/be/modelsdev-cache.json +82 -0
  143. package/src/be/modelsdev-cache.ts +2 -0
  144. package/src/be/pricing-refresh.ts +154 -12
  145. package/src/be/scripts/typecheck.ts +15 -0
  146. package/src/be/seed-scripts/catalog/task-failure-audit.ts +61 -9
  147. package/src/be/swarm-config-guard.ts +6 -0
  148. package/src/cli.tsx +10 -0
  149. package/src/commands/claude-managed-setup.ts +3 -1
  150. package/src/commands/runner.ts +76 -2
  151. package/src/extensions/contract-types.generated.ts +1 -1
  152. package/src/heartbeat/heartbeat.ts +64 -15
  153. package/src/heartbeat/templates.ts +2 -2
  154. package/src/http/agents.ts +49 -1
  155. package/src/http/index.ts +8 -0
  156. package/src/http/models-catalog.ts +357 -8
  157. package/src/http/poll.ts +45 -2
  158. package/src/http/schedules.ts +36 -6
  159. package/src/http/session-data.ts +2 -5
  160. package/src/http/stats.ts +6 -8
  161. package/src/http/tasks.ts +26 -1
  162. package/src/http/usage-cache.ts +56 -15
  163. package/src/http/workflows.ts +35 -8
  164. package/src/prompts/session-templates.ts +1 -0
  165. package/src/providers/claude-managed-models.ts +47 -161
  166. package/src/providers/codex-adapter.ts +4 -4
  167. package/src/providers/codex-models.ts +65 -198
  168. package/src/providers/reasoning-effort.ts +44 -140
  169. package/src/rbac/legacy-policy.ts +25 -0
  170. package/src/rbac/permissions.ts +10 -0
  171. package/src/scripts-runtime/sdk-allowlist.ts +4 -0
  172. package/src/scripts-runtime/types/stdlib.d.ts +15 -0
  173. package/src/scripts-runtime/types/swarm-extension.d.ts +3 -0
  174. package/src/scripts-runtime/types/swarm-sdk.d.ts +15 -0
  175. package/src/server.ts +6 -0
  176. package/src/slack/message-text.ts +56 -2
  177. package/src/slack/thread-buffer.ts +10 -2
  178. package/src/tests/agents-list-model-display.test.ts +4 -6
  179. package/src/tests/base-prompt.test.ts +3 -0
  180. package/src/tests/claude-adapter.test.ts +10 -0
  181. package/src/tests/claude-managed-adapter.test.ts +12 -18
  182. package/src/tests/codex-adapter.test.ts +2 -1
  183. package/src/tests/codex-gpt6-release.test.ts +66 -5
  184. package/src/tests/context-window.test.ts +7 -5
  185. package/src/tests/db-extraction-contract.test.ts +1 -0
  186. package/src/tests/db-task-read-contract.test.ts +5 -1
  187. package/src/tests/db-task-write-contract.test.ts +4 -1
  188. package/src/tests/docs-index-footprint.test.js +3 -3
  189. package/src/tests/explicit-model-validation.test.ts +332 -0
  190. package/src/tests/harness-model-support.test.ts +374 -0
  191. package/src/tests/heartbeat-checklist-config.test.ts +71 -0
  192. package/src/tests/heartbeat-checklist.test.ts +4 -2
  193. package/src/tests/heartbeat-orphan-resume.test.ts +48 -0
  194. package/src/tests/human-free-flag.test.ts +279 -0
  195. package/src/tests/human-free-oracle.ts +57 -0
  196. package/src/tests/human-free-reclassify.test.ts +669 -0
  197. package/src/tests/migration-046-budgets.test.ts +3 -3
  198. package/src/tests/model-catalog-migration.test.ts +134 -0
  199. package/src/tests/model-catalog-table.test.ts +249 -0
  200. package/src/tests/model-catalog-write-access.test.ts +387 -0
  201. package/src/tests/model-tier-resolution.test.ts +519 -0
  202. package/src/tests/models-catalog.test.ts +1 -1
  203. package/src/tests/multi-runtime-enabled-stats.test.ts +65 -1
  204. package/src/tests/package-publish.test.ts +137 -36
  205. package/src/tests/pricing-routes.test.ts +2 -2
  206. package/src/tests/rbac-engine.test.ts +42 -2
  207. package/src/tests/reasoning-effort.test.ts +109 -0
  208. package/src/tests/refresh-modelsdev-carryforward.test.ts +5 -5
  209. package/src/tests/runtime-model-catalog.test.ts +86 -0
  210. package/src/tests/script-connections.test.ts +5 -0
  211. package/src/tests/seed-scripts.test.ts +89 -0
  212. package/src/tests/session-costs.test.ts +14 -11
  213. package/src/tests/slack-message-text.test.ts +101 -0
  214. package/src/tests/slack-thread-buffer-context.test.ts +93 -0
  215. package/src/tests/task-cascade-fail.test.ts +148 -2
  216. package/src/tests/tool-annotations.test.ts +1 -1
  217. package/src/tests/usage-cache.test.ts +166 -0
  218. package/src/tests/workflow-executors.test.ts +5 -2
  219. package/src/tests/workflow-system-one-decision-review.test.ts +1021 -0
  220. package/src/tests/workflow-system-one-decision-save-warnings.test.ts +365 -0
  221. package/src/tests/workflow-system-one-decision.test.ts +2063 -0
  222. package/src/tools/model-catalog/index.ts +137 -0
  223. package/src/tools/schedules/create-schedule.ts +16 -1
  224. package/src/tools/schedules/patch-schedule.ts +17 -0
  225. package/src/tools/schedules/update-schedule.ts +17 -0
  226. package/src/tools/send-task.ts +14 -0
  227. package/src/tools/task-action.ts +12 -0
  228. package/src/tools/tool-config.ts +3 -1
  229. package/src/tools/workflows/create-workflow.ts +9 -2
  230. package/src/tools/workflows/patch-workflow-node.ts +17 -7
  231. package/src/tools/workflows/patch-workflow.ts +9 -2
  232. package/src/tools/workflows/update-workflow.ts +9 -2
  233. package/src/types.ts +30 -1
  234. package/src/utils/context-window.ts +47 -53
  235. package/src/utils/harness-cli-version.ts +115 -0
  236. package/src/utils/harness-model-error.ts +33 -0
  237. package/src/utils/runtime-model-catalog.ts +137 -0
  238. package/src/workflows/approval-resolution.ts +39 -0
  239. package/src/workflows/definition.ts +7 -0
  240. package/src/workflows/engine.ts +69 -1
  241. package/src/workflows/executors/agent-task.ts +9 -0
  242. package/src/workflows/executors/base.ts +53 -0
  243. package/src/workflows/executors/human-in-the-loop.ts +8 -8
  244. package/src/workflows/executors/index.ts +1 -0
  245. package/src/workflows/executors/registry.ts +2 -0
  246. package/src/workflows/executors/system-one-decision.ts +1232 -0
  247. package/src/workflows/executors/system-one-providers.ts +301 -0
  248. package/src/workflows/executors/workflow-llm.ts +13 -1
  249. package/src/workflows/readiness.ts +86 -0
  250. package/src/workflows/recovery.ts +12 -12
  251. package/src/workflows/resume.ts +23 -9
  252. package/templates/schedules/daily-blocker-digest/content.md +4 -5
  253. package/templates/skills/agent-browser/SKILL.md +6 -5
  254. package/templates/skills/agent-browser/content.md +6 -5
  255. package/templates/skills/heartbeat-runbook/config.json +1 -1
  256. package/templates/skills/heartbeat-runbook/content.md +2 -2
  257. package/templates/skills/swarm-scripts/SKILL.md +1 -1
  258. package/templates/skills/swarm-scripts/content.md +1 -1
  259. package/templates/skills/workflow-iterate/content.md +81 -1
  260. package/templates/workflows/claude-code-changelog-watch/content.md +2 -0
  261. package/templates/workflows/pr-review-status-sweep/content.md +2 -0
  262. package/dist/claude-sdk-session-t442qz6f.js +0 -39108
  263. package/dist/cli-0xjkj1j7.js +0 -61
  264. package/dist/cli-65hjmev5.js +0 -1141
  265. package/dist/cli-aw1jwp1r.js +0 -14075
  266. package/dist/cli-h967nb6r.js +0 -6603
  267. package/dist/cli-vapyk8sw.js +0 -20
  268. package/dist/cli-ws6rqycm.js +0 -247
  269. package/dist/pricing-refresh-v1cbr9te.js +0 -27
  270. package/src/providers/modelsdev-reasoning.json +0 -6453
  271. package/dist/{codex-login-x3jk4etn.js → codex-login-he9vrn90.js} +3 -3
  272. package/dist/{index-f24115ge.js → index-2wsdsnmm.js} +3 -3
@@ -36,7 +36,9 @@ import {
36
36
  import type { SteerDeliveryResult } from "../providers/types.ts";
37
37
  import { initTelemetry, telemetry } from "../telemetry.ts";
38
38
  import {
39
+ type ModelTierOverrides,
39
40
  type ProviderName,
41
+ parseWorkerModelTierOverrides,
40
42
  type ReasoningEffort,
41
43
  type RepoGuidelines,
42
44
  resolveTaskModelSelection,
@@ -55,9 +57,11 @@ import {
55
57
  type RateLimitWindowTelemetry,
56
58
  resolveCodexCreditsExhaustedCooldownMs,
57
59
  } from "../utils/error-tracker.ts";
60
+ import { probeHarnessCliVersion, reportHarnessModelOutcome } from "../utils/harness-cli-version.ts";
58
61
  import { resolveHarnessProvider } from "../utils/harness-provider.ts";
59
62
  import { prettyPrintLine, prettyPrintStderr } from "../utils/pretty-print.ts";
60
63
  import { terminateRegisteredProcessGroups } from "../utils/process-group.ts";
64
+ import { refreshRuntimeModelCatalog } from "../utils/runtime-model-catalog.ts";
61
65
  import { resolveScriptsOnlyMode } from "../utils/scripts-only-mode.ts";
62
66
  import { scrubSecrets } from "../utils/secret-scrubber.ts";
63
67
  import { refreshSkillsIfChanged } from "../utils/skills-refresh.ts";
@@ -1220,6 +1224,8 @@ const SWARM_TOOL_LABELS: Record<string, string | null> = {
1220
1224
  "set-config": "⚙️ Setting config",
1221
1225
  "list-config": "⚙️ Listing config",
1222
1226
  "delete-config": "⚙️ Deleting config",
1227
+ "model-catalog-refresh": "🧠 Refreshing model catalog",
1228
+ "model-catalog-overlay-upsert": "🧠 Updating model catalog overlay",
1223
1229
  // Schedules
1224
1230
  "create-schedule": "📅 Creating schedule",
1225
1231
  "list-schedules": "📅 Listing schedules",
@@ -2909,6 +2915,16 @@ interface PollOptions {
2909
2915
  pollInterval: number;
2910
2916
  pollTimeout: number;
2911
2917
  since?: string; // Optional: for filtering finished tasks
2918
+ /** Live harness provider; keys the MODEL_TIER_* overrides sent with the poll. */
2919
+ harnessProvider?: ProviderName;
2920
+ }
2921
+
2922
+ /**
2923
+ * The worker's own MODEL_TIER_* overrides, parsed from its process env (not
2924
+ * the swarm_config-merged env: global config is already visible server-side).
2925
+ */
2926
+ function workerModelTierOverrides(provider: ProviderName): ModelTierOverrides {
2927
+ return parseWorkerModelTierOverrides(process.env, provider);
2912
2928
  }
2913
2929
 
2914
2930
  type RequesterProfile = NonNullable<Trigger["requestedBy"]>;
@@ -2990,6 +3006,8 @@ export async function registerAgent(opts: {
2990
3006
  provider,
2991
3007
  harness_provider: harnessProvider,
2992
3008
  runtimeInstanceId: opts.runtimeInstanceId,
3009
+ modelTierOverrides: workerModelTierOverrides(harnessProvider),
3010
+ harnessCliVersion: (await probeHarnessCliVersion(harnessProvider)) ?? undefined,
2993
3011
  }),
2994
3012
  });
2995
3013
 
@@ -3035,6 +3053,14 @@ async function pollForTrigger(opts: PollOptions): Promise<Trigger | null> {
3035
3053
 
3036
3054
  async function pollForTriggerOnce(opts: PollOptions): Promise<Trigger | null> {
3037
3055
  const startTime = Date.now();
3056
+ // Keep the process-wide model catalog fresh (TTL-cached, never throws) so
3057
+ // context windows, pricing and reasoning levels cover models added to the
3058
+ // API's catalog after this worker booted.
3059
+ void refreshRuntimeModelCatalog({
3060
+ apiUrl: opts.apiUrl,
3061
+ apiKey: opts.apiKey,
3062
+ agentId: opts.agentId,
3063
+ });
3038
3064
  const headers: Record<string, string> = {
3039
3065
  "X-Agent-ID": opts.agentId,
3040
3066
  };
@@ -3045,6 +3071,11 @@ async function pollForTriggerOnce(opts: PollOptions): Promise<Trigger | null> {
3045
3071
  headers.Authorization = `Bearer ${opts.apiKey}`;
3046
3072
  }
3047
3073
  injectTraceContext(headers);
3074
+ if (opts.harnessProvider) {
3075
+ headers["X-Model-Tier-Overrides"] = encodeURIComponent(
3076
+ JSON.stringify(workerModelTierOverrides(opts.harnessProvider)),
3077
+ );
3078
+ }
3048
3079
 
3049
3080
  while (Date.now() - startTime < opts.pollTimeout) {
3050
3081
  try {
@@ -3527,6 +3558,9 @@ async function spawnProviderProcess(
3527
3558
  taskId?: string;
3528
3559
  model?: string;
3529
3560
  modelTier?: string;
3561
+ /** Server claim-time resolution (task.resolvedModel); wins over the local one. */
3562
+ resolvedModel?: string;
3563
+ modelSource?: string;
3530
3564
  effort?: ReasoningEffort;
3531
3565
  resumeSessionId?: string;
3532
3566
  harnessProvider: ProviderName;
@@ -3562,7 +3596,7 @@ async function spawnProviderProcess(
3562
3596
  opts.apiKey,
3563
3597
  opts.agentId,
3564
3598
  process.env,
3565
- opts.model,
3599
+ opts.resolvedModel || opts.model,
3566
3600
  {
3567
3601
  repoId: sessionRepo?.id,
3568
3602
  provider: adapter.name as ProviderName,
@@ -3619,7 +3653,20 @@ async function spawnProviderProcess(
3619
3653
  harnessProvider: opts.harnessProvider,
3620
3654
  env: freshEnv,
3621
3655
  });
3622
- const taskModel = taskModelSelection.model || "";
3656
+ // The server resolves the model at claim time (worker-env > tier-config >
3657
+ // tier-default, `latest:` aliases, guardrails) and records it on the task.
3658
+ // The local resolution stays as a check: a difference usually means the
3659
+ // server saw a stale MODEL_TIER_* override or a swarm_config tier value.
3660
+ if (
3661
+ opts.resolvedModel &&
3662
+ taskModelSelection.model &&
3663
+ taskModelSelection.model !== opts.resolvedModel
3664
+ ) {
3665
+ console.log(
3666
+ `[${opts.role}] model resolution mismatch for task ${opts.taskId ?? "?"}: server=${opts.resolvedModel} (${opts.modelSource ?? "?"}) local=${taskModelSelection.model}; using server value`,
3667
+ );
3668
+ }
3669
+ const taskModel = opts.resolvedModel || taskModelSelection.model || "";
3623
3670
  const model = taskModel || configModel || "";
3624
3671
 
3625
3672
  // Resolve Codex OAuth pool slot BEFORE building ProviderSessionConfig so we
@@ -4517,6 +4564,17 @@ async function checkCompletedProcesses(
4517
4564
  console.log(`[${role}] Detected error for task ${taskId.slice(0, 8)}: ${failureReason}`);
4518
4565
  }
4519
4566
 
4567
+ // Record whether this worker's CLI accepts the model (harness_model_support).
4568
+ void reportHarnessModelOutcome({
4569
+ apiUrl: apiConfig.apiUrl,
4570
+ apiKey: apiConfig.apiKey,
4571
+ agentId: apiConfig.agentId,
4572
+ harness: harnessProvider,
4573
+ model,
4574
+ exitCode: result.exitCode,
4575
+ failureReason,
4576
+ });
4577
+
4520
4578
  // If rate-limited and we know which key was used, report it.
4521
4579
  // Codex adapter prefixes failure reasons with `[rate-limit]` /
4522
4580
  // `[usage-limit]` (see codex-adapter.formatTerminalError); Claude
@@ -5911,6 +5969,8 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
5911
5969
  taskId: task.id,
5912
5970
  model: (task as { model?: string }).model,
5913
5971
  modelTier: (task as { modelTier?: string }).modelTier,
5972
+ resolvedModel: (task as { resolvedModel?: string }).resolvedModel,
5973
+ modelSource: (task as { modelSource?: string }).modelSource,
5914
5974
  effort: (task as { effort?: ReasoningEffort }).effort,
5915
5975
  harnessProvider: state.harnessProvider,
5916
5976
  cwd: resumeCwd,
@@ -6127,6 +6187,7 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
6127
6187
  pollInterval: PollIntervalMs,
6128
6188
  runtimeInstanceId,
6129
6189
  pollTimeout: effectiveTimeout,
6190
+ harnessProvider: state.harnessProvider,
6130
6191
  });
6131
6192
 
6132
6193
  if (trigger) {
@@ -6407,6 +6468,16 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
6407
6468
  .join("\n\n");
6408
6469
  const taskSystemPrompt = taskPromptParts + cwdWarning;
6409
6470
 
6471
+ // The server refused the task's explicit model for this worker's CLI
6472
+ // version (harness_model_support = unsupported): fail fast, no spawn.
6473
+ const modelUnsupported = (trigger.task as { modelUnsupported?: string } | undefined)
6474
+ ?.modelUnsupported;
6475
+ if (trigger.taskId && modelUnsupported) {
6476
+ console.log(`[${role}] ${modelUnsupported}`);
6477
+ await ensureTaskFinished(apiConfig, role, trigger.taskId, 1, modelUnsupported);
6478
+ continue;
6479
+ }
6480
+
6410
6481
  iteration++;
6411
6482
  const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
6412
6483
  const taskIdSlice = trigger.taskId?.slice(0, 8) || "notask";
@@ -6472,6 +6543,9 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
6472
6543
  taskId: trigger.taskId,
6473
6544
  model: taskModel,
6474
6545
  modelTier: taskModelTier,
6546
+ resolvedModel: (trigger.task as { resolvedModel?: string } | undefined)
6547
+ ?.resolvedModel,
6548
+ modelSource: (trigger.task as { modelSource?: string } | undefined)?.modelSource,
6475
6549
  effort: taskEffort,
6476
6550
  harnessProvider: state.harnessProvider,
6477
6551
  cwd: effectiveCwd,
@@ -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 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';
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 resolvedModel?: string | undefined;\n modelSource?: string | undefined;\n modelAlias?: string | 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';
@@ -28,6 +28,7 @@ import {
28
28
  getStaleUnassignedAffinityTasks,
29
29
  getStalledInProgressTasks,
30
30
  getSupersededTasksWithoutResume,
31
+ getSupersededTasksWithUnsettledDependents,
31
32
  getTaskById,
32
33
  getTaskStats,
33
34
  getTasksByStatus,
@@ -42,6 +43,7 @@ import {
42
43
  releaseStaleOfferedTasksForOfflineAgents,
43
44
  releaseStaleProcessingInbox,
44
45
  releaseStaleReviewingTasks,
46
+ settleSupersededTaskDependents,
45
47
  supersedeTask,
46
48
  updateAgentStatus,
47
49
  } from "../be/db";
@@ -66,6 +68,7 @@ import {
66
68
  resolveLeadOnlyRecoveryAssignment,
67
69
  } from "../tasks/worker-follow-up";
68
70
  import type { AgentTask } from "../types";
71
+ import { isEnvFlagEnabled } from "../utils/env-flag";
69
72
  import { isMultiRuntimeEnabled } from "../utils/multi-runtime";
70
73
  import { scrubSecrets } from "../utils/secret-scrubber";
71
74
  import { getExecutorRegistry } from "../workflows";
@@ -217,12 +220,30 @@ export const HEARTBEAT_RESUME_PIN_GRACE_MIN = (() => {
217
220
  */
218
221
  const POOL_AFFINITY_ESCALATION_MIN = Number(process.env.POOL_AFFINITY_ESCALATION_MIN) || 15;
219
222
 
220
- /** Heartbeat checklist interval: how often to check HEARTBEAT.md (default: 30 min) */
221
- const HEARTBEAT_CHECKLIST_INTERVAL_MS =
222
- Number(process.env.HEARTBEAT_CHECKLIST_INTERVAL_MS) || 30 * 60 * 1000;
223
+ const DEFAULT_HEARTBEAT_CHECKLIST_INTERVAL_MS = 30 * 60 * 1000;
223
224
 
224
- /** Whether to disable the heartbeat checklist entirely */
225
- const HEARTBEAT_CHECKLIST_DISABLE = Boolean(process.env.HEARTBEAT_CHECKLIST_DISABLE);
225
+ /**
226
+ * Heartbeat checklist interval: how often to check HEARTBEAT.md (default: 30 min).
227
+ *
228
+ * An explicit `0` (or negative) turns the RECURRING checklist tick off; the
229
+ * one-shot boot triage still runs. Absent, empty, or non-numeric values fall
230
+ * back to the default (not `Number(x) || default`, which would coerce `0` to 30 min).
231
+ */
232
+ function heartbeatChecklistIntervalMs(): number {
233
+ const raw = process.env.HEARTBEAT_CHECKLIST_INTERVAL_MS?.trim();
234
+ if (!raw) return DEFAULT_HEARTBEAT_CHECKLIST_INTERVAL_MS;
235
+ const parsed = Number(raw);
236
+ if (!Number.isFinite(parsed)) return DEFAULT_HEARTBEAT_CHECKLIST_INTERVAL_MS;
237
+ return Math.max(0, parsed);
238
+ }
239
+
240
+ /**
241
+ * Whether to disable the heartbeat checklist entirely (recurring tick AND boot
242
+ * triage). Parsed with the shared env-flag helper, so `"false"`/`"0"` keep it on.
243
+ */
244
+ function isHeartbeatChecklistDisabled(): boolean {
245
+ return isEnvFlagEnabled("HEARTBEAT_CHECKLIST_DISABLE", false);
246
+ }
226
247
 
227
248
  // ============================================================================
228
249
  // Types
@@ -275,6 +296,7 @@ export interface HeartbeatFindings {
275
296
 
276
297
  let heartbeatInterval: ReturnType<typeof setInterval> | null = null;
277
298
  let checklistInterval: ReturnType<typeof setInterval> | null = null;
299
+ let bootTriageTimeout: ReturnType<typeof setTimeout> | null = null;
278
300
  let isSweeping = false;
279
301
  let beforeHeartbeatSupersedeForTests: ((task: AgentTask) => void | Promise<void>) | null = null;
280
302
 
@@ -530,10 +552,15 @@ async function repairSupersededWithoutResume(findings: HeartbeatFindings): Promi
530
552
  new Date(now - ORPHAN_SUPERSEDE_MAX_AGE_MS).toISOString(),
531
553
  );
532
554
  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;
555
+ const resume =
556
+ task.agentId && getNextResumeGeneration(task) <= maxResumeGenerations()
557
+ ? await createResumeFollowUp({ parentId: task.id, reason: "crash_recovery" })
558
+ : null;
559
+ if (resume?.kind !== "created" || !task.agentId) {
560
+ // No resume will carry the work: its dependents fail as they would have.
561
+ await settleSupersededTaskDependents(task.id, null);
562
+ continue;
563
+ }
537
564
  await backfillSupersedeTaskResumeTaskId(task.id, resume.task.id);
538
565
  findings.autoResumedTasks.push({
539
566
  taskId: task.id,
@@ -545,6 +572,16 @@ async function repairSupersededWithoutResume(findings: HeartbeatFindings): Promi
545
572
  `[Heartbeat] Created missing resume ${resume.task.id.slice(0, 8)} for superseded task ${task.id.slice(0, 8)}`,
546
573
  );
547
574
  }
575
+
576
+ // A crash after the resume was created but before the backfill leaves
577
+ // dependents waiting on the superseded task; settle them onto that resume.
578
+ const unsettled = await getSupersededTasksWithUnsettledDependents(
579
+ new Date(now - ORPHAN_SUPERSEDE_MIN_AGE_MS).toISOString(),
580
+ new Date(now - ORPHAN_SUPERSEDE_MAX_AGE_MS).toISOString(),
581
+ );
582
+ for (const { taskId, resumeTaskId } of unsettled) {
583
+ await settleSupersededTaskDependents(taskId, resumeTaskId);
584
+ }
548
585
  }
549
586
 
550
587
  function isRemediationAction(value: unknown): value is RemediationAction {
@@ -679,6 +716,7 @@ async function remediateCrashedWorkerTask(
679
716
  resume.kind === "skipped"
680
717
  ? `resume_creation_skipped_${resume.reason}`
681
718
  : "resume_creation_skipped_workflow";
719
+ await settleSupersededTaskDependents(task.id, null);
682
720
  const failed = await failTask(task.id, reason);
683
721
  if (failed) {
684
722
  findings.autoFailedTasks.push({
@@ -1835,19 +1873,26 @@ export async function createBootTriageTask(): Promise<void> {
1835
1873
  /**
1836
1874
  * Start the heartbeat checklist polling loop (separate from the infrastructure sweep).
1837
1875
  */
1838
- export function startHeartbeatChecklist(intervalMs = HEARTBEAT_CHECKLIST_INTERVAL_MS): void {
1839
- if (HEARTBEAT_CHECKLIST_DISABLE) {
1876
+ export function startHeartbeatChecklist(intervalMs = heartbeatChecklistIntervalMs()): void {
1877
+ if (isHeartbeatChecklistDisabled()) {
1840
1878
  console.log("[Heartbeat] Checklist disabled via HEARTBEAT_CHECKLIST_DISABLE");
1841
1879
  return;
1842
1880
  }
1843
- if (checklistInterval) {
1881
+ if (checklistInterval || bootTriageTimeout) {
1844
1882
  return; // Already running
1845
1883
  }
1846
1884
 
1847
- console.log(`[Heartbeat] Checklist starting with ${intervalMs}ms interval`);
1848
-
1849
1885
  // Boot triage at T+90s — after reboot sweep (T+5s) has completed and results are available
1850
- setTimeout(() => createBootTriageTask(), 90_000);
1886
+ bootTriageTimeout = setTimeout(() => createBootTriageTask(), 90_000);
1887
+
1888
+ if (intervalMs <= 0) {
1889
+ console.log(
1890
+ "[Heartbeat] Recurring checklist off (HEARTBEAT_CHECKLIST_INTERVAL_MS=0); boot triage still scheduled",
1891
+ );
1892
+ return;
1893
+ }
1894
+
1895
+ console.log(`[Heartbeat] Checklist starting with ${intervalMs}ms interval`);
1851
1896
 
1852
1897
  // Recurring checklist starts from the second interval onward
1853
1898
  checklistInterval = setInterval(() => {
@@ -1864,6 +1909,10 @@ export function startHeartbeatChecklist(intervalMs = HEARTBEAT_CHECKLIST_INTERVA
1864
1909
  * Stop the heartbeat checklist polling loop.
1865
1910
  */
1866
1911
  export function stopHeartbeatChecklist(): void {
1912
+ if (bootTriageTimeout) {
1913
+ clearTimeout(bootTriageTimeout);
1914
+ bootTriageTimeout = null;
1915
+ }
1867
1916
  if (checklistInterval) {
1868
1917
  clearInterval(checklistInterval);
1869
1918
  checklistInterval = null;
@@ -28,8 +28,8 @@ Goal: Review system status and your standing orders, take action if needed.
28
28
  ## Instructions
29
29
  1. **Read your HEARTBEAT.md** — run \`read /workspace/HEARTBEAT.md\` to get the latest standing orders (the snapshot above may be slightly stale).
30
30
  2. **Prune tracked HEARTBEAT items FIRST.** Active Blockers + Watch Items + Open Discussion combined must stay at ≤10 items; 20 is the absolute max only when genuinely super busy. This cap does not apply to evergreen Standing Orders / Governance / Playbook-index reference sections. Before adding anything, re-check every tracked item against its lift trigger and remove anything resolved, stale, or past its trigger date. Lift incident detail to memory; never keep it inline in HEARTBEAT.md. Every new tracked item must include an explicit lift trigger + date; if it has no removal condition, do not add it. If the tracked list is already at/over cap, prune before (or instead of) adding — the cap is binding.
31
- 3. **Run seeded heartbeat data-gathering.** Use \`script-run\` with global script \`Heartbeat Audit\` and pass the current HEARTBEAT.md text as \`heartbeatMarkdown\`. It covers Rules #10/#13/#15/#16/#17: resolved stale PRs, pool-target risk schedules, schedule/provider failure clusters, and whether daily-blocker-digest ran today. The Slack thread-reply check (Rule #11) stays Lead-side; the script runtime has no Slack token.
32
- 4. Review the system status above plus the \`Heartbeat Audit\` result for anything that needs attention (stalled tasks, idle workers with available work, anomalies).
31
+ 3. **Run seeded heartbeat data-gathering.** Use \`script-run\` with the global scripts \`schedule-health\` and \`task-failure-audit\`, both with args \`{ "days": 1, "publishPage": false }\`. They report failing schedules and failure clusters. Check any other standing order in HEARTBEAT.md yourself. Scripts can read Slack too (\`ctx.swarm.slack_read\` or the seeded \`slack-thread-flatten\` script), so a recurring Slack check can be scripted.
32
+ 4. Review the system status above plus the script results for anything that needs attention (stalled tasks, idle workers with available work, anomalies).
33
33
  5. **CRITICAL — Reboot failure triage:** Failures with reason "worker session not found" or "worker session heartbeat is stale" indicate tasks that were INTERRUPTED by a server restart. These are NOT "expected auto-cleanup" — they represent work that was lost mid-execution. For each such failure:
34
34
  - Check what the task was (via \`get-task-details\` with the task ID from the failure)
35
35
  - If a retry task was auto-created (tagged \`reboot-retry\`), verify it is progressing
@@ -1,4 +1,5 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import { isAlias } from "@desplega/model-catalog";
2
3
  import { ensure } from "@desplega.ai/business-use";
3
4
  import { z } from "zod";
4
5
  import {
@@ -29,6 +30,12 @@ import {
29
30
  upsertSwarmConfig,
30
31
  } from "../be/db";
31
32
  import { createEvent } from "../be/events";
33
+ import { setAgentHarnessCliVersion } from "../be/harness-model-support";
34
+ import {
35
+ sanitizeModelTierOverrides,
36
+ setAgentModelTierOverrides,
37
+ } from "../be/model-tier-resolution";
38
+ import { explicitModelError } from "../be/model-validation";
32
39
  import {
33
40
  getRuntimeInstanceById,
34
41
  listRuntimeInstancesForAgent,
@@ -150,6 +157,14 @@ const registerAgent = route({
150
157
  * enforcing the mode-dependent requirement.
151
158
  */
152
159
  runtimeInstanceId: z.string().min(1).optional(),
160
+ /**
161
+ * The worker's parsed MODEL_TIER_* env overrides, {provider: {tier: model}}.
162
+ * Values only. Stored on the agent row for claim-time resolution; `{}`
163
+ * clears, omitted leaves the stored value unchanged.
164
+ */
165
+ modelTierOverrides: z.record(z.string(), z.record(z.string(), z.string())).optional(),
166
+ /** The worker's harness CLI version (`claude --version` / `codex --version`). */
167
+ harnessCliVersion: z.string().max(64).optional(),
153
168
  }),
154
169
  responses: {
155
170
  200: {
@@ -246,7 +261,7 @@ const updateAgentRuntimeRoute = route({
246
261
  pattern: ["api", "agents", null, "runtime"],
247
262
  summary: "Update an agent's runtime harness and default model",
248
263
  description:
249
- "Updates `agents.harness_provider` and agent-scoped runtime config. The settings apply to future provider sessions. For `model`, `reasoning_effort`, and `claude.transport`: omit the field to leave it unchanged, send `null` to clear the corresponding override, or send a value to set it.",
264
+ "Updates `agents.harness_provider` and agent-scoped runtime config. The settings apply to future provider sessions. For `model`, `reasoning_effort`, and `claude.transport`: omit the field to leave it unchanged, send `null` to clear the corresponding override, or send a value to set it. A `model` the model catalog does not list is a 400 unless `allow_custom_model` is true. A `latest:` alias is not accepted here (aliases resolve for a task's model and for `MODEL_TIER_*` values); `reasoning_effort` must be one the harness and model support.",
250
265
  tags: ["Agents"],
251
266
  params: z.object({ id: z.string() }),
252
267
  query: z.object({ repoId: z.string().optional() }),
@@ -708,6 +723,17 @@ export async function handleAgentRegister(
708
723
  return { agent, created: true };
709
724
  });
710
725
 
726
+ if (parsed.body.harnessCliVersion !== undefined) {
727
+ await setAgentHarnessCliVersion(agentId, parsed.body.harnessCliVersion.trim() || null);
728
+ }
729
+
730
+ if (parsed.body.modelTierOverrides) {
731
+ await setAgentModelTierOverrides(
732
+ agentId,
733
+ sanitizeModelTierOverrides(parsed.body.modelTierOverrides),
734
+ );
735
+ }
736
+
711
737
  telemetry.agent("registered", {
712
738
  role: parsed.body.role,
713
739
  capabilities: parsed.body.capabilities ?? [],
@@ -1108,6 +1134,28 @@ export async function handleAgentsRest(
1108
1134
  }
1109
1135
  }
1110
1136
 
1137
+ // An explicit model must be in the catalog unless the caller marks it custom
1138
+ // (`allow_custom_model`). `null` clears the override and `undefined` leaves it.
1139
+ if (typeof model === "string" && isAlias(model)) {
1140
+ jsonError(
1141
+ res,
1142
+ `A latest: alias cannot be an agent's default model ("${model}"). Set the alias on a task, or as a MODEL_TIER_<PROVIDER>_<TIER> value.`,
1143
+ 400,
1144
+ );
1145
+ return true;
1146
+ }
1147
+ if (typeof model === "string") {
1148
+ const modelError = await explicitModelError({
1149
+ model,
1150
+ allowCustomModel: allow_custom_model,
1151
+ harnessProvider: harness_provider,
1152
+ });
1153
+ if (modelError) {
1154
+ jsonError(res, modelError, 400);
1155
+ return true;
1156
+ }
1157
+ }
1158
+
1111
1159
  let agent: Agent | null;
1112
1160
  try {
1113
1161
  agent = await getDbClient().transaction(async () => {
package/src/http/index.ts CHANGED
@@ -15,6 +15,7 @@ import {
15
15
  upsertSwarmConfig,
16
16
  } from "../be/db";
17
17
  import { startDbRetention, stopDbRetention } from "../be/db-retention";
18
+ import { startHumanFreeDrain, stopHumanFreeDrain } from "../be/human-free-drain";
18
19
  import {
19
20
  enqueueAuditRow,
20
21
  flushAuditBuffer,
@@ -488,6 +489,9 @@ async function shutdown() {
488
489
  // Stop opt-in session, agent-log, and event retention before closing SQLite.
489
490
  await stopDbRetention();
490
491
 
492
+ // Stop the human-free reclassification drain before closing SQLite.
493
+ stopHumanFreeDrain();
494
+
491
495
  // Stop scratch-script retention garbage collector
492
496
  stopScratchScriptGc();
493
497
 
@@ -771,6 +775,10 @@ httpServer
771
775
  // read on each tick, so config reloads take effect without a restart.
772
776
  await startDbRetention();
773
777
 
778
+ // Finish the human-free reclassification a request left queued (a very large
779
+ // task tree, or a restart mid-drain). Wakes on demand; see human-free-drain.ts.
780
+ startHumanFreeDrain();
781
+
774
782
  // (RBAC audit sink is wired pre-listen — see above httpServer.listen.)
775
783
 
776
784
  // Background backfill: re-embed any agent_memory rows with wrong-dimension