@desplega.ai/agent-swarm 2.7.5 → 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 (319) hide show
  1. package/dist/{acp-adapter-g3ads55s.js → acp-adapter-7b4vk3h5.js} +5 -4
  2. package/dist/{actions-zwfykqqk.js → actions-3tt153af.js} +18 -17
  3. package/dist/{amp-adapter-z3s0ssdt.js → amp-adapter-njpts0yr.js} +1 -1
  4. package/dist/{anthropic-messages-x4q6rfq5.js → anthropic-messages-xey2skwx.js} +7 -7
  5. package/dist/{app-vmp4fgep.js → app-nky4378f.js} +3 -3
  6. package/dist/{assistant-vwde653f.js → assistant-jnb669pa.js} +24 -22
  7. package/dist/{azure-openai-responses-s1w5n70z.js → azure-openai-responses-26t7j9ra.js} +5 -5
  8. package/dist/{boot-reembed-375t7tbk.js → boot-reembed-9mgvaztt.js} +1 -1
  9. package/dist/{boot-reembed-gq9rcpnj.js → boot-reembed-qs2djdej.js} +1 -1
  10. package/dist/{boot-scrub-sweep-nd938kyf.js → boot-scrub-sweep-yjpdj7s1.js} +3 -3
  11. package/dist/{boot-seeds-aykx51v3.js → boot-seeds-5cm6ambd.js} +4 -4
  12. package/dist/{boxd-k1j26g3h.js → boxd-nv507va8.js} +5 -5
  13. package/dist/{claude-adapter-8j4hm099.js → claude-adapter-fn287zmm.js} +5 -4
  14. package/dist/{claude-managed-setup-0wyynpn1.js → claude-managed-setup-wtdz2mj2.js} +1 -1
  15. package/dist/{claude-sdk-session-q6wj5hmy.js → claude-sdk-session-x91kqyhm.js} +9677 -9514
  16. package/dist/{cli-j3jvnpe8.js → cli-08bmz9z9.js} +1 -1
  17. package/dist/{cli-gjr2y2xk.js → cli-0agcv1v5.js} +4 -1
  18. package/dist/{cli-y24sc29k.js → cli-1c6tzt8g.js} +30 -8
  19. package/dist/{cli-mjzynrse.js → cli-35rsp8ye.js} +1 -1
  20. package/dist/{cli-rpm2sgv0.js → cli-3fg0bdcv.js} +19 -2
  21. package/dist/{cli-nn4d8dne.js → cli-5b456bpf.js} +2 -2
  22. package/dist/{cli-bhjt02p8.js → cli-5jnhv72d.js} +1 -1
  23. package/dist/{cli-fqzx7gwa.js → cli-5zx699j9.js} +2 -2
  24. package/dist/{cli-hf6zt5m6.js → cli-69szn7w7.js} +17 -1
  25. package/dist/{cli-vqypxzh4.js → cli-6g0hb4cp.js} +8 -8
  26. package/dist/{cli-tqgg40ek.js → cli-6pfd54vx.js} +9 -8
  27. package/dist/{cli-bsn8nhrq.js → cli-6vcp76qk.js} +3 -3
  28. package/dist/{cli-zppwg9p9.js → cli-72v799xn.js} +1 -1
  29. package/dist/cli-82j36f2m.js +45 -0
  30. package/dist/{cli-hn9hxmz9.js → cli-8305gbq0.js} +29 -16
  31. package/dist/{cli-z8eh3ex8.js → cli-8sckt74b.js} +8 -8
  32. package/dist/{cli-bnz7a7qf.js → cli-95jzxcp3.js} +1 -1
  33. package/dist/{cli-0bxqnvq9.js → cli-adeegh7d.js} +29 -7
  34. package/dist/{cli-g61qxmge.js → cli-afkqkrtw.js} +3 -3
  35. package/dist/{cli-7a42nfy0.js → cli-ayayvfg4.js} +24 -4
  36. package/dist/{cli-bqt1mptt.js → cli-az48n0nr.js} +4 -4
  37. package/dist/{cli-z6jkx23q.js → cli-bfv7vdg6.js} +9 -9
  38. package/dist/{cli-pywf3eh5.js → cli-caj33vcq.js} +5 -5
  39. package/dist/{cli-5pag6rmn.js → cli-cb2a8pna.js} +1 -1
  40. package/dist/{cli-wvaawacd.js → cli-cppkf404.js} +5236 -72
  41. package/dist/{cli-mry0qzym.js → cli-ddaanqkd.js} +123 -10
  42. package/dist/{cli-0gey5fzy.js → cli-e0kmphd6.js} +2 -2
  43. package/dist/{cli-zh3fp8v2.js → cli-e46hg0xw.js} +1 -1
  44. package/dist/{cli-jm2vfpfd.js → cli-e5bzrn01.js} +1 -1
  45. package/dist/{cli-qxnf8z2z.js → cli-excnnjg2.js} +165 -153
  46. package/dist/{cli-71ptav7x.js → cli-ferkkrg9.js} +1 -1
  47. package/dist/{cli-vpf89f34.js → cli-fmjd0t1y.js} +3 -3
  48. package/dist/{cli-rm6hqj3z.js → cli-fyt2g4wd.js} +33 -139
  49. package/dist/cli-g08zdaec.js +55 -0
  50. package/dist/{cli-ehq736cn.js → cli-g7s47cc5.js} +4 -4
  51. package/dist/{cli-82ae98pz.js → cli-gz6q4eve.js} +3 -3
  52. package/dist/{cli-82jg7fp9.js → cli-he5nxaa8.js} +1 -1
  53. package/dist/{cli-rqkxrmgw.js → cli-je518txm.js} +39 -14
  54. package/dist/{cli-8hs41zy2.js → cli-jtjtabxe.js} +1 -1
  55. package/dist/{cli-611a1294.js → cli-k0wwfqbg.js} +18 -1
  56. package/dist/{cli-nzxr556s.js → cli-k1nje3v3.js} +38 -43
  57. package/dist/{cli-3h4nhjn8.js → cli-k8196sh2.js} +9 -7
  58. package/dist/{cli-afrrppsw.js → cli-k899ehvh.js} +2 -2
  59. package/dist/{cli-0731hvbw.js → cli-m0da8j35.js} +23 -9
  60. package/dist/cli-m2z8jagn.js +12078 -0
  61. package/dist/{cli-8veytvbh.js → cli-mkk9rnzz.js} +5 -5
  62. package/dist/{cli-7wspm5gm.js → cli-n6bv12jn.js} +528 -6
  63. package/dist/{cli-spknvxbq.js → cli-nmzhp1b5.js} +14 -12
  64. package/dist/{cli-p599f1qm.js → cli-p82cnnjq.js} +1 -1
  65. package/dist/{cli-xtjhfmpc.js → cli-pc6n7fre.js} +1 -1
  66. package/dist/{cli-vbyfwh92.js → cli-pjctyea3.js} +2 -2
  67. package/dist/{cli-nmbmy64z.js → cli-pq73h5mz.js} +3 -3
  68. package/dist/{cli-aa6yxc7m.js → cli-pz4rk021.js} +1 -1
  69. package/dist/{cli-7wcyk2s5.js → cli-q3tkegby.js} +1 -1
  70. package/dist/cli-qghe0h66.js +67 -0
  71. package/dist/{cli-6s7vdcta.js → cli-qh4d4tvk.js} +4 -4
  72. package/dist/{cli-mmwm7zbf.js → cli-rtt8g4v9.js} +1 -1
  73. package/dist/{cli-kg8hchyg.js → cli-sa02jsqt.js} +1 -1
  74. package/dist/{cli-1h3wcpbw.js → cli-snpj5zbj.js} +2 -2
  75. package/dist/{cli-a7v854rf.js → cli-tgrv9cjx.js} +263 -12
  76. package/dist/{cli-a47xy8eq.js → cli-vz22fyqv.js} +186 -54
  77. package/dist/{cli-1ayk1eas.js → cli-wk5yrfwd.js} +285 -42
  78. package/dist/{cli-1338796d.js → cli-ws19768n.js} +1755 -119
  79. package/dist/{cli-70r77n04.js → cli-xae4g51r.js} +1 -1
  80. package/dist/{cli-gf5hbmdr.js → cli-yjdgea32.js} +3 -3
  81. package/dist/{cli-xbkdm710.js → cli-yndmfxb3.js} +3 -3
  82. package/dist/{cli-rhw40jgb.js → cli-yyj3j5m4.js} +2 -2
  83. package/dist/cli.js +26 -25
  84. package/dist/{cloudflare-workers-ai-system-one-bm60fjm0.js → cloudflare-workers-ai-system-one-ef2a980q.js} +2 -2
  85. package/dist/{codex-adapter-q6k3gnyg.js → codex-adapter-cf2yxe9m.js} +3 -2
  86. package/dist/{codex-hook-9efm2x1m.js → codex-hook-a0c6g6t6.js} +1 -1
  87. package/dist/{codex-session-runner-3mwmk3d9.js → codex-session-runner-6vfzdgbd.js} +3 -2
  88. package/dist/{commands-evrenjn9.js → commands-t3mfy7vn.js} +9 -3
  89. package/dist/{controller-xjp8vswh.js → controller-q7xrbaz0.js} +8 -3
  90. package/dist/{controllers-connect-pp5jnxvc.js → controllers-connect-c6nm70kg.js} +20 -19
  91. package/dist/{cursor-adapter-yf47w4t0.js → cursor-adapter-ftd81ga8.js} +1 -1
  92. package/dist/{db-zxpw6aht.js → db-dk572x25.js} +3 -3
  93. package/dist/{db-rgfsh91f.js → db-vemtpmcc.js} +14 -4
  94. package/dist/{devtools-82fzqra8.js → devtools-qxfgbsqz.js} +1 -1
  95. package/dist/{dispatcher-smasgws7.js → dispatcher-kh7ecxh9.js} +17 -16
  96. package/dist/{docker-rrjhaqt9.js → docker-c1a3gqmf.js} +89 -22
  97. package/dist/{dsh-adapter-ftyy8dnp.js → dsh-adapter-v8628r89.js} +1 -1
  98. package/dist/{e2b-w1p0bnpm.js → e2b-fr12yccx.js} +5 -5
  99. package/dist/{e2b-qcaj675y.js → e2b-x6ywzak3.js} +3 -3
  100. package/dist/{easter-egg-3d-yyts7p16.js → easter-egg-3d-nenw4x63.js} +1 -1
  101. package/dist/{embedding-space-1z0mbg95.js → embedding-space-0jepb239.js} +6 -6
  102. package/dist/{embeddings-ygeg7tm7.js → embeddings-xxt49yxg.js} +1 -1
  103. package/dist/{execute-14qhqp4d.js → execute-1mhw6e4x.js} +1 -1
  104. package/dist/{external-channel-ttgrs3sx.js → external-channel-55v12e60.js} +2 -2
  105. package/dist/{fx-adapter-gt6pncj2.js → fx-adapter-bs8wqrk5.js} +6 -5
  106. package/dist/{google-generative-ai-yvmdxyxa.js → google-generative-ai-v94hr7h0.js} +5 -5
  107. package/dist/{google-vertex-jrtdh9gs.js → google-vertex-g7vr6cf8.js} +5 -5
  108. package/dist/{grok-adapter-sm4m5k4g.js → grok-adapter-gpjqmkcq.js} +6 -5
  109. package/dist/handlers-zxmp1ta4.js +32 -0
  110. package/dist/{heartbeat-qahz4p62.js → heartbeat-k42svnr0.js} +20 -19
  111. package/dist/{hook-4fd5wq5w.js → hook-114m7hap.js} +4 -3
  112. package/dist/{hook-kj3d398m.js → hook-6nphweh0.js} +24 -9
  113. package/dist/{http-1ted0j1k.js → http-r8cc4wz7.js} +1196 -211
  114. package/dist/index-a8phxqnk.js +37 -0
  115. package/dist/{index-q4b917zs.js → index-e3s8wmxy.js} +1 -1
  116. package/dist/{index-6rq3r5zb.js → index-fpk4bcd9.js} +1 -1
  117. package/dist/{index-5rk6rfzr.js → index-fvzke5kx.js} +1 -1
  118. package/dist/index-k6h00t4c.js +30 -0
  119. package/dist/{index-7v51awbn.js → index-nh3wyqdt.js} +1027 -577
  120. package/dist/{index-yqhz88yy.js → index-pxn30mse.js} +8 -8
  121. package/dist/index-xrz7te8h.js +33 -0
  122. package/dist/{keepalive-6cf311t5.js → keepalive-x060nt9p.js} +3 -3
  123. package/dist/lead-956sd2yc.js +71 -0
  124. package/dist/{maintenance-a4bm1bmt.js → maintenance-2xxfexhf.js} +1 -1
  125. package/dist/{mistral-conversations-4pmwj1c0.js → mistral-conversations-3gjpdb4b.js} +4 -4
  126. package/dist/{oauth-refresh-sweep-3h0dfw1k.js → oauth-refresh-sweep-fckc4ar5.js} +2 -2
  127. package/dist/{onboard-myn5hraa.js → onboard-zc48j237.js} +5 -5
  128. package/dist/{openai-codex-responses-44j573nm.js → openai-codex-responses-8pbxq9h5.js} +5 -5
  129. package/dist/{openai-completions-a4gee2a8.js → openai-completions-rm00s912.js} +4 -4
  130. package/dist/{openai-decisions-w4qkrcar.js → openai-decisions-8v957r2r.js} +1 -1
  131. package/dist/{openai-responses-rbp6h5kr.js → openai-responses-agxpf96k.js} +5 -5
  132. package/dist/{opencode-adapter-p2nkb5am.js → opencode-adapter-r690h1cr.js} +4 -3
  133. package/dist/{otel-impl-a1a0ef6b.js → otel-impl-r5xbh81q.js} +4 -4
  134. package/dist/{pi-mono-adapter-rzmpb72a.js → pi-mono-adapter-c7v8sdec.js} +7 -6
  135. package/dist/{pricing-refresh-83drrnxj.js → pricing-refresh-3pmre11v.js} +3 -3
  136. package/dist/{rbac-roles-s8y6ecb5.js → rbac-roles-3vbshwsb.js} +1 -1
  137. package/dist/{rbac-roles-0fb6e7pj.js → rbac-roles-910mmkgz.js} +2 -2
  138. package/dist/{render-v2-twmbgrz4.js → render-v2-pxykkbab.js} +3 -3
  139. package/dist/{resume-zfqc36xj.js → resume-adda0bwy.js} +6 -6
  140. package/dist/{runtime-qwggdpqr.js → runtime-fnbm7stk.js} +1 -1
  141. package/dist/{session-access-sc9pn9da.js → session-access-2fvamrdc.js} +4 -4
  142. package/dist/{session-source-backfill-qh07xr8g.js → session-source-backfill-08xvv0sm.js} +2 -2
  143. package/dist/{setup-4fr0m7a1.js → setup-1aqmvmh1.js} +3 -3
  144. package/dist/{typesafe-system-one-5mavm259.js → typesafe-system-one-0bmqcxfg.js} +2 -2
  145. package/dist/{virtual-modules-fh4ha15b.js → virtual-modules-d2pjk9j1.js} +5 -5
  146. package/dist/{work-objects-0wr8zxct.js → work-objects-9s0yrm5j.js} +20 -19
  147. package/dist/{worker-follow-up-ktyyazyp.js → worker-follow-up-3q78fqmr.js} +17 -16
  148. package/dist/worker-y15hy08h.js +71 -0
  149. package/openapi.json +289 -4
  150. package/package.json +9 -8
  151. package/src/apps/definition.ts +6 -5
  152. package/src/be/db/page-theme-tokens.ts +12 -0
  153. package/src/be/db/script-runs.ts +23 -0
  154. package/src/be/db/tasks/read.ts +131 -4
  155. package/src/be/db/tasks/write.ts +19 -0
  156. package/src/be/db-client.ts +114 -11
  157. package/src/be/db.ts +49 -6
  158. package/src/be/memory/providers/sqlite-store.ts +49 -12
  159. package/src/be/migrations/260_runtime_pool_cache.sql +20 -0
  160. package/src/be/migrations/262_session_visibility_indexes.sql +18 -0
  161. package/src/be/migrations/263_task_list_filter_index.sql +11 -0
  162. package/src/be/migrations/264_slack_thread_cover_index.sql +9 -0
  163. package/src/be/migrations/266_script_runs_source_task.sql +11 -0
  164. package/src/be/migrations/269_pages_design_system.sql +23 -0
  165. package/src/be/runtime-pools/cache-cleanup.ts +109 -0
  166. package/src/be/runtime-pools/db.ts +65 -6
  167. package/src/be/runtime-pools/remote-controller.ts +5 -0
  168. package/src/be/runtime-pools/supervisor.ts +33 -2
  169. package/src/be/scripts/typecheck.ts +22 -0
  170. package/src/be/seed/schedules-seeder.ts +0 -10
  171. package/src/be/seed-scripts/catalog/catalog-report.inline.ts +2 -0
  172. package/src/be/seed-scripts/catalog/catalog-report.ts +2 -0
  173. package/src/be/seed-scripts/catalog/memory-eval.ts +2 -0
  174. package/src/be/seed-scripts/catalog/swarm-update-check.ts +150 -0
  175. package/src/be/seed-scripts/catalog/task-failure-audit.ts +5 -1
  176. package/src/be/seed-scripts/index.ts +9 -0
  177. package/src/be/seed-skills/bundled-files.generated.json +3 -3
  178. package/src/be/swarm-config-guard.ts +32 -0
  179. package/src/be/task-auto-title-listener.ts +127 -0
  180. package/src/commands/controller.ts +4 -0
  181. package/src/commands/entrypoint-api-readiness.ts +74 -0
  182. package/src/commands/onboard/agent-fs-version.ts +1 -1
  183. package/src/commands/profile-sync.ts +151 -16
  184. package/src/commands/runner.ts +15 -7
  185. package/src/hooks/hook.ts +20 -9
  186. package/src/http/index.ts +4 -1
  187. package/src/http/list-total-cache.ts +14 -7
  188. package/src/http/memory.ts +38 -9
  189. package/src/http/pages-public.ts +93 -129
  190. package/src/http/pages.ts +94 -21
  191. package/src/http/script-runs.ts +174 -5
  192. package/src/http/status.ts +10 -2
  193. package/src/http/tasks.ts +4 -2
  194. package/src/pages/browser-sdk.ts +48 -21
  195. package/src/pages/color-literals.ts +115 -0
  196. package/src/pages/page-head.ts +766 -0
  197. package/src/pages/version.ts +3 -0
  198. package/src/providers/amp-adapter.ts +1 -1
  199. package/src/providers/codex-adapter.ts +1 -0
  200. package/src/providers/codex-app-server.ts +27 -1
  201. package/src/providers/fx-adapter.ts +1 -1
  202. package/src/providers/pi-mono-extension.ts +30 -15
  203. package/src/runtime-providers/controller-protocol.ts +14 -1
  204. package/src/runtime-providers/controller.ts +5 -0
  205. package/src/runtime-providers/docker.ts +108 -23
  206. package/src/runtime-providers/types.ts +16 -0
  207. package/src/script-workflows/workflow-ctx.ts +8 -0
  208. package/src/scripts-runtime/types/stdlib.d.ts +22 -0
  209. package/src/scripts-runtime/types/swarm-sdk.d.ts +22 -0
  210. package/src/server-user.ts +1 -0
  211. package/src/slack/commands.ts +5 -1
  212. package/src/slack/handlers.ts +37 -2
  213. package/src/slack/left-threads.ts +27 -4
  214. package/src/tasks/follow-up-gate.ts +318 -0
  215. package/src/tasks/follow-up-redelegation-guard.ts +155 -0
  216. package/src/tasks/worker-follow-up.ts +21 -1
  217. package/src/tests/claude-artifacts-read-script.test.ts +209 -0
  218. package/src/tests/codex-app-server.test.ts +59 -0
  219. package/src/tests/codex-gateway-route.test.ts +50 -1
  220. package/src/tests/create-page-tool.test.ts +501 -0
  221. package/src/tests/db-client-busy-retry.test.ts +81 -0
  222. package/src/tests/db-client.test.ts +21 -0
  223. package/src/tests/db-task-read-contract.test.ts +9 -2
  224. package/src/tests/db-task-write-contract.test.ts +7 -1
  225. package/src/tests/db-tasks-read.test.ts +21 -0
  226. package/src/tests/entrypoint-api-readiness.test.ts +416 -210
  227. package/src/tests/explicit-model-validation.test.ts +14 -8
  228. package/src/tests/fixtures/pages/head-v1.html +56 -0
  229. package/src/tests/follow-up-gate-shadow.test.ts +513 -0
  230. package/src/tests/follow-up-redelivery-guard.test.ts +432 -202
  231. package/src/tests/identity-field-budget.test.ts +1 -1
  232. package/src/tests/internal-ai/credentials.test.ts +62 -0
  233. package/src/tests/internal-ai/task-title.test.ts +283 -0
  234. package/src/tests/list-total-cache.test.ts +27 -2
  235. package/src/tests/memory-fts-populate.test.ts +11 -9
  236. package/src/tests/memory-retrieval-purge.test.ts +80 -0
  237. package/src/tests/merge-gate-detect-changes.test.ts +145 -0
  238. package/src/tests/migration-260-runtime-pool-cache.test.ts +118 -0
  239. package/src/tests/page-proxy.test.ts +1 -1
  240. package/src/tests/page-session-dev-cookie.test.ts +188 -0
  241. package/src/tests/pages-frame-ancestors.test.ts +28 -1
  242. package/src/tests/pages-head-v2.test.ts +534 -0
  243. package/src/tests/pages-http.test.ts +330 -0
  244. package/src/tests/pages-public-html.test.ts +43 -0
  245. package/src/tests/pages-storage.test.ts +182 -0
  246. package/src/tests/pages-versioning.test.ts +72 -0
  247. package/src/tests/profile-sync-stale-writeback.test.ts +334 -0
  248. package/src/tests/profile-sync.test.ts +11 -15
  249. package/src/tests/query-audit-plans.test.ts +27 -0
  250. package/src/tests/runtime-cache-cleanup.test.ts +367 -0
  251. package/src/tests/runtime-e2b-pools.test.ts +41 -0
  252. package/src/tests/runtime-pools-db.test.ts +98 -0
  253. package/src/tests/runtime-pools-http.test.ts +29 -0
  254. package/src/tests/runtime-providers-docker.test.ts +253 -0
  255. package/src/tests/runtime-remote-controller.test.ts +66 -2
  256. package/src/tests/runtime-supervisor.test.ts +66 -2
  257. package/src/tests/scheduled-task-required-params-migration.test.ts +3 -1
  258. package/src/tests/script-connections.test.ts +13 -0
  259. package/src/tests/script-runs-http.test.ts +684 -2
  260. package/src/tests/seed-registry.test.ts +28 -6
  261. package/src/tests/seed-schedules.test.ts +61 -8
  262. package/src/tests/seed-scripts.test.ts +168 -2
  263. package/src/tests/slack-leave-thread.test.ts +145 -5
  264. package/src/tests/slack-render-v2-query-plans.test.ts +24 -2
  265. package/src/tests/status.test.ts +11 -0
  266. package/src/tests/swarm-diff.test.ts +121 -2
  267. package/src/tests/task-auto-title-config-guard.test.ts +37 -0
  268. package/src/tests/task-auto-title-listener.test.ts +165 -0
  269. package/src/tests/tasks-readable-row.test.ts +155 -0
  270. package/src/tests/theme-import.test.ts +758 -0
  271. package/src/tests/themes-page-theme.test.ts +155 -0
  272. package/src/tests/themes-presets.test.ts +172 -0
  273. package/src/tests/workflow-script-principal.test.ts +2 -1
  274. package/src/themes/classics.ts +949 -0
  275. package/src/themes/import.ts +420 -0
  276. package/src/themes/page-fields.ts +40 -0
  277. package/src/themes/page-theme.ts +102 -0
  278. package/src/themes/presets.ts +561 -0
  279. package/src/themes/tokens.ts +218 -0
  280. package/src/tools/app-upsert.ts +1 -1
  281. package/src/tools/create-page.ts +118 -12
  282. package/src/tools/send-task.ts +35 -47
  283. package/src/tools/slack-leave-thread.ts +1 -1
  284. package/src/tools/task-dedup.ts +45 -21
  285. package/src/tools/utils.ts +33 -0
  286. package/src/types.ts +83 -3
  287. package/src/utils/constants.ts +19 -0
  288. package/src/utils/follow-up-gate-schedule-ids.ts +13 -0
  289. package/src/utils/internal-ai/credentials.ts +146 -93
  290. package/src/utils/internal-ai/index.ts +10 -0
  291. package/src/utils/internal-ai/models.ts +18 -0
  292. package/src/utils/internal-ai/task-title.ts +282 -0
  293. package/src/utils/page-session.ts +37 -0
  294. package/templates/schedules/daily-status-report/content.md +88 -2
  295. package/templates/schedules/daily-swarm-update-check/content.md +16 -40
  296. package/templates/skills/apps/SKILL.md +1 -1
  297. package/templates/skills/apps/content.md +1 -1
  298. package/templates/skills/composio-google-docs/content.md +10 -0
  299. package/templates/skills/heartbeat-runbook/content.md +1 -1
  300. package/templates/skills/pages/config.json +1 -1
  301. package/templates/skills/pages/content.md +539 -86
  302. package/templates/skills/pages/files/examples/annotated-pr.html +21 -12
  303. package/templates/skills/pages/files/examples/multiplayer-board.html +41 -41
  304. package/templates/skills/pages/files/examples/report-page.html +125 -124
  305. package/templates/skills/script-workflows/content.md +19 -1
  306. package/templates/skills/swarm-docs/content.md +1 -0
  307. package/templates/skills/swarm-scripts/SKILL.md +2 -0
  308. package/templates/skills/swarm-scripts/content.md +2 -0
  309. package/dist/cli-2pnswgh8.js +0 -189
  310. package/dist/cli-dznrgaep.js +0 -30
  311. package/dist/cli-w3hcrxht.js +0 -6985
  312. package/dist/handlers-vwmba1m2.js +0 -31
  313. package/dist/index-5r71rf98.js +0 -32
  314. package/dist/index-anmwxs87.js +0 -29
  315. package/dist/index-spp0ra5c.js +0 -36
  316. package/dist/lead-fjy418cw.js +0 -69
  317. package/dist/worker-0c5tyzqe.js +0 -69
  318. package/templates/schedules/daily-workflow-health-audit/config.json +0 -19
  319. package/templates/schedules/daily-workflow-health-audit/content.md +0 -191
