@vellumai/assistant 0.7.2 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (424) hide show
  1. package/ARCHITECTURE.md +45 -29
  2. package/Dockerfile +1 -0
  3. package/__tests__/permissions/gateway-threshold-reader.test.ts +236 -9
  4. package/bun.lock +3 -0
  5. package/docs/architecture/memory.md +5 -2
  6. package/knip.json +1 -0
  7. package/node_modules/@vellumai/gateway-client/src/ipc-client.ts +13 -4
  8. package/node_modules/@vellumai/ipc-server-utils/bun.lock +24 -0
  9. package/node_modules/@vellumai/ipc-server-utils/package.json +18 -0
  10. package/node_modules/@vellumai/ipc-server-utils/src/index.ts +6 -0
  11. package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.test.ts +430 -0
  12. package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.ts +221 -0
  13. package/node_modules/@vellumai/ipc-server-utils/tsconfig.json +20 -0
  14. package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +0 -9
  15. package/node_modules/@vellumai/slack-text/src/index.test.ts +18 -35
  16. package/node_modules/@vellumai/slack-text/src/index.ts +2 -48
  17. package/openapi.yaml +470 -25
  18. package/package.json +3 -1
  19. package/src/__tests__/annotate-risk-options.test.ts +291 -0
  20. package/src/__tests__/app-control-flow.test.ts +21 -11
  21. package/src/__tests__/approval-cascade.test.ts +8 -16
  22. package/src/__tests__/approval-routes-http.test.ts +6 -0
  23. package/src/__tests__/assistant-event-hub.test.ts +48 -0
  24. package/src/__tests__/assistant-event.test.ts +0 -10
  25. package/src/__tests__/assistant-events-sse-hardening.test.ts +2 -7
  26. package/src/__tests__/assistant-feature-flags-integration.test.ts +18 -0
  27. package/src/__tests__/auto-analysis-end-to-end.test.ts +48 -0
  28. package/src/__tests__/background-workers-disk-pressure.test.ts +268 -0
  29. package/src/__tests__/call-constants.test.ts +10 -1
  30. package/src/__tests__/call-controller.test.ts +127 -0
  31. package/src/__tests__/call-conversation-messages.test.ts +8 -2
  32. package/src/__tests__/channel-inbound-disk-pressure.test.ts +537 -0
  33. package/src/__tests__/channel-readiness-service.test.ts +4 -2
  34. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +58 -28
  35. package/src/__tests__/config-loader-backfill.test.ts +379 -0
  36. package/src/__tests__/config-loader-platform-defaults.test.ts +284 -1
  37. package/src/__tests__/config-schema.test.ts +1 -0
  38. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +18 -9
  39. package/src/__tests__/config-watcher.test.ts +140 -69
  40. package/src/__tests__/context-search-agent-runner.test.ts +61 -3
  41. package/src/__tests__/context-search-conversations-source.test.ts +0 -24
  42. package/src/__tests__/context-search-fanout.test.ts +0 -1
  43. package/src/__tests__/context-search-memory-source.test.ts +6 -33
  44. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -2
  45. package/src/__tests__/context-search-pkb-source.test.ts +12 -7
  46. package/src/__tests__/context-search-workspace-source.test.ts +0 -1
  47. package/src/__tests__/conversation-abort-tool-results.test.ts +1 -0
  48. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +223 -0
  49. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -1
  50. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -1
  51. package/src/__tests__/conversation-agent-loop.test.ts +457 -8
  52. package/src/__tests__/conversation-confirmation-signals.test.ts +5 -13
  53. package/src/__tests__/conversation-error.test.ts +150 -3
  54. package/src/__tests__/conversation-init.benchmark.test.ts +1 -1
  55. package/src/__tests__/conversation-process-callsite.test.ts +38 -0
  56. package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -0
  57. package/src/__tests__/conversation-runtime-assembly.test.ts +74 -0
  58. package/src/__tests__/conversation-slash-unknown.test.ts +1 -0
  59. package/src/__tests__/conversation-speed-override.test.ts +0 -3
  60. package/src/__tests__/conversation-store.test.ts +0 -18
  61. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +170 -9
  62. package/src/__tests__/conversation-surfaces-app-control.test.ts +15 -4
  63. package/src/__tests__/conversation-surfaces-data-persist.test.ts +476 -0
  64. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +61 -5
  65. package/src/__tests__/conversation-workspace-injection.test.ts +1 -1
  66. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
  67. package/src/__tests__/credentials-cli.test.ts +7 -0
  68. package/src/__tests__/cu-unified-flow.test.ts +176 -10
  69. package/src/__tests__/date-context.test.ts +164 -2
  70. package/src/__tests__/disk-pressure-guard.test.ts +262 -0
  71. package/src/__tests__/disk-pressure-lifecycle.test.ts +168 -0
  72. package/src/__tests__/disk-pressure-policy.test.ts +241 -0
  73. package/src/__tests__/disk-pressure-routes.test.ts +379 -0
  74. package/src/__tests__/disk-pressure-tools.test.ts +277 -0
  75. package/src/__tests__/disk-usage.test.ts +150 -0
  76. package/src/__tests__/events-client-registration.test.ts +52 -0
  77. package/src/__tests__/events-dev-bypass-actor.test.ts +162 -0
  78. package/src/__tests__/file-write-tool.test.ts +4 -10
  79. package/src/__tests__/filing-service.test.ts +2 -20
  80. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +10 -26
  81. package/src/__tests__/heartbeat-disk-pressure.test.ts +183 -0
  82. package/src/__tests__/heartbeat-service.test.ts +260 -11
  83. package/src/__tests__/host-app-control-proxy.test.ts +195 -25
  84. package/src/__tests__/host-bash-proxy.test.ts +227 -34
  85. package/src/__tests__/host-bash-routes.test.ts +178 -13
  86. package/src/__tests__/host-cu-proxy.test.ts +210 -3
  87. package/src/__tests__/host-cu-routes-targeted.test.ts +141 -12
  88. package/src/__tests__/host-file-proxy-targeted.test.ts +48 -9
  89. package/src/__tests__/host-file-proxy.test.ts +268 -6
  90. package/src/__tests__/host-file-routes-targeted.test.ts +175 -17
  91. package/src/__tests__/host-transfer-proxy-targeted.test.ts +408 -59
  92. package/src/__tests__/host-transfer-routes-targeted.test.ts +232 -17
  93. package/src/__tests__/http-user-message-parity.test.ts +107 -1
  94. package/src/__tests__/injector-chain.test.ts +36 -16
  95. package/src/__tests__/injector-disk-pressure.test.ts +224 -0
  96. package/src/__tests__/injector-pkb-v2-silenced.test.ts +10 -7
  97. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +154 -67
  98. package/src/__tests__/managed-profile-guard.test.ts +18 -0
  99. package/src/__tests__/mcp-abort-signal.test.ts +130 -0
  100. package/src/__tests__/memory-admin-recall.test.ts +3 -11
  101. package/src/__tests__/memory-retrieval-pipeline.test.ts +22 -1
  102. package/src/__tests__/normalize-onboarding.test.ts +180 -0
  103. package/src/__tests__/notification-decision-fallback.test.ts +91 -0
  104. package/src/__tests__/notification-decision-strategy.test.ts +22 -0
  105. package/src/__tests__/oauth-cli.test.ts +121 -0
  106. package/src/__tests__/oauth-connect-routes.test.ts +316 -0
  107. package/src/__tests__/oauth-provider-seed-logos.test.ts +24 -2
  108. package/src/__tests__/onboarding-persona-write.test.ts +308 -0
  109. package/src/__tests__/openai-provider.test.ts +45 -8
  110. package/src/__tests__/persist-onboarding-artifacts.test.ts +44 -64
  111. package/src/__tests__/platform-callback-registration.test.ts +21 -4
  112. package/src/__tests__/platform.test.ts +2 -1
  113. package/src/__tests__/playbook-execution.test.ts +0 -43
  114. package/src/__tests__/plugin-tool-contribution.test.ts +47 -0
  115. package/src/__tests__/prechat-onboarding-contract.test.ts +214 -27
  116. package/src/__tests__/provider-tool-name.test.ts +23 -0
  117. package/src/__tests__/relay-server.test.ts +60 -5
  118. package/src/__tests__/runtime-events-sse.test.ts +4 -8
  119. package/src/__tests__/scheduler-disk-pressure.test.ts +148 -0
  120. package/src/__tests__/secret-ingress-http.test.ts +0 -1
  121. package/src/__tests__/secret-prompt-log-hygiene.test.ts +7 -5
  122. package/src/__tests__/secret-prompter-channel-fallback.test.ts +7 -5
  123. package/src/__tests__/secret-response-routing.test.ts +7 -5
  124. package/src/__tests__/server-history-render.test.ts +82 -0
  125. package/src/__tests__/skill-include-graph.test.ts +31 -0
  126. package/src/__tests__/skill-load-tool.test.ts +44 -16
  127. package/src/__tests__/skills.test.ts +39 -0
  128. package/src/__tests__/suggestion-routes.test.ts +46 -0
  129. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -42
  130. package/src/__tests__/tool-executor.test.ts +155 -0
  131. package/src/__tests__/twilio-validation.test.ts +2 -2
  132. package/src/__tests__/voice-session-bridge.test.ts +3 -0
  133. package/src/__tests__/workspace-migration-065-bump-stale-heartbeat-interval.test.ts +122 -0
  134. package/src/__tests__/workspace-migration-066-seed-heartbeat-callsite-cost-default.test.ts +285 -0
  135. package/src/__tests__/workspace-migration-068-release-notes-local-timezone.test.ts +90 -0
  136. package/src/__tests__/workspace-migration-069-seed-onboarding-threads.test.ts +120 -0
  137. package/src/__tests__/workspace-migration-071-remove-safe-storage-release-note.test.ts +206 -0
  138. package/src/__tests__/workspace-migration-safe-storage-limits-release.test.ts +78 -0
  139. package/src/agent/loop.ts +11 -0
  140. package/src/approvals/guardian-request-resolvers.ts +3 -32
  141. package/src/backup/snapshot-lock.ts +2 -27
  142. package/src/bundler/compiler-tools.ts +3 -2
  143. package/src/calls/call-constants.ts +5 -8
  144. package/src/calls/call-controller.ts +130 -67
  145. package/src/calls/call-conversation-messages.ts +46 -10
  146. package/src/calls/relay-server.ts +7 -1
  147. package/src/calls/voice-session-bridge.ts +1 -1
  148. package/src/cli/commands/__tests__/webhooks.test.ts +0 -4
  149. package/src/cli/commands/bash.ts +35 -108
  150. package/src/cli/commands/contacts.ts +64 -25
  151. package/src/cli/commands/credentials.ts +56 -0
  152. package/src/cli/commands/memory-v2.ts +11 -10
  153. package/src/cli/commands/oauth/__tests__/connect.test.ts +401 -219
  154. package/src/cli/commands/oauth/connect.ts +124 -40
  155. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -3
  156. package/src/cli/commands/platform/__tests__/connect.test.ts +7 -1
  157. package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -1
  158. package/src/cli/commands/platform/__tests__/status.test.ts +103 -6
  159. package/src/cli/commands/platform/index.ts +16 -7
  160. package/src/cli/commands/status.ts +57 -0
  161. package/src/cli/program.ts +4 -2
  162. package/src/config/assistant-feature-flags.ts +13 -3
  163. package/src/config/bundled-skills/app-builder/SKILL.md +1 -3
  164. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +4 -3
  165. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +13 -7
  166. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +2 -2
  167. package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +2 -2
  168. package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +2 -2
  169. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -2
  170. package/src/config/env.ts +0 -8
  171. package/src/config/feature-flag-registry.json +13 -5
  172. package/src/config/loader.ts +199 -27
  173. package/src/config/schemas/__tests__/memory-v2.test.ts +10 -5
  174. package/src/config/schemas/call-site-catalog.ts +14 -0
  175. package/src/config/schemas/channels.ts +0 -5
  176. package/src/config/schemas/heartbeat.ts +1 -1
  177. package/src/config/schemas/llm.ts +2 -0
  178. package/src/config/schemas/memory-lifecycle.ts +13 -0
  179. package/src/config/schemas/memory-v2.ts +76 -12
  180. package/src/config/schemas/platform.ts +43 -3
  181. package/src/config/schemas/services.ts +28 -0
  182. package/src/config/seed-inference-profiles.ts +230 -33
  183. package/src/contacts/contact-store.ts +0 -25
  184. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +32 -0
  185. package/src/daemon/__tests__/conversation-tool-setup.test.ts +86 -25
  186. package/src/daemon/assistant-attachments.ts +4 -4
  187. package/src/daemon/config-watcher.ts +85 -57
  188. package/src/daemon/conversation-agent-loop-handlers.ts +38 -0
  189. package/src/daemon/conversation-agent-loop.ts +183 -43
  190. package/src/daemon/conversation-error.ts +87 -15
  191. package/src/daemon/conversation-lifecycle.ts +22 -10
  192. package/src/daemon/conversation-process.ts +8 -0
  193. package/src/daemon/conversation-runtime-assembly.ts +26 -0
  194. package/src/daemon/conversation-store.ts +2 -2
  195. package/src/daemon/conversation-surfaces.ts +211 -29
  196. package/src/daemon/conversation-tool-setup.ts +66 -19
  197. package/src/daemon/conversation.ts +18 -23
  198. package/src/daemon/date-context.ts +71 -22
  199. package/src/daemon/disk-pressure-background-gate.ts +73 -0
  200. package/src/daemon/disk-pressure-guard.ts +343 -0
  201. package/src/daemon/disk-pressure-policy.ts +163 -0
  202. package/src/daemon/handlers/shared.ts +26 -1
  203. package/src/daemon/handlers/skills.ts +3 -4
  204. package/src/daemon/host-app-control-proxy.ts +137 -41
  205. package/src/daemon/host-bash-proxy.ts +47 -22
  206. package/src/daemon/host-browser-proxy.ts +1 -1
  207. package/src/daemon/host-cu-proxy.ts +50 -4
  208. package/src/daemon/host-file-proxy.ts +44 -8
  209. package/src/daemon/host-transfer-proxy.ts +97 -6
  210. package/src/daemon/lifecycle.ts +167 -101
  211. package/src/daemon/meet-host-supervisor.ts +4 -4
  212. package/src/daemon/meet-manifest-loader.ts +0 -1
  213. package/src/daemon/memory-v2-startup.ts +66 -15
  214. package/src/daemon/message-protocol.ts +3 -0
  215. package/src/daemon/message-types/conversations.ts +4 -0
  216. package/src/daemon/message-types/disk-pressure.ts +9 -0
  217. package/src/daemon/message-types/messages.ts +22 -1
  218. package/src/daemon/profiler-run-store.ts +5 -5
  219. package/src/daemon/tool-setup-types.ts +2 -2
  220. package/src/documents/document-store.ts +119 -0
  221. package/src/filing/filing-service.ts +29 -5
  222. package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +9 -16
  223. package/src/heartbeat/__tests__/heartbeat-run-store.test.ts +36 -0
  224. package/src/heartbeat/heartbeat-run-store.ts +13 -0
  225. package/src/heartbeat/heartbeat-service.ts +205 -31
  226. package/src/home/feed-scheduler.ts +18 -0
  227. package/src/inbound/platform-callback-registration.ts +8 -15
  228. package/src/ipc/__tests__/clients-list-ipc.test.ts +169 -0
  229. package/src/ipc/assistant-server.ts +149 -38
  230. package/src/ipc/gateway-client.ts +37 -3
  231. package/src/ipc/skill-server.ts +99 -42
  232. package/src/live-voice/live-voice-archive.ts +4 -4
  233. package/src/live-voice/protocol.ts +5 -7
  234. package/src/media/image-service.ts +1 -7
  235. package/src/memory/__tests__/fixtures/memory-v2-activation-fixtures.ts +21 -13
  236. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +34 -51
  237. package/src/memory/__tests__/memory-v2-activation-log-store.test.ts +0 -6
  238. package/src/memory/__tests__/memory-v2-concept-frequency.test.ts +272 -0
  239. package/src/memory/admin.ts +5 -9
  240. package/src/memory/context-search/agent-runner.ts +19 -2
  241. package/src/memory/context-search/sources/conversations.ts +2 -11
  242. package/src/memory/context-search/sources/memory-v2.ts +1 -16
  243. package/src/memory/context-search/sources/memory.ts +2 -3
  244. package/src/memory/context-search/sources/pkb.ts +2 -3
  245. package/src/memory/context-search/types.ts +0 -1
  246. package/src/memory/conversation-crud.ts +4 -12
  247. package/src/memory/db-init.ts +2 -0
  248. package/src/memory/embedding-runtime-manager.ts +119 -5
  249. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +136 -82
  250. package/src/memory/graph/__tests__/handle-remember-v2.test.ts +11 -26
  251. package/src/memory/graph/conversation-graph-memory.ts +72 -61
  252. package/src/memory/graph/extraction.ts +1 -3
  253. package/src/memory/graph/graph-search.test.ts +11 -67
  254. package/src/memory/graph/graph-search.ts +4 -24
  255. package/src/memory/graph/retriever.test.ts +12 -1
  256. package/src/memory/graph/retriever.ts +10 -15
  257. package/src/memory/graph/tool-handlers.ts +3 -4
  258. package/src/memory/graph/tools.ts +4 -4
  259. package/src/memory/indexer.ts +53 -45
  260. package/src/memory/job-handlers/backfill.ts +2 -11
  261. package/src/memory/job-handlers/cleanup.ts +43 -0
  262. package/src/memory/job-handlers/embedding.ts +6 -8
  263. package/src/memory/job-handlers/summarization.ts +2 -7
  264. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +116 -0
  265. package/src/memory/jobs/embed-concept-page.ts +223 -87
  266. package/src/memory/jobs-store.ts +48 -0
  267. package/src/memory/jobs-worker.ts +85 -43
  268. package/src/memory/memory-v2-activation-log-store.ts +32 -14
  269. package/src/memory/memory-v2-concept-frequency.ts +169 -0
  270. package/src/memory/migrations/239-trace-events-created-at-index.ts +18 -0
  271. package/src/memory/migrations/index.ts +1 -0
  272. package/src/memory/pkb/pkb-search.test.ts +7 -0
  273. package/src/memory/pkb/pkb-search.ts +4 -5
  274. package/src/memory/qdrant-client.ts +3 -13
  275. package/src/memory/rerank-local.ts +374 -0
  276. package/src/memory/search/semantic.ts +10 -72
  277. package/src/memory/trace-event-store.ts +1 -17
  278. package/src/memory/v2/__tests__/activation.test.ts +346 -255
  279. package/src/memory/v2/__tests__/consolidation-job.test.ts +61 -40
  280. package/src/memory/v2/__tests__/injection.test.ts +297 -190
  281. package/src/memory/v2/__tests__/prompts-consolidation.test.ts +61 -2
  282. package/src/memory/v2/__tests__/qdrant.test.ts +326 -9
  283. package/src/memory/v2/__tests__/reranker.test.ts +338 -0
  284. package/src/memory/v2/__tests__/sim.test.ts +113 -196
  285. package/src/memory/v2/__tests__/skill-store.test.ts +71 -65
  286. package/src/memory/v2/__tests__/static-context.test.ts +77 -14
  287. package/src/memory/v2/__tests__/sweep-job.test.ts +19 -33
  288. package/src/memory/v2/activation.ts +149 -156
  289. package/src/memory/v2/consolidation-job.ts +69 -20
  290. package/src/memory/v2/injection.ts +75 -68
  291. package/src/memory/v2/page-store.ts +39 -0
  292. package/src/memory/v2/prompts/consolidation.ts +41 -1
  293. package/src/memory/v2/qdrant.ts +306 -46
  294. package/src/memory/v2/reranker.ts +177 -0
  295. package/src/memory/v2/sim.ts +77 -110
  296. package/src/memory/v2/skill-content.ts +4 -3
  297. package/src/memory/v2/skill-store.ts +82 -59
  298. package/src/memory/v2/static-context.ts +26 -8
  299. package/src/memory/v2/sweep-job.ts +5 -6
  300. package/src/memory/v2/types.ts +17 -10
  301. package/src/notifications/copy-composer.ts +47 -0
  302. package/src/notifications/decision-engine.ts +46 -0
  303. package/src/notifications/signal.ts +4 -0
  304. package/src/oauth/AGENTS.md +3 -1
  305. package/src/oauth/__tests__/oauth-connect-state.test.ts +137 -0
  306. package/src/oauth/connect-orchestrator.ts +2 -0
  307. package/src/oauth/connection-resolver.test.ts +66 -1
  308. package/src/oauth/connection-resolver.ts +55 -1
  309. package/src/oauth/oauth-connect-state.ts +77 -0
  310. package/src/oauth/seed-providers.ts +58 -1
  311. package/src/permissions/gateway-threshold-reader.ts +116 -8
  312. package/src/permissions/prompter.ts +86 -96
  313. package/src/permissions/secret-prompter.ts +31 -31
  314. package/src/plugins/defaults/injectors.ts +36 -4
  315. package/src/plugins/defaults/memory-retrieval.ts +5 -6
  316. package/src/plugins/types.ts +7 -0
  317. package/src/proactive-artifact/aux-message-injector.ts +74 -0
  318. package/src/proactive-artifact/decision.test.ts +226 -0
  319. package/src/proactive-artifact/decision.ts +165 -0
  320. package/src/proactive-artifact/index.ts +7 -0
  321. package/src/proactive-artifact/job.test.ts +914 -0
  322. package/src/proactive-artifact/job.ts +366 -0
  323. package/src/proactive-artifact/message-copy.ts +58 -0
  324. package/src/proactive-artifact/trigger-state.test.ts +277 -0
  325. package/src/proactive-artifact/trigger-state.ts +119 -0
  326. package/src/prompts/normalize-onboarding.ts +80 -0
  327. package/src/prompts/persona-resolver.ts +101 -9
  328. package/src/prompts/system-prompt.ts +21 -7
  329. package/src/prompts/templates/BOOTSTRAP.md +13 -5
  330. package/src/prompts/templates/SOUL.md +13 -28
  331. package/src/providers/__tests__/retry-callsite.test.ts +222 -1
  332. package/src/providers/model-intents.ts +7 -0
  333. package/src/providers/openrouter/client.ts +8 -0
  334. package/src/providers/retry.ts +50 -0
  335. package/src/providers/types.ts +1 -0
  336. package/src/runtime/__tests__/agent-wake.test.ts +456 -3
  337. package/src/runtime/agent-wake.ts +238 -100
  338. package/src/runtime/assistant-event-hub.ts +36 -6
  339. package/src/runtime/assistant-event.ts +0 -1
  340. package/src/runtime/auth/__tests__/route-policy.test.ts +64 -0
  341. package/src/runtime/auth/route-policy.ts +15 -1
  342. package/src/runtime/auth/same-actor.ts +216 -0
  343. package/src/runtime/channel-approvals.ts +3 -2
  344. package/src/runtime/channel-retry-sweep.ts +65 -1
  345. package/src/runtime/local-actor-identity.ts +52 -11
  346. package/src/runtime/pending-interactions.ts +27 -15
  347. package/src/runtime/routes/__tests__/client-routes.test.ts +155 -0
  348. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +0 -5
  349. package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +1 -1
  350. package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +147 -0
  351. package/src/runtime/routes/approval-routes.ts +7 -3
  352. package/src/runtime/routes/client-routes.ts +20 -2
  353. package/src/runtime/routes/consolidation-routes.ts +8 -9
  354. package/src/runtime/routes/contact-routes.ts +0 -25
  355. package/src/runtime/routes/conversation-query-routes.ts +44 -1
  356. package/src/runtime/routes/conversation-routes.ts +35 -26
  357. package/src/runtime/routes/debug-bash-routes.ts +165 -0
  358. package/src/runtime/routes/disk-pressure-routes.ts +121 -0
  359. package/src/runtime/routes/document-pdf-renderer.ts +6 -2
  360. package/src/runtime/routes/documents-routes.ts +2 -75
  361. package/src/runtime/routes/events-routes.ts +41 -9
  362. package/src/runtime/routes/filing-routes.ts +2 -3
  363. package/src/runtime/routes/host-bash-routes.ts +23 -3
  364. package/src/runtime/routes/host-cu-routes.ts +33 -6
  365. package/src/runtime/routes/host-file-routes.ts +32 -6
  366. package/src/runtime/routes/host-transfer-routes.ts +79 -16
  367. package/src/runtime/routes/identity-routes.ts +7 -138
  368. package/src/runtime/routes/inbound-message-handler.ts +77 -12
  369. package/src/runtime/routes/index.ts +6 -0
  370. package/src/runtime/routes/memory-item-routes.test.ts +37 -17
  371. package/src/runtime/routes/memory-item-routes.ts +5 -6
  372. package/src/runtime/routes/memory-v2-routes.ts +136 -17
  373. package/src/runtime/routes/oauth-connect-routes.ts +153 -0
  374. package/src/runtime/verification-outbound-actions.ts +4 -4
  375. package/src/schedule/run-script.ts +37 -5
  376. package/src/schedule/scheduler.ts +20 -1
  377. package/src/security/encrypted-store.ts +2 -0
  378. package/src/security/secure-keys.ts +55 -0
  379. package/src/skills/include-graph.ts +35 -13
  380. package/src/skills/remote-skill-policy.ts +4 -10
  381. package/src/subagent/index.ts +1 -7
  382. package/src/subagent/manager.ts +1 -15
  383. package/src/tasks/task-runner.ts +0 -1
  384. package/src/tasks/task-store.ts +0 -3
  385. package/src/tools/background-tool-registry.ts +17 -3
  386. package/src/tools/document/document-tool.ts +20 -0
  387. package/src/tools/executor.ts +18 -2
  388. package/src/tools/host-filesystem/edit.test.ts +151 -0
  389. package/src/tools/host-filesystem/edit.ts +43 -1
  390. package/src/tools/host-filesystem/read.test.ts +129 -0
  391. package/src/tools/host-filesystem/read.ts +43 -1
  392. package/src/tools/host-filesystem/transfer.test.ts +127 -2
  393. package/src/tools/host-filesystem/transfer.ts +56 -11
  394. package/src/tools/host-filesystem/write.test.ts +134 -0
  395. package/src/tools/host-filesystem/write.ts +43 -1
  396. package/src/tools/host-terminal/host-shell.ts +13 -6
  397. package/src/tools/mcp/mcp-tool-factory.ts +2 -1
  398. package/src/tools/memory/register.test.ts +14 -9
  399. package/src/tools/memory/register.ts +1 -2
  400. package/src/tools/permission-checker.ts +15 -0
  401. package/src/tools/provider-tool-name.ts +28 -0
  402. package/src/tools/registry.ts +30 -9
  403. package/src/tools/skills/load.ts +24 -20
  404. package/src/tools/terminal/shell.ts +9 -1
  405. package/src/tools/tool-approval-handler.ts +31 -6
  406. package/src/tools/tool-name-aliases.ts +19 -0
  407. package/src/tools/types.ts +43 -3
  408. package/src/tts/provider-catalog.ts +3 -5
  409. package/src/util/disk-usage.ts +138 -0
  410. package/src/util/platform.ts +21 -11
  411. package/src/util/process-liveness.ts +26 -0
  412. package/src/workspace/heartbeat-service.ts +19 -0
  413. package/src/workspace/migrations/065-bump-stale-heartbeat-interval.ts +60 -0
  414. package/src/workspace/migrations/066-seed-heartbeat-callsite-cost-default.ts +146 -0
  415. package/src/workspace/migrations/067-release-notes-safe-storage-limits.ts +14 -0
  416. package/src/workspace/migrations/068-release-notes-local-timezone.ts +65 -0
  417. package/src/workspace/migrations/069-seed-onboarding-threads.ts +28 -0
  418. package/src/workspace/migrations/070-memory-v2-summary-schema-rebuild.ts +31 -0
  419. package/src/workspace/migrations/071-remove-safe-storage-release-note.ts +111 -0
  420. package/src/workspace/migrations/registry.ts +14 -0
  421. package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +0 -167
  422. package/src/memory/v2/__tests__/skill-qdrant.test.ts +0 -657
  423. package/src/memory/v2/skill-qdrant.ts +0 -404
  424. package/src/signals/bash.ts +0 -198
