@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
@@ -0,0 +1,147 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { resolveKvView, sliceKvTarget, summarizeKvShape } from "../kv-view";
3
+
4
+ describe("resolveKvView", () => {
5
+ const stored = JSON.stringify({ a: { rows: [{ id: 0 }, { id: 1 }, { id: 2 }] }, s: "abcdef" });
6
+
7
+ test("a JSON string resolves dot paths, numeric segments index arrays", () => {
8
+ expect(resolveKvView(stored, { path: "a.rows.1.id" })).toEqual({
9
+ ok: true,
10
+ value: 1,
11
+ view: { path: "a.rows.1.id", type: "number" },
12
+ });
13
+ expect(resolveKvView(stored, { path: "a.rows", offset: 1, limit: 1 })).toMatchObject({
14
+ ok: true,
15
+ value: [{ id: 1 }],
16
+ view: { total: 3, offset: 1, returned: 1, nextOffset: 2 },
17
+ });
18
+ });
19
+
20
+ test("objects page by key, strings by character", () => {
21
+ expect(resolveKvView({ x: 1, y: 2, z: 3 }, { offset: 1, limit: 1 })).toMatchObject({
22
+ value: { y: 2 },
23
+ view: { type: "object", total: 3, nextOffset: 2 },
24
+ });
25
+ expect(resolveKvView(stored, { path: "s", offset: 2, limit: 3 })).toMatchObject({
26
+ value: "cde",
27
+ view: { type: "string", returned: 3, nextOffset: 5 },
28
+ });
29
+ });
30
+
31
+ test("out-of-range offsets return an empty page with the real total", () => {
32
+ expect(resolveKvView(stored, { path: "a.rows", offset: 99 })).toMatchObject({
33
+ ok: true,
34
+ value: [],
35
+ view: { total: 3, offset: 99, returned: 0, nextOffset: null },
36
+ });
37
+ });
38
+
39
+ test("missing keys, bad indices, empty segments and scalar paging are errors", () => {
40
+ expect(resolveKvView(stored, { path: "a.nope" })).toEqual({
41
+ ok: false,
42
+ error: 'path "a.nope" not found: no key "nope" under "a"',
43
+ });
44
+ expect(resolveKvView(stored, { path: "a.rows.3" })).toMatchObject({ ok: false });
45
+ expect(resolveKvView(stored, { path: "a.rows.-1" })).toMatchObject({ ok: false });
46
+ expect(resolveKvView(stored, { path: "a..rows" })).toMatchObject({ ok: false });
47
+ expect(resolveKvView(stored, { path: "a.rows.0.id.x" })).toMatchObject({ ok: false });
48
+ expect(resolveKvView(42, { offset: 1 })).toMatchObject({ ok: false });
49
+ });
50
+
51
+ test("a non-JSON string rejects a path but pages its characters", () => {
52
+ expect(resolveKvView("plain {not json", { path: "a" })).toMatchObject({ ok: false });
53
+ expect(resolveKvView("plain {not json", { offset: 6, limit: 5 })).toMatchObject({
54
+ ok: true,
55
+ value: "{not ",
56
+ });
57
+ // A string holding a JSON scalar stays a plain string.
58
+ expect(resolveKvView('"quoted"', { offset: 1, limit: 6 })).toMatchObject({ value: "quoted" });
59
+ });
60
+
61
+ test("a string page never ends on a lone high surrogate", () => {
62
+ const sliced = sliceKvTarget("a🙂b", 0, 2);
63
+ expect(sliced).toMatchObject({ value: "a", returned: 1, nextOffset: 1 });
64
+ });
65
+
66
+ test("limit 1 on an astral character returns the whole pair and advances", () => {
67
+ const text = "🙂🙃x";
68
+ const seen: string[] = [];
69
+ let offset: number | null = 0;
70
+ while (offset !== null && seen.length < 10) {
71
+ const page = sliceKvTarget(text, offset, 1)!;
72
+ expect(page.returned).toBeGreaterThan(0);
73
+ seen.push(page.value as string);
74
+ offset = page.nextOffset;
75
+ }
76
+ expect(seen).toEqual(["🙂", "🙃", "x"]);
77
+ });
78
+ });
79
+
80
+ describe("summarizeKvShape", () => {
81
+ test("expands the dominant object down to where the bulk lives", () => {
82
+ const rows = Array.from({ length: 300 }, (_, id) => ({ id, note: "x".repeat(50) }));
83
+ const shape = summarizeKvShape({
84
+ version: 1,
85
+ toolName: "script-run",
86
+ outcome: { ok: true, message: "done", data: { status: 200, data: { result: { rows } } } },
87
+ });
88
+ expect(shape[0]).toMatchObject({
89
+ path: "outcome.data.data.result.rows",
90
+ type: "array",
91
+ items: 300,
92
+ });
93
+ expect(shape[0]!.bytes).toBe(Buffer.byteLength(JSON.stringify(rows)));
94
+ // Crumbs under 1% of the value are left out.
95
+ expect(shape.map((entry) => entry.path)).not.toContain("version");
96
+ });
97
+
98
+ test("escapes keys with dots so every shape path resolves", () => {
99
+ const rows = Array.from({ length: 50 }, (_, id) => ({ id, note: "x".repeat(50) }));
100
+ const value = { "rows.v2": rows, "a\\b": { "c.d": "y".repeat(2_000) } };
101
+ const shape = summarizeKvShape(value);
102
+ expect(shape.map((entry) => entry.path)).toEqual(["rows\\.v2", "a\\\\b.c\\.d"]);
103
+ for (const entry of shape) {
104
+ expect(resolveKvView(value, { path: entry.path, offset: 0 })).toMatchObject({
105
+ ok: true,
106
+ view: { path: entry.path, type: entry.type, total: entry.items },
107
+ });
108
+ }
109
+ });
110
+
111
+ test("keeps long paths exact and replaces unaddressable ones with their nearest ancestor", () => {
112
+ const rows = Array.from({ length: 80 }, (_, id) => ({ id, note: "x".repeat(60) }));
113
+ const longKey = "L".repeat(200);
114
+ expect(summarizeKvShape({ [longKey]: rows })[0]).toMatchObject({ path: longKey, items: 80 });
115
+ // An empty key has no path syntax: its parent object stands in.
116
+ expect(summarizeKvShape({ "": rows, small: 1 })[0]).toMatchObject({
117
+ path: "",
118
+ type: "object",
119
+ items: 2,
120
+ });
121
+ expect(summarizeKvShape({ outer: { "": rows, side: [1] } })[0]).toMatchObject({
122
+ path: "outer",
123
+ type: "object",
124
+ });
125
+ // Longer than kv-get's path limit: the root stands in.
126
+ expect(summarizeKvShape({ ["X".repeat(1_100)]: rows })[0]).toMatchObject({ path: "" });
127
+ });
128
+
129
+ test("is bounded by total path bytes", () => {
130
+ const value = Object.fromEntries(
131
+ Array.from({ length: 8 }, (_, i) => [`${i}`.repeat(600), "v".repeat(1_000)]),
132
+ );
133
+ const shape = summarizeKvShape(value);
134
+ const pathBytes = shape.reduce((sum, entry) => sum + JSON.stringify(entry.path).length, 0);
135
+ expect(pathBytes).toBeLessThanOrEqual(2_048);
136
+ expect(shape.filter((entry) => entry.path.length === 600)).toHaveLength(3);
137
+ expect(shape.at(-1)).toMatchObject({ path: "", type: "object", items: 8 });
138
+ });
139
+
140
+ test("is bounded to maxEntries", () => {
141
+ const wide = Object.fromEntries(
142
+ Array.from({ length: 50 }, (_, i) => [`k${i}`, "v".repeat(100)]),
143
+ );
144
+ expect(summarizeKvShape(wide)).toHaveLength(8);
145
+ expect(summarizeKvShape(wide, 3)).toHaveLength(3);
146
+ });
147
+ });
@@ -9,17 +9,20 @@ import {
9
9
  import type { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
10
10
  import {
11
11
  closeDb,
12
+ completeTask,
12
13
  createAgent,
13
14
  createTaskExtended,
14
15
  createUser,
15
16
  getDbClient,
16
17
  getTaskById,
17
18
  initDb,
19
+ startTask,
18
20
  } from "../be/db";
19
21
  import { type IdentityActor, mintToken, revokeToken } from "../be/users";
20
22
  import { handleCore } from "../http/core";
21
23
  import { handleMcp } from "../http/mcp";
22
24
  import { handleMcpUser } from "../http/mcp-user";
25
+ import { handlePoll } from "../http/poll";
23
26
  import { listenOnFreePort } from "./test-net";
24
27
 
25
28
  const TEST_DB_PATH = "./test-mcp-user-route.sqlite";
@@ -80,6 +83,7 @@ beforeEach(async () => {
80
83
  await client.run("DELETE FROM user_tokens");
81
84
  await client.run("DELETE FROM agent_tasks");
82
85
  await client.run("DELETE FROM users");
86
+ await client.run("DELETE FROM agents");
83
87
  });
84
88
 
85
89
  function endpoint(path = "/mcp-user"): string {
@@ -125,6 +129,32 @@ async function mcpPost(
125
129
  return { response, payload, text };
126
130
  }
127
131
 
132
+ async function callPoll(agentId: string): Promise<{
133
+ status: number;
134
+ body: { trigger: { type: string; [key: string]: unknown } | null } | { error: string };
135
+ }> {
136
+ let status = 200;
137
+ let bodyStr = "";
138
+ const req = {
139
+ method: "GET",
140
+ url: "/api/poll",
141
+ headers: { "x-agent-id": agentId },
142
+ } as unknown as Parameters<typeof handlePoll>[0];
143
+ const res = {
144
+ setHeader() {},
145
+ writeHead(code: number) {
146
+ status = code;
147
+ },
148
+ end(body?: string) {
149
+ bodyStr = body ?? "";
150
+ },
151
+ } as unknown as Parameters<typeof handlePoll>[1];
152
+
153
+ const handled = await handlePoll(req, res, ["api", "poll"], new URLSearchParams(), agentId);
154
+ if (!handled) throw new Error("handlePoll did not handle the request");
155
+ return { status, body: bodyStr ? JSON.parse(bodyStr) : { trigger: null } };
156
+ }
157
+
128
158
  async function initialize(
129
159
  token: string,
130
160
  path = "/mcp-user",
@@ -237,7 +267,7 @@ describe("/mcp-user auth and tool surface", () => {
237
267
  expect(response.status).toBe(401);
238
268
  });
239
269
 
240
- test("valid active-user token initializes and tools/list returns exactly the 6 task tools", async () => {
270
+ test("valid active-user token initializes and exposes the narrow send-task schema", async () => {
241
271
  const user = await createUser({ name: "Active User" });
242
272
  const token = (await mintToken(user.id, "active", ACTOR)).plaintext;
243
273
  const sessionId = await initialize(token);
@@ -250,23 +280,106 @@ describe("/mcp-user auth and tool surface", () => {
250
280
  );
251
281
 
252
282
  expect(response.status).toBe(200);
253
- const result = payload as { result: { tools: Array<{ name: string }> } };
283
+ const result = payload as {
284
+ result: {
285
+ tools: Array<{ name: string; inputSchema: { properties?: Record<string, unknown> } }>;
286
+ };
287
+ };
254
288
  const names = result.result.tools.map((tool) => tool.name).sort();
255
289
  expect(names).toEqual(
256
- [
257
- "cancel-task",
258
- "get-task-details",
259
- "get-tasks",
260
- "send-task",
261
- "steer-task",
262
- "task-action",
263
- ].sort(),
290
+ ["cancel-task", "get-task-details", "get-tasks", "send-task", "steer-task"].sort(),
291
+ );
292
+ const sendTask = result.result.tools.find((tool) => tool.name === "send-task");
293
+ expect(Object.keys(sendTask?.inputSchema.properties ?? {}).sort()).toEqual(
294
+ ["model", "modelTier", "outputSchema", "priority", "tags", "task", "taskType"].sort(),
264
295
  );
265
296
  });
266
297
 
267
- test("send-task over /mcp-user records requestedByUserId and get-tasks returns only that user's tasks", async () => {
298
+ test("send-task assigns user work to the Lead with internal routing proof", async () => {
299
+ const lead = await createAgent({
300
+ id: "10000000-0000-4000-8000-000000000001",
301
+ name: "User MCP Lead",
302
+ isLead: true,
303
+ status: "idle",
304
+ });
305
+ const user = await createUser({ name: "Lead Requester" });
306
+ const token = (await mintToken(user.id, "lead-route", ACTOR)).plaintext;
307
+ const sessionId = await initialize(token);
308
+ await notifyInitialized(token, sessionId);
309
+
310
+ const { response, payload } = await mcpPost(
311
+ token,
312
+ {
313
+ jsonrpc: "2.0",
314
+ id: 3,
315
+ method: "tools/call",
316
+ params: { name: "send-task", arguments: { task: "route this through the lead" } },
317
+ },
318
+ sessionId,
319
+ );
320
+
321
+ expect(response.status).toBe(200);
322
+ const result = payload as { result: { structuredContent: { task: { id: string } } } };
323
+ const task = await getTaskById(result.result.structuredContent.task.id);
324
+ expect(task).toMatchObject({
325
+ status: "pending",
326
+ agentId: lead.id,
327
+ requestedByUserId: user.id,
328
+ routingReason: "skill",
329
+ routingSource: "engine_default",
330
+ routingNote: "User MCP ingress assigns new work to the Lead for delegation.",
331
+ });
332
+ expect(task?.offeredTo).toBeUndefined();
333
+ expect(task?.routingAffinity).toBeUndefined();
334
+ });
335
+
336
+ test("send-task queues for a busy Lead and poll starts it after capacity returns", async () => {
337
+ const lead = await createAgent({
338
+ id: "10000000-0000-4000-8000-000000000002",
339
+ name: "Busy User MCP Lead",
340
+ isLead: true,
341
+ status: "idle",
342
+ maxTasks: 1,
343
+ });
344
+ const activeTask = await createTaskExtended("existing lead work", { agentId: lead.id });
345
+ expect((await startTask(activeTask.id))?.status).toBe("in_progress");
346
+ const user = await createUser({ name: "Busy Lead Requester" });
347
+ const token = (await mintToken(user.id, "busy-lead-route", ACTOR)).plaintext;
348
+ const sessionId = await initialize(token);
349
+ await notifyInitialized(token, sessionId);
350
+
351
+ const { payload } = await mcpPost(
352
+ token,
353
+ {
354
+ jsonrpc: "2.0",
355
+ id: 3,
356
+ method: "tools/call",
357
+ params: { name: "send-task", arguments: { task: "wait for the busy lead" } },
358
+ },
359
+ sessionId,
360
+ );
361
+
362
+ const result = payload as { result: { structuredContent: { task: { id: string } } } };
363
+ const task = await getTaskById(result.result.structuredContent.task.id);
364
+ expect(task).toMatchObject({ status: "pending", agentId: lead.id });
365
+
366
+ const blockedPoll = await callPoll(lead.id);
367
+ expect(blockedPoll.status).toBe(200);
368
+ if ("error" in blockedPoll.body) throw new Error("unexpected poll error");
369
+ expect(blockedPoll.body.trigger).toBeNull();
370
+ expect((await getTaskById(task!.id))?.status).toBe("pending");
371
+
372
+ await completeTask(activeTask.id, "capacity returned");
373
+ const readyPoll = await callPoll(lead.id);
374
+ expect(readyPoll.status).toBe(200);
375
+ if ("error" in readyPoll.body) throw new Error("unexpected poll error");
376
+ expect(readyPoll.body.trigger?.type).toBe("task_assigned");
377
+ expect((readyPoll.body.trigger as { taskId: string }).taskId).toBe(task?.id);
378
+ expect((await getTaskById(task!.id))?.status).toBe("in_progress");
379
+ });
380
+
381
+ test("send-task refuses with no Lead and creates no task", async () => {
268
382
  const user = await createUser({ name: "Task Requester" });
269
- const otherUser = await createUser({ name: "Other Task Requester" });
270
383
  const token = (await mintToken(user.id, "task", ACTOR)).plaintext;
271
384
  const sessionId = await initialize(token);
272
385
  await notifyInitialized(token, sessionId);
@@ -283,9 +396,81 @@ describe("/mcp-user auth and tool surface", () => {
283
396
  );
284
397
 
285
398
  expect(response.status).toBe(200);
286
- const result = payload as { result: { structuredContent: { task: { id: string } } } };
287
- const taskId = result.result.structuredContent.task.id;
288
- expect((await getTaskById(taskId))?.requestedByUserId).toBe(user.id);
399
+ const result = payload as {
400
+ result: { isError?: boolean; structuredContent: { success: boolean; message: string } };
401
+ };
402
+ expect(result.result.isError).toBe(true);
403
+ expect(result.result.structuredContent).toMatchObject({
404
+ success: false,
405
+ message: "No online Lead is available. Start or register a Lead before sending a task.",
406
+ });
407
+ expect(
408
+ (await getDbClient().get<{ count: number }>("SELECT COUNT(*) AS count FROM agent_tasks"))
409
+ ?.count,
410
+ ).toBe(0);
411
+ });
412
+
413
+ test("send-task refuses with only an offline Lead and creates no task", async () => {
414
+ await createAgent({
415
+ id: "10000000-0000-4000-8000-000000000004",
416
+ name: "Offline User MCP Lead",
417
+ isLead: true,
418
+ status: "offline",
419
+ });
420
+ const user = await createUser({ name: "Offline Lead Requester" });
421
+ const token = (await mintToken(user.id, "offline-lead", ACTOR)).plaintext;
422
+ const sessionId = await initialize(token);
423
+ await notifyInitialized(token, sessionId);
424
+
425
+ const { payload } = await mcpPost(
426
+ token,
427
+ {
428
+ jsonrpc: "2.0",
429
+ id: 3,
430
+ method: "tools/call",
431
+ params: { name: "send-task", arguments: { task: "do not queue without a Lead" } },
432
+ },
433
+ sessionId,
434
+ );
435
+
436
+ const result = payload as {
437
+ result: { isError?: boolean; structuredContent: { success: boolean; message: string } };
438
+ };
439
+ expect(result.result.isError).toBe(true);
440
+ expect(result.result.structuredContent.success).toBe(false);
441
+ expect(
442
+ (await getDbClient().get<{ count: number }>("SELECT COUNT(*) AS count FROM agent_tasks"))
443
+ ?.count,
444
+ ).toBe(0);
445
+ });
446
+
447
+ test("send-task preserves requester ownership and get-tasks filters foreign work", async () => {
448
+ const lead = await createAgent({ name: "Ownership Lead", isLead: true, status: "idle" });
449
+ const user = await createUser({ name: "Task Requester" });
450
+ const otherUser = await createUser({ name: "Other Task Requester" });
451
+ const token = (await mintToken(user.id, "task-ownership", ACTOR)).plaintext;
452
+ const sessionId = await initialize(token);
453
+ await notifyInitialized(token, sessionId);
454
+
455
+ const sent = await mcpPost(
456
+ token,
457
+ {
458
+ jsonrpc: "2.0",
459
+ id: 3,
460
+ method: "tools/call",
461
+ params: { name: "send-task", arguments: { task: "owned user mcp task" } },
462
+ },
463
+ sessionId,
464
+ );
465
+ const sentResult = sent.payload as {
466
+ result: { structuredContent: { task: { id: string } } };
467
+ };
468
+ const taskId = sentResult.result.structuredContent.task.id;
469
+ expect(await getTaskById(taskId)).toMatchObject({
470
+ status: "pending",
471
+ agentId: lead.id,
472
+ requestedByUserId: user.id,
473
+ });
289
474
  const foreignTask = await createTaskExtended("foreign user mcp task", {
290
475
  requestedByUserId: otherUser.id,
291
476
  });
@@ -312,6 +497,63 @@ describe("/mcp-user auth and tool surface", () => {
312
497
  expect(listResult.result.structuredContent.tasks).toHaveLength(1);
313
498
  });
314
499
 
500
+ test("send-task ignores injected routing fields and keeps internal routing authoritative", async () => {
501
+ const lead = await createAgent({
502
+ id: "10000000-0000-4000-8000-000000000003",
503
+ name: "Authoritative User MCP Lead",
504
+ isLead: true,
505
+ status: "idle",
506
+ });
507
+ const attacker = await createAgent({
508
+ id: "20000000-0000-4000-8000-000000000001",
509
+ name: "Injected Target",
510
+ isLead: false,
511
+ status: "idle",
512
+ });
513
+ const user = await createUser({ name: "Injection Requester" });
514
+ const foreignUser = await createUser({ name: "Foreign Requester" });
515
+ const token = (await mintToken(user.id, "routing-injection", ACTOR)).plaintext;
516
+ const sessionId = await initialize(token);
517
+ await notifyInitialized(token, sessionId);
518
+
519
+ const { response, payload } = await mcpPost(
520
+ token,
521
+ {
522
+ jsonrpc: "2.0",
523
+ id: 3,
524
+ method: "tools/call",
525
+ params: {
526
+ name: "send-task",
527
+ arguments: {
528
+ task: "keep server routing authoritative",
529
+ agentId: attacker.id,
530
+ routingReason: "human_pinned",
531
+ routingNote: "Injected routing note must not survive.",
532
+ offerMode: false,
533
+ leadOnly: true,
534
+ allowDuplicate: true,
535
+ requestedByUserId: foreignUser.id,
536
+ },
537
+ },
538
+ },
539
+ sessionId,
540
+ );
541
+
542
+ expect(response.status).toBe(200);
543
+ const result = payload as { result: { structuredContent: { task: { id: string } } } };
544
+ const task = await getTaskById(result.result.structuredContent.task.id);
545
+ expect(task).toMatchObject({
546
+ status: "pending",
547
+ agentId: lead.id,
548
+ requestedByUserId: user.id,
549
+ routingReason: "skill",
550
+ routingSource: "engine_default",
551
+ routingNote: "User MCP ingress assigns new work to the Lead for delegation.",
552
+ });
553
+ expect(task?.offeredTo).toBeUndefined();
554
+ expect(task?.routingAffinity?.leadOnly).not.toBe(true);
555
+ });
556
+
315
557
  test("owner /mcp initialize requires a known X-Agent-ID", async () => {
316
558
  const missing = await mcpPost(
317
559
  API_KEY,
@@ -0,0 +1,143 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
+ import { unlink } from "node:fs/promises";
3
+ import { closeDb, createAgent, getDbClient, initDb } from "../be/db";
4
+ import { SqliteMemoryStore } from "../be/memory/providers/sqlite-store";
5
+ import { memorySnippet, renderMemoriesPrompt } from "../prompts/memories";
6
+
7
+ const longPrompt = `Implement the thing. ${"Background detail that is not the outcome. ".repeat(12)}`;
8
+
9
+ describe("renderMemoriesPrompt — outcome, date and source", () => {
10
+ test("a task_completion line shows the output, not the start of a >300-char prompt", () => {
11
+ expect(longPrompt.length).toBeGreaterThan(300);
12
+ const prompt = renderMemoriesPrompt([
13
+ {
14
+ id: "m-1",
15
+ name: "Task: Implement the thing",
16
+ content: `Task: ${longPrompt}\n\nOutput:\nShipped PR #42; tests green.`,
17
+ similarity: 0.9,
18
+ source: "task_completion",
19
+ createdAt: "2026-09-29T07:20:00.000Z",
20
+ },
21
+ ]);
22
+ expect(prompt).toContain(
23
+ "- [2026-09-29, task_completion] **Task: Implement the thing** (id: m-1): Shipped PR #42; tests green.",
24
+ );
25
+ expect(prompt).not.toContain("Background detail");
26
+ });
27
+
28
+ test("a failed task_completion shows the failure reason without the boilerplate tail", () => {
29
+ const snippet = memorySnippet({
30
+ id: "m-2",
31
+ name: "x",
32
+ content: `Task: ${longPrompt}\n\nFailure reason:\nCI red on main.\n\nThis task failed. Learn from this to avoid repeating the mistake.`,
33
+ similarity: 0.9,
34
+ source: "task_completion",
35
+ });
36
+ expect(snippet).toBe("FAILED: CI red on main.");
37
+ });
38
+
39
+ test("the last Output marker wins when the task prompt embeds an earlier one", () => {
40
+ const snippet = memorySnippet({
41
+ id: "m-3",
42
+ name: "x",
43
+ content:
44
+ 'Task: Worker task completed.\nTask: "y"\n\nOutput:\nold worker output\n\nOutput:\nreview done',
45
+ similarity: 0.9,
46
+ source: "task_completion",
47
+ });
48
+ expect(snippet).toBe("review done");
49
+ });
50
+
51
+ test("summary wins when set; other sources keep their content; missing metadata adds no prefix", () => {
52
+ expect(
53
+ memorySnippet({
54
+ id: "m-4",
55
+ name: "x",
56
+ content: `Task: ${longPrompt}\n\nOutput:\nlong output`,
57
+ similarity: 0.9,
58
+ source: "task_completion",
59
+ summary: " short summary ",
60
+ }),
61
+ ).toBe("short summary");
62
+ const prompt = renderMemoriesPrompt([
63
+ { id: "m-5", name: "manual note", content: "Output:\nkeep me", similarity: 0.9 },
64
+ ]);
65
+ expect(prompt).toContain("- **manual note** (id: m-5): Output:\nkeep me");
66
+ });
67
+ });
68
+
69
+ describe("200_backfill_auto_memory_expiry", () => {
70
+ const TEST_DB_PATH = "./test-memory-injection-outcome.sqlite";
71
+ const agentId = crypto.randomUUID();
72
+ let store: SqliteMemoryStore;
73
+
74
+ beforeAll(async () => {
75
+ for (const suffix of ["", "-wal", "-shm"]) {
76
+ try {
77
+ await unlink(TEST_DB_PATH + suffix);
78
+ } catch {}
79
+ }
80
+ initDb(TEST_DB_PATH);
81
+ await createAgent({ id: agentId, name: "Backfill Agent", isLead: false, status: "idle" });
82
+ store = new SqliteMemoryStore();
83
+ });
84
+
85
+ afterAll(async () => {
86
+ closeDb();
87
+ for (const suffix of ["", "-wal", "-shm"]) {
88
+ try {
89
+ await unlink(TEST_DB_PATH + suffix);
90
+ } catch {}
91
+ }
92
+ });
93
+
94
+ test("sets a 7-day expiry on legacy agent-scope auto memories only", async () => {
95
+ const base = { agentId, name: "m", content: "Task: x\n\nOutput:\ny" };
96
+ const completion = await store.store({ ...base, scope: "agent", source: "task_completion" });
97
+ const session = await store.store({ ...base, scope: "agent", source: "session_summary" });
98
+ const swarm = await store.store({ ...base, scope: "swarm", source: "task_completion" });
99
+ const manual = await store.store({ ...base, scope: "agent", source: "manual" });
100
+ const longterm = await store.store({
101
+ ...base,
102
+ scope: "agent",
103
+ source: "task_completion",
104
+ key: "/longterm/facts/backfill/keep",
105
+ });
106
+ const alreadySet = await store.store({ ...base, scope: "agent", source: "task_completion" });
107
+
108
+ const db = getDbClient();
109
+ const legacy = [completion, session, swarm, manual, longterm];
110
+ for (const row of legacy) {
111
+ await db.run("UPDATE agent_memory SET expiresAt = NULL WHERE id = ?", [row.id]);
112
+ }
113
+ const presetExpiry = "2099-01-01T00:00:00.000Z";
114
+ await db.run("UPDATE agent_memory SET expiresAt = ? WHERE id = ?", [
115
+ presetExpiry,
116
+ alreadySet.id,
117
+ ]);
118
+
119
+ const sql = await Bun.file(
120
+ new URL("../be/migrations/200_backfill_auto_memory_expiry.sql", import.meta.url),
121
+ ).text();
122
+ const before = Date.now();
123
+ await db.run(sql);
124
+
125
+ const expiry = async (id: string) =>
126
+ (
127
+ await db.get<{ expiresAt: string | null }>(
128
+ "SELECT expiresAt FROM agent_memory WHERE id = ?",
129
+ [id],
130
+ )
131
+ )?.expiresAt ?? null;
132
+
133
+ for (const row of [completion, session]) {
134
+ const value = await expiry(row.id);
135
+ expect(value).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
136
+ const days = (Date.parse(value!) - before) / 86_400_000;
137
+ expect(days).toBeGreaterThan(6.99);
138
+ expect(days).toBeLessThan(7.01);
139
+ }
140
+ for (const row of [swarm, manual, longterm]) expect(await expiry(row.id)).toBeNull();
141
+ expect(await expiry(alreadySet.id)).toBe(presetExpiry);
142
+ });
143
+ });