@desplega.ai/agent-swarm 1.164.1 → 1.166.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 (328) hide show
  1. package/README.md +1 -1
  2. package/dist/acp-adapter-yzfxn8vj.js +23 -0
  3. package/dist/{actions-hp2h32dx.js → actions-qz3m692w.js} +24 -28
  4. package/dist/{amp-adapter-zfd7djyx.js → amp-adapter-j1xbmsqz.js} +7 -7
  5. package/dist/{app-w2d26h4f.js → app-43vrdmv8.js} +9 -10
  6. package/dist/{assistant-45pc4en7.js → assistant-ertffmka.js} +25 -29
  7. package/dist/{boot-reembed-k1q2s4fc.js → boot-reembed-998jtpdg.js} +12 -15
  8. package/dist/{boot-reembed-6rhcpsjf.js → boot-reembed-s7k7c6v2.js} +13 -15
  9. package/dist/boot-scrub-sweep-nepsk4t1.js +211 -0
  10. package/dist/{boot-seeds-ex93edx7.js → boot-seeds-vvzs1zhk.js} +12 -12
  11. package/dist/{claude-adapter-hcxd0szm.js → claude-adapter-2zvmekrp.js} +15 -17
  12. package/dist/{claude-managed-adapter-fz7xps2r.js → claude-managed-adapter-s49wbk8t.js} +5 -5
  13. package/dist/{claude-managed-setup-xx6f15h9.js → claude-managed-setup-cvhqq8qn.js} +4 -4
  14. package/dist/{claude-sdk-session-6nb9raem.js → claude-sdk-session-kkbxtw5q.js} +2624 -2606
  15. package/dist/{cli-etbrt8x1.js → cli-0nhdwxd5.js} +4 -4
  16. package/dist/{cli-jeefgppq.js → cli-0qjcsxgz.js} +7 -7
  17. package/dist/{cli-6behs2bt.js → cli-32bca7nr.js} +1 -1
  18. package/dist/{cli-gt8v96qx.js → cli-3ddybbbx.js} +4 -4
  19. package/dist/{acp-adapter-rxvek4ym.js → cli-3qn4r334.js} +317 -35
  20. package/dist/{cli-1d3w4xm3.js → cli-4r1sjfc4.js} +14 -6
  21. package/dist/{cli-691f2rxm.js → cli-66zh26sp.js} +68 -16
  22. package/dist/{cli-et8wdyqh.js → cli-68r1j4sg.js} +9 -9
  23. package/dist/{cli-xb7kyanp.js → cli-8kgvmkak.js} +3 -3
  24. package/dist/{cli-fdpkw8vk.js → cli-8t229cmr.js} +8 -8
  25. package/dist/{cli-gc54qc0p.js → cli-aqcey741.js} +1 -1
  26. package/dist/{cli-4q4cmr4t.js → cli-cpfmqz3n.js} +8 -8
  27. package/dist/{cli-a9bw0xxt.js → cli-df56tqdw.js} +997 -408
  28. package/dist/{cli-n7h1kqpf.js → cli-dmtrxqhg.js} +6 -1
  29. package/dist/{cli-xfb7333f.js → cli-dw2q9f7g.js} +5 -5
  30. package/dist/{cli-4drstbb9.js → cli-ecknp7p1.js} +81 -23
  31. package/dist/{cli-v0pvvhfq.js → cli-efnd2kce.js} +36 -1
  32. package/dist/{cli-1wetf34q.js → cli-f2n17zhp.js} +1 -1
  33. package/dist/{cli-2c8eky5y.js → cli-feh3dkah.js} +1 -1
  34. package/dist/cli-fn697bnt.js +126 -0
  35. package/dist/{cli-v5pr2fgt.js → cli-gt8azyet.js} +3 -3
  36. package/dist/{cli-dbszf3rw.js → cli-h3x613ze.js} +4 -4
  37. package/dist/{cli-nwhb8y2e.js → cli-hf5s9pgf.js} +1 -1
  38. package/dist/cli-j2jbr8af.js +516 -0
  39. package/dist/{cli-agjzafns.js → cli-jhm6hzjn.js} +593 -10
  40. package/dist/{cli-j0tmc908.js → cli-jvj6a0cv.js} +484 -477
  41. package/dist/{cli-xxz89c2n.js → cli-mb75wpxf.js} +1 -1
  42. package/dist/{cli-07s39g1n.js → cli-mv2520nm.js} +1 -1
  43. package/dist/{cli-v90vfwyb.js → cli-mwegs4af.js} +5 -5
  44. package/dist/{cli-bz1gbac9.js → cli-p4ztb80h.js} +2 -2
  45. package/dist/{cli-1wd1n2ya.js → cli-rfphkzh5.js} +1 -1
  46. package/dist/{cli-976r1zqx.js → cli-rggdq8wq.js} +5 -5
  47. package/dist/{cli-3ajw5v5v.js → cli-rsy933yz.js} +6 -6
  48. package/dist/{cli-yj790e19.js → cli-tadfgd0t.js} +8 -1
  49. package/dist/{cli-cn53e7vc.js → cli-ttbsse3f.js} +6 -1
  50. package/dist/{cli-dyqsrqcf.js → cli-v2a8x45d.js} +4 -4
  51. package/dist/{cli-wbvnn6tk.js → cli-v6vv7dy8.js} +91 -9
  52. package/dist/{cli-z2ap2c0c.js → cli-vzc73wet.js} +90 -55
  53. package/dist/cli-wt7ccy4r.js +2654 -0
  54. package/dist/{cli-4cn19jvn.js → cli-x7kpr72a.js} +3 -3
  55. package/dist/{cli-003785v9.js → cli-xvc3q9br.js} +1 -1
  56. package/dist/{cli-v0e7jpr5.js → cli-xvdgasgy.js} +13 -4
  57. package/dist/{cli-asqeg4ve.js → cli-y59h2pk9.js} +2 -2
  58. package/dist/{cli-0wn3ypqv.js → cli-yshaazs1.js} +14121 -3478
  59. package/dist/{cli-wg8yjqtw.js → cli-ysnyhws9.js} +16 -3
  60. package/dist/cli-zp67qg5x.js +92 -0
  61. package/dist/{cli-g3qgrzck.js → cli-zsh093hk.js} +71 -3
  62. package/dist/{cli-ma3zvh39.js → cli-zy7vwsne.js} +6066 -5829
  63. package/dist/cli.js +79 -26
  64. package/dist/{codex-adapter-pcxdn10v.js → codex-adapter-dw1wxqt8.js} +10 -10
  65. package/dist/{codex-hook-b1fb7c2k.js → codex-hook-d2rkvf39.js} +1 -1
  66. package/dist/{codex-login-059nm145.js → codex-login-terbdsqq.js} +2 -2
  67. package/dist/{codex-session-runner-xzwwddp8.js → codex-session-runner-3cgb8y1g.js} +10 -10
  68. package/dist/{commands-98at8mkn.js → commands-mhxam6z2.js} +9 -9
  69. package/dist/{cursor-adapter-dkg6qnkp.js → cursor-adapter-2qda3st7.js} +6 -6
  70. package/dist/{db-01g8wtvw.js → db-sjks418k.js} +11 -9
  71. package/dist/{devin-adapter-qmttt2fz.js → devin-adapter-bffjj5n4.js} +2 -2
  72. package/dist/{dispatcher-nn2k1wxd.js → dispatcher-1es759rn.js} +22 -26
  73. package/dist/{dsh-adapter-w4d4vwdq.js → dsh-adapter-bek4cyw9.js} +6 -6
  74. package/dist/{e2b-9e4g2hvt.js → e2b-nd50eccg.js} +1 -1
  75. package/dist/{event-bus-z7v2t26e.js → event-bus-vnfthafg.js} +2 -2
  76. package/dist/grok-adapter-ar9cw9jg.js +29 -0
  77. package/dist/{handlers-b0kqsbg8.js → handlers-1gkbhktr.js} +25 -29
  78. package/dist/{hook-sap7v1aj.js → hook-ds2zxq9v.js} +16 -18
  79. package/dist/{hook-pz50gqep.js → hook-vxa3tcsy.js} +10 -12
  80. package/dist/{http-ncr8rmd0.js → http-3h21z9md.js} +679 -227
  81. package/dist/{index-c4aqcyz1.js → index-9b65j1m5.js} +11 -11
  82. package/dist/{index-r46s01nq.js → index-bn6cy8ps.js} +22 -26
  83. package/dist/{index-38kqdt3e.js → index-dkfxpkmm.js} +23 -27
  84. package/dist/{index-630kj5vd.js → index-f8xnqrye.js} +55 -43
  85. package/dist/{index-fdk1t4d6.js → index-n0rxa4ca.js} +11962 -9900
  86. package/dist/{index-9nrj7797.js → index-n99fycq7.js} +22 -26
  87. package/dist/{keepalive-31613r2h.js → keepalive-3xspa9jw.js} +14 -11
  88. package/dist/{lead-mx59tr5s.js → lead-6qphc124.js} +43 -43
  89. package/dist/{maintenance-55ff39ca.js → maintenance-0teem1bb.js} +10 -12
  90. package/dist/{node-f0ykebbv.js → node-9sg7ajby.js} +25 -7
  91. package/dist/{oauth-refresh-sweep-anma954w.js → oauth-refresh-sweep-sh9q25xp.js} +13 -10
  92. package/dist/{onboard-b2fy8ftb.js → onboard-54qmnqxe.js} +4 -3
  93. package/dist/{opencode-adapter-p1s92tmy.js → opencode-adapter-wjfa2wvd.js} +6 -6
  94. package/dist/{otel-impl-9brvt7xv.js → otel-impl-ycqzp2a1.js} +2 -2
  95. package/dist/{pi-mono-adapter-kz4rrwvd.js → pi-mono-adapter-e77e9zst.js} +6 -6
  96. package/dist/{pricing-refresh-7w8mbmva.js → pricing-refresh-atnkngqs.js} +10 -10
  97. package/dist/{rbac-roles-ejp7qwvb.js → rbac-roles-9pjphh9q.js} +10 -10
  98. package/dist/{rbac-roles-nhk4r7bc.js → rbac-roles-j78wdksm.js} +10 -10
  99. package/dist/{render-v2-2gzt8skq.js → render-v2-pxjs49fx.js} +9 -10
  100. package/dist/{setup-5et8t412.js → setup-ymej0xtx.js} +2 -2
  101. package/dist/{work-objects-tb0er7qa.js → work-objects-3nh4ymzb.js} +26 -30
  102. package/dist/{worker-62cv075d.js → worker-qa9fkz5s.js} +43 -43
  103. package/dist/{x-vbxb31jr.js → x-ma4gnfgy.js} +2 -2
  104. package/openapi.json +457 -31
  105. package/package.json +14 -6
  106. package/plugin/commands/create-pr.md +7 -1
  107. package/plugin/pi-skills/create-pr/SKILL.md +7 -1
  108. package/src/agentmail/app.ts +5 -4
  109. package/src/be/automation-preflight.ts +11 -9
  110. package/src/be/boot-scrub-sweep.ts +302 -0
  111. package/src/be/connector-codes.ts +81 -0
  112. package/src/be/crypto/key-bootstrap.ts +1 -1
  113. package/src/be/db/approvals.ts +15 -1
  114. package/src/be/db/codex-oauth-device-flows.ts +40 -0
  115. package/src/be/db/config-secrets.ts +31 -0
  116. package/src/be/db/runtime.ts +4 -0
  117. package/src/be/db/script-runs.ts +15 -0
  118. package/src/be/db/tasks/write.ts +1 -1
  119. package/src/be/db-queries/mcp-oauth.ts +5 -0
  120. package/src/be/db-queries/oauth.ts +77 -0
  121. package/src/be/db.ts +162 -59
  122. package/src/be/events.ts +17 -1
  123. package/src/be/memory/index-content.ts +2 -2
  124. package/src/be/memory/providers/sqlite-store.ts +63 -11
  125. package/src/be/memory/types.ts +14 -0
  126. package/src/be/migrations/197_connector_codes.sql +23 -0
  127. package/src/be/migrations/198_permission_audit_guest.sql +33 -0
  128. package/src/be/migrations/200_backfill_auto_memory_expiry.sql +20 -0
  129. package/src/be/migrations/201_workflow_replay_columns.sql +10 -0
  130. package/src/be/migrations/202_codex_oauth_device_flows.sql +24 -0
  131. package/src/be/model-tier-resolution.ts +2 -1
  132. package/src/be/model-validation.ts +1 -1
  133. package/src/be/pricing-normalize.ts +3 -0
  134. package/src/be/rbac-audit.ts +2 -1
  135. package/src/be/scripts/db.ts +28 -4
  136. package/src/be/scripts/source-secrets.ts +37 -0
  137. package/src/be/scripts/typecheck.ts +23 -2
  138. package/src/be/scrub-json.ts +32 -0
  139. package/src/be/sealed-json.ts +59 -0
  140. package/src/be/secret-registry.ts +131 -0
  141. package/src/be/seed-pricing.ts +14 -0
  142. package/src/be/users.ts +7 -1
  143. package/src/be/workflow-replay.ts +77 -0
  144. package/src/cli.tsx +7 -0
  145. package/src/commands/provider-credentials.ts +46 -8
  146. package/src/commands/runner.ts +2 -10
  147. package/src/extensions/contract-types.generated.ts +1 -1
  148. package/src/github/app.ts +4 -4
  149. package/src/heartbeat/heartbeat.ts +20 -8
  150. package/src/hooks/pr-body-guard.ts +1 -1
  151. package/src/http/agents.ts +1 -0
  152. package/src/http/approval-requests.ts +1 -0
  153. package/src/http/apps.ts +3 -0
  154. package/src/http/auth.ts +4 -1
  155. package/src/http/codex-oauth-device.ts +8 -10
  156. package/src/http/codex-oauth-keep-warm.ts +2 -2
  157. package/src/http/config-values.ts +59 -0
  158. package/src/http/config.ts +5 -0
  159. package/src/http/db-query-bounded.ts +3 -0
  160. package/src/http/db-query-shared.ts +107 -15
  161. package/src/http/db-query.ts +10 -2
  162. package/src/http/extensions.ts +1 -0
  163. package/src/http/index.ts +23 -6
  164. package/src/http/integrations.ts +1 -38
  165. package/src/http/ip-rate-limit.ts +82 -0
  166. package/src/http/kv.ts +38 -4
  167. package/src/http/mcp-servers.ts +1 -0
  168. package/src/http/memory.ts +9 -0
  169. package/src/http/models-catalog.ts +2 -0
  170. package/src/http/operator-actor.ts +4 -0
  171. package/src/http/page-proxy.ts +22 -0
  172. package/src/http/pages.ts +4 -2
  173. package/src/http/realtime.ts +1 -1
  174. package/src/http/request-principal.ts +14 -1
  175. package/src/http/script-connections.ts +8 -0
  176. package/src/http/script-runs.ts +12 -4
  177. package/src/http/scripts.ts +15 -0
  178. package/src/http/session-cost-recompute.ts +3 -2
  179. package/src/http/tasks.ts +9 -0
  180. package/src/http/users.ts +226 -0
  181. package/src/http/workflows.ts +3 -2
  182. package/src/http.ts +1 -0
  183. package/src/integrations/kapso/client.ts +3 -1
  184. package/src/jira/app.ts +2 -2
  185. package/src/kv-view.ts +324 -0
  186. package/src/linear/app.ts +2 -2
  187. package/src/oauth/mcp-wrapper.ts +1 -1
  188. package/src/prompts/memories.ts +42 -1
  189. package/src/providers/acp-adapter.ts +85 -24
  190. package/src/providers/acp-target-catalog.ts +54 -1
  191. package/src/providers/acp-targets.ts +358 -4
  192. package/src/providers/amp-adapter.ts +1 -1
  193. package/src/providers/cursor-adapter.ts +1 -1
  194. package/src/providers/grok-adapter.ts +186 -0
  195. package/src/providers/index.ts +5 -1
  196. package/src/providers/pricing-sources.md +6 -0
  197. package/src/providers/reasoning-effort.ts +17 -1
  198. package/src/providers/types.ts +2 -1
  199. package/src/rbac/can.ts +4 -3
  200. package/src/rbac/legacy-policy.ts +3 -3
  201. package/src/rbac/types.ts +6 -0
  202. package/src/script-workflows/supervisor.ts +11 -2
  203. package/src/scripts-runtime/eval-harness.ts +2 -12
  204. package/src/scripts-runtime/executors/quickjs-bundle.ts +152 -0
  205. package/src/scripts-runtime/executors/quickjs-prelude.ts +285 -0
  206. package/src/scripts-runtime/executors/quickjs-runner.ts +580 -0
  207. package/src/scripts-runtime/executors/quickjs-worker.ts +81 -0
  208. package/src/scripts-runtime/executors/quickjs.ts +196 -0
  209. package/src/scripts-runtime/executors/registry.ts +2 -0
  210. package/src/scripts-runtime/executors/sourcemap.ts +79 -0
  211. package/src/scripts-runtime/script-error-hints.ts +14 -0
  212. package/src/scripts-runtime/swarm-sdk.ts +9 -2
  213. package/src/scripts-runtime/types/stdlib.d.ts +27 -8
  214. package/src/scripts-runtime/types/swarm-extension.d.ts +3 -0
  215. package/src/scripts-runtime/types/swarm-sdk.d.ts +27 -8
  216. package/src/server-user.ts +2 -20
  217. package/src/server.ts +3 -1
  218. package/src/slack/app.ts +4 -0
  219. package/src/slack/config.ts +3 -1
  220. package/src/slack/egress-scrub.ts +87 -0
  221. package/src/tests/acp-adapter.test.ts +191 -2
  222. package/src/tests/boot-scrub-sweep.test.ts +390 -0
  223. package/src/tests/budget-user-scope.test.ts +7 -21
  224. package/src/tests/check-pr-body.test.ts +149 -6
  225. package/src/tests/check-text-columns.test.ts +145 -0
  226. package/src/tests/claude-adapter-binary.test.ts +1 -1
  227. package/src/tests/claude-sdk-process.fixture.ts +13 -2
  228. package/src/tests/codex-oauth-device.test.ts +23 -37
  229. package/src/tests/codex-oauth-keep-warm.test.ts +29 -0
  230. package/src/tests/console-scrub.test.ts +141 -0
  231. package/src/tests/credential-check.test.ts +90 -0
  232. package/src/tests/db-query-denylist.test.ts +202 -0
  233. package/src/tests/db-task-read-contract.test.ts +1 -0
  234. package/src/tests/db-task-write-contract.test.ts +1 -0
  235. package/src/tests/docs-index-footprint.test.js +3 -3
  236. package/src/tests/entrypoint-acp-target-binary.test.ts +44 -1
  237. package/src/tests/events-scrub.test.ts +88 -0
  238. package/src/tests/explicit-model-validation.test.ts +81 -0
  239. package/src/tests/extensions-pre-task.test.ts +33 -1
  240. package/src/tests/fixtures/grok/acp-updates-recorded.jsonl +53 -0
  241. package/src/tests/fixtures/grok/prompt-response-meta.json +36 -0
  242. package/src/tests/fixtures/secret-corpus.ts +320 -0
  243. package/src/tests/grok-adapter.test.ts +647 -0
  244. package/src/tests/heartbeat-checklist.test.ts +44 -0
  245. package/src/tests/integration-disable-flags.test.ts +156 -0
  246. package/src/tests/kv-http.test.ts +34 -0
  247. package/src/tests/kv-tool.test.ts +274 -1
  248. package/src/tests/kv-view.test.ts +147 -0
  249. package/src/tests/mcp-user-route.test.ts +257 -15
  250. package/src/tests/memory-injection-outcome.test.ts +143 -0
  251. package/src/tests/model-tier-resolution.test.ts +76 -3
  252. package/src/tests/page-guest-session.test.ts +424 -0
  253. package/src/tests/page-proxy.test.ts +1 -1
  254. package/src/tests/page-viewer-auth.test.ts +22 -1
  255. package/src/tests/pr-body-leaks.test.ts +157 -2
  256. package/src/tests/preload.ts +10 -0
  257. package/src/tests/provider-registration.test.ts +3 -1
  258. package/src/tests/rbac-audit.test.ts +32 -0
  259. package/src/tests/rbac-mcp-admission-e2e.test.ts +4 -62
  260. package/src/tests/script-connections.test.ts +9 -0
  261. package/src/tests/script-executor-conformance.test.ts +3 -0
  262. package/src/tests/script-executor-quickjs.test.ts +442 -0
  263. package/src/tests/script-executor-registry.test.ts +6 -1
  264. package/src/tests/script-runs-http.test.ts +126 -1
  265. package/src/tests/script-runs-scrub.test.ts +275 -0
  266. package/src/tests/scripts-http.test.ts +82 -1
  267. package/src/tests/scripts-mcp-e2e.test.ts +42 -6
  268. package/src/tests/scripts-runtime.test.ts +37 -0
  269. package/src/tests/scrubbed-text-12e.test.ts +289 -0
  270. package/src/tests/sealed-key-guard.test.ts +166 -0
  271. package/src/tests/secret-canary-sweep.test.ts +958 -0
  272. package/src/tests/secret-registry.test.ts +355 -0
  273. package/src/tests/secret-rules.test.ts +230 -0
  274. package/src/tests/secret-scrubber.test.ts +317 -1
  275. package/src/tests/session-costs-golden.test.ts +17 -0
  276. package/src/tests/slack-egress-scrub.test.ts +216 -0
  277. package/src/tests/swarm-tool-result-gate.test.ts +143 -30
  278. package/src/tests/task-tools-ctx.test.ts +3 -0
  279. package/src/tests/user-token-routes.test.ts +342 -3
  280. package/src/tests/workflow-replay.test.ts +518 -0
  281. package/src/tests/workflow-scrub.test.ts +242 -0
  282. package/src/tests/workflow-system-one-decision-providers.test.ts +737 -0
  283. package/src/tools/db-query.ts +1 -1
  284. package/src/tools/inject-learning.ts +1 -1
  285. package/src/tools/kv/kv-get.ts +136 -6
  286. package/src/tools/send-task.ts +61 -17
  287. package/src/tools/utils.ts +186 -43
  288. package/src/tools/workflows/get-workflow-run.ts +3 -2
  289. package/src/types.ts +16 -1
  290. package/src/utils/console-scrub-install.ts +5 -0
  291. package/src/utils/console-scrub.ts +57 -0
  292. package/src/utils/credentials.ts +8 -0
  293. package/src/utils/page-session.ts +6 -2
  294. package/src/utils/pr-body-leaks.ts +149 -10
  295. package/src/utils/request-auth-context.ts +2 -0
  296. package/src/utils/secret-rules.generated.ts +2055 -0
  297. package/src/utils/secret-scrubber.ts +662 -36
  298. package/src/workflows/engine.ts +39 -4
  299. package/src/workflows/executors/notify.ts +6 -1
  300. package/src/workflows/executors/system-one-decision.ts +31 -12
  301. package/src/workflows/executors/system-one-openai-wire.ts +168 -0
  302. package/src/workflows/executors/system-one-providers.ts +112 -11
  303. package/src/workflows/executors/system-one-wire.ts +79 -0
  304. package/src/workflows/recovery.ts +10 -1
  305. package/src/workflows/resume.ts +62 -3
  306. package/src/workflows/retry-poller.ts +3 -0
  307. package/templates/ai-toolbox.manifest.json +2 -2
  308. package/templates/skills/agent-browser/SKILL.md +5 -5
  309. package/templates/skills/agent-browser/config.json +1 -1
  310. package/templates/skills/agent-browser/content.md +4 -4
  311. package/templates/skills/artifacts/content.md +17 -7
  312. package/templates/skills/jira-interaction/config.json +1 -1
  313. package/templates/skills/jira-interaction/content.md +56 -205
  314. package/templates/skills/linear-interaction/config.json +1 -1
  315. package/templates/skills/linear-interaction/content.md +31 -50
  316. package/templates/skills/pages/content.md +8 -4
  317. package/templates/skills/qa/content.md +2 -2
  318. package/templates/skills/workflow-iterate/content.md +4 -4
  319. package/tsconfig.json +3 -3
  320. package/dist/boot-scrub-logs-t8xg7wd7.js +0 -99
  321. package/dist/cli-0a7r5331.js +0 -168
  322. package/dist/cli-93rxw9yd.js +0 -951
  323. package/dist/cli-cvk6yzw5.js +0 -40
  324. package/dist/cli-dgnj0zcc.js +0 -40
  325. package/dist/cli-qt5wzq6v.js +0 -256
  326. package/dist/cli-v7fqehqd.js +0 -9281
  327. package/dist/cli-vbr859rz.js +0 -572
  328. package/src/be/boot-scrub-logs.ts +0 -132
