@desplega.ai/agent-swarm 1.158.0 → 1.159.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 (272) hide show
  1. package/dist/{acp-adapter-xaq4n6pc.js → acp-adapter-qhch0n2v.js} +1 -1
  2. package/dist/{actions-p9hz5yat.js → actions-dtsh1mfb.js} +33 -29
  3. package/dist/{anthropic-messages-51pz26qe.js → anthropic-messages-f2h06k64.js} +8 -3
  4. package/dist/{app-g26wdcz6.js → app-y1b9srva.js} +10 -9
  5. package/dist/{assistant-wydfqkb3.js → assistant-31hwy5xj.js} +34 -30
  6. package/dist/{azure-openai-responses-8gksqtmz.js → azure-openai-responses-0k1mfax5.js} +12 -13
  7. package/dist/{boot-reembed-se8zmdcp.js → boot-reembed-48d2qpkt.js} +11 -11
  8. package/dist/{boot-reembed-g5ckg9yb.js → boot-reembed-w0bryhkr.js} +12 -12
  9. package/dist/{boot-scrub-logs-wfxqan61.js → boot-scrub-logs-t65wbrn7.js} +9 -8
  10. package/dist/{boot-seeds-s3q01xj3.js → boot-seeds-h7c32hvm.js} +12 -11
  11. package/dist/{claude-adapter-tv2x4f36.js → claude-adapter-1t6611ne.js} +26 -21
  12. package/dist/{claude-managed-adapter-41b2s7fd.js → claude-managed-adapter-werp659j.js} +4 -2
  13. package/dist/{claude-managed-setup-60g8aazx.js → claude-managed-setup-y6rdgbc1.js} +11 -2
  14. package/dist/claude-sdk-session-zhkyk8yw.js +29319 -0
  15. package/dist/{cli-vj6h9kc2.js → cli-0m7wct0d.js} +59 -1
  16. package/dist/{cli-wscdvcqz.js → cli-0n2e4x49.js} +1 -1
  17. package/dist/cli-0xqde53b.js +176 -0
  18. package/dist/{cli-xberxa1r.js → cli-10ym38qc.js} +2 -2
  19. package/dist/{cli-5b6b62tz.js → cli-13hw4z91.js} +1 -1
  20. package/dist/{cli-rqz8qbw7.js → cli-20035trh.js} +50 -18
  21. package/dist/{cli-k0g1sjzm.js → cli-31nkw3g3.js} +3 -3
  22. package/dist/cli-3gvrr8vj.js +1422 -0
  23. package/dist/{cli-c07r9a2m.js → cli-48527fpw.js} +9 -8
  24. package/dist/{cli-24k806sc.js → cli-4aj25szq.js} +2 -2
  25. package/dist/{cli-7gtvvpbh.js → cli-4tvgz99m.js} +7 -7
  26. package/dist/cli-503ya656.js +455 -0
  27. package/dist/{cli-cw9zck8j.js → cli-5f2hq128.js} +8 -4
  28. package/dist/{cli-s8eet254.js → cli-68bmme5p.js} +11078 -27833
  29. package/dist/{cli-ftfvggzx.js → cli-79aq9cgb.js} +794 -563
  30. package/dist/cli-7cxw8b60.js +14910 -0
  31. package/dist/cli-7kyk9jr3.js +22 -0
  32. package/dist/cli-7zttzetj.js +324 -0
  33. package/dist/{cli-y95wp3tg.js → cli-96qd5psa.js} +1 -1
  34. package/dist/{cli-g8djp6qe.js → cli-9yg65c1j.js} +22 -0
  35. package/dist/{cli-3gmcwfyx.js → cli-ardpzhjm.js} +5 -5
  36. package/dist/{cli-7s3syk22.js → cli-b6thtzac.js} +5 -5
  37. package/dist/{cli-00w2c66p.js → cli-bgv70kym.js} +13 -2
  38. package/dist/{cli-j55n64b6.js → cli-bv0a38v9.js} +163 -44
  39. package/dist/cli-ca2w7zv7.js +115 -0
  40. package/dist/cli-cpvsaqsw.js +333 -0
  41. package/dist/cli-cygyqj3b.js +754 -0
  42. package/dist/{cli-x24pshtq.js → cli-dvjpkkjd.js} +6 -6
  43. package/dist/{cli-5d10vd2m.js → cli-e0ndxhtk.js} +4 -4
  44. package/dist/{cli-4198qc9v.js → cli-e4xnc0qp.js} +15 -3
  45. package/dist/{cli-rjehvrts.js → cli-eg7cdg68.js} +8 -8
  46. package/dist/cli-ejpygnqz.js +3815 -0
  47. package/dist/{cli-fgqxwrpv.js → cli-fjtp3463.js} +1631 -127
  48. package/dist/{cli-zt1227k1.js → cli-fkqhbg8g.js} +5 -3
  49. package/dist/{cli-47m6dkd3.js → cli-g9df2m37.js} +409 -168
  50. package/dist/cli-gzyh0j0g.js +8455 -0
  51. package/dist/cli-hgqfttfc.js +183 -0
  52. package/dist/cli-hj65wsts.js +17755 -0
  53. package/dist/cli-j9z8mfx3.js +476 -0
  54. package/dist/{cli-aehm1rbq.js → cli-kc3dp84d.js} +1135 -61
  55. package/dist/{cli-h38x0qhg.js → cli-kdnehe0t.js} +4 -3
  56. package/dist/{cli-20cj37vt.js → cli-m4excttz.js} +136 -2216
  57. package/dist/{cli-8qmme6vd.js → cli-mg24hkt4.js} +2 -2
  58. package/dist/{cli-xhaeg2nc.js → cli-mzkqpxee.js} +1 -1
  59. package/dist/{cli-q67y4wzm.js → cli-phsaj4xq.js} +24 -2
  60. package/dist/{cli-pw9rj5t9.js → cli-pqetb83f.js} +1 -176
  61. package/dist/{cli-rj19hc34.js → cli-qjxy52w7.js} +1 -1
  62. package/dist/{cli-53sz8gkz.js → cli-qv4jv8bx.js} +1 -1
  63. package/dist/cli-qvnnx4ct.js +1051 -0
  64. package/dist/cli-rb9m6rpe.js +4980 -0
  65. package/dist/{cli-tt9k8r35.js → cli-rfezr056.js} +476 -909
  66. package/dist/{cli-ggm92wcn.js → cli-rgjprct3.js} +1 -0
  67. package/dist/{cli-9httdpyq.js → cli-rrqmqr1s.js} +1 -1
  68. package/dist/cli-rykfmgwg.js +102 -0
  69. package/dist/{cli-eetrvv91.js → cli-t3p77qk1.js} +1 -1
  70. package/dist/{cli-68w09sjw.js → cli-vn0qv9cj.js} +25 -88
  71. package/dist/{cli-987hg0aa.js → cli-vwbbaecp.js} +2 -2
  72. package/dist/cli-wd9hz40d.js +69 -0
  73. package/dist/{cli-j57qkrpq.js → cli-wgjx0q33.js} +13 -13
  74. package/dist/cli-xgpf00sk.js +2149 -0
  75. package/dist/{cli-mcnhgnn1.js → cli-ykk5jaax.js} +184 -26
  76. package/dist/{cli-aky8tctm.js → cli-z9x0sh1t.js} +3 -3
  77. package/dist/{cli-1hkrch3k.js → cli-zw65mab6.js} +5 -5
  78. package/dist/cli.js +38 -29
  79. package/dist/cloudflare-workers-ai-system-one-1sg586qv.js +46 -0
  80. package/dist/{codex-adapter-cnm8wk11.js → codex-adapter-jqfqqa21.js} +21 -14
  81. package/dist/{codex-hook-9hptmdhj.js → codex-hook-5eys3dya.js} +2 -2
  82. package/dist/{codex-session-runner-gfg547zj.js → codex-session-runner-zd341dsm.js} +21 -14
  83. package/dist/{commands-e3bn0p75.js → commands-vfsysz4s.js} +9 -8
  84. package/dist/{db-03v9kzr2.js → db-s3znhvbn.js} +15 -10
  85. package/dist/{dispatcher-nrmg8yht.js → dispatcher-5xn0c7rf.js} +32 -28
  86. package/dist/{dsh-adapter-k03nfqa6.js → dsh-adapter-894mgfvs.js} +3 -3
  87. package/dist/{e2b-93x1k0j5.js → e2b-ez07znam.js} +4 -3
  88. package/dist/execute-71ysgv35.js +965 -0
  89. package/dist/{google-generative-ai-bxmv50nc.js → google-generative-ai-1nfcdphp.js} +5 -4
  90. package/dist/{google-vertex-809af514.js → google-vertex-1tqcd7b1.js} +5 -4
  91. package/dist/{handlers-aqcedbyb.js → handlers-5w4jwycr.js} +35 -31
  92. package/dist/{hook-7vkskkre.js → hook-853bxrr6.js} +16 -13
  93. package/dist/{hook-ybvmrmhq.js → hook-z0hbxj85.js} +16 -13
  94. package/dist/{http-2eqa8t24.js → http-akc3wy3m.js} +600 -123
  95. package/dist/{index-f6cryxwe.js → index-4a5njnm1.js} +2 -59
  96. package/dist/{index-a3vn96n8.js → index-5sz16ksh.js} +187 -47
  97. package/dist/{index-c6zsv1bd.js → index-bjmjh9h4.js} +11 -11
  98. package/dist/{index-chgdkq36.js → index-cd151qkq.js} +32 -28
  99. package/dist/{index-x71bvq0p.js → index-k661dzzr.js} +33 -29
  100. package/dist/{index-1yks3qcr.js → index-ttpbhnjg.js} +32 -28
  101. package/dist/{keepalive-vtkf5n99.js → keepalive-8p9rpvz5.js} +11 -10
  102. package/dist/{lead-h3sp357x.js → lead-jp69z5h3.js} +49 -45
  103. package/dist/llama-cpp-classify-t2tccams.js +335 -0
  104. package/dist/{maintenance-jzeqtk3q.js → maintenance-fwx4ev0k.js} +12 -12
  105. package/dist/{mistral-conversations-zx0kn554.js → mistral-conversations-bhvwqcnk.js} +14 -17
  106. package/dist/{oauth-refresh-sweep-qp3dzmdp.js → oauth-refresh-sweep-cwcq1ctx.js} +10 -9
  107. package/dist/{onboard-tmsp0qbz.js → onboard-2b4q0t84.js} +6 -5
  108. package/dist/{openai-codex-responses-4vjqzvjp.js → openai-codex-responses-nekthgks.js} +21 -8
  109. package/dist/{openai-completions-9hjrqbw8.js → openai-completions-hxtzwj0s.js} +8 -11
  110. package/dist/{openai-responses-7b1pn0xt.js → openai-responses-w4r118w7.js} +22 -17
  111. package/dist/{opencode-adapter-x6x2da1z.js → opencode-adapter-zz7xvjj6.js} +6 -4
  112. package/dist/{openrouter-images-9f9ek4rw.js → openrouter-images-b6sqqjgn.js} +4 -6
  113. package/dist/{otel-impl-9mz73q65.js → otel-impl-g8rs4vwr.js} +1 -1
  114. package/dist/{pi-messages-txpjj39q.js → pi-messages-vxdt8v7j.js} +2 -1
  115. package/dist/{pi-mono-adapter-m6dvv0dz.js → pi-mono-adapter-n0me28wt.js} +29 -17
  116. package/dist/pricing-refresh-0v77385v.js +39 -0
  117. package/dist/{rbac-roles-c9wqz83x.js → rbac-roles-krjyazbj.js} +10 -9
  118. package/dist/{render-v2-gryrh9h8.js → render-v2-3wjsp9aw.js} +10 -9
  119. package/dist/runtime-ktjysacx.js +24 -0
  120. package/dist/{setup-q9720bt6.js → setup-yeqryke6.js} +6 -5
  121. package/dist/typesafe-system-one-96960jxp.js +27 -0
  122. package/dist/{virtual-modules-3htpgn8x.js → virtual-modules-dzdeq1sf.js} +21 -13
  123. package/dist/{work-objects-b76x7deb.js → work-objects-3pakpkw0.js} +35 -31
  124. package/dist/{worker-6vrxbv41.js → worker-85testgj.js} +49 -45
  125. package/openapi.json +863 -14
  126. package/package.json +9 -8
  127. package/src/be/db/agents.ts +20 -0
  128. package/src/be/db/tasks/human-free.ts +442 -0
  129. package/src/be/db/tasks/read.ts +56 -1
  130. package/src/be/db/tasks/write.ts +107 -11
  131. package/src/be/db.ts +89 -165
  132. package/src/be/harness-model-support.ts +182 -0
  133. package/src/be/human-free-drain.ts +67 -0
  134. package/src/be/migrations/182_task_human_free_flag.sql +63 -0
  135. package/src/be/migrations/183_human_free_reclassify_queue.sql +25 -0
  136. package/src/be/migrations/184_model_catalog.sql +120 -0
  137. package/src/be/model-catalog-store.ts +644 -0
  138. package/src/be/model-tier-keys.ts +31 -0
  139. package/src/be/model-tier-resolution.ts +489 -0
  140. package/src/be/model-validation.ts +112 -0
  141. package/src/be/models-catalog.ts +61 -7
  142. package/src/be/modelsdev-cache.json +82 -0
  143. package/src/be/modelsdev-cache.ts +2 -0
  144. package/src/be/pricing-refresh.ts +154 -12
  145. package/src/be/scripts/typecheck.ts +15 -0
  146. package/src/be/seed-scripts/catalog/task-failure-audit.ts +61 -9
  147. package/src/be/swarm-config-guard.ts +6 -0
  148. package/src/cli.tsx +10 -0
  149. package/src/commands/claude-managed-setup.ts +3 -1
  150. package/src/commands/runner.ts +76 -2
  151. package/src/extensions/contract-types.generated.ts +1 -1
  152. package/src/heartbeat/heartbeat.ts +64 -15
  153. package/src/heartbeat/templates.ts +2 -2
  154. package/src/http/agents.ts +49 -1
  155. package/src/http/index.ts +8 -0
  156. package/src/http/models-catalog.ts +357 -8
  157. package/src/http/poll.ts +45 -2
  158. package/src/http/schedules.ts +36 -6
  159. package/src/http/session-data.ts +2 -5
  160. package/src/http/stats.ts +6 -8
  161. package/src/http/tasks.ts +26 -1
  162. package/src/http/usage-cache.ts +56 -15
  163. package/src/http/workflows.ts +35 -8
  164. package/src/prompts/session-templates.ts +1 -0
  165. package/src/providers/claude-managed-models.ts +47 -161
  166. package/src/providers/codex-adapter.ts +4 -4
  167. package/src/providers/codex-models.ts +65 -198
  168. package/src/providers/reasoning-effort.ts +44 -140
  169. package/src/rbac/legacy-policy.ts +25 -0
  170. package/src/rbac/permissions.ts +10 -0
  171. package/src/scripts-runtime/sdk-allowlist.ts +4 -0
  172. package/src/scripts-runtime/types/stdlib.d.ts +15 -0
  173. package/src/scripts-runtime/types/swarm-extension.d.ts +3 -0
  174. package/src/scripts-runtime/types/swarm-sdk.d.ts +15 -0
  175. package/src/server.ts +6 -0
  176. package/src/slack/message-text.ts +56 -2
  177. package/src/slack/thread-buffer.ts +10 -2
  178. package/src/tests/agents-list-model-display.test.ts +4 -6
  179. package/src/tests/base-prompt.test.ts +3 -0
  180. package/src/tests/claude-adapter.test.ts +10 -0
  181. package/src/tests/claude-managed-adapter.test.ts +12 -18
  182. package/src/tests/codex-adapter.test.ts +2 -1
  183. package/src/tests/codex-gpt6-release.test.ts +66 -5
  184. package/src/tests/context-window.test.ts +7 -5
  185. package/src/tests/db-extraction-contract.test.ts +1 -0
  186. package/src/tests/db-task-read-contract.test.ts +5 -1
  187. package/src/tests/db-task-write-contract.test.ts +4 -1
  188. package/src/tests/docs-index-footprint.test.js +3 -3
  189. package/src/tests/explicit-model-validation.test.ts +332 -0
  190. package/src/tests/harness-model-support.test.ts +374 -0
  191. package/src/tests/heartbeat-checklist-config.test.ts +71 -0
  192. package/src/tests/heartbeat-checklist.test.ts +4 -2
  193. package/src/tests/heartbeat-orphan-resume.test.ts +48 -0
  194. package/src/tests/human-free-flag.test.ts +279 -0
  195. package/src/tests/human-free-oracle.ts +57 -0
  196. package/src/tests/human-free-reclassify.test.ts +669 -0
  197. package/src/tests/migration-046-budgets.test.ts +3 -3
  198. package/src/tests/model-catalog-migration.test.ts +134 -0
  199. package/src/tests/model-catalog-table.test.ts +249 -0
  200. package/src/tests/model-catalog-write-access.test.ts +387 -0
  201. package/src/tests/model-tier-resolution.test.ts +519 -0
  202. package/src/tests/models-catalog.test.ts +1 -1
  203. package/src/tests/multi-runtime-enabled-stats.test.ts +65 -1
  204. package/src/tests/package-publish.test.ts +137 -36
  205. package/src/tests/pricing-routes.test.ts +2 -2
  206. package/src/tests/rbac-engine.test.ts +42 -2
  207. package/src/tests/reasoning-effort.test.ts +109 -0
  208. package/src/tests/refresh-modelsdev-carryforward.test.ts +5 -5
  209. package/src/tests/runtime-model-catalog.test.ts +86 -0
  210. package/src/tests/script-connections.test.ts +5 -0
  211. package/src/tests/seed-scripts.test.ts +89 -0
  212. package/src/tests/session-costs.test.ts +14 -11
  213. package/src/tests/slack-message-text.test.ts +101 -0
  214. package/src/tests/slack-thread-buffer-context.test.ts +93 -0
  215. package/src/tests/task-cascade-fail.test.ts +148 -2
  216. package/src/tests/tool-annotations.test.ts +1 -1
  217. package/src/tests/usage-cache.test.ts +166 -0
  218. package/src/tests/workflow-executors.test.ts +5 -2
  219. package/src/tests/workflow-system-one-decision-review.test.ts +1021 -0
  220. package/src/tests/workflow-system-one-decision-save-warnings.test.ts +365 -0
  221. package/src/tests/workflow-system-one-decision.test.ts +2063 -0
  222. package/src/tools/model-catalog/index.ts +137 -0
  223. package/src/tools/schedules/create-schedule.ts +16 -1
  224. package/src/tools/schedules/patch-schedule.ts +17 -0
  225. package/src/tools/schedules/update-schedule.ts +17 -0
  226. package/src/tools/send-task.ts +14 -0
  227. package/src/tools/task-action.ts +12 -0
  228. package/src/tools/tool-config.ts +3 -1
  229. package/src/tools/workflows/create-workflow.ts +9 -2
  230. package/src/tools/workflows/patch-workflow-node.ts +17 -7
  231. package/src/tools/workflows/patch-workflow.ts +9 -2
  232. package/src/tools/workflows/update-workflow.ts +9 -2
  233. package/src/types.ts +30 -1
  234. package/src/utils/context-window.ts +47 -53
  235. package/src/utils/harness-cli-version.ts +115 -0
  236. package/src/utils/harness-model-error.ts +33 -0
  237. package/src/utils/runtime-model-catalog.ts +137 -0
  238. package/src/workflows/approval-resolution.ts +39 -0
  239. package/src/workflows/definition.ts +7 -0
  240. package/src/workflows/engine.ts +69 -1
  241. package/src/workflows/executors/agent-task.ts +9 -0
  242. package/src/workflows/executors/base.ts +53 -0
  243. package/src/workflows/executors/human-in-the-loop.ts +8 -8
  244. package/src/workflows/executors/index.ts +1 -0
  245. package/src/workflows/executors/registry.ts +2 -0
  246. package/src/workflows/executors/system-one-decision.ts +1232 -0
  247. package/src/workflows/executors/system-one-providers.ts +301 -0
  248. package/src/workflows/executors/workflow-llm.ts +13 -1
  249. package/src/workflows/readiness.ts +86 -0
  250. package/src/workflows/recovery.ts +12 -12
  251. package/src/workflows/resume.ts +23 -9
  252. package/templates/schedules/daily-blocker-digest/content.md +4 -5
  253. package/templates/skills/agent-browser/SKILL.md +6 -5
  254. package/templates/skills/agent-browser/content.md +6 -5
  255. package/templates/skills/heartbeat-runbook/config.json +1 -1
  256. package/templates/skills/heartbeat-runbook/content.md +2 -2
  257. package/templates/skills/swarm-scripts/SKILL.md +1 -1
  258. package/templates/skills/swarm-scripts/content.md +1 -1
  259. package/templates/skills/workflow-iterate/content.md +81 -1
  260. package/templates/workflows/claude-code-changelog-watch/content.md +2 -0
  261. package/templates/workflows/pr-review-status-sweep/content.md +2 -0
  262. package/dist/claude-sdk-session-t442qz6f.js +0 -39108
  263. package/dist/cli-0xjkj1j7.js +0 -61
  264. package/dist/cli-65hjmev5.js +0 -1141
  265. package/dist/cli-aw1jwp1r.js +0 -14075
  266. package/dist/cli-h967nb6r.js +0 -6603
  267. package/dist/cli-vapyk8sw.js +0 -20
  268. package/dist/cli-ws6rqycm.js +0 -247
  269. package/dist/pricing-refresh-v1cbr9te.js +0 -27
  270. package/src/providers/modelsdev-reasoning.json +0 -6453
  271. package/dist/{codex-login-x3jk4etn.js → codex-login-he9vrn90.js} +3 -3
  272. package/dist/{index-f24115ge.js → index-2wsdsnmm.js} +3 -3
