@desplega.ai/agent-swarm 1.157.0 → 1.158.0

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 (256) hide show
  1. package/dist/{acp-adapter-qhqrk4q0.js → acp-adapter-xaq4n6pc.js} +1 -1
  2. package/dist/{actions-9rrg0zxm.js → actions-p9hz5yat.js} +15 -15
  3. package/dist/{app-enysy1zd.js → app-g26wdcz6.js} +4 -4
  4. package/dist/{artifact-td4q4dch.js → artifact-3v5t9jtg.js} +1 -1
  5. package/dist/{assistant-zegamy48.js → assistant-wydfqkb3.js} +16 -16
  6. package/dist/{boot-reembed-r6shx5qx.js → boot-reembed-g5ckg9yb.js} +6 -6
  7. package/dist/{boot-reembed-zfxb937k.js → boot-reembed-se8zmdcp.js} +5 -5
  8. package/dist/{boot-scrub-logs-k5y0se6r.js → boot-scrub-logs-wfxqan61.js} +3 -3
  9. package/dist/{seed-pricing-vqxa06qf.js → boot-seeds-s3q01xj3.js} +10 -10
  10. package/dist/{claude-adapter-5yc7hxwv.js → claude-adapter-tv2x4f36.js} +7 -7
  11. package/dist/{claude-managed-adapter-ea2aga6w.js → claude-managed-adapter-41b2s7fd.js} +2 -2
  12. package/dist/{claude-sdk-session-j4ywn3xf.js → claude-sdk-session-t442qz6f.js} +8 -8
  13. package/dist/{cli-e781c6f9.js → cli-0xjkj1j7.js} +1 -0
  14. package/dist/{cli-58rgvkt4.js → cli-1hkrch3k.js} +2 -2
  15. package/dist/{cli-yscm6cv8.js → cli-20cj37vt.js} +1866 -9
  16. package/dist/{cli-h0wdadsx.js → cli-4198qc9v.js} +7 -3
  17. package/dist/{cli-awwphhcs.js → cli-47m6dkd3.js} +362 -126
  18. package/dist/{cli-wb2zvbfg.js → cli-53sz8gkz.js} +1 -1
  19. package/dist/{cli-005pze6s.js → cli-5d10vd2m.js} +1 -1
  20. package/dist/{cli-v53rbqp3.js → cli-65hjmev5.js} +57 -17
  21. package/dist/{cli-scwq51gn.js → cli-68w09sjw.js} +7 -1
  22. package/dist/{cli-s98skd5r.js → cli-7gtvvpbh.js} +4 -4
  23. package/dist/{cli-y3nxwx0t.js → cli-7s3syk22.js} +18 -11
  24. package/dist/{cli-74h6f2bp.js → cli-8qmme6vd.js} +2 -2
  25. package/dist/{cli-wsed24mk.js → cli-987hg0aa.js} +2 -2
  26. package/dist/{cli-9n76jszt.js → cli-9httdpyq.js} +1 -1
  27. package/dist/{cli-f3va018d.js → cli-aky8tctm.js} +26 -93
  28. package/dist/{cli-sgyqyyxe.js → cli-c07r9a2m.js} +2 -1
  29. package/dist/{cli-c41wjx0r.js → cli-fgqxwrpv.js} +291 -12
  30. package/dist/{cli-01vfmq8c.js → cli-ftfvggzx.js} +607 -164
  31. package/dist/{cli-ytxs7mn1.js → cli-g8djp6qe.js} +17 -0
  32. package/dist/{cli-avkd6mqv.js → cli-h967nb6r.js} +551 -0
  33. package/dist/{cli-r9ee9c4w.js → cli-j55n64b6.js} +23 -22
  34. package/dist/{cli-9p33cz9h.js → cli-j57qkrpq.js} +86 -28
  35. package/dist/{cli-tjmghmm5.js → cli-q67y4wzm.js} +6 -3
  36. package/dist/{cli-n6zq3kbd.js → cli-rj19hc34.js} +1 -1
  37. package/dist/{cli-jytrw2na.js → cli-rjehvrts.js} +4 -4
  38. package/dist/{cli-tpv97m71.js → cli-rqz8qbw7.js} +245 -75
  39. package/dist/{cli-p37sg0a9.js → cli-s8eet254.js} +1 -1
  40. package/dist/cli-t1ze64h3.js +811 -0
  41. package/dist/{cli-6jjzxv3b.js → cli-ws6rqycm.js} +3 -2
  42. package/dist/{cli-xf5b7y1f.js → cli-wscdvcqz.js} +13 -5
  43. package/dist/{cli-5j7bm4tb.js → cli-x24pshtq.js} +5 -5
  44. package/dist/{cli-nbp6m6gp.js → cli-xberxa1r.js} +51 -26
  45. package/dist/{cli-vx9p3qdt.js → cli-xhaeg2nc.js} +1 -1
  46. package/dist/cli.js +29 -15
  47. package/dist/{codex-adapter-gns3s6c3.js → codex-adapter-cnm8wk11.js} +5 -5
  48. package/dist/{codex-session-runner-gqnxys5w.js → codex-session-runner-gfg547zj.js} +5 -5
  49. package/dist/{commands-hxy2y2sp.js → commands-e3bn0p75.js} +3 -3
  50. package/dist/{db-w8ghhsgf.js → db-03v9kzr2.js} +17 -3
  51. package/dist/{dispatcher-cp1rknjz.js → dispatcher-nrmg8yht.js} +14 -14
  52. package/dist/{dsh-adapter-1dwg97zv.js → dsh-adapter-k03nfqa6.js} +2 -2
  53. package/dist/{handlers-egfhr351.js → handlers-aqcedbyb.js} +17 -17
  54. package/dist/{hook-x4kftwzt.js → hook-7vkskkre.js} +19 -11
  55. package/dist/{hook-fbxjqfjz.js → hook-ybvmrmhq.js} +4 -4
  56. package/dist/{http-c7jfx990.js → http-2eqa8t24.js} +432 -195
  57. package/dist/{index-4k9hct21.js → index-1yks3qcr.js} +14 -14
  58. package/dist/{index-jtvpgb8f.js → index-a3vn96n8.js} +30 -27
  59. package/dist/{index-dc2ek6bw.js → index-c6zsv1bd.js} +5 -5
  60. package/dist/{index-yy07ef2s.js → index-chgdkq36.js} +14 -14
  61. package/dist/{index-r6bty6tb.js → index-x71bvq0p.js} +15 -15
  62. package/dist/{keepalive-f7ekefyc.js → keepalive-vtkf5n99.js} +5 -5
  63. package/dist/{lead-h053syts.js → lead-h3sp357x.js} +26 -26
  64. package/dist/{maintenance-a3w6zs54.js → maintenance-jzeqtk3q.js} +6 -6
  65. package/dist/{oauth-refresh-sweep-vra1dhkd.js → oauth-refresh-sweep-qp3dzmdp.js} +4 -4
  66. package/dist/{onboard-mfgzeqxs.js → onboard-tmsp0qbz.js} +278 -81
  67. package/dist/{opencode-adapter-z64xjzec.js → opencode-adapter-x6x2da1z.js} +3 -3
  68. package/dist/{otel-impl-2ez6xk57.js → otel-impl-9mz73q65.js} +416 -301
  69. package/dist/{pi-mono-adapter-2b7mw16r.js → pi-mono-adapter-m6dvv0dz.js} +2 -2
  70. package/dist/{pricing-refresh-6sw65tck.js → pricing-refresh-v1cbr9te.js} +5 -5
  71. package/dist/{rbac-roles-nm6924sy.js → rbac-roles-c9wqz83x.js} +4 -4
  72. package/dist/{render-v2-f026j6c4.js → render-v2-gryrh9h8.js} +4 -4
  73. package/dist/{setup-w606fr1q.js → setup-q9720bt6.js} +2 -2
  74. package/dist/{virtual-modules-8qheft18.js → virtual-modules-3htpgn8x.js} +1 -1
  75. package/dist/{work-objects-ezmggdne.js → work-objects-b76x7deb.js} +17 -17
  76. package/dist/{worker-1mbyv29k.js → worker-6vrxbv41.js} +26 -26
  77. package/openapi.json +1626 -629
  78. package/package.json +2 -1
  79. package/src/apps/catalog.generated.json +229 -0
  80. package/src/apps/page-validator.ts +71 -16
  81. package/src/apps/row-store.ts +66 -11
  82. package/src/apps/sync.ts +59 -17
  83. package/src/artifact-sdk/browser-sdk.ts +436 -2
  84. package/src/be/approval-sweeps.ts +109 -0
  85. package/src/be/boot-seeds.ts +40 -0
  86. package/src/be/chunking.ts +1 -1
  87. package/src/be/db/agents.ts +20 -0
  88. package/src/be/db/approvals.ts +78 -0
  89. package/src/be/db/tasks/read.ts +197 -39
  90. package/src/be/db/tasks/write.ts +41 -9
  91. package/src/be/db-client.ts +107 -48
  92. package/src/be/db.ts +219 -76
  93. package/src/be/events.ts +33 -1
  94. package/src/be/memory/graph-expansion.ts +10 -3
  95. package/src/be/memory/providers/sqlite-store.ts +55 -13
  96. package/src/be/memory/raters/retrieval.ts +5 -2
  97. package/src/be/memory/reranker.ts +26 -2
  98. package/src/be/memory/types.ts +2 -0
  99. package/src/be/migrations/166_approval_request_auto_cancellation.sql +9 -0
  100. package/src/be/migrations/171_memory_retrieval_relevance.sql +8 -0
  101. package/src/be/migrations/175_workflow_runs_list_index.sql +11 -0
  102. package/src/be/migrations/177_events_event_agent_created_index.sql +11 -0
  103. package/src/be/models-catalog.ts +2 -0
  104. package/src/be/modelsdev-cache.json +1857 -0
  105. package/src/be/modelsdev-cache.ts +14 -1
  106. package/src/be/pricing-normalize.ts +2 -0
  107. package/src/be/scripts/typecheck.ts +1 -0
  108. package/src/be/seed-pricing.ts +3 -1
  109. package/src/be/seed-skills/bundled-files.generated.json +1 -1
  110. package/src/be/steering.ts +53 -3
  111. package/src/be/swarm-config-guard.ts +3 -0
  112. package/src/be/task-citations.ts +29 -0
  113. package/src/cli.tsx +14 -1
  114. package/src/commands/onboard/compose-generator.ts +8 -3
  115. package/src/commands/onboard/container-engine.ts +220 -0
  116. package/src/commands/onboard/manifest.ts +1 -0
  117. package/src/commands/onboard/steps/health-check.tsx +4 -1
  118. package/src/commands/onboard/steps/prereq-check.tsx +50 -54
  119. package/src/commands/onboard/steps/start.tsx +31 -16
  120. package/src/commands/onboard/types.ts +7 -2
  121. package/src/commands/onboard.tsx +39 -4
  122. package/src/commands/profile-sync.ts +79 -26
  123. package/src/commands/runner.ts +8 -1
  124. package/src/extensions/catalog.generated.json +17 -0
  125. package/src/extensions/contract-types.generated.ts +1 -1
  126. package/src/heartbeat/heartbeat.ts +263 -103
  127. package/src/hooks/hook.ts +21 -12
  128. package/src/http/agents.ts +35 -0
  129. package/src/http/approval-requests.ts +136 -64
  130. package/src/http/apps.ts +14 -3
  131. package/src/http/config.ts +15 -1
  132. package/src/http/events.ts +62 -2
  133. package/src/http/index.ts +6 -4
  134. package/src/http/memory.ts +21 -7
  135. package/src/http/schedules.ts +1 -1
  136. package/src/http/session-data.ts +9 -4
  137. package/src/http/sessions.ts +25 -18
  138. package/src/http/stats.ts +19 -7
  139. package/src/http/status.ts +11 -1
  140. package/src/http/tasks.ts +40 -8
  141. package/src/http/webhooks.ts +67 -46
  142. package/src/http/workflows.ts +15 -20
  143. package/src/linear/outbound.ts +37 -15
  144. package/src/otel-impl.ts +151 -1
  145. package/src/otel.ts +14 -3
  146. package/src/prompts/memories.ts +19 -2
  147. package/src/providers/claude-managed-models.ts +8 -0
  148. package/src/providers/modelsdev-reasoning.json +551 -0
  149. package/src/rbac/legacy-policy.ts +16 -0
  150. package/src/rbac/permissions.ts +4 -0
  151. package/src/scheduler/schedule-task.ts +3 -0
  152. package/src/scripts-runtime/sdk-allowlist.ts +1 -0
  153. package/src/scripts-runtime/swarm-sdk.ts +9 -0
  154. package/src/scripts-runtime/types/stdlib.d.ts +1 -0
  155. package/src/scripts-runtime/types/swarm-extension.d.ts +1 -0
  156. package/src/scripts-runtime/types/swarm-sdk.d.ts +1 -0
  157. package/src/server.ts +12 -7
  158. package/src/slack/app.ts +35 -4
  159. package/src/slack/connection-state.ts +17 -0
  160. package/src/tasks/context-key.ts +1 -0
  161. package/src/tasks/worker-follow-up.ts +6 -2
  162. package/src/tests/agent-task-activity.test.ts +74 -0
  163. package/src/tests/agents-list-model-display.test.ts +6 -1
  164. package/src/tests/approval-auto-cancellation-migration.test.ts +104 -0
  165. package/src/tests/approval-requests.test.ts +395 -0
  166. package/src/tests/approval-sweeps-config.test.ts +47 -0
  167. package/src/tests/approval-sweeps.test.ts +508 -0
  168. package/src/tests/apps-kibo-blocks.test.ts +77 -0
  169. package/src/tests/apps-spike.test.ts +56 -1
  170. package/src/tests/apps-sync-engine.test.ts +20 -9
  171. package/src/tests/boot-seeds.test.ts +170 -0
  172. package/src/tests/cancel-approval-request-tool.test.ts +115 -0
  173. package/src/tests/claude-managed-adapter.test.ts +6 -0
  174. package/src/tests/config-key-filter.test.ts +80 -0
  175. package/src/tests/context-window.test.ts +1 -0
  176. package/src/tests/db-client-busy-retry.test.ts +96 -0
  177. package/src/tests/db-client.test.ts +60 -1
  178. package/src/tests/db-task-read-contract.test.ts +10 -0
  179. package/src/tests/db-task-write-contract.test.ts +9 -0
  180. package/src/tests/docs-index-footprint.test.js +3 -3
  181. package/src/tests/events-http.test.ts +103 -0
  182. package/src/tests/extensions-catalog.test.ts +22 -17
  183. package/src/tests/extensions-pre-task.test.ts +107 -0
  184. package/src/tests/fixtures/kibo-blocks-definition.json.txt +105 -0
  185. package/src/tests/follow-up-config-inheritance.test.ts +260 -0
  186. package/src/tests/heartbeat-orphan-resume.test.ts +84 -0
  187. package/src/tests/heartbeat-reboot-live.test.ts +95 -0
  188. package/src/tests/heartbeat-stall-cas.test.ts +80 -0
  189. package/src/tests/heartbeat.test.ts +99 -3
  190. package/src/tests/hook-mcp-config.test.ts +29 -0
  191. package/src/tests/http-api-integration.test.ts +16 -2
  192. package/src/tests/kv-storage.test.ts +62 -0
  193. package/src/tests/linear-outbound-sync.test.ts +120 -5
  194. package/src/tests/list-endpoint-slimming.test.ts +91 -0
  195. package/src/tests/memory-graph-organic-route.test.ts +155 -0
  196. package/src/tests/memory-http-recall-gating.test.ts +14 -4
  197. package/src/tests/memory-hybrid.test.ts +161 -1
  198. package/src/tests/memory-prompt-recall-crowdout.test.ts +297 -0
  199. package/src/tests/memory.test.ts +22 -0
  200. package/src/tests/onboard-container-engine.test.ts +240 -0
  201. package/src/tests/otel-event-loop-and-logs.test.ts +99 -0
  202. package/src/tests/pricing-routes.test.ts +14 -0
  203. package/src/tests/profile-sync-warning.test.ts +8 -4
  204. package/src/tests/promote-draft-task-route.test.ts +19 -0
  205. package/src/tests/provider-registration.test.ts +296 -42
  206. package/src/tests/rbac-engine.test.ts +30 -0
  207. package/src/tests/reasoning-effort.test.ts +8 -0
  208. package/src/tests/script-connections.test.ts +7 -0
  209. package/src/tests/sdk-allowlist.test.ts +39 -0
  210. package/src/tests/session-costs-model-key-normalize.test.ts +1 -0
  211. package/src/tests/sessions-detail-batch.test.ts +250 -0
  212. package/src/tests/sessions.test.ts +118 -0
  213. package/src/tests/slack-config.test.ts +22 -0
  214. package/src/tests/sonnet-5-5-registry.test.ts +44 -0
  215. package/src/tests/status.test.ts +36 -1
  216. package/src/tests/swarm-diff.test.ts +316 -10
  217. package/src/tests/task-completion-idempotency.test.ts +2 -1
  218. package/src/tests/tasks-requested-by-filter.test.ts +3 -1
  219. package/src/tests/tool-annotations.test.ts +1 -1
  220. package/src/tests/ui-list-slim-projections.test.ts +158 -0
  221. package/src/tests/webhooks-task-blocked.test.ts +127 -0
  222. package/src/tests/workflow-approval-recovery.test.ts +203 -0
  223. package/src/tests/workflow-hitl-routing.test.ts +22 -1
  224. package/src/tests/workflow-hitl-slack-card.test.ts +52 -0
  225. package/src/tests/workflow-http-v2.test.ts +99 -8
  226. package/src/tests/workflow-retry-validation.test.ts +56 -0
  227. package/src/tests/workflow-tla-races.test.ts +389 -0
  228. package/src/tests/workflow-validation-port-routing.test.ts +113 -1
  229. package/src/tools/cancel-approval-request.ts +57 -0
  230. package/src/tools/memory-search.ts +1 -0
  231. package/src/tools/request-human-input.ts +4 -2
  232. package/src/tools/send-task.ts +4 -0
  233. package/src/tools/templates.ts +19 -0
  234. package/src/tools/tool-config.ts +2 -1
  235. package/src/tools/workflows/list-workflow-runs.ts +1 -0
  236. package/src/types.ts +38 -0
  237. package/src/utils/context-window.ts +1 -0
  238. package/src/workflows/approval-cancel.ts +152 -0
  239. package/src/workflows/approval-notifications.ts +84 -0
  240. package/src/workflows/checkpoint.ts +10 -5
  241. package/src/workflows/definition.ts +21 -0
  242. package/src/workflows/engine.ts +152 -21
  243. package/src/workflows/executors/human-in-the-loop.ts +1 -1
  244. package/src/workflows/recovery.ts +100 -45
  245. package/src/workflows/resume.ts +49 -18
  246. package/src/workflows/retry-poller.ts +71 -21
  247. package/templates/extensions/README.md +1 -0
  248. package/templates/extensions/github-sender-allowlist/README.md +18 -0
  249. package/templates/extensions/github-sender-allowlist/hooks.ts +55 -0
  250. package/templates/extensions/github-sender-allowlist/manifest.json +8 -0
  251. package/templates/skills/apps/content.md +20 -1
  252. package/templates/skills/code-quality/content.md +26 -1
  253. package/templates/skills/pages/content.md +16 -4
  254. package/templates/skills/pages/files/examples/multiplayer-board.html +3 -12
  255. package/dist/cli-2daksqps.js +0 -378
  256. package/dist/rbac-roles-3gxmnb20.js +0 -37
