@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
@@ -167,6 +167,66 @@ function makeFakeAppServer(
167
167
  };
168
168
  }
169
169
 
170
+ // Cost JSON (minus durationMs) the pre-split adapter (origin/main 97deb6c6)
171
+ // produced for the notification sequence in the "unchanged by the context
172
+ // split" test.
173
+ const PRE_SPLIT_COST_JSON =
174
+ '{"sessionId":"","taskId":"","agentId":"agent-test","totalCostUsd":0.3675,"inputTokens":390000,"outputTokens":7500,"reasoningOutputTokens":1900,"cacheReadTokens":320000,"cacheWriteTokens":3000,"numTurns":2,"model":"gpt-5.4","isError":false,"provider":"codex"}';
175
+
176
+ type AppServerTokenUsage = {
177
+ total: Record<string, number>;
178
+ last?: Record<string, number>;
179
+ modelContextWindow?: number | null;
180
+ };
181
+
182
+ function breakdown(input: number, cached: number, output: number, reasoning = 0, cacheWrite = 0) {
183
+ return {
184
+ inputTokens: input,
185
+ cachedInputTokens: cached,
186
+ cacheWriteInputTokens: cacheWrite,
187
+ outputTokens: output,
188
+ reasoningOutputTokens: reasoning,
189
+ totalTokens: input + output,
190
+ };
191
+ }
192
+
193
+ /**
194
+ * Run an app-server session where each turn sends one
195
+ * `thread/tokenUsage/updated` notification per model request. The first turn
196
+ * is the initial prompt; later turns are queued follow-ups.
197
+ */
198
+ async function runAppServerTurns(
199
+ turns: AppServerTokenUsage[][],
200
+ ): Promise<{ emitted: ProviderEvent[]; result: ProviderResult }> {
201
+ const threadId = "thread-token-usage";
202
+ let turnNumber = 0;
203
+ const fake = makeFakeAppServer((method, _params, emit) => {
204
+ if (method === "thread/start") return { thread: { id: threadId } };
205
+ if (method !== "turn/start") throw new Error(`Unexpected method: ${method}`);
206
+ const turnId = `turn-${++turnNumber}`;
207
+ const updates = turns[turnNumber - 1] ?? [];
208
+ queueMicrotask(() => {
209
+ emit("turn/started", { threadId, turn: { id: turnId, status: "inProgress" } });
210
+ for (const tokenUsage of updates) {
211
+ emit("thread/tokenUsage/updated", { threadId, turnId, tokenUsage });
212
+ }
213
+ emit("turn/completed", { threadId, turn: { id: turnId, status: "completed", error: null } });
214
+ });
215
+ return { turn: { id: turnId } };
216
+ });
217
+ const session = await new CodexAdapter({
218
+ bypassSubprocess: true,
219
+ appServerFactory: () => fake.client as never,
220
+ }).createSession(testConfig({ taskId: "", apiUrl: "", apiKey: "" }));
221
+ const emitted: ProviderEvent[] = [];
222
+ session.onEvent((event) => emitted.push(event));
223
+ for (let i = 1; i < turns.length; i++) {
224
+ await session.deliverSteering?.({ mode: "queue", text: `follow-up ${i}` });
225
+ }
226
+ const result = await session.waitForCompletion();
227
+ return { emitted, result };
228
+ }
229
+
170
230
  /**
171
231
  * Because `CodexSession` is not exported, we load the module source and
172
232
  * instantiate it via `eval` of a small helper module. This is brittle but
@@ -431,6 +491,88 @@ describe("Codex app-server session", () => {
431
491
  ).toMatchObject({ toolName: "Write" });
432
492
  });
433
493
 
494
+ test("context follows the latest request, not the turn's summed usage", async () => {
495
+ // Two requests in one turn. The cumulative total (1.3M input) exceeds the
496
+ // 1.05M gpt-5.4 window; neither single request does.
497
+ const { emitted } = await runAppServerTurns([
498
+ [
499
+ {
500
+ total: breakdown(600_000, 500_000, 1_000),
501
+ last: breakdown(600_000, 500_000, 1_000),
502
+ },
503
+ {
504
+ total: breakdown(1_300_000, 1_100_000, 3_000),
505
+ last: breakdown(700_000, 600_000, 2_000),
506
+ },
507
+ ],
508
+ ]);
509
+
510
+ const snapshots = emitted.filter((event) => event.type === "context_usage");
511
+ expect(snapshots.map((event) => event.contextUsedTokens)).toEqual([601_000, 702_000]);
512
+ for (const snapshot of snapshots) {
513
+ expect(snapshot.contextUsedTokens).toBeLessThanOrEqual(702_000);
514
+ expect(snapshot.contextTotalTokens).toBe(1_050_000);
515
+ expect(snapshot.contextPercent).toBeLessThan(100);
516
+ expect(snapshot.contextFormula).toBe("input-cache-output");
517
+ }
518
+ expect(snapshots.at(-1)?.contextPercent).toBeCloseTo((702_000 / 1_050_000) * 100, 6);
519
+ });
520
+
521
+ test("per-turn cost and usage totals are unchanged by the context split", async () => {
522
+ // `last` and `modelContextWindow` must not move cost or usage totals.
523
+ const { result } = await runAppServerTurns([
524
+ [
525
+ {
526
+ total: breakdown(120_000, 90_000, 2_000, 500, 1_000),
527
+ last: breakdown(120_000, 90_000, 2_000, 500, 1_000),
528
+ modelContextWindow: 258_400,
529
+ },
530
+ {
531
+ total: breakdown(250_000, 200_000, 5_000, 1_200, 2_500),
532
+ last: breakdown(130_000, 110_000, 3_000, 700, 1_500),
533
+ modelContextWindow: 258_400,
534
+ },
535
+ ],
536
+ [
537
+ {
538
+ total: breakdown(390_000, 320_000, 7_500, 1_900, 3_000),
539
+ last: breakdown(140_000, 120_000, 2_500, 700, 500),
540
+ modelContextWindow: 258_400,
541
+ },
542
+ ],
543
+ ]);
544
+
545
+ const { durationMs: _durationMs, ...cost } = result.cost ?? ({} as Record<string, unknown>);
546
+ expect(JSON.stringify(cost)).toBe(PRE_SPLIT_COST_JSON);
547
+ });
548
+
549
+ test("modelContextWindow from the app-server wins over the models.dev fallback", async () => {
550
+ const { emitted } = await runAppServerTurns([
551
+ [
552
+ {
553
+ total: breakdown(50_000, 40_000, 1_000),
554
+ last: breakdown(50_000, 40_000, 1_000),
555
+ modelContextWindow: 258_400,
556
+ },
557
+ {
558
+ total: breakdown(110_000, 90_000, 2_000),
559
+ last: breakdown(60_000, 50_000, 1_000),
560
+ modelContextWindow: null,
561
+ },
562
+ ],
563
+ ]);
564
+
565
+ const snapshots = emitted.filter((event) => event.type === "context_usage");
566
+ expect(snapshots).toHaveLength(2);
567
+ expect(snapshots[0]).toMatchObject({ contextUsedTokens: 51_000, contextTotalTokens: 258_400 });
568
+ expect(snapshots[0]?.contextPercent).toBeCloseTo((51_000 / 258_400) * 100, 6);
569
+ // A null window falls back to the models.dev window for the model.
570
+ expect(snapshots[1]).toMatchObject({
571
+ contextUsedTokens: 61_000,
572
+ contextTotalTokens: 1_050_000,
573
+ });
574
+ });
575
+
434
576
  test("abort uses turn/interrupt and scrubs complete provider events", async () => {
435
577
  const requests: string[] = [];
436
578
  const fake = makeFakeAppServer((method, _params, emit) => {
@@ -550,6 +692,13 @@ describe("Codex app-server session", () => {
550
692
  outputTokens: 9,
551
693
  reasoningOutputTokens: 6,
552
694
  },
695
+ last: {
696
+ inputTokens: 30,
697
+ cachedInputTokens: 11,
698
+ cacheWriteInputTokens: 4,
699
+ outputTokens: 4,
700
+ reasoningOutputTokens: 2,
701
+ },
553
702
  },
554
703
  });
555
704
  emit("turn/completed", {
@@ -581,9 +730,10 @@ describe("Codex app-server session", () => {
581
730
  expect(
582
731
  emitted.find((event) => event.type === "custom" && event.name === "codex.reasoning"),
583
732
  ).toMatchObject({ data: { text: "First thought\nSecond thought" } });
584
- expect(emitted.find((event) => event.type === "context_usage")).toMatchObject({
585
- contextUsedTokens: 53,
586
- });
733
+ // Context comes from `last` (30 + 4), not the turn total (44 + 9).
734
+ expect(emitted.filter((event) => event.type === "context_usage")).toEqual([
735
+ expect.objectContaining({ contextUsedTokens: 34 }),
736
+ ]);
587
737
  });
588
738
 
589
739
  test("keeps failed-turn usage and treats retrying errors as diagnostics", async () => {
@@ -701,6 +851,10 @@ describe("CodexSession event mapping", () => {
701
851
  { type: "thread.started", thread_id: "thread-abc" },
702
852
  { type: "turn.started" },
703
853
  { type: "item.completed", item: agentMsg as ThreadItem },
854
+ {
855
+ type: "context.updated",
856
+ usage: { input_tokens: 100, cached_input_tokens: 25, output_tokens: 50 },
857
+ },
704
858
  {
705
859
  type: "turn.completed",
706
860
  usage: { input_tokens: 100, cached_input_tokens: 25, output_tokens: 50 },
@@ -808,15 +962,9 @@ describe("CodexSession event mapping", () => {
808
962
  }
809
963
  });
810
964
 
811
- test("Phase 9: chatty turn uses the models.dev context window under the unified formula", async () => {
812
- // Phase 9 deliberately swapped Codex's per-adapter peak-proxy formula
813
- // (`(input - cached) + output`) for the unified `input + output` formula
814
- // shared with every other provider. The trade-off: a chatty Codex turn
815
- // — where `input_tokens` is the SUM across every model call in the turn
816
- // — over-reports compared to the peak-proxy variant. The clamp at 100%
817
- // keeps the gauge sensible; downstream consumers reading the new
818
- // `contextFormula='input-cache-output'` tag know it's apples-to-apples
819
- // across providers. Numbers below are from the verify-plan transcript.
965
+ test("a chatty turn's summed usage is billed but never reported as context", async () => {
966
+ // A turn's usage sums every model request in it, so it can exceed the
967
+ // window. Only `context.updated` (one request) may become context_usage.
820
968
  const agentMsg: AgentMessageItem = {
821
969
  id: "msg-1",
822
970
  type: "agent_message",
@@ -829,9 +977,9 @@ describe("CodexSession event mapping", () => {
829
977
  {
830
978
  type: "turn.completed",
831
979
  usage: {
832
- input_tokens: 357142, // total exceeded the old hardcoded 200k window
833
- cached_input_tokens: 278912, // most of input is cache reuse
834
- output_tokens: 2156,
980
+ input_tokens: 4_850_000, // summed across the turn, above the 1.05M window
981
+ cached_input_tokens: 4_700_000,
982
+ output_tokens: 54_343,
835
983
  },
836
984
  },
837
985
  ];
@@ -843,22 +991,14 @@ describe("CodexSession event mapping", () => {
843
991
 
844
992
  const { emitted } = await runSessionWithFakeThread(events, config);
845
993
 
846
- const contextUsage = emitted.find((e) => e.type === "context_usage");
847
- expect(contextUsage).toBeDefined();
848
- if (contextUsage && contextUsage.type === "context_usage") {
849
- // Phase 9 unified: input + output = 357142 + 2156 = 359298.
850
- expect(contextUsage.contextUsedTokens).toBe(359298);
851
- expect(contextUsage.contextTotalTokens).toBe(1_050_000);
852
- expect(contextUsage.contextPercent).toBeCloseTo((359298 / 1_050_000) * 100, 6);
853
- expect(contextUsage.contextFormula).toBe("input-cache-output");
854
- }
994
+ expect(emitted.some((e) => e.type === "context_usage")).toBe(false);
855
995
 
856
996
  // Cost still uses the full input_tokens — billing semantics are
857
997
  // preserved (cached portion gets the cached rate, uncached gets full).
858
998
  const resultEvent = emitted.findLast((e) => e.type === "result");
859
999
  if (resultEvent && resultEvent.type === "result") {
860
- expect(resultEvent.cost.inputTokens).toBe(357142);
861
- expect(resultEvent.cost.cacheReadTokens).toBe(278912);
1000
+ expect(resultEvent.cost.inputTokens).toBe(4_850_000);
1001
+ expect(resultEvent.cost.cacheReadTokens).toBe(4_700_000);
862
1002
  expect(resultEvent.cost.totalCostUsd).toBeGreaterThan(0);
863
1003
  }
864
1004
  });
@@ -0,0 +1,76 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { codexOAuthAuthJson } from "../../scripts/e2e/harness";
3
+ import { authJsonToCredentials, credentialsToAuthJson } from "../providers/codex-oauth/auth-json";
4
+ import type { CodexOAuthCredentials } from "../providers/codex-oauth/types";
5
+
6
+ const credentials: CodexOAuthCredentials = {
7
+ access: "example-access-token",
8
+ refresh: "example-refresh-token",
9
+ accountId: "example-account",
10
+ expires: Date.UTC(2099, 0, 1),
11
+ };
12
+
13
+ describe("Codex auth.json timestamps", () => {
14
+ test("records the write time separately from the access-token expiry", () => {
15
+ const before = Date.now();
16
+ const auth = credentialsToAuthJson(credentials);
17
+ const after = Date.now();
18
+
19
+ expect(Date.parse(auth.last_refresh)).toBeGreaterThanOrEqual(before);
20
+ expect(Date.parse(auth.last_refresh)).toBeLessThanOrEqual(after);
21
+ expect(authJsonToCredentials(auth).expires).toBe(credentials.expires);
22
+ });
23
+
24
+ test("reads expiry from a native CLI JWT instead of last_refresh", () => {
25
+ const expiresSeconds = 1_800_000_000;
26
+ const access = `header.${btoa(JSON.stringify({ exp: expiresSeconds }))}.signature`;
27
+ const auth = {
28
+ auth_mode: "chatgpt" as const,
29
+ OPENAI_API_KEY: null,
30
+ tokens: {
31
+ id_token: access,
32
+ access_token: access,
33
+ refresh_token: credentials.refresh,
34
+ account_id: credentials.accountId,
35
+ },
36
+ last_refresh: "2026-10-01T00:00:00.000Z",
37
+ };
38
+
39
+ expect(authJsonToCredentials(auth).expires).toBe(expiresSeconds * 1000);
40
+ });
41
+
42
+ test("prefers the current JWT over metadata left from an older token", () => {
43
+ const auth = credentialsToAuthJson(credentials);
44
+ auth.tokens.access_token = `header.${btoa(JSON.stringify({ exp: 1_800_000_000 }))}.signature`;
45
+ expect(authJsonToCredentials(auth).expires).toBe(1_800_000_000_000);
46
+ });
47
+
48
+ test("retains the explicit expiry when a JWT has no usable exp claim", () => {
49
+ for (const exp of [undefined, "1800000000", null, -1]) {
50
+ const access = `header.${btoa(JSON.stringify({ exp }))}.signature`;
51
+ const auth = credentialsToAuthJson({ ...credentials, access });
52
+ expect(authJsonToCredentials(auth).expires).toBe(credentials.expires);
53
+ }
54
+ });
55
+
56
+ test("keeps the legacy expiry-in-last_refresh fallback for opaque tokens", () => {
57
+ const auth = credentialsToAuthJson(credentials);
58
+ delete auth.expires;
59
+ auth.last_refresh = new Date(credentials.expires).toISOString();
60
+ expect(authJsonToCredentials(auth).expires).toBe(credentials.expires);
61
+ });
62
+
63
+ test("preserves expiry without exposing a pool refresh token", () => {
64
+ const auth = credentialsToAuthJson(credentials, { includeRefreshToken: false });
65
+ expect(auth.tokens.refresh_token).toBe("");
66
+ expect(authJsonToCredentials(auth).expires).toBe(credentials.expires);
67
+ });
68
+
69
+ test("seeds the black-box harness with separate refresh and expiry timestamps", () => {
70
+ const before = Date.now();
71
+ const auth = JSON.parse(codexOAuthAuthJson(JSON.stringify(credentials)));
72
+ expect(Date.parse(auth.last_refresh)).toBeGreaterThanOrEqual(before);
73
+ expect(Date.parse(auth.last_refresh)).toBeLessThanOrEqual(Date.now());
74
+ expect(authJsonToCredentials(auth)).toEqual(credentials);
75
+ });
76
+ });
@@ -355,6 +355,7 @@ describe("credentialsToAuthJson", () => {
355
355
  accountId: "c724a178-abc",
356
356
  };
357
357
 
358
+ const before = Date.now();
358
359
  const authJson = credentialsToAuthJson(creds);
359
360
  expect(authJson.auth_mode).toBe("chatgpt");
360
361
  expect(authJson.OPENAI_API_KEY).toBeNull();
@@ -362,7 +363,9 @@ describe("credentialsToAuthJson", () => {
362
363
  expect(authJson.tokens.refresh_token).toBe(creds.refresh);
363
364
  expect(authJson.tokens.account_id).toBe(creds.accountId);
364
365
  expect(authJson.tokens.id_token).toBe(creds.access);
365
- expect(authJson.last_refresh).toBe(new Date(creds.expires).toISOString());
366
+ expect(Date.parse(authJson.last_refresh)).toBeGreaterThanOrEqual(before);
367
+ expect(Date.parse(authJson.last_refresh)).toBeLessThanOrEqual(Date.now());
368
+ expect(authJson.expires).toBe(creds.expires);
366
369
  });
367
370
  });
368
371
 
@@ -9,6 +9,7 @@ import {
9
9
  } from "../be/db";
10
10
  import { getOAuthApp, upsertAuthorization, upsertOAuthApp } from "../be/db-queries/oauth";
11
11
  import {
12
+ deleteScriptConnection,
12
13
  getScriptApiConnectionDescriptors,
13
14
  getScriptConnectionById,
14
15
  listRelationalCredentialBindings,
@@ -718,3 +719,53 @@ describe("embedded connection auth", () => {
718
719
  }
719
720
  });
720
721
  });
722
+
723
+ describe("deleteScriptConnection", () => {
724
+ test("removes the connection, its managed binding, and its derived inline secret", async () => {
725
+ createdConfigKeys.push("connection.deleteVendor.secret");
726
+ const connection = await upsertScriptConnection({
727
+ slug: "deleteVendor",
728
+ kind: "graphql",
729
+ baseUrl: "https://api.vendor.test/graphql",
730
+ allowedHosts: ["api.vendor.test"],
731
+ auth: { type: "bearer", secret: "example-delete-tok-123" },
732
+ });
733
+ createdConnectionIds.push(connection.id);
734
+ expect(await managedBindingFor(connection.id)).not.toBeNull();
735
+ expect(await getSwarmConfigs({ key: "connection.deleteVendor.secret" })).toHaveLength(1);
736
+
737
+ const deleted = await deleteScriptConnection(connection.id);
738
+
739
+ expect(deleted?.id).toBe(connection.id);
740
+ expect(await getScriptConnectionById(connection.id)).toBeNull();
741
+ expect(await managedBindingFor(connection.id)).toBeNull();
742
+ expect(await getSwarmConfigs({ key: "connection.deleteVendor.secret" })).toHaveLength(0);
743
+ });
744
+
745
+ test("keeps a shared configKey secret the connection does not own", async () => {
746
+ createdConfigKeys.push("SHARED_DELETE_VENDOR_KEY");
747
+ await upsertSwarmConfig({
748
+ scope: "global",
749
+ key: "SHARED_DELETE_VENDOR_KEY",
750
+ value: "example-shared-tok",
751
+ isSecret: true,
752
+ });
753
+ const connection = await upsertScriptConnection({
754
+ slug: "sharedDeleteVendor",
755
+ kind: "graphql",
756
+ baseUrl: "https://api.vendor.test/graphql",
757
+ allowedHosts: ["api.vendor.test"],
758
+ auth: { type: "bearer", configKey: "SHARED_DELETE_VENDOR_KEY" },
759
+ });
760
+ createdConnectionIds.push(connection.id);
761
+
762
+ expect((await deleteScriptConnection(connection.id))?.id).toBe(connection.id);
763
+
764
+ expect(await managedBindingFor(connection.id)).toBeNull();
765
+ expect(await getSwarmConfigs({ key: "SHARED_DELETE_VENDOR_KEY" })).toHaveLength(1);
766
+ });
767
+
768
+ test("returns null for an unknown id", async () => {
769
+ expect(await deleteScriptConnection("00000000-0000-4000-8000-000000000000")).toBeNull();
770
+ });
771
+ });
@@ -32,9 +32,9 @@ describe("getContextWindowSize", () => {
32
32
  expect(getContextWindowSize("sonnet")).toBe(1_000_000);
33
33
  });
34
34
 
35
- test("returns 200K for haiku models", () => {
35
+ test("returns 200K for Haiku 4.5 and 1M for the haiku shortname (Haiku 5.5)", () => {
36
36
  expect(getContextWindowSize("claude-haiku-4-5")).toBe(200_000);
37
- expect(getContextWindowSize("haiku")).toBe(200_000);
37
+ expect(getContextWindowSize("haiku")).toBe(1_000_000);
38
38
  });
39
39
 
40
40
  test("reads OpenAI windows from the catalog", () => {
@@ -1,6 +1,7 @@
1
1
  import { describe, expect, mock, test } from "bun:test";
2
2
  import {
3
3
  buildCredStatusReport,
4
+ CREDENTIAL_PROVIDER_CHECKERS,
4
5
  checkProviderCredentials,
5
6
  isBedrockMode,
6
7
  isCredCheckDisabled,
@@ -8,6 +9,7 @@ import {
8
9
  shouldRefreshBedrockStatus,
9
10
  validateProviderCredentials,
10
11
  } from "../commands/provider-credentials";
12
+ import { ACP_TARGET_IDS } from "../providers/acp-target-catalog";
11
13
  import { checkClaudeCredentials } from "../providers/claude-adapter";
12
14
  import { checkClaudeManagedCredentials } from "../providers/claude-managed-adapter";
13
15
  import { checkCodexCredentials } from "../providers/codex-adapter";
@@ -660,6 +662,54 @@ describe("checkOpencodeCredentials", () => {
660
662
  });
661
663
  });
662
664
 
665
+ describe("ACP readiness", () => {
666
+ test.each(["typo", "", " \t "])("rejects unsupported target %j", async (target) => {
667
+ const status = await checkProviderCredentials("acp", {
668
+ ACP_TARGET: target,
669
+ ACP_TARGET_COMMAND: "custom-acp --stdio",
670
+ });
671
+ expect(status.ready).toBe(false);
672
+ expect(status.missing).toEqual(["ACP_TARGET"]);
673
+ expect(status.hint).toContain("ACP_TARGET");
674
+ for (const supported of ACP_TARGET_IDS) expect(status.hint).toContain(supported);
675
+ });
676
+
677
+ test.each([undefined, "custom"])("requires a command for target %s", async (target) => {
678
+ const status = await CREDENTIAL_PROVIDER_CHECKERS.acp({ ACP_TARGET: target });
679
+ expect(status.ready).toBe(false);
680
+ expect(status.missing).toEqual(["ACP_TARGET_COMMAND"]);
681
+ expect(status.hint).toContain("ACP_TARGET_COMMAND");
682
+ expect(status.hint).toContain("ACP_COMMAND");
683
+ });
684
+
685
+ test.each(["", " \t "])("rejects a blank custom command %j", async (command) => {
686
+ const status = await checkProviderCredentials("acp", {
687
+ ACP_TARGET: "custom",
688
+ ACP_TARGET_COMMAND: command,
689
+ ACP_COMMAND: "legacy-acp",
690
+ });
691
+ expect(status.ready).toBe(false);
692
+ expect(status.missing).toEqual(["ACP_TARGET_COMMAND"]);
693
+ });
694
+
695
+ test.each(["ACP_TARGET_COMMAND", "ACP_COMMAND"])("accepts a command from %s", async (key) => {
696
+ for (const target of [undefined, "custom"]) {
697
+ const status = await checkProviderCredentials("acp", {
698
+ ACP_TARGET: target,
699
+ [key]: "custom-acp --stdio",
700
+ });
701
+ expect(status).toEqual({ ready: true, missing: [], satisfiedBy: "sdk-delegated" });
702
+ }
703
+ });
704
+
705
+ test.each(
706
+ ACP_TARGET_IDS.filter((target) => target !== "custom"),
707
+ )("keeps preset %s ready without a custom command", async (target) => {
708
+ const status = await checkProviderCredentials("acp", { ACP_TARGET: target });
709
+ expect(status).toEqual({ ready: true, missing: [], satisfiedBy: "sdk-delegated" });
710
+ });
711
+ });
712
+
663
713
  // ─── dispatcher ──────────────────────────────────────────────────────────────
664
714
 
665
715
  describe("checkProviderCredentials dispatcher", () => {
@@ -720,7 +770,7 @@ describe("checkProviderCredentials dispatcher", () => {
720
770
  ).ready,
721
771
  ).toBe(true);
722
772
 
723
- const acpStatus = await checkProviderCredentials("acp", {});
773
+ const acpStatus = await checkProviderCredentials("acp", { ACP_TARGET: "opencode" });
724
774
  expect(acpStatus.ready).toBe(true);
725
775
  expect(acpStatus.satisfiedBy).toBe("sdk-delegated");
726
776
  });
@@ -78,7 +78,7 @@ describe("docs API index footprint", () => {
78
78
  expect(tasks).toContain('"path":"/api/task-templates","method":"get"');
79
79
  });
80
80
 
81
- test("OpenAPI and generated pages keep all 399 operations", async () => {
81
+ test("OpenAPI and generated pages keep all 400 operations", async () => {
82
82
  const spec = await Bun.file(resolve(repoRoot, "openapi.json")).json();
83
83
  const operationKeys = [];
84
84
  const tags = new Set();
@@ -103,8 +103,8 @@ describe("docs API index footprint", () => {
103
103
  expect(spec.paths["/api/agents/{id}/runtime"]?.get).toBeDefined();
104
104
 
105
105
  const generatedKeys = await generatedOperationKeys();
106
- expect(operationKeys).toHaveLength(399);
107
- expect(generatedKeys).toHaveLength(399);
106
+ expect(operationKeys).toHaveLength(400);
107
+ expect(generatedKeys).toHaveLength(400);
108
108
  expect(new Set(generatedKeys)).toEqual(new Set(operationKeys));
109
109
  });
110
110
 
@@ -9,7 +9,10 @@ import type { ProviderEvent, ProviderSessionConfig } from "../providers/types";
9
9
  import { DEFAULT_MODEL_TIER_MAP } from "../types";
10
10
  import { getModelAwareCredentialVars } from "../utils/credentials";
11
11
  import { resolveHarnessProvider } from "../utils/harness-provider";
12
- import { DEFAULT_OPENROUTER_BASE_URL } from "../utils/openrouter-base-url";
12
+ import {
13
+ DEFAULT_OPENROUTER_BASE_URL,
14
+ OPENROUTER_APP_ATTRIBUTION_HEADERS,
15
+ } from "../utils/openrouter-base-url";
13
16
 
14
17
  const directories: string[] = [];
15
18
  afterEach(async () => {
@@ -166,6 +169,7 @@ describe("dsh harness", () => {
166
169
  openrouter: {
167
170
  apiKeyEnv: "OPENROUTER_API_KEY",
168
171
  baseURL: DEFAULT_OPENROUTER_BASE_URL,
172
+ headers: OPENROUTER_APP_ATTRIBUTION_HEADERS,
169
173
  api: "openai-completions",
170
174
  models: [{ id: modelId }],
171
175
  },
@@ -316,6 +320,24 @@ describe("dsh harness", () => {
316
320
  model: "deepseek/deepseek-v4.1-flash",
317
321
  });
318
322
  expect(patch[2].config.providers.openrouter.baseURL).toBe("https://gateway.example/proxy/v1");
323
+ // A gateway gets no OpenRouter app attribution.
324
+ expect(patch[2].config.providers.openrouter).not.toHaveProperty("headers");
325
+ });
326
+
327
+ test("OPENROUTER_APP_ATTRIBUTION=false drops the attribution headers", async () => {
328
+ const config = await fixture();
329
+ config.model = "openrouter/vendor/new-model";
330
+ config.env = {
331
+ ...config.env,
332
+ DEEPSEEK_API_KEY: "",
333
+ OPENROUTER_API_KEY: "openrouter-test-key",
334
+ OPENROUTER_APP_ATTRIBUTION: "false",
335
+ };
336
+ const session = await new DshAdapter().createSession(config);
337
+ expect((await session.waitForCompletion()).isError).toBe(false);
338
+ const { patch } = await Bun.file(join(config.cwd, "invocation.json")).json();
339
+ expect(patch[2].config.providers.openrouter.baseURL).toBe(DEFAULT_OPENROUTER_BASE_URL);
340
+ expect(patch[2].config.providers.openrouter).not.toHaveProperty("headers");
319
341
  });
320
342
 
321
343
  test("bare models keep direct DeepSeek when both keys exist", async () => {
@@ -354,7 +376,7 @@ describe("dsh harness", () => {
354
376
  await rename(join(config.cwd, "dsh"), join(config.cwd, "npx"));
355
377
  config.env = { ...config.env, DSH_BINARY: "", PATH: config.cwd };
356
378
  await expect(new DshAdapter().createSession(config)).rejects.toThrow(
357
- "dsh CLI not found. Install @deepseek-ai/dsh@0.2.1-alpha.1",
379
+ "dsh CLI not found. Install @deepseek-ai/dsh@0.2.1-alpha.2",
358
380
  );
359
381
  expect(await Bun.file(join(config.cwd, "invocation.json")).exists()).toBe(false);
360
382
  });