@desplega.ai/agent-swarm 1.131.0 → 1.133.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (265) hide show
  1. package/README.md +2 -2
  2. package/dist/{actions-w53mbns9.js → actions-cskpayd7.js} +11 -9
  3. package/dist/{anthropic-messages-rjmd2e7n.js → anthropic-messages-tpxtbx7b.js} +28 -12
  4. package/dist/{app-m9gwqsqv.js → app-ang77nbb.js} +8 -6
  5. package/dist/{assistant-bp65m6f3.js → assistant-b4exebdw.js} +16 -13
  6. package/dist/{azure-openai-responses-c0yjmbfd.js → azure-openai-responses-m3921t4y.js} +8 -8
  7. package/dist/{boot-reembed-62p0at2k.js → boot-reembed-ps2aftpb.js} +9 -7
  8. package/dist/{boot-reembed-xmfp6wdh.js → boot-reembed-znyk3nxb.js} +10 -8
  9. package/dist/{boot-scrub-logs-rr8q2dms.js → boot-scrub-logs-2ax3xgz1.js} +7 -5
  10. package/dist/{claude-adapter-2y47mrkd.js → claude-adapter-3ct8kgxw.js} +13 -9
  11. package/dist/{claude-managed-adapter-gv19tdew.js → claude-managed-adapter-akx4ntf2.js} +2 -2
  12. package/dist/{cli-t1xze5qh.js → cli-13w2hwkh.js} +1 -1
  13. package/dist/{cli-efxfnq6q.js → cli-1532szb1.js} +8 -6
  14. package/dist/{cli-d2tfkcga.js → cli-23d3rseb.js} +33 -17
  15. package/dist/{cli-nm5gtp1x.js → cli-2a75z9c5.js} +196 -7
  16. package/dist/{cli-fywms69x.js → cli-2t30rj0d.js} +1 -1
  17. package/dist/{cli-cvgjwnh1.js → cli-3c3tdgab.js} +10 -10
  18. package/dist/{cli-pm6xbh84.js → cli-3zzxa9jz.js} +46 -16
  19. package/dist/{cli-cdeg08wx.js → cli-4k6233gc.js} +1 -1
  20. package/dist/{cli-t55yw3a1.js → cli-5p2tz05r.js} +2 -2
  21. package/dist/{cli-0rcyq6d9.js → cli-5z2yhzvd.js} +125 -47
  22. package/dist/{cli-3kf51j15.js → cli-6zfq31nj.js} +9 -3
  23. package/dist/{cli-er3a4bb0.js → cli-8m9de10a.js} +6 -1
  24. package/dist/{cli-gm9cbhbq.js → cli-8t14vg9w.js} +27 -8
  25. package/dist/{cli-72q8m8gk.js → cli-9nfc2es8.js} +6 -6
  26. package/dist/{cli-5ze9rv97.js → cli-9ta3pwms.js} +11 -4
  27. package/dist/{cli-78xwvkhj.js → cli-avr0ns7z.js} +2 -2
  28. package/dist/{cli-n6h8qt8n.js → cli-c897wyhw.js} +12 -7
  29. package/dist/{cli-2q72qz2s.js → cli-dgk20ft7.js} +15 -6
  30. package/dist/cli-et1bbwqf.js +13 -0
  31. package/dist/{cli-bqw0mt1n.js → cli-f5w27pj3.js} +3 -3
  32. package/dist/{cli-2wadx7p2.js → cli-f8dre4bz.js} +2 -2
  33. package/dist/{cli-e7ckcg4v.js → cli-ftwpv451.js} +170 -387
  34. package/dist/{cli-g6b8x1h8.js → cli-g854s2dw.js} +1 -1
  35. package/dist/{cli-5kx36evn.js → cli-ggmdxeqa.js} +13 -8
  36. package/dist/cli-hkwfe3qj.js +530 -0
  37. package/dist/{cli-5dsbcjjw.js → cli-jqwnqx9m.js} +1 -1
  38. package/dist/{cli-9dr5qzc0.js → cli-ny122vh4.js} +21 -10
  39. package/dist/{cli-5v4rw6yd.js → cli-pvzqhbs9.js} +2 -2
  40. package/dist/cli-pzq5rjym.js +15 -0
  41. package/dist/{cli-xpxde7rm.js → cli-qes8ef67.js} +48 -30
  42. package/dist/{cli-sajgpkqg.js → cli-qs9hhsvb.js} +1723 -325
  43. package/dist/cli-qys3rhhz.js +27 -0
  44. package/dist/{cli-rzbpmxdr.js → cli-r6tpm37h.js} +2 -2
  45. package/dist/{cli-qv61hp37.js → cli-rch80c76.js} +2 -2
  46. package/dist/{cli-0258203w.js → cli-rpp0x4zy.js} +137 -17
  47. package/dist/{cli-5n6e5yfn.js → cli-s1k0wxhe.js} +12 -7
  48. package/dist/{cli-psh3c4va.js → cli-sfd9v7sj.js} +1 -1
  49. package/dist/{cli-70xqj0z3.js → cli-sh3ytsyj.js} +49 -39
  50. package/dist/{cli-zsspvvm2.js → cli-w1mp9jy7.js} +1 -1
  51. package/dist/{cli-d3x3x1c9.js → cli-xdb4mpnp.js} +2 -2
  52. package/dist/{cli-57khth04.js → cli-xmyqv00z.js} +120 -4
  53. package/dist/{cli-yw8vghx2.js → cli-xv8t8p3c.js} +65 -32
  54. package/dist/{cli-f1am9szv.js → cli-xxv5pn4f.js} +1 -1
  55. package/dist/{cli-t76vw5db.js → cli-y08jvxzx.js} +601 -140
  56. package/dist/{cli-a4jzdpa3.js → cli-zfbf3srj.js} +650 -245
  57. package/dist/{cli-a40rnf71.js → cli-zmq89k34.js} +6 -6
  58. package/dist/{cli-qmdnhpej.js → cli-zr9q011v.js} +23 -13
  59. package/dist/cli.js +22 -22
  60. package/dist/{codex-adapter-d24q75wk.js → codex-adapter-bef93pq4.js} +9 -7
  61. package/dist/{codex-hook-hq54t9ft.js → codex-hook-e8dba65p.js} +3 -2
  62. package/dist/{codex-session-runner-g50vcsex.js → codex-session-runner-5pkxys7x.js} +9 -7
  63. package/dist/{commands-c3nrc5hg.js → commands-e23s1bff.js} +9 -7
  64. package/dist/{db-nt7fg3tw.js → db-m3nabatn.js} +17 -5
  65. package/dist/{google-generative-ai-hysrd8qx.js → google-generative-ai-8maw55xh.js} +11 -8
  66. package/dist/{google-vertex-p5arrga0.js → google-vertex-8vjqmx8g.js} +11 -8
  67. package/dist/{handlers-fkr6swat.js → handlers-akazedvr.js} +16 -13
  68. package/dist/{hook-77wpvnjc.js → hook-ze4cpf0v.js} +55 -41
  69. package/dist/{hook-g019yehy.js → hook-zfqyyenw.js} +10 -7
  70. package/dist/{http-jewj0syk.js → http-sja0n6kw.js} +730 -150
  71. package/dist/index-0gr8vd6k.js +39 -0
  72. package/dist/{index-7n2xpbd4.js → index-354vkhdv.js} +154 -74
  73. package/dist/{index-070v4eyb.js → index-5qthek5t.js} +2 -3
  74. package/dist/{index-hfvggd0z.js → index-95t3cqq5.js} +16 -14
  75. package/dist/{index-9znhar3x.js → index-sea66619.js} +3 -4
  76. package/dist/{index-6gm5he8h.js → index-vpn36mma.js} +15 -13
  77. package/dist/{keepalive-j0fjvqfd.js → keepalive-3sp3meqj.js} +16 -10
  78. package/dist/{lead-1n8x30xe.js → lead-mefeewkf.js} +39 -36
  79. package/dist/{maintenance-gvev11sa.js → maintenance-8tbrbjfg.js} +10 -8
  80. package/dist/mistral-conversations-00zrp7g4.js +717 -0
  81. package/dist/{oauth-refresh-sweep-s9ecsvh0.js → oauth-refresh-sweep-srjwjz6m.js} +8 -6
  82. package/dist/{onboard-pvm5b38w.js → onboard-mt2tgyjg.js} +7 -7
  83. package/dist/{openai-codex-responses-5g27pd6f.js → openai-codex-responses-map32h2s.js} +18 -18
  84. package/dist/{openai-completions-3vkmfz2a.js → openai-completions-w871b1cq.js} +12 -11
  85. package/dist/{openai-responses-dg4t7j7a.js → openai-responses-b0j7xnw8.js} +15 -12
  86. package/dist/{opencode-adapter-1gm38aba.js → opencode-adapter-b39jefcj.js} +5 -3
  87. package/dist/{openrouter-images-g3gm8931.js → openrouter-images-srfkywy0.js} +4 -4
  88. package/dist/{otel-impl-p6jvgd9c.js → otel-impl-z6pxt3j9.js} +224 -9
  89. package/dist/{pi-mono-adapter-t091s27g.js → pi-mono-adapter-btvqmj58.js} +1138 -408
  90. package/dist/{pricing-refresh-k49rcyed.js → pricing-refresh-kyq8cezh.js} +9 -7
  91. package/dist/{rbac-roles-cr80g96t.js → rbac-roles-3ryfrfna.js} +8 -6
  92. package/dist/{rbac-roles-30pspvy7.js → rbac-roles-6qfvbee1.js} +9 -7
  93. package/dist/{render-v2-xm457ng1.js → render-v2-p61ppjey.js} +8 -6
  94. package/dist/{seed-pricing-yhpv35fr.js → seed-pricing-j56bbj4y.js} +8 -6
  95. package/dist/{setup-nytnmwwr.js → setup-tpr6c1kn.js} +2 -2
  96. package/dist/{worker-msytr2cw.js → worker-9w1v0mbb.js} +39 -36
  97. package/openapi.json +542 -16
  98. package/package.json +6 -6
  99. package/src/be/budget-refusal-notify.ts +14 -7
  100. package/src/be/db-queries/task-artifacts.ts +182 -0
  101. package/src/be/db.ts +783 -169
  102. package/src/be/events.ts +5 -0
  103. package/src/be/migrations/130_scratch_script_retention_grace.sql +6 -0
  104. package/src/be/migrations/131_session_logs_created_at_index.sql +2 -0
  105. package/src/be/migrations/132_multi_runtime_instances.sql +44 -0
  106. package/src/be/migrations/133_workflow_run_trigger_type.sql +12 -0
  107. package/src/be/migrations/135_backfill_task_pull_request_attachments.sql +228 -0
  108. package/src/be/migrations/136_task_requester_provenance.sql +37 -0
  109. package/src/be/migrations/137_memory_retrieval_composite_index.sql +1 -0
  110. package/src/be/multi-runtime.ts +430 -0
  111. package/src/be/scripts/db.ts +43 -1
  112. package/src/be/scripts/extract-schema.ts +3 -1
  113. package/src/be/scripts/retention.ts +164 -0
  114. package/src/be/scripts/run-saved.ts +16 -1
  115. package/src/be/scripts/typecheck.ts +3 -2
  116. package/src/be/seed-skills/index.ts +7 -0
  117. package/src/be/steering.ts +1 -1
  118. package/src/be/swarm-config-guard.ts +18 -0
  119. package/src/commands/credential-wait.ts +54 -0
  120. package/src/commands/onboard/steps/core-credentials.tsx +1 -1
  121. package/src/commands/onboard/steps/custom-templates.tsx +1 -1
  122. package/src/commands/onboard/steps/harness-credentials.tsx +1 -1
  123. package/src/commands/onboard/steps/health-check.tsx +1 -1
  124. package/src/commands/onboard/steps/start.tsx +1 -1
  125. package/src/commands/profile-sync-audit.ts +56 -0
  126. package/src/commands/profile-sync.ts +287 -15
  127. package/src/commands/provider-credentials.ts +46 -19
  128. package/src/commands/runner.ts +196 -33
  129. package/src/commands/templates.ts +22 -0
  130. package/src/github/handlers.ts +55 -9
  131. package/src/heartbeat/heartbeat.ts +71 -12
  132. package/src/hooks/hook.ts +49 -33
  133. package/src/http/active-sessions.ts +11 -1
  134. package/src/http/agents.ts +261 -30
  135. package/src/http/all-routes.ts +1 -0
  136. package/src/http/config.ts +13 -2
  137. package/src/http/core.ts +102 -4
  138. package/src/http/db-query-bounded.ts +313 -0
  139. package/src/http/db-query-shared.ts +249 -0
  140. package/src/http/db-query.ts +54 -42
  141. package/src/http/events.ts +2 -0
  142. package/src/http/index.ts +27 -2
  143. package/src/http/mcp-bridge.ts +37 -3
  144. package/src/http/mcp.ts +12 -0
  145. package/src/http/memory.ts +13 -2
  146. package/src/http/openapi.ts +1 -0
  147. package/src/http/poll.ts +70 -27
  148. package/src/http/route-def.ts +43 -9
  149. package/src/http/schedules.ts +2 -2
  150. package/src/http/scripts.ts +31 -4
  151. package/src/http/session-data.ts +44 -0
  152. package/src/http/tasks.ts +2 -2
  153. package/src/http/users.ts +74 -52
  154. package/src/http/webhooks.ts +5 -2
  155. package/src/http/workflows.ts +1 -4
  156. package/src/http/x.ts +14 -0
  157. package/src/integrations/kapso/inbound.ts +1 -1
  158. package/src/jira/outbound.ts +39 -8
  159. package/src/jira/webhook-lifecycle.ts +13 -2
  160. package/src/linear/outbound.ts +43 -10
  161. package/src/oauth/keepalive.ts +13 -2
  162. package/src/prompts/base-prompt.ts +1 -1
  163. package/src/prompts/session-templates.ts +45 -7
  164. package/src/providers/claude-adapter.ts +6 -0
  165. package/src/providers/claude-managed-models.ts +34 -10
  166. package/src/providers/codex-adapter.ts +6 -0
  167. package/src/providers/opencode-adapter.ts +6 -0
  168. package/src/providers/pi-mono-adapter.ts +8 -0
  169. package/src/providers/pricing-sources.md +24 -0
  170. package/src/queue-stall-alarm.ts +216 -0
  171. package/src/scheduler/scheduler.ts +7 -0
  172. package/src/script-workflows/executor.ts +2 -1
  173. package/src/scripts-runtime/executors/native.ts +2 -1
  174. package/src/scripts-runtime/executors/types.ts +5 -0
  175. package/src/scripts-runtime/loader.ts +5 -0
  176. package/src/scripts-runtime/swarm-config.ts +8 -0
  177. package/src/scripts-runtime/swarm-sdk.ts +7 -0
  178. package/src/scripts-runtime/types/stdlib.d.ts +1 -0
  179. package/src/scripts-runtime/types/swarm-sdk.d.ts +1 -0
  180. package/src/tests/budget-claim-gate.test.ts +20 -0
  181. package/src/tests/claude-adapter.test.ts +32 -0
  182. package/src/tests/claude-managed-adapter.test.ts +38 -7
  183. package/src/tests/codex-adapter.test.ts +30 -0
  184. package/src/tests/create-task-validation.test.ts +128 -0
  185. package/src/tests/credential-wait.test.ts +67 -1
  186. package/src/tests/db-query.test.ts +711 -2
  187. package/src/tests/http-api-integration.test.ts +81 -0
  188. package/src/tests/http-users.test.ts +166 -1
  189. package/src/tests/identity-field-budget.test.ts +176 -0
  190. package/src/tests/mcp-tools-user.test.ts +34 -0
  191. package/src/tests/mcp-user-route.test.ts +17 -0
  192. package/src/tests/metrics-http.test.ts +44 -0
  193. package/src/tests/multi-runtime-registration.test.ts +2703 -0
  194. package/src/tests/openapi-response-contract.test.ts +24 -0
  195. package/src/tests/openapi-route-headers.test.ts +83 -0
  196. package/src/tests/opencode-adapter.test.ts +40 -0
  197. package/src/tests/outbound-listener-rejection.test.ts +138 -0
  198. package/src/tests/pi-mono-adapter.test.ts +98 -0
  199. package/src/tests/profile-sync-warning.test.ts +277 -0
  200. package/src/tests/profile-sync.test.ts +232 -6
  201. package/src/tests/prompt-template-session.test.ts +32 -3
  202. package/src/tests/queue-stall-alarm.test.ts +226 -0
  203. package/src/tests/requester-comms.test.ts +32 -0
  204. package/src/tests/runner-requester-profile.test.ts +38 -0
  205. package/src/tests/sandboxed-process.test.ts +45 -0
  206. package/src/tests/schedule-target-type.test.ts +38 -0
  207. package/src/tests/script-connections.test.ts +16 -3
  208. package/src/tests/scripts-external-api.test.ts +8 -3
  209. package/src/tests/scripts-http.test.ts +12 -0
  210. package/src/tests/scripts-retention.test.ts +384 -0
  211. package/src/tests/scripts-runtime-identity.test.ts +375 -0
  212. package/src/tests/scripts-runtime.test.ts +4 -3
  213. package/src/tests/sdk-allowlist.test.ts +47 -0
  214. package/src/tests/send-task-requested-by.test.ts +9 -0
  215. package/src/tests/session-costs.test.ts +506 -1
  216. package/src/tests/shutdown-error-scrubbing.test.ts +92 -0
  217. package/src/tests/steering-core.test.ts +28 -0
  218. package/src/tests/task-artifacts.test.ts +386 -0
  219. package/src/tests/task-attachments-schema.test.ts +17 -1
  220. package/src/tests/task-completion-idempotency.test.ts +57 -0
  221. package/src/tests/task-pull-request-backfill-migration.test.ts +321 -0
  222. package/src/tests/update-profile-api.test.ts +67 -39
  223. package/src/tests/update-profile-auth.test.ts +48 -4
  224. package/src/tests/user-attribution-migration.test.ts +54 -0
  225. package/src/tests/user-identity.test.ts +72 -0
  226. package/src/tests/workflow-async-v2.test.ts +39 -1
  227. package/src/tests/workflow-http-v2.test.ts +3 -2
  228. package/src/tests/workflow-mcp-trigger-schema.test.ts +52 -3
  229. package/src/tests/workflow-retry-v2.test.ts +45 -1
  230. package/src/tests/workflow-swarm-script.test.ts +14 -5
  231. package/src/tools/db-query.ts +18 -8
  232. package/src/tools/get-task-details.ts +24 -3
  233. package/src/tools/manage-user.ts +36 -3
  234. package/src/tools/poll-task.ts +93 -14
  235. package/src/tools/script-common.ts +5 -0
  236. package/src/tools/send-task.ts +1 -4
  237. package/src/tools/store-progress.ts +13 -2
  238. package/src/tools/swarm-config/delete-config.ts +9 -0
  239. package/src/tools/swarm-config/set-config.ts +3 -2
  240. package/src/tools/task-action.ts +30 -0
  241. package/src/tools/task-tool-ctx.ts +8 -1
  242. package/src/tools/update-profile.ts +16 -21
  243. package/src/tools/utils.ts +5 -0
  244. package/src/tools/workflows/trigger-workflow.ts +7 -1
  245. package/src/types.ts +165 -1
  246. package/src/utils/github-pull-request.ts +76 -0
  247. package/src/utils/identity-field-budget.ts +73 -0
  248. package/src/utils/multi-runtime.ts +20 -0
  249. package/src/utils/requester-comms.ts +24 -0
  250. package/src/utils/sandboxed-process.ts +40 -14
  251. package/src/workflows/engine.ts +22 -2
  252. package/src/workflows/executors/swarm-script.ts +24 -1
  253. package/src/workflows/resume.ts +19 -11
  254. package/src/workflows/retry-poller.ts +1 -0
  255. package/src/workflows/triggers.ts +4 -0
  256. package/templates/schedules/daily-compounding-reflection/content.md +12 -3
  257. package/templates/skills/db-query-guidance/config.json +14 -0
  258. package/templates/skills/db-query-guidance/content.md +57 -0
  259. package/dist/cli-3te4jdkm.js +0 -225
  260. package/dist/cli-bmze986d.js +0 -7
  261. package/dist/cli-kh3ba2zf.js +0 -607
  262. package/dist/index-hp1kdbjj.js +0 -36
  263. package/dist/mistral-conversations-81fwav0j.js +0 -30578
  264. package/dist/otel-nqa12wf3.js +0 -3149
  265. package/dist/{pi-messages-tqj8q6b9.js → pi-messages-sssy32x4.js} +3 -3
