@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
@@ -0,0 +1,250 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
+ import {
3
+ createServer as createHttpServer,
4
+ type IncomingMessage,
5
+ type Server,
6
+ type ServerResponse,
7
+ } from "node:http";
8
+ import {
9
+ closeDb,
10
+ createAgent,
11
+ createTaskExtended,
12
+ getDbClient,
13
+ getRootTaskChain,
14
+ getTaskById,
15
+ initDb,
16
+ } from "../be/db";
17
+ import { getTaskSteeringFields, getTaskSteeringFieldsForTasks } from "../be/steering";
18
+ import { getTaskCitations, getTaskCitationsForTasks } from "../be/task-citations";
19
+ import { handleSessions } from "../http/sessions";
20
+ import { getPathSegments, parseQueryParams } from "../http/utils";
21
+ import type { AgentTask } from "../types";
22
+ import { listenOnFreePort } from "./test-net";
23
+
24
+ let server: Server;
25
+ let baseUrl: string;
26
+ let rootId: string;
27
+ const chainIds: string[] = [];
28
+
29
+ async function addCitation(taskId: string, index: number, general = false) {
30
+ await getDbClient().run(
31
+ `INSERT INTO task_citations (task_id, citation_index, kind, ref, label, quote, resolved_url, verified, general)
32
+ VALUES (?, ?, 'url', ?, ?, NULL, ?, 'unchecked', ?)`,
33
+ [
34
+ taskId,
35
+ index,
36
+ `https://example.com/${taskId}/${index}`,
37
+ `c${index}`,
38
+ `https://example.com/${taskId}/${index}`,
39
+ general ? 1 : 0,
40
+ ],
41
+ );
42
+ }
43
+
44
+ /** The pre-batch handler body: one agent lookup and one citations query per task. */
45
+ async function unbatchedSession(id: string) {
46
+ const root = (await getTaskById(id))!;
47
+ const chain = await getRootTaskChain(id);
48
+ const decorate = async (task: AgentTask) => ({
49
+ ...task,
50
+ ...(await getTaskSteeringFields(task)),
51
+ citations: await getTaskCitations(task.id),
52
+ });
53
+ return { root: await decorate(root), chain: await Promise.all(chain.map(decorate)) };
54
+ }
55
+
56
+ beforeAll(async () => {
57
+ initDb(":memory:");
58
+ server = createHttpServer(async (req: IncomingMessage, res: ServerResponse) => {
59
+ const ok = await handleSessions(
60
+ req,
61
+ res,
62
+ getPathSegments(req.url || ""),
63
+ parseQueryParams(req.url || ""),
64
+ );
65
+ if (!ok) {
66
+ res.writeHead(404);
67
+ res.end();
68
+ }
69
+ });
70
+ baseUrl = `http://127.0.0.1:${await listenOnFreePort(server)}`;
71
+
72
+ const lead = await createAgent({
73
+ name: "lead",
74
+ isLead: true,
75
+ status: "idle",
76
+ capabilities: [],
77
+ harnessProvider: "claude",
78
+ });
79
+ const codex = await createAgent({
80
+ name: "codex worker",
81
+ isLead: false,
82
+ status: "idle",
83
+ capabilities: [],
84
+ harnessProvider: "codex",
85
+ });
86
+ const pi = await createAgent({
87
+ name: "pi worker",
88
+ isLead: false,
89
+ status: "idle",
90
+ capabilities: [],
91
+ provider: "pi",
92
+ });
93
+
94
+ const root = await createTaskExtended("session root", { agentId: lead.id, source: "api" });
95
+ rootId = root.id;
96
+ const childA = await createTaskExtended("child A", {
97
+ agentId: codex.id,
98
+ parentTaskId: root.id,
99
+ source: "api",
100
+ });
101
+ const childB = await createTaskExtended("child B", {
102
+ agentId: pi.id,
103
+ parentTaskId: root.id,
104
+ source: "api",
105
+ });
106
+ const grandchild = await createTaskExtended("grandchild of A", {
107
+ agentId: codex.id,
108
+ parentTaskId: childA.id,
109
+ source: "api",
110
+ });
111
+ const unassigned = await createTaskExtended("unassigned child", {
112
+ parentTaskId: childB.id,
113
+ source: "api",
114
+ provider: "devin",
115
+ });
116
+ const ghost = await createTaskExtended("child of a deleted agent", {
117
+ agentId: lead.id,
118
+ parentTaskId: root.id,
119
+ source: "api",
120
+ });
121
+ await getDbClient().run("UPDATE agent_tasks SET agentId = ? WHERE id = ?", [
122
+ "00000000-0000-4000-8000-000000000000",
123
+ ghost.id,
124
+ ]);
125
+ chainIds.push(childA.id, childB.id, grandchild.id, unassigned.id, ghost.id);
126
+
127
+ // Out-of-order inserts so the per-task citation_index ordering is exercised.
128
+ await addCitation(root.id, 3);
129
+ await addCitation(root.id, 1, true);
130
+ await addCitation(root.id, 2);
131
+ await addCitation(childA.id, 2);
132
+ await addCitation(childA.id, 1);
133
+ await addCitation(grandchild.id, 1, true);
134
+ });
135
+
136
+ afterAll(async () => {
137
+ await new Promise<void>((resolve) => server.close(() => resolve()));
138
+ closeDb();
139
+ });
140
+
141
+ describe("GET /api/sessions/{rootTaskId} batching", () => {
142
+ test("response is identical to the per-task path on a multi-task chain", async () => {
143
+ const res = await fetch(`${baseUrl}/api/sessions/${rootId}`);
144
+ expect(res.status).toBe(200);
145
+ const body = await res.json();
146
+ const expected = JSON.parse(JSON.stringify(await unbatchedSession(rootId)));
147
+
148
+ expect(body).toEqual(expected);
149
+ // Guard against a vacuous match: the fixture covers every branch.
150
+ expect(body.chain.map((task: AgentTask) => task.id).sort()).toEqual(
151
+ [rootId, ...chainIds].sort(),
152
+ );
153
+ expect(body.root.isLeadTask).toBe(true);
154
+ expect(body.root.citations.map((c: { index: number }) => c.index)).toEqual([1, 2, 3]);
155
+ expect(body.root.citations[0].general).toBe(true);
156
+ const byId = new Map(body.chain.map((task: AgentTask) => [task.id, task]));
157
+ const [childA, childB, grandchild, unassigned, ghost] = chainIds.map((id) => byId.get(id)) as {
158
+ isLeadTask: boolean;
159
+ supportedSteerModes: string[];
160
+ citations: { index: number }[];
161
+ }[];
162
+ expect(childA!.citations.map((c) => c.index)).toEqual([1, 2]);
163
+ expect(grandchild!.citations).toHaveLength(1);
164
+ expect(childB!.citations).toEqual([]);
165
+ expect(unassigned!.isLeadTask).toBe(false);
166
+ expect(ghost!.isLeadTask).toBe(false);
167
+ });
168
+
169
+ test("404 for an unknown root is unchanged", async () => {
170
+ const res = await fetch(`${baseUrl}/api/sessions/00000000-0000-4000-8000-000000000001`);
171
+ expect(res.status).toBe(404);
172
+ });
173
+
174
+ test("getRootTaskChain seeks the chain instead of scanning agent_tasks, same order", async () => {
175
+ const OLD_CHAIN_SQL = `WITH RECURSIVE chain(id) AS (
176
+ SELECT id FROM agent_tasks WHERE id = ?
177
+ UNION ALL
178
+ SELECT t.id FROM agent_tasks t
179
+ JOIN chain c ON t.parentTaskId = c.id
180
+ )
181
+ SELECT t.id FROM agent_tasks t
182
+ JOIN chain ON chain.id = t.id
183
+ ORDER BY t.createdAt`;
184
+ // Ties on createdAt: the old plan returned them in idx_agent_tasks_created (rowid) order.
185
+ const ids = [rootId, ...chainIds];
186
+ const saved = await getDbClient().query<{ id: string; createdAt: string }>(
187
+ `SELECT id, createdAt FROM agent_tasks WHERE id IN (${ids.map(() => "?").join(",")})`,
188
+ ids,
189
+ );
190
+ await getDbClient().run(
191
+ `UPDATE agent_tasks SET createdAt = '2026-01-01T00:00:00.000Z' WHERE id IN (${ids
192
+ .slice(1)
193
+ .map(() => "?")
194
+ .join(",")})`,
195
+ ids.slice(1),
196
+ );
197
+ try {
198
+ const old = await getDbClient().query<{ id: string }>(OLD_CHAIN_SQL, [rootId]);
199
+ expect((await getRootTaskChain(rootId)).map((t) => t.id)).toEqual(old.map((r) => r.id));
200
+ } finally {
201
+ for (const row of saved) {
202
+ await getDbClient().run("UPDATE agent_tasks SET createdAt = ? WHERE id = ?", [
203
+ row.createdAt,
204
+ row.id,
205
+ ]);
206
+ }
207
+ }
208
+
209
+ // Capture the SQL the real call issues, then ask SQLite how it runs it.
210
+ const client = getDbClient();
211
+ const query = client.query.bind(client);
212
+ let issued = "";
213
+ client.query = ((sql: string, params?: unknown[]) => {
214
+ issued = sql;
215
+ return query(sql, params as never);
216
+ }) as typeof client.query;
217
+ try {
218
+ await getRootTaskChain(rootId);
219
+ } finally {
220
+ client.query = query;
221
+ }
222
+ const plan = await client.query<{ detail: string }>(`EXPLAIN QUERY PLAN ${issued}`, [rootId]);
223
+ expect(plan.map((r) => r.detail)).not.toContain("SCAN t USING INDEX idx_agent_tasks_created");
224
+ });
225
+
226
+ test("batched loaders chunk past 500 ids and match the per-id loaders", async () => {
227
+ const chain = await getRootTaskChain(rootId);
228
+ // Pad with ids that have no rows so the real ones straddle chunk boundaries.
229
+ const padding = Array.from(
230
+ { length: 1200 },
231
+ (_, i) => `00000000-0000-4000-8000-${String(i).padStart(12, "0")}`,
232
+ );
233
+ const ids = [...padding.slice(0, 499), ...chain.map((t) => t.id), ...padding.slice(499)];
234
+
235
+ const citations = await getTaskCitationsForTasks(ids);
236
+ expect(citations.size).toBe(ids.length);
237
+ for (const task of chain) {
238
+ expect(citations.get(task.id)).toEqual(await getTaskCitations(task.id));
239
+ }
240
+ expect(citations.get(padding[0]!)).toEqual([]);
241
+
242
+ const fakeTasks = padding.map((agentId, i) => ({ ...chain[0]!, id: `fake-${i}`, agentId }));
243
+ const tasks = [...fakeTasks.slice(0, 499), ...chain, ...fakeTasks.slice(499)];
244
+ const steering = await getTaskSteeringFieldsForTasks(tasks);
245
+ expect(steering.size).toBe(tasks.length);
246
+ for (const task of tasks) {
247
+ expect(steering.get(task.id)).toEqual(await getTaskSteeringFields(task));
248
+ }
249
+ });
250
+ });
@@ -6,11 +6,13 @@ import {
6
6
  createAgent,
7
7
  createTaskExtended,
8
8
  createUser,
9
+ getDbClient,
9
10
  getRootTaskChain,
10
11
  initDb,
11
12
  listRecentSessions,
12
13
  updateTaskTitle,
13
14
  } from "../be/db";
