@desplega.ai/agent-swarm 2.7.4 → 2.7.5

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 (253) hide show
  1. package/README.md +1 -1
  2. package/dist/acp-adapter-g3ads55s.js +15 -0
  3. package/dist/{actions-gfrr9vb7.js → actions-zwfykqqk.js} +18 -18
  4. package/dist/{amp-adapter-0s58gm46.js → amp-adapter-z3s0ssdt.js} +3 -3
  5. package/dist/{app-stvq692g.js → app-vmp4fgep.js} +3 -3
  6. package/dist/{assistant-5s37fr9n.js → assistant-vwde653f.js} +28 -21
  7. package/dist/{boot-reembed-yzy1841n.js → boot-reembed-375t7tbk.js} +1 -1
  8. package/dist/{boot-reembed-cv82gz7y.js → boot-reembed-gq9rcpnj.js} +1 -1
  9. package/dist/{boot-scrub-sweep-8k4q95ap.js → boot-scrub-sweep-nd938kyf.js} +4 -4
  10. package/dist/{boot-seeds-ftt9htah.js → boot-seeds-aykx51v3.js} +4 -4
  11. package/dist/{boxd-515v1t8z.js → boxd-k1j26g3h.js} +1 -1
  12. package/dist/{claude-adapter-t00wrzeh.js → claude-adapter-8j4hm099.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-0wyynpn1.js} +3 -3
  15. package/dist/{claude-sdk-session-vj0c2azx.js → claude-sdk-session-q6wj5hmy.js} +9 -9
  16. package/dist/{cli-wndv9p7g.js → cli-0731hvbw.js} +6 -6
  17. package/dist/{cli-d2pzqej1.js → cli-0bxqnvq9.js} +719 -7
  18. package/dist/{cli-jkt52mgt.js → cli-0gey5fzy.js} +2 -2
  19. package/dist/{cli-1yekbyqm.js → cli-1338796d.js} +205 -48
  20. package/dist/{cli-1s1dqn1j.js → cli-1ayk1eas.js} +171 -123
  21. package/dist/{cli-a4btqqft.js → cli-1b7btd38.js} +1 -1
  22. package/dist/{cli-a2jw6ee5.js → cli-1p89xny0.js} +1 -1
  23. package/dist/{cli-v726afdm.js → cli-2pnswgh8.js} +1 -1
  24. package/dist/{cli-kvtc7aez.js → cli-3h4nhjn8.js} +42 -14
  25. package/dist/{cli-655xm9ms.js → cli-583frbyr.js} +1 -1
  26. package/dist/{cli-4r12t6w6.js → cli-5c4hjzne.js} +101 -4
  27. package/dist/{cli-az0bpf8p.js → cli-5pag6rmn.js} +3 -3
  28. package/dist/{cli-36g23z21.js → cli-6s7vdcta.js} +4 -4
  29. package/dist/{cli-rtk9g7av.js → cli-73jzwksr.js} +6 -2
  30. package/dist/{cli-jg0nfda6.js → cli-75gkpjvw.js} +52 -21
  31. package/dist/{cli-a81qevyg.js → cli-7a42nfy0.js} +20 -36
  32. package/dist/{cli-0sbkss7h.js → cli-7wcyk2s5.js} +2 -2
  33. package/dist/{cli-41z8xwj0.js → cli-7wspm5gm.js} +14 -4
  34. package/dist/{cli-byqfbnjw.js → cli-82ae98pz.js} +9 -9
  35. package/dist/{cli-bq2pcphn.js → cli-8hs41zy2.js} +1 -1
  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-j4kt1mpb.js → cli-a47xy8eq.js} +3 -3
  39. package/dist/{cli-ycm6180s.js → cli-a7v854rf.js} +5 -3
  40. package/dist/{cli-98pke4vk.js → cli-ae7mrdf9.js} +2 -2
  41. package/dist/{cli-ahd90bwt.js → cli-afrrppsw.js} +6 -6
  42. package/dist/{cli-8yy525ad.js → cli-bhjt02p8.js} +2 -2
  43. package/dist/{cli-v1yw3wha.js → cli-bnz7a7qf.js} +4 -4
  44. package/dist/{cli-j4kw3rsx.js → cli-cm5yqn35.js} +18 -0
  45. package/dist/{cli-h55y2bjc.js → cli-dhar9qay.js} +2 -2
  46. package/dist/{cli-5gr1g711.js → cli-fqzx7gwa.js} +2 -2
  47. package/dist/{cli-tzrpv5sc.js → cli-ftyxyspd.js} +1 -1
  48. package/dist/cli-fw2w0kva.js +47 -0
  49. package/dist/{cli-2dfbv8qr.js → cli-fy6x31ps.js} +1 -0
  50. package/dist/{cli-mvckssyq.js → cli-g61qxmge.js} +5 -5
  51. package/dist/{cli-aq58fk5m.js → cli-g83s9ewh.js} +7 -1
  52. package/dist/{cli-224epwwf.js → cli-hn9hxmz9.js} +3 -3
  53. package/dist/{cli-5hjced2v.js → cli-j3jvnpe8.js} +6 -6
  54. package/dist/{cli-dae4sbgy.js → cli-jm2vfpfd.js} +2 -2
  55. package/dist/{cli-zn15c46z.js → cli-mjzynrse.js} +1 -1
  56. package/dist/{cli-a1fhj4n9.js → cli-mmwm7zbf.js} +2 -2
  57. package/dist/{cli-cq1pf336.js → cli-mry0qzym.js} +5 -5
  58. package/dist/{cli-bwty35gx.js → cli-mthwvjre.js} +1 -1
  59. package/dist/{cli-s0exq74r.js → cli-nmbmy64z.js} +3 -3
  60. package/dist/{cli-wcmnan2g.js → cli-nn4d8dne.js} +5 -5
  61. package/dist/{cli-jg4p5t36.js → cli-nzxr556s.js} +121 -51
  62. package/dist/{cli-bcz074zx.js → cli-p599f1qm.js} +1 -1
  63. package/dist/{cli-k76tgq03.js → cli-pywf3eh5.js} +2 -2
  64. package/dist/{cli-wk9md5px.js → cli-qxnf8z2z.js} +2 -2
  65. package/dist/{cli-je8yeyc5.js → cli-rhw40jgb.js} +5 -5
  66. package/dist/{cli-avgcz4ra.js → cli-rm6hqj3z.js} +406 -35
  67. package/dist/{cli-0fgr1w9b.js → cli-rpm2sgv0.js} +2 -2
  68. package/dist/{cli-t282jgab.js → cli-rqkxrmgw.js} +12 -12
  69. package/dist/{cli-2qpvmmkw.js → cli-s0qnd4wv.js} +1 -1
  70. package/dist/{cli-pwyevs29.js → cli-spknvxbq.js} +22 -21
  71. package/dist/{cli-9y354g3j.js → cli-srye3vdt.js} +1 -1
  72. package/dist/{cli-pe09p6dv.js → cli-tqgg40ek.js} +1 -1
  73. package/dist/{cli-f255yaqn.js → cli-vbyfwh92.js} +3 -3
  74. package/dist/{cli-gcbdjt47.js → cli-vfym7wmz.js} +6 -6
  75. package/dist/{cli-edmm9d9y.js → cli-vqypxzh4.js} +7 -7
  76. package/dist/{cli-1gm8jcyr.js → cli-wvaawacd.js} +13 -13
  77. package/dist/{cli-eny54typ.js → cli-wx3rhnm0.js} +1 -1
  78. package/dist/{cli-b1afgtj3.js → cli-xbkdm710.js} +3 -3
  79. package/dist/{cli-mc97xy2s.js → cli-xtjhfmpc.js} +16 -4
  80. package/dist/{cli-eqx6k993.js → cli-y24sc29k.js} +25 -13
  81. package/dist/{cli-ktmvb20s.js → cli-z6jkx23q.js} +12 -12
  82. package/dist/{cli-7n5sbw0z.js → cli-z8eh3ex8.js} +5 -5
  83. package/dist/cli-zh3fp8v2.js +151 -0
  84. package/dist/{cli-13jvmew3.js → cli-zppwg9p9.js} +7 -7
  85. package/dist/cli.js +18 -18
  86. package/dist/{codex-adapter-g0gctw29.js → codex-adapter-q6k3gnyg.js} +5 -5
  87. package/dist/{codex-hook-vrzz2xhr.js → codex-hook-9efm2x1m.js} +1 -1
  88. package/dist/{codex-login-0gg46ygn.js → codex-login-y0pmff1c.js} +1 -1
  89. package/dist/{codex-session-runner-dr2cxe1c.js → codex-session-runner-3mwmk3d9.js} +6 -6
  90. package/dist/{commands-1p07cvff.js → commands-evrenjn9.js} +2 -2
  91. package/dist/{controller-z0s7sv6r.js → controller-xjp8vswh.js} +4 -4
  92. package/dist/{controllers-connect-x9kvvhyk.js → controllers-connect-pp5jnxvc.js} +20 -20
  93. package/dist/{cursor-adapter-m5b98pvk.js → cursor-adapter-yf47w4t0.js} +2 -2
  94. package/dist/{db-rsh04kh3.js → db-rgfsh91f.js} +2 -2
  95. package/dist/{db-capk2raj.js → db-zxpw6aht.js} +3 -3
  96. package/dist/{devin-adapter-yxm87fe9.js → devin-adapter-r9phbnc9.js} +1 -1
  97. package/dist/{dispatcher-304q6fwa.js → dispatcher-smasgws7.js} +17 -17
  98. package/dist/{docker-7rxt0rry.js → docker-rrjhaqt9.js} +2 -2
  99. package/dist/{dsh-adapter-btewem0y.js → dsh-adapter-ftyy8dnp.js} +2 -2
  100. package/dist/{e2b-renc0k8q.js → e2b-qcaj675y.js} +2 -2
  101. package/dist/{e2b-7fc9b9bd.js → e2b-w1p0bnpm.js} +2 -2
  102. package/dist/{easter-egg-3d-fm1p3n4d.js → easter-egg-3d-yyts7p16.js} +1 -1
  103. package/dist/{embedding-space-s68v49ct.js → embedding-space-1z0mbg95.js} +6 -6
  104. package/dist/{embeddings-4ms3604w.js → embeddings-ygeg7tm7.js} +1 -1
  105. package/dist/{event-bus-dfjx2qdh.js → event-bus-g0ka3pbw.js} +1 -1
  106. package/dist/{execute-mcdgccjs.js → execute-14qhqp4d.js} +1 -1
  107. package/dist/{external-channel-9dxwfztn.js → external-channel-ttgrs3sx.js} +2 -2
  108. package/dist/fx-adapter-gt6pncj2.js +18 -0
  109. package/dist/{grok-adapter-05ecv4h0.js → grok-adapter-sm4m5k4g.js} +8 -8
  110. package/dist/handlers-vwmba1m2.js +31 -0
  111. package/dist/{heartbeat-3n94heh3.js → heartbeat-qahz4p62.js} +20 -20
  112. package/dist/{hook-zxwje86c.js → hook-4fd5wq5w.js} +4 -4
  113. package/dist/{hook-aadrn69x.js → hook-kj3d398m.js} +6 -6
  114. package/dist/{http-xzxk9mst.js → http-1ted0j1k.js} +100 -64
  115. package/dist/index-5r71rf98.js +32 -0
  116. package/dist/{index-d34f6r87.js → index-6rq3r5zb.js} +1 -1
  117. package/dist/{index-9j4zwcht.js → index-7v51awbn.js} +158 -672
  118. package/dist/index-anmwxs87.js +29 -0
  119. package/dist/{index-qeb82z37.js → index-q4b917zs.js} +1 -1
  120. package/dist/index-spp0ra5c.js +36 -0
  121. package/dist/{keepalive-w4aakbyk.js → keepalive-6cf311t5.js} +4 -4
  122. package/dist/lead-fjy418cw.js +69 -0
  123. package/dist/{maintenance-hbptpk6p.js → maintenance-a4bm1bmt.js} +1 -1
  124. package/dist/{oauth-refresh-sweep-217semrv.js → oauth-refresh-sweep-3h0dfw1k.js} +3 -3
  125. package/dist/{onboard-vz2s5df3.js → onboard-myn5hraa.js} +3 -3
  126. package/dist/{opencode-adapter-gtfd7d7v.js → opencode-adapter-p2nkb5am.js} +5 -5
  127. package/dist/{otel-impl-aefave37.js → otel-impl-a1a0ef6b.js} +2 -2
  128. package/dist/{pi-mono-adapter-f8h5yseh.js → pi-mono-adapter-rzmpb72a.js} +117 -23
  129. package/dist/{pricing-refresh-m1yhh9ya.js → pricing-refresh-83drrnxj.js} +3 -3
  130. package/dist/{rbac-roles-svj72cdf.js → rbac-roles-0fb6e7pj.js} +2 -2
  131. package/dist/{rbac-roles-rrw8twdz.js → rbac-roles-s8y6ecb5.js} +1 -1
  132. package/dist/{render-v2-amwbnfxh.js → render-v2-twmbgrz4.js} +3 -3
  133. package/dist/{resume-9cd6wf6z.js → resume-zfqc36xj.js} +7 -7
  134. package/dist/{runtime-fp4vrx7m.js → runtime-qwggdpqr.js} +1 -1
  135. package/dist/{session-access-21t6yjyg.js → session-access-sc9pn9da.js} +4 -4
  136. package/dist/{session-source-backfill-pxbzcsc6.js → session-source-backfill-qh07xr8g.js} +3 -3
  137. package/dist/{setup-4592tqz4.js → setup-4fr0m7a1.js} +1 -1
  138. package/dist/{virtual-modules-gzfnpmnz.js → virtual-modules-fh4ha15b.js} +2 -2
  139. package/dist/{work-objects-t9tcf174.js → work-objects-0wr8zxct.js} +20 -20
  140. package/dist/worker-0c5tyzqe.js +69 -0
  141. package/dist/{worker-follow-up-q32jh4yk.js → worker-follow-up-ktyyazyp.js} +17 -17
  142. package/dist/{x-b6wxntdt.js → x-vmmye1kb.js} +2 -2
  143. package/openapi.json +97 -32
  144. package/package.json +1 -1
  145. package/src/be/db/session-summaries.ts +8 -2
  146. package/src/be/db/session-tree.ts +174 -61
  147. package/src/be/db/session-visibility.ts +5 -4
  148. package/src/be/db.ts +8 -4
  149. package/src/be/migrations/259_session_summaries_open_tasks.sql +111 -0
  150. package/src/be/model-validation.ts +5 -1
  151. package/src/be/pricing-normalize.ts +7 -2
  152. package/src/be/scripts/typecheck.ts +1 -0
  153. package/src/be/seed/schedules-seeder.ts +135 -9
  154. package/src/be/seed/workflows-seeder.ts +65 -1
  155. package/src/be/seed-pricing.ts +23 -0
  156. package/src/be/steering.ts +50 -3
  157. package/src/be/swarm-config-guard.ts +1 -0
  158. package/src/commands/provider-credentials.ts +29 -2
  159. package/src/commands/runner.ts +86 -5
  160. package/src/extensions/contract-types.generated.ts +1 -1
  161. package/src/extensions/contract.ts +27 -1
  162. package/src/extensions/dispatcher.ts +4 -0
  163. package/src/http/agents.ts +1 -0
  164. package/src/http/mcp-bridge.ts +4 -0
  165. package/src/integrations/chat-sdk/inbound.ts +15 -5
  166. package/src/integrations/chat-sdk/steering.ts +3 -1
  167. package/src/kv-reserved-namespaces.ts +2 -0
  168. package/src/prompts/base-prompt.ts +4 -1
  169. package/src/prompts/session-templates.ts +26 -0
  170. package/src/providers/acp-adapter.ts +190 -26
  171. package/src/providers/acp-targets.ts +306 -6
  172. package/src/providers/amp-adapter.ts +1 -1
  173. package/src/providers/codex-adapter.ts +9 -44
  174. package/src/providers/codex-oauth/flow.ts +24 -3
  175. package/src/providers/codex-oauth/storage.ts +56 -17
  176. package/src/providers/codex-oauth/subscription.ts +67 -0
  177. package/src/providers/fx-adapter.ts +211 -0
  178. package/src/providers/index.ts +5 -1
  179. package/src/providers/mcp-loopback-proxy.ts +94 -0
  180. package/src/providers/pi-mono-adapter.ts +98 -13
  181. package/src/providers/pricing-sources.md +8 -0
  182. package/src/providers/reasoning-effort.ts +17 -1
  183. package/src/providers/types.ts +2 -1
  184. package/src/scheduler/scheduler.ts +36 -3
  185. package/src/scripts-runtime/sdk-allowlist.ts +1 -0
  186. package/src/scripts-runtime/types/stdlib.d.ts +5 -0
  187. package/src/scripts-runtime/types/swarm-extension.d.ts +33 -0
  188. package/src/scripts-runtime/types/swarm-sdk.d.ts +5 -0
  189. package/src/server.ts +7 -0
  190. package/src/slack/assistant.ts +17 -1
  191. package/src/slack/handlers.ts +18 -5
  192. package/src/slack/left-threads.ts +65 -0
  193. package/src/slack/steering.ts +3 -0
  194. package/src/slack/thread-buffer.ts +40 -16
  195. package/src/tests/acp-adapter.test.ts +79 -0
  196. package/src/tests/base-prompt.test.ts +12 -0
  197. package/src/tests/chat-sdk-inbound.test.ts +30 -0
  198. package/src/tests/chatgpt-subscription-model-groups.test.ts +62 -0
  199. package/src/tests/chatgpt-subscription-pi.test.ts +170 -0
  200. package/src/tests/chatgpt-subscription-session-bench.test.ts +198 -0
  201. package/src/tests/codex-oauth-credential-info.test.ts +40 -0
  202. package/src/tests/codex-oauth-refresh-egress.test.ts +194 -0
  203. package/src/tests/context-window.test.ts +7 -0
  204. package/src/tests/explicit-model-validation.test.ts +99 -0
  205. package/src/tests/extensions-run-context.test.ts +11 -0
  206. package/src/tests/extensions-steering.test.ts +353 -0
  207. package/src/tests/extensions-validate.test.ts +5 -0
  208. package/src/tests/fixtures/extensions/steering-policy/hooks.ts +39 -0
  209. package/src/tests/fixtures/extensions/steering-policy/manifest.json +7 -0
  210. package/src/tests/fixtures/fake-fx.ts +167 -0
  211. package/src/tests/fixtures/fx/acp-responses-recorded.json +157 -0
  212. package/src/tests/fixtures/fx/acp-updates-recorded.jsonl +15 -0
  213. package/src/tests/fx-adapter.test.ts +771 -0
  214. package/src/tests/grok-adapter.test.ts +36 -0
  215. package/src/tests/mcp-loopback-proxy.test.ts +193 -0
  216. package/src/tests/model-tier-resolution.test.ts +3 -3
  217. package/src/tests/pricing-refresh.test.ts +5 -5
  218. package/src/tests/prompt-template-session.test.ts +5 -3
  219. package/src/tests/provider-registration.test.ts +2 -1
  220. package/src/tests/query-audit-plans.test.ts +3 -2
  221. package/src/tests/script-connections.test.ts +2 -0
  222. package/src/tests/seed-model-profiles.test.ts +2 -2
  223. package/src/tests/seed-schedules.test.ts +138 -0
  224. package/src/tests/session-cost-recompute-unit.test.ts +39 -0
  225. package/src/tests/session-costs-golden.test.ts +26 -0
  226. package/src/tests/session-summaries-plans.test.ts +120 -0
  227. package/src/tests/session-summaries.test.ts +72 -12
  228. package/src/tests/sessions-tree.test.ts +11 -3
  229. package/src/tests/slack-leave-thread.test.ts +431 -0
  230. package/src/tests/tool-annotations.test.ts +2 -1
  231. package/src/tests/workflow-engine-v2.test.ts +42 -0
  232. package/src/tests/workflow-script-principal.test.ts +207 -7
  233. package/src/tools/slack-leave-thread.ts +104 -0
  234. package/src/tools/steer-task.ts +3 -2
  235. package/src/tools/tool-config.ts +2 -1
  236. package/src/types.ts +19 -0
  237. package/src/utils/codex-auth-failure.ts +30 -0
  238. package/src/utils/context-window.ts +4 -1
  239. package/src/utils/credentials.ts +7 -0
  240. package/src/utils/fx-models.ts +43 -0
  241. package/src/workflows/cooldown.ts +3 -2
  242. package/src/workflows/engine.ts +24 -9
  243. package/templates/schedules/dreaming-daily/config.json +1 -1
  244. package/templates/schedules/dreaming-daily/content.md +6 -14
  245. package/templates/skills/swarm-docs/content.md +1 -0
  246. package/templates/workflows/dreaming/content.md +5 -1
  247. package/dist/acp-adapter-96dc64m2.js +0 -15
  248. package/dist/handlers-jg7v74ty.js +0 -31
  249. package/dist/index-8c1zbz59.js +0 -29
  250. package/dist/index-8k8tmfxd.js +0 -32
  251. package/dist/index-hq617xdj.js +0 -36
  252. package/dist/lead-567kw7x3.js +0 -68
  253. package/dist/worker-fjdqra6q.js +0 -68
