@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
@@ -27,6 +27,7 @@
27
27
  * but it must be VISIBLE.
28
28
  */
29
29
 
30
+ import { rename } from "node:fs/promises";
30
31
  import { resolveTemplateAsync } from "../prompts/resolver.ts";
31
32
  import type { Agent, ProfileExpectedHashes, SwarmEvent } from "../types.ts";
32
33
  import { MAX_PROFILE_FILE_LENGTH } from "../utils/constants.ts";
@@ -52,10 +53,13 @@ export const SETUP_SCRIPT_PATH = "/workspace/start-up.sh";
52
53
  // Flow:
53
54
  // 1. Runner writes DB content → /workspace/*.md at session start.
54
55
  // 2. Runner records SHA-256 hashes of the written content (the "baselines").
55
- // 3. At session end, sync compares current file hash against its baseline.
56
+ // 3. Every sync (session end AND on-edit) compares each file's hash against
57
+ // its baseline.
56
58
  // - Hash matches → file untouched by the agent → skip sync (preserves
57
59
  // any DB-side edits Lead made during the session).
58
60
  // - Hash differs → agent modified the file → sync it back to DB.
61
+ // 4. After the API accepts a file, its baseline advances to the synced hash,
62
+ // so a file edited once is not re-sent over a later DB-side edit.
59
63
  // ──────────────────────────────────────────────────────────────────────────
60
64
  export const IDENTITY_BASELINES_PATH = "/tmp/identity-baselines.json";
61
65
 
@@ -337,8 +341,71 @@ export async function prependProfileSyncRejectionBanner(
337
341
  : { prompt, injected: false };
338
342
  }
339
343
 
344
+ /**
345
+ * Serializes every read-modify-write of IDENTITY_BASELINES_PATH across
346
+ * processes: the runner, the Claude hook and the pi extension each advance it,
347
+ * and two unserialized advances drop one another's hashes.
348
+ */
349
+ export const IDENTITY_BASELINES_LOCK_PATH = `${IDENTITY_BASELINES_PATH}.lock`;
350
+ /** The baselines critical section is one small read and write; wait this long for it. */
351
+ const BASELINES_LOCK_WAIT_MS = 5_000;
352
+
353
+ /** Profile-file IO, injectable so tests never touch the real workspace. */
354
+ export interface ProfileFileIO {
355
+ readFile: FileReader;
356
+ writeFile: FileWriter;
357
+ /** Lock for baseline updates; defaults to IDENTITY_BASELINES_LOCK_PATH. */
358
+ baselinesLockPath?: string;
359
+ }
360
+
361
+ export const PROFILE_FILE_IO: ProfileFileIO = {
362
+ readFile: readFileIfExists,
363
+ writeFile: writeFileAtomic,
364
+ };
365
+
366
+ /**
367
+ * Replace the baselines at boot. Taken under the baselines lock so an advance
368
+ * still in flight from an earlier session cannot reapply its pre-boot read; if
369
+ * the lock is unavailable the boot baselines are written anyway.
370
+ */
340
371
  export async function writeIdentityBaselines(baselines: IdentityBaselines): Promise<void> {
341
- await Bun.write(IDENTITY_BASELINES_PATH, JSON.stringify(baselines));
372
+ const write = () => writeFileAtomic(IDENTITY_BASELINES_PATH, JSON.stringify(baselines));
373
+ const locked = await withFileLock(IDENTITY_BASELINES_LOCK_PATH, write, {
374
+ waitMs: BASELINES_LOCK_WAIT_MS,
375
+ });
376
+ if (!locked.acquired) await write();
377
+ }
378
+
379
+ /**
380
+ * The baselines the runner records at boot: the hash of each DB value the
381
+ * workspace file now derives from. An empty HEARTBEAT.md is a valid DB value
382
+ * and is never rewritten at boot, so it is baselined too; otherwise the empty
383
+ * file that survives a restart would count as an edit and be sent over a later
384
+ * DB edit. The setup script is baselined from the entrypoint-composed file.
385
+ */
386
+ export function bootIdentityBaselines(
387
+ profile: {
388
+ soulMd?: string;
389
+ identityMd?: string;
390
+ toolsMd?: string;
391
+ heartbeatMd?: string | null;
392
+ claudeMd?: string;
393
+ },
394
+ setupScriptFile: string | undefined,
395
+ agentId = "unknown",
396
+ ): IdentityBaselines {
397
+ const baselines: IdentityBaselines = {};
398
+ if (profile.soulMd) baselines.soulMd = contentSha256(profile.soulMd);
399
+ if (profile.identityMd) baselines.identityMd = contentSha256(profile.identityMd);
400
+ if (profile.toolsMd) baselines.toolsMd = contentSha256(profile.toolsMd);
401
+ if (typeof profile.heartbeatMd === "string") {
402
+ baselines.heartbeatMd = contentSha256(profile.heartbeatMd);
403
+ }
404
+ if (profile.claudeMd) baselines.claudeMd = contentSha256(profile.claudeMd);
405
+ const setupContent =
406
+ setupScriptFile === undefined ? null : extractSetupScriptContent(setupScriptFile, agentId);
407
+ if (setupContent) baselines.setupScript = contentSha256(setupContent);
408
+ return baselines;
342
409
  }