@@ -573,7 +573,8 @@ describe("Tasks", () => {
573
573
  expect(status).toBe(200);
574
574
  expect(body.tasks).toBeDefined();
575
575
  expect(Array.isArray(body.tasks)).toBe(true);
576
- expect(body.total).toBeGreaterThanOrEqual(2);
576
+ // `total` is an opt-in COUNT(*): omitted unless `includeTotal=true`.
577
+ expect(body.total).toBeUndefined();
577
578
  });
578
579
 
579
580
  test("GET /api/tasks?status=pending — filter by status", async () => {
@@ -599,12 +600,25 @@ describe("Tasks", () => {
599
600
  });
600
601
 
601
602
  test("GET /api/tasks?limit=1 — pagination", async () => {
602
- const { status, body } = await get("/api/tasks?limit=1");
603
+ const { status, body } = await get("/api/tasks?limit=1&includeTotal=true");
603
604
  expect(status).toBe(200);
604
605
  expect(body.tasks.length).toBe(1);
605
606
  expect(body.total).toBeGreaterThanOrEqual(2);
606
607
  });
607
608
 
609
+ test("GET /api/tasks?fields=timeline — narrow timeline rows", async () => {
610
+ const { status, body } = await get("/api/tasks?fields=timeline&orderBy=createdAt&limit=5");
611
+ expect(status).toBe(200);
612
+ expect(body.tasks.length).toBeGreaterThanOrEqual(1);
613
+ for (const t of body.tasks) {
614
+ expect(typeof t.id).toBe("string");
615
+ expect(typeof t.status).toBe("string");
616
+ expect(t.progress).toBeUndefined();
617
+ expect(t.tags).toBeUndefined();
618
+ }
619
+ expect(body.total).toBeUndefined();
620
+ });
621
+
608
622
  test("GET /api/tasks/:id — get specific task", async () => {
609
623
  const { status, body } = await get(`/api/tasks/${ids.task}`);
610
624
  expect(status).toBe(200);
@@ -9,7 +9,9 @@ import {
9
9
  getKv,
10
10
  incrKv,
11
11
  initDb,
12
+ type KvJsonFieldEquals,
12
13
  KvTypeCollisionError,
14
+ kvPrefixUpperBound,
13
15
  listKv,
14
16
  sweepExpiredKv,
15
17
  sweepExpiredKvPrefix,
@@ -200,6 +202,66 @@ describe("kv-storage helpers", () => {
200
202
  expect(exact.map((e) => e.key)).toEqual(["x_1"]);
201
203
  });
202
204
 
205
+ test("listKv prefix is an exact, case-sensitive range on the primary key", async () => {
206
+ for (const key of ["m/row/1", "m/row/2", "M/row/3", "m/rowx", "m/row", "n/row/1", "m/row/é"]) {
207
+ await upsertKv({ namespace: NS, key, value: 1, valueType: "integer" });
208
+ }
209
+ const rows = await listKv(NS, { prefix: "m/row/", limit: 100, offset: 0 });
210
+ expect(rows.map((e) => e.key)).toEqual(["m/row/1", "m/row/2", "m/row/é"]);
211
+ expect(await countKv(NS, { prefix: "m/row/" })).toBe(3);
212
+ expect(kvPrefixUpperBound("m/row/")).toBe("m/row0");
213
+ expect(kvPrefixUpperBound(`a${String.fromCodePoint(0x10ffff)}`)).toBe("b");
214
+ expect(kvPrefixUpperBound(String.fromCodePoint(0xd7ff))).toBe(String.fromCodePoint(0xe000));
215
+
216
+ const plan = await getDbClient().query<{ detail: string }>(
217
+ `EXPLAIN QUERY PLAN SELECT key FROM kv_entries
218
+ WHERE namespace = ? AND (expires_at IS NULL OR expires_at > ?) AND key >= ? AND key < ?`,
219
+ [NS, 0, "m/row/", "m/row0"],
220
+ );
221
+ expect(plan.map((row) => row.detail).join(" ")).toContain("key>? AND key<?");
222
+ });
223
+
224
+ test("listKv jsonFieldEquals narrows rows in SQL and skips corrupt JSON", async () => {
225
+ await upsertKv({
226
+ namespace: NS,
227
+ key: "r/1",
228
+ value: { run: "a", ok: true, n: 2 },
229
+ valueType: "json",
230
+ });
231
+ await upsertKv({
232
+ namespace: NS,
233
+ key: "r/2",
234
+ value: { run: "b", ok: false, n: 2 },
235
+ valueType: "json",
236
+ });
237
+ await upsertKv({
238
+ namespace: NS,
239
+ key: "r/3",
240
+ value: { run: "a", ok: false, n: 3.5 },
241
+ valueType: "json",
242
+ });
243
+ await getDbClient().run(
244
+ "INSERT INTO kv_entries (namespace, key, value, value_type) VALUES (?, ?, ?, 'json')",
245
+ [NS, "r/4", "{not json"],
246
+ );
247
+ const keys = async (jsonFieldEquals: KvJsonFieldEquals[]) =>
248
+ (await listKv(NS, { prefix: "r/", limit: 100, offset: 0, jsonFieldEquals })).map(
249
+ (e) => e.key,
250
+ );
251
+ expect(await keys([])).toEqual(["r/1", "r/2", "r/3", "r/4"]);
252
+ expect(await keys([{ field: "run", value: "a" }])).toEqual(["r/1", "r/3"]);
253
+ expect(await keys([{ field: "ok", value: true }])).toEqual(["r/1"]);
254
+ // Non-safe-integer numbers are not prefiltered (the caller re-checks).
255
+ expect(await keys([{ field: "n", value: 3.5 }])).toEqual(["r/1", "r/2", "r/3", "r/4"]);
256
+ expect(
257
+ await keys([
258
+ { field: "run", value: "a" },
259
+ { field: "n", value: 2 },
260
+ ]),
261
+ ).toEqual(["r/1"]);
262
+ expect(await keys([{ field: "missing", value: "a" }])).toEqual([]);
263
+ });
264
+
203
265
  test("incrKv creates from missing", async () => {
204
266
  const entry = await incrKv(NS, "counter", 3);
205
267
  expect(entry.value).toBe(3);
@@ -2,7 +2,12 @@ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, mock, tes
2
2
  import { unlink } from "node:fs/promises";
3
3
  import { closeDb, initDb } from "../be/db";
4
4
  import { createTrackerSync, getTrackerSync, updateTrackerSync } from "../be/db-queries/tracker";
5
- import { initLinearOutboundSync, teardownLinearOutboundSync } from "../linear/outbound";
5
+ import {
6
+ formatLinearOutcomeBody,
7
+ initLinearOutboundSync,
8
+ LINEAR_BODY_MAX,
9
+ teardownLinearOutboundSync,
10
+ } from "../linear/outbound";
6
11
  import { taskSessionMap } from "../linear/sync";
7
12
  import { workflowEventBus } from "../workflows/event-bus";
8
13
 
@@ -79,8 +84,8 @@ describe("Linear Outbound Sync", () => {
79
84
  const callArgs = mockCreateComment.mock.calls[0] as unknown[];
80
85
  const arg = callArgs[0] as { issueId: string; body: string };
81
86
  expect(arg.issueId).toBe("LIN-OUT-COMPLETED");
82
- expect(arg.body).toContain("Task completed");
83
- expect(arg.body).toContain("All done!");
87
+ // The output is the whole comment: no boilerplate, no collapsed `+++` section.
88
+ expect(arg.body).toBe("All done!");
84
89
 
85
90
  // Verify sync record updated
86
91
  const updated = await getTrackerSync("linear", "task", "outbound-task-completed");
@@ -107,8 +112,118 @@ describe("Linear Outbound Sync", () => {
107
112
  const callArgs = mockCreateComment.mock.calls[0] as unknown[];
108
113
  const arg = callArgs[0] as { issueId: string; body: string };
109
114
  expect(arg.issueId).toBe("LIN-OUT-FAILED");
110
- expect(arg.body).toContain("Task failed");
111
- expect(arg.body).toContain("Build error in module X");
115
+ expect(arg.body).toBe("Task failed.\n\nBuild error in module X");
116
+ });
117
+
118
+ test("task.completed with an AgentSession posts ONE response activity carrying the full output", async () => {
119
+ const taskId = "outbound-session-completed";
120
+ await createTrackerSync({
121
+ provider: "linear",
122
+ entityType: "task",
123
+ swarmId: taskId,
124
+ externalId: "LIN-OUT-SESSION-COMPLETED",
125
+ syncDirection: "bidirectional",
126
+ });
127
+ taskSessionMap.set(taskId, "session-completed");
128
+ // Longer than the old 2000-char cut, shorter than the Linear body cap.
129
+ const output = `## Answer\n\n${"x".repeat(5000)}`;
130
+
131
+ workflowEventBus.emit("task.completed", { taskId, output });
132
+ await new Promise((resolve) => setTimeout(resolve, 10));
133
+
134
+ expect(mockEndAgentSession).toHaveBeenCalledTimes(1);
135
+ expect(mockEndAgentSession.mock.calls[0] as unknown[]).toEqual([
136
+ "session-completed",
137
+ output,
138
+ "response",
139
+ ]);
140
+ expect(mockCreateComment).not.toHaveBeenCalled();
141
+ expect(taskSessionMap.has(taskId)).toBe(false);
142
+ });
143
+
144
+ test("task.failed with an AgentSession posts ONE error activity with the plain reason", async () => {
145
+ const taskId = "outbound-session-failed";
146
+ await createTrackerSync({
147
+ provider: "linear",
148
+ entityType: "task",
149
+ swarmId: taskId,
150
+ externalId: "LIN-OUT-SESSION-FAILED",
151
+ syncDirection: "bidirectional",
152
+ });
153
+ taskSessionMap.set(taskId, "session-failed");
154
+
155
+ workflowEventBus.emit("task.failed", { taskId, failureReason: "tsc:check failed" });
156
+ await new Promise((resolve) => setTimeout(resolve, 10));
157
+
158
+ expect(mockEndAgentSession).toHaveBeenCalledTimes(1);
159
+ expect(mockEndAgentSession.mock.calls[0] as unknown[]).toEqual([
160
+ "session-failed",
161
+ "Task failed.\n\ntsc:check failed",
162
+ "error",
163
+ ]);
164
+ expect(mockCreateComment).not.toHaveBeenCalled();
165
+ });
166
+
167
+ test("task.completed with no output still ends the session with a short body", async () => {
168
+ const taskId = "outbound-session-empty";
169
+ await createTrackerSync({
170
+ provider: "linear",
171
+ entityType: "task",
172
+ swarmId: taskId,
173
+ externalId: "LIN-OUT-SESSION-EMPTY",
174
+ syncDirection: "bidirectional",
175
+ });
176
+ taskSessionMap.set(taskId, "session-empty");
177
+
178
+ workflowEventBus.emit("task.completed", { taskId, output: " " });
179
+ await new Promise((resolve) => setTimeout(resolve, 10));
180
+
181
+ expect(mockEndAgentSession.mock.calls[0] as unknown[]).toEqual([
182
+ "session-empty",
183
+ "Task completed.",
184
+ "response",
185
+ ]);
186
+ });
187
+
188
+ describe("formatLinearOutcomeBody", () => {
189
+ const savedDashboard = process.env.SWARM_DASHBOARD_URL;
190
+ const savedApp = process.env.APP_URL;
191
+
192
+ afterEach(() => {
193
+ if (savedDashboard === undefined) delete process.env.SWARM_DASHBOARD_URL;
194
+ else process.env.SWARM_DASHBOARD_URL = savedDashboard;
195
+ if (savedApp === undefined) delete process.env.APP_URL;
196
+ else process.env.APP_URL = savedApp;
197
+ });
198
+
199
+ test("returns text at the cap unchanged", () => {
200
+ const text = "a".repeat(LINEAR_BODY_MAX);
201
+ expect(formatLinearOutcomeBody(text, "t1")).toBe(text);
202
+ });
203
+
204
+ test("caps oversized text with a visible note linking the swarm task", () => {
205
+ delete process.env.SWARM_DASHBOARD_URL;
206
+ process.env.APP_URL = "https://swarm.example.com/";
207
+ const text = "b".repeat(LINEAR_BODY_MAX + 500);
208
+
209
+ const body = formatLinearOutcomeBody(text, "task-123");
210
+
211
+ expect(body.length).toBeLessThanOrEqual(LINEAR_BODY_MAX);
212
+ expect(body.startsWith("b".repeat(1000))).toBe(true);
213
+ expect(body).toContain(`of ${text.length} characters`);
214
+ expect(body.endsWith("Full text: https://swarm.example.com/tasks/task-123_")).toBe(true);
215
+ expect(body).not.toContain("+++");
216
+ });
217
+
218
+ test("names the task id when no dashboard URL is configured", () => {
219
+ delete process.env.SWARM_DASHBOARD_URL;
220
+ delete process.env.APP_URL;
221
+
222
+ const body = formatLinearOutcomeBody("c".repeat(LINEAR_BODY_MAX + 1), "task-456");
223
+
224
+ expect(body.length).toBeLessThanOrEqual(LINEAR_BODY_MAX);
225
+ expect(body.endsWith("Full text: swarm task `task-456`_")).toBe(true);
226
+ });
112
227
  });
113
228
 
114
229
  test("no-op when no tracker_sync mapping exists", async () => {
@@ -11,6 +11,7 @@ import {
11
11
  getAgentById,
12
12
  getAllAgents,
13
13
  getAllTasks,
14
+ getDbClient,
14
15
  getScheduledTasks,
15
16
  initDb,
16
17
  listAllPages,
@@ -227,6 +228,96 @@ describe("list-endpoint slimming", () => {
227
228
  expect(full?.totalCostUsd).toBeCloseTo(0.0168, 6);
228
229
  });
229
230
 
231
+ test("getAllTasks — SQL-side preview matches truncating the full text", async () => {
232
+ // The SQL cut counts UTF-8 bytes while JS slices UTF-16 units, so pin the
233
+ // boundary with astral (surrogate-pair) and multibyte characters. SQLite's
234
+ // text functions stop at an embedded NUL, so pin that too, plus a leading
235
+ // BOM a default TextDecoder would strip. The BOM goes in as SQL bytes:
236
+ // bun:sqlite drops a leading BOM when it binds a string.
237
+ const agentId = "preview-parity-agent";
238
+ const marker = "preview-parity-";
239
+ const texts = [
240
+ `${marker}${"a".repeat(285)}`, // exactly 300 units
241
+ `${marker}${"a".repeat(286)}`, // 301 units
242
+ `${marker}${"😀".repeat(200)}`,
243
+ `${marker}${"é".repeat(284)}😀`,
244
+ `${marker}${"ñ".repeat(500)}`,
245
+ `${marker}short`,
246
+ "a\u0000b",
247
+ `\u0000${"x".repeat(350)}`,
248
+ ];
249
+ for (const text of texts) await createTaskExtended(text, { agentId });
250
+ const bom = await createTaskExtended(`${marker}bom`, { agentId });
251
+ texts.push(`\uFEFF${marker}bom`);
252
+ await getDbClient().run(
253
+ "UPDATE agent_tasks SET task = CAST(x'EFBBBF' AS TEXT) || task WHERE id = ?",
254
+ [bom.id],
255
+ );
256
+
257
+ const full = await getAllTasks({ agentId, limit: 50 });
258
+ expect(full.map((t) => t.task).sort()).toEqual([...texts].sort());
259
+ for (const opts of [{ slim: true }, { fields: "timeline" as const }]) {
260
+ const rows = await getAllTasks({ agentId, limit: 50 }, opts);
261
+ expect(rows.map((t) => t.id)).toEqual(full.map((t) => t.id));
262
+ for (const [i, row] of full.entries()) {
263
+ const expected = row.task.length > 300 ? `${row.task.slice(0, 300)}…` : row.task;
264
+ expect(rows[i]!.task).toBe(expected);
265
+ }
266
+ }
267
+ });
268
+
269
+ test("getAllTasks — slim and full pages hold the same rows in the same order", async () => {
270
+ for (let i = 0; i < 12; i++) {
271
+ await createTaskExtended(`page-parity ${i}`, { agentId: "slim-agent-1" });
272
+ }
273
+ for (const orderBy of ["lastUpdatedAt", "createdAt"] as const) {
274
+ for (const offset of [0, 5, 10]) {
275
+ const filters = { search: "page-parity", limit: 5, offset, orderBy };
276
+ const slimIds = (await getAllTasks(filters, { slim: true })).map((t) => t.id);
277
+ const fullIds = (await getAllTasks(filters)).map((t) => t.id);
278
+ const timelineIds = (await getAllTasks(filters, { fields: "timeline" })).map((t) => t.id);
279
+ expect(slimIds).toEqual(fullIds);
280
+ expect(timelineIds).toEqual(fullIds);
281
+ }
282
+ }
283
+ });
284
+
285
+ test("getAllTasks — timeline projection carries only what the timeline draws", async () => {
286
+ const task = await createTaskExtended(`timeline-row ${"T".repeat(1000)}`, {
287
+ agentId: "slim-agent-1",
288
+ tags: ["timeline"],
289
+ });
290
+ await createSessionCost({
291
+ sessionId: "timeline-cost-session",
292
+ taskId: task.id,
293
+ agentId: "slim-agent-1",
294
+ totalCostUsd: 0.25,
295
+ durationMs: 1000,
296
+ numTurns: 1,
297
+ model: "test-model",
298
+ });
299
+
300
+ const [row] = await getAllTasks({ search: "timeline-row", limit: 1 }, { fields: "timeline" });
301
+ expect(Object.keys(row!).sort()).toEqual(
302
+ [
303
+ "agentId",
304
+ "createdAt",
305
+ "finishedAt",
306
+ "id",
307
+ "lastUpdatedAt",
308
+ "parentTaskId",
309
+ "peakContextTokens",
310
+ "status",
311
+ "task",
312
+ "title",
313
+ "totalCostUsd",
314
+ ].sort(),
315
+ );
316
+ expect(row!.id).toBe(task.id);
317
+ expect(row!.task).toBe(`${task.task.slice(0, 300)}…`);
318
+ expect(row!.totalCostUsd).toBeCloseTo(0.25, 6);
319
+ });
320
+
230
321
  test("listRecentSessions — slim root is a truncated task summary", async () => {
231
322
  const longText = "Q".repeat(2000);
232
323
  await createTaskExtended(longText, { agentId: "slim-agent-1" });
@@ -0,0 +1,155 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
+ import { randomUUID } from "node:crypto";
3
+ import { unlink } from "node:fs/promises";
4
+ import { closeDb, createAgent, getDbClient, initDb } from "../be/db";
5
+ import { expandCandidatesWithGraph } from "../be/memory/graph-expansion";
6
+ import { SqliteMemoryStore } from "../be/memory/providers/sqlite-store";
7
+ import { computeScore, rerank } from "../be/memory/reranker";
8
+
9
+ // PR #1689 review on 2e8c2f24: expansion replaced an organic duplicate with
10
+ // its graph route on the UNCAPPED graph score, then rerank() capped that route
11
+ // at a low-quality parent's composite. The neighbour's stronger organic match
12
+ // was gone. Fresh rows, default boosts, link strength 1, vector-only arm so
13
+ // each similarity is the fixture cosine:
14
+ // - task_completion parent: relevance 0.90, composite 0.90 x 0.7 = 0.63
15
+ // - manual neighbour, also a direct match: relevance 0.60, composite 0.90
16
+ // - unrelated manual row: relevance 0.54, composite 0.81
17
+
18
+ const TEST_DB_PATH = "./test-memory-graph-organic-route.sqlite";
19
+ const agentId = randomUUID();
20
+
21
+ /** Unit vector: `cos` on axis 2 (the query axis), the remainder on `axis`. */
22
+ function mix(cos: number, axis: number): Float32Array {
23
+ const embedding = new Float32Array(512);
24
+ embedding[2] = cos;
25
+ embedding[axis] = Math.sqrt(1 - cos * cos);
26
+ return embedding;
27
+ }
28
+
29
+ const queryEmbedding = mix(1, 3);
30
+
31
+ let parentId: string;
32
+ let neighborId: string;
33
+ let otherId: string;
34
+ let prevHybridFlag: string | undefined;
35
+ let prevGraphFlag: string | undefined;
36
+
37
+ async function searchAndExpand() {
38
+ const candidates = await new SqliteMemoryStore().search(queryEmbedding, agentId, {
39
+ scope: "all",
40
+ limit: 15,
41
+ });
42
+ const expanded = await expandCandidatesWithGraph(candidates, agentId, { scope: "all" });
43
+ return { candidates, expanded };
44
+ }
45
+
46
+ beforeAll(async () => {
47
+ prevHybridFlag = process.env.MEMORY_HYBRID_SEARCH;
48
+ prevGraphFlag = process.env.MEMORY_GRAPH_EXPANSION;
49
+ process.env.MEMORY_HYBRID_SEARCH = "0";
50
+ delete process.env.MEMORY_GRAPH_EXPANSION;
51
+ for (const suffix of ["", "-wal", "-shm"]) {
52
+ try {
53
+ await unlink(TEST_DB_PATH + suffix);
54
+ } catch {}
55
+ }
56
+ initDb(TEST_DB_PATH);
57
+ await createAgent({ id: agentId, name: "Organic Route Agent", isLead: false, status: "idle" });
58
+
59
+ const store = new SqliteMemoryStore();
60
+ const parent = await store.store({
61
+ agentId,
62
+ scope: "agent",
63
+ name: "completed task summary",
64
+ content: "What the task did.",
65
+ source: "task_completion",
66
+ });
67
+ parentId = parent.id;
68
+ await store.updateEmbedding(parentId, mix(0.9, 4), "test");
69
+
70
+ const neighbor = await store.store({
71
+ agentId,
72
+ scope: "agent",
73
+ name: "manual note linked from the summary",
74
+ content: "A manual note that also matches the query directly.",
75
+ source: "manual",
76
+ });
77
+ neighborId = neighbor.id;
78
+ await store.updateEmbedding(neighborId, mix(0.6, 5), "test");
79
+
80
+ const other = await store.store({
81
+ agentId,
82
+ scope: "agent",
83
+ name: "unrelated manual note",
84
+ content: "Another manual note.",
85
+ source: "manual",
86
+ });
87
+ otherId = other.id;
88
+ await store.updateEmbedding(otherId, mix(0.54, 6), "test");
89
+
90
+ const nowIso = new Date().toISOString();
91
+ await getDbClient().run(
92
+ `INSERT INTO memory_link
93
+ (id, from_memory_id, linkType, targetKind, targetId, strength, resolver, sourceText, metadata, createdAt, updatedAt)
94
+ VALUES (?, ?, 'wikilink', 'memory', ?, 1.0, 'wikilink', 'note', NULL, ?, ?)`,
95
+ [randomUUID(), parentId, neighborId, nowIso, nowIso],
96
+ );
97
+ });
98
+
99
+ afterAll(async () => {
100
+ if (prevHybridFlag === undefined) delete process.env.MEMORY_HYBRID_SEARCH;
101
+ else process.env.MEMORY_HYBRID_SEARCH = prevHybridFlag;
102
+ if (prevGraphFlag === undefined) delete process.env.MEMORY_GRAPH_EXPANSION;
103
+ else process.env.MEMORY_GRAPH_EXPANSION = prevGraphFlag;
104
+ closeDb();
105
+ for (const suffix of ["", "-wal", "-shm"]) {
106
+ try {
107
+ await unlink(TEST_DB_PATH + suffix);
108
+ } catch {}
109
+ }
110
+ });
111
+
112
+ describe("graph route of a neighbour that already matches organically", () => {
113
+ test("fixture reproduces the review: the uncapped graph route beats the organic one, the capped route does not", async () => {
114
+ const { candidates } = await searchAndExpand();
115
+ const now = new Date();
116
+ const organic = candidates.find((c) => c.id === neighborId)!;
117
+ const parent = candidates.find((c) => c.id === parentId)!;
118
+ expect(organic.retrievalSource).toBe("vec");
119
+ expect(parent.retrievalSource).toBe("vec");
120
+ expect(candidates.find((c) => c.id === otherId)?.retrievalSource).toBe("vec");
121
+
122
+ const graphRoute = { ...organic, similarity: 0.9 * 0.7, retrievalSource: "graph" as const };
123
+ const organicScore = computeScore(organic, now);
124
+ const parentScore = computeScore(parent, now);
125
+ expect(organicScore).toBeCloseTo(0.9, 2);
126
+ expect(parentScore).toBeCloseTo(0.63, 2);
127
+ expect(computeScore(graphRoute, now)).toBeGreaterThan(organicScore);
128
+ expect(Math.min(computeScore(graphRoute, now), parentScore)).toBeLessThan(organicScore);
129
+ });
130
+
131
+ test("a low-quality parent's link does not demote a neighbour's stronger organic match", async () => {
132
+ const { expanded } = await searchAndExpand();
133
+ const neighbor = expanded.filter((c) => c.id === neighborId);
134
+ expect(neighbor).toHaveLength(1);
135
+ expect(neighbor[0]!.retrievalSource).toBe("vec");
136
+ expect(neighbor[0]!.graphParentId).toBeUndefined();
137
+
138
+ const [top] = rerank(expanded, { limit: 1 });
139
+ expect(top!.id).toBe(neighborId);
140
+ expect(top!.retrievalSource).toBe("vec");
141
+ expect(top!.compositeScore!).toBeCloseTo(0.9, 2);
142
+ });
143
+
144
+ test("the result matches search without graph expansion", async () => {
145
+ process.env.MEMORY_GRAPH_EXPANSION = "0";
146
+ try {
147
+ const { expanded } = await searchAndExpand();
148
+ const [top] = rerank(expanded, { limit: 1 });
149
+ expect(top!.id).toBe(neighborId);
150
+ expect(top!.compositeScore!).toBeCloseTo(0.9, 2);
151
+ } finally {
152
+ delete process.env.MEMORY_GRAPH_EXPANSION;
153
+ }
154
+ });
155
+ });
@@ -5,7 +5,6 @@ import type { IncomingMessage, ServerResponse } from "node:http";
5
5
  import { Readable } from "node:stream";
6
6
  import { closeDb, createAgent, getDbClient, initDb } from "../be/db";
7
7
  import * as realMemoryModule from "../be/memory";
8
- import { sourceQuality } from "../be/memory/reranker";
9
8
  import { SIMILARITY_THRESHOLD } from "../prompts/memories";
10
9
  import type { AgentMemory } from "../types";
11
10
 
@@ -111,7 +110,9 @@ mock.module("../be/memory", () => ({
111
110
  if (options.queryText === "prompt recall") {
112
111
  return [
113
112
  candidate(memory, 0.95),
114
- candidate(thresholdMemory, SIMILARITY_THRESHOLD / sourceQuality(thresholdMemory.source)),
113
+ // Relevance exactly at the threshold; the manual-source boost lifts
114
+ // the composite well above it. The gate must read relevance.
115
+ candidate(thresholdMemory, SIMILARITY_THRESHOLD),
115
116
  ];
116
117
  }
117
118
  return [candidate(memory, 0.95)];
@@ -281,16 +282,25 @@ describe("memory HTTP recall capture gating", () => {
281
282
  ]);
282
283
  });
283
284
 
284
- test("prompt recall excludes memories at the injection threshold", async () => {
285
+ test("prompt recall gates on relevance, not the boosted composite score", async () => {
285
286
  const response = await callMemoryRoute(
286
287
  "POST",
287
288
  "/api/memory/search",
288
289
  ["api", "memory", "search"],
289
290
  { query: "prompt recall", intent: "pre-task memory recall", limit: 5 },
290
- { "x-memory-consumption": "prompt" },
291
+ { "x-memory-consumption": "prompt", "x-source-task-id": sourceTaskId },
291
292
  );
292
293
 
293
294
  expect(response.statusCode).toBe(200);
295
+ const threshold = response.body.results.find((r: any) => r.id === thresholdMemoryId);
296
+ expect(threshold.similarity).toBeGreaterThan(SIMILARITY_THRESHOLD);
297
+ expect(threshold.rawSimilarity).toBe(SIMILARITY_THRESHOLD);
298
+ expect(
299
+ await getDbClient().get<{ similarity: number; relevance: number }>(
300
+ "SELECT similarity, relevance FROM memory_retrieval WHERE memoryId = ?",
301
+ [thresholdMemoryId],
302
+ ),
303
+ ).toEqual({ similarity: threshold.similarity, relevance: SIMILARITY_THRESHOLD });
294
304
  expect(response.body.results.map((result: any) => result.accessCount)).toEqual([1, 0]);
295
305
  expect(
296
306
  await getDbClient().get<{ accessCount: number }>(