@desplega.ai/agent-swarm 1.165.0 → 1.167.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 (296) hide show
  1. package/dist/acp-adapter-zpvy6321.js +34 -0
  2. package/dist/{actions-jna0b3wm.js → actions-fq2zrwtf.js} +30 -34
  3. package/dist/{amp-adapter-0eve81pz.js → amp-adapter-5gbk7501.js} +7 -7
  4. package/dist/{anthropic-messages-hst62ws9.js → anthropic-messages-13yn502s.js} +4 -4
  5. package/dist/{app-75hm5cfa.js → app-nctetn1e.js} +11 -11
  6. package/dist/{assistant-yzd51j33.js → assistant-0aq8gjvj.js} +31 -35
  7. package/dist/{azure-openai-responses-mh8tvf78.js → azure-openai-responses-9st34pfz.js} +5 -5
  8. package/dist/{boot-reembed-z158xq32.js → boot-reembed-1ze5853n.js} +14 -16
  9. package/dist/{boot-reembed-2ex2mcsh.js → boot-reembed-fmp5ww69.js} +15 -16
  10. package/dist/boot-scrub-sweep-cbp8xd4e.js +212 -0
  11. package/dist/{boot-seeds-2bmnh18p.js → boot-seeds-22tx8a53.js} +13 -12
  12. package/dist/{claude-adapter-jbj1mycz.js → claude-adapter-0jhgeh1d.js} +25 -25
  13. package/dist/{claude-managed-adapter-17ggz09t.js → claude-managed-adapter-tzwxy803.js} +5 -5
  14. package/dist/{claude-managed-setup-ptm1nzm2.js → claude-managed-setup-nrhfx5b0.js} +5 -5
  15. package/dist/{claude-sdk-session-9re1g7r2.js → claude-sdk-session-wh77rh83.js} +2632 -2614
  16. package/dist/{cli-zsh093hk.js → cli-014vns2y.js} +63 -1
  17. package/dist/{cli-qvfk83fy.js → cli-13xwcqvj.js} +2 -2
  18. package/dist/{cli-8gpbwy09.js → cli-1yrcfa5s.js} +5 -5
  19. package/dist/{cli-xtgtyafv.js → cli-577bxkf6.js} +212 -451
  20. package/dist/{cli-ma3zvh39.js → cli-5qkcdv8c.js} +6118 -5874
  21. package/dist/{cli-cwxhbf43.js → cli-5t5my0h8.js} +85 -2
  22. package/dist/{cli-zvckv750.js → cli-69egj654.js} +2 -2
  23. package/dist/{cli-9xb89xca.js → cli-6bpvv8h4.js} +9 -9
  24. package/dist/{cli-90v942q1.js → cli-6qxmjpem.js} +8 -8
  25. package/dist/{cli-9866bd07.js → cli-6vfz44sr.js} +1 -1
  26. package/dist/{cli-32bca7nr.js → cli-7avy4v8w.js} +1 -1
  27. package/dist/{cli-xb3c6aat.js → cli-7mhy3ths.js} +21 -18
  28. package/dist/{cli-q38n4bq0.js → cli-95zs84qw.js} +3 -3
  29. package/dist/cli-a35hzdve.js +33 -0
  30. package/dist/{cli-x3x0qwky.js → cli-bxqez1p0.js} +51 -23
  31. package/dist/{cli-prt9emct.js → cli-c2tpw9ck.js} +3 -3
  32. package/dist/{cli-0n4fasz0.js → cli-c8r99akr.js} +16 -16
  33. package/dist/{cli-5anb0ev3.js → cli-cexctgn5.js} +89 -26
  34. package/dist/{cli-bns5c8k9.js → cli-cf2vgn9d.js} +105 -91
  35. package/dist/{cli-ng97fjq7.js → cli-cmxkbzzs.js} +33 -7
  36. package/dist/{cli-nexwh8zg.js → cli-g7k7zbcz.js} +12 -5
  37. package/dist/{cli-mmzwwptd.js → cli-hf5s9pgf.js} +1 -1
  38. package/dist/{cli-nvvfd8bq.js → cli-jbzbr36n.js} +4091 -3793
  39. package/dist/{cli-zn2v3t8v.js → cli-jvmtxqtc.js} +27 -15
  40. package/dist/{cli-pkke6geq.js → cli-km4x66j0.js} +83 -65
  41. package/dist/cli-kw0jcfje.js +516 -0
  42. package/dist/{cli-28smfvbn.js → cli-m7vmeepg.js} +32 -19
  43. package/dist/{cli-hpryr6em.js → cli-mb75wpxf.js} +1 -1
  44. package/dist/{cli-qdv8kgz7.js → cli-mkdsy1nq.js} +14018 -3333
  45. package/dist/{cli-b0f70z24.js → cli-mn8qkd8v.js} +1 -1
  46. package/dist/{cli-420bdgw0.js → cli-mv2520nm.js} +1 -1
  47. package/dist/{cli-n9ehc28t.js → cli-mxry91sa.js} +31 -10
  48. package/dist/{cli-wfajtmf7.js → cli-mxvnnv5q.js} +596 -13
  49. package/dist/cli-nkqjj42q.js +104 -0
  50. package/dist/{cli-e7t4snpa.js → cli-p8rt8spp.js} +5 -5
  51. package/dist/{cli-gh5yqzpe.js → cli-pkj1w46r.js} +12 -7
  52. package/dist/cli-q1vnz8wv.js +96 -0
  53. package/dist/{cli-fj86xr3c.js → cli-qd97v6z2.js} +2 -2
  54. package/dist/{cli-ba66wyhy.js → cli-qzmzwegq.js} +60 -20
  55. package/dist/{cli-5b2fjjzh.js → cli-r7eeddkq.js} +14 -9
  56. package/dist/{cli-8ssjez7e.js → cli-rhy7z1rp.js} +22 -11
  57. package/dist/{cli-4mevd1hf.js → cli-s9y81s7j.js} +5 -5
  58. package/dist/{cli-59jn5twa.js → cli-tc5zec7v.js} +7 -7
  59. package/dist/{cli-ttbsse3f.js → cli-tmhsxhb8.js} +1 -1
  60. package/dist/{cli-k6xx3pve.js → cli-tx2zvn23.js} +4 -4
  61. package/dist/{cli-pjxnr1jn.js → cli-ty4bzkqr.js} +5 -5
  62. package/dist/{cli-nfygw434.js → cli-v1pwxreq.js} +55 -52
  63. package/dist/{cli-ftry7975.js → cli-v1w7pbd8.js} +3 -2
  64. package/dist/{cli-10zmp1p7.js → cli-vr5avem2.js} +27 -32
  65. package/dist/{cli-bzr0ftht.js → cli-wnb9b2g3.js} +19 -0
  66. package/dist/cli-wt7ccy4r.js +2654 -0
  67. package/dist/{cli-50eg13rj.js → cli-wydz3feg.js} +549 -321
  68. package/dist/{cli-krqm5f11.js → cli-xezn9nkh.js} +16 -13
  69. package/dist/{cli-8kt0bmgx.js → cli-xf713zz0.js} +2 -2
  70. package/dist/{cli-x7afk5wm.js → cli-xvc3q9br.js} +1 -1
  71. package/dist/{cli-9s3h836g.js → cli-xvdgasgy.js} +1 -1
  72. package/dist/{cli-j0pdrmwg.js → cli-y0kjpfm9.js} +3 -3
  73. package/dist/{cli-6zcfw0x7.js → cli-y195hv1a.js} +1 -1
  74. package/dist/{cli-amwhs8h2.js → cli-ykt8f7va.js} +22 -9
  75. package/dist/{cli-1kzfvd1z.js → cli-ymmc1zxg.js} +15 -11
  76. package/dist/{cli-xnzrzets.js → cli-ypgba1a8.js} +2 -2
  77. package/dist/{cli-bdv39kgj.js → cli-z1dqhc9y.js} +31 -20
  78. package/dist/{cli-fw6vdcq5.js → cli-zp67qg5x.js} +34 -1
  79. package/dist/{cli-qbksc292.js → cli-zy39d433.js} +2 -0
  80. package/dist/cli.js +36 -32
  81. package/dist/{cloudflare-workers-ai-system-one-tt484t61.js → cloudflare-workers-ai-system-one-sta45jf9.js} +7 -5
  82. package/dist/{codex-adapter-3qnxshje.js → codex-adapter-cgesnjkr.js} +20 -20
  83. package/dist/{codex-login-brkxhhdg.js → codex-login-g1cyt5zm.js} +2 -2
  84. package/dist/{codex-session-runner-612wnapp.js → codex-session-runner-8kd7ddhr.js} +20 -20
  85. package/dist/{commands-2ybfcxkj.js → commands-h6zaaxym.js} +11 -10
  86. package/dist/{cursor-adapter-p3jgdehv.js → cursor-adapter-v8mm6bp5.js} +7 -7
  87. package/dist/{db-f4ans0jj.js → db-qg3myn53.js} +13 -10
  88. package/dist/{devin-adapter-dhzt01ff.js → devin-adapter-bffjj5n4.js} +2 -2
  89. package/dist/{dispatcher-pk182apd.js → dispatcher-7vnwj9qw.js} +28 -32
  90. package/dist/{dsh-adapter-7twaeps1.js → dsh-adapter-tz6hfp5r.js} +7 -7
  91. package/dist/{e2b-y8t3jzyz.js → e2b-nd50eccg.js} +1 -1
  92. package/dist/{easter-egg-3d-mtpnw06s.js → easter-egg-3d-66zxervv.js} +2 -2
  93. package/dist/{event-bus-w7ac4n3c.js → event-bus-vnfthafg.js} +2 -2
  94. package/dist/{execute-s4ccjq3n.js → execute-h4jgevyw.js} +61 -11
  95. package/dist/{google-generative-ai-d6nphg3q.js → google-generative-ai-b8zxrpax.js} +5 -5
  96. package/dist/{google-vertex-cpshkref.js → google-vertex-nrdd697t.js} +5 -5
  97. package/dist/grok-adapter-jdt2dz2s.js +39 -0
  98. package/dist/{handlers-g2wnr4t9.js → handlers-2qtkpncr.js} +31 -35
  99. package/dist/{hook-f7jt9vnm.js → hook-51yfcgsv.js} +48 -39
  100. package/dist/{hook-bbdpnkb4.js → hook-qnbdgwvd.js} +15 -17
  101. package/dist/{http-w40q4w94.js → http-jsmtag08.js} +536 -231
  102. package/dist/{index-056j5kzn.js → index-2t9kesn1.js} +13 -12
  103. package/dist/{index-cj1r9q83.js → index-bt8wkv1w.js} +29 -33
  104. package/dist/{index-7n70jwbb.js → index-cx1sb9bq.js} +42 -35
  105. package/dist/{index-9bfssf54.js → index-ekywyv2v.js} +28 -32
  106. package/dist/{index-fdk1t4d6.js → index-n0rxa4ca.js} +11962 -9900
  107. package/dist/{index-k8xc083e.js → index-t4g06w93.js} +28 -32
  108. package/dist/{keepalive-1tj10tf4.js → keepalive-4aezx4bj.js} +16 -12
  109. package/dist/{lead-st0bg7v4.js → lead-r44bkpm5.js} +52 -56
  110. package/dist/{llama-cpp-classify-tgj35k8d.js → llama-cpp-classify-6erb7wwv.js} +3 -1
  111. package/dist/{maintenance-nz71zats.js → maintenance-grz5jrzh.js} +12 -13
  112. package/dist/{mistral-conversations-8445ddjn.js → mistral-conversations-059y51zt.js} +4 -4
  113. package/dist/{node-f0ykebbv.js → node-qm9eb6z0.js} +25 -7
  114. package/dist/{oauth-refresh-sweep-aefzwvvq.js → oauth-refresh-sweep-wm4ctjfj.js} +15 -11
  115. package/dist/{onboard-kgpr9mbn.js → onboard-v60zgqf0.js} +2 -2
  116. package/dist/{openai-codex-responses-9ynm88zy.js → openai-codex-responses-jzxkta9b.js} +8 -7
  117. package/dist/{openai-completions-nhqwjmaw.js → openai-completions-663yncfq.js} +4 -4
  118. package/dist/openai-decisions-vj9nkpf8.js +162 -0
  119. package/dist/{openai-responses-xx1c4kgm.js → openai-responses-m3g43rrt.js} +5 -5
  120. package/dist/{opencode-adapter-0c8e70dk.js → opencode-adapter-yjyqynzf.js} +8 -8
  121. package/dist/{openrouter-images-8yx3487g.js → openrouter-images-kr60n6b9.js} +1 -1
  122. package/dist/{otel-impl-x8zp4y5a.js → otel-impl-ghh4dyf8.js} +2 -2
  123. package/dist/{pi-messages-vxdt8v7j.js → pi-messages-xszk613n.js} +1 -1
  124. package/dist/{pi-mono-adapter-t280ah5c.js → pi-mono-adapter-0eg8kw2c.js} +69 -52
  125. package/dist/{pricing-refresh-9e0q7cwk.js → pricing-refresh-mjrqvnvw.js} +12 -11
  126. package/dist/{rbac-roles-790j3ga8.js → rbac-roles-axv4d74x.js} +11 -10
  127. package/dist/{rbac-roles-kcp6zge2.js → rbac-roles-fd5tskj9.js} +11 -10
  128. package/dist/{render-v2-tch4nefj.js → render-v2-k50j33qd.js} +11 -11
  129. package/dist/{runtime-v2t1r79e.js → runtime-h9ha1mq6.js} +4 -4
  130. package/dist/{setup-rcm303ns.js → setup-4syh0pd2.js} +2 -2
  131. package/dist/{typesafe-system-one-datfr82p.js → typesafe-system-one-e988vty7.js} +7 -5
  132. package/dist/{virtual-modules-wm8mxdxx.js → virtual-modules-ftr9kwe8.js} +11 -11
  133. package/dist/{work-objects-ctg7t70w.js → work-objects-jcdbg8rr.js} +32 -36
  134. package/dist/{worker-5dp0xm55.js → worker-x8j8c9sx.js} +52 -56
  135. package/dist/{x-v3yw66zs.js → x-ma4gnfgy.js} +2 -2
  136. package/openapi.json +5 -2
  137. package/package.json +15 -11
  138. package/src/be/automation-preflight.ts +9 -4
  139. package/src/be/boot-scrub-sweep.ts +302 -0
  140. package/src/be/crypto/key-bootstrap.ts +1 -1
  141. package/src/be/db/approvals.ts +15 -1
  142. package/src/be/db/codex-oauth-device-flows.ts +40 -0
  143. package/src/be/db/config-secrets.ts +31 -0
  144. package/src/be/db/runtime.ts +4 -0
  145. package/src/be/db/script-runs.ts +15 -0
  146. package/src/be/db/tasks/write.ts +1 -1
  147. package/src/be/db-queries/mcp-oauth.ts +5 -0
  148. package/src/be/db-queries/oauth.ts +77 -0
  149. package/src/be/db.ts +162 -59
  150. package/src/be/events.ts +17 -1
  151. package/src/be/memory/index-content.ts +2 -2
  152. package/src/be/memory/providers/openai-embedding.ts +7 -2
  153. package/src/be/memory/providers/sqlite-store.ts +63 -11
  154. package/src/be/memory/raters/llm-summarizer.ts +7 -2
  155. package/src/be/memory/types.ts +14 -0
  156. package/src/be/migrations/201_workflow_replay_columns.sql +10 -0
  157. package/src/be/migrations/202_codex_oauth_device_flows.sql +24 -0
  158. package/src/be/modelsdev-cache.json +61 -0
  159. package/src/be/scripts/db.ts +28 -4
  160. package/src/be/scripts/source-secrets.ts +37 -0
  161. package/src/be/scrub-json.ts +32 -0
  162. package/src/be/sealed-json.ts +59 -0
  163. package/src/be/secret-registry.ts +131 -0
  164. package/src/be/seed-pricing.ts +84 -53
  165. package/src/be/swarm-config-guard.ts +13 -0
  166. package/src/be/workflow-replay.ts +77 -0
  167. package/src/commands/provider-credentials.ts +28 -5
  168. package/src/commands/runner.ts +7 -12
  169. package/src/heartbeat/heartbeat.ts +91 -19
  170. package/src/hooks/hook.ts +72 -27
  171. package/src/http/codex-oauth-device.ts +8 -10
  172. package/src/http/db-query-bounded.ts +3 -0
  173. package/src/http/db-query-shared.ts +107 -15
  174. package/src/http/db-query.ts +10 -2
  175. package/src/http/index.ts +23 -6
  176. package/src/http/mcp.ts +5 -0
  177. package/src/http/onboarding.ts +2 -0
  178. package/src/http/script-runs.ts +9 -1
  179. package/src/http/scripts.ts +15 -0
  180. package/src/http/workflows.ts +3 -2
  181. package/src/oauth/mcp-wrapper.ts +1 -1
  182. package/src/providers/acp-adapter.ts +25 -11
  183. package/src/providers/acp-target-catalog.ts +35 -1
  184. package/src/providers/acp-targets.ts +78 -7
  185. package/src/providers/amp-adapter.ts +1 -1
  186. package/src/providers/claude-adapter.ts +26 -0
  187. package/src/providers/codex-adapter.ts +30 -3
  188. package/src/providers/codex-oauth/auth-json.ts +21 -4
  189. package/src/providers/codex-oauth/types.ts +2 -0
  190. package/src/providers/cursor-adapter.ts +1 -1
  191. package/src/providers/dsh-adapter.ts +10 -1
  192. package/src/providers/grok-adapter.ts +18 -2
  193. package/src/providers/opencode-adapter.ts +21 -5
  194. package/src/providers/pi-mono-extension.ts +56 -25
  195. package/src/providers/pricing-sources.md +2 -0
  196. package/src/script-workflows/supervisor.ts +11 -2
  197. package/src/scripts-runtime/eval-harness.ts +2 -12
  198. package/src/scripts-runtime/executors/quickjs-bundle.ts +152 -0
  199. package/src/scripts-runtime/executors/quickjs-prelude.ts +285 -0
  200. package/src/scripts-runtime/executors/quickjs-runner.ts +580 -0
  201. package/src/scripts-runtime/executors/quickjs-worker.ts +81 -0
  202. package/src/scripts-runtime/executors/quickjs.ts +196 -0
  203. package/src/scripts-runtime/executors/registry.ts +2 -0
  204. package/src/scripts-runtime/executors/sourcemap.ts +79 -0
  205. package/src/scripts-runtime/script-error-hints.ts +14 -0
  206. package/src/server.ts +66 -4
  207. package/src/tests/acp-adapter.test.ts +277 -12
  208. package/src/tests/boot-scrub-sweep.test.ts +390 -0
  209. package/src/tests/check-text-columns.test.ts +145 -0
  210. package/src/tests/ci-unit-test-report.test.ts +73 -0
  211. package/src/tests/claude-default-route.test.ts +75 -0
  212. package/src/tests/claude-hook-output.test.ts +233 -0
  213. package/src/tests/claude-sdk-process.fixture.ts +13 -2
  214. package/src/tests/codex-adapter-pool-auth-revalidate.test.ts +4 -2
  215. package/src/tests/codex-adapter.test.ts +166 -26
  216. package/src/tests/codex-oauth-auth-timestamps.test.ts +76 -0
  217. package/src/tests/codex-oauth-device.test.ts +23 -37
  218. package/src/tests/codex-oauth.test.ts +4 -1
  219. package/src/tests/context-window.test.ts +2 -2
  220. package/src/tests/credential-check.test.ts +51 -1
  221. package/src/tests/db-query-denylist.test.ts +202 -0
  222. package/src/tests/db-task-read-contract.test.ts +1 -0
  223. package/src/tests/db-task-write-contract.test.ts +1 -0
  224. package/src/tests/dsh-adapter.test.ts +23 -1
  225. package/src/tests/enabled-tools.test.ts +269 -0
  226. package/src/tests/entrypoint-acp-target-binary.test.ts +31 -1
  227. package/src/tests/entrypoint-codex-oauth-seed.test.ts +12 -0
  228. package/src/tests/events-scrub.test.ts +88 -0
  229. package/src/tests/fixtures/secret-corpus.ts +320 -0
  230. package/src/tests/grok-adapter.test.ts +13 -0
  231. package/src/tests/haiku-5-5-pricing.test.ts +38 -0
  232. package/src/tests/heartbeat-checklist.test.ts +158 -1
  233. package/src/tests/onboarding.test.ts +44 -0
  234. package/src/tests/opencode-adapter.test.ts +66 -3
  235. package/src/tests/opencode-plugin.test.ts +37 -0
  236. package/src/tests/openrouter-attribution-call-sites.test.ts +204 -0
  237. package/src/tests/openrouter-base-url.test.ts +30 -39
  238. package/src/tests/pi-mono-extension.test.ts +161 -2
  239. package/src/tests/pricing-refresh.test.ts +70 -1
  240. package/src/tests/reasoning-effort.test.ts +3 -1
  241. package/src/tests/script-connections.test.ts +5 -0
  242. package/src/tests/script-executor-conformance.test.ts +3 -0
  243. package/src/tests/script-executor-quickjs.test.ts +442 -0
  244. package/src/tests/script-executor-registry.test.ts +6 -1
  245. package/src/tests/script-runs-http.test.ts +88 -1
  246. package/src/tests/script-runs-scrub.test.ts +275 -0
  247. package/src/tests/scripts-http.test.ts +82 -1
  248. package/src/tests/scrubbed-text-12e.test.ts +289 -0
  249. package/src/tests/sealed-key-guard.test.ts +166 -0
  250. package/src/tests/secret-canary-sweep.test.ts +958 -0
  251. package/src/tests/secret-registry.test.ts +355 -0
  252. package/src/tests/secret-rules.test.ts +230 -0
  253. package/src/tests/secret-scrubber.test.ts +3 -2
  254. package/src/tests/telemetry-trigger.test.ts +12 -1
  255. package/src/tests/workflow-llm.test.ts +14 -0
  256. package/src/tests/workflow-replay.test.ts +518 -0
  257. package/src/tests/workflow-scrub.test.ts +242 -0
  258. package/src/tests/workflow-system-one-decision-providers.test.ts +33 -0
  259. package/src/tools/db-query.ts +1 -1
  260. package/src/tools/inject-learning.ts +1 -1
  261. package/src/tools/request-human-input.ts +7 -2
  262. package/src/tools/workflows/get-workflow-run.ts +3 -2
  263. package/src/types.ts +1 -1
  264. package/src/utils/enabled-tools.ts +27 -0
  265. package/src/utils/internal-ai/complete-structured.ts +11 -3
  266. package/src/utils/openrouter-base-url.ts +23 -0
  267. package/src/utils/secret-rules.generated.ts +2055 -0
  268. package/src/utils/secret-scrubber.ts +363 -29
  269. package/src/workflows/engine.ts +39 -4
  270. package/src/workflows/executors/raw-llm.ts +1 -0
  271. package/src/workflows/executors/system-one-decision.ts +2 -0
  272. package/src/workflows/executors/validate.ts +1 -0
  273. package/src/workflows/executors/workflow-llm.ts +9 -2
  274. package/src/workflows/recovery.ts +10 -1
  275. package/src/workflows/resume.ts +62 -3
  276. package/src/workflows/retry-poller.ts +3 -0
  277. package/templates/skills/agent-browser/SKILL.md +2 -0
  278. package/templates/skills/agent-browser/content.md +2 -0
  279. package/templates/skills/code-quality/content.md +1 -0
  280. package/templates/skills/heartbeat-runbook/content.md +1 -1
  281. package/templates/skills/work-on-task/SKILL.md +4 -0
  282. package/templates/skills/work-on-task/content.md +4 -0
  283. package/templates/skills/workflow-iterate/content.md +9 -0
  284. package/dist/acp-adapter-w6zws6me.js +0 -23
  285. package/dist/boot-scrub-logs-246yn076.js +0 -99
  286. package/dist/cli-3a4m1aws.js +0 -572
  287. package/dist/cli-3dpj7atn.js +0 -11
  288. package/dist/cli-a87b04k8.js +0 -951
  289. package/dist/cli-cvk6yzw5.js +0 -40
  290. package/dist/cli-gvcgmx69.js +0 -183
  291. package/dist/cli-h1spv8v4.js +0 -427
  292. package/dist/cli-jqt9hrnz.js +0 -168
  293. package/dist/cli-yhmwmv0g.js +0 -9281
  294. package/dist/grok-adapter-yt9pab3k.js +0 -29
  295. package/src/be/boot-scrub-logs.ts +0 -132
  296. package/dist/{codex-hook-d2rkvf39.js → codex-hook-15dgc4cb.js} +1 -1