343
410
 
344
411
  export async function readIdentityBaselines(
@@ -353,6 +420,42 @@ export async function readIdentityBaselines(
353
420
  }
354
421
  }
355
422
 
423
+ /**
424
+ * Record the hashes of profile files the API just accepted: the DB now holds
425
+ * that content, so the same bytes on disk are no longer an edit. The update
426
+ * runs under IDENTITY_BASELINES_LOCK_PATH so concurrent syncs never drop each
427
+ * other's hashes. Best effort: on failure the older baselines stay (logged).
428
+ */
429
+ export async function advanceIdentityBaselines(
430
+ synced: Record<string, string>,
431
+ io: ProfileFileIO = PROFILE_FILE_IO,
432
+ ): Promise<void> {
433
+ if (Object.keys(synced).length === 0) return;
434
+ const update = async () => {
435
+ const baselines = (await readIdentityBaselines(io.readFile)) ?? {};
436
+ for (const [field, content] of Object.entries(synced)) {
437
+ baselines[field] = contentSha256(content);
438
+ }
439
+ await io.writeFile(IDENTITY_BASELINES_PATH, JSON.stringify(baselines));
440
+ };
441
+ try {
442
+ const locked = await withFileLock(
443
+ io.baselinesLockPath ?? IDENTITY_BASELINES_LOCK_PATH,
444
+ update,
445
+ {
446
+ waitMs: BASELINES_LOCK_WAIT_MS,
447
+ },
448
+ );
449
+ if (locked.acquired) return;
450
+ // No flock on this platform: nothing else can serialize it, so update anyway.
451
+ if (locked.reason === "unsupported") return await update();
452
+ console.warn(`[profile-sync] identity baselines not advanced: lock ${locked.reason}`);
453
+ } catch (err) {
454
+ const msg = err instanceof Error ? err.message : String(err);
455
+ console.warn(scrubSecrets(`[profile-sync] identity baselines not advanced: ${msg}`));
456
+ }
457
+ }
458
+
356
459
  /**
357
460
  * Claude Code's personal-file CLAUDE.md path. This is what the Claude plugin
358
461
  * Stop hook reads and owns — the runner only uses it as a backstop for an
@@ -581,6 +684,8 @@ export interface ProfileSyncOptions {
581
684
  claudeMdLock?: { path?: string; waitMs?: number };
582
685
  /** Injectable fetch for tests. Defaults to the global `fetch`. */
583
686
  fetchImpl?: typeof fetch;
687
+ /** Injectable profile-file IO for tests. Defaults to the worker's FS. */
688
+ io?: ProfileFileIO;
584
689
  }
585
690
 
