@desplega.ai/agent-swarm 2.7.6 → 2.7.7

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 (244) hide show
  1. package/dist/{acp-adapter-bp91d37f.js → acp-adapter-7b4vk3h5.js} +5 -4
  2. package/dist/{actions-yj0eeaqh.js → actions-3tt153af.js} +18 -18
  3. package/dist/{amp-adapter-4pg9h34q.js → amp-adapter-njpts0yr.js} +1 -1
  4. package/dist/{anthropic-messages-x4q6rfq5.js → anthropic-messages-xey2skwx.js} +7 -7
  5. package/dist/{app-909e97cr.js → app-nky4378f.js} +3 -3
  6. package/dist/{assistant-qdyb5fyy.js → assistant-jnb669pa.js} +19 -19
  7. package/dist/{azure-openai-responses-s1w5n70z.js → azure-openai-responses-26t7j9ra.js} +5 -5
  8. package/dist/{boot-reembed-fk0s9z18.js → boot-reembed-9mgvaztt.js} +1 -1
  9. package/dist/{boot-reembed-gs1rzrnh.js → boot-reembed-qs2djdej.js} +1 -1
  10. package/dist/{boot-scrub-sweep-4qfd4kfx.js → boot-scrub-sweep-yjpdj7s1.js} +3 -3
  11. package/dist/{boot-seeds-yr00k13d.js → boot-seeds-5cm6ambd.js} +4 -4
  12. package/dist/{boxd-vdmfssd9.js → boxd-nv507va8.js} +1 -1
  13. package/dist/{claude-adapter-qta1f1xn.js → claude-adapter-fn287zmm.js} +5 -4
  14. package/dist/{claude-managed-setup-sc6j4dm0.js → claude-managed-setup-wtdz2mj2.js} +1 -1
  15. package/dist/{claude-sdk-session-zgrz6yvc.js → claude-sdk-session-x91kqyhm.js} +8288 -8224
  16. package/dist/{cli-wp9sp1qb.js → cli-08bmz9z9.js} +1 -1
  17. package/dist/{cli-yy3rhsdw.js → cli-1c6tzt8g.js} +7 -7
  18. package/dist/{cli-q0k1w5zd.js → cli-35rsp8ye.js} +1 -1
  19. package/dist/{cli-xnrwazav.js → cli-3fg0bdcv.js} +1 -1
  20. package/dist/{cli-89knwags.js → cli-5b456bpf.js} +2 -2
  21. package/dist/{cli-drr5v6av.js → cli-5jnhv72d.js} +1 -1
  22. package/dist/{cli-dxae2yg7.js → cli-5zx699j9.js} +2 -2
  23. package/dist/{cli-0at8w34w.js → cli-6g0hb4cp.js} +8 -8
  24. package/dist/{cli-e3r2neck.js → cli-6pfd54vx.js} +5 -5
  25. package/dist/{cli-bsn8nhrq.js → cli-6vcp76qk.js} +3 -3
  26. package/dist/{cli-ekwks2vs.js → cli-72v799xn.js} +1 -1
  27. package/dist/cli-82j36f2m.js +45 -0
  28. package/dist/{cli-yx4kfngt.js → cli-8305gbq0.js} +29 -16
  29. package/dist/{cli-4necwtsv.js → cli-8sckt74b.js} +8 -8
  30. package/dist/{cli-vvpx3gsx.js → cli-95jzxcp3.js} +1 -1
  31. package/dist/{cli-hr2e2fft.js → cli-adeegh7d.js} +7 -7
  32. package/dist/{cli-m48g6p95.js → cli-afkqkrtw.js} +3 -3
  33. package/dist/{cli-0qatpy8q.js → cli-ayayvfg4.js} +24 -4
  34. package/dist/{cli-bqt1mptt.js → cli-az48n0nr.js} +4 -4
  35. package/dist/{cli-zm72anhg.js → cli-bfv7vdg6.js} +9 -9
  36. package/dist/{cli-hgaazk69.js → cli-caj33vcq.js} +5 -5
  37. package/dist/{cli-3ypk9vk1.js → cli-cb2a8pna.js} +1 -1
  38. package/dist/{cli-waqbxg08.js → cli-cppkf404.js} +17 -17
  39. package/dist/{cli-yn4kxs06.js → cli-ddaanqkd.js} +4 -4
  40. package/dist/{cli-386tyxxg.js → cli-e0kmphd6.js} +2 -2
  41. package/dist/{cli-54t00gt8.js → cli-e46hg0xw.js} +1 -1
  42. package/dist/{cli-jm2vfpfd.js → cli-e5bzrn01.js} +1 -1
  43. package/dist/{cli-436532ck.js → cli-excnnjg2.js} +165 -153
  44. package/dist/{cli-71ptav7x.js → cli-ferkkrg9.js} +1 -1
  45. package/dist/{cli-vpf89f34.js → cli-fmjd0t1y.js} +3 -3
  46. package/dist/{cli-pd7vagab.js → cli-fyt2g4wd.js} +3 -3
  47. package/dist/{cli-gdhpcbhf.js → cli-g08zdaec.js} +1 -1
  48. package/dist/{cli-ehq736cn.js → cli-g7s47cc5.js} +4 -4
  49. package/dist/{cli-svj5090z.js → cli-gz6q4eve.js} +3 -3
  50. package/dist/{cli-8snvgs5r.js → cli-je518txm.js} +10 -10
  51. package/dist/{cli-rmmmrjkd.js → cli-jtjtabxe.js} +1 -1
  52. package/dist/{cli-611a1294.js → cli-k0wwfqbg.js} +18 -1
  53. package/dist/{cli-es6va8jk.js → cli-k1nje3v3.js} +38 -43
  54. package/dist/{cli-p5gp8pcx.js → cli-k8196sh2.js} +6 -6
  55. package/dist/{cli-nwj0ee2w.js → cli-k899ehvh.js} +2 -2
  56. package/dist/{cli-decfb6v8.js → cli-m0da8j35.js} +23 -9
  57. package/dist/{cli-he9wchme.js → cli-mkk9rnzz.js} +4 -4
  58. package/dist/{cli-nhrvpaj7.js → cli-n6bv12jn.js} +510 -4
  59. package/dist/{cli-8bc1w0tw.js → cli-nmzhp1b5.js} +14 -12
  60. package/dist/{cli-0yv5z2pg.js → cli-p82cnnjq.js} +1 -1
  61. package/dist/{cli-c60vetmm.js → cli-pc6n7fre.js} +1 -1
  62. package/dist/{cli-xds5f77d.js → cli-pjctyea3.js} +2 -2
  63. package/dist/{cli-xbbndvjy.js → cli-pq73h5mz.js} +3 -3
  64. package/dist/{cli-aa6yxc7m.js → cli-pz4rk021.js} +1 -1
  65. package/dist/{cli-xnqgpshy.js → cli-q3tkegby.js} +1 -1
  66. package/dist/cli-qghe0h66.js +67 -0
  67. package/dist/{cli-zbbwez0r.js → cli-qh4d4tvk.js} +4 -4
  68. package/dist/{cli-w6dt9ged.js → cli-rtt8g4v9.js} +1 -1
  69. package/dist/{cli-1h3wcpbw.js → cli-snpj5zbj.js} +2 -2
  70. package/dist/{cli-a7v854rf.js → cli-tgrv9cjx.js} +263 -12
  71. package/dist/{cli-faxe6thg.js → cli-vz22fyqv.js} +186 -54
  72. package/dist/{cli-k2gzwsj2.js → cli-wk5yrfwd.js} +160 -26
  73. package/dist/{cli-shpbx5ee.js → cli-ws19768n.js} +1641 -49
  74. package/dist/{cli-70r77n04.js → cli-xae4g51r.js} +1 -1
  75. package/dist/{cli-kqdg1bsb.js → cli-yndmfxb3.js} +2 -2
  76. package/dist/{cli-bjb3z51z.js → cli-yyj3j5m4.js} +2 -2
  77. package/dist/cli.js +21 -21
  78. package/dist/{cloudflare-workers-ai-system-one-bm60fjm0.js → cloudflare-workers-ai-system-one-ef2a980q.js} +2 -2
  79. package/dist/{codex-adapter-ayd1fb6h.js → codex-adapter-cf2yxe9m.js} +3 -2
  80. package/dist/{codex-hook-9efm2x1m.js → codex-hook-a0c6g6t6.js} +1 -1
  81. package/dist/{codex-session-runner-jesa32xc.js → codex-session-runner-6vfzdgbd.js} +3 -2
  82. package/dist/{commands-hanewgf2.js → commands-t3mfy7vn.js} +3 -3
  83. package/dist/{controller-dt1n3c1q.js → controller-q7xrbaz0.js} +3 -3
  84. package/dist/{controllers-connect-xx8x64n5.js → controllers-connect-c6nm70kg.js} +19 -19
  85. package/dist/{cursor-adapter-9s4dwwdw.js → cursor-adapter-ftd81ga8.js} +1 -1
  86. package/dist/{db-3vttcbv4.js → db-dk572x25.js} +3 -3
  87. package/dist/{db-9dv0g1ab.js → db-vemtpmcc.js} +4 -2
  88. package/dist/{dispatcher-tx9hd1sv.js → dispatcher-kh7ecxh9.js} +17 -17
  89. package/dist/{dsh-adapter-007y4ejs.js → dsh-adapter-v8628r89.js} +1 -1
  90. package/dist/{e2b-we6hs6q5.js → e2b-fr12yccx.js} +1 -1
  91. package/dist/{e2b-a19d28rz.js → e2b-x6ywzak3.js} +1 -1
  92. package/dist/{easter-egg-3d-rgbrqb8s.js → easter-egg-3d-nenw4x63.js} +1 -1
  93. package/dist/{embedding-space-ny1e2940.js → embedding-space-0jepb239.js} +6 -6
  94. package/dist/{embeddings-2t386t13.js → embeddings-xxt49yxg.js} +1 -1
  95. package/dist/{execute-pksh0ceh.js → execute-1mhw6e4x.js} +1 -1
  96. package/dist/{external-channel-phgmhmd7.js → external-channel-55v12e60.js} +2 -2
  97. package/dist/{fx-adapter-fy8rr921.js → fx-adapter-bs8wqrk5.js} +6 -5
  98. package/dist/{google-generative-ai-8y3m3k4b.js → google-generative-ai-v94hr7h0.js} +5 -5
  99. package/dist/{google-vertex-w3v98n7b.js → google-vertex-g7vr6cf8.js} +5 -5
  100. package/dist/{grok-adapter-q0abvpaj.js → grok-adapter-gpjqmkcq.js} +6 -5
  101. package/dist/handlers-zxmp1ta4.js +32 -0
  102. package/dist/{heartbeat-kadyek8p.js → heartbeat-k42svnr0.js} +20 -20
  103. package/dist/{hook-9k551xnj.js → hook-114m7hap.js} +4 -3
  104. package/dist/{hook-5zs3msf6.js → hook-6nphweh0.js} +24 -9
  105. package/dist/{http-mhz9vw1f.js → http-r8cc4wz7.js} +1025 -173
  106. package/dist/index-a8phxqnk.js +37 -0
  107. package/dist/{index-vdygbq10.js → index-e3s8wmxy.js} +1 -1
  108. package/dist/{index-q361cg2f.js → index-fpk4bcd9.js} +1 -1
  109. package/dist/index-k6h00t4c.js +30 -0
  110. package/dist/{index-rvr00sg7.js → index-nh3wyqdt.js} +1017 -574
  111. package/dist/index-xrz7te8h.js +33 -0
  112. package/dist/{keepalive-w56f32cg.js → keepalive-x060nt9p.js} +3 -3
  113. package/dist/lead-956sd2yc.js +71 -0
  114. package/dist/{maintenance-7mdgxcea.js → maintenance-2xxfexhf.js} +1 -1
  115. package/dist/{mistral-conversations-4pmwj1c0.js → mistral-conversations-3gjpdb4b.js} +4 -4
  116. package/dist/{oauth-refresh-sweep-29txy9vk.js → oauth-refresh-sweep-fckc4ar5.js} +2 -2
  117. package/dist/{onboard-kpsrkarj.js → onboard-zc48j237.js} +3 -3
  118. package/dist/{openai-codex-responses-44j573nm.js → openai-codex-responses-8pbxq9h5.js} +5 -5
  119. package/dist/{openai-completions-a4gee2a8.js → openai-completions-rm00s912.js} +4 -4
  120. package/dist/{openai-decisions-w4qkrcar.js → openai-decisions-8v957r2r.js} +1 -1
  121. package/dist/{openai-responses-rbp6h5kr.js → openai-responses-agxpf96k.js} +5 -5
  122. package/dist/{opencode-adapter-j25vm3rr.js → opencode-adapter-r690h1cr.js} +4 -3
  123. package/dist/{otel-impl-6kkct7a8.js → otel-impl-r5xbh81q.js} +4 -4
  124. package/dist/{pi-mono-adapter-exhkzhn2.js → pi-mono-adapter-c7v8sdec.js} +7 -6
  125. package/dist/{pricing-refresh-n2x7jh6k.js → pricing-refresh-3pmre11v.js} +3 -3
  126. package/dist/{rbac-roles-81ypx6es.js → rbac-roles-3vbshwsb.js} +1 -1
  127. package/dist/{rbac-roles-b5htkjby.js → rbac-roles-910mmkgz.js} +2 -2
  128. package/dist/{render-v2-qn6jmpk4.js → render-v2-pxykkbab.js} +3 -3
  129. package/dist/{resume-wd30wx23.js → resume-adda0bwy.js} +6 -6
  130. package/dist/{runtime-4hsg7h60.js → runtime-fnbm7stk.js} +1 -1
  131. package/dist/{session-access-5qvfevhk.js → session-access-2fvamrdc.js} +4 -4
  132. package/dist/{session-source-backfill-n1czavbr.js → session-source-backfill-08xvv0sm.js} +2 -2
  133. package/dist/{setup-dnb56qy5.js → setup-1aqmvmh1.js} +1 -1
  134. package/dist/{typesafe-system-one-5mavm259.js → typesafe-system-one-0bmqcxfg.js} +2 -2
  135. package/dist/{virtual-modules-76xxv9fs.js → virtual-modules-d2pjk9j1.js} +5 -5
  136. package/dist/{work-objects-4n21tfkm.js → work-objects-9s0yrm5j.js} +20 -20
  137. package/dist/{worker-follow-up-77hvzxww.js → worker-follow-up-3q78fqmr.js} +17 -17
  138. package/dist/worker-y15hy08h.js +71 -0
  139. package/openapi.json +197 -4
  140. package/package.json +5 -5
  141. package/src/apps/definition.ts +6 -5
  142. package/src/be/db/page-theme-tokens.ts +12 -0
  143. package/src/be/db/tasks/read.ts +9 -1
  144. package/src/be/db/tasks/write.ts +19 -0
  145. package/src/be/db-client.ts +107 -7
  146. package/src/be/db.ts +34 -2
  147. package/src/be/migrations/269_pages_design_system.sql +23 -0
  148. package/src/be/seed/schedules-seeder.ts +0 -10
  149. package/src/be/seed-scripts/catalog/catalog-report.inline.ts +2 -0
  150. package/src/be/seed-scripts/catalog/catalog-report.ts +2 -0
  151. package/src/be/seed-scripts/catalog/memory-eval.ts +2 -0
  152. package/src/be/seed-scripts/catalog/swarm-update-check.ts +150 -0
  153. package/src/be/seed-scripts/index.ts +9 -0
  154. package/src/be/seed-skills/bundled-files.generated.json +3 -3
  155. package/src/be/swarm-config-guard.ts +32 -0
  156. package/src/be/task-auto-title-listener.ts +127 -0
  157. package/src/commands/onboard/agent-fs-version.ts +1 -1
  158. package/src/commands/profile-sync.ts +151 -16
  159. package/src/commands/runner.ts +15 -7
  160. package/src/hooks/hook.ts +20 -9
  161. package/src/http/index.ts +3 -0
  162. package/src/http/pages-public.ts +93 -129
  163. package/src/http/pages.ts +94 -21
  164. package/src/pages/browser-sdk.ts +48 -21
  165. package/src/pages/color-literals.ts +115 -0
  166. package/src/pages/page-head.ts +766 -0
  167. package/src/pages/version.ts +3 -0
  168. package/src/providers/amp-adapter.ts +1 -1
  169. package/src/providers/codex-adapter.ts +1 -0
  170. package/src/providers/codex-app-server.ts +27 -1
  171. package/src/providers/pi-mono-extension.ts +30 -15
  172. package/src/tasks/follow-up-gate.ts +318 -0
  173. package/src/tasks/worker-follow-up.ts +21 -1
  174. package/src/tests/codex-app-server.test.ts +59 -0
  175. package/src/tests/codex-gateway-route.test.ts +50 -1
  176. package/src/tests/create-page-tool.test.ts +501 -0
  177. package/src/tests/db-client-busy-retry.test.ts +81 -0
  178. package/src/tests/db-task-read-contract.test.ts +1 -0
  179. package/src/tests/db-task-write-contract.test.ts +2 -0
  180. package/src/tests/db-tasks-read.test.ts +21 -0
  181. package/src/tests/fixtures/pages/head-v1.html +56 -0
  182. package/src/tests/follow-up-gate-shadow.test.ts +513 -0
  183. package/src/tests/identity-field-budget.test.ts +1 -1
  184. package/src/tests/internal-ai/credentials.test.ts +62 -0
  185. package/src/tests/internal-ai/task-title.test.ts +283 -0
  186. package/src/tests/merge-gate-detect-changes.test.ts +145 -0
  187. package/src/tests/page-proxy.test.ts +1 -1
  188. package/src/tests/page-session-dev-cookie.test.ts +188 -0
  189. package/src/tests/pages-frame-ancestors.test.ts +28 -1
  190. package/src/tests/pages-head-v2.test.ts +534 -0
  191. package/src/tests/pages-http.test.ts +330 -0
  192. package/src/tests/pages-public-html.test.ts +43 -0
  193. package/src/tests/pages-storage.test.ts +182 -0
  194. package/src/tests/pages-versioning.test.ts +72 -0
  195. package/src/tests/profile-sync-stale-writeback.test.ts +334 -0
  196. package/src/tests/profile-sync.test.ts +11 -15
  197. package/src/tests/scheduled-task-required-params-migration.test.ts +3 -1
  198. package/src/tests/script-connections.test.ts +2 -0
  199. package/src/tests/seed-registry.test.ts +28 -6
  200. package/src/tests/seed-schedules.test.ts +61 -8
  201. package/src/tests/seed-scripts.test.ts +142 -2
  202. package/src/tests/swarm-diff.test.ts +121 -2
  203. package/src/tests/task-auto-title-config-guard.test.ts +37 -0
  204. package/src/tests/task-auto-title-listener.test.ts +165 -0
  205. package/src/tests/theme-import.test.ts +758 -0
  206. package/src/tests/themes-page-theme.test.ts +155 -0
  207. package/src/tests/themes-presets.test.ts +172 -0
  208. package/src/tests/workflow-script-principal.test.ts +2 -1
  209. package/src/themes/classics.ts +949 -0
  210. package/src/themes/import.ts +420 -0
  211. package/src/themes/page-fields.ts +40 -0
  212. package/src/themes/page-theme.ts +102 -0
  213. package/src/themes/presets.ts +561 -0
  214. package/src/themes/tokens.ts +218 -0
  215. package/src/tools/app-upsert.ts +1 -1
  216. package/src/tools/create-page.ts +118 -12
  217. package/src/tools/utils.ts +33 -0
  218. package/src/types.ts +45 -3
  219. package/src/utils/constants.ts +19 -0
  220. package/src/utils/follow-up-gate-schedule-ids.ts +13 -0
  221. package/src/utils/internal-ai/credentials.ts +146 -93
  222. package/src/utils/internal-ai/index.ts +10 -0
  223. package/src/utils/internal-ai/models.ts +18 -0
  224. package/src/utils/internal-ai/task-title.ts +282 -0
  225. package/src/utils/page-session.ts +37 -0
  226. package/templates/schedules/daily-status-report/content.md +88 -2
  227. package/templates/schedules/daily-swarm-update-check/content.md +16 -40
  228. package/templates/skills/apps/SKILL.md +1 -1
  229. package/templates/skills/apps/content.md +1 -1
  230. package/templates/skills/heartbeat-runbook/content.md +1 -1
  231. package/templates/skills/pages/config.json +1 -1
  232. package/templates/skills/pages/content.md +539 -86
  233. package/templates/skills/pages/files/examples/annotated-pr.html +21 -12
  234. package/templates/skills/pages/files/examples/multiplayer-board.html +41 -41
  235. package/templates/skills/pages/files/examples/report-page.html +125 -124
  236. package/dist/cli-2pnswgh8.js +0 -189
  237. package/dist/handlers-by3sfxyx.js +0 -32
  238. package/dist/index-4f2pvxae.js +0 -37
  239. package/dist/index-98gz7tmp.js +0 -30
  240. package/dist/index-k08z2cak.js +0 -33
  241. package/dist/lead-enq35nqp.js +0 -70
  242. package/dist/worker-gg1576j3.js +0 -70
  243. package/templates/schedules/daily-workflow-health-audit/config.json +0 -19
  244. package/templates/schedules/daily-workflow-health-audit/content.md +0 -191
