@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
@@ -177,6 +177,11 @@ export type BusyRetryOptions = {
177
177
  * Defaults to `DEFAULT_FINAL_ATTEMPT_SPIN_MS`.
178
178
  */
179
179
  finalAttemptSpinMs?: number;
180
+ /**
181
+ * Longest gap between spinning probes while an external holder is
182
+ * detected (see `BusyGate`). Defaults to `DEFAULT_MAX_GATE_MS`.
183
+ */
184
+ maxGateMs?: number;
180
185
  };
181
186
 
182
187
  /**
@@ -247,8 +252,81 @@ function isSqliteBusy(err: unknown): boolean {
247
252
  return typeof code === "string" && code.startsWith("SQLITE_BUSY");
248
253
  }
249
254
 
255
+ /** Ceiling on the gap between spinning probes while a holder is detected. */
256
+ const DEFAULT_MAX_GATE_MS = 1_000;
257
+ /** A quiet stretch this long ends a BUSY episode even without a proven write. */
258
+ const GATE_DECAY_MS = 5_000;
259
+
260
+ /**
261
+ * Process-wide memory of a cross-process write-lock holder.
262
+ *
263
+ * Every retrying writer used to spin `attemptSpinMs` (100ms, or 250ms for the
264
+ * last attempt) SYNCHRONOUSLY inside the FIFO lock on every attempt. With N
265
+ * writers queued behind a holder that lasts minutes, one round of attempts
266
+ * costs N x 100ms of blocked event loop, and each attempt waits for that
267
+ * whole round before its own turn. A 5-minute Litestream snapshot turned
268
+ * the nominal 11s retry budget into 90-105s request hangs, 3s /health and
269
+ * 3s page loads, although only writes were contending.
270
+ *
271
+ * The gate records a BUSY seen by a spinning probe. For the next interval
272
+ * (doubling from `attemptSpinMs` up to `maxGateMs`) every attempt runs with
273
+ * a ZERO busy_timeout: it still tries, so recovery is immediate, but it
274
+ * fails in microseconds instead of blocking the loop, and the async backoff
275
+ * sleeps do the waiting. Once the interval lapses, the next attempt is the
276
+ * lone spinning probe again.
277
+ */
278
+ class BusyGate {
279
+ private openUntil = 0;
280
+ private streak = 0;
281
+ private lastBusyAt = 0;
282
+ private episodeStartedAt = 0;
283
+ private episodeProbes = 0;
284
+
285
+ constructor(
286
+ private readonly baseMs: number,
287
+ private readonly maxMs: number,
288
+ ) {}
289
+
290
+ /** True while attempts must not spin. */
291
+ isOpen(now = performance.now()): boolean {
292
+ return now < this.openUntil;
293
+ }
294
+
295
+ /** A spinning probe hit BUSY: a holder exists. Open the gate and widen it. */
296
+ recordBusy(now = performance.now()): void {
297
+ if (this.streak > 0 && now - this.lastBusyAt > GATE_DECAY_MS) this.endEpisode(now, "decayed");
298
+ if (this.streak === 0) {
299
+ this.episodeStartedAt = now;
300
+ this.episodeProbes = 0;
301
+ console.warn(
302
+ "[db-client] SQLITE_BUSY: another process holds the write lock; spinning at most one probe per gate interval until it releases",
303
+ );
304
+ }
305
+ this.streak += 1;
306
+ this.episodeProbes += 1;
307
+ this.lastBusyAt = now;
308
+ const interval = Math.min(this.maxMs, this.baseMs * 2 ** Math.min(this.streak - 1, 20));
309
+ this.openUntil = now + interval;
310
+ }
311
+
312
+ /** A write got through after BUSY: the holder is gone. */
313
+ recordRecovered(now = performance.now()): void {
314
+ if (this.streak === 0) return;
315
+ this.endEpisode(now, "released");
316
+ }
317
+
318
+ private endEpisode(now: number, how: "released" | "decayed"): void {
319
+ console.warn(
320
+ `[db-client] write lock ${how} after ${Math.round(now - this.episodeStartedAt)}ms (${this.episodeProbes} spinning probes)`,
321
+ );
322
+ this.streak = 0;
323
+ this.openUntil = 0;
324
+ }
325
+ }
326
+
250
327
  class BunSqliteClient implements DbClient {
251
328
  private readonly lock = new FifoLock();
329
+ private readonly gate: BusyGate;
252
330
  private savepointSeq = 0;
253
331
  /** Lazily captured ambient busy_timeout, restored after each short-spin attempt. */
254
332
  private restoreBusyTimeoutMs: number | null = null;
@@ -256,7 +334,12 @@ class BunSqliteClient implements DbClient {
256
334
  constructor(
257
335
  private readonly getDatabase: () => Database,
258
336
  private readonly busyRetry: BusyRetryOptions = DEFAULT_BUSY_RETRY,
259
- ) {}
337
+ ) {
338
+ this.gate = new BusyGate(
339
+ busyRetry.attemptSpinMs,
340
+ Math.max(busyRetry.attemptSpinMs, busyRetry.maxGateMs ?? DEFAULT_MAX_GATE_MS),
341
+ );
342
+ }
260
343
 
261
344
  query<T>(sql: string, params: DbParam[] = []): Promise<T[]> {
262
345
  return this.execute(sql, (db) => db.query<T, DbParam[]>(sql).all(...params));
@@ -352,6 +435,7 @@ class BunSqliteClient implements DbClient {
352
435
  // not. The short spin keeps the driver's synchronous busy wait off
353
436
  // the event loop; the real waiting is the caller's async backoff.
354
437
  this.shortSpin(db, () => db.run("BEGIN IMMEDIATE"), finalAttempt);
438
+ this.gate.recordRecovered();
355
439
  } catch (err) {
356
440
  if (isSqliteBusy(err)) return { ok: false, busyBegin: err };
357
441
  throw err;
@@ -366,7 +450,7 @@ class BunSqliteClient implements DbClient {
366
450
  // is exact. The open transaction pins the connection, so these
367
451
  // retries hold the FIFO lock and their sleeps block queued work;
368
452
  // WAL-mode COMMIT BUSY is rare enough for that to be acceptable.
369
- await this.retryHoldingLock(() => this.shortSpin(db, () => db.run("COMMIT")));
453
+ await this.retryHoldingLock(() => this.shortSpin(db, () => db.run("COMMIT"), false, false));
370
454
  }
371
455
  for (const hook of ctx.afterCommit) this.scheduleHook(hook);
372
456
  return { ok: true, value: result };
@@ -478,6 +562,7 @@ class BunSqliteClient implements DbClient {
478
562
  return timed(() => op(db));
479
563
  }
480
564
  let sleptMs = 0;
565
+ let sawBusy = false;
481
566
  for (let attemptIndex = 0; ; attemptIndex++) {
482
567
  const next = this.busyRetry.backoffMs[attemptIndex];
483
568
  const canRetryAfter = next !== undefined && sleptMs + next <= this.busyRetry.maxWaitMs;
@@ -488,9 +573,14 @@ class BunSqliteClient implements DbClient {
488
573
  // exact. The final attempt spins `finalAttemptSpinMs` (see
489
574
  // transaction()).
490
575
  const db = this.getDatabase();
491
- return timed(() => this.shortSpin(db, () => op(db), !canRetryAfter), canRetryAfter);
576
+ const result = timed(() => this.shortSpin(db, () => op(db), !canRetryAfter), canRetryAfter);
577
+ // A statement that was BUSY earlier in this call and now landed is a
578
+ // write that got through: the external holder is gone.
579
+ if (sawBusy) this.gate.recordRecovered();
580
+ return result;
492
581
  } catch (err) {
493
582
  if (!isSqliteBusy(err) || !canRetryAfter || next === undefined) throw err;
583
+ sawBusy = true;
494
584
  backoff = next;
495
585
  sleptMs += next;
496
586
  } finally {
@@ -509,17 +599,27 @@ class BunSqliteClient implements DbClient {
509
599
  * lock is held, so the set/attempt/restore sequence cannot interleave with
510
600
  * other client work.
511
601
  */
512
- private shortSpin<T>(db: Database, attempt: () => T, finalAttempt = false): T {
602
+ private shortSpin<T>(db: Database, attempt: () => T, finalAttempt = false, gateable = true): T {
513
603
  if (this.restoreBusyTimeoutMs === null) {
514
604
  const row = db.query<{ timeout: number }, []>("PRAGMA busy_timeout").get();
515
605
  this.restoreBusyTimeoutMs = row?.timeout ?? 5000;
516
606
  }
517
- const spinMs = finalAttempt
518
- ? (this.busyRetry.finalAttemptSpinMs ?? DEFAULT_FINAL_ATTEMPT_SPIN_MS)
519
- : this.busyRetry.attemptSpinMs;
607
+ // While a holder is known (BusyGate) the attempt runs without a spin: it
608
+ // still tries, so a released lock is taken at once, but a held one costs
609
+ // microseconds of event loop instead of a full spin per queued writer.
610
+ // Only the spinning probe that follows a lapsed gate reports BUSY back.
611
+ const gated = gateable && this.gate.isOpen();
612
+ const spinMs = gated
613
+ ? 0
614
+ : finalAttempt
615
+ ? (this.busyRetry.finalAttemptSpinMs ?? DEFAULT_FINAL_ATTEMPT_SPIN_MS)
616
+ : this.busyRetry.attemptSpinMs;
520
617
  db.exec(`PRAGMA busy_timeout = ${spinMs}`);
521
618
  try {
522
619
  return attempt();
620
+ } catch (err) {
621
+ if (gateable && !gated && isSqliteBusy(err)) this.gate.recordBusy();
622
+ throw err;
523
623
  } finally {
524
624
  db.exec(`PRAGMA busy_timeout = ${this.restoreBusyTimeoutMs}`);
525
625
  }
package/src/be/db.ts CHANGED
@@ -198,6 +198,7 @@ import {
198
198
  } from "./db/context-versions";
199
199
  import { clearAgentModelProfileOnOverrideChange } from "./db/model-profiles";
200
200
  import { pageSearchSql } from "./db/page-search";
201
+ import { parsePageThemeTokens } from "./db/page-theme-tokens";
201
202
  import { getDb, getDbClient } from "./db/runtime";
202
203
  import {
203
204
  assertAssetKeyMovesAllowed,
@@ -413,6 +414,7 @@ export {
413
414
  recordTaskProviderIfUnset,
414
415
  resetOrphanedInProgressTasksForAgent,
415
416
  resumeTask,
417
+ setTaskTitleIfUnset,
416
418
  settleSupersededTaskDependents,
417
419
  startTask,
418
420
  supersedeTask,
@@ -8619,6 +8621,9 @@ type PageRow = {
8619
8621
  createdAt: string;
8620
8622
  updatedAt: string;
8621
8623
  view_count: number;
8624
+ theme: string | null;
8625
+ theme_tokens: string | null;
8626
+ design_version: number;
8622
8627
  };
8623
8628
 
8624
8629
  function rowToPage(row: PageRow): Page {
@@ -8636,6 +8641,9 @@ function rowToPage(row: PageRow): Page {
8636
8641
  needsCredentials: row.needsCredentials
8637
8642
  ? (JSON.parse(row.needsCredentials) as string[])
8638
8643
  : undefined,
8644
+ theme: row.theme ?? null,
8645
+ themeTokens: parsePageThemeTokens(row.theme_tokens),
8646
+ designVersion: typeof row.design_version === "number" ? row.design_version : 1,
8639
8647
  viewCount: typeof row.view_count === "number" ? row.view_count : 0,
8640
8648
  createdAt: normalizeDateRequired(row.createdAt),
8641
8649
  updatedAt: normalizeDateRequired(row.updatedAt),
@@ -8653,13 +8661,17 @@ export async function createPage(data: {
8653
8661
  passwordHash?: string;
8654
8662
  body: string;
8655
8663
  needsCredentials?: string[];
8664
+ theme?: string | null;
8665
+ themeTokens?: Page["themeTokens"];
8666
+ /** Defaults to 2 (the swarm design system). Rows inserted outside this function default to 1. */
8667
+ designVersion?: number;
8656
8668
  }): Promise<Page> {
8657
8669
  // Match the historical SQL default ID shape while making the value
8658
8670
  // available before insert so the default namespace can include it.
8659
8671
  const id = crypto.randomUUID().replace(/-/g, "");
8660
8672
  const row = await getDbClient().get<PageRow>(
8661
- `INSERT INTO pages (id, "key", agentId, slug, title, description, contentType, authMode, passwordHash, body, needsCredentials)
8662
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
8673
+ `INSERT INTO pages (id, "key", agentId, slug, title, description, contentType, authMode, passwordHash, body, needsCredentials, theme, theme_tokens, design_version)
8674
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
8663
8675
  [
8664
8676
  id,
8665
8677
  normalizeAssetKey(data.key ?? defaultAssetKey("page", id)),
@@ -8672,6 +8684,9 @@ export async function createPage(data: {
8672
8684
  data.passwordHash ?? null,
8673
8685
  data.body,
8674
8686
  data.needsCredentials ? JSON.stringify(data.needsCredentials) : null,
8687
+ data.theme ?? null,
8688
+ data.themeTokens ? JSON.stringify(data.themeTokens) : null,
8689
+ data.designVersion ?? 2,
8675
8690
  ],
8676
8691
  );
8677
8692
  if (!row) throw new Error("Failed to create page");
@@ -8858,6 +8873,11 @@ export async function updatePage(
8858
8873
  body?: string;
8859
8874
  needsCredentials?: string[] | null;
8860
8875
  slug?: string;
8876
+ /** undefined keeps the stored value, null clears it. */
8877
+ theme?: string | null;
8878
+ /** undefined keeps the stored value, null clears it. */
8879
+ themeTokens?: Page["themeTokens"];
8880
+ designVersion?: number;
8861
8881
  },
8862
8882
  ): Promise<Page | null> {
8863
8883
  const updates: string[] = [];
@@ -8898,6 +8918,18 @@ export async function updatePage(
8898
8918
  updates.push("slug = ?");
8899
8919
  params.push(data.slug);
8900
8920
  }
8921
+ if (data.theme !== undefined) {
8922
+ updates.push("theme = ?");
8923
+ params.push(data.theme);
8924
+ }
8925
+ if (data.themeTokens !== undefined) {
8926
+ updates.push("theme_tokens = ?");
8927
+ params.push(data.themeTokens ? JSON.stringify(data.themeTokens) : null);
8928
+ }
8929
+ if (data.designVersion !== undefined) {
8930
+ updates.push("design_version = ?");
8931
+ params.push(data.designVersion);
8932
+ }
8901
8933
  if (updates.length === 0) return getPage(id);
8902
8934
  updates.push("updatedAt = ?");
8903
8935
  params.push(new Date().toISOString());
@@ -0,0 +1,23 @@
1
+ -- Pages design system v2: three columns that say how an HTML page is styled.
2
+ --
3
+ -- design_version Which design system serves the page. 1 is the original
4
+ -- look (Tailwind v3 Play CDN, `--swarm-*` hex tokens) and stays
5
+ -- frozen. 2 is the swarm design system (theme tokens, `swarm-*`
6
+ -- primitives, Tailwind v4). The DEFAULT makes every existing row
7
+ -- read 1, so old pages keep their look. New pages get 2 from
8
+ -- createPage(), which inserts the value explicitly.
9
+ -- theme Preset id from the dashboard theme registry (for example
10
+ -- `paper`). NULL pins nothing: the page follows the viewer's
11
+ -- theme, then falls back to `hive`. Design version 2 only.
12
+ -- theme_tokens JSON `{"light": {...}, "dark": {...}}` of per-page overrides,
13
+ -- keyed by page token names (`--color-primary`, `--radius-lg`).
14
+ -- Stored normalized by importThemeTokens() (src/themes/import.ts).
15
+ -- NULL means no overrides. Design version 2 only.
16
+ --
17
+ -- Both theme columns are NULL for every existing row. Neither one has a CHECK:
18
+ -- the preset ids live in code (src/themes/presets.ts), so the write paths
19
+ -- validate them, and a stale id read from the table is ignored at render time.
20
+
21
+ ALTER TABLE pages ADD COLUMN theme TEXT;
22
+ ALTER TABLE pages ADD COLUMN theme_tokens TEXT;
23
+ ALTER TABLE pages ADD COLUMN design_version INTEGER NOT NULL DEFAULT 1;
@@ -29,12 +29,6 @@ import dailySwarmUpdateCheckConfig from "../../../templates/schedules/daily-swar
29
29
  import dailySwarmUpdateCheckContent from "../../../templates/schedules/daily-swarm-update-check/content.md" with {
30
30
  type: "text",
31
31
  };
32
- import dailyWorkflowHealthAuditConfig from "../../../templates/schedules/daily-workflow-health-audit/config.json" with {
33
- type: "text",
34
- };
35
- import dailyWorkflowHealthAuditContent from "../../../templates/schedules/daily-workflow-health-audit/content.md" with {
36
- type: "text",
37
- };
38
32
  import dreamingDailyConfig from "../../../templates/schedules/dreaming-daily/config.json" with {
39
33
  type: "text",
40
34
  };
@@ -153,10 +147,6 @@ const BUILT_IN_SCHEDULE_SOURCES: readonly ScheduleTemplateSource[] = [
153
147
  config: asText(dailySwarmUpdateCheckConfig),
154
148
  content: asText(dailySwarmUpdateCheckContent),
155
149
  },
156
- {
157
- config: asText(dailyWorkflowHealthAuditConfig),
158
- content: asText(dailyWorkflowHealthAuditContent),
159
- },
160
150
  { config: asText(dreamingDailyConfig), content: asText(dreamingDailyContent) },
161
151
  { config: asText(gtmWeeklyReviewConfig), content: asText(gtmWeeklyReviewContent) },
162
152
  {
@@ -745,6 +745,8 @@ export async function publishCatalogReportPage(report: CatalogReport, ctx: any):
745
745
  description: report.description,
746
746
  contentType: "text/html",
747
747
  authMode: "authed",
748
+ // The report ships its own palette, written for the v1 page head.
749
+ designVersion: 1,
748
750
  body: renderCatalogReportPage(report),
749
751
  });
750
752
  const payload = response?.data ?? response;
@@ -745,6 +745,8 @@ export async function publishCatalogReportPage(report: CatalogReport, ctx: any):
745
745
  description: report.description,
746
746
  contentType: "text/html",
747
747
  authMode: "authed",
748
+ // The report ships its own palette, written for the v1 page head.
749
+ designVersion: 1,
748
750
  body: renderCatalogReportPage(report),
749
751
  });
750
752
  const payload = response?.data ?? response;
@@ -522,6 +522,8 @@ export default async function memoryEval(args: any, ctx: any) {
522
522
  description: `3-axis memory quality dashboard: carry-forward ${report.axis1.score}%, preferences usefulness ${report.axis2.avgUsefulness}, freshness ${report.axis3.freshScore}%. ${history.length} data points.`,
523
523
  contentType: "text/html",
524
524
  authMode: "authed",
525
+ // The dashboard ships its own palette, written for the v1 page head.
526
+ designVersion: 1,
525
527
  body: html,
526
528
  });
527
529
  const payload = response?.data ?? response;
@@ -0,0 +1,150 @@
1
+ import { z } from "zod";
2
+
3
+ export const argsSchema = z.object({
4
+ dryRun: z
5
+ .boolean()
6
+ .optional()
7
+ .describe("Compute the verdict but do not notify the lead when an update is available"),
8
+ });
9
+
10
+ const RELEASES_URL = "https://api.github.com/repos/desplega-ai/agent-swarm/releases/latest";
11
+
12
+ /** Parse `major.minor.patch` (a leading `v` and a pre-release/build suffix are ignored). */
13
+ export function parseVersion(value: unknown): [number, number, number] | null {
14
+ if (typeof value !== "string") return null;
15
+ const m = value
16
+ .trim()
17
+ .replace(/^v/i, "")
18
+ .match(/^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/);
19
+ if (!m) return null;
20
+ return [Number(m[1]), Number(m[2]), Number(m[3])];
21
+ }
22
+
23
+ /** Numeric compare of two parsed versions: negative, zero or positive. Never a string compare. */
24
+ export function compareVersions(a: [number, number, number], b: [number, number, number]): number {
25
+ for (let i = 0; i < 3; i++) {
26
+ const diff = (a[i] as number) - (b[i] as number);
27
+ if (diff !== 0) return diff;
28
+ }
29
+ return 0;
30
+ }
31
+
32
+ function errorText(error: unknown): string {
33
+ return String((error as any)?.message ?? error).slice(0, 200);
34
+ }
35
+
36
+ /** The version the API server is running, from its own unauthenticated `/health` route. */
37
+ async function runningVersion(ctx: any): Promise<string | null> {
38
+ try {
39
+ const base = ctx.stdlib.Redacted.value(ctx.swarm.config.mcpBaseUrl).replace(/\/+$/, "");
40
+ const res: any = await ctx.stdlib.fetch(base + "/health", { retries: 1, timeoutMs: 10_000 });
41
+ if (!res.ok) return null;
42
+ const body: any = await res.json();
43
+ return typeof body?.version === "string" && body.version.trim() ? body.version.trim() : null;
44
+ } catch {
45
+ return null;
46
+ }
47
+ }
48
+
49
+ async function resolveLeadId(ctx: any): Promise<string | null> {
50
+ const res: any = await ctx.swarm.swarm_get({});
51
+ const data = res?.data ?? res;
52
+ const agents: any[] = Array.isArray(data?.agents) ? data.agents : Array.isArray(data) ? data : [];
53
+ const lead = agents.find((agent) => agent?.isLead === true || agent?.isLead === 1);
54
+ return typeof lead?.id === "string" ? lead.id : null;
55
+ }
56
+
57
+ /**
58
+ * Compare the running agent-swarm version with the latest published GitHub
59
+ * release and, only when a newer release exists, hand the lead one task to
60
+ * tell the operator through its admin delivery channel.
61
+ */
62
+ export default async function swarmUpdateCheck(args: any, ctx: any) {
63
+ const parsed = argsSchema.safeParse(args ?? {});
64
+ if (!parsed.success) return { error: "invalid args: " + parsed.error.message };
65
+ const dryRun = parsed.data.dryRun === true;
66
+
67
+ const running = await runningVersion(ctx);
68
+ const runningParsed = parseVersion(running);
69
+ if (!running || !runningParsed) {
70
+ // Unknown is not "up to date": say so and stop, never guess a version.
71
+ return {
72
+ checked: false,
73
+ status: "unknown",
74
+ running,
75
+ message: `Unable to determine running version: ${running ? `unparseable "${running}"` : "not reported by /health"}.`,
76
+ };
77
+ }
78
+
79
+ let release: any;
80
+ try {
81
+ // One attempt: a failed GitHub call is reported, never retried in a loop.
82
+ const res: any = await ctx.stdlib.fetch(RELEASES_URL, {
83
+ retries: 1,
84
+ timeoutMs: 15_000,
85
+ headers: { Accept: "application/vnd.github+json", "User-Agent": "agent-swarm-update-check" },
86
+ });
87
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
88
+ release = await res.json();
89
+ } catch (error) {
90
+ return {
91
+ checked: false,
92
+ status: "github-unreachable",
93
+ running,
94
+ message: `Could not reach GitHub releases API: ${errorText(error)}`,
95
+ };
96
+ }
97
+
98
+ const latest = typeof release?.tag_name === "string" ? release.tag_name.replace(/^v/i, "") : null;
99
+ const latestParsed = parseVersion(latest);
100
+ if (!latest || !latestParsed) {
101
+ return {
102
+ checked: false,
103
+ status: "github-unreachable",
104
+ running,
105
+ message: `Could not read a version from the latest release tag: ${JSON.stringify(release?.tag_name ?? null)}.`,
106
+ };
107
+ }
108
+ const releaseUrl =
109
+ typeof release?.html_url === "string"
110
+ ? release.html_url
111
+ : `https://github.com/desplega-ai/agent-swarm/releases/tag/v${latest}`;
112
+
113
+ if (compareVersions(latestParsed, runningParsed) <= 0) {
114
+ return {
115
+ checked: true,
116
+ status: "up-to-date",
117
+ running,
118
+ latest,
119
+ message: `Up to date: running ${running}, latest published ${latest}.`,
120
+ };
121
+ }
122
+
123
+ const message = `Update available: running ${running}, latest ${latest} (${releaseUrl})`;
124
+ if (dryRun) {
125
+ return { checked: true, status: "update-available", running, latest, releaseUrl, notified: false, message };
126
+ }
127
+
128
+ const leadId = await resolveLeadId(ctx);
129
+ // A detected update that reaches nobody is a failed run, not a quiet success.
130
+ if (!leadId) throw new Error(`${message}, but the swarm has no lead to notify.`);
131
+ const sent: any = await ctx.swarm.task_send({
132
+ agentId: leadId,
133
+ routingReason: "skill",
134
+ routingNote: "Lead owns the operator's admin delivery channel for release notices.",
135
+ tags: ["swarm-update-check"],
136
+ task: [
137
+ `A newer agent-swarm release is published: running ${running}, latest ${latest}.`,
138
+ `Release: ${releaseUrl}`,
139
+ "",
140
+ "Post one short message to your configured admin delivery channel (the in-app fallback if none is configured) naming the running version, the new version and the release URL. Then complete with that message as the output.",
141
+ "Do not attempt the upgrade yourself: this notice only detects and reports.",
142
+ ].join("\n"),
143
+ });
144
+ const taskId = sent?.data?.task?.id ?? null;
145
+ if (sent?.success === false || sent?.data?.success === false || !taskId) {
146
+ const reason = sent?.data?.message ?? sent?.data?.error ?? `status ${sent?.status}`;
147
+ throw new Error(`${message}, but notifying the lead failed: ${errorText(reason)}`);
148
+ }
149
+ return { checked: true, status: "update-available", running, latest, releaseUrl, notified: true, leadTaskId: taskId, message };
150
+ }
@@ -60,6 +60,7 @@ import starHistoryRefreshSrc from "./catalog/star-history-refresh.ts" with { typ
60
60
  // @ts-expect-error Bun text imports synthesize a default string for this helper.
61
61
  import starHistoryRendererSrc from "./catalog/star-history-renderer.ts" with { type: "text" };
62
62
  import swarmOverviewSrc from "./catalog/swarm-overview.ts" with { type: "text" };
63
+ import swarmUpdateCheckSrc from "./catalog/swarm-update-check.ts" with { type: "text" };
63
64
  import taskContextGatheringSrc from "./catalog/task-context-gathering.ts" with { type: "text" };
64
65
  import taskFailureAuditSrc from "./catalog/task-failure-audit.ts" with { type: "text" };
65
66
  import textDiffSrc from "./catalog/text-diff.ts" with { type: "text" };
@@ -217,6 +218,14 @@ export const SEED_SCRIPTS: SeedScript[] = [
217
218
  "Task onboarding: one call instead of task_get plus multiple memory_search calls. Pass the task description split into 2-4 natural-language queries.",
218
219
  source: asText(taskContextGatheringSrc),
219
220
  },
221
+ {
222
+ name: "swarm-update-check",
223
+ description:
224
+ "Compare the running agent-swarm version (API /health) with the latest GitHub release of desplega-ai/agent-swarm as numeric major.minor.patch, and hand the lead one notify task only when a newer release exists.",
225
+ intent:
226
+ "Daily zero-config upgrade watchdog behind the daily-swarm-update-check schedule; dryRun reports the verdict without notifying.",
227
+ source: asText(swarmUpdateCheckSrc),
228
+ },
220
229
  {
221
230
  name: "schedule-health",
222
231
  description: