@vellumai/assistant 0.7.3 → 0.8.1

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 (778) hide show
  1. package/AGENTS.md +11 -0
  2. package/ARCHITECTURE.md +29 -28
  3. package/Dockerfile +6 -4
  4. package/README.md +2 -2
  5. package/__tests__/permissions/gateway-threshold-reader.test.ts +236 -9
  6. package/bun.lock +3 -0
  7. package/docker-entrypoint.sh +16 -0
  8. package/eslint-rules/__tests__/cli-no-daemon-internals.test.ts +420 -0
  9. package/eslint-rules/cli-no-daemon-internals.js +283 -0
  10. package/eslint.config.mjs +12 -0
  11. package/knip.json +3 -1
  12. package/node_modules/@vellumai/ipc-server-utils/bun.lock +24 -0
  13. package/node_modules/@vellumai/ipc-server-utils/package.json +18 -0
  14. package/node_modules/@vellumai/ipc-server-utils/src/index.ts +6 -0
  15. package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.test.ts +430 -0
  16. package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.ts +221 -0
  17. package/node_modules/@vellumai/ipc-server-utils/tsconfig.json +20 -0
  18. package/node_modules/@vellumai/skill-host-contracts/src/client.ts +10 -1
  19. package/openapi.yaml +4126 -959
  20. package/package.json +5 -1
  21. package/scripts/generate-openapi.ts +52 -4
  22. package/scripts/sync-llm-catalog.ts +165 -0
  23. package/scripts/sync-web-search-catalog.ts +107 -0
  24. package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +169 -0
  25. package/src/__tests__/agent-loop-override-profile.test.ts +26 -1
  26. package/src/__tests__/annotate-risk-options.test.ts +291 -0
  27. package/src/__tests__/anthropic-provider.test.ts +92 -2
  28. package/src/__tests__/app-control-flow.test.ts +7 -0
  29. package/src/__tests__/approval-cascade.test.ts +8 -16
  30. package/src/__tests__/approval-routes-http.test.ts +6 -0
  31. package/src/__tests__/assistant-events-sse-shed.test.ts +232 -0
  32. package/src/__tests__/auto-analysis-end-to-end.test.ts +12 -25
  33. package/src/__tests__/avatar-identity-sync.test.ts +87 -0
  34. package/src/__tests__/background-workers-disk-pressure.test.ts +11 -22
  35. package/src/__tests__/btw-routes.test.ts +1 -0
  36. package/src/__tests__/call-constants.test.ts +10 -1
  37. package/src/__tests__/call-controller.test.ts +127 -0
  38. package/src/__tests__/call-site-routing-provider.test.ts +172 -45
  39. package/src/__tests__/cancel-resolves-conversation-key.test.ts +44 -3
  40. package/src/__tests__/channel-policy.test.ts +12 -0
  41. package/src/__tests__/checker.test.ts +89 -0
  42. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +88 -30
  43. package/src/__tests__/compact-event-conversation-id-guard.test.ts +33 -5
  44. package/src/__tests__/compaction-strip-metadata-clear.test.ts +26 -1
  45. package/src/__tests__/config-loader-backfill.test.ts +526 -102
  46. package/src/__tests__/config-loader-corrupt.test.ts +68 -0
  47. package/src/__tests__/config-loader-platform-defaults.test.ts +345 -8
  48. package/src/__tests__/config-schema-cmd.test.ts +63 -29
  49. package/src/__tests__/config-schema.test.ts +14 -3
  50. package/src/__tests__/config-set-platform-guard.test.ts +75 -152
  51. package/src/__tests__/config-set-route.test.ts +198 -0
  52. package/src/__tests__/config-watcher.test.ts +6 -0
  53. package/src/__tests__/contacts-tools.test.ts +51 -199
  54. package/src/__tests__/context-search-agent-protocol.test.ts +21 -2
  55. package/src/__tests__/context-search-agent-runner.test.ts +22 -138
  56. package/src/__tests__/context-search-conversations-source.test.ts +42 -16
  57. package/src/__tests__/context-search-fanout.test.ts +20 -157
  58. package/src/__tests__/context-search-memory-source.test.ts +3 -26
  59. package/src/__tests__/context-search-memory-v2-source.test.ts +3 -3
  60. package/src/__tests__/context-search-types.test.ts +7 -2
  61. package/src/__tests__/context-window-manager.test.ts +389 -1
  62. package/src/__tests__/conversation-abort-tool-results.test.ts +1 -6
  63. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -1
  64. package/src/__tests__/conversation-agent-loop-overflow.test.ts +2 -1
  65. package/src/__tests__/conversation-agent-loop.test.ts +3 -3
  66. package/src/__tests__/conversation-confirmation-signals.test.ts +5 -13
  67. package/src/__tests__/conversation-crud-inference-profile.test.ts +100 -0
  68. package/src/__tests__/conversation-error.test.ts +38 -0
  69. package/src/__tests__/conversation-fork-crud.test.ts +241 -1
  70. package/src/__tests__/conversation-inference-profile-route.test.ts +14 -14
  71. package/src/__tests__/conversation-init.benchmark.test.ts +2 -1
  72. package/src/__tests__/conversation-lifecycle.test.ts +124 -0
  73. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +100 -1
  74. package/src/__tests__/conversation-process-callsite.test.ts +22 -7
  75. package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -6
  76. package/src/__tests__/conversation-runtime-assembly.test.ts +19 -10
  77. package/src/__tests__/conversation-slash-commands.test.ts +194 -2
  78. package/src/__tests__/conversation-slash-unknown.test.ts +1 -6
  79. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +170 -9
  80. package/src/__tests__/conversation-surfaces-app-control.test.ts +323 -3
  81. package/src/__tests__/conversation-surfaces-data-persist.test.ts +73 -1
  82. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +59 -0
  83. package/src/__tests__/conversation-workspace-injection.test.ts +1 -7
  84. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -7
  85. package/src/__tests__/credential-security-invariants.test.ts +5 -6
  86. package/src/__tests__/daemon-credential-client.test.ts +56 -1
  87. package/src/__tests__/db-activation-state-fk-cascade.test.ts +132 -0
  88. package/src/__tests__/db-conversation-inference-profile-migration.test.ts +37 -0
  89. package/src/__tests__/db-memory-graph-event-date-repair.test.ts +43 -20
  90. package/src/__tests__/db-proxy-transaction.test.ts +206 -0
  91. package/src/__tests__/external-plugin-loader.test.ts +458 -0
  92. package/src/__tests__/filing-service.test.ts +25 -22
  93. package/src/__tests__/fixtures/mock-chrome-extension.ts +5 -0
  94. package/src/__tests__/gateway-only-guard.test.ts +0 -1
  95. package/src/__tests__/graph-extraction-event-date.test.ts +34 -0
  96. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +10 -34
  97. package/src/__tests__/heartbeat-disk-pressure.test.ts +21 -8
  98. package/src/__tests__/heartbeat-service.test.ts +50 -233
  99. package/src/__tests__/history-repair.test.ts +89 -0
  100. package/src/__tests__/host-app-control-proxy.test.ts +109 -1
  101. package/src/__tests__/host-app-control-routes.test.ts +247 -1
  102. package/src/__tests__/host-browser-proxy.test.ts +416 -20
  103. package/src/__tests__/host-browser-routes.test.ts +325 -33
  104. package/src/__tests__/host-proxy-preactivation.test.ts +211 -0
  105. package/src/__tests__/inference-no-mode-boot-e2e.test.ts +246 -0
  106. package/src/__tests__/inference-profile-reaper.test.ts +154 -0
  107. package/src/__tests__/inference-profile-session-handler.test.ts +398 -0
  108. package/src/__tests__/inference-profile-session-ipc.test.ts +236 -0
  109. package/src/__tests__/injector-chain.test.ts +24 -16
  110. package/src/__tests__/injector-pkb-v2-silenced.test.ts +10 -7
  111. package/src/__tests__/inline-skill-load-permissions.test.ts +6 -1
  112. package/src/__tests__/install-skill-routing.test.ts +2 -2
  113. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +169 -67
  114. package/src/__tests__/llm-callsite-catalog.test.ts +20 -1
  115. package/src/__tests__/llm-catalog-parity.test.ts +146 -0
  116. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +188 -0
  117. package/src/__tests__/llm-request-log-source-factory.test.ts +124 -0
  118. package/src/__tests__/llm-resolver.test.ts +46 -0
  119. package/src/__tests__/managed-profile-guard.test.ts +131 -2
  120. package/src/__tests__/mcp-auth-routes.test.ts +1 -0
  121. package/src/__tests__/mcp-cli.test.ts +182 -220
  122. package/src/__tests__/mcp-health-check.test.ts +56 -27
  123. package/src/__tests__/memory-jobs-worker-lanes.test.ts +18 -11
  124. package/src/__tests__/message-complete-display-id.test.ts +175 -0
  125. package/src/__tests__/notification-decision-fallback.test.ts +91 -0
  126. package/src/__tests__/notification-decision-strategy.test.ts +22 -0
  127. package/src/__tests__/notification-platform-adapter.test.ts +229 -0
  128. package/src/__tests__/oauth-cli.test.ts +38 -1888
  129. package/src/__tests__/oauth-commands-routes.test.ts +711 -0
  130. package/src/__tests__/oauth-connect-routes.test.ts +174 -11
  131. package/src/__tests__/oauth-providers-routes.test.ts +14 -10
  132. package/src/__tests__/openai-responses-cutover-guard.test.ts +33 -12
  133. package/src/__tests__/openai-responses-provider.test.ts +17 -0
  134. package/src/__tests__/plugin-bootstrap.test.ts +31 -2
  135. package/src/__tests__/plugin-route-contribution.test.ts +31 -3
  136. package/src/__tests__/plugin-tool-contribution.test.ts +31 -3
  137. package/src/__tests__/plugin-types.test.ts +13 -11
  138. package/src/__tests__/process-message-background-slack.test.ts +46 -0
  139. package/src/__tests__/profile-entry-status.test.ts +43 -0
  140. package/src/__tests__/provider-managed-proxy-integration.test.ts +12 -4
  141. package/src/__tests__/provider-registry-ollama.test.ts +12 -4
  142. package/src/__tests__/provider-send-message-override-profile.test.ts +10 -4
  143. package/src/__tests__/relay-server.test.ts +164 -2
  144. package/src/__tests__/retry-thinking-tool-choice.test.ts +15 -0
  145. package/src/__tests__/schedule-retry.test.ts +56 -4
  146. package/src/__tests__/schedule-routes.test.ts +104 -0
  147. package/src/__tests__/scheduler-disk-pressure.test.ts +0 -4
  148. package/src/__tests__/scheduler-recurrence.test.ts +87 -34
  149. package/src/__tests__/scheduler-reuse-conversation.test.ts +161 -5
  150. package/src/__tests__/scheduler-wake.test.ts +0 -63
  151. package/src/__tests__/secret-allowlist.test.ts +1 -0
  152. package/src/__tests__/secret-prompt-log-hygiene.test.ts +7 -5
  153. package/src/__tests__/secret-prompter-channel-fallback.test.ts +7 -5
  154. package/src/__tests__/secret-response-routing.test.ts +7 -5
  155. package/src/__tests__/secret-routes-managed-proxy.test.ts +12 -4
  156. package/src/__tests__/server-history-render.test.ts +82 -0
  157. package/src/__tests__/shell-credential-ref.test.ts +95 -3
  158. package/src/__tests__/shell-tool-proxy-mode.test.ts +14 -0
  159. package/src/__tests__/skill-include-graph.test.ts +31 -0
  160. package/src/__tests__/skill-load-feature-flag.test.ts +1 -0
  161. package/src/__tests__/skill-load-tool.test.ts +42 -16
  162. package/src/__tests__/skills.test.ts +39 -0
  163. package/src/__tests__/subagent-call-site-routing.test.ts +78 -16
  164. package/src/__tests__/suggestion-routes.test.ts +3 -3
  165. package/src/__tests__/sync-message-contract.test.ts +63 -0
  166. package/src/__tests__/task-scheduler.test.ts +88 -23
  167. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -42
  168. package/src/__tests__/tool-executor.test.ts +155 -0
  169. package/src/__tests__/update-bulletin-job.test.ts +96 -193
  170. package/src/__tests__/usage-cli.test.ts +11 -73
  171. package/src/__tests__/user-plugin-loader.test.ts +145 -0
  172. package/src/__tests__/vercel-config.test.ts +168 -0
  173. package/src/__tests__/voice-session-bridge.test.ts +3 -0
  174. package/src/__tests__/web-search-catalog-parity.test.ts +86 -0
  175. package/src/__tests__/web-search.test.ts +303 -2
  176. package/src/__tests__/workspace-migration-039-drop-legacy-llm-keys.test.ts +1 -21
  177. package/src/__tests__/workspace-migration-057-repair-stale-gemini-model-ids.test.ts +58 -0
  178. package/src/__tests__/workspace-migration-069-seed-onboarding-threads.test.ts +153 -0
  179. package/src/__tests__/workspace-migration-071-remove-safe-storage-release-note.test.ts +206 -0
  180. package/src/__tests__/workspace-migration-072-seed-reply-suggestion-callsite.test.ts +191 -0
  181. package/src/__tests__/workspace-migration-076-drop-services-inference-mode.test.ts +211 -0
  182. package/src/__tests__/workspace-migration-077-seed-memory-router-callsite.test.ts +174 -0
  183. package/src/__tests__/workspace-migration-079-home-feed-notification-only.test.ts +323 -0
  184. package/src/__tests__/workspace-migration-080-restrict-vercel-api-token-metadata.test.ts +299 -0
  185. package/src/__tests__/workspace-migration-081-backfill-bash-allowed-tools.test.ts +410 -0
  186. package/src/__tests__/workspace-migration-082-backfill-managed-profile-labels.test.ts +268 -0
  187. package/src/__tests__/workspace-migration-safe-storage-limits-release.test.ts +15 -27
  188. package/src/__tests__/workspace-migration-unify-llm-callsite-configs.test.ts +3 -3
  189. package/src/__tests__/workspace-release-notes-feature-flag-guard.test.ts +115 -0
  190. package/src/acp/__tests__/helpers/which-stub.ts +4 -2
  191. package/src/acp/resolve-agent.test.ts +25 -0
  192. package/src/acp/resolve-agent.ts +13 -2
  193. package/src/acp/session-manager.ts +14 -0
  194. package/src/agent/loop.ts +11 -0
  195. package/src/approvals/guardian-decision-primitive.ts +0 -13
  196. package/src/approvals/guardian-request-resolvers.ts +19 -102
  197. package/src/calls/call-constants.ts +5 -8
  198. package/src/calls/call-controller.ts +130 -67
  199. package/src/calls/relay-server.ts +42 -1
  200. package/src/calls/relay-setup-router.ts +36 -0
  201. package/src/calls/types.ts +1 -0
  202. package/src/calls/voice-session-bridge.ts +24 -5
  203. package/src/channels/config.ts +14 -1
  204. package/src/channels/types.ts +1 -0
  205. package/src/cli/AGENTS.md +164 -4
  206. package/src/cli/__tests__/notifications.test.ts +54 -0
  207. package/src/cli/commands/__tests__/avatar.test.ts +540 -0
  208. package/src/cli/commands/__tests__/backup.test.ts +236 -776
  209. package/src/cli/commands/__tests__/cache.test.ts +1 -1
  210. package/src/cli/commands/__tests__/changelog.test.ts +593 -0
  211. package/src/cli/commands/__tests__/channel-verification-sessions.test.ts +503 -0
  212. package/src/cli/commands/__tests__/conversations-import.test.ts +515 -0
  213. package/src/cli/commands/__tests__/domain-register.test.ts +140 -167
  214. package/src/cli/commands/__tests__/domain-status.test.ts +137 -76
  215. package/src/cli/commands/__tests__/email-attachment.test.ts +314 -337
  216. package/src/cli/commands/__tests__/email-core.test.ts +579 -0
  217. package/src/cli/commands/__tests__/image-generation.test.ts +87 -824
  218. package/src/cli/commands/__tests__/inference-send.test.ts +30 -266
  219. package/src/cli/commands/__tests__/inference-session.test.ts +423 -0
  220. package/src/cli/commands/__tests__/memory-v2.test.ts +81 -110
  221. package/src/cli/commands/__tests__/skills.test.ts +563 -0
  222. package/src/cli/commands/__tests__/status.test.ts +249 -0
  223. package/src/cli/commands/__tests__/stt.test.ts +320 -0
  224. package/src/cli/commands/__tests__/tts-synthesize.test.ts +4 -603
  225. package/src/cli/commands/__tests__/tts.test.ts +321 -0
  226. package/src/cli/commands/__tests__/webhooks.test.ts +86 -511
  227. package/src/cli/commands/attachment.ts +8 -3
  228. package/src/cli/commands/audit.ts +95 -64
  229. package/src/cli/commands/auth.ts +61 -58
  230. package/src/cli/commands/avatar.ts +276 -390
  231. package/src/cli/commands/backup.ts +409 -505
  232. package/src/cli/commands/bash.ts +9 -5
  233. package/src/cli/commands/browser.ts +28 -9
  234. package/src/cli/commands/cache.ts +9 -4
  235. package/src/cli/commands/changelog.ts +414 -0
  236. package/src/cli/commands/channel-verification-sessions.ts +238 -317
  237. package/src/cli/commands/clients.ts +8 -3
  238. package/src/cli/commands/completions.ts +9 -9
  239. package/src/cli/commands/config.ts +102 -72
  240. package/src/cli/commands/contacts.ts +575 -696
  241. package/src/cli/commands/conversations-defer.ts +17 -69
  242. package/src/cli/commands/conversations-import.ts +90 -253
  243. package/src/cli/commands/conversations.ts +346 -436
  244. package/src/cli/commands/credential-execution.ts +9 -6
  245. package/src/cli/commands/credentials.ts +456 -736
  246. package/src/cli/commands/domain.ts +128 -206
  247. package/src/cli/commands/email.ts +606 -794
  248. package/src/cli/commands/gateway.ts +8 -1
  249. package/src/cli/commands/image-generation.ts +157 -205
  250. package/src/cli/commands/inference-providers.ts +352 -0
  251. package/src/cli/commands/inference-session.ts +415 -0
  252. package/src/cli/commands/inference.ts +87 -65
  253. package/src/cli/commands/keys.ts +8 -3
  254. package/src/cli/commands/mcp.ts +103 -287
  255. package/src/cli/commands/memory-v2.ts +163 -517
  256. package/src/cli/commands/notifications.ts +33 -7
  257. package/src/cli/commands/oauth/apps.ts +292 -261
  258. package/src/cli/commands/oauth/connect.ts +182 -345
  259. package/src/cli/commands/oauth/disconnect.ts +16 -215
  260. package/src/cli/commands/oauth/index.ts +49 -45
  261. package/src/cli/commands/oauth/mode.ts +43 -199
  262. package/src/cli/commands/oauth/ping.ts +17 -125
  263. package/src/cli/commands/oauth/providers.ts +732 -921
  264. package/src/cli/commands/oauth/request.ts +60 -350
  265. package/src/cli/commands/oauth/shared.ts +11 -121
  266. package/src/cli/commands/oauth/status.ts +31 -121
  267. package/src/cli/commands/oauth/token.ts +13 -55
  268. package/src/cli/commands/pending.ts +19 -10
  269. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +133 -183
  270. package/src/cli/commands/platform/__tests__/connect.test.ts +66 -181
  271. package/src/cli/commands/platform/__tests__/disconnect.test.ts +71 -227
  272. package/src/cli/commands/platform/__tests__/status.test.ts +169 -287
  273. package/src/cli/commands/platform/connect.ts +16 -80
  274. package/src/cli/commands/platform/disconnect.ts +14 -112
  275. package/src/cli/commands/platform/index.ts +177 -246
  276. package/src/cli/commands/routes.ts +153 -336
  277. package/src/cli/commands/sequence.ts +316 -360
  278. package/src/cli/commands/skills.ts +449 -671
  279. package/src/cli/commands/status.ts +58 -37
  280. package/src/cli/commands/stt.ts +94 -262
  281. package/src/cli/commands/task.ts +14 -40
  282. package/src/cli/commands/trust.ts +8 -3
  283. package/src/cli/commands/tts.ts +162 -167
  284. package/src/cli/commands/ui.ts +35 -42
  285. package/src/cli/commands/usage.ts +188 -126
  286. package/src/cli/commands/watchers.ts +8 -3
  287. package/src/cli/commands/webhooks.ts +99 -193
  288. package/src/cli/lib/__tests__/register-command.test.ts +85 -0
  289. package/src/cli/lib/daemon-credential-client.ts +4 -5
  290. package/src/cli/lib/nested-value.ts +44 -0
  291. package/src/cli/lib/open-browser.ts +36 -0
  292. package/src/cli/lib/register-command.ts +19 -0
  293. package/src/cli/lib/time-ago.ts +34 -0
  294. package/src/cli/program.ts +2 -4
  295. package/src/cli/utils/__tests__/conversation-id.test.ts +66 -0
  296. package/src/cli/utils/__tests__/parse-duration.test.ts +49 -0
  297. package/src/cli/utils/conversation-id.ts +30 -0
  298. package/src/cli/utils/parse-duration.ts +41 -0
  299. package/src/config/acp-defaults.test.ts +5 -1
  300. package/src/config/acp-defaults.ts +11 -4
  301. package/src/config/bundled-skills/acp/TOOLS.json +2 -2
  302. package/src/config/bundled-skills/app-builder/SKILL.md +1 -3
  303. package/src/config/bundled-skills/app-control/TOOLS.json +32 -0
  304. package/src/config/bundled-skills/contacts/SKILL.md +12 -45
  305. package/src/config/bundled-skills/contacts/TOOLS.json +0 -57
  306. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +0 -12
  307. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +0 -58
  308. package/src/config/bundled-tool-registry.ts +0 -2
  309. package/src/config/feature-flag-registry.json +17 -17
  310. package/src/config/llm-resolver.ts +16 -1
  311. package/src/config/loader.ts +148 -33
  312. package/src/config/raw-config-utils.ts +2 -30
  313. package/src/config/schema.ts +4 -0
  314. package/src/config/schemas/__tests__/memory-v2.test.ts +49 -0
  315. package/src/config/schemas/call-site-catalog.ts +29 -7
  316. package/src/config/schemas/llm-request-logs.ts +57 -0
  317. package/src/config/schemas/llm.ts +52 -2
  318. package/src/config/schemas/memory-retrospective.ts +48 -0
  319. package/src/config/schemas/memory-v2.ts +33 -2
  320. package/src/config/schemas/memory.ts +4 -0
  321. package/src/config/schemas/services.ts +15 -12
  322. package/src/config/seed-inference-profiles.ts +195 -134
  323. package/src/contacts/contact-store.ts +0 -61
  324. package/src/context/window-manager.ts +191 -5
  325. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +111 -0
  326. package/src/daemon/__tests__/conversation-tool-setup.test.ts +109 -4
  327. package/src/daemon/__tests__/daemon-skill-host.test.ts +10 -4
  328. package/src/daemon/approval-generators.ts +23 -29
  329. package/src/daemon/config-watcher.ts +2 -0
  330. package/src/daemon/conversation-agent-loop-handlers.ts +56 -0
  331. package/src/daemon/conversation-agent-loop.ts +140 -107
  332. package/src/daemon/conversation-error.ts +21 -0
  333. package/src/daemon/conversation-lifecycle.ts +68 -13
  334. package/src/daemon/conversation-process.ts +36 -19
  335. package/src/daemon/conversation-runtime-assembly.ts +14 -5
  336. package/src/daemon/conversation-slash.ts +175 -23
  337. package/src/daemon/conversation-store.ts +17 -10
  338. package/src/daemon/conversation-surfaces.ts +92 -26
  339. package/src/daemon/conversation-tool-setup.ts +33 -19
  340. package/src/daemon/conversation.ts +49 -10
  341. package/src/daemon/external-plugins-bootstrap.ts +18 -8
  342. package/src/daemon/guardian-action-generators.ts +7 -22
  343. package/src/daemon/handlers/config-model.ts +8 -126
  344. package/src/daemon/handlers/config-slack-channel.ts +10 -7
  345. package/src/daemon/handlers/config-vercel.ts +3 -1
  346. package/src/daemon/handlers/shared.ts +26 -0
  347. package/src/daemon/handlers/skills.ts +84 -5
  348. package/src/daemon/history-repair.ts +33 -6
  349. package/src/daemon/host-app-control-proxy.ts +44 -19
  350. package/src/daemon/host-bash-proxy.ts +85 -158
  351. package/src/daemon/host-browser-proxy.ts +97 -36
  352. package/src/daemon/host-cu-proxy.ts +1 -1
  353. package/src/daemon/host-file-proxy.ts +1 -1
  354. package/src/daemon/host-proxy-base.ts +13 -1
  355. package/src/daemon/host-proxy-preactivation.ts +25 -1
  356. package/src/daemon/host-transfer-proxy.ts +2 -2
  357. package/src/daemon/identity-helpers.ts +19 -0
  358. package/src/daemon/lifecycle.ts +128 -114
  359. package/src/daemon/meet-host-supervisor.ts +15 -15
  360. package/src/daemon/memory-v2-startup.ts +62 -14
  361. package/src/daemon/message-protocol.ts +6 -0
  362. package/src/daemon/message-types/bookmarks.ts +18 -0
  363. package/src/daemon/message-types/conversations.ts +12 -9
  364. package/src/daemon/message-types/messages.ts +28 -2
  365. package/src/daemon/message-types/sync.ts +60 -0
  366. package/src/daemon/pkb-reminder-builder.test.ts +54 -13
  367. package/src/daemon/pkb-reminder-builder.ts +21 -7
  368. package/src/daemon/process-message.ts +56 -23
  369. package/src/daemon/server.ts +23 -18
  370. package/src/daemon/shutdown-handlers.ts +0 -2
  371. package/src/daemon/tool-setup-types.ts +9 -0
  372. package/src/daemon/tool-side-effects.ts +6 -4
  373. package/src/daemon/wake-target-adapter.ts +11 -0
  374. package/src/documents/document-store.ts +35 -1
  375. package/src/export/transcript-formatter.ts +61 -2
  376. package/src/filing/filing-service.ts +42 -56
  377. package/src/heartbeat/__tests__/heartbeat-service.test.ts +359 -0
  378. package/src/heartbeat/heartbeat-run-store.ts +2 -1
  379. package/src/heartbeat/heartbeat-service.ts +149 -128
  380. package/src/home/__tests__/feed-types.test.ts +63 -131
  381. package/src/home/__tests__/feed-writer.test.ts +77 -278
  382. package/src/home/__tests__/post-connect-feed.test.ts +9 -12
  383. package/src/home/feed-types.ts +19 -73
  384. package/src/home/feed-writer.ts +25 -156
  385. package/src/home/post-connect-feed.ts +1 -3
  386. package/src/ipc/__tests__/cli-ipc.test.ts +2 -0
  387. package/src/ipc/__tests__/email-ipc.test.ts +506 -0
  388. package/src/ipc/__tests__/exit-helper.test.ts +104 -0
  389. package/src/ipc/__tests__/streaming-client.test.ts +237 -0
  390. package/src/ipc/__tests__/streaming-framing.test.ts +142 -0
  391. package/src/ipc/assistant-server.ts +148 -42
  392. package/src/ipc/cli-client.ts +370 -50
  393. package/src/ipc/routes/db-proxy-transaction.ts +151 -0
  394. package/src/ipc/skill-routes/__tests__/events-ipc.test.ts +60 -0
  395. package/src/ipc/skill-routes/events.ts +30 -3
  396. package/src/ipc/skill-server.ts +99 -42
  397. package/src/live-voice/__tests__/live-voice-session-manager.test.ts +46 -0
  398. package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +1 -0
  399. package/src/live-voice/live-voice-session-manager.ts +11 -4
  400. package/src/live-voice/live-voice-session.ts +14 -6
  401. package/src/memory/__tests__/bookmark-crud.test.ts +258 -0
  402. package/src/memory/__tests__/bookmark-schema.test.ts +181 -0
  403. package/src/memory/__tests__/conversation-types.test.ts +36 -0
  404. package/src/memory/__tests__/find-most-recent-retrospective-for.test.ts +130 -0
  405. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +10 -57
  406. package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +177 -0
  407. package/src/memory/__tests__/memory-retrospective-job.test.ts +328 -0
  408. package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +213 -0
  409. package/src/memory/__tests__/memory-retrospective-trigger-check.test.ts +90 -0
  410. package/src/memory/__tests__/memory-v2-activation-log-store.test.ts +69 -0
  411. package/src/memory/__tests__/memory-v2-concept-frequency.test.ts +3 -0
  412. package/src/memory/bookmark-crud.ts +179 -0
  413. package/src/memory/context-search/__tests__/agent-runner-redaction.test.ts +31 -9
  414. package/src/memory/context-search/agent-protocol.ts +5 -1
  415. package/src/memory/context-search/agent-runner.ts +60 -85
  416. package/src/memory/context-search/limits.ts +1 -4
  417. package/src/memory/context-search/search.ts +23 -113
  418. package/src/memory/context-search/sources/conversations.ts +18 -6
  419. package/src/memory/context-search/sources/memory-v2.ts +40 -31
  420. package/src/memory/context-search/sources/memory.ts +9 -2
  421. package/src/memory/context-search/sources/workspace.ts +13 -10
  422. package/src/memory/context-search/types.ts +1 -1
  423. package/src/memory/conversation-bootstrap.ts +11 -0
  424. package/src/memory/conversation-crud.ts +312 -10
  425. package/src/memory/conversation-queries.ts +9 -5
  426. package/src/memory/conversation-title-service.ts +1 -0
  427. package/src/memory/conversation-types.ts +16 -0
  428. package/src/memory/db-init.ts +14 -0
  429. package/src/memory/embedding-backend.ts +2 -1
  430. package/src/memory/embedding-runtime-manager.ts +1 -2
  431. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +104 -61
  432. package/src/memory/graph/__tests__/handle-remember-v2.test.ts +11 -26
  433. package/src/memory/graph/__tests__/remember-description.test.ts +55 -0
  434. package/src/memory/graph/conversation-graph-memory.ts +108 -14
  435. package/src/memory/graph/extraction.ts +4 -0
  436. package/src/memory/graph/graph-memory-state-store.ts +16 -3
  437. package/src/memory/graph/graph-search.test.ts +6 -5
  438. package/src/memory/graph/graph-search.ts +3 -4
  439. package/src/memory/graph/retriever.test.ts +12 -7
  440. package/src/memory/graph/retriever.ts +4 -5
  441. package/src/memory/graph/tool-handlers.ts +20 -11
  442. package/src/memory/graph/tools.ts +48 -9
  443. package/src/memory/indexer.ts +18 -2
  444. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +120 -6
  445. package/src/memory/jobs/embed-concept-page.ts +261 -89
  446. package/src/memory/jobs-store.ts +51 -1
  447. package/src/memory/jobs-worker.ts +60 -7
  448. package/src/memory/llm-request-log-source-clickhouse.ts +317 -0
  449. package/src/memory/llm-request-log-source-local.ts +26 -0
  450. package/src/memory/llm-request-log-source.ts +97 -0
  451. package/src/memory/llm-request-log-store.ts +1 -1
  452. package/src/memory/memory-retrospective-constants.ts +13 -0
  453. package/src/memory/memory-retrospective-enqueue.ts +114 -0
  454. package/src/memory/memory-retrospective-job.ts +351 -0
  455. package/src/memory/memory-retrospective-startup-cleanup.ts +108 -0
  456. package/src/memory/memory-retrospective-state.ts +162 -0
  457. package/src/memory/memory-retrospective-trigger-check.ts +91 -0
  458. package/src/memory/memory-v2-activation-log-store.ts +49 -5
  459. package/src/memory/memory-v2-concept-frequency.ts +4 -0
  460. package/src/memory/message-content.ts +38 -1
  461. package/src/memory/migrations/227-add-conversation-inference-profile.ts +6 -1
  462. package/src/memory/migrations/228-rename-inference-profile-snake-case.ts +20 -7
  463. package/src/memory/migrations/229-delete-private-conversations.test.ts +70 -1
  464. package/src/memory/migrations/229-delete-private-conversations.ts +12 -0
  465. package/src/memory/migrations/231-repair-memory-graph-event-dates.ts +16 -2
  466. package/src/memory/migrations/240-conversation-inference-profile-session.ts +25 -0
  467. package/src/memory/migrations/241-activation-state-fk-cascade.ts +50 -0
  468. package/src/memory/migrations/242-message-bookmarks.ts +38 -0
  469. package/src/memory/migrations/243-provider-connections.ts +68 -0
  470. package/src/memory/migrations/244-provider-connection-status-label.ts +23 -0
  471. package/src/memory/migrations/245-memory-retrospective-state.ts +36 -0
  472. package/src/memory/migrations/246-backfill-provider-connection-label.ts +81 -0
  473. package/src/memory/migrations/__tests__/244-provider-connection-status-label.test.ts +84 -0
  474. package/src/memory/migrations/__tests__/245-memory-retrospective-state.test.ts +125 -0
  475. package/src/memory/migrations/__tests__/246-backfill-provider-connection-label.test.ts +192 -0
  476. package/src/memory/migrations/index.ts +7 -0
  477. package/src/memory/pkb/pkb-search.test.ts +6 -5
  478. package/src/memory/pkb/pkb-search.ts +4 -5
  479. package/src/memory/published-pages-store.ts +16 -0
  480. package/src/memory/qdrant-client.ts +3 -0
  481. package/src/memory/schema/bookmarks.ts +38 -0
  482. package/src/memory/schema/conversations.ts +2 -0
  483. package/src/memory/schema/index.ts +2 -0
  484. package/src/memory/schema/inference.ts +29 -0
  485. package/src/memory/schema/memory-core.ts +9 -0
  486. package/src/memory/search/semantic.ts +5 -9
  487. package/src/memory/v2/__tests__/__snapshots__/prompts-router.test.ts.snap +27 -0
  488. package/src/memory/v2/__tests__/activation-store.test.ts +5 -5
  489. package/src/memory/v2/__tests__/activation.test.ts +46 -9
  490. package/src/memory/v2/__tests__/backfill-jobs.test.ts +38 -21
  491. package/src/memory/v2/__tests__/consolidation-job.test.ts +140 -163
  492. package/src/memory/v2/__tests__/edge-index.test.ts +1 -1
  493. package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +111 -0
  494. package/src/memory/v2/__tests__/injection.test.ts +768 -33
  495. package/src/memory/v2/__tests__/migration.test.ts +7 -3
  496. package/src/memory/v2/__tests__/page-index.test.ts +277 -0
  497. package/src/memory/v2/__tests__/page-store.test.ts +14 -1
  498. package/src/memory/v2/__tests__/prompts-router.test.ts +257 -0
  499. package/src/memory/v2/__tests__/qdrant.test.ts +382 -9
  500. package/src/memory/v2/__tests__/reranker.test.ts +4 -4
  501. package/src/memory/v2/__tests__/router.test.ts +516 -0
  502. package/src/memory/v2/__tests__/sim.test.ts +163 -8
  503. package/src/memory/v2/__tests__/skill-store.test.ts +58 -3
  504. package/src/memory/v2/__tests__/static-context.test.ts +8 -35
  505. package/src/memory/v2/__tests__/sweep-job.test.ts +114 -33
  506. package/src/memory/v2/activation-store.ts +34 -5
  507. package/src/memory/v2/activation.ts +40 -27
  508. package/src/memory/v2/backfill-jobs.ts +17 -84
  509. package/src/memory/v2/consolidation-job.ts +92 -86
  510. package/src/memory/v2/frontmatter-sweep.ts +91 -0
  511. package/src/memory/v2/injection.ts +466 -115
  512. package/src/memory/v2/migration.ts +117 -20
  513. package/src/memory/v2/page-index.ts +191 -0
  514. package/src/memory/v2/page-store.ts +42 -0
  515. package/src/memory/v2/prompts/consolidation.ts +14 -7
  516. package/src/memory/v2/prompts/router.ts +192 -0
  517. package/src/memory/v2/qdrant.ts +307 -133
  518. package/src/memory/v2/reranker.ts +14 -7
  519. package/src/memory/v2/router.ts +322 -0
  520. package/src/memory/v2/sim.ts +88 -34
  521. package/src/memory/v2/skill-store.ts +118 -29
  522. package/src/memory/v2/static-context.ts +20 -17
  523. package/src/memory/v2/sweep-job.ts +127 -102
  524. package/src/memory/v2/types.ts +16 -5
  525. package/src/memory/validation.ts +13 -0
  526. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +182 -0
  527. package/src/notifications/__tests__/home-feed-side-effect.test.ts +199 -0
  528. package/src/notifications/__tests__/signal-registry.test.ts +17 -0
  529. package/src/notifications/adapters/platform.ts +171 -0
  530. package/src/notifications/conversation-pairing.ts +2 -2
  531. package/src/notifications/copy-composer.ts +61 -12
  532. package/src/notifications/decision-engine.ts +46 -0
  533. package/src/notifications/destination-resolver.ts +21 -0
  534. package/src/notifications/emit-signal.ts +28 -1
  535. package/src/notifications/home-feed-side-effect.ts +111 -0
  536. package/src/notifications/signal.ts +5 -0
  537. package/src/permissions/checker.ts +12 -0
  538. package/src/permissions/gateway-threshold-reader.ts +116 -8
  539. package/src/permissions/ipc-risk-types.ts +2 -0
  540. package/src/permissions/prompter.ts +86 -96
  541. package/src/permissions/secret-prompter.ts +31 -31
  542. package/src/plugin-api/index.ts +13 -0
  543. package/src/plugin-api/package.json +12 -0
  544. package/src/plugin-api/types.ts +62 -0
  545. package/src/plugins/defaults/injectors.ts +20 -5
  546. package/src/plugins/external-plugin-loader.ts +294 -0
  547. package/src/plugins/types.ts +46 -30
  548. package/src/plugins/user-loader.ts +64 -41
  549. package/src/proactive-artifact/job.test.ts +63 -8
  550. package/src/proactive-artifact/job.ts +20 -2
  551. package/src/proactive-artifact/message-copy.ts +18 -1
  552. package/src/proactive-artifact/trigger-state.test.ts +9 -0
  553. package/src/proactive-artifact/trigger-state.ts +4 -0
  554. package/src/prompts/__tests__/system-prompt.test.ts +105 -0
  555. package/src/prompts/system-prompt.ts +22 -1
  556. package/src/prompts/templates/SOUL.md +13 -28
  557. package/src/prompts/update-bulletin-job.ts +61 -73
  558. package/src/providers/__tests__/dispatch-connection-routing.test.ts +279 -0
  559. package/src/providers/__tests__/inference.test.ts +288 -0
  560. package/src/providers/__tests__/provider-env-vars.test.ts +6 -0
  561. package/src/providers/__tests__/provider-secret-catalog.test.ts +6 -0
  562. package/src/providers/__tests__/retry-callsite.test.ts +14 -32
  563. package/src/providers/__tests__/satellite-connection-routing.test.ts +510 -0
  564. package/src/providers/__tests__/search-provider-catalog.test.ts +80 -0
  565. package/src/providers/anthropic/client.ts +95 -26
  566. package/src/providers/call-site-routing.ts +94 -16
  567. package/src/providers/connection-resolution.ts +163 -0
  568. package/src/providers/inference/__tests__/connections-status-label.test.ts +250 -0
  569. package/src/providers/inference/adapter-factory.ts +173 -0
  570. package/src/providers/inference/auth.ts +112 -0
  571. package/src/providers/inference/backfill.ts +196 -0
  572. package/src/providers/inference/connections.ts +356 -0
  573. package/src/providers/inference/resolve-auth.ts +65 -0
  574. package/src/providers/model-catalog.ts +104 -6
  575. package/src/providers/openai/responses-provider.ts +4 -2
  576. package/src/providers/provider-env-vars.ts +17 -7
  577. package/src/providers/provider-secret-catalog.ts +49 -30
  578. package/src/providers/provider-send-message.ts +41 -20
  579. package/src/providers/registry.ts +143 -159
  580. package/src/providers/retry.ts +18 -10
  581. package/src/providers/search-provider-catalog.ts +121 -0
  582. package/src/runtime/AGENTS.md +18 -5
  583. package/src/runtime/__tests__/background-job-runner.test.ts +357 -0
  584. package/src/runtime/__tests__/pre-first-message-gate.test.ts +82 -0
  585. package/src/runtime/actor-trust-resolver.ts +32 -10
  586. package/src/runtime/agent-wake.ts +35 -6
  587. package/src/runtime/assistant-event-hub.ts +3 -85
  588. package/src/runtime/auth/route-policy.ts +304 -8
  589. package/src/runtime/auth/same-actor.ts +2 -0
  590. package/src/runtime/background-job-runner.ts +339 -0
  591. package/src/runtime/btw-sidechain.ts +1 -0
  592. package/src/runtime/channel-approvals.ts +3 -2
  593. package/src/runtime/guardian-reply-router.ts +0 -10
  594. package/src/runtime/http-router.ts +36 -1
  595. package/src/runtime/http-server.ts +31 -5
  596. package/src/runtime/http-types.ts +2 -0
  597. package/src/runtime/middleware/__tests__/request-logger.test.ts +162 -0
  598. package/src/runtime/middleware/request-logger.ts +62 -1
  599. package/src/runtime/pending-interactions.ts +19 -15
  600. package/src/runtime/pre-first-message-gate.ts +83 -0
  601. package/src/runtime/routes/__tests__/backup-routes.test.ts +8 -1
  602. package/src/runtime/routes/__tests__/bookmark-routes.test.ts +251 -0
  603. package/src/runtime/routes/__tests__/connection-routes-vs-cli-parity.test.ts +142 -0
  604. package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +315 -0
  605. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +189 -0
  606. package/src/runtime/routes/__tests__/home-feed-routes.test.ts +15 -136
  607. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +736 -0
  608. package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +147 -0
  609. package/src/runtime/routes/__tests__/stt-routes.test.ts +5 -1
  610. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +384 -0
  611. package/src/runtime/routes/__tests__/tts-routes.test.ts +6 -2
  612. package/src/runtime/routes/acp-routes.ts +10 -8
  613. package/src/runtime/routes/app-management-routes.ts +228 -3
  614. package/src/runtime/routes/approval-routes.ts +7 -21
  615. package/src/runtime/routes/audit-routes.ts +43 -0
  616. package/src/runtime/routes/auth-routes.ts +72 -0
  617. package/src/runtime/routes/avatar-routes.ts +273 -20
  618. package/src/runtime/routes/backup-routes.ts +406 -2
  619. package/src/runtime/routes/bookmark-routes.ts +154 -0
  620. package/src/runtime/routes/channel-verification-routes.ts +2 -1
  621. package/src/runtime/routes/consolidation-routes.ts +8 -9
  622. package/src/runtime/routes/contact-routes.ts +0 -160
  623. package/src/runtime/routes/conversation-cli-routes.ts +192 -0
  624. package/src/runtime/routes/conversation-management-routes.ts +30 -43
  625. package/src/runtime/routes/conversation-query-routes.ts +373 -82
  626. package/src/runtime/routes/conversation-routes.ts +31 -10
  627. package/src/runtime/routes/conversations-import-routes.ts +229 -0
  628. package/src/runtime/routes/credential-routes.ts +540 -0
  629. package/src/runtime/routes/debug-bash-routes.ts +2 -0
  630. package/src/runtime/routes/debug-routes.ts +2 -2
  631. package/src/runtime/routes/document-pdf-renderer.ts +5 -1
  632. package/src/runtime/routes/domain-routes.ts +167 -0
  633. package/src/runtime/routes/email-routes.ts +603 -0
  634. package/src/runtime/routes/errors.ts +2 -2
  635. package/src/runtime/routes/events-routes.ts +192 -0
  636. package/src/runtime/routes/filing-routes.ts +2 -3
  637. package/src/runtime/routes/home-feed-routes.ts +6 -78
  638. package/src/runtime/routes/host-app-control-routes.ts +44 -2
  639. package/src/runtime/routes/host-browser-routes.ts +103 -22
  640. package/src/runtime/routes/http-adapter.ts +2 -0
  641. package/src/runtime/routes/identity-routes.ts +5 -0
  642. package/src/runtime/routes/image-generation-routes.ts +99 -0
  643. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +137 -1
  644. package/src/runtime/routes/inbound-stages/background-dispatch.ts +87 -7
  645. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +156 -0
  646. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +22 -7
  647. package/src/runtime/routes/index.ts +36 -0
  648. package/src/runtime/routes/inference-profile-session-handler.ts +312 -0
  649. package/src/runtime/routes/inference-profile-session-reaper.ts +98 -0
  650. package/src/runtime/routes/inference-profile-session-routes.ts +146 -0
  651. package/src/runtime/routes/inference-provider-connection-routes.ts +317 -0
  652. package/src/runtime/routes/inference-send-routes.ts +115 -0
  653. package/src/runtime/routes/integrations/twilio.ts +1 -0
  654. package/src/runtime/routes/mcp-auth-routes.ts +283 -9
  655. package/src/runtime/routes/memory-item-routes.test.ts +3 -9
  656. package/src/runtime/routes/memory-item-routes.ts +5 -6
  657. package/src/runtime/routes/memory-v2-routes.ts +105 -404
  658. package/src/runtime/routes/notification-routes.ts +2 -0
  659. package/src/runtime/routes/oauth-apps.ts +112 -7
  660. package/src/runtime/routes/oauth-commands-routes.ts +1007 -0
  661. package/src/runtime/routes/oauth-connect-routes.ts +67 -5
  662. package/src/runtime/routes/oauth-providers.ts +298 -8
  663. package/src/runtime/routes/platform-routes.ts +336 -0
  664. package/src/runtime/routes/playground/inject-failures.ts +2 -1
  665. package/src/runtime/routes/playground/reset-circuit.ts +2 -1
  666. package/src/runtime/routes/playground/state.ts +2 -1
  667. package/src/runtime/routes/publish-routes.ts +221 -0
  668. package/src/runtime/routes/schedule-routes.ts +82 -0
  669. package/src/runtime/routes/sequence-routes.ts +291 -0
  670. package/src/runtime/routes/settings-routes.ts +2 -10
  671. package/src/runtime/routes/skills-routes.ts +31 -1
  672. package/src/runtime/routes/stt-routes.ts +240 -3
  673. package/src/runtime/routes/surface-action-routes.ts +43 -7
  674. package/src/runtime/routes/tts-routes.ts +67 -0
  675. package/src/runtime/routes/types.ts +32 -0
  676. package/src/runtime/routes/user-routes-cli.ts +243 -0
  677. package/src/runtime/routes/webhook-routes.ts +165 -0
  678. package/src/runtime/sync/resource-sync-events.ts +25 -0
  679. package/src/runtime/sync/sync-publisher.test.ts +105 -0
  680. package/src/runtime/sync/sync-publisher.ts +21 -0
  681. package/src/schedule/scheduler.ts +200 -123
  682. package/src/security/__tests__/provider-key-env-fallback.test.ts +12 -6
  683. package/src/security/secret-patterns.ts +3 -0
  684. package/src/sequence/engine.ts +38 -40
  685. package/src/skills/include-graph.ts +35 -13
  686. package/src/subagent/manager.ts +20 -15
  687. package/src/tools/browser/__tests__/browser-execution-acquire.test.ts +206 -0
  688. package/src/tools/browser/browser-execution.ts +15 -4
  689. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +174 -0
  690. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/ws-transport.test.ts +16 -13
  691. package/src/tools/browser/cdp-client/extension-cdp-client.ts +24 -1
  692. package/src/tools/browser/cdp-client/factory.ts +66 -5
  693. package/src/tools/browser/runtime-check.ts +77 -0
  694. package/src/tools/document/document-tool.ts +20 -0
  695. package/src/tools/executor.ts +18 -2
  696. package/src/tools/memory/register.test.ts +10 -8
  697. package/src/tools/memory/register.ts +9 -1
  698. package/src/tools/network/__tests__/web-search.test.ts +156 -0
  699. package/src/tools/network/web-search.ts +280 -37
  700. package/src/tools/permission-checker.ts +28 -5
  701. package/src/tools/skills/load.ts +24 -20
  702. package/src/tools/subagent/spawn.ts +3 -3
  703. package/src/tools/terminal/shell.ts +44 -0
  704. package/src/tools/tool-name-aliases.ts +19 -0
  705. package/src/tools/types.ts +19 -1
  706. package/src/usage/attribution.ts +3 -2
  707. package/src/util/pricing.ts +86 -160
  708. package/src/watcher/__tests__/engine.test.ts +301 -0
  709. package/src/watcher/constants.ts +7 -0
  710. package/src/watcher/engine.ts +90 -90
  711. package/src/workspace/migrations/046-seed-conversation-starters-callsite.ts +6 -9
  712. package/src/workspace/migrations/054-seed-recall-callsite.ts +10 -1
  713. package/src/workspace/migrations/057-repair-stale-gemini-model-ids.ts +28 -4
  714. package/src/workspace/migrations/067-release-notes-safe-storage-limits.ts +4 -62
  715. package/src/workspace/migrations/069-seed-onboarding-threads.ts +34 -0
  716. package/src/workspace/migrations/070-memory-v2-summary-schema-rebuild.ts +31 -0
  717. package/src/workspace/migrations/071-remove-safe-storage-release-note.ts +111 -0
  718. package/src/workspace/migrations/072-seed-reply-suggestion-callsite.ts +104 -0
  719. package/src/workspace/migrations/073-repair-recall-callsite-empty-profile.ts +93 -0
  720. package/src/workspace/migrations/074-drop-deprecated-secret-detection-keys.ts +117 -0
  721. package/src/workspace/migrations/075-memory-v2-bm25-b-default-reembed.ts +61 -0
  722. package/src/workspace/migrations/076-drop-services-inference-mode.ts +62 -0
  723. package/src/workspace/migrations/077-seed-memory-router-callsite.ts +89 -0
  724. package/src/workspace/migrations/078-release-notes-tavily-web-search.ts +66 -0
  725. package/src/workspace/migrations/079-home-feed-notification-only.ts +197 -0
  726. package/src/workspace/migrations/080-restrict-vercel-api-token-metadata.ts +182 -0
  727. package/src/workspace/migrations/081-backfill-bash-allowed-tools-for-injection-credentials.ts +160 -0
  728. package/src/workspace/migrations/082-backfill-managed-profile-labels.ts +154 -0
  729. package/src/workspace/migrations/registry.ts +28 -0
  730. package/src/workspace/migrations/runner.ts +13 -2
  731. package/src/workspace/migrations/types.ts +13 -3
  732. package/src/workspace/provider-commit-message-generator.ts +3 -2
  733. package/src/__tests__/context-search-pkb-source.test.ts +0 -492
  734. package/src/__tests__/credentials-cli.test.ts +0 -1225
  735. package/src/__tests__/memory-admin-recall.test.ts +0 -213
  736. package/src/approvals/__tests__/guardian-feed-event.test.ts +0 -303
  737. package/src/cli/commands/__tests__/email-download.test.ts +0 -260
  738. package/src/cli/commands/__tests__/email-list.test.ts +0 -216
  739. package/src/cli/commands/__tests__/email-register.test.ts +0 -186
  740. package/src/cli/commands/__tests__/email-send.test.ts +0 -416
  741. package/src/cli/commands/__tests__/email-status.test.ts +0 -185
  742. package/src/cli/commands/__tests__/email-unregister.test.ts +0 -168
  743. package/src/cli/commands/__tests__/routes.test.ts +0 -562
  744. package/src/cli/commands/__tests__/stt-transcribe.test.ts +0 -454
  745. package/src/cli/commands/autonomy.ts +0 -365
  746. package/src/cli/commands/memory.ts +0 -424
  747. package/src/cli/commands/oauth/__tests__/connect.test.ts +0 -1201
  748. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +0 -686
  749. package/src/cli/commands/oauth/__tests__/mode.test.ts +0 -632
  750. package/src/cli/commands/oauth/__tests__/ping.test.ts +0 -631
  751. package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +0 -573
  752. package/src/cli/commands/oauth/__tests__/providers-register.test.ts +0 -330
  753. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +0 -521
  754. package/src/cli/commands/oauth/__tests__/status.test.ts +0 -551
  755. package/src/cli/commands/oauth/__tests__/token.test.ts +0 -420
  756. package/src/cli/lib/daemon-avatar-client.ts +0 -37
  757. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +0 -87
  758. package/src/config/bundled-skills/messaging/tools/__tests__/messaging-feed-events.test.ts +0 -207
  759. package/src/daemon/__tests__/conversation-feed-event.test.ts +0 -304
  760. package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +0 -233
  761. package/src/home/__tests__/assistant-feed-authoring.test.ts +0 -156
  762. package/src/home/__tests__/emit-feed-event.test.ts +0 -169
  763. package/src/home/__tests__/feed-population-integration.test.ts +0 -312
  764. package/src/home/__tests__/feed-scheduler.test.ts +0 -222
  765. package/src/home/__tests__/phase5-exit-criteria.test.ts +0 -229
  766. package/src/home/__tests__/platform-gmail-digest.test.ts +0 -222
  767. package/src/home/__tests__/rollup-producer.test.ts +0 -507
  768. package/src/home/assistant-feed-authoring.ts +0 -135
  769. package/src/home/emit-feed-event.ts +0 -169
  770. package/src/home/feed-scheduler.ts +0 -281
  771. package/src/home/platform-gmail-digest.ts +0 -163
  772. package/src/home/rewrite-command-preview.ts +0 -66
  773. package/src/home/rewrite-feed-title.ts +0 -58
  774. package/src/home/rollup-producer.ts +0 -426
  775. package/src/memory/admin.ts +0 -326
  776. package/src/memory/context-search/sources/pkb.ts +0 -477
  777. package/src/memory/graph/compaction.ts +0 -299
  778. /package/src/cli/{commands → lib}/cache-fs.ts +0 -0
