@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
@@ -179,7 +179,7 @@ describe("Stop-hook profile sync visibility", () => {
179
179
  throw new Error("network unavailable");
180
180
  }) as typeof fetch,
181
181
  }),
182
- ).resolves.toBeUndefined();
182
+ ).resolves.toBe(false);
183
183
  expect(String(errorSpy.mock.calls[0]?.[0])).toContain(
184
184
  "claudeMd profile sync errored: network unavailable",
185
185
  );
@@ -3,6 +3,7 @@ import {
3
3
  DEFAULT_MODEL,
4
4
  type ResolveCredentialOptions,
5
5
  resolveCredential,
6
+ resolveCredentials,
6
7
  } from "../../utils/internal-ai/credentials.js";
7
8
 
8
9
  /**
@@ -331,3 +332,64 @@ describe("resolveCredential", () => {
331
332
  }
332
333
  });
333
334
  });
335
+
336
+ describe("resolveCredentials", () => {
337
+ test("returns every credential in precedence order", async () => {
338
+ const creds = await resolveCredentials(
339
+ makeOpts({
340
+ env: {
341
+ OPENROUTER_API_KEY: "example-or",
342
+ ANTHROPIC_API_KEY: "example-ant",
343
+ OPENAI_API_KEY: "example-oai",
344
+ CLAUDE_CODE_OAUTH_TOKEN: "example-claude",
345
+ },
346
+ }),
347
+ );
348
+ expect(creds.map((cred) => cred.kind)).toEqual([
349
+ "openrouter",
350
+ "anthropic",
351
+ "openai",
352
+ "claude-cli",
353
+ ]);
354
+ });
355
+
356
+ test("`kinds` restricts the lookup and `limit` stops early", async () => {
357
+ const env = { OPENROUTER_API_KEY: "example-or", OPENAI_API_KEY: "example-oai" };
358
+ const onlyOpenai = await resolveCredentials({ ...makeOpts({ env }), kinds: ["openai"] });
359
+ expect(onlyOpenai.map((cred) => cred.kind)).toEqual(["openai"]);
360
+
361
+ const first = await resolveCredentials({ ...makeOpts({ env }), limit: 1 });
362
+ expect(first.map((cred) => cred.kind)).toEqual(["openrouter"]);
363
+
364
+ const none = await resolveCredentials({ ...makeOpts({ env }), kinds: ["anthropic"] });
365
+ expect(none).toEqual([]);
366
+ });
367
+
368
+ test("the codex probe runs only when its kind is requested", async () => {
369
+ let probed = 0;
370
+ const base = {
371
+ env: { OPENROUTER_API_KEY: "example-or" },
372
+ apiUrl: "http://localhost:3013",
373
+ apiKey: "k",
374
+ _getValidCodexOAuth: async () => {
375
+ probed++;
376
+ return { access: "a", refresh: "r", expires: Date.now() + 60_000, accountId: "acct" };
377
+ },
378
+ _getOAuthApiKey: async () => ({ apiKey: "codex-access" }),
379
+ };
380
+ const envOnly = await resolveCredentials({
381
+ ...makeOpts(base),
382
+ kinds: ["openrouter", "openai"],
383
+ });
384
+ expect(envOnly.map((cred) => cred.kind)).toEqual(["openrouter"]);
385
+ expect(probed).toBe(0);
386
+
387
+ const codex = await resolveCredentials({ ...makeOpts(base), kinds: ["openai-codex"] });
388
+ expect(codex.map((cred) => cred.kind)).toEqual(["openai-codex"]);
389
+ expect(probed).toBe(1);
390
+
391
+ const all = await resolveCredentials(makeOpts(base));
392
+ expect(all.map((cred) => cred.kind)).toEqual(["openrouter", "openai-codex"]);
393
+ expect(probed).toBe(2);
394
+ });
395
+ });
@@ -0,0 +1,283 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { completeStructuredWithModel } from "../../utils/internal-ai/complete-structured.js";
3
+ import type {
4
+ CredentialKind,
5
+ ResolvedCredential,
6
+ resolveCredentials,
7
+ } from "../../utils/internal-ai/credentials.js";
8
+ import {
9
+ buildTaskTitlePlan,
10
+ generateTaskTitle,
11
+ kindForModelString,
12
+ normalizeTaskTitle,
13
+ prepareTaskTextForTitle,
14
+ TASK_TITLE_MAX_CHARS,
15
+ TASK_TITLE_MAX_TASK_CHARS,
16
+ TASK_TITLE_MODEL_CANDIDATES,
17
+ TASK_TITLE_MODEL_ENV,
18
+ } from "../../utils/internal-ai/task-title.js";
19
+
20
+ const LONG_TASK =
21
+ "Investigate why the nightly Playwright suite times out on the sessions page after the sidebar redesign and fix the flake.";
22
+
23
+ function credential(kind: CredentialKind): ResolvedCredential {
24
+ return kind === "claude-cli"
25
+ ? { kind, modelDefault: "haiku" }
26
+ : { kind, apiKey: `key-${kind}`, modelDefault: `${kind}/default` };
27
+ }
28
+
29
+ /** `resolveCredentials` stub that offers exactly `kinds` and records every lookup. */
30
+ function resolver(kinds: CredentialKind[], calls: CredentialKind[][] = []) {
31
+ const resolve: typeof resolveCredentials = async (opts) => {
32
+ const wanted = [...(opts?.kinds ?? [])];
33
+ calls.push(wanted);
34
+ return wanted.filter((kind) => kinds.includes(kind)).map(credential);
35
+ };
36
+ return { resolve, calls };
37
+ }
38
+
39
+ type CompleteFn = typeof completeStructuredWithModel;
40
+
41
+ /** `completeStructuredWithModel` stub keyed by model string; absent = failure (null). */
42
+ function completer(answers: Record<string, string | Error>, models: string[] = []) {
43
+ const complete: CompleteFn = async (opts) => {
44
+ const model = opts._credentialOverride?.modelDefault ?? "<none>";
45
+ models.push(model);
46
+ const answer = answers[model];
47
+ if (answer instanceof Error) throw answer;
48
+ if (answer === undefined) return null;
49
+ return { data: { title: answer }, model };
50
+ };
51
+ return { complete, models };
52
+ }
53
+
54
+ describe("normalizeTaskTitle", () => {
55
+ test("collapses whitespace, strips quotes, prefix, markdown and trailing punctuation", () => {
56
+ expect(normalizeTaskTitle(' "Fix sessions sidebar flake." ')).toBe(
57
+ "Fix sessions sidebar flake",
58
+ );
59
+ expect(normalizeTaskTitle("Title: **Rotate Slack token**")).toBe("Rotate Slack token");
60
+ expect(normalizeTaskTitle("“Add retry to poller”…")).toBe("Add retry to poller");
61
+ expect(normalizeTaskTitle("`Bump bun to 1.4`")).toBe("Bump bun to 1.4");
62
+ });
63
+
64
+ test("keeps question marks and inner punctuation", () => {
65
+ expect(normalizeTaskTitle("Why does the poller stall?")).toBe("Why does the poller stall?");
66
+ expect(normalizeTaskTitle("Migrate agent-fs 0.16.3 pins")).toBe("Migrate agent-fs 0.16.3 pins");
67
+ });
68
+
69
+ test("cuts long titles at a word boundary under the limit", () => {
70
+ const words = Array.from({ length: 40 }, (_, i) => `word${i}`).join(" ");
71
+ const title = normalizeTaskTitle(words);
72
+ expect(title.length).toBeLessThanOrEqual(TASK_TITLE_MAX_CHARS);
73
+ expect(title.endsWith("word")).toBe(false);
74
+ expect(words.startsWith(title)).toBe(true);
75
+ });
76
+
77
+ test("returns an empty string when nothing usable remains", () => {
78
+ expect(normalizeTaskTitle('""')).toBe("");
79
+ expect(normalizeTaskTitle(" ... ")).toBe("");
80
+ });
81
+ });
82
+
83
+ describe("prepareTaskTextForTitle", () => {
84
+ test("drops the sibling-awareness block and collapses blank lines", () => {
85
+ const task = `<sibling_tasks_in_progress>\n- other\n</sibling_tasks_in_progress>\n\n\n\nFix the login bug\n\n\n\nin the auth module`;
86
+ expect(prepareTaskTextForTitle(task)).toBe("Fix the login bug\n\nin the auth module");
87
+ });
88
+
89
+ test("keeps a prompt without the block and truncates long prompts", () => {
90
+ expect(prepareTaskTextForTitle(" hello\r\nworld ")).toBe("hello\nworld");
91
+ const long = "x".repeat(TASK_TITLE_MAX_TASK_CHARS + 500);
92
+ const prepared = prepareTaskTextForTitle(long);
93
+ expect(prepared.length).toBe(TASK_TITLE_MAX_TASK_CHARS + 2);
94
+ expect(prepared.endsWith(" …")).toBe(true);
95
+ });
96
+ });
97
+
98
+ describe("buildTaskTitlePlan", () => {
99
+ test("default plan is the candidate table", () => {
100
+ expect(buildTaskTitlePlan({})).toBe(TASK_TITLE_MODEL_CANDIDATES);
101
+ });
102
+
103
+ test("a pinned model moves its provider first and the model to the front", () => {
104
+ const plan = buildTaskTitlePlan({ [TASK_TITLE_MODEL_ENV]: "openai/gpt-5-nano" });
105
+ expect(plan[0]).toEqual({ kind: "openai", models: ["openai/gpt-5-nano", "openai/gpt-6-luna"] });
106
+ expect(plan.slice(1).map((group) => group.kind)).toEqual([
107
+ "openrouter",
108
+ "anthropic",
109
+ "openai-codex",
110
+ "claude-cli",
111
+ ]);
112
+ });
113
+
114
+ test("a pinned model outside the table is still tried first", () => {
115
+ const plan = buildTaskTitlePlan({ [TASK_TITLE_MODEL_ENV]: "anthropic/claude-haiku-4-5" });
116
+ expect(plan[0]?.models).toEqual(["anthropic/claude-haiku-4-5", "anthropic/claude-haiku-5-5"]);
117
+ });
118
+
119
+ test("a bare alias pins the claude CLI; an unsupported provider is ignored", () => {
120
+ expect(buildTaskTitlePlan({ [TASK_TITLE_MODEL_ENV]: "sonnet" })[0]).toEqual({
121
+ kind: "claude-cli",
122
+ models: ["sonnet", "haiku"],
123
+ });
124
+ expect(buildTaskTitlePlan({ [TASK_TITLE_MODEL_ENV]: "google/gemini-2.5-flash-lite" })).toBe(
125
+ TASK_TITLE_MODEL_CANDIDATES,
126
+ );
127
+ });
128
+
129
+ test("kindForModelString maps provider prefixes", () => {
130
+ expect(kindForModelString("openrouter/deepseek/deepseek-v4.1-flash")).toBe("openrouter");
131
+ expect(kindForModelString("openai-codex/gpt-6-luna")).toBe("openai-codex");
132
+ expect(kindForModelString("haiku")).toBe("claude-cli");
133
+ expect(kindForModelString("mistral/ministral-3b")).toBeNull();
134
+ });
135
+ });
136
+
137
+ describe("generateTaskTitle", () => {
138
+ test("short prompts are skipped before any credential lookup", async () => {
139
+ const { resolve, calls } = resolver(["openrouter"]);
140
+ const { complete, models } = completer({});
141
+ const result = await generateTaskTitle({
142
+ task: "fix the login bug",
143
+ env: {},
144
+ _resolveCredentials: resolve,
145
+ _completeStructured: complete,
146
+ });
147
+ expect(result).toEqual({ ok: false, reason: "short-task" });
148
+ expect(calls).toEqual([]);
149
+ expect(models).toEqual([]);
150
+ });
151
+
152
+ test("uses the first provider with a credential and its cheapest model", async () => {
153
+ const { resolve } = resolver(["anthropic", "openai"]);
154
+ const { complete, models } = completer({
155
+ "anthropic/claude-haiku-5-5": "Fix nightly Playwright timeout on sessions page",
156
+ });
157
+ const result = await generateTaskTitle({
158
+ task: LONG_TASK,
159
+ env: {},
160
+ _resolveCredentials: resolve,
161
+ _completeStructured: complete,
162
+ });
163
+ expect(result).toEqual({
164
+ ok: true,
165
+ title: "Fix nightly Playwright timeout on sessions page",
166
+ model: "anthropic/claude-haiku-5-5",
167
+ kind: "anthropic",
168
+ });
169
+ expect(models).toEqual(["anthropic/claude-haiku-5-5"]);
170
+ });
171
+
172
+ test("falls through models, then providers, on null results and thrown errors", async () => {
173
+ const { resolve } = resolver(["openrouter", "openai"]);
174
+ const { complete, models } = completer({
175
+ "openrouter/deepseek/deepseek-v4.1-flash": new Error("429 rate limited"),
176
+ // claude-haiku-5.5 absent → null
177
+ "openrouter/openai/gpt-6-luna": " ",
178
+ "openai/gpt-6-luna": "Sessions page Playwright flake",
179
+ });
180
+ const result = await generateTaskTitle({
181
+ task: LONG_TASK,
182
+ env: {},
183
+ _resolveCredentials: resolve,
184
+ _completeStructured: complete,
185
+ });
186
+ expect(result).toEqual({
187
+ ok: true,
188
+ title: "Sessions page Playwright flake",
189
+ model: "openai/gpt-6-luna",
190
+ kind: "openai",
191
+ });
192
+ expect(models).toEqual([
193
+ "openrouter/deepseek/deepseek-v4.1-flash",
194
+ "openrouter/anthropic/claude-haiku-5.5",
195
+ "openrouter/openai/gpt-6-luna",
196
+ "openai/gpt-6-luna",
197
+ ]);
198
+ });
199
+
200
+ test("resolves one credential kind at a time and stops before the codex probe when an earlier provider answers", async () => {
201
+ const { resolve, calls } = resolver(["openrouter", "openai-codex"]);
202
+ const { complete } = completer({ "openrouter/deepseek/deepseek-v4.1-flash": "A title" });
203
+ await generateTaskTitle({
204
+ task: LONG_TASK,
205
+ env: {},
206
+ apiUrl: "http://127.0.0.1:1",
207
+ apiKey: "k",
208
+ _resolveCredentials: resolve,
209
+ _completeStructured: complete,
210
+ });
211
+ expect(calls).toEqual([["openrouter"]]);
212
+ });
213
+
214
+ test("reports no-credentials when nothing resolves and all-models-failed otherwise", async () => {
215
+ const none = resolver([]);
216
+ const silent = completer({});
217
+ expect(
218
+ await generateTaskTitle({
219
+ task: LONG_TASK,
220
+ env: {},
221
+ _resolveCredentials: none.resolve,
222
+ _completeStructured: silent.complete,
223
+ }),
224
+ ).toEqual({ ok: false, reason: "no-credentials" });
225
+ // Every kind was asked exactly once, in precedence order.
226
+ expect(none.calls.map((call) => call[0])).toEqual(
227
+ TASK_TITLE_MODEL_CANDIDATES.map((group) => group.kind),
228
+ );
229
+
230
+ const some = resolver(["claude-cli"]);
231
+ expect(
232
+ await generateTaskTitle({
233
+ task: LONG_TASK,
234
+ env: {},
235
+ _resolveCredentials: some.resolve,
236
+ _completeStructured: silent.complete,
237
+ }),
238
+ ).toEqual({ ok: false, reason: "all-models-failed" });
239
+ expect(silent.models).toEqual(["haiku"]);
240
+ });
241
+
242
+ test("the pinned model runs first and the credential override carries it", async () => {
243
+ const { resolve } = resolver(["openrouter", "openai"]);
244
+ let seenOverride: ResolvedCredential | undefined;
245
+ const complete: CompleteFn = async (opts) => {
246
+ seenOverride = opts._credentialOverride;
247
+ return { data: { title: "Pinned" }, model: "openai/gpt-5-nano" };
248
+ };
249
+ const result = await generateTaskTitle({
250
+ task: LONG_TASK,
251
+ env: { [TASK_TITLE_MODEL_ENV]: "openai/gpt-5-nano" },
252
+ _resolveCredentials: resolve,
253
+ _completeStructured: complete,
254
+ });
255
+ expect(result).toEqual({
256
+ ok: true,
257
+ title: "Pinned",
258
+ model: "openai/gpt-5-nano",
259
+ kind: "openai",
260
+ });
261
+ expect(seenOverride).toEqual({
262
+ kind: "openai",
263
+ apiKey: "key-openai",
264
+ modelDefault: "openai/gpt-5-nano",
265
+ });
266
+ });
267
+
268
+ test("an aborted signal stops the walk", async () => {
269
+ const { resolve } = resolver(["openrouter"]);
270
+ const { complete, models } = completer({ "openrouter/deepseek/deepseek-v4.1-flash": "T" });
271
+ const controller = new AbortController();
272
+ controller.abort();
273
+ const result = await generateTaskTitle({
274
+ task: LONG_TASK,
275
+ env: {},
276
+ signal: controller.signal,
277
+ _resolveCredentials: resolve,
278
+ _completeStructured: complete,
279
+ });
280
+ expect(result).toEqual({ ok: false, reason: "no-credentials" });
281
+ expect(models).toEqual([]);
282
+ });
283
+ });
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, test } from "bun:test";
2
- import { createListTotalCache } from "../http/list-total-cache";
2
+ import { createListTotalCache, LIST_TOTAL_TTL_MS } from "../http/list-total-cache";
3
3
 
