@desplega.ai/agent-swarm 1.162.0 → 1.164.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 (240) hide show
  1. package/README.md +2 -1
  2. package/dist/{acp-adapter-74m73ae9.js → acp-adapter-8t22d18g.js} +2 -2
  3. package/dist/{actions-4sr14gnt.js → actions-49xrhww7.js} +32 -29
  4. package/dist/amp-adapter-p230tajt.js +34 -0
  5. package/dist/{anthropic-messages-8xxrzky9.js → anthropic-messages-hst62ws9.js} +3 -3
  6. package/dist/{app-pt68fbwp.js → app-tgzw3f3d.js} +11 -10
  7. package/dist/{assistant-cv3jytks.js → assistant-dgfjm07j.js} +33 -30
  8. package/dist/{azure-openai-responses-f5cqsyj8.js → azure-openai-responses-mh8tvf78.js} +9 -68
  9. package/dist/{boot-reembed-4ht4gqth.js → boot-reembed-0qzs614r.js} +13 -12
  10. package/dist/{boot-reembed-nxpcvpj9.js → boot-reembed-dgp6peyx.js} +12 -11
  11. package/dist/{boot-scrub-logs-k93kyw8k.js → boot-scrub-logs-fxnk021a.js} +10 -9
  12. package/dist/{boot-seeds-1skcrs0e.js → boot-seeds-xe7adjvm.js} +14 -13
  13. package/dist/{claude-adapter-r5g0tzge.js → claude-adapter-hx0n8a31.js} +22 -20
  14. package/dist/{claude-managed-adapter-yxkfxg8j.js → claude-managed-adapter-hskapeen.js} +3 -3
  15. package/dist/{claude-managed-setup-pkyrgshb.js → claude-managed-setup-p7mt6dfq.js} +1 -1
  16. package/dist/{claude-sdk-session-jxyxnrwd.js → claude-sdk-session-d61a9c8x.js} +22 -20
  17. package/dist/{cli-twrh04ap.js → cli-0y30kqz4.js} +3 -3
  18. package/dist/{cli-5xhpb3kv.js → cli-0z9dj97t.js} +9 -9
  19. package/dist/{cli-t845n9xk.js → cli-1nzb8ftr.js} +2 -2
  20. package/dist/{cli-kdkwd3yr.js → cli-1wd1n2ya.js} +38 -10
  21. package/dist/{cli-vxe6qa4h.js → cli-23az412e.js} +1 -1
  22. package/dist/{cli-bsky8g7w.js → cli-26x4d89j.js} +1 -1
  23. package/dist/{cli-swfmp1fx.js → cli-2ar0x7cz.js} +9 -9
  24. package/dist/cli-2ksgp01n.js +933 -0
  25. package/dist/{cli-rnyzn44p.js → cli-2zhhnz2h.js} +1 -1
  26. package/dist/{cli-vc9bp061.js → cli-3qaw32we.js} +5 -5
  27. package/dist/{cli-vy3ed88x.js → cli-402m8btc.js} +31 -3
  28. package/dist/{cli-5bcd7sfy.js → cli-43fh8fzw.js} +3 -3
  29. package/dist/{cli-5k6k8ta3.js → cli-4f7s6k3q.js} +13 -5
  30. package/dist/{cli-sqvfa090.js → cli-4ntg6tkp.js} +6 -5
  31. package/dist/{cli-kh04eqwe.js → cli-4y9bnpvy.js} +1 -1
  32. package/dist/{cli-qjjvzwzd.js → cli-5c2j5yvx.js} +1 -1
  33. package/dist/{cli-2zy95vhm.js → cli-6891ft0y.js} +2 -2
  34. package/dist/{cli-tre4bdgq.js → cli-6behs2bt.js} +1 -1
  35. package/dist/cli-7mm2r1kp.js +17 -0
  36. package/dist/cli-7xcgj20z.js +68 -0
  37. package/dist/{cli-4cwxn7gp.js → cli-812vt3ht.js} +2 -2
  38. package/dist/{cli-mbssg1xv.js → cli-8kt0bmgx.js} +1 -1
  39. package/dist/{cli-wjgs5wxe.js → cli-8rpq35c0.js} +7 -7
  40. package/dist/{cli-jxs2ygaw.js → cli-9m8fk2v7.js} +74 -29
  41. package/dist/{cli-pkd6s2wy.js → cli-9q3wt23f.js} +2 -2
  42. package/dist/{cli-s3hwpt9v.js → cli-9s6fsg07.js} +30 -10
  43. package/dist/{cli-xqpk3rp7.js → cli-9x3bwfd3.js} +28 -5
  44. package/dist/{cli-qrgt5bqf.js → cli-agamgm0p.js} +1 -1
  45. package/dist/{cli-13hw4z91.js → cli-b0f70z24.js} +1 -1
  46. package/dist/{cli-gpqpmns7.js → cli-cgrvsz2t.js} +2 -2
  47. package/dist/{cli-2hhfhfq5.js → cli-cn53e7vc.js} +8 -1
  48. package/dist/{cli-p0r8sxj5.js → cli-crjazzsg.js} +222 -4
  49. package/dist/{cli-tcen9p5w.js → cli-d37n8b7c.js} +5 -5
  50. package/dist/{cli-wxkkd0e0.js → cli-dgh781z9.js} +3 -3
  51. package/dist/cli-dsdg2wrk.js +516 -0
  52. package/dist/{cli-q24re23n.js → cli-e2xnc4m5.js} +4 -4
  53. package/dist/{cli-f7kvq46p.js → cli-erxeg64j.js} +20 -0
  54. package/dist/{cli-8ev9xqnx.js → cli-fajbc7sa.js} +17 -219
  55. package/dist/{cli-g739zqzt.js → cli-fj86xr3c.js} +2 -2
  56. package/dist/{cli-zvhd3f64.js → cli-fpbvcf55.js} +3 -3
  57. package/dist/{cli-kp42yek0.js → cli-fyra5y8k.js} +424 -71
  58. package/dist/{cli-jyy6pm2f.js → cli-g3qgrzck.js} +15 -3
  59. package/dist/{cli-eg0b3p0k.js → cli-g5erf0zt.js} +23 -14
  60. package/dist/{cli-cvatsam7.js → cli-gcvzvq3e.js} +4 -4
  61. package/dist/{cli-89s4ngw3.js → cli-gvcgmx69.js} +1 -1
  62. package/dist/{cli-7tre70zp.js → cli-j261c831.js} +138 -128
  63. package/dist/{cli-sptrj5f5.js → cli-j3me0jw2.js} +2 -2
  64. package/dist/{cli-zxqx25nz.js → cli-k6w44yvq.js} +6 -6
  65. package/dist/cli-ndx5yz5n.js +3299 -0
  66. package/dist/{cli-ykk5jaax.js → cli-nexwh8zg.js} +34 -29
  67. package/dist/{cli-rfjw9jw2.js → cli-ng97fjq7.js} +2 -2
  68. package/dist/{cli-4x0csdjq.js → cli-ngne1dng.js} +2 -2
  69. package/dist/{cli-4dcpaefp.js → cli-pfzrhb8n.js} +2 -2
  70. package/dist/{cli-hb5c2x5y.js → cli-q38n4bq0.js} +2 -2
  71. package/dist/{cli-cebmgwsc.js → cli-r109nq7n.js} +4 -4
  72. package/dist/{cli-nq7yqw7b.js → cli-t7g0vmtc.js} +4 -4
  73. package/dist/{cli-jf7wmv1r.js → cli-tsvj33y0.js} +58 -19
  74. package/dist/{cli-7af8yhav.js → cli-vbr859rz.js} +7 -4
  75. package/dist/{cli-nt8hecg9.js → cli-wk3v6h55.js} +67 -38
  76. package/dist/{cli-vyqs9ya8.js → cli-wwvk0tsy.js} +7 -7
  77. package/dist/{cli-rj0gqwwp.js → cli-y2de3bzp.js} +1 -1
  78. package/dist/{cli-hmsw1gpy.js → cli-y74170pb.js} +4 -4
  79. package/dist/cli-ydt3zkwv.js +70 -0
  80. package/dist/{cli-xk79303e.js → cli-yj790e19.js} +9 -1
  81. package/dist/cli-zt3w1cn1.js +137 -0
  82. package/dist/{cli-trc24wk2.js → cli-zxg80zs9.js} +1 -1
  83. package/dist/cli.js +23 -22
  84. package/dist/{cloudflare-workers-ai-system-one-k8ddqjkq.js → cloudflare-workers-ai-system-one-tt484t61.js} +2 -2
  85. package/dist/{codex-adapter-esvmnhn7.js → codex-adapter-e3pky8aj.js} +14 -13
  86. package/dist/{codex-hook-m73jmqgx.js → codex-hook-b1fb7c2k.js} +4 -3
  87. package/dist/{codex-session-runner-ban9excr.js → codex-session-runner-1gq27w2t.js} +14 -13
  88. package/dist/{commands-75413p2n.js → commands-q07ezjzn.js} +10 -9
  89. package/dist/cursor-adapter-50cz7yba.js +32 -0
  90. package/dist/{db-7f5pecwg.js → db-wwt0sq9q.js} +18 -9
  91. package/dist/{dispatcher-fh9at8nz.js → dispatcher-wrg35vdd.js} +31 -28
  92. package/dist/{dsh-adapter-wpze65d4.js → dsh-adapter-sa6rxfpc.js} +6 -6
  93. package/dist/{e2b-gjgdjp91.js → e2b-v5np73yj.js} +4 -4
  94. package/dist/{easter-egg-3d-3w5c2n4c.js → easter-egg-3d-h2hx9b64.js} +2 -2
  95. package/dist/{event-bus-sfx8dx3t.js → event-bus-vkbhzt4n.js} +1 -1
  96. package/dist/{execute-f50wg9nh.js → execute-6gczxy8v.js} +42 -13
  97. package/dist/{google-generative-ai-jxxpnegy.js → google-generative-ai-d6nphg3q.js} +3 -3
  98. package/dist/{google-vertex-x7q1angm.js → google-vertex-cpshkref.js} +3 -3
  99. package/dist/{handlers-w13787bb.js → handlers-rv3gxvsp.js} +34 -31
  100. package/dist/{hook-46g1jrvx.js → hook-bcf5m80y.js} +15 -13
  101. package/dist/{hook-4tpts2ry.js → hook-z8b1jm2d.js} +16 -14
  102. package/dist/{http-19pzbfrt.js → http-56f7yt97.js} +932 -134
  103. package/dist/{index-scptvdxg.js → index-0kncjnrm.js} +30 -23
  104. package/dist/{index-dqmp15bq.js → index-1e6cg96n.js} +31 -28
  105. package/dist/{index-qb4q1y3e.js → index-8xgdb3k8.js} +31 -28
  106. package/dist/index-adw4zr9j.js +1020 -0
  107. package/dist/{index-h5gs261z.js → index-etdrp7yp.js} +140 -140
  108. package/dist/{index-8fd4dm4h.js → index-f9zscmft.js} +32 -29
  109. package/dist/index-fdk1t4d6.js +91297 -0
  110. package/dist/{index-s75wc4wd.js → index-jvb37h77.js} +12 -11
  111. package/dist/{keepalive-xa5ts9cd.js → keepalive-rp8daj4s.js} +12 -11
  112. package/dist/{lead-qp4xkvjq.js → lead-dfp77erc.js} +45 -40
  113. package/dist/{maintenance-rm5885hm.js → maintenance-wtmw3jqv.js} +13 -12
  114. package/dist/{mistral-conversations-rw7fbt22.js → mistral-conversations-8445ddjn.js} +2 -2
  115. package/dist/{oauth-refresh-sweep-1csdfebf.js → oauth-refresh-sweep-yhzqt95v.js} +11 -10
  116. package/dist/{onboard-r6b78cs4.js → onboard-9z8rwbcy.js} +2 -2
  117. package/dist/{openai-codex-responses-efvrgrq3.js → openai-codex-responses-9ynm88zy.js} +3 -3
  118. package/dist/{openai-completions-bbbrrz4z.js → openai-completions-nhqwjmaw.js} +2 -2
  119. package/dist/{openai-responses-9jjyrcm1.js → openai-responses-xx1c4kgm.js} +3 -3
  120. package/dist/{opencode-adapter-kb3g6pkf.js → opencode-adapter-1nxb6555.js} +5 -5
  121. package/dist/{otel-impl-0y9knpky.js → otel-impl-b83gaxw2.js} +1 -1
  122. package/dist/{pi-mono-adapter-z6s1bme2.js → pi-mono-adapter-ddxz8pjp.js} +18 -17
  123. package/dist/{pricing-refresh-mth26tgp.js → pricing-refresh-tmjvazb6.js} +12 -11
  124. package/dist/{rbac-roles-kwntb23q.js → rbac-roles-pa0gzjws.js} +11 -10
  125. package/dist/{rbac-roles-vpk4ebm8.js → rbac-roles-rhexa9s2.js} +12 -11
  126. package/dist/{render-v2-hcm7h00y.js → render-v2-ask84n8y.js} +11 -10
  127. package/dist/{runtime-vfjsfsc5.js → runtime-ckjnj3vf.js} +4 -4
  128. package/dist/{setup-hbyzd65h.js → setup-5ga5gdsy.js} +2 -2
  129. package/dist/{typesafe-system-one-2ef30pec.js → typesafe-system-one-datfr82p.js} +2 -2
  130. package/dist/{virtual-modules-9bv7ry91.js → virtual-modules-bmp3zwjf.js} +12 -11
  131. package/dist/{work-objects-m09sxnhe.js → work-objects-snnh0t7j.js} +34 -31
  132. package/dist/{worker-0h35rvk4.js → worker-rxyymrhb.js} +45 -40
  133. package/openapi.json +997 -39
  134. package/package.json +6 -5
  135. package/src/be/db/active-sessions.ts +30 -0
  136. package/src/be/db/approvals.ts +54 -0
  137. package/src/be/db.ts +25 -4
  138. package/src/be/mcp-server-checked-update.ts +34 -0
  139. package/src/be/mcp-server-stdio-gate.ts +34 -0
  140. package/src/be/memory/key-browser.ts +304 -0
  141. package/src/be/memory/providers/sqlite-store.ts +16 -3
  142. package/src/be/migrations/194_memory_orphan_chunks.sql +15 -0
  143. package/src/be/migrations/195_approval_request_approvals.sql +6 -0
  144. package/src/be/model-validation.ts +12 -2
  145. package/src/be/multi-runtime.ts +4 -3
  146. package/src/be/pricing-normalize.ts +11 -0
  147. package/src/be/repo-checked-update.ts +37 -0
  148. package/src/be/repo-merge-policy.ts +15 -0
  149. package/src/be/seed-pricing.ts +48 -1
  150. package/src/be/swarm-config-guard.ts +1 -0
  151. package/src/commands/provider-credentials.ts +44 -2
  152. package/src/extensions/contract-types.generated.ts +1 -1
  153. package/src/extensions/contract.ts +100 -0
  154. package/src/extensions/dispatcher.ts +19 -0
  155. package/src/extensions/post-bridge.ts +191 -0
  156. package/src/github/handlers.ts +128 -2
  157. package/src/gitlab/workflow-action-name.ts +15 -0
  158. package/src/heartbeat/heartbeat.ts +4 -3
  159. package/src/http/active-sessions.ts +6 -5
  160. package/src/http/agents.ts +10 -1
  161. package/src/http/approval-requests.ts +271 -35
  162. package/src/http/index.ts +1 -1
  163. package/src/http/mcp-servers.ts +74 -50
  164. package/src/http/memory.ts +275 -6
  165. package/src/http/repos.ts +58 -18
  166. package/src/http/request-principal.ts +19 -0
  167. package/src/http/session-cost-recompute.ts +64 -0
  168. package/src/http/session-data.ts +11 -0
  169. package/src/http/tasks.ts +9 -1
  170. package/src/http/webhooks.ts +17 -5
  171. package/src/prompts/base-prompt.ts +9 -4
  172. package/src/prompts/resolver.ts +1 -1
  173. package/src/prompts/session-templates.ts +28 -0
  174. package/src/providers/amp-adapter.ts +1232 -0
  175. package/src/providers/cursor-adapter.ts +638 -0
  176. package/src/providers/index.ts +9 -1
  177. package/src/providers/pricing-sources.md +39 -0
  178. package/src/providers/reasoning-effort.ts +15 -1
  179. package/src/providers/types.ts +11 -1
  180. package/src/rbac/legacy-policy.ts +23 -8
  181. package/src/rbac/permissions.ts +27 -1
  182. package/src/scripts-runtime/types/swarm-extension.d.ts +119 -0
  183. package/src/tests/amp-adapter.test.ts +822 -0
  184. package/src/tests/amp-budget-export-failed.test.ts +133 -0
  185. package/src/tests/amp-pricing-helpers.ts +16 -0
  186. package/src/tests/amp-pricing.test.ts +218 -0
  187. package/src/tests/api-drain-worker-e2e.test.ts +18 -7
  188. package/src/tests/approval-respond-auth.test.ts +371 -0
  189. package/src/tests/base-prompt.test.ts +13 -0
  190. package/src/tests/claude-adapter-binary.test.ts +24 -0
  191. package/src/tests/claude-adapter.test.ts +8 -0
  192. package/src/tests/cursor-adapter.test.ts +690 -0
  193. package/src/tests/db-task-read-contract.test.ts +5 -0
  194. package/src/tests/db-task-write-contract.test.ts +5 -0
  195. package/src/tests/docs-index-footprint.test.js +3 -3
  196. package/src/tests/extensions-post-bridge.test.ts +669 -0
  197. package/src/tests/extensions-run-context.test.ts +61 -0
  198. package/src/tests/fixtures/fake-amp.ts +304 -0
  199. package/src/tests/github-handlers-inline-comments.test.ts +147 -1
  200. package/src/tests/gitlab-workflow-event-names.test.ts +136 -0
  201. package/src/tests/mcp-input-ergonomics.test.ts +15 -10
  202. package/src/tests/mcp-server-stdio-gate.test.ts +438 -0
  203. package/src/tests/memory-http-access.test.ts +221 -0
  204. package/src/tests/memory-keys-endpoint.test.ts +406 -0
  205. package/src/tests/memory-store.test.ts +166 -0
  206. package/src/tests/model-tier-resolution.test.ts +3 -3
  207. package/src/tests/pricing-refresh.test.ts +5 -4
  208. package/src/tests/prompt-template-session.test.ts +5 -3
  209. package/src/tests/provider-process-group.test.ts +41 -0
  210. package/src/tests/provider-registration.test.ts +3 -1
  211. package/src/tests/rbac-charact-misc-tools.test.ts +18 -3
  212. package/src/tests/rbac-engine.test.ts +15 -11
  213. package/src/tests/rbac-wire-e2e.test.ts +4 -9
  214. package/src/tests/repo-allow-merge-gate.test.ts +382 -0
  215. package/src/tests/script-connections.test.ts +4 -0
  216. package/src/tests/send-task-requester-gate.test.ts +260 -0
  217. package/src/tests/session-activity-liveness.test.ts +248 -0
  218. package/src/tests/session-costs-golden.test.ts +28 -0
  219. package/src/tests/webhooks-task-blocked.test.ts +4 -4
  220. package/src/tests/write-gate.ts +68 -0
  221. package/src/tests/x402-client.test.ts +124 -0
  222. package/src/tests/x402-spending-tracker.test.ts +66 -0
  223. package/src/tools/mcp-servers/mcp-server-create.ts +23 -1
  224. package/src/tools/mcp-servers/mcp-server-update.ts +46 -28
  225. package/src/tools/memory-delete.ts +3 -3
  226. package/src/tools/repos/update-repo.ts +26 -5
  227. package/src/tools/send-task.ts +24 -1
  228. package/src/tools/steer-task.ts +1 -1
  229. package/src/types.ts +37 -2
  230. package/src/utils/amp-models.ts +114 -0
  231. package/src/utils/credentials.ts +8 -0
  232. package/src/utils/process-group.ts +79 -0
  233. package/src/workflows/event-bus.ts +28 -0
  234. package/src/x402/client.ts +36 -14
  235. package/src/x402/spending-tracker.ts +64 -3
  236. package/templates/skills/agent-browser/SKILL.md +5 -0
  237. package/templates/skills/agent-browser/content.md +5 -0
  238. package/templates/skills/heartbeat-runbook/content.md +1 -0
  239. package/templates/skills/swarm-extensions/content.md +4 -0
  240. package/dist/cli-y89t4nca.js +0 -67
