@desplega.ai/agent-swarm 2.3.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 (247) hide show
  1. package/README.md +1 -0
  2. package/dist/{acp-adapter-v85a2bqy.js → acp-adapter-kedstxx6.js} +6 -6
  3. package/dist/{actions-zgcxfspj.js → actions-vget6r1k.js} +18 -16
  4. package/dist/{amp-adapter-jw9xfz42.js → amp-adapter-xeb5mktv.js} +2 -2
  5. package/dist/{app-4e1s4675.js → app-mqap8t8m.js} +3 -3
  6. package/dist/{assistant-v238n9dk.js → assistant-h0gv97mv.js} +20 -18
  7. package/dist/{boot-reembed-p666cw5n.js → boot-reembed-qr8a894z.js} +1 -1
  8. package/dist/{boot-reembed-w6pm64j4.js → boot-reembed-rj85jjhw.js} +1 -1
  9. package/dist/{boot-scrub-sweep-jcyjagn3.js → boot-scrub-sweep-eyfvn6v9.js} +3 -3
  10. package/dist/{boot-seeds-gbedmamp.js → boot-seeds-xs9a8dx1.js} +4 -4
  11. package/dist/{claude-adapter-q7wspzt8.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-wza784fb.js → claude-managed-setup-gkfgks4j.js} +1 -1
  14. package/dist/{claude-sdk-session-yf400ptr.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-30m5q8mc.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-fhvqt2fp.js → cli-2rwet3r7.js} +1 -1
  22. package/dist/{cli-azs9bhc5.js → cli-3wjs22x3.js} +2 -2
  23. package/dist/{cli-dxwh4j32.js → cli-4fhyq711.js} +45 -36
  24. package/dist/{cli-yv3n83x8.js → cli-5ff2sjyt.js} +1 -1
  25. package/dist/{cli-6vcyw6v7.js → cli-5s5hx57y.js} +5 -3
  26. package/dist/{cli-hz64bcnj.js → cli-6ftrrwm1.js} +9 -371
  27. package/dist/{cli-zes4tsbb.js → cli-8b9wvjps.js} +1 -1
  28. package/dist/cli-8cxkw1f5.js +382 -0
  29. package/dist/{cli-4s1kqrqg.js → cli-8rwvksbc.js} +2 -2
  30. package/dist/{cli-e09hj50q.js → cli-8wkb09q5.js} +11 -11
  31. package/dist/{cli-yv0vgscc.js → cli-a1esmmee.js} +13 -2
  32. package/dist/{cli-4hmaheb6.js → cli-avmqhatn.js} +7 -7
  33. package/dist/{cli-g6p7c2my.js → cli-b80a1100.js} +1 -1
  34. package/dist/{cli-tr24eeny.js → cli-bfc3k1x5.js} +3 -3
  35. package/dist/{cli-ka01m5rb.js → cli-bhwa8rzc.js} +13 -11
  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-a17tzmn0.js → cli-ecshf504.js} +2 -2
  41. package/dist/{cli-d43xqzbe.js → cli-eqgfs202.js} +1 -1
  42. package/dist/{cli-ppapy34w.js → cli-f3nx1h8z.js} +2 -2
  43. package/dist/{cli-ea7chd26.js → cli-ffbrkh1r.js} +74 -35
  44. package/dist/{cli-xgn4rwyv.js → cli-kk2r80n5.js} +4 -4
  45. package/dist/{cli-hn3pftj2.js → cli-kz116kvw.js} +3 -3
  46. package/dist/{cli-1v8w782f.js → cli-mvg2hf8p.js} +4 -4
  47. package/dist/{cli-5d1m1qca.js → cli-mz1ezzzh.js} +1 -1
  48. package/dist/{cli-jhmjwqcg.js → cli-nme7wery.js} +6 -6
  49. package/dist/{cli-65q70v2v.js → cli-nrrg0fvy.js} +18 -4
  50. package/dist/{cli-8jtrekpy.js → cli-p1m2b6gc.js} +7 -7
  51. package/dist/{cli-re4wkbzx.js → cli-rg7ntypm.js} +328 -47
  52. package/dist/{cli-fp4xbw07.js → cli-rsd7ga7v.js} +10 -8
  53. package/dist/{cli-etvn5ej2.js → cli-sppc0cmw.js} +1 -1
  54. package/dist/{cli-qrwp40st.js → cli-tg9ksbn8.js} +1 -1
  55. package/dist/{cli-6p7ddg72.js → cli-tgvpmnwz.js} +2 -2
  56. package/dist/{cli-q1812jbw.js → cli-tj4j2ts4.js} +6 -6
  57. package/dist/{cli-kqypty34.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-y3dvz6ge.js → cli-x1p5dhq2.js} +3 -3
  62. package/dist/{cli-8qsx817b.js → cli-x719j3vs.js} +219 -629
  63. package/dist/{cli-9e9kjqsj.js → cli-xfe63zx3.js} +1 -1
  64. package/dist/{cli-wtg6edvd.js → cli-ywyrffff.js} +1 -1
  65. package/dist/{cli-0rh20bp3.js → cli-zvze9rbm.js} +1 -1
  66. package/dist/cli.js +14 -14
  67. package/dist/{codex-adapter-nhjj2veb.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-6hggrjbx.js → codex-session-runner-0vzn09t8.js} +3 -3
  70. package/dist/{commands-mrw1ph2w.js → commands-psqhkttb.js} +2 -2
  71. package/dist/{controller-pt1zam5w.js → controller-t20vz2k8.js} +2 -2
  72. package/dist/{controllers-connect-ymw5541d.js → controllers-connect-gkxmqw9d.js} +24 -20
  73. package/dist/{cursor-adapter-cf4zh5x0.js → cursor-adapter-3v542qe5.js} +1 -1
  74. package/dist/{db-hy80219q.js → db-2xbyenjr.js} +2 -2
  75. package/dist/{db-hhpy6m4x.js → db-nc1rd869.js} +3 -2
  76. package/dist/{dispatcher-6pdjtkr1.js → dispatcher-ewceb8ts.js} +17 -15
  77. package/dist/{dsh-adapter-fbx06re2.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-j6vb6ya1.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-1193sprp.js → grok-adapter-vj88xr23.js} +7 -7
  84. package/dist/handlers-zbs90bny.js +39 -0
  85. package/dist/{heartbeat-h1w1ktce.js → heartbeat-t2m9r3wa.js} +20 -18
  86. package/dist/{hook-skrbja18.js → hook-8femx1bg.js} +4 -4
  87. package/dist/{hook-ejja4bpe.js → hook-xa677f8r.js} +5 -5
  88. package/dist/{http-mzh41mfj.js → http-xpmmq028.js} +868 -253
  89. package/dist/{index-vjn3wbzf.js → index-8pwc1jc8.js} +17 -15
  90. package/dist/{index-epr2gnbw.js → index-cdzyfrsp.js} +1 -1
  91. package/dist/{index-xmmkyx82.js → index-dg2tmj99.js} +17 -15
  92. package/dist/index-nxa6me0q.js +41 -0
  93. package/dist/{index-6rmnn89d.js → index-s7ckdajq.js} +9 -9
  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-4djvbm2j.js → keepalive-3nr4st3r.js} +3 -3
  98. package/dist/{lead-61kqp604.js → lead-td8h2sfn.js} +33 -31
  99. package/dist/{maintenance-f3xhnjsm.js → maintenance-verwpe2c.js} +1 -1
  100. package/dist/{oauth-refresh-sweep-fpd6x6rw.js → oauth-refresh-sweep-9q2t0y3q.js} +2 -2
  101. package/dist/{onboard-196tjgyn.js → onboard-aytqedmq.js} +2 -2
  102. package/dist/{opencode-adapter-eeesgfqk.js → opencode-adapter-f4hryjny.js} +4 -4
  103. package/dist/{otel-impl-g8fh4c21.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-sdcf5yaz.js → pricing-refresh-5nvme3sm.js} +3 -3
  106. package/dist/{rbac-roles-bsp5657v.js → rbac-roles-pd8c26m1.js} +5 -3
  107. package/dist/{rbac-roles-2x4g1wb5.js → rbac-roles-vbpy21mg.js} +2 -2
  108. package/dist/{render-v2-0pyb39bb.js → render-v2-7nqgvk2q.js} +3 -3
  109. package/dist/{resume-a86tkj6r.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-ypbzcapd.js → setup-n39szm6b.js} +1 -1
  113. package/dist/{virtual-modules-wzk6jf3a.js → virtual-modules-wyp2k6z3.js} +2 -2
  114. package/dist/{work-objects-yjp8de12.js → work-objects-jem2fre0.js} +20 -18
  115. package/dist/{worker-4ptnn1ge.js → worker-wr6xgfr2.js} +33 -31
  116. package/openapi.json +844 -110
  117. package/package.json +1 -1
  118. package/src/apps/action-caller.ts +6 -0
  119. package/src/apps/script-types.ts +16 -3
  120. package/src/be/db/active-sessions.ts +5 -0
  121. package/src/be/db/session-visibility.ts +29 -0
  122. package/src/be/db/tasks/read.ts +61 -3
  123. package/src/be/db/tasks/write.ts +15 -6
  124. package/src/be/db.ts +92 -33
  125. package/src/be/events.ts +11 -0
  126. package/src/be/migrations/230_session_shares.sql +23 -0
  127. package/src/be/rbac-roles.ts +2 -0
  128. package/src/be/session-shares.ts +244 -0
  129. package/src/be/swarm-config-guard.ts +23 -2
  130. package/src/commands/runner.ts +11 -0
  131. package/src/http/active-sessions.ts +13 -2
  132. package/src/http/agents.ts +10 -3
  133. package/src/http/all-routes.ts +1 -0
  134. package/src/http/api-keys.ts +3 -1
  135. package/src/http/approval-requests.ts +2 -1
  136. package/src/http/apps.ts +36 -11
  137. package/src/http/config.ts +3 -0
  138. package/src/http/context.ts +2 -0
  139. package/src/http/core.ts +39 -2
  140. package/src/http/events.ts +6 -2
  141. package/src/http/favorite-owner.ts +1 -1
  142. package/src/http/fs.ts +38 -3
  143. package/src/http/index.ts +3 -1
  144. package/src/http/memory.ts +3 -1
  145. package/src/http/ranking.ts +367 -0
  146. package/src/http/realtime.ts +26 -21
  147. package/src/http/route-def.ts +77 -11
  148. package/src/http/script-runs.ts +2 -1
  149. package/src/http/scripts.ts +8 -0
  150. package/src/http/session-access.ts +179 -0
  151. package/src/http/session-data.ts +10 -2
  152. package/src/http/sessions.ts +212 -12
  153. package/src/http/stats.ts +7 -3
  154. package/src/http/tasks.ts +83 -7
  155. package/src/http/used-by.ts +11 -3
  156. package/src/prompts/session-templates.ts +3 -1
  157. package/src/providers/amp-adapter.ts +1 -1
  158. package/src/ranking/telemetry-ranking.ts +147 -0
  159. package/src/ranking/usage.ts +57 -0
  160. package/src/rbac/admission.ts +22 -7
  161. package/src/rbac/legacy-policy.ts +2 -0
  162. package/src/rbac/permissions.ts +9 -0
  163. package/src/realtime/auth.ts +69 -0
  164. package/src/realtime/tickets.ts +4 -1
  165. package/src/realtime/transport.ts +77 -9
  166. package/src/slack/blocks.ts +18 -0
  167. package/src/slack/dm.ts +29 -0
  168. package/src/telemetry-contract/EVENTS.md +3 -0
  169. package/src/telemetry-contract/SOURCE.json +37 -4
  170. package/src/telemetry-contract/catalog.json +69 -0
  171. package/src/telemetry-contract/ranking/fixtures/expected.json +30 -0
  172. package/src/telemetry-contract/ranking/fixtures/invalid/bad-category.json +11 -0
  173. package/src/telemetry-contract/ranking/fixtures/invalid/bad-claim.json +11 -0
  174. package/src/telemetry-contract/ranking/fixtures/invalid/blank-description.json +11 -0
  175. package/src/telemetry-contract/ranking/fixtures/invalid/blank-name.json +11 -0
  176. package/src/telemetry-contract/ranking/fixtures/invalid/description-too-long.json +11 -0
  177. package/src/telemetry-contract/ranking/fixtures/invalid/domain-uppercase.json +11 -0
  178. package/src/telemetry-contract/ranking/fixtures/invalid/domain-with-scheme.json +11 -0
  179. package/src/telemetry-contract/ranking/fixtures/invalid/domain-with-www.json +11 -0
  180. package/src/telemetry-contract/ranking/fixtures/invalid/http-source-url.json +11 -0
  181. package/src/telemetry-contract/ranking/fixtures/invalid/missing-description.json +10 -0
  182. package/src/telemetry-contract/ranking/fixtures/invalid/missing-version.json +10 -0
  183. package/src/telemetry-contract/ranking/fixtures/invalid/name-not-string.json +11 -0
  184. package/src/telemetry-contract/ranking/fixtures/invalid/name-too-long.json +11 -0
  185. package/src/telemetry-contract/ranking/fixtures/invalid/not-object.json +1 -0
  186. package/src/telemetry-contract/ranking/fixtures/invalid/null.json +1 -0
  187. package/src/telemetry-contract/ranking/fixtures/invalid/other-with-subcategory.json +11 -0
  188. package/src/telemetry-contract/ranking/fixtures/invalid/oversize.json +1 -0
  189. package/src/telemetry-contract/ranking/fixtures/invalid/short-claim.json +11 -0
  190. package/src/telemetry-contract/ranking/fixtures/invalid/subcategory-other-category.json +11 -0
  191. package/src/telemetry-contract/ranking/fixtures/invalid/trailing-data.json +1 -0
  192. package/src/telemetry-contract/ranking/fixtures/invalid/unknown-field.json +12 -0
  193. package/src/telemetry-contract/ranking/fixtures/invalid/unsupported-version.json +11 -0
  194. package/src/telemetry-contract/ranking/fixtures/invalid/updated-at-not-date.json +11 -0
  195. package/src/telemetry-contract/ranking/fixtures/invalid/updated-at-offset.json +11 -0
  196. package/src/telemetry-contract/ranking/fixtures/invalid/wrong-case-field.json +11 -0
  197. package/src/telemetry-contract/ranking/fixtures/valid/category-without-subcategory.json +11 -0
  198. package/src/telemetry-contract/ranking/fixtures/valid/full.json +11 -0
  199. package/src/telemetry-contract/ranking/fixtures/valid/minimal.json +11 -0
  200. package/src/telemetry-contract/ranking/fixtures/valid/padded-text.json +11 -0
  201. package/src/telemetry-contract/ranking/fixtures/valid/unicode-name.json +11 -0
  202. package/src/telemetry-contract/ranking/listing.v1.schema.json +61 -0
  203. package/src/telemetry-contract/ranking/taxonomy.json +8 -0
  204. package/src/telemetry-contract/ranking-taxonomy.gen.ts +11 -0
  205. package/src/telemetry-contract/types.gen.ts +708 -1
  206. package/src/telemetry.ts +26 -2
  207. package/src/tests/apps-rbac.test.ts +189 -2
  208. package/src/tests/apps-script-types.test.ts +16 -3
  209. package/src/tests/check-pr-body.test.ts +23 -0
  210. package/src/tests/check-ranking-entry.test.ts +146 -0
  211. package/src/tests/db-extraction-contract.test.ts +1 -0
  212. package/src/tests/db-task-read-contract.test.ts +1 -0
  213. package/src/tests/docs-index-footprint.test.js +2 -2
  214. package/src/tests/org-identity.test.ts +44 -0
  215. package/src/tests/ranking-listing.test.ts +112 -0
  216. package/src/tests/ranking-route.test.ts +319 -0
  217. package/src/tests/ranking-usage.test.ts +50 -0
  218. package/src/tests/rbac-admission-e2e.test.ts +8 -0
  219. package/src/tests/rbac-admission.test.ts +38 -0
  220. package/src/tests/rbac-charact-http.test.ts +4 -3
  221. package/src/tests/rbac-engine.test.ts +2 -0
  222. package/src/tests/rbac-roles.test.ts +2 -0
  223. package/src/tests/realtime-room-surfaces.test.ts +3 -1
  224. package/src/tests/realtime-session-join-race.test.ts +142 -0
  225. package/src/tests/realtime-tickets.test.ts +24 -17
  226. package/src/tests/realtime-transport.test.ts +91 -0
  227. package/src/tests/session-route-posture.test.ts +639 -0
  228. package/src/tests/session-sharing-routes.test.ts +282 -0
  229. package/src/tests/session-sharing.test.ts +526 -0
  230. package/src/tests/sessions-detail-batch.test.ts +7 -1
  231. package/src/tests/slack-tool-blocks.test.ts +101 -0
  232. package/src/tests/slack-update.test.ts +55 -0
  233. package/src/tests/slack-user-dm.test.ts +177 -0
  234. package/src/tests/used-by-route.test.ts +10 -0
  235. package/src/tools/slack-post.ts +18 -12
  236. package/src/tools/slack-read.ts +13 -1
  237. package/src/tools/slack-reply.ts +7 -10
  238. package/src/tools/slack-start-thread.ts +18 -11
  239. package/src/tools/slack-update.ts +2 -9
  240. package/src/types.ts +35 -0
  241. package/src/utils/org-identity.ts +33 -0
  242. package/templates/skills/apps/SKILL.md +17 -0
  243. package/templates/skills/apps/content.md +17 -0
  244. package/dist/handlers-tb3xgjnm.js +0 -37
  245. package/dist/index-nyfcdpky.js +0 -39
  246. package/dist/{cli-n5mrsg0f.js → cli-60cgh1rv.js} +3 -3
  247. /package/dist/{cli-j0v75y13.js → cli-y83zk5sb.js} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@desplega.ai/agent-swarm",
