@wrongstack/vector-memory 0.320.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -249,6 +249,11 @@ function encodeVector(vec) {
249
249
  return Buffer.from(vec.buffer, vec.byteOffset, vec.byteLength);
250
250
  }
251
251
  function decodeVector(buf) {
252
+ if (buf.byteLength % 4 !== 0) {
253
+ throw new Error(
254
+ `decodeVector: invalid vector byteLength ${buf.byteLength} (must be a multiple of 4)`
255
+ );
256
+ }
252
257
  const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
253
258
  const copy = new Float32Array(buf.byteLength / 4);
254
259
  for (let i = 0; i < copy.length; i++) {
@@ -264,6 +269,12 @@ import * as path from "node:path";
264
269
  import { withFileLock } from "@wrongstack/core/utils";
265
270
  import { loadRuntimeDatabaseSync } from "@wrongstack/persistence";
266
271
  import { cosineSimilarity, HashingEmbeddingProvider } from "@wrongstack/sage";
272
+ var DEFAULT_SEARCH_LIMIT = 10;
273
+ function normalizeLimit(limit) {
274
+ if (limit === void 0 || !Number.isFinite(limit)) return DEFAULT_SEARCH_LIMIT;
275
+ const floored = Math.floor(limit);
276
+ return floored < 1 ? DEFAULT_SEARCH_LIMIT : floored;
277
+ }
267
278
  var DEFAULT_DIRECTORY = ".wrongstack/vector-memory";
268
279
  var DEFAULT_FILENAME = "vector-memory.db";
269
280
  var DEFAULT_LOCK_TIMEOUT_MS = 5e3;
@@ -525,7 +536,7 @@ var VectorMemoryStore = class _VectorMemoryStore {
525
536
  }
526
537
  async search(query, opts = {}) {
527
538
  this.assertOpen();
528
- const limit = opts.limit ?? 10;
539
+ const limit = normalizeLimit(opts.limit);
529
540
  const threshold = opts.threshold ?? 0;
530
541
  const includeVectors = opts.includeVectors === true;
531
542
  if (typeof query !== "string" || query.trim().length === 0) return [];
@@ -554,7 +565,7 @@ var VectorMemoryStore = class _VectorMemoryStore {
554
565
  const vec = decodeVector(row.vec_blob);
555
566
  const raw = cosineSimilarity(queryVec, vec);
556
567
  const score = Math.max(0, Math.min(1, raw));
557
- if (score < threshold) continue;
568
+ if (!Number.isFinite(score) || score < threshold) continue;
558
569
  if (top.length >= limit && score <= (top[top.length - 1]?.score ?? 0)) continue;
559
570
  let at = top.length;
560
571
  while (at > 0 && (top[at - 1]?.score ?? 0) < score) at--;
@@ -1214,30 +1225,26 @@ async function fuseWithVectorMemory(query, lexical, options = {}) {
1214
1225
  }
1215
1226
  const sageById = /* @__PURE__ */ new Map();
1216
1227
  for (const memory of lexical) sageById.set(memory.id, memory);
1217
- for (const hit of vectorHits) {
1218
- const sageId = hit.entry.metadata?.["sageId"];
1219
- if (typeof sageId === "string" && !sageById.has(sageId)) {
1220
- continue;
1221
- }
1222
- }
1223
1228
  const lexicalRanked = lexical.map((memory, index) => ({
1224
1229
  memory,
1225
1230
  rankScore: lexicalRankScore(index, lexical.length),
1226
1231
  lexicalScore: lexicalRankScore(index, lexical.length)
1227
1232
  }));
1228
1233
  const vectorRanked = [];
1234
+ const seenVectorSageIds = /* @__PURE__ */ new Set();
1229
1235
  for (let i = 0; i < vectorHits.length; i++) {
1230
1236
  const hit = vectorHits[i];
1231
1237
  const sageId = hit.entry.metadata?.["sageId"];
1232
- if (typeof sageId !== "string") continue;
1238
+ if (typeof sageId !== "string" || seenVectorSageIds.has(sageId)) continue;
1233
1239
  const memory = sageById.get(sageId);
1234
1240
  if (!memory) continue;
1241
+ seenVectorSageIds.add(sageId);
1235
1242
  vectorRanked.push({ memory, vectorScore: hit.score });
1236
1243
  }
1237
1244
  const fused = /* @__PURE__ */ new Map();
1238
1245
  for (let i = 0; i < lexicalRanked.length; i++) {
1239
1246
  const c = lexicalRanked[i];
1240
- const rrf = weight * 0 + (1 - weight) * (1 / (k + i + 1));
1247
+ const rrf = (1 - weight) * (1 / (k + i + 1));
1241
1248
  fused.set(c.memory.id, {
1242
1249
  memory: c.memory,
1243
1250
  vectorScore: null,
@@ -1581,12 +1588,16 @@ async function runSearchRace(query, lexical, vectorStore, options = {}) {
1581
1588
  preview: previewText(mem.text, 140)
1582
1589
  });
1583
1590
  }
1591
+ const overlapById = /* @__PURE__ */ new Map();
1592
+ for (const row of overlap) overlapById.set(row.id, row);
1584
1593
  for (const hit of vectorHits) {
1585
1594
  const sageId = hit.entry.metadata?.["sageId"];
1586
1595
  if (typeof sageId !== "string") continue;
1587
- const existing = overlap.find((row) => row.id === sageId);
1596
+ const existing = overlapById.get(sageId);
1588
1597
  if (existing) {
1589
- existing.vectorScore = hit.score;
1598
+ if (existing.vectorScore === null) {
1599
+ existing.vectorScore = hit.score;
1600
+ }
1590
1601
  continue;
1591
1602
  }
1592
1603
  if (seenIds.has(sageId)) continue;
@@ -1598,8 +1609,8 @@ async function runSearchRace(query, lexical, vectorStore, options = {}) {
1598
1609
  preview: previewText(hit.entry.text, 140)
1599
1610
  });
1600
1611
  }
1601
- for (let i = overlap.length - 1; i >= 0; i--) {
1602
- const row = overlap[i];
1612
+ const finalOverlap = [];
1613
+ for (const row of overlap) {
1603
1614
  if (row.vectorScore === null) {
1604
1615
  lexicalOnly.push({
1605
1616
  id: row.id,
@@ -1607,26 +1618,25 @@ async function runSearchRace(query, lexical, vectorStore, options = {}) {
1607
1618
  vectorScore: null,
1608
1619
  preview: row.preview
1609
1620
  });
1610
- overlap.splice(i, 1);
1621
+ } else {
1622
+ finalOverlap.push(row);
1611
1623
  }
1612
1624
  }
1613
- const lexicalCount = lexicalOnly.length + overlap.length;
1614
- const vectorCount = vectorOnly.length + overlap.length;
1625
+ const lexicalCount = lexicalOnly.length + finalOverlap.length;
1626
+ const vectorCount = vectorOnly.length + finalOverlap.length;
1615
1627
  const denom = Math.max(lexicalCount, vectorCount, 1);
1616
1628
  return {
1617
1629
  query,
1618
1630
  lexicalOnly,
1619
1631
  vectorOnly,
1620
- // The sweep above removed every row still carrying a null vectorScore,
1621
- // so every remaining row holds a real number here.
1622
- overlap,
1632
+ overlap: finalOverlap,
1623
1633
  metrics: {
1624
1634
  lexicalCount,
1625
1635
  vectorCount,
1626
- overlapCount: overlap.length,
1636
+ overlapCount: finalOverlap.length,
1627
1637
  lexicalOnlyRatio: lexicalCount === 0 ? 0 : lexicalOnly.length / lexicalCount,
1628
1638
  vectorOnlyRatio: vectorCount === 0 ? 0 : vectorOnly.length / vectorCount,
1629
- agreementRatio: overlap.length / denom
1639
+ agreementRatio: finalOverlap.length / denom
1630
1640
  }
1631
1641
  };
1632
1642
  }
@@ -6,8 +6,15 @@
6
6
  *
7
7
  * The fusion is a Reciprocal Rank Fusion (RRF)-style blend rather than a
8
8
  * raw cosine-blend, so it stays robust when one side returns nothing
9
- * (e.g. embedding provider unavailable). All inputs are optional:
10
- * - `lexical` may be `[]` → fusion falls back to pure vector ranking.
9
+ * (e.g. embedding provider unavailable).
10
+ *
11
+ * IMPORTANT — the vector channel can only RE-RANK the lexical list, never
12
+ * extend it. A vector hit is kept only when its `metadata.sageId` resolves to
13
+ * a memory already present in `lexical`, because the caller's lexical list is
14
+ * the authoritative source of `Sage` objects and this function will not
15
+ * fabricate one (pinned by tests/sage-fusion.test.ts). Therefore:
16
+ * - `lexical` may be `[]` → the result is `[]`, whatever the vector channel
17
+ * found. This is NOT a fallback to pure vector ranking.
11
18
  * - `vector` may be `[]` → fusion falls back to pure lexical ranking.
12
19
  * - either may be `undefined` → fusion silently drops that channel.
13
20
  *
@@ -40,6 +47,10 @@ export interface SageFusionOptions {
40
47
  /**
41
48
  * Cosine threshold below which a vector-only hit is dropped (no lexical
42
49
  * counterpart to lift it). Default 0 — keep all, let RRF decide.
50
+ *
51
+ * Currently inert: vector-only hits cannot occur (see the module docstring),
52
+ * so nothing ever reaches this threshold. Kept for API stability and for
53
+ * the day the vector channel is allowed to extend the candidate set.
43
54
  */
44
55
  vectorOnlyThreshold?: number | undefined;
45
56
  }
@@ -51,7 +62,11 @@ export interface SageFusionHit {
51
62
  lexicalScore: number | null;
52
63
  /** 0..1, monotonically higher = better. */
53
64
  finalScore: number;
54
- /** Where the candidate came from. */
65
+ /**
66
+ * Where the candidate came from. `'vector'` is currently unreachable —
67
+ * every candidate originates in the lexical list (see the module
68
+ * docstring); the vector channel only upgrades one to `'both'`.
69
+ */
55
70
  source: 'lexical' | 'vector' | 'both';
56
71
  }
57
72
  declare function lexicalRankScore(index: number, total: number): number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrongstack/vector-memory",
3
- "version": "0.320.1",
3
+ "version": "1.0.2",
4
4
  "license": "MIT",
5
5
  "description": "WrongStack Vector Memory — an additional vector-search memory store powered by @huggingface/transformers (local ONNX embeddings), alongside the SAGE lexical memory system.",
6
6
  "repository": {
@@ -27,9 +27,9 @@
27
27
  "README.md"
28
28
  ],
29
29
  "dependencies": {
30
- "@wrongstack/core": "0.320.1",
31
- "@wrongstack/persistence": "0.320.1",
32
- "@wrongstack/sage": "0.320.1"
30
+ "@wrongstack/core": "1.0.2",
31
+ "@wrongstack/persistence": "1.0.2",
32
+ "@wrongstack/sage": "1.0.2"
33
33
  },
34
34
  "optionalDependencies": {
35
35
  "@huggingface/transformers": "^4.2.0"