@desplega.ai/agent-swarm 1.165.0 → 1.167.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (296) hide show
  1. package/dist/acp-adapter-zpvy6321.js +34 -0
  2. package/dist/{actions-jna0b3wm.js → actions-fq2zrwtf.js} +30 -34
  3. package/dist/{amp-adapter-0eve81pz.js → amp-adapter-5gbk7501.js} +7 -7
  4. package/dist/{anthropic-messages-hst62ws9.js → anthropic-messages-13yn502s.js} +4 -4
  5. package/dist/{app-75hm5cfa.js → app-nctetn1e.js} +11 -11
  6. package/dist/{assistant-yzd51j33.js → assistant-0aq8gjvj.js} +31 -35
  7. package/dist/{azure-openai-responses-mh8tvf78.js → azure-openai-responses-9st34pfz.js} +5 -5
  8. package/dist/{boot-reembed-z158xq32.js → boot-reembed-1ze5853n.js} +14 -16
  9. package/dist/{boot-reembed-2ex2mcsh.js → boot-reembed-fmp5ww69.js} +15 -16
  10. package/dist/boot-scrub-sweep-cbp8xd4e.js +212 -0
  11. package/dist/{boot-seeds-2bmnh18p.js → boot-seeds-22tx8a53.js} +13 -12
  12. package/dist/{claude-adapter-jbj1mycz.js → claude-adapter-0jhgeh1d.js} +25 -25
  13. package/dist/{claude-managed-adapter-17ggz09t.js → claude-managed-adapter-tzwxy803.js} +5 -5
  14. package/dist/{claude-managed-setup-ptm1nzm2.js → claude-managed-setup-nrhfx5b0.js} +5 -5
  15. package/dist/{claude-sdk-session-9re1g7r2.js → claude-sdk-session-wh77rh83.js} +2632 -2614
  16. package/dist/{cli-zsh093hk.js → cli-014vns2y.js} +63 -1
  17. package/dist/{cli-qvfk83fy.js → cli-13xwcqvj.js} +2 -2
  18. package/dist/{cli-8gpbwy09.js → cli-1yrcfa5s.js} +5 -5
  19. package/dist/{cli-xtgtyafv.js → cli-577bxkf6.js} +212 -451
  20. package/dist/{cli-ma3zvh39.js → cli-5qkcdv8c.js} +6118 -5874
  21. package/dist/{cli-cwxhbf43.js → cli-5t5my0h8.js} +85 -2
  22. package/dist/{cli-zvckv750.js → cli-69egj654.js} +2 -2
  23. package/dist/{cli-9xb89xca.js → cli-6bpvv8h4.js} +9 -9
  24. package/dist/{cli-90v942q1.js → cli-6qxmjpem.js} +8 -8
  25. package/dist/{cli-9866bd07.js → cli-6vfz44sr.js} +1 -1
  26. package/dist/{cli-32bca7nr.js → cli-7avy4v8w.js} +1 -1
  27. package/dist/{cli-xb3c6aat.js → cli-7mhy3ths.js} +21 -18
  28. package/dist/{cli-q38n4bq0.js → cli-95zs84qw.js} +3 -3
  29. package/dist/cli-a35hzdve.js +33 -0
  30. package/dist/{cli-x3x0qwky.js → cli-bxqez1p0.js} +51 -23
  31. package/dist/{cli-prt9emct.js → cli-c2tpw9ck.js} +3 -3
  32. package/dist/{cli-0n4fasz0.js → cli-c8r99akr.js} +16 -16
  33. package/dist/{cli-5anb0ev3.js → cli-cexctgn5.js} +89 -26
  34. package/dist/{cli-bns5c8k9.js → cli-cf2vgn9d.js} +105 -91
  35. package/dist/{cli-ng97fjq7.js → cli-cmxkbzzs.js} +33 -7
  36. package/dist/{cli-nexwh8zg.js → cli-g7k7zbcz.js} +12 -5
  37. package/dist/{cli-mmzwwptd.js → cli-hf5s9pgf.js} +1 -1
  38. package/dist/{cli-nvvfd8bq.js → cli-jbzbr36n.js} +4091 -3793
  39. package/dist/{cli-zn2v3t8v.js → cli-jvmtxqtc.js} +27 -15
  40. package/dist/{cli-pkke6geq.js → cli-km4x66j0.js} +83 -65
  41. package/dist/cli-kw0jcfje.js +516 -0
  42. package/dist/{cli-28smfvbn.js → cli-m7vmeepg.js} +32 -19
  43. package/dist/{cli-hpryr6em.js → cli-mb75wpxf.js} +1 -1
  44. package/dist/{cli-qdv8kgz7.js → cli-mkdsy1nq.js} +14018 -3333
  45. package/dist/{cli-b0f70z24.js → cli-mn8qkd8v.js} +1 -1
  46. package/dist/{cli-420bdgw0.js → cli-mv2520nm.js} +1 -1
  47. package/dist/{cli-n9ehc28t.js → cli-mxry91sa.js} +31 -10
  48. package/dist/{cli-wfajtmf7.js → cli-mxvnnv5q.js} +596 -13
  49. package/dist/cli-nkqjj42q.js +104 -0
  50. package/dist/{cli-e7t4snpa.js → cli-p8rt8spp.js} +5 -5
  51. package/dist/{cli-gh5yqzpe.js → cli-pkj1w46r.js} +12 -7
  52. package/dist/cli-q1vnz8wv.js +96 -0
  53. package/dist/{cli-fj86xr3c.js → cli-qd97v6z2.js} +2 -2
  54. package/dist/{cli-ba66wyhy.js → cli-qzmzwegq.js} +60 -20
  55. package/dist/{cli-5b2fjjzh.js → cli-r7eeddkq.js} +14 -9
  56. package/dist/{cli-8ssjez7e.js → cli-rhy7z1rp.js} +22 -11
  57. package/dist/{cli-4mevd1hf.js → cli-s9y81s7j.js} +5 -5
  58. package/dist/{cli-59jn5twa.js → cli-tc5zec7v.js} +7 -7
  59. package/dist/{cli-ttbsse3f.js → cli-tmhsxhb8.js} +1 -1
  60. package/dist/{cli-k6xx3pve.js → cli-tx2zvn23.js} +4 -4
  61. package/dist/{cli-pjxnr1jn.js → cli-ty4bzkqr.js} +5 -5
  62. package/dist/{cli-nfygw434.js → cli-v1pwxreq.js} +55 -52
  63. package/dist/{cli-ftry7975.js → cli-v1w7pbd8.js} +3 -2
  64. package/dist/{cli-10zmp1p7.js → cli-vr5avem2.js} +27 -32
  65. package/dist/{cli-bzr0ftht.js → cli-wnb9b2g3.js} +19 -0
  66. package/dist/cli-wt7ccy4r.js +2654 -0
  67. package/dist/{cli-50eg13rj.js → cli-wydz3feg.js} +549 -321
  68. package/dist/{cli-krqm5f11.js → cli-xezn9nkh.js} +16 -13
  69. package/dist/{cli-8kt0bmgx.js → cli-xf713zz0.js} +2 -2
  70. package/dist/{cli-x7afk5wm.js → cli-xvc3q9br.js} +1 -1
  71. package/dist/{cli-9s3h836g.js → cli-xvdgasgy.js} +1 -1
  72. package/dist/{cli-j0pdrmwg.js → cli-y0kjpfm9.js} +3 -3
  73. package/dist/{cli-6zcfw0x7.js → cli-y195hv1a.js} +1 -1
  74. package/dist/{cli-amwhs8h2.js → cli-ykt8f7va.js} +22 -9
  75. package/dist/{cli-1kzfvd1z.js → cli-ymmc1zxg.js} +15 -11
  76. package/dist/{cli-xnzrzets.js → cli-ypgba1a8.js} +2 -2
  77. package/dist/{cli-bdv39kgj.js → cli-z1dqhc9y.js} +31 -20
  78. package/dist/{cli-fw6vdcq5.js → cli-zp67qg5x.js} +34 -1
  79. package/dist/{cli-qbksc292.js → cli-zy39d433.js} +2 -0
  80. package/dist/cli.js +36 -32
  81. package/dist/{cloudflare-workers-ai-system-one-tt484t61.js → cloudflare-workers-ai-system-one-sta45jf9.js} +7 -5
  82. package/dist/{codex-adapter-3qnxshje.js → codex-adapter-cgesnjkr.js} +20 -20
  83. package/dist/{codex-login-brkxhhdg.js → codex-login-g1cyt5zm.js} +2 -2
  84. package/dist/{codex-session-runner-612wnapp.js → codex-session-runner-8kd7ddhr.js} +20 -20
  85. package/dist/{commands-2ybfcxkj.js → commands-h6zaaxym.js} +11 -10
  86. package/dist/{cursor-adapter-p3jgdehv.js → cursor-adapter-v8mm6bp5.js} +7 -7
  87. package/dist/{db-f4ans0jj.js → db-qg3myn53.js} +13 -10
  88. package/dist/{devin-adapter-dhzt01ff.js → devin-adapter-bffjj5n4.js} +2 -2
  89. package/dist/{dispatcher-pk182apd.js → dispatcher-7vnwj9qw.js} +28 -32
  90. package/dist/{dsh-adapter-7twaeps1.js → dsh-adapter-tz6hfp5r.js} +7 -7
  91. package/dist/{e2b-y8t3jzyz.js → e2b-nd50eccg.js} +1 -1
  92. package/dist/{easter-egg-3d-mtpnw06s.js → easter-egg-3d-66zxervv.js} +2 -2
  93. package/dist/{event-bus-w7ac4n3c.js → event-bus-vnfthafg.js} +2 -2
  94. package/dist/{execute-s4ccjq3n.js → execute-h4jgevyw.js} +61 -11
  95. package/dist/{google-generative-ai-d6nphg3q.js → google-generative-ai-b8zxrpax.js} +5 -5
  96. package/dist/{google-vertex-cpshkref.js → google-vertex-nrdd697t.js} +5 -5
  97. package/dist/grok-adapter-jdt2dz2s.js +39 -0
  98. package/dist/{handlers-g2wnr4t9.js → handlers-2qtkpncr.js} +31 -35
  99. package/dist/{hook-f7jt9vnm.js → hook-51yfcgsv.js} +48 -39
  100. package/dist/{hook-bbdpnkb4.js → hook-qnbdgwvd.js} +15 -17
  101. package/dist/{http-w40q4w94.js → http-jsmtag08.js} +536 -231
  102. package/dist/{index-056j5kzn.js → index-2t9kesn1.js} +13 -12
  103. package/dist/{index-cj1r9q83.js → index-bt8wkv1w.js} +29 -33
  104. package/dist/{index-7n70jwbb.js → index-cx1sb9bq.js} +42 -35
  105. package/dist/{index-9bfssf54.js → index-ekywyv2v.js} +28 -32
  106. package/dist/{index-fdk1t4d6.js → index-n0rxa4ca.js} +11962 -9900
  107. package/dist/{index-k8xc083e.js → index-t4g06w93.js} +28 -32
  108. package/dist/{keepalive-1tj10tf4.js → keepalive-4aezx4bj.js} +16 -12
  109. package/dist/{lead-st0bg7v4.js → lead-r44bkpm5.js} +52 -56
  110. package/dist/{llama-cpp-classify-tgj35k8d.js → llama-cpp-classify-6erb7wwv.js} +3 -1
  111. package/dist/{maintenance-nz71zats.js → maintenance-grz5jrzh.js} +12 -13
  112. package/dist/{mistral-conversations-8445ddjn.js → mistral-conversations-059y51zt.js} +4 -4
  113. package/dist/{node-f0ykebbv.js → node-qm9eb6z0.js} +25 -7
  114. package/dist/{oauth-refresh-sweep-aefzwvvq.js → oauth-refresh-sweep-wm4ctjfj.js} +15 -11
  115. package/dist/{onboard-kgpr9mbn.js → onboard-v60zgqf0.js} +2 -2
  116. package/dist/{openai-codex-responses-9ynm88zy.js → openai-codex-responses-jzxkta9b.js} +8 -7
  117. package/dist/{openai-completions-nhqwjmaw.js → openai-completions-663yncfq.js} +4 -4
  118. package/dist/openai-decisions-vj9nkpf8.js +162 -0
  119. package/dist/{openai-responses-xx1c4kgm.js → openai-responses-m3g43rrt.js} +5 -5
  120. package/dist/{opencode-adapter-0c8e70dk.js → opencode-adapter-yjyqynzf.js} +8 -8
  121. package/dist/{openrouter-images-8yx3487g.js → openrouter-images-kr60n6b9.js} +1 -1
  122. package/dist/{otel-impl-x8zp4y5a.js → otel-impl-ghh4dyf8.js} +2 -2
  123. package/dist/{pi-messages-vxdt8v7j.js → pi-messages-xszk613n.js} +1 -1
  124. package/dist/{pi-mono-adapter-t280ah5c.js → pi-mono-adapter-0eg8kw2c.js} +69 -52
  125. package/dist/{pricing-refresh-9e0q7cwk.js → pricing-refresh-mjrqvnvw.js} +12 -11
  126. package/dist/{rbac-roles-790j3ga8.js → rbac-roles-axv4d74x.js} +11 -10
  127. package/dist/{rbac-roles-kcp6zge2.js → rbac-roles-fd5tskj9.js} +11 -10
  128. package/dist/{render-v2-tch4nefj.js → render-v2-k50j33qd.js} +11 -11
  129. package/dist/{runtime-v2t1r79e.js → runtime-h9ha1mq6.js} +4 -4
  130. package/dist/{setup-rcm303ns.js → setup-4syh0pd2.js} +2 -2
  131. package/dist/{typesafe-system-one-datfr82p.js → typesafe-system-one-e988vty7.js} +7 -5
  132. package/dist/{virtual-modules-wm8mxdxx.js → virtual-modules-ftr9kwe8.js} +11 -11
  133. package/dist/{work-objects-ctg7t70w.js → work-objects-jcdbg8rr.js} +32 -36
  134. package/dist/{worker-5dp0xm55.js → worker-x8j8c9sx.js} +52 -56
  135. package/dist/{x-v3yw66zs.js → x-ma4gnfgy.js} +2 -2
  136. package/openapi.json +5 -2
  137. package/package.json +15 -11
  138. package/src/be/automation-preflight.ts +9 -4
  139. package/src/be/boot-scrub-sweep.ts +302 -0
  140. package/src/be/crypto/key-bootstrap.ts +1 -1
  141. package/src/be/db/approvals.ts +15 -1
  142. package/src/be/db/codex-oauth-device-flows.ts +40 -0
  143. package/src/be/db/config-secrets.ts +31 -0
  144. package/src/be/db/runtime.ts +4 -0
  145. package/src/be/db/script-runs.ts +15 -0
  146. package/src/be/db/tasks/write.ts +1 -1
  147. package/src/be/db-queries/mcp-oauth.ts +5 -0
  148. package/src/be/db-queries/oauth.ts +77 -0
  149. package/src/be/db.ts +162 -59
  150. package/src/be/events.ts +17 -1
  151. package/src/be/memory/index-content.ts +2 -2
  152. package/src/be/memory/providers/openai-embedding.ts +7 -2
  153. package/src/be/memory/providers/sqlite-store.ts +63 -11
  154. package/src/be/memory/raters/llm-summarizer.ts +7 -2
  155. package/src/be/memory/types.ts +14 -0
  156. package/src/be/migrations/201_workflow_replay_columns.sql +10 -0
  157. package/src/be/migrations/202_codex_oauth_device_flows.sql +24 -0
  158. package/src/be/modelsdev-cache.json +61 -0
  159. package/src/be/scripts/db.ts +28 -4
  160. package/src/be/scripts/source-secrets.ts +37 -0
  161. package/src/be/scrub-json.ts +32 -0
  162. package/src/be/sealed-json.ts +59 -0
  163. package/src/be/secret-registry.ts +131 -0
  164. package/src/be/seed-pricing.ts +84 -53
  165. package/src/be/swarm-config-guard.ts +13 -0
  166. package/src/be/workflow-replay.ts +77 -0
  167. package/src/commands/provider-credentials.ts +28 -5
  168. package/src/commands/runner.ts +7 -12
  169. package/src/heartbeat/heartbeat.ts +91 -19
  170. package/src/hooks/hook.ts +72 -27
  171. package/src/http/codex-oauth-device.ts +8 -10
  172. package/src/http/db-query-bounded.ts +3 -0
  173. package/src/http/db-query-shared.ts +107 -15
  174. package/src/http/db-query.ts +10 -2
  175. package/src/http/index.ts +23 -6
  176. package/src/http/mcp.ts +5 -0
  177. package/src/http/onboarding.ts +2 -0
  178. package/src/http/script-runs.ts +9 -1
  179. package/src/http/scripts.ts +15 -0
  180. package/src/http/workflows.ts +3 -2
  181. package/src/oauth/mcp-wrapper.ts +1 -1
  182. package/src/providers/acp-adapter.ts +25 -11
  183. package/src/providers/acp-target-catalog.ts +35 -1
  184. package/src/providers/acp-targets.ts +78 -7
  185. package/src/providers/amp-adapter.ts +1 -1
  186. package/src/providers/claude-adapter.ts +26 -0
  187. package/src/providers/codex-adapter.ts +30 -3
  188. package/src/providers/codex-oauth/auth-json.ts +21 -4
  189. package/src/providers/codex-oauth/types.ts +2 -0
  190. package/src/providers/cursor-adapter.ts +1 -1
  191. package/src/providers/dsh-adapter.ts +10 -1
  192. package/src/providers/grok-adapter.ts +18 -2
  193. package/src/providers/opencode-adapter.ts +21 -5
  194. package/src/providers/pi-mono-extension.ts +56 -25
  195. package/src/providers/pricing-sources.md +2 -0
  196. package/src/script-workflows/supervisor.ts +11 -2
  197. package/src/scripts-runtime/eval-harness.ts +2 -12
  198. package/src/scripts-runtime/executors/quickjs-bundle.ts +152 -0
  199. package/src/scripts-runtime/executors/quickjs-prelude.ts +285 -0
  200. package/src/scripts-runtime/executors/quickjs-runner.ts +580 -0
  201. package/src/scripts-runtime/executors/quickjs-worker.ts +81 -0
  202. package/src/scripts-runtime/executors/quickjs.ts +196 -0
  203. package/src/scripts-runtime/executors/registry.ts +2 -0
  204. package/src/scripts-runtime/executors/sourcemap.ts +79 -0
  205. package/src/scripts-runtime/script-error-hints.ts +14 -0
  206. package/src/server.ts +66 -4
  207. package/src/tests/acp-adapter.test.ts +277 -12
  208. package/src/tests/boot-scrub-sweep.test.ts +390 -0
  209. package/src/tests/check-text-columns.test.ts +145 -0
  210. package/src/tests/ci-unit-test-report.test.ts +73 -0
  211. package/src/tests/claude-default-route.test.ts +75 -0
  212. package/src/tests/claude-hook-output.test.ts +233 -0
  213. package/src/tests/claude-sdk-process.fixture.ts +13 -2
  214. package/src/tests/codex-adapter-pool-auth-revalidate.test.ts +4 -2
  215. package/src/tests/codex-adapter.test.ts +166 -26
  216. package/src/tests/codex-oauth-auth-timestamps.test.ts +76 -0
  217. package/src/tests/codex-oauth-device.test.ts +23 -37
  218. package/src/tests/codex-oauth.test.ts +4 -1
  219. package/src/tests/context-window.test.ts +2 -2
  220. package/src/tests/credential-check.test.ts +51 -1
  221. package/src/tests/db-query-denylist.test.ts +202 -0
  222. package/src/tests/db-task-read-contract.test.ts +1 -0
  223. package/src/tests/db-task-write-contract.test.ts +1 -0
  224. package/src/tests/dsh-adapter.test.ts +23 -1
  225. package/src/tests/enabled-tools.test.ts +269 -0
  226. package/src/tests/entrypoint-acp-target-binary.test.ts +31 -1
  227. package/src/tests/entrypoint-codex-oauth-seed.test.ts +12 -0
  228. package/src/tests/events-scrub.test.ts +88 -0
  229. package/src/tests/fixtures/secret-corpus.ts +320 -0
  230. package/src/tests/grok-adapter.test.ts +13 -0
  231. package/src/tests/haiku-5-5-pricing.test.ts +38 -0
  232. package/src/tests/heartbeat-checklist.test.ts +158 -1
  233. package/src/tests/onboarding.test.ts +44 -0
  234. package/src/tests/opencode-adapter.test.ts +66 -3
  235. package/src/tests/opencode-plugin.test.ts +37 -0
  236. package/src/tests/openrouter-attribution-call-sites.test.ts +204 -0
  237. package/src/tests/openrouter-base-url.test.ts +30 -39
  238. package/src/tests/pi-mono-extension.test.ts +161 -2
  239. package/src/tests/pricing-refresh.test.ts +70 -1
  240. package/src/tests/reasoning-effort.test.ts +3 -1
  241. package/src/tests/script-connections.test.ts +5 -0
  242. package/src/tests/script-executor-conformance.test.ts +3 -0
  243. package/src/tests/script-executor-quickjs.test.ts +442 -0
  244. package/src/tests/script-executor-registry.test.ts +6 -1
  245. package/src/tests/script-runs-http.test.ts +88 -1
  246. package/src/tests/script-runs-scrub.test.ts +275 -0
  247. package/src/tests/scripts-http.test.ts +82 -1
  248. package/src/tests/scrubbed-text-12e.test.ts +289 -0
  249. package/src/tests/sealed-key-guard.test.ts +166 -0
  250. package/src/tests/secret-canary-sweep.test.ts +958 -0
  251. package/src/tests/secret-registry.test.ts +355 -0
  252. package/src/tests/secret-rules.test.ts +230 -0
  253. package/src/tests/secret-scrubber.test.ts +3 -2
  254. package/src/tests/telemetry-trigger.test.ts +12 -1
  255. package/src/tests/workflow-llm.test.ts +14 -0
  256. package/src/tests/workflow-replay.test.ts +518 -0
  257. package/src/tests/workflow-scrub.test.ts +242 -0
  258. package/src/tests/workflow-system-one-decision-providers.test.ts +33 -0
  259. package/src/tools/db-query.ts +1 -1
  260. package/src/tools/inject-learning.ts +1 -1
  261. package/src/tools/request-human-input.ts +7 -2
  262. package/src/tools/workflows/get-workflow-run.ts +3 -2
  263. package/src/types.ts +1 -1
  264. package/src/utils/enabled-tools.ts +27 -0
  265. package/src/utils/internal-ai/complete-structured.ts +11 -3
  266. package/src/utils/openrouter-base-url.ts +23 -0
  267. package/src/utils/secret-rules.generated.ts +2055 -0
  268. package/src/utils/secret-scrubber.ts +363 -29
  269. package/src/workflows/engine.ts +39 -4
  270. package/src/workflows/executors/raw-llm.ts +1 -0
  271. package/src/workflows/executors/system-one-decision.ts +2 -0
  272. package/src/workflows/executors/validate.ts +1 -0
  273. package/src/workflows/executors/workflow-llm.ts +9 -2
  274. package/src/workflows/recovery.ts +10 -1
  275. package/src/workflows/resume.ts +62 -3
  276. package/src/workflows/retry-poller.ts +3 -0
  277. package/templates/skills/agent-browser/SKILL.md +2 -0
  278. package/templates/skills/agent-browser/content.md +2 -0
  279. package/templates/skills/code-quality/content.md +1 -0
  280. package/templates/skills/heartbeat-runbook/content.md +1 -1
  281. package/templates/skills/work-on-task/SKILL.md +4 -0
  282. package/templates/skills/work-on-task/content.md +4 -0
  283. package/templates/skills/workflow-iterate/content.md +9 -0
  284. package/dist/acp-adapter-w6zws6me.js +0 -23
  285. package/dist/boot-scrub-logs-246yn076.js +0 -99
  286. package/dist/cli-3a4m1aws.js +0 -572
  287. package/dist/cli-3dpj7atn.js +0 -11
  288. package/dist/cli-a87b04k8.js +0 -951
  289. package/dist/cli-cvk6yzw5.js +0 -40
  290. package/dist/cli-gvcgmx69.js +0 -183
  291. package/dist/cli-h1spv8v4.js +0 -427
  292. package/dist/cli-jqt9hrnz.js +0 -168
  293. package/dist/cli-yhmwmv0g.js +0 -9281
  294. package/dist/grok-adapter-yt9pab3k.js +0 -29
  295. package/src/be/boot-scrub-logs.ts +0 -132
  296. package/dist/{codex-hook-d2rkvf39.js → codex-hook-15dgc4cb.js} +1 -1
