@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
@@ -78,6 +78,7 @@ describe("initTelemetry", () => {
78
78
  expect(writes).toEqual([
79
79
  { key: "telemetry_installation_id", value: id as string },
80
80
  { key: "telemetry_installed_at", value: installedAt as string },
81
+ { key: "telemetry_org_id", value: expect.stringMatching(/^org_[0-9a-f]{16}$/) },
81
82
  ]);
82
83
  });
83
84
 
@@ -96,7 +97,10 @@ describe("initTelemetry", () => {
96
97
  const id = _getInstallationIdForTests();
97
98
  expect(id).not.toBeNull();
98
99
  expect(id).toMatch(/^ephemeral_[0-9a-f]{16}$/);
99
- expect(writes).toEqual([]);
100
+ // Only the org ID is written (the install ID could not be).
101
+ expect(writes).toEqual([
102
+ { key: "telemetry_org_id", value: expect.stringMatching(/^org_[0-9a-f]{16}$/) },
103
+ ]);
100
104
  // Config access is failing — nowhere durable to persist an anchor, so
101
105
  // leave it null rather than faking a fresh mint.
102
106
  expect(_getInstalledAtForTests()).toBeNull();
@@ -121,7 +125,10 @@ describe("initTelemetry", () => {
121
125
  // installed_at failure must not unwind and discard the installationId
122
126
  // write that already succeeded.
123
127
  expect(id).toMatch(/^install_[0-9a-f]{16}$/);
124
- expect(writes).toEqual([{ key: "telemetry_installation_id", value: id as string }]);
128
+ expect(writes).toEqual([
129
+ { key: "telemetry_installation_id", value: id as string },
130
+ { key: "telemetry_org_id", value: expect.stringMatching(/^org_[0-9a-f]{16}$/) },
131
+ ]);
125
132
  // No fabricated/unpersisted anchor emitted this session.
126
133
  expect(_getInstalledAtForTests()).toBeNull();
127
134
  });
@@ -141,7 +148,10 @@ describe("initTelemetry", () => {
141
148
  // now() as a stand-in install date — that back-fills a wrong date
142
149
  // instead of an honestly-absent one. No write, no fabricated anchor.
143
150
  expect(_getInstallationIdForTests()).toBe(existing);
144
- expect(writes).toEqual([]);
151
+ // No install-anchor write; the org ID is the only thing minted.
152
+ expect(writes).toEqual([
153
+ { key: "telemetry_org_id", value: expect.stringMatching(/^org_[0-9a-f]{16}$/) },
154
+ ]);
145
155
  expect(_getInstalledAtForTests()).toBeNull();
146
156
  });
147
157
 
@@ -163,7 +173,9 @@ describe("initTelemetry", () => {
163
173
  );
164
174
  expect(_getInstallationIdForTests()).toBe(existing);
165
175
  expect(_getInstalledAtForTests()).toBe(existingInstalledAt);
166
- expect(writes).toEqual([]);
176
+ expect(writes).toEqual([
177
+ { key: "telemetry_org_id", value: expect.stringMatching(/^org_[0-9a-f]{16}$/) },
178
+ ]);
167
179
  });
168
180
  });
169
181
 
@@ -244,6 +256,7 @@ describe("initTelemetry", () => {
244
256
  return undefined;
245
257
  },
246
258
  async () => {},
259
+ { generateIfMissing: true },
247
260
  );
248
261
 
