@desplega.ai/agent-swarm 1.166.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 (194) hide show
  1. package/dist/acp-adapter-zpvy6321.js +34 -0
  2. package/dist/{actions-qz3m692w.js → actions-fq2zrwtf.js} +25 -25
  3. package/dist/{amp-adapter-j1xbmsqz.js → amp-adapter-5gbk7501.js} +4 -4
  4. package/dist/{anthropic-messages-hst62ws9.js → anthropic-messages-13yn502s.js} +4 -4
  5. package/dist/{app-43vrdmv8.js → app-nctetn1e.js} +10 -9
  6. package/dist/{assistant-ertffmka.js → assistant-0aq8gjvj.js} +26 -26
  7. package/dist/{azure-openai-responses-mh8tvf78.js → azure-openai-responses-9st34pfz.js} +5 -5
  8. package/dist/{boot-reembed-998jtpdg.js → boot-reembed-1ze5853n.js} +9 -8
  9. package/dist/{boot-reembed-s7k7c6v2.js → boot-reembed-fmp5ww69.js} +9 -8
  10. package/dist/{boot-scrub-sweep-nepsk4t1.js → boot-scrub-sweep-cbp8xd4e.js} +10 -9
  11. package/dist/{boot-seeds-vvzs1zhk.js → boot-seeds-22tx8a53.js} +12 -11
  12. package/dist/{claude-adapter-2zvmekrp.js → claude-adapter-0jhgeh1d.js} +24 -22
  13. package/dist/{claude-managed-adapter-s49wbk8t.js → claude-managed-adapter-tzwxy803.js} +4 -4
  14. package/dist/{claude-managed-setup-cvhqq8qn.js → claude-managed-setup-nrhfx5b0.js} +3 -3
  15. package/dist/{claude-sdk-session-kkbxtw5q.js → claude-sdk-session-wh77rh83.js} +22 -22
  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-dw2q9f7g.js → cli-1yrcfa5s.js} +4 -4
  19. package/dist/{cli-jvj6a0cv.js → cli-577bxkf6.js} +27 -17
  20. package/dist/{cli-zy7vwsne.js → cli-5qkcdv8c.js} +16 -9
  21. package/dist/{cli-cwxhbf43.js → cli-5t5my0h8.js} +85 -2
  22. package/dist/{cli-y59h2pk9.js → cli-69egj654.js} +1 -1
  23. package/dist/{cli-68r1j4sg.js → cli-6bpvv8h4.js} +3 -3
  24. package/dist/{cli-cpfmqz3n.js → cli-6qxmjpem.js} +5 -5
  25. package/dist/{cli-aqcey741.js → cli-6vfz44sr.js} +1 -1
  26. package/dist/{cli-32bca7nr.js → cli-7avy4v8w.js} +1 -1
  27. package/dist/{cli-f2n17zhp.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-gt8azyet.js → cli-c2tpw9ck.js} +1 -1
  32. package/dist/{cli-ecknp7p1.js → cli-c8r99akr.js} +10 -10
  33. package/dist/{cli-3qn4r334.js → cli-cexctgn5.js} +20 -14
  34. package/dist/{cli-vzc73wet.js → cli-cf2vgn9d.js} +87 -69
  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-nvvfd8bq.js → cli-jbzbr36n.js} +4091 -3793
  38. package/dist/{cli-3ddybbbx.js → cli-jvmtxqtc.js} +26 -14
  39. package/dist/{cli-pkke6geq.js → cli-km4x66j0.js} +83 -65
  40. package/dist/{cli-j2jbr8af.js → cli-kw0jcfje.js} +1 -1
  41. package/dist/{cli-8t229cmr.js → cli-m7vmeepg.js} +30 -17
  42. package/dist/{cli-yshaazs1.js → cli-mkdsy1nq.js} +62 -20
  43. package/dist/{cli-b0f70z24.js → cli-mn8qkd8v.js} +1 -1
  44. package/dist/{cli-0qjcsxgz.js → cli-mxry91sa.js} +30 -9
  45. package/dist/cli-nkqjj42q.js +104 -0
  46. package/dist/{cli-rggdq8wq.js → cli-p8rt8spp.js} +3 -3
  47. package/dist/{cli-v2a8x45d.js → cli-pkj1w46r.js} +10 -5
  48. package/dist/cli-q1vnz8wv.js +96 -0
  49. package/dist/{cli-fj86xr3c.js → cli-qd97v6z2.js} +2 -2
  50. package/dist/{cli-66zh26sp.js → cli-qzmzwegq.js} +57 -17
  51. package/dist/{cli-p4ztb80h.js → cli-r7eeddkq.js} +13 -8
  52. package/dist/{cli-8ssjez7e.js → cli-rhy7z1rp.js} +22 -11
  53. package/dist/{cli-4mevd1hf.js → cli-s9y81s7j.js} +5 -5
  54. package/dist/{cli-h3x613ze.js → cli-tc5zec7v.js} +6 -6
  55. package/dist/{cli-ttbsse3f.js → cli-tmhsxhb8.js} +1 -1
  56. package/dist/{cli-0nhdwxd5.js → cli-tx2zvn23.js} +3 -3
  57. package/dist/{cli-mwegs4af.js → cli-ty4bzkqr.js} +3 -3
  58. package/dist/{cli-ysnyhws9.js → cli-v1pwxreq.js} +54 -51
  59. package/dist/{cli-feh3dkah.js → cli-v1w7pbd8.js} +2 -1
  60. package/dist/{cli-10zmp1p7.js → cli-vr5avem2.js} +27 -32
  61. package/dist/{cli-bzr0ftht.js → cli-wnb9b2g3.js} +19 -0
  62. package/dist/{cli-df56tqdw.js → cli-wydz3feg.js} +108 -57
  63. package/dist/{cli-krqm5f11.js → cli-xezn9nkh.js} +16 -13
  64. package/dist/{cli-8kt0bmgx.js → cli-xf713zz0.js} +2 -2
  65. package/dist/{cli-x7kpr72a.js → cli-y0kjpfm9.js} +2 -2
  66. package/dist/{cli-6zcfw0x7.js → cli-y195hv1a.js} +1 -1
  67. package/dist/{cli-fn697bnt.js → cli-ykt8f7va.js} +21 -8
  68. package/dist/{cli-4r1sjfc4.js → cli-ymmc1zxg.js} +8 -8
  69. package/dist/{cli-8kgvmkak.js → cli-ypgba1a8.js} +1 -1
  70. package/dist/{cli-rsy933yz.js → cli-z1dqhc9y.js} +30 -19
  71. package/dist/{cli-qbksc292.js → cli-zy39d433.js} +2 -0
  72. package/dist/cli.js +26 -26
  73. package/dist/{cloudflare-workers-ai-system-one-tt484t61.js → cloudflare-workers-ai-system-one-sta45jf9.js} +7 -5
  74. package/dist/{codex-adapter-dw1wxqt8.js → codex-adapter-cgesnjkr.js} +18 -18
  75. package/dist/{codex-login-terbdsqq.js → codex-login-g1cyt5zm.js} +1 -1
  76. package/dist/{codex-session-runner-3cgb8y1g.js → codex-session-runner-8kd7ddhr.js} +18 -18
  77. package/dist/{commands-mhxam6z2.js → commands-h6zaaxym.js} +9 -8
  78. package/dist/{cursor-adapter-2qda3st7.js → cursor-adapter-v8mm6bp5.js} +5 -5
  79. package/dist/{db-sjks418k.js → db-qg3myn53.js} +9 -8
  80. package/dist/{dispatcher-1es759rn.js → dispatcher-7vnwj9qw.js} +24 -24
  81. package/dist/{dsh-adapter-bek4cyw9.js → dsh-adapter-tz6hfp5r.js} +5 -5
  82. package/dist/{easter-egg-3d-mtpnw06s.js → easter-egg-3d-66zxervv.js} +2 -2
  83. package/dist/{execute-s4ccjq3n.js → execute-h4jgevyw.js} +61 -11
  84. package/dist/{google-generative-ai-d6nphg3q.js → google-generative-ai-b8zxrpax.js} +5 -5
  85. package/dist/{google-vertex-cpshkref.js → google-vertex-nrdd697t.js} +5 -5
  86. package/dist/{grok-adapter-ar9cw9jg.js → grok-adapter-jdt2dz2s.js} +18 -8
  87. package/dist/{handlers-1gkbhktr.js → handlers-2qtkpncr.js} +27 -27
  88. package/dist/{hook-ds2zxq9v.js → hook-51yfcgsv.js} +42 -31
  89. package/dist/{hook-vxa3tcsy.js → hook-qnbdgwvd.js} +14 -14
  90. package/dist/{http-3h21z9md.js → http-jsmtag08.js} +320 -98
  91. package/dist/{index-9b65j1m5.js → index-2t9kesn1.js} +9 -8
  92. package/dist/{index-dkfxpkmm.js → index-bt8wkv1w.js} +25 -25
  93. package/dist/{index-f8xnqrye.js → index-cx1sb9bq.js} +33 -24
  94. package/dist/{index-bn6cy8ps.js → index-ekywyv2v.js} +24 -24
  95. package/dist/{index-n99fycq7.js → index-t4g06w93.js} +24 -24
  96. package/dist/{keepalive-3xspa9jw.js → keepalive-4aezx4bj.js} +11 -10
  97. package/dist/{lead-6qphc124.js → lead-r44bkpm5.js} +47 -47
  98. package/dist/{llama-cpp-classify-tgj35k8d.js → llama-cpp-classify-6erb7wwv.js} +3 -1
  99. package/dist/{maintenance-0teem1bb.js → maintenance-grz5jrzh.js} +9 -8
  100. package/dist/{mistral-conversations-8445ddjn.js → mistral-conversations-059y51zt.js} +4 -4
  101. package/dist/{node-9sg7ajby.js → node-qm9eb6z0.js} +1 -1
  102. package/dist/{oauth-refresh-sweep-sh9q25xp.js → oauth-refresh-sweep-wm4ctjfj.js} +10 -9
  103. package/dist/{onboard-54qmnqxe.js → onboard-v60zgqf0.js} +2 -2
  104. package/dist/{openai-codex-responses-9ynm88zy.js → openai-codex-responses-jzxkta9b.js} +8 -7
  105. package/dist/{openai-completions-nhqwjmaw.js → openai-completions-663yncfq.js} +4 -4
  106. package/dist/openai-decisions-vj9nkpf8.js +162 -0
  107. package/dist/{openai-responses-xx1c4kgm.js → openai-responses-m3g43rrt.js} +5 -5
  108. package/dist/{opencode-adapter-wjfa2wvd.js → opencode-adapter-yjyqynzf.js} +7 -7
  109. package/dist/{openrouter-images-8yx3487g.js → openrouter-images-kr60n6b9.js} +1 -1
  110. package/dist/{otel-impl-ycqzp2a1.js → otel-impl-ghh4dyf8.js} +1 -1
  111. package/dist/{pi-messages-vxdt8v7j.js → pi-messages-xszk613n.js} +1 -1
  112. package/dist/{pi-mono-adapter-e77e9zst.js → pi-mono-adapter-0eg8kw2c.js} +68 -51
  113. package/dist/{pricing-refresh-atnkngqs.js → pricing-refresh-mjrqvnvw.js} +11 -10
  114. package/dist/{rbac-roles-j78wdksm.js → rbac-roles-axv4d74x.js} +10 -9
  115. package/dist/{rbac-roles-9pjphh9q.js → rbac-roles-fd5tskj9.js} +9 -8
  116. package/dist/{render-v2-pxjs49fx.js → render-v2-k50j33qd.js} +10 -9
  117. package/dist/{runtime-v2t1r79e.js → runtime-h9ha1mq6.js} +4 -4
  118. package/dist/{setup-ymej0xtx.js → setup-4syh0pd2.js} +2 -2
  119. package/dist/{typesafe-system-one-datfr82p.js → typesafe-system-one-e988vty7.js} +7 -5
  120. package/dist/{virtual-modules-wm8mxdxx.js → virtual-modules-ftr9kwe8.js} +11 -11
  121. package/dist/{work-objects-3nh4ymzb.js → work-objects-jcdbg8rr.js} +27 -27
  122. package/dist/{worker-qa9fkz5s.js → worker-x8j8c9sx.js} +47 -47
  123. package/openapi.json +1 -1
  124. package/package.json +8 -8
  125. package/src/be/memory/providers/openai-embedding.ts +7 -2
  126. package/src/be/memory/raters/llm-summarizer.ts +7 -2
  127. package/src/be/modelsdev-cache.json +61 -0
  128. package/src/be/seed-pricing.ts +84 -53
  129. package/src/be/swarm-config-guard.ts +13 -0
  130. package/src/commands/provider-credentials.ts +28 -5
  131. package/src/commands/runner.ts +7 -12
  132. package/src/heartbeat/heartbeat.ts +91 -19
  133. package/src/hooks/hook.ts +72 -27
  134. package/src/http/mcp.ts +5 -0
  135. package/src/http/onboarding.ts +2 -0
  136. package/src/providers/acp-targets.ts +9 -4
  137. package/src/providers/amp-adapter.ts +1 -1
  138. package/src/providers/claude-adapter.ts +26 -0
  139. package/src/providers/codex-adapter.ts +30 -3
  140. package/src/providers/codex-oauth/auth-json.ts +21 -4
  141. package/src/providers/codex-oauth/types.ts +2 -0
  142. package/src/providers/dsh-adapter.ts +10 -1
  143. package/src/providers/grok-adapter.ts +18 -2
  144. package/src/providers/opencode-adapter.ts +21 -5
  145. package/src/providers/pi-mono-extension.ts +56 -25
  146. package/src/providers/pricing-sources.md +2 -0
  147. package/src/server.ts +63 -3
  148. package/src/tests/acp-adapter.test.ts +152 -12
  149. package/src/tests/ci-unit-test-report.test.ts +73 -0
  150. package/src/tests/claude-default-route.test.ts +75 -0
  151. package/src/tests/claude-hook-output.test.ts +233 -0
  152. package/src/tests/codex-adapter-pool-auth-revalidate.test.ts +4 -2
  153. package/src/tests/codex-adapter.test.ts +166 -26
  154. package/src/tests/codex-oauth-auth-timestamps.test.ts +76 -0
  155. package/src/tests/codex-oauth.test.ts +4 -1
  156. package/src/tests/context-window.test.ts +2 -2
  157. package/src/tests/credential-check.test.ts +51 -1
  158. package/src/tests/dsh-adapter.test.ts +23 -1
  159. package/src/tests/enabled-tools.test.ts +269 -0
  160. package/src/tests/entrypoint-codex-oauth-seed.test.ts +12 -0
  161. package/src/tests/grok-adapter.test.ts +13 -0
  162. package/src/tests/haiku-5-5-pricing.test.ts +38 -0
  163. package/src/tests/heartbeat-checklist.test.ts +158 -1
  164. package/src/tests/onboarding.test.ts +44 -0
  165. package/src/tests/opencode-adapter.test.ts +66 -3
  166. package/src/tests/opencode-plugin.test.ts +37 -0
  167. package/src/tests/openrouter-attribution-call-sites.test.ts +204 -0
  168. package/src/tests/openrouter-base-url.test.ts +30 -39
  169. package/src/tests/pi-mono-extension.test.ts +161 -2
  170. package/src/tests/pricing-refresh.test.ts +70 -1
  171. package/src/tests/reasoning-effort.test.ts +3 -1
  172. package/src/tests/telemetry-trigger.test.ts +12 -1
  173. package/src/tests/workflow-llm.test.ts +14 -0
  174. package/src/tests/workflow-system-one-decision-providers.test.ts +33 -0
  175. package/src/tools/request-human-input.ts +7 -2
  176. package/src/utils/enabled-tools.ts +27 -0
  177. package/src/utils/internal-ai/complete-structured.ts +11 -3
  178. package/src/utils/openrouter-base-url.ts +23 -0
  179. package/src/workflows/executors/raw-llm.ts +1 -0
  180. package/src/workflows/executors/system-one-decision.ts +2 -0
  181. package/src/workflows/executors/validate.ts +1 -0
  182. package/src/workflows/executors/workflow-llm.ts +9 -2
  183. package/templates/skills/agent-browser/SKILL.md +2 -0
  184. package/templates/skills/agent-browser/content.md +2 -0
  185. package/templates/skills/code-quality/content.md +1 -0
  186. package/templates/skills/heartbeat-runbook/content.md +1 -1
  187. package/templates/skills/work-on-task/SKILL.md +4 -0
  188. package/templates/skills/work-on-task/content.md +4 -0
  189. package/templates/skills/workflow-iterate/content.md +9 -0
  190. package/dist/acp-adapter-yzfxn8vj.js +0 -23
  191. package/dist/cli-3dpj7atn.js +0 -11
  192. package/dist/cli-gvcgmx69.js +0 -183
  193. package/dist/{cli-jhm6hzjn.js → cli-mxvnnv5q.js} +7 -7
  194. package/dist/{codex-hook-d2rkvf39.js → codex-hook-15dgc4cb.js} +1 -1
