@desplega.ai/agent-swarm 2.5.3 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (349) hide show
  1. package/README.md +2 -2
  2. package/dist/{acp-adapter-gr2fcyyj.js → acp-adapter-dgfakfpw.js} +9 -9
  3. package/dist/{actions-wv766ghf.js → actions-fbqma8aa.js} +21 -21
  4. package/dist/{amp-adapter-speb906t.js → amp-adapter-en0jy6hs.js} +4 -4
  5. package/dist/{app-ag29n14f.js → app-k8a5cgpp.js} +3 -3
  6. package/dist/{assistant-3x8bz3eg.js → assistant-a16433gr.js} +30 -26
  7. package/dist/{boot-reembed-jsmfrpzf.js → boot-reembed-6c1sjfnr.js} +1 -1
  8. package/dist/{boot-reembed-5b3d2eea.js → boot-reembed-f4nn561n.js} +1 -1
  9. package/dist/{boot-scrub-sweep-s73gerxy.js → boot-scrub-sweep-d4w9gajf.js} +4 -4
  10. package/dist/{boot-seeds-5zmax2wr.js → boot-seeds-9bmkspyw.js} +4 -4
  11. package/dist/{claude-adapter-hswdnm3t.js → claude-adapter-nys1kjxa.js} +9 -9
  12. package/dist/{claude-managed-adapter-cvbzj1h6.js → claude-managed-adapter-55207973.js} +3 -3
  13. package/dist/{claude-managed-setup-3k8m3hhc.js → claude-managed-setup-3cm35gey.js} +11 -4
  14. package/dist/{claude-sdk-session-fkbyk66x.js → claude-sdk-session-nw5yd8cp.js} +9313 -9260
  15. package/dist/{cli-210xd1f4.js → cli-03gg9pd1.js} +4 -4
  16. package/dist/{cli-kxpfx2xr.js → cli-0z1xjzwa.js} +2 -2
  17. package/dist/cli-1tem6ns9.js +894 -0
  18. package/dist/{cli-25j01dpj.js → cli-24j1jeeb.js} +3 -3
  19. package/dist/{cli-ta8bd7th.js → cli-2sjhpsns.js} +2 -2
  20. package/dist/{cli-p859hcc8.js → cli-39aet655.js} +2 -2
  21. package/dist/{cli-g1cw7xtt.js → cli-39rrw8yp.js} +1 -1
  22. package/dist/{cli-e2ar4bp1.js → cli-3aqd4jaa.js} +121 -47
  23. package/dist/{cli-jjznk3f7.js → cli-3fwqy71j.js} +69 -35
  24. package/dist/{cli-p9rafdrb.js → cli-3gfvmzht.js} +7 -7
  25. package/dist/{cli-03vpq45j.js → cli-3ndmpxxv.js} +2 -2
  26. package/dist/{cli-aecdcr77.js → cli-4hxrf0p4.js} +228 -74
  27. package/dist/{cli-mz9afxxf.js → cli-4rf0f5vg.js} +15 -3
  28. package/dist/{cli-qzme5949.js → cli-5rj83bam.js} +284 -202
  29. package/dist/{cli-fsdprwrg.js → cli-5sckpqcx.js} +2 -2
  30. package/dist/{cli-cz5nmdxt.js → cli-683y0b3a.js} +2 -2
  31. package/dist/{cli-3ngdk5te.js → cli-6y51ryva.js} +2 -2
  32. package/dist/{cli-n4c8e3s5.js → cli-708wdy32.js} +1 -1
  33. package/dist/{cli-pasyvjmm.js → cli-737pyqxr.js} +1 -1
  34. package/dist/{cli-gx4rm0qx.js → cli-7enjwya9.js} +1 -1
  35. package/dist/{cli-6x9ma5w5.js → cli-803188pb.js} +1 -1
  36. package/dist/{cli-avdvgcay.js → cli-9abdgn5t.js} +28 -9
  37. package/dist/{cli-99xgw26r.js → cli-a2rr0fmg.js} +1 -1
  38. package/dist/{cli-e5bxnjps.js → cli-a34pr3w1.js} +7 -7
  39. package/dist/{cli-502eqv93.js → cli-abkm71vr.js} +1 -1
  40. package/dist/{cli-mmhj8vd7.js → cli-ag2yrnt5.js} +11 -2
  41. package/dist/{cli-n45mr7zh.js → cli-aj8djyqt.js} +5 -5
  42. package/dist/{cli-1fc85mvt.js → cli-art7c1zh.js} +5 -5
  43. package/dist/{cli-40jsnrjm.js → cli-az25fn5f.js} +10 -8
  44. package/dist/{cli-9s05422w.js → cli-bqztfyve.js} +1 -1
  45. package/dist/{cli-bn0rewsy.js → cli-c081bj3b.js} +2 -2
  46. package/dist/{cli-svrj71pg.js → cli-c2habvsf.js} +12 -12
  47. package/dist/{cli-s49hpn37.js → cli-ca6banwz.js} +85 -19
  48. package/dist/{cli-hhxjnjzs.js → cli-cz37jqss.js} +12 -10
  49. package/dist/{cli-6n8axjv1.js → cli-d2dpf95n.js} +2 -2
  50. package/dist/{cli-tx00c0h6.js → cli-dqygeeh5.js} +17 -1
  51. package/dist/{cli-z21whrrg.js → cli-emhjcj2y.js} +17 -17
  52. package/dist/{cli-e8jyq7d8.js → cli-esakngh8.js} +7 -3
  53. package/dist/{cli-ha9rmwgq.js → cli-fb8m699c.js} +2 -2
  54. package/dist/{cli-35gtk1p6.js → cli-ffvsj2qw.js} +44 -13
  55. package/dist/{cli-ce0ak9ms.js → cli-g20jjwt4.js} +26 -26
  56. package/dist/{cli-kxawjrxg.js → cli-g8ka5j4j.js} +772 -194
  57. package/dist/{cli-vwn81jbs.js → cli-gf95q3bm.js} +5 -5
  58. package/dist/{cli-s53vp709.js → cli-gnsb1zxa.js} +2 -2
  59. package/dist/{cli-nwyw3zhg.js → cli-gxee0bqa.js} +1 -1
  60. package/dist/{cli-gpv60278.js → cli-hfeb68z4.js} +7 -7
  61. package/dist/{cli-0ebey9hg.js → cli-j6rktc91.js} +1 -1
  62. package/dist/{cli-ewm3pwbq.js → cli-k21gx5ht.js} +46 -23
  63. package/dist/{cli-egjggehm.js → cli-kf4jree9.js} +2 -2
  64. package/dist/{cli-dabehxhp.js → cli-kt907da6.js} +1 -1
  65. package/dist/{cli-p0rzjkya.js → cli-m5a9m0ak.js} +32 -13
  66. package/dist/{cli-fzcn3d87.js → cli-m70bajzr.js} +3 -3
  67. package/dist/{cli-w1zwctj2.js → cli-mtnj4fwa.js} +11 -5
  68. package/dist/{cli-gpsz7g88.js → cli-mvjz5an3.js} +16 -12
  69. package/dist/{cli-hcpj2xqz.js → cli-n7asmce6.js} +225 -3
  70. package/dist/{cli-y561h5a9.js → cli-nn6vx2mq.js} +5 -5
  71. package/dist/cli-nq677csy.js +140 -0
  72. package/dist/{cli-a5ebc95n.js → cli-pfk92eqz.js} +18 -15
  73. package/dist/{cli-p6zs9h4y.js → cli-rr7zx19j.js} +767 -249
  74. package/dist/{cli-qgf8emrn.js → cli-sqma6vw1.js} +10 -7
  75. package/dist/{cli-0xk505e8.js → cli-sxgtf5fw.js} +1 -1
  76. package/dist/{cli-nzfnt2gs.js → cli-tf1n0r0b.js} +1 -1
  77. package/dist/{cli-qqzmjxkf.js → cli-v49fkghp.js} +11 -6
  78. package/dist/{cli-x77gen2p.js → cli-w2656ka1.js} +7 -7
  79. package/dist/{cli-5kqnemen.js → cli-yvy48785.js} +1 -1
  80. package/dist/{cli-8s5wx3nz.js → cli-zqne1qcj.js} +44 -17
  81. package/dist/cli.js +22 -22
  82. package/dist/{codex-adapter-n8ka66vf.js → codex-adapter-d95q4qsn.js} +7 -7
  83. package/dist/{codex-hook-e2he7aca.js → codex-hook-brpnycn5.js} +1 -1
  84. package/dist/{codex-login-rn7c5gxt.js → codex-login-8cg0vhf2.js} +1 -1
  85. package/dist/{codex-session-runner-fsebts37.js → codex-session-runner-gdwd8h0c.js} +8 -8
  86. package/dist/{commands-t04rt2pc.js → commands-jqmgk54b.js} +2 -2
  87. package/dist/{controller-rgna04r2.js → controller-z3jq589f.js} +30 -4
  88. package/dist/{controllers-connect-bqm1rtft.js → controllers-connect-r2za39ef.js} +23 -23
  89. package/dist/{cursor-adapter-swr2k95y.js → cursor-adapter-4mr4jgkw.js} +2 -3
  90. package/dist/{db-a4m7haw0.js → db-ppv2tze7.js} +3 -3
  91. package/dist/{db-24ck09xy.js → db-w36wwb5g.js} +4 -2
  92. package/dist/{devin-adapter-4ycy4vrf.js → devin-adapter-byen2cxt.js} +1 -1
  93. package/dist/{dispatcher-nwf9ss0q.js → dispatcher-pv5h77vf.js} +20 -20
  94. package/dist/{docker-nfy2gc8t.js → docker-fdzyewpz.js} +4 -4
  95. package/dist/{dsh-adapter-kpdnhjst.js → dsh-adapter-d4pdpb3r.js} +2 -3
  96. package/dist/{e2b-9gkdhm3c.js → e2b-222hsajc.js} +2 -2
  97. package/dist/{e2b-cjtf8k1z.js → e2b-fn3zacvt.js} +2 -2
  98. package/dist/{easter-egg-3d-e4qv8dxj.js → easter-egg-3d-4sp4dvmc.js} +1 -1
  99. package/dist/{event-bus-3z78ep5n.js → event-bus-pghg8hqy.js} +1 -1
  100. package/dist/{execute-0xpz8950.js → execute-jev839y1.js} +1 -1
  101. package/dist/{external-channel-j1e2p34p.js → external-channel-y6nkzhnn.js} +2 -2
  102. package/dist/{grok-adapter-qtjz0whk.js → grok-adapter-t08b9ntm.js} +10 -10
  103. package/dist/handlers-eqsnmv95.js +41 -0
  104. package/dist/{heartbeat-vtesda5d.js → heartbeat-vn31xwdk.js} +23 -23
  105. package/dist/{hook-b2hhj6rd.js → hook-6d0zbvvt.js} +6 -5
  106. package/dist/{hook-8wqaszda.js → hook-cpjs8q51.js} +13 -7
  107. package/dist/{http-mxcqhhcs.js → http-9c6medda.js} +1085 -136
  108. package/dist/{index-spwxjgay.js → index-38t35h0n.js} +20 -20
  109. package/dist/{index-1r8wfpzs.js → index-g6k9tctj.js} +1 -1
  110. package/dist/{index-srg522m6.js → index-k0yt1tvq.js} +20 -20
  111. package/dist/{index-31ceyf42.js → index-pkma1yf8.js} +1 -1
  112. package/dist/{index-n1vbvta9.js → index-qhjf2htm.js} +29 -24
  113. package/dist/index-yqgbfgaj.js +43 -0
  114. package/dist/{keepalive-dsd8rtgx.js → keepalive-em0117bp.js} +4 -4
  115. package/dist/lead-23e7z59t.js +79 -0
  116. package/dist/{maintenance-jqmqwnet.js → maintenance-kbme3qy9.js} +1 -1
  117. package/dist/{oauth-refresh-sweep-q2c3bnvm.js → oauth-refresh-sweep-xysj4yse.js} +3 -3
  118. package/dist/{onboard-w4yd7s8t.js → onboard-p6q0vz97.js} +3 -3
  119. package/dist/{opencode-adapter-9tppvxar.js → opencode-adapter-esv3k9yn.js} +7 -7
  120. package/dist/{otel-impl-f36byzbd.js → otel-impl-54qhqh1z.js} +2 -2
  121. package/dist/{pi-mono-adapter-tqsge1ay.js → pi-mono-adapter-mymbwhj0.js} +41 -14
  122. package/dist/{pricing-refresh-xvak5p4v.js → pricing-refresh-yb75wv33.js} +3 -3
  123. package/dist/{rbac-roles-gfn30fb8.js → rbac-roles-1vp2mw4p.js} +2 -2
  124. package/dist/{rbac-roles-682k2mt5.js → rbac-roles-dexhc9x8.js} +1 -1
  125. package/dist/render-v2-tjay5hhf.js +16 -0
  126. package/dist/{resume-2dvq348g.js → resume-8ayf37g5.js} +11 -9
  127. package/dist/{runtime-bt5pkpm1.js → runtime-hqe5cqcd.js} +1 -1
  128. package/dist/{session-access-7f21ekq9.js → session-access-fdhp987s.js} +4 -4
  129. package/dist/session-source-backfill-qgtc86z1.js +110 -0
  130. package/dist/{setup-epffxtbk.js → setup-4rrmqcq3.js} +1 -1
  131. package/dist/{virtual-modules-mnfbh5mr.js → virtual-modules-52km00d6.js} +2 -2
  132. package/dist/{work-objects-xz1a5rqk.js → work-objects-p28fgd70.js} +23 -23
  133. package/dist/worker-e3w7vye1.js +79 -0
  134. package/dist/{x-pbpezr0s.js → x-wxrzvzx8.js} +2 -2
  135. package/openapi.json +1323 -16
  136. package/package.json +5 -5
  137. package/plugin/commands/create-pr.md +2 -2
  138. package/plugin/pi-skills/create-pr/SKILL.md +2 -2
  139. package/src/assets/source-key.ts +95 -0
  140. package/src/be/db/api-keys.ts +80 -0
  141. package/src/be/db/approvals.ts +48 -0
  142. package/src/be/db/session-labels.ts +28 -0
  143. package/src/be/db/session-moves.ts +5 -6
  144. package/src/be/db/session-tree.ts +153 -14
  145. package/src/be/db/tasks/read.ts +11 -0
  146. package/src/be/db/tasks/write.ts +21 -6
  147. package/src/be/db/workflow-idempotency.ts +183 -0
  148. package/src/be/db/workflow-runs.ts +500 -7
  149. package/src/be/db-retention.ts +61 -6
  150. package/src/be/db.ts +126 -131
  151. package/src/be/memory/providers/openai-embedding.ts +5 -1
  152. package/src/be/migrations/242_workflow_idempotency_keys.sql +40 -0
  153. package/src/be/migrations/244_api_key_validation.sql +4 -0
  154. package/src/be/onboarding.ts +4 -0
  155. package/src/be/scripts/typecheck.ts +1 -1
  156. package/src/be/sealed-json.ts +1 -0
  157. package/src/be/seed-scripts/catalog/compound-insights.inline.ts +2 -2
  158. package/src/be/seed-scripts/catalog/compound-insights.ts +2 -2
  159. package/src/be/seed-skills/bundled-files.generated.json +1 -1
  160. package/src/be/session-source-backfill.ts +163 -0
  161. package/src/be/sql-connections.ts +7 -1
  162. package/src/be/swarm-config-guard.ts +3 -0
  163. package/src/be/users.ts +9 -0
  164. package/src/comb/review-batch.ts +28 -1
  165. package/src/comb/templates.ts +7 -2
  166. package/src/commands/claude-managed-setup.ts +7 -1
  167. package/src/commands/controller.ts +33 -0
  168. package/src/commands/credential-refresh.ts +76 -1
  169. package/src/commands/credential-wait.ts +2 -2
  170. package/src/commands/provider-credentials.ts +210 -170
  171. package/src/commands/runner.ts +31 -6
  172. package/src/extensions/apply-task-create.ts +64 -11
  173. package/src/extensions/contract-types.generated.ts +1 -1
  174. package/src/extensions/contract.ts +30 -0
  175. package/src/extensions/dispatcher.ts +4 -0
  176. package/src/extensions/post-bridge.ts +8 -0
  177. package/src/hooks/context-bound-note.ts +196 -0
  178. package/src/hooks/hook.ts +12 -0
  179. package/src/http/all-routes.ts +2 -0
  180. package/src/http/api-keys.ts +53 -0
  181. package/src/http/assets.ts +15 -0
  182. package/src/http/codex-oauth-accounts.ts +191 -0
  183. package/src/http/comb.ts +15 -2
  184. package/src/http/config.ts +49 -17
  185. package/src/http/core.ts +6 -0
  186. package/src/http/fleet-benchmark.ts +5 -2
  187. package/src/http/idempotency-key.ts +117 -0
  188. package/src/http/index.ts +11 -0
  189. package/src/http/integrations.ts +5 -1
  190. package/src/http/kv.ts +8 -3
  191. package/src/http/onboarding.ts +6 -1
  192. package/src/http/pool-keys.ts +354 -0
  193. package/src/http/sessions.ts +37 -12
  194. package/src/http/stats.ts +1 -0
  195. package/src/http/status.ts +26 -12
  196. package/src/http/tasks.ts +3 -0
  197. package/src/http/used-by.ts +7 -2
  198. package/src/http/workflows.ts +207 -33
  199. package/src/integrations/chat-sdk/inbound.ts +27 -0
  200. package/src/kv-reserved-namespaces.ts +19 -0
  201. package/src/prompts/context-bound-note.ts +48 -0
  202. package/src/prompts/session-templates.ts +25 -1
  203. package/src/providers/acp-targets.ts +1 -1
  204. package/src/providers/amp-adapter.ts +30 -8
  205. package/src/providers/claude-adapter.ts +34 -5
  206. package/src/providers/claude-managed-adapter.ts +42 -9
  207. package/src/providers/claude-sdk-session.ts +10 -0
  208. package/src/providers/claude-session-events.ts +13 -0
  209. package/src/providers/codex-adapter.ts +83 -11
  210. package/src/providers/opencode-adapter.ts +18 -0
  211. package/src/providers/pi-mono-adapter.ts +35 -0
  212. package/src/runtime-providers/docker.ts +6 -2
  213. package/src/scheduler/scheduler.ts +3 -2
  214. package/src/scripts-runtime/swarm-sdk.ts +7 -1
  215. package/src/scripts-runtime/types/stdlib.d.ts +1 -1
  216. package/src/scripts-runtime/types/swarm-extension.d.ts +52 -8
  217. package/src/scripts-runtime/types/swarm-sdk.d.ts +1 -1
  218. package/src/slack/assistant.ts +7 -0
  219. package/src/slack/channel-labels.ts +74 -0
  220. package/src/slack/handlers.ts +8 -0
  221. package/src/slack/inbound-files.ts +5 -2
  222. package/src/slack/thread-buffer.ts +4 -0
  223. package/src/tasks/sibling-awareness.ts +12 -3
  224. package/src/telemetry-context.ts +5 -6
  225. package/src/telemetry-identity.ts +3 -3
  226. package/src/tests/amp-adapter.test.ts +97 -0
  227. package/src/tests/api-keys-validation.test.ts +170 -0
  228. package/src/tests/approval-requests.test.ts +10 -7
  229. package/src/tests/approval-sweeps.test.ts +12 -6
  230. package/src/tests/base-prompt.test.ts +1 -1
  231. package/src/tests/claude-hook-output.test.ts +79 -1
  232. package/src/tests/claude-managed-adapter.test.ts +321 -0
  233. package/src/tests/claude-routes-e2e.test.ts +7 -2
  234. package/src/tests/codex-adapter.test.ts +163 -0
  235. package/src/tests/codex-credential-pools.test.ts +120 -0
  236. package/src/tests/codex-gateway-route.test.ts +165 -0
  237. package/src/tests/codex-oauth-accounts.test.ts +357 -0
  238. package/src/tests/comb-review-batch.test.ts +63 -2
  239. package/src/tests/config-pool-keys.test.ts +533 -0
  240. package/src/tests/context-bound-note.test.ts +269 -0
  241. package/src/tests/credential-check.test.ts +290 -2
  242. package/src/tests/credential-refresh.test.ts +172 -1
  243. package/src/tests/db-retention.test.ts +9 -3
  244. package/src/tests/db-task-read-contract.test.ts +1 -0
  245. package/src/tests/db-task-write-contract.test.ts +1 -3
  246. package/src/tests/docs-index-footprint.test.js +3 -3
  247. package/src/tests/extensions-pre-task.test.ts +325 -0
  248. package/src/tests/fetch-resolved-env.test.ts +17 -0
  249. package/src/tests/fixtures/extensions/record-blocks/hooks.ts +10 -0
  250. package/src/tests/fixtures/extensions/record-blocks/manifest.json +7 -0
  251. package/src/tests/fixtures/extensions/record-task-create/hooks.ts +9 -0
  252. package/src/tests/fixtures/extensions/record-task-create/manifest.json +7 -0
  253. package/src/tests/fixtures/fake-amp.ts +22 -8
  254. package/src/tests/fixtures/held-template-render.ts +42 -0
  255. package/src/tests/fixtures/workflow-idempotency-trigger.child.ts +73 -0
  256. package/src/tests/fleet-benchmark.test.ts +8 -0
  257. package/src/tests/get-workflow-run-tool.test.ts +98 -0
  258. package/src/tests/integrations-http.test.ts +32 -2
  259. package/src/tests/internal-ai/complete-structured.test.ts +37 -1
  260. package/src/tests/internal-ai/credentials.test.ts +23 -0
  261. package/src/tests/kv-http.test.ts +32 -0
  262. package/src/tests/kv-tool.test.ts +25 -0
  263. package/src/tests/multi-runtime-registration.test.ts +12 -12
  264. package/src/tests/onboarding.test.ts +108 -1
  265. package/src/tests/openai-embedding-credentials.test.ts +35 -0
  266. package/src/tests/openapi-route-headers.test.ts +2 -0
  267. package/src/tests/opencode-adapter.test.ts +134 -0
  268. package/src/tests/openrouter-attribution-call-sites.test.ts +2 -2
  269. package/src/tests/org-website.test.ts +36 -0
  270. package/src/tests/pi-mono-adapter.test.ts +105 -0
  271. package/src/tests/prompt-template-session.test.ts +4 -3
  272. package/src/tests/provider-key-probes.test.ts +181 -0
  273. package/src/tests/rbac-e2e-helpers.test.ts +88 -1
  274. package/src/tests/rbac-e2e-helpers.ts +36 -11
  275. package/src/tests/runner-push-delivery.test.ts +25 -0
  276. package/src/tests/runtime-controller-liveness.test.ts +133 -0
  277. package/src/tests/runtime-providers-docker.test.ts +2 -0
  278. package/src/tests/script-connections.test.ts +5 -0
  279. package/src/tests/script-run-config-scope.test.ts +91 -3
  280. package/src/tests/script-runs-retention.test.ts +212 -0
  281. package/src/tests/sdk-allowlist.test.ts +39 -0
  282. package/src/tests/session-logs.test.ts +70 -0
  283. package/src/tests/session-source-backfill.test.ts +429 -0
  284. package/src/tests/session-source-keys.test.ts +236 -0
  285. package/src/tests/session-source-labels.test.ts +419 -0
  286. package/src/tests/sessions-detail-batch.test.ts +2 -0
  287. package/src/tests/sessions-move.test.ts +131 -0
  288. package/src/tests/sessions-tree.test.ts +336 -2
  289. package/src/tests/slack-inbound-files.test.ts +103 -23
  290. package/src/tests/status.test.ts +101 -18
  291. package/src/tests/tasks-workflow-run-filter.test.ts +146 -0
  292. package/src/tests/test-net.ts +31 -0
  293. package/src/tests/transcription.test.ts +8 -0
  294. package/src/tests/used-by-route.test.ts +11 -0
  295. package/src/tests/workflow-cascade-cancel.test.ts +1341 -0
  296. package/src/tests/workflow-foreach-supersede-retry.test.ts +488 -0
  297. package/src/tests/workflow-http-v2.test.ts +131 -0
  298. package/src/tests/workflow-idempotency.test.ts +915 -0
  299. package/src/tests/workflow-step-order.test.ts +92 -0
  300. package/src/tests/workflow-sub-workflow.test.ts +30 -0
  301. package/src/tools/get-tasks.ts +8 -0
  302. package/src/tools/kv/kv-read-auth.ts +4 -2
  303. package/src/tools/task-action.ts +1 -1
  304. package/src/tools/workflows/cancel-workflow-run.ts +1 -1
  305. package/src/tools/workflows/get-workflow-run.ts +14 -4
  306. package/src/types.ts +24 -0
  307. package/src/utils/company-profile.ts +51 -1
  308. package/src/utils/context-window.ts +8 -6
  309. package/src/utils/credentials.ts +5 -2
  310. package/src/utils/internal-ai/complete-structured.ts +5 -0
  311. package/src/utils/internal-ai/credentials.ts +4 -3
  312. package/src/utils/provider-key-probes.ts +328 -0
  313. package/src/utils/transcription.ts +4 -1
  314. package/src/workflows/approval-cancel.ts +15 -13
  315. package/src/workflows/approval-notifications.ts +5 -1
  316. package/src/workflows/checkpoint.ts +6 -3
  317. package/src/workflows/engine.ts +30 -8
  318. package/src/workflows/executors/foreach.ts +2 -2
  319. package/src/workflows/executors/sub-workflow.ts +8 -1
  320. package/src/workflows/foreach-driver.ts +10 -3
  321. package/src/workflows/foreach-join.ts +29 -6
  322. package/src/workflows/recovery.ts +6 -1
  323. package/src/workflows/resume.ts +168 -56
  324. package/src/workflows/retry-poller.ts +49 -3
  325. package/src/workflows/task-step-routing.ts +29 -7
  326. package/templates/ai-toolbox.manifest.json +8 -8
  327. package/templates/community/open-prompt-showreel/PLAYBOOK.md +5 -6
  328. package/templates/community/open-prompt-showreel/lead-prompt.md +1 -1
  329. package/templates/community/open-prompt-showreel/skills/open-prompt-showreel/SKILL.md +4 -4
  330. package/templates/skills/agent-browser/SKILL.md +4 -4
  331. package/templates/skills/agent-browser/content.md +4 -4
  332. package/templates/skills/code-quality/config.json +1 -1
  333. package/templates/skills/code-quality/content.md +2 -2
  334. package/templates/skills/delegate-work/content.md +29 -25
  335. package/templates/skills/delegate-work/files/scripts/codex-exec.sh +5 -5
  336. package/templates/skills/qa/content.md +2 -2
  337. package/templates/skills/researching/content.md +4 -0
  338. package/templates/skills/swarm-scripts/SKILL.md +1 -0
  339. package/templates/skills/swarm-scripts/content.md +1 -0
  340. package/templates/skills/work-on-task/SKILL.md +1 -1
  341. package/templates/skills/work-on-task/content.md +1 -1
  342. package/dist/cli-7421jzsw.js +0 -84
  343. package/dist/cli-e45e4m6b.js +0 -413
  344. package/dist/cli-zw88zjba.js +0 -307
  345. package/dist/handlers-19xr6dnz.js +0 -41
  346. package/dist/index-kw2mmat7.js +0 -43
  347. package/dist/lead-zyerk1x2.js +0 -79
  348. package/dist/render-v2-hgytz8nn.js +0 -12
  349. package/dist/worker-42t8emjt.js +0 -79
