@desplega.ai/agent-swarm 2.4.0 → 2.4.2

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 (239) hide show
  1. package/README.md +1 -1
  2. package/dist/{acp-adapter-kedstxx6.js → acp-adapter-dv7hmx4r.js} +9 -9
  3. package/dist/{actions-vget6r1k.js → actions-51nc1gny.js} +21 -21
  4. package/dist/{amp-adapter-xeb5mktv.js → amp-adapter-abp5tdx8.js} +4 -4
  5. package/dist/{app-mqap8t8m.js → app-9h14qvr6.js} +3 -3
  6. package/dist/{assistant-h0gv97mv.js → assistant-wv1gz3mt.js} +22 -22
  7. package/dist/{boot-reembed-qr8a894z.js → boot-reembed-6yn53ms9.js} +1 -1
  8. package/dist/{boot-reembed-rj85jjhw.js → boot-reembed-7fcka4z5.js} +1 -1
  9. package/dist/{boot-scrub-sweep-eyfvn6v9.js → boot-scrub-sweep-pm1580ax.js} +4 -4
  10. package/dist/{boot-seeds-xs9a8dx1.js → boot-seeds-g5hfc1x8.js} +4 -4
  11. package/dist/{claude-adapter-bm23cye7.js → claude-adapter-gf3kxhf0.js} +9 -9
  12. package/dist/{claude-managed-adapter-1mwrsm9d.js → claude-managed-adapter-h2y889ge.js} +2 -2
  13. package/dist/{claude-managed-setup-gkfgks4j.js → claude-managed-setup-h00hsgtz.js} +2 -2
  14. package/dist/{claude-sdk-session-apb4ypz3.js → claude-sdk-session-s2k1qd8y.js} +13 -13
  15. package/dist/{cli-tj4j2ts4.js → cli-02qm7src.js} +8 -8
  16. package/dist/{cli-1cevz46c.js → cli-0qvtffe9.js} +6 -6
  17. package/dist/{cli-avmqhatn.js → cli-10hhg0km.js} +8 -8
  18. package/dist/{cli-7gh7020n.js → cli-1dmcq2eh.js} +1 -1
  19. package/dist/{cli-kz116kvw.js → cli-239yjk27.js} +7 -7
  20. package/dist/{cli-7ayprxp7.js → cli-2dfbv8qr.js} +8 -0
  21. package/dist/{cli-a49vpbf3.js → cli-5kqnemen.js} +1 -1
  22. package/dist/{cli-x719j3vs.js → cli-5rk36sjf.js} +442 -205
  23. package/dist/{cli-fb6t92v8.js → cli-6x9ma5w5.js} +1 -1
  24. package/dist/{cli-c8fwpgqb.js → cli-7225gwj7.js} +2 -2
  25. package/dist/{cli-5s5hx57y.js → cli-7awf1fp8.js} +1 -1
  26. package/dist/{cli-bn26xbcs.js → cli-8nyhf7za.js} +1 -1
  27. package/dist/{cli-ywyrffff.js → cli-8sxqsbbq.js} +1 -1
  28. package/dist/{cli-rsd7ga7v.js → cli-8zwe9mpk.js} +11 -11
  29. package/dist/{cli-y83zk5sb.js → cli-a0kcty51.js} +4 -4
  30. package/dist/{cli-8b9wvjps.js → cli-a4gvwttf.js} +4 -4
  31. package/dist/{cli-jhp7w1fy.js → cli-a5ebc95n.js} +2 -2
  32. package/dist/{cli-mvg2hf8p.js → cli-ahtpy9kf.js} +14 -7
  33. package/dist/{cli-x1p5dhq2.js → cli-bcj753sq.js} +3 -3
  34. package/dist/{cli-p22zdvnt.js → cli-bn0rewsy.js} +3 -3
  35. package/dist/{cli-p1m2b6gc.js → cli-d2mb5s8a.js} +7 -7
  36. package/dist/{cli-rg7ntypm.js → cli-d3qqdy4c.js} +456 -94
  37. package/dist/{cli-c5sbmw1v.js → cli-d82btycs.js} +94 -15
  38. package/dist/{cli-02vhhssk.js → cli-efaqv1c7.js} +5 -5
  39. package/dist/{cli-vjfpshn3.js → cli-ejs3m1sx.js} +1 -1
  40. package/dist/{cli-kk2r80n5.js → cli-faypxz7q.js} +4 -4
  41. package/dist/{cli-a1esmmee.js → cli-fpmsqq59.js} +7 -2
  42. package/dist/{cli-wrv09v7q.js → cli-ftyypbg5.js} +1 -1
  43. package/dist/{cli-zvze9rbm.js → cli-fyzjmseb.js} +3 -3
  44. package/dist/{cli-8rwvksbc.js → cli-g0hgtz4p.js} +5 -5
  45. package/dist/{cli-nrrg0fvy.js → cli-h38rjv87.js} +133 -12
  46. package/dist/{cli-tg9ksbn8.js → cli-h93c9vb3.js} +2 -2
  47. package/dist/{cli-eqgfs202.js → cli-hqhzg1qq.js} +2 -2
  48. package/dist/{cli-bhwa8rzc.js → cli-hyfw2j1e.js} +13 -12
  49. package/dist/{cli-3wjs22x3.js → cli-j4rqdf2n.js} +2 -2
  50. package/dist/{cli-6ftrrwm1.js → cli-n85dqg6d.js} +5 -5
  51. package/dist/{cli-8cxkw1f5.js → cli-nmqdc2t3.js} +2 -2
  52. package/dist/{cli-4fhyq711.js → cli-nn47d7e8.js} +13 -13
  53. package/dist/{cli-x3en6ct3.js → cli-nwyw3zhg.js} +1 -1
  54. package/dist/{cli-3avfh5az.js → cli-pemywhfq.js} +2 -2
  55. package/dist/{cli-xfe63zx3.js → cli-pkht5hap.js} +2 -2
  56. package/dist/{cli-sppc0cmw.js → cli-pnp2j5zg.js} +2 -2
  57. package/dist/{cli-wk8ccrz6.js → cli-pw7v69vp.js} +2 -2
  58. package/dist/{cli-tgvpmnwz.js → cli-r99kc9yq.js} +5 -5
  59. package/dist/{cli-bfc3k1x5.js → cli-rdyw3mct.js} +9 -9
  60. package/dist/{cli-b80a1100.js → cli-s6vr7r2d.js} +2 -2
  61. package/dist/{cli-zv4vy2b0.js → cli-s95dknxh.js} +1 -1
  62. package/dist/{cli-ttaz6c7m.js → cli-saw9a6f6.js} +6 -5
  63. package/dist/{cli-mz1ezzzh.js → cli-shvfedvm.js} +3 -3
  64. package/dist/{cli-rdr166ht.js → cli-smvcw9z5.js} +1 -1
  65. package/dist/{cli-2rwet3r7.js → cli-sv5gb0yb.js} +1 -1
  66. package/dist/{cli-5ff2sjyt.js → cli-t30yj95q.js} +2 -2
  67. package/dist/{cli-ptkpaqfy.js → cli-wt645v7q.js} +1 -1
  68. package/dist/{cli-f3nx1h8z.js → cli-wxbn3yfx.js} +2 -2
  69. package/dist/{cli-wyrr38xm.js → cli-x5mq1fyq.js} +1 -1
  70. package/dist/{cli-ccn0eybf.js → cli-xbzvet96.js} +6 -6
  71. package/dist/{cli-4mkvwgzr.js → cli-xf234ws8.js} +1 -1
  72. package/dist/{cli-nme7wery.js → cli-y2x52gs2.js} +46 -13
  73. package/dist/{cli-ffbrkh1r.js → cli-y8n5k515.js} +46 -38
  74. package/dist/{cli-ecshf504.js → cli-z4wd2ftv.js} +7 -7
  75. package/dist/{cli-dsgj3e7a.js → cli-zfg9enh9.js} +2 -2
  76. package/dist/{cli-yxqag76b.js → cli-zw88zjba.js} +1 -1
  77. package/dist/cli.js +20 -20
  78. package/dist/{codex-adapter-pp2gq8gr.js → codex-adapter-ttygw382.js} +6 -6
  79. package/dist/{codex-hook-qwzccd64.js → codex-hook-x7nejyrr.js} +7 -4
  80. package/dist/{codex-login-35p4sk85.js → codex-login-m819c41r.js} +1 -1
  81. package/dist/{codex-session-runner-0vzn09t8.js → codex-session-runner-60b58vv3.js} +7 -7
  82. package/dist/{commands-psqhkttb.js → commands-edmdwa4x.js} +2 -2
  83. package/dist/{controller-t20vz2k8.js → controller-m263fg43.js} +4 -4
  84. package/dist/{controllers-connect-gkxmqw9d.js → controllers-connect-5fym8sw1.js} +23 -23
  85. package/dist/{cursor-adapter-3v542qe5.js → cursor-adapter-cz6nm6mj.js} +3 -3
  86. package/dist/{db-nc1rd869.js → db-2y0mgstc.js} +3 -3
  87. package/dist/{db-2xbyenjr.js → db-nn0hcmx5.js} +6 -2
  88. package/dist/{devin-adapter-7egzmt0k.js → devin-adapter-zgqadvnm.js} +1 -1
  89. package/dist/{dispatcher-ewceb8ts.js → dispatcher-0a9wwjcx.js} +20 -20
  90. package/dist/{docker-njavd3fb.js → docker-fkgtr75t.js} +2 -2
  91. package/dist/{dsh-adapter-75xwh78t.js → dsh-adapter-3n3net33.js} +3 -3
  92. package/dist/{e2b-5r6a5jeb.js → e2b-7am6tmq6.js} +2 -2
  93. package/dist/{e2b-pw1nvtmh.js → e2b-thdcxtjd.js} +2 -2
  94. package/dist/{easter-egg-3d-xsc47jwf.js → easter-egg-3d-5cgynaja.js} +1 -1
  95. package/dist/{event-bus-101fenkh.js → event-bus-9vpk8gmq.js} +1 -1
  96. package/dist/{execute-gzmw1mnw.js → execute-t4e6m3rw.js} +1 -1
  97. package/dist/{external-channel-a5dsmxn4.js → external-channel-zhvt95n3.js} +2 -2
  98. package/dist/{grok-adapter-vj88xr23.js → grok-adapter-qnr1hp2y.js} +10 -10
  99. package/dist/handlers-dkcarkd0.js +39 -0
  100. package/dist/{heartbeat-t2m9r3wa.js → heartbeat-gz97r7ac.js} +23 -23
  101. package/dist/{hook-xa677f8r.js → hook-1mmdgjqs.js} +16 -9
  102. package/dist/{hook-8femx1bg.js → hook-x8rthca4.js} +6 -6
  103. package/dist/{http-xpmmq028.js → http-r8f85xr9.js} +274 -160
  104. package/dist/index-5jecgp3n.js +41 -0
  105. package/dist/index-aex0eba9.js +40 -0
  106. package/dist/{index-syztqdt6.js → index-g6tbb6ve.js} +1 -1
  107. package/dist/{index-cdzyfrsp.js → index-smxyk04s.js} +1 -1
  108. package/dist/{index-dg2tmj99.js → index-vzvfqzfj.js} +20 -20
  109. package/dist/{index-s7ckdajq.js → index-y7rbcrc1.js} +10 -10
  110. package/dist/{keepalive-3nr4st3r.js → keepalive-64c0f34c.js} +4 -4
  111. package/dist/lead-b1da0f97.js +78 -0
  112. package/dist/{maintenance-verwpe2c.js → maintenance-mnxpar12.js} +1 -1
  113. package/dist/{oauth-refresh-sweep-9q2t0y3q.js → oauth-refresh-sweep-36hs2v4h.js} +3 -3
  114. package/dist/{onboard-aytqedmq.js → onboard-xes8de61.js} +3 -3
  115. package/dist/{opencode-adapter-f4hryjny.js → opencode-adapter-3s7ax02e.js} +7 -7
  116. package/dist/{otel-impl-hckw4wt1.js → otel-impl-fzc57kcq.js} +2 -2
  117. package/dist/{pi-mono-adapter-hxby0ce1.js → pi-mono-adapter-ytp7gz8b.js} +22 -11
  118. package/dist/{pricing-refresh-5nvme3sm.js → pricing-refresh-1nek2w7b.js} +3 -3
  119. package/dist/{rbac-roles-vbpy21mg.js → rbac-roles-sqefb5hw.js} +2 -2
  120. package/dist/{rbac-roles-pd8c26m1.js → rbac-roles-ttyfaep2.js} +1 -1
  121. package/dist/{render-v2-7nqgvk2q.js → render-v2-vb527jwn.js} +3 -3
  122. package/dist/{resume-b6djcj3c.js → resume-jcdzg1bn.js} +10 -8
  123. package/dist/{runtime-txrxc9za.js → runtime-xe3aqy4x.js} +1 -1
  124. package/dist/{session-access-fc3f9y12.js → session-access-zsp4wsp0.js} +4 -4
  125. package/dist/{setup-n39szm6b.js → setup-1nb2zqd3.js} +1 -1
  126. package/dist/{virtual-modules-wyp2k6z3.js → virtual-modules-4r3zkc20.js} +2 -2
  127. package/dist/{work-objects-jem2fre0.js → work-objects-61w08fwr.js} +23 -23
  128. package/dist/worker-c1e68hpa.js +78 -0
  129. package/dist/{x-8ewt8gxn.js → x-y45mthwn.js} +2 -2
  130. package/openapi.json +491 -52
  131. package/package.json +3 -3
  132. package/src/apps/action-runner.ts +146 -0
  133. package/src/apps/catalog.generated.json +3 -2
  134. package/src/apps/definition.ts +152 -4
  135. package/src/apps/row-store.ts +23 -9
  136. package/src/apps/schema-migrate.ts +14 -6
  137. package/src/apps/script-types.ts +2 -1
  138. package/src/apps/sync.ts +20 -19
  139. package/src/be/db/tasks/read.ts +112 -0
  140. package/src/be/db.ts +16 -4
  141. package/src/be/fleet-benchmark-stats.ts +10 -5
  142. package/src/be/models-catalog.ts +4 -0
  143. package/src/be/onboarding.ts +6 -0
  144. package/src/be/scripts/typecheck.ts +13 -0
  145. package/src/be/swarm-config-guard.ts +25 -1
  146. package/src/be/telemetry-enable.ts +1 -1
  147. package/src/commands/provider-credentials.ts +12 -2
  148. package/src/commands/runner.ts +3 -0
  149. package/src/heartbeat/heartbeat.ts +2 -2
  150. package/src/hooks/codex-hook.ts +6 -2
  151. package/src/hooks/hook.ts +7 -0
  152. package/src/hooks/pr-body-guard.ts +38 -11
  153. package/src/hooks/pr-room-guard.ts +88 -0
  154. package/src/http/agents.ts +1 -0
  155. package/src/http/apps.ts +99 -183
  156. package/src/http/fleet-benchmark.ts +79 -30
  157. package/src/http/global-config.ts +7 -0
  158. package/src/http/ranking.ts +24 -40
  159. package/src/http/status.ts +49 -43
  160. package/src/http/tasks.ts +49 -1
  161. package/src/http/telemetry-ui.ts +1 -1
  162. package/src/http/used-by.ts +55 -19
  163. package/src/http/users.ts +2 -2
  164. package/src/providers/acp-targets.ts +1 -1
  165. package/src/providers/amp-adapter.ts +1 -1
  166. package/src/providers/pi-mono-adapter.ts +15 -0
  167. package/src/providers/reasoning-effort.ts +2 -1
  168. package/src/providers/types.ts +2 -0
  169. package/src/ranking/telemetry-ranking.ts +5 -33
  170. package/src/scripts-runtime/sdk-allowlist.ts +7 -0
  171. package/src/scripts-runtime/types/stdlib.d.ts +31 -0
  172. package/src/scripts-runtime/types/swarm-sdk.d.ts +31 -0
  173. package/src/server.ts +4 -0
  174. package/src/telemetry-context.ts +19 -1
  175. package/src/telemetry-contract/EVENTS.md +42 -8
  176. package/src/telemetry-contract/SOURCE.json +6 -5
  177. package/src/telemetry-contract/catalog.json +225 -5
  178. package/src/telemetry-contract/ranking/fixtures/valid/no-claim.json +10 -0
  179. package/src/telemetry-contract/ranking/listing.v1.schema.json +2 -2
  180. package/src/telemetry-contract/types.gen.ts +225 -27
  181. package/src/telemetry-identity.ts +47 -9
  182. package/src/telemetry-snapshot.ts +9 -1
  183. package/src/telemetry-ui-events.ts +9 -2
  184. package/src/telemetry.ts +29 -6
  185. package/src/tests/app-action-run-tool.test.ts +178 -0
  186. package/src/tests/app-row-tools.test.ts +385 -0
  187. package/src/tests/apps-json-column.test.ts +611 -0
  188. package/src/tests/apps-script-types.test.ts +2 -0
  189. package/src/tests/apps-sync-engine.test.ts +42 -0
  190. package/src/tests/check-pr-body.test.ts +0 -23
  191. package/src/tests/credential-check.test.ts +38 -0
  192. package/src/tests/db-task-read-contract.test.ts +4 -0
  193. package/src/tests/db-task-write-contract.test.ts +2 -0
  194. package/src/tests/docs-index-footprint.test.js +2 -2
  195. package/src/tests/fleet-benchmark.test.ts +122 -2
  196. package/src/tests/http-users.test.ts +62 -1
  197. package/src/tests/model-catalog-table.test.ts +63 -0
  198. package/src/tests/model-profiles-api.test.ts +19 -7
  199. package/src/tests/models-catalog.test.ts +50 -0
  200. package/src/tests/onboarding.test.ts +21 -0
  201. package/src/tests/org-website.test.ts +62 -0
  202. package/src/tests/pi-mono-adapter.test.ts +23 -0
  203. package/src/tests/pr-body-leaks.test.ts +4 -0
  204. package/src/tests/pr-room-guard.test.ts +242 -0
  205. package/src/tests/ranking-listing.test.ts +8 -23
  206. package/src/tests/ranking-route.test.ts +49 -47
  207. package/src/tests/reasoning-effort.test.ts +39 -3
  208. package/src/tests/runner-harness-telemetry-env.test.ts +62 -1
  209. package/src/tests/status.test.ts +158 -9
  210. package/src/tests/swarm-tool-result-gate.test.ts +2 -4
  211. package/src/tests/task-lineage.test.ts +180 -0
  212. package/src/tests/telemetry-context.test.ts +128 -2
  213. package/src/tests/telemetry-init.test.ts +62 -5
  214. package/src/tests/telemetry-ui-route.test.ts +123 -0
  215. package/src/tests/tool-annotations.test.ts +3 -2
  216. package/src/tests/used-by-route.test.ts +144 -10
  217. package/src/tests/user-token-rest-auth.test.ts +38 -0
  218. package/src/tests/workflow-agent-task-retry.test.ts +130 -4
  219. package/src/tests/workflow-wait-recovery.test.ts +116 -4
  220. package/src/tools/app-action-run.ts +63 -0
  221. package/src/tools/app-rows.ts +239 -0
  222. package/src/tools/model-catalog/index.ts +1 -1
  223. package/src/tools/tool-config.ts +8 -0
  224. package/src/tools/utils.ts +1 -1
  225. package/src/tools/workflows/retry-workflow-run.ts +1 -1
  226. package/src/types.ts +17 -0
  227. package/src/utils/company-profile.ts +24 -0
  228. package/src/utils/org-website.ts +58 -21
  229. package/src/workflows/recovery.ts +10 -22
  230. package/src/workflows/resume.ts +16 -0
  231. package/src/workflows/task-step-routing.ts +47 -7
  232. package/templates/skills/apps/SKILL.md +44 -4
  233. package/templates/skills/apps/content.md +44 -4
  234. package/dist/handlers-zbs90bny.js +0 -39
  235. package/dist/index-8pwc1jc8.js +0 -40
  236. package/dist/index-nxa6me0q.js +0 -41
  237. package/dist/lead-td8h2sfn.js +0 -78
  238. package/dist/worker-wr6xgfr2.js +0 -78
  239. package/src/tests/check-ranking-entry.test.ts +0 -146
