zelari-code 2.6.3 → 2.8.0
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 +24 -2
- package/dist/cli/hooks/useChatTurn.js +109 -1
- package/dist/cli/hooks/useChatTurn.js.map +1 -1
- package/dist/cli/hooks/useSlashDispatch.js +7 -0
- package/dist/cli/hooks/useSlashDispatch.js.map +1 -1
- package/dist/cli/kraken/executor.js +73 -0
- package/dist/cli/kraken/executor.js.map +1 -1
- package/dist/cli/main.bundled.js +5323 -1502
- package/dist/cli/main.bundled.js.map +4 -4
- package/dist/cli/main.js +46 -0
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/memory/fileBackend.js +8 -0
- package/dist/cli/memory/fileBackend.js.map +1 -1
- package/dist/cli/memory/jsonApi.js +83 -0
- package/dist/cli/memory/jsonApi.js.map +1 -0
- package/dist/cli/memory/legacyImport.js +91 -0
- package/dist/cli/memory/legacyImport.js.map +1 -0
- package/dist/cli/memory/mcpAdapter.js +255 -0
- package/dist/cli/memory/mcpAdapter.js.map +1 -0
- package/dist/cli/memory/mcpCli.js +19 -0
- package/dist/cli/memory/mcpCli.js.map +1 -0
- package/dist/cli/memory/mcpServer.js +126 -0
- package/dist/cli/memory/mcpServer.js.map +1 -0
- package/dist/cli/memory/promotion.js +68 -0
- package/dist/cli/memory/promotion.js.map +1 -0
- package/dist/cli/memory/serviceFactory.js +94 -0
- package/dist/cli/memory/serviceFactory.js.map +1 -0
- package/dist/cli/memory/sqliteBackend.js +636 -0
- package/dist/cli/memory/sqliteBackend.js.map +1 -0
- package/dist/cli/memory/sqliteCodec.js +114 -0
- package/dist/cli/memory/sqliteCodec.js.map +1 -0
- package/dist/cli/memory/sqliteRpc.js +120 -0
- package/dist/cli/memory/sqliteRpc.js.map +1 -0
- package/dist/cli/memory/sqliteSchema.js +181 -0
- package/dist/cli/memory/sqliteSchema.js.map +1 -0
- package/dist/cli/memory/sqliteWorker.mjs +264 -0
- package/dist/cli/runHeadless.js +97 -3
- package/dist/cli/runHeadless.js.map +1 -1
- package/dist/cli/semantic/embeddings.js +13 -1
- package/dist/cli/semantic/embeddings.js.map +1 -1
- package/dist/cli/semantic/provider.js +11 -2
- package/dist/cli/semantic/provider.js.map +1 -1
- package/dist/cli/slashCommands.js +10 -0
- package/dist/cli/slashCommands.js.map +1 -1
- package/dist/cli/slashHandlers/krakenGraph.js +11 -0
- package/dist/cli/slashHandlers/krakenGraph.js.map +1 -1
- package/dist/cli/slashHandlers/memory.js +239 -0
- package/dist/cli/slashHandlers/memory.js.map +1 -0
- package/dist/cli/toolRegistry.js +4 -0
- package/dist/cli/toolRegistry.js.map +1 -1
- package/dist/cli/tools/taskTool.js +41 -0
- package/dist/cli/tools/taskTool.js.map +1 -1
- package/dist/cli/zelariMission.js +6 -0
- package/dist/cli/zelariMission.js.map +1 -1
- package/package.json +4 -3
|
@@ -0,0 +1,636 @@
|
|
|
1
|
+
/** Native SQLite cognitive-memory backend (Node 24 `node:sqlite`). */
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
+
import { promises as fs } from 'node:fs';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import { MemoryEdgeInputSchema, MemoryEdgeSchema, MemoryNodeSchema, lexicalRelevance, } from '@zelari/core/memory';
|
|
6
|
+
import { decodeEdge, decodeNode, decodeVersion, edgeSqlValues, nodeSqlValues, } from './sqliteCodec.js';
|
|
7
|
+
import { SqliteWorkerRpc } from './sqliteRpc.js';
|
|
8
|
+
import { SQLITE_MEMORY_BASE_SCHEMA, SQLITE_MEMORY_FTS_SCHEMA, SQLITE_MEMORY_MIGRATIONS, SQLITE_MEMORY_SCHEMA_VERSION, } from './sqliteSchema.js';
|
|
9
|
+
const NODE_COLUMNS = `
|
|
10
|
+
id, schema_version, project_id, kind, content, importance, confidence,
|
|
11
|
+
status, visibility, tags_json, source_json, created_at, updated_at, valid_from,
|
|
12
|
+
valid_until, recorded_at, retracted_at, embedding_ref, metadata_json`;
|
|
13
|
+
const INSERT_NODE_SQL = `INSERT INTO memory_nodes (${NODE_COLUMNS})
|
|
14
|
+
VALUES (${Array.from({ length: 19 }, () => '?').join(',')})`;
|
|
15
|
+
const SOURCE_COLUMNS = {
|
|
16
|
+
agent: 'agent', sessionId: 'sessionId', missionId: 'missionId',
|
|
17
|
+
sliceId: 'sliceId', tentacleId: 'tentacleId', councilMemberId: 'councilMemberId',
|
|
18
|
+
skillId: 'skillId', verificationId: 'verificationId', file: 'file', symbol: 'symbol',
|
|
19
|
+
commit: 'commit', branch: 'branch', worktree: 'worktree', toolCallId: 'toolCallId',
|
|
20
|
+
client: 'client',
|
|
21
|
+
};
|
|
22
|
+
function boundedLimit(value, fallback = 50) {
|
|
23
|
+
return Math.max(1, Math.min(Math.floor(value ?? fallback), 100_000));
|
|
24
|
+
}
|
|
25
|
+
function ftsExpression(text) {
|
|
26
|
+
const terms = text.toLocaleLowerCase().match(/[\p{L}\p{N}_-]{2,}/gu) ?? [];
|
|
27
|
+
return [...new Set(terms)].slice(0, 32).map((term) => `"${term.replace(/"/g, '')}"*`).join(' OR ');
|
|
28
|
+
}
|
|
29
|
+
function withOptional(target, key, value) {
|
|
30
|
+
if (value === null || value === undefined || value === '')
|
|
31
|
+
return target;
|
|
32
|
+
return { ...target, [key]: value };
|
|
33
|
+
}
|
|
34
|
+
function contentHash(content) {
|
|
35
|
+
return createHash('sha256').update(content).digest('hex');
|
|
36
|
+
}
|
|
37
|
+
export class SQLiteMemoryBackend {
|
|
38
|
+
rpc = new SqliteWorkerRpc();
|
|
39
|
+
initialized = false;
|
|
40
|
+
fts = false;
|
|
41
|
+
projectRoot = '';
|
|
42
|
+
options;
|
|
43
|
+
databasePath = '';
|
|
44
|
+
lastMigration;
|
|
45
|
+
constructor(options = {}) {
|
|
46
|
+
this.options = options;
|
|
47
|
+
}
|
|
48
|
+
async init(projectRoot) {
|
|
49
|
+
let resolved;
|
|
50
|
+
try {
|
|
51
|
+
resolved = await fs.realpath(projectRoot);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
resolved = path.resolve(projectRoot);
|
|
55
|
+
}
|
|
56
|
+
if (this.initialized && resolved === this.projectRoot)
|
|
57
|
+
return;
|
|
58
|
+
if (this.initialized)
|
|
59
|
+
await this.close();
|
|
60
|
+
const filename = this.options.filename ?? 'memory.db';
|
|
61
|
+
if (path.basename(filename) !== filename || filename === '.' || filename === '..') {
|
|
62
|
+
throw new Error('SQLite memory filename must not contain a path.');
|
|
63
|
+
}
|
|
64
|
+
const zelariDirectory = path.join(resolved, '.zelari');
|
|
65
|
+
const directory = path.join(zelariDirectory, 'memory');
|
|
66
|
+
for (const candidate of [zelariDirectory, directory]) {
|
|
67
|
+
let stat;
|
|
68
|
+
try {
|
|
69
|
+
stat = await fs.lstat(candidate);
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
if (error.code !== 'ENOENT')
|
|
73
|
+
throw error;
|
|
74
|
+
try {
|
|
75
|
+
await fs.mkdir(candidate);
|
|
76
|
+
}
|
|
77
|
+
catch (mkdirError) {
|
|
78
|
+
if (mkdirError.code !== 'EEXIST')
|
|
79
|
+
throw mkdirError;
|
|
80
|
+
}
|
|
81
|
+
stat = await fs.lstat(candidate);
|
|
82
|
+
}
|
|
83
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
84
|
+
throw new Error(`Memory directory is not a real directory: ${candidate}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const canonicalDirectory = await fs.realpath(directory);
|
|
88
|
+
const relativeDirectory = path.relative(resolved, canonicalDirectory);
|
|
89
|
+
if (relativeDirectory.startsWith('..') || path.isAbsolute(relativeDirectory)) {
|
|
90
|
+
throw new Error('SQLite memory directory resolves outside the active project.');
|
|
91
|
+
}
|
|
92
|
+
this.projectRoot = resolved;
|
|
93
|
+
this.databasePath = path.join(canonicalDirectory, filename);
|
|
94
|
+
const opened = await this.rpc.open({
|
|
95
|
+
dbPath: this.databasePath,
|
|
96
|
+
schemaSql: SQLITE_MEMORY_BASE_SCHEMA,
|
|
97
|
+
ftsSql: SQLITE_MEMORY_FTS_SCHEMA,
|
|
98
|
+
schemaVersion: SQLITE_MEMORY_SCHEMA_VERSION,
|
|
99
|
+
migrations: SQLITE_MEMORY_MIGRATIONS,
|
|
100
|
+
timeoutMs: this.options.busyTimeoutMs ?? 5_000,
|
|
101
|
+
});
|
|
102
|
+
this.fts = opened.fts;
|
|
103
|
+
this.lastMigration = opened.migratedFrom === undefined
|
|
104
|
+
? undefined
|
|
105
|
+
: {
|
|
106
|
+
from: opened.migratedFrom,
|
|
107
|
+
to: SQLITE_MEMORY_SCHEMA_VERSION,
|
|
108
|
+
...(opened.backupPath ? { backupPath: opened.backupPath } : {}),
|
|
109
|
+
};
|
|
110
|
+
this.initialized = true;
|
|
111
|
+
}
|
|
112
|
+
assertReady() {
|
|
113
|
+
if (!this.initialized)
|
|
114
|
+
throw new Error('SQLiteMemoryBackend.init(projectRoot) must be called first.');
|
|
115
|
+
}
|
|
116
|
+
async add(input) {
|
|
117
|
+
this.assertReady();
|
|
118
|
+
const now = new Date().toISOString();
|
|
119
|
+
const createdAt = input.createdAt ?? now;
|
|
120
|
+
const recordedAt = input.recordedAt ?? createdAt;
|
|
121
|
+
const node = MemoryNodeSchema.parse({
|
|
122
|
+
id: input.id ?? `mem_${randomUUID()}`,
|
|
123
|
+
schemaVersion: 1,
|
|
124
|
+
projectId: input.projectId,
|
|
125
|
+
kind: input.kind,
|
|
126
|
+
content: input.content,
|
|
127
|
+
importance: input.importance ?? 0.5,
|
|
128
|
+
confidence: input.confidence ?? 0.5,
|
|
129
|
+
status: input.status ?? 'active',
|
|
130
|
+
visibility: input.visibility ?? 'project',
|
|
131
|
+
tags: input.tags ?? [],
|
|
132
|
+
source: input.source ?? {},
|
|
133
|
+
createdAt,
|
|
134
|
+
updatedAt: now,
|
|
135
|
+
validFrom: input.validFrom,
|
|
136
|
+
validUntil: input.validUntil,
|
|
137
|
+
recordedAt,
|
|
138
|
+
embeddingRef: input.embeddingRef,
|
|
139
|
+
metadata: input.metadata ?? {},
|
|
140
|
+
});
|
|
141
|
+
await this.rpc.batch([
|
|
142
|
+
{ sql: INSERT_NODE_SQL, params: nodeSqlValues(node) },
|
|
143
|
+
{
|
|
144
|
+
sql: `INSERT INTO memory_versions
|
|
145
|
+
(version_id, memory_id, revision, snapshot_json, recorded_at, actor, reason)
|
|
146
|
+
VALUES (?, ?, 1, ?, ?, ?, ?)`,
|
|
147
|
+
params: [
|
|
148
|
+
`ver_${randomUUID()}`, node.id, JSON.stringify(node), recordedAt,
|
|
149
|
+
node.source.agent ?? null, 'created',
|
|
150
|
+
],
|
|
151
|
+
},
|
|
152
|
+
]);
|
|
153
|
+
return node;
|
|
154
|
+
}
|
|
155
|
+
async get(id) {
|
|
156
|
+
this.assertReady();
|
|
157
|
+
const row = await this.rpc.statement({
|
|
158
|
+
sql: 'SELECT * FROM memory_nodes WHERE id = ?', params: [id], mode: 'get',
|
|
159
|
+
});
|
|
160
|
+
return decodeNode(row);
|
|
161
|
+
}
|
|
162
|
+
async update(id, patch) {
|
|
163
|
+
const current = await this.get(id);
|
|
164
|
+
if (!current)
|
|
165
|
+
throw new Error(`Memory ${id} was not found.`);
|
|
166
|
+
const now = new Date().toISOString();
|
|
167
|
+
let updated = {
|
|
168
|
+
...current,
|
|
169
|
+
...(patch.kind ? { kind: patch.kind } : {}),
|
|
170
|
+
...(patch.content !== undefined ? { content: patch.content } : {}),
|
|
171
|
+
...(patch.importance !== undefined ? { importance: patch.importance } : {}),
|
|
172
|
+
...(patch.confidence !== undefined ? { confidence: patch.confidence } : {}),
|
|
173
|
+
...(patch.status ? { status: patch.status } : {}),
|
|
174
|
+
...(patch.visibility ? { visibility: patch.visibility } : {}),
|
|
175
|
+
...(patch.tags ? { tags: patch.tags } : {}),
|
|
176
|
+
...(patch.source ? { source: patch.source } : {}),
|
|
177
|
+
...(patch.metadata ? { metadata: patch.metadata } : {}),
|
|
178
|
+
updatedAt: now,
|
|
179
|
+
recordedAt: now,
|
|
180
|
+
};
|
|
181
|
+
updated = withOptional(updated, 'validFrom', patch.validFrom ?? current.validFrom);
|
|
182
|
+
updated = withOptional(updated, 'validUntil', patch.validUntil ?? current.validUntil);
|
|
183
|
+
updated = withOptional(updated, 'embeddingRef', patch.embeddingRef ?? current.embeddingRef);
|
|
184
|
+
if (patch.validFrom === null)
|
|
185
|
+
delete updated.validFrom;
|
|
186
|
+
if (patch.validUntil === null)
|
|
187
|
+
delete updated.validUntil;
|
|
188
|
+
if (patch.embeddingRef === null)
|
|
189
|
+
delete updated.embeddingRef;
|
|
190
|
+
if (updated.status === 'retracted')
|
|
191
|
+
updated.retractedAt = current.retractedAt ?? now;
|
|
192
|
+
else
|
|
193
|
+
delete updated.retractedAt;
|
|
194
|
+
updated = MemoryNodeSchema.parse(updated);
|
|
195
|
+
await this.rpc.batch([
|
|
196
|
+
{
|
|
197
|
+
sql: `UPDATE memory_nodes SET
|
|
198
|
+
schema_version=?, project_id=?, kind=?, content=?, importance=?, confidence=?,
|
|
199
|
+
status=?, visibility=?, tags_json=?, source_json=?, created_at=?, updated_at=?, valid_from=?,
|
|
200
|
+
valid_until=?, recorded_at=?, retracted_at=?, embedding_ref=?, metadata_json=?
|
|
201
|
+
WHERE id=?`,
|
|
202
|
+
params: [...nodeSqlValues(updated).slice(1), updated.id],
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
sql: `INSERT INTO memory_versions
|
|
206
|
+
(version_id, memory_id, revision, snapshot_json, recorded_at, actor, reason)
|
|
207
|
+
VALUES (?, ?, (SELECT COALESCE(MAX(revision), 0) + 1 FROM memory_versions WHERE memory_id=?), ?, ?, ?, ?)`,
|
|
208
|
+
params: [
|
|
209
|
+
`ver_${randomUUID()}`, updated.id, updated.id, JSON.stringify(updated), now,
|
|
210
|
+
patch.actor ?? null, patch.reason ?? 'updated',
|
|
211
|
+
],
|
|
212
|
+
},
|
|
213
|
+
...(patch.content !== undefined && patch.content !== current.content
|
|
214
|
+
? [{ sql: 'DELETE FROM memory_embeddings WHERE memory_id=?', params: [updated.id] }]
|
|
215
|
+
: []),
|
|
216
|
+
]);
|
|
217
|
+
return updated;
|
|
218
|
+
}
|
|
219
|
+
async search(query) {
|
|
220
|
+
this.assertReady();
|
|
221
|
+
if (query.asOf)
|
|
222
|
+
return this.searchAsOf(query);
|
|
223
|
+
const text = query.text?.trim() ?? '';
|
|
224
|
+
const expression = text ? ftsExpression(text) : '';
|
|
225
|
+
try {
|
|
226
|
+
return await this.searchCurrent(query, text, expression, this.fts && Boolean(expression));
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
if (!this.fts)
|
|
230
|
+
throw error;
|
|
231
|
+
this.fts = false;
|
|
232
|
+
return this.searchCurrent(query, text, expression, false);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
async searchCurrent(query, text, expression, useFts) {
|
|
236
|
+
const params = [];
|
|
237
|
+
const where = [];
|
|
238
|
+
if (useFts) {
|
|
239
|
+
where.push('memory_fts MATCH ?');
|
|
240
|
+
params.push(expression);
|
|
241
|
+
}
|
|
242
|
+
else if (text) {
|
|
243
|
+
const terms = text.toLocaleLowerCase().match(/[\p{L}\p{N}_-]{2,}/gu) ?? [];
|
|
244
|
+
if (terms.length === 0)
|
|
245
|
+
return [];
|
|
246
|
+
where.push(`(${terms.slice(0, 16).map(() => '(LOWER(n.content) LIKE ? OR LOWER(n.tags_json) LIKE ?)').join(' OR ')})`);
|
|
247
|
+
for (const term of terms.slice(0, 16))
|
|
248
|
+
params.push(`%${term}%`, `%${term}%`);
|
|
249
|
+
}
|
|
250
|
+
this.addFilters(query, where, params, 'n');
|
|
251
|
+
const from = useFts
|
|
252
|
+
? 'memory_fts JOIN memory_nodes n ON n.id = memory_fts.node_id'
|
|
253
|
+
: 'memory_nodes n';
|
|
254
|
+
const order = useFts ? 'bm25(memory_fts), n.importance DESC' : 'n.updated_at DESC';
|
|
255
|
+
params.push(boundedLimit(query.limit));
|
|
256
|
+
const rows = await this.rpc.statement({
|
|
257
|
+
sql: `SELECT n.* FROM ${from}${where.length ? ` WHERE ${where.join(' AND ')}` : ''} ORDER BY ${order} LIMIT ?`,
|
|
258
|
+
params,
|
|
259
|
+
mode: 'all',
|
|
260
|
+
});
|
|
261
|
+
return rows
|
|
262
|
+
.map(decodeNode)
|
|
263
|
+
.filter((node) => Boolean(node))
|
|
264
|
+
.map((node) => ({ node, lexicalRelevance: text ? lexicalRelevance(text, node) : 0 }));
|
|
265
|
+
}
|
|
266
|
+
addFilters(query, where, params, alias) {
|
|
267
|
+
if (query.projectId) {
|
|
268
|
+
where.push(`${alias}.project_id = ?`);
|
|
269
|
+
params.push(query.projectId);
|
|
270
|
+
}
|
|
271
|
+
const inFilter = (column, values) => {
|
|
272
|
+
if (!values?.length)
|
|
273
|
+
return;
|
|
274
|
+
where.push(`${alias}.${column} IN (${values.map(() => '?').join(',')})`);
|
|
275
|
+
params.push(...values);
|
|
276
|
+
};
|
|
277
|
+
inFilter('kind', query.kinds);
|
|
278
|
+
inFilter('status', query.statuses);
|
|
279
|
+
inFilter('visibility', query.visibilities);
|
|
280
|
+
if (query.externalClient) {
|
|
281
|
+
where.push(`(${alias}.visibility='project' OR (${alias}.visibility='private' AND json_extract(${alias}.source_json,'$.client')=?))`);
|
|
282
|
+
params.push(query.externalClient);
|
|
283
|
+
}
|
|
284
|
+
if (query.minImportance !== undefined) {
|
|
285
|
+
where.push(`${alias}.importance >= ?`);
|
|
286
|
+
params.push(query.minImportance);
|
|
287
|
+
}
|
|
288
|
+
if (query.minConfidence !== undefined) {
|
|
289
|
+
where.push(`${alias}.confidence >= ?`);
|
|
290
|
+
params.push(query.minConfidence);
|
|
291
|
+
}
|
|
292
|
+
if (query.createdAfter) {
|
|
293
|
+
where.push(`${alias}.created_at >= ?`);
|
|
294
|
+
params.push(query.createdAfter);
|
|
295
|
+
}
|
|
296
|
+
if (query.createdBefore) {
|
|
297
|
+
where.push(`${alias}.created_at <= ?`);
|
|
298
|
+
params.push(query.createdBefore);
|
|
299
|
+
}
|
|
300
|
+
if (!query.includeHistorical && !query.asOf) {
|
|
301
|
+
const now = new Date().toISOString();
|
|
302
|
+
where.push(`(${alias}.valid_from IS NULL OR ${alias}.valid_from <= ?)`);
|
|
303
|
+
where.push(`(${alias}.valid_until IS NULL OR ${alias}.valid_until > ?)`);
|
|
304
|
+
params.push(now, now);
|
|
305
|
+
}
|
|
306
|
+
for (const tag of query.tags ?? []) {
|
|
307
|
+
where.push(`EXISTS (SELECT 1 FROM json_each(${alias}.tags_json) WHERE value = ?)`);
|
|
308
|
+
params.push(tag);
|
|
309
|
+
}
|
|
310
|
+
for (const [key, value] of Object.entries(query.source ?? {})) {
|
|
311
|
+
const jsonKey = SOURCE_COLUMNS[key];
|
|
312
|
+
if (!jsonKey || value === undefined)
|
|
313
|
+
continue;
|
|
314
|
+
where.push(`json_extract(${alias}.source_json, '$.${jsonKey}') = ?`);
|
|
315
|
+
params.push(String(value));
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
async listSemanticSources(projectId, model, options = {}) {
|
|
319
|
+
this.assertReady();
|
|
320
|
+
const params = [model, projectId];
|
|
321
|
+
const missing = options.force ? '' : ' AND e.memory_id IS NULL';
|
|
322
|
+
const cursor = options.cursor ? ' AND n.id > ?' : '';
|
|
323
|
+
if (options.cursor)
|
|
324
|
+
params.push(options.cursor);
|
|
325
|
+
params.push(boundedLimit(options.limit, 1_000));
|
|
326
|
+
const rows = await this.rpc.statement({
|
|
327
|
+
sql: `SELECT n.* FROM memory_nodes n
|
|
328
|
+
LEFT JOIN memory_embeddings e ON e.memory_id=n.id AND e.model=?
|
|
329
|
+
WHERE n.project_id=? AND n.status='active'${missing}${cursor}
|
|
330
|
+
ORDER BY n.id ASC LIMIT ?`,
|
|
331
|
+
params,
|
|
332
|
+
mode: 'all',
|
|
333
|
+
});
|
|
334
|
+
return rows
|
|
335
|
+
.map(decodeNode)
|
|
336
|
+
.filter((node) => Boolean(node))
|
|
337
|
+
.map((node) => ({ node, contentHash: contentHash(node.content) }));
|
|
338
|
+
}
|
|
339
|
+
async upsertSemanticEmbeddings(records) {
|
|
340
|
+
this.assertReady();
|
|
341
|
+
if (records.length === 0)
|
|
342
|
+
return;
|
|
343
|
+
const steps = records.map((record) => {
|
|
344
|
+
if (!record.vector.length || !record.vector.every(Number.isFinite)) {
|
|
345
|
+
throw new Error(`Invalid embedding vector for ${record.memoryId}.`);
|
|
346
|
+
}
|
|
347
|
+
return {
|
|
348
|
+
sql: `INSERT INTO memory_embeddings
|
|
349
|
+
(memory_id,project_id,model,content_hash,dimensions,vector_json,indexed_at)
|
|
350
|
+
VALUES (?,?,?,?,?,?,?)
|
|
351
|
+
ON CONFLICT(memory_id,model) DO UPDATE SET
|
|
352
|
+
project_id=excluded.project_id,
|
|
353
|
+
content_hash=excluded.content_hash,
|
|
354
|
+
dimensions=excluded.dimensions,
|
|
355
|
+
vector_json=excluded.vector_json,
|
|
356
|
+
indexed_at=excluded.indexed_at`,
|
|
357
|
+
params: [
|
|
358
|
+
record.memoryId, record.projectId, record.model, record.contentHash,
|
|
359
|
+
record.vector.length, JSON.stringify(record.vector), record.indexedAt,
|
|
360
|
+
],
|
|
361
|
+
};
|
|
362
|
+
});
|
|
363
|
+
await this.rpc.batch(steps);
|
|
364
|
+
}
|
|
365
|
+
async semanticSearch(query, vector, model) {
|
|
366
|
+
this.assertReady();
|
|
367
|
+
if (!vector.length || !vector.every(Number.isFinite))
|
|
368
|
+
return [];
|
|
369
|
+
const params = [model];
|
|
370
|
+
const where = ['e.model=?'];
|
|
371
|
+
this.addFilters(query, where, params, 'n');
|
|
372
|
+
params.push(100_000);
|
|
373
|
+
const rows = await this.rpc.vectorSearch({
|
|
374
|
+
sql: `SELECT n.*, e.content_hash, e.dimensions, e.vector_json
|
|
375
|
+
FROM memory_embeddings e JOIN memory_nodes n ON n.id=e.memory_id
|
|
376
|
+
WHERE ${where.join(' AND ')} ORDER BY n.updated_at DESC LIMIT ?`,
|
|
377
|
+
params,
|
|
378
|
+
vector,
|
|
379
|
+
limit: boundedLimit(query.limit),
|
|
380
|
+
});
|
|
381
|
+
return rows
|
|
382
|
+
.map((row) => {
|
|
383
|
+
const node = decodeNode(row);
|
|
384
|
+
if (!node)
|
|
385
|
+
return null;
|
|
386
|
+
return {
|
|
387
|
+
node,
|
|
388
|
+
semanticRelevance: Number(row.semantic_relevance ?? 0),
|
|
389
|
+
};
|
|
390
|
+
})
|
|
391
|
+
.filter((candidate) => Boolean(candidate))
|
|
392
|
+
.slice(0, boundedLimit(query.limit));
|
|
393
|
+
}
|
|
394
|
+
async semanticStatus(projectId, model) {
|
|
395
|
+
this.assertReady();
|
|
396
|
+
if (!model)
|
|
397
|
+
return { state: 'disabled', indexed: 0, stale: 0 };
|
|
398
|
+
const { indexed, stale, corrupt, lastIndexedAt } = await this.rpc.semanticStatus({ model, projectId });
|
|
399
|
+
return {
|
|
400
|
+
state: corrupt > 0 || stale > 0 ? 'degraded' : 'ready',
|
|
401
|
+
model,
|
|
402
|
+
indexed,
|
|
403
|
+
stale,
|
|
404
|
+
...(lastIndexedAt ? { lastIndexedAt } : {}),
|
|
405
|
+
...(corrupt > 0 ? { detail: `${corrupt} corrupt or stale embedding record(s)` } : {}),
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
async searchAsOf(query) {
|
|
409
|
+
const rows = await this.rpc.statement({
|
|
410
|
+
sql: `SELECT v.snapshot_json FROM memory_versions v
|
|
411
|
+
JOIN (
|
|
412
|
+
SELECT memory_id, MAX(revision) revision
|
|
413
|
+
FROM memory_versions WHERE recorded_at <= ? GROUP BY memory_id
|
|
414
|
+
) latest ON latest.memory_id=v.memory_id AND latest.revision=v.revision`,
|
|
415
|
+
params: [query.asOf], mode: 'all',
|
|
416
|
+
});
|
|
417
|
+
const text = query.text?.trim() ?? '';
|
|
418
|
+
const matches = rows
|
|
419
|
+
.map((row) => {
|
|
420
|
+
try {
|
|
421
|
+
const parsed = MemoryNodeSchema.safeParse(JSON.parse(String(row.snapshot_json)));
|
|
422
|
+
return parsed.success ? parsed.data : undefined;
|
|
423
|
+
}
|
|
424
|
+
catch {
|
|
425
|
+
return undefined;
|
|
426
|
+
}
|
|
427
|
+
})
|
|
428
|
+
.filter((node) => Boolean(node))
|
|
429
|
+
.filter((node) => this.matchesInMemory(node, query))
|
|
430
|
+
.map((node) => ({ node, lexicalRelevance: text ? lexicalRelevance(text, node) : 0 }))
|
|
431
|
+
.filter((candidate) => !text || candidate.lexicalRelevance > 0)
|
|
432
|
+
.sort((a, b) => b.lexicalRelevance - a.lexicalRelevance || b.node.importance - a.node.importance);
|
|
433
|
+
return matches.slice(0, boundedLimit(query.limit));
|
|
434
|
+
}
|
|
435
|
+
matchesInMemory(node, query) {
|
|
436
|
+
if (query.projectId && node.projectId !== query.projectId)
|
|
437
|
+
return false;
|
|
438
|
+
if (query.kinds?.length && !query.kinds.includes(node.kind))
|
|
439
|
+
return false;
|
|
440
|
+
if (query.statuses?.length && !query.statuses.includes(node.status))
|
|
441
|
+
return false;
|
|
442
|
+
if (query.visibilities?.length && !query.visibilities.includes(node.visibility ?? 'project'))
|
|
443
|
+
return false;
|
|
444
|
+
if (query.externalClient && node.visibility === 'private' && node.source.client !== query.externalClient)
|
|
445
|
+
return false;
|
|
446
|
+
if (query.tags?.some((tag) => !node.tags.includes(tag)))
|
|
447
|
+
return false;
|
|
448
|
+
if (query.minImportance !== undefined && node.importance < query.minImportance)
|
|
449
|
+
return false;
|
|
450
|
+
if (query.minConfidence !== undefined && node.confidence < query.minConfidence)
|
|
451
|
+
return false;
|
|
452
|
+
if (query.createdAfter && node.createdAt < query.createdAfter)
|
|
453
|
+
return false;
|
|
454
|
+
if (query.createdBefore && node.createdAt > query.createdBefore)
|
|
455
|
+
return false;
|
|
456
|
+
const at = query.asOf ?? new Date().toISOString();
|
|
457
|
+
if ((query.asOf || !query.includeHistorical) && node.validFrom && node.validFrom > at)
|
|
458
|
+
return false;
|
|
459
|
+
if ((query.asOf || !query.includeHistorical) && node.validUntil && node.validUntil <= at)
|
|
460
|
+
return false;
|
|
461
|
+
return Object.entries(query.source ?? {}).every(([key, value]) => value === undefined || node.source[key] === value);
|
|
462
|
+
}
|
|
463
|
+
async addEdge(raw) {
|
|
464
|
+
this.assertReady();
|
|
465
|
+
const input = MemoryEdgeInputSchema.parse(raw);
|
|
466
|
+
const existing = await this.rpc.statement({
|
|
467
|
+
sql: 'SELECT * FROM memory_edges WHERE from_id=? AND to_id=? AND relation=? AND valid_until IS NULL LIMIT 1',
|
|
468
|
+
params: [input.from, input.to, input.relation], mode: 'get',
|
|
469
|
+
});
|
|
470
|
+
const decoded = decodeEdge(existing);
|
|
471
|
+
if (decoded)
|
|
472
|
+
return decoded;
|
|
473
|
+
const edge = MemoryEdgeSchema.parse({
|
|
474
|
+
...input,
|
|
475
|
+
id: input.id ?? `edge_${randomUUID()}`,
|
|
476
|
+
strength: input.strength ?? 1,
|
|
477
|
+
confidence: input.confidence ?? 0.8,
|
|
478
|
+
createdAt: new Date().toISOString(),
|
|
479
|
+
metadata: input.metadata ?? {},
|
|
480
|
+
});
|
|
481
|
+
await this.rpc.statement({
|
|
482
|
+
sql: `INSERT INTO memory_edges
|
|
483
|
+
(id, from_id, to_id, relation, strength, confidence, created_at, created_by, valid_from, valid_until, metadata_json)
|
|
484
|
+
VALUES (${Array.from({ length: 11 }, () => '?').join(',')})`,
|
|
485
|
+
params: edgeSqlValues(edge), mode: 'run',
|
|
486
|
+
});
|
|
487
|
+
return edge;
|
|
488
|
+
}
|
|
489
|
+
async edges(nodeId, options = {}) {
|
|
490
|
+
this.assertReady();
|
|
491
|
+
const params = [];
|
|
492
|
+
const direction = options.direction ?? 'both';
|
|
493
|
+
if (direction === 'in') {
|
|
494
|
+
params.push(nodeId);
|
|
495
|
+
}
|
|
496
|
+
else if (direction === 'out') {
|
|
497
|
+
params.push(nodeId);
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
params.push(nodeId, nodeId);
|
|
501
|
+
}
|
|
502
|
+
const where = [direction === 'in' ? 'to_id=?' : direction === 'out' ? 'from_id=?' : '(from_id=? OR to_id=?)'];
|
|
503
|
+
if (options.relations?.length) {
|
|
504
|
+
where.push(`relation IN (${options.relations.map(() => '?').join(',')})`);
|
|
505
|
+
params.push(...options.relations);
|
|
506
|
+
}
|
|
507
|
+
if (options.asOf) {
|
|
508
|
+
where.push('(valid_from IS NULL OR valid_from <= ?) AND (valid_until IS NULL OR valid_until > ?)');
|
|
509
|
+
params.push(options.asOf, options.asOf);
|
|
510
|
+
}
|
|
511
|
+
else {
|
|
512
|
+
const now = new Date().toISOString();
|
|
513
|
+
where.push('(valid_from IS NULL OR valid_from <= ?) AND (valid_until IS NULL OR valid_until > ?)');
|
|
514
|
+
params.push(now, now);
|
|
515
|
+
}
|
|
516
|
+
params.push(boundedLimit(options.limit, 100));
|
|
517
|
+
const rows = await this.rpc.statement({
|
|
518
|
+
sql: `SELECT * FROM memory_edges WHERE ${where.join(' AND ')} ORDER BY created_at DESC LIMIT ?`,
|
|
519
|
+
params, mode: 'all',
|
|
520
|
+
});
|
|
521
|
+
return rows.map(decodeEdge).filter((edge) => Boolean(edge));
|
|
522
|
+
}
|
|
523
|
+
async history(id) {
|
|
524
|
+
this.assertReady();
|
|
525
|
+
const rows = await this.rpc.statement({
|
|
526
|
+
sql: 'SELECT * FROM memory_versions WHERE memory_id=? ORDER BY revision ASC',
|
|
527
|
+
params: [id], mode: 'all',
|
|
528
|
+
});
|
|
529
|
+
return rows.map(decodeVersion).filter((version) => Boolean(version));
|
|
530
|
+
}
|
|
531
|
+
async getAt(id, timestamp) {
|
|
532
|
+
this.assertReady();
|
|
533
|
+
const row = await this.rpc.statement({
|
|
534
|
+
sql: 'SELECT snapshot_json FROM memory_versions WHERE memory_id=? AND recorded_at<=? ORDER BY revision DESC LIMIT 1',
|
|
535
|
+
params: [id, timestamp], mode: 'get',
|
|
536
|
+
});
|
|
537
|
+
if (!row)
|
|
538
|
+
return null;
|
|
539
|
+
try {
|
|
540
|
+
const parsed = MemoryNodeSchema.safeParse(JSON.parse(String(row.snapshot_json)));
|
|
541
|
+
return parsed.success ? parsed.data : null;
|
|
542
|
+
}
|
|
543
|
+
catch {
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
async retract(id, reason, actor) {
|
|
548
|
+
await this.update(id, { status: 'retracted', reason: reason ?? 'retracted', actor });
|
|
549
|
+
}
|
|
550
|
+
async delete(id) {
|
|
551
|
+
this.assertReady();
|
|
552
|
+
const result = await this.rpc.statement({
|
|
553
|
+
sql: 'DELETE FROM memory_nodes WHERE id=?', params: [id], mode: 'run',
|
|
554
|
+
});
|
|
555
|
+
return result.changes > 0;
|
|
556
|
+
}
|
|
557
|
+
async stats() {
|
|
558
|
+
this.assertReady();
|
|
559
|
+
const [nodes, edges] = await Promise.all([
|
|
560
|
+
this.rpc.statement({
|
|
561
|
+
sql: `SELECT COUNT(*) nodes,
|
|
562
|
+
COALESCE(SUM(status='active'),0) active,
|
|
563
|
+
COALESCE(SUM(status='superseded'),0) superseded,
|
|
564
|
+
COALESCE(SUM(status='retracted'),0) retracted,
|
|
565
|
+
COALESCE(SUM(status='archived'),0) archived,
|
|
566
|
+
COALESCE(SUM(status='active' AND json_extract(metadata_json,'$.writeClass')='candidate'),0) candidates
|
|
567
|
+
FROM memory_nodes`, mode: 'get',
|
|
568
|
+
}),
|
|
569
|
+
this.rpc.statement({ sql: 'SELECT COUNT(*) edges FROM memory_edges', mode: 'get' }),
|
|
570
|
+
]);
|
|
571
|
+
return {
|
|
572
|
+
backend: 'sqlite', path: this.databasePath, schemaVersion: SQLITE_MEMORY_SCHEMA_VERSION,
|
|
573
|
+
nodes: Number(nodes.nodes ?? 0), edges: Number(edges.edges ?? 0),
|
|
574
|
+
active: Number(nodes.active ?? 0), superseded: Number(nodes.superseded ?? 0),
|
|
575
|
+
retracted: Number(nodes.retracted ?? 0), archived: Number(nodes.archived ?? 0),
|
|
576
|
+
unconsolidatedCandidates: Number(nodes.candidates ?? 0),
|
|
577
|
+
semanticIndex: 'disabled',
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
async doctor() {
|
|
581
|
+
this.assertReady();
|
|
582
|
+
const checks = [];
|
|
583
|
+
const schema = await this.rpc.statement({ sql: 'PRAGMA user_version', mode: 'get' });
|
|
584
|
+
const actualVersion = Number(Object.values(schema)[0] ?? 0);
|
|
585
|
+
checks.push({
|
|
586
|
+
name: 'schema',
|
|
587
|
+
ok: actualVersion === SQLITE_MEMORY_SCHEMA_VERSION,
|
|
588
|
+
detail: `database v${actualVersion}; runtime v${SQLITE_MEMORY_SCHEMA_VERSION}`,
|
|
589
|
+
});
|
|
590
|
+
try {
|
|
591
|
+
const integrity = await this.rpc.statement({ sql: 'PRAGMA integrity_check', mode: 'get' });
|
|
592
|
+
const detail = String(Object.values(integrity)[0] ?? 'unknown');
|
|
593
|
+
checks.push({ name: 'integrity', ok: detail === 'ok', detail });
|
|
594
|
+
}
|
|
595
|
+
catch (error) {
|
|
596
|
+
checks.push({ name: 'integrity', ok: false, detail: error instanceof Error ? error.message : String(error) });
|
|
597
|
+
}
|
|
598
|
+
const foreign = await this.rpc.statement({ sql: 'PRAGMA foreign_key_check', mode: 'all' });
|
|
599
|
+
checks.push({ name: 'foreign-keys', ok: foreign.length === 0, detail: foreign.length === 0 ? 'ok' : `${foreign.length} violation(s)` });
|
|
600
|
+
checks.push({ name: 'fts', ok: this.fts, detail: this.fts ? 'FTS5 ready' : 'FTS5 unavailable; LIKE fallback active' });
|
|
601
|
+
return { ok: checks.every((check) => check.ok || check.name === 'fts'), backend: 'sqlite', path: this.databasePath, checks };
|
|
602
|
+
}
|
|
603
|
+
async export(projectId) {
|
|
604
|
+
this.assertReady();
|
|
605
|
+
const [nodeRows, edgeRows, versionRows] = await Promise.all([
|
|
606
|
+
this.rpc.statement({ sql: 'SELECT * FROM memory_nodes WHERE project_id=? ORDER BY created_at', params: [projectId], mode: 'all' }),
|
|
607
|
+
this.rpc.statement({ sql: `SELECT e.* FROM memory_edges e JOIN memory_nodes n ON n.id=e.from_id WHERE n.project_id=? ORDER BY e.created_at`, params: [projectId], mode: 'all' }),
|
|
608
|
+
this.rpc.statement({ sql: `SELECT v.* FROM memory_versions v JOIN memory_nodes n ON n.id=v.memory_id WHERE n.project_id=? ORDER BY v.memory_id,v.revision`, params: [projectId], mode: 'all' }),
|
|
609
|
+
]);
|
|
610
|
+
return {
|
|
611
|
+
schemaVersion: 1, exportedAt: new Date().toISOString(), projectId,
|
|
612
|
+
nodes: nodeRows.map(decodeNode).filter((node) => Boolean(node)),
|
|
613
|
+
edges: edgeRows.map(decodeEdge).filter((edge) => Boolean(edge)),
|
|
614
|
+
versions: versionRows.map(decodeVersion).filter((version) => Boolean(version)),
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
async hasImport(sourceId) {
|
|
618
|
+
this.assertReady();
|
|
619
|
+
const row = await this.rpc.statement({
|
|
620
|
+
sql: 'SELECT source_id FROM memory_imports WHERE source_id=?', params: [sourceId], mode: 'get',
|
|
621
|
+
});
|
|
622
|
+
return Boolean(row);
|
|
623
|
+
}
|
|
624
|
+
async recordImport(sourceId, memoryId) {
|
|
625
|
+
this.assertReady();
|
|
626
|
+
await this.rpc.statement({
|
|
627
|
+
sql: 'INSERT OR IGNORE INTO memory_imports(source_id,memory_id,imported_at) VALUES (?,?,?)',
|
|
628
|
+
params: [sourceId, memoryId, new Date().toISOString()], mode: 'run',
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
async close() {
|
|
632
|
+
await this.rpc.close();
|
|
633
|
+
this.initialized = false;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
//# sourceMappingURL=sqliteBackend.js.map
|