@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@desplega.ai/agent-swarm",
3
- "version": "1.161.0",
3
+ "version": "1.162.0",
4
4
  "description": "agent-swarm.dev — an open-source operating system for AI work: a lead agent breaks goals into tasks, routes them to specialized workers such as Claude Code or Codex, runs each worker in an isolated container, and preserves shared memory, tools, schedules, and review gates so delegated work compounds across sessions.",
5
5
  "keywords": [
6
6
  "agent-swarm.dev",
@@ -82,6 +82,13 @@
82
82
  "check:ai-toolbox-skills": "bun scripts/sync-ai-toolbox-skills.ts --check",
83
83
  "check:skill-sources": "bun scripts/check-skill-sources.ts",
84
84
  "check:operator-skill": "bun scripts/check-operator-skill.ts",
85
+ "sync:telemetry-contract": "bun scripts/sync-telemetry-contract.ts",
86
+ "check:telemetry-contract": "bun scripts/sync-telemetry-contract.ts --check",
87
+ "build:telemetry-docs": "bun scripts/gen-telemetry-docs.ts",
88
+ "check:telemetry-docs": "bun scripts/gen-telemetry-docs.ts --check",
89
+ "build:claude-mod": "bun scripts/build-claude-mod.ts",
90
+ "check:claude-mod": "bun scripts/build-claude-mod.ts --check",
91
+ "test:claude-mod": "bash scripts/test-claude-mod.sh",
85
92
  "build:script-types": "bun scripts/bundle-script-types.ts",
86
93
  "build:extension-types": "bun scripts/bundle-extension-types.ts",
87
94
  "build:extension-schema": "bun scripts/build-extension-manifest-schema.ts",
@@ -187,17 +194,17 @@
187
194
  "bun": ">=1.3.12"
188
195
  },