@@ -0,0 +1,233 @@
1
+ /**
2
+ * Claude Code hook stdout contract.
3
+ *
4
+ * Claude Code parses hook stdout as JSON only when the whole stdout is one
5
+ * JSON object. A plain status line before a block made the block land as
6
+ * text, and the tool call ran anyway. Plain stdout on PreToolUse/PostToolUse
7
+ * never reaches the model, so nudges on those events must be JSON
8
+ * `additionalContext`.
9
+ *
10
+ * Each test runs the real hook as a subprocess against a fake API.
11
+ */
12
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
13
+ import { mkdtemp, rm } from "node:fs/promises";
14
+ import { tmpdir } from "node:os";
15
+ import { join } from "node:path";
16
+ import pkg from "../../package.json";
17
+ import { CHILD_PROCESS_TEST_BUDGET_MS, runChild } from "./test-proc";
18
+
19
+ const SERVER_NAME = pkg.config?.name ?? "agent-swarm";
20
+ const HOOK_PATH = join(import.meta.dir, "..", "hooks", "hook.ts");
21
+
22
+ type FakeState = {
23
+ isLead: boolean;
24
+ cancelledTaskId: string | null;
25
+ shouldBlockPolling: boolean;
26
+ };
27
+
28
+ const state: FakeState = { isLead: false, cancelledTaskId: null, shouldBlockPolling: false };
29
+ let server: ReturnType<typeof Bun.serve>;
30
+ let projectDir: string;
31
+
32
+ beforeAll(async () => {
33
+ server = Bun.serve({
34
+ port: 0,
35
+ fetch(req) {
36
+ const url = new URL(req.url);
37
+ if (url.pathname === "/me") {
38
+ return Response.json({
39
+ id: "00000000-0000-4000-8000-000000000001",
40
+ name: "probe",
41
+ isLead: state.isLead,
42
+ status: "busy",
43
+ shouldBlockPolling: state.shouldBlockPolling,
44
+ inbox: {
45
+ unreadCount: 2,
46
+ mentionsCount: 0,
47
+ offeredTasksCount: 1,
48
+ poolTasksCount: 0,
49
+ inProgressCount: 1,
50
+ recentMentions: [],
51
+ },
52
+ });
53
+ }
54
+ if (url.pathname === "/cancelled-tasks") {
55
+ const cancelled = state.cancelledTaskId
56
+ ? [{ id: state.cancelledTaskId, task: "t", failureReason: "stop now" }]
57
+ : [];
58
+ return Response.json({ cancelled });
59
+ }
60
+ return Response.json({ ok: true });
61
+ },
62
+ });
63
+ projectDir = await mkdtemp(join(tmpdir(), "claude-hook-output-"));
64
+ await Bun.write(
65
+ join(projectDir, ".mcp.json"),
66
+ JSON.stringify({
67
+ mcpServers: {
68
+ [SERVER_NAME]: {
69
+ url: `http://127.0.0.1:${server.port}/mcp`,
70
+ headers: {
71
+ Authorization: "Bearer test",
72
+ "X-Agent-ID": "00000000-0000-4000-8000-000000000001",
73
+ },
74
+ },
75
+ },
76
+ }),
77
+ );
78
+ });
79
+
80
+ afterAll(async () => {
81
+ server.stop(true);
82
+ await rm(projectDir, { recursive: true, force: true });
83
+ });
84
+
85
+ async function runHook(event: Record<string, unknown>, taskId: string) {
86
+ const taskFile = join(projectDir, `task-${taskId}.json`);
87
+ await Bun.write(
88
+ taskFile,
89
+ JSON.stringify({ taskId, agentId: "a", startedAt: new Date().toISOString() }),
90
+ );
91
+ const env: Record<string, string | undefined> = {
92
+ ...process.env,
93
+ CLAUDE_PROJECT_DIR: projectDir,
94
+ TASK_FILE: taskFile,
95
+ };
96
+ return runChild(["bun", HOOK_PATH], {
97
+ cwd: projectDir,
98
+ env,
99
+ stdin: JSON.stringify({ cwd: projectDir, ...event }),
100
+ });
101
+ }
102
+
103
+ /** Assert stdout is exactly one JSON object and return it. */
104
+ function parseSoleJson(stdout: string): Record<string, unknown> {
105
+ const trimmed = stdout.trim();
106
+ expect(trimmed.split("\n")).toHaveLength(1);
107
+ return JSON.parse(trimmed) as Record<string, unknown>;
108
+ }
109
+
110
+ const bashTouch = {
111
+ tool_name: "Bash",
112
+ tool_input: { command: "touch /tmp/probe" },
113
+ };
114
+
115
+ describe("claude hook stdout", () => {
116
+ test(
117
+ "blocked PreToolUse prints exactly one deny JSON object",
118
+ async () => {
119
+ const taskId = crypto.randomUUID();
120
+ Object.assign(state, { isLead: false, cancelledTaskId: taskId, shouldBlockPolling: false });
121
+ const result = await runHook({ hook_event_name: "PreToolUse", ...bashTouch }, taskId);
122
+ expect(result.exitCode).toBe(0);
123
+ const out = parseSoleJson(result.stdout) as {
124
+ hookSpecificOutput: Record<string, string>;
125
+ };
126
+ expect(out.hookSpecificOutput.hookEventName).toBe("PreToolUse");
127
+ expect(out.hookSpecificOutput.permissionDecision).toBe("deny");
128
+ expect(out.hookSpecificOutput.permissionDecisionReason).toContain("TASK CANCELLED");
129
+ },
130
+ CHILD_PROCESS_TEST_BUDGET_MS,
131
+ );
132
+
133
+ test(
134
+ "blocked UserPromptSubmit prints only the block JSON, no status line",
135
+ async () => {
136
+ const taskId = crypto.randomUUID();
137
+ Object.assign(state, { isLead: false, cancelledTaskId: taskId, shouldBlockPolling: false });
138
+ const result = await runHook({ hook_event_name: "UserPromptSubmit", prompt: "go" }, taskId);
139
+ expect(result.exitCode).toBe(0);
140
+ const out = parseSoleJson(result.stdout);
141
+ expect(out.decision).toBe("block");
142
+ expect(String(out.reason)).toContain("TASK CANCELLED");
143
+ },
144
+ CHILD_PROCESS_TEST_BUDGET_MS,
145
+ );
146
+
147
+ test(
148
+ "UserPromptSubmit without a block still prints the status line and tray",
149
+ async () => {
150
+ Object.assign(state, { isLead: false, cancelledTaskId: null, shouldBlockPolling: false });
151
+ const result = await runHook(
152
+ { hook_event_name: "UserPromptSubmit", prompt: "go" },
153
+ crypto.randomUUID(),
154
+ );
155
+ expect(result.stdout).toContain('You are registered as worker agent "probe"');
156
+ expect(result.stdout).toContain("📬 2 unread");
157
+ },
158
+ CHILD_PROCESS_TEST_BUDGET_MS,
159
+ );
160
+
161
+ test(
162
+ "unblocked PreToolUse prints nothing on stdout",
163
+ async () => {
164
+ Object.assign(state, { isLead: false, cancelledTaskId: null, shouldBlockPolling: false });
165
+ const result = await runHook(
166
+ { hook_event_name: "PreToolUse", ...bashTouch },
167
+ crypto.randomUUID(),
168
+ );
169
+ expect(result.exitCode).toBe(0);
170
+ expect(result.stdout.trim()).toBe("");
171
+ },
172
+ CHILD_PROCESS_TEST_BUDGET_MS,
173
+ );
174
+
175
+ test(
176
+ "polling limit blocks poll-task with one deny JSON object",
177
+ async () => {
178
+ Object.assign(state, { isLead: true, cancelledTaskId: null, shouldBlockPolling: true });
179
+ const result = await runHook(
180
+ { hook_event_name: "PreToolUse", tool_name: "mcp__agent-swarm__poll-task", tool_input: {} },
181
+ crypto.randomUUID(),
182
+ );
183
+ const out = parseSoleJson(result.stdout) as {
184
+ hookSpecificOutput: Record<string, string>;
185
+ };
186
+ expect(out.hookSpecificOutput.permissionDecision).toBe("deny");
187
+ expect(out.hookSpecificOutput.permissionDecisionReason).toContain("POLLING LIMIT");
188
+ },
189
+ CHILD_PROCESS_TEST_BUDGET_MS,
190
+ );
191
+
192
+ test(
193
+ "loop warning reaches the model as PreToolUse additionalContext",
194
+ async () => {
195
+ Object.assign(state, { isLead: false, cancelledTaskId: null, shouldBlockPolling: false });
196
+ const taskId = crypto.randomUUID();
197
+ let stdout = "";
198
+ try {
199
+ for (let i = 0; i < 14 && stdout.trim() === ""; i++) {
200
+ stdout = (await runHook({ hook_event_name: "PreToolUse", ...bashTouch }, taskId)).stdout;
201
+ }
202
+ } finally {
203
+ await rm(`/tmp/agent-swarm-tool-history/${taskId}.json`, { force: true });
204
+ }
205
+ const out = parseSoleJson(stdout) as { hookSpecificOutput: Record<string, string> };
206
+ expect(out.hookSpecificOutput.hookEventName).toBe("PreToolUse");
207
+ expect(out.hookSpecificOutput.additionalContext).toStartWith("Warning: ");
208
+ },
209
+ CHILD_PROCESS_TEST_BUDGET_MS * 3,
210
+ );
211
+
212
+ test(
213
+ "lead send-task reminder reaches the model as PostToolUse additionalContext",
214
+ async () => {
215
+ Object.assign(state, { isLead: true, cancelledTaskId: null, shouldBlockPolling: false });
216
+ const result = await runHook(
217
+ {
218
+ hook_event_name: "PostToolUse",
219
+ tool_name: "mcp__agent-swarm__send-task",
220
+ tool_input: {},
221
+ tool_response: { task: { id: "task-123" } },
222
+ },
223
+ crypto.randomUUID(),
224
+ );
225
+ const out = parseSoleJson(result.stdout) as {
226
+ hookSpecificOutput: Record<string, string>;
227
+ };
228
+ expect(out.hookSpecificOutput.hookEventName).toBe("PostToolUse");
229
+ expect(out.hookSpecificOutput.additionalContext).toContain("Task ID: task-123.");
230
+ },
231
+ CHILD_PROCESS_TEST_BUDGET_MS,
232
+ );
233
+ });
@@ -1,3 +1,5 @@
1
+ import { rename } from "node:fs/promises";
2
+
1
3
  const mode = process.env.CLAUDE_SDK_FIXTURE_MODE;
