@oh-my-pi/pi-mnemopi 17.0.7 → 17.0.8

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/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [17.0.8] - 2026-07-22
6
+
7
+ ### Changed
8
+
9
+ - Optimized vector operations (exact vector-index search, SHMR similarity clustering, and default-similarity MMR rerank) by migrating hot loops to native batch kernels, resulting in significant performance improvements (up to 1.8x faster top-K search, 2.4x faster pairwise clustering, and 22-36x faster MMR reranking).
10
+
5
11
  ## [17.0.4] - 2026-07-18
6
12
 
7
13
  ### Fixed
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-mnemopi",
4
- "version": "17.0.7",
4
+ "version": "17.0.8",
5
5
  "description": "Local SQLite memory engine for Oh My Pi agents",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -39,9 +39,10 @@
39
39
  "fmt": "biome format --write ."
40
40
  },
41
41
  "dependencies": {
42
- "@oh-my-pi/pi-ai": "17.0.7",
43
- "@oh-my-pi/pi-catalog": "17.0.7",
44
- "@oh-my-pi/pi-utils": "17.0.7",
42
+ "@oh-my-pi/pi-ai": "17.0.8",
43
+ "@oh-my-pi/pi-catalog": "17.0.8",
44
+ "@oh-my-pi/pi-natives": "17.0.8",
45
+ "@oh-my-pi/pi-utils": "17.0.8",
45
46
  "lru-cache": "11.5.2"
46
47
  },
