@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
package/src/http/core.ts CHANGED
@@ -547,6 +547,8 @@ export async function handleCore(
547
547
  return true;
548
548
  }
549
549
  req.headers["x-runtime-instance-id"] = auth.runtimeId;
550
+ }
551
+ if (auth.tokenKind !== "task") {
550
552
  const sourceHeader = req.headers["x-source-task-id"];
551
553
  const taskId = Array.isArray(sourceHeader) ? sourceHeader[0] : sourceHeader;
552
554
  if (taskId !== undefined && (await getTaskById(taskId))?.agentId !== auth.agentId) {
package/src/http/fs.ts CHANGED
@@ -1,18 +1,25 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  import { z } from "zod";
3
3
  import { deleteTaskAttachment, getTaskAttachments, getTaskById } from "../be/db";
4
+ import { appendDraftTaskText } from "../be/db/tasks/draft-text";
4
5
  import {
5
6
  ensureAgentFsCredentialsForAgent,
6
7
  inviteEmailToSharedOrg,
7
8
  } from "../be/seed/agent-fs-provision";
8
- import { MAX_TASK_ATTACHMENT_BYTES, recordTaskAttachmentUpload } from "../be/task-attachment-store";
9
- import { type FileObject, type FileScope, FilesError, normalizeFilesError } from "../fs/provider";
9
+ import {
10
+ backingProviderId,
11
+ MAX_TASK_ATTACHMENT_BYTES,
12
+ recordTaskAttachmentUpload,
13
+ scopeFromAttachment,
14
+ } from "../be/task-attachment-store";
15
+ import { type FileObject, FilesError, normalizeFilesError } from "../fs/provider";
10
16
  import { getFileStorageProvider } from "../fs/registry";
11
17
  import { can, type RbacResource } from "../rbac";
12
18
  import { type TaskAttachment, TaskAttachmentSchema } from "../types";
13
19
  import { attachmentContentDisposition } from "../utils/content-disposition";
14
20
  import { getCurrentRequestAuth } from "../utils/request-auth-context";
15
21
  import { scrubSecrets } from "../utils/secret-scrubber";
22
+ import { transcribeAudioNotes } from "../utils/transcription";
16
23
  import { requestPrincipal } from "./request-principal";
17
24
  import { route } from "./route-def";
18
25
  import { canReadSession, SESSION_READ_DENIED } from "./session-access";
@@ -147,7 +154,7 @@ const uploadTaskFileRoute = route({
147
154
  session: { taskParam: "taskId" },
148
155
  summary: "Upload a binary task file attachment",
149
156
  description:
150
- "Accepts a raw binary request body. Pass the display/path name as the `name` query parameter.",
157
+ "Accepts a raw binary request body. Pass the display/path name as the `name` query parameter. An `audio/*` file uploaded to a `draft` task gets a `[File: …]` line and its transcript (or why there is none) appended to the task text before the response; a failed transcription never fails the upload.",
151
158
  tags: ["FS"],
152
159
  params: taskParams,
153
160
  query: uploadQuery,
@@ -331,7 +338,14 @@ export async function handleFs(
331
338
  const task = await getTaskById(parsed.params.taskId);
332
339
  if (!task) return notFound(res, "Task not found");
333
340
  if (!(await canMutateTask(task, myAgentId, req))) return forbidden(res);
334
- return sendUpload(req, res, parsed.params.taskId, parsed.query, myAgentId ?? null);
341
+ return sendUpload(
342
+ req,
343
+ res,
344
+ parsed.params.taskId,
345
+ parsed.query,
346
+ myAgentId ?? null,
347
+ task.status === "draft",
348
+ );
335
349
  }
336
350
 
337
351
  if (listTaskFilesRoute.match(req.method, pathSegments)) {
@@ -366,6 +380,7 @@ async function sendUpload(
366
380
  taskId: string,
367
381
  query: z.infer<typeof uploadQuery>,
368
382
  agentId: string | null,
383
+ isDraft: boolean,
369
384
  ): Promise<boolean> {
370
385
  const body = await readRawBody(req, MAX_TASK_ATTACHMENT_BYTES);
371
386
  if (body === BODY_TOO_LARGE) {
@@ -417,10 +432,45 @@ async function sendUpload(
417
432
  isPrimary: query.isPrimary === "true",
418
433
  createdBy: auth?.kind === "user" ? auth.userId : undefined,
419
434
  });
435
+ const mimeType = contentType.split(";")[0]!.trim().toLowerCase();
436
+ if (isDraft && mimeType.startsWith("audio/")) {
437
+ await appendAudioTranscript(taskId, query.name, mimeType, body);
438
+ }
420
439
  uploadTaskFileRoute.respond(res, 201, attachment);
421
440
  return true;
422
441
  }
423
442
 
443
+ /**
444
+ * A voice note uploaded to a draft (the composer's pre-dispatch window) gets
445
+ * its transcript in the task text, as one sent over Slack or a chat app does.
446
+ * Runs before the upload responds, so the composer promotes the draft only
447
+ * after the text is final. Never throws: the attachment has already landed.
448
+ */
449
+ async function appendAudioTranscript(
450
+ taskId: string,
451
+ name: string,
452
+ mimeType: string,
453
+ body: Buffer,
454
+ ): Promise<void> {
455
+ try {
456
+ const notes = await transcribeAudioNotes(
457
+ [{ key: name, name, data: new Blob([body], { type: mimeType }), mimeType }],
458
+ "[fs]",
459
+ );
460
+ const note = notes.get(name);
461
+ if (!note) return;
462
+ if (!(await appendDraftTaskText(taskId, `[File: ${name} (${mimeType})]\n${note}`))) {
463
+ console.warn(`[fs] task ${taskId} left draft before the transcript of ${name} was added`);
464
+ }
465
+ } catch (error) {
466
+ console.warn(
467
+ scrubSecrets(
468
+ `[fs] could not add the transcript of ${name} to task ${taskId}: ${error instanceof Error ? error.message : String(error)}`,
469
+ ),
470
+ );
471
+ }
472
+ }
473
+
424
474
  async function sendDownload(res: ServerResponse, attachment: TaskAttachment): Promise<boolean> {
425
475
  const provider = getFileStorageProvider();
426
476
  if (backingProviderId(attachment) !== provider.id) {
@@ -491,36 +541,6 @@ async function sendDelete(res: ServerResponse, attachment: TaskAttachment): Prom
491
541
  return true;
492
542
  }
493
543
 
494
- // Resolve against the row's ACTUAL stored key + its own org/drive, rather than
495
- // reconstructing `tasks/<taskId>/<name>`. Attachments written to agent-fs via the
496
- // CLI live at arbitrary paths (`misc/…`, `smoke/…`); reconstruction mis-resolves
497
- // them, which previously caused downloads to 404 and deletes to orphan the real
498
- // file. New uploads have no stored key yet at scope-build time and keep the
499
- // forward-looking layout.
500
- function scopeFromAttachment(attachment: TaskAttachment): FileScope {
501
- const storedKey = attachment.providerKey ?? attachment.path ?? "";
502
- return {
503
- taskId: attachment.taskId,
504
- name: attachment.name,
505
- key: storedKey.trim() ? storedKey : undefined,
506
- orgId: attachment.orgId,
507
- driveId: attachment.driveId,
508
- };
509
- }
510
-
511
- // Which file-storage provider (if any) actually holds this attachment's bytes.
512
- // Only agent-fs objects and local-fs uploads are provider-backed; `shared-fs`
513
- // worker-volume pointers, `url`, and `page` attachments have no provider object,
514
- // so they must never be fetched or deleted through a provider (doing so would
515
- // mis-resolve and, on delete, orphan the real target). Note local-fs uploads are
516
- // recorded with kind "shared-fs" but an explicit local-fs providerId, so key off
517
- // both signals rather than kind alone.
518
- function backingProviderId(attachment: TaskAttachment): string | null {
519
- if (attachment.kind === "agent-fs") return attachment.providerId ?? "agent-fs";
520
- if (attachment.providerId === "local-fs") return "local-fs";
521
- return null;
522
- }
523
-
524
544
  async function findAttachment(
525
545
  req: IncomingMessage,
526
546
  myAgentId: string | undefined,
@@ -0,0 +1,37 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import { MCP_MAX_REQUEST_BODY_BYTES } from "@/mcp-limits";
3
+ import { parseBody, RequestBodyTooLargeError } from "./utils";
4
+
5
+ /** Returned by `readMcpBody` after it has answered the request with 413. */
6
+ export const MCP_BODY_TOO_LARGE = Symbol("mcp-body-too-large");
7
+
8
+ /**
9
+ * Read and parse a JSON-RPC request body for an MCP HTTP handler, capped at
10
+ * `MCP_MAX_REQUEST_BODY_BYTES`. A declared Content-Length over the cap is
11
+ * refused before reading; otherwise the read stops once the cap is crossed.
12
+ * Either way the caller gets `MCP_BODY_TOO_LARGE` after a 413 with a JSON-RPC
13
+ * error body (the SDK transport's shape), and the body never reaches
14
+ * `JSON.parse`.
15
+ */
16
+ export async function readMcpBody(
17
+ req: IncomingMessage,
18
+ res: ServerResponse,
19
+ ): Promise<unknown | typeof MCP_BODY_TOO_LARGE> {
20
+ try {
21
+ return await parseBody(req, MCP_MAX_REQUEST_BODY_BYTES);
22
+ } catch (error) {
23
+ if (!(error instanceof RequestBodyTooLargeError)) throw error;
24
+ res.writeHead(413, { "Content-Type": "application/json", Connection: "close" });
25
+ res.end(
26
+ JSON.stringify({
27
+ jsonrpc: "2.0",
28
+ error: {
29
+ code: -32000,
30
+ message: `Payload Too Large: Request body must not exceed ${MCP_MAX_REQUEST_BODY_BYTES} bytes`,
31
+ },
32
+ id: null,
33
+ }),
34
+ );
35
+ return MCP_BODY_TOO_LARGE;
36
+ }
37
+ }
@@ -12,6 +12,7 @@ import {
12
12
  markMcpTransportActivity,
13
13
  respondNoMcpSession,
14
14
  } from "./mcp";
15
+ import { MCP_BODY_TOO_LARGE, readMcpBody } from "./mcp-body";
15
16
 
16
17
  function unauthorized(res: ServerResponse): true {
17
18
  res.writeHead(401, { "Content-Type": "application/json" });
@@ -59,11 +60,8 @@ export async function handleMcpUser(
59
60
  }
60
61
 
61
62
  if (req.method === "POST") {
62
- const chunks: Buffer[] = [];
63
- for await (const chunk of req) {
64
- chunks.push(chunk);
65
- }
66
- const body = JSON.parse(Buffer.concat(chunks).toString());
63
+ const body = await readMcpBody(req, res);
64
+ if (body === MCP_BODY_TOO_LARGE) return true;
67
65
 
68
66
  let transport: StreamableHTTPServerTransport;
69
67
 
package/src/http/mcp.ts CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  parseTaskToolManifest,
14
14
  selectTaskTools,
15
15
  } from "@/utils/task-tool-manifest";
16
+ import { MCP_BODY_TOO_LARGE, readMcpBody } from "./mcp-body";
16
17
 
17
18
  export type McpTransportActivity = Record<string, number>;
18
19
  export type McpSessionAgents = Record<string, string>;
@@ -166,19 +167,17 @@ export async function handleMcp(
166
167
  if (agentMismatch) return true;
167
168
 
168
169
  if (req.method === "POST") {
169
- const chunks: Buffer[] = [];
170
- for await (const chunk of req) {
171
- chunks.push(chunk);
172
- }
173
- const body = JSON.parse(Buffer.concat(chunks).toString());
170
+ const body = await readMcpBody(req, res);
171
+ if (body === MCP_BODY_TOO_LARGE) return true;
172
+ const rpc = body as { method?: unknown; id?: unknown } | null;
174
173
 
175
- if (!sessionId && body.method === "server/discover") {
174
+ if (!sessionId && rpc?.method === "server/discover") {
176
175
  res.writeHead(200, { "Content-Type": "application/json" });
177
176
  res.end(
178
177
  JSON.stringify({
179
178
  jsonrpc: "2.0",
180
179
  error: { code: -32601, message: "Method not found" },
181
- id: body.id ?? null,
180
+ id: rpc.id ?? null,
182
181
  }),
183
182
  );
184
183
  return true;
@@ -35,6 +35,7 @@ import {
35
35
  } from "../be/memory/raters/types";
36
36
  import { rerank } from "../be/memory/reranker";
37
37
  import { getRetrievalsForAgent, hasRetrievalForTask } from "../be/memory/retrieval-store";
38
+ import { searchMemoryCandidates } from "../be/memory/search-candidates";
38
39
  import { getUsefulnessStats } from "../be/memory/usefulness-stats";
39
40
  import { shouldPersistAutomaticTaskMemory } from "../memory/automatic-task-gate";
40
41
  import { buildRecallQuery } from "../memory/recall-query";
@@ -916,14 +917,19 @@ export async function handleMemory(
916
917
  const queryEmbedding = await provider.embed(query);
917
918
 
918
919
  const candidateLimit = Math.min(limit, 20) * CANDIDATE_SET_MULTIPLIER;
919
- const candidates = await store.search(queryEmbedding ?? new Float32Array(0), myAgentId, {
920
- scope,
921
- limit: candidateLimit,
922
- source,
923
- isLead: false,
924
- queryText: query,
925
- keyPrefix,
926
- });
920
+ const candidates = await searchMemoryCandidates(
921
+ store,
922
+ queryEmbedding ?? new Float32Array(0),
923
+ myAgentId,
924
+ {
925
+ scope,
926
+ limit: candidateLimit,
927
+ source,
928
+ isLead: false,
929
+ queryText: query,
930
+ keyPrefix,
931
+ },
932
+ );
927
933
  // Default-on 1-hop memory_link neighbor expansion (disable with
928
934
  // MEMORY_GRAPH_EXPANSION=0|false).
929
935
  const expanded = await expandCandidatesWithGraph(candidates, myAgentId, {
@@ -1039,13 +1045,18 @@ export async function handleMemory(
1039
1045
  4096,
1040
1046
  Math.max(offset + pageLimit, pageLimit) * CANDIDATE_SET_MULTIPLIER,
1041
1047
  );
1042
- let candidates = await store.search(queryEmbedding ?? new Float32Array(0), viewer.agentId, {
1043
- scope,
1044
- limit: candidateLimit,
1045
- isLead: viewer.seesAll,
1046
- source,
1047
- queryText: query.trim(),
1048
- });
1048
+ let candidates = await searchMemoryCandidates(
1049
+ store,
1050
+ queryEmbedding ?? new Float32Array(0),
1051
+ viewer.agentId,
1052
+ {
1053
+ scope,
1054
+ limit: candidateLimit,
1055
+ isLead: viewer.seesAll,
1056
+ source,
1057
+ queryText: query.trim(),
1058
+ },
1059
+ );
1049
1060
  if (agentId) {
1050
1061
  candidates = candidates.filter((c) => c.agentId === agentId);
1051
1062
  }
@@ -1,7 +1,11 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  import { getPage } from "../be/db";
3
3
  import { getApiKey } from "../utils/api-key";
4
- import { extractAndVerifyCookie, parseCookieHeader } from "../utils/page-session";
4
+ import {
5
+ extractLegacyPageSession,
6
+ extractPageSession,
7
+ isCookieSafePageId,
8
+ } from "../utils/page-session";
5
9
  import { route } from "./route-def";
6
10
  import { jsonError } from "./utils";
7
11
 
@@ -11,7 +15,10 @@ import { jsonError } from "./utils";
11
15
  *
12
16
  * Flow:
13
17
  * 1. Browser hits `/@swarm/api/<rest>` from inside an iframe of `/p/:id`.
14
- * 2. We parse the `page_session` cookie, verify HMAC + expiry.
18
+ * 2. The browser SDK names its page in `X-Swarm-Page-Id` (else we read the
19
+ * same-host `/p/<id>` Referer). We read that page's `page_session_<id>`
20
+ * cookie (or the legacy `page_session`), verify HMAC + expiry, and
21
+ * require the signed payload to name the same page.
15
22
  * 3. Look up the page and recover the signed viewer identity.
16
23
  * 4. Re-issue the request to the same API server's `/api/<rest>` with
17
24
  * the operator bearer plus the original signed session token. The bearer NEVER
@@ -119,6 +126,48 @@ route({
119
126
 
120
127
  const PROXY_PREFIX = "/@swarm/api/";
121
128
 
129
+ /** Request header the browser SDK uses to say which page's session to use. */
130
+ export const PAGE_ID_REQUEST_HEADER = "x-swarm-page-id";
131
+
132
+ const PAGE_PATH_RE = /^\/p\/([^/.]+)/;
133
+
134
+ /**
135
+ * Which page the caller claims to be: the `X-Swarm-Page-Id` header, else the
136
+ * `/p/<id>` path of a same-host Referer (pages that call `fetch` directly
137
+ * instead of the SDK). This only selects which cookie to read. It grants
138
+ * nothing: the cookie's signed payload must still name this page, and the
139
+ * forwarded `X-Page-Id` comes from that payload. `undefined` means the caller
140
+ * named no page (a client predating per-page cookies). `null` means it named
141
+ * one that cannot be a page id.
142
+ * Exported for tests.
143
+ */
144
+ export function requestedPageId(req: IncomingMessage): string | null | undefined {
145
+ const raw = req.headers[PAGE_ID_REQUEST_HEADER];
146
+ const fromHeader = Array.isArray(raw) ? raw[0] : raw;
147
+ if (fromHeader !== undefined) return isCookieSafePageId(fromHeader) ? fromHeader : null;
148
+
149
+ const referer = req.headers.referer;
150
+ if (!referer) return undefined;
151
+ let refererUrl: URL;
152
+ try {
153
+ refererUrl = new URL(referer);
154
+ } catch {
155
+ return undefined;
156
+ }
157
+ // A cross-site Referer must not pick a cookie: a custom header would force a
158
+ // CORS preflight, a Referer does not.
159
+ if (refererUrl.host !== req.headers.host) return undefined;
160
+ const match = PAGE_PATH_RE.exec(refererUrl.pathname);
161
+ if (!match) return undefined;
162
+ let fromReferer: string;
163
+ try {
164
+ fromReferer = decodeURIComponent(match[1]!);
165
+ } catch {
166
+ return undefined;
167
+ }
168
+ return isCookieSafePageId(fromReferer) ? fromReferer : undefined;
169
+ }
170
+
122
171
  /**
123
172
  * Reject a proxied suffix that smells like path traversal or an attempt to
124
173
  * smuggle an extra path segment via percent-encoding (e.g. `%2e%2e`, `%2f`).
@@ -194,11 +243,18 @@ export async function handlePageProxy(req: IncomingMessage, res: ServerResponse)
194
243
  }
195
244
 
196
245
  // ─── Cookie validation ────────────────────────────────────────────────────
197
- const payload = await extractAndVerifyCookie(req);
198
- if (!payload) {
246
+ const claimedPageId = requestedPageId(req);
247
+ const session =
248
+ claimedPageId === undefined
249
+ ? await extractLegacyPageSession(req)
250
+ : claimedPageId === null
251
+ ? null
252
+ : await extractPageSession(req, claimedPageId);
253
+ if (!session) {
199
254
  jsonError(res, "no page session", 401);
200
255
  return true;
201
256
  }
257
+ const { payload } = session;
202
258
 
203
259
  const page = await getPage(payload.pageId);
204
260
  if (!page) {
@@ -236,7 +292,7 @@ export async function handlePageProxy(req: IncomingMessage, res: ServerResponse)
236
292
  const headers: Record<string, string> = {
237
293
  Authorization: `Bearer ${apiKey}`,
238
294
  "X-Page-Id": page.id,
239
- "X-Page-Session": parseCookieHeader(req.headers.cookie, "page_session") ?? "",
295
+ "X-Page-Session": session.token,
240
296
  };
241
297
  // The page owner is not the browser viewer. The receiving auth layer
242
298
  // verifies this original signed token before resolving any user identity.
@@ -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",