opencode-mem 2.11.8 → 2.11.9
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/README.md +8 -12
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -0
- package/dist/services/api-handlers.js +6 -6
- package/dist/services/cleanup-service.js +1 -1
- package/dist/services/client.js +1 -1
- package/dist/services/deduplication-service.js +1 -1
- package/dist/services/migration-service.js +3 -3
- package/dist/services/sqlite/shard-manager.d.ts +1 -1
- package/dist/services/sqlite/shard-manager.d.ts.map +1 -1
- package/dist/services/sqlite/shard-manager.js +12 -1
- package/dist/services/sqlite/vector-search.d.ts +8 -4
- package/dist/services/sqlite/vector-search.d.ts.map +1 -1
- package/dist/services/sqlite/vector-search.js +107 -44
- package/dist/services/vector-backends/backend-factory.d.ts +3 -0
- package/dist/services/vector-backends/backend-factory.d.ts.map +1 -0
- package/dist/services/vector-backends/backend-factory.js +104 -0
- package/dist/services/vector-backends/exact-scan-backend.d.ts +39 -0
- package/dist/services/vector-backends/exact-scan-backend.d.ts.map +1 -0
- package/dist/services/vector-backends/exact-scan-backend.js +63 -0
- package/dist/services/vector-backends/types.d.ts +51 -0
- package/dist/services/vector-backends/types.d.ts.map +1 -0
- package/dist/services/vector-backends/types.js +1 -0
- package/dist/services/vector-backends/usearch-backend.d.ts +47 -0
- package/dist/services/vector-backends/usearch-backend.d.ts.map +1 -0
- package/dist/services/vector-backends/usearch-backend.js +174 -0
- package/package.json +3 -3
- package/dist/services/sqlite/hnsw-index.d.ts +0 -37
- package/dist/services/sqlite/hnsw-index.d.ts.map +0 -1
- package/dist/services/sqlite/hnsw-index.js +0 -235
package/README.md
CHANGED
|
@@ -20,25 +20,21 @@ A persistent memory system for AI coding agents that enables long-term context r
|
|
|
20
20
|
|
|
21
21
|
## Core Features
|
|
22
22
|
|
|
23
|
-
Local vector database with SQLite +
|
|
23
|
+
Local vector database with SQLite + USearch-first vector indexing and ExactScan fallback, persistent project memories, automatic user profile learning, unified memory-prompt timeline, full-featured web UI, intelligent prompt-based memory extraction, multi-provider AI support (OpenAI, Anthropic), 12+ local embedding models, smart deduplication, and built-in privacy protection.
|
|
24
24
|
|
|
25
25
|
## Prerequisites
|
|
26
26
|
|
|
27
|
-
This plugin uses `
|
|
27
|
+
This plugin uses `USearch` for preferred in-memory vector indexing with automatic ExactScan fallback. No custom SQLite build or browser runtime shim is required.
|
|
28
28
|
|
|
29
|
-
**
|
|
29
|
+
**Recommended runtime:**
|
|
30
30
|
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
- `make` or CMake
|
|
31
|
+
- Bun
|
|
32
|
+
- Standard OpenCode plugin environment
|
|
34
33
|
|
|
35
|
-
**
|
|
34
|
+
**Notes:**
|
|
36
35
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
| **macOS** | Xcode Command Line Tools: `xcode-select --install` |
|
|
40
|
-
| **Linux** | Build essentials: `sudo apt install build-essential python3` (Debian/Ubuntu) or `sudo pacman -S base-devel python` (Arch) |
|
|
41
|
-
| **Windows** | Visual Studio Build Tools with C++ workload, or Windows Build Tools: `npm install -g windows-build-tools` |
|
|
36
|
+
- If `USearch` is unavailable or fails at runtime, the plugin automatically falls back to exact vector scanning.
|
|
37
|
+
- SQLite remains the source of truth; search indexes are rebuilt from SQLite data when needed.
|
|
42
38
|
|
|
43
39
|
## Getting Started
|
|
44
40
|
|
package/dist/config.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ export declare const CONFIG: {
|
|
|
20
20
|
memoryApiUrl: string | undefined;
|
|
21
21
|
memoryApiKey: string | undefined;
|
|
22
22
|
memoryTemperature: number | false | undefined;
|
|
23
|
+
vectorBackend: "usearch-first" | "usearch" | "exact-scan";
|
|
23
24
|
aiSessionRetentionDays: number;
|
|
24
25
|
webServerEnabled: boolean;
|
|
25
26
|
webServerPort: number;
|
package/dist/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAoaA,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;oBAwBb,aAAa,GACb,kBAAkB,GAClB,WAAW;;;;;mBAMX,eAAe,GACf,SAAS,GACT,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAoCV,OAAO,GACP,QAAQ;;CAEf,CAAC;AAEF,wBAAgB,YAAY,IAAI,OAAO,CAEtC"}
|
package/dist/config.js
CHANGED
|
@@ -27,6 +27,7 @@ const DEFAULTS = {
|
|
|
27
27
|
autoCaptureEnabled: true,
|
|
28
28
|
autoCaptureMaxIterations: 5,
|
|
29
29
|
autoCaptureIterationTimeout: 30000,
|
|
30
|
+
vectorBackend: "usearch-first",
|
|
30
31
|
aiSessionRetentionDays: 7,
|
|
31
32
|
webServerEnabled: true,
|
|
32
33
|
webServerPort: 4747,
|
|
@@ -347,6 +348,7 @@ export const CONFIG = {
|
|
|
347
348
|
memoryApiUrl: fileConfig.memoryApiUrl,
|
|
348
349
|
memoryApiKey: resolveSecretValue(fileConfig.memoryApiKey),
|
|
349
350
|
memoryTemperature: fileConfig.memoryTemperature,
|
|
351
|
+
vectorBackend: (fileConfig.vectorBackend ?? "usearch-first"),
|
|
350
352
|
aiSessionRetentionDays: fileConfig.aiSessionRetentionDays ?? DEFAULTS.aiSessionRetentionDays,
|
|
351
353
|
webServerEnabled: fileConfig.webServerEnabled ?? DEFAULTS.webServerEnabled,
|
|
352
354
|
webServerPort: fileConfig.webServerPort ?? DEFAULTS.webServerPort,
|
|
@@ -258,7 +258,7 @@ export async function handleAddMemory(data) {
|
|
|
258
258
|
metadata: JSON.stringify({ source: "api" }),
|
|
259
259
|
};
|
|
260
260
|
const db = connectionManager.getConnection(shard.dbPath);
|
|
261
|
-
vectorSearch.insertVector(db, record, shard);
|
|
261
|
+
await vectorSearch.insertVector(db, record, shard);
|
|
262
262
|
shardManager.incrementVectorCount(shard.id);
|
|
263
263
|
return { success: true, data: { id } };
|
|
264
264
|
}
|
|
@@ -360,7 +360,7 @@ export async function handleUpdateMemory(id, data) {
|
|
|
360
360
|
projectName: existingMemory.project_name,
|
|
361
361
|
gitRepoUrl: existingMemory.git_repo_url,
|
|
362
362
|
};
|
|
363
|
-
vectorSearch.insertVector(db, updatedRecord, foundShard);
|
|
363
|
+
await vectorSearch.insertVector(db, updatedRecord, foundShard);
|
|
364
364
|
shardManager.incrementVectorCount(foundShard.id);
|
|
365
365
|
return { success: true };
|
|
366
366
|
}
|
|
@@ -870,12 +870,12 @@ export async function handleRunTagMigrationBatch(batchSize = 5) {
|
|
|
870
870
|
}
|
|
871
871
|
}
|
|
872
872
|
const vector = await embeddingService.embedWithTimeout(m.content);
|
|
873
|
+
const tagsVector = currentTags.length
|
|
874
|
+
? await embeddingService.embedWithTimeout(currentTags.join(", "))
|
|
875
|
+
: undefined;
|
|
873
876
|
const vectorBuffer = new Uint8Array(vector.buffer);
|
|
874
877
|
db.prepare("UPDATE memories SET vector = ?, updated_at = ? WHERE id = ?").run(vectorBuffer, Date.now(), m.id);
|
|
875
|
-
|
|
876
|
-
.getIndexManager()
|
|
877
|
-
.getIndex(shard.scope, shard.scopeHash, shard.shardIndex);
|
|
878
|
-
await index.insert(m.id, vector);
|
|
878
|
+
await vectorSearch.updateVector(db, m.id, vector, shard, tagsVector);
|
|
879
879
|
migrationProgress.processed++;
|
|
880
880
|
batchProcessed++;
|
|
881
881
|
}
|
|
@@ -61,7 +61,7 @@ export class CleanupService {
|
|
|
61
61
|
if (protectedMemoryIds.has(memory.id)) {
|
|
62
62
|
continue;
|
|
63
63
|
}
|
|
64
|
-
vectorSearch.deleteVector(db, memory.id);
|
|
64
|
+
await vectorSearch.deleteVector(db, memory.id, shard);
|
|
65
65
|
shardManager.decrementVectorCount(shard.id);
|
|
66
66
|
totalDeleted++;
|
|
67
67
|
if (memory.container_tag?.includes("_user_")) {
|
package/dist/services/client.js
CHANGED
|
@@ -128,7 +128,7 @@ export class LocalMemoryClient {
|
|
|
128
128
|
metadata: Object.keys(dynamicMetadata).length > 0 ? JSON.stringify(dynamicMetadata) : undefined,
|
|
129
129
|
};
|
|
130
130
|
const db = connectionManager.getConnection(shard.dbPath);
|
|
131
|
-
vectorSearch.insertVector(db, record, shard);
|
|
131
|
+
await vectorSearch.insertVector(db, record, shard);
|
|
132
132
|
shardManager.incrementVectorCount(shard.id);
|
|
133
133
|
return { success: true, id };
|
|
134
134
|
}
|
|
@@ -36,7 +36,7 @@ export class DeduplicationService {
|
|
|
36
36
|
const toDelete = duplicates.slice(1);
|
|
37
37
|
for (const dup of toDelete) {
|
|
38
38
|
try {
|
|
39
|
-
vectorSearch.deleteVector(db, dup.id);
|
|
39
|
+
await vectorSearch.deleteVector(db, dup.id, shard);
|
|
40
40
|
shardManager.decrementVectorCount(shard.id);
|
|
41
41
|
exactDeleted++;
|
|
42
42
|
}
|
|
@@ -105,7 +105,7 @@ export class MigrationService {
|
|
|
105
105
|
total: mismatch.shardMismatches.length,
|
|
106
106
|
currentShard: String(shardInfo.shardId),
|
|
107
107
|
});
|
|
108
|
-
shardManager.deleteShard(shardInfo.shardId);
|
|
108
|
+
await shardManager.deleteShard(shardInfo.shardId);
|
|
109
109
|
deletedShards++;
|
|
110
110
|
}
|
|
111
111
|
catch (error) {
|
|
@@ -168,7 +168,7 @@ export class MigrationService {
|
|
|
168
168
|
isPinned: memory.is_pinned || 0,
|
|
169
169
|
});
|
|
170
170
|
}
|
|
171
|
-
shardManager.deleteShard(shardInfo.shardId);
|
|
171
|
+
await shardManager.deleteShard(shardInfo.shardId);
|
|
172
172
|
for (const memory of tempMemories) {
|
|
173
173
|
try {
|
|
174
174
|
const vector = await embeddingService.embedWithTimeout(memory.content);
|
|
@@ -176,7 +176,7 @@ export class MigrationService {
|
|
|
176
176
|
const hash = memory.containerTag.split("_").slice(2).join("_");
|
|
177
177
|
const newShard = shardManager.getWriteShard(scope, hash);
|
|
178
178
|
const newDb = connectionManager.getConnection(newShard.dbPath);
|
|
179
|
-
vectorSearch.insertVector(newDb, {
|
|
179
|
+
await vectorSearch.insertVector(newDb, {
|
|
180
180
|
id: memory.id,
|
|
181
181
|
content: memory.content,
|
|
182
182
|
vector,
|
|
@@ -17,7 +17,7 @@ export declare class ShardManager {
|
|
|
17
17
|
incrementVectorCount(shardId: number): void;
|
|
18
18
|
decrementVectorCount(shardId: number): void;
|
|
19
19
|
getShardByPath(dbPath: string): ShardInfo | null;
|
|
20
|
-
deleteShard(shardId: number): void
|
|
20
|
+
deleteShard(shardId: number): Promise<void>;
|
|
21
21
|
}
|
|
22
22
|
export declare const shardManager: ShardManager;
|
|
23
23
|
//# sourceMappingURL=shard-manager.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shard-manager.d.ts","sourceRoot":"","sources":["../../../src/services/sqlite/shard-manager.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"shard-manager.d.ts","sourceRoot":"","sources":["../../../src/services/sqlite/shard-manager.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAO5C,qBAAa,YAAY;IACvB,OAAO,CAAC,UAAU,CAAe;IACjC,OAAO,CAAC,YAAY,CAAS;;IAQ7B,OAAO,CAAC,cAAc;IAqBtB,OAAO,CAAC,YAAY;IAKpB,OAAO,CAAC,iBAAiB;IAKzB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI;IAsB9E,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,SAAS,EAAE,MAAM,GAAG,SAAS,EAAE;IAgCvE,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,SAAS;IA2BxF,OAAO,CAAC,WAAW;IA8CnB,OAAO,CAAC,YAAY;IA4BpB,OAAO,CAAC,iBAAiB;IAYzB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,SAAS,EAAE,MAAM,GAAG,SAAS;IA+BtE,OAAO,CAAC,iBAAiB;IAOzB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAO3C,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAO3C,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI;IAkB1C,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAkClD;AAED,eAAO,MAAM,YAAY,cAAqB,CAAC"}
|
|
@@ -4,6 +4,7 @@ import { existsSync } from "node:fs";
|
|
|
4
4
|
import { CONFIG } from "../../config.js";
|
|
5
5
|
import { connectionManager } from "./connection-manager.js";
|
|
6
6
|
import { log } from "../logger.js";
|
|
7
|
+
import { vectorSearch } from "./vector-search.js";
|
|
7
8
|
const Database = getDatabase();
|
|
8
9
|
const METADATA_DB_NAME = "metadata.db";
|
|
9
10
|
export class ShardManager {
|
|
@@ -251,11 +252,21 @@ export class ShardManager {
|
|
|
251
252
|
createdAt: row.created_at,
|
|
252
253
|
};
|
|
253
254
|
}
|
|
254
|
-
deleteShard(shardId) {
|
|
255
|
+
async deleteShard(shardId) {
|
|
255
256
|
const stmt = this.metadataDb.prepare(`SELECT * FROM shards WHERE id = ?`);
|
|
256
257
|
const row = stmt.get(shardId);
|
|
257
258
|
if (row) {
|
|
258
259
|
const fullPath = this.resolveStoredPath(row.db_path, row.scope);
|
|
260
|
+
await vectorSearch.deleteShardIndexes({
|
|
261
|
+
id: row.id,
|
|
262
|
+
scope: row.scope,
|
|
263
|
+
scopeHash: row.scope_hash,
|
|
264
|
+
shardIndex: row.shard_index,
|
|
265
|
+
dbPath: fullPath,
|
|
266
|
+
vectorCount: row.vector_count,
|
|
267
|
+
isActive: row.is_active === 1,
|
|
268
|
+
createdAt: row.created_at,
|
|
269
|
+
});
|
|
259
270
|
connectionManager.closeConnection(fullPath);
|
|
260
271
|
try {
|
|
261
272
|
const fs = require("node:fs");
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import { HNSWIndexManager } from "./hnsw-index.js";
|
|
2
1
|
import type { MemoryRecord, SearchResult, ShardInfo } from "./types.js";
|
|
2
|
+
import type { VectorBackend } from "../vector-backends/types.js";
|
|
3
3
|
declare const Database: typeof import("bun:sqlite").Database;
|
|
4
4
|
type DatabaseType = typeof Database.prototype;
|
|
5
5
|
export declare class VectorSearch {
|
|
6
|
-
|
|
6
|
+
private readonly backendPromise;
|
|
7
|
+
private readonly fallbackBackend;
|
|
8
|
+
constructor(backend?: VectorBackend, fallbackBackend?: VectorBackend);
|
|
9
|
+
private getBackend;
|
|
10
|
+
insertVector(db: DatabaseType, record: MemoryRecord, shard?: ShardInfo): Promise<void>;
|
|
7
11
|
searchInShard(shard: ShardInfo, queryVector: Float32Array, containerTag: string, limit: number, queryText?: string): Promise<SearchResult[]>;
|
|
8
12
|
searchAcrossShards(shards: ShardInfo[], queryVector: Float32Array, containerTag: string, limit: number, similarityThreshold: number, queryText?: string): Promise<SearchResult[]>;
|
|
9
13
|
deleteVector(db: DatabaseType, memoryId: string, shard?: ShardInfo): Promise<void>;
|
|
@@ -17,8 +21,8 @@ export declare class VectorSearch {
|
|
|
17
21
|
getDistinctTags(db: DatabaseType): any[];
|
|
18
22
|
pinMemory(db: DatabaseType, memoryId: string): void;
|
|
19
23
|
unpinMemory(db: DatabaseType, memoryId: string): void;
|
|
20
|
-
|
|
21
|
-
|
|
24
|
+
rebuildIndexForShard(db: DatabaseType, scope: string, scopeHash: string, shardIndex: number): Promise<void>;
|
|
25
|
+
deleteShardIndexes(shard: ShardInfo): Promise<void>;
|
|
22
26
|
}
|
|
23
27
|
export declare const vectorSearch: VectorSearch;
|
|
24
28
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vector-search.d.ts","sourceRoot":"","sources":["../../../src/services/sqlite/vector-search.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"vector-search.d.ts","sourceRoot":"","sources":["../../../src/services/sqlite/vector-search.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAGxE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAEjE,QAAA,MAAM,QAAQ,sCAAgB,CAAC;AAC/B,KAAK,YAAY,GAAG,OAAO,QAAQ,CAAC,SAAS,CAAC;AAM9C,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAyB;IACxD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAgB;gBAEpC,OAAO,CAAC,EAAE,aAAa,EAAE,eAAe,GAAE,aAAsC;YAO9E,UAAU;IAIlB,YAAY,CAAC,EAAE,EAAE,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,KAAK,CAAC,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAyCtF,aAAa,CACjB,KAAK,EAAE,SAAS,EAChB,WAAW,EAAE,YAAY,EACzB,YAAY,EAAE,MAAM,EACpB,KAAK,EAAE,MAAM,EACb,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,YAAY,EAAE,CAAC;IAyHpB,kBAAkB,CACtB,MAAM,EAAE,SAAS,EAAE,EACnB,WAAW,EAAE,YAAY,EACzB,YAAY,EAAE,MAAM,EACpB,KAAK,EAAE,MAAM,EACb,mBAAmB,EAAE,MAAM,EAC3B,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,YAAY,EAAE,CAAC;IAiBpB,YAAY,CAAC,EAAE,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAUlF,YAAY,CAChB,EAAE,EAAE,YAAY,EAChB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,YAAY,EACpB,KAAK,CAAC,EAAE,SAAS,EACjB,UAAU,CAAC,EAAE,YAAY,GACxB,OAAO,CAAC,IAAI,CAAC;IAkBhB,YAAY,CAAC,EAAE,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,GAAG,EAAE;IAW1E,cAAc,CAAC,EAAE,EAAE,YAAY,GAAG,GAAG,EAAE;IAKvC,aAAa,CAAC,EAAE,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,GAAG,GAAG,GAAG,IAAI;IAK7D,sBAAsB,CAAC,EAAE,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,GAAG,GAAG,EAAE;IAgBlE,YAAY,CAAC,EAAE,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM;IAM5D,eAAe,CAAC,EAAE,EAAE,YAAY,GAAG,MAAM;IAMzC,eAAe,CAAC,EAAE,EAAE,YAAY,GAAG,GAAG,EAAE;IAexC,SAAS,CAAC,EAAE,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IAKnD,WAAW,CAAC,EAAE,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IAK/C,oBAAoB,CACxB,EAAE,EAAE,YAAY,EAChB,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,IAAI,CAAC;IAgBV,kBAAkB,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;CAI1D;AAED,eAAO,MAAM,YAAY,cAAqB,CAAC"}
|
|
@@ -1,45 +1,93 @@
|
|
|
1
1
|
import { getDatabase } from "./sqlite-bootstrap.js";
|
|
2
2
|
import { connectionManager } from "./connection-manager.js";
|
|
3
|
-
import { HNSWIndexManager } from "./hnsw-index.js";
|
|
4
3
|
import { log } from "../logger.js";
|
|
5
4
|
import { CONFIG } from "../../config.js";
|
|
5
|
+
import { createVectorBackend } from "../vector-backends/backend-factory.js";
|
|
6
|
+
import { ExactScanBackend } from "../vector-backends/exact-scan-backend.js";
|
|
6
7
|
const Database = getDatabase();
|
|
7
|
-
|
|
8
|
+
function toBlob(vector) {
|
|
9
|
+
return vector ? new Uint8Array(vector.buffer) : null;
|
|
10
|
+
}
|
|
8
11
|
export class VectorSearch {
|
|
9
|
-
|
|
12
|
+
backendPromise;
|
|
13
|
+
fallbackBackend;
|
|
14
|
+
constructor(backend, fallbackBackend = new ExactScanBackend()) {
|
|
15
|
+
this.backendPromise = backend
|
|
16
|
+
? Promise.resolve(backend)
|
|
17
|
+
: createVectorBackend({ vectorBackend: CONFIG.vectorBackend });
|
|
18
|
+
this.fallbackBackend = fallbackBackend;
|
|
19
|
+
}
|
|
20
|
+
async getBackend() {
|
|
21
|
+
return this.backendPromise;
|
|
22
|
+
}
|
|
23
|
+
async insertVector(db, record, shard) {
|
|
10
24
|
const insertMemory = db.prepare(`
|
|
11
25
|
INSERT INTO memories (
|
|
12
26
|
id, content, vector, tags_vector, container_tag, tags, type, created_at, updated_at,
|
|
13
27
|
metadata, display_name, user_name, user_email, project_path, project_name, git_repo_url
|
|
14
28
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
15
29
|
`);
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
if (record.tagsVector) {
|
|
25
|
-
const tagsIndex = hnswIndexManager.getTagsIndex(shard.scope, shard.scopeHash, shard.shardIndex);
|
|
26
|
-
tagsIndex.insert(record.id, record.tagsVector).catch((err) => {
|
|
27
|
-
log("HNSW tags insert error", { memoryId: record.id, error: String(err) });
|
|
28
|
-
});
|
|
30
|
+
insertMemory.run(record.id, record.content, toBlob(record.vector), toBlob(record.tagsVector), record.containerTag, record.tags || null, record.type || null, record.createdAt, record.updatedAt, record.metadata || null, record.displayName || null, record.userName || null, record.userEmail || null, record.projectPath || null, record.projectName || null, record.gitRepoUrl || null);
|
|
31
|
+
try {
|
|
32
|
+
if (shard) {
|
|
33
|
+
const backend = await this.getBackend();
|
|
34
|
+
await backend.insert({ id: record.id, vector: record.vector, shard, kind: "content" });
|
|
35
|
+
if (record.tagsVector) {
|
|
36
|
+
await backend.insert({ id: record.id, vector: record.tagsVector, shard, kind: "tags" });
|
|
37
|
+
}
|
|
29
38
|
}
|
|
30
39
|
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
db.prepare(`DELETE FROM memories WHERE id = ?`).run(record.id);
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
31
44
|
}
|
|
32
45
|
async searchInShard(shard, queryVector, containerTag, limit, queryText) {
|
|
33
46
|
const db = connectionManager.getConnection(shard.dbPath);
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
await
|
|
47
|
+
const backend = await this.getBackend();
|
|
48
|
+
let contentResults;
|
|
49
|
+
let tagsResults;
|
|
50
|
+
try {
|
|
51
|
+
await backend.rebuildFromShard({ db, shard, kind: "content" });
|
|
52
|
+
await backend.rebuildFromShard({ db, shard, kind: "tags" });
|
|
53
|
+
contentResults = await backend.search({
|
|
54
|
+
db,
|
|
55
|
+
shard,
|
|
56
|
+
kind: "content",
|
|
57
|
+
queryVector,
|
|
58
|
+
limit: limit * 4,
|
|
59
|
+
});
|
|
60
|
+
tagsResults = await backend.search({
|
|
61
|
+
db,
|
|
62
|
+
shard,
|
|
63
|
+
kind: "tags",
|
|
64
|
+
queryVector,
|
|
65
|
+
limit: limit * 4,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
log("Vector search degraded to exact scan in shard", {
|
|
70
|
+
shardId: shard.id,
|
|
71
|
+
backend: backend.getBackendName(),
|
|
72
|
+
error: String(error),
|
|
73
|
+
});
|
|
74
|
+
await this.fallbackBackend.rebuildFromShard({ db, shard, kind: "content" });
|
|
75
|
+
await this.fallbackBackend.rebuildFromShard({ db, shard, kind: "tags" });
|
|
76
|
+
contentResults = await this.fallbackBackend.search({
|
|
77
|
+
db,
|
|
78
|
+
shard,
|
|
79
|
+
kind: "content",
|
|
80
|
+
queryVector,
|
|
81
|
+
limit: limit * 4,
|
|
82
|
+
});
|
|
83
|
+
tagsResults = await this.fallbackBackend.search({
|
|
84
|
+
db,
|
|
85
|
+
shard,
|
|
86
|
+
kind: "tags",
|
|
87
|
+
queryVector,
|
|
88
|
+
limit: limit * 4,
|
|
89
|
+
});
|
|
40
90
|
}
|
|
41
|
-
const contentResults = await contentIndex.search(queryVector, limit * 4);
|
|
42
|
-
const tagsResults = await tagsIndex.search(queryVector, limit * 4);
|
|
43
91
|
const scoreMap = new Map();
|
|
44
92
|
for (const r of contentResults) {
|
|
45
93
|
scoreMap.set(r.id, { contentSim: 1 - r.distance, tagsSim: 0 });
|
|
@@ -59,7 +107,7 @@ export class VectorSearch {
|
|
|
59
107
|
const placeholders = ids.map(() => "?").join(",");
|
|
60
108
|
const rows = db
|
|
61
109
|
.prepare(`
|
|
62
|
-
SELECT * FROM memories
|
|
110
|
+
SELECT * FROM memories
|
|
63
111
|
WHERE id IN (${placeholders}) AND container_tag = ?
|
|
64
112
|
`)
|
|
65
113
|
.all(...ids, containerTag);
|
|
@@ -69,7 +117,7 @@ export class VectorSearch {
|
|
|
69
117
|
.split(/[\s,]+/)
|
|
70
118
|
.filter((w) => w.length > 1)
|
|
71
119
|
: [];
|
|
72
|
-
|
|
120
|
+
const hydratedResults = rows.map((row) => {
|
|
73
121
|
const scores = scoreMap.get(row.id);
|
|
74
122
|
const memoryTagsStr = row.tags || "";
|
|
75
123
|
const memoryTags = memoryTagsStr.split(",").map((t) => t.trim().toLowerCase());
|
|
@@ -96,6 +144,8 @@ export class VectorSearch {
|
|
|
96
144
|
isPinned: row.is_pinned,
|
|
97
145
|
};
|
|
98
146
|
});
|
|
147
|
+
hydratedResults.sort((a, b) => b.similarity - a.similarity);
|
|
148
|
+
return hydratedResults;
|
|
99
149
|
}
|
|
100
150
|
async searchAcrossShards(shards, queryVector, containerTag, limit, similarityThreshold, queryText) {
|
|
101
151
|
const shardPromises = shards.map(async (shard) => {
|
|
@@ -115,27 +165,27 @@ export class VectorSearch {
|
|
|
115
165
|
async deleteVector(db, memoryId, shard) {
|
|
116
166
|
db.prepare(`DELETE FROM memories WHERE id = ?`).run(memoryId);
|
|
117
167
|
if (shard) {
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
await
|
|
168
|
+
const backend = await this.getBackend();
|
|
169
|
+
await backend.delete({ id: memoryId, shard, kind: "content" });
|
|
170
|
+
await backend.delete({ id: memoryId, shard, kind: "tags" });
|
|
121
171
|
}
|
|
122
172
|
}
|
|
123
173
|
async updateVector(db, memoryId, vector, shard, tagsVector) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const contentIndex = hnswIndexManager.getIndex(shard.scope, shard.scopeHash, shard.shardIndex);
|
|
129
|
-
await contentIndex.insert(memoryId, vector);
|
|
174
|
+
db.prepare(`UPDATE memories SET vector = ?, tags_vector = ? WHERE id = ?`).run(toBlob(vector), toBlob(tagsVector), memoryId);
|
|
175
|
+
if (shard) {
|
|
176
|
+
const backend = await this.getBackend();
|
|
177
|
+
await backend.insert({ id: memoryId, vector, shard, kind: "content" });
|
|
130
178
|
if (tagsVector) {
|
|
131
|
-
|
|
132
|
-
|
|
179
|
+
await backend.insert({ id: memoryId, vector: tagsVector, shard, kind: "tags" });
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
await backend.delete({ id: memoryId, shard, kind: "tags" });
|
|
133
183
|
}
|
|
134
184
|
}
|
|
135
185
|
}
|
|
136
186
|
listMemories(db, containerTag, limit) {
|
|
137
187
|
const stmt = db.prepare(`
|
|
138
|
-
SELECT * FROM memories
|
|
188
|
+
SELECT * FROM memories
|
|
139
189
|
WHERE container_tag = ?
|
|
140
190
|
ORDER BY created_at DESC
|
|
141
191
|
LIMIT ?
|
|
@@ -152,7 +202,7 @@ export class VectorSearch {
|
|
|
152
202
|
}
|
|
153
203
|
getMemoriesBySessionID(db, sessionID) {
|
|
154
204
|
const stmt = db.prepare(`
|
|
155
|
-
SELECT * FROM memories
|
|
205
|
+
SELECT * FROM memories
|
|
156
206
|
WHERE metadata LIKE ?
|
|
157
207
|
ORDER BY created_at DESC
|
|
158
208
|
`);
|
|
@@ -175,7 +225,7 @@ export class VectorSearch {
|
|
|
175
225
|
}
|
|
176
226
|
getDistinctTags(db) {
|
|
177
227
|
const stmt = db.prepare(`
|
|
178
|
-
SELECT DISTINCT
|
|
228
|
+
SELECT DISTINCT
|
|
179
229
|
container_tag,
|
|
180
230
|
display_name,
|
|
181
231
|
user_name,
|
|
@@ -195,11 +245,24 @@ export class VectorSearch {
|
|
|
195
245
|
const stmt = db.prepare(`UPDATE memories SET is_pinned = 0 WHERE id = ?`);
|
|
196
246
|
stmt.run(memoryId);
|
|
197
247
|
}
|
|
198
|
-
async
|
|
199
|
-
await
|
|
248
|
+
async rebuildIndexForShard(db, scope, scopeHash, shardIndex) {
|
|
249
|
+
const backend = await this.getBackend();
|
|
250
|
+
const shard = {
|
|
251
|
+
id: 0,
|
|
252
|
+
scope: scope,
|
|
253
|
+
scopeHash,
|
|
254
|
+
shardIndex,
|
|
255
|
+
dbPath: "",
|
|
256
|
+
vectorCount: 0,
|
|
257
|
+
isActive: true,
|
|
258
|
+
createdAt: Date.now(),
|
|
259
|
+
};
|
|
260
|
+
await backend.rebuildFromShard({ db, shard, kind: "content" });
|
|
261
|
+
await backend.rebuildFromShard({ db, shard, kind: "tags" });
|
|
200
262
|
}
|
|
201
|
-
|
|
202
|
-
|
|
263
|
+
async deleteShardIndexes(shard) {
|
|
264
|
+
const backend = await this.getBackend();
|
|
265
|
+
await backend.deleteShardIndexes({ shard });
|
|
203
266
|
}
|
|
204
267
|
}
|
|
205
268
|
export const vectorSearch = new VectorSearch();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"backend-factory.d.ts","sourceRoot":"","sources":["../../../src/services/vector-backends/backend-factory.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,2BAA2B,EAAE,MAAM,YAAY,CAAC;AA4E7E,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,2BAA2B,GACnC,OAAO,CAAC,aAAa,CAAC,CAsCxB"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { CONFIG } from "../../config.js";
|
|
2
|
+
import { log } from "../logger.js";
|
|
3
|
+
import { ExactScanBackend } from "./exact-scan-backend.js";
|
|
4
|
+
import { USearchBackend } from "./usearch-backend.js";
|
|
5
|
+
class FallbackAwareBackend {
|
|
6
|
+
strategy;
|
|
7
|
+
primary;
|
|
8
|
+
fallback;
|
|
9
|
+
activeBackend;
|
|
10
|
+
constructor(strategy, primary, fallback) {
|
|
11
|
+
this.strategy = strategy;
|
|
12
|
+
this.primary = primary;
|
|
13
|
+
this.fallback = fallback;
|
|
14
|
+
this.activeBackend = primary;
|
|
15
|
+
}
|
|
16
|
+
getBackendName() {
|
|
17
|
+
return this.activeBackend.getBackendName();
|
|
18
|
+
}
|
|
19
|
+
async insert(args) {
|
|
20
|
+
await this.activeBackend.insert(args);
|
|
21
|
+
}
|
|
22
|
+
async insertBatch(args) {
|
|
23
|
+
await this.activeBackend.insertBatch(args);
|
|
24
|
+
}
|
|
25
|
+
async delete(args) {
|
|
26
|
+
await this.activeBackend.delete(args);
|
|
27
|
+
}
|
|
28
|
+
async search(args) {
|
|
29
|
+
try {
|
|
30
|
+
return await this.activeBackend.search(args);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
this.logDegrade("search", error);
|
|
34
|
+
this.activeBackend = this.fallback;
|
|
35
|
+
return this.fallback.search(args);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async rebuildFromShard(args) {
|
|
39
|
+
try {
|
|
40
|
+
await this.activeBackend.rebuildFromShard(args);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
this.logDegrade("rebuild", error);
|
|
44
|
+
this.activeBackend = this.fallback;
|
|
45
|
+
await this.fallback.rebuildFromShard(args);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async deleteShardIndexes(args) {
|
|
49
|
+
await this.primary.deleteShardIndexes(args);
|
|
50
|
+
await this.fallback.deleteShardIndexes(args);
|
|
51
|
+
}
|
|
52
|
+
logDegrade(operation, error) {
|
|
53
|
+
log("Vector backend degraded to exact-scan", {
|
|
54
|
+
strategy: this.strategy,
|
|
55
|
+
severity: this.strategy === "usearch" ? "warning" : "info",
|
|
56
|
+
operation,
|
|
57
|
+
error: String(error),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async function defaultUSearchProbe() {
|
|
62
|
+
try {
|
|
63
|
+
await import("usearch");
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export async function createVectorBackend(options) {
|
|
71
|
+
const exactScanBackend = new ExactScanBackend();
|
|
72
|
+
if (options.vectorBackend === "exact-scan") {
|
|
73
|
+
return exactScanBackend;
|
|
74
|
+
}
|
|
75
|
+
const probeUSearch = options.probeUSearch ?? defaultUSearchProbe;
|
|
76
|
+
if (!(await probeUSearch())) {
|
|
77
|
+
if (options.vectorBackend === "usearch") {
|
|
78
|
+
log("Vector backend degraded to exact-scan", {
|
|
79
|
+
strategy: "usearch",
|
|
80
|
+
severity: "warning",
|
|
81
|
+
operation: "probe",
|
|
82
|
+
error: "USearch unavailable",
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return exactScanBackend;
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
const usearchBackend = options.createUSearchBackend?.() ??
|
|
89
|
+
new USearchBackend({
|
|
90
|
+
baseDir: CONFIG.storagePath,
|
|
91
|
+
dimensions: CONFIG.embeddingDimensions,
|
|
92
|
+
});
|
|
93
|
+
return new FallbackAwareBackend(options.vectorBackend, usearchBackend, exactScanBackend);
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
log("Vector backend degraded to exact-scan", {
|
|
97
|
+
strategy: options.vectorBackend,
|
|
98
|
+
severity: options.vectorBackend === "usearch" ? "warning" : "info",
|
|
99
|
+
operation: "create",
|
|
100
|
+
error: String(error),
|
|
101
|
+
});
|
|
102
|
+
return exactScanBackend;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { BackendInsertItem, BackendSearchResult, VectorBackend, VectorBackendSearchParams, VectorKind } from "./types.js";
|
|
2
|
+
import type { ShardInfo } from "../sqlite/types.js";
|
|
3
|
+
interface RankedRow {
|
|
4
|
+
id: string;
|
|
5
|
+
vector: Float32Array;
|
|
6
|
+
}
|
|
7
|
+
export declare class ExactScanBackend implements VectorBackend {
|
|
8
|
+
getBackendName(): string;
|
|
9
|
+
rankVectors(rows: RankedRow[], queryVector: Float32Array, limit: number): BackendSearchResult[];
|
|
10
|
+
insert(_args: {
|
|
11
|
+
id: string;
|
|
12
|
+
vector: Float32Array;
|
|
13
|
+
shard: ShardInfo;
|
|
14
|
+
kind: VectorKind;
|
|
15
|
+
}): Promise<void>;
|
|
16
|
+
insertBatch(_args: {
|
|
17
|
+
items: BackendInsertItem[];
|
|
18
|
+
shard: ShardInfo;
|
|
19
|
+
kind: VectorKind;
|
|
20
|
+
}): Promise<void>;
|
|
21
|
+
delete(_args: {
|
|
22
|
+
id: string;
|
|
23
|
+
shard: ShardInfo;
|
|
24
|
+
kind: VectorKind;
|
|
25
|
+
}): Promise<void>;
|
|
26
|
+
search(args: VectorBackendSearchParams): Promise<BackendSearchResult[]>;
|
|
27
|
+
rebuildFromShard(_args: {
|
|
28
|
+
db: unknown;
|
|
29
|
+
shard: ShardInfo;
|
|
30
|
+
kind: VectorKind;
|
|
31
|
+
}): Promise<void>;
|
|
32
|
+
deleteShardIndexes(_args: {
|
|
33
|
+
shard: ShardInfo;
|
|
34
|
+
}): Promise<void>;
|
|
35
|
+
private decodeVector;
|
|
36
|
+
private cosineSimilarity;
|
|
37
|
+
}
|
|
38
|
+
export {};
|
|
39
|
+
//# sourceMappingURL=exact-scan-backend.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exact-scan-backend.d.ts","sourceRoot":"","sources":["../../../src/services/vector-backends/exact-scan-backend.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,iBAAiB,EACjB,mBAAmB,EACnB,aAAa,EACb,yBAAyB,EACzB,UAAU,EACX,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAEpD,UAAU,SAAS;IACjB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,YAAY,CAAC;CACtB;AAQD,qBAAa,gBAAiB,YAAW,aAAa;IACpD,cAAc,IAAI,MAAM;IAIxB,WAAW,CAAC,IAAI,EAAE,SAAS,EAAE,EAAE,WAAW,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,GAAG,mBAAmB,EAAE;IAUzF,MAAM,CAAC,KAAK,EAAE;QAClB,EAAE,EAAE,MAAM,CAAC;QACX,MAAM,EAAE,YAAY,CAAC;QACrB,KAAK,EAAE,SAAS,CAAC;QACjB,IAAI,EAAE,UAAU,CAAC;KAClB,GAAG,OAAO,CAAC,IAAI,CAAC;IAEX,WAAW,CAAC,KAAK,EAAE;QACvB,KAAK,EAAE,iBAAiB,EAAE,CAAC;QAC3B,KAAK,EAAE,SAAS,CAAC;QACjB,IAAI,EAAE,UAAU,CAAC;KAClB,GAAG,OAAO,CAAC,IAAI,CAAC;IAEX,MAAM,CAAC,KAAK,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,SAAS,CAAC;QAAC,IAAI,EAAE,UAAU,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAEhF,MAAM,CAAC,IAAI,EAAE,yBAAyB,GAAG,OAAO,CAAC,mBAAmB,EAAE,CAAC;IAwBvE,gBAAgB,CAAC,KAAK,EAAE;QAC5B,EAAE,EAAE,OAAO,CAAC;QACZ,KAAK,EAAE,SAAS,CAAC;QACjB,IAAI,EAAE,UAAU,CAAC;KAClB,GAAG,OAAO,CAAC,IAAI,CAAC;IAEX,kBAAkB,CAAC,KAAK,EAAE;QAAE,KAAK,EAAE,SAAS,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAEpE,OAAO,CAAC,YAAY;IAcpB,OAAO,CAAC,gBAAgB;CAuBzB"}
|