@vellumai/assistant 0.7.1 → 0.7.2

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 (535) hide show
  1. package/ARCHITECTURE.md +32 -49
  2. package/Dockerfile +1 -0
  3. package/README.md +1 -2
  4. package/__tests__/permissions/gateway-threshold-reader.test.ts +9 -3
  5. package/bun.lock +26 -26
  6. package/docs/architecture/security.md +20 -0
  7. package/docs/plugins.md +7 -9
  8. package/knip.json +1 -0
  9. package/node_modules/@vellumai/gateway-client/src/index.ts +1 -0
  10. package/node_modules/@vellumai/gateway-client/src/ipc-client.ts +39 -1
  11. package/node_modules/@vellumai/gateway-client/src/types.ts +11 -0
  12. package/node_modules/@vellumai/service-contracts/package.json +2 -0
  13. package/node_modules/@vellumai/service-contracts/src/__tests__/contracts.test.ts +4 -0
  14. package/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +107 -0
  15. package/node_modules/@vellumai/service-contracts/src/index.ts +5 -1
  16. package/node_modules/@vellumai/service-contracts/src/ingress.ts +24 -0
  17. package/node_modules/@vellumai/service-contracts/src/twilio-ingress.ts +84 -0
  18. package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +9 -0
  19. package/node_modules/@vellumai/twilio-client/bun.lock +24 -0
  20. package/node_modules/@vellumai/twilio-client/package.json +18 -0
  21. package/node_modules/@vellumai/twilio-client/src/__tests__/twilio-client.test.ts +128 -0
  22. package/node_modules/@vellumai/twilio-client/src/index.ts +179 -0
  23. package/node_modules/@vellumai/twilio-client/tsconfig.json +20 -0
  24. package/openapi.yaml +565 -12
  25. package/package.json +6 -3
  26. package/src/__tests__/app-builder-tool-scripts.test.ts +3 -3
  27. package/src/__tests__/app-bundler.test.ts +170 -1
  28. package/src/__tests__/app-control-flow.test.ts +374 -0
  29. package/src/__tests__/app-control-no-global-cgevent.test.ts +98 -0
  30. package/src/__tests__/app-control-tool-schemas.test.ts +621 -0
  31. package/src/__tests__/app-executors.test.ts +30 -43
  32. package/src/__tests__/approval-routes-http.test.ts +23 -6
  33. package/src/__tests__/assistant-event-hub-machine-name.test.ts +146 -0
  34. package/src/__tests__/assistant-event-hub-targeted.test.ts +257 -0
  35. package/src/__tests__/assistant-event-hub.test.ts +109 -2
  36. package/src/__tests__/assistant-event.test.ts +10 -0
  37. package/src/__tests__/assistant-events-sse-hardening.test.ts +7 -2
  38. package/src/__tests__/assistant-feature-flags-integration.test.ts +11 -7
  39. package/src/__tests__/background-shell-host-bash.test.ts +14 -15
  40. package/src/__tests__/bootstrap-turn-cleanup.test.ts +44 -0
  41. package/src/__tests__/btw-routes.test.ts +13 -4
  42. package/src/__tests__/call-controller.test.ts +49 -1
  43. package/src/__tests__/call-domain.test.ts +0 -2
  44. package/src/__tests__/call-routes-http.test.ts +0 -2
  45. package/src/__tests__/channel-readiness-service.test.ts +59 -1
  46. package/src/__tests__/checker.test.ts +3 -4
  47. package/src/__tests__/config-loader-backfill.test.ts +90 -155
  48. package/src/__tests__/config-loader-platform-defaults.test.ts +196 -0
  49. package/src/__tests__/config-schema-cmd.test.ts +0 -1
  50. package/src/__tests__/config-set-platform-guard.test.ts +48 -4
  51. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +2 -2
  52. package/src/__tests__/config-watcher.test.ts +2 -2
  53. package/src/__tests__/conversation-app-control-instantiation.test.ts +392 -0
  54. package/src/__tests__/conversation-app-control-lifecycle.test.ts +237 -0
  55. package/src/__tests__/conversation-init.benchmark.test.ts +0 -2
  56. package/src/__tests__/conversation-lifecycle.test.ts +36 -0
  57. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +283 -0
  58. package/src/__tests__/conversation-routes-disk-view.test.ts +6 -0
  59. package/src/__tests__/conversation-routes-guardian-reply.test.ts +120 -72
  60. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
  61. package/src/__tests__/conversation-slash-commands.test.ts +0 -4
  62. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +202 -0
  63. package/src/__tests__/conversation-surfaces-app-control.test.ts +317 -0
  64. package/src/__tests__/credential-execution-feature-gates.test.ts +5 -12
  65. package/src/__tests__/credential-execution-managed-contract.test.ts +3 -131
  66. package/src/__tests__/credentials-cli.test.ts +5 -12
  67. package/src/__tests__/cu-unified-flow.test.ts +185 -23
  68. package/src/__tests__/daemon-credential-client.test.ts +101 -19
  69. package/src/__tests__/db-schedule-syntax-migration.test.ts +2 -0
  70. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
  71. package/src/__tests__/gateway-only-enforcement.test.ts +0 -1
  72. package/src/__tests__/guardian-verification-voice-binding.test.ts +0 -2
  73. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +0 -2
  74. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +0 -1
  75. package/src/__tests__/heartbeat-service.test.ts +718 -1
  76. package/src/__tests__/helpers/call-route-handler.ts +7 -1
  77. package/src/__tests__/host-app-control-proxy.test.ts +602 -0
  78. package/src/__tests__/host-app-control-routes.test.ts +263 -0
  79. package/src/__tests__/host-bash-proxy.test.ts +246 -47
  80. package/src/__tests__/host-bash-routes.test.ts +294 -0
  81. package/src/__tests__/host-browser-proxy.test.ts +24 -22
  82. package/src/__tests__/host-browser-routes.test.ts +39 -13
  83. package/src/__tests__/host-cu-proxy.test.ts +41 -52
  84. package/src/__tests__/host-cu-routes-targeted.test.ts +300 -0
  85. package/src/__tests__/host-file-edit-tool.test.ts +47 -1
  86. package/src/__tests__/host-file-proxy-targeted.test.ts +339 -0
  87. package/src/__tests__/host-file-proxy.test.ts +37 -43
  88. package/src/__tests__/host-file-read-tool.test.ts +17 -0
  89. package/src/__tests__/host-file-routes-targeted.test.ts +262 -0
  90. package/src/__tests__/host-file-write-tool.test.ts +42 -1
  91. package/src/__tests__/host-proxy-base.test.ts +312 -0
  92. package/src/__tests__/host-shell-tool.test.ts +22 -4
  93. package/src/__tests__/host-transfer-proxy-targeted.test.ts +583 -0
  94. package/src/__tests__/host-transfer-proxy.test.ts +121 -22
  95. package/src/__tests__/host-transfer-routes-targeted.test.ts +447 -0
  96. package/src/__tests__/http-user-message-parity.test.ts +1 -0
  97. package/src/__tests__/identity-intro-cache.test.ts +29 -0
  98. package/src/__tests__/identity-routes.test.ts +103 -1
  99. package/src/__tests__/init-feature-flag-overrides.test.ts +26 -3
  100. package/src/__tests__/inline-command-runner.test.ts +0 -1
  101. package/src/__tests__/inline-skill-load-permissions.test.ts +5 -11
  102. package/src/__tests__/integration-status.test.ts +85 -5
  103. package/src/__tests__/intent-routing.test.ts +0 -1
  104. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +95 -5
  105. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +17 -0
  106. package/src/__tests__/managed-skill-lifecycle.test.ts +0 -1
  107. package/src/__tests__/mcp-auth-routes.test.ts +197 -0
  108. package/src/__tests__/mcp-cli.test.ts +338 -2
  109. package/src/__tests__/memory-jobs-worker-lanes.test.ts +188 -0
  110. package/src/__tests__/migration-import-commit-http.test.ts +108 -2
  111. package/src/__tests__/mock-gateway-ipc.ts +1 -0
  112. package/src/__tests__/oauth-cli.test.ts +0 -2
  113. package/src/__tests__/oauth2-gateway-transport.test.ts +0 -1
  114. package/src/__tests__/persistence-secret-redaction.test.ts +299 -0
  115. package/src/__tests__/platform-bash-auto-approve.test.ts +5 -9
  116. package/src/__tests__/prechat-onboarding-contract.test.ts +3 -1
  117. package/src/__tests__/process-message-background-slack.test.ts +2 -0
  118. package/src/__tests__/provider-commit-message-generator.test.ts +0 -1
  119. package/src/__tests__/public-ingress-urls.test.ts +97 -0
  120. package/src/__tests__/require-fresh-approval.test.ts +0 -1
  121. package/src/__tests__/retry-backoff.test.ts +87 -0
  122. package/src/__tests__/runtime-events-sse.test.ts +10 -6
  123. package/src/__tests__/sanitize-config-for-transfer.test.ts +24 -2
  124. package/src/__tests__/schedule-retry.test.ts +715 -0
  125. package/src/__tests__/script-proxy-mitm-handler.test.ts +1 -1
  126. package/src/__tests__/secret-ingress-http.test.ts +1 -0
  127. package/src/__tests__/send-endpoint-busy.test.ts +3 -0
  128. package/src/__tests__/shell-tool-proxy-mode.test.ts +0 -1
  129. package/src/__tests__/skill-feature-flags.test.ts +43 -41
  130. package/src/__tests__/skill-load-feature-flag.test.ts +13 -14
  131. package/src/__tests__/skill-load-inline-command.test.ts +0 -51
  132. package/src/__tests__/skill-load-inline-includes.test.ts +0 -43
  133. package/src/__tests__/skill-projection.benchmark.test.ts +0 -1
  134. package/src/__tests__/skill-script-runner-sandbox.test.ts +0 -1
  135. package/src/__tests__/slack-channel-config.test.ts +9 -14
  136. package/src/__tests__/system-prompt-ask-mode.test.ts +0 -1
  137. package/src/__tests__/system-prompt.test.ts +0 -1
  138. package/src/__tests__/telegram-config.test.ts +0 -1
  139. package/src/__tests__/test-preload.ts +8 -0
  140. package/src/__tests__/tool-approval-handler.test.ts +3 -4
  141. package/src/__tests__/tool-audit-listener.test.ts +48 -0
  142. package/src/__tests__/tool-execute-pipeline.test.ts +0 -1
  143. package/src/__tests__/tool-execution-abort-cleanup.test.ts +0 -1
  144. package/src/__tests__/tool-executor-lifecycle-events.test.ts +0 -1
  145. package/src/__tests__/tool-executor.test.ts +0 -1
  146. package/src/__tests__/twilio-config.test.ts +3 -16
  147. package/src/__tests__/twilio-routes.test.ts +3 -5
  148. package/src/__tests__/twilio-validation.test.ts +93 -0
  149. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +1 -4
  150. package/src/__tests__/verification-control-plane-policy.test.ts +2 -4
  151. package/src/__tests__/voice-ingress-preflight.test.ts +19 -0
  152. package/src/__tests__/workspace-migration-006-services-config.test.ts +3 -2
  153. package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +1 -5
  154. package/src/__tests__/workspace-migration-down-functions.test.ts +8 -8
  155. package/src/__tests__/workspace-migration-unify-llm-callsite-configs.test.ts +10 -6
  156. package/src/backup/__tests__/paths.test.ts +0 -22
  157. package/src/backup/__tests__/restore.test.ts +51 -151
  158. package/src/backup/paths.ts +2 -18
  159. package/src/backup/restore.ts +107 -231
  160. package/src/bundler/app-bundler.ts +51 -3
  161. package/src/calls/relay-server.ts +4 -44
  162. package/src/calls/twilio-config.ts +2 -17
  163. package/src/calls/twilio-rest.ts +33 -105
  164. package/src/calls/twilio-routes.ts +11 -12
  165. package/src/channels/types.ts +8 -7
  166. package/src/cli/commands/__tests__/backup.test.ts +6 -277
  167. package/src/cli/commands/__tests__/gateway.test.ts +288 -0
  168. package/src/cli/commands/__tests__/memory-v2.test.ts +4 -0
  169. package/src/cli/commands/__tests__/webhooks.test.ts +0 -1
  170. package/src/cli/commands/backup.ts +6 -331
  171. package/src/cli/commands/clients.ts +36 -37
  172. package/src/cli/commands/contacts.ts +73 -0
  173. package/src/cli/commands/conversations.ts +2 -5
  174. package/src/cli/commands/credentials.ts +15 -7
  175. package/src/cli/commands/domain.ts +66 -15
  176. package/src/cli/commands/gateway.ts +183 -0
  177. package/src/cli/commands/keys.ts +9 -6
  178. package/src/cli/commands/mcp.ts +116 -156
  179. package/src/cli/commands/memory-v2.ts +296 -1
  180. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -1
  181. package/src/cli/commands/platform/__tests__/connect.test.ts +0 -2
  182. package/src/cli/commands/platform/__tests__/disconnect.test.ts +0 -2
  183. package/src/cli/commands/platform/__tests__/status.test.ts +13 -15
  184. package/src/cli/commands/platform/disconnect.ts +5 -4
  185. package/src/cli/commands/platform/index.ts +0 -18
  186. package/src/cli/lib/daemon-credential-client.ts +110 -28
  187. package/src/cli/program.ts +2 -0
  188. package/src/config/assistant-feature-flags.ts +67 -10
  189. package/src/config/bundled-skills/acp/SKILL.md +6 -0
  190. package/src/config/bundled-skills/acp/TOOLS.json +1 -22
  191. package/src/config/bundled-skills/app-builder/SKILL.md +14 -109
  192. package/src/config/bundled-skills/app-builder/TOOLS.json +1 -28
  193. package/src/config/bundled-skills/app-builder/tools/app-create.ts +1 -10
  194. package/src/config/bundled-skills/app-control/SKILL.md +75 -0
  195. package/src/config/bundled-skills/app-control/TOOLS.json +299 -0
  196. package/src/config/bundled-skills/app-control/tools/app-control-click.ts +12 -0
  197. package/src/config/bundled-skills/app-control/tools/app-control-combo.ts +12 -0
  198. package/src/config/bundled-skills/app-control/tools/app-control-drag.ts +12 -0
  199. package/src/config/bundled-skills/app-control/tools/app-control-observe.ts +12 -0
  200. package/src/config/bundled-skills/app-control/tools/app-control-press.ts +12 -0
  201. package/src/config/bundled-skills/app-control/tools/app-control-sequence.ts +12 -0
  202. package/src/config/bundled-skills/app-control/tools/app-control-start.ts +12 -0
  203. package/src/config/bundled-skills/app-control/tools/app-control-stop.ts +12 -0
  204. package/src/config/bundled-skills/app-control/tools/app-control-type.ts +12 -0
  205. package/src/config/bundled-skills/computer-use/SKILL.md +6 -0
  206. package/src/config/bundled-skills/computer-use/TOOLS.json +67 -43
  207. package/src/config/bundled-skills/contacts/TOOLS.json +0 -16
  208. package/src/config/bundled-skills/document/TOOLS.json +0 -8
  209. package/src/config/bundled-skills/followups/TOOLS.json +0 -12
  210. package/src/config/bundled-skills/image-studio/SKILL.md +4 -0
  211. package/src/config/bundled-skills/image-studio/TOOLS.json +0 -4
  212. package/src/config/bundled-skills/media-processing/TOOLS.json +0 -24
  213. package/src/config/bundled-skills/messaging/TOOLS.json +0 -40
  214. package/src/config/bundled-skills/phone-calls/TOOLS.json +0 -12
  215. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +19 -4
  216. package/src/config/bundled-skills/playbooks/TOOLS.json +0 -16
  217. package/src/config/bundled-skills/schedule/TOOLS.json +14 -14
  218. package/src/config/bundled-skills/sequences/TOOLS.json +0 -36
  219. package/src/config/bundled-skills/settings/SKILL.md +4 -0
  220. package/src/config/bundled-skills/settings/TOOLS.json +0 -12
  221. package/src/config/bundled-skills/skill-management/SKILL.md +6 -0
  222. package/src/config/bundled-skills/skill-management/TOOLS.json +0 -8
  223. package/src/config/bundled-skills/subagent/SKILL.md +6 -2
  224. package/src/config/bundled-skills/subagent/TOOLS.json +0 -20
  225. package/src/config/bundled-skills/transcribe/SKILL.md +4 -0
  226. package/src/config/bundled-skills/transcribe/TOOLS.json +0 -4
  227. package/src/config/bundled-tool-registry.ts +21 -0
  228. package/src/config/env-registry.ts +0 -2
  229. package/src/config/env.ts +19 -12
  230. package/src/config/feature-flag-registry.json +21 -133
  231. package/src/config/loader.ts +73 -99
  232. package/src/config/sanitize-for-transfer.ts +2 -0
  233. package/src/config/schemas/__tests__/memory-lifecycle.test.ts +80 -0
  234. package/src/config/schemas/__tests__/memory-v2.test.ts +7 -4
  235. package/src/config/schemas/calls.ts +0 -9
  236. package/src/config/schemas/heartbeat.ts +63 -0
  237. package/src/config/schemas/ingress.ts +10 -6
  238. package/src/config/schemas/llm.ts +5 -10
  239. package/src/config/schemas/memory-lifecycle.ts +77 -24
  240. package/src/config/schemas/memory-v2.ts +48 -4
  241. package/src/config/schemas/platform.ts +6 -0
  242. package/src/config/schemas/services.ts +1 -15
  243. package/src/config/schemas/skills.ts +0 -6
  244. package/src/config/seed-inference-profiles.ts +1 -1
  245. package/src/contacts/contact-store.ts +0 -30
  246. package/src/contacts/contacts-write.ts +0 -27
  247. package/src/context/window-manager.ts +1 -2
  248. package/src/credential-execution/feature-gates.ts +10 -10
  249. package/src/credential-execution/process-manager.ts +12 -41
  250. package/src/daemon/__tests__/conversation-tool-setup.test.ts +126 -5
  251. package/src/daemon/bootstrap-turn-cleanup.ts +45 -0
  252. package/src/daemon/config-watcher.ts +4 -3
  253. package/src/daemon/conversation-agent-loop-handlers.ts +21 -3
  254. package/src/daemon/conversation-agent-loop.ts +32 -28
  255. package/src/daemon/conversation-lifecycle.ts +8 -1
  256. package/src/daemon/conversation-process.ts +16 -11
  257. package/src/daemon/conversation-runtime-assembly.ts +2 -2
  258. package/src/daemon/conversation-surfaces.ts +125 -4
  259. package/src/daemon/conversation-tool-setup.ts +16 -55
  260. package/src/daemon/conversation.ts +21 -2
  261. package/src/daemon/doordash-steps.ts +1 -1
  262. package/src/daemon/handlers/shared.ts +4 -1
  263. package/src/daemon/host-app-control-proxy.ts +293 -0
  264. package/src/daemon/host-bash-proxy.ts +84 -74
  265. package/src/daemon/host-browser-proxy.ts +67 -82
  266. package/src/daemon/host-cu-proxy.ts +81 -86
  267. package/src/daemon/host-file-proxy.ts +93 -69
  268. package/src/daemon/host-proxy-base.ts +294 -0
  269. package/src/daemon/host-proxy-preactivation.ts +82 -0
  270. package/src/daemon/host-transfer-proxy.ts +247 -129
  271. package/src/daemon/lifecycle.ts +115 -117
  272. package/src/daemon/message-protocol.ts +3 -8
  273. package/src/daemon/message-types/contacts.ts +23 -1
  274. package/src/daemon/message-types/conversations.ts +11 -8
  275. package/src/daemon/message-types/host-app-control.ts +150 -0
  276. package/src/daemon/message-types/host-bash.ts +4 -0
  277. package/src/daemon/message-types/host-cu.ts +2 -0
  278. package/src/daemon/message-types/host-file.ts +4 -0
  279. package/src/daemon/message-types/host-transfer.ts +3 -0
  280. package/src/daemon/message-types/schedules.ts +8 -3
  281. package/src/daemon/message-types/skills.ts +2 -2
  282. package/src/daemon/process-message.ts +18 -1
  283. package/src/daemon/shutdown-handlers.ts +0 -3
  284. package/src/daemon/tool-setup-types.ts +51 -0
  285. package/src/daemon/tool-side-effects.ts +1 -1
  286. package/src/events/tool-audit-listener.ts +2 -1
  287. package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +15 -7
  288. package/src/heartbeat/__tests__/heartbeat-run-store.test.ts +216 -0
  289. package/src/heartbeat/heartbeat-run-store.ts +236 -0
  290. package/src/heartbeat/heartbeat-service.ts +280 -49
  291. package/src/home/__tests__/post-connect-feed.test.ts +99 -0
  292. package/src/home/__tests__/relationship-state-writer.test.ts +11 -9
  293. package/src/home/__tests__/suggested-prompts.test.ts +89 -0
  294. package/src/home/post-connect-feed.ts +68 -0
  295. package/src/home/relationship-state-writer.ts +17 -92
  296. package/src/home/suggested-prompts.ts +46 -10
  297. package/src/inbound/public-ingress-urls.ts +32 -34
  298. package/src/ipc/__tests__/route-error-envelope.test.ts +80 -0
  299. package/src/ipc/assistant-server.ts +14 -1
  300. package/src/ipc/cli-client.ts +32 -1
  301. package/src/live-voice/live-voice-metrics.ts +10 -10
  302. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +304 -0
  303. package/src/mcp/mcp-auth-orchestrator.ts +213 -0
  304. package/src/mcp/mcp-auth-state.ts +133 -0
  305. package/src/mcp/mcp-oauth-provider.ts +19 -0
  306. package/src/memory/__tests__/jobs-store-job-classes.test.ts +24 -0
  307. package/src/memory/__tests__/qdrant-client-sentinel.test.ts +49 -0
  308. package/src/memory/__tests__/sparse-tokenize.test.ts +66 -0
  309. package/src/memory/anisotropy.test.ts +247 -0
  310. package/src/memory/anisotropy.ts +443 -0
  311. package/src/memory/auto-analysis-constants.ts +17 -0
  312. package/src/memory/auto-analysis-guard.ts +5 -15
  313. package/src/memory/canonical-guardian-store.ts +7 -7
  314. package/src/memory/context-search/__tests__/agent-runner-redaction.test.ts +122 -0
  315. package/src/memory/context-search/agent-protocol.ts +6 -6
  316. package/src/memory/context-search/agent-runner.ts +32 -7
  317. package/src/memory/context-search/sources/memory-v2.ts +17 -5
  318. package/src/memory/conversation-crud.ts +1 -1
  319. package/src/memory/conversation-key-store.ts +2 -15
  320. package/src/memory/db-init.ts +4 -0
  321. package/src/memory/embedding-backend.ts +9 -21
  322. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +49 -4
  323. package/src/memory/graph/conversation-graph-memory.ts +1 -24
  324. package/src/memory/graph/graph-search.ts +8 -0
  325. package/src/memory/graph/retriever.ts +28 -0
  326. package/src/memory/graph/tools.ts +1 -1
  327. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +8 -2
  328. package/src/memory/jobs/embed-concept-page.ts +28 -2
  329. package/src/memory/jobs/embed-pkb-file.test.ts +2 -2
  330. package/src/memory/jobs-store.ts +66 -22
  331. package/src/memory/jobs-worker.ts +112 -63
  332. package/src/memory/memory-v2-activation-log-store.ts +1 -1
  333. package/src/memory/migrations/237-heartbeat-runs.ts +45 -0
  334. package/src/memory/migrations/238-schedule-retry-policy.ts +20 -0
  335. package/src/memory/migrations/index.ts +5 -0
  336. package/src/memory/migrations/registry.ts +8 -0
  337. package/src/memory/pkb/pkb-search.ts +7 -0
  338. package/src/memory/qdrant-client.ts +50 -20
  339. package/src/memory/schema/infrastructure.ts +15 -0
  340. package/src/memory/search/semantic.ts +7 -0
  341. package/src/memory/sparse-tokenize.ts +49 -0
  342. package/src/memory/v2/__tests__/activation.test.ts +77 -95
  343. package/src/memory/v2/__tests__/injection.test.ts +43 -21
  344. package/src/memory/v2/__tests__/sim.test.ts +166 -6
  345. package/src/memory/v2/__tests__/sparse-bm25.test.ts +292 -0
  346. package/src/memory/v2/__tests__/static-context.test.ts +0 -1
  347. package/src/memory/v2/activation.ts +69 -88
  348. package/src/memory/v2/consolidation-job.ts +3 -5
  349. package/src/memory/v2/constants.ts +7 -0
  350. package/src/memory/v2/injection.ts +86 -53
  351. package/src/memory/v2/prompts/consolidation.ts +312 -91
  352. package/src/memory/v2/qdrant.ts +99 -1
  353. package/src/memory/v2/sim.ts +126 -16
  354. package/src/memory/v2/skill-qdrant.ts +12 -3
  355. package/src/memory/v2/skill-store.ts +16 -1
  356. package/src/memory/v2/sparse-bm25.ts +245 -0
  357. package/src/memory/v2/static-context.ts +6 -5
  358. package/src/messaging/providers/gmail/types.ts +0 -49
  359. package/src/messaging/providers/slack/adapter.ts +1 -31
  360. package/src/messaging/providers/slack/types.ts +0 -32
  361. package/src/notifications/README.md +10 -10
  362. package/src/notifications/broadcaster.ts +1 -1
  363. package/src/notifications/guardian-question-mode.ts +5 -5
  364. package/src/oauth/connect-orchestrator.ts +4 -0
  365. package/src/oauth/credential-token-resolver.ts +1 -3
  366. package/src/oauth/manual-token-connection.ts +0 -4
  367. package/src/outbound-proxy/index.ts +1 -37
  368. package/src/outbound-proxy/logging.ts +1 -1
  369. package/src/outbound-proxy/policy.ts +6 -5
  370. package/src/outbound-proxy/router.ts +2 -1
  371. package/src/permissions/approval-policy.test.ts +6 -275
  372. package/src/permissions/approval-policy.ts +0 -51
  373. package/src/permissions/checker.test.ts +0 -1
  374. package/src/permissions/checker.ts +3 -17
  375. package/src/permissions/gateway-threshold-reader.ts +2 -0
  376. package/src/permissions/prompter.ts +34 -1
  377. package/src/permissions/secret-prompter.ts +6 -2
  378. package/src/prompts/bootstrap-cleanup.ts +27 -0
  379. package/src/prompts/system-prompt.ts +3 -18
  380. package/src/prompts/templates/SOUL.md +13 -1
  381. package/src/providers/speech-to-text/provider-catalog.ts +7 -8
  382. package/src/runtime/assistant-event-hub.ts +118 -96
  383. package/src/runtime/assistant-event.ts +1 -0
  384. package/src/runtime/auth/__tests__/middleware.test.ts +11 -56
  385. package/src/runtime/auth/middleware.ts +0 -96
  386. package/src/runtime/auth/route-policy.ts +19 -0
  387. package/src/runtime/btw-sidechain.ts +2 -3
  388. package/src/runtime/channel-invite-transport.ts +2 -48
  389. package/src/runtime/channel-invite-transports/email.ts +1 -1
  390. package/src/runtime/channel-invite-transports/slack.ts +1 -1
  391. package/src/runtime/channel-invite-transports/telegram.ts +1 -1
  392. package/src/runtime/channel-invite-transports/voice.ts +1 -1
  393. package/src/runtime/channel-invite-transports/whatsapp.ts +1 -1
  394. package/src/runtime/channel-invite-types.ts +54 -0
  395. package/src/runtime/channel-readiness-service.ts +32 -13
  396. package/src/runtime/http-server.ts +3 -329
  397. package/src/runtime/http-types.ts +0 -5
  398. package/src/runtime/migrations/__tests__/vbundle-import-parity.test.ts +413 -0
  399. package/src/runtime/migrations/__tests__/vbundle-import-policy.test.ts +260 -0
  400. package/src/runtime/migrations/__tests__/vbundle-import-version-compat.test.ts +189 -0
  401. package/src/runtime/migrations/__tests__/vbundle-streaming-importer.test.ts +153 -1
  402. package/src/runtime/migrations/__tests__/vbundle-symlink-importer.test.ts +451 -0
  403. package/src/runtime/migrations/__tests__/vbundle-symlink-streaming-importer.test.ts +0 -0
  404. package/src/runtime/migrations/__tests__/vbundle-symlink-streaming.test.ts +515 -0
  405. package/src/runtime/migrations/__tests__/vbundle-symlink-tar.test.ts +437 -0
  406. package/src/runtime/migrations/__tests__/vbundle-symlink-walker.test.ts +319 -0
  407. package/src/runtime/migrations/__tests__/vbundle-validator-v1-schema.test.ts +51 -1
  408. package/src/runtime/migrations/migration-transport.ts +7 -7
  409. package/src/runtime/migrations/vbundle-builder.ts +327 -60
  410. package/src/runtime/migrations/vbundle-import-analyzer.ts +4 -4
  411. package/src/runtime/migrations/vbundle-import-policy.ts +172 -0
  412. package/src/runtime/migrations/vbundle-importer.ts +245 -68
  413. package/src/runtime/migrations/vbundle-streaming-importer.ts +326 -35
  414. package/src/runtime/migrations/vbundle-streaming-validator.ts +157 -4
  415. package/src/runtime/migrations/vbundle-tar-stream.ts +15 -6
  416. package/src/runtime/migrations/vbundle-validator.ts +114 -0
  417. package/src/runtime/pending-interactions.ts +35 -9
  418. package/src/runtime/routes/__tests__/backup-routes.test.ts +22 -150
  419. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +98 -0
  420. package/src/runtime/routes/__tests__/gateway-log-routes.test.ts +242 -0
  421. package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +112 -0
  422. package/src/runtime/routes/approval-interception-types.ts +13 -0
  423. package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +1 -1
  424. package/src/runtime/routes/backup-routes.ts +15 -38
  425. package/src/runtime/routes/btw-routes.ts +14 -37
  426. package/src/runtime/routes/client-routes.ts +1 -0
  427. package/src/runtime/routes/contact-prompt-routes.ts +183 -0
  428. package/src/runtime/routes/conversation-query-routes.ts +36 -1
  429. package/src/runtime/routes/conversation-routes.ts +30 -13
  430. package/src/runtime/routes/document-pdf-renderer.ts +165 -0
  431. package/src/runtime/routes/documents-routes.ts +30 -0
  432. package/src/runtime/routes/errors.ts +19 -4
  433. package/src/runtime/routes/events-routes.ts +12 -6
  434. package/src/runtime/routes/gateway-log-routes.ts +79 -0
  435. package/src/runtime/routes/guardian-approval-interception.ts +2 -8
  436. package/src/runtime/routes/heartbeat-routes.ts +103 -38
  437. package/src/runtime/routes/host-app-control-routes.ts +134 -0
  438. package/src/runtime/routes/host-bash-routes.ts +36 -6
  439. package/src/runtime/routes/host-browser-routes.ts +108 -13
  440. package/src/runtime/routes/host-cu-routes.ts +44 -14
  441. package/src/runtime/routes/host-file-routes.ts +33 -10
  442. package/src/runtime/routes/host-transfer-routes.ts +64 -24
  443. package/src/runtime/routes/http-adapter.ts +1 -0
  444. package/src/runtime/routes/identity-intro-cache.ts +30 -0
  445. package/src/runtime/routes/identity-routes.ts +15 -43
  446. package/src/runtime/routes/inbound-message-handler.ts +1 -9
  447. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -7
  448. package/src/runtime/routes/inbound-stages/edit-intercept.ts +0 -8
  449. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +0 -20
  450. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +5 -13
  451. package/src/runtime/routes/index.ts +8 -0
  452. package/src/runtime/routes/mcp-auth-routes.ts +132 -0
  453. package/src/runtime/routes/memory-item-routes.ts +10 -12
  454. package/src/runtime/routes/memory-v2-routes.ts +441 -1
  455. package/src/runtime/routes/migration-routes.ts +96 -0
  456. package/src/runtime/routes/schedule-routes.ts +7 -0
  457. package/src/runtime/verification-templates.ts +4 -7
  458. package/src/schedule/integration-status.ts +66 -2
  459. package/src/schedule/recurrence-engine.ts +4 -1
  460. package/src/schedule/retry-backoff.ts +18 -0
  461. package/src/schedule/retry-policy.ts +82 -0
  462. package/src/schedule/schedule-recovery.ts +64 -0
  463. package/src/schedule/schedule-store.ts +106 -2
  464. package/src/schedule/scheduler-types.ts +25 -0
  465. package/src/schedule/scheduler.ts +63 -38
  466. package/src/security/oauth-callback-registry.ts +8 -0
  467. package/src/sequence/analytics.ts +5 -5
  468. package/src/sequence/engine.ts +1 -1
  469. package/src/skills/catalog-files.ts +2 -8
  470. package/src/skills/include-graph.ts +5 -5
  471. package/src/skills/remote-skill-policy.ts +5 -5
  472. package/src/skills/skill-file-provider.ts +1 -1
  473. package/src/skills/skill-file-types.ts +13 -0
  474. package/src/skills/skillssh-audit-types.ts +28 -0
  475. package/src/skills/skillssh-registry.ts +8 -21
  476. package/src/telemetry/types.ts +2 -0
  477. package/src/telemetry/usage-telemetry-reporter.test.ts +21 -0
  478. package/src/telemetry/usage-telemetry-reporter.ts +1 -0
  479. package/src/tools/app-control/skill-proxy-bridge.ts +28 -0
  480. package/src/tools/apps/executors.ts +56 -69
  481. package/src/tools/browser/__tests__/browser-status.test.ts +21 -18
  482. package/src/tools/browser/browser-execution.ts +2 -2
  483. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +55 -4
  484. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/ws-transport.test.ts +12 -6
  485. package/src/tools/browser/cdp-client/factory.ts +23 -24
  486. package/src/tools/browser/cdp-client/index.ts +1 -14
  487. package/src/tools/computer-use/definitions.ts +42 -20
  488. package/src/tools/executor.ts +2 -0
  489. package/src/tools/host-filesystem/edit.ts +26 -0
  490. package/src/tools/host-filesystem/read.ts +26 -0
  491. package/src/tools/host-filesystem/transfer.ts +31 -1
  492. package/src/tools/host-filesystem/write.ts +26 -0
  493. package/src/tools/host-terminal/host-shell.ts +58 -0
  494. package/src/tools/schedule/create.ts +6 -0
  495. package/src/tools/schedule/list.ts +2 -0
  496. package/src/tools/schedule/update.ts +10 -0
  497. package/src/tools/shared/filesystem/file-ops-service.ts +2 -0
  498. package/src/tools/shared/filesystem/path-policy.ts +25 -1
  499. package/src/tools/skills/load.ts +0 -32
  500. package/src/tools/tool-approval-handler.ts +1 -5
  501. package/src/tools/types.ts +4 -0
  502. package/src/usage/pricing.ts +1 -1
  503. package/src/workspace/hatched-date.ts +86 -0
  504. package/src/workspace/migrations/003-seed-device-id.ts +1 -1
  505. package/src/workspace/migrations/006-services-config.ts +8 -5
  506. package/src/workspace/migrations/016-extract-feature-flags-to-protected.ts +3 -9
  507. package/src/workspace/migrations/021-move-signals-to-workspace.ts +4 -10
  508. package/src/workspace/migrations/022-move-hooks-to-workspace.ts +4 -10
  509. package/src/workspace/migrations/023-move-config-files-to-workspace.ts +4 -11
  510. package/src/workspace/migrations/024-move-runtime-files-to-workspace.ts +3 -10
  511. package/src/workspace/migrations/040-seed-latency-callsite-defaults.ts +3 -2
  512. package/src/workspace/migrations/050-seed-main-agent-opus-callsite.ts +2 -1
  513. package/src/workspace/migrations/059-move-pid-to-workspace.ts +3 -8
  514. package/src/workspace/migrations/061-move-backup-key-to-workspace.ts +3 -8
  515. package/src/workspace/migrations/AGENTS.md +1 -1
  516. package/src/workspace/migrations/migrate-to-workspace-volume.ts +4 -10
  517. package/src/workspace/migrations/utils.ts +21 -0
  518. package/src/__tests__/host-browser-e2e-cloud.test.ts +0 -443
  519. package/src/__tests__/host-browser-e2e-self-hosted-capability.test.ts +0 -226
  520. package/src/__tests__/host-browser-ws-events-e2e.test.ts +0 -427
  521. package/src/__tests__/twilio-rest.test.ts +0 -34
  522. package/src/backup/__tests__/backup-key.test.ts +0 -152
  523. package/src/backup/__tests__/backup-worker.test.ts +0 -782
  524. package/src/backup/__tests__/offsite-writer.test.ts +0 -641
  525. package/src/backup/__tests__/stream-crypt.test.ts +0 -228
  526. package/src/backup/backup-key.ts +0 -137
  527. package/src/backup/backup-worker.ts +0 -472
  528. package/src/backup/offsite-writer.ts +0 -222
  529. package/src/backup/stream-crypt.ts +0 -263
  530. package/src/daemon/message-types/pairing.ts +0 -58
  531. package/src/outbound-proxy/config.ts +0 -20
  532. package/src/outbound-proxy/health.ts +0 -18
  533. package/src/outbound-proxy/types.ts +0 -150
  534. package/src/runtime/capability-tokens.ts +0 -190
  535. package/src/signals/mcp-reload.ts +0 -18