15
+ import { withSiblingAwareness } from "../tasks/sibling-awareness";
14
16
 
15
17
  const TEST_DB_PATH = "./test-sessions.sqlite";
16
18
 
@@ -240,4 +242,120 @@ describe("sessions — getRootTaskChain + listRecentSessions", () => {
240
242
  const afterClear = (await listRecentSessions({ limit: 50 })).find((s) => s.root.id === root.id);
241
243
  expect(afterClear?.root.title).toBeUndefined();
242
244
  });
245
+
246
+ test("contextKeyPrefix — returns only matching roots, total agrees, children never appear", async () => {
247
+ const pageKey = "task:ui:workflow:ctxwf1";
248
+ const a = await createTaskExtended("ctx session a", { contextKey: `${pageKey}:n1` });
249
+ const b = await createTaskExtended("ctx session b", { contextKey: `${pageKey}:n2` });
250
+ // Child inherits the parent's contextKey but must not surface as its own row.
251
+ const child = await createTaskExtended("ctx child", { parentTaskId: a.id });
252
+ expect(child.contextKey).toBe(`${pageKey}:n1`);
253
+ // Longer id sharing the textual prefix: excluded by the trailing `:`.
254
+ const longer = await createTaskExtended("ctx longer id", {
255
+ contextKey: "task:ui:workflow:ctxwf1x:n1",
256
+ });
257
+ // Positive control: without the filter every root is listed.
258
+ const all = await listRecentSessions({ limit: 500 });
259
+ expect(all.some((s) => s.root.id === longer.id)).toBe(true);
260
+
261
+ const rows = await listRecentSessions({ limit: 50, contextKeyPrefix: `${pageKey}:` });
262
+ expect(rows.map((s) => s.root.id).sort()).toEqual([a.id, b.id].sort());
263
+ expect(await countSessions({ contextKeyPrefix: `${pageKey}:` })).toBe(2);
264
+ expect(rows.find((s) => s.root.id === a.id)?.chainTaskCount).toBe(2);
265
+ expect(rows.some((s) => s.root.id === child.id)).toBe(false);
266
+ });
267
+
268
+ test("contextKeyPrefix + q — server-side search keeps newest-first order, ties break on createdAt", async () => {
269
+ const pageKey = "task:ui:workflow:ctxsearch";
270
+ const setTimes = (id: string, createdAt: string, lastUpdatedAt: string) =>
271
+ getDbClient().run("UPDATE agent_tasks SET createdAt = ?, lastUpdatedAt = ? WHERE id = ?", [
272
+ createdAt,
273
+ lastUpdatedAt,
274
+ id,
275
+ ]);
276
+ const oldest = await createTaskExtended("legend overlaps the chart", {
277
+ contextKey: `${pageKey}:n1`,
278
+ });
279
+ const middle = await createTaskExtended("axis labels are cut", { contextKey: `${pageKey}:n2` });
280
+ const newest = await createTaskExtended("legend colors wrong", { contextKey: `${pageKey}:n3` });
281
+ // Same last activity as `newest`, created earlier: sorts just below it.
282
+ const tied = await createTaskExtended("legend font", { contextKey: `${pageKey}:n4` });
283
+ await setTimes(oldest.id, "2026-09-01T10:00:00.000Z", "2026-09-01T10:05:00.000Z");
284
+ await setTimes(middle.id, "2026-09-10T10:00:00.000Z", "2026-09-10T10:05:00.000Z");
285
+ await setTimes(newest.id, "2026-09-20T10:00:00.000Z", "2026-09-20T10:05:00.000Z");
286
+ await setTimes(tied.id, "2026-09-19T10:00:00.000Z", "2026-09-20T10:05:00.000Z");
287
+ await updateTaskTitle(middle.id, "Legend position");
288
+
289
+ const all = await listRecentSessions({ limit: 50, contextKeyPrefix: `${pageKey}:` });
290
+ expect(all.map((s) => s.root.id)).toEqual([newest.id, tied.id, middle.id, oldest.id]);
291
+
292
+ // `q` filters in SQL on task text and custom title, case-insensitive.
293
+ const hits = await listRecentSessions({
294
+ limit: 50,
295
+ contextKeyPrefix: `${pageKey}:`,
296
+ q: "LEGEND",
297
+ });
298
+ expect(hits.map((s) => s.root.id)).toEqual([newest.id, tied.id, middle.id, oldest.id]);
299
+ const narrow = await listRecentSessions({
300
+ limit: 50,
301
+ contextKeyPrefix: `${pageKey}:`,
302
+ q: "overlaps",
303
+ });
304
+ expect(narrow.map((s) => s.root.id)).toEqual([oldest.id]);
305
+ expect(await countSessions({ contextKeyPrefix: `${pageKey}:`, q: "colors" })).toBe(1);
306
+ // A match outside the page is not returned.
307
+ expect(
308
+ await listRecentSessions({ limit: 50, contextKeyPrefix: `${pageKey}:`, q: "no such text" }),
309
+ ).toEqual([]);
310
+ });
311
+
312
+ test("contextKeyPrefix — LIKE wildcards in the prefix match literally", async () => {
313
+ const literal = await createTaskExtended("literal wildcard", {
314
+ contextKey: "task:ui:route:a%25_b:n1",
315
+ });
316
+ // Would match `a%25_b` under an unescaped LIKE (`_` = any char), must not here.
317
+ await createTaskExtended("wildcard decoy", { contextKey: "task:ui:route:a%25xb:n1" });
318
+
319
+ const rows = await listRecentSessions({
320
+ limit: 50,
321
+ contextKeyPrefix: "task:ui:route:a%25_b:",
322
+ });
323
+ expect(rows.map((s) => s.root.id)).toEqual([literal.id]);
324
+ expect(await countSessions({ contextKeyPrefix: "task:ui:route:a%25_b:" })).toBe(1);
325
+ expect(await countSessions({ contextKeyPrefix: "task:ui:route:%" })).toBe(0);
326
+ });
327
+
328
+ test("per-session keys keep a second session on the same page a root (sibling-awareness trap)", async () => {
329
+ const pageKey = "task:ui:workflow:ctxwf2";
330
+ const lead = await createAgent({
331
+ id: "sessions-test-agent-ctx",
332
+ name: "Sessions Ctx Lead",
333
+ isLead: true,
334
+ status: "idle",
335
+ });
336
+ const first = await createTaskExtended("first session", {
337
+ agentId: lead.id,
338
+ contextKey: `${pageKey}:n1`,
339
+ });
340
+ expect(first.status).toBe("pending");
341
+
342
+ // POST /api/tasks defaults agentId to the lead, which is what arms the trap.
343
+ // Positive control: a shared key WOULD nest the new session under the first.
344
+ const shared = await withSiblingAwareness("same key", {
345
+ agentId: lead.id,
346
+ contextKey: `${pageKey}:n1`,
347
+ });
348
+ expect(shared.options.parentTaskId).toBe(first.id);
349
+
350
+ const prepared = await withSiblingAwareness("second session", {
351
+ agentId: lead.id,
352
+ contextKey: `${pageKey}:n2`,
353
+ });
354
+ expect(prepared.options.parentTaskId).toBeUndefined();
355
+ const second = await createTaskExtended(prepared.description, prepared.options);
356
+ expect(second.parentTaskId).toBeUndefined();
357
+
358
+ const rows = await listRecentSessions({ limit: 50, contextKeyPrefix: `${pageKey}:` });
359
+ expect(rows.map((s) => s.root.id).sort()).toEqual([first.id, second.id].sort());
360
+ });
243
361
  });