47
48
  "peerDependencies": {
package/src/core/mmr.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { mmrRerankIndices } from "@oh-my-pi/pi-natives";
2
+
1
3
  export interface MmrResult {
2
4
  readonly content?: string;
3
5
  readonly score?: number;
@@ -33,6 +35,34 @@ export function mmrRerank<T extends MmrResult>(
33
35
  const first = sortedResults[0];
34
36
  if (first === undefined) return [];
35
37
 
38
+ // Native batch kernel: one N-API crossing selects all indices. Only valid
39
+ // for the default Jaccard similarity; custom similarity functions stay in
40
+ // TS. Lone-surrogate contents also stay in TS: N-API converts them to
41
+ // U+FFFD, which would merge distinct tokens. NaN topK stays in TS so the
42
+ // pre-native contract (loop guard is false, first result still returned)
43
+ // is preserved.
44
+ if (
45
+ similarityFn === jaccardSimilarity &&
46
+ !Number.isNaN(limit) &&
47
+ sortedResults.every(result => (result.content ?? "").isWellFormed())
48
+ ) {
49
+ // Pre-lowercase with JS semantics so contextual mappings (Final_Sigma:
50
+ // "ΟΣ" -> "ος") are applied before the context-insensitive native
51
+ // lowercase, which is idempotent on already-lowercased text.
52
+ const contents = sortedResults.map(result => (result.content ?? "").toLowerCase());
53
+ const scores = Float64Array.from(sortedResults, result => result.score ?? 0);
54
+ // Clamp before the u32 N-API boundary: Infinity or >= 2**32 would
55
+ // otherwise wrap (ToUint32) and silently return nothing.
56
+ const nativeLimit = Math.min(limit, sortedResults.length);
57
+ const picked = mmrRerankIndices(contents, scores, lambdaParam, nativeLimit);
58
+ const out: T[] = [];
59
+ for (const index of picked) {
60
+ const item = sortedResults[index];
61
+ if (item !== undefined) out.push(item);
62
+ }
63
+ return out;
64
+ }
65
+
36
66
  const selected: T[] = [first];
37
67
  const remaining = sortedResults.slice(1);
38
68
 
package/src/core/shmr.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { Database } from "bun:sqlite";
2
2
  import { createHash } from "node:crypto";
3
+ import { cosineSimilarityPairs } from "@oh-my-pi/pi-natives";
3
4
  import { logger } from "@oh-my-pi/pi-utils";
4
5
  import * as embeddings from "./embeddings";
5
6
  import { cosineSimilarity } from "./vector-math";
@@ -166,17 +167,23 @@ export async function clusterBySimilarity(items: readonly ShmrItem[], threshold:
166
167
  if (items.length === 0) return [];
167
168
  const vectors = await resolveItemVectors(items);
168
169
  const adjacency: number[][] = Array.from({ length: items.length }, () => []);
169
- for (let i = 0; i < items.length; i++) {
170
- const leftEmbedding = vectors[i];
171
- if (leftEmbedding === undefined) continue;
172
- for (let j = i + 1; j < items.length; j++) {
173
- const rightEmbedding = vectors[j];
174
- if (rightEmbedding === undefined) continue;
175
- if (cosineSimilarity(leftEmbedding, rightEmbedding) >= threshold) {
176
- adjacency[i]?.push(j);
177
- adjacency[j]?.push(i);
178
- }
179
- }
170
+ // Native batch kernel: one N-API crossing evaluates all O(n^2) pairs.
171
+ // Vectors are zero-padded to a shared dim, matching the TS `?? 0`
172
+ // missing-element semantics of cosineSimilarity.
173
+ let dim = 0;
174
+ for (const vector of vectors) if (vector.length > dim) dim = vector.length;
175
+ const flat = new Float64Array(vectors.length * dim);
176
+ for (let i = 0; i < vectors.length; i++) {
177
+ const vector = vectors[i];
178
+ if (vector === undefined) continue;
179
+ for (let col = 0; col < vector.length; col++) flat[i * dim + col] = vector[col] ?? 0;
180
+ }
181
+ const pairs = cosineSimilarityPairs(flat, vectors.length, dim, threshold);
182
+ for (let p = 0; p < pairs.length; p += 2) {
183
+ const i = pairs[p] ?? 0;
184
+ const j = pairs[p + 1] ?? 0;
185
+ adjacency[i]?.push(j);
186
+ adjacency[j]?.push(i);
180
187
  }
181
188
  const visited = new Set<number>();
182
189
  const clusters: ShmrItem[][] = [];
@@ -1,3 +1,5 @@
1
+ import { vectorIndexTopK } from "@oh-my-pi/pi-natives";
2
+
1
3
  export interface ExactVectorSearchHit<TId> {
2
4
  id: TId;
3
5
  score: number;
@@ -66,19 +68,16 @@ export function searchExactVectorIndex<TId>(
66
68
  queryNormSq += value * value;
67
69
  }
68
70
  if (queryNormSq <= 0) return [];
69
- const queryNorm = Math.sqrt(queryNormSq);
70
- const queryDimensions = Math.min(query.length, index.dimensions);
71
+ // Native batch kernel: one N-API crossing scores every row and ranks the
72
+ // top k with the same stable ordering as the TS sort. TS guards above
73
+ // (finite query, positive norm, non-empty index) are preserved. Clamp k to
74
+ // the row count before the u32 boundary: Infinity or >= 2**32 would
75
+ // otherwise wrap (ToUint32) and return no hits.
76
+ const topK = vectorIndexTopK(index.matrix, index.dimensions, Float64Array.from(query), Math.min(k, index.count));
71
77
  const hits: ExactVectorSearchHit<TId>[] = [];
72
-
73
- for (let row = 0; row < index.count; row += 1) {
74
- const offset = row * index.dimensions;
75
- let score = 0;
76
- for (let col = 0; col < queryDimensions; col += 1) {
77
- score += index.matrix[offset + col] * ((query[col] ?? 0) / queryNorm);
78
- }
79
- hits.push({ id: index.ids[row] as TId, score });
78
+ for (let i = 0; i < topK.indices.length; i += 1) {
79
+ const row = topK.indices[i] ?? 0;
80
+ hits.push({ id: index.ids[row] as TId, score: topK.scores[i] ?? 0 });
80
81
  }
81
-
82
- hits.sort((a, b) => b.score - a.score);
83
- return hits.slice(0, Math.min(k, hits.length));
82
+ return hits;
84
83
  }