@desplega.ai/agent-swarm 2.7.4 → 2.7.6

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 (326) hide show
  1. package/README.md +1 -1
  2. package/dist/acp-adapter-bp91d37f.js +15 -0
  3. package/dist/{actions-gfrr9vb7.js → actions-yj0eeaqh.js} +19 -18
  4. package/dist/{amp-adapter-0s58gm46.js → amp-adapter-4pg9h34q.js} +3 -3
  5. package/dist/{app-stvq692g.js → app-909e97cr.js} +3 -3
  6. package/dist/{assistant-5s37fr9n.js → assistant-qdyb5fyy.js} +32 -23
  7. package/dist/{boot-reembed-yzy1841n.js → boot-reembed-fk0s9z18.js} +1 -1
  8. package/dist/{boot-reembed-cv82gz7y.js → boot-reembed-gs1rzrnh.js} +1 -1
  9. package/dist/{boot-scrub-sweep-8k4q95ap.js → boot-scrub-sweep-4qfd4kfx.js} +4 -4
  10. package/dist/{boot-seeds-ftt9htah.js → boot-seeds-yr00k13d.js} +4 -4
  11. package/dist/{boxd-515v1t8z.js → boxd-vdmfssd9.js} +5 -5
  12. package/dist/{claude-adapter-t00wrzeh.js → claude-adapter-qta1f1xn.js} +7 -7
  13. package/dist/{claude-managed-adapter-g6vmjccb.js → claude-managed-adapter-dvs2xw5n.js} +2 -2
  14. package/dist/{claude-managed-setup-afpa82f7.js → claude-managed-setup-sc6j4dm0.js} +3 -3
  15. package/dist/{claude-sdk-session-vj0c2azx.js → claude-sdk-session-zgrz6yvc.js} +9175 -9076
  16. package/dist/{cli-gjr2y2xk.js → cli-0agcv1v5.js} +4 -1
  17. package/dist/{cli-edmm9d9y.js → cli-0at8w34w.js} +7 -7
  18. package/dist/{cli-a81qevyg.js → cli-0qatpy8q.js} +20 -36
  19. package/dist/{cli-bcz074zx.js → cli-0yv5z2pg.js} +1 -1
  20. package/dist/{cli-a4btqqft.js → cli-1b7btd38.js} +1 -1
  21. package/dist/{cli-a2jw6ee5.js → cli-1p89xny0.js} +1 -1
  22. package/dist/{cli-v726afdm.js → cli-2pnswgh8.js} +1 -1
  23. package/dist/{cli-jkt52mgt.js → cli-386tyxxg.js} +2 -2
  24. package/dist/{cli-az0bpf8p.js → cli-3ypk9vk1.js} +3 -3
  25. package/dist/{cli-wk9md5px.js → cli-436532ck.js} +4 -4
  26. package/dist/{cli-7n5sbw0z.js → cli-4necwtsv.js} +5 -5
  27. package/dist/cli-54t00gt8.js +151 -0
  28. package/dist/{cli-655xm9ms.js → cli-583frbyr.js} +1 -1
  29. package/dist/{cli-4r12t6w6.js → cli-5c4hjzne.js} +101 -4
  30. package/dist/{cli-hf6zt5m6.js → cli-69szn7w7.js} +17 -1
  31. package/dist/{cli-rtk9g7av.js → cli-73jzwksr.js} +6 -2
  32. package/dist/{cli-jg0nfda6.js → cli-75gkpjvw.js} +52 -21
  33. package/dist/{cli-wcmnan2g.js → cli-89knwags.js} +5 -5
  34. package/dist/{cli-pwyevs29.js → cli-8bc1w0tw.js} +22 -21
  35. package/dist/{cli-t282jgab.js → cli-8snvgs5r.js} +41 -16
  36. package/dist/{cli-vqgnyh76.js → cli-98rw5dt5.js} +15 -3
  37. package/dist/{cli-9cctx1tn.js → cli-a22h291q.js} +1 -1
  38. package/dist/{cli-ycm6180s.js → cli-a7v854rf.js} +5 -3
  39. package/dist/{cli-98pke4vk.js → cli-ae7mrdf9.js} +2 -2
  40. package/dist/{cli-je8yeyc5.js → cli-bjb3z51z.js} +5 -5
  41. package/dist/{cli-mc97xy2s.js → cli-c60vetmm.js} +16 -4
  42. package/dist/{cli-j4kw3rsx.js → cli-cm5yqn35.js} +18 -0
  43. package/dist/{cli-wndv9p7g.js → cli-decfb6v8.js} +6 -6
  44. package/dist/{cli-h55y2bjc.js → cli-dhar9qay.js} +2 -2
  45. package/dist/{cli-8yy525ad.js → cli-drr5v6av.js} +2 -2
  46. package/dist/{cli-5gr1g711.js → cli-dxae2yg7.js} +2 -2
  47. package/dist/{cli-pe09p6dv.js → cli-e3r2neck.js} +8 -7
  48. package/dist/{cli-13jvmew3.js → cli-ekwks2vs.js} +7 -7
  49. package/dist/{cli-jg4p5t36.js → cli-es6va8jk.js} +121 -51
  50. package/dist/{cli-j4kt1mpb.js → cli-faxe6thg.js} +3 -3
  51. package/dist/{cli-tzrpv5sc.js → cli-ftyxyspd.js} +1 -1
  52. package/dist/cli-fw2w0kva.js +47 -0
  53. package/dist/{cli-2dfbv8qr.js → cli-fy6x31ps.js} +1 -0
  54. package/dist/{cli-aq58fk5m.js → cli-g83s9ewh.js} +7 -1
  55. package/dist/cli-gdhpcbhf.js +55 -0
  56. package/dist/{cli-82jg7fp9.js → cli-he5nxaa8.js} +1 -1
  57. package/dist/{cli-8veytvbh.js → cli-he9wchme.js} +1 -1
  58. package/dist/{cli-k76tgq03.js → cli-hgaazk69.js} +2 -2
  59. package/dist/{cli-d2pzqej1.js → cli-hr2e2fft.js} +741 -7
  60. package/dist/{cli-dae4sbgy.js → cli-jm2vfpfd.js} +2 -2
  61. package/dist/{cli-1s1dqn1j.js → cli-k2gzwsj2.js} +297 -140
  62. package/dist/{cli-b1afgtj3.js → cli-kqdg1bsb.js} +3 -3
  63. package/dist/cli-m2z8jagn.js +12078 -0
  64. package/dist/{cli-mvckssyq.js → cli-m48g6p95.js} +5 -5
  65. package/dist/{cli-bwty35gx.js → cli-mthwvjre.js} +1 -1
  66. package/dist/{cli-41z8xwj0.js → cli-nhrvpaj7.js} +34 -8
  67. package/dist/{cli-ahd90bwt.js → cli-nwj0ee2w.js} +6 -6
  68. package/dist/{cli-kvtc7aez.js → cli-p5gp8pcx.js} +44 -14
  69. package/dist/{cli-avgcz4ra.js → cli-pd7vagab.js} +436 -171
  70. package/dist/{cli-zn15c46z.js → cli-q0k1w5zd.js} +1 -1
  71. package/dist/{cli-bq2pcphn.js → cli-rmmmrjkd.js} +1 -1
  72. package/dist/{cli-2qpvmmkw.js → cli-s0qnd4wv.js} +1 -1
  73. package/dist/{cli-kg8hchyg.js → cli-sa02jsqt.js} +1 -1
  74. package/dist/{cli-1yekbyqm.js → cli-shpbx5ee.js} +283 -82
  75. package/dist/{cli-9y354g3j.js → cli-srye3vdt.js} +1 -1
  76. package/dist/{cli-byqfbnjw.js → cli-svj5090z.js} +9 -9
  77. package/dist/{cli-gcbdjt47.js → cli-vfym7wmz.js} +6 -6
  78. package/dist/{cli-v1yw3wha.js → cli-vvpx3gsx.js} +4 -4
  79. package/dist/{cli-a1fhj4n9.js → cli-w6dt9ged.js} +2 -2
  80. package/dist/{cli-1gm8jcyr.js → cli-waqbxg08.js} +5232 -68
  81. package/dist/{cli-5hjced2v.js → cli-wp9sp1qb.js} +6 -6
  82. package/dist/{cli-eny54typ.js → cli-wx3rhnm0.js} +1 -1
  83. package/dist/{cli-s0exq74r.js → cli-xbbndvjy.js} +3 -3
  84. package/dist/{cli-f255yaqn.js → cli-xds5f77d.js} +3 -3
  85. package/dist/{cli-0sbkss7h.js → cli-xnqgpshy.js} +2 -2
  86. package/dist/{cli-0fgr1w9b.js → cli-xnrwazav.js} +20 -3
  87. package/dist/{cli-gf5hbmdr.js → cli-yjdgea32.js} +3 -3
  88. package/dist/{cli-cq1pf336.js → cli-yn4kxs06.js} +124 -11
  89. package/dist/{cli-224epwwf.js → cli-yx4kfngt.js} +3 -3
  90. package/dist/{cli-eqx6k993.js → cli-yy3rhsdw.js} +47 -13
  91. package/dist/{cli-36g23z21.js → cli-zbbwez0r.js} +4 -4
  92. package/dist/{cli-ktmvb20s.js → cli-zm72anhg.js} +12 -12
  93. package/dist/cli.js +26 -25
  94. package/dist/{codex-adapter-g0gctw29.js → codex-adapter-ayd1fb6h.js} +5 -5
  95. package/dist/{codex-hook-vrzz2xhr.js → codex-hook-9efm2x1m.js} +1 -1
  96. package/dist/{codex-login-0gg46ygn.js → codex-login-y0pmff1c.js} +1 -1
  97. package/dist/{codex-session-runner-dr2cxe1c.js → codex-session-runner-jesa32xc.js} +6 -6
  98. package/dist/{commands-1p07cvff.js → commands-hanewgf2.js} +9 -3
  99. package/dist/{controller-z0s7sv6r.js → controller-dt1n3c1q.js} +9 -4
  100. package/dist/{controllers-connect-x9kvvhyk.js → controllers-connect-xx8x64n5.js} +22 -21
  101. package/dist/{cursor-adapter-m5b98pvk.js → cursor-adapter-9s4dwwdw.js} +2 -2
  102. package/dist/{db-capk2raj.js → db-3vttcbv4.js} +3 -3
  103. package/dist/{db-rsh04kh3.js → db-9dv0g1ab.js} +12 -4
  104. package/dist/{devin-adapter-yxm87fe9.js → devin-adapter-r9phbnc9.js} +1 -1
  105. package/dist/{devtools-82fzqra8.js → devtools-qxfgbsqz.js} +1 -1
  106. package/dist/{dispatcher-304q6fwa.js → dispatcher-tx9hd1sv.js} +18 -17
  107. package/dist/{docker-7rxt0rry.js → docker-c1a3gqmf.js} +91 -24
  108. package/dist/{dsh-adapter-btewem0y.js → dsh-adapter-007y4ejs.js} +2 -2
  109. package/dist/{e2b-renc0k8q.js → e2b-a19d28rz.js} +4 -4
  110. package/dist/{e2b-7fc9b9bd.js → e2b-we6hs6q5.js} +6 -6
  111. package/dist/{easter-egg-3d-fm1p3n4d.js → easter-egg-3d-rgbrqb8s.js} +1 -1
  112. package/dist/{embedding-space-s68v49ct.js → embedding-space-ny1e2940.js} +6 -6
  113. package/dist/{embeddings-4ms3604w.js → embeddings-2t386t13.js} +1 -1
  114. package/dist/{event-bus-dfjx2qdh.js → event-bus-g0ka3pbw.js} +1 -1
  115. package/dist/{execute-mcdgccjs.js → execute-pksh0ceh.js} +1 -1
  116. package/dist/{external-channel-9dxwfztn.js → external-channel-phgmhmd7.js} +2 -2
  117. package/dist/fx-adapter-fy8rr921.js +18 -0
  118. package/dist/{google-generative-ai-yvmdxyxa.js → google-generative-ai-8y3m3k4b.js} +1 -1
  119. package/dist/{google-vertex-jrtdh9gs.js → google-vertex-w3v98n7b.js} +1 -1
  120. package/dist/{grok-adapter-05ecv4h0.js → grok-adapter-q0abvpaj.js} +8 -8
  121. package/dist/handlers-by3sfxyx.js +32 -0
  122. package/dist/{heartbeat-3n94heh3.js → heartbeat-kadyek8p.js} +21 -20
  123. package/dist/{hook-aadrn69x.js → hook-5zs3msf6.js} +6 -6
  124. package/dist/{hook-zxwje86c.js → hook-9k551xnj.js} +4 -4
  125. package/dist/{http-xzxk9mst.js → http-mhz9vw1f.js} +272 -103
  126. package/dist/index-4f2pvxae.js +37 -0
  127. package/dist/index-98gz7tmp.js +30 -0
  128. package/dist/{index-5rk6rfzr.js → index-fvzke5kx.js} +1 -1
  129. package/dist/index-k08z2cak.js +33 -0
  130. package/dist/{index-yqhz88yy.js → index-pxn30mse.js} +8 -8
  131. package/dist/{index-d34f6r87.js → index-q361cg2f.js} +1 -1
  132. package/dist/{index-9j4zwcht.js → index-rvr00sg7.js} +168 -675
  133. package/dist/{index-qeb82z37.js → index-vdygbq10.js} +1 -1
  134. package/dist/{keepalive-w4aakbyk.js → keepalive-w56f32cg.js} +4 -4
  135. package/dist/lead-enq35nqp.js +70 -0
  136. package/dist/{maintenance-hbptpk6p.js → maintenance-7mdgxcea.js} +1 -1
  137. package/dist/{oauth-refresh-sweep-217semrv.js → oauth-refresh-sweep-29txy9vk.js} +3 -3
  138. package/dist/{onboard-vz2s5df3.js → onboard-kpsrkarj.js} +5 -5
  139. package/dist/{opencode-adapter-gtfd7d7v.js → opencode-adapter-j25vm3rr.js} +5 -5
  140. package/dist/{otel-impl-aefave37.js → otel-impl-6kkct7a8.js} +2 -2
  141. package/dist/{pi-mono-adapter-f8h5yseh.js → pi-mono-adapter-exhkzhn2.js} +117 -23
  142. package/dist/{pricing-refresh-m1yhh9ya.js → pricing-refresh-n2x7jh6k.js} +3 -3
  143. package/dist/{rbac-roles-rrw8twdz.js → rbac-roles-81ypx6es.js} +1 -1
  144. package/dist/{rbac-roles-svj72cdf.js → rbac-roles-b5htkjby.js} +2 -2
  145. package/dist/{render-v2-amwbnfxh.js → render-v2-qn6jmpk4.js} +3 -3
  146. package/dist/{resume-9cd6wf6z.js → resume-wd30wx23.js} +7 -7
  147. package/dist/{runtime-fp4vrx7m.js → runtime-4hsg7h60.js} +1 -1
  148. package/dist/{session-access-21t6yjyg.js → session-access-5qvfevhk.js} +4 -4
  149. package/dist/{session-source-backfill-pxbzcsc6.js → session-source-backfill-n1czavbr.js} +3 -3
  150. package/dist/{setup-4592tqz4.js → setup-dnb56qy5.js} +3 -3
  151. package/dist/{virtual-modules-gzfnpmnz.js → virtual-modules-76xxv9fs.js} +2 -2
  152. package/dist/{work-objects-t9tcf174.js → work-objects-4n21tfkm.js} +21 -20
  153. package/dist/{worker-follow-up-q32jh4yk.js → worker-follow-up-77hvzxww.js} +18 -17
  154. package/dist/worker-gg1576j3.js +70 -0
  155. package/dist/{x-b6wxntdt.js → x-vmmye1kb.js} +2 -2
  156. package/openapi.json +189 -32
  157. package/package.json +8 -7
  158. package/src/be/db/script-runs.ts +23 -0
  159. package/src/be/db/session-summaries.ts +8 -2
  160. package/src/be/db/session-tree.ts +174 -61
  161. package/src/be/db/session-visibility.ts +5 -4
  162. package/src/be/db/tasks/read.ts +122 -3
  163. package/src/be/db-client.ts +8 -5
  164. package/src/be/db.ts +23 -8
  165. package/src/be/memory/providers/sqlite-store.ts +49 -12
  166. package/src/be/migrations/259_session_summaries_open_tasks.sql +111 -0
  167. package/src/be/migrations/260_runtime_pool_cache.sql +20 -0
  168. package/src/be/migrations/262_session_visibility_indexes.sql +18 -0
  169. package/src/be/migrations/263_task_list_filter_index.sql +11 -0
  170. package/src/be/migrations/264_slack_thread_cover_index.sql +9 -0
  171. package/src/be/migrations/266_script_runs_source_task.sql +11 -0
  172. package/src/be/model-validation.ts +5 -1
  173. package/src/be/pricing-normalize.ts +7 -2
  174. package/src/be/runtime-pools/cache-cleanup.ts +109 -0
  175. package/src/be/runtime-pools/db.ts +65 -6
  176. package/src/be/runtime-pools/remote-controller.ts +5 -0
  177. package/src/be/runtime-pools/supervisor.ts +33 -2
  178. package/src/be/scripts/typecheck.ts +23 -0
  179. package/src/be/seed/schedules-seeder.ts +135 -9
  180. package/src/be/seed/workflows-seeder.ts +65 -1
  181. package/src/be/seed-pricing.ts +23 -0
  182. package/src/be/seed-scripts/catalog/task-failure-audit.ts +5 -1
  183. package/src/be/steering.ts +50 -3
  184. package/src/be/swarm-config-guard.ts +1 -0
  185. package/src/commands/controller.ts +4 -0
  186. package/src/commands/entrypoint-api-readiness.ts +74 -0
  187. package/src/commands/provider-credentials.ts +29 -2
  188. package/src/commands/runner.ts +86 -5
  189. package/src/extensions/contract-types.generated.ts +1 -1
  190. package/src/extensions/contract.ts +27 -1
  191. package/src/extensions/dispatcher.ts +4 -0
  192. package/src/http/agents.ts +1 -0
  193. package/src/http/index.ts +1 -1
  194. package/src/http/list-total-cache.ts +14 -7
  195. package/src/http/mcp-bridge.ts +4 -0
  196. package/src/http/memory.ts +38 -9
  197. package/src/http/script-runs.ts +174 -5
  198. package/src/http/status.ts +10 -2
  199. package/src/http/tasks.ts +4 -2
  200. package/src/integrations/chat-sdk/inbound.ts +15 -5
  201. package/src/integrations/chat-sdk/steering.ts +3 -1
  202. package/src/kv-reserved-namespaces.ts +2 -0
  203. package/src/prompts/base-prompt.ts +4 -1
  204. package/src/prompts/session-templates.ts +26 -0
  205. package/src/providers/acp-adapter.ts +190 -26
  206. package/src/providers/acp-targets.ts +306 -6
  207. package/src/providers/amp-adapter.ts +1 -1
  208. package/src/providers/codex-adapter.ts +9 -44
  209. package/src/providers/codex-oauth/flow.ts +24 -3
  210. package/src/providers/codex-oauth/storage.ts +56 -17
  211. package/src/providers/codex-oauth/subscription.ts +67 -0
  212. package/src/providers/fx-adapter.ts +211 -0
  213. package/src/providers/index.ts +5 -1
  214. package/src/providers/mcp-loopback-proxy.ts +94 -0
  215. package/src/providers/pi-mono-adapter.ts +98 -13
  216. package/src/providers/pricing-sources.md +8 -0
  217. package/src/providers/reasoning-effort.ts +17 -1
  218. package/src/providers/types.ts +2 -1
  219. package/src/runtime-providers/controller-protocol.ts +14 -1
  220. package/src/runtime-providers/controller.ts +5 -0
  221. package/src/runtime-providers/docker.ts +108 -23
  222. package/src/runtime-providers/types.ts +16 -0
  223. package/src/scheduler/scheduler.ts +36 -3
  224. package/src/script-workflows/workflow-ctx.ts +8 -0
  225. package/src/scripts-runtime/sdk-allowlist.ts +1 -0
  226. package/src/scripts-runtime/types/stdlib.d.ts +27 -0
  227. package/src/scripts-runtime/types/swarm-extension.d.ts +33 -0
  228. package/src/scripts-runtime/types/swarm-sdk.d.ts +27 -0
  229. package/src/server-user.ts +1 -0
  230. package/src/server.ts +7 -0
  231. package/src/slack/assistant.ts +17 -1
  232. package/src/slack/commands.ts +5 -1
  233. package/src/slack/handlers.ts +53 -5
  234. package/src/slack/left-threads.ts +88 -0
  235. package/src/slack/steering.ts +3 -0
  236. package/src/slack/thread-buffer.ts +40 -16
  237. package/src/tasks/follow-up-redelegation-guard.ts +155 -0
  238. package/src/tests/acp-adapter.test.ts +79 -0
  239. package/src/tests/base-prompt.test.ts +12 -0
  240. package/src/tests/chat-sdk-inbound.test.ts +30 -0
  241. package/src/tests/chatgpt-subscription-model-groups.test.ts +62 -0
  242. package/src/tests/chatgpt-subscription-pi.test.ts +170 -0
  243. package/src/tests/chatgpt-subscription-session-bench.test.ts +198 -0
  244. package/src/tests/claude-artifacts-read-script.test.ts +209 -0
  245. package/src/tests/codex-oauth-credential-info.test.ts +40 -0
  246. package/src/tests/codex-oauth-refresh-egress.test.ts +194 -0
  247. package/src/tests/context-window.test.ts +7 -0
  248. package/src/tests/db-client.test.ts +21 -0
  249. package/src/tests/db-task-read-contract.test.ts +8 -2
  250. package/src/tests/db-task-write-contract.test.ts +5 -1
  251. package/src/tests/entrypoint-api-readiness.test.ts +416 -210
  252. package/src/tests/explicit-model-validation.test.ts +113 -8
  253. package/src/tests/extensions-run-context.test.ts +11 -0
  254. package/src/tests/extensions-steering.test.ts +353 -0
  255. package/src/tests/extensions-validate.test.ts +5 -0
  256. package/src/tests/fixtures/extensions/steering-policy/hooks.ts +39 -0
  257. package/src/tests/fixtures/extensions/steering-policy/manifest.json +7 -0
  258. package/src/tests/fixtures/fake-fx.ts +167 -0
  259. package/src/tests/fixtures/fx/acp-responses-recorded.json +157 -0
  260. package/src/tests/fixtures/fx/acp-updates-recorded.jsonl +15 -0
  261. package/src/tests/follow-up-redelivery-guard.test.ts +432 -202
  262. package/src/tests/fx-adapter.test.ts +771 -0
  263. package/src/tests/grok-adapter.test.ts +36 -0
  264. package/src/tests/list-total-cache.test.ts +27 -2
  265. package/src/tests/mcp-loopback-proxy.test.ts +193 -0
  266. package/src/tests/memory-fts-populate.test.ts +11 -9
  267. package/src/tests/memory-retrieval-purge.test.ts +80 -0
  268. package/src/tests/migration-260-runtime-pool-cache.test.ts +118 -0
  269. package/src/tests/model-tier-resolution.test.ts +3 -3
  270. package/src/tests/pricing-refresh.test.ts +5 -5
  271. package/src/tests/prompt-template-session.test.ts +5 -3
  272. package/src/tests/provider-registration.test.ts +2 -1
  273. package/src/tests/query-audit-plans.test.ts +30 -2
  274. package/src/tests/runtime-cache-cleanup.test.ts +367 -0
  275. package/src/tests/runtime-e2b-pools.test.ts +41 -0
  276. package/src/tests/runtime-pools-db.test.ts +98 -0
  277. package/src/tests/runtime-pools-http.test.ts +29 -0
  278. package/src/tests/runtime-providers-docker.test.ts +253 -0
  279. package/src/tests/runtime-remote-controller.test.ts +66 -2
  280. package/src/tests/runtime-supervisor.test.ts +66 -2
  281. package/src/tests/script-connections.test.ts +13 -0
  282. package/src/tests/script-runs-http.test.ts +684 -2
  283. package/src/tests/seed-model-profiles.test.ts +2 -2
  284. package/src/tests/seed-schedules.test.ts +138 -0
  285. package/src/tests/seed-scripts.test.ts +26 -0
  286. package/src/tests/session-cost-recompute-unit.test.ts +39 -0
  287. package/src/tests/session-costs-golden.test.ts +26 -0
  288. package/src/tests/session-summaries-plans.test.ts +120 -0
  289. package/src/tests/session-summaries.test.ts +72 -12
  290. package/src/tests/sessions-tree.test.ts +11 -3
  291. package/src/tests/slack-leave-thread.test.ts +571 -0
  292. package/src/tests/slack-render-v2-query-plans.test.ts +24 -2
  293. package/src/tests/status.test.ts +11 -0
  294. package/src/tests/tasks-readable-row.test.ts +155 -0
  295. package/src/tests/tool-annotations.test.ts +2 -1
  296. package/src/tests/workflow-engine-v2.test.ts +42 -0
  297. package/src/tests/workflow-script-principal.test.ts +207 -7
  298. package/src/tools/send-task.ts +35 -47
  299. package/src/tools/slack-leave-thread.ts +104 -0
  300. package/src/tools/steer-task.ts +3 -2
  301. package/src/tools/task-dedup.ts +45 -21
  302. package/src/tools/tool-config.ts +2 -1
  303. package/src/types.ts +57 -0
  304. package/src/utils/codex-auth-failure.ts +30 -0
  305. package/src/utils/context-window.ts +4 -1
  306. package/src/utils/credentials.ts +7 -0
  307. package/src/utils/fx-models.ts +43 -0
  308. package/src/workflows/cooldown.ts +3 -2
  309. package/src/workflows/engine.ts +24 -9
  310. package/templates/schedules/dreaming-daily/config.json +1 -1
  311. package/templates/schedules/dreaming-daily/content.md +6 -14
  312. package/templates/skills/composio-google-docs/content.md +10 -0
  313. package/templates/skills/script-workflows/content.md +19 -1
  314. package/templates/skills/swarm-docs/content.md +2 -0
  315. package/templates/skills/swarm-scripts/SKILL.md +2 -0
  316. package/templates/skills/swarm-scripts/content.md +2 -0
  317. package/templates/workflows/dreaming/content.md +5 -1
  318. package/dist/acp-adapter-96dc64m2.js +0 -15
  319. package/dist/cli-dznrgaep.js +0 -30
  320. package/dist/cli-w3hcrxht.js +0 -6985
  321. package/dist/handlers-jg7v74ty.js +0 -31
  322. package/dist/index-8c1zbz59.js +0 -29
  323. package/dist/index-8k8tmfxd.js +0 -32
  324. package/dist/index-hq617xdj.js +0 -36
  325. package/dist/lead-567kw7x3.js +0 -68
  326. package/dist/worker-fjdqra6q.js +0 -68