@@ -446,11 +446,12 @@ export const AppElementsSchema = z.record(AppNameSchema, AppElementSchema);
446
446
 
447
447
  /**
448
448
  * Preset theme id applied to the app's rendered surface. The catalog of
449
- * presets lives in the dashboard (`apps/ui/src/lib/themes.ts`) — the server
450
- * validates shape only, and the renderer resolves unknown ids to its default
451
- * theme, so definitions stay portable across dashboard versions whose preset
452
- * catalogs differ. Viewers can override it per-user via the reserved
453
- * `$theme` user-config key (see `user-config.ts`).
449
+ * presets lives in `src/themes/presets.ts` (the dashboard re-exports it from
450
+ * `apps/ui/src/lib/themes.ts`). App definitions are validated by shape only,
451
+ * and the renderer resolves unknown ids to its default theme, so definitions
452
+ * stay portable across dashboard versions whose preset catalogs differ.
453
+ * Viewers can override it per-user via the reserved `$theme` user-config key
454
+ * (see `user-config.ts`).
454
455
  */
455
456
  export const AppThemeIdSchema = z.string().regex(/^[a-z][a-z0-9-]{0,39}$/, {
456
457
  message: "must be a lowercase slug (letters, digits, dashes)",
@@ -0,0 +1,12 @@
1
+ import { type Page, PageThemeTokensSchema } from "../../types";
2
+
3
+ /** Stored `theme_tokens` JSON, or null when the column is empty, is not JSON, or has the wrong shape. */
4
+ export function parsePageThemeTokens(raw: string | null): Page["themeTokens"] {
5
+ if (!raw) return null;
6
+ try {
7
+ const parsed = PageThemeTokensSchema.safeParse(JSON.parse(raw));
8
+ return parsed.success ? parsed.data : null;
9
+ } catch {
10
+ return null;
11
+ }
12
+ }
@@ -13,3 +13,26 @@ export async function getScriptRunExecutionArgs(id: string): Promise<unknown> {
13
13
  );
14
14
  return row ? openSealedJson(row.args) : undefined;
15
15
  }
16
+
17
+ /** A durable run's follow-up re-delegation guard decision (see migration 266). */
18
+ export type ScriptRunFollowUpGuard = "passed" | "same-work" | "loop-cap";
19
+
20
+ export async function getScriptRunFollowUpGuard(
21
+ runId: string,
22
+ ): Promise<ScriptRunFollowUpGuard | null> {
23
+ const row = await getDbClient().get<{ followUpGuard: ScriptRunFollowUpGuard | null }>(
24
+ "SELECT followUpGuard FROM script_runs WHERE id = ?",
25
+ [runId],
26
+ );
27
+ return row?.followUpGuard ?? null;
28
+ }
29
+
30
+ export async function setScriptRunFollowUpGuard(
31
+ runId: string,
32
+ decision: ScriptRunFollowUpGuard,
33
+ ): Promise<void> {
34
+ await getDbClient().run("UPDATE script_runs SET followUpGuard = ? WHERE id = ?", [
35
+ decision,
36
+ runId,
37
+ ]);
38
+ }
@@ -869,6 +869,52 @@ export function readableSessionCondition(
869
869
  };
870
870
  }
