@vellumai/assistant 0.4.49 → 0.4.50

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 (239) hide show
  1. package/ARCHITECTURE.md +24 -33
  2. package/README.md +3 -3
  3. package/docs/architecture/memory.md +180 -119
  4. package/package.json +2 -2
  5. package/src/__tests__/agent-loop.test.ts +3 -1
  6. package/src/__tests__/anthropic-provider.test.ts +114 -23
  7. package/src/__tests__/approval-cascade.test.ts +1 -15
  8. package/src/__tests__/approval-routes-http.test.ts +2 -0
  9. package/src/__tests__/assistant-feature-flag-guard.test.ts +0 -23
  10. package/src/__tests__/canonical-guardian-store.test.ts +95 -0
  11. package/src/__tests__/checker.test.ts +13 -0
  12. package/src/__tests__/config-schema.test.ts +1 -68
  13. package/src/__tests__/context-memory-e2e.test.ts +11 -100
  14. package/src/__tests__/conversation-routes-guardian-reply.test.ts +8 -0
  15. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
  16. package/src/__tests__/credential-security-e2e.test.ts +1 -0
  17. package/src/__tests__/credential-vault-unit.test.ts +4 -0
  18. package/src/__tests__/credential-vault.test.ts +13 -1
  19. package/src/__tests__/cu-unified-flow.test.ts +532 -0
  20. package/src/__tests__/date-context.test.ts +93 -77
  21. package/src/__tests__/deterministic-verification-control-plane.test.ts +64 -0
  22. package/src/__tests__/guardian-routing-invariants.test.ts +93 -0
  23. package/src/__tests__/history-repair.test.ts +245 -0
  24. package/src/__tests__/host-cu-proxy.test.ts +165 -3
  25. package/src/__tests__/http-user-message-parity.test.ts +1 -0
  26. package/src/__tests__/invite-redemption-service.test.ts +65 -1
  27. package/src/__tests__/keychain-broker-client.test.ts +4 -4
  28. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +56 -18
  29. package/src/__tests__/memory-lifecycle-e2e.test.ts +244 -387
  30. package/src/__tests__/memory-recall-quality.test.ts +244 -407
  31. package/src/__tests__/memory-regressions.experimental.test.ts +126 -101
  32. package/src/__tests__/memory-regressions.test.ts +477 -2841
  33. package/src/__tests__/memory-retrieval.benchmark.test.ts +33 -150
  34. package/src/__tests__/memory-upsert-concurrency.test.ts +5 -244
  35. package/src/__tests__/mime-builder.test.ts +28 -0
  36. package/src/__tests__/native-web-search.test.ts +1 -0
  37. package/src/__tests__/oauth-cli.test.ts +572 -5
  38. package/src/__tests__/oauth-store.test.ts +120 -6
  39. package/src/__tests__/qdrant-collection-migration.test.ts +53 -8
  40. package/src/__tests__/registry.test.ts +0 -1
  41. package/src/__tests__/relay-server.test.ts +46 -1
  42. package/src/__tests__/schedule-tools.test.ts +32 -0
  43. package/src/__tests__/script-proxy-certs.test.ts +1 -1
  44. package/src/__tests__/secret-onetime-send.test.ts +1 -0
  45. package/src/__tests__/secure-keys.test.ts +7 -2
  46. package/src/__tests__/send-endpoint-busy.test.ts +3 -0
  47. package/src/__tests__/session-abort-tool-results.test.ts +1 -14
  48. package/src/__tests__/session-agent-loop-overflow.test.ts +1583 -0
  49. package/src/__tests__/session-agent-loop.test.ts +19 -15
  50. package/src/__tests__/session-confirmation-signals.test.ts +1 -15
  51. package/src/__tests__/session-error.test.ts +124 -2
  52. package/src/__tests__/session-history-web-search.test.ts +918 -0
  53. package/src/__tests__/session-pre-run-repair.test.ts +1 -14
  54. package/src/__tests__/session-provider-retry-repair.test.ts +25 -28
  55. package/src/__tests__/session-queue.test.ts +37 -27
  56. package/src/__tests__/session-runtime-assembly.test.ts +54 -0
  57. package/src/__tests__/session-slash-known.test.ts +1 -15
  58. package/src/__tests__/session-slash-queue.test.ts +1 -15
  59. package/src/__tests__/session-slash-unknown.test.ts +1 -15
  60. package/src/__tests__/session-workspace-cache-state.test.ts +3 -33
  61. package/src/__tests__/session-workspace-injection.test.ts +3 -37
  62. package/src/__tests__/session-workspace-tool-tracking.test.ts +3 -37
  63. package/src/__tests__/skills-install-extract.test.ts +93 -0
  64. package/src/__tests__/skillssh-registry.test.ts +451 -0
  65. package/src/__tests__/trust-store.test.ts +15 -0
  66. package/src/__tests__/voice-invite-redemption.test.ts +32 -1
  67. package/src/agent/ax-tree-compaction.test.ts +51 -0
  68. package/src/agent/loop.ts +39 -12
  69. package/src/approvals/AGENTS.md +1 -1
  70. package/src/approvals/guardian-request-resolvers.ts +14 -2
  71. package/src/bundler/compiler-tools.ts +66 -2
  72. package/src/calls/call-domain.ts +132 -0
  73. package/src/calls/call-store.ts +6 -0
  74. package/src/calls/relay-server.ts +43 -5
  75. package/src/calls/relay-setup-router.ts +17 -1
  76. package/src/calls/twilio-config.ts +1 -1
  77. package/src/calls/types.ts +3 -1
  78. package/src/cli/commands/doctor.ts +4 -3
  79. package/src/cli/commands/mcp.ts +46 -59
  80. package/src/cli/commands/memory.ts +16 -165
  81. package/src/cli/commands/oauth/apps.ts +31 -2
  82. package/src/cli/commands/oauth/connections.ts +431 -97
  83. package/src/cli/commands/oauth/providers.ts +15 -1
  84. package/src/cli/commands/sessions.ts +5 -2
  85. package/src/cli/commands/skills.ts +173 -1
  86. package/src/cli/http-client.ts +0 -20
  87. package/src/cli/main-screen.tsx +2 -2
  88. package/src/cli/program.ts +5 -6
  89. package/src/cli.ts +4 -10
  90. package/src/config/bundled-skills/computer-use/TOOLS.json +1 -1
  91. package/src/config/bundled-skills/computer-use/tools/computer-use-observe.ts +12 -0
  92. package/src/config/bundled-tool-registry.ts +2 -5
  93. package/src/config/schema.ts +1 -12
  94. package/src/config/schemas/memory-lifecycle.ts +0 -9
  95. package/src/config/schemas/memory-processing.ts +0 -180
  96. package/src/config/schemas/memory-retrieval.ts +32 -104
  97. package/src/config/schemas/memory.ts +0 -10
  98. package/src/config/types.ts +0 -4
  99. package/src/context/window-manager.ts +4 -1
  100. package/src/daemon/config-watcher.ts +61 -3
  101. package/src/daemon/daemon-control.ts +1 -1
  102. package/src/daemon/date-context.ts +114 -31
  103. package/src/daemon/handlers/sessions.ts +18 -13
  104. package/src/daemon/handlers/skills.ts +20 -1
  105. package/src/daemon/history-repair.ts +72 -8
  106. package/src/daemon/host-cu-proxy.ts +55 -26
  107. package/src/daemon/lifecycle.ts +31 -3
  108. package/src/daemon/mcp-reload-service.ts +2 -2
  109. package/src/daemon/message-types/computer-use.ts +1 -12
  110. package/src/daemon/message-types/memory.ts +4 -16
  111. package/src/daemon/message-types/messages.ts +1 -0
  112. package/src/daemon/message-types/sessions.ts +4 -0
  113. package/src/daemon/server.ts +12 -1
  114. package/src/daemon/session-agent-loop-handlers.ts +38 -0
  115. package/src/daemon/session-agent-loop.ts +334 -48
  116. package/src/daemon/session-error.ts +89 -6
  117. package/src/daemon/session-history.ts +17 -7
  118. package/src/daemon/session-media-retry.ts +6 -2
  119. package/src/daemon/session-memory.ts +69 -149
  120. package/src/daemon/session-process.ts +10 -1
  121. package/src/daemon/session-runtime-assembly.ts +49 -19
  122. package/src/daemon/session-surfaces.ts +4 -1
  123. package/src/daemon/session-tool-setup.ts +7 -1
  124. package/src/daemon/session.ts +12 -2
  125. package/src/instrument.ts +61 -1
  126. package/src/memory/admin.ts +2 -191
  127. package/src/memory/canonical-guardian-store.ts +38 -2
  128. package/src/memory/conversation-crud.ts +0 -33
  129. package/src/memory/conversation-queries.ts +22 -3
  130. package/src/memory/db-init.ts +28 -0
  131. package/src/memory/embedding-backend.ts +84 -8
  132. package/src/memory/embedding-types.ts +9 -1
  133. package/src/memory/indexer.ts +7 -46
  134. package/src/memory/items-extractor.ts +274 -76
  135. package/src/memory/job-handlers/backfill.ts +2 -127
  136. package/src/memory/job-handlers/cleanup.ts +2 -16
  137. package/src/memory/job-handlers/extraction.ts +2 -138
  138. package/src/memory/job-handlers/index-maintenance.ts +1 -6
  139. package/src/memory/job-handlers/summarization.ts +3 -148
  140. package/src/memory/job-utils.ts +21 -59
  141. package/src/memory/jobs-store.ts +1 -159
  142. package/src/memory/jobs-worker.ts +9 -52
  143. package/src/memory/migrations/104-core-indexes.ts +3 -3
  144. package/src/memory/migrations/149-oauth-tables.ts +2 -0
  145. package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +98 -0
  146. package/src/memory/migrations/151-oauth-providers-ping-url.ts +11 -0
  147. package/src/memory/migrations/152-memory-item-supersession.ts +44 -0
  148. package/src/memory/migrations/153-drop-entity-tables.ts +15 -0
  149. package/src/memory/migrations/154-drop-fts.ts +20 -0
  150. package/src/memory/migrations/155-drop-conflicts.ts +7 -0
  151. package/src/memory/migrations/156-call-session-invite-metadata.ts +24 -0
  152. package/src/memory/migrations/index.ts +7 -0
  153. package/src/memory/qdrant-client.ts +148 -51
  154. package/src/memory/raw-query.ts +1 -1
  155. package/src/memory/retriever.test.ts +294 -273
  156. package/src/memory/retriever.ts +421 -645
  157. package/src/memory/schema/calls.ts +2 -0
  158. package/src/memory/schema/memory-core.ts +3 -48
  159. package/src/memory/schema/oauth.ts +2 -0
  160. package/src/memory/search/formatting.ts +263 -176
  161. package/src/memory/search/lexical.ts +1 -254
  162. package/src/memory/search/ranking.ts +0 -455
  163. package/src/memory/search/semantic.ts +100 -14
  164. package/src/memory/search/staleness.ts +47 -0
  165. package/src/memory/search/tier-classifier.ts +21 -0
  166. package/src/memory/search/types.ts +15 -77
  167. package/src/memory/task-memory-cleanup.ts +4 -6
  168. package/src/messaging/providers/gmail/mime-builder.ts +17 -7
  169. package/src/oauth/byo-connection.test.ts +8 -1
  170. package/src/oauth/oauth-store.ts +113 -27
  171. package/src/oauth/seed-providers.ts +6 -0
  172. package/src/oauth/token-persistence.ts +11 -3
  173. package/src/permissions/defaults.ts +1 -0
  174. package/src/permissions/trust-store.ts +23 -1
  175. package/src/playbooks/playbook-compiler.ts +1 -1
  176. package/src/prompts/system-prompt.ts +18 -2
  177. package/src/providers/anthropic/client.ts +56 -126
  178. package/src/providers/types.ts +7 -1
  179. package/src/runtime/AGENTS.md +9 -0
  180. package/src/runtime/auth/route-policy.ts +6 -3
  181. package/src/runtime/guardian-reply-router.ts +24 -22
  182. package/src/runtime/http-server.ts +2 -2
  183. package/src/runtime/invite-redemption-service.ts +19 -1
  184. package/src/runtime/invite-service.ts +25 -0
  185. package/src/runtime/pending-interactions.ts +2 -2
  186. package/src/runtime/routes/brain-graph-routes.ts +10 -90
  187. package/src/runtime/routes/conversation-routes.ts +9 -1
  188. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -12
  189. package/src/runtime/routes/memory-item-routes.test.ts +754 -0
  190. package/src/runtime/routes/memory-item-routes.ts +503 -0
  191. package/src/runtime/routes/session-management-routes.ts +3 -3
  192. package/src/runtime/routes/settings-routes.ts +2 -2
  193. package/src/runtime/routes/trust-rules-routes.ts +14 -0
  194. package/src/runtime/routes/workspace-routes.ts +2 -1
  195. package/src/security/keychain-broker-client.ts +17 -4
  196. package/src/security/secure-keys.ts +25 -3
  197. package/src/security/token-manager.ts +36 -36
  198. package/src/skills/catalog-install.ts +74 -18
  199. package/src/skills/skillssh-registry.ts +503 -0
  200. package/src/tools/assets/search.ts +5 -1
  201. package/src/tools/computer-use/definitions.ts +0 -10
  202. package/src/tools/computer-use/registry.ts +1 -1
  203. package/src/tools/credentials/vault.ts +1 -3
  204. package/src/tools/memory/definitions.ts +4 -13
  205. package/src/tools/memory/handlers.test.ts +83 -103
  206. package/src/tools/memory/handlers.ts +50 -85
  207. package/src/tools/schedule/create.ts +8 -1
  208. package/src/tools/schedule/update.ts +8 -1
  209. package/src/tools/skills/load.ts +25 -2
  210. package/src/__tests__/clarification-resolver.test.ts +0 -193
  211. package/src/__tests__/conflict-intent-tokenization.test.ts +0 -160
  212. package/src/__tests__/conflict-policy.test.ts +0 -269
  213. package/src/__tests__/conflict-store.test.ts +0 -372
  214. package/src/__tests__/contradiction-checker.test.ts +0 -361
  215. package/src/__tests__/entity-extractor.test.ts +0 -211
  216. package/src/__tests__/entity-search.test.ts +0 -1117
  217. package/src/__tests__/profile-compiler.test.ts +0 -392
  218. package/src/__tests__/session-conflict-gate.test.ts +0 -1228
  219. package/src/__tests__/session-profile-injection.test.ts +0 -557
  220. package/src/config/bundled-skills/knowledge-graph/SKILL.md +0 -25
  221. package/src/config/bundled-skills/knowledge-graph/TOOLS.json +0 -66
  222. package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +0 -211
  223. package/src/daemon/session-conflict-gate.ts +0 -167
  224. package/src/daemon/session-dynamic-profile.ts +0 -77
  225. package/src/memory/clarification-resolver.ts +0 -417
  226. package/src/memory/conflict-intent.ts +0 -205
  227. package/src/memory/conflict-policy.ts +0 -127
  228. package/src/memory/conflict-store.ts +0 -410
  229. package/src/memory/contradiction-checker.ts +0 -508
  230. package/src/memory/entity-extractor.ts +0 -535
  231. package/src/memory/format-recall.ts +0 -47
  232. package/src/memory/fts-reconciler.ts +0 -165
  233. package/src/memory/job-handlers/conflict.ts +0 -200
  234. package/src/memory/profile-compiler.ts +0 -195
  235. package/src/memory/recall-cache.ts +0 -117
  236. package/src/memory/search/entity.ts +0 -535
  237. package/src/memory/search/query-expansion.test.ts +0 -70
  238. package/src/memory/search/query-expansion.ts +0 -118
  239. package/src/runtime/routes/mcp-routes.ts +0 -20
