@desplega.ai/agent-swarm 2.2.0 → 2.4.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 (266) hide show
  1. package/README.md +1 -0
  2. package/dist/{acp-adapter-k0vwfyvn.js → acp-adapter-kedstxx6.js} +6 -6
  3. package/dist/{actions-w4pnb8vz.js → actions-vget6r1k.js} +18 -16
  4. package/dist/{amp-adapter-0cmxb2t5.js → amp-adapter-xeb5mktv.js} +2 -2
  5. package/dist/{app-zneem7g9.js → app-mqap8t8m.js} +3 -3
  6. package/dist/{assistant-wbzd2rnr.js → assistant-h0gv97mv.js} +20 -18
  7. package/dist/{boot-reembed-4kx69j3g.js → boot-reembed-qr8a894z.js} +1 -1
  8. package/dist/{boot-reembed-db0wwgy1.js → boot-reembed-rj85jjhw.js} +1 -1
  9. package/dist/{boot-scrub-sweep-tfv4d3mm.js → boot-scrub-sweep-eyfvn6v9.js} +3 -3
  10. package/dist/{boot-seeds-w2hmkxn5.js → boot-seeds-xs9a8dx1.js} +4 -4
  11. package/dist/{claude-adapter-5qrc2paq.js → claude-adapter-bm23cye7.js} +5 -5
  12. package/dist/{claude-managed-adapter-n7ahjyx6.js → claude-managed-adapter-1mwrsm9d.js} +2 -2
  13. package/dist/{claude-managed-setup-7ra32em4.js → claude-managed-setup-gkfgks4j.js} +1 -1
  14. package/dist/{claude-sdk-session-cd02gc76.js → claude-sdk-session-apb4ypz3.js} +5 -5
  15. package/dist/cli-02vhhssk.js +848 -0
  16. package/dist/{cli-4ync7m60.js → cli-0ebey9hg.js} +1 -1
  17. package/dist/{cli-665j3feb.js → cli-0xk505e8.js} +1 -1
  18. package/dist/{cli-8c1m9mhy.js → cli-1cevz46c.js} +5 -5
  19. package/dist/{cli-gzvb3d4a.js → cli-1jx0srjt.js} +3 -3
  20. package/dist/{cli-zj4d6afs.js → cli-1pa3rz69.js} +21 -6
  21. package/dist/{cli-vmbakbq0.js → cli-2rwet3r7.js} +1 -1
  22. package/dist/{cli-azs9bhc5.js → cli-3wjs22x3.js} +2 -2
  23. package/dist/{cli-tba8fxf9.js → cli-4fhyq711.js} +45 -36
  24. package/dist/{cli-ak4c12mh.js → cli-5ff2sjyt.js} +1 -1
  25. package/dist/{cli-6q7pyd3k.js → cli-5s5hx57y.js} +5 -3
  26. package/dist/{cli-h23mr2s6.js → cli-6ftrrwm1.js} +9 -371
  27. package/dist/{cli-9ac0mkg5.js → cli-8b9wvjps.js} +1 -1
  28. package/dist/cli-8cxkw1f5.js +382 -0
  29. package/dist/{cli-xhgtay7w.js → cli-8rwvksbc.js} +2 -2
  30. package/dist/{cli-e09hj50q.js → cli-8wkb09q5.js} +11 -11
  31. package/dist/{cli-ksrphveb.js → cli-a1esmmee.js} +14 -2
  32. package/dist/{cli-xnp2vkx7.js → cli-avmqhatn.js} +7 -7
  33. package/dist/{cli-xx13gqf3.js → cli-b80a1100.js} +1 -1
  34. package/dist/{cli-z1hh99j0.js → cli-bfc3k1x5.js} +3 -3
  35. package/dist/{cli-0k9rcwfy.js → cli-bhwa8rzc.js} +33 -15
  36. package/dist/{cli-8cvvytad.js → cli-bn26xbcs.js} +2 -2
  37. package/dist/{cli-k28p6xh7.js → cli-c8fwpgqb.js} +1 -1
  38. package/dist/{cli-rc3r0bnh.js → cli-ccn0eybf.js} +1 -1
  39. package/dist/{cli-9td5ss65.js → cli-dsgj3e7a.js} +2 -2
  40. package/dist/{cli-jrk8mj0t.js → cli-ecshf504.js} +2 -2
  41. package/dist/{cli-raejy0mp.js → cli-eqgfs202.js} +1 -1
  42. package/dist/{cli-ppapy34w.js → cli-f3nx1h8z.js} +2 -2
  43. package/dist/{cli-c9vrwcpb.js → cli-ffbrkh1r.js} +74 -35
  44. package/dist/{cli-mnry16kh.js → cli-kk2r80n5.js} +4 -4
  45. package/dist/{cli-c0dpzwnx.js → cli-kz116kvw.js} +3 -3
  46. package/dist/{cli-e5y285rg.js → cli-mvg2hf8p.js} +4 -4
  47. package/dist/{cli-2jm992rm.js → cli-mz1ezzzh.js} +2 -2
  48. package/dist/{cli-dw0njd9q.js → cli-nme7wery.js} +6 -6
  49. package/dist/{cli-mvsfbbgq.js → cli-nrrg0fvy.js} +18 -4
  50. package/dist/{cli-8jtrekpy.js → cli-p1m2b6gc.js} +7 -7
  51. package/dist/{cli-4c7k75rq.js → cli-rg7ntypm.js} +378 -84
  52. package/dist/{cli-z3dk189k.js → cli-rsd7ga7v.js} +10 -8
  53. package/dist/{cli-etvn5ej2.js → cli-sppc0cmw.js} +1 -1
  54. package/dist/{cli-8w65vwee.js → cli-tg9ksbn8.js} +1 -1
  55. package/dist/{cli-th8krqbf.js → cli-tgvpmnwz.js} +2 -2
  56. package/dist/{cli-hwc4a9xm.js → cli-tj4j2ts4.js} +6 -6
  57. package/dist/{cli-jhhb2v9n.js → cli-ttaz6c7m.js} +2 -2
  58. package/dist/{cli-w7z94e6c.js → cli-tx00c0h6.js} +4 -2
  59. package/dist/{cli-4g629sgr.js → cli-wrv09v7q.js} +20 -1
  60. package/dist/{cli-cby329ny.js → cli-wyrr38xm.js} +2 -2
  61. package/dist/{cli-5ks6x36q.js → cli-x1p5dhq2.js} +3 -3
  62. package/dist/{cli-bddxamg7.js → cli-x719j3vs.js} +220 -629
  63. package/dist/{cli-ztcxv5nv.js → cli-xfe63zx3.js} +1 -1
  64. package/dist/{cli-q6gw45wj.js → cli-ywyrffff.js} +1 -1
  65. package/dist/{cli-p46xdr1m.js → cli-zvze9rbm.js} +1 -1
  66. package/dist/cli.js +15 -15
  67. package/dist/{codex-adapter-m0fx3v3s.js → codex-adapter-pp2gq8gr.js} +3 -3
  68. package/dist/{codex-hook-x6tm6y5m.js → codex-hook-qwzccd64.js} +1 -1
  69. package/dist/{codex-session-runner-j9m2gfyy.js → codex-session-runner-0vzn09t8.js} +3 -3
  70. package/dist/{commands-ktnqwrv4.js → commands-psqhkttb.js} +2 -2
  71. package/dist/{controller-nex2he8v.js → controller-t20vz2k8.js} +2 -2
  72. package/dist/{controllers-connect-6yhwqv9v.js → controllers-connect-gkxmqw9d.js} +24 -20
  73. package/dist/{cursor-adapter-02s5ekva.js → cursor-adapter-3v542qe5.js} +1 -1
  74. package/dist/{db-jvr86v7f.js → db-2xbyenjr.js} +2 -2
  75. package/dist/{db-19tw9r5s.js → db-nc1rd869.js} +3 -2
  76. package/dist/{dispatcher-d3mnsg2y.js → dispatcher-ewceb8ts.js} +17 -15
  77. package/dist/{dsh-adapter-63rvej88.js → dsh-adapter-75xwh78t.js} +1 -1
  78. package/dist/{easter-egg-3d-6hg2r942.js → easter-egg-3d-xsc47jwf.js} +1 -1
  79. package/dist/{execute-bcqc69a5.js → execute-gzmw1mnw.js} +1 -1
  80. package/dist/{external-channel-44cwer32.js → external-channel-a5dsmxn4.js} +2 -2
  81. package/dist/{google-generative-ai-phpmwxvg.js → google-generative-ai-bmq8gkf3.js} +1 -1
  82. package/dist/{google-vertex-es9ybgdy.js → google-vertex-b1rvp52s.js} +1 -1
  83. package/dist/{grok-adapter-ysp907f6.js → grok-adapter-vj88xr23.js} +7 -7
  84. package/dist/handlers-zbs90bny.js +39 -0
  85. package/dist/{heartbeat-2etacsqq.js → heartbeat-t2m9r3wa.js} +20 -18
  86. package/dist/{hook-mzxxs5n2.js → hook-8femx1bg.js} +4 -4
  87. package/dist/{hook-3kvmax6d.js → hook-xa677f8r.js} +5 -5
  88. package/dist/{http-fn978m0g.js → http-xpmmq028.js} +940 -269
  89. package/dist/{index-e4tsa1h7.js → index-8pwc1jc8.js} +17 -15
  90. package/dist/{index-w2wr89xd.js → index-cdzyfrsp.js} +1 -1
  91. package/dist/{index-1r8hw4vd.js → index-dg2tmj99.js} +17 -15
  92. package/dist/index-nxa6me0q.js +41 -0
  93. package/dist/{index-039x2sag.js → index-s7ckdajq.js} +42 -14
  94. package/dist/{index-wgsw9xcz.js → index-syztqdt6.js} +1 -1
  95. package/dist/{index-x0q2a4f9.js → index-ysd45kgc.js} +1 -1
  96. package/dist/{index-g909y089.js → index-zgvqzjsw.js} +8 -8
  97. package/dist/{keepalive-d4nzs76x.js → keepalive-3nr4st3r.js} +3 -3
  98. package/dist/{lead-8h5n9k5f.js → lead-td8h2sfn.js} +33 -31
  99. package/dist/{maintenance-ea6jfw42.js → maintenance-verwpe2c.js} +1 -1
  100. package/dist/{oauth-refresh-sweep-3f25djq3.js → oauth-refresh-sweep-9q2t0y3q.js} +2 -2
  101. package/dist/{onboard-pvk7hdb2.js → onboard-aytqedmq.js} +2 -2
  102. package/dist/{opencode-adapter-eeesgfqk.js → opencode-adapter-f4hryjny.js} +4 -4
  103. package/dist/{otel-impl-qb7nhgdp.js → otel-impl-hckw4wt1.js} +1 -1
  104. package/dist/{pi-mono-adapter-zasp3ze3.js → pi-mono-adapter-hxby0ce1.js} +6 -6
  105. package/dist/{pricing-refresh-6ehwkcmp.js → pricing-refresh-5nvme3sm.js} +3 -3
  106. package/dist/{rbac-roles-km0f9cmm.js → rbac-roles-pd8c26m1.js} +5 -3
  107. package/dist/{rbac-roles-9648nh00.js → rbac-roles-vbpy21mg.js} +2 -2
  108. package/dist/{render-v2-xkze1wef.js → render-v2-7nqgvk2q.js} +3 -3
  109. package/dist/{resume-tq0bbvt5.js → resume-b6djcj3c.js} +6 -6
  110. package/dist/{runtime-ar8310ba.js → runtime-txrxc9za.js} +1 -1
  111. package/dist/session-access-fc3f9y12.js +25 -0
  112. package/dist/{setup-rnrazxvd.js → setup-n39szm6b.js} +1 -1
  113. package/dist/{virtual-modules-wzk6jf3a.js → virtual-modules-wyp2k6z3.js} +2 -2
  114. package/dist/{work-objects-e75q2rqs.js → work-objects-jem2fre0.js} +20 -18
  115. package/dist/{worker-4ww4hjef.js → worker-wr6xgfr2.js} +33 -31
  116. package/openapi.json +1011 -115
  117. package/package.json +2 -2
  118. package/src/apps/action-caller.ts +6 -0
  119. package/src/apps/script-types.ts +16 -3
  120. package/src/be/asset-key-audit.ts +4 -13
  121. package/src/be/db/active-sessions.ts +5 -0
  122. package/src/be/db/session-visibility.ts +29 -0
  123. package/src/be/db/tasks/read.ts +61 -3
  124. package/src/be/db/tasks/write.ts +15 -6
  125. package/src/be/db.ts +149 -66
  126. package/src/be/events.ts +11 -0
  127. package/src/be/migrations/227_inbox_item_state_last_read_at.sql +1 -0
  128. package/src/be/migrations/230_session_shares.sql +23 -0
  129. package/src/be/rbac-roles.ts +2 -0
  130. package/src/be/seed-scripts/catalog/heartbeat-jev-gate.ts +22 -4
  131. package/src/be/session-shares.ts +244 -0
  132. package/src/be/swarm-config-guard.ts +23 -2
  133. package/src/commands/runner.ts +11 -0
  134. package/src/heartbeat/heartbeat.ts +25 -7
  135. package/src/http/active-sessions.ts +13 -2
  136. package/src/http/agents.ts +10 -3
  137. package/src/http/all-routes.ts +1 -0
  138. package/src/http/api-keys.ts +3 -1
  139. package/src/http/approval-requests.ts +2 -1
  140. package/src/http/apps.ts +36 -11
  141. package/src/http/assets.ts +26 -4
  142. package/src/http/config.ts +3 -0
  143. package/src/http/context.ts +2 -0
  144. package/src/http/core.ts +39 -2
  145. package/src/http/events.ts +6 -2
  146. package/src/http/favorite-owner.ts +1 -1
  147. package/src/http/fs.ts +38 -3
  148. package/src/http/inbox-state.ts +58 -8
  149. package/src/http/index.ts +3 -1
  150. package/src/http/memory.ts +3 -1
  151. package/src/http/ranking.ts +367 -0
  152. package/src/http/realtime.ts +26 -21
  153. package/src/http/route-def.ts +77 -11
  154. package/src/http/script-runs.ts +2 -1
  155. package/src/http/scripts.ts +8 -0
  156. package/src/http/session-access.ts +179 -0
  157. package/src/http/session-data.ts +10 -2
  158. package/src/http/sessions.ts +212 -12
  159. package/src/http/stats.ts +7 -3
  160. package/src/http/tasks.ts +83 -7
  161. package/src/http/used-by.ts +11 -3
  162. package/src/kv-reserved-namespaces.ts +1 -0
  163. package/src/prompts/session-templates.ts +3 -1
  164. package/src/providers/amp-adapter.ts +1 -1
  165. package/src/ranking/telemetry-ranking.ts +147 -0
  166. package/src/ranking/usage.ts +57 -0
  167. package/src/rbac/admission.ts +22 -7
  168. package/src/rbac/legacy-policy.ts +2 -0
  169. package/src/rbac/permissions.ts +9 -0
  170. package/src/realtime/auth.ts +69 -0
  171. package/src/realtime/tickets.ts +4 -1
  172. package/src/realtime/transport.ts +77 -9
  173. package/src/slack/blocks.ts +18 -0
  174. package/src/slack/dm.ts +29 -0
  175. package/src/telemetry-contract/EVENTS.md +3 -0
  176. package/src/telemetry-contract/SOURCE.json +37 -4
  177. package/src/telemetry-contract/catalog.json +69 -0
  178. package/src/telemetry-contract/ranking/fixtures/expected.json +30 -0
  179. package/src/telemetry-contract/ranking/fixtures/invalid/bad-category.json +11 -0
  180. package/src/telemetry-contract/ranking/fixtures/invalid/bad-claim.json +11 -0
  181. package/src/telemetry-contract/ranking/fixtures/invalid/blank-description.json +11 -0
  182. package/src/telemetry-contract/ranking/fixtures/invalid/blank-name.json +11 -0
  183. package/src/telemetry-contract/ranking/fixtures/invalid/description-too-long.json +11 -0
  184. package/src/telemetry-contract/ranking/fixtures/invalid/domain-uppercase.json +11 -0
  185. package/src/telemetry-contract/ranking/fixtures/invalid/domain-with-scheme.json +11 -0
  186. package/src/telemetry-contract/ranking/fixtures/invalid/domain-with-www.json +11 -0
  187. package/src/telemetry-contract/ranking/fixtures/invalid/http-source-url.json +11 -0
  188. package/src/telemetry-contract/ranking/fixtures/invalid/missing-description.json +10 -0
  189. package/src/telemetry-contract/ranking/fixtures/invalid/missing-version.json +10 -0
  190. package/src/telemetry-contract/ranking/fixtures/invalid/name-not-string.json +11 -0
  191. package/src/telemetry-contract/ranking/fixtures/invalid/name-too-long.json +11 -0
  192. package/src/telemetry-contract/ranking/fixtures/invalid/not-object.json +1 -0
  193. package/src/telemetry-contract/ranking/fixtures/invalid/null.json +1 -0
  194. package/src/telemetry-contract/ranking/fixtures/invalid/other-with-subcategory.json +11 -0
  195. package/src/telemetry-contract/ranking/fixtures/invalid/oversize.json +1 -0
  196. package/src/telemetry-contract/ranking/fixtures/invalid/short-claim.json +11 -0
  197. package/src/telemetry-contract/ranking/fixtures/invalid/subcategory-other-category.json +11 -0
  198. package/src/telemetry-contract/ranking/fixtures/invalid/trailing-data.json +1 -0
  199. package/src/telemetry-contract/ranking/fixtures/invalid/unknown-field.json +12 -0
  200. package/src/telemetry-contract/ranking/fixtures/invalid/unsupported-version.json +11 -0
  201. package/src/telemetry-contract/ranking/fixtures/invalid/updated-at-not-date.json +11 -0
  202. package/src/telemetry-contract/ranking/fixtures/invalid/updated-at-offset.json +11 -0
  203. package/src/telemetry-contract/ranking/fixtures/invalid/wrong-case-field.json +11 -0
  204. package/src/telemetry-contract/ranking/fixtures/valid/category-without-subcategory.json +11 -0
  205. package/src/telemetry-contract/ranking/fixtures/valid/full.json +11 -0
  206. package/src/telemetry-contract/ranking/fixtures/valid/minimal.json +11 -0
  207. package/src/telemetry-contract/ranking/fixtures/valid/padded-text.json +11 -0
  208. package/src/telemetry-contract/ranking/fixtures/valid/unicode-name.json +11 -0
  209. package/src/telemetry-contract/ranking/listing.v1.schema.json +61 -0
  210. package/src/telemetry-contract/ranking/taxonomy.json +8 -0
  211. package/src/telemetry-contract/ranking-taxonomy.gen.ts +11 -0
  212. package/src/telemetry-contract/types.gen.ts +708 -1
  213. package/src/telemetry.ts +26 -2
  214. package/src/tests/apps-rbac.test.ts +189 -2
  215. package/src/tests/apps-script-types.test.ts +16 -3
  216. package/src/tests/asset-key-api.test.ts +128 -0
  217. package/src/tests/asset-key-invariants.test.ts +179 -3
  218. package/src/tests/asset-key-migration.test.ts +35 -0
  219. package/src/tests/check-pr-body.test.ts +23 -0
  220. package/src/tests/check-ranking-entry.test.ts +146 -0
  221. package/src/tests/db-extraction-contract.test.ts +1 -0
  222. package/src/tests/db-task-read-contract.test.ts +1 -0
  223. package/src/tests/docs-index-footprint.test.js +2 -2
  224. package/src/tests/heartbeat-checklist.test.ts +88 -0
  225. package/src/tests/heartbeat-jev-gate.test.ts +394 -10
  226. package/src/tests/inbox-state.test.ts +167 -0
  227. package/src/tests/kv-tool.test.ts +15 -0
  228. package/src/tests/org-identity.test.ts +44 -0
  229. package/src/tests/ranking-listing.test.ts +112 -0
  230. package/src/tests/ranking-route.test.ts +319 -0
  231. package/src/tests/ranking-usage.test.ts +50 -0
  232. package/src/tests/rbac-admission-e2e.test.ts +8 -0
  233. package/src/tests/rbac-admission.test.ts +38 -0
  234. package/src/tests/rbac-charact-http.test.ts +4 -3
  235. package/src/tests/rbac-engine.test.ts +2 -0
  236. package/src/tests/rbac-roles.test.ts +2 -0
  237. package/src/tests/realtime-room-surfaces.test.ts +3 -1
  238. package/src/tests/realtime-session-join-race.test.ts +142 -0
  239. package/src/tests/realtime-tickets.test.ts +24 -17
  240. package/src/tests/realtime-transport.test.ts +91 -0
  241. package/src/tests/script-connections.test.ts +2 -0
  242. package/src/tests/session-route-posture.test.ts +639 -0
  243. package/src/tests/session-sharing-routes.test.ts +282 -0
  244. package/src/tests/session-sharing.test.ts +526 -0
  245. package/src/tests/sessions-detail-batch.test.ts +7 -1
  246. package/src/tests/slack-tool-blocks.test.ts +101 -0
  247. package/src/tests/slack-update.test.ts +55 -0
  248. package/src/tests/slack-user-dm.test.ts +177 -0
  249. package/src/tests/used-by-route.test.ts +10 -0
  250. package/src/tools/slack-post.ts +18 -12
  251. package/src/tools/slack-read.ts +13 -1
  252. package/src/tools/slack-reply.ts +7 -10
  253. package/src/tools/slack-start-thread.ts +18 -11
  254. package/src/tools/slack-update.ts +2 -9
  255. package/src/types.ts +37 -0
  256. package/src/utils/org-identity.ts +33 -0
  257. package/templates/skills/apps/SKILL.md +17 -0
  258. package/templates/skills/apps/content.md +17 -0
  259. package/templates/skills/code-quality/content.md +2 -2
  260. package/templates/skills/pages/content.md +4 -0
  261. package/templates/skills/swarm-scripts/SKILL.md +6 -0
  262. package/templates/skills/swarm-scripts/content.md +6 -0
  263. package/dist/handlers-t5pbabwf.js +0 -37
  264. package/dist/index-qyjbb7br.js +0 -39
  265. package/dist/{cli-n5mrsg0f.js → cli-60cgh1rv.js} +3 -3
  266. /package/dist/{cli-j0v75y13.js → cli-y83zk5sb.js} +0 -0
