@desplega.ai/agent-swarm 2.4.4 → 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 (221) hide show
  1. package/dist/{acp-adapter-thq04adh.js → acp-adapter-2wdkktv6.js} +1 -1
  2. package/dist/{actions-j6ywh6x5.js → actions-c40ebcm4.js} +18 -18
  3. package/dist/{amp-adapter-k8b5xmj8.js → amp-adapter-wb67m95z.js} +2 -2
  4. package/dist/{app-rb7vc02s.js → app-353vre8g.js} +3 -3
  5. package/dist/{assistant-ftta1zjf.js → assistant-sj1njh23.js} +21 -21
  6. package/dist/{boot-reembed-srp6v6af.js → boot-reembed-b9c3az3c.js} +1 -1
  7. package/dist/{boot-reembed-xns7h5h9.js → boot-reembed-qdkvxeem.js} +1 -1
  8. package/dist/{boot-scrub-sweep-3qqrh2mz.js → boot-scrub-sweep-m3wh8eks.js} +2 -2
  9. package/dist/{boot-seeds-9ks8hce7.js → boot-seeds-j2273s54.js} +4 -4
  10. package/dist/{claude-adapter-k5ksyres.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-n5a5eqks.js → claude-managed-setup-smrqk5j5.js} +3 -3
  13. package/dist/{claude-sdk-session-2j3j4kwn.js → claude-sdk-session-38xnz21e.js} +8284 -8267
  14. package/dist/{cli-cynvc9fj.js → cli-018r4jq6.js} +5 -5
  15. package/dist/{cli-9fhkkpj5.js → cli-1f3qzw6y.js} +1 -1
  16. package/dist/{cli-dqb4n629.js → cli-1sp861s4.js} +7 -7
  17. package/dist/{cli-gqvh8d8a.js → cli-1syn2xy1.js} +10 -10
  18. package/dist/{cli-qkhc5aqy.js → cli-37j04k05.js} +11 -11
  19. package/dist/{cli-1d70crsy.js → cli-3a45zcgm.js} +1 -1
  20. package/dist/{cli-t7cyef5g.js → cli-43x64hv3.js} +4 -4
  21. package/dist/{cli-hj3debgw.js → cli-4zfqgjc5.js} +2 -2
  22. package/dist/{cli-55qmbp6z.js → cli-7c9qs530.js} +1 -1
  23. package/dist/{cli-9qtjfd60.js → cli-848hxdwj.js} +1 -1
  24. package/dist/{cli-pccfmtb5.js → cli-94gwyg7m.js} +7 -7
  25. package/dist/{cli-51y09c6p.js → cli-9hstj09h.js} +5 -5
  26. package/dist/{cli-zrwpctp4.js → cli-9yqgm9nd.js} +22 -22
  27. package/dist/{cli-esxerzg7.js → cli-a92r1127.js} +1 -1
  28. package/dist/{cli-b8k7epj1.js → cli-be2161h1.js} +7 -7
  29. package/dist/{cli-xc27fwzg.js → cli-bnes4z6b.js} +2 -2
  30. package/dist/{cli-zrk6jxv8.js → cli-cxxe61f6.js} +1 -1
  31. package/dist/{cli-amyhm3gj.js → cli-dk44dxec.js} +1 -1
  32. package/dist/{cli-nc35pxb7.js → cli-eww120g8.js} +1 -0
  33. package/dist/{cli-a0kcty51.js → cli-f01h25yt.js} +1 -1
  34. package/dist/{cli-82d3nkc3.js → cli-f0g3pgvf.js} +1 -1
  35. package/dist/{cli-1ephgwnq.js → cli-ftc1g5tj.js} +2 -2
  36. package/dist/{cli-2v045xyf.js → cli-fv5hepyy.js} +3 -3
  37. package/dist/{cli-bnz0r3kc.js → cli-g35m2xwm.js} +4 -4
  38. package/dist/{cli-qje9k2nh.js → cli-g9ypd5en.js} +12 -2
  39. package/dist/{cli-w6nadkh4.js → cli-j99azg3g.js} +3 -3
  40. package/dist/{cli-er83cjbn.js → cli-k8g16gz3.js} +416 -116
  41. package/dist/{cli-4dg55wsk.js → cli-kpw04va0.js} +1 -1
  42. package/dist/{cli-3cgngyd8.js → cli-ms85309d.js} +33 -15
  43. package/dist/{cli-v3yr2gbg.js → cli-n72rg53p.js} +1471 -635
  44. package/dist/{cli-37yb7307.js → cli-p14m2cnp.js} +1 -1
  45. package/dist/{cli-2zqpkvtt.js → cli-qfxc84gh.js} +1 -1
  46. package/dist/{cli-44159k28.js → cli-qwhy2qma.js} +1 -1
  47. package/dist/{cli-2y91wnbh.js → cli-st8aw1m6.js} +221 -9
  48. package/dist/{cli-vq97g1jq.js → cli-tw4p90jb.js} +92 -192
  49. package/dist/{cli-te6b5wvg.js → cli-v0yrfmdh.js} +1 -1
  50. package/dist/{cli-xj0re5tm.js → cli-v8fjakwf.js} +1 -1
  51. package/dist/{cli-p20r8w42.js → cli-wt0jf9hm.js} +215 -80
  52. package/dist/{cli-q6r7t299.js → cli-xpa64rek.js} +33 -11
  53. package/dist/{cli-vdm20wej.js → cli-z7d29rw9.js} +2 -2
  54. package/dist/cli.js +19 -19
  55. package/dist/{codex-adapter-968xwfy7.js → codex-adapter-tn6k5mkg.js} +1 -1
  56. package/dist/{codex-session-runner-ht4z99fa.js → codex-session-runner-7vjt7jbw.js} +1 -1
  57. package/dist/{commands-pcymaeg9.js → commands-t7rz9146.js} +2 -2
  58. package/dist/{controller-fpjyk46r.js → controller-m2jasma5.js} +2 -2
  59. package/dist/{controllers-connect-mwzx1fh6.js → controllers-connect-9ftpex99.js} +19 -19
  60. package/dist/{cursor-adapter-cp4s2e2f.js → cursor-adapter-mjv79e4z.js} +1 -1
  61. package/dist/{db-xsjg9jqq.js → db-6sfrn9g3.js} +1 -1
  62. package/dist/{db-sgd12bt9.js → db-rn673r06.js} +3 -3
  63. package/dist/{dispatcher-x0jfw2c9.js → dispatcher-c155xz0p.js} +17 -17
  64. package/dist/{dsh-adapter-jrq7ma7h.js → dsh-adapter-3mmszc1x.js} +1 -1
  65. package/dist/{external-channel-8njj4h3b.js → external-channel-35k6tkv8.js} +2 -2
  66. package/dist/{grok-adapter-kkr97v2f.js → grok-adapter-tfrq7tg1.js} +2 -2
  67. package/dist/handlers-7gw64dxw.js +39 -0
  68. package/dist/{heartbeat-wd03zhn0.js → heartbeat-dyg4npkp.js} +20 -20
  69. package/dist/{hook-f0vghj42.js → hook-drspbwpe.js} +1 -1
  70. package/dist/{hook-xzj2ts2p.js → hook-x26b31bg.js} +1 -1
  71. package/dist/{http-4hwxv62p.js → http-pk972xyw.js} +355 -216
  72. package/dist/{index-ww2vznkp.js → index-arytgj5q.js} +1 -1
  73. package/dist/{index-hg0x2cdv.js → index-bgdyvcnv.js} +17 -17
  74. package/dist/{index-c4ykd0f1.js → index-dr94zbnh.js} +13348 -11611
  75. package/dist/{index-gdh8jrdm.js → index-gabbcdn2.js} +17 -17
  76. package/dist/{index-97q4xn03.js → index-jwxkkex2.js} +129 -75
  77. package/dist/index-tfb26zhy.js +41 -0
  78. package/dist/{keepalive-hrh7xnr9.js → keepalive-7yrng7px.js} +3 -3
  79. package/dist/{lead-ee8vazz2.js → lead-7e02z839.js} +29 -29
  80. package/dist/{maintenance-e8wnwnqx.js → maintenance-e6cemdqt.js} +1 -1
  81. package/dist/{node-3v3f08f7.js → node-de6akczr.js} +10 -9
  82. package/dist/{oauth-refresh-sweep-zm0yj4sj.js → oauth-refresh-sweep-yw3xdbcj.js} +2 -2
  83. package/dist/{onboard-dtwcq603.js → onboard-0a9y5yrc.js} +2 -2
  84. package/dist/{otel-impl-w3fbhpn0.js → otel-impl-trjpkfb7.js} +1 -1
  85. package/dist/{pricing-refresh-j38dehfs.js → pricing-refresh-d2b5my0y.js} +3 -3
  86. package/dist/{rbac-roles-ejxx9b35.js → rbac-roles-hey3yv9d.js} +2 -2
  87. package/dist/{rbac-roles-ae8yttq4.js → rbac-roles-nxh5ahwk.js} +1 -1
  88. package/dist/{registry-rxw10xzg.js → registry-ajhexe44.js} +1 -1
  89. package/dist/{render-v2-n16n0phe.js → render-v2-rr3p2atg.js} +3 -3
  90. package/dist/{resume-z0fgjc38.js → resume-2efkf1s1.js} +5 -5
  91. package/dist/{session-access-cv3mq6mc.js → session-access-s2mttxpw.js} +4 -4
  92. package/dist/{setup-mafhj3yt.js → setup-vypma5yt.js} +1 -1
  93. package/dist/{work-objects-8b86mgjp.js → work-objects-rvhckdkp.js} +20 -20
  94. package/dist/{worker-16s75hm5.js → worker-5snytpbd.js} +29 -29
  95. package/openapi.json +9 -2
  96. package/package.json +7 -7
  97. package/src/apps/catalog.generated.json +206 -4
  98. package/src/apps/page-validator.ts +13 -1
  99. package/src/be/chunking.ts +42 -9
  100. package/src/be/db/tasks/draft-text.ts +15 -0
  101. package/src/be/db/tasks/read.ts +14 -1
  102. package/src/be/db.ts +5 -1
  103. package/src/be/memory/index-content.ts +36 -17
  104. package/src/be/memory/providers/openai-embedding.ts +115 -61
  105. package/src/be/memory/providers/sqlite-store.ts +69 -23
  106. package/src/be/memory/reranker.ts +31 -1
  107. package/src/be/memory/search-candidates.ts +47 -0
  108. package/src/be/memory/types.ts +14 -0
  109. package/src/be/scripts/typecheck.ts +2 -0
  110. package/src/be/seed-scripts/catalog/heartbeat-jev-gate.ts +106 -54
  111. package/src/be/seed-scripts/index.ts +2 -2
  112. package/src/be/swarm-config-guard.ts +24 -1
  113. package/src/be/task-attachment-store.ts +30 -0
  114. package/src/extensions/catalog.generated.json +4 -4
  115. package/src/extensions/contract-types.generated.ts +1 -1
  116. package/src/extensions/contract.ts +7 -0
  117. package/src/fs/provider.ts +13 -1
  118. package/src/heartbeat/heartbeat.ts +31 -11
  119. package/src/http/fs.ts +54 -34
  120. package/src/http/mcp-body.ts +37 -0
  121. package/src/http/mcp-user.ts +3 -5
  122. package/src/http/mcp.ts +6 -7
  123. package/src/http/memory.ts +26 -15
  124. package/src/http/page-proxy.ts +61 -5
  125. package/src/http/pages-public.ts +27 -44
  126. package/src/http/pages.ts +4 -4
  127. package/src/http/script-runs.ts +2 -0
  128. package/src/http/users.ts +12 -1
  129. package/src/integrations/chat-sdk/adapters.ts +7 -3
  130. package/src/integrations/chat-sdk/attachments.ts +90 -12
  131. package/src/integrations/chat-sdk/facts.ts +120 -0
  132. package/src/integrations/chat-sdk/inbound.ts +156 -8
  133. package/src/integrations/chat-sdk/limits.ts +24 -0
  134. package/src/integrations/chat-sdk/outbound-files.ts +186 -0
  135. package/src/integrations/chat-sdk/outbound.ts +234 -4
  136. package/src/integrations/chat-sdk/relay.ts +4 -1
  137. package/src/integrations/chat-sdk/router.ts +28 -2
  138. package/src/integrations/chat-sdk/steering.ts +9 -0
  139. package/src/integrations/chat-sdk/templates.ts +14 -0
  140. package/src/integrations/chat-sdk/thread-buffer.ts +4 -0
  141. package/src/mcp-limits.ts +20 -0
  142. package/src/pages/browser-sdk.ts +8 -3
  143. package/src/pages/feedback-overlay.ts +1 -1
  144. package/src/providers/amp-adapter.ts +1 -1
  145. package/src/providers/codex-adapter.ts +106 -119
  146. package/src/providers/codex-agents-md.ts +35 -229
  147. package/src/providers/cursor-adapter.ts +1 -1
  148. package/src/realtime/transport.ts +1 -2
  149. package/src/script-workflows/workflow-ctx.ts +1 -0
  150. package/src/scripts-runtime/types/stdlib.d.ts +2 -0
  151. package/src/scripts-runtime/types/swarm-extension.d.ts +8 -0
  152. package/src/scripts-runtime/types/swarm-sdk.d.ts +2 -0
  153. package/src/server-user.ts +2 -0
  154. package/src/server.ts +2 -0
  155. package/src/slack/assistant.ts +2 -2
  156. package/src/slack/files.ts +2 -0
  157. package/src/slack/handlers.ts +4 -4
  158. package/src/slack/inbound-files.ts +39 -6
  159. package/src/slack/thread-buffer.ts +2 -1
  160. package/src/tasks/task-terminal-effects.ts +21 -17
  161. package/src/tests/apps-card-grid.test.ts +197 -0
  162. package/src/tests/apps-catalog-additions.test.ts +120 -0
  163. package/src/tests/apps-tree.test.ts +129 -0
  164. package/src/tests/chat-sdk-attachments-reply.test.ts +52 -0
  165. package/src/tests/chat-sdk-config.test.ts +11 -0
  166. package/src/tests/chat-sdk-facts.test.ts +23 -0
  167. package/src/tests/chat-sdk-inbound.test.ts +558 -4
  168. package/src/tests/chat-sdk-outbound-files.test.ts +652 -0
  169. package/src/tests/codex-adapter.test.ts +167 -242
  170. package/src/tests/config-api-only-keys.test.ts +9 -0
  171. package/src/tests/cors-allowlist.test.ts +1 -1
  172. package/src/tests/extensions-example-slack-reply-gate.test.ts +262 -91
  173. package/src/tests/heartbeat-checklist.test.ts +33 -0
  174. package/src/tests/heartbeat-jev-gate-credentials.test.ts +44 -0
  175. package/src/tests/heartbeat-jev-gate.test.ts +242 -1
  176. package/src/tests/kv-page-proxy.test.ts +91 -5
  177. package/src/tests/launch-password-rejection.test.ts +2 -2
  178. package/src/tests/mcp-input-caps.test.ts +294 -0
  179. package/src/tests/memory-chunked-search-starvation.test.ts +200 -0
  180. package/src/tests/memory-embedding-input-cap.test.ts +114 -0
  181. package/src/tests/memory.test.ts +14 -0
  182. package/src/tests/page-guest-session.test.ts +19 -8
  183. package/src/tests/page-proxy-authed.test.ts +1 -1
  184. package/src/tests/page-proxy.test.ts +17 -10
  185. package/src/tests/page-session.test.ts +102 -1
  186. package/src/tests/pages-authed-mode.test.ts +82 -12
  187. package/src/tests/pages-password-mode.test.ts +5 -5
  188. package/src/tests/realtime-transport.test.ts +13 -7
  189. package/src/tests/script-runs-http.test.ts +84 -0
  190. package/src/tests/scripts-http.test.ts +10 -1
  191. package/src/tests/swarm-config-export-to-env.test.ts +22 -3
  192. package/src/tests/task-audio-transcription.test.ts +299 -0
  193. package/src/tests/task-completion-memory-chunking.test.ts +434 -0
  194. package/src/tests/task-terminal-secret-scrub.test.ts +6 -1
  195. package/src/tests/transcription.test.ts +299 -0
  196. package/src/tests/workflow-system-one-decision-providers.test.ts +98 -1
  197. package/src/tests/workflow-system-one-decision.test.ts +2 -1
  198. package/src/tools/chat-access.ts +10 -0
  199. package/src/tools/chat-post.ts +31 -11
  200. package/src/tools/chat-reply.ts +32 -10
  201. package/src/tools/memory-search.ts +3 -1
  202. package/src/tools/system-one-decide.ts +17 -4
  203. package/src/types.ts +6 -0
  204. package/src/utils/config-env-export.ts +3 -1
  205. package/src/utils/page-session.ts +133 -20
  206. package/src/utils/transcription.ts +246 -0
  207. package/src/workflows/executors/system-one-decision.ts +28 -7
  208. package/src/workflows/executors/system-one-providers.ts +39 -4
  209. package/templates/extensions/slack-reply-gate/README.md +24 -22
  210. package/templates/extensions/slack-reply-gate/hooks.ts +146 -95
  211. package/templates/extensions/slack-reply-gate/manifest.json +2 -2
  212. package/templates/skills/apps/SKILL.md +28 -6
  213. package/templates/skills/apps/content.md +28 -6
  214. package/templates/skills/chat-sdk/content.md +6 -2
  215. package/templates/skills/code-quality/content.md +2 -1
  216. package/templates/skills/heartbeat-runbook/content.md +5 -5
  217. package/templates/skills/pages/content.md +6 -4
  218. package/templates/skills/swarm-scripts/SKILL.md +2 -2
  219. package/templates/skills/swarm-scripts/content.md +2 -2
  220. package/dist/handlers-0pvee9nw.js +0 -39
  221. package/dist/index-thm0y6a8.js +0 -41
