@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
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";
@@ -1731,7 +1733,9 @@ export async function moveAssetKey(input: {
1731
1733
  id: string;
1732
1734
  key: string;
1733
1735
  changedBy?: string;
1734
- }): Promise<boolean> {
1736
+ /** Task moves only: descendant task ids the caller already authorized to move with the task. */
1737
+ descendantTaskIds?: readonly string[];
1738
+ }): Promise<false | { movedTaskIds?: string[] }> {
1735
1739
  const key = normalizeAssetKey(input.key);
1736
1740
  // auditAssetKeys stays sync (shared with the boot audit); run it inside a
1737
1741
  // client transaction so this request-path read cannot observe another
@@ -1746,7 +1750,7 @@ export async function moveAssetKey(input: {
1746
1750
  }
1747
1751
  const previousKey = await currentAssetKey(input.entityType, input.id);
1748
1752
  if (!previousKey) return false;
1749
- if (previousKey === key) return true;
1753
+ if (previousKey === key && input.entityType !== "task") return {};
1750
1754
  if (input.entityType === "file") {
1751
1755
  const mapping = await getAssetKeyMapping(input.id);
1752
1756
  if (mapping?.sourceEntityType === "task-attachment") {
@@ -1757,48 +1761,64 @@ export async function moveAssetKey(input: {
1757
1761
  }
1758
1762
  const now = new Date().toISOString();
1759
1763
 
1760
- await getDbClient().transaction(async (tx) => {
1764
+ return getDbClient().transaction(async (tx) => {
1761
1765
  switch (input.entityType) {
1762
1766
  case "task": {
1763
- const mappedFiles = await tx.query<{ id: string; key: string }>(
1764
- `SELECT DISTINCT m.id, m."key" AS key
1767
+ const root = await getTaskById(input.id);
1768
+ if (!root) return false;
1769
+ const tasks = [root];
1770
+ for (const id of input.descendantTaskIds ?? []) {
1771
+ const task = id === root.id ? null : await getTaskById(id);
1772
+ if (task) tasks.push(task);
1773
+ }
1774
+ const movedTaskIds: string[] = [];
1775
+ for (const task of tasks) {
1776
+ if (task.key === key) continue;
1777
+ // A file shared with another task keeps the namespace of the task whose attachment
1778
+ // sources its mapping. Mappings with no attachment source follow any attaching task.
1779
+ const mappedFiles = await tx.query<{ id: string; key: string }>(
1780
+ `SELECT DISTINCT m.id, m."key" AS key
1765
1781
  FROM asset_key_mappings m
1766
1782
  JOIN task_attachments a
1767
1783
  ON m.provider_id = COALESCE(NULLIF(a.provider_id, ''), 'agent-fs')
1768
1784
  AND m.provider_org_id = COALESCE(a.agent_fs_org_id, '')
1769
1785
  AND m.provider_drive_id = COALESCE(a.agent_fs_drive_id, '')
1770
1786
  AND m.provider_key = COALESCE(NULLIF(a.provider_key, ''), a.path)
1771
- WHERE a.task_id = ?`,
1772
- [input.id],
1773
- );
1774
- await tx.run(
1775
- 'UPDATE agent_tasks SET "key" = ?, lastUpdatedAt = ?, updated_by = ? WHERE id = ?',
1776
- [key, now, input.changedBy ?? null, input.id],
1777
- );
1778
- await tx.run(
1779
- `UPDATE asset_key_mappings
1780
- SET "key" = ?, updated_at = ?, updated_by = ?
1781
- WHERE EXISTS (
1782
- SELECT 1 FROM task_attachments a
1783
1787
  WHERE a.task_id = ?
1784
- AND asset_key_mappings.provider_id = COALESCE(NULLIF(a.provider_id, ''), 'agent-fs')
1785
- AND asset_key_mappings.provider_org_id = COALESCE(a.agent_fs_org_id, '')
1786
- AND asset_key_mappings.provider_drive_id = COALESCE(a.agent_fs_drive_id, '')
1787
- AND asset_key_mappings.provider_key = COALESCE(NULLIF(a.provider_key, ''), a.path)
1788
- )`,
1789
- [key, now, input.changedBy ?? null, input.id],
1790
- );
1791
- for (const mapping of mappedFiles) {
1792
- if (mapping.key === key) continue;
1788
+ AND (m.source_entity_type IS NOT 'task-attachment'
1789
+ OR m.source_entity_id IN (SELECT id FROM task_attachments WHERE task_id = ?))`,
1790
+ [task.id, task.id],
1791
+ );
1792
+ // The namespace is metadata, and lastUpdatedAt tracks task activity, as in updateTaskTitle.
1793
+ await tx.run('UPDATE agent_tasks SET "key" = ?, updated_by = ? WHERE id = ?', [
1794
+ key,
1795
+ input.changedBy ?? null,
1796
+ task.id,
1797
+ ]);
1798
+ for (const mapping of mappedFiles) {
1799
+ if (mapping.key === key) continue;
1800
+ await tx.run(
1801
+ 'UPDATE asset_key_mappings SET "key" = ?, updated_at = ?, updated_by = ? WHERE id = ?',
1802
+ [key, now, input.changedBy ?? null, mapping.id],
1803
+ );
1804
+ await insertAssetKeyHistory({
1805
+ entityType: "file",
1806
+ entityId: mapping.id,
1807
+ previousKey: mapping.key,
1808
+ newKey: key,
1809
+ changedBy: input.changedBy,
1810
+ });
1811
+ }
1793
1812
  await insertAssetKeyHistory({
1794
- entityType: "file",
1795
- entityId: mapping.id,
1796
- previousKey: mapping.key,
1813
+ entityType: "task",
1814
+ entityId: task.id,
1815
+ previousKey: task.key,
1797
1816
  newKey: key,
1798
1817
  changedBy: input.changedBy,
1799
1818
  });
1819
+ movedTaskIds.push(task.id);
1800
1820
  }
1801
- break;
1821
+ return { movedTaskIds };
1802
1822
  }
1803
1823
  case "workflow":
1804
1824
  await tx.run(
@@ -1849,8 +1869,8 @@ export async function moveAssetKey(input: {
1849
1869
  newKey: key,
1850
1870
  changedBy: input.changedBy,
1851
1871
  });
1872
+ return {};
1852
1873
  });
1853
- return true;
1854
1874
  }
1855
1875
 
1856
1876
  export async function deleteTaskAttachment(id: string): Promise<boolean> {
@@ -2063,13 +2083,17 @@ export async function getPendingSteeringForTask(taskId: string): Promise<Steerin
2063
2083
  return await getSteeringMessagesForTask(taskId, { status: "pending" });
2064
2084
  }
2065
2085
 
2066
- 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;
2067
2091
  const rows = await getDbClient().query<TaskSteeringMessageRow>(
2068
2092
  `${STEERING_MESSAGE_SELECT}
2069
2093
  JOIN agent_tasks t ON t.id = m.task_id
2070
- WHERE t.agentId = ? AND m.status = 'pending'
2094
+ WHERE t.agentId = ? AND m.status = 'pending'${readable ? ` AND ${readable.sql}` : ""}
2071
2095
  ORDER BY m.created_at ASC, m.rowid ASC`,
2072
- [agentId],
2096
+ [agentId, ...(readable?.params ?? [])],
2073
2097
  );
2074
2098
  return rows.map(rowToSteeringMessage);
2075
2099
  }
@@ -2167,27 +2191,33 @@ export async function hasPendingSteering(taskId: string): Promise<boolean> {
2167
2191
  // Combined Queries (Agent with Tasks)
2168
2192
  // ============================================================================
2169
2193
 
2170
- 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> {
2171
2198
  return await getDbClient().transaction(
2172
2199
  async () => {
2173
2200
  const agent = await getAgentById(id);
2174
2201
  if (!agent) return null;
2175
2202
 
2176
- const tasks = await getTasksByAgentId(id);
2203
+ const tasks = await getTasksByAgentId(id, readableBy);
2177
2204
  return { ...agent, tasks };
2178
2205
  },
2179
2206
  { readOnly: true },
2180
2207
  );
2181
2208
  }
2182
2209
 
2183
- 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[]> {
2184
2214
  return await getDbClient().transaction(
2185
2215
  async () => {
2186
2216
  const agents = await getAllAgents({ slim: opts?.slim ?? false });
2187
2217
  return await Promise.all(
2188
2218
  agents.map(async (agent) => ({
2189
2219
  ...agent,
2190
- tasks: await getTasksByAgentId(agent.id),
2220
+ tasks: await getTasksByAgentId(agent.id, opts?.readableBy),
2191
2221
  })),
2192
2222
  );
2193
2223
  },
@@ -2250,10 +2280,16 @@ export async function createLogEntry(entry: {
2250
2280
  return rowToAgentLog(row);
2251
2281
  }
2252
2282
 
2253
- 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;
2254
2290
  const rows = await getDbClient().query<AgentLogRow>(
2255
- "SELECT * FROM agent_log WHERE agentId = ? ORDER BY createdAt DESC",
2256
- [agentId],
2291
+ `SELECT * FROM agent_log WHERE agentId = ?${readable ? ` AND ${readable.sql}` : ""} ORDER BY createdAt DESC`,
2292
+ [agentId, ...(readable?.params ?? [])],
2257
2293
  );
2258
2294
  return rows.map(rowToAgentLog);
2259
2295
  }
@@ -2297,16 +2333,24 @@ export async function getLogsByEventType(eventType: AgentLogEventType): Promise<
2297
2333
  return rows.map(rowToAgentLog);
2298
2334
  }
2299
2335
 
2300
- 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 ?? [];
2301
2344
  if (limit) {
2302
2345
  const rows = await getDbClient().query<AgentLogRow>(
2303
- "SELECT * FROM agent_log WHERE eventType != 'agent_status_change' ORDER BY createdAt DESC LIMIT ?",
2304
- [limit],
2346
+ `SELECT * FROM agent_log WHERE eventType != 'agent_status_change'${readable ? ` AND ${readable.sql}` : ""} ORDER BY createdAt DESC LIMIT ?`,
2347
+ [...readableParams, limit],
2305
2348
  );
2306
2349
  return rows.map(rowToAgentLog);
2307
2350
  }
2308
2351
  const rows = await getDbClient().query<AgentLogRow>(
2309
- "SELECT * FROM agent_log ORDER BY createdAt DESC",
2352
+ `SELECT * FROM agent_log${readable ? ` WHERE ${readable.sql}` : ""} ORDER BY createdAt DESC`,
2353
+ readableParams,
2310
2354
  );
2311
2355
  return rows.map(rowToAgentLog);
2312
2356
  }
@@ -5802,8 +5846,13 @@ export interface ConcurrentContext {
5802
5846
  * Returns processing inbox messages, recent task delegations by leads,
5803
5847
  * and currently active (in-progress) tasks across the swarm.
5804
5848
  */
5805
- export async function getConcurrentContext(): Promise<ConcurrentContext> {
5849
+ export async function getConcurrentContext(readableBy?: {
5850
+ userId: string | null;
5851
+ }): Promise<ConcurrentContext> {
5806
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 ?? [];
5807
5856
 
5808
5857
  // 1. Inbox messages currently being processed (status = 'processing')
5809
5858
  const processingInboxMessages = await client.query<{
@@ -5832,9 +5881,9 @@ export async function getConcurrentContext(): Promise<ConcurrentContext> {
5832
5881
  FROM agent_tasks t
5833
5882
  LEFT JOIN agents a ON t.agentId = a.id
5834
5883
  WHERE t.createdAt > ?
5835
- AND t.creatorAgentId IN (SELECT id FROM agents WHERE isLead = 1)
5884
+ AND t.creatorAgentId IN (SELECT id FROM agents WHERE isLead = 1)${readableSql}
5836
5885
  ORDER BY t.createdAt DESC`,
5837
- [fiveMinutesAgo],
5886
+ [fiveMinutesAgo, ...readableParams],
5838
5887
  );
5839
5888
 
5840
5889
  // 3. Currently in-progress tasks across the swarm
@@ -5850,8 +5899,9 @@ export async function getConcurrentContext(): Promise<ConcurrentContext> {
5850
5899
  `SELECT t.id, t.task, t.agentId, a.name as agentName, t.status, t.createdAt, t.progress
5851
5900
  FROM agent_tasks t
5852
5901
  LEFT JOIN agents a ON t.agentId = a.id
5853
- WHERE t.status = 'in_progress'
5902
+ WHERE t.status = 'in_progress'${readableSql}
5854
5903
  ORDER BY t.createdAt DESC`,
5904
+ readableParams,
5855
5905
  );
5856
5906
 
5857
5907
  return {
@@ -7139,15 +7189,27 @@ export async function deleteActiveSessionById(id: string): Promise<boolean> {
7139
7189
  return result.changes > 0;
7140
7190
  }
7141
7191
 
7142
- 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[] = [];
7143
7198
  if (agentId) {
7144
- return await getDbClient().query<ActiveSession>(
7145
- "SELECT * FROM active_sessions WHERE agentId = ? ORDER BY startedAt DESC",
7146
- [agentId],
7147
- );
7199
+ conditions.push("agentId = ?");
7200
+ params.push(agentId);
7148
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 ")}` : "";
7149
7210
  return await getDbClient().query<ActiveSession>(
7150
- "SELECT * FROM active_sessions ORDER BY startedAt DESC",
7211
+ `SELECT * FROM active_sessions ${where} ORDER BY startedAt DESC`,
7212
+ params,
7151
7213
  );
7152
7214
  }
7153
7215
 
@@ -12513,6 +12575,7 @@ interface InboxItemStateRow {
12513
12575
  dismissedAt: string | null;
12514
12576
  doneAt: string | null;
12515
12577
  readAt: string | null;
12578
+ lastReadAt: string | null;
12516
12579
  createdAt: string;
12517
12580
  lastUpdatedAt: string;
12518
12581
  }
@@ -12528,6 +12591,7 @@ function rowToInboxItemState(row: InboxItemStateRow): InboxItemState {
12528
12591
  dismissedAt: row.dismissedAt ?? undefined,
12529
12592
  doneAt: row.doneAt ?? undefined,
12530
12593
  readAt: row.readAt ?? undefined,
12594
+ lastReadAt: row.lastReadAt ?? undefined,
12531
12595
  createdAt: row.createdAt,
12532
12596
  lastUpdatedAt: row.lastUpdatedAt,
12533
12597
  };
@@ -12568,6 +12632,7 @@ export async function upsertInboxState(opts: {
12568
12632
  doneAt?: string;
12569
12633
  /** First-viewed timestamp. Sticky: once set, later calls never clear or move it. */
12570
12634
  readAt?: string;
12635
+ lastReadAt?: string | null;
12571
12636
  }): Promise<InboxItemState> {
12572
12637
  const now = new Date().toISOString();
12573
12638
  // Auto-derive timestamps from status when not explicitly provided.
@@ -12579,14 +12644,15 @@ export async function upsertInboxState(opts: {
12579
12644
  // SQLite upsert via UNIQUE(userId, itemType, itemId). readAt is sticky: the
12580
12645
  // first non-null value written wins on every subsequent conflict.
12581
12646
  const row = await getDbClient().get<InboxItemStateRow>(
12582
- `INSERT INTO inbox_item_state (userId, itemType, itemId, status, snoozeUntil, dismissedAt, doneAt, readAt, createdAt, lastUpdatedAt)
12583
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
12647
+ `INSERT INTO inbox_item_state (userId, itemType, itemId, status, snoozeUntil, dismissedAt, doneAt, readAt, lastReadAt, createdAt, lastUpdatedAt)
12648
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
12584
12649
  ON CONFLICT(userId, itemType, itemId) DO UPDATE SET
12585
12650
  status = excluded.status,
12586
12651
  snoozeUntil = excluded.snoozeUntil,
12587
12652
  dismissedAt = excluded.dismissedAt,
12588
12653
  doneAt = excluded.doneAt,
12589
12654
  readAt = COALESCE(inbox_item_state.readAt, excluded.readAt),
12655
+ lastReadAt = CASE WHEN ? THEN excluded.lastReadAt ELSE inbox_item_state.lastReadAt END,
12590
12656
  lastUpdatedAt = excluded.lastUpdatedAt
12591
12657
  RETURNING *`,
12592
12658
  [
@@ -12598,8 +12664,10 @@ export async function upsertInboxState(opts: {
12598
12664
  dismissedAt,
12599
12665
  doneAt,
12600
12666
  readAt,
12667
+ opts.lastReadAt ?? null,
12601
12668
  now,
12602
12669
  now,
12670
+ opts.lastReadAt !== undefined ? 1 : 0,
12603
12671
  ],
12604
12672
  );
12605
12673
  if (!row) throw new Error("Failed to upsert inbox state");
@@ -12881,6 +12949,8 @@ interface ListRecentSessionsOpts {
12881
12949
  contextKeyPrefix?: string;
12882
12950
  /** When true, return slim `SessionListItemSummary` rows (default: full). */
12883
12951
  slim?: boolean;
12952
+ /** Restrict to sessions this viewer may open (session sharing). */
12953
+ viewer?: SessionViewer;
12884
12954
  }
12885
12955
 
12886
12956
  export async function listRecentSessions(
@@ -12899,7 +12969,7 @@ export async function listRecentSessions(
12899
12969
  const requestedByUserId = opts?.requestedByUserId?.trim() || undefined;
12900
12970
 
12901
12971
  const conditions: string[] = ["r.parentTaskId IS NULL"];
12902
- const params: (string | number)[] = [];
12972
+ const params: (string | number | null)[] = [];
12903
12973
 
12904
12974
  if (sources.length > 0) {
12905
12975
  conditions.push(`r.source IN (${sources.map(() => "?").join(", ")})`);
@@ -12918,6 +12988,11 @@ export async function listRecentSessions(
12918
12988
  conditions.push(`r.contextKey LIKE ? ESCAPE '\\'`);
12919
12989
  params.push(literalPrefixPattern(opts.contextKeyPrefix));
12920
12990
  }
12991
+ if (opts?.viewer) {
12992
+ const viewer = sessionViewerCondition(opts.viewer);
12993
+ conditions.push(viewer.sql);
12994
+ params.push(...viewer.params);
12995
+ }
12921
12996
  params.push(limit, offset);
12922
12997
 
12923
12998
  const rootRows = await getDbClient().query<
@@ -12989,35 +13064,43 @@ export async function listRecentSessions(
12989
13064
  * a plain count, no recursive chain walk needed.
12990
13065
  */
12991
13066
  export async function countSessions(
12992
- opts?: Pick<ListRecentSessionsOpts, "source" | "q" | "requestedByUserId" | "contextKeyPrefix">,
13067
+ opts?: Pick<
13068
+ ListRecentSessionsOpts,
13069
+ "source" | "q" | "requestedByUserId" | "contextKeyPrefix" | "viewer"
13070
+ >,
12993
13071
  ): Promise<number> {
12994
13072
  const sources = opts?.source?.filter((s) => s.length > 0) ?? [];
12995
13073
  const q = opts?.q?.trim();
12996
13074
  const requestedByUserId = opts?.requestedByUserId?.trim() || undefined;
12997
13075
 
12998
- const conditions: string[] = ["parentTaskId IS NULL"];
12999
- const params: string[] = [];
13076
+ const conditions: string[] = ["r.parentTaskId IS NULL"];
13077
+ const params: (string | null)[] = [];
13000
13078
 
13001
13079
  if (sources.length > 0) {
13002
- conditions.push(`source IN (${sources.map(() => "?").join(", ")})`);
13080
+ conditions.push(`r.source IN (${sources.map(() => "?").join(", ")})`);
13003
13081
  params.push(...sources);
13004
13082
  }
13005
13083
  if (q && q.length > 0) {
13006
- conditions.push("(lower(task) LIKE ? OR lower(COALESCE(title, '')) LIKE ?)");
13084
+ conditions.push("(lower(r.task) LIKE ? OR lower(COALESCE(r.title, '')) LIKE ?)");
13007
13085
  const like = `%${q.toLowerCase()}%`;
13008
13086
  params.push(like, like);
13009
13087
  }
13010
13088
  if (requestedByUserId) {
13011
- conditions.push("requestedByUserId = ?");
13089
+ conditions.push("r.requestedByUserId = ?");
13012
13090
  params.push(requestedByUserId);
13013
13091
  }
13014
13092
  if (opts?.contextKeyPrefix) {
13015
- conditions.push(`contextKey LIKE ? ESCAPE '\\'`);
13093
+ conditions.push(`r.contextKey LIKE ? ESCAPE '\\'`);
13016
13094
  params.push(literalPrefixPattern(opts.contextKeyPrefix));
13017
13095
  }
13096
+ if (opts?.viewer) {
13097
+ const viewer = sessionViewerCondition(opts.viewer);
13098
+ conditions.push(viewer.sql);
13099
+ params.push(...viewer.params);
13100
+ }
13018
13101
 
13019
13102
  const row = await getDbClient().get<{ count: number }>(
13020
- `SELECT COUNT(*) AS count FROM agent_tasks WHERE ${conditions.join(" AND ")}`,
13103
+ `SELECT COUNT(*) AS count FROM agent_tasks r WHERE ${conditions.join(" AND ")}`,
13021
13104
  params,
13022
13105
  );
13023
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 @@
1
+ ALTER TABLE inbox_item_state ADD COLUMN lastReadAt TEXT;
@@ -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 })[];
@@ -138,8 +138,7 @@ async function buildState(ctx: any, leadId: string, nowMs: number, timeZone: str
138
138
  // db_query_concurrency_cap on a busy swarm.
139
139
  const lead = await q("SELECT heartbeatMd FROM agents WHERE id = ?", [leadId]);
140
140
  const prev = await q(
141
- `SELECT id, substr(output, 1, 400) AS output,
142
- substr(task, 1, instr(task, '## Your Standing Orders')) AS status
141
+ `SELECT id, substr(output, 1, 400) AS output
143
142
  FROM agent_tasks
144
143
  WHERE agentId = ? AND taskType = 'heartbeat-checklist' AND status = 'completed'
145
144
  ORDER BY createdAt DESC LIMIT 1`,
@@ -163,9 +162,28 @@ async function buildState(ctx: any, leadId: string, nowMs: number, timeZone: str
163
162
  "SELECT status, COUNT(*) AS n FROM agents WHERE COALESCE(role, '') != 'extension' GROUP BY status",
164
163
  );
165
164
 
165
+ // Only the producer's receipt proves which full IDs the checklist actually showed.
166
+ // Old checklists and invalid receipts conservatively treat all current failures as new.
167
+ let reportedIds: unknown = [];
168
+ if (prev[0]?.id) {
169
+ try {
170
+ const receipt = await ctx.swarm.kv_getOrNull({
171
+ namespace: "heartbeat-checklist:reported-failures",
172
+ key: prev[0].id,
173
+ });
174
+ reportedIds = typeof receipt?.value === "string" ? JSON.parse(receipt.value) : receipt?.value;
175
+ } catch {
176
+ reportedIds = [];
177
+ }
178
+ }
179
+ const prevIds = new Set(
180
+ Array.isArray(reportedIds) && reportedIds.every((id) => typeof id === "string") ? reportedIds : [],
181
+ );
166
182
  const fails = parseFailures(failed.map(renderFailureLine).join("\n"));
167
- const prevIds = new Set(parseFailures(String(prev[0]?.status ?? "")).map((f) => f.id));
168
- const newFails = fails.filter((f) => !prevIds.has(f.id));
183
+ // Compare full IDs before rendering the replay's eight-character display IDs.
184
+ const newFails = parseFailures(
185
+ failed.filter((f) => !prevIds.has(f.id)).map(renderFailureLine).join("\n"),
186
+ );
169
187
 
170
188
  const { stamp, zone } = localStamp(nowMs, timeZone);
171
189
  const mmdd = new Date(nowMs).toISOString().slice(5, 10);