@desplega.ai/agent-swarm 1.162.0 → 1.164.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 (240) hide show
  1. package/README.md +2 -1
  2. package/dist/{acp-adapter-74m73ae9.js → acp-adapter-8t22d18g.js} +2 -2
  3. package/dist/{actions-4sr14gnt.js → actions-49xrhww7.js} +32 -29
  4. package/dist/amp-adapter-p230tajt.js +34 -0
  5. package/dist/{anthropic-messages-8xxrzky9.js → anthropic-messages-hst62ws9.js} +3 -3
  6. package/dist/{app-pt68fbwp.js → app-tgzw3f3d.js} +11 -10
  7. package/dist/{assistant-cv3jytks.js → assistant-dgfjm07j.js} +33 -30
  8. package/dist/{azure-openai-responses-f5cqsyj8.js → azure-openai-responses-mh8tvf78.js} +9 -68
  9. package/dist/{boot-reembed-4ht4gqth.js → boot-reembed-0qzs614r.js} +13 -12
  10. package/dist/{boot-reembed-nxpcvpj9.js → boot-reembed-dgp6peyx.js} +12 -11
  11. package/dist/{boot-scrub-logs-k93kyw8k.js → boot-scrub-logs-fxnk021a.js} +10 -9
  12. package/dist/{boot-seeds-1skcrs0e.js → boot-seeds-xe7adjvm.js} +14 -13
  13. package/dist/{claude-adapter-r5g0tzge.js → claude-adapter-hx0n8a31.js} +22 -20
  14. package/dist/{claude-managed-adapter-yxkfxg8j.js → claude-managed-adapter-hskapeen.js} +3 -3
  15. package/dist/{claude-managed-setup-pkyrgshb.js → claude-managed-setup-p7mt6dfq.js} +1 -1
  16. package/dist/{claude-sdk-session-jxyxnrwd.js → claude-sdk-session-d61a9c8x.js} +22 -20
  17. package/dist/{cli-twrh04ap.js → cli-0y30kqz4.js} +3 -3
  18. package/dist/{cli-5xhpb3kv.js → cli-0z9dj97t.js} +9 -9
  19. package/dist/{cli-t845n9xk.js → cli-1nzb8ftr.js} +2 -2
  20. package/dist/{cli-kdkwd3yr.js → cli-1wd1n2ya.js} +38 -10
  21. package/dist/{cli-vxe6qa4h.js → cli-23az412e.js} +1 -1
  22. package/dist/{cli-bsky8g7w.js → cli-26x4d89j.js} +1 -1
  23. package/dist/{cli-swfmp1fx.js → cli-2ar0x7cz.js} +9 -9
  24. package/dist/cli-2ksgp01n.js +933 -0
  25. package/dist/{cli-rnyzn44p.js → cli-2zhhnz2h.js} +1 -1
  26. package/dist/{cli-vc9bp061.js → cli-3qaw32we.js} +5 -5
  27. package/dist/{cli-vy3ed88x.js → cli-402m8btc.js} +31 -3
  28. package/dist/{cli-5bcd7sfy.js → cli-43fh8fzw.js} +3 -3
  29. package/dist/{cli-5k6k8ta3.js → cli-4f7s6k3q.js} +13 -5
  30. package/dist/{cli-sqvfa090.js → cli-4ntg6tkp.js} +6 -5
  31. package/dist/{cli-kh04eqwe.js → cli-4y9bnpvy.js} +1 -1
  32. package/dist/{cli-qjjvzwzd.js → cli-5c2j5yvx.js} +1 -1
  33. package/dist/{cli-2zy95vhm.js → cli-6891ft0y.js} +2 -2
  34. package/dist/{cli-tre4bdgq.js → cli-6behs2bt.js} +1 -1
  35. package/dist/cli-7mm2r1kp.js +17 -0
  36. package/dist/cli-7xcgj20z.js +68 -0
  37. package/dist/{cli-4cwxn7gp.js → cli-812vt3ht.js} +2 -2
  38. package/dist/{cli-mbssg1xv.js → cli-8kt0bmgx.js} +1 -1
  39. package/dist/{cli-wjgs5wxe.js → cli-8rpq35c0.js} +7 -7
  40. package/dist/{cli-jxs2ygaw.js → cli-9m8fk2v7.js} +74 -29
  41. package/dist/{cli-pkd6s2wy.js → cli-9q3wt23f.js} +2 -2
  42. package/dist/{cli-s3hwpt9v.js → cli-9s6fsg07.js} +30 -10
  43. package/dist/{cli-xqpk3rp7.js → cli-9x3bwfd3.js} +28 -5
  44. package/dist/{cli-qrgt5bqf.js → cli-agamgm0p.js} +1 -1
  45. package/dist/{cli-13hw4z91.js → cli-b0f70z24.js} +1 -1
  46. package/dist/{cli-gpqpmns7.js → cli-cgrvsz2t.js} +2 -2
  47. package/dist/{cli-2hhfhfq5.js → cli-cn53e7vc.js} +8 -1
  48. package/dist/{cli-p0r8sxj5.js → cli-crjazzsg.js} +222 -4
  49. package/dist/{cli-tcen9p5w.js → cli-d37n8b7c.js} +5 -5
  50. package/dist/{cli-wxkkd0e0.js → cli-dgh781z9.js} +3 -3
  51. package/dist/cli-dsdg2wrk.js +516 -0
  52. package/dist/{cli-q24re23n.js → cli-e2xnc4m5.js} +4 -4
  53. package/dist/{cli-f7kvq46p.js → cli-erxeg64j.js} +20 -0
  54. package/dist/{cli-8ev9xqnx.js → cli-fajbc7sa.js} +17 -219
  55. package/dist/{cli-g739zqzt.js → cli-fj86xr3c.js} +2 -2
  56. package/dist/{cli-zvhd3f64.js → cli-fpbvcf55.js} +3 -3
  57. package/dist/{cli-kp42yek0.js → cli-fyra5y8k.js} +424 -71
  58. package/dist/{cli-jyy6pm2f.js → cli-g3qgrzck.js} +15 -3
  59. package/dist/{cli-eg0b3p0k.js → cli-g5erf0zt.js} +23 -14
  60. package/dist/{cli-cvatsam7.js → cli-gcvzvq3e.js} +4 -4
  61. package/dist/{cli-89s4ngw3.js → cli-gvcgmx69.js} +1 -1
  62. package/dist/{cli-7tre70zp.js → cli-j261c831.js} +138 -128
  63. package/dist/{cli-sptrj5f5.js → cli-j3me0jw2.js} +2 -2
  64. package/dist/{cli-zxqx25nz.js → cli-k6w44yvq.js} +6 -6
  65. package/dist/cli-ndx5yz5n.js +3299 -0
  66. package/dist/{cli-ykk5jaax.js → cli-nexwh8zg.js} +34 -29
  67. package/dist/{cli-rfjw9jw2.js → cli-ng97fjq7.js} +2 -2
  68. package/dist/{cli-4x0csdjq.js → cli-ngne1dng.js} +2 -2
  69. package/dist/{cli-4dcpaefp.js → cli-pfzrhb8n.js} +2 -2
  70. package/dist/{cli-hb5c2x5y.js → cli-q38n4bq0.js} +2 -2
  71. package/dist/{cli-cebmgwsc.js → cli-r109nq7n.js} +4 -4
  72. package/dist/{cli-nq7yqw7b.js → cli-t7g0vmtc.js} +4 -4
  73. package/dist/{cli-jf7wmv1r.js → cli-tsvj33y0.js} +58 -19
  74. package/dist/{cli-7af8yhav.js → cli-vbr859rz.js} +7 -4
  75. package/dist/{cli-nt8hecg9.js → cli-wk3v6h55.js} +67 -38
  76. package/dist/{cli-vyqs9ya8.js → cli-wwvk0tsy.js} +7 -7
  77. package/dist/{cli-rj0gqwwp.js → cli-y2de3bzp.js} +1 -1
  78. package/dist/{cli-hmsw1gpy.js → cli-y74170pb.js} +4 -4
  79. package/dist/cli-ydt3zkwv.js +70 -0
  80. package/dist/{cli-xk79303e.js → cli-yj790e19.js} +9 -1
  81. package/dist/cli-zt3w1cn1.js +137 -0
  82. package/dist/{cli-trc24wk2.js → cli-zxg80zs9.js} +1 -1
  83. package/dist/cli.js +23 -22
  84. package/dist/{cloudflare-workers-ai-system-one-k8ddqjkq.js → cloudflare-workers-ai-system-one-tt484t61.js} +2 -2
  85. package/dist/{codex-adapter-esvmnhn7.js → codex-adapter-e3pky8aj.js} +14 -13
  86. package/dist/{codex-hook-m73jmqgx.js → codex-hook-b1fb7c2k.js} +4 -3
  87. package/dist/{codex-session-runner-ban9excr.js → codex-session-runner-1gq27w2t.js} +14 -13
  88. package/dist/{commands-75413p2n.js → commands-q07ezjzn.js} +10 -9
  89. package/dist/cursor-adapter-50cz7yba.js +32 -0
  90. package/dist/{db-7f5pecwg.js → db-wwt0sq9q.js} +18 -9
  91. package/dist/{dispatcher-fh9at8nz.js → dispatcher-wrg35vdd.js} +31 -28
  92. package/dist/{dsh-adapter-wpze65d4.js → dsh-adapter-sa6rxfpc.js} +6 -6
  93. package/dist/{e2b-gjgdjp91.js → e2b-v5np73yj.js} +4 -4
  94. package/dist/{easter-egg-3d-3w5c2n4c.js → easter-egg-3d-h2hx9b64.js} +2 -2
  95. package/dist/{event-bus-sfx8dx3t.js → event-bus-vkbhzt4n.js} +1 -1
  96. package/dist/{execute-f50wg9nh.js → execute-6gczxy8v.js} +42 -13
  97. package/dist/{google-generative-ai-jxxpnegy.js → google-generative-ai-d6nphg3q.js} +3 -3
  98. package/dist/{google-vertex-x7q1angm.js → google-vertex-cpshkref.js} +3 -3
  99. package/dist/{handlers-w13787bb.js → handlers-rv3gxvsp.js} +34 -31
  100. package/dist/{hook-46g1jrvx.js → hook-bcf5m80y.js} +15 -13
  101. package/dist/{hook-4tpts2ry.js → hook-z8b1jm2d.js} +16 -14
  102. package/dist/{http-19pzbfrt.js → http-56f7yt97.js} +932 -134
  103. package/dist/{index-scptvdxg.js → index-0kncjnrm.js} +30 -23
  104. package/dist/{index-dqmp15bq.js → index-1e6cg96n.js} +31 -28
  105. package/dist/{index-qb4q1y3e.js → index-8xgdb3k8.js} +31 -28
  106. package/dist/index-adw4zr9j.js +1020 -0
  107. package/dist/{index-h5gs261z.js → index-etdrp7yp.js} +140 -140
  108. package/dist/{index-8fd4dm4h.js → index-f9zscmft.js} +32 -29
  109. package/dist/index-fdk1t4d6.js +91297 -0
  110. package/dist/{index-s75wc4wd.js → index-jvb37h77.js} +12 -11
  111. package/dist/{keepalive-xa5ts9cd.js → keepalive-rp8daj4s.js} +12 -11
  112. package/dist/{lead-qp4xkvjq.js → lead-dfp77erc.js} +45 -40
  113. package/dist/{maintenance-rm5885hm.js → maintenance-wtmw3jqv.js} +13 -12
  114. package/dist/{mistral-conversations-rw7fbt22.js → mistral-conversations-8445ddjn.js} +2 -2
  115. package/dist/{oauth-refresh-sweep-1csdfebf.js → oauth-refresh-sweep-yhzqt95v.js} +11 -10
  116. package/dist/{onboard-r6b78cs4.js → onboard-9z8rwbcy.js} +2 -2
  117. package/dist/{openai-codex-responses-efvrgrq3.js → openai-codex-responses-9ynm88zy.js} +3 -3
  118. package/dist/{openai-completions-bbbrrz4z.js → openai-completions-nhqwjmaw.js} +2 -2
  119. package/dist/{openai-responses-9jjyrcm1.js → openai-responses-xx1c4kgm.js} +3 -3
  120. package/dist/{opencode-adapter-kb3g6pkf.js → opencode-adapter-1nxb6555.js} +5 -5
  121. package/dist/{otel-impl-0y9knpky.js → otel-impl-b83gaxw2.js} +1 -1
  122. package/dist/{pi-mono-adapter-z6s1bme2.js → pi-mono-adapter-ddxz8pjp.js} +18 -17
  123. package/dist/{pricing-refresh-mth26tgp.js → pricing-refresh-tmjvazb6.js} +12 -11
  124. package/dist/{rbac-roles-kwntb23q.js → rbac-roles-pa0gzjws.js} +11 -10
  125. package/dist/{rbac-roles-vpk4ebm8.js → rbac-roles-rhexa9s2.js} +12 -11
  126. package/dist/{render-v2-hcm7h00y.js → render-v2-ask84n8y.js} +11 -10
  127. package/dist/{runtime-vfjsfsc5.js → runtime-ckjnj3vf.js} +4 -4
  128. package/dist/{setup-hbyzd65h.js → setup-5ga5gdsy.js} +2 -2
  129. package/dist/{typesafe-system-one-2ef30pec.js → typesafe-system-one-datfr82p.js} +2 -2
  130. package/dist/{virtual-modules-9bv7ry91.js → virtual-modules-bmp3zwjf.js} +12 -11
  131. package/dist/{work-objects-m09sxnhe.js → work-objects-snnh0t7j.js} +34 -31
  132. package/dist/{worker-0h35rvk4.js → worker-rxyymrhb.js} +45 -40
  133. package/openapi.json +997 -39
  134. package/package.json +6 -5
  135. package/src/be/db/active-sessions.ts +30 -0
  136. package/src/be/db/approvals.ts +54 -0
  137. package/src/be/db.ts +25 -4
  138. package/src/be/mcp-server-checked-update.ts +34 -0
  139. package/src/be/mcp-server-stdio-gate.ts +34 -0
  140. package/src/be/memory/key-browser.ts +304 -0
  141. package/src/be/memory/providers/sqlite-store.ts +16 -3
  142. package/src/be/migrations/194_memory_orphan_chunks.sql +15 -0
  143. package/src/be/migrations/195_approval_request_approvals.sql +6 -0
  144. package/src/be/model-validation.ts +12 -2
  145. package/src/be/multi-runtime.ts +4 -3
  146. package/src/be/pricing-normalize.ts +11 -0
  147. package/src/be/repo-checked-update.ts +37 -0
  148. package/src/be/repo-merge-policy.ts +15 -0
  149. package/src/be/seed-pricing.ts +48 -1
  150. package/src/be/swarm-config-guard.ts +1 -0
  151. package/src/commands/provider-credentials.ts +44 -2
  152. package/src/extensions/contract-types.generated.ts +1 -1
  153. package/src/extensions/contract.ts +100 -0
  154. package/src/extensions/dispatcher.ts +19 -0
  155. package/src/extensions/post-bridge.ts +191 -0
  156. package/src/github/handlers.ts +128 -2
  157. package/src/gitlab/workflow-action-name.ts +15 -0
  158. package/src/heartbeat/heartbeat.ts +4 -3
  159. package/src/http/active-sessions.ts +6 -5
  160. package/src/http/agents.ts +10 -1
  161. package/src/http/approval-requests.ts +271 -35
  162. package/src/http/index.ts +1 -1
  163. package/src/http/mcp-servers.ts +74 -50
  164. package/src/http/memory.ts +275 -6
  165. package/src/http/repos.ts +58 -18
  166. package/src/http/request-principal.ts +19 -0
  167. package/src/http/session-cost-recompute.ts +64 -0
  168. package/src/http/session-data.ts +11 -0
  169. package/src/http/tasks.ts +9 -1
  170. package/src/http/webhooks.ts +17 -5
  171. package/src/prompts/base-prompt.ts +9 -4
  172. package/src/prompts/resolver.ts +1 -1
  173. package/src/prompts/session-templates.ts +28 -0
  174. package/src/providers/amp-adapter.ts +1232 -0
  175. package/src/providers/cursor-adapter.ts +638 -0
  176. package/src/providers/index.ts +9 -1
  177. package/src/providers/pricing-sources.md +39 -0
  178. package/src/providers/reasoning-effort.ts +15 -1
  179. package/src/providers/types.ts +11 -1
  180. package/src/rbac/legacy-policy.ts +23 -8
  181. package/src/rbac/permissions.ts +27 -1
  182. package/src/scripts-runtime/types/swarm-extension.d.ts +119 -0
  183. package/src/tests/amp-adapter.test.ts +822 -0
  184. package/src/tests/amp-budget-export-failed.test.ts +133 -0
  185. package/src/tests/amp-pricing-helpers.ts +16 -0
  186. package/src/tests/amp-pricing.test.ts +218 -0
  187. package/src/tests/api-drain-worker-e2e.test.ts +18 -7
  188. package/src/tests/approval-respond-auth.test.ts +371 -0
  189. package/src/tests/base-prompt.test.ts +13 -0
  190. package/src/tests/claude-adapter-binary.test.ts +24 -0
  191. package/src/tests/claude-adapter.test.ts +8 -0
  192. package/src/tests/cursor-adapter.test.ts +690 -0
  193. package/src/tests/db-task-read-contract.test.ts +5 -0
  194. package/src/tests/db-task-write-contract.test.ts +5 -0
  195. package/src/tests/docs-index-footprint.test.js +3 -3
  196. package/src/tests/extensions-post-bridge.test.ts +669 -0
  197. package/src/tests/extensions-run-context.test.ts +61 -0
  198. package/src/tests/fixtures/fake-amp.ts +304 -0
  199. package/src/tests/github-handlers-inline-comments.test.ts +147 -1
  200. package/src/tests/gitlab-workflow-event-names.test.ts +136 -0
  201. package/src/tests/mcp-input-ergonomics.test.ts +15 -10
  202. package/src/tests/mcp-server-stdio-gate.test.ts +438 -0
  203. package/src/tests/memory-http-access.test.ts +221 -0
  204. package/src/tests/memory-keys-endpoint.test.ts +406 -0
  205. package/src/tests/memory-store.test.ts +166 -0
  206. package/src/tests/model-tier-resolution.test.ts +3 -3
  207. package/src/tests/pricing-refresh.test.ts +5 -4
  208. package/src/tests/prompt-template-session.test.ts +5 -3
  209. package/src/tests/provider-process-group.test.ts +41 -0
  210. package/src/tests/provider-registration.test.ts +3 -1
  211. package/src/tests/rbac-charact-misc-tools.test.ts +18 -3
  212. package/src/tests/rbac-engine.test.ts +15 -11
  213. package/src/tests/rbac-wire-e2e.test.ts +4 -9
  214. package/src/tests/repo-allow-merge-gate.test.ts +382 -0
  215. package/src/tests/script-connections.test.ts +4 -0
  216. package/src/tests/send-task-requester-gate.test.ts +260 -0
  217. package/src/tests/session-activity-liveness.test.ts +248 -0
  218. package/src/tests/session-costs-golden.test.ts +28 -0
  219. package/src/tests/webhooks-task-blocked.test.ts +4 -4
  220. package/src/tests/write-gate.ts +68 -0
  221. package/src/tests/x402-client.test.ts +124 -0
  222. package/src/tests/x402-spending-tracker.test.ts +66 -0
  223. package/src/tools/mcp-servers/mcp-server-create.ts +23 -1
  224. package/src/tools/mcp-servers/mcp-server-update.ts +46 -28
  225. package/src/tools/memory-delete.ts +3 -3
  226. package/src/tools/repos/update-repo.ts +26 -5
  227. package/src/tools/send-task.ts +24 -1
  228. package/src/tools/steer-task.ts +1 -1
  229. package/src/types.ts +37 -2
  230. package/src/utils/amp-models.ts +114 -0
  231. package/src/utils/credentials.ts +8 -0
  232. package/src/utils/process-group.ts +79 -0
  233. package/src/workflows/event-bus.ts +28 -0
  234. package/src/x402/client.ts +36 -14
  235. package/src/x402/spending-tracker.ts +64 -3
  236. package/templates/skills/agent-browser/SKILL.md +5 -0
  237. package/templates/skills/agent-browser/content.md +5 -0
  238. package/templates/skills/heartbeat-runbook/content.md +1 -0
  239. package/templates/skills/swarm-extensions/content.md +4 -0
  240. package/dist/cli-y89t4nca.js +0 -67
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Who may list and delete memories over HTTP (`POST /api/memory/list`,
3
+ * `DELETE /api/memory/{id}`).
4
+ *
5
+ * Drives the real `handleMemory` handler against the real SQLite store behind
6
+ * the same auth resolution `handleCore` performs, so each case presents a real
7
+ * credential: the shared key, the shared key with `X-Agent-ID` (what the script
8
+ * SDK's `memory_delete` sends), an `aseph_` agent session token, or an `aswt_`
9
+ * user token.
10
+ */
11
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
12
+ import { unlink } from "node:fs/promises";
13
+ import { createServer, type Server } from "node:http";
14
+ import { closeDb, createAgent, createUser, initDb } from "../be/db";
15
+ import { getMemoryStore } from "../be/memory";
16
+ import { type IdentityActor, mintSessionToken, mintToken } from "../be/users";
17
+ import { resolveHttpRequestAuth } from "../http/auth";
18
+ import { handleMemory } from "../http/memory";
19
+ import { setRequestAuth } from "../utils/request-auth-context";
20
+ import { listenOnFreePort } from "./test-net";
21
+
22
+ const TEST_DB_PATH = "./test-memory-http-access.sqlite";
23
+ const API_KEY = "memory-http-access-test-key";
24
+ const ACTOR: IdentityActor = { kind: "operator", id: "memory-http-access-test" };
25
+
26
+ let server: Server;
27
+ let baseUrl = "";
28
+ let workerA = "";
29
+ let workerB = "";
30
+ let leadId = "";
31
+ let workerASession = "";
32
+ let userToken = "";
33
+
34
+ function createAuthedServer(): Server {
35
+ return createServer(async (req, res) => {
36
+ const auth = await resolveHttpRequestAuth(req, API_KEY);
37
+ if (!auth) {
38
+ res.writeHead(401, { "Content-Type": "application/json" });
39
+ res.end(JSON.stringify({ error: "Unauthorized" }));
40
+ return;
41
+ }
42
+ setRequestAuth(req, auth);
43
+ const url = new URL(req.url ?? "/", "http://localhost");
44
+ const raw = req.headers["x-agent-id"];
45
+ const agentIdHeader = Array.isArray(raw) ? raw[0] : raw;
46
+ const handled = await handleMemory(
47
+ req,
48
+ res,
49
+ url.pathname.split("/").filter(Boolean),
50
+ agentIdHeader,
51
+ );
52
+ if (!handled) {
53
+ res.writeHead(404, { "Content-Type": "application/json" });
54
+ res.end(JSON.stringify({ error: "Not found" }));
55
+ }
56
+ });
57
+ }
58
+
59
+ const operator = () => ({ Authorization: `Bearer ${API_KEY}` });
60
+ const asAgent = (agentId: string) => ({ ...operator(), "X-Agent-ID": agentId });
61
+ const bearer = (token: string) => ({ Authorization: `Bearer ${token}` });
62
+
63
+ async function remember(agentId: string, scope: "agent" | "swarm", name: string) {
64
+ return getMemoryStore().store({
65
+ agentId,
66
+ scope,
67
+ name,
68
+ content: `${name} body`,
69
+ source: "manual",
70
+ });
71
+ }
72
+
73
+ async function del(id: string, headers: Record<string, string>) {
74
+ const res = await fetch(`${baseUrl}/api/memory/${id}`, { method: "DELETE", headers });
75
+ return res.status;
76
+ }
77
+
78
+ async function list(headers: Record<string, string>, body: Record<string, unknown> = {}) {
79
+ const res = await fetch(`${baseUrl}/api/memory/list`, {
80
+ method: "POST",
81
+ headers: { "Content-Type": "application/json", ...headers },
82
+ body: JSON.stringify({ limit: 100, ...body }),
83
+ });
84
+ const json = (await res.json()) as {
85
+ results: { id: string; agentId: string | null; scope: string }[];
86
+ total: number;
87
+ };
88
+ return { status: res.status, ...json };
89
+ }
90
+
91
+ async function exists(id: string) {
92
+ return (await getMemoryStore().peek(id)) !== null;
93
+ }
94
+
95
+ beforeAll(async () => {
96
+ try {
97
+ await unlink(TEST_DB_PATH);
98
+ } catch {}
99
+ initDb(TEST_DB_PATH);
100
+
101
+ workerA = crypto.randomUUID();
102
+ workerB = crypto.randomUUID();
103
+ leadId = crypto.randomUUID();
104
+ await createAgent({ id: workerA, name: "memory-worker-a", isLead: false, status: "idle" });
105
+ await createAgent({ id: workerB, name: "memory-worker-b", isLead: false, status: "idle" });
106
+ await createAgent({ id: leadId, name: "memory-lead", isLead: true, status: "idle" });
107
+
108
+ workerASession = (await mintSessionToken(workerA, crypto.randomUUID(), 60_000)).plaintext;
109
+ const user = await createUser({ name: "Memory admin" });
110
+ userToken = (await mintToken(user.id, "memory-admin", ACTOR)).plaintext;
111
+
112
+ server = createAuthedServer();
113
+ const port = await listenOnFreePort(server);
114
+ baseUrl = `http://127.0.0.1:${port}`;
115
+ });
116
+
117
+ afterAll(async () => {
118
+ await new Promise<void>((resolve) => server.close(() => resolve()));
119
+ closeDb();
120
+ for (const suffix of ["", "-wal", "-shm"]) {
121
+ try {
122
+ await unlink(`${TEST_DB_PATH}${suffix}`);
123
+ } catch {}
124
+ }
125
+ });
126
+
127
+ describe("DELETE /api/memory/{id}", () => {
128
+ test("a worker cannot delete another agent's memory", async () => {
129
+ const memory = await remember(workerB, "agent", "b-private");
130
+ expect(await del(memory.id, asAgent(workerA))).toBe(403);
131
+ expect(await exists(memory.id)).toBe(true);
132
+ });
133
+
134
+ test("an aseph_ session token is held to the same rule", async () => {
135
+ const memory = await remember(workerB, "agent", "b-private-session");
136
+ expect(await del(memory.id, bearer(workerASession))).toBe(403);
137
+ expect(await exists(memory.id)).toBe(true);
138
+ });
139
+
140
+ test("a worker cannot delete a swarm-scope memory, even one it wrote", async () => {
141
+ const theirs = await remember(workerB, "swarm", "b-swarm");
142
+ const own = await remember(workerA, "swarm", "a-swarm");
143
+ expect(await del(theirs.id, asAgent(workerA))).toBe(403);
144
+ expect(await del(own.id, asAgent(workerA))).toBe(403);
145
+ expect(await exists(theirs.id)).toBe(true);
146
+ expect(await exists(own.id)).toBe(true);
147
+ });
148
+
149
+ test("a worker deletes its own agent-scope memory", async () => {
150
+ const memory = await remember(workerA, "agent", "a-private");
151
+ expect(await del(memory.id, asAgent(workerA))).toBe(200);
152
+ expect(await exists(memory.id)).toBe(false);
153
+ });
154
+
155
+ test("the lead deletes a swarm-scope memory but not another agent's agent-scope memory", async () => {
156
+ const swarm = await remember(workerB, "swarm", "b-swarm-for-lead");
157
+ const priv = await remember(workerB, "agent", "b-private-for-lead");
158
+ expect(await del(swarm.id, asAgent(leadId))).toBe(200);
159
+ expect(await del(priv.id, asAgent(leadId))).toBe(403);
160
+ expect(await exists(swarm.id)).toBe(false);
161
+ expect(await exists(priv.id)).toBe(true);
162
+ });
163
+
164
+ test("the operator key and a user token delete any memory", async () => {
165
+ const one = await remember(workerB, "agent", "b-private-for-operator");
166
+ const two = await remember(workerB, "swarm", "b-swarm-for-user");
167
+ expect(await del(one.id, operator())).toBe(200);
168
+ expect(await del(two.id, bearer(userToken))).toBe(200);
169
+ expect(await exists(one.id)).toBe(false);
170
+ expect(await exists(two.id)).toBe(false);
171
+ });
172
+
173
+ test("a missing memory is a 404 for every caller", async () => {
174
+ expect(await del(crypto.randomUUID(), asAgent(workerA))).toBe(404);
175
+ expect(await del(crypto.randomUUID(), operator())).toBe(404);
176
+ });
177
+ });
178
+
179
+ describe("POST /api/memory/list", () => {
180
+ let aPrivate = "";
181
+ let bPrivate = "";
182
+ let bSwarm = "";
183
+
184
+ beforeAll(async () => {
185
+ aPrivate = (await remember(workerA, "agent", "list-a-private")).id;
186
+ bPrivate = (await remember(workerB, "agent", "list-b-private")).id;
187
+ bSwarm = (await remember(workerB, "swarm", "list-b-swarm")).id;
188
+ });
189
+
190
+ test("a worker sees its own rows and swarm rows, never another agent's private rows", async () => {
191
+ const res = await list(asAgent(workerA));
192
+ expect(res.status).toBe(200);
193
+ const ids = res.results.map((r) => r.id);
194
+ expect(ids).toContain(aPrivate);
195
+ expect(ids).toContain(bSwarm);
196
+ expect(ids).not.toContain(bPrivate);
197
+ expect(res.results.every((r) => r.agentId === workerA || r.scope === "swarm")).toBe(true);
198
+ expect(res.total).toBe(res.results.length);
199
+ });
200
+
201
+ test("filtering a worker's list by another agent returns only that agent's swarm rows", async () => {
202
+ const res = await list(asAgent(workerA), { agentId: workerB });
203
+ const ids = res.results.map((r) => r.id);
204
+ expect(ids).toContain(bSwarm);
205
+ expect(ids).not.toContain(bPrivate);
206
+ });
207
+
208
+ test("scope=agent for a worker is its own private rows only", async () => {
209
+ const res = await list(bearer(workerASession), { scope: "agent" });
210
+ const ids = res.results.map((r) => r.id);
211
+ expect(ids).toContain(aPrivate);
212
+ expect(ids).not.toContain(bPrivate);
213
+ });
214
+
215
+ test("the operator key, a user token and the lead see every agent's rows", async () => {
216
+ for (const headers of [operator(), bearer(userToken), asAgent(leadId)]) {
217
+ const ids = (await list(headers)).results.map((r) => r.id);
218
+ expect(ids).toEqual(expect.arrayContaining([aPrivate, bPrivate, bSwarm]));
219
+ }
220
+ });
221
+ });
@@ -0,0 +1,406 @@
1
+ /**
2
+ * GET /api/memory/keys, GET /api/memory/chunks and the key/updatedAt/rating
3
+ * fields on POST /api/memory/list — the dashboard memory browser's reads.
4
+ */
5
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
6
+ import { unlink } from "node:fs/promises";
7
+ import type { IncomingMessage, ServerResponse } from "node:http";
8
+ import { Readable } from "node:stream";
9
+ import { closeDb, createAgent, getDbClient, initDb } from "../be/db";
10
+ import { checkChunkIntegrity } from "../be/memory/key-browser";
11
+ import { handleMemory } from "../http/memory";
12
+ import { getPathSegments } from "../http/utils";
13
+ import { setRequestAuth } from "../utils/request-auth-context";
14
+
15
+ const TEST_DB_PATH = "./test-memory-keys-endpoint.sqlite";
16
+
17
+ const AGENT_A = "aaaa0000-0000-4000-8000-00000000ke01";
18
+ const AGENT_B = "aaaa0000-0000-4000-8000-00000000ke02";
19
+
20
+ const LITMUS_KEY = "/longterm/facts/agenticos/litmus-check-allowlists";
21
+ const SIGTERM_KEY = "/longterm/facts/swarm-deploy/worker-sigterm-handoff-vs-api-drain";
22
+ const PERSON_KEY = "/longterm/entities/people/taras";
23
+
24
+ const IDS = {
25
+ litmus0: "11110000-0000-4000-8000-000000000001",
26
+ litmus1: "11110000-0000-4000-8000-000000000002",
27
+ sigterm0: "11110000-0000-4000-8000-000000000003",
28
+ sigterm1: "11110000-0000-4000-8000-000000000004",
29
+ personA: "11110000-0000-4000-8000-000000000005",
30
+ personB: "11110000-0000-4000-8000-000000000006",
31
+ outside: "11110000-0000-4000-8000-000000000007",
32
+ unkeyed: "11110000-0000-4000-8000-000000000008",
33
+ };
34
+
35
+ async function insertMemory(row: {
36
+ id: string;
37
+ key: string | null;
38
+ name: string;
39
+ content: string;
40
+ scope?: "agent" | "swarm";
41
+ agentId?: string | null;
42
+ chunkIndex?: number;
43
+ totalChunks?: number;
44
+ accessCount?: number;
45
+ alpha?: number;
46
+ beta?: number;
47
+ createdAt?: string;
48
+ updatedAt?: string | null;
49
+ }) {
50
+ const created = row.createdAt ?? "2026-10-01T00:00:00.000Z";
51
+ await getDbClient().run(
52
+ `INSERT INTO agent_memory (id, agentId, scope, key, name, content, source, chunkIndex,
53
+ totalChunks, tags, createdAt, updatedAt, accessedAt, accessCount, alpha, beta)
54
+ VALUES (?, ?, ?, ?, ?, ?, 'manual', ?, ?, '[]', ?, ?, ?, ?, ?, ?)`,
55
+ [
56
+ row.id,
57
+ row.agentId ?? null,
58
+ row.scope ?? "swarm",
59
+ row.key,
60
+ row.name,
61
+ row.content,
62
+ row.chunkIndex ?? 0,
63
+ row.totalChunks ?? 1,
64
+ created,
65
+ row.updatedAt ?? null,
66
+ created,
67
+ row.accessCount ?? 0,
68
+ row.alpha ?? 1,
69
+ row.beta ?? 1,
70
+ ],
71
+ );
72
+ }
73
+
74
+ function fakeReqRes(method: string, path: string, body?: unknown) {
75
+ const req = Readable.from(body === undefined ? [] : [JSON.stringify(body)]) as Readable &
76
+ Partial<IncomingMessage>;
77
+ req.method = method;
78
+ req.url = path;
79
+ req.headers = body === undefined ? {} : { "content-type": "application/json" };
80
+ setRequestAuth(req as IncomingMessage, { kind: "operator", fingerprint: "memory-keys-test" });
81
+
82
+ const captured = { status: 0, body: "" };
83
+ const res = {
84
+ writeHead(status: number) {
85
+ captured.status = status;
86
+ return this;
87
+ },
88
+ setHeader() {
89
+ return this;
90
+ },
91
+ end(chunk?: string) {
92
+ if (chunk) captured.body = chunk;
93
+ return this;
94
+ },
95
+ } as unknown as ServerResponse;
96
+ return { req: req as IncomingMessage, res, captured };
97
+ }
98
+
99
+ async function call(method: string, path: string, body?: unknown, agentId?: string) {
100
+ const { req, res, captured } = fakeReqRes(method, path, body);
101
+ const handled = await handleMemory(req, res, getPathSegments(path), agentId);
102
+ expect(handled).toBe(true);
103
+ return { status: captured.status, body: captured.body ? JSON.parse(captured.body) : null };
104
+ }
105
+
106
+ describe("memory browser endpoints", () => {
107
+ beforeAll(async () => {
108
+ for (const suffix of ["", "-wal", "-shm"]) {
109
+ try {
110
+ await unlink(TEST_DB_PATH + suffix);
111
+ } catch {}
112
+ }
113
+ initDb(TEST_DB_PATH);
114
+ for (const id of [AGENT_A, AGENT_B]) {
115
+ await createAgent({ id, name: `keys-${id.slice(-4)}`, isLead: false, status: "idle" });
116
+ }
117
+
118
+ // Complete 2-chunk memory; names differ across chunks, as in production.
119
+ await insertMemory({
120
+ id: IDS.litmus1,
121
+ key: LITMUS_KEY,
122
+ name: "litmus allowlists (part 2)",
123
+ content: "B".repeat(10),
124
+ chunkIndex: 1,
125
+ totalChunks: 2,
126
+ accessCount: 2,
127
+ alpha: 1,
128
+ beta: 2,
129
+ updatedAt: "2026-10-03T00:00:00.000Z",
130
+ });
131
+ await insertMemory({
132
+ id: IDS.litmus0,
133
+ key: LITMUS_KEY,
134
+ name: "litmus allowlists",
135
+ content: "A".repeat(7),
136
+ chunkIndex: 0,
137
+ totalChunks: 2,
138
+ accessCount: 3,
139
+ alpha: 3,
140
+ beta: 1,
141
+ });
142
+ // Stale chunk 0 says 1 chunk; orphan chunk 1 says 2.
143
+ await insertMemory({
144
+ id: IDS.sigterm0,
145
+ key: SIGTERM_KEY,
146
+ name: "sigterm handoff",
147
+ content: "new single chunk",
148
+ chunkIndex: 0,
149
+ totalChunks: 1,
150
+ });
151
+ await insertMemory({
152
+ id: IDS.sigterm1,
153
+ key: SIGTERM_KEY,
154
+ name: "sigterm handoff (old tail)",
155
+ content: "orphan tail",
156
+ chunkIndex: 1,
157
+ totalChunks: 2,
158
+ });
159
+ // Same key, agent scope, two owners: two documents.
160
+ await insertMemory({
161
+ id: IDS.personA,
162
+ key: PERSON_KEY,
163
+ name: "person A",
164
+ content: "a",
165
+ scope: "agent",
166
+ agentId: AGENT_A,
167
+ });
168
+ await insertMemory({
169
+ id: IDS.personB,
170
+ key: PERSON_KEY,
171
+ name: "person B",
172
+ content: "b",
173
+ scope: "agent",
174
+ agentId: AGENT_B,
175
+ });
176
+ await insertMemory({ id: IDS.outside, key: "/notes/x", name: "outside", content: "x" });
177
+ await insertMemory({ id: IDS.unkeyed, key: null, name: "unkeyed", content: "plain" });
178
+
179
+ await getDbClient().run(
180
+ `INSERT INTO memory_rating (id, memoryId, taskId, source, signal, weight, reasoning, createdAt)
181
+ VALUES (?, ?, NULL, 'llm', 1, 0.5, NULL, ?), (?, ?, NULL, 'llm', -1, 0.5, NULL, ?)`,
182
+ [
183
+ crypto.randomUUID(),
184
+ IDS.litmus0,
185
+ "2026-10-02T00:00:00.000Z",
186
+ crypto.randomUUID(),
187
+ IDS.litmus1,
188
+ "2026-10-02T00:00:00.000Z",
189
+ ],
190
+ );
191
+ });
192
+
193
+ afterAll(async () => {
194
+ closeDb();
195
+ for (const suffix of ["", "-wal", "-shm"]) {
196
+ try {
197
+ await unlink(TEST_DB_PATH + suffix);
198
+ } catch {}
199
+ }
200
+ });
201
+
202
+ describe("GET /api/memory/keys", () => {
203
+ test("groups chunk rows into one row per document under /longterm/ by default", async () => {
204
+ const { status, body } = await call("GET", "/api/memory/keys");
205
+ expect(status).toBe(200);
206
+ expect(body.prefix).toBe("/longterm/");
207
+ expect(body.truncated).toBe(false);
208
+ const keys = body.keys.map((k: { key: string; agentId: string | null }) => k.key);
209
+ expect(keys).toEqual([PERSON_KEY, PERSON_KEY, LITMUS_KEY, SIGTERM_KEY]);
210
+ });
211
+
212
+ test("aggregates usage, tokens, rating and dates over chunks", async () => {
213
+ const { body } = await call("GET", "/api/memory/keys");
214
+ const litmus = body.keys.find((k: { key: string }) => k.key === LITMUS_KEY);
215
+ expect(litmus).toMatchObject({
216
+ memoryId: IDS.litmus0,
217
+ name: "litmus allowlists",
218
+ scope: "swarm",
219
+ agentId: null,
220
+ chunkRows: 2,
221
+ totalChunks: 2,
222
+ complete: true,
223
+ chars: 17,
224
+ estTokens: 5,
225
+ accessCount: 5,
226
+ alpha: 4,
227
+ beta: 3,
228
+ usefulRatings: 1,
229
+ notUsefulRatings: 1,
230
+ updatedAt: "2026-10-03T00:00:00.000Z",
231
+ });
232
+ expect(litmus.rating).toBeCloseTo(4 / 7, 6);
233
+ });
234
+
235
+ test("flags an orphan chunk as incomplete", async () => {
236
+ const { body } = await call("GET", "/api/memory/keys");
237
+ const sigterm = body.keys.find((k: { key: string }) => k.key === SIGTERM_KEY);
238
+ expect(sigterm).toMatchObject({ chunkRows: 2, totalChunks: 2, complete: false });
239
+ });
240
+
241
+ test("keeps agent-scope documents from different owners apart", async () => {
242
+ const { body } = await call("GET", "/api/memory/keys?prefix=/longterm/entities/");
243
+ expect(body.keys.map((k: { agentId: string }) => k.agentId).sort()).toEqual(
244
+ [AGENT_A, AGENT_B].sort(),
245
+ );
246
+ });
247
+
248
+ test("prefix is literal and limit truncates", async () => {
249
+ const none = await call("GET", "/api/memory/keys?prefix=/longterm/%25");
250
+ expect(none.body.keys).toEqual([]);
251
+ const one = await call("GET", "/api/memory/keys?limit=1");
252
+ expect(one.body.keys).toHaveLength(1);
253
+ expect(one.body.truncated).toBe(true);
254
+ });
255
+ });
256
+
257
+ describe("GET /api/memory/chunks", () => {
258
+ test("returns every chunk of a complete memory in chunkIndex order", async () => {
259
+ const { status, body } = await call("GET", `/api/memory/chunks?memoryId=${IDS.litmus1}`);
260
+ expect(status).toBe(200);
261
+ expect(body.key).toBe(LITMUS_KEY);
262
+ expect(body.chunks.map((c: { id: string }) => c.id)).toEqual([IDS.litmus0, IDS.litmus1]);
263
+ expect(body.estTokens).toBe(5);
264
+ expect(body.integrity.ok).toBe(true);
265
+ expect(body.integrity.issues).toEqual([]);
266
+ });
267
+
268
+ test("reports conflicting chunk counts for the orphan chunk", async () => {
269
+ const { body } = await call(
270
+ "GET",
271
+ `/api/memory/chunks?key=${encodeURIComponent(SIGTERM_KEY)}&scope=swarm&agentId=`,
272
+ );
273
+ expect(body.chunks.map((c: { id: string }) => c.id)).toEqual([IDS.sigterm0, IDS.sigterm1]);
274
+ expect(body.integrity).toMatchObject({
275
+ ok: false,
276
+ expectedChunks: 2,
277
+ conflictingTotals: [1, 2],
278
+ missingIndexes: [],
279
+ duplicateIndexes: [],
280
+ });
281
+ expect(body.integrity.issues[0]).toContain("disagree");
282
+ });
283
+
284
+ test("scopes a key lookup to its owner", async () => {
285
+ const { body } = await call(
286
+ "GET",
287
+ `/api/memory/chunks?key=${encodeURIComponent(PERSON_KEY)}&scope=agent&agentId=${AGENT_B}`,
288
+ );
289
+ expect(body.chunks.map((c: { id: string }) => c.id)).toEqual([IDS.personB]);
290
+ });
291
+
292
+ test("an unkeyed memory is its own document", async () => {
293
+ const { body } = await call("GET", `/api/memory/chunks?memoryId=${IDS.unkeyed}`);
294
+ expect(body.key).toBeNull();
295
+ expect(body.chunks).toHaveLength(1);
296
+ expect(body.integrity.ok).toBe(true);
297
+ });
298
+
299
+ test("does not count as an access", async () => {
300
+ await call("GET", `/api/memory/chunks?memoryId=${IDS.litmus0}`);
301
+ const row = await getDbClient().get<{ accessCount: number }>(
302
+ "SELECT accessCount FROM agent_memory WHERE id = ?",
303
+ [IDS.litmus0],
304
+ );
305
+ expect(row?.accessCount).toBe(3);
306
+ });
307
+
308
+ test("404 for an unknown memory, 400 without memoryId or key", async () => {
309
+ const missing = await call(
310
+ "GET",
311
+ "/api/memory/chunks?memoryId=99990000-0000-4000-8000-000000000000",
312
+ );
313
+ expect(missing.status).toBe(404);
314
+ const bad = await call("GET", "/api/memory/chunks");
315
+ expect(bad.status).toBe(400);
316
+ });
317
+ });
318
+
319
+ describe("agent viewer", () => {
320
+ test("keys show only the agent's own rows plus swarm-scope rows", async () => {
321
+ const { body } = await call("GET", "/api/memory/keys", undefined, AGENT_A);
322
+ const docs = body.keys.map((k: { key: string; agentId: string | null }) => [
323
+ k.key,
324
+ k.agentId,
325
+ ]);
326
+ expect(docs).toEqual([
327
+ [PERSON_KEY, AGENT_A],
328
+ [LITMUS_KEY, null],
329
+ [SIGTERM_KEY, null],
330
+ ]);
331
+ });
332
+
333
+ test("another agent's agent-scope chunks read as not found", async () => {
334
+ const hidden = await call(
335
+ "GET",
336
+ `/api/memory/chunks?memoryId=${IDS.personB}`,
337
+ undefined,
338
+ AGENT_A,
339
+ );
340
+ expect(hidden.status).toBe(404);
341
+ const own = await call(
342
+ "GET",
343
+ `/api/memory/chunks?memoryId=${IDS.personA}`,
344
+ undefined,
345
+ AGENT_A,
346
+ );
347
+ expect(own.status).toBe(200);
348
+ const swarm = await call(
349
+ "GET",
350
+ `/api/memory/chunks?memoryId=${IDS.litmus0}`,
351
+ undefined,
352
+ AGENT_A,
353
+ );
354
+ expect(swarm.status).toBe(200);
355
+ });
356
+
357
+ test("a key-only lookup never returns another owner's rows", async () => {
358
+ const path = `/api/memory/chunks?key=${encodeURIComponent(PERSON_KEY)}`;
359
+ const asB = await call("GET", path, undefined, AGENT_B);
360
+ expect(asB.status).toBe(200);
361
+ expect(asB.body.chunks.map((c: { id: string }) => c.id)).toEqual([IDS.personB]);
362
+ const asA = await call("GET", path, undefined, AGENT_A);
363
+ expect(asA.body.chunks.map((c: { id: string }) => c.id)).toEqual([IDS.personA]);
364
+ });
365
+
366
+ test("an operator key-only lookup returns one document, not every owner's rows", async () => {
367
+ const { body } = await call(
368
+ "GET",
369
+ `/api/memory/chunks?key=${encodeURIComponent(PERSON_KEY)}`,
370
+ );
371
+ expect(body.chunks).toHaveLength(1);
372
+ expect(body.chunks[0].agentId).toBe(body.agentId);
373
+ });
374
+ });
375
+
376
+ describe("POST /api/memory/list", () => {
377
+ test("includes key, updatedAt and rating", async () => {
378
+ const { status, body } = await call("POST", "/api/memory/list", { limit: 100 });
379
+ expect(status).toBe(200);
380
+ const litmus1 = body.results.find((r: { id: string }) => r.id === IDS.litmus1);
381
+ expect(litmus1).toMatchObject({ key: LITMUS_KEY, updatedAt: "2026-10-03T00:00:00.000Z" });
382
+ expect(litmus1.rating).toBeCloseTo(1 / 3, 6);
383
+ const unkeyed = body.results.find((r: { id: string }) => r.id === IDS.unkeyed);
384
+ expect(unkeyed).toMatchObject({ key: null, updatedAt: null, rating: 0.5 });
385
+ });
386
+ });
387
+ });
388
+
389
+ describe("checkChunkIntegrity", () => {
390
+ test("missing, duplicate and out-of-range chunks", () => {
391
+ const result = checkChunkIntegrity([
392
+ { id: "a", chunkIndex: 0, totalChunks: 3 },
393
+ { id: "b", chunkIndex: 0, totalChunks: 3 },
394
+ { id: "c", chunkIndex: 3, totalChunks: 3 },
395
+ ]);
396
+ expect(result.ok).toBe(false);
397
+ expect(result.missingIndexes).toEqual([1, 2]);
398
+ expect(result.duplicateIndexes).toEqual([0]);
399
+ expect(result.outOfRangeIds).toEqual(["c"]);
400
+ expect(result.issues).toHaveLength(3);
401
+ });
402
+
403
+ test("a single complete row is fine", () => {
404
+ expect(checkChunkIntegrity([{ id: "a", chunkIndex: 0, totalChunks: 1 }]).ok).toBe(true);
405
+ });
406
+ });