3
- "version": "2.3.0",
3
+ "version": "2.4.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",
@@ -0,0 +1,6 @@
1
+ export type AppActionCaller = {
2
+ kind: "user" | "agent" | "operator" | "guest";
3
+ id: string | null;
4
+ name?: string;
5
+ pageId?: string;
6
+ };
@@ -8,6 +8,13 @@ const APP_TYPES_PREAMBLE = `// ── Swarm Apps: generated per-app types (sourc
8
8
  // Rows are the app's declared columns plus the 5 system columns. Query
9
9
  // overloads narrow only when appId AND query are string literals.
10
10
 
11
+ export type AppActionCaller = {
12
+ kind: "user" | "agent" | "operator" | "guest";
13
+ id: string | null;
14
+ name?: string;
15
+ pageId?: string;
16
+ };
17
+
11
18
  export interface SwarmAppQueryResult<Row> {
12
19
  success: boolean;
13
20
  status: number;
@@ -175,9 +182,8 @@ function renderQuery(
175
182
  }
176
183
 
177
184
  function renderApp(app: AppRecord, namespace: string): string {
178
- // Reserve the generated alias so a model named `actionName` dedupes to
179
- // `ActionName_2` instead of colliding with `type ActionName`.
180
- const modelNames = new Set<string>(["ActionName"]);
185
+ // Reserve action types so model names cannot collide with them.
186
+ const modelNames = new Set<string>(["ActionName", "ActionArgs", "AppActionCaller"]);
181
187
  const renderedModels = Object.entries(app.definition.models).map(([modelName, model]) =>
182
188
  renderModel(modelName, model, modelNames),
183
189
  );
@@ -202,6 +208,13 @@ ${renderedModels.map((model) => model.source).join("\n\n")}
202
208
 
203
209
  /** Declared actions. Invocation is REST-only: POST /api/apps/<id>/actions/<name>. */
204
210
  export type ActionName = ${actionType};
211
+
212
+ /** Script action arguments include server-stamped app and caller context. */
213
+ export interface ActionArgs {
214
+ app: { id: ${JSON.stringify(app.id)} };
215
+ caller: AppActionCaller;
216
+ [key: string]: unknown;
217
+ }
205
218
  }
206
219
 
207
220
  export interface SwarmSdk {
@@ -1,3 +1,4 @@
1
+ import type { ActiveSession } from "../../types";
1
2
  import { getDbClient } from "./runtime";
2
3
 
3
4
  /**
@@ -28,3 +29,7 @@ export async function refreshActiveSessionOnActivity(
28
29
  );
29
30
  return result.changes > 0;
30
31
  }
32
+
33
+ export async function getActiveSessionById(id: string): Promise<ActiveSession | null> {
34
+ return getDbClient().get<ActiveSession>("SELECT * FROM active_sessions WHERE id = ?", [id]);
35
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Session-list visibility for session sharing. `all`: sessions the viewer
3
+ * owns, ownerless sessions, and sessions shared with the viewer or everyone.
4
+ * `mine`: owned by the viewer. `shared`: owned by someone else and shared with
5
+ * the viewer or everyone. A null `userId` (a guest page session) owns nothing.
6
+ */
7
+ export type SessionViewer = { userId: string | null; scope: "all" | "mine" | "shared" };
8
+
9
+ /** SQL condition + params restricting root tasks (alias `r`) to what `viewer` may open. */
10
+ export function sessionViewerCondition(viewer: SessionViewer): {
11
+ sql: string;
12
+ params: (string | null)[];
13
+ } {
14
+ const shared = `EXISTS (SELECT 1 FROM session_shares s
15
+ WHERE s.rootTaskId = r.id AND s.revokedAt IS NULL AND (s.userId IS NULL OR s.userId = ?))`;
16
+ if (viewer.scope === "mine") {
17
+ return { sql: "r.requestedByUserId = ?", params: [viewer.userId] };
18
+ }
19
+ if (viewer.scope === "shared") {
20
+ return {
21
+ sql: `(r.requestedByUserId IS NOT NULL AND r.requestedByUserId IS NOT ? AND ${shared})`,
22
+ params: [viewer.userId, viewer.userId],
23
+ };
24
+ }
25
+ return {
26
+ sql: `(r.requestedByUserId IS NULL OR r.requestedByUserId = ? OR ${shared})`,
27
+ params: [viewer.userId, viewer.userId],
28
+ };
29
+ }
@@ -556,10 +556,14 @@ export async function hasNonTerminalRerouteDecisionChild(parentId: string): Prom
556
556
  return row !== undefined && row !== null;
557
557
  }
558
558
 
559
- export async function getTasksByAgentId(agentId: string): Promise<AgentTask[]> {
559
+ export async function getTasksByAgentId(
560
+ agentId: string,
561
+ readableBy?: { userId: string | null },
562
+ ): Promise<AgentTask[]> {
563
+ const readable = readableBy ? readableSessionCondition(readableBy.userId) : undefined;
560
564
  const rows = await getDbClient().query<AgentTaskRow>(
561
- "SELECT * FROM agent_tasks WHERE agentId = ? ORDER BY createdAt DESC",
562
- [agentId],
565
+ `SELECT * FROM agent_tasks WHERE agentId = ?${readable ? ` AND ${readable.sql}` : ""} ORDER BY createdAt DESC`,
566
+ [agentId, ...(readable?.params ?? [])],
563
567
  );
564
568
  return rows.map(rowToAgentTask);
565
569
  }
@@ -642,6 +646,48 @@ export interface TaskFilters {
642
646
  limit?: number;
643
647
  offset?: number;
644
648
  includeHeartbeat?: boolean;
649
+ /** Only tasks of sessions this user may read; `userId: null` is a guest. */
650
+ readableBy?: { userId: string | null };
651
+ }
652
+
653
+ /**
654
+ * The SQL twin of `sessionAccessForRoot` (../../session-shares). A session's
655
+ * root is its topmost existing task; the session is hidden when the root has
656
+ * an owner other than the user and no active share names the user or
657
+ * everyone. Descending from hidden roots is linear in their tasks at any chain
658
+ * depth, and as a WHERE term it keeps paging and `COUNT(*)` on the same rows.
659
+ * `parentTaskId` is fixed at insert to an existing task, so no parent cycle
660
+ * (which would have no root) can form.
661
+ *
662
+ * `taskIdColumn` applies the same filter to any table keyed by task id. A
663
+ * nullable column keeps its task-less rows: they belong to no session.
664
+ */
665
+ export function readableSessionCondition(
666
+ userId: string | null,
667
+ taskIdColumn = "agent_tasks.id",
668
+ opts: { nullable?: boolean } = {},
669
+ ): { sql: string; params: string[] } {
670
+ const otherOwner = userId
671
+ ? "r.requestedByUserId IS NOT NULL AND r.requestedByUserId != ?"
672
+ : "r.requestedByUserId IS NOT NULL";
673
+ const shareMatch = userId ? "(s.userId IS NULL OR s.userId = ?)" : "s.userId IS NULL";
674
+ const notHidden = `${taskIdColumn} NOT IN (
675
+ WITH RECURSIVE hidden(id) AS (
676
+ SELECT r.id FROM agent_tasks r
677
+ WHERE (r.parentTaskId IS NULL
678
+ OR NOT EXISTS (SELECT 1 FROM agent_tasks p WHERE p.id = r.parentTaskId))
679
+ AND ${otherOwner}
680
+ AND NOT EXISTS (SELECT 1 FROM session_shares s
681
+ WHERE s.rootTaskId = r.id AND s.revokedAt IS NULL AND ${shareMatch})
682
+ UNION ALL
683
+ SELECT c.id FROM agent_tasks c JOIN hidden ON c.parentTaskId = hidden.id
684
+ )
685
+ SELECT id FROM hidden
686
+ )`;
687
+ return {
688
+ sql: opts.nullable ? `(${taskIdColumn} IS NULL OR ${notHidden})` : notHidden,
689
+ params: userId ? [userId, userId] : [],
690
+ };
645
691
  }
646
692
 
647
693
  export function getAllTasks(filters?: TaskFilters): Promise<AgentTask[]>;
@@ -756,6 +802,12 @@ export async function getAllTasks(
756
802
  );
757
803
  }
758
804
 
805
+ if (filters?.readableBy) {
806
+ const readable = readableSessionCondition(filters.readableBy.userId);
807
+ conditions.push(readable.sql);
808
+ params.push(...readable.params);
809
+ }
810
+
759
811
  const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
760
812
  const limit = filters?.limit ?? 25;
761
813
  const offset = filters?.offset ?? 0;
@@ -916,6 +968,12 @@ export async function getTasksCount(
916
968
  );
917
969
  }
918
970
 
971
+ if (filters?.readableBy) {
972
+ const readable = readableSessionCondition(filters.readableBy.userId);
973
+ conditions.push(readable.sql);
974
+ params.push(...readable.params);
975
+ }
976
+
919
977
  const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
920
978
  const query = `SELECT COUNT(*) as count FROM agent_tasks ${whereClause}`;
921
979
 
@@ -27,6 +27,7 @@ import {
27
27
  type AgentTaskRow,
28
28
  getTaskById,
29
29
  NEVER_STARTED_TASK_STATUSES,
30
+ readableSessionCondition,
30
31
  rowToAgentTask,
31
32
  } from "./read";
32
33
 
@@ -1019,12 +1020,16 @@ export async function resumeTask(taskId: string): Promise<AgentTask | null> {
1019
1020
  * Used on startup to resume tasks that were interrupted by deployment.
1020
1021
  * Returns tasks ordered by creation time (oldest first for FIFO).
1021
1022
  */
1022
- export async function getPausedTasksForAgent(agentId: string): Promise<AgentTask[]> {
1023
+ export async function getPausedTasksForAgent(
1024
+ agentId: string,
1025
+ readableBy?: { userId: string | null },
1026
+ ): Promise<AgentTask[]> {
1027
+ const readable = readableBy ? readableSessionCondition(readableBy.userId) : undefined;
1023
1028
  const rows = await getDbClient().query<AgentTaskRow>(
1024
1029
  `SELECT * FROM agent_tasks
