@vellumai/assistant 0.8.3 → 0.8.5

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 (665) hide show
  1. package/ARCHITECTURE.md +2 -2
  2. package/docker-entrypoint.sh +0 -1
  3. package/docs/browser-use-architecture-phase2.md +1 -1
  4. package/knip.json +2 -1
  5. package/node_modules/@vellumai/gateway-client/src/types.ts +2 -0
  6. package/openapi.yaml +1492 -100
  7. package/package.json +1 -1
  8. package/src/__tests__/agent-loop-exit-reason.test.ts +4 -5
  9. package/src/__tests__/agent-loop-override-profile.test.ts +1 -1
  10. package/src/__tests__/agent-loop.test.ts +88 -3
  11. package/src/__tests__/anthropic-provider.test.ts +302 -33
  12. package/src/__tests__/approval-cascade.test.ts +1 -1
  13. package/src/__tests__/assistant-event-hub-self-exclusion.test.ts +293 -0
  14. package/src/__tests__/assistant-feature-flags-integration.test.ts +3 -3
  15. package/src/__tests__/audit-log-rotation.test.ts +70 -16
  16. package/src/__tests__/background-workers-disk-pressure.test.ts +4 -3
  17. package/src/__tests__/btw-routes.test.ts +2 -3
  18. package/src/__tests__/call-controller.test.ts +0 -1
  19. package/src/__tests__/cancel-resolves-conversation-key.test.ts +1 -1
  20. package/src/__tests__/channel-delivery-store.test.ts +193 -0
  21. package/src/__tests__/channel-guardian.test.ts +3 -3
  22. package/src/__tests__/channel-reply-delivery.test.ts +284 -5
  23. package/src/__tests__/channel-retry-sweep.test.ts +274 -1
  24. package/src/__tests__/checker.test.ts +6 -15
  25. package/src/__tests__/compaction-events.test.ts +2 -1
  26. package/src/__tests__/compactor-call-site-logging.test.ts +214 -0
  27. package/src/__tests__/compactor-preserved-tail-count.test.ts +110 -0
  28. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +5 -11
  29. package/src/__tests__/computer-use-tools.test.ts +2 -4
  30. package/src/__tests__/config-watcher.test.ts +1 -1
  31. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +0 -1
  32. package/src/__tests__/context-token-estimator.test.ts +91 -1
  33. package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
  34. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +1 -1
  35. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +55 -4
  36. package/src/__tests__/conversation-agent-loop-overflow.test.ts +228 -8
  37. package/src/__tests__/conversation-agent-loop.test.ts +188 -129
  38. package/src/__tests__/conversation-app-control-instantiation.test.ts +2 -5
  39. package/src/__tests__/conversation-app-control-lifecycle.test.ts +1 -1
  40. package/src/__tests__/conversation-clean-command.test.ts +137 -0
  41. package/src/__tests__/conversation-clear-safety.test.ts +25 -25
  42. package/src/__tests__/conversation-confirmation-signals.test.ts +1 -1
  43. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +1 -1
  44. package/src/__tests__/conversation-disk-view-integration.test.ts +2 -2
  45. package/src/__tests__/conversation-error.test.ts +31 -0
  46. package/src/__tests__/conversation-fork-crud.test.ts +324 -0
  47. package/src/__tests__/conversation-lifecycle.test.ts +53 -12
  48. package/src/__tests__/conversation-load-history-repair.test.ts +1 -1
  49. package/src/__tests__/conversation-load-history-stripped.test.ts +279 -0
  50. package/src/__tests__/conversation-pairing.test.ts +2 -2
  51. package/src/__tests__/conversation-process-callsite.test.ts +1 -1
  52. package/src/__tests__/conversation-provider-retry-repair.test.ts +2 -1
  53. package/src/__tests__/conversation-queue.test.ts +1 -1
  54. package/src/__tests__/conversation-routes-disk-view.test.ts +109 -0
  55. package/src/__tests__/conversation-routes-slash-commands.test.ts +35 -0
  56. package/src/__tests__/conversation-runtime-assembly.test.ts +264 -81
  57. package/src/__tests__/conversation-seed-composer.test.ts +66 -4
  58. package/src/__tests__/conversation-skill-tools.test.ts +2 -5
  59. package/src/__tests__/conversation-slash-commands.test.ts +36 -8
  60. package/src/__tests__/conversation-slash-queue.test.ts +1 -1
  61. package/src/__tests__/conversation-slash-unknown.test.ts +1 -1
  62. package/src/__tests__/conversation-speed-override.test.ts +1 -1
  63. package/src/__tests__/conversation-store.test.ts +1 -1
  64. package/src/__tests__/conversation-surfaces-task-progress.test.ts +220 -0
  65. package/src/__tests__/conversation-sync-tags.test.ts +99 -32
  66. package/src/__tests__/conversation-workspace-cache-state.test.ts +2 -1
  67. package/src/__tests__/conversation-workspace-injection.test.ts +5 -1
  68. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -1
  69. package/src/__tests__/credential-execution-feature-gates.test.ts +9 -7
  70. package/src/__tests__/credential-execution-tools.test.ts +6 -6
  71. package/src/__tests__/credential-security-invariants.test.ts +7 -0
  72. package/src/__tests__/credential-vault-unit.test.ts +2 -2
  73. package/src/__tests__/cu-unified-flow.test.ts +10 -1
  74. package/src/__tests__/dm-backfill.test.ts +64 -0
  75. package/src/__tests__/dm-persistence.test.ts +33 -0
  76. package/src/__tests__/document-find-replace.test.ts +501 -0
  77. package/src/__tests__/dynamic-page-surface.test.ts +2 -2
  78. package/src/__tests__/email-html-renderer.test.ts +12 -0
  79. package/src/__tests__/first-greeting.test.ts +23 -2
  80. package/src/__tests__/gateway-flag-listener.test.ts +237 -0
  81. package/src/__tests__/gemini-provider.test.ts +78 -0
  82. package/src/__tests__/guardian-dispatch.test.ts +0 -1
  83. package/src/__tests__/guardian-outbound-http.test.ts +7 -5
  84. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +1 -1
  85. package/src/__tests__/headless-browser-navigate.test.ts +172 -0
  86. package/src/__tests__/heartbeat-disk-pressure.test.ts +4 -0
  87. package/src/__tests__/heartbeat-service.test.ts +4 -0
  88. package/src/__tests__/host-bash-proxy.test.ts +6 -0
  89. package/src/__tests__/host-browser-proxy.test.ts +10 -0
  90. package/src/__tests__/host-cu-proxy.test.ts +8 -1
  91. package/src/__tests__/host-file-proxy.test.ts +8 -1
  92. package/src/__tests__/host-shell-tool.test.ts +1 -1
  93. package/src/__tests__/host-transfer-proxy.test.ts +8 -1
  94. package/src/__tests__/identity-routes.test.ts +57 -0
  95. package/src/__tests__/inbound-slack-persistence.test.ts +3 -0
  96. package/src/__tests__/init-feature-flag-overrides.test.ts +5 -6
  97. package/src/__tests__/injector-chain.test.ts +2 -0
  98. package/src/__tests__/injector-document-comments.test.ts +378 -0
  99. package/src/__tests__/injector-pkb-v2-silenced.test.ts +4 -25
  100. package/src/__tests__/list-messages-attachments.test.ts +21 -17
  101. package/src/__tests__/list-messages-hidden-metadata.test.ts +217 -0
  102. package/src/__tests__/list-messages-page-latest.test.ts +130 -14
  103. package/src/__tests__/list-messages-tool-merge.test.ts +77 -17
  104. package/src/__tests__/llm-context-normalization.test.ts +0 -2
  105. package/src/__tests__/llm-request-log-call-site.test.ts +136 -0
  106. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +26 -0
  107. package/src/__tests__/llm-resolver.test.ts +161 -9
  108. package/src/__tests__/llm-usage-store.test.ts +66 -0
  109. package/src/__tests__/log-export-routes.test.ts +99 -2
  110. package/src/__tests__/logger.test.ts +89 -0
  111. package/src/__tests__/mcp-abort-signal.test.ts +2 -2
  112. package/src/__tests__/media-generate-image.test.ts +31 -0
  113. package/src/__tests__/memory-v2-static-injector.test.ts +7 -7
  114. package/src/__tests__/message-queue-steer.test.ts +114 -0
  115. package/src/__tests__/model-intents.test.ts +2 -4
  116. package/src/__tests__/notification-guardian-path.test.ts +0 -1
  117. package/src/__tests__/onboarding-template-contract.test.ts +1 -1
  118. package/src/__tests__/openai-provider.test.ts +151 -0
  119. package/src/__tests__/openai-responses-provider.test.ts +118 -16
  120. package/src/__tests__/outbound-slack-persistence.test.ts +187 -20
  121. package/src/__tests__/pending-interactions-resolved-event.test.ts +189 -0
  122. package/src/__tests__/platform-bash-auto-approve.test.ts +2 -2
  123. package/src/__tests__/platform.test.ts +2 -5
  124. package/src/__tests__/plugin-api-tool-definition.test.ts +92 -0
  125. package/src/__tests__/plugin-bootstrap.test.ts +2 -2
  126. package/src/__tests__/plugin-source-watcher.test.ts +302 -0
  127. package/src/__tests__/plugin-tool-contribution.test.ts +13 -6
  128. package/src/__tests__/plugin-types.test.ts +3 -2
  129. package/src/__tests__/prechat-onboarding-contract.test.ts +131 -98
  130. package/src/__tests__/pricing.test.ts +12 -0
  131. package/src/__tests__/process-message-background-slack.test.ts +1 -51
  132. package/src/__tests__/process-message-display-content.test.ts +21 -16
  133. package/src/__tests__/prune-jobs-changes-parser.test.ts +61 -0
  134. package/src/__tests__/registry.test.ts +2 -8
  135. package/src/__tests__/require-fresh-approval.test.ts +2 -2
  136. package/src/__tests__/runtime-events-sse-bilingual.test.ts +154 -0
  137. package/src/__tests__/server-history-render.test.ts +83 -4
  138. package/src/__tests__/shell-tool-proxy-mode.test.ts +1 -1
  139. package/src/__tests__/skill-feature-flags.test.ts +2 -2
  140. package/src/__tests__/skill-projection-feature-flag.test.ts +4 -7
  141. package/src/__tests__/skill-projection.benchmark.test.ts +2 -6
  142. package/src/__tests__/skill-tool-factory.test.ts +1 -1
  143. package/src/__tests__/steer-tool-repair.test.ts +249 -0
  144. package/src/__tests__/subagent-notify-parent.test.ts +1 -1
  145. package/src/__tests__/suggestion-routes.test.ts +1 -0
  146. package/src/__tests__/sync-message-contract.test.ts +59 -0
  147. package/src/__tests__/system-prompt.test.ts +161 -124
  148. package/src/__tests__/terminal-tools.test.ts +12 -2
  149. package/src/__tests__/thinking-block-replay.test.ts +113 -0
  150. package/src/__tests__/thread-backfill.test.ts +370 -22
  151. package/src/__tests__/tool-approval-handler.test.ts +1 -5
  152. package/src/__tests__/tool-execute-pipeline.test.ts +2 -2
  153. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +2 -5
  154. package/src/__tests__/tool-executor-lifecycle-events.test.ts +15 -5
  155. package/src/__tests__/tool-executor.test.ts +89 -53
  156. package/src/__tests__/tool-grant-request-escalation.test.ts +1 -6
  157. package/src/__tests__/tool-result-metadata-plumbing.test.ts +167 -0
  158. package/src/__tests__/trusted-contact-approval-notifier.test.ts +0 -1
  159. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +1 -6
  160. package/src/__tests__/trusted-contact-multichannel.test.ts +0 -1
  161. package/src/__tests__/twilio-routes.test.ts +1 -1
  162. package/src/__tests__/ui-file-upload-surface.test.ts +2 -2
  163. package/src/__tests__/usage-routes.test.ts +3 -0
  164. package/src/__tests__/verification-control-plane-policy.test.ts +2 -2
  165. package/src/__tests__/web-fetch.test.ts +2 -2
  166. package/src/__tests__/workspace-git-service.test.ts +94 -10
  167. package/src/__tests__/workspace-migration-088-deprecate-background-conversation-override.test.ts +158 -0
  168. package/src/__tests__/workspace-migration-089-move-memory-tree-out-of-v3.test.ts +86 -0
  169. package/src/acp/__tests__/prepare-agent-env.test.ts +146 -0
  170. package/src/acp/prepare-agent-env.ts +78 -0
  171. package/src/acp/session-manager.ts +1 -1
  172. package/src/agent/attachments.ts +1 -0
  173. package/src/agent/loop.ts +65 -20
  174. package/src/api/README.md +5 -0
  175. package/src/api/index.ts +4 -0
  176. package/src/api/package.json +10 -0
  177. package/src/background-wake/background-wake-routes.test.ts +233 -0
  178. package/src/background-wake/next-wake.test.ts +289 -0
  179. package/src/background-wake/next-wake.ts +172 -0
  180. package/src/background-wake/runtime-registry.ts +24 -0
  181. package/src/browser/operations.ts +15 -0
  182. package/src/cli/commands/__tests__/browser.test.ts +23 -5
  183. package/src/cli/commands/__tests__/conversations-slack.test.ts +572 -0
  184. package/src/cli/commands/__tests__/domain-register.test.ts +110 -0
  185. package/src/cli/commands/__tests__/domain-status.test.ts +33 -33
  186. package/src/cli/commands/__tests__/inference-send.test.ts +108 -5
  187. package/src/cli/commands/__tests__/memory-v2-compare-render.test.ts +98 -0
  188. package/src/cli/commands/__tests__/memory-v2.test.ts +10 -12
  189. package/src/cli/commands/__tests__/memory-v3-render.test.ts +340 -0
  190. package/src/cli/commands/browser.ts +247 -0
  191. package/src/cli/commands/conversations.ts +128 -1
  192. package/src/cli/commands/domain.ts +91 -41
  193. package/src/cli/commands/inference-providers.ts +147 -1
  194. package/src/cli/commands/inference.ts +93 -40
  195. package/src/cli/commands/memory-v2-compare-render.ts +115 -0
  196. package/src/cli/commands/memory-v2.ts +483 -0
  197. package/src/cli/commands/memory-v3-render.ts +344 -0
  198. package/src/cli/commands/memory-v3.ts +316 -0
  199. package/src/cli/commands/notifications.ts +24 -2
  200. package/src/cli/program.ts +2 -0
  201. package/src/cli/utils/conversation-id.ts +17 -5
  202. package/src/config/assistant-feature-flags.ts +21 -9
  203. package/src/config/bundled-skills/app-builder/SKILL.md +2 -2
  204. package/src/config/bundled-skills/document-editor/SKILL.md +124 -0
  205. package/src/config/bundled-skills/document-editor/TOOLS.json +258 -0
  206. package/src/config/bundled-skills/document-editor/tools/comment-list.ts +12 -0
  207. package/src/config/bundled-skills/document-editor/tools/comment-reply.ts +12 -0
  208. package/src/config/bundled-skills/document-editor/tools/comment-resolve.ts +12 -0
  209. package/src/config/bundled-skills/document-editor/tools/document-find.ts +12 -0
  210. package/src/config/bundled-skills/document-editor/tools/document-open.ts +12 -0
  211. package/src/config/bundled-skills/document-editor/tools/document-replace-text.ts +12 -0
  212. package/src/config/bundled-skills/image-studio/SKILL.md +4 -0
  213. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +2 -2
  214. package/src/config/bundled-skills/media-processing/SKILL.md +8 -0
  215. package/src/config/bundled-skills/media-processing/tools/ingest-media.ts +13 -8
  216. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +10 -3
  217. package/src/config/bundled-skills/phone-calls/references/TRANSCRIPTS.md +16 -14
  218. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +7 -2
  219. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +7 -2
  220. package/src/config/bundled-skills/schedule/SKILL.md +8 -0
  221. package/src/config/bundled-tool-registry.ts +24 -12
  222. package/src/config/call-site-defaults.ts +20 -0
  223. package/src/config/feature-flag-registry.json +115 -3
  224. package/src/config/llm-resolver.ts +16 -2
  225. package/src/config/schemas/__tests__/memory-v2.test.ts +217 -1
  226. package/src/config/schemas/call-site-catalog.ts +35 -0
  227. package/src/config/schemas/llm.ts +14 -0
  228. package/src/config/schemas/memory-v2.ts +294 -1
  229. package/src/config/schemas/memory.ts +2 -1
  230. package/src/context/compactor.ts +60 -1
  231. package/src/context/token-estimator.ts +47 -4
  232. package/src/context/window-manager.ts +25 -0
  233. package/src/conversations/__tests__/message-consolidation.test.ts +350 -0
  234. package/src/conversations/message-consolidation.ts +404 -0
  235. package/src/credential-health/credential-health-service.ts +34 -19
  236. package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +1 -1
  237. package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -6
  238. package/src/daemon/__tests__/meet-manifest-loader.test.ts +1 -1
  239. package/src/daemon/__tests__/native-web-search-metadata.test.ts +357 -0
  240. package/src/daemon/__tests__/web-search-status-text.test.ts +287 -0
  241. package/src/daemon/conversation-agent-loop-handlers.ts +155 -36
  242. package/src/daemon/conversation-agent-loop.ts +307 -88
  243. package/src/daemon/conversation-error.ts +31 -1
  244. package/src/daemon/conversation-lifecycle.ts +149 -118
  245. package/src/daemon/conversation-messaging.ts +3 -0
  246. package/src/daemon/conversation-process.ts +273 -0
  247. package/src/daemon/conversation-queue-manager.ts +14 -0
  248. package/src/daemon/conversation-runtime-assembly.ts +145 -84
  249. package/src/daemon/conversation-slash.ts +37 -5
  250. package/src/daemon/conversation-surfaces.ts +45 -2
  251. package/src/daemon/conversation-tool-setup.ts +70 -3
  252. package/src/daemon/conversation-usage.ts +2 -0
  253. package/src/daemon/conversation.ts +54 -32
  254. package/src/daemon/disk-pressure-guard.ts +14 -2
  255. package/src/daemon/first-greeting.ts +10 -0
  256. package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +498 -0
  257. package/src/daemon/handlers/config-a2a.ts +160 -0
  258. package/src/daemon/handlers/config-model.test.ts +2 -0
  259. package/src/daemon/handlers/conversations.ts +90 -3
  260. package/src/daemon/handlers/shared.ts +92 -29
  261. package/src/daemon/host-bash-proxy.ts +1 -1
  262. package/src/daemon/host-browser-proxy.ts +5 -5
  263. package/src/daemon/host-cu-proxy.ts +5 -5
  264. package/src/daemon/host-file-proxy.ts +5 -5
  265. package/src/daemon/host-proxy-base.ts +4 -4
  266. package/src/daemon/host-transfer-proxy.ts +11 -11
  267. package/src/daemon/lifecycle.ts +40 -23
  268. package/src/daemon/meet-manifest-loader.ts +1 -7
  269. package/src/daemon/message-protocol.ts +4 -0
  270. package/src/daemon/message-types/conversations.ts +14 -9
  271. package/src/daemon/message-types/document-comments.ts +50 -0
  272. package/src/daemon/message-types/home.ts +1 -13
  273. package/src/daemon/message-types/messages.ts +66 -7
  274. package/src/daemon/message-types/surfaces.ts +3 -1
  275. package/src/daemon/message-types/sync.ts +14 -0
  276. package/src/daemon/message-types/web-activity.ts +57 -0
  277. package/src/daemon/plugin-source-watcher.ts +135 -3
  278. package/src/daemon/process-message.ts +69 -12
  279. package/src/daemon/shutdown-handlers.ts +24 -5
  280. package/src/daemon/switch-inference-profile-tool.ts +52 -0
  281. package/src/daemon/tool-setup-types.ts +13 -0
  282. package/src/daemon/trust-context.ts +6 -0
  283. package/src/documents/document-comments-store.test.ts +338 -0
  284. package/src/documents/document-comments-store.ts +237 -0
  285. package/src/documents/document-store.ts +202 -0
  286. package/src/events/relationship-state-updated.ts +25 -0
  287. package/src/heartbeat/__tests__/heartbeat-service.test.ts +1 -2
  288. package/src/heartbeat/heartbeat-service.ts +1 -0
  289. package/src/home/__tests__/suggested-prompts.test.ts +33 -2
  290. package/src/home/feed-types.ts +6 -1
  291. package/src/home/home-content-refresh.ts +52 -0
  292. package/src/home/home-greeting-cache.ts +69 -0
  293. package/src/home/home-greeting.ts +85 -0
  294. package/src/home/suggested-prompts.ts +168 -9
  295. package/src/ipc/gateway-flag-listener.ts +123 -0
  296. package/src/ipc/skill-routes/registries.ts +8 -12
  297. package/src/memory/__tests__/db-async-query.test.ts +165 -0
  298. package/src/memory/__tests__/db-maintenance.test.ts +115 -0
  299. package/src/memory/__tests__/jobs-store-enqueue-gate.test.ts +241 -0
  300. package/src/memory/__tests__/jobs-store-job-classes.test.ts +28 -1
  301. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +135 -2
  302. package/src/memory/__tests__/memory-retrospective-job.test.ts +327 -6
  303. package/src/memory/auto-analysis-enqueue.ts +5 -1
  304. package/src/memory/conversation-crud.ts +191 -100
  305. package/src/memory/conversation-starters-cadence.ts +3 -1
  306. package/src/memory/conversation-title-service.ts +19 -3
  307. package/src/memory/db-async-query.ts +214 -0
  308. package/src/memory/db-init.ts +26 -0
  309. package/src/memory/db-maintenance.ts +30 -21
  310. package/src/memory/delivery-crud.ts +41 -0
  311. package/src/memory/delivery-status.ts +141 -15
  312. package/src/memory/external-conversation-store.ts +32 -1
  313. package/src/memory/graph/bootstrap.ts +8 -1
  314. package/src/memory/graph/capability-seed.ts +7 -3
  315. package/src/memory/graph/conversation-graph-memory.ts +100 -17
  316. package/src/memory/graph/extraction.ts +1 -5
  317. package/src/memory/graph/graph-search.ts +7 -1
  318. package/src/memory/indexer.ts +28 -18
  319. package/src/memory/job-handlers/cleanup.ts +76 -18
  320. package/src/memory/job-handlers/conversation-starters.ts +1 -4
  321. package/src/memory/jobs/embed-pkb-file.ts +6 -1
  322. package/src/memory/jobs-store.ts +14 -0
  323. package/src/memory/jobs-worker.ts +68 -15
  324. package/src/memory/llm-request-log-source-clickhouse.ts +42 -2
  325. package/src/memory/llm-request-log-source-local.ts +7 -0
  326. package/src/memory/llm-request-log-source.ts +9 -2
  327. package/src/memory/llm-request-log-store.ts +43 -1
  328. package/src/memory/llm-usage-store.ts +24 -0
  329. package/src/memory/memory-retrospective-constants.ts +28 -0
  330. package/src/memory/memory-retrospective-enqueue.ts +11 -3
  331. package/src/memory/memory-retrospective-job.ts +413 -18
  332. package/src/memory/memory-retrospective-startup-cleanup.ts +3 -3
  333. package/src/memory/memory-v2-activation-log-store.ts +41 -14
  334. package/src/memory/migrations/100-core-tables.ts +1 -0
  335. package/src/memory/migrations/109-external-conversation-bindings.ts +1 -0
  336. package/src/memory/migrations/253-conversation-last-notified-profile.ts +15 -0
  337. package/src/memory/migrations/253-document-comments.ts +47 -0
  338. package/src/memory/migrations/254-external-conversation-binding-chat-name.ts +43 -0
  339. package/src/memory/migrations/255-channel-inbound-delivery-attempts.ts +24 -0
  340. package/src/memory/migrations/256-memory-v2-injection-events.ts +113 -0
  341. package/src/memory/migrations/257-strip-base-url-non-openai-compatible.ts +22 -0
  342. package/src/memory/migrations/258-onboarding-events-prior-assistants.ts +13 -0
  343. package/src/memory/migrations/259-conversation-cleaned-at.ts +33 -0
  344. package/src/memory/migrations/260-rename-cleaned-at.ts +44 -0
  345. package/src/memory/migrations/261-llm-usage-add-raw-usage.ts +36 -0
  346. package/src/memory/migrations/262-memory-v3-coactivation.ts +57 -0
  347. package/src/memory/migrations/263-memory-v3-auto-edges.ts +50 -0
  348. package/src/memory/migrations/264-llm-request-log-call-site.ts +29 -0
  349. package/src/memory/migrations/index.ts +34 -0
  350. package/src/memory/migrations/registry.ts +58 -0
  351. package/src/memory/onboarding-events-store.ts +7 -0
  352. package/src/memory/schema/calls.ts +1 -0
  353. package/src/memory/schema/conversations.ts +3 -0
  354. package/src/memory/schema/infrastructure.ts +22 -0
  355. package/src/memory/tool-usage-store.ts +36 -8
  356. package/src/memory/v2/__tests__/consolidation-job.test.ts +1 -0
  357. package/src/memory/v2/__tests__/harness-compare.test.ts +186 -0
  358. package/src/memory/v2/__tests__/harness-metrics.test.ts +74 -0
  359. package/src/memory/v2/__tests__/harness-oracle.test.ts +257 -0
  360. package/src/memory/v2/__tests__/harness-replay-input.test.ts +225 -0
  361. package/src/memory/v2/__tests__/harness-runner.test.ts +109 -0
  362. package/src/memory/v2/__tests__/injection-events.test.ts +318 -0
  363. package/src/memory/v2/__tests__/injection.test.ts +158 -112
  364. package/src/memory/v2/__tests__/page-index.test.ts +365 -1
  365. package/src/memory/v2/__tests__/qdrant.test.ts +36 -0
  366. package/src/memory/v2/__tests__/router.test.ts +660 -4
  367. package/src/memory/v2/consolidation-job.ts +14 -0
  368. package/src/memory/v2/harness/compare.ts +57 -0
  369. package/src/memory/v2/harness/metrics.ts +124 -0
  370. package/src/memory/v2/harness/oracle.ts +145 -0
  371. package/src/memory/v2/harness/replay-input.ts +224 -0
  372. package/src/memory/v2/harness/retriever.ts +74 -0
  373. package/src/memory/v2/harness/router-retriever.ts +43 -0
  374. package/src/memory/v2/harness/runner.ts +106 -0
  375. package/src/memory/v2/harness/trace.ts +58 -0
  376. package/src/memory/v2/injection-events.ts +101 -0
  377. package/src/memory/v2/injection.ts +42 -25
  378. package/src/memory/v2/page-index.ts +209 -7
  379. package/src/memory/v2/page-store.ts +18 -0
  380. package/src/memory/v2/prompts/router.ts +26 -1
  381. package/src/memory/v2/qdrant.ts +14 -2
  382. package/src/memory/v2/router.ts +369 -62
  383. package/src/memory/v3/__tests__/coactivation-store.test.ts +422 -0
  384. package/src/memory/v3/__tests__/consolidation-job.test.ts +468 -0
  385. package/src/memory/v3/__tests__/edge-learning-job.test.ts +324 -0
  386. package/src/memory/v3/__tests__/edges.test.ts +563 -0
  387. package/src/memory/v3/__tests__/filter.test.ts +512 -0
  388. package/src/memory/v3/__tests__/gate.test.ts +574 -0
  389. package/src/memory/v3/__tests__/index-composition.test.ts +233 -0
  390. package/src/memory/v3/__tests__/loop.test.ts +530 -0
  391. package/src/memory/v3/__tests__/retriever.test.ts +226 -0
  392. package/src/memory/v3/__tests__/scouts.test.ts +440 -0
  393. package/src/memory/v3/__tests__/shadow-middleware.test.ts +312 -0
  394. package/src/memory/v3/__tests__/system-prompts.test.ts +154 -0
  395. package/src/memory/v3/__tests__/traversal.test.ts +469 -0
  396. package/src/memory/v3/__tests__/tree-index.test.ts +280 -0
  397. package/src/memory/v3/__tests__/tree-store.test.ts +529 -0
  398. package/src/memory/v3/__tests__/tree-walk.test.ts +707 -0
  399. package/src/memory/v3/__tests__/validate.test.ts +245 -0
  400. package/src/memory/v3/auto-edges.ts +223 -0
  401. package/src/memory/v3/coactivation-store.ts +124 -0
  402. package/src/memory/v3/consolidation-job.ts +323 -0
  403. package/src/memory/v3/edge-learning-job.ts +160 -0
  404. package/src/memory/v3/edges.ts +249 -0
  405. package/src/memory/v3/filter.ts +281 -0
  406. package/src/memory/v3/gate.ts +334 -0
  407. package/src/memory/v3/index-composition.ts +113 -0
  408. package/src/memory/v3/llm-capture.ts +46 -0
  409. package/src/memory/v3/loop.ts +382 -0
  410. package/src/memory/v3/maintenance.ts +144 -0
  411. package/src/memory/v3/prompt-context.ts +33 -0
  412. package/src/memory/v3/prompts/consolidation.ts +458 -0
  413. package/src/memory/v3/prompts/system-prompts.ts +196 -0
  414. package/src/memory/v3/retriever.ts +33 -0
  415. package/src/memory/v3/scouts.ts +420 -0
  416. package/src/memory/v3/shadow-middleware.ts +305 -0
  417. package/src/memory/v3/traversal.ts +206 -0
  418. package/src/memory/v3/tree-index.ts +237 -0
  419. package/src/memory/v3/tree-store.ts +394 -0
  420. package/src/memory/v3/tree-walk.ts +351 -0
  421. package/src/memory/v3/types.ts +65 -0
  422. package/src/memory/v3/validate.ts +300 -0
  423. package/src/messaging/providers/index.ts +7 -1
  424. package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +329 -3
  425. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +34 -1
  426. package/src/messaging/providers/slack/adapter.ts +178 -25
  427. package/src/messaging/providers/slack/api.test.ts +54 -0
  428. package/src/messaging/providers/slack/api.ts +119 -3
  429. package/src/messaging/providers/slack/client.ts +12 -0
  430. package/src/messaging/providers/slack/deep-link.ts +20 -1
  431. package/src/messaging/providers/slack/message-metadata.test.ts +48 -0
  432. package/src/messaging/providers/slack/message-metadata.ts +156 -0
  433. package/src/messaging/providers/slack/render-transcript.test.ts +107 -75
  434. package/src/messaging/providers/slack/render-transcript.ts +176 -49
  435. package/src/messaging/providers/slack/send.test.ts +77 -0
  436. package/src/messaging/providers/slack/send.ts +8 -2
  437. package/src/messaging/providers/slack/types.ts +14 -0
  438. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +4 -1
  439. package/src/notifications/__tests__/home-feed-side-effect.test.ts +116 -54
  440. package/src/notifications/adapters/macos.ts +18 -1
  441. package/src/notifications/adapters/platform.ts +1 -1
  442. package/src/notifications/conversation-seed-composer.ts +14 -2
  443. package/src/notifications/decision-engine.ts +1 -4
  444. package/src/notifications/deferred-emit.ts +135 -0
  445. package/src/notifications/emit-signal.ts +38 -50
  446. package/src/notifications/home-feed-side-effect.ts +60 -30
  447. package/src/oauth/connect-orchestrator.ts +3 -0
  448. package/src/oauth/credential-token-resolver.ts +2 -0
  449. package/src/oauth/manual-token-connection.ts +19 -0
  450. package/src/oauth/oauth-store.ts +12 -0
  451. package/src/oauth/seed-providers.ts +22 -0
  452. package/src/permissions/prompter.ts +8 -5
  453. package/src/permissions/question-prompter.ts +5 -2
  454. package/src/permissions/secret-prompter.ts +6 -3
  455. package/src/plugin-api/index.ts +4 -0
  456. package/src/plugin-api/types.ts +7 -33
  457. package/src/plugins/defaults/index.ts +6 -0
  458. package/src/plugins/defaults/injectors.ts +100 -20
  459. package/src/plugins/external-plugin-loader.ts +5 -68
  460. package/src/plugins/types.ts +11 -16
  461. package/src/proactive-artifact/aux-message-injector.ts +17 -4
  462. package/src/prompts/__tests__/system-prompt.test.ts +46 -2
  463. package/src/prompts/__tests__/task-progress-hint-section.test.ts +3 -9
  464. package/src/prompts/normalize-onboarding.ts +40 -0
  465. package/src/prompts/persona-resolver.ts +36 -21
  466. package/src/prompts/sections.ts +69 -19
  467. package/src/prompts/system-prompt.ts +118 -216
  468. package/src/prompts/template-detection.ts +37 -0
  469. package/src/prompts/templates/BOOTSTRAP-CONTENT-AUTOMATION.md +141 -0
  470. package/src/prompts/templates/BOOTSTRAP.md +10 -2
  471. package/src/prompts/templates/VOICE.md +3 -0
  472. package/src/prompts/templates/system-sections.ts +281 -9
  473. package/src/providers/__tests__/connection-model-compat.test.ts +234 -0
  474. package/src/providers/__tests__/retry-callsite.test.ts +85 -5
  475. package/src/providers/anthropic/client.ts +159 -66
  476. package/src/providers/call-site-routing.ts +14 -2
  477. package/src/providers/connection-model-compat.ts +38 -0
  478. package/src/providers/connection-resolution.ts +16 -2
  479. package/src/providers/fireworks/client.ts +20 -2
  480. package/src/providers/gemini/client.ts +49 -6
  481. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +342 -0
  482. package/src/providers/inference/__tests__/base-url-security.test.ts +189 -0
  483. package/src/providers/inference/__tests__/codex-token-refresh.test.ts +254 -0
  484. package/src/providers/inference/adapter-factory.ts +18 -1
  485. package/src/providers/inference/auth.ts +3 -3
  486. package/src/providers/inference/codex-token-refresh.ts +128 -0
  487. package/src/providers/inference/resolve-auth.ts +49 -6
  488. package/src/providers/minimax/client.ts +106 -0
  489. package/src/providers/model-catalog.ts +91 -1
  490. package/src/providers/model-intents.ts +1 -1
  491. package/src/providers/openai/chat-completions-provider.ts +63 -23
  492. package/src/providers/openai/codex-models.ts +18 -0
  493. package/src/providers/openai/responses-provider.ts +86 -23
  494. package/src/providers/openrouter/client.ts +5 -1
  495. package/src/providers/provider-send-message.ts +7 -1
  496. package/src/providers/retry.ts +34 -3
  497. package/src/providers/thinking-config.ts +26 -1
  498. package/src/providers/types.ts +25 -0
  499. package/src/providers/usage-tracking.ts +2 -0
  500. package/src/runtime/AGENTS.md +2 -2
  501. package/src/runtime/__tests__/agent-wake.test.ts +214 -0
  502. package/src/runtime/__tests__/background-job-runner.test.ts +128 -0
  503. package/src/runtime/agent-wake.ts +152 -56
  504. package/src/runtime/assistant-event-hub.ts +76 -6
  505. package/src/runtime/auth/route-policy.ts +43 -3
  506. package/src/runtime/background-job-runner.ts +26 -0
  507. package/src/runtime/btw-sidechain.ts +0 -6
  508. package/src/runtime/channel-reply-delivery.ts +182 -47
  509. package/src/runtime/channel-retry-sweep.ts +141 -16
  510. package/src/runtime/http-types.ts +7 -6
  511. package/src/runtime/migrations/vbundle-builder.ts +10 -3
  512. package/src/runtime/pending-interactions.ts +50 -8
  513. package/src/runtime/routes/__tests__/content-source-routes.test.ts +162 -0
  514. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +161 -1
  515. package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +14 -0
  516. package/src/runtime/routes/__tests__/memory-v2-simulate-route.test.ts +290 -0
  517. package/src/runtime/routes/__tests__/plugins-routes.test.ts +512 -0
  518. package/src/runtime/routes/__tests__/sanity-routes.test.ts +280 -0
  519. package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +266 -0
  520. package/src/runtime/routes/acp-routes.test.ts +255 -6
  521. package/src/runtime/routes/acp-routes.ts +8 -1
  522. package/src/runtime/routes/approval-routes.ts +4 -1
  523. package/src/runtime/routes/avatar-routes.ts +10 -10
  524. package/src/runtime/routes/background-wake-routes.ts +188 -0
  525. package/src/runtime/routes/browser-tabs-routes.ts +200 -0
  526. package/src/runtime/routes/btw-routes.ts +0 -6
  527. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +246 -0
  528. package/src/runtime/routes/content-source-routes.ts +78 -0
  529. package/src/runtime/routes/conversation-cli-routes.ts +147 -2
  530. package/src/runtime/routes/conversation-list-routes.ts +12 -4
  531. package/src/runtime/routes/conversation-management-routes.ts +77 -20
  532. package/src/runtime/routes/conversation-query-routes.ts +196 -31
  533. package/src/runtime/routes/conversation-routes.ts +472 -425
  534. package/src/runtime/routes/conversation-starter-routes.ts +6 -3
  535. package/src/runtime/routes/disk-pressure-routes.ts +1 -1
  536. package/src/runtime/routes/document-comments-routes.ts +287 -0
  537. package/src/runtime/routes/documents-routes.ts +33 -0
  538. package/src/runtime/routes/domain-routes.ts +60 -10
  539. package/src/runtime/routes/email-routes.ts +5 -2
  540. package/src/runtime/routes/events-routes.ts +54 -10
  541. package/src/runtime/routes/group-routes.ts +24 -8
  542. package/src/runtime/routes/home-feed-routes.ts +6 -3
  543. package/src/runtime/routes/host-app-control-routes.ts +1 -1
  544. package/src/runtime/routes/host-browser-routes.ts +17 -2
  545. package/src/runtime/routes/host-cu-routes.ts +2 -2
  546. package/src/runtime/routes/identity-routes.ts +21 -0
  547. package/src/runtime/routes/inbound-message-handler.ts +288 -58
  548. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +96 -3
  549. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +365 -6
  550. package/src/runtime/routes/inbound-stages/background-dispatch.ts +283 -82
  551. package/src/runtime/routes/index.ts +20 -4
  552. package/src/runtime/routes/inference-profile-session-handler.ts +22 -12
  553. package/src/runtime/routes/inference-profile-session-routes.ts +7 -1
  554. package/src/runtime/routes/inference-provider-connection-routes.ts +63 -7
  555. package/src/runtime/routes/integrations/a2a.ts +60 -1
  556. package/src/runtime/routes/llm-call-sites-routes.ts +32 -5
  557. package/src/runtime/routes/log-export-routes.ts +39 -0
  558. package/src/runtime/routes/memory-item-routes.ts +8 -3
  559. package/src/runtime/routes/memory-v2-routes.ts +427 -0
  560. package/src/runtime/routes/memory-v3-routes.ts +316 -0
  561. package/src/runtime/routes/migration-routes.ts +21 -24
  562. package/src/runtime/routes/notification-routes.ts +19 -2
  563. package/src/runtime/routes/plugins-routes.ts +337 -0
  564. package/src/runtime/routes/question-routes.ts +4 -1
  565. package/src/runtime/routes/rename-conversation-routes.ts +6 -2
  566. package/src/runtime/routes/sanity-routes.ts +159 -0
  567. package/src/runtime/routes/secret-routes.ts +25 -5
  568. package/src/runtime/routes/settings-routes.ts +12 -11
  569. package/src/runtime/routes/slack-channel-routes.ts +188 -0
  570. package/src/runtime/routes/workspace-routes.ts +25 -10
  571. package/src/runtime/services/conversation-serializer.ts +30 -4
  572. package/src/runtime/sync/resource-sync-events.ts +106 -38
  573. package/src/runtime/sync/sync-publisher.test.ts +49 -0
  574. package/src/runtime/sync/sync-publisher.ts +2 -1
  575. package/src/runtime/verification-outbound-actions.ts +73 -1
  576. package/src/schedule/integration-status.ts +3 -1
  577. package/src/security/__tests__/oauth2-device-code.test.ts +479 -0
  578. package/src/security/oauth2-device-code.ts +307 -0
  579. package/src/security/oauth2.ts +26 -9
  580. package/src/security/secure-keys.ts +5 -0
  581. package/src/skills/catalog-install.ts +6 -2
  582. package/src/telemetry/types.ts +12 -0
  583. package/src/telemetry/usage-telemetry-reporter.test.ts +48 -0
  584. package/src/telemetry/usage-telemetry-reporter.ts +1 -0
  585. package/src/tools/acp/spawn.test.ts +119 -0
  586. package/src/tools/acp/spawn.ts +15 -2
  587. package/src/tools/apps/definitions.ts +2 -8
  588. package/src/tools/ask-question/ask-question-tool.test.ts +3 -3
  589. package/src/tools/ask-question/ask-question-tool.ts +38 -45
  590. package/src/tools/browser/__tests__/pinned-tabs.test.ts +150 -0
  591. package/src/tools/browser/browser-execution.ts +106 -0
  592. package/src/tools/browser/cdp-client/__tests__/browser-tabs-factory.test.ts +402 -0
  593. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +28 -0
  594. package/src/tools/browser/cdp-client/__tests__/types.test.ts +4 -0
  595. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +22 -0
  596. package/src/tools/browser/cdp-client/extension-cdp-client.ts +42 -2
  597. package/src/tools/browser/cdp-client/factory.ts +171 -4
  598. package/src/tools/browser/cdp-client/local-cdp-client.ts +21 -0
  599. package/src/tools/browser/cdp-client/types.ts +101 -0
  600. package/src/tools/browser/pinned-tabs.ts +146 -0
  601. package/src/tools/computer-use/definitions.ts +22 -78
  602. package/src/tools/credential-execution/make-authenticated-request.ts +3 -9
  603. package/src/tools/credential-execution/manage-secure-command-tool.ts +3 -9
  604. package/src/tools/credential-execution/run-authenticated-command.ts +3 -9
  605. package/src/tools/credentials/vault.ts +3 -9
  606. package/src/tools/document/document-comment-tool.test.ts +379 -0
  607. package/src/tools/document/document-comment-tool.ts +156 -0
  608. package/src/tools/document/document-tool.ts +187 -2
  609. package/src/tools/execution-target.ts +21 -23
  610. package/src/tools/executor.ts +6 -1
  611. package/src/tools/filesystem/edit.ts +3 -9
  612. package/src/tools/filesystem/list.ts +3 -9
  613. package/src/tools/filesystem/read.ts +3 -9
  614. package/src/tools/filesystem/write.ts +3 -9
  615. package/src/tools/host-filesystem/edit.ts +3 -9
  616. package/src/tools/host-filesystem/read.ts +3 -9
  617. package/src/tools/host-filesystem/transfer.ts +3 -9
  618. package/src/tools/host-filesystem/write.ts +3 -9
  619. package/src/tools/host-terminal/host-shell.ts +3 -9
  620. package/src/tools/mcp/mcp-tool-factory.ts +1 -8
  621. package/src/tools/memory/register.test.ts +1 -1
  622. package/src/tools/memory/register.ts +4 -9
  623. package/src/tools/network/__tests__/web-fetch-metadata.test.ts +229 -0
  624. package/src/tools/network/__tests__/web-search-metadata.test.ts +346 -0
  625. package/src/tools/network/domain-normalize.ts +17 -0
  626. package/src/tools/network/web-fetch.ts +216 -73
  627. package/src/tools/network/web-search.ts +216 -98
  628. package/src/tools/registry.ts +7 -23
  629. package/src/tools/schema-transforms.ts +1 -1
  630. package/src/tools/skills/execute.ts +3 -9
  631. package/src/tools/skills/load.ts +3 -9
  632. package/src/tools/skills/skill-tool-factory.ts +1 -8
  633. package/src/tools/subagent/notify-parent.ts +3 -9
  634. package/src/tools/system/request-permission.ts +3 -9
  635. package/src/tools/terminal/safe-env.ts +3 -2
  636. package/src/tools/terminal/shell.ts +3 -9
  637. package/src/tools/tool-approval-handler.ts +19 -12
  638. package/src/tools/tool-defaults.ts +94 -0
  639. package/src/tools/types.ts +31 -98
  640. package/src/tools/ui-surface/definitions.ts +9 -23
  641. package/src/types/onboarding-context.ts +4 -0
  642. package/src/usage/pricing.ts +23 -0
  643. package/src/usage/types.ts +12 -0
  644. package/src/util/__tests__/favicon.test.ts +84 -0
  645. package/src/util/favicon.ts +40 -0
  646. package/src/util/logger.ts +16 -7
  647. package/src/util/platform.ts +7 -7
  648. package/src/util/sqlite3-runtime.ts +65 -0
  649. package/src/workspace/git-service.ts +75 -4
  650. package/src/workspace/migrations/086-revert-stale-gemini-mis-rewrites.ts +1 -0
  651. package/src/workspace/migrations/088-deprecate-background-conversation-override.ts +103 -0
  652. package/src/workspace/migrations/089-move-memory-tree-out-of-v3.ts +86 -0
  653. package/src/workspace/migrations/registry.ts +4 -0
  654. package/src/__tests__/compaction-strip-metadata-clear.test.ts +0 -206
  655. package/src/__tests__/message-complete-display-id.test.ts +0 -175
  656. package/src/config/bundled-skills/document/SKILL.md +0 -54
  657. package/src/config/bundled-skills/document/TOOLS.json +0 -106
  658. package/src/daemon/seed-files.ts +0 -18
  659. package/src/prompts/cache-boundary.ts +0 -8
  660. package/src/runtime/routes/interface-routes.ts +0 -43
  661. /package/src/config/bundled-skills/{document → document-editor}/tools/document-create.ts +0 -0
  662. /package/src/config/bundled-skills/{document → document-editor}/tools/document-delete.ts +0 -0
  663. /package/src/config/bundled-skills/{document → document-editor}/tools/document-list.ts +0 -0
  664. /package/src/config/bundled-skills/{document → document-editor}/tools/document-read.ts +0 -0
  665. /package/src/config/bundled-skills/{document → document-editor}/tools/document-update.ts +0 -0