249
262
  telemetry.onboarding("step_completed", {
@@ -370,7 +383,9 @@ describe("initTelemetry", () => {
370
383
  { generateIfMissing: true },
371
384
  );
372
385
  expect(_getInstallationIdForTests()).toBe(existing);
373
- expect(writesB).toEqual([]);
386
+ expect(writesB).toEqual([
387
+ { key: "telemetry_org_id", value: expect.stringMatching(/^org_[0-9a-f]{16}$/) },
388
+ ]);
374
389
  });
375
390
 
376
391
  describe("_resolveCloudMode (URL → is_cloud)", () => {
@@ -470,8 +485,9 @@ describe("initTelemetry", () => {
470
485
  process.env.E2B_SANDBOX_ID = "sbx_test123";
471
486
  await initTelemetry(
472
487
  "api-server",
473
- async () => "install_e2b_test",
488
+ async (key) => (key === "telemetry_installation_id" ? "install_e2b_test" : undefined),
474
489
  async () => {},
490
+ { generateIfMissing: true },
475
491
  );
476
492
 
477
493
  track({ event: "server.started", properties: { port: 3013 } });
@@ -486,8 +502,9 @@ describe("initTelemetry", () => {
486
502
  delete process.env.E2B_SANDBOX_ID;
487
503
  await initTelemetry(
488
504
  "api-server",
489
- async () => "install_no_e2b",
505
+ async (key) => (key === "telemetry_installation_id" ? "install_no_e2b" : undefined),
490
506
  async () => {},
507
+ { generateIfMissing: true },
491
508
  );
492
509
 
493
510
  track({ event: "test.event", properties: {} });
@@ -501,8 +518,9 @@ describe("initTelemetry", () => {
501
518
  process.env.E2B_SANDBOX_ID = "sbx_override_test";
502
519
  await initTelemetry(
503
520
  "api-server",
504
- async () => "install_e2b_override",
521
+ async (key) => (key === "telemetry_installation_id" ? "install_e2b_override" : undefined),
505
522
  async () => {},
523
+ { generateIfMissing: true },
506
524
  );
507
525
 
508
526
  track({ event: "test.event", properties: { is_e2b: false } });
@@ -534,8 +552,9 @@ describe("initTelemetry", () => {
534
552
  process.env.MCP_BASE_URL = "https://agent-swarm-mcp.desplega.sh";
535
553
  await initTelemetry(
536
554
  "worker",
537
- async () => "install_cloud_test",
555
+ async (key) => (key === "telemetry_installation_id" ? "install_cloud_test" : undefined),
538
556
  async () => {},
557
+ { generateIfMissing: true },
539
558
  );
540
559
 
541
560
  track({ event: "server.started", properties: { port: 3013 } });
@@ -553,8 +572,9 @@ describe("initTelemetry", () => {
553
572
  process.env.MCP_BASE_URL = "http://localhost:3013";
554
573
  await initTelemetry(
555
574
  "worker",
556
- async () => "install_self_test",
575
+ async (key) => (key === "telemetry_installation_id" ? "install_self_test" : undefined),
557
576
  async () => {},
577
+ { generateIfMissing: true },
558
578
  );
559
579
 
560
580
  track({ event: "test.event", properties: {} });
@@ -569,8 +589,9 @@ describe("initTelemetry", () => {
569
589
  delete process.env.MCP_BASE_URL;
570
590
  await initTelemetry(
571
591
  "api-server",
572
- async () => "install_no_url",
592
+ async (key) => (key === "telemetry_installation_id" ? "install_no_url" : undefined),
573
593
  async () => {},
594
+ { generateIfMissing: true },
574
595
  );
575
596
 
576
597
  track({ event: "test.event", properties: {} });
@@ -590,8 +611,10 @@ describe("initTelemetry", () => {
590
611
  process.env.SWARM_CLOUD = "true";
591
612
  await initTelemetry(
592
613
  "api-server",
593
- async () => "install_hosted_cloud_test",
614
+ async (key) =>
615
+ key === "telemetry_installation_id" ? "install_hosted_cloud_test" : undefined,
594
616
  async () => {},
617
+ { generateIfMissing: true },
595
618
  );
596
619
 
597
620
  track({ event: "server.started", properties: { port: 3013 } });
@@ -611,8 +634,9 @@ describe("initTelemetry", () => {
611
634
  process.env.MCP_BASE_URL = "https://agent-swarm-mcp.desplega.sh";
612
635
  await initTelemetry(
613
636
  "worker",
614
- async () => "install_override_test",
637
+ async (key) => (key === "telemetry_installation_id" ? "install_override_test" : undefined),
615
638
  async () => {},
639
+ { generateIfMissing: true },
616
640
  );
617
641
 
618
642
  track({
@@ -645,8 +669,9 @@ describe("initTelemetry", () => {
645
669
  test("includes the package version on every event and ignores caller overrides", async () => {
646
670
  await initTelemetry(
647
671
  "worker",
648
- async () => "install_version_test",
672
+ async (key) => (key === "telemetry_installation_id" ? "install_version_test" : undefined),
649
673
  async () => {},
674
+ { generateIfMissing: true },
650
675
  );
651
676
 
652
677
  track({ event: "test.event", properties: { swarmVersion: "spoofed" } });
@@ -682,8 +707,9 @@ describe("initTelemetry", () => {
682
707
  process.env.NODE_ENV = "development";
683
708
  await initTelemetry(
684
709
  "api-server",
685
- async () => "install_default_env",
710
+ async (key) => (key === "telemetry_installation_id" ? "install_default_env" : undefined),
686
711
  async () => {},
712
+ { generateIfMissing: true },
687
713
  );
688
714
 
689
715
  track({ event: "test.event", properties: {} });
@@ -698,8 +724,9 @@ describe("initTelemetry", () => {
698
724
  process.env.DESPLEGA_TELEMETRY_ENV = "development";
699
725
  await initTelemetry(
700
726
  "api-server",
701
- async () => "install_explicit_env",
727
+ async (key) => (key === "telemetry_installation_id" ? "install_explicit_env" : undefined),
702
728
  async () => {},
729
+ { generateIfMissing: true },
703
730
  );
704
731
 
705
732
  track({ event: "test.event", properties: {} });
@@ -713,8 +740,9 @@ describe("initTelemetry", () => {
713
740
  process.env.NODE_ENV = "test";
714
741
  await initTelemetry(
715
742
  "api-server",
716
- async () => "install_test_env",
743
+ async (key) => (key === "telemetry_installation_id" ? "install_test_env" : undefined),
717
744
  async () => {},
745
+ { generateIfMissing: true },
718
746
  );
719
747
 
720
748
  track({ event: "test.event", properties: {} });
@@ -915,8 +943,10 @@ describe("initTelemetry", () => {
915
943
  test("integration.connected emits only allowlisted provider values", async () => {
916
944
  await initTelemetry(
917
945
  "api-server",
918
- async () => "install_integration_test",
946
+ async (key) =>
947
+ key === "telemetry_installation_id" ? "install_integration_test" : undefined,
919
948
  async () => {},
949
+ { generateIfMissing: true },
920
950
  );
921
951
 
922
952
  emitIntegrationConnected("code_repo", "https://github.com/desplega/agent-swarm", true);
@@ -935,8 +965,10 @@ describe("initTelemetry", () => {
935
965
  test("integration.connected omits a free-form PII-looking provider", async () => {
936
966
  await initTelemetry(
937
967
  "api-server",
938
- async () => "install_integration_test",
968
+ async (key) =>
969
+ key === "telemetry_installation_id" ? "install_integration_test" : undefined,
939
970
  async () => {},
971
+ { generateIfMissing: true },
940
972
  );
941
973
 
942
974
  emitIntegrationConnected("other", "someone@example.com", false);
@@ -0,0 +1,229 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import { unlink } from "node:fs/promises";
3
+ import {
4
+ closeDb,
5
+ createAgent,
6
+ createTaskExtended,
7
+ createUser,
8
+ getDbClient,
9
+ initDb,
10
+ } from "../be/db";
11
+ import { attachRole, detachRole } from "../be/rbac-roles";
12
+ import { linkIdentity, mintToken, revokeToken } from "../be/users";
13
+ import { _resetTelemetryStateForTests, initTelemetry } from "../telemetry";
14
+ import { collectOrgSnapshot, emitOrgSnapshotIfDue, LAST_SNAPSHOT_KEY } from "../telemetry-snapshot";
15
+
16
+ // initTelemetry no-ops when ANONYMIZED_TELEMETRY=false, and the CI env may set it.
17
+ process.env.ANONYMIZED_TELEMETRY = "true";
18
+
19
+ const TEST_DB_PATH = "./test-telemetry-snapshot.sqlite";
20
+ const ACTOR = { kind: "system", id: "telemetry-snapshot-test" } as const;
21
+ const DAY_MS = 24 * 60 * 60_000;
22
+
23
+ async function removeTestDb(): Promise<void> {
24
+ for (const suffix of ["", "-wal", "-shm"]) {
25
+ try {
26
+ await unlink(TEST_DB_PATH + suffix);
27
+ } catch {
28
+ // File does not exist.
29
+ }
30
+ }
31
+ }
32
+
33
+ function memoryConfig(initial: Record<string, string> = {}) {
34
+ const store = new Map(Object.entries(initial));
35
+ return {
36
+ store,
37
+ get: async (key: string) => store.get(key),
38
+ set: async (key: string, value: string) => {
39
+ store.set(key, value);
40
+ },
41
+ };
42
+ }
43
+
44
+ /** Fixture: 3 active users, 1 suspended, 1 invited, 2 agents. */
45
+ async function seedFixture() {
46
+ const admin = await createUser({ name: "Admin Person", email: "admin.person@corp.example" });
47
+ const tokenUser = await createUser({ name: "Token Person" });
48
+ const revokedUser = await createUser({ name: "Revoked Person" });
49
+ const suspended = await createUser({
50
+ name: "Suspended Person",
51
+ email: "suspended@corp.example",
52
+ status: "suspended",
53
+ });
54
+ const invited = await createUser({ name: "Invited Person", status: "invited" });
55
+ for (const user of [tokenUser, revokedUser, suspended, invited]) {
56
+ await detachRole(user.id, "admin");
57
+ await attachRole(user.id, "requester");
58
+ }
59
+
60
+ await mintToken(tokenUser.id, "live", ACTOR);
61
+ const revoked = await mintToken(revokedUser.id, "revoked", ACTOR);
62
+ const revokedRow = await getDbClient().get<{ id: string }>(
63
+ "SELECT id FROM user_tokens WHERE userId = ?",
64
+ [revokedUser.id],
65
+ );
66
+ await revokeToken(revokedRow?.id ?? revoked.tokenId, ACTOR);
67
+
68
+ await linkIdentity(admin.id, "slack", "U-ADMIN", ACTOR);
69
+ await linkIdentity(admin.id, "github", "admin-gh", ACTOR);
70
+ await linkIdentity(tokenUser.id, "composio", "c-1", ACTOR);
71
+ await linkIdentity(tokenUser.id, "kapso", "k-1", ACTOR);
72
+ await linkIdentity(revokedUser.id, "jira", "j-1", ACTOR);
73
+ await linkIdentity(revokedUser.id, "linear", "l-1", ACTOR);
74
+ await linkIdentity(revokedUser.id, "gitlab", "g-1", ACTOR);
75
+ // A suspended user's links and an invited user's links do not count.
76
+ await linkIdentity(suspended.id, "slack", "U-SUSPENDED", ACTOR);
77
+
78
+ await createAgent({
79
+ id: "aaaa0000-0000-4000-8000-0000000000a1",
80
+ name: "A1",
81
+ isLead: true,
82
+ status: "idle",
83
+ });
84
+ await createAgent({
85
+ id: "bbbb0000-0000-4000-8000-0000000000b1",
86
+ name: "B1",
87
+ isLead: false,
88
+ status: "idle",
89
+ });
90
+
91
+ // Two recent tasks by one user, one recent by another, one old by a third.
92
+ const recentA = await createTaskExtended("recent a", { requestedByUserId: admin.id });
93
+ await createTaskExtended("recent a again", { requestedByUserId: admin.id });
94
+ await createTaskExtended("recent b", { requestedByUserId: revokedUser.id });
95
+ const old = await createTaskExtended("old", { requestedByUserId: tokenUser.id });
96
+ await getDbClient().run("UPDATE agent_tasks SET createdAt = ? WHERE id = ?", [
97
+ new Date(Date.now() - 10 * DAY_MS).toISOString(),
98
+ old.id,
99
+ ]);
100
+ expect(recentA.id).toBeTruthy();
101
+ return { admin, tokenUser, revokedUser, suspended, invited };
102
+ }
103
+
104
+ describe("org snapshot", () => {
105
+ const originalFetch = globalThis.fetch;
106
+ let sent: Array<{ event: string; properties: Record<string, unknown> }>;
107
+
108
+ beforeEach(async () => {
109
+ closeDb();
110
+ await removeTestDb();
111
+ initDb(TEST_DB_PATH);
112
+ _resetTelemetryStateForTests();
113
+ process.env.ANONYMIZED_TELEMETRY = "true";
114
+ sent = [];
115
+ globalThis.fetch = (async (_url: string, init?: { body?: string }) => {
116
+ if (init?.body) sent.push(JSON.parse(init.body));
117
+ return new Response(null, { status: 204 });
118
+ }) as typeof fetch;
119
+ });
120
+
121
+ afterEach(async () => {
122
+ globalThis.fetch = originalFetch;
123
+ process.env.ANONYMIZED_TELEMETRY = "true";
124
+ _resetTelemetryStateForTests();
125
+ closeDb();
126
+ await removeTestDb();
127
+ });
128
+
129
+ async function boot(config = memoryConfig({ telemetry_installation_id: "install_snapshot" })) {
130
+ await initTelemetry("api-server", config.get, config.set, { generateIfMissing: true });
131
+ return config;
132
+ }
133
+
134
+ const tick = () => new Promise((resolve) => setTimeout(resolve, 0));
135
+
136
+ test("counts match the fixture rows", async () => {
137
+ await seedFixture();
138
+ expect(await collectOrgSnapshot()).toEqual({
139
+ users_total: 3,
140
+ users_suspended: 1,
141
+ users_admin: 1,
142
+ users_with_login: 2, // the admin's email and the live token; the revoked one has neither
143
+ users_linked_slack: 1,
144
+ users_linked_github: 1,
145
+ users_linked_gitlab: 1,
146
+ users_linked_linear: 1,
147
+ users_linked_jira: 1,
148
+ users_linked_other: 1, // composio + kapso on one user count once
149
+ users_active_7d: 2,
150
+ agents_total: 2,
151
+ });
152
+ });
153
+
154
+ test("an empty install snapshots all zeros", async () => {
155
+ const snapshot = await collectOrgSnapshot();
156
+ expect(Object.values(snapshot).every((n) => n === 0)).toBe(true);
157
+ });
158
+
159
+ test("the payload has no @, no email local part, no name and no raw user ID", async () => {
160
+ const { admin, tokenUser } = await seedFixture();
161
+ const config = await boot();
162
+ expect(await emitOrgSnapshotIfDue({ getConfig: config.get, setConfig: config.set })).toBe(true);
163
+ await tick();
164
+
165
+ // Seeding fires task events after boot; only the snapshot is under test.
166
+ const snapshots = sent.filter((e) => e.event === "org.snapshot");
167
+ expect(snapshots).toHaveLength(1);
168
+ expect(snapshots[0]?.properties).toMatchObject({ users_total: 3, agents_total: 2 });
169
+ const text = JSON.stringify(snapshots[0]);
170
+ expect(text).not.toContain("@");
171
+ for (const secret of [admin.id, tokenUser.id, "admin.person", "Admin Person", "corp.example"]) {
172
+ expect(text).not.toContain(secret);
173
+ }
174
+ });
175
+
176
+ test("one send per 24 hours, across a simulated restart", async () => {
177
+ await seedFixture();
178
+ const config = await boot();
179
+ const t0 = Date.parse("2026-10-01T00:00:00.000Z");
180
+ const deps = (now: number) => ({
181
+ getConfig: config.get,
182
+ setConfig: config.set,
183
+ now: () => now,
184
+ });
185
+
186
+ expect(await emitOrgSnapshotIfDue(deps(t0))).toBe(true);
187
+ expect(config.store.get(LAST_SNAPSHOT_KEY)).toBe(new Date(t0).toISOString());
188
+ expect(await emitOrgSnapshotIfDue(deps(t0 + 60_000))).toBe(false);
189
+
190
+ // Restart: module state is gone, the stored timestamp is what remains.
191
+ _resetTelemetryStateForTests();
192
+ await boot(config);
193
+ expect(await emitOrgSnapshotIfDue(deps(t0 + DAY_MS - 60_000))).toBe(false);
194
+ expect(await emitOrgSnapshotIfDue(deps(t0 + DAY_MS + 60_000))).toBe(true);
195
+ await tick();
196
+ expect(sent.filter((e) => e.event === "org.snapshot")).toHaveLength(2);
197
+ });
198
+
199
+ test("two overlapping ticks send once", async () => {
200
+ await seedFixture();
201
+ const config = await boot();
202
+ const deps = { getConfig: config.get, setConfig: config.set };
203
+ const [a, b] = await Promise.all([emitOrgSnapshotIfDue(deps), emitOrgSnapshotIfDue(deps)]);
204
+ await tick();
205
+ expect([a, b]).toEqual([true, true]); // both callers share the one in-flight send
206
+ expect(sent.filter((e) => e.event === "org.snapshot")).toHaveLength(1);
207
+ });
208
+
209
+ test("nothing is sent or recorded when opted out", async () => {
210
+ await seedFixture();
211
+ process.env.ANONYMIZED_TELEMETRY = "false";
212
+ const config = await boot(memoryConfig());
213
+ expect(await emitOrgSnapshotIfDue({ getConfig: config.get, setConfig: config.set })).toBe(
214
+ false,
215
+ );
216
+ await tick();
217
+ expect(sent).toEqual([]);
218
+ expect(config.store.has(LAST_SNAPSHOT_KEY)).toBe(false);
219
+ });
220
+
221
+ test("nothing is sent or recorded before telemetry has an identity, so the next tick retries", async () => {
222
+ await seedFixture();
223
+ const config = memoryConfig(); // never booted: no install ID, no org ID
224
+ expect(await emitOrgSnapshotIfDue({ getConfig: config.get, setConfig: config.set })).toBe(
225
+ false,
226
+ );
227
+ expect(config.store.has(LAST_SNAPSHOT_KEY)).toBe(false);
228
+ });
229
+ });
@@ -0,0 +1,200 @@
1
+ import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test";
2
+ import { unlink } from "node:fs/promises";
3
+ import {
4
+ closeDb,
5
+ completeTask,
6
+ createAgent,
7
+ createTaskExtended,
8
+ failTask,
9
+ getDbClient,
10
+ initDb,
11
+ updateAgentProfile,
12
+ } from "../be/db";
13
+ import { checkHeartbeatChecklist, createBootTriageTask } from "../heartbeat/heartbeat";
14
+ // Side-effect import: the checklist and boot-triage prompts come from the heartbeat templates.
15
+ import "../heartbeat/templates";
16
+ import { telemetry } from "../telemetry";
17
+ import { _resetTriggerSurfaceCacheForTests, resolveTriggerSurface } from "../telemetry-trigger";
18
+
19
+ // initTelemetry is not called here, but emitTaskTelemetry checks the opt-out
20
+ // env first, and the CI env may set it.
21
+ process.env.ANONYMIZED_TELEMETRY = "true";
22
+
23
+ const TEST_DB_PATH = "./test-telemetry-trigger.sqlite";
24
+ const WORKER_ID = "bbbb0000-0000-4000-8000-000000000003";
25
+ const LEAD_ID = "aaaa0000-0000-4000-8000-000000000003";
26
+
27
+ async function removeTestDb(): Promise<void> {
28
+ for (const suffix of ["", "-wal", "-shm"]) {
29
+ try {
30
+ await unlink(TEST_DB_PATH + suffix);
31
+ } catch {
32
+ // File does not exist.
33
+ }
34
+ }
35
+ }
36
+
37
+ async function flush(): Promise<void> {
38
+ for (let i = 0; i < 8; i++) await new Promise((resolve) => setTimeout(resolve, 0));
39
+ }
40
+
41
+ type Call = { event: string; props: Record<string, unknown> };
42
+
43
+ describe("trigger_surface on task telemetry", () => {
44
+ let spy: ReturnType<typeof spyOn>;
45
+ let calls: Call[];
46
+
47
+ const forTask = (taskId: string, event: string) =>
48
+ calls.find((c) => c.props.taskId === taskId && c.event === event);
49
+
50
+ beforeEach(async () => {
51
+ closeDb();
52
+ await removeTestDb();
53
+ initDb(TEST_DB_PATH);
54
+ _resetTriggerSurfaceCacheForTests();
55
+ await createAgent({ id: WORKER_ID, name: "Trigger Worker", isLead: false, status: "idle" });
56
+ await createAgent({ id: LEAD_ID, name: "Trigger Lead", isLead: true, status: "idle" });
57
+ calls = [];
58
+ spy = spyOn(telemetry, "taskEvent").mockImplementation((event, props) => {
59
+ calls.push({ event, props: props as Record<string, unknown> });
60
+ });
61
+ });
62
+
63
+ afterEach(async () => {
64
+ spy.mockRestore();
65
+ closeDb();
66
+ await removeTestDb();
67
+ });
68
+
69
+ test("a slack root with an mcp child and a system follow-up reports slack on every created and completed event", async () => {
70
+ const root = await createTaskExtended("slack ask", { agentId: LEAD_ID, source: "slack" });
71
+ const child = await createTaskExtended("delegated", {
72
+ agentId: WORKER_ID,
73
+ source: "mcp",
74
+ parentTaskId: root.id,
75
+ });
76
+ const followUp = await createTaskExtended("follow-up", {
77
+ agentId: LEAD_ID,
78
+ source: "system",
79
+ parentTaskId: child.id,
80
+ });
81
+ await flush();
82
+ await completeTask(root.id, "done");
83
+ await completeTask(child.id, "done");
84
+ await completeTask(followUp.id, "done");
85
+ await flush();
86
+
87
+ const tasks = [
88
+ { id: root.id, taskSource: "slack" },
89
+ { id: child.id, taskSource: "mcp" },
90
+ { id: followUp.id, taskSource: "system" },
91
+ ];
92
+ for (const { id, taskSource } of tasks) {
93
+ for (const event of ["created", "completed"]) {
94
+ const call = forTask(id, event);
95
+ expect(call?.props.trigger_surface).toBe("slack");
96
+ expect(call?.props.task_source).toBe(taskSource);
97
+ }
98
+ }
99
+ });
100
+
101
+ test("a failed child reports its root's surface", async () => {
102
+ const root = await createTaskExtended("linear ask", { agentId: LEAD_ID, source: "linear" });
103
+ const child = await createTaskExtended("delegated", {
104
+ agentId: WORKER_ID,
105
+ source: "mcp",
106
+ parentTaskId: root.id,
107
+ });
108
+ await flush();
109
+ await failTask(child.id, "nope");
110
+ await flush();
111
+
112
+ const failed = forTask(child.id, "failed");
113
+ expect(failed?.props.trigger_surface).toBe("linear");
114
+ expect(failed?.props.task_source).toBe("mcp");
115
+ });
116
+
117
+ test("no task event carries a source property", async () => {
118
+ const root = await createTaskExtended("api ask", { agentId: LEAD_ID, source: "api" });
119
+ await flush();
120
+ await completeTask(root.id, "done");
121
+ await flush();
122
+ expect(calls.length).toBeGreaterThan(0);
123
+ for (const call of calls) expect("source" in call.props).toBe(false);
124
+ });
125
+
126
+ // The two heartbeat creators default to source "mcp" when they omit `source`,
127
+ // so each one is run for real and judged on the row it persists and the
128
+ // task.created event it emits.
129
+ const heartbeatCreators: Array<{ taskType: string; create: () => Promise<void> }> = [
130
+ { taskType: "boot-triage", create: createBootTriageTask },
131
+ {
132
+ taskType: "heartbeat-checklist",
133
+ create: async () => {
134
+ await updateAgentProfile(LEAD_ID, { heartbeatMd: "- Check for stuck tasks\n" });
135
+ await checkHeartbeatChecklist();
136
+ },
137
+ },
138
+ ];
139
+
140
+ for (const { taskType, create } of heartbeatCreators) {
141
+ test(`a ${taskType} task reports system, not mcp`, async () => {
142
+ await create();
143
+ await flush();
144
+
145
+ const row = await getDbClient().get<{ id: string; source: string }>(
146
+ "SELECT id, source FROM agent_tasks WHERE taskType = ?",
147
+ [taskType],
148
+ );
149
+ // The row says system too, so the dashboard UI stops calling it mcp.
150
+ expect(row?.source).toBe("system");
151
+
152
+ const created = forTask(row?.id as string, "created");
153
+ expect(created?.props.trigger_surface).toBe("system");
154
+ expect(created?.props.task_source).toBe("system");
155
+ });
156
+ }
157
+
158
+ test("an orphan (parent row deleted) uses the deepest row that still exists", async () => {
159
+ const root = await createTaskExtended("root", { agentId: LEAD_ID, source: "github" });
160
+ const child = await createTaskExtended("child", {
161
+ agentId: WORKER_ID,
162
+ source: "mcp",
163
+ parentTaskId: root.id,
164
+ });
165
+ _resetTriggerSurfaceCacheForTests();
166
+ await getDbClient().run("PRAGMA foreign_keys = OFF");
167
+ await getDbClient().run("DELETE FROM agent_tasks WHERE id = ?", [root.id]);
168
+ expect(await resolveTriggerSurface(child.id)).toBe("mcp");
169
+ });
170
+
171
+ test("a task that no longer exists falls back to the caller's source, uncached", async () => {
172
+ expect(await resolveTriggerSurface("missing-task", "schedule")).toBe("schedule");
173
+ expect(await resolveTriggerSurface("missing-task", "weird")).toBe("other");
174
+ });
175
+
176
+ test("the root surface is memoized, because a task's root is fixed at creation", async () => {
177
+ const root = await createTaskExtended("root", { agentId: LEAD_ID, source: "jira" });
178
+ expect(await resolveTriggerSurface(root.id)).toBe("jira");
179
+ await getDbClient().run("PRAGMA foreign_keys = OFF");
180
+ await getDbClient().run("DELETE FROM agent_tasks WHERE id = ?", [root.id]);
181
+ expect(await resolveTriggerSurface(root.id)).toBe("jira");
182
+ });
183
+
184
+ test("a chain deeper than the cap stops at the cap instead of looping", async () => {
185
+ // 60 tasks in a line, inserted as raw rows to avoid 60 full createTask calls.
186
+ const ids = Array.from({ length: 60 }, (_, i) => `chain-${String(i).padStart(2, "0")}`);
187
+ await getDbClient().run("PRAGMA foreign_keys = OFF");
188
+ for (const [i, id] of ids.entries()) {
189
+ await getDbClient().run(
190
+ `INSERT INTO agent_tasks (id, task, status, source, parentTaskId, createdAt, lastUpdatedAt)
191
+ VALUES (?, 'x', 'pending', ?, ?, datetime('now'), datetime('now'))`,
192
+ [id, i === 0 ? "slack" : "mcp", i === 0 ? null : ids[i - 1]],
193
+ );
194
+ }
195
+ // Depth 50 above the tail is chain-09, an mcp task: the walk stopped at the cap.
196
+ expect(await resolveTriggerSurface("chain-59")).toBe("mcp");
197
+ // A chain inside the cap reaches the slack root.
198
+ expect(await resolveTriggerSurface("chain-40")).toBe("slack");
199
+ });
200
+ });
@@ -17,6 +17,7 @@
17
17
  export async function restoreAllTemplateDefinitions(): Promise<void> {
18
18
  const ts = Date.now();
19
19
  await import(`../agentmail/templates?t=${ts}`);
20
+ await import(`../comb/templates?t=${ts}`);
20
21
  await import(`../commands/templates?t=${ts}`);
21
22
  await import(`../github/templates?t=${ts}`);
22
23
  await import(`../gitlab/templates?t=${ts}`);