@@ -4,7 +4,14 @@
4
4
  * file-based override and falls back to the bundled prompt when the
5
5
  * override is missing/empty/unreadable.
6
6
  */
7
- import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
7
+ import { execFileSync } from "node:child_process";
8
+ import {
9
+ mkdirSync,
10
+ mkdtempSync,
11
+ rmSync,
12
+ symlinkSync,
13
+ writeFileSync,
14
+ } from "node:fs";
8
15
  import { homedir, tmpdir } from "node:os";
9
16
  import { join } from "node:path";
10
17
  import {
@@ -67,7 +74,14 @@ beforeEach(() => {
67
74
  });
68
75
 
69
76
  afterEach(() => {
70
- for (const entry of ["custom-prompt.md", "empty.md", "no-placeholder.md"]) {
77
+ for (const entry of [
78
+ "custom-prompt.md",
79
+ "empty.md",
80
+ "no-placeholder.md",
81
+ "huge.md",
82
+ "link.md",
83
+ "fifo",
84
+ ]) {
71
85
  rmSync(join(tmpWorkspace, entry), { force: true });
72
86
  }
73
87
  });
@@ -178,4 +192,49 @@ describe("resolveConsolidationPrompt — failure modes", () => {
178
192
  const data = warnCalls[0].data as Record<string, unknown>;
179
193
  expect(data.reason).toBe("empty_override");
180
194
  });
195
+
196
+ test("falls back to bundled prompt when the override exceeds the size limit", () => {
197
+ const path = join(tmpWorkspace, "huge.md");
198
+ // 1 MiB + 1 byte — just over the cap so we don't waste test memory.
199
+ writeFileSync(path, Buffer.alloc(1 * 1024 * 1024 + 1, 0x61));
200
+
201
+ const result = resolveConsolidationPrompt(path, CUTOFF);
202
+
203
+ expect(result).toBe(bundledPrompt());
204
+ expect(warnCalls).toHaveLength(1);
205
+ const data = warnCalls[0].data as Record<string, unknown>;
206
+ expect(data.reason).toBe("oversized_override");
207
+ expect(data.size).toBe(1 * 1024 * 1024 + 1);
208
+ });
209
+
210
+ test("falls back to bundled prompt when the override is a symlink", () => {
211
+ const target = join(tmpWorkspace, "custom-prompt.md");
212
+ writeFileSync(target, "real prompt body\n");
213
+ const link = join(tmpWorkspace, "link.md");
214
+ symlinkSync(target, link);
215
+
216
+ const result = resolveConsolidationPrompt(link, CUTOFF);
217
+
218
+ expect(result).toBe(bundledPrompt());
219
+ expect(warnCalls).toHaveLength(1);
220
+ const data = warnCalls[0].data as Record<string, unknown>;
221
+ expect(data.reason).toBe("not_regular_file");
222
+ });
223
+
224
+ test("falls back to bundled prompt when the override is a FIFO", () => {
225
+ const fifoPath = join(tmpWorkspace, "fifo");
226
+ try {
227
+ execFileSync("mkfifo", [fifoPath]);
228
+ } catch {
229
+ // mkfifo unavailable on this platform — skip without failing.
230
+ return;
231
+ }
232
+
233
+ const result = resolveConsolidationPrompt(fifoPath, CUTOFF);
234
+
235
+ expect(result).toBe(bundledPrompt());
236
+ expect(warnCalls).toHaveLength(1);
237
+ const data = warnCalls[0].data as Record<string, unknown>;
238
+ expect(data.reason).toBe("not_regular_file");
239
+ });
181
240
  });
