@desplega.ai/agent-swarm 1.166.0 → 1.168.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 (221) hide show
  1. package/dist/acp-adapter-zhejfecx.js +34 -0
  2. package/dist/{actions-qz3m692w.js → actions-7jg840nr.js} +25 -25
  3. package/dist/{amp-adapter-j1xbmsqz.js → amp-adapter-ye6rnetm.js} +4 -4
  4. package/dist/{anthropic-messages-hst62ws9.js → anthropic-messages-13yn502s.js} +4 -4
  5. package/dist/{app-43vrdmv8.js → app-ffes2kyf.js} +10 -9
  6. package/dist/{assistant-ertffmka.js → assistant-fhm5smyz.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-20q3nzvg.js} +9 -8
  9. package/dist/{boot-reembed-s7k7c6v2.js → boot-reembed-jvfadhsk.js} +9 -8
  10. package/dist/{boot-scrub-sweep-nepsk4t1.js → boot-scrub-sweep-jtfajzve.js} +10 -9
  11. package/dist/{boot-seeds-vvzs1zhk.js → boot-seeds-9qt0j8j2.js} +12 -11
  12. package/dist/{claude-adapter-2zvmekrp.js → claude-adapter-a8znhthz.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-7x7chdrs.js} +7608 -7596
  16. package/dist/{cli-zsh093hk.js → cli-014vns2y.js} +63 -1
  17. package/dist/{cli-8kgvmkak.js → cli-0jngabtb.js} +1 -1
  18. package/dist/{cli-qvfk83fy.js → cli-13xwcqvj.js} +2 -2
  19. package/dist/{cli-0nhdwxd5.js → cli-43jg9xdw.js} +3 -3
  20. package/dist/{cli-cpfmqz3n.js → cli-47zfzshm.js} +5 -5
  21. package/dist/{cli-zy7vwsne.js → cli-5qkcdv8c.js} +16 -9
  22. package/dist/{cli-cwxhbf43.js → cli-5t5my0h8.js} +85 -2
  23. package/dist/{cli-y59h2pk9.js → cli-69egj654.js} +1 -1
  24. package/dist/{cli-68r1j4sg.js → cli-6bpvv8h4.js} +3 -3
  25. package/dist/{cli-yshaazs1.js → cli-74ez9b66.js} +63 -20
  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-fn697bnt.js → cli-9q0bh411.js} +21 -8
  30. package/dist/{cli-rggdq8wq.js → cli-9q24yyvs.js} +3 -3
  31. package/dist/cli-a35hzdve.js +33 -0
  32. package/dist/{cli-3qn4r334.js → cli-aj1e0h0d.js} +20 -14
  33. package/dist/{cli-rsy933yz.js → cli-bcspch6h.js} +30 -19
  34. package/dist/{cli-x3x0qwky.js → cli-bxqez1p0.js} +51 -23
  35. package/dist/{cli-ng97fjq7.js → cli-cmxkbzzs.js} +33 -7
  36. package/dist/{cli-df56tqdw.js → cli-ee0402n1.js} +198 -70
  37. package/dist/{cli-mwegs4af.js → cli-ew0c3a1j.js} +3 -3
  38. package/dist/{cli-ysnyhws9.js → cli-ewhwpyrb.js} +54 -51
  39. package/dist/{cli-j2jbr8af.js → cli-fhj4y16f.js} +1 -1
  40. package/dist/{cli-8t229cmr.js → cli-fqq06kkb.js} +30 -17
  41. package/dist/{cli-jvj6a0cv.js → cli-g6gx2dn1.js} +54 -19
  42. package/dist/{cli-nexwh8zg.js → cli-g7k7zbcz.js} +12 -5
  43. package/dist/{cli-ecknp7p1.js → cli-j05x7pnb.js} +10 -10
  44. package/dist/{cli-gt8azyet.js → cli-jb931ba2.js} +1 -1
  45. package/dist/{cli-nvvfd8bq.js → cli-jbzbr36n.js} +4091 -3793
  46. package/dist/{cli-3ddybbbx.js → cli-jvmtxqtc.js} +26 -14
  47. package/dist/{cli-pkke6geq.js → cli-km4x66j0.js} +83 -65
  48. package/dist/{cli-b0f70z24.js → cli-mn8qkd8v.js} +1 -1
  49. package/dist/{cli-0qjcsxgz.js → cli-nctfv2yy.js} +99 -9
  50. package/dist/{cli-4r1sjfc4.js → cli-ng1pmdx6.js} +10 -10
  51. package/dist/cli-nkqjj42q.js +104 -0
  52. package/dist/{cli-aqcey741.js → cli-p5jqk8fr.js} +1 -1
  53. package/dist/{cli-66zh26sp.js → cli-pgehz8e1.js} +57 -17
  54. package/dist/cli-q1vnz8wv.js +96 -0
  55. package/dist/{cli-fj86xr3c.js → cli-qd97v6z2.js} +2 -2
  56. package/dist/{cli-p4ztb80h.js → cli-r7eeddkq.js} +13 -8
  57. package/dist/{cli-8ssjez7e.js → cli-rhy7z1rp.js} +22 -11
  58. package/dist/{cli-vzc73wet.js → cli-rr5z1j4p.js} +87 -69
  59. package/dist/{cli-4mevd1hf.js → cli-s9y81s7j.js} +5 -5
  60. package/dist/{cli-dw2q9f7g.js → cli-svah6b6w.js} +4 -4
  61. package/dist/{cli-h3x613ze.js → cli-tc5zec7v.js} +6 -6
  62. package/dist/{cli-ttbsse3f.js → cli-tmhsxhb8.js} +1 -1
  63. package/dist/{cli-feh3dkah.js → cli-v1w7pbd8.js} +2 -1
  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-krqm5f11.js → cli-xezn9nkh.js} +16 -13
  67. package/dist/{cli-8kt0bmgx.js → cli-xf713zz0.js} +2 -2
  68. package/dist/{cli-6zcfw0x7.js → cli-y195hv1a.js} +1 -1
  69. package/dist/{cli-x7kpr72a.js → cli-ytmmyj7f.js} +2 -2
  70. package/dist/{cli-v2a8x45d.js → cli-z48ggfyq.js} +11 -6
  71. package/dist/{cli-qbksc292.js → cli-zy39d433.js} +2 -0
  72. package/dist/cli.js +28 -28
  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-n63k33d3.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-xkxfd81a.js} +18 -18
  77. package/dist/{commands-mhxam6z2.js → commands-q4mfhn51.js} +9 -8
  78. package/dist/{cursor-adapter-2qda3st7.js → cursor-adapter-v8mm6bp5.js} +5 -5
  79. package/dist/{db-sjks418k.js → db-1t0bj3pc.js} +9 -8
  80. package/dist/{dispatcher-1es759rn.js → dispatcher-k1fhavcf.js} +24 -24
  81. package/dist/{dsh-adapter-bek4cyw9.js → dsh-adapter-5423p70m.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-phrndeb8.js} +18 -8
  87. package/dist/{handlers-1gkbhktr.js → handlers-59ywqwzh.js} +27 -27
  88. package/dist/{hook-vxa3tcsy.js → hook-atj0swc2.js} +14 -14
  89. package/dist/{hook-ds2zxq9v.js → hook-mew1chq9.js} +42 -31
  90. package/dist/{http-3h21z9md.js → http-ef2fnn9v.js} +953 -98
  91. package/dist/{index-n99fycq7.js → index-48pb9erw.js} +24 -24
  92. package/dist/{index-bn6cy8ps.js → index-9fc2h7ww.js} +24 -24
  93. package/dist/{index-9b65j1m5.js → index-gr69c43q.js} +9 -8
  94. package/dist/{index-f8xnqrye.js → index-h26wjcfy.js} +43 -24
  95. package/dist/{index-dkfxpkmm.js → index-w2yhye23.js} +25 -25
  96. package/dist/{keepalive-3xspa9jw.js → keepalive-qhcqz56v.js} +11 -10
  97. package/dist/{lead-6qphc124.js → lead-hpk5r3b1.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-nvkyp2xe.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-ngskthwh.js} +10 -9
  103. package/dist/{onboard-54qmnqxe.js → onboard-yfnkqhxc.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-hhm64r6m.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-35f91nx2.js} +11 -10
  114. package/dist/{rbac-roles-j78wdksm.js → rbac-roles-97w370qj.js} +10 -9
  115. package/dist/{rbac-roles-9pjphh9q.js → rbac-roles-z6sje3gy.js} +9 -8
  116. package/dist/{render-v2-pxjs49fx.js → render-v2-n4v66bj3.js} +10 -9
  117. package/dist/{runtime-v2t1r79e.js → runtime-h9ha1mq6.js} +4 -4
  118. package/dist/{setup-ymej0xtx.js → setup-m3hhyv5f.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-mxf2c842.js} +27 -27
  122. package/dist/{worker-qa9fkz5s.js → worker-9t07z1gq.js} +47 -47
  123. package/openapi.json +72 -1
  124. package/package.json +10 -10
  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/script-connections.ts +40 -6
  129. package/src/be/seed-pricing.ts +84 -53
  130. package/src/be/swarm-config-guard.ts +14 -0
  131. package/src/commands/provider-credentials.ts +28 -5
  132. package/src/commands/runner.ts +7 -12
  133. package/src/extensions/catalog.generated.json +17 -0
  134. package/src/heartbeat/heartbeat.ts +91 -19
  135. package/src/hooks/hook.ts +72 -27
  136. package/src/http/mcp.ts +5 -0
  137. package/src/http/onboarding.ts +2 -0
  138. package/src/http/script-connections.ts +35 -0
  139. package/src/providers/acp-targets.ts +10 -5
  140. package/src/providers/amp-adapter.ts +1 -1
  141. package/src/providers/claude-adapter.ts +32 -0
  142. package/src/providers/claude-background-keepalive.ts +100 -0
  143. package/src/providers/claude-sdk-session.ts +6 -0
  144. package/src/providers/codex-adapter.ts +30 -3
  145. package/src/providers/codex-oauth/auth-json.ts +21 -4
  146. package/src/providers/codex-oauth/types.ts +2 -0
  147. package/src/providers/dsh-adapter.ts +11 -2
  148. package/src/providers/grok-adapter.ts +18 -2
  149. package/src/providers/opencode-adapter.ts +21 -5
  150. package/src/providers/pi-mono-extension.ts +56 -25
  151. package/src/providers/pricing-sources.md +2 -0
  152. package/src/scheduler/scheduler.ts +82 -13
  153. package/src/scripts-runtime/executors/registry.ts +1 -1
  154. package/src/server.ts +63 -3
  155. package/src/tests/acp-adapter.test.ts +152 -12
  156. package/src/tests/ci-unit-test-report.test.ts +73 -0
  157. package/src/tests/claude-background-keepalive-wiring.test.ts +220 -0
  158. package/src/tests/claude-background-keepalive.fixture.ts +162 -0
  159. package/src/tests/claude-background-keepalive.test.ts +139 -0
  160. package/src/tests/claude-default-route.test.ts +75 -0
  161. package/src/tests/claude-hook-output.test.ts +233 -0
  162. package/src/tests/codex-adapter-pool-auth-revalidate.test.ts +4 -2
  163. package/src/tests/codex-adapter.test.ts +166 -26
  164. package/src/tests/codex-oauth-auth-timestamps.test.ts +76 -0
  165. package/src/tests/codex-oauth.test.ts +4 -1
  166. package/src/tests/connection-embedded-auth.test.ts +51 -0
  167. package/src/tests/context-window.test.ts +2 -2
  168. package/src/tests/credential-check.test.ts +51 -1
  169. package/src/tests/docs-index-footprint.test.js +3 -3
  170. package/src/tests/dsh-adapter.test.ts +24 -2
  171. package/src/tests/enabled-tools.test.ts +269 -0
  172. package/src/tests/entrypoint-codex-oauth-seed.test.ts +12 -0
  173. package/src/tests/extensions-catalog.test.ts +1 -0
  174. package/src/tests/extensions-example-slack-reply-gate.test.ts +756 -0
  175. package/src/tests/grok-adapter.test.ts +13 -0
  176. package/src/tests/haiku-5-5-pricing.test.ts +38 -0
  177. package/src/tests/heartbeat-checklist.test.ts +158 -1
  178. package/src/tests/onboarding.test.ts +44 -0
  179. package/src/tests/opencode-adapter.test.ts +66 -3
  180. package/src/tests/opencode-plugin.test.ts +37 -0
  181. package/src/tests/openrouter-attribution-call-sites.test.ts +204 -0
  182. package/src/tests/openrouter-base-url.test.ts +30 -39
  183. package/src/tests/pi-mono-extension.test.ts +161 -2
  184. package/src/tests/pricing-refresh.test.ts +70 -1
  185. package/src/tests/reasoning-effort.test.ts +3 -1
  186. package/src/tests/schedule-target-type.test.ts +136 -0
  187. package/src/tests/script-connections-http.test.ts +45 -0
  188. package/src/tests/script-connections-mcp.test.ts +41 -0
  189. package/src/tests/script-executor-registry.test.ts +18 -0
  190. package/src/tests/send-task-self-send.test.ts +107 -0
  191. package/src/tests/telemetry-trigger.test.ts +12 -1
  192. package/src/tests/workflow-llm.test.ts +14 -0
  193. package/src/tests/workflow-system-one-decision-providers.test.ts +33 -0
  194. package/src/tools/request-human-input.ts +7 -2
  195. package/src/tools/script-connections/tool.ts +35 -3
  196. package/src/tools/send-task.ts +9 -1
  197. package/src/utils/enabled-tools.ts +27 -0
  198. package/src/utils/internal-ai/complete-structured.ts +11 -3
  199. package/src/utils/openrouter-base-url.ts +23 -0
  200. package/src/workflows/executors/raw-llm.ts +1 -0
  201. package/src/workflows/executors/system-one-decision.ts +2 -0
  202. package/src/workflows/executors/validate.ts +1 -0
  203. package/src/workflows/executors/workflow-llm.ts +9 -2
  204. package/templates/extensions/README.md +1 -0
  205. package/templates/extensions/slack-reply-gate/README.md +98 -0
  206. package/templates/extensions/slack-reply-gate/hooks.ts +582 -0
  207. package/templates/extensions/slack-reply-gate/manifest.json +8 -0
  208. package/templates/skills/agent-browser/SKILL.md +2 -0
  209. package/templates/skills/agent-browser/content.md +2 -0
  210. package/templates/skills/code-quality/content.md +1 -0
  211. package/templates/skills/heartbeat-runbook/content.md +1 -1
  212. package/templates/skills/swarm-scripts/SKILL.md +10 -0
  213. package/templates/skills/swarm-scripts/content.md +10 -0
  214. package/templates/skills/work-on-task/SKILL.md +4 -0
  215. package/templates/skills/work-on-task/content.md +4 -0
  216. package/templates/skills/workflow-iterate/content.md +9 -0
  217. package/dist/acp-adapter-yzfxn8vj.js +0 -23
  218. package/dist/cli-3dpj7atn.js +0 -11
  219. package/dist/cli-gvcgmx69.js +0 -183
  220. package/dist/{cli-jhm6hzjn.js → cli-mxvnnv5q.js} +7 -7
  221. package/dist/{codex-hook-d2rkvf39.js → codex-hook-15dgc4cb.js} +1 -1
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" },
@@ -26,6 +26,7 @@ import {
26
26
  type ConnectionAuthSummary,
27
27
  connectionAuthInputFromFlat,
28
28
  connectionAuthSummary,
29
+ deleteScriptConnection,
29
30
  findCredentialBindingByIdentity,
30
31
  getCredentialBindingById,
31
32
  getScriptConnectionById,
@@ -554,6 +555,27 @@ const setConnectionEnabledRoute = route({
554
555
  rbac: { permission: "script-connection.manage" },
555
556
  });
556
557
 
558
+ const deleteConnectionRoute = route({
559
+ method: "delete",
560
+ path: "/api/script-connections/{id}",
561
+ pattern: ["api", "script-connections", null],
562
+ operationId: "script_connections_delete",
563
+ summary: "Delete a script connection",
564
+ description:
565
+ "Hard-deletes the connection, its auto-managed credential binding, and any derived inline secret it owns.",
566
+ tags: ["Script Connections"],
567
+ params: idParamsSchema,
568
+ responses: {
569
+ 200: {
570
+ description: "Script connection deleted",
571
+ schema: z.object({ deleted: z.literal(true), id: z.string() }),
572
+ },
573
+ 403: { description: "Only the lead agent can manage script connections" },
574
+ 404: { description: "Script connection not found" },
575
+ },
576
+ rbac: { permission: "script-connection.manage" },
577
+ });
578
+
557
579
  const listCredentialBindingsRoute = route({
558
580
  method: "get",
559
581
  path: "/api/credential-bindings",
@@ -2111,6 +2133,19 @@ export async function handleScriptConnections(
2111
2133
  return true;
2112
2134
  }
2113
2135
 
2136
+ if (deleteConnectionRoute.match(req.method, pathSegments)) {
2137
+ const parsed = await deleteConnectionRoute.parse(req, res, pathSegments, queryParams);
2138
+ if (!parsed) return true;
2139
+ if (!(await ensureConnectionAdmin(req, res, agentId))) return true;
2140
+ const deleted = await deleteScriptConnection(parsed.params.id);
2141
+ if (!deleted) {
2142
+ jsonError(res, "Script connection not found.", 404);
2143
+ return true;
2144
+ }
2145
+ deleteConnectionRoute.respond(res, 200, { deleted: true, id: deleted.id });
2146
+ return true;
2147
+ }
2148
+
2114
2149
  if (listCredentialBindingsRoute.match(req.method, pathSegments)) {
2115
2150
  const includeManaged = queryParams.get("includeManaged") === "true";
2116
2151
  listCredentialBindingsRoute.respond(res, 200, {
@@ -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,
@@ -248,7 +253,7 @@ const geminiTargetProfile: AcpTargetProfile = {
248
253
  /**
249
254
  * Copilot CLI has no system-prompt flag. Under `--acp` it reads custom
250
255
  * instructions from the git root, the cwd, and every directory listed in
251
- * COPILOT_CUSTOM_INSTRUCTIONS_DIRS. Verified against 1.0.93: from those extra
256
+ * COPILOT_CUSTOM_INSTRUCTIONS_DIRS. Verified against 1.0.94: from those extra
252
257
  * directories it loads `.github/instructions/*.instructions.md`, but not
253
258
  * `AGENTS.md`. So the swarm prompt goes into a per-task directory outside the
254
259
  * repo, and the repo's own AGENTS.md is never touched.
@@ -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.1791547250-gb79b2a";
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,12 +34,14 @@ 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,
40
41
  terminateProcessGroup,
41
42
  } from "../utils/process-group";
42
43
  import { scrubSecrets } from "../utils/secret-scrubber";
44
+ import { ClaudeBackgroundKeepalive } from "./claude-background-keepalive";
43
45
  import { normalizeClaudeMessage } from "./claude-session-events";
44
46
  import { resolveSlashSkillPrompt } from "./codex-skill-resolver";
45
47
  import { CTX_MODE_NUDGE_EVERY } from "./ctx-mode-env";
@@ -738,6 +740,30 @@ export function buildClaudeCodeRuntimeEnv(
738
740
  };
739
741
  }
740
742
 
743
+ /**
744
+ * `ANTHROPIC_CUSTOM_HEADERS` ("Name: value" per line) with OpenRouter app
745
+ * attribution appended, when `ANTHROPIC_BASE_URL` points at openrouter.ai.
746
+ * The operator's lines are kept in order (they may carry gateway
747
+ * credentials); only lines naming one of our headers are replaced. Returns
748
+ * `{}` when nothing changes, so the inherited value stays byte-identical.
749
+ *
750
+ * Exported for unit testing.
751
+ */
752
+ export function claudeOpenRouterAttributionEnv(env: Record<string, string | undefined>): {
753
+ ANTHROPIC_CUSTOM_HEADERS?: string;
754
+ } {
755
+ const headers = getOpenRouterAttributionHeaders(env.ANTHROPIC_BASE_URL ?? "", env);
756
+ const names = new Set(Object.keys(headers).map((name) => name.toLowerCase()));
757
+ if (names.size === 0) return {};
758
+ const kept = (env.ANTHROPIC_CUSTOM_HEADERS ?? "").split("\n").filter((line) => {
759
+ const colon = line.indexOf(":");
760
+ const name = colon === -1 ? "" : line.slice(0, colon).trim().toLowerCase();
761
+ return line.trim() !== "" && !names.has(name);
762
+ });
763
+ const ours = Object.entries(headers).map(([name, value]) => `${name}: ${value}`);
764
+ return { ANTHROPIC_CUSTOM_HEADERS: [...kept, ...ours].join("\n") };
765
+ }
766
+
741
767
  export function buildClaudeSessionEnvironment(
742
768
  config: ProviderSessionConfig,
743
769
  model: string,
@@ -751,6 +777,7 @@ export function buildClaudeSessionEnvironment(
751
777
  env: {
752
778
  ENABLE_PROMPT_CACHING_1H: "1",
753
779
  ...sourceEnv,
780
+ ...claudeOpenRouterAttributionEnv(sourceEnv),
754
781
  ...buildClaudeCodeRuntimeEnv(sourceEnv),
755
782
  ...buildClaudeCodeOtelEnv(sourceEnv),
756
783
  ...(reasoningApplication.kind === "claude-env" ? reasoningApplication.env : {}),
@@ -878,6 +905,8 @@ class ClaudeSession implements ProviderSession {
878
905
  private lastAssistantText = "";
879
906
  /** Per-session stream-json transcript used by the parent-owned session summarizer. */
880
907
  private transcript: string[];
908
+ /** Keeps the session heartbeat fresh while Claude idles on background tasks. */
909
+ private backgroundKeepalive: ClaudeBackgroundKeepalive;
881
910
  readonly deliverSteering?: (delivery: SteerDelivery) => Promise<SteerDeliveryResult>;
882
911
 
883
912
  constructor(
@@ -899,6 +928,7 @@ class ClaudeSession implements ProviderSession {
899
928
  this.contextWindowSize = getContextWindowSize(model);
900
929
  this.systemPromptFile = systemPromptFile;
901
930
  this.transcript = [`User: ${scrubSecrets(config.prompt)}`];
931
+ this.backgroundKeepalive = new ClaudeBackgroundKeepalive(config);
902
932
  const cmd = this.buildCommand();
903
933
 
904
934
  console.log(
@@ -1132,6 +1162,7 @@ class ClaudeSession implements ProviderSession {
1132
1162
  try {
1133
1163
  await Promise.all([stdoutPromise, stderrPromise]);
1134
1164
  } finally {
1165
+ this.backgroundKeepalive.stop();
1135
1166
  this.closeStdin();
1136
1167
  }
1137
1168
  await logFileHandle.end();
@@ -1200,6 +1231,7 @@ class ClaudeSession implements ProviderSession {
1200
1231
  if (json.type === "result") {
1201
1232
  this.closeStdin();
1202
1233
  }
1234
+ this.backgroundKeepalive.observe(json);
1203
1235
 
1204
1236
  const normalized = normalizeClaudeMessage(json, {
1205
1237
  taskId: this.config.taskId,
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Keeps a Claude session's heartbeat fresh while it waits on background work.
3
+ *
4
+ * A Claude session that starts a `run_in_background` Bash job (or backgrounds a
5
+ * subagent) can end its turn and sit idle until the job's task_notification
6
+ * wakes it. During that wait it makes no tool calls and streams no output, so
7
+ * neither liveness input (the PostToolUse heartbeat and session-log ingestion)
8
+ * moves `active_sessions.lastHeartbeatAt`, and the stalled-task sweep classifies
9
+ * the live session as stale after `HEARTBEAT_STALL_STALE_HB_MIN`.
10
+ *
11
+ * Claude Code emits `system/background_tasks_changed` with the full set of live
12
+ * background tasks on every membership change (REPLACE semantics). While that
13
+ * set holds at least one non-ambient task, this pings the session heartbeat on
14
+ * an interval. With no background work the timer never runs, so a hung session
15
+ * still goes stale. A background job that never settles is bounded too: the
16
+ * keepalive stops once the session has emitted nothing for
17
+ * `BACKGROUND_KEEPALIVE_MAX_IDLE_MS`.
18
+ */
19
+
20
+ /** Ping interval; well inside the 15-minute stale-session threshold. */
21
+ export const BACKGROUND_KEEPALIVE_INTERVAL_MS = 60_000;
22
+ /** Longest silent wait on background work that the keepalive will cover. */
23
+ export const BACKGROUND_KEEPALIVE_MAX_IDLE_MS = 2 * 60 * 60_000;
24
+
25
+ type BackgroundTaskEntry = { task_id?: unknown; ambient?: unknown };
26
+
27
+ export interface ClaudeBackgroundKeepaliveOptions {
28
+ apiUrl: string;
29
+ apiKey: string;
30
+ agentId: string;
31
+ taskId: string;
32
+ intervalMs?: number;
33
+ maxIdleMs?: number;
34
+ }
35
+
36
+ export class ClaudeBackgroundKeepalive {
37
+ private liveTaskIds: string[] = [];
38
+ private timer: ReturnType<typeof setInterval> | undefined;
39
+ private lastMessageAt = Date.now();
40
+ private stopped = false;
41
+ private readonly intervalMs: number;
42
+ private readonly maxIdleMs: number;
43
+
44
+ constructor(private readonly options: ClaudeBackgroundKeepaliveOptions) {
45
+ this.intervalMs = options.intervalMs ?? BACKGROUND_KEEPALIVE_INTERVAL_MS;
46
+ this.maxIdleMs = options.maxIdleMs ?? BACKGROUND_KEEPALIVE_MAX_IDLE_MS;
47
+ }
48
+
49
+ /** Feed every protocol message the Claude process emits. */
50
+ observe(message: { type?: string; subtype?: string; tasks?: unknown }): void {
51
+ if (this.stopped) return;
52
+ this.lastMessageAt = Date.now();
53
+ if (message.type === "system" && message.subtype === "background_tasks_changed") {
54
+ const tasks = Array.isArray(message.tasks) ? (message.tasks as BackgroundTaskEntry[]) : [];
55
+ // Ambient tasks (live-update watchers, skip_transcript tasks) are not
56
+ // activity; Claude Code asks hosts to leave them out of activity signals.
57
+ this.liveTaskIds = tasks
58
+ .filter((task) => task && task.ambient !== true && typeof task.task_id === "string")
59
+ .map((task) => task.task_id as string);
60
+ }
61
+ // Any message restarts a keepalive the idle cap stopped, while work is live.
62
+ if (this.liveTaskIds.length > 0) this.start();
63
+ else this.clearTimer();
64
+ }
65
+
66
+ /** Stop pinging for good; call when the session ends. */
67
+ stop(): void {
68
+ this.stopped = true;
69
+ this.liveTaskIds = [];
70
+ this.clearTimer();
71
+ }
72
+
73
+ private start(): void {
74
+ if (this.timer !== undefined) return;
75
+ if (!this.options.apiUrl || !this.options.taskId) return;
76
+ this.timer = setInterval(() => this.tick(), this.intervalMs);
77
+ }
78
+
79
+ private tick(): void {
80
+ if (Date.now() - this.lastMessageAt > this.maxIdleMs) {
81
+ this.clearTimer();
82
+ return;
83
+ }
84
+ const { apiUrl, apiKey, agentId, taskId } = this.options;
85
+ void fetch(`${apiUrl}/api/active-sessions/heartbeat/${encodeURIComponent(taskId)}`, {
86
+ method: "PUT",
87
+ headers: {
88
+ "Content-Type": "application/json",
89
+ Authorization: `Bearer ${apiKey}`,
90
+ "X-Agent-ID": agentId,
91
+ },
92
+ }).catch(() => {});
93
+ }
94
+
95
+ private clearTimer(): void {
96
+ if (this.timer === undefined) return;
97
+ clearInterval(this.timer);
98
+ this.timer = undefined;
99
+ }
100
+ }
@@ -29,6 +29,7 @@ import {
29
29
  cleanupTaskFile,
30
30
  runClaudeSessionSummary,
31
31
  } from "./claude-adapter";
32
+ import { ClaudeBackgroundKeepalive } from "./claude-background-keepalive";
32
33
  import type { ClaudeProtocolMessage } from "./claude-session-events";
33
34
  import { normalizeClaudeMessage } from "./claude-session-events";
34
35
  import type {
@@ -317,8 +318,11 @@ class ClaudeSdkSession implements ProviderSession {
317
318
  private acceptingInput = true;
318
319
  private errorTracker = new SessionErrorTracker();
319
320
  private shutdownPromise: Promise<void> | undefined;
321
+ /** Keeps the session heartbeat fresh while Claude idles on background tasks. */
322
+ private backgroundKeepalive: ClaudeBackgroundKeepalive;
320
323
 
321
324
  constructor(private options: CreateClaudeSdkSessionOptions) {
325
+ this.backgroundKeepalive = new ClaudeBackgroundKeepalive(options.config);
322
326
  this.model = options.model;
323
327
  this.contextWindowSize = getContextWindowSize(options.model);
324
328
  this.transcript = [`User: ${scrubSecrets(options.config.prompt)}`];
@@ -406,6 +410,7 @@ class ClaudeSdkSession implements ProviderSession {
406
410
 
407
411
  private handleMessage(message: SDKMessage): boolean {
408
412
  const json = message as unknown as ClaudeProtocolMessage;
413
+ this.backgroundKeepalive.observe(json);
409
414
  const normalized = normalizeClaudeMessage(json, {
410
415
  taskId: this.options.config.taskId,
411
416
  agentId: this.options.config.agentId,
@@ -514,6 +519,7 @@ class ClaudeSdkSession implements ProviderSession {
514
519
  this.emit({ type: "error", message, category: "sdk_transport" });
515
520
  }
516
521
  } finally {
522
+ this.backgroundKeepalive.stop();
517
523
  this.acceptingInput = false;
518
524
  this.input.close();
519
525
  if (this.aborted) {
@@ -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
  });