@vellumai/assistant 0.7.2 → 0.7.3

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 (347) hide show
  1. package/ARCHITECTURE.md +16 -1
  2. package/docs/architecture/memory.md +5 -2
  3. package/node_modules/@vellumai/gateway-client/src/ipc-client.ts +13 -4
  4. package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +0 -9
  5. package/node_modules/@vellumai/slack-text/src/index.test.ts +18 -35
  6. package/node_modules/@vellumai/slack-text/src/index.ts +2 -48
  7. package/openapi.yaml +449 -22
  8. package/package.json +1 -1
  9. package/src/__tests__/app-control-flow.test.ts +21 -11
  10. package/src/__tests__/assistant-event-hub.test.ts +48 -0
  11. package/src/__tests__/assistant-event.test.ts +0 -10
  12. package/src/__tests__/assistant-events-sse-hardening.test.ts +2 -7
  13. package/src/__tests__/assistant-feature-flags-integration.test.ts +18 -0
  14. package/src/__tests__/auto-analysis-end-to-end.test.ts +62 -1
  15. package/src/__tests__/background-workers-disk-pressure.test.ts +268 -0
  16. package/src/__tests__/call-conversation-messages.test.ts +8 -2
  17. package/src/__tests__/channel-inbound-disk-pressure.test.ts +537 -0
  18. package/src/__tests__/channel-readiness-service.test.ts +4 -2
  19. package/src/__tests__/config-loader-backfill.test.ts +379 -0
  20. package/src/__tests__/config-schema.test.ts +1 -0
  21. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +18 -9
  22. package/src/__tests__/config-watcher.test.ts +140 -69
  23. package/src/__tests__/context-search-agent-runner.test.ts +61 -3
  24. package/src/__tests__/context-search-conversations-source.test.ts +0 -24
  25. package/src/__tests__/context-search-fanout.test.ts +0 -1
  26. package/src/__tests__/context-search-memory-source.test.ts +3 -7
  27. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -2
  28. package/src/__tests__/context-search-pkb-source.test.ts +0 -1
  29. package/src/__tests__/context-search-workspace-source.test.ts +0 -1
  30. package/src/__tests__/conversation-abort-tool-results.test.ts +6 -0
  31. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +223 -0
  32. package/src/__tests__/conversation-agent-loop.test.ts +454 -5
  33. package/src/__tests__/conversation-error.test.ts +150 -3
  34. package/src/__tests__/conversation-process-callsite.test.ts +43 -0
  35. package/src/__tests__/conversation-provider-retry-repair.test.ts +6 -0
  36. package/src/__tests__/conversation-runtime-assembly.test.ts +65 -0
  37. package/src/__tests__/conversation-slash-unknown.test.ts +6 -0
  38. package/src/__tests__/conversation-speed-override.test.ts +0 -3
  39. package/src/__tests__/conversation-store.test.ts +0 -18
  40. package/src/__tests__/conversation-surfaces-app-control.test.ts +15 -4
  41. package/src/__tests__/conversation-surfaces-data-persist.test.ts +404 -0
  42. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +2 -5
  43. package/src/__tests__/conversation-workspace-injection.test.ts +6 -0
  44. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +6 -0
  45. package/src/__tests__/credentials-cli.test.ts +7 -0
  46. package/src/__tests__/cu-unified-flow.test.ts +176 -10
  47. package/src/__tests__/date-context.test.ts +164 -2
  48. package/src/__tests__/disk-pressure-guard.test.ts +262 -0
  49. package/src/__tests__/disk-pressure-lifecycle.test.ts +168 -0
  50. package/src/__tests__/disk-pressure-policy.test.ts +241 -0
  51. package/src/__tests__/disk-pressure-routes.test.ts +379 -0
  52. package/src/__tests__/disk-pressure-tools.test.ts +277 -0
  53. package/src/__tests__/disk-usage.test.ts +150 -0
  54. package/src/__tests__/events-client-registration.test.ts +52 -0
  55. package/src/__tests__/events-dev-bypass-actor.test.ts +162 -0
  56. package/src/__tests__/file-write-tool.test.ts +4 -10
  57. package/src/__tests__/filing-service.test.ts +3 -4
  58. package/src/__tests__/heartbeat-disk-pressure.test.ts +183 -0
  59. package/src/__tests__/heartbeat-service.test.ts +260 -11
  60. package/src/__tests__/host-app-control-proxy.test.ts +195 -25
  61. package/src/__tests__/host-bash-proxy.test.ts +227 -34
  62. package/src/__tests__/host-bash-routes.test.ts +178 -13
  63. package/src/__tests__/host-cu-proxy.test.ts +210 -3
  64. package/src/__tests__/host-cu-routes-targeted.test.ts +141 -12
  65. package/src/__tests__/host-file-proxy-targeted.test.ts +48 -9
  66. package/src/__tests__/host-file-proxy.test.ts +268 -6
  67. package/src/__tests__/host-file-routes-targeted.test.ts +175 -17
  68. package/src/__tests__/host-transfer-proxy-targeted.test.ts +408 -59
  69. package/src/__tests__/host-transfer-routes-targeted.test.ts +232 -17
  70. package/src/__tests__/http-user-message-parity.test.ts +107 -1
  71. package/src/__tests__/injector-chain.test.ts +18 -6
  72. package/src/__tests__/injector-disk-pressure.test.ts +224 -0
  73. package/src/__tests__/managed-profile-guard.test.ts +18 -0
  74. package/src/__tests__/mcp-abort-signal.test.ts +130 -0
  75. package/src/__tests__/memory-admin-recall.test.ts +3 -11
  76. package/src/__tests__/memory-retrieval-pipeline.test.ts +22 -1
  77. package/src/__tests__/normalize-onboarding.test.ts +180 -0
  78. package/src/__tests__/oauth-connect-routes.test.ts +316 -0
  79. package/src/__tests__/oauth-provider-seed-logos.test.ts +24 -2
  80. package/src/__tests__/onboarding-persona-write.test.ts +308 -0
  81. package/src/__tests__/openai-provider.test.ts +45 -8
  82. package/src/__tests__/persist-onboarding-artifacts.test.ts +44 -64
  83. package/src/__tests__/platform-callback-registration.test.ts +21 -4
  84. package/src/__tests__/platform.test.ts +2 -1
  85. package/src/__tests__/playbook-execution.test.ts +0 -43
  86. package/src/__tests__/plugin-tool-contribution.test.ts +47 -0
  87. package/src/__tests__/prechat-onboarding-contract.test.ts +214 -27
  88. package/src/__tests__/provider-tool-name.test.ts +23 -0
  89. package/src/__tests__/relay-server.test.ts +15 -4
  90. package/src/__tests__/runtime-events-sse.test.ts +4 -8
  91. package/src/__tests__/scheduler-disk-pressure.test.ts +148 -0
  92. package/src/__tests__/secret-ingress-http.test.ts +0 -1
  93. package/src/__tests__/suggestion-routes.test.ts +46 -0
  94. package/src/__tests__/twilio-validation.test.ts +2 -2
  95. package/src/__tests__/workspace-migration-065-bump-stale-heartbeat-interval.test.ts +122 -0
  96. package/src/__tests__/workspace-migration-066-seed-heartbeat-callsite-cost-default.test.ts +285 -0
  97. package/src/__tests__/workspace-migration-068-release-notes-local-timezone.test.ts +90 -0
  98. package/src/__tests__/workspace-migration-safe-storage-limits-release.test.ts +90 -0
  99. package/src/approvals/guardian-decision-primitive.ts +13 -0
  100. package/src/approvals/guardian-request-resolvers.ts +16 -17
  101. package/src/backup/snapshot-lock.ts +2 -27
  102. package/src/bundler/compiler-tools.ts +3 -2
  103. package/src/calls/call-conversation-messages.ts +46 -10
  104. package/src/cli/commands/__tests__/webhooks.test.ts +0 -4
  105. package/src/cli/commands/bash.ts +35 -108
  106. package/src/cli/commands/contacts.ts +64 -25
  107. package/src/cli/commands/credentials.ts +56 -0
  108. package/src/cli/commands/memory-v2.ts +7 -6
  109. package/src/cli/commands/oauth/__tests__/connect.test.ts +437 -1
  110. package/src/cli/commands/oauth/connect.ts +127 -1
  111. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -3
  112. package/src/cli/commands/platform/__tests__/connect.test.ts +7 -1
  113. package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -1
  114. package/src/cli/commands/platform/__tests__/status.test.ts +103 -6
  115. package/src/cli/commands/platform/index.ts +16 -7
  116. package/src/cli/commands/status.ts +57 -0
  117. package/src/cli/program.ts +4 -2
  118. package/src/config/assistant-feature-flags.ts +13 -3
  119. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +4 -3
  120. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +13 -7
  121. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +2 -2
  122. package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +2 -2
  123. package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +2 -2
  124. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -2
  125. package/src/config/env.ts +0 -8
  126. package/src/config/feature-flag-registry.json +27 -3
  127. package/src/config/loader.ts +127 -8
  128. package/src/config/schemas/__tests__/memory-v2.test.ts +10 -5
  129. package/src/config/schemas/call-site-catalog.ts +14 -0
  130. package/src/config/schemas/channels.ts +0 -5
  131. package/src/config/schemas/heartbeat.ts +1 -1
  132. package/src/config/schemas/llm.ts +2 -0
  133. package/src/config/schemas/memory-lifecycle.ts +13 -0
  134. package/src/config/schemas/memory-v2.ts +75 -11
  135. package/src/config/schemas/platform.ts +43 -3
  136. package/src/config/schemas/services.ts +28 -0
  137. package/src/config/seed-inference-profiles.ts +230 -33
  138. package/src/contacts/contact-store.ts +0 -25
  139. package/src/daemon/__tests__/conversation-tool-setup.test.ts +86 -25
  140. package/src/daemon/assistant-attachments.ts +4 -4
  141. package/src/daemon/config-watcher.ts +85 -57
  142. package/src/daemon/conversation-agent-loop-handlers.ts +6 -0
  143. package/src/daemon/conversation-agent-loop.ts +170 -33
  144. package/src/daemon/conversation-error.ts +87 -15
  145. package/src/daemon/conversation-lifecycle.ts +1 -3
  146. package/src/daemon/conversation-process.ts +8 -0
  147. package/src/daemon/conversation-runtime-assembly.ts +26 -0
  148. package/src/daemon/conversation-store.ts +2 -2
  149. package/src/daemon/conversation-surfaces.ts +195 -15
  150. package/src/daemon/conversation-tool-setup.ts +57 -14
  151. package/src/daemon/conversation.ts +17 -22
  152. package/src/daemon/date-context.ts +71 -22
  153. package/src/daemon/disk-pressure-background-gate.ts +73 -0
  154. package/src/daemon/disk-pressure-guard.ts +343 -0
  155. package/src/daemon/disk-pressure-policy.ts +163 -0
  156. package/src/daemon/handlers/shared.ts +0 -1
  157. package/src/daemon/handlers/skills.ts +3 -4
  158. package/src/daemon/host-app-control-proxy.ts +137 -41
  159. package/src/daemon/host-bash-proxy.ts +46 -21
  160. package/src/daemon/host-cu-proxy.ts +49 -3
  161. package/src/daemon/host-file-proxy.ts +43 -7
  162. package/src/daemon/host-transfer-proxy.ts +95 -4
  163. package/src/daemon/lifecycle.ts +79 -28
  164. package/src/daemon/meet-host-supervisor.ts +4 -4
  165. package/src/daemon/meet-manifest-loader.ts +0 -1
  166. package/src/daemon/memory-v2-startup.ts +14 -4
  167. package/src/daemon/message-protocol.ts +3 -0
  168. package/src/daemon/message-types/conversations.ts +4 -0
  169. package/src/daemon/message-types/disk-pressure.ts +9 -0
  170. package/src/daemon/message-types/messages.ts +3 -0
  171. package/src/daemon/profiler-run-store.ts +5 -5
  172. package/src/daemon/tool-setup-types.ts +2 -2
  173. package/src/documents/document-store.ts +85 -0
  174. package/src/filing/filing-service.ts +30 -5
  175. package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +9 -16
  176. package/src/heartbeat/__tests__/heartbeat-run-store.test.ts +36 -0
  177. package/src/heartbeat/heartbeat-run-store.ts +13 -0
  178. package/src/heartbeat/heartbeat-service.ts +205 -31
  179. package/src/home/feed-scheduler.ts +18 -0
  180. package/src/inbound/platform-callback-registration.ts +8 -15
  181. package/src/ipc/__tests__/clients-list-ipc.test.ts +169 -0
  182. package/src/ipc/assistant-server.ts +56 -2
  183. package/src/ipc/gateway-client.ts +37 -3
  184. package/src/live-voice/live-voice-archive.ts +4 -4
  185. package/src/live-voice/protocol.ts +5 -7
  186. package/src/media/image-service.ts +1 -7
  187. package/src/memory/__tests__/fixtures/memory-v2-activation-fixtures.ts +21 -13
  188. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +52 -22
  189. package/src/memory/__tests__/memory-v2-activation-log-store.test.ts +0 -6
  190. package/src/memory/__tests__/memory-v2-concept-frequency.test.ts +272 -0
  191. package/src/memory/admin.ts +5 -9
  192. package/src/memory/context-search/agent-runner.ts +19 -2
  193. package/src/memory/context-search/sources/conversations.ts +2 -11
  194. package/src/memory/context-search/sources/memory-v2.ts +5 -4
  195. package/src/memory/context-search/sources/memory.ts +0 -1
  196. package/src/memory/context-search/types.ts +0 -1
  197. package/src/memory/conversation-crud.ts +4 -12
  198. package/src/memory/db-init.ts +2 -0
  199. package/src/memory/embedding-runtime-manager.ts +119 -5
  200. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +32 -21
  201. package/src/memory/graph/conversation-graph-memory.ts +42 -54
  202. package/src/memory/graph/extraction.ts +1 -3
  203. package/src/memory/graph/graph-search.test.ts +10 -67
  204. package/src/memory/graph/graph-search.ts +1 -20
  205. package/src/memory/graph/retriever.test.ts +6 -0
  206. package/src/memory/graph/retriever.ts +6 -10
  207. package/src/memory/indexer.ts +54 -45
  208. package/src/memory/job-handlers/backfill.ts +2 -11
  209. package/src/memory/job-handlers/cleanup.ts +43 -0
  210. package/src/memory/job-handlers/embedding.ts +6 -8
  211. package/src/memory/job-handlers/summarization.ts +2 -7
  212. package/src/memory/jobs-store.ts +48 -0
  213. package/src/memory/jobs-worker.ts +81 -43
  214. package/src/memory/memory-v2-activation-log-store.ts +32 -14
  215. package/src/memory/memory-v2-concept-frequency.ts +169 -0
  216. package/src/memory/migrations/239-trace-events-created-at-index.ts +18 -0
  217. package/src/memory/migrations/index.ts +1 -0
  218. package/src/memory/pkb/pkb-search.test.ts +6 -0
  219. package/src/memory/qdrant-client.ts +0 -13
  220. package/src/memory/rerank-local.ts +374 -0
  221. package/src/memory/search/semantic.ts +6 -67
  222. package/src/memory/trace-event-store.ts +1 -17
  223. package/src/memory/v2/__tests__/activation.test.ts +311 -250
  224. package/src/memory/v2/__tests__/consolidation-job.test.ts +40 -8
  225. package/src/memory/v2/__tests__/injection.test.ts +157 -167
  226. package/src/memory/v2/__tests__/prompts-consolidation.test.ts +61 -2
  227. package/src/memory/v2/__tests__/qdrant.test.ts +16 -0
  228. package/src/memory/v2/__tests__/reranker.test.ts +338 -0
  229. package/src/memory/v2/__tests__/sim.test.ts +5 -199
  230. package/src/memory/v2/__tests__/skill-store.test.ts +71 -65
  231. package/src/memory/v2/__tests__/static-context.test.ts +76 -1
  232. package/src/memory/v2/activation.ts +149 -156
  233. package/src/memory/v2/consolidation-job.ts +62 -12
  234. package/src/memory/v2/injection.ts +47 -60
  235. package/src/memory/v2/prompts/consolidation.ts +36 -1
  236. package/src/memory/v2/qdrant.ts +99 -0
  237. package/src/memory/v2/reranker.ts +177 -0
  238. package/src/memory/v2/sim.ts +10 -84
  239. package/src/memory/v2/skill-content.ts +4 -3
  240. package/src/memory/v2/skill-store.ts +82 -59
  241. package/src/memory/v2/static-context.ts +22 -0
  242. package/src/memory/v2/types.ts +10 -10
  243. package/src/notifications/copy-composer.ts +13 -0
  244. package/src/notifications/signal.ts +4 -0
  245. package/src/oauth/AGENTS.md +3 -1
  246. package/src/oauth/__tests__/oauth-connect-state.test.ts +137 -0
  247. package/src/oauth/connect-orchestrator.ts +2 -0
  248. package/src/oauth/connection-resolver.test.ts +66 -1
  249. package/src/oauth/connection-resolver.ts +55 -1
  250. package/src/oauth/oauth-connect-state.ts +77 -0
  251. package/src/oauth/seed-providers.ts +58 -1
  252. package/src/plugins/defaults/injectors.ts +35 -2
  253. package/src/plugins/defaults/memory-retrieval.ts +5 -6
  254. package/src/plugins/types.ts +7 -0
  255. package/src/proactive-artifact/aux-message-injector.ts +74 -0
  256. package/src/proactive-artifact/decision.test.ts +226 -0
  257. package/src/proactive-artifact/decision.ts +165 -0
  258. package/src/proactive-artifact/index.ts +7 -0
  259. package/src/proactive-artifact/job.test.ts +867 -0
  260. package/src/proactive-artifact/job.ts +352 -0
  261. package/src/proactive-artifact/message-copy.ts +41 -0
  262. package/src/proactive-artifact/trigger-state.test.ts +277 -0
  263. package/src/proactive-artifact/trigger-state.ts +119 -0
  264. package/src/prompts/normalize-onboarding.ts +80 -0
  265. package/src/prompts/persona-resolver.ts +101 -9
  266. package/src/prompts/system-prompt.ts +21 -7
  267. package/src/prompts/templates/BOOTSTRAP.md +13 -5
  268. package/src/providers/__tests__/retry-callsite.test.ts +222 -1
  269. package/src/providers/model-intents.ts +7 -0
  270. package/src/providers/openrouter/client.ts +8 -0
  271. package/src/providers/retry.ts +50 -0
  272. package/src/providers/types.ts +1 -0
  273. package/src/runtime/__tests__/agent-wake.test.ts +456 -3
  274. package/src/runtime/agent-wake.ts +238 -100
  275. package/src/runtime/assistant-event-hub.ts +36 -6
  276. package/src/runtime/assistant-event.ts +0 -1
  277. package/src/runtime/auth/__tests__/route-policy.test.ts +64 -0
  278. package/src/runtime/auth/route-policy.ts +14 -1
  279. package/src/runtime/auth/same-actor.ts +216 -0
  280. package/src/runtime/channel-retry-sweep.ts +65 -1
  281. package/src/runtime/guardian-reply-router.ts +10 -0
  282. package/src/runtime/local-actor-identity.ts +52 -11
  283. package/src/runtime/pending-interactions.ts +8 -0
  284. package/src/runtime/routes/__tests__/client-routes.test.ts +155 -0
  285. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +0 -5
  286. package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +1 -1
  287. package/src/runtime/routes/client-routes.ts +20 -2
  288. package/src/runtime/routes/contact-routes.ts +0 -25
  289. package/src/runtime/routes/conversation-routes.ts +35 -26
  290. package/src/runtime/routes/debug-bash-routes.ts +163 -0
  291. package/src/runtime/routes/disk-pressure-routes.ts +121 -0
  292. package/src/runtime/routes/document-pdf-renderer.ts +6 -2
  293. package/src/runtime/routes/documents-routes.ts +2 -75
  294. package/src/runtime/routes/events-routes.ts +41 -9
  295. package/src/runtime/routes/host-bash-routes.ts +23 -3
  296. package/src/runtime/routes/host-cu-routes.ts +33 -6
  297. package/src/runtime/routes/host-file-routes.ts +32 -6
  298. package/src/runtime/routes/host-transfer-routes.ts +79 -16
  299. package/src/runtime/routes/identity-routes.ts +7 -138
  300. package/src/runtime/routes/inbound-message-handler.ts +77 -12
  301. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +3 -0
  302. package/src/runtime/routes/index.ts +6 -0
  303. package/src/runtime/routes/memory-item-routes.test.ts +41 -15
  304. package/src/runtime/routes/memory-v2-routes.ts +33 -0
  305. package/src/runtime/routes/oauth-connect-routes.ts +153 -0
  306. package/src/runtime/verification-outbound-actions.ts +4 -4
  307. package/src/schedule/run-script.ts +37 -5
  308. package/src/schedule/scheduler.ts +20 -1
  309. package/src/security/encrypted-store.ts +2 -0
  310. package/src/security/secure-keys.ts +55 -0
  311. package/src/skills/remote-skill-policy.ts +4 -10
  312. package/src/subagent/index.ts +1 -7
  313. package/src/subagent/manager.ts +1 -15
  314. package/src/tasks/task-runner.ts +0 -1
  315. package/src/tasks/task-store.ts +0 -3
  316. package/src/tools/background-tool-registry.ts +17 -3
  317. package/src/tools/host-filesystem/edit.test.ts +151 -0
  318. package/src/tools/host-filesystem/edit.ts +43 -1
  319. package/src/tools/host-filesystem/read.test.ts +129 -0
  320. package/src/tools/host-filesystem/read.ts +43 -1
  321. package/src/tools/host-filesystem/transfer.test.ts +127 -2
  322. package/src/tools/host-filesystem/transfer.ts +56 -11
  323. package/src/tools/host-filesystem/write.test.ts +134 -0
  324. package/src/tools/host-filesystem/write.ts +43 -1
  325. package/src/tools/host-terminal/host-shell.ts +13 -6
  326. package/src/tools/mcp/mcp-tool-factory.ts +2 -1
  327. package/src/tools/memory/register.test.ts +12 -9
  328. package/src/tools/memory/register.ts +1 -2
  329. package/src/tools/provider-tool-name.ts +28 -0
  330. package/src/tools/registry.ts +30 -9
  331. package/src/tools/terminal/shell.ts +9 -1
  332. package/src/tools/tool-approval-handler.ts +31 -6
  333. package/src/tools/types.ts +24 -2
  334. package/src/tts/provider-catalog.ts +3 -5
  335. package/src/util/disk-usage.ts +138 -0
  336. package/src/util/platform.ts +21 -11
  337. package/src/util/process-liveness.ts +26 -0
  338. package/src/workspace/heartbeat-service.ts +19 -0
  339. package/src/workspace/migrations/065-bump-stale-heartbeat-interval.ts +60 -0
  340. package/src/workspace/migrations/066-seed-heartbeat-callsite-cost-default.ts +146 -0
  341. package/src/workspace/migrations/067-release-notes-safe-storage-limits.ts +72 -0
  342. package/src/workspace/migrations/068-release-notes-local-timezone.ts +65 -0
  343. package/src/workspace/migrations/registry.ts +8 -0
  344. package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +0 -167
  345. package/src/memory/v2/__tests__/skill-qdrant.test.ts +0 -657
  346. package/src/memory/v2/skill-qdrant.ts +0 -404
  347. package/src/signals/bash.ts +0 -198
