@monoes/memory 1.0.19 → 1.0.21
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/dist/hnsw-distance.d.ts +31 -0
- package/dist/hnsw-distance.d.ts.map +1 -0
- package/dist/hnsw-distance.js +106 -0
- package/dist/hnsw-distance.js.map +1 -0
- package/dist/hnsw-graph-ops.d.ts +29 -0
- package/dist/hnsw-graph-ops.d.ts.map +1 -0
- package/dist/hnsw-graph-ops.js +96 -0
- package/dist/hnsw-graph-ops.js.map +1 -0
- package/dist/hnsw-heap.d.ts +45 -0
- package/dist/hnsw-heap.d.ts.map +1 -0
- package/dist/hnsw-heap.js +155 -0
- package/dist/hnsw-heap.js.map +1 -0
- package/dist/hnsw-index.d.ts +18 -43
- package/dist/hnsw-index.d.ts.map +1 -1
- package/dist/hnsw-index.js +21 -463
- package/dist/hnsw-index.js.map +1 -1
- package/dist/hnsw-quantizer.d.ts +28 -0
- package/dist/hnsw-quantizer.d.ts.map +1 -0
- package/dist/hnsw-quantizer.js +100 -0
- package/dist/hnsw-quantizer.js.map +1 -0
- package/dist/hnsw-serialize.d.ts +39 -0
- package/dist/hnsw-serialize.d.ts.map +1 -0
- package/dist/hnsw-serialize.js +54 -0
- package/dist/hnsw-serialize.js.map +1 -0
- package/dist/migration-loaders.d.ts +38 -0
- package/dist/migration-loaders.d.ts.map +1 -0
- package/dist/migration-loaders.js +269 -0
- package/dist/migration-loaders.js.map +1 -0
- package/dist/migration-transform.d.ts +21 -0
- package/dist/migration-transform.d.ts.map +1 -0
- package/dist/migration-transform.js +124 -0
- package/dist/migration-transform.js.map +1 -0
- package/dist/migration.d.ts +38 -22
- package/dist/migration.d.ts.map +1 -1
- package/dist/migration.js +35 -348
- package/dist/migration.js.map +1 -1
- package/dist/sql-backend-ann.d.ts +46 -0
- package/dist/sql-backend-ann.d.ts.map +1 -0
- package/dist/sql-backend-ann.js +120 -0
- package/dist/sql-backend-ann.js.map +1 -0
- package/dist/sql-backend-introspect.d.ts +17 -0
- package/dist/sql-backend-introspect.d.ts.map +1 -0
- package/dist/sql-backend-introspect.js +114 -0
- package/dist/sql-backend-introspect.js.map +1 -0
- package/dist/sql-backend-reads.d.ts +53 -0
- package/dist/sql-backend-reads.d.ts.map +1 -0
- package/dist/sql-backend-reads.js +295 -0
- package/dist/sql-backend-reads.js.map +1 -0
- package/dist/sql-backend-writes.d.ts +64 -0
- package/dist/sql-backend-writes.d.ts.map +1 -0
- package/dist/sql-backend-writes.js +270 -0
- package/dist/sql-backend-writes.js.map +1 -0
- package/dist/sql-backend.d.ts +39 -96
- package/dist/sql-backend.d.ts.map +1 -1
- package/dist/sql-backend.js +49 -706
- package/dist/sql-backend.js.map +1 -1
- package/dist/types-backend.d.ts +187 -0
- package/dist/types-backend.d.ts.map +1 -0
- package/dist/types-backend.js +8 -0
- package/dist/types-backend.js.map +1 -0
- package/dist/types-core.d.ts +180 -0
- package/dist/types-core.d.ts.map +1 -0
- package/dist/types-core.js +8 -0
- package/dist/types-core.js.map +1 -0
- package/dist/types-misc.d.ts +172 -0
- package/dist/types-misc.d.ts.map +1 -0
- package/dist/types-misc.js +56 -0
- package/dist/types-misc.js.map +1 -0
- package/dist/types.d.ts +8 -516
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +5 -49
- package/dist/types.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-path methods for SqlBackend (get/query/search family).
|
|
3
|
+
*
|
|
4
|
+
* File-size sweep: split out of sql-backend.ts. Mixed into
|
|
5
|
+
* SqlBackend.prototype at the bottom of sql-backend.ts.
|
|
6
|
+
*
|
|
7
|
+
* @module v1/memory/sql-backend-reads
|
|
8
|
+
*/
|
|
9
|
+
import { cosineSimilarity } from './math-utils.js';
|
|
10
|
+
/** Cap on votes/rows pulled in one go, guarding against unbounded memory use. */
|
|
11
|
+
const MAX_QUERY_LIMIT = 10_000;
|
|
12
|
+
export const sqlBackendReadMethods = {
|
|
13
|
+
async get(id, agentId) {
|
|
14
|
+
this.ensureInitialized();
|
|
15
|
+
const startTime = performance.now();
|
|
16
|
+
const row = this.driver?.get('SELECT memory_entries.*, emb.embedding AS _emb FROM memory_entries LEFT JOIN memory_embeddings emb ON emb.entry_id = memory_entries.id WHERE memory_entries.id = ?', [id]);
|
|
17
|
+
if (!row)
|
|
18
|
+
return null;
|
|
19
|
+
// Collaborative memory promotion — https://arxiv.org/abs/2505.18279
|
|
20
|
+
const AGENT_ID_RE = /^[a-zA-Z0-9_-]{1,128}$/;
|
|
21
|
+
if (agentId && AGENT_ID_RE.test(agentId)) {
|
|
22
|
+
try {
|
|
23
|
+
this.driver?.run('INSERT OR IGNORE INTO agent_reads (entry_id, agent_id, read_at) VALUES (?, ?, ?)', [id, agentId, Date.now()]);
|
|
24
|
+
this._readCount++;
|
|
25
|
+
if (this._readCount % 1000 === 0)
|
|
26
|
+
this.checkAndPromoteEntry(id);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
/* non-critical */
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const entry = this.rowToEntry(row);
|
|
33
|
+
this.emit('entry:retrieved', { id, duration: performance.now() - startTime });
|
|
34
|
+
return entry;
|
|
35
|
+
},
|
|
36
|
+
/**
|
|
37
|
+
* Promote an entry to 'team' once 3+ distinct agents have read it within 24h.
|
|
38
|
+
* https://arxiv.org/abs/2505.18279
|
|
39
|
+
*/
|
|
40
|
+
checkAndPromoteEntry(entryId) {
|
|
41
|
+
const d = this.driver;
|
|
42
|
+
if (!d)
|
|
43
|
+
return;
|
|
44
|
+
const cutoff = Date.now() - 24 * 60 * 60 * 1000;
|
|
45
|
+
d.run('DELETE FROM agent_reads WHERE read_at <= ?', [cutoff]);
|
|
46
|
+
const row = d.get('SELECT COUNT(DISTINCT agent_id) as cnt FROM agent_reads WHERE entry_id = ? AND read_at > ?', [entryId, cutoff]);
|
|
47
|
+
if (Number(row?.cnt ?? 0) >= 3) {
|
|
48
|
+
d.run("UPDATE memory_entries SET access_level = 'team' WHERE id = ? AND access_level = 'private'", [entryId]);
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
async getByKey(namespace, key) {
|
|
52
|
+
this.ensureInitialized();
|
|
53
|
+
const startTime = performance.now();
|
|
54
|
+
const row = this.driver?.get('SELECT memory_entries.*, emb.embedding AS _emb FROM memory_entries LEFT JOIN memory_embeddings emb ON emb.entry_id = memory_entries.id WHERE memory_entries.namespace = ? AND memory_entries.key = ?', [namespace, key]);
|
|
55
|
+
if (!row)
|
|
56
|
+
return null;
|
|
57
|
+
const entry = this.rowToEntry(row);
|
|
58
|
+
this.emit('entry:retrieved', { namespace, key, duration: performance.now() - startTime });
|
|
59
|
+
return entry;
|
|
60
|
+
},
|
|
61
|
+
async query(query) {
|
|
62
|
+
this.ensureInitialized();
|
|
63
|
+
const startTime = performance.now();
|
|
64
|
+
// PKG-3: LEFT JOIN memory_embeddings once so rowToEntry can read the
|
|
65
|
+
// embedding column without an N+1 round-trip per result row.
|
|
66
|
+
let sql = 'SELECT memory_entries.*, emb.embedding AS _emb FROM memory_entries LEFT JOIN memory_embeddings emb ON emb.entry_id = memory_entries.id WHERE 1=1';
|
|
67
|
+
const params = [];
|
|
68
|
+
if (query.namespace) {
|
|
69
|
+
sql += ' AND namespace = ?';
|
|
70
|
+
params.push(query.namespace);
|
|
71
|
+
}
|
|
72
|
+
if (query.key) {
|
|
73
|
+
sql += ' AND key = ?';
|
|
74
|
+
params.push(query.key);
|
|
75
|
+
}
|
|
76
|
+
if (query.keyPrefix) {
|
|
77
|
+
sql += ' AND key LIKE ?';
|
|
78
|
+
params.push(`${query.keyPrefix}%`);
|
|
79
|
+
}
|
|
80
|
+
if (query.memoryType) {
|
|
81
|
+
sql += ' AND type = ?';
|
|
82
|
+
params.push(query.memoryType);
|
|
83
|
+
}
|
|
84
|
+
if (query.accessLevel) {
|
|
85
|
+
sql += ' AND access_level = ?';
|
|
86
|
+
params.push(query.accessLevel);
|
|
87
|
+
}
|
|
88
|
+
if (query.ownerId) {
|
|
89
|
+
sql += ' AND owner_id = ?';
|
|
90
|
+
params.push(query.ownerId);
|
|
91
|
+
}
|
|
92
|
+
if (query.createdAfter) {
|
|
93
|
+
sql += ' AND created_at >= ?';
|
|
94
|
+
params.push(query.createdAfter);
|
|
95
|
+
}
|
|
96
|
+
if (query.createdBefore) {
|
|
97
|
+
sql += ' AND created_at <= ?';
|
|
98
|
+
params.push(query.createdBefore);
|
|
99
|
+
}
|
|
100
|
+
if (query.updatedAfter) {
|
|
101
|
+
sql += ' AND updated_at >= ?';
|
|
102
|
+
params.push(query.updatedAfter);
|
|
103
|
+
}
|
|
104
|
+
if (query.updatedBefore) {
|
|
105
|
+
sql += ' AND updated_at <= ?';
|
|
106
|
+
params.push(query.updatedBefore);
|
|
107
|
+
}
|
|
108
|
+
// Bi-temporal event-time filters (arXiv:2501.13956 — Zep/Graphiti)
|
|
109
|
+
if (query.eventAfter) {
|
|
110
|
+
sql += ' AND event_at >= ?';
|
|
111
|
+
params.push(query.eventAfter);
|
|
112
|
+
}
|
|
113
|
+
if (query.eventBefore) {
|
|
114
|
+
sql += ' AND event_at <= ?';
|
|
115
|
+
params.push(query.eventBefore);
|
|
116
|
+
}
|
|
117
|
+
if (!query.includeExpired) {
|
|
118
|
+
sql += ' AND (expires_at IS NULL OR expires_at > ?)';
|
|
119
|
+
params.push(Date.now());
|
|
120
|
+
}
|
|
121
|
+
// MemoryQuery.tags is documented as "entries must have all specified tags"
|
|
122
|
+
// (types.ts). Counting distinct matches enforces that; an EXISTS(... IN ...)
|
|
123
|
+
// would be ANY-match, which is exactly the divergence that made the two old
|
|
124
|
+
// backends return different result sets for the same call.
|
|
125
|
+
if (query.tags && query.tags.length > 0) {
|
|
126
|
+
this.validateTags(query.tags);
|
|
127
|
+
const placeholders = query.tags.map(() => '?').join(', ');
|
|
128
|
+
sql += ` AND (
|
|
129
|
+
SELECT COUNT(DISTINCT t.tag) FROM memory_entry_tags t
|
|
130
|
+
WHERE t.entry_id = memory_entries.id AND t.tag IN (${placeholders})
|
|
131
|
+
) = ?`;
|
|
132
|
+
params.push(...query.tags, query.tags.length);
|
|
133
|
+
}
|
|
134
|
+
const colMap = {
|
|
135
|
+
createdAt: 'created_at',
|
|
136
|
+
updatedAt: 'updated_at',
|
|
137
|
+
lastAccessedAt: 'last_accessed_at',
|
|
138
|
+
accessCount: 'access_count',
|
|
139
|
+
key: 'key',
|
|
140
|
+
};
|
|
141
|
+
const orderCol = query.sortField && query.sortField !== 'score' && colMap[query.sortField]
|
|
142
|
+
? colMap[query.sortField]
|
|
143
|
+
: 'created_at';
|
|
144
|
+
const orderDir = query.sortDirection === 'asc' ? 'ASC' : 'DESC';
|
|
145
|
+
sql += ` ORDER BY ${orderCol} ${orderDir} LIMIT ?`;
|
|
146
|
+
const effectiveLimit = Math.min(Math.max(1, query.limit ?? MAX_QUERY_LIMIT), MAX_QUERY_LIMIT);
|
|
147
|
+
params.push(effectiveLimit);
|
|
148
|
+
if (query.offset) {
|
|
149
|
+
sql += ' OFFSET ?';
|
|
150
|
+
params.push(query.offset);
|
|
151
|
+
}
|
|
152
|
+
const rows = this.driver?.all(sql, params) ?? [];
|
|
153
|
+
const results = rows.map((r) => this.rowToEntry(r));
|
|
154
|
+
const duration = performance.now() - startTime;
|
|
155
|
+
this.stats.queryCount++;
|
|
156
|
+
this.stats.totalQueryTime += duration;
|
|
157
|
+
this.emit('query:executed', { query, resultCount: results.length, duration });
|
|
158
|
+
return results;
|
|
159
|
+
},
|
|
160
|
+
/**
|
|
161
|
+
* Semantic search. Below MONOMIND_HNSW_THRESHOLD active embedded entries
|
|
162
|
+
* (default 5000), brute-force cosine over stored embeddings — namespace-
|
|
163
|
+
* and TTL-filtered in SQL — stays cheaper (a few tens of ms at second-brain
|
|
164
|
+
* scale). Above it, getAnnIndex() builds (or loads a persisted) HNSW graph
|
|
165
|
+
* and this searches that instead; results are still namespace/threshold
|
|
166
|
+
* filtered post-search to match the brute-force semantics exactly.
|
|
167
|
+
*/
|
|
168
|
+
async search(embedding, options) {
|
|
169
|
+
this.ensureInitialized();
|
|
170
|
+
const ns = options.filters?.namespace;
|
|
171
|
+
const ann = await this.getAnnIndex(embedding.length).catch(() => null);
|
|
172
|
+
if (ann) {
|
|
173
|
+
const applyFilters = (raw) => {
|
|
174
|
+
const out = [];
|
|
175
|
+
for (const r of raw) {
|
|
176
|
+
const entry = ann.entries.get(r.id);
|
|
177
|
+
if (!entry)
|
|
178
|
+
continue;
|
|
179
|
+
if (ns && entry.namespace !== ns)
|
|
180
|
+
continue;
|
|
181
|
+
const score = 1 - r.distance;
|
|
182
|
+
if (options.threshold !== undefined && score < options.threshold)
|
|
183
|
+
continue;
|
|
184
|
+
out.push({ entry, score, distance: r.distance });
|
|
185
|
+
if (out.length >= options.k)
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
return out;
|
|
189
|
+
};
|
|
190
|
+
const overFetch = Math.max(options.k * 4, options.k + 20);
|
|
191
|
+
let results = applyFilters(await ann.index.search(embedding, Math.min(overFetch, ann.entries.size)));
|
|
192
|
+
// A fixed over-fetch multiple assumes matches are spread roughly evenly
|
|
193
|
+
// through the globally-nearest candidates. A namespace filter can
|
|
194
|
+
// violate that — a namespace's true nearest neighbors may simply not be
|
|
195
|
+
// among the top `overFetch` globally, understating recall (or
|
|
196
|
+
// returning nothing) even though matches exist elsewhere in the graph.
|
|
197
|
+
// There's no way to know how deep those matches rank without searching
|
|
198
|
+
// further, so the only correct fallback is to widen all the way to the
|
|
199
|
+
// full index rather than guessing a bigger-but-still-arbitrary number.
|
|
200
|
+
if (ns && results.length < options.k && ann.entries.size > overFetch) {
|
|
201
|
+
results = applyFilters(await ann.index.search(embedding, ann.entries.size));
|
|
202
|
+
}
|
|
203
|
+
results.sort((a, b) => b.score - a.score);
|
|
204
|
+
return results;
|
|
205
|
+
}
|
|
206
|
+
const rows = this.driver?.iterate(`SELECT e.*, emb.embedding AS _emb
|
|
207
|
+
FROM memory_entries e
|
|
208
|
+
JOIN memory_embeddings emb ON emb.entry_id = e.id
|
|
209
|
+
WHERE (e.expires_at IS NULL OR e.expires_at = 0 OR e.expires_at > ?)
|
|
210
|
+
${ns ? 'AND e.namespace = ?' : ''}`, ns ? [Date.now(), ns] : [Date.now()]) ?? [];
|
|
211
|
+
const results = [];
|
|
212
|
+
for (const row of rows) {
|
|
213
|
+
const buf = row._emb;
|
|
214
|
+
if (!buf || buf.byteLength % 4 !== 0)
|
|
215
|
+
continue;
|
|
216
|
+
const vec = new Float32Array(buf.buffer, buf.byteOffset, buf.byteLength / 4);
|
|
217
|
+
if (vec.length !== embedding.length)
|
|
218
|
+
continue;
|
|
219
|
+
const similarity = cosineSimilarity(embedding, vec);
|
|
220
|
+
if (options.threshold !== undefined && similarity < options.threshold)
|
|
221
|
+
continue;
|
|
222
|
+
// PKG-3: row already carries _emb from the JOIN; rowToEntry reads it
|
|
223
|
+
// directly instead of re-querying memory_embeddings per row.
|
|
224
|
+
results.push({ entry: this.rowToEntry(row), score: similarity, distance: 1 - similarity });
|
|
225
|
+
}
|
|
226
|
+
results.sort((a, b) => b.score - a.score);
|
|
227
|
+
return results.slice(0, options.k);
|
|
228
|
+
},
|
|
229
|
+
/**
|
|
230
|
+
* FTS5-accelerated keyword search (Issue #66).
|
|
231
|
+
*
|
|
232
|
+
* When the FTS5 index is available, text matching runs inside SQLite via
|
|
233
|
+
* `MATCH` — orders of magnitude faster than loading 50k rows into JS. When
|
|
234
|
+
* FTS5 is unavailable (e.g. sql.js WASM compiled without the extension) the
|
|
235
|
+
* method returns `null` so the caller can fall back to JS-side matching.
|
|
236
|
+
*
|
|
237
|
+
* `queryText` is the raw user query; it is FTS5-tokenized automatically.
|
|
238
|
+
* Special characters are escaped to prevent FTS5 syntax errors.
|
|
239
|
+
*/
|
|
240
|
+
async keywordSearch(queryText, options = {}) {
|
|
241
|
+
this.ensureInitialized();
|
|
242
|
+
if (!this._fts5Available)
|
|
243
|
+
return null;
|
|
244
|
+
const limit = Math.min(Math.max(1, options.limit ?? 50), MAX_QUERY_LIMIT);
|
|
245
|
+
// Escape FTS5 special characters and build a query where every token must
|
|
246
|
+
// appear (implicit AND). Tokens shorter than 2 chars are dropped — they
|
|
247
|
+
// produce noise and FTS5 may reject single-char tokens depending on the
|
|
248
|
+
// tokenizer configuration.
|
|
249
|
+
const tokens = queryText
|
|
250
|
+
.replace(/[":*^~(){}[\]\\]/g, ' ')
|
|
251
|
+
.split(/\s+/)
|
|
252
|
+
.filter((t) => t.length > 1)
|
|
253
|
+
.map((t) => `"${t}"`);
|
|
254
|
+
if (!tokens.length)
|
|
255
|
+
return null;
|
|
256
|
+
const d = this.driver;
|
|
257
|
+
const ns = options.namespace;
|
|
258
|
+
const search = (match) => d.all(`SELECT f.entry_id, f.key, f.content, e.namespace, rank
|
|
259
|
+
FROM memory_entries_fts f
|
|
260
|
+
JOIN memory_entries e ON e.id = f.entry_id
|
|
261
|
+
WHERE memory_entries_fts MATCH ?
|
|
262
|
+
AND (e.expires_at IS NULL OR e.expires_at = 0 OR e.expires_at > ?)
|
|
263
|
+
${ns ? 'AND e.namespace = ?' : ''}
|
|
264
|
+
ORDER BY rank
|
|
265
|
+
LIMIT ?`, ns ? [match, Date.now(), ns, limit] : [match, Date.now(), limit]);
|
|
266
|
+
// Preserve the precise all-terms query first. Natural-language queries
|
|
267
|
+
// often contain context words absent from the target document, though; a
|
|
268
|
+
// zero-result strict search should still surface the best lexical leads.
|
|
269
|
+
let rows = search(tokens.join(' '));
|
|
270
|
+
if (rows.length === 0 && tokens.length > 1)
|
|
271
|
+
rows = search(tokens.join(' OR '));
|
|
272
|
+
return rows.map((r) => ({
|
|
273
|
+
id: String(r.entry_id),
|
|
274
|
+
key: String(r.key),
|
|
275
|
+
content: String(r.content),
|
|
276
|
+
namespace: String(r.namespace),
|
|
277
|
+
// FTS5 rank is negative (lower = better match); invert to a 0–1 score.
|
|
278
|
+
rank: Number(r.rank),
|
|
279
|
+
}));
|
|
280
|
+
},
|
|
281
|
+
async count(namespace) {
|
|
282
|
+
this.ensureInitialized();
|
|
283
|
+
const row = namespace
|
|
284
|
+
? this.driver?.get('SELECT COUNT(*) as count FROM memory_entries WHERE namespace = ?', [
|
|
285
|
+
namespace,
|
|
286
|
+
])
|
|
287
|
+
: this.driver?.get('SELECT COUNT(*) as count FROM memory_entries');
|
|
288
|
+
return Number(row?.count ?? 0);
|
|
289
|
+
},
|
|
290
|
+
async listNamespaces() {
|
|
291
|
+
this.ensureInitialized();
|
|
292
|
+
return (this.driver?.all('SELECT DISTINCT namespace FROM memory_entries') ?? []).map((r) => String(r.namespace));
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
//# sourceMappingURL=sql-backend-reads.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sql-backend-reads.js","sourceRoot":"","sources":["../src/sql-backend-reads.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Write-path methods for SqlBackend (store/update/delete family).
|
|
3
|
+
*
|
|
4
|
+
* File-size sweep: split out of sql-backend.ts. Mixed into
|
|
5
|
+
* SqlBackend.prototype at the bottom of sql-backend.ts.
|
|
6
|
+
*
|
|
7
|
+
* @module v1/memory/sql-backend-writes
|
|
8
|
+
*/
|
|
9
|
+
import type { SqlBackend } from './sql-backend.js';
|
|
10
|
+
import type { MemoryEntry, MemoryEntryUpdate } from './types.js';
|
|
11
|
+
export declare const sqlBackendWriteMethods: {
|
|
12
|
+
store(this: SqlBackend, entry: MemoryEntry): Promise<void>;
|
|
13
|
+
/**
|
|
14
|
+
* Compare-and-swap update: writes ONLY if the row's stored `version` still
|
|
15
|
+
* equals `expectedVersion`. The check and the write are one SQL statement
|
|
16
|
+
* (`UPDATE ... WHERE id = ? AND version = ?`), so SQLite's own single-
|
|
17
|
+
* statement atomicity — the same guarantee `store()`'s INSERT OR REPLACE
|
|
18
|
+
* already relies on — makes this a real compare-and-swap against every
|
|
19
|
+
* other writer, in this process or another, with no bridge-level lock.
|
|
20
|
+
*
|
|
21
|
+
* Exists for memory-KG review finding K5: a getByKey() + store() read-
|
|
22
|
+
* merge-write (what every caller did before this) leaves a window in which
|
|
23
|
+
* two concurrent callers each merge onto the same row and the second
|
|
24
|
+
* store() silently overwrites the first's contribution. A caller that reads
|
|
25
|
+
* a row's version, merges onto it, and writes back through here instead
|
|
26
|
+
* gets `false` when its merge has gone stale, rather than winning a race it
|
|
27
|
+
* did not know it was running.
|
|
28
|
+
*
|
|
29
|
+
* Only the columns a claim-ledger merge (memory-kg.ts) actually changes are
|
|
30
|
+
* updated; embeddings are left untouched, matching storeSync's own rule that
|
|
31
|
+
* a revision without a new vector keeps the one it had.
|
|
32
|
+
*
|
|
33
|
+
* @returns true when the write landed, false when the row's version had
|
|
34
|
+
* already moved (or the row no longer exists) — the caller must re-read and
|
|
35
|
+
* re-merge, never assume the write happened.
|
|
36
|
+
*/
|
|
37
|
+
storeIfVersion(this: SqlBackend, entry: MemoryEntry, expectedVersion: number): Promise<boolean>;
|
|
38
|
+
/**
|
|
39
|
+
* Compare-and-swap create: the create-side counterpart to `storeIfVersion`.
|
|
40
|
+
* Inserts ONLY if no row already occupies this id or this namespace/key pair
|
|
41
|
+
* (the schema's `UNIQUE(namespace, key)` index — see sql-schema.ts).
|
|
42
|
+
*
|
|
43
|
+
* Exists for the same K5 race on the CREATE side: two concurrent callers
|
|
44
|
+
* asserting the same new KG entity mint the same deterministic `key` but a
|
|
45
|
+
* different random row `id` (see memory-bridge.ts's upsert). Plain `store()`
|
|
46
|
+
* (INSERT OR REPLACE) resolves that collision by silently deleting whichever
|
|
47
|
+
* row landed first — exactly the lost update K5 describes, just on first
|
|
48
|
+
* write instead of a merge. `INSERT OR IGNORE` instead leaves the first row
|
|
49
|
+
* untouched and reports that it did, so the loser re-reads and merges onto
|
|
50
|
+
* it instead of clobbering it.
|
|
51
|
+
*
|
|
52
|
+
* @returns true when this call created the row, false when one already
|
|
53
|
+
* existed — the caller must re-read and merge rather than assume it won.
|
|
54
|
+
*/
|
|
55
|
+
storeIfAbsent(this: SqlBackend, entry: MemoryEntry): Promise<boolean>;
|
|
56
|
+
/** Synchronous store body, shared by store() and bulkInsert(). */
|
|
57
|
+
storeSync(this: SqlBackend, entry: MemoryEntry): void;
|
|
58
|
+
bulkInsert(this: SqlBackend, entries: MemoryEntry[]): Promise<void>;
|
|
59
|
+
update(this: SqlBackend, id: string, update: MemoryEntryUpdate): Promise<MemoryEntry | null>;
|
|
60
|
+
delete(this: SqlBackend, id: string): Promise<boolean>;
|
|
61
|
+
bulkDelete(this: SqlBackend, ids: string[]): Promise<number>;
|
|
62
|
+
clearNamespace(this: SqlBackend, namespace: string): Promise<number>;
|
|
63
|
+
};
|
|
64
|
+
//# sourceMappingURL=sql-backend-writes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sql-backend-writes.d.ts","sourceRoot":"","sources":["../src/sql-backend-writes.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,OAAO,KAAK,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEjE,eAAO,MAAM,sBAAsB;IAC3B,KAAK,OAAO,UAAU,SAAS,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAahE;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACG,cAAc,OACZ,UAAU,SACT,WAAW,mBACD,MAAM,GACtB,OAAO,CAAC,OAAO,CAAC;IAgCnB;;;;;;;;;;;;;;;;OAgBG;IACG,aAAa,OAAO,UAAU,SAAS,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC;IAqD3E,kEAAkE;IAClE,SAAS,OAAO,UAAU,SAAS,WAAW,GAAG,IAAI;IAwE/C,UAAU,OAAO,UAAU,WAAW,WAAW,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IASnE,MAAM,OACJ,UAAU,MACZ,MAAM,UACF,iBAAiB,GACxB,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;IAoBxB,MAAM,OAAO,UAAU,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAetD,UAAU,OAAO,UAAU,OAAO,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC;IAgB5D,cAAc,OAAO,UAAU,aAAa,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAgB3E,CAAC"}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Write-path methods for SqlBackend (store/update/delete family).
|
|
3
|
+
*
|
|
4
|
+
* File-size sweep: split out of sql-backend.ts. Mixed into
|
|
5
|
+
* SqlBackend.prototype at the bottom of sql-backend.ts.
|
|
6
|
+
*
|
|
7
|
+
* @module v1/memory/sql-backend-writes
|
|
8
|
+
*/
|
|
9
|
+
export const sqlBackendWriteMethods = {
|
|
10
|
+
async store(entry) {
|
|
11
|
+
this.ensureInitialized();
|
|
12
|
+
const startTime = performance.now();
|
|
13
|
+
this.validateTags(entry.tags);
|
|
14
|
+
this.driver?.transaction(() => this.storeSync(entry));
|
|
15
|
+
const duration = performance.now() - startTime;
|
|
16
|
+
this.stats.writeCount++;
|
|
17
|
+
this.stats.totalWriteTime += duration;
|
|
18
|
+
this.emit('entry:stored', { id: entry.id, duration });
|
|
19
|
+
},
|
|
20
|
+
/**
|
|
21
|
+
* Compare-and-swap update: writes ONLY if the row's stored `version` still
|
|
22
|
+
* equals `expectedVersion`. The check and the write are one SQL statement
|
|
23
|
+
* (`UPDATE ... WHERE id = ? AND version = ?`), so SQLite's own single-
|
|
24
|
+
* statement atomicity — the same guarantee `store()`'s INSERT OR REPLACE
|
|
25
|
+
* already relies on — makes this a real compare-and-swap against every
|
|
26
|
+
* other writer, in this process or another, with no bridge-level lock.
|
|
27
|
+
*
|
|
28
|
+
* Exists for memory-KG review finding K5: a getByKey() + store() read-
|
|
29
|
+
* merge-write (what every caller did before this) leaves a window in which
|
|
30
|
+
* two concurrent callers each merge onto the same row and the second
|
|
31
|
+
* store() silently overwrites the first's contribution. A caller that reads
|
|
32
|
+
* a row's version, merges onto it, and writes back through here instead
|
|
33
|
+
* gets `false` when its merge has gone stale, rather than winning a race it
|
|
34
|
+
* did not know it was running.
|
|
35
|
+
*
|
|
36
|
+
* Only the columns a claim-ledger merge (memory-kg.ts) actually changes are
|
|
37
|
+
* updated; embeddings are left untouched, matching storeSync's own rule that
|
|
38
|
+
* a revision without a new vector keeps the one it had.
|
|
39
|
+
*
|
|
40
|
+
* @returns true when the write landed, false when the row's version had
|
|
41
|
+
* already moved (or the row no longer exists) — the caller must re-read and
|
|
42
|
+
* re-merge, never assume the write happened.
|
|
43
|
+
*/
|
|
44
|
+
async storeIfVersion(entry, expectedVersion) {
|
|
45
|
+
this.ensureInitialized();
|
|
46
|
+
this.validateTags(entry.tags);
|
|
47
|
+
const d = this.driver;
|
|
48
|
+
return d.transaction(() => {
|
|
49
|
+
const changes = d.run(`UPDATE memory_entries
|
|
50
|
+
SET content = ?, tags = ?, metadata = ?, updated_at = ?, version = ?, "references" = ?
|
|
51
|
+
WHERE id = ? AND version = ?`, [
|
|
52
|
+
entry.content,
|
|
53
|
+
JSON.stringify(entry.tags),
|
|
54
|
+
JSON.stringify(entry.metadata),
|
|
55
|
+
entry.updatedAt,
|
|
56
|
+
entry.version,
|
|
57
|
+
JSON.stringify(entry.references),
|
|
58
|
+
entry.id,
|
|
59
|
+
expectedVersion,
|
|
60
|
+
]);
|
|
61
|
+
if (changes === 0)
|
|
62
|
+
return false;
|
|
63
|
+
d.run('DELETE FROM memory_entry_tags WHERE entry_id = ?', [entry.id]);
|
|
64
|
+
for (const tag of entry.tags) {
|
|
65
|
+
d.run('INSERT OR IGNORE INTO memory_entry_tags (entry_id, tag) VALUES (?, ?)', [
|
|
66
|
+
entry.id,
|
|
67
|
+
tag,
|
|
68
|
+
]);
|
|
69
|
+
}
|
|
70
|
+
return true;
|
|
71
|
+
});
|
|
72
|
+
},
|
|
73
|
+
/**
|
|
74
|
+
* Compare-and-swap create: the create-side counterpart to `storeIfVersion`.
|
|
75
|
+
* Inserts ONLY if no row already occupies this id or this namespace/key pair
|
|
76
|
+
* (the schema's `UNIQUE(namespace, key)` index — see sql-schema.ts).
|
|
77
|
+
*
|
|
78
|
+
* Exists for the same K5 race on the CREATE side: two concurrent callers
|
|
79
|
+
* asserting the same new KG entity mint the same deterministic `key` but a
|
|
80
|
+
* different random row `id` (see memory-bridge.ts's upsert). Plain `store()`
|
|
81
|
+
* (INSERT OR REPLACE) resolves that collision by silently deleting whichever
|
|
82
|
+
* row landed first — exactly the lost update K5 describes, just on first
|
|
83
|
+
* write instead of a merge. `INSERT OR IGNORE` instead leaves the first row
|
|
84
|
+
* untouched and reports that it did, so the loser re-reads and merges onto
|
|
85
|
+
* it instead of clobbering it.
|
|
86
|
+
*
|
|
87
|
+
* @returns true when this call created the row, false when one already
|
|
88
|
+
* existed — the caller must re-read and merge rather than assume it won.
|
|
89
|
+
*/
|
|
90
|
+
async storeIfAbsent(entry) {
|
|
91
|
+
this.ensureInitialized();
|
|
92
|
+
this.validateTags(entry.tags);
|
|
93
|
+
const d = this.driver;
|
|
94
|
+
return d.transaction(() => {
|
|
95
|
+
const changes = d.run(`INSERT OR IGNORE INTO memory_entries (
|
|
96
|
+
id, key, content, type, namespace, tags, metadata, owner_id, access_level,
|
|
97
|
+
created_at, updated_at, expires_at, event_at, version, "references",
|
|
98
|
+
access_count, last_accessed_at
|
|
99
|
+
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`, [
|
|
100
|
+
entry.id,
|
|
101
|
+
entry.key,
|
|
102
|
+
entry.content,
|
|
103
|
+
entry.type,
|
|
104
|
+
entry.namespace,
|
|
105
|
+
JSON.stringify(entry.tags),
|
|
106
|
+
JSON.stringify(entry.metadata),
|
|
107
|
+
entry.ownerId || null,
|
|
108
|
+
entry.accessLevel,
|
|
109
|
+
entry.createdAt,
|
|
110
|
+
entry.updatedAt,
|
|
111
|
+
entry.expiresAt || null,
|
|
112
|
+
entry.eventAt ?? null,
|
|
113
|
+
entry.version,
|
|
114
|
+
JSON.stringify(entry.references),
|
|
115
|
+
entry.accessCount,
|
|
116
|
+
entry.lastAccessedAt,
|
|
117
|
+
]);
|
|
118
|
+
if (changes === 0)
|
|
119
|
+
return false;
|
|
120
|
+
for (const tag of entry.tags) {
|
|
121
|
+
d.run('INSERT OR IGNORE INTO memory_entry_tags (entry_id, tag) VALUES (?, ?)', [
|
|
122
|
+
entry.id,
|
|
123
|
+
tag,
|
|
124
|
+
]);
|
|
125
|
+
}
|
|
126
|
+
if (entry.embedding && entry.embedding.byteLength > 0) {
|
|
127
|
+
const bytes = Buffer.from(entry.embedding.buffer, entry.embedding.byteOffset, entry.embedding.byteLength);
|
|
128
|
+
d.run('INSERT OR REPLACE INTO memory_embeddings (entry_id, embedding) VALUES (?, ?)', [
|
|
129
|
+
entry.id,
|
|
130
|
+
bytes,
|
|
131
|
+
]);
|
|
132
|
+
}
|
|
133
|
+
return true;
|
|
134
|
+
});
|
|
135
|
+
},
|
|
136
|
+
/** Synchronous store body, shared by store() and bulkInsert(). */
|
|
137
|
+
storeSync(entry) {
|
|
138
|
+
const d = this.driver;
|
|
139
|
+
// Read any existing embedding BEFORE INSERT OR REPLACE fires the CASCADE
|
|
140
|
+
// that would delete it — an entry updated without a vector must keep the
|
|
141
|
+
// one it already had.
|
|
142
|
+
let embeddingToStore = entry.embedding;
|
|
143
|
+
if (!embeddingToStore) {
|
|
144
|
+
const existing = d.get('SELECT embedding FROM memory_embeddings WHERE entry_id = ?', [
|
|
145
|
+
entry.id,
|
|
146
|
+
]);
|
|
147
|
+
const buf = existing?.embedding;
|
|
148
|
+
if (buf && buf.byteLength > 0) {
|
|
149
|
+
embeddingToStore = new Float32Array(buf.buffer, buf.byteOffset, buf.byteLength / 4);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
d.run(`INSERT OR REPLACE INTO memory_entries (
|
|
153
|
+
id, key, content, type, namespace, tags, metadata, owner_id, access_level,
|
|
154
|
+
created_at, updated_at, expires_at, event_at, version, "references",
|
|
155
|
+
access_count, last_accessed_at
|
|
156
|
+
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`, [
|
|
157
|
+
entry.id,
|
|
158
|
+
entry.key,
|
|
159
|
+
entry.content,
|
|
160
|
+
entry.type,
|
|
161
|
+
entry.namespace,
|
|
162
|
+
JSON.stringify(entry.tags),
|
|
163
|
+
JSON.stringify(entry.metadata),
|
|
164
|
+
entry.ownerId || null,
|
|
165
|
+
entry.accessLevel,
|
|
166
|
+
entry.createdAt,
|
|
167
|
+
entry.updatedAt,
|
|
168
|
+
entry.expiresAt || null,
|
|
169
|
+
entry.eventAt ?? null,
|
|
170
|
+
entry.version,
|
|
171
|
+
JSON.stringify(entry.references),
|
|
172
|
+
entry.accessCount,
|
|
173
|
+
entry.lastAccessedAt,
|
|
174
|
+
]);
|
|
175
|
+
d.run('DELETE FROM memory_entry_tags WHERE entry_id = ?', [entry.id]);
|
|
176
|
+
for (const tag of entry.tags) {
|
|
177
|
+
d.run('INSERT OR IGNORE INTO memory_entry_tags (entry_id, tag) VALUES (?, ?)', [
|
|
178
|
+
entry.id,
|
|
179
|
+
tag,
|
|
180
|
+
]);
|
|
181
|
+
}
|
|
182
|
+
if (embeddingToStore) {
|
|
183
|
+
// Slice by byteOffset/byteLength: for copies under Node's 4KB pooling
|
|
184
|
+
// threshold, `.buffer` refers to the entire shared pool, so writing it
|
|
185
|
+
// whole silently stores unrelated memory.
|
|
186
|
+
const bytes = Buffer.from(embeddingToStore.buffer, embeddingToStore.byteOffset, embeddingToStore.byteLength);
|
|
187
|
+
d.run('INSERT OR REPLACE INTO memory_embeddings (entry_id, embedding) VALUES (?, ?)', [
|
|
188
|
+
entry.id,
|
|
189
|
+
bytes,
|
|
190
|
+
]);
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
async bulkInsert(entries) {
|
|
194
|
+
this.ensureInitialized();
|
|
195
|
+
for (const e of entries)
|
|
196
|
+
this.validateTags(e.tags);
|
|
197
|
+
this.driver?.transaction(() => {
|
|
198
|
+
for (const entry of entries)
|
|
199
|
+
this.storeSync(entry);
|
|
200
|
+
});
|
|
201
|
+
this.emit('bulk:inserted', { count: entries.length });
|
|
202
|
+
},
|
|
203
|
+
async update(id, update) {
|
|
204
|
+
this.ensureInitialized();
|
|
205
|
+
const entry = await this.get(id);
|
|
206
|
+
if (!entry)
|
|
207
|
+
return null;
|
|
208
|
+
if (update.content !== undefined)
|
|
209
|
+
entry.content = update.content;
|
|
210
|
+
if (update.tags !== undefined)
|
|
211
|
+
entry.tags = update.tags;
|
|
212
|
+
if (update.metadata !== undefined)
|
|
213
|
+
entry.metadata = { ...entry.metadata, ...update.metadata };
|
|
214
|
+
if (update.accessLevel !== undefined)
|
|
215
|
+
entry.accessLevel = update.accessLevel;
|
|
216
|
+
if (update.expiresAt !== undefined)
|
|
217
|
+
entry.expiresAt = update.expiresAt;
|
|
218
|
+
if (update.references !== undefined)
|
|
219
|
+
entry.references = update.references;
|
|
220
|
+
entry.updatedAt = Date.now();
|
|
221
|
+
entry.version++;
|
|
222
|
+
await this.store(entry);
|
|
223
|
+
this.emit('entry:updated', { id });
|
|
224
|
+
return entry;
|
|
225
|
+
},
|
|
226
|
+
async delete(id) {
|
|
227
|
+
this.ensureInitialized();
|
|
228
|
+
const d = this.driver;
|
|
229
|
+
// Explicit tag cleanup as well as the CASCADE — belt and braces, since FK
|
|
230
|
+
// enforcement is a pragma that not every driver honours.
|
|
231
|
+
d.run('DELETE FROM memory_entry_tags WHERE entry_id = ?', [id]);
|
|
232
|
+
d.run('DELETE FROM memory_embeddings WHERE entry_id = ?', [id]);
|
|
233
|
+
const changes = d.run('DELETE FROM memory_entries WHERE id = ?', [id]);
|
|
234
|
+
if (changes > 0) {
|
|
235
|
+
this.emit('entry:deleted', { id });
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
return false;
|
|
239
|
+
},
|
|
240
|
+
async bulkDelete(ids) {
|
|
241
|
+
this.ensureInitialized();
|
|
242
|
+
const d = this.driver;
|
|
243
|
+
const count = d.transaction(() => {
|
|
244
|
+
let deleted = 0;
|
|
245
|
+
for (const id of ids) {
|
|
246
|
+
d.run('DELETE FROM memory_entry_tags WHERE entry_id = ?', [id]);
|
|
247
|
+
d.run('DELETE FROM memory_embeddings WHERE entry_id = ?', [id]);
|
|
248
|
+
if (d.run('DELETE FROM memory_entries WHERE id = ?', [id]) > 0)
|
|
249
|
+
deleted++;
|
|
250
|
+
}
|
|
251
|
+
return deleted;
|
|
252
|
+
});
|
|
253
|
+
this.emit('bulk:deleted', { count });
|
|
254
|
+
return count;
|
|
255
|
+
},
|
|
256
|
+
async clearNamespace(namespace) {
|
|
257
|
+
this.ensureInitialized();
|
|
258
|
+
const d = this.driver;
|
|
259
|
+
const count = d.transaction(() => {
|
|
260
|
+
d.run(`DELETE FROM memory_entry_tags
|
|
261
|
+
WHERE entry_id IN (SELECT id FROM memory_entries WHERE namespace = ?)`, [namespace]);
|
|
262
|
+
const changes = d.run('DELETE FROM memory_entries WHERE namespace = ?', [namespace]);
|
|
263
|
+
d.run('DELETE FROM memory_embeddings WHERE entry_id NOT IN (SELECT id FROM memory_entries)');
|
|
264
|
+
return changes;
|
|
265
|
+
});
|
|
266
|
+
this.emit('namespace:cleared', { namespace, count });
|
|
267
|
+
return count;
|
|
268
|
+
},
|
|
269
|
+
};
|
|
270
|
+
//# sourceMappingURL=sql-backend-writes.js.map
|