@@ -7,6 +7,16 @@ import { CANDIDATE_SET_MULTIPLIER } from "../be/memory/constants";
7
7
  import { listEdgesForAgent } from "../be/memory/edges-store";
8
8
  import { expandCandidatesWithGraph } from "../be/memory/graph-expansion";
9
9
  import { indexMemoryContent } from "../be/memory/index-content";
10
+ import {
11
+ checkChunkIntegrity,
12
+ estimateTokens,
13
+ getMemoryChunks,
14
+ getPosteriorsByIds,
15
+ listMemoryKeys,
16
+ MEMORY_KEYS_DEFAULT_PREFIX,
17
+ MEMORY_KEYS_MAX_LIMIT,
18
+ posteriorMean,
19
+ } from "../be/memory/key-browser";
10
20
  import { MemoryKeyError } from "../be/memory/key-guard";
11
21
  import { MEMORY_KEY_MAX_LENGTH } from "../be/memory/key-paths";
12
22
  import { refreshLinks } from "../be/memory/link-resolver";
@@ -223,15 +233,24 @@ const MemoryListResultItemSchema = z.object({
223
233
  chunkIndex: z.number(),
224
234
  totalChunks: z.number(),
225
235
  tags: z.array(z.string()),
236
+ key: z.string().nullable(),
237
+ updatedAt: z.string().nullable(),
238
+ rating: z.number().describe("Usefulness posterior mean alpha / (alpha + beta); 0.5 = no signal"),
226
239
  });
