@aeriondyseti/vector-memory-mcp 1.1.0-dev.6 → 2.0.0-rc
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 +22 -4
- package/package.json +12 -18
- package/scripts/migrate-from-lancedb.ts +56 -0
- package/scripts/smoke-test.ts +699 -0
- package/scripts/test-runner.ts +11 -1
- package/src/db/connection.ts +18 -4
- package/src/db/conversation.repository.ts +164 -79
- package/src/db/memory.repository.ts +182 -170
- package/src/db/migrations.ts +70 -0
- package/src/db/sqlite-utils.ts +78 -0
- package/src/http/server.ts +40 -35
- package/src/index.ts +33 -3
- package/src/mcp/server.ts +2 -1
- package/src/migration.ts +254 -0
- package/dist/package.json +0 -71
- package/dist/scripts/test-runner.d.ts +0 -9
- package/dist/scripts/test-runner.d.ts.map +0 -1
- package/dist/scripts/test-runner.js +0 -61
- package/dist/scripts/test-runner.js.map +0 -1
- package/dist/scripts/warmup.d.ts +0 -8
- package/dist/scripts/warmup.d.ts.map +0 -1
- package/dist/scripts/warmup.js +0 -61
- package/dist/scripts/warmup.js.map +0 -1
- package/dist/src/config/index.d.ts +0 -41
- package/dist/src/config/index.d.ts.map +0 -1
- package/dist/src/config/index.js +0 -75
- package/dist/src/config/index.js.map +0 -1
- package/dist/src/db/connection.d.ts +0 -3
- package/dist/src/db/connection.d.ts.map +0 -1
- package/dist/src/db/connection.js +0 -10
- package/dist/src/db/connection.js.map +0 -1
- package/dist/src/db/conversation.repository.d.ts +0 -26
- package/dist/src/db/conversation.repository.d.ts.map +0 -1
- package/dist/src/db/conversation.repository.js +0 -73
- package/dist/src/db/conversation.repository.js.map +0 -1
- package/dist/src/db/conversation.schema.d.ts +0 -4
- package/dist/src/db/conversation.schema.d.ts.map +0 -1
- package/dist/src/db/conversation.schema.js +0 -15
- package/dist/src/db/conversation.schema.js.map +0 -1
- package/dist/src/db/lancedb-utils.d.ts +0 -57
- package/dist/src/db/lancedb-utils.d.ts.map +0 -1
- package/dist/src/db/lancedb-utils.js +0 -124
- package/dist/src/db/lancedb-utils.js.map +0 -1
- package/dist/src/db/memory.repository.d.ts +0 -40
- package/dist/src/db/memory.repository.d.ts.map +0 -1
- package/dist/src/db/memory.repository.js +0 -183
- package/dist/src/db/memory.repository.js.map +0 -1
- package/dist/src/db/schema.d.ts +0 -7
- package/dist/src/db/schema.d.ts.map +0 -1
- package/dist/src/db/schema.js +0 -19
- package/dist/src/db/schema.js.map +0 -1
- package/dist/src/http/mcp-transport.d.ts +0 -19
- package/dist/src/http/mcp-transport.d.ts.map +0 -1
- package/dist/src/http/mcp-transport.js +0 -191
- package/dist/src/http/mcp-transport.js.map +0 -1
- package/dist/src/http/server.d.ts +0 -13
- package/dist/src/http/server.d.ts.map +0 -1
- package/dist/src/http/server.js +0 -224
- package/dist/src/http/server.js.map +0 -1
- package/dist/src/index.d.ts +0 -3
- package/dist/src/index.d.ts.map +0 -1
- package/dist/src/index.js +0 -68
- package/dist/src/index.js.map +0 -1
- package/dist/src/mcp/handlers.d.ts +0 -15
- package/dist/src/mcp/handlers.d.ts.map +0 -1
- package/dist/src/mcp/handlers.js +0 -313
- package/dist/src/mcp/handlers.js.map +0 -1
- package/dist/src/mcp/server.d.ts +0 -5
- package/dist/src/mcp/server.d.ts.map +0 -1
- package/dist/src/mcp/server.js +0 -22
- package/dist/src/mcp/server.js.map +0 -1
- package/dist/src/mcp/tools.d.ts +0 -13
- package/dist/src/mcp/tools.d.ts.map +0 -1
- package/dist/src/mcp/tools.js +0 -352
- package/dist/src/mcp/tools.js.map +0 -1
- package/dist/src/services/conversation.service.d.ts +0 -38
- package/dist/src/services/conversation.service.d.ts.map +0 -1
- package/dist/src/services/conversation.service.js +0 -252
- package/dist/src/services/conversation.service.js.map +0 -1
- package/dist/src/services/embeddings.service.d.ts +0 -12
- package/dist/src/services/embeddings.service.d.ts.map +0 -1
- package/dist/src/services/embeddings.service.js +0 -37
- package/dist/src/services/embeddings.service.js.map +0 -1
- package/dist/src/services/memory.service.d.ts +0 -40
- package/dist/src/services/memory.service.d.ts.map +0 -1
- package/dist/src/services/memory.service.js +0 -258
- package/dist/src/services/memory.service.js.map +0 -1
- package/dist/src/services/parsers/claude-code.parser.d.ts +0 -8
- package/dist/src/services/parsers/claude-code.parser.d.ts.map +0 -1
- package/dist/src/services/parsers/claude-code.parser.js +0 -191
- package/dist/src/services/parsers/claude-code.parser.js.map +0 -1
- package/dist/src/services/parsers/types.d.ts +0 -9
- package/dist/src/services/parsers/types.d.ts.map +0 -1
- package/dist/src/services/parsers/types.js +0 -2
- package/dist/src/services/parsers/types.js.map +0 -1
- package/dist/src/types/conversation.d.ts +0 -99
- package/dist/src/types/conversation.d.ts.map +0 -1
- package/dist/src/types/conversation.js +0 -2
- package/dist/src/types/conversation.js.map +0 -1
- package/dist/src/types/memory.d.ts +0 -30
- package/dist/src/types/memory.d.ts.map +0 -1
- package/dist/src/types/memory.js +0 -18
- package/dist/src/types/memory.js.map +0 -1
- package/src/db/conversation.schema.ts +0 -33
- package/src/db/lancedb-utils.ts +0 -142
- package/src/db/schema.ts +0 -38
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
import {
|
|
3
|
+
serializeVector,
|
|
4
|
+
deserializeVector,
|
|
5
|
+
safeParseJsonObject,
|
|
6
|
+
sanitizeFtsQuery,
|
|
7
|
+
hybridRRF,
|
|
8
|
+
topByRRF,
|
|
9
|
+
} from "./sqlite-utils.js";
|
|
5
10
|
import {
|
|
6
11
|
type Memory,
|
|
7
12
|
type HybridRow,
|
|
@@ -9,206 +14,213 @@ import {
|
|
|
9
14
|
} from "../types/memory.js";
|
|
10
15
|
|
|
11
16
|
export class MemoryRepository {
|
|
12
|
-
|
|
13
|
-
private migrationPromise: Promise<void> | null = null;
|
|
14
|
-
|
|
15
|
-
// FTS index mutex — once created, the promise is never cleared (index persists in LanceDB)
|
|
16
|
-
private ensureFtsIndex: () => Promise<void>;
|
|
17
|
-
|
|
18
|
-
// Cached reranker — k=60 is constant, no need to recreate per search
|
|
19
|
-
private getReranker = createRerankerMutex();
|
|
20
|
-
|
|
21
|
-
constructor(private db: lancedb.Connection) {
|
|
22
|
-
this.ensureFtsIndex = createFtsMutex(() => this.getTable());
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
private async getTable() {
|
|
26
|
-
const names = await this.db.tableNames();
|
|
27
|
-
if (names.includes(TABLE_NAME)) {
|
|
28
|
-
const table = await this.db.openTable(TABLE_NAME);
|
|
29
|
-
await this.ensureMigration(table);
|
|
30
|
-
return table;
|
|
31
|
-
}
|
|
32
|
-
// Create with empty data to initialize schema
|
|
33
|
-
return await this.db.createTable(TABLE_NAME, [], { schema: memorySchema });
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Ensures schema migration has run. Uses a mutex pattern identical to ensureFtsIndex.
|
|
38
|
-
* Adds columns introduced after the initial schema (usefulness, access_count, last_accessed).
|
|
39
|
-
*/
|
|
40
|
-
private ensureMigration(table: Table): Promise<void> {
|
|
41
|
-
if (this.migrationPromise) {
|
|
42
|
-
return this.migrationPromise;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
this.migrationPromise = this.migrateSchemaIfNeeded(table).catch((error) => {
|
|
46
|
-
this.migrationPromise = null;
|
|
47
|
-
throw error;
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
return this.migrationPromise;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Inspects the existing table schema and adds any missing columns with safe defaults.
|
|
55
|
-
* This handles databases created before the hybrid memory system was introduced.
|
|
56
|
-
*/
|
|
57
|
-
private async migrateSchemaIfNeeded(table: Table): Promise<void> {
|
|
58
|
-
const schema = await table.schema();
|
|
59
|
-
const existingFields = new Set(schema.fields.map((f) => f.name));
|
|
60
|
-
|
|
61
|
-
const migrations: { name: string; valueSql: string }[] = [];
|
|
17
|
+
constructor(private db: Database) {}
|
|
62
18
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if (!existingFields.has("access_count")) {
|
|
67
|
-
migrations.push({ name: "access_count", valueSql: "cast(0 as int)" });
|
|
68
|
-
}
|
|
69
|
-
if (!existingFields.has("last_accessed")) {
|
|
70
|
-
migrations.push({ name: "last_accessed", valueSql: "cast(NULL as timestamp)" });
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (migrations.length > 0) {
|
|
74
|
-
await table.addColumns(migrations);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Row mapping
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
77
22
|
|
|
78
23
|
/**
|
|
79
|
-
* Converts a raw
|
|
24
|
+
* Converts a raw SQLite row from the `memories` table to a Memory object.
|
|
25
|
+
* Vector is fetched separately when needed; pass it in if available.
|
|
80
26
|
*/
|
|
81
|
-
private rowToMemory(
|
|
27
|
+
private rowToMemory(
|
|
28
|
+
row: Record<string, unknown>,
|
|
29
|
+
embedding: number[] = [],
|
|
30
|
+
): Memory {
|
|
82
31
|
return {
|
|
83
32
|
id: row.id as string,
|
|
84
33
|
content: row.content as string,
|
|
85
|
-
embedding
|
|
86
|
-
metadata: safeParseJsonObject(row.metadata
|
|
87
|
-
createdAt:
|
|
88
|
-
updatedAt:
|
|
89
|
-
supersededBy: row.superseded_by as string
|
|
34
|
+
embedding,
|
|
35
|
+
metadata: safeParseJsonObject(row.metadata),
|
|
36
|
+
createdAt: new Date(row.created_at as number),
|
|
37
|
+
updatedAt: new Date(row.updated_at as number),
|
|
38
|
+
supersededBy: (row.superseded_by as string) ?? null,
|
|
90
39
|
usefulness: (row.usefulness as number) ?? 0,
|
|
91
40
|
accessCount: (row.access_count as number) ?? 0,
|
|
92
|
-
lastAccessed:
|
|
93
|
-
|
|
94
|
-
|
|
41
|
+
lastAccessed:
|
|
42
|
+
row.last_accessed != null
|
|
43
|
+
? new Date(row.last_accessed as number)
|
|
44
|
+
: null,
|
|
95
45
|
};
|
|
96
46
|
}
|
|
97
47
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
created_at: memory.createdAt.getTime(),
|
|
107
|
-
updated_at: memory.updatedAt.getTime(),
|
|
108
|
-
superseded_by: memory.supersededBy,
|
|
109
|
-
usefulness: memory.usefulness,
|
|
110
|
-
access_count: memory.accessCount,
|
|
111
|
-
last_accessed: memory.lastAccessed?.getTime() ?? null,
|
|
112
|
-
},
|
|
113
|
-
]);
|
|
48
|
+
/**
|
|
49
|
+
* Fetch the embedding vector for a memory id from the vec0 table.
|
|
50
|
+
*/
|
|
51
|
+
private getEmbedding(id: string): number[] {
|
|
52
|
+
const row = this.db
|
|
53
|
+
.prepare("SELECT vector FROM memories_vec WHERE id = ?")
|
|
54
|
+
.get(id) as { vector: Buffer } | null;
|
|
55
|
+
return row ? deserializeVector(row.vector) : [];
|
|
114
56
|
}
|
|
115
57
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// Public API
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
119
61
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
62
|
+
async insert(memory: Memory): Promise<void> {
|
|
63
|
+
const tx = this.db.transaction(() => {
|
|
64
|
+
this.db
|
|
65
|
+
.prepare(
|
|
66
|
+
`INSERT INTO memories (id, content, metadata, created_at, updated_at, superseded_by, usefulness, access_count, last_accessed)
|
|
67
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
68
|
+
)
|
|
69
|
+
.run(
|
|
70
|
+
memory.id,
|
|
71
|
+
memory.content,
|
|
72
|
+
JSON.stringify(memory.metadata),
|
|
73
|
+
memory.createdAt.getTime(),
|
|
74
|
+
memory.updatedAt.getTime(),
|
|
75
|
+
memory.supersededBy,
|
|
76
|
+
memory.usefulness,
|
|
77
|
+
memory.accessCount,
|
|
78
|
+
memory.lastAccessed?.getTime() ?? null,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
this.db
|
|
82
|
+
.prepare("INSERT INTO memories_vec (id, vector) VALUES (?, ?)")
|
|
83
|
+
.run(memory.id, serializeVector(memory.embedding));
|
|
84
|
+
|
|
85
|
+
this.db
|
|
86
|
+
.prepare("INSERT INTO memories_fts (id, content) VALUES (?, ?)")
|
|
87
|
+
.run(memory.id, memory.content);
|
|
88
|
+
});
|
|
123
89
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
90
|
+
tx();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async upsert(memory: Memory): Promise<void> {
|
|
94
|
+
const tx = this.db.transaction(() => {
|
|
95
|
+
// Main table supports INSERT OR REPLACE
|
|
96
|
+
this.db
|
|
97
|
+
.prepare(
|
|
98
|
+
`INSERT OR REPLACE INTO memories (id, content, metadata, created_at, updated_at, superseded_by, usefulness, access_count, last_accessed)
|
|
99
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
100
|
+
)
|
|
101
|
+
.run(
|
|
102
|
+
memory.id,
|
|
103
|
+
memory.content,
|
|
104
|
+
JSON.stringify(memory.metadata),
|
|
105
|
+
memory.createdAt.getTime(),
|
|
106
|
+
memory.updatedAt.getTime(),
|
|
107
|
+
memory.supersededBy,
|
|
108
|
+
memory.usefulness,
|
|
109
|
+
memory.accessCount,
|
|
110
|
+
memory.lastAccessed?.getTime() ?? null,
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
// vec0 virtual tables don't support REPLACE — delete then insert
|
|
114
|
+
this.db.prepare("DELETE FROM memories_vec WHERE id = ?").run(memory.id);
|
|
115
|
+
this.db
|
|
116
|
+
.prepare("INSERT INTO memories_vec (id, vector) VALUES (?, ?)")
|
|
117
|
+
.run(memory.id, serializeVector(memory.embedding));
|
|
118
|
+
|
|
119
|
+
// fts5 virtual tables don't support REPLACE — delete then insert
|
|
120
|
+
this.db.prepare("DELETE FROM memories_fts WHERE id = ?").run(memory.id);
|
|
121
|
+
this.db
|
|
122
|
+
.prepare("INSERT INTO memories_fts (id, content) VALUES (?, ?)")
|
|
123
|
+
.run(memory.id, memory.content);
|
|
137
124
|
});
|
|
125
|
+
|
|
126
|
+
tx();
|
|
138
127
|
}
|
|
139
128
|
|
|
140
129
|
async findById(id: string): Promise<Memory | null> {
|
|
141
|
-
const
|
|
142
|
-
|
|
130
|
+
const row = this.db
|
|
131
|
+
.prepare("SELECT * FROM memories WHERE id = ?")
|
|
132
|
+
.get(id) as Record<string, unknown> | null;
|
|
143
133
|
|
|
144
|
-
if (
|
|
145
|
-
return null;
|
|
146
|
-
}
|
|
134
|
+
if (!row) return null;
|
|
147
135
|
|
|
148
|
-
|
|
136
|
+
const embedding = this.getEmbedding(id);
|
|
137
|
+
return this.rowToMemory(row, embedding);
|
|
149
138
|
}
|
|
150
139
|
|
|
151
140
|
async findByIds(ids: string[]): Promise<Memory[]> {
|
|
152
141
|
if (ids.length === 0) return [];
|
|
153
|
-
const table = await this.getTable();
|
|
154
|
-
const inList = ids.map((id) => `'${escapeSql(id)}'`).join(", ");
|
|
155
|
-
const results = await table
|
|
156
|
-
.query()
|
|
157
|
-
.where(`id IN (${inList})`)
|
|
158
|
-
.toArray();
|
|
159
|
-
return results.map((row) => this.rowToMemory(row as Record<string, unknown>));
|
|
160
|
-
}
|
|
161
142
|
|
|
162
|
-
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const existing = await table.query().where(`id = '${escapeSql(id)}'`).limit(1).toArray();
|
|
167
|
-
if (existing.length === 0) {
|
|
168
|
-
return false;
|
|
169
|
-
}
|
|
143
|
+
const placeholders = ids.map(() => "?").join(", ");
|
|
144
|
+
const rows = this.db
|
|
145
|
+
.prepare(`SELECT * FROM memories WHERE id IN (${placeholders})`)
|
|
146
|
+
.all(...ids) as Array<Record<string, unknown>>;
|
|
170
147
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
values: {
|
|
175
|
-
superseded_by: DELETED_TOMBSTONE,
|
|
176
|
-
updated_at: now,
|
|
177
|
-
},
|
|
148
|
+
return rows.map((row) => {
|
|
149
|
+
const embedding = this.getEmbedding(row.id as string);
|
|
150
|
+
return this.rowToMemory(row, embedding);
|
|
178
151
|
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async markDeleted(id: string): Promise<boolean> {
|
|
155
|
+
const result = this.db
|
|
156
|
+
.prepare(
|
|
157
|
+
"UPDATE memories SET superseded_by = ?, updated_at = ? WHERE id = ?",
|
|
158
|
+
)
|
|
159
|
+
.run(DELETED_TOMBSTONE, Date.now(), id);
|
|
179
160
|
|
|
180
|
-
return
|
|
161
|
+
return result.changes > 0;
|
|
181
162
|
}
|
|
182
163
|
|
|
183
164
|
/**
|
|
184
|
-
*
|
|
185
|
-
* Uses RRF (Reciprocal Rank Fusion) to combine rankings from both search methods.
|
|
186
|
-
*
|
|
187
|
-
* @param embedding - Query embedding vector
|
|
188
|
-
* @param query - Text query for full-text search
|
|
189
|
-
* @param limit - Maximum number of results to return
|
|
190
|
-
* @returns Array of HybridRow containing full Memory data plus RRF score
|
|
165
|
+
* Hybrid search combining vector KNN and FTS5, fused with Reciprocal Rank Fusion.
|
|
191
166
|
*/
|
|
192
|
-
async findHybrid(
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
.
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
167
|
+
async findHybrid(
|
|
168
|
+
embedding: number[],
|
|
169
|
+
query: string,
|
|
170
|
+
limit: number,
|
|
171
|
+
): Promise<HybridRow[]> {
|
|
172
|
+
const candidateLimit = limit * 3;
|
|
173
|
+
const vecBuf = serializeVector(embedding);
|
|
174
|
+
|
|
175
|
+
// Vector KNN search
|
|
176
|
+
const vectorResults = this.db
|
|
177
|
+
.prepare(
|
|
178
|
+
"SELECT id, distance FROM memories_vec WHERE vector MATCH ? AND k = ? ORDER BY distance",
|
|
179
|
+
)
|
|
180
|
+
.all(vecBuf, candidateLimit) as Array<{ id: string; distance: number }>;
|
|
181
|
+
|
|
182
|
+
// Full-text search
|
|
183
|
+
const ftsQuery = sanitizeFtsQuery(query);
|
|
184
|
+
const ftsResults = this.db
|
|
185
|
+
.prepare(
|
|
186
|
+
"SELECT id FROM memories_fts WHERE memories_fts MATCH ? LIMIT ?",
|
|
187
|
+
)
|
|
188
|
+
.all(ftsQuery, candidateLimit) as Array<{ id: string }>;
|
|
189
|
+
|
|
190
|
+
// Compute RRF scores and pick top ids
|
|
191
|
+
const rrfScores = hybridRRF(vectorResults, ftsResults);
|
|
192
|
+
const topIds = topByRRF(rrfScores, limit);
|
|
193
|
+
|
|
194
|
+
if (topIds.length === 0) return [];
|
|
195
|
+
|
|
196
|
+
// Fetch full rows for the winning ids (service layer handles deleted filtering)
|
|
197
|
+
const placeholders = topIds.map(() => "?").join(", ");
|
|
198
|
+
const rows = this.db
|
|
199
|
+
.prepare(
|
|
200
|
+
`SELECT * FROM memories WHERE id IN (${placeholders})`,
|
|
201
|
+
)
|
|
202
|
+
.all(...topIds) as Array<Record<string, unknown>>;
|
|
203
|
+
|
|
204
|
+
// Build a lookup for quick access
|
|
205
|
+
const rowMap = new Map<string, Record<string, unknown>>();
|
|
206
|
+
for (const row of rows) {
|
|
207
|
+
rowMap.set(row.id as string, row);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Return results in RRF-ranked order, skipping any that were deleted
|
|
211
|
+
const results: HybridRow[] = [];
|
|
212
|
+
for (const id of topIds) {
|
|
213
|
+
const row = rowMap.get(id);
|
|
214
|
+
if (!row) continue; // deleted or missing
|
|
215
|
+
|
|
216
|
+
const memEmbedding = this.getEmbedding(id);
|
|
217
|
+
const memory = this.rowToMemory(row, memEmbedding);
|
|
218
|
+
results.push({
|
|
209
219
|
...memory,
|
|
210
|
-
rrfScore: (
|
|
211
|
-
};
|
|
212
|
-
}
|
|
220
|
+
rrfScore: rrfScores.get(id) ?? 0,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return results;
|
|
213
225
|
}
|
|
214
226
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Run all schema migrations. Safe to call on every startup (uses IF NOT EXISTS).
|
|
5
|
+
*/
|
|
6
|
+
export function runMigrations(db: Database): void {
|
|
7
|
+
// -- Memories --
|
|
8
|
+
db.exec(`
|
|
9
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
10
|
+
id TEXT PRIMARY KEY,
|
|
11
|
+
content TEXT NOT NULL,
|
|
12
|
+
metadata TEXT NOT NULL DEFAULT '{}',
|
|
13
|
+
created_at INTEGER NOT NULL,
|
|
14
|
+
updated_at INTEGER NOT NULL,
|
|
15
|
+
superseded_by TEXT,
|
|
16
|
+
usefulness REAL NOT NULL DEFAULT 0.0,
|
|
17
|
+
access_count INTEGER NOT NULL DEFAULT 0,
|
|
18
|
+
last_accessed INTEGER
|
|
19
|
+
)
|
|
20
|
+
`);
|
|
21
|
+
|
|
22
|
+
db.exec(`
|
|
23
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS memories_vec USING vec0(
|
|
24
|
+
id TEXT PRIMARY KEY,
|
|
25
|
+
vector float[384]
|
|
26
|
+
)
|
|
27
|
+
`);
|
|
28
|
+
|
|
29
|
+
db.exec(`
|
|
30
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
|
31
|
+
id UNINDEXED,
|
|
32
|
+
content
|
|
33
|
+
)
|
|
34
|
+
`);
|
|
35
|
+
|
|
36
|
+
// -- Conversation History --
|
|
37
|
+
db.exec(`
|
|
38
|
+
CREATE TABLE IF NOT EXISTS conversation_history (
|
|
39
|
+
id TEXT PRIMARY KEY,
|
|
40
|
+
content TEXT NOT NULL,
|
|
41
|
+
metadata TEXT NOT NULL DEFAULT '{}',
|
|
42
|
+
created_at INTEGER NOT NULL,
|
|
43
|
+
session_id TEXT NOT NULL,
|
|
44
|
+
role TEXT NOT NULL,
|
|
45
|
+
message_index_start INTEGER NOT NULL,
|
|
46
|
+
message_index_end INTEGER NOT NULL,
|
|
47
|
+
project TEXT NOT NULL
|
|
48
|
+
)
|
|
49
|
+
`);
|
|
50
|
+
|
|
51
|
+
db.exec(`
|
|
52
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS conversation_history_vec USING vec0(
|
|
53
|
+
id TEXT PRIMARY KEY,
|
|
54
|
+
vector float[384]
|
|
55
|
+
)
|
|
56
|
+
`);
|
|
57
|
+
|
|
58
|
+
db.exec(`
|
|
59
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS conversation_history_fts USING fts5(
|
|
60
|
+
id UNINDEXED,
|
|
61
|
+
content
|
|
62
|
+
)
|
|
63
|
+
`);
|
|
64
|
+
|
|
65
|
+
// -- Indexes --
|
|
66
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_conversation_session_id ON conversation_history(session_id)`);
|
|
67
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_conversation_project ON conversation_history(project)`);
|
|
68
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_conversation_role ON conversation_history(role)`);
|
|
69
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_conversation_created_at ON conversation_history(created_at)`);
|
|
70
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
|
|
3
|
+
/** RRF constant matching the previous LanceDB reranker default */
|
|
4
|
+
export const RRF_K = 60;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Serialize a number[] embedding to the raw float32 bytes sqlite-vec expects.
|
|
8
|
+
*/
|
|
9
|
+
export function serializeVector(vec: number[]): Buffer {
|
|
10
|
+
return Buffer.from(new Float32Array(vec).buffer);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Deserialize raw float32 bytes from sqlite-vec back to number[].
|
|
15
|
+
*/
|
|
16
|
+
export function deserializeVector(buf: Buffer): number[] {
|
|
17
|
+
return Array.from(new Float32Array(buf.buffer, buf.byteOffset, buf.byteLength / 4));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Sanitize a user query for FTS5 by quoting each token as a literal.
|
|
22
|
+
* Prevents FTS5 syntax errors from special characters like AND, OR, *, etc.
|
|
23
|
+
*/
|
|
24
|
+
export function sanitizeFtsQuery(query: string): string {
|
|
25
|
+
const tokens = query.trim().split(/\s+/).filter(Boolean);
|
|
26
|
+
if (tokens.length === 0) return '""';
|
|
27
|
+
return tokens.map(t => `"${t.replace(/"/g, '""')}"`).join(" ");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Compute hybrid RRF scores from two ranked result lists.
|
|
32
|
+
* Returns a map of id -> combined RRF score, sorted descending.
|
|
33
|
+
*/
|
|
34
|
+
export function hybridRRF(
|
|
35
|
+
vectorResults: Array<{ id: string }>,
|
|
36
|
+
ftsResults: Array<{ id: string }>,
|
|
37
|
+
k: number = RRF_K
|
|
38
|
+
): Map<string, number> {
|
|
39
|
+
const scores = new Map<string, number>();
|
|
40
|
+
|
|
41
|
+
vectorResults.forEach((r, i) => {
|
|
42
|
+
const rank = i + 1;
|
|
43
|
+
scores.set(r.id, (scores.get(r.id) ?? 0) + 1 / (k + rank));
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
ftsResults.forEach((r, i) => {
|
|
47
|
+
const rank = i + 1;
|
|
48
|
+
scores.set(r.id, (scores.get(r.id) ?? 0) + 1 / (k + rank));
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
return scores;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Sort ids by RRF score descending and return top N.
|
|
56
|
+
*/
|
|
57
|
+
export function topByRRF(scores: Map<string, number>, limit: number): string[] {
|
|
58
|
+
return [...scores.entries()]
|
|
59
|
+
.sort((a, b) => b[1] - a[1])
|
|
60
|
+
.slice(0, limit)
|
|
61
|
+
.map(([id]) => id);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Safely parse a JSON string, returning an empty object on failure.
|
|
66
|
+
* Ported from lancedb-utils.ts.
|
|
67
|
+
*/
|
|
68
|
+
export function safeParseJsonObject(raw: unknown): Record<string, unknown> {
|
|
69
|
+
if (typeof raw !== "string") return {};
|
|
70
|
+
try {
|
|
71
|
+
const parsed = JSON.parse(raw);
|
|
72
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
|
|
73
|
+
? parsed
|
|
74
|
+
: {};
|
|
75
|
+
} catch {
|
|
76
|
+
return {};
|
|
77
|
+
}
|
|
78
|
+
}
|
package/src/http/server.ts
CHANGED
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
2
|
import { cors } from "hono/cors";
|
|
3
|
-
import { serve as nodeServe } from "@hono/node-server";
|
|
4
3
|
import { createServer } from "net";
|
|
4
|
+
import { writeFileSync, mkdirSync, unlinkSync } from "fs";
|
|
5
|
+
import { join } from "path";
|
|
5
6
|
import type { MemoryService } from "../services/memory.service.js";
|
|
6
7
|
import type { Config } from "../config/index.js";
|
|
7
8
|
import { isDeleted } from "../types/memory.js";
|
|
8
9
|
import { createMcpRoutes } from "./mcp-transport.js";
|
|
9
10
|
import type { Memory, SearchIntent } from "../types/memory.js";
|
|
10
11
|
|
|
11
|
-
// Detect runtime
|
|
12
|
-
const isBun = typeof globalThis.Bun !== "undefined";
|
|
13
|
-
|
|
14
12
|
/**
|
|
15
13
|
* Check if a port is available by attempting to bind to it
|
|
16
14
|
*/
|
|
@@ -56,6 +54,31 @@ async function findAvailablePort(
|
|
|
56
54
|
});
|
|
57
55
|
}
|
|
58
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Write a lockfile so hooks can discover which port this server bound to.
|
|
59
|
+
* Written atomically after the HTTP server successfully binds.
|
|
60
|
+
*/
|
|
61
|
+
function writeLockfile(port: number): void {
|
|
62
|
+
const dir = join(process.cwd(), ".vector-memory");
|
|
63
|
+
mkdirSync(dir, { recursive: true });
|
|
64
|
+
writeFileSync(
|
|
65
|
+
join(dir, "server.lock"),
|
|
66
|
+
JSON.stringify({ port, pid: process.pid }),
|
|
67
|
+
"utf8"
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Remove the lockfile on clean shutdown so stale files don't linger.
|
|
73
|
+
*/
|
|
74
|
+
export function removeLockfile(): void {
|
|
75
|
+
try {
|
|
76
|
+
unlinkSync(join(process.cwd(), ".vector-memory", "server.lock"));
|
|
77
|
+
} catch {
|
|
78
|
+
// already gone — fine
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
59
82
|
export interface HttpServerOptions {
|
|
60
83
|
memoryService: MemoryService;
|
|
61
84
|
config: Config;
|
|
@@ -246,37 +269,19 @@ export async function startHttpServer(
|
|
|
246
269
|
// Find an available port (uses configured port if available, otherwise picks a random one)
|
|
247
270
|
const actualPort = await findAvailablePort(config.httpPort, config.httpHost);
|
|
248
271
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
fetch: app.fetch,
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
console.error(
|
|
258
|
-
`[vector-memory-mcp] HTTP server listening on http://${config.httpHost}:${actualPort}`
|
|
259
|
-
);
|
|
260
|
-
|
|
261
|
-
return {
|
|
262
|
-
stop: () => server.stop(),
|
|
263
|
-
port: actualPort,
|
|
264
|
-
};
|
|
265
|
-
} else {
|
|
266
|
-
// Use Node.js server via @hono/node-server
|
|
267
|
-
const server = nodeServe({
|
|
268
|
-
fetch: app.fetch,
|
|
269
|
-
port: actualPort,
|
|
270
|
-
hostname: config.httpHost,
|
|
271
|
-
});
|
|
272
|
+
const server = Bun.serve({
|
|
273
|
+
port: actualPort,
|
|
274
|
+
hostname: config.httpHost,
|
|
275
|
+
fetch: app.fetch,
|
|
276
|
+
});
|
|
272
277
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
278
|
+
writeLockfile(actualPort);
|
|
279
|
+
console.error(
|
|
280
|
+
`[vector-memory-mcp] HTTP server listening on http://${config.httpHost}:${actualPort}`
|
|
281
|
+
);
|
|
276
282
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
}
|
|
283
|
+
return {
|
|
284
|
+
stop: () => { removeLockfile(); server.stop(); },
|
|
285
|
+
port: actualPort,
|
|
286
|
+
};
|
|
282
287
|
}
|