@@ -242,6 +242,28 @@ describe("Heartbeat Checklist", () => {
242
242
  expect(tasks.length).toBe(1);
243
243
  });
244
244
 
245
+ test("dedup: superseded heartbeat-checklist does not block a new task", async () => {
246
+ const lead = await createAgent({ name: "lead", isLead: true, status: "idle" });
247
+ await updateAgentProfile(lead.id, { heartbeatMd: "- Check tasks\n" });
248
+ await checkHeartbeatChecklist();
249
+ const previous = await getDbClient().get<{ id: string }>(
250
+ "SELECT id FROM agent_tasks WHERE taskType = 'heartbeat-checklist'",
251
+ );
252
+ expect(previous).toBeDefined();
253
+ await getDbClient().run("UPDATE agent_tasks SET status = 'superseded' WHERE id = ?", [
254
+ previous!.id,
255
+ ]);
256
+
257
+ await checkHeartbeatChecklist();
258
+
259
+ const tasks = await getDbClient().query<{ id: string; status: string }>(
260
+ "SELECT id, status FROM agent_tasks WHERE taskType = 'heartbeat-checklist'",
261
+ );
262
+ expect(tasks).toHaveLength(2);
263
+ expect(tasks.find((task) => task.id === previous!.id)?.status).toBe("superseded");
264
+ expect(tasks.find((task) => task.id !== previous!.id)?.status).toBe("pending");
265
+ });
266
+
245
267
  test("created task includes system status with [auto-generated] labels", async () => {
246
268
  const lead = await createAgent({ name: "lead", isLead: true, status: "idle" });
247
269
  await updateAgentProfile(lead.id, {
@@ -427,6 +449,28 @@ describe("Heartbeat Checklist", () => {
427
449
  expect(tasks.length).toBe(1);
428
450
  });
429
451
 
452
+ test("dedup: superseded boot-triage does not block a new task", async () => {
453
+ const lead = await createAgent({ name: "lead", isLead: true, status: "idle" });
454
+ await updateAgentProfile(lead.id, { heartbeatMd: "- Check tasks\n" });
455
+ await createBootTriageTask();
456
+ const previous = await getDbClient().get<{ id: string }>(
457
+ "SELECT id FROM agent_tasks WHERE taskType = 'boot-triage'",
458
+ );
459
+ expect(previous).toBeDefined();
460
+ await getDbClient().run("UPDATE agent_tasks SET status = 'superseded' WHERE id = ?", [
461
+ previous!.id,
462
+ ]);
463
+
464
+ await createBootTriageTask();
465
+
466
+ const tasks = await getDbClient().query<{ id: string; status: string }>(
467
+ "SELECT id, status FROM agent_tasks WHERE taskType = 'boot-triage'",
468
+ );
469
+ expect(tasks).toHaveLength(2);
470
+ expect(tasks.find((task) => task.id === previous!.id)?.status).toBe("superseded");
471
+ expect(tasks.find((task) => task.id !== previous!.id)?.status).toBe("pending");
472
+ });
473
+
430
474
  test("boot-triage has correct tags", async () => {
431
475
  await createAgent({ name: "lead", isLead: true, status: "idle" });
432
476
 
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Integration `*_DISABLE` flags parse identically via the shared env-flag
3
+ * parser (issue #1939).
4
+ *
5
+ * GITHUB_DISABLE, JIRA_DISABLE, LINEAR_DISABLE, AGENTMAIL_DISABLE and
6
+ * SLACK_DISABLE used to hand-roll `value === "true" || value === "1"`,
7
+ * silently ignoring "TRUE", "True" or " 1 " — while the config validator
8
+ * and `isEnvFlagEnabled` (already used by GITLAB_DISABLE /
9
+ * AZURE_DEVOPS_DISABLE) accept any case. Every read now goes through
10
+ * `isEnvFlagEnabled`, so these tests pin the shared-parser behavior at
11
+ * each integration's entry point.
12
+ */
13
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
14
+ import { initAgentMail, isAgentMailEnabled, resetAgentMail } from "../agentmail/app";
15
+ import { initGitHub, isGitHubEnabled, resetGitHub } from "../github/app";
16
+ import { isJiraEnabled, resetJira } from "../jira/app";
17
+ import { isLinearEnabled, resetLinear } from "../linear/app";
18
+ import { getSlackConfiguration } from "../slack/config";
19
+
20
+ const ENV_KEYS = [
21
+ "GITHUB_DISABLE",
22
+ "GITHUB_WEBHOOK_SECRET",
23
+ "JIRA_DISABLE",
24
+ "JIRA_CLIENT_ID",
25
+ "LINEAR_DISABLE",
26
+ "LINEAR_CLIENT_ID",
27
+ "AGENTMAIL_DISABLE",
28
+ "AGENTMAIL_WEBHOOK_SECRET",
29
+ ] as const;
30
+ const originalEnv = new Map(ENV_KEYS.map((key) => [key, process.env[key]]));
31
+
32
+ // Spellings the hand-rolled `=== "true" || === "1"` checks used to ignore.
33
+ const TRUTHY_SPELLINGS = ["true", "TRUE", "True", "1", " 1 ", " True "];
34
+ // Everything that must NOT disable the integration.
35
+ const NON_DISABLING = ["false", "FALSE", "0", " 0 ", "", undefined];
36
+
37
+ beforeEach(() => {
38
+ for (const key of ENV_KEYS) delete process.env[key];
39
+ });
40
+
41
+ afterEach(() => {
42
+ resetGitHub();
43
+ resetAgentMail();
44
+ resetJira();
45
+ resetLinear();
46
+ for (const key of ENV_KEYS) {
47
+ const value = originalEnv.get(key);
48
+ if (value === undefined) delete process.env[key];
49
+ else process.env[key] = value;
50
+ }
51
+ });
52
+
53
+ describe("GitHub GITHUB_DISABLE", () => {
54
+ test("every truthy spelling disables the integration", () => {
55
+ process.env.GITHUB_WEBHOOK_SECRET = "secret";
56
+ for (const spelling of TRUTHY_SPELLINGS) {
57
+ process.env.GITHUB_DISABLE = spelling;
58
+ expect(isGitHubEnabled()).toBe(false);
59
+ }
60
+ });
61
+
62
+ test("non-disabling values keep the integration enabled", () => {
63
+ process.env.GITHUB_WEBHOOK_SECRET = "secret";
64
+ for (const value of NON_DISABLING) {
65
+ if (value === undefined) delete process.env.GITHUB_DISABLE;
66
+ else process.env.GITHUB_DISABLE = value;
67
+ expect(isGitHubEnabled()).toBe(true);
68
+ }
69
+ });
70
+
71
+ test("initGitHub honors TRUE even with credentials configured", () => {
72
+ process.env.GITHUB_WEBHOOK_SECRET = "secret";
73
+ process.env.GITHUB_DISABLE = "TRUE";
74
+ resetGitHub();
75
+ expect(initGitHub()).toBe(false);
76
+ });
77
+ });
78
+
79
+ describe("Jira JIRA_DISABLE", () => {
80
+ test("every truthy spelling disables the integration", () => {
81
+ process.env.JIRA_CLIENT_ID = "client-id";
82
+ for (const spelling of TRUTHY_SPELLINGS) {
83
+ process.env.JIRA_DISABLE = spelling;
84
+ expect(isJiraEnabled()).toBe(false);
85
+ }
86
+ });
87
+
88
+ test("non-disabling values keep the integration enabled", () => {
89
+ process.env.JIRA_CLIENT_ID = "client-id";
90
+ for (const value of NON_DISABLING) {
91
+ if (value === undefined) delete process.env.JIRA_DISABLE;
92
+ else process.env.JIRA_DISABLE = value;
93
+ expect(isJiraEnabled()).toBe(true);
94
+ }
95
+ });
96
+ });
97
+
98
+ describe("Linear LINEAR_DISABLE", () => {
99
+ test("every truthy spelling disables the integration", () => {
100
+ process.env.LINEAR_CLIENT_ID = "client-id";
101
+ for (const spelling of TRUTHY_SPELLINGS) {
102
+ process.env.LINEAR_DISABLE = spelling;
103
+ expect(isLinearEnabled()).toBe(false);
104
+ }
105
+ });
106
+
107
+ test("non-disabling values keep the integration enabled", () => {
108
+ process.env.LINEAR_CLIENT_ID = "client-id";
109
+ for (const value of NON_DISABLING) {
110
+ if (value === undefined) delete process.env.LINEAR_DISABLE;
111
+ else process.env.LINEAR_DISABLE = value;
112
+ expect(isLinearEnabled()).toBe(true);
113
+ }
114
+ });
115
+ });
116
+
117
+ describe("AgentMail AGENTMAIL_DISABLE", () => {
118
+ test("every truthy spelling disables the integration", () => {
119
+ process.env.AGENTMAIL_WEBHOOK_SECRET = "secret";
120
+ for (const spelling of TRUTHY_SPELLINGS) {
121
+ process.env.AGENTMAIL_DISABLE = spelling;
122
+ expect(isAgentMailEnabled()).toBe(false);
123
+ }
124
+ });
125
+
126
+ test("non-disabling values keep the integration enabled", () => {
127
+ process.env.AGENTMAIL_WEBHOOK_SECRET = "secret";
128
+ for (const value of NON_DISABLING) {
129
+ if (value === undefined) delete process.env.AGENTMAIL_DISABLE;
130
+ else process.env.AGENTMAIL_DISABLE = value;
131
+ expect(isAgentMailEnabled()).toBe(true);
132
+ }
133
+ });
134
+
135
+ test("initAgentMail honors TRUE even with credentials configured", () => {
136
+ process.env.AGENTMAIL_WEBHOOK_SECRET = "secret";
137
+ process.env.AGENTMAIL_DISABLE = "TRUE";
138
+ resetAgentMail();
139
+ expect(initAgentMail()).toBe(false);
140
+ });
141
+ });
142
+
143
+ describe("Slack SLACK_DISABLE", () => {
144
+ test("every truthy spelling reports disabled", () => {
145
+ for (const spelling of TRUTHY_SPELLINGS) {
146
+ expect(getSlackConfiguration({ SLACK_DISABLE: spelling }).disabled).toBe(true);
147
+ }
148
+ });
149
+
150
+ test("non-disabling values report enabled", () => {
151
+ for (const value of NON_DISABLING) {
152
+ const env = value === undefined ? {} : { SLACK_DISABLE: value };
153
+ expect(getSlackConfiguration(env).disabled).toBe(false);
154
+ }
155
+ });
156
+ });
@@ -246,6 +246,40 @@ describe("/api/kv REST — explicit namespace shape", () => {
246
246
  expect((await get.json()).value).toBe("hi");
247
247
  });
248
248
 
249
+ test("GET with path/offset/limit returns a view of a JSON string value", async () => {
250
+ const ns = "swarm:test:view";
251
+ const stored = JSON.stringify({ outcome: { data: { rows: [10, 11, 12, 13, 14] } } });
252
+ const put = await authedFetch(`/api/kv/_/${encodeURIComponent(ns)}/spill`, {
253
+ method: "PUT",
254
+ body: JSON.stringify({ value: stored, valueType: "string" }),
255
+ agentId,
256
+ });
257
+ expect(put.status).toBe(200);
258
+ const base = `/api/kv/_/${encodeURIComponent(ns)}/spill`;
259
+
260
+ const page = await authedFetch(`${base}?path=outcome.data.rows&offset=1&limit=2`, { agentId });
261
+ expect(page.status).toBe(200);
262
+ const body = await page.json();
263
+ expect(body.value).toEqual([11, 12]);
264
+ expect(body.view).toEqual({
265
+ path: "outcome.data.rows",
266
+ type: "array",
267
+ total: 5,
268
+ offset: 1,
269
+ returned: 2,
270
+ nextOffset: 3,
271
+ });
272
+
273
+ const whole = await authedFetch(base, { agentId });
274
+ const wholeBody = await whole.json();
275
+ expect(wholeBody.value).toBe(stored);
276
+ expect(wholeBody).not.toHaveProperty("view");
277
+
278
+ const missing = await authedFetch(`${base}?path=outcome.nope`, { agentId });
279
+ expect(missing.status).toBe(400);
280
+ expect((await missing.json()).error).toContain('no key "nope"');
281
+ });
282
+
249
283
  test("list with explicit namespace", async () => {
250
284
  const ns = "swarm:test:explicit-list";
251
285
  await authedFetch(`/api/kv/_/${encodeURIComponent(ns)}/k`, {
@@ -15,6 +15,7 @@ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:tes
15
15
  import { unlink } from "node:fs/promises";
16
16
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
17
17
  import { closeDb, createAgent, getDbClient, initDb } from "../be/db";
18
+ import { summarizeKvShape } from "../kv-view";
18
19
  import {
19
20
  registerKvDeleteTool,
20
21
  registerKvGetTool,
@@ -22,7 +23,12 @@ import {
22
23
  registerKvListTool,
23
24
  registerKvSetTool,
24
25
  } from "../tools/kv";
25
- import { finalizeSwarmToolResult, mcpOverflowNamespace } from "../tools/utils";
26
+ import {
27
+ finalizeSwarmToolResult,
28
+ MCP_RESULT_WIRE_LIMIT_BYTES,
29
+ mcpOverflowNamespace,
30
+ wireChannelBytes,
31
+ } from "../tools/utils";
26
32
 
27
33
  const TEST_DB_PATH = "./test-kv-tool.sqlite";
28
34
 
@@ -172,6 +178,273 @@ describe("kv MCP tools", () => {
172
178
  expect(intruderList.structuredContent.entries).toBeUndefined();
173
179
  });
174
180
 
181
+ type ViewResult = {
182
+ content: Array<{ type: string; text: string }>;
183
+ structuredContent: {
184
+ success: boolean;
185
+ message: string;
186
+ details?: string;
187
+ entry?: { value?: unknown; key?: string };
188
+ view?: {
189
+ path: string;
190
+ type: string;
191
+ total?: number;
192
+ offset?: number;
193
+ returned?: number;
194
+ nextOffset?: number | null;
195
+ };
196
+ };
197
+ };
198
+
199
+ test("kv-get path/offset/limit follows the spill hint to a bounded, pageable slice", async () => {
200
+ const tools = buildServer();
201
+ const rows = Array.from({ length: 400 }, (_, id) => ({ id, note: "r".repeat(100) }));
202
+ const spill = await finalizeSwarmToolResult(
203
+ "script-run",
204
+ {
205
+ ok: true,
206
+ message: "Script run completed.",
207
+ details: `result:\n${JSON.stringify({ rows }, null, 2)}`,
208
+ data: { status: 200, data: { result: { rows } } },
209
+ },
210
+ { agentId: agentA },
211
+ );
212
+ const truncation = (
213
+ spill.structuredContent as {
214
+ truncation: { retrieval: string; shape: Array<{ path: string; items?: number }> };
215
+ }
216
+ ).truncation;
217
+ expect(truncation.shape).toContainEqual(
218
+ expect.objectContaining({ path: "outcome.data.data.result.rows", type: "array", items: 400 }),
219
+ );
220
+ // The hint is a literal, copy-pasteable call.
221
+ const hinted = JSON.parse(
222
+ truncation.retrieval.slice("kv-get(".length, truncation.retrieval.indexOf(") returns")),
223
+ ) as { namespace: string; key: string; path: string; offset: number };
224
+ expect(hinted.path).toBe("outcome.data.data.result.rows");
225
+
226
+ const first = (await tools.get.handler(hinted, meta(agentA))) as ViewResult;
227
+ const view = first.structuredContent.view!;
228
+ expect(first.structuredContent.success).toBe(true);
229
+ expect(view).toMatchObject({
230
+ path: "outcome.data.data.result.rows",
231
+ type: "array",
232
+ total: 400,
233
+ });
234
+ expect(view.returned).toBeGreaterThan(0);
235
+ expect(view.returned).toBeLessThan(400);
236
+ expect(view.nextOffset).toBe(view.returned);
237
+ expect(JSON.parse(first.structuredContent.details!)).toEqual(rows.slice(0, view.returned));
238
+ expect(first.structuredContent.entry).not.toHaveProperty("value");
239
+ expect(first.structuredContent.message).toContain("continue at view.nextOffset");
240
+ expect(wireChannelBytes(first)).toBeLessThanOrEqual(MCP_RESULT_WIRE_LIMIT_BYTES);
241
+
242
+ const next = (await tools.get.handler(
243
+ { ...hinted, offset: view.nextOffset!, limit: 5 },
244
+ meta(agentA),
245
+ )) as ViewResult;
246
+ expect(next.structuredContent.view).toMatchObject({ offset: view.nextOffset, returned: 5 });
247
+ expect(JSON.parse(next.structuredContent.details!)).toEqual(
248
+ rows.slice(view.nextOffset!, view.nextOffset! + 5),
249
+ );
250
+
251
+ const pastEnd = (await tools.get.handler(
252
+ { ...hinted, offset: 10_000 },
253
+ meta(agentA),
254
+ )) as ViewResult;
255
+ expect(pastEnd.structuredContent.success).toBe(true);
256
+ expect(pastEnd.structuredContent.view).toMatchObject({
257
+ total: 400,
258
+ returned: 0,
259
+ nextOffset: null,
260
+ });
261
+ expect(pastEnd.structuredContent.details).toBe("[]");
262
+
263
+ const missing = (await tools.get.handler(
264
+ { ...hinted, path: "outcome.data.nope" },
265
+ meta(agentA),
266
+ )) as ViewResult;
267
+ expect(missing.structuredContent.success).toBe(false);
268
+ expect(missing.structuredContent.message).toContain('no key "nope" under "outcome.data"');
269
+
270
+ const badIndex = (await tools.get.handler(
271
+ { ...hinted, path: "outcome.data.data.result.rows.400" },
272
+ meta(agentA),
273
+ )) as ViewResult;
274
+ expect(badIndex.structuredContent.success).toBe(false);
275
+ expect(badIndex.structuredContent.message).toContain("array of 400");
276
+
277
+ const oneRow = (await tools.get.handler(
278
+ { ...hinted, path: "outcome.data.data.result.rows.7" },
279
+ meta(agentA),
280
+ )) as ViewResult;
281
+ expect(JSON.parse(oneRow.structuredContent.details!)).toEqual(rows[7]);
282
+ });
283
+
284
+ test("a wide-object spill hint pages the parent of its scalar leaves", async () => {
285
+ const tools = buildServer();
286
+ const wide = Object.fromEntries(
287
+ Array.from({ length: 2_000 }, (_, i) => [`k${i}`, 1_000_000_000.5 + i]),
288
+ );
289
+ const spill = await finalizeSwarmToolResult(
290
+ "wide-tool",
291
+ { ok: true, message: "Done.", data: wide },
292
+ { agentId: agentA },
293
+ );
294
+ const { retrieval, shape } = (
295
+ spill.structuredContent as {
296
+ truncation: { retrieval: string; shape: Array<{ path: string; type: string }> };
297
+ }
298
+ ).truncation;
299
+ expect(shape[0]).toMatchObject({ type: "number" });
300
+ const hinted = JSON.parse(retrieval.slice("kv-get(".length, retrieval.indexOf(") returns")));
301
+ expect(hinted).toMatchObject({ path: "outcome.data", offset: 0 });
302
+
303
+ const page = (await tools.get.handler(hinted, meta(agentA))) as ViewResult;
304
+ expect(page.structuredContent.success).toBe(true);
305
+ expect(page.structuredContent.view).toMatchObject({ type: "object", total: 2_000 });
306
+ expect(page.structuredContent.view!.returned).toBeGreaterThan(0);
307
+ });
308
+
309
+ test("kv-get view on a plain string pages characters and rejects a JSON path", async () => {
310
+ const tools = buildServer();
311
+ await tools.set.handler(
312
+ { key: "plain", value: "hello world, not json", valueType: "string" },
313
+ meta(agentA),
314
+ );
315
+ const chars = (await tools.get.handler(
316
+ { key: "plain", offset: 6, limit: 5 },
317
+ meta(agentA),
318
+ )) as ViewResult;
319
+ expect(chars.structuredContent.success).toBe(true);
320
+ expect(chars.structuredContent.details).toBe('"world"');
321
+ expect(chars.structuredContent.view).toMatchObject({
322
+ type: "string",
323
+ total: 21,
324
+ returned: 5,
325
+ nextOffset: 11,
326
+ });
327
+
328
+ await tools.set.handler(
329
+ { key: "spaced", value: "a \n b", valueType: "string" },
330
+ meta(agentA),
331
+ );
332
+ for (const [offset, slice] of [
333
+ [1, " \n "],
334
+ [0, "a "],
335
+ ] as const) {
336
+ const exact = (await tools.get.handler(
337
+ { key: "spaced", offset, limit: slice.length },
338
+ meta(agentA),
339
+ )) as ViewResult;
340
+ expect(JSON.parse(exact.structuredContent.details!)).toBe(slice);
341
+ expect(JSON.parse(exact.content[0]!.text.split("\n\n").at(-1)!)).toBe(slice);
342
+ }
343
+
344
+ const pathed = (await tools.get.handler(
345
+ { key: "plain", path: "a" },
346
+ meta(agentA),
347
+ )) as ViewResult;
348
+ expect(pathed.structuredContent.success).toBe(false);
349
+ expect(pathed.structuredContent.message).toContain("plain string");
350
+
351
+ await tools.set.handler({ key: "num", value: 42 }, meta(agentA));
352
+ const scalar = (await tools.get.handler({ key: "num", offset: 1 }, meta(agentA))) as ViewResult;
353
+ expect(scalar.structuredContent.success).toBe(false);
354
+ expect(scalar.structuredContent.message).toContain("is a number");
355
+ });
356
+
357
+ test("kv-get view names a narrower path when one item alone exceeds the cap", async () => {
358
+ const tools = buildServer();
359
+ await tools.set.handler(
360
+ { key: "fat-items", value: { items: [{ blob: "f".repeat(20_000) }, { id: 2 }] } },
361
+ meta(agentA),
362
+ );
363
+ const res = (await tools.get.handler(
364
+ { key: "fat-items", path: "items" },
365
+ meta(agentA),
366
+ )) as ViewResult;
367
+ expect(res.structuredContent.success).toBe(true);
368
+ expect(res.structuredContent.view).toMatchObject({ returned: 0, nextOffset: 0, total: 2 });
369
+ expect(res.structuredContent.message).toContain('narrow the path to "items.0"');
370
+ expect(wireChannelBytes(res)).toBeLessThanOrEqual(MCP_RESULT_WIRE_LIMIT_BYTES);
371
+
372
+ const narrowed = (await tools.get.handler(
373
+ { key: "fat-items", path: "items.0.blob", limit: 100 },
374
+ meta(agentA),
375
+ )) as ViewResult;
376
+ expect(JSON.parse(narrowed.structuredContent.details!)).toBe("f".repeat(100));
377
+ });
378
+
379
+ test("a shrunk kv-get page counts its note, so both channels stay within the cap", async () => {
380
+ const tools = buildServer();
381
+ const channelBytes = (res: ViewResult) => ({
382
+ text: Buffer.byteLength(res.content.map((part) => part.text).join(""), "utf8"),
383
+ structured: Buffer.byteLength(JSON.stringify(res.structuredContent), "utf8"),
384
+ });
385
+ const longKey = "k".repeat(900);
386
+ await tools.set.handler(
387
+ {
388
+ key: "near-cap",
389
+ value: {
390
+ text: "a".repeat(20_000),
391
+ rows: Array.from({ length: 400 }, (_, id) => ({ id, note: "r".repeat(100) })),
392
+ [longKey]: [{ blob: "f".repeat(20_000) }],
393
+ },
394
+ },
395
+ meta(agentA),
396
+ );
397
+ await tools.set.handler(
398
+ { key: "dense", value: "x".repeat(20_000), valueType: "string" },
399
+ meta(agentA),
400
+ );
401
+ const cases = [
402
+ { key: "dense", args: { offset: 0 }, note: "Page shrunk" },
403
+ { args: { path: "text" }, note: "Page shrunk" },
404
+ { args: { path: "text", offset: 3 }, note: "Page shrunk" },
405
+ { args: { path: "rows" }, note: "Page shrunk" },
406
+ { args: { path: longKey }, note: `narrow the path to "${longKey}.0"` },
407
+ ];
408
+ for (const { key = "near-cap", args, note } of cases) {
409
+ const res = (await tools.get.handler({ key, ...args }, meta(agentA))) as ViewResult;
410
+ expect(res.structuredContent.success).toBe(true);
411
+ expect(res.structuredContent.message).toContain(note);
412
+ const bytes = channelBytes(res);
413
+ expect(bytes.text).toBeLessThanOrEqual(MCP_RESULT_WIRE_LIMIT_BYTES);
414
+ expect(bytes.structured).toBeLessThanOrEqual(MCP_RESULT_WIRE_LIMIT_BYTES);
415
+ }
416
+ });
417
+
418
+ test("every spill shape path, long or empty-keyed, works as a kv-get path", async () => {
419
+ const tools = buildServer();
420
+ const rows = (n: number) =>
421
+ Array.from({ length: n }, (_, id) => ({ id, note: "r".repeat(60) }));
422
+ const longKey = "L".repeat(200);
423
+ const values = {
424
+ long: { [longKey]: rows(80), small: 1 },
425
+ emptyRoot: { "": rows(80), small: 1 },
426
+ emptyNested: { outer: { "": rows(80), side: rows(40) }, small: 1 },
427
+ overMax: { ["X".repeat(1_100)]: rows(80), small: 1 },
428
+ };
429
+ for (const [key, value] of Object.entries(values)) {
430
+ await tools.set.handler({ key, value: JSON.stringify(value) }, meta(agentA));
431
+ const shape = summarizeKvShape(value);
432
+ expect(shape.length).toBeGreaterThan(0);
433
+ for (const entry of shape) {
434
+ const res = (await tools.get.handler(
435
+ { key, path: entry.path, ...(entry.items === undefined ? {} : { offset: 0 }) },
436
+ meta(agentA),
437
+ )) as ViewResult;
438
+ expect(res.structuredContent.success).toBe(true);
439
+ expect(res.structuredContent.view).toMatchObject({ path: entry.path, type: entry.type });
440
+ if (entry.items !== undefined) {
441
+ expect(res.structuredContent.view!.total).toBe(entry.items);
442
+ }
443
+ }
444
+ if (key === "long") expect(shape[0]!.path).toBe(longKey);
445
+ }
446
+ });
447
+
175
448
  test("kv-incr creates + increments + reports value", async () => {
176
449
  const tools = buildServer();
177
450
  const r1 = (await tools.incr.handler({ key: "ctr", by: 5 }, meta(agentA))) as StructuredResult<{