@@ -0,0 +1,338 @@
1
+ /**
2
+ * Tests for `memory/v2/reranker.ts` — public `rerankCandidates` function.
3
+ *
4
+ * Mocks the underlying `LocalRerankBackend` and the `readPage` page reader so
5
+ * the test is hermetic (no subprocess, no filesystem). Verifies the public
6
+ * contract: scores keyed by slug, fail-open on backend failure, page-read
7
+ * failures drop slugs silently, LRU cache hits skip the backend.
8
+ */
9
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
10
+
11
+ import { makeMockLogger } from "../../../__tests__/helpers/mock-logger.js";
12
+ import type { AssistantConfig } from "../../../config/types.js";
13
+
14
+ mock.module("../../../util/logger.js", () => ({
15
+ getLogger: () => makeMockLogger(),
16
+ }));
17
+
18
+ mock.module("../../../util/platform.js", () => ({
19
+ getWorkspaceDir: () => "/tmp/test-workspace",
20
+ }));
21
+
22
+ const backendState = {
23
+ scores: [] as number[],
24
+ shouldThrow: false,
25
+ calls: [] as Array<{ queries: string[]; passages: string[] }>,
26
+ };
27
+ mock.module("../../rerank-local.js", () => ({
28
+ getOrCreateRerankBackend: (_model: string, _dtype: string) => ({
29
+ score: async (queries: string[], passages: string[]): Promise<number[]> => {
30
+ backendState.calls.push({
31
+ queries: [...queries],
32
+ passages: [...passages],
33
+ });
34
+ if (backendState.shouldThrow) throw new Error("backend down");
35
+ return backendState.scores.slice(0, passages.length);
36
+ },
37
+ }),
38
+ }));
39
+
40
+ const pageState = {
41
+ pages: new Map<string, { body: string } | null>(),
42
+ failingSlugs: new Set<string>(),
43
+ };
44
+ // Partial mock — Bun's `mock.module` is process-wide, so we re-export every
45
+ // real symbol and override only `readPage`. Without this, sibling test files
46
+ // that import `listPages` etc. would crash with "Export not found".
47
+ const realPageStore = await import("../page-store.js");
48
+ mock.module("../page-store.js", () => ({
49
+ ...realPageStore,
50
+ readPage: async (_dir: string, slug: string) => {
51
+ if (pageState.failingSlugs.has(slug)) {
52
+ throw new Error("read failure");
53
+ }
54
+ return pageState.pages.get(slug) ?? null;
55
+ },
56
+ }));
57
+
58
+ const { rerankCandidates, _resetRerankCacheForTests } =
59
+ await import("../reranker.js");
60
+
61
+ function configWithModel(model = "test-model"): AssistantConfig {
62
+ return {
63
+ memory: {
64
+ v2: {
65
+ rerank: {
66
+ model,
67
+ enabled: true,
68
+ top_k: 50,
69
+ alpha: 0.3,
70
+ dtype: "q8",
71
+ },
72
+ },
73
+ },
74
+ } as unknown as AssistantConfig;
75
+ }
76
+
77
+ function resetState() {
78
+ backendState.scores = [];
79
+ backendState.shouldThrow = false;
80
+ backendState.calls.length = 0;
81
+ pageState.pages.clear();
82
+ pageState.failingSlugs.clear();
83
+ _resetRerankCacheForTests();
84
+ }
85
+
86
+ beforeEach(resetState);
87
+ afterEach(resetState);
88
+
89
+ /**
90
+ * Convenience: run `rerankCandidates` with a single query and unwrap the
91
+ * returned array. Most tests below assert the contract for the
92
+ * legacy-equivalent single-query path.
93
+ */
94
+ async function rerankSingle(
95
+ query: string,
96
+ candidates: readonly string[],
97
+ config: AssistantConfig,
98
+ ): Promise<Map<string, number>> {
99
+ const out = await rerankCandidates([query], candidates, config);
100
+ return out[0] ?? new Map();
101
+ }
102
+
103
+ describe("rerankCandidates", () => {
104
+ test("returns empty maps for empty candidates", async () => {
105
+ const out = await rerankCandidates(["query"], [], configWithModel());
106
+ expect(out).toHaveLength(1);
107
+ expect(out[0].size).toBe(0);
108
+ expect(backendState.calls).toHaveLength(0);
109
+ });
110
+
111
+ test("returns empty array when no queries are passed", async () => {
112
+ const out = await rerankCandidates([], ["a"], configWithModel());
113
+ expect(out).toHaveLength(0);
114
+ expect(backendState.calls).toHaveLength(0);
115
+ });
116
+
117
+ test("returns empty map for whitespace-only query", async () => {
118
+ pageState.pages.set("a", { body: "content" });
119
+ const out = await rerankSingle(" ", ["a"], configWithModel());
120
+ expect(out.size).toBe(0);
121
+ expect(backendState.calls).toHaveLength(0);
122
+ });
123
+
124
+ test("scores returned keyed by slug, in [0, 1]", async () => {
125
+ pageState.pages.set("a", { body: "first paragraph of a" });
126
+ pageState.pages.set("b", { body: "first paragraph of b" });
127
+ backendState.scores = [0.9, 0.1];
128
+
129
+ const out = await rerankSingle("query", ["a", "b"], configWithModel());
130
+
131
+ expect(out.get("a")).toBe(0.9);
132
+ expect(out.get("b")).toBe(0.1);
133
+ });
134
+
135
+ test("clamps scores to [0, 1]", async () => {
136
+ pageState.pages.set("a", { body: "x" });
137
+ pageState.pages.set("b", { body: "x" });
138
+ backendState.scores = [1.5, -0.2];
139
+
140
+ const out = await rerankSingle("query", ["a", "b"], configWithModel());
141
+
142
+ expect(out.get("a")).toBe(1);
143
+ expect(out.get("b")).toBe(0);
144
+ });
145
+
146
+ test("drops slugs whose page failed to read; others present", async () => {
147
+ pageState.pages.set("a", { body: "x" });
148
+ pageState.failingSlugs.add("b");
149
+ pageState.pages.set("c", { body: "y" });
150
+ backendState.scores = [0.5, 0.7];
151
+
152
+ const out = await rerankSingle("query", ["a", "b", "c"], configWithModel());
153
+
154
+ expect(out.has("b")).toBe(false);
155
+ expect(out.get("a")).toBe(0.5);
156
+ expect(out.get("c")).toBe(0.7);
157
+ });
158
+
159
+ test("drops slugs whose page is null (missing on disk)", async () => {
160
+ pageState.pages.set("a", { body: "x" });
161
+ pageState.pages.set("missing", null);
162
+ backendState.scores = [0.5];
163
+
164
+ const out = await rerankSingle(
165
+ "query",
166
+ ["a", "missing"],
167
+ configWithModel(),
168
+ );
169
+
170
+ expect(out.size).toBe(1);
171
+ expect(out.get("a")).toBe(0.5);
172
+ expect(out.has("missing")).toBe(false);
173
+ });
174
+
175
+ test("returns empty map when backend throws (fail-open)", async () => {
176
+ pageState.pages.set("a", { body: "x" });
177
+ backendState.shouldThrow = true;
178
+
179
+ const out = await rerankSingle("query", ["a"], configWithModel());
180
+
181
+ expect(out.size).toBe(0);
182
+ });
183
+
184
+ test("returns empty map when no pages load (no backend call)", async () => {
185
+ pageState.failingSlugs.add("a");
186
+
187
+ const out = await rerankSingle("query", ["a"], configWithModel());
188
+
189
+ expect(out.size).toBe(0);
190
+ expect(backendState.calls).toHaveLength(0);
191
+ });
192
+
193
+ test("LRU cache hit skips the backend on identical inputs", async () => {
194
+ pageState.pages.set("a", { body: "x" });
195
+ backendState.scores = [0.7];
196
+
197
+ const first = await rerankSingle("query", ["a"], configWithModel());
198
+ const second = await rerankSingle("query", ["a"], configWithModel());
199
+
200
+ expect(first.get("a")).toBe(0.7);
201
+ expect(second.get("a")).toBe(0.7);
202
+ // Backend called only once — second call hit the cache.
203
+ expect(backendState.calls).toHaveLength(1);
204
+ });
205
+
206
+ test("cache key insensitive to candidate order", async () => {
207
+ pageState.pages.set("a", { body: "x" });
208
+ pageState.pages.set("b", { body: "y" });
209
+ backendState.scores = [0.5, 0.6];
210
+
211
+ await rerankSingle("query", ["a", "b"], configWithModel());
212
+ await rerankSingle("query", ["b", "a"], configWithModel());
213
+
214
+ // Same query, same set of candidates — second call hits cache.
215
+ expect(backendState.calls).toHaveLength(1);
216
+ });
217
+
218
+ test("passage construction caps at 240 chars after slug newline", async () => {
219
+ const longBody = "a".repeat(500);
220
+ pageState.pages.set("slug", { body: longBody });
221
+ backendState.scores = [0.5];
222
+
223
+ await rerankSingle("q", ["slug"], configWithModel());
224
+
225
+ expect(backendState.calls).toHaveLength(1);
226
+ const passage = backendState.calls[0].passages[0];
227
+ // "slug\n" prefix + 240 chars of body
228
+ expect(passage.startsWith("slug\n")).toBe(true);
229
+ expect(passage.length).toBeLessThanOrEqual(5 + 240);
230
+ });
231
+
232
+ test("first paragraph is taken (body truncated at blank line)", async () => {
233
+ pageState.pages.set("slug", {
234
+ body: "first para line\n\nsecond para should not appear",
235
+ });
236
+ backendState.scores = [0.5];
237
+
238
+ await rerankSingle("q", ["slug"], configWithModel());
239
+
240
+ const passage = backendState.calls[0].passages[0];
241
+ expect(passage).toContain("first para line");
242
+ expect(passage).not.toContain("second para");
243
+ });
244
+
245
+ test("multiple queries are batched into one backend call", async () => {
246
+ pageState.pages.set("a", { body: "x" });
247
+ pageState.pages.set("b", { body: "y" });
248
+ // Two queries × two passages = four scores, query-major:
249
+ // q1×a, q1×b, q2×a, q2×b
250
+ backendState.scores = [0.9, 0.1, 0.2, 0.8];
251
+
252
+ const out = await rerankCandidates(
253
+ ["user-text", "assistant-text"],
254
+ ["a", "b"],
255
+ configWithModel(),
256
+ );
257
+
258
+ expect(out).toHaveLength(2);
259
+ // Backend invoked exactly once with both queries' pairs.
260
+ expect(backendState.calls).toHaveLength(1);
261
+ expect(backendState.calls[0].queries).toEqual([
262
+ "user-text",
263
+ "user-text",
264
+ "assistant-text",
265
+ "assistant-text",
266
+ ]);
267
+ expect(backendState.calls[0].passages).toHaveLength(4);
268
+
269
+ expect(out[0].get("a")).toBe(0.9);
270
+ expect(out[0].get("b")).toBeCloseTo(0.1, 6);
271
+ expect(out[1].get("a")).toBe(0.2);
272
+ expect(out[1].get("b")).toBe(0.8);
273
+ });
274
+
275
+ test("partial cache hit skips the backend for the cached query", async () => {
276
+ pageState.pages.set("a", { body: "x" });
277
+ // Prime the cache with q1.
278
+ backendState.scores = [0.7];
279
+ await rerankCandidates(["q1"], ["a"], configWithModel());
280
+ expect(backendState.calls).toHaveLength(1);
281
+
282
+ // Now request both q1 (cached) and q2 (fresh). The backend should see
283
+ // only q2's pair, not q1's.
284
+ backendState.scores = [0.4];
285
+ const out = await rerankCandidates(["q1", "q2"], ["a"], configWithModel());
286
+
287
+ expect(backendState.calls).toHaveLength(2);
288
+ expect(backendState.calls[1].queries).toEqual(["q2"]);
289
+ expect(out[0].get("a")).toBe(0.7);
290
+ expect(out[1].get("a")).toBe(0.4);
291
+ });
292
+
293
+ test("forwards configured dtype to the backend factory", async () => {
294
+ pageState.pages.set("a", { body: "x" });
295
+ backendState.scores = [0.5];
296
+ const dtypes: string[] = [];
297
+
298
+ // Re-mock the factory just for this test to capture the dtype arg.
299
+ mock.module("../../rerank-local.js", () => ({
300
+ getOrCreateRerankBackend: (_model: string, dtype: string) => {
301
+ dtypes.push(dtype);
302
+ return {
303
+ score: async (
304
+ queries: string[],
305
+ passages: string[],
306
+ ): Promise<number[]> => {
307
+ backendState.calls.push({
308
+ queries: [...queries],
309
+ passages: [...passages],
310
+ });
311
+ return backendState.scores.slice(0, passages.length);
312
+ },
313
+ };
314
+ },
315
+ }));
316
+ const { rerankCandidates: freshRerank, _resetRerankCacheForTests: reset } =
317
+ await import("../reranker.js");
318
+ reset();
319
+
320
+ const config = {
321
+ memory: {
322
+ v2: {
323
+ rerank: {
324
+ model: "test-model",
325
+ enabled: true,
326
+ top_k: 50,
327
+ alpha: 0.3,
328
+ dtype: "fp32",
329
+ },
330
+ },
331
+ },
332
+ } as unknown as AssistantConfig;
333
+
334
+ await freshRerank(["q"], ["a"], config);
335
+
336
+ expect(dtypes).toContain("fp32");
337
+ });
338
+ });
@@ -70,16 +70,6 @@ const state = {
70
70
  points: Array<{ score?: number; payload: Record<string, unknown> }>;
71
71
  }>,