@@ -41,6 +41,7 @@ import {
41
41
  } from "bun:test";
42
42
 
43
43
  import { drizzle } from "drizzle-orm/bun-sqlite";
44
+ import { z } from "zod";
44
45
 
45
46
  import { makeMockLogger } from "../../../__tests__/helpers/mock-logger.js";
46
47
  import type { AssistantConfig } from "../../../config/types.js";
@@ -114,8 +115,11 @@ class MockQdrantClient {
114
115
  _name: string,
115
116
  params: { using: string; limit: number; filter?: unknown },
116
117
  ) {
117
- const queue = state.queryResponses[params.using as "dense" | "sparse"];
118
- return queue.shift() ?? { points: [] };
118
+ // The four-channel hybrid query fires body-dense, body-sparse,
119
+ // summary-dense, summary-sparse in order; both dense channels share
120
+ // the dense queue and both sparse channels share the sparse queue.
121
+ const channel = params.using.endsWith("sparse") ? "sparse" : "dense";
122
+ return state.queryResponses[channel].shift() ?? { points: [] };
119
123
  }
120
124
  }
121
125
 
@@ -150,6 +154,11 @@ mock.module("../skill-store.js", () => ({
150
154
  isSkillSlug: (slug: string) => slug.startsWith("skills/"),
151
155
  SKILL_SLUG_PREFIX: "skills/",
152
156
  skillSlugFor: (id: string) => `skills/${id}`,
157
+ // PR 4 added `listSkillEntries`; `page-index.ts` (transitively imported
158
+ // via `page-store.ts` and `skill-store.ts`) consumes it at module-init
159
+ // time. Tests stage skill content via `skillState.entries`; expose them
160
+ // here so the page-index loader sees a consistent view.
161
+ listSkillEntries: () => Array.from(skillState.entries.values()),
153
162
  }));