@@ -2,12 +2,15 @@ import type { Adapter, Message, Thread } from "chat";
2
2
  import { z } from "zod";
3
3
  import {
4
4
  getAgentById,
5
+ getAgentWorkingOnContextKey,
5
6
  getAllAgents,
6
7
  getLatestActiveTaskByContextKey,
7
8
  getLatestTaskByContextKey,
8
9
  getLeadAgent,
9
10
  isExtensionAgent,
10
11
  } from "@/be/db";
12
+ import { resolveIdentity } from "@/be/identity";
13
+ import { recordUnmappedIdentity } from "@/be/unmapped-identities";
11
14
  import { dispatchPre } from "@/extensions/dispatcher";
12
15
  import { resolveTemplate } from "@/prompts/resolver";
13
16
  import { chatSdkContextKey } from "@/tasks/context-key";
@@ -21,19 +24,25 @@ import {
21
24
  buildChatAttachmentText,
22
25
  createChatTaskWithFiles,
23
26
  fetchChatFiles,
27
+ formatTooLargeReply,
24
28
  toChatFiles,
29
+ transcribeChatAudio,
25
30
  } from "./attachments";
26
31
  import { getChatThreadResetAt } from "./commands";
32
+ import { type ChatAddressedReason, formatChatHeaderFacts, readChatMessageFacts } from "./facts";
27
33
  import { errorMessage } from "./logger";
