@vellumai/assistant 0.7.2 → 0.8.0

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 (424) hide show
  1. package/ARCHITECTURE.md +45 -29
  2. package/Dockerfile +1 -0
  3. package/__tests__/permissions/gateway-threshold-reader.test.ts +236 -9
  4. package/bun.lock +3 -0
  5. package/docs/architecture/memory.md +5 -2
  6. package/knip.json +1 -0
  7. package/node_modules/@vellumai/gateway-client/src/ipc-client.ts +13 -4
  8. package/node_modules/@vellumai/ipc-server-utils/bun.lock +24 -0
  9. package/node_modules/@vellumai/ipc-server-utils/package.json +18 -0
  10. package/node_modules/@vellumai/ipc-server-utils/src/index.ts +6 -0
  11. package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.test.ts +430 -0
  12. package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.ts +221 -0
  13. package/node_modules/@vellumai/ipc-server-utils/tsconfig.json +20 -0
  14. package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +0 -9
  15. package/node_modules/@vellumai/slack-text/src/index.test.ts +18 -35
  16. package/node_modules/@vellumai/slack-text/src/index.ts +2 -48
  17. package/openapi.yaml +470 -25
  18. package/package.json +3 -1
  19. package/src/__tests__/annotate-risk-options.test.ts +291 -0
  20. package/src/__tests__/app-control-flow.test.ts +21 -11
  21. package/src/__tests__/approval-cascade.test.ts +8 -16
  22. package/src/__tests__/approval-routes-http.test.ts +6 -0
  23. package/src/__tests__/assistant-event-hub.test.ts +48 -0
  24. package/src/__tests__/assistant-event.test.ts +0 -10
  25. package/src/__tests__/assistant-events-sse-hardening.test.ts +2 -7
  26. package/src/__tests__/assistant-feature-flags-integration.test.ts +18 -0
  27. package/src/__tests__/auto-analysis-end-to-end.test.ts +48 -0
  28. package/src/__tests__/background-workers-disk-pressure.test.ts +268 -0
  29. package/src/__tests__/call-constants.test.ts +10 -1
  30. package/src/__tests__/call-controller.test.ts +127 -0
  31. package/src/__tests__/call-conversation-messages.test.ts +8 -2
  32. package/src/__tests__/channel-inbound-disk-pressure.test.ts +537 -0
  33. package/src/__tests__/channel-readiness-service.test.ts +4 -2
  34. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +58 -28
  35. package/src/__tests__/config-loader-backfill.test.ts +379 -0
  36. package/src/__tests__/config-loader-platform-defaults.test.ts +284 -1
  37. package/src/__tests__/config-schema.test.ts +1 -0
  38. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +18 -9
  39. package/src/__tests__/config-watcher.test.ts +140 -69
  40. package/src/__tests__/context-search-agent-runner.test.ts +61 -3
  41. package/src/__tests__/context-search-conversations-source.test.ts +0 -24
  42. package/src/__tests__/context-search-fanout.test.ts +0 -1
  43. package/src/__tests__/context-search-memory-source.test.ts +6 -33
  44. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -2
  45. package/src/__tests__/context-search-pkb-source.test.ts +12 -7
  46. package/src/__tests__/context-search-workspace-source.test.ts +0 -1
  47. package/src/__tests__/conversation-abort-tool-results.test.ts +1 -0
  48. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +223 -0
  49. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -1
  50. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -1
  51. package/src/__tests__/conversation-agent-loop.test.ts +457 -8
  52. package/src/__tests__/conversation-confirmation-signals.test.ts +5 -13
  53. package/src/__tests__/conversation-error.test.ts +150 -3
  54. package/src/__tests__/conversation-init.benchmark.test.ts +1 -1
  55. package/src/__tests__/conversation-process-callsite.test.ts +38 -0
  56. package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -0
  57. package/src/__tests__/conversation-runtime-assembly.test.ts +74 -0
  58. package/src/__tests__/conversation-slash-unknown.test.ts +1 -0
  59. package/src/__tests__/conversation-speed-override.test.ts +0 -3
  60. package/src/__tests__/conversation-store.test.ts +0 -18
  61. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +170 -9
  62. package/src/__tests__/conversation-surfaces-app-control.test.ts +15 -4
  63. package/src/__tests__/conversation-surfaces-data-persist.test.ts +476 -0
  64. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +61 -5
  65. package/src/__tests__/conversation-workspace-injection.test.ts +1 -1
  66. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
  67. package/src/__tests__/credentials-cli.test.ts +7 -0
  68. package/src/__tests__/cu-unified-flow.test.ts +176 -10
  69. package/src/__tests__/date-context.test.ts +164 -2
  70. package/src/__tests__/disk-pressure-guard.test.ts +262 -0
  71. package/src/__tests__/disk-pressure-lifecycle.test.ts +168 -0
  72. package/src/__tests__/disk-pressure-policy.test.ts +241 -0
  73. package/src/__tests__/disk-pressure-routes.test.ts +379 -0
  74. package/src/__tests__/disk-pressure-tools.test.ts +277 -0
  75. package/src/__tests__/disk-usage.test.ts +150 -0
  76. package/src/__tests__/events-client-registration.test.ts +52 -0
  77. package/src/__tests__/events-dev-bypass-actor.test.ts +162 -0
  78. package/src/__tests__/file-write-tool.test.ts +4 -10
  79. package/src/__tests__/filing-service.test.ts +2 -20
  80. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +10 -26
  81. package/src/__tests__/heartbeat-disk-pressure.test.ts +183 -0
  82. package/src/__tests__/heartbeat-service.test.ts +260 -11
  83. package/src/__tests__/host-app-control-proxy.test.ts +195 -25
  84. package/src/__tests__/host-bash-proxy.test.ts +227 -34
  85. package/src/__tests__/host-bash-routes.test.ts +178 -13
  86. package/src/__tests__/host-cu-proxy.test.ts +210 -3
  87. package/src/__tests__/host-cu-routes-targeted.test.ts +141 -12
  88. package/src/__tests__/host-file-proxy-targeted.test.ts +48 -9
  89. package/src/__tests__/host-file-proxy.test.ts +268 -6
  90. package/src/__tests__/host-file-routes-targeted.test.ts +175 -17
  91. package/src/__tests__/host-transfer-proxy-targeted.test.ts +408 -59
  92. package/src/__tests__/host-transfer-routes-targeted.test.ts +232 -17
  93. package/src/__tests__/http-user-message-parity.test.ts +107 -1
  94. package/src/__tests__/injector-chain.test.ts +36 -16
  95. package/src/__tests__/injector-disk-pressure.test.ts +224 -0
  96. package/src/__tests__/injector-pkb-v2-silenced.test.ts +10 -7
  97. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +154 -67
  98. package/src/__tests__/managed-profile-guard.test.ts +18 -0
  99. package/src/__tests__/mcp-abort-signal.test.ts +130 -0
  100. package/src/__tests__/memory-admin-recall.test.ts +3 -11
  101. package/src/__tests__/memory-retrieval-pipeline.test.ts +22 -1
  102. package/src/__tests__/normalize-onboarding.test.ts +180 -0
  103. package/src/__tests__/notification-decision-fallback.test.ts +91 -0
  104. package/src/__tests__/notification-decision-strategy.test.ts +22 -0
  105. package/src/__tests__/oauth-cli.test.ts +121 -0
  106. package/src/__tests__/oauth-connect-routes.test.ts +316 -0
  107. package/src/__tests__/oauth-provider-seed-logos.test.ts +24 -2
  108. package/src/__tests__/onboarding-persona-write.test.ts +308 -0
  109. package/src/__tests__/openai-provider.test.ts +45 -8
  110. package/src/__tests__/persist-onboarding-artifacts.test.ts +44 -64
  111. package/src/__tests__/platform-callback-registration.test.ts +21 -4
  112. package/src/__tests__/platform.test.ts +2 -1
  113. package/src/__tests__/playbook-execution.test.ts +0 -43
  114. package/src/__tests__/plugin-tool-contribution.test.ts +47 -0
  115. package/src/__tests__/prechat-onboarding-contract.test.ts +214 -27
  116. package/src/__tests__/provider-tool-name.test.ts +23 -0
  117. package/src/__tests__/relay-server.test.ts +60 -5
  118. package/src/__tests__/runtime-events-sse.test.ts +4 -8
  119. package/src/__tests__/scheduler-disk-pressure.test.ts +148 -0
  120. package/src/__tests__/secret-ingress-http.test.ts +0 -1
  121. package/src/__tests__/secret-prompt-log-hygiene.test.ts +7 -5
  122. package/src/__tests__/secret-prompter-channel-fallback.test.ts +7 -5
  123. package/src/__tests__/secret-response-routing.test.ts +7 -5
  124. package/src/__tests__/server-history-render.test.ts +82 -0
  125. package/src/__tests__/skill-include-graph.test.ts +31 -0
  126. package/src/__tests__/skill-load-tool.test.ts +44 -16
  127. package/src/__tests__/skills.test.ts +39 -0
  128. package/src/__tests__/suggestion-routes.test.ts +46 -0
  129. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -42
  130. package/src/__tests__/tool-executor.test.ts +155 -0
  131. package/src/__tests__/twilio-validation.test.ts +2 -2
  132. package/src/__tests__/voice-session-bridge.test.ts +3 -0
  133. package/src/__tests__/workspace-migration-065-bump-stale-heartbeat-interval.test.ts +122 -0
  134. package/src/__tests__/workspace-migration-066-seed-heartbeat-callsite-cost-default.test.ts +285 -0
  135. package/src/__tests__/workspace-migration-068-release-notes-local-timezone.test.ts +90 -0
  136. package/src/__tests__/workspace-migration-069-seed-onboarding-threads.test.ts +120 -0
  137. package/src/__tests__/workspace-migration-071-remove-safe-storage-release-note.test.ts +206 -0
  138. package/src/__tests__/workspace-migration-safe-storage-limits-release.test.ts +78 -0
  139. package/src/agent/loop.ts +11 -0
  140. package/src/approvals/guardian-request-resolvers.ts +3 -32
  141. package/src/backup/snapshot-lock.ts +2 -27
  142. package/src/bundler/compiler-tools.ts +3 -2
  143. package/src/calls/call-constants.ts +5 -8
  144. package/src/calls/call-controller.ts +130 -67
  145. package/src/calls/call-conversation-messages.ts +46 -10
  146. package/src/calls/relay-server.ts +7 -1
  147. package/src/calls/voice-session-bridge.ts +1 -1
  148. package/src/cli/commands/__tests__/webhooks.test.ts +0 -4
  149. package/src/cli/commands/bash.ts +35 -108
  150. package/src/cli/commands/contacts.ts +64 -25
  151. package/src/cli/commands/credentials.ts +56 -0
  152. package/src/cli/commands/memory-v2.ts +11 -10
  153. package/src/cli/commands/oauth/__tests__/connect.test.ts +401 -219
  154. package/src/cli/commands/oauth/connect.ts +124 -40
  155. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -3
  156. package/src/cli/commands/platform/__tests__/connect.test.ts +7 -1
  157. package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -1
  158. package/src/cli/commands/platform/__tests__/status.test.ts +103 -6
  159. package/src/cli/commands/platform/index.ts +16 -7
  160. package/src/cli/commands/status.ts +57 -0
  161. package/src/cli/program.ts +4 -2
  162. package/src/config/assistant-feature-flags.ts +13 -3
  163. package/src/config/bundled-skills/app-builder/SKILL.md +1 -3
  164. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +4 -3
  165. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +13 -7
  166. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +2 -2
  167. package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +2 -2
  168. package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +2 -2
  169. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -2
  170. package/src/config/env.ts +0 -8
  171. package/src/config/feature-flag-registry.json +13 -5
  172. package/src/config/loader.ts +199 -27
  173. package/src/config/schemas/__tests__/memory-v2.test.ts +10 -5
  174. package/src/config/schemas/call-site-catalog.ts +14 -0
  175. package/src/config/schemas/channels.ts +0 -5
  176. package/src/config/schemas/heartbeat.ts +1 -1
  177. package/src/config/schemas/llm.ts +2 -0
  178. package/src/config/schemas/memory-lifecycle.ts +13 -0
  179. package/src/config/schemas/memory-v2.ts +76 -12
  180. package/src/config/schemas/platform.ts +43 -3
  181. package/src/config/schemas/services.ts +28 -0
  182. package/src/config/seed-inference-profiles.ts +230 -33
  183. package/src/contacts/contact-store.ts +0 -25
  184. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +32 -0
  185. package/src/daemon/__tests__/conversation-tool-setup.test.ts +86 -25
  186. package/src/daemon/assistant-attachments.ts +4 -4
  187. package/src/daemon/config-watcher.ts +85 -57
  188. package/src/daemon/conversation-agent-loop-handlers.ts +38 -0
  189. package/src/daemon/conversation-agent-loop.ts +183 -43
  190. package/src/daemon/conversation-error.ts +87 -15
  191. package/src/daemon/conversation-lifecycle.ts +22 -10
  192. package/src/daemon/conversation-process.ts +8 -0
  193. package/src/daemon/conversation-runtime-assembly.ts +26 -0
  194. package/src/daemon/conversation-store.ts +2 -2
  195. package/src/daemon/conversation-surfaces.ts +211 -29
  196. package/src/daemon/conversation-tool-setup.ts +66 -19
  197. package/src/daemon/conversation.ts +18 -23
  198. package/src/daemon/date-context.ts +71 -22
  199. package/src/daemon/disk-pressure-background-gate.ts +73 -0
  200. package/src/daemon/disk-pressure-guard.ts +343 -0
  201. package/src/daemon/disk-pressure-policy.ts +163 -0
  202. package/src/daemon/handlers/shared.ts +26 -1
  203. package/src/daemon/handlers/skills.ts +3 -4
  204. package/src/daemon/host-app-control-proxy.ts +137 -41
  205. package/src/daemon/host-bash-proxy.ts +47 -22
  206. package/src/daemon/host-browser-proxy.ts +1 -1
  207. package/src/daemon/host-cu-proxy.ts +50 -4
  208. package/src/daemon/host-file-proxy.ts +44 -8
  209. package/src/daemon/host-transfer-proxy.ts +97 -6
  210. package/src/daemon/lifecycle.ts +167 -101
  211. package/src/daemon/meet-host-supervisor.ts +4 -4
  212. package/src/daemon/meet-manifest-loader.ts +0 -1
  213. package/src/daemon/memory-v2-startup.ts +66 -15
  214. package/src/daemon/message-protocol.ts +3 -0
  215. package/src/daemon/message-types/conversations.ts +4 -0
  216. package/src/daemon/message-types/disk-pressure.ts +9 -0
  217. package/src/daemon/message-types/messages.ts +22 -1
  218. package/src/daemon/profiler-run-store.ts +5 -5
  219. package/src/daemon/tool-setup-types.ts +2 -2
  220. package/src/documents/document-store.ts +119 -0
  221. package/src/filing/filing-service.ts +29 -5
  222. package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +9 -16
  223. package/src/heartbeat/__tests__/heartbeat-run-store.test.ts +36 -0
  224. package/src/heartbeat/heartbeat-run-store.ts +13 -0
  225. package/src/heartbeat/heartbeat-service.ts +205 -31
  226. package/src/home/feed-scheduler.ts +18 -0
  227. package/src/inbound/platform-callback-registration.ts +8 -15
  228. package/src/ipc/__tests__/clients-list-ipc.test.ts +169 -0
  229. package/src/ipc/assistant-server.ts +149 -38
  230. package/src/ipc/gateway-client.ts +37 -3
  231. package/src/ipc/skill-server.ts +99 -42
  232. package/src/live-voice/live-voice-archive.ts +4 -4
  233. package/src/live-voice/protocol.ts +5 -7
  234. package/src/media/image-service.ts +1 -7
  235. package/src/memory/__tests__/fixtures/memory-v2-activation-fixtures.ts +21 -13
  236. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +34 -51
  237. package/src/memory/__tests__/memory-v2-activation-log-store.test.ts +0 -6
  238. package/src/memory/__tests__/memory-v2-concept-frequency.test.ts +272 -0
  239. package/src/memory/admin.ts +5 -9
  240. package/src/memory/context-search/agent-runner.ts +19 -2
  241. package/src/memory/context-search/sources/conversations.ts +2 -11
  242. package/src/memory/context-search/sources/memory-v2.ts +1 -16
  243. package/src/memory/context-search/sources/memory.ts +2 -3
  244. package/src/memory/context-search/sources/pkb.ts +2 -3
  245. package/src/memory/context-search/types.ts +0 -1
  246. package/src/memory/conversation-crud.ts +4 -12
  247. package/src/memory/db-init.ts +2 -0
  248. package/src/memory/embedding-runtime-manager.ts +119 -5
  249. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +136 -82
  250. package/src/memory/graph/__tests__/handle-remember-v2.test.ts +11 -26
  251. package/src/memory/graph/conversation-graph-memory.ts +72 -61
  252. package/src/memory/graph/extraction.ts +1 -3
  253. package/src/memory/graph/graph-search.test.ts +11 -67
  254. package/src/memory/graph/graph-search.ts +4 -24
  255. package/src/memory/graph/retriever.test.ts +12 -1
  256. package/src/memory/graph/retriever.ts +10 -15
  257. package/src/memory/graph/tool-handlers.ts +3 -4
  258. package/src/memory/graph/tools.ts +4 -4
  259. package/src/memory/indexer.ts +53 -45
  260. package/src/memory/job-handlers/backfill.ts +2 -11
  261. package/src/memory/job-handlers/cleanup.ts +43 -0
  262. package/src/memory/job-handlers/embedding.ts +6 -8
  263. package/src/memory/job-handlers/summarization.ts +2 -7
  264. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +116 -0
  265. package/src/memory/jobs/embed-concept-page.ts +223 -87
  266. package/src/memory/jobs-store.ts +48 -0
  267. package/src/memory/jobs-worker.ts +85 -43
  268. package/src/memory/memory-v2-activation-log-store.ts +32 -14
  269. package/src/memory/memory-v2-concept-frequency.ts +169 -0
  270. package/src/memory/migrations/239-trace-events-created-at-index.ts +18 -0
  271. package/src/memory/migrations/index.ts +1 -0
  272. package/src/memory/pkb/pkb-search.test.ts +7 -0
  273. package/src/memory/pkb/pkb-search.ts +4 -5
  274. package/src/memory/qdrant-client.ts +3 -13
  275. package/src/memory/rerank-local.ts +374 -0
  276. package/src/memory/search/semantic.ts +10 -72
  277. package/src/memory/trace-event-store.ts +1 -17
  278. package/src/memory/v2/__tests__/activation.test.ts +346 -255
  279. package/src/memory/v2/__tests__/consolidation-job.test.ts +61 -40
  280. package/src/memory/v2/__tests__/injection.test.ts +297 -190
  281. package/src/memory/v2/__tests__/prompts-consolidation.test.ts +61 -2
  282. package/src/memory/v2/__tests__/qdrant.test.ts +326 -9
  283. package/src/memory/v2/__tests__/reranker.test.ts +338 -0
  284. package/src/memory/v2/__tests__/sim.test.ts +113 -196
  285. package/src/memory/v2/__tests__/skill-store.test.ts +71 -65
  286. package/src/memory/v2/__tests__/static-context.test.ts +77 -14
  287. package/src/memory/v2/__tests__/sweep-job.test.ts +19 -33
  288. package/src/memory/v2/activation.ts +149 -156
  289. package/src/memory/v2/consolidation-job.ts +69 -20
  290. package/src/memory/v2/injection.ts +75 -68
  291. package/src/memory/v2/page-store.ts +39 -0
  292. package/src/memory/v2/prompts/consolidation.ts +41 -1
  293. package/src/memory/v2/qdrant.ts +306 -46
  294. package/src/memory/v2/reranker.ts +177 -0
  295. package/src/memory/v2/sim.ts +77 -110
  296. package/src/memory/v2/skill-content.ts +4 -3
  297. package/src/memory/v2/skill-store.ts +82 -59
  298. package/src/memory/v2/static-context.ts +26 -8
  299. package/src/memory/v2/sweep-job.ts +5 -6
  300. package/src/memory/v2/types.ts +17 -10
  301. package/src/notifications/copy-composer.ts +47 -0
  302. package/src/notifications/decision-engine.ts +46 -0
  303. package/src/notifications/signal.ts +4 -0
  304. package/src/oauth/AGENTS.md +3 -1
  305. package/src/oauth/__tests__/oauth-connect-state.test.ts +137 -0
  306. package/src/oauth/connect-orchestrator.ts +2 -0
  307. package/src/oauth/connection-resolver.test.ts +66 -1
  308. package/src/oauth/connection-resolver.ts +55 -1
  309. package/src/oauth/oauth-connect-state.ts +77 -0
  310. package/src/oauth/seed-providers.ts +58 -1
  311. package/src/permissions/gateway-threshold-reader.ts +116 -8
  312. package/src/permissions/prompter.ts +86 -96
  313. package/src/permissions/secret-prompter.ts +31 -31
  314. package/src/plugins/defaults/injectors.ts +36 -4
  315. package/src/plugins/defaults/memory-retrieval.ts +5 -6
  316. package/src/plugins/types.ts +7 -0
  317. package/src/proactive-artifact/aux-message-injector.ts +74 -0
  318. package/src/proactive-artifact/decision.test.ts +226 -0
  319. package/src/proactive-artifact/decision.ts +165 -0
  320. package/src/proactive-artifact/index.ts +7 -0
  321. package/src/proactive-artifact/job.test.ts +914 -0
  322. package/src/proactive-artifact/job.ts +366 -0
  323. package/src/proactive-artifact/message-copy.ts +58 -0
  324. package/src/proactive-artifact/trigger-state.test.ts +277 -0
  325. package/src/proactive-artifact/trigger-state.ts +119 -0
  326. package/src/prompts/normalize-onboarding.ts +80 -0
  327. package/src/prompts/persona-resolver.ts +101 -9
  328. package/src/prompts/system-prompt.ts +21 -7
  329. package/src/prompts/templates/BOOTSTRAP.md +13 -5
  330. package/src/prompts/templates/SOUL.md +13 -28
  331. package/src/providers/__tests__/retry-callsite.test.ts +222 -1
  332. package/src/providers/model-intents.ts +7 -0
  333. package/src/providers/openrouter/client.ts +8 -0
  334. package/src/providers/retry.ts +50 -0
  335. package/src/providers/types.ts +1 -0
  336. package/src/runtime/__tests__/agent-wake.test.ts +456 -3
  337. package/src/runtime/agent-wake.ts +238 -100
  338. package/src/runtime/assistant-event-hub.ts +36 -6
  339. package/src/runtime/assistant-event.ts +0 -1
  340. package/src/runtime/auth/__tests__/route-policy.test.ts +64 -0
  341. package/src/runtime/auth/route-policy.ts +15 -1
  342. package/src/runtime/auth/same-actor.ts +216 -0
  343. package/src/runtime/channel-approvals.ts +3 -2
  344. package/src/runtime/channel-retry-sweep.ts +65 -1
  345. package/src/runtime/local-actor-identity.ts +52 -11
  346. package/src/runtime/pending-interactions.ts +27 -15
  347. package/src/runtime/routes/__tests__/client-routes.test.ts +155 -0
  348. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +0 -5
  349. package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +1 -1
  350. package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +147 -0
  351. package/src/runtime/routes/approval-routes.ts +7 -3
  352. package/src/runtime/routes/client-routes.ts +20 -2
  353. package/src/runtime/routes/consolidation-routes.ts +8 -9
  354. package/src/runtime/routes/contact-routes.ts +0 -25
  355. package/src/runtime/routes/conversation-query-routes.ts +44 -1
  356. package/src/runtime/routes/conversation-routes.ts +35 -26
  357. package/src/runtime/routes/debug-bash-routes.ts +165 -0
  358. package/src/runtime/routes/disk-pressure-routes.ts +121 -0
  359. package/src/runtime/routes/document-pdf-renderer.ts +6 -2
  360. package/src/runtime/routes/documents-routes.ts +2 -75
  361. package/src/runtime/routes/events-routes.ts +41 -9
  362. package/src/runtime/routes/filing-routes.ts +2 -3
  363. package/src/runtime/routes/host-bash-routes.ts +23 -3
  364. package/src/runtime/routes/host-cu-routes.ts +33 -6
  365. package/src/runtime/routes/host-file-routes.ts +32 -6
  366. package/src/runtime/routes/host-transfer-routes.ts +79 -16
  367. package/src/runtime/routes/identity-routes.ts +7 -138
  368. package/src/runtime/routes/inbound-message-handler.ts +77 -12
  369. package/src/runtime/routes/index.ts +6 -0
  370. package/src/runtime/routes/memory-item-routes.test.ts +37 -17
  371. package/src/runtime/routes/memory-item-routes.ts +5 -6
  372. package/src/runtime/routes/memory-v2-routes.ts +136 -17
  373. package/src/runtime/routes/oauth-connect-routes.ts +153 -0
  374. package/src/runtime/verification-outbound-actions.ts +4 -4
  375. package/src/schedule/run-script.ts +37 -5
  376. package/src/schedule/scheduler.ts +20 -1
  377. package/src/security/encrypted-store.ts +2 -0
  378. package/src/security/secure-keys.ts +55 -0
  379. package/src/skills/include-graph.ts +35 -13
  380. package/src/skills/remote-skill-policy.ts +4 -10
  381. package/src/subagent/index.ts +1 -7
  382. package/src/subagent/manager.ts +1 -15
  383. package/src/tasks/task-runner.ts +0 -1
  384. package/src/tasks/task-store.ts +0 -3
  385. package/src/tools/background-tool-registry.ts +17 -3
  386. package/src/tools/document/document-tool.ts +20 -0
  387. package/src/tools/executor.ts +18 -2
  388. package/src/tools/host-filesystem/edit.test.ts +151 -0
  389. package/src/tools/host-filesystem/edit.ts +43 -1
  390. package/src/tools/host-filesystem/read.test.ts +129 -0
  391. package/src/tools/host-filesystem/read.ts +43 -1
  392. package/src/tools/host-filesystem/transfer.test.ts +127 -2
  393. package/src/tools/host-filesystem/transfer.ts +56 -11
  394. package/src/tools/host-filesystem/write.test.ts +134 -0
  395. package/src/tools/host-filesystem/write.ts +43 -1
  396. package/src/tools/host-terminal/host-shell.ts +13 -6
  397. package/src/tools/mcp/mcp-tool-factory.ts +2 -1
  398. package/src/tools/memory/register.test.ts +14 -9
  399. package/src/tools/memory/register.ts +1 -2
  400. package/src/tools/permission-checker.ts +15 -0
  401. package/src/tools/provider-tool-name.ts +28 -0
  402. package/src/tools/registry.ts +30 -9
  403. package/src/tools/skills/load.ts +24 -20
  404. package/src/tools/terminal/shell.ts +9 -1
  405. package/src/tools/tool-approval-handler.ts +31 -6
  406. package/src/tools/tool-name-aliases.ts +19 -0
  407. package/src/tools/types.ts +43 -3
  408. package/src/tts/provider-catalog.ts +3 -5
  409. package/src/util/disk-usage.ts +138 -0
  410. package/src/util/platform.ts +21 -11
  411. package/src/util/process-liveness.ts +26 -0
  412. package/src/workspace/heartbeat-service.ts +19 -0
  413. package/src/workspace/migrations/065-bump-stale-heartbeat-interval.ts +60 -0
  414. package/src/workspace/migrations/066-seed-heartbeat-callsite-cost-default.ts +146 -0
  415. package/src/workspace/migrations/067-release-notes-safe-storage-limits.ts +14 -0
  416. package/src/workspace/migrations/068-release-notes-local-timezone.ts +65 -0
  417. package/src/workspace/migrations/069-seed-onboarding-threads.ts +28 -0
  418. package/src/workspace/migrations/070-memory-v2-summary-schema-rebuild.ts +31 -0
  419. package/src/workspace/migrations/071-remove-safe-storage-release-note.ts +111 -0
  420. package/src/workspace/migrations/registry.ts +14 -0
  421. package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +0 -167
  422. package/src/memory/v2/__tests__/skill-qdrant.test.ts +0 -657
  423. package/src/memory/v2/skill-qdrant.ts +0 -404
  424. package/src/signals/bash.ts +0 -198