@@ -15,6 +15,7 @@ import crypto from "node:crypto";
15
15
  import { unlink } from "node:fs/promises";
16
16
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
17
17
  import { closeDb, getPageBySlug, getPageVersions, initDb } from "../be/db";
18
+ import { COLOR_LITERAL_THRESHOLD, countColorLiterals } from "../pages/color-literals";
18
19
  import { registerCreatePageTool } from "../tools/create-page";
19
20
 
20
21
  const TEST_DB_PATH = "./test-create-page-tool.sqlite";
@@ -211,6 +212,506 @@ describe("create_page MCP tool", () => {
211
212
  expect(row?.passwordHash).not.toBe("example-open-sesame");
212
213
  expect(await Bun.password.verify("example-open-sesame", row!.passwordHash!)).toBe(true);
213
214
  });
215
+
216
+ describe("design fields", () => {
217
+ type ToolResult = {
218
+ isError?: boolean;
219
+ content: Array<{ text: string }>;
220
+ structuredContent: {
221
+ id?: string;
222
+ message: string;
223
+ details?: string;
224
+ nudge?: string;
225
+ designVersion?: number;
226
+ ignoredTokens?: string[];
227
+ colorLiteralCount?: number;
228
+ usesDesignSystem?: boolean;
229
+ lightOnlyColors?: boolean;
230
+ };
231
+ };
232
+
233
+ const NUDGE_TEXT = (count: number) =>
234
+ `This page hardcodes ${count} color values. Use theme tokens (var(--color-primary), bg-primary, text-muted-foreground) so theme and dark-mode switches restyle it. Until then the page stays light in a dark dashboard.`;
235
+ const V1_NUDGE =
236
+ "This page is on designVersion 1 (the original look), so design system classes, tokens and themes have no effect. Pass designVersion: 2.";
237
+ const LIGHT_ONLY_NUDGE =
238
+ "Your themeTokens set colors for light mode only. Dark mode keeps the preset's dark colors; add a dark map (or a .dark block) to restyle it too.";
239
+ const IGNORED_MESSAGE =
240
+ "Some theme tokens were ignored (outside the page token contract or referencing a variable outside it).";
241
+
242
+ const uniqueSlug = () => `design-${crypto.randomUUID().slice(0, 8)}`;
243
+
244
+ /** An HTML page whose <style> block holds exactly `count` color literals. */
245
+ const styledPage = (count: number) =>
246
+ `<h1>Report</h1><style>${Array.from(
247
+ { length: count },
248
+ (_, i) => `.c${i}{color:rgb(${i} 0 0)}`,
249
+ ).join("")}</style>`;
250
+
251
+ async function call(args: Record<string, unknown>): Promise<ToolResult> {
252
+ const tool = buildServer();
253
+ return (await tool.handler(
254
+ {
255
+ title: "Design Page",
256
+ slug: uniqueSlug(),
257
+ body: "<h1>hi</h1>",
258
+ contentType: "text/html",
259
+ authMode: "public",
260
+ ...args,
261
+ },
262
+ fakeMeta,
263
+ )) as ToolResult;
264
+ }
265
+
266
+ test("a new page gets design version 2 with no theme and no tokens", async () => {
267
+ const slug = uniqueSlug();
268
+ const result = await call({ slug });
269
+ expect(result.isError).toBe(false);
270
+ const row = await getPageBySlug(agentId, slug);
271
+ expect(row?.designVersion).toBe(2);
272
+ expect(row?.theme).toBeNull();
273
+ expect(row?.themeTokens).toBeNull();
274
+ expect(result.structuredContent.ignoredTokens).toBeUndefined();
275
+ });
276
+
277
+ test("an explicit design version and a preset theme are stored on create", async () => {
278
+ const slug = uniqueSlug();
279
+ await call({ slug, theme: "paper", designVersion: 1 });
280
+ const row = await getPageBySlug(agentId, slug);
281
+ expect(row?.theme).toBe("paper");
282
+ expect(row?.designVersion).toBe(1);
283
+ });
284
+
285
+ test("an unknown theme returns an error that lists the presets and saves nothing", async () => {
286
+ const slug = uniqueSlug();
287
+ const result = await call({ slug, theme: "neon" });
288
+ expect(result.isError).toBe(true);
289
+ expect(result.structuredContent.message).toContain('Unknown theme "neon"');
290
+ for (const id of ["hive", "paper", "github"]) {
291
+ expect(result.structuredContent.message).toContain(id);
292
+ }
293
+ expect(await getPageBySlug(agentId, slug)).toBeNull();
294
+ });
295
+
296
+ test("shadcn-style tokens are stored normalized and the ignored names are reported", async () => {
297
+ const slug = uniqueSlug();
298
+ const result = await call({
299
+ slug,
300
+ themeTokens: {
301
+ light: {
302
+ "--primary": "oklch(0.55 0.2 260)",
303
+ background: "oklch(1 0 0)",
304
+ radius: "0.5rem",
305
+ "--sidebar": "oklch(0.9 0 0)",
306
+ "--shadow-2xs": "0 1px 2px #0000",
307
+ },
308
+ dark: { "--primary": "oklch(0.7 0.15 260)" },
309
+ },
310
+ });
311
+ expect(result.isError).toBe(false);
312
+ expect(result.structuredContent.ignoredTokens).toEqual(["--sidebar", "--shadow-2xs"]);
313
+ expect(result.structuredContent.message).toContain(IGNORED_MESSAGE);
314
+ expect(result.structuredContent.details).toContain(
315
+ "Ignored theme tokens: --sidebar, --shadow-2xs",
316
+ );
317
+ expect(result.content[0]!.text).toContain("--shadow-2xs");
318
+
319
+ const row = await getPageBySlug(agentId, slug);
320
+ expect(row?.themeTokens).toEqual({
321
+ light: {
322
+ "--color-primary": "oklch(0.55 0.2 260)",
323
+ "--color-background": "oklch(1 0 0)",
324
+ "--radius-sm": "calc(0.5rem - 4px)",
325
+ "--radius-md": "calc(0.5rem - 2px)",
326
+ "--radius-lg": "0.5rem",
327
+ "--radius-xl": "calc(0.5rem + 4px)",
328
+ },
329
+ dark: { "--color-primary": "oklch(0.7 0.15 260)" },
330
+ });
331
+ });
332
+
333
+ test("tokens with nothing to ignore report nothing", async () => {
334
+ const result = await call({
335
+ themeTokens: { light: { "--color-primary": "oklch(0.55 0.2 260)" } },
336
+ });
337
+ expect(result.isError).toBe(false);
338
+ expect(result.structuredContent.ignoredTokens).toBeUndefined();
339
+ expect(result.structuredContent.message).not.toContain("ignored");
340
+ expect(result.structuredContent.details).not.toContain("Ignored");
341
+ });
342
+
343
+ test("the css format works", async () => {
344
+ const slug = uniqueSlug();
345
+ const result = await call({
346
+ slug,
347
+ themeTokens: {
348
+ format: "css",
349
+ css: ":root { --primary: 222.2 47.4% 11.2%; --tracking-normal: 0em; } .dark { --primary: 210 40% 98%; }",
350
+ },
351
+ });
352
+ expect(result.isError).toBe(false);
353
+ expect(result.structuredContent.ignoredTokens).toEqual(["--tracking-normal"]);
354
+ const row = await getPageBySlug(agentId, slug);
355
+ expect(row?.themeTokens).toEqual({
356
+ light: { "--color-primary": "hsl(222.2 47.4% 11.2%)" },
357
+ dark: { "--color-primary": "hsl(210 40% 98%)" },
358
+ });
359
+ });
360
+
361
+ test("bad themeTokens return an error that names the problem and save nothing", async () => {
362
+ const slug = uniqueSlug();
363
+ const unsafe = await call({
364
+ slug,
365
+ themeTokens: { light: { "--color-primary": "red; } body { display: none" } },
366
+ });
367
+ expect(unsafe.isError).toBe(true);
368
+ expect(unsafe.structuredContent.message).toContain("Invalid themeTokens");
369
+ expect(unsafe.structuredContent.message).toContain("--color-primary");
370
+
371
+ const missingCss = await call({ slug, themeTokens: { format: "css" } });
372
+ expect(missingCss.isError).toBe(true);
373
+ expect(missingCss.structuredContent.message).toContain("Invalid themeTokens");
374
+
375
+ expect(await getPageBySlug(agentId, slug)).toBeNull();
376
+ });
377
+
378
+ test("an upsert keeps the design fields it omits and clears them with null", async () => {
379
+ const slug = uniqueSlug();
380
+ const tokens = { light: { "--color-ring": "oklch(0.6 0.2 30)" } };
381
+ await call({ slug, theme: "paper", themeTokens: tokens, designVersion: 1 });
382
+ const stored = {
383
+ theme: "paper",
384
+ themeTokens: { light: { "--color-ring": "oklch(0.6 0.2 30)" }, dark: {} },
385
+ };
386
+
387
+ // Omitted fields keep the stored values, including an older design version.
388
+ await call({ slug, body: "<h1>v2</h1>" });
389
+ let row = await getPageBySlug(agentId, slug);
390
+ expect(row?.body).toBe("<h1>v2</h1>");
391
+ expect(row).toMatchObject({ ...stored, designVersion: 1 });
392
+
393
+ // A given version moves the page. The theme and the tokens stay.
394
+ await call({ slug, body: "<h1>v3</h1>", designVersion: 2 });
395
+ row = await getPageBySlug(agentId, slug);
396
+ expect(row).toMatchObject({ ...stored, designVersion: 2 });
397
+
398
+ // A new theme replaces the old one. The tokens stay.
399
+ await call({ slug, body: "<h1>v4</h1>", theme: "github" });
400
+ row = await getPageBySlug(agentId, slug);
401
+ expect(row).toMatchObject({ theme: "github", themeTokens: stored.themeTokens });
402
+
403
+ // null clears one field at a time.
404
+ await call({ slug, body: "<h1>v5</h1>", theme: null });
405
+ row = await getPageBySlug(agentId, slug);
406
+ expect(row?.theme).toBeNull();
407
+ expect(row?.themeTokens).toEqual(stored.themeTokens);
408
+ await call({ slug, body: "<h1>v6</h1>", themeTokens: null });
409
+ row = await getPageBySlug(agentId, slug);
410
+ expect(row?.themeTokens).toBeNull();
411
+ expect(row?.designVersion).toBe(2);
412
+ });
413
+
414
+ test("a rejected upsert leaves the page and its history alone", async () => {
415
+ const slug = uniqueSlug();
416
+ const first = await call({ slug, body: "<h1>kept</h1>", theme: "paper" });
417
+ const result = await call({ slug, body: "<h1>lost</h1>", theme: "neon" });
418
+ expect(result.isError).toBe(true);
419
+
420
+ const row = await getPageBySlug(agentId, slug);
421
+ expect(row?.body).toBe("<h1>kept</h1>");
422
+ expect(row?.theme).toBe("paper");
423
+ expect(await getPageVersions(first.structuredContent.id!)).toHaveLength(0);
424
+ });
425
+
426
+ test("a reference outside the contract is reported with the reference", async () => {
427
+ const result = await call({
428
+ themeTokens: {
429
+ format: "css",
430
+ css: ":root { --primary: oklch(0.55 0.2 260); --font-sans: var(--font-geist-sans); } .dark { --primary: oklch(0.7 0.15 260); }",
431
+ },
432
+ });
433
+ expect(result.isError).toBe(false);
434
+ expect(result.structuredContent.ignoredTokens).toEqual([
435
+ "--font-sans (references --font-geist-sans)",
436
+ ]);
437
+ expect(result.structuredContent.message).toContain(IGNORED_MESSAGE);
438
+ expect(result.structuredContent.details).toContain(
439
+ "Ignored theme tokens: --font-sans (references --font-geist-sans)",
440
+ );
441
+ });
442
+
443
+ test("the result reports the saved design version in data and in the message", async () => {
444
+ const slug = uniqueSlug();
445
+ const created = await call({ slug });
446
+ expect(created.structuredContent.designVersion).toBe(2);
447
+ expect(created.structuredContent.message).toContain("designVersion=2)");
448
+
449
+ const legacy = await call({ designVersion: 1 });
450
+ expect(legacy.structuredContent.designVersion).toBe(1);
451
+ expect(legacy.structuredContent.message).toContain("designVersion=1)");
452
+
453
+ const json = await call({ body: '{"type":"text"}', contentType: "application/json" });
454
+ expect(json.structuredContent.designVersion).toBe(2);
455
+ expect(json.structuredContent.usesDesignSystem).toBeUndefined();
456
+ expect(json.structuredContent.lightOnlyColors).toBeUndefined();
457
+ });
458
+
459
+ test("a design version 1 HTML page that uses the design system gets the version nudge", async () => {
460
+ const designSystemCalls: Array<Record<string, unknown>> = [
461
+ { body: '<main class="swarm-page"><h1>Report</h1></main>' },
462
+ { body: "<p class='lead swarm-lede'>x</p>" },
463
+ { body: "<style>.a{color:var(--color-primary)}</style>" },
464
+ { theme: "paper" },
465
+ { themeTokens: { light: { "--radius-lg": "8px" } } },
466
+ ];
467
+ for (const extra of designSystemCalls) {
468
+ const result = await call({ designVersion: 1, ...extra });
469
+ expect(result.isError).toBe(false);
470
+ expect(result.structuredContent.usesDesignSystem).toBe(true);
471
+ expect(result.structuredContent.nudge).toBe(V1_NUDGE);
472
+ }
473
+ });
474
+
475
+ test("the version nudge stays quiet on plain v1 pages, on v2 pages and on other content types", async () => {
476
+ const plain = await call({
477
+ designVersion: 1,
478
+ // <swarm-diff> works on both design versions, and a cleared theme is not a theme.
479
+ body: '<h1>PR</h1><swarm-diff>{"files":[]}</swarm-diff><p class="not-swarm-x">x</p>',
480
+ theme: null,
481
+ themeTokens: null,
482
+ });
483
+ expect(plain.structuredContent.usesDesignSystem).toBe(false);
484
+ expect(plain.structuredContent.nudge).toBeUndefined();
485
+
486
+ const v2 = await call({ body: '<main class="swarm-page">x</main>', theme: "paper" });
487
+ expect(v2.structuredContent.usesDesignSystem).toBe(true);
488
+ expect(v2.structuredContent.nudge).toBeUndefined();
489
+
490
+ const svg = await call({
491
+ designVersion: 1,
492
+ body: '<svg xmlns="http://www.w3.org/2000/svg" class="swarm-page"/>',
493
+ contentType: "image/svg+xml",
494
+ theme: "paper",
495
+ });
496
+ expect(svg.structuredContent.usesDesignSystem).toBeUndefined();
497
+ expect(svg.structuredContent.nudge).toBeUndefined();
498
+ });
499
+
500
+ test("an upsert that keeps a design version 1 page gets the version nudge", async () => {
501
+ const slug = uniqueSlug();
502
+ await call({ slug, body: "<h1>old</h1>", designVersion: 1 });
503
+ const rewritten = await call({ slug, body: '<main class="swarm-page">new</main>' });
504
+ expect(rewritten.structuredContent.designVersion).toBe(1);
505
+ expect(rewritten.structuredContent.nudge).toBe(V1_NUDGE);
506
+
507
+ const moved = await call({
508
+ slug,
509
+ body: '<main class="swarm-page">new</main>',
510
+ designVersion: 2,
511
+ });
512
+ expect(moved.structuredContent.designVersion).toBe(2);
513
+ expect(moved.structuredContent.nudge).toBeUndefined();
514
+ });
515
+
516
+ test("stored tokens with light colors and no dark colors get the light-only nudge", async () => {
517
+ const slug = uniqueSlug();
518
+ const lightOnly = await call({
519
+ slug,
520
+ themeTokens: { light: { "--color-primary": "oklch(0.55 0.2 260)", "--radius-lg": "8px" } },
521
+ });
522
+ expect(lightOnly.structuredContent.lightOnlyColors).toBe(true);
523
+ expect(lightOnly.structuredContent.nudge).toBe(LIGHT_ONLY_NUDGE);
524
+
525
+ // The note follows the stored tokens, so an upsert that keeps them repeats it.
526
+ const kept = await call({ slug, body: "<h1>again</h1>" });
527
+ expect(kept.structuredContent.lightOnlyColors).toBe(true);
528
+ expect(kept.structuredContent.nudge).toBe(LIGHT_ONLY_NUDGE);
529
+
530
+ const quiet: Array<Record<string, unknown>> = [
531
+ { themeTokens: { light: { "--font-sans": "Inter", "--radius-lg": "8px" } } },
532
+ {
533
+ themeTokens: {
534
+ light: { "--color-primary": "oklch(0.55 0.2 260)" },
535
+ dark: { "--color-primary": "oklch(0.7 0.15 260)" },
536
+ },
537
+ },
538
+ { themeTokens: { dark: { "--color-primary": "oklch(0.7 0.15 260)" } } },
539
+ {},
540
+ ];
541
+ for (const extra of quiet) {
542
+ const result = await call(extra);
543
+ expect(result.structuredContent.lightOnlyColors).toBe(false);
544
+ expect(result.structuredContent.nudge).toBeUndefined();
545
+ }
546
+ });
547
+
548
+ test("the nudge joins every sentence that applies, in a fixed order", async () => {
549
+ const lightOnly = { light: { "--color-primary": "oklch(0.55 0.2 260)" } };
550
+ const legacy = await call({ designVersion: 1, themeTokens: lightOnly });
551
+ expect(legacy.structuredContent.nudge).toBe(`${V1_NUDGE} ${LIGHT_ONLY_NUDGE}`);
552
+
553
+ const current = await call({ body: styledPage(9), themeTokens: lightOnly });
554
+ expect(current.structuredContent.nudge).toBe(`${LIGHT_ONLY_NUDGE} ${NUDGE_TEXT(9)}`);
555
+ expect(current.content[0]!.text).toContain(`${LIGHT_ONLY_NUDGE} ${NUDGE_TEXT(9)}`);
556
+ });
557
+
558
+ test("the color nudge fires at 8 color literals and not at 7", async () => {
559
+ // The page server shares this threshold: from 8 on, dark mode keeps the page light.
560
+ expect(COLOR_LITERAL_THRESHOLD).toBe(8);
561
+ const seven = await call({ body: styledPage(7) });
562
+ expect(seven.structuredContent.colorLiteralCount).toBe(7);
563
+ expect(seven.structuredContent.nudge).toBeUndefined();
564
+
565
+ const eight = await call({ body: styledPage(8) });
566
+ expect(eight.structuredContent.colorLiteralCount).toBe(8);
567
+ expect(eight.structuredContent.nudge).toBe(NUDGE_TEXT(8));
568
+ expect(eight.content[0]!.text).toContain(NUDGE_TEXT(8));
569
+ });
570
+
571
+ test("the color count covers style attributes and ignores colors elsewhere", async () => {
572
+ const result = await call({
573
+ body: [
574
+ "<style>.a{color:#fff;background:#0f172a}</style>",
575
+ '<p style="color:#2563eb;border:1px solid hsl(0 0% 90%)">a</p>',
576
+ "<p style='fill:oklch(0.5 0.2 30)'>b</p>",
577
+ // Not counted: text, script, and attributes that are not style.
578
+ "<p>Use #fff or rgb(0 0 0) for white.</p>",
579
+ '<script>const c = "#ff0000";</script>',
580
+ '<svg fill="#fff"><circle stroke="rgb(0 0 0)"/></svg>',
581
+ ].join(""),
582
+ });
583
+ expect(result.structuredContent.colorLiteralCount).toBe(5);
584
+ });
585
+
586
+ test("pages on design version 1, and pages that are not HTML, get no color count or nudge", async () => {
587
+ const legacy = await call({ body: styledPage(20), designVersion: 1 });
588
+ expect(legacy.structuredContent.colorLiteralCount).toBeUndefined();
589
+ expect(legacy.structuredContent.nudge).toBeUndefined();
590
+
591
+ const json = await call({
592
+ body: JSON.stringify({ note: styledPage(20) }),
593
+ contentType: "application/json",
594
+ });
595
+ expect(json.structuredContent.colorLiteralCount).toBeUndefined();
596
+ expect(json.structuredContent.nudge).toBeUndefined();
597
+
598
+ const svg = await call({
599
+ body: `<svg xmlns="http://www.w3.org/2000/svg">${styledPage(20)}</svg>`,
600
+ contentType: "image/svg+xml",
601
+ });
602
+ expect(svg.structuredContent.colorLiteralCount).toBeUndefined();
603
+ expect(svg.structuredContent.nudge).toBeUndefined();
604
+ });
605
+
606
+ test("an upsert is judged by the design version the page keeps", async () => {
607
+ const slug = uniqueSlug();
608
+ await call({ slug, body: "<h1>old</h1>", designVersion: 1 });
609
+ const kept = await call({ slug, body: styledPage(20) });
610
+ expect(kept.structuredContent.colorLiteralCount).toBeUndefined();
611
+ expect(kept.structuredContent.nudge).toBeUndefined();
612
+
613
+ const moved = await call({ slug, body: styledPage(20), designVersion: 2 });
614
+ expect(moved.structuredContent.colorLiteralCount).toBe(20);
615
+ expect(moved.structuredContent.nudge).toBe(NUDGE_TEXT(20));
616
+ });
617
+
618
+ test("a failed save carries no nudge", async () => {
619
+ const result = await call({ body: styledPage(20), theme: "neon" });
620
+ expect(result.isError).toBe(true);
621
+ expect(result.structuredContent.nudge).toBeUndefined();
622
+ });
623
+ });
624
+ });
625
+
626
+ describe("countColorLiterals", () => {
627
+ test("counts every color literal form in a style block", () => {
628
+ const css =
629
+ ".a{color:#fff;background:#ffffff;border-color:#ffffff80;outline-color:#fffa}" +
630
+ ".b{color:rgb(0 0 0);fill:rgba(0,0,0,.5);stroke:hsl(0 0% 0%);x:hsla(0,0%,0%,.5);y:oklch(0.5 0.2 30)}";
631
+ expect(countColorLiterals(`<style>${css}</style>`)).toBe(9);
632
+ });
633
+
634
+ test("counts style attributes with either quote and every style block", () => {
635
+ const html =
636
+ "<p style=\"color:#fff\">a</p><p style='background:rgb(0 0 0)'>b</p>" +
637
+ "<style>.a{color:#000}</style><style media=print>.b{color:#111}</style>";
638
+ expect(countColorLiterals(html)).toBe(4);
639
+ });
640
+
641
+ test("is case-insensitive", () => {
642
+ expect(countColorLiterals("<STYLE>.a{color:#FFF;b:RGB(0 0 0)}</STYLE>")).toBe(2);
643
+ });
644
+
645
+ test("counts an unclosed style block to the end of the body", () => {
646
+ expect(countColorLiterals("<style>.a{color:#fff}.b{color:#000}")).toBe(2);
647
+ });
648
+
649
+ test("ignores colors outside style regions", () => {
650
+ const html =
651
+ "<p>#fff rgb(0 0 0)</p><script>var c = '#ff0000'</script>" +
652
+ '<svg fill="#fff"></svg><p data-style="color:#fff"></p>';
653
+ expect(countColorLiterals(html)).toBe(0);
654
+ });
655
+
656
+ test("ignores hex-like text that is not a color", () => {
657
+ const css = ".a{width:#12345}.b{x:#1234567}#header{}.c{content:'#xyz'}.d{color:var(--c)}";
658
+ expect(countColorLiterals(`<style>${css}</style>`)).toBe(0);
659
+ });
660
+
661
+ test("counts white and black as values in style regions, not inside names", () => {
662
+ const css =
663
+ ".a{color:white;background:black;border:1px solid white}" +
664
+ ".b{white-space:nowrap;--black:1;font-family:'Archivo Black'}.white{}#black{}";
665
+ expect(countColorLiterals(`<style>${css}</style>`)).toBe(3);
666
+ expect(countColorLiterals('<p style="background: black">x</p>')).toBe(1);
667
+ expect(countColorLiterals("<p>white on black</p>")).toBe(0);
668
+ });
669
+
670
+ test("counts color classes in class attributes", () => {
671
+ const palette =
672
+ '<p class="bg-blue-500 hover:text-gray-600/80 dark:border-zinc-800 !ring-red-50">a</p>' +
673
+ "<p class='from-sky-400 via-violet-500 to-rose-950 shadow-slate-900 divide-stone-100'>b</p>" +
674
+ '<p class="outline-amber-300 decoration-lime-200 accent-teal-700 caret-cyan-600">c</p>' +
675
+ '<p class="placeholder-neutral-400 fill-emerald-500 stroke-indigo-500 text-fuchsia-500">d</p>' +
676
+ '<p class="bg-orange-100 text-yellow-800 border-green-300 bg-purple-50 text-pink-400">e</p>';
677
+ expect(countColorLiterals(palette)).toBe(22);
678
+ expect(countColorLiterals('<p class="bg-white text-black bg-white/80 text-white">x</p>')).toBe(
679
+ 4,
680
+ );
681
+ expect(
682
+ countColorLiterals(
683
+ '<p class="bg-[#fff] text-[rgb(0_0_0)] border-[hsl(0_0%_90%)] ring-[oklch(0.5_0.2_30)]">x</p>',
684
+ ),
685
+ ).toBe(4);
686
+ });
687
+
688
+ test("ignores token classes, other class names, and color classes outside class attributes", () => {
689
+ const html =
690
+ '<p class="bg-card text-muted-foreground border-border bg-primary swarm-panel">a</p>' +
691
+ '<p class="border-white ring-black my-bg-red-500 bg-red text-red bg-blue-500x">b</p>' +
692
+ '<p data-class="bg-red-500">c</p><p>bg-red-500 text-white</p>' +
693
+ "<style>.bg-white{display:block}</style>";
694
+ expect(countColorLiterals(html)).toBe(0);
695
+ });
696
+
697
+ test("returns 0 for a body without style regions", () => {
698
+ expect(countColorLiterals("")).toBe(0);
699
+ expect(countColorLiterals("<h1>plain</h1>")).toBe(0);
700
+ });
701
+
702
+ test("stays fast on a body of many unterminated tags", () => {
703
+ // 20,000 "<style " tags with no ">" cost about a second with an unbounded
704
+ // attribute match and about 2 ms with the capped one.
705
+ const started = performance.now();
706
+ expect(countColorLiterals("<style ".repeat(20_000))).toBe(0);
707
+ expect(countColorLiterals(' style="'.repeat(20_000))).toBe(0);
708
+ expect(countColorLiterals(' class="'.repeat(20_000))).toBe(0);
709
+ expect(countColorLiterals(`<p class="${"bg-red-".repeat(20_000)}">`)).toBe(0);
710
+ expect(countColorLiterals(`<p class="bg-red-${"5".repeat(100_000)}x">`)).toBe(0);
711
+ expect(countColorLiterals(`<p class="${"-[".repeat(50_000)}">`)).toBe(0);
712
+ expect(countColorLiterals(`<style>${"whites ".repeat(20_000)}</style>`)).toBe(0);
713
+ expect(performance.now() - started).toBeLessThan(500);
714
+ });
214
715
  });
