@desplega.ai/agent-swarm 1.161.0 → 1.162.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 (224) hide show
  1. package/README.md +3 -2
  2. package/dist/{acp-adapter-xx489vvb.js → acp-adapter-74m73ae9.js} +1 -1
  3. package/dist/{actions-ykjkn0m9.js → actions-4sr14gnt.js} +34 -31
  4. package/dist/{anthropic-messages-5qx80j9k.js → anthropic-messages-8xxrzky9.js} +22 -26
  5. package/dist/{app-0hdqa4k1.js → app-pt68fbwp.js} +13 -11
  6. package/dist/{artifact-3v5t9jtg.js → artifact-fey7dvj9.js} +3 -2
  7. package/dist/{assistant-am2g5g8a.js → assistant-cv3jytks.js} +37 -34
  8. package/dist/{azure-openai-responses-fszbkp0e.js → azure-openai-responses-f5cqsyj8.js} +15 -10
  9. package/dist/{boot-reembed-xy9t7avg.js → boot-reembed-4ht4gqth.js} +13 -11
  10. package/dist/{boot-reembed-gt5gjkt4.js → boot-reembed-nxpcvpj9.js} +12 -10
  11. package/dist/{boot-scrub-logs-9x0985y5.js → boot-scrub-logs-k93kyw8k.js} +9 -7
  12. package/dist/{boot-seeds-trv0d4vg.js → boot-seeds-1skcrs0e.js} +13 -10
  13. package/dist/{claude-adapter-jb6e1e54.js → claude-adapter-r5g0tzge.js} +23 -20
  14. package/dist/{claude-managed-setup-d1q9hv2b.js → claude-managed-setup-pkyrgshb.js} +3 -2
  15. package/dist/{claude-sdk-session-zga700s2.js → claude-sdk-session-jxyxnrwd.js} +5565 -5512
  16. package/dist/{cli-we0q7t0e.js → cli-2zy95vhm.js} +137 -64
  17. package/dist/{cli-35pj2yva.js → cli-4cwxn7gp.js} +38 -11
  18. package/dist/{cli-ad4qxm6j.js → cli-4dcpaefp.js} +2 -2
  19. package/dist/{cli-82c1pacn.js → cli-4x0csdjq.js} +71 -37
  20. package/dist/{cli-sqm3r5ps.js → cli-5bcd7sfy.js} +14 -18
  21. package/dist/{cli-d56tgd45.js → cli-5k6k8ta3.js} +2 -2
  22. package/dist/{cli-1er0rhz8.js → cli-5xhpb3kv.js} +15 -15
  23. package/dist/{cli-yrg7c73k.js → cli-7af8yhav.js} +10 -183
  24. package/dist/{cli-r6fsxc76.js → cli-7tre70zp.js} +513 -961
  25. package/dist/{cli-bh7jed3k.js → cli-8ev9xqnx.js} +34 -18
  26. package/dist/cli-bsky8g7w.js +449 -0
  27. package/dist/{cli-hn04t9db.js → cli-cebmgwsc.js} +14 -10
  28. package/dist/{cli-cfkrztza.js → cli-cvatsam7.js} +2 -2
  29. package/dist/{cli-2658f6at.js → cli-cvk6yzw5.js} +6 -8
  30. package/dist/{cli-ptr8h7kp.js → cli-eg0b3p0k.js} +5 -218
  31. package/dist/{cli-zb6xbk61.js → cli-g739zqzt.js} +4 -4
  32. package/dist/{cli-dv6cdjs2.js → cli-gd4favx9.js} +28 -7
  33. package/dist/{cli-5p3x4zna.js → cli-gpqpmns7.js} +2 -2
  34. package/dist/{cli-5nbmpy8w.js → cli-hb5c2x5y.js} +1 -1
  35. package/dist/{cli-jpf7e1zq.js → cli-hmsw1gpy.js} +3 -3
  36. package/dist/{cli-1cz0c18q.js → cli-jf7wmv1r.js} +2521 -733
  37. package/dist/{cli-qwsnmjdd.js → cli-jxs2ygaw.js} +168 -61
  38. package/dist/cli-kdkwd3yr.js +615 -0
  39. package/dist/{cli-hgmnf38m.js → cli-kh04eqwe.js} +1 -1
  40. package/dist/{cli-0383bp7f.js → cli-kp42yek0.js} +425 -90
  41. package/dist/{cli-ybgv4kw2.js → cli-mbssg1xv.js} +9 -2
  42. package/dist/{cli-jsp14b6c.js → cli-mzfth0bp.js} +38 -1
  43. package/dist/{cli-8g6mc14k.js → cli-nq7yqw7b.js} +16 -5
  44. package/dist/{cli-ee8bc46j.js → cli-nt8hecg9.js} +33 -32
  45. package/dist/{cli-9s68w752.js → cli-p0r8sxj5.js} +1 -1
  46. package/dist/{cli-58e8yr0d.js → cli-pkd6s2wy.js} +4 -2
  47. package/dist/{cli-17xpf7cm.js → cli-q24re23n.js} +6 -4
  48. package/dist/cli-qjjvzwzd.js +222 -0
  49. package/dist/cli-qrgt5bqf.js +189 -0
  50. package/dist/{cli-et8m9xsv.js → cli-rfjw9jw2.js} +22 -10
  51. package/dist/{cli-798vkbks.js → cli-rj0gqwwp.js} +2 -2
  52. package/dist/{cli-121ph74b.js → cli-rnyzn44p.js} +1 -1
  53. package/dist/{cli-vnnz5ncg.js → cli-s3hwpt9v.js} +95 -16
  54. package/dist/{cli-7119ja3w.js → cli-sptrj5f5.js} +2 -2
  55. package/dist/{cli-a8c82p2t.js → cli-sqvfa090.js} +13 -6
  56. package/dist/{cli-phfg8vy8.js → cli-swfmp1fx.js} +26 -26
  57. package/dist/{cli-nsdxtcbg.js → cli-tcen9p5w.js} +8 -5
  58. package/dist/{cli-9m03pzga.js → cli-vc9bp061.js} +5 -5
  59. package/dist/{cli-rtx9dygp.js → cli-vxe6qa4h.js} +1 -1
  60. package/dist/{cli-ecfxwnyb.js → cli-vy3ed88x.js} +1 -1
  61. package/dist/{cli-0c7ata9h.js → cli-vyqs9ya8.js} +6 -6
  62. package/dist/cli-wbvnn6tk.js +861 -0
  63. package/dist/{cli-kgd3p0bm.js → cli-wjgs5wxe.js} +6 -6
  64. package/dist/cli-wxkkd0e0.js +33 -0
  65. package/dist/{cli-akfj32vj.js → cli-xqpk3rp7.js} +5 -4
  66. package/dist/cli-zp38xvtm.js +9487 -0
  67. package/dist/{cli-3zc55ctx.js → cli-zvhd3f64.js} +2 -2
  68. package/dist/{cli-ze1d3wzv.js → cli-zxqx25nz.js} +5 -5
  69. package/dist/cli.js +32 -25
  70. package/dist/{cloudflare-workers-ai-system-one-k6fbxw9d.js → cloudflare-workers-ai-system-one-k8ddqjkq.js} +8 -6
  71. package/dist/{codex-adapter-04v97vep.js → codex-adapter-esvmnhn7.js} +13 -13
  72. package/dist/{codex-hook-20me6byw.js → codex-hook-m73jmqgx.js} +19 -3
  73. package/dist/{codex-session-runner-ax69mqq3.js → codex-session-runner-ban9excr.js} +13 -13
  74. package/dist/{commands-18gy08n1.js → commands-75413p2n.js} +9 -7
  75. package/dist/{db-d2pvjpyt.js → db-7f5pecwg.js} +11 -7
  76. package/dist/{dispatcher-8wbwyzjg.js → dispatcher-fh9at8nz.js} +33 -30
  77. package/dist/{dsh-adapter-ysqk9snv.js → dsh-adapter-wpze65d4.js} +3 -3
  78. package/dist/{e2b-esmt1r16.js → e2b-gjgdjp91.js} +10 -9
  79. package/dist/{pi-logo-animation-e9a0t68z.js → easter-egg-3d-3w5c2n4c.js} +122 -68
  80. package/dist/{execute-cmagrbrz.js → execute-f50wg9nh.js} +29 -6
  81. package/dist/{google-generative-ai-k6n5nbpp.js → google-generative-ai-jxxpnegy.js} +2 -2
  82. package/dist/{google-vertex-44xxtcvp.js → google-vertex-x7q1angm.js} +5 -5
  83. package/dist/{handlers-6b69m5kp.js → handlers-w13787bb.js} +36 -33
  84. package/dist/{hook-srnkt0hb.js → hook-46g1jrvx.js} +28 -15
  85. package/dist/{hook-k3mydf2n.js → hook-4tpts2ry.js} +20 -17
  86. package/dist/{http-hrf80ca1.js → http-19pzbfrt.js} +964 -176
  87. package/dist/{index-9rxdvj6w.js → index-8fd4dm4h.js} +34 -31
  88. package/dist/{index-1asbaj62.js → index-dqmp15bq.js} +35 -32
  89. package/dist/{index-45evq4ee.js → index-qb4q1y3e.js} +33 -30
  90. package/dist/{index-k4nj18qs.js → index-s75wc4wd.js} +12 -10
  91. package/dist/{index-xmevk2b9.js → index-scptvdxg.js} +27 -25
  92. package/dist/{keepalive-sfdz6ksp.js → keepalive-xa5ts9cd.js} +11 -9
  93. package/dist/{lead-ej91e9hw.js → lead-qp4xkvjq.js} +45 -41
  94. package/dist/{maintenance-80dnd7ke.js → maintenance-rm5885hm.js} +13 -11
  95. package/dist/{mistral-conversations-pc3ntnxt.js → mistral-conversations-rw7fbt22.js} +4 -4
  96. package/dist/{node-a5e00qhp.js → node-8zkdq42h.js} +834 -833
  97. package/dist/{oauth-refresh-sweep-m1etk2mx.js → oauth-refresh-sweep-1csdfebf.js} +10 -8
  98. package/dist/{onboard-0q8sdfqc.js → onboard-r6b78cs4.js} +10 -10
  99. package/dist/{openai-codex-responses-n7n683p2.js → openai-codex-responses-efvrgrq3.js} +4 -4
  100. package/dist/{openai-completions-jpaj5g2b.js → openai-completions-bbbrrz4z.js} +12 -8
  101. package/dist/{openai-responses-9a3nme9j.js → openai-responses-9jjyrcm1.js} +15 -10
  102. package/dist/{opencode-adapter-4wcgmvgf.js → opencode-adapter-kb3g6pkf.js} +2 -2
  103. package/dist/{otel-impl-hnqm9mxq.js → otel-impl-0y9knpky.js} +5 -5
  104. package/dist/{pi-mono-adapter-d6sqm9wr.js → pi-mono-adapter-z6s1bme2.js} +100 -19
  105. package/dist/{pricing-refresh-y60z64pm.js → pricing-refresh-mth26tgp.js} +11 -9
  106. package/dist/{rbac-roles-8pa0754b.js → rbac-roles-kwntb23q.js} +13 -9
  107. package/dist/rbac-roles-vpk4ebm8.js +45 -0
  108. package/dist/{registry-v5wytg0s.js → registry-km6t1jd2.js} +1 -1
  109. package/dist/{render-v2-418s1vj7.js → render-v2-hcm7h00y.js} +13 -11
  110. package/dist/{runtime-3pknc168.js → runtime-vfjsfsc5.js} +4 -4
  111. package/dist/{setup-yghdkj4x.js → setup-hbyzd65h.js} +2 -2
  112. package/dist/{virtual-modules-jcceevt5.js → virtual-modules-9bv7ry91.js} +9 -8
  113. package/dist/{work-objects-j1sz20v9.js → work-objects-m09sxnhe.js} +36 -33
  114. package/dist/{worker-d4gcvkjt.js → worker-0h35rvk4.js} +45 -41
  115. package/openapi.json +491 -12
  116. package/package.json +13 -6
  117. package/src/be/api-drain.ts +184 -0
  118. package/src/be/db/tasks/read.ts +31 -6
  119. package/src/be/db/tasks/write.ts +5 -4
  120. package/src/be/db.ts +42 -18
  121. package/src/be/memory/link-resolver.ts +29 -3
  122. package/src/be/migrations/192_favorites_agent_fs_path.sql +36 -0
  123. package/src/be/onboarding.ts +7 -1
  124. package/src/be/seed-prompt-templates.ts +1 -0
  125. package/src/be/swarm-config-guard.ts +35 -16
  126. package/src/be/task-citations.ts +11 -5
  127. package/src/be/users.ts +3 -0
  128. package/src/cli.tsx +3 -2
  129. package/src/comb/agent-fs.ts +39 -0
  130. package/src/comb/config.ts +37 -0
  131. package/src/comb/markers.ts +37 -0
  132. package/src/comb/review-batch.ts +486 -0
  133. package/src/comb/templates.ts +88 -0
  134. package/src/commands/context-preamble.ts +4 -1
  135. package/src/commands/hook.ts +2 -1
  136. package/src/commands/runner.ts +262 -68
  137. package/src/commands/templates.ts +10 -0
  138. package/src/extensions/contract-types.generated.ts +1 -1
  139. package/src/fs/agent-fs-provider.ts +106 -2
  140. package/src/heartbeat/heartbeat.ts +4 -0
  141. package/src/hooks/codex-hook.ts +25 -1
  142. package/src/hooks/hook.ts +13 -0
  143. package/src/hooks/pr-body-guard.ts +983 -0
  144. package/src/http/all-routes.ts +1 -0
  145. package/src/http/assets.ts +5 -17
  146. package/src/http/comb.ts +131 -0
  147. package/src/http/core.ts +2 -1
  148. package/src/http/fs.ts +9 -17
  149. package/src/http/index.ts +23 -9
  150. package/src/http/onboarding.ts +21 -2
  151. package/src/http/poll.ts +30 -3
  152. package/src/http/realtime.ts +62 -0
  153. package/src/http/request-principal.ts +22 -0
  154. package/src/http/status.ts +34 -2
  155. package/src/kv-reserved-namespaces.ts +10 -5
  156. package/src/providers/dsh-adapter.ts +1 -1
  157. package/src/providers/pi-mono-adapter.ts +123 -0
  158. package/src/rbac/legacy-policy.ts +1 -0
  159. package/src/rbac/permissions.ts +4 -0
  160. package/src/realtime/auth.ts +63 -1
  161. package/src/realtime/tickets.ts +52 -0
  162. package/src/realtime/transport.ts +51 -22
  163. package/src/scripts-runtime/types/swarm-extension.d.ts +3 -1
  164. package/src/telemetry-context.ts +175 -0
  165. package/src/telemetry-contract/EVENTS.md +81 -0
  166. package/src/telemetry-contract/SOURCE.json +9 -0
  167. package/src/telemetry-contract/catalog.json +1335 -0
  168. package/src/telemetry-contract/index.ts +60 -0
  169. package/src/telemetry-contract/types.gen.ts +1971 -0
  170. package/src/telemetry-identity.ts +129 -0
  171. package/src/telemetry-snapshot.ts +211 -0
  172. package/src/telemetry-trigger.ts +125 -0
  173. package/src/telemetry-types.typetest.ts +40 -0
  174. package/src/telemetry.ts +271 -67
  175. package/src/tests/api-drain-e2e.test.ts +249 -0
  176. package/src/tests/api-drain-worker-e2e.test.ts +216 -0
  177. package/src/tests/api-drain.test.ts +351 -0
  178. package/src/tests/comb-config-guard.test.ts +38 -0
  179. package/src/tests/comb-links.test.ts +162 -0
  180. package/src/tests/comb-markers.test.ts +68 -0
  181. package/src/tests/comb-review-batch.test.ts +770 -0
  182. package/src/tests/comb-status.test.ts +152 -0
  183. package/src/tests/db-task-read-contract.test.ts +2 -0
  184. package/src/tests/db-task-write-contract.test.ts +1 -0
  185. package/src/tests/docs-index-footprint.test.js +3 -3
  186. package/src/tests/dsh-adapter.test.ts +1 -1
  187. package/src/tests/favorites-agent-fs-path.test.ts +226 -0
  188. package/src/tests/memory-link-resolver.test.ts +34 -0
  189. package/src/tests/onboarding.test.ts +108 -0
  190. package/src/tests/pi-mono-adapter.test.ts +265 -2
  191. package/src/tests/pr-body-leaks.test.ts +789 -0
  192. package/src/tests/preload.ts +8 -0
  193. package/src/tests/rbac-engine.test.ts +19 -0
  194. package/src/tests/realtime-room-auth.test.ts +104 -1
  195. package/src/tests/realtime-tickets.test.ts +74 -0
  196. package/src/tests/realtime-transport.test.ts +131 -4
  197. package/src/tests/runner-api-drain.test.ts +301 -0
  198. package/src/tests/runner-fallback-output.test.ts +61 -0
  199. package/src/tests/runner-fenced-json-output.test.ts +178 -0
  200. package/src/tests/script-connections.test.ts +2 -0
  201. package/src/tests/send-task-requested-by.test.ts +133 -15
  202. package/src/tests/task-lifecycle-telemetry.test.ts +8 -3
  203. package/src/tests/telemetry-context.test.ts +314 -0
  204. package/src/tests/telemetry-init.test.ts +51 -19
  205. package/src/tests/telemetry-snapshot.test.ts +229 -0
  206. package/src/tests/telemetry-trigger.test.ts +200 -0
  207. package/src/tests/template-registry-helpers.ts +1 -0
  208. package/src/tests/workflow-agent-task-retry.test.ts +363 -0
  209. package/src/tests/workflow-definition-telemetry.test.ts +4 -4
  210. package/src/tools/poll-task.ts +20 -1
  211. package/src/tools/send-task.ts +9 -1
  212. package/src/tools/task-action.ts +11 -0
  213. package/src/types.ts +2 -1
  214. package/src/utils/api-drain.ts +24 -0
  215. package/src/utils/constants.ts +57 -16
  216. package/src/utils/pr-body-leaks.ts +172 -0
  217. package/src/utils/task-attachment-links.ts +14 -8
  218. package/src/workflows/recovery.ts +26 -1
  219. package/src/workflows/resume.ts +31 -4
  220. package/src/workflows/task-step-routing.ts +75 -2
  221. package/tsconfig.json +3 -0
  222. package/dist/cli-tjf4cn8z.js +0 -9119
  223. package/dist/cli-tsk3nz8a.js +0 -582
  224. package/dist/{cli-t6v2ah3r.js → cli-twrh04ap.js} +3 -3