@@ -5,6 +5,11 @@ import { getLogger } from "../util/logger.js";
5
5
 
6
6
  const log = getLogger("qdrant-client");
7
7
 
8
+ export interface QdrantSparseVector {
9
+ indices: number[];
10
+ values: number[];
11
+ }
12
+
8
13
  export interface QdrantClientConfig {
9
14
  url: string;
10
15
  collection: string;
@@ -28,7 +33,6 @@ export interface QdrantPointPayload {
28
33
  conversation_id?: string;
29
34
  message_id?: string;
30
35
  memory_scope_id?: string;
31
- entity_ids?: string[];
32
36
  modality?: "text" | "image" | "audio" | "video";
33
37
  }
34
38
 
@@ -79,17 +83,29 @@ export class VellumQdrantClient {
79
83
  this.embeddingModel = config.embeddingModel;
80
84
  }
81
85
 
82
- async ensureCollection(): Promise<void> {
83
- if (this.collectionReady) return;
86
+ async ensureCollection(): Promise<{ migrated: boolean }> {
87
+ if (this.collectionReady) return { migrated: false };
88
+
89
+ let migrated = false;
84
90
 
85
91
  try {
86
92
  const exists = await this.client.collectionExists(this.collection);
87
93
  if (exists.exists) {
88
94
  try {
89
95
  const info = await this.client.getCollection(this.collection);
90
- const currentSize = (
91
- info.config?.params?.vectors as { size?: number }
92
- )?.size;
96
+ const vectorsConfig = info.config?.params?.vectors;
97
+
98
+ // Detect whether the collection uses unnamed vectors (legacy) vs named vectors
99
+ const isUnnamedVectors =
100
+ vectorsConfig != null &&
101
+ typeof vectorsConfig === "object" &&
102
+ "size" in vectorsConfig;
103
+
104
+ const currentSize = isUnnamedVectors
105
+ ? (vectorsConfig as { size?: number })?.size
106
+ : (vectorsConfig as Record<string, { size?: number }> | undefined)
107
+ ?.dense?.size;
108
+
93
109
  const dimMismatch =
94
110
  currentSize != null && currentSize !== this.vectorSize;
95
111
 
@@ -102,7 +118,19 @@ export class VellumQdrantClient {
102
118
  }
103
119
  }
104
120
 
105
- if (dimMismatch || modelMismatch) {
121
+ if (isUnnamedVectors) {
122
+ log.warn(
123
+ {
124
+ collection: this.collection,
125
+ currentSize,
126
+ expectedSize: this.vectorSize,
127
+ },
128
+ "Qdrant collection uses unnamed vectors (legacy) — deleting and recreating with named vectors. Embeddings will be re-indexed.",
129
+ );
130
+ await this.client.deleteCollection(this.collection);
131
+ migrated = true;
132
+ // Fall through to collection creation below
133
+ } else if (dimMismatch || modelMismatch) {
106
134
  log.warn(
107
135
  {
108
136
  collection: this.collection,
@@ -113,12 +141,13 @@ export class VellumQdrantClient {
113
141
  "Qdrant collection incompatible (dimension or model change) — deleting and recreating. Embeddings will be regenerated on demand.",
114
142
  );
115
143
  await this.client.deleteCollection(this.collection);
144
+ migrated = true;
116
145
  // Fall through to collection creation below
117
146
  } else {
118
147
  if (await this.ensurePayloadIndexesSafe()) {
119
148
  this.collectionReady = true;
120
149
  }
121
- return;
150
+ return { migrated: false };
122
151
  }
123
152
  } catch (err) {
124
153
  log.warn(
@@ -128,7 +157,7 @@ export class VellumQdrantClient {
128
157
  if (await this.ensurePayloadIndexesSafe()) {
129
158
  this.collectionReady = true;
130
159
  }
131
- return;
160
+ return { migrated: false };
132
161
  }
133
162
  }
134
163
  } catch {
@@ -137,15 +166,20 @@ export class VellumQdrantClient {
137
166
 
138
167
  log.info(
139
168
  { collection: this.collection, vectorSize: this.vectorSize },
140
- "Creating Qdrant collection",
169
+ "Creating Qdrant collection with named vectors (dense + sparse)",
141
170
  );
142
171
 
143
172
  try {
144
173
  await this.client.createCollection(this.collection, {
145
174
  vectors: {
146
- size: this.vectorSize,
147
- distance: "Cosine",
148
- on_disk: this.onDisk,
175
+ dense: {
176
+ size: this.vectorSize,
177
+ distance: "Cosine",
178
+ on_disk: this.onDisk,
179
+ },
180
+ },
181
+ sparse_vectors: {
182
+ sparse: {}, // Qdrant auto-infers sparse vector params
149
183
  },
150
184
  hnsw_config: {
151
185
  on_disk: this.onDisk,
@@ -174,7 +208,7 @@ export class VellumQdrantClient {
174
208
  if (await this.ensurePayloadIndexesSafe()) {
175
209
  this.collectionReady = true;
176
210
  }
177
- return;
211
+ return { migrated };
178
212
  }
179
213
  throw err;
180
214
  }
@@ -191,6 +225,8 @@ export class VellumQdrantClient {
191
225
  "Qdrant collection created with payload indexes",
192
226
  );
193
227
  }
228
+
229
+ return { migrated };
194
230
  }
195
231
 
196
232
  async upsert(
@@ -198,6 +234,7 @@ export class VellumQdrantClient {
198
234
  targetId: string,
199
235
  vector: number[],
200
236
  payload: Omit<QdrantPointPayload, "target_type" | "target_id">,
237
+ sparseVector?: QdrantSparseVector,
201
238
  ): Promise<string> {
202
239
  await this.ensureCollection();
203
240
 
@@ -205,20 +242,33 @@ export class VellumQdrantClient {
205
242
  const existing = await this.findByTarget(targetType, targetId);
206
243
  const pointId = existing ?? uuid();
207
244
 
245
+ const namedVector: Record<
246
+ string,
247
+ number[] | { indices: number[]; values: number[] }
248
+ > = {
249
+ dense: vector,
250
+ };
251
+ if (sparseVector) {
252
+ namedVector.sparse = {
253
+ indices: sparseVector.indices,
254
+ values: sparseVector.values,
255
+ };
256
+ }
257
+
258
+ const point = {
259
+ id: pointId,
260
+ vector: namedVector,
261
+ payload: {
262
+ target_type: targetType,
263
+ target_id: targetId,
264
+ ...payload,
265
+ },
266
+ };
267
+
208
268
  try {
209
269
  await this.client.upsert(this.collection, {
210
270
  wait: true,
211
- points: [
212
- {
213
- id: pointId,
214
- vector,
215
- payload: {
216
- target_type: targetType,
217
- target_id: targetId,
218
- ...payload,
219
- },
220
- },
221
- ],
271
+ points: [point],
222
272
  });
223
273
  } catch (err) {
224
274
  if (this.isCollectionMissing(err)) {
@@ -226,17 +276,7 @@ export class VellumQdrantClient {
226
276
  await this.ensureCollection();
227
277
  await this.client.upsert(this.collection, {
228
278
  wait: true,
229
- points: [
230
- {
231
- id: pointId,
232
- vector,
233
- payload: {
234
- target_type: targetType,
235
- target_id: targetId,
236
- ...payload,
237
- },
238
- },
239
- ],
279
+ points: [point],
240
280
  });
241
281
  } else {
242
282
  throw err;
@@ -253,26 +293,22 @@ export class VellumQdrantClient {
253
293
  ): Promise<QdrantSearchResult[]> {
254
294
  await this.ensureCollection();
255
295
 
296
+ const searchParams = {
297
+ vector: { name: "dense", vector },
298
+ limit,
299
+ with_payload: true,
300
+ score_threshold: 0.0,
301
+ filter: filter as Parameters<QdrantRestClient["search"]>[1]["filter"],
302
+ };
303
+
256
304
  let results;
257
305
  try {
258
- results = await this.client.search(this.collection, {
259
- vector,
260
- limit,
261
- with_payload: true,
262
- score_threshold: 0.0,
263
- filter: filter as Parameters<QdrantRestClient["search"]>[1]["filter"],
264
- });
306
+ results = await this.client.search(this.collection, searchParams);
265
307
  } catch (err) {
266
308
  if (this.isCollectionMissing(err)) {
267
309
  this.collectionReady = false;
268
310
  await this.ensureCollection();
269
- results = await this.client.search(this.collection, {
270
- vector,
271
- limit,
272
- with_payload: true,
273
- score_threshold: 0.0,
274
- filter: filter as Parameters<QdrantRestClient["search"]>[1]["filter"],
275
- });
311
+ results = await this.client.search(this.collection, searchParams);
276
312
  } else {
277
313
  throw err;
278
314
  }
@@ -334,6 +370,65 @@ export class VellumQdrantClient {
334
370
  return this.search(vector, limit, filter);
335
371
  }
336
372
 
373
+ /**
374
+ * Hybrid search using both dense and sparse vectors with RRF fusion.
375
+ * Performs two prefetch queries (dense + sparse) and fuses results
376
+ * using Reciprocal Rank Fusion via Qdrant's query API.
377
+ */
378
+ async hybridSearch(params: {
379
+ denseVector: number[];
380
+ sparseVector: QdrantSparseVector;
381
+ filter?: object;
382
+ limit: number;
383
+ prefetchLimit?: number;
384
+ }): Promise<QdrantSearchResult[]> {
385
+ await this.ensureCollection();
386
+
387
+ const { denseVector, sparseVector, filter, limit, prefetchLimit } = params;
388
+ const effectivePrefetchLimit = prefetchLimit ?? 40;
389
+
390
+ const queryParams = {
391
+ prefetch: [
392
+ {
393
+ query: denseVector as unknown as number[],
394
+ using: "dense",
395
+ limit: effectivePrefetchLimit,
396
+ },
397
+ {
398
+ query: {
399
+ indices: sparseVector.indices,
400
+ values: sparseVector.values,
401
+ },
402
+ using: "sparse",
403
+ limit: effectivePrefetchLimit,
404
+ },
405
+ ],
406
+ query: { fusion: "rrf" as const },
407
+ limit,
408
+ filter: filter as Record<string, unknown> | undefined,
409
+ with_payload: true,
410
+ };
411
+
412
+ let results;
413
+ try {
414
+ results = await this.client.query(this.collection, queryParams);
415
+ } catch (err) {
416
+ if (this.isCollectionMissing(err)) {
417
+ this.collectionReady = false;
418
+ await this.ensureCollection();
419
+ results = await this.client.query(this.collection, queryParams);
420
+ } else {
421
+ throw err;
422
+ }
423
+ }
424
+
425
+ return (results.points ?? []).map((point) => ({
426
+ id: typeof point.id === "string" ? point.id : String(point.id),
427
+ score: point.score ?? 0,
428
+ payload: point.payload as unknown as QdrantPointPayload,
429
+ }));
430
+ }
431
+
337
432
  async deleteByTarget(targetType: string, targetId: string): Promise<void> {
338
433
  await this.ensureCollection();
339
434
 
@@ -492,7 +587,9 @@ export class VellumQdrantClient {
492
587
  points: [
493
588
  {
494
589
  id: this.SENTINEL_ID,
495
- vector: new Array(this.vectorSize).fill(0), // zero vector, never matched in search
590
+ vector: {
591
+ dense: new Array(this.vectorSize).fill(0), // zero vector, never matched in search
592
+ },
496
593
  payload: { _meta: true, embedding_model: model },
497
594
  },
498
595
  ],
@@ -24,7 +24,7 @@
24
24
  * use Drizzle's `sql` template, but raw SQL is acceptable when simpler.
25
25
  *
26
26
  * - **Bulk deletes across virtual tables**: Operations like clearing
27
- * memory_segment_fts that reference virtual tables not modeled in Drizzle.
27
+ * messages_fts that reference virtual tables not modeled in Drizzle.
28
28
  *
29
29
  * For everything else — selects, inserts, updates, deletes, joins, aggregations,
30
30
  * filtering, ordering, pagination — use Drizzle.