@@ -3,13 +3,9 @@ import { z } from "zod";
3
3
  import { getDbClient, upsertSwarmConfig } from "../be/db";
4
4
  import {
5
5
  buildListing,
6
- mintRankingClaim,
7
6
  PUBLIC_RANKING_ENABLED_KEY,
8
7
  RANKING_CATEGORIES,
9
- rankingEntryFile,
10
8
  rankingFormSchema,
11
- rankingLeaveUrl,
12
- rankingPrUrl,
13
9
  readTelemetryRanking,
14
10
  TELEMETRY_RANKING_KEY,
15
11
  type TelemetryRanking,
@@ -23,14 +19,14 @@ import {
23
19
  } from "../utils/org-identity";
24
20
  import { ensureConfigAdmin } from "./config";
25
21
  import { scheduleIntegrationsReload } from "./core";
22
+ import { globalConfig } from "./global-config";
26
23
  import { route } from "./route-def";
27
- import { globalConfig, telemetryProxyEndpoint } from "./used-by";
24
+ import { telemetryProxyEndpoint } from "./used-by";
28
25
 
29
26
  // ─── Schemas ─────────────────────────────────────────────────────────────────
30
27
 
31
28
  const ListingSchema = z.object({
32
29
  v: z.literal(1),
33
- claim: z.string(),
34
30
  name: z.string(),
35
31
  domain: z.string(),
36
32
  category: z.enum(RANKING_CATEGORIES),
@@ -49,26 +45,21 @@ const RankingStateSchema = z.object({
49
45
  name: z.string().nullable(),
50
46
  domain: z.string().nullable(),
51
47
  listing: ListingSchema.nullable(),
52
- /** False when the stored listing was built for another website: a new PR is needed. */
48
+ /** False when the stored listing was built for another website: Get ranked again. */
53
49
  listingMatchesDomain: z.boolean(),
54
- fileJson: z.string().nullable(),
55
- prUrl: z.string().nullable(),
50
+ /** The proxy page that checks whether the domain would be ranked. */
56
51
  verifyUrl: z.string().nullable(),
57
52
  });
58
53
  export type RankingState = z.infer<typeof RankingStateSchema>;
59
54
 
60
55
  const OptInResponseSchema = z.object({
61
56
  domain: z.string(),
62
- claim: z.string(),
63
57
  listing: ListingSchema,
64
- fileJson: z.string(),
65
- prUrl: z.string(),
66
58
  verifyUrl: z.string(),
67
59
  });
68
60
 
69
61
  const OptOutResponseSchema = z.object({
70
62
  enabled: z.literal(false),
71
- leaveUrl: z.string().nullable(),
72
63
  });
73
64
 
74
65
  // ─── Route Definitions ───────────────────────────────────────────────────────
@@ -79,7 +70,7 @@ const getRanking = route({
79
70
  pattern: ["api", "ranking"],
80
71
  summary: "Public ranking state",
81
72
  description:
82
- "Whether this install can join the public agent-swarm.dev ranking (telemetry on, organization name and website set), whether it is opted in, and its stored listing. The listing carries the private ranking claim, so only a config admin can read it.",
73
+ "Whether this install can join the public agent-swarm.dev ranking (telemetry on, organization name and website set), whether it is opted in, and its stored listing. Only a config admin can read it.",
83
74
  tags: ["Telemetry"],
84
75
  responses: {
85
76
  200: { description: "Ranking state", schema: RankingStateSchema },
@@ -95,7 +86,7 @@ const optIn = route({
95
86
  pattern: ["api", "ranking", "opt-in"],
96
87
  summary: "Opt this install into the public ranking",
97
88
  description:
98
- "Builds the telemetry_ranking listing from the organization name and website plus the chosen category, stores it, turns PUBLIC_RANKING_ENABLED on and sends ranking.opted_in. The install is ranked only after a ranking PR with the returned file is merged into desplega-ai/agent-swarm.",
89
+ "Builds the telemetry_ranking listing from the organization name and website plus the chosen category, stores it, turns PUBLIC_RANKING_ENABLED on and sends ranking.opted_in. That is the whole opt-in: the install shows on agent-swarm.dev/ranking at the next hourly refresh.",
99
90
  tags: ["Telemetry"],
100
91
  body: rankingFormSchema,
101
92
  responses: {
@@ -117,7 +108,7 @@ const optOut = route({
117
108
  pattern: ["api", "ranking", "opt-out"],
118
109
  summary: "Leave the public ranking",
119
110
  description:
120
- "Turns PUBLIC_RANKING_ENABLED off and sends ranking.opted_out, so workers stop sending ranking.usage. Returns the GitHub page that deletes the ranking entry.",
111
+ "Turns PUBLIC_RANKING_ENABLED off and sends ranking.opted_out, so workers stop sending ranking.usage. The install leaves agent-swarm.dev/ranking at the next hourly refresh.",
121
112
  tags: ["Telemetry"],
122
113
  responses: {
123
114
  200: { description: "Opted out", schema: OptOutResponseSchema },
@@ -163,11 +154,17 @@ async function telemetryReady(): Promise<boolean> {
163
154
  return isTelemetryEnabled() && Boolean(await globalConfig("telemetry_installation_id"));
164
155
  }
165
156
 
166
- async function verifyUrl(listing: Pick<TelemetryRanking, "domain" | "claim">): Promise<string> {
167
- const params = new URLSearchParams({ domain: listing.domain, claim: listing.claim });
157
+ async function verifyUrl(domain: string): Promise<string> {
158
+ const params = new URLSearchParams({ domain });
168
159
  return `${await telemetryProxyEndpoint("/v1/ranking/verify")}?${params.toString()}`;
169
160
  }
170
161
 
162
+ /** The listing without the claim a v2.4.0 opt-in stored. */
163
+ function withoutClaim(listing: TelemetryRanking): Omit<TelemetryRanking, "claim"> {
164
+ const { claim: _claim, ...rest } = listing;
165
+ return rest;
166
+ }
167
+
171
168
  async function setEnabled(enabled: boolean): Promise<void> {
172
169
  await upsertSwarmConfig({
173
170
  scope: "global",
@@ -179,7 +176,7 @@ async function setEnabled(enabled: boolean): Promise<void> {
179
176
  function emitOptedIn(listing: TelemetryRanking): void {
180
177
  telemetry.ranking(
181
178
  "opted_in",
182
- { listing: JSON.stringify(listing) },
179
+ { listing: JSON.stringify(withoutClaim(listing)) },
183
180
  { orgName: listing.name, orgWebsite: listing.domain },
184
181
  );
185
182
  }
@@ -204,17 +201,15 @@ export async function buildRankingState(): Promise<RankingState> {
204
201
  enabled: enabled && listingMatchesDomain,
205
202
  name: identity.name,
206
203
  domain: identity.website,
207
- listing,
204
+ listing: listing ? withoutClaim(listing) : null,
208
205
  listingMatchesDomain,
209
- fileJson: current ? rankingEntryFile(current) : null,
210
- prUrl: current ? rankingPrUrl(current) : null,
211
- verifyUrl: current ? await verifyUrl(current) : null,
206
+ verifyUrl: current ? await verifyUrl(current.domain) : null,
212
207
  };
213
208
  }
214
209
 
215
210
  /**
216
211
  * Keep the listing in step with the org identity after a global config write.
217
- * A renamed org rebuilds the listing with the same claim and re-sends it; a
212
+ * A renamed org rebuilds the listing and re-sends it; a
218
213
  * cleared or placeholder name, a changed or cleared website, or the setting
219
214
  * turned off by hand opts the install out.
220
215
  */
@@ -238,7 +233,7 @@ export async function syncRankingAfterConfigChange(
238
233
  if (identity.ok && identity.website === listing.domain) {
239
234
  if (identity.name === listing.name) return;
240
235
  try {
241
- rebuilt = buildListing(identity, listing, listing.claim);
236
+ rebuilt = buildListing(identity, listing);
242
237
  } catch {
243
238
  // A name the listing cannot carry (over 60 characters) leaves the ranking.
244
239
  }
@@ -272,7 +267,6 @@ export async function handleRanking(
272
267
  if (getRanking.match(req.method, pathSegments)) {
273
268
  const parsed = await getRanking.parse(req, res, pathSegments, queryParams);
274
269
  if (!parsed) return true;
275
- // The state carries the claim and the PR that proves ownership of the listing.
276
270
  if (!(await ensureConfigAdmin(req, res, "config.write.any"))) return true;
277
271
  getRanking.respond(res, 200, await buildRankingState());
278
272
  return true;
@@ -300,16 +294,12 @@ export async function handleRanking(
300
294
  return true;
301
295
  }
302
296
 
303
- const website = identity.website;
304
297
  const db = getDbClient();
305
- // Read the stored claim under the write lock: two concurrent first opt-ins
306
- // must return the same claim, or the one merged into community/ranking is lost.
298
+ // The listing and the flag land together, and the event fires only after both commit.
307
299
  const built = await db.transaction(async () => {
308
- const stored = await storedListing();
309
- const claim = stored?.domain === website ? stored.claim : mintRankingClaim();
310
300
  let listing: TelemetryRanking;
311
301
  try {
312
- listing = buildListing(identity, parsed.body, claim);
302
+ listing = buildListing(identity, parsed.body);
313
303
  } catch (error) {
314
304
  return { ok: false as const, error };
315
305
  }
@@ -339,11 +329,8 @@ export async function handleRanking(
339
329
 
340
330
  optIn.respond(res, 200, {
341
331
  domain: listing.domain,
342
- claim: listing.claim,
343
332
  listing,
344
- fileJson: rankingEntryFile(listing),
345
- prUrl: rankingPrUrl(listing),
346
- verifyUrl: await verifyUrl(listing),
333
+ verifyUrl: await verifyUrl(listing.domain),
347
334
  });
348
335
  return true;
349
336
  }
@@ -356,10 +343,7 @@ export async function handleRanking(
356
343
  await setEnabled(false);
357
344
  if (listing) emitOptedOut(listing.domain);
358
345
  scheduleIntegrationsReload();
359
- optOut.respond(res, 200, {
360
- enabled: false,
361
- leaveUrl: listing ? rankingLeaveUrl(listing.domain) : null,
362
- });
346
+ optOut.respond(res, 200, { enabled: false });
363
347
  return true;
364
348
  }
365
349
 
@@ -197,7 +197,7 @@ export type AutomationStatus = z.infer<typeof AutomationStatusSchema>;
197
197
  * - `broken` — harness or workers blocking (creds missing, no live workers ever).
198
198
  * - `degraded` — at least one optional integration `unverified`/`configured`
199
199
  * while another is configured, OR harness creds present but
200
- * never tested (`configured`).
200
+ * the latest live test failed or never ran (`configured`).
201
201
  * - `ok` — harness + workers `verified`; no integration left in
202
202
  * `configured` state.
203
203
  */
@@ -237,12 +237,12 @@ export const TestConnectionResponseSchema = z.object({
237
237
  /**
238
238
  * Read all worker reports for a given harness provider and reduce them to a
239
239
  * single milestone-grade rollup. Reports are joined from `agents.cred_status`
240
- * (migration 055) — no provider-adapter code runs here.
240
+ * (migration 055) — no provider-adapter code runs here. Offline agents are
241
+ * excluded: `workers` and `reports` count online agents only.
241
242
  *
242
- * `verified` ⇐ at least one worker has `liveTest.ok === true` and the test
243
- * is fresher than `getCredVerifyTtlMs()`.
243
+ * `verified` ⇐ the provider's most recent live test passed, whatever its age.
244
244
  * `configured` ⇐ at least one worker has `ready: true` (presence check
245
- * passed) but no fresh passing live test.
245
+ * passed) but the latest live test failed or none ever ran.
246
246
  * `unverified` ⇐ no worker reported, or all reporting workers have
247
247
  * `ready: false`.
248
248
  */
@@ -258,14 +258,6 @@ export interface CredRollup {
258
258
  oldestReportAgeMs: number | null;
259
259
  }
260
260
 
261
- function getCredVerifyTtlMs(): number {
262
- const raw = process.env.SWARM_VERIFY_TTL_MS;
263
- if (!raw) return 3_600_000; // 1h default — matches pre-refactor behavior.
264
- const parsed = Number.parseInt(raw, 10);
265
- if (Number.isNaN(parsed) || parsed <= 0) return 3_600_000;
266
- return parsed;
267
- }
268
-
269
261
  /**
270
262
  * Compatibility stub. The pre-refactor in-memory test-connection cache is
271
263
  * gone — credential state now lives on agent rows. Tests still call this
@@ -276,7 +268,11 @@ export function _resetTestConnectionCache(): void {
276
268
  }
277
269
 
278
270
  export async function rollupCredStatusForProvider(provider: string): Promise<CredRollup> {
279
- const agents = await listAgentsWithCredStatusByProvider(provider);
271
+ // An offline agent keeps its last cred_status after `/close`, so only online
272
+ // agents decide the rollup; a stale failure must not override a live pass.
273
+ const agents = (await listAgentsWithCredStatusByProvider(provider)).filter(
274
+ (a) => a.status !== "offline",
275
+ );
280
276
  const reports = agents.map((a) => a.credStatus).filter((s): s is AgentCredStatus => s != null);
281
277
 
282
278
  if (reports.length === 0) {
@@ -291,23 +287,19 @@ export async function rollupCredStatusForProvider(provider: string): Promise<Cre
291
287
  };
292
288
  }
293
289
 
294
- // Pick the most-recent passing live test, if any, then fall back to
295
- // most-recent live test of any kind.
296
- const ttl = getCredVerifyTtlMs();
290
+ // The provider's latest live test decides, whatever its age. Workers run the
291
+ // test at boot and re-run it only when credentials change, so an old pass is
292
+ // the normal state of a long-running worker, not a missing check.
297
293
  const now = Date.now();
298
- const isFreshPassingReport = (report: AgentCredStatus): boolean =>
299
- report.liveTest?.ok === true && now - (report.liveTest?.testedAt ?? 0) < ttl;
300
- const passing = reports
301
- .filter(isFreshPassingReport)
302
- .sort((a, b) => (b.liveTest?.testedAt ?? 0) - (a.liveTest?.testedAt ?? 0));
294
+ const passing = reports.filter((r) => r.liveTest?.ok === true);
303
295
  const anyLive = reports
304
296
  .filter((r) => r.liveTest != null)
305
297
  .sort((a, b) => (b.liveTest?.testedAt ?? 0) - (a.liveTest?.testedAt ?? 0));
306
- const latestLiveTest = passing[0]?.liveTest ?? anyLive[0]?.liveTest ?? null;
298
+ const latestLiveTest = anyLive[0]?.liveTest ?? null;
307
299
 
308
300
  const anyReady = reports.some((r) => r.ready);
309
301
  const state: CredRollupState =
310
- passing.length > 0 ? "verified" : anyReady ? "configured" : "unverified";
302
+ latestLiveTest?.ok === true ? "verified" : anyReady ? "configured" : "unverified";
311
303
 
312
304
  // For UI hints, pick the most-recent missing[] from a not-ready report.
313
305
  const latestNotReady = reports
@@ -349,23 +341,19 @@ function buildIdentity(): StatusIdentity {
349
341
  // ─── Setup milestones ────────────────────────────────────────────────────────
350
342
 
351
343
  /**
352
- * Compose a one-line, per-provider description for the milestone hint.
353
- * Examples:
354
- * "1 worker · live test ok"
355
- * "2 workers · presence ok, awaiting live test"
356
- * "missing: OPENAI_API_KEY"
344
+ * One short phrase for a provider that needs attention, e.g. "live test
345
+ * failed" or "missing: OPENAI_API_KEY". A verified provider gets none.
357
346
  */
358
- function describeRoll(roll: CredRollup): string {
347
+ function describeRoll(roll: CredRollup): string | null {
348
+ if (roll.state === "verified") return null;
359
349
  if (roll.workers === 0) return "no workers";
360
- if (roll.reports === 0) {
361
- return `${roll.workers} ${roll.workers === 1 ? "worker" : "workers"}, none reported`;
350
+ if (roll.reports === 0) return "no credential report";
351
+ if (roll.state === "configured") {
352
+ return roll.latestLiveTest?.ok === false ? "live test failed" : "not live-tested";
362
353
  }
363
- const w = `${roll.workers} ${roll.workers === 1 ? "worker" : "workers"}`;
364
- if (roll.state === "verified") return `${w} · live test ok`;
365
- if (roll.state === "configured") return `${w} · presence ok, awaiting live test`;
366
354
  return roll.latestMissing.length > 0
367
355
  ? `missing: ${roll.latestMissing.join(", ")}`
368
- : "creds blocked";
356
+ : "credentials blocked";
369
357
  }
370
358
 
371
359
  /**
@@ -375,7 +363,7 @@ function describeRoll(roll: CredRollup): string {
375
363
  * workers via migration 054/055), runs the cred-status rollup per provider,
376
364
  * and aggregates:
377
365
  *
378
- * `verified` — every provider in the fleet has a fresh passing live test.
366
+ * `verified` — every provider's latest live test passed.
379
367
  * `configured` — every provider has at least `configured`, ≥1 not verified.
380
368
  * `unverified` — any provider has `unverified` (no reports OR all-blocked).
381
369
  *
@@ -398,13 +386,25 @@ async function harnessMilestone(): Promise<SetupMilestone> {
398
386
  }
399
387
 
400
388
  const perProvider: { provider: z.infer<typeof ProviderNameSchema>; roll: CredRollup }[] = [];
389
+ let recognised = 0;
401
390
  for (const { provider } of fleet) {
402
391
  const parsed = ProviderNameSchema.safeParse(provider);
403
392
  if (!parsed.success) continue;
404
- perProvider.push({
405
- provider: parsed.data,
406
- roll: await rollupCredStatusForProvider(parsed.data),
407
- });
393
+ recognised++;
394
+ const roll = await rollupCredStatusForProvider(parsed.data);
395
+ // A provider whose agents are all offline has nothing live to judge.
396
+ if (roll.workers === 0) continue;
397
+ perProvider.push({ provider: parsed.data, roll });
398
+ }
399
+
400
+ if (recognised > 0 && perProvider.length === 0) {
401
+ return {
402
+ id: "harness",
403
+ label: "Harness configured",
404
+ state: "unverified",
405
+ hint: "No agents online. Start a worker to run a live credential test.",
406
+ action_url: "/agents",
407
+ };
408
408
  }
409
409
 
410
410
  if (perProvider.length === 0) {
@@ -424,7 +424,13 @@ async function harnessMilestone(): Promise<SetupMilestone> {
424
424
  ? "configured"
425
425
  : "unverified";
426
426
 
427
- const hint = perProvider.map((p) => `${p.provider}: ${describeRoll(p.roll)}`).join(" · ");
427
+ const hint =
428
+ perProvider
429
+ .flatMap((p) => {
430
+ const phrase = describeRoll(p.roll);
431
+ return phrase ? [`${p.provider}: ${phrase}`] : [];
432
+ })
433
+ .join(" · ") || undefined;
428
434
  const singleProvider = perProvider.length === 1 ? perProvider[0]?.provider : undefined;
429
435
 
430
436
  return {
@@ -848,7 +854,7 @@ export async function handleStatus(
848
854
  if (roll.workers === 0) {
849
855
  json(res, {
850
856
  ok: false,
851
- error: `No workers registered with HARNESS_PROVIDER=${provider}. Start a worker — its boot loop will run a live test and report it here.`,
857
+ error: `No online workers with HARNESS_PROVIDER=${provider}. Start a worker — its boot loop will run a live test and report it here.`,
852
858
  latency_ms: 0,
853
859
  });
854
860
  return true;
package/src/http/tasks.ts CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  getSteeringMessagesForTask,
21
21
  getTaskAttachments,
22
22
  getTaskById,
23
+ getTaskLineage,
23
24
  getTasksCount,
24
25
  markSteeringDelivered,
25
26
  markSteeringHandled,
@@ -29,6 +30,7 @@ import {
29
30
  resumeTask,
30
31
  settleSupersededTaskDependents,
31
32
  supersedeTask,
33
+ TASK_LINEAGE_LIMITS,
32
34
  updateAgentStatusFromCapacity,
33
35
  updateTaskClaudeSessionId,
34
36
  updateTaskProgress,
@@ -73,6 +75,7 @@ import {
73
75
  SteeringSourceSchema,
74
76
  SteerModeSchema,
75
77
  SteerResultSchema,
78
+ SUPERSEDED_WORKFLOW_TASK_REASON,
76
79
  splitLegacyModelAlias,
77
80
  TaskAttachmentSchema,
78
81
  } from "../types";
@@ -577,6 +580,33 @@ const getTask = route({
577
580
  },
578
581
  });
579
582
 
583
+ const getTaskLineageRoute = route({
584
+ method: "get",
585
+ path: "/api/tasks/{id}/lineage",
586
+ pattern: ["api", "tasks", null, "lineage"],
587
+ session: { taskParam: "id" },
588
+ summary: "Get the tree a task belongs to (ancestors and descendants)",
589
+ description: `Follows \`parentTaskId\` up to the topmost ancestor, then returns every descendant of that root as slim task rows, the task's own subtree first. Bounded: at most ${TASK_LINEAGE_LIMITS.maxAncestors} hops up, \`maxDepth\` levels down and about twice \`maxNodes\` rows; \`truncated\` is true when any task in the tree is missing from the response.`,
590
+ tags: ["Tasks"],
591
+ params: z.object({ id: z.string() }),
592
+ query: z.object({
593
+ maxNodes: z.coerce.number().int().min(1).max(TASK_LINEAGE_LIMITS.maxNodes).optional(),
594
+ maxDepth: z.coerce.number().int().min(1).max(TASK_LINEAGE_LIMITS.maxDepth).optional(),
595
+ }),
596
+ responses: {
597
+ 200: {
598
+ description: "The lineage tree as a flat list linked by `parentTaskId`",
599
+ schema: z.object({
600
+ rootTaskId: z.string(),
601
+ tasks: z.array(AgentTaskSummarySchema.extend({ workflowRunId: z.string().optional() })),
602
+ truncated: z.boolean(),
603
+ }),
604
+ },
605
+ 403: { description: "A user caller cannot read the task's session" },
606
+ 404: { description: "Task not found" },
607
+ },
608
+ });
609
+
580
610
  const updateTaskProgressRoute = route({
581
611
  method: "post",
582
612
  path: "/api/tasks/{id}/progress",
@@ -1120,6 +1150,8 @@ export async function handleTasks(
1120
1150
  offeredTo: parsed.body.offeredTo || undefined,
1121
1151
  dir: parsed.body.dir || undefined,
1122
1152
  parentTaskId: parsed.body.parentTaskId || undefined,
1153
+ // Human chat starts a new output contract; agent children keep inheriting.
1154
+ inheritParentOutputSchema: principal.kind === "agent",
1123
1155
  source: parsed.body.source || "api",
1124
1156
  outputSchema: parsed.body.outputSchema || undefined,
1125
1157
  contextKey: parsed.body.contextKey || undefined,
@@ -1614,6 +1646,22 @@ export async function handleTasks(
1614
1646
  return true;
1615
1647
  }
1616
1648
 
1649
+ if (getTaskLineageRoute.match(req.method, pathSegments)) {
1650
+ const parsed = await getTaskLineageRoute.parse(req, res, pathSegments, queryParams);
1651
+ if (!parsed) return true;
1652
+ // Every task in the tree shares the session root, so the path posture covers them all.
1653
+ const lineage = await getTaskLineage(parsed.params.id, {
1654
+ maxNodes: parsed.query.maxNodes,
1655
+ maxDepth: parsed.query.maxDepth,
1656
+ });
1657
+ if (!lineage) {
1658
+ jsonError(res, "Task not found", 404);
1659
+ return true;
1660
+ }
1661
+ getTaskLineageRoute.respond(res, 200, lineage);
1662
+ return true;
1663
+ }
1664
+
1617
1665
  if (updateTaskProgressRoute.match(req.method, pathSegments)) {
1618
1666
  const parsed = await updateTaskProgressRoute.parse(req, res, pathSegments, queryParams);
1619
1667
  if (!parsed) return true;
@@ -2014,7 +2062,7 @@ export async function handleTasks(
2014
2062
  // Check this BEFORE the supersede UPDATE so we don't leave a workflow
2015
2063
  // step in `superseded` if the engine expects `failed`.
2016
2064
  if (task.workflowRunStepId != null) {
2017
- const failed = await failTask(parsed.params.id, "superseded_workflow_task");
2065
+ const failed = await failTask(parsed.params.id, SUPERSEDED_WORKFLOW_TASK_REASON);
2018
2066
  ensure({
2019
2067
  id: "task.workflow_step_failed_on_supersede",
2020
2068
  flow: "task",
@@ -43,7 +43,7 @@ const enableTelemetry = route({
43
43
  description:
44
44
  "Stores `ANONYMIZED_TELEMETRY=true` and applies it to the running server: the install ID is minted if needed and `telemetry.enabled` is sent once, so no restart is needed.",
45
45
  tags: ["Telemetry"],
46
- body: z.object({ via: z.enum(["banner", "settings"]).default("banner") }),
46
+ body: z.object({ via: z.enum(["banner", "settings", "benchmark"]).default("banner") }),
47
47
  responses: {
48
48
  200: {
49
49
  description: "Telemetry is on",
@@ -1,12 +1,14 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  import { z } from "zod";
3
- import { getSwarmConfigs, upsertSwarmConfig } from "../be/db";
3
+ import { getDbClient, upsertSwarmConfig } from "../be/db";
4
4
  import { isTelemetryEnabled, trackRelayed } from "../telemetry";
5
+ import { USER_FUNCTIONS } from "../utils/company-profile";
5
6
  import { orgIdentityStatus } from "../utils/org-identity";
6
7
  import { normalizeOrgWebsite, TELEMETRY_PROXY_ORIGIN } from "../utils/org-website";
7
8
  import { ensureConfigAdmin } from "./config";
8
9
  import { scheduleIntegrationsReload } from "./core";
9
10
  import { resolveHttpFavoriteOwner } from "./favorite-owner";
11
+ import { globalConfig } from "./global-config";
10
12
  import { route } from "./route-def";
11
13
  import { jsonError } from "./utils";
12
14
 
@@ -16,6 +18,9 @@ const CONSENT_AT_KEY = "SWARM_USED_BY_CONSENT_AT";
16
18
  // still reaches the proxy after the name or website is changed or cleared.
17
19
  const GRANTED_NAME_KEY = "SWARM_USED_BY_ORG_NAME";
18
20
  const GRANTED_WEBSITE_KEY = "SWARM_USED_BY_ORG_WEBSITE";
21
+ const CONTACT_EMAIL_KEY = "SWARM_USED_BY_CONTACT_EMAIL";
22
+ const CONTACT_FUNCTION_KEY = "SWARM_USED_BY_CONTACT_FUNCTION";
23
+ const QUOTE_KEY = "SWARM_USED_BY_QUOTE";
19
24
  const TIMEOUT_MS = 10_000;
20
25
 
21
26
  // ─── Route Definitions ───────────────────────────────────────────────────────
@@ -26,17 +31,30 @@ const usedBy = route({
26
31
  pattern: ["api", "used-by"],
27
32
  summary: "Grant or revoke the 'used by' logo consent",
28
33
  description:
29
- "Records whether agent-swarm.dev may show this organization's logo as a company that uses Agent Swarm. Sends the organization name and website to the telemetry proxy, where a person approves each logo by hand. An explicit admin action, so it is not gated by the telemetry opt-out.",
34
+ "Records consent to show the organization's logo on agent-swarm.dev. A grant requires a valid contact email. It sends organization identity, contact details, and an optional quote to the proxy for manual review. This explicit admin action works when telemetry is off. A revoke clears the stored contact details and quote.",
30
35
  tags: ["Telemetry"],
31
- body: z.object({ granted: z.boolean() }),
36
+ body: z
37
+ .object({
38
+ granted: z.boolean(),
39
+ contactEmail: z
40
+ .string()
41
+ .max(320, "Contact email must be at most 320 characters")
42
+ .email("Contact email must be a valid email address")
43
+ .optional(),
44
+ contactFunction: z.enum(USER_FUNCTIONS).optional(),
45
+ quote: z.string().max(280).optional(),
46
+ })
47
+ .refine((body) => !body.granted || body.contactEmail !== undefined, {
48
+ path: ["contactEmail"],
49
+ message: "Contact email is required when granting logo consent",
50
+ }),
32
51
  responses: {
33
52
  200: {
34
53
  description: "Consent recorded",
35
54
  schema: z.object({ granted: z.boolean(), changedAt: z.string() }),
36
55
  },
37
56
  400: {
38
- description:
39
- "Organization name or website is not set (a revoke falls back to the identity of the last grant)",
57
+ description: "Invalid contact details, or missing organization name or website",
40
58
  },
41
59
  401: { description: "Unauthorized" },
42
60
  403: { description: "Only a config admin can change the consent" },
@@ -47,12 +65,6 @@ const usedBy = route({
47
65
 
48
66
  // ─── Helpers ─────────────────────────────────────────────────────────────────
49
67
 
50
- /** A global config value, falling back to the process env. */
51
- export async function globalConfig(key: string): Promise<string | undefined> {
52
- const stored = (await getSwarmConfigs({ scope: "global", key }))?.[0]?.value?.trim();
53
- return stored || process.env[key]?.trim() || undefined;
54
- }
55
-
56
68
  /** A proxy path next to the configured `/v1/feedback` endpoint, so a stubbed proxy gets every route. */
57
69
  export async function telemetryProxyEndpoint(path: `/v1/${string}`): Promise<string> {
58
70
  const feedback = await globalConfig("feedback_endpoint");
@@ -80,7 +92,7 @@ export async function handleUsedBy(
80
92
  if (!parsed) return true;
81
93
  if (!(await ensureConfigAdmin(req, res, "config.write.any"))) return true;
82
94
 
83
- const { granted } = parsed.body;
95
+ const { granted, contactEmail, contactFunction, quote } = parsed.body;
84
96
  // The shared identity reader. The grant keeps its own rule (any non-empty
85
97
  // name), so it does not apply the ranking's placeholder-name check.
86
98
  const current = orgIdentityStatus({
@@ -116,6 +128,9 @@ export async function handleUsedBy(
116
128
  org_website: orgWebsite,
117
129
  ...(orgDomain ? { org_domain: orgDomain } : {}),
118
130
  granted,
131
+ ...(granted
132
+ ? { contact_email: contactEmail, contact_function: contactFunction, quote }
133
+ : {}),
119
134
  changed_at: changedAt,
120
135
  }),
121
136
  signal: AbortSignal.timeout(TIMEOUT_MS),
@@ -129,18 +144,39 @@ export async function handleUsedBy(
129
144
  return true;
130
145
  }
131
146
 
132
- await upsertSwarmConfig({ scope: "global", key: CONSENT_KEY, value: String(granted) });
133
- await upsertSwarmConfig({ scope: "global", key: CONSENT_AT_KEY, value: changedAt });
134
- if (granted) {
135
- await upsertSwarmConfig({ scope: "global", key: GRANTED_NAME_KEY, value: orgName });
136
- await upsertSwarmConfig({ scope: "global", key: GRANTED_WEBSITE_KEY, value: orgWebsite });
137
- }
147
+ await getDbClient().transaction(async () => {
148
+ await upsertSwarmConfig({ scope: "global", key: CONSENT_KEY, value: String(granted) });
149
+ await upsertSwarmConfig({ scope: "global", key: CONSENT_AT_KEY, value: changedAt });
150
+ if (granted) {
151
+ await upsertSwarmConfig({ scope: "global", key: GRANTED_NAME_KEY, value: orgName });
152
+ await upsertSwarmConfig({ scope: "global", key: GRANTED_WEBSITE_KEY, value: orgWebsite });
153
+ }
154
+ // Contact details stay out of agent env. The email is also a secret row,
155
+ // so `GET /api/config` masks it for every reader.
156
+ for (const [key, value, isSecret] of [
157
+ [CONTACT_EMAIL_KEY, contactEmail, true],
158
+ [CONTACT_FUNCTION_KEY, contactFunction, false],
159
+ [QUOTE_KEY, quote, false],
160
+ ] as const) {
161
+ await upsertSwarmConfig({
162
+ scope: "global",
163
+ key,
164
+ value: granted ? (value ?? "") : "",
165
+ isSecret,
166
+ exportToEnv: false,
167
+ });
168
+ }
169
+ });
138
170
  scheduleIntegrationsReload();
139
171
 
140
172
  const owner = await resolveHttpFavoriteOwner(req, myAgentId);
141
173
  trackRelayed(
142
174
  "used_by.consent_changed",
143
- { granted },
175
+ {
176
+ granted,
177
+ has_quote: granted && Boolean(quote),
178
+ has_function: granted && Boolean(contactFunction),
179
+ },
144
180
  { actor: owner?.userId ? { userId: owner.userId } : undefined, emitter: "ui" },
145
181
  );
146
182
  usedBy.respond(res, 200, { granted, changedAt });
package/src/http/users.ts CHANGED
@@ -205,7 +205,7 @@ const createUserRoute = route({
205
205
  tags: ["Users"],
206
206
  body: z.object({
207
207
  name: z.string().min(1),
208
- email: z.string().optional(),
208
+ email: z.string().email("Email must be a valid email address").optional(),
209
209
  role: z.string().optional(),
210
210
  notes: z.string().optional(),
211
211
  emailAliases: z.array(z.string()).optional(),
@@ -307,7 +307,7 @@ const updateUserRoute = route({
307
307
  body: z
308
308
  .object({
309
309
  name: z.string().min(1).optional(),
310
- email: z.string().optional(),
310
+ email: z.string().email("Email must be a valid email address").nullable().optional(),
311
311
  role: z.string().optional(),
312
312
  notes: z.string().optional(),
313
313
  emailAliases: z.array(z.string()).optional(),
@@ -359,7 +359,7 @@ const copilotTargetProfile: AcpTargetProfile = {
359
359
  },
360
360
  };
361
361
 
362
- /** Qwen Code's `--auth-type` choices (0.25.2). `qwen-oauth` needs a browser, so it is left out. */
362
+ /** Qwen Code's `--auth-type` choices (0.25.3). `qwen-oauth` needs a browser, so it is left out. */
363
363
  const QWEN_AUTH_TYPES = ["openai", "openai-responses", "anthropic", "gemini", "vertex-ai"] as const;
364
364
 
365
365
  // Qwen Code is a Gemini CLI fork: QWEN_SYSTEM_MD replaces its built-in prompt
@@ -44,7 +44,7 @@ import type {
44
44
  * default; the image installs exactly this version and the per-task settings
45
45
  * set `amp.updates.mode: "disabled"`. Bump it deliberately, with a live run.
46
46
  */
47
- export const AMP_PACKAGE = "@ampcode/cli@0.0.1792324855-g9c86a5";
47
+ export const AMP_PACKAGE = "@ampcode/cli@0.0.1792411243-gb8cffc";
48
48
 
49
49
  /** MCP server name. Amp exposes its tools as `agent_swarm` / `store_progress` (dashes become underscores). */
50
50
  const AMP_MCP_SERVER_NAME = "agent-swarm";