@desplega.ai/agent-swarm 2.4.3 → 2.4.5

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 (270) hide show
  1. package/dist/{acp-adapter-1885dpr7.js → acp-adapter-2wdkktv6.js} +1 -2
  2. package/dist/{actions-pn76n32q.js → actions-c40ebcm4.js} +18 -18
  3. package/dist/{amp-adapter-af84rx1d.js → amp-adapter-wb67m95z.js} +2 -2
  4. package/dist/{app-6sv8mhzm.js → app-353vre8g.js} +3 -3
  5. package/dist/{assistant-sc2tf825.js → assistant-sj1njh23.js} +21 -21
  6. package/dist/{boot-reembed-y3m0ydzj.js → boot-reembed-b9c3az3c.js} +1 -1
  7. package/dist/{boot-reembed-gvxjs2py.js → boot-reembed-qdkvxeem.js} +1 -1
  8. package/dist/{boot-scrub-sweep-1j9mk05f.js → boot-scrub-sweep-m3wh8eks.js} +2 -2
  9. package/dist/{boot-seeds-37sp626b.js → boot-seeds-j2273s54.js} +4 -4
  10. package/dist/{claude-adapter-rp0jtnwr.js → claude-adapter-m7q0a085.js} +2 -2
  11. package/dist/{claude-managed-adapter-h2y889ge.js → claude-managed-adapter-5ptb6tzs.js} +2 -2
  12. package/dist/{claude-managed-setup-rdt248k7.js → claude-managed-setup-smrqk5j5.js} +3 -3
  13. package/dist/{claude-sdk-session-53rcw5we.js → claude-sdk-session-38xnz21e.js} +8284 -8267
  14. package/dist/{cli-6eyncg0y.js → cli-018r4jq6.js} +34 -6
  15. package/dist/{cli-sds5zzs8.js → cli-1f3qzw6y.js} +78 -12
  16. package/dist/{cli-f0p0f6ja.js → cli-1sp861s4.js} +7 -7
  17. package/dist/{cli-dm8p2dpf.js → cli-1syn2xy1.js} +10 -10
  18. package/dist/{cli-tkf4x6ha.js → cli-37j04k05.js} +11 -11
  19. package/dist/{cli-v87dr6e3.js → cli-3a45zcgm.js} +1 -1
  20. package/dist/{cli-1n9gdg56.js → cli-43x64hv3.js} +4 -4
  21. package/dist/{cli-p0qerd2r.js → cli-4zfqgjc5.js} +2 -2
  22. package/dist/{cli-aa3tb29r.js → cli-7c9qs530.js} +1 -1
  23. package/dist/{cli-9qtjfd60.js → cli-848hxdwj.js} +1 -1
  24. package/dist/{cli-1xsraagq.js → cli-94gwyg7m.js} +8 -8
  25. package/dist/{cli-0dxxwfkt.js → cli-9hstj09h.js} +17 -14
  26. package/dist/{cli-1012xb7j.js → cli-9yqgm9nd.js} +22 -22
  27. package/dist/{cli-pygdsgh8.js → cli-a92r1127.js} +1 -1
  28. package/dist/{cli-8vverzf7.js → cli-ba8ne1es.js} +5 -0
  29. package/dist/{cli-bxrqvpg5.js → cli-be2161h1.js} +7 -7
  30. package/dist/{cli-npfe9t1s.js → cli-bnes4z6b.js} +2 -2
  31. package/dist/{cli-aypbg9r2.js → cli-cxxe61f6.js} +1 -1
  32. package/dist/{cli-vq9gqarq.js → cli-dk44dxec.js} +1 -1
  33. package/dist/{cli-836ehxmt.js → cli-eww120g8.js} +3 -2
  34. package/dist/{cli-a0kcty51.js → cli-f01h25yt.js} +1 -1
  35. package/dist/{cli-mzkwkbdy.js → cli-f0g3pgvf.js} +1 -1
  36. package/dist/{cli-1ma335fz.js → cli-ftc1g5tj.js} +2 -2
  37. package/dist/{cli-4yef8ghv.js → cli-fv5hepyy.js} +3 -3
  38. package/dist/{cli-58vwn100.js → cli-g35m2xwm.js} +4 -4
  39. package/dist/{cli-qje9k2nh.js → cli-g9ypd5en.js} +12 -2
  40. package/dist/{cli-gx0j2khs.js → cli-j99azg3g.js} +3 -3
  41. package/dist/{cli-er83cjbn.js → cli-k8g16gz3.js} +416 -116
  42. package/dist/{cli-qp2m4btv.js → cli-kpw04va0.js} +2 -2
  43. package/dist/{cli-8y99kfey.js → cli-ms85309d.js} +33 -15
  44. package/dist/{cli-kgnkyabk.js → cli-n72rg53p.js} +1555 -645
  45. package/dist/{cli-e4d78xsv.js → cli-p14m2cnp.js} +1 -1
  46. package/dist/{cli-1eszr343.js → cli-qfxc84gh.js} +1 -1
  47. package/dist/{cli-wk28hzqn.js → cli-qwhy2qma.js} +1 -1
  48. package/dist/{cli-73ezfhha.js → cli-st8aw1m6.js} +228 -11
  49. package/dist/{cli-zvbb4n0e.js → cli-tw4p90jb.js} +92 -192
  50. package/dist/{cli-038fga5k.js → cli-v0yrfmdh.js} +148 -183
  51. package/dist/{cli-6aen3ezb.js → cli-v8fjakwf.js} +1 -1
  52. package/dist/{cli-99jez72r.js → cli-w9etnnxk.js} +6 -1
  53. package/dist/{cli-vcgscaan.js → cli-wt0jf9hm.js} +217 -80
  54. package/dist/{cli-s9rr4gr2.js → cli-xpa64rek.js} +1065 -305
  55. package/dist/{cli-h0mgxann.js → cli-z7d29rw9.js} +2 -2
  56. package/dist/cli.js +45 -20
  57. package/dist/{codex-adapter-2zrmab9b.js → codex-adapter-tn6k5mkg.js} +1 -1
  58. package/dist/{codex-session-runner-kbkz62vc.js → codex-session-runner-7vjt7jbw.js} +1 -1
  59. package/dist/{commands-hd574th6.js → commands-t7rz9146.js} +2 -2
  60. package/dist/{controller-pz5mcfg4.js → controller-m2jasma5.js} +2 -2
  61. package/dist/{controllers-connect-5rgq2hfk.js → controllers-connect-9ftpex99.js} +19 -19
  62. package/dist/{cursor-adapter-mgaprq0j.js → cursor-adapter-mjv79e4z.js} +1 -1
  63. package/dist/{db-6nf1207q.js → db-6sfrn9g3.js} +1 -1
  64. package/dist/{db-h06dxp05.js → db-rn673r06.js} +3 -3
  65. package/dist/{dispatcher-zthpy4m1.js → dispatcher-c155xz0p.js} +17 -17
  66. package/dist/{dsh-adapter-64dhw0j7.js → dsh-adapter-3mmszc1x.js} +1 -1
  67. package/dist/{external-channel-x01t7ke6.js → external-channel-35k6tkv8.js} +2 -2
  68. package/dist/{grok-adapter-h21jv594.js → grok-adapter-tfrq7tg1.js} +2 -3
  69. package/dist/handlers-7gw64dxw.js +39 -0
  70. package/dist/{heartbeat-zzfksckz.js → heartbeat-dyg4npkp.js} +20 -20
  71. package/dist/{hook-z4zhzj1f.js → hook-drspbwpe.js} +1 -1
  72. package/dist/{hook-3xfqhh2s.js → hook-x26b31bg.js} +1 -1
  73. package/dist/{http-q813tmya.js → http-pk972xyw.js} +571 -294
  74. package/dist/{index-va3y63es.js → index-6srwvd5b.js} +1 -2
  75. package/dist/{index-x8xbyn1t.js → index-arytgj5q.js} +1 -1
  76. package/dist/{index-wbbs14c5.js → index-bgdyvcnv.js} +17 -17
  77. package/dist/{index-s98037v0.js → index-cbxkmfc5.js} +1 -2
  78. package/dist/{index-c4ykd0f1.js → index-dr94zbnh.js} +13348 -11611
  79. package/dist/{index-m0a4ydqj.js → index-gabbcdn2.js} +17 -17
  80. package/dist/{index-220tk9bw.js → index-jwxkkex2.js} +139 -76
  81. package/dist/index-tfb26zhy.js +41 -0
  82. package/dist/{keepalive-y5t1my7m.js → keepalive-7yrng7px.js} +3 -3
  83. package/dist/{lead-2xwtq66b.js → lead-7e02z839.js} +29 -30
  84. package/dist/{maintenance-ybfsaedv.js → maintenance-e6cemdqt.js} +1 -1
  85. package/dist/{node-3v3f08f7.js → node-de6akczr.js} +10 -9
  86. package/dist/{oauth-refresh-sweep-909jmn12.js → oauth-refresh-sweep-yw3xdbcj.js} +2 -2
  87. package/dist/{onboard-xn6fkkys.js → onboard-0a9y5yrc.js} +2 -2
  88. package/dist/{otel-impl-e6mn0z13.js → otel-impl-trjpkfb7.js} +1 -1
  89. package/dist/{pricing-refresh-ygd362ep.js → pricing-refresh-d2b5my0y.js} +3 -3
  90. package/dist/{rbac-roles-mb470p4e.js → rbac-roles-hey3yv9d.js} +2 -2
  91. package/dist/{rbac-roles-c9axvfnz.js → rbac-roles-nxh5ahwk.js} +2 -2
  92. package/dist/{registry-rxw10xzg.js → registry-ajhexe44.js} +1 -1
  93. package/dist/{render-v2-g0306nh1.js → render-v2-rr3p2atg.js} +3 -3
  94. package/dist/{resume-v315hnq6.js → resume-2efkf1s1.js} +5 -5
  95. package/dist/{session-access-2zg285a1.js → session-access-s2mttxpw.js} +6 -4
  96. package/dist/{setup-a4mtf02k.js → setup-vypma5yt.js} +1 -1
  97. package/dist/{work-objects-9fr0fgtn.js → work-objects-rvhckdkp.js} +20 -20
  98. package/dist/{worker-qae22jeb.js → worker-5snytpbd.js} +29 -30
  99. package/dist/worker-token-xf11yyds.js +136 -0
  100. package/openapi.json +303 -13
  101. package/package.json +8 -8
  102. package/src/apps/action-runner.ts +4 -0
  103. package/src/apps/catalog.generated.json +209 -4
  104. package/src/apps/page-validator.ts +21 -3
  105. package/src/be/chunking.ts +42 -9
  106. package/src/be/db/tasks/draft-text.ts +15 -0
  107. package/src/be/db/tasks/read.ts +14 -1
  108. package/src/be/db.ts +5 -1
  109. package/src/be/memory/index-content.ts +36 -17
  110. package/src/be/memory/providers/openai-embedding.ts +115 -61
  111. package/src/be/memory/providers/sqlite-store.ts +69 -23
  112. package/src/be/memory/reranker.ts +31 -1
  113. package/src/be/memory/search-candidates.ts +47 -0
  114. package/src/be/memory/types.ts +14 -0
  115. package/src/be/migrations/232_agent_session_tokens.sql +41 -0
  116. package/src/be/scripts/typecheck.ts +2 -0
  117. package/src/be/seed-scripts/catalog/heartbeat-jev-gate.ts +116 -55
  118. package/src/be/seed-scripts/index.ts +2 -2
  119. package/src/be/swarm-config-guard.ts +40 -1
  120. package/src/be/task-attachment-store.ts +30 -0
  121. package/src/be/users.ts +169 -12
  122. package/src/cli.tsx +24 -0
  123. package/src/commands/worker-token.ts +164 -0
  124. package/src/extensions/catalog.generated.json +4 -4
  125. package/src/extensions/contract-types.generated.ts +1 -1
  126. package/src/extensions/contract.ts +7 -0
  127. package/src/fs/provider.ts +13 -1
  128. package/src/heartbeat/heartbeat.ts +31 -11
  129. package/src/http/apps.ts +43 -5
  130. package/src/http/codex-oauth-keep-warm.ts +27 -9
  131. package/src/http/config.ts +5 -3
  132. package/src/http/core.ts +2 -0
  133. package/src/http/fs.ts +54 -34
  134. package/src/http/mcp-body.ts +37 -0
  135. package/src/http/mcp-user.ts +3 -5
  136. package/src/http/mcp.ts +6 -7
  137. package/src/http/memory.ts +26 -15
  138. package/src/http/page-proxy.ts +61 -5
  139. package/src/http/pages-public.ts +27 -44
  140. package/src/http/pages.ts +4 -4
  141. package/src/http/script-runs.ts +2 -0
  142. package/src/http/session-access.ts +51 -2
  143. package/src/http/sessions.ts +249 -10
  144. package/src/http/tasks.ts +4 -32
  145. package/src/http/users.ts +12 -1
  146. package/src/integrations/chat-sdk/adapters.ts +7 -3
  147. package/src/integrations/chat-sdk/attachments.ts +90 -12
  148. package/src/integrations/chat-sdk/facts.ts +120 -0
  149. package/src/integrations/chat-sdk/inbound.ts +156 -8
  150. package/src/integrations/chat-sdk/limits.ts +24 -0
  151. package/src/integrations/chat-sdk/outbound-files.ts +186 -0
  152. package/src/integrations/chat-sdk/outbound.ts +234 -4
  153. package/src/integrations/chat-sdk/relay.ts +4 -1
  154. package/src/integrations/chat-sdk/router.ts +28 -2
  155. package/src/integrations/chat-sdk/steering.ts +9 -0
  156. package/src/integrations/chat-sdk/templates.ts +14 -0
  157. package/src/integrations/chat-sdk/thread-buffer.ts +4 -0
  158. package/src/mcp-limits.ts +20 -0
  159. package/src/pages/browser-sdk.ts +8 -3
  160. package/src/pages/feedback-overlay.ts +1 -1
  161. package/src/providers/acp-adapter.ts +33 -13
  162. package/src/providers/acp-swarm-events.ts +1 -1
  163. package/src/providers/amp-adapter.ts +1 -1
  164. package/src/providers/codex-adapter.ts +106 -119
  165. package/src/providers/codex-agents-md.ts +35 -229
  166. package/src/providers/codex-oauth/storage.ts +1 -1
  167. package/src/providers/cursor-adapter.ts +1 -1
  168. package/src/rbac/legacy-policy.ts +3 -1
  169. package/src/rbac/permissions.ts +4 -0
  170. package/src/realtime/transport.ts +1 -2
  171. package/src/script-workflows/workflow-ctx.ts +1 -0
  172. package/src/scripts-runtime/types/stdlib.d.ts +2 -0
  173. package/src/scripts-runtime/types/swarm-extension.d.ts +8 -0
  174. package/src/scripts-runtime/types/swarm-sdk.d.ts +2 -0
  175. package/src/server-user.ts +2 -0
  176. package/src/server.ts +2 -0
  177. package/src/slack/assistant.ts +2 -2
  178. package/src/slack/files.ts +2 -0
  179. package/src/slack/handlers.ts +4 -4
  180. package/src/slack/inbound-files.ts +39 -6
  181. package/src/slack/thread-buffer.ts +2 -1
  182. package/src/tasks/task-terminal-effects.ts +21 -17
  183. package/src/tests/acp-adapter.test.ts +10 -23
  184. package/src/tests/acp-fake-agent.ts +36 -0
  185. package/src/tests/acp-swarm-events.test.ts +1 -1
  186. package/src/tests/agent-session-tokens.test.ts +452 -0
  187. package/src/tests/apps-action-parent-task.test.ts +295 -0
  188. package/src/tests/apps-card-grid.test.ts +197 -0
  189. package/src/tests/apps-catalog-additions.test.ts +120 -0
  190. package/src/tests/apps-tree.test.ts +129 -0
  191. package/src/tests/chat-sdk-attachments-reply.test.ts +52 -0
  192. package/src/tests/chat-sdk-config.test.ts +11 -0
  193. package/src/tests/chat-sdk-facts.test.ts +23 -0
  194. package/src/tests/chat-sdk-inbound.test.ts +558 -4
  195. package/src/tests/chat-sdk-outbound-files.test.ts +652 -0
  196. package/src/tests/codex-adapter.test.ts +167 -242
  197. package/src/tests/codex-oauth-keep-warm.test.ts +85 -1
  198. package/src/tests/config-api-only-keys.test.ts +9 -0
  199. package/src/tests/config-session-auth.test.ts +14 -0
  200. package/src/tests/cors-allowlist.test.ts +1 -1
  201. package/src/tests/docs-index-footprint.test.js +2 -2
  202. package/src/tests/extensions-example-slack-reply-gate.test.ts +262 -91
  203. package/src/tests/grok-adapter.test.ts +3 -3
  204. package/src/tests/heartbeat-checklist.test.ts +33 -0
  205. package/src/tests/heartbeat-jev-gate-credentials.test.ts +44 -0
  206. package/src/tests/heartbeat-jev-gate.test.ts +265 -1
  207. package/src/tests/kv-page-proxy.test.ts +91 -5
  208. package/src/tests/launch-password-rejection.test.ts +2 -2
  209. package/src/tests/mcp-input-caps.test.ts +294 -0
  210. package/src/tests/memory-chunked-search-starvation.test.ts +200 -0
  211. package/src/tests/memory-embedding-input-cap.test.ts +114 -0
  212. package/src/tests/memory.test.ts +14 -0
  213. package/src/tests/migration-232-agent-session-tokens.test.ts +67 -0
  214. package/src/tests/page-guest-session.test.ts +19 -8
  215. package/src/tests/page-proxy-authed.test.ts +1 -1
  216. package/src/tests/page-proxy.test.ts +17 -10
  217. package/src/tests/page-session.test.ts +102 -1
  218. package/src/tests/pages-authed-mode.test.ts +82 -12
  219. package/src/tests/pages-password-mode.test.ts +5 -5
  220. package/src/tests/rbac-engine.test.ts +1 -0
  221. package/src/tests/realtime-transport.test.ts +13 -7
  222. package/src/tests/runtime-child-token-cleanup.test.ts +3 -3
  223. package/src/tests/script-connections.test.ts +2 -0
  224. package/src/tests/script-runs-http.test.ts +84 -0
  225. package/src/tests/scripts-http.test.ts +10 -1
  226. package/src/tests/session-route-posture.test.ts +2 -0
  227. package/src/tests/swarm-config-export-to-env.test.ts +22 -3
  228. package/src/tests/task-audio-transcription.test.ts +299 -0
  229. package/src/tests/task-completion-memory-chunking.test.ts +434 -0
  230. package/src/tests/task-terminal-secret-scrub.test.ts +6 -1
  231. package/src/tests/transcription.test.ts +299 -0
  232. package/src/tests/worker-token-cli.test.ts +76 -0
  233. package/src/tests/workflow-executors.test.ts +4 -2
  234. package/src/tests/workflow-system-one-decision-providers.test.ts +98 -1
  235. package/src/tests/workflow-system-one-decision.test.ts +2 -1
  236. package/src/tests/workflow-system-one-extraction.test.ts +805 -0
  237. package/src/tools/chat-access.ts +10 -0
  238. package/src/tools/chat-post.ts +31 -11
  239. package/src/tools/chat-reply.ts +32 -10
  240. package/src/tools/memory-search.ts +3 -1
  241. package/src/tools/system-one-decide.ts +17 -4
  242. package/src/types.ts +8 -1
  243. package/src/utils/config-env-export.ts +3 -1
  244. package/src/utils/page-session.ts +133 -20
  245. package/src/utils/request-auth-context.ts +2 -1
  246. package/src/utils/transcription.ts +246 -0
  247. package/src/workflows/definition.ts +7 -1
  248. package/src/workflows/engine.ts +18 -12
  249. package/src/workflows/executors/index.ts +1 -0
  250. package/src/workflows/executors/registry.ts +2 -0
  251. package/src/workflows/executors/system-one-decision.ts +84 -262
  252. package/src/workflows/executors/system-one-extract-wire.ts +57 -0
  253. package/src/workflows/executors/system-one-extraction.ts +830 -0
  254. package/src/workflows/executors/system-one-providers.ts +316 -114
  255. package/src/workflows/executors/system-one-transport.ts +264 -0
  256. package/templates/extensions/slack-reply-gate/README.md +24 -22
  257. package/templates/extensions/slack-reply-gate/hooks.ts +146 -95
  258. package/templates/extensions/slack-reply-gate/manifest.json +2 -2
  259. package/templates/official/lead/HEARTBEAT.md +1 -1
  260. package/templates/skills/apps/SKILL.md +31 -7
  261. package/templates/skills/apps/content.md +31 -7
  262. package/templates/skills/chat-sdk/content.md +6 -2
  263. package/templates/skills/code-quality/content.md +2 -1
  264. package/templates/skills/heartbeat-runbook/content.md +8 -8
  265. package/templates/skills/pages/content.md +6 -4
  266. package/templates/skills/swarm-scripts/SKILL.md +2 -2
  267. package/templates/skills/swarm-scripts/content.md +2 -2
  268. package/dist/cli-v1sjhn52.js +0 -7
  269. package/dist/handlers-2qjw434w.js +0 -39
  270. package/dist/index-d0g18bax.js +0 -41