871
871
 
872
+ /** Parent hops the row-at-a-time walk takes before it falls back to the set form. */
873
+ const READABLE_WALK_MAX_DEPTH = 64;
874
+
875
+ /**
876
+ * The row-at-a-time form of {@link readableSessionCondition}, for a query that
877
+ * stops at a LIMIT. It walks up from each row it tests to the root, so a page
878
+ * costs a few key lookups per row instead of the set of every hidden task,
879
+ * which on a large DB took hundreds of milliseconds per request. The rule is
880
+ * the same: the root is the first task whose parent is null or missing. A row
881
+ * whose walk finds no root within {@link READABLE_WALK_MAX_DEPTH} hops (a very
882
+ * deep chain, or a parent cycle written before migration 253) falls back to
883
+ * the set form, which SQLite then builds once for the statement. A count reads
884
+ * every row, so it keeps the set form.
885
+ */
886
+ function readableSessionRowCondition(
887
+ userId: string | null,
888
+ taskIdColumn = "agent_tasks.id",
889
+ ): { sql: string; params: string[] } {
890
+ const otherOwner = userId
891
+ ? "up.requestedByUserId IS NOT NULL AND up.requestedByUserId != ?"
892
+ : "up.requestedByUserId IS NOT NULL";
893
+ const shareMatch = userId ? "(s.userId IS NULL OR s.userId = ?)" : "s.userId IS NULL";
894
+ const set = readableSessionCondition(userId, taskIdColumn);
895
+ return {
896
+ sql: `(CASE (
897
+ WITH RECURSIVE up(id, parentTaskId, requestedByUserId, scheduleId, depth) AS (
898
+ SELECT u.id, u.parentTaskId, u.requestedByUserId, u.scheduleId, 0
899
+ FROM agent_tasks u WHERE u.id = ${taskIdColumn}
900
+ UNION ALL
901
+ SELECT p.id, p.parentTaskId, p.requestedByUserId, p.scheduleId, up.depth + 1
902
+ FROM up JOIN agent_tasks p ON p.id = up.parentTaskId
903
+ WHERE up.depth < ${READABLE_WALK_MAX_DEPTH}
904
+ )
905
+ SELECT CASE WHEN ${otherOwner} AND up.scheduleId IS NULL
906
+ AND NOT EXISTS (SELECT 1 FROM session_shares s
907
+ WHERE s.rootTaskId = up.id AND s.revokedAt IS NULL AND ${shareMatch})
908
+ THEN 1 ELSE 0 END
909
+ FROM up
910
+ WHERE up.parentTaskId IS NULL
911
+ OR NOT EXISTS (SELECT 1 FROM agent_tasks p WHERE p.id = up.parentTaskId)
912
+ LIMIT 1
913
+ ) WHEN 0 THEN 1 WHEN 1 THEN 0 ELSE ${set.sql} END)`,
914
+ params: [...(userId ? [userId, userId] : []), ...set.params],
915
+ };
916
+ }
917
+
872
918
  export function getAllTasks(filters?: TaskFilters): Promise<AgentTask[]>;