154
163
 
155
164
  // ---------------------------------------------------------------------------
@@ -176,6 +185,93 @@ mock.module("../../memory-v2-activation-log-store.js", () => ({
176
185
  },
177
186
  }));
178
187
 
188
+ // ---------------------------------------------------------------------------
189
+ // Page-store mock — pass-through with optional per-slug failure injection
190
+ // ---------------------------------------------------------------------------
191
+ //
192
+ // Most tests want the real `readPage` (it walks the temp workspace seeded in
193
+ // `beforeAll`). The error-isolation tests stage a slug whose `readPage` call
194
+ // must throw — typically a Zod validation error mimicking the real-world
195
+ // "unrecognized frontmatter key" failure that motivated this work. Tests
196
+ // stage entries via `pageStoreState.failingSlugs` and reset in `resetState`.
197
+ //
198
+ // Bun's `mock.module` mutates the module's exports object in place, so
199
+ // `realPageStore.readPage` AFTER the mock applies would resolve to the mock
200
+ // itself — calling it would recurse. We capture the original function value
201
+ // (not a property lookup) before installing the mock so the pass-through
202
+ // path has a real reference to the underlying implementation.
203
+
204
+ const realPageStoreModule = await import("../page-store.js");
205
+ const realReadPage = realPageStoreModule.readPage;
206
+ const pageStoreState = {
207
+ failingSlugs: new Map<string, Error>(),
208
+ };
209
+ mock.module("../page-store.js", () => ({
210
+ ...realPageStoreModule,
211
+ readPage: async (workspaceDir: string, slug: string) => {
212
+ const err = pageStoreState.failingSlugs.get(slug);
213
+ if (err) throw err;
214
+ return realReadPage(workspaceDir, slug);
215
+ },
216
+ }));
217
+
218
+ // ---------------------------------------------------------------------------
219
+ // Router mock — programmable per-call result
220
+ // ---------------------------------------------------------------------------
221
+ //
222
+ // PR 10 wires `runRouter` into `injectMemoryV2Block` behind the
223
+ // `memory.v2.router.enabled` flag. The activation-mode tests above never
224
+ // flip the flag, so the default mock returns a no-op result and the router
225
+ // branch is never exercised. Router-mode tests set `routerState.nextResult`
226
+ // to stage a deterministic outcome before each call.
227
+
228
+ interface RouterResultStub {
229
+ selectedSlugs: string[];
230
+ failureReason: string | null;
231
+ }
232
+
233
+ const routerState = {
234
+ nextResult: null as RouterResultStub | null,
235
+ callCount: 0,
236
+ };
237
+
238
+ mock.module("../router.js", () => ({
239
+ runRouter: async () => {
240
+ routerState.callCount++;
241
+ return (
242
+ routerState.nextResult ?? {
243
+ selectedSlugs: [],
244
+ failureReason: null,
245
+ }
246
+ );
247
+ },
248
+ }));
249
+
250
+ // ---------------------------------------------------------------------------
251
+ // Activation-store mock — pass-through with optional `save` failure injection
252
+ // ---------------------------------------------------------------------------
253
+ //
254
+ // One regression test forces `save` to throw to exercise the
255
+ // `injectMemoryV2Block` outer try/finally — telemetry must still be flushed
256
+ // (with `mode: "errored"`) and the error must propagate. Default behavior
257
+ // delegates to the real activation-store so the rest of the suite stays
258
+ // untouched. Same pre-mock function-capture trick as `readPage` above.
259
+
260
+ const realActivationStoreModule = await import("../activation-store.js");
261
+ const realSave = realActivationStoreModule.save;
262
+ const activationStoreState = {
263
+ saveShouldThrow: false,
264
+ };
265
+ mock.module("../activation-store.js", () => ({
266
+ ...realActivationStoreModule,
267
+ save: async (...args: Parameters<typeof realSave>) => {
268
+ if (activationStoreState.saveShouldThrow) {
269
+ throw new Error("simulated activation-store save failure");
270
+ }
271
+ return realSave(...args);
272
+ },
273
+ }));
274
+
179
275
  // ---------------------------------------------------------------------------