@@ -12,13 +12,16 @@ import {
12
12
  createUser,
13
13
  getDbClient,
14
14
  getKv,
15
+ getLogsByTaskId,
15
16
  getTaskAttachments,
16
17
  getTaskById,
17
18
  initDb,
18
19
  startTask,
19
20
  upsertKv,
20
21
  } from "../be/db";
22
+ import { installExtension } from "../be/extensions/db";
21
23
  import { linkIdentity } from "../be/users";
24
+ import { enableExtension, stopExtensionRuntime } from "../extensions/lifecycle";
22
25
  import { resetFileStorageProviderForTests } from "../fs/registry";
23
26
  import { drainBackground } from "../integrations/chat-sdk/background";
24
27
  import { CHAT_CHANNEL_SETTINGS_NAMESPACE } from "../integrations/chat-sdk/reactions";
@@ -42,6 +45,8 @@ import {
42
45
  telegramUpdate,
43
46
  } from "./chat-sdk-harness";
44
47
 
48
+ import { loadBundleFixture } from "./fixtures/extensions/load";
49
+
45
50
  const TEST_DB_PATH = "./test-chat-sdk-inbound.sqlite";
46
51
  const ENV_KEYS = [
47
52
  "AGENT_FS_LOCAL_DIR",
@@ -271,6 +276,31 @@ describe("Chat SDK inbound pipeline", () => {
271
276
  expect(sendCount()).toBe(sendsBefore);
272
277
  });
273
278
 
279
+ test("an extension-blocked steer never falls back to a follow-up task", async () => {
280
+ await postTelegramUpdate(telegramUpdate(group(-2025), "@swarm_test_bot plan the report"));
281
+ await flushChatBuffers();
282
+ const [task] = await chatTasks(-2025);
283
+ expect(task).toBeDefined();
284
+ await startTask(task!.id);
285
+ const bundle = await loadBundleFixture("steering-policy");
286
+ const { extension } = await installExtension({ ...bundle, config: { action: "block" } });
287
+ await enableExtension(extension.id);
288
+ try {
289
+ const blocked = telegramUpdate(group(-2025), "Do not deliver this correction");
290
+ await postTelegramUpdate(blocked);
291
+ await flushChatBuffers();
292
+ expect(await chatTasks(-2025)).toHaveLength(1);
293
+ expect(await steeringRows(task!.id)).toEqual([]);
294
+ expect(reactionsOn(-2025, blocked)).toEqual(["👎"]);
295
+ expect(
296
+ (await getLogsByTaskId(task!.id)).filter((log) => log.eventType === "task_steering"),
297
+ ).toEqual([]);
298
+ } finally {
299
+ await stopExtensionRuntime();
300
+ await getDbClient().run("DELETE FROM extensions WHERE id = ?", [extension.id]);
301
+ }
302
+ });
303
+
274
304
  test("a channel that opts into text acks also gets the steering reply", async () => {
275
305
  await upsertKv({
276
306
  namespace: CHAT_CHANNEL_SETTINGS_NAMESPACE,
@@ -0,0 +1,62 @@
1
+ /**
2
+ * The pi model picker's ChatGPT subscription group: present only while a
3
+ * `codex_oauth` pool slot exists, listing the codex harness models under
4
+ * `openai-codex/<id>`.
5
+ */
6
+
7
+ import { describe, expect, test } from "bun:test";
8
+ import type { SwarmConfig } from "../../apps/ui/src/api/types";
9
+ import {
10
+ CHATGPT_SUBSCRIPTION_LABEL,
11
+ effortLevelsFor,
12
+ findKnownModel,
13
+ modelGroupsForHarness,
14
+ } from "../../apps/ui/src/lib/agent-runtime-models";
15
+
16
+ function configRow(key: string, value: string): SwarmConfig {
17
+ return { key, value } as SwarmConfig;
18
+ }
19
+
20
+ const withSlot = [configRow("codex_oauth_0", "********")];
21
+
22
+ describe("ChatGPT subscription group", () => {
23
+ test("hidden on pi without a codex_oauth slot", () => {
24
+ const groups = modelGroupsForHarness("pi", [configRow("codex_oauth_0", "")], undefined);
25
+ expect(groups.find((g) => g.provider === CHATGPT_SUBSCRIPTION_LABEL)).toBeUndefined();
26
+ });
27
+
28
+ test("shown and enabled on pi with a slot, holding the codex list as openai-codex ids", () => {
29
+ const group = modelGroupsForHarness("pi", withSlot, undefined).find(
30
+ (g) => g.provider === CHATGPT_SUBSCRIPTION_LABEL,
31
+ );
32
+ const codexIds = modelGroupsForHarness("codex", undefined, undefined)[0].models.map(
33
+ (m) => `openai-codex/${m.id}`,
34
+ );
35
+ expect(group?.enabled).toBe(true);
36
+ expect(group?.models.map((m) => m.id)).toEqual(codexIds);
37
+ expect(group?.models.every((m) => m.cost === undefined)).toBe(true);
38
+ });
39
+
40
+ test("not offered on opencode or codex", () => {
41
+ for (const harness of ["opencode", "codex"] as const) {
42
+ const groups = modelGroupsForHarness(harness, withSlot, undefined);
43
+ expect(groups.find((g) => g.provider === CHATGPT_SUBSCRIPTION_LABEL)).toBeUndefined();
44
+ }
45
+ });
46
+
47
+ test("a stored subscription id displays as its catalog model and keeps its effort levels", () => {
48
+ const known = findKnownModel("openai-codex/gpt-5.6-sol");
49
+ expect(known?.provider).toBe(CHATGPT_SUBSCRIPTION_LABEL);
50
+ expect(known?.label).not.toBe("openai-codex/gpt-5.6-sol");
51
+ expect(effortLevelsFor("pi", "openai-codex/gpt-5.6-sol")).toEqual(
52
+ effortLevelsFor("pi", "openai/gpt-5.6-sol"),
53
+ );
54
+ expect(effortLevelsFor("pi", "openai-codex/gpt-5.6-sol").length).toBeGreaterThan(0);
55
+ });
56
+
57
+ test("a mixed-case prefix keeps its effort levels", () => {
58
+ expect(effortLevelsFor("pi", "OpenAI-Codex/gpt-5.6-sol")).toEqual(
59
+ effortLevelsFor("pi", "openai/gpt-5.6-sol"),
60
+ );
61
+ });
62
+ });
@@ -0,0 +1,170 @@
1
+ /**
2
+ * pi on the ChatGPT subscription pool (`openai-codex/<id>` models): the
3
+ * credential handed to pi never carries the pool's refresh token, the gate
4
+ * reports ready on pool slots, and the model resolves to pi-ai's ChatGPT
5
+ * backend provider.
6
+ */
7
+
8
+ import { describe, expect, test } from "bun:test";
9
+ import { CodexOAuthRefreshError } from "../providers/codex-oauth/storage";
10
+ import {
11
+ chatgptSubscriptionAuthFailure,
12
+ loadChatgptSubscriptionCredential,
13
+ } from "../providers/codex-oauth/subscription";
14
+ import {
15
+ checkPiMonoCredentials,
16
+ createPiRuntimeAuth,
17
+ resolveModel,
18
+ } from "../providers/pi-mono-adapter";
19
+ import { supportedReasoningLevels } from "../providers/reasoning-effort";
20
+ import { isCodexAuthFailureReason } from "../utils/codex-auth-failure";
21
+
22
+ const CREDS = {
23
+ access: "subscription-access-token-for-tests",
24
+ refresh: "pool-refresh-token-must-not-leak",
25
+ expires: Date.now() + 24 * 60 * 60 * 1000,
26
+ accountId: "acct-subscription",
27
+ };
28
+ const CONFIG = {
29
+ apiUrl: "http://localhost:3013",
30
+ apiKey: "test-key",
31
+ codexSlot: 2,
32
+ model: "openai-codex/gpt-5.6-sol",
33
+ };
34
+ const noFiles = { existsSync: () => false };
35
+
36
+ describe("loadChatgptSubscriptionCredential", () => {
37
+ test("revalidates the runner's slot and blanks the refresh token", async () => {
38
+ let slotSeen: number | undefined;
39
+ const creds = await loadChatgptSubscriptionCredential(CONFIG, {
40
+ getValid: async (_url, _key, slot) => {
41
+ slotSeen = slot;
42
+ return CREDS;
43
+ },
44
+ });
45
+ expect(slotSeen).toBe(2);
46
+ expect(creds).toEqual({ ...CREDS, refresh: "" });
47
+ });
48
+
49
+ test("no pool slot is an auth error, not a silent fallback", async () => {
50
+ await expect(
51
+ loadChatgptSubscriptionCredential({ ...CONFIG, codexSlot: undefined }),
52
+ ).rejects.toThrow("[auth-error]");
53
+ });
54
+
55
+ test("a rejected refresh carries the codex pool failure reason", async () => {
56
+ await expect(
57
+ loadChatgptSubscriptionCredential(CONFIG, {
58
+ getValid: async () => {
59
+ throw new CodexOAuthRefreshError(2, "refresh_rejected", "abcde", 401, "revoked");
60
+ },
61
+ }),
62
+ ).rejects.toThrow("Codex pool slot 2 [...abcde] revalidation failed: refresh rejected");
63
+ });
64
+
65
+ test("a quarantined slot fails fast", async () => {
66
+ await expect(
67
+ loadChatgptSubscriptionCredential(CONFIG, { getValid: async () => null }),
68
+ ).rejects.toThrow("no credentials found in config store");
69
+ });
70
+ });
71
+
72
+ describe("checkPiMonoCredentials with a subscription model", () => {
73
+ const model = { MODEL_OVERRIDE: "openai-codex/gpt-5.6-sol" };
74
+
75
+ test("ready when the pool has slots", async () => {
76
+ const status = await checkPiMonoCredentials(
77
+ { ...model, CODEX_OAUTH_POOL_SLOTS: "2" },
78
+ { homeDir: "/home/test", fs: noFiles },
79
+ );
80
+ expect(status).toEqual({ ready: true, missing: [], satisfiedBy: "env" });
81
+ });
82
+
83
+ test("not ready without slots, even with other keys or a pi auth.json", async () => {
84
+ const status = await checkPiMonoCredentials(
85
+ { ...model, OPENAI_API_KEY: "sk-test" },
86
+ { homeDir: "/home/test", fs: { existsSync: () => true } },
87
+ );
88
+ expect(status.ready).toBe(false);
89
+ expect(status.missing).toEqual(["CODEX_OAUTH"]);
90
+ });
91
+
92
+ test("pool slots alone satisfy the permissive (no MODEL_OVERRIDE) check", async () => {
93
+ const status = await checkPiMonoCredentials(
94
+ { CODEX_OAUTH_POOL_SLOTS: "1" },
95
+ { homeDir: "/home/test", fs: noFiles },
96
+ );
97
+ expect(status.ready).toBe(true);
98
+ });
99
+ });
100
+
101
+ describe("pi model resolution and auth", () => {
102
+ test("a shipped id resolves to pi-ai's openai-codex model", () => {
103
+ const model = resolveModel("openai-codex/gpt-5.6-sol", {});
104
+ expect({ provider: model?.provider, id: model?.id }).toEqual({
105
+ provider: "openai-codex",
106
+ id: "gpt-5.6-sol",
107
+ });
108
+ });
109
+
110
+ test("a catalog id pi-ai does not ship reuses the provider's transport", () => {
111
+ const model = resolveModel("openai-codex/gpt-7-test", {});
112
+ expect({ provider: model?.provider, id: model?.id, api: model?.api }).toEqual({
113
+ provider: "openai-codex",
114
+ id: "gpt-7-test",
115
+ api: "openai-codex-responses",
116
+ });
117
+ });
118
+
119
+ test("the session runtime authenticates openai-codex from memory with no refresh token", async () => {
120
+ const runtime = await createPiRuntimeAuth({}, undefined, { ...CREDS, refresh: "" });
121
+ const auth = await runtime.getAuth("openai-codex");
122
+ expect(auth?.auth.apiKey).toBe(CREDS.access);
123
+ const listed = await runtime.listCredentials();
124
+ expect(listed).toContainEqual({ providerId: "openai-codex", type: "oauth" });
125
+ });
126
+ });
127
+
128
+ describe("chatgptSubscriptionAuthFailure", () => {
129
+ const revoked =
130
+ "Your authentication token has been invalidated. Please try signing in again. (token_invalidated)";
131
+
132
+ test("a rejected subscription login mid-run is a counted auth failure for its slot", () => {
133
+ const reason = chatgptSubscriptionAuthFailure(revoked, CONFIG);
134
+ expect(reason).toStartWith("[auth-error] Codex pool slot 2 authentication failed");
135
+ expect(reason).toContain(revoked);
136
+ expect(isCodexAuthFailureReason(reason)).toBe(true);
137
+ expect(
138
+ isCodexAuthFailureReason(chatgptSubscriptionAuthFailure("401 Unauthorized", CONFIG)),
139
+ ).toBe(true);
140
+ });
141
+
142
+ test("the backend's 401 for an unparseable access token counts too", () => {
143
+ // Live body for a slot holding a malformed access token (pi surfaces it without the status).
144
+ const reason = chatgptSubscriptionAuthFailure(
145
+ "Could not parse your authentication token. Please try signing in again.",
146
+ CONFIG,
147
+ );
148
+ expect(isCodexAuthFailureReason(reason)).toBe(true);
149
+ });
150
+
151
+ test("other errors, models and slotless sessions are left alone", () => {
152
+ expect(chatgptSubscriptionAuthFailure("Codex stream ended without a stop reason", CONFIG)).toBe(
153
+ undefined,
154
+ );
155
+ expect(
156
+ chatgptSubscriptionAuthFailure(revoked, { ...CONFIG, model: "openai/gpt-5.6-sol" }),
157
+ ).toBe(undefined);
158
+ expect(chatgptSubscriptionAuthFailure(revoked, { ...CONFIG, codexSlot: undefined })).toBe(
159
+ undefined,
160
+ );
161
+ });
162
+ });
163
+
164
+ describe("subscription effort levels on the worker", () => {
165
+ test("a mixed-case prefix keeps the openai levels", () => {
166
+ const levels = supportedReasoningLevels("pi", "OpenAI-Codex/gpt-5.6-sol");
167
+ expect(levels.length).toBeGreaterThan(0);
168
+ expect(levels).toEqual(supportedReasoningLevels("pi", "openai/gpt-5.6-sol"));
169
+ });
170
+ });
@@ -0,0 +1,198 @@
1
+ /**
2
+ * The auth-failure bench for a ChatGPT subscription pool slot (pi on
3
+ * `openai-codex/<id>`). A refresh rejected while the session starts fails
4
+ * `createSession`, before the completion-time report, so the runner reports it
5
+ * there. A login rejected mid-run reaches the completion-time report as a
6
+ * counted `[auth-error]`. Session starts go through `spawnProviderProcess`,
7
+ * the boundary the new-task and resumed-task paths share, against the real
8
+ * key-status routes.
9
+ */
10
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
11
+ import { mkdtemp, rm, unlink } from "node:fs/promises";
12
+ import { createServer, type Server } from "node:http";
13
+ import { tmpdir } from "node:os";
14
+ import { join } from "node:path";
15
+ import { closeDb, initDb } from "../be/db";
16
+ import { reportKeyCompletionOutcome, spawnProviderProcess } from "../commands/runner";
17
+ import { handleApiKeys } from "../http/api-keys";
18
+ import { deriveCodexKeySuffix } from "../providers/codex-oauth/auth-json";
19
+ import { CodexOAuthRefreshError } from "../providers/codex-oauth/storage";
20
+ import {
21
+ chatgptSubscriptionAuthFailure,
22
+ loadChatgptSubscriptionCredential,
23
+ } from "../providers/codex-oauth/subscription";
24
+ import type { CodexOAuthCredentials } from "../providers/codex-oauth/types";
25
+ import type { ProviderSessionConfig } from "../providers/types";
26
+ import { listenOnFreePort } from "./test-net";
27
+
28
+ const TEST_DB = `./test-chatgpt-subscription-session-bench-${Date.now()}.sqlite`;
29
+ const TASK_ID = "00000000-0000-4000-8000-000000000001";
30
+ const AGENT_ID = "00000000-0000-4000-8000-0000000000aa";
31
+ const MODEL = "openai-codex/gpt-5.6-sol";
32
+
33
+ let server: Server;
34
+ let baseUrl = "";
35
+ let logDir = "";
36
+ /** The pool the config store serves: `codex_oauth_<slot>` entries. */
37
+ let pool: Array<{ slot: number; creds: CodexOAuthCredentials }> = [];
38
+
39
+ beforeAll(async () => {
40
+ initDb(TEST_DB);
41
+ logDir = await mkdtemp(join(tmpdir(), "chatgpt-sub-bench-"));
42
+ server = createServer(async (req, res) => {
43
+ const url = new URL(req.url || "/", "http://localhost");
44
+ if (req.method === "GET" && url.pathname === "/api/config/resolved") {
45
+ const configs = pool.map(({ slot, creds }) => ({
46
+ key: `codex_oauth_${slot}`,
47
+ value: JSON.stringify(creds),
48
+ }));
49
+ res.writeHead(200, { "Content-Type": "application/json" });
50
+ res.end(JSON.stringify({ configs }));
51
+ return;
52
+ }
53
+ const handled = await handleApiKeys(
54
+ req,
55
+ res,
56
+ url.pathname.split("/").filter(Boolean),
57
+ url.searchParams,
58
+ );
59
+ if (!handled) {
60
+ res.writeHead(404, { "Content-Type": "application/json" });
61
+ res.end("{}");
62
+ }
63
+ });
64
+ baseUrl = `http://127.0.0.1:${await listenOnFreePort(server)}`;
65
+ });
66
+
67
+ afterAll(async () => {
68
+ await new Promise<void>((resolve) => server.close(() => resolve()));
69
+ closeDb();
70
+ for (const suffix of ["", "-wal", "-shm"]) await unlink(`${TEST_DB}${suffix}`).catch(() => {});
71
+ await rm(logDir, { recursive: true, force: true });
72
+ });
73
+
74
+ async function slotStatus(keySuffix: string) {
75
+ const res = await fetch(`${baseUrl}/api/keys/status?keyType=CODEX_OAUTH`);
76
+ const body = (await res.json()) as { keys: Array<Record<string, unknown>> };
77
+ return body.keys.find((k) => k.keySuffix === keySuffix);
78
+ }
79
+
80
+ /** Serve a one-slot pool; returns the slot's key suffix. */
81
+ function serveSlot(slot: number, accountId: string): string {
82
+ const creds: CodexOAuthCredentials = {
83
+ access: `at_synthetic_${accountId}`,
84
+ refresh: `rt_synthetic_${accountId}`,
85
+ expires: Date.now() - 1000,
86
+ accountId,
87
+ };
88
+ pool = [{ slot, creds }];
89
+ return deriveCodexKeySuffix(creds.access, accountId);
90
+ }
91
+
92
+ /** A pi adapter whose session start revalidates the slot with `getValid`. */
93
+ function piAdapter(getValid: () => Promise<never>) {
94
+ const slots: Array<number | undefined> = [];
95
+ const adapter = {
96
+ name: "pi",
97
+ traits: {},
98
+ canResume: async () => false,
99
+ formatCommand: (name: string) => name,
100
+ createSession: async (config: ProviderSessionConfig) => {
101
+ slots.push(config.codexSlot);
102
+ await loadChatgptSubscriptionCredential(config, { getValid });
103
+ throw new Error("unreachable");
104
+ },
105
+ } as unknown as Parameters<typeof spawnProviderProcess>[0];
106
+ return { adapter, slots };
107
+ }
108
+
109
+ /**
110
+ * Options as the poll loop passes them: a new task carries the claim's
111
+ * resolved model, a resumed task only its own pin and never a native resume
112
+ * id.
113
+ */
114
+ function startOpts(kind: "new" | "resumed") {
115
+ const base = {
116
+ prompt: kind === "new" ? "do the task" : "resume the task",
117
+ logFile: join(logDir, `${kind}.jsonl`),
118
+ role: "worker",
119
+ apiUrl: baseUrl,
120
+ apiKey: "test-key",
121
+ agentId: AGENT_ID,
122
+ runnerSessionId: crypto.randomUUID(),
123
+ iteration: 1,
124
+ taskId: TASK_ID,
125
+ model: MODEL,
126
+ harnessProvider: "pi" as const,
127
+ cwd: logDir,
128
+ };
129
+ return kind === "new"
130
+ ? { ...base, resolvedModel: MODEL, modelSource: "task", triggerSurface: "api" }
131
+ : { ...base, resumeSessionId: undefined };
132
+ }
133
+
134
+ function startSession(
135
+ adapter: Parameters<typeof spawnProviderProcess>[0],
136
+ kind: "new" | "resumed",
137
+ ) {
138
+ return spawnProviderProcess(adapter, startOpts(kind), logDir, true);
139
+ }
140
+
141
+ describe("ChatGPT subscription slot bench", () => {
142
+ for (const [kind, slot] of [
143
+ ["new", 1],
144
+ ["resumed", 2],
145
+ ] as const) {
146
+ test(`a refresh rejected at ${kind}-task session start benches the slot on the second start`, async () => {
147
+ const keySuffix = serveSlot(slot, `acct-${kind}`);
148
+ const { adapter, slots } = piAdapter(async () => {
149
+ throw new CodexOAuthRefreshError(slot, "refresh_rejected", keySuffix, 401, "invalid_grant");
150
+ });
151
+
152
+ for (let i = 0; i < 2; i++) {
153
+ await expect(startSession(adapter, kind)).rejects.toThrow("refresh rejected (401");
154
+ }
155
+
156
+ expect(slots).toEqual([slot, slot]);
157
+ expect(await slotStatus(keySuffix)).toMatchObject({
158
+ status: "rate_limited",
159
+ consecutiveAuthFailures: 2,
160
+ });
161
+ });
162
+ }
163
+
164
+ test("a lock timeout at session start is transient and never counts", async () => {
165
+ const keySuffix = serveSlot(4, "acct-lock");
166
+ const { adapter } = piAdapter(async () => {
167
+ throw new CodexOAuthRefreshError(4, "lock_timeout", keySuffix);
168
+ });
169
+
170
+ for (let i = 0; i < 2; i++) {
171
+ await expect(startSession(adapter, "new")).rejects.toThrow("refresh lock");
172
+ }
173
+
174
+ expect((await slotStatus(keySuffix))?.consecutiveAuthFailures ?? 0).toBe(0);
175
+ });
176
+
177
+ test("a login rejected mid-run benches the slot through the completion report", async () => {
178
+ const reason = chatgptSubscriptionAuthFailure(
179
+ "Your authentication token has been invalidated. Please try signing in again.",
180
+ { codexSlot: 3, model: "openai-codex/gpt-5.6-sol" },
181
+ );
182
+ for (let i = 0; i < 2; i++) {
183
+ await reportKeyCompletionOutcome({
184
+ apiUrl: baseUrl,
185
+ apiKey: "test-key",
186
+ credential: { keyType: "CODEX_OAUTH", keySuffix: "subC3", keyIndex: 3 },
187
+ taskId: TASK_ID,
188
+ exitCode: 1,
189
+ failureReason: reason,
190
+ });
191
+ }
192
+
193
+ expect(await slotStatus("subC3")).toMatchObject({
194
+ status: "rate_limited",
195
+ consecutiveAuthFailures: 2,
196
+ });
197
+ });
198
+ });
@@ -143,4 +143,44 @@ describe("resolveCodexOAuthCredentialInfo — issue #1102 bug 1", () => {
143
143
  expect(info!.selection.index).toBe(0);
144
144
  expect(info!.selection.total).toBe(1);
145
145
  });
146
+
147
+ it("poolOnly (pi subscription) picks a slot without writing ~/.codex/auth.json", async () => {
148
+ const creds = {
149
+ access:
150
+ "header.eyJodHRwczovL2FwaS5vcGVuYWkuY29tL2F1dGgiOnsiY2hhdGdwdF9hY2NvdW50X2lkIjoiYWNjLXBvb2wifX0=.sig",
151
+ refresh: "rt_pool",
152
+ expires: Date.now() + 3_600_000,
153
+ accountId: "acc-pool",
154
+ };
155
+ globalThis.fetch = (async (url: string | URL | Request) => {
156
+ const href = typeof url === "string" ? url : url instanceof URL ? url.href : url.url;
157
+ if (href.includes("/api/config/resolved")) {
158
+ return new Response(
159
+ JSON.stringify({ configs: [{ key: "codex_oauth_1", value: JSON.stringify(creds) }] }),
160
+ { status: 200, headers: { "Content-Type": "application/json" } },
161
+ );
162
+ }
163
+ if (href.includes("/api/keys/available")) {
164
+ return new Response(JSON.stringify({ availableIndices: [1] }), { status: 200 });
165
+ }
166
+ return new Response("not found", { status: 404 });
167
+ }) as typeof fetch;
168
+
169
+ const info = await resolveCodexOAuthCredentialInfo(MOCK_API_URL, MOCK_API_KEY, {
170
+ poolOnly: true,
171
+ });
172
+
173
+ expect(info?.isPoolBacked).toBe(true);
174
+ expect(info?.selection.index).toBe(1);
175
+ expect(await Bun.file(join(tmpHome, ".codex", "auth.json")).exists()).toBe(false);
176
+ });
177
+
178
+ it("poolOnly ignores a standalone auth.json: no slot means no subscription", async () => {
179
+ mockEmptyConfigStore();
180
+ await writeStandaloneAuthJson();
181
+
182
+ expect(
183
+ await resolveCodexOAuthCredentialInfo(MOCK_API_URL, MOCK_API_KEY, { poolOnly: true }),
184
+ ).toBeNull();
185
+ });
146
186
  });