72
72
  },
73
- // Separate response queue for the dedicated skills collection so a test
74
- // querying both concept pages and skills doesn't have to interleave.
75
- skillQueryResponses: {
76
- dense: [] as Array<{
77
- points: Array<{ score?: number; payload: Record<string, unknown> }>;
78
- }>,
79
- sparse: [] as Array<{
80
- points: Array<{ score?: number; payload: Record<string, unknown> }>;
81
- }>,
82
- },
83
73
  queryCalls: [] as Array<{
84
74
  collection: string;
85
75
  using: string;
@@ -147,11 +137,7 @@ class MockQdrantClient {
147
137
  filter: params.filter,
148
138
  });
149
139
  const channel = params.using as "dense" | "sparse";
150
- const queue =
151
- name === "memory_v2_skills"
152
- ? state.skillQueryResponses[channel]
153
- : state.queryResponses[channel];
154
- return queue.shift() ?? { points: [] };
140
+ return state.queryResponses[channel].shift() ?? { points: [] };
155
141
  }
156
142
  }
157
143
 
@@ -159,10 +145,7 @@ mock.module("@qdrant/js-client-rest", () => ({
159
145
  QdrantClient: MockQdrantClient,
160
146
  }));
161
147
 
162
- const { simBatch, simSkillBatch, clamp01, effectiveWeights } =
163
- await import("../sim.js");
164
- const { _resetMemoryV2SkillQdrantForTests } =
165
- await import("../skill-qdrant.js");
148
+ const { simBatch, clamp01, effectiveWeights } = await import("../sim.js");
166
149
  const { _resetMemoryV2QdrantForTests } = await import("../qdrant.js");