@@ -48,20 +48,43 @@ export interface ConceptPagePayload {
48
48
  export interface ConceptPageQueryResult {
49
49
  slug: string;
50
50
  /**
51
- * Dense cosine similarity, when the slug appeared in the dense top-`limit`.
52
- * `undefined` if the slug only appeared in the sparse channel.
51
+ * Dense cosine similarity against the page body, when the slug appeared in
52
+ * the body dense top-`limit`. `undefined` if the slug only appeared in the
53
+ * sparse channel — or in a summary-side channel.
53
54
  */
54
55
  denseScore?: number;
55
56
  /**
56
- * Sparse score, when the slug appeared in the sparse top-`limit`.
57
- * `undefined` if the slug only appeared in the dense channel. Lives on a
58
- * different scale than `denseScore` — callers must normalize before fusing.
57
+ * Sparse score against the page body, when the slug appeared in the body
58
+ * sparse top-`limit`. `undefined` if the slug only appeared in the dense
59
+ * channel. Lives on a different scale than `denseScore` — callers must
60
+ * normalize before fusing.
59
61
  */
60
62
  sparseScore?: number;
63
+ /**
64
+ * Dense cosine similarity against the page's frontmatter `summary`, when
65
+ * the page has a summary embedded and the slug appeared in the summary
66
+ * dense top-`limit`. `undefined` for pages without a summary embedding —
67
+ * those fall back to body-only scoring.
68
+ */
69
+ summaryDenseScore?: number;
70
+ /**
71
+ * Sparse score against the page's frontmatter `summary`, paired with
72
+ * `summaryDenseScore`. `undefined` for pages without a summary embedding.
73
+ */
74
+ summarySparseScore?: number;
61
75
  }
62
76
 
63
77
  let _client: QdrantRestClient | null = null;
64
78
  let _collectionReady = false;
79
+ let _collectionReadyPromise: Promise<{ migrated: boolean }> | null = null;
80
+
81
+ /**
82
+ * Named vectors the v2 concept-page collection must expose. Existing
83
+ * collections that lack any of these get destructively recreated by
84
+ * `ensureConceptPageCollectionOnce` — see the `migrated` return flag.
85
+ */
86
+ const REQUIRED_DENSE_VECTORS = ["dense", "summary_dense"] as const;
87
+ const REQUIRED_SPARSE_VECTORS = ["sparse", "summary_sparse"] as const;
65
88
 
66
89
  /** Lazily create a Qdrant REST client bound to the resolved URL. */
67
90
  function getClient(): QdrantRestClient {
@@ -75,27 +98,67 @@ function getClient(): QdrantRestClient {
75
98
  }
76
99
 
77
100
  /**
78
- * Create the v2 concept-page collection if it does not already exist.
79
- * Idempotent: a no-op when the collection is already present.
80
- *
81
- * Vector layout mirrors `VellumQdrantClient.ensureCollection` named dense
82
- * (cosine, configurable size + on-disk) and sparse vectors. The vector size
83
- * and on-disk flag inherit from `config.memory.qdrant` so v2 stays aligned
84
- * with the user's existing embedding backend without separate knobs.
101
+ * Create the v2 concept-page collection if it does not already exist, or
102
+ * destructively recreate it when the existing schema is missing any of the
103
+ * required named vectors (see `REQUIRED_DENSE_VECTORS` /
104
+ * `REQUIRED_SPARSE_VECTORS`). The latter case is signalled to callers via
105
+ * `{ migrated: true }` so they can enqueue a backfill — pre-#29823
106
+ * collections lack `summary_dense` / `summary_sparse` and every query
107
+ * referencing those named vectors fails with HTTP 400 until the collection
108
+ * is rebuilt. Mirrors `VellumQdrantClient.ensureCollection` for v1.
85
109
  */
86
- export async function ensureConceptPageCollection(): Promise<void> {
87
- if (_collectionReady) return;
110
+ export async function ensureConceptPageCollection(): Promise<{
111
+ migrated: boolean;
112
+ }> {
113
+ if (_collectionReady) return { migrated: false };
114
+ if (_collectionReadyPromise) return _collectionReadyPromise;
115
+
116
+ _collectionReadyPromise = ensureConceptPageCollectionOnce().finally(() => {
117
+ _collectionReadyPromise = null;
118
+ });
119
+ return _collectionReadyPromise;
120
+ }
88
121
 
122
+ async function ensureConceptPageCollectionOnce(): Promise<{
123
+ migrated: boolean;
124
+ }> {
89
125
  const client = getClient();
90
126
  const config = getConfig();
91
127
  const vectorSize = config.memory.qdrant.vectorSize;
92
128
  const onDisk = config.memory.qdrant.onDisk;
93
129
 
130
+ let migrated = false;
131
+
94
132
  try {
95
133
  const exists = await client.collectionExists(MEMORY_V2_COLLECTION);
96
134
  if (exists.exists) {
97
- _collectionReady = true;
98
- return;
135
+ // Assume compatible on probe failure rather than risk a destructive
136
+ // recreate — mirrors v1's posture in `VellumQdrantClient.ensureCollection`.
137
+ let info: Awaited<ReturnType<typeof client.getCollection>>;
138
+ try {
139
+ info = await client.getCollection(MEMORY_V2_COLLECTION);
140
+ } catch (err) {
141
+ log.warn(
142
+ { err, collection: MEMORY_V2_COLLECTION },
143
+ "Failed to probe v2 collection schema; assuming compatible",
144
+ );
145
+ _collectionReady = true;
146
+ return { migrated: false };
147
+ }
148
+
149
+ const missing = missingNamedVectors(info);
150
+ if (missing.length === 0) {
151
+ _collectionReady = true;
152
+ return { migrated: false };
153
+ }
154
+
155
+ log.warn(
156
+ { collection: MEMORY_V2_COLLECTION, missingNamedVectors: missing },
157
+ "Memory v2 concept-page collection schema drift detected — deleting and recreating; embeddings will be regenerated by background reembed",
158
+ );
159
+ await client.deleteCollection(MEMORY_V2_COLLECTION);
160
+ migrated = true;
161
+ // Fall through to creation below.
99
162
  }
100
163
  } catch (err) {
101
164
  // Treat "not found"-shaped errors as "needs creation" and fall through.
@@ -115,15 +178,28 @@ export async function ensureConceptPageCollection(): Promise<void> {
115
178
  distance: "Cosine",
116
179
  on_disk: onDisk,
117
180
  },
181
+ // Optional second dense vector covering the page's frontmatter
182
+ // `summary`. Pages without a summary store nothing under this name —
183
+ // Qdrant supports per-point named-vector subsets — so the named-vector
184
+ // index stays cheap until summaries are populated.
185
+ summary_dense: {
186
+ size: vectorSize,
187
+ distance: "Cosine",
188
+ on_disk: onDisk,
189
+ },
118
190
  },
119
191
  sparse_vectors: {
120
192
  sparse: {}, // Qdrant auto-infers sparse vector params
193
+ summary_sparse: {}, // BM25 sparse vector for the summary
121
194
  },
122
195
  hnsw_config: {
123
196
  on_disk: onDisk,
124
197
  m: 16,
125
198
  ef_construct: 100,
126
199
  },
200
+ optimizers_config: {
201
+ default_segment_number: 2,
202
+ },
127
203
  on_disk_payload: onDisk,
128
204
  });
129
205
  } catch (err) {
@@ -134,7 +210,7 @@ export async function ensureConceptPageCollection(): Promise<void> {
134
210
  (err as { status: number }).status === 409
135
211
  ) {
136
212
  _collectionReady = true;
137
- return;
213
+ return { migrated };
138
214
  }
139
215
  throw err;
140
216
  }
@@ -147,32 +223,86 @@ export async function ensureConceptPageCollection(): Promise<void> {
147
223
  });