@@ -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,
package/src/hooks/hook.ts CHANGED
@@ -168,12 +168,25 @@ export async function postHookProfileUpdate({
168
168
  }
169
169
 
170
170
  /**
171
- * Hook response for blocking actions
171
+ * Hook JSON responses. Claude Code parses stdout as JSON only when the whole
172
+ * stdout is one JSON object, so these must be the only stdout of the event.
172
173
  * See: https://code.claude.com/docs/en/hooks
173
174
  */
174
- interface HookBlockResponse {
175
- decision: "block";
176
- reason: string;
175
+ type HookBlockResponse =
176
+ | {
177
+ hookSpecificOutput: {
178
+ hookEventName: "PreToolUse";
179
+ permissionDecision: "deny";
180
+ permissionDecisionReason: string;
181
+ };
182
+ }
183
+ | { decision: "block"; reason: string };
184
+
185
+ interface HookContextResponse {
186
+ hookSpecificOutput: {
187
+ hookEventName: "PreToolUse" | "PostToolUse";
188
+ additionalContext: string;
189
+ };
177
190
  }
178
191
 
179
192
  /**
@@ -850,12 +863,33 @@ export async function handleHook(): Promise<void> {
850
863
 
851
864
  /**
852
865
  * Output a blocking response to stop Claude from continuing.
853
- * This is used when a task has been cancelled.
866
+ * Must be the only stdout of the event: any plain line before it makes
867
+ * Claude Code read the whole stdout as text and run the tool anyway.
854
868
  */
855
869
  const outputBlockResponse = (reason: string): void => {
856
- const response: HookBlockResponse = {
857
- decision: "block",
858
- reason,
870
+ const response: HookBlockResponse =
871
+ msg.hook_event_name === "PreToolUse"
872
+ ? {
873
+ hookSpecificOutput: {
874
+ hookEventName: "PreToolUse",
875
+ permissionDecision: "deny",
876
+ permissionDecisionReason: reason,
877
+ },
878
+ }
879
+ : { decision: "block", reason };
880
+ console.log(JSON.stringify(response));
881
+ };
882
+
883
+ /**
884
+ * Output context for the model on a tool event. Plain stdout on PreToolUse
885
+ * and PostToolUse never reaches the model; additionalContext does.
886
+ */
887
+ const outputToolContext = (
888
+ hookEventName: "PreToolUse" | "PostToolUse",
889
+ additionalContext: string,
890
+ ): void => {
891
+ const response: HookContextResponse = {
892
+ hookSpecificOutput: { hookEventName, additionalContext },
859
893
  };
860
894
  console.log(JSON.stringify(response));
861
895
  };
@@ -988,8 +1022,23 @@ export async function handleHook(): Promise<void> {
988
1022
  // Get current agent info
989
1023
  const agentInfo = await getAgentInfo();
990
1024
 
991
- // Always output agent status with system tray
992
- if (agentInfo) {
1025
+ // A block must be the only stdout of the event, so a cancelled task is
1026
+ // checked before the status line goes out.
1027
+ if (
1028
+ msg.hook_event_name === "UserPromptSubmit" &&
1029
+ agentInfo &&
1030
+ !agentInfo.isLead &&
1031
+ agentInfo.status === "busy"
1032
+ ) {
1033
+ if (await checkAndBlockIfCancelled(true)) {
1034
+ return;
1035
+ }
1036
+ }
1037
+
1038
+ // Output agent status with system tray. Skipped on tool events: their plain
1039
+ // stdout never reaches the model and would break a JSON decision.
1040
+ const isToolEvent = msg.hook_event_name === "PreToolUse" || msg.hook_event_name === "PostToolUse";
1041
+ if (agentInfo && !isToolEvent) {
993
1042
  // Base status line
994
1043
  console.log(
995
1044
  `You are registered as ${agentInfo.isLead ? "lead" : "worker"} agent "${agentInfo.name}" (ID: ${agentInfo.id}, status: ${agentInfo.status}).`,
@@ -1004,7 +1053,7 @@ export async function handleHook(): Promise<void> {
1004
1053
  }
1005
1054
  } else if (
1006
1055
  shouldShowRegistrationNudge({
1007
- agentInfoPresent: false,
1056
+ agentInfoPresent: Boolean(agentInfo),
1008
1057
  eventType: msg.hook_event_name,
1009
1058
  hasAgentIdHeader: hasAgentIdHeader(),
1010
1059
  })
@@ -1131,8 +1180,7 @@ export async function handleHook(): Promise<void> {
1131
1180
  }
1132
1181
 
1133
1182
  // Block `gh pr create|edit` when the body for a public repo leaks internal
1134
- // identifiers. Exit code 2 blocks the call even though the status line
1135
- // above already went to stdout, and stderr reaches the model.
1183
+ // identifiers. Exit code 2 blocks the call and stderr reaches the model.
1136
1184
  if (msg.tool_name === "Bash") {
1137
1185
  const prBodyBlock = await guardGhPrBody(msg.tool_input, msg.cwd ?? process.cwd());
1138
1186
  if (prBodyBlock) {
@@ -1142,6 +1190,9 @@ export async function handleHook(): Promise<void> {
1142
1190
  }
1143
1191
  }
1144
1192
 
1193
+ // Emitted last, so a later block stays the only stdout of the event.
1194
+ let loopWarning: string | undefined;
1195
+
1145
1196
  // Tool loop detection (workers only, when processing a task)
1146
1197
  if (agentInfo && !agentInfo.isLead && agentInfo.status === "busy") {
1147
1198
  const loopTaskFile = await readTaskFile();
@@ -1162,7 +1213,7 @@ export async function handleHook(): Promise<void> {
1162
1213
  }
1163
1214
 
1164
1215
  if (loopResult.severity === "warning" && loopResult.reason) {
1165
- console.log(`Warning: ${loopResult.reason}`);
1216
+ loopWarning = `Warning: ${loopResult.reason}`;
1166
1217
  }
1167
1218
  }
1168
1219
  }
@@ -1179,6 +1230,9 @@ export async function handleHook(): Promise<void> {
1179
1230
  }
1180
1231
  }
1181
1232
 
1233
+ if (loopWarning) {
1234
+ outputToolContext("PreToolUse", loopWarning);
1235
+ }
1182
1236
  break;
1183
1237
  }
1184
1238
 
@@ -1200,7 +1254,8 @@ export async function handleHook(): Promise<void> {
1200
1254
  method: "PUT",
1201
1255
  headers: mcpConfig!.headers,
1202
1256
  }).catch((e) => {
1203
- console.debug("Failed to update agent activity timestamp:", e);
1257
+ // stderr: stdout on this event is reserved for one JSON object.
1258
+ console.error("Failed to update agent activity timestamp:", e);
1204
1259
  });
1205
1260
  }
1206
1261
 
@@ -1270,7 +1325,8 @@ export async function handleHook(): Promise<void> {
1270
1325
  if (msg.tool_name?.endsWith("send-task")) {
1271
1326
  const maybeTaskId = (msg.tool_response as { task?: { id?: string } })?.task?.id;
1272
1327
 
1273
- console.log(
1328
+ outputToolContext(
1329
+ "PostToolUse",
1274
1330
  `Task sent successfully.${maybeTaskId ? ` Task ID: ${maybeTaskId}.` : ""} Monitor progress using the get-task-details tool periodically.`,
1275
1331
  );
1276
1332
  }
@@ -1278,17 +1334,6 @@ export async function handleHook(): Promise<void> {
1278
1334
  }
1279
1335
  break;
1280
1336
 
1281
- case "UserPromptSubmit": {
1282
- // For worker agents, check if their task has been cancelled
1283
- // This catches cancellations at the start of a new iteration
1284
- if (agentInfo && !agentInfo.isLead && agentInfo.status === "busy") {
1285
- if (await checkAndBlockIfCancelled(true)) {
1286
- return; // Exit early
1287
- }
1288
- }
1289
- break;
1290
- }
1291
-
1292
1337
  case "Stop":
1293
1338
  // Clean up any artifact tunnels managed by PM2
1294
1339
  try {
package/src/http/mcp.ts CHANGED
@@ -244,6 +244,11 @@ export async function handleMcp(
244
244
  }
245
245
  const server = await createServer({
246
246
  preloadedTools,
247
+ // Agent row, then global row, then deployment env. A blank agent row
248
+ // resolves to "unset" for that agent.
249
+ enabledTools:
250
+ configs.find((config) => config.key === "SWARM_ENABLED_TOOLS")?.value ??
251
+ process.env.SWARM_ENABLED_TOOLS,
247
252
  scriptsOnly: resolveScriptsOnlyMode({
248
253
  env: process.env.SCRIPTS_ONLY_MCP,
249
254
  configValue,
@@ -21,6 +21,7 @@ import {
21
21
  } from "../be/onboarding";
22
22
  import { assertUrlSafe, publicEndpointSsrfOptions } from "../oauth/mcp-wrapper";
23
23
  import { ProviderNameSchema } from "../types";
24
+ import { getOpenRouterAttributionHeaders } from "../utils/openrouter-base-url";
24
25
  import { ensureConfigAdmin } from "./config";
25
26
  import { scheduleIntegrationsReload } from "./core";
26
27
  import { route } from "./route-def";
@@ -255,6 +256,7 @@ async function handleMemoryProbe(body: OnboardingMemoryRequest): Promise<Onboard
255
256
  const client = new OpenAI({
256
257
  baseURL: endpoint.toString(),
257
258
  apiKey,
259
+ defaultHeaders: getOpenRouterAttributionHeaders(endpoint.toString()),
258
260
  timeout: 15_000,
259
261
  maxRetries: 0,
260
262
  fetchOptions: { redirect: "manual" },
@@ -2,6 +2,7 @@ import { mkdir, mkdtemp, rm } from "node:fs/promises";
2
2
  import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { normalizeGrokModel } from "@desplega/model-catalog";
5
+ import type { Config } from "@opencode-ai/sdk";
5
6
  import { clampContextPercent, getContextWindowSize } from "../utils/context-window";
6
7
  import {
7
8
  ACP_TARGET_IDS,
@@ -9,6 +10,7 @@ import {
9
10
  getAcpTargetCatalogEntry,
10
11
  isAcpTarget,
11
12
  } from "./acp-target-catalog";
13
+ import { applyOpenRouterBaseUrlOverride } from "./opencode-adapter";
12
14
  import { applyReasoningEffort } from "./reasoning-effort";
13
15
  import type { CostData, CostModelUsage, ProviderEvent, ProviderSessionConfig } from "./types";
14
16
 
@@ -124,8 +126,8 @@ function configuredOptions(config: ProviderSessionConfig): Record<string, string
124
126
  return options;
125
127
  }
126
128
 
127
- function withModelInJsonEnv(env: Record<string, string>, key: string, model: string): void {
128
- if (!model.trim()) return;
129
+ function withOpencodeConfig(env: Record<string, string>, model: string): void {
130
+ const key = "OPENCODE_CONFIG_CONTENT";
129
131
  let value: Record<string, unknown> = {};
130
132
  const existing = env[key];
131
133
  if (existing) {
@@ -138,7 +140,9 @@ function withModelInJsonEnv(env: Record<string, string>, key: string, model: str
138
140
  console.warn(`\x1b[33m[acp]\x1b[0m Replacing invalid ${key} JSON for model fallback`);
139
141
  }
140
142
  }
141
- env[key] = JSON.stringify({ ...value, model: model.trim() });
143
+ if (model.trim()) value.model = model.trim();
144
+ applyOpenRouterBaseUrlOverride(value as Config, env);
145
+ env[key] = JSON.stringify(value);
142
146
  }
143
147
 
144
148
  function parseCommand(command: string, args: string | undefined): string[] {
@@ -203,7 +207,8 @@ const opencodeTargetProfile: AcpTargetProfile = {
203
207
  const env = baseTargetEnv(config);
204
208
  const entry = getAcpTargetCatalogEntry("opencode");
205
209
  copyEnvKeys(config, env, entry.envKeys);
206
- withModelInJsonEnv(env, "OPENCODE_CONFIG_CONTENT", config.model);
210
+ copyEnvKeys(config, env, ["OPENROUTER_APP_ATTRIBUTION"]);
211
+ withOpencodeConfig(env, config.model);
207
212
  return env;
208
213
  },
209
214
  configuredOptions,
@@ -44,7 +44,7 @@ import type {
44
44
  * default; the image installs exactly this version and the per-task settings
45
45
  * set `amp.updates.mode: "disabled"`. Bump it deliberately, with a live run.
46
46
  */
47
- export const AMP_PACKAGE = "@ampcode/cli@0.0.1791403257-g05ffa9";
47
+ export const AMP_PACKAGE = "@ampcode/cli@0.0.1791460855-g1f688c";
48
48
 
49
49
  /** MCP server name. Amp exposes its tools as `agent_swarm` / `store_progress` (dashes become underscores). */
50
50
  const AMP_MCP_SERVER_NAME = "agent-swarm";
@@ -34,6 +34,7 @@ import {
34
34
  import { withFileLock } from "../utils/file-lock";
35
35
  import { fetchInstalledMcpServers } from "../utils/mcp-server-fetcher";
36
36
  import { swarmRuntimeInstanceId } from "../utils/multi-runtime";
37
+ import { getOpenRouterAttributionHeaders } from "../utils/openrouter-base-url";
37
38
  import {
38
39
  detachedProcessGroup,
39
40
  registerProcessGroup,
@@ -738,6 +739,30 @@ export function buildClaudeCodeRuntimeEnv(
738
739
  };
739
740
  }
740
741
 
742
+ /**
743
+ * `ANTHROPIC_CUSTOM_HEADERS` ("Name: value" per line) with OpenRouter app
744
+ * attribution appended, when `ANTHROPIC_BASE_URL` points at openrouter.ai.
745
+ * The operator's lines are kept in order (they may carry gateway
746
+ * credentials); only lines naming one of our headers are replaced. Returns
747
+ * `{}` when nothing changes, so the inherited value stays byte-identical.
748
+ *
749
+ * Exported for unit testing.
750
+ */
751
+ export function claudeOpenRouterAttributionEnv(env: Record<string, string | undefined>): {
752
+ ANTHROPIC_CUSTOM_HEADERS?: string;
753
+ } {
754
+ const headers = getOpenRouterAttributionHeaders(env.ANTHROPIC_BASE_URL ?? "", env);
755
+ const names = new Set(Object.keys(headers).map((name) => name.toLowerCase()));
756
+ if (names.size === 0) return {};
757
+ const kept = (env.ANTHROPIC_CUSTOM_HEADERS ?? "").split("\n").filter((line) => {
758
+ const colon = line.indexOf(":");
759
+ const name = colon === -1 ? "" : line.slice(0, colon).trim().toLowerCase();
760
+ return line.trim() !== "" && !names.has(name);
761
+ });
762
+ const ours = Object.entries(headers).map(([name, value]) => `${name}: ${value}`);
763
+ return { ANTHROPIC_CUSTOM_HEADERS: [...kept, ...ours].join("\n") };
764
+ }
765
+
741
766
  export function buildClaudeSessionEnvironment(
742
767
  config: ProviderSessionConfig,
743
768
  model: string,
@@ -751,6 +776,7 @@ export function buildClaudeSessionEnvironment(
751
776
  env: {
752
777
  ENABLE_PROMPT_CACHING_1H: "1",
753
778
  ...sourceEnv,
779
+ ...claudeOpenRouterAttributionEnv(sourceEnv),
754
780
  ...buildClaudeCodeRuntimeEnv(sourceEnv),
755
781
  ...buildClaudeCodeOtelEnv(sourceEnv),
756
782
  ...(reasoningApplication.kind === "claude-env" ? reasoningApplication.env : {}),
@@ -147,6 +147,8 @@ export type ThreadEvent =
147
147
  usage?: Usage;
148
148
  }
149
149
  | { type: "turn.interrupted"; usage?: Usage }
150
+ /** Latest single-request usage: a context-window snapshot, not billable usage. */
151
+ | { type: "context.updated"; usage: Usage; contextWindow?: number }
150
152
  | { type: "error"; message: string; codexErrorInfo?: string | null; willRetry?: boolean };
151
153
 
152
154
  interface CodexThread {
@@ -833,8 +835,25 @@ class AppServerCodexThread implements CodexThread {
833
835
  }
834
836
  break;
835
837
  case "thread/tokenUsage/updated": {
836
- const tokenUsage = params.tokenUsage as { total?: unknown } | undefined;
838
+ const tokenUsage = params.tokenUsage as
839
+ | { total?: unknown; last?: unknown; modelContextWindow?: unknown }
840
+ | undefined;
841
+ // `total` is cumulative for the thread and drives cost accounting at
842
+ // turn end. `last` is the most recent model request, which is the
843
+ // only figure that reflects what currently sits in the context window.
837
844
  this.latestTotalUsage = normalizeUsage(tokenUsage?.total);
845
+ const last = normalizeUsage(tokenUsage?.last);
846
+ if (last) {
847
+ push({
848
+ type: "context.updated",
849
+ usage: last,
850
+ contextWindow:
851
+ typeof tokenUsage?.modelContextWindow === "number" &&
852
+ tokenUsage.modelContextWindow > 0
853
+ ? tokenUsage.modelContextWindow
854
+ : undefined,
855
+ });
856
+ }
838
857
  break;
839
858
  }
840
859
  case "error": {
@@ -1394,6 +1413,9 @@ export class CodexSession implements ProviderSession {
1394
1413
  case "turn.interrupted":
1395
1414
  this.accountUsage(event.usage);
1396
1415
  break;
1416
+ case "context.updated":
1417
+ this.emitContextUsage(event.usage, event.contextWindow ?? this.contextWindow);
1418
+ break;
1397
1419
  case "error": {
1398
1420
  const { message } = this.formatTerminalError(event.message, event.codexErrorInfo);
1399
1421
  if (event.willRetry === true) {
@@ -1420,7 +1442,12 @@ export class CodexSession implements ProviderSession {
1420
1442
  this.accumulatedUsage.reasoning_output_tokens + usage.reasoning_output_tokens,
1421
1443
  }
1422
1444
  : usage;
1445
+ // No context_usage here: a turn's usage sums every model request in the
1446
+ // turn, so it measures consumption, not window occupancy. Context comes
1447
+ // from the per-request `context.updated` snapshots instead.
1448
+ }
1423
1449
 
1450
+ private emitContextUsage(usage: Usage, contextWindow: number): void {
1424
1451
  // Codex input tokens already include the cached read and write subsets.
1425
1452
  // Passing either subset again would inflate context usage.
1426
1453
  const contextUsed = computeContextUsedUnified({
@@ -1432,8 +1459,8 @@ export class CodexSession implements ProviderSession {
1432
1459
  this.emit({
1433
1460
  type: "context_usage",
1434
1461
  contextUsedTokens: contextUsed,
1435
- contextTotalTokens: this.contextWindow,
1436
- contextPercent: clampContextPercent(contextUsed, this.contextWindow) ?? 0,
1462
+ contextTotalTokens: contextWindow,
1463
+ contextPercent: clampContextPercent(contextUsed, contextWindow) ?? 0,
1437
1464
  outputTokens: usage.output_tokens,
1438
1465
  contextFormula: CONTEXT_FORMULA,
1439
1466
  });
@@ -2,7 +2,7 @@
2
2
  * Conversion utilities between CodexOAuthCredentials (our internal type)
3
3
  * and ~/.codex/auth.json (the format the Codex CLI reads natively).
4
4
  *
5
- * The Codex CLI expects auth.json in this exact format:
5
+ * The Codex CLI expects these native auth.json fields:
6
6
  * {
7
7
  * "auth_mode": "chatgpt",
8
8
  * "OPENAI_API_KEY": null,
@@ -14,6 +14,7 @@
14
14
  * },
15
15
  * "last_refresh": "<ISO 8601>"
16
16
  * }
17
+ * Swarm also writes optional `expires` metadata for lossless conversion.
17
18
  *
18
19
  * Note: `id_token` is set to the `access_token` value because the token
19
20
  * exchange endpoint doesn't return a separate `id_token`. This matches
@@ -23,7 +24,7 @@
23
24
  */
24
25
 
25
26
  import { codexPlanFromClaim } from "../../utils/subscription-plans.js";
26
- import { extractChatgptPlanType, extractChatgptUserId } from "./flow.js";
27
+ import { decodeJwt, extractChatgptPlanType, extractChatgptUserId } from "./flow.js";
27
28
  import type { CodexAuthJson, CodexOAuthCredentials } from "./types.js";
28
29
 
29
30
  /**
@@ -96,15 +97,31 @@ export function credentialsToAuthJson(
96
97
  refresh_token: includeRefreshToken ? creds.refresh : "",
97
98
  account_id: creds.accountId,
98
99
  },
99
- last_refresh: new Date(creds.expires).toISOString(),
100
+ last_refresh: new Date().toISOString(),
101
+ // Swarm-only metadata keeps opaque-token round trips lossless. Codex
102
+ // ignores this field and may drop it when it rewrites auth.json.
103
+ expires: creds.expires,
100
104
  };
101
105
  }
102
106
 
103
107
  export function authJsonToCredentials(auth: CodexAuthJson): CodexOAuthCredentials {
108
+ // A CLI refresh may replace the access token and discard our metadata.
109
+ // Prefer its JWT expiry, which describes the token actually in the file.
110
+ const exp = decodeJwt(auth.tokens.access_token)?.exp;
111
+ const jwtExpires = typeof exp === "number" ? exp * 1000 : NaN;
112
+ const expires =
113
+ Number.isFinite(jwtExpires) && jwtExpires >= 0
114
+ ? jwtExpires
115
+ : typeof auth.expires === "number" && Number.isFinite(auth.expires)
116
+ ? auth.expires
117
+ : new Date(auth.last_refresh).getTime();
118
+
104
119
  return {
105
120
  access: auth.tokens.access_token,
106
121
  refresh: auth.tokens.refresh_token,
107
- expires: new Date(auth.last_refresh).getTime(),
122
+ // Legacy swarm files put expiry in last_refresh; keep that fallback
123
+ // only when neither the token nor the explicit metadata carries it.
124
+ expires,
108
125
  accountId: auth.tokens.account_id,
109
126
  };
110
127
  }
@@ -28,6 +28,8 @@ export type CodexAuthJson = {
28
28
  account_id: string;
29
29
  };
30
30
  last_refresh: string;
31
+ /** Swarm-only access-token expiry (milliseconds); native Codex files omit it. */
32
+ expires?: number;
31
33
  };
32
34
 
33
35
  type TokenResult =
@@ -9,7 +9,10 @@ import {
9
9
  } from "../utils/context-window";
10
10
  import { addDshStepUsage, type DshStepUsage, normalizeDshStepUsage } from "../utils/dsh-usage";
11
11
  import { swarmRuntimeInstanceId } from "../utils/multi-runtime";
12
- import { getOpenRouterBaseUrl } from "../utils/openrouter-base-url";
12
+ import {
13
+ getOpenRouterAttributionHeaders,
14
+ getOpenRouterBaseUrl,
15
+ } from "../utils/openrouter-base-url";
13
16
  import {
14
17
  detachedProcessGroup,
15
18
  registerProcessGroup,
@@ -342,6 +345,9 @@ export class DshAdapter implements ProviderAdapter {
342
345
  const directory = await mkdtemp(join(tmpdir(), "swarm-dsh-"));
343
346
  try {
344
347
  const patchPath = join(directory, "patch.json");
348
+ const openRouterHeaders = openrouter
349
+ ? getOpenRouterAttributionHeaders(getOpenRouterBaseUrl(env), env)
350
+ : {};
345
351
  const runtimeInstanceId = swarmRuntimeInstanceId();
346
352
  // JSON is valid YAML. Values stay data, including arbitrary system prompts.
347
353
  await writeFile(
@@ -364,6 +370,9 @@ export class DshAdapter implements ProviderAdapter {
364
370
  openrouter: {
365
371
  apiKeyEnv: keyEnv,
366
372
  baseURL: getOpenRouterBaseUrl(env),
373
+ ...(Object.keys(openRouterHeaders).length > 0
374
+ ? { headers: openRouterHeaders }
375
+ : {}),
367
376
  api: "openai-completions",
368
377
  // Declare the selected ID even if the bundled catalog predates it.
369
378
  models: [