227
240
 
228
241
  const listMemory = route({
229
242
  method: "post",
230
243
  path: "/api/memory/list",
231
244
  pattern: ["api", "memory", "list"],
232
- summary: "List or semantically search memories across all agents (debug/admin)",
245
+ summary: "List or semantically search memories (debug/admin)",
246
+ description:
247
+ "The operator key, a user, and the lead see every agent's memories. Any other agent (an `aseph_` session token, or the shared key with `X-Agent-ID`) sees only its own memories and swarm-scope memories.",
233
248
  tags: ["Memory"],
234
249
  auth: { apiKey: true },
250
+ rbac: {
251
+ ungated:
252
+ "read-only listing; the handler narrows an agent principal to its own and swarm-scope rows",
253
+ },
235
254
  body: z.object({
236
255
  query: z
237
256
  .string()
@@ -266,6 +285,136 @@ const listMemory = route({
266
285
  },
267
286
  });
268
287
 
288
+ // Mirrors `MemoryKeySummary` (src/be/memory/key-browser.ts).
289
+ const MemoryKeySummarySchema = z.object({
290
+ key: z.string(),
291
+ scope: AgentMemoryScopeSchema,
292
+ agentId: z.string().nullable(),
293
+ memoryId: z.string(),
294
+ name: z.string(),
295
+ source: AgentMemorySourceSchema,
296
+ chunkRows: z.number(),
297
+ totalChunks: z.number(),
298
+ complete: z.boolean(),
299
+ chars: z.number(),
300
+ estTokens: z.number(),
301
+ accessCount: z.number(),
302
+ lastAccessedAt: z.string().nullable(),
303
+ rating: z.number(),
304
+ alpha: z.number(),
305
+ beta: z.number(),
306
+ usefulRatings: z.number(),
307
+ notUsefulRatings: z.number(),
308
+ createdAt: z.string(),
309
+ updatedAt: z.string(),
310
+ });
311
+
312
+ const listMemoryKeysRoute = route({
313
+ method: "get",
314
+ path: "/api/memory/keys",
315
+ pattern: ["api", "memory", "keys"],
316
+ summary:
317
+ "One aggregate row per keyed memory under a key prefix: chunks, usage, rating, estimated tokens (debug/admin)",
318
+ description:
319
+ "The operator key, a user, and the lead see every agent's memories. Any other agent sees only its own memories and swarm-scope memories.",
320
+ tags: ["Memory"],
321
+ auth: { apiKey: true },
322
+ query: z.object({
323
+ prefix: z
324
+ .string()
325
+ .min(1)
326
+ .max(MEMORY_KEY_MAX_LENGTH)
327
+ .default(MEMORY_KEYS_DEFAULT_PREFIX)
328
+ .describe("Literal, case-sensitive key prefix (default '/longterm/')."),
329
+ limit: z.coerce.number().int().min(1).max(MEMORY_KEYS_MAX_LIMIT).default(MEMORY_KEYS_MAX_LIMIT),
330
+ }),
331
+ responses: {
332
+ 200: {
333
+ description:
334
+ "Keyed memories grouped by (key, scope, agentId). accessCount is summed over chunk rows; rating pools alpha/beta over chunk rows; estTokens = ceil(chars / 4)",
335
+ schema: z.object({
336
+ prefix: z.string(),
337
+ keys: z.array(MemoryKeySummarySchema),
338
+ truncated: z.boolean(),
339
+ }),
340
+ },
341
+ 400: { description: "Validation error" },
342
+ },
343
+ });
344
+
345
+ const MemoryChunkSchema = z.object({
346
+ id: z.string(),
347
+ agentId: z.string().nullable(),
348
+ scope: AgentMemoryScopeSchema,
349
+ key: z.string().nullable(),
350
+ name: z.string(),
351
+ content: z.string(),
352
+ source: AgentMemorySourceSchema,
353
+ sourceTaskId: z.string().nullable(),
354
+ sourcePath: z.string().nullable(),
355
+ chunkIndex: z.number(),
356
+ totalChunks: z.number(),
357
+ tags: z.array(z.string()),
358
+ createdAt: z.string(),
359
+ updatedAt: z.string().nullable(),
360
+ accessedAt: z.string(),
361
+ expiresAt: z.string().nullable(),
362
+ accessCount: z.number(),
363
+ embeddingModel: z.string().nullable(),
364
+ rating: z.number(),
365
+ alpha: z.number(),
366
+ beta: z.number(),
367
+ version: z.number(),
368
+ estTokens: z.number(),
369
+ });
370
+
371
+ const getMemoryChunksRoute = route({
372
+ method: "get",
373
+ path: "/api/memory/chunks",
374
+ pattern: ["api", "memory", "chunks"],
375
+ summary:
376
+ "Every chunk row of one memory in chunkIndex order, with a chunk-integrity check (debug/admin). Does not count as an access",
377
+ description:
378
+ "Visibility matches the memory list: an agent other than the lead gets 404 for another agent's agent-scope memory.",
379
+ tags: ["Memory"],
380
+ auth: { apiKey: true },
381
+ query: z
382
+ .object({
383
+ memoryId: z.string().uuid().optional().describe("Any chunk row of the memory."),
384
+ key: z.string().min(1).max(MEMORY_KEY_MAX_LENGTH).optional(),
385
+ scope: AgentMemoryScopeSchema.optional(),
386
+ agentId: z
387
+ .string()
388
+ .optional()
389
+ .describe("Owner agent id; pass an empty string for rows without one."),
390
+ })
391
+ .refine((q) => q.memoryId || q.key, { message: "memoryId or key required" }),
392
+ responses: {
393
+ 200: {
394
+ description: "Chunk rows and integrity findings",
395
+ schema: z.object({
396
+ key: z.string().nullable(),
397
+ scope: AgentMemoryScopeSchema,
398
+ agentId: z.string().nullable(),
399
+ chunks: z.array(MemoryChunkSchema),
400
+ estTokens: z.number(),
401
+ integrity: z.object({
402
+ ok: z.boolean(),
403
+ expectedChunks: z.number(),
404
+ presentIndexes: z.array(z.number()),
405
+ missingIndexes: z.array(z.number()),
406
+ duplicateIndexes: z.array(z.number()),
407
+ conflictingTotals: z.array(z.number()),
408
+ outOfRangeIds: z.array(z.string()),
409
+ issues: z.array(z.string()),
410
+ }),
411
+ }),
412
+ },
413
+ 400: { description: "Validation error" },
414
+ 404: { description: "Memory not found" },
415
+ },
416
+ });
417
+
269
418
  // Mirrors `MemoryVecPopulateStats` (src/be/memory/types.ts).
270
419
  const MemoryVecPopulateStatsSchema = z.object({
271
420
  attempted: z.number(),
@@ -395,11 +544,15 @@ const deleteMemoryById = route({
395
544
  path: "/api/memory/{id}",
396
545
  pattern: ["api", "memory", null],
397
546
  summary: "Delete a single memory by ID (debug/admin)",
547
+ description:
548
+ "The operator key and users may delete any memory. The lead may delete its own memories and swarm-scope memories. Any other agent may delete only its own agent-scope memories.",
398
549
  tags: ["Memory"],
399
550
  auth: { apiKey: true },
551
+ rbac: { permission: "memory.delete.any" },
400
552
  params: z.object({ id: z.string().uuid() }),
401
553
  responses: {
402
554
  200: { description: "Memory deleted", schema: z.object({ deleted: z.boolean() }) },
555
+ 403: { description: "Caller may not delete this memory" },
403
556
  404: { description: "Memory not found" },
404
557
  },
405
558
  });
@@ -643,6 +796,12 @@ async function ingestPrincipal(
643
796
  return { kind: "agent", agentId: "", isLead: false };
644
797
  }
645
798
 
799
+ /** Whose rows a memory list may show. `seesAll` lifts the own-or-swarm filter. */
800
+ function memoryListViewer(principal: RbacPrincipal): { agentId: string; seesAll: boolean } {
801
+ const seesAll = can({ principal, verb: "memory.read.any", source: "http" }).allow;
802
+ return { agentId: principal.kind === "agent" ? principal.agentId : "", seesAll };
803
+ }
804
+
646
805
  export async function handleMemory(
647
806
  req: IncomingMessage,
648
807
  res: ServerResponse,
@@ -852,6 +1011,9 @@ export async function handleMemory(
852
1011
 
853
1012
  const { query, agentId, scope, source, sourcePath, limit, offset } = parsed.body;
854
1013
  const store = getMemoryStore();
1014
+ // Humans and the lead see every agent's rows; any other agent sees its own
1015
+ // rows plus swarm-scope rows (the store's non-lead scope conditions).
1016
+ const viewer = memoryListViewer(await ingestPrincipal(req, myAgentId));
855
1017
  const pageLimit = Math.min(limit, 100);
856
1018
  const pathNeedle = sourcePath?.trim().toLowerCase();
857
1019
  const matchesPath = (p: string | null) =>
@@ -866,10 +1028,10 @@ export async function handleMemory(
866
1028
  4096,
867
1029
  Math.max(offset + pageLimit, pageLimit) * CANDIDATE_SET_MULTIPLIER,
868
1030
  );
869
- let candidates = await store.search(queryEmbedding ?? new Float32Array(0), agentId ?? "", {
1031
+ let candidates = await store.search(queryEmbedding ?? new Float32Array(0), viewer.agentId, {
870
1032
  scope,
871
1033
  limit: candidateLimit,
872
- isLead: true,
1034
+ isLead: viewer.seesAll,
873
1035
  source,
874
1036
  queryText: query.trim(),
875
1037
  });
@@ -904,6 +1066,9 @@ export async function handleMemory(
904
1066
  chunkIndex: r.chunkIndex,
905
1067
  totalChunks: r.totalChunks,
906
1068
  tags: r.tags,
1069
+ key: r.key ?? null,
1070
+ updatedAt: r.updatedAt ?? null,
1071
+ rating: posteriorMean(r.alpha, r.beta),
907
1072
  })),
908
1073
  total: candidates.length,
909
1074
  limit: pageLimit,
@@ -917,13 +1082,14 @@ export async function handleMemory(
917
1082
  scope,
918
1083
  limit: pageLimit,
919
1084
  offset,
920
- isLead: true,
1085
+ isLead: viewer.seesAll,
921
1086
  ownerAgentId: agentId,
922
1087
  source,
923
1088
  sourcePath: pathNeedle,
924
1089
  };
925
- const rows = await store.list(agentId ?? "", listOptions);
926
- const total = await store.count(agentId ?? "", listOptions);
1090
+ const rows = await store.list(viewer.agentId, listOptions);
1091
+ const total = await store.count(viewer.agentId, listOptions);
1092
+ const posteriors = await getPosteriorsByIds(rows.map((r) => r.id));
927
1093
 
928
1094
  listMemory.respond(res, 200, {
929
1095
  results: rows.map((r) => ({
@@ -943,6 +1109,12 @@ export async function handleMemory(
943
1109
  chunkIndex: r.chunkIndex,
944
1110
  totalChunks: r.totalChunks,
945
1111
  tags: r.tags,
1112
+ key: r.key ?? null,
1113
+ updatedAt: r.updatedAt ?? null,
1114
+ rating: (() => {
1115
+ const p = posteriors.get(r.id);
1116
+ return p ? posteriorMean(p.alpha, p.beta) : 0.5;
1117
+ })(),
946
1118
  })),
947
1119
  total,
948
1120
  limit: pageLimit,
@@ -956,6 +1128,88 @@ export async function handleMemory(
956
1128
  return true;
957
1129
  }
958
1130
 
1131
+ if (listMemoryKeysRoute.match(req.method, pathSegments)) {
1132
+ const parsed = await listMemoryKeysRoute.parse(
1133
+ req,
1134
+ res,
1135
+ pathSegments,
1136
+ parseQueryParams(req.url || ""),
1137
+ );
1138
+ if (!parsed) return true;
1139
+ const { prefix, limit } = parsed.query;
1140
+ const viewer = memoryListViewer(await ingestPrincipal(req, myAgentId));
1141
+ const { keys, truncated } = await listMemoryKeys({
1142
+ prefix,
1143
+ limit,
1144
+ visibleToAgentId: viewer.seesAll ? undefined : viewer.agentId,
1145
+ });
1146
+ listMemoryKeysRoute.respond(res, 200, {
1147
+ prefix,
1148
+ keys: keys.map((k) => ({
1149
+ ...k,
1150
+ scope: AgentMemoryScopeSchema.parse(k.scope),
1151
+ source: AgentMemorySourceSchema.parse(k.source),
1152
+ })),
1153
+ truncated,
1154
+ });
1155
+ return true;
1156
+ }
1157
+
1158
+ if (getMemoryChunksRoute.match(req.method, pathSegments)) {
1159
+ const parsed = await getMemoryChunksRoute.parse(
1160
+ req,
1161
+ res,
1162
+ pathSegments,
1163
+ parseQueryParams(req.url || ""),
1164
+ );
1165
+ if (!parsed) return true;
1166
+ const { memoryId, key, scope, agentId } = parsed.query;
1167
+ // Same visibility as the list: a hidden memory reads as not found.
1168
+ const viewer = memoryListViewer(await ingestPrincipal(req, myAgentId));
1169
+ const visibility = { visibleToAgentId: viewer.seesAll ? undefined : viewer.agentId };
1170
+ const doc = memoryId
1171
+ ? await getMemoryChunks({ memoryId }, visibility)
1172
+ : await getMemoryChunks({ key: key as string, scope, agentId }, visibility);
1173
+ if (!doc) {
1174
+ jsonError(res, "Memory not found", 404);
1175
+ return true;
1176
+ }
1177
+ const chunks = doc.chunks.map((c) => ({
1178
+ id: c.id,
1179
+ agentId: c.agentId,
1180
+ scope: AgentMemoryScopeSchema.parse(c.scope),
1181
+ key: c.key,
1182
+ name: c.name,
1183
+ content: c.content,
1184
+ source: AgentMemorySourceSchema.parse(c.source),
1185
+ sourceTaskId: c.sourceTaskId,
1186
+ sourcePath: c.sourcePath,
1187
+ chunkIndex: c.chunkIndex,
1188
+ totalChunks: c.totalChunks,
1189
+ tags: c.tags,
1190
+ createdAt: c.createdAt,
1191
+ updatedAt: c.updatedAt ?? null,
1192
+ accessedAt: c.accessedAt,
1193
+ expiresAt: c.expiresAt ?? null,
1194
+ accessCount: c.accessCount ?? 0,
1195
+ embeddingModel: c.embeddingModel ?? null,
1196
+ rating: posteriorMean(c.alpha, c.beta),
1197
+ alpha: c.alpha,
1198
+ beta: c.beta,
1199
+ version: c.version ?? 1,
1200
+ estTokens: estimateTokens(c.content.length),
1201
+ }));
1202
+ getMemoryChunksRoute.respond(res, 200, {
1203
+ key: doc.key,
1204
+ scope: AgentMemoryScopeSchema.parse(doc.scope),
1205
+ agentId: doc.agentId,
1206
+ chunks,
1207
+ estTokens: estimateTokens(chunks.reduce((sum, c) => sum + c.content.length, 0)),
1208
+ integrity: checkChunkIntegrity(chunks),
1209
+ });
1210
+ return true;
1211
+ }
1212
+
959
1213
  if (editMemory.match(req.method, pathSegments)) {
960
1214
  if (!myAgentId) {
961
1215
  jsonError(res, "Missing X-Agent-ID header", 400);
@@ -1062,6 +1316,21 @@ export async function handleMemory(
1062
1316
  if (!parsed) return true;
1063
1317
 
1064
1318
  const store = getMemoryStore();
1319
+ const memory = await store.peek(parsed.params.id);
1320
+ if (!memory) {
1321
+ jsonError(res, "Memory not found", 404);
1322
+ return true;
1323
+ }
1324
+ const decision = can({
1325
+ principal: await ingestPrincipal(req, myAgentId),
1326
+ verb: "memory.delete.any",
1327
+ resource: { kind: "owned", ownerAgentId: memory.agentId, scope: memory.scope },
1328
+ source: "http",
1329
+ });
1330
+ if (!decision.allow) {
1331
+ jsonError(res, `Forbidden: ${decision.reason}`, 403);
1332
+ return true;
1333
+ }
1065
1334
  const deleted = await store.delete(parsed.params.id);
1066
1335
  if (!deleted) {
1067
1336
  jsonError(res, "Memory not found", 404);
package/src/http/repos.ts CHANGED
@@ -1,14 +1,12 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  import { z } from "zod";
3
- import {
4
- createSwarmRepo,
5
- deleteSwarmRepo,
6
- getSwarmRepoById,
7
- getSwarmRepos,
8
- updateSwarmRepo,
9
- } from "../be/db";
3
+ import { createSwarmRepo, deleteSwarmRepo, getSwarmRepoById, getSwarmRepos } from "../be/db";
4
+ import { updateSwarmRepoChecked } from "../be/repo-checked-update";
5
+ import { changesAllowMerge } from "../be/repo-merge-policy";
6
+ import { can } from "../rbac";
10
7
  import { emitIntegrationConnected } from "../telemetry";
11
8
  import { RepoGuidelinesInputSchema, RepoHooksSchema, SwarmRepoSchema } from "../types";
9
+ import { agentFirstPrincipal } from "./request-principal";
12
10
  import { route } from "./route-def";
13
11
  import { json, jsonError } from "./utils";
14
12
 
@@ -63,8 +61,13 @@ const createRepo = route({
63
61
  responses: {
64
62
  201: { description: "Repo created", schema: SwarmRepoSchema },
65
63
  400: { description: "Validation error", schema: z.object({ error: z.string() }) },
64
+ 403: {
65
+ description: "Only the lead, the operator or a user can turn allowMerge on",
66
+ schema: z.object({ error: z.string() }),
67
+ },
66
68
  409: { description: "Duplicate repo", schema: z.object({ error: z.string() }) },
67
69
  },
70
+ rbac: { permission: "repo.merge-policy.write" },
68
71
  });
69
72
 
70
73
  const updateRepo = route({
@@ -85,9 +88,14 @@ const updateRepo = route({
85
88
  }),
86
89
  responses: {
87
90
  200: { description: "Repo updated", schema: SwarmRepoSchema },
91
+ 403: {
92
+ description: "Only the lead, the operator or a user can change allowMerge",
93
+ schema: z.object({ error: z.string() }),
94
+ },
88
95
  404: { description: "Repo not found", schema: z.object({ error: z.string() }) },
89
96
  409: { description: "Duplicate repo", schema: z.object({ error: z.string() }) },
90
97
  },
98
+ rbac: { permission: "repo.merge-policy.write" },
91
99
  });
92
100
 
93
101
  const deleteRepo = route({
@@ -105,11 +113,26 @@ const deleteRepo = route({
105
113
 
106
114
  // ─── Handler ─────────────────────────────────────────────────────────────────
107
115
 
116
+ /** The refusal text when the caller may not change allowMerge, or null when they may. */
117
+ async function allowMergeRefusal(
118
+ req: IncomingMessage,
119
+ myAgentId: string | undefined,
120
+ ): Promise<string | null> {
121
+ const decision = can({
122
+ principal: await agentFirstPrincipal(req, myAgentId),
123
+ verb: "repo.merge-policy.write",
124
+ resource: { kind: "none" },
125
+ source: "http",
126
+ });
127
+ return decision.allow ? null : `Forbidden: ${decision.reason}`;
128
+ }
129
+
108
130
  export async function handleRepos(
109
131
  req: IncomingMessage,
110
132
  res: ServerResponse,
111
133
  pathSegments: string[],
112
134
  queryParams: URLSearchParams,
135
+ myAgentId: string | undefined,
113
136
  ): Promise<boolean> {
114
137
  if (getRepo.match(req.method, pathSegments)) {
115
138
  const parsed = await getRepo.parse(req, res, pathSegments, queryParams);
@@ -137,6 +160,13 @@ export async function handleRepos(
137
160
  if (createRepo.match(req.method, pathSegments)) {
138
161
  const parsed = await createRepo.parse(req, res, pathSegments, queryParams);
139
162
  if (!parsed) return true;
163
+ if (changesAllowMerge(null, parsed.body.guidelines)) {
164
+ const refusal = await allowMergeRefusal(req, myAgentId);
165
+ if (refusal) {
166
+ jsonError(res, refusal, 403);
167
+ return true;
168
+ }
169
+ }
140
170
  try {
141
171
  const repo = await createSwarmRepo({
142
172
  url: parsed.body.url,
@@ -168,20 +198,30 @@ export async function handleRepos(
168
198
  const parsed = await updateRepo.parse(req, res, pathSegments, queryParams);
169
199
  if (!parsed) return true;
170
200
  try {
171
- const updated = await updateSwarmRepo(parsed.params.id, {
172
- url: parsed.body.url,
173
- name: parsed.body.name,
174
- clonePath: parsed.body.clonePath,
175
- defaultBranch: parsed.body.defaultBranch,
176
- autoClone: parsed.body.autoClone,
177
- hooks: parsed.body.hooks,
178
- guidelines: parsed.body.guidelines,
179
- });
180
- if (!updated) {
201
+ // The comparison with the stored allowMerge, the decision and the write share one
202
+ // transaction, so a stale edit cannot overwrite a merge-policy change committed after it.
203
+ const result = await updateSwarmRepoChecked(
204
+ parsed.params.id,
205
+ {
206
+ url: parsed.body.url,
207
+ name: parsed.body.name,
208
+ clonePath: parsed.body.clonePath,
209
+ defaultBranch: parsed.body.defaultBranch,
210
+ autoClone: parsed.body.autoClone,
211
+ hooks: parsed.body.hooks,
212
+ guidelines: parsed.body.guidelines,
213
+ },
214
+ () => allowMergeRefusal(req, myAgentId),
215
+ );
216
+ if (result.kind === "refused") {
217
+ jsonError(res, result.refusal, 403);
218
+ return true;
219
+ }
220
+ if (result.kind === "not-found") {
181
221
  jsonError(res, "Repo not found", 404);
182
222
  return true;
183
223
  }
184
- json(res, updated);
224
+ json(res, result.repo);
185
225
  } catch (error) {
186
226
  const msg = (error as Error).message;
187
227
  if (msg.includes("UNIQUE constraint")) {
@@ -20,3 +20,22 @@ export async function requestPrincipal(
20
20
  const agent = await getAgentById(myAgentId);
21
21
  return { kind: "agent", agentId: myAgentId, isLead: agent?.isLead ?? false };
22
22
  }
23
+
24
+ /**
25
+ * The principal of a keyed HTTP request that may act as an agent. Workers share the swarm API
26
+ * key, so the key alone authenticates as the operator. An X-Agent-ID, or an `aseph_` session
27
+ * token, names the agent on top of it, and its lead flag is read live. A user token stays the
28
+ * user. A request with no agent identity is the operator, as for the dashboard and the runner.
29
+ * Unlike `requestPrincipal`, an agent identity wins over the operator key.
30
+ */
31
+ export async function agentFirstPrincipal(
32
+ req: IncomingMessage,
33
+ myAgentId: string | undefined,
34
+ ): Promise<RbacPrincipal> {
35
+ const auth = getRequestAuth(req);
36
+ if (auth?.kind === "user") return { kind: "user", userId: auth.userId };
37
+ const agentId = auth?.kind === "agent" ? auth.agentId : myAgentId;
38
+ if (!agentId) return { kind: "operator" };
39
+ const agent = await getAgentById(agentId);
40
+ return { kind: "agent", agentId, isLead: agent?.isLead === true };
41
+ }
@@ -5,6 +5,7 @@ import type {
5
5
  SessionCostModelBreakdown,
6
6
  SessionCostSource,
7
7
  } from "../types";
8
+ import { AMP_ESTIMATE_MODELS, ampBillsCacheWrites, resolveAmpModel } from "../utils/amp-models";
8
9
 
9
10
  const PER_MILLION = 1_000_000;
10
11
 
@@ -121,6 +122,52 @@ function unpricedResult(
121
122
  };
122
123
  }
123
124
 
125
+ /**
126
+ * An amp row with no per-model usage means the thread export failed, and the
127
+ * adapter sent the stream's token totals under the requested mode or pin.
128
+ * Recording $0 would let the session skip budget admission, so the totals are
129
+ * priced at the pin, or at the model the mode is known to run, falling back to
130
+ * the `medium` model when the pin has no rates. The row is tagged `estimated`.
131
+ */
132
+ async function estimateAmpCost(
133
+ input: SessionCostRecomputeInput,
134
+ lookupRate: PricingRateLookup,
135
+ ): Promise<SessionCostRecomputeResult> {
136
+ let selection: ReturnType<typeof resolveAmpModel> | undefined;
137
+ try {
138
+ selection = resolveAmpModel(input.model);
139
+ } catch {
140
+ selection = undefined;
141
+ }
142
+ const candidates = [
143
+ ...new Set([
144
+ selection?.pin,
145
+ selection ? AMP_ESTIMATE_MODELS[selection.baseMode] : undefined,
146
+ AMP_ESTIMATE_MODELS.medium,
147
+ ]),
148
+ ].filter((model): model is string => !!model);
149
+ for (const model of candidates) {
150
+ // Same rule as the export parser: only Anthropic bills cache creation as writes.
151
+ const writes = ampBillsCacheWrites(model);
152
+ const costUsd = await priceModel(
153
+ "amp",
154
+ {
155
+ model,
156
+ inputTokens: input.inputTokens + (writes ? 0 : input.cacheWriteTokens),
157
+ outputTokens: input.outputTokens,
158
+ cacheReadTokens: input.cacheReadTokens,
159
+ cacheWriteTokens: writes ? input.cacheWriteTokens : 0,
160
+ },
161
+ null,
162
+ input.atEpochMs,
163
+ lookupRate,
164
+ );
165
+ if (costUsd != null)
166
+ return { totalCostUsd: costUsd, costSource: "estimated", modelBreakdown: undefined };
167
+ }
168
+ return unpricedResult(input, undefined);
169
+ }
170
+
124
171
  export async function recomputeSessionCost(
125
172
  input: SessionCostRecomputeInput,
126
173
  lookupRate: PricingRateLookup,
@@ -135,6 +182,23 @@ export async function recomputeSessionCost(
135
182
  };
136
183
  }
137
184
 
185
+ // Amp's own recorded cost (`amp threads usage`) is what it billed, including
186
+ // subagent threads the token counts miss. The adapter sends it only when
187
+ // every request was billed through Amp, so it wins over the token price,
188
+ // which still fills the per-model breakdown.
189
+ if (input.provider === "amp" && input.harnessCostUsd > 0) {
190
+ const priced = await recomputeSessionCost({ ...input, harnessCostUsd: 0 }, lookupRate);
191
+ return {
192
+ totalCostUsd: input.harnessCostUsd,
193
+ costSource: "harness",
194
+ modelBreakdown: priced.modelBreakdown,
195
+ };
196
+ }
197
+
198
+ // amp's top-level model is the mode or pin it was asked for; only the thread
199
+ // export's per-model usage says what ran and how its cache tokens bill.
200
+ if (input.provider === "amp" && !modelUsageEntries) return estimateAmpCost(input, lookupRate);
201
+
138
202
  const split = cacheWriteSplit(input);
139
203
  const usages: SessionCostModelUsageInput[] = modelUsageEntries
140
204
  ? modelUsageEntries
@@ -13,6 +13,7 @@ import {
13
13
  getSessionCostsFiltered,
14
14
  getSessionLogsByTaskId,
15
15
  getTaskById,
16
+ refreshActiveSessionOnActivity,
16
17
  } from "../be/db";
17
18
  import { recordSessionCost } from "../otel";
18
19
  import { incrementServerSessionsProcessed } from "../server-runtime-counters";
@@ -319,6 +320,16 @@ export async function handleSessionData(
319
320
  cli: parsed.body.cli || "claude",
320
321
  lines: parsed.body.lines,
321
322
  });
323
+ // Every harness streams its output through this endpoint, so a task that
324
+ // is producing output keeps its session alive here instead of per
325
+ // provider. Best-effort: a liveness write must never fail log storage.
326
+ if (parsed.body.taskId) {
327
+ try {
328
+ await refreshActiveSessionOnActivity(parsed.body.taskId);
329
+ } catch (error) {
330
+ console.warn("[HTTP] Failed to refresh session liveness from session logs:", error);
331
+ }
332
+ }
322
333
  createSessionLogsRoute.respond(res, 201, {
323
334
  success: true,
324
335
  count: parsed.body.lines.length,
package/src/http/tasks.ts CHANGED
@@ -888,7 +888,15 @@ export async function handleTasks(
888
888
  let requestedByUserId = trustedUserId ?? undefined;
889
889
  const trustBodyRequestedByUserId = process.env.TRUST_BODY_REQUESTED_BY_USER_ID !== "false";
890
890
  if (trustBodyRequestedByUserId && !requestedByUserId && parsed.body.requestedByUserId) {
891
- const candidate = await findUserById(parsed.body.requestedByUserId);
891
+ // A worker on the shared key is not the operator: its X-Agent-ID names it, so the body
892
+ // hint is dropped like it is when the worker has a requester of its own.
893
+ const mayAssign = can({
894
+ principal: await resolveTaskWritePrincipal(req, myAgentId),
895
+ verb: "task.requester.assign",
896
+ resource: { kind: "none" },
897
+ source: "http",
898
+ }).allow;
899
+ const candidate = mayAssign ? await findUserById(parsed.body.requestedByUserId) : null;
892
900
  if (candidate) requestedByUserId = candidate.id;
893
901
  }
894
902