1025
- WHERE agentId = ? AND status = 'paused'
1030
+ WHERE agentId = ? AND status = 'paused'${readable ? ` AND ${readable.sql}` : ""}
1026
1031
  ORDER BY createdAt ASC, rowid ASC`,
1027
- [agentId],
1032
+ [agentId, ...(readable?.params ?? [])],
1028
1033
  );
1029
1034
  return rows.map(rowToAgentTask);
1030
1035
  }
@@ -1093,15 +1098,19 @@ export async function resetOrphanedInProgressTasksForAgent(
1093
1098
  * Used by hooks to detect task cancellation and stop the worker loop.
1094
1099
  * Returns tasks cancelled within the last 5 minutes.
1095
1100
  */
1096
- export async function getRecentlyCancelledTasksForAgent(agentId: string): Promise<AgentTask[]> {
1101
+ export async function getRecentlyCancelledTasksForAgent(
1102
+ agentId: string,
1103
+ readableBy?: { userId: string | null },
1104
+ ): Promise<AgentTask[]> {
1097
1105
  const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000).toISOString();
1106
+ const readable = readableBy ? readableSessionCondition(readableBy.userId) : undefined;
1098
1107
  const rows = await getDbClient().query<AgentTaskRow>(
1099
1108
  `SELECT * FROM agent_tasks