@@ -183,6 +183,55 @@ function projectCostBlock(
183
183
  return rows;
184
184
  }
185
185
 
186
+ /** A projected row plus the models.dev id it came from, before normalized ids are collapsed. */
187
+ interface SourcedSeedRow {
188
+ sourceId: string;
189
+ row: PricingSeedRow;
190
+ }
191
+
192
+ /** Whether `candidate` beats `current` as the source of a normalized `key`. */
193
+ function outranksSource(candidate: string, current: string, key: string): boolean {
194
+ const a = candidate.toLowerCase();
195
+ const b = current.toLowerCase();
196
+ if ((a === key) !== (b === key)) return a === key;
197
+ return a > b;
198
+ }
199
+
200
+ /**
201
+ * `normalizeModelKey` can fold several models.dev ids onto one key: amp strips
202
+ * OpenAI's dated snapshots, so `gpt-4o`, `gpt-4o-2024-05-13`, `gpt-4o-2024-08-06`
203
+ * and `gpt-4o-2024-11-20` all become `amp|gpt-4o`, at different prices. The
204
+ * refresh writes every changed row at one `effective_from`, so two prices for
205
+ * one (provider, model, tokenClass) hit the pricing primary key and roll back
206
+ * the whole batch.
207
+ *
208
+ * Rule: one source id wins per (provider, normalized model), and all its token
209
+ * classes come from it. The id that is already canonical (the undated alias)
210
+ * wins; failing that, the greatest id, i.e. the newest dated snapshot. A source
211
+ * id repeated across catalog sections keeps its first row per token class.
212
+ */
213
+ function collapseNormalizedIds(sourced: SourcedSeedRow[]): PricingSeedRow[] {
214
+ const modelKey = (row: PricingSeedRow) => `${row.provider}\u0000${row.model}`;
215
+ const winners = new Map<string, string>();
216
+ for (const { sourceId, row } of sourced) {
217
+ const current = winners.get(modelKey(row));
218
+ if (current === undefined || outranksSource(sourceId, current, row.model)) {
219
+ winners.set(modelKey(row), sourceId);
220
+ }
221
+ }
222
+
223
+ const seen = new Set<string>();
224
+ const rows: PricingSeedRow[] = [];
225
+ for (const { sourceId, row } of sourced) {
226
+ if (winners.get(modelKey(row)) !== sourceId) continue;
227
+ const rowKey = `${modelKey(row)}\u0000${row.tokenClass}`;
228
+ if (seen.has(rowKey)) continue;
229
+ seen.add(rowKey);
230
+ rows.push(row);
231
+ }
232
+ return rows;
233
+ }
234
+
186
235
  /**
187
236
  * Build the full set of seed rows from a loaded models.dev cache.
188
237
  *
@@ -191,7 +240,10 @@ function projectCostBlock(
191
240
  * explicit and auditable.
192
241
  */
