@desplega.ai/agent-swarm 2.5.2 → 2.5.4

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 (236) hide show
  1. package/dist/{acp-adapter-csh0zz3b.js → acp-adapter-ek38cwsv.js} +5 -5
  2. package/dist/{actions-6xx079d1.js → actions-92mepq03.js} +19 -19
  3. package/dist/{amp-adapter-8n6f0hz8.js → amp-adapter-9gaxg015.js} +4 -4
  4. package/dist/{app-ft6r6v23.js → app-ctn8gsgx.js} +3 -3
  5. package/dist/{assistant-vt2cyfzd.js → assistant-zyr5kz47.js} +21 -21
  6. package/dist/{boot-reembed-tgrfy27t.js → boot-reembed-dwzfttdr.js} +1 -1
  7. package/dist/{boot-reembed-r3e808ky.js → boot-reembed-w75maga2.js} +1 -1
  8. package/dist/{boot-scrub-sweep-masdwxkm.js → boot-scrub-sweep-e9kq43kf.js} +3 -3
  9. package/dist/{boot-seeds-0n82qc7q.js → boot-seeds-x28v3fhb.js} +4 -4
  10. package/dist/{claude-adapter-s7j47k06.js → claude-adapter-f0afqnne.js} +5 -5
  11. package/dist/{claude-managed-adapter-gt6pa35d.js → claude-managed-adapter-x81a7r65.js} +3 -3
  12. package/dist/{claude-managed-setup-3hzrgrnk.js → claude-managed-setup-bqzwjd9x.js} +2 -2
  13. package/dist/{claude-sdk-session-0zrdzyx2.js → claude-sdk-session-7b7whkaq.js} +9307 -9254
  14. package/dist/{cli-a5ebc95n.js → cli-0shx9j93.js} +16 -13
  15. package/dist/{cli-4qkg9968.js → cli-19202kew.js} +4 -4
  16. package/dist/{cli-cyfgxnvb.js → cli-1r5ad7j5.js} +19 -17
  17. package/dist/{cli-jmcjbjkt.js → cli-25n3mv5d.js} +9 -7
  18. package/dist/cli-2655k16q.js +140 -0
  19. package/dist/{cli-wadzapwb.js → cli-277c31dw.js} +1 -1
  20. package/dist/{cli-n6kmqngw.js → cli-4b2z6n0m.js} +29 -7
  21. package/dist/{cli-7ccf7akh.js → cli-5bnsc0e1.js} +9 -7
  22. package/dist/{cli-qrjv7t18.js → cli-621ce7kw.js} +1 -1
  23. package/dist/{cli-xzge46sj.js → cli-64mh9mf3.js} +20 -6
  24. package/dist/{cli-b6tnjdvx.js → cli-6a1xg1p7.js} +6 -2
  25. package/dist/{cli-ydg6ep51.js → cli-6n8axjv1.js} +1 -1
  26. package/dist/{cli-e91vshb1.js → cli-7p4jxgtt.js} +5 -5
  27. package/dist/{cli-f3pfn4x3.js → cli-9j129brq.js} +9 -3
  28. package/dist/{cli-xw9q173h.js → cli-9s05422w.js} +1 -1
  29. package/dist/{cli-15xd0j7w.js → cli-abs8ndtr.js} +12 -12
  30. package/dist/{cli-h5d22pfs.js → cli-asq5ydm4.js} +219 -309
  31. package/dist/{cli-w9etnnxk.js → cli-bf8ma3f1.js} +120 -50
  32. package/dist/{cli-0vgy5wfw.js → cli-bvpgr9ny.js} +2 -2
  33. package/dist/{cli-3e9vs93g.js → cli-c61mbdds.js} +4 -4
  34. package/dist/{cli-e5bxnjps.js → cli-c952c7ac.js} +5 -5
  35. package/dist/{cli-p859hcc8.js → cli-dew6x255.js} +1 -1
  36. package/dist/{cli-tx00c0h6.js → cli-dqygeeh5.js} +17 -1
  37. package/dist/{cli-gpv60278.js → cli-e42vss7b.js} +2 -2
  38. package/dist/{cli-b63d08rs.js → cli-eftzyh9y.js} +1 -1
  39. package/dist/{cli-fgn7yg5r.js → cli-ethgy7ha.js} +1 -1
  40. package/dist/{cli-86zpfje5.js → cli-ff1tm9n9.js} +1 -1
  41. package/dist/{cli-cf63dnxj.js → cli-g9vd9ece.js} +37 -12
  42. package/dist/{cli-j3dwd4kz.js → cli-gbfxp48k.js} +770 -126
  43. package/dist/{cli-n2918sed.js → cli-gn5af0x5.js} +21 -21
  44. package/dist/{cli-d172svcw.js → cli-gwhjmh8g.js} +4 -4
  45. package/dist/{cli-stxdrqs2.js → cli-hh5we352.js} +24 -5
  46. package/dist/{cli-0ebey9hg.js → cli-j6rktc91.js} +1 -1
  47. package/dist/{cli-csh8fnbg.js → cli-jb30dzvk.js} +473 -58
  48. package/dist/{cli-vaxw3bg0.js → cli-jbnn4vxb.js} +3 -3
  49. package/dist/{cli-ha22h0eg.js → cli-k4tjsdx9.js} +45 -40
  50. package/dist/{cli-f55b6ky0.js → cli-m4ecdxvz.js} +40 -13
  51. package/dist/{cli-mntxtkhn.js → cli-mz9afxxf.js} +2 -1
  52. package/dist/{cli-ta8bd7th.js → cli-pzatvw94.js} +1 -1
  53. package/dist/{cli-3qv8naq8.js → cli-rr7zx19j.js} +1591 -353
  54. package/dist/{cli-b1tv22wr.js → cli-s682zgqz.js} +1 -1
  55. package/dist/{cli-bpbcpd8b.js → cli-sbwsxvtd.js} +1 -1
  56. package/dist/{cli-0xk505e8.js → cli-sxgtf5fw.js} +1 -1
  57. package/dist/{cli-6y43v79h.js → cli-tsma7p8v.js} +1 -1
  58. package/dist/{cli-edtm741g.js → cli-wghjkr0e.js} +5 -5
  59. package/dist/{cli-aaqnqpyw.js → cli-wx2nj19m.js} +30 -11
  60. package/dist/{cli-y3dzwyy3.js → cli-x0f2v69c.js} +180 -60
  61. package/dist/{cli-kcnwn85g.js → cli-xtrz6bzs.js} +7 -7
  62. package/dist/{cli-14v1sxxy.js → cli-xzfq4at3.js} +7 -7
  63. package/dist/cli.js +20 -20
  64. package/dist/{codex-adapter-brh2tcxr.js → codex-adapter-wch12q99.js} +3 -3
  65. package/dist/{codex-hook-e2he7aca.js → codex-hook-brpnycn5.js} +1 -1
  66. package/dist/{codex-session-runner-0dnj21qs.js → codex-session-runner-41pfcr95.js} +2 -2
  67. package/dist/{commands-1cnvseqx.js → commands-9849dkj6.js} +2 -2
  68. package/dist/{controller-kc7s9xhn.js → controller-ja0sd95p.js} +29 -3
  69. package/dist/{controllers-connect-jzx7t4bn.js → controllers-connect-xp05dj6x.js} +20 -20
  70. package/dist/{cursor-adapter-qpwp5sfg.js → cursor-adapter-623qxqew.js} +1 -2
  71. package/dist/{db-4vq88jkc.js → db-47vj40dy.js} +3 -3
  72. package/dist/{db-v5ytg5pw.js → db-w759qmx0.js} +2 -2
  73. package/dist/{dispatcher-trtcrqae.js → dispatcher-7b8e4z2j.js} +17 -17
  74. package/dist/{docker-nfy2gc8t.js → docker-s84hjs91.js} +2 -2
  75. package/dist/{dsh-adapter-f20xwn1z.js → dsh-adapter-2ezw3xea.js} +2 -3
  76. package/dist/{external-channel-n9bymc2n.js → external-channel-e2s028da.js} +2 -2
  77. package/dist/{grok-adapter-4kpmmnr6.js → grok-adapter-ys7gaqee.js} +6 -6
  78. package/dist/{handlers-ptsraqa8.js → handlers-wy1dr6ja.js} +20 -20
  79. package/dist/{heartbeat-60qtxtjf.js → heartbeat-w3mntghj.js} +20 -20
  80. package/dist/{hook-rbktcstg.js → hook-3jjywnpb.js} +9 -3
  81. package/dist/{hook-ngtvh729.js → hook-g7kvv6x9.js} +3 -2
  82. package/dist/{http-z8xy72gp.js → http-tkp7d4kb.js} +2984 -2483
  83. package/dist/{index-r70q0xjx.js → index-1ef1myq2.js} +17 -17
  84. package/dist/{index-a9nsavhx.js → index-3n8m9ea2.js} +1 -1
  85. package/dist/{index-cbxkmfc5.js → index-9pn1pfkf.js} +1 -1
  86. package/dist/{index-s193j5fh.js → index-nmm79qgr.js} +20 -20
  87. package/dist/{index-6srwvd5b.js → index-p4xfz37z.js} +1 -1
  88. package/dist/{index-06qkmcx6.js → index-q7m26kvs.js} +33 -23
  89. package/dist/{index-1cscynkb.js → index-xdaahthb.js} +17 -17
  90. package/dist/{keepalive-a9srcphj.js → keepalive-v4ta8wgg.js} +3 -3
  91. package/dist/{lead-3devneqy.js → lead-sawnyqxk.js} +35 -35
  92. package/dist/{maintenance-0er16h2y.js → maintenance-84n1ex21.js} +1 -1
  93. package/dist/{oauth-refresh-sweep-b65jcyw2.js → oauth-refresh-sweep-vjnnandw.js} +2 -2
  94. package/dist/{onboard-wqywz5py.js → onboard-sfmpe4np.js} +2 -2
  95. package/dist/{opencode-adapter-e56086m1.js → opencode-adapter-jmpst4q4.js} +4 -4
  96. package/dist/{otel-impl-vk32s884.js → otel-impl-v6cc0zve.js} +1 -1
  97. package/dist/{pi-mono-adapter-tzps8f38.js → pi-mono-adapter-fk8fkcf6.js} +33 -6
  98. package/dist/{pricing-refresh-t27anv4z.js → pricing-refresh-fsk1e1zc.js} +3 -3
  99. package/dist/{rbac-roles-452665y0.js → rbac-roles-prmkdnkw.js} +2 -2
  100. package/dist/{rbac-roles-bmcgsxjn.js → rbac-roles-zxmxqa39.js} +1 -1
  101. package/dist/{render-v2-0ttsk2j4.js → render-v2-pjwxvnkw.js} +3 -3
  102. package/dist/{resume-a26kvr41.js → resume-frdtgfa0.js} +9 -5
  103. package/dist/{session-access-dgh9nkwk.js → session-access-fpez4y5q.js} +4 -4
  104. package/dist/{setup-9vc9qahm.js → setup-mq6m3m8q.js} +1 -1
  105. package/dist/{work-objects-vn8nkxwn.js → work-objects-kfahhjsq.js} +20 -20
  106. package/dist/{worker-1x1h8vnm.js → worker-v5e5rxj3.js} +35 -35
  107. package/openapi.json +3078 -2424
  108. package/package.json +7 -7
  109. package/plugin/commands/create-pr.md +2 -2
  110. package/plugin/pi-skills/create-pr/SKILL.md +2 -2
  111. package/src/be/approval-config-question.ts +389 -0
  112. package/src/be/automation-preflight.ts +37 -3
  113. package/src/be/db/approvals.ts +48 -0
  114. package/src/be/db/session-tree.ts +73 -15
  115. package/src/be/db/skill-search.ts +87 -0
  116. package/src/be/db/tasks/read.ts +29 -2
  117. package/src/be/db/tasks/write.ts +15 -5
  118. package/src/be/db/workflow-idempotency.ts +183 -0
  119. package/src/be/db/workflow-runs.ts +366 -8
  120. package/src/be/db.ts +92 -22
  121. package/src/be/migrations/239_skills_fts.sql +40 -0
  122. package/src/be/migrations/240_workflow_run_parent_step_attempt.sql +37 -0
  123. package/src/be/migrations/241_skills_meta_fts.sql +39 -0
  124. package/src/be/migrations/242_workflow_idempotency_keys.sql +40 -0
  125. package/src/be/sealed-json.ts +1 -0
  126. package/src/be/seed-scripts/catalog/compound-insights.inline.ts +2 -2
  127. package/src/be/seed-scripts/catalog/compound-insights.ts +2 -2
  128. package/src/be/seed-skills/bundled-files.generated.json +1 -1
  129. package/src/be/sql-connections.ts +7 -1
  130. package/src/be/swarm-config-guard.ts +25 -0
  131. package/src/be/users.ts +9 -0
  132. package/src/commands/controller.ts +33 -0
  133. package/src/commands/runner.ts +6 -2
  134. package/src/extensions/apply-task-create.ts +57 -11
  135. package/src/extensions/catalog.generated.json +4 -4
  136. package/src/extensions/contract-types.generated.ts +1 -1
  137. package/src/extensions/contract.ts +25 -0
  138. package/src/extensions/dispatcher.ts +4 -0
  139. package/src/extensions/post-bridge.ts +8 -0
  140. package/src/hooks/context-bound-note.ts +196 -0
  141. package/src/hooks/hook.ts +12 -0
  142. package/src/http/approval-requests.ts +209 -38
  143. package/src/http/config.ts +70 -53
  144. package/src/http/idempotency-key.ts +117 -0
  145. package/src/http/sessions.ts +27 -2
  146. package/src/http/workflows.ts +129 -24
  147. package/src/integrations/chat-sdk/inbound.ts +5 -4
  148. package/src/prompts/context-bound-note.ts +48 -0
  149. package/src/prompts/session-templates.ts +25 -1
  150. package/src/providers/amp-adapter.ts +30 -8
  151. package/src/providers/claude-adapter.ts +29 -4
  152. package/src/providers/claude-managed-adapter.ts +26 -0
  153. package/src/providers/claude-sdk-session.ts +10 -0
  154. package/src/providers/claude-session-events.ts +13 -0
  155. package/src/providers/codex-adapter.ts +12 -0
  156. package/src/providers/dsh-adapter.ts +1 -1
  157. package/src/providers/opencode-adapter.ts +18 -0
  158. package/src/providers/pi-mono-adapter.ts +35 -0
  159. package/src/runtime-providers/docker.ts +6 -2
  160. package/src/scripts-runtime/types/swarm-extension.d.ts +35 -0
  161. package/src/tests/amp-adapter.test.ts +97 -0
  162. package/src/tests/approval-config-question.test.ts +702 -0
  163. package/src/tests/approval-requests.test.ts +10 -7
  164. package/src/tests/approval-sweeps.test.ts +12 -6
  165. package/src/tests/base-prompt.test.ts +1 -1
  166. package/src/tests/chat-sdk-inbound.test.ts +20 -0
  167. package/src/tests/claude-hook-output.test.ts +79 -1
  168. package/src/tests/claude-managed-adapter.test.ts +275 -0
  169. package/src/tests/codex-adapter.test.ts +163 -0
  170. package/src/tests/context-bound-note.test.ts +269 -0
  171. package/src/tests/dsh-adapter.test.ts +1 -1
  172. package/src/tests/entrypoint-gitlab-credential-helper.test.ts +273 -0
  173. package/src/tests/extensions-example-comms-adherence-gate.test.ts +321 -12
  174. package/src/tests/extensions-pre-task.test.ts +273 -0
  175. package/src/tests/fetch-resolved-env.test.ts +17 -0
  176. package/src/tests/fixtures/extensions/record-blocks/hooks.ts +10 -0
  177. package/src/tests/fixtures/extensions/record-blocks/manifest.json +7 -0
  178. package/src/tests/fixtures/fake-amp.ts +22 -8
  179. package/src/tests/fixtures/held-template-render.ts +42 -0
  180. package/src/tests/fixtures/workflow-idempotency-trigger.child.ts +73 -0
  181. package/src/tests/list-endpoint-slimming.test.ts +33 -0
  182. package/src/tests/migration-240-workflow-run-parent-step-attempt.test.ts +55 -0
  183. package/src/tests/openapi-route-headers.test.ts +2 -0
  184. package/src/tests/opencode-adapter.test.ts +134 -0
  185. package/src/tests/pi-mono-adapter.test.ts +105 -0
  186. package/src/tests/prompt-template-session.test.ts +4 -3
  187. package/src/tests/runner-push-delivery.test.ts +25 -0
  188. package/src/tests/runtime-controller-liveness.test.ts +133 -0
  189. package/src/tests/runtime-providers-docker.test.ts +2 -0
  190. package/src/tests/script-connections.test.ts +9 -0
  191. package/src/tests/script-run-config-scope.test.ts +91 -3
  192. package/src/tests/session-logs.test.ts +70 -0
  193. package/src/tests/sessions-tree.test.ts +205 -1
  194. package/src/tests/skill-search-long-content.test.ts +193 -0
  195. package/src/tests/skill-search.test.ts +173 -22
  196. package/src/tests/workflow-cascade-cancel.test.ts +1169 -0
  197. package/src/tests/workflow-foreach-supersede-retry.test.ts +488 -0
  198. package/src/tests/workflow-idempotency.test.ts +915 -0
  199. package/src/tests/workflow-sub-workflow.test.ts +538 -1
  200. package/src/tools/request-human-input.ts +27 -5
  201. package/src/tools/workflows/cancel-workflow-run.ts +1 -1
  202. package/src/types.ts +7 -0
  203. package/src/utils/context-window.ts +8 -6
  204. package/src/workflows/approval-cancel.ts +15 -13
  205. package/src/workflows/approval-notifications.ts +15 -4
  206. package/src/workflows/checkpoint.ts +72 -35
  207. package/src/workflows/engine.ts +262 -93
  208. package/src/workflows/executors/base.ts +6 -0
  209. package/src/workflows/executors/foreach.ts +2 -2
  210. package/src/workflows/executors/human-in-the-loop.ts +25 -1
  211. package/src/workflows/executors/sub-workflow.ts +45 -16
  212. package/src/workflows/foreach-driver.ts +10 -3
  213. package/src/workflows/foreach-join.ts +29 -6
  214. package/src/workflows/recovery.ts +68 -1
  215. package/src/workflows/resume.ts +228 -77
  216. package/src/workflows/retry-poller.ts +74 -7
  217. package/src/workflows/task-step-routing.ts +29 -7
  218. package/templates/ai-toolbox.manifest.json +8 -8
  219. package/templates/community/open-prompt-showreel/PLAYBOOK.md +5 -6
  220. package/templates/community/open-prompt-showreel/lead-prompt.md +1 -1
  221. package/templates/community/open-prompt-showreel/skills/open-prompt-showreel/SKILL.md +4 -4
  222. package/templates/extensions/comms-adherence-gate/README.md +38 -13
  223. package/templates/extensions/comms-adherence-gate/hooks.ts +272 -69
  224. package/templates/extensions/comms-adherence-gate/manifest.json +2 -2
  225. package/templates/skills/agent-browser/SKILL.md +4 -4
  226. package/templates/skills/agent-browser/content.md +4 -4
  227. package/templates/skills/code-quality/config.json +1 -1
  228. package/templates/skills/code-quality/content.md +2 -2
  229. package/templates/skills/delegate-work/content.md +29 -25
  230. package/templates/skills/delegate-work/files/scripts/codex-exec.sh +5 -5
  231. package/templates/skills/qa/content.md +2 -2
  232. package/templates/skills/researching/content.md +2 -0
  233. package/templates/skills/turso-interaction/content.md +1 -1
  234. package/templates/skills/work-on-task/SKILL.md +1 -1
  235. package/templates/skills/work-on-task/content.md +1 -1
  236. package/templates/skills/workflow-iterate/content.md +8 -1