2
4
  const stateFile = process.env.CLAUDE_SDK_FIXTURE_STATE_FILE;
3
5
  const sessionId = "11111111-1111-4111-8111-111111111111";
@@ -13,6 +15,15 @@ if (!mode || !stateFile) {
13
15
  process.exit(2);
14
16
  }
15
17
 
18
+ // The test polls for the state file and parses it as soon as it exists.
19
+ // Bun.write creates the file before filling it, so write-then-rename keeps
20
+ // the reader from ever seeing an empty file.
21
+ async function writeStateFile(path: string, state: Record<string, unknown>): Promise<void> {
22
+ const temporaryPath = `${path}.tmp`;
23
+ await Bun.write(temporaryPath, JSON.stringify(state));
24
+ await rename(temporaryPath, path);
25
+ }
26
+
16
27
  function writeMessage(message: Record<string, unknown>): void {
17
28
  console.log(JSON.stringify(message));
18
29
  }
@@ -145,7 +156,7 @@ if (mode === "cancel") {
145
156
  stdout: "ignore",
146
157
  stderr: "ignore",
147
158
  });
148
- await Bun.write(stateFile, JSON.stringify({ fixturePid: process.pid, childPid: child.pid }));
159
+ await writeStateFile(stateFile, { fixturePid: process.pid, childPid: child.pid });
149
160
  }