@@ -1,4 +1,4 @@
1
- import type { IncomingMessage } from "node:http";
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  import { getSteeringMessageById } from "../be/db";
3
3
  import { getActiveSessionById } from "../be/db/active-sessions";
4
4
  import { getUserGrant } from "../be/rbac-roles";
@@ -10,10 +10,12 @@ import {
10
10
  sessionAccessForRoot,
11
11
  } from "../be/session-shares";
12
12
  import { findUserById } from "../be/users";
13
- import { isRbacEnabled } from "../rbac";
13
+ import { can, isRbacEnabled, type RbacPrincipal } from "../rbac";
14
+ import type { AgentTask } from "../types";
14
15
  import { getRequestAuth } from "../utils/request-auth-context";
15
16
  import { dashboardUserIdHeader } from "./favorite-owner";
16
17
  import { type RouteDef, routePathParams } from "./route-def";
18
+ import { jsonError } from "./utils";
17
19
 
18
20
  /**
19
21
  * Who a request acts for when it touches a session.
@@ -76,6 +78,53 @@ export function readableByFor(actor: SessionActor): { userId: string | null } |
76
78
  export const SESSION_WRITE_DENIED =
77
79
  "You can't write in this session. Ask the owner to share it with edit access.";
78
80
 
81
+ /** True unless the request acts for a user who lacks write access to the task's session. */
82
+ export async function hasSessionWrite(
83
+ req: IncomingMessage,
84
+ myAgentId: string | undefined,
85
+ taskId: string,
86
+ ): Promise<boolean> {
87
+ const access = await sessionAccessFor(await resolveSessionActor(req, myAgentId), taskId);
88
+ return hasSessionAccess(access, "write");
89
+ }
90
+
91
+ /**
92
+ * The parent gate shared by `POST /api/tasks` and `POST /api/apps/{id}/actions/{name}`.
93
+ * The child inherits the parent's thread context, so an agent caller must own or
94
+ * have created the parent (`task.parent.attach`; operator and user callers keep
95
+ * their access), and a follow-up in someone else's session needs a write share.
96
+ * Writes the 403 and returns true when denied.
97
+ */
98
+ export async function denyParentAttach(
99
+ req: IncomingMessage,
100
+ res: ServerResponse,
101
+ myAgentId: string | undefined,
102
+ parentTask: Pick<AgentTask, "id" | "requestedByUserId" | "creatorAgentId" | "agentId">,
103
+ principal: RbacPrincipal,
104
+ ): Promise<boolean> {
105
+ const attach = can({
106
+ principal,
107
+ verb: "task.parent.attach",
108
+ resource: {
109
+ kind: "task",
110
+ taskId: parentTask.id,
111
+ requestedByUserId: parentTask.requestedByUserId,
112
+ creatorAgentId: parentTask.creatorAgentId,
113
+ agentId: parentTask.agentId,
114
+ },
115
+ source: "http",
116
+ });
117
+ if (!attach.allow) {
118
+ jsonError(res, `You don't have context for parent task ${parentTask.id}.`, 403);
119
+ return true;
120
+ }
121
+ if (!(await hasSessionWrite(req, myAgentId, parentTask.id))) {
122
+ jsonError(res, SESSION_WRITE_DENIED, 403);
123
+ return true;
124
+ }
125
+ return false;
126
+ }
127
+
79
128
  /**
80
129
  * The denial for an actor lacking `needed` access to the session holding
81
130
  * `taskId`, or null. An unknown task passes, so the handler still answers 404.
@@ -2,11 +2,14 @@ import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  import { z } from "zod";
3
3
  import {
4
4
  countSessions,
5
+ createAgent,
6
+ getAgentById,
5
7
  getDbClient,
6
8
  getRootTaskChain,
7
9
  getTaskById,
8
10
  listRecentSessions,
9
11
  } from "../be/db";
12
+ import { getUserGrant } from "../be/rbac-roles";
10
13
  import {
11
14
  getSessionRoot,
12
15
  listActiveSessionShares,
@@ -18,13 +21,17 @@ import {
18
21
  import { getTaskSteeringFieldsForTasks } from "../be/steering";
19
22
  import { getTaskCitationsForTasks, TaskCitationSchema } from "../be/task-citations";
20
23
  import {
24
+ AgentTokenExistsError,
21
25
  findUserById,
22
26
  InvalidRuntimeTokenError,
27
+ mintAgentToken,
23
28
  mintRuntimeToken,
24
29
  mintSessionToken,
30
+ revokeAgentTokens,
25
31
  revokeSessionToken,
32
+ rotateAgentToken,
26
33
  } from "../be/users";
27
- import { can } from "../rbac";
34
+ import { can, isRbacEnabled } from "../rbac";
28
35
  import { sessionAccessTopic } from "../realtime/auth";
29
36
  import { realtimeBus } from "../realtime/bus";
30
37
  import {
@@ -39,6 +46,7 @@ import {
39
46
  } from "../types";
40
47
  import { getRequestAuth } from "../utils/request-auth-context";
41
48
  import { isRuntimePoolsEnabled } from "../utils/runtime-pools-flag";
49
+ import { agentFirstPrincipal } from "./request-principal";
42
50
  import { route } from "./route-def";
43
51
  import { resolveSessionActor, type SessionActor } from "./session-access";
44
52
  import { jsonError } from "./utils";
@@ -118,7 +126,7 @@ const mintSessionTokenRoute = route({
118
126
  },
119
127
  summary: "Mint an ephemeral task or runtime token",
120
128
  description:
121
- "Operators can mint task or runtime tokens. Runtime tokens can mint task tokens for their own assigned tasks. Child tokens expire with their parent runtime.",
129
+ "Operators can mint task or runtime tokens. Runtime and agent tokens can mint task tokens for their own assigned tasks. A child token never outlives its parent and is revoked with it.",
122
130
  tags: ["Sessions"],
123
131
  body: z
124
132
  .object({
@@ -147,7 +155,8 @@ const mintSessionTokenRoute = route({
147
155
  },
148
156
  auth: { apiKey: true },
149
157
  rbac: {
150
- ungated: "operator or runtime token minting for its own assigned task, enforced in the handler",
158
+ ungated:
159
+ "operator, or a runtime or agent token minting for its own assigned task, enforced in the handler",
151
160
  },
152
161
  });
153
162
 
@@ -157,7 +166,7 @@ const revokeSessionTokenRoute = route({
157
166
  pattern: ["api", "sessions", "tokens", null],
158
167
  summary: "Revoke an ephemeral session token",
159
168
  description:
160
- "Operators can revoke any ephemeral token. With runtime pools enabled, runtime tokens can revoke their own child task tokens. Repeated revocation has no effect.",
169
+ "Operators can revoke any ephemeral token. Agent tokens, and runtime tokens when runtime pools are enabled, can revoke their own child task tokens. Repeated revocation has no effect.",
161
170
  tags: ["Sessions"],
162
171
  params: z.object({ tokenId: z.string() }),
163
172
  responses: {
@@ -167,10 +176,234 @@ const revokeSessionTokenRoute = route({
167
176
  },
168
177
  auth: { apiKey: true },
169
178
  rbac: {
170
- ungated: "operator or runtime token revoking its own child, enforced in the handler",
179
+ ungated:
180
+ "operator, or a runtime or agent token revoking its own child, enforced in the handler",
171
181
  },
172
182
  });
173
183
 
184
+ /** Default rotation grace: the old worker token keeps working this long. */
185
+ const AGENT_TOKEN_ROTATE_GRACE_SEC = 300;
186
+
187
+ const AgentTokenSchema = z.object({
188
+ agentId: z.string(),
189
+ tokenId: z.string(),
190
+ plaintext: z.string(),
191
+ expiresAt: z.string().nullable().describe("null when the token does not expire"),
192
+ });
193
+
194
+ const agentTokenTtl = z
195
+ .number()
196
+ .int()
197
+ .positive()
198
+ .optional()
199
+ .describe("Token lifetime in ms. Omit for the default (no expiry).");
200
+
201
+ const mintAgentTokenRoute = route({
202
+ method: "post",
203
+ path: "/api/agents/{id}/token",
204
+ pattern: ["api", "agents", null, "token"],
205
+ operationId: "agents_mintWorkerToken",
206
+ summary: "Mint an agent-bound worker token",
207
+ description:
208
+ "Returns the token once. A worker runs on it in place of the swarm API key and acts only as this agent. Creates the agent when it does not exist and `name` is given. Fails with 409 when the agent already holds an active token.",
209
+ tags: ["Sessions"],
210
+ params: z.object({ id: z.string().min(1) }),
211
+ body: z
212
+ .object({ name: z.string().min(1).optional(), ttlMs: agentTokenTtl })
213
+ .strict()
214
+ .optional(),
215
+ responses: {
216
+ 200: {
217
+ description: "Minted worker token",
218
+ schema: AgentTokenSchema.extend({ agentCreated: z.boolean() }),
219
+ },
220
+ 403: { description: "Operator or user credentials required" },
221
+ 404: { description: "Agent not found and no name given" },
222
+ 409: { description: "Agent already holds an active token" },
223
+ },
224
+ rbac: { permission: "agent.token.manage" },
225
+ });
226
+
227
+ const rotateAgentTokenRoute = route({
228
+ method: "post",
229
+ path: "/api/agents/{id}/token/rotate",
230
+ pattern: ["api", "agents", null, "token", "rotate"],
231
+ operationId: "agents_rotateWorkerToken",
232
+ summary: "Rotate an agent-bound worker token",
233
+ description:
234
+ "Returns a new token once. The previous token keeps working for `graceSec` (default 300), then expires. Use revoke for a token that must stop now.",
235
+ tags: ["Sessions"],
236
+ params: z.object({ id: z.string().min(1) }),
237
+ body: z
238
+ .object({ graceSec: z.number().int().min(0).max(86_400).optional(), ttlMs: agentTokenTtl })
239
+ .strict()
240
+ .optional(),
241
+ responses: {
242
+ 200: {
243
+ description: "New worker token",
244
+ schema: AgentTokenSchema.extend({ previousExpiresAt: z.string() }),
245
+ },
246
+ 403: { description: "Operator or user credentials required" },
247
+ 404: { description: "Agent holds no active token" },
248
+ },
249
+ rbac: { permission: "agent.token.manage" },
250
+ });
251
+
252
+ const revokeAgentTokenRoute = route({
253
+ method: "delete",
254
+ path: "/api/agents/{id}/token",
255
+ pattern: ["api", "agents", null, "token"],
256
+ operationId: "agents_revokeWorkerToken",
257
+ summary: "Revoke an agent-bound worker token",
258
+ description:
259
+ "Revokes every active worker token of the agent and the task tokens they minted. Open realtime connections on them close.",
260
+ tags: ["Sessions"],
261
+ params: z.object({ id: z.string().min(1) }),
262
+ responses: {
263
+ 200: {
264
+ description: "Number of worker tokens revoked (0 when none was active)",
265
+ schema: z.object({ revoked: z.number().int() }),
266
+ },
267
+ 403: { description: "Operator or user credentials required" },
268
+ },
269
+ rbac: { permission: "agent.token.manage" },
270
+ });
271
+
272
+ const AGENT_TOKEN_ROUTES = [mintAgentTokenRoute, rotateAgentTokenRoute, revokeAgentTokenRoute];
273
+
274
+ /**
275
+ * Gate the worker-token routes. Only an operator key or a user may manage
276
+ * them, never a session token. The principal is agent-first: an operator key
277
+ * sent with X-Agent-ID acts as that agent and is refused, as on the
278
+ * controller-token route.
279
+ */
280
+ async function ensureAgentTokenManager(
281
+ req: IncomingMessage,
282
+ res: ServerResponse,
283
+ agentId: string,
284
+ ): Promise<boolean> {
285
+ const auth = getRequestAuth(req);
286
+ if (auth?.kind !== "operator" && auth?.kind !== "user") {
287
+ jsonError(res, "Worker tokens require operator or user credentials", 403);
288
+ return false;
289
+ }
290
+ const header = req.headers["x-agent-id"];
291
+ const decision = can({
292
+ principal: await agentFirstPrincipal(req, Array.isArray(header) ? header[0] : header),
293
+ verb: "agent.token.manage",
294
+ resource: { kind: "agent", agentId },
295
+ source: "http",
296
+ });
297
+ if (!decision.allow) {
298
+ jsonError(res, decision.reason, 403);
299
+ return false;
300
+ }
301
+ // A worker token carries the target agent's privileges and reads that
302
+ // agent's resolved config, global secrets included. A user without a
303
+ // grant-all role must already hold both, and may not manage the token of an
304
+ // agent that may itself act on any agent (a lead).
305
+ if (auth.kind === "user" && isRbacEnabled()) {
306
+ const grant = await getUserGrant(auth.userId);
307
+ if (!grant.grantsAll) {
308
+ const target = await getAgentById(agentId);
309
+ const targetActsOnAny = can({
310
+ principal: { kind: "agent", agentId, isLead: target?.isLead ?? false },
311
+ verb: "agent.act.any",
312
+ resource: { kind: "agent", agentId },
313
+ source: "http",
314
+ }).allow;
315
+ if (targetActsOnAny) {
316
+ jsonError(res, "A lead agent's worker token requires a grant-all role", 403);
317
+ return false;
318
+ }
319
+ const missing = (["agent.act.any", "config.read.secrets"] as const).filter(
320
+ (verb) => !grant.verbs.has(verb),
321
+ );
322
+ if (missing.length > 0) {
323
+ jsonError(res, `Worker tokens also require: ${missing.join(", ")}`, 403);
324
+ return false;
325
+ }
326
+ }
327
+ }
328
+ return true;
329
+ }
330
+
331
+ async function handleAgentTokens(
332
+ req: IncomingMessage,
333
+ res: ServerResponse,
334
+ pathSegments: string[],
335
+ queryParams: URLSearchParams,
336
+ ): Promise<boolean> {
337
+ const matched = AGENT_TOKEN_ROUTES.find((def) => def.match(req.method, pathSegments));
338
+ if (!matched) return false;
339
+ if (!(await ensureAgentTokenManager(req, res, pathSegments[2]!))) return true;
340
+ res.setHeader("Cache-Control", "no-store");
341
+
342
+ if (matched === mintAgentTokenRoute) {
343
+ const parsed = await mintAgentTokenRoute.parse(req, res, pathSegments, queryParams);
344
+ if (!parsed) return true;
345
+ const agentId = parsed.params.id;
346
+ const result = await getDbClient()
347
+ .transaction(async () => {
348
+ let agentCreated = false;
349
+ if (!(await getAgentById(agentId))) {
350
+ if (!parsed.body?.name) return { status: 404 as const };
351
+ // Same defaults as a worker's first registration; it is offline until it connects.
352
+ await createAgent({
353
+ id: agentId,
354
+ name: parsed.body.name,
355
+ isLead: false,
356
+ status: "offline",
357
+ capabilities: [],
358
+ maxTasks: 1,
359
+ });
360
+ agentCreated = true;
361
+ }
362
+ return {
363
+ status: 200 as const,
364
+ agentCreated,
365
+ token: await mintAgentToken(agentId, parsed.body?.ttlMs),
366
+ };
367
+ })
368
+ .catch((error) => {
369
+ if (error instanceof AgentTokenExistsError) return { status: 409 as const };
370
+ throw error;
371
+ });
372
+ if (result.status === 404) {
373
+ jsonError(res, "Agent not found; pass name to create it", 404);
374
+ return true;
375
+ }
376
+ if (result.status === 409) {
377
+ jsonError(res, "Agent already holds an active worker token; rotate or revoke it", 409);
378
+ return true;
379
+ }
380
+ mintAgentTokenRoute.respond(res, 200, {
381
+ agentId,
382
+ ...result.token,
383
+ agentCreated: result.agentCreated,
384
+ });
385
+ return true;
386
+ }
387
+
388
+ if (matched === rotateAgentTokenRoute) {
389
+ const parsed = await rotateAgentTokenRoute.parse(req, res, pathSegments, queryParams);
390
+ if (!parsed) return true;
391
+ const graceSec = parsed.body?.graceSec ?? AGENT_TOKEN_ROTATE_GRACE_SEC;
392
+ const token = await rotateAgentToken(parsed.params.id, graceSec * 1000, parsed.body?.ttlMs);
393
+ if (!token) {
394
+ jsonError(res, "Agent holds no active worker token", 404);
395
+ return true;
396
+ }
397
+ rotateAgentTokenRoute.respond(res, 200, { agentId: parsed.params.id, ...token });
398
+ return true;
399
+ }
400
+
401
+ const parsed = await revokeAgentTokenRoute.parse(req, res, pathSegments, queryParams);
402
+ if (!parsed) return true;
403
+ revokeAgentTokenRoute.respond(res, 200, { revoked: await revokeAgentTokens(parsed.params.id) });
404
+ return true;
405
+ }
406
+
174
407
  const listSessions = route({
175
408
  method: "get",
176
409
  path: "/api/sessions",
@@ -309,9 +542,11 @@ export async function handleSessions(
309
542
  queryParams: URLSearchParams,
310
543
  myAgentId?: string,
311
544
  ): Promise<boolean> {
545
+ if (await handleAgentTokens(req, res, pathSegments, queryParams)) return true;
546
+
312
547
  if (mintSessionTokenRoute.match(req.method, pathSegments)) {
313
548
  const auth = getRequestAuth(req);
314
- if (auth?.kind !== "operator" && !(auth?.kind === "agent" && auth.runtimeId)) {
549
+ if (auth?.kind !== "operator" && !(auth?.kind === "agent" && auth.tokenKind !== "task")) {
315
550
  jsonError(res, "Unauthorized", 401);
316
551
  return true;
317
552
  }
@@ -334,7 +569,7 @@ export async function handleSessions(
334
569
  return {
335
570
  token: await mintSessionToken(agentId, taskId!, ttlMs!, {
336
571
  tokenId: auth.tokenId,
337
- runtimeId: auth.runtimeId!,
572
+ runtimeId: auth.runtimeId,
338
573
  }),
339
574
  };
340
575
  }
@@ -365,7 +600,10 @@ export async function handleSessions(
365
600
  const auth = getRequestAuth(req);
366
601
  if (
367
602
  auth?.kind !== "operator" &&
368
- !(isRuntimePoolsEnabled() && auth?.kind === "agent" && auth.runtimeId)
603
+ !(
604
+ auth?.kind === "agent" &&
605
+ (auth.tokenKind === "agent" || (auth.tokenKind === "runtime" && isRuntimePoolsEnabled()))
606
+ )
369
607
  ) {
370
608
  jsonError(res, "Unauthorized", 401);
371
609
  return true;
@@ -375,8 +613,9 @@ export async function handleSessions(
375
613
  const revoked = await getDbClient().transaction(async (client) => {
376
614
  if (auth.kind === "agent") {
377
615
  const child = await client.get(
378
- "SELECT id FROM session_tokens WHERE id = ? AND agentId = ? AND parentRuntimeId = ? AND taskId IS NOT NULL",
379
- [parsed.params.tokenId, auth.agentId, auth.runtimeId!],
616
+ `SELECT id FROM session_tokens WHERE id = ? AND agentId = ? AND taskId IS NOT NULL
617
+ AND ${auth.runtimeId ? "parentRuntimeId" : "parentTokenId"} = ?`,
618
+ [parsed.params.tokenId, auth.agentId, auth.runtimeId ?? auth.tokenId],
380
619
  );
381
620
  if (!child) return false;
382
621
  }
package/src/http/tasks.ts CHANGED
@@ -87,6 +87,7 @@ import { buildTaskRequestedBy, TaskRequestedBySchema } from "./requested-by";
87
87
  import { bodyTask, route } from "./route-def";
88
88
  import {
89
89
  canReadSession,
90
+ denyParentAttach,
90
91
  readableByFor,
91
92
  resolveSessionActor,
92
93
  SESSION_READ_DENIED,
@@ -779,16 +780,6 @@ const updateTaskTitleRoute = route({
779
780
  },
780
781
  });
781
782
 
782
- /** True unless the request acts for a user who lacks write access to the task's session. */
783
- async function hasSessionWrite(
784
- req: IncomingMessage,
785
- myAgentId: string | undefined,
786
- taskId: string,
787
- ): Promise<boolean> {
788
- const access = await sessionAccessFor(await resolveSessionActor(req, myAgentId), taskId);
789
- return hasSessionAccess(access, "write");
790
- }
791
-
792
783
  async function canSteerTask(
793
784
  req: IncomingMessage,
794
785
  myAgentId: string | undefined,
@@ -1069,29 +1060,10 @@ export async function handleTasks(
1069
1060
  ? await getTaskById(parsed.body.parentTaskId)
1070
1061
  : null;
1071
1062
  // The child inherits the parent's thread context, so an agent caller must own or
1072
- // have created the parent. Operator and user callers keep their access.
1063
+ // have created the parent, and a follow-up in someone else's session needs a
1064
+ // write share. The same gate guards app actions that chain under a task.
1073
1065
  const principal = await resolveTaskWritePrincipal(req, myAgentId);
1074
- if (
1075
- parentTask &&
1076
- !can({
1077
- principal,
1078
- verb: "task.parent.attach",
1079
- resource: {
1080
- kind: "task",
1081
- taskId: parentTask.id,
1082
- requestedByUserId: parentTask.requestedByUserId,
1083
- creatorAgentId: parentTask.creatorAgentId,
1084
- agentId: parentTask.agentId,
1085
- },
1086
- source: "http",
1087
- }).allow
1088
- ) {
1089
- jsonError(res, `You don't have context for parent task ${parentTask.id}.`, 403);
1090
- return true;
1091
- }
1092
- // A follow-up in someone else's session needs a write share.
1093
- if (parentTask && !(await hasSessionWrite(req, myAgentId, parentTask.id))) {
1094
- jsonError(res, SESSION_WRITE_DENIED, 403);
1066
+ if (parentTask && (await denyParentAttach(req, res, myAgentId, parentTask, principal))) {
1095
1067
  return true;
1096
1068
  }
1097
1069
  // A chat-sdk contextKey binds the task to a chat thread that its assignee and creator
package/src/http/users.ts CHANGED
@@ -550,7 +550,18 @@ const deleteIdentityRoute = route({
550
550
 
551
551
  // ─── Helpers ─────────────────────────────────────────────────────────────────
552
552
 
553
- const UNMAPPED_KINDS = ["slack", "github", "gitlab", "azure-devops", "linear", "kapso"] as const;
553
+ const UNMAPPED_KINDS = [
554
+ "slack",
555
+ "github",
556
+ "gitlab",
557
+ "azure-devops",
558
+ "linear",
559
+ "kapso",
560
+ // Chat SDK adapters record unknown senders under the adapter name.
561
+ "telegram",
562
+ "discord",
563
+ "teams",
564
+ ] as const;
554
565
 
555
566
  /**
556
567
  * Group the two-key-per-identity kv entries (`<externalId>:meta` json +
@@ -63,9 +63,13 @@ export async function loadAdapter(
63
63
  case "telegram": {
64
64
  const { createTelegramAdapter } = await import("@chat-adapter/telegram");
65
65
  // Explicit `false` blocks the TELEGRAM_ALLOW_UNVERIFIED_WEBHOOKS env fallback.
66
- return createTelegramAdapter({ ...base, allowUnverifiedWebhooks: false } as Parameters<
67
- typeof createTelegramAdapter
68
- >[0]);
66
+ // Explicit `true` makes a reply to the bot a mention whatever
67
+ // TELEGRAM_MENTION_ON_REPLY says.
68
+ return createTelegramAdapter({
69
+ ...base,
70
+ allowUnverifiedWebhooks: false,
71
+ mentionOnReply: true,
72
+ } as Parameters<typeof createTelegramAdapter>[0]);
69
73
  }
70
74
  case "slack": {
71
75
  const { createSlackAdapter } = await import("@chat-adapter/slack");
@@ -9,7 +9,9 @@
9
9
  * row. The task is created in `draft` while its uploads run, with its lease
10
10
  * renewed, so no worker can claim it before its attachments exist. Every file
11
11
  * also stays in the task text as a `[File: ...]` line. Only a fetch failure is
12
- * flagged there; upload and record failures are logged.
12
+ * flagged there; upload and record failures are logged. Audio files (voice
13
+ * notes) get their transcript in the task text when transcription is
14
+ * configured, and keep the audio attached.
13
15
  */
14
16
  import { mkdtemp, rm } from "node:fs/promises";
15
17
  import { tmpdir } from "node:os";
@@ -21,12 +23,28 @@ import { providerPath } from "@/fs/provider";
21
23
  import { getFileStorageProvider } from "@/fs/registry";
22
24
  import { createTaskWithSiblingAwareness } from "@/tasks/sibling-awareness";
23
25
  import type { AgentTask, CreateTaskOptions } from "@/types";
26
+ import { transcribeAudioNotes } from "@/utils/transcription";
24
27
  import { errorMessage } from "./logger";
25
28
 
26
29
  /** How often a draft's lease is renewed while its uploads run; the sweep waits 5 minutes. */
27
30
  const DRAFT_LEASE_REFRESH_MS = 60_000;
28
31
  /** A file a user sent the bot, like one uploaded from the dashboard composer. */
29
- const SHARED_FILE_INTENT = "user-upload";
32
+ export const SHARED_FILE_INTENT = "user-upload";
33
+
34
+ const MB = 1024 * 1024;
35
+ /**
36
+ * Per-file inbound cap by adapter. A Telegram bot can download at most 20 MB
37
+ * through `getFile`; a larger file fails at Telegram before our own cap.
38
+ * Other adapters use the task attachment cap.
39
+ */
40
+ const CHAT_INBOUND_FILE_LIMITS: Record<string, number> = { telegram: 20 * MB };
41
+
42
+ export function chatInboundFileLimit(adapter: string): number {
43
+ return Math.min(
44
+ CHAT_INBOUND_FILE_LIMITS[adapter] ?? MAX_TASK_ATTACHMENT_BYTES,
45
+ MAX_TASK_ATTACHMENT_BYTES,
46
+ );
47
+ }
30
48
 
31
49
  /**
32
50
  * One attachment of a buffered chat message. `id` is `<messageId>-<n>` with
@@ -43,7 +61,8 @@ export interface ChatFile {
43
61
  attachment: Attachment;
44
62
  }
45
63
 
46
- export type ChatFileFailure = { file: ChatFile; reason: string };
64
+ /** `tooLarge` marks a file over the inbound cap; the user gets a reply naming it. */
65
+ export type ChatFileFailure = { file: ChatFile; reason: string; tooLarge?: boolean };
47
66
 
48
67
  export type FetchedChatFile = { file: ChatFile; path: string; sizeBytes: number; sha256: string };
49
68
 
@@ -55,7 +74,7 @@ export type InboundChatFiles = {
55
74
  [Symbol.asyncDispose](): Promise<void>;
56
75
  };
57
76
 
58
- function formatFileSize(bytes: number): string {
77
+ export function formatFileSize(bytes: number): string {
59
78
  if (bytes < 1024) return `${bytes} B`;
60
79
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
61
80
  if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
@@ -81,20 +100,71 @@ export function toChatFiles(
81
100
 
82
101
  /**
83
102
  * `[File: <name> (<mime>, <size>) id=<id>]` per file, followed by
84
- * `(not attached: <reason>)` when the file never reached the task.
103
+ * `(not attached: <reason>)` when the file never reached the task, and by the
104
+ * file's note (a voice-note transcript) on the next line.
85
105
  */
86
- export function buildChatAttachmentText(files: ChatFile[], failed: ChatFileFailure[] = []): string {
106
+ export function buildChatAttachmentText(
107
+ files: ChatFile[],
108
+ failed: ChatFileFailure[] = [],
109
+ notes: Map<string, string> = new Map(),
110
+ ): string {
87
111
  const reasons = new Map(failed.map((f) => [f.file.id, f.reason]));
88
112
  return files
89
113
  .map((f) => {
90
114
  const size = typeof f.size === "number" ? formatFileSize(f.size) : "unknown size";
91
115
  const line = `[File: ${f.name} (${f.mimeType ?? "unknown type"}, ${size}) id=${f.id}]`;
92
116
  const reason = reasons.get(f.id);
93
- return reason ? `${line} (not attached: ${reason})` : line;
117
+ const note = notes.get(f.id);
118
+ return [reason ? `${line} (not attached: ${reason})` : line, note].filter(Boolean).join("\n");
94
119
  })
95
120
  .join("\n");
96
121
  }
97
122
 
123
+ /**
124
+ * A sender-chosen file name as an inline code span, so the bot's reply shows it
125
+ * as literal text and never renders a link, image or heading from it. Backslash
126
+ * escapes are not enough: the Chat SDK parser still autolinks bare URLs.
127
+ */
128
+ function markdownLiteral(value: string): string {
129
+ const text = value.replace(/[\r\n]+/g, " ");
130
+ const longestRun = Math.max(0, ...(text.match(/`+/g) ?? []).map((run) => run.length));
131
+ const fence = "`".repeat(longestRun + 1);
132
+ // CommonMark strips one space from each end of a padded span.
133
+ const pad = /^[` ]|[` ]$/.test(text) ? " " : "";
134
+ return `${fence}${pad}${text}${pad}${fence}`;
135
+ }
136
+
137
+ /** The reply posted to the thread when files were over the inbound cap. */
138
+ export function formatTooLargeReply(failed: ChatFileFailure[]): string {
139
+ const lines = failed.map((f) => `- ${markdownLiteral(f.file.name)}: ${f.reason}`);
140
+ return [
141
+ `Too large to read, so the swarm did not get ${failed.length === 1 ? "this file" : "these files"}:`,
142
+ ...lines,
143
+ "Send a smaller file or a link.",
144
+ ].join("\n");
145
+ }
146
+
147
+ /**
148
+ * Transcribe every fetched audio file. Returns one task-text note per file:
149
+ * the transcript, the failure, or (when no provider is configured) a line
150
+ * saying so. Never throws.
151
+ */
152
+ export async function transcribeChatAudio(
153
+ fetched: FetchedChatFile[],
154
+ ): Promise<Map<string, string>> {
155
+ return transcribeAudioNotes(
156
+ fetched
157
+ .filter(({ file }) => file.attachment.type === "audio")
158
+ .map(({ file, path }) => ({
159
+ key: file.id,
160
+ name: file.name,
161
+ data: Bun.file(path),
162
+ mimeType: file.mimeType,
163
+ })),
164
+ "[chat-sdk]",
165
+ );
166
+ }
167
+
98
168
  async function readAttachment(attachment: Attachment): Promise<Uint8Array> {
99
169
  if (attachment.data instanceof Blob) return new Uint8Array(await attachment.data.arrayBuffer());
100
170
  if (attachment.data) return new Uint8Array(attachment.data);
@@ -119,17 +189,25 @@ export async function fetchChatFiles(files: ChatFile[]): Promise<InboundChatFile
119
189
  };
120
190
  if (files.length === 0) return result;
121
191
 
122
- const limit = `larger than the ${MAX_TASK_ATTACHMENT_BYTES / (1024 * 1024)} MB limit`;
192
+ const tooLarge = (file: ChatFile, size: number): ChatFileFailure => {
193
+ const limit = chatInboundFileLimit(file.adapter);
194
+ const platform = file.adapter.charAt(0).toUpperCase() + file.adapter.slice(1);
195
+ return {
196
+ file,
197
+ reason: `too large (${formatFileSize(size)}, the limit for files sent to the ${platform} bot is ${formatFileSize(limit)})`,
198
+ tooLarge: true,
199
+ };
200
+ };
123
201
  dir = await mkdtemp(join(tmpdir(), "chat-files-"));
124
202
  for (const [index, file] of files.entries()) {
125
- if (typeof file.size === "number" && file.size > MAX_TASK_ATTACHMENT_BYTES) {
126
- result.failed.push({ file, reason: limit });
203
+ if (typeof file.size === "number" && file.size > chatInboundFileLimit(file.adapter)) {
204
+ result.failed.push(tooLarge(file, file.size));
127
205
  continue;
128
206
  }
129
207
  try {
130
208
  const bytes = await readAttachment(file.attachment);
131
- if (bytes.byteLength > MAX_TASK_ATTACHMENT_BYTES) {
132
- result.failed.push({ file, reason: limit });
209
+ if (bytes.byteLength > chatInboundFileLimit(file.adapter)) {
210
+ result.failed.push(tooLarge(file, bytes.byteLength));
133
211
  continue;
134
212
  }
135
213
  const path = join(dir, String(index));