873
919
  export function getAllTasks(
874
920
  filters: TaskFilters | undefined,
@@ -977,6 +1023,14 @@ export async function getAllTasks(
977
1023
  params.push(filters.requestedByUserId);
978
1024
  }
979
1025
 
1026
+ // Without selective filters, tiny LIMITs can make SQLite choose a full
1027
+ // scan and sort when ANALYZE statistics are absent. Keep filtered pages
1028
+ // planner-controlled so they can use their selective indexes.
1029
+ const pageIndex =
1030
+ conditions.length === 0 && !filters?.readableBy && filters?.orderBy !== "createdAt"
1031
+ ? "INDEXED BY idx_agent_tasks_last_updated"
1032
+ : "";
1033
+
980
1034
  // Exclude system/heartbeat tasks by default. The flag is still called
981
1035
  // `includeHeartbeat` for backward compat with existing API callers, but we
982
1036
  // also gate boot-triage + heartbeat-checklist behind it since those are
@@ -988,7 +1042,8 @@ export async function getAllTasks(
988
1042
  }
989
1043
 
990
1044
  if (filters?.readableBy) {
991
- const readable = readableSessionCondition(filters.readableBy.userId);
1045
+ // The page stops at LIMIT, so test only the rows it reads.
1046
+ const readable = readableSessionRowCondition(filters.readableBy.userId);
992
1047
  conditions.push(readable.sql);
993
1048
  params.push(...readable.params);
994
1049
  }
@@ -1009,7 +1064,7 @@ export async function getAllTasks(
1009
1064
  // lastUpdatedAt index for a full scan. The outer sort adds it only to fix
1010
1065
  // the order of exact ties inside the page.
1011
1066
  const query = `WITH page AS (
1012
- SELECT rowid AS rid FROM agent_tasks ${whereClause}
1067
+ SELECT rowid AS rid FROM agent_tasks ${pageIndex} ${whereClause}
1013
1068
  ORDER BY ${orderColumns.join(", ")} LIMIT ${limit} OFFSET ${offset}
1014
1069
  )
1015
1070
  SELECT ${LIST_COLUMNS[fields]},
@@ -1865,7 +1920,44 @@ export async function getMostRecentTaskInThread(
1865
1920
  return row ? rowToAgentTask(row) : null;
1866
1921
  }
1867
1922
 
1868
- export async function findCompletedTaskInThread(
1923
+ /**
1924
+ * One page of completed tasks in a Slack thread, most recent first. Lead
1925
+ * `follow-up` sessions are excluded: they coordinate the work, they are not the
1926
+ * work, so they never count as "the request was already handled". The caller
1927
+ * matches descriptions in JS, so it pages through the whole window with the
1928
+ * `before` keyset cursor (the last row of the previous page) instead of
1929
+ * loading every row in one query.
1930
+ */
1931
+ export async function findCompletedTasksInThread(
1932
+ channelId: string,
1933
+ threadTs: string,
1934
+ windowMinutes: number,
1935
+ limit = 200,
1936
+ before?: { lastUpdatedAt: string; id: string },
1937
+ ): Promise<AgentTask[]> {
1938
+ const since = new Date(Date.now() - windowMinutes * 60 * 1000).toISOString();
1939
+ const cursorSql = before ? "AND (lastUpdatedAt < ? OR (lastUpdatedAt = ? AND id < ?))" : "";
1940
+ const cursorParams = before ? [before.lastUpdatedAt, before.lastUpdatedAt, before.id] : [];
1941
+ const rows = await getDbClient().query<AgentTaskRow>(
1942
+ `SELECT * FROM agent_tasks
1943
+ WHERE slackChannelId = ?
1944
+ AND slackThreadTs = ?
1945
+ AND status = 'completed'
1946
+ AND COALESCE(taskType, '') != 'follow-up'
1947
+ AND lastUpdatedAt > ?
1948
+ ${cursorSql}
1949
+ ORDER BY lastUpdatedAt DESC, id DESC
1950
+ LIMIT ?`,
1951
+ [channelId, threadTs, since, ...cursorParams, limit],
1952
+ );
1953
+ return rows.map(rowToAgentTask);
1954
+ }
1955
+
1956
+ /**
1957
+ * Most recent FAILED task in a Slack thread. A failure more recent than a
1958
+ * completion means the follow-up re-delegation guard must let a retry through.
1959
+ */
1960
+ export async function findRecentFailedTaskInThread(
1869
1961
  channelId: string,
1870
1962
  threadTs: string,
1871
1963
  windowMinutes: number,
@@ -1875,7 +1967,7 @@ export async function findCompletedTaskInThread(
1875
1967
  `SELECT * FROM agent_tasks
1876
1968
  WHERE slackChannelId = ?
1877
1969
  AND slackThreadTs = ?
1878
- AND status = 'completed'
1970
+ AND status = 'failed'
1879
1971
  AND lastUpdatedAt > ?
1880
1972
  ORDER BY lastUpdatedAt DESC
1881
1973
  LIMIT 1`,
@@ -1884,6 +1976,41 @@ export async function findCompletedTaskInThread(
1884
1976
  return row ? rowToAgentTask(row) : null;
1885
1977
  }
1886
1978
 
1979
+ /**
1980
+ * Count the dispatches one agent made into a Slack thread since the window
1981
+ * start: each `send-task` task counts once, and each durable script run counts
1982
+ * once however many `ctx.step.agentTask` steps it created (step tasks share the
1983
+ * `script-run:<runId>:` contextKey prefix). A run is dated by its first step,
1984
+ * the dispatch the guard checked, so a run that started before the window does
1985
+ * not count again for steps it adds later. The follow-up re-delegation loop cap
1986
+ * reads this.
1987
+ */
1988
+ export async function countAgentDispatchesInThread(
1989
+ channelId: string,
1990
+ threadTs: string,
1991
+ creatorAgentId: string,
1992
+ windowMinutes: number,
1993
+ ): Promise<number> {
1994
+ const since = new Date(Date.now() - windowMinutes * 60 * 1000).toISOString();
1995
+ const row = await getDbClient().get<{ c: number }>(
1996
+ `SELECT COUNT(*) AS c FROM (
1997
+ SELECT MIN(createdAt) AS firstAt FROM agent_tasks
1998
+ WHERE slackChannelId = ?
1999
+ AND slackThreadTs = ?
2000
+ AND creatorAgentId = ?
2001
+ AND source = 'mcp'
2002
+ AND (createdAt > ? OR taskType = 'script-run-step')
2003
+ GROUP BY CASE
2004
+ WHEN taskType = 'script-run-step' AND contextKey LIKE 'script-run:%:%'
2005
+ THEN substr(contextKey, 1, instr(substr(contextKey, 12), ':') + 10)
2006
+ ELSE id
2007
+ END
2008
+ ) WHERE firstAt > ?`,
2009
+ [channelId, threadTs, creatorAgentId, since, since],
2010
+ );
2011
+ return row?.c ?? 0;
2012
+ }
2013
+
1887
2014
  /**
1888
2015
  * Find the most recent CANCELLED task in a Slack thread. Used by the
1889
2016
  * follow-up re-delegation guard so a cancellation (worker SIGTERM,
@@ -394,6 +394,25 @@ export async function updateTaskTitle(
394
394
  return row ? rowToAgentTask(row) : null;
395
395
  }
396
396
 
397
+ /**
398
+ * Sets a generated display title only while the task has none. A rename that
399
+ * landed first (a person, or another API replica) wins: the row is left alone
400
+ * and `null` comes back. Like `updateTaskTitle`, it does not bump
401
+ * `lastUpdatedAt`.
402
+ */
403
+ export async function setTaskTitleIfUnset(
404
+ taskId: string,
405
+ title: string,
406
+ ): Promise<AgentTask | null> {
407
+ const normalized = title.trim();
408
+ if (!normalized) return null;
409
+ const row = await getDbClient().get<AgentTaskRow>(
410
+ "UPDATE agent_tasks SET title = ? WHERE id = ? AND title IS NULL RETURNING *",
411
+ [normalized, taskId],
412
+ );
413
+ return row ? rowToAgentTask(row) : null;
414
+ }
415
+
397
416
  export async function completeTask(
398
417
  id: string,
399
418
  output?: string,
@@ -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 };
@@ -468,11 +552,17 @@ class BunSqliteClient implements DbClient {
468
552
  if (!willRetry) warnIfSlowStatement(sql, ms);
469
553
  }
470
554
  };
555
+ // The first call opens the database and applies migrations; resolve it
556
+ // before the timer, so boot work is not logged as this statement's cost.
557
+ // Outside a transaction it resolves inside the retry loop, so a boot that
558
+ // hits SQLITE_BUSY boots again.
471
559
  const ctx = txContext.getStore();
472
560
  if (ctx && !ctx.closed) {
473
- return timed(() => op(this.getDatabase()));
561
+ const db = this.getDatabase();
562
+ return timed(() => op(db));
474
563
  }
475
564
  let sleptMs = 0;
565
+ let sawBusy = false;
476
566
  for (let attemptIndex = 0; ; attemptIndex++) {
477
567
  const next = this.busyRetry.backoffMs[attemptIndex];
478
568
  const canRetryAfter = next !== undefined && sleptMs + next <= this.busyRetry.maxWaitMs;
@@ -482,12 +572,15 @@ class BunSqliteClient implements DbClient {
482
572
  // A top-level single statement is atomic, so a repeat on BUSY is
483
573
  // exact. The final attempt spins `finalAttemptSpinMs` (see
484
574
  // transaction()).
485
- return timed(
486
- () => this.shortSpin(this.getDatabase(), () => op(this.getDatabase()), !canRetryAfter),
487
- canRetryAfter,
488
- );
575
+ const db = this.getDatabase();
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;
489
581
  } catch (err) {
490
582
  if (!isSqliteBusy(err) || !canRetryAfter || next === undefined) throw err;
583
+ sawBusy = true;
491
584
  backoff = next;
492
585
  sleptMs += next;
493
586
  } finally {
@@ -506,17 +599,27 @@ class BunSqliteClient implements DbClient {
506
599
  * lock is held, so the set/attempt/restore sequence cannot interleave with
507
600
  * other client work.
508
601
  */
509
- private shortSpin<T>(db: Database, attempt: () => T, finalAttempt = false): T {
602
+ private shortSpin<T>(db: Database, attempt: () => T, finalAttempt = false, gateable = true): T {
510
603
  if (this.restoreBusyTimeoutMs === null) {
511
604
  const row = db.query<{ timeout: number }, []>("PRAGMA busy_timeout").get();
512
605
  this.restoreBusyTimeoutMs = row?.timeout ?? 5000;
513
606
  }
514
- const spinMs = finalAttempt
515
- ? (this.busyRetry.finalAttemptSpinMs ?? DEFAULT_FINAL_ATTEMPT_SPIN_MS)
516
- : 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;
517
617
  db.exec(`PRAGMA busy_timeout = ${spinMs}`);
518
618
  try {
519
619
  return attempt();
620
+ } catch (err) {
621
+ if (gateable && !gated && isSqliteBusy(err)) this.gate.recordBusy();
622
+ throw err;
520
623
  } finally {
521
624
  db.exec(`PRAGMA busy_timeout = ${this.restoreBusyTimeoutMs}`);
522
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,
@@ -313,7 +314,11 @@ export {
313
314
  getContextVersionHistory,
314
315
  getLatestContextVersion,
315
316
  } from "./db/context-versions";
316
- export { getScriptRunExecutionArgs } from "./db/script-runs";
317
+ export {
318
+ getScriptRunExecutionArgs,
319
+ getScriptRunFollowUpGuard,
320
+ setScriptRunFollowUpGuard,
321
+ } from "./db/script-runs";
317
322
  export {
318
323
  AssetKeyMovesBlockedError,
319
324
  assertAssetKeyMovesAllowed,
@@ -336,12 +341,14 @@ export {
336
341
  } from "./db/runtime";
337
342
 
338
343
  export {
344
+ countAgentDispatchesInThread,
339
345
  type ExistingTrackerContextWork,
340
346
  type ExistingTrackerContextWorkReason,
341
347
  findActiveTasksByVcs,
342
- findCompletedTaskInThread,
348
+ findCompletedTasksInThread,
343
349
  findExistingLinearTrackerContextWork,
344
350
  findRecentCancelledTaskInThread,
351
+ findRecentFailedTaskInThread,
345
352
  findTaskByGitHub,
346
353
  findTaskByVcs,
347
354
  getAgentCurrentTask,
@@ -407,6 +414,7 @@ export {
407
414
  recordTaskProviderIfUnset,
408
415
  resetOrphanedInProgressTasksForAgent,
409
416
  resumeTask,
417
+ setTaskTitleIfUnset,
410
418
  settleSupersededTaskDependents,
411
419
  startTask,
412
420
  supersedeTask,
@@ -8613,6 +8621,9 @@ type PageRow = {
8613
8621
  createdAt: string;
8614
8622
  updatedAt: string;
8615
8623
  view_count: number;
8624
+ theme: string | null;
8625
+ theme_tokens: string | null;
8626
+ design_version: number;
8616
8627
  };
8617
8628
 
8618
8629
  function rowToPage(row: PageRow): Page {
@@ -8630,6 +8641,9 @@ function rowToPage(row: PageRow): Page {
8630
8641
  needsCredentials: row.needsCredentials
8631
8642
  ? (JSON.parse(row.needsCredentials) as string[])
8632
8643
  : undefined,
8644
+ theme: row.theme ?? null,
8645
+ themeTokens: parsePageThemeTokens(row.theme_tokens),
8646
+ designVersion: typeof row.design_version === "number" ? row.design_version : 1,
8633
8647
  viewCount: typeof row.view_count === "number" ? row.view_count : 0,
8634
8648
  createdAt: normalizeDateRequired(row.createdAt),
8635
8649
  updatedAt: normalizeDateRequired(row.updatedAt),
@@ -8647,13 +8661,17 @@ export async function createPage(data: {
8647
8661
  passwordHash?: string;
8648
8662
  body: string;
8649
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;
8650
8668
  }): Promise<Page> {
8651
8669
  // Match the historical SQL default ID shape while making the value
8652
8670
  // available before insert so the default namespace can include it.
8653
8671
  const id = crypto.randomUUID().replace(/-/g, "");
8654
8672
  const row = await getDbClient().get<PageRow>(
8655
- `INSERT INTO pages (id, "key", agentId, slug, title, description, contentType, authMode, passwordHash, body, needsCredentials)
8656
- 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 *`,
8657
8675
  [
8658
8676
  id,
8659
8677
  normalizeAssetKey(data.key ?? defaultAssetKey("page", id)),
@@ -8666,6 +8684,9 @@ export async function createPage(data: {
8666
8684
  data.passwordHash ?? null,
8667
8685
  data.body,
8668
8686
  data.needsCredentials ? JSON.stringify(data.needsCredentials) : null,
8687
+ data.theme ?? null,
8688
+ data.themeTokens ? JSON.stringify(data.themeTokens) : null,
8689
+ data.designVersion ?? 2,
8669
8690
  ],
8670
8691
  );
8671
8692
  if (!row) throw new Error("Failed to create page");
@@ -8852,6 +8873,11 @@ export async function updatePage(
8852
8873
  body?: string;
8853
8874
  needsCredentials?: string[] | null;
8854
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;
8855
8881
  },
8856
8882
  ): Promise<Page | null> {
8857
8883
  const updates: string[] = [];
@@ -8892,6 +8918,18 @@ export async function updatePage(
8892
8918
  updates.push("slug = ?");
8893
8919
  params.push(data.slug);
8894
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
+ }
8895
8933
  if (updates.length === 0) return getPage(id);
8896
8934
  updates.push("updatedAt = ?");
8897
8935
  params.push(new Date().toISOString());
@@ -14116,6 +14154,7 @@ type ScriptRunRow = {
14116
14154
  last_heartbeat_at: string | null;
14117
14155
  idempotencyKey: string | null;
14118
14156
  requestedByUserId: string | null;
14157
+ sourceTaskId: string | null;
14119
14158
  created_by: string | null;
14120
14159
  updated_by: string | null;
14121
14160
  };
@@ -14160,6 +14199,7 @@ function rowToScriptRun(row: ScriptRunRow): ScriptRun {
14160
14199
  lastHeartbeatAt: row.last_heartbeat_at ?? undefined,
14161
14200
  idempotencyKey: row.idempotencyKey ?? undefined,
14162
14201
  requestedByUserId: row.requestedByUserId ?? undefined,
14202
+ sourceTaskId: row.sourceTaskId ?? undefined,
14163
14203
  };
14164
14204
  }
14165
14205
 
@@ -14188,6 +14228,7 @@ export async function createScriptRun(data: {
14188
14228
  scriptName?: string;
14189
14229
  idempotencyKey?: string;
14190
14230
  requestedByUserId?: string;
14231
+ sourceTaskId?: string;
14191
14232
  createdBy?: string;
14192
14233
  updatedBy?: string;
14193
14234
  }): Promise<{ run: ScriptRun; existing: boolean }> {
@@ -14202,8 +14243,9 @@ export async function createScriptRun(data: {
14202
14243
 
14203
14244
  const row = await client.get<ScriptRunRow>(
14204
14245
  `INSERT INTO script_runs
14205
- (id, agentId, scriptName, source, args, idempotencyKey, requestedByUserId, created_by, updated_by)
14206
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
14246
+ (id, agentId, scriptName, source, args, idempotencyKey, requestedByUserId, sourceTaskId,
14247
+ created_by, updated_by)
14248
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
14207
14249
  RETURNING *`,
14208
14250
  [
14209
14251
  data.id,
@@ -14216,6 +14258,7 @@ export async function createScriptRun(data: {
14216
14258
  sealJson(data.args ?? null),
14217
14259
  data.idempotencyKey ?? null,
14218
14260
  data.requestedByUserId ?? null,
14261
+ data.sourceTaskId ?? null,
14219
14262
  data.createdBy ?? null,
14220
14263
  data.updatedBy ?? data.createdBy ?? null,
14221
14264
  ],