@@ -4,6 +4,7 @@ import { closeDb, initDb } from "../be/db";
4
4
  import { getEnabledCapabilities } from "../server";
5
5
  import { getSlackApp, startSlackApp, stopSlackApp } from "../slack/app";
6
6
  import { getSlackConfiguration, isSlackConfigured } from "../slack/config";
7
+ import { getSlackConnectionState } from "../slack/connection-state";
7
8
 
8
9
  const SLACK_ENV_KEYS = [
9
10
  "NODE_ENV",
@@ -134,3 +135,24 @@ test.each([
134
135
  closeDb();
135
136
  }
136
137
  }, 20_000);
138
+
139
+ test("Socket Mode lifecycle drives the live connection state read by /status", async () => {
140
+ const slack = await startSlackMock(false);
141
+ initDb(":memory:");
142
+ try {
143
+ Object.assign(process.env, slack.mock.env);
144
+ expect(getSlackConnectionState()).toBe("disconnected");
145
+
146
+ expect(await startSlackApp()).toBe(true);
147
+ await slack.mock.waitForConnection(10_000);
148
+ // Bolt resolves start() on Slack's hello, which is what emits `connected`.
149
+ expect(getSlackConnectionState()).toBe("connected");
150
+
151
+ await stopSlackApp();
152
+ expect(getSlackConnectionState()).toBe("disconnected");
153
+ } finally {
154
+ await stopSlackApp();
155
+ await stopSlackMock(slack, false);
156
+ closeDb();
157
+ }
158
+ }, 20_000);
@@ -0,0 +1,44 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { findKnownModel } from "../../apps/ui/src/lib/agent-runtime-models";
3
+ import { loadModelsDevCache } from "../be/modelsdev-cache";
4
+ import { buildModelsDevSeedRows } from "../be/seed-pricing";
5
+
6
+ describe("Sonnet 5.5 registry", () => {
7
+ test("is selectable and keeps Sonnet 5 selectable", () => {
8
+ expect(findKnownModel("claude-sonnet-5-5")).toMatchObject({
9
+ id: "claude-sonnet-5-5",
10
+ reasoningLevels: ["low", "medium", "high", "xhigh"],
11
+ });
12
+ expect(findKnownModel("claude-sonnet-5")?.id).toBe("claude-sonnet-5");
13
+ });
14
+
15
+ test("seeds canonical and alias rates without changing Sonnet 5", () => {
16
+ const cache = loadModelsDevCache();
17
+ expect(cache).not.toBeNull();
18
+ const rows = buildModelsDevSeedRows(cache!);
19
+ for (const provider of ["claude", "claude-managed"] as const) {
20
+ for (const model of ["claude-sonnet-5-5", "sonnet"]) {
21
+ const rates = Object.fromEntries(
22
+ rows
23
+ .filter((row) => row.provider === provider && row.model === model)
24
+ .map((row) => [row.tokenClass, row.pricePerMillionUsd]),
25
+ );
26
+ expect(rates).toEqual({
27
+ input: 2,
28
+ output: 10,
29
+ cached_input: 0.2,
30
+ cache_write: 2.5,
31
+ cache_write_1h: 4,
32
+ });
33
+ }
34
+ expect(
35
+ rows.find(
36
+ (row) =>
37
+ row.provider === provider &&
38
+ row.model === "claude-sonnet-5" &&
39
+ row.tokenClass === "input",
40
+ )?.pricePerMillionUsd,
41
+ ).toBe(2);
42
+ }
43
+ });
44
+ });
@@ -40,6 +40,7 @@ import {
40
40
  rollupCredStatusForProvider,
41
41
  type SetupMilestone,
42
42
  } from "../http/status";