586
691
  /**
@@ -658,7 +763,7 @@ export function extractSetupScriptContent(raw: string, agentId = "unknown"): str
658
763
  *
659
764
  * When `baselines` is provided, skips any field whose content hash matches the
660
765
  * baseline (i.e. the file was not modified during the session). This prevents
661
- * session-end sync from clobbering DB-side edits made by Lead.
766
+ * a sync from clobbering DB-side edits made by Lead.
662
767
  */
663
768
  export function buildIdentityPayload(
664
769
  files: {
@@ -729,7 +834,7 @@ export function buildIdentityPayload(
729
834
  export type FileReader = (path: string) => Promise<string | undefined>;
730
835
 
731
836
  /** Default file reader — reads from the worker's local FS via Bun. */
732
- async function readFileIfExists(path: string): Promise<string | undefined> {
837
+ export async function readFileIfExists(path: string): Promise<string | undefined> {
733
838
  try {
734
839
  const file = Bun.file(path);
735
840
  if (!(await file.exists())) return undefined;
@@ -739,6 +844,16 @@ async function readFileIfExists(path: string): Promise<string | undefined> {
739
844
  }
740
845
  }
741
846
 
847
+ /** Writes a file's text. */
848
+ export type FileWriter = (path: string, content: string) => Promise<void>;
849
+
850
+ /** Write via a temp file and rename, so a concurrent reader never sees a partial file. */
851
+ async function writeFileAtomic(path: string, content: string): Promise<void> {
852
+ const tmp = `${path}.${process.pid}.${crypto.randomUUID()}.tmp`;
853
+ await Bun.write(tmp, content);
854
+ await rename(tmp, path);
855
+ }
856
+
742
857
  /**
743
858
  * Compare the five local profile markdown files with the authenticated agent's
744
859
  * stored DB values. This must run inside the agent's local environment; a
@@ -788,11 +903,11 @@ export async function auditProfileDivergences(
788
903
  * `fields` selects which groups to include. The file reader is injectable so
789
904
  * the field-selection / guard logic can be unit-tested without touching the FS.
790
905
  *
791
- * When `changeSource` is `"session_sync"`, loads baseline hashes written at
792
- * session start and skips identity fields whose content hasn't changed — this
793
- * prevents blind-overwriting DB-side edits made by Lead during the session.
794
- * On-edit syncs (`"self_edit"`) bypass baselines entirely since the agent
795
- * explicitly changed the file and the new content should propagate.
906
+ * Loads the baseline hashes and skips identity fields and the setup script
907
+ * whose content hasn't changed — this prevents blind-overwriting DB-side edits
908
+ * made by Lead during the session. That holds for on-edit syncs
909
+ * (`"self_edit"`) too: an edit to one file triggers a sync of every identity
910
+ * file, and the untouched ones still hold the boot-time copy.
796
911
  */
797
912
  export async function collectProfilePayloads(
798
913
  fields: ProfileSyncField[],
@@ -803,7 +918,7 @@ export async function collectProfilePayloads(
803
918
  ): Promise<ProfilePayload[]> {
804
919
  const payloads: ProfilePayload[] = [];
805
920
 
806
- const baselines = changeSource === "session_sync" ? await readIdentityBaselines(readFile) : null;
921
+ const baselines = await readIdentityBaselines(readFile);
807
922
 
808
923
  if (fields.includes("identity")) {
809
924
  const updates = buildIdentityPayload(
@@ -843,7 +958,7 @@ export async function collectProfilePayloads(
843
958
  const raw = await readFile(SETUP_SCRIPT_PATH);
844
959
  if (raw !== undefined) {
845
960
  const content = extractSetupScriptContent(raw, agentId);
846
- if (content !== null) {
961
+ if (content !== null && baselines?.setupScript !== contentSha256(content)) {
847
962
  payloads.push({ label: "setup", body: { setupScript: content, changeSource } });
848
963
  }
849
964
  }
@@ -855,12 +970,13 @@ export async function collectProfilePayloads(
855
970
  /**
856
971
  * POST a single profile update. NON-FATAL but VISIBLE: a non-2xx response or a
857
972
  * thrown error is logged (scrubbed) and swallowed so it never fails the task,
858
- * but — unlike the original copies — it is never silently ignored.
973
+ * but — unlike the original copies — it is never silently ignored. Resolves to
974
+ * whether the API accepted it.
859
975
  */
860
976
  export async function postProfileUpdate(
861
977
  opts: Pick<ProfileSyncOptions, "agentId" | "apiUrl" | "apiKey" | "fetchImpl">,
862
978
  payload: ProfilePayload,
863
- ): Promise<void> {
979
+ ): Promise<boolean> {
864
980
  const doFetch = opts.fetchImpl ?? fetch;
865
981
  try {
866
982
  const resp = await doFetch(`${opts.apiUrl}/api/agents/${opts.agentId}/profile`, {
@@ -885,10 +1001,26 @@ export async function postProfileUpdate(
885
1001
  ),
886
1002
  );
887
1003
  }
1004
+ return resp.ok;
888
1005
  } catch (err) {
889
1006
  const msg = err instanceof Error ? err.message : String(err);
890
1007
  console.warn(scrubSecrets(`[profile-sync] ${payload.label} sync errored: ${msg}`));
1008
+ return false;
1009
+ }
1010
+ }
1011
+
1012
+ /**
1013
+ * The baseline-tracked profile fields a payload carried. Only for payloads sent
1014
+ * outside the CLAUDE.md lock: the hook-owned personal CLAUDE.md keeps its own
1015
+ * lineage record, but an unconditional CLAUDE.md sync (the workspace copy, or a
1016
+ * self_edit) is tracked by its baseline like every other file.
1017
+ */
1018
+ function baselineFieldsOf(payload: ProfilePayload): Record<string, string> {
1019
+ const fields: Record<string, string> = {};
1020
+ for (const [field, value] of Object.entries(payload.body)) {
1021
+ if (field !== "changeSource" && typeof value === "string") fields[field] = value;
891
1022
  }
1023
+ return fields;
892
1024
  }
893
1025
 
894
1026
  /**
@@ -912,16 +1044,19 @@ export async function syncProfileFilesToServer(opts: ProfileSyncOptions): Promis
912
1044
  fields.includes("claude") && syncsHookOwnedClaudeMd(claudeMdPath, changeSource);
913
1045
  const unlockedFields = claudeUnderLock ? fields.filter((f) => f !== "claude") : fields;
914
1046
 
1047
+ const io = opts.io ?? PROFILE_FILE_IO;
915
1048
  const payloads = await collectProfilePayloads(
916
1049
  unlockedFields,
917
1050
  changeSource,
918
- readFileIfExists,
1051
+ io.readFile,
919
1052
  claudeMdPath,
920
1053
  opts.agentId,
921
1054
  );
1055
+ const synced: Record<string, string> = {};
922
1056
  for (const payload of payloads) {
923
- await postProfileUpdate(opts, payload);
1057
+ if (await postProfileUpdate(opts, payload)) Object.assign(synced, baselineFieldsOf(payload));
924
1058
  }
1059
+ await advanceIdentityBaselines(synced, io);
925
1060
 
926
1061
  if (claudeUnderLock) {
927
1062
  const { path: lockPath = CLAUDE_MD_LOCK_PATH, waitMs = RUNNER_LOCK_WAIT_MS } =
@@ -933,7 +1068,7 @@ export async function syncProfileFilesToServer(opts: ProfileSyncOptions): Promis
933
1068
  const claude = await collectProfilePayloads(
934
1069
  ["claude"],
935
1070
  changeSource,
936
- readFileIfExists,
1071
+ io.readFile,
937
1072
  claudeMdPath,
938
1073
  opts.agentId,
939
1074
  );
@@ -135,7 +135,7 @@ import {
135
135
  } from "./credential-wait.ts";
136
136
  import { refreshIdentityIfChanged } from "./identity-refresh.ts";
137
137
  import {
138
- contentSha256,
138
+ bootIdentityBaselines,
139
139
  prependProfileSyncRejectionBanner,
140
140
  resolveClaudeMdPath,
141
141
  syncProfileFilesToServer,
@@ -6779,12 +6779,20 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
6779
6779
  // are skipped, which prevents clobbering DB-side edits made by Lead via
6780
6780
  // update-profile during the running session.
6781
6781
  try {
6782
- const baselines: Record<string, string> = {};
6783
- if (agentSoulMd) baselines.soulMd = contentSha256(agentSoulMd);
6784
- if (agentIdentityMd) baselines.identityMd = contentSha256(agentIdentityMd);
6785
- if (agentToolsMd) baselines.toolsMd = contentSha256(agentToolsMd);
6786
- if (agentHeartbeatMd) baselines.heartbeatMd = contentSha256(agentHeartbeatMd);
6787
- if (agentClaudeMd) baselines.claudeMd = contentSha256(agentClaudeMd);
6782
+ // start-up.sh is not rewritten here when it exists: the entrypoint composed
6783
+ // it from the DB at container start, so its agent-managed section is the base.
6784
+ const setupFile = Bun.file("/workspace/start-up.sh");
6785
+ const baselines = bootIdentityBaselines(
6786
+ {
6787
+ soulMd: agentSoulMd,
6788
+ identityMd: agentIdentityMd,
6789
+ toolsMd: agentToolsMd,
6790
+ heartbeatMd: agentHeartbeatMd,
6791
+ claudeMd: agentClaudeMd,
6792
+ },
6793
+ (await setupFile.exists()) ? await setupFile.text() : undefined,
6794
+ agentId,
6795
+ );
6788
6796
  await writeIdentityBaselines(baselines);
6789
6797
  console.log(`[${role}] Recorded identity file baselines for session-end sync`);
6790
6798
  } catch {
package/src/hooks/hook.ts CHANGED
@@ -11,6 +11,7 @@ import {
11
11
  } from "../be/memory/raters/llm";
12
12
  import { materializeClaudeMd, stopClaudeMd } from "../commands/claude-md-session";
13
13
  import {
14
+ advanceIdentityBaselines,
14
15
  buildIndependentIdentityPayloads,
15
16
  contentSha256,
16
17
  fetchWithSyncTimeout,
@@ -148,7 +149,7 @@ export async function postHookProfileUpdate({
148
149
  body: ProfilePayload["body"];
149
150
  label: string;
150
151
  fetchImpl?: typeof fetch;
151
- }): Promise<void> {
152
+ }): Promise<boolean> {
152
153
  try {
153
154
  const response = await fetchImpl(url, {
154
155
  method: "PUT",
@@ -166,9 +167,11 @@ export async function postHookProfileUpdate({
166
167
  ),
167
168
  );
168
169
  }
170
+ return response.ok;
169
171
  } catch (error) {
170
172
  const message = error instanceof Error ? error.message : String(error);
171
173
  console.error(scrubSecrets(`[hook] ${label} profile sync errored: ${message}`));
174
+ return false;
172
175
  }
173
176
  }
174
177
 
@@ -676,10 +679,10 @@ export async function handleHook(): Promise<void> {
676
679
  /**
677
680
  * Sync SOUL.md and IDENTITY.md content back to the server.
678
681
  *
679
- * When `changeSource` is `"session_sync"` (the Stop-hook default), loads
680
- * baseline hashes written at session start and skips any file whose content
681
- * hasn't changed. This prevents the session-end sync from clobbering DB-side
682
- * edits that Lead made via `update-profile` during the running session.
682
+ * Loads the baseline hashes and skips any file whose content hasn't changed.
683
+ * This prevents clobbering DB-side edits that Lead made via `update-profile`
684
+ * during the running session: at Stop, and on a `"self_edit"` too, where an
685
+ * edit to one file re-reads all four.
683
686
  */
684
687
  const syncIdentityFilesToServer = async (
685
688
  agentId: string,
@@ -687,7 +690,7 @@ export async function handleHook(): Promise<void> {
687
690
  ): Promise<void> => {
688
691
  if (!mcpConfig) return;
689
692
 
690
- const baselines = changeSource === "session_sync" ? await readIdentityBaselines() : null;
693
+ const baselines = await readIdentityBaselines();
691
694
 
692
695
  const updates: Record<string, string> = {};
693
696
 
@@ -747,14 +750,19 @@ export async function handleHook(): Promise<void> {
747
750
 
748
751
  if (Object.keys(updates).length === 0) return;
749
752
 
750
- for (const payload of buildIndependentIdentityPayloads(updates, changeSource)) {
751
- await postHookProfileUpdate({
753
+ const synced: Record<string, string> = {};
754
+ for (const [field, value] of Object.entries(updates)) {
755
+ const [payload] = buildIndependentIdentityPayloads({ [field]: value }, changeSource);
756
+ if (!payload) continue;
757
+ const ok = await postHookProfileUpdate({
752
758
  url: `${getBaseUrl()}/api/agents/${agentId}/profile`,
753
759
  headers: mcpConfig.headers,
754
760
  body: payload.body,
755
761
  label: payload.label,
756
762
  });
763
+ if (ok) synced[field] = value;
757
764
  }
765
+ await advanceIdentityBaselines(synced);
758
766
  };
759
767
 
760
768
  /**
@@ -792,9 +800,11 @@ export async function handleHook(): Promise<void> {
792
800
  warnProfileFileTooLarge(agentId, "setupScript", content.length, "hook");
793
801
  return;
794
802
  }
803
+ // Unchanged since it last matched the DB: not an edit, so don't send it.
804
+ if ((await readIdentityBaselines())?.setupScript === contentSha256(content)) return;
795
805
 
796
806
  try {
797
- await fetch(`${getBaseUrl()}/api/agents/${agentId}/profile`, {
807
+ const resp = await fetch(`${getBaseUrl()}/api/agents/${agentId}/profile`, {
798
808
  method: "PUT",
799
809
  headers: {
800
810
  ...mcpConfig.headers,
@@ -802,6 +812,7 @@ export async function handleHook(): Promise<void> {
802
812
  },
803
813
  body: JSON.stringify({ setupScript: content, changeSource }),
804
814
  });
815
+ if (resp.ok) await advanceIdentityBaselines({ setupScript: content });
805
816
  } catch {
806
817
  /* silently fail */
807
818
  }
package/src/http/index.ts CHANGED
@@ -33,6 +33,7 @@ import { startRuntimeSupervisor, stopRuntimeSupervisor } from "../be/runtime-poo
33
33
  import { startScratchScriptGc, stopScratchScriptGc } from "../be/scripts/retention";
34
34
  import { loadSecretRegistry } from "../be/secret-registry";
35
35
  import { seedLegacyCapabilitiesConfig } from "../be/seed-capabilities";
36
+ import { startTaskAutoTitleListener } from "../be/task-auto-title-listener";
36
37
  import { initApiTelemetry } from "../be/telemetry-enable";
37
38
  import { startWorkerNudgeListener } from "../be/worker-nudge-listener";
38
39
  import { initBitbucket } from "../bitbucket";
@@ -465,6 +466,7 @@ const detachRealtime = attachRealtimeTransport(httpServer);
465
466
  const { attachControllerTransport } = await import("./controllers-connect");
466
467
  const detachControllers = attachControllerTransport(httpServer);
467
468
  const stopWorkerNudges = startWorkerNudgeListener();
469
+ const taskAutoTitles = startTaskAutoTitleListener();
468
470
  const stopRoomDeletion = realtimeBus.subscribe("room:namespace-deleted", (namespace) => {
469
471
  void Promise.resolve(removeNamespaceRooms(String(namespace))).catch((error) =>
470
472
  console.error("[rooms] Deletion failed:", scrubSecrets(String(error))),
@@ -474,6 +476,7 @@ globalState.__closeRealtime = () => {
474
476
  detachRealtime();
475
477
  detachControllers();
476
478
  stopWorkerNudges();
479
+ taskAutoTitles.stop();
477
480
  stopRoomDeletion();
478
481
  };
479
482
  globalState.__httpServer = httpServer;
@@ -877,7 +880,7 @@ httpServer
877
880
  // Delete expired MCP overflow values (10-min tick, off the spill path)
878
881
  startMcpOverflowSweep();
879
882
 
880
- // Start expired-memory garbage collector (1-hour tick, immediate first run)
883
+ // Start expired-memory garbage collector (1-hour tick, first run 2 min after boot)
881
884
  await startMemoryGc();
882
885
 
883
886
  // Start the opt-in DB retention sweep after config hydration. Every key is
@@ -3,8 +3,11 @@
3
3
  *
4
4
  * A list total is a COUNT over the same filters as the page, and on the large
5
5
  * tables behind the sessions and task lists it costs as much as the page
6
- * itself. Page 1 (offset 0) always counts, so a write (a new session, a
7
- * triage move) shows at once where the user looks first. Later pages reuse
6
+ * itself. By default page 1 (offset 0) always counts, so a write (a new
7
+ * session, a triage move) shows at once where the user looks first. A list
8
+ * whose count is expensive and whose total may lag can pass
9
+ * `firstPageTtlMs`: page 1 then reuses a count younger than that, so
10
+ * dashboard tabs that poll the same list share one count. Later pages reuse
8
11
  * that count for 30 s. A cold later page (deep link, expired entry) counts
9
12
  * too, so `total` is always set.
10
13
  */
@@ -13,7 +16,7 @@ export const LIST_TOTAL_TTL_MS = 30_000;
13
16
  const MAX_ENTRIES = 500;
14
17
 
15
18
  interface Entry {
16
- expiresAt: number;
19
+ countedAt: number;
17
20
  total: Promise<number>;
18
21
  }
19
22
 
@@ -34,19 +37,23 @@ function filterKey(value: unknown): string {
34
37
  );
35
38
  }
36
39
 
37
- export function createListTotalCache(ttlMs = LIST_TOTAL_TTL_MS): ListTotalCache {
40
+ export function createListTotalCache(
41
+ ttlMs = LIST_TOTAL_TTL_MS,
42
+ firstPageTtlMs = 0,
43
+ ): ListTotalCache {
38
44
  const entries = new Map<string, Entry>();
39
45
  return {
40
46
  get(filters, offset, compute) {
41
47
  const key = filterKey(filters);
42
48
  const now = Date.now();
43
49
  const hit = entries.get(key);
44
- if (hit && hit.expiresAt > now && (offset ?? 0) > 0) return hit.total;
50
+ const maxAge = (offset ?? 0) > 0 ? ttlMs : Math.min(ttlMs, firstPageTtlMs);
51
+ if (hit && hit.countedAt + maxAge > now) return hit.total;
45
52
  if (hit) entries.delete(key);
46
53
 
47
- // Cache the promise, so a later page that arrives mid-count shares it.
54
+ // Cache the promise, so a request that arrives mid-count shares it.
48
55
  const total = compute();
49
- entries.set(key, { expiresAt: now + ttlMs, total });
56
+ entries.set(key, { countedAt: now, total });
50
57
  total.catch(() => {
51
58
  if (entries.get(key)?.total === total) entries.delete(key);
52
59
  });
@@ -1568,17 +1568,31 @@ const MEMORY_GC_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
1568
1568
  let memoryGcTimer: ReturnType<typeof setInterval> | null = null;
1569
1569
 
1570
1570
  const SEARCH_RETRIEVAL_TTL_DAYS = 90;
1571
+ /** Rows per purge statement. The purge yields the event loop between batches. */
1572
+ const SEARCH_RETRIEVAL_PURGE_BATCH = 2000;
1571
1573
 
1572
- async function purgeStaleSearchRetrievals(): Promise<number> {
1574
+ export async function purgeStaleSearchRetrievals(
1575
+ batchSize = SEARCH_RETRIEVAL_PURGE_BATCH,
1576
+ ): Promise<number> {
1573
1577
  try {
1574
1578
  const cutoff = new Date(
1575
1579
  Date.now() - SEARCH_RETRIEVAL_TTL_DAYS * 24 * 60 * 60 * 1000,
1576
1580
  ).toISOString();
1577
- const result = await getDbClient().run(
1578
- "DELETE FROM memory_retrieval WHERE eventType = 'search' AND retrievedAt < ?",
1579
- [cutoff],
1580
- );
1581
- return result.changes;
1581
+ let purged = 0;
1582
+ for (;;) {
1583
+ // `+eventType` keeps the planner on the retrievedAt range. Without
1584
+ // sqlite_stat1 it prefers the eventType equality, which reads every
1585
+ // search row to test its date, even when nothing is stale.
1586
+ const result = await getDbClient().run(
1587
+ `DELETE FROM memory_retrieval WHERE rowid IN (
1588
+ SELECT rowid FROM memory_retrieval
1589
+ WHERE +eventType = 'search' AND retrievedAt < ? LIMIT ?)`,
1590
+ [cutoff, batchSize],
1591
+ );
1592
+ purged += result.changes;
1593
+ if (result.changes < batchSize) return purged;
1594
+ await new Promise<void>((resolve) => setImmediate(resolve));
1595
+ }
1582
1596
  } catch (err) {
1583
1597
  console.error("[memory-gc] Search retrieval purge failed:", (err as Error).message);
1584
1598
  return 0;
@@ -1602,11 +1616,22 @@ async function runMemoryGcTick(label: "Initial" | "Periodic"): Promise<void> {
1602
1616
  }
1603
1617
  }
1604
1618
 
1605
- export async function startMemoryGc(intervalMs = MEMORY_GC_INTERVAL_MS): Promise<void> {
1619
+ /** The first purge waits this long, so it does not compete with boot work. */
1620
+ const MEMORY_GC_INITIAL_DELAY_MS = 2 * 60 * 1000;
1621
+ let memoryGcInitialTimer: ReturnType<typeof setTimeout> | null = null;
1622
+
1623
+ export async function startMemoryGc(
1624
+ intervalMs = MEMORY_GC_INTERVAL_MS,
1625
+ initialDelayMs = MEMORY_GC_INITIAL_DELAY_MS,
1626
+ ): Promise<void> {
1606
1627
  if (memoryGcTimer) return;
1607
1628
 
1608
- // Run immediately on startup to clear any backlog
1609
- await runMemoryGcTick("Initial");
1629
+ // Clear any backlog soon after startup, but off the boot path.
1630
+ memoryGcInitialTimer = setTimeout(() => {
1631
+ memoryGcInitialTimer = null;
1632
+ void runMemoryGcTick("Initial");
1633
+ }, initialDelayMs);
1634
+ if (typeof memoryGcInitialTimer?.unref === "function") memoryGcInitialTimer.unref();
1610
1635
 
1611
1636
  memoryGcTimer = setInterval(() => {
1612
1637
  void runMemoryGcTick("Periodic");
@@ -1615,6 +1640,10 @@ export async function startMemoryGc(intervalMs = MEMORY_GC_INTERVAL_MS): Promise
1615
1640
  }
1616
1641
 
1617
1642
  export function stopMemoryGc(): void {
1643
+ if (memoryGcInitialTimer) {
1644
+ clearTimeout(memoryGcInitialTimer);
1645
+ memoryGcInitialTimer = null;
1646
+ }
1618
1647
  if (memoryGcTimer) {
1619
1648
  clearInterval(memoryGcTimer);
1620
1649
  memoryGcTimer = null;