@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
@@ -3,12 +3,40 @@ import { getOpenRouterAttributionHeaders } from "../../../utils/openrouter-base-
3
3
  import { DEFAULT_EMBEDDING_MODEL, EMBEDDING_DIMENSIONS } from "../constants";
4
4
  import type { EmbeddingProvider } from "../types";
5
5
 
6
+ // text-embedding-3-* reject any input over 8,192 tokens with a 400, and one
7
+ // such input fails every input in its request. There is no tokenizer here, and
8
+ // dense text (UUIDs, hashes, CJK) measures 1.2-1.8 chars per token, so no char
9
+ // cap is safe by itself: start at ~3 chars per token and halve on rejection.
10
+ export const MAX_EMBEDDING_INPUT_CHARS = 24_000;
11
+ const MIN_EMBEDDING_INPUT_CHARS = 1_000;
12
+ // The API also caps one request at 300,000 tokens. Even at one char per token,
13
+ // this keeps a request under it.
14
+ const MAX_BATCH_REQUEST_CHARS = 200_000;
15
+
6
16
  interface OpenAIEmbeddingConfig {
7
17
  apiKey?: string;
8
18
  model?: string;
9
19
  dimensions?: number;
10
20
  }
11
21
 
22
+ function cleanInput(text: string): string {
23
+ return text
24
+ .replace(/[\n\r]/g, " ")
25
+ .trim()
26
+ .slice(0, MAX_EMBEDDING_INPUT_CHARS);
27
+ }
28
+
29
+ /** A 400 is about the request's inputs, so the other inputs can still embed. */
30
+ function isInputRejection(err: unknown): boolean {
31
+ return err instanceof OpenAI.APIError && err.status === 400;
32
+ }
33
+
34
+ function isInputTooLong(err: unknown): boolean {
35
+ return (
36
+ isInputRejection(err) && /maximum (context|input) length/i.test((err as Error).message ?? "")
37
+ );
38
+ }
39
+
12
40
  export class OpenAIEmbeddingProvider implements EmbeddingProvider {
13
41
  readonly name: string;
14
42
  readonly dimensions: number;
@@ -47,83 +75,109 @@ export class OpenAIEmbeddingProvider implements EmbeddingProvider {
47
75
  return this.client;
48
76
  }
49
77
 
50
- async embed(text: string): Promise<Float32Array | null> {
51
- const client = this.getClient();
52
- if (!client) return null;
53
-
54
- try {
55
- const cleaned = text.replace(/[\n\r]/g, " ").trim();
56
- if (!cleaned) return null;
57
-
58
- const response = await client.embeddings.create({
59
- model: this.model,
60
- input: cleaned,
61
- dimensions: this.dimensions,
62
- // Without it the SDK asks for base64 and decodes any reply as base64. A
63
- // gateway that answers with plain floats would then yield garbage.
64
- encoding_format: "float",
65
- });
78
+ private async request(client: OpenAI, input: string | string[]) {
79
+ return client.embeddings.create({
80
+ model: this.model,
81
+ input,
82
+ dimensions: this.dimensions,
83
+ // Without it the SDK asks for base64 and decodes any reply as base64. A
84
+ // gateway that answers with plain floats would then yield garbage.
85
+ encoding_format: "float",
86
+ });
87
+ }
66
88
 
67
- const values = response.data[0]?.embedding;
68
- if (!values) return null;
89
+ private toVector(values: number[] | undefined, label: string): Float32Array | null {
90
+ if (!values) return null;
91
+ if (values.length !== this.dimensions) {
92
+ console.error(
93
+ `[memory] ${label} dimension mismatch: expected=${this.dimensions} got=${values.length}. Provider may not support the 'dimensions' parameter.`,
94
+ );
95
+ return null;
96
+ }
97
+ return new Float32Array(values);
98
+ }
69
99
 
70
- if (values.length !== this.dimensions) {
71
- console.error(
72
- `[memory] Embedding dimension mismatch: expected=${this.dimensions} got=${values.length}. Provider may not support the 'dimensions' parameter.`,
73
- );
100
+ /** Embed one cleaned input, cutting it in half each time the provider says it is too long. */
101
+ private async embedOne(client: OpenAI, cleaned: string): Promise<Float32Array | null> {
102
+ let input = cleaned;
103
+ while (true) {
104
+ try {
105
+ const response = await this.request(client, input);
106
+ return this.toVector(response.data[0]?.embedding, "Embedding");
107
+ } catch (err) {
108
+ const message = (err as Error).message;
109
+ if (isInputTooLong(err) && input.length > MIN_EMBEDDING_INPUT_CHARS) {
110
+ const next = Math.max(MIN_EMBEDDING_INPUT_CHARS, Math.floor(input.length / 2));
111
+ console.warn(
112
+ `[memory] Embedding input over the token cap at ${input.length} chars, retrying at ${next}: ${message}`,
113
+ );
114
+ input = input.slice(0, next);
115
+ continue;
116
+ }
117
+ console.error("[memory] Embedding failed:", message);
74
118
  return null;
75
119
  }
76
-
77
- return new Float32Array(values);
78
- } catch (err) {
79
- console.error("[memory] Embedding failed:", (err as Error).message);
80
- return null;
81
120
  }
82
121
  }
83
122
 
84
- async embedBatch(texts: string[]): Promise<(Float32Array | null)[]> {
123
+ async embed(text: string): Promise<Float32Array | null> {
85
124
  const client = this.getClient();
86
- if (!client) return texts.map(() => null);
125
+ if (!client) return null;
87
126
 
88
- const cleaned = texts.map((t) => t.replace(/[\n\r]/g, " ").trim());
89
- const nonEmptyIndices: number[] = [];
90
- const nonEmptyTexts: string[] = [];
127
+ const cleaned = cleanInput(text);
128
+ if (!cleaned) return null;
129
+ return this.embedOne(client, cleaned);
130
+ }
131
+
132
+ async embedBatch(texts: string[]): Promise<(Float32Array | null)[]> {
133
+ const results: (Float32Array | null)[] = texts.map(() => null);
134
+ const client = this.getClient();
135
+ if (!client) return results;
91
136
 
137
+ const cleaned = texts.map(cleanInput);
138
+ const groups: number[][] = [];
139
+ let group: number[] = [];
140
+ let groupChars = 0;
92
141
  for (let i = 0; i < cleaned.length; i++) {
93
- if (cleaned[i]) {
94
- nonEmptyIndices.push(i);
95
- nonEmptyTexts.push(cleaned[i]!);
142
+ const length = cleaned[i]!.length;
143
+ if (length === 0) continue;
144
+ if (group.length > 0 && groupChars + length > MAX_BATCH_REQUEST_CHARS) {
145
+ groups.push(group);
146
+ group = [];
147
+ groupChars = 0;
96
148
  }
149
+ group.push(i);
150
+ groupChars += length;
97
151
  }
152
+ if (group.length > 0) groups.push(group);
98
153
 
99
- if (nonEmptyTexts.length === 0) return texts.map(() => null);
100
-
101
- try {
102
- const response = await client.embeddings.create({
103
- model: this.model,
104
- input: nonEmptyTexts,
105
- dimensions: this.dimensions,
106
- encoding_format: "float",
107
- });
108
-
109
- const results: (Float32Array | null)[] = texts.map(() => null);
110
- for (const item of response.data) {
111
- const originalIndex = nonEmptyIndices[item.index];
112
- if (originalIndex !== undefined && item.embedding) {
113
- if (item.embedding.length !== this.dimensions) {
114
- console.error(
115
- `[memory] Batch embedding dimension mismatch: expected=${this.dimensions} got=${item.embedding.length}. Provider may not support the 'dimensions' parameter.`,
116
- );
117
- continue;
118
- }
119
- results[originalIndex] = new Float32Array(item.embedding);
154
+ for (const indices of groups) {
155
+ try {
156
+ const response = await this.request(
157
+ client,
158
+ indices.map((i) => cleaned[i]!),
159
+ );
160
+ for (const item of response.data) {
161
+ const originalIndex = indices[item.index];
162
+ if (originalIndex === undefined) continue;
163
+ results[originalIndex] = this.toVector(item.embedding, "Batch embedding");
164
+ }
165
+ } catch (err) {
166
+ if (!isInputRejection(err)) {
167
+ console.error("[memory] Batch embedding failed:", (err as Error).message);
168
+ continue;
169
+ }
170
+ // One rejected input fails the whole request: embed each input alone so
171
+ // only that input is cut down, and the rest embed as they are.
172
+ console.warn(
173
+ `[memory] Batch embedding rejected, embedding ${indices.length} inputs one by one: ${(err as Error).message}`,
174
+ );
175
+ for (const i of indices) {
176
+ results[i] = await this.embedOne(client, cleaned[i]!);
120
177
  }
121
178
  }
122
-
123
- return results;
124
- } catch (err) {
125
- console.error("[memory] Batch embedding failed:", (err as Error).message);
126
- return texts.map(() => null);
127
179
  }
180
+
181
+ return results;
128
182
  }
129
183
  }
@@ -12,8 +12,10 @@ import {
12
12
  } from "../constants";
13
13
  import { isLongtermKey, LONGTERM_ROOT, tierSource } from "../key-paths";
14
14
  import { recencyDecay } from "../reranker";
15
+ import { MAX_CANDIDATE_FETCH } from "../search-candidates";
15
16
  import type {
16
17
  MemoryCandidate,
18
+ MemoryDocumentRef,
17
19
  MemoryEditInput,
18
20
  MemoryEditResult,
19
21
  MemoryHealth,
@@ -29,6 +31,16 @@ import type {
29
31
 
30
32
  const VECTOR_BYTES = EMBEDDING_DIMENSIONS * Float32Array.BYTES_PER_ELEMENT;
31
33
 
34
+ /**
35
+ * Rows one hybrid or FTS arm fetches for a `limit`-row search: 4x overfetch up
36
+ * to 100, never fewer than `limit`. A fixed 100 ceiling returned the same rows
37
+ * for every wider limit, so searchMemoryCandidates read it as exhaustion while
38
+ * other documents sat behind 100 chunks of one task.
39
+ */
40
+ function armFetchLimit(limit: number): number {
41
+ return Math.min(Math.max(Math.min(limit * 4, 100), limit), MAX_CANDIDATE_FETCH);
42
+ }
43
+
32
44
  // memory_fts.memory_id is UNINDEXED (FTS5 cannot index it), so a CORRELATED
33
45
  // lookup against it is a full table scan per outer row. The old backfill used
34
46
  // `WHERE NOT EXISTS (SELECT 1 FROM memory_fts f WHERE f.memory_id = m.id)`,
@@ -512,6 +524,32 @@ export class SqliteMemoryStore implements MemoryStore {
512
524
  return results;
513
525
  }
514
526
 
527
+ async replaceDocument(
528
+ document: MemoryDocumentRef,
529
+ inputs: MemoryInput[],
530
+ ): Promise<AgentMemory[]> {
531
+ const results: AgentMemory[] = [];
532
+ await getDbClient().transaction(async (tx) => {
533
+ // Only this owner's rows of this source, in this scope, under this key.
534
+ // Another source holding the key is left alone and the insert fails.
535
+ const rows = await tx.query<{ id: string }>(
536
+ `SELECT id FROM agent_memory
537
+ WHERE key = ? AND scope = ? AND COALESCE(agentId, '') = COALESCE(?, '') AND source = ?`,
538
+ [document.key, document.scope, document.agentId ?? null, document.source],
539
+ );
540
+ const ids = rows.map((row) => row.id);
541
+ if (ids.length > 0) {
542
+ await this.purgeByIds(ids);
543
+ const placeholders = ids.map(() => "?").join(",");
544
+ await tx.run(`DELETE FROM agent_memory WHERE id IN (${placeholders})`, ids);
545
+ }
546
+ for (const input of inputs) {
547
+ results.push(await this.store(input));
548
+ }
549
+ });
550
+ return results;
551
+ }
552
+
515
553
  async get(id: string): Promise<AgentMemory | null> {
516
554
  const db = getDbClient();
517
555
  const row = await db.get<AgentMemoryRow>("SELECT * FROM agent_memory WHERE id = ?", [id]);
@@ -624,7 +662,7 @@ export class SqliteMemoryStore implements MemoryStore {
624
662
  keyPrefix?: string;
625
663
  },
626
664
  ): Promise<MemoryCandidate[]> {
627
- const overfetchLimit = Math.min(Math.max(options.limit * 4, options.limit), 100);
665
+ const overfetchLimit = armFetchLimit(options.limit);
628
666
  const vectorCandidates = await this.searchWithVec(queryEmbedding, agentId, {
629
667
  ...options,
630
668
  limit: overfetchLimit,
@@ -725,7 +763,7 @@ export class SqliteMemoryStore implements MemoryStore {
725
763
  }
726
764
 
727
765
  try {
728
- const sqlLimit = Math.min(Math.max(limit * 4, limit), 100);
766
+ const sqlLimit = armFetchLimit(limit);
729
767
  const rows = await getDbClient().query<AgentMemoryRow & { rank: number }>(
730
768
  `SELECT m.*, bm25(memory_fts) AS rank
731
769
  FROM memory_fts
@@ -1254,29 +1292,37 @@ export class SqliteMemoryStore implements MemoryStore {
1254
1292
  }
1255
1293
 
1256
1294
  async updateEmbedding(id: string, embedding: Float32Array, model: string): Promise<void> {
1257
- const db = getDbClient();
1258
1295
  const buffer = serializeEmbedding(embedding);
1259
- await db.run("UPDATE agent_memory SET embedding = ?, embeddingModel = ? WHERE id = ?", [
1260
- buffer,
1261
- model,
1262
- id,
1263
- ]);
1264
-
1265
- if (this.vecInitialized && this.getVecTableSchema()) {
1266
- const vecBuffer = this.toVecBuffer(embedding);
1267
- if (!vecBuffer) {
1268
- console.warn(
1269
- `[memory-vec] update skipped memory_id=${id} reason=invalid_dimensions dimensions=${embedding.length} expected=${EMBEDDING_DIMENSIONS}`,
1270
- );
1271
- return;
1272
- }
1273
- try {
1274
- await db.run("DELETE FROM memory_vec WHERE memory_id = ?", [id]);
1275
- await db.run("INSERT INTO memory_vec(memory_id, embedding) VALUES (?, ?)", [id, vecBuffer]);
1276
- } catch (err) {
1277
- console.error(`[memory-vec] update failed memory_id=${id}: ${(err as Error).message}`);
1296
+ // One transaction: a delete or replaceDocument queued behind this write
1297
+ // cannot land between the row UPDATE and the vec insert and orphan it.
1298
+ await getDbClient().transaction(async (tx) => {
1299
+ const updated = await tx.run(
1300
+ "UPDATE agent_memory SET embedding = ?, embeddingModel = ? WHERE id = ?",
1301
+ [buffer, model, id],
1302
+ );
1303
+ // The row was deleted (or replaced) while its embedding was in flight:
1304
+ // a vec row now would point at nothing.
1305
+ if (updated.changes === 0) return;
1306
+
1307
+ if (this.vecInitialized && this.getVecTableSchema()) {
1308
+ const vecBuffer = this.toVecBuffer(embedding);
1309
+ if (!vecBuffer) {
1310
+ console.warn(
1311
+ `[memory-vec] update skipped memory_id=${id} reason=invalid_dimensions dimensions=${embedding.length} expected=${EMBEDDING_DIMENSIONS}`,
1312
+ );
1313
+ return;
1314
+ }
1315
+ try {
1316
+ await tx.run("DELETE FROM memory_vec WHERE memory_id = ?", [id]);
1317
+ await tx.run("INSERT INTO memory_vec(memory_id, embedding) VALUES (?, ?)", [
1318
+ id,
1319
+ vecBuffer,
1320
+ ]);
1321
+ } catch (err) {
1322
+ console.error(`[memory-vec] update failed memory_id=${id}: ${(err as Error).message}`);
1323
+ }
1278
1324
  }
1279
- }
1325
+ });
1280
1326
  }
1281
1327
 
1282
1328
  async rewriteForScrub(
@@ -160,5 +160,35 @@ export function rerank(candidates: MemoryCandidate[], options: RerankOptions): M
160
160
  });
161
161
 
162
162
  scored.sort((a, b) => b.similarity - a.similarity || b.rawSimilarity - a.rawSimilarity);
163
- return scored.slice(0, limit);
163
+ return collapseTaskCompletionChunks(scored).slice(0, limit);
164
+ }
165
+
166
+ /**
167
+ * The document rerank() collapses a candidate into, or null when the candidate
168
+ * is ranked on its own (anything but a chunk of a multi-chunk task memory).
169
+ */
170
+ export function collapsedDocumentKey(
171
+ candidate: Pick<MemoryCandidate, "source" | "totalChunks" | "key" | "agentId" | "scope">,
172
+ ): string | null {
173
+ if (candidate.source !== "task_completion" || candidate.totalChunks <= 1 || !candidate.key) {
174
+ return null;
175
+ }
176
+ return JSON.stringify([candidate.agentId, candidate.scope, candidate.key]);
177
+ }
178
+
179
+ /**
180
+ * Keep only the best-ranked chunk of each multi-chunk task_completion memory.
181
+ * One long task output splits into a hundred chunks or more, which would
182
+ * otherwise fill every result slot. Chunks of one task share a key (see
183
+ * taskCompletionMemoryKey), scoped by owner and visibility.
184
+ */
185
+ function collapseTaskCompletionChunks<T extends MemoryCandidate>(ranked: T[]): T[] {
186
+ const seen = new Set<string>();
187
+ return ranked.filter((candidate) => {
188
+ const document = collapsedDocumentKey(candidate);
189
+ if (document === null) return true;
190
+ if (seen.has(document)) return false;
191
+ seen.add(document);
192
+ return true;
193
+ });
164
194
  }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Candidate fetch for memory search, sized in documents rather than rows.
3
+ *
4
+ * rerank() collapses the chunks of one task_completion document into its best
5
+ * chunk. When those chunks fill the candidate window, the collapse leaves
6
+ * fewer documents than the caller asked for while other matches sit just past
7
+ * the cutoff. This fetch widens the window until it holds `limit` documents,
8
+ * or the store has nothing more to return.
9
+ */
10
+ import { collapsedDocumentKey } from "./reranker";
11
+ import type { MemoryCandidate, MemorySearchOptions, MemoryStore } from "./types";
12
+
13
+ /** Upper bound on one widened fetch. */
14
+ export const MAX_CANDIDATE_FETCH = 4096;
15
+ const WIDEN_FACTOR = 4;
16
+
17
+ export function countCandidateDocuments(candidates: MemoryCandidate[]): number {
18
+ const documents = new Set<string>();
19
+ let standalone = 0;
20
+ for (const candidate of candidates) {
21
+ const document = collapsedDocumentKey(candidate);
22
+ if (document === null) standalone++;
23
+ else documents.add(document);
24
+ }
25
+ return standalone + documents.size;
26
+ }
27
+
28
+ export async function searchMemoryCandidates(
29
+ store: MemoryStore,
30
+ embedding: Float32Array,
31
+ agentId: string,
32
+ options: MemorySearchOptions & { limit: number },
33
+ ): Promise<MemoryCandidate[]> {
34
+ let fetchLimit = options.limit;
35
+ let rows = await store.search(embedding, agentId, { ...options, limit: fetchLimit });
36
+ while (fetchLimit < MAX_CANDIDATE_FETCH) {
37
+ const documents = countCandidateDocuments(rows);
38
+ // Enough documents, or no chunk duplicates for the collapse to remove.
39
+ if (documents >= options.limit || documents === rows.length) break;
40
+ const nextLimit = Math.min(fetchLimit * WIDEN_FACTOR, MAX_CANDIDATE_FETCH);
41
+ const wider = await store.search(embedding, agentId, { ...options, limit: nextLimit });
42
+ if (wider.length <= rows.length) break; // the store is exhausted
43
+ rows = wider;
44
+ fetchLimit = nextLimit;
45
+ }
46
+ return rows;
47
+ }
@@ -20,6 +20,12 @@ export interface EmbeddingProvider {
20
20
  export interface MemoryStore {
21
21
  store(input: MemoryInput): Promise<AgentMemory>;
22
22
  storeBatch(inputs: MemoryInput[]): Promise<AgentMemory[]>;
23
+ /**
24
+ * In one transaction, delete every row of `document` (same key, scope,
25
+ * owner and source) and insert `inputs`. Rows of other owners, scopes or
26
+ * sources are never touched.
27
+ */
28
+ replaceDocument(document: MemoryDocumentRef, inputs: MemoryInput[]): Promise<AgentMemory[]>;
23
29
  get(id: string): Promise<AgentMemory | null>;
24
30
  peek(id: string): Promise<AgentMemory | null>;
25
31
  search(
@@ -79,6 +85,14 @@ export interface MemoryInput {
79
85
  key?: string | null;
80
86
  }
81
87
 
88
+ /** The rows of one keyed document: every chunk shares these four fields. */
89
+ export interface MemoryDocumentRef {
90
+ key: string;
91
+ scope: AgentMemoryScope;
92
+ agentId: string | null;
93
+ source: AgentMemorySource;
94
+ }
95
+
82
96
  export interface MemoryCandidate extends AgentMemory {
83
97
  similarity: number;
84
98
  /** Raw cosine similarity before reranking (preserved for diagnostics). */
@@ -0,0 +1,41 @@
1
+ PRAGMA defer_foreign_keys = ON;
2
+
3
+ -- Add agent-bound worker tokens: a fourth binding that names only the agent.
4
+ -- parentTokenId links a child task token to the agent token that minted it,
5
+ -- so revoking the agent token revokes its children.
6
+ CREATE TABLE session_tokens_new (
7
+ id TEXT PRIMARY KEY,
8
+ tokenHash TEXT NOT NULL UNIQUE,
9
+ tokenPreview TEXT NOT NULL,
10
+ agentId TEXT NOT NULL,
11
+ taskId TEXT,
12
+ runtimeId TEXT,
13
+ parentRuntimeId TEXT,
14
+ controllerId TEXT,
15
+ parentTokenId TEXT,
16
+ expiresAt TEXT NOT NULL,
17
+ revokedAt TEXT,
18
+ createdAt TEXT NOT NULL DEFAULT (datetime('now')),
19
+ lastUsedAt TEXT,
20
+ CHECK (
21
+ (taskId IS NOT NULL AND runtimeId IS NULL AND controllerId IS NULL
22
+ AND (parentRuntimeId IS NULL OR parentTokenId IS NULL))
23
+ OR (taskId IS NULL AND runtimeId IS NOT NULL AND parentRuntimeId IS NULL AND controllerId IS NULL AND parentTokenId IS NULL)
24
+ OR (controllerId IS NOT NULL AND taskId IS NULL AND runtimeId IS NULL AND parentRuntimeId IS NULL AND parentTokenId IS NULL)
25
+ OR (taskId IS NULL AND runtimeId IS NULL AND controllerId IS NULL AND parentRuntimeId IS NULL AND parentTokenId IS NULL)
26
+ )
27
+ );
28
+
29
+ INSERT INTO session_tokens_new
30
+ (id, tokenHash, tokenPreview, agentId, taskId, runtimeId, parentRuntimeId, controllerId,
31
+ expiresAt, revokedAt, createdAt, lastUsedAt)
32
+ SELECT id, tokenHash, tokenPreview, agentId, taskId, runtimeId, parentRuntimeId, controllerId,
33
+ expiresAt, revokedAt, createdAt, lastUsedAt
34
+ FROM session_tokens;
35
+
36
+ DROP TABLE session_tokens;
37
+ ALTER TABLE session_tokens_new RENAME TO session_tokens;
38
+ CREATE INDEX idx_session_tokens_agentId ON session_tokens(agentId);
39
+ CREATE INDEX idx_session_tokens_runtimeId ON session_tokens(runtimeId);
40
+ CREATE INDEX idx_session_tokens_parentRuntimeId ON session_tokens(parentRuntimeId);
41
+ CREATE INDEX idx_session_tokens_parentTokenId ON session_tokens(parentTokenId);
@@ -55,6 +55,8 @@ export type AgentTaskStepConfig = {
55
55
  routingNote?: string;
56
56
  tags?: string[];
57
57
  priority?: number;
58
+ /** Task IDs that must complete before this task starts. */
59
+ dependsOn?: string[];
58
60
  offerMode?: boolean;
59
61
  dir?: string;
60
62
  vcsRepo?: string;