@@ -362,7 +362,14 @@ describe("onboarding state", () => {
362
362
  signals: { providers: Array<Record<string, unknown>> };
363
363
  };
364
364
  expect(body.signals.providers).toEqual([
365
- { provider: "claude", state: "verified", workers: 2, verifiedWorkers: 2 },
365
+ {
366
+ provider: "claude",
367
+ state: "verified",
368
+ workers: 2,
369
+ verifiedWorkers: 2,
370
+ error: null,
371
+ warning: null,
372
+ },
366
373
  ]);
367
374
  expect(body.state.steps.ai).toMatchObject({
368
375
  status: "done",
@@ -370,6 +377,57 @@ describe("onboarding state", () => {
370
377
  });
371
378
  });
372
379
 
380
+ test("GET carries the latest live test error and the pool warning", async () => {
381
+ const report = (liveTest: {
382
+ ok: boolean;
383
+ error: string | null;
384
+ latency_ms: number;
385
+ testedAt: number;
386
+ warning?: string;
387
+ }) => ({
388
+ ready: true,
389
+ missing: [],
390
+ satisfiedBy: "env" as const,
391
+ hint: null,
392
+ liveTest,
393
+ reportedAt: Date.now(),
394
+ reportKind: "boot" as const,
395
+ });
396
+ const failing = await createAgent({ name: "codex-worker", isLead: false, status: "idle" });
397
+ await setAgentHarnessProvider(failing.id, "codex");
398
+ await updateAgentCredStatus(
399
+ failing.id,
400
+ report({ ok: false, error: "HTTP 401: bad key", latency_ms: 5, testedAt: Date.now() }),
401
+ );
402
+ const warned = await createAgent({ name: "pi-worker", isLead: false, status: "idle" });
403
+ await setAgentHarnessProvider(warned.id, "pi");
404
+ await updateAgentCredStatus(
405
+ warned.id,
406
+ report({
407
+ ok: true,
408
+ error: null,
409
+ latency_ms: 5,
410
+ testedAt: Date.now(),
411
+ warning: "1 of 2 keys failed: key ending abcde (HTTP 401).",
412
+ }),
413
+ );
414
+
415
+ const body = (await (await request("/api/onboarding")).json()) as {
416
+ signals: { providers: Array<Record<string, unknown>> };
417
+ };
418
+ const byProvider = Object.fromEntries(body.signals.providers.map((p) => [p.provider, p]));
419
+ expect(byProvider.codex).toMatchObject({
420
+ state: "configured",
421
+ error: "HTTP 401: bad key",
422
+ warning: null,
423
+ });
424
+ expect(byProvider.pi).toMatchObject({
425
+ state: "verified",
426
+ error: null,
427
+ warning: "1 of 2 keys failed: key ending abcde (HTTP 401).",
428
+ });
429
+ });
430
+
373
431
  test("GET derives integrations from environment configuration", async () => {
374
432
  process.env.GITHUB_TOKEN = "example-github-token";
375
433
  const body = (await (await request("/api/onboarding")).json()) as {
@@ -853,6 +911,55 @@ describe("onboarding memory probe", () => {
853
911
  expect((await getState()).steps.memory.status).toBe("todo");
854
912
  });
855
913
 
914
+ test("probes and saves the first member of a reused OpenAI pool", async () => {
915
+ const originalFetch = globalThis.fetch;
916
+ let authorization: string | null = null;
917
+ process.env.OPENAI_API_KEY = "example-first, example-second";
918
+ globalThis.fetch = (async (input, init) => {
919
+ const req = new Request(input, init);
920
+ if (new URL(req.url).hostname !== "api.openai.com") return originalFetch(input, init);
921
+ authorization = req.headers.get("authorization");
922
+ return embeddingResponse(512);
923
+ }) as typeof fetch;
924
+ try {
925
+ const response = await request("/api/onboarding/memory", {
926
+ method: "POST",
927
+ body: { preset: "openai", reuseKey: "OPENAI_API_KEY" },
928
+ });
929
+ expect(await response.json()).toMatchObject({ ok: true, dimensions: 512 });
930
+ expect(authorization).toBe("Bearer example-first");
931
+ const configs = await getSwarmConfigs({ scope: "global", key: "EMBEDDING_API_KEY" });
932
+ expect(configs[0]?.value).toBe("example-first");
933
+ } finally {
934
+ globalThis.fetch = originalFetch;
935
+ }
936
+ });
937
+
938
+ for (const keyVar of ["EMBEDDING_API_KEY", "OPENAI_API_KEY"]) {
939
+ test(`existing memory probes the first member of ${keyVar}`, async () => {
940
+ const originalFetch = globalThis.fetch;
941
+ let authorization: string | null = null;
942
+ process.env[keyVar] = "example-first, example-second";
943
+ process.env.EMBEDDING_API_BASE_URL = "https://api.openai.com/v1";
944
+ globalThis.fetch = (async (input, init) => {
945
+ const req = new Request(input, init);
946
+ if (new URL(req.url).hostname !== "api.openai.com") return originalFetch(input, init);
947
+ authorization = req.headers.get("authorization");
948
+ return embeddingResponse(512);
949
+ }) as typeof fetch;
950
+ try {
951
+ const response = await request("/api/onboarding/memory", {
952
+ method: "POST",
953
+ body: { preset: "existing" },
954
+ });
955
+ expect(await response.json()).toMatchObject({ ok: true, dimensions: 512 });
956
+ expect(authorization).toBe("Bearer example-first");
957
+ } finally {
958
+ globalThis.fetch = originalFetch;
959
+ }
960
+ });
961
+ }
962
+
856
963
  test("does not send a reused OpenAI key to another host", async () => {
857
964
  let hits = 0;
858
965
  const fake = startEmbeddingServer(() => {
@@ -0,0 +1,35 @@
1
+ import { expect, test } from "bun:test";
2
+ import { OpenAIEmbeddingProvider } from "../be/memory/providers/openai-embedding";
3
+
4
+ for (const source of ["config", "EMBEDDING_API_KEY", "OPENAI_API_KEY"] as const) {
5
+ test(`embedding selects one key from ${source}`, async () => {
6
+ const originalFetch = globalThis.fetch;
7
+ const saved = {
8
+ EMBEDDING_API_KEY: process.env.EMBEDDING_API_KEY,
9
+ OPENAI_API_KEY: process.env.OPENAI_API_KEY,
10
+ };
11
+ delete process.env.EMBEDDING_API_KEY;
12
+ delete process.env.OPENAI_API_KEY;
13
+ const pool = "example-first, example-second";
14
+ if (source !== "config") process.env[source] = pool;
15
+ let authorization: string | null = null;
16
+ globalThis.fetch = (async (input, init) => {
17
+ authorization = new Request(input, init).headers.get("authorization");
18
+ return Response.json({ data: [{ embedding: [0.5] }] });
19
+ }) as typeof fetch;
20
+ try {
21
+ const provider = new OpenAIEmbeddingProvider({
22
+ dimensions: 1,
23
+ ...(source === "config" ? { apiKey: pool } : {}),
24
+ });
25
+ expect(await provider.embed("test input")).toEqual(new Float32Array([0.5]));
26
+ expect(["Bearer example-first", "Bearer example-second"]).toContain(authorization);
27
+ } finally {
28
+ globalThis.fetch = originalFetch;
29
+ for (const [key, value] of Object.entries(saved)) {
30
+ if (value === undefined) delete process.env[key];
31
+ else process.env[key] = value;
32
+ }
33
+ }
34
+ });
35
+ }
@@ -75,6 +75,8 @@ describe("runtime identity header in OpenAPI", () => {
75
75
  "GET /api/poll",
76
76
  "POST /api/mcp-bridge",
77
77
  "POST /api/scripts/run",
78
+ "POST /api/workflows",
79
+ "POST /api/workflows/{id}/trigger",
78
80
  "POST /close",
79
81
  "POST /ping",
80
82
  "PUT /api/agents/{id}/credential-status",
@@ -10,6 +10,7 @@
10
10
  import { afterAll, afterEach, beforeAll, describe, expect, spyOn, test } from "bun:test";
11
11
  import type { OpenCodeEvent } from "@opencode/sdk";
12
12
  import { normalizeSessionLogs, type SessionLogRecord } from "../../apps/ui/src/logs-parser";
13
+ import { renderContextBoundNote } from "../prompts/context-bound-note";
13
14
  import {
14
15
  _setOpencodeSdkLoaderForTests,
15
16
  _setOpencodeSessionHooksForTests,
@@ -22,6 +23,7 @@ import {
22
23
  import { createSkillInvokeTracker, type SkillInvoke } from "../providers/skill-invoke";
23
24
  import type { ProviderEvent, ProviderSessionConfig } from "../providers/types";
24
25
  import { clearVolatileSecretsForTesting } from "../utils/secret-scrubber";
26
+ import { HELD_NOTE_TEXT, holdTemplateRender } from "./fixtures/held-template-render";
25
27
 
26
28
  const SESSION_ID = "ses_test";
27
29
 
@@ -68,6 +70,7 @@ type FakeHostOptions = {
68
70
  tools?: string[];
69
71
  /** Model responses handed to the plugin's `http.response` hook at startup. */
70
72
  responses?: Response[];
73
+ syntheticError?: Error;
71
74
  };
72
75
 
73
76
  /**
@@ -85,6 +88,7 @@ function makeFakeHost(events: OpenCodeEvent[], opts: FakeHostOptions = {}) {
85
88
  createOptions: undefined as unknown as Record<string, unknown>,
86
89
  created: [] as unknown[],
87
90
  prompts: [] as Array<Record<string, unknown>>,
91
+ synthetics: [] as Array<Record<string, unknown>>,
88
92
  interrupts: [] as unknown[],
89
93
  closeCalls: 0,
90
94
  mcpListCalls: 0,
@@ -116,6 +120,11 @@ function makeFakeHost(events: OpenCodeEvent[], opts: FakeHostOptions = {}) {
116
120
  host.interrupts.push(input);
117
121
  return {};
118
122
  },
123
+ synthetic: async (input: Record<string, unknown>) => {
124
+ host.synthetics.push(input);
125
+ if (opts.syntheticError) throw opts.syntheticError;
126
+ return {};
127
+ },
119
128
  },
120
129
  events: {
121
130
  async *subscribe() {
@@ -815,6 +824,131 @@ describe("OpencodeSession — abort and steering", () => {
815
824
  });
816
825
  });
817
826
 
827
+ describe("OpencodeSession — context-bound note", () => {
828
+ const boundConfig = (threshold: string) =>
829
+ testConfig({ env: { CONTEXT_BOUND_NOTE_TOKENS: threshold } });
830
+ const step = (sessionID: string, input: number, read = 0, write = 0) =>
831
+ ev("session.step.ended", { sessionID, tokens: tokens(input, 10, read, write) });
832
+
833
+ test("steers the note once at the threshold, not again on a larger step", async () => {
834
+ const { session, host } = await startSession([], boundConfig("1000"));
835
+ // 600 input + 300 cache read + 100 cache write = 1,000 (output does not count).
836
+ session.handleOpencodeEvent(step(SESSION_ID, 500, 300, 100));
837
+ session.handleOpencodeEvent(step(SESSION_ID, 600, 300, 100));
838
+ session.handleOpencodeEvent(step(SESSION_ID, 5_000));
839
+ await waitFor(() => host.synthetics.length > 0);
840
+ await Bun.sleep(20);
841
+ expect(host.synthetics).toEqual([
842
+ {
843
+ sessionID: SESSION_ID,
844
+ text: await renderContextBoundNote(1_000, "openrouter/google/gemini-3-flash-preview"),
845
+ delivery: "steer",
846
+ resume: false,
847
+ },
848
+ ]);
849
+ expect(host.synthetics[0]?.text).toContain("1,000");
850
+ session.handleOpencodeEvent(succeeded);
851
+ expect((await session.waitForCompletion()).isError).toBe(false);
852
+ });
853
+
854
+ test("the note shows the window share for a known model and omits it for an unknown one", async () => {
855
+ const noteFor = async (model: string) => {
856
+ const { session, host } = await startSession(
857
+ [],
858
+ testConfig({ model, env: { CONTEXT_BOUND_NOTE_TOKENS: "1000" } }),
859
+ );
860
+ session.handleOpencodeEvent(step(SESSION_ID, 524_288));
861
+ await waitFor(() => host.synthetics.length > 0);
862
+ session.handleOpencodeEvent(succeeded);
863
+ await session.waitForCompletion();
864
+ return host.synthetics[0]?.text;
865
+ };
866
+ const rest =
867
+ "Keep this task bounded: finish the current step, store-progress, and complete or defer instead of starting new exploration.";
868
+ expect(await noteFor("openrouter/google/gemini-3-flash-preview")).toBe(
869
+ `Context is 524,288 tokens (50% of the 1,048,576-token window). ${rest}`,
870
+ );
871
+ expect(await noteFor("openrouter/made-up/model")).toBe(`Context is 524,288 tokens. ${rest}`);
872
+ });
873
+
874
+ test("off when CONTEXT_BOUND_NOTE_TOKENS is unset", async () => {
875
+ const { session, host } = await startSession([]);
876
+ session.handleOpencodeEvent(step(SESSION_ID, 900_000));
877
+ await Bun.sleep(20);
878
+ expect(host.synthetics).toEqual([]);
879
+ session.handleOpencodeEvent(succeeded);
880
+ await session.waitForCompletion();
881
+ });
882
+
883
+ test("no-op below the threshold and for a child session's steps", async () => {
884
+ const { session, host } = await startSession([], boundConfig("1000"));
885
+ session.handleOpencodeEvent(step(SESSION_ID, 999));
886
+ session.handleOpencodeEvent(step("ses_child", 50_000));
887
+ await Bun.sleep(20);
888
+ expect(host.synthetics).toEqual([]);
889
+ session.handleOpencodeEvent(succeeded);
890
+ await session.waitForCompletion();
891
+ });
892
+
893
+ test("off at 0", async () => {
894
+ const { session, host } = await startSession([], boundConfig("0"));
895
+ session.handleOpencodeEvent(step(SESSION_ID, 250_000));
896
+ await Bun.sleep(20);
897
+ expect(host.synthetics).toEqual([]);
898
+ session.handleOpencodeEvent(succeeded);
899
+ await session.waitForCompletion();
900
+ });
901
+
902
+ test("fails open on corrupt step tokens", async () => {
903
+ const { session, host } = await startSession([], boundConfig("1000"));
904
+ session.handleOpencodeEvent(
905
+ ev("session.step.ended", {
906
+ sessionID: SESSION_ID,
907
+ tokens: { input: "lots", output: 1, reasoning: 0, cache: { read: null, write: {} } },
908
+ }),
909
+ );
910
+ await Bun.sleep(20);
911
+ expect(host.synthetics).toEqual([]);
912
+ session.handleOpencodeEvent(succeeded);
913
+ expect((await session.waitForCompletion()).isError).toBe(false);
914
+ });
915
+
916
+ test("a rejected steer still lets the session complete", async () => {
917
+ const { session, host } = await startSession([], boundConfig("1000"), {
918
+ syntheticError: new Error("synthetic refused"),
919
+ });
920
+ session.handleOpencodeEvent(step(SESSION_ID, 5_000));
921
+ await waitFor(() => host.synthetics.length > 0);
922
+ session.handleOpencodeEvent(succeeded);
923
+ expect((await session.waitForCompletion()).isError).toBe(false);
924
+ });
925
+
926
+ test("a note rendered after the session completed is dropped", async () => {
927
+ const lateSynthetics = async (releaseInSession: boolean) => {
928
+ const held = holdTemplateRender();
929
+ try {
930
+ const { session, host } = await startSession([], boundConfig("1000"));
931
+ session.handleOpencodeEvent(step(SESSION_ID, 5_000));
932
+ await held.requested;
933
+ if (releaseInSession) {
934
+ held.release();
935
+ await waitFor(() => host.synthetics.length > 0);
936
+ }
937
+ session.handleOpencodeEvent(succeeded);
938
+ expect((await session.waitForCompletion()).isError).toBe(false);
939
+ held.release();
940
+ await Bun.sleep(50);
941
+ return host.synthetics.map((s) => s.text);
942
+ } finally {
943
+ held.stop();
944
+ }
945
+ };
946
+ expect(await lateSynthetics(false)).toEqual([]);
947
+ // Control: ready while the session runs.
948
+ expect(await lateSynthetics(true)).toEqual([HELD_NOTE_TEXT]);
949
+ });
950
+ });
951
+
818
952
  describe("OpenRouter attribution config", () => {
819
953
  test("overrides harness attribution while preserving custom headers and options", () => {
820
954
  const config = {
@@ -164,12 +164,12 @@ describe("workflow raw-llm and validate nodes", () => {
164
164
  });
165
165
  });
166
166
 
167
- describe("OpenRouter credential check (GET /models)", () => {
167
+ describe("OpenRouter credential check (GET /key)", () => {
168
168
  for (const c of CASES) {
169
169
  test(`${c.name}`, async () => {
170
170
  setEnv(c.env);
171
171
  await validateProviderCredentials("grok", { OPENROUTER_API_KEY: "example-or-key" });
172
- expect(requests[0]?.url.endsWith("/models")).toBe(true);
172
+ expect(requests[0]?.url.endsWith("/key")).toBe(true);
173
173
  expectAttribution(requests[0]?.headers, c.attributed);
174
174
  });
175
175
  }
@@ -5,6 +5,8 @@ import {
5
5
  COMPANY_ACQUISITION_SOURCES,
6
6
  COMPANY_TEAM_SIZES,
7
7
  COMPANY_USE_CASES,
8
+ reportedUseCase,
9
+ reportedUserFunction,
8
10
  } from "../utils/company-profile";
9
11
  import {
10
12
  isFilledOrgLogoUrl,
@@ -157,6 +159,40 @@ describe("company profile config guards", () => {
157
159
  }
158
160
  });
159
161
 
162
+ describe("profile values the telemetry catalog does not list yet", () => {
163
+ const input = {
164
+ orgId: "org_0123456789abcdef",
165
+ installationId: "install_0123456789abcdef",
166
+ isCloud: false,
167
+ isE2b: false,
168
+ swarmVersion: "1.0.0",
169
+ actor: { userId: "user-1" },
170
+ };
171
+
172
+ test("the config guards accept them", () => {
173
+ for (const value of ["content", "research", "seo", "admin"]) {
174
+ expect(validateConfigValue("SWARM_ORG_USE_CASE", value)).toBeNull();
175
+ }
176
+ });
177
+
178
+ test("stay out of the telemetry context, as if unset", () => {
179
+ for (const useCase of ["content", "research", "seo", "admin"]) {
180
+ expect(reportedUseCase(useCase)).toBeUndefined();
181
+ expect(buildContext({ ...input, useCase })).not.toHaveProperty("use_case");
182
+ }
183
+ for (const userFunction of ["marketing", "sales", "research"]) {
184
+ expect(reportedUserFunction(userFunction)).toBeUndefined();
185
+ expect(buildContext({ ...input, userFunction })).not.toHaveProperty("user_function");
186
+ }
187
+ });
188
+
189
+ test("the catalog values still report", () => {
190
+ const context = buildContext({ ...input, useCase: "docs", userFunction: "founder" });
191
+ expect(context.use_case).toBe("docs");
192
+ expect(context.user_function).toBe("founder");
193
+ });
194
+ });
195
+
160
196
  describe("org_website in the telemetry context", () => {
161
197
  const input = {
162
198
  orgId: "org_0123456789abcdef",
@@ -12,6 +12,7 @@ import { tmpdir } from "node:os";
12
12
  import { join } from "node:path";
13
13
  import { OPENAI_COMPATIBLE_PROVIDERS } from "@desplega/model-catalog";
14
14
  import * as piCodingAgent from "@earendil-works/pi-coding-agent";
15
+ import { renderContextBoundNote } from "../prompts/context-bound-note";
15
16
  import { configureHttpResolver, resetHttpResolver } from "../prompts/resolver";
16
17
  import {
17
18
  checkPiMonoCredentials,
@@ -24,6 +25,7 @@ import {
24
25
  writeSessionModelsJson,
25
26
  } from "../providers/pi-mono-adapter";
26
27
  import type { ProviderSessionConfig } from "../providers/types";
28
+ import { HELD_NOTE_TEXT, holdTemplateRender } from "./fixtures/held-template-render";
27
29
 
28
30
  describe("PiMonoAdapter", () => {
29
31
  test("name is 'pi'", () => {
@@ -1187,6 +1189,8 @@ function makeMockAgentSession(opts: {
1187
1189
  steerCalls?: string[];
1188
1190
  followUpCalls?: string[];
1189
1191
  steeringError?: string;
1192
+ /** Awaited after each call's events, as a real turn keeps running its tools. */
1193
+ afterEvents?: () => Promise<unknown>;
1190
1194
  }): AgentSession {
1191
1195
  const listeners: Array<(event: AgentSessionEvent) => void> = [];
1192
1196
  const scripts = opts.turns ?? [opts.events ?? []];
@@ -1210,6 +1214,7 @@ function makeMockAgentSession(opts: {
1210
1214
  for (const event of scripts[call] ?? []) {
1211
1215
  for (const l of listeners) l(event);
1212
1216
  }
1217
+ await opts.afterEvents?.();
1213
1218
  if (opts.throwError) throw new Error(opts.throwError);
1214
1219
  },
1215
1220
  getContextUsage: () => null,
@@ -1754,3 +1759,103 @@ describe("classifyAwsSdkError — all 4 categories (quick summary)", () => {
1754
1759
  }
1755
1760
  });
1756
1761
  });
1762
+
1763
+ describe("PiMonoSession context-bound note", () => {
1764
+ /** An assistant `message_end` with this request usage; a tool call unless `final`. */
1765
+ function assistantTurn(usage: unknown, final = false): AgentSessionEvent {
1766
+ return {
1767
+ type: "message_end",
1768
+ message: {
1769
+ role: "assistant",
1770
+ content: final
1771
+ ? [{ type: "text", text: "Done" }]
1772
+ : [{ type: "toolCall", id: "call-1", name: "bash", arguments: {} }],
1773
+ stopReason: final ? "stop" : "toolUse",
1774
+ usage,
1775
+ },
1776
+ } as unknown as AgentSessionEvent;
1777
+ }
1778
+
1779
+ const usage = (input: number) => ({ input, cacheRead: 300, cacheWrite: 200, output: 50 });
1780
+
1781
+ async function steersFor(
1782
+ threshold: string,
1783
+ events: AgentSessionEvent[],
1784
+ afterEvents: () => Promise<unknown> = () => Bun.sleep(50),
1785
+ model?: string,
1786
+ ): Promise<string[]> {
1787
+ const steerCalls: string[] = [];
1788
+ const session = new PiMonoSession(
1789
+ makeMockAgentSession({ events, steerCalls, afterEvents }),
1790
+ {
1791
+ ...makeSessionConfig(join(tmpLogDir, `bound-note-${crypto.randomUUID()}.log`)),
1792
+ env: { CONTEXT_BOUND_NOTE_TOKENS: threshold },
1793
+ ...(model ? { model } : {}),
1794
+ },
1795
+ false,
1796
+ );
1797
+ await session.waitForCompletion();
1798
+ return steerCalls;
1799
+ }
1800
+
1801
+ test("steers the note once when a tool-call request reaches the threshold", async () => {
1802
+ const steers = await steersFor("1000", [
1803
+ assistantTurn(usage(400)),
1804
+ assistantTurn(usage(500)),
1805
+ assistantTurn(usage(900)),
1806
+ assistantTurn(usage(1500), true),
1807
+ ]);
1808
+ // 500 + 300 + 200 reaches 1000; the larger later request does not steer again.
1809
+ expect(steers).toEqual([await renderContextBoundNote(1000)]);
1810
+ });
1811
+
1812
+ test("the note shows the window share for a known model and omits it for an unknown one", async () => {
1813
+ const events = () => [assistantTurn(usage(209_500)), assistantTurn(usage(400), true)];
1814
+ const rest =
1815
+ "Keep this task bounded: finish the current step, store-progress, and complete or defer instead of starting new exploration.";
1816
+ expect(await steersFor("200000", events(), undefined, "anthropic/claude-sonnet-4-6")).toEqual([
1817
+ `Context is 210,000 tokens (21% of the 1,000,000-token window). ${rest}`,
1818
+ ]);
1819
+ expect(await steersFor("200000", events(), undefined, "made-up/model")).toEqual([
1820
+ `Context is 210,000 tokens. ${rest}`,
1821
+ ]);
1822
+ });
1823
+
1824
+ test("no-op below the threshold and when disabled at 0", async () => {
1825
+ const events = [assistantTurn(usage(400)), assistantTurn(usage(400), true)];
1826
+ expect(await steersFor("1000", events)).toEqual([]);
1827
+ expect(await steersFor("0", events)).toEqual([]);
1828
+ });
1829
+
1830
+ test("fails open on missing or corrupt usage", async () => {
1831
+ const events = [
1832
+ assistantTurn(undefined),
1833
+ assistantTurn({ input: "lots", cacheRead: {}, cacheWrite: null }),
1834
+ assistantTurn(undefined, true),
1835
+ ];
1836
+ expect(await steersFor("1", events)).toEqual([]);
1837
+ });
1838
+
1839
+ test("never steers after the final reply, which would start another turn", async () => {
1840
+ expect(await steersFor("1", [assistantTurn(usage(5000), true)])).toEqual([]);
1841
+ });
1842
+
1843
+ test("a note rendered after its tool turn ended is dropped", async () => {
1844
+ const turnEnd = { type: "turn_end" } as unknown as AgentSessionEvent;
1845
+ const lateSteers = async (events: AgentSessionEvent[]) => {
1846
+ const held = holdTemplateRender();
1847
+ try {
1848
+ return await steersFor("1", events, async () => {
1849
+ held.release();
1850
+ await Bun.sleep(50);
1851
+ });
1852
+ } finally {
1853
+ held.stop();
1854
+ }
1855
+ };
1856
+ // Pi already read steering for that turn; the next reply could be the final one.
1857
+ expect(await lateSteers([assistantTurn(usage(5000)), turnEnd])).toEqual([]);
1858
+ // Control: ready while the tool turn is still running.
1859
+ expect(await lateSteers([assistantTurn(usage(5000))])).toEqual([HELD_NOTE_TEXT]);
1860
+ });
1861
+ });
@@ -22,6 +22,7 @@ const SYSTEM_TEMPLATES = [
22
22
  "system.agent.communication",
23
23
  "system.agent.communication.remote",
24
24
  "system.agent.compaction.goal_reminder",
25
+ "system.agent.context.bound_note",
25
26
  "system.agent.cursor.first_message",
26
27
  "system.agent.lead",
27
28
  "system.agent.memory",
@@ -117,11 +118,11 @@ describe("Session templates: registration", () => {
117
118
  expect(sessionSystemEventTypes("session")).toEqual([...SESSION_TEMPLATES].sort());
118
119
  });
119
120
 
120
- test("registers 31 system and session templates in total", () => {
121
+ test("registers 32 system and session templates in total", () => {
121
122
  const all = getAllTemplateDefinitions();
122
123
  const sessionSystem = all.filter((d) => d.category === "system" || d.category === "session");
123
- // 26 system blocks + 5 session composites.
124
- expect(sessionSystem.length).toBe(31);
124
+ // 27 system blocks + 5 session composites.
125
+ expect(sessionSystem.length).toBe(32);
125
126
  });
126
127
 
127
128
  test("drops the v1 blocks that prompt v2 deleted", () => {