@cohaku/cli 0.2.6
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/chunk-2CDNPZUE.js +47 -0
- package/dist/chunk-2MC3FCZP.js +30589 -0
- package/dist/chunk-437HAVIY.js +2863 -0
- package/dist/chunk-7WYEO45L.js +64 -0
- package/dist/chunk-HMH4HWDA.js +1136 -0
- package/dist/dashboard/assets/index-BbaNHCTp.css +1 -0
- package/dist/dashboard/assets/index-VgrI7y2q.js +11 -0
- package/dist/dashboard/index.html +14 -0
- package/dist/index.js +3479 -0
- package/dist/mcp-QOTZLT7H.js +37 -0
- package/dist/serve-3QZ3TVQG.js +70 -0
- package/dist/start-VKAEMLQE.js +81 -0
- package/dist/status-PG7GHTRM.js +76 -0
- package/package.json +54 -0
|
@@ -0,0 +1,1136 @@
|
|
|
1
|
+
import { createRequire } from 'module'; const require = createRequire(import.meta.url);
|
|
2
|
+
|
|
3
|
+
// src/shared/engine.ts
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import { homedir } from "os";
|
|
6
|
+
import { execSync } from "child_process";
|
|
7
|
+
|
|
8
|
+
// ../core/src/storage/sqlite-driver.ts
|
|
9
|
+
import Database from "better-sqlite3";
|
|
10
|
+
import * as sqliteVec from "sqlite-vec";
|
|
11
|
+
|
|
12
|
+
// ../shared/src/constants.ts
|
|
13
|
+
var DEFAULT_WEIGHTS = {
|
|
14
|
+
semantic: 0.4,
|
|
15
|
+
bm25: 0.25,
|
|
16
|
+
recency: 0.2,
|
|
17
|
+
salience: 0.15
|
|
18
|
+
};
|
|
19
|
+
var MEMORY_LAYER_PRIORITY = {
|
|
20
|
+
rule: 10,
|
|
21
|
+
working: 5,
|
|
22
|
+
long_term: 1
|
|
23
|
+
};
|
|
24
|
+
var DEFAULT_DECAY_RATE = 0.01;
|
|
25
|
+
var DEFAULT_EMBEDDING_DIMENSIONS = 384;
|
|
26
|
+
|
|
27
|
+
// ../core/src/storage/schema.ts
|
|
28
|
+
var SCHEMA_VERSION = 2;
|
|
29
|
+
var CREATE_TABLES = `
|
|
30
|
+
-- Memory (3-layer)
|
|
31
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
32
|
+
id TEXT PRIMARY KEY,
|
|
33
|
+
project_id TEXT,
|
|
34
|
+
content TEXT NOT NULL,
|
|
35
|
+
layer TEXT NOT NULL DEFAULT 'long_term',
|
|
36
|
+
type TEXT NOT NULL DEFAULT 'note',
|
|
37
|
+
priority INTEGER NOT NULL DEFAULT 1,
|
|
38
|
+
importance REAL NOT NULL DEFAULT 0.5,
|
|
39
|
+
tags TEXT,
|
|
40
|
+
expires_at TEXT,
|
|
41
|
+
last_accessed_at TEXT NOT NULL,
|
|
42
|
+
created_at TEXT NOT NULL,
|
|
43
|
+
updated_at TEXT NOT NULL,
|
|
44
|
+
deleted_at TEXT
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
-- FTS5 virtual table (BM25 search)
|
|
48
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
|
49
|
+
content,
|
|
50
|
+
tags,
|
|
51
|
+
content='memories',
|
|
52
|
+
content_rowid='rowid',
|
|
53
|
+
tokenize='porter'
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
-- FTS5 sync triggers
|
|
57
|
+
CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
|
|
58
|
+
INSERT INTO memories_fts(rowid, content, tags)
|
|
59
|
+
VALUES (new.rowid, new.content, new.tags);
|
|
60
|
+
END;
|
|
61
|
+
|
|
62
|
+
CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
|
|
63
|
+
INSERT INTO memories_fts(memories_fts, rowid, content, tags)
|
|
64
|
+
VALUES ('delete', old.rowid, old.content, old.tags);
|
|
65
|
+
END;
|
|
66
|
+
|
|
67
|
+
CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
|
|
68
|
+
INSERT INTO memories_fts(memories_fts, rowid, content, tags)
|
|
69
|
+
VALUES ('delete', old.rowid, old.content, old.tags);
|
|
70
|
+
INSERT INTO memories_fts(rowid, content, tags)
|
|
71
|
+
VALUES (new.rowid, new.content, new.tags);
|
|
72
|
+
END;
|
|
73
|
+
|
|
74
|
+
-- Entity nodes
|
|
75
|
+
CREATE TABLE IF NOT EXISTS nodes (
|
|
76
|
+
id TEXT PRIMARY KEY,
|
|
77
|
+
project_id TEXT,
|
|
78
|
+
name TEXT NOT NULL,
|
|
79
|
+
name_embedding BLOB,
|
|
80
|
+
summary TEXT,
|
|
81
|
+
entity_type TEXT,
|
|
82
|
+
created_at TEXT NOT NULL,
|
|
83
|
+
updated_at TEXT NOT NULL
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
-- Edges (bi-temporal)
|
|
87
|
+
CREATE TABLE IF NOT EXISTS edges (
|
|
88
|
+
id TEXT PRIMARY KEY,
|
|
89
|
+
project_id TEXT,
|
|
90
|
+
source_id TEXT NOT NULL REFERENCES nodes(id),
|
|
91
|
+
target_id TEXT NOT NULL REFERENCES nodes(id),
|
|
92
|
+
relation TEXT NOT NULL,
|
|
93
|
+
fact TEXT,
|
|
94
|
+
fact_embedding BLOB,
|
|
95
|
+
created_at TEXT NOT NULL,
|
|
96
|
+
expired_at TEXT,
|
|
97
|
+
valid_at TEXT,
|
|
98
|
+
invalid_at TEXT
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
-- Episodes
|
|
102
|
+
CREATE TABLE IF NOT EXISTS episodes (
|
|
103
|
+
id TEXT PRIMARY KEY,
|
|
104
|
+
project_id TEXT,
|
|
105
|
+
content TEXT NOT NULL,
|
|
106
|
+
content_type TEXT,
|
|
107
|
+
source TEXT,
|
|
108
|
+
created_at TEXT NOT NULL
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
-- Episode references
|
|
112
|
+
CREATE TABLE IF NOT EXISTS episode_refs (
|
|
113
|
+
episode_id TEXT REFERENCES episodes(id),
|
|
114
|
+
ref_type TEXT,
|
|
115
|
+
ref_id TEXT,
|
|
116
|
+
PRIMARY KEY (episode_id, ref_type, ref_id)
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
-- Sessions
|
|
120
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
121
|
+
id TEXT PRIMARY KEY,
|
|
122
|
+
project_id TEXT,
|
|
123
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
124
|
+
started_at TEXT NOT NULL,
|
|
125
|
+
ended_at TEXT,
|
|
126
|
+
checkpoint_count INTEGER NOT NULL DEFAULT 0,
|
|
127
|
+
working_memory_snapshot TEXT,
|
|
128
|
+
metadata TEXT
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
-- Config
|
|
132
|
+
CREATE TABLE IF NOT EXISTS config (
|
|
133
|
+
key TEXT PRIMARY KEY,
|
|
134
|
+
value TEXT NOT NULL
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
-- Indexes
|
|
138
|
+
CREATE INDEX IF NOT EXISTS idx_memories_layer ON memories(layer);
|
|
139
|
+
CREATE INDEX IF NOT EXISTS idx_memories_deleted ON memories(deleted_at);
|
|
140
|
+
CREATE INDEX IF NOT EXISTS idx_memories_project ON memories(project_id);
|
|
141
|
+
CREATE INDEX IF NOT EXISTS idx_nodes_project ON nodes(project_id);
|
|
142
|
+
CREATE INDEX IF NOT EXISTS idx_edges_source ON edges(source_id);
|
|
143
|
+
CREATE INDEX IF NOT EXISTS idx_edges_target ON edges(target_id);
|
|
144
|
+
CREATE INDEX IF NOT EXISTS idx_edges_expired ON edges(expired_at);
|
|
145
|
+
CREATE INDEX IF NOT EXISTS idx_edges_project ON edges(project_id);
|
|
146
|
+
CREATE INDEX IF NOT EXISTS idx_episodes_project ON episodes(project_id);
|
|
147
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
|
|
148
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id);
|
|
149
|
+
`;
|
|
150
|
+
var MIGRATE_V1_TO_V2 = `
|
|
151
|
+
ALTER TABLE memories ADD COLUMN project_id TEXT;
|
|
152
|
+
ALTER TABLE nodes ADD COLUMN project_id TEXT;
|
|
153
|
+
ALTER TABLE edges ADD COLUMN project_id TEXT;
|
|
154
|
+
ALTER TABLE episodes ADD COLUMN project_id TEXT;
|
|
155
|
+
ALTER TABLE sessions ADD COLUMN project_id TEXT;
|
|
156
|
+
|
|
157
|
+
CREATE INDEX IF NOT EXISTS idx_memories_project ON memories(project_id);
|
|
158
|
+
CREATE INDEX IF NOT EXISTS idx_nodes_project ON nodes(project_id);
|
|
159
|
+
CREATE INDEX IF NOT EXISTS idx_edges_project ON edges(project_id);
|
|
160
|
+
CREATE INDEX IF NOT EXISTS idx_episodes_project ON episodes(project_id);
|
|
161
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id);
|
|
162
|
+
`;
|
|
163
|
+
function createVecTables(dimensions) {
|
|
164
|
+
return `
|
|
165
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS vec_memories USING vec0(
|
|
166
|
+
memory_id TEXT PRIMARY KEY,
|
|
167
|
+
embedding float[${dimensions}]
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS vec_nodes USING vec0(
|
|
171
|
+
node_id TEXT PRIMARY KEY,
|
|
172
|
+
embedding float[${dimensions}]
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS vec_edges USING vec0(
|
|
176
|
+
edge_id TEXT PRIMARY KEY,
|
|
177
|
+
embedding float[${dimensions}]
|
|
178
|
+
);
|
|
179
|
+
`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ../core/src/utils/temporal.ts
|
|
183
|
+
function nowISO() {
|
|
184
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
185
|
+
}
|
|
186
|
+
function toVecBlob(embedding) {
|
|
187
|
+
return Buffer.from(new Float32Array(embedding).buffer);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ../core/src/storage/sqlite-driver.ts
|
|
191
|
+
var SqliteDriver = class {
|
|
192
|
+
constructor(dbPath, dimensions = DEFAULT_EMBEDDING_DIMENSIONS) {
|
|
193
|
+
this.dbPath = dbPath;
|
|
194
|
+
this.dimensions = dimensions;
|
|
195
|
+
}
|
|
196
|
+
db;
|
|
197
|
+
async initialize() {
|
|
198
|
+
this.db = new Database(this.dbPath);
|
|
199
|
+
this.db.pragma("journal_mode = WAL");
|
|
200
|
+
this.db.pragma("foreign_keys = ON");
|
|
201
|
+
sqliteVec.load(this.db);
|
|
202
|
+
this.db.exec("CREATE TABLE IF NOT EXISTS config (key TEXT PRIMARY KEY, value TEXT NOT NULL)");
|
|
203
|
+
const existing = this.db.prepare("SELECT value FROM config WHERE key = ?").get("schema_version");
|
|
204
|
+
const currentVersion = existing ? Number(existing.value) : 0;
|
|
205
|
+
if (currentVersion >= 1 && currentVersion < 2) {
|
|
206
|
+
for (const stmt of MIGRATE_V1_TO_V2.split(";").filter((s) => s.trim())) {
|
|
207
|
+
this.db.exec(stmt);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
this.db.exec(CREATE_TABLES);
|
|
211
|
+
const vecSQL = createVecTables(this.dimensions);
|
|
212
|
+
for (const stmt of vecSQL.split(";").filter((s) => s.trim())) {
|
|
213
|
+
this.db.exec(stmt);
|
|
214
|
+
}
|
|
215
|
+
this.db.prepare("INSERT INTO config (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run("schema_version", String(SCHEMA_VERSION));
|
|
216
|
+
}
|
|
217
|
+
close() {
|
|
218
|
+
this.db.close();
|
|
219
|
+
}
|
|
220
|
+
// ── Project Filter Helper ──
|
|
221
|
+
buildProjectFilter(projectIds, tableAlias) {
|
|
222
|
+
if (!projectIds) return { clause: "", params: [] };
|
|
223
|
+
const hasNull = projectIds.includes(null);
|
|
224
|
+
const nonNull = projectIds.filter((id) => id !== null);
|
|
225
|
+
const conditions = [];
|
|
226
|
+
const params = [];
|
|
227
|
+
if (hasNull) {
|
|
228
|
+
conditions.push(`${tableAlias}.project_id IS NULL`);
|
|
229
|
+
}
|
|
230
|
+
if (nonNull.length > 0) {
|
|
231
|
+
conditions.push(`${tableAlias}.project_id IN (${nonNull.map(() => "?").join(", ")})`);
|
|
232
|
+
params.push(...nonNull);
|
|
233
|
+
}
|
|
234
|
+
if (conditions.length === 0) return { clause: "", params: [] };
|
|
235
|
+
return { clause: `AND (${conditions.join(" OR ")})`, params };
|
|
236
|
+
}
|
|
237
|
+
// ── Memory ──
|
|
238
|
+
async addMemory(memory) {
|
|
239
|
+
const tagsStr = memory.tags ? JSON.stringify(memory.tags) : null;
|
|
240
|
+
this.db.transaction(() => {
|
|
241
|
+
this.db.prepare(`
|
|
242
|
+
INSERT INTO memories (id, project_id, content, layer, type, priority, importance, tags, expires_at, last_accessed_at, created_at, updated_at, deleted_at)
|
|
243
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
244
|
+
`).run(
|
|
245
|
+
memory.id,
|
|
246
|
+
memory.projectId ?? null,
|
|
247
|
+
memory.content,
|
|
248
|
+
memory.layer,
|
|
249
|
+
memory.type,
|
|
250
|
+
memory.priority,
|
|
251
|
+
memory.importance,
|
|
252
|
+
tagsStr,
|
|
253
|
+
memory.expiresAt ?? null,
|
|
254
|
+
memory.lastAccessedAt,
|
|
255
|
+
memory.createdAt,
|
|
256
|
+
memory.updatedAt,
|
|
257
|
+
memory.deletedAt ?? null
|
|
258
|
+
);
|
|
259
|
+
if (memory.embedding) {
|
|
260
|
+
this.db.prepare(`
|
|
261
|
+
INSERT INTO vec_memories (memory_id, embedding) VALUES (?, ?)
|
|
262
|
+
`).run(memory.id, toVecBlob(memory.embedding));
|
|
263
|
+
}
|
|
264
|
+
})();
|
|
265
|
+
}
|
|
266
|
+
async getMemory(id) {
|
|
267
|
+
const row = this.db.prepare("SELECT * FROM memories WHERE id = ? AND deleted_at IS NULL").get(id);
|
|
268
|
+
return row ? this.rowToMemory(row) : null;
|
|
269
|
+
}
|
|
270
|
+
async updateMemory(id, input) {
|
|
271
|
+
const sets = ["updated_at = ?"];
|
|
272
|
+
const values = [(/* @__PURE__ */ new Date()).toISOString()];
|
|
273
|
+
if (input.content !== void 0) {
|
|
274
|
+
sets.push("content = ?");
|
|
275
|
+
values.push(input.content);
|
|
276
|
+
}
|
|
277
|
+
if (input.layer !== void 0) {
|
|
278
|
+
sets.push("layer = ?");
|
|
279
|
+
values.push(input.layer);
|
|
280
|
+
}
|
|
281
|
+
if (input.type !== void 0) {
|
|
282
|
+
sets.push("type = ?");
|
|
283
|
+
values.push(input.type);
|
|
284
|
+
}
|
|
285
|
+
if (input.importance !== void 0) {
|
|
286
|
+
sets.push("importance = ?");
|
|
287
|
+
values.push(input.importance);
|
|
288
|
+
}
|
|
289
|
+
if (input.tags !== void 0) {
|
|
290
|
+
sets.push("tags = ?");
|
|
291
|
+
values.push(JSON.stringify(input.tags));
|
|
292
|
+
}
|
|
293
|
+
if (input.expiresAt !== void 0) {
|
|
294
|
+
sets.push("expires_at = ?");
|
|
295
|
+
values.push(input.expiresAt);
|
|
296
|
+
}
|
|
297
|
+
values.push(id);
|
|
298
|
+
this.db.prepare(`UPDATE memories SET ${sets.join(", ")} WHERE id = ?`).run(...values);
|
|
299
|
+
}
|
|
300
|
+
async updateMemoryEmbedding(id, embedding) {
|
|
301
|
+
this.db.transaction(() => {
|
|
302
|
+
this.db.prepare("DELETE FROM vec_memories WHERE memory_id = ?").run(id);
|
|
303
|
+
this.db.prepare(
|
|
304
|
+
"INSERT INTO vec_memories (memory_id, embedding) VALUES (?, ?)"
|
|
305
|
+
).run(id, toVecBlob(embedding));
|
|
306
|
+
})();
|
|
307
|
+
}
|
|
308
|
+
async deleteMemory(id) {
|
|
309
|
+
this.db.prepare("UPDATE memories SET deleted_at = ? WHERE id = ?").run((/* @__PURE__ */ new Date()).toISOString(), id);
|
|
310
|
+
}
|
|
311
|
+
async searchMemories(embedding, limit, projectIds) {
|
|
312
|
+
const { clause, params } = this.buildProjectFilter(projectIds, "m");
|
|
313
|
+
const rows = this.db.prepare(`
|
|
314
|
+
SELECT m.*, v.distance
|
|
315
|
+
FROM vec_memories v
|
|
316
|
+
INNER JOIN memories m ON m.id = v.memory_id
|
|
317
|
+
WHERE v.embedding MATCH ?
|
|
318
|
+
AND m.deleted_at IS NULL
|
|
319
|
+
AND k = ?
|
|
320
|
+
${clause}
|
|
321
|
+
`).all(toVecBlob(embedding), limit, ...params);
|
|
322
|
+
return rows.map((row) => ({
|
|
323
|
+
memory: this.rowToMemory(row),
|
|
324
|
+
distance: row.distance
|
|
325
|
+
}));
|
|
326
|
+
}
|
|
327
|
+
async fullTextSearch(query, limit, projectIds) {
|
|
328
|
+
const { clause, params } = this.buildProjectFilter(projectIds, "m");
|
|
329
|
+
const rows = this.db.prepare(`
|
|
330
|
+
SELECT m.*, fts.rank
|
|
331
|
+
FROM memories_fts fts
|
|
332
|
+
INNER JOIN memories m ON m.rowid = fts.rowid
|
|
333
|
+
WHERE memories_fts MATCH ?
|
|
334
|
+
AND m.deleted_at IS NULL
|
|
335
|
+
${clause}
|
|
336
|
+
ORDER BY fts.rank
|
|
337
|
+
LIMIT ?
|
|
338
|
+
`).all(query, ...params, limit);
|
|
339
|
+
return rows.map((row) => ({
|
|
340
|
+
memory: this.rowToMemory(row),
|
|
341
|
+
rank: row.rank
|
|
342
|
+
}));
|
|
343
|
+
}
|
|
344
|
+
async findSimilarMemories(embedding, threshold, limit, projectIds) {
|
|
345
|
+
const results = await this.searchMemories(embedding, limit * 3, projectIds);
|
|
346
|
+
return results.filter((r) => r.distance <= threshold).slice(0, limit);
|
|
347
|
+
}
|
|
348
|
+
async listMemories(limit, projectIds) {
|
|
349
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
350
|
+
const { clause, params } = this.buildProjectFilter(projectIds, "memories");
|
|
351
|
+
const rows = this.db.prepare(`
|
|
352
|
+
SELECT * FROM memories
|
|
353
|
+
WHERE deleted_at IS NULL
|
|
354
|
+
AND (expires_at IS NULL OR expires_at > ?)
|
|
355
|
+
${clause}
|
|
356
|
+
ORDER BY updated_at DESC
|
|
357
|
+
LIMIT ?
|
|
358
|
+
`).all(now, ...params, limit);
|
|
359
|
+
return rows.map((row) => this.rowToMemory(row));
|
|
360
|
+
}
|
|
361
|
+
async listNodes(limit, projectIds) {
|
|
362
|
+
const { clause, params } = this.buildProjectFilter(projectIds, "nodes");
|
|
363
|
+
const rows = this.db.prepare(`
|
|
364
|
+
SELECT * FROM nodes WHERE 1=1 ${clause} ORDER BY updated_at DESC LIMIT ?
|
|
365
|
+
`).all(...params, limit);
|
|
366
|
+
return rows.map((row) => this.rowToNode(row));
|
|
367
|
+
}
|
|
368
|
+
async listEdges(limit, includeExpired, projectIds) {
|
|
369
|
+
const expiredClause = includeExpired ? "" : "AND expired_at IS NULL";
|
|
370
|
+
const { clause, params } = this.buildProjectFilter(projectIds, "edges");
|
|
371
|
+
const rows = this.db.prepare(`
|
|
372
|
+
SELECT * FROM edges WHERE 1=1 ${expiredClause} ${clause} ORDER BY created_at DESC LIMIT ?
|
|
373
|
+
`).all(...params, limit);
|
|
374
|
+
return rows.map((row) => this.rowToEdge(row));
|
|
375
|
+
}
|
|
376
|
+
async listMemoriesByLayer(layer, limit, projectIds) {
|
|
377
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
378
|
+
const { clause, params } = this.buildProjectFilter(projectIds, "memories");
|
|
379
|
+
const rows = this.db.prepare(`
|
|
380
|
+
SELECT * FROM memories
|
|
381
|
+
WHERE layer = ?
|
|
382
|
+
AND deleted_at IS NULL
|
|
383
|
+
AND (expires_at IS NULL OR expires_at > ?)
|
|
384
|
+
${clause}
|
|
385
|
+
ORDER BY updated_at DESC
|
|
386
|
+
LIMIT ?
|
|
387
|
+
`).all(layer, now, ...params, limit);
|
|
388
|
+
return rows.map((row) => this.rowToMemory(row));
|
|
389
|
+
}
|
|
390
|
+
// ── Nodes ──
|
|
391
|
+
async upsertNode(node) {
|
|
392
|
+
this.db.transaction(() => {
|
|
393
|
+
this.db.prepare(`
|
|
394
|
+
INSERT INTO nodes (id, project_id, name, summary, entity_type, created_at, updated_at)
|
|
395
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
396
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
397
|
+
name = excluded.name,
|
|
398
|
+
summary = excluded.summary,
|
|
399
|
+
entity_type = excluded.entity_type,
|
|
400
|
+
updated_at = excluded.updated_at
|
|
401
|
+
`).run(node.id, node.projectId ?? null, node.name, node.summary ?? null, node.entityType ?? null, node.createdAt, node.updatedAt);
|
|
402
|
+
if (node.nameEmbedding) {
|
|
403
|
+
this.db.prepare(`
|
|
404
|
+
DELETE FROM vec_nodes WHERE node_id = ?
|
|
405
|
+
`).run(node.id);
|
|
406
|
+
this.db.prepare(`
|
|
407
|
+
INSERT INTO vec_nodes (node_id, embedding) VALUES (?, ?)
|
|
408
|
+
`).run(node.id, toVecBlob(node.nameEmbedding));
|
|
409
|
+
}
|
|
410
|
+
})();
|
|
411
|
+
}
|
|
412
|
+
async getNode(id) {
|
|
413
|
+
const row = this.db.prepare("SELECT * FROM nodes WHERE id = ?").get(id);
|
|
414
|
+
return row ? this.rowToNode(row) : null;
|
|
415
|
+
}
|
|
416
|
+
async searchNodes(embedding, limit, projectIds) {
|
|
417
|
+
const { clause, params } = this.buildProjectFilter(projectIds, "n");
|
|
418
|
+
const rows = this.db.prepare(`
|
|
419
|
+
SELECT n.*
|
|
420
|
+
FROM vec_nodes v
|
|
421
|
+
INNER JOIN nodes n ON n.id = v.node_id
|
|
422
|
+
WHERE v.embedding MATCH ?
|
|
423
|
+
AND k = ?
|
|
424
|
+
${clause}
|
|
425
|
+
`).all(toVecBlob(embedding), limit, ...params);
|
|
426
|
+
return rows.map((row) => this.rowToNode(row));
|
|
427
|
+
}
|
|
428
|
+
// ── Edges ──
|
|
429
|
+
async upsertEdge(edge) {
|
|
430
|
+
this.db.transaction(() => {
|
|
431
|
+
this.db.prepare(`
|
|
432
|
+
INSERT INTO edges (id, project_id, source_id, target_id, relation, fact, created_at, expired_at, valid_at, invalid_at)
|
|
433
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
434
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
435
|
+
relation = excluded.relation,
|
|
436
|
+
fact = excluded.fact,
|
|
437
|
+
expired_at = excluded.expired_at,
|
|
438
|
+
valid_at = excluded.valid_at,
|
|
439
|
+
invalid_at = excluded.invalid_at
|
|
440
|
+
`).run(
|
|
441
|
+
edge.id,
|
|
442
|
+
edge.projectId ?? null,
|
|
443
|
+
edge.sourceId,
|
|
444
|
+
edge.targetId,
|
|
445
|
+
edge.relation,
|
|
446
|
+
edge.fact ?? null,
|
|
447
|
+
edge.createdAt,
|
|
448
|
+
edge.expiredAt ?? null,
|
|
449
|
+
edge.validAt ?? null,
|
|
450
|
+
edge.invalidAt ?? null
|
|
451
|
+
);
|
|
452
|
+
if (edge.factEmbedding) {
|
|
453
|
+
this.db.prepare(`
|
|
454
|
+
DELETE FROM vec_edges WHERE edge_id = ?
|
|
455
|
+
`).run(edge.id);
|
|
456
|
+
this.db.prepare(`
|
|
457
|
+
INSERT INTO vec_edges (edge_id, embedding) VALUES (?, ?)
|
|
458
|
+
`).run(edge.id, toVecBlob(edge.factEmbedding));
|
|
459
|
+
}
|
|
460
|
+
})();
|
|
461
|
+
}
|
|
462
|
+
async getEdge(id) {
|
|
463
|
+
const row = this.db.prepare("SELECT * FROM edges WHERE id = ?").get(id);
|
|
464
|
+
return row ? this.rowToEdge(row) : null;
|
|
465
|
+
}
|
|
466
|
+
async invalidateEdge(id, expiredAt, invalidAt) {
|
|
467
|
+
this.db.prepare("UPDATE edges SET expired_at = ?, invalid_at = ? WHERE id = ?").run(expiredAt, invalidAt, id);
|
|
468
|
+
}
|
|
469
|
+
async searchEdges(embedding, filters, limit, projectIds) {
|
|
470
|
+
let whereClause = "1=1";
|
|
471
|
+
const params = [toVecBlob(embedding), limit];
|
|
472
|
+
if (filters.excludeExpired !== false) {
|
|
473
|
+
whereClause += " AND e.expired_at IS NULL";
|
|
474
|
+
}
|
|
475
|
+
if (filters.asOf) {
|
|
476
|
+
whereClause += " AND (e.valid_at IS NULL OR e.valid_at <= ?)";
|
|
477
|
+
params.push(filters.asOf);
|
|
478
|
+
whereClause += " AND (e.invalid_at IS NULL OR e.invalid_at > ?)";
|
|
479
|
+
params.push(filters.asOf);
|
|
480
|
+
}
|
|
481
|
+
const { clause: projectClause, params: projectParams } = this.buildProjectFilter(projectIds, "e");
|
|
482
|
+
whereClause += ` ${projectClause}`;
|
|
483
|
+
params.push(...projectParams);
|
|
484
|
+
const rows = this.db.prepare(`
|
|
485
|
+
SELECT e.*
|
|
486
|
+
FROM vec_edges v
|
|
487
|
+
INNER JOIN edges e ON e.id = v.edge_id
|
|
488
|
+
WHERE v.embedding MATCH ?
|
|
489
|
+
AND k = ?
|
|
490
|
+
AND ${whereClause}
|
|
491
|
+
`).all(...params);
|
|
492
|
+
return rows.map((row) => this.rowToEdge(row));
|
|
493
|
+
}
|
|
494
|
+
// ── Graph Traversal ──
|
|
495
|
+
async getEdgesForNode(nodeId) {
|
|
496
|
+
const rows = this.db.prepare(`
|
|
497
|
+
SELECT * FROM edges
|
|
498
|
+
WHERE (source_id = ? OR target_id = ?) AND expired_at IS NULL
|
|
499
|
+
`).all(nodeId, nodeId);
|
|
500
|
+
return rows.map((row) => this.rowToEdge(row));
|
|
501
|
+
}
|
|
502
|
+
async getNeighbors(nodeId, depth) {
|
|
503
|
+
if (depth < 1) return [];
|
|
504
|
+
const oneHop = this.db.prepare(`
|
|
505
|
+
SELECT DISTINCT n.* FROM nodes n
|
|
506
|
+
INNER JOIN edges e ON (e.source_id = ? AND e.target_id = n.id)
|
|
507
|
+
OR (e.target_id = ? AND e.source_id = n.id)
|
|
508
|
+
WHERE e.expired_at IS NULL
|
|
509
|
+
`).all(nodeId, nodeId);
|
|
510
|
+
if (depth === 1) {
|
|
511
|
+
return oneHop.map((row) => this.rowToNode(row));
|
|
512
|
+
}
|
|
513
|
+
const visited = /* @__PURE__ */ new Set([nodeId]);
|
|
514
|
+
const results = [];
|
|
515
|
+
for (const row of oneHop) {
|
|
516
|
+
visited.add(row.id);
|
|
517
|
+
results.push(this.rowToNode(row));
|
|
518
|
+
}
|
|
519
|
+
for (const node of [...results]) {
|
|
520
|
+
const twoHop = this.db.prepare(`
|
|
521
|
+
SELECT DISTINCT n.* FROM nodes n
|
|
522
|
+
INNER JOIN edges e ON (e.source_id = ? AND e.target_id = n.id)
|
|
523
|
+
OR (e.target_id = ? AND e.source_id = n.id)
|
|
524
|
+
WHERE e.expired_at IS NULL
|
|
525
|
+
`).all(node.id, node.id);
|
|
526
|
+
for (const row of twoHop) {
|
|
527
|
+
if (!visited.has(row.id)) {
|
|
528
|
+
visited.add(row.id);
|
|
529
|
+
results.push(this.rowToNode(row));
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
return results;
|
|
534
|
+
}
|
|
535
|
+
// ── Episodes ──
|
|
536
|
+
async addEpisode(episode) {
|
|
537
|
+
this.db.prepare(`
|
|
538
|
+
INSERT INTO episodes (id, project_id, content, content_type, source, created_at)
|
|
539
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
540
|
+
`).run(episode.id, episode.projectId ?? null, episode.content, episode.contentType ?? null, episode.source ?? null, episode.createdAt);
|
|
541
|
+
}
|
|
542
|
+
async addEpisodeRef(ref) {
|
|
543
|
+
this.db.prepare(`
|
|
544
|
+
INSERT OR IGNORE INTO episode_refs (episode_id, ref_type, ref_id)
|
|
545
|
+
VALUES (?, ?, ?)
|
|
546
|
+
`).run(ref.episodeId, ref.refType, ref.refId);
|
|
547
|
+
}
|
|
548
|
+
async getEpisode(id) {
|
|
549
|
+
const row = this.db.prepare("SELECT * FROM episodes WHERE id = ?").get(id);
|
|
550
|
+
return row ? this.rowToEpisode(row) : null;
|
|
551
|
+
}
|
|
552
|
+
async listEpisodes(limit, projectIds) {
|
|
553
|
+
const { clause, params } = this.buildProjectFilter(projectIds, "episodes");
|
|
554
|
+
const rows = this.db.prepare(`SELECT * FROM episodes WHERE 1=1 ${clause} ORDER BY created_at DESC LIMIT ?`).all(...params, limit);
|
|
555
|
+
return rows.map((row) => this.rowToEpisode(row));
|
|
556
|
+
}
|
|
557
|
+
async getEpisodeRefs(episodeId) {
|
|
558
|
+
const rows = this.db.prepare("SELECT * FROM episode_refs WHERE episode_id = ?").all(episodeId);
|
|
559
|
+
return rows.map((row) => ({
|
|
560
|
+
episodeId: row.episode_id,
|
|
561
|
+
refType: row.ref_type,
|
|
562
|
+
refId: row.ref_id
|
|
563
|
+
}));
|
|
564
|
+
}
|
|
565
|
+
// ── Sessions ──
|
|
566
|
+
async addSession(session) {
|
|
567
|
+
this.db.prepare(`
|
|
568
|
+
INSERT INTO sessions (id, project_id, status, started_at, ended_at, checkpoint_count, working_memory_snapshot, metadata)
|
|
569
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
570
|
+
`).run(
|
|
571
|
+
session.id,
|
|
572
|
+
session.projectId ?? null,
|
|
573
|
+
session.status,
|
|
574
|
+
session.startedAt,
|
|
575
|
+
session.endedAt ?? null,
|
|
576
|
+
session.checkpointCount,
|
|
577
|
+
session.workingMemorySnapshot ?? null,
|
|
578
|
+
session.metadata ?? null
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
async updateSession(id, updates) {
|
|
582
|
+
const sets = [];
|
|
583
|
+
const values = [];
|
|
584
|
+
if (updates.status !== void 0) {
|
|
585
|
+
sets.push("status = ?");
|
|
586
|
+
values.push(updates.status);
|
|
587
|
+
}
|
|
588
|
+
if (updates.endedAt !== void 0) {
|
|
589
|
+
sets.push("ended_at = ?");
|
|
590
|
+
values.push(updates.endedAt);
|
|
591
|
+
}
|
|
592
|
+
if (updates.checkpointCount !== void 0) {
|
|
593
|
+
sets.push("checkpoint_count = ?");
|
|
594
|
+
values.push(updates.checkpointCount);
|
|
595
|
+
}
|
|
596
|
+
if (updates.workingMemorySnapshot !== void 0) {
|
|
597
|
+
sets.push("working_memory_snapshot = ?");
|
|
598
|
+
values.push(updates.workingMemorySnapshot);
|
|
599
|
+
}
|
|
600
|
+
if (updates.metadata !== void 0) {
|
|
601
|
+
sets.push("metadata = ?");
|
|
602
|
+
values.push(updates.metadata);
|
|
603
|
+
}
|
|
604
|
+
if (sets.length === 0) return;
|
|
605
|
+
values.push(id);
|
|
606
|
+
this.db.prepare(`UPDATE sessions SET ${sets.join(", ")} WHERE id = ?`).run(...values);
|
|
607
|
+
}
|
|
608
|
+
async getSession(id) {
|
|
609
|
+
const row = this.db.prepare("SELECT * FROM sessions WHERE id = ?").get(id);
|
|
610
|
+
return row ? this.rowToSession(row) : null;
|
|
611
|
+
}
|
|
612
|
+
async listSessions(limit, projectIds) {
|
|
613
|
+
const { clause, params } = this.buildProjectFilter(projectIds, "sessions");
|
|
614
|
+
const rows = this.db.prepare(`SELECT * FROM sessions WHERE 1=1 ${clause} ORDER BY started_at DESC LIMIT ?`).all(...params, limit);
|
|
615
|
+
return rows.map((row) => this.rowToSession(row));
|
|
616
|
+
}
|
|
617
|
+
// ── Config ──
|
|
618
|
+
async getConfig(key) {
|
|
619
|
+
const row = this.db.prepare("SELECT value FROM config WHERE key = ?").get(key);
|
|
620
|
+
return row?.value ?? null;
|
|
621
|
+
}
|
|
622
|
+
async setConfig(key, value) {
|
|
623
|
+
this.db.prepare("INSERT INTO config (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(key, value);
|
|
624
|
+
}
|
|
625
|
+
// ── Row Converters ──
|
|
626
|
+
rowToMemory(row) {
|
|
627
|
+
return {
|
|
628
|
+
id: row.id,
|
|
629
|
+
projectId: row.project_id ?? void 0,
|
|
630
|
+
content: row.content,
|
|
631
|
+
layer: row.layer,
|
|
632
|
+
type: row.type,
|
|
633
|
+
priority: row.priority,
|
|
634
|
+
importance: row.importance,
|
|
635
|
+
tags: row.tags ? JSON.parse(row.tags) : void 0,
|
|
636
|
+
expiresAt: row.expires_at ?? void 0,
|
|
637
|
+
lastAccessedAt: row.last_accessed_at,
|
|
638
|
+
createdAt: row.created_at,
|
|
639
|
+
updatedAt: row.updated_at,
|
|
640
|
+
deletedAt: row.deleted_at ?? void 0
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
rowToNode(row) {
|
|
644
|
+
return {
|
|
645
|
+
id: row.id,
|
|
646
|
+
projectId: row.project_id ?? void 0,
|
|
647
|
+
name: row.name,
|
|
648
|
+
summary: row.summary ?? void 0,
|
|
649
|
+
entityType: row.entity_type ?? void 0,
|
|
650
|
+
createdAt: row.created_at,
|
|
651
|
+
updatedAt: row.updated_at
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
rowToEdge(row) {
|
|
655
|
+
return {
|
|
656
|
+
id: row.id,
|
|
657
|
+
projectId: row.project_id ?? void 0,
|
|
658
|
+
sourceId: row.source_id,
|
|
659
|
+
targetId: row.target_id,
|
|
660
|
+
relation: row.relation,
|
|
661
|
+
fact: row.fact ?? void 0,
|
|
662
|
+
createdAt: row.created_at,
|
|
663
|
+
expiredAt: row.expired_at ?? void 0,
|
|
664
|
+
validAt: row.valid_at ?? void 0,
|
|
665
|
+
invalidAt: row.invalid_at ?? void 0
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
rowToEpisode(row) {
|
|
669
|
+
return {
|
|
670
|
+
id: row.id,
|
|
671
|
+
projectId: row.project_id ?? void 0,
|
|
672
|
+
content: row.content,
|
|
673
|
+
contentType: row.content_type ?? void 0,
|
|
674
|
+
source: row.source ?? void 0,
|
|
675
|
+
createdAt: row.created_at
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
rowToSession(row) {
|
|
679
|
+
return {
|
|
680
|
+
id: row.id,
|
|
681
|
+
projectId: row.project_id ?? void 0,
|
|
682
|
+
status: row.status,
|
|
683
|
+
startedAt: row.started_at,
|
|
684
|
+
endedAt: row.ended_at ?? void 0,
|
|
685
|
+
checkpointCount: row.checkpoint_count,
|
|
686
|
+
workingMemorySnapshot: row.working_memory_snapshot ?? void 0,
|
|
687
|
+
metadata: row.metadata ?? void 0
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
// ../core/src/embedding/local-embedding.ts
|
|
693
|
+
var LocalEmbedding = class {
|
|
694
|
+
constructor(cacheDir) {
|
|
695
|
+
this.cacheDir = cacheDir;
|
|
696
|
+
}
|
|
697
|
+
dimensions = 384;
|
|
698
|
+
pipe = null;
|
|
699
|
+
initPromise = null;
|
|
700
|
+
async getPipeline() {
|
|
701
|
+
if (this.pipe) return this.pipe;
|
|
702
|
+
if (!this.initPromise) {
|
|
703
|
+
this.initPromise = (async () => {
|
|
704
|
+
const { pipeline, env } = await import("@xenova/transformers");
|
|
705
|
+
if (this.cacheDir) {
|
|
706
|
+
env.cacheDir = this.cacheDir;
|
|
707
|
+
}
|
|
708
|
+
const pipe = await pipeline("feature-extraction", "Xenova/all-MiniLM-L6-v2");
|
|
709
|
+
return pipe;
|
|
710
|
+
})();
|
|
711
|
+
}
|
|
712
|
+
this.pipe = await this.initPromise;
|
|
713
|
+
return this.pipe;
|
|
714
|
+
}
|
|
715
|
+
async embed(text) {
|
|
716
|
+
const pipe = await this.getPipeline();
|
|
717
|
+
const result = await pipe(text, { pooling: "mean", normalize: true });
|
|
718
|
+
return Array.from(result.data).slice(0, this.dimensions);
|
|
719
|
+
}
|
|
720
|
+
async embedBatch(texts) {
|
|
721
|
+
return Promise.all(texts.map((t) => this.embed(t)));
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
|
|
725
|
+
// ../core/src/engine/memory-engine.ts
|
|
726
|
+
import { mkdirSync } from "fs";
|
|
727
|
+
import { dirname } from "path";
|
|
728
|
+
|
|
729
|
+
// ../../node_modules/.pnpm/nanoid@5.1.6/node_modules/nanoid/index.js
|
|
730
|
+
import { webcrypto as crypto } from "crypto";
|
|
731
|
+
|
|
732
|
+
// ../../node_modules/.pnpm/nanoid@5.1.6/node_modules/nanoid/url-alphabet/index.js
|
|
733
|
+
var urlAlphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
|
|
734
|
+
|
|
735
|
+
// ../../node_modules/.pnpm/nanoid@5.1.6/node_modules/nanoid/index.js
|
|
736
|
+
var POOL_SIZE_MULTIPLIER = 128;
|
|
737
|
+
var pool;
|
|
738
|
+
var poolOffset;
|
|
739
|
+
function fillPool(bytes) {
|
|
740
|
+
if (!pool || pool.length < bytes) {
|
|
741
|
+
pool = Buffer.allocUnsafe(bytes * POOL_SIZE_MULTIPLIER);
|
|
742
|
+
crypto.getRandomValues(pool);
|
|
743
|
+
poolOffset = 0;
|
|
744
|
+
} else if (poolOffset + bytes > pool.length) {
|
|
745
|
+
crypto.getRandomValues(pool);
|
|
746
|
+
poolOffset = 0;
|
|
747
|
+
}
|
|
748
|
+
poolOffset += bytes;
|
|
749
|
+
}
|
|
750
|
+
function nanoid(size = 21) {
|
|
751
|
+
fillPool(size |= 0);
|
|
752
|
+
let id = "";
|
|
753
|
+
for (let i = poolOffset - size; i < poolOffset; i++) {
|
|
754
|
+
id += urlAlphabet[pool[i] & 63];
|
|
755
|
+
}
|
|
756
|
+
return id;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// ../core/src/utils/id.ts
|
|
760
|
+
function generateId() {
|
|
761
|
+
return nanoid();
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// ../core/src/engine/scoring.ts
|
|
765
|
+
function decay(importance, lastAccessedAt, rate = 0.01) {
|
|
766
|
+
const ageHours = (Date.now() - new Date(lastAccessedAt).getTime()) / 36e5;
|
|
767
|
+
return importance * Math.exp(-rate * ageHours);
|
|
768
|
+
}
|
|
769
|
+
function recencyScore(timestamp, halfLifeHours = 24) {
|
|
770
|
+
const ageHours = (Date.now() - new Date(timestamp).getTime()) / 36e5;
|
|
771
|
+
return Math.exp(-Math.LN2 / halfLifeHours * ageHours);
|
|
772
|
+
}
|
|
773
|
+
function hybridScore(semantic, bm25, recency, salience, weights) {
|
|
774
|
+
return semantic * weights.semantic + bm25 * weights.bm25 + recency * weights.recency + salience * weights.salience;
|
|
775
|
+
}
|
|
776
|
+
function normalizeBm25(rank) {
|
|
777
|
+
return 1 / (1 + Math.abs(rank));
|
|
778
|
+
}
|
|
779
|
+
function distanceToSimilarity(distance) {
|
|
780
|
+
return 1 / (1 + distance);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// ../core/src/engine/context.ts
|
|
784
|
+
async function getContext(storage, options = {}) {
|
|
785
|
+
const { maxMemories = 50, projectIds } = options;
|
|
786
|
+
const rules = await storage.listMemoriesByLayer("rule", maxMemories, projectIds);
|
|
787
|
+
const remaining1 = maxMemories - rules.length;
|
|
788
|
+
const working = remaining1 > 0 ? await storage.listMemoriesByLayer("working", Math.ceil(remaining1 * 0.6), projectIds) : [];
|
|
789
|
+
const remaining2 = maxMemories - rules.length - working.length;
|
|
790
|
+
const longTerm = remaining2 > 0 ? await storage.listMemoriesByLayer("long_term", remaining2, projectIds) : [];
|
|
791
|
+
const memories = [...rules, ...working, ...longTerm];
|
|
792
|
+
const totalCount = memories.length;
|
|
793
|
+
return {
|
|
794
|
+
memories,
|
|
795
|
+
totalCount,
|
|
796
|
+
truncated: totalCount >= maxMemories
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// ../core/src/engine/memory-engine.ts
|
|
801
|
+
var MemoryEngine = class {
|
|
802
|
+
storage;
|
|
803
|
+
embedding;
|
|
804
|
+
weights;
|
|
805
|
+
decayRate;
|
|
806
|
+
defaultProjectId;
|
|
807
|
+
constructor(config) {
|
|
808
|
+
mkdirSync(dirname(config.dbPath), { recursive: true });
|
|
809
|
+
this.storage = new SqliteDriver(config.dbPath);
|
|
810
|
+
this.embedding = new LocalEmbedding(config.embeddingCacheDir);
|
|
811
|
+
this.weights = { ...DEFAULT_WEIGHTS, ...config.weights };
|
|
812
|
+
this.decayRate = config.decayRate ?? DEFAULT_DECAY_RATE;
|
|
813
|
+
this.defaultProjectId = config.defaultProjectId;
|
|
814
|
+
}
|
|
815
|
+
async initialize() {
|
|
816
|
+
await this.storage.initialize();
|
|
817
|
+
}
|
|
818
|
+
close() {
|
|
819
|
+
this.storage.close();
|
|
820
|
+
}
|
|
821
|
+
// ── Scope Helpers ──
|
|
822
|
+
resolveProjectId(scope) {
|
|
823
|
+
if (scope === "global") return void 0;
|
|
824
|
+
return this.defaultProjectId;
|
|
825
|
+
}
|
|
826
|
+
resolveProjectFilter(scope) {
|
|
827
|
+
if (scope === "all") return void 0;
|
|
828
|
+
if (scope === "global") return [null];
|
|
829
|
+
if (this.defaultProjectId) return [this.defaultProjectId, null];
|
|
830
|
+
return void 0;
|
|
831
|
+
}
|
|
832
|
+
// ── Memory ──
|
|
833
|
+
async addMemory(input) {
|
|
834
|
+
const now = nowISO();
|
|
835
|
+
const layer = input.layer ?? "long_term";
|
|
836
|
+
const memory = {
|
|
837
|
+
id: generateId(),
|
|
838
|
+
projectId: this.resolveProjectId(input.scope),
|
|
839
|
+
content: input.content,
|
|
840
|
+
layer,
|
|
841
|
+
type: input.type ?? "note",
|
|
842
|
+
priority: MEMORY_LAYER_PRIORITY[layer] ?? 1,
|
|
843
|
+
importance: input.importance ?? 0.5,
|
|
844
|
+
tags: input.tags,
|
|
845
|
+
embedding: await this.embedding.embed(input.content),
|
|
846
|
+
expiresAt: input.expiresAt,
|
|
847
|
+
lastAccessedAt: now,
|
|
848
|
+
createdAt: now,
|
|
849
|
+
updatedAt: now
|
|
850
|
+
};
|
|
851
|
+
await this.storage.addMemory(memory);
|
|
852
|
+
return memory;
|
|
853
|
+
}
|
|
854
|
+
async searchMemories(query) {
|
|
855
|
+
const limit = query.limit ?? 10;
|
|
856
|
+
const weights = { ...this.weights, ...query.weights };
|
|
857
|
+
const projectIds = this.resolveProjectFilter(query.scope);
|
|
858
|
+
const queryEmbedding = query.embedding ?? await this.embedding.embed(query.text);
|
|
859
|
+
const semanticResults = await this.storage.searchMemories(queryEmbedding, limit * 2, projectIds);
|
|
860
|
+
let bm25Results = [];
|
|
861
|
+
try {
|
|
862
|
+
bm25Results = await this.storage.fullTextSearch(query.text, limit * 2, projectIds);
|
|
863
|
+
} catch {
|
|
864
|
+
}
|
|
865
|
+
const candidateMap = /* @__PURE__ */ new Map();
|
|
866
|
+
for (const { memory } of semanticResults) candidateMap.set(memory.id, memory);
|
|
867
|
+
for (const { memory } of bm25Results) candidateMap.set(memory.id, memory);
|
|
868
|
+
const semanticScoreMap = /* @__PURE__ */ new Map();
|
|
869
|
+
for (const { memory, distance } of semanticResults) {
|
|
870
|
+
semanticScoreMap.set(memory.id, distanceToSimilarity(distance));
|
|
871
|
+
}
|
|
872
|
+
const bm25ScoreMap = /* @__PURE__ */ new Map();
|
|
873
|
+
for (const { memory, rank } of bm25Results) {
|
|
874
|
+
bm25ScoreMap.set(memory.id, normalizeBm25(rank));
|
|
875
|
+
}
|
|
876
|
+
const layerFilter = query.layers ? new Set(query.layers) : null;
|
|
877
|
+
const results = [];
|
|
878
|
+
for (const [id, memory] of candidateMap) {
|
|
879
|
+
if (layerFilter && !layerFilter.has(memory.layer)) continue;
|
|
880
|
+
if (memory.expiresAt && new Date(memory.expiresAt) < /* @__PURE__ */ new Date()) continue;
|
|
881
|
+
const semantic = semanticScoreMap.get(id) ?? 0;
|
|
882
|
+
const bm25 = bm25ScoreMap.get(id) ?? 0;
|
|
883
|
+
const rec = recencyScore(memory.lastAccessedAt);
|
|
884
|
+
const sal = decay(memory.importance, memory.lastAccessedAt, this.decayRate);
|
|
885
|
+
const score = hybridScore(semantic, bm25, rec, sal, weights);
|
|
886
|
+
results.push({
|
|
887
|
+
id: memory.id,
|
|
888
|
+
type: "memory",
|
|
889
|
+
content: memory.content,
|
|
890
|
+
score,
|
|
891
|
+
scoreBreakdown: { semantic, bm25, recency: rec, salience: sal }
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
results.sort((a, b) => b.score - a.score);
|
|
895
|
+
return results.slice(0, limit);
|
|
896
|
+
}
|
|
897
|
+
async updateMemory(id, input) {
|
|
898
|
+
await this.storage.updateMemory(id, input);
|
|
899
|
+
if (input.content !== void 0) {
|
|
900
|
+
const newEmbedding = await this.embedding.embed(input.content);
|
|
901
|
+
await this.storage.updateMemoryEmbedding(id, newEmbedding);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
async deleteMemory(id) {
|
|
905
|
+
await this.storage.deleteMemory(id);
|
|
906
|
+
}
|
|
907
|
+
async getMemory(id) {
|
|
908
|
+
return this.storage.getMemory(id);
|
|
909
|
+
}
|
|
910
|
+
async listMemories(options) {
|
|
911
|
+
const limit = options?.limit ?? 100;
|
|
912
|
+
const projectIds = this.resolveProjectFilter(options?.scope);
|
|
913
|
+
if (options?.layer) {
|
|
914
|
+
return this.storage.listMemoriesByLayer(options.layer, limit, projectIds);
|
|
915
|
+
}
|
|
916
|
+
return this.storage.listMemories(limit, projectIds);
|
|
917
|
+
}
|
|
918
|
+
async listEntities(options) {
|
|
919
|
+
const limit = options?.limit ?? 100;
|
|
920
|
+
const projectIds = this.resolveProjectFilter(options?.scope);
|
|
921
|
+
return this.storage.listNodes(limit, projectIds);
|
|
922
|
+
}
|
|
923
|
+
async listEdges(options) {
|
|
924
|
+
const limit = options?.limit ?? 100;
|
|
925
|
+
const projectIds = this.resolveProjectFilter(options?.scope);
|
|
926
|
+
return this.storage.listEdges(limit, options?.includeExpired ?? false, projectIds);
|
|
927
|
+
}
|
|
928
|
+
// ── Deduplication & Consolidation ──
|
|
929
|
+
async findDuplicates(memoryId, threshold = 0.3) {
|
|
930
|
+
const target = await this.storage.getMemory(memoryId);
|
|
931
|
+
if (!target) throw new Error(`Memory not found: ${memoryId}`);
|
|
932
|
+
const embedding = target.embedding ?? await this.embedding.embed(target.content);
|
|
933
|
+
const projectIds = this.resolveProjectFilter("project");
|
|
934
|
+
const similar = await this.storage.findSimilarMemories(embedding, threshold, 20, projectIds);
|
|
935
|
+
return similar.filter((r) => r.memory.id !== memoryId);
|
|
936
|
+
}
|
|
937
|
+
async consolidateMemories(sourceIds, mergedContent, layer) {
|
|
938
|
+
const merged = await this.addMemory({
|
|
939
|
+
content: mergedContent,
|
|
940
|
+
layer: layer ?? "long_term",
|
|
941
|
+
type: "note"
|
|
942
|
+
});
|
|
943
|
+
for (const id of sourceIds) {
|
|
944
|
+
await this.storage.deleteMemory(id);
|
|
945
|
+
}
|
|
946
|
+
return merged;
|
|
947
|
+
}
|
|
948
|
+
// ── Graph ──
|
|
949
|
+
async addEntity(input) {
|
|
950
|
+
const now = nowISO();
|
|
951
|
+
const node = {
|
|
952
|
+
id: generateId(),
|
|
953
|
+
projectId: this.resolveProjectId(input.scope),
|
|
954
|
+
name: input.name,
|
|
955
|
+
nameEmbedding: await this.embedding.embed(input.name),
|
|
956
|
+
summary: input.summary,
|
|
957
|
+
entityType: input.entityType,
|
|
958
|
+
createdAt: now,
|
|
959
|
+
updatedAt: now
|
|
960
|
+
};
|
|
961
|
+
await this.storage.upsertNode(node);
|
|
962
|
+
return node;
|
|
963
|
+
}
|
|
964
|
+
async addEdge(input) {
|
|
965
|
+
const now = nowISO();
|
|
966
|
+
const edge = {
|
|
967
|
+
id: generateId(),
|
|
968
|
+
projectId: this.resolveProjectId(input.scope),
|
|
969
|
+
sourceId: input.sourceId,
|
|
970
|
+
targetId: input.targetId,
|
|
971
|
+
relation: input.relation,
|
|
972
|
+
fact: input.fact,
|
|
973
|
+
factEmbedding: input.fact ? await this.embedding.embed(input.fact) : void 0,
|
|
974
|
+
createdAt: now,
|
|
975
|
+
validAt: input.validAt
|
|
976
|
+
};
|
|
977
|
+
await this.storage.upsertEdge(edge);
|
|
978
|
+
return edge;
|
|
979
|
+
}
|
|
980
|
+
async getEdge(id) {
|
|
981
|
+
return this.storage.getEdge(id);
|
|
982
|
+
}
|
|
983
|
+
async invalidateEdge(edgeId) {
|
|
984
|
+
const now = nowISO();
|
|
985
|
+
await this.storage.invalidateEdge(edgeId, now, now);
|
|
986
|
+
}
|
|
987
|
+
async updateEdge(edgeId, input) {
|
|
988
|
+
const old = await this.storage.getEdge(edgeId);
|
|
989
|
+
if (!old) return null;
|
|
990
|
+
const now = nowISO();
|
|
991
|
+
await this.storage.invalidateEdge(edgeId, now, now);
|
|
992
|
+
return this.addEdge({
|
|
993
|
+
sourceId: old.sourceId,
|
|
994
|
+
targetId: old.targetId,
|
|
995
|
+
relation: input.relation ?? old.relation,
|
|
996
|
+
fact: input.fact ?? old.fact,
|
|
997
|
+
validAt: now
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
async searchGraph(query, limit = 10, options) {
|
|
1001
|
+
const depth = options?.traverseDepth ?? 1;
|
|
1002
|
+
const projectIds = this.resolveProjectFilter(options?.scope);
|
|
1003
|
+
const queryEmbedding = await this.embedding.embed(query);
|
|
1004
|
+
const nodes = await this.storage.searchNodes(queryEmbedding, Math.ceil(limit / 2), projectIds);
|
|
1005
|
+
const edges = await this.storage.searchEdges(queryEmbedding, { excludeExpired: true }, Math.ceil(limit / 2), projectIds);
|
|
1006
|
+
const results = [];
|
|
1007
|
+
const seenNodeIds = /* @__PURE__ */ new Set();
|
|
1008
|
+
for (const node of nodes) {
|
|
1009
|
+
seenNodeIds.add(node.id);
|
|
1010
|
+
results.push({
|
|
1011
|
+
id: node.id,
|
|
1012
|
+
type: "node",
|
|
1013
|
+
content: `${node.name}${node.summary ? ": " + node.summary : ""}`,
|
|
1014
|
+
score: 1
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
if (depth > 0) {
|
|
1018
|
+
for (const node of nodes) {
|
|
1019
|
+
const neighbors = await this.storage.getNeighbors(node.id, depth);
|
|
1020
|
+
for (const neighbor of neighbors) {
|
|
1021
|
+
if (!seenNodeIds.has(neighbor.id)) {
|
|
1022
|
+
seenNodeIds.add(neighbor.id);
|
|
1023
|
+
results.push({
|
|
1024
|
+
id: neighbor.id,
|
|
1025
|
+
type: "node",
|
|
1026
|
+
content: `${neighbor.name}${neighbor.summary ? ": " + neighbor.summary : ""}`,
|
|
1027
|
+
score: 0.5
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
for (const edge of edges) {
|
|
1034
|
+
results.push({
|
|
1035
|
+
id: edge.id,
|
|
1036
|
+
type: "edge",
|
|
1037
|
+
content: `${edge.relation}: ${edge.fact ?? ""}`,
|
|
1038
|
+
score: 0.8
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
results.sort((a, b) => b.score - a.score);
|
|
1042
|
+
return results.slice(0, limit);
|
|
1043
|
+
}
|
|
1044
|
+
// ── Episodes ──
|
|
1045
|
+
async addEpisode(input, refs) {
|
|
1046
|
+
const episode = {
|
|
1047
|
+
id: generateId(),
|
|
1048
|
+
projectId: this.resolveProjectId(input.scope),
|
|
1049
|
+
content: input.content,
|
|
1050
|
+
contentType: input.contentType,
|
|
1051
|
+
source: input.source,
|
|
1052
|
+
createdAt: nowISO()
|
|
1053
|
+
};
|
|
1054
|
+
await this.storage.addEpisode(episode);
|
|
1055
|
+
if (refs) {
|
|
1056
|
+
for (const ref of refs) {
|
|
1057
|
+
await this.storage.addEpisodeRef({ ...ref, episodeId: episode.id });
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
return episode;
|
|
1061
|
+
}
|
|
1062
|
+
async getEpisode(id) {
|
|
1063
|
+
return this.storage.getEpisode(id);
|
|
1064
|
+
}
|
|
1065
|
+
async listEpisodes(limit = 20, scope) {
|
|
1066
|
+
const projectIds = this.resolveProjectFilter(scope);
|
|
1067
|
+
return this.storage.listEpisodes(limit, projectIds);
|
|
1068
|
+
}
|
|
1069
|
+
// ── Sessions ──
|
|
1070
|
+
async sessionStart(metadata) {
|
|
1071
|
+
const session = {
|
|
1072
|
+
id: generateId(),
|
|
1073
|
+
projectId: this.resolveProjectId("project"),
|
|
1074
|
+
status: "active",
|
|
1075
|
+
startedAt: nowISO(),
|
|
1076
|
+
checkpointCount: 0,
|
|
1077
|
+
metadata
|
|
1078
|
+
};
|
|
1079
|
+
await this.storage.addSession(session);
|
|
1080
|
+
return session;
|
|
1081
|
+
}
|
|
1082
|
+
async sessionEnd(id) {
|
|
1083
|
+
await this.storage.updateSession(id, {
|
|
1084
|
+
status: "ended",
|
|
1085
|
+
endedAt: nowISO()
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
async sessionCheckpoint(id) {
|
|
1089
|
+
const session = await this.storage.getSession(id);
|
|
1090
|
+
if (!session) throw new Error(`Session not found: ${id}`);
|
|
1091
|
+
await this.storage.updateSession(id, {
|
|
1092
|
+
checkpointCount: session.checkpointCount + 1
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
async sessionList(limit = 20, scope) {
|
|
1096
|
+
const projectIds = this.resolveProjectFilter(scope);
|
|
1097
|
+
return this.storage.listSessions(limit, projectIds);
|
|
1098
|
+
}
|
|
1099
|
+
// ── Context ──
|
|
1100
|
+
async getContext(options) {
|
|
1101
|
+
const projectIds = this.resolveProjectFilter(options?.scope);
|
|
1102
|
+
return getContext(this.storage, { ...options, projectIds });
|
|
1103
|
+
}
|
|
1104
|
+
};
|
|
1105
|
+
|
|
1106
|
+
// src/shared/engine.ts
|
|
1107
|
+
var DEFAULT_DB_PATH2 = join(homedir(), ".config", "cohaku", "memory.db");
|
|
1108
|
+
function getDbPath() {
|
|
1109
|
+
return process.env["COHAKU_DB_PATH"] ?? DEFAULT_DB_PATH2;
|
|
1110
|
+
}
|
|
1111
|
+
function detectProjectId() {
|
|
1112
|
+
const envProjectId = process.env["COHAKU_PROJECT_ID"];
|
|
1113
|
+
if (envProjectId) return envProjectId;
|
|
1114
|
+
try {
|
|
1115
|
+
const gitRoot = execSync("git rev-parse --show-toplevel", {
|
|
1116
|
+
encoding: "utf-8",
|
|
1117
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1118
|
+
}).trim();
|
|
1119
|
+
return gitRoot || void 0;
|
|
1120
|
+
} catch {
|
|
1121
|
+
return void 0;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
async function createEngine() {
|
|
1125
|
+
const dbPath = getDbPath();
|
|
1126
|
+
const projectId = detectProjectId();
|
|
1127
|
+
const engine = new MemoryEngine({ dbPath, defaultProjectId: projectId });
|
|
1128
|
+
await engine.initialize();
|
|
1129
|
+
return { engine, dbPath, projectId };
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
export {
|
|
1133
|
+
getDbPath,
|
|
1134
|
+
detectProjectId,
|
|
1135
|
+
createEngine
|
|
1136
|
+
};
|