@@ -73,6 +73,26 @@ mock.module("../skill-store.js", () => ({
73
73
  listSkillEntries: () => skillState.entries,
74
74
  }));
75
75
 
76
+ // Stub `computeInjectionScores` so tier-2 tests can dictate scores
77
+ // without spinning up a real bun:sqlite db. Real production wiring is
78
+ // covered by the splitTier2 unit tests in page-index.test.ts and the
79
+ // score-formula tests in injection-events.test.ts.
80
+ const scoresStub = new Map<string, number>();
81
+ mock.module("../injection-events.js", () => ({
82
+ computeInjectionScores: (
83
+ _db: unknown,
84
+ slugs: readonly string[],
85
+ _now: number,
86
+ ): Map<string, number> => {
87
+ const out = new Map<string, number>();
88
+ for (const slug of slugs) {
89
+ const score = scoresStub.get(slug);
90
+ if (score !== undefined && score > 0) out.set(slug, score);
91
+ }
92
+ return out;
93
+ },
94
+ }));
95
+
76
96
  // Provider stub. Each test sets `providerStub` to control the response;
77
97
  // `null` simulates "no configured provider available".
78
98
  let providerStub: Provider | null = null;
@@ -96,7 +116,7 @@ mock.module("../../../providers/provider-send-message.js", () => ({
96
116
  // them. No mock needed for `daemon/identity-helpers.js`; it tolerates a
97
117
  // missing IDENTITY.md by returning null.
98
118
 
99
- const { runRouter } = await import("../router.js");
119
+ const { runRouter, applyHistoricalCharBudget } = await import("../router.js");
100
120
  const { getPageIndex, invalidatePageIndex } = await import("../page-index.js");
101
121
  const { writePage } = await import("../page-store.js");
102
122
 
@@ -112,6 +132,7 @@ beforeEach(() => {
112
132
  providerStub = null;
113
133
  providerCalls.length = 0;
114
134
  warnLogs.length = 0;
135
+ scoresStub.clear();
115
136
  invalidatePageIndex();
116
137
  });
117
138
 
@@ -194,7 +215,13 @@ function makePage(
194
215
  // fields the router actually reads. Cast through `as unknown` because the
195
216
  // production type is a heavy nested schema; we only exercise the v2.router
196
217
  // branch in this test file.
197
- function makeConfig(overrides?: { maxPageIds?: number }) {
218
+ function makeConfig(overrides?: {
219
+ maxPageIds?: number;
220
+ batchSize?: number | null;
221
+ tier1Size?: number | null;
222
+ tier2Size?: number | null;
223
+ historicalPairsMaxChars?: number | null;
224
+ }) {
198
225
  return {
199
226
  memory: {
200
227
  v2: {
@@ -202,6 +229,11 @@ function makeConfig(overrides?: { maxPageIds?: number }) {
202
229
  router: {
203
230
  enabled: true,
204
231
  max_page_ids: overrides?.maxPageIds ?? 25,
232
+ batch_size: overrides?.batchSize ?? null,
233
+ tier1_size: overrides?.tier1Size ?? null,
234
+ tier2_size: overrides?.tier2Size ?? null,
235
+ historical_pairs_max_chars:
236
+ overrides?.historicalPairsMaxChars ?? null,
205
237
  },
206
238
  },
207
239
  },
@@ -209,8 +241,12 @@ function makeConfig(overrides?: { maxPageIds?: number }) {
209
241
  }
210
242
 
211
243
  const COMMON_PARAMS = {
212
- userMessage: "What's on my plate today?",
213
- assistantMessage: "Let me check your plan.",
244
+ recentTurnPairs: [
245
+ {
246
+ assistantMessage: "Let me check your plan.",
247
+ userMessage: "What's on my plate today?",
248
+ },
249
+ ],
214
250
  nowText: "2026-05-10 14:00 PT",
215
251
  priorEverInjected: [] as { slug: string; turn: number }[],
216
252
  };
@@ -231,6 +267,7 @@ describe("runRouter — early bails", () => {
231
267
 
232
268
  expect(result).toEqual({
233
269
  selectedSlugs: [],
270
+ sourceBySlug: new Map(),
234
271
  failureReason: "empty_index",
235
272
  });
236
273
  // Provider must NOT be invoked when there is nothing to route.
@@ -286,6 +323,7 @@ describe("runRouter — successful tool_use", () => {
286
323
 
287
324
  expect(result).toEqual({
288
325
  selectedSlugs: [],
326
+ sourceBySlug: new Map(),
289
327
  failureReason: null,
290
328
  });
291
329
  });
@@ -387,6 +425,78 @@ describe("runRouter — successful tool_use", () => {
387
425
  expect(blockB.cache_control).toBeUndefined();
388
426
  });
389
427
 
428
+ test("runRouterBatch front-truncates the oldest <last_turn> message when the char budget is exceeded", async () => {
429
+ await writePage(workspaceDir, makePage("alpha", { summary: "A" }));
430
+ providerStub = makeProvider(toolUseResponse([1]));
431
+
432
+ const longAssistant = "A".repeat(2_000);
433
+ const longUser = "B".repeat(2_000);
434
+ const recentAssistant = "Short prior.";
435
+ const justArrived = "What's relevant?";
436
+
437
+ await runRouter({
438
+ workspaceDir,
439
+ recentTurnPairs: [
440
+ { assistantMessage: longAssistant, userMessage: longUser },
441
+ { assistantMessage: recentAssistant, userMessage: justArrived },
442
+ ],
443
+ nowText: "now",
444
+ priorEverInjected: [],
445
+ // Budget: just enough room for the most-recent pair plus the old user
446
+ // line in full, leaving a small slice for the very oldest assistant
447
+ // (which should be front-truncated with the `…` marker).
448
+ config: makeConfig({
449
+ historicalPairsMaxChars:
450
+ recentAssistant.length + justArrived.length + longUser.length + 50,
451
+ }),
452
+ });
453
+
454
+ const [call] = providerCalls;
455
+ const userMsg = call.messages[0];
456
+ const blockB = userMsg.content[1] as { text: string };
457
+
458
+ // The just-arrived user message and the prior assistant reply survive
459
+ // verbatim because they're newest in the walk.
460
+ expect(blockB.text).toContain(`[user]: ${justArrived}`);
461
+ expect(blockB.text).toContain(`[assistant]: ${recentAssistant}`);
462
+
463
+ // The older user message survives verbatim (next newest after the
464
+ // most-recent pair).
465
+ expect(blockB.text).toContain(`[user]: ${longUser}`);
466
+
467
+ // The oldest message in the walk (the older assistant) is
468
+ // front-truncated, so its rendered line starts with the `…` marker
469
+ // and ends with the suffix of the original text.
470
+ expect(blockB.text).toContain("[assistant]: …");
471
+ expect(blockB.text.endsWith(`A\n</last_turn>`)).toBe(false); // sanity
472
+ // The full untruncated long-assistant string must NOT appear.
473
+ expect(blockB.text.includes(longAssistant)).toBe(false);
474
+ // The TAIL of the long-assistant string SHOULD appear (kept from front-truncation).
475
+ expect(blockB.text).toContain(longAssistant.slice(-10));
476
+ });
477
+
478
+ test("null historical_pairs_max_chars renders pairs verbatim regardless of size", async () => {
479
+ await writePage(workspaceDir, makePage("alpha", { summary: "A" }));
480
+ providerStub = makeProvider(toolUseResponse([1]));
481
+
482
+ const huge = "X".repeat(5_000);
483
+ await runRouter({
484
+ workspaceDir,
485
+ recentTurnPairs: [
486
+ { assistantMessage: huge, userMessage: "just arrived" },
487
+ ],
488
+ nowText: "now",
489
+ priorEverInjected: [],
490
+ config: makeConfig(), // historical_pairs_max_chars: null
491
+ });
492
+
493
+ const [call] = providerCalls;
494
+ const blockB = call.messages[0].content[1] as { text: string };
495
+ expect(blockB.text).toContain(`[assistant]: ${huge}`);
496
+ expect(blockB.text).toContain("[user]: just arrived");
497
+ expect(blockB.text).not.toContain("…");
498
+ });
499
+
390
500
  test("de-duplicates repeated IDs from the model while preserving order", async () => {
391
501
  providerStub = makeProvider(toolUseResponse([2, 1, 2]));
392
502
 
@@ -529,3 +639,549 @@ describe("runRouter — failure modes", () => {
529
639
  expect(providerCalls[0].options?.signal).toBe(controller.signal);
530
640
  });
531
641
  });
642
+
643
+ // ---------------------------------------------------------------------------
644
+ // Batched routing (config.memory.v2.router.batch_size).
645
+ // ---------------------------------------------------------------------------
646
+
647
+ describe("runRouter — batched (batch_size set)", () => {
648
+ beforeEach(async () => {
649
+ // 5 pages → at batch_size=2 we get ceil(5/2)=3 batches.
650
+ await writePage(workspaceDir, makePage("alpha", { summary: "A" }));
651
+ await writePage(workspaceDir, makePage("bravo", { summary: "B" }));
652
+ await writePage(workspaceDir, makePage("charlie", { summary: "C" }));
653
+ await writePage(workspaceDir, makePage("delta", { summary: "D" }));
654
+ await writePage(workspaceDir, makePage("echo", { summary: "E" }));
655
+ });
656
+
657
+ test("fires one provider call per batch in parallel", async () => {
658
+ // Every batch returns its local id 1 → at most 3 distinct slugs in the
659
+ // union (one per batch), but we don't assert WHICH slugs the FNV
660
+ // bucketing picks; just that the provider was called once per batch.
661
+ providerStub = makeProvider(toolUseResponse([1]));
662
+
663
+ const result = await runRouter({
664
+ workspaceDir,
665
+ ...COMMON_PARAMS,
666
+ config: makeConfig({ batchSize: 2 }),
667
+ });
668
+
669
+ expect(result.failureReason).toBeNull();
670
+ expect(providerCalls.length).toBeGreaterThan(1);
671
+ expect(providerCalls.length).toBeLessThanOrEqual(3);
672
+ // Every batch picked its own local id 1 → distinct slugs in union.
673
+ expect(result.selectedSlugs.length).toBe(providerCalls.length);
674
+ expect(new Set(result.selectedSlugs).size).toBe(
675
+ result.selectedSlugs.length,
676
+ );
677
+ });
678
+
679
+ test("each batch's system prompt contains only its own subset of slugs", async () => {
680
+ providerStub = makeProvider(toolUseResponse([1]));
681
+ await runRouter({
682
+ workspaceDir,
683
+ ...COMMON_PARAMS,
684
+ config: makeConfig({ batchSize: 2 }),
685
+ });
686
+
687
+ // Across all batch calls, every slug appears in exactly one prompt.
688
+ const allSlugs = ["alpha", "bravo", "charlie", "delta", "echo"];
689
+ const appearances = new Map<string, number>(allSlugs.map((s) => [s, 0]));
690
+ for (const call of providerCalls) {
691
+ for (const slug of allSlugs) {
692
+ if (call.systemPrompt?.includes(slug)) {
693
+ appearances.set(slug, (appearances.get(slug) ?? 0) + 1);
694
+ }
695
+ }
696
+ }
697
+ for (const slug of allSlugs) {
698
+ expect(appearances.get(slug)).toBe(1);
699
+ }
700
+ });
701
+
702
+ test("union of selected slugs is deduplicated across batches", async () => {
703
+ // Every batch returns its local id 1. Same slug could appear in only
704
+ // one batch (since each slug lives in exactly one batch), so the union
705
+ // is naturally unique. Sanity-check the dedup path with a 2-call response.
706
+ providerStub = makeProvider(toolUseResponse([1, 1]));
707
+ const result = await runRouter({
708
+ workspaceDir,
709
+ ...COMMON_PARAMS,
710
+ config: makeConfig({ batchSize: 2 }),
711
+ });
712
+ expect(result.failureReason).toBeNull();
713
+ expect(new Set(result.selectedSlugs).size).toBe(
714
+ result.selectedSlugs.length,
715
+ );
716
+ });
717
+
718
+ test("priorEverInjected is filtered to the batch's own slugs as local IDs", async () => {
719
+ providerStub = makeProvider(toolUseResponse([1]));
720
+ await runRouter({
721
+ workspaceDir,
722
+ ...COMMON_PARAMS,
723
+ priorEverInjected: [
724
+ { slug: "alpha", turn: 1 },
725
+ { slug: "echo", turn: 1 },
726
+ ],
727
+ config: makeConfig({ batchSize: 2 }),
728
+ });
729
+
730
+ // Exactly the batches containing alpha or echo should mention any
731
+ // already_injected_id; other batches should have an empty list.
732
+ for (const call of providerCalls) {
733
+ const text =
734
+ (call.messages[0].content as Array<{ text?: string }>)[1]?.text ?? "";
735
+ const hasAlpha = call.systemPrompt?.includes("alpha");
736
+ const hasEcho = call.systemPrompt?.includes("echo");
737
+ const expectsId = hasAlpha || hasEcho;
738
+ // Block contents: "<already_injected_ids>\n{ids}\n</already_injected_ids>"
739
+ const match = text.match(
740
+ /<already_injected_ids>\n([^\n]*)\n<\/already_injected_ids>/,
741
+ );
742
+ const idsStr = match?.[1] ?? "";
743
+ if (expectsId) {
744
+ expect(idsStr.trim().length).toBeGreaterThan(0);
745
+ } else {
746
+ expect(idsStr.trim()).toBe("");
747
+ }
748
+ }
749
+ });
750
+
751
+ test("partial failure: one batch fails, others succeed → union returned with success", async () => {
752
+ let callCount = 0;
753
+ providerStub = {
754
+ name: "partial-failure",
755
+ sendMessage: async (messages, tools, systemPrompt, options) => {
756
+ callCount += 1;
757
+ providerCalls.push({ messages, tools, systemPrompt, options });
758
+ if (callCount === 1) throw new Error("batch 1 boom");
759
+ return toolUseResponse([1]);
760
+ },
761
+ };
762
+
763
+ const result = await runRouter({
764
+ workspaceDir,
765
+ ...COMMON_PARAMS,
766
+ config: makeConfig({ batchSize: 2 }),
767
+ });
768
+
769
+ expect(result.failureReason).toBeNull();
770
+ expect(result.selectedSlugs.length).toBeGreaterThan(0);
771
+ expect(providerCalls.length).toBeGreaterThan(1);
772
+ });
773
+
774
+ test("all batches fail → unified failure with first batch's reason", async () => {
775
+ providerStub = {
776
+ name: "all-fail",
777
+ sendMessage: async () => {
778
+ throw new Error("all batches boom");
779
+ },
780
+ };
781
+
782
+ const result = await runRouter({
783
+ workspaceDir,
784
+ ...COMMON_PARAMS,
785
+ config: makeConfig({ batchSize: 2 }),
786
+ });
787
+
788
+ expect(result.failureReason).toBe("api_error");
789
+ expect(result.selectedSlugs).toEqual([]);
790
+ });
791
+
792
+ test("union across batches is truncated to global max_page_ids", async () => {
793
+ // 5 pages, batch_size=1 → 5 batches, each picks its own local id 1.
794
+ // Without a global cap the union would be 5; max_page_ids=2 forces
795
+ // truncation back to 2.
796
+ providerStub = makeProvider(toolUseResponse([1]));
797
+
798
+ const result = await runRouter({
799
+ workspaceDir,
800
+ ...COMMON_PARAMS,
801
+ config: makeConfig({ batchSize: 1, maxPageIds: 2 }),
802
+ });
803
+
804
+ expect(result.failureReason).toBeNull();
805
+ expect(result.selectedSlugs).toHaveLength(2);
806
+ // sourceBySlug must stay aligned with the truncated selection.
807
+ expect(result.sourceBySlug.size).toBe(2);
808
+ for (const slug of result.selectedSlugs) {
809
+ expect(result.sourceBySlug.get(slug)).toBeDefined();
810
+ }
811
+ const warned = warnLogs.some((l) =>
812
+ JSON.stringify(l.args).includes("union across batches exceeded"),
813
+ );
814
+ expect(warned).toBe(true);
815
+ });
816
+
817
+ test("batch_size larger than index size is single batch (same as v3)", async () => {
818
+ providerStub = makeProvider(toolUseResponse([1]));
819
+ const result = await runRouter({
820
+ workspaceDir,
821
+ ...COMMON_PARAMS,
822
+ config: makeConfig({ batchSize: 1000 }),
823
+ });
824
+ expect(result.failureReason).toBeNull();
825
+ expect(providerCalls).toHaveLength(1);
826
+ });
827
+ });
828
+
829
+ // ---------------------------------------------------------------------------
830
+ // Tier 1 (recently modified) splitting.
831
+ // ---------------------------------------------------------------------------
832
+
833
+ const { utimes } = await import("node:fs/promises");
834
+
835
+ describe("runRouter — tier 1 (recently modified)", () => {
836
+ async function setMtime(slug: string, epochMs: number): Promise<void> {
837
+ const seconds = epochMs / 1000;
838
+ await utimes(
839
+ join(workspaceDir, "memory", "concepts", `${slug}.md`),
840
+ seconds,
841
+ seconds,
842
+ );
843
+ }
844
+
845
+ beforeEach(async () => {
846
+ await writePage(workspaceDir, makePage("alpha", { summary: "A" }));
847
+ await writePage(workspaceDir, makePage("bravo", { summary: "B" }));
848
+ await writePage(workspaceDir, makePage("charlie", { summary: "C" }));
849
+ await writePage(workspaceDir, makePage("delta", { summary: "D" }));
850
+ await writePage(workspaceDir, makePage("echo", { summary: "E" }));
851
+ });
852
+
853
+ test("tier1_size + batch_size both null is the v3 single-batch path", async () => {
854
+ providerStub = makeProvider(toolUseResponse([1]));
855
+ const result = await runRouter({
856
+ workspaceDir,
857
+ ...COMMON_PARAMS,
858
+ config: makeConfig(),
859
+ });
860
+ expect(result.failureReason).toBeNull();
861
+ expect(providerCalls).toHaveLength(1);
862
+ });
863
+
864
+ test("tier1_size=2 + batch_size=null produces 2 batches (tier1 + rest)", async () => {
865
+ providerStub = makeProvider(toolUseResponse([1]));
866
+ const result = await runRouter({
867
+ workspaceDir,
868
+ ...COMMON_PARAMS,
869
+ config: makeConfig({ tier1Size: 2 }),
870
+ });
871
+ expect(result.failureReason).toBeNull();
872
+ expect(providerCalls).toHaveLength(2);
873
+ });
874
+
875
+ test("tier 1 contains the most recently modified pages", async () => {
876
+ // Stamp distinct mtimes so the ordering is unambiguous.
877
+ await setMtime("alpha", 1_000_000);
878
+ await setMtime("bravo", 5_000_000); // most recent
879
+ await setMtime("charlie", 2_000_000);
880
+ await setMtime("delta", 4_000_000); // 2nd most recent
881
+ await setMtime("echo", 3_000_000);
882
+
883
+ providerStub = makeProvider(toolUseResponse([1]));
884
+ await runRouter({
885
+ workspaceDir,
886
+ ...COMMON_PARAMS,
887
+ config: makeConfig({ tier1Size: 2 }),
888
+ });
889
+
890
+ // Tier 1 is the first provider call. Match `[N] slug` lines specifically
891
+ // — string-search on slug name alone would false-positive on prompt
892
+ // template text that may mention the same words.
893
+ const tier1Prompt = providerCalls[0].systemPrompt ?? "";
894
+ const indexedSlugs = new Set(
895
+ [...tier1Prompt.matchAll(/^\[\d+\] (\S+)/gm)].map((m) => m[1]),
896
+ );
897
+ expect(indexedSlugs).toEqual(new Set(["bravo", "delta"]));
898
+ });
899
+
900
+ test("tier1_size=2 + batch_size=2 puts every slug in exactly one batch", async () => {
901
+ providerStub = makeProvider(toolUseResponse([1]));
902
+ await runRouter({
903
+ workspaceDir,
904
+ ...COMMON_PARAMS,
905
+ config: makeConfig({ tier1Size: 2, batchSize: 2 }),
906
+ });
907
+ // 5 pages, tier1=2, rest=3 → 1 tier1 batch + 1-or-2 tier3 batches
908
+ // depending on whether FNV hash distributes the 3 rest slugs into both
909
+ // buckets. The empty-batch filter drops a bucket that lands empty, so
910
+ // the strong invariant is "every slug appears in exactly one batch."
911
+ expect(providerCalls.length).toBeGreaterThanOrEqual(2);
912
+ expect(providerCalls.length).toBeLessThanOrEqual(3);
913
+
914
+ const allSlugs = ["alpha", "bravo", "charlie", "delta", "echo"];
915
+ const appearances = new Map<string, number>(
916
+ allSlugs.map((s) => [s, 0] as [string, number]),
917
+ );
918
+ for (const call of providerCalls) {
919
+ for (const slug of allSlugs) {
920
+ if (call.systemPrompt?.includes(slug)) {
921
+ appearances.set(slug, (appearances.get(slug) ?? 0) + 1);
922
+ }
923
+ }
924
+ }
925
+ for (const slug of allSlugs) {
926
+ expect(appearances.get(slug)).toBe(1);
927
+ }
928
+ });
929
+
930
+ test("tier1_size >= total pages → single tier 1 batch, no rest", async () => {
931
+ providerStub = makeProvider(toolUseResponse([1]));
932
+ await runRouter({
933
+ workspaceDir,
934
+ ...COMMON_PARAMS,
935
+ config: makeConfig({ tier1Size: 100 }),
936
+ });
937
+ // 5 pages, tier1_size=100 → only tier 1 fires; the empty rest is dropped.
938
+ expect(providerCalls).toHaveLength(1);
939
+ });
940
+ });
941
+
942
+ // ---------------------------------------------------------------------------
943
+ // Tier 2 (highest-EMA) splitting.
944
+ // ---------------------------------------------------------------------------
945
+
946
+ describe("runRouter — tier 2 (highest EMA)", () => {
947
+ // Any non-null value passes the `params.database` check in the orchestrator;
948
+ // the real db is never touched because computeInjectionScores is mocked.
949
+ const stubDb = {} as Parameters<typeof runRouter>[0]["database"];
950
+
951
+ beforeEach(async () => {
952
+ await writePage(workspaceDir, makePage("alpha", { summary: "A" }));
953
+ await writePage(workspaceDir, makePage("bravo", { summary: "B" }));
954
+ await writePage(workspaceDir, makePage("charlie", { summary: "C" }));
955
+ await writePage(workspaceDir, makePage("delta", { summary: "D" }));
956
+ await writePage(workspaceDir, makePage("echo", { summary: "E" }));
957
+ });
958
+
959
+ test("tier2_size + tier1_size both null is the v3 single-batch path", async () => {
960
+ providerStub = makeProvider(toolUseResponse([1]));
961
+ await runRouter({
962
+ workspaceDir,
963
+ ...COMMON_PARAMS,
964
+ config: makeConfig(),
965
+ database: stubDb,
966
+ });
967
+ expect(providerCalls).toHaveLength(1);
968
+ });
969
+
970
+ test("tier2_size=2 produces 2 batches (tier 2 + rest)", async () => {
971
+ scoresStub.set("alpha", 1.0);
972
+ scoresStub.set("bravo", 5.0);
973
+ scoresStub.set("delta", 4.0);
974
+
975
+ providerStub = makeProvider(toolUseResponse([1]));
976
+ await runRouter({
977
+ workspaceDir,
978
+ ...COMMON_PARAMS,
979
+ config: makeConfig({ tier2Size: 2 }),
980
+ database: stubDb,
981
+ });
982
+ expect(providerCalls.length).toBe(2);
983
+
984
+ // Tier 2 is the first batch (no tier 1 in this test). Its prompt should
985
+ // contain exactly the top-2 by score: bravo (5) and delta (4).
986
+ const tier2Prompt = providerCalls[0].systemPrompt ?? "";
987
+ const indexedSlugs = new Set(
988
+ [...tier2Prompt.matchAll(/^\[\d+\] (\S+)/gm)].map((m) => m[1]),
989
+ );
990
+ expect(indexedSlugs).toEqual(new Set(["bravo", "delta"]));
991
+ });
992
+
993
+ test("tier 1 then tier 2 then rest — three batches in that order", async () => {
994
+ // Pin mtimes so tier 1 is deterministic: alpha + bravo are the two
995
+ // most recent (highest mtime values). Tier 2 runs on the rest
996
+ // (charlie, delta, echo) using scores we control.
997
+ const { utimes } = await import("node:fs/promises");
998
+ const stamp = async (slug: string, s: number) =>
999
+ utimes(join(workspaceDir, "memory", "concepts", `${slug}.md`), s, s);
1000
+ await stamp("alpha", 9000);
1001
+ await stamp("bravo", 8000);
1002
+ await stamp("charlie", 3000);
1003
+ await stamp("delta", 2000);
1004
+ await stamp("echo", 1000);
1005
+ invalidatePageIndex();
1006
+
1007
+ scoresStub.set("charlie", 2.0);
1008
+ scoresStub.set("delta", 3.0);
1009
+ // echo has no score → ineligible for tier 2.
1010
+
1011
+ providerStub = makeProvider(toolUseResponse([1]));
1012
+ await runRouter({
1013
+ workspaceDir,
1014
+ ...COMMON_PARAMS,
1015
+ config: makeConfig({ tier1Size: 2, tier2Size: 2 }),
1016
+ database: stubDb,
1017
+ });
1018
+ expect(providerCalls.length).toBe(3);
1019
+
1020
+ // Tier 2 batch (index 1) contains charlie + delta. Echo went to rest.
1021
+ const tier2Prompt = providerCalls[1].systemPrompt ?? "";
1022
+ const tier2Slugs = new Set(
1023
+ [...tier2Prompt.matchAll(/^\[\d+\] (\S+)/gm)].map((m) => m[1]),
1024
+ );
1025
+ expect(tier2Slugs).toEqual(new Set(["charlie", "delta"]));
1026
+
1027
+ // Echo (no score) must land in the rest batch, not tier 2.
1028
+ const restPrompt = providerCalls[2].systemPrompt ?? "";
1029
+ const restSlugs = new Set(
1030
+ [...restPrompt.matchAll(/^\[\d+\] (\S+)/gm)].map((m) => m[1]),
1031
+ );
1032
+ expect(restSlugs).toEqual(new Set(["echo"]));
1033
+ });
1034
+
1035
+ test("score=0 pages stay in rest even when tier2_size is large", async () => {
1036
+ // Only one page has a positive score; tier2_size=100 should NOT pull in
1037
+ // zero-score pages.
1038
+ scoresStub.set("bravo", 5.0);
1039
+
1040
+ providerStub = makeProvider(toolUseResponse([1]));
1041
+ await runRouter({
1042
+ workspaceDir,
1043
+ ...COMMON_PARAMS,
1044
+ config: makeConfig({ tier2Size: 100 }),
1045
+ database: stubDb,
1046
+ });
1047
+ expect(providerCalls.length).toBe(2);
1048
+ const tier2Prompt = providerCalls[0].systemPrompt ?? "";
1049
+ expect(tier2Prompt).toContain("[1] bravo");
1050
+ expect(tier2Prompt).not.toMatch(/^\[\d+\] alpha/m);
1051
+ });
1052
+
1053
+ test("sourceBySlug tags each selection with its batch tier", async () => {
1054
+ scoresStub.set("bravo", 5.0);
1055
+ scoresStub.set("delta", 3.0);
1056
+
1057
+ providerStub = makeProvider(toolUseResponse([1]));
1058
+ const result = await runRouter({
1059
+ workspaceDir,
1060
+ ...COMMON_PARAMS,
1061
+ config: makeConfig({ tier1Size: 1, tier2Size: 1 }),
1062
+ database: stubDb,
1063
+ });
1064
+
1065
+ // Every selected slug should have a tier tag — exactly one of:
1066
+ // "tier1", "tier2", or "tier3:N" (N starts at 0).
1067
+ for (const slug of result.selectedSlugs) {
1068
+ const source = result.sourceBySlug.get(slug);
1069
+ expect(source).toBeDefined();
1070
+ expect(
1071
+ source === "tier1" ||
1072
+ source === "tier2" ||
1073
+ source!.startsWith("tier3:"),
1074
+ ).toBe(true);
1075
+ }
1076
+ // Tier 1 + tier 2 + (some tier 3 batches) ≥ 3 batches → at least one
1077
+ // slug per tier should be present in the source map across the union.
1078
+ const tags = new Set(result.sourceBySlug.values());
1079
+ expect(tags.has("tier1")).toBe(true);
1080
+ expect(tags.has("tier2")).toBe(true);
1081
+ });
1082
+
1083
+ test("tier2_size set without database logs a warn and skips tier 2", async () => {
1084
+ scoresStub.set("bravo", 5.0);
1085
+ providerStub = makeProvider(toolUseResponse([1]));
1086
+ await runRouter({
1087
+ workspaceDir,
1088
+ ...COMMON_PARAMS,
1089
+ config: makeConfig({ tier2Size: 2 }),
1090
+ // No database → tier 2 silently skipped.
1091
+ });
1092
+ expect(providerCalls).toHaveLength(1);
1093
+ const warned = warnLogs.some((l) =>
1094
+ JSON.stringify(l.args).includes("tier2_size set but no database"),
1095
+ );
1096
+ expect(warned).toBe(true);
1097
+ });
1098
+ });
1099
+
1100
+ // ---------------------------------------------------------------------------
1101
+ // applyHistoricalCharBudget — pure helper covering the cap semantics.
1102
+ // ---------------------------------------------------------------------------
1103
+
1104
+ describe("applyHistoricalCharBudget", () => {
1105
+ test("null budget is a no-op (returns a shallow copy)", () => {
1106
+ const pairs = [
1107
+ { assistantMessage: "older asst", userMessage: "older user" },
1108
+ { assistantMessage: "newer asst", userMessage: "newer user" },
1109
+ ];
1110
+ const out = applyHistoricalCharBudget(pairs, null);
1111
+ expect(out).toEqual(pairs);
1112
+ // shallow copy — not the same array reference, so callers can mutate freely
1113
+ expect(out).not.toBe(pairs);
1114
+ });
1115
+
1116
+ test("budget that fits every message returns content unchanged", () => {
1117
+ const pairs = [
1118
+ { assistantMessage: "AA", userMessage: "UU" },
1119
+ { assistantMessage: "BB", userMessage: "VV" },
1120
+ ];
1121
+ const total = "AA".length + "UU".length + "BB".length + "VV".length; // 8
1122
+ const out = applyHistoricalCharBudget(pairs, total);
1123
+ expect(out).toEqual(pairs);
1124
+ });
1125
+
1126
+ test("front-truncates the oldest still-includable message when the cap is exceeded", () => {
1127
+ // Newest user is 10 chars, newest assistant is 10, older user is 10,
1128
+ // older assistant is 20. Budget 35 leaves remaining = 35 - 10 - 10 - 10 = 5
1129
+ // for the older assistant; 5 - 1 marker char = 4 kept chars from the END.
1130
+ const pairs = [
1131
+ { assistantMessage: "ABCDEFGHIJKLMNOPQRST", userMessage: "old-user--" },
1132
+ { assistantMessage: "abcdefghij", userMessage: "uvwxyzUVWX" },
1133
+ ];
1134
+ const out = applyHistoricalCharBudget(pairs, 35);
1135
+ expect(out).toEqual([
1136
+ { assistantMessage: "…QRST", userMessage: "old-user--" },
1137
+ { assistantMessage: "abcdefghij", userMessage: "uvwxyzUVWX" },
1138
+ ]);
1139
+ // Sanity: total content chars equals the budget.
1140
+ const totalChars = out.reduce(
1141
+ (acc, p) => acc + p.assistantMessage.length + p.userMessage.length,
1142
+ 0,
1143
+ );
1144
+ expect(totalChars).toBe(35);
1145
+ });
1146
+
1147
+ test("drops older pairs entirely when even their first message has no room", () => {
1148
+ // Budget 20 fits the most-recent pair exactly (10 + 10 = 20) and leaves
1149
+ // zero room for the older pair, which is dropped entirely.
1150
+ const pairs = [
1151
+ { assistantMessage: "OLD-ASST00", userMessage: "OLD-USER00" },
1152
+ { assistantMessage: "NEW-ASST00", userMessage: "NEW-USER00" },
1153
+ ];
1154
+ const out = applyHistoricalCharBudget(pairs, 20);
1155
+ expect(out).toEqual([
1156
+ { assistantMessage: "NEW-ASST00", userMessage: "NEW-USER00" },
1157
+ ]);
1158
+ });
1159
+
1160
+ test("drops the older message of the current pair when the user line consumes the whole budget", () => {
1161
+ // Budget 10 just barely covers the newest user (10 chars). The pair's
1162
+ // own assistant message has no room and is dropped (left empty).
1163
+ const pairs = [
1164
+ { assistantMessage: "ASSISTANTX", userMessage: "USER-NEW10" },
1165
+ ];
1166
+ const out = applyHistoricalCharBudget(pairs, 10);
1167
+ expect(out).toEqual([{ assistantMessage: "", userMessage: "USER-NEW10" }]);
1168
+ });
1169
+
1170
+ test("non-positive budgets return an empty array (no message survives)", () => {
1171
+ const pairs = [{ assistantMessage: "x", userMessage: "y" }];
1172
+ expect(applyHistoricalCharBudget(pairs, 0)).toEqual(pairs);
1173
+ // Negative budgets are degenerate but should not throw.
1174
+ expect(applyHistoricalCharBudget(pairs, -5)).toEqual(pairs);
1175
+ });
1176
+
1177
+ test("budget smaller than the truncation marker drops the would-truncate message", () => {
1178
+ // Budget 11: covers full newest user (10 chars). Remaining 1 char is not
1179
+ // enough room for the marker, so the next message (newest assistant)
1180
+ // is dropped entirely rather than emitting a marker-only message.
1181
+ const pairs = [
1182
+ { assistantMessage: "ASSISTANTX", userMessage: "USER-NEW10" },
1183
+ ];
1184
+ const out = applyHistoricalCharBudget(pairs, 11);
1185
+ expect(out).toEqual([{ assistantMessage: "", userMessage: "USER-NEW10" }]);
1186
+ });
1187
+ });