167
150
 
168
151
  // ---------------------------------------------------------------------------
@@ -175,15 +158,12 @@ function resetState(): void {
175
158
  state.embedReturn = [[0.1, 0.2, 0.3]];
176
159
  state.queryResponses.dense.length = 0;
177
160
  state.queryResponses.sparse.length = 0;
178
- state.skillQueryResponses.dense.length = 0;
179
- state.skillQueryResponses.sparse.length = 0;
180
161
  state.queryCalls.length = 0;
181
162
  // Bun's `mock.module` persists across files in the same process, so the
182
- // qdrant modules' singletons may already hold a MockQdrantClient instance
183
- // from a sibling test file. Reset both readiness caches so each test in
184
- // this file gets a fresh `new QdrantClient()` resolved against our mock.
163
+ // qdrant module's singleton may already hold a MockQdrantClient instance
164
+ // from a sibling test file. Reset readiness so each test in this file
165
+ // gets a fresh `new QdrantClient()` resolved against our mock.
185
166
  _resetMemoryV2QdrantForTests();
186
- _resetMemoryV2SkillQdrantForTests();
187
167
  }
188
168
 
189
169
  function configWithWeights(
@@ -533,177 +513,3 @@ describe("simBatch", () => {
533
513
  }
534
514
  });
535
515
  });
