@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
@@ -13,7 +13,7 @@
13
13
  * 302-redirect to the SPA `/pages/:id` route (the JSON renderer lives
14
14
  * in the SPA, not the API — step-6/7).
15
15
  * - `auth_mode === 'authed'`: returns 401. step-4 narrows this to also
16
- * accept a valid `page_session` cookie.
16
+ * accept a valid `page_session_<id>` cookie.
17
17
  * - `auth_mode === 'password'`: returns 401. step-5 narrows this to also
18
18
  * accept `?key=` query param + HTTP Basic.
19
19
  *
@@ -33,7 +33,7 @@ import {
33
33
  import { type Page, PageSchema } from "../types";
34
34
  import { getAppUrl, getConfiguredAppUrls } from "../utils/constants";
35
35
  import { getNodeEnv } from "../utils/node-env";
36
- import { extractAndVerifyCookie, issuePageSessionCookie } from "../utils/page-session";
36
+ import { extractAndVerifyCookie, pageSessionSetCookies } from "../utils/page-session";
37
37
  import { scrubSecrets } from "../utils/secret-scrubber";
38
38
  import { route } from "./route-def";
39
39
 
@@ -357,16 +357,15 @@ function buildCsp(): string {
357
357
  /**
358
358
  * Decide whether a page is reachable based on cookie alone. Public pages are
359
359
  * always reachable; authed AND password pages can ALSO pass via a valid
360
- * `page_session` cookie scoped to the same page id (the cookie is the proof
361
- * once the user has successfully unlocked once). For password pages, when
362
- * the cookie is absent/invalid the caller falls through to the `?key=` +
363
- * Basic-auth resolution path; for authed pages the caller surfaces 401.
360
+ * `page_session_<id>` cookie (or the legacy `page_session`) whose signed
361
+ * payload names this page (the cookie is the proof once the user has
362
+ * successfully unlocked once). For password pages, when the cookie is
363
+ * absent/invalid the caller falls through to the `?key=` + Basic-auth
364
+ * resolution path; for authed pages the caller surfaces 401.
364
365
  *
365
366
  * The caller passes the cookie payload (already verified by
366
- * `extractAndVerifyCookie`) — `null` when no cookie was sent or it failed
367
- * verification. Cross-page reuse (cookie for page A presented for page B)
368
- * surfaces as a distinct `403` so misconfigurations are debuggable, NOT a
369
- * generic 401.
367
+ * `extractAndVerifyCookie`, which ignores sessions for other pages) — `null`
368
+ * when no session for this page was sent or it failed verification.
370
369
  *
371
370
  * For password pages with no/bad cookie, returns `{ ok: false, status: 401,
372
371
  * needsPassword: true }` so the handler knows to try the password flow before
@@ -374,7 +373,7 @@ function buildCsp(): string {
374
373
  */
375
374
  type AccessResult =
376
375
  | { ok: true }
377
- | { ok: false; status: 401 | 403; reason: string; needsPassword?: boolean };
376
+ | { ok: false; status: 401; reason: string; needsPassword?: boolean };
378
377
 
379
378
  function isAccessible(
380
379
  page: Page,
@@ -382,27 +381,9 @@ function isAccessible(
382
381
  ): AccessResult {
383
382
  if (page.authMode === "public") return { ok: true };
384
383
 
385
- // Cookie-first path. A cookie scoped to a DIFFERENT page id is "stale" —
386
- // for password mode we silently ignore it and fall through to the password
387
- // flow so the user can recover via `?key=` / Basic without manual cookie
388
- // clearing. For authed mode we surface 403 (the SPA's launch-retry path
389
- // handles recovery; direct browser access to authed pages is rare).
390
- if (cookiePayload) {
391
- if (cookiePayload.pageId === page.id) return { ok: true };
392
- if (page.authMode === "password") {
393
- return {
394
- ok: false,
395
- status: 401,
396
- reason: "password required",
397
- needsPassword: true,
398
- };
399
- }
400
- return {
401
- ok: false,
402
- status: 403,
403
- reason: "page-session cookie scoped to a different page id",
404
- };
405
- }
384
+ // Cookie-first path. Sessions for other pages never reach here, so an open
385
+ // tab of page A no longer fails once page B is launched in the same browser.
386
+ if (cookiePayload?.pageId === page.id) return { ok: true };
406
387
 
407
388
  if (page.authMode === "authed") {
408
389
  return {
@@ -523,7 +504,7 @@ export async function handlePagesPublic(
523
504
  // Pull + verify the page-session cookie ONCE — `null` covers "no cookie",
524
505
  // "tampered signature", "expired", "malformed". The access decision below
525
506
  // discriminates per-authMode.
526
- const cookiePayload = await extractAndVerifyCookie(req);
507
+ const cookiePayload = await extractAndVerifyCookie(req, page.id);
527
508
 
528
509
  const access = isAccessible(page, cookiePayload);
529
510
 
@@ -531,7 +512,7 @@ export async function handlePagesPublic(
531
512
  // password flow mints a fresh cookie inline. Empty string = no cookie to
532
513
  // attach. We thread this through the handler so the existing 200-response
533
514
  // paths below can opt-in without duplicating their branch logic.
534
- let inlineSetCookie = "";
515
+ let inlineSetCookie: string[] = [];
535
516
 
536
517
  if (!access.ok) {
537
518
  // Password flow: cookie missing/invalid, try `?key=` then Basic.
@@ -547,7 +528,7 @@ export async function handlePagesPublic(
547
528
  matched = false;
548
529
  }
549
530
  if (matched) {
550
- inlineSetCookie = await issuePageSessionCookie(page.id, {
531
+ inlineSetCookie = await pageSessionSetCookies(req, page.id, {
551
532
  dev: isLocalhostRequest(req),
552
533
  });
553
534
  // fall through to the regular 200 path below.
@@ -586,7 +567,7 @@ export async function handlePagesPublic(
586
567
  // that `respond()` writes via `res.writeHead()` — Node gives the latter
587
568
  // precedence, which is the value we want here anyway.
588
569
  res.setHeader("Cache-Control", "no-store");
589
- if (inlineSetCookie) res.setHeader("Set-Cookie", inlineSetCookie);
570
+ if (inlineSetCookie.length > 0) res.setHeader("Set-Cookie", inlineSetCookie);
590
571
  publicPageJsonRoute.respond(res, 200, {
591
572
  id: page.id,
592
573
  version: 1, // edit-counter is API-internal; SPA reads via /api/pages/:id/versions
@@ -602,13 +583,13 @@ export async function handlePagesPublic(
602
583
 
603
584
  // SVG stays byte-for-byte intact, including when ?print=1 is supplied.
604
585
  if (page.contentType === "image/svg+xml") {
605
- const headers: Record<string, string> = {
586
+ const headers: Record<string, string | string[]> = {
606
587
  "Content-Type": "image/svg+xml",
607
588
  "Cache-Control": page.authMode === "public" ? "public, max-age=1800" : "private, no-store",
608
589
  "X-Content-Type-Options": "nosniff",
609
590
  "Content-Security-Policy": "default-src 'none'; style-src 'unsafe-inline'; sandbox",
610
591
  };
611
- if (inlineSetCookie) headers["Set-Cookie"] = inlineSetCookie;
592
+ if (inlineSetCookie.length > 0) headers["Set-Cookie"] = inlineSetCookie;
612
593
  res.writeHead(200, headers);
613
594
  res.end(page.body);
614
595
  await bumpViewCount(page.id);
@@ -625,20 +606,22 @@ export async function handlePagesPublic(
625
606
  // `/p/:id` — render either HTML directly or 302→SPA for JSON.
626
607
  if (page.contentType === "application/json") {
627
608
  if (wantsPrint) {
628
- const headers: Record<string, string> = {
609
+ const headers: Record<string, string | string[]> = {
629
610
  "Content-Type": "text/html; charset=utf-8",
630
611
  "Cache-Control": "no-store",
631
612
  "Content-Security-Policy": buildCsp(),
632
613
  "X-Content-Type-Options": "nosniff",
633
614
  };
634
- if (inlineSetCookie) headers["Set-Cookie"] = inlineSetCookie;
615
+ if (inlineSetCookie.length > 0) headers["Set-Cookie"] = inlineSetCookie;
635
616
  res.writeHead(200, headers);
636
617
  res.end(printableJsonPageHtml(page));
637
618
  await bumpViewCount(page.id);
638
619
  return true;
639
620
  }
640
- const headers: Record<string, string> = { Location: `${getAppBaseUrl()}/pages/${page.id}` };
641
- if (inlineSetCookie) headers["Set-Cookie"] = inlineSetCookie;
621
+ const headers: Record<string, string | string[]> = {
622
+ Location: `${getAppBaseUrl()}/pages/${page.id}`,
623
+ };
624
+ if (inlineSetCookie.length > 0) headers["Set-Cookie"] = inlineSetCookie;
642
625
  res.writeHead(302, headers);
643
626
  res.end();
644
627
  // 302 redirects are intentionally NOT counted — they're a stop-over for
@@ -656,14 +639,14 @@ export async function handlePagesPublic(
656
639
  injectBrowserSdk(page.body, feedback ? feedbackKeyGuardScript() : "") +
657
640
  (wantsPrint ? PRINT_AUTOTRIGGER_SCRIPT : "") +
658
641
  (feedback ? feedbackOverlayScripts(page.id) : "");
659
- const headers: Record<string, string> = {
642
+ const headers: Record<string, string | string[]> = {
660
643
  "Content-Type": "text/html; charset=utf-8",
661
644
  "Cache-Control": "no-store",
662
645
  "Content-Security-Policy": buildCsp(),
663
646
  // Defence-in-depth: prevent MIME sniffing and clickjacking outside the SPA.
664
647
  "X-Content-Type-Options": "nosniff",
665
648
  };
666
- if (inlineSetCookie) headers["Set-Cookie"] = inlineSetCookie;
649
+ if (inlineSetCookie.length > 0) headers["Set-Cookie"] = inlineSetCookie;
667
650
  res.writeHead(200, headers);
668
651
  res.end(html);
669
652
  await bumpViewCount(page.id);
package/src/http/pages.ts CHANGED
@@ -36,7 +36,7 @@ import {
36
36
  } from "../types";
37
37
  import { getAppUrl, getPublicMcpBaseUrl } from "../utils/constants";
38
38
  import { getNodeEnv } from "../utils/node-env";
39
- import { issuePageSessionCookie } from "../utils/page-session";
39
+ import { pageSessionSetCookies } from "../utils/page-session";
40
40
  import { getRequestAuth } from "../utils/request-auth-context";
41
41
  import { resolveHttpFavoriteOwner } from "./favorite-owner";
42
42
  import { route } from "./route-def";
@@ -175,7 +175,7 @@ const resolvePageRoute = route({
175
175
  * `?key=` query / HTTP Basic on `/p/:id` directly (step-5). Bearer-side
176
176
  * issuance would bypass the password check entirely.
177
177
  *
178
- * Response: 204 No Content + `Set-Cookie: page_session=<signed>; HttpOnly; ...`.
178
+ * Response: 204 No Content + `Set-Cookie: page_session_<id>=<signed>; HttpOnly; ...`.
179
179
  */
180
180
  const launchPageRoute = route({
181
181
  method: "post",
@@ -981,13 +981,13 @@ export async function handlePages(
981
981
  : auth?.kind === "operator"
982
982
  ? { name: `guest-${randomUUID().slice(0, 8)}`, op: true as const }
983
983
  : undefined;
984
- const cookie = await issuePageSessionCookie(page.id, {
984
+ const cookies = await pageSessionSetCookies(req, page.id, {
985
985
  dev: isDevRequest(req),
986
986
  ...identity,
987
987
  });
988
988
 
989
989
  applyLaunchCors(req, res);
990
- res.setHeader("Set-Cookie", cookie);
990
+ res.setHeader("Set-Cookie", cookies);
991
991
  res.writeHead(204);
992
992
  res.end();
993
993
  return true;
@@ -101,6 +101,7 @@ const agentTaskBodySchema = z.object({
101
101
  routingNote: z.string().max(200).optional(),
102
102
  tags: z.array(z.string()).optional(),
103
103
  priority: z.number().int().min(0).max(100).optional(),
104
+ dependsOn: z.array(z.uuid()).optional(),
104
105
  offerMode: z.boolean().optional(),
105
106
  dir: z.string().min(1).optional(),
106
107
  vcsRepo: z.string().min(1).optional(),
@@ -655,6 +656,7 @@ export async function handleScriptRuns(
655
656
  : undefined,
656
657
  tags: parsed.body.tags,
657
658
  priority: parsed.body.priority,
659
+ dependsOn: parsed.body.dependsOn,
658
660
  offeredTo: parsed.body.offerMode ? parsed.body.agentId : undefined,
659
661
  taskType: "script-run-step",
660
662
  source: "mcp",
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));
@@ -0,0 +1,120 @@
1
+ import type { Message, Thread } from "chat";
2
+
3
+ /** Why a message counts as meant for the swarm. `none`: it reached the router unaddressed. */
4
+ export type ChatAddressedReason =
5
+ | "dm"
6
+ | "mention"
7
+ | "reply-to-bot"
8
+ | "requester-follow-up"
9
+ | "none";
10
+
11
+ /**
12
+ * Facts about one inbound message that only core can read: the chat it came
13
+ * from (Telegram `message.raw.chat`) and whether it answers the bot.
14
+ */
15
+ export interface ChatMessageFacts {
16
+ /** Platform chat type, such as Telegram `private`, `group` or `supergroup`. */
17
+ chatType?: string;
18
+ isForum?: boolean;
19
+ /** Group or channel title. */
20
+ title?: string;
21
+ /** Forum topic: `"<name>"` when the update carries it, else `#<thread id>`. */
22
+ topic?: string;
23
+ /** The sender as people know them: `Name (@username)`. */
24
+ from: string;
25
+ /** The message replies to one of the bot's own messages. */
26
+ replyToBot: boolean;
27
+ /** Start of the message it replies to. */
28
+ replyToSnippet?: string;
29
+ }
30
+
31
+ const SNIPPET_MAX = 80;
32
+
33
+ interface TelegramRawMessage {
34
+ message_id?: number;
35
+ message_thread_id?: number;
36
+ is_topic_message?: boolean;
37
+ chat?: { type?: string; title?: string; is_forum?: boolean };
38
+ from?: { first_name?: string; last_name?: string; username?: string };
39
+ reply_to_message?: {
40
+ message_id?: number;
41
+ from?: { id?: number | string };
42
+ text?: string;
43
+ caption?: string;
44
+ forum_topic_created?: { name?: string };
45
+ };
46
+ }
47
+
48
+ function snippet(text: string | undefined): string | undefined {
49
+ const flat = text?.replace(/\s+/g, " ").trim();
50
+ if (!flat) return undefined;
51
+ return flat.length > SNIPPET_MAX ? `${flat.slice(0, SNIPPET_MAX)}...` : flat;
52
+ }
53
+
54
+ function personLabel(name: string | undefined, username: string | undefined, fallback: string) {
55
+ if (name && username && name !== username) return `${name} (@${username})`;
56
+ if (username) return `@${username}`;
57
+ return name || fallback;
58
+ }
59
+
60
+ function telegramFacts(raw: TelegramRawMessage, message: Message, botUserId?: string) {
61
+ const reply = raw.reply_to_message;
62
+ // In a forum topic every plain message replies to the topic's creation
63
+ // message, so that reply is the topic, not an answer to anyone.
64
+ const replyIsTopicRoot =
65
+ reply !== undefined &&
66
+ (reply.forum_topic_created !== undefined ||
67
+ (raw.is_topic_message === true && reply.message_id === raw.message_thread_id));
68
+ const realReply = reply && !replyIsTopicRoot ? reply : undefined;
69
+ const topicName = replyIsTopicRoot ? reply?.forum_topic_created?.name : undefined;
70
+ const name = [raw.from?.first_name, raw.from?.last_name].filter(Boolean).join(" ").trim();
71
+ return {
72
+ chatType: raw.chat?.type,
73
+ isForum: raw.chat?.is_forum === true ? true : undefined,
74
+ title: raw.chat?.title,
75
+ topic:
76
+ raw.is_topic_message === true && raw.message_thread_id !== undefined
77
+ ? topicName
78
+ ? `"${topicName}"`
79
+ : `#${raw.message_thread_id}`
80
+ : undefined,
81
+ from: personLabel(name || undefined, raw.from?.username, message.author.userId),
82
+ replyToBot:
83
+ realReply?.from?.id !== undefined &&
84
+ botUserId !== undefined &&
85
+ String(realReply.from.id) === botUserId,
86
+ replyToSnippet: snippet(realReply?.text ?? realReply?.caption),
87
+ } satisfies ChatMessageFacts;
88
+ }
89
+
90
+ /** Read the chat facts of one message. Telegram reads the raw update; other adapters the SDK message. */
91
+ export function readChatMessageFacts(thread: Thread, message: Message): ChatMessageFacts {
92
+ const raw = message.raw as TelegramRawMessage | undefined;
93
+ if (thread.adapter.name === "telegram" && raw && typeof raw === "object") {
94
+ return telegramFacts(raw, message, thread.adapter.botUserId);
95
+ }
96
+ const { fullName, userName, userId } = message.author;
97
+ return {
98
+ chatType: thread.isDM ? "dm" : undefined,
99
+ from: personLabel(fullName || undefined, userName || undefined, userId),
100
+ replyToBot: message.replyTo?.author.isMe === true,
101
+ replyToSnippet: snippet(message.replyTo?.text),
102
+ };
103
+ }
104
+
105
+ /** One line of facts for the task header: `telegram · supergroup "Life" · from: ... · addressed: ...`. */
106
+ export function formatChatHeaderFacts(
107
+ adapter: string,
108
+ facts: ChatMessageFacts,
109
+ addressed: ChatAddressedReason,
110
+ ): string {
111
+ // Title, topic and sender are user-set: one line each, so none can forge a header line.
112
+ const oneLine = (text: string) => text.replace(/\s+/g, " ").trim();
113
+ const parts = [adapter];
114
+ if (facts.chatType)
115
+ parts.push(facts.title ? `${facts.chatType} "${oneLine(facts.title)}"` : facts.chatType);
116
+ if (facts.isForum) parts.push(facts.topic ? `forum topic ${oneLine(facts.topic)}` : "forum");
117
+ parts.push(`from: ${oneLine(facts.from)}`, `addressed: ${addressed}`);
118
+ if (facts.replyToSnippet) parts.push(`reply-to: "${facts.replyToSnippet}"`);
119
+ return parts.join(" · ");
120
+ }