43
+ import { setSlackConnectionState } from "../slack/connection-state";
43
44
  import type { AgentCredStatus } from "../types";
44
45
 
45
46
  // Helper for tests: stamp an agent row with a cred_status snapshot so the
@@ -170,6 +171,7 @@ beforeEach(async () => {
170
171
  clearEnv();
171
172
  await clearTables();
172
173
  _resetTestConnectionCache();
174
+ setSlackConnectionState("disconnected");
173
175
  });
174
176
 
175
177
  afterEach(() => {
@@ -455,7 +457,7 @@ describe("setup milestones", () => {
455
457
  process.env.SLACK_APP_TOKEN = "xapp-test";
456
458
  const b = await buildStatusPayload();
457
459
  expect(getMilestone(b, "slack").state).toBe("configured");
458
- expect(getMilestone(b, "slack").hint).toContain("not been verified");
460
+ expect(getMilestone(b, "slack").hint).toContain("not connected");
459
461
 
460
462
  process.env.SLACK_MODE = "http";
461
463
  delete process.env.SLACK_APP_TOKEN;
@@ -469,6 +471,26 @@ describe("setup milestones", () => {
469
471
  expect(getMilestone(c, "slack").state).toBe("unverified");
470
472
  });
471
473
 
474
+ test("slack: a live Socket Mode connection reports verified; a dropped one does not", async () => {
475
+ process.env.SLACK_BOT_TOKEN = "xoxb-test";
476
+ process.env.SLACK_APP_TOKEN = "xapp-test";
477
+
478
+ setSlackConnectionState("connected");
479
+ const live = getMilestone(await buildStatusPayload(), "slack");
480
+ expect(live.state).toBe("verified");
481
+ expect(live.hint).toBeUndefined();
482
+
483
+ setSlackConnectionState("connecting");
484
+ const reconnecting = getMilestone(await buildStatusPayload(), "slack");
485
+ expect(reconnecting.state).toBe("configured");
486
+ expect(reconnecting.hint).toContain("not connected");
487
+
488
+ // A stale "connected" never verifies credentials that are now missing.
489
+ setSlackConnectionState("connected");
490
+ delete process.env.SLACK_APP_TOKEN;
491
+ expect(getMilestone(await buildStatusPayload(), "slack").state).toBe("unverified");
492
+ });
493
+
472
494
  test("slack: invalid mode fails closed with a specific hint", async () => {
473
495
  process.env.SLACK_MODE = "webhook";
474
496
  process.env.SLACK_BOT_TOKEN = "xoxb-test";
@@ -1064,6 +1086,19 @@ describe("computeHealth (Phase 2)", () => {
1064
1086
  expect(computeHealth(synthetic)).toBe("degraded");
1065
1087
  });
1066
1088
 
1089
+ test("`ok` when a connected Slack reports `verified` next to other connected integrations", () => {
1090
+ const synthetic: SetupMilestone[] = [
1091
+ { id: "harness", label: "Harness", state: "verified" },
1092
+ { id: "slack", label: "Slack", state: "verified" },
1093
+ { id: "github", label: "GitHub", state: "verified" },
1094
+ { id: "linear", label: "Linear", state: "verified" },
1095
+ { id: "jira", label: "Jira", state: "unverified" },
1096
+ { id: "workers", label: "Workers", state: "verified" },
1097
+ { id: "first_task", label: "First task", state: "verified" },
1098
+ ];
1099
+ expect(computeHealth(synthetic)).toBe("ok");
1100
+ });
1101
+
1067
1102
  test("integrations in `unverified` alone do NOT degrade health", () => {
1068
1103
  // No integrations are connected — that's the common shape, not a
1069
1104
  // problem. Health should be `ok` as long as harness + workers verified.