@@ -236,6 +236,7 @@ export async function hybridQueryConceptPages(
236
236
  sparse: SparseEmbedding,
237
237
  limit: number,
238
238
  restrictToSlugs?: readonly string[],
239
+ options?: { skipSparse?: boolean },
239
240
  ): Promise<ConceptPageQueryResult[]> {
240
241
  if (restrictToSlugs && restrictToSlugs.length === 0) {
241
242
  // An empty restriction means "no candidates"; skip the round-trip.
@@ -249,6 +250,13 @@ export async function hybridQueryConceptPages(
249
250
  ? { must: [{ key: "slug", match: { any: [...restrictToSlugs] } }] }
250
251
  : undefined;
251
252
 
253
+ // When the caller weighted sparse to zero, skip the round-trip entirely.
254
+ // The downstream fuser (`fuseHit` in `sim.ts`) already treats a missing
255
+ // sparse score as a 0 contribution, so omitting the query is a pure
256
+ // optimization — and it's also the kill switch operators use to dodge a
257
+ // Qdrant 1.13.x sparse-index crash that we've reproduced in the wild.
258
+ const skipSparse = options?.skipSparse ?? false;
259
+
252
260
  const denseQuery = () =>
253
261
  client.query(MEMORY_V2_COLLECTION, {
254
262
  query: dense,
@@ -267,7 +275,14 @@ export async function hybridQueryConceptPages(
267
275
  });
268
276
 
269
277
  // Run both queries concurrently — they hit independent named vectors.
270
- const runQueries = async () => Promise.all([denseQuery(), sparseQuery()]);
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.
281
+ const emptyResult = {
282
+ points: [] as Array<{ payload?: unknown; score?: number }>,
283
+ };
284
+ const runQueries = async () =>
285
+ Promise.all([denseQuery(), skipSparse ? emptyResult : sparseQuery()]);
271
286
 
272
287
  let denseResults;
273
288
  let sparseResults;
@@ -305,6 +320,89 @@ export async function hybridQueryConceptPages(
305
320
  return Array.from(merged.values());
306
321
  }
307
322
 
323
+ /**
324
+ * Page through the v2 concept-page collection and return up to `maxSamples`
325
+ * stored dense vectors. Used by the anisotropy-fit pipeline to compute a
326
+ * corpus mean + top-k principal components without re-embedding every page.
327
+ *
328
+ * Sparse vectors are skipped — anisotropy is a dense-embedding phenomenon, and
329
+ * pulling the sparse side would just inflate the response. Payload is also
330
+ * skipped because the fit doesn't need slug identity.
331
+ *
332
+ * Returns an empty array when the collection is empty or missing. Caller
333
+ * decides what to do (typically: surface a "no vectors to fit" error).
334
+ */
335
+ export async function sampleConceptPageDenseVectors(
336
+ maxSamples: number,
337
+ ): Promise<number[][]> {
338
+ if (maxSamples <= 0) return [];
339
+ await ensureConceptPageCollection();
340
+
341
+ const client = getClient();
342
+ const out: number[][] = [];
343
+ let offset: string | number | undefined = undefined;
344
+ // Same pagination guard pattern as the rest of the file — bounds the loop
345
+ // even if Qdrant somehow keeps handing back a non-null offset.
346
+ const maxIterations = 10_000;
347
+ const batchSize = Math.min(256, maxSamples);
348
+
349
+ for (let i = 0; i < maxIterations; i++) {
350
+ if (out.length >= maxSamples) break;
351
+ const remaining = maxSamples - out.length;
352
+ let result;
353
+ try {
354
+ result = await client.scroll(MEMORY_V2_COLLECTION, {
355
+ limit: Math.min(batchSize, remaining),
356
+ with_payload: false,
357
+ // Fetch only the dense named vector — sparse is irrelevant for
358
+ // anisotropy correction.
359
+ with_vector: ["dense"],
360
+ ...(offset !== undefined ? { offset } : {}),
361
+ });
362
+ } catch (err) {
363
+ if (isCollectionMissing(err)) {
364
+ _collectionReady = false;
365
+ return out;
366
+ }
367
+ throw err;
368
+ }
369
+
370
+ for (const point of result.points) {
371
+ const v = extractDenseVector(point.vector);
372
+ if (v) out.push(v);
373
+ if (out.length >= maxSamples) break;
374
+ }
375
+
376
+ const next = result.next_page_offset;
377
+ if (next == null) break;
378
+ offset = typeof next === "string" ? next : (next as number);
379
+ }
380
+
381
+ return out;
382
+ }
383
+
384
+ /**
385
+ * Pull the `dense` named-vector payload out of a Qdrant point. Defensively
386
+ * handles both the named-vector shape (`{ dense: [...] }`) and the legacy
387
+ * unnamed-vector shape (`number[]`) so older collection layouts don't trip
388
+ * the sampler. Returns `null` for shapes we don't recognise.
389
+ */
390
+ function extractDenseVector(vector: unknown): number[] | null {
391
+ if (Array.isArray(vector)) {
392
+ if (vector.every((n) => typeof n === "number")) {
393
+ return vector as number[];
394
+ }
395
+ return null;
396
+ }
397
+ if (vector && typeof vector === "object") {
398
+ const dense = (vector as { dense?: unknown }).dense;
399
+ if (Array.isArray(dense) && dense.every((n) => typeof n === "number")) {
400
+ return dense as number[];
401
+ }
402
+ }
403
+ return null;
404
+ }
405
+
308
406
  /**
309
407
  * Detect "collection not found" errors so callers can reset readiness and
310
408
  * retry after an external deletion (e.g. workspace reset).
@@ -26,13 +26,12 @@
26
26
  // only as a per-turn ordering signal, not compared across turns.
27
27
 
28
28
  import type { AssistantConfig } from "../../config/types.js";
29
- import {
30
- embedWithBackend,
31
- generateSparseEmbedding,
32
- } from "../embedding-backend.js";
29
+ import { applyCorrectionIfCalibrated } from "../anisotropy.js";
30
+ import { embedWithBackend } from "../embedding-backend.js";
33
31
  import { clampUnitInterval } from "../validation.js";
34
32
  import { hybridQueryConceptPages } from "./qdrant.js";
35
33
  import { hybridQuerySkills } from "./skill-qdrant.js";
34
+ import { generateBm25QueryEmbedding } from "./sparse-bm25.js";
36
35
 
37
36
  /**
38
37
  * Clamp a value into the closed unit interval [0, 1]. Re-exported under the
@@ -40,6 +39,79 @@ import { hybridQuerySkills } from "./skill-qdrant.js";
40
39
  */
41
40
  export const clamp01 = clampUnitInterval;
42
41
 
42
+ /**
43
+ * Built-in defaults for adaptive sparse weighting. Live here (not in the
44
+ * config schema) so operators don't see two new knobs in their config until
45
+ * they actually want to tune them.
46
+ *
47
+ * Below `MIN_SPREAD`, the sparse channel is treated as no-signal (its scores
48
+ * are uniform across the candidate set, so it can't rank anything) and the
49
+ * sparse weight collapses to 0. At or above `FULL_SPREAD`, sparse weight
50
+ * stays at its configured value. Linear interpolation between.
51
+ */
52
+ const ADAPTIVE_SPARSE_MIN_SPREAD = 0.2;
53
+ const ADAPTIVE_SPARSE_FULL_SPREAD = 0.5;
54
+
55
+ /**
56
+ * Per-query effective dense + sparse weights, derived from the configured
57
+ * base weights and the spread of normalized sparse scores across the hit
58
+ * set. When the sparse channel can't discriminate (low spread or fewer
59
+ * than two sparse-bearing candidates), its weight collapses and dense
60
+ * weight is boosted to compensate so `dense + sparse` still equals
61
+ * `baseDense + baseSparse` and `fused` stays interpretable as a [0, 1]
62
+ * similarity.
63
+ *
64
+ * Pure function — exported so the diagnostic surface in
65
+ * `memory-v2-routes.explain-similarity` can show the effective weights and
66
+ * the measured spread alongside per-channel score statistics.
67
+ */
68
+ export function effectiveWeights(
69
+ hits: ReadonlyArray<{ sparseScore?: number }>,
70
+ maxSparse: number,
71
+ baseDense: number,
72
+ baseSparse: number,
73
+ config: AssistantConfig,
74
+ ): { dense: number; sparse: number; spread: number } {
75
+ // Short-circuit when the channel is already disabled or unscored. Returning
76
+ // base weights here keeps `fused` numerically identical to today's output
77
+ // for the no-sparse-signal cases the existing tests assume.
78
+ if (baseSparse === 0 || maxSparse === 0) {
79
+ return { dense: baseDense, sparse: baseSparse, spread: 0 };
80
+ }
81
+ let min = Infinity;
82
+ let max = -Infinity;
83
+ let count = 0;
84
+ for (const h of hits) {
85
+ if (h.sparseScore === undefined) continue;
86
+ const norm = h.sparseScore / maxSparse;
87
+ if (norm < min) min = norm;
88
+ if (norm > max) max = norm;
89
+ count++;
90
+ }
91
+ // With < 2 sparse-bearing hits the spread is undefined — fall back to base
92
+ // weights so single-hit retrievals still surface their sparse contribution
93
+ // (and the existing fusion-math tests stay green).
94
+ if (count < 2) {
95
+ return { dense: baseDense, sparse: baseSparse, spread: 0 };
96
+ }
97
+ const spread = max - min;
98
+
99
+ const minSpread =
100
+ config.memory.v2.min_sparse_spread ?? ADAPTIVE_SPARSE_MIN_SPREAD;
101
+ const fullSpread =
102
+ config.memory.v2.full_sparse_spread ?? ADAPTIVE_SPARSE_FULL_SPREAD;
103
+ // Degenerate config (full <= min): no interpolation range. Don't try to
104
+ // adapt; trust the operator's base weights and report the measured spread
105
+ // for diagnostics.
106
+ if (fullSpread <= minSpread) {
107
+ return { dense: baseDense, sparse: baseSparse, spread };
108
+ }
109
+ const factor = clamp01((spread - minSpread) / (fullSpread - minSpread));
110
+ const sparse = baseSparse * factor;
111
+ const dense = baseDense + (baseSparse - sparse);
112
+ return { dense, sparse, spread };
113
+ }
114
+
43
115
  /**
44
116
  * Compute hybrid (dense + sparse) similarity scores between a query text and
45
117
  * a fixed set of candidate concept-page slugs.
@@ -63,8 +135,13 @@ export const clamp01 = clampUnitInterval;
63
135
  * Edge cases:
64
136
  * - Empty `candidateSlugs` → returns an empty map without touching Qdrant
65
137
  * or the embedding backend.
66
- * - Empty query text or all-zero sparse vector still queries (dense may
67
- * still hit), and the sparse contribution to fusion is zero.
138
+ * - Empty / whitespace-only `text` returns an empty map without touching
139
+ * Qdrant or the embedding backend. The Gemini embedding API rejects empty
140
+ * content with HTTP 400, and short-circuiting here prevents the failure
141
+ * from cascading through `Promise.all` in `computeOwnActivation` (e.g.
142
+ * turn 1 has no prior assistant message, so its `simBatch` channel is
143
+ * called with `""`). Treating the channel's contribution as 0 is the
144
+ * same outcome a no-hit query would produce.
68
145
  */
69
146
  export async function simBatch(
70
147
  text: string,
@@ -74,12 +151,20 @@ export async function simBatch(
74
151
  if (candidateSlugs.length === 0) {
75
152
  return new Map();
76
153
  }
154
+ if (text.trim().length === 0) {
155
+ return new Map();
156
+ }
77
157
 
78
- // Sparse uses the shared TF-IDF encoder so the query and stored vectors
79
- // share a vocabulary with PKB indexing.
158
+ // Sparse uses BM25: the query side encodes binary occurrences per token,
159
+ // and the stored doc vectors carry the IDF · TF-saturated weights — Qdrant
160
+ // dot product then yields the BM25 score directly.
80
161
  const denseResult = await embedWithBackend(config, [text]);
81
- const denseVector = denseResult.vectors[0];
82
- const sparseVector = generateSparseEmbedding(text);
162
+ const denseVector = await applyCorrectionIfCalibrated(
163
+ denseResult.vectors[0],
164
+ denseResult.provider,
165
+ denseResult.model,
166
+ );
167
+ const sparseVector = generateBm25QueryEmbedding(text);
83
168
 
84
169
  const hits = await hybridQueryConceptPages(
85
170
  denseVector,
@@ -93,8 +178,15 @@ export async function simBatch(
93
178
  }
94
179
 
95
180
  const maxSparse = computeMaxSparse(hits);
96
- const { dense_weight: denseWeight, sparse_weight: sparseWeight } =
181
+ const { dense_weight: baseDense, sparse_weight: baseSparse } =
97
182
  config.memory.v2;
183
+ const { dense: denseWeight, sparse: sparseWeight } = effectiveWeights(
184
+ hits,
185
+ maxSparse,
186
+ baseDense,
187
+ baseSparse,
188
+ config,
189
+ );
98
190
 
99
191
  const scores = new Map<string, number>();
100
192
  for (const hit of hits) {
@@ -122,8 +214,12 @@ export async function simBatch(
122
214
  * Edge cases:
123
215
  * - Empty `ids` → returns an empty map without touching Qdrant or the
124
216
  * embedding backend.
125
- * - Empty query text → still queries (dense may still hit), and the sparse
126
- * contribution is zero.
217
+ * - Empty / whitespace-only `text`returns an empty map without touching
218
+ * Qdrant or the embedding backend. Same rationale as {@link simBatch}:
219
+ * Gemini rejects empty content with HTTP 400, so the activation pipeline
220
+ * would otherwise fail on turn 1 (where the assistant-text channel is
221
+ * `""`). Treating the channel's contribution as 0 matches a no-hit
222
+ * query.
127
223
  */
128
224
  export async function simSkillBatch(
129
225
  text: string,
@@ -133,10 +229,17 @@ export async function simSkillBatch(
133
229
  if (ids.length === 0) {
134
230
  return new Map();
135
231
  }
232
+ if (text.trim().length === 0) {
233
+ return new Map();
234
+ }
136
235
 
137
236
  const denseResult = await embedWithBackend(config, [text]);
138
- const denseVector = denseResult.vectors[0];
139
- const sparseVector = generateSparseEmbedding(text);
237
+ const denseVector = await applyCorrectionIfCalibrated(
238
+ denseResult.vectors[0],
239
+ denseResult.provider,
240
+ denseResult.model,
241
+ );
242
+ const sparseVector = generateBm25QueryEmbedding(text);
140
243
 
141
244
  const hits = await hybridQuerySkills(
142
245
  denseVector,
@@ -160,8 +263,15 @@ export async function simSkillBatch(
160
263
  }
161
264
 
162
265
  const maxSparse = computeMaxSparse(filtered);
163
- const { dense_weight: denseWeight, sparse_weight: sparseWeight } =
266
+ const { dense_weight: baseDense, sparse_weight: baseSparse } =
164
267
  config.memory.v2;
268
+ const { dense: denseWeight, sparse: sparseWeight } = effectiveWeights(
269
+ filtered,
270
+ maxSparse,
271
+ baseDense,
272
+ baseSparse,
273
+ config,
274
+ );
165
275
 
166
276
  const scores = new Map<string, number>();
167
277
  for (const hit of filtered) {
@@ -278,14 +278,14 @@ export async function pruneSkillsExcept(
278
278
  * candidate set is already known so the activation scorer gets scores for
279
279
  * exactly those ids rather than Qdrant's global top-`limit`. An empty list
280
280
  * short-circuits to no results — the caller is asking for "nothing", not
281
- * "everything". Undefined queries the full collection (used by
282
- * `selectSkillCandidates` to discover candidates from the global top-K).
281
+ * "everything". Undefined queries the full collection.
283
282
  */
284
283
  export async function hybridQuerySkills(
285
284
  dense: number[],
286
285
  sparse: SparseEmbedding,
287
286
  limit: number,
288
287
  restrictToIds?: readonly string[],
288
+ options?: { skipSparse?: boolean },
289
289
  ): Promise<SkillQueryResult[]> {
290
290
  if (restrictToIds && restrictToIds.length === 0) {
291
291
  // An empty restriction means "no candidates"; skip the round-trip.
@@ -299,6 +299,11 @@ export async function hybridQuerySkills(
299
299
  ? { must: [{ key: "id", match: { any: [...restrictToIds] } }] }
300
300
  : undefined;
301
301
 
302
+ // Same opt-in short-circuit as `hybridQueryConceptPages`: skip the sparse
303
+ // round-trip entirely so we sidestep the Qdrant 1.13.x sparse-index OOM
304
+ // crash when operators flip sparse off via `sparse_weight: 0`.
305
+ const skipSparse = options?.skipSparse ?? false;
306
+
302
307
  const denseQuery = () =>
303
308
  client.query(MEMORY_V2_SKILLS_COLLECTION, {
304
309
  query: dense,
@@ -317,7 +322,11 @@ export async function hybridQuerySkills(
317
322
  });
318
323
 
319
324
  // Run both queries concurrently — they hit independent named vectors.
320
- const runQueries = async () => Promise.all([denseQuery(), sparseQuery()]);
325
+ const emptyResult = {
326
+ points: [] as Array<{ payload?: unknown; score?: number }>,
327
+ };
328
+ const runQueries = async () =>
329
+ Promise.all([denseQuery(), skipSparse ? emptyResult : sparseQuery()]);
321
330
 
322
331
  let denseResults;
323
332
  let sparseResults;
@@ -25,6 +25,7 @@ import {
25
25
  fromSkillSummary,
26
26
  } from "../../skills/skill-memory.js";
27
27
  import { getLogger } from "../../util/logger.js";
28
+ import { applyCorrectionIfCalibrated } from "../anisotropy.js";
28
29
  import {
29
30
  embedWithBackend,
30
31
  generateSparseEmbedding,
@@ -122,10 +123,15 @@ export async function seedV2SkillEntries(): Promise<void> {
122
123
 
123
124
  // Embed all content strings in one batched call. Sparse vectors are
124
125
  // computed in-process (no network).
125
- const { vectors: denseVectors } = await embedWithBackend(
126
+ const embedded = await embedWithBackend(
126
127
  config,
127
128
  seeds.map((s) => s.content),
128
129
  );
130
+ const denseVectors = await Promise.all(
131
+ embedded.vectors.map((v) =>
132
+ applyCorrectionIfCalibrated(v, embedded.provider, embedded.model),
133
+ ),
134
+ );
129
135
 
130
136
  const now = Date.now();
131
137
  const nextEntries = new Map<string, SkillEntry>();
@@ -170,6 +176,15 @@ export function getSkillCapability(id: string): SkillEntry | null {
170
176
  return entries?.get(id) ?? null;
171
177
  }
172
178
 
179
+ /**
180
+ * Every skill id in the cache — both installed-and-enabled skills and
181
+ * uninstalled-catalog skills. Empty before the first `seedV2SkillEntries`
182
+ * run completes.
183
+ */
184
+ export function getAllSkillIds(): string[] {
185
+ return entries ? [...entries.keys()] : [];
186
+ }
187
+
173
188
  /** @internal Test-only: clear the module-level cache. */
174
189
  export function _resetSkillStoreForTests(): void {
175
190
  entries = null;
@@ -0,0 +1,245 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Memory v2 — BM25 sparse channel
3
+ // ---------------------------------------------------------------------------
4
+ //
5
+ // Replaces the legacy TF-only sparse embedding (`generateSparseEmbedding` in
6
+ // `../embedding-backend.ts`) with a real Okapi BM25 implementation. Common
7
+ // words like "i", "am", "the" no longer dominate sparse matching the way they
8
+ // did when every token was weighted equally.
9
+ //
10
+ // BM25 score for document `d` and query `q`:
11
+ //
12
+ // score(d, q) = Σ_t∈q IDF(t) · TF_sat(d, t)
13
+ // TF_sat(d, t) = tf(d, t) · (k1 + 1)
14
+ // / (tf(d, t) + k1 · (1 - b + b · |d| / avg_dl))
15
+ // IDF(t) = log( (N - df(t) + 0.5) / (df(t) + 0.5) + 1 )
16
+ //
17
+ // `+1` inside the IDF log keeps the result non-negative even when df(t) > N/2,
18
+ // matching the variant Lucene uses for `BM25Similarity`.
19
+ //
20
+ // **Asymmetric encoding**: documents carry the full BM25 weight per token
21
+ // (IDF · TF_sat baked into the stored vector), and queries carry binary
22
+ // occurrence per token. Qdrant's sparse dot product then reduces to the BM25
23
+ // score directly. Putting BM25 on the doc side means the weights need
24
+ // recomputing whenever the corpus DF or avg_dl changes — operators trigger
25
+ // that with `assistant memory v2 reembed` after major content shifts.
26
+
27
+ import { readFile } from "node:fs/promises";
28
+
29
+ import type { SparseEmbedding } from "../embedding-types.js";
30
+ import {
31
+ SPARSE_VOCAB_SIZE,
32
+ tokenHash,
33
+ tokenizeStemmed,
34
+ } from "../sparse-tokenize.js";
35
+ import { listPages } from "./page-store.js";
36
+
37
+ /**
38
+ * Aggregate corpus statistics used to weight a BM25 document vector. Held in
39
+ * memory after a startup walk over `memory/concepts/`.
40
+ */
41
+ export interface CorpusStats {
42
+ /** Total document count over which DF was accumulated. */
43
+ totalDocs: number;
44
+ /** hashedTokenIndex (in `[0, SPARSE_VOCAB_SIZE)`) → distinct-doc count. */
45
+ df: Map<number, number>;
46
+ /** Average document length in tokens, post-tokenize. */
47
+ avgDl: number;
48
+ /** Wall-clock millis at build time — used by diagnostics, not the formula. */
49
+ builtAt: number;
50
+ }
51
+
52
+ /** BM25 hyperparameters. Standard Lucene/Elasticsearch defaults. */
53
+ export interface Bm25Params {
54
+ /** TF saturation curve. ~1.2 is standard. */
55
+ k1: number;
56
+ /** Length normalization. 0 = none, 1 = full. ~0.75 is standard. */
57
+ b: number;
58
+ }
59
+
60
+ let _conceptPageStats: CorpusStats | null = null;
61
+
62
+ /**
63
+ * Latest in-memory corpus stats for `memory/concepts/`, or `null` if a build
64
+ * has not yet completed. Callers must handle `null` and fall back to legacy
65
+ * TF-only behavior so the daemon remains usable during the brief startup
66
+ * window before {@link rebuildConceptPageCorpusStats} finishes.
67
+ */
68
+ export function getConceptPageCorpusStats(): CorpusStats | null {
69
+ return _conceptPageStats;
70
+ }
71
+
72
+ /**
73
+ * Walk every concept page on disk, accumulate document frequency per hashed
74
+ * token bucket, and average document length. Atomically swaps the result into
75
+ * the module-level cache when the walk succeeds. On error the previous stats
76
+ * stay live.
77
+ *
78
+ * Reads bodies via `readPage`-equivalent direct file reads to avoid paying for
79
+ * frontmatter parsing on every page (we only need the body for sparse).
80
+ */
81
+ export async function rebuildConceptPageCorpusStats(
82
+ workspaceDir: string,
83
+ ): Promise<void> {
84
+ const slugs = await listPages(workspaceDir);
85
+ if (slugs.length === 0) {
86
+ _conceptPageStats = {
87
+ totalDocs: 0,
88
+ df: new Map(),
89
+ avgDl: 0,
90
+ builtAt: Date.now(),
91
+ };
92
+ return;
93
+ }
94
+
95
+ const df = new Map<number, number>();
96
+ let totalTokens = 0;
97
+ let docsCounted = 0;
98
+
99
+ for (const slug of slugs) {
100
+ const body = await readPageBodyForStats(workspaceDir, slug);
101
+ if (body === null) continue;
102
+ const tokens = tokenizeStemmed(body);
103
+ if (tokens.length === 0) continue;
104
+ totalTokens += tokens.length;
105
+ docsCounted += 1;
106
+ const seen = new Set<number>();
107
+ for (const token of tokens) {
108
+ const idx = tokenHash(token, SPARSE_VOCAB_SIZE);
109
+ if (seen.has(idx)) continue;
110
+ seen.add(idx);
111
+ df.set(idx, (df.get(idx) ?? 0) + 1);
112
+ }
113
+ }
114
+
115
+ _conceptPageStats = {
116
+ totalDocs: docsCounted,
117
+ df,
118
+ avgDl: docsCounted > 0 ? totalTokens / docsCounted : 0,
119
+ builtAt: Date.now(),
120
+ };
121
+ }
122
+
123
+ /**
124
+ * Read just the body of a page for stats accumulation. Skips the YAML
125
+ * frontmatter without invoking the schema-validating `readPage` parser, since
126
+ * any parse failure surfaced there would abort the whole rebuild — and we
127
+ * only need the prose half for tokenization.
128
+ */
129
+ async function readPageBodyForStats(
130
+ workspaceDir: string,
131
+ slug: string,
132
+ ): Promise<string | null> {
133
+ const path = `${workspaceDir}/memory/concepts/${slug}.md`;
134
+ let raw: string;
135
+ try {
136
+ raw = await readFile(path, "utf-8");
137
+ } catch {
138
+ return null;
139
+ }
140
+ // Strip a leading `---\n...\n---\n` block if present; otherwise return raw.
141
+ if (raw.startsWith("---")) {
142
+ const closing = raw.indexOf("\n---", 3);
143
+ if (closing !== -1) {
144
+ const after = raw.indexOf("\n", closing + 4);
145
+ if (after !== -1) return raw.slice(after + 1);
146
+ }
147
+ }
148
+ return raw;
149
+ }
150
+
151
+ /**
152
+ * Compute the BM25 IDF weight for a hashed token bucket. Returns `0` when the
153
+ * token's df equals the corpus size (a token in every document carries no
154
+ * discriminating power).
155
+ */
156
+ function computeIdf(stats: CorpusStats, hashIdx: number): number {
157
+ const df = stats.df.get(hashIdx) ?? 0;
158
+ const numerator = stats.totalDocs - df + 0.5;
159
+ const denominator = df + 0.5;
160
+ return Math.log(numerator / denominator + 1);
161
+ }
162
+
163
+ /**
164
+ * Document-side BM25-weighted sparse vector. Each emitted value is
165
+ * `IDF(t) · TF_sat(d, t)` so the dot product against a binary query vector
166
+ * (see {@link generateBm25QueryEmbedding}) yields the BM25 score.
167
+ *
168
+ * Returns an empty embedding for empty input or when the corpus is empty
169
+ * (every IDF would be zero anyway).
170
+ */
171
+ export function generateBm25DocEmbedding(
172
+ text: string,
173
+ stats: CorpusStats,
174
+ params: Bm25Params,
175
+ ): SparseEmbedding {
176
+ const tokens = tokenizeStemmed(text);
177
+ if (tokens.length === 0 || stats.totalDocs === 0) {
178
+ return { indices: [], values: [] };
179
+ }
180
+
181
+ // Per-document term frequencies, keyed by hashed bucket.
182
+ const tf = new Map<number, number>();
183
+ for (const token of tokens) {
184
+ const idx = tokenHash(token, SPARSE_VOCAB_SIZE);
185
+ tf.set(idx, (tf.get(idx) ?? 0) + 1);
186
+ }
187
+
188
+ const docLen = tokens.length;
189
+ // avg_dl can be 0 only when totalDocs is 0, which we already short-circuited.
190
+ const lengthFactor = 1 - params.b + (params.b * docLen) / stats.avgDl;
191
+ const indices: number[] = [];
192
+ const values: number[] = [];
193
+
194
+ for (const [idx, freq] of tf) {
195
+ const idf = computeIdf(stats, idx);
196
+ if (idf === 0) continue; // Skip tokens that contribute nothing to scores.
197
+ const saturated =
198
+ (freq * (params.k1 + 1)) / (freq + params.k1 * lengthFactor);
199
+ const weight = idf * saturated;
200
+ if (weight === 0) continue;
201
+ indices.push(idx);
202
+ values.push(weight);
203
+ }
204
+
205
+ return { indices, values };
206
+ }
207
+
208
+ /**
209
+ * Query-side sparse vector — binary occurrence per distinct query token. The
210
+ * dot product `Σ_t v_q(t) · v_d(t)` against a BM25-weighted document vector
211
+ * is exactly the BM25 score, since `v_q(t) = 1` for tokens in the query and
212
+ * `0` otherwise.
213
+ *
214
+ * Stateless — does not need corpus stats, so callers can use this on every
215
+ * turn without coordinating with {@link rebuildConceptPageCorpusStats}.
216
+ */
217
+ export function generateBm25QueryEmbedding(text: string): SparseEmbedding {
218
+ const tokens = tokenizeStemmed(text);
219
+ if (tokens.length === 0) {
220
+ return { indices: [], values: [] };
221
+ }
222
+
223
+ const seen = new Set<number>();
224
+ const indices: number[] = [];
225
+ const values: number[] = [];
226
+ for (const token of tokens) {
227
+ const idx = tokenHash(token, SPARSE_VOCAB_SIZE);
228
+ if (seen.has(idx)) continue;
229
+ seen.add(idx);
230
+ indices.push(idx);
231
+ values.push(1);
232
+ }
233
+
234
+ return { indices, values };
235
+ }
236
+
237
+ /** @internal Test-only: reset module-level singletons. */
238
+ export function _resetCorpusStatsForTests(): void {
239
+ _conceptPageStats = null;
240
+ }
241
+
242
+ /** @internal Test-only: install a fixture stats table without disk I/O. */
243
+ export function _setCorpusStatsForTests(stats: CorpusStats | null): void {
244
+ _conceptPageStats = stats;
245
+ }
@@ -6,11 +6,12 @@
6
6
  // and returns a concatenated, header-wrapped block ready to splice into the
7
7
  // current user message via the injector chain.
8
8
  //
9
- // Pairs with the v2 per-turn activation block (`prependMemoryV2Block` in
10
- // `conversation-graph-memory.ts`) that block carries activated concept
11
- // pages selected by the activation pipeline; this static block carries the
12
- // always-relevant aggregate views written by consolidation and the user.
13
- // Both land on the user message so the system prompt stays cache-stable.
9
+ // Pairs with the v2 per-turn activation block (`maybeRouteV2Injection` in
10
+ // `conversation-graph-memory.ts`, which threads through `injectTextBlock`)
11
+ // that block carries activated concept pages selected by the activation
12
+ // pipeline; this static block carries the always-relevant aggregate views
13
+ // written by consolidation and the user. Both land on the user message so
14
+ // the system prompt stays cache-stable.
14
15
  //
15
16
  // Refresh cadence is owned by the caller: the agent loop only passes the
16
17
  // content through when `mode === "full"` (first turn / post-compaction),