@@ -0,0 +1,152 @@
1
+ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from "bun:test";
2
+ import { unlink } from "node:fs/promises";
3
+ import { closeDb, initDb } from "../be/db";
4
+ import { resetFileStorageProvider } from "../fs/registry";
5
+ import { buildStatusPayload } from "../http/status";
6
+
7
+ const TEST_DB_PATH = "./test-comb-status.sqlite";
8
+
9
+ const ENV_KEYS = [
10
+ "COMB_ENABLED",
11
+ "AGENT_FS_API_URL",
12
+ "AGENT_FS_PUBLIC_URL",
13
+ "AGENT_FS_LIVE_URL",
14
+ "AGENT_FS_DEFAULT_ORG_ID",
15
+ "AGENT_FS_DEFAULT_DRIVE_ID",
16
+ "API_AGENT_FS_API_KEY",
17
+ "AGENT_FS_API_KEY",
18
+ ];
19
+ const savedEnv = new Map<string, string | undefined>();
20
+
21
+ async function removeDbFiles(): Promise<void> {
22
+ for (const suffix of ["", "-wal", "-shm"]) {
23
+ await unlink(TEST_DB_PATH + suffix).catch(() => {});
24
+ }
25
+ }
26
+
27
+ function restoreEnv(): void {
28
+ for (const key of ENV_KEYS) {
29
+ const value = savedEnv.get(key);
30
+ if (value === undefined) delete process.env[key];
31
+ else process.env[key] = value;
32
+ }
33
+ }
34
+
35
+ async function comb() {
36
+ return (await buildStatusPayload()).agent_fs.comb;
37
+ }
38
+
39
+ beforeAll(async () => {
40
+ for (const key of ENV_KEYS) savedEnv.set(key, process.env[key]);
41
+ await removeDbFiles();
42
+ initDb(TEST_DB_PATH);
43
+ });
44
+
45
+ afterAll(async () => {
46
+ closeDb();
47
+ await removeDbFiles();
48
+ restoreEnv();
49
+ });
50
+
51
+ beforeEach(() => {
52
+ for (const key of ENV_KEYS) delete process.env[key];
53
+ resetFileStorageProvider();
54
+ });
55
+
56
+ afterEach(() => {
57
+ restoreEnv();
58
+ resetFileStorageProvider();
59
+ });
60
+
61
+ describe("/status agent_fs.comb", () => {
62
+ test("is off by default", async () => {
63
+ process.env.AGENT_FS_API_URL = "http://agent-fs:7433";
64
+ const result = await comb();
65
+ expect(result.enabled).toBe(false);
66
+ expect(result.api_url).toBe("http://agent-fs:7433");
67
+ });
68
+
69
+ test("stays off with the flag on but no AGENT_FS_API_URL", async () => {
70
+ process.env.COMB_ENABLED = "true";
71
+ process.env.AGENT_FS_PUBLIC_URL = "http://localhost:7433";
72
+ expect((await comb()).enabled).toBe(false);
73
+ });
74
+
75
+ test("turns on with the flag and AGENT_FS_API_URL", async () => {
76
+ process.env.AGENT_FS_API_URL = "http://agent-fs:7433";
77
+ for (const value of ["true", " TRUE ", "1"]) {
78
+ process.env.COMB_ENABLED = value;
79
+ expect((await comb()).enabled).toBe(true);
80
+ }
81
+ for (const value of ["false", "0", "yes", ""]) {
82
+ process.env.COMB_ENABLED = value;
83
+ expect((await comb()).enabled).toBe(false);
84
+ }
85
+ });
86
+
87
+ test("AGENT_FS_PUBLIC_URL wins over AGENT_FS_API_URL, without a trailing slash", async () => {
88
+ process.env.COMB_ENABLED = "true";
89
+ process.env.AGENT_FS_API_URL = "http://agent-fs:7433/";
90
+ expect((await comb()).api_url).toBe("http://agent-fs:7433");
91
+ process.env.AGENT_FS_PUBLIC_URL = "https://files.example.com/";
92
+ expect((await comb()).api_url).toBe("https://files.example.com");
93
+ });
94
+
95
+ test("ids come from AGENT_FS_DEFAULT_*, and live_url from AGENT_FS_LIVE_URL", async () => {
96
+ process.env.COMB_ENABLED = "true";
97
+ process.env.AGENT_FS_API_URL = "http://agent-fs:7433";
98
+ let result = await comb();
99
+ expect(result.org_id).toBeNull();
100
+ expect(result.drive_id).toBeNull();
101
+ expect(result.live_url).toBe("https://live.agent-fs.dev");
102
+
103
+ process.env.AGENT_FS_DEFAULT_ORG_ID = "org-1";
104
+ process.env.AGENT_FS_DEFAULT_DRIVE_ID = "drive-1";
105
+ process.env.AGENT_FS_LIVE_URL = "https://live.example.com/";
106
+ result = await comb();
107
+ expect(result).toEqual({
108
+ enabled: true,
109
+ api_url: "http://agent-fs:7433",
110
+ live_url: "https://live.example.com",
111
+ org_id: "org-1",
112
+ drive_id: "drive-1",
113
+ service_user_id: null,
114
+ });
115
+ });
116
+
117
+ test("service_user_id is the bootstrap key's agent-fs user, asked once", async () => {
118
+ let meCalls = 0;
119
+ const agentFs = Bun.serve({
120
+ port: 0,
121
+ fetch: (request) => {
122
+ if (new URL(request.url).pathname !== "/auth/me") return new Response("", { status: 404 });
123
+ meCalls++;
124
+ return request.headers.get("authorization") === "Bearer af_boot"
125
+ ? Response.json({ userId: "svc-user", email: "swarm-admin@agent-fs.local" })
126
+ : Response.json({ error: "UNAUTHORIZED" }, { status: 401 });
127
+ },
128
+ });
129
+ try {
130
+ process.env.AGENT_FS_API_URL = `http://localhost:${agentFs.port}`;
131
+ process.env.API_AGENT_FS_API_KEY = "af_boot";
132
+ process.env.AGENT_FS_DEFAULT_ORG_ID = "org-1";
133
+ process.env.AGENT_FS_DEFAULT_DRIVE_ID = "drive-1";
134
+
135
+ // Comb off: no lookup.
136
+ expect((await comb()).service_user_id).toBeNull();
137
+ expect(meCalls).toBe(0);
138
+
139
+ process.env.COMB_ENABLED = "true";
140
+ expect((await comb()).service_user_id).toBe("svc-user");
141
+ expect((await comb()).service_user_id).toBe("svc-user");
142
+ expect(meCalls).toBe(1);
143
+
144
+ // A rejected key reports null instead of failing /status.
145
+ process.env.API_AGENT_FS_API_KEY = "af_other";
146
+ resetFileStorageProvider();
147
+ expect((await comb()).service_user_id).toBeNull();
148
+ } finally {
149
+ agentFs.stop(true);
150
+ }
151
+ });
152
+ });
@@ -441,6 +441,7 @@ value:installSkill
441
441
  value:installSystemDefaultSkillsForAgent
