@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.
- package/dist/acp-adapter-zpvy6321.js +34 -0
- package/dist/{actions-jna0b3wm.js → actions-fq2zrwtf.js} +30 -34
- package/dist/{amp-adapter-0eve81pz.js → amp-adapter-5gbk7501.js} +7 -7
- package/dist/{anthropic-messages-hst62ws9.js → anthropic-messages-13yn502s.js} +4 -4
- package/dist/{app-75hm5cfa.js → app-nctetn1e.js} +11 -11
- package/dist/{assistant-yzd51j33.js → assistant-0aq8gjvj.js} +31 -35
- package/dist/{azure-openai-responses-mh8tvf78.js → azure-openai-responses-9st34pfz.js} +5 -5
- package/dist/{boot-reembed-z158xq32.js → boot-reembed-1ze5853n.js} +14 -16
- package/dist/{boot-reembed-2ex2mcsh.js → boot-reembed-fmp5ww69.js} +15 -16
- package/dist/boot-scrub-sweep-cbp8xd4e.js +212 -0
- package/dist/{boot-seeds-2bmnh18p.js → boot-seeds-22tx8a53.js} +13 -12
- package/dist/{claude-adapter-jbj1mycz.js → claude-adapter-0jhgeh1d.js} +25 -25
- package/dist/{claude-managed-adapter-17ggz09t.js → claude-managed-adapter-tzwxy803.js} +5 -5
- package/dist/{claude-managed-setup-ptm1nzm2.js → claude-managed-setup-nrhfx5b0.js} +5 -5
- package/dist/{claude-sdk-session-9re1g7r2.js → claude-sdk-session-wh77rh83.js} +2632 -2614
- package/dist/{cli-zsh093hk.js → cli-014vns2y.js} +63 -1
- package/dist/{cli-qvfk83fy.js → cli-13xwcqvj.js} +2 -2
- package/dist/{cli-8gpbwy09.js → cli-1yrcfa5s.js} +5 -5
- package/dist/{cli-xtgtyafv.js → cli-577bxkf6.js} +212 -451
- package/dist/{cli-ma3zvh39.js → cli-5qkcdv8c.js} +6118 -5874
- package/dist/{cli-cwxhbf43.js → cli-5t5my0h8.js} +85 -2
- package/dist/{cli-zvckv750.js → cli-69egj654.js} +2 -2
- package/dist/{cli-9xb89xca.js → cli-6bpvv8h4.js} +9 -9
- package/dist/{cli-90v942q1.js → cli-6qxmjpem.js} +8 -8
- package/dist/{cli-9866bd07.js → cli-6vfz44sr.js} +1 -1
- package/dist/{cli-32bca7nr.js → cli-7avy4v8w.js} +1 -1
- package/dist/{cli-xb3c6aat.js → cli-7mhy3ths.js} +21 -18
- package/dist/{cli-q38n4bq0.js → cli-95zs84qw.js} +3 -3
- package/dist/cli-a35hzdve.js +33 -0
- package/dist/{cli-x3x0qwky.js → cli-bxqez1p0.js} +51 -23
- package/dist/{cli-prt9emct.js → cli-c2tpw9ck.js} +3 -3
- package/dist/{cli-0n4fasz0.js → cli-c8r99akr.js} +16 -16
- package/dist/{cli-5anb0ev3.js → cli-cexctgn5.js} +89 -26
- package/dist/{cli-bns5c8k9.js → cli-cf2vgn9d.js} +105 -91
- package/dist/{cli-ng97fjq7.js → cli-cmxkbzzs.js} +33 -7
- package/dist/{cli-nexwh8zg.js → cli-g7k7zbcz.js} +12 -5
- package/dist/{cli-mmzwwptd.js → cli-hf5s9pgf.js} +1 -1
- package/dist/{cli-nvvfd8bq.js → cli-jbzbr36n.js} +4091 -3793
- package/dist/{cli-zn2v3t8v.js → cli-jvmtxqtc.js} +27 -15
- package/dist/{cli-pkke6geq.js → cli-km4x66j0.js} +83 -65
- package/dist/cli-kw0jcfje.js +516 -0
- package/dist/{cli-28smfvbn.js → cli-m7vmeepg.js} +32 -19
- package/dist/{cli-hpryr6em.js → cli-mb75wpxf.js} +1 -1
- package/dist/{cli-qdv8kgz7.js → cli-mkdsy1nq.js} +14018 -3333
- package/dist/{cli-b0f70z24.js → cli-mn8qkd8v.js} +1 -1
- package/dist/{cli-420bdgw0.js → cli-mv2520nm.js} +1 -1
- package/dist/{cli-n9ehc28t.js → cli-mxry91sa.js} +31 -10
- package/dist/{cli-wfajtmf7.js → cli-mxvnnv5q.js} +596 -13
- package/dist/cli-nkqjj42q.js +104 -0
- package/dist/{cli-e7t4snpa.js → cli-p8rt8spp.js} +5 -5
- package/dist/{cli-gh5yqzpe.js → cli-pkj1w46r.js} +12 -7
- package/dist/cli-q1vnz8wv.js +96 -0
- package/dist/{cli-fj86xr3c.js → cli-qd97v6z2.js} +2 -2
- package/dist/{cli-ba66wyhy.js → cli-qzmzwegq.js} +60 -20
- package/dist/{cli-5b2fjjzh.js → cli-r7eeddkq.js} +14 -9
- package/dist/{cli-8ssjez7e.js → cli-rhy7z1rp.js} +22 -11
- package/dist/{cli-4mevd1hf.js → cli-s9y81s7j.js} +5 -5
- package/dist/{cli-59jn5twa.js → cli-tc5zec7v.js} +7 -7
- package/dist/{cli-ttbsse3f.js → cli-tmhsxhb8.js} +1 -1
- package/dist/{cli-k6xx3pve.js → cli-tx2zvn23.js} +4 -4
- package/dist/{cli-pjxnr1jn.js → cli-ty4bzkqr.js} +5 -5
- package/dist/{cli-nfygw434.js → cli-v1pwxreq.js} +55 -52
- package/dist/{cli-ftry7975.js → cli-v1w7pbd8.js} +3 -2
- package/dist/{cli-10zmp1p7.js → cli-vr5avem2.js} +27 -32
- package/dist/{cli-bzr0ftht.js → cli-wnb9b2g3.js} +19 -0
- package/dist/cli-wt7ccy4r.js +2654 -0
- package/dist/{cli-50eg13rj.js → cli-wydz3feg.js} +549 -321
- package/dist/{cli-krqm5f11.js → cli-xezn9nkh.js} +16 -13
- package/dist/{cli-8kt0bmgx.js → cli-xf713zz0.js} +2 -2
- package/dist/{cli-x7afk5wm.js → cli-xvc3q9br.js} +1 -1
- package/dist/{cli-9s3h836g.js → cli-xvdgasgy.js} +1 -1
- package/dist/{cli-j0pdrmwg.js → cli-y0kjpfm9.js} +3 -3
- package/dist/{cli-6zcfw0x7.js → cli-y195hv1a.js} +1 -1
- package/dist/{cli-amwhs8h2.js → cli-ykt8f7va.js} +22 -9
- package/dist/{cli-1kzfvd1z.js → cli-ymmc1zxg.js} +15 -11
- package/dist/{cli-xnzrzets.js → cli-ypgba1a8.js} +2 -2
- package/dist/{cli-bdv39kgj.js → cli-z1dqhc9y.js} +31 -20
- package/dist/{cli-fw6vdcq5.js → cli-zp67qg5x.js} +34 -1
- package/dist/{cli-qbksc292.js → cli-zy39d433.js} +2 -0
- package/dist/cli.js +36 -32
- package/dist/{cloudflare-workers-ai-system-one-tt484t61.js → cloudflare-workers-ai-system-one-sta45jf9.js} +7 -5
- package/dist/{codex-adapter-3qnxshje.js → codex-adapter-cgesnjkr.js} +20 -20
- package/dist/{codex-login-brkxhhdg.js → codex-login-g1cyt5zm.js} +2 -2
- package/dist/{codex-session-runner-612wnapp.js → codex-session-runner-8kd7ddhr.js} +20 -20
- package/dist/{commands-2ybfcxkj.js → commands-h6zaaxym.js} +11 -10
- package/dist/{cursor-adapter-p3jgdehv.js → cursor-adapter-v8mm6bp5.js} +7 -7
- package/dist/{db-f4ans0jj.js → db-qg3myn53.js} +13 -10
- package/dist/{devin-adapter-dhzt01ff.js → devin-adapter-bffjj5n4.js} +2 -2
- package/dist/{dispatcher-pk182apd.js → dispatcher-7vnwj9qw.js} +28 -32
- package/dist/{dsh-adapter-7twaeps1.js → dsh-adapter-tz6hfp5r.js} +7 -7
- package/dist/{e2b-y8t3jzyz.js → e2b-nd50eccg.js} +1 -1
- package/dist/{easter-egg-3d-mtpnw06s.js → easter-egg-3d-66zxervv.js} +2 -2
- package/dist/{event-bus-w7ac4n3c.js → event-bus-vnfthafg.js} +2 -2
- package/dist/{execute-s4ccjq3n.js → execute-h4jgevyw.js} +61 -11
- package/dist/{google-generative-ai-d6nphg3q.js → google-generative-ai-b8zxrpax.js} +5 -5
- package/dist/{google-vertex-cpshkref.js → google-vertex-nrdd697t.js} +5 -5
- package/dist/grok-adapter-jdt2dz2s.js +39 -0
- package/dist/{handlers-g2wnr4t9.js → handlers-2qtkpncr.js} +31 -35
- package/dist/{hook-f7jt9vnm.js → hook-51yfcgsv.js} +48 -39
- package/dist/{hook-bbdpnkb4.js → hook-qnbdgwvd.js} +15 -17
- package/dist/{http-w40q4w94.js → http-jsmtag08.js} +536 -231
- package/dist/{index-056j5kzn.js → index-2t9kesn1.js} +13 -12
- package/dist/{index-cj1r9q83.js → index-bt8wkv1w.js} +29 -33
- package/dist/{index-7n70jwbb.js → index-cx1sb9bq.js} +42 -35
- package/dist/{index-9bfssf54.js → index-ekywyv2v.js} +28 -32
- package/dist/{index-fdk1t4d6.js → index-n0rxa4ca.js} +11962 -9900
- package/dist/{index-k8xc083e.js → index-t4g06w93.js} +28 -32
- package/dist/{keepalive-1tj10tf4.js → keepalive-4aezx4bj.js} +16 -12
- package/dist/{lead-st0bg7v4.js → lead-r44bkpm5.js} +52 -56
- package/dist/{llama-cpp-classify-tgj35k8d.js → llama-cpp-classify-6erb7wwv.js} +3 -1
- package/dist/{maintenance-nz71zats.js → maintenance-grz5jrzh.js} +12 -13
- package/dist/{mistral-conversations-8445ddjn.js → mistral-conversations-059y51zt.js} +4 -4
- package/dist/{node-f0ykebbv.js → node-qm9eb6z0.js} +25 -7
- package/dist/{oauth-refresh-sweep-aefzwvvq.js → oauth-refresh-sweep-wm4ctjfj.js} +15 -11
- package/dist/{onboard-kgpr9mbn.js → onboard-v60zgqf0.js} +2 -2
- package/dist/{openai-codex-responses-9ynm88zy.js → openai-codex-responses-jzxkta9b.js} +8 -7
- package/dist/{openai-completions-nhqwjmaw.js → openai-completions-663yncfq.js} +4 -4
- package/dist/openai-decisions-vj9nkpf8.js +162 -0
- package/dist/{openai-responses-xx1c4kgm.js → openai-responses-m3g43rrt.js} +5 -5
- package/dist/{opencode-adapter-0c8e70dk.js → opencode-adapter-yjyqynzf.js} +8 -8
- package/dist/{openrouter-images-8yx3487g.js → openrouter-images-kr60n6b9.js} +1 -1
- package/dist/{otel-impl-x8zp4y5a.js → otel-impl-ghh4dyf8.js} +2 -2
- package/dist/{pi-messages-vxdt8v7j.js → pi-messages-xszk613n.js} +1 -1
- package/dist/{pi-mono-adapter-t280ah5c.js → pi-mono-adapter-0eg8kw2c.js} +69 -52
- package/dist/{pricing-refresh-9e0q7cwk.js → pricing-refresh-mjrqvnvw.js} +12 -11
- package/dist/{rbac-roles-790j3ga8.js → rbac-roles-axv4d74x.js} +11 -10
- package/dist/{rbac-roles-kcp6zge2.js → rbac-roles-fd5tskj9.js} +11 -10
- package/dist/{render-v2-tch4nefj.js → render-v2-k50j33qd.js} +11 -11
- package/dist/{runtime-v2t1r79e.js → runtime-h9ha1mq6.js} +4 -4
- package/dist/{setup-rcm303ns.js → setup-4syh0pd2.js} +2 -2
- package/dist/{typesafe-system-one-datfr82p.js → typesafe-system-one-e988vty7.js} +7 -5
- package/dist/{virtual-modules-wm8mxdxx.js → virtual-modules-ftr9kwe8.js} +11 -11
- package/dist/{work-objects-ctg7t70w.js → work-objects-jcdbg8rr.js} +32 -36
- package/dist/{worker-5dp0xm55.js → worker-x8j8c9sx.js} +52 -56
- package/dist/{x-v3yw66zs.js → x-ma4gnfgy.js} +2 -2
- package/openapi.json +5 -2
- package/package.json +15 -11
- package/src/be/automation-preflight.ts +9 -4
- package/src/be/boot-scrub-sweep.ts +302 -0
- package/src/be/crypto/key-bootstrap.ts +1 -1
- package/src/be/db/approvals.ts +15 -1
- package/src/be/db/codex-oauth-device-flows.ts +40 -0
- package/src/be/db/config-secrets.ts +31 -0
- package/src/be/db/runtime.ts +4 -0
- package/src/be/db/script-runs.ts +15 -0
- package/src/be/db/tasks/write.ts +1 -1
- package/src/be/db-queries/mcp-oauth.ts +5 -0
- package/src/be/db-queries/oauth.ts +77 -0
- package/src/be/db.ts +162 -59
- package/src/be/events.ts +17 -1
- package/src/be/memory/index-content.ts +2 -2
- package/src/be/memory/providers/openai-embedding.ts +7 -2
- package/src/be/memory/providers/sqlite-store.ts +63 -11
- package/src/be/memory/raters/llm-summarizer.ts +7 -2
- package/src/be/memory/types.ts +14 -0
- package/src/be/migrations/201_workflow_replay_columns.sql +10 -0
- package/src/be/migrations/202_codex_oauth_device_flows.sql +24 -0
- package/src/be/modelsdev-cache.json +61 -0
- package/src/be/scripts/db.ts +28 -4
- package/src/be/scripts/source-secrets.ts +37 -0
- package/src/be/scrub-json.ts +32 -0
- package/src/be/sealed-json.ts +59 -0
- package/src/be/secret-registry.ts +131 -0
- package/src/be/seed-pricing.ts +84 -53
- package/src/be/swarm-config-guard.ts +13 -0
- package/src/be/workflow-replay.ts +77 -0
- package/src/commands/provider-credentials.ts +28 -5
- package/src/commands/runner.ts +7 -12
- package/src/heartbeat/heartbeat.ts +91 -19
- package/src/hooks/hook.ts +72 -27
- package/src/http/codex-oauth-device.ts +8 -10
- package/src/http/db-query-bounded.ts +3 -0
- package/src/http/db-query-shared.ts +107 -15
- package/src/http/db-query.ts +10 -2
- package/src/http/index.ts +23 -6
- package/src/http/mcp.ts +5 -0
- package/src/http/onboarding.ts +2 -0
- package/src/http/script-runs.ts +9 -1
- package/src/http/scripts.ts +15 -0
- package/src/http/workflows.ts +3 -2
- package/src/oauth/mcp-wrapper.ts +1 -1
- package/src/providers/acp-adapter.ts +25 -11
- package/src/providers/acp-target-catalog.ts +35 -1
- package/src/providers/acp-targets.ts +78 -7
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/claude-adapter.ts +26 -0
- package/src/providers/codex-adapter.ts +30 -3
- package/src/providers/codex-oauth/auth-json.ts +21 -4
- package/src/providers/codex-oauth/types.ts +2 -0
- package/src/providers/cursor-adapter.ts +1 -1
- package/src/providers/dsh-adapter.ts +10 -1
- package/src/providers/grok-adapter.ts +18 -2
- package/src/providers/opencode-adapter.ts +21 -5
- package/src/providers/pi-mono-extension.ts +56 -25
- package/src/providers/pricing-sources.md +2 -0
- package/src/script-workflows/supervisor.ts +11 -2
- package/src/scripts-runtime/eval-harness.ts +2 -12
- package/src/scripts-runtime/executors/quickjs-bundle.ts +152 -0
- package/src/scripts-runtime/executors/quickjs-prelude.ts +285 -0
- package/src/scripts-runtime/executors/quickjs-runner.ts +580 -0
- package/src/scripts-runtime/executors/quickjs-worker.ts +81 -0
- package/src/scripts-runtime/executors/quickjs.ts +196 -0
- package/src/scripts-runtime/executors/registry.ts +2 -0
- package/src/scripts-runtime/executors/sourcemap.ts +79 -0
- package/src/scripts-runtime/script-error-hints.ts +14 -0
- package/src/server.ts +66 -4
- package/src/tests/acp-adapter.test.ts +277 -12
- package/src/tests/boot-scrub-sweep.test.ts +390 -0
- package/src/tests/check-text-columns.test.ts +145 -0
- package/src/tests/ci-unit-test-report.test.ts +73 -0
- package/src/tests/claude-default-route.test.ts +75 -0
- package/src/tests/claude-hook-output.test.ts +233 -0
- package/src/tests/claude-sdk-process.fixture.ts +13 -2
- package/src/tests/codex-adapter-pool-auth-revalidate.test.ts +4 -2
- package/src/tests/codex-adapter.test.ts +166 -26
- package/src/tests/codex-oauth-auth-timestamps.test.ts +76 -0
- package/src/tests/codex-oauth-device.test.ts +23 -37
- package/src/tests/codex-oauth.test.ts +4 -1
- package/src/tests/context-window.test.ts +2 -2
- package/src/tests/credential-check.test.ts +51 -1
- package/src/tests/db-query-denylist.test.ts +202 -0
- package/src/tests/db-task-read-contract.test.ts +1 -0
- package/src/tests/db-task-write-contract.test.ts +1 -0
- package/src/tests/dsh-adapter.test.ts +23 -1
- package/src/tests/enabled-tools.test.ts +269 -0
- package/src/tests/entrypoint-acp-target-binary.test.ts +31 -1
- package/src/tests/entrypoint-codex-oauth-seed.test.ts +12 -0
- package/src/tests/events-scrub.test.ts +88 -0
- package/src/tests/fixtures/secret-corpus.ts +320 -0
- package/src/tests/grok-adapter.test.ts +13 -0
- package/src/tests/haiku-5-5-pricing.test.ts +38 -0
- package/src/tests/heartbeat-checklist.test.ts +158 -1
- package/src/tests/onboarding.test.ts +44 -0
- package/src/tests/opencode-adapter.test.ts +66 -3
- package/src/tests/opencode-plugin.test.ts +37 -0
- package/src/tests/openrouter-attribution-call-sites.test.ts +204 -0
- package/src/tests/openrouter-base-url.test.ts +30 -39
- package/src/tests/pi-mono-extension.test.ts +161 -2
- package/src/tests/pricing-refresh.test.ts +70 -1
- package/src/tests/reasoning-effort.test.ts +3 -1
- package/src/tests/script-connections.test.ts +5 -0
- package/src/tests/script-executor-conformance.test.ts +3 -0
- package/src/tests/script-executor-quickjs.test.ts +442 -0
- package/src/tests/script-executor-registry.test.ts +6 -1
- package/src/tests/script-runs-http.test.ts +88 -1
- package/src/tests/script-runs-scrub.test.ts +275 -0
- package/src/tests/scripts-http.test.ts +82 -1
- package/src/tests/scrubbed-text-12e.test.ts +289 -0
- package/src/tests/sealed-key-guard.test.ts +166 -0
- package/src/tests/secret-canary-sweep.test.ts +958 -0
- package/src/tests/secret-registry.test.ts +355 -0
- package/src/tests/secret-rules.test.ts +230 -0
- package/src/tests/secret-scrubber.test.ts +3 -2
- package/src/tests/telemetry-trigger.test.ts +12 -1
- package/src/tests/workflow-llm.test.ts +14 -0
- package/src/tests/workflow-replay.test.ts +518 -0
- package/src/tests/workflow-scrub.test.ts +242 -0
- package/src/tests/workflow-system-one-decision-providers.test.ts +33 -0
- package/src/tools/db-query.ts +1 -1
- package/src/tools/inject-learning.ts +1 -1
- package/src/tools/request-human-input.ts +7 -2
- package/src/tools/workflows/get-workflow-run.ts +3 -2
- package/src/types.ts +1 -1
- package/src/utils/enabled-tools.ts +27 -0
- package/src/utils/internal-ai/complete-structured.ts +11 -3
- package/src/utils/openrouter-base-url.ts +23 -0
- package/src/utils/secret-rules.generated.ts +2055 -0
- package/src/utils/secret-scrubber.ts +363 -29
- package/src/workflows/engine.ts +39 -4
- package/src/workflows/executors/raw-llm.ts +1 -0
- package/src/workflows/executors/system-one-decision.ts +2 -0
- package/src/workflows/executors/validate.ts +1 -0
- package/src/workflows/executors/workflow-llm.ts +9 -2
- package/src/workflows/recovery.ts +10 -1
- package/src/workflows/resume.ts +62 -3
- package/src/workflows/retry-poller.ts +3 -0
- package/templates/skills/agent-browser/SKILL.md +2 -0
- package/templates/skills/agent-browser/content.md +2 -0
- package/templates/skills/code-quality/content.md +1 -0
- package/templates/skills/heartbeat-runbook/content.md +1 -1
- package/templates/skills/work-on-task/SKILL.md +4 -0
- package/templates/skills/work-on-task/content.md +4 -0
- package/templates/skills/workflow-iterate/content.md +9 -0
- package/dist/acp-adapter-w6zws6me.js +0 -23
- package/dist/boot-scrub-logs-246yn076.js +0 -99
- package/dist/cli-3a4m1aws.js +0 -572
- package/dist/cli-3dpj7atn.js +0 -11
- package/dist/cli-a87b04k8.js +0 -951
- package/dist/cli-cvk6yzw5.js +0 -40
- package/dist/cli-gvcgmx69.js +0 -183
- package/dist/cli-h1spv8v4.js +0 -427
- package/dist/cli-jqt9hrnz.js +0 -168
- package/dist/cli-yhmwmv0g.js +0 -9281
- package/dist/grok-adapter-yt9pab3k.js +0 -29
- package/src/be/boot-scrub-logs.ts +0 -132
- 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
|
|
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
|
|
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 !==
|
|
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(
|
|
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
|
-
|
|
585
|
-
|
|
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("
|
|
812
|
-
//
|
|
813
|
-
//
|
|
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:
|
|
833
|
-
cached_input_tokens:
|
|
834
|
-
output_tokens:
|
|
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
|
-
|
|
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(
|
|
861
|
-
expect(resultEvent.cost.cacheReadTokens).toBe(
|
|
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
|
+
});
|