Skip to content

Commit d5581bb

Browse files
committed
refactor(vector-search): replace sqlite-vec with hnswlib-wasm
Replace native SQLite extension (sqlite-vec) with pure WASM-based HNSW implementation for cross-platform compatibility. Removes complex dylib building and custom SQLite loading logic, simplifying deployment and maintenance. Vector operations now use hnswlib-wasm with persistent on-disk indexes stored alongside SQLite databases. BREAKING CHANGE: removes sqlite-vec dependency and macOS native dylib bundles.
1 parent f1571ce commit d5581bb

16 files changed

Lines changed: 352 additions & 478 deletions

.github/workflows/release.yml

Lines changed: 0 additions & 167 deletions
Original file line numberDiff line numberDiff line change
@@ -5,174 +5,16 @@ on:
55
tags:
66
- 'v*'
77

8-
env:
9-
SQLITE_VEC_VERSION: "0.1.7-alpha.8"
10-
118
jobs:
12-
build-sqlite:
13-
runs-on: ${{ matrix.runs-on }}
14-
strategy:
15-
matrix:
16-
include:
17-
- arch: arm64
18-
runs-on: macos-15
19-
- arch: x64
20-
runs-on: macos-15-intel
21-
22-
steps:
23-
- uses: actions/checkout@v4
24-
25-
- name: Get latest SQLite version
26-
id: sqlite-version
27-
run: |
28-
DOWNLOAD_PATH=$(curl -sL https://sqlite.org/download.html | grep -o '[0-9]*/sqlite-amalgamation-[0-9]*\.zip' | head -1)
29-
VERSION=$(echo "$DOWNLOAD_PATH" | grep -o 'sqlite-amalgamation-[0-9]*' | sed 's/sqlite-amalgamation-//')
30-
YEAR=$(echo "$DOWNLOAD_PATH" | cut -d'/' -f1)
31-
echo "version=$VERSION" >> $GITHUB_OUTPUT
32-
echo "year=$YEAR" >> $GITHUB_OUTPUT
33-
34-
- name: Check existing dylib
35-
id: check
36-
run: |
37-
DYLIB_PATH="native/darwin-${{ matrix.arch }}/libsqlite3.dylib"
38-
39-
if [ ! -f "$DYLIB_PATH" ]; then
40-
echo "needs-update=true" >> $GITHUB_OUTPUT
41-
echo "Dylib not found, will build"
42-
else
43-
EXISTING_SIZE=$(stat -f%z "$DYLIB_PATH" 2>/dev/null || stat -c%s "$DYLIB_PATH" 2>/dev/null || echo "0")
44-
if [ "$EXISTING_SIZE" -lt 1400000 ]; then
45-
echo "needs-update=true" >> $GITHUB_OUTPUT
46-
echo "Dylib too small (likely missing sqlite-vec), will rebuild"
47-
else
48-
if strings "$DYLIB_PATH" | grep -q "vec_version"; then
49-
echo "needs-update=false" >> $GITHUB_OUTPUT
50-
echo "Dylib exists with sqlite-vec"
51-
else
52-
echo "needs-update=true" >> $GITHUB_OUTPUT
53-
echo "Dylib missing sqlite-vec, will rebuild"
54-
fi
55-
fi
56-
fi
57-
58-
- name: Download SQLite and sqlite-vec
59-
if: steps.check.outputs.needs-update == 'true'
60-
run: |
61-
curl -LO "https://sqlite.org/${{ steps.sqlite-version.outputs.year }}/sqlite-amalgamation-${{ steps.sqlite-version.outputs.version }}.zip"
62-
unzip "sqlite-amalgamation-${{ steps.sqlite-version.outputs.version }}.zip"
63-
64-
curl -LO "https://github.com/asg017/sqlite-vec/releases/download/v${{ env.SQLITE_VEC_VERSION }}/sqlite-vec-${{ env.SQLITE_VEC_VERSION }}-amalgamation.zip"
65-
unzip "sqlite-vec-${{ env.SQLITE_VEC_VERSION }}-amalgamation.zip"
66-
67-
- name: Build combined dylib
68-
if: steps.check.outputs.needs-update == 'true'
69-
run: |
70-
SQLITE_DIR="sqlite-amalgamation-${{ steps.sqlite-version.outputs.version }}"
71-
72-
gcc -c "$SQLITE_DIR/sqlite3.c" -o sqlite3.o \
73-
-DSQLITE_ENABLE_FTS5 \
74-
-DSQLITE_ENABLE_RTREE \
75-
-DSQLITE_ENABLE_GEOPOLY
76-
77-
gcc -c sqlite-vec.c -o sqlite-vec.o \
78-
-I"$SQLITE_DIR" \
79-
-DSQLITE_CORE \
80-
-DSQLITE_VEC_STATIC
81-
82-
cat > vec_init.c << 'INIT_EOF'
83-
#include "sqlite3.h"
84-
extern int sqlite3_vec_init(sqlite3 *db, char **pzErrMsg, const sqlite3_api_routines *pApi);
85-
__attribute__((constructor))
86-
static void vec_auto_register(void) {
87-
sqlite3_auto_extension((void(*)(void))sqlite3_vec_init);
88-
}
89-
INIT_EOF
90-
91-
gcc -c vec_init.c -o vec_init.o -I"$SQLITE_DIR"
92-
93-
gcc -dynamiclib \
94-
-o libsqlite3.dylib \
95-
sqlite3.o sqlite-vec.o vec_init.o \
96-
-install_name @rpath/libsqlite3.dylib \
97-
-lpthread -ldl -lm
98-
99-
- name: Verify dylib
100-
if: steps.check.outputs.needs-update == 'true'
101-
run: |
102-
otool -D libsqlite3.dylib
103-
file libsqlite3.dylib
104-
ls -la libsqlite3.dylib
105-
106-
- name: Upload artifact
107-
uses: actions/upload-artifact@v4
108-
with:
109-
name: libsqlite3-darwin-${{ matrix.arch }}
110-
path: libsqlite3.dylib
111-
retention-days: 1
112-
if-no-files-found: ignore
113-
114-
commit-dylib:
115-
needs: build-sqlite
116-
runs-on: ubuntu-latest
117-
permissions:
118-
contents: write
119-
outputs:
120-
changed: ${{ steps.update.outputs.changed }}
121-
steps:
122-
- uses: actions/checkout@v4
123-
with:
124-
ref: main
125-
fetch-depth: 0
126-
127-
- name: Download artifacts
128-
uses: actions/download-artifact@v4
129-
with:
130-
path: artifacts
131-
132-
- name: Update dylib files
133-
id: update
134-
run: |
135-
CHANGED="false"
136-
137-
if [ -f "artifacts/libsqlite3-darwin-arm64/libsqlite3.dylib" ]; then
138-
mkdir -p native/darwin-arm64
139-
cp artifacts/libsqlite3-darwin-arm64/libsqlite3.dylib native/darwin-arm64/
140-
CHANGED="true"
141-
echo "Updated arm64 dylib"
142-
fi
143-
144-
if [ -f "artifacts/libsqlite3-darwin-x64/libsqlite3.dylib" ]; then
145-
mkdir -p native/darwin-x64
146-
cp artifacts/libsqlite3-darwin-x64/libsqlite3.dylib native/darwin-x64/
147-
CHANGED="true"
148-
echo "Updated x64 dylib"
149-
fi
150-
151-
echo "changed=$CHANGED" >> $GITHUB_OUTPUT
152-
153-
- name: Commit and push
154-
if: steps.update.outputs.changed == 'true'
155-
run: |
156-
git config user.name "github-actions[bot]"
157-
git config user.email "github-actions[bot]@users.noreply.github.com"
158-
git add native/
159-
git commit -m "chore: update SQLite+sqlite-vec dylib [skip ci]"
160-
git push origin main
161-
1629
release:
163-
needs: [build-sqlite, commit-dylib]
16410
runs-on: ubuntu-latest
16511
permissions:
16612
contents: write
16713
steps:
16814
- uses: actions/checkout@v4
16915
with:
170-
ref: main
17116
fetch-depth: 0
17217

173-
- name: Pull latest changes
174-
run: git pull origin main
175-
17618
- uses: oven-sh/setup-bun@v2
17719

17820
- run: bun install
@@ -192,16 +34,7 @@ jobs:
19234
env:
19335
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
19436

195-
- name: Prepare release assets
196-
run: |
197-
mkdir -p release-assets
198-
cp native/darwin-arm64/libsqlite3.dylib release-assets/libsqlite3-darwin-arm64.dylib
199-
cp native/darwin-x64/libsqlite3.dylib release-assets/libsqlite3-darwin-x64.dylib
200-
20137
- name: Create GitHub Release
20238
uses: softprops/action-gh-release@v2
20339
with:
20440
generate_release_notes: true
205-
files: |
206-
release-assets/libsqlite3-darwin-arm64.dylib
207-
release-assets/libsqlite3-darwin-x64.dylib

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ A persistent memory system for AI coding agents that enables long-term context r
2020

2121
## Core Features
2222

23-
Local vector database with SQLite, 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.
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.
2424

2525
## Getting Started
2626

@@ -32,7 +32,7 @@ Add to your OpenCode configuration at `~/.config/opencode/opencode.json`:
3232
}
3333
```
3434

35-
The plugin downloads automatically on next startup. On macOS, the plugin automatically downloads a compatible SQLite library during installation. If the download fails, install Homebrew SQLite as fallback: `brew install sqlite`
35+
The plugin downloads automatically on next startup. No additional dependencies required - vector search is powered by pure WASM for cross-platform compatibility.
3636

3737
## Usage Examples
3838

bun.lock

Lines changed: 3 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

native/darwin-arm64/.gitkeep

Whitespace-only changes.
-1.63 MB
Binary file not shown.

native/darwin-x64/.gitkeep

Whitespace-only changes.

native/darwin-x64/libsqlite3.dylib

-1.45 MB
Binary file not shown.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "opencode-mem",
3-
"version": "2.10.0",
3+
"version": "2.11.0",
44
"description": "OpenCode plugin that gives coding agents persistent memory using local vector database",
55
"type": "module",
66
"main": "dist/plugin.js",
@@ -36,6 +36,7 @@
3636
"@opencode-ai/plugin": "^1.0.162",
3737
"@xenova/transformers": "^2.17.2",
3838
"franc-min": "^6.2.0",
39+
"hnswlib-wasm": "^0.8.2",
3940
"iso-639-3": "^3.0.1"
4041
},
4142
"devDependencies": {
@@ -54,7 +55,6 @@
5455
},
5556
"files": [
5657
"dist",
57-
"native",
5858
"package.json"
5959
],
6060
"lint-staged": {

src/services/api-handlers.ts

Lines changed: 15 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,7 @@ export async function handleAddMemory(data: {
327327
metadata: JSON.stringify({ source: "api" }),
328328
};
329329
const db = connectionManager.getConnection(shard.dbPath);
330-
vectorSearch.insertVector(db, record);
330+
vectorSearch.insertVector(db, record, shard);
331331
shardManager.incrementVectorCount(shard.id);
332332
return { success: true, data: { id } };
333333
} catch (error) {
@@ -352,7 +352,7 @@ export async function handleDeleteMemory(
352352
const linkedPromptId = metadata?.promptId;
353353
if (linkedPromptId) userPromptManager.deletePrompt(linkedPromptId);
354354
}
355-
vectorSearch.deleteVector(db, id);
355+
await vectorSearch.deleteVector(db, id, shard);
356356
shardManager.decrementVectorCount(shard.id);
357357
return {
358358
success: true,
@@ -406,7 +406,7 @@ export async function handleUpdateMemory(
406406
}
407407
if (!foundShard || !existingMemory) return { success: false, error: "Memory not found" };
408408
const db = connectionManager.getConnection(foundShard.dbPath);
409-
vectorSearch.deleteVector(db, id);
409+
await vectorSearch.deleteVector(db, id, foundShard);
410410
shardManager.decrementVectorCount(foundShard.id);
411411

412412
const newContent = data.content || existingMemory.content;
@@ -436,7 +436,7 @@ export async function handleUpdateMemory(
436436
projectName: existingMemory.project_name,
437437
gitRepoUrl: existingMemory.git_repo_url,
438438
};
439-
vectorSearch.insertVector(db, updatedRecord);
439+
vectorSearch.insertVector(db, updatedRecord, foundShard);
440440
shardManager.incrementVectorCount(foundShard.id);
441441
return { success: true };
442442
} catch (error) {
@@ -497,7 +497,7 @@ export async function handleSearch(
497497
const shards = shardManager.getAllShards(scope, hash);
498498
for (const shard of shards) {
499499
try {
500-
const results = vectorSearch.searchInShard(shard, queryVector, tag, pageSize * 2);
500+
const results = await vectorSearch.searchInShard(shard, queryVector, tag, pageSize * 2);
501501
memoryResults.push(...results);
502502
} catch (error) {
503503
log("Shard search error", { shardId: shard.id, error: String(error) });
@@ -520,7 +520,12 @@ export async function handleSearch(
520520
const shards = shardManager.getAllShards(scope, hash);
521521
for (const shard of shards) {
522522
try {
523-
const results = vectorSearch.searchInShard(shard, queryVector, containerTag, pageSize);
523+
const results = await vectorSearch.searchInShard(
524+
shard,
525+
queryVector,
526+
containerTag,
527+
pageSize
528+
);
524529
memoryResults.push(...results);
525530
} catch (error) {
526531
log("Shard search error", { shardId: shard.id, error: String(error) });
@@ -1053,21 +1058,10 @@ export async function handleRunTagMigrationBatch(
10531058
m.id
10541059
);
10551060

1056-
db.prepare("DELETE FROM vec_memories WHERE memory_id = ?").run(m.id);
1057-
db.prepare("INSERT INTO vec_memories (memory_id, embedding) VALUES (?, ?)").run(
1058-
m.id,
1059-
vectorBuffer
1060-
);
1061-
1062-
if (currentTags.length > 0) {
1063-
const tagsVector = await embeddingService.embedWithTimeout(currentTags.join(", "));
1064-
const tagsVectorBuffer = new Uint8Array(tagsVector.buffer);
1065-
db.prepare("DELETE FROM vec_tags WHERE memory_id = ?").run(m.id);
1066-
db.prepare("INSERT INTO vec_tags (memory_id, embedding) VALUES (?, ?)").run(
1067-
m.id,
1068-
tagsVectorBuffer
1069-
);
1070-
}
1061+
const index = vectorSearch
1062+
.getIndexManager()
1063+
.getIndex(shard.scope, shard.scopeHash, shard.shardIndex);
1064+
await index.insert(m.id, vector);
10711065

10721066
migrationProgress.processed++;
10731067
batchProcessed++;

src/services/client.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ export class LocalMemoryClient {
195195
};
196196

197197
const db = connectionManager.getConnection(shard.dbPath);
198-
vectorSearch.insertVector(db, record);
198+
vectorSearch.insertVector(db, record, shard);
199199
shardManager.incrementVectorCount(shard.id);
200200

201201
return { success: true as const, id };
@@ -219,7 +219,7 @@ export class LocalMemoryClient {
219219
const memory = vectorSearch.getMemoryById(db, memoryId);
220220

221221
if (memory) {
222-
vectorSearch.deleteVector(db, memoryId);
222+
await vectorSearch.deleteVector(db, memoryId, shard);
223223
shardManager.decrementVectorCount(shard.id);
224224
return { success: true };
225225
}

0 commit comments

Comments
 (0)