1100
1109
  WHERE agentId = ?
1101
1110
  AND status = 'cancelled'
1102
- AND finishedAt > ?
1111
+ AND finishedAt > ?${readable ? ` AND ${readable.sql}` : ""}
1103
1112
  ORDER BY finishedAt DESC`,
1104
- [agentId, fiveMinutesAgo],
1113
+ [agentId, fiveMinutesAgo, ...(readable?.params ?? [])],
1105
1114
  );
1106
1115
  return rows.map(rowToAgentTask);
1107
1116
  }
package/src/be/db.ts CHANGED
@@ -195,6 +195,7 @@ import {
195
195
  } from "./db/context-versions";
196
196
  import { clearAgentModelProfileOnOverrideChange } from "./db/model-profiles";
197
197
  import { getDb, getDbClient } from "./db/runtime";
198
+ import { type SessionViewer, sessionViewerCondition } from "./db/session-visibility";
198
199
  import { classifyTaskHumanFree, reclassifyTaskHumanFree } from "./db/tasks/human-free";
199
200
  import {
200
201
  type AgentTaskRow,
@@ -203,6 +204,7 @@ import {
203
204
  findExistingLinearTrackerContextWork,
204
205
  getTaskById,
205
206
  getTasksByAgentId,
207
+ readableSessionCondition,
206
208
  rowToAgentTask,
207
209
  rowToAgentTaskSummary,
208
210
  } from "./db/tasks/read";
@@ -2081,13 +2083,17 @@ export async function getPendingSteeringForTask(taskId: string): Promise<Steerin
2081
2083
  return await getSteeringMessagesForTask(taskId, { status: "pending" });
2082
2084
  }
2083
2085
 
2084
- export async function getPendingSteeringForAgent(agentId: string): Promise<SteeringMessage[]> {
2086
+ export async function getPendingSteeringForAgent(
2087
+ agentId: string,
2088
+ readableBy?: { userId: string | null },
2089
+ ): Promise<SteeringMessage[]> {
2090
+ const readable = readableBy ? readableSessionCondition(readableBy.userId, "t.id") : undefined;
2085
2091
  const rows = await getDbClient().query<TaskSteeringMessageRow>(
2086
2092
  `${STEERING_MESSAGE_SELECT}