28
- import { emitChatMessageSent } from "./outbound";
34
+ import { emitChatMessageSent, postChatMessage } from "./outbound";
29
35
  import {
30
36
  type ChatAgentMatch,
31
37
  type ChatRouteTarget,
38
+ chatOwnerWindowStart,
39
+ getChatChannelMapping,
40
+ hasRoutingToken,
32
41
  routeChatMessage,
33
42
  stripRoutingTokens,
34
43
  } from "./router";
35
44
  import { getChatInstance } from "./runtime";
36
- import { readPersistedThreadHistory } from "./state";
45
+ import { readPersistedThreadHistory, SqliteChatState } from "./state";
37
46
  import { formatChatSteeringAck, requestChatThreadSteering } from "./steering";
38
47
  import {
39
48
  bufferChatMessage,
@@ -109,11 +118,37 @@ export async function handleChatInbound(
109
118
  if (immediate) text = text.replace(/^!now\s*/i, "").trim();
110
119
  const attachments = message.attachments ?? [];
111
120
 
121
+ const facts = readChatMessageFacts(thread, message);
122
+ const isDirectMessage = kind === "dm" || thread.isDM;
123
+ const mentioned = kind === "mention" || message.isMention === true;
124
+ let addressed: ChatAddressedReason = isDirectMessage
125
+ ? "dm"
126
+ : facts.replyToBot
127
+ ? "reply-to-bot"
128
+ : mentioned
129
+ ? "mention"
130
+ : "none";
131
+ // A Telegram group is one shared conversation, not a thread about the task:
132
+ // an unaddressed message there is side chatter unless its sender asked for
133
+ // the thread's open task. Chatter neither steers nor reaches the router.
134
+ if (addressed === "none" && (facts.chatType === "group" || facts.chatType === "supergroup")) {
135
+ const unaddressed = await classifyUnaddressedGroupMessage(thread, message.author.userId, text);
136
+ if (unaddressed === "chatter") {
137
+ console.log(
138
+ `[chat-sdk] ${thread.id}: unaddressed group message from a non-requester, ignored`,
139
+ );
140
+ return;
141
+ }
142
+ if (unaddressed === "requester-follow-up") addressed = unaddressed;
143
+ }
144
+
112
145
  // A thread with messages already waiting keeps its order: append, do not steer past them.
146
+ // Another sender's message takes the buffer, so it gets its own header and requester.
113
147
  if (!immediate && attachments.length === 0 && text && !isChatThreadBuffered(thread.id)) {
114
148
  const steering = await requestChatThreadSteering({
115
149
  contextKey: chatSdkContextKey(thread.id),
116
150
  message: text,
151
+ resolveSender: () => resolveChatSender(adapter, message.author.userId),
117
152
  });
118
153
  if (steering) {
119
154
  console.log(
@@ -148,8 +183,10 @@ export async function handleChatInbound(
148
183
  text,
149
184
  userId: message.author.userId,
150
185
  userName: message.author.userName || undefined,
151
- botMentioned: kind === "mention" || message.isMention === true,
152
- isDirectMessage: kind === "dm" || thread.isDM,
186
+ botMentioned: mentioned || facts.replyToBot,
187
+ isDirectMessage,
188
+ addressed,
189
+ facts,
153
190
  attachments,
154
191
  });
155
192
  // Subscribed-thread messages may be dropped by the router at flush time;
@@ -159,6 +196,76 @@ export async function handleChatInbound(
159
196
  if (immediate) await instantFlushChat(thread.id);
160
197
  }
161
198
 
199
+ const REQUESTER_KEY_PREFIX = "swarm:requester:";
200
+ const REQUESTER_TTL_MS = 30 * 24 * 60 * 60 * 1000;
201
+
202
+ /** Platform user id of whoever asked for the thread's latest task. */
203
+ async function rememberChatThreadRequester(threadId: string, userId: string): Promise<void> {
204
+ try {
205
+ await new SqliteChatState().set(`${REQUESTER_KEY_PREFIX}${threadId}`, userId, REQUESTER_TTL_MS);
206
+ } catch (error) {
207
+ console.warn(`[chat-sdk] Could not store the thread requester: ${errorMessage(error)}`);
208
+ }
209
+ }
210
+
211
+ /**
212
+ * An unaddressed message outside a DM. `routable`: a routing token or an
213
+ * agent channel mapping routes it whoever sent it. `requester-follow-up`: its
214
+ * sender asked for the thread's task and the owner window is open. Anything
215
+ * else is `chatter`.
216
+ */
217
+ async function classifyUnaddressedGroupMessage(
218
+ thread: Thread,
219
+ senderId: string,
220
+ text: string,
221
+ ): Promise<"routable" | "requester-follow-up" | "chatter"> {
222
+ if (hasRoutingToken(text)) return "routable";
223
+ if ((await getChatChannelMapping(thread.adapter.name, thread.channelId))?.agentId) {
224
+ return "routable";
225
+ }
226
+ const owner = await getAgentWorkingOnContextKey(
227
+ chatSdkContextKey(thread.id),
228
+ "chat",
229
+ chatOwnerWindowStart(),
230
+ );
231
+ if (!owner) return "chatter";
232
+ const requester = await new SqliteChatState().get<string>(`${REQUESTER_KEY_PREFIX}${thread.id}`);
233
+ return requester === senderId ? "requester-follow-up" : "chatter";
234
+ }
235
+
236
+ /**
237
+ * Canonical user behind a chat sender, through the same identity registry as
238
+ * Slack: `(<adapter>, <platform user id>)` in `user_external_ids`. An unknown
239
+ * sender goes to the Unmapped tracker and the task has no requester.
240
+ */
241
+ async function resolveChatRequester(
242
+ adapter: string,
243
+ item: ChatBufferedMessage,
244
+ ): Promise<string | undefined> {
245
+ return resolveChatSender(adapter, item.userId, {
246
+ sampleEventType: "chat.message",
247
+ sampleContext: `${item.facts.from}: ${item.text}`,
248
+ });
249
+ }
250
+
251
+ /** {@link resolveChatRequester} for a bare sender id; records it as unmapped only when `sample` is set. */
252
+ async function resolveChatSender(
253
+ adapter: string,
254
+ userId: string,
255
+ sample?: { sampleEventType: string; sampleContext: string },
256
+ ): Promise<string | undefined> {
257
+ // Telegram `sender_chat` posts (anonymous admins, linked channels) are no person.
258
+ if (!userId || userId.startsWith("chat:")) return undefined;
259
+ try {
260
+ const resolution = await resolveIdentity(adapter, userId);
261
+ if (resolution.status === "resolved") return resolution.userId;
262
+ if (sample) await recordUnmappedIdentity(adapter, userId, sample);
263
+ } catch (error) {
264
+ console.warn(`[chat-sdk] Could not resolve the requester: ${errorMessage(error)}`);
265
+ }
266
+ return undefined;
267
+ }
268
+
162
269
  /**
163
270
  * Earlier messages of the thread for the task description, minus the flushed
164
271
  * ones and anything before the last `/reset`. The adapter's view is merged with
@@ -265,6 +372,11 @@ export async function flushChatThread(
265
372
  .join("\n---\n");
266
373
  const botMentioned = items.some((item) => item.botMentioned);
267
374
  const isDirectMessage = items.some((item) => item.isDirectMessage);
375
+ // The task's requester: the first sender who addressed the swarm.
376
+ const requester = items.find((item) => item.addressed !== "none") ?? first;
377
+ // One task carries every sender's text, so a batch from several people
378
+ // gets no requester: one sender's identity must not vouch for another's words.
379
+ const otherSenders = new Set(items.map((item) => item.userId)).size - 1;
268
380
 
269
381
  // After `/reset` the thread's earlier tasks are not context: the new task gets
270
382
  // no parent and no thread history older than the reset.
@@ -285,6 +397,9 @@ export async function flushChatThread(
285
397
  text: rawText,
286
398
  botMentioned,
287
399
  isDirectMessage,
400
+ ...(first.facts.chatType ? { chatType: first.facts.chatType } : {}),
401
+ ...(first.facts.isForum ? { isForum: true } : {}),
402
+ replyToBot: items.some((item) => item.facts.replyToBot),
288
403
  ...(mostRecentTask ? { threadContext: { adapter, threadId } } : {}),
289
404
  },
290
405
  {
@@ -354,7 +469,19 @@ export async function flushChatThread(
354
469
  const body = stripRoutingTokens(rawText);
355
470
  const files = items.flatMap((item) => toChatFiles(adapter, item.messageId, item.attachments));
356
471
  await using inbound = await fetchChatFiles(files);
357
- const fileText = buildChatAttachmentText(inbound.files, inbound.failed);
472
+ const tooLarge = inbound.failed.filter((failure) => failure.tooLarge);
473
+ if (tooLarge.length > 0) {
474
+ // The user would otherwise never learn the file was dropped.
475
+ try {
476
+ await postChatMessage({ adapter, threadId, text: formatTooLargeReply(tooLarge) });
477
+ } catch (error) {
478
+ console.warn(
479
+ `[chat-sdk] ${threadId}: could not post the too-large reply: ${errorMessage(error)}`,
480
+ );
481
+ }
482
+ }
483
+ const notes = await transcribeChatAudio(inbound.fetched);
484
+ const fileText = buildChatAttachmentText(inbound.files, inbound.failed, notes);
358
485
  const combined = [body, fileText].filter(Boolean).join("\n\n");
359
486
  const marked =
360
487
  items.length > 1
@@ -364,9 +491,26 @@ export async function flushChatThread(
364
491
  mostRecentTask && thread
365
492
  ? await getChatThreadContext(thread, new Set(items.map((item) => item.messageId)), resetAt)
366
493
  : "";
367
- const description = threadContext
368
- ? `${resolveTemplate("chat.message.thread_context", { thread_messages: threadContext }).text}\n\n${marked}`
369
- : marked;
494
+ const header = resolveTemplate("chat.message.header", {
495
+ chat_facts: formatChatHeaderFacts(
496
+ adapter,
497
+ otherSenders > 0
498
+ ? { ...requester.facts, from: `${requester.facts.from} +${otherSenders} more` }
499
+ : requester.facts,
500
+ requester.addressed,
501
+ ),
502
+ }).text;
503
+ const description = [
504
+ header,
505
+ threadContext
506
+ ? resolveTemplate("chat.message.thread_context", { thread_messages: threadContext }).text
507
+ : "",
508
+ marked,
509
+ ]
510
+ .filter(Boolean)
511
+ .join("\n\n");
512
+ const requestedByUserId =
513
+ otherSenders > 0 ? undefined : await resolveChatRequester(adapter, requester);
370
514
 
371
515
  const latestActive = await getLatestActiveTaskByContextKey(contextKey, "chat");
372
516
  const dependsOn = !immediate && latestActive ? [latestActive.id] : undefined;
@@ -382,7 +526,10 @@ export async function flushChatThread(
382
526
  priority: 70,
383
527
  contextKey,
384
528
  dependsOn,
529
+ requestedByUserId,
385
530
  parentTaskId: mostRecentTask?.id,
531
+ // The parent's requester asked an earlier message: an unknown sender stays unattributed.
532
+ inheritParentRequester: false,
386
533
  // A human message in the thread: never answer it in the previous task's
387
534
  // structured-output contract.
388
535
  inheritParentOutputSchema: false,
@@ -404,6 +551,7 @@ export async function flushChatThread(
404
551
  }
405
552
  }
406
553
  if (created > 0) {
554
+ await rememberChatThreadRequester(threadId, requester.userId);
407
555
  await subscribe(thread);
408
556
  // Subscribed-thread messages start typing only here, once a task exists.
409
557
  startTypingKeepalive(adapter, threadId);
@@ -19,6 +19,30 @@ export function chatMessageLimit(adapter: string): number {
19
19
  return CHAT_MESSAGE_LIMITS[adapter] ?? FALLBACK_LIMIT;
20
20
  }
21
21
 
22
+ const MB = 1024 * 1024;
23
+
24
+ /**
25
+ * Per-file outbound upload ceiling, in bytes. Every adapter is capped at
26
+ * 50 MB, the task attachment cap, because a file is read into memory before
27
+ * it is sent. Telegram's Bot API takes 50 MB per upload (photos 10 MB, see
28
+ * `TELEGRAM_PHOTO_LIMIT`); Discord takes 10 MB without a server boost.
29
+ */
30
+ const CHAT_UPLOAD_LIMITS: Record<string, number> = {
31
+ telegram: 50 * MB,
32
+ discord: 10 * MB,
33
+ };
34
+ const FALLBACK_UPLOAD_LIMIT = 50 * MB;
35
+
36
+ /** Telegram sends a larger image with `sendDocument` instead of `sendPhoto`. */
37
+ export const TELEGRAM_PHOTO_LIMIT = 10 * MB;
38
+
39
+ export function chatUploadLimit(adapter: string): number {
40
+ return CHAT_UPLOAD_LIMITS[adapter] ?? FALLBACK_UPLOAD_LIMIT;
41
+ }
42
+
43
+ /** Most files one platform message can carry (Telegram media groups, Discord). */
44
+ export const CHAT_FILES_PER_MESSAGE = 10;
45
+
22
46
  /**
23
47
  * Split `text` into chunks of at most `limit` characters. A chunk ends at the
24
48
  * last line break inside the window, else at the last space, else at the
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Files an agent sends to a chat thread: task attachments, read through the
3
+ * active file provider, and agent-fs paths, read with the caller's own agent-fs
4
+ * credential. `postChatMessage` sends them.
5
+ */
6
+ import { getResolvedConfig, getTaskAttachments } from "@/be/db";
7
+ import { backingProviderId, scopeFromAttachment } from "@/be/task-attachment-store";
8
+ import { AgentFsProvider } from "@/fs/agent-fs-provider";
9
+ import {
10
+ type FileScope,
11
+ type FileStorageProvider,
12
+ FilesError,
13
+ normalizeStoredKey,
14
+ } from "@/fs/provider";
15
+ import { getFileStorageProvider } from "@/fs/registry";
16
+ import type { TaskAttachment } from "@/types";
17
+ import { createValueScrubber } from "@/utils/secret-scrubber";
18
+ import { SHARED_FILE_INTENT } from "./attachments";
19
+ import { errorMessage } from "./logger";
20
+ import { ChatFileTooLargeError, type OutboundChatFile } from "./outbound";
21
+
22
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
23
+
24
+ /** The caller's own agent-fs credential and default drive. Never the API's `API_AGENT_FS_API_KEY`. */
25
+ export type CallerAgentFs = { apiUrl: string; apiKey: string; orgId: string; driveId: string };
26
+
27
+ async function download(
28
+ scope: FileScope,
29
+ maxBytes: number,
30
+ provider: Pick<FileStorageProvider, "download"> = getFileStorageProvider(),
31
+ ): Promise<{ data: Uint8Array; mimeType?: string }> {
32
+ const response = await provider.download(scope);
33
+ const length = Number(response.headers.get("content-length"));
34
+ if (Number.isFinite(length) && length > maxBytes) {
35
+ await response.body?.cancel();
36
+ throw new ChatFileTooLargeError(length);
37
+ }
38
+ const data = new Uint8Array(await response.arrayBuffer());
39
+ if (data.byteLength > maxBytes) throw new ChatFileTooLargeError(data.byteLength);
40
+ return { data, mimeType: response.headers.get("content-type") ?? undefined };
41
+ }
42
+
43
+ /** A task attachment as a file to send, or undefined when the active provider does not hold its bytes. */
44
+ export function chatFileOfTaskAttachment(attachment: TaskAttachment): OutboundChatFile | undefined {
45
+ if (backingProviderId(attachment) !== getFileStorageProvider().id) return undefined;
46
+ return {
47
+ name: attachment.name,
48
+ mimeType: attachment.mimeType,
49
+ sizeBytes: attachment.sizeBytes,
50
+ load: (maxBytes) => download(scopeFromAttachment(attachment), maxBytes),
51
+ };
52
+ }
53
+
54
+ /**
55
+ * The segments of a tool-supplied path, each decoded once and checked, so an
56
+ * encoded `..`, separator, `?` or `#` cannot leave the drive or change the endpoint.
57
+ */
58
+ function pathSegments(path: string): string[] {
59
+ return normalizeStoredKey(path)
60
+ .split("/")
61
+ .map((segment) => {
62
+ let decoded = segment;
63
+ try {
64
+ decoded = decodeURIComponent(segment);
65
+ } catch {
66
+ // A lone `%` is part of the name; it is encoded below.
67
+ }
68
+ if (!decoded || decoded === "." || decoded === ".." || /[/\\\0]/.test(decoded)) {
69
+ throw new FilesError(
70
+ "Provider",
71
+ "Path must not contain empty, `.` or `..` segments, or encoded separators",
72
+ );
73
+ }
74
+ return decoded;
75
+ });
76
+ }
77
+
78
+ /** The agent-fs credential and drive the caller itself holds, or undefined when it has none. */
79
+ async function callerAgentFs(agentId: string | undefined): Promise<CallerAgentFs | undefined> {
80
+ if (!agentId) return undefined;
81
+ const configs = await getResolvedConfig(agentId);
82
+ const value = (key: string) => configs.find((c) => c.key === key)?.value?.trim();
83
+ const apiUrl = value("AGENT_FS_API_URL") || process.env.AGENT_FS_API_URL?.trim();
84
+ const apiKey = value("AGENT_FS_API_KEY");
85
+ const orgId = value("AGENT_FS_DEFAULT_ORG_ID");
86
+ const driveId = value("AGENT_FS_DEFAULT_DRIVE_ID");
87
+ return apiUrl && apiKey && orgId && driveId ? { apiUrl, apiKey, orgId, driveId } : undefined;
88
+ }
89
+
90
+ /**
91
+ * A file at a path of the caller's agent-fs drive, read with the caller's own
92
+ * credential: a path the caller cannot read is not sent, and the thread gets a note.
93
+ */
94
+ export function chatFileOfPath(path: string, access: CallerAgentFs): OutboundChatFile {
95
+ const segments = pathSegments(path);
96
+ const name = segments.at(-1)!;
97
+ const key = segments.map(encodeURIComponent).join("/");
98
+ // The caller's key can be shorter than the global scrubber's floor, so a
99
+ // storage error body or transport error is scrubbed of it by exact value.
100
+ const keyScrubber = createValueScrubber([{ value: access.apiKey, name: "AGENT_FS_API_KEY" }]);
101
+ return {
102
+ name,
103
+ load: async (maxBytes) => {
104
+ try {
105
+ return await download(
106
+ { taskId: "chat", name, key, orgId: access.orgId, driveId: access.driveId },
107
+ maxBytes,
108
+ new AgentFsProvider(access),
109
+ );
110
+ } catch (error) {
111
+ if (error instanceof ChatFileTooLargeError) throw error;
112
+ if (error instanceof FilesError && error.code === "Unauthorized") {
113
+ throw new Error("your agent-fs credential cannot read this path");
114
+ }
115
+ throw new Error(keyScrubber.text(error instanceof Error ? error.message : String(error)));
116
+ }
117
+ },
118
+ };
119
+ }
120
+
121
+ /**
122
+ * Resolve the `attachments` of `chat-reply` and `chat-post`: an attachment id
123
+ * of `taskId`, or an agent-fs path read as `agentId`. Returns an error naming
124
+ * the first entry that cannot be sent, so the tool refuses before posting anything.
125
+ */
126
+ export async function resolveChatAttachmentRefs(
127
+ refs: string[],
128
+ taskId: string | undefined,
129
+ agentId: string | undefined,
130
+ ): Promise<{ files: OutboundChatFile[] } | { error: string }> {
131
+ let taskAttachments: TaskAttachment[] | undefined;
132
+ let access: CallerAgentFs | undefined | null = null;
133
+ const files: OutboundChatFile[] = [];
134
+ for (const ref of refs) {
135
+ if (UUID_PATTERN.test(ref)) {
136
+ if (!taskId) return { error: `Attachment ${ref}: no current task to look it up in.` };
137
+ taskAttachments ??= await getTaskAttachments(taskId);
138
+ const attachment = taskAttachments.find((a) => a.id === ref);
139
+ if (!attachment)
140
+ return { error: `Attachment ${ref} is not an attachment of task ${taskId}.` };
141
+ const file = chatFileOfTaskAttachment(attachment);
142
+ if (!file) {
143
+ return {
144
+ error: `Attachment ${ref} (${attachment.kind}) is not stored in the swarm file provider, so it cannot be sent as a file. Post its link instead.`,
145
+ };
146
+ }
147
+ files.push(file);
148
+ continue;
149
+ }
150
+ if (access === null) access = await callerAgentFs(agentId);
151
+ if (!access) {
152
+ return {
153
+ error: `Path ${ref}: you have no agent-fs credential and drive to read it with. Attach the file to your task and send its attachment id.`,
154
+ };
155
+ }
156
+ try {
157
+ files.push(chatFileOfPath(ref, access));
158
+ } catch (error) {
159
+ return { error: `Path ${ref}: ${errorMessage(error)}` };
160
+ }
161
+ }
162
+ return { files };
163
+ }
164
+
165
+ /** A Slack file an agent fetched into its task; recorded with that agent's id. */
166
+ const FETCHED_SLACK_FILE_INTENT = "slack-file";
167
+
168
+ /**
169
+ * The output files of a task, for the result relay: attachments an agent
170
+ * added that the active provider holds. Input files (a user's upload, a Slack
171
+ * file an agent fetched) are left out, as the Slack relay does, so they are not
172
+ * echoed back or carried into another chat thread.
173
+ */
174
+ export async function taskOutputChatFiles(taskId: string): Promise<OutboundChatFile[]> {
175
+ return (await getTaskAttachments(taskId)).flatMap((attachment) => {
176
+ if (attachment.agentId === null) return [];
177
+ if (
178
+ attachment.intent === SHARED_FILE_INTENT ||
179
+ attachment.intent === FETCHED_SLACK_FILE_INTENT
180
+ ) {
181
+ return [];
182
+ }
183
+ const file = chatFileOfTaskAttachment(attachment);
184
+ return file ? [file] : [];
185
+ });
186
+ }