@@ -36,6 +36,11 @@ import {
36
36
  updateAgentStatus,
37
37
  } from "../be/db";
38
38
  import { repointTrackerSyncBySwarmId } from "../be/db-queries/tracker";
39
+ import {
40
+ agentsWithLiveRuntime,
41
+ countActiveRuntimeInstancesForAgent,
42
+ expireStaleRuntimeInstances,
43
+ } from "../be/multi-runtime";
39
44
  import { promotePendingSteeringForTask } from "../be/steering";
40
45
  import { resolveTemplate } from "../prompts/resolver";
41
46
  import {
@@ -48,6 +53,7 @@ import {
48
53
  REBOOT_RETRY_PIN_TAG,
49
54
  } from "../tasks/worker-follow-up";
50
55
  import type { AgentTask } from "../types";
56
+ import { isMultiRuntimeEnabled } from "../utils/multi-runtime";
51
57
  import { scrubSecrets } from "../utils/secret-scrubber";
52
58
  import { getExecutorRegistry } from "../workflows";
53
59
  import { recoverIncompleteRuns } from "../workflows/recovery";
@@ -212,6 +218,7 @@ export interface HeartbeatFindings {
212
218
  mentionProcessing: number;
213
219
  inboxProcessing: number;
214
220
  workflowRuns: number;
221
+ staleRuntimes: number;
215
222
  };
216
223
  }