2087
2093
  JOIN agent_tasks t ON t.id = m.task_id
2088
- WHERE t.agentId = ? AND m.status = 'pending'
2094
+ WHERE t.agentId = ? AND m.status = 'pending'${readable ? ` AND ${readable.sql}` : ""}
2089
2095
  ORDER BY m.created_at ASC, m.rowid ASC`,
2090
- [agentId],
2096
+ [agentId, ...(readable?.params ?? [])],
2091
2097
  );
2092
2098
  return rows.map(rowToSteeringMessage);
2093
2099
  }
@@ -2185,27 +2191,33 @@ export async function hasPendingSteering(taskId: string): Promise<boolean> {
2185
2191
  // Combined Queries (Agent with Tasks)
2186
2192
  // ============================================================================
2187
2193
 
2188
- export async function getAgentWithTasks(id: string): Promise<AgentWithTasks | null> {
2194
+ export async function getAgentWithTasks(
2195
+ id: string,
2196
+ readableBy?: { userId: string | null },
2197
+ ): Promise<AgentWithTasks | null> {
2189
2198
  return await getDbClient().transaction(
2190
2199
  async () => {
2191
2200
  const agent = await getAgentById(id);
2192
2201
  if (!agent) return null;
2193
2202
 
2194
- const tasks = await getTasksByAgentId(id);
2203
+ const tasks = await getTasksByAgentId(id, readableBy);
2195
2204
  return { ...agent, tasks };
2196
2205
  },
2197
2206
  { readOnly: true },
2198
2207
  );
2199
2208
  }
2200
2209
 
2201
- export async function getAllAgentsWithTasks(opts?: { slim?: boolean }): Promise<AgentWithTasks[]> {
2210
+ export async function getAllAgentsWithTasks(opts?: {
2211
+ slim?: boolean;
2212
+ readableBy?: { userId: string | null };
2213
+ }): Promise<AgentWithTasks[]> {
2202
2214
  return await getDbClient().transaction(
2203
2215
  async () => {
2204
2216
  const agents = await getAllAgents({ slim: opts?.slim ?? false });
2205
2217
  return await Promise.all(
2206
2218
  agents.map(async (agent) => ({
2207
2219
  ...agent,
2208
- tasks: await getTasksByAgentId(agent.id),
2220
+ tasks: await getTasksByAgentId(agent.id, opts?.readableBy),
2209
2221
  })),
2210
2222
  );
2211
2223
  },
@@ -2268,10 +2280,16 @@ export async function createLogEntry(entry: {
2268
2280
  return rowToAgentLog(row);
2269
2281
  }
2270
2282
 
2271
- export async function getLogsByAgentId(agentId: string): Promise<AgentLog[]> {
2283
+ export async function getLogsByAgentId(
2284
+ agentId: string,
2285
+ readableBy?: { userId: string | null },
2286
+ ): Promise<AgentLog[]> {
2287
+ const readable = readableBy
2288
+ ? readableSessionCondition(readableBy.userId, "agent_log.taskId", { nullable: true })
2289
+ : undefined;
2272
2290
  const rows = await getDbClient().query<AgentLogRow>(
2273
- "SELECT * FROM agent_log WHERE agentId = ? ORDER BY createdAt DESC",
2274
- [agentId],
2291
+ `SELECT * FROM agent_log WHERE agentId = ?${readable ? ` AND ${readable.sql}` : ""} ORDER BY createdAt DESC`,
2292
+ [agentId, ...(readable?.params ?? [])],
2275
2293
  );
2276
2294
  return rows.map(rowToAgentLog);
2277
2295
  }
@@ -2315,16 +2333,24 @@ export async function getLogsByEventType(eventType: AgentLogEventType): Promise<
2315
2333
  return rows.map(rowToAgentLog);
2316
2334
  }
2317
2335
 
2318
- export async function getAllLogs(limit?: number): Promise<AgentLog[]> {
2336
+ export async function getAllLogs(
2337
+ limit?: number,
2338
+ readableBy?: { userId: string | null },
2339
+ ): Promise<AgentLog[]> {
2340
+ const readable = readableBy
2341
+ ? readableSessionCondition(readableBy.userId, "agent_log.taskId", { nullable: true })
2342
+ : undefined;
2343
+ const readableParams = readable?.params ?? [];
2319
2344
  if (limit) {
2320
2345
  const rows = await getDbClient().query<AgentLogRow>(
2321
- "SELECT * FROM agent_log WHERE eventType != 'agent_status_change' ORDER BY createdAt DESC LIMIT ?",
2322
- [limit],
2346
+ `SELECT * FROM agent_log WHERE eventType != 'agent_status_change'${readable ? ` AND ${readable.sql}` : ""} ORDER BY createdAt DESC LIMIT ?`,
2347
+ [...readableParams, limit],
2323
2348
  );
2324
2349
  return rows.map(rowToAgentLog);
2325
2350
  }
2326
2351
  const rows = await getDbClient().query<AgentLogRow>(
2327
- "SELECT * FROM agent_log ORDER BY createdAt DESC",
2352
+ `SELECT * FROM agent_log${readable ? ` WHERE ${readable.sql}` : ""} ORDER BY createdAt DESC`,
2353
+ readableParams,
2328
2354
  );
2329
2355
  return rows.map(rowToAgentLog);
2330
2356
  }
@@ -5820,8 +5846,13 @@ export interface ConcurrentContext {
5820
5846
  * Returns processing inbox messages, recent task delegations by leads,
5821
5847
  * and currently active (in-progress) tasks across the swarm.
5822
5848
  */
5823
- export async function getConcurrentContext(): Promise<ConcurrentContext> {
5849
+ export async function getConcurrentContext(readableBy?: {
5850
+ userId: string | null;
5851
+ }): Promise<ConcurrentContext> {
5824
5852
  const client = getDbClient();
5853
+ const readable = readableBy ? readableSessionCondition(readableBy.userId, "t.id") : undefined;
5854
+ const readableSql = readable ? ` AND ${readable.sql}` : "";
5855
+ const readableParams = readable?.params ?? [];
5825
5856
 
5826
5857
  // 1. Inbox messages currently being processed (status = 'processing')
5827
5858
  const processingInboxMessages = await client.query<{
@@ -5850,9 +5881,9 @@ export async function getConcurrentContext(): Promise<ConcurrentContext> {
5850
5881
  FROM agent_tasks t
5851
5882
  LEFT JOIN agents a ON t.agentId = a.id
5852
5883
  WHERE t.createdAt > ?
5853
- AND t.creatorAgentId IN (SELECT id FROM agents WHERE isLead = 1)
5884
+ AND t.creatorAgentId IN (SELECT id FROM agents WHERE isLead = 1)${readableSql}
5854
5885
  ORDER BY t.createdAt DESC`,