@@ -0,0 +1,244 @@
1
+ import { getDbClient } from "./db";
2
+
3
+ /**
4
+ * Session sharing store. A session is a root task (`parentTaskId IS NULL`) and
5
+ * its descendants; its owner is the root's `requestedByUserId`.
6
+ *
7
+ * Rows in `session_shares` are never deleted. Revoking sets `revokedAt`, and an
8
+ * access change revokes the active row and inserts a new one in the same
9
+ * transaction, so the rows for a root are its full share history.
10
+ */
11
+
12
+ export type SessionShareAccess = "read" | "write";
13
+ export type SessionAccess = "owner" | SessionShareAccess | "none";
14
+ export type SessionEveryoneAccess = SessionShareAccess | "none";
15
+
16
+ export type SessionShareRow = {
17
+ id: string;
18
+ rootTaskId: string;
19
+ /** NULL = everyone. */
20
+ userId: string | null;
21
+ access: SessionShareAccess;
22
+ createdAt: string;
23
+ revokedAt: string | null;
24
+ revokedBy: string | null;
25
+ };
26
+
27
+ export type SessionRoot = { id: string; requestedByUserId: string | null };
28
+
29
+ export type SessionSharesState = {
30
+ everyone: SessionEveryoneAccess;
31
+ users: { userId: string; access: SessionShareAccess; createdAt: string }[];
32
+ };
33
+
34
+ export type DesiredSessionShares = {
35
+ everyone: SessionEveryoneAccess;
36
+ users: { userId: string; access: SessionShareAccess }[];
37
+ };
38
+
39
+ const RANK: Record<SessionAccess, number> = { none: 0, read: 1, write: 2, owner: 3 };
40
+
41
+ /** True when `access` grants at least `needed`. */
42
+ export function hasSessionAccess(access: SessionAccess, needed: SessionAccess): boolean {
43
+ return RANK[access] >= RANK[needed];
44
+ }
45
+
46
+ /**
47
+ * The session root of any task: walk `parentTaskId` up to the topmost task
48
+ * that still exists. No depth cap, so a chain of any length resolves to its
49
+ * actual root; a corrupt parent cycle has no top and resolves to null.
50
+ */
51
+ export async function getSessionRoot(taskId: string): Promise<SessionRoot | null> {
52
+ return (await getSessionRoots([taskId])).get(taskId) ?? null;
53
+ }
54
+
55
+ const ROOT_LOOKUP_CHUNK = 500;
56
+
57
+ type AncestorRow = { id: string; parentTaskId: string | null; requestedByUserId: string | null };
58
+
59
+ /** `getSessionRoot` for many tasks; unknown tasks and tasks in a cycle are absent. */
60
+ export async function getSessionRoots(taskIds: string[]): Promise<Map<string, SessionRoot>> {
61
+ const ids = [...new Set(taskIds)];
62
+ // Every ancestor once (`UNION` drops revisits, which also ends a cycle),
63
+ // then resolve each chain in memory: linear in the tasks involved.
64
+ const tasks = new Map<string, AncestorRow>();
65
+ for (let start = 0; start < ids.length; start += ROOT_LOOKUP_CHUNK) {
66
+ const chunk = ids.slice(start, start + ROOT_LOOKUP_CHUNK).filter((id) => !tasks.has(id));
67
+ if (chunk.length === 0) continue;
68
+ const rows = await getDbClient().query<AncestorRow>(
69
+ `WITH RECURSIVE up(id) AS (
70
+ SELECT id FROM agent_tasks WHERE id IN (${chunk.map(() => "?").join(", ")})
71
+ UNION
72
+ SELECT t.parentTaskId FROM agent_tasks t JOIN up ON t.id = up.id
73
+ WHERE t.parentTaskId IS NOT NULL
74
+ )
75
+ SELECT t.id, t.parentTaskId, t.requestedByUserId
76
+ FROM up CROSS JOIN agent_tasks t ON t.id = up.id`,
77
+ chunk,
78
+ );
79
+ for (const row of rows) tasks.set(row.id, row);
80
+ }
81
+
82
+ const resolved = new Map<string, SessionRoot | null>();
83
+ const rootOf = (taskId: string): SessionRoot | null => {
84
+ const path: string[] = [];
85
+ const onPath = new Set<string>();
86
+ let current = tasks.get(taskId);
87
+ let root: SessionRoot | null = null;
88
+ while (current) {
89
+ const known = resolved.get(current.id);
90
+ if (known !== undefined) {
91
+ root = known;
92
+ break;
93
+ }
94
+ if (onPath.has(current.id)) break;
95
+ path.push(current.id);
96
+ onPath.add(current.id);
97
+ const parent = current.parentTaskId ? tasks.get(current.parentTaskId) : undefined;
98
+ if (!parent) {
99
+ root = { id: current.id, requestedByUserId: current.requestedByUserId };
100
+ break;
101
+ }
102
+ current = parent;
103
+ }
104
+ for (const id of path) resolved.set(id, root);
105
+ return root;
106
+ };
107
+
108
+ const roots = new Map<string, SessionRoot>();
109
+ for (const id of ids) {
110
+ const root = rootOf(id);
111
+ if (root) roots.set(id, root);
112
+ }
113
+ return roots;
114
+ }
115
+
116
+ /**
117
+ * What `actingUserId` may do in the session rooted at `root`.
118
+ *
119
+ * - `owner` for the root's requester, and for every user when the root has no
120
+ * owner (Slack and schedule sessions stay open, as before sharing existed).
121
+ * - Otherwise the best active share naming the user or everyone; `none` when
122
+ * there is none. A root with no rows is private to its owner.
123
+ *
124
+ * A null `actingUserId` is a viewer with no user (a guest page session): it
125
+ * gets ownerless roots and everyone shares only. Callers that act for no user
126
+ * (agents, operator-key calls with no picked dashboard user) skip this check
127
+ * entirely; see `resolveSessionActor`.
128
+ */
129
+ export async function sessionAccessForRoot(
130
+ actingUserId: string | null,
131
+ root: SessionRoot,
132
+ ): Promise<SessionAccess> {
133
+ if (!root.requestedByUserId) return "owner";
134
+ if (actingUserId && root.requestedByUserId === actingUserId) return "owner";
135
+ const rows = await getDbClient().query<{ access: SessionShareAccess }>(
136
+ `SELECT access FROM session_shares
137
+ WHERE rootTaskId = ? AND revokedAt IS NULL AND (userId IS NULL OR userId = ?)`,
138
+ [root.id, actingUserId],
139
+ );
140
+ if (rows.some((row) => row.access === "write")) return "write";
141
+ return rows.length > 0 ? "read" : "none";
142
+ }
143
+
144
+ /** `sessionAccessForRoot` for any task in the session; `none` for an unknown task. */
145
+ export async function sessionAccess(
146
+ actingUserId: string | null,
147
+ taskId: string,
148
+ ): Promise<SessionAccess> {
149
+ const root = await getSessionRoot(taskId);
150
+ return root ? sessionAccessForRoot(actingUserId, root) : "none";
151
+ }
152
+
153
+ export async function listActiveSessionShares(rootTaskId: string): Promise<SessionShareRow[]> {
154
+ return getDbClient().query<SessionShareRow>(
155
+ `SELECT id, rootTaskId, userId, access, createdAt, revokedAt, revokedBy
156
+ FROM session_shares
157
+ WHERE rootTaskId = ? AND revokedAt IS NULL
158
+ ORDER BY createdAt, rowid`,
159
+ [rootTaskId],
160
+ );
161
+ }
162
+
163
+ /** Every row for a root, revoked ones included, oldest first. */
164
+ export async function listSessionShareHistory(rootTaskId: string): Promise<SessionShareRow[]> {
165
+ return getDbClient().query<SessionShareRow>(
166
+ `SELECT id, rootTaskId, userId, access, createdAt, revokedAt, revokedBy
167
+ FROM session_shares
168
+ WHERE rootTaskId = ?
169
+ ORDER BY createdAt, rowid`,
170
+ [rootTaskId],
171
+ );
172
+ }
173
+
174
+ export function toSessionSharesState(rows: SessionShareRow[]): SessionSharesState {
175
+ const everyone = rows.find((row) => row.userId === null);
176
+ return {
177
+ everyone: everyone?.access ?? "none",
178
+ users: rows
179
+ .filter((row) => row.userId !== null)
180
+ .map((row) => ({ userId: row.userId!, access: row.access, createdAt: row.createdAt })),
181
+ };
182
+ }
183
+
184
+ /**
185
+ * Make the active shares of `root` equal `desired`, in one write transaction.
186
+ * A target whose access changes has its active row revoked and a new row
187
+ * inserted; a target no longer listed is revoked; nothing is deleted or
188
+ * updated in place apart from revocation. The owner is never stored.
189
+ */
190
+ export async function setSessionShares(
191
+ root: SessionRoot,
192
+ desired: DesiredSessionShares,
193
+ actorUserId: string | null,
194
+ ): Promise<SessionSharesState> {
195
+ const want = new Map<string, SessionShareAccess>();
196
+ if (desired.everyone !== "none") want.set("*", desired.everyone);
197
+ for (const user of desired.users) {
198
+ if (user.userId !== root.requestedByUserId) want.set(user.userId, user.access);
199
+ }
200
+
201
+ return getDbClient().transaction(async (tx) => {
202
+ const now = new Date().toISOString();
203
+ const active = await tx.query<SessionShareRow>(
204
+ `SELECT id, rootTaskId, userId, access, createdAt, revokedAt, revokedBy
205
+ FROM session_shares WHERE rootTaskId = ? AND revokedAt IS NULL`,
206
+ [root.id],
207
+ );
208
+ const kept = new Set<string>();
209
+ for (const row of active) {
210
+ const target = row.userId ?? "*";
211
+ if (want.get(target) === row.access) {
212
+ kept.add(target);
213
+ continue;
214
+ }
215
+ await tx.run(
216
+ "UPDATE session_shares SET revokedAt = ?, revokedBy = ?, updated_by = ? WHERE id = ?",
217
+ [now, actorUserId, actorUserId, row.id],
218
+ );
219
+ }
220
+ for (const [target, access] of want) {
221
+ if (kept.has(target)) continue;
222
+ await tx.run(
223
+ `INSERT INTO session_shares (id, rootTaskId, userId, access, createdAt, created_by, updated_by)
224
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
225
+ [
226
+ crypto.randomUUID(),
227
+ root.id,
228
+ target === "*" ? null : target,
229
+ access,
230
+ now,
231
+ actorUserId,
232
+ actorUserId,
233
+ ],
234
+ );
235
+ }
236
+ const rows = await tx.query<SessionShareRow>(
237
+ `SELECT id, rootTaskId, userId, access, createdAt, revokedAt, revokedBy
238
+ FROM session_shares WHERE rootTaskId = ? AND revokedAt IS NULL
239
+ ORDER BY createdAt, rowid`,
240
+ [root.id],
241
+ );
242
+ return toSessionSharesState(rows);
243
+ });
244
+ }
@@ -1,4 +1,5 @@
1
1
  import { validateChatSdkAdaptersValue } from "../integrations/chat-sdk/schema";
2
+ import { parseTelemetryRanking, TELEMETRY_RANKING_KEY } from "../ranking/telemetry-ranking";
2
3
  import { validateQuickJSPoolSize } from "../scripts-runtime/executors/quickjs-pool-config";
3
4
  import { normalizeSlackReactionShortcode } from "../slack/reaction-shortcode";
4
5
  import { ProviderNameSchema } from "../types";
@@ -69,15 +70,24 @@ export function isApiOnlyConfigKey(key: string): boolean {
69
70
  }
70
71
 
71
72
  /** Config rows owned by dedicated API surfaces, not the generic config API. */
72
- export const INTERNAL_CONFIG_KEYS = new Set(["onboarding_state", "runtime_owner_id"]);
73
+ export const INTERNAL_CONFIG_KEYS = new Set([
74
+ "onboarding_state",
75
+ "runtime_owner_id",
76
+ TELEMETRY_RANKING_KEY,
77
+ ]);
73
78
 
74
79
  export function isInternalConfigKey(key: string): boolean {
75
80
  return INTERNAL_CONFIG_KEYS.has(key.toLowerCase());
76
81
  }
77
82
 
78
83
  export function internalConfigKeyError(key: string): Error {
84
+ const lower = key.toLowerCase();
79
85
  const owner =
80
- key.toLowerCase() === "runtime_owner_id" ? "the runtime supervisor" : "/api/onboarding";
86
+ lower === "runtime_owner_id"
87
+ ? "the runtime supervisor"
88
+ : lower === TELEMETRY_RANKING_KEY
89
+ ? "/api/ranking"
90
+ : "/api/onboarding";
81
91
  return new Error(`Key '${key}' is managed by ${owner}`);
82
92
  }
83
93
 
@@ -301,6 +311,16 @@ const VALIDATED_KEYS: Record<string, ConfigValidator> = {
301
311
  return "Invalid TASK_TOOL_MANIFESTS (expected JSON with taskTypes and/or schedules maps, each selecting at most 16 known swarm tools)";
302
312
  }
303
313
  },
314
+ // Owned by /api/ranking (an internal key); parsed here as defense in depth.
315
+ [TELEMETRY_RANKING_KEY.toUpperCase()]: (value) => {
316
+ try {
317
+ if (typeof value !== "string") throw new Error("Expected JSON string");
318
+ parseTelemetryRanking(value);
319
+ return null;
320
+ } catch {
321
+ return "Invalid telemetry_ranking (expected the v1 ranking listing JSON)";
322
+ }
323
+ },
304
324
  // MCP tool allowlist: comma-separated names or a JSON array of strings.
305
325
  // Unknown names are accepted here and warned about at session init.
306
326
  SWARM_ENABLED_TOOLS: (value) => {
@@ -405,6 +425,7 @@ const VALIDATED_KEYS: Record<string, ConfigValidator> = {
405
425
  },
406
426
  ...booleanValidators([
407
427
  "SWARM_USED_BY_CONSENT",
428
+ "PUBLIC_RANKING_ENABLED",
408
429
  "SWARM_DEV_MODE",
409
430
  "MULTI_RUNTIME_ENABLED",
410
431
  "STEERING_ENABLED",
@@ -55,6 +55,7 @@ import {
55
55
  skillFromToolArgs,
56
56
  } from "../providers/skill-invoke.ts";
57
57
  import type { SteerDeliveryResult } from "../providers/types.ts";
58
+ import { rankingUsageProps } from "../ranking/usage.ts";
58
59
  import { WorkerRealtimeClient } from "../realtime/worker-client";
59
60
  import { resumedTaskModelSelection, type TaskModelPinFields } from "../tasks/model-pin.ts";
60
61
  import { initTelemetry, telemetry } from "../telemetry.ts";
@@ -1228,6 +1229,10 @@ export const RELOADABLE_ENV_KEYS: ReadonlySet<string> = new Set([
1228
1229
  "TEMPLATE_REGISTRY_URL",
1229
1230
  "SLACK_DISABLE",
1230
1231
  "SWARM_ORG_NAME",
1232
+ // Gate the public-ranking `ranking.usage` emit, so clearing the website or
1233
+ // turning the setting off stops it on running workers.
1234
+ "SWARM_ORG_WEBSITE",
1235
+ "PUBLIC_RANKING_ENABLED",
1231
1236
  // pi reads these from process.env for its traits and its session.
1232
1237
  "PI_TOOL_DEFERRAL",
1233
1238
  "PI_CODEMODE",
@@ -4923,6 +4928,12 @@ async function spawnProviderProcess(
4923
4928
  },
4924
4929
  { userId: opts.requestedByUserId ?? null },
4925
4930
  );
4931
+ // Public ranking: token counts only, while the install is opted in.
4932
+ const rankingUsage = rankingUsageProps(result.cost, {
4933
+ model: result.cost.model,
4934
+ provider: result.cost.provider ?? opts.harnessProvider,
4935
+ });
4936
+ if (rankingUsage) telemetry.ranking("usage", rankingUsage);
4926
4937
  }
4927
4938
 
4928
4939
  // Post completion context usage snapshot
@@ -48,6 +48,7 @@ import {
48
48
  settleSupersededTaskDependents,
49
49
  supersedeTask,
50
50
  updateAgentStatus,
51
+ upsertKv,
51
52
  } from "../be/db";
52
53
  import { repointTrackerSyncBySwarmId } from "../be/db-queries/tracker";
53
54
  import { poolTaskRunsOnHarness } from "../be/model-validation";
@@ -1461,7 +1462,10 @@ export function isEffectivelyEmpty(content: string): boolean {
1461
1462
  /**
1462
1463
  * Gather current system status as a markdown string for the lead's checklist task.
1463
1464
  */
1464
- export async function gatherSystemStatus(options?: { isBootTriage?: boolean }): Promise<string> {
1465
+ export async function gatherSystemStatus(options?: {
1466
+ isBootTriage?: boolean;
1467
+ reportedFailures?: Array<{ id: string; line: string }>;
1468
+ }): Promise<string> {
1465
1469
  const stats = await getTaskStats();
1466
1470
  const stalledTasks = await getStalledInProgressTasks(stallThresholdMinutes());
1467
1471
  const agents = await getAllAgents();
@@ -1494,6 +1498,7 @@ export async function gatherSystemStatus(options?: { isBootTriage?: boolean }):
1494
1498
 
1495
1499
  // Recent failures with reasons and pattern detection (last 6 hours)
1496
1500
  const recentFailures = await getRecentFailedTasks(6);
1501
+ const renderedFailures = recentFailures.slice(0, 10);
1497
1502
  if (recentFailures.length > 0) {
1498
1503
  sections.push("");
1499
1504
  sections.push("## Recent Failures (last 6h) [auto-generated]");
@@ -1520,12 +1525,12 @@ export async function gatherSystemStatus(options?: { isBootTriage?: boolean }):
1520
1525
 
1521
1526
  // List individual failures (max 10)
1522
1527
  sections.push("");
1523
- for (const task of recentFailures.slice(0, 10)) {
1528
+ for (const task of renderedFailures) {
1524
1529
  const agentSlice = task.agentId?.slice(0, 8) ?? "unassigned";
1525
1530
  const reason = task.failureReason?.slice(0, 100) ?? "no reason";
1526
- sections.push(
1527
- `- [${task.id.slice(0, 8)}] "${task.task.slice(0, 50)}" — agent: ${agentSlice}, reason: ${reason}, at: ${task.finishedAt}`,
1528
- );
1531
+ const line = `- [${task.id.slice(0, 8)}] "${task.task.slice(0, 50)}" — agent: ${agentSlice}, reason: ${reason}, at: ${task.finishedAt}`;
1532
+ sections.push(line);
1533
+ options?.reportedFailures?.push({ id: task.id, line });
1529
1534
  }
1530
1535
  if (recentFailures.length > 10) {
1531
1536
  sections.push(`- ... and ${recentFailures.length - 10} more`);
@@ -1786,7 +1791,8 @@ export async function checkHeartbeatChecklist(): Promise<void> {
1786
1791
  return;
1787
1792
  }
1788
1793
 
1789
- let systemStatus = await gatherSystemStatus();
1794
+ const reportedFailures: Array<{ id: string; line: string }> = [];
1795
+ let systemStatus = await gatherSystemStatus({ reportedFailures });
1790
1796
  // Shadow mode leaves the prompt untouched so the Lead's own quiet/non-quiet
1791
1797
  // outcome stays an independent check on the gate's verdict.
1792
1798
  if (gateMode === "enforce" && gate?.verdict === "wake" && gate.summary) {
@@ -1803,7 +1809,7 @@ export async function checkHeartbeatChecklist(): Promise<void> {
1803
1809
 
1804
1810
  if (result.skipped) return;
1805
1811
 
1806
- await createTaskExtended(result.text, {
1812
+ const task = await createTaskExtended(result.text, {
1807
1813
  agentId: lead.id,
1808
1814
  routingReason: "skill",
1809
1815
  routingSource: "engine_default",
@@ -1815,6 +1821,18 @@ export async function checkHeartbeatChecklist(): Promise<void> {
1815
1821
  priority: 60,
1816
1822
  });
1817
1823
 
1824
+ try {
1825
+ await upsertKv({
1826
+ namespace: "heartbeat-checklist:reported-failures",
1827
+ key: task.id,
1828
+ value: reportedFailures.filter(({ line }) => result.text.includes(line)).map(({ id }) => id),
1829
+ valueType: "json",
1830
+ expiresAt: Date.now() + 7 * 24 * 3600e3,
1831
+ });
1832
+ } catch (err) {
1833
+ console.error(`[Heartbeat] Failed to save reported failures for checklist ${task.id}:`, err);
1834
+ }
1835
+
1818
1836
  console.log(`[Heartbeat] Checklist task created for lead ${lead.name}`);
1819
1837
  }
1820
1838
 
@@ -13,7 +13,8 @@ import {
13
13
  } from "../be/db";
14
14
  import { ActiveSessionSchema, AgentTaskSchema } from "../types";
15
15
  import { isMultiRuntimeEnabled } from "../utils/multi-runtime";
16
- import { route } from "./route-def";
16
+ import { bodyTask, route } from "./route-def";
17
+ import { readableByFor, resolveSessionActor } from "./session-access";
17
18
  import { jsonError } from "./utils";
18
19
 
19
20
  // ─── Route Definitions ───────────────────────────────────────────────────────
@@ -39,6 +40,7 @@ const createActiveSession = route({
39
40
  method: "post",
40
41
  path: "/api/active-sessions",
41
42
  pattern: ["api", "active-sessions"],
43
+ session: bodyTask("taskId"),
42
44
  summary: "Create a new active session",
43
45
  tags: ["Active Sessions"],
44
46
  body: z.object({
@@ -60,6 +62,7 @@ const deleteSessionByTask = route({
60
62
  method: "delete",
61
63
  path: "/api/active-sessions/by-task/{taskId}",
62
64
  pattern: ["api", "active-sessions", "by-task", null],
65
+ session: { taskParam: "taskId" },
63
66
  summary: "Delete active session by task ID",
64
67
  tags: ["Active Sessions"],
65
68
  params: z.object({ taskId: z.string() }),
@@ -72,6 +75,7 @@ const deleteSessionById = route({
72
75
  method: "delete",
73
76
  path: "/api/active-sessions/{id}",
74
77
  pattern: ["api", "active-sessions", null],
78
+ session: { activeSessionParam: "id" },
75
79
  summary: "Delete active session by ID",
76
80
  tags: ["Active Sessions"],
77
81
  params: z.object({ id: z.string() }),
@@ -84,6 +88,7 @@ const heartbeatSession = route({
84
88
  method: "put",
85
89
  path: "/api/active-sessions/heartbeat/{taskId}",
86
90
  pattern: ["api", "active-sessions", "heartbeat", null],
91
+ session: { taskParam: "taskId" },
87
92
  summary: "Update heartbeat for an active session",
88
93
  tags: ["Active Sessions"],
89
94
  params: z.object({ taskId: z.string() }),
@@ -96,6 +101,7 @@ const updateProviderSession = route({
96
101
  method: "put",
97
102
  path: "/api/active-sessions/provider-session/{taskId}",
98
103
  pattern: ["api", "active-sessions", "provider-session", null],
104
+ session: { taskParam: "taskId" },
99
105
  summary: "Update provider session ID on an active session",
100
106
  tags: ["Active Sessions"],
101
107
  params: z.object({ taskId: z.string() }),
@@ -109,6 +115,7 @@ const cleanupSessions = route({
109
115
  method: "post",
110
116
  path: "/api/active-sessions/cleanup",
111
117
  pattern: ["api", "active-sessions", "cleanup"],
118
+ session: { agentWide: "Deletes every session of an agent, or every stale session." },
112
119
  summary: "Clean up stale sessions",
113
120
  tags: ["Active Sessions"],
114
121
  body: z
@@ -126,6 +133,7 @@ const recoverOrphanedTasks = route({
126
133
  method: "post",
127
134
  path: "/api/active-sessions/recover-orphaned-tasks",
128
135
  pattern: ["api", "active-sessions", "recover-orphaned-tasks"],
136
+ session: { none: "Its handler admits only the calling agent, never a user." },
129
137
  summary: "Recover orphaned in-progress tasks for an agent",
130
138
  tags: ["Active Sessions"],
131
139
  body: z.object({
@@ -153,7 +161,10 @@ export async function handleActiveSessions(
153
161
  if (listActiveSessions.match(req.method, pathSegments)) {
154
162
  const parsed = await listActiveSessions.parse(req, res, pathSegments, queryParams);
155
163
  if (!parsed) return true;
156
- const sessions = await getActiveSessions(parsed.query.agentId || undefined);
164
+ const sessions = await getActiveSessions(
165
+ parsed.query.agentId || undefined,
166
+ readableByFor(await resolveSessionActor(req, myAgentId)),
167
+ );
157
168
  listActiveSessions.respond(res, 200, { sessions });
158
169
  return true;
159
170
  }
@@ -90,6 +90,7 @@ import { getRequestAuth } from "../utils/request-auth-context";
90
90
  import { isRuntimePoolsEnabled } from "../utils/runtime-pools-flag";
91
91
  import { scrubSecrets } from "../utils/secret-scrubber";
92
92
  import { route, runtimeInstanceHeader } from "./route-def";
93
+ import { readableByFor, resolveSessionActor } from "./session-access";
93
94
  import { agentWithCapacity, json, jsonError } from "./utils";
94
95
 
95
96
  function singleHeaderValue(req: IncomingMessage, name: string): string | undefined {
@@ -844,7 +845,7 @@ export async function handleAgentsRest(
844
845
  res: ServerResponse,
845
846
  pathSegments: string[],
846
847
  queryParams: URLSearchParams,
847
- _myAgentId: string | undefined,
848
+ myAgentId: string | undefined,
848
849
  ): Promise<boolean> {
849
850
  if (listAgents.match(req.method, pathSegments)) {
850
851
  const parsed = await listAgents.parse(req, res, pathSegments, queryParams);
@@ -853,7 +854,10 @@ export async function handleAgentsRest(
853
854
  // List responses default to slim (no identity markdown); `?fields=full` restores it.
854
855
  const slim = parsed.query.fields !== "full";
855
856
  const agents = includeTasks
856
- ? await getAllAgentsWithTasks({ slim })
857
+ ? await getAllAgentsWithTasks({
858
+ slim,
859
+ readableBy: readableByFor(await resolveSessionActor(req, myAgentId)),
860
+ })
857
861
  : await getAllAgents({ slim });
858
862
  const [agentsWithCapacity, transports] = await Promise.all([
859
863
  Promise.all(agents.map(agentWithCapacity)),
@@ -1537,7 +1541,10 @@ export async function handleAgentsRest(
1537
1541
  if (!parsed) return true;
1538
1542
  const includeTasks = parsed.query.include === "tasks";
1539
1543
  const agent = includeTasks
1540
- ? await getAgentWithTasks(parsed.params.id)
1544
+ ? await getAgentWithTasks(
1545
+ parsed.params.id,
1546
+ readableByFor(await resolveSessionActor(req, myAgentId)),
1547
+ )
1541
1548
  : await getAgentById(parsed.params.id);
1542
1549
 
1543
1550
  if (!agent) {
@@ -67,6 +67,7 @@ import "./task-templates";
67
67
  import "./telemetry-ui";
68
68
  import "./trackers/jira";
69
69
  import "./trackers/linear";
70
+ import "./ranking";
70
71
  import "./used-by";
71
72
  import "./fleet-benchmark";
72
73
  import "./users";
@@ -20,7 +20,7 @@ import {
20
20
  SUBSCRIPTION_PLANS,
21
21
  SUBSCRIPTION_PLANS_CHECKED_AT,
22
22
  } from "../utils/subscription-plans";
23
- import { route } from "./route-def";
23
+ import { bodyTask, route } from "./route-def";
24
24
  import { clearUsageCache } from "./usage-cache";
25
25
  import { jsonError } from "./utils";
26
26
 
@@ -36,6 +36,7 @@ const reportUsage = route({
36
36
  method: "post",
37
37
  path: "/api/keys/report-usage",
38
38
  pattern: ["api", "keys", "report-usage"],
39
+ session: bodyTask("taskId"),
39
40
  summary: "Record which API key was used for a task",
40
41
  tags: ["API Keys"],
41
42
  body: z.object({
@@ -82,6 +83,7 @@ const reportAuthFailure = route({
82
83
  method: "post",
83
84
  path: "/api/keys/report-auth-failure",
84
85
  pattern: ["api", "keys", "report-auth-failure"],
86
+ session: bodyTask("taskId"),
85
87
  summary: "Record an auth failure for a pooled key; bench it after 2 in a row",
86
88
  tags: ["API Keys"],
87
89
  body: z.object({
@@ -23,7 +23,7 @@ import { getRequestAuth } from "../utils/request-auth-context";
23
23
  import { cancelApprovalRequest } from "../workflows/approval-cancel";
24
24
  import { createApprovalFollowUpTask } from "../workflows/approval-notifications";
25
25
  import { workflowEventBus } from "../workflows/event-bus";
26
- import { route } from "./route-def";
26
+ import { bodyTask, route } from "./route-def";
27
27
  import { jsonError } from "./utils";
28
28
 
29
29
  // ─── Route Definitions ───────────────────────────────────────────────────────
@@ -255,6 +255,7 @@ const createRoute = route({
255
255
  method: "post",
256
256
  path: "/api/approval-requests",
257
257
  pattern: ["api", "approval-requests"],
258
+ session: bodyTask("sourceTaskId"),
258
259
  summary: "Create a new approval request",
259
260
  tags: ["ApprovalRequests"],
260
261
  body: z.object({