217
224
 
@@ -285,9 +292,15 @@ export async function codeLevelTriage(): Promise<HeartbeatFindings> {
285
292
  mentionProcessing: 0,
286
293
  inboxProcessing: 0,
287
294
  workflowRuns: 0,
295
+ staleRuntimes: 0,
288
296
  },
289
297
  };
290
298
 
299
+ // 0. Retire runtimes that stopped pinging, before anything reasons about
300
+ // which workers are alive — otherwise auto-assignment below can hand a task
301
+ // to an agent this same sweep is about to mark offline, stranding it.
302
+ findings.staleCleanup.staleRuntimes = expireStaleRuntimeInstances().expired;
303
+
291
304
  // 1. Detect and remediate stalled tasks (tiered: auto-fail dead workers)
292
305
  detectAndRemediateStalledTasks(findings);
293
306
 
@@ -423,7 +436,7 @@ function remediateCrashedWorkerTask(
423
436
  `[Heartbeat] Workflow-step task ${task.id.slice(0, 8)} failed — engine will handle retry (${opts.shortLabel})`,
424
437
  );
425
438
  const remaining = getActiveTaskCount(task.agentId);
426
- if (remaining === 0) updateAgentStatus(task.agentId, "idle");
439
+ if (remaining === 0) restoreAgentIdleAfterRemediation(task.agentId);
427
440
  }
