@vellumai/assistant 0.8.4 → 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 (438) hide show
  1. package/ARCHITECTURE.md +2 -2
  2. package/docs/browser-use-architecture-phase2.md +1 -1
  3. package/knip.json +2 -1
  4. package/openapi.yaml +809 -11
  5. package/package.json +1 -1
  6. package/src/__tests__/anthropic-provider.test.ts +34 -37
  7. package/src/__tests__/assistant-event-hub-self-exclusion.test.ts +293 -0
  8. package/src/__tests__/assistant-feature-flags-integration.test.ts +3 -3
  9. package/src/__tests__/audit-log-rotation.test.ts +70 -16
  10. package/src/__tests__/background-workers-disk-pressure.test.ts +3 -3
  11. package/src/__tests__/btw-routes.test.ts +2 -3
  12. package/src/__tests__/call-controller.test.ts +0 -1
  13. package/src/__tests__/cancel-resolves-conversation-key.test.ts +1 -1
  14. package/src/__tests__/channel-guardian.test.ts +3 -3
  15. package/src/__tests__/checker.test.ts +6 -15
  16. package/src/__tests__/compaction-events.test.ts +1 -0
  17. package/src/__tests__/compactor-call-site-logging.test.ts +214 -0
  18. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +5 -11
  19. package/src/__tests__/computer-use-tools.test.ts +2 -4
  20. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +0 -1
  21. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +1 -1
  22. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -1
  23. package/src/__tests__/conversation-agent-loop-overflow.test.ts +197 -2
  24. package/src/__tests__/conversation-agent-loop.test.ts +163 -122
  25. package/src/__tests__/conversation-app-control-instantiation.test.ts +2 -5
  26. package/src/__tests__/conversation-clear-safety.test.ts +25 -25
  27. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +1 -1
  28. package/src/__tests__/conversation-disk-view-integration.test.ts +2 -2
  29. package/src/__tests__/conversation-error.test.ts +31 -0
  30. package/src/__tests__/conversation-fork-crud.test.ts +178 -15
  31. package/src/__tests__/conversation-lifecycle.test.ts +52 -11
  32. package/src/__tests__/{conversation-load-cleaned-at.test.ts → conversation-load-history-stripped.test.ts} +13 -13
  33. package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -0
  34. package/src/__tests__/conversation-routes-disk-view.test.ts +109 -0
  35. package/src/__tests__/conversation-routes-slash-commands.test.ts +35 -0
  36. package/src/__tests__/conversation-skill-tools.test.ts +2 -5
  37. package/src/__tests__/conversation-store.test.ts +1 -1
  38. package/src/__tests__/conversation-sync-tags.test.ts +99 -32
  39. package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -0
  40. package/src/__tests__/conversation-workspace-injection.test.ts +1 -1
  41. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
  42. package/src/__tests__/credential-execution-feature-gates.test.ts +9 -7
  43. package/src/__tests__/credential-execution-tools.test.ts +6 -6
  44. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  45. package/src/__tests__/credential-vault-unit.test.ts +2 -2
  46. package/src/__tests__/dynamic-page-surface.test.ts +2 -2
  47. package/src/__tests__/email-html-renderer.test.ts +12 -0
  48. package/src/__tests__/gateway-flag-listener.test.ts +237 -0
  49. package/src/__tests__/gemini-provider.test.ts +78 -0
  50. package/src/__tests__/guardian-dispatch.test.ts +0 -1
  51. package/src/__tests__/guardian-outbound-http.test.ts +7 -5
  52. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +1 -1
  53. package/src/__tests__/heartbeat-disk-pressure.test.ts +4 -0
  54. package/src/__tests__/heartbeat-service.test.ts +4 -0
  55. package/src/__tests__/host-shell-tool.test.ts +1 -1
  56. package/src/__tests__/init-feature-flag-overrides.test.ts +5 -6
  57. package/src/__tests__/list-messages-tool-merge.test.ts +70 -11
  58. package/src/__tests__/llm-request-log-call-site.test.ts +136 -0
  59. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +26 -0
  60. package/src/__tests__/llm-resolver.test.ts +77 -9
  61. package/src/__tests__/llm-usage-store.test.ts +66 -0
  62. package/src/__tests__/logger.test.ts +89 -0
  63. package/src/__tests__/mcp-abort-signal.test.ts +2 -2
  64. package/src/__tests__/media-generate-image.test.ts +31 -0
  65. package/src/__tests__/memory-v2-static-injector.test.ts +7 -7
  66. package/src/__tests__/model-intents.test.ts +2 -4
  67. package/src/__tests__/notification-guardian-path.test.ts +0 -1
  68. package/src/__tests__/onboarding-template-contract.test.ts +1 -1
  69. package/src/__tests__/openai-provider.test.ts +46 -0
  70. package/src/__tests__/openai-responses-provider.test.ts +114 -12
  71. package/src/__tests__/pending-interactions-resolved-event.test.ts +0 -1
  72. package/src/__tests__/platform-bash-auto-approve.test.ts +2 -2
  73. package/src/__tests__/platform.test.ts +2 -2
  74. package/src/__tests__/plugin-api-tool-definition.test.ts +92 -0
  75. package/src/__tests__/plugin-bootstrap.test.ts +2 -2
  76. package/src/__tests__/plugin-tool-contribution.test.ts +13 -6
  77. package/src/__tests__/plugin-types.test.ts +3 -2
  78. package/src/__tests__/prechat-onboarding-contract.test.ts +131 -98
  79. package/src/__tests__/pricing.test.ts +12 -0
  80. package/src/__tests__/prune-jobs-changes-parser.test.ts +61 -0
  81. package/src/__tests__/registry.test.ts +2 -8
  82. package/src/__tests__/require-fresh-approval.test.ts +2 -2
  83. package/src/__tests__/runtime-events-sse-bilingual.test.ts +154 -0
  84. package/src/__tests__/shell-tool-proxy-mode.test.ts +1 -1
  85. package/src/__tests__/skill-feature-flags.test.ts +2 -2
  86. package/src/__tests__/skill-projection-feature-flag.test.ts +4 -7
  87. package/src/__tests__/skill-projection.benchmark.test.ts +2 -6
  88. package/src/__tests__/skill-tool-factory.test.ts +1 -1
  89. package/src/__tests__/subagent-notify-parent.test.ts +1 -1
  90. package/src/__tests__/suggestion-routes.test.ts +1 -0
  91. package/src/__tests__/sync-message-contract.test.ts +59 -0
  92. package/src/__tests__/system-prompt.test.ts +145 -131
  93. package/src/__tests__/terminal-tools.test.ts +1 -1
  94. package/src/__tests__/tool-approval-handler.test.ts +1 -5
  95. package/src/__tests__/tool-execute-pipeline.test.ts +2 -2
  96. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +2 -5
  97. package/src/__tests__/tool-executor-lifecycle-events.test.ts +15 -5
  98. package/src/__tests__/tool-executor.test.ts +9 -62
  99. package/src/__tests__/tool-grant-request-escalation.test.ts +1 -6
  100. package/src/__tests__/trusted-contact-approval-notifier.test.ts +0 -1
  101. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +1 -6
  102. package/src/__tests__/trusted-contact-multichannel.test.ts +0 -1
  103. package/src/__tests__/ui-file-upload-surface.test.ts +2 -2
  104. package/src/__tests__/usage-routes.test.ts +3 -0
  105. package/src/__tests__/verification-control-plane-policy.test.ts +2 -2
  106. package/src/__tests__/workspace-git-service.test.ts +6 -5
  107. package/src/__tests__/workspace-migration-089-move-memory-tree-out-of-v3.test.ts +86 -0
  108. package/src/acp/__tests__/prepare-agent-env.test.ts +146 -0
  109. package/src/acp/prepare-agent-env.ts +78 -0
  110. package/src/acp/session-manager.ts +1 -1
  111. package/src/agent/loop.ts +8 -0
  112. package/src/api/README.md +5 -0
  113. package/src/api/index.ts +4 -0
  114. package/src/api/package.json +10 -0
  115. package/src/background-wake/background-wake-routes.test.ts +233 -0
  116. package/src/background-wake/runtime-registry.ts +24 -0
  117. package/src/cli/commands/__tests__/browser.test.ts +23 -5
  118. package/src/cli/commands/__tests__/domain-register.test.ts +110 -0
  119. package/src/cli/commands/__tests__/domain-status.test.ts +33 -33
  120. package/src/cli/commands/__tests__/inference-send.test.ts +108 -5
  121. package/src/cli/commands/__tests__/memory-v2-compare-render.test.ts +98 -0
  122. package/src/cli/commands/__tests__/memory-v2.test.ts +1 -0
  123. package/src/cli/commands/__tests__/memory-v3-render.test.ts +340 -0
  124. package/src/cli/commands/browser.ts +247 -0
  125. package/src/cli/commands/domain.ts +91 -41
  126. package/src/cli/commands/inference.ts +93 -40
  127. package/src/cli/commands/memory-v2-compare-render.ts +115 -0
  128. package/src/cli/commands/memory-v2.ts +176 -1
  129. package/src/cli/commands/memory-v3-render.ts +344 -0
  130. package/src/cli/commands/memory-v3.ts +316 -0
  131. package/src/cli/program.ts +2 -0
  132. package/src/config/assistant-feature-flags.ts +21 -9
  133. package/src/config/bundled-skills/document-editor/SKILL.md +11 -2
  134. package/src/config/bundled-skills/document-editor/TOOLS.json +18 -0
  135. package/src/config/bundled-skills/document-editor/tools/document-open.ts +12 -0
  136. package/src/config/bundled-skills/image-studio/SKILL.md +4 -0
  137. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +2 -2
  138. package/src/config/bundled-skills/media-processing/tools/ingest-media.ts +13 -8
  139. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +10 -3
  140. package/src/config/bundled-skills/phone-calls/references/TRANSCRIPTS.md +16 -14
  141. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +7 -2
  142. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +7 -2
  143. package/src/config/bundled-tool-registry.ts +2 -0
  144. package/src/config/call-site-defaults.ts +7 -6
  145. package/src/config/feature-flag-registry.json +16 -0
  146. package/src/config/schemas/__tests__/memory-v2.test.ts +213 -1
  147. package/src/config/schemas/call-site-catalog.ts +21 -7
  148. package/src/config/schemas/llm.ts +12 -1
  149. package/src/config/schemas/memory-v2.ts +246 -0
  150. package/src/config/schemas/memory.ts +2 -1
  151. package/src/context/compactor.ts +52 -0
  152. package/src/conversations/__tests__/message-consolidation.test.ts +350 -0
  153. package/src/conversations/message-consolidation.ts +404 -0
  154. package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +1 -1
  155. package/src/daemon/__tests__/meet-manifest-loader.test.ts +1 -1
  156. package/src/daemon/conversation-agent-loop-handlers.ts +2 -13
  157. package/src/daemon/conversation-agent-loop.ts +126 -76
  158. package/src/daemon/conversation-error.ts +31 -1
  159. package/src/daemon/conversation-lifecycle.ts +27 -22
  160. package/src/daemon/conversation-runtime-assembly.ts +10 -9
  161. package/src/daemon/conversation-tool-setup.ts +63 -3
  162. package/src/daemon/conversation-usage.ts +2 -0
  163. package/src/daemon/conversation.ts +14 -29
  164. package/src/daemon/disk-pressure-guard.ts +14 -2
  165. package/src/daemon/handlers/config-model.test.ts +1 -0
  166. package/src/daemon/handlers/conversations.ts +11 -3
  167. package/src/daemon/host-browser-proxy.ts +5 -5
  168. package/src/daemon/host-cu-proxy.ts +4 -4
  169. package/src/daemon/host-file-proxy.ts +4 -4
  170. package/src/daemon/host-proxy-base.ts +4 -4
  171. package/src/daemon/host-transfer-proxy.ts +10 -10
  172. package/src/daemon/lifecycle.ts +23 -20
  173. package/src/daemon/meet-manifest-loader.ts +1 -7
  174. package/src/daemon/message-types/conversations.ts +6 -9
  175. package/src/daemon/message-types/home.ts +1 -13
  176. package/src/daemon/message-types/messages.ts +6 -14
  177. package/src/daemon/message-types/sync.ts +14 -0
  178. package/src/daemon/shutdown-handlers.ts +24 -5
  179. package/src/daemon/switch-inference-profile-tool.ts +52 -0
  180. package/src/daemon/tool-setup-types.ts +13 -0
  181. package/src/events/relationship-state-updated.ts +25 -0
  182. package/src/heartbeat/__tests__/heartbeat-service.test.ts +1 -1
  183. package/src/home/home-greeting.ts +0 -9
  184. package/src/home/suggested-prompts.ts +0 -9
  185. package/src/ipc/gateway-flag-listener.ts +123 -0
  186. package/src/ipc/skill-routes/registries.ts +8 -12
  187. package/src/memory/__tests__/db-async-query.test.ts +165 -0
  188. package/src/memory/__tests__/db-maintenance.test.ts +115 -0
  189. package/src/memory/__tests__/jobs-store-enqueue-gate.test.ts +241 -0
  190. package/src/memory/__tests__/jobs-store-job-classes.test.ts +28 -1
  191. package/src/memory/__tests__/memory-retrospective-job.test.ts +7 -0
  192. package/src/memory/auto-analysis-enqueue.ts +5 -1
  193. package/src/memory/conversation-crud.ts +71 -70
  194. package/src/memory/conversation-starters-cadence.ts +3 -1
  195. package/src/memory/conversation-title-service.ts +19 -3
  196. package/src/memory/db-async-query.ts +214 -0
  197. package/src/memory/db-init.ts +10 -0
  198. package/src/memory/db-maintenance.ts +30 -21
  199. package/src/memory/graph/bootstrap.ts +8 -1
  200. package/src/memory/graph/capability-seed.ts +7 -3
  201. package/src/memory/graph/conversation-graph-memory.ts +100 -17
  202. package/src/memory/graph/extraction.ts +1 -5
  203. package/src/memory/graph/graph-search.ts +7 -1
  204. package/src/memory/indexer.ts +28 -18
  205. package/src/memory/job-handlers/cleanup.ts +76 -18
  206. package/src/memory/job-handlers/conversation-starters.ts +1 -4
  207. package/src/memory/jobs/embed-pkb-file.ts +6 -1
  208. package/src/memory/jobs-store.ts +14 -0
  209. package/src/memory/jobs-worker.ts +55 -22
  210. package/src/memory/llm-request-log-source-clickhouse.ts +42 -2
  211. package/src/memory/llm-request-log-source-local.ts +7 -0
  212. package/src/memory/llm-request-log-source.ts +9 -2
  213. package/src/memory/llm-request-log-store.ts +43 -1
  214. package/src/memory/llm-usage-store.ts +24 -0
  215. package/src/memory/memory-retrospective-enqueue.ts +8 -1
  216. package/src/memory/memory-retrospective-job.ts +5 -0
  217. package/src/memory/memory-v2-activation-log-store.ts +15 -6
  218. package/src/memory/migrations/260-rename-cleaned-at.ts +44 -0
  219. package/src/memory/migrations/261-llm-usage-add-raw-usage.ts +36 -0
  220. package/src/memory/migrations/262-memory-v3-coactivation.ts +57 -0
  221. package/src/memory/migrations/263-memory-v3-auto-edges.ts +50 -0
  222. package/src/memory/migrations/264-llm-request-log-call-site.ts +29 -0
  223. package/src/memory/migrations/index.ts +17 -0
  224. package/src/memory/migrations/registry.ts +33 -0
  225. package/src/memory/schema/conversations.ts +1 -1
  226. package/src/memory/schema/infrastructure.ts +21 -0
  227. package/src/memory/tool-usage-store.ts +36 -8
  228. package/src/memory/v2/__tests__/consolidation-job.test.ts +1 -0
  229. package/src/memory/v2/__tests__/harness-compare.test.ts +186 -0
  230. package/src/memory/v2/__tests__/harness-metrics.test.ts +74 -0
  231. package/src/memory/v2/__tests__/harness-oracle.test.ts +257 -0
  232. package/src/memory/v2/__tests__/harness-replay-input.test.ts +225 -0
  233. package/src/memory/v2/__tests__/harness-runner.test.ts +109 -0
  234. package/src/memory/v2/__tests__/injection.test.ts +127 -98
  235. package/src/memory/v2/__tests__/qdrant.test.ts +36 -0
  236. package/src/memory/v2/__tests__/router.test.ts +171 -3
  237. package/src/memory/v2/harness/compare.ts +57 -0
  238. package/src/memory/v2/harness/metrics.ts +124 -0
  239. package/src/memory/v2/harness/oracle.ts +145 -0
  240. package/src/memory/v2/harness/replay-input.ts +224 -0
  241. package/src/memory/v2/harness/retriever.ts +74 -0
  242. package/src/memory/v2/harness/router-retriever.ts +43 -0
  243. package/src/memory/v2/harness/runner.ts +106 -0
  244. package/src/memory/v2/harness/trace.ts +58 -0
  245. package/src/memory/v2/injection.ts +21 -15
  246. package/src/memory/v2/prompts/router.ts +26 -1
  247. package/src/memory/v2/qdrant.ts +14 -2
  248. package/src/memory/v2/router.ts +171 -18
  249. package/src/memory/v3/__tests__/coactivation-store.test.ts +422 -0
  250. package/src/memory/v3/__tests__/consolidation-job.test.ts +468 -0
  251. package/src/memory/v3/__tests__/edge-learning-job.test.ts +324 -0
  252. package/src/memory/v3/__tests__/edges.test.ts +563 -0
  253. package/src/memory/v3/__tests__/filter.test.ts +512 -0
  254. package/src/memory/v3/__tests__/gate.test.ts +574 -0
  255. package/src/memory/v3/__tests__/index-composition.test.ts +233 -0
  256. package/src/memory/v3/__tests__/loop.test.ts +530 -0
  257. package/src/memory/v3/__tests__/retriever.test.ts +226 -0
  258. package/src/memory/v3/__tests__/scouts.test.ts +440 -0
  259. package/src/memory/v3/__tests__/shadow-middleware.test.ts +312 -0
  260. package/src/memory/v3/__tests__/system-prompts.test.ts +154 -0
  261. package/src/memory/v3/__tests__/traversal.test.ts +469 -0
  262. package/src/memory/v3/__tests__/tree-index.test.ts +280 -0
  263. package/src/memory/v3/__tests__/tree-store.test.ts +529 -0
  264. package/src/memory/v3/__tests__/tree-walk.test.ts +707 -0
  265. package/src/memory/v3/__tests__/validate.test.ts +245 -0
  266. package/src/memory/v3/auto-edges.ts +223 -0
  267. package/src/memory/v3/coactivation-store.ts +124 -0
  268. package/src/memory/v3/consolidation-job.ts +323 -0
  269. package/src/memory/v3/edge-learning-job.ts +160 -0
  270. package/src/memory/v3/edges.ts +249 -0
  271. package/src/memory/v3/filter.ts +281 -0
  272. package/src/memory/v3/gate.ts +334 -0
  273. package/src/memory/v3/index-composition.ts +113 -0
  274. package/src/memory/v3/llm-capture.ts +46 -0
  275. package/src/memory/v3/loop.ts +382 -0
  276. package/src/memory/v3/maintenance.ts +144 -0
  277. package/src/memory/v3/prompt-context.ts +33 -0
  278. package/src/memory/v3/prompts/consolidation.ts +458 -0
  279. package/src/memory/v3/prompts/system-prompts.ts +196 -0
  280. package/src/memory/v3/retriever.ts +33 -0
  281. package/src/memory/v3/scouts.ts +420 -0
  282. package/src/memory/v3/shadow-middleware.ts +305 -0
  283. package/src/memory/v3/traversal.ts +206 -0
  284. package/src/memory/v3/tree-index.ts +237 -0
  285. package/src/memory/v3/tree-store.ts +394 -0
  286. package/src/memory/v3/tree-walk.ts +351 -0
  287. package/src/memory/v3/types.ts +65 -0
  288. package/src/memory/v3/validate.ts +300 -0
  289. package/src/notifications/adapters/macos.ts +18 -1
  290. package/src/notifications/adapters/platform.ts +1 -1
  291. package/src/notifications/decision-engine.ts +1 -4
  292. package/src/notifications/emit-signal.ts +29 -49
  293. package/src/permissions/prompter.ts +3 -3
  294. package/src/permissions/question-prompter.ts +5 -2
  295. package/src/permissions/secret-prompter.ts +2 -2
  296. package/src/plugin-api/index.ts +4 -0
  297. package/src/plugin-api/types.ts +7 -33
  298. package/src/plugins/defaults/index.ts +6 -0
  299. package/src/plugins/defaults/injectors.ts +18 -11
  300. package/src/plugins/external-plugin-loader.ts +5 -68
  301. package/src/plugins/types.ts +11 -16
  302. package/src/proactive-artifact/aux-message-injector.ts +17 -4
  303. package/src/prompts/__tests__/task-progress-hint-section.test.ts +3 -9
  304. package/src/prompts/persona-resolver.ts +36 -21
  305. package/src/prompts/sections.ts +39 -7
  306. package/src/prompts/system-prompt.ts +50 -185
  307. package/src/prompts/templates/BOOTSTRAP.md +2 -2
  308. package/src/prompts/templates/system-sections.ts +230 -8
  309. package/src/providers/__tests__/connection-model-compat.test.ts +234 -0
  310. package/src/providers/__tests__/retry-callsite.test.ts +85 -5
  311. package/src/providers/anthropic/client.ts +32 -66
  312. package/src/providers/call-site-routing.ts +14 -2
  313. package/src/providers/connection-model-compat.ts +38 -0
  314. package/src/providers/connection-resolution.ts +16 -2
  315. package/src/providers/gemini/client.ts +49 -6
  316. package/src/providers/inference/adapter-factory.ts +3 -0
  317. package/src/providers/minimax/client.ts +106 -0
  318. package/src/providers/model-catalog.ts +43 -0
  319. package/src/providers/model-intents.ts +1 -1
  320. package/src/providers/openai/chat-completions-provider.ts +6 -3
  321. package/src/providers/openai/codex-models.ts +18 -0
  322. package/src/providers/openai/responses-provider.ts +78 -21
  323. package/src/providers/provider-send-message.ts +7 -1
  324. package/src/providers/retry.ts +34 -3
  325. package/src/providers/thinking-config.ts +26 -1
  326. package/src/providers/usage-tracking.ts +2 -0
  327. package/src/runtime/AGENTS.md +2 -2
  328. package/src/runtime/agent-wake.ts +1 -0
  329. package/src/runtime/assistant-event-hub.ts +76 -6
  330. package/src/runtime/auth/route-policy.ts +36 -0
  331. package/src/runtime/btw-sidechain.ts +0 -6
  332. package/src/runtime/http-types.ts +0 -2
  333. package/src/runtime/migrations/vbundle-builder.ts +10 -3
  334. package/src/runtime/pending-interactions.ts +0 -1
  335. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +106 -0
  336. package/src/runtime/routes/__tests__/memory-v2-simulate-route.test.ts +25 -6
  337. package/src/runtime/routes/__tests__/plugins-routes.test.ts +512 -0
  338. package/src/runtime/routes/acp-routes.test.ts +255 -6
  339. package/src/runtime/routes/acp-routes.ts +8 -1
  340. package/src/runtime/routes/avatar-routes.ts +10 -10
  341. package/src/runtime/routes/background-wake-routes.ts +188 -0
  342. package/src/runtime/routes/browser-tabs-routes.ts +200 -0
  343. package/src/runtime/routes/btw-routes.ts +0 -6
  344. package/src/runtime/routes/conversation-cli-routes.ts +1 -1
  345. package/src/runtime/routes/conversation-list-routes.ts +12 -4
  346. package/src/runtime/routes/conversation-management-routes.ts +77 -20
  347. package/src/runtime/routes/conversation-query-routes.ts +142 -36
  348. package/src/runtime/routes/conversation-routes.ts +252 -410
  349. package/src/runtime/routes/conversation-starter-routes.ts +6 -3
  350. package/src/runtime/routes/disk-pressure-routes.ts +1 -1
  351. package/src/runtime/routes/domain-routes.ts +60 -10
  352. package/src/runtime/routes/email-routes.ts +5 -2
  353. package/src/runtime/routes/events-routes.ts +54 -10
  354. package/src/runtime/routes/group-routes.ts +24 -8
  355. package/src/runtime/routes/host-browser-routes.ts +10 -2
  356. package/src/runtime/routes/host-cu-routes.ts +2 -2
  357. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +96 -3
  358. package/src/runtime/routes/index.ts +8 -0
  359. package/src/runtime/routes/inference-profile-session-handler.ts +22 -12
  360. package/src/runtime/routes/inference-profile-session-routes.ts +7 -1
  361. package/src/runtime/routes/llm-call-sites-routes.ts +32 -5
  362. package/src/runtime/routes/memory-item-routes.ts +8 -3
  363. package/src/runtime/routes/memory-v2-routes.ts +215 -5
  364. package/src/runtime/routes/memory-v3-routes.ts +316 -0
  365. package/src/runtime/routes/migration-routes.ts +21 -24
  366. package/src/runtime/routes/plugins-routes.ts +337 -0
  367. package/src/runtime/routes/rename-conversation-routes.ts +6 -2
  368. package/src/runtime/routes/secret-routes.ts +25 -5
  369. package/src/runtime/routes/settings-routes.ts +12 -11
  370. package/src/runtime/routes/slack-channel-routes.ts +5 -4
  371. package/src/runtime/routes/workspace-routes.ts +25 -10
  372. package/src/runtime/sync/resource-sync-events.ts +106 -38
  373. package/src/runtime/sync/sync-publisher.test.ts +49 -0
  374. package/src/runtime/sync/sync-publisher.ts +2 -1
  375. package/src/runtime/verification-outbound-actions.ts +73 -1
  376. package/src/telemetry/types.ts +12 -0
  377. package/src/telemetry/usage-telemetry-reporter.test.ts +48 -0
  378. package/src/telemetry/usage-telemetry-reporter.ts +1 -0
  379. package/src/tools/acp/spawn.test.ts +119 -0
  380. package/src/tools/acp/spawn.ts +15 -2
  381. package/src/tools/apps/definitions.ts +2 -8
  382. package/src/tools/ask-question/ask-question-tool.test.ts +3 -3
  383. package/src/tools/ask-question/ask-question-tool.ts +38 -45
  384. package/src/tools/browser/__tests__/pinned-tabs.test.ts +70 -0
  385. package/src/tools/browser/browser-execution.ts +16 -3
  386. package/src/tools/browser/cdp-client/__tests__/browser-tabs-factory.test.ts +402 -0
  387. package/src/tools/browser/cdp-client/__tests__/types.test.ts +3 -0
  388. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +12 -0
  389. package/src/tools/browser/cdp-client/extension-cdp-client.ts +27 -1
  390. package/src/tools/browser/cdp-client/factory.ts +100 -17
  391. package/src/tools/browser/cdp-client/local-cdp-client.ts +12 -0
  392. package/src/tools/browser/cdp-client/types.ts +65 -0
  393. package/src/tools/browser/pinned-tabs.ts +96 -40
  394. package/src/tools/computer-use/definitions.ts +22 -78
  395. package/src/tools/credential-execution/make-authenticated-request.ts +3 -9
  396. package/src/tools/credential-execution/manage-secure-command-tool.ts +3 -9
  397. package/src/tools/credential-execution/run-authenticated-command.ts +3 -9
  398. package/src/tools/credentials/vault.ts +3 -9
  399. package/src/tools/document/document-tool.ts +59 -0
  400. package/src/tools/execution-target.ts +21 -23
  401. package/src/tools/executor.ts +6 -1
  402. package/src/tools/filesystem/edit.ts +3 -9
  403. package/src/tools/filesystem/list.ts +3 -9
  404. package/src/tools/filesystem/read.ts +3 -9
  405. package/src/tools/filesystem/write.ts +3 -9
  406. package/src/tools/host-filesystem/edit.ts +3 -9
  407. package/src/tools/host-filesystem/read.ts +3 -9
  408. package/src/tools/host-filesystem/transfer.ts +3 -9
  409. package/src/tools/host-filesystem/write.ts +3 -9
  410. package/src/tools/host-terminal/host-shell.ts +3 -9
  411. package/src/tools/mcp/mcp-tool-factory.ts +1 -8
  412. package/src/tools/memory/register.test.ts +1 -1
  413. package/src/tools/memory/register.ts +4 -9
  414. package/src/tools/network/web-fetch.ts +3 -9
  415. package/src/tools/network/web-search.ts +25 -32
  416. package/src/tools/registry.ts +7 -23
  417. package/src/tools/schema-transforms.ts +1 -1
  418. package/src/tools/skills/execute.ts +3 -9
  419. package/src/tools/skills/load.ts +3 -9
  420. package/src/tools/skills/skill-tool-factory.ts +1 -8
  421. package/src/tools/subagent/notify-parent.ts +3 -9
  422. package/src/tools/system/request-permission.ts +3 -9
  423. package/src/tools/terminal/shell.ts +3 -9
  424. package/src/tools/tool-defaults.ts +94 -0
  425. package/src/tools/types.ts +27 -98
  426. package/src/tools/ui-surface/definitions.ts +6 -22
  427. package/src/usage/pricing.ts +23 -0
  428. package/src/usage/types.ts +12 -0
  429. package/src/util/logger.ts +16 -7
  430. package/src/util/platform.ts +7 -2
  431. package/src/util/sqlite3-runtime.ts +65 -0
  432. package/src/workspace/migrations/086-revert-stale-gemini-mis-rewrites.ts +1 -0
  433. package/src/workspace/migrations/089-move-memory-tree-out-of-v3.ts +86 -0
  434. package/src/workspace/migrations/registry.ts +2 -0
  435. package/src/__tests__/compaction-strip-metadata-clear.test.ts +0 -206
  436. package/src/__tests__/message-complete-display-id.test.ts +0 -175
  437. package/src/daemon/query-complexity-router.ts +0 -75
  438. package/src/prompts/cache-boundary.ts +0 -8