442
442
  value:isAgentEligibleForTask
443
443
  value:isExtensionAgent
444
+ value:isLinearTrackerContextKey
444
445
  value:isPendingSlackMessage
445
446
  value:isPoolAffinityEnforcementEnabled
446
447
  value:isSettledSlackMessage
@@ -621,6 +622,7 @@ value:getTasksByStatus
621
622
  value:getTasksCount
622
623
  value:hasNonTerminalRerouteDecisionChild
623
624
  value:hasNonTerminalResumeChild
625
+ value:isLinearTrackerContextKey
624
626
  value:markFinalizedSlackRelaysDelivered
625
627
  value:markSlackRelayAttempted
626
628
  value:markSlackRelayDelivered
@@ -464,6 +464,7 @@ value:installSkill
464
464
  value:installSystemDefaultSkillsForAgent
465
465
  value:isAgentEligibleForTask
466
466
  value:isExtensionAgent
467
+ value:isLinearTrackerContextKey
467
468
  value:isPendingSlackMessage
468
469
  value:isPoolAffinityEnforcementEnabled
469
470
  value:isSettledSlackMessage
@@ -78,7 +78,7 @@ describe("docs API index footprint", () => {
78
78
  expect(tasks).toContain('"path":"/api/task-templates","method":"get"');
79
79
  });