193
242
  export function buildModelsDevSeedRows(cache: ModelsDevCache): PricingSeedRow[] {
194
- const rows: PricingSeedRow[] = [];
243
+ const sourced: SourcedSeedRow[] = [];
244
+ const add = (sourceId: string, projected: PricingSeedRow[]) => {
245
+ for (const row of projected) sourced.push({ sourceId, row });
246
+ };
195
247
 
196
248
  // ---- Anthropic / claude family ----------------------------------------
197
249
  // The 'claude' provider (local-CLI adapter) reports the model id as the
@@ -202,9 +254,7 @@ export function buildModelsDevSeedRows(cache: ModelsDevCache): PricingSeedRow[]
202
254
  for (const [id, model] of Object.entries(anthropic)) {
203
255
  if (!model?.cost) continue;
204
256
  for (const provider of ["claude", "claude-managed"] as const) {
205
- for (const row of projectCostBlock(provider, id, model.cost)) {
206
- rows.push(row);
207
- }
257
+ add(id, projectCostBlock(provider, id, model.cost));
208
258
  }
209
259
  }
210
260
  // Anthropic shortnames (opus/sonnet/haiku) → resolve to the current default.
@@ -214,9 +264,7 @@ export function buildModelsDevSeedRows(cache: ModelsDevCache): PricingSeedRow[]
214
264
  (cache.anthropic ? ANTHROPIC_SHORTNAME_PRICING_FALLBACKS[fullId] : undefined);
215
265
  if (!cost) continue;
216
266
  for (const provider of ["claude", "claude-managed"] as const) {
217
- for (const row of projectCostBlock(provider, shortname, cost)) {
218
- rows.push(row);
219
- }
267
+ add(shortname, projectCostBlock(provider, shortname, cost));
220
268
  }
221
269
  }
