@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
@@ -32,7 +32,11 @@ type StubInstance = {
32
32
  innerHTML: string;
33
33
  textContent: string;
34
34
  isConnected: boolean;
35
+ style: { cssText: string; transform: string };
36
+ children: StubInstance[];
37
+ hasAttribute?: (name: string) => boolean;
35
38
  connectedCallback?: () => void;
39
+ disconnectedCallback?: () => void;
36
40
  dispatchEvent: (evt: unknown) => boolean;
37
41
  };
38
42
 
@@ -44,24 +48,52 @@ type StubInstance = {
44
48
  * callers can simulate the real-browser parse-order race
45
49
  * (`connectedCallback` fires → children get appended → microtask drains).
46
50
  */
47
- function makeRig(): {
51
+ function makeRig(
52
+ swarmSdk?: unknown,
53
+ readyState = "complete",
54
+ sdkConstructor?: new () => { room: (name?: string, options?: unknown) => Promise<unknown> },
55
+ ): {
48
56
  Ctor: new () => StubInstance;
57
+ CursorsCtor: new () => StubInstance;
58
+ SdkCtor?: new () => { room: (name?: string, options?: unknown) => Promise<unknown> };
59
+ body: StubInstance;
49
60
  setText: (el: StubInstance, attrs: Record<string, string>, text: string) => void;
61
+ fireDocumentEvent: (name: string, event: unknown) => void;
62
+ fireNextFrame: () => void;
50
63
  flushMicrotasks: () => Promise<void>;
51
64
  } {
52
65
  const registry = new Map<string, new () => StubInstance>();
66
+ const documentListeners = new Map<string, ((event: unknown) => void)[]>();
67
+ const animationFrames = new Map<number, (time: number) => void>();
68
+ let nextAnimationFrame = 1;
53
69
 
54
70
  class StubHTMLElement {
55
71
  innerHTML = "";
56
72
  textContent = "";
57
73
  isConnected = true;
74
+ style = { cssText: "", transform: "" };
75
+ parentNode: StubHTMLElement | null = null;
76
+ children: StubHTMLElement[] = [];
58
77
  _attrs: Record<string, string> = {};
59
78
  getAttribute(name: string): string | null {
60
79
  return this._attrs[name] ?? null;
61
80
  }
81
+ hasAttribute(name: string): boolean {
82
+ return Object.hasOwn(this._attrs, name);
83
+ }
62
84
  setAttribute(name: string, value: string): void {
63
85
  this._attrs[name] = value;
64
86
  }
87
+ appendChild(child: StubHTMLElement): StubHTMLElement {
88
+ child.parentNode = this;
89
+ this.children.push(child);
90
+ return child;
91
+ }
92
+ removeChild(child: StubHTMLElement): StubHTMLElement {
93
+ this.children = this.children.filter((item) => item !== child);
94
+ child.parentNode = null;
95
+ return child;
96
+ }
65
97
  connectedCallback?(): void;
66
98
  closest(_selector: string): null {
67
99
  return null;
@@ -85,6 +117,20 @@ function makeRig(): {
85
117
 
86
118
  const win = {
87
119
  customElements,
120
+ swarmSdk: swarmSdk as
121
+ | { room?: (name: string, options?: unknown) => Promise<unknown> }
122
+ | undefined,
123
+ SwarmSDK: sdkConstructor,
124
+ innerWidth: 1000,
125
+ innerHeight: 500,
126
+ scrollX: 0,
127
+ scrollY: 0,
128
+ requestAnimationFrame: (callback: (time: number) => void) => {
129
+ const id = nextAnimationFrame++;
130
+ animationFrames.set(id, callback);
131
+ return id;
132
+ },
133
+ cancelAnimationFrame: (id: number) => animationFrames.delete(id),
88
134
  swarmUi: undefined as { renderDiff?: (rootEl: unknown, data: unknown) => void } | undefined,
89
135
  HTMLElement: StubHTMLElement,
90
136
  CustomEvent: class {
@@ -96,10 +142,27 @@ function makeRig(): {
96
142
  };
97
143
 
98
144
  // Provide `document` stubs the jump-list path uses.
145
+ const body = new StubHTMLElement();
99
146
  const doc = {
147
+ readyState,
148
+ documentElement: { scrollWidth: 1000, scrollHeight: 500, clientWidth: 1000, clientHeight: 500 },
149
+ body,
150
+ createElement: () => new StubHTMLElement(),
100
151
  querySelectorAll: () => [],
101
- addEventListener: () => {},
102
- removeEventListener: () => {},
152
+ addEventListener: (name: string, handler: (event: unknown) => void) => {
153
+ const handlers = documentListeners.get(name) ?? [];
154
+ handlers.push(handler);
155
+ documentListeners.set(name, handlers);
156
+ },
157
+ removeEventListener: (name: string, handler: (event: unknown) => void) => {
158
+ documentListeners.set(
159
+ name,
160
+ (documentListeners.get(name) ?? []).filter((item) => item !== handler),
161
+ );
162
+ },
163
+ fireEvent: (name: string, event: unknown) => {
164
+ for (const handler of documentListeners.get(name) ?? []) handler(event);
165
+ },
103
166
  };
104
167
 
105
168
  // Evaluate SWARM_UI_JS with our stubs in scope. The IIFE inside the
@@ -113,9 +176,9 @@ function makeRig(): {
113
176
  "CustomEvent",
114
177
  "queueMicrotask",
115
178
  "console",
116
- `${SWARM_UI_JS}\nreturn window.customElements.get('swarm-diff');`,
179
+ `${SWARM_UI_JS}\nreturn { diff: window.customElements.get('swarm-diff'), cursors: window.customElements.get('swarm-cursors'), sdk: window.SwarmSDK };`,
117
180
  );
118
- const Ctor = factory(
181
+ const constructors = factory(
119
182
  win,
120
183
  customElements,
121
184
  doc,
@@ -123,20 +186,39 @@ function makeRig(): {
123
186
  win.CustomEvent,
124
187
  queueMicrotask,
125
188
  console,
126
- ) as (new () => StubInstance) | undefined;
189
+ ) as {
190
+ diff?: new () => StubInstance;
191
+ cursors?: new () => StubInstance;
192
+ sdk?: new () => { room: (name?: string, options?: unknown) => Promise<unknown> };
193
+ };
194
+ const Ctor = constructors.diff;
127
195
  if (!Ctor) throw new Error("custom element did not register");
196
+ const CursorsCtor = constructors.cursors;
197
+ if (!CursorsCtor) throw new Error("swarm-cursors custom element did not register");
128
198
 
129
199
  return {
130
200
  Ctor,
201
+ CursorsCtor,
202
+ SdkCtor: constructors.sdk,
203
+ body,
131
204
  setText(el, attrs, text) {
132
205
  (el as unknown as { _attrs: Record<string, string> })._attrs = attrs;
133
206
  el.textContent = text;
134
207
  },
208
+ fireDocumentEvent(name, event) {
209
+ doc.fireEvent(name, event);
210
+ },
211
+ fireNextFrame() {
212
+ const next = animationFrames.entries().next().value as
213
+ | [number, (time: number) => void]
214
+ | undefined;
215
+ if (!next) return;
216
+ animationFrames.delete(next[0]);
217
+ next[1](0);
218
+ },
135
219
  async flushMicrotasks() {
136
- // Two yields: one drains the connectedCallback microtask, the next one
137
- // drains anything queued by the render path itself.
138
- await Promise.resolve();
139
- await Promise.resolve();
220
+ // Drain the chained async room-open and render callbacks.
221
+ for (let i = 0; i < 5; i++) await Promise.resolve();
140
222
  },
141
223
  };
142
224
  }
@@ -164,6 +246,7 @@ describe("SWARM_UI_JS", () => {
164
246
  test("defines swarm-diff + swarm-diff-jumps custom elements", () => {
165
247
  expect(SWARM_UI_JS).toContain("customElements.define('swarm-diff'");
166
248
  expect(SWARM_UI_JS).toContain("customElements.define('swarm-diff-jumps'");
249
+ expect(SWARM_UI_JS).toContain("customElements.define('swarm-cursors'");
167
250
  });
168
251
 
169
252
  test("exposes window.swarmUi.renderDiff as a programmatic entry point", () => {
@@ -172,6 +255,229 @@ describe("SWARM_UI_JS", () => {
172
255
  });
173
256
  });
174
257
 
258
+ describe("<swarm-cursors> presence merge", () => {
259
+ test("reuses a page room's schema and preserves page presence fields", async () => {
260
+ const sent: unknown[] = [];
261
+ const roomOptions: unknown[] = [];
262
+ const roomHandlers = new Map<string, (value: unknown) => void>();
263
+ let closeCount = 0;
264
+ const room = {
265
+ me: { userId: "self" },
266
+ presence: {
267
+ peers: [] as unknown[],
268
+ set(data: unknown) {
269
+ sent.push(structuredClone(data));
270
+ },
271
+ },
272
+ on(name: string, handler: (value: unknown) => void) {
273
+ roomHandlers.set(name, handler);
274
+ return () => roomHandlers.delete(name);
275
+ },
276
+ async close() {
277
+ closeCount++;
278
+ },
279
+ };
280
+ let pageRoomSchema: number | undefined;
281
+ class PageSdk {
282
+ async room(_name?: string, options?: unknown) {
283
+ const requestedSchema =
284
+ (options as { schemaVersion?: number } | undefined)?.schemaVersion ?? 1;
285
+ if (pageRoomSchema !== undefined && requestedSchema !== pageRoomSchema)
286
+ throw new Error("Room already opened with a different schema version");
287
+ pageRoomSchema = requestedSchema;
288
+ roomOptions.push(options);
289
+ return room;
290
+ }
291
+ }
292
+ const swarmSdk = new PageSdk();
293
+ const rig = makeRig(swarmSdk, "loading", PageSdk);
294
+ if (!rig.SdkCtor) throw new Error("SDK constructor was not wrapped");
295
+ expect(swarmSdk).toBeInstanceOf(rig.SdkCtor);
296
+ const pageSdk = new rig.SdkCtor();
297
+
298
+ // Page code may set its data before the cursor element connects.
299
+ const board = (await pageSdk.room(undefined, { schemaVersion: 2 })) as typeof room;
300
+ const mismatchedOpens = await Promise.allSettled([
301
+ pageSdk.room(undefined, { schemaVersion: 3 }),
302
+ pageSdk.room(undefined, { schemaVersion: 4 }),
303
+ ]);
304
+ expect(mismatchedOpens.map(({ status }) => status)).toEqual(["rejected", "rejected"]);
305
+ board.presence.set({ selectedCard: "card-1" });
306
+
307
+ const cursors = new rig.CursorsCtor();
308
+ rig.setText(cursors, { room: "default" }, "");
309
+ cursors.connectedCallback?.();
310
+ rig.fireDocumentEvent("DOMContentLoaded", undefined);
311
+ await rig.flushMicrotasks();
312
+ rig.fireDocumentEvent("pointermove", { pageX: 250, pageY: 125, clientX: 250, clientY: 125 });
313
+
314
+ expect(sent.at(-1)).toEqual({ selectedCard: "card-1", __cursor: { x: 0.25, y: 0.25 } });
315
+
316
+ // Later page updates also keep the active cursor in the replacement payload.
317
+ board.presence.set({ selectedCard: "card-2", tool: "pen" });
318
+ expect(sent.at(-1)).toEqual({
319
+ selectedCard: "card-2",
320
+ tool: "pen",
321
+ __cursor: { x: 0.25, y: 0.25 },
322
+ });
323
+ rig.fireDocumentEvent("pointerleave", undefined);
324
+ expect(sent.at(-1)).toEqual({ selectedCard: "card-2", tool: "pen" });
325
+ cursors.disconnectedCallback?.();
326
+ await rig.flushMicrotasks();
327
+ expect(roomOptions).toEqual([{ schemaVersion: 2 }]);
328
+ expect(closeCount).toBe(0);
329
+ });
330
+
331
+ test("closes a cursor-only room when the element disconnects", async () => {
332
+ let closeCount = 0;
333
+ const room = {
334
+ me: { userId: "self" },
335
+ presence: { peers: [], set: () => {} },
336
+ on: () => () => {},
337
+ async close() {
338
+ closeCount++;
339
+ },
340
+ };
341
+ const rig = makeRig({ room: async () => room });
342
+ const cursors = new rig.CursorsCtor();
343
+ rig.setText(cursors, { room: "cursor-only" }, "");
344
+ cursors.connectedCallback?.();
345
+ await rig.flushMicrotasks();
346
+
347
+ cursors.disconnectedCallback?.();
348
+ await rig.flushMicrotasks();
349
+ expect(closeCount).toBe(1);
350
+ });
351
+
352
+ test("keeps a shared cursor active while another cursor element remains", async () => {
353
+ const sent: unknown[] = [];
354
+ let closeCount = 0;
355
+ const room = {
356
+ me: { userId: "self" },
357
+ presence: {
358
+ peers: [],
359
+ set(data: unknown) {
360
+ sent.push(structuredClone(data));
361
+ },
362
+ },
363
+ on: () => () => {},
364
+ async close() {
365
+ closeCount++;
366
+ },
367
+ };
368
+ const rig = makeRig({ room: async () => room });
369
+ const first = new rig.CursorsCtor();
370
+ const second = new rig.CursorsCtor();
371
+ rig.setText(first, { room: "shared" }, "");
372
+ rig.setText(second, { room: "shared" }, "");
373
+ first.connectedCallback?.();
374
+ await rig.flushMicrotasks();
375
+
376
+ rig.fireDocumentEvent("pointermove", { pageX: 100, pageY: 100, clientX: 100, clientY: 100 });
377
+ second.connectedCallback?.();
378
+ await rig.flushMicrotasks();
379
+ first.disconnectedCallback?.();
380
+ expect(sent.at(-1)).toEqual({ __cursor: { x: 0.1, y: 0.2 } });
381
+ expect(closeCount).toBe(0);
382
+
383
+ second.disconnectedCallback?.();
384
+ await rig.flushMicrotasks();
385
+ expect(sent.at(-1)).toEqual({});
386
+ expect(closeCount).toBe(1);
387
+ });
388
+
389
+ test("uses a page room reopened after close", async () => {
390
+ const firstSent: unknown[] = [];
391
+ const reopenedSent: unknown[] = [];
392
+ let firstCloseCount = 0;
393
+ let reopenedCloseCount = 0;
394
+ const makeRoom = (sent: unknown[], onClose: () => void) => ({
395
+ me: { userId: "self" },
396
+ presence: {
397
+ peers: [],
398
+ set(data: unknown) {
399
+ sent.push(structuredClone(data));
400
+ },
401
+ },
402
+ on: () => () => {},
403
+ async close() {
404
+ onClose();
405
+ },
406
+ });
407
+ const firstRoom = makeRoom(firstSent, () => firstCloseCount++);
408
+ const reopenedRoom = makeRoom(reopenedSent, () => reopenedCloseCount++);
409
+ let roomOpens = 0;
410
+ const swarmSdk = {
411
+ room: async (_name?: string) => (roomOpens++ === 0 ? firstRoom : reopenedRoom),
412
+ };
413
+ const rig = makeRig(swarmSdk);
414
+ const board = await swarmSdk.room("board");
415
+ await board.close();
416
+ await swarmSdk.room("board");
417
+
418
+ const cursors = new rig.CursorsCtor();
419
+ rig.setText(cursors, { room: "board" }, "");
420
+ cursors.connectedCallback?.();
421
+ await rig.flushMicrotasks();
422
+ rig.fireDocumentEvent("pointermove", { pageX: 300, pageY: 100, clientX: 300, clientY: 100 });
423
+
424
+ expect(roomOpens).toBe(2);
425
+ expect(firstSent).toHaveLength(0);
426
+ expect(reopenedSent.at(-1)).toEqual({ __cursor: { x: 0.3, y: 0.2 } });
427
+ cursors.disconnectedCallback?.();
428
+ expect(reopenedCloseCount).toBe(0);
429
+ });
430
+
431
+ test("renders remote people and removes an unchanged cursor after idle", async () => {
432
+ const roomHandlers = new Map<string, (value: unknown) => void>();
433
+ const room = {
434
+ me: { userId: "self" },
435
+ presence: { peers: [], set: () => {} },
436
+ on(name: string, handler: (value: unknown) => void) {
437
+ roomHandlers.set(name, handler);
438
+ return () => roomHandlers.delete(name);
439
+ },
440
+ async close() {},
441
+ };
442
+ const rig = makeRig({ room: async () => room });
443
+ const cursors = new rig.CursorsCtor();
444
+ rig.setText(cursors, { room: "board" }, "");
445
+ cursors.connectedCallback?.();
446
+
447
+ const realNow = Date.now;
448
+ let now = 1000;
449
+ Date.now = () => now;
450
+ try {
451
+ await rig.flushMicrotasks();
452
+ const peers = [
453
+ { userId: "self", name: "Self", kind: "human", data: { __cursor: { x: 0.1, y: 0.1 } } },
454
+ { userId: "agent-1", name: "Agent", kind: "agent", data: { __cursor: { x: 0.9, y: 0.9 } } },
455
+ {
456
+ userId: "person-1",
457
+ name: "Avery",
458
+ kind: "human",
459
+ data: { __cursor: { x: 0.25, y: 0.5 } },
460
+ },
461
+ ];
462
+ roomHandlers.get("presence")?.(peers);
463
+
464
+ const overlay = rig.body.children[0] as StubInstance;
465
+ expect(overlay.children).toHaveLength(1);
466
+ expect(overlay.children[0]?.innerHTML).toContain("Avery");
467
+ expect(overlay.children[0]?.innerHTML).toContain("<svg");
468
+ rig.fireNextFrame();
469
+ expect(overlay.children[0]?.style.transform).toBe("translate3d(250px,250px,0)");
470
+
471
+ now = 5000;
472
+ roomHandlers.get("presence")?.(peers);
473
+ expect(overlay.children).toHaveLength(0);
474
+ } finally {
475
+ Date.now = realNow;
476
+ cursors.disconnectedCallback?.();
477
+ }
478
+ });
479
+ });
480
+
175
481
  describe("<swarm-diff> render", () => {
176
482
  test("constructs and renders without throwing on the example input", async () => {
177
483
  const html = await renderViaStub(JSON.stringify(EXAMPLE_HUNK), {
@@ -831,7 +831,7 @@ describe("worker task follow-up creation", () => {
831
831
  expect(rows[0]!.task).not.toContain("post the URL");
832
832
  });
833
833
 
834
- test("inherits followUpConfig from parent task when child has no override", async () => {
834
+ test("a continuation inherits followUpConfig from its parent when it has no override", async () => {
835
835
  await createAgent({
836
836
  name: "follow-up-lead-inheritance",
837
837
  isLead: true,
@@ -851,6 +851,7 @@ describe("worker task follow-up creation", () => {
851
851
  const child = await createTaskExtended("Child task", {
852
852
  agentId: worker.id,
853
853
  parentTaskId: parent.id,
854
+ inheritParentFollowUpConfig: true,
854
855
  });
855
856
  await startTask(child.id);
856
857
 
@@ -206,7 +206,9 @@ describe("GET /api/tasks — requestedByUserId route wiring", () => {
206
206
  requestedByUserId: routeUserId,
207
207
  });
208
208
 
209
- const res = await listTasks("&requestedByUserId=00000000-0000-4000-8000-000000000000");
209
+ const res = await listTasks(
210
+ "&requestedByUserId=00000000-0000-4000-8000-000000000000&includeTotal=true",
211
+ );
210
212
  expect(res.status).toBe(200);
211
213
  const body = (await res.json()) as { tasks: unknown[]; total: number };
212
214
  expect(body.tasks).toEqual([]);
@@ -378,7 +378,7 @@ describe("Tool Annotations & Classification", () => {
378
378
  // Includes 11 skill tools, 7 MCP server tools, reusable script tools, and the
379
379
  // native Kapso/WhatsApp tools (register/unregister number + send/reply message).
380
380
  expect(count).toBeGreaterThanOrEqual(45);
381
- expect(count).toBeLessThanOrEqual(148);
381
+ expect(count).toBeLessThanOrEqual(149);
382
382
  });
383
383
 
384
384
  test("core tools are fewer than deferred tools", () => {
@@ -0,0 +1,158 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
+ import { unlink } from "node:fs/promises";
3
+ import { createServer as createHttpServer, type Server } from "node:http";
4
+ import {
5
+ closeDb,
6
+ createApprovalRequest,
7
+ createScheduledTask,
8
+ initDb,
9
+ listApprovalRequestSummaries,
10
+ listApprovalRequests,
11
+ resolveApprovalRequest,
12
+ } from "../be/db";
13
+ import { handleApprovalRequests } from "../http/approval-requests";
14
+ import { handleStats } from "../http/stats";
15
+ import { listenOnFreePort } from "./test-net";
16
+
17
+ const TEST_DB_PATH = "./test-ui-list-slim-projections.sqlite";
18
+
19
+ /**
20
+ * `/api/scheduled-tasks` and `/api/approval-requests` serve the UI list pages.
21
+ * `fields=slim` is opt-in: callers that don't pass it keep the full rows.
22
+ */
23
+ describe("UI list slim projections", () => {
24
+ let server: Server;
25
+ let baseUrl = "";
26
+ let approvalId = "";
27
+ const template = `T${"x".repeat(5000)}`;
28
+
29
+ beforeAll(async () => {
30
+ for (const suffix of ["", "-wal", "-shm"]) {
31
+ try {
32
+ await unlink(`${TEST_DB_PATH}${suffix}`);
33
+ } catch {}
34
+ }
35
+ initDb(TEST_DB_PATH);
36
+
37
+ server = createHttpServer(async (req, res) => {
38
+ const url = new URL(req.url ?? "/", "http://localhost");
39
+ const segments = url.pathname.split("/").filter(Boolean);
40
+ if (await handleStats(req, res, segments, url.searchParams)) return;
41
+ if (await handleApprovalRequests(req, res, segments, url.searchParams)) return;
42
+ res.writeHead(404);
43
+ res.end();
44
+ });
45
+ baseUrl = `http://127.0.0.1:${await listenOnFreePort(server)}`;
46
+
47
+ await createScheduledTask({
48
+ name: "slim-list-schedule",
49
+ taskTemplate: template,
50
+ cronExpression: "0 9 * * *",
51
+ description: "daily report",
52
+ });
53
+
54
+ approvalId = crypto.randomUUID();
55
+ await createApprovalRequest({
56
+ id: approvalId,
57
+ title: "Ship it?",
58
+ questions: [
59
+ { id: "q1", type: "approval", label: "Approve?", description: "D".repeat(2000) },
60
+ { id: "q2", type: "text", label: "Why?" },
61
+ ],
62
+ approvers: { policy: "any" },
63
+ sourceTaskId: "source-task-1",
64
+ timeoutSeconds: 3600,
65
+ notificationChannels: [{ channel: "slack", target: "C123" }],
66
+ });
67
+ await resolveApprovalRequest(approvalId, {
68
+ status: "approved",
69
+ responses: { q1: { approved: true }, q2: "because" },
70
+ resolvedBy: "user-1",
71
+ });
72
+ });
73
+
74
+ afterAll(async () => {
75
+ await new Promise<void>((resolve) => server.close(() => resolve()));
76
+ closeDb();
77
+ for (const suffix of ["", "-wal", "-shm"]) {
78
+ try {
79
+ await unlink(`${TEST_DB_PATH}${suffix}`);
80
+ } catch {}
81
+ }
82
+ });
83
+
84
+ async function get(path: string) {
85
+ const res = await fetch(`${baseUrl}${path}`);
86
+ expect(res.status).toBe(200);
87
+ return (await res.json()) as Record<string, Array<Record<string, unknown>>>;
88
+ }
89
+
90
+ test("/api/scheduled-tasks keeps full rows by default", async () => {
91
+ const { scheduledTasks } = await get("/api/scheduled-tasks");
92
+ const row = scheduledTasks.find((s) => s.name === "slim-list-schedule");
93
+ expect(row?.taskTemplate).toBe(template);
94
+ expect(row && "taskTemplatePreview" in row).toBe(false);
95
+ });
96
+
97
+ test("/api/scheduled-tasks?fields=slim swaps taskTemplate for a preview", async () => {
98
+ const { scheduledTasks } = await get("/api/scheduled-tasks?fields=slim");
99
+ const row = scheduledTasks.find((s) => s.name === "slim-list-schedule");
100
+ expect(row).toBeDefined();
101
+ expect(row && "taskTemplate" in row).toBe(false);
102
+ expect(String(row?.taskTemplatePreview).length).toBeLessThan(template.length);
103
+ // Every other field the schedules list page reads is identical to the full row.
104
+ const full = (await get("/api/scheduled-tasks")).scheduledTasks.find(
105
+ (s) => s.name === "slim-list-schedule",
106
+ );
107
+ const { taskTemplate: _omitted, ...fullWithoutTemplate } = full ?? {};
108
+ const { taskTemplatePreview: _preview, ...slimWithoutPreview } = row ?? {};
109
+ expect(slimWithoutPreview).toEqual(fullWithoutTemplate);
110
+ });
111
+
112
+ test("/api/approval-requests keeps full rows by default", async () => {
113
+ const { approvalRequests } = await get("/api/approval-requests");
114
+ const row = approvalRequests.find((r) => r.id === approvalId);
115
+ expect(row?.questions).toHaveLength(2);
116
+ expect(row?.responses).toEqual({ q1: { approved: true }, q2: "because" });
117
+ expect(row && "questionCount" in row).toBe(false);
118
+ });
119
+
120
+ test("/api/approval-requests?fields=slim drops bodies and counts questions", async () => {
121
+ const { approvalRequests } = await get("/api/approval-requests?fields=slim&status=approved");
122
+ const row = approvalRequests.find((r) => r.id === approvalId);
123
+ expect(row).toBeDefined();
124
+ for (const dropped of [
125
+ "questions",
126
+ "approvers",
127
+ "responses",
128
+ "resolutionReason",
129
+ "notificationChannels",
130
+ ]) {
131
+ expect(row && dropped in row).toBe(false);
132
+ }
133
+ expect(row?.questionCount).toBe(2);
134
+ expect(row?.title).toBe("Ship it?");
135
+ expect(row?.status).toBe("approved");
136
+ expect(row?.resolvedBy).toBe("user-1");
137
+ expect(row?.sourceTaskId).toBe("source-task-1");
138
+ expect(row?.workflowRunId).toBeNull();
139
+ expect(typeof row?.createdAt).toBe("string");
140
+ expect(typeof row?.expiresAt).toBe("string");
141
+ });
142
+
143
+ test("slim and full DB reads agree on filters, order and limit", async () => {
144
+ for (let i = 0; i < 3; i++) {
145
+ await createApprovalRequest({
146
+ id: crypto.randomUUID(),
147
+ title: `pending ${i}`,
148
+ questions: [{ id: "q1", type: "boolean", label: "Ok?" }],
149
+ approvers: { policy: "any" },
150
+ });
151
+ }
152
+ const filters = { status: "pending", limit: 2 };
153
+ const full = await listApprovalRequests(filters);
154
+ const slim = await listApprovalRequestSummaries(filters);
155
+ expect(slim.map((r) => r.id)).toEqual(full.map((r) => r.id));
156
+ expect(slim.map((r) => r.questionCount)).toEqual(full.map((r) => r.questions.length));
157
+ });
158
+ });