@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
@@ -60,7 +60,6 @@ const state = {
60
60
  embedCalls: [] as Array<{ inputs: unknown[] }>,
61
61
  sparseCalls: [] as string[],
62
62
  embedReturn: [[0.1, 0.2, 0.3]] as number[][],
63
- sparseReturn: { indices: [1, 2, 3], values: [0.5, 0.5, 0.5] },
64
63
  // Programmable Qdrant query response — one entry per `using` channel,
65
64
  // shifted in order so each test can stage dense + sparse results.
66
65
  queryResponses: {
@@ -90,7 +89,7 @@ const state = {
90
89
  };
91
90
 
92
91
  // Re-export every real symbol from the embedding-backend module, overriding
93
- // only the two we control. Bun's `mock.module` replacement is process-wide,
92
+ // only the one we control. Bun's `mock.module` replacement is process-wide,
94
93
  // so a partial mock here would break sibling test files that import other
95
94
  // exports from the same module (`selectEmbeddingBackend`, etc.).
96
95
  const realEmbeddingBackend = await import("../../embedding-backend.js");
@@ -104,9 +103,23 @@ mock.module("../../embedding-backend.js", () => ({
104
103
  vectors: state.embedReturn,
105
104
  };
106
105
  },
107
- generateSparseEmbedding: (text: string) => {
106
+ }));
107
+
108
+ // `sim.ts` builds the query-side sparse vector via BM25's
109
+ // `generateBm25QueryEmbedding`. Wrap it to record the call text, then
110
+ // delegate to the real implementation so the resulting sparse vector is
111
+ // well-formed. Capture the function reference *before* registering the
112
+ // mock — ESM live bindings resolve through the namespace at call time, so
113
+ // `realSparseBm25.fn(...)` after `mock.module` would route into the
114
+ // mocked version and recurse.
115
+ const realSparseBm25 = await import("../sparse-bm25.js");
116
+ const realGenerateBm25QueryEmbedding =
117
+ realSparseBm25.generateBm25QueryEmbedding;
118
+ mock.module("../sparse-bm25.js", () => ({
119
+ ...realSparseBm25,
120
+ generateBm25QueryEmbedding: (text: string) => {
108
121
  state.sparseCalls.push(text);
109
- return state.sparseReturn;
122
+ return realGenerateBm25QueryEmbedding(text);
110
123
  },
111
124
  }));
112
125
 
@@ -146,7 +159,8 @@ mock.module("@qdrant/js-client-rest", () => ({
146
159
  QdrantClient: MockQdrantClient,
147
160
  }));
148
161
 
149
- const { simBatch, simSkillBatch, clamp01 } = await import("../sim.js");
162
+ const { simBatch, simSkillBatch, clamp01, effectiveWeights } =
163
+ await import("../sim.js");
150
164
  const { _resetMemoryV2SkillQdrantForTests } =
151
165
  await import("../skill-qdrant.js");
152
166
  const { _resetMemoryV2QdrantForTests } = await import("../qdrant.js");
@@ -159,7 +173,6 @@ function resetState(): void {
159
173
  state.embedCalls.length = 0;
160
174
  state.sparseCalls.length = 0;
161
175
  state.embedReturn = [[0.1, 0.2, 0.3]];
162
- state.sparseReturn = { indices: [1, 2, 3], values: [0.5, 0.5, 0.5] };
163
176
  state.queryResponses.dense.length = 0;
164
177
  state.queryResponses.sparse.length = 0;
165
178
  state.skillQueryResponses.dense.length = 0;
@@ -216,6 +229,120 @@ afterEach(resetState);
216
229
  // clamp01
217
230
  // ---------------------------------------------------------------------------
218
231
 
232
+ // ---------------------------------------------------------------------------
233
+ // effectiveWeights — adaptive sparse weighting
234
+ // ---------------------------------------------------------------------------
235
+
236
+ describe("effectiveWeights", () => {
237
+ // The helper takes a generic config, but only reads
238
+ // `memory.v2.min_sparse_spread` / `full_sparse_spread`. Build a minimal
239
+ // shape so test cases can opt into custom thresholds vs the built-in
240
+ // defaults (0.2 / 0.5).
241
+ function configWithSpreadOverrides(
242
+ min?: number,
243
+ full?: number,
244
+ ): AssistantConfig {
245
+ return {
246
+ memory: { v2: { min_sparse_spread: min, full_sparse_spread: full } },
247
+ } as unknown as AssistantConfig;
248
+ }
249
+ const baseConfig = configWithSpreadOverrides();
250
+
251
+ test("returns base weights when sparse weight is zero", () => {
252
+ const result = effectiveWeights(
253
+ [{ sparseScore: 1 }, { sparseScore: 2 }],
254
+ 2,
255
+ 1.0,
256
+ 0.0,
257
+ baseConfig,
258
+ );
259
+ expect(result.dense).toBe(1.0);
260
+ expect(result.sparse).toBe(0);
261
+ });
262
+
263
+ test("returns base weights when fewer than 2 sparse-bearing hits", () => {
264
+ // Single sparse-bearing hit — spread is undefined.
265
+ const result = effectiveWeights(
266
+ [{ sparseScore: 5 }, {}],
267
+ 5,
268
+ 0.7,
269
+ 0.3,
270
+ baseConfig,
271
+ );
272
+ expect(result.dense).toBeCloseTo(0.7);
273
+ expect(result.sparse).toBeCloseTo(0.3);
274
+ expect(result.spread).toBe(0);
275
+ });
276
+
277
+ test("collapses sparse weight to 0 when spread is below min_sparse_spread", () => {
278
+ // Three hits with sparseNorm = {0.95, 0.97, 1.0} → spread 0.05 < 0.2.
279
+ const hits = [
280
+ { sparseScore: 9.5 },
281
+ { sparseScore: 9.7 },
282
+ { sparseScore: 10 },
283
+ ];
284
+ const result = effectiveWeights(hits, 10, 0.7, 0.3, baseConfig);
285
+ expect(result.spread).toBeCloseTo(0.05, 6);
286
+ expect(result.sparse).toBeCloseTo(0, 6);
287
+ // Dense compensates: gets the full sparse weight added back.
288
+ expect(result.dense).toBeCloseTo(1.0, 6);
289
+ });
290
+
291
+ test("preserves base weights when spread reaches full_sparse_spread", () => {
292
+ // sparseNorm = {0.5, 1.0} → spread 0.5 === default full threshold.
293
+ const hits = [{ sparseScore: 5 }, { sparseScore: 10 }];
294
+ const result = effectiveWeights(hits, 10, 0.7, 0.3, baseConfig);
295
+ expect(result.spread).toBeCloseTo(0.5, 6);
296
+ expect(result.sparse).toBeCloseTo(0.3, 6);
297
+ expect(result.dense).toBeCloseTo(0.7, 6);
298
+ });
299
+
300
+ test("interpolates linearly between min and full thresholds", () => {
301
+ // sparseNorm = {0.65, 1.0} → spread 0.35; midway between 0.2 and 0.5
302
+ // → factor = 0.5; effSparse = 0.5 * 0.3 = 0.15; effDense = 0.7 + 0.15.
303
+ const hits = [{ sparseScore: 6.5 }, { sparseScore: 10 }];
304
+ const result = effectiveWeights(hits, 10, 0.7, 0.3, baseConfig);
305
+ expect(result.spread).toBeCloseTo(0.35, 6);
306
+ expect(result.sparse).toBeCloseTo(0.15, 6);
307
+ expect(result.dense).toBeCloseTo(0.85, 6);
308
+ });
309
+
310
+ test("config overrides min and full thresholds", () => {
311
+ // Custom: min=0.0, full=1.0 — spread is now in [0, 1] linearly.
312
+ const config = configWithSpreadOverrides(0.0, 1.0);
313
+ const hits = [{ sparseScore: 8 }, { sparseScore: 10 }];
314
+ const result = effectiveWeights(hits, 10, 0.7, 0.3, config);
315
+ // spread 0.2; factor = 0.2; effSparse = 0.06.
316
+ expect(result.spread).toBeCloseTo(0.2, 6);
317
+ expect(result.sparse).toBeCloseTo(0.06, 6);
318
+ expect(result.dense).toBeCloseTo(0.94, 6);
319
+ });
320
+
321
+ test("falls back to base weights when full <= min (degenerate config)", () => {
322
+ const config = configWithSpreadOverrides(0.5, 0.3);
323
+ const hits = [{ sparseScore: 5 }, { sparseScore: 10 }];
324
+ const result = effectiveWeights(hits, 10, 0.7, 0.3, config);
325
+ expect(result.sparse).toBeCloseTo(0.3, 6);
326
+ expect(result.dense).toBeCloseTo(0.7, 6);
327
+ });
328
+
329
+ test("dense + sparse always equals baseDense + baseSparse", () => {
330
+ // Property check: total weight is preserved across the spread spectrum
331
+ // so `fused` stays interpretable as a [0, 1] similarity regardless of
332
+ // how aggressively sparse is collapsed.
333
+ const cases = [
334
+ [{ sparseScore: 1 }, { sparseScore: 1.05 }], // tiny spread
335
+ [{ sparseScore: 1 }, { sparseScore: 5 }], // mid spread
336
+ [{ sparseScore: 1 }, { sparseScore: 10 }], // full spread
337
+ ];
338
+ for (const hits of cases) {
339
+ const maxSparse = Math.max(...hits.map((h) => h.sparseScore));
340
+ const result = effectiveWeights(hits, maxSparse, 0.7, 0.3, baseConfig);
341
+ expect(result.dense + result.sparse).toBeCloseTo(1.0, 6);
342
+ }
343
+ });
344
+ });
345
+
219
346
  describe("clamp01", () => {
220
347
  test("passes values already in [0, 1] through unchanged", () => {
221
348
  expect(clamp01(0)).toBe(0);
@@ -247,6 +374,24 @@ describe("simBatch", () => {
247
374
  expect(state.queryCalls).toHaveLength(0);
248
375
  });
249
376
 
377
+ test("empty text returns empty map without touching backends", async () => {
378
+ // Turn 1 has no prior assistant message, so `computeOwnActivation` calls
379
+ // `simBatch("", slugs, config)`. Gemini rejects empty content with HTTP
380
+ // 400 — short-circuit here so the activation pipeline doesn't crash.
381
+ const config = configWithWeights(0.7, 0.3);
382
+
383
+ for (const text of ["", " ", "\n\n"]) {
384
+ state.embedCalls.length = 0;
385
+ state.sparseCalls.length = 0;
386
+ state.queryCalls.length = 0;
387
+ const out = await simBatch(text, ["alice-vscode"], config);
388
+ expect(out.size).toBe(0);
389
+ expect(state.embedCalls).toHaveLength(0);
390
+ expect(state.sparseCalls).toHaveLength(0);
391
+ expect(state.queryCalls).toHaveLength(0);
392
+ }
393
+ });
394
+
250
395
  test("identical text yields ~1.0 when both channels max out", async () => {
251
396
  const config = configWithWeights(0.7, 0.3);
252
397
  stageHybridResponse([
@@ -425,6 +570,21 @@ describe("simSkillBatch", () => {
425
570
  expect(state.queryCalls).toHaveLength(0);
426
571
  });
427
572
 
573
+ test("empty text returns empty map without touching backends", async () => {
574
+ const config = configWithWeights(0.7, 0.3);
575
+
576
+ for (const text of ["", " ", "\n\n"]) {
577
+ state.embedCalls.length = 0;
578
+ state.sparseCalls.length = 0;
579
+ state.queryCalls.length = 0;
580
+ const out = await simSkillBatch(text, ["example-skill-a"], config);
581
+ expect(out.size).toBe(0);
582
+ expect(state.embedCalls).toHaveLength(0);
583
+ expect(state.sparseCalls).toHaveLength(0);
584
+ expect(state.queryCalls).toHaveLength(0);
585
+ }
586
+ });
587
+
428
588
  test("queries the dedicated skills collection and forwards an id-IN filter", async () => {
429
589
  const config = configWithWeights(0.7, 0.3);
430
590
  stageSkillHybridResponse([]);
@@ -0,0 +1,292 @@
1
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
5
+
6
+ import {
7
+ SPARSE_VOCAB_SIZE,
8
+ tokenHash,
9
+ tokenizeStemmed,
10
+ } from "../../sparse-tokenize.js";
11
+ import {
12
+ _resetCorpusStatsForTests,
13
+ _setCorpusStatsForTests,
14
+ type Bm25Params,
15
+ type CorpusStats,
16
+ generateBm25DocEmbedding,
17
+ generateBm25QueryEmbedding,
18
+ getConceptPageCorpusStats,
19
+ rebuildConceptPageCorpusStats,
20
+ } from "../sparse-bm25.js";
21
+
22
+ const PARAMS: Bm25Params = { k1: 1.2, b: 0.75 };
23
+
24
+ /**
25
+ * Resolve the hashed bucket where `word` lands after the production
26
+ * tokenize+stem pipeline. Mirrors what `generateBm25DocEmbedding` /
27
+ * `generateBm25QueryEmbedding` do internally so fixtures stay in lockstep
28
+ * with the encoder.
29
+ */
30
+ function stemmedBucket(word: string): number {
31
+ const tokens = tokenizeStemmed(word);
32
+ if (tokens.length !== 1) {
33
+ throw new Error(
34
+ `stemmedBucket expects a single-token input; got ${tokens.length} for "${word}"`,
35
+ );
36
+ }
37
+ return tokenHash(tokens[0], SPARSE_VOCAB_SIZE);
38
+ }
39
+
40
+ /** Sum of v_q · v_d across two sparse vectors — BM25 score under the design. */
41
+ function dotProduct(
42
+ q: { indices: number[]; values: number[] },
43
+ d: { indices: number[]; values: number[] },
44
+ ): number {
45
+ const dMap = new Map<number, number>();
46
+ for (let i = 0; i < d.indices.length; i++)
47
+ dMap.set(d.indices[i], d.values[i]);
48
+ let sum = 0;
49
+ for (let i = 0; i < q.indices.length; i++) {
50
+ const dv = dMap.get(q.indices[i]);
51
+ if (dv !== undefined) sum += q.values[i] * dv;
52
+ }
53
+ return sum;
54
+ }
55
+
56
+ function makeWorkspace(pages: Record<string, string>): string {
57
+ const dir = mkdtempSync(join(tmpdir(), "bm25-"));
58
+ const conceptsDir = join(dir, "memory", "concepts");
59
+ mkdirSync(conceptsDir, { recursive: true });
60
+ for (const [slug, body] of Object.entries(pages)) {
61
+ const slugPath = join(conceptsDir, `${slug}.md`);
62
+ mkdirSync(join(slugPath, ".."), { recursive: true });
63
+ writeFileSync(slugPath, body, "utf-8");
64
+ }
65
+ return dir;
66
+ }
67
+
68
+ describe("rebuildConceptPageCorpusStats", () => {
69
+ beforeEach(() => {
70
+ _resetCorpusStatsForTests();
71
+ });
72
+ afterEach(() => {
73
+ _resetCorpusStatsForTests();
74
+ });
75
+
76
+ test("empty workspace produces empty stats with totalDocs=0", async () => {
77
+ const dir = makeWorkspace({});
78
+ try {
79
+ await rebuildConceptPageCorpusStats(dir);
80
+ const stats = getConceptPageCorpusStats();
81
+ expect(stats).not.toBeNull();
82
+ expect(stats!.totalDocs).toBe(0);
83
+ expect(stats!.df.size).toBe(0);
84
+ expect(stats!.avgDl).toBe(0);
85
+ } finally {
86
+ rmSync(dir, { recursive: true, force: true });
87
+ }
88
+ });
89
+
90
+ test("3-doc corpus yields correct DF counts and avgDl", async () => {
91
+ const dir = makeWorkspace({
92
+ a: "supplements zinc magnesium",
93
+ b: "zinc magnesium iron",
94
+ c: "supplements iron",
95
+ });
96
+ try {
97
+ await rebuildConceptPageCorpusStats(dir);
98
+ const stats = getConceptPageCorpusStats();
99
+ expect(stats).not.toBeNull();
100
+ expect(stats!.totalDocs).toBe(3);
101
+ // avgDl = (3 + 3 + 2) / 3
102
+ expect(stats!.avgDl).toBeCloseTo(8 / 3, 6);
103
+ // supplements appears in 2 docs, zinc in 2, magnesium in 2, iron in 2
104
+ expect(stats!.df.get(stemmedBucket("supplements"))).toBe(2);
105
+ expect(stats!.df.get(stemmedBucket("zinc"))).toBe(2);
106
+ expect(stats!.df.get(stemmedBucket("magnesium"))).toBe(2);
107
+ expect(stats!.df.get(stemmedBucket("iron"))).toBe(2);
108
+ } finally {
109
+ rmSync(dir, { recursive: true, force: true });
110
+ }
111
+ });
112
+
113
+ test("strips YAML frontmatter from page body before tokenizing", async () => {
114
+ const body = "---\ntitle: foo\nedges: []\n---\nactual prose content";
115
+ const dir = makeWorkspace({ a: body });
116
+ try {
117
+ await rebuildConceptPageCorpusStats(dir);
118
+ const stats = getConceptPageCorpusStats();
119
+ expect(stats).not.toBeNull();
120
+ // "actual prose content" → 3 tokens, so avg_dl should be 3, not 8.
121
+ expect(stats!.avgDl).toBe(3);
122
+ // "title", "edges" should not be in DF (frontmatter stripped).
123
+ expect(stats!.df.get(stemmedBucket("title"))).toBeUndefined();
124
+ expect(stats!.df.get(stemmedBucket("prose"))).toBe(1);
125
+ } finally {
126
+ rmSync(dir, { recursive: true, force: true });
127
+ }
128
+ });
129
+ });
130
+
131
+ describe("generateBm25DocEmbedding", () => {
132
+ test("token in every document gets IDF=0 and is omitted from the vector", () => {
133
+ // 4 docs, "the" appears in all 4
134
+ const theBucket = stemmedBucket("the");
135
+ const stats: CorpusStats = {
136
+ totalDocs: 4,
137
+ df: new Map([[theBucket, 4]]),
138
+ avgDl: 5,
139
+ builtAt: 0,
140
+ };
141
+ const vec = generateBm25DocEmbedding("the the the", stats, PARAMS);
142
+ // The token "the" has IDF = log((4 - 4 + 0.5)/(4 + 0.5) + 1) = log(0.5/4.5 + 1)
143
+ // ≈ log(1.111) ≈ 0.105 — non-zero. The "in every doc → IDF=0" claim only
144
+ // holds for the simpler IDF variant; with Lucene's +1 we get a small
145
+ // positive value. Confirm the value is small but present.
146
+ expect(vec.indices.length).toBe(1);
147
+ expect(vec.values[0]).toBeGreaterThan(0);
148
+ expect(vec.values[0]).toBeLessThan(0.5);
149
+ });
150
+
151
+ test("rare token gets high IDF weight", () => {
152
+ // 100 docs, "supplements" in 1 doc, "the" in 100.
153
+ const supplementsBucket = stemmedBucket("supplements");
154
+ const theBucket = stemmedBucket("the");
155
+ const stats: CorpusStats = {
156
+ totalDocs: 100,
157
+ df: new Map([
158
+ [supplementsBucket, 1],
159
+ [theBucket, 100],
160
+ ]),
161
+ avgDl: 10,
162
+ builtAt: 0,
163
+ };
164
+ const vec = generateBm25DocEmbedding("the supplements the", stats, PARAMS);
165
+ const indexMap = new Map<number, number>();
166
+ for (let i = 0; i < vec.indices.length; i++) {
167
+ indexMap.set(vec.indices[i], vec.values[i]);
168
+ }
169
+ // supplements weight should massively exceed the weight.
170
+ const supplementsWeight = indexMap.get(supplementsBucket) ?? 0;
171
+ const theWeight = indexMap.get(theBucket) ?? 0;
172
+ expect(supplementsWeight).toBeGreaterThan(theWeight * 10);
173
+ });
174
+
175
+ test("TF saturation: tf=10 score is nowhere near 10x of tf=1", () => {
176
+ const supplementsBucket = stemmedBucket("supplements");
177
+ const stats: CorpusStats = {
178
+ totalDocs: 100,
179
+ df: new Map([[supplementsBucket, 1]]),
180
+ // Set avg_dl equal to doc length → length factor = 1.
181
+ avgDl: 1,
182
+ builtAt: 0,
183
+ };
184
+ // tf=1 doc and tf=10 doc, both length-1-equivalent thanks to avg_dl above.
185
+ // (We need real strings of the same length to get b=0.75 to behave; for
186
+ // this test we use single-token inputs and avg_dl matched to the input
187
+ // length to isolate the TF saturation effect.)
188
+ const vec1 = generateBm25DocEmbedding(
189
+ "supplements",
190
+ { ...stats, avgDl: 1 },
191
+ PARAMS,
192
+ );
193
+ const vec10 = generateBm25DocEmbedding(
194
+ "supplements supplements supplements supplements supplements " +
195
+ "supplements supplements supplements supplements supplements",
196
+ { ...stats, avgDl: 10 },
197
+ PARAMS,
198
+ );
199
+ const w1 = vec1.values[0];
200
+ const w10 = vec10.values[0];
201
+ // Under k1=1.2 and length-normalized inputs, TF saturation caps the
202
+ // ratio near (k1+1)/k1 ≈ 1.83. Ratio must be far below 10.
203
+ expect(w10 / w1).toBeGreaterThan(1.5);
204
+ expect(w10 / w1).toBeLessThan(2.0);
205
+ });
206
+
207
+ test("length normalization: short doc with one match scores higher than long doc with one match", () => {
208
+ const supplementsBucket = stemmedBucket("supplements");
209
+ const stats: CorpusStats = {
210
+ totalDocs: 100,
211
+ df: new Map([[supplementsBucket, 1]]),
212
+ avgDl: 5,
213
+ builtAt: 0,
214
+ };
215
+ const shortDoc = generateBm25DocEmbedding(
216
+ "supplements zinc",
217
+ stats,
218
+ PARAMS,
219
+ );
220
+ const longDoc = generateBm25DocEmbedding(
221
+ "supplements " +
222
+ "the the the the the the the the the the the the the the the",
223
+ stats,
224
+ PARAMS,
225
+ );
226
+ const queryVec = generateBm25QueryEmbedding("supplements");
227
+ const shortScore = dotProduct(queryVec, shortDoc);
228
+ const longScore = dotProduct(queryVec, longDoc);
229
+ expect(shortScore).toBeGreaterThan(longScore);
230
+ });
231
+ });
232
+
233
+ describe("generateBm25QueryEmbedding", () => {
234
+ test("emits binary occurrence per distinct token", () => {
235
+ const vec = generateBm25QueryEmbedding("supplements supplements zinc");
236
+ expect(vec.indices.length).toBe(2);
237
+ for (const v of vec.values) expect(v).toBe(1);
238
+ });
239
+
240
+ test("empty input yields empty vector", () => {
241
+ const vec = generateBm25QueryEmbedding("");
242
+ expect(vec.indices.length).toBe(0);
243
+ expect(vec.values.length).toBe(0);
244
+ });
245
+ });
246
+
247
+ describe("end-to-end ranking: BM25 fixes the supplements bug", () => {
248
+ test("BM25 ranks topical doc above narrative doc; pure TF does not", () => {
249
+ // Doc A: short focused page about supplements
250
+ // Doc B: long narrative repeating "I am" with one supplements mention
251
+ const docA = "I am taking magnesium and zinc as supplements";
252
+ const docB =
253
+ "I am tired I am sad I am alone I am bored I am happy I am " +
254
+ "tired again I am sad again I am alone again I am bored again " +
255
+ "supplements";
256
+ const query = "supplements am";
257
+
258
+ // Build stats from these 2 docs.
259
+ const tokensA = tokenizeStemmed(docA);
260
+ const tokensB = tokenizeStemmed(docB);
261
+ const df = new Map<number, number>();
262
+ for (const tokens of [tokensA, tokensB]) {
263
+ const seen = new Set<number>();
264
+ for (const t of tokens) {
265
+ const idx = tokenHash(t, SPARSE_VOCAB_SIZE);
266
+ if (seen.has(idx)) continue;
267
+ seen.add(idx);
268
+ df.set(idx, (df.get(idx) ?? 0) + 1);
269
+ }
270
+ }
271
+ const stats: CorpusStats = {
272
+ totalDocs: 2,
273
+ df,
274
+ avgDl: (tokensA.length + tokensB.length) / 2,
275
+ builtAt: 0,
276
+ };
277
+ _setCorpusStatsForTests(stats);
278
+
279
+ const docVecA = generateBm25DocEmbedding(docA, stats, PARAMS);
280
+ const docVecB = generateBm25DocEmbedding(docB, stats, PARAMS);
281
+ const queryVec = generateBm25QueryEmbedding(query);
282
+
283
+ const scoreA = dotProduct(queryVec, docVecA);
284
+ const scoreB = dotProduct(queryVec, docVecB);
285
+
286
+ // BM25 should rank A above B — the supplement-focused short doc wins
287
+ // over the long personal-narrative doc with one supplement mention.
288
+ expect(scoreA).toBeGreaterThan(scoreB);
289
+
290
+ _resetCorpusStatsForTests();
291
+ });
292
+ });
@@ -41,7 +41,6 @@ mock.module("../../../config/loader.js", () => ({
41
41
  memory: { v2: { enabled: configMemoryV2Enabled } },
42
42
  }),
43
43
  loadRawConfig: () => ({}),
44
- saveConfig: () => {},
45
44
  saveRawConfig: () => {},
46
45
  invalidateConfigCache: () => {},
47
46
  getNestedValue: () => undefined,