5855
- [fiveMinutesAgo],
5886
+ [fiveMinutesAgo, ...readableParams],
5856
5887
  );
5857
5888
 
5858
5889
  // 3. Currently in-progress tasks across the swarm
@@ -5868,8 +5899,9 @@ export async function getConcurrentContext(): Promise<ConcurrentContext> {
5868
5899
  `SELECT t.id, t.task, t.agentId, a.name as agentName, t.status, t.createdAt, t.progress
5869
5900
  FROM agent_tasks t
5870
5901
  LEFT JOIN agents a ON t.agentId = a.id
5871
- WHERE t.status = 'in_progress'
5902
+ WHERE t.status = 'in_progress'${readableSql}
5872
5903
  ORDER BY t.createdAt DESC`,
5904
+ readableParams,
5873
5905
  );
5874
5906
 
5875
5907
  return {
@@ -7157,15 +7189,27 @@ export async function deleteActiveSessionById(id: string): Promise<boolean> {
7157
7189
  return result.changes > 0;
7158
7190
  }
7159
7191
 
7160
- export async function getActiveSessions(agentId?: string): Promise<ActiveSession[]> {
7192
+ export async function getActiveSessions(
7193
+ agentId?: string,
7194
+ readableBy?: { userId: string | null },
7195
+ ): Promise<ActiveSession[]> {
7196
+ const conditions: string[] = [];
7197
+ const params: string[] = [];
7161
7198
  if (agentId) {
7162
- return await getDbClient().query<ActiveSession>(
7163
- "SELECT * FROM active_sessions WHERE agentId = ? ORDER BY startedAt DESC",
7164
- [agentId],
7165
- );
7199
+ conditions.push("agentId = ?");
7200
+ params.push(agentId);
7166
7201
  }
7202
+ if (readableBy) {
7203
+ const readable = readableSessionCondition(readableBy.userId, "active_sessions.taskId", {
7204
+ nullable: true,
7205
+ });
7206
+ conditions.push(readable.sql);
7207
+ params.push(...readable.params);
7208
+ }
7209
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
7167
7210
  return await getDbClient().query<ActiveSession>(
7168
- "SELECT * FROM active_sessions ORDER BY startedAt DESC",
7211
+ `SELECT * FROM active_sessions ${where} ORDER BY startedAt DESC`,
7212
+ params,
7169
7213
  );
7170
7214
  }
7171
7215
 
@@ -12905,6 +12949,8 @@ interface ListRecentSessionsOpts {
12905
12949
  contextKeyPrefix?: string;
12906
12950
  /** When true, return slim `SessionListItemSummary` rows (default: full). */
12907
12951
  slim?: boolean;
12952
+ /** Restrict to sessions this viewer may open (session sharing). */
12953
+ viewer?: SessionViewer;
12908
12954
  }
12909
12955
 
12910
12956
  export async function listRecentSessions(
@@ -12923,7 +12969,7 @@ export async function listRecentSessions(
12923
12969
  const requestedByUserId = opts?.requestedByUserId?.trim() || undefined;
12924
12970
 
12925
12971
  const conditions: string[] = ["r.parentTaskId IS NULL"];
12926
- const params: (string | number)[] = [];
12972
+ const params: (string | number | null)[] = [];
12927
12973
 
12928
12974
  if (sources.length > 0) {
12929
12975
  conditions.push(`r.source IN (${sources.map(() => "?").join(", ")})`);
@@ -12942,6 +12988,11 @@ export async function listRecentSessions(
12942
12988
  conditions.push(`r.contextKey LIKE ? ESCAPE '\\'`);
12943
12989
  params.push(literalPrefixPattern(opts.contextKeyPrefix));
12944
12990
  }
12991
+ if (opts?.viewer) {
12992
+ const viewer = sessionViewerCondition(opts.viewer);
12993
+ conditions.push(viewer.sql);
12994
+ params.push(...viewer.params);
12995
+ }
12945
12996
  params.push(limit, offset);
12946
12997
 
12947
12998
  const rootRows = await getDbClient().query<
@@ -13013,35 +13064,43 @@ export async function listRecentSessions(
13013
13064
  * a plain count, no recursive chain walk needed.
13014
13065
  */
13015
13066
  export async function countSessions(
13016
- opts?: Pick<ListRecentSessionsOpts, "source" | "q" | "requestedByUserId" | "contextKeyPrefix">,
13067
+ opts?: Pick<
13068
+ ListRecentSessionsOpts,
13069
+ "source" | "q" | "requestedByUserId" | "contextKeyPrefix" | "viewer"
13070
+ >,
13017
13071
  ): Promise<number> {
13018
13072
  const sources = opts?.source?.filter((s) => s.length > 0) ?? [];
13019
13073
  const q = opts?.q?.trim();
13020
13074
  const requestedByUserId = opts?.requestedByUserId?.trim() || undefined;
13021
13075
 
13022
- const conditions: string[] = ["parentTaskId IS NULL"];
13023
- const params: string[] = [];
13076
+ const conditions: string[] = ["r.parentTaskId IS NULL"];
13077
+ const params: (string | null)[] = [];
13024
13078
 
13025
13079
  if (sources.length > 0) {
13026
- conditions.push(`source IN (${sources.map(() => "?").join(", ")})`);
13080
+ conditions.push(`r.source IN (${sources.map(() => "?").join(", ")})`);
13027
13081
  params.push(...sources);
13028
13082
  }
13029
13083
  if (q && q.length > 0) {
13030
- conditions.push("(lower(task) LIKE ? OR lower(COALESCE(title, '')) LIKE ?)");
13084
+ conditions.push("(lower(r.task) LIKE ? OR lower(COALESCE(r.title, '')) LIKE ?)");
13031
13085
  const like = `%${q.toLowerCase()}%`;
13032
13086
  params.push(like, like);
13033
13087
  }
13034
13088
  if (requestedByUserId) {
13035
- conditions.push("requestedByUserId = ?");
13089
+ conditions.push("r.requestedByUserId = ?");
13036
13090
  params.push(requestedByUserId);
13037
13091
  }
13038
13092
  if (opts?.contextKeyPrefix) {
13039
- conditions.push(`contextKey LIKE ? ESCAPE '\\'`);
13093
+ conditions.push(`r.contextKey LIKE ? ESCAPE '\\'`);
13040
13094
  params.push(literalPrefixPattern(opts.contextKeyPrefix));
13041
13095
  }
13096
+ if (opts?.viewer) {
13097
+ const viewer = sessionViewerCondition(opts.viewer);
13098
+ conditions.push(viewer.sql);
13099
+ params.push(...viewer.params);
13100
+ }
13042
13101
 
13043
13102
  const row = await getDbClient().get<{ count: number }>(
13044
- `SELECT COUNT(*) AS count FROM agent_tasks WHERE ${conditions.join(" AND ")}`,
13103
+ `SELECT COUNT(*) AS count FROM agent_tasks r WHERE ${conditions.join(" AND ")}`,
13045
13104
  params,
13046
13105
  );
13047
13106
  return row?.count ?? 0;
package/src/be/events.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { EventCategory, EventName, EventSource, EventStatus, SwarmEvent } from "../types";
2
2
  import { type ScrubbedText, scrubObject, scrubSecrets } from "../utils/secret-scrubber";
3
3
  import { getDbClient } from "./db";
4
+ import { readableSessionCondition } from "./db/tasks/read";
4
5
  import type { DbExecutor } from "./db-client";
5
6
  import { breaksJsonValidity } from "./scrub-json";
6
7
  import { recordSkillInvocation } from "./skill-invocations";
@@ -262,10 +263,20 @@ export async function getEventsFiltered(filters: {
262
263
  * the agent's event history.
263
264
  */
264
265
  latestPerDataField?: { events: EventName[]; dataFields: string[] };
266
+ /** Only events outside sessions this user may not read; `userId: null` is a guest. */
267
+ readableBy?: { userId: string | null };
265
268
  }): Promise<SwarmEvent[]> {
266
269
  const conditions: string[] = [];
267
270
  const params: (string | number)[] = [];
268
271
 
272
+ if (filters.readableBy) {
273
+ const readable = readableSessionCondition(filters.readableBy.userId, "events.taskId", {
274
+ nullable: true,
275
+ });
276
+ conditions.push(readable.sql);
277
+ params.push(...readable.params);
278
+ }
279
+
269
280
  if (filters.category) {
270
281
  conditions.push("category = ?");
271
282
  params.push(filters.category);
@@ -0,0 +1,23 @@
1
+ -- Session sharing. A session is a root task (parentTaskId IS NULL) plus its
2
+ -- descendants. A root with no active row stays private to its owner
3
+ -- (agent_tasks.requestedByUserId); a root with no owner stays open to all.
4
+ --
5
+ -- Rows are never deleted: revoking sets revokedAt/revokedBy, and an access
6
+ -- change revokes the active row and inserts a new one in one transaction, so
7
+ -- every row for a root is its share history. userId NULL means everyone.
8
+ CREATE TABLE session_shares (
9
+ id TEXT PRIMARY KEY,
10
+ rootTaskId TEXT NOT NULL REFERENCES agent_tasks(id) ON DELETE CASCADE,
11
+ userId TEXT REFERENCES users(id),
12
+ access TEXT NOT NULL CHECK (access IN ('read', 'write')),
13
+ createdAt TEXT NOT NULL,
14
+ revokedAt TEXT,
15
+ revokedBy TEXT REFERENCES users(id),
16
+ created_by TEXT REFERENCES users(id),
17
+ updated_by TEXT REFERENCES users(id)
18
+ );
19
+
20
+ -- At most one active row per (root, target); revoked rows pile up beside it.
21
+ CREATE UNIQUE INDEX session_shares_active
22
+ ON session_shares(rootTaskId, COALESCE(userId, '*')) WHERE revokedAt IS NULL;
23
+ CREATE INDEX session_shares_user ON session_shares(userId) WHERE revokedAt IS NULL;
@@ -35,6 +35,8 @@ export const BUILTIN_ROLES = [
35
35
  "task.action.own",
36
36
  "task.fs.mutate",
37
37
  "favorite.write.own",
38
+ "session.share",
39
+ "session.presence",
38
40
  ],
39
41
  },
40
42
  ] satisfies (BuiltinRoleDefinition & { isBuiltin: true })[];