148
224
 
149
225
  _collectionReady = true;
226
+ return { migrated };
227
+ }
228
+
229
+ /**
230
+ * Return the names of required named vectors absent from the collection's
231
+ * current schema. An empty array means the collection is fully migrated.
232
+ *
233
+ * If the response shape is unparseable (e.g. Qdrant returns an unexpected
234
+ * structure) we treat it as "everything is missing" so the caller's drift
235
+ * branch fires — combined with the `getCollection` try/catch in the caller,
236
+ * a thrown probe falls back to "assume compatible" while a parsed-but-empty
237
+ * response triggers the safer recreate.
238
+ */
239
+ function missingNamedVectors(
240
+ info: Awaited<ReturnType<QdrantRestClient["getCollection"]>>,
241
+ ): string[] {
242
+ const params = info.config?.params;
243
+ const dense = params?.vectors;
244
+ const sparse = (params as { sparse_vectors?: unknown } | undefined)
245
+ ?.sparse_vectors;
246
+ const denseNames =
247
+ dense && typeof dense === "object" && !("size" in dense)
248
+ ? new Set(Object.keys(dense))
249
+ : new Set<string>();
250
+ const sparseNames =
251
+ sparse && typeof sparse === "object"
252
+ ? new Set(Object.keys(sparse as Record<string, unknown>))
253
+ : new Set<string>();
254
+
255
+ const missing: string[] = [];
256
+ for (const name of REQUIRED_DENSE_VECTORS) {
257
+ if (!denseNames.has(name)) missing.push(name);
258
+ }
259
+ for (const name of REQUIRED_SPARSE_VECTORS) {
260
+ if (!sparseNames.has(name)) missing.push(name);
261
+ }
262
+ return missing;
150
263
  }