4
4
  function counter(values: number[]) {
5
5
  let calls = 0;
@@ -10,7 +10,7 @@ function counter(values: number[]) {
10
10
  }
11
11
 
12
12
  describe("list total cache", () => {
13
- test("page 1 always counts; later pages reuse its count", async () => {
13
+ test("page 1 always counts by default; later pages reuse its count", async () => {
14
14
  const cache = createListTotalCache();
15
15
  const count = counter([10, 11, 12]);
16
16
 
@@ -24,6 +24,31 @@ describe("list total cache", () => {
24
24
  expect(count.calls()).toBe(2);
25
25
  });
26
26
 
27
+ test("page 1 polls within the first-page TTL share one count", async () => {
28
+ const cache = createListTotalCache(LIST_TOTAL_TTL_MS, 50);
29
+ const count = counter([10, 11]);
30
+ expect(await cache.get({ q: "a" }, 0, count.compute)).toBe(10);
31
+ expect(await cache.get({ q: "a" }, 0, count.compute)).toBe(10);
32
+ expect(count.calls()).toBe(1);
33
+ await Bun.sleep(60);
34
+ expect(await cache.get({ q: "a" }, 0, count.compute)).toBe(11);
35
+ expect(count.calls()).toBe(2);
36
+ });
37
+
38
+ test("with a first-page TTL, concurrent page 1 requests share an in-flight count", async () => {
39
+ const cache = createListTotalCache(LIST_TOTAL_TTL_MS, 1000);
40
+ let calls = 0;
41
+ const compute = async () => {
42
+ calls++;
43
+ await Bun.sleep(5);
44
+ return 4;
45
+ };
46
+ expect(await Promise.all([cache.get({}, 0, compute), cache.get({}, 0, compute)])).toEqual([
47
+ 4, 4,
48
+ ]);
49
+ expect(calls).toBe(1);
50
+ });
51
+
27
52
  test("a cold later page counts, so total is always set", async () => {
28
53
  const cache = createListTotalCache();
29
54
  const count = counter([7]);
@@ -2,8 +2,8 @@ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
2
  import { unlink } from "node:fs/promises";
3
3
  import { closeDb, createAgent, getDb, getDbClient, initDb } from "../be/db";
4
4
  import {
5
- FTS_DELETE_EXTRA_SQL,
6
- FTS_MISSING_IDS_SQL,
5
+ FTS_SCAN_BATCH_SQL,
6
+ MEMORY_ID_BATCH_SQL,
7
7
  SqliteMemoryStore,
8
8
  } from "../be/memory/providers/sqlite-store";
9
9
 
@@ -62,16 +62,18 @@ describe("memory_fts boot backfill", () => {
62
62
  // Positive control: the old anti-join scans memory_fts once per memory.
63
63
  expect(planOf(QUADRATIC_BACKFILL_SQL).join(" | ")).toContain("CORRELATED");
64
64
 
65
- for (const sql of [FTS_MISSING_IDS_SQL, FTS_DELETE_EXTRA_SQL]) {
66
- const plan = planOf(sql).join(" | ");
67
- expect(plan).not.toContain("CORRELATED");
68
- // Exactly one pass over memory_fts per statement.
69
- expect(plan.match(/SCAN memory_fts/g)?.length).toBe(1);
70
- }
65
+ // Each batch seeks a rowid range of one table. The orphan check is a
66
+ // lookup on the agent_memory primary key, never a scan of memory_fts.
67
+ const scan = planOf(FTS_SCAN_BATCH_SQL.replace(/\?/g, "0")).join(" | ");
68
+ expect(scan).toMatch(/SCAN memory_fts VIRTUAL TABLE INDEX \d+:>/);
69
+ expect(scan.match(/memory_fts/g)?.length).toBe(1);
70
+ expect(scan).toContain("SEARCH m USING COVERING INDEX sqlite_autoindex_agent_memory_1 (id=?)");
71
+ const ids = planOf(MEMORY_ID_BATCH_SQL.replace(/\?/g, "0")).join(" | ");
72
+ expect(ids).toContain("SEARCH agent_memory USING INTEGER PRIMARY KEY (rowid>?)");
71
73
  });
72
74
 
73
75
  test("a new store backfills missing rows across batches and drops orphans", async () => {
74
- const total = 1050; // crosses two 500-row batch boundaries
76
+ const total = 1050; // crosses two 500-row insert batches and one 1000-row scan batch
75
77
  const seed = await store.store({
76
78
  agentId,
77
79
  scope: "agent",
@@ -0,0 +1,80 @@
1
+ import { afterAll, beforeAll, describe, expect, spyOn, test } from "bun:test";
2
+ import { unlink } from "node:fs/promises";
3
+ import { closeDb, getDb, getDbClient, initDb } from "../be/db";
4
+ import { purgeStaleSearchRetrievals, startMemoryGc, stopMemoryGc } from "../http/memory";
5
+
6
+ const TEST_DB_PATH = "./test-memory-retrieval-purge.sqlite";
7
+ const DAY = 24 * 60 * 60 * 1000;
8
+
9
+ async function removeDbFiles(): Promise<void> {
10
+ for (const suffix of ["", "-wal", "-shm"])
11
+ await unlink(`${TEST_DB_PATH}${suffix}`).catch(() => undefined);
12
+ }
13
+
14
+ async function insertRetrieval(id: string, eventType: string, ageDays: number): Promise<void> {
15
+ await getDbClient().run(
16
+ `INSERT INTO memory_retrieval (id, agentId, memoryId, retrievedAt, eventType)
17
+ VALUES (?, 'agent-1', 'memory-1', ?, ?)`,
18
+ [id, new Date(Date.now() - ageDays * DAY).toISOString(), eventType],
19
+ );
20
+ }
21
+
22
+ describe("purgeStaleSearchRetrievals", () => {
23
+ beforeAll(async () => {
24
+ await removeDbFiles();
25
+ initDb(TEST_DB_PATH);
26
+ });
27
+
28
+ afterAll(async () => {
29
+ closeDb();
30
+ await removeDbFiles();
31
+ });
32
+
33
+ test("deletes stale search rows in batches and keeps fresh and get rows", async () => {
34
+ for (let i = 0; i < 5; i++) await insertRetrieval(`stale-search-${i}`, "search", 120);
35
+ await insertRetrieval("fresh-search", "search", 10);
36
+ await insertRetrieval("stale-get", "get", 120);
37
+
38
+ const run = spyOn(getDbClient(), "run");
39
+ let purged: number;
40
+ let calls: (typeof run.mock.calls)[number][];
41
+ try {
42
+ purged = await purgeStaleSearchRetrievals(2);
43
+ calls = [...run.mock.calls];
44
+ } finally {
45
+ run.mockRestore();
46
+ }
47
+ expect(purged).toBe(5);
48
+ // Batches of 2: 2 + 2 + 1, and the short batch ends the loop.
49
+ expect(calls).toHaveLength(3);
50
+
51
+ const left = await getDbClient().query<{ id: string }>(
52
+ "SELECT id FROM memory_retrieval ORDER BY id",
53
+ );
54
+ expect(left.map((row) => row.id)).toEqual(["fresh-search", "stale-get"]);
55
+
56
+ // Without sqlite_stat1 the plan must stay on the retrievedAt range, not
57
+ // read every search row through the eventType index.
58
+ const [sql, params] = calls[0]!;
59
+ const plan = getDb()
60
+ .query<{ detail: string }, any[]>(`EXPLAIN QUERY PLAN ${sql}`)
61
+ .all(...((params ?? []) as any[]))
62
+ .map((row) => row.detail)
63
+ .join("\n");
64
+ expect(plan).toContain("USING INDEX idx_memret_retrieved_at (retrievedAt<?)");
65
+ expect(plan).not.toContain("idx_memret_event_type");
66
+ });
67
+ });
68
+
69
+ describe("startMemoryGc", () => {
70
+ test("defers the first purge off the boot path", async () => {
71
+ const timeout = spyOn(globalThis, "setTimeout");
72
+ try {
73
+ await startMemoryGc(60 * 60 * 1000, 120_000);
74
+ expect(timeout.mock.calls.some(([, ms]) => ms === 120_000)).toBe(true);
75
+ } finally {
76
+ stopMemoryGc();
77
+ timeout.mockRestore();
78
+ }
79
+ });
80
+ });
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Runs the merge gate's "Compute per-domain change flags" step, verbatim from
3
+ * .github/workflows/merge-gate.yml, against throwaway git repos.
4
+ *
5
+ * Regression: a rename from a covered path to an excluded one (src/x.ts ->
6
+ * docs/x.ts) used to list only the destination, so the E2E Contract job (and
7
+ * every other src/-keyed flag) skipped a PR that removed a server input.
8
+ */
9
+ import { afterAll, describe, expect, test } from "bun:test";
10
+ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
11
+ import { tmpdir } from "node:os";
12
+ import { dirname, join } from "node:path";
13
+ import { CHILD_PROCESS_TEST_BUDGET_MS, expectChildOk, runChild } from "./test-proc";
14
+
15
+ type Workflow = {
16
+ jobs: Record<string, { steps: Array<{ id?: string; run?: string }> }>;
17
+ };
18
+
19
+ // The GitHub expression the step uses for the diff base; the test substitutes a local branch.
20
+ const BASE_REF_EXPR = ["$", "{{ steps.base.outputs.ref }}"].join("");
21
+ const WORKFLOW_PATH = join(import.meta.dir, "../../.github/workflows/merge-gate.yml");
22
+
23
+ async function loadFlagStep(): Promise<string> {
24
+ const wf = Bun.YAML.parse(await Bun.file(WORKFLOW_PATH).text()) as Workflow;
25
+ const step = wf.jobs["detect-changes"]?.steps.find((s) => s.id === "check");
26
+ if (!step?.run) throw new Error("detect-changes step `check` not found in merge-gate.yml");
27
+ return step.run.replaceAll(BASE_REF_EXPR, "base");
28
+ }
29
+
30
+ // Isolate from the host's git config (a global diff.renames=false would make
31
+ // the positive control pass for the wrong reason).
32
+ const GIT_ENV = {
33
+ PATH: process.env.PATH,
34
+ HOME: tmpdir(),
35
+ GIT_CONFIG_NOSYSTEM: "1",
36
+ GIT_CONFIG_GLOBAL: "/dev/null",
37
+ GIT_AUTHOR_NAME: "t",
38
+ GIT_AUTHOR_EMAIL: "t@example.com",
39
+ GIT_COMMITTER_NAME: "t",
40
+ GIT_COMMITTER_EMAIL: "t@example.com",
41
+ };
42
+
43
+ const dirs: string[] = [];
44
+ afterAll(async () => {
45
+ await Promise.all(dirs.map((d) => rm(d, { recursive: true, force: true })));
46
+ });
47
+
48
+ async function git(cwd: string, ...args: string[]) {
49
+ return expectChildOk(await runChild(["git", ...args], { cwd, env: GIT_ENV }), `git ${args[0]}`);
50
+ }
51
+
52
+ // Large enough that git's default rename detection pairs the two sides.
53
+ const BODY = Array.from({ length: 40 }, (_, i) => `export const v${i} = ${i};`).join("\n");
54
+
55
+ /** Builds a repo whose `base` branch holds `files`, applies `change` on HEAD, and returns the step's flags. */
56
+ async function flagsFor(
57
+ files: string[],
58
+ change: (repo: string) => Promise<void>,
59
+ script?: string,
60
+ ): Promise<Record<string, string>> {
61
+ const repo = await mkdtemp(join(tmpdir(), "merge-gate-flags-"));
62
+ dirs.push(repo);
63
+ await git(repo, "init", "-q", "-b", "base");
64
+ for (const f of files) {
65
+ await mkdir(dirname(join(repo, f)), { recursive: true });
66
+ await writeFile(join(repo, f), `${BODY}\n// ${f}\n`);
67
+ }
68
+ await git(repo, "add", "-A");
69
+ await git(repo, "commit", "-qm", "base");
70
+ await git(repo, "checkout", "-qb", "pr");
71
+ await change(repo);
72
+ await git(repo, "add", "-A");
73
+ await git(repo, "commit", "-qm", "pr");
74
+
75
+ const out = join(repo, ".gh-output");
76
+ const res = expectChildOk(
77
+ await runChild(["bash", "-c", script ?? (await loadFlagStep())], {
78
+ cwd: repo,
79
+ env: { ...GIT_ENV, GITHUB_OUTPUT: out },
80
+ }),
81
+ "flag step",
82
+ );
83
+ expect(res.stdout).toContain("Changed files:");
84
+ const flags: Record<string, string> = {};
85
+ for (const line of (await Bun.file(out).text()).split("\n")) {
86
+ const [k, v] = line.split("=");
87
+ if (k && v) flags[k] = v;
88
+ }
89
+ return flags;
90
+ }
91
+
92
+ const mv = (from: string, to: string) => async (repo: string) => {
93
+ await mkdir(dirname(join(repo, to)), { recursive: true });
94
+ await git(repo, "mv", from, to);
95
+ };
96
+
97
+ describe("merge-gate detect-changes flags", () => {
98
+ test(
99
+ "renaming a server file out of src/ still runs E2E",
100
+ async () => {
101
+ const flags = await flagsFor(["src/critical.ts"], mv("src/critical.ts", "docs/critical.ts"));
102
+ expect(flags.e2e).toBe("true");
103
+ expect(flags.test).toBe("true");
104
+ expect(flags.lint).toBe("true");
105
+ },
106
+ CHILD_PROCESS_TEST_BUDGET_MS,
107
+ );
108
+
109
+ test(
110
+ "renaming a docs file into src/ runs E2E",
111
+ async () => {
112
+ const flags = await flagsFor(["docs/notes.ts"], mv("docs/notes.ts", "src/notes.ts"));
113
+ expect(flags.e2e).toBe("true");
114
+ },
115
+ CHILD_PROCESS_TEST_BUDGET_MS,
116
+ );
117
+
118
+ test(
119
+ "a docs-only change skips E2E, tests, and lint",
120
+ async () => {
121
+ const flags = await flagsFor(["src/critical.ts", "docs/readme.md"], async (repo) => {
122
+ await writeFile(join(repo, "docs/readme.md"), "edited\n");
123
+ });
124
+ expect(flags.e2e).toBe("false");
125
+ expect(flags.test).toBe("false");
126
+ expect(flags.lint).toBe("false");
127
+ },
128
+ CHILD_PROCESS_TEST_BUDGET_MS,
129
+ );
130
+
131
+ test(
132
+ "control: with rename detection on, the outbound move hides from E2E",
133
+ async () => {
134
+ const step = await loadFlagStep();
135
+ expect(step).toContain("git diff --no-renames --name-only");
136
+ const flags = await flagsFor(
137
+ ["src/critical.ts"],
138
+ mv("src/critical.ts", "docs/critical.ts"),
139
+ step.replace("git diff --no-renames --name-only", "git diff --name-only"),
140
+ );
141
+ expect(flags.e2e).toBe("false");
142
+ },
143
+ CHILD_PROCESS_TEST_BUDGET_MS,
144
+ );
145
+ });