@@ -27,10 +27,39 @@ mock.module("../../qdrant-client.js", () => ({
27
27
  // records every call and lets each test program the next response.
28
28
  type MockPoint = {
29
29
  id: string;
30
- vector: { dense: number[]; sparse: { indices: number[]; values: number[] } };
30
+ vector: {
31
+ dense: number[];
32
+ sparse: { indices: number[]; values: number[] };
33
+ summary_dense?: number[];
34
+ summary_sparse?: { indices: number[]; values: number[] };
35
+ };
31
36
  payload: { slug: string; updated_at: number };
32
37
  };
33
38
 
39
+ type MockCollectionInfo = {
40
+ config: {
41
+ params: {
42
+ vectors?: Record<string, { size: number }> | { size: number };
43
+ sparse_vectors?: Record<string, unknown>;
44
+ };
45
+ };
46
+ };
47
+
48
+ const FULL_SCHEMA_INFO: MockCollectionInfo = {
49
+ config: {
50
+ params: {
51
+ vectors: {
52
+ dense: { size: 384 },
53
+ summary_dense: { size: 384 },
54
+ },
55
+ sparse_vectors: {
56
+ sparse: {},
57
+ summary_sparse: {},
58
+ },
59
+ },
60
+ },
61
+ };
62
+
34
63
  const state = {
35
64
  collectionExistsBeforeCreate: false,
36
65
  collectionExistsCalls: 0,
@@ -39,6 +68,10 @@ const state = {
39
68
  createIndexCalls: [] as Array<{ field_name: string; field_schema: string }>,
40
69
  upsertCalls: [] as Array<{ wait: boolean; points: MockPoint[] }>,
41
70
  deleteCalls: [] as Array<{ wait: boolean; points: string[] }>,
71
+ // Tracks `client.deleteCollection(name)` calls (distinct from `delete()`,
72
+ // which targets points). The schema-drift recreate path drops the
73
+ // collection entirely and we want to assert it ran exactly once.
74
+ deleteCollectionCalls: [] as string[],
42
75
  queryCalls: [] as Array<{
43
76
  using: string;
44
77
  query: unknown;
@@ -55,6 +88,17 @@ const state = {
55
88
  }>,
56
89
  },
57
90
  createCollectionThrows: null as Error | null,
91
+ // Schema returned by `client.getCollection`. Tests that exercise the
92
+ // drift path point this at a partial schema; the default mirrors a fully
93
+ // migrated collection so the no-drift path is the silent default.
94
+ getCollectionInfo: FULL_SCHEMA_INFO as MockCollectionInfo,
95
+ getCollectionThrows: null as Error | null,
96
+ getCollectionCalls: 0,
97
+ // Point count returned by `client.count`. Used by `countConceptPagePoints`
98
+ // which the lifecycle hook reads for the empty-after-create recovery path.
99
+ countResult: 0,
100
+ countThrows: null as Error | null,
101
+ countCalls: 0,
58
102
  // Throw queue for upsert: first call shifts and throws if non-null;
59
103
  // subsequent calls succeed once the queue is exhausted.
60
104
  upsertThrowQueue: [] as Array<Error | null>,
@@ -66,13 +110,29 @@ class MockQdrantClient {
66
110
  state.collectionExistsCalls++;
67
111
  return { exists: state.collectionExistsBeforeCreate };
68
112
  }
113
+ async getCollection(_name: string) {
114
+ state.getCollectionCalls++;
115
+ if (state.getCollectionThrows) throw state.getCollectionThrows;
116
+ return state.getCollectionInfo;
117
+ }
69
118
  async createCollection(_name: string, params: unknown) {
70
119
  state.createCollectionCalls++;
71
120
  state.createCollectionParams = params;
72
121
  if (state.createCollectionThrows) throw state.createCollectionThrows;
73
122
  state.collectionExistsBeforeCreate = true;
123
+ state.getCollectionInfo = FULL_SCHEMA_INFO;
124
+ return {};
125
+ }
126
+ async deleteCollection(name: string) {
127
+ state.deleteCollectionCalls.push(name);
128
+ state.collectionExistsBeforeCreate = false;
74
129
  return {};
75
130
  }
131
+ async count(_name: string, _opts: { exact: boolean }) {
132
+ state.countCalls++;
133
+ if (state.countThrows) throw state.countThrows;
134
+ return { count: state.countResult };
135
+ }
76
136
  async createPayloadIndex(
77
137
  _name: string,
78
138
  params: { field_name: string; field_schema: string },
@@ -102,7 +162,14 @@ class MockQdrantClient {
102
162
  },
103
163
  ) {
104
164
  state.queryCalls.push(params);
105
- const queue = state.queryResponses[params.using as "dense" | "sparse"];
165
+ // Both `dense` and `summary_dense` consume from the dense queue (and
166
+ // similarly for sparse). The four-channel hybrid query fires them in
167
+ // order: body-dense, body-sparse, summary-dense, summary-sparse — so
168
+ // queue order matches call order.
169
+ const queue =
170
+ state.queryResponses[
171
+ params.using.endsWith("sparse") ? "sparse" : "dense"
172
+ ];
106
173
  return queue.shift() ?? { points: [] };
107
174
  }
108
175
  }
@@ -116,6 +183,7 @@ const {
116
183
  upsertConceptPageEmbedding,
117
184
  deleteConceptPageEmbedding,
118
185
  hybridQueryConceptPages,
186
+ countConceptPagePoints,
119
187
  MEMORY_V2_COLLECTION,
120
188
  _resetMemoryV2QdrantForTests,
121
189
  } = await import("../qdrant.js");
@@ -128,10 +196,17 @@ function resetState(): void {
128
196
  state.createIndexCalls.length = 0;
129
197
  state.upsertCalls.length = 0;
130
198
  state.deleteCalls.length = 0;
199
+ state.deleteCollectionCalls.length = 0;
131
200
  state.queryCalls.length = 0;
132
201
  state.queryResponses.dense.length = 0;
133
202
  state.queryResponses.sparse.length = 0;
134
203
  state.createCollectionThrows = null;
204
+ state.getCollectionInfo = FULL_SCHEMA_INFO;
205
+ state.getCollectionThrows = null;
206
+ state.getCollectionCalls = 0;
207
+ state.countResult = 0;
208
+ state.countThrows = null;
209
+ state.countCalls = 0;
135
210
  state.upsertThrowQueue.length = 0;
136
211
  _resetMemoryV2QdrantForTests();
137
212
  }
@@ -140,7 +215,7 @@ describe("memory v2 qdrant — collection lifecycle", () => {
140
215
  beforeEach(resetState);
141
216
  afterEach(resetState);
142
217
 
143
- test("creates the collection with named dense + sparse vectors", async () => {
218
+ test("creates the collection with named dense + sparse vectors (body and summary)", async () => {
144
219
  state.collectionExistsBeforeCreate = false;
145
220
 
146
221
  await ensureConceptPageCollection();
@@ -149,8 +224,12 @@ describe("memory v2 qdrant — collection lifecycle", () => {
149
224
  const params = state.createCollectionParams as {
150
225
  vectors: {
151
226
  dense: { size: number; distance: string; on_disk: boolean };
227
+ summary_dense: { size: number; distance: string; on_disk: boolean };
228
+ };
229
+ sparse_vectors: {
230
+ sparse: Record<string, unknown>;
231
+ summary_sparse: Record<string, unknown>;
152
232
  };
153
- sparse_vectors: { sparse: Record<string, unknown> };
154
233
  hnsw_config: { on_disk: boolean; m: number; ef_construct: number };
155
234
  on_disk_payload: boolean;
156
235
  };
@@ -159,7 +238,14 @@ describe("memory v2 qdrant — collection lifecycle", () => {
159
238
  distance: "Cosine",
160
239
  on_disk: true,
161
240
  });
241
+ // Summary side mirrors body so the activation pipeline can fuse symmetrically.
242
+ expect(params.vectors.summary_dense).toEqual({
243
+ size: 384,
244
+ distance: "Cosine",
245
+ on_disk: true,
246
+ });
162
247
  expect(params.sparse_vectors.sparse).toEqual({});
248
+ expect(params.sparse_vectors.summary_sparse).toEqual({});
163
249
  expect(params.hnsw_config).toEqual({
164
250
  on_disk: true,
165
251
  m: 16,
@@ -190,6 +276,22 @@ describe("memory v2 qdrant — collection lifecycle", () => {
190
276
  expect(state.collectionExistsCalls).toBe(1);
191
277
  });
192
278
 
279
+ test("deduplicates concurrent collection creation", async () => {
280
+ state.collectionExistsBeforeCreate = false;
281
+
282
+ await Promise.all([
283
+ ensureConceptPageCollection(),
284
+ ensureConceptPageCollection(),
285
+ ensureConceptPageCollection(),
286
+ ]);
287
+
288
+ expect(state.collectionExistsCalls).toBe(1);
289
+ expect(state.createCollectionCalls).toBe(1);
290
+ expect(state.createIndexCalls).toEqual([
291
+ { field_name: "slug", field_schema: "keyword" },
292
+ ]);
293
+ });
294
+
193
295
  test("treats 409-on-create as success (concurrent creation race)", async () => {
194
296
  state.collectionExistsBeforeCreate = false;
195
297
  const conflict = Object.assign(new Error("Conflict"), { status: 409 });
@@ -203,6 +305,115 @@ describe("memory v2 qdrant — collection lifecycle", () => {
203
305
  // expected to have created it (it ran the same code).
204
306
  expect(state.createIndexCalls).toEqual([]);
205
307
  });
308
+
309
+ test("detects missing summary_dense / summary_sparse on an existing collection and recreates", async () => {
310
+ // Pre-#29823 schema: only body channels, no summary_*.
311
+ state.collectionExistsBeforeCreate = true;
312
+ state.getCollectionInfo = {
313
+ config: {
314
+ params: {
315
+ vectors: { dense: { size: 384 } },
316
+ sparse_vectors: { sparse: {} },
317
+ },
318
+ },
319
+ };
320
+
321
+ const result = await ensureConceptPageCollection();
322
+
323
+ // Drift path probed once, dropped the collection once, and recreated
324
+ // with the full four-vector schema (the create-success branch resets
325
+ // `getCollectionInfo` to FULL_SCHEMA_INFO so a follow-up probe agrees).
326
+ expect(state.getCollectionCalls).toBe(1);
327
+ expect(state.deleteCollectionCalls).toEqual([MEMORY_V2_COLLECTION]);
328
+ expect(state.createCollectionCalls).toBe(1);
329
+ expect(result).toEqual({ migrated: true });
330
+
331
+ // Recreated schema carries summary_dense + summary_sparse.
332
+ const params = state.createCollectionParams as {
333
+ vectors: Record<string, unknown>;
334
+ sparse_vectors: Record<string, unknown>;
335
+ };
336
+ expect(params.vectors.summary_dense).toBeDefined();
337
+ expect(params.sparse_vectors.summary_sparse).toBeDefined();
338
+ });
339
+
340
+ test("leaves a fully migrated collection untouched", async () => {
341
+ // Default `getCollectionInfo` is FULL_SCHEMA_INFO — already migrated.
342
+ state.collectionExistsBeforeCreate = true;
343
+
344
+ const result = await ensureConceptPageCollection();
345
+
346
+ expect(state.getCollectionCalls).toBe(1);
347
+ expect(state.deleteCollectionCalls).toEqual([]);
348
+ expect(state.createCollectionCalls).toBe(0);
349
+ expect(result).toEqual({ migrated: false });
350
+ });
351
+
352
+ test("getCollection failure is treated as compatible (no destructive recreate)", async () => {
353
+ state.collectionExistsBeforeCreate = true;
354
+ state.getCollectionThrows = new Error("transient REST error");
355
+
356
+ const result = await ensureConceptPageCollection();
357
+
358
+ expect(state.getCollectionCalls).toBe(1);
359
+ expect(state.deleteCollectionCalls).toEqual([]);
360
+ expect(state.createCollectionCalls).toBe(0);
361
+ expect(result).toEqual({ migrated: false });
362
+ });
363
+
364
+ test("concurrent ensure during a schema rebuild only deletes/creates once", async () => {
365
+ state.collectionExistsBeforeCreate = true;
366
+ state.getCollectionInfo = {
367
+ config: {
368
+ params: {
369
+ vectors: { dense: { size: 384 } },
370
+ sparse_vectors: { sparse: {} },
371
+ },
372
+ },
373
+ };
374
+
375
+ const results = await Promise.all([
376
+ ensureConceptPageCollection(),
377
+ ensureConceptPageCollection(),
378
+ ensureConceptPageCollection(),
379
+ ]);
380
+
381
+ expect(state.deleteCollectionCalls).toEqual([MEMORY_V2_COLLECTION]);
382
+ expect(state.createCollectionCalls).toBe(1);
383
+ // All three concurrent callers see the same migrated signal so any one
384
+ // of them is safe to enqueue the reembed (the lifecycle hook is the
385
+ // single producer in practice).
386
+ expect(results).toEqual([
387
+ { migrated: true },
388
+ { migrated: true },
389
+ { migrated: true },
390
+ ]);
391
+ });
392
+ });
393
+
394
+ describe("memory v2 qdrant — point count", () => {
395
+ beforeEach(resetState);
396
+ afterEach(resetState);
397
+
398
+ test("returns the approximate Qdrant count for the v2 collection", async () => {
399
+ state.collectionExistsBeforeCreate = true;
400
+ state.countResult = 1185;
401
+
402
+ const count = await countConceptPagePoints();
403
+
404
+ expect(count).toBe(1185);
405
+ expect(state.countCalls).toBe(1);
406
+ });
407
+
408
+ test("returns 0 when the count call fails (treated as needs-reembed)", async () => {
409
+ state.collectionExistsBeforeCreate = true;
410
+ state.countThrows = new Error("Qdrant unreachable");
411
+
412
+ const count = await countConceptPagePoints();
413
+
414
+ expect(count).toBe(0);
415
+ expect(state.countCalls).toBe(1);
416
+ });
206
417
  });
207
418
 
208
419
  describe("memory v2 qdrant — upsert", () => {
@@ -233,12 +444,67 @@ describe("memory v2 qdrant — upsert", () => {
233
444
  indices: [1, 2],
234
445
  values: [0.5, 0.5],
235
446
  });
447
+ // No summary vectors when caller didn't pass them — Qdrant accepts a
448
+ // partial named-vector subset, and pages without a frontmatter summary
449
+ // legitimately have nothing to embed on the summary side.
450
+ const vectorRecord = point.vector as unknown as Record<string, unknown>;
451
+ expect(vectorRecord.summary_dense).toBeUndefined();
452
+ expect(vectorRecord.summary_sparse).toBeUndefined();
236
453
  // Point ID is a UUID-shaped string derived from the slug.
237
454
  expect(point.id).toMatch(
238
455
  /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/,
239
456
  );
240
457
  });
241
458
 
459
+ test("upserts summary vectors alongside body vectors when both are provided", async () => {
460
+ state.collectionExistsBeforeCreate = true;
461
+
462
+ await upsertConceptPageEmbedding({
463
+ slug: "summarized-page",
464
+ dense: [0.1, 0.2, 0.3],
465
+ sparse: { indices: [1, 2], values: [0.5, 0.5] },
466
+ summary: {
467
+ dense: [0.4, 0.5, 0.6],
468
+ sparse: { indices: [3, 4], values: [0.7, 0.7] },
469
+ },
470
+ updatedAt: 1714000000000,
471
+ });
472
+
473
+ expect(state.upsertCalls).toHaveLength(1);
474
+ const [point] = state.upsertCalls[0].points;
475
+ const vectorRecord = point.vector as unknown as Record<string, unknown>;
476
+ expect(vectorRecord.dense).toEqual([0.1, 0.2, 0.3]);
477
+ expect(vectorRecord.sparse).toEqual({
478
+ indices: [1, 2],
479
+ values: [0.5, 0.5],
480
+ });
481
+ expect(vectorRecord.summary_dense).toEqual([0.4, 0.5, 0.6]);
482
+ expect(vectorRecord.summary_sparse).toEqual({
483
+ indices: [3, 4],
484
+ values: [0.7, 0.7],
485
+ });
486
+ });
487
+
488
+ test("omits summary vectors when the summary block is undefined", async () => {
489
+ // The grouped-shape signature enforces summary as a paired { dense, sparse }
490
+ // block; passing `undefined` (or omitting it) leaves the summary vectors off
491
+ // the point entirely so query-time fusion stays symmetric.
492
+ state.collectionExistsBeforeCreate = true;
493
+
494
+ await upsertConceptPageEmbedding({
495
+ slug: "no-summary",
496
+ dense: [0.1],
497
+ sparse: { indices: [1], values: [1] },
498
+ // summary intentionally omitted
499
+ updatedAt: 1,
500
+ });
501
+
502
+ const [point] = state.upsertCalls[0].points;
503
+ const vectorRecord = point.vector as unknown as Record<string, unknown>;
504
+ expect(vectorRecord.summary_dense).toBeUndefined();
505
+ expect(vectorRecord.summary_sparse).toBeUndefined();
506
+ });
507
+
242
508
  test("two upserts for the same slug share the same point id (overwrites in place)", async () => {
243
509
  state.collectionExistsBeforeCreate = true;
244
510
 
@@ -341,8 +607,9 @@ describe("memory v2 qdrant — hybrid query", () => {
341
607
  beforeEach(resetState);
342
608
  afterEach(resetState);
343
609
 
344
- test("runs both dense and sparse queries and returns per-channel scores", async () => {
610
+ test("runs all four channels (body dense/sparse + summary dense/sparse) and returns per-channel scores", async () => {
345
611
  state.collectionExistsBeforeCreate = true;
612
+ // Body channel hits.
346
613
  state.queryResponses.dense.push({
347
614
  points: [
348
615
  { score: 0.91, payload: { slug: "alice-prefers-vs-code" } },
@@ -355,6 +622,14 @@ describe("memory v2 qdrant — hybrid query", () => {
355
622
  { score: 3, payload: { slug: "bob-uses-zsh" } },
356
623
  ],
357
624
  });
625
+ // Summary channel hits — queue order is body-dense, body-sparse,
626
+ // summary-dense, summary-sparse, so push summaries after bodies.
627
+ state.queryResponses.dense.push({
628
+ points: [{ score: 0.81, payload: { slug: "alice-prefers-vs-code" } }],
629
+ });
630
+ state.queryResponses.sparse.push({
631
+ points: [{ score: 9, payload: { slug: "alice-prefers-vs-code" } }],
632
+ });
358
633
 
359
634
  const results = await hybridQueryConceptPages(
360
635
  [0.1, 0.2, 0.3],
@@ -362,14 +637,19 @@ describe("memory v2 qdrant — hybrid query", () => {
362
637
  5,
363
638
  );
364
639
 
365
- // Both queries fired, with the same limit and the right `using`.
366
- expect(state.queryCalls).toHaveLength(2);
640
+ // All four queries fired with the same limit and distinct `using`.
641
+ expect(state.queryCalls).toHaveLength(4);
367
642
  const usings = state.queryCalls.map((c) => c.using).sort();
368
- expect(usings).toEqual(["dense", "sparse"]);
643
+ expect(usings).toEqual([
644
+ "dense",
645
+ "sparse",
646
+ "summary_dense",
647
+ "summary_sparse",
648
+ ]);
369
649
  expect(state.queryCalls.every((c) => c.limit === 5)).toBe(true);
370
650
  expect(state.queryCalls.every((c) => c.with_payload === true)).toBe(true);
371
651
 
372
- // Each slug exposes both channel scores.
652
+ // Alice has hits on all four channels; bob is body-only.
373
653
  expect(results).toHaveLength(2);
374
654
  const alice = results.find((r) => r.slug === "alice-prefers-vs-code");
375
655
  const bob = results.find((r) => r.slug === "bob-uses-zsh");
@@ -377,6 +657,8 @@ describe("memory v2 qdrant — hybrid query", () => {
377
657
  slug: "alice-prefers-vs-code",
378
658
  denseScore: 0.91,
379
659
  sparseScore: 12,
660
+ summaryDenseScore: 0.81,
661
+ summarySparseScore: 9,
380
662
  });
381
663
  expect(bob).toEqual({
382
664
  slug: "bob-uses-zsh",
@@ -387,6 +669,8 @@ describe("memory v2 qdrant — hybrid query", () => {
387
669
 
388
670
  test("dense-only hits leave sparseScore undefined (and vice versa)", async () => {
389
671
  state.collectionExistsBeforeCreate = true;
672
+ // Body dense + sparse hits. Summary channels stay empty (no push) →
673
+ // they fall through to `{ points: [] }` and produce no summary scores.
390
674
  state.queryResponses.dense.push({
391
675
  points: [{ score: 0.7, payload: { slug: "dense-only" } }],
392
676
  });
@@ -404,8 +688,41 @@ describe("memory v2 qdrant — hybrid query", () => {
404
688
  const sparseOnly = results.find((r) => r.slug === "sparse-only");
405
689
  expect(denseOnly).toEqual({ slug: "dense-only", denseScore: 0.7 });
406
690
  expect(denseOnly?.sparseScore).toBeUndefined();
691
+ expect(denseOnly?.summaryDenseScore).toBeUndefined();
407
692
  expect(sparseOnly).toEqual({ slug: "sparse-only", sparseScore: 2 });
408
693
  expect(sparseOnly?.denseScore).toBeUndefined();
694
+ expect(sparseOnly?.summarySparseScore).toBeUndefined();
695
+ });
696
+
697
+ test("returns summary-channel scores when only the summary side hits", async () => {
698
+ // Page has no body hits but matches via the summary embedding —
699
+ // exercises the path where `simBatch` falls back to summary-only.
700
+ state.collectionExistsBeforeCreate = true;
701
+ // Body channels empty.
702
+ state.queryResponses.dense.push({ points: [] });
703
+ state.queryResponses.sparse.push({ points: [] });
704
+ // Summary channels hit.
705
+ state.queryResponses.dense.push({
706
+ points: [{ score: 0.6, payload: { slug: "summary-only" } }],
707
+ });
708
+ state.queryResponses.sparse.push({
709
+ points: [{ score: 4, payload: { slug: "summary-only" } }],
710
+ });
711
+
712
+ const results = await hybridQueryConceptPages(
713
+ [0.1],
714
+ { indices: [1], values: [1] },
715
+ 5,
716
+ );
717
+
718
+ const summaryOnly = results.find((r) => r.slug === "summary-only");
719
+ expect(summaryOnly).toEqual({
720
+ slug: "summary-only",
721
+ summaryDenseScore: 0.6,
722
+ summarySparseScore: 4,
723
+ });
724
+ expect(summaryOnly?.denseScore).toBeUndefined();
725
+ expect(summaryOnly?.sparseScore).toBeUndefined();
409
726
  });
410
727
 
411
728
  test("does not use Qdrant-side RRF fusion (separate per-channel queries)", async () => {