189
196
  "dependencies": {
190
- "@agentclientprotocol/sdk": "1.6.0",
197
+ "@agentclientprotocol/sdk": "1.7.0",
191
198
  "@ai-sdk/openai": "^3.0.41",
192
- "@anthropic-ai/claude-agent-sdk": "0.3.287",
199
+ "@anthropic-ai/claude-agent-sdk": "0.3.289",
193
200
  "@anthropic-ai/sdk": "^0.131.0",
194
201
  "@asteasolutions/zod-to-openapi": "^8.0.0",
195
202
  "@aws-sdk/client-bedrock": "3.1048.0",
196
203
  "@desplega.ai/business-use": "^0.4.2",
197
204
  "@desplega.ai/localtunnel": "^2.2.0",
198
- "@earendil-works/pi-agent-core": "^1.0.0",
199
- "@earendil-works/pi-ai": "^1.0.0",
200
- "@earendil-works/pi-coding-agent": "^1.0.0",
205
+ "@earendil-works/pi-agent-core": "^1.0.2",
206
+ "@earendil-works/pi-ai": "^1.0.2",
207
+ "@earendil-works/pi-coding-agent": "^1.0.2",
201
208
  "@inkjs/ui": "^2.0.0",
202
209
  "@linear/sdk": "^77.0.0",
203
210
  "@modelcontextprotocol/sdk": "1.29.0",
@@ -0,0 +1,184 @@
1
+ /**
2
+ * API drain: what the API does between SIGTERM and closing its HTTP server.
3
+ *
4
+ * On a deploy the orchestrator stops the API and the workers together, and the
5
+ * workers' own SIGTERM handoff (`POST /api/tasks/:id/supersede`) lands after the
6
+ * API is gone, so every in-flight task falls through to the heartbeat sweep.
7
+ * Draining flips that order: the API keeps serving, dispatches no new work,
8
+ * marks every response with `X-Swarm-Draining: 1` (see `src/utils/api-drain.ts`),
9
+ * and waits, bounded, until the tasks it saw in flight have been handed off.
10
+ *
11
+ * The state is process-local. A new API process starts not draining.
12
+ */
13
+
14
+ import { API_DRAIN_MAX_MS_LIMIT } from "../utils/api-drain";
15
+ import { scrubSecrets } from "../utils/secret-scrubber";
16
+ import { getDbClient } from "./db";
17
+
18
+ /**
19
+ * Default cap on the wait for worker handoffs: `0`, the drain is off. It is
20
+ * opt-in because it makes every live worker supersede its in-flight tasks on
21
+ * any API SIGTERM, including an API-only restart where those tasks would
22
+ * otherwise keep running. A deploy that stops the API and the workers together
23
+ * sets `API_DRAIN_MAX_MS` (e.g. `30000`) to turn it on.
24
+ */
25
+ export const DEFAULT_API_DRAIN_MAX_MS = 0;
26
+ const DRAIN_CHECK_INTERVAL_MS = 500;
27
+ /**
28
+ * A worker pings every few seconds. One silent for longer than this is not
29
+ * expected to hand anything off; the heartbeat sweep owns its tasks.
30
+ */
31
+ const LIVE_WORKER_WINDOW_MS = 30_000;
32
+
33
+ let drainStartedAt: number | null = null;
34
+
35
+ export function isApiDraining(): boolean {
36
+ return drainStartedAt !== null;
37
+ }
38
+
39
+ /** Enter the draining state. Returns false when already draining. */
40
+ export function beginApiDrain(now: number = Date.now()): boolean {
41
+ if (drainStartedAt !== null) return false;
42
+ drainStartedAt = now;
43
+ return true;
44
+ }
45
+
46
+ /** Test seam: the state is module-level, so tests reset it between cases. */
47
+ export function resetApiDrainForTesting(): void {
48
+ drainStartedAt = null;
49
+ }
50
+
51
+ /**
52
+ * `API_DRAIN_MAX_MS`, capped at `API_DRAIN_MAX_MS_LIMIT`. Unset, empty, or
53
+ * invalid means `DEFAULT_API_DRAIN_MAX_MS` (`0`, the drain is off).
54
+ */
55
+ export function resolveApiDrainMaxMs(
56
+ env: Record<string, string | undefined> = process.env,
57
+ ): number {
58
+ const raw = env.API_DRAIN_MAX_MS;
59
+ if (raw === undefined || raw.trim() === "") return DEFAULT_API_DRAIN_MAX_MS;
60
+ const parsed = Number(raw);
61
+ if (!Number.isFinite(parsed) || parsed < 0) return DEFAULT_API_DRAIN_MAX_MS;
62
+ return Math.min(Math.floor(parsed), API_DRAIN_MAX_MS_LIMIT);
63
+ }
64
+
65
+ /** `in_progress` tasks held by an agent that pinged recently, i.e. one that can still hand off. */
66
+ export async function listLiveInFlightTaskIds(now: number = Date.now()): Promise<string[]> {
67
+ const cutoff = new Date(now - LIVE_WORKER_WINDOW_MS).toISOString();
68
+ const rows = await getDbClient().query<{ id: string }>(
69
+ `SELECT t.id AS id
70
+ FROM agent_tasks t
71
+ JOIN agents a ON a.id = t.agentId
72
+ WHERE t.status = 'in_progress' AND a.status != 'offline' AND a.lastUpdatedAt >= ?`,
73
+ [cutoff],
74
+ );
75
+ return rows.map((row) => row.id);
76
+ }
77
+
78
+ /** How many of `taskIds` are still `in_progress`. */
79
+ export async function countStillInProgress(taskIds: string[]): Promise<number> {
80
+ if (taskIds.length === 0) return 0;
81
+ const row = await getDbClient().get<{ n: number }>(
82
+ `SELECT count(*) AS n FROM agent_tasks
83
+ WHERE status = 'in_progress' AND id IN (SELECT value FROM json_each(?))`,
84
+ [JSON.stringify(taskIds)],
85
+ );
86
+ return row?.n ?? 0;
87
+ }
88
+
89
+ export interface ApiDrainDeps {
90
+ env?: Record<string, string | undefined>;
91
+ now?: () => number;
92
+ sleep?: (ms: number) => Promise<void>;
93
+ listInFlight?: () => Promise<string[]>;
94
+ countRemaining?: (taskIds: string[]) => Promise<number>;
95
+ log?: (message: string) => void;
96
+ }
97
+
98
+ export interface ApiDrainOutcome {
99
+ /** False when the cap is `0` (the default): the API did not enter the draining state. */
100
+ enabled: boolean;
101
+ /** Live in-flight tasks seen when the drain began. */
102
+ inFlight: number;
103
+ /** Of those, still `in_progress` when the wait ended. */
104
+ remaining: number;
105
+ waitedMs: number;
106
+ timedOut: boolean;
107
+ }
108
+
109
+ /**
110
+ * Enter the draining state, then wait until the live in-flight tasks are handed
111
+ * off or the cap passes. Never throws: a failed read ends the wait, because a
112
+ * shutdown that cannot finish is worse than one that skips the drain.
113
+ */
114
+ export async function drainApi(deps: ApiDrainDeps = {}): Promise<ApiDrainOutcome> {
115
+ const now = deps.now ?? Date.now;
116
+ const sleep = deps.sleep ?? ((ms: number) => Bun.sleep(ms));
117
+ const log = deps.log ?? ((message: string) => console.log(message));
118
+ const listInFlight = deps.listInFlight ?? (() => listLiveInFlightTaskIds(now()));
119
+ const countRemaining = deps.countRemaining ?? countStillInProgress;
120
+ const maxMs = resolveApiDrainMaxMs(deps.env);
121
+ const startedAt = now();
122
+
123
+ if (maxMs === 0) {
124
+ log("[drain] off (API_DRAIN_MAX_MS is 0 or unset): closing without waiting for handoffs");
125
+ return { enabled: false, inFlight: 0, remaining: 0, waitedMs: 0, timedOut: false };
126
+ }
127
+
128
+ beginApiDrain(startedAt);
129
+
130
+ let taskIds: string[];
131
+ try {
132
+ taskIds = await listInFlight();
133
+ } catch (err) {
134
+ log(`[drain] could not read in-flight tasks, closing without waiting: ${describe(err)}`);
135
+ return {
136
+ enabled: true,
137
+ inFlight: 0,
138
+ remaining: 0,
139
+ waitedMs: now() - startedAt,
140
+ timedOut: false,
141
+ };
142
+ }
143
+
144
+ if (taskIds.length === 0) {
145
+ log("[drain] draining: no in-flight tasks on live workers");
146
+ return {
147
+ enabled: true,
148
+ inFlight: 0,
149
+ remaining: 0,
150
+ waitedMs: now() - startedAt,
151
+ timedOut: false,
152
+ };
153
+ }
154
+
155
+ log(
156
+ `[drain] draining: waiting up to ${maxMs}ms for ${taskIds.length} in-flight task(s) to be handed off`,
157
+ );
158
+
159
+ const deadline = startedAt + maxMs;
160
+ let remaining = taskIds.length;
161
+ while (true) {
162
+ try {
163
+ remaining = await countRemaining(taskIds);
164
+ } catch (err) {
165
+ log(`[drain] could not re-read in-flight tasks, closing now: ${describe(err)}`);
166
+ break;
167
+ }
168
+ if (remaining === 0 || now() >= deadline) break;
169
+ await sleep(Math.min(DRAIN_CHECK_INTERVAL_MS, Math.max(0, deadline - now())));
170
+ }
171
+
172
+ const waitedMs = now() - startedAt;
173
+ const timedOut = remaining > 0;
174
+ log(
175
+ timedOut
176
+ ? `[drain] timed out after ${waitedMs}ms with ${remaining} of ${taskIds.length} task(s) still in_progress`
177
+ : `[drain] all ${taskIds.length} in-flight task(s) handed off after ${waitedMs}ms`,
178
+ );
179
+ return { enabled: true, inFlight: taskIds.length, remaining, waitedMs, timedOut };
180
+ }
181
+
182
+ function describe(err: unknown): string {
183
+ return scrubSecrets(err instanceof Error ? err.message : String(err));
184
+ }
@@ -1161,7 +1161,9 @@ export type ExistingTrackerContextWork = {
1161
1161
 
1162
1162
  const LINEAR_TRACKER_CONTEXT_KEY_PREFIX = "task:trackers:linear:";
1163
1163
 
1164
- function isLinearTrackerContextKey(contextKey: string | null | undefined): contextKey is string {
1164
+ export function isLinearTrackerContextKey(
1165
+ contextKey: string | null | undefined,
1166
+ ): contextKey is string {
1165
1167
  return !!contextKey && contextKey.startsWith(LINEAR_TRACKER_CONTEXT_KEY_PREFIX);
1166
1168
  }
1167
1169
 
@@ -1170,28 +1172,51 @@ function isLinearTrackerContextKey(contextKey: string | null | undefined): conte
1170
1172
  *
1171
1173
  * Active means any non-terminal task. A completed task with persisted VCS PR/MR
1172
1174
  * metadata is also treated as existing work because the task can be complete
1173
- * while the PR is still awaiting review/merge.
1175
+ * while the PR is still awaiting review/merge. Delegation excludes the tracker
1176
+ * root and all its descendants from both lookups.
1174
1177
  */
1175
1178
  export async function findExistingLinearTrackerContextWork(
1176
1179
  contextKey: string | null | undefined,
1180
+ delegatingParentTaskId?: string,
1177
1181
  ): Promise<ExistingTrackerContextWork | null> {
1178
1182
  if (!isLinearTrackerContextKey(contextKey)) return null;
1179
1183
 
1184
+ const lineageCte = `WITH RECURSIVE
1185
+ ancestors(id, parentTaskId) AS (
1186
+ SELECT id, parentTaskId FROM agent_tasks WHERE id = ? AND contextKey = ?
1187
+ UNION
1188
+ SELECT parent.id, parent.parentTaskId FROM agent_tasks parent
1189
+ JOIN ancestors child ON parent.id = child.parentTaskId
1190
+ WHERE parent.contextKey = ?
1191
+ ),
1192
+ lineage(id) AS (
1193
+ SELECT id FROM ancestors
1194
+ WHERE parentTaskId IS NULL OR parentTaskId NOT IN (SELECT id FROM ancestors)
1195
+ UNION
1196
+ SELECT child.id FROM agent_tasks child
1197
+ JOIN lineage parent ON child.parentTaskId = parent.id
1198
+ )`;
1199
+ const lineageParams = [delegatingParentTaskId ?? null, contextKey, contextKey];
1200
+
1180
1201
  const activeRow = await getDbClient().get<AgentTaskRow>(
1181
- `SELECT * FROM agent_tasks
1202
+ `${lineageCte}
1203
+ SELECT * FROM agent_tasks
1182
1204
  WHERE contextKey = ?
1205
+ AND id NOT IN (SELECT id FROM lineage)
1183
1206
  AND status NOT IN ('completed', 'failed', 'cancelled', 'superseded')
1184
1207
  ORDER BY lastUpdatedAt DESC
1185
1208
  LIMIT 1`,
1186
- [contextKey],
1209
+ [...lineageParams, contextKey],
1187
1210
  );
1188
1211
  if (activeRow) {
1189
1212
  return { task: rowToAgentTask(activeRow), reason: "active_task" };
1190
1213
  }
1191
1214
 
1192
1215
  const linkedPrRow = await getDbClient().get<AgentTaskRow>(
1193
- `SELECT * FROM agent_tasks
1216
+ `${lineageCte}
1217
+ SELECT * FROM agent_tasks
1194
1218
  WHERE contextKey = ?
1219
+ AND id NOT IN (SELECT id FROM lineage)
1195
1220
  AND status = 'completed'
1196
1221
  AND vcsProvider IS NOT NULL
1197
1222
  AND vcsRepo IS NOT NULL
@@ -1199,7 +1224,7 @@ export async function findExistingLinearTrackerContextWork(
1199
1224
  AND vcsUrl IS NOT NULL
1200
1225
  ORDER BY lastUpdatedAt DESC
1201
1226
  LIMIT 1`,
1202
- [contextKey],
1227
+ [...lineageParams, contextKey],
1203
1228
  );
1204
1229
  if (linkedPrRow) {
1205
1230
  return { task: rowToAgentTask(linkedPrRow), reason: "linked_open_pr" };
@@ -1,6 +1,6 @@
1
1
  import pkg from "../../../../package.json";
2
2
  import { defaultAssetKey } from "../../../assets/key";
3
- import type { telemetry } from "../../../telemetry";
3
+ import type { TaskTelemetryEvent, TaskTelemetryInput } from "../../../telemetry-trigger";
4
4
  import type {
5
5
  Agent,
6
6
  AgentLog,
@@ -42,10 +42,11 @@ type TaskWriteDependencies = {
42
42
  agentId: string | null,
43
43
  sourceTexts: Array<string | null | undefined>,
44
44
  ) => Promise<TaskAttachment[]>;
45
- emitTaskLifecycleTelemetryAfterCommit: (
46
- event: string,
47
- props: Parameters<typeof telemetry.taskEvent>[1],
45
+ emitTaskLifecycleTelemetryAfterCommit: <S extends TaskTelemetryEvent>(
46
+ event: S,
47
+ props: TaskTelemetryInput<S>,
48
48
  verify?: (task: AgentTask | null) => boolean,
49
+ actorUserId?: string | null,
49
50
  ) => void;
50
51
  taskContextForTelemetry: (task: AgentTask) => {
51
52
  provider?: ProviderName;
package/src/be/db.ts CHANGED
@@ -10,6 +10,12 @@ import {
10
10
  import { realtimeBus } from "../realtime/bus";
11
11
  import { slackChannelFromContextKey } from "../tasks/slack-routing";
12
12
  import { _resolveIntegrationType, emitIntegrationConnected, telemetry } from "../telemetry";
13
+ import { scheduleOrgDomainRecompute } from "../telemetry-identity";
14
+ import {
15
+ emitTaskTelemetry,
16
+ type TaskTelemetryEvent,
17
+ type TaskTelemetryInput,
18
+ } from "../telemetry-trigger";
13
19
  import type {
14
20
  ActiveSession,
15
21
  Agent,
@@ -281,6 +287,7 @@ export {
281
287
  getTasksCount,
282
288
  hasNonTerminalRerouteDecisionChild,
283
289
  hasNonTerminalResumeChild,
290
+ isLinearTrackerContextKey,
284
291
  markFinalizedSlackRelaysDelivered,
285
292
  markSlackRelayAttempted,
286
293
  markSlackRelayDelivered,
@@ -331,7 +338,6 @@ configureTaskReadDependencies({
331
338
  previewText: (text, maxChars) => previewText(text, maxChars),
332
339
  });
333
340
 
334
- type TaskTelemetryProps = Parameters<typeof telemetry.taskEvent>[1];
335
341
  type TaskTelemetryContext = {
336
342
  provider?: ProviderName;
337
343
  harnessVariant?: string;
@@ -347,23 +353,27 @@ function assetKeyPrefixPattern(input: string): string {
347
353
  return literalPrefixPattern(normalizeAssetKey(input));
348
354
  }
349
355
 
350
- function emitTaskLifecycleTelemetryAfterCommit(
351
- event: string,
352
- props: TaskTelemetryProps,
356
+ function emitTaskLifecycleTelemetryAfterCommit<S extends TaskTelemetryEvent>(
357
+ event: S,
358
+ props: TaskTelemetryInput<S>,
353
359
  verify?: (task: AgentTask | null) => boolean,
360
+ actorUserId?: string | null,
354
361
  ): void {
355
362
  // afterCommit (not queueMicrotask): under an async client transaction,
356
363
  // microtasks drain before COMMIT, so the verify read could observe
357
364
  // uncommitted state. afterCommit runs strictly post-COMMIT/ROLLBACK.
358
365
  getDbClient().afterCommit(() => {
366
+ // `trigger_surface` is resolved (one read up the parent chain) and
367
+ // `source` renamed to `task_source` inside emitTaskTelemetry, so every
368
+ // lifecycle event of a task reports the same root surface.
359
369
  if (!verify) {
360
- telemetry.taskEvent(event, props);
370
+ void emitTaskTelemetry(event, props, actorUserId);
361
371
  return;
362
372
  }
363
- getTaskById(props.taskId)
373
+ getTaskById((props as { taskId: string }).taskId)
364
374
  .then((task) => {
365
375
  if (!verify(task)) return;
366
- telemetry.taskEvent(event, props);
376
+ return emitTaskTelemetry(event, props, actorUserId);
367
377
  })
368
378
  .catch((err) =>
369
379
  console.error(
@@ -2896,6 +2906,8 @@ export async function createTaskExtended(
2896
2906
  priority: row.priority,
2897
2907
  },
2898
2908
  (task) => task !== null,
2909
+ // The human who asked (a pseudonymous user_ref), null for system tasks.
2910
+ row.requestedByUserId,
2899
2911
  );
2900
2912
 
2901
2913
  getDbClient().afterCommit(() => {
@@ -7352,12 +7364,17 @@ export async function createWorkflow(
7352
7364
  if (!row) throw new Error("Failed to create workflow");
7353
7365
  const workflow = rowToWorkflow(row);
7354
7366
  // afterCommit: a caller's transaction that rolls back must not report the workflow.
7367
+ const requestUserId = getCurrentRequestUserId();
7355
7368
  getDbClient().afterCommit(() =>
7356
- telemetry.workflow("created", {
7357
- workflowId: workflow.id,
7358
- nodeCount: workflow.definition.nodes.length,
7359
- ...(source ? { source } : {}),
7360
- }),
7369
+ telemetry.workflow(
7370
+ "created",
7371
+ {
7372
+ workflowId: workflow.id,
7373
+ nodeCount: workflow.definition.nodes.length,
7374
+ ...(source ? { via: source } : {}),
7375
+ },
7376
+ { userId: requestUserId ?? null },
7377
+ ),
7361
7378
  );
7362
7379
  return workflow;
7363
7380
  }
@@ -7578,11 +7595,13 @@ async function deleteWorkflowRows(id: string, source?: "api" | "mcp"): Promise<b
7578
7595
  const deleted = result.changes > 0;
7579
7596
  if (deleted) {
7580
7597
  // afterCommit: a caller's transaction that rolls back must not report the delete.
7598
+ const requestUserId = getCurrentRequestUserId();
7581
7599
  getDbClient().afterCommit(() =>
7582
- telemetry.workflow("deleted", {
7583
- workflowId: id,
7584
- ...(source ? { source } : {}),
7585
- }),
7600
+ telemetry.workflow(
7601
+ "deleted",
7602
+ { workflowId: id, ...(source ? { via: source } : {}) },
7603
+ { userId: requestUserId ?? null },
7604
+ ),
7586
7605
  );
7587
7606
  }
7588
7607
  return deleted;
@@ -7669,6 +7688,7 @@ export async function getWorkflowRun(id: string): Promise<WorkflowRun | null> {
7669
7688
 
7670
7689
  function emitWorkflowTerminalTelemetry(run: WorkflowRun): void {
7671
7690
  if (run.status !== "completed" && run.status !== "failed") return;
7691
+ const status = run.status;
7672
7692
 
7673
7693
  // afterCommit (not queueMicrotask): under an async client transaction,
7674
7694
  // microtasks drain before COMMIT, so the verify read below could observe
@@ -7678,9 +7698,9 @@ function emitWorkflowTerminalTelemetry(run: WorkflowRun): void {
7678
7698
  // crashing the process as an unhandled rejection.
7679
7699
  getDbClient().afterCommit(async () => {
7680
7700
  const latest = await getWorkflowRun(run.id);
7681
- if (!latest || latest.status !== run.status) return;
7701
+ if (!latest || latest.status !== status) return;
7682
7702
  const steps = await getWorkflowRunStepsByRunId(run.id);
7683
- telemetry.workflow(run.status, {
7703
+ telemetry.workflow(status, {
7684
7704
  workflowId: run.workflowId,
7685
7705
  durationMs: run.startedAt ? Date.now() - new Date(run.startedAt).getTime() : undefined,
7686
7706
  stepsCompleted: steps.filter((step) => step.status === "completed").length,
@@ -12046,6 +12066,8 @@ export async function createUser(data: {
12046
12066
  ],
12047
12067
  );
12048
12068
  if (!row) throw new Error("Failed to create user");
12069
+ // The org's email domain may come from this user (debounced, post-commit).
12070
+ getDbClient().afterCommit(scheduleOrgDomainRecompute);
12049
12071
  return rowToUser(row);
12050
12072
  }
12051
12073
 
@@ -12118,6 +12140,7 @@ export async function updateUser(
12118
12140
  `UPDATE users SET ${sets.join(", ")} WHERE id = ? RETURNING *`,
12119
12141
  params,
12120
12142
  );
12143
+ if (row) getDbClient().afterCommit(scheduleOrgDomainRecompute);
12121
12144
  return row ? rowToUser(row) : null;
12122
12145
  }
12123
12146
 
@@ -12227,6 +12250,7 @@ export async function deleteUser(id: string, replacementUserId?: string): Promis
12227
12250
  await reclassifyTaskHumanFree(reclassifySeedIds);
12228
12251
 
12229
12252
  const result = await tx.run("DELETE FROM users WHERE id = ?", [id]);
12253
+ if (result.changes > 0) getDbClient().afterCommit(scheduleOrgDomainRecompute);
12230
12254
  return result.changes > 0;
12231
12255
  });
12232
12256
  }
@@ -6,6 +6,7 @@
6
6
  * Phase 1: capture layer only — no traversal tools, no reranker integration.
7
7
  */
8
8
  import { getDbClient } from "@/be/db";
9
+ import { getConfiguredAppUrls } from "@/utils/constants";
9
10
 
10
11
  export type LinkType =
11
12
  | "wikilink"
@@ -41,8 +42,8 @@ const WIKILINK_RE = /\[\[([^\]]+)\]\]/g;
41
42
  const PR_HASH_RE = /(?:^|[\s(])#(\d{1,5})(?=[\s,.)!?]|$)/gm;
42
43
  const PR_PREFIX_RE = /\bPR\s*#(\d{1,5})\b/gi;
43
44
  const GITHUB_PR_URL_RE = /https?:\/\/github\.com\/([\w.-]+)\/([\w.-]+)\/pull\/(\d+)/g;
44
- const AGENT_FS_PATH_RE =
45
- /(?:agent-fs|live\.agent-fs\.dev)\/file\/~\/([a-f0-9-]+)\/([a-f0-9-]+)\/([\w/.%-]+)/g;
45
+ const AGENT_FS_LIVE_HOSTS = String.raw`agent-fs|live\.agent-fs\.dev`;
46
+ const AGENT_FS_FILE_ROUTE = String.raw`\/file\/~\/([a-f0-9-]+)\/([a-f0-9-]+)\/([\w/.%-]+)`;
46
47
  const AGENT_UI_PAGE_RE = /(?:app\.[^/]+|localhost:\d+)\/pages\/([a-f0-9-]+)/g;
47
48
  const AGENT_UI_TASK_RE = /(?:app\.[^/]+|localhost:\d+)\/tasks\/([a-f0-9-]+)/g;
48
49
 
@@ -111,9 +112,34 @@ const prMatcher: Matcher = (content) => {
111
112
  return results;
112
113
  };
113
114
 
115
+ function escapeRegExp(value: string): string {
116
+ return value.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
117
+ }
118
+
119
+ /**
120
+ * agent-fs file links: the live UI, and the dashboard (Comb) at a configured
121
+ * `APP_URL` / `DASHBOARD_URL` or `localhost:<port>`. Both use the same
122
+ * `/file/~/<org>/<drive>/<path>` route and path encoding, so they resolve to
123
+ * the same target. Built per call: a config reload can change the app URLs.
124
+ * A host must start the host name (`evil-swarm.example.com` does not match
125
+ * `swarm.example.com`). Hosts are case-insensitive.
126
+ */
127
+ function agentFsPathRegex(): RegExp {
128
+ const appHosts = getConfiguredAppUrls().flatMap((value) => {
129
+ try {
130
+ const url = new URL(value);
131
+ return [escapeRegExp(`${url.host}${url.pathname.replace(/\/+$/, "")}`)];
132
+ } catch {
133
+ return [];
134
+ }
135
+ });
136
+ const hosts = [AGENT_FS_LIVE_HOSTS, ...appHosts, String.raw`localhost:\d+`].join("|");
137
+ return new RegExp(`(?<![\\w.-])(?:${hosts})${AGENT_FS_FILE_ROUTE}`, "gi");
138
+ }
139
+
114
140
  const agentFsMatcher: Matcher = (content) => {
115
141
  const results: MatcherResult[] = [];
116
- for (const match of content.matchAll(AGENT_FS_PATH_RE)) {
142
+ for (const match of content.matchAll(agentFsPathRegex())) {
117
143
  const orgId = match[1]!;
118
144
  const driveId = match[2]!;
119
145
  const path = match[3]!;
@@ -0,0 +1,36 @@
1
+ -- Comb pins: an agent-fs file or folder can be a favorite. The item id is
2
+ -- `<orgId>/<driveId>/<path>` (a folder path ends with "/"). SQLite cannot
3
+ -- change a CHECK in place, so rebuild the table with the 116 shape plus the
4
+ -- new item type. Every row is copied unchanged.
5
+
6
+ CREATE TABLE user_favorites_new (
7
+ id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
8
+ favoriteScope TEXT NOT NULL,
9
+ userId TEXT REFERENCES users(id) ON DELETE CASCADE,
10
+ itemType TEXT NOT NULL CHECK (itemType IN ('page','workflow','schedule','agent-fs-path')),
11
+ itemId TEXT NOT NULL,
12
+ createdAt TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
13
+ lastUpdatedAt TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
14
+ created_by TEXT,
15
+ updated_by TEXT,
16
+ UNIQUE (favoriteScope, itemType, itemId)
17
+ );
18
+
19
+ INSERT INTO user_favorites_new (
20
+ id, favoriteScope, userId, itemType, itemId,
21
+ createdAt, lastUpdatedAt, created_by, updated_by
22
+ )
23
+ SELECT
24
+ id, favoriteScope, userId, itemType, itemId,
25
+ createdAt, lastUpdatedAt, created_by, updated_by
26
+ FROM user_favorites;
27
+
28
+ DROP TABLE user_favorites;
29
+ ALTER TABLE user_favorites_new RENAME TO user_favorites;
30
+
31
+ CREATE INDEX idx_user_favorites_scope_type
32
+ ON user_favorites(favoriteScope, itemType);
33
+ CREATE INDEX idx_user_favorites_user_type
34
+ ON user_favorites(userId, itemType);
35
+ CREATE INDEX idx_user_favorites_item
36
+ ON user_favorites(itemType, itemId);
@@ -1,5 +1,6 @@
1
1
  import { telemetry } from "../telemetry";
2
2
  import { ProviderNameSchema } from "../types";
3
+ import { getCurrentRequestUserId } from "../utils/request-auth-context";
3
4
  import { z } from "../utils/zod-openapi";
4
5
  import { getDbClient, getSwarmConfigs, getTaskById, upsertSwarmConfig } from "./db";
5
6
  import { validateConfigValue } from "./swarm-config-guard";
@@ -23,6 +24,7 @@ export const OnboardingErrorClassSchema = z.enum([
23
24
  "timeout",
24
25
  "dimension",
25
26
  "model",
27
+ "endpoint",
26
28
  "not_enabled",
27
29
  "expired",
28
30
  "unknown",
@@ -48,6 +50,7 @@ export const OnboardingMemoryPresetSchema = z.enum([
48
50
  "openai",
49
51
  "openrouter",
50
52
  "vercel",
53
+ "azure",
51
54
  "custom",
52
55
  "existing",
53
56
  ]);
@@ -359,8 +362,11 @@ function addTelemetry<Event extends OnboardingTelemetryEvent>(
359
362
  }
360
363
 
361
364
  function queueTelemetry(events: PendingTelemetry[]): void {
365
+ // Captured now: the request's user is ambient state, and the post-commit hook
366
+ // may run after it has moved on. Null for an operator (API key) request.
367
+ const actor = { userId: getCurrentRequestUserId() ?? null };
362
368
  for (const item of events) {
363
- getDbClient().afterCommit(() => telemetry.onboarding(item.event, item.properties));
369
+ getDbClient().afterCommit(() => telemetry.onboarding(item.event, item.properties, actor));
364
370
  }
365
371
  }
366
372
 
@@ -15,6 +15,7 @@ import { getPromptTemplates, resetPromptTemplateToDefault, upsertPromptTemplate
15
15
  // handler imports, but runner/session templates are only loaded by the worker. Importing them
16
16
  // here ensures all templates are available for the render endpoint and seeded to the DB.
17
17
  import "../prompts/session-templates";
18
+ import "../comb/templates";
18
19
  import "../commands/templates";
19
20
  import "../tools/templates";
20
21