@@ -418,7 +418,7 @@ describe("Approval Requests", () => {
418
418
  expect(result).toBeNull();
419
419
  });
420
420
 
421
- test("does not resolve a workflow approval after its run and HITL step are cancelled", async () => {
421
+ test("does not resolve a workflow approval after its run is cancelled", async () => {
422
422
  const workflow = await createWorkflow({
423
423
  name: `approval-cancel-${crypto.randomUUID()}`,
424
424
  definition: { nodes: [] },
@@ -432,11 +432,12 @@ describe("Approval Requests", () => {
432
432
  nodeId: "approval",
433
433
  nodeType: "human-in-the-loop",
434
434
  });
435
- await updateWorkflowRun(runId, { status: "cancelled" });
436
- await updateWorkflowRunStep(stepId, { status: "cancelled" });
435
+ // Asked while the run was live; the run was cancelled after, leaving
436
+ // the request pending (a cancelled step's trigger would close it).
437
437
  const approval = await createApprovalRequest(
438
438
  makeApprovalData({ workflowRunId: runId, workflowRunStepId: stepId }),
439
439
  );
440
+ await updateWorkflowRun(runId, { status: "cancelled" });
440
441
 
441
442
  const result = await resolveApprovalRequest(
442
443
  approval.id,
@@ -1864,15 +1865,17 @@ describe("Approval Requests", () => {
1864
1865
 
1865
1866
  test("cancel on a waiting run cancels the run, its step, and its task", async () => {
1866
1867
  const { runId, stepId, approvalId } = await makeRun("waiting");
1868
+ // Linked directly: the engine binds a step's task while the step is
1869
+ // still running, and this step is already parked.
1867
1870
  const task = await createTaskExtended("step task", {
1868
1871
  agentId: owner.id,
1869
1872
  source: "mcp",
1870
1873
  workflowRunId: runId,
1871
- workflowRunStepId: stepId,
1872
1874
  });
1873
- await getDbClient().run("UPDATE agent_tasks SET status = 'in_progress' WHERE id = ?", [
1874
- task.id,
1875
- ]);
1875
+ await getDbClient().run(
1876
+ "UPDATE agent_tasks SET status = 'in_progress', workflowRunStepId = ? WHERE id = ?",
1877
+ [stepId, task.id],
1878
+ );
1876
1879
 
1877
1880
  const res = await post(
1878
1881
  `/api/approval-requests/${approvalId}/cancel`,
@@ -223,15 +223,17 @@ describe("approval sweeps", () => {
223
223
 
224
224
  test("cancels the waiting run, its step, and its linked task", async () => {
225
225
  const { runId, stepId } = await makeRun("waiting");
226
+ // Linked directly: the engine binds a step's task while the step is
227
+ // still running, and this step is already parked.
226
228
  const task = await createTaskExtended("step task", {
227
229
  agentId,
228
230
  source: "mcp",
229
231
  workflowRunId: runId,
230
- workflowRunStepId: stepId,
231
232
  });
232
- await getDbClient().run("UPDATE agent_tasks SET status = 'in_progress' WHERE id = ?", [
233
- task.id,
234
- ]);
233
+ await getDbClient().run(
234
+ "UPDATE agent_tasks SET status = 'in_progress', workflowRunStepId = ? WHERE id = ?",
235
+ [stepId, task.id],
236
+ );
235
237
  const id = await makeRequest({ ageDays: 8, workflowRunId: runId, workflowRunStepId: stepId });
236
238
 
237
239
  const result = await autoCancelStaleApprovalRequests();
@@ -300,8 +302,10 @@ describe("approval sweeps", () => {
300
302
  });
301
303
 
302
304
  test("leaves a failed run as it is", async () => {
303
- const { runId, stepId } = await makeRun("failed");
305
+ // The request was asked while the run was live; the run failed after.
306
+ const { runId, stepId } = await makeRun("waiting");
304
307
  const id = await makeRequest({ ageDays: 8, workflowRunId: runId, workflowRunStepId: stepId });
308
+ await updateWorkflowRun(runId, { status: "failed" });
305
309
 
306
310
  const result = await autoCancelStaleApprovalRequests();
307
311
 
@@ -415,13 +419,15 @@ describe("approval sweeps", () => {
415
419
  });
416
420
 
417
421
  test("times out a request on a completed run", async () => {
418
- const { runId, stepId } = await makeRun("completed");
422
+ // The request was asked while the run was live; the run completed after.
423
+ const { runId, stepId } = await makeRun("waiting");
419
424
  const id = await makeRequest({
420
425
  ageDays: 0,
421
426
  expiresInMs: -60_000,
422
427
  workflowRunId: runId,
423
428
  workflowRunStepId: stepId,
424
429
  });
430
+ await updateWorkflowRun(runId, { status: "completed" });
425
431
  await timeoutExpiredApprovalRequests();
426
432
  expect((await getApprovalRequestById(id))!.status).toBe("timeout");
427
433
  });
@@ -490,7 +490,7 @@ describe("getBasePrompt: outputs section", () => {
490
490
  expect(result).toContain("## Outputs");
491
491
  expect(result).toContain(AGENT_FS_LINE);
492
492
  expect(result).toContain(
493
- "For a file anyone outside the swarm opens, use an `agent-fs share-create` link; `AGENT_FS_LIVE_URL` links are for signed-in teammates.",
493
+ `Give people the live app link \`\${AGENT_FS_LIVE_URL}/file/~/<org>/<drive>/<path>\`. Make an \`agent-fs share-create\` link only when the requester asks for one or the reader has no agent-fs login.`,
494
494
  );
495
495
  expect(result).not.toContain(NO_AGENT_FS_LINE);
496
496
  });
@@ -332,6 +332,26 @@ describe("Chat SDK inbound pipeline", () => {
332
332
  }
333
333
  });
334
334
 
335
+ test("a mention in a subscribed group gets 👀 before the flush, and the flush does not react again", async () => {
336
+ await postTelegramUpdate(telegramUpdate(group(-2991), "@swarm_test_bot first ask"));
337
+ await flushChatBuffers();
338
+ const ask = telegramUpdate(group(-2991), "@swarm_test_bot second ask");
339
+ await postTelegramUpdate(ask);
340
+ expect(reactionsOn(-2991, ask)).toEqual(["👀"]);
341
+ await flushChatBuffers();
342
+ expect(await chatTasks(-2991)).toHaveLength(2);
343
+ expect(reactionsOn(-2991, ask)).toEqual(["👀"]);
344
+ });
345
+
346
+ test("a !now mention in a subscribed group gets ⚡, not 👀", async () => {
347
+ await postTelegramUpdate(telegramUpdate(group(-2992), "@swarm_test_bot first ask"));
348
+ await flushChatBuffers();
349
+ const ask = telegramUpdate(group(-2992), "@swarm_test_bot !now second ask");
350
+ await postTelegramUpdate(ask);
351
+ expect(await chatTasks(-2992)).toHaveLength(2);
352
+ expect(reactionsOn(-2992, ask)).toEqual(["⚡"]);
353
+ });
354
+
335
355
  test("two asks back to back each anchor to their own message, not the parent's", async () => {
336
356
  const firstAsk = telegramUpdate(group(-2010), "@swarm_test_bot !now summarize the PR");
337
357
  await postTelegramUpdate(firstAsk);
@@ -82,7 +82,11 @@ afterAll(async () => {
82
82
  await rm(projectDir, { recursive: true, force: true });
83
83
  });
84
84
 
85
- async function runHook(event: Record<string, unknown>, taskId: string) {
85
+ async function runHook(
86
+ event: Record<string, unknown>,
87
+ taskId: string,
88
+ extraEnv: Record<string, string> = {},
89
+ ) {
86
90
  const taskFile = join(projectDir, `task-${taskId}.json`);
87
91
  await Bun.write(
88
92
  taskFile,
@@ -92,6 +96,8 @@ async function runHook(event: Record<string, unknown>, taskId: string) {
92
96
  ...process.env,
93
97
  CLAUDE_PROJECT_DIR: projectDir,
94
98
  TASK_FILE: taskFile,
99
+ CONTEXT_BOUND_NOTE_TOKENS: undefined,
100
+ ...extraEnv,
95
101
  };
96
102
  return runChild(["bun", HOOK_PATH], {
97
103
  cwd: projectDir,
@@ -107,6 +113,9 @@ function parseSoleJson(stdout: string): Record<string, unknown> {
107
113
  return JSON.parse(trimmed) as Record<string, unknown>;
108
114
  }
109
115
 
116
+ /** The note is off unless CONTEXT_BOUND_NOTE_TOKENS is set. */
117
+ const noteOn = { CONTEXT_BOUND_NOTE_TOKENS: "200000" };
118
+
110
119
  const bashTouch = {
111
120
  tool_name: "Bash",
112
121
  tool_input: { command: "touch /tmp/probe" },
@@ -230,4 +239,73 @@ describe("claude hook stdout", () => {
230
239
  },
231
240
  CHILD_PROCESS_TEST_BUDGET_MS,
232
241
  );
242
+
243
+ test(
244
+ "worker context-bound note fires once as PostToolUse additionalContext",
245
+ async () => {
246
+ Object.assign(state, { isLead: false, cancelledTaskId: null, shouldBlockPolling: false });
247
+ const taskId = crypto.randomUUID();
248
+ await Bun.write(
249
+ join(projectDir, `task-${taskId}.json.context.json`),
250
+ JSON.stringify({ contextTokens: 250_000, model: "claude-opus-5-5" }),
251
+ );
252
+ const event = { hook_event_name: "PostToolUse", ...bashTouch, tool_response: {} };
253
+
254
+ const first = await runHook(event, taskId, noteOn);
255
+ const out = parseSoleJson(first.stdout) as { hookSpecificOutput: Record<string, string> };
256
+ expect(out.hookSpecificOutput.hookEventName).toBe("PostToolUse");
257
+ expect(out.hookSpecificOutput.additionalContext).toStartWith(
258
+ "Context is 250,000 tokens (25% of the 1,000,000-token window). Keep this task bounded",
259
+ );
260
+
261
+ const second = await runHook(event, taskId, noteOn);
262
+ expect(second.exitCode).toBe(0);
263
+ expect(second.stdout.trim()).toBe("");
264
+ },
265
+ CHILD_PROCESS_TEST_BUDGET_MS * 2,
266
+ );
267
+
268
+ test(
269
+ "a subagent tool call does not consume the context-bound note",
270
+ async () => {
271
+ Object.assign(state, { isLead: false, cancelledTaskId: null, shouldBlockPolling: false });
272
+ const taskId = crypto.randomUUID();
273
+ await Bun.write(
274
+ join(projectDir, `task-${taskId}.json.context.json`),
275
+ JSON.stringify({ contextTokens: 250_000, model: "made-up-model" }),
276
+ );
277
+ const event = { hook_event_name: "PostToolUse", ...bashTouch, tool_response: {} };
278
+
279
+ const child = await runHook({ ...event, agent_id: "child" }, taskId, noteOn);
280
+ expect(child.exitCode).toBe(0);
281
+ expect(child.stdout.trim()).toBe("");
282
+
283
+ // Unknown model: no window share rather than a percent of the 200k fallback.
284
+ const main = await runHook(event, taskId, noteOn);
285
+ const out = parseSoleJson(main.stdout) as { hookSpecificOutput: Record<string, string> };
286
+ expect(out.hookSpecificOutput.additionalContext).toStartWith(
287
+ "Context is 250,000 tokens. Keep this task bounded",
288
+ );
289
+ },
290
+ CHILD_PROCESS_TEST_BUDGET_MS * 2,
291
+ );
292
+
293
+ test(
294
+ "the context-bound note is off when CONTEXT_BOUND_NOTE_TOKENS is unset",
295
+ async () => {
296
+ Object.assign(state, { isLead: false, cancelledTaskId: null, shouldBlockPolling: false });
297
+ const taskId = crypto.randomUUID();
298
+ await Bun.write(
299
+ join(projectDir, `task-${taskId}.json.context.json`),
300
+ JSON.stringify({ contextTokens: 900_000, model: "claude-opus-5-5" }),
301
+ );
302
+ const result = await runHook(
303
+ { hook_event_name: "PostToolUse", ...bashTouch, tool_response: {} },
304
+ taskId,
305
+ );
306
+ expect(result.exitCode).toBe(0);
307
+ expect(result.stdout.trim()).toBe("");
308
+ },
309
+ CHILD_PROCESS_TEST_BUDGET_MS,
310
+ );
233
311
  });
@@ -1,6 +1,7 @@
1
1
  import { afterAll, afterEach, beforeAll, describe, expect, test } from "bun:test";
2
2
  import { mkdirSync, rmSync } from "node:fs";
3
3
  import { join } from "node:path";
4
+ import { renderContextBoundNote } from "../prompts/context-bound-note";
4
5
  import { createProviderAdapter } from "../providers";
5
6
  import {
6
7
  ClaudeManagedAdapter,
@@ -13,6 +14,7 @@ import {
13
14
  getClaudeManagedModelPricing,
14
15
  } from "../providers/claude-managed-models";
15
16
  import type { ProviderEvent, ProviderSessionConfig } from "../providers/types";
17
+ import { HELD_NOTE_TEXT, holdTemplateRender } from "./fixtures/held-template-render";
16
18
 
17
19
  // Stash + restore env vars so this file plays nicely with the rest of the
18
20
  // suite (other tests don't expect MANAGED_AGENT_ID / MANAGED_ENVIRONMENT_ID
@@ -1503,3 +1505,276 @@ describe("ClaudeManagedAdapter (Phase 6) — full happy-path integration", () =>
1503
1505
  expect(result.output).toBe("Read the file and here's what I found.");
1504
1506
  });
1505
1507
  });
1508
+
1509
+ describe("ClaudeManagedAdapter — one-time context-bound note", () => {
1510
+ const tmpLogDir = `/tmp/claude-managed-adapter-bound-note-${Date.now()}`;
1511
+ let seq = 0;
1512
+
1513
+ beforeAll(() => {
1514
+ mkdirSync(tmpLogDir, { recursive: true });
1515
+ process.env.ANTHROPIC_API_KEY = "sk-test";
1516
+ process.env.MANAGED_AGENT_ID = "agent_x";
1517
+ process.env.MANAGED_ENVIRONMENT_ID = "env_x";
1518
+ });
1519
+
1520
+ afterAll(() => {
1521
+ rmSync(tmpLogDir, { recursive: true, force: true });
1522
+ });
1523
+
1524
+ /** A main-thread model request whose prompt is `input + cacheRead + cacheWrite`. */
1525
+ function requestEnd(input: unknown, cacheRead: unknown = 0, cacheWrite: unknown = 0) {
1526
+ seq += 1;
1527
+ return {
1528
+ type: "span.model_request_end",
1529
+ id: `req-${seq}`,
1530
+ processed_at: "2026-01-01T00:00:00Z",
1531
+ is_error: false,
1532
+ model_request_start_id: `start-${seq}`,
1533
+ model_usage: {
1534
+ input_tokens: input,
1535
+ output_tokens: 10,
1536
+ cache_read_input_tokens: cacheRead,
1537
+ cache_creation_input_tokens: cacheWrite,
1538
+ },
1539
+ };
1540
+ }
1541
+
1542
+ function toolUse(kind: "agent.tool_use" | "agent.mcp_tool_use" = "agent.tool_use") {
1543
+ seq += 1;
1544
+ return {
1545
+ type: kind,
1546
+ id: `tu-${seq}`,
1547
+ processed_at: "2026-01-01T00:00:00Z",
1548
+ name: "read_file",
1549
+ ...(kind === "agent.mcp_tool_use" ? { mcp_server_name: "agent-swarm" } : {}),
1550
+ input: { path: `/tmp/${seq}` },
1551
+ };
1552
+ }
1553
+
1554
+ const idle = () => ({
1555
+ type: "session.status_idle",
1556
+ id: `idle-${++seq}`,
1557
+ processed_at: "2026-01-01T00:00:00Z",
1558
+ stop_reason: { type: "end_turn" },
1559
+ });
1560
+
1561
+ /** Run a session over `events` and return the texts of every message sent after the prompt. */
1562
+ async function runNotes(
1563
+ events: Array<Record<string, unknown>>,
1564
+ threshold: string,
1565
+ onSend?: Parameters<typeof makeFakeClient>[0]["onSend"],
1566
+ model?: string,
1567
+ ) {
1568
+ const spy = makeFakeClient({
1569
+ streamEvents: async function* () {
1570
+ for (const e of events) yield e;
1571
+ },
1572
+ onSend,
1573
+ });
1574
+ const adapter = new ClaudeManagedAdapter({ client: spy.client });
1575
+ const session = await adapter.createSession(
1576
+ tConfig({
1577
+ logFile: join(tmpLogDir, `note-${++seq}.log`),
1578
+ env: { CONTEXT_BOUND_NOTE_TOKENS: threshold },
1579
+ ...(model ? { model } : {}),
1580
+ }),
1581
+ );
1582
+ const result = await session.waitForCompletion();
1583
+ // The note is rendered and sent off the event loop; let it land.
1584
+ await Bun.sleep(150);
1585
+ const notes = spy.sent.slice(1).map((sent) => {
1586
+ const content = sent.events[0]?.content as Array<{ text?: string }> | undefined;
1587
+ return { type: sent.events[0]?.type, text: content?.[0]?.text };
1588
+ });
1589
+ return { result, notes };
1590
+ }
1591
+
1592
+ test("fires once when a tool call follows a request at the threshold, not again", async () => {
1593
+ const { result, notes } = await runNotes(
1594
+ [
1595
+ requestEnd(150_000, 40_000, 10_000),
1596
+ toolUse(),
1597
+ requestEnd(240_000, 10_000, 0),
1598
+ toolUse("agent.mcp_tool_use"),
1599
+ requestEnd(260_000),
1600
+ toolUse(),
1601
+ idle(),
1602
+ ],
1603
+ "200000",
1604
+ );
1605
+ expect(result.isError).toBe(false);
1606
+ expect(notes).toEqual([
1607
+ { type: "user.message", text: await renderContextBoundNote(200_000, "claude-sonnet-4-6") },
1608
+ ]);
1609
+ });
1610
+
1611
+ test("the note shows the window share for a known model and omits it for an unknown one", async () => {
1612
+ const noteFor = async (model: string) =>
1613
+ (await runNotes([requestEnd(250_000), toolUse(), idle()], "200000", undefined, model)).notes;
1614
+ const rest =
1615
+ "Keep this task bounded: finish the current step, store-progress, and complete or defer instead of starting new exploration.";
1616
+ expect(await noteFor("claude-sonnet-4-6")).toEqual([
1617
+ {
1618
+ type: "user.message",
1619
+ text: `Context is 250,000 tokens (25% of the 1,000,000-token window). ${rest}`,
1620
+ },
1621
+ ]);
1622
+ expect(await noteFor("made-up-model")).toEqual([
1623
+ { type: "user.message", text: `Context is 250,000 tokens. ${rest}` },
1624
+ ]);
1625
+ });
1626
+
1627
+ test("no-op below the threshold, then fires once it is crossed", async () => {
1628
+ const { notes } = await runNotes(
1629
+ [requestEnd(199_999), toolUse(), requestEnd(205_000), toolUse(), idle()],
1630
+ "200000",
1631
+ );
1632
+ expect(notes).toEqual([
1633
+ { type: "user.message", text: await renderContextBoundNote(205_000, "claude-sonnet-4-6") },
1634
+ ]);
1635
+ });
1636
+
1637
+ test("off at 0 and when unset", async () => {
1638
+ const events = () => [requestEnd(500_000), toolUse(), idle()];
1639
+ expect((await runNotes(events(), "0")).notes).toEqual([]);
1640
+ expect((await runNotes(events(), "")).notes).toEqual([]);
1641
+ // Control: the same session with a threshold gets the note.
1642
+ expect((await runNotes(events(), "200000")).notes).toHaveLength(1);
1643
+ });
1644
+
1645
+ test("no note when the request ends the turn without a tool call", async () => {
1646
+ expect((await runNotes([requestEnd(300_000), idle()], "200000")).notes).toEqual([]);
1647
+ // Control: a tool call after the same request carries the note.
1648
+ expect((await runNotes([requestEnd(300_000), toolUse(), idle()], "200000")).notes).toHaveLength(
1649
+ 1,
1650
+ );
1651
+ });
1652
+
1653
+ test("fails open on missing or corrupt usage", async () => {
1654
+ const { result, notes } = await runNotes(
1655
+ [
1656
+ // Missing: a tool call before any request reported usage.
1657
+ toolUse(),
1658
+ // Corrupt: non-numeric usage fields.
1659
+ requestEnd("lots", null, {}),
1660
+ toolUse(),
1661
+ requestEnd(Number.NaN),
1662
+ toolUse(),
1663
+ // Control: a valid request afterwards still gets the note.
1664
+ requestEnd(210_000),
1665
+ toolUse(),
1666
+ idle(),
1667
+ ],
1668
+ "200000",
1669
+ );
1670
+ expect(result.isError).toBe(false);
1671
+ expect(notes).toEqual([
1672
+ { type: "user.message", text: await renderContextBoundNote(210_000, "claude-sonnet-4-6") },
1673
+ ]);
1674
+ });
1675
+
1676
+ test("fails open when sending the note rejects", async () => {
1677
+ let sends = 0;
1678
+ const { result, notes } = await runNotes(
1679
+ [requestEnd(300_000), toolUse(), requestEnd(310_000), toolUse(), idle()],
1680
+ "200000",
1681
+ () => {
1682
+ sends += 1;
1683
+ if (sends > 1) throw new Error("send failed");
1684
+ },
1685
+ );
1686
+ expect(result.isError).toBe(false);
1687
+ // One attempt only: a failed send does not re-arm the note.
1688
+ expect(notes).toHaveLength(1);
1689
+ });
1690
+
1691
+ describe("note rendered late", () => {
1692
+ const toolResult = (toolUseId: string) => ({
1693
+ type: "agent.tool_result",
1694
+ id: `tr-${++seq}`,
1695
+ processed_at: "2026-01-01T00:00:00Z",
1696
+ tool_use_id: toolUseId,
1697
+ content: [],
1698
+ });
1699
+
1700
+ /**
1701
+ * Holds the render until the stream reaches `releaseAt`. Returns the
1702
+ * `user.message` events sent after the prompt, read once the late render
1703
+ * had time to land.
1704
+ */
1705
+ async function lateNotes(
1706
+ releaseAt: "tool-open" | "tool-returned" | "idle" | "abort",
1707
+ ): Promise<{ notes: unknown[]; isError: boolean }> {
1708
+ const held = holdTemplateRender();
1709
+ const tool = toolUse();
1710
+ let unblock!: () => void;
1711
+ const blocked = new Promise<void>((resolve) => {
1712
+ unblock = resolve;
1713
+ });
1714
+ const spy = makeFakeClient({
1715
+ streamEvents: async function* () {
1716
+ yield requestEnd(300_000);
1717
+ yield tool;
1718
+ await held.requested;
1719
+ if (releaseAt === "tool-open") {
1720
+ held.release();
1721
+ await Bun.sleep(50);
1722
+ }
1723
+ yield toolResult(tool.id);
1724
+ if (releaseAt === "tool-returned") {
1725
+ held.release();
1726
+ await Bun.sleep(50);
1727
+ }
1728
+ if (releaseAt === "abort") await blocked;
1729
+ yield idle();
1730
+ },
1731
+ });
1732
+ try {
1733
+ const session = await new ClaudeManagedAdapter({ client: spy.client }).createSession(
1734
+ tConfig({
1735
+ logFile: join(tmpLogDir, `late-${++seq}.log`),
1736
+ env: { CONTEXT_BOUND_NOTE_TOKENS: "200000" },
1737
+ }),
1738
+ );
1739
+ if (releaseAt === "abort") {
1740
+ await held.requested;
1741
+ await session.abort();
1742
+ unblock();
1743
+ }
1744
+ const result = await session.waitForCompletion();
1745
+ held.release();
1746
+ await Bun.sleep(100);
1747
+ const notes = spy.sent
1748
+ .slice(1)
1749
+ .filter((sent) => sent.events[0]?.type === "user.message")
1750
+ .map((sent) => sent.events[0]);
1751
+ return { notes, isError: result.isError };
1752
+ } finally {
1753
+ unblock();
1754
+ held.stop();
1755
+ }
1756
+ }
1757
+
1758
+ test("control: a note ready while its tool call is open is sent", async () => {
1759
+ const { notes, isError } = await lateNotes("tool-open");
1760
+ expect(isError).toBe(false);
1761
+ expect(notes).toEqual([
1762
+ { type: "user.message", content: [{ type: "text", text: HELD_NOTE_TEXT }] },
1763
+ ]);
1764
+ });
1765
+
1766
+ test("dropped once the tool result is back and the final reply is pending", async () => {
1767
+ expect((await lateNotes("tool-returned")).notes).toEqual([]);
1768
+ });
1769
+
1770
+ test("dropped after the session went idle", async () => {
1771
+ const { notes, isError } = await lateNotes("idle");
1772
+ expect(isError).toBe(false);
1773
+ expect(notes).toEqual([]);
1774
+ });
1775
+
1776
+ test("dropped after abort", async () => {
1777
+ expect((await lateNotes("abort")).notes).toEqual([]);
1778
+ });
1779
+ });
1780
+ });