222
270
  // Pi-mono uses anthropic models via OpenRouter mirrors; project those too.
@@ -225,9 +273,7 @@ export function buildModelsDevSeedRows(cache: ModelsDevCache): PricingSeedRow[]
225
273
  anthropic[fullId]?.cost ??
226
274
  (cache.anthropic ? ANTHROPIC_SHORTNAME_PRICING_FALLBACKS[fullId] : undefined);
227
275
  if (!cost) continue;
228
- for (const row of projectCostBlock("pi", shortname, cost, { anthropicBilled: true })) {
229
- rows.push(row);
230
- }
276
+ add(shortname, projectCostBlock("pi", shortname, cost, { anthropicBilled: true }));
231
277
  }
232
278
 
233
279
  // ---- Amp (Anthropic, OpenAI, Google and Fireworks models by id) ---------
@@ -237,9 +283,7 @@ export function buildModelsDevSeedRows(cache: ModelsDevCache): PricingSeedRow[]
237
283
  for (const section of ["anthropic", "openai", "google", "fireworks-ai"]) {
238
284
  for (const [id, model] of Object.entries(cache[section]?.models ?? {})) {
239
285
  if (!model?.cost) continue;
240
- for (const row of projectCostBlock("amp", id, model.cost)) {
241
- rows.push(row);
242
- }
286
+ add(id, projectCostBlock("amp", id, model.cost));
243
287
  }
244
288
  }
