opencode-mem 2.11.2 → 2.11.4
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 +19 -1
- package/dist/services/sqlite/hnsw-index.d.ts +1 -1
- package/dist/services/sqlite/hnsw-index.d.ts.map +1 -1
- package/dist/services/sqlite/hnsw-index.js +63 -33
- package/dist/services/sqlite/vector-search.d.ts.map +1 -1
- package/dist/services/sqlite/vector-search.js +39 -15
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -22,6 +22,24 @@ A persistent memory system for AI coding agents that enables long-term context r
|
|
|
22
22
|
|
|
23
23
|
Local vector database with SQLite + HNSW (hnswlib-wasm), 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
|
+
## Prerequisites
|
|
26
|
+
|
|
27
|
+
This plugin uses `hnswlib-node` for fast vector similarity search, which requires native compilation. Ensure you have:
|
|
28
|
+
|
|
29
|
+
**All platforms:**
|
|
30
|
+
|
|
31
|
+
- Python 3.x
|
|
32
|
+
- A C++ compiler (gcc, clang, or MSVC)
|
|
33
|
+
- `make` or CMake
|
|
34
|
+
|
|
35
|
+
**Platform-specific setup:**
|
|
36
|
+
|
|
37
|
+
| Platform | Requirements |
|
|
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` |
|
|
42
|
+
|
|
25
43
|
## Getting Started
|
|
26
44
|
|
|
27
45
|
Add to your OpenCode configuration at `~/.config/opencode/opencode.json`:
|
|
@@ -32,7 +50,7 @@ Add to your OpenCode configuration at `~/.config/opencode/opencode.json`:
|
|
|
32
50
|
}
|
|
33
51
|
```
|
|
34
52
|
|
|
35
|
-
The plugin downloads automatically on next startup.
|
|
53
|
+
The plugin downloads automatically on next startup.
|
|
36
54
|
|
|
37
55
|
## Usage Examples
|
|
38
56
|
|
|
@@ -13,7 +13,6 @@ export declare class HNSWIndex {
|
|
|
13
13
|
private initialized;
|
|
14
14
|
constructor(dimensions: number, indexPath: string);
|
|
15
15
|
private ensureInitialized;
|
|
16
|
-
private vectorToArray;
|
|
17
16
|
insert(id: string, vector: Float32Array): Promise<void>;
|
|
18
17
|
insertBatch(items: HNSWIndexData[]): Promise<void>;
|
|
19
18
|
search(queryVector: Float32Array, k: number): Promise<{
|
|
@@ -29,6 +28,7 @@ export declare class HNSWIndexManager {
|
|
|
29
28
|
private baseDir;
|
|
30
29
|
constructor(baseDir: string);
|
|
31
30
|
getIndex(scope: string, scopeHash: string, shardIndex: number): HNSWIndex;
|
|
31
|
+
getTagsIndex(scope: string, scopeHash: string, shardIndex: number): HNSWIndex;
|
|
32
32
|
rebuildFromShard(db: any, scope: string, scopeHash: string, shardIndex: number): Promise<void>;
|
|
33
33
|
deleteIndex(scope: string, scopeHash: string, shardIndex: number): Promise<void>;
|
|
34
34
|
cleanupOrphanedIndexes(validKeys: Set<string>): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hnsw-index.d.ts","sourceRoot":"","sources":["../../../src/services/sqlite/hnsw-index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"hnsw-index.d.ts","sourceRoot":"","sources":["../../../src/services/sqlite/hnsw-index.ts"],"names":[],"mappings":"AAsBA,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,YAAY,CAAC;CACtB;AAED,qBAAa,SAAS;IACpB,OAAO,CAAC,KAAK,CAAa;IAC1B,OAAO,CAAC,KAAK,CAAkC;IAC/C,OAAO,CAAC,UAAU,CAAkC;IACpD,OAAO,CAAC,MAAM,CAAa;IAC3B,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,WAAW,CAAiB;IACpC,OAAO,CAAC,WAAW,CAAkB;gBAEzB,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;YAKnC,iBAAiB;IAyCzB,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAgBvD,WAAW,CAAC,KAAK,EAAE,aAAa,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAkBlD,MAAM,CAAC,WAAW,EAAE,YAAY,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAkBzF,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAYjC,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAmB3B,QAAQ,IAAI,MAAM;CAGnB;AAED,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,OAAO,CAAqC;IACpD,OAAO,CAAC,OAAO,CAAS;gBAEZ,OAAO,EAAE,MAAM;IAO3B,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,SAAS;IAWzE,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,SAAS;IAWvE,gBAAgB,CACpB,EAAE,EAAE,GAAG,EACP,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,IAAI,CAAC;IAoCV,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAoBhF,sBAAsB,CAAC,SAAS,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;CA0BpE"}
|
|
@@ -2,7 +2,14 @@ import { mkdirSync, existsSync, writeFileSync, readFileSync, unlinkSync, readdir
|
|
|
2
2
|
import { join, dirname, basename } from "node:path";
|
|
3
3
|
import { log } from "../logger.js";
|
|
4
4
|
import { CONFIG } from "../../config.js";
|
|
5
|
-
|
|
5
|
+
let HNSWLib = null;
|
|
6
|
+
async function loadHNSWLib() {
|
|
7
|
+
if (!HNSWLib) {
|
|
8
|
+
const module = await import("hnswlib-wasm");
|
|
9
|
+
HNSWLib = module;
|
|
10
|
+
}
|
|
11
|
+
return HNSWLib;
|
|
12
|
+
}
|
|
6
13
|
export class HNSWIndex {
|
|
7
14
|
index = null;
|
|
8
15
|
idMap = new Map();
|
|
@@ -19,14 +26,15 @@ export class HNSWIndex {
|
|
|
19
26
|
async ensureInitialized() {
|
|
20
27
|
if (this.initialized)
|
|
21
28
|
return;
|
|
29
|
+
const hnsw = await loadHNSWLib();
|
|
22
30
|
const dir = dirname(this.indexPath);
|
|
23
31
|
if (!existsSync(dir)) {
|
|
24
32
|
mkdirSync(dir, { recursive: true });
|
|
25
33
|
}
|
|
26
34
|
if (existsSync(this.indexPath)) {
|
|
27
35
|
try {
|
|
28
|
-
this.index = new HierarchicalNSW("cosine", this.dimensions);
|
|
29
|
-
|
|
36
|
+
this.index = new hnsw.HierarchicalNSW("cosine", this.dimensions);
|
|
37
|
+
this.index.readIndex(this.indexPath);
|
|
30
38
|
const metaPath = this.indexPath + ".meta";
|
|
31
39
|
if (existsSync(metaPath)) {
|
|
32
40
|
const meta = JSON.parse(readFileSync(metaPath, "utf-8"));
|
|
@@ -41,20 +49,15 @@ export class HNSWIndex {
|
|
|
41
49
|
path: this.indexPath,
|
|
42
50
|
error: String(error),
|
|
43
51
|
});
|
|
44
|
-
this.index = new HierarchicalNSW("cosine", this.dimensions);
|
|
45
|
-
await this.index.initIndex(this.maxElements);
|
|
52
|
+
this.index = new hnsw.HierarchicalNSW("cosine", this.dimensions, this.maxElements);
|
|
46
53
|
}
|
|
47
54
|
}
|
|
48
55
|
else {
|
|
49
|
-
this.index = new HierarchicalNSW("cosine", this.dimensions);
|
|
50
|
-
await this.index.initIndex(this.maxElements);
|
|
56
|
+
this.index = new hnsw.HierarchicalNSW("cosine", this.dimensions, this.maxElements);
|
|
51
57
|
log("HNSW index created", { path: this.indexPath, dimensions: this.dimensions });
|
|
52
58
|
}
|
|
53
59
|
this.initialized = true;
|
|
54
60
|
}
|
|
55
|
-
vectorToArray(vector) {
|
|
56
|
-
return Array.from(vector);
|
|
57
|
-
}
|
|
58
61
|
async insert(id, vector) {
|
|
59
62
|
await this.ensureInitialized();
|
|
60
63
|
if (this.reverseMap.has(id)) {
|
|
@@ -62,7 +65,7 @@ export class HNSWIndex {
|
|
|
62
65
|
this.index.markDelete(internalId);
|
|
63
66
|
}
|
|
64
67
|
const internalId = this.nextId++;
|
|
65
|
-
|
|
68
|
+
this.index.addPoint(vector, internalId);
|
|
66
69
|
this.idMap.set(internalId, id);
|
|
67
70
|
this.reverseMap.set(id, internalId);
|
|
68
71
|
await this.save();
|
|
@@ -75,7 +78,7 @@ export class HNSWIndex {
|
|
|
75
78
|
this.index.markDelete(internalId);
|
|
76
79
|
}
|
|
77
80
|
const internalId = this.nextId++;
|
|
78
|
-
|
|
81
|
+
this.index.addPoint(item.vector, internalId);
|
|
79
82
|
this.idMap.set(internalId, item.id);
|
|
80
83
|
this.reverseMap.set(item.id, internalId);
|
|
81
84
|
}
|
|
@@ -84,7 +87,7 @@ export class HNSWIndex {
|
|
|
84
87
|
async search(queryVector, k) {
|
|
85
88
|
await this.ensureInitialized();
|
|
86
89
|
try {
|
|
87
|
-
const results =
|
|
90
|
+
const results = this.index.searchKnn(queryVector, k);
|
|
88
91
|
return results.neighbors
|
|
89
92
|
.map((internalId, idx) => ({
|
|
90
93
|
id: this.idMap.get(internalId) || "",
|
|
@@ -114,7 +117,7 @@ export class HNSWIndex {
|
|
|
114
117
|
if (!existsSync(dir)) {
|
|
115
118
|
mkdirSync(dir, { recursive: true });
|
|
116
119
|
}
|
|
117
|
-
|
|
120
|
+
this.index.writeIndex(this.indexPath);
|
|
118
121
|
const metaPath = this.indexPath + ".meta";
|
|
119
122
|
const meta = {
|
|
120
123
|
nextId: this.nextId,
|
|
@@ -144,34 +147,61 @@ export class HNSWIndexManager {
|
|
|
144
147
|
}
|
|
145
148
|
return this.indexes.get(key);
|
|
146
149
|
}
|
|
150
|
+
getTagsIndex(scope, scopeHash, shardIndex) {
|
|
151
|
+
const key = `${scope}_${scopeHash}_${shardIndex}_tags`;
|
|
152
|
+
if (!this.indexes.has(key)) {
|
|
153
|
+
const indexPath = join(this.baseDir, scope + "s", `${key}.hnsw`);
|
|
154
|
+
this.indexes.set(key, new HNSWIndex(CONFIG.embeddingDimensions, indexPath));
|
|
155
|
+
}
|
|
156
|
+
return this.indexes.get(key);
|
|
157
|
+
}
|
|
147
158
|
async rebuildFromShard(db, scope, scopeHash, shardIndex) {
|
|
148
|
-
const
|
|
149
|
-
const
|
|
150
|
-
const
|
|
159
|
+
const contentIndex = this.getIndex(scope, scopeHash, shardIndex);
|
|
160
|
+
const tagsIndex = this.getTagsIndex(scope, scopeHash, shardIndex);
|
|
161
|
+
const rows = db.prepare("SELECT id, vector, tags_vector FROM memories").all();
|
|
162
|
+
const contentItems = [];
|
|
163
|
+
const tagsItems = [];
|
|
151
164
|
for (const row of rows) {
|
|
152
165
|
if (row.vector) {
|
|
153
166
|
const vector = new Float32Array(row.vector.buffer);
|
|
154
|
-
|
|
167
|
+
contentItems.push({ id: row.id, vector });
|
|
168
|
+
}
|
|
169
|
+
if (row.tags_vector) {
|
|
170
|
+
const tagsVector = new Float32Array(row.tags_vector.buffer);
|
|
171
|
+
tagsItems.push({ id: row.id, vector: tagsVector });
|
|
155
172
|
}
|
|
156
173
|
}
|
|
157
|
-
if (
|
|
158
|
-
await
|
|
159
|
-
log("HNSW index rebuilt", { scope, scopeHash, shardIndex, count: items.length });
|
|
174
|
+
if (contentItems.length > 0) {
|
|
175
|
+
await contentIndex.insertBatch(contentItems);
|
|
160
176
|
}
|
|
177
|
+
if (tagsItems.length > 0) {
|
|
178
|
+
await tagsIndex.insertBatch(tagsItems);
|
|
179
|
+
}
|
|
180
|
+
log("HNSW indexes rebuilt", {
|
|
181
|
+
scope,
|
|
182
|
+
scopeHash,
|
|
183
|
+
shardIndex,
|
|
184
|
+
content: contentItems.length,
|
|
185
|
+
tags: tagsItems.length,
|
|
186
|
+
});
|
|
161
187
|
}
|
|
162
188
|
async deleteIndex(scope, scopeHash, shardIndex) {
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
189
|
+
const contentKey = `${scope}_${scopeHash}_${shardIndex}`;
|
|
190
|
+
const tagsKey = `${scope}_${scopeHash}_${shardIndex}_tags`;
|
|
191
|
+
this.indexes.delete(contentKey);
|
|
192
|
+
this.indexes.delete(tagsKey);
|
|
193
|
+
for (const key of [contentKey, tagsKey]) {
|
|
194
|
+
const indexPath = join(this.baseDir, scope + "s", `${key}.hnsw`);
|
|
195
|
+
const metaPath = indexPath + ".meta";
|
|
196
|
+
try {
|
|
197
|
+
if (existsSync(indexPath))
|
|
198
|
+
unlinkSync(indexPath);
|
|
199
|
+
if (existsSync(metaPath))
|
|
200
|
+
unlinkSync(metaPath);
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
log("Error deleting HNSW index files", { path: indexPath, error: String(error) });
|
|
204
|
+
}
|
|
175
205
|
}
|
|
176
206
|
}
|
|
177
207
|
async cleanupOrphanedIndexes(validKeys) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vector-search.d.ts","sourceRoot":"","sources":["../../../src/services/sqlite/vector-search.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAGnD,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAExE,QAAA,MAAM,QAAQ,sCAAgB,CAAC;AAC/B,KAAK,YAAY,GAAG,OAAO,QAAQ,CAAC,SAAS,CAAC;AAI9C,qBAAa,YAAY;IACvB,YAAY,CAAC,EAAE,EAAE,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,KAAK,CAAC,EAAE,SAAS,GAAG,IAAI;
|
|
1
|
+
{"version":3,"file":"vector-search.d.ts","sourceRoot":"","sources":["../../../src/services/sqlite/vector-search.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAGnD,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAExE,QAAA,MAAM,QAAQ,sCAAgB,CAAC;AAC/B,KAAK,YAAY,GAAG,OAAO,QAAQ,CAAC,SAAS,CAAC;AAI9C,qBAAa,YAAY;IACvB,YAAY,CAAC,EAAE,EAAE,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,KAAK,CAAC,EAAE,SAAS,GAAG,IAAI;IAqDvE,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;IA6EpB,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;IAkBlF,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;IA4BhB,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,gBAAgB,CACpB,EAAE,EAAE,YAAY,EAChB,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,IAAI,CAAC;IAIhB,eAAe,IAAI,gBAAgB;CAGpC;AAED,eAAO,MAAM,YAAY,cAAqB,CAAC"}
|
|
@@ -9,27 +9,45 @@ export class VectorSearch {
|
|
|
9
9
|
insertVector(db, record, shard) {
|
|
10
10
|
const insertMemory = db.prepare(`
|
|
11
11
|
INSERT INTO memories (
|
|
12
|
-
id, content, vector, container_tag, tags, type, created_at, updated_at,
|
|
12
|
+
id, content, vector, tags_vector, container_tag, tags, type, created_at, updated_at,
|
|
13
13
|
metadata, display_name, user_name, user_email, project_path, project_name, git_repo_url
|
|
14
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
14
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
15
15
|
`);
|
|
16
16
|
const vectorBuffer = new Uint8Array(record.vector.buffer);
|
|
17
|
-
|
|
17
|
+
const tagsVectorBuffer = record.tagsVector ? new Uint8Array(record.tagsVector.buffer) : null;
|
|
18
|
+
insertMemory.run(record.id, record.content, vectorBuffer, tagsVectorBuffer, 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);
|
|
18
19
|
if (shard && record.vector) {
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
log("HNSW insert error", { memoryId: record.id, error: String(err) });
|
|
20
|
+
const contentIndex = hnswIndexManager.getIndex(shard.scope, shard.scopeHash, shard.shardIndex);
|
|
21
|
+
contentIndex.insert(record.id, record.vector).catch((err) => {
|
|
22
|
+
log("HNSW content insert error", { memoryId: record.id, error: String(err) });
|
|
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
|
+
});
|
|
29
|
+
}
|
|
23
30
|
}
|
|
24
31
|
}
|
|
25
32
|
async searchInShard(shard, queryVector, containerTag, limit, queryText) {
|
|
26
33
|
const db = connectionManager.getConnection(shard.dbPath);
|
|
27
|
-
const
|
|
28
|
-
const
|
|
34
|
+
const contentIndex = hnswIndexManager.getIndex(shard.scope, shard.scopeHash, shard.shardIndex);
|
|
35
|
+
const tagsIndex = hnswIndexManager.getTagsIndex(shard.scope, shard.scopeHash, shard.shardIndex);
|
|
36
|
+
const contentResults = await contentIndex.search(queryVector, limit * 4);
|
|
37
|
+
const tagsResults = await tagsIndex.search(queryVector, limit * 4);
|
|
29
38
|
const scoreMap = new Map();
|
|
30
39
|
for (const r of contentResults) {
|
|
31
40
|
scoreMap.set(r.id, { contentSim: 1 - r.distance, tagsSim: 0 });
|
|
32
41
|
}
|
|
42
|
+
for (const r of tagsResults) {
|
|
43
|
+
const entry = scoreMap.get(r.id);
|
|
44
|
+
if (entry) {
|
|
45
|
+
entry.tagsSim = 1 - r.distance;
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
scoreMap.set(r.id, { contentSim: 0, tagsSim: 1 - r.distance });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
33
51
|
const ids = Array.from(scoreMap.keys());
|
|
34
52
|
if (ids.length === 0)
|
|
35
53
|
return [];
|
|
@@ -55,8 +73,8 @@ export class VectorSearch {
|
|
|
55
73
|
const matches = queryWords.filter((w) => memoryTags.some((t) => t.includes(w) || w.includes(t))).length;
|
|
56
74
|
exactMatchBoost = matches / Math.max(queryWords.length, 1);
|
|
57
75
|
}
|
|
58
|
-
const
|
|
59
|
-
const similarity =
|
|
76
|
+
const finalTagsSim = Math.max(scores.tagsSim, exactMatchBoost);
|
|
77
|
+
const similarity = scores.contentSim * 0.6 + finalTagsSim * 0.4;
|
|
60
78
|
return {
|
|
61
79
|
id: row.id,
|
|
62
80
|
memory: row.content,
|
|
@@ -92,16 +110,22 @@ export class VectorSearch {
|
|
|
92
110
|
async deleteVector(db, memoryId, shard) {
|
|
93
111
|
db.prepare(`DELETE FROM memories WHERE id = ?`).run(memoryId);
|
|
94
112
|
if (shard) {
|
|
95
|
-
const
|
|
96
|
-
|
|
113
|
+
const contentIndex = hnswIndexManager.getIndex(shard.scope, shard.scopeHash, shard.shardIndex);
|
|
114
|
+
const tagsIndex = hnswIndexManager.getTagsIndex(shard.scope, shard.scopeHash, shard.shardIndex);
|
|
115
|
+
await Promise.all([contentIndex.delete(memoryId), tagsIndex.delete(memoryId)]);
|
|
97
116
|
}
|
|
98
117
|
}
|
|
99
118
|
async updateVector(db, memoryId, vector, shard, tagsVector) {
|
|
100
119
|
const vectorBuffer = new Uint8Array(vector.buffer);
|
|
101
|
-
|
|
120
|
+
const tagsVectorBuffer = tagsVector ? new Uint8Array(tagsVector.buffer) : null;
|
|
121
|
+
db.prepare(`UPDATE memories SET vector = ?, tags_vector = ? WHERE id = ?`).run(vectorBuffer, tagsVectorBuffer, memoryId);
|
|
102
122
|
if (shard && vector) {
|
|
103
|
-
const
|
|
104
|
-
await
|
|
123
|
+
const contentIndex = hnswIndexManager.getIndex(shard.scope, shard.scopeHash, shard.shardIndex);
|
|
124
|
+
await contentIndex.insert(memoryId, vector);
|
|
125
|
+
if (tagsVector) {
|
|
126
|
+
const tagsIndex = hnswIndexManager.getTagsIndex(shard.scope, shard.scopeHash, shard.shardIndex);
|
|
127
|
+
await tagsIndex.insert(memoryId, tagsVector);
|
|
128
|
+
}
|
|
105
129
|
}
|
|
106
130
|
}
|
|
107
131
|
listMemories(db, containerTag, limit) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-mem",
|
|
3
|
-
"version": "2.11.
|
|
3
|
+
"version": "2.11.4",
|
|
4
4
|
"description": "OpenCode plugin that gives coding agents persistent memory using local vector database",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/plugin.js",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"@opencode-ai/plugin": "^1.0.162",
|
|
37
37
|
"@xenova/transformers": "^2.17.2",
|
|
38
38
|
"franc-min": "^6.2.0",
|
|
39
|
-
"hnswlib-
|
|
39
|
+
"hnswlib-wasm": "^0.8.2",
|
|
40
40
|
"iso-639-3": "^3.0.1"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|