@desplega.ai/agent-swarm 2.7.0 → 2.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (236) hide show
  1. package/dist/{acp-adapter-ma1js5n7.js → acp-adapter-8dn5tq56.js} +6 -6
  2. package/dist/{actions-1vhmtvg9.js → actions-3r6gw7dy.js} +18 -18
  3. package/dist/{amp-adapter-sxqqvmdk.js → amp-adapter-wjtq413p.js} +3 -3
  4. package/dist/{anthropic-messages-txs190dd.js → anthropic-messages-n4cby1rz.js} +4 -4
  5. package/dist/{app-mwx1d5a9.js → app-d53yhykk.js} +3 -3
  6. package/dist/{assistant-yx1p6g82.js → assistant-ksrawj3k.js} +20 -20
  7. package/dist/{boot-reembed-7h78vmvv.js → boot-reembed-fxfw17m2.js} +10 -3
  8. package/dist/{boot-reembed-5h37a2c2.js → boot-reembed-k5y5xbj0.js} +15 -8
  9. package/dist/{boot-scrub-sweep-8mawd4gw.js → boot-scrub-sweep-se41j9fd.js} +3 -3
  10. package/dist/{boot-seeds-ca84dhnn.js → boot-seeds-r3kwwv8b.js} +4 -4
  11. package/dist/{claude-adapter-wqt4hfkq.js → claude-adapter-ahmyf231.js} +7 -7
  12. package/dist/{claude-managed-adapter-6fjqtr2z.js → claude-managed-adapter-5knamegc.js} +3 -3
  13. package/dist/{claude-managed-setup-rqv98sea.js → claude-managed-setup-mdc7dy6b.js} +2 -2
  14. package/dist/{claude-sdk-session-f29f3b2x.js → claude-sdk-session-n5wen9zk.js} +8728 -8599
  15. package/dist/{cli-m5e802r7.js → cli-0dqtv2jg.js} +2 -2
  16. package/dist/{cli-e1595bgx.js → cli-0tww7dnz.js} +2 -2
  17. package/dist/{cli-2y7v133v.js → cli-0w4e4v58.js} +7 -7
  18. package/dist/{cli-d20qvpr2.js → cli-2sjm3t4r.js} +1 -1
  19. package/dist/{cli-gf2850q7.js → cli-3cywb44q.js} +18 -1
  20. package/dist/{cli-8wkb09q5.js → cli-3k9jej0c.js} +2 -2
  21. package/dist/{cli-twctnkng.js → cli-3v2cyqb8.js} +3 -3
  22. package/dist/{cli-he9gbdx0.js → cli-5qt9vzmq.js} +39 -20
  23. package/dist/{cli-a6ds2epe.js → cli-5rp1w4cr.js} +1 -1
  24. package/dist/{cli-6srv653c.js → cli-6vwjsetx.js} +4 -4
  25. package/dist/{cli-1hhbzkbb.js → cli-6w2yrrc0.js} +8 -8
  26. package/dist/{cli-f12mr20z.js → cli-75tdydgv.js} +1 -1
  27. package/dist/{cli-j3y0x2n4.js → cli-8feswf2v.js} +43 -33
  28. package/dist/{cli-07q03xm2.js → cli-8wd3mbyj.js} +1 -1
  29. package/dist/{cli-ymjpkxgn.js → cli-9csfthbw.js} +8 -6
  30. package/dist/{cli-f1e0ptjj.js → cli-b41p764a.js} +21 -2
  31. package/dist/{cli-kv7sbc2c.js → cli-c1t533hh.js} +3 -3
  32. package/dist/{cli-54sj64fc.js → cli-c28jxpcv.js} +1 -1
  33. package/dist/{cli-8mbc9za9.js → cli-cb50g9cd.js} +1 -1
  34. package/dist/{cli-c668vvcf.js → cli-d8gaphtr.js} +1 -1
  35. package/dist/{cli-86ehn4s5.js → cli-dgtfysne.js} +2 -2
  36. package/dist/{cli-sjctjr48.js → cli-dscnhn18.js} +1 -1
  37. package/dist/{cli-j0ppywdr.js → cli-dv11pgpv.js} +12 -12
  38. package/dist/{cli-1m9x8dr5.js → cli-ekptgk2m.js} +5 -5
  39. package/dist/{cli-9gsm2rzp.js → cli-f3mzy662.js} +17 -17
  40. package/dist/{cli-s0mgx214.js → cli-fk7k8s9x.js} +2 -2
  41. package/dist/{cli-vbhgkw9h.js → cli-h3e7bhvn.js} +3 -3
  42. package/dist/{cli-yd2nreqw.js → cli-hkxqdmnb.js} +2 -2
  43. package/dist/{cli-8wmj18cz.js → cli-hpkvgdad.js} +7 -4
  44. package/dist/{cli-wqr7cea4.js → cli-hq2v4rr5.js} +37 -9
  45. package/dist/{cli-sgqm2582.js → cli-k1zb7raw.js} +5 -5
  46. package/dist/{cli-4rvr7zfx.js → cli-k4awgvzm.js} +5 -0
  47. package/dist/{cli-ay40dnj3.js → cli-k7d3hvya.js} +2 -2
  48. package/dist/{cli-j9bpmykh.js → cli-kbtdftbw.js} +5 -5
  49. package/dist/{cli-x37r7qwf.js → cli-kj69b9tw.js} +25 -5
  50. package/dist/{cli-gsfk58nw.js → cli-kk9jy4nm.js} +1 -1
  51. package/dist/{cli-39aet655.js → cli-m52t4cta.js} +1 -1
  52. package/dist/{cli-h5jeg46p.js → cli-mdadewqp.js} +5 -5
  53. package/dist/{cli-a89zdqz9.js → cli-nf6k60sn.js} +1 -1
  54. package/dist/{cli-v6shg7na.js → cli-nzdf09js.js} +1 -1
  55. package/dist/{cli-rr7zx19j.js → cli-prcyzecg.js} +12 -12
  56. package/dist/{cli-6a9qfa8b.js → cli-pzafyqtk.js} +118 -48
  57. package/dist/{cli-48gxjmcz.js → cli-qvaa1gjp.js} +87 -24
  58. package/dist/{cli-qj442fws.js → cli-rg4b2g6j.js} +1 -1
  59. package/dist/{cli-aypd09nf.js → cli-rm66vqdz.js} +1 -1
  60. package/dist/{cli-9pshx6rd.js → cli-s504c3w7.js} +1 -1
  61. package/dist/{cli-ptfq202x.js → cli-swkx9jm2.js} +3 -3
  62. package/dist/{cli-m30k9777.js → cli-thsaq4pd.js} +16 -16
  63. package/dist/{cli-60sty27p.js → cli-twjnyvs8.js} +2 -2
  64. package/dist/{cli-rd5m70gj.js → cli-typdpe8g.js} +4 -4
  65. package/dist/{cli-yxbsmx56.js → cli-vg8pwmsd.js} +6 -6
  66. package/dist/{cli-s09j9vp5.js → cli-vhpckzvp.js} +1 -1
  67. package/dist/{cli-dv97kdzv.js → cli-wdxgsdz5.js} +2 -2
  68. package/dist/{cli-7a6p4e65.js → cli-wtjg6je3.js} +1 -1
  69. package/dist/{cli-b3yzyc57.js → cli-x3cngb1h.js} +44 -12
  70. package/dist/{cli-6mgdba42.js → cli-xbz6mcg4.js} +2 -2
  71. package/dist/{cli-2pvzmas1.js → cli-y4tdxsqb.js} +3 -3
  72. package/dist/{cli-f4dbh7a5.js → cli-yqrsp3ny.js} +7 -7
  73. package/dist/cli.js +27 -19
  74. package/dist/{codex-adapter-zk59x2zh.js → codex-adapter-7wepzq15.js} +5 -5
  75. package/dist/{codex-hook-xyjhvxwd.js → codex-hook-pbdh4xhx.js} +2 -2
  76. package/dist/{codex-session-runner-kh6s4j3n.js → codex-session-runner-02sb49e4.js} +5 -5
  77. package/dist/{commands-68et1qv2.js → commands-3rcspxct.js} +2 -2
  78. package/dist/{controller-fyrbnzg4.js → controller-8ar4avd6.js} +3 -3
  79. package/dist/{controllers-connect-nep9h2xv.js → controllers-connect-ajvh9h0v.js} +19 -19
  80. package/dist/{cursor-adapter-yss3pg7t.js → cursor-adapter-gd0sa7bh.js} +1 -1
  81. package/dist/{db-28gjyvxj.js → db-dv9j7ts5.js} +2 -2
  82. package/dist/{db-8ftwvzba.js → db-z26vgdvj.js} +3 -3
  83. package/dist/{dispatcher-hayexmkr.js → dispatcher-8m0htjq8.js} +17 -17
  84. package/dist/{dsh-adapter-46wawff9.js → dsh-adapter-y8gyk130.js} +1 -1
  85. package/dist/{e2b-fn3zacvt.js → e2b-74vg9b7e.js} +1 -1
  86. package/dist/{e2b-222hsajc.js → e2b-jyay8wzn.js} +1 -1
  87. package/dist/{easter-egg-3d-8d1e0h4m.js → easter-egg-3d-28f880xx.js} +1 -1
  88. package/dist/{execute-cjrfxfnm.js → execute-9seb79vh.js} +1 -1
  89. package/dist/{external-channel-xbmxcg09.js → external-channel-smz7jyth.js} +2 -2
  90. package/dist/{getMachineId-bsd-brn0xym3.js → getMachineId-bsd-whfre6gr.js} +3 -3
  91. package/dist/{getMachineId-darwin-t339sfxm.js → getMachineId-darwin-5zjynb19.js} +3 -3
  92. package/dist/{getMachineId-linux-5eq95d8b.js → getMachineId-linux-8mksrrsq.js} +3 -3
  93. package/dist/{getMachineId-unsupported-qavrtc7j.js → getMachineId-unsupported-fxtqsdt2.js} +3 -3
  94. package/dist/{getMachineId-win-q7fe1wx7.js → getMachineId-win-nzfmz857.js} +3 -3
  95. package/dist/{grok-adapter-ag4wks2a.js → grok-adapter-z0nnvtbn.js} +7 -7
  96. package/dist/handlers-pspryh6g.js +41 -0
  97. package/dist/{heartbeat-psfvcgwp.js → heartbeat-f928aqyp.js} +20 -20
  98. package/dist/{hook-cyq4mryn.js → hook-d7c605r4.js} +7 -7
  99. package/dist/{hook-8pz04697.js → hook-q9hv5xf9.js} +5 -5
  100. package/dist/{http-haj1nd6k.js → http-q6j4k989.js} +934 -180
  101. package/dist/{index-z5h22pes.js → index-5nb266jr.js} +17 -17
  102. package/dist/{index-ysd45kgc.js → index-8fxt5eah.js} +10 -10
  103. package/dist/{index-zgvqzjsw.js → index-cekzwhez.js} +5 -5
  104. package/dist/{index-zyq8ct52.js → index-ch0c1xff.js} +17 -17
  105. package/dist/{index-syvhnvtw.js → index-ebnjhxh1.js} +1 -1
  106. package/dist/{index-y1ajx04a.js → index-ed8dy8rn.js} +20 -20
  107. package/dist/{index-jwt2bj6f.js → index-ejqdkkqx.js} +11 -10
  108. package/dist/{index-dr94zbnh.js → index-farm1zsn.js} +32661 -29443
  109. package/dist/{index-q52f47pe.js → index-yn1yxg85.js} +1 -1
  110. package/dist/{keepalive-xdvsamvj.js → keepalive-zzc3hks1.js} +3 -3
  111. package/dist/{lead-hqg57kdp.js → lead-c0pjcaw4.js} +36 -36
  112. package/dist/{maintenance-4y2xzh3a.js → maintenance-kqsxzpzx.js} +1 -1
  113. package/dist/{oauth-refresh-sweep-jd4r7zbs.js → oauth-refresh-sweep-7qt0g55z.js} +2 -2
  114. package/dist/{onboard-18447q9q.js → onboard-k7x13fqg.js} +118 -5
  115. package/dist/{opencode-adapter-26b2frdf.js → opencode-adapter-tqvrtter.js} +5 -5
  116. package/dist/{otel-impl-f9tgr8yw.js → otel-impl-sqkf6k3b.js} +97 -97
  117. package/dist/{pi-mono-adapter-qn6qbga1.js → pi-mono-adapter-m2qnafxm.js} +63 -9
  118. package/dist/{pricing-refresh-dw2tgb53.js → pricing-refresh-wkpwhgvs.js} +3 -3
  119. package/dist/{rbac-roles-s4nwj2m3.js → rbac-roles-dpn0c0wj.js} +7 -4
  120. package/dist/{rbac-roles-aygm2760.js → rbac-roles-wk7xd6a6.js} +2 -2
  121. package/dist/{render-v2-yfvdfr0x.js → render-v2-ex2m4g2k.js} +3 -3
  122. package/dist/{resume-1bmvjbf3.js → resume-725g9b0b.js} +7 -7
  123. package/dist/{runtime-f7tpzedy.js → runtime-kajas15g.js} +1 -1
  124. package/dist/{session-access-r5garej6.js → session-access-0j2ezjje.js} +8 -4
  125. package/dist/{session-source-backfill-424wzv0r.js → session-source-backfill-62202awe.js} +2 -2
  126. package/dist/{setup-8tygvxkq.js → setup-9sj11ax6.js} +1 -1
  127. package/dist/{virtual-modules-xsw323b3.js → virtual-modules-c6fcq6zr.js} +2 -2
  128. package/dist/{work-objects-ggnghpgf.js → work-objects-0y07495y.js} +20 -20
  129. package/dist/{worker-sqjd1nqf.js → worker-d4q9sqbc.js} +36 -36
  130. package/openapi.json +1070 -9
  131. package/package.json +3 -3
  132. package/src/be/connector-codes.ts +65 -22
  133. package/src/be/memory/boot-reembed.ts +4 -1
  134. package/src/be/memory/constants.ts +6 -0
  135. package/src/be/memory/embedding-recovery.ts +48 -0
  136. package/src/be/memory/index-content.ts +5 -2
  137. package/src/be/memory/providers/openai-embedding.ts +65 -14
  138. package/src/be/memory/task-learning.ts +110 -8
  139. package/src/be/memory/types.ts +13 -2
  140. package/src/be/migrations/246_connector_code_kind.sql +11 -0
  141. package/src/be/onboarding.ts +125 -15
  142. package/src/be/rbac-roles.ts +4 -1
  143. package/src/be/scripts/embeddings.ts +2 -2
  144. package/src/be/seed/agent-fs-provision.ts +75 -14
  145. package/src/be/swarm-config-guard.ts +3 -0
  146. package/src/cli.tsx +9 -0
  147. package/src/comb/config.ts +6 -1
  148. package/src/commands/onboard/agent-fs-version.ts +5 -0
  149. package/src/commands/onboard/compose-generator.ts +112 -1
  150. package/src/commands/onboard/env-generator.ts +9 -0
  151. package/src/commands/onboard/tei-image.ts +12 -0
  152. package/src/commands/onboard/types.ts +7 -0
  153. package/src/commands/onboard.tsx +11 -1
  154. package/src/commands/runner.ts +14 -1
  155. package/src/http/agentfs-proxy.ts +369 -0
  156. package/src/http/all-routes.ts +1 -0
  157. package/src/http/codex-oauth-device.ts +8 -1
  158. package/src/http/fs.ts +64 -10
  159. package/src/http/inbox-state.ts +27 -1
  160. package/src/http/index.ts +4 -1
  161. package/src/http/memory.ts +7 -4
  162. package/src/http/onboarding.ts +139 -11
  163. package/src/http/session-access.ts +24 -0
  164. package/src/http/sessions.ts +10 -6
  165. package/src/http/tasks.ts +85 -11
  166. package/src/http/telemetry-ui.ts +1 -1
  167. package/src/http/user-roles.ts +4 -1
  168. package/src/http/users.ts +272 -3
  169. package/src/http/utils.ts +22 -3
  170. package/src/prompts/base-prompt.ts +24 -9
  171. package/src/prompts/session-templates.ts +26 -2
  172. package/src/providers/amp-adapter.ts +1 -1
  173. package/src/providers/codex-adapter.ts +6 -1
  174. package/src/providers/cursor-adapter.ts +1 -1
  175. package/src/providers/pi-mono-adapter.ts +58 -3
  176. package/src/rbac/legacy-policy.ts +1 -0
  177. package/src/rbac/permissions.ts +4 -0
  178. package/src/tests/agent-fs-provision-seeder.test.ts +261 -92
  179. package/src/tests/agentfs-proxy.test.ts +1106 -0
  180. package/src/tests/base-prompt.test.ts +69 -2
  181. package/src/tests/codex-adapter.test.ts +46 -8
  182. package/src/tests/codex-oauth-device.test.ts +12 -1
  183. package/src/tests/comb-config-guard.test.ts +20 -0
  184. package/src/tests/comb-links.test.ts +79 -1
  185. package/src/tests/comb-status.test.ts +44 -0
  186. package/src/tests/docs-index-footprint.test.js +3 -3
  187. package/src/tests/fixtures/onboard/baseline-states.ts +68 -0
  188. package/src/tests/fixtures/onboard/content.compose.yml.txt +190 -0
  189. package/src/tests/fixtures/onboard/content.env.txt +29 -0
  190. package/src/tests/fixtures/onboard/dev.compose.yml.txt +155 -0
  191. package/src/tests/fixtures/onboard/dev.env.txt +28 -0
  192. package/src/tests/fixtures/onboard/full-all.compose.yml.txt +582 -0
  193. package/src/tests/fixtures/onboard/full-all.env.txt +59 -0
  194. package/src/tests/fixtures/onboard/full.compose.yml.txt +435 -0
  195. package/src/tests/fixtures/onboard/full.env.txt +36 -0
  196. package/src/tests/fixtures/onboard/research.compose.yml.txt +155 -0
  197. package/src/tests/fixtures/onboard/research.env.txt +28 -0
  198. package/src/tests/fixtures/onboard/solo.compose.yml.txt +85 -0
  199. package/src/tests/fixtures/onboard/solo.env.txt +26 -0
  200. package/src/tests/http-log-scrubbing.test.ts +34 -1
  201. package/src/tests/http-task-write-ownership.test.ts +5 -1
  202. package/src/tests/invites-http.test.ts +464 -0
  203. package/src/tests/memory-embedding-input-cap.test.ts +122 -1
  204. package/src/tests/memory-embedding-recovery.test.ts +137 -0
  205. package/src/tests/onboard-compose.test.ts +142 -0
  206. package/src/tests/onboard-env.test.ts +24 -0
  207. package/src/tests/onboard-non-interactive.test.ts +29 -0
  208. package/src/tests/onboard-tei-image.test.ts +36 -0
  209. package/src/tests/onboarding.test.ts +451 -4
  210. package/src/tests/openai-embedding-provider.test.ts +105 -2
  211. package/src/tests/pi-mono-session-cost-on-abort.test.ts +270 -0
  212. package/src/tests/preload-env.test.ts +4 -0
  213. package/src/tests/preload.ts +4 -0
  214. package/src/tests/prompt-template-session.test.ts +19 -3
  215. package/src/tests/rbac-admission-e2e.test.ts +22 -6
  216. package/src/tests/rbac-admission.test.ts +342 -4
  217. package/src/tests/rbac-engine.test.ts +1 -0
  218. package/src/tests/rbac-roles.test.ts +9 -1
  219. package/src/tests/send-task-requester-gate.test.ts +64 -3
  220. package/src/tests/sessions-move.test.ts +39 -1
  221. package/src/tests/store-progress-learning.test.ts +95 -2
  222. package/src/tests/whoami-route.test.ts +41 -0
  223. package/src/tools/inject-learning.ts +1 -1
  224. package/src/tools/memory-edit.ts +1 -1
  225. package/src/tools/memory-search.ts +1 -1
  226. package/src/tools/store-progress.ts +5 -1
  227. package/src/tools/utils.ts +12 -1
  228. package/src/utils/constants.ts +39 -0
  229. package/src/utils/task-attachment-links.ts +15 -3
  230. package/templates/skills/swarm-docs/content.md +1 -1
  231. package/dist/handlers-tzmevqb5.js +0 -41
  232. package/dist/{azure-openai-responses-9kfwhmn7.js → azure-openai-responses-43asa23s.js} +0 -0
  233. package/dist/{node-vxx896fa.js → node-t28cc56b.js} +0 -0
  234. package/dist/{openai-codex-responses-79s8xnbh.js → openai-codex-responses-jhr3sjtm.js} +3 -3
  235. package/dist/{openai-completions-tdbyts0x.js → openai-completions-gs6yn37k.js} +3 -3
  236. /package/dist/{openai-responses-dm3t5vqp.js → openai-responses-jqm9vwwk.js} +0 -0