245
289
 
@@ -247,16 +291,12 @@ export function buildModelsDevSeedRows(cache: ModelsDevCache): PricingSeedRow[]
247
291
  const openai = cache.openai?.models ?? {};
248
292
  for (const [id, model] of Object.entries(openai)) {
249
293
  if (!model?.cost) continue;
250
- for (const row of projectCostBlock("codex", id, model.cost)) {
251
- rows.push(row);
252
- }
294
+ add(id, projectCostBlock("codex", id, model.cost));
253
295
  // Phase 2 fix — pi-mono can route to openai models through the
254
296
  // github-copilot proxy (`github-copilot/gpt-5.4`). The lookup helper
255
297
  // strips the prefix, so we seed the bare id under `pi` too. Without this
256
298
  // every gh-copilot-backed pi run fell through to `costSource='unpriced'`.
257
- for (const row of projectCostBlock("pi", id, model.cost)) {
258
- rows.push(row);
259
- }
299
+ add(id, projectCostBlock("pi", id, model.cost));
260
300
  }
261
301
 
262
302
  // ---- Cursor (bare vendor ids through Cursor's hosted inference) --------
@@ -267,27 +307,21 @@ export function buildModelsDevSeedRows(cache: ModelsDevCache): PricingSeedRow[]
267
307
  for (const vendor of ["openai", "anthropic", "google", "xai"] as const) {
268
308
  for (const [id, model] of Object.entries(cache[vendor]?.models ?? {})) {
269
309
  if (!model?.cost) continue;
270
- for (const row of projectCostBlock("cursor", id, model.cost, { anthropicBilled: false })) {
271
- rows.push(row);
272
- }
310
+ add(id, projectCostBlock("cursor", id, model.cost, { anthropicBilled: false }));
273
311
  }
274
312
  }
275
313
 
276
314
  // ---- Grok CLI (bare xAI ids, billed at xAI's API rates via XAI_API_KEY) --
277
315
  for (const [id, model] of Object.entries(cache.xai?.models ?? {})) {
278
316
  if (!model?.cost) continue;
279
- for (const row of projectCostBlock("grok", id, model.cost, { anthropicBilled: false })) {
280
- rows.push(row);
281
- }
317
+ add(id, projectCostBlock("grok", id, model.cost, { anthropicBilled: false }));
282
318
  }
283
319
 
284
320
  // ---- DeepSeek direct API (dsh with DEEPSEEK_API_KEY, bare ids) ---------
285
321
  const deepseek = cache.deepseek?.models ?? {};
286
322
  for (const [id, model] of Object.entries(deepseek)) {
287
323
  if (!model?.cost) continue;
288
- for (const row of projectCostBlock("dsh", id, model.cost)) {
289
- rows.push(row);
290
- }
324
+ add(id, projectCostBlock("dsh", id, model.cost));
291
325
  }
292
326
 
293
327
  // ---- OpenRouter passthrough (covers gemini + every opencode-routed model)