80
80
 
81
- test("OpenAPI and generated pages keep all 392 operations", async () => {
81
+ test("OpenAPI and generated pages keep all 394 operations", async () => {
82
82
  const spec = await Bun.file(resolve(repoRoot, "openapi.json")).json();
83
83
  const operationKeys = [];
84
84
  const tags = new Set();
@@ -103,8 +103,8 @@ describe("docs API index footprint", () => {
103
103
  expect(spec.paths["/api/agents/{id}/runtime"]?.get).toBeDefined();
104
104
 
105
105
  const generatedKeys = await generatedOperationKeys();
106
- expect(operationKeys).toHaveLength(392);
107
- expect(generatedKeys).toHaveLength(392);
106
+ expect(operationKeys).toHaveLength(394);
107
+ expect(generatedKeys).toHaveLength(394);
108
108
  expect(new Set(generatedKeys)).toEqual(new Set(operationKeys));
109
109
  });
110
110
 
@@ -354,7 +354,7 @@ describe("dsh harness", () => {
354
354
  await rename(join(config.cwd, "dsh"), join(config.cwd, "npx"));
355
355
  config.env = { ...config.env, DSH_BINARY: "", PATH: config.cwd };
356
356
  await expect(new DshAdapter().createSession(config)).rejects.toThrow(
357
- "dsh CLI not found. Install @deepseek-ai/dsh@0.2.0-rc.2",
357
+ "dsh CLI not found. Install @deepseek-ai/dsh@0.2.1-alpha.1",
358
358
  );
359
359
  expect(await Bun.file(join(config.cwd, "invocation.json")).exists()).toBe(false);
360
360
  });
@@ -0,0 +1,226 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
3
+ import { unlink } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ import { Readable } from "node:stream";
6
+ import { closeDb, getDb, initDb } from "../be/db";
7
+ import { runMigrations } from "../be/migrations/runner";
8
+ import { handleFavorites } from "../http/favorites";
9
+ import { getPathSegments, parseQueryParams } from "../http/utils";
10
+ import { setRequestAuth } from "../utils/request-auth-context";
11
+
12
+ // Migration 192 adds the Comb pin item type `agent-fs-path` to user_favorites.
13
+
14
+ const TEST_DB_PATH = "./test-favorites-agent-fs-path.sqlite";
15
+ const MIGRATIONS_DIR = join(import.meta.dir, "../be/migrations");
16
+
17
+ async function removeDb(path: string): Promise<void> {
18
+ for (const suffix of ["", "-wal", "-shm"]) {
19
+ try {
20
+ await unlink(`${path}${suffix}`);
21
+ } catch {}
22
+ }
23
+ }
24
+
25
+ function jsonReq(method: string, url: string, body?: unknown) {
26
+ const raw = body === undefined ? "" : JSON.stringify(body);
27
+ const req = Readable.from(raw ? [Buffer.from(raw)] : []) as Readable & {
28
+ method: string;
29
+ url: string;
30
+ headers: Record<string, string>;
31
+ };
32
+ req.method = method;
33
+ req.url = url;
34
+ req.headers = { "content-type": "application/json" };
35
+ setRequestAuth(req, { kind: "operator", fingerprint: "op:test-comb-pins" });
36
+ return req;
37
+ }
38
+
39
+ async function call(method: string, url: string, body?: unknown) {
40
+ const req = jsonReq(method, url, body);
41
+ let statusCode = 200;
42
+ const chunks: string[] = [];
43
+ const res = {
44
+ setHeader: () => {},
45
+ writeHead: (code: number) => {
46
+ statusCode = code;
47
+ },
48
+ end: (chunk?: string) => {
49
+ if (chunk) chunks.push(chunk);
50
+ },
51
+ };
52
+ await handleFavorites(
53
+ req,
54
+ res as never,
55
+ getPathSegments(req.url),
56
+ parseQueryParams(req.url),
57
+ undefined,
58
+ );
59
+ return { statusCode, body: chunks.length > 0 ? JSON.parse(chunks.join("")) : null };
60
+ }
61
+
62
+ describe("favorites: agent-fs-path item type (fresh DB)", () => {
63
+ beforeAll(async () => {
64
+ await removeDb(TEST_DB_PATH);
65
+ initDb(TEST_DB_PATH);
66
+ });
67
+
68
+ afterAll(async () => {
69
+ closeDb();
70
+ await removeDb(TEST_DB_PATH);
71
+ });
72
+
73
+ test("PUT pins a folder and a file, GET lists them back", async () => {
74
+ for (const itemId of ["org/drive/comb-qa/", "org/drive/comb-qa/my notes 100%.md"]) {
75
+ const put = await call("PUT", "/api/favorites", {
76
+ itemType: "agent-fs-path",
77
+ itemId,
78
+ favorite: true,
79
+ });
80
+ expect(put).toMatchObject({
81
+ statusCode: 200,
82
+ body: { favorite: true, itemType: "agent-fs-path", itemId },
83
+ });
84
+ }
85
+
86
+ const list = await call("GET", "/api/favorites?itemType=agent-fs-path");
87
+ expect(list.statusCode).toBe(200);
88
+ expect([...list.body.favoriteIds].sort()).toEqual([
89
+ "org/drive/comb-qa/",
90
+ "org/drive/comb-qa/my notes 100%.md",
91
+ ]);
92
+ for (const row of list.body.favorites) expect(row.itemType).toBe("agent-fs-path");
93
+
94
+ // Other item types do not see the pins.
95
+ expect((await call("GET", "/api/favorites?itemType=page")).body.favoriteIds).toEqual([]);
96
+ });
97
+
98
+ test("PUT with favorite=false unpins", async () => {
99
+ await call("PUT", "/api/favorites", {
100
+ itemType: "agent-fs-path",
101
+ itemId: "org/drive/comb-qa/",
102
+ favorite: false,
103
+ });
104
+ const list = await call("GET", "/api/favorites?itemType=agent-fs-path");
105
+ expect(list.body.favoriteIds).toEqual(["org/drive/comb-qa/my notes 100%.md"]);
106
+ });
107
+
108
+ test("GET lists pins newest first", async () => {
109
+ const db = getDb();
110
+ const ids = ["org/drive/order/a.md", "org/drive/order/b.md", "org/drive/order/c.md"];
111
+ for (const [i, itemId] of ids.entries()) {
112
+ await call("PUT", "/api/favorites", { itemType: "agent-fs-path", itemId, favorite: true });
113
+ db.run(
114
+ "UPDATE user_favorites SET lastUpdatedAt = ? WHERE itemType = 'agent-fs-path' AND itemId = ?",
115
+ [`2030-01-0${i + 1}T00:00:00.000Z`, itemId],
116
+ );
117
+ }
118
+ const list = await call("GET", "/api/favorites?itemType=agent-fs-path");
119
+ const listed = list.body.favoriteIds.filter((id: string) => id.startsWith("org/drive/order/"));
120
+ expect(listed).toEqual([...ids].reverse());
121
+ });
122
+
123
+ test("an unknown item type is still rejected", async () => {
124
+ const put = await call("PUT", "/api/favorites", {
125
+ itemType: "agent-fs-file",
126
+ itemId: "org/drive/a.md",
127
+ favorite: true,
128
+ });
129
+ expect(put.statusCode).toBe(400);
130
+ const list = await call("GET", "/api/favorites?itemType=agent-fs-file");
131
+ expect(list.statusCode).toBe(400);
132
+ });
133
+ });
134
+
135
+ describe("migration 192 (existing DB)", () => {
136
+ // In memory: the full migration chain on a file DB passed the 10 s test timeout on a loaded CI shard.
137
+ test("keeps every favorite row and the indexes, and widens the CHECK", async () => {
138
+ const db = new Database(":memory:");
139
+ try {
140
+ runMigrations(db);
141
+
142
+ // Roll back to the pre-192 shape: re-run 116 on the empty table (it
143
+ // rebuilds user_favorites with the old CHECK and the same indexes).
144
+ db.exec(await Bun.file(join(MIGRATIONS_DIR, "116_favorite_principal_scope.sql")).text());
145
+ db.run("DELETE FROM _migrations WHERE version = 192");
146
+ expect(() =>
147
+ db.run(
148
+ "INSERT INTO user_favorites (favoriteScope, itemType, itemId) VALUES ('operator:x', 'agent-fs-path', 'o/d/a.md')",
149
+ ),
150
+ ).toThrow(/CHECK constraint failed/);
151
+
152
+ db.run("INSERT INTO users (id, name) VALUES ('user-1', 'Pin User')");
153
+ const insert = db.prepare(
154
+ `INSERT INTO user_favorites
155
+ (id, favoriteScope, userId, itemType, itemId, createdAt, lastUpdatedAt, created_by, updated_by)
156
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
157
+ );
158
+ // Page, workflow, and schedule favorites for a user scope and the
159
+ // operator scope, with distinct timestamps and audit values per row.
160
+ let n = 0;
161
+ for (const [scope, userId] of [
162
+ ["user:user-1", "user-1"],
163
+ ["operator", null],
164
+ ] as const) {
165
+ for (const itemType of ["page", "workflow", "schedule"]) {
166
+ n++;
167
+ insert.run(
168
+ `fav-${n}`,
169
+ scope,
170
+ userId,
171
+ itemType,
172
+ `${itemType}-${n}`,
173
+ `2026-09-0${n}T00:00:00.000Z`,
174
+ `2026-09-1${n}T00:00:00.000Z`,
175
+ n % 2 ? "op:abc" : null,
176
+ n % 3 ? userId : null,
177
+ );
178
+ }
179
+ }
180
+ const before = db.query("SELECT * FROM user_favorites ORDER BY id").all();
181
+ expect(before).toHaveLength(6);
182
+
183
+ runMigrations(db);
184
+
185
+ expect(db.query("SELECT version FROM _migrations WHERE version = 192").get()).not.toBeNull();
186
+ expect(db.query("SELECT * FROM user_favorites ORDER BY id").all()).toEqual(before);
187
+
188
+ const indexes = db
189
+ .query<{ name: string; unique: number }, []>("PRAGMA index_list(user_favorites)")
190
+ .all();
191
+ expect(indexes.map((index) => index.name)).toEqual(
192
+ expect.arrayContaining([
193
+ "idx_user_favorites_scope_type",
194
+ "idx_user_favorites_user_type",
195
+ "idx_user_favorites_item",
196
+ ]),
197
+ );
198
+ // The UNIQUE (favoriteScope, itemType, itemId) constraint survives.
199
+ const unique = indexes
200
+ .filter((index) => index.unique === 1)
201
+ .map((index) =>
202
+ db
203
+ .query<{ name: string }, []>(`PRAGMA index_info("${index.name}")`)
204
+ .all()
205
+ .map((column) => column.name),
206
+ );
207
+ expect(unique).toContainEqual(["favoriteScope", "itemType", "itemId"]);
208
+
209
+ // The new type is accepted, an unknown type is still rejected.
210
+ db.run(
211
+ "INSERT INTO user_favorites (favoriteScope, itemType, itemId) VALUES ('operator', 'agent-fs-path', 'o/d/comb-qa/')",
212
+ );
213
+ expect(() =>
214
+ db.run(
215
+ "INSERT INTO user_favorites (favoriteScope, itemType, itemId) VALUES ('operator', 'bogus', 'x')",
216
+ ),
217
+ ).toThrow(/CHECK constraint failed/);
218
+
219
+ // The user FK still cascades.
220
+ db.run("DELETE FROM users WHERE id = 'user-1'");
221
+ expect(db.query("SELECT id FROM user_favorites WHERE userId = 'user-1'").all()).toEqual([]);
222
+ } finally {
223
+ db.close();
224
+ }
225
+ });
226
+ });
@@ -61,6 +61,40 @@ describe("resolveLinks", () => {
61
61
  expect(fsLinks[0]!.resolver).toBe("agent-fs-path");
62
62
  });
63
63
 
64
+ test("a dashboard (Comb) file link resolves to the same target as the live URL", () => {
65
+ const route =
66
+ "/file/~/648a5f3c-35c8-4f11-8673-b89de52cd6bd/2faf73ba-4eee-4472-8b3b-359c4ed6bfbb/thoughts/final%20plan.md";
67
+ const savedAppUrl = process.env.APP_URL;
68
+ const savedDashboardUrl = process.env.DASHBOARD_URL;
69
+ process.env.APP_URL = "https://swarm.example.com/dash/";
70
+ delete process.env.DASHBOARD_URL;
71
+ try {
72
+ const fsTargets = (content: string) =>
73
+ resolveLinks(content)
74
+ .filter((l) => l.linkType === "agent-fs-file")
75
+ .map((l) => ({ targetId: l.targetId, metadata: l.metadata }));
76
+ const live = fsTargets(`Plan at https://live.agent-fs.dev${route}`);
77
+ expect(live).toHaveLength(1);
78
+ expect(fsTargets(`Plan at https://swarm.example.com/dash${route}`)).toEqual(live);
79
+ expect(fsTargets(`Plan at http://localhost:5274${route}`)).toEqual(live);
80
+ // Another host, or the app host without its path prefix, is not a file link.
81
+ expect(fsTargets(`Plan at https://other.example.com${route}`)).toEqual([]);
82
+ expect(fsTargets(`Plan at https://swarm.example.com${route}`)).toEqual([]);
83
+ // A known host must start the host name.
84
+ expect(fsTargets(`Plan at https://evil-swarm.example.com/dash${route}`)).toEqual([]);
85
+ expect(fsTargets(`Plan at https://notlocalhost:9${route}`)).toEqual([]);
86
+ expect(fsTargets(`Plan at https://myagent-fs${route}`)).toEqual([]);
87
+ // Hosts are case-insensitive.
88
+ expect(fsTargets(`Plan at https://SWARM.Example.com/dash${route}`)).toEqual(live);
89
+ expect(fsTargets(`Plan at https://LIVE.AGENT-FS.DEV${route}`)).toEqual(live);
90
+ } finally {
91
+ if (savedAppUrl === undefined) delete process.env.APP_URL;
92
+ else process.env.APP_URL = savedAppUrl;
93
+ if (savedDashboardUrl === undefined) delete process.env.DASHBOARD_URL;
94
+ else process.env.DASHBOARD_URL = savedDashboardUrl;
95
+ }
96
+ });
97
+
64
98
  test("extracts agent-ui page links", () => {
65
99
  const links = resolveLinks(
66
100
  "See app.agent-swarm.dev/pages/abc12345-1234-1234-1234-123456789abc",
@@ -613,6 +613,114 @@ describe("onboarding memory probe", () => {
613
613
  });
614
614
  });
615
615
 
616
+ test("splits a 404 into a missing path and a missing model or deployment", async () => {
617
+ const cases = [
618
+ {
619
+ // Azure without `/openai/v1`.
620
+ body: { error: { code: "404", message: "Resource not found" } },
621
+ errorClass: "endpoint",
622
+ error: "The endpoint path was not found (404). Check the base URL.",
623
+ },
624
+ {
625
+ // A proxy or server page with no JSON body.
626
+ body: "404 page not found",
627
+ errorClass: "endpoint",
628
+ error: "The endpoint path was not found (404). Check the base URL.",
629
+ },
630
+ {
631
+ body: {
632
+ error: {
633
+ message: "The model `nope` does not exist or you do not have access to it.",
634
+ type: "invalid_request_error",
635
+ code: "model_not_found",
636
+ },
637
+ },
638
+ errorClass: "model",
639
+ error: "The endpoint could not find the embedding model or deployment (404).",
640
+ },
641
+ {
642
+ body: {
643
+ error: {
644
+ code: "DeploymentNotFound",
645
+ message: "The API deployment for this resource does not exist.",
646
+ },
647
+ },
648
+ errorClass: "model",
649
+ error: "The endpoint could not find the embedding model or deployment (404).",
650
+ },
651
+ ] as const;
652
+
653
+ for (const testCase of cases) {
654
+ const fake = startEmbeddingServer(() =>
655
+ typeof testCase.body === "string"
656
+ ? new Response(testCase.body, { status: 404 })
657
+ : Response.json(testCase.body, { status: 404 }),
658
+ );
659
+ try {
660
+ const response = await request("/api/onboarding/memory", {
661
+ method: "POST",
662
+ body: {
663
+ preset: "azure",
664
+ baseUrl: fake.baseUrl,
665
+ model: "my-embedding-deployment",
666
+ apiKey: "azure-resource-key",
667
+ },
668
+ });
669
+ expect(response.status).toBe(200);
670
+ expect(await response.json()).toMatchObject({
671
+ ok: false,
672
+ errorClass: testCase.errorClass,
673
+ error: testCase.error,
674
+ });
675
+ } finally {
676
+ fake.server.stop(true);
677
+ }
678
+ }
679
+ // A failed step keeps its first error class.
680
+ expect((await getState()).steps.memory).toMatchObject({
681
+ status: "failed",
682
+ errorClass: "endpoint",
683
+ });
684
+ });
685
+
686
+ test("the azure preset saves its base URL and deployment name", async () => {
687
+ let path = "";
688
+ let model = "";
689
+ const fake = Bun.serve({
690
+ hostname: "127.0.0.1",
691
+ port: 0,
692
+ async fetch(req) {
693
+ path = new URL(req.url).pathname;
694
+ model = ((await req.json()) as { model: string }).model;
695
+ return embeddingResponse(512);
696
+ },
697
+ });
698
+ try {
699
+ const baseUrl = `http://127.0.0.1:${fake.port}/openai/v1`;
700
+ const response = await request("/api/onboarding/memory", {
701
+ method: "POST",
702
+ body: {
703
+ preset: "azure",
704
+ baseUrl,
705
+ model: "my-embedding-deployment",
706
+ apiKey: "azure-resource-key",
707
+ },
708
+ });
709
+ expect(await response.json()).toMatchObject({ ok: true, dimensions: 512 });
710
+ expect(path).toBe("/openai/v1/embeddings");
711
+ expect(model).toBe("my-embedding-deployment");
712
+ const configs = await getSwarmConfigs({ scope: "global" });
713
+ const saved = Object.fromEntries(configs.map((config) => [config.key, config.value]));
714
+ expect(saved).toMatchObject({
715
+ EMBEDDING_API_BASE_URL: baseUrl,
716
+ EMBEDDING_MODEL: "my-embedding-deployment",
717
+ });
718
+ expect((await getState()).steps.memory).toMatchObject({ status: "done", method: "azure" });
719
+ } finally {
720
+ fake.stop(true);
721
+ }
722
+ });
723
+
616
724
  test("a wrong dimension keeps memory done after a successful probe", async () => {
617
725
  let dimensions = 512;
618
726
  const fake = startEmbeddingServer(() => embeddingResponse(dimensions));