180
276
  // Workspace + DB setup
181
277
  // ---------------------------------------------------------------------------
@@ -229,6 +325,18 @@ ref_files:
229
325
  ---
230
326
  Demo body content.`,
231
327
  );
328
+ // A page WITH a `summary` in its frontmatter — exercises the summary-only
329
+ // injection path. Body is intentionally longer than the summary so tests
330
+ // can assert that the body is *not* injected when the summary is present.
331
+ writeFileSync(
332
+ join(tmpWorkspace, "memory", "concepts", "summarized-page.md"),
333
+ `---
334
+ edges: []
335
+ ref_files: []
336
+ summary: A short prose description of the summarized page that retrieval injects in place of the full body.
337
+ ---
338
+ Long-form body content that should NOT appear in the injection block when the page has a summary in frontmatter — the agent reads the file on demand instead.`,
339
+ );
232
340
  });
233
341
 
234
342
  afterAll(() => {
@@ -284,8 +392,10 @@ function makeConfig(
284
392
  epsilon: number;
285
393
  dense_weight: number;
286
394
  sparse_weight: number;
395
+ router: { enabled: boolean; max_page_ids?: number };
287
396
  }> = {},
288
397
  ): AssistantConfig {
398
+ const { router, ...rest } = overrides;
289
399
  return {
290
400
  memory: {
291
401
  v2: {
@@ -299,7 +409,8 @@ function makeConfig(
299
409
  epsilon: 0.01,
300
410
  dense_weight: 1.0,
301
411
  sparse_weight: 0.0,
302
- ...overrides,
412
+ router: { enabled: false, max_page_ids: 25, ...(router ?? {}) },
413
+ ...rest,
303
414
  },
304
415
  },
305
416
  } as unknown as AssistantConfig;
@@ -308,14 +419,26 @@ function makeConfig(
308
419
  /**
309
420
  * Stage one set of dense/sparse hits, used uniformly by every `simBatch`
310
421
  * channel call (user/assistant/now) AND by the un-restricted ANN candidate
311
- * query. The candidate query runs first, then three simBatch calls, so we
312
- * push 4 dense + 4 sparse responses per turn.
422
+ * query. The candidate query runs first, then three simBatch calls that's
423
+ * `channels` (= 4) logical hybrid queries. Each logical hybrid query now
424
+ * fires a four-channel fan-out (body dense, body sparse, summary dense,
425
+ * summary sparse), so we push 2 dense + 2 sparse responses per logical
426
+ * call to match the post-summary-vector wire pattern.
313
427
  *
314
428
  * Each entry is mapped to a hit per channel; pass `denseScore`/`sparseScore`
315
- * undefined to omit a slug from that channel.
429
+ * undefined to omit a slug from that channel. `summaryDenseScore` /
430
+ * `summarySparseScore` route to the summary-side channels — tests that
431
+ * don't care about summary scoring leave them undefined and the summary
432
+ * channel falls back to body-only behavior.
316
433
  */
317
434
  function stageTurn(
318
- hits: Array<{ slug: string; denseScore?: number; sparseScore?: number }>,
435
+ hits: Array<{
436
+ slug: string;
437
+ denseScore?: number;
438
+ sparseScore?: number;
439
+ summaryDenseScore?: number;
440
+ summarySparseScore?: number;
441
+ }>,
319
442
  channels = 4,
320
443
  ): void {
321
444
  // Clear any leftovers from a prior turn before staging this one so unused
@@ -336,6 +459,22 @@ function stageTurn(
336
459
  .filter((h) => h.sparseScore !== undefined)
337
460
  .map((h) => ({ score: h.sparseScore, payload: { slug: h.slug } })),
338
461
  });
462
+ state.queryResponses.dense.push({
463
+ points: hits
464
+ .filter((h) => h.summaryDenseScore !== undefined)
465
+ .map((h) => ({
466
+ score: h.summaryDenseScore,
467
+ payload: { slug: h.slug },
468
+ })),
469
+ });
470
+ state.queryResponses.sparse.push({
471
+ points: hits
472
+ .filter((h) => h.summarySparseScore !== undefined)
473
+ .map((h) => ({
474
+ score: h.summarySparseScore,
475
+ payload: { slug: h.slug },
476
+ })),
477
+ });
339
478
  }
340
479
  }
341
480
 
@@ -347,6 +486,10 @@ function resetState(): void {
347
486
  skillState.entries.clear();
348
487
  telemetryState.recordCalls.length = 0;
349
488
  telemetryState.recordShouldThrow = false;
489
+ pageStoreState.failingSlugs.clear();
490
+ activationStoreState.saveShouldThrow = false;
491
+ routerState.nextResult = null;
492
+ routerState.callCount = 0;
350
493
  // The qdrant module caches its client; the cached client may be a
351
494
  // MockQdrantClient instance from a sibling test file. Reset to force a
352
495
  // fresh `new QdrantClient()` against this file's mock.
@@ -395,7 +538,7 @@ describe("injectMemoryV2Block", () => {
395
538
  expect(result.block).not.toContain("<memory>");
396
539
  expect(result.block).not.toContain("</memory>");
397
540
  expect(result.block).not.toContain("## What I Remember Right Now");
398
- expect(result.block).toContain("### alice-vscode");
541
+ expect(result.block).toContain("# memory/concepts/alice-vscode.md");
399
542
  expect(result.block).toContain("VS Code");
400
543
 
401
544
  // State persisted: alice's activation is above epsilon and recorded;
@@ -484,10 +627,10 @@ describe("injectMemoryV2Block", () => {
484
627
  });
485
628
 
486
629
  expect(result.toInject).toEqual(["carol-jazz"]);
487
- expect(result.block).toContain("### carol-jazz");
630
+ expect(result.block).toContain("# memory/concepts/carol-jazz.md");
488
631
  // The block only shows the new slug — alice's attachment lives on the
489
632
  // previous turn's user message.
490
- expect(result.block).not.toContain("### alice-vscode");
633
+ expect(result.block).not.toContain("# memory/concepts/alice-vscode.md");
491
634
 
492
635
  const persisted = await hydrate(db, "conv-1");
493
636
  expect(persisted!.everInjected).toEqual([
@@ -532,7 +675,7 @@ describe("injectMemoryV2Block", () => {
532
675
  });
533
676
 
534
677
  expect(result.toInject).toEqual(["alice-vscode"]);
535
- expect(result.block).toContain("### alice-vscode");
678
+ expect(result.block).toContain("# memory/concepts/alice-vscode.md");
536
679
 
537
680
  const persisted = await hydrate(db, "conv-1");
538
681
  expect(persisted!.everInjected).toEqual([
@@ -540,6 +683,74 @@ describe("injectMemoryV2Block", () => {
540
683
  ]);
541
684
  });
542
685
 
686
+ test("page with summary renders as path + summary, no body, with the CRITICAL header", async () => {
687
+ // Pages whose frontmatter carries a `summary` should inject only the
688
+ // summary text behind the path header — the agent reads the full file
689
+ // on demand. The leading `**CRITICAL:**` line tells the agent how to
690
+ // read the block.
691
+ stageTurn([{ slug: "summarized-page", denseScore: 0.9 }]);
692
+
693
+ const result = await injectMemoryV2Block({
694
+ database: db,
695
+ conversationId: "conv-1",
696
+ currentTurn: 1,
697
+ userMessage: "tell me about the summarized page",
698
+ assistantMessage: "",
699
+ nowText: "Now",
700
+ messageId: "msg-1",
701
+ config: makeConfig(),
702
+ });
703
+
704
+ expect(result.block).not.toBeNull();
705
+ expect(result.block).toContain(
706
+ "**CRITICAL:** These are page summaries. Read the page file if it looks relevant.",
707
+ );
708
+ expect(result.block).toContain(
709
+ "# memory/concepts/summarized-page.md\nA short prose description",
710
+ );
711
+ // Body is NOT in the block — the agent must follow up with a read tool.
712
+ expect(result.block).not.toContain("Long-form body content");
713
+ // Frontmatter is also omitted; the path header carries the identifying
714
+ // information by itself, and edges flow through the activation graph.
715
+ expect(result.block).not.toContain("---\nedges:");
716
+ });
717
+
718
+ test("mixed batch — summary page renders short, fallback page renders full", async () => {
719
+ // Both pages rank into top-K. summarized-page has a summary → short
720
+ // form. frontmatter-demo has no summary → full-page fallback. The
721
+ // single CRITICAL header sits at the top regardless.
722
+ stageTurn([
723
+ { slug: "summarized-page", denseScore: 0.95 },
724
+ { slug: "frontmatter-demo", denseScore: 0.85 },
725
+ ]);
726
+
727
+ const result = await injectMemoryV2Block({
728
+ database: db,
729
+ conversationId: "conv-1",
730
+ currentTurn: 1,
731
+ userMessage: "show me everything",
732
+ assistantMessage: "",
733
+ nowText: "Now",
734
+ messageId: "msg-1",
735
+ config: makeConfig(),
736
+ });
737
+
738
+ expect(result.block).not.toBeNull();
739
+ // CRITICAL header appears exactly once.
740
+ const criticalCount = (
741
+ result.block!.match(/\*\*CRITICAL:\*\* These are page summaries\./g) ?? []
742
+ ).length;
743
+ expect(criticalCount).toBe(1);
744
+ // summarized-page → short form (path + summary, no body, no frontmatter).
745
+ expect(result.block).toContain("# memory/concepts/summarized-page.md\nA");
746
+ expect(result.block).not.toContain("Long-form body content");
747
+ // frontmatter-demo → full-page fallback (path + frontmatter + body).
748
+ expect(result.block).toContain(
749
+ "# memory/concepts/frontmatter-demo.md\n---\n",
750
+ );
751
+ expect(result.block).toContain("Demo body content.");
752
+ });
753
+
543
754
  test("includes the page frontmatter (edges, ref_files) in each rendered section", async () => {
544
755
  // The frontmatter (`edges`, `ref_files`) lives on disk above the page
545
756
  // body and is part of the page's content. Injection must reproduce both
@@ -560,8 +771,12 @@ describe("injectMemoryV2Block", () => {
560
771
  });
561
772
 
562
773
  expect(result.block).not.toBeNull();
563
- // Slug header is immediately followed by the frontmatter open delimiter.
564
- expect(result.block).toContain("### frontmatter-demo\n---\n");
774
+ // Path header is immediately followed by the frontmatter open delimiter.
775
+ // The fallback path renders the full page (frontmatter + body) when the
776
+ // page has no `summary` field — `frontmatter-demo` predates the field.
777
+ expect(result.block).toContain(
778
+ "# memory/concepts/frontmatter-demo.md\n---\n",
779
+ );
565
780
  // Both fields render in YAML block style with their populated values.
566
781
  expect(result.block).toContain("edges:\n - alice-vscode");
567
782
  expect(result.block).toContain("ref_files:\n - images/demo.jpg");
@@ -589,19 +804,21 @@ describe("injectMemoryV2Block", () => {
589
804
  });
590
805
 
591
806
  expect(result.toInject).toEqual(["carol-jazz", "alice-vscode"]);
592
- const carolIdx = result.block!.indexOf("### carol-jazz");
593
- const aliceIdx = result.block!.indexOf("### alice-vscode");
807
+ const carolIdx = result.block!.indexOf("# memory/concepts/carol-jazz.md");
808
+ const aliceIdx = result.block!.indexOf("# memory/concepts/alice-vscode.md");
594
809
  expect(carolIdx).toBeGreaterThan(-1);
595
810
  expect(aliceIdx).toBeGreaterThan(-1);
596
811
  expect(carolIdx).toBeLessThan(aliceIdx);
597
812
  });
598
813
 
599
814
  test("persists sparse state — only slugs above epsilon survive", async () => {
600
- // Carol scores high; alice/bob essentially zero. After saving, only
601
- // carol should appear in the persisted state map.
815
+ // Carol scores high; alice essentially zero. After saving, only carol
816
+ // should appear in the persisted state map. denseScore is the raw
817
+ // Qdrant cosine in [-1, 1]; alice uses -1 so the post `(x+1)/2`
818
+ // unit-mapping pins her fused score to 0 — below epsilon.
602
819
  stageTurn([
603
820
  { slug: "carol-jazz", denseScore: 1.0 },
604
- { slug: "alice-vscode", denseScore: 0.0 },
821
+ { slug: "alice-vscode", denseScore: -1.0 },
605
822
  ]);
606
823
  await injectMemoryV2Block({
607
824
  database: db,
@@ -692,7 +909,7 @@ describe("injectMemoryV2Block", () => {
692
909
  expect(result.block).not.toContain("<memory>");
693
910
  expect(result.block).not.toContain("</memory>");
694
911
  expect(result.block).not.toContain("## What I Remember Right Now");
695
- expect(result.block).not.toContain("### alice-vscode");
912
+ expect(result.block).not.toContain("# memory/concepts/alice-vscode.md");
696
913
  expect(result.block).toContain("### Skills You Can Use");
697
914
  expect(result.block).toContain(
698
915
  '- The "Example Skill A" skill (example-skill-a) is available. Helps with examples. → use skill_load to activate',
@@ -731,11 +948,13 @@ describe("injectMemoryV2Block", () => {
731
948
  );
732
949
  expect(result.block).not.toBeNull();
733
950
 
734
- const aliceIdx = result.block!.indexOf("### alice-vscode");
951
+ const aliceHeaderIdx = result.block!.indexOf(
952
+ "# memory/concepts/alice-vscode.md",
953
+ );
735
954
  const skillsIdx = result.block!.indexOf("### Skills You Can Use");
736
- expect(aliceIdx).toBeGreaterThan(-1);
955
+ expect(aliceHeaderIdx).toBeGreaterThan(-1);
737
956
  expect(skillsIdx).toBeGreaterThan(-1);
738
- expect(aliceIdx).toBeLessThan(skillsIdx);
957
+ expect(aliceHeaderIdx).toBeLessThan(skillsIdx);
739
958
 
740
959
  expect(result.block).toContain(
741
960
  '- The "Example Skill A" skill (example-skill-a) is available. Helps with examples. → use skill_load to activate',
@@ -790,9 +1009,10 @@ describe("injectMemoryV2Block", () => {
790
1009
 
791
1010
  test("skill slugs whose entry is missing from the cache are dropped silently", async () => {
792
1011
  // The skill ranks into top-K but the in-process cache no longer knows
793
- // its content (skill uninstalled mid-run). The render path drops it
794
- // without surfacing it as a `missingSlugs` page-missing event that
795
- // status is reserved for on-disk concept pages, not catalog-derived
1012
+ // its content (skill uninstalled mid-run, or a startup race where the
1013
+ // Qdrant row landed before the skill cache was seeded). The render path
1014
+ // drops it without surfacing it as a `missingSlugs` page-missing event —
1015
+ // that status is reserved for on-disk concept pages, not catalog-derived
796
1016
  // skill entries.
797
1017
  stageTurn([{ slug: "skills/missing-skill", denseScore: 0.9 }]);
798
1018
  // No `stageSkills` call — cache stays empty.
@@ -808,10 +1028,16 @@ describe("injectMemoryV2Block", () => {
808
1028
  config: makeConfig(),
809
1029
  });
810
1030
 
811
- // `toInject` still records the slug (it ranked into top-K) but the
812
- // block collapses to null because the only entry was a cache miss.
813
- expect(result.toInject).toEqual(["skills/missing-skill"]);
1031
+ // The skill is excluded from `toInject` (and `everInjected`) so future
1032
+ // per-turn runs re-attempt the attach once the cache is populated.
1033
+ // `block` collapses to null because the only candidate was a cache miss.
1034
+ expect(result.toInject).toEqual([]);
814
1035
  expect(result.block).toBeNull();
1036
+
1037
+ // Persisted `everInjected` must not record the missing skill — that
1038
+ // would block retry on a later turn until compaction-driven eviction.
1039
+ const persisted = await hydrate(db, "conv-1");
1040
+ expect(persisted!.everInjected).toEqual([]);
815
1041
  });
816
1042
 
817
1043
  test("returns null when both concept pages and skills are empty", async () => {
@@ -864,7 +1090,7 @@ describe("injectMemoryV2Block", () => {
864
1090
  });
865
1091
 
866
1092
  expect(result.block).not.toBeNull();
867
- expect(result.block).toContain("### alice-vscode");
1093
+ expect(result.block).toContain("# memory/concepts/alice-vscode.md");
868
1094
  // No newly-injected slug — alice was already in everInjected.
869
1095
  expect(result.toInject).toEqual([]);
870
1096
 
@@ -900,9 +1126,9 @@ describe("injectMemoryV2Block", () => {
900
1126
  });
901
1127
 
902
1128
  expect(result.block).not.toBeNull();
903
- expect(result.block).toContain("### alice-vscode");
904
- expect(result.block).toContain("### bob-coffee");
905
- expect(result.block).toContain("### carol-jazz");
1129
+ expect(result.block).toContain("# memory/concepts/alice-vscode.md");
1130
+ expect(result.block).toContain("# memory/concepts/bob-coffee.md");
1131
+ expect(result.block).toContain("# memory/concepts/carol-jazz.md");
906
1132
  // The seeded directed edges (alice→bob, bob→alice, frontmatter-demo→alice)
907
1133
  // mean alice has two incoming predecessors and bob has one, so directed
908
1134
  // spread normalizes alice's activation more aggressively than bob's. The
@@ -1163,11 +1389,520 @@ describe("injectMemoryV2Block", () => {
1163
1389
  expect(telemetryState.recordCalls.length).toBe(0);
1164
1390
  expect(result.toInject).toEqual(["alice-vscode"]);
1165
1391
  expect(result.block).not.toBeNull();
1166
- expect(result.block).toContain("### alice-vscode");
1392
+ expect(result.block).toContain("# memory/concepts/alice-vscode.md");
1167
1393
 
1168
1394
  const persisted = await hydrate(db, "conv-1");
1169
1395
  expect(persisted!.everInjected).toEqual([
1170
1396
  { slug: "alice-vscode", turn: 1 },
1171
1397
  ]);
1172
1398
  });
1399
+
1400
+ // ---------------------------------------------------------------------------
1401
+ // Per-page error isolation + on-throw telemetry
1402
+ // ---------------------------------------------------------------------------
1403
+
1404
+ test("one slug's page-read failing isolates the error — other slugs still render and the corrupt slug records `status: corrupt`", async () => {
1405
+ // Two slugs rank into top-K together. Carol's page reads cleanly; alice's
1406
+ // `readPage` throws a ZodError mimicking the real "unrecognized
1407
+ // frontmatter key" failure that motivated this work. Before the fix, the
1408
+ // bare `Promise.all` rejected and the entire turn lost its block AND its
1409
+ // activation log row. With per-page isolation, carol still renders and
1410
+ // the activation log row marks alice as `corrupt` (telemetry remains
1411
+ // observable for triage).
1412
+ const zodErr = z.object({ x: z.string() }).safeParse({ x: 1 }).error!;
1413
+ pageStoreState.failingSlugs.set("alice-vscode", zodErr);
1414
+ stageTurn([
1415
+ { slug: "alice-vscode", denseScore: 0.95 },
1416
+ { slug: "carol-jazz", denseScore: 0.9 },
1417
+ ]);
1418
+
1419
+ const result = await injectMemoryV2Block({
1420
+ database: db,
1421
+ conversationId: "conv-1",
1422
+ currentTurn: 1,
1423
+ userMessage: "music and editors",
1424
+ assistantMessage: "",
1425
+ nowText: "Now",
1426
+ messageId: "msg-1",
1427
+ config: makeConfig(),
1428
+ });
1429
+
1430
+ // (a) Block is non-null and contains content from the OTHER slug; alice
1431
+ // is dropped from the rendered block but does not poison the batch.
1432
+ expect(result.block).not.toBeNull();
1433
+ expect(result.block).toContain("# memory/concepts/carol-jazz.md");
1434
+ expect(result.block).not.toContain("# memory/concepts/alice-vscode.md");
1435
+
1436
+ // (b) Activation log row exists with carol `injected` and alice
1437
+ // `corrupt`. Status `corrupt` is reserved for read-time throws and is
1438
+ // distinct from `page_missing` (which is null-return / file vanished).
1439
+ expect(telemetryState.recordCalls.length).toBe(1);
1440
+ const row = telemetryState.recordCalls[0] as {
1441
+ mode: string;
1442
+ concepts: Array<{ slug: string; status: string }>;
1443
+ };
1444
+ expect(row.mode).toBe("per-turn");
1445
+ const byslug = new Map(row.concepts.map((c) => [c.slug, c]));
1446
+ expect(byslug.get("alice-vscode")!.status).toBe("corrupt");
1447
+ expect(byslug.get("carol-jazz")!.status).toBe("injected");
1448
+
1449
+ // (c) Both slugs land in `toInject` and `everInjected` — same handling
1450
+ // as `page_missing` (see the phantom-slug test): the slug was attempted
1451
+ // this turn, telemetry records the outcome, and we don't keep re-trying
1452
+ // a stale Qdrant / edge-index entry on every subsequent turn.
1453
+ expect(new Set(result.toInject)).toEqual(
1454
+ new Set(["alice-vscode", "carol-jazz"]),
1455
+ );
1456
+ const persisted = await hydrate(db, "conv-1");
1457
+ const everInjectedSlugs = persisted!.everInjected.map((e) => e.slug);
1458
+ expect(new Set(everInjectedSlugs)).toEqual(
1459
+ new Set(["alice-vscode", "carol-jazz"]),
1460
+ );
1461
+ });
1462
+
1463
+ test("a throw before renderInjectionBlock still flushes telemetry as `mode: errored` and re-throws", async () => {
1464
+ // The activation-state save throws — the most realistic non-render
1465
+ // failure mode (transient SQLite write error mid-injection). The
1466
+ // `injectMemoryV2Block` outer try/finally must (a) flush an activation
1467
+ // log row tagged `mode: "errored"` so silent failures stay observable
1468
+ // in the database, and (b) re-throw so callers (e.g. `prepareMemory`'s
1469
+ // outer catch) see the original error and can degrade gracefully.
1470
+ activationStoreState.saveShouldThrow = true;
1471
+ stageTurn([{ slug: "alice-vscode", denseScore: 0.9 }]);
1472
+
1473
+ let threw: unknown = undefined;
1474
+ try {
1475
+ await injectMemoryV2Block({
1476
+ database: db,
1477
+ conversationId: "conv-1",
1478
+ currentTurn: 1,
1479
+ userMessage: "Alice's editor",
1480
+ assistantMessage: "",
1481
+ nowText: "Now",
1482
+ messageId: "msg-1",
1483
+ config: makeConfig(),
1484
+ });
1485
+ } catch (err) {
1486
+ threw = err;
1487
+ }
1488
+
1489
+ // The original error propagates to the caller.
1490
+ expect(threw).toBeInstanceOf(Error);
1491
+ expect((threw as Error).message).toContain(
1492
+ "simulated activation-store save failure",
1493
+ );
1494
+
1495
+ // A telemetry row was still written, tagged `errored`. `concepts` is
1496
+ // empty because the throw fired before the row-builder ran — that's
1497
+ // expected and documented as part of the contract.
1498
+ expect(telemetryState.recordCalls.length).toBe(1);
1499
+ const row = telemetryState.recordCalls[0] as {
1500
+ mode: string;
1501
+ conversationId: string;
1502
+ turn: number;
1503
+ concepts: unknown[];
1504
+ };
1505
+ expect(row.mode).toBe("errored");
1506
+ expect(row.conversationId).toBe("conv-1");
1507
+ expect(row.turn).toBe(1);
1508
+ expect(row.concepts).toEqual([]);
1509
+ });
1510
+
1511
+ test("activation pipeline routes through `finalizeInjection` — telemetry shape and config snapshot match the contract", async () => {
1512
+ // Pure-refactor regression check: `injectMemoryV2Block` now delegates the
1513
+ // tail (state save + render + telemetry finalization + log write) to a
1514
+ // private `finalizeInjection` helper. This test asserts the helper is
1515
+ // exercised by verifying `recordMemoryV2ActivationLog` is called with the
1516
+ // same arg shape as before — same conversationId/turn/mode, same config
1517
+ // snapshot, and a fully populated concept row whose status was finalized
1518
+ // to `"injected"` on the freshly-attached slug.
1519
+ stageTurn([{ slug: "alice-vscode", denseScore: 0.9 }]);
1520
+
1521
+ const result = await injectMemoryV2Block({
1522
+ database: db,
1523
+ conversationId: "conv-finalize",
1524
+ currentTurn: 7,
1525
+ userMessage: "Alice's editor",
1526
+ assistantMessage: "",
1527
+ nowText: "Now",
1528
+ messageId: "msg-finalize",
1529
+ config: makeConfig(),
1530
+ });
1531
+
1532
+ // The helper rendered + persisted just like the original tail did.
1533
+ expect(result.block).toContain("alice-vscode");
1534
+ expect(result.toInject).toEqual(["alice-vscode"]);
1535
+
1536
+ expect(telemetryState.recordCalls.length).toBe(1);
1537
+ const row = telemetryState.recordCalls[0] as {
1538
+ conversationId: string;
1539
+ turn: number;
1540
+ mode: string;
1541
+ concepts: Array<{
1542
+ slug: string;
1543
+ status: string;
1544
+ finalActivation: number;
1545
+ }>;
1546
+ config: {
1547
+ d: number;
1548
+ c_user: number;
1549
+ c_assistant: number;
1550
+ c_now: number;
1551
+ k: number;
1552
+ hops: number;
1553
+ top_k: number;
1554
+ epsilon: number;
1555
+ };
1556
+ };
1557
+ expect(row.conversationId).toBe("conv-finalize");
1558
+ expect(row.turn).toBe(7);
1559
+ expect(row.mode).toBe("per-turn");
1560
+ // Config snapshot must include all eight tunables — proves the helper is
1561
+ // pulling from `config.memory.v2` rather than synthesizing a partial.
1562
+ expect(Object.keys(row.config).sort()).toEqual(
1563
+ [
1564
+ "c_assistant",
1565
+ "c_now",
1566
+ "c_user",
1567
+ "d",
1568
+ "epsilon",
1569
+ "hops",
1570
+ "k",
1571
+ "top_k",
1572
+ ].sort(),
1573
+ );
1574
+ // Status finalization ran inside the helper — alice was selected and
1575
+ // rendered, so its row reads `injected`.
1576
+ const aliceRow = row.concepts.find((c) => c.slug === "alice-vscode");
1577
+ expect(aliceRow?.status).toBe("injected");
1578
+ });
1579
+
1580
+ // ---------------------------------------------------------------------------
1581
+ // Router mode (flag-gated)
1582
+ // ---------------------------------------------------------------------------
1583
+
1584
+ describe("router mode", () => {
1585
+ test("flag-on: router-selected slugs render and append to everInjected", async () => {
1586
+ // Router picks alice. The activation pipeline never runs — we don't
1587
+ // stage any qdrant responses here, and that's intentional. The
1588
+ // candidate set comes straight from the router's `selectedSlugs`.
1589
+ routerState.nextResult = {
1590
+ selectedSlugs: ["alice-vscode"],
1591
+ failureReason: null,
1592
+ };
1593
+
1594
+ const result = await injectMemoryV2Block({
1595
+ database: db,
1596
+ conversationId: "conv-router-1",
1597
+ currentTurn: 1,
1598
+ userMessage: "Tell me about Alice",
1599
+ assistantMessage: "",
1600
+ nowText: "Now",
1601
+ messageId: "msg-1",
1602
+ config: makeConfig({ router: { enabled: true } }),
1603
+ });
1604
+
1605
+ expect(routerState.callCount).toBe(1);
1606
+ expect(result.toInject).toEqual(["alice-vscode"]);
1607
+ expect(result.block).not.toBeNull();
1608
+ expect(result.block).toContain("# memory/concepts/alice-vscode.md");
1609
+
1610
+ const persisted = await hydrate(db, "conv-router-1");
1611
+ expect(persisted!.everInjected).toEqual([
1612
+ { slug: "alice-vscode", turn: 1 },
1613
+ ]);
1614
+ // Router mode persists an empty sparse activation map — the router
1615
+ // does not compute spreading-activation scores.
1616
+ expect(persisted!.state).toEqual({});
1617
+
1618
+ // Telemetry: success rows get `mode: "router"` and `source: "router"`,
1619
+ // with all activation fields zeroed.
1620
+ expect(telemetryState.recordCalls.length).toBe(1);
1621
+ const row = telemetryState.recordCalls[0] as {
1622
+ mode: string;
1623
+ concepts: Array<{
1624
+ slug: string;
1625
+ source: string;
1626
+ status: string;
1627
+ finalActivation: number;
1628
+ ownActivation: number;
1629
+ }>;
1630
+ };
1631
+ expect(row.mode).toBe("router");
1632
+ const aliceRow = row.concepts.find((c) => c.slug === "alice-vscode");
1633
+ expect(aliceRow).toBeDefined();
1634
+ expect(aliceRow!.source).toBe("router");
1635
+ expect(aliceRow!.status).toBe("injected");
1636
+ expect(aliceRow!.finalActivation).toBe(0);
1637
+ expect(aliceRow!.ownActivation).toBe(0);
1638
+ });
1639
+
1640
+ test("flag-on: router failure logs warn, writes mode:`errored` telemetry, returns null block", async () => {
1641
+ routerState.nextResult = {
1642
+ selectedSlugs: [],
1643
+ failureReason: "api_error",
1644
+ };
1645
+
1646
+ const result = await injectMemoryV2Block({
1647
+ database: db,
1648
+ conversationId: "conv-router-fail",
1649
+ currentTurn: 3,
1650
+ userMessage: "anything",
1651
+ assistantMessage: "ok",
1652
+ nowText: "Now",
1653
+ messageId: "msg-fail",
1654
+ config: makeConfig({ router: { enabled: true } }),
1655
+ });
1656
+
1657
+ expect(result.block).toBeNull();
1658
+ expect(result.toInject).toEqual([]);
1659
+
1660
+ // Stub state still advanced.
1661
+ const persisted = await hydrate(db, "conv-router-fail");
1662
+ expect(persisted).not.toBeNull();
1663
+ expect(persisted!.currentTurn).toBe(3);
1664
+ expect(persisted!.messageId).toBe("msg-fail");
1665
+ expect(persisted!.state).toEqual({});
1666
+ expect(persisted!.everInjected).toEqual([]);
1667
+
1668
+ // Single telemetry row with `mode: "errored"` (not `"router"`).
1669
+ expect(telemetryState.recordCalls.length).toBe(1);
1670
+ const row = telemetryState.recordCalls[0] as {
1671
+ mode: string;
1672
+ conversationId: string;
1673
+ turn: number;
1674
+ concepts: unknown[];
1675
+ };
1676
+ expect(row.mode).toBe("errored");
1677
+ expect(row.conversationId).toBe("conv-router-fail");
1678
+ expect(row.turn).toBe(3);
1679
+ expect(row.concepts).toEqual([]);
1680
+ });
1681
+
1682
+ test("flag-on: router abstention (empty selectedSlugs, no failure) writes mode:`router` row with no injected pages", async () => {
1683
+ routerState.nextResult = {
1684
+ selectedSlugs: [],
1685
+ failureReason: null,
1686
+ };
1687
+
1688
+ const result = await injectMemoryV2Block({
1689
+ database: db,
1690
+ conversationId: "conv-router-abstain",
1691
+ currentTurn: 1,
1692
+ userMessage: "small talk",
1693
+ assistantMessage: "",
1694
+ nowText: "Now",
1695
+ messageId: "msg-abstain",
1696
+ config: makeConfig({ router: { enabled: true } }),
1697
+ });
1698
+
1699
+ expect(result.block).toBeNull();
1700
+ expect(result.toInject).toEqual([]);
1701
+
1702
+ // No prior everInjected to dedup against, so toInject is empty and
1703
+ // nothing renders. State still advanced.
1704
+ const persisted = await hydrate(db, "conv-router-abstain");
1705
+ expect(persisted!.everInjected).toEqual([]);
1706
+ expect(persisted!.currentTurn).toBe(1);
1707
+
1708
+ // Telemetry: `mode: "router"` row with zero injected pages.
1709
+ expect(telemetryState.recordCalls.length).toBe(1);
1710
+ const row = telemetryState.recordCalls[0] as {
1711
+ mode: string;
1712
+ concepts: Array<{ slug: string; status: string }>;
1713
+ };
1714
+ expect(row.mode).toBe("router");
1715
+ const injectedCount = row.concepts.filter(
1716
+ (c) => c.status === "injected",
1717
+ ).length;
1718
+ expect(injectedCount).toBe(0);
1719
+ });
1720
+
1721
+ test("flag-on: router-selected slug whose page is missing on disk records `page_missing` and is NOT added to everInjected", async () => {
1722
+ routerState.nextResult = {
1723
+ selectedSlugs: ["phantom-router-slug"],
1724
+ failureReason: null,
1725
+ };
1726
+
1727
+ const result = await injectMemoryV2Block({
1728
+ database: db,
1729
+ conversationId: "conv-router-missing",
1730
+ currentTurn: 1,
1731
+ userMessage: "phantom",
1732
+ assistantMessage: "",
1733
+ nowText: "Now",
1734
+ messageId: "msg-missing",
1735
+ config: makeConfig({ router: { enabled: true } }),
1736
+ });
1737
+
1738
+ // No backing page → block collapses to null.
1739
+ expect(result.block).toBeNull();
1740
+ // toInject mirrors `newlyInjected` from `finalizeInjection` — the
1741
+ // missing slug still flowed through `slugsToRender` so it's recorded
1742
+ // here (matching the activation-mode phantom-slug contract).
1743
+ expect(result.toInject).toEqual(["phantom-router-slug"]);
1744
+
1745
+ // Activation-mode parity: the phantom slug DOES land in everInjected
1746
+ // so we don't infinite-retry it. (This matches the behavior the
1747
+ // existing `returns null block when toInject slugs all reference
1748
+ // missing pages` test asserts for activation mode.)
1749
+ const persisted = await hydrate(db, "conv-router-missing");
1750
+ expect(persisted!.everInjected).toEqual([
1751
+ { slug: "phantom-router-slug", turn: 1 },
1752
+ ]);
1753
+
1754
+ // Telemetry: `status: "page_missing"` for the phantom slug.
1755
+ expect(telemetryState.recordCalls.length).toBe(1);
1756
+ const row = telemetryState.recordCalls[0] as {
1757
+ mode: string;
1758
+ concepts: Array<{ slug: string; status: string; source: string }>;
1759
+ };
1760
+ expect(row.mode).toBe("router");
1761
+ const phantom = row.concepts.find(
1762
+ (c) => c.slug === "phantom-router-slug",
1763
+ );
1764
+ expect(phantom).toBeDefined();
1765
+ expect(phantom!.status).toBe("page_missing");
1766
+ expect(phantom!.source).toBe("router");
1767
+ });
1768
+
1769
+ test("flag-on: router re-picking a prior-everInjected slug does NOT re-render it; non-overlapping picks render and append to everInjected", async () => {
1770
+ // Turn 1: router picks alice. Standard append.
1771
+ routerState.nextResult = {
1772
+ selectedSlugs: ["alice-vscode"],
1773
+ failureReason: null,
1774
+ };
1775
+ const turn1 = await injectMemoryV2Block({
1776
+ database: db,
1777
+ conversationId: "conv-router-dedup",
1778
+ currentTurn: 1,
1779
+ userMessage: "Tell me about Alice",
1780
+ assistantMessage: "",
1781
+ nowText: "Now",
1782
+ messageId: "msg-1",
1783
+ config: makeConfig({ router: { enabled: true } }),
1784
+ });
1785
+ expect(turn1.toInject).toEqual(["alice-vscode"]);
1786
+
1787
+ // Turn 2: router re-picks alice (the "re-anchor" prompt branch) AND
1788
+ // adds bob. The block must NOT contain alice's body — her cached
1789
+ // attachment from turn 1 is still on the prior user message — but
1790
+ // must contain bob's.
1791
+ telemetryState.recordCalls.length = 0;
1792
+ routerState.nextResult = {
1793
+ selectedSlugs: ["alice-vscode", "bob-coffee"],
1794
+ failureReason: null,
1795
+ };
1796
+ const turn2 = await injectMemoryV2Block({
1797
+ database: db,
1798
+ conversationId: "conv-router-dedup",
1799
+ currentTurn: 2,
1800
+ userMessage: "And Bob?",
1801
+ assistantMessage: "Sure",
1802
+ nowText: "Now",
1803
+ messageId: "msg-2",
1804
+ config: makeConfig({ router: { enabled: true } }),
1805
+ });
1806
+
1807
+ // Re-picked alice was deduped; only bob is freshly injected.
1808
+ expect(turn2.toInject).toEqual(["bob-coffee"]);
1809
+ expect(turn2.block).not.toBeNull();
1810
+ expect(turn2.block).toContain("# memory/concepts/bob-coffee.md");
1811
+ expect(turn2.block).toContain("Bob takes his coffee");
1812
+ expect(turn2.block).not.toContain("VS Code");
1813
+ expect(turn2.block).not.toContain("# memory/concepts/alice-vscode.md");
1814
+
1815
+ // everInjected only gained bob — alice was already there.
1816
+ const persisted = await hydrate(db, "conv-router-dedup");
1817
+ expect(persisted!.everInjected).toEqual([
1818
+ { slug: "alice-vscode", turn: 1 },
1819
+ { slug: "bob-coffee", turn: 2 },
1820
+ ]);
1821
+ });
1822
+
1823
+ test("flag-on: telemetry distinguishes `source: router` (router picks) from `source: carry_over` (prior-everInjected slugs the router did not re-pick)", async () => {
1824
+ // Turn 1: seed everInjected with alice.
1825
+ routerState.nextResult = {
1826
+ selectedSlugs: ["alice-vscode"],
1827
+ failureReason: null,
1828
+ };
1829
+ await injectMemoryV2Block({
1830
+ database: db,
1831
+ conversationId: "conv-router-source",
1832
+ currentTurn: 1,
1833
+ userMessage: "Alice",
1834
+ assistantMessage: "",
1835
+ nowText: "Now",
1836
+ messageId: "msg-1",
1837
+ config: makeConfig({ router: { enabled: true } }),
1838
+ });
1839
+ telemetryState.recordCalls.length = 0;
1840
+
1841
+ // Turn 2: router picks bob only. alice is still in everInjected but
1842
+ // not re-picked — her telemetry row must read `source: carry_over`,
1843
+ // not `source: router`.
1844
+ routerState.nextResult = {
1845
+ selectedSlugs: ["bob-coffee"],
1846
+ failureReason: null,
1847
+ };
1848
+ await injectMemoryV2Block({
1849
+ database: db,
1850
+ conversationId: "conv-router-source",
1851
+ currentTurn: 2,
1852
+ userMessage: "Bob",
1853
+ assistantMessage: "",
1854
+ nowText: "Now",
1855
+ messageId: "msg-2",
1856
+ config: makeConfig({ router: { enabled: true } }),
1857
+ });
1858
+
1859
+ expect(telemetryState.recordCalls.length).toBe(1);
1860
+ const row = telemetryState.recordCalls[0] as {
1861
+ mode: string;
1862
+ concepts: Array<{ slug: string; source: string; status: string }>;
1863
+ };
1864
+ expect(row.mode).toBe("router");
1865
+ const aliceRow = row.concepts.find((c) => c.slug === "alice-vscode");
1866
+ const bobRow = row.concepts.find((c) => c.slug === "bob-coffee");
1867
+ expect(aliceRow).toBeDefined();
1868
+ expect(bobRow).toBeDefined();
1869
+ expect(aliceRow!.source).toBe("carry_over");
1870
+ expect(aliceRow!.status).toBe("in_context");
1871
+ expect(bobRow!.source).toBe("router");
1872
+ expect(bobRow!.status).toBe("injected");
1873
+ });
1874
+
1875
+ test("flag-off (default): activation pipeline still runs unchanged", async () => {
1876
+ // Regression check — with the router flag explicitly off (the
1877
+ // production default), `runRouter` must never be called and the
1878
+ // activation pipeline drives the selection just like before.
1879
+ stageTurn([{ slug: "alice-vscode", denseScore: 0.9 }]);
1880
+ routerState.nextResult = {
1881
+ selectedSlugs: ["should-not-be-used"],
1882
+ failureReason: null,
1883
+ };
1884
+
1885
+ const result = await injectMemoryV2Block({
1886
+ database: db,
1887
+ conversationId: "conv-flag-off",
1888
+ currentTurn: 1,
1889
+ userMessage: "Alice's editor",
1890
+ assistantMessage: "",
1891
+ nowText: "Now",
1892
+ messageId: "msg-1",
1893
+ config: makeConfig({ router: { enabled: false } }),
1894
+ });
1895
+
1896
+ // Router was not called.
1897
+ expect(routerState.callCount).toBe(0);
1898
+ // Activation pipeline produced its normal result.
1899
+ expect(result.toInject).toEqual(["alice-vscode"]);
1900
+ expect(result.block).toContain("# memory/concepts/alice-vscode.md");
1901
+
1902
+ // Telemetry row carries the activation mode, not router.
1903
+ expect(telemetryState.recordCalls.length).toBe(1);
1904
+ const row = telemetryState.recordCalls[0] as { mode: string };
1905
+ expect(row.mode).toBe("per-turn");
1906
+ });
1907
+ });
1173
1908
  });