@@ -1,13 +1,24 @@
1
- import { mkdir, mkdtemp, rm } from "node:fs/promises";
1
+ import { chmod, mkdir, mkdtemp, rm } from "node:fs/promises";
2
2
  import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
+ import type {
5
+ ContentBlock,
6
+ InitializeResponse,
7
+ PromptRequest,
8
+ PromptResponse,
9
+ SessionUpdate,
10
+ } from "@agentclientprotocol/sdk/unstable";
4
11
  import {
5
12
  baseUrlOverride,
6
13
  normalizeGrokModel,
7
14
  providerBaseUrl,
8
15
  providerForModel,
9
16
  } from "@desplega/model-catalog";
17
+ import { resolveTemplateAsync, resolveTemplateFromCode } from "../prompts/resolver";
18
+ import "../prompts/session-templates";
10
19
  import { clampContextPercent, getContextWindowSize } from "../utils/context-window";
20
+ import { requireFxRoute } from "../utils/fx-models";
21
+ import { scrubSecrets } from "../utils/secret-scrubber";
11
22
  import {
12
23
  ACP_TARGET_IDS,
13
24
  type AcpTarget,
@@ -20,11 +31,44 @@ import {
20
31
  type OpencodeConfig,
21
32
  } from "./opencode-adapter";
22
33
  import { applyReasoningEffort } from "./reasoning-effort";
23
- import type { CostData, CostModelUsage, ProviderEvent, ProviderSessionConfig } from "./types";
34
+ import type {
35
+ CostData,
36
+ CostModelUsage,
37
+ ProviderEvent,
38
+ ProviderSessionConfig,
39
+ SteerDelivery,
40
+ SteerDeliveryResult,
41
+ } from "./types";
42
+
43
+ /** What a target's `sessionSystemPrompt` hook sends as `session/new` `systemPrompt`. */
44
+ export interface AcpSessionSystemPrompt {
45
+ /** Text content blocks, sent as the `systemPrompt` array (append mode). */
46
+ blocks: ContentBlock[];
47
+ /** Set when the blocks carry only part of the prompt and the whole of it is in this file. */
48
+ artifactPath?: string;
49
+ }
50
+
51
+ /** What the adapter hands a target's `steer` hook for one steering message. */
52
+ export interface AcpSteerContext {
53
+ sessionId: string;
54
+ delivery: SteerDelivery;
55
+ /** The `initialize` result, for capability gating. */
56
+ initResult: InitializeResponse;
57
+ /** Sends `session/prompt` on the session's ACP connection. */
58
+ prompt(params: PromptRequest): Promise<PromptResponse>;
59
+ /** Observes raw session updates until the returned function is called. */
60
+ onUpdate(listener: (update: SessionUpdate) => void): () => void;
61
+ }
62
+
63
+ /** Native session resume over `session/resume`. */
64
+ export interface AcpResumeHook {
65
+ readonly method: "session/resume";
66
+ canResume(sessionId: string): Promise<boolean>;
67
+ }
24
68
 
25
69
  export interface AcpTargetProfile {
26
70
  /** An operator-selectable ACP target, or a first-class harness built on the ACP client. */
27
- readonly target: AcpTarget | "grok";
71
+ readonly target: AcpTarget | "grok" | "fx";
28
72
  command(config: ProviderSessionConfig): string[];
29
73
  env(config: ProviderSessionConfig): Record<string, string>;
30
74
  configuredOptions(config: ProviderSessionConfig): Record<string, string | boolean>;
@@ -35,8 +79,11 @@ export interface AcpTargetProfile {
35
79
  sessionMeta?(config: ProviderSessionConfig): Record<string, unknown> | undefined;
36
80
  /** A clearer message for a target error whose own text misleads, else undefined. */
37
81
  describeError?(message: string): string | undefined;
38
- /** Rewrites a translated event for display, e.g. unwrapping a tool proxy. */
39
- rewriteEvent?(event: ProviderEvent): ProviderEvent;
82
+ /**
83
+ * Rewrites a translated event for display, e.g. unwrapping a tool proxy.
84
+ * `update` is the ACP session update the event was translated from.
85
+ */
86
+ rewriteEvent?(event: ProviderEvent, update?: SessionUpdate): ProviderEvent;
40
87
  /** Usage, cost and the model that ran, from the `session/prompt` response `_meta`. */
41
88
  promptCost?(
42
89
  meta: Record<string, unknown> | null | undefined,
@@ -47,6 +94,23 @@ export interface AcpTargetProfile {
47
94
  meta: Record<string, unknown> | null | undefined,
48
95
  model: string,
49
96
  ): Extract<ProviderEvent, { type: "context_usage" }> | undefined;
97
+ // Opt-in hooks. A target without them behaves exactly as before they existed.
98
+ /**
99
+ * The standard `systemPrompt` for `session/new`. Sent only when the agent
100
+ * advertises `sessionCapabilities.systemPrompt`.
101
+ */
102
+ sessionSystemPrompt?(config: ProviderSessionConfig): Promise<AcpSessionSystemPrompt | undefined>;
103
+ /**
104
+ * The target accepts MCP URLs only over HTTPS or explicit loopback HTTP. A
105
+ * plain-HTTP, non-loopback server URL (the swarm API at `http://api:3013`)
106
+ * goes through a per-session 127.0.0.1 forwarder, and SSE servers are dropped
107
+ * unless the agent advertises `mcpCapabilities.sse`.
108
+ */
109
+ readonly requiresSecureMcpUrl?: boolean;
110
+ /** Live steering. Absent means the session has no `deliverSteering`. */
111
+ steer?(context: AcpSteerContext): Promise<SteerDeliveryResult>;
112
+ /** Native resume: `config.resumeSessionId` is resumed instead of creating a session. */
113
+ readonly resume?: AcpResumeHook;
50
114
  }
51
115
 
52
116
  export class AcpTargetResolutionError extends Error {
@@ -359,7 +423,7 @@ const copilotTargetProfile: AcpTargetProfile = {
359
423
  },
360
424
  };
361
425
 
362
- /** Qwen Code's `--auth-type` choices (0.26.3). `qwen-oauth` needs a browser, so it is left out. */
426
+ /** Qwen Code's `--auth-type` choices (0.26.4). `qwen-oauth` needs a browser, so it is left out. */
363
427
  const QWEN_AUTH_TYPES = ["openai", "openai-responses", "anthropic", "gemini", "vertex-ai"] as const;
364
428
 
365
429
  // Qwen Code is a Gemini CLI fork: QWEN_SYSTEM_MD replaces its built-in prompt
@@ -696,6 +760,242 @@ export const grokTargetProfile: AcpTargetProfile = {
696
760
  },
697
761
  };
698
762
 
763
+ // ---------------------------------------------------------------------------
764
+ // fx (Vercel Labs' `fx acp`), measured against fx 0.0.15 on 2026-10-29.
765
+ // ---------------------------------------------------------------------------
766
+
767
+ /** Above this many UTF-8 bytes the prompt goes to a file; fx caps `systemPrompt` at 64 KiB. */
768
+ export const FX_SYSTEM_PROMPT_INLINE_MAX_BYTES = 60 * 1024;
769
+ /** How much of an oversized prompt is still sent inline, ahead of the pointer line. */
770
+ export const FX_SYSTEM_PROMPT_HEAD_BYTES = 56 * 1024;
771
+ const FX_SYSTEM_PROMPT_FILE_TEMPLATE = "system.agent.fx.system_prompt_file";
772
+ /** fx streams this notice as an assistant message chunk; it is not model output. */
773
+ const FX_CONTEXT_NOTICE_PREFIX = "[context] ";
774
+ const FX_AUTH_ERROR_RE =
775
+ /authentication is unavailable|credential is unavailable|MissingApiKey|unauthorized|\b401\b/i;
776
+
777
+ /**
778
+ * Per-task scratch dir for fx: `acp.log` (`--log-file`) and, for an oversized
779
+ * prompt, `SYSTEM.md`. Outside the task cwd, removed when the session ends.
780
+ */
781
+ export function fxRunDir(config: Pick<ProviderSessionConfig, "taskId">): string {
782
+ const safe = config.taskId.replace(/[^A-Za-z0-9._-]/g, "_") || "session";
783
+ return join(tmpdir(), `fx-${safe}`);
784
+ }
785
+
786
+ /** The first `max` bytes of `bytes` as text, never ending inside a UTF-8 sequence. */
787
+ function utf8Head(bytes: Uint8Array, max: number): string {
788
+ let end = Math.min(max, bytes.length);
789
+ // Back off continuation bytes (10xxxxxx) so a multi-byte character is not split.
790
+ while (end > 0 && end < bytes.length && ((bytes[end] ?? 0) & 0xc0) === 0x80) end -= 1;
791
+ return new TextDecoder().decode(bytes.subarray(0, end));
792
+ }
793
+
794
+ async function fxSystemPromptPointer(path: string): Promise<string> {
795
+ try {
796
+ const result = await resolveTemplateAsync(FX_SYSTEM_PROMPT_FILE_TEMPLATE, { path });
797
+ if (!result.skipped && result.text.trim()) return result.text;
798
+ } catch {
799
+ // Fall through to the code default.
800
+ }
801
+ // A skipped or blank override must not drop the pointer to the full prompt.
802
+ return resolveTemplateFromCode(FX_SYSTEM_PROMPT_FILE_TEMPLATE, { path }).text;
803
+ }
804
+
805
+ /**
806
+ * The `systemPrompt` fx gets. Up to 60 KiB it is the prompt itself. A larger
807
+ * one is written whole to `<dir>/SYSTEM.md`; fx then gets its first 56 KiB plus
808
+ * a line telling the model to read that file first (fx returns `-32602
809
+ * systemPrompt exceeds the 64 KiB limit` above 65,536 bytes).
810
+ */
811
+ export async function fxSystemPrompt(
812
+ systemPrompt: string,
813
+ dir: string,
814
+ ): Promise<AcpSessionSystemPrompt | undefined> {
815
+ if (!systemPrompt.trim()) return undefined;
816
+ const bytes = new TextEncoder().encode(systemPrompt);
817
+ if (bytes.length <= FX_SYSTEM_PROMPT_INLINE_MAX_BYTES) {
818
+ return { blocks: [{ type: "text", text: systemPrompt }] };
819
+ }
820
+ const artifactPath = join(dir, "SYSTEM.md");
821
+ await mkdir(dir, { recursive: true });
822
+ await Bun.write(artifactPath, systemPrompt);
823
+ await chmod(artifactPath, 0o600);
824
+ const head = utf8Head(bytes, FX_SYSTEM_PROMPT_HEAD_BYTES);
825
+ const pointer = await fxSystemPromptPointer(artifactPath);
826
+ return { blocks: [{ type: "text", text: `${head}\n\n${pointer}` }], artifactPath };
827
+ }
828
+
829
+ /** `~/.fx/sessions/v2/<id>/log.jsonl`: where fx 0.0.15 keeps a saved session. */
830
+ export async function fxSessionExists(
831
+ sessionId: string,
832
+ home: string | undefined,
833
+ ): Promise<boolean> {
834
+ if (!home || !/^[A-Za-z0-9_-]+$/.test(sessionId)) return false;
835
+ return Bun.file(join(home, ".fx", "sessions", "v2", sessionId, "log.jsonl")).exists();
836
+ }
837
+
838
+ type FxToolCallMeta = { fx?: { toolCall?: { mcp?: { server?: unknown; tool?: unknown } } } };
839
+
840
+ /**
841
+ * fx's display adjustments:
842
+ * - The `[context] … capability_search` notice arrives as its own assistant
843
+ * chunk. It becomes an `fx_context_notice` custom event, so it never reaches
844
+ * the task output.
845
+ * - A `tool_call` titles the action ("Running", "Get the agent swarm") and
846
+ * names the tool in a non-standard `name` field (`shell`, `skill`,
847
+ * `mcp_swarm_get-swarm`), with the MCP server and tool in
848
+ * `_meta.fx.toolCall.mcp`. The tool start carries that name, MCP calls as
849
+ * `mcp__<server>__<tool>` like every other harness.
850
+ */
851
+ export function rewriteFxEvent(event: ProviderEvent, update?: SessionUpdate): ProviderEvent {
852
+ if (
853
+ event.type === "message" &&
854
+ event.role === "assistant" &&
855
+ event.content.startsWith(FX_CONTEXT_NOTICE_PREFIX)
856
+ ) {
857
+ return {
858
+ type: "custom",
859
+ name: "fx_context_notice",
860
+ data: { text: event.content, messageId: event.messageId ?? null },
861
+ };
862
+ }
863
+ if (event.type === "tool_start" && update?.sessionUpdate === "tool_call") {
864
+ const mcp = (update._meta as FxToolCallMeta | null | undefined)?.fx?.toolCall?.mcp;
865
+ if (typeof mcp?.server === "string" && typeof mcp.tool === "string" && mcp.server && mcp.tool) {
866
+ return { ...event, toolName: `mcp__${mcp.server}__${mcp.tool}` };
867
+ }
868
+ const name = (update as { name?: unknown }).name;
869
+ if (typeof name === "string" && name.trim()) return { ...event, toolName: name.trim() };
870
+ }
871
+ return event;
872
+ }
873
+
874
+ /**
875
+ * fx's native steer: a second `session/prompt` with `_meta.fx.steer` during the
876
+ * running turn. fx applies it at the next tool boundary without cancelling and
877
+ * echoes it as a `user_message_chunk` carrying `_meta.fx.steering`; the steer
878
+ * request itself resolves with the turn, `_meta.fx.steering: "absorbed"`.
879
+ * Either one counts as delivered. The echo arrives first, so the row is
880
+ * `delivered` while the turn still runs and `accept-steer` can mark it handled.
881
+ * Any other answer is undeliverable and the runner promotes the message.
882
+ */
883
+ export async function steerFx({
884
+ sessionId,
885
+ delivery,
886
+ initResult,
887
+ prompt,
888
+ onUpdate,
889
+ }: AcpSteerContext): Promise<SteerDeliveryResult> {
890
+ if (delivery.mode !== "steer") {
891
+ return { delivered: false, reason: `fx supports steer only, not ${delivery.mode}` };
892
+ }
893
+ const meta = initResult.agentCapabilities?._meta as { fx?: { steering?: unknown } } | undefined;
894
+ if (meta?.fx?.steering !== true) {
895
+ return { delivered: false, reason: "fx did not advertise _meta.fx.steering" };
896
+ }
897
+ return new Promise<SteerDeliveryResult>((resolve) => {
898
+ let settled = false;
899
+ let stopListening = () => {};
900
+ const settle = (result: SteerDeliveryResult) => {
901
+ if (settled) return;
902
+ settled = true;
903
+ stopListening();
904
+ resolve(result);
905
+ };
906
+ stopListening = onUpdate((update) => {
907
+ if (update.sessionUpdate !== "user_message_chunk") return;
908
+ const steering = (update._meta as { fx?: { steering?: unknown } } | null | undefined)?.fx
909
+ ?.steering;
910
+ if (!steering || update.content.type !== "text" || update.content.text !== delivery.text) {
911
+ return;
912
+ }
913
+ settle({ delivered: true, mode: "steer" });
914
+ });
915
+ prompt({
916
+ sessionId,
917
+ prompt: [{ type: "text", text: delivery.text }],
918
+ _meta: { fx: { steer: true } },
919
+ }).then(
920
+ (response) => {
921
+ const state = (response._meta as { fx?: { steering?: unknown } } | null | undefined)?.fx
922
+ ?.steering;
923
+ settle(
924
+ state === "absorbed"
925
+ ? { delivered: true, mode: "steer" }
926
+ : {
927
+ delivered: false,
928
+ reason: `fx did not absorb the steer (stopReason ${response.stopReason}, steering ${JSON.stringify(state ?? null)})`,
929
+ },
930
+ );
931
+ },
932
+ (err: unknown) => {
933
+ const message = err instanceof Error ? err.message : String(err);
934
+ settle({ delivered: false, reason: scrubSecrets(`fx steer failed: ${message}`) });
935
+ },
936
+ );
937
+ });
938
+ }
939
+
940
+ export const fxTargetProfile: AcpTargetProfile = {
941
+ target: "fx",
942
+ command(config) {
943
+ const binary = readEnv(config, "FX_BINARY")?.trim() || "fx";
944
+ const route = requireFxRoute(config.model);
945
+ return [
946
+ binary,
947
+ "acp",
948
+ "--model",
949
+ route.fxModel,
950
+ "--log-file",
951
+ join(fxRunDir(config), "acp.log"),
952
+ ];
953
+ },
954
+ env(config) {
955
+ const route = requireFxRoute(config.model);
956
+ const env = baseTargetEnv(config);
957
+ // Only the route's own key: an OpenRouter session never sees OPENAI_API_KEY.
958
+ copyEnvKeys(config, env, [route.keyVar]);
959
+ // Process-local selection and permissions. `session/set_mode` and the
960
+ // `mode` / `provider` config options persist into ~/.fx/settings.json, so
961
+ // the adapter never sends them. Auto mode's reviewer holds swarm MCP calls
962
+ // headless, so the session runs full-access.
963
+ env.FX_PROVIDER = route.fxProvider;
964
+ env.FX_PERMISSION_MODE = "full-access";
965
+ env.FX_AUTO_UPGRADE = "0";
966
+ env.FX_NO_OPEN_BROWSER = "1";
967
+ return env;
968
+ },
969
+ // The model rides on `--model`. Only fx's AI Gateway provider has an
970
+ // `effort` option; custom connections send no reasoning control.
971
+ configuredOptions(config): Record<string, string | boolean> {
972
+ if (!config.reasoningEffort) return {};
973
+ const effort = applyReasoningEffort("fx", config.model.trim(), config.reasoningEffort);
974
+ return effort.kind === "fx-effort" ? { effort: effort.reasoningEffort } : {};
975
+ },
976
+ async writeSystemPromptArtifact(config) {
977
+ // acp.log can hold session/new params, including the ephemeral MCP bearer.
978
+ await mkdir(fxRunDir(config), { recursive: true, mode: 0o700 });
979
+ },
980
+ async cleanupSystemPromptArtifact(config) {
981
+ await rm(fxRunDir(config), { recursive: true, force: true });
982
+ },
983
+ sessionSystemPrompt(config) {
984
+ return fxSystemPrompt(config.systemPrompt ?? "", fxRunDir(config));
985
+ },
986
+ requiresSecureMcpUrl: true,
987
+ steer: steerFx,
988
+ resume: {
989
+ method: "session/resume",
990
+ canResume: (sessionId) => fxSessionExists(sessionId, process.env.HOME),
991
+ },
992
+ rewriteEvent: rewriteFxEvent,
993
+ describeError(message) {
994
+ if (!FX_AUTH_ERROR_RE.test(message)) return undefined;
995
+ return `fx could not authenticate the model route (OPENROUTER_API_KEY, OPENAI_API_KEY or AI_GATEWAY_API_KEY invalid or missing): ${message}`;
996
+ },
997
+ };
998
+
699
999
  export function resolveAcpTarget(config: ProviderSessionConfig): AcpTargetProfile {
700
1000
  const target = readEnv(config, "ACP_TARGET") ?? "custom";
701
1001
  if (!isAcpTarget(target)) {
@@ -45,7 +45,7 @@ import type {
45
45
  * default; the image installs exactly this version and the per-task settings
46
46
  * set `amp.updates.mode: "disabled"`. Bump it deliberately, with a live run.
47
47
  */
48
- export const AMP_PACKAGE = "@ampcode/cli@0.0.1793246451-g9e5ffe";
48
+ export const AMP_PACKAGE = "@ampcode/cli@0.0.1793363705-g5c9ae5";
49
49
 
50
50
  /** MCP server name. Amp exposes its tools as `agent_swarm` / `store_progress` (dashes become underscores). */
51
51
  const AMP_MCP_SERVER_NAME = "agent-swarm";
@@ -40,6 +40,7 @@ import {
40
40
  import { buildRatingsFromLlm, fetchRetrievalsForTask, postRatings } from "../be/memory/raters/llm";
41
41
  import { contextBoundNoteThreshold, createContextBoundNote } from "../hooks/context-bound-note";
42
42
  import { getApiKey } from "../utils/api-key";
43
+ import { isCodexRejectedAccessError } from "../utils/codex-auth-failure";
43
44
  import {
44
45
  CONTEXT_FORMULA,
45
46
  clampContextPercent,
@@ -67,7 +68,7 @@ import { computeCodexCostUsd, getCodexContextWindow, resolveCodexModel } from ".
67
68
  import { credentialsToAuthJson } from "./codex-oauth/auth-json.js";
68
69
  import { hasCodexOAuthPoolSlots } from "./codex-oauth/env-keys.js";
69
70
  import { codexUserHome } from "./codex-oauth/home.js";
70
- import { CodexOAuthRefreshError, getValidCodexOAuth } from "./codex-oauth/storage.js";
71
+ import { buildPoolRevalidationFailureReason, getValidCodexOAuth } from "./codex-oauth/storage.js";
71
72
  import { pruneCodexRollouts } from "./codex-rollouts";
72
73
  import { resolveCodexPrompt } from "./codex-skill-resolver";
73
74
  import { createCodexSwarmEventHandler } from "./codex-swarm-events";
@@ -89,6 +90,8 @@ import type {
89
90
  SteerDeliveryResult,
90
91
  } from "./types";
91
92
 
93
+ export { buildPoolRevalidationFailureReason } from "./codex-oauth/storage.js";
94
+
92
95
  type CodexConfig = Record<string, CodexConfigValue | undefined>;
93
96
 
94
97
  /** `model_provider` id of the generated gateway block. */
@@ -442,29 +445,6 @@ export async function resolveCodexAuthMode(
442
445
  return currentMode;
443
446
  }
444
447
 
445
- /**
446
- * Build the `failureReason` for a pool-slot OAuth revalidation failure
447
- * (see `resolveCodexAuthMode` above).
448
- *
449
- * Wording matters: every variant carries `[auth-error]`, but the runner's
450
- * `isCodexAuthFailureReason` counts only a confirmed refresh rejection toward
451
- * the auth-failure bench (2 in a row). A lock-wait timeout ("waiting for the
452
- * refresh lock") and any other thrown error (refresh-lock HTTP or network
453
- * failure, config-store persistence failure: "not a confirmed auth
454
- * rejection") are excluded as transient, so an outage does NOT bench a
455
- * healthy login.
456
- */
457
- export function buildPoolRevalidationFailureReason(err: unknown, slot: number): string {
458
- if (err instanceof CodexOAuthRefreshError) {
459
- if (err.reason === "lock_timeout") {
460
- return `[auth-error] Codex pool slot ${slot} [...${err.keySuffix}] revalidation failed: timed out waiting for the refresh lock — transient, will retry on next task.`;
461
- }
462
- return `[auth-error] Codex pool slot ${slot} [...${err.keySuffix}] revalidation failed: refresh rejected (${err.status ?? "unknown status"} ${err.body ?? ""}) — credential likely revoked; re-run codex-login for this slot.`;
463
- }
464
- const message = err instanceof Error ? err.message : String(err);
465
- return `[auth-error] Codex pool slot ${slot} revalidation failed (not a confirmed auth rejection, transient): ${message}`;
466
- }
467
-
468
448
  /**
469
449
  * Build the configuration supplied through the `thread/start` request.
470
450
  * It overrides the baseline `~/.codex/config.toml` in the worker image.
@@ -1594,26 +1574,11 @@ export class CodexSession implements ProviderSession {
1594
1574
  };
1595
1575
  }
1596
1576
 
1597
- // Bad / missing / invalid API key — codexErrorInfo: "Unauthorized".
1598
- // Also catches the "revoked-while-valid" OAuth pool failure mode: a
1599
- // credential revoked server-side while its access token is still
1600
- // unexpired never enters `getValidCodexOAuth` (the token looks fresh, it
1601
- // fast-returns) — the ONLY artifact of that death mode is the spawned
1602
- // Codex CLI self-refreshing straight from the blanked-refresh-token
1603
- // auth.json and surfacing this turn-level error.
1604
- const authPatterns = [
1605
- "unauthorized",
1606
- "http 401",
1607
- " 401 ",
1608
- "invalid api key",
1609
- "invalid_api_key",
1610
- "missing api key",
1611
- "no api key",
1612
- "authentication failed",
1613
- "failed to refresh token",
1614
- "invalid 'refresh_token'",
1615
- ];
1616
- if (authPatterns.some((p) => normalized.includes(p))) {
1577
+ // Bad / missing / invalid API key — codexErrorInfo: "Unauthorized" — or
1578
+ // a pool login revoked while its access token is unexpired: the spawned
1579
+ // Codex CLI self-refreshes from the blanked-refresh-token auth.json and
1580
+ // surfaces this turn-level error.
1581
+ if (isCodexRejectedAccessError(normalized)) {
1617
1582
  const slot = this.config.codexSlot;
1618
1583
  const guidance =
1619
1584
  slot === undefined
@@ -150,14 +150,19 @@ export async function refreshAccessToken(refreshToken: string): Promise<TokenRes
150
150
  });
151
151
 
152
152
  if (!response.ok) {
153
- const text = await response.text().catch(() => "");
153
+ // The endpoint may echo the submitted refresh token: redact before the
154
+ // text reaches a log, the thrown error or a task's failure reason.
155
+ const text = redactTokens(await response.text().catch(() => ""), [refreshToken]);
154
156
  console.error("[codex-oauth] Token refresh failed:", response.status, text);
155
157
  return { type: "failed", status: response.status, error: text.slice(0, 300) };
156
158
  }
157
159
 
158
160
  const json = (await response.json()) as Record<string, unknown>;
159
161
  if (!json.access_token || !json.refresh_token || typeof json.expires_in !== "number") {
160
- console.error("[codex-oauth] Token refresh response missing fields:", json);
162
+ console.error(
163
+ "[codex-oauth] Token refresh response missing fields; got keys:",
164
+ Object.keys(json).join(", "),
165
+ );
161
166
  return { type: "failed" };
162
167
  }
163
168
 
@@ -168,11 +173,27 @@ export async function refreshAccessToken(refreshToken: string): Promise<TokenRes
168
173
  expires: Date.now() + (json.expires_in as number) * 1000,
169
174
  };
170
175
  } catch (error) {
171
- console.error("[codex-oauth] Token refresh error:", error);
176
+ const message = error instanceof Error ? error.message : String(error);
177
+ console.error("[codex-oauth] Token refresh error:", redactTokens(message, [refreshToken]));
172
178
  return { type: "failed" };
173
179
  }
174
180
  }
175
181
 
182
+ /**
183
+ * `text` with every given token, raw or URL/JSON-encoded, replaced by a
184
+ * marker, then run through `scrubSecrets`. For error text from an endpoint
185
+ * that was sent those tokens and may echo them back.
186
+ */
187
+ export function redactTokens(text: string, tokens: readonly (string | undefined)[]): string {
188
+ let out = text;
189
+ for (const token of tokens) {
190
+ if (!token) continue;
191
+ const forms = new Set([token, encodeURIComponent(token), JSON.stringify(token).slice(1, -1)]);
192
+ for (const form of forms) out = out.split(form).join("[REDACTED:codex_oauth_token]");
193
+ }
194
+ return scrubSecrets(out);
195
+ }
196
+
176
197
  export function getAccountId(accessToken: string): string | null {
177
198
  const payload = decodeJwt(accessToken);
178
199
  const auth = payload?.[JWT_CLAIM_PATH] as Record<string, unknown> | undefined;
@@ -9,8 +9,9 @@
9
9
  * until the 071 migration renames it.
10
10
  */
11
11
 
12
+ import { scrubSecrets } from "../../utils/secret-scrubber.js";
12
13
  import { deriveCodexKeySuffix } from "./auth-json.js";
13
- import { refreshAccessToken } from "./flow.js";
14
+ import { redactTokens, refreshAccessToken } from "./flow.js";
14
15
  import type { CodexOAuthCredentials } from "./types.js";
15
16
 
16
17
  /** Legacy single-credential key — kept for backwards-compat fallback reads. */
@@ -102,6 +103,30 @@ export class CodexOAuthRefreshError extends Error {
102
103
  }
103
104
  }
104
105
 
106
+ /**
107
+ * Build the `failureReason` for a pool-slot OAuth revalidation failure
108
+ * (`resolveCodexAuthMode` in codex-adapter.ts, and the pi ChatGPT
109
+ * subscription path in `subscription.ts`).
110
+ *
111
+ * Wording matters: every variant carries `[auth-error]`, but the runner's
112
+ * `isCodexAuthFailureReason` counts only a confirmed refresh rejection toward
113
+ * the auth-failure bench (2 in a row). A lock-wait timeout ("waiting for the
114
+ * refresh lock") and any other thrown error (refresh-lock HTTP or network
115
+ * failure, config-store persistence failure: "not a confirmed auth
116
+ * rejection") are excluded as transient, so an outage does NOT bench a
117
+ * healthy login.
118
+ */
119
+ export function buildPoolRevalidationFailureReason(err: unknown, slot: number): string {
120
+ if (err instanceof CodexOAuthRefreshError) {
121
+ if (err.reason === "lock_timeout") {
122
+ return `[auth-error] Codex pool slot ${slot} [...${err.keySuffix}] revalidation failed: timed out waiting for the refresh lock — transient, will retry on next task.`;
123
+ }
124
+ return `[auth-error] Codex pool slot ${slot} [...${err.keySuffix}] revalidation failed: refresh rejected (${err.status ?? "unknown status"} ${scrubSecrets(err.body ?? "")}) — credential likely revoked; re-run codex-login for this slot.`;
125
+ }
126
+ const message = scrubSecrets(err instanceof Error ? err.message : String(err));
127
+ return `[auth-error] Codex pool slot ${slot} revalidation failed (not a confirmed auth rejection, transient): ${message}`;
128
+ }
129
+
105
130
  /**
106
131
  * Acquire the cross-process refresh lock for a Codex OAuth slot via the API
107
132
  * server's `oauth_refresh_locks` table (migration 077). Worker-side code
@@ -246,23 +271,33 @@ export async function storeCodexOAuth(
246
271
  slot = 0,
247
272
  ): Promise<void> {
248
273
  const key = codexOAuthKeyForSlot(slot);
249
- const res = await fetch(`${apiUrl}/api/config`, {
250
- method: "PUT",
251
- headers: {
252
- "Content-Type": "application/json",
253
- Authorization: `Bearer ${apiKey}`,
254
- },
255
- body: JSON.stringify({
256
- scope: "global",
257
- key,
258
- value: JSON.stringify(creds),
259
- isSecret: true,
260
- description: `Codex ChatGPT OAuth credentials slot ${slot} (stored by codex-login)`,
261
- }),
262
- });
274
+ // The request carries the tokens, so a transport error or the store's reply
275
+ // may echo them. Every error leaving here is rebuilt from redacted text,
276
+ // with no `cause` pointing back at the raw one.
277
+ const redact = (text: string) => redactTokens(text, [creds.access, creds.refresh]);
278
+ let res: Response;
279
+ try {
280
+ res = await fetch(`${apiUrl}/api/config`, {
281
+ method: "PUT",
282
+ headers: {
283
+ "Content-Type": "application/json",
284
+ Authorization: `Bearer ${apiKey}`,
285
+ },
286
+ body: JSON.stringify({
287
+ scope: "global",
288
+ key,
289
+ value: JSON.stringify(creds),
290
+ isSecret: true,
291
+ description: `Codex ChatGPT OAuth credentials slot ${slot} (stored by codex-login)`,
292
+ }),
293
+ });
294
+ } catch (error) {
295
+ const message = error instanceof Error ? error.message : String(error);
296
+ throw new Error(`Failed to store ${key} config: ${redact(message)}`);
297
+ }
263
298
 
264
299
  if (!res.ok) {
265
- const text = await res.text().catch(() => "");
300
+ const text = redact(await res.text().catch(() => ""));
266
301
  throw new Error(`Failed to store ${key} config: HTTP ${res.status} ${text}`);
267
302
  }
268
303
  }
@@ -343,7 +378,11 @@ export async function persistCodexOAuth(
343
378
  try {
344
379
  await storeCodexOAuth(apiUrl, apiKey, creds, slot);
345
380
  } catch (err) {
346
- console.error("[codex-oauth] persistCodexOAuth failed (non-fatal):", err);
381
+ const message = err instanceof Error ? err.message : String(err);
382
+ console.error(
383
+ "[codex-oauth] persistCodexOAuth failed (non-fatal):",
384
+ redactTokens(message, [creds.access, creds.refresh]),
385
+ );
347
386
  }
348
387
  }
349
388