@@ -1691,32 +1691,35 @@ describe("Session Costs API", () => {
1691
1691
  try {
1692
1692
  await getAttributionByPerson({ startDate: "2026-08-19", endDate: "2026-08-19" });
1693
1693
  const call = querySpy.mock.calls.find(([sql]) =>
1694
- String(sql).includes("task_tree(rootId, taskId, output)"),
1694
+ String(sql).includes("task_tree(rootId, taskId)"),
1695
1695
  );
1696
1696
  const sql = String(call?.[0] ?? "");
1697
- const seed = sql.slice(0, sql.indexOf("task_tree(rootId, taskId, output)"));
1697
+ const seed = sql.slice(0, sql.indexOf("task_tree(rootId, taskId)"));
1698
1698
 
1699
1699
  expect(seed).toContain("selected_roots");
1700
1700
  expect(seed).toContain("t.requestedByUserId IS NOT NULL");
1701
1701
  expect(seed).toContain("t.createdAt >= ?");
1702
1702
  expect(seed).toContain("t.createdAt < ?");
1703
1703
  expect(seed).toContain("t.parentTaskId IS NULL");
1704
+ expect(seed).toContain("t.isHumanFree = 0");
1704
1705
  expect(seed).not.toContain("human_free_tasks");
1705
1706
  expect(sql).toMatch(
1706
- /task_tree\(rootId, taskId, output\) AS \(\s*SELECT id, id, output\s*FROM selected_roots/,
1707
+ /task_tree\(rootId, taskId\) AS \(\s*SELECT id, id\s*FROM selected_roots/,
1707
1708
  );
1709
+ // The recursion carries ids only; a task's output is read by id afterwards.
1710
+ expect(sql).not.toContain("task_tree(rootId, taskId, output)");
1708
1711
  expect(sql.match(/\?/g)).toHaveLength(2);
1709
1712
 
1713
+ // Reach reads the stored human-free flag; it walks no ancestry.
1710
1714
  const reachCall = querySpy.mock.calls.find(([preparedSql]) =>
1711
- String(preparedSql).includes("task_ancestry("),
1715
+ String(preparedSql).includes("COUNT(DISTINCT t.agentId)"),
1712
1716
  );
1713
1717
  const reachSql = String(reachCall?.[0] ?? "");
1714
- const reachSeed = reachSql.slice(0, reachSql.indexOf("task_ancestry("));
1715
- expect(reachSeed).toContain("report_tasks");
1716
- expect(reachSeed).toContain("t.requestedByUserId IS NOT NULL");
1717
- expect(reachSeed).toContain("t.createdAt >= ?");
1718
- expect(reachSeed).toContain("t.createdAt < ?");
1719
- expect(reachSql).toContain("JOIN task_ancestry child ON parent.id = child.parentTaskId");
1718
+ expect(reachSql).toContain("t.requestedByUserId IS NOT NULL");
1719
+ expect(reachSql).toContain("t.createdAt >= ?");
1720
+ expect(reachSql).toContain("t.createdAt < ?");
1721
+ expect(reachSql).toContain("t.isHumanFree = 0");
1722
+ expect(reachSql).not.toContain("RECURSIVE");
1720
1723
  expect(reachSql.match(/\?/g)).toHaveLength(2);
1721
1724
  } finally {
1722
1725
  querySpy.mockRestore();
@@ -2107,7 +2110,7 @@ describe("Session Costs API", () => {
2107
2110
  try {
2108
2111
  await getAttributionByPerson({});
2109
2112
  const call = querySpy.mock.calls.find(([sql]) =>
2110
- String(sql).includes("task_tree(rootId, taskId, output)"),
2113
+ String(sql).includes("task_tree(rootId, taskId)"),
2111
2114
  );
2112
2115
  const sql = String(call?.[0] ?? "");
2113
2116
 
@@ -240,6 +240,107 @@ describe("extractSlackMessageText", () => {
240
240
  expect(result).toContain("Details here");
241
241
  });
242
242
 
243
+ // Shape of a real multi-line human message: Slack sends mrkdwn `text` and a
244
+ // rich_text block with one element per styled run, mention and link.
245
+ const multiLineHumanMessage = {
246
+ text: "<@U0A1B2C3D> can you check the deploy?\n*It failed* twice &amp; the logs say:\n• timeout on <https://ci.example.com/run/42|run 42>\n• retry :fire:",
247
+ blocks: [
248
+ {
249
+ type: "rich_text",
250
+ elements: [
251
+ {
252
+ type: "rich_text_section",
253
+ elements: [
254
+ { type: "user", user_id: "U0A1B2C3D" },
255
+ { type: "text", text: " can you check the deploy?\n" },
256
+ { type: "text", text: "It failed", style: { bold: true } },
257
+ { type: "text", text: " twice & the logs say:\n" },
258
+ ],
259
+ },
260
+ {
261
+ type: "rich_text_list",
262
+ style: "bullet",
263
+ elements: [
264
+ {
265
+ type: "rich_text_section",
266
+ elements: [
267
+ { type: "text", text: "timeout on " },
268
+ { type: "link", url: "https://ci.example.com/run/42", text: "run 42" },
269
+ ],
270
+ },
271
+ {
272
+ type: "rich_text_section",
273
+ elements: [
274
+ { type: "text", text: "retry " },
275
+ { type: "emoji", name: "fire" },
276
+ ],
277
+ },
278
+ ],
279
+ },
280
+ ],
281
+ },
282
+ ],
283
+ };
284
+
285
+ test("multi-line rich_text human message renders once, as the top-level text", () => {
286
+ const result = extractSlackMessageText(multiLineHumanMessage);
287
+ expect(result).toBe(multiLineHumanMessage.text);
288
+ expect(result.split("can you check the deploy?").length - 1).toBe(1);
289
+ });
290
+
291
+ test("single-line rich_text with a mention renders once and keeps the mention", () => {
292
+ const msg = {
293
+ text: "hey <@U0A1B2C3D> ship it",
294
+ blocks: [
295
+ {
296
+ type: "rich_text",
297
+ elements: [
298
+ {
299
+ type: "rich_text_section",
300
+ elements: [
301
+ { type: "text", text: "hey " },
302
+ { type: "user", user_id: "U0A1B2C3D" },
303
+ { type: "text", text: " ship it" },
304
+ ],
305
+ },
306
+ ],
307
+ },
308
+ ],
309
+ };
310
+ expect(extractSlackMessageText(msg)).toBe("hey <@U0A1B2C3D> ship it");
311
+ });
312
+
313
+ test("negative control: rich_text that differs from top-level text keeps both", () => {
314
+ const msg = {
315
+ text: "Deploy summary",
316
+ blocks: [
317
+ {
318
+ type: "rich_text",
319
+ elements: [
320
+ {
321
+ type: "rich_text_section",
322
+ elements: [{ type: "text", text: "Deploy failed on step 3\nRolled back to v1.2" }],
323
+ },
324
+ ],
325
+ },
326
+ ],
327
+ };
328
+ expect(extractSlackMessageText(msg)).toBe(
329
+ "Deploy summary\nDeploy failed on step 3\nRolled back to v1.2",
330
+ );
331
+ });
332
+
333
+ test("negative control: rich_text dedupe leaves non-rich_text blocks in place", () => {
334
+ const msg = {
335
+ ...multiLineHumanMessage,
336
+ blocks: [
337
+ ...multiLineHumanMessage.blocks,
338
+ { type: "context", elements: [{ type: "mrkdwn", text: "Sent from CI bot" }] },
339
+ ],
340
+ };
341
+ expect(extractSlackMessageText(msg)).toBe(`${multiLineHumanMessage.text}\nSent from CI bot`);
342
+ });
343
+
243
344
  test("context block elements are captured", () => {
244
345
  const msg = {
245
346
  text: "",
@@ -0,0 +1,93 @@
1
+ import { afterAll, beforeAll, describe, expect, mock, test } from "bun:test";
2
+ import { unlinkSync } from "node:fs";
3
+ import { closeDb, createAgent, getLatestActiveTaskInThread, initDb } from "../be/db";
4
+
5
+ process.env.SLACK_RENDER_V2 = "false";
6
+ process.env.STEERING_ENABLED = "false";
7
+
8
+ const TEST_DB_PATH = "./test-slack-thread-buffer-context.sqlite";
9
+
10
+ // Replies Slack returns for the thread: the root, one earlier human reply, and
11
+ // the buffered follow-up itself (conversations.replies includes it by the time
12
+ // the buffer flushes).
13
+ const threadMessages = [
14
+ { ts: "7000.0001", user: "U0ROOT", text: "original request" },
15
+ { ts: "7000.0005", user: "U0EARLIER", text: "earlier reply that is only context" },
16
+ {
17
+ ts: "7000.0010",
18
+ user: "U0FOLLOWUP",
19
+ text: "please also fix the retry\nand add a test for it",
20
+ blocks: [
21
+ {
22
+ type: "rich_text",
23
+ elements: [
24
+ {
25
+ type: "rich_text_section",
26
+ elements: [{ type: "text", text: "please also fix the retry\nand add a test for it" }],
27
+ },
28
+ ],
29
+ },
30
+ ],
31
+ },
32
+ ];
33
+
34
+ const realSlackApp = await import("../slack/app");
35
+ mock.module("../slack/app", () => ({
36
+ ...realSlackApp,
37
+ getSlackApp: () => ({
38
+ client: {
39
+ conversations: { replies: async () => ({ ok: true, messages: threadMessages }) },
40
+ users: { info: async () => ({ ok: true, user: { profile: {} } }) },
41
+ chat: { postMessage: async () => ({ ok: true, ts: "7000.0099" }) },
42
+ },
43
+ }),
44
+ }));
45
+
46
+ const { instantFlush, bufferThreadMessage } = await import("../slack/thread-buffer");
47
+
48
+ beforeAll(async () => {
49
+ initDb(TEST_DB_PATH);
50
+ await createAgent({ name: "lead-agent", isLead: true, status: "idle", capabilities: [] });
51
+ });
52
+
53
+ afterAll(() => {
54
+ closeDb();
55
+ for (const suffix of ["", "-wal", "-shm"]) {
56
+ try {
57
+ unlinkSync(`${TEST_DB_PATH}${suffix}`);
58
+ } catch {}
59
+ }
60
+ });
61
+
62
+ describe("buffered follow-up task description", () => {
63
+ test("carries each buffered message once, outside <thread_context>", async () => {
64
+ const channelId = "C700";
65
+ const threadTs = "7000.0001";
66
+ bufferThreadMessage(
67
+ channelId,
68
+ threadTs,
69
+ "please also fix the retry\nand add a test for it",
70
+ "U0FOLLOWUP",
71
+ "7000.0010",
72
+ );
73
+
74
+ await instantFlush(`${channelId}:${threadTs}`);
75
+
76
+ const task = await getLatestActiveTaskInThread(channelId, threadTs);
77
+ expect(task).not.toBeNull();
78
+ const description = task!.task;
79
+ const context = description.slice(
80
+ description.indexOf("<thread_context>"),
81
+ description.indexOf("</thread_context>"),
82
+ );
83
+
84
+ // The follow-up appears exactly once, in the buffered section.
85
+ expect(description.split("please also fix the retry").length - 1).toBe(1);
86
+ expect(context).not.toContain("please also fix the retry");
87
+ expect(description).toContain("1 message(s) buffered");
88
+
89
+ // Negative control: messages that are not buffered stay in the context.
90
+ expect(context).toContain("original request");
91
+ expect(context).toContain("earlier reply that is only context");
92
+ });
93
+ });
@@ -1,6 +1,7 @@
1
1
  import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
2
  import { unlinkSync } from "node:fs";
3
3
  import {
4
+ backfillSupersedeTaskResumeTaskId,
4
5
  cancelTask,
5
6
  cascadeFailDependents,
6
7
  closeDb,
@@ -230,7 +231,7 @@ describe("cascadeFailDependents", () => {
230
231
  expect(childAfter!.failureReason).toContain("was cancelled");
231
232
  });
232
233
 
233
- test("supersedeTask cascades to dependents", async () => {
234
+ test("supersedeTask with a resume id re-points never-started dependents", async () => {
234
235
  const agent = await createAgent({
235
236
  name: "cascade-worker-6",
236
237
  isLead: false,
@@ -239,19 +240,164 @@ describe("cascadeFailDependents", () => {
239
240
  });
240
241
 
241
242
  const parent = await createTaskExtended("Parent supersede", { agentId: agent.id });
243
+ const other = await createTaskExtended("Other dependency", { agentId: agent.id });
242
244
  const child = await createTaskExtended("Child of superseded", {
245
+ agentId: agent.id,
246
+ dependsOn: [parent.id, other.id],
247
+ });
248
+ const resume = await createTaskExtended("Resume of parent", { agentId: agent.id });
249
+
250
+ await startTask(parent.id);
251
+ await supersedeTask(parent.id, { reason: "context limit", resumeTaskId: resume.id });
252
+
253
+ const childAfter = await getTaskById(child.id);
254
+ expect(childAfter!.status).toBe("pending");
255
+ expect(childAfter!.dependsOn).toEqual([resume.id, other.id]);
256
+
257
+ const log = await getDbClient().get<{ oldValue: string; newValue: string; metadata: string }>(
258
+ "SELECT oldValue, newValue, metadata FROM agent_log WHERE taskId = ? AND eventType = 'task_dependency_repointed'",
259
+ [child.id],
260
+ );
261
+ expect(log?.oldValue).toBe(parent.id);
262
+ expect(log?.newValue).toBe(resume.id);
263
+ expect(JSON.parse(log!.metadata).reason).toBe("supersede_resume");
264
+ });
265
+
266
+ test("supersedeTask without a resume id defers dependents until the backfill", async () => {
267
+ const agent = await createAgent({
268
+ name: "cascade-worker-6b",
269
+ isLead: false,
270
+ status: "idle",
271
+ capabilities: [],
272
+ });
273
+
274
+ const parent = await createTaskExtended("Parent supersede late resume", { agentId: agent.id });
275
+ const child = await createTaskExtended("Child of late-resumed", {
276
+ agentId: agent.id,
277
+ dependsOn: [parent.id],
278
+ });
279
+
280
+ await startTask(parent.id);
281
+ await supersedeTask(parent.id, { reason: "graceful_shutdown", resumeTaskId: null });
282
+
283
+ // The window between supersede and resume creation must not fail it.
284
+ expect((await getTaskById(child.id))!.status).toBe("pending");
285
+
286
+ const resume = await createTaskExtended("Late resume", { agentId: agent.id });
287
+ await backfillSupersedeTaskResumeTaskId(parent.id, resume.id);
288
+
289
+ const childAfter = await getTaskById(child.id);
290
+ expect(childAfter!.status).toBe("pending");
291
+ expect(childAfter!.dependsOn).toEqual([resume.id]);
292
+ });
293
+
294
+ test("supersede settle does not rewrite a dependent that already started", async () => {
295
+ const agent = await createAgent({
296
+ name: "cascade-worker-6c",
297
+ isLead: false,
298
+ status: "idle",
299
+ capabilities: [],
300
+ });
301
+
302
+ const parent = await createTaskExtended("Parent supersede started dep", {
303
+ agentId: agent.id,
304
+ });
305
+ const child = await createTaskExtended("Started child", {
243
306
  agentId: agent.id,
244
307
  dependsOn: [parent.id],
245
308
  });
309
+ const resume = await createTaskExtended("Resume for started dep", { agentId: agent.id });
246
310
 
247
311
  await startTask(parent.id);
248
- await supersedeTask(parent.id, { reason: "context limit", resumeTaskId: null });
312
+ await startTask(child.id);
313
+ await supersedeTask(parent.id, { reason: "context limit", resumeTaskId: resume.id });
249
314
 
250
315
  const childAfter = await getTaskById(child.id);
316
+ expect(childAfter!.dependsOn).toEqual([parent.id]);
251
317
  expect(childAfter!.status).toBe("failed");
252
318
  expect(childAfter!.failureReason).toContain("was superseded");
253
319
  });
254
320
 
321
+ test("concurrent settlements of a shared dependent keep both replacements", async () => {
322
+ const agent = await createAgent({
323
+ name: "cascade-worker-6d",
324
+ isLead: false,
325
+ status: "idle",
326
+ capabilities: [],
327
+ });
328
+
329
+ const a = await createTaskExtended("Shared parent A", { agentId: agent.id });
330
+ const b = await createTaskExtended("Shared parent B", { agentId: agent.id });
331
+ const child = await createTaskExtended("Depends on A and B", {
332
+ agentId: agent.id,
333
+ dependsOn: [a.id, b.id],
334
+ });
335
+ const ra = await createTaskExtended("Resume of A", { agentId: agent.id });
336
+ const rb = await createTaskExtended("Resume of B", { agentId: agent.id });
337
+
338
+ for (const parent of [a, b]) {
339
+ await startTask(parent.id);
340
+ await supersedeTask(parent.id, { reason: "crash_recovery", resumeTaskId: null });
341
+ }
342
+ await Promise.all([
343
+ backfillSupersedeTaskResumeTaskId(a.id, ra.id),
344
+ backfillSupersedeTaskResumeTaskId(b.id, rb.id),
345
+ ]);
346
+
347
+ const childAfter = await getTaskById(child.id);
348
+ expect(childAfter!.status).toBe("pending");
349
+ expect(childAfter!.dependsOn).toEqual([ra.id, rb.id]);
350
+ });
351
+
352
+ // A dependent's status changes right after any read the settlement makes.
353
+ // An accept out of `offered` is still never-started and must be re-pointed;
354
+ // the claim-into-in_progress guard is covered by the started-dependent test.
355
+ for (const moveTo of ["reviewing", "pending"] as const) {
356
+ test(`settlement racing offered -> ${moveTo} still re-points the dependent`, async () => {
357
+ const agent = await createAgent({
358
+ name: `cascade-worker-6e-${moveTo}`,
359
+ isLead: false,
360
+ status: "idle",
361
+ capabilities: [],
362
+ });
363
+
364
+ const parent = await createTaskExtended("Parent of offered dep", { agentId: agent.id });
365
+ const child = await createTaskExtended("Offered child", {
366
+ agentId: agent.id,
367
+ dependsOn: [parent.id],
368
+ });
369
+ const resume = await createTaskExtended("Resume for offered dep", { agentId: agent.id });
370
+ const client = getDbClient();
371
+ await client.run("UPDATE agent_tasks SET status = 'offered' WHERE id = ?", [child.id]);
372
+ await startTask(parent.id);
373
+ await supersedeTask(parent.id, { reason: "crash_recovery", resumeTaskId: null });
374
+
375
+ const query = client.query.bind(client);
376
+ let raced = false;
377
+ client.query = (async (sql: string, params?: unknown[]) => {
378
+ const rows = (await query(sql, params as never)) as { id?: string; status?: string }[];
379
+ if (!raced && rows.some((r) => r.id === child.id && r.status === "offered")) {
380
+ raced = true;
381
+ await client.run("UPDATE agent_tasks SET status = ? WHERE id = ?", [moveTo, child.id]);
382
+ }
383
+ return rows;
384
+ }) as typeof client.query;
385
+ try {
386
+ await backfillSupersedeTaskResumeTaskId(parent.id, resume.id);
387
+ } finally {
388
+ client.query = query;
389
+ }
390
+ // No read to race (single-statement settlement): the accept lands after.
391
+ if (!raced) {
392
+ await client.run("UPDATE agent_tasks SET status = ? WHERE id = ?", [moveTo, child.id]);
393
+ }
394
+
395
+ const childAfter = await getTaskById(child.id);
396
+ expect(childAfter!.status).toBe(moveTo);
397
+ expect(childAfter!.dependsOn).toEqual([resume.id]);
398
+ });
399
+ }
400
+
255
401
  test("wide fan-out: multiple dependents all cascade-failed", async () => {
256
402
  const agent = await createAgent({
257
403
  name: "cascade-worker-7",
@@ -378,7 +378,7 @@ describe("Tool Annotations & Classification", () => {
378
378
  // Includes 11 skill tools, 7 MCP server tools, reusable script tools, and the
379
379
  // native Kapso/WhatsApp tools (register/unregister number + send/reply message).
380
380
  expect(count).toBeGreaterThanOrEqual(45);
381
- expect(count).toBeLessThanOrEqual(149);
381
+ expect(count).toBeLessThanOrEqual(151);
382
382
  });
383
383
 
384
384
  test("core tools are fewer than deferred tools", () => {
@@ -0,0 +1,166 @@
1
+ import { afterEach, beforeEach, describe, expect, setSystemTime, test } from "bun:test";
2
+ import {
3
+ cachedUsageReport,
4
+ clearUsageCache,
5
+ USAGE_CACHE_MAX_STALE_MS,
6
+ USAGE_CACHE_TTL_MS,
7
+ } from "../http/usage-cache";
8
+
9
+ const T0 = new Date("2026-09-29T12:00:00.000Z");
10
+
11
+ function loader(values: unknown[]) {
12
+ let calls = 0;
13
+ const load = async () => {
14
+ const value = values[Math.min(calls, values.length - 1)];
15
+ calls++;
16
+ return value;
17
+ };
18
+ return { load, calls: () => calls };
19
+ }
20
+
21
+ // The background refresh is a promise chain; let it settle.
22
+ const settle = () => new Promise((resolve) => setTimeout(resolve, 0));
23
+
24
+ describe("cachedUsageReport", () => {
25
+ beforeEach(() => {
26
+ clearUsageCache();
27
+ setSystemTime(T0);
28
+ });
29
+ afterEach(() => {
30
+ clearUsageCache();
31
+ setSystemTime();
32
+ });
33
+
34
+ test("serves a fresh entry without loading again", async () => {
35
+ const source = loader(["a", "b"]);
36
+ expect(await cachedUsageReport("k", source.load)).toBe("a");
37
+ setSystemTime(new Date(T0.getTime() + USAGE_CACHE_TTL_MS - 1));
38
+ expect(await cachedUsageReport("k", source.load)).toBe("a");
39
+ expect(source.calls()).toBe(1);
40
+ });
41
+
42
+ test("keeps keys apart", async () => {
43
+ const source = loader(["a", "b"]);
44
+ expect(await cachedUsageReport("one", source.load)).toBe("a");
45
+ expect(await cachedUsageReport("two", source.load)).toBe("b");
46
+ expect(source.calls()).toBe(2);
47
+ });
48
+
49
+ test("past the bucket it answers with the stale value and refreshes once in the background", async () => {
50
+ const source = loader(["old", "new"]);
51
+ await cachedUsageReport("k", source.load);
52
+
53
+ setSystemTime(new Date(T0.getTime() + USAGE_CACHE_TTL_MS + 1));
54
+ expect(await cachedUsageReport("k", source.load)).toBe("old");
55
+ expect(await cachedUsageReport("k", source.load)).toBe("old");
56
+ await settle();
57
+ // One refresh served both stale reads.
58
+ expect(source.calls()).toBe(2);
59
+ expect(await cachedUsageReport("k", source.load)).toBe("new");
60
+ expect(source.calls()).toBe(2);
61
+ });
62
+
63
+ test("a stale read does not wait for the refresh", async () => {
64
+ let release: (value: string) => void = () => {};
65
+ const slow = new Promise<string>((resolve) => {
66
+ release = resolve;
67
+ });
68
+ await cachedUsageReport("k", async () => "old");
69
+
70
+ setSystemTime(new Date(T0.getTime() + USAGE_CACHE_TTL_MS + 1));
71
+ expect(await cachedUsageReport("k", () => slow)).toBe("old");
72
+ release("new");
73
+ await settle();
74
+ expect(await cachedUsageReport("k", async () => "unused")).toBe("new");
75
+ });
76
+
77
+ test("blocks on a load once an entry is older than the stale limit", async () => {
78
+ const source = loader(["old", "new"]);
79
+ await cachedUsageReport("k", source.load);
80
+
81
+ setSystemTime(new Date(T0.getTime() + USAGE_CACHE_MAX_STALE_MS + 1));
82
+ expect(await cachedUsageReport("k", source.load)).toBe("new");
83
+ expect(source.calls()).toBe(2);
84
+ });
85
+
86
+ test("concurrent misses share one load", async () => {
87
+ let calls = 0;
88
+ const load = async () => {
89
+ calls++;
90
+ await settle();
91
+ return "shared";
92
+ };
93
+ const results = await Promise.all([
94
+ cachedUsageReport("k", load),
95
+ cachedUsageReport("k", load),
96
+ cachedUsageReport("k", load),
97
+ ]);
98
+ expect(results).toEqual(["shared", "shared", "shared"]);
99
+ expect(calls).toBe(1);
100
+ });
101
+
102
+ test("a failed background refresh keeps the stale value and the next read retries", async () => {
103
+ await cachedUsageReport("k", async () => "old");
104
+ setSystemTime(new Date(T0.getTime() + USAGE_CACHE_TTL_MS + 1));
105
+
106
+ const original = console.error;
107
+ console.error = () => {};
108
+ try {
109
+ expect(
110
+ await cachedUsageReport("k", async () => {
111
+ throw new Error("db busy");
112
+ }),
113
+ ).toBe("old");
114
+ await settle();
115
+ } finally {
116
+ console.error = original;
117
+ }
118
+
119
+ expect(await cachedUsageReport("k", async () => "retried")).toBe("old");
120
+ await settle();
121
+ expect(await cachedUsageReport("k", async () => "unused")).toBe("retried");
122
+ });
123
+
124
+ test("a load with no stale value to fall back on rejects to the caller", async () => {
125
+ await expect(
126
+ cachedUsageReport("k", async () => {
127
+ throw new Error("db busy");
128
+ }),
129
+ ).rejects.toThrow("db busy");
130
+ // The failure is not cached.
131
+ expect(await cachedUsageReport("k", async () => "ok")).toBe("ok");
132
+ });
133
+
134
+ test("clearing drops entries, and a load already running cannot write back", async () => {
135
+ let release: (value: string) => void = () => {};
136
+ const inFlight = new Promise<string>((resolve) => {
137
+ release = resolve;
138
+ });
139
+ const pending = cachedUsageReport("k", () => inFlight);
140
+
141
+ clearUsageCache();
142
+ release("before-clear");
143
+ expect(await pending).toBe("before-clear");
144
+
145
+ // Nothing was stored, so the next read loads again.
146
+ expect(await cachedUsageReport("k", async () => "after-clear")).toBe("after-clear");
147
+ });
148
+
149
+ test("evicts the oldest entry past the size cap", async () => {
150
+ for (let i = 0; i < 100; i++) await cachedUsageReport(`k${i}`, async () => `v${i}`);
151
+ await cachedUsageReport("extra", async () => "extra");
152
+
153
+ let reloaded = false;
154
+ await cachedUsageReport("k0", async () => {
155
+ reloaded = true;
156
+ return "v0-again";
157
+ });
158
+ expect(reloaded).toBe(true);
159
+ let survivorReloaded = false;
160
+ await cachedUsageReport("k99", async () => {
161
+ survivorReloaded = true;
162
+ return "unused";
163
+ });
164
+ expect(survivorReloaded).toBe(false);
165
+ });
166
+ });
@@ -984,7 +984,7 @@ describe("ValidateExecutor", () => {
984
984
  // ─── Registry Wiring ─────────────────────────────────────────
985
985
 
986
986
  describe("createExecutorRegistry", () => {
987
- test("registers all 12 executors (8 instant + 4 async)", () => {
987
+ test("registers all 13 executors (9 instant + 4 async)", () => {
988
988
  const registry = createExecutorRegistry(mockDeps);
989
989
  const types = registry.types();
990
990
 
@@ -992,6 +992,7 @@ describe("createExecutorRegistry", () => {
992
992
  expect(types).toContain("code-match");
993
993
  expect(types).toContain("notify");
994
994
  expect(types).toContain("raw-llm");
995
+ expect(types).toContain("system-one-decision");
995
996
  expect(types).toContain("script");
996
997
  expect(types).toContain("swarm-script");
997
998
  expect(types).toContain("vcs");
@@ -1000,7 +1001,7 @@ describe("createExecutorRegistry", () => {
1000
1001
  expect(types).toContain("foreach");
1001
1002
  expect(types).toContain("human-in-the-loop");
1002
1003
  expect(types).toContain("wait");
1003
- expect(types).toHaveLength(12);
1004
+ expect(types).toHaveLength(13);
1004
1005
  });
1005
1006
 
1006
1007
  test("instant executors have mode instant, async executors have mode async", () => {
@@ -1022,6 +1023,8 @@ describe("createExecutorRegistry", () => {
1022
1023
  expect(registry.get("foreach").mode).toBe("async");
1023
1024
  expect(registry.get("human-in-the-loop").mode).toBe("async");
1024
1025
  expect(registry.get("wait").mode).toBe("async");
1026
+ // Answers at once, but waits when `humanReview` sends an answer to a person.
1027
+ expect(registry.get("system-one-decision").mode).toBe("async");
1025
1028
  });
1026
1029
 
1027
1030
  test("get() retrieves the correct executor by type", () => {