151
264
 
152
265
  /**
153
266
  * Upsert a concept page's dense + sparse embedding. The point ID is derived
154
267
  * deterministically from the slug so subsequent calls for the same slug
155
268
  * replace the prior point in place rather than accumulating duplicates.
269
+ *
270
+ * `summary` is optional — supplied when the page's frontmatter carries a
271
+ * `summary`, omitted otherwise. Pages without a summary store only the body
272
+ * vectors and fall back to body-only scoring at query time. The grouped
273
+ * shape enforces at the type level that summary dense and sparse are
274
+ * always written together.
156
275
  */
157
276
  export async function upsertConceptPageEmbedding(params: {
158
277
  slug: string;
159
278
  dense: number[];
160
279
  sparse: SparseEmbedding;
280
+ summary?: { dense: number[]; sparse: SparseEmbedding };
161
281
  updatedAt: number;
162
282
  }): Promise<void> {
163
283
  await ensureConceptPageCollection();
164
284
 
165
- const { slug, dense, sparse, updatedAt } = params;
285
+ const { slug, dense, sparse, summary, updatedAt } = params;
166
286
  const client = getClient();
167
287
  const pointId = pointIdForSlug(slug);
168
288
 
289
+ // Qdrant lets us upsert any subset of named vectors per point. The summary
290
+ // entries appear only when the caller passed a `summary` block — pairing
291
+ // them at the type level keeps query-time fusion symmetric with the body
292
+ // channels.
293
+ const vector: Record<string, number[] | SparseEmbedding> = { dense, sparse };
294
+ if (summary) {
295
+ vector.summary_dense = summary.dense;
296
+ vector.summary_sparse = summary.sparse;
297
+ }
298
+
169
299
  const upsertOnce = () =>
170
300
  client.upsert(MEMORY_V2_COLLECTION, {
171
301
  wait: true,
172
302
  points: [
173
303
  {
174
304
  id: pointId,
175
- vector: { dense, sparse },
305
+ vector,
176
306
  payload: { slug, updated_at: updatedAt },
177
307
  },
178
308
  ],
@@ -215,15 +345,134 @@ export async function deleteConceptPageEmbedding(slug: string): Promise<void> {
215
345
  }
216
346
  }
217
347
 
348
+ /**
349
+ * Remove every point whose slug starts with the given prefix and whose
350
+ * remaining suffix is not in `activeSuffixes`. Used by the skill-seed flow to
351
+ * drop stale `skills/<id>` slugs after a skill is uninstalled or disabled,
352
+ * since skills now share the concept-page collection rather than living in a
353
+ * dedicated one.
354
+ *
355
+ * Idempotent: when the live `<prefix>*` slugs already match `activeSuffixes`,
356
+ * the function performs a single scroll and no deletes.
357
+ */
358
+ export async function pruneSlugsWithPrefixExcept(
359
+ prefix: string,
360
+ activeSuffixes: readonly string[],
361
+ ): Promise<void> {
362
+ await ensureConceptPageCollection();
363
+
364
+ const client = getClient();
365
+ const activeSet = new Set(activeSuffixes);
366
+
367
+ const doPrune = async (): Promise<void> => {
368
+ const stalePointIds: Array<string | number> = [];
369
+ let offset: string | number | undefined = undefined;
370
+ const maxIterations = 10_000;
371
+ const batchSize = 256;
372
+ for (let i = 0; i < maxIterations; i++) {
373
+ const result = await client.scroll(MEMORY_V2_COLLECTION, {
374
+ limit: batchSize,
375
+ with_payload: true,
376
+ with_vector: false,
377
+ ...(offset !== undefined ? { offset } : {}),
378
+ });
379
+ for (const point of result.points) {
380
+ const slug = (point.payload as { slug?: unknown } | null)?.slug;
381
+ if (typeof slug !== "string") continue;
382
+ if (!slug.startsWith(prefix)) continue;
383
+ const suffix = slug.slice(prefix.length);
384
+ if (!activeSet.has(suffix)) {
385
+ stalePointIds.push(point.id);
386
+ }
387
+ }
388
+ const next = result.next_page_offset;
389
+ if (next == null) break;
390
+ offset = typeof next === "string" ? next : (next as number);
391
+ }
392
+
393
+ if (stalePointIds.length === 0) return;
394
+
395
+ await client.delete(MEMORY_V2_COLLECTION, {
396
+ wait: true,
397
+ points: stalePointIds,
398
+ });
399
+ };
400
+
401
+ try {
402
+ await doPrune();
403
+ } catch (err) {
404
+ if (isCollectionMissing(err)) {
405
+ _collectionReady = false;
406
+ await ensureConceptPageCollection();
407
+ await doPrune();
408
+ return;
409
+ }
410
+ throw err;
411
+ }
412
+ }
413
+
414
+ /**
415
+ * Approximate count of points in the v2 concept-page collection. Used by the
416
+ * daemon-startup rebuild hook to detect "collection exists but empty" — the
417
+ * crash-mid-rebuild recovery case where a prior boot dropped + recreated the
418
+ * collection but died before reembed completed. Returns `0` if the collection
419
+ * does not exist or the count call fails (treated as "needs reembed" by the
420
+ * caller).
421
+ */
422
+ export async function countConceptPagePoints(): Promise<number> {
423
+ await ensureConceptPageCollection();
424
+ try {
425
+ const result = await getClient().count(MEMORY_V2_COLLECTION, {
426
+ exact: false,
427
+ });
428
+ return result.count;
429
+ } catch (err) {
430
+ log.warn(
431
+ { err, collection: MEMORY_V2_COLLECTION },
432
+ "Failed to count v2 concept-page collection — treating as empty",
433
+ );
434
+ return 0;
435
+ }
436
+ }
437
+
438
+ /**
439
+ * Best-effort delete of the legacy `memory_v2_skills` Qdrant collection. Skill
440
+ * embeddings now live alongside concept pages in `memory_v2_concept_pages`
441
+ * under the `skills/<id>` slug prefix, so the dedicated collection is dead
442
+ * weight on installs upgraded from the split-collection era. Fire-and-forget:
443
+ * on a fresh install (collection never existed) or a transient Qdrant
444
+ * unavailable, we log and move on.
445
+ */
446
+ export async function dropLegacySkillsCollection(): Promise<void> {
447
+ try {
448
+ const client = getClient();
449
+ const exists = await client.collectionExists("memory_v2_skills");
450
+ if (!exists.exists) return;
451
+ await client.deleteCollection("memory_v2_skills");
452
+ log.info("Deleted legacy memory_v2_skills Qdrant collection");
453
+ } catch (err) {
454
+ log.warn(
455
+ { err },
456
+ "Failed to drop legacy memory_v2_skills collection — non-fatal",
457
+ );
458
+ }
459
+ }
460
+
218
461
  /**
219
462
  * Run separate dense and sparse queries against the concept-page collection
220
463
  * and return per-channel scores per slug. Callers fuse these — typically via
221
464
  * a normalized weighted-sum — because RRF would discard the score magnitudes
222
465
  * the activation formula needs.
223
466
  *
467
+ * Four channels are queried concurrently: body dense, body sparse, summary
468
+ * dense, summary sparse. The summary channels only return hits for pages whose
469
+ * frontmatter carries a `summary` (and therefore stored `summary_dense` /
470
+ * `summary_sparse` named vectors at upsert time). Pages without a summary
471
+ * surface body-only scores; callers fall back to body-only fusion for those.
472
+ *
224
473
  * Each channel returns up to `limit` hits. A slug is included in the result
225
- * if it appears in either channel; the missing channel's score is left
226
- * `undefined` so callers can detect single-channel matches.
474
+ * if it appears in any channel; missing channel scores stay `undefined` so
475
+ * callers can distinguish "no match in this channel" from "match with score 0".
227
476
  *
228
477
  * `restrictToSlugs`, when provided, filters the search server-side to only
229
478
  * those slugs (Qdrant `slug IN [...]` filter). Used by `simBatch` when the
@@ -257,42 +506,51 @@ export async function hybridQueryConceptPages(
257
506
  // Qdrant 1.13.x sparse-index crash that we've reproduced in the wild.
258
507
  const skipSparse = options?.skipSparse ?? false;
259
508
 
260
- const denseQuery = () =>
509
+ const queryDense = (using: string) =>
261
510
  client.query(MEMORY_V2_COLLECTION, {
262
511
  query: dense,
263
- using: "dense",
512
+ using,
264
513
  limit,
265
514
  with_payload: true,
266
515
  filter,
267
516
  });
268
- const sparseQuery = () =>
517
+ const querySparse = (using: string) =>
269
518
  client.query(MEMORY_V2_COLLECTION, {
270
519
  query: sparse,
271
- using: "sparse",
520
+ using,
272
521
  limit,
273
522
  with_payload: true,
274
523
  filter,
275
524
  });
276
525
 
277
- // Run both queries concurrently — they hit independent named vectors.
278
- // When sparse is gated off we still resolve a Promise so the destructuring
279
- // below stays uniform; the empty `points: []` matches the shape of a
280
- // no-hit Qdrant response.
526
+ // Run all four channels concurrently — they hit independent named vectors.
527
+ // When sparse is gated off the sparse channels still resolve a Promise so
528
+ // the destructuring below stays uniform; the empty `points: []` matches
529
+ // the shape of a no-hit Qdrant response.
281
530
  const emptyResult = {
282
531
  points: [] as Array<{ payload?: unknown; score?: number }>,
283
532
  };
284
533
  const runQueries = async () =>
285
- Promise.all([denseQuery(), skipSparse ? emptyResult : sparseQuery()]);
534
+ Promise.all([
535
+ queryDense("dense"),
536
+ skipSparse ? emptyResult : querySparse("sparse"),
537
+ queryDense("summary_dense"),
538
+ skipSparse ? emptyResult : querySparse("summary_sparse"),
539
+ ]);
286
540
 
287
541
  let denseResults;
288
542
  let sparseResults;
543
+ let summaryDenseResults;
544
+ let summarySparseResults;
289
545
  try {
290
- [denseResults, sparseResults] = await runQueries();
546
+ [denseResults, sparseResults, summaryDenseResults, summarySparseResults] =
547
+ await runQueries();
291
548
  } catch (err) {
292
549
  if (isCollectionMissing(err)) {
293
550
  _collectionReady = false;
294
551
  await ensureConceptPageCollection();
295
- [denseResults, sparseResults] = await runQueries();
552
+ [denseResults, sparseResults, summaryDenseResults, summarySparseResults] =
553
+ await runQueries();
296
554
  } else {
297
555
  throw err;
298
556
  }
@@ -301,21 +559,22 @@ export async function hybridQueryConceptPages(
301
559
  // Merge by slug. Missing-side scores stay undefined so the fuser can tell
302
560
  // "no match in this channel" apart from "match with score 0".
303
561
  const merged = new Map<string, ConceptPageQueryResult>();
304
- for (const point of denseResults.points ?? []) {
305
- const slug = (point.payload as { slug?: unknown } | null)?.slug;
306
- if (typeof slug !== "string") continue;
307
- merged.set(slug, { slug, denseScore: point.score ?? 0 });
308
- }
309
- for (const point of sparseResults.points ?? []) {
310
- const slug = (point.payload as { slug?: unknown } | null)?.slug;
311
- if (typeof slug !== "string") continue;
312
- const existing = merged.get(slug);
313
- if (existing) {
314
- existing.sparseScore = point.score ?? 0;
315
- } else {
316
- merged.set(slug, { slug, sparseScore: point.score ?? 0 });
562
+ const recordHit = (
563
+ points: Array<{ payload?: unknown; score?: number }> | undefined,
564
+ set: (entry: ConceptPageQueryResult, score: number) => void,
565
+ ): void => {
566
+ for (const point of points ?? []) {
567
+ const slug = (point.payload as { slug?: unknown } | null)?.slug;
568
+ if (typeof slug !== "string") continue;
569
+ const existing = merged.get(slug) ?? { slug };
570
+ set(existing, point.score ?? 0);
571
+ merged.set(slug, existing);
317
572
  }
318
- }
573
+ };
574
+ recordHit(denseResults.points, (e, s) => (e.denseScore = s));
575
+ recordHit(sparseResults.points, (e, s) => (e.sparseScore = s));
576
+ recordHit(summaryDenseResults.points, (e, s) => (e.summaryDenseScore = s));
577
+ recordHit(summarySparseResults.points, (e, s) => (e.summarySparseScore = s));
319
578
 
320
579
  return Array.from(merged.values());
321
580
  }
@@ -437,4 +696,5 @@ function pointIdForSlug(slug: string): string {
437
696
  export function _resetMemoryV2QdrantForTests(): void {
438
697
  _client = null;
439
698
  _collectionReady = false;
699
+ _collectionReadyPromise = null;
440
700
  }
@@ -0,0 +1,177 @@
1
+ /** Memory v2 cross-encoder rerank — `(query, page-preview)` pairs scored by a local model. */
2
+
3
+ import { createHash } from "node:crypto";
4
+
5
+ import type { AssistantConfig } from "../../config/types.js";
6
+ import { getLogger } from "../../util/logger.js";
7
+ import { getWorkspaceDir } from "../../util/platform.js";
8
+ import { getOrCreateRerankBackend } from "../rerank-local.js";
9
+ import { readPage } from "./page-store.js";
10
+
11
+ const log = getLogger("memory-v2-reranker");
12
+
13
+ // ~512-token model context for bge-reranker-base; cap input to bound payload.
14
+ const PASSAGE_CHAR_CAP = 240;
15
+
16
+ interface CacheEntry {
17
+ scores: Map<string, number>;
18
+ ts: number;
19
+ }
20
+
21
+ const CACHE_TTL_MS = 2 * 60 * 1000;
22
+ const CACHE_MAX_ENTRIES = 64;
23
+ const cache = new Map<string, CacheEntry>();
24
+
25
+ function cacheKey(query: string, slugs: readonly string[]): string {
26
+ const sorted = [...slugs].sort().join("\0");
27
+ return createHash("sha256").update(`${query}\0${sorted}`).digest("hex");
28
+ }
29
+
30
+ function evictExpired(now: number): void {
31
+ for (const [k, v] of cache) {
32
+ if (now - v.ts > CACHE_TTL_MS) cache.delete(k);
33
+ }
34
+ if (cache.size > CACHE_MAX_ENTRIES) {
35
+ const toDrop = cache.size - CACHE_MAX_ENTRIES;
36
+ let i = 0;
37
+ for (const k of cache.keys()) {
38
+ if (i++ >= toDrop) break;
39
+ cache.delete(k);
40
+ }
41
+ }
42
+ }
43
+
44
+ function buildPassage(slug: string, body: string): string {
45
+ const trimmed = body.replace(/^\s+/, "");
46
+ const blank = trimmed.search(/\n\s*\n/);
47
+ const para = blank === -1 ? trimmed : trimmed.slice(0, blank);
48
+ const stripped = para.replace(/^#+\s.*\n/, "").trim();
49
+ const compact = stripped.replace(/\s+/g, " ").slice(0, PASSAGE_CHAR_CAP);
50
+ return `${slug}\n${compact}`;
51
+ }
52
+
53
+ /**
54
+ * Run the cross-encoder over each candidate's first-paragraph preview for
55
+ * one or more queries against the same candidate set. Returns one
56
+ * `Map<slug, score>` per query, in the same order as the `queries` array.
57
+ *
58
+ * Multi-query batching: the user-channel and assistant-channel queries share
59
+ * a candidate set per turn, so scoring them in a single tokenizer +
60
+ * forward-pass call avoids the ONNX-invocation overhead of two serialised
61
+ * worker round-trips. Cache hits short-circuit per-query independently —
62
+ * a whitespace-only query yields an empty Map without hitting the backend.
63
+ *
64
+ * Failures (worker down, page read errors) yield empty Maps so callers can
65
+ * fall back to pure fused scores. Per-batch normalisation and boost math
66
+ * live in `computeOwnActivation`.
67
+ */
68
+ export async function rerankCandidates(
69
+ queries: readonly string[],
70
+ candidates: readonly string[],
71
+ config: AssistantConfig,
72
+ ): Promise<Array<Map<string, number>>> {
73
+ if (queries.length === 0) return [];
74
+ if (candidates.length === 0) return queries.map(() => new Map());
75
+
76
+ const now = Date.now();
77
+ evictExpired(now);
78
+
79
+ const results: Array<Map<string, number> | null> = queries.map(() => null);
80
+ const uncachedIndices: number[] = [];
81
+ for (let i = 0; i < queries.length; i++) {
82
+ const q = queries[i];
83
+ if (q.trim().length === 0) {
84
+ results[i] = new Map();
85
+ continue;
86
+ }
87
+ const key = cacheKey(q, candidates);
88
+ const cached = cache.get(key);
89
+ if (cached) {
90
+ // Refresh insertion order so frequently-hit entries survive eviction.
91
+ cache.delete(key);
92
+ cache.set(key, { ...cached, ts: now });
93
+ results[i] = new Map(cached.scores);
94
+ } else {
95
+ uncachedIndices.push(i);
96
+ }
97
+ }
98
+
99
+ const finalize = (): Array<Map<string, number>> =>
100
+ results.map((r) => r ?? new Map());
101
+
102
+ if (uncachedIndices.length === 0) return finalize();
103
+
104
+ const workspaceDir = getWorkspaceDir();
105
+ const pages = await Promise.all(
106
+ candidates.map((slug) =>
107
+ readPage(workspaceDir, slug).catch((err) => {
108
+ log.debug({ err, slug }, "Reranker skipping page that failed to load");
109
+ return null;
110
+ }),
111
+ ),
112
+ );
113
+ const passages: string[] = [];
114
+ const slugsForPassages: string[] = [];
115
+ for (let i = 0; i < candidates.length; i++) {
116
+ const page = pages[i];
117
+ if (!page) continue;
118
+ passages.push(buildPassage(candidates[i], page.body));
119
+ slugsForPassages.push(candidates[i]);
120
+ }
121
+
122
+ if (passages.length === 0) {
123
+ for (const i of uncachedIndices) results[i] = new Map();
124
+ return finalize();
125
+ }
126
+
127
+ // One tokenizer + ONNX forward pass over every uncached query × passage
128
+ // pair. Pairs are laid out query-major: queries[uncached[0]] × passages,
129
+ // then queries[uncached[1]] × passages, etc.
130
+ const batchQueries: string[] = [];
131
+ const batchPassages: string[] = [];
132
+ for (const qi of uncachedIndices) {
133
+ const q = queries[qi];
134
+ for (const p of passages) {
135
+ batchQueries.push(q);
136
+ batchPassages.push(p);
137
+ }
138
+ }
139
+
140
+ const { model, dtype } = config.memory.v2.rerank;
141
+ let scores: number[];
142
+ try {
143
+ const backend = getOrCreateRerankBackend(model, dtype);
144
+ scores = await backend.score(batchQueries, batchPassages);
145
+ } catch (err) {
146
+ log.warn(
147
+ { err, model, n: batchPassages.length },
148
+ "Rerank backend failed; falling back to pure fused scores",
149
+ );
150
+ for (const i of uncachedIndices) results[i] = new Map();
151
+ return finalize();
152
+ }
153
+
154
+ for (let j = 0; j < uncachedIndices.length; j++) {
155
+ const qi = uncachedIndices[j];
156
+ const offset = j * passages.length;
157
+ const result = new Map<string, number>();
158
+ for (let i = 0; i < slugsForPassages.length; i++) {
159
+ const s = scores[offset + i];
160
+ if (typeof s !== "number" || Number.isNaN(s)) continue;
161
+ // sigmoid output should already be in [0, 1]; clamp defensively.
162
+ result.set(slugsForPassages[i], Math.max(0, Math.min(1, s)));
163
+ }
164
+ results[qi] = result;
165
+ cache.set(cacheKey(queries[qi], candidates), {
166
+ scores: new Map(result),
167
+ ts: now,
168
+ });
169
+ }
170
+
171
+ return finalize();
172
+ }
173
+
174
+ /** @internal Test-only: clear the LRU cache. */
175
+ export function _resetRerankCacheForTests(): void {
176
+ cache.clear();
177
+ }