@linxiraos/pi-mnemopi 1.0.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 (135) hide show
  1. package/CHANGELOG.md +248 -0
  2. package/README.md +108 -0
  3. package/dist/types/cli.d.ts +35 -0
  4. package/dist/types/config.d.ts +108 -0
  5. package/dist/types/core/aaak.d.ts +55 -0
  6. package/dist/types/core/annotations.d.ts +75 -0
  7. package/dist/types/core/banks.d.ts +33 -0
  8. package/dist/types/core/beam/consolidate.d.ts +38 -0
  9. package/dist/types/core/beam/helpers.d.ts +69 -0
  10. package/dist/types/core/beam/index.d.ts +59 -0
  11. package/dist/types/core/beam/recall.d.ts +52 -0
  12. package/dist/types/core/beam/schema.d.ts +2 -0
  13. package/dist/types/core/beam/store.d.ts +57 -0
  14. package/dist/types/core/beam/types.d.ts +268 -0
  15. package/dist/types/core/binary-vectors.d.ts +54 -0
  16. package/dist/types/core/chat-normalize.d.ts +13 -0
  17. package/dist/types/core/content-sanitizer.d.ts +18 -0
  18. package/dist/types/core/cost-log.d.ts +13 -0
  19. package/dist/types/core/embeddings.d.ts +93 -0
  20. package/dist/types/core/entities.d.ts +9 -0
  21. package/dist/types/core/episodic-graph.d.ts +89 -0
  22. package/dist/types/core/extraction/client.d.ts +35 -0
  23. package/dist/types/core/extraction/diagnostics.d.ts +51 -0
  24. package/dist/types/core/extraction/prompts.d.ts +2 -0
  25. package/dist/types/core/extraction.d.ts +34 -0
  26. package/dist/types/core/fastembed-model-cache.d.ts +2 -0
  27. package/dist/types/core/fastembed-runtime.d.ts +41 -0
  28. package/dist/types/core/index.d.ts +6 -0
  29. package/dist/types/core/llm-backends.d.ts +23 -0
  30. package/dist/types/core/local-llm.d.ts +20 -0
  31. package/dist/types/core/memory.d.ts +186 -0
  32. package/dist/types/core/migrations/e6-triplestore-split.d.ts +17 -0
  33. package/dist/types/core/migrations/index.d.ts +1 -0
  34. package/dist/types/core/mmr.d.ts +8 -0
  35. package/dist/types/core/orchestrator.d.ts +20 -0
  36. package/dist/types/core/patterns.d.ts +61 -0
  37. package/dist/types/core/plugins.d.ts +109 -0
  38. package/dist/types/core/polyphonic-recall.d.ts +66 -0
  39. package/dist/types/core/query-cache.d.ts +45 -0
  40. package/dist/types/core/query-intent.d.ts +20 -0
  41. package/dist/types/core/recall-diagnostics.d.ts +48 -0
  42. package/dist/types/core/runtime-options.d.ts +76 -0
  43. package/dist/types/core/shmr.d.ts +62 -0
  44. package/dist/types/core/streaming.d.ts +136 -0
  45. package/dist/types/core/synonyms.d.ts +46 -0
  46. package/dist/types/core/temporal-parser.d.ts +16 -0
  47. package/dist/types/core/token-counter.d.ts +8 -0
  48. package/dist/types/core/triples.d.ts +63 -0
  49. package/dist/types/core/typed-memory.d.ts +39 -0
  50. package/dist/types/core/vector-index.d.ts +16 -0
  51. package/dist/types/core/vector-math.d.ts +1 -0
  52. package/dist/types/core/veracity-consolidation.d.ts +60 -0
  53. package/dist/types/core/weibull.d.ts +96 -0
  54. package/dist/types/db.d.ts +18 -0
  55. package/dist/types/diagnose.d.ts +24 -0
  56. package/dist/types/dr/index.d.ts +1 -0
  57. package/dist/types/dr/recovery.d.ts +68 -0
  58. package/dist/types/index.d.ts +6 -0
  59. package/dist/types/mcp-server.d.ts +40 -0
  60. package/dist/types/mcp-tools.d.ts +484 -0
  61. package/dist/types/migrations/e6-triplestore-split.d.ts +1 -0
  62. package/dist/types/migrations/index.d.ts +1 -0
  63. package/dist/types/types.d.ts +145 -0
  64. package/dist/types/util/datetime.d.ts +8 -0
  65. package/dist/types/util/env.d.ts +10 -0
  66. package/dist/types/util/ids.d.ts +3 -0
  67. package/dist/types/util/lru.d.ts +12 -0
  68. package/dist/types/util/regex.d.ts +11 -0
  69. package/package.json +108 -0
  70. package/src/cli.ts +398 -0
  71. package/src/config.ts +373 -0
  72. package/src/core/aaak.ts +142 -0
  73. package/src/core/annotations.ts +456 -0
  74. package/src/core/banks.ts +133 -0
  75. package/src/core/beam/consolidate.ts +1132 -0
  76. package/src/core/beam/helpers.ts +831 -0
  77. package/src/core/beam/index.ts +358 -0
  78. package/src/core/beam/recall.ts +1225 -0
  79. package/src/core/beam/schema.ts +426 -0
  80. package/src/core/beam/store.ts +1083 -0
  81. package/src/core/beam/types.ts +303 -0
  82. package/src/core/binary-vectors.ts +317 -0
  83. package/src/core/chat-normalize.ts +160 -0
  84. package/src/core/content-sanitizer.ts +136 -0
  85. package/src/core/cost-log.ts +101 -0
  86. package/src/core/embeddings.ts +624 -0
  87. package/src/core/entities.ts +263 -0
  88. package/src/core/episodic-graph.ts +708 -0
  89. package/src/core/extraction/client.ts +187 -0
  90. package/src/core/extraction/diagnostics.ts +193 -0
  91. package/src/core/extraction/prompts.ts +31 -0
  92. package/src/core/extraction.ts +491 -0
  93. package/src/core/fastembed-model-cache.ts +39 -0
  94. package/src/core/fastembed-runtime.ts +172 -0
  95. package/src/core/index.ts +39 -0
  96. package/src/core/llm-backends.ts +54 -0
  97. package/src/core/local-llm.ts +475 -0
  98. package/src/core/memory.ts +680 -0
  99. package/src/core/migrations/e6-triplestore-split.ts +211 -0
  100. package/src/core/migrations/index.ts +1 -0
  101. package/src/core/mmr.ts +101 -0
  102. package/src/core/orchestrator.ts +62 -0
  103. package/src/core/patterns.ts +484 -0
  104. package/src/core/plugins.ts +375 -0
  105. package/src/core/polyphonic-recall.ts +563 -0
  106. package/src/core/query-cache.ts +351 -0
  107. package/src/core/query-intent.ts +139 -0
  108. package/src/core/recall-diagnostics.ts +157 -0
  109. package/src/core/runtime-options.ts +130 -0
  110. package/src/core/shmr.ts +567 -0
  111. package/src/core/streaming.ts +419 -0
  112. package/src/core/synonyms.ts +197 -0
  113. package/src/core/temporal-parser.ts +363 -0
  114. package/src/core/token-counter.ts +30 -0
  115. package/src/core/triples.ts +452 -0
  116. package/src/core/typed-memory.ts +407 -0
  117. package/src/core/vector-index.ts +83 -0
  118. package/src/core/vector-math.ts +23 -0
  119. package/src/core/veracity-consolidation.ts +477 -0
  120. package/src/core/weibull.ts +124 -0
  121. package/src/db.ts +186 -0
  122. package/src/diagnose.ts +174 -0
  123. package/src/dr/index.ts +1 -0
  124. package/src/dr/recovery.ts +405 -0
  125. package/src/index.ts +33 -0
  126. package/src/mcp-server.ts +155 -0
  127. package/src/mcp-tools.ts +970 -0
  128. package/src/migrations/e6-triplestore-split.ts +1 -0
  129. package/src/migrations/index.ts +1 -0
  130. package/src/types.ts +157 -0
  131. package/src/util/datetime.ts +69 -0
  132. package/src/util/env.ts +65 -0
  133. package/src/util/ids.ts +19 -0
  134. package/src/util/lru.ts +48 -0
  135. package/src/util/regex.ts +165 -0