536
-
537
- // ---------------------------------------------------------------------------
538
- // simSkillBatch
539
- // ---------------------------------------------------------------------------
540
-
541
- /**
542
- * Stage a single hybrid response on the dedicated skills queues. Mirrors
543
- * `stageHybridResponse` but uses `payload.id` (skills' Qdrant payload key)
544
- * instead of `payload.slug`.
545
- */
546
- function stageSkillHybridResponse(
547
- hits: Array<{ id: string; denseScore?: number; sparseScore?: number }>,
548
- ): void {
549
- state.skillQueryResponses.dense.push({
550
- points: hits
551
- .filter((h) => h.denseScore !== undefined)
552
- .map((h) => ({ score: h.denseScore, payload: { id: h.id } })),
553
- });
554
- state.skillQueryResponses.sparse.push({
555
- points: hits
556
- .filter((h) => h.sparseScore !== undefined)
557
- .map((h) => ({ score: h.sparseScore, payload: { id: h.id } })),
558
- });
559
- }
560
-
561
- describe("simSkillBatch", () => {
562
- test("empty id list returns empty map without touching backends", async () => {
563
- const config = configWithWeights(0.7, 0.3);
564
-
565
- const out = await simSkillBatch("anything", [], config);
566
-
567
- expect(out.size).toBe(0);
568
- expect(state.embedCalls).toHaveLength(0);
569
- expect(state.sparseCalls).toHaveLength(0);
570
- expect(state.queryCalls).toHaveLength(0);
571
- });
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
-
588
- test("queries the dedicated skills collection and forwards an id-IN filter", async () => {
589
- const config = configWithWeights(0.7, 0.3);
590
- stageSkillHybridResponse([]);
591
-
592
- await simSkillBatch(
593
- "query",
594
- ["example-skill-a", "example-skill-b"],
595
- config,
596
- );
597
-
598
- expect(state.queryCalls).toHaveLength(2);
599
- for (const call of state.queryCalls) {
600
- expect(call.collection).toBe("memory_v2_skills");
601
- // The candidate ids are forwarded as a Qdrant filter so Qdrant scores
602
- // exactly the candidate set, not its global top-K. Without this,
603
- // candidate ids absent from the global top-K silently score 0.
604
- expect(call.filter).toEqual({
605
- must: [
606
- { key: "id", match: { any: ["example-skill-a", "example-skill-b"] } },
607
- ],
608
- });
609
- // Limit equals the candidate count.
610
- expect(call.limit).toBe(2);
611
- }
612
- });
613
-
614
- test("fuses dense + sparse with the configured weight blend", async () => {
615
- const config = configWithWeights(0.4, 0.6);
616
- stageSkillHybridResponse([
617
- { id: "example-skill-a", denseScore: 0.5, sparseScore: 4 }, // sparse-norm 1.0
618
- { id: "example-skill-b", denseScore: 0.25, sparseScore: 2 }, // sparse-norm 0.5
619
- ]);
620
-
621
- const out = await simSkillBatch(
622
- "query",
623
- ["example-skill-a", "example-skill-b"],
624
- config,
625
- );
626
-
627
- // example-skill-a: 0.4 * 0.5 + 0.6 * 1.0 = 0.8
628
- // example-skill-b: 0.4 * 0.25 + 0.6 * 0.5 = 0.4
629
- expect(out.get("example-skill-a")).toBeCloseTo(0.8, 6);
630
- expect(out.get("example-skill-b")).toBeCloseTo(0.4, 6);
631
- });
632
-
633
- test("dense-only and sparse-only hits are handled symmetrically", async () => {
634
- const config = configWithWeights(0.7, 0.3);
635
- stageSkillHybridResponse([
636
- { id: "example-skill-a", denseScore: 0.5 /* sparse omitted */ },
637
- { id: "example-skill-b", sparseScore: 8 /* dense omitted */ },
638
- ]);
639
-
640
- const out = await simSkillBatch(
641
- "query",
642
- ["example-skill-a", "example-skill-b"],
643
- config,
644
- );
645
-
646
- // example-skill-a: 0.7 * 0.5 + 0.3 * 0 = 0.35
647
- // example-skill-b: 0.7 * 0 + 0.3 * 1.0 = 0.30 (sparse-norm = 8/8)
648
- expect(out.get("example-skill-a")).toBeCloseTo(0.35, 6);
649
- expect(out.get("example-skill-b")).toBeCloseTo(0.3, 6);
650
- });
651
-
652
- test("forwards candidate ids as the Qdrant restriction; only candidates in result", async () => {
653
- // The bug we're guarding against: when the skills collection has more
654
- // skills than `ids.length`, calling `hybridQuerySkills` without a filter
655
- // returns Qdrant's global top-K. Candidate ids absent from that top-K
656
- // would silently score 0. The fix is to forward the candidate ids as a
657
- // server-side restriction so Qdrant scores exactly the candidate set.
658
- const config = configWithWeights(0.7, 0.3);
659
- stageSkillHybridResponse([
660
- { id: "example-skill-a", denseScore: 0.5, sparseScore: 1 },
661
- // `example-skill-c` would never be returned in production once the
662
- // filter is applied; the post-filter in simSkillBatch defensively
663
- // drops it even if a stale payload slips through.
664
- { id: "example-skill-c", denseScore: 0.9, sparseScore: 1 },
665
- ]);
666
-
667
- const out = await simSkillBatch(
668
- "query",
669
- ["example-skill-a", "example-skill-b"],
670
- config,
671
- );
672
-
673
- // The Qdrant filter was forwarded — both channels carry the id-IN
674
- // restriction matching the caller's candidate set.
675
- expect(state.queryCalls).toHaveLength(2);
676
- for (const call of state.queryCalls) {
677
- expect(call.filter).toEqual({
678
- must: [
679
- { key: "id", match: { any: ["example-skill-a", "example-skill-b"] } },
680
- ],
681
- });
682
- }
683
- // Only candidate ids appear in the result map.
684
- expect(out.has("example-skill-a")).toBe(true);
685
- expect(out.has("example-skill-c")).toBe(false);
686
- });
687
-
688
- test("returned scores are clamped into [0, 1]", async () => {
689
- const config = configWithWeights(0.8, 0.5); // intentionally sums to > 1
690
- stageSkillHybridResponse([
691
- { id: "example-skill-a", denseScore: 1.0, sparseScore: 1 },
692
- ]);
693
-
694
- const out = await simSkillBatch("query", ["example-skill-a"], config);
695
-
696
- expect(out.get("example-skill-a")).toBe(1);
697
- });
698
-
699
- test("embeds the query text exactly once via dense + sparse backends", async () => {
700
- const config = configWithWeights(0.7, 0.3);
701
- stageSkillHybridResponse([]);
702
-
703
- await simSkillBatch("hello skill", ["example-skill-a"], config);
704
-
705
- expect(state.embedCalls).toHaveLength(1);
706
- expect(state.embedCalls[0].inputs).toEqual(["hello skill"]);
707
- expect(state.sparseCalls).toEqual(["hello skill"]);
708
- });
709
- });