215
716
 
216
717
  describe("create_page MCP tool capability gating", () => {
@@ -306,4 +306,85 @@ describe("db-client SQLITE_BUSY retry", () => {
306
306
  expect(rows.map((r) => r.name)).toEqual(["pre-existing"]);
307
307
  await released;
308
308
  });
309
+ test("a crowd of queued writers does not spin the event loop while a holder persists", async () => {
310
+ // Regression for the 31/10 stalls: a long external holder (a Litestream
311
+ // snapshot) with many writers queued. Each writer used to spin its full
312
+ // attemptSpinMs inside the FIFO lock on every attempt, so a round of
313
+ // attempts blocked the loop for writers x spin and the nominal retry
314
+ // budget stretched to minutes.
315
+ const crowdClient = createBunSqliteClient(() => main, {
316
+ maxWaitMs: 4_000,
317
+ backoffMs: [25, 50, 100, 250, ...Array<number>(20).fill(250)],
318
+ attemptSpinMs: 50,
319
+ finalAttemptSpinMs: 50,
320
+ });
321
+ const writers = 60;
322
+ const released = holdWriteLock(1_500);
323
+
324
+ let maxLagMs = 0;
325
+ let last = performance.now();
326
+ const ticker = setInterval(() => {
327
+ const now = performance.now();
328
+ maxLagMs = Math.max(maxLagMs, now - last - 10);
329
+ last = now;
330
+ }, 10);
331
+
332
+ const startedAt = performance.now();
333
+ const results = await Promise.allSettled(
334
+ Array.from({ length: writers }, (_, i) =>
335
+ crowdClient.run("INSERT INTO items (name) VALUES (?)", [`crowd-${i}`]),
336
+ ),
337
+ );
338
+ const wallMs = performance.now() - startedAt;
339
+ clearInterval(ticker);
340
+ await released;
341
+
342
+ expect(results.every((r) => r.status === "fulfilled")).toBe(true);
343
+ // Ungated: 60 writers x 50ms = 3s of blocked loop per round of attempts.
344
+ expect(maxLagMs).toBeLessThan(500);
345
+ // Ungated the crowd needed several seconds of spin rounds after release.
346
+ expect(wallMs).toBeLessThan(3_000);
347
+ const rows = await crowdClient.query<{ n: number }>(
348
+ "SELECT COUNT(*) AS n FROM items WHERE name LIKE 'crowd-%'",
349
+ );
350
+ expect(rows[0]?.n).toBe(writers);
351
+ });
352
+
353
+ test("a held lock past the budget fails fast for the whole crowd", async () => {
354
+ const crowdClient = createBunSqliteClient(() => main, {
355
+ maxWaitMs: 400,
356
+ backoffMs: [25, 50, 100, 250],
357
+ attemptSpinMs: 50,
358
+ finalAttemptSpinMs: 50,
359
+ });
360
+ const released = holdWriteLock(2_500);
361
+ const startedAt = performance.now();
362
+ const results = await Promise.allSettled(
363
+ Array.from({ length: 40 }, (_, i) =>
364
+ crowdClient.run("INSERT INTO items (name) VALUES (?)", [`doomed-${i}`]),
365
+ ),
366
+ );
367
+ const wallMs = performance.now() - startedAt;
368
+ await released;
369
+ expect(results.every((r) => r.status === "rejected")).toBe(true);
370
+ for (const r of results) {
371
+ expect((r as PromiseRejectedResult).reason.code).toBe("SQLITE_BUSY");
372
+ }
373
+ // Ungated: 40 writers x 5 spins x 50ms = 10s. Gated: well under 2s.
374
+ expect(wallMs).toBeLessThan(2_000);
375
+ });
376
+
377
+ test("the gate closes once a blocked write lands, so the next BUSY probes afresh", async () => {
378
+ const released = holdWriteLock(150);
379
+ await client.run("INSERT INTO items (name) VALUES (?)", ["after-hold"]);
380
+ await released;
381
+ // New episode: a second, short hold is still bridged.
382
+ const second = holdWriteLock(100);
383
+ await client.run("INSERT INTO items (name) VALUES (?)", ["after-second-hold"]);
384
+ await second;
385
+ const rows = await client.query<{ name: string }>(
386
+ "SELECT name FROM items WHERE name LIKE 'after-%' ORDER BY name",
387
+ );
388
+ expect(rows.map((r) => r.name)).toEqual(["after-hold", "after-second-hold"]);
389
+ });
309
390
  });