@@ -0,0 +1,303 @@
1
+ import type { Database } from "bun:sqlite";
2
+
3
+ export type JsonPrimitive = string | number | boolean | null;
4
+ export type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue };
5
+ export type Metadata = Record<string, JsonValue>;
6
+
7
+ export type MemoryScope = "global" | "session" | "channel" | string;
8
+ export type TrustTier = "STATED" | "OBSERVED" | "INFERRED" | "SYSTEM" | string;
9
+ export type Veracity =
10
+ | "unknown"
11
+ | "likely_true"
12
+ | "true"
13
+ | "false"
14
+ | "stated"
15
+ | "inferred"
16
+ | "tool"
17
+ | "imported"
18
+ | "contested"
19
+ | string;
20
+
21
+ export interface BeamPluginManager {
22
+ emit?(event: BeamEvent): void | Promise<void>;
23
+ close?(): void | Promise<void>;
24
+ }
25
+
26
+ export interface AnnotationStoreLike {
27
+ add?(memoryId: string, kind: string, value: string, options?: AnnotationWriteOptions): unknown;
28
+ addMany?(memoryId: string, kind: string, values: readonly string[], options?: AnnotationWriteOptions): unknown;
29
+ queryByMemory?(memoryId: string, kind?: string): unknown;
30
+ queryByKind?(kind: string, value?: string): unknown;
31
+ getDistinctValues?(kind: string): string[];
32
+ }
33
+
34
+ export interface TripleStoreLike {
35
+ add?(subject: string, predicate: string, object: string, options?: TripleWriteOptions): unknown;
36
+ query?(subject?: string, predicate?: string, asOf?: string): unknown;
37
+ }
38
+
39
+ export interface BeamCaches {
40
+ timestampParse: Map<string, Date>;
41
+ polyphonicEngine?: unknown;
42
+ extractionClient?: unknown;
43
+ extractionBuffer: unknown[];
44
+ [key: string]: unknown;
45
+ }
46
+
47
+ export interface BeamConfig {
48
+ workingMemoryLimit: number;
49
+ workingMemoryTtlHours: number;
50
+ recencyHalflifeHours: number;
51
+ vecWeight: number;
52
+ ftsWeight: number;
53
+ importanceWeight: number;
54
+ useCloud: boolean;
55
+ localLlmEnabled: boolean;
56
+ maxEpisodeChars: number;
57
+ proactiveLinking?: boolean;
58
+ }
59
+
60
+ export interface BeamMemoryOptions {
61
+ sessionId?: string;
62
+ dbPath?: string;
63
+ authorId?: string | null;
64
+ authorType?: string | null;
65
+ channelId?: string | null;
66
+ useCloud?: boolean;
67
+ proactiveLinking?: boolean;
68
+ eventEmitter?: (event: BeamEvent) => void;
69
+ pluginManager?: BeamPluginManager | null;
70
+ annotations?: AnnotationStoreLike | null;
71
+ triples?: TripleStoreLike | null;
72
+ config?: Partial<BeamConfig>;
73
+ }
74
+
75
+ export interface BeamMemoryState {
76
+ db: Database;
77
+ dbPath?: string;
78
+ sessionId: string;
79
+ authorId: string | null;
80
+ authorType: string | null;
81
+ channelId: string;
82
+ useCloud: boolean;
83
+ eventEmitter?: (event: BeamEvent) => void;
84
+ pluginManager: BeamPluginManager | null;
85
+ annotations: AnnotationStoreLike | null;
86
+ triples: TripleStoreLike | null;
87
+ episodicGraph: unknown | null;
88
+ veracityConsolidator: unknown | null;
89
+ caches: BeamCaches;
90
+ config: BeamConfig;
91
+ /** Tracks in-flight background fact-extraction tasks scheduled by `remember(..., { extract: true })`. */
92
+ pendingExtractions?: Set<Promise<void>>;
93
+ }
94
+
95
+ export interface AnnotationWriteOptions {
96
+ source?: string;
97
+ confidence?: number;
98
+ }
99
+
100
+ export interface TripleWriteOptions {
101
+ validFrom?: string;
102
+ source?: string;
103
+ confidence?: number;
104
+ }
105
+
106
+ export interface BeamEvent {
107
+ type: string;
108
+ memoryId?: string;
109
+ content?: string;
110
+ source?: string;
111
+ importance?: number;
112
+ sessionId: string;
113
+ timestamp: string;
114
+ metadata?: Metadata;
115
+ }
116
+
117
+ export interface RememberOptions {
118
+ source?: string;
119
+ importance?: number;
120
+ metadata?: Metadata | null;
121
+ extract?: boolean;
122
+ extractEntities?: boolean;
123
+ /**
124
+ * Override the text passed to fact and graph extraction. When unset
125
+ * (default) extraction runs over the stored `content`. Use this when the
126
+ * stored memory is a multi-author transcript but only a subset (e.g. the
127
+ * user-authored turns) should drive deterministic fact extraction — see
128
+ * coding-agent issue #3372 where assistant prose was being mis-attributed
129
+ * as user `Instruction:` memories.
130
+ */
131
+ extractText?: string;
132
+ /**
133
+ * Override the text passed to embeddings and FTS indexing. Stored `content`
134
+ * remains unchanged; when unset, embeddings and FTS use `content`.
135
+ */
136
+ embedText?: string;
137
+ veracity?: Veracity;
138
+ memoryType?: string;
139
+ scope?: MemoryScope;
140
+ trustTier?: TrustTier;
141
+ timestamp?: string;
142
+ }
143
+
144
+ export interface RememberBatchOptions {
145
+ extract?: boolean;
146
+ extractEntities?: boolean;
147
+ veracity?: Veracity;
148
+ memoryType?: string;
149
+ scope?: MemoryScope;
150
+ trustTier?: TrustTier;
151
+ }
152
+
153
+ export interface RememberBatchItem extends RememberOptions {
154
+ content: string;
155
+ }
156
+
157
+ export interface RecallOptions {
158
+ fromDate?: string | null;
159
+ toDate?: string | null;
160
+ authorId?: string | null;
161
+ authorType?: string | null;
162
+ channelId?: string | null;
163
+ includeWorking?: boolean;
164
+ queryTime?: string | Date | null;
165
+ temporalWeight?: number;
166
+ temporalHalflife?: number;
167
+ vecWeight?: number;
168
+ ftsWeight?: number;
169
+ importanceWeight?: number;
170
+ queryEmbedding?: readonly number[] | null;
171
+ useSynonyms?: boolean;
172
+ useIntent?: boolean;
173
+ useMmr?: boolean;
174
+ mmrLambda?: number;
175
+ /**
176
+ * Maximum characters of `content` returned per {@link RecallResult}. When the
177
+ * stored content exceeds this, the preview is clipped and the trailing
178
+ * character is replaced with `…` so callers can see it was truncated. The
179
+ * full row is always reachable via {@link BeamMemoryState.get}. `0` or a
180
+ * negative value disables clipping. Defaults to
181
+ * {@link RECALL_CONTENT_PREVIEW_CHARS} (500).
182
+ */
183
+ contentPreviewChars?: number;
184
+ }
185
+
186
+ export interface RecallEnhancedOptions extends RecallOptions {
187
+ useCache?: boolean;
188
+ includeFacts?: boolean;
189
+ }
190
+
191
+ export interface MemoryRow {
192
+ id: string;
193
+ content: string;
194
+ source: string | null;
195
+ timestamp: string | null;
196
+ session_id: string;
197
+ importance: number;
198
+ metadata_json: string | null;
199
+ veracity: Veracity;
200
+ memory_type?: string | null;
201
+ recall_count?: number;
202
+ last_recalled?: string | null;
203
+ valid_until?: string | null;
204
+ superseded_by?: string | null;
205
+ scope?: MemoryScope;
206
+ author_id?: string | null;
207
+ author_type?: string | null;
208
+ channel_id?: string | null;
209
+ trust_tier?: TrustTier;
210
+ validator?: string | null;
211
+ validated_at?: string | null;
212
+ validation_count?: number;
213
+ event_date?: string | null;
214
+ event_date_precision?: string | null;
215
+ temporal_tags?: string | null;
216
+ corrected_by?: number | null;
217
+ created_at: string;
218
+ }
219
+
220
+ export interface WorkingMemoryRow extends MemoryRow {
221
+ consolidated_at?: string | null;
222
+ }
223
+
224
+ export interface EpisodicMemoryRow extends MemoryRow {
225
+ rowid: number;
226
+ summary_of: string;
227
+ tier: number;
228
+ degraded_at?: string | null;
229
+ binary_vector?: Uint8Array | null;
230
+ }
231
+
232
+ export type RecallTierLabel = "working" | "episodic" | "fact" | string;
233
+
234
+ export interface RecallVoiceScores {
235
+ vec?: number;
236
+ fts?: number;
237
+ keyword?: number;
238
+ importance?: number;
239
+ recency_decay?: number;
240
+ temporal?: number;
241
+ [key: string]: number | undefined;
242
+ }
243
+
244
+ type RecallRowFields = Omit<Partial<EpisodicMemoryRow>, "tier"> & Partial<WorkingMemoryRow>;
245
+
246
+ export type RecallResult = RecallRowFields & {
247
+ [key: string]: unknown;
248
+ id: string;
249
+ content: string;
250
+ /**
251
+ * True when {@link content} is a clipped preview of the stored row. The
252
+ * clip is capped at {@link RecallOptions.contentPreviewChars} (default 500)
253
+ * and the last character is replaced with `…`. Fetch the full row via
254
+ * `memory://<id>` (mnemopi backend) or the `Mnemopi.get(id)` API.
255
+ */
256
+ truncated?: boolean;
257
+ /** Original character count of `content` before {@link truncated} clipping. */
258
+ full_length?: number;
259
+ score?: number;
260
+ distance?: number;
261
+ rank?: number;
262
+ tier?: RecallTierLabel;
263
+ tier_label?: RecallTierLabel;
264
+ degradation_tier?: number;
265
+ keyword_score?: number;
266
+ dense_score?: number;
267
+ fts_score?: number;
268
+ importance_score?: number;
269
+ recency_score?: number;
270
+ temporal_score?: number;
271
+ explanation?: string;
272
+ voice_scores?: RecallVoiceScores;
273
+ metadata?: Metadata;
274
+ };
275
+
276
+ export interface BeamStats {
277
+ count: number;
278
+ by_source?: Record<string, number>;
279
+ by_session?: Record<string, number>;
280
+ oldest?: string | null;
281
+ newest?: string | null;
282
+ [key: string]: JsonValue | Record<string, number> | undefined;
283
+ }
284
+
285
+ export interface MemoriaRetrieveResult {
286
+ ability: string;
287
+ query: string;
288
+ results: unknown[];
289
+ }
290
+
291
+ export interface SleepResult {
292
+ dry_run: boolean;
293
+ sessions?: Record<string, unknown>;
294
+ items_consolidated?: number;
295
+ [key: string]: unknown;
296
+ }
297
+
298
+ export interface ImportStats {
299
+ working_memory: Record<string, number>;
300
+ episodic_memory: Record<string, number>;
301
+ scratchpad: Record<string, number>;
302
+ consolidation_log: Record<string, number>;
303
+ }
@@ -0,0 +1,317 @@
1
+ import type { Database } from "bun:sqlite";
2
+
3
+ import { embeddingDim, type VecType } from "../config";
4
+ import { closeQuietly, type DatabasePath, openDatabase } from "../db";
5
+
6
+ export { cosineSimilarity } from "./vector-math";
7
+
8
+ export const BITS_PER_BYTE = 8;
9
+ export const EMBEDDING_DIM = embeddingDim();
10
+ export const BYTES_PER_VECTOR = Math.ceil(EMBEDDING_DIM / BITS_PER_BYTE);
11
+
12
+ const POPCOUNT_TABLE = new Uint8Array(256);
13
+ for (let i = 0; i < POPCOUNT_TABLE.length; i += 1) {
14
+ let value = i;
15
+ let count = 0;
16
+ while (value !== 0) {
17
+ value &= value - 1;
18
+ count += 1;
19
+ }
20
+ POPCOUNT_TABLE[i] = count;
21
+ }
22
+
23
+ export interface BinaryVectorSearchResult {
24
+ memory_id: string;
25
+ distance: number;
26
+ score: number;
27
+ }
28
+
29
+ export interface BinaryVectorStats {
30
+ total_vectors: number;
31
+ avg_bytes_per_vector: number;
32
+ max_bytes: number;
33
+ min_bytes: number;
34
+ compression_ratio: number;
35
+ theoretical_size_mb: number;
36
+ }
37
+
38
+ export interface BinaryVectorStoreOptions {
39
+ dbPath?: DatabasePath;
40
+ tableName?: string;
41
+ conn?: Database;
42
+ }
43
+
44
+ interface VectorRow {
45
+ memory_id: string;
46
+ binary_vector: Uint8Array | ArrayBuffer | Buffer;
47
+ original_dim: number | null;
48
+ magnitude: number | null;
49
+ }
50
+
51
+ interface StatsRow {
52
+ count: number;
53
+ avg_bytes: number | null;
54
+ max_bytes: number | null;
55
+ min_bytes: number | null;
56
+ }
57
+
58
+ function assertSqlIdentifier(name: string): string {
59
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
60
+ throw new Error(`Invalid SQL identifier: ${name}`);
61
+ }
62
+ return name;
63
+ }
64
+
65
+ function toFiniteNumber(value: number | string | boolean | null | undefined): number {
66
+ const n = Number(value ?? 0);
67
+ return Number.isFinite(n) ? n : 0;
68
+ }
69
+
70
+ function magnitude(embedding: readonly number[]): number {
71
+ let sum = 0;
72
+ for (let i = 0; i < embedding.length; i += 1) {
73
+ const value = toFiniteNumber(embedding[i]);
74
+ sum += value * value;
75
+ }
76
+ return Math.sqrt(sum);
77
+ }
78
+
79
+ function bytesFromBlob(blob: Uint8Array | ArrayBuffer | Buffer): Uint8Array {
80
+ if (blob instanceof Uint8Array) {
81
+ return blob;
82
+ }
83
+ return new Uint8Array(blob);
84
+ }
85
+
86
+ function isReadonlyMap(
87
+ value: ReadonlyMap<string, Uint8Array | ArrayBuffer> | Record<string, Uint8Array | ArrayBuffer>,
88
+ ): value is ReadonlyMap<string, Uint8Array | ArrayBuffer> {
89
+ const candidate = value as Partial<ReadonlyMap<string, Uint8Array | ArrayBuffer>> & {
90
+ [Symbol.iterator]?: unknown;
91
+ };
92
+ return (
93
+ typeof candidate.get === "function" &&
94
+ typeof candidate.has === "function" &&
95
+ typeof candidate.forEach === "function" &&
96
+ typeof candidate.size === "number" &&
97
+ typeof candidate[Symbol.iterator] === "function"
98
+ );
99
+ }
100
+
101
+ export function getVecType(env: NodeJS.ProcessEnv = process.env): VecType {
102
+ const value = (env.MNEMOPI_VEC_TYPE ?? "int8").trim().toLowerCase();
103
+ if (value === "float32" || value === "int8" || value === "bit") {
104
+ return value;
105
+ }
106
+ return "float32";
107
+ }
108
+
109
+ export const VEC_TYPE: VecType = getVecType();
110
+
111
+ export function quantizeInt8(embedding: readonly number[]): Int8Array {
112
+ const out = new Int8Array(embedding.length);
113
+ for (let i = 0; i < embedding.length; i += 1) {
114
+ const value = Math.max(-1, Math.min(1, toFiniteNumber(embedding[i])));
115
+ out[i] = value >= 0 ? Math.round(value * 127) : -Math.round(-value * 127);
116
+ }
117
+ return out;
118
+ }
119
+
120
+ export function maximallyInformativeBinarization(embedding: readonly number[]): Uint8Array {
121
+ const dim = Math.min(embedding.length, EMBEDDING_DIM);
122
+ const nBytes = Math.ceil(dim / BITS_PER_BYTE);
123
+ const out = new Uint8Array(nBytes);
124
+ for (let i = 0; i < dim; i += 1) {
125
+ if (toFiniteNumber(embedding[i]) > 0) {
126
+ const byteIndex = i >> 3;
127
+ out[byteIndex] = (out[byteIndex] ?? 0) | (1 << (7 - (i & 7)));
128
+ }
129
+ }
130
+ return out;
131
+ }
132
+
133
+ export function hammingDistance(binaryA: Uint8Array | ArrayBuffer, binaryB: Uint8Array | ArrayBuffer): number {
134
+ const a = binaryA instanceof Uint8Array ? binaryA : new Uint8Array(binaryA);
135
+ const b = binaryB instanceof Uint8Array ? binaryB : new Uint8Array(binaryB);
136
+ const shared = Math.min(a.length, b.length);
137
+ let distance = 0;
138
+ for (let i = 0; i < shared; i += 1) {
139
+ distance += POPCOUNT_TABLE[(a[i] ?? 0) ^ (b[i] ?? 0)] ?? 0;
140
+ }
141
+ for (let i = shared; i < a.length; i += 1) {
142
+ distance += POPCOUNT_TABLE[a[i] ?? 0] ?? 0;
143
+ }
144
+ for (let i = shared; i < b.length; i += 1) {
145
+ distance += POPCOUNT_TABLE[b[i] ?? 0] ?? 0;
146
+ }
147
+ return distance;
148
+ }
149
+
150
+ function hammingDistanceForDimension(
151
+ binaryA: Uint8Array | ArrayBuffer,
152
+ binaryB: Uint8Array | ArrayBuffer,
153
+ dim: number,
154
+ ): number {
155
+ const a = binaryA instanceof Uint8Array ? binaryA : new Uint8Array(binaryA);
156
+ const b = binaryB instanceof Uint8Array ? binaryB : new Uint8Array(binaryB);
157
+ const effectiveDim = Math.max(0, Math.trunc(dim));
158
+ const wholeBytes = effectiveDim >> 3;
159
+ let distance = 0;
160
+ for (let i = 0; i < wholeBytes; i += 1) {
161
+ distance += POPCOUNT_TABLE[(a[i] ?? 0) ^ (b[i] ?? 0)] ?? 0;
162
+ }
163
+ const remainingBits = effectiveDim & 7;
164
+ if (remainingBits > 0) {
165
+ const mask = (0xff << (BITS_PER_BYTE - remainingBits)) & 0xff;
166
+ distance += POPCOUNT_TABLE[((a[wholeBytes] ?? 0) ^ (b[wholeBytes] ?? 0)) & mask] ?? 0;
167
+ }
168
+ return distance;
169
+ }
170
+
171
+ export function informationTheoreticScore(distance: number, dim: number = EMBEDDING_DIM): number {
172
+ if (dim <= 0) {
173
+ return 0;
174
+ }
175
+ return 1.0 - distance / dim;
176
+ }
177
+
178
+ export class BinaryVectorStore {
179
+ readonly conn: Database;
180
+ readonly dbPath: DatabasePath;
181
+ readonly tableName: string;
182
+ private readonly ownsConnection: boolean;
183
+
184
+ constructor(options: BinaryVectorStoreOptions = {}) {
185
+ this.dbPath = options.dbPath ?? ":memory:";
186
+ this.tableName = assertSqlIdentifier(options.tableName ?? "binary_vectors");
187
+ this.conn = options.conn ?? openDatabase(this.dbPath, { create: true, readwrite: true });
188
+ this.ownsConnection = options.conn === undefined;
189
+ this.initTable();
190
+ }
191
+
192
+ private initTable(): void {
193
+ this.conn.exec(`
194
+ CREATE TABLE IF NOT EXISTS ${this.tableName} (
195
+ memory_id TEXT PRIMARY KEY,
196
+ binary_vector BLOB NOT NULL,
197
+ original_dim INTEGER DEFAULT ${EMBEDDING_DIM},
198
+ magnitude REAL DEFAULT 1.0,
199
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
200
+ )
201
+ `);
202
+ }
203
+
204
+ static maximallyInformativeBinarization(embedding: readonly number[]): Uint8Array {
205
+ return maximallyInformativeBinarization(embedding);
206
+ }
207
+ static hammingDistance(binaryA: Uint8Array | ArrayBuffer, binaryB: Uint8Array | ArrayBuffer): number {
208
+ return hammingDistance(binaryA, binaryB);
209
+ }
210
+ static informationTheoreticScore(distance: number, dim: number = EMBEDDING_DIM): number {
211
+ return informationTheoreticScore(distance, dim);
212
+ }
213
+ storeVector(memoryId: string, embedding: readonly number[]): void {
214
+ const binary = maximallyInformativeBinarization(embedding);
215
+ this.conn
216
+ .query(
217
+ `INSERT OR REPLACE INTO ${this.tableName}
218
+ (memory_id, binary_vector, original_dim, magnitude)
219
+ VALUES (?, ?, ?, ?)`,
220
+ )
221
+ .run(memoryId, binary, Math.min(embedding.length, EMBEDDING_DIM), magnitude(embedding));
222
+ }
223
+ search(queryEmbedding: readonly number[], topK = 10): BinaryVectorSearchResult[] {
224
+ const queryDim = Math.min(queryEmbedding.length, EMBEDDING_DIM);
225
+ const queryBinary = maximallyInformativeBinarization(queryEmbedding);
226
+ const rows = this.conn
227
+ .query(`SELECT memory_id, binary_vector, original_dim, magnitude FROM ${this.tableName}`)
228
+ .all() as VectorRow[];
229
+ const results: BinaryVectorSearchResult[] = [];
230
+ for (const row of rows) {
231
+ const storedDim = Math.max(0, Math.min(EMBEDDING_DIM, Math.trunc(toFiniteNumber(row.original_dim))));
232
+ const comparedDim = Math.min(queryDim, storedDim);
233
+ const distance = hammingDistanceForDimension(queryBinary, bytesFromBlob(row.binary_vector), comparedDim);
234
+ results.push({
235
+ memory_id: row.memory_id,
236
+ distance,
237
+ score: informationTheoreticScore(distance, comparedDim),
238
+ });
239
+ }
240
+ results.sort((a, b) => b.score - a.score || a.memory_id.localeCompare(b.memory_id));
241
+ return results.slice(0, Math.max(0, Math.trunc(topK)));
242
+ }
243
+
244
+ searchBatch(queryEmbeddings: readonly (readonly number[])[], topK = 10): BinaryVectorSearchResult[][] {
245
+ return queryEmbeddings.map(embedding => this.search(embedding, topK));
246
+ }
247
+ deleteVector(memoryId: string): void {
248
+ this.conn.query(`DELETE FROM ${this.tableName} WHERE memory_id = ?`).run(memoryId);
249
+ }
250
+ getStats(): BinaryVectorStats {
251
+ const row = this.conn
252
+ .query(
253
+ `SELECT COUNT(*) AS count,
254
+ AVG(LENGTH(binary_vector)) AS avg_bytes,
255
+ MAX(LENGTH(binary_vector)) AS max_bytes,
256
+ MIN(LENGTH(binary_vector)) AS min_bytes
257
+ FROM ${this.tableName}`,
258
+ )
259
+ .get() as StatsRow;
260
+ const count = row.count;
261
+ const bytesPerVector = row.avg_bytes ?? 0;
262
+ return {
263
+ total_vectors: count,
264
+ avg_bytes_per_vector: bytesPerVector,
265
+ max_bytes: row.max_bytes ?? 0,
266
+ min_bytes: row.min_bytes ?? 0,
267
+ compression_ratio: BYTES_PER_VECTOR / (EMBEDDING_DIM * 4),
268
+ theoretical_size_mb: (count * BYTES_PER_VECTOR) / (1024 * 1024),
269
+ };
270
+ }
271
+ close(): void {
272
+ if (this.ownsConnection) {
273
+ closeQuietly(this.conn);
274
+ }
275
+ }
276
+ }
277
+
278
+ export class FastBinarySearch {
279
+ private readonly memoryIds: string[];
280
+ private readonly vectors: Uint8Array[];
281
+
282
+ constructor(
283
+ binaryVectors: ReadonlyMap<string, Uint8Array | ArrayBuffer> | Record<string, Uint8Array | ArrayBuffer>,
284
+ ) {
285
+ this.memoryIds = [];
286
+ this.vectors = [];
287
+ if (isReadonlyMap(binaryVectors)) {
288
+ for (const [memoryId, vector] of binaryVectors) {
289
+ this.memoryIds.push(memoryId);
290
+ this.vectors.push(vector instanceof Uint8Array ? vector : new Uint8Array(vector));
291
+ }
292
+ } else {
293
+ for (const memoryId in binaryVectors) {
294
+ const vector = binaryVectors[memoryId];
295
+ if (vector !== undefined) {
296
+ this.memoryIds.push(memoryId);
297
+ this.vectors.push(vector instanceof Uint8Array ? vector : new Uint8Array(vector));
298
+ }
299
+ }
300
+ }
301
+ }
302
+
303
+ search(queryBinary: Uint8Array | ArrayBuffer, topK = 10): BinaryVectorSearchResult[] {
304
+ const query = queryBinary instanceof Uint8Array ? queryBinary : new Uint8Array(queryBinary);
305
+ const results: BinaryVectorSearchResult[] = [];
306
+ for (let i = 0; i < this.vectors.length; i += 1) {
307
+ const distance = hammingDistance(query, this.vectors[i] ?? new Uint8Array());
308
+ results.push({
309
+ memory_id: this.memoryIds[i] ?? "",
310
+ distance,
311
+ score: informationTheoreticScore(distance),
312
+ });
313
+ }
314
+ results.sort((a, b) => a.distance - b.distance || a.memory_id.localeCompare(b.memory_id));
315
+ return results.slice(0, Math.max(0, Math.trunc(topK)));
316
+ }
317
+ }