150
161
 
151
162
  async function handleLine(line: string): Promise<boolean> {
@@ -191,7 +202,7 @@ async function handleLine(line: string): Promise<boolean> {
191
202
  }
192
203
 
193
204
  if (mode !== "queue" || userMessages.length < 3) return false;
194
- await Bun.write(stateFile, JSON.stringify({ argv, userMessages }));
205
+ await writeStateFile(stateFile, { argv, userMessages });
195
206
  for (let index = 0; index < userMessages.length; index++) {
196
207
  const turn = index + 1;
197
208
  writeMessage(assistantMessage(`accepted:${userMessages[index]}`, turn));
@@ -34,6 +34,7 @@
34
34
  import { afterAll, afterEach, beforeAll, describe, expect, it } from "bun:test";
35
35
  import { unlink } from "node:fs/promises";
36
36
  import { createServer, type Server } from "node:http";
37
+ import { join } from "node:path";
37
38
  import { closeDb, initDb } from "../be/db";
38
39
  import { handleOAuthLocks } from "../http/oauth-locks";
39
40
  import { resolveCodexAuthMode } from "../providers/codex-adapter.js";
@@ -45,6 +46,7 @@ const TEST_DB_PATH = "./test-codex-adapter-pool-auth-revalidate.sqlite";
45
46
  const MOCK_API_URL = "http://localhost:3013";
46
47
  const MOCK_API_KEY = "example-test-api-key";
47
48
  const FAKE_HOME = "/home/fake-pool-worker";
49
+ const FAKE_AUTH_PATH = join(FAKE_HOME, ".codex", "auth.json");
48
50
 
49
51
  process.env.SECRETS_ENCRYPTION_KEY = Buffer.alloc(32, 4).toString("base64");
50
52
 
@@ -106,14 +108,14 @@ function fakeAuthJsonFs(initialContent: string) {
106
108
  let file: string | null = initialContent;
107
109
  const fs = {
108
110
  readFile: async (path: string): Promise<string> => {
109
- if (path !== `${FAKE_HOME}/.codex/auth.json` || file === null) {
111
+ if (path !== FAKE_AUTH_PATH || file === null) {
110
112
  throw new Error("ENOENT");
111
113
  }
112
114
  return file;
113
115
  },
114
116
  mkdir: async (): Promise<undefined> => undefined,
115
117
  writeFile: async (path: string, data: string): Promise<void> => {
116
- expect(path).toBe(`${FAKE_HOME}/.codex/auth.json`);
118
+ expect(path).toBe(FAKE_AUTH_PATH);
117
119
  file = data;
118
120
  },
119
121
  };
@@ -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
+ });