@@ -1,4 +1,4 @@
1
- import { afterAll, beforeAll, describe, expect, test } from "bun:test";
1
+ import { afterAll, beforeAll, describe, expect, type Mock, spyOn, test } from "bun:test";
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import type { ZodType } from "zod";
4
4
  import {
@@ -10,6 +10,8 @@ import {
10
10
  initDb,
11
11
  startTask,
12
12
  } from "../be/db";
13
+ import { getEmbeddingProvider, getMemoryStore } from "../be/memory";
14
+ import { EMBEDDING_DIMENSIONS } from "../be/memory/constants";
13
15
  import { registerStoreProgressTool } from "../tools/store-progress";
14
16
 
15
17
  interface ToolResult {
@@ -73,7 +75,7 @@ function learningRows(taskId: string): MemoryRow[] {
73
75
 
74
76
  function learningData(result: ToolResult) {
75
77
  return result.structuredContent?.learning as
76
- | { status: string; key?: string; error?: string }
78
+ | { status: string; key?: string; memoryId?: string; similarity?: number; error?: string }
77
79
  | undefined;
78
80
  }
79
81
 
@@ -286,3 +288,94 @@ describe("store-progress learning", () => {
286
288
  expect(learningRows(t.id)).toHaveLength(0);
287
289
  });
288
290
  });
291
+
292
+ describe("store-progress learning near-duplicate guard", () => {
293
+ const vectors = new Map<string, Float32Array>();
294
+ let embedSpy: Mock<(text: string) => Promise<Float32Array | null>>;
295
+
296
+ /** Unit vector on `axis`, tilted toward `axis + 1` so its cosine to the bare axis is `cos`. */
297
+ function vector(axis: number, cos = 1): Float32Array {
298
+ const v = new Float32Array(EMBEDDING_DIMENSIONS);
299
+ v[axis] = cos;
300
+ v[axis + 1] = Math.sqrt(1 - cos ** 2);
301
+ return v;
302
+ }
303
+
304
+ /** Store a learning and give it its embedding now, not in the background. */
305
+ async function seed(text: string, embedding: Float32Array, workstream = "seeded") {
306
+ vectors.set(text, embedding);
307
+ const t = await task();
308
+ const result = await call({ taskId: t.id, learning: { text, workstream } });
309
+ const memoryId = learningData(result)!.memoryId!;
310
+ await getMemoryStore().updateEmbedding(memoryId, embedding, "test-embedding");
311
+ return { task: t, memoryId, key: learningData(result)!.key! };
312
+ }
313
+
314
+ beforeAll(() => {
315
+ embedSpy = spyOn(getEmbeddingProvider(), "embed").mockImplementation(
316
+ async (text: string) => vectors.get(text) ?? null,
317
+ );
318
+ });
319
+ afterAll(() => embedSpy.mockRestore());
320
+
321
+ test("a near duplicate in another workstream is not stored and rates the existing row", async () => {
322
+ const existing = await seed("Upsert scripts from file bytes\nPOST the raw file.", vector(10));
323
+ const text = "Byte-exact script upsert from a local file\nPOST it to the API.";
324
+ vectors.set(text, vector(10, 0.9));
325
+ const t = await task();
326
+ const result = await call({ taskId: t.id, learning: { text, workstream: "other" } });
327
+
328
+ expect(learningData(result)).toMatchObject({
329
+ status: "duplicate",
330
+ memoryId: existing.memoryId,
331
+ key: existing.key,
332
+ });
333
+ expect(learningData(result)?.similarity).toBeCloseTo(0.9, 2);
334
+ expect(message(result)).toContain(`Learning not stored: it matches ${existing.key}`);
335
+ expect(String(result.structuredContent?.nudge)).toContain(`memory-edit that memory instead`);
336
+ expect(String(result.structuredContent?.nudge)).toContain(existing.memoryId);
337
+ expect(learningRows(t.id)).toHaveLength(0);
338
+ const ratings = getDb()
339
+ .query<{ source: string; signal: number; taskId: string }, [string]>(
340
+ "SELECT source, signal, taskId FROM memory_rating WHERE memoryId = ?",
341
+ )
342
+ .all(existing.memoryId);
343
+ expect(ratings).toEqual([{ source: "learning-duplicate", signal: 1, taskId: t.id }]);
344
+ });
345
+
346
+ test("a related learning below the threshold is stored", async () => {
347
+ await seed("Retry flaky pushes once\nThe pre-push hook is slow.", vector(20));
348
+ const text = "Pre-push hooks take five minutes\nLet them finish.";
349
+ vectors.set(text, vector(20, 0.7));
350
+ const t = await task();
351
+ const result = await call({ taskId: t.id, learning: text });
352
+
353
+ expect(learningData(result)?.status).toBe("stored");
354
+ expect(learningRows(t.id)).toHaveLength(1);
355
+ });
356
+
357
+ test("a failed search falls through to the normal insert", async () => {
358
+ await seed("Kebab keys only\nSlugs are lowercase.", vector(30));
359
+ const text = "Keys are kebab case\nSlugs stay lowercase.";
360
+ vectors.set(text, vector(30, 0.95));
361
+ const searchSpy = spyOn(getMemoryStore(), "search").mockRejectedValue(new Error("vec down"));
362
+ try {
363
+ const t = await task();
364
+ const result = await call({ taskId: t.id, progress: "kept", learning: text });
365
+ expect(learningData(result)?.status).toBe("stored");
366
+ expect(learningRows(t.id)).toHaveLength(1);
367
+ expect((await getTaskById(t.id))?.progress).toBe("kept");
368
+ } finally {
369
+ searchSpy.mockRestore();
370
+ }
371
+ });
372
+
373
+ test("an exact replay on the same task is replayed, not a duplicate", async () => {
374
+ const text = "Replay before similarity\nThe same task retrying is not a duplicate.";
375
+ const seeded = await seed(text, vector(40));
376
+ const replay = await call({ taskId: seeded.task.id, learning: { text, workstream: "seeded" } });
377
+
378
+ expect(learningData(replay)).toMatchObject({ status: "replayed", memoryId: seeded.memoryId });
379
+ expect(learningRows(seeded.task.id)).toHaveLength(1);
380
+ });
381
+ });
@@ -17,6 +17,7 @@ import {
17
17
  type ServerResponse,
18
18
  } from "node:http";
19
19
  import { closeDb, createUser, initDb, updateUser } from "../be/db";
20
+ import { ensureRbacSeedsSynced, setUserRoles } from "../be/rbac-roles";
20
21
  import { type IdentityActor, mintToken, revokeToken } from "../be/users";
21
22
  import { handleCore } from "../http/core";
22
23
  import { handleUsers } from "../http/users";
@@ -30,6 +31,7 @@ const ACTOR: IdentityActor = { kind: "operator", id: "op:test" };
30
31
  interface WhoamiBody {
31
32
  kind: "operator" | "user";
32
33
  user: { id: string; name: string; status: string } | null;
34
+ grants?: { grantsAll: boolean; verbs: string[]; roles: string[] };
33
35
  }
34
36
 
35
37
  function createTestServer(): Server {
@@ -61,6 +63,7 @@ describe("GET /api/whoami", () => {
61
63
  beforeAll(async () => {
62
64
  cleanupDb();
63
65
  initDb(TEST_DB_PATH);
66
+ ensureRbacSeedsSynced({ quiet: true });
64
67
  server = createTestServer();
65
68
  port = await listenOnFreePort(server);
66
69
  });
@@ -82,6 +85,7 @@ describe("GET /api/whoami", () => {
82
85
  const body = (await res.json()) as WhoamiBody;
83
86
  expect(body.kind).toBe("operator");
84
87
  expect(body.user).toBeNull();
88
+ expect(body.grants?.grantsAll).toBe(true);
85
89
  });
86
90
 
87
91
  test("active user token resolves to its bound user", async () => {
@@ -96,6 +100,43 @@ describe("GET /api/whoami", () => {
96
100
  expect(body.user?.name).toBe("Whoami User");
97
101
  });
98
102
 
103
+ test("requester token reports its role and verbs, without grantsAll", async () => {
104
+ const user = await createUser({ name: "Requester User", email: "req@example.com" });
105
+ await setUserRoles(user.id, ["requester"], null);
106
+ const { plaintext } = await mintToken(user.id, "rest", ACTOR);
107
+
108
+ const body = (await (await whoami(plaintext)).json()) as WhoamiBody;
109
+ expect(body.grants?.grantsAll).toBe(false);
110
+ expect(body.grants?.roles).toEqual(["requester"]);
111
+ expect(body.grants?.verbs).toContain("task.create.own");
112
+ expect(body.grants?.verbs).not.toContain("config.write.any");
113
+ });
114
+
115
+ test("a new user holds admin by default: grantsAll", async () => {
116
+ const user = await createUser({ name: "Default Admin" });
117
+ const { plaintext } = await mintToken(user.id, "rest", ACTOR);
118
+
119
+ const body = (await (await whoami(plaintext)).json()) as WhoamiBody;
120
+ expect(body.grants?.grantsAll).toBe(true);
121
+ expect(body.grants?.roles).toEqual(["admin"]);
122
+ });
123
+
124
+ test("RBAC_ENABLED=false reports grantsAll for a viewer", async () => {
125
+ const user = await createUser({ name: "Viewer User" });
126
+ await setUserRoles(user.id, ["viewer"], null);
127
+ const { plaintext } = await mintToken(user.id, "rest", ACTOR);
128
+ const previous = process.env.RBAC_ENABLED;
129
+ process.env.RBAC_ENABLED = "false";
130
+ try {
131
+ const body = (await (await whoami(plaintext)).json()) as WhoamiBody;
132
+ expect(body.grants?.grantsAll).toBe(true);
133
+ expect(body.grants?.roles).toEqual(["viewer"]);
134
+ } finally {
135
+ if (previous === undefined) delete process.env.RBAC_ENABLED;
136
+ else process.env.RBAC_ENABLED = previous;
137
+ }
138
+ });
139
+
99
140
  test("revoked token is rejected at auth (401)", async () => {
100
141
  const user = await createUser({ name: "Revoked User" });
101
142
  const { plaintext, tokenId } = await mintToken(user.id, "rest", ACTOR);
@@ -73,7 +73,7 @@ export const registerInjectLearningTool = (server: McpServer) => {
73
73
  // Generate and store embedding (async, best-effort)
74
74
  try {
75
75
  const provider = getEmbeddingProvider();
76
- const embedding = await provider.embed(memory.content);
76
+ const embedding = await provider.embed(memory.content, { kind: "document" });
77
77
  if (embedding) {
78
78
  await store.updateEmbedding(memory.id, embedding, provider.name);
79
79
  }
@@ -183,7 +183,7 @@ export const registerMemoryEditTool = (server: McpServer) => {
183
183
  content !== undefined || oldString !== undefined || newString !== undefined;
184
184
  if (result.changed && (newKey === undefined || contentEdited)) {
185
185
  const provider = getEmbeddingProvider();
186
- const embedding = await provider.embed(result.memory.content);
186
+ const embedding = await provider.embed(result.memory.content, { kind: "document" });
187
187
  if (embedding) await store.updateEmbedding(result.memory.id, embedding, provider.name);
188
188
  try {
189
189
  // Edit path: prune links derived from removed content (sequel links survive).
@@ -120,7 +120,7 @@ export const registerMemorySearchTool = (server: McpServer) => {
120
120
  // Try vector search first
121
121
  const provider = getEmbeddingProvider();
122
122
  const store = getMemoryStore();
123
- const queryEmbedding = await provider.embed(query);
123
+ const queryEmbedding = await provider.embed(query, { kind: "query" });
124
124
 
125
125
  const candidateLimit = limit * CANDIDATE_SET_MULTIPLIER;
126
126
  const candidates = await searchMemoryCandidates(
@@ -125,17 +125,21 @@ export const storeProgressOutputSchema = swarmToolOutputSchema({
125
125
  key: z.string().optional(),
126
126
  memoryId: z.string().optional(),
127
127
  workstream: z.string().optional(),
128
+ similarity: z.number().optional(),
128
129
  error: z.string().optional(),
129
130
  })
130
131
  .optional()
131
132
  .describe(
132
- "Present when the call carried a learning: stored, replayed (same text already stored for this task), or failed with error.",
133
+ "Present when the call carried a learning: stored, replayed (same text already stored for this task), duplicate (an existing swarm memory at key/memoryId says the same; similarity is the raw cosine), or failed with error.",
133
134
  ),
134
135
  });
135
136
 
136
137
  function learningSentence(result: TaskLearningResult): string {
137
138
  if (result.status === "stored") return `Learning stored at ${result.key}.`;
138
139
  if (result.status === "replayed") return `Learning already stored at ${result.key}; skipped.`;
140
+ if (result.status === "duplicate") {
141
+ return `Learning not stored: it matches ${result.key} (similarity ${result.similarity}).`;
142
+ }
139
143
  return `Learning not stored: ${result.error}.`;
140
144
  }
141
145
 
@@ -297,6 +297,17 @@ const storeProgressBlockedWaitingNudge = (r: SwarmToolResult): string | undefine
297
297
  return `That reads as blocked-waiting, ${formatIdleDuration(ms)} since your last update — keep working if you can, and if the wait is genuinely long, defer-task is one way to resume later instead of polling.`;
298
298
  };
299
299
 
300
+ const storeProgressNudge = (r: SwarmToolResult): string | undefined => {
301
+ const learning = (r.data as { learning?: { status?: unknown; memoryId?: unknown } } | undefined)
302
+ ?.learning;
303
+ // nudgeMiddleware permits exactly one steer: the duplicate-learning steer
304
+ // names a concrete next call, so it wins over the blocked-waiting reminder.
305
+ if (r.ok && learning?.status === "duplicate" && typeof learning.memoryId === "string") {
306
+ return `That learning repeats memory ${learning.memoryId}; if your text adds something, memory-edit that memory instead of storing a new one.`;
307
+ }
308
+ return storeProgressBlockedWaitingNudge(r);
309
+ };
310
+
300
311
  const workflowLongScriptTimeoutNudge = (r: SwarmToolResult): string | undefined => {
301
312
  if (!r.ok) return undefined;
302
313
  const hint = (r.data as { longScriptTimeoutHint?: unknown } | undefined)?.longScriptTimeoutHint;
@@ -311,7 +322,7 @@ const workflowLongScriptTimeoutNudge = (r: SwarmToolResult): string | undefined
311
322
  export const NUDGES: Record<string, (result: SwarmToolResult) => string | undefined> = {
312
323
  "defer-task": (r) =>
313
324
  r.ok ? "Stop working on this task now; the wake-up task will carry your note." : undefined,
314
- "store-progress": storeProgressBlockedWaitingNudge,
325
+ "store-progress": storeProgressNudge,
315
326
  "script-run": scriptRunNudge,
316
327
  "script-upsert": scriptAuthoringNudge,
317
328
  "launch-script-run": scriptAuthoringNudge,
@@ -89,6 +89,36 @@ export function getAgentFsLiveUrl(): string {
89
89
  return (raw || DEFAULT_AGENT_FS_LIVE_URL).replace(/\/+$/, "");
90
90
  }
91
91
 
92
+ /** Where people-facing agent-fs file links point: Comb in the dashboard, or the live app. */
93
+ export type AgentFsLinkTarget = "dashboard" | "live";
94
+
95
+ /**
96
+ * The operator's `AGENT_FS_LINK_TARGET` choice (trimmed, lowercased). Null when
97
+ * unset, blank, or invalid: every surface then keeps its automatic behavior.
98
+ */
99
+ export function readAgentFsLinkTarget(): AgentFsLinkTarget | null {
100
+ const raw = process.env.AGENT_FS_LINK_TARGET?.trim().toLowerCase();
101
+ return raw === "dashboard" || raw === "live" ? raw : null;
102
+ }
103
+
104
+ /**
105
+ * The base URL (no trailing slash) that a people-facing file link starts with,
106
+ * per {@link readAgentFsLinkTarget}. `dashboard` uses `APP_URL` when one is
107
+ * configured (and, for attachments, Comb is on), else the live app. Null when
108
+ * no target is set: the caller keeps its current code path.
109
+ *
110
+ * The prompt surface skips the Comb check: workers do not get `COMB_ENABLED`,
111
+ * and an explicit `dashboard` is the operator's choice.
112
+ */
113
+ export function resolveAgentFsLinkBase(surface: "prompt" | "attachment"): string | null {
114
+ const target = readAgentFsLinkTarget();
115
+ if (target === null) return null;
116
+ if (target === "dashboard" && getConfiguredAppUrls().length > 0) {
117
+ if (surface === "prompt" || isCombEnabled()) return getAppUrl();
118
+ }
119
+ return getAgentFsLiveUrl();
120
+ }
121
+
92
122
  /**
93
123
  * Optional fallback agent-fs `org_id` for attachments that store only `path`.
94
124
  * Strictly opt-in — when neither env var is set, the renderer keeps the
@@ -166,6 +196,15 @@ export function isCombEnabled(): boolean {
166
196
  return isEnvFlagEnabled("COMB_ENABLED", false) && Boolean(apiUrl);
167
197
  }
168
198
 
199
+ /**
200
+ * `AGENT_FS_PROXY`: the API serves agent-fs at `/agentfs/*` (plus the public
201
+ * `/share/*` and `/site/*` pages), and Comb uses `<public API URL>/agentfs`.
202
+ * Read per request, so a config upsert applies without a restart. Off by default.
203
+ */
204
+ export function isAgentFsProxyEnabled(): boolean {
205
+ return isEnvFlagEnabled("AGENT_FS_PROXY", false);
206
+ }
207
+
169
208
  /**
170
209
  * The dashboard (Comb) URL for an agent-fs file, same id and encoding rules as
171
210
  * {@link buildAgentFsLiveUrl}. Null unless Comb is on AND an explicit
@@ -1,5 +1,11 @@
1
1
  import type { TaskAttachment } from "../types";
2
- import { buildAgentFsLiveUrl, buildCombFileUrl, getAppUrl } from "./constants";
2
+ import {
3
+ agentFsFileRoute,
4
+ buildAgentFsLiveUrl,
5
+ buildCombFileUrl,
6
+ getAppUrl,
7
+ resolveAgentFsLinkBase,
8
+ } from "./constants";
3
9
 
4
10
  function shellQuote(value: string): string {
5
11
  return `'${value.replace(/'/g, `'\\''`)}'`;
@@ -26,8 +32,9 @@ export function taskAttachmentFetchCommand(
26
32
  /**
27
33
  * The link a human (Slack, prompts, citations) sees for an attachment. An
28
34
  * agent-fs file links to the dashboard (Comb) when Comb is on and `APP_URL` is
29
- * set, else to the agent-fs live UI, else to the raw `agent-fs:<path>`.
30
- * `comb: false` skips the dashboard link.
35
+ * set, else to the agent-fs live UI, else to the raw `agent-fs:<path>`. An
36
+ * explicit `AGENT_FS_LINK_TARGET` picks the base instead (see
37
+ * `resolveAgentFsLinkBase`). `comb: false` always links to the live UI.
31
38
  */
32
39
  export function taskAttachmentDisplayUrl(
33
40
  attachment: TaskAttachment,
@@ -40,6 +47,11 @@ export function taskAttachmentDisplayUrl(
40
47
 
41
48
  if (attachment.providerId === "agent-fs" || attachment.kind === "agent-fs") {
42
49
  const file = { path: attachment.path, orgId: attachment.orgId, driveId: attachment.driveId };
50
+ const linkBase = opts.comb === false ? null : resolveAgentFsLinkBase("attachment");
51
+ if (linkBase !== null) {
52
+ const route = agentFsFileRoute(file);
53
+ return route ? `${linkBase}/file/~/${route}` : `agent-fs:${attachment.path ?? ""}`;
54
+ }
43
55
  const combUrl = opts.comb === false ? null : buildCombFileUrl(file);
44
56
  return combUrl ?? buildAgentFsLiveUrl(file) ?? `agent-fs:${attachment.path ?? ""}`;
45
57
  }
@@ -118,7 +118,7 @@ Fetch the page as markdown: `curl -fsSL https://docs.agent-swarm.dev/llms.mdx/do
118
118
  - [Overview](https://docs.agent-swarm.dev/docs/ui)
119
119
  - [Sessions](https://docs.agent-swarm.dev/docs/ui/sessions)
120
120
  - [Configuration](https://docs.agent-swarm.dev/docs/ui/configuration)
121
- - [Comb (beta)](https://docs.agent-swarm.dev/docs/ui/comb)
121
+ - [Comb](https://docs.agent-swarm.dev/docs/ui/comb)
122
122
  - [Integrations](https://docs.agent-swarm.dev/docs/integrations)
123
123
  - [Slack Integration](https://docs.agent-swarm.dev/docs/integrations/slack)
124
124
  - [GitHub App Integration](https://docs.agent-swarm.dev/docs/integrations/github)
@@ -1,41 +0,0 @@
1
- import {
2
- isUserAllowed2,
3
- registerMessageHandler2
4
- } from "./cli-f4dbh7a5.js";
5
- import"./cli-sgqm2582.js";
6
- import"./cli-2pvzmas1.js";
7
- import"./cli-6a9qfa8b.js";
8
- import"./cli-737pyqxr.js";
9
- import"./cli-8mbc9za9.js";
10
- import"./cli-qz93y3x5.js";
11
- import"./cli-3h26v7yj.js";
12
- import"./cli-60sty27p.js";
13
- import"./cli-j9bpmykh.js";
14
- import"./cli-3ggwdtb0.js";
15
- import"./cli-aypd09nf.js";
16
- import"./cli-rd5m70gj.js";
17
- import"./cli-d20qvpr2.js";
18
- import"./cli-g9ypd5en.js";
19
- import"./cli-twctnkng.js";
20
- import"./cli-j0ppywdr.js";
21
- import"./cli-1m9x8dr5.js";
22
- import"./cli-0z1xjzwa.js";
23
- import"./cli-e8xyjvfc.js";
24
- import"./cli-wqr7cea4.js";
25
- import"./cli-13p25grh.js";
26
- import"./cli-0adyhvq2.js";
27
- import"./cli-m30k9777.js";
28
- import"./cli-s09j9vp5.js";
29
- import"./cli-6srv653c.js";
30
- import"./cli-a89zdqz9.js";
31
- import"./cli-rycdaj0a.js";
32
- import"./cli-445c1vp7.js";
33
- import"./cli-kvzgak7c.js";
34
- import"./cli-2nf3b08t.js";
35
- import"./cli-8wmj18cz.js";
36
- import"./cli-48gxjmcz.js";
37
- import"./cli-kt907da6.js";
38
- export {
39
- isUserAllowed2 as isUserAllowed,
40
- registerMessageHandler2 as registerMessageHandler
41
- };
File without changes
@@ -9,6 +9,9 @@ import {
9
9
  } from "./cli-2nf3b08t.js";
10
10
  import"./cli-445c1vp7.js";
11
11
  import"./cli-kvzgak7c.js";
12
+ import {
13
+ combineAbortSignals
14
+ } from "./cli-aphet7e8.js";
12
15
  import {
13
16
  getResponsesToolSupport,
14
17
  convertResponsesMessages,
@@ -18,9 +21,6 @@ import {
18
21
  import {
19
22
  clampOpenAIPromptCacheKey
20
23
  } from "./cli-dxqypk0n.js";
21
- import {
22
- combineAbortSignals
23
- } from "./cli-aphet7e8.js";
24
24
  import {
25
25
  registerSessionResourceCleanup,
26
26
  uuidv7
@@ -24,12 +24,12 @@ import {
24
24
  import {
25
25
  clampOpenAIPromptCacheKey
26
26
  } from "./cli-dxqypk0n.js";
27
- import {
28
- retryProviderRequest
29
- } from "./cli-w6pm0v4s.js";
30
27
  import {
31
28
  shortHash
32
29
  } from "./cli-j3xf15kq.js";
30
+ import {
31
+ retryProviderRequest
32
+ } from "./cli-w6pm0v4s.js";
33
33
  import {
34
34
  normalizeProviderError,
35
35
  formatProviderError