@@ -566,6 +566,7 @@ value:setBudgetRefusalFollowUpTaskId
566
566
  value:setFavorite
567
567
  value:setScriptRunFollowUpGuard
568
568
  value:setSlackMessageTracking
569
+ value:setTaskTitleIfUnset
569
570
  value:setUserFavorite
570
571
  value:settleSupersededTaskDependents
571
572
  value:shouldBlockPolling
@@ -589,6 +589,7 @@ value:setBudgetRefusalFollowUpTaskId
589
589
  value:setFavorite
590
590
  value:setScriptRunFollowUpGuard
591
591
  value:setSlackMessageTracking
592
+ value:setTaskTitleIfUnset
592
593
  value:setUserFavorite
593
594
  value:settleSupersededTaskDependents
594
595
  value:shouldBlockPolling
@@ -659,6 +660,7 @@ value:pauseTask
659
660
  value:recordTaskProviderIfUnset
660
661
  value:resetOrphanedInProgressTasksForAgent
661
662
  value:resumeTask
663
+ value:setTaskTitleIfUnset
662
664
  value:settleSupersededTaskDependents
663
665
  value:startTask
664
666
  value:supersedeTask
@@ -36,6 +36,27 @@ beforeEach(async () => {
36
36
  });
37
37
  afterEach(() => db.closeDb());
38
38
 
39
+ test("unfiltered small pages use the lastUpdatedAt index without planner statistics", async () => {
40
+ const client = db.getDbClient();
41
+ for (const includeHeartbeat of [false, true]) {
42
+ for (const limit of [1, 5, 50]) {
43
+ const query = spyOn(client, "query");
44
+ let sql = "";
45
+ let params: unknown[] = [];
46
+ try {
47
+ await db.getAllTasks({ limit, includeHeartbeat });
48
+ [sql, params = []] = query.mock.calls.find(([s]) => s.includes("WITH page AS"))!;
49
+ } finally {
50
+ query.mockRestore();
51
+ }
52
+ const plan = await client.query<{ detail: string }>(`EXPLAIN QUERY PLAN ${sql}`, params);
53
+ expect(plan.map((row) => row.detail).join("\n")).toContain(
54
+ "SCAN agent_tasks USING INDEX idx_agent_tasks_last_updated",
55
+ );
56
+ }
57
+ }
58
+ });
59
+
39
60
  // Characterized against the pre-extraction facade at 8a8b7b53.
40
61
  const cases: {
41
62
  name: string;