428
441
  return;
429
442
  }
@@ -443,7 +456,7 @@ function remediateCrashedWorkerTask(
443
456
  })`,
444
457
  );
445
458
  const remaining = getActiveTaskCount(task.agentId);
446
- if (remaining === 0) updateAgentStatus(task.agentId, "idle");
459
+ if (remaining === 0) restoreAgentIdleAfterRemediation(task.agentId);
447
460
  }
448
461
  return;
449
462
  }
@@ -462,7 +475,7 @@ function remediateCrashedWorkerTask(
462
475
  `[Heartbeat] Auto-failed task ${task.id.slice(0, 8)} — ${RESUME_BUDGET_EXHAUSTED_REASON} (${opts.shortLabel})`,
463
476
  );
464
477
  const remaining = getActiveTaskCount(task.agentId);
465
- if (remaining === 0) updateAgentStatus(task.agentId, "idle");
478
+ if (remaining === 0) restoreAgentIdleAfterRemediation(task.agentId);
466
479
  }
467
480
  return;
468
481
  }
@@ -537,7 +550,7 @@ function remediateCrashedWorkerTask(
537
550
  if (opts.cleanupActiveSession) deleteActiveSession(task.id);
538
551
 
539
552
  const remaining = getActiveTaskCount(task.agentId);
540
- if (remaining === 0) updateAgentStatus(task.agentId, "idle");
553
+ if (remaining === 0) restoreAgentIdleAfterRemediation(task.agentId);
541
554
  }
542
555
 
543
556
  /**
@@ -622,7 +635,7 @@ export async function runRebootSweep(): Promise<void> {
622
635
 
623
636
  // Fix agent status
624
637
  if (getActiveTaskCount(task.agentId) === 0) {
625
- updateAgentStatus(task.agentId, "idle");
638
+ restoreAgentIdleAfterRemediation(task.agentId);
626
639
  }
627
640
 
628
641
  // Don't retry system-generated heartbeat tasks
@@ -760,8 +773,20 @@ function autoAssignPoolTasks(findings: HeartbeatFindings): void {
760
773
  // assigning to them would just have them exit on their next poll. Filter on
761
774
  // the rows already returned (emptyPollCount is populated) rather than
762
775
  // re-querying per worker via shouldBlockPolling().
776
+ // A multi-runtime agent whose runtimes have all died still reads as idle
777
+ // until its rows expire; assigning to it would strand the task, since
778
+ // nothing is left to poll for it.
779
+ // While the mode is on, only an agent with a live runtime can poll, so
780
+ // anything else — dead runtimes, or a worker that has not re-registered
781
+ // since the flag was enabled — would have the task stranded on it. With
782
+ // the flag off this is inert: legacy workers stop refreshing their
783
+ // retained rows, and filtering on them would park tasks on healthy agents.
784
+ const multiRuntime = isMultiRuntimeEnabled();
785
+ const withLiveRuntime = multiRuntime ? agentsWithLiveRuntime() : null;
763
786
  const idleWorkers = getIdleWorkersWithCapacity().filter(
764
- (w) => (w.emptyPollCount ?? 0) < MAX_EMPTY_POLLS,
787
+ (w) =>
788
+ (w.emptyPollCount ?? 0) < MAX_EMPTY_POLLS &&
789
+ (withLiveRuntime === null || withLiveRuntime.has(w.id)),
765
790
  );
766
791
  if (idleWorkers.length === 0) return;
767
792
 
@@ -959,6 +984,16 @@ function escalateStarvedPoolTasks(findings: HeartbeatFindings): void {
959
984
  }
960
985
  }
961
986
 
987
+ /**
988
+ * Return a remediated agent to idle — unless multi-runtime liveness says no
989
+ * process is serving it. Recovery still runs for the task; the agent just
990
+ * must not be advertised as available when nothing can poll for it.
991
+ */
992
+ function restoreAgentIdleAfterRemediation(agentId: string): void {
993
+ if (isMultiRuntimeEnabled() && countActiveRuntimeInstancesForAgent(agentId) === 0) return;
994
+ updateAgentStatus(agentId, "idle");
995
+ }
996
+
962
997
  /**
963
998
  * Call existing stale resource cleanup functions.
964
999
  */
@@ -1271,8 +1306,13 @@ export async function runHeartbeatSweep(): Promise<void> {
1271
1306
  mentionProcessing: 0,
1272
1307
  inboxProcessing: 0,
1273
1308
  workflowRuns: 0,
1309
+ staleRuntimes: 0,
1274
1310
  },
1275
1311
  };
1312
+ // Expiry runs even on a cleanup-only tick: an idle agent whose runtime
1313
+ // stopped pinging is exactly the case the preflight gate sees as
1314
+ // "nothing actionable", and it would otherwise stay available forever.
1315
+ cleanupOnlyFindings.staleCleanup.staleRuntimes = expireStaleRuntimeInstances().expired;
1276
1316
  await cleanupStaleResources(cleanupOnlyFindings);
1277
1317
  logFindings(cleanupOnlyFindings);
1278
1318
  return; // Nothing actionable — bail early
@@ -1345,14 +1385,28 @@ export function startHeartbeat(intervalMs = defaultIntervalMs()): void {
1345
1385
 
1346
1386
  console.log(`[Heartbeat] Starting with ${intervalMs}ms interval`);
1347
1387
 
1348
- // Run aggressive reboot sweep first (no thresholds), then normal sweep cycle
1349
- setTimeout(async () => {
1350
- await runRebootSweep();
1351
- runHeartbeatSweep();
1388
+ // Run aggressive reboot sweep first (no thresholds), then normal sweep cycle.
1389
+ // Both sweeps are best-effort: `runHeartbeatSweep` resets `isSweeping` in a
1390
+ // `finally` but has no `catch`, so a throw anywhere inside it escapes as an
1391
+ // unhandled rejection unless it is caught here.
1392
+ setTimeout(() => {
1393
+ runRebootSweep()
1394
+ .then(() => runHeartbeatSweep())
1395
+ .catch((err) =>
1396
+ console.error(
1397
+ "[Heartbeat] boot sweep failed:",
1398
+ scrubSecrets(err instanceof Error ? err.message : String(err)),
1399
+ ),
1400
+ );
1352
1401
  }, 5000);
1353
1402
 
1354
1403
  heartbeatInterval = setInterval(() => {
1355
- runHeartbeatSweep();
1404
+ runHeartbeatSweep().catch((err) =>
1405
+ console.error(
1406
+ "[Heartbeat] sweep failed:",
1407
+ scrubSecrets(err instanceof Error ? err.message : String(err)),
1408
+ ),
1409
+ );
1356
1410
  }, intervalMs);
1357
1411
 
1358
1412
  // Also start the checklist interval
@@ -1434,7 +1488,12 @@ export function startHeartbeatChecklist(intervalMs = HEARTBEAT_CHECKLIST_INTERVA
1434
1488
 
1435
1489
  // Recurring checklist starts from the second interval onward
1436
1490
  checklistInterval = setInterval(() => {
1437
- checkHeartbeatChecklist();
1491
+ checkHeartbeatChecklist().catch((err) =>
1492
+ console.error(
1493
+ "[Heartbeat] checklist check failed:",
1494
+ scrubSecrets(err instanceof Error ? err.message : String(err)),
1495
+ ),
1496
+ );
1438
1497
  }, intervalMs);
1439
1498
  }
1440
1499
 
package/src/hooks/hook.ts CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  type RetrievalRow,
11
11
  } from "../be/memory/raters/llm";
12
12
  import {
13
+ buildIndependentIdentityPayloads,
13
14
  contentSha256,
14
15
  readIdentityBaselines,
15
16
  warnProfileFileTooLarge,
@@ -110,6 +111,42 @@ export function shouldShowRegistrationNudge(opts: {
110
111
  return true;
111
112
  }
112
113
 
114
+ export async function postHookProfileUpdate({
115
+ url,
116
+ headers,
117
+ body,
118
+ label,
119
+ fetchImpl = fetch,
120
+ }: {
121
+ url: string;
122
+ headers: Record<string, string>;
123
+ body: Record<string, string>;
124
+ label: string;
125
+ fetchImpl?: typeof fetch;
126
+ }): Promise<void> {
127
+ try {
128
+ const response = await fetchImpl(url, {
129
+ method: "PUT",
130
+ headers: {
131
+ ...headers,
132
+ "Content-Type": "application/json",
133
+ },
134
+ body: JSON.stringify(body),
135
+ });
136
+ if (!response.ok) {
137
+ const responseBody = await response.text();
138
+ console.error(
139
+ scrubSecrets(
140
+ `[hook] ${label} profile sync failed: HTTP ${response.status}${responseBody ? ` — ${responseBody}` : ""}`,
141
+ ),
142
+ );
143
+ }
144
+ } catch (error) {
145
+ const message = error instanceof Error ? error.message : String(error);
146
+ console.error(scrubSecrets(`[hook] ${label} profile sync errored: ${message}`));
147
+ }
148
+ }
149
+
113
150
  /**
114
151
  * Hook response for blocking actions
115
152
  * See: https://code.claude.com/docs/en/hooks
@@ -637,23 +674,12 @@ export async function handleHook(): Promise<void> {
637
674
  const content = await file.text();
638
675
 
639
676
  if (!content.trim()) return;
640
- if (content.length > MAX_PROFILE_FILE_LENGTH) {
641
- warnProfileFileTooLarge(agentId, "claudeMd", content.length, "hook");
642
- return;
643
- }
644
-
645
- try {
646
- await fetch(`${getBaseUrl()}/api/agents/${agentId}/profile`, {
647
- method: "PUT",
648
- headers: {
649
- ...mcpConfig.headers,
650
- "Content-Type": "application/json",
651
- },
652
- body: JSON.stringify({ claudeMd: content, changeSource: "session_sync" }),
653
- });
654
- } catch {
655
- // Silently fail - don't block shutdown
656
- }
677
+ await postHookProfileUpdate({
678
+ url: `${getBaseUrl()}/api/agents/${agentId}/profile`,
679
+ headers: mcpConfig.headers,
680
+ body: { claudeMd: content, changeSource: "session_sync" },
681
+ label: "claudeMd",
682
+ });
657
683
  };
658
684
 
659
685
  // Minimum length for SOUL.md and IDENTITY.md to prevent accidental corruption.
@@ -684,8 +710,6 @@ export async function handleHook(): Promise<void> {
684
710
  const content = await soulFile.text();
685
711
  if (baselines?.soulMd && contentSha256(content) === baselines.soulMd) {
686
712
  // Unchanged during session — skip to preserve Lead's DB edits
687
- } else if (content.length > MAX_PROFILE_FILE_LENGTH) {
688
- warnProfileFileTooLarge(agentId, "soulMd", content.length, "hook");
689
713
  } else if (content.trim()) {
690
714
  if (content.length < IDENTITY_FILE_MIN_LENGTH) {
691
715
  console.error(
@@ -702,8 +726,6 @@ export async function handleHook(): Promise<void> {
702
726
  const content = await identityFile.text();
703
727
  if (baselines?.identityMd && contentSha256(content) === baselines.identityMd) {
704
728
  // Unchanged during session — skip
705
- } else if (content.length > MAX_PROFILE_FILE_LENGTH) {
706
- warnProfileFileTooLarge(agentId, "identityMd", content.length, "hook");
707
729
  } else if (content.trim()) {
708
730
  if (content.length < IDENTITY_FILE_MIN_LENGTH) {
709
731
  console.error(
@@ -720,8 +742,6 @@ export async function handleHook(): Promise<void> {
720
742
  const content = await toolsMdFile.text();
721
743
  if (baselines?.toolsMd && contentSha256(content) === baselines.toolsMd) {
722
744
  // Unchanged during session — skip
723
- } else if (content.length > MAX_PROFILE_FILE_LENGTH) {
724
- warnProfileFileTooLarge(agentId, "toolsMd", content.length, "hook");
725
745
  } else if (content.trim()) {
726
746
  updates.toolsMd = content;
727
747
  }
@@ -741,17 +761,13 @@ export async function handleHook(): Promise<void> {
741
761
 
742
762
  if (Object.keys(updates).length === 0) return;
743
763
 
744
- try {
745
- await fetch(`${getBaseUrl()}/api/agents/${agentId}/profile`, {
746
- method: "PUT",
747
- headers: {
748
- ...mcpConfig.headers,
749
- "Content-Type": "application/json",
750
- },
751
- body: JSON.stringify({ ...updates, changeSource }),
764
+ for (const payload of buildIndependentIdentityPayloads(updates, changeSource)) {
765
+ await postHookProfileUpdate({
766
+ url: `${getBaseUrl()}/api/agents/${agentId}/profile`,
767
+ headers: mcpConfig.headers,
768
+ body: payload.body,
769
+ label: payload.label,
752
770
  });
753
- } catch {
754
- // Silently fail
755
771
  }
756
772
  };
757
773
 
@@ -12,6 +12,7 @@ import {
12
12
  updateActiveSessionProviderSessionId,
13
13
  } from "../be/db";
14
14
  import { ActiveSessionSchema, AgentTaskSchema } from "../types";
15
+ import { isMultiRuntimeEnabled } from "../utils/multi-runtime";
15
16
  import { route } from "./route-def";
16
17
  import { jsonError } from "./utils";
17
18
 
@@ -47,6 +48,7 @@ const createActiveSession = route({
47
48
  inboxMessageId: z.string().optional(),
48
49
  taskDescription: z.string().optional(),
49
50
  runnerSessionId: z.string().optional(),
51
+ runtimeInstanceId: z.string().optional(),
50
52
  }),
51
53
  responses: {
52
54
  201: { description: "Session created", schema: z.object({ session: ActiveSessionSchema }) },
@@ -166,6 +168,7 @@ export async function handleActiveSessions(
166
168
  inboxMessageId: parsed.body.inboxMessageId,
167
169
  taskDescription: parsed.body.taskDescription,
168
170
  runnerSessionId: parsed.body.runnerSessionId,
171
+ runtimeInstanceId: parsed.body.runtimeInstanceId,
169
172
  });
170
173
  createActiveSession.respond(res, 201, { session });
171
174
  return true;
@@ -211,7 +214,14 @@ export async function handleActiveSessions(
211
214
  if (!parsed) return true;
212
215
  let cleaned = 0;
213
216
  if (parsed.body?.agentId) {
214
- cleaned = cleanupAgentSessions(parsed.body.agentId);
217
+ // Multi-runtime: several processes share this agent id, and a booting
218
+ // worker has no evidence distinguishing a crashed predecessor's session
219
+ // from a live-but-quiet sibling's — sessions heartbeat on tool activity
220
+ // only, and during the activation window a live worker's runtime may
221
+ // have no row at all. Reclamation stays with the heartbeat's
222
+ // stalled-task classifier (stale session AND stale task), backstopped
223
+ // by the sweep's stale-session cleanup; boot cleanup deletes nothing.
224
+ cleaned = isMultiRuntimeEnabled() ? 0 : cleanupAgentSessions(parsed.body.agentId);
215
225
  } else {
216
226
  cleaned = cleanupStaleSessions(parsed.body?.maxAgeMinutes ?? 30);
217
227
  }