@@ -21,9 +21,11 @@
21
21
  * since NOW.md only changes when the model rewrites it. We set the 1h
22
22
  * TTL explicitly here to match the provider-side breakpoints; the
23
23
  * default 5m would force unnecessary cache re-creation.
24
- * The Anthropic provider also auto-applies a 1h breakpoint on the last text
25
- * block of a turn-starting user message, so the trailing uncached block does
26
- * not need an explicit `cache_control`.
24
+ * The trailing user-message block holds `<last_turn>` content that changes
25
+ * every call (new user turn + new prior assistant reply), so we pass
26
+ * `disableTurnStartCache: true` to the provider to suppress its auto-applied
27
+ * 1h breakpoint there — caching it would create unused cache entries (pure
28
+ * cache_creation cost with no future hit).
27
29
  *
28
30
  * This module is pure orchestration — it does not mutate activation state,
29
31
  * write any files, or update the conversation. PR 10 wires it into
@@ -155,10 +157,31 @@ function emptyBatchResult(
155
157
  return { selectedSlugs: [], failureReason: reason };
156
158
  }
157
159
 
160
+ /**
161
+ * One `(assistant, user)` turn pair rendered inside `<last_turn>`. The
162
+ * pair represents the assistant's reply followed by the user message
163
+ * that came after. The most recent pair's `userMessage` is the
164
+ * just-arrived turn that triggered the router; older pairs are walked
165
+ * back from conversation history. `assistantMessage` is the empty
166
+ * string for the oldest pair when there was no prior assistant reply
167
+ * (conversation start) — `runRouterBatch` skips the `[assistant]:`
168
+ * line entirely in that case.
169
+ */
170
+ export interface RouterTurnPair {
171
+ assistantMessage: string;
172
+ userMessage: string;
173
+ }
174
+
158
175
  interface RunRouterParams {
159
176
  workspaceDir: string;
160
- userMessage: string;
161
- assistantMessage: string;
177
+ /**
178
+ * Recent assistant/user turn pairs, oldest first. Must contain at
179
+ * least one entry. The last entry's `userMessage` is the just-arrived
180
+ * user turn the router is routing for; entries before it are walked
181
+ * back from conversation history. The number of pairs the production
182
+ * caller passes is controlled by `memory.v2.router.historical_pairs`.
183
+ */
184
+ recentTurnPairs: readonly RouterTurnPair[];
162
185
  /** Verbatim contents to inject into `<now>...</now>` on this turn. */
163
186
  nowText: string;
164
187
  /** Slugs already injected on prior turns (used to seed `<already_injected_ids>`). */
@@ -172,6 +195,28 @@ interface RunRouterParams {
172
195
  * only exercise tier 1 / tier 3 paths can omit it.
173
196
  */
174
197
  database?: DrizzleDb;
198
+ /**
199
+ * Per-call profile override forwarded to `getConfiguredProvider`. When
200
+ * set, the `memoryRouter` call site resolves against this profile name
201
+ * instead of the workspace active profile. The simulator route uses
202
+ * this to compare different profiles against the same query; live
203
+ * router callers leave it unset.
204
+ */
205
+ overrideProfile?: string;
206
+ /**
207
+ * Skip the post-union truncation to `max_page_ids`. Used by the
208
+ * simulator so the playground can show the full untruncated router
209
+ * output across all batches. Live callers (`injectViaRouter`) leave
210
+ * this unset so the bounded-injection contract holds.
211
+ */
212
+ disableUnionCap?: boolean;
213
+ /**
214
+ * Per-call inline router system-prompt override. Takes precedence
215
+ * over `memory.v2.router.router_prompt_path` and the bundled body.
216
+ * Used by the simulator playground for ad-hoc prompt comparisons.
217
+ * Live callers leave this unset.
218
+ */
219
+ routerPromptOverride?: string;
175
220
  }
176
221
 
177
222
  /**
@@ -207,7 +252,11 @@ export async function runRouter(
207
252
  return emptyResult("empty_index");
208
253
  }
209
254
 
210
- const provider = await getConfiguredProvider("memoryRouter");
255
+ const provider = await getConfiguredProvider("memoryRouter", {
256
+ ...(params.overrideProfile !== undefined
257
+ ? { overrideProfile: params.overrideProfile }
258
+ : {}),
259
+ });
211
260
  if (!provider) {
212
261
  log.warn("memoryRouter provider unavailable; router skipped");
213
262
  return emptyResult("no_provider");
@@ -302,15 +351,18 @@ export async function runRouter(
302
351
  // exceed it (e.g. 10 batches × 10 selections each ≫ 25 cap). Apply a final
303
352
  // truncation so RouterResult honors the contract that injection.ts trusts.
304
353
  // Iteration order above is tier 1 → tier 2 → tier 3:0 → … so earlier-tier
305
- // slugs win the truncation.
306
- const maxPageIds = config.memory?.v2?.router?.max_page_ids ?? 25;
307
- if (selectedSlugs.length > maxPageIds) {
308
- log.warn(
309
- { unionSize: selectedSlugs.length, max: maxPageIds },
310
- "Router union across batches exceeded max_page_ids; truncating",
311
- );
312
- const dropped = selectedSlugs.splice(maxPageIds);
313
- for (const slug of dropped) sourceBySlug.delete(slug);
354
+ // slugs win the truncation. The simulator passes `disableUnionCap` so the
355
+ // playground can show the full untruncated union for analysis.
356
+ if (!params.disableUnionCap) {
357
+ const maxPageIds = config.memory?.v2?.router?.max_page_ids ?? 25;
358
+ if (selectedSlugs.length > maxPageIds) {
359
+ log.warn(
360
+ { unionSize: selectedSlugs.length, max: maxPageIds },
361
+ "Router union across batches exceeded max_page_ids; truncating",
362
+ );
363
+ const dropped = selectedSlugs.splice(maxPageIds);
364
+ for (const slug of dropped) sourceBySlug.delete(slug);
365
+ }
314
366
  }
315
367
  return { selectedSlugs, sourceBySlug, failureReason: null };
316
368
  }
@@ -331,8 +383,7 @@ async function runRouterBatch(
331
383
  ): Promise<RouterBatchResult> {
332
384
  const {
333
385
  workspaceDir,
334
- userMessage,
335
- assistantMessage,
386
+ recentTurnPairs,
336
387
  nowText,
337
388
  priorEverInjected,
338
389
  config,
@@ -349,6 +400,7 @@ async function runRouterBatch(
349
400
  userName: resolveUserName(workspaceDir),
350
401
  pageIndexBlock: batchIndex.rendered,
351
402
  },
403
+ params.routerPromptOverride ?? null,
352
404
  );
353
405
 
354
406
  // Filter prior-injected to slugs present in THIS batch and map to
@@ -360,6 +412,29 @@ async function runRouterBatch(
360
412
  if (local) priorIds.push(local.id);
361
413
  }
362
414
 
415
+ // Trim the pairs down to the configured `<last_turn>` content budget,
416
+ // newest-message-first so the just-arrived user turn keeps full claim
417
+ // on the cap and the oldest still-includable message is front-truncated
418
+ // (rather than dropping the most recent message). `null` is a no-op.
419
+ const cappedPairs = applyHistoricalCharBudget(
420
+ recentTurnPairs,
421
+ config.memory?.v2?.router?.historical_pairs_max_chars ?? null,
422
+ );
423
+
424
+ // Render `<last_turn>` chronologically: each pair emits the prior
425
+ // assistant reply followed by the user message that came after.
426
+ // `assistantMessage` is the empty string on the oldest pair when there
427
+ // was no prior assistant reply (conversation start) — skip that line
428
+ // so we don't emit a dangling `[assistant]:`.
429
+ const lastTurnLines: string[] = [];
430
+ for (const pair of cappedPairs) {
431
+ if (pair.assistantMessage.trim().length > 0) {
432
+ lastTurnLines.push(`[assistant]: ${pair.assistantMessage}`);
433
+ }
434
+ lastTurnLines.push(`[user]: ${pair.userMessage}`);
435
+ }
436
+ const lastTurnBlock = `<last_turn>\n${lastTurnLines.join("\n")}\n</last_turn>`;
437
+
363
438
  const userMsg: Message = {
364
439
  role: "user",
365
440
  content: [
@@ -368,7 +443,7 @@ async function runRouterBatch(
368
443
  type: "text",
369
444
  text:
370
445
  `<already_injected_ids>\n${priorIds.join(", ")}\n</already_injected_ids>\n\n` +
371
- `<last_turn>\n[user]: ${userMessage}\n[assistant]: ${assistantMessage}\n</last_turn>`,
446
+ lastTurnBlock,
372
447
  },
373
448
  ],
374
449
  };
@@ -386,6 +461,7 @@ async function runRouterBatch(
386
461
  config: {
387
462
  callSite: "memoryRouter" as const,
388
463
  tool_choice: { type: "tool" as const, name: ROUTER_TOOL_NAME },
464
+ disableTurnStartCache: true,
389
465
  },
390
466
  ...(signal ? { signal } : {}),
391
467
  },
@@ -453,6 +529,83 @@ async function runRouterBatch(
453
529
  return { selectedSlugs, failureReason: null };
454
530
  }
455
531
 
532
+ /** Truncation marker prepended to a front-truncated historical message. */
533
+ const HISTORICAL_TRUNCATION_MARKER = "…";
534
+
535
+ /**
536
+ * Apply the `<last_turn>` content character budget to a chronological
537
+ * pairs array. The just-arrived user message has first claim on the
538
+ * budget; older messages are added newest-first until exhausted. The
539
+ * oldest still-includable message is front-truncated with a leading
540
+ * `…` so it joins coherently with the next message in time. Older pairs
541
+ * whose content doesn't fit are dropped entirely.
542
+ *
543
+ * Counts message content only — framing characters (`[assistant]: `,
544
+ * `[user]: `, newlines) are not deducted from the budget. The cap is a
545
+ * conservative upper bound on the dialogue content surfaced to the
546
+ * router, not on the exact rendered block size.
547
+ *
548
+ * Exported for tests; production calls it via `runRouterBatch`.
549
+ */
550
+ export function applyHistoricalCharBudget(
551
+ pairs: readonly RouterTurnPair[],
552
+ maxChars: number | null,
553
+ ): RouterTurnPair[] {
554
+ if (maxChars === null || maxChars <= 0) return [...pairs];
555
+
556
+ type WalkedMsg = {
557
+ role: "user" | "assistant";
558
+ text: string;
559
+ pairIdx: number;
560
+ };
561
+ // Walk every message newest-first. Within a single pair the user
562
+ // message came AFTER the assistant message chronologically, so the
563
+ // user line gets first claim on the budget.
564
+ const walked: WalkedMsg[] = [];
565
+ for (let i = pairs.length - 1; i >= 0; i--) {
566
+ walked.push({ role: "user", text: pairs[i].userMessage, pairIdx: i });
567
+ walked.push({
568
+ role: "assistant",
569
+ text: pairs[i].assistantMessage,
570
+ pairIdx: i,
571
+ });
572
+ }
573
+
574
+ let used = 0;
575
+ const included = new Map<number, { assistant: string; user: string }>();
576
+ for (const msg of walked) {
577
+ const remaining = maxChars - used;
578
+ if (remaining <= 0) break;
579
+ let textToInclude: string;
580
+ let stop = false;
581
+ if (msg.text.length <= remaining) {
582
+ textToInclude = msg.text;
583
+ used += msg.text.length;
584
+ } else {
585
+ // Front-truncate so the surviving suffix of an older message
586
+ // connects to the next message (in chronological order) without
587
+ // a syntactic seam. The marker counts toward the budget so the
588
+ // emitted text never exceeds `maxChars` cumulatively.
589
+ if (remaining <= HISTORICAL_TRUNCATION_MARKER.length) break;
590
+ const keepChars = remaining - HISTORICAL_TRUNCATION_MARKER.length;
591
+ textToInclude = HISTORICAL_TRUNCATION_MARKER + msg.text.slice(-keepChars);
592
+ used = maxChars;
593
+ stop = true;
594
+ }
595
+ const slot = included.get(msg.pairIdx) ?? { assistant: "", user: "" };
596
+ if (msg.role === "user") slot.user = textToInclude;
597
+ else slot.assistant = textToInclude;
598
+ included.set(msg.pairIdx, slot);
599
+ if (stop) break;
600
+ }
601
+
602
+ const sortedIdxs = [...included.keys()].sort((a, b) => a - b);
603
+ return sortedIdxs.map((idx) => {
604
+ const slot = included.get(idx)!;
605
+ return { assistantMessage: slot.assistant, userMessage: slot.user };
606
+ });
607
+ }
608
+
456
609
  /**
457
610
  * Build a text content block carrying an ephemeral `cache_control`
458
611
  * breakpoint with a 1h TTL. The Anthropic SDK accepts the field as an extra
@@ -0,0 +1,422 @@
1
+ /**
2
+ * Tests for `assistant/src/memory/v3/coactivation-store.ts`, its sibling
3
+ * migration `262-memory-v3-coactivation.ts`, and the loop's co-activation
4
+ * emission (`loop.ts`, gated by `config.memory.v3.write.coactivation`).
5
+ *
6
+ * Coverage:
7
+ * - Migration creates the table + both indexes; safe to re-run.
8
+ * - recordCoactivations / readCoactivations round-trip; empty list is a
9
+ * no-op; `since` filters by created_at.
10
+ * - A scripted 2-pass loop emits the expected pass-1 → pass-2 rows with the
11
+ * correct pass_gap when the flag is on, and nothing when it is off.
12
+ *
13
+ * Uses an in-memory bun:sqlite database — no real workspace DB. The loop's
14
+ * lane modules are stubbed via `mock.module`, matching `loop.test.ts`.
15
+ */
16
+
17
+ import { Database } from "bun:sqlite";
18
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
19
+
20
+ import { drizzle } from "drizzle-orm/bun-sqlite";
21
+
22
+ import { makeMockLogger } from "../../../__tests__/helpers/mock-logger.js";
23
+
24
+ mock.module("../../../util/logger.js", () => ({
25
+ getLogger: () => makeMockLogger(),
26
+ }));
27
+
28
+ import type { DrizzleDb } from "../../db-connection.js";
29
+ import { getSqliteFrom } from "../../db-connection.js";
30
+ import {
31
+ downMemoryV3Coactivation,
32
+ migrateMemoryV3Coactivation,
33
+ } from "../../migrations/262-memory-v3-coactivation.js";
34
+ import * as schema from "../../schema.js";
35
+ import type {
36
+ RetrievalInput,
37
+ RetrievalOutput,
38
+ } from "../../v2/harness/retriever.js";
39
+ import type { GateDecision, ScoutResult } from "../../v2/harness/trace.js";
40
+ import {
41
+ type CoactivationRow,
42
+ readCoactivations,
43
+ recordCoactivations,
44
+ } from "../coactivation-store.js";
45
+
46
+ // memory_checkpoints is required by withCrashRecovery and is normally created
47
+ // by an early core migration. Stand it up by hand so the v3 migration can run
48
+ // in isolation against a fresh in-memory DB.
49
+ const CHECKPOINTS_DDL = /*sql*/ `
50
+ CREATE TABLE memory_checkpoints (
51
+ key TEXT PRIMARY KEY,
52
+ value TEXT NOT NULL,
53
+ updated_at INTEGER NOT NULL
54
+ )
55
+ `;
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // Loop lane stubs — installed before importing the module under test. Mirrors
59
+ // loop.test.ts: each test rewires the `lane` refs before calling the loop.
60
+ // ---------------------------------------------------------------------------
61
+
62
+ interface RunScoutsResult {
63
+ scouts: ScoutResult[];
64
+ sticky: Set<string>;
65
+ bypass: Set<string>;
66
+ }
67
+ interface FilterResult {
68
+ kept: string[];
69
+ trace: { judged: string[]; dropped: string[] };
70
+ failureReason?: string;
71
+ }
72
+ interface WalkResult {
73
+ pages: Set<string>;
74
+ levels: Array<{
75
+ node: string;
76
+ considered: string[];
77
+ descended: string[];
78
+ skipped: string[];
79
+ reasoning: string;
80
+ }>;
81
+ }
82
+ interface ExpandResult {
83
+ pulled: Set<string>;
84
+ expansions: Array<{ from: string; pulled: string[] }>;
85
+ }
86
+ interface GateResult {
87
+ decision: GateDecision;
88
+ selectedSlugs: string[];
89
+ }
90
+
91
+ const lane = {
92
+ scouts: [] as RunScoutsResult[],
93
+ filter: [] as FilterResult[],
94
+ walk: [] as WalkResult[],
95
+ edges: [] as ExpandResult[],
96
+ gate: [] as GateResult[],
97
+ };
98
+
99
+ function nextOf<T>(list: T[], index: number): T {
100
+ return list[Math.min(index, list.length - 1)];
101
+ }
102
+
103
+ let scoutCallCount = 0;
104
+ let filterCallCount = 0;
105
+ let walkCallCount = 0;
106
+ let edgeCallCount = 0;
107
+ let gateCallCount = 0;
108
+
109
+ mock.module("../scouts.js", () => ({
110
+ runScouts: async (): Promise<RunScoutsResult> =>
111
+ nextOf(lane.scouts, scoutCallCount++),
112
+ }));
113
+ mock.module("../filter.js", () => ({
114
+ filterDenseHits: async (): Promise<FilterResult> =>
115
+ nextOf(lane.filter, filterCallCount++),
116
+ }));
117
+ mock.module("../tree-walk.js", () => ({
118
+ runTreeWalk: async (): Promise<WalkResult> =>
119
+ nextOf(lane.walk, walkCallCount++),
120
+ }));
121
+ mock.module("../edges.js", () => ({
122
+ expandEdges: async (): Promise<ExpandResult> =>
123
+ nextOf(lane.edges, edgeCallCount++),
124
+ }));
125
+ mock.module("../gate.js", () => ({
126
+ runGate: async (): Promise<GateResult> => nextOf(lane.gate, gateCallCount++),
127
+ }));
128
+ mock.module("../tree-index.js", () => ({
129
+ getTreeIndex: async () => ({
130
+ nodes: new Map(),
131
+ childrenByNode: new Map(),
132
+ parentsByNode: new Map(),
133
+ pageParents: new Map(),
134
+ root: "_root",
135
+ }),
136
+ }));
137
+ mock.module("../../v2/page-index.js", () => ({
138
+ getPageIndex: async () => ({
139
+ entries: [],
140
+ bySlug: new Map(),
141
+ byId: new Map(),
142
+ rendered: "",
143
+ }),
144
+ }));
145
+
146
+ const { runRetrievalLoop } = await import("../loop.js");
147
+
148
+ let sqlite: Database;
149
+ let database: DrizzleDb;
150
+
151
+ beforeEach(() => {
152
+ sqlite = new Database(":memory:");
153
+ database = drizzle(sqlite, { schema });
154
+ getSqliteFrom(database).exec(CHECKPOINTS_DDL);
155
+ migrateMemoryV3Coactivation(database);
156
+
157
+ lane.scouts = [];
158
+ lane.filter = [];
159
+ lane.walk = [];
160
+ lane.edges = [];
161
+ lane.gate = [];
162
+ scoutCallCount = 0;
163
+ filterCallCount = 0;
164
+ walkCallCount = 0;
165
+ edgeCallCount = 0;
166
+ gateCallCount = 0;
167
+ });
168
+
169
+ afterEach(() => {
170
+ sqlite.close();
171
+ });
172
+
173
+ function scout(laneName: ScoutResult["lane"], slugs: string[]): ScoutResult {
174
+ return { lane: laneName, slugs };
175
+ }
176
+
177
+ function makeInput(opts?: {
178
+ passCap?: number;
179
+ coactivation?: boolean;
180
+ }): RetrievalInput {
181
+ return {
182
+ workspaceDir: "/tmp/does-not-matter",
183
+ recentTurnPairs: [],
184
+ nowText: "NOW",
185
+ priorEverInjected: [],
186
+ config: {
187
+ memory: {
188
+ v3: {
189
+ passCap: opts?.passCap ?? 3,
190
+ lanes: {
191
+ hot: true,
192
+ sparse: true,
193
+ dense: true,
194
+ tree: true,
195
+ edges: true,
196
+ },
197
+ write: {
198
+ enabled: false,
199
+ consolidateIntervalMs: 3600000,
200
+ coactivation: opts?.coactivation ?? false,
201
+ },
202
+ },
203
+ },
204
+ } as unknown as RetrievalInput["config"],
205
+ };
206
+ }
207
+
208
+ // ---------------------------------------------------------------------------
209
+ // Migration.
210
+ // ---------------------------------------------------------------------------
211
+
212
+ describe("migrateMemoryV3Coactivation", () => {
213
+ test("creates table and both indexes; safe to re-run", () => {
214
+ migrateMemoryV3Coactivation(database);
215
+ migrateMemoryV3Coactivation(database);
216
+
217
+ const raw = getSqliteFrom(database);
218
+ const table = raw
219
+ .query(
220
+ `SELECT name FROM sqlite_master WHERE type='table' AND name='memory_v3_coactivation'`,
221
+ )
222
+ .get();
223
+ expect(table).toBeTruthy();
224
+
225
+ const indexNames = new Set(
226
+ (
227
+ raw
228
+ .query(
229
+ `SELECT name FROM sqlite_master WHERE type='index' AND tbl_name='memory_v3_coactivation'`,
230
+ )
231
+ .all() as Array<{ name: string }>
232
+ ).map((r) => r.name),
233
+ );
234
+ expect(indexNames.has("idx_memory_v3_coactivation_pair")).toBe(true);
235
+ expect(indexNames.has("idx_memory_v3_coactivation_time")).toBe(true);
236
+ });
237
+
238
+ test("downMemoryV3Coactivation drops the table", () => {
239
+ downMemoryV3Coactivation(database);
240
+ const table = getSqliteFrom(database)
241
+ .query(
242
+ `SELECT name FROM sqlite_master WHERE type='table' AND name='memory_v3_coactivation'`,
243
+ )
244
+ .get();
245
+ expect(table).toBeFalsy();
246
+ });
247
+ });
248
+
249
+ // ---------------------------------------------------------------------------
250
+ // Store.
251
+ // ---------------------------------------------------------------------------
252
+
253
+ describe("recordCoactivations / readCoactivations", () => {
254
+ test("round-trips rows oldest-first", () => {
255
+ const rows: CoactivationRow[] = [
256
+ {
257
+ conversationId: "conv-1",
258
+ turn: 3,
259
+ sourceSlug: "alice",
260
+ targetSlug: "bob",
261
+ passGap: 1,
262
+ used: 0,
263
+ createdAt: 1_000,
264
+ },
265
+ {
266
+ conversationId: "conv-1",
267
+ turn: 3,
268
+ sourceSlug: "alice",
269
+ targetSlug: "carol",
270
+ passGap: 2,
271
+ used: 0,
272
+ createdAt: 2_000,
273
+ },
274
+ ];
275
+ recordCoactivations(database, rows);
276
+
277
+ const read = readCoactivations(database);
278
+ expect(read).toHaveLength(2);
279
+ expect(read[0]).toMatchObject({
280
+ conversationId: "conv-1",
281
+ turn: 3,
282
+ sourceSlug: "alice",
283
+ targetSlug: "bob",
284
+ passGap: 1,
285
+ used: 0,
286
+ createdAt: 1_000,
287
+ });
288
+ expect(read[1].targetSlug).toBe("carol");
289
+ expect(read[1].passGap).toBe(2);
290
+ });
291
+
292
+ test("empty list is a no-op", () => {
293
+ recordCoactivations(database, []);
294
+ expect(readCoactivations(database)).toHaveLength(0);
295
+ });
296
+
297
+ test("since filters by created_at", () => {
298
+ recordCoactivations(database, [
299
+ {
300
+ conversationId: "c",
301
+ turn: 1,
302
+ sourceSlug: "a",
303
+ targetSlug: "b",
304
+ passGap: 1,
305
+ used: 0,
306
+ createdAt: 100,
307
+ },
308
+ {
309
+ conversationId: "c",
310
+ turn: 1,
311
+ sourceSlug: "a",
312
+ targetSlug: "c",
313
+ passGap: 1,
314
+ used: 0,
315
+ createdAt: 500,
316
+ },
317
+ ]);
318
+ const recent = readCoactivations(database, 300);
319
+ expect(recent).toHaveLength(1);
320
+ expect(recent[0].targetSlug).toBe("c");
321
+ });
322
+ });
323
+
324
+ // ---------------------------------------------------------------------------
325
+ // Loop emission.
326
+ // ---------------------------------------------------------------------------
327
+
328
+ describe("runRetrievalLoop — co-activation emission", () => {
329
+ /**
330
+ * Script a 2-pass loop: pass 1 surfaces `a` (hot) + `b` (sparse); pass 2
331
+ * surfaces `c` (dense). The gate says "more" on pass 1 (selecting a, b) and
332
+ * "ready" on pass 2 (selecting a, b, c). So `c` is the only pass-2 target,
333
+ * paired with pass-1 hits a and b → two rows, both pass_gap=1.
334
+ */
335
+ function scriptTwoPass(): void {
336
+ lane.scouts = [
337
+ {
338
+ scouts: [scout("hot", ["a"]), scout("sparse", ["b"])],
339
+ sticky: new Set(),
340
+ bypass: new Set(),
341
+ },
342
+ {
343
+ scouts: [scout("dense", ["c"])],
344
+ sticky: new Set(),
345
+ bypass: new Set(),
346
+ },
347
+ ];
348
+ // Pass 1 has no dense scout, so the filter is only called on pass 2 (one
349
+ // filter call per dense pass) — its single entry keeps `c`.
350
+ lane.filter = [{ kept: ["c"], trace: { judged: ["c"], dropped: [] } }];
351
+ lane.walk = [
352
+ { pages: new Set(), levels: [] },
353
+ { pages: new Set(), levels: [] },
354
+ ];
355
+ lane.edges = [
356
+ { pulled: new Set(), expansions: [] },
357
+ { pulled: new Set(), expansions: [] },
358
+ ];
359
+ lane.gate = [
360
+ {
361
+ decision: { decision: "more", questions: ["q"] },
362
+ selectedSlugs: ["a", "b"],
363
+ },
364
+ { decision: { decision: "ready" }, selectedSlugs: ["a", "b", "c"] },
365
+ ];
366
+ }
367
+
368
+ test("emits pass-1 → pass-2 rows with correct pass_gap when flag is on", async () => {
369
+ scriptTwoPass();
370
+ const out: RetrievalOutput = await runRetrievalLoop(
371
+ makeInput({ passCap: 3, coactivation: true }),
372
+ { db: database, conversationId: "conv-42", turn: 7 },
373
+ );
374
+ expect(out.selectedSlugs).toEqual(["a", "b", "c"]);
375
+
376
+ const rows = readCoactivations(database);
377
+ // c (pass 2) paired with each pass-1 hit a and b → two rows.
378
+ expect(rows).toHaveLength(2);
379
+ const pairs = rows.map((r) => `${r.sourceSlug}->${r.targetSlug}`).sort();
380
+ expect(pairs).toEqual(["a->c", "b->c"]);
381
+ for (const r of rows) {
382
+ expect(r.targetSlug).toBe("c");
383
+ expect(r.passGap).toBe(1);
384
+ expect(r.used).toBe(0);
385
+ expect(r.conversationId).toBe("conv-42");
386
+ expect(r.turn).toBe(7);
387
+ }
388
+ });
389
+
390
+ test("emits nothing when the flag is off", async () => {
391
+ scriptTwoPass();
392
+ await runRetrievalLoop(makeInput({ passCap: 3, coactivation: false }), {
393
+ db: database,
394
+ conversationId: "conv-42",
395
+ turn: 7,
396
+ });
397
+ expect(readCoactivations(database)).toHaveLength(0);
398
+ });
399
+
400
+ test("single-pass selection emits nothing (no later-surfaced target)", async () => {
401
+ lane.scouts = [
402
+ {
403
+ scouts: [scout("hot", ["a"]), scout("sparse", ["b"])],
404
+ sticky: new Set(),
405
+ bypass: new Set(),
406
+ },
407
+ ];
408
+ lane.filter = [{ kept: [], trace: { judged: [], dropped: [] } }];
409
+ lane.walk = [{ pages: new Set(), levels: [] }];
410
+ lane.edges = [{ pulled: new Set(), expansions: [] }];
411
+ lane.gate = [
412
+ { decision: { decision: "ready" }, selectedSlugs: ["a", "b"] },
413
+ ];
414
+
415
+ await runRetrievalLoop(makeInput({ passCap: 3, coactivation: true }), {
416
+ db: database,
417
+ conversationId: "conv-1",
418
+ turn: 1,
419
+ });
420
+ expect(readCoactivations(database)).toHaveLength(0);
421
+ });
422
+ });