@@ -295,48 +329,45 @@ export function buildModelsDevSeedRows(cache: ModelsDevCache): PricingSeedRow[]
295
329
  for (const [id, model] of Object.entries(openrouter)) {
296
330
  if (!model?.cost) continue;
297
331
  // opencode routes whatever model the user picks; we project them all.
298
- for (const row of projectCostBlock("opencode", id, model.cost)) {
299
- rows.push(row);
300
- }
332
+ add(id, projectCostBlock("opencode", id, model.cost));
301
333
  // pi-mono also routes via OpenRouter when only OPENROUTER_API_KEY is set
302
334
  // (see src/providers/pi-mono-adapter.ts). Without this projection, pi runs
303
335
  // against non-anthropic models (e.g. deepseek/deepseek-v4-flash) fall
304
336
  // through to costSource='unpriced' even though the model is in the
305
337
  // models.dev snapshot.
306
- for (const row of projectCostBlock("pi", id, model.cost, {
307
- anthropicBilled: id.startsWith("anthropic/"),
308
- })) {
309
- rows.push(row);
310
- }
338
+ add(
339
+ id,
340
+ projectCostBlock("pi", id, model.cost, {
341
+ anthropicBilled: id.startsWith("anthropic/"),
342
+ }),
343
+ );
311
344
  // dsh routes `openrouter/<id>` models (all four tier defaults) through
312
345
  // OpenRouter, so it bills at the same rates.
313
- for (const row of projectCostBlock("dsh", id, model.cost, {
314
- anthropicBilled: id.startsWith("anthropic/"),
315
- })) {
316
- rows.push(row);
317
- }
346
+ add(
347
+ id,
348
+ projectCostBlock("dsh", id, model.cost, {
349
+ anthropicBilled: id.startsWith("anthropic/"),
350
+ }),
351
+ );
318
352
  // grok runs `openrouter/<id>` models as OpenAI-compatible BYOK models.
319
- for (const row of projectCostBlock("grok", id, model.cost, {
320
- anthropicBilled: id.startsWith("anthropic/"),
321
- })) {
322
- rows.push(row);
323
- }
353
+ add(
354
+ id,
355
+ projectCostBlock("grok", id, model.cost, {
356
+ anthropicBilled: id.startsWith("anthropic/"),
357
+ }),
358
+ );
324
359
  // Gemini specifically: also project under the 'gemini' provider so
325
360
  // internal-ai callers that tag with provider='gemini' find a hit.
326
361
  if (id.startsWith("google/")) {
327
362
  const geminiKey = id.replace(/^google\//, "");
328
- for (const row of projectCostBlock("gemini", geminiKey, model.cost)) {
329
- rows.push(row);
330
- }
363
+ add(geminiKey, projectCostBlock("gemini", geminiKey, model.cost));
331
364
  // Also store under the full openrouter id so the same row resolves
332
365
  // whether the caller passes "google/..." or the stripped name.
333
- for (const row of projectCostBlock("gemini", id, model.cost)) {
334
- rows.push(row);
335
- }
366
+ add(id, projectCostBlock("gemini", id, model.cost));
336
367
  }
337
368
  }
338
369
 
339
- return rows;
370
+ return collapseNormalizedIds(sourced);
340
371
  }
341
372
 
342
373
  /** Build the exact provider rows inserted by the boot-time pricing seeder. */
@@ -1,6 +1,7 @@
1
1
  import { normalizeSlackReactionShortcode } from "../slack/reaction-shortcode";
2
2
  import { ProviderNameSchema } from "../types";
3
3
  import { API_DRAIN_MAX_MS_LIMIT } from "../utils/api-drain";
4
+ import { parseEnabledTools } from "../utils/enabled-tools";
4
5
  import { parseTaskToolManifest } from "../utils/task-tool-manifest";
5
6
  import { isTierConfigKey, validateTierConfigValue } from "./model-tier-keys";
6
7
 
@@ -212,6 +213,17 @@ const VALIDATED_KEYS: Record<string, ConfigValidator> = {
212
213
  return "Invalid TASK_TOOL_MANIFESTS (expected JSON with taskTypes and/or schedules maps, each selecting at most 16 known swarm tools)";
213
214
  }
214
215
  },
216
+ // MCP tool allowlist: comma-separated names or a JSON array of strings.
217
+ // Unknown names are accepted here and warned about at session init.
218
+ SWARM_ENABLED_TOOLS: (value) => {
219
+ try {
220
+ if (typeof value !== "string") throw new Error("Expected string");
221
+ parseEnabledTools(value);
222
+ return null;
223
+ } catch {
224
+ return "Invalid SWARM_ENABLED_TOOLS (expected comma-separated tool names or a JSON array of strings)";
225
+ }
226
+ },
215
227
  FEEDBACK_ENDPOINT: (value) => {
216
228
  if (typeof value !== "string") {
217
229
  return "Invalid FEEDBACK_ENDPOINT (must use HTTPS, or HTTP on a loopback host)";
@@ -304,6 +316,7 @@ const VALIDATED_KEYS: Record<string, ConfigValidator> = {
304
316
  "PI_TOOL_DEFERRAL",
305
317
  "PI_CODEMODE",
306
318
  "PI_CODEMODE_MODELS",
319
+ "OPENROUTER_APP_ATTRIBUTION",
307
320
  "CURSOR_NATIVE_SYSTEM_PROMPT",
308
321
  "SLACK_DISABLE",
309
322
  "SLACK_RENDER_V2",
@@ -0,0 +1,77 @@
1
+ import { openSealedJson } from "./sealed-json";
2
+
3
+ /**
4
+ * Which copy of a workflow payload a reader gets.
5
+ *
6
+ * - `replay`: the exact value from the sealed `*_replay` column. Resume, retry,
7
+ * dedup and recovery rebuild the live ctx from it, so a downstream consumer
8
+ * sees the same bytes after a restart as in the uninterrupted run.
9
+ * - `display`: the scrubbed column, for API responses, MCP tools and the UI.
10
+ */
11
+ export type WorkflowPayloadView = "replay" | "display";
12
+
13
+ /**
14
+ * A sealed replay copy exists but cannot be opened (missing or rotated
15
+ * encryption key, corrupt ciphertext). The scrubbed copy holds redaction
16
+ * markers, so replaying it would silently feed them to downstream nodes: the
17
+ * run fails with this error instead.
18
+ */
19
+ export class WorkflowReplayStateError extends Error {
20
+ constructor(
21
+ readonly runId: string,
22
+ readonly target: string,
23
+ cause: unknown,
24
+ ) {
25
+ super(
26
+ `Workflow run ${runId} cannot replay ${target}: its sealed replay copy could not be opened (${
27
+ cause instanceof Error ? cause.message : String(cause)
28
+ }). The redacted copy is never replayed. Restore the SECRETS_ENCRYPTION_KEY that sealed it, then retry the run.`,
29
+ );
30
+ this.name = "WorkflowReplayStateError";
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Set `target[key]` from a workflow payload pair.
36
+ *
37
+ * `display` parses the scrubbed column. `replay` opens the sealed column
38
+ * lazily, on first read of the property: status-only readers (cancel, polls,
39
+ * cooldown) never decrypt and keep working without the key. Rows written before
40
+ * the replay columns existed have no sealed copy and fall back to the plain
41
+ * column.
42
+ */
43
+ export function defineWorkflowPayload<T extends object>(
44
+ target: T,
45
+ key: keyof T & string,
46
+ plain: string | null | undefined,
47
+ sealed: string | null | undefined,
48
+ view: WorkflowPayloadView,
49
+ where: { runId: string; target: string },
50
+ ): T {
51
+ if (view === "display" || !sealed) {
52
+ (target as Record<string, unknown>)[key] = plain ? JSON.parse(plain) : undefined;
53
+ return target;
54
+ }
55
+ let opened = false;
56
+ let value: unknown;
57
+ Object.defineProperty(target, key, {
58
+ enumerable: true,
59
+ configurable: true,
60
+ get() {
61
+ if (!opened) {
62
+ try {
63
+ value = openSealedJson(sealed);
64
+ } catch (err) {
65
+ throw new WorkflowReplayStateError(where.runId, where.target, err);
66
+ }
67
+ opened = true;
68
+ }
69
+ return value;
70
+ },
71
+ set(next: unknown) {
72
+ value = next;
73
+ opened = true;
74
+ },
75
+ });
76
+ return target;
77
+ }
@@ -19,6 +19,7 @@
19
19
  */
20
20
 
21
21
  import { deriveDefaultRoute, validateRoute } from "@desplega/model-routing";
22
+ import { ACP_TARGET_IDS, isAcpTarget } from "../providers/acp-target-catalog";
22
23
  import { checkAmpCredentials, liveTestAmpCredentials } from "../providers/amp-adapter";
23
24
  import { checkClaudeCredentials } from "../providers/claude-adapter";
24
25
  import { checkClaudeManagedCredentials } from "../providers/claude-managed-adapter";
@@ -38,7 +39,10 @@ import type {
38
39
  ReasoningEffort,
39
40
  } from "../types";
40
41
  import { CLAUDE_CREDENTIALS_HINT } from "../utils/credentials";
41
- import { getOpenRouterBaseUrl } from "../utils/openrouter-base-url";
42
+ import {
43
+ getOpenRouterAttributionHeaders,
44
+ getOpenRouterBaseUrl,
45
+ } from "../utils/openrouter-base-url";
42
46
  import { scrubSecrets } from "../utils/secret-scrubber";
43
47
 
44
48
  export type SupportedProvider =
@@ -114,7 +118,7 @@ export const REQUIRED_CRED_VARS_BY_PROVIDER: Record<SupportedProvider, readonly
114
118
  devin: ["DEVIN_API_KEY", "DEVIN_ORG_ID"],
115
119
  opencode: ["OPENROUTER_API_KEY", "ANTHROPIC_API_KEY", "OPENAI_API_KEY"],
116
120
  pi: ["ANTHROPIC_API_KEY", "OPENROUTER_API_KEY", "OPENAI_API_KEY"],
117
- // The ACP target process owns its own auth, so the swarm requires nothing.
121
+ // The ACP target owns its auth; custom targets still need a command.
118
122
  acp: [],
119
123
  dsh: ["DEEPSEEK_API_KEY", "OPENROUTER_API_KEY"],
120
124
  amp: ["AMP_API_KEY"],
@@ -142,7 +146,25 @@ export const CREDENTIAL_PROVIDER_CHECKERS: Record<SupportedProvider, CredentialC
142
146
  const { checkPiMonoCredentials } = await import("../providers/pi-mono-adapter");
143
147
  return checkPiMonoCredentials(env, opts);
144
148
  },
145
- acp: () => ({ ready: true, missing: [], satisfiedBy: "sdk-delegated" }),
149
+ acp: (env) => {
150
+ const target = env.ACP_TARGET ?? "custom";
151
+ if (!isAcpTarget(target)) {
152
+ return {
153
+ ready: false,
154
+ missing: ["ACP_TARGET"],
155
+ hint: `Set ACP_TARGET to one of: ${ACP_TARGET_IDS.join(", ")}.`,
156
+ };
157
+ }
158
+ const command = env.ACP_TARGET_COMMAND ?? env.ACP_COMMAND;
159
+ if (target === "custom" && !command?.trim()) {
160
+ return {
161
+ ready: false,
162
+ missing: ["ACP_TARGET_COMMAND"],
163
+ hint: "Set ACP_TARGET_COMMAND (or ACP_COMMAND) to an ACP-compatible executable, or select a preset with ACP_TARGET.",
164
+ };
165
+ }
166
+ return { ready: true, missing: [], satisfiedBy: "sdk-delegated" };
167
+ },
146
168
  };
147
169
 
148
170
  /**
@@ -288,9 +310,10 @@ async function checkXaiApiKey(apiKey: string): Promise<LiveValidationResult> {
288
310
  }
289
311
 
290
312
  async function checkOpenRouter(apiKey: string): Promise<LiveValidationResult> {
291
- const r = await timedFetch(`${getOpenRouterBaseUrl()}/models`, {
313
+ const url = `${getOpenRouterBaseUrl()}/models`;
314
+ const r = await timedFetch(url, {
292
315
  method: "GET",
293
- headers: { Authorization: `Bearer ${apiKey}` },
316
+ headers: { ...getOpenRouterAttributionHeaders(url), Authorization: `Bearer ${apiKey}` },
294
317
  });
295
318
  if (r.ok) return { ok: true, latency_ms: r.latency_ms };
296
319
  return {
@@ -21,7 +21,10 @@ import {
21
21
  import { type RelevantMemory, renderMemoriesPrompt } from "../prompts/memories.ts";
22
22
  import { configureHttpResolver, resolveTemplateAsync } from "../prompts/resolver.ts";
23
23
  import { renderSteeringDelivery } from "../prompts/steering-delivery.ts";
24
- import { authJsonToCredentialSelection } from "../providers/codex-oauth/auth-json.js";
24
+ import {
25
+ authJsonToCredentialSelection,
26
+ credentialsToAuthJson,
27
+ } from "../providers/codex-oauth/auth-json.js";
25
28
  import { materializeCodexAuthJson } from "../providers/codex-oauth/auth-json-fs.js";
26
29
  import {
27
30
  CODEX_OAUTH_POOL_SLOTS_ENV,
@@ -1937,17 +1940,9 @@ export async function resolveCodexOAuthCredentialInfo(
1937
1940
  await materializeCodexAuthJson(selectedSlot, slotEntry.creds, {
1938
1941
  includeRefreshToken: false,
1939
1942
  });
1940
- const authJson = {
1941
- auth_mode: "chatgpt" as const,
1942
- OPENAI_API_KEY: null,
1943
- tokens: {
1944
- id_token: slotEntry.creds.access,
1945
- access_token: slotEntry.creds.access,
1946
- refresh_token: slotEntry.creds.refresh,
1947
- account_id: slotEntry.creds.accountId,
1948
- },
1949
- last_refresh: new Date(slotEntry.creds.expires).toISOString(),
1950
- };
1943
+ const authJson = credentialsToAuthJson(slotEntry.creds, {
1944
+ includeRefreshToken: false,
1945
+ });
1951
1946
  const sel = {
1952
1947
  ...authJsonToCredentialSelection(authJson, selectedSlot, slots.length),
1953
1948
  authFailureFence,
@@ -1,3 +1,4 @@
1
+ import pkg from "../../package.json";
1
2
  import {
2
3
  autoCancelStaleApprovalRequests,
3
4
  timeoutExpiredApprovalRequests,
@@ -247,6 +248,15 @@ function isHeartbeatChecklistDisabled(): boolean {
247
248
  return isEnvFlagEnabled("HEARTBEAT_CHECKLIST_DISABLE", false);
248
249
  }
249
250
 
251
+ /**
252
+ * Whether to create the boot-triage task even when the boot is clean (no
253
+ * interrupted, stalled, orphaned, or resume-less superseded work). Off by
254
+ * default: a clean boot only logs a one-line note.
255
+ */
256
+ function isBootTriageAlways(): boolean {
257
+ return isEnvFlagEnabled("HEARTBEAT_BOOT_TRIAGE_ALWAYS", false);
258
+ }
259
+
250
260
  // ============================================================================
251
261
  // Types
252
262
  // ============================================================================
@@ -1576,24 +1586,7 @@ export async function gatherSystemStatus(options?: { isBootTriage?: boolean }):
1576
1586
  }
1577
1587
 
1578
1588
  // Orphaned pending/offered tasks (assigned to workers with no active session)
1579
- const orphanedTasks: AgentTask[] = [];
1580
-
1581
- for (const status of ["pending", "offered"] as const) {
1582
- const tasks = await getTasksByStatus(status);
1583
-
1584
- for (const task of tasks) {
1585
- // 'pending' tasks carry their holder in agentId; 'offered' tasks have
1586
- // not been accepted yet, so agentId is still NULL and the offeree
1587
- // lives in offeredTo instead (#1190) — `!task.agentId` alone used to
1588
- // skip every offered row here, hiding this whole class of orphan.
1589
- const holderId = status === "offered" ? task.offeredTo : task.agentId;
1590
- if (!holderId) continue;
1591
- const agent = agents.find((a) => a.id === holderId);
1592
- if (!agent || agent.status === "offline") {
1593
- orphanedTasks.push(task);
1594
- }
1595
- }
1596
- }
1589
+ const orphanedTasks = await getOrphanedPendingOrOfferedTasks(agents);
1597
1590
 
1598
1591
  if (orphanedTasks.length > 0) {
1599
1592
  sections.push("");
@@ -1617,6 +1610,60 @@ export async function gatherSystemStatus(options?: { isBootTriage?: boolean }):
1617
1610
  return sections.join("\n");
1618
1611
  }
1619
1612
 
1613
+ /** Pending/offered tasks whose holder is offline or no longer registered. */
1614
+ async function getOrphanedPendingOrOfferedTasks(
1615
+ agents: Awaited<ReturnType<typeof getAllAgents>>,
1616
+ ): Promise<AgentTask[]> {
1617
+ const orphanedTasks: AgentTask[] = [];
1618
+
1619
+ for (const status of ["pending", "offered"] as const) {
1620
+ const tasks = await getTasksByStatus(status);
1621
+
1622
+ for (const task of tasks) {
1623
+ // 'pending' tasks carry their holder in agentId; 'offered' tasks have
1624
+ // not been accepted yet, so agentId is still NULL and the offeree
1625
+ // lives in offeredTo instead (#1190) — `!task.agentId` alone used to
1626
+ // skip every offered row here, hiding this whole class of orphan.
1627
+ const holderId = status === "offered" ? task.offeredTo : task.agentId;
1628
+ if (!holderId) continue;
1629
+ const agent = agents.find((a) => a.id === holderId);
1630
+ if (!agent || agent.status === "offline") {
1631
+ orphanedTasks.push(task);
1632
+ }
1633
+ }
1634
+ }
1635
+
1636
+ return orphanedTasks;
1637
+ }
1638
+
1639
+ export interface BootTriageFindings {
1640
+ rebootInterrupted: number;
1641
+ stalled: number;
1642
+ orphaned: number;
1643
+ supersededWithoutResume: number;
1644
+ }
1645
+
1646
+ /**
1647
+ * Count the boot signals the Lead would have to act on. All zero means a
1648
+ * clean boot: nothing was interrupted, stuck, orphaned, or left without a
1649
+ * resume, so a Lead task would only echo the deploy.
1650
+ */
1651
+ export async function getBootTriageFindings(): Promise<BootTriageFindings> {
1652
+ const now = Date.now();
1653
+ const agents = await getAllAgents();
1654
+ return {
1655
+ rebootInterrupted: getRebootAffectedTasks().length,
1656
+ stalled: (await getStalledInProgressTasks(stallThresholdMinutes())).length,
1657
+ orphaned: (await getOrphanedPendingOrOfferedTasks(agents)).length,
1658
+ supersededWithoutResume: (
1659
+ await getSupersededTasksWithoutResume(
1660
+ new Date(now - ORPHAN_SUPERSEDE_MIN_AGE_MS).toISOString(),
1661
+ new Date(now - ORPHAN_SUPERSEDE_MAX_AGE_MS).toISOString(),
1662
+ )
1663
+ ).length,
1664
+ };
1665
+ }
1666
+
1620
1667
  /**
1621
1668
  * Check HEARTBEAT.md content and create a checklist task for the lead if needed.
1622
1669
  */
@@ -1858,6 +1905,8 @@ export function stopHeartbeat(): void {
1858
1905
  /**
1859
1906
  * Create a one-off boot triage task for the lead after a server restart.
1860
1907
  * Uses the same HEARTBEAT.md content but with reboot-specific context prepended.
1908
+ * Skipped on a clean boot (see getBootTriageFindings) unless
1909
+ * HEARTBEAT_BOOT_TRIAGE_ALWAYS is set.
1861
1910
  */
1862
1911
  export async function createBootTriageTask(): Promise<void> {
1863
1912
  const lead = await getLeadAgent();
@@ -1876,7 +1925,30 @@ export async function createBootTriageTask(): Promise<void> {
1876
1925
  );
1877
1926
  if (existing) return;
1878
1927
 
1879
- const systemStatus = await gatherSystemStatus({ isBootTriage: true });
1928
+ if (!isBootTriageAlways()) {
1929
+ // A failed read falls through to the Lead task: an unknown boot is not a
1930
+ // clean one.
1931
+ const findings = await getBootTriageFindings().catch((err) => {
1932
+ console.error(
1933
+ "[Heartbeat] Boot triage findings failed, creating Lead task:",
1934
+ scrubSecrets(err instanceof Error ? err.message : String(err)),
1935
+ );
1936
+ return null;
1937
+ });
1938
+ if (findings && Object.values(findings).every((count) => count === 0)) {
1939
+ console.log(
1940
+ `[Heartbeat] Clean boot (v${pkg.version}): 0 interrupted, stalled, orphaned, or resume-less superseded tasks; no boot-triage task for lead ${lead.name}`,
1941
+ );
1942
+ return;
1943
+ }
1944
+ }
1945
+
1946
+ // A reader that failed above can fail again here; the task must still land.
1947
+ const systemStatus = await gatherSystemStatus({ isBootTriage: true }).catch((err) => {
1948
+ const reason = scrubSecrets(err instanceof Error ? err.message : String(err));
1949
+ console.error("[Heartbeat] Boot triage status gathering failed:", reason);
1950
+ return `## System status unavailable\n\nStatus gathering failed after this boot (${reason}). Investigate the task, agent, and DB state directly.`;
1951
+ });
1880
1952
 
1881
1953
  const result = resolveTemplate("heartbeat.boot-triage", {
1882
1954
  system_status: systemStatus,