claude-memory-layer 1.0.5 → 1.0.7
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/.claude/settings.local.json +2 -1
- package/.claude-plugin/plugin.json +3 -3
- package/.history/package_20260201134912.json +45 -0
- package/.history/package_20260201142928.json +46 -0
- package/CLAUDE.md +20 -5
- package/README.md +26 -26
- package/dist/.claude-plugin/plugin.json +3 -3
- package/dist/cli/index.js +562 -8
- package/dist/cli/index.js.map +4 -4
- package/dist/core/index.js.map +1 -1
- package/dist/server/api/index.js +4363 -0
- package/dist/server/api/index.js.map +7 -0
- package/dist/server/index.js +4423 -0
- package/dist/server/index.js.map +7 -0
- package/dist/ui/index.html +745 -0
- package/package.json +3 -2
- package/scripts/build.ts +32 -3
- package/src/cli/index.ts +85 -6
- package/src/core/types.ts +1 -1
- package/src/mcp/index.ts +2 -2
- package/src/mcp/tools.ts +1 -1
- package/src/server/api/stats.ts +54 -1
- package/src/ui/index.html +745 -0
|
@@ -0,0 +1,4363 @@
|
|
|
1
|
+
import { createRequire } from 'module';
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
3
|
+
import { dirname } from 'path';
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = dirname(__filename);
|
|
7
|
+
|
|
8
|
+
// src/server/api/index.ts
|
|
9
|
+
import { Hono as Hono6 } from "hono";
|
|
10
|
+
|
|
11
|
+
// src/server/api/sessions.ts
|
|
12
|
+
import { Hono } from "hono";
|
|
13
|
+
|
|
14
|
+
// src/services/memory-service.ts
|
|
15
|
+
import * as path from "path";
|
|
16
|
+
import * as os from "os";
|
|
17
|
+
import * as fs from "fs";
|
|
18
|
+
import * as crypto2 from "crypto";
|
|
19
|
+
|
|
20
|
+
// src/core/event-store.ts
|
|
21
|
+
import { randomUUID } from "crypto";
|
|
22
|
+
|
|
23
|
+
// src/core/canonical-key.ts
|
|
24
|
+
import { createHash } from "crypto";
|
|
25
|
+
var MAX_KEY_LENGTH = 200;
|
|
26
|
+
function makeCanonicalKey(title, context) {
|
|
27
|
+
let normalized = title.normalize("NFKC");
|
|
28
|
+
normalized = normalized.toLowerCase();
|
|
29
|
+
normalized = normalized.replace(/[^\p{L}\p{N}\s]/gu, "");
|
|
30
|
+
normalized = normalized.replace(/\s+/g, " ").trim();
|
|
31
|
+
let key = normalized;
|
|
32
|
+
if (context?.project) {
|
|
33
|
+
key = `${context.project}::${key}`;
|
|
34
|
+
}
|
|
35
|
+
if (key.length > MAX_KEY_LENGTH) {
|
|
36
|
+
const hashSuffix = createHash("md5").update(key).digest("hex").slice(0, 8);
|
|
37
|
+
key = key.slice(0, MAX_KEY_LENGTH - 9) + "_" + hashSuffix;
|
|
38
|
+
}
|
|
39
|
+
return key;
|
|
40
|
+
}
|
|
41
|
+
function makeDedupeKey(content, sessionId) {
|
|
42
|
+
const contentHash = createHash("sha256").update(content).digest("hex");
|
|
43
|
+
return `${sessionId}:${contentHash}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// src/core/db-wrapper.ts
|
|
47
|
+
import duckdb from "duckdb";
|
|
48
|
+
function convertBigInts(obj) {
|
|
49
|
+
if (obj === null || obj === void 0)
|
|
50
|
+
return obj;
|
|
51
|
+
if (typeof obj === "bigint")
|
|
52
|
+
return Number(obj);
|
|
53
|
+
if (obj instanceof Date)
|
|
54
|
+
return obj;
|
|
55
|
+
if (Array.isArray(obj))
|
|
56
|
+
return obj.map(convertBigInts);
|
|
57
|
+
if (typeof obj === "object") {
|
|
58
|
+
const result = {};
|
|
59
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
60
|
+
result[key] = convertBigInts(value);
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
return obj;
|
|
65
|
+
}
|
|
66
|
+
function toDate(value) {
|
|
67
|
+
if (value instanceof Date)
|
|
68
|
+
return value;
|
|
69
|
+
if (typeof value === "string")
|
|
70
|
+
return new Date(value);
|
|
71
|
+
if (typeof value === "number")
|
|
72
|
+
return new Date(value);
|
|
73
|
+
return new Date(String(value));
|
|
74
|
+
}
|
|
75
|
+
function createDatabase(path2) {
|
|
76
|
+
return new duckdb.Database(path2);
|
|
77
|
+
}
|
|
78
|
+
function dbRun(db, sql, params = []) {
|
|
79
|
+
return new Promise((resolve2, reject) => {
|
|
80
|
+
if (params.length === 0) {
|
|
81
|
+
db.run(sql, (err) => {
|
|
82
|
+
if (err)
|
|
83
|
+
reject(err);
|
|
84
|
+
else
|
|
85
|
+
resolve2();
|
|
86
|
+
});
|
|
87
|
+
} else {
|
|
88
|
+
db.run(sql, ...params, (err) => {
|
|
89
|
+
if (err)
|
|
90
|
+
reject(err);
|
|
91
|
+
else
|
|
92
|
+
resolve2();
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
function dbAll(db, sql, params = []) {
|
|
98
|
+
return new Promise((resolve2, reject) => {
|
|
99
|
+
if (params.length === 0) {
|
|
100
|
+
db.all(sql, (err, rows) => {
|
|
101
|
+
if (err)
|
|
102
|
+
reject(err);
|
|
103
|
+
else
|
|
104
|
+
resolve2(convertBigInts(rows || []));
|
|
105
|
+
});
|
|
106
|
+
} else {
|
|
107
|
+
db.all(sql, ...params, (err, rows) => {
|
|
108
|
+
if (err)
|
|
109
|
+
reject(err);
|
|
110
|
+
else
|
|
111
|
+
resolve2(convertBigInts(rows || []));
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
function dbClose(db) {
|
|
117
|
+
return new Promise((resolve2, reject) => {
|
|
118
|
+
db.close((err) => {
|
|
119
|
+
if (err)
|
|
120
|
+
reject(err);
|
|
121
|
+
else
|
|
122
|
+
resolve2();
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// src/core/event-store.ts
|
|
128
|
+
var EventStore = class {
|
|
129
|
+
constructor(dbPath) {
|
|
130
|
+
this.dbPath = dbPath;
|
|
131
|
+
this.db = createDatabase(dbPath);
|
|
132
|
+
}
|
|
133
|
+
db;
|
|
134
|
+
initialized = false;
|
|
135
|
+
/**
|
|
136
|
+
* Initialize database schema
|
|
137
|
+
*/
|
|
138
|
+
async initialize() {
|
|
139
|
+
if (this.initialized)
|
|
140
|
+
return;
|
|
141
|
+
await dbRun(this.db, `
|
|
142
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
143
|
+
id VARCHAR PRIMARY KEY,
|
|
144
|
+
event_type VARCHAR NOT NULL,
|
|
145
|
+
session_id VARCHAR NOT NULL,
|
|
146
|
+
timestamp TIMESTAMP NOT NULL,
|
|
147
|
+
content TEXT NOT NULL,
|
|
148
|
+
canonical_key VARCHAR NOT NULL,
|
|
149
|
+
dedupe_key VARCHAR UNIQUE,
|
|
150
|
+
metadata JSON
|
|
151
|
+
)
|
|
152
|
+
`);
|
|
153
|
+
await dbRun(this.db, `
|
|
154
|
+
CREATE TABLE IF NOT EXISTS event_dedup (
|
|
155
|
+
dedupe_key VARCHAR PRIMARY KEY,
|
|
156
|
+
event_id VARCHAR NOT NULL,
|
|
157
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
158
|
+
)
|
|
159
|
+
`);
|
|
160
|
+
await dbRun(this.db, `
|
|
161
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
162
|
+
id VARCHAR PRIMARY KEY,
|
|
163
|
+
started_at TIMESTAMP NOT NULL,
|
|
164
|
+
ended_at TIMESTAMP,
|
|
165
|
+
project_path VARCHAR,
|
|
166
|
+
summary TEXT,
|
|
167
|
+
tags JSON
|
|
168
|
+
)
|
|
169
|
+
`);
|
|
170
|
+
await dbRun(this.db, `
|
|
171
|
+
CREATE TABLE IF NOT EXISTS insights (
|
|
172
|
+
id VARCHAR PRIMARY KEY,
|
|
173
|
+
insight_type VARCHAR NOT NULL,
|
|
174
|
+
content TEXT NOT NULL,
|
|
175
|
+
canonical_key VARCHAR NOT NULL,
|
|
176
|
+
confidence FLOAT,
|
|
177
|
+
source_events JSON,
|
|
178
|
+
created_at TIMESTAMP,
|
|
179
|
+
last_updated TIMESTAMP
|
|
180
|
+
)
|
|
181
|
+
`);
|
|
182
|
+
await dbRun(this.db, `
|
|
183
|
+
CREATE TABLE IF NOT EXISTS embedding_outbox (
|
|
184
|
+
id VARCHAR PRIMARY KEY,
|
|
185
|
+
event_id VARCHAR NOT NULL,
|
|
186
|
+
content TEXT NOT NULL,
|
|
187
|
+
status VARCHAR DEFAULT 'pending',
|
|
188
|
+
retry_count INT DEFAULT 0,
|
|
189
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
190
|
+
processed_at TIMESTAMP,
|
|
191
|
+
error_message TEXT
|
|
192
|
+
)
|
|
193
|
+
`);
|
|
194
|
+
await dbRun(this.db, `
|
|
195
|
+
CREATE TABLE IF NOT EXISTS projection_offsets (
|
|
196
|
+
projection_name VARCHAR PRIMARY KEY,
|
|
197
|
+
last_event_id VARCHAR,
|
|
198
|
+
last_timestamp TIMESTAMP,
|
|
199
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
200
|
+
)
|
|
201
|
+
`);
|
|
202
|
+
await dbRun(this.db, `
|
|
203
|
+
CREATE TABLE IF NOT EXISTS memory_levels (
|
|
204
|
+
event_id VARCHAR PRIMARY KEY,
|
|
205
|
+
level VARCHAR NOT NULL DEFAULT 'L0',
|
|
206
|
+
promoted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
207
|
+
)
|
|
208
|
+
`);
|
|
209
|
+
await dbRun(this.db, `
|
|
210
|
+
CREATE TABLE IF NOT EXISTS entries (
|
|
211
|
+
entry_id VARCHAR PRIMARY KEY,
|
|
212
|
+
created_ts TIMESTAMP NOT NULL,
|
|
213
|
+
entry_type VARCHAR NOT NULL,
|
|
214
|
+
title VARCHAR NOT NULL,
|
|
215
|
+
content_json JSON NOT NULL,
|
|
216
|
+
stage VARCHAR NOT NULL DEFAULT 'raw',
|
|
217
|
+
status VARCHAR DEFAULT 'active',
|
|
218
|
+
superseded_by VARCHAR,
|
|
219
|
+
build_id VARCHAR,
|
|
220
|
+
evidence_json JSON,
|
|
221
|
+
canonical_key VARCHAR,
|
|
222
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
223
|
+
)
|
|
224
|
+
`);
|
|
225
|
+
await dbRun(this.db, `
|
|
226
|
+
CREATE TABLE IF NOT EXISTS entities (
|
|
227
|
+
entity_id VARCHAR PRIMARY KEY,
|
|
228
|
+
entity_type VARCHAR NOT NULL,
|
|
229
|
+
canonical_key VARCHAR NOT NULL,
|
|
230
|
+
title VARCHAR NOT NULL,
|
|
231
|
+
stage VARCHAR NOT NULL DEFAULT 'raw',
|
|
232
|
+
status VARCHAR NOT NULL DEFAULT 'active',
|
|
233
|
+
current_json JSON NOT NULL,
|
|
234
|
+
title_norm VARCHAR,
|
|
235
|
+
search_text VARCHAR,
|
|
236
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
237
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
238
|
+
)
|
|
239
|
+
`);
|
|
240
|
+
await dbRun(this.db, `
|
|
241
|
+
CREATE TABLE IF NOT EXISTS entity_aliases (
|
|
242
|
+
entity_type VARCHAR NOT NULL,
|
|
243
|
+
canonical_key VARCHAR NOT NULL,
|
|
244
|
+
entity_id VARCHAR NOT NULL,
|
|
245
|
+
is_primary BOOLEAN DEFAULT FALSE,
|
|
246
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
247
|
+
PRIMARY KEY(entity_type, canonical_key)
|
|
248
|
+
)
|
|
249
|
+
`);
|
|
250
|
+
await dbRun(this.db, `
|
|
251
|
+
CREATE TABLE IF NOT EXISTS edges (
|
|
252
|
+
edge_id VARCHAR PRIMARY KEY,
|
|
253
|
+
src_type VARCHAR NOT NULL,
|
|
254
|
+
src_id VARCHAR NOT NULL,
|
|
255
|
+
rel_type VARCHAR NOT NULL,
|
|
256
|
+
dst_type VARCHAR NOT NULL,
|
|
257
|
+
dst_id VARCHAR NOT NULL,
|
|
258
|
+
meta_json JSON,
|
|
259
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
260
|
+
)
|
|
261
|
+
`);
|
|
262
|
+
await dbRun(this.db, `
|
|
263
|
+
CREATE TABLE IF NOT EXISTS vector_outbox (
|
|
264
|
+
job_id VARCHAR PRIMARY KEY,
|
|
265
|
+
item_kind VARCHAR NOT NULL,
|
|
266
|
+
item_id VARCHAR NOT NULL,
|
|
267
|
+
embedding_version VARCHAR NOT NULL,
|
|
268
|
+
status VARCHAR NOT NULL DEFAULT 'pending',
|
|
269
|
+
retry_count INT DEFAULT 0,
|
|
270
|
+
error VARCHAR,
|
|
271
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
272
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
273
|
+
UNIQUE(item_kind, item_id, embedding_version)
|
|
274
|
+
)
|
|
275
|
+
`);
|
|
276
|
+
await dbRun(this.db, `
|
|
277
|
+
CREATE TABLE IF NOT EXISTS build_runs (
|
|
278
|
+
build_id VARCHAR PRIMARY KEY,
|
|
279
|
+
started_at TIMESTAMP NOT NULL,
|
|
280
|
+
finished_at TIMESTAMP,
|
|
281
|
+
extractor_model VARCHAR NOT NULL,
|
|
282
|
+
extractor_prompt_hash VARCHAR NOT NULL,
|
|
283
|
+
embedder_model VARCHAR NOT NULL,
|
|
284
|
+
embedding_version VARCHAR NOT NULL,
|
|
285
|
+
idris_version VARCHAR NOT NULL,
|
|
286
|
+
schema_version VARCHAR NOT NULL,
|
|
287
|
+
status VARCHAR NOT NULL DEFAULT 'running',
|
|
288
|
+
error VARCHAR
|
|
289
|
+
)
|
|
290
|
+
`);
|
|
291
|
+
await dbRun(this.db, `
|
|
292
|
+
CREATE TABLE IF NOT EXISTS pipeline_metrics (
|
|
293
|
+
id VARCHAR PRIMARY KEY,
|
|
294
|
+
ts TIMESTAMP NOT NULL,
|
|
295
|
+
stage VARCHAR NOT NULL,
|
|
296
|
+
latency_ms DOUBLE NOT NULL,
|
|
297
|
+
success BOOLEAN NOT NULL,
|
|
298
|
+
error VARCHAR,
|
|
299
|
+
session_id VARCHAR
|
|
300
|
+
)
|
|
301
|
+
`);
|
|
302
|
+
await dbRun(this.db, `
|
|
303
|
+
CREATE TABLE IF NOT EXISTS working_set (
|
|
304
|
+
id VARCHAR PRIMARY KEY,
|
|
305
|
+
event_id VARCHAR NOT NULL,
|
|
306
|
+
added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
307
|
+
relevance_score FLOAT DEFAULT 1.0,
|
|
308
|
+
topics JSON,
|
|
309
|
+
expires_at TIMESTAMP
|
|
310
|
+
)
|
|
311
|
+
`);
|
|
312
|
+
await dbRun(this.db, `
|
|
313
|
+
CREATE TABLE IF NOT EXISTS consolidated_memories (
|
|
314
|
+
memory_id VARCHAR PRIMARY KEY,
|
|
315
|
+
summary TEXT NOT NULL,
|
|
316
|
+
topics JSON,
|
|
317
|
+
source_events JSON,
|
|
318
|
+
confidence FLOAT DEFAULT 0.5,
|
|
319
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
320
|
+
accessed_at TIMESTAMP,
|
|
321
|
+
access_count INTEGER DEFAULT 0
|
|
322
|
+
)
|
|
323
|
+
`);
|
|
324
|
+
await dbRun(this.db, `
|
|
325
|
+
CREATE TABLE IF NOT EXISTS continuity_log (
|
|
326
|
+
log_id VARCHAR PRIMARY KEY,
|
|
327
|
+
from_context_id VARCHAR,
|
|
328
|
+
to_context_id VARCHAR,
|
|
329
|
+
continuity_score FLOAT,
|
|
330
|
+
transition_type VARCHAR,
|
|
331
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
332
|
+
)
|
|
333
|
+
`);
|
|
334
|
+
await dbRun(this.db, `
|
|
335
|
+
CREATE TABLE IF NOT EXISTS endless_config (
|
|
336
|
+
key VARCHAR PRIMARY KEY,
|
|
337
|
+
value JSON,
|
|
338
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
339
|
+
)
|
|
340
|
+
`);
|
|
341
|
+
await dbRun(this.db, `CREATE INDEX IF NOT EXISTS idx_entries_type ON entries(entry_type)`);
|
|
342
|
+
await dbRun(this.db, `CREATE INDEX IF NOT EXISTS idx_entries_stage ON entries(stage)`);
|
|
343
|
+
await dbRun(this.db, `CREATE INDEX IF NOT EXISTS idx_entries_canonical ON entries(canonical_key)`);
|
|
344
|
+
await dbRun(this.db, `CREATE INDEX IF NOT EXISTS idx_entities_type_key ON entities(entity_type, canonical_key)`);
|
|
345
|
+
await dbRun(this.db, `CREATE INDEX IF NOT EXISTS idx_entities_status ON entities(status)`);
|
|
346
|
+
await dbRun(this.db, `CREATE INDEX IF NOT EXISTS idx_edges_src ON edges(src_id, rel_type)`);
|
|
347
|
+
await dbRun(this.db, `CREATE INDEX IF NOT EXISTS idx_edges_dst ON edges(dst_id, rel_type)`);
|
|
348
|
+
await dbRun(this.db, `CREATE INDEX IF NOT EXISTS idx_edges_rel ON edges(rel_type)`);
|
|
349
|
+
await dbRun(this.db, `CREATE INDEX IF NOT EXISTS idx_outbox_status ON vector_outbox(status)`);
|
|
350
|
+
await dbRun(this.db, `CREATE INDEX IF NOT EXISTS idx_working_set_expires ON working_set(expires_at)`);
|
|
351
|
+
await dbRun(this.db, `CREATE INDEX IF NOT EXISTS idx_working_set_relevance ON working_set(relevance_score DESC)`);
|
|
352
|
+
await dbRun(this.db, `CREATE INDEX IF NOT EXISTS idx_consolidated_confidence ON consolidated_memories(confidence DESC)`);
|
|
353
|
+
await dbRun(this.db, `CREATE INDEX IF NOT EXISTS idx_continuity_created ON continuity_log(created_at)`);
|
|
354
|
+
this.initialized = true;
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Append event to store (AXIOMMIND Principle 2: Append-only)
|
|
358
|
+
* Returns existing event ID if duplicate (Principle 3: Idempotency)
|
|
359
|
+
*/
|
|
360
|
+
async append(input) {
|
|
361
|
+
await this.initialize();
|
|
362
|
+
const canonicalKey = makeCanonicalKey(input.content);
|
|
363
|
+
const dedupeKey = makeDedupeKey(input.content, input.sessionId);
|
|
364
|
+
const existing = await dbAll(
|
|
365
|
+
this.db,
|
|
366
|
+
`SELECT event_id FROM event_dedup WHERE dedupe_key = ?`,
|
|
367
|
+
[dedupeKey]
|
|
368
|
+
);
|
|
369
|
+
if (existing.length > 0) {
|
|
370
|
+
return {
|
|
371
|
+
success: true,
|
|
372
|
+
eventId: existing[0].event_id,
|
|
373
|
+
isDuplicate: true
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
const id = randomUUID();
|
|
377
|
+
const timestamp = input.timestamp.toISOString();
|
|
378
|
+
try {
|
|
379
|
+
await dbRun(
|
|
380
|
+
this.db,
|
|
381
|
+
`INSERT INTO events (id, event_type, session_id, timestamp, content, canonical_key, dedupe_key, metadata)
|
|
382
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
383
|
+
[
|
|
384
|
+
id,
|
|
385
|
+
input.eventType,
|
|
386
|
+
input.sessionId,
|
|
387
|
+
timestamp,
|
|
388
|
+
input.content,
|
|
389
|
+
canonicalKey,
|
|
390
|
+
dedupeKey,
|
|
391
|
+
JSON.stringify(input.metadata || {})
|
|
392
|
+
]
|
|
393
|
+
);
|
|
394
|
+
await dbRun(
|
|
395
|
+
this.db,
|
|
396
|
+
`INSERT INTO event_dedup (dedupe_key, event_id) VALUES (?, ?)`,
|
|
397
|
+
[dedupeKey, id]
|
|
398
|
+
);
|
|
399
|
+
await dbRun(
|
|
400
|
+
this.db,
|
|
401
|
+
`INSERT INTO memory_levels (event_id, level) VALUES (?, 'L0')`,
|
|
402
|
+
[id]
|
|
403
|
+
);
|
|
404
|
+
return { success: true, eventId: id, isDuplicate: false };
|
|
405
|
+
} catch (error) {
|
|
406
|
+
return {
|
|
407
|
+
success: false,
|
|
408
|
+
error: error instanceof Error ? error.message : String(error)
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Get events by session ID
|
|
414
|
+
*/
|
|
415
|
+
async getSessionEvents(sessionId) {
|
|
416
|
+
await this.initialize();
|
|
417
|
+
const rows = await dbAll(
|
|
418
|
+
this.db,
|
|
419
|
+
`SELECT * FROM events WHERE session_id = ? ORDER BY timestamp ASC`,
|
|
420
|
+
[sessionId]
|
|
421
|
+
);
|
|
422
|
+
return rows.map(this.rowToEvent);
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Get recent events
|
|
426
|
+
*/
|
|
427
|
+
async getRecentEvents(limit = 100) {
|
|
428
|
+
await this.initialize();
|
|
429
|
+
const rows = await dbAll(
|
|
430
|
+
this.db,
|
|
431
|
+
`SELECT * FROM events ORDER BY timestamp DESC LIMIT ?`,
|
|
432
|
+
[limit]
|
|
433
|
+
);
|
|
434
|
+
return rows.map(this.rowToEvent);
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Get event by ID
|
|
438
|
+
*/
|
|
439
|
+
async getEvent(id) {
|
|
440
|
+
await this.initialize();
|
|
441
|
+
const rows = await dbAll(
|
|
442
|
+
this.db,
|
|
443
|
+
`SELECT * FROM events WHERE id = ?`,
|
|
444
|
+
[id]
|
|
445
|
+
);
|
|
446
|
+
if (rows.length === 0)
|
|
447
|
+
return null;
|
|
448
|
+
return this.rowToEvent(rows[0]);
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Create or update session
|
|
452
|
+
*/
|
|
453
|
+
async upsertSession(session) {
|
|
454
|
+
await this.initialize();
|
|
455
|
+
const existing = await dbAll(
|
|
456
|
+
this.db,
|
|
457
|
+
`SELECT id FROM sessions WHERE id = ?`,
|
|
458
|
+
[session.id]
|
|
459
|
+
);
|
|
460
|
+
if (existing.length === 0) {
|
|
461
|
+
await dbRun(
|
|
462
|
+
this.db,
|
|
463
|
+
`INSERT INTO sessions (id, started_at, project_path, tags)
|
|
464
|
+
VALUES (?, ?, ?, ?)`,
|
|
465
|
+
[
|
|
466
|
+
session.id,
|
|
467
|
+
(session.startedAt || /* @__PURE__ */ new Date()).toISOString(),
|
|
468
|
+
session.projectPath || null,
|
|
469
|
+
JSON.stringify(session.tags || [])
|
|
470
|
+
]
|
|
471
|
+
);
|
|
472
|
+
} else {
|
|
473
|
+
const updates = [];
|
|
474
|
+
const values = [];
|
|
475
|
+
if (session.endedAt) {
|
|
476
|
+
updates.push("ended_at = ?");
|
|
477
|
+
values.push(session.endedAt.toISOString());
|
|
478
|
+
}
|
|
479
|
+
if (session.summary) {
|
|
480
|
+
updates.push("summary = ?");
|
|
481
|
+
values.push(session.summary);
|
|
482
|
+
}
|
|
483
|
+
if (session.tags) {
|
|
484
|
+
updates.push("tags = ?");
|
|
485
|
+
values.push(JSON.stringify(session.tags));
|
|
486
|
+
}
|
|
487
|
+
if (updates.length > 0) {
|
|
488
|
+
values.push(session.id);
|
|
489
|
+
await dbRun(
|
|
490
|
+
this.db,
|
|
491
|
+
`UPDATE sessions SET ${updates.join(", ")} WHERE id = ?`,
|
|
492
|
+
values
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Get session by ID
|
|
499
|
+
*/
|
|
500
|
+
async getSession(id) {
|
|
501
|
+
await this.initialize();
|
|
502
|
+
const rows = await dbAll(
|
|
503
|
+
this.db,
|
|
504
|
+
`SELECT * FROM sessions WHERE id = ?`,
|
|
505
|
+
[id]
|
|
506
|
+
);
|
|
507
|
+
if (rows.length === 0)
|
|
508
|
+
return null;
|
|
509
|
+
const row = rows[0];
|
|
510
|
+
return {
|
|
511
|
+
id: row.id,
|
|
512
|
+
startedAt: toDate(row.started_at),
|
|
513
|
+
endedAt: row.ended_at ? toDate(row.ended_at) : void 0,
|
|
514
|
+
projectPath: row.project_path,
|
|
515
|
+
summary: row.summary,
|
|
516
|
+
tags: row.tags ? JSON.parse(row.tags) : void 0
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Add to embedding outbox (Single-Writer Pattern)
|
|
521
|
+
*/
|
|
522
|
+
async enqueueForEmbedding(eventId, content) {
|
|
523
|
+
await this.initialize();
|
|
524
|
+
const id = randomUUID();
|
|
525
|
+
await dbRun(
|
|
526
|
+
this.db,
|
|
527
|
+
`INSERT INTO embedding_outbox (id, event_id, content, status, retry_count)
|
|
528
|
+
VALUES (?, ?, ?, 'pending', 0)`,
|
|
529
|
+
[id, eventId, content]
|
|
530
|
+
);
|
|
531
|
+
return id;
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Get pending outbox items
|
|
535
|
+
*/
|
|
536
|
+
async getPendingOutboxItems(limit = 32) {
|
|
537
|
+
await this.initialize();
|
|
538
|
+
const pending = await dbAll(
|
|
539
|
+
this.db,
|
|
540
|
+
`SELECT * FROM embedding_outbox
|
|
541
|
+
WHERE status = 'pending'
|
|
542
|
+
ORDER BY created_at
|
|
543
|
+
LIMIT ?`,
|
|
544
|
+
[limit]
|
|
545
|
+
);
|
|
546
|
+
if (pending.length === 0)
|
|
547
|
+
return [];
|
|
548
|
+
const ids = pending.map((r) => r.id);
|
|
549
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
550
|
+
await dbRun(
|
|
551
|
+
this.db,
|
|
552
|
+
`UPDATE embedding_outbox SET status = 'processing' WHERE id IN (${placeholders})`,
|
|
553
|
+
ids
|
|
554
|
+
);
|
|
555
|
+
return pending.map((row) => ({
|
|
556
|
+
id: row.id,
|
|
557
|
+
eventId: row.event_id,
|
|
558
|
+
content: row.content,
|
|
559
|
+
status: "processing",
|
|
560
|
+
retryCount: row.retry_count,
|
|
561
|
+
createdAt: toDate(row.created_at),
|
|
562
|
+
errorMessage: row.error_message
|
|
563
|
+
}));
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Mark outbox items as done
|
|
567
|
+
*/
|
|
568
|
+
async completeOutboxItems(ids) {
|
|
569
|
+
if (ids.length === 0)
|
|
570
|
+
return;
|
|
571
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
572
|
+
await dbRun(
|
|
573
|
+
this.db,
|
|
574
|
+
`DELETE FROM embedding_outbox WHERE id IN (${placeholders})`,
|
|
575
|
+
ids
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Mark outbox items as failed
|
|
580
|
+
*/
|
|
581
|
+
async failOutboxItems(ids, error) {
|
|
582
|
+
if (ids.length === 0)
|
|
583
|
+
return;
|
|
584
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
585
|
+
await dbRun(
|
|
586
|
+
this.db,
|
|
587
|
+
`UPDATE embedding_outbox
|
|
588
|
+
SET status = CASE WHEN retry_count >= 3 THEN 'failed' ELSE 'pending' END,
|
|
589
|
+
retry_count = retry_count + 1,
|
|
590
|
+
error_message = ?
|
|
591
|
+
WHERE id IN (${placeholders})`,
|
|
592
|
+
[error, ...ids]
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Update memory level
|
|
597
|
+
*/
|
|
598
|
+
async updateMemoryLevel(eventId, level) {
|
|
599
|
+
await this.initialize();
|
|
600
|
+
await dbRun(
|
|
601
|
+
this.db,
|
|
602
|
+
`UPDATE memory_levels SET level = ?, promoted_at = CURRENT_TIMESTAMP WHERE event_id = ?`,
|
|
603
|
+
[level, eventId]
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Get memory level statistics
|
|
608
|
+
*/
|
|
609
|
+
async getLevelStats() {
|
|
610
|
+
await this.initialize();
|
|
611
|
+
const rows = await dbAll(
|
|
612
|
+
this.db,
|
|
613
|
+
`SELECT level, COUNT(*) as count FROM memory_levels GROUP BY level`
|
|
614
|
+
);
|
|
615
|
+
return rows;
|
|
616
|
+
}
|
|
617
|
+
// ============================================================
|
|
618
|
+
// Endless Mode Helper Methods
|
|
619
|
+
// ============================================================
|
|
620
|
+
/**
|
|
621
|
+
* Get database instance for Endless Mode stores
|
|
622
|
+
*/
|
|
623
|
+
getDatabase() {
|
|
624
|
+
return this.db;
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Get config value for endless mode
|
|
628
|
+
*/
|
|
629
|
+
async getEndlessConfig(key) {
|
|
630
|
+
await this.initialize();
|
|
631
|
+
const rows = await dbAll(
|
|
632
|
+
this.db,
|
|
633
|
+
`SELECT value FROM endless_config WHERE key = ?`,
|
|
634
|
+
[key]
|
|
635
|
+
);
|
|
636
|
+
if (rows.length === 0)
|
|
637
|
+
return null;
|
|
638
|
+
return JSON.parse(rows[0].value);
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Set config value for endless mode
|
|
642
|
+
*/
|
|
643
|
+
async setEndlessConfig(key, value) {
|
|
644
|
+
await this.initialize();
|
|
645
|
+
await dbRun(
|
|
646
|
+
this.db,
|
|
647
|
+
`INSERT OR REPLACE INTO endless_config (key, value, updated_at)
|
|
648
|
+
VALUES (?, ?, CURRENT_TIMESTAMP)`,
|
|
649
|
+
[key, JSON.stringify(value)]
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Get all sessions
|
|
654
|
+
*/
|
|
655
|
+
async getAllSessions() {
|
|
656
|
+
await this.initialize();
|
|
657
|
+
const rows = await dbAll(
|
|
658
|
+
this.db,
|
|
659
|
+
`SELECT * FROM sessions ORDER BY started_at DESC`
|
|
660
|
+
);
|
|
661
|
+
return rows.map((row) => ({
|
|
662
|
+
id: row.id,
|
|
663
|
+
startedAt: toDate(row.started_at),
|
|
664
|
+
endedAt: row.ended_at ? toDate(row.ended_at) : void 0,
|
|
665
|
+
projectPath: row.project_path,
|
|
666
|
+
summary: row.summary,
|
|
667
|
+
tags: row.tags ? JSON.parse(row.tags) : void 0
|
|
668
|
+
}));
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Close database connection
|
|
672
|
+
*/
|
|
673
|
+
async close() {
|
|
674
|
+
await dbClose(this.db);
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Convert database row to MemoryEvent
|
|
678
|
+
*/
|
|
679
|
+
rowToEvent(row) {
|
|
680
|
+
return {
|
|
681
|
+
id: row.id,
|
|
682
|
+
eventType: row.event_type,
|
|
683
|
+
sessionId: row.session_id,
|
|
684
|
+
timestamp: toDate(row.timestamp),
|
|
685
|
+
content: row.content,
|
|
686
|
+
canonicalKey: row.canonical_key,
|
|
687
|
+
dedupeKey: row.dedupe_key,
|
|
688
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : void 0
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
// src/core/vector-store.ts
|
|
694
|
+
import * as lancedb from "@lancedb/lancedb";
|
|
695
|
+
var VectorStore = class {
|
|
696
|
+
constructor(dbPath) {
|
|
697
|
+
this.dbPath = dbPath;
|
|
698
|
+
}
|
|
699
|
+
db = null;
|
|
700
|
+
table = null;
|
|
701
|
+
tableName = "conversations";
|
|
702
|
+
/**
|
|
703
|
+
* Initialize LanceDB connection
|
|
704
|
+
*/
|
|
705
|
+
async initialize() {
|
|
706
|
+
if (this.db)
|
|
707
|
+
return;
|
|
708
|
+
this.db = await lancedb.connect(this.dbPath);
|
|
709
|
+
try {
|
|
710
|
+
const tables = await this.db.tableNames();
|
|
711
|
+
if (tables.includes(this.tableName)) {
|
|
712
|
+
this.table = await this.db.openTable(this.tableName);
|
|
713
|
+
}
|
|
714
|
+
} catch {
|
|
715
|
+
this.table = null;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Add or update vector record
|
|
720
|
+
*/
|
|
721
|
+
async upsert(record) {
|
|
722
|
+
await this.initialize();
|
|
723
|
+
if (!this.db) {
|
|
724
|
+
throw new Error("Database not initialized");
|
|
725
|
+
}
|
|
726
|
+
const data = {
|
|
727
|
+
id: record.id,
|
|
728
|
+
eventId: record.eventId,
|
|
729
|
+
sessionId: record.sessionId,
|
|
730
|
+
eventType: record.eventType,
|
|
731
|
+
content: record.content,
|
|
732
|
+
vector: record.vector,
|
|
733
|
+
timestamp: record.timestamp,
|
|
734
|
+
metadata: JSON.stringify(record.metadata || {})
|
|
735
|
+
};
|
|
736
|
+
if (!this.table) {
|
|
737
|
+
this.table = await this.db.createTable(this.tableName, [data]);
|
|
738
|
+
} else {
|
|
739
|
+
await this.table.add([data]);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* Add multiple vector records in batch
|
|
744
|
+
*/
|
|
745
|
+
async upsertBatch(records) {
|
|
746
|
+
if (records.length === 0)
|
|
747
|
+
return;
|
|
748
|
+
await this.initialize();
|
|
749
|
+
if (!this.db) {
|
|
750
|
+
throw new Error("Database not initialized");
|
|
751
|
+
}
|
|
752
|
+
const data = records.map((record) => ({
|
|
753
|
+
id: record.id,
|
|
754
|
+
eventId: record.eventId,
|
|
755
|
+
sessionId: record.sessionId,
|
|
756
|
+
eventType: record.eventType,
|
|
757
|
+
content: record.content,
|
|
758
|
+
vector: record.vector,
|
|
759
|
+
timestamp: record.timestamp,
|
|
760
|
+
metadata: JSON.stringify(record.metadata || {})
|
|
761
|
+
}));
|
|
762
|
+
if (!this.table) {
|
|
763
|
+
this.table = await this.db.createTable(this.tableName, data);
|
|
764
|
+
} else {
|
|
765
|
+
await this.table.add(data);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* Search for similar vectors
|
|
770
|
+
*/
|
|
771
|
+
async search(queryVector, options = {}) {
|
|
772
|
+
await this.initialize();
|
|
773
|
+
if (!this.table) {
|
|
774
|
+
return [];
|
|
775
|
+
}
|
|
776
|
+
const { limit = 5, minScore = 0.7, sessionId } = options;
|
|
777
|
+
let query = this.table.search(queryVector).distanceType("cosine").limit(limit * 2);
|
|
778
|
+
if (sessionId) {
|
|
779
|
+
query = query.where(`sessionId = '${sessionId}'`);
|
|
780
|
+
}
|
|
781
|
+
const results = await query.toArray();
|
|
782
|
+
return results.filter((r) => {
|
|
783
|
+
const distance = r._distance || 0;
|
|
784
|
+
const score = 1 - distance / 2;
|
|
785
|
+
return score >= minScore;
|
|
786
|
+
}).slice(0, limit).map((r) => {
|
|
787
|
+
const distance = r._distance || 0;
|
|
788
|
+
const score = 1 - distance / 2;
|
|
789
|
+
return {
|
|
790
|
+
id: r.id,
|
|
791
|
+
eventId: r.eventId,
|
|
792
|
+
content: r.content,
|
|
793
|
+
score,
|
|
794
|
+
sessionId: r.sessionId,
|
|
795
|
+
eventType: r.eventType,
|
|
796
|
+
timestamp: r.timestamp
|
|
797
|
+
};
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* Delete vector by event ID
|
|
802
|
+
*/
|
|
803
|
+
async delete(eventId) {
|
|
804
|
+
if (!this.table)
|
|
805
|
+
return;
|
|
806
|
+
await this.table.delete(`eventId = '${eventId}'`);
|
|
807
|
+
}
|
|
808
|
+
/**
|
|
809
|
+
* Get total count of vectors
|
|
810
|
+
*/
|
|
811
|
+
async count() {
|
|
812
|
+
if (!this.table)
|
|
813
|
+
return 0;
|
|
814
|
+
const result = await this.table.countRows();
|
|
815
|
+
return result;
|
|
816
|
+
}
|
|
817
|
+
/**
|
|
818
|
+
* Check if vector exists for event
|
|
819
|
+
*/
|
|
820
|
+
async exists(eventId) {
|
|
821
|
+
if (!this.table)
|
|
822
|
+
return false;
|
|
823
|
+
const results = await this.table.search([]).where(`eventId = '${eventId}'`).limit(1).toArray();
|
|
824
|
+
return results.length > 0;
|
|
825
|
+
}
|
|
826
|
+
};
|
|
827
|
+
|
|
828
|
+
// src/core/embedder.ts
|
|
829
|
+
import { pipeline } from "@xenova/transformers";
|
|
830
|
+
var Embedder = class {
|
|
831
|
+
pipeline = null;
|
|
832
|
+
modelName;
|
|
833
|
+
initialized = false;
|
|
834
|
+
constructor(modelName = "Xenova/all-MiniLM-L6-v2") {
|
|
835
|
+
this.modelName = modelName;
|
|
836
|
+
}
|
|
837
|
+
/**
|
|
838
|
+
* Initialize the embedding pipeline
|
|
839
|
+
*/
|
|
840
|
+
async initialize() {
|
|
841
|
+
if (this.initialized)
|
|
842
|
+
return;
|
|
843
|
+
this.pipeline = await pipeline("feature-extraction", this.modelName);
|
|
844
|
+
this.initialized = true;
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* Generate embedding for a single text
|
|
848
|
+
*/
|
|
849
|
+
async embed(text) {
|
|
850
|
+
await this.initialize();
|
|
851
|
+
if (!this.pipeline) {
|
|
852
|
+
throw new Error("Embedding pipeline not initialized");
|
|
853
|
+
}
|
|
854
|
+
const output = await this.pipeline(text, {
|
|
855
|
+
pooling: "mean",
|
|
856
|
+
normalize: true
|
|
857
|
+
});
|
|
858
|
+
const vector = Array.from(output.data);
|
|
859
|
+
return {
|
|
860
|
+
vector,
|
|
861
|
+
model: this.modelName,
|
|
862
|
+
dimensions: vector.length
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
/**
|
|
866
|
+
* Generate embeddings for multiple texts in batch
|
|
867
|
+
*/
|
|
868
|
+
async embedBatch(texts) {
|
|
869
|
+
await this.initialize();
|
|
870
|
+
if (!this.pipeline) {
|
|
871
|
+
throw new Error("Embedding pipeline not initialized");
|
|
872
|
+
}
|
|
873
|
+
const results = [];
|
|
874
|
+
const batchSize = 32;
|
|
875
|
+
for (let i = 0; i < texts.length; i += batchSize) {
|
|
876
|
+
const batch = texts.slice(i, i + batchSize);
|
|
877
|
+
for (const text of batch) {
|
|
878
|
+
const output = await this.pipeline(text, {
|
|
879
|
+
pooling: "mean",
|
|
880
|
+
normalize: true
|
|
881
|
+
});
|
|
882
|
+
const vector = Array.from(output.data);
|
|
883
|
+
results.push({
|
|
884
|
+
vector,
|
|
885
|
+
model: this.modelName,
|
|
886
|
+
dimensions: vector.length
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
return results;
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* Get embedding dimensions for the current model
|
|
894
|
+
*/
|
|
895
|
+
async getDimensions() {
|
|
896
|
+
const result = await this.embed("test");
|
|
897
|
+
return result.dimensions;
|
|
898
|
+
}
|
|
899
|
+
/**
|
|
900
|
+
* Check if embedder is ready
|
|
901
|
+
*/
|
|
902
|
+
isReady() {
|
|
903
|
+
return this.initialized && this.pipeline !== null;
|
|
904
|
+
}
|
|
905
|
+
/**
|
|
906
|
+
* Get model name
|
|
907
|
+
*/
|
|
908
|
+
getModelName() {
|
|
909
|
+
return this.modelName;
|
|
910
|
+
}
|
|
911
|
+
};
|
|
912
|
+
var defaultEmbedder = null;
|
|
913
|
+
function getDefaultEmbedder() {
|
|
914
|
+
if (!defaultEmbedder) {
|
|
915
|
+
defaultEmbedder = new Embedder();
|
|
916
|
+
}
|
|
917
|
+
return defaultEmbedder;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// src/core/vector-outbox.ts
|
|
921
|
+
var DEFAULT_CONFIG = {
|
|
922
|
+
embeddingVersion: "v1",
|
|
923
|
+
maxRetries: 3,
|
|
924
|
+
stuckThresholdMs: 5 * 60 * 1e3,
|
|
925
|
+
// 5 minutes
|
|
926
|
+
cleanupDays: 7
|
|
927
|
+
};
|
|
928
|
+
|
|
929
|
+
// src/core/vector-worker.ts
|
|
930
|
+
var DEFAULT_CONFIG2 = {
|
|
931
|
+
batchSize: 32,
|
|
932
|
+
pollIntervalMs: 1e3,
|
|
933
|
+
maxRetries: 3
|
|
934
|
+
};
|
|
935
|
+
var VectorWorker = class {
|
|
936
|
+
eventStore;
|
|
937
|
+
vectorStore;
|
|
938
|
+
embedder;
|
|
939
|
+
config;
|
|
940
|
+
running = false;
|
|
941
|
+
pollTimeout = null;
|
|
942
|
+
constructor(eventStore, vectorStore, embedder, config = {}) {
|
|
943
|
+
this.eventStore = eventStore;
|
|
944
|
+
this.vectorStore = vectorStore;
|
|
945
|
+
this.embedder = embedder;
|
|
946
|
+
this.config = { ...DEFAULT_CONFIG2, ...config };
|
|
947
|
+
}
|
|
948
|
+
/**
|
|
949
|
+
* Start the worker polling loop
|
|
950
|
+
*/
|
|
951
|
+
start() {
|
|
952
|
+
if (this.running)
|
|
953
|
+
return;
|
|
954
|
+
this.running = true;
|
|
955
|
+
this.poll();
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* Stop the worker
|
|
959
|
+
*/
|
|
960
|
+
stop() {
|
|
961
|
+
this.running = false;
|
|
962
|
+
if (this.pollTimeout) {
|
|
963
|
+
clearTimeout(this.pollTimeout);
|
|
964
|
+
this.pollTimeout = null;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
/**
|
|
968
|
+
* Process a single batch of outbox items
|
|
969
|
+
*/
|
|
970
|
+
async processBatch() {
|
|
971
|
+
const items = await this.eventStore.getPendingOutboxItems(this.config.batchSize);
|
|
972
|
+
if (items.length === 0) {
|
|
973
|
+
return 0;
|
|
974
|
+
}
|
|
975
|
+
const successful = [];
|
|
976
|
+
const failed = [];
|
|
977
|
+
try {
|
|
978
|
+
const embeddings = await this.embedder.embedBatch(items.map((i) => i.content));
|
|
979
|
+
const records = [];
|
|
980
|
+
for (let i = 0; i < items.length; i++) {
|
|
981
|
+
const item = items[i];
|
|
982
|
+
const embedding = embeddings[i];
|
|
983
|
+
const event = await this.eventStore.getEvent(item.eventId);
|
|
984
|
+
if (!event) {
|
|
985
|
+
failed.push(item.id);
|
|
986
|
+
continue;
|
|
987
|
+
}
|
|
988
|
+
records.push({
|
|
989
|
+
id: `vec_${item.id}`,
|
|
990
|
+
eventId: item.eventId,
|
|
991
|
+
sessionId: event.sessionId,
|
|
992
|
+
eventType: event.eventType,
|
|
993
|
+
content: item.content,
|
|
994
|
+
vector: embedding.vector,
|
|
995
|
+
timestamp: event.timestamp.toISOString(),
|
|
996
|
+
metadata: event.metadata
|
|
997
|
+
});
|
|
998
|
+
successful.push(item.id);
|
|
999
|
+
}
|
|
1000
|
+
if (records.length > 0) {
|
|
1001
|
+
await this.vectorStore.upsertBatch(records);
|
|
1002
|
+
}
|
|
1003
|
+
if (successful.length > 0) {
|
|
1004
|
+
await this.eventStore.completeOutboxItems(successful);
|
|
1005
|
+
}
|
|
1006
|
+
if (failed.length > 0) {
|
|
1007
|
+
await this.eventStore.failOutboxItems(failed, "Event not found");
|
|
1008
|
+
}
|
|
1009
|
+
return successful.length;
|
|
1010
|
+
} catch (error) {
|
|
1011
|
+
const allIds = items.map((i) => i.id);
|
|
1012
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1013
|
+
await this.eventStore.failOutboxItems(allIds, errorMessage);
|
|
1014
|
+
throw error;
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
/**
|
|
1018
|
+
* Poll for new items
|
|
1019
|
+
*/
|
|
1020
|
+
async poll() {
|
|
1021
|
+
if (!this.running)
|
|
1022
|
+
return;
|
|
1023
|
+
try {
|
|
1024
|
+
await this.processBatch();
|
|
1025
|
+
} catch (error) {
|
|
1026
|
+
console.error("Vector worker error:", error);
|
|
1027
|
+
}
|
|
1028
|
+
this.pollTimeout = setTimeout(() => this.poll(), this.config.pollIntervalMs);
|
|
1029
|
+
}
|
|
1030
|
+
/**
|
|
1031
|
+
* Process all pending items (blocking)
|
|
1032
|
+
*/
|
|
1033
|
+
async processAll() {
|
|
1034
|
+
let totalProcessed = 0;
|
|
1035
|
+
let processed;
|
|
1036
|
+
do {
|
|
1037
|
+
processed = await this.processBatch();
|
|
1038
|
+
totalProcessed += processed;
|
|
1039
|
+
} while (processed > 0);
|
|
1040
|
+
return totalProcessed;
|
|
1041
|
+
}
|
|
1042
|
+
/**
|
|
1043
|
+
* Check if worker is running
|
|
1044
|
+
*/
|
|
1045
|
+
isRunning() {
|
|
1046
|
+
return this.running;
|
|
1047
|
+
}
|
|
1048
|
+
};
|
|
1049
|
+
function createVectorWorker(eventStore, vectorStore, embedder, config) {
|
|
1050
|
+
const worker = new VectorWorker(eventStore, vectorStore, embedder, config);
|
|
1051
|
+
return worker;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// src/core/matcher.ts
|
|
1055
|
+
var DEFAULT_CONFIG3 = {
|
|
1056
|
+
weights: {
|
|
1057
|
+
semanticSimilarity: 0.4,
|
|
1058
|
+
ftsScore: 0.25,
|
|
1059
|
+
recencyBonus: 0.2,
|
|
1060
|
+
statusWeight: 0.15
|
|
1061
|
+
},
|
|
1062
|
+
minCombinedScore: 0.92,
|
|
1063
|
+
minGap: 0.03,
|
|
1064
|
+
suggestionThreshold: 0.75
|
|
1065
|
+
};
|
|
1066
|
+
var Matcher = class {
|
|
1067
|
+
config;
|
|
1068
|
+
constructor(config = {}) {
|
|
1069
|
+
this.config = {
|
|
1070
|
+
...DEFAULT_CONFIG3,
|
|
1071
|
+
...config,
|
|
1072
|
+
weights: { ...DEFAULT_CONFIG3.weights, ...config.weights }
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
/**
|
|
1076
|
+
* Calculate combined score using AXIOMMIND weighted formula
|
|
1077
|
+
*/
|
|
1078
|
+
calculateCombinedScore(semanticScore, ftsScore = 0, recencyDays = 0, isActive = true) {
|
|
1079
|
+
const { weights } = this.config;
|
|
1080
|
+
const recencyBonus = Math.max(0, 1 - recencyDays / 30);
|
|
1081
|
+
const statusMultiplier = isActive ? 1 : 0.7;
|
|
1082
|
+
const combinedScore = weights.semanticSimilarity * semanticScore + weights.ftsScore * ftsScore + weights.recencyBonus * recencyBonus + weights.statusWeight * statusMultiplier;
|
|
1083
|
+
return Math.min(1, combinedScore);
|
|
1084
|
+
}
|
|
1085
|
+
/**
|
|
1086
|
+
* Classify match confidence based on AXIOMMIND thresholds
|
|
1087
|
+
*/
|
|
1088
|
+
classifyConfidence(topScore, secondScore) {
|
|
1089
|
+
const { minCombinedScore, minGap, suggestionThreshold } = this.config;
|
|
1090
|
+
const gap = secondScore !== null ? topScore - secondScore : Infinity;
|
|
1091
|
+
if (topScore >= minCombinedScore && gap >= minGap) {
|
|
1092
|
+
return "high";
|
|
1093
|
+
}
|
|
1094
|
+
if (topScore >= suggestionThreshold) {
|
|
1095
|
+
return "suggested";
|
|
1096
|
+
}
|
|
1097
|
+
return "none";
|
|
1098
|
+
}
|
|
1099
|
+
/**
|
|
1100
|
+
* Match search results to find best memory
|
|
1101
|
+
*/
|
|
1102
|
+
matchSearchResults(results, getEventAge) {
|
|
1103
|
+
if (results.length === 0) {
|
|
1104
|
+
return {
|
|
1105
|
+
match: null,
|
|
1106
|
+
confidence: "none"
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
const scoredResults = results.map((result) => {
|
|
1110
|
+
const ageDays = getEventAge(result.eventId);
|
|
1111
|
+
const combinedScore = this.calculateCombinedScore(
|
|
1112
|
+
result.score,
|
|
1113
|
+
0,
|
|
1114
|
+
// FTS score - would need to be passed in
|
|
1115
|
+
ageDays,
|
|
1116
|
+
true
|
|
1117
|
+
// Assume active
|
|
1118
|
+
);
|
|
1119
|
+
return {
|
|
1120
|
+
result,
|
|
1121
|
+
combinedScore
|
|
1122
|
+
};
|
|
1123
|
+
});
|
|
1124
|
+
scoredResults.sort((a, b) => b.combinedScore - a.combinedScore);
|
|
1125
|
+
const topResult = scoredResults[0];
|
|
1126
|
+
const secondScore = scoredResults.length > 1 ? scoredResults[1].combinedScore : null;
|
|
1127
|
+
const confidence = this.classifyConfidence(topResult.combinedScore, secondScore);
|
|
1128
|
+
const match = {
|
|
1129
|
+
event: {
|
|
1130
|
+
id: topResult.result.eventId,
|
|
1131
|
+
eventType: topResult.result.eventType,
|
|
1132
|
+
sessionId: topResult.result.sessionId,
|
|
1133
|
+
timestamp: new Date(topResult.result.timestamp),
|
|
1134
|
+
content: topResult.result.content,
|
|
1135
|
+
canonicalKey: "",
|
|
1136
|
+
// Would need to be fetched
|
|
1137
|
+
dedupeKey: ""
|
|
1138
|
+
// Would need to be fetched
|
|
1139
|
+
},
|
|
1140
|
+
score: topResult.combinedScore
|
|
1141
|
+
};
|
|
1142
|
+
const gap = secondScore !== null ? topResult.combinedScore - secondScore : void 0;
|
|
1143
|
+
const alternatives = confidence === "suggested" ? scoredResults.slice(1, 4).map((sr) => ({
|
|
1144
|
+
event: {
|
|
1145
|
+
id: sr.result.eventId,
|
|
1146
|
+
eventType: sr.result.eventType,
|
|
1147
|
+
sessionId: sr.result.sessionId,
|
|
1148
|
+
timestamp: new Date(sr.result.timestamp),
|
|
1149
|
+
content: sr.result.content,
|
|
1150
|
+
canonicalKey: "",
|
|
1151
|
+
dedupeKey: ""
|
|
1152
|
+
},
|
|
1153
|
+
score: sr.combinedScore
|
|
1154
|
+
})) : void 0;
|
|
1155
|
+
return {
|
|
1156
|
+
match: confidence !== "none" ? match : null,
|
|
1157
|
+
confidence,
|
|
1158
|
+
gap,
|
|
1159
|
+
alternatives
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
/**
|
|
1163
|
+
* Calculate days between two dates
|
|
1164
|
+
*/
|
|
1165
|
+
static calculateAgeDays(timestamp) {
|
|
1166
|
+
const now = /* @__PURE__ */ new Date();
|
|
1167
|
+
const diffMs = now.getTime() - timestamp.getTime();
|
|
1168
|
+
return diffMs / (1e3 * 60 * 60 * 24);
|
|
1169
|
+
}
|
|
1170
|
+
/**
|
|
1171
|
+
* Get current configuration
|
|
1172
|
+
*/
|
|
1173
|
+
getConfig() {
|
|
1174
|
+
return { ...this.config };
|
|
1175
|
+
}
|
|
1176
|
+
};
|
|
1177
|
+
var defaultMatcher = null;
|
|
1178
|
+
function getDefaultMatcher() {
|
|
1179
|
+
if (!defaultMatcher) {
|
|
1180
|
+
defaultMatcher = new Matcher();
|
|
1181
|
+
}
|
|
1182
|
+
return defaultMatcher;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
// src/core/retriever.ts
|
|
1186
|
+
var DEFAULT_OPTIONS = {
|
|
1187
|
+
topK: 5,
|
|
1188
|
+
minScore: 0.7,
|
|
1189
|
+
maxTokens: 2e3,
|
|
1190
|
+
includeSessionContext: true
|
|
1191
|
+
};
|
|
1192
|
+
var Retriever = class {
|
|
1193
|
+
eventStore;
|
|
1194
|
+
vectorStore;
|
|
1195
|
+
embedder;
|
|
1196
|
+
matcher;
|
|
1197
|
+
sharedStore;
|
|
1198
|
+
sharedVectorStore;
|
|
1199
|
+
constructor(eventStore, vectorStore, embedder, matcher, sharedOptions) {
|
|
1200
|
+
this.eventStore = eventStore;
|
|
1201
|
+
this.vectorStore = vectorStore;
|
|
1202
|
+
this.embedder = embedder;
|
|
1203
|
+
this.matcher = matcher;
|
|
1204
|
+
this.sharedStore = sharedOptions?.sharedStore;
|
|
1205
|
+
this.sharedVectorStore = sharedOptions?.sharedVectorStore;
|
|
1206
|
+
}
|
|
1207
|
+
/**
|
|
1208
|
+
* Set shared stores after construction
|
|
1209
|
+
*/
|
|
1210
|
+
setSharedStores(sharedStore, sharedVectorStore) {
|
|
1211
|
+
this.sharedStore = sharedStore;
|
|
1212
|
+
this.sharedVectorStore = sharedVectorStore;
|
|
1213
|
+
}
|
|
1214
|
+
/**
|
|
1215
|
+
* Retrieve relevant memories for a query
|
|
1216
|
+
*/
|
|
1217
|
+
async retrieve(query, options = {}) {
|
|
1218
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
1219
|
+
const queryEmbedding = await this.embedder.embed(query);
|
|
1220
|
+
const searchResults = await this.vectorStore.search(queryEmbedding.vector, {
|
|
1221
|
+
limit: opts.topK * 2,
|
|
1222
|
+
// Get extra for filtering
|
|
1223
|
+
minScore: opts.minScore,
|
|
1224
|
+
sessionId: opts.sessionId
|
|
1225
|
+
});
|
|
1226
|
+
const matchResult = this.matcher.matchSearchResults(
|
|
1227
|
+
searchResults,
|
|
1228
|
+
(eventId) => this.getEventAgeDays(eventId)
|
|
1229
|
+
);
|
|
1230
|
+
const memories = await this.enrichResults(searchResults.slice(0, opts.topK), opts);
|
|
1231
|
+
const context = this.buildContext(memories, opts.maxTokens);
|
|
1232
|
+
return {
|
|
1233
|
+
memories,
|
|
1234
|
+
matchResult,
|
|
1235
|
+
totalTokens: this.estimateTokens(context),
|
|
1236
|
+
context
|
|
1237
|
+
};
|
|
1238
|
+
}
|
|
1239
|
+
/**
|
|
1240
|
+
* Retrieve with unified search (project + shared)
|
|
1241
|
+
*/
|
|
1242
|
+
async retrieveUnified(query, options = {}) {
|
|
1243
|
+
const projectResult = await this.retrieve(query, options);
|
|
1244
|
+
if (!options.includeShared || !this.sharedStore || !this.sharedVectorStore) {
|
|
1245
|
+
return projectResult;
|
|
1246
|
+
}
|
|
1247
|
+
try {
|
|
1248
|
+
const queryEmbedding = await this.embedder.embed(query);
|
|
1249
|
+
const sharedVectorResults = await this.sharedVectorStore.search(
|
|
1250
|
+
queryEmbedding.vector,
|
|
1251
|
+
{
|
|
1252
|
+
limit: options.topK || 5,
|
|
1253
|
+
minScore: options.minScore || 0.7,
|
|
1254
|
+
excludeProjectHash: options.projectHash
|
|
1255
|
+
}
|
|
1256
|
+
);
|
|
1257
|
+
const sharedMemories = [];
|
|
1258
|
+
for (const result of sharedVectorResults) {
|
|
1259
|
+
const entry = await this.sharedStore.get(result.entryId);
|
|
1260
|
+
if (entry) {
|
|
1261
|
+
if (!options.projectHash || entry.sourceProjectHash !== options.projectHash) {
|
|
1262
|
+
sharedMemories.push(entry);
|
|
1263
|
+
await this.sharedStore.recordUsage(entry.entryId);
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
const unifiedContext = this.buildUnifiedContext(projectResult, sharedMemories);
|
|
1268
|
+
return {
|
|
1269
|
+
...projectResult,
|
|
1270
|
+
context: unifiedContext,
|
|
1271
|
+
totalTokens: this.estimateTokens(unifiedContext),
|
|
1272
|
+
sharedMemories
|
|
1273
|
+
};
|
|
1274
|
+
} catch (error) {
|
|
1275
|
+
console.error("Shared search failed:", error);
|
|
1276
|
+
return projectResult;
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
/**
|
|
1280
|
+
* Build unified context combining project and shared memories
|
|
1281
|
+
*/
|
|
1282
|
+
buildUnifiedContext(projectResult, sharedMemories) {
|
|
1283
|
+
let context = projectResult.context;
|
|
1284
|
+
if (sharedMemories.length > 0) {
|
|
1285
|
+
context += "\n\n## Cross-Project Knowledge\n\n";
|
|
1286
|
+
for (const memory of sharedMemories.slice(0, 3)) {
|
|
1287
|
+
context += `### ${memory.title}
|
|
1288
|
+
`;
|
|
1289
|
+
if (memory.symptoms.length > 0) {
|
|
1290
|
+
context += `**Symptoms:** ${memory.symptoms.join(", ")}
|
|
1291
|
+
`;
|
|
1292
|
+
}
|
|
1293
|
+
context += `**Root Cause:** ${memory.rootCause}
|
|
1294
|
+
`;
|
|
1295
|
+
context += `**Solution:** ${memory.solution}
|
|
1296
|
+
`;
|
|
1297
|
+
if (memory.technologies && memory.technologies.length > 0) {
|
|
1298
|
+
context += `**Technologies:** ${memory.technologies.join(", ")}
|
|
1299
|
+
`;
|
|
1300
|
+
}
|
|
1301
|
+
context += `_Confidence: ${(memory.confidence * 100).toFixed(0)}%_
|
|
1302
|
+
|
|
1303
|
+
`;
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
return context;
|
|
1307
|
+
}
|
|
1308
|
+
/**
|
|
1309
|
+
* Retrieve memories from a specific session
|
|
1310
|
+
*/
|
|
1311
|
+
async retrieveFromSession(sessionId) {
|
|
1312
|
+
return this.eventStore.getSessionEvents(sessionId);
|
|
1313
|
+
}
|
|
1314
|
+
/**
|
|
1315
|
+
* Get recent memories across all sessions
|
|
1316
|
+
*/
|
|
1317
|
+
async retrieveRecent(limit = 100) {
|
|
1318
|
+
return this.eventStore.getRecentEvents(limit);
|
|
1319
|
+
}
|
|
1320
|
+
/**
|
|
1321
|
+
* Enrich search results with full event data
|
|
1322
|
+
*/
|
|
1323
|
+
async enrichResults(results, options) {
|
|
1324
|
+
const memories = [];
|
|
1325
|
+
for (const result of results) {
|
|
1326
|
+
const event = await this.eventStore.getEvent(result.eventId);
|
|
1327
|
+
if (!event)
|
|
1328
|
+
continue;
|
|
1329
|
+
let sessionContext;
|
|
1330
|
+
if (options.includeSessionContext) {
|
|
1331
|
+
sessionContext = await this.getSessionContext(event.sessionId, event.id);
|
|
1332
|
+
}
|
|
1333
|
+
memories.push({
|
|
1334
|
+
event,
|
|
1335
|
+
score: result.score,
|
|
1336
|
+
sessionContext
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
return memories;
|
|
1340
|
+
}
|
|
1341
|
+
/**
|
|
1342
|
+
* Get surrounding context from the same session
|
|
1343
|
+
*/
|
|
1344
|
+
async getSessionContext(sessionId, eventId) {
|
|
1345
|
+
const sessionEvents = await this.eventStore.getSessionEvents(sessionId);
|
|
1346
|
+
const eventIndex = sessionEvents.findIndex((e) => e.id === eventId);
|
|
1347
|
+
if (eventIndex === -1)
|
|
1348
|
+
return void 0;
|
|
1349
|
+
const start = Math.max(0, eventIndex - 1);
|
|
1350
|
+
const end = Math.min(sessionEvents.length, eventIndex + 2);
|
|
1351
|
+
const contextEvents = sessionEvents.slice(start, end);
|
|
1352
|
+
if (contextEvents.length <= 1)
|
|
1353
|
+
return void 0;
|
|
1354
|
+
return contextEvents.filter((e) => e.id !== eventId).map((e) => `[${e.eventType}]: ${e.content.slice(0, 200)}...`).join("\n");
|
|
1355
|
+
}
|
|
1356
|
+
/**
|
|
1357
|
+
* Build context string from memories (respecting token limit)
|
|
1358
|
+
*/
|
|
1359
|
+
buildContext(memories, maxTokens) {
|
|
1360
|
+
const parts = [];
|
|
1361
|
+
let currentTokens = 0;
|
|
1362
|
+
for (const memory of memories) {
|
|
1363
|
+
const memoryText = this.formatMemory(memory);
|
|
1364
|
+
const memoryTokens = this.estimateTokens(memoryText);
|
|
1365
|
+
if (currentTokens + memoryTokens > maxTokens) {
|
|
1366
|
+
break;
|
|
1367
|
+
}
|
|
1368
|
+
parts.push(memoryText);
|
|
1369
|
+
currentTokens += memoryTokens;
|
|
1370
|
+
}
|
|
1371
|
+
if (parts.length === 0) {
|
|
1372
|
+
return "";
|
|
1373
|
+
}
|
|
1374
|
+
return `## Relevant Memories
|
|
1375
|
+
|
|
1376
|
+
${parts.join("\n\n---\n\n")}`;
|
|
1377
|
+
}
|
|
1378
|
+
/**
|
|
1379
|
+
* Format a single memory for context
|
|
1380
|
+
*/
|
|
1381
|
+
formatMemory(memory) {
|
|
1382
|
+
const { event, score, sessionContext } = memory;
|
|
1383
|
+
const date = event.timestamp.toISOString().split("T")[0];
|
|
1384
|
+
let text = `**${event.eventType}** (${date}, score: ${score.toFixed(2)})
|
|
1385
|
+
${event.content}`;
|
|
1386
|
+
if (sessionContext) {
|
|
1387
|
+
text += `
|
|
1388
|
+
|
|
1389
|
+
_Context:_ ${sessionContext}`;
|
|
1390
|
+
}
|
|
1391
|
+
return text;
|
|
1392
|
+
}
|
|
1393
|
+
/**
|
|
1394
|
+
* Estimate token count (rough approximation)
|
|
1395
|
+
*/
|
|
1396
|
+
estimateTokens(text) {
|
|
1397
|
+
return Math.ceil(text.length / 4);
|
|
1398
|
+
}
|
|
1399
|
+
/**
|
|
1400
|
+
* Get event age in days (for recency scoring)
|
|
1401
|
+
*/
|
|
1402
|
+
getEventAgeDays(eventId) {
|
|
1403
|
+
return 0;
|
|
1404
|
+
}
|
|
1405
|
+
};
|
|
1406
|
+
function createRetriever(eventStore, vectorStore, embedder, matcher) {
|
|
1407
|
+
return new Retriever(eventStore, vectorStore, embedder, matcher);
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
// src/core/graduation.ts
|
|
1411
|
+
var DEFAULT_CRITERIA = {
|
|
1412
|
+
L0toL1: {
|
|
1413
|
+
minAccessCount: 1,
|
|
1414
|
+
minConfidence: 0.5,
|
|
1415
|
+
minCrossSessionRefs: 0,
|
|
1416
|
+
maxAgeDays: 30
|
|
1417
|
+
},
|
|
1418
|
+
L1toL2: {
|
|
1419
|
+
minAccessCount: 3,
|
|
1420
|
+
minConfidence: 0.7,
|
|
1421
|
+
minCrossSessionRefs: 1,
|
|
1422
|
+
maxAgeDays: 60
|
|
1423
|
+
},
|
|
1424
|
+
L2toL3: {
|
|
1425
|
+
minAccessCount: 5,
|
|
1426
|
+
minConfidence: 0.85,
|
|
1427
|
+
minCrossSessionRefs: 2,
|
|
1428
|
+
maxAgeDays: 90
|
|
1429
|
+
},
|
|
1430
|
+
L3toL4: {
|
|
1431
|
+
minAccessCount: 10,
|
|
1432
|
+
minConfidence: 0.92,
|
|
1433
|
+
minCrossSessionRefs: 3,
|
|
1434
|
+
maxAgeDays: 180
|
|
1435
|
+
}
|
|
1436
|
+
};
|
|
1437
|
+
var GraduationPipeline = class {
|
|
1438
|
+
eventStore;
|
|
1439
|
+
criteria;
|
|
1440
|
+
metrics = /* @__PURE__ */ new Map();
|
|
1441
|
+
constructor(eventStore, criteria = {}) {
|
|
1442
|
+
this.eventStore = eventStore;
|
|
1443
|
+
this.criteria = {
|
|
1444
|
+
L0toL1: { ...DEFAULT_CRITERIA.L0toL1, ...criteria.L0toL1 },
|
|
1445
|
+
L1toL2: { ...DEFAULT_CRITERIA.L1toL2, ...criteria.L1toL2 },
|
|
1446
|
+
L2toL3: { ...DEFAULT_CRITERIA.L2toL3, ...criteria.L2toL3 },
|
|
1447
|
+
L3toL4: { ...DEFAULT_CRITERIA.L3toL4, ...criteria.L3toL4 }
|
|
1448
|
+
};
|
|
1449
|
+
}
|
|
1450
|
+
/**
|
|
1451
|
+
* Record an access to an event (used for graduation scoring)
|
|
1452
|
+
*/
|
|
1453
|
+
recordAccess(eventId, fromSessionId, confidence = 1) {
|
|
1454
|
+
const existing = this.metrics.get(eventId);
|
|
1455
|
+
if (existing) {
|
|
1456
|
+
existing.accessCount++;
|
|
1457
|
+
existing.lastAccessed = /* @__PURE__ */ new Date();
|
|
1458
|
+
existing.confidence = Math.max(existing.confidence, confidence);
|
|
1459
|
+
} else {
|
|
1460
|
+
this.metrics.set(eventId, {
|
|
1461
|
+
eventId,
|
|
1462
|
+
accessCount: 1,
|
|
1463
|
+
lastAccessed: /* @__PURE__ */ new Date(),
|
|
1464
|
+
crossSessionRefs: 0,
|
|
1465
|
+
confidence
|
|
1466
|
+
});
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
/**
|
|
1470
|
+
* Evaluate if an event should graduate to the next level
|
|
1471
|
+
*/
|
|
1472
|
+
async evaluateGraduation(eventId, currentLevel) {
|
|
1473
|
+
const metrics = this.metrics.get(eventId);
|
|
1474
|
+
if (!metrics) {
|
|
1475
|
+
return {
|
|
1476
|
+
eventId,
|
|
1477
|
+
fromLevel: currentLevel,
|
|
1478
|
+
toLevel: currentLevel,
|
|
1479
|
+
success: false,
|
|
1480
|
+
reason: "No metrics available for event"
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
1483
|
+
const nextLevel = this.getNextLevel(currentLevel);
|
|
1484
|
+
if (!nextLevel) {
|
|
1485
|
+
return {
|
|
1486
|
+
eventId,
|
|
1487
|
+
fromLevel: currentLevel,
|
|
1488
|
+
toLevel: currentLevel,
|
|
1489
|
+
success: false,
|
|
1490
|
+
reason: "Already at maximum level"
|
|
1491
|
+
};
|
|
1492
|
+
}
|
|
1493
|
+
const criteria = this.getCriteria(currentLevel, nextLevel);
|
|
1494
|
+
const evaluation = this.checkCriteria(metrics, criteria);
|
|
1495
|
+
if (evaluation.passed) {
|
|
1496
|
+
await this.eventStore.updateMemoryLevel(eventId, nextLevel);
|
|
1497
|
+
return {
|
|
1498
|
+
eventId,
|
|
1499
|
+
fromLevel: currentLevel,
|
|
1500
|
+
toLevel: nextLevel,
|
|
1501
|
+
success: true
|
|
1502
|
+
};
|
|
1503
|
+
}
|
|
1504
|
+
return {
|
|
1505
|
+
eventId,
|
|
1506
|
+
fromLevel: currentLevel,
|
|
1507
|
+
toLevel: currentLevel,
|
|
1508
|
+
success: false,
|
|
1509
|
+
reason: evaluation.reason
|
|
1510
|
+
};
|
|
1511
|
+
}
|
|
1512
|
+
/**
|
|
1513
|
+
* Run graduation evaluation for all events at a given level
|
|
1514
|
+
*/
|
|
1515
|
+
async graduateBatch(level) {
|
|
1516
|
+
const results = [];
|
|
1517
|
+
for (const [eventId, metrics] of this.metrics) {
|
|
1518
|
+
const result = await this.evaluateGraduation(eventId, level);
|
|
1519
|
+
results.push(result);
|
|
1520
|
+
}
|
|
1521
|
+
return results;
|
|
1522
|
+
}
|
|
1523
|
+
/**
|
|
1524
|
+
* Extract insights from graduated events (L1+)
|
|
1525
|
+
*/
|
|
1526
|
+
extractInsights(events) {
|
|
1527
|
+
const insights = [];
|
|
1528
|
+
const patterns = this.detectPatterns(events);
|
|
1529
|
+
for (const pattern of patterns) {
|
|
1530
|
+
insights.push({
|
|
1531
|
+
id: crypto.randomUUID(),
|
|
1532
|
+
insightType: "pattern",
|
|
1533
|
+
content: pattern.description,
|
|
1534
|
+
canonicalKey: pattern.key,
|
|
1535
|
+
confidence: pattern.confidence,
|
|
1536
|
+
sourceEvents: pattern.eventIds,
|
|
1537
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
1538
|
+
lastUpdated: /* @__PURE__ */ new Date()
|
|
1539
|
+
});
|
|
1540
|
+
}
|
|
1541
|
+
const preferences = this.detectPreferences(events);
|
|
1542
|
+
for (const pref of preferences) {
|
|
1543
|
+
insights.push({
|
|
1544
|
+
id: crypto.randomUUID(),
|
|
1545
|
+
insightType: "preference",
|
|
1546
|
+
content: pref.description,
|
|
1547
|
+
canonicalKey: pref.key,
|
|
1548
|
+
confidence: pref.confidence,
|
|
1549
|
+
sourceEvents: pref.eventIds,
|
|
1550
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
1551
|
+
lastUpdated: /* @__PURE__ */ new Date()
|
|
1552
|
+
});
|
|
1553
|
+
}
|
|
1554
|
+
return insights;
|
|
1555
|
+
}
|
|
1556
|
+
/**
|
|
1557
|
+
* Get the next level in the graduation pipeline
|
|
1558
|
+
*/
|
|
1559
|
+
getNextLevel(current) {
|
|
1560
|
+
const levels = ["L0", "L1", "L2", "L3", "L4"];
|
|
1561
|
+
const currentIndex = levels.indexOf(current);
|
|
1562
|
+
if (currentIndex === -1 || currentIndex >= levels.length - 1) {
|
|
1563
|
+
return null;
|
|
1564
|
+
}
|
|
1565
|
+
return levels[currentIndex + 1];
|
|
1566
|
+
}
|
|
1567
|
+
/**
|
|
1568
|
+
* Get criteria for level transition
|
|
1569
|
+
*/
|
|
1570
|
+
getCriteria(from, to) {
|
|
1571
|
+
const key = `${from}to${to}`;
|
|
1572
|
+
return this.criteria[key] || DEFAULT_CRITERIA.L0toL1;
|
|
1573
|
+
}
|
|
1574
|
+
/**
|
|
1575
|
+
* Check if metrics meet criteria
|
|
1576
|
+
*/
|
|
1577
|
+
checkCriteria(metrics, criteria) {
|
|
1578
|
+
if (metrics.accessCount < criteria.minAccessCount) {
|
|
1579
|
+
return {
|
|
1580
|
+
passed: false,
|
|
1581
|
+
reason: `Access count ${metrics.accessCount} < ${criteria.minAccessCount}`
|
|
1582
|
+
};
|
|
1583
|
+
}
|
|
1584
|
+
if (metrics.confidence < criteria.minConfidence) {
|
|
1585
|
+
return {
|
|
1586
|
+
passed: false,
|
|
1587
|
+
reason: `Confidence ${metrics.confidence} < ${criteria.minConfidence}`
|
|
1588
|
+
};
|
|
1589
|
+
}
|
|
1590
|
+
if (metrics.crossSessionRefs < criteria.minCrossSessionRefs) {
|
|
1591
|
+
return {
|
|
1592
|
+
passed: false,
|
|
1593
|
+
reason: `Cross-session refs ${metrics.crossSessionRefs} < ${criteria.minCrossSessionRefs}`
|
|
1594
|
+
};
|
|
1595
|
+
}
|
|
1596
|
+
const ageDays = (Date.now() - metrics.lastAccessed.getTime()) / (1e3 * 60 * 60 * 24);
|
|
1597
|
+
if (ageDays > criteria.maxAgeDays) {
|
|
1598
|
+
return {
|
|
1599
|
+
passed: false,
|
|
1600
|
+
reason: `Event too old: ${ageDays.toFixed(1)} days > ${criteria.maxAgeDays}`
|
|
1601
|
+
};
|
|
1602
|
+
}
|
|
1603
|
+
return { passed: true };
|
|
1604
|
+
}
|
|
1605
|
+
/**
|
|
1606
|
+
* Detect patterns in events
|
|
1607
|
+
*/
|
|
1608
|
+
detectPatterns(events) {
|
|
1609
|
+
const keyGroups = /* @__PURE__ */ new Map();
|
|
1610
|
+
for (const event of events) {
|
|
1611
|
+
const existing = keyGroups.get(event.canonicalKey) || [];
|
|
1612
|
+
existing.push(event);
|
|
1613
|
+
keyGroups.set(event.canonicalKey, existing);
|
|
1614
|
+
}
|
|
1615
|
+
const patterns = [];
|
|
1616
|
+
for (const [key, groupEvents] of keyGroups) {
|
|
1617
|
+
if (groupEvents.length >= 2) {
|
|
1618
|
+
patterns.push({
|
|
1619
|
+
key,
|
|
1620
|
+
description: `Repeated topic: ${key.slice(0, 50)}`,
|
|
1621
|
+
confidence: Math.min(1, groupEvents.length / 5),
|
|
1622
|
+
eventIds: groupEvents.map((e) => e.id)
|
|
1623
|
+
});
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
return patterns;
|
|
1627
|
+
}
|
|
1628
|
+
/**
|
|
1629
|
+
* Detect user preferences from events
|
|
1630
|
+
*/
|
|
1631
|
+
detectPreferences(events) {
|
|
1632
|
+
const preferenceKeywords = ["prefer", "like", "want", "always", "never", "favorite"];
|
|
1633
|
+
const preferences = [];
|
|
1634
|
+
for (const event of events) {
|
|
1635
|
+
if (event.eventType !== "user_prompt")
|
|
1636
|
+
continue;
|
|
1637
|
+
const lowerContent = event.content.toLowerCase();
|
|
1638
|
+
for (const keyword of preferenceKeywords) {
|
|
1639
|
+
if (lowerContent.includes(keyword)) {
|
|
1640
|
+
preferences.push({
|
|
1641
|
+
key: `preference_${keyword}_${event.id.slice(0, 8)}`,
|
|
1642
|
+
description: `User preference: ${event.content.slice(0, 100)}`,
|
|
1643
|
+
confidence: 0.7,
|
|
1644
|
+
eventIds: [event.id]
|
|
1645
|
+
});
|
|
1646
|
+
break;
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
return preferences;
|
|
1651
|
+
}
|
|
1652
|
+
/**
|
|
1653
|
+
* Get graduation statistics
|
|
1654
|
+
*/
|
|
1655
|
+
async getStats() {
|
|
1656
|
+
return this.eventStore.getLevelStats();
|
|
1657
|
+
}
|
|
1658
|
+
};
|
|
1659
|
+
function createGraduationPipeline(eventStore) {
|
|
1660
|
+
return new GraduationPipeline(eventStore);
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
// src/core/shared-event-store.ts
|
|
1664
|
+
var SharedEventStore = class {
|
|
1665
|
+
constructor(dbPath) {
|
|
1666
|
+
this.dbPath = dbPath;
|
|
1667
|
+
this.db = createDatabase(dbPath);
|
|
1668
|
+
}
|
|
1669
|
+
db;
|
|
1670
|
+
initialized = false;
|
|
1671
|
+
async initialize() {
|
|
1672
|
+
if (this.initialized)
|
|
1673
|
+
return;
|
|
1674
|
+
await dbRun(this.db, `
|
|
1675
|
+
CREATE TABLE IF NOT EXISTS shared_troubleshooting (
|
|
1676
|
+
entry_id VARCHAR PRIMARY KEY,
|
|
1677
|
+
source_project_hash VARCHAR NOT NULL,
|
|
1678
|
+
source_entry_id VARCHAR NOT NULL,
|
|
1679
|
+
title VARCHAR NOT NULL,
|
|
1680
|
+
symptoms JSON NOT NULL,
|
|
1681
|
+
root_cause TEXT NOT NULL,
|
|
1682
|
+
solution TEXT NOT NULL,
|
|
1683
|
+
topics JSON NOT NULL,
|
|
1684
|
+
technologies JSON,
|
|
1685
|
+
confidence REAL NOT NULL DEFAULT 0.8,
|
|
1686
|
+
usage_count INTEGER DEFAULT 0,
|
|
1687
|
+
last_used_at TIMESTAMP,
|
|
1688
|
+
promoted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
1689
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
1690
|
+
UNIQUE(source_project_hash, source_entry_id)
|
|
1691
|
+
)
|
|
1692
|
+
`);
|
|
1693
|
+
await dbRun(this.db, `
|
|
1694
|
+
CREATE TABLE IF NOT EXISTS shared_best_practices (
|
|
1695
|
+
entry_id VARCHAR PRIMARY KEY,
|
|
1696
|
+
source_project_hash VARCHAR NOT NULL,
|
|
1697
|
+
source_entry_id VARCHAR NOT NULL,
|
|
1698
|
+
title VARCHAR NOT NULL,
|
|
1699
|
+
content_json JSON NOT NULL,
|
|
1700
|
+
topics JSON NOT NULL,
|
|
1701
|
+
confidence REAL NOT NULL DEFAULT 0.8,
|
|
1702
|
+
usage_count INTEGER DEFAULT 0,
|
|
1703
|
+
promoted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
1704
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
1705
|
+
UNIQUE(source_project_hash, source_entry_id)
|
|
1706
|
+
)
|
|
1707
|
+
`);
|
|
1708
|
+
await dbRun(this.db, `
|
|
1709
|
+
CREATE TABLE IF NOT EXISTS shared_common_errors (
|
|
1710
|
+
entry_id VARCHAR PRIMARY KEY,
|
|
1711
|
+
source_project_hash VARCHAR NOT NULL,
|
|
1712
|
+
source_entry_id VARCHAR NOT NULL,
|
|
1713
|
+
title VARCHAR NOT NULL,
|
|
1714
|
+
error_pattern TEXT NOT NULL,
|
|
1715
|
+
solution TEXT NOT NULL,
|
|
1716
|
+
topics JSON NOT NULL,
|
|
1717
|
+
technologies JSON,
|
|
1718
|
+
confidence REAL NOT NULL DEFAULT 0.8,
|
|
1719
|
+
usage_count INTEGER DEFAULT 0,
|
|
1720
|
+
promoted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
1721
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
1722
|
+
UNIQUE(source_project_hash, source_entry_id)
|
|
1723
|
+
)
|
|
1724
|
+
`);
|
|
1725
|
+
await dbRun(this.db, `
|
|
1726
|
+
CREATE INDEX IF NOT EXISTS idx_shared_ts_confidence
|
|
1727
|
+
ON shared_troubleshooting(confidence DESC)
|
|
1728
|
+
`);
|
|
1729
|
+
await dbRun(this.db, `
|
|
1730
|
+
CREATE INDEX IF NOT EXISTS idx_shared_ts_usage
|
|
1731
|
+
ON shared_troubleshooting(usage_count DESC)
|
|
1732
|
+
`);
|
|
1733
|
+
await dbRun(this.db, `
|
|
1734
|
+
CREATE INDEX IF NOT EXISTS idx_shared_ts_source
|
|
1735
|
+
ON shared_troubleshooting(source_project_hash)
|
|
1736
|
+
`);
|
|
1737
|
+
this.initialized = true;
|
|
1738
|
+
}
|
|
1739
|
+
getDatabase() {
|
|
1740
|
+
return this.db;
|
|
1741
|
+
}
|
|
1742
|
+
isInitialized() {
|
|
1743
|
+
return this.initialized;
|
|
1744
|
+
}
|
|
1745
|
+
async close() {
|
|
1746
|
+
await dbClose(this.db);
|
|
1747
|
+
this.initialized = false;
|
|
1748
|
+
}
|
|
1749
|
+
};
|
|
1750
|
+
function createSharedEventStore(dbPath) {
|
|
1751
|
+
return new SharedEventStore(dbPath);
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
// src/core/shared-store.ts
|
|
1755
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1756
|
+
var SharedStore = class {
|
|
1757
|
+
constructor(sharedEventStore) {
|
|
1758
|
+
this.sharedEventStore = sharedEventStore;
|
|
1759
|
+
}
|
|
1760
|
+
get db() {
|
|
1761
|
+
return this.sharedEventStore.getDatabase();
|
|
1762
|
+
}
|
|
1763
|
+
/**
|
|
1764
|
+
* Promote a verified troubleshooting entry to shared storage
|
|
1765
|
+
*/
|
|
1766
|
+
async promoteEntry(input) {
|
|
1767
|
+
const entryId = randomUUID2();
|
|
1768
|
+
await dbRun(
|
|
1769
|
+
this.db,
|
|
1770
|
+
`INSERT INTO shared_troubleshooting (
|
|
1771
|
+
entry_id, source_project_hash, source_entry_id,
|
|
1772
|
+
title, symptoms, root_cause, solution, topics,
|
|
1773
|
+
technologies, confidence, promoted_at
|
|
1774
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
|
1775
|
+
ON CONFLICT (source_project_hash, source_entry_id)
|
|
1776
|
+
DO UPDATE SET
|
|
1777
|
+
title = excluded.title,
|
|
1778
|
+
symptoms = excluded.symptoms,
|
|
1779
|
+
root_cause = excluded.root_cause,
|
|
1780
|
+
solution = excluded.solution,
|
|
1781
|
+
topics = excluded.topics,
|
|
1782
|
+
technologies = excluded.technologies,
|
|
1783
|
+
confidence = CASE
|
|
1784
|
+
WHEN excluded.confidence > shared_troubleshooting.confidence
|
|
1785
|
+
THEN excluded.confidence
|
|
1786
|
+
ELSE shared_troubleshooting.confidence
|
|
1787
|
+
END`,
|
|
1788
|
+
[
|
|
1789
|
+
entryId,
|
|
1790
|
+
input.sourceProjectHash,
|
|
1791
|
+
input.sourceEntryId,
|
|
1792
|
+
input.title,
|
|
1793
|
+
JSON.stringify(input.symptoms),
|
|
1794
|
+
input.rootCause,
|
|
1795
|
+
input.solution,
|
|
1796
|
+
JSON.stringify(input.topics),
|
|
1797
|
+
JSON.stringify(input.technologies || []),
|
|
1798
|
+
input.confidence
|
|
1799
|
+
]
|
|
1800
|
+
);
|
|
1801
|
+
return entryId;
|
|
1802
|
+
}
|
|
1803
|
+
/**
|
|
1804
|
+
* Search troubleshooting entries by text query
|
|
1805
|
+
*/
|
|
1806
|
+
async search(query, options) {
|
|
1807
|
+
const topK = options?.topK || 5;
|
|
1808
|
+
const minConfidence = options?.minConfidence || 0.5;
|
|
1809
|
+
const searchPattern = `%${query}%`;
|
|
1810
|
+
const rows = await dbAll(
|
|
1811
|
+
this.db,
|
|
1812
|
+
`SELECT * FROM shared_troubleshooting
|
|
1813
|
+
WHERE (title LIKE ? OR root_cause LIKE ? OR solution LIKE ?)
|
|
1814
|
+
AND confidence >= ?
|
|
1815
|
+
ORDER BY confidence DESC, usage_count DESC
|
|
1816
|
+
LIMIT ?`,
|
|
1817
|
+
[searchPattern, searchPattern, searchPattern, minConfidence, topK]
|
|
1818
|
+
);
|
|
1819
|
+
return rows.map(this.rowToEntry);
|
|
1820
|
+
}
|
|
1821
|
+
/**
|
|
1822
|
+
* Search by topics
|
|
1823
|
+
*/
|
|
1824
|
+
async searchByTopics(topics, options) {
|
|
1825
|
+
const topK = options?.topK || 5;
|
|
1826
|
+
if (topics.length === 0) {
|
|
1827
|
+
return [];
|
|
1828
|
+
}
|
|
1829
|
+
const topicConditions = topics.map(() => `topics LIKE ?`).join(" OR ");
|
|
1830
|
+
const topicParams = topics.map((t) => `%"${t}"%`);
|
|
1831
|
+
let query = `SELECT * FROM shared_troubleshooting WHERE (${topicConditions})`;
|
|
1832
|
+
const params = [...topicParams];
|
|
1833
|
+
if (options?.excludeProjectHash) {
|
|
1834
|
+
query += ` AND source_project_hash != ?`;
|
|
1835
|
+
params.push(options.excludeProjectHash);
|
|
1836
|
+
}
|
|
1837
|
+
query += ` ORDER BY confidence DESC, usage_count DESC LIMIT ?`;
|
|
1838
|
+
params.push(topK);
|
|
1839
|
+
const rows = await dbAll(this.db, query, params);
|
|
1840
|
+
return rows.map(this.rowToEntry);
|
|
1841
|
+
}
|
|
1842
|
+
/**
|
|
1843
|
+
* Record usage of a shared entry (for ranking)
|
|
1844
|
+
*/
|
|
1845
|
+
async recordUsage(entryId) {
|
|
1846
|
+
await dbRun(
|
|
1847
|
+
this.db,
|
|
1848
|
+
`UPDATE shared_troubleshooting
|
|
1849
|
+
SET usage_count = usage_count + 1,
|
|
1850
|
+
last_used_at = CURRENT_TIMESTAMP
|
|
1851
|
+
WHERE entry_id = ?`,
|
|
1852
|
+
[entryId]
|
|
1853
|
+
);
|
|
1854
|
+
}
|
|
1855
|
+
/**
|
|
1856
|
+
* Get entry by ID
|
|
1857
|
+
*/
|
|
1858
|
+
async get(entryId) {
|
|
1859
|
+
const rows = await dbAll(
|
|
1860
|
+
this.db,
|
|
1861
|
+
`SELECT * FROM shared_troubleshooting WHERE entry_id = ?`,
|
|
1862
|
+
[entryId]
|
|
1863
|
+
);
|
|
1864
|
+
if (rows.length === 0)
|
|
1865
|
+
return null;
|
|
1866
|
+
return this.rowToEntry(rows[0]);
|
|
1867
|
+
}
|
|
1868
|
+
/**
|
|
1869
|
+
* Get entry by source (project hash + entry ID)
|
|
1870
|
+
*/
|
|
1871
|
+
async getBySource(projectHash, sourceEntryId) {
|
|
1872
|
+
const rows = await dbAll(
|
|
1873
|
+
this.db,
|
|
1874
|
+
`SELECT * FROM shared_troubleshooting
|
|
1875
|
+
WHERE source_project_hash = ? AND source_entry_id = ?`,
|
|
1876
|
+
[projectHash, sourceEntryId]
|
|
1877
|
+
);
|
|
1878
|
+
if (rows.length === 0)
|
|
1879
|
+
return null;
|
|
1880
|
+
return this.rowToEntry(rows[0]);
|
|
1881
|
+
}
|
|
1882
|
+
/**
|
|
1883
|
+
* Check if an entry already exists in shared store
|
|
1884
|
+
*/
|
|
1885
|
+
async exists(projectHash, sourceEntryId) {
|
|
1886
|
+
const result = await dbAll(
|
|
1887
|
+
this.db,
|
|
1888
|
+
`SELECT COUNT(*) as count FROM shared_troubleshooting
|
|
1889
|
+
WHERE source_project_hash = ? AND source_entry_id = ?`,
|
|
1890
|
+
[projectHash, sourceEntryId]
|
|
1891
|
+
);
|
|
1892
|
+
return (result[0]?.count || 0) > 0;
|
|
1893
|
+
}
|
|
1894
|
+
/**
|
|
1895
|
+
* Get all entries (with limit for safety)
|
|
1896
|
+
*/
|
|
1897
|
+
async getAll(options) {
|
|
1898
|
+
const limit = options?.limit || 100;
|
|
1899
|
+
const rows = await dbAll(
|
|
1900
|
+
this.db,
|
|
1901
|
+
`SELECT * FROM shared_troubleshooting
|
|
1902
|
+
ORDER BY confidence DESC, usage_count DESC
|
|
1903
|
+
LIMIT ?`,
|
|
1904
|
+
[limit]
|
|
1905
|
+
);
|
|
1906
|
+
return rows.map(this.rowToEntry);
|
|
1907
|
+
}
|
|
1908
|
+
/**
|
|
1909
|
+
* Get total count
|
|
1910
|
+
*/
|
|
1911
|
+
async count() {
|
|
1912
|
+
const result = await dbAll(
|
|
1913
|
+
this.db,
|
|
1914
|
+
`SELECT COUNT(*) as count FROM shared_troubleshooting`
|
|
1915
|
+
);
|
|
1916
|
+
return result[0]?.count || 0;
|
|
1917
|
+
}
|
|
1918
|
+
/**
|
|
1919
|
+
* Get statistics
|
|
1920
|
+
*/
|
|
1921
|
+
async getStats() {
|
|
1922
|
+
const countResult = await dbAll(
|
|
1923
|
+
this.db,
|
|
1924
|
+
`SELECT COUNT(*) as count FROM shared_troubleshooting`
|
|
1925
|
+
);
|
|
1926
|
+
const total = countResult[0]?.count || 0;
|
|
1927
|
+
const avgResult = await dbAll(
|
|
1928
|
+
this.db,
|
|
1929
|
+
`SELECT AVG(confidence) as avg FROM shared_troubleshooting`
|
|
1930
|
+
);
|
|
1931
|
+
const averageConfidence = avgResult[0]?.avg || 0;
|
|
1932
|
+
const usageResult = await dbAll(
|
|
1933
|
+
this.db,
|
|
1934
|
+
`SELECT SUM(usage_count) as total FROM shared_troubleshooting`
|
|
1935
|
+
);
|
|
1936
|
+
const totalUsageCount = usageResult[0]?.total || 0;
|
|
1937
|
+
const entries = await this.getAll({ limit: 1e3 });
|
|
1938
|
+
const topicCounts = {};
|
|
1939
|
+
for (const entry of entries) {
|
|
1940
|
+
for (const topic of entry.topics) {
|
|
1941
|
+
topicCounts[topic] = (topicCounts[topic] || 0) + 1;
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
const topTopics = Object.entries(topicCounts).map(([topic, count]) => ({ topic, count })).sort((a, b) => b.count - a.count).slice(0, 10);
|
|
1945
|
+
return { total, averageConfidence, topTopics, totalUsageCount };
|
|
1946
|
+
}
|
|
1947
|
+
/**
|
|
1948
|
+
* Delete an entry
|
|
1949
|
+
*/
|
|
1950
|
+
async delete(entryId) {
|
|
1951
|
+
const before = await this.count();
|
|
1952
|
+
await dbRun(
|
|
1953
|
+
this.db,
|
|
1954
|
+
`DELETE FROM shared_troubleshooting WHERE entry_id = ?`,
|
|
1955
|
+
[entryId]
|
|
1956
|
+
);
|
|
1957
|
+
const after = await this.count();
|
|
1958
|
+
return before > after;
|
|
1959
|
+
}
|
|
1960
|
+
rowToEntry(row) {
|
|
1961
|
+
return {
|
|
1962
|
+
entryId: row.entry_id,
|
|
1963
|
+
sourceProjectHash: row.source_project_hash,
|
|
1964
|
+
sourceEntryId: row.source_entry_id,
|
|
1965
|
+
title: row.title,
|
|
1966
|
+
symptoms: JSON.parse(row.symptoms || "[]"),
|
|
1967
|
+
rootCause: row.root_cause,
|
|
1968
|
+
solution: row.solution,
|
|
1969
|
+
topics: JSON.parse(row.topics || "[]"),
|
|
1970
|
+
technologies: JSON.parse(row.technologies || "[]"),
|
|
1971
|
+
confidence: row.confidence,
|
|
1972
|
+
usageCount: row.usage_count || 0,
|
|
1973
|
+
lastUsedAt: row.last_used_at ? toDate(row.last_used_at) : void 0,
|
|
1974
|
+
promotedAt: toDate(row.promoted_at),
|
|
1975
|
+
createdAt: toDate(row.created_at)
|
|
1976
|
+
};
|
|
1977
|
+
}
|
|
1978
|
+
};
|
|
1979
|
+
function createSharedStore(sharedEventStore) {
|
|
1980
|
+
return new SharedStore(sharedEventStore);
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
// src/core/shared-vector-store.ts
|
|
1984
|
+
import * as lancedb2 from "@lancedb/lancedb";
|
|
1985
|
+
var SharedVectorStore = class {
|
|
1986
|
+
constructor(dbPath) {
|
|
1987
|
+
this.dbPath = dbPath;
|
|
1988
|
+
}
|
|
1989
|
+
db = null;
|
|
1990
|
+
table = null;
|
|
1991
|
+
tableName = "shared_knowledge";
|
|
1992
|
+
/**
|
|
1993
|
+
* Initialize LanceDB connection
|
|
1994
|
+
*/
|
|
1995
|
+
async initialize() {
|
|
1996
|
+
if (this.db)
|
|
1997
|
+
return;
|
|
1998
|
+
this.db = await lancedb2.connect(this.dbPath);
|
|
1999
|
+
try {
|
|
2000
|
+
const tables = await this.db.tableNames();
|
|
2001
|
+
if (tables.includes(this.tableName)) {
|
|
2002
|
+
this.table = await this.db.openTable(this.tableName);
|
|
2003
|
+
}
|
|
2004
|
+
} catch {
|
|
2005
|
+
this.table = null;
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
/**
|
|
2009
|
+
* Add or update a shared vector record
|
|
2010
|
+
*/
|
|
2011
|
+
async upsert(record) {
|
|
2012
|
+
await this.initialize();
|
|
2013
|
+
if (!this.db) {
|
|
2014
|
+
throw new Error("Database not initialized");
|
|
2015
|
+
}
|
|
2016
|
+
const data = {
|
|
2017
|
+
id: record.id,
|
|
2018
|
+
entryId: record.entryId,
|
|
2019
|
+
entryType: record.entryType,
|
|
2020
|
+
content: record.content,
|
|
2021
|
+
vector: record.vector,
|
|
2022
|
+
topics: JSON.stringify(record.topics),
|
|
2023
|
+
sourceProjectHash: record.sourceProjectHash || ""
|
|
2024
|
+
};
|
|
2025
|
+
if (!this.table) {
|
|
2026
|
+
this.table = await this.db.createTable(this.tableName, [data]);
|
|
2027
|
+
} else {
|
|
2028
|
+
try {
|
|
2029
|
+
await this.table.delete(`entryId = '${record.entryId}'`);
|
|
2030
|
+
} catch {
|
|
2031
|
+
}
|
|
2032
|
+
await this.table.add([data]);
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
/**
|
|
2036
|
+
* Add multiple records in batch
|
|
2037
|
+
*/
|
|
2038
|
+
async upsertBatch(records) {
|
|
2039
|
+
if (records.length === 0)
|
|
2040
|
+
return;
|
|
2041
|
+
await this.initialize();
|
|
2042
|
+
if (!this.db) {
|
|
2043
|
+
throw new Error("Database not initialized");
|
|
2044
|
+
}
|
|
2045
|
+
const data = records.map((record) => ({
|
|
2046
|
+
id: record.id,
|
|
2047
|
+
entryId: record.entryId,
|
|
2048
|
+
entryType: record.entryType,
|
|
2049
|
+
content: record.content,
|
|
2050
|
+
vector: record.vector,
|
|
2051
|
+
topics: JSON.stringify(record.topics),
|
|
2052
|
+
sourceProjectHash: record.sourceProjectHash || ""
|
|
2053
|
+
}));
|
|
2054
|
+
if (!this.table) {
|
|
2055
|
+
this.table = await this.db.createTable(this.tableName, data);
|
|
2056
|
+
} else {
|
|
2057
|
+
await this.table.add(data);
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
/**
|
|
2061
|
+
* Search for similar vectors
|
|
2062
|
+
*/
|
|
2063
|
+
async search(queryVector, options = {}) {
|
|
2064
|
+
await this.initialize();
|
|
2065
|
+
if (!this.table) {
|
|
2066
|
+
return [];
|
|
2067
|
+
}
|
|
2068
|
+
const { limit = 5, minScore = 0.7, excludeProjectHash, entryType } = options;
|
|
2069
|
+
let query = this.table.search(queryVector).distanceType("cosine").limit(limit * 2);
|
|
2070
|
+
const filters = [];
|
|
2071
|
+
if (excludeProjectHash) {
|
|
2072
|
+
filters.push(`sourceProjectHash != '${excludeProjectHash}'`);
|
|
2073
|
+
}
|
|
2074
|
+
if (entryType) {
|
|
2075
|
+
filters.push(`entryType = '${entryType}'`);
|
|
2076
|
+
}
|
|
2077
|
+
if (filters.length > 0) {
|
|
2078
|
+
query = query.where(filters.join(" AND "));
|
|
2079
|
+
}
|
|
2080
|
+
const results = await query.toArray();
|
|
2081
|
+
return results.filter((r) => {
|
|
2082
|
+
const distance = r._distance || 0;
|
|
2083
|
+
const score = 1 - distance / 2;
|
|
2084
|
+
return score >= minScore;
|
|
2085
|
+
}).slice(0, limit).map((r) => {
|
|
2086
|
+
const distance = r._distance || 0;
|
|
2087
|
+
const score = 1 - distance / 2;
|
|
2088
|
+
return {
|
|
2089
|
+
id: r.id,
|
|
2090
|
+
entryId: r.entryId,
|
|
2091
|
+
content: r.content,
|
|
2092
|
+
score,
|
|
2093
|
+
entryType: r.entryType
|
|
2094
|
+
};
|
|
2095
|
+
});
|
|
2096
|
+
}
|
|
2097
|
+
/**
|
|
2098
|
+
* Delete vector by entry ID
|
|
2099
|
+
*/
|
|
2100
|
+
async delete(entryId) {
|
|
2101
|
+
if (!this.table)
|
|
2102
|
+
return;
|
|
2103
|
+
await this.table.delete(`entryId = '${entryId}'`);
|
|
2104
|
+
}
|
|
2105
|
+
/**
|
|
2106
|
+
* Get total count
|
|
2107
|
+
*/
|
|
2108
|
+
async count() {
|
|
2109
|
+
if (!this.table)
|
|
2110
|
+
return 0;
|
|
2111
|
+
return this.table.countRows();
|
|
2112
|
+
}
|
|
2113
|
+
/**
|
|
2114
|
+
* Check if vector exists for entry
|
|
2115
|
+
*/
|
|
2116
|
+
async exists(entryId) {
|
|
2117
|
+
if (!this.table)
|
|
2118
|
+
return false;
|
|
2119
|
+
try {
|
|
2120
|
+
const results = await this.table.search([]).where(`entryId = '${entryId}'`).limit(1).toArray();
|
|
2121
|
+
return results.length > 0;
|
|
2122
|
+
} catch {
|
|
2123
|
+
return false;
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
};
|
|
2127
|
+
function createSharedVectorStore(dbPath) {
|
|
2128
|
+
return new SharedVectorStore(dbPath);
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
// src/core/shared-promoter.ts
|
|
2132
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
2133
|
+
var SharedPromoter = class {
|
|
2134
|
+
constructor(sharedStore, sharedVectorStore, embedder, config) {
|
|
2135
|
+
this.sharedStore = sharedStore;
|
|
2136
|
+
this.sharedVectorStore = sharedVectorStore;
|
|
2137
|
+
this.embedder = embedder;
|
|
2138
|
+
this.config = config;
|
|
2139
|
+
}
|
|
2140
|
+
/**
|
|
2141
|
+
* Check if an entry is eligible for promotion
|
|
2142
|
+
*/
|
|
2143
|
+
isEligibleForPromotion(entry) {
|
|
2144
|
+
if (entry.entryType !== "troubleshooting") {
|
|
2145
|
+
return false;
|
|
2146
|
+
}
|
|
2147
|
+
if (entry.stage !== "verified" && entry.stage !== "certified") {
|
|
2148
|
+
return false;
|
|
2149
|
+
}
|
|
2150
|
+
if (entry.status !== "active") {
|
|
2151
|
+
return false;
|
|
2152
|
+
}
|
|
2153
|
+
return true;
|
|
2154
|
+
}
|
|
2155
|
+
/**
|
|
2156
|
+
* Promote a verified troubleshooting entry to shared storage
|
|
2157
|
+
*/
|
|
2158
|
+
async promoteEntry(entry, projectHash) {
|
|
2159
|
+
if (!this.isEligibleForPromotion(entry)) {
|
|
2160
|
+
return {
|
|
2161
|
+
success: false,
|
|
2162
|
+
skipped: true,
|
|
2163
|
+
skipReason: `Entry not eligible: type=${entry.entryType}, stage=${entry.stage}, status=${entry.status}`
|
|
2164
|
+
};
|
|
2165
|
+
}
|
|
2166
|
+
const exists = await this.sharedStore.exists(projectHash, entry.entryId);
|
|
2167
|
+
if (exists) {
|
|
2168
|
+
return {
|
|
2169
|
+
success: true,
|
|
2170
|
+
skipped: true,
|
|
2171
|
+
skipReason: "Entry already exists in shared store"
|
|
2172
|
+
};
|
|
2173
|
+
}
|
|
2174
|
+
try {
|
|
2175
|
+
const content = entry.contentJson;
|
|
2176
|
+
const confidence = this.calculateConfidence(entry);
|
|
2177
|
+
const minConfidence = this.config?.minConfidenceForPromotion ?? 0.8;
|
|
2178
|
+
if (confidence < minConfidence) {
|
|
2179
|
+
return {
|
|
2180
|
+
success: false,
|
|
2181
|
+
skipped: true,
|
|
2182
|
+
skipReason: `Confidence ${confidence} below threshold ${minConfidence}`
|
|
2183
|
+
};
|
|
2184
|
+
}
|
|
2185
|
+
const input = {
|
|
2186
|
+
sourceProjectHash: projectHash,
|
|
2187
|
+
sourceEntryId: entry.entryId,
|
|
2188
|
+
title: entry.title,
|
|
2189
|
+
symptoms: content.symptoms || [],
|
|
2190
|
+
rootCause: content.rootCause || "",
|
|
2191
|
+
solution: content.solution || "",
|
|
2192
|
+
topics: this.extractTopics(entry),
|
|
2193
|
+
technologies: content.technologies || [],
|
|
2194
|
+
confidence
|
|
2195
|
+
};
|
|
2196
|
+
const entryId = await this.sharedStore.promoteEntry(input);
|
|
2197
|
+
const embeddingContent = this.createEmbeddingContent(input);
|
|
2198
|
+
const embedding = await this.embedder.embed(embeddingContent);
|
|
2199
|
+
await this.sharedVectorStore.upsert({
|
|
2200
|
+
id: randomUUID3(),
|
|
2201
|
+
entryId,
|
|
2202
|
+
entryType: "troubleshooting",
|
|
2203
|
+
content: embeddingContent,
|
|
2204
|
+
vector: embedding.vector,
|
|
2205
|
+
topics: input.topics,
|
|
2206
|
+
sourceProjectHash: projectHash
|
|
2207
|
+
});
|
|
2208
|
+
return {
|
|
2209
|
+
success: true,
|
|
2210
|
+
entryId
|
|
2211
|
+
};
|
|
2212
|
+
} catch (error) {
|
|
2213
|
+
return {
|
|
2214
|
+
success: false,
|
|
2215
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2216
|
+
};
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
/**
|
|
2220
|
+
* Batch promote multiple entries
|
|
2221
|
+
*/
|
|
2222
|
+
async promoteEntries(entries, projectHash) {
|
|
2223
|
+
const results = /* @__PURE__ */ new Map();
|
|
2224
|
+
for (const entry of entries) {
|
|
2225
|
+
const result = await this.promoteEntry(entry, projectHash);
|
|
2226
|
+
results.set(entry.entryId, result);
|
|
2227
|
+
}
|
|
2228
|
+
return results;
|
|
2229
|
+
}
|
|
2230
|
+
/**
|
|
2231
|
+
* Extract topics from entry
|
|
2232
|
+
*/
|
|
2233
|
+
extractTopics(entry) {
|
|
2234
|
+
const topics = [];
|
|
2235
|
+
const titleWords = entry.title.toLowerCase().split(/[\s\-_]+/).filter((w) => w.length > 3 && !this.isStopWord(w));
|
|
2236
|
+
topics.push(...titleWords);
|
|
2237
|
+
const content = entry.contentJson;
|
|
2238
|
+
if (content.topics && Array.isArray(content.topics)) {
|
|
2239
|
+
topics.push(...content.topics.map((t) => String(t).toLowerCase()));
|
|
2240
|
+
}
|
|
2241
|
+
if (content.technologies && Array.isArray(content.technologies)) {
|
|
2242
|
+
topics.push(...content.technologies.map((t) => String(t).toLowerCase()));
|
|
2243
|
+
}
|
|
2244
|
+
return [...new Set(topics)];
|
|
2245
|
+
}
|
|
2246
|
+
/**
|
|
2247
|
+
* Check if word is a stop word
|
|
2248
|
+
*/
|
|
2249
|
+
isStopWord(word) {
|
|
2250
|
+
const stopWords = /* @__PURE__ */ new Set([
|
|
2251
|
+
"the",
|
|
2252
|
+
"and",
|
|
2253
|
+
"for",
|
|
2254
|
+
"with",
|
|
2255
|
+
"this",
|
|
2256
|
+
"that",
|
|
2257
|
+
"from",
|
|
2258
|
+
"have",
|
|
2259
|
+
"been",
|
|
2260
|
+
"were",
|
|
2261
|
+
"are",
|
|
2262
|
+
"was",
|
|
2263
|
+
"had",
|
|
2264
|
+
"has",
|
|
2265
|
+
"will",
|
|
2266
|
+
"would",
|
|
2267
|
+
"could",
|
|
2268
|
+
"should",
|
|
2269
|
+
"when",
|
|
2270
|
+
"where",
|
|
2271
|
+
"what",
|
|
2272
|
+
"which",
|
|
2273
|
+
"while",
|
|
2274
|
+
"error",
|
|
2275
|
+
"problem",
|
|
2276
|
+
"issue"
|
|
2277
|
+
]);
|
|
2278
|
+
return stopWords.has(word);
|
|
2279
|
+
}
|
|
2280
|
+
/**
|
|
2281
|
+
* Calculate confidence score for entry
|
|
2282
|
+
*/
|
|
2283
|
+
calculateConfidence(entry) {
|
|
2284
|
+
let confidence = 0.8;
|
|
2285
|
+
if (entry.stage === "certified") {
|
|
2286
|
+
confidence = 0.95;
|
|
2287
|
+
}
|
|
2288
|
+
return Math.min(confidence, 1);
|
|
2289
|
+
}
|
|
2290
|
+
/**
|
|
2291
|
+
* Create embedding content from input
|
|
2292
|
+
*/
|
|
2293
|
+
createEmbeddingContent(input) {
|
|
2294
|
+
const parts = [];
|
|
2295
|
+
parts.push(`Problem: ${input.title}`);
|
|
2296
|
+
if (input.symptoms.length > 0) {
|
|
2297
|
+
parts.push(`Symptoms: ${input.symptoms.join(", ")}`);
|
|
2298
|
+
}
|
|
2299
|
+
if (input.rootCause) {
|
|
2300
|
+
parts.push(`Root Cause: ${input.rootCause}`);
|
|
2301
|
+
}
|
|
2302
|
+
if (input.solution) {
|
|
2303
|
+
parts.push(`Solution: ${input.solution}`);
|
|
2304
|
+
}
|
|
2305
|
+
if (input.technologies && input.technologies.length > 0) {
|
|
2306
|
+
parts.push(`Technologies: ${input.technologies.join(", ")}`);
|
|
2307
|
+
}
|
|
2308
|
+
return parts.join("\n");
|
|
2309
|
+
}
|
|
2310
|
+
};
|
|
2311
|
+
function createSharedPromoter(sharedStore, sharedVectorStore, embedder, config) {
|
|
2312
|
+
return new SharedPromoter(sharedStore, sharedVectorStore, embedder, config);
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2315
|
+
// src/core/metadata-extractor.ts
|
|
2316
|
+
function createToolObservationEmbedding(toolName, metadata, success) {
|
|
2317
|
+
const parts = [];
|
|
2318
|
+
parts.push(`Tool: ${toolName}`);
|
|
2319
|
+
if (metadata.filePath) {
|
|
2320
|
+
parts.push(`File: ${metadata.filePath}`);
|
|
2321
|
+
}
|
|
2322
|
+
if (metadata.command) {
|
|
2323
|
+
parts.push(`Command: ${metadata.command}`);
|
|
2324
|
+
}
|
|
2325
|
+
if (metadata.pattern) {
|
|
2326
|
+
parts.push(`Pattern: ${metadata.pattern}`);
|
|
2327
|
+
}
|
|
2328
|
+
if (metadata.url) {
|
|
2329
|
+
try {
|
|
2330
|
+
const url = new URL(metadata.url);
|
|
2331
|
+
parts.push(`URL: ${url.hostname}`);
|
|
2332
|
+
} catch {
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
parts.push(`Result: ${success ? "Success" : "Failed"}`);
|
|
2336
|
+
return parts.join("\n");
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
// src/core/working-set-store.ts
|
|
2340
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
2341
|
+
var WorkingSetStore = class {
|
|
2342
|
+
constructor(eventStore, config) {
|
|
2343
|
+
this.eventStore = eventStore;
|
|
2344
|
+
this.config = config;
|
|
2345
|
+
}
|
|
2346
|
+
get db() {
|
|
2347
|
+
return this.eventStore.getDatabase();
|
|
2348
|
+
}
|
|
2349
|
+
/**
|
|
2350
|
+
* Add an event to the working set
|
|
2351
|
+
*/
|
|
2352
|
+
async add(eventId, relevanceScore = 1, topics) {
|
|
2353
|
+
const expiresAt = new Date(
|
|
2354
|
+
Date.now() + this.config.workingSet.timeWindowHours * 60 * 60 * 1e3
|
|
2355
|
+
);
|
|
2356
|
+
await dbRun(
|
|
2357
|
+
this.db,
|
|
2358
|
+
`INSERT OR REPLACE INTO working_set (id, event_id, added_at, relevance_score, topics, expires_at)
|
|
2359
|
+
VALUES (?, ?, CURRENT_TIMESTAMP, ?, ?, ?)`,
|
|
2360
|
+
[
|
|
2361
|
+
randomUUID4(),
|
|
2362
|
+
eventId,
|
|
2363
|
+
relevanceScore,
|
|
2364
|
+
JSON.stringify(topics || []),
|
|
2365
|
+
expiresAt.toISOString()
|
|
2366
|
+
]
|
|
2367
|
+
);
|
|
2368
|
+
await this.enforceLimit();
|
|
2369
|
+
}
|
|
2370
|
+
/**
|
|
2371
|
+
* Get the current working set
|
|
2372
|
+
*/
|
|
2373
|
+
async get() {
|
|
2374
|
+
await this.cleanup();
|
|
2375
|
+
const rows = await dbAll(
|
|
2376
|
+
this.db,
|
|
2377
|
+
`SELECT ws.*, e.*
|
|
2378
|
+
FROM working_set ws
|
|
2379
|
+
JOIN events e ON ws.event_id = e.id
|
|
2380
|
+
ORDER BY ws.relevance_score DESC, ws.added_at DESC
|
|
2381
|
+
LIMIT ?`,
|
|
2382
|
+
[this.config.workingSet.maxEvents]
|
|
2383
|
+
);
|
|
2384
|
+
const events = rows.map((row) => ({
|
|
2385
|
+
id: row.id,
|
|
2386
|
+
eventType: row.event_type,
|
|
2387
|
+
sessionId: row.session_id,
|
|
2388
|
+
timestamp: toDate(row.timestamp),
|
|
2389
|
+
content: row.content,
|
|
2390
|
+
canonicalKey: row.canonical_key,
|
|
2391
|
+
dedupeKey: row.dedupe_key,
|
|
2392
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : void 0
|
|
2393
|
+
}));
|
|
2394
|
+
return {
|
|
2395
|
+
recentEvents: events,
|
|
2396
|
+
lastActivity: events.length > 0 ? events[0].timestamp : /* @__PURE__ */ new Date(),
|
|
2397
|
+
continuityScore: await this.calculateContinuityScore()
|
|
2398
|
+
};
|
|
2399
|
+
}
|
|
2400
|
+
/**
|
|
2401
|
+
* Get working set items (metadata only)
|
|
2402
|
+
*/
|
|
2403
|
+
async getItems() {
|
|
2404
|
+
const rows = await dbAll(
|
|
2405
|
+
this.db,
|
|
2406
|
+
`SELECT * FROM working_set ORDER BY relevance_score DESC, added_at DESC`
|
|
2407
|
+
);
|
|
2408
|
+
return rows.map((row) => ({
|
|
2409
|
+
id: row.id,
|
|
2410
|
+
eventId: row.event_id,
|
|
2411
|
+
addedAt: toDate(row.added_at),
|
|
2412
|
+
relevanceScore: row.relevance_score,
|
|
2413
|
+
topics: row.topics ? JSON.parse(row.topics) : void 0,
|
|
2414
|
+
expiresAt: toDate(row.expires_at)
|
|
2415
|
+
}));
|
|
2416
|
+
}
|
|
2417
|
+
/**
|
|
2418
|
+
* Update relevance score for an event
|
|
2419
|
+
*/
|
|
2420
|
+
async updateRelevance(eventId, score) {
|
|
2421
|
+
await dbRun(
|
|
2422
|
+
this.db,
|
|
2423
|
+
`UPDATE working_set SET relevance_score = ? WHERE event_id = ?`,
|
|
2424
|
+
[score, eventId]
|
|
2425
|
+
);
|
|
2426
|
+
}
|
|
2427
|
+
/**
|
|
2428
|
+
* Prune specific events from working set (after consolidation)
|
|
2429
|
+
*/
|
|
2430
|
+
async prune(eventIds) {
|
|
2431
|
+
if (eventIds.length === 0)
|
|
2432
|
+
return;
|
|
2433
|
+
const placeholders = eventIds.map(() => "?").join(",");
|
|
2434
|
+
await dbRun(
|
|
2435
|
+
this.db,
|
|
2436
|
+
`DELETE FROM working_set WHERE event_id IN (${placeholders})`,
|
|
2437
|
+
eventIds
|
|
2438
|
+
);
|
|
2439
|
+
}
|
|
2440
|
+
/**
|
|
2441
|
+
* Get the count of items in working set
|
|
2442
|
+
*/
|
|
2443
|
+
async count() {
|
|
2444
|
+
const result = await dbAll(
|
|
2445
|
+
this.db,
|
|
2446
|
+
`SELECT COUNT(*) as count FROM working_set`
|
|
2447
|
+
);
|
|
2448
|
+
return result[0]?.count || 0;
|
|
2449
|
+
}
|
|
2450
|
+
/**
|
|
2451
|
+
* Clear the entire working set
|
|
2452
|
+
*/
|
|
2453
|
+
async clear() {
|
|
2454
|
+
await dbRun(this.db, `DELETE FROM working_set`);
|
|
2455
|
+
}
|
|
2456
|
+
/**
|
|
2457
|
+
* Check if an event is in the working set
|
|
2458
|
+
*/
|
|
2459
|
+
async contains(eventId) {
|
|
2460
|
+
const result = await dbAll(
|
|
2461
|
+
this.db,
|
|
2462
|
+
`SELECT COUNT(*) as count FROM working_set WHERE event_id = ?`,
|
|
2463
|
+
[eventId]
|
|
2464
|
+
);
|
|
2465
|
+
return (result[0]?.count || 0) > 0;
|
|
2466
|
+
}
|
|
2467
|
+
/**
|
|
2468
|
+
* Refresh expiration for an event (rehears al - keep relevant items longer)
|
|
2469
|
+
*/
|
|
2470
|
+
async refresh(eventId) {
|
|
2471
|
+
const newExpiresAt = new Date(
|
|
2472
|
+
Date.now() + this.config.workingSet.timeWindowHours * 60 * 60 * 1e3
|
|
2473
|
+
);
|
|
2474
|
+
await dbRun(
|
|
2475
|
+
this.db,
|
|
2476
|
+
`UPDATE working_set SET expires_at = ? WHERE event_id = ?`,
|
|
2477
|
+
[newExpiresAt.toISOString(), eventId]
|
|
2478
|
+
);
|
|
2479
|
+
}
|
|
2480
|
+
/**
|
|
2481
|
+
* Clean up expired items
|
|
2482
|
+
*/
|
|
2483
|
+
async cleanup() {
|
|
2484
|
+
await dbRun(
|
|
2485
|
+
this.db,
|
|
2486
|
+
`DELETE FROM working_set WHERE expires_at < datetime('now')`
|
|
2487
|
+
);
|
|
2488
|
+
}
|
|
2489
|
+
/**
|
|
2490
|
+
* Enforce the maximum size limit
|
|
2491
|
+
* Removes lowest relevance items when over limit
|
|
2492
|
+
*/
|
|
2493
|
+
async enforceLimit() {
|
|
2494
|
+
const maxEvents = this.config.workingSet.maxEvents;
|
|
2495
|
+
const keepIds = await dbAll(
|
|
2496
|
+
this.db,
|
|
2497
|
+
`SELECT id FROM working_set
|
|
2498
|
+
ORDER BY relevance_score DESC, added_at DESC
|
|
2499
|
+
LIMIT ?`,
|
|
2500
|
+
[maxEvents]
|
|
2501
|
+
);
|
|
2502
|
+
if (keepIds.length === 0)
|
|
2503
|
+
return;
|
|
2504
|
+
const keepIdList = keepIds.map((r) => r.id);
|
|
2505
|
+
const placeholders = keepIdList.map(() => "?").join(",");
|
|
2506
|
+
await dbRun(
|
|
2507
|
+
this.db,
|
|
2508
|
+
`DELETE FROM working_set WHERE id NOT IN (${placeholders})`,
|
|
2509
|
+
keepIdList
|
|
2510
|
+
);
|
|
2511
|
+
}
|
|
2512
|
+
/**
|
|
2513
|
+
* Calculate continuity score based on recent context transitions
|
|
2514
|
+
*/
|
|
2515
|
+
async calculateContinuityScore() {
|
|
2516
|
+
const result = await dbAll(
|
|
2517
|
+
this.db,
|
|
2518
|
+
`SELECT AVG(continuity_score) as avg_score
|
|
2519
|
+
FROM continuity_log
|
|
2520
|
+
WHERE created_at > datetime('now', '-1 hour')`
|
|
2521
|
+
);
|
|
2522
|
+
return result[0]?.avg_score ?? 0.5;
|
|
2523
|
+
}
|
|
2524
|
+
/**
|
|
2525
|
+
* Get topics from current working set for context matching
|
|
2526
|
+
*/
|
|
2527
|
+
async getActiveTopics() {
|
|
2528
|
+
const rows = await dbAll(
|
|
2529
|
+
this.db,
|
|
2530
|
+
`SELECT topics FROM working_set WHERE topics IS NOT NULL`
|
|
2531
|
+
);
|
|
2532
|
+
const allTopics = /* @__PURE__ */ new Set();
|
|
2533
|
+
for (const row of rows) {
|
|
2534
|
+
const topics = JSON.parse(row.topics);
|
|
2535
|
+
topics.forEach((t) => allTopics.add(t));
|
|
2536
|
+
}
|
|
2537
|
+
return Array.from(allTopics);
|
|
2538
|
+
}
|
|
2539
|
+
};
|
|
2540
|
+
function createWorkingSetStore(eventStore, config) {
|
|
2541
|
+
return new WorkingSetStore(eventStore, config);
|
|
2542
|
+
}
|
|
2543
|
+
|
|
2544
|
+
// src/core/consolidated-store.ts
|
|
2545
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
2546
|
+
var ConsolidatedStore = class {
|
|
2547
|
+
constructor(eventStore) {
|
|
2548
|
+
this.eventStore = eventStore;
|
|
2549
|
+
}
|
|
2550
|
+
get db() {
|
|
2551
|
+
return this.eventStore.getDatabase();
|
|
2552
|
+
}
|
|
2553
|
+
/**
|
|
2554
|
+
* Create a new consolidated memory
|
|
2555
|
+
*/
|
|
2556
|
+
async create(input) {
|
|
2557
|
+
const memoryId = randomUUID5();
|
|
2558
|
+
await dbRun(
|
|
2559
|
+
this.db,
|
|
2560
|
+
`INSERT INTO consolidated_memories
|
|
2561
|
+
(memory_id, summary, topics, source_events, confidence, created_at)
|
|
2562
|
+
VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,
|
|
2563
|
+
[
|
|
2564
|
+
memoryId,
|
|
2565
|
+
input.summary,
|
|
2566
|
+
JSON.stringify(input.topics),
|
|
2567
|
+
JSON.stringify(input.sourceEvents),
|
|
2568
|
+
input.confidence
|
|
2569
|
+
]
|
|
2570
|
+
);
|
|
2571
|
+
return memoryId;
|
|
2572
|
+
}
|
|
2573
|
+
/**
|
|
2574
|
+
* Get a consolidated memory by ID
|
|
2575
|
+
*/
|
|
2576
|
+
async get(memoryId) {
|
|
2577
|
+
const rows = await dbAll(
|
|
2578
|
+
this.db,
|
|
2579
|
+
`SELECT * FROM consolidated_memories WHERE memory_id = ?`,
|
|
2580
|
+
[memoryId]
|
|
2581
|
+
);
|
|
2582
|
+
if (rows.length === 0)
|
|
2583
|
+
return null;
|
|
2584
|
+
return this.rowToMemory(rows[0]);
|
|
2585
|
+
}
|
|
2586
|
+
/**
|
|
2587
|
+
* Search consolidated memories by query (simple text search)
|
|
2588
|
+
*/
|
|
2589
|
+
async search(query, options) {
|
|
2590
|
+
const topK = options?.topK || 5;
|
|
2591
|
+
const rows = await dbAll(
|
|
2592
|
+
this.db,
|
|
2593
|
+
`SELECT * FROM consolidated_memories
|
|
2594
|
+
WHERE summary LIKE ?
|
|
2595
|
+
ORDER BY confidence DESC
|
|
2596
|
+
LIMIT ?`,
|
|
2597
|
+
[`%${query}%`, topK]
|
|
2598
|
+
);
|
|
2599
|
+
return rows.map(this.rowToMemory);
|
|
2600
|
+
}
|
|
2601
|
+
/**
|
|
2602
|
+
* Search by topics
|
|
2603
|
+
*/
|
|
2604
|
+
async searchByTopics(topics, options) {
|
|
2605
|
+
const topK = options?.topK || 5;
|
|
2606
|
+
const topicConditions = topics.map(() => `topics LIKE ?`).join(" OR ");
|
|
2607
|
+
const topicParams = topics.map((t) => `%"${t}"%`);
|
|
2608
|
+
const rows = await dbAll(
|
|
2609
|
+
this.db,
|
|
2610
|
+
`SELECT * FROM consolidated_memories
|
|
2611
|
+
WHERE ${topicConditions}
|
|
2612
|
+
ORDER BY confidence DESC
|
|
2613
|
+
LIMIT ?`,
|
|
2614
|
+
[...topicParams, topK]
|
|
2615
|
+
);
|
|
2616
|
+
return rows.map(this.rowToMemory);
|
|
2617
|
+
}
|
|
2618
|
+
/**
|
|
2619
|
+
* Get all consolidated memories ordered by confidence
|
|
2620
|
+
*/
|
|
2621
|
+
async getAll(options) {
|
|
2622
|
+
const limit = options?.limit || 100;
|
|
2623
|
+
const rows = await dbAll(
|
|
2624
|
+
this.db,
|
|
2625
|
+
`SELECT * FROM consolidated_memories
|
|
2626
|
+
ORDER BY confidence DESC, created_at DESC
|
|
2627
|
+
LIMIT ?`,
|
|
2628
|
+
[limit]
|
|
2629
|
+
);
|
|
2630
|
+
return rows.map(this.rowToMemory);
|
|
2631
|
+
}
|
|
2632
|
+
/**
|
|
2633
|
+
* Get recently created memories
|
|
2634
|
+
*/
|
|
2635
|
+
async getRecent(options) {
|
|
2636
|
+
const limit = options?.limit || 10;
|
|
2637
|
+
const hours = options?.hours || 24;
|
|
2638
|
+
const rows = await dbAll(
|
|
2639
|
+
this.db,
|
|
2640
|
+
`SELECT * FROM consolidated_memories
|
|
2641
|
+
WHERE created_at > datetime('now', '-${hours} hours')
|
|
2642
|
+
ORDER BY created_at DESC
|
|
2643
|
+
LIMIT ?`,
|
|
2644
|
+
[limit]
|
|
2645
|
+
);
|
|
2646
|
+
return rows.map(this.rowToMemory);
|
|
2647
|
+
}
|
|
2648
|
+
/**
|
|
2649
|
+
* Mark a memory as accessed (tracks usage for importance scoring)
|
|
2650
|
+
*/
|
|
2651
|
+
async markAccessed(memoryId) {
|
|
2652
|
+
await dbRun(
|
|
2653
|
+
this.db,
|
|
2654
|
+
`UPDATE consolidated_memories
|
|
2655
|
+
SET accessed_at = CURRENT_TIMESTAMP,
|
|
2656
|
+
access_count = access_count + 1
|
|
2657
|
+
WHERE memory_id = ?`,
|
|
2658
|
+
[memoryId]
|
|
2659
|
+
);
|
|
2660
|
+
}
|
|
2661
|
+
/**
|
|
2662
|
+
* Update confidence score for a memory
|
|
2663
|
+
*/
|
|
2664
|
+
async updateConfidence(memoryId, confidence) {
|
|
2665
|
+
await dbRun(
|
|
2666
|
+
this.db,
|
|
2667
|
+
`UPDATE consolidated_memories
|
|
2668
|
+
SET confidence = ?
|
|
2669
|
+
WHERE memory_id = ?`,
|
|
2670
|
+
[confidence, memoryId]
|
|
2671
|
+
);
|
|
2672
|
+
}
|
|
2673
|
+
/**
|
|
2674
|
+
* Delete a consolidated memory
|
|
2675
|
+
*/
|
|
2676
|
+
async delete(memoryId) {
|
|
2677
|
+
await dbRun(
|
|
2678
|
+
this.db,
|
|
2679
|
+
`DELETE FROM consolidated_memories WHERE memory_id = ?`,
|
|
2680
|
+
[memoryId]
|
|
2681
|
+
);
|
|
2682
|
+
}
|
|
2683
|
+
/**
|
|
2684
|
+
* Get count of consolidated memories
|
|
2685
|
+
*/
|
|
2686
|
+
async count() {
|
|
2687
|
+
const result = await dbAll(
|
|
2688
|
+
this.db,
|
|
2689
|
+
`SELECT COUNT(*) as count FROM consolidated_memories`
|
|
2690
|
+
);
|
|
2691
|
+
return result[0]?.count || 0;
|
|
2692
|
+
}
|
|
2693
|
+
/**
|
|
2694
|
+
* Get most accessed memories (for importance scoring)
|
|
2695
|
+
*/
|
|
2696
|
+
async getMostAccessed(limit = 10) {
|
|
2697
|
+
const rows = await dbAll(
|
|
2698
|
+
this.db,
|
|
2699
|
+
`SELECT * FROM consolidated_memories
|
|
2700
|
+
WHERE access_count > 0
|
|
2701
|
+
ORDER BY access_count DESC
|
|
2702
|
+
LIMIT ?`,
|
|
2703
|
+
[limit]
|
|
2704
|
+
);
|
|
2705
|
+
return rows.map(this.rowToMemory);
|
|
2706
|
+
}
|
|
2707
|
+
/**
|
|
2708
|
+
* Get statistics about consolidated memories
|
|
2709
|
+
*/
|
|
2710
|
+
async getStats() {
|
|
2711
|
+
const total = await this.count();
|
|
2712
|
+
const avgResult = await dbAll(
|
|
2713
|
+
this.db,
|
|
2714
|
+
`SELECT AVG(confidence) as avg FROM consolidated_memories`
|
|
2715
|
+
);
|
|
2716
|
+
const averageConfidence = avgResult[0]?.avg || 0;
|
|
2717
|
+
const recentResult = await dbAll(
|
|
2718
|
+
this.db,
|
|
2719
|
+
`SELECT COUNT(*) as count FROM consolidated_memories
|
|
2720
|
+
WHERE created_at > datetime('now', '-24 hours')`
|
|
2721
|
+
);
|
|
2722
|
+
const recentCount = recentResult[0]?.count || 0;
|
|
2723
|
+
const allMemories = await this.getAll({ limit: 1e3 });
|
|
2724
|
+
const topicCounts = {};
|
|
2725
|
+
for (const memory of allMemories) {
|
|
2726
|
+
for (const topic of memory.topics) {
|
|
2727
|
+
topicCounts[topic] = (topicCounts[topic] || 0) + 1;
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
return {
|
|
2731
|
+
total,
|
|
2732
|
+
averageConfidence,
|
|
2733
|
+
topicCounts,
|
|
2734
|
+
recentCount
|
|
2735
|
+
};
|
|
2736
|
+
}
|
|
2737
|
+
/**
|
|
2738
|
+
* Check if source events are already consolidated
|
|
2739
|
+
*/
|
|
2740
|
+
async isAlreadyConsolidated(eventIds) {
|
|
2741
|
+
for (const eventId of eventIds) {
|
|
2742
|
+
const result = await dbAll(
|
|
2743
|
+
this.db,
|
|
2744
|
+
`SELECT COUNT(*) as count FROM consolidated_memories
|
|
2745
|
+
WHERE source_events LIKE ?`,
|
|
2746
|
+
[`%"${eventId}"%`]
|
|
2747
|
+
);
|
|
2748
|
+
if ((result[0]?.count || 0) > 0)
|
|
2749
|
+
return true;
|
|
2750
|
+
}
|
|
2751
|
+
return false;
|
|
2752
|
+
}
|
|
2753
|
+
/**
|
|
2754
|
+
* Get the last consolidation time
|
|
2755
|
+
*/
|
|
2756
|
+
async getLastConsolidationTime() {
|
|
2757
|
+
const result = await dbAll(
|
|
2758
|
+
this.db,
|
|
2759
|
+
`SELECT created_at FROM consolidated_memories
|
|
2760
|
+
ORDER BY created_at DESC
|
|
2761
|
+
LIMIT 1`
|
|
2762
|
+
);
|
|
2763
|
+
if (result.length === 0)
|
|
2764
|
+
return null;
|
|
2765
|
+
return new Date(result[0].created_at);
|
|
2766
|
+
}
|
|
2767
|
+
/**
|
|
2768
|
+
* Convert database row to ConsolidatedMemory
|
|
2769
|
+
*/
|
|
2770
|
+
rowToMemory(row) {
|
|
2771
|
+
return {
|
|
2772
|
+
memoryId: row.memory_id,
|
|
2773
|
+
summary: row.summary,
|
|
2774
|
+
topics: JSON.parse(row.topics || "[]"),
|
|
2775
|
+
sourceEvents: JSON.parse(row.source_events || "[]"),
|
|
2776
|
+
confidence: row.confidence,
|
|
2777
|
+
createdAt: toDate(row.created_at),
|
|
2778
|
+
accessedAt: row.accessed_at ? toDate(row.accessed_at) : void 0,
|
|
2779
|
+
accessCount: row.access_count || 0
|
|
2780
|
+
};
|
|
2781
|
+
}
|
|
2782
|
+
};
|
|
2783
|
+
function createConsolidatedStore(eventStore) {
|
|
2784
|
+
return new ConsolidatedStore(eventStore);
|
|
2785
|
+
}
|
|
2786
|
+
|
|
2787
|
+
// src/core/consolidation-worker.ts
|
|
2788
|
+
var ConsolidationWorker = class {
|
|
2789
|
+
constructor(workingSetStore, consolidatedStore, config) {
|
|
2790
|
+
this.workingSetStore = workingSetStore;
|
|
2791
|
+
this.consolidatedStore = consolidatedStore;
|
|
2792
|
+
this.config = config;
|
|
2793
|
+
}
|
|
2794
|
+
running = false;
|
|
2795
|
+
timeout = null;
|
|
2796
|
+
lastActivity = /* @__PURE__ */ new Date();
|
|
2797
|
+
/**
|
|
2798
|
+
* Start the consolidation worker
|
|
2799
|
+
*/
|
|
2800
|
+
start() {
|
|
2801
|
+
if (this.running)
|
|
2802
|
+
return;
|
|
2803
|
+
this.running = true;
|
|
2804
|
+
this.scheduleNext();
|
|
2805
|
+
}
|
|
2806
|
+
/**
|
|
2807
|
+
* Stop the consolidation worker
|
|
2808
|
+
*/
|
|
2809
|
+
stop() {
|
|
2810
|
+
this.running = false;
|
|
2811
|
+
if (this.timeout) {
|
|
2812
|
+
clearTimeout(this.timeout);
|
|
2813
|
+
this.timeout = null;
|
|
2814
|
+
}
|
|
2815
|
+
}
|
|
2816
|
+
/**
|
|
2817
|
+
* Record activity (resets idle timer)
|
|
2818
|
+
*/
|
|
2819
|
+
recordActivity() {
|
|
2820
|
+
this.lastActivity = /* @__PURE__ */ new Date();
|
|
2821
|
+
}
|
|
2822
|
+
/**
|
|
2823
|
+
* Check if currently running
|
|
2824
|
+
*/
|
|
2825
|
+
isRunning() {
|
|
2826
|
+
return this.running;
|
|
2827
|
+
}
|
|
2828
|
+
/**
|
|
2829
|
+
* Force a consolidation run (manual trigger)
|
|
2830
|
+
*/
|
|
2831
|
+
async forceRun() {
|
|
2832
|
+
return await this.consolidate();
|
|
2833
|
+
}
|
|
2834
|
+
/**
|
|
2835
|
+
* Schedule the next consolidation check
|
|
2836
|
+
*/
|
|
2837
|
+
scheduleNext() {
|
|
2838
|
+
if (!this.running)
|
|
2839
|
+
return;
|
|
2840
|
+
this.timeout = setTimeout(
|
|
2841
|
+
() => this.run(),
|
|
2842
|
+
this.config.consolidation.triggerIntervalMs
|
|
2843
|
+
);
|
|
2844
|
+
}
|
|
2845
|
+
/**
|
|
2846
|
+
* Run consolidation check
|
|
2847
|
+
*/
|
|
2848
|
+
async run() {
|
|
2849
|
+
if (!this.running)
|
|
2850
|
+
return;
|
|
2851
|
+
try {
|
|
2852
|
+
await this.checkAndConsolidate();
|
|
2853
|
+
} catch (error) {
|
|
2854
|
+
console.error("Consolidation error:", error);
|
|
2855
|
+
}
|
|
2856
|
+
this.scheduleNext();
|
|
2857
|
+
}
|
|
2858
|
+
/**
|
|
2859
|
+
* Check conditions and consolidate if needed
|
|
2860
|
+
*/
|
|
2861
|
+
async checkAndConsolidate() {
|
|
2862
|
+
const workingSet = await this.workingSetStore.get();
|
|
2863
|
+
if (!this.shouldConsolidate(workingSet)) {
|
|
2864
|
+
return;
|
|
2865
|
+
}
|
|
2866
|
+
await this.consolidate();
|
|
2867
|
+
}
|
|
2868
|
+
/**
|
|
2869
|
+
* Perform consolidation
|
|
2870
|
+
*/
|
|
2871
|
+
async consolidate() {
|
|
2872
|
+
const workingSet = await this.workingSetStore.get();
|
|
2873
|
+
if (workingSet.recentEvents.length < 3) {
|
|
2874
|
+
return 0;
|
|
2875
|
+
}
|
|
2876
|
+
const groups = this.groupByTopic(workingSet.recentEvents);
|
|
2877
|
+
let consolidatedCount = 0;
|
|
2878
|
+
for (const group of groups) {
|
|
2879
|
+
if (group.events.length < 3)
|
|
2880
|
+
continue;
|
|
2881
|
+
const eventIds = group.events.map((e) => e.id);
|
|
2882
|
+
const alreadyConsolidated = await this.consolidatedStore.isAlreadyConsolidated(eventIds);
|
|
2883
|
+
if (alreadyConsolidated)
|
|
2884
|
+
continue;
|
|
2885
|
+
const summary = await this.summarize(group);
|
|
2886
|
+
await this.consolidatedStore.create({
|
|
2887
|
+
summary,
|
|
2888
|
+
topics: group.topics,
|
|
2889
|
+
sourceEvents: eventIds,
|
|
2890
|
+
confidence: this.calculateConfidence(group)
|
|
2891
|
+
});
|
|
2892
|
+
consolidatedCount++;
|
|
2893
|
+
}
|
|
2894
|
+
if (consolidatedCount > 0) {
|
|
2895
|
+
const consolidatedEventIds = groups.filter((g) => g.events.length >= 3).flatMap((g) => g.events.map((e) => e.id));
|
|
2896
|
+
const oldEventIds = consolidatedEventIds.filter((id) => {
|
|
2897
|
+
const event = workingSet.recentEvents.find((e) => e.id === id);
|
|
2898
|
+
if (!event)
|
|
2899
|
+
return false;
|
|
2900
|
+
const ageHours = (Date.now() - event.timestamp.getTime()) / (1e3 * 60 * 60);
|
|
2901
|
+
return ageHours > this.config.workingSet.timeWindowHours / 2;
|
|
2902
|
+
});
|
|
2903
|
+
if (oldEventIds.length > 0) {
|
|
2904
|
+
await this.workingSetStore.prune(oldEventIds);
|
|
2905
|
+
}
|
|
2906
|
+
}
|
|
2907
|
+
return consolidatedCount;
|
|
2908
|
+
}
|
|
2909
|
+
/**
|
|
2910
|
+
* Check if consolidation should run
|
|
2911
|
+
*/
|
|
2912
|
+
shouldConsolidate(workingSet) {
|
|
2913
|
+
if (workingSet.recentEvents.length >= this.config.consolidation.triggerEventCount) {
|
|
2914
|
+
return true;
|
|
2915
|
+
}
|
|
2916
|
+
const idleTime = Date.now() - this.lastActivity.getTime();
|
|
2917
|
+
if (idleTime >= this.config.consolidation.triggerIdleMs) {
|
|
2918
|
+
return true;
|
|
2919
|
+
}
|
|
2920
|
+
return false;
|
|
2921
|
+
}
|
|
2922
|
+
/**
|
|
2923
|
+
* Group events by topic using simple keyword extraction
|
|
2924
|
+
*/
|
|
2925
|
+
groupByTopic(events) {
|
|
2926
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2927
|
+
for (const event of events) {
|
|
2928
|
+
const topics = this.extractTopics(event.content);
|
|
2929
|
+
for (const topic of topics) {
|
|
2930
|
+
if (!groups.has(topic)) {
|
|
2931
|
+
groups.set(topic, { topics: [topic], events: [] });
|
|
2932
|
+
}
|
|
2933
|
+
const group = groups.get(topic);
|
|
2934
|
+
if (!group.events.find((e) => e.id === event.id)) {
|
|
2935
|
+
group.events.push(event);
|
|
2936
|
+
}
|
|
2937
|
+
}
|
|
2938
|
+
}
|
|
2939
|
+
const mergedGroups = this.mergeOverlappingGroups(Array.from(groups.values()));
|
|
2940
|
+
return mergedGroups;
|
|
2941
|
+
}
|
|
2942
|
+
/**
|
|
2943
|
+
* Extract topics from content using simple keyword extraction
|
|
2944
|
+
*/
|
|
2945
|
+
extractTopics(content) {
|
|
2946
|
+
const topics = [];
|
|
2947
|
+
const codePatterns = [
|
|
2948
|
+
/\b(function|class|interface|type|const|let|var)\s+(\w+)/gi,
|
|
2949
|
+
/\b(import|export)\s+.*?from\s+['"]([^'"]+)['"]/gi,
|
|
2950
|
+
/\bfile[:\s]+([^\s,]+)/gi
|
|
2951
|
+
];
|
|
2952
|
+
for (const pattern of codePatterns) {
|
|
2953
|
+
let match;
|
|
2954
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
2955
|
+
const keyword = match[2] || match[1];
|
|
2956
|
+
if (keyword && keyword.length > 2) {
|
|
2957
|
+
topics.push(keyword.toLowerCase());
|
|
2958
|
+
}
|
|
2959
|
+
}
|
|
2960
|
+
}
|
|
2961
|
+
const commonTerms = [
|
|
2962
|
+
"bug",
|
|
2963
|
+
"fix",
|
|
2964
|
+
"error",
|
|
2965
|
+
"issue",
|
|
2966
|
+
"feature",
|
|
2967
|
+
"test",
|
|
2968
|
+
"refactor",
|
|
2969
|
+
"implement",
|
|
2970
|
+
"add",
|
|
2971
|
+
"remove",
|
|
2972
|
+
"update",
|
|
2973
|
+
"change",
|
|
2974
|
+
"modify",
|
|
2975
|
+
"create",
|
|
2976
|
+
"delete"
|
|
2977
|
+
];
|
|
2978
|
+
const contentLower = content.toLowerCase();
|
|
2979
|
+
for (const term of commonTerms) {
|
|
2980
|
+
if (contentLower.includes(term)) {
|
|
2981
|
+
topics.push(term);
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
return [...new Set(topics)].slice(0, 5);
|
|
2985
|
+
}
|
|
2986
|
+
/**
|
|
2987
|
+
* Merge groups that have significant event overlap
|
|
2988
|
+
*/
|
|
2989
|
+
mergeOverlappingGroups(groups) {
|
|
2990
|
+
const merged = [];
|
|
2991
|
+
for (const group of groups) {
|
|
2992
|
+
let foundMerge = false;
|
|
2993
|
+
for (const existing of merged) {
|
|
2994
|
+
const overlap = group.events.filter(
|
|
2995
|
+
(e) => existing.events.some((ex) => ex.id === e.id)
|
|
2996
|
+
);
|
|
2997
|
+
if (overlap.length > group.events.length / 2) {
|
|
2998
|
+
existing.topics = [.../* @__PURE__ */ new Set([...existing.topics, ...group.topics])];
|
|
2999
|
+
for (const event of group.events) {
|
|
3000
|
+
if (!existing.events.find((e) => e.id === event.id)) {
|
|
3001
|
+
existing.events.push(event);
|
|
3002
|
+
}
|
|
3003
|
+
}
|
|
3004
|
+
foundMerge = true;
|
|
3005
|
+
break;
|
|
3006
|
+
}
|
|
3007
|
+
}
|
|
3008
|
+
if (!foundMerge) {
|
|
3009
|
+
merged.push(group);
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
3012
|
+
return merged;
|
|
3013
|
+
}
|
|
3014
|
+
/**
|
|
3015
|
+
* Generate summary for a group of events
|
|
3016
|
+
* Rule-based extraction (no LLM by default)
|
|
3017
|
+
*/
|
|
3018
|
+
async summarize(group) {
|
|
3019
|
+
if (this.config.consolidation.useLLMSummarization) {
|
|
3020
|
+
return this.ruleBasedSummary(group);
|
|
3021
|
+
}
|
|
3022
|
+
return this.ruleBasedSummary(group);
|
|
3023
|
+
}
|
|
3024
|
+
/**
|
|
3025
|
+
* Rule-based summary generation
|
|
3026
|
+
*/
|
|
3027
|
+
ruleBasedSummary(group) {
|
|
3028
|
+
const keyPoints = [];
|
|
3029
|
+
for (const event of group.events.slice(0, 10)) {
|
|
3030
|
+
const keyPoint = this.extractKeyPoint(event.content);
|
|
3031
|
+
if (keyPoint) {
|
|
3032
|
+
keyPoints.push(keyPoint);
|
|
3033
|
+
}
|
|
3034
|
+
}
|
|
3035
|
+
const topicsStr = group.topics.slice(0, 3).join(", ");
|
|
3036
|
+
const summary = [
|
|
3037
|
+
`Topics: ${topicsStr}`,
|
|
3038
|
+
"",
|
|
3039
|
+
"Key points:",
|
|
3040
|
+
...keyPoints.map((kp) => `- ${kp}`)
|
|
3041
|
+
].join("\n");
|
|
3042
|
+
return summary;
|
|
3043
|
+
}
|
|
3044
|
+
/**
|
|
3045
|
+
* Extract key point from content
|
|
3046
|
+
*/
|
|
3047
|
+
extractKeyPoint(content) {
|
|
3048
|
+
const sentences = content.split(/[.!?\n]+/).filter((s) => s.trim().length > 10);
|
|
3049
|
+
if (sentences.length === 0)
|
|
3050
|
+
return null;
|
|
3051
|
+
const firstSentence = sentences[0].trim();
|
|
3052
|
+
if (firstSentence.length > 100) {
|
|
3053
|
+
return firstSentence.slice(0, 100) + "...";
|
|
3054
|
+
}
|
|
3055
|
+
return firstSentence;
|
|
3056
|
+
}
|
|
3057
|
+
/**
|
|
3058
|
+
* Calculate confidence score for a group
|
|
3059
|
+
*/
|
|
3060
|
+
calculateConfidence(group) {
|
|
3061
|
+
const eventScore = Math.min(group.events.length / 10, 1);
|
|
3062
|
+
const timeScore = this.calculateTimeProximity(group.events);
|
|
3063
|
+
const topicScore = Math.min(3 / group.topics.length, 1);
|
|
3064
|
+
return eventScore * 0.4 + timeScore * 0.4 + topicScore * 0.2;
|
|
3065
|
+
}
|
|
3066
|
+
/**
|
|
3067
|
+
* Calculate time proximity score
|
|
3068
|
+
*/
|
|
3069
|
+
calculateTimeProximity(events) {
|
|
3070
|
+
if (events.length < 2)
|
|
3071
|
+
return 1;
|
|
3072
|
+
const timestamps = events.map((e) => e.timestamp.getTime()).sort((a, b) => a - b);
|
|
3073
|
+
const timeSpan = timestamps[timestamps.length - 1] - timestamps[0];
|
|
3074
|
+
const avgGap = timeSpan / (events.length - 1);
|
|
3075
|
+
const hourInMs = 60 * 60 * 1e3;
|
|
3076
|
+
return Math.max(0, 1 - avgGap / (24 * hourInMs));
|
|
3077
|
+
}
|
|
3078
|
+
};
|
|
3079
|
+
function createConsolidationWorker(workingSetStore, consolidatedStore, config) {
|
|
3080
|
+
return new ConsolidationWorker(workingSetStore, consolidatedStore, config);
|
|
3081
|
+
}
|
|
3082
|
+
|
|
3083
|
+
// src/core/continuity-manager.ts
|
|
3084
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
3085
|
+
var ContinuityManager = class {
|
|
3086
|
+
constructor(eventStore, config) {
|
|
3087
|
+
this.eventStore = eventStore;
|
|
3088
|
+
this.config = config;
|
|
3089
|
+
}
|
|
3090
|
+
lastContext = null;
|
|
3091
|
+
get db() {
|
|
3092
|
+
return this.eventStore.getDatabase();
|
|
3093
|
+
}
|
|
3094
|
+
/**
|
|
3095
|
+
* Calculate continuity score between current and previous context
|
|
3096
|
+
*/
|
|
3097
|
+
async calculateScore(currentContext, previousContext) {
|
|
3098
|
+
const prev = previousContext || this.lastContext;
|
|
3099
|
+
if (!prev) {
|
|
3100
|
+
this.lastContext = currentContext;
|
|
3101
|
+
return { score: 0.5, transitionType: "break" };
|
|
3102
|
+
}
|
|
3103
|
+
let score = 0;
|
|
3104
|
+
const topicOverlap = this.calculateOverlap(
|
|
3105
|
+
currentContext.topics,
|
|
3106
|
+
prev.topics
|
|
3107
|
+
);
|
|
3108
|
+
score += topicOverlap * 0.3;
|
|
3109
|
+
const fileOverlap = this.calculateOverlap(
|
|
3110
|
+
currentContext.files,
|
|
3111
|
+
prev.files
|
|
3112
|
+
);
|
|
3113
|
+
score += fileOverlap * 0.2;
|
|
3114
|
+
const timeDiff = currentContext.timestamp - prev.timestamp;
|
|
3115
|
+
const decayHours = this.config.continuity.topicDecayHours;
|
|
3116
|
+
const timeScore = Math.exp(-timeDiff / (decayHours * 36e5));
|
|
3117
|
+
score += timeScore * 0.3;
|
|
3118
|
+
const entityOverlap = this.calculateOverlap(
|
|
3119
|
+
currentContext.entities,
|
|
3120
|
+
prev.entities
|
|
3121
|
+
);
|
|
3122
|
+
score += entityOverlap * 0.2;
|
|
3123
|
+
const transitionType = this.determineTransitionType(score);
|
|
3124
|
+
await this.logTransition(currentContext, prev, score, transitionType);
|
|
3125
|
+
this.lastContext = currentContext;
|
|
3126
|
+
return { score, transitionType };
|
|
3127
|
+
}
|
|
3128
|
+
/**
|
|
3129
|
+
* Create a context snapshot from current state
|
|
3130
|
+
*/
|
|
3131
|
+
createSnapshot(id, content, metadata) {
|
|
3132
|
+
return {
|
|
3133
|
+
id,
|
|
3134
|
+
timestamp: Date.now(),
|
|
3135
|
+
topics: this.extractTopics(content),
|
|
3136
|
+
files: metadata?.files || this.extractFiles(content),
|
|
3137
|
+
entities: metadata?.entities || this.extractEntities(content)
|
|
3138
|
+
};
|
|
3139
|
+
}
|
|
3140
|
+
/**
|
|
3141
|
+
* Get recent continuity logs
|
|
3142
|
+
*/
|
|
3143
|
+
async getRecentLogs(limit = 10) {
|
|
3144
|
+
const rows = await dbAll(
|
|
3145
|
+
this.db,
|
|
3146
|
+
`SELECT * FROM continuity_log
|
|
3147
|
+
ORDER BY created_at DESC
|
|
3148
|
+
LIMIT ?`,
|
|
3149
|
+
[limit]
|
|
3150
|
+
);
|
|
3151
|
+
return rows.map((row) => ({
|
|
3152
|
+
logId: row.log_id,
|
|
3153
|
+
fromContextId: row.from_context_id,
|
|
3154
|
+
toContextId: row.to_context_id,
|
|
3155
|
+
continuityScore: row.continuity_score,
|
|
3156
|
+
transitionType: row.transition_type,
|
|
3157
|
+
createdAt: toDate(row.created_at)
|
|
3158
|
+
}));
|
|
3159
|
+
}
|
|
3160
|
+
/**
|
|
3161
|
+
* Get average continuity score over time period
|
|
3162
|
+
*/
|
|
3163
|
+
async getAverageScore(hours = 1) {
|
|
3164
|
+
const result = await dbAll(
|
|
3165
|
+
this.db,
|
|
3166
|
+
`SELECT AVG(continuity_score) as avg_score
|
|
3167
|
+
FROM continuity_log
|
|
3168
|
+
WHERE created_at > datetime('now', '-${hours} hours')`
|
|
3169
|
+
);
|
|
3170
|
+
return result[0]?.avg_score ?? 0.5;
|
|
3171
|
+
}
|
|
3172
|
+
/**
|
|
3173
|
+
* Get transition type distribution
|
|
3174
|
+
*/
|
|
3175
|
+
async getTransitionStats(hours = 24) {
|
|
3176
|
+
const rows = await dbAll(
|
|
3177
|
+
this.db,
|
|
3178
|
+
`SELECT transition_type, COUNT(*) as count
|
|
3179
|
+
FROM continuity_log
|
|
3180
|
+
WHERE created_at > datetime('now', '-${hours} hours')
|
|
3181
|
+
GROUP BY transition_type`
|
|
3182
|
+
);
|
|
3183
|
+
const stats = {
|
|
3184
|
+
seamless: 0,
|
|
3185
|
+
topic_shift: 0,
|
|
3186
|
+
break: 0
|
|
3187
|
+
};
|
|
3188
|
+
for (const row of rows) {
|
|
3189
|
+
stats[row.transition_type] = row.count;
|
|
3190
|
+
}
|
|
3191
|
+
return stats;
|
|
3192
|
+
}
|
|
3193
|
+
/**
|
|
3194
|
+
* Clear old continuity logs
|
|
3195
|
+
*/
|
|
3196
|
+
async cleanup(olderThanDays = 7) {
|
|
3197
|
+
const result = await dbAll(
|
|
3198
|
+
this.db,
|
|
3199
|
+
`DELETE FROM continuity_log
|
|
3200
|
+
WHERE created_at < datetime('now', '-${olderThanDays} days')
|
|
3201
|
+
RETURNING COUNT(*) as changes`
|
|
3202
|
+
);
|
|
3203
|
+
return result[0]?.changes || 0;
|
|
3204
|
+
}
|
|
3205
|
+
/**
|
|
3206
|
+
* Calculate overlap between two arrays
|
|
3207
|
+
*/
|
|
3208
|
+
calculateOverlap(a, b) {
|
|
3209
|
+
if (a.length === 0 || b.length === 0)
|
|
3210
|
+
return 0;
|
|
3211
|
+
const setA = new Set(a.map((s) => s.toLowerCase()));
|
|
3212
|
+
const setB = new Set(b.map((s) => s.toLowerCase()));
|
|
3213
|
+
const intersection = [...setA].filter((x) => setB.has(x));
|
|
3214
|
+
const union = /* @__PURE__ */ new Set([...setA, ...setB]);
|
|
3215
|
+
return intersection.length / union.size;
|
|
3216
|
+
}
|
|
3217
|
+
/**
|
|
3218
|
+
* Determine transition type based on score
|
|
3219
|
+
*/
|
|
3220
|
+
determineTransitionType(score) {
|
|
3221
|
+
if (score >= this.config.continuity.minScoreForSeamless) {
|
|
3222
|
+
return "seamless";
|
|
3223
|
+
} else if (score >= 0.4) {
|
|
3224
|
+
return "topic_shift";
|
|
3225
|
+
} else {
|
|
3226
|
+
return "break";
|
|
3227
|
+
}
|
|
3228
|
+
}
|
|
3229
|
+
/**
|
|
3230
|
+
* Log a context transition
|
|
3231
|
+
*/
|
|
3232
|
+
async logTransition(current, previous, score, type) {
|
|
3233
|
+
await dbRun(
|
|
3234
|
+
this.db,
|
|
3235
|
+
`INSERT INTO continuity_log
|
|
3236
|
+
(log_id, from_context_id, to_context_id, continuity_score, transition_type, created_at)
|
|
3237
|
+
VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,
|
|
3238
|
+
[randomUUID6(), previous.id, current.id, score, type]
|
|
3239
|
+
);
|
|
3240
|
+
}
|
|
3241
|
+
/**
|
|
3242
|
+
* Extract topics from content
|
|
3243
|
+
*/
|
|
3244
|
+
extractTopics(content) {
|
|
3245
|
+
const topics = [];
|
|
3246
|
+
const contentLower = content.toLowerCase();
|
|
3247
|
+
const langPatterns = [
|
|
3248
|
+
{ pattern: /typescript|\.ts\b/i, topic: "typescript" },
|
|
3249
|
+
{ pattern: /javascript|\.js\b/i, topic: "javascript" },
|
|
3250
|
+
{ pattern: /python|\.py\b/i, topic: "python" },
|
|
3251
|
+
{ pattern: /rust|\.rs\b/i, topic: "rust" },
|
|
3252
|
+
{ pattern: /go\b|golang/i, topic: "go" }
|
|
3253
|
+
];
|
|
3254
|
+
for (const { pattern, topic } of langPatterns) {
|
|
3255
|
+
if (pattern.test(content)) {
|
|
3256
|
+
topics.push(topic);
|
|
3257
|
+
}
|
|
3258
|
+
}
|
|
3259
|
+
const devTopics = [
|
|
3260
|
+
"api",
|
|
3261
|
+
"database",
|
|
3262
|
+
"test",
|
|
3263
|
+
"bug",
|
|
3264
|
+
"feature",
|
|
3265
|
+
"refactor",
|
|
3266
|
+
"component",
|
|
3267
|
+
"function",
|
|
3268
|
+
"class",
|
|
3269
|
+
"module",
|
|
3270
|
+
"hook",
|
|
3271
|
+
"deploy",
|
|
3272
|
+
"build",
|
|
3273
|
+
"config",
|
|
3274
|
+
"docker",
|
|
3275
|
+
"git"
|
|
3276
|
+
];
|
|
3277
|
+
for (const topic of devTopics) {
|
|
3278
|
+
if (contentLower.includes(topic)) {
|
|
3279
|
+
topics.push(topic);
|
|
3280
|
+
}
|
|
3281
|
+
}
|
|
3282
|
+
return [...new Set(topics)].slice(0, 10);
|
|
3283
|
+
}
|
|
3284
|
+
/**
|
|
3285
|
+
* Extract file paths from content
|
|
3286
|
+
*/
|
|
3287
|
+
extractFiles(content) {
|
|
3288
|
+
const filePatterns = [
|
|
3289
|
+
/(?:^|\s)([a-zA-Z0-9_\-./]+\.[a-zA-Z0-9]+)(?:\s|$|:)/gm,
|
|
3290
|
+
/['"](\.?\/[^'"]+\.[a-zA-Z0-9]+)['"]/g,
|
|
3291
|
+
/file[:\s]+([^\s,]+)/gi
|
|
3292
|
+
];
|
|
3293
|
+
const files = /* @__PURE__ */ new Set();
|
|
3294
|
+
for (const pattern of filePatterns) {
|
|
3295
|
+
let match;
|
|
3296
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
3297
|
+
const file = match[1];
|
|
3298
|
+
if (file && file.length > 3 && file.length < 100) {
|
|
3299
|
+
if (!file.match(/^(https?:|mailto:|ftp:)/i)) {
|
|
3300
|
+
files.add(file);
|
|
3301
|
+
}
|
|
3302
|
+
}
|
|
3303
|
+
}
|
|
3304
|
+
}
|
|
3305
|
+
return Array.from(files).slice(0, 10);
|
|
3306
|
+
}
|
|
3307
|
+
/**
|
|
3308
|
+
* Extract entity names from content (functions, classes, variables)
|
|
3309
|
+
*/
|
|
3310
|
+
extractEntities(content) {
|
|
3311
|
+
const entities = /* @__PURE__ */ new Set();
|
|
3312
|
+
const entityPatterns = [
|
|
3313
|
+
/\b(function|const|let|var|class|interface|type)\s+([a-zA-Z_][a-zA-Z0-9_]*)/g,
|
|
3314
|
+
/\b([A-Z][a-zA-Z0-9_]*(?:Component|Service|Store|Manager|Handler|Factory|Provider))\b/g,
|
|
3315
|
+
/\b(use[A-Z][a-zA-Z0-9_]*)\b/g
|
|
3316
|
+
// React hooks
|
|
3317
|
+
];
|
|
3318
|
+
for (const pattern of entityPatterns) {
|
|
3319
|
+
let match;
|
|
3320
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
3321
|
+
const entity = match[2] || match[1];
|
|
3322
|
+
if (entity && entity.length > 2) {
|
|
3323
|
+
entities.add(entity);
|
|
3324
|
+
}
|
|
3325
|
+
}
|
|
3326
|
+
}
|
|
3327
|
+
return Array.from(entities).slice(0, 20);
|
|
3328
|
+
}
|
|
3329
|
+
/**
|
|
3330
|
+
* Reset the last context (for testing or manual reset)
|
|
3331
|
+
*/
|
|
3332
|
+
resetLastContext() {
|
|
3333
|
+
this.lastContext = null;
|
|
3334
|
+
}
|
|
3335
|
+
/**
|
|
3336
|
+
* Get the last context snapshot
|
|
3337
|
+
*/
|
|
3338
|
+
getLastContext() {
|
|
3339
|
+
return this.lastContext;
|
|
3340
|
+
}
|
|
3341
|
+
};
|
|
3342
|
+
function createContinuityManager(eventStore, config) {
|
|
3343
|
+
return new ContinuityManager(eventStore, config);
|
|
3344
|
+
}
|
|
3345
|
+
|
|
3346
|
+
// src/services/memory-service.ts
|
|
3347
|
+
function normalizePath(projectPath) {
|
|
3348
|
+
const expanded = projectPath.startsWith("~") ? path.join(os.homedir(), projectPath.slice(1)) : projectPath;
|
|
3349
|
+
try {
|
|
3350
|
+
return fs.realpathSync(expanded);
|
|
3351
|
+
} catch {
|
|
3352
|
+
return path.resolve(expanded);
|
|
3353
|
+
}
|
|
3354
|
+
}
|
|
3355
|
+
function hashProjectPath(projectPath) {
|
|
3356
|
+
const normalizedPath = normalizePath(projectPath);
|
|
3357
|
+
return crypto2.createHash("sha256").update(normalizedPath).digest("hex").slice(0, 8);
|
|
3358
|
+
}
|
|
3359
|
+
function getProjectStoragePath(projectPath) {
|
|
3360
|
+
const hash = hashProjectPath(projectPath);
|
|
3361
|
+
return path.join(os.homedir(), ".claude-code", "memory", "projects", hash);
|
|
3362
|
+
}
|
|
3363
|
+
var REGISTRY_PATH = path.join(os.homedir(), ".claude-code", "memory", "session-registry.json");
|
|
3364
|
+
var SHARED_STORAGE_PATH = path.join(os.homedir(), ".claude-code", "memory", "shared");
|
|
3365
|
+
var MemoryService = class {
|
|
3366
|
+
eventStore;
|
|
3367
|
+
vectorStore;
|
|
3368
|
+
embedder;
|
|
3369
|
+
matcher;
|
|
3370
|
+
retriever;
|
|
3371
|
+
graduation;
|
|
3372
|
+
vectorWorker = null;
|
|
3373
|
+
initialized = false;
|
|
3374
|
+
// Endless Mode components
|
|
3375
|
+
workingSetStore = null;
|
|
3376
|
+
consolidatedStore = null;
|
|
3377
|
+
consolidationWorker = null;
|
|
3378
|
+
continuityManager = null;
|
|
3379
|
+
endlessMode = "session";
|
|
3380
|
+
// Shared Store components (cross-project knowledge)
|
|
3381
|
+
sharedEventStore = null;
|
|
3382
|
+
sharedStore = null;
|
|
3383
|
+
sharedVectorStore = null;
|
|
3384
|
+
sharedPromoter = null;
|
|
3385
|
+
sharedStoreConfig = null;
|
|
3386
|
+
projectHash = null;
|
|
3387
|
+
constructor(config) {
|
|
3388
|
+
const storagePath = this.expandPath(config.storagePath);
|
|
3389
|
+
if (!fs.existsSync(storagePath)) {
|
|
3390
|
+
fs.mkdirSync(storagePath, { recursive: true });
|
|
3391
|
+
}
|
|
3392
|
+
this.projectHash = config.projectHash || null;
|
|
3393
|
+
this.sharedStoreConfig = config.sharedStoreConfig ?? { enabled: true };
|
|
3394
|
+
this.eventStore = new EventStore(path.join(storagePath, "events.duckdb"));
|
|
3395
|
+
this.vectorStore = new VectorStore(path.join(storagePath, "vectors"));
|
|
3396
|
+
this.embedder = config.embeddingModel ? new Embedder(config.embeddingModel) : getDefaultEmbedder();
|
|
3397
|
+
this.matcher = getDefaultMatcher();
|
|
3398
|
+
this.retriever = createRetriever(
|
|
3399
|
+
this.eventStore,
|
|
3400
|
+
this.vectorStore,
|
|
3401
|
+
this.embedder,
|
|
3402
|
+
this.matcher
|
|
3403
|
+
);
|
|
3404
|
+
this.graduation = createGraduationPipeline(this.eventStore);
|
|
3405
|
+
}
|
|
3406
|
+
/**
|
|
3407
|
+
* Initialize all components
|
|
3408
|
+
*/
|
|
3409
|
+
async initialize() {
|
|
3410
|
+
if (this.initialized)
|
|
3411
|
+
return;
|
|
3412
|
+
await this.eventStore.initialize();
|
|
3413
|
+
await this.vectorStore.initialize();
|
|
3414
|
+
await this.embedder.initialize();
|
|
3415
|
+
this.vectorWorker = createVectorWorker(
|
|
3416
|
+
this.eventStore,
|
|
3417
|
+
this.vectorStore,
|
|
3418
|
+
this.embedder
|
|
3419
|
+
);
|
|
3420
|
+
this.vectorWorker.start();
|
|
3421
|
+
const savedMode = await this.eventStore.getEndlessConfig("mode");
|
|
3422
|
+
if (savedMode === "endless") {
|
|
3423
|
+
this.endlessMode = "endless";
|
|
3424
|
+
await this.initializeEndlessMode();
|
|
3425
|
+
}
|
|
3426
|
+
if (this.sharedStoreConfig?.enabled !== false) {
|
|
3427
|
+
await this.initializeSharedStore();
|
|
3428
|
+
}
|
|
3429
|
+
this.initialized = true;
|
|
3430
|
+
}
|
|
3431
|
+
/**
|
|
3432
|
+
* Initialize Shared Store components
|
|
3433
|
+
*/
|
|
3434
|
+
async initializeSharedStore() {
|
|
3435
|
+
const sharedPath = this.sharedStoreConfig?.sharedStoragePath ? this.expandPath(this.sharedStoreConfig.sharedStoragePath) : SHARED_STORAGE_PATH;
|
|
3436
|
+
if (!fs.existsSync(sharedPath)) {
|
|
3437
|
+
fs.mkdirSync(sharedPath, { recursive: true });
|
|
3438
|
+
}
|
|
3439
|
+
this.sharedEventStore = createSharedEventStore(
|
|
3440
|
+
path.join(sharedPath, "shared.duckdb")
|
|
3441
|
+
);
|
|
3442
|
+
await this.sharedEventStore.initialize();
|
|
3443
|
+
this.sharedStore = createSharedStore(this.sharedEventStore);
|
|
3444
|
+
this.sharedVectorStore = createSharedVectorStore(
|
|
3445
|
+
path.join(sharedPath, "vectors")
|
|
3446
|
+
);
|
|
3447
|
+
await this.sharedVectorStore.initialize();
|
|
3448
|
+
this.sharedPromoter = createSharedPromoter(
|
|
3449
|
+
this.sharedStore,
|
|
3450
|
+
this.sharedVectorStore,
|
|
3451
|
+
this.embedder,
|
|
3452
|
+
this.sharedStoreConfig || void 0
|
|
3453
|
+
);
|
|
3454
|
+
this.retriever.setSharedStores(this.sharedStore, this.sharedVectorStore);
|
|
3455
|
+
}
|
|
3456
|
+
/**
|
|
3457
|
+
* Start a new session
|
|
3458
|
+
*/
|
|
3459
|
+
async startSession(sessionId, projectPath) {
|
|
3460
|
+
await this.initialize();
|
|
3461
|
+
await this.eventStore.upsertSession({
|
|
3462
|
+
id: sessionId,
|
|
3463
|
+
startedAt: /* @__PURE__ */ new Date(),
|
|
3464
|
+
projectPath
|
|
3465
|
+
});
|
|
3466
|
+
}
|
|
3467
|
+
/**
|
|
3468
|
+
* End a session
|
|
3469
|
+
*/
|
|
3470
|
+
async endSession(sessionId, summary) {
|
|
3471
|
+
await this.initialize();
|
|
3472
|
+
await this.eventStore.upsertSession({
|
|
3473
|
+
id: sessionId,
|
|
3474
|
+
endedAt: /* @__PURE__ */ new Date(),
|
|
3475
|
+
summary
|
|
3476
|
+
});
|
|
3477
|
+
}
|
|
3478
|
+
/**
|
|
3479
|
+
* Store a user prompt
|
|
3480
|
+
*/
|
|
3481
|
+
async storeUserPrompt(sessionId, content, metadata) {
|
|
3482
|
+
await this.initialize();
|
|
3483
|
+
const result = await this.eventStore.append({
|
|
3484
|
+
eventType: "user_prompt",
|
|
3485
|
+
sessionId,
|
|
3486
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
3487
|
+
content,
|
|
3488
|
+
metadata
|
|
3489
|
+
});
|
|
3490
|
+
if (result.success && !result.isDuplicate) {
|
|
3491
|
+
await this.eventStore.enqueueForEmbedding(result.eventId, content);
|
|
3492
|
+
}
|
|
3493
|
+
return result;
|
|
3494
|
+
}
|
|
3495
|
+
/**
|
|
3496
|
+
* Store an agent response
|
|
3497
|
+
*/
|
|
3498
|
+
async storeAgentResponse(sessionId, content, metadata) {
|
|
3499
|
+
await this.initialize();
|
|
3500
|
+
const result = await this.eventStore.append({
|
|
3501
|
+
eventType: "agent_response",
|
|
3502
|
+
sessionId,
|
|
3503
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
3504
|
+
content,
|
|
3505
|
+
metadata
|
|
3506
|
+
});
|
|
3507
|
+
if (result.success && !result.isDuplicate) {
|
|
3508
|
+
await this.eventStore.enqueueForEmbedding(result.eventId, content);
|
|
3509
|
+
}
|
|
3510
|
+
return result;
|
|
3511
|
+
}
|
|
3512
|
+
/**
|
|
3513
|
+
* Store a session summary
|
|
3514
|
+
*/
|
|
3515
|
+
async storeSessionSummary(sessionId, summary) {
|
|
3516
|
+
await this.initialize();
|
|
3517
|
+
const result = await this.eventStore.append({
|
|
3518
|
+
eventType: "session_summary",
|
|
3519
|
+
sessionId,
|
|
3520
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
3521
|
+
content: summary
|
|
3522
|
+
});
|
|
3523
|
+
if (result.success && !result.isDuplicate) {
|
|
3524
|
+
await this.eventStore.enqueueForEmbedding(result.eventId, summary);
|
|
3525
|
+
}
|
|
3526
|
+
return result;
|
|
3527
|
+
}
|
|
3528
|
+
/**
|
|
3529
|
+
* Store a tool observation
|
|
3530
|
+
*/
|
|
3531
|
+
async storeToolObservation(sessionId, payload) {
|
|
3532
|
+
await this.initialize();
|
|
3533
|
+
const content = JSON.stringify(payload);
|
|
3534
|
+
const result = await this.eventStore.append({
|
|
3535
|
+
eventType: "tool_observation",
|
|
3536
|
+
sessionId,
|
|
3537
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
3538
|
+
content,
|
|
3539
|
+
metadata: {
|
|
3540
|
+
toolName: payload.toolName,
|
|
3541
|
+
success: payload.success
|
|
3542
|
+
}
|
|
3543
|
+
});
|
|
3544
|
+
if (result.success && !result.isDuplicate) {
|
|
3545
|
+
const embeddingContent = createToolObservationEmbedding(
|
|
3546
|
+
payload.toolName,
|
|
3547
|
+
payload.metadata || {},
|
|
3548
|
+
payload.success
|
|
3549
|
+
);
|
|
3550
|
+
await this.eventStore.enqueueForEmbedding(result.eventId, embeddingContent);
|
|
3551
|
+
}
|
|
3552
|
+
return result;
|
|
3553
|
+
}
|
|
3554
|
+
/**
|
|
3555
|
+
* Retrieve relevant memories for a query
|
|
3556
|
+
*/
|
|
3557
|
+
async retrieveMemories(query, options) {
|
|
3558
|
+
await this.initialize();
|
|
3559
|
+
if (this.vectorWorker) {
|
|
3560
|
+
await this.vectorWorker.processAll();
|
|
3561
|
+
}
|
|
3562
|
+
if (options?.includeShared && this.sharedStore) {
|
|
3563
|
+
return this.retriever.retrieveUnified(query, {
|
|
3564
|
+
...options,
|
|
3565
|
+
includeShared: true,
|
|
3566
|
+
projectHash: this.projectHash || void 0
|
|
3567
|
+
});
|
|
3568
|
+
}
|
|
3569
|
+
return this.retriever.retrieve(query, options);
|
|
3570
|
+
}
|
|
3571
|
+
/**
|
|
3572
|
+
* Get session history
|
|
3573
|
+
*/
|
|
3574
|
+
async getSessionHistory(sessionId) {
|
|
3575
|
+
await this.initialize();
|
|
3576
|
+
return this.eventStore.getSessionEvents(sessionId);
|
|
3577
|
+
}
|
|
3578
|
+
/**
|
|
3579
|
+
* Get recent events
|
|
3580
|
+
*/
|
|
3581
|
+
async getRecentEvents(limit = 100) {
|
|
3582
|
+
await this.initialize();
|
|
3583
|
+
return this.eventStore.getRecentEvents(limit);
|
|
3584
|
+
}
|
|
3585
|
+
/**
|
|
3586
|
+
* Get memory statistics
|
|
3587
|
+
*/
|
|
3588
|
+
async getStats() {
|
|
3589
|
+
await this.initialize();
|
|
3590
|
+
const recentEvents = await this.eventStore.getRecentEvents(1e4);
|
|
3591
|
+
const vectorCount = await this.vectorStore.count();
|
|
3592
|
+
const levelStats = await this.graduation.getStats();
|
|
3593
|
+
return {
|
|
3594
|
+
totalEvents: recentEvents.length,
|
|
3595
|
+
vectorCount,
|
|
3596
|
+
levelStats
|
|
3597
|
+
};
|
|
3598
|
+
}
|
|
3599
|
+
/**
|
|
3600
|
+
* Process pending embeddings
|
|
3601
|
+
*/
|
|
3602
|
+
async processPendingEmbeddings() {
|
|
3603
|
+
if (this.vectorWorker) {
|
|
3604
|
+
return this.vectorWorker.processAll();
|
|
3605
|
+
}
|
|
3606
|
+
return 0;
|
|
3607
|
+
}
|
|
3608
|
+
/**
|
|
3609
|
+
* Format retrieval results as context for Claude
|
|
3610
|
+
*/
|
|
3611
|
+
formatAsContext(result) {
|
|
3612
|
+
if (!result.context) {
|
|
3613
|
+
return "";
|
|
3614
|
+
}
|
|
3615
|
+
const confidence = result.matchResult.confidence;
|
|
3616
|
+
let header = "";
|
|
3617
|
+
if (confidence === "high") {
|
|
3618
|
+
header = "\u{1F3AF} **High-confidence memory match found:**\n\n";
|
|
3619
|
+
} else if (confidence === "suggested") {
|
|
3620
|
+
header = "\u{1F4A1} **Suggested memories (may be relevant):**\n\n";
|
|
3621
|
+
}
|
|
3622
|
+
return header + result.context;
|
|
3623
|
+
}
|
|
3624
|
+
// ============================================================
|
|
3625
|
+
// Shared Store Methods (Cross-Project Knowledge)
|
|
3626
|
+
// ============================================================
|
|
3627
|
+
/**
|
|
3628
|
+
* Check if shared store is enabled and initialized
|
|
3629
|
+
*/
|
|
3630
|
+
isSharedStoreEnabled() {
|
|
3631
|
+
return this.sharedStore !== null;
|
|
3632
|
+
}
|
|
3633
|
+
/**
|
|
3634
|
+
* Promote an entry to shared storage
|
|
3635
|
+
*/
|
|
3636
|
+
async promoteToShared(entry) {
|
|
3637
|
+
if (!this.sharedPromoter || !this.projectHash) {
|
|
3638
|
+
return {
|
|
3639
|
+
success: false,
|
|
3640
|
+
error: "Shared store not initialized or project hash not set"
|
|
3641
|
+
};
|
|
3642
|
+
}
|
|
3643
|
+
return this.sharedPromoter.promoteEntry(entry, this.projectHash);
|
|
3644
|
+
}
|
|
3645
|
+
/**
|
|
3646
|
+
* Get shared store statistics
|
|
3647
|
+
*/
|
|
3648
|
+
async getSharedStoreStats() {
|
|
3649
|
+
if (!this.sharedStore)
|
|
3650
|
+
return null;
|
|
3651
|
+
return this.sharedStore.getStats();
|
|
3652
|
+
}
|
|
3653
|
+
/**
|
|
3654
|
+
* Search shared troubleshooting entries
|
|
3655
|
+
*/
|
|
3656
|
+
async searchShared(query, options) {
|
|
3657
|
+
if (!this.sharedStore)
|
|
3658
|
+
return [];
|
|
3659
|
+
return this.sharedStore.search(query, options);
|
|
3660
|
+
}
|
|
3661
|
+
/**
|
|
3662
|
+
* Get project hash for this service
|
|
3663
|
+
*/
|
|
3664
|
+
getProjectHash() {
|
|
3665
|
+
return this.projectHash;
|
|
3666
|
+
}
|
|
3667
|
+
// ============================================================
|
|
3668
|
+
// Endless Mode Methods
|
|
3669
|
+
// ============================================================
|
|
3670
|
+
/**
|
|
3671
|
+
* Get the default endless mode config
|
|
3672
|
+
*/
|
|
3673
|
+
getDefaultEndlessConfig() {
|
|
3674
|
+
return {
|
|
3675
|
+
enabled: true,
|
|
3676
|
+
workingSet: {
|
|
3677
|
+
maxEvents: 100,
|
|
3678
|
+
timeWindowHours: 24,
|
|
3679
|
+
minRelevanceScore: 0.5
|
|
3680
|
+
},
|
|
3681
|
+
consolidation: {
|
|
3682
|
+
triggerIntervalMs: 36e5,
|
|
3683
|
+
// 1 hour
|
|
3684
|
+
triggerEventCount: 100,
|
|
3685
|
+
triggerIdleMs: 18e5,
|
|
3686
|
+
// 30 minutes
|
|
3687
|
+
useLLMSummarization: false
|
|
3688
|
+
},
|
|
3689
|
+
continuity: {
|
|
3690
|
+
minScoreForSeamless: 0.7,
|
|
3691
|
+
topicDecayHours: 48
|
|
3692
|
+
}
|
|
3693
|
+
};
|
|
3694
|
+
}
|
|
3695
|
+
/**
|
|
3696
|
+
* Initialize Endless Mode components
|
|
3697
|
+
*/
|
|
3698
|
+
async initializeEndlessMode() {
|
|
3699
|
+
const config = await this.getEndlessConfig();
|
|
3700
|
+
this.workingSetStore = createWorkingSetStore(this.eventStore, config);
|
|
3701
|
+
this.consolidatedStore = createConsolidatedStore(this.eventStore);
|
|
3702
|
+
this.consolidationWorker = createConsolidationWorker(
|
|
3703
|
+
this.workingSetStore,
|
|
3704
|
+
this.consolidatedStore,
|
|
3705
|
+
config
|
|
3706
|
+
);
|
|
3707
|
+
this.continuityManager = createContinuityManager(this.eventStore, config);
|
|
3708
|
+
this.consolidationWorker.start();
|
|
3709
|
+
}
|
|
3710
|
+
/**
|
|
3711
|
+
* Get Endless Mode configuration
|
|
3712
|
+
*/
|
|
3713
|
+
async getEndlessConfig() {
|
|
3714
|
+
const savedConfig = await this.eventStore.getEndlessConfig("config");
|
|
3715
|
+
return savedConfig || this.getDefaultEndlessConfig();
|
|
3716
|
+
}
|
|
3717
|
+
/**
|
|
3718
|
+
* Set Endless Mode configuration
|
|
3719
|
+
*/
|
|
3720
|
+
async setEndlessConfig(config) {
|
|
3721
|
+
const current = await this.getEndlessConfig();
|
|
3722
|
+
const merged = { ...current, ...config };
|
|
3723
|
+
await this.eventStore.setEndlessConfig("config", merged);
|
|
3724
|
+
}
|
|
3725
|
+
/**
|
|
3726
|
+
* Set memory mode (session or endless)
|
|
3727
|
+
*/
|
|
3728
|
+
async setMode(mode) {
|
|
3729
|
+
await this.initialize();
|
|
3730
|
+
if (mode === this.endlessMode)
|
|
3731
|
+
return;
|
|
3732
|
+
this.endlessMode = mode;
|
|
3733
|
+
await this.eventStore.setEndlessConfig("mode", mode);
|
|
3734
|
+
if (mode === "endless") {
|
|
3735
|
+
await this.initializeEndlessMode();
|
|
3736
|
+
} else {
|
|
3737
|
+
if (this.consolidationWorker) {
|
|
3738
|
+
this.consolidationWorker.stop();
|
|
3739
|
+
this.consolidationWorker = null;
|
|
3740
|
+
}
|
|
3741
|
+
this.workingSetStore = null;
|
|
3742
|
+
this.consolidatedStore = null;
|
|
3743
|
+
this.continuityManager = null;
|
|
3744
|
+
}
|
|
3745
|
+
}
|
|
3746
|
+
/**
|
|
3747
|
+
* Get current memory mode
|
|
3748
|
+
*/
|
|
3749
|
+
getMode() {
|
|
3750
|
+
return this.endlessMode;
|
|
3751
|
+
}
|
|
3752
|
+
/**
|
|
3753
|
+
* Check if endless mode is active
|
|
3754
|
+
*/
|
|
3755
|
+
isEndlessModeActive() {
|
|
3756
|
+
return this.endlessMode === "endless";
|
|
3757
|
+
}
|
|
3758
|
+
/**
|
|
3759
|
+
* Add event to Working Set (Endless Mode)
|
|
3760
|
+
*/
|
|
3761
|
+
async addToWorkingSet(eventId, relevanceScore) {
|
|
3762
|
+
if (!this.workingSetStore)
|
|
3763
|
+
return;
|
|
3764
|
+
await this.workingSetStore.add(eventId, relevanceScore);
|
|
3765
|
+
}
|
|
3766
|
+
/**
|
|
3767
|
+
* Get the current Working Set
|
|
3768
|
+
*/
|
|
3769
|
+
async getWorkingSet() {
|
|
3770
|
+
if (!this.workingSetStore)
|
|
3771
|
+
return null;
|
|
3772
|
+
return this.workingSetStore.get();
|
|
3773
|
+
}
|
|
3774
|
+
/**
|
|
3775
|
+
* Search consolidated memories
|
|
3776
|
+
*/
|
|
3777
|
+
async searchConsolidated(query, options) {
|
|
3778
|
+
if (!this.consolidatedStore)
|
|
3779
|
+
return [];
|
|
3780
|
+
return this.consolidatedStore.search(query, options);
|
|
3781
|
+
}
|
|
3782
|
+
/**
|
|
3783
|
+
* Get all consolidated memories
|
|
3784
|
+
*/
|
|
3785
|
+
async getConsolidatedMemories(limit) {
|
|
3786
|
+
if (!this.consolidatedStore)
|
|
3787
|
+
return [];
|
|
3788
|
+
return this.consolidatedStore.getAll({ limit });
|
|
3789
|
+
}
|
|
3790
|
+
/**
|
|
3791
|
+
* Calculate continuity score for current context
|
|
3792
|
+
*/
|
|
3793
|
+
async calculateContinuity(content, metadata) {
|
|
3794
|
+
if (!this.continuityManager)
|
|
3795
|
+
return null;
|
|
3796
|
+
const snapshot = this.continuityManager.createSnapshot(
|
|
3797
|
+
crypto2.randomUUID(),
|
|
3798
|
+
content,
|
|
3799
|
+
metadata
|
|
3800
|
+
);
|
|
3801
|
+
return this.continuityManager.calculateScore(snapshot);
|
|
3802
|
+
}
|
|
3803
|
+
/**
|
|
3804
|
+
* Record activity (for consolidation idle trigger)
|
|
3805
|
+
*/
|
|
3806
|
+
recordActivity() {
|
|
3807
|
+
if (this.consolidationWorker) {
|
|
3808
|
+
this.consolidationWorker.recordActivity();
|
|
3809
|
+
}
|
|
3810
|
+
}
|
|
3811
|
+
/**
|
|
3812
|
+
* Force a consolidation run
|
|
3813
|
+
*/
|
|
3814
|
+
async forceConsolidation() {
|
|
3815
|
+
if (!this.consolidationWorker)
|
|
3816
|
+
return 0;
|
|
3817
|
+
return this.consolidationWorker.forceRun();
|
|
3818
|
+
}
|
|
3819
|
+
/**
|
|
3820
|
+
* Get Endless Mode status
|
|
3821
|
+
*/
|
|
3822
|
+
async getEndlessModeStatus() {
|
|
3823
|
+
await this.initialize();
|
|
3824
|
+
let workingSetSize = 0;
|
|
3825
|
+
let continuityScore = 0.5;
|
|
3826
|
+
let consolidatedCount = 0;
|
|
3827
|
+
let lastConsolidation = null;
|
|
3828
|
+
if (this.workingSetStore) {
|
|
3829
|
+
workingSetSize = await this.workingSetStore.count();
|
|
3830
|
+
const workingSet = await this.workingSetStore.get();
|
|
3831
|
+
continuityScore = workingSet.continuityScore;
|
|
3832
|
+
}
|
|
3833
|
+
if (this.consolidatedStore) {
|
|
3834
|
+
consolidatedCount = await this.consolidatedStore.count();
|
|
3835
|
+
lastConsolidation = await this.consolidatedStore.getLastConsolidationTime();
|
|
3836
|
+
}
|
|
3837
|
+
return {
|
|
3838
|
+
mode: this.endlessMode,
|
|
3839
|
+
workingSetSize,
|
|
3840
|
+
continuityScore,
|
|
3841
|
+
consolidatedCount,
|
|
3842
|
+
lastConsolidation
|
|
3843
|
+
};
|
|
3844
|
+
}
|
|
3845
|
+
/**
|
|
3846
|
+
* Format Endless Mode context for Claude
|
|
3847
|
+
*/
|
|
3848
|
+
async formatEndlessContext(query) {
|
|
3849
|
+
if (!this.isEndlessModeActive()) {
|
|
3850
|
+
return "";
|
|
3851
|
+
}
|
|
3852
|
+
const workingSet = await this.getWorkingSet();
|
|
3853
|
+
const consolidated = await this.searchConsolidated(query, { topK: 3 });
|
|
3854
|
+
const continuity = await this.calculateContinuity(query);
|
|
3855
|
+
const parts = [];
|
|
3856
|
+
if (continuity) {
|
|
3857
|
+
const statusEmoji = continuity.transitionType === "seamless" ? "\u{1F517}" : continuity.transitionType === "topic_shift" ? "\u21AA\uFE0F" : "\u{1F195}";
|
|
3858
|
+
parts.push(`${statusEmoji} Context: ${continuity.transitionType} (score: ${continuity.score.toFixed(2)})`);
|
|
3859
|
+
}
|
|
3860
|
+
if (workingSet && workingSet.recentEvents.length > 0) {
|
|
3861
|
+
parts.push("\n## Recent Context (Working Set)");
|
|
3862
|
+
const recent = workingSet.recentEvents.slice(0, 5);
|
|
3863
|
+
for (const event of recent) {
|
|
3864
|
+
const preview = event.content.slice(0, 80) + (event.content.length > 80 ? "..." : "");
|
|
3865
|
+
const time = event.timestamp.toLocaleTimeString();
|
|
3866
|
+
parts.push(`- ${time} [${event.eventType}] ${preview}`);
|
|
3867
|
+
}
|
|
3868
|
+
}
|
|
3869
|
+
if (consolidated.length > 0) {
|
|
3870
|
+
parts.push("\n## Related Knowledge (Consolidated)");
|
|
3871
|
+
for (const memory of consolidated) {
|
|
3872
|
+
parts.push(`- ${memory.topics.slice(0, 3).join(", ")}: ${memory.summary.slice(0, 100)}...`);
|
|
3873
|
+
}
|
|
3874
|
+
}
|
|
3875
|
+
return parts.join("\n");
|
|
3876
|
+
}
|
|
3877
|
+
/**
|
|
3878
|
+
* Shutdown service
|
|
3879
|
+
*/
|
|
3880
|
+
async shutdown() {
|
|
3881
|
+
if (this.consolidationWorker) {
|
|
3882
|
+
this.consolidationWorker.stop();
|
|
3883
|
+
}
|
|
3884
|
+
if (this.vectorWorker) {
|
|
3885
|
+
this.vectorWorker.stop();
|
|
3886
|
+
}
|
|
3887
|
+
if (this.sharedEventStore) {
|
|
3888
|
+
await this.sharedEventStore.close();
|
|
3889
|
+
}
|
|
3890
|
+
await this.eventStore.close();
|
|
3891
|
+
}
|
|
3892
|
+
/**
|
|
3893
|
+
* Expand ~ to home directory
|
|
3894
|
+
*/
|
|
3895
|
+
expandPath(p) {
|
|
3896
|
+
if (p.startsWith("~")) {
|
|
3897
|
+
return path.join(os.homedir(), p.slice(1));
|
|
3898
|
+
}
|
|
3899
|
+
return p;
|
|
3900
|
+
}
|
|
3901
|
+
};
|
|
3902
|
+
var serviceCache = /* @__PURE__ */ new Map();
|
|
3903
|
+
var GLOBAL_KEY = "__global__";
|
|
3904
|
+
function getDefaultMemoryService() {
|
|
3905
|
+
if (!serviceCache.has(GLOBAL_KEY)) {
|
|
3906
|
+
serviceCache.set(GLOBAL_KEY, new MemoryService({
|
|
3907
|
+
storagePath: "~/.claude-code/memory"
|
|
3908
|
+
}));
|
|
3909
|
+
}
|
|
3910
|
+
return serviceCache.get(GLOBAL_KEY);
|
|
3911
|
+
}
|
|
3912
|
+
function getMemoryServiceForProject(projectPath, sharedStoreConfig) {
|
|
3913
|
+
const hash = hashProjectPath(projectPath);
|
|
3914
|
+
if (!serviceCache.has(hash)) {
|
|
3915
|
+
const storagePath = getProjectStoragePath(projectPath);
|
|
3916
|
+
serviceCache.set(hash, new MemoryService({
|
|
3917
|
+
storagePath,
|
|
3918
|
+
projectHash: hash,
|
|
3919
|
+
sharedStoreConfig
|
|
3920
|
+
}));
|
|
3921
|
+
}
|
|
3922
|
+
return serviceCache.get(hash);
|
|
3923
|
+
}
|
|
3924
|
+
|
|
3925
|
+
// src/server/api/sessions.ts
|
|
3926
|
+
var sessionsRouter = new Hono();
|
|
3927
|
+
sessionsRouter.get("/", async (c) => {
|
|
3928
|
+
const page = parseInt(c.req.query("page") || "1", 10);
|
|
3929
|
+
const pageSize = parseInt(c.req.query("pageSize") || "20", 10);
|
|
3930
|
+
try {
|
|
3931
|
+
const memoryService = getDefaultMemoryService();
|
|
3932
|
+
await memoryService.initialize();
|
|
3933
|
+
const recentEvents = await memoryService.getRecentEvents(1e3);
|
|
3934
|
+
const sessionMap = /* @__PURE__ */ new Map();
|
|
3935
|
+
for (const event of recentEvents) {
|
|
3936
|
+
const existing = sessionMap.get(event.sessionId);
|
|
3937
|
+
if (!existing) {
|
|
3938
|
+
sessionMap.set(event.sessionId, {
|
|
3939
|
+
id: event.sessionId,
|
|
3940
|
+
startedAt: event.timestamp,
|
|
3941
|
+
eventCount: 1,
|
|
3942
|
+
lastEventAt: event.timestamp
|
|
3943
|
+
});
|
|
3944
|
+
} else {
|
|
3945
|
+
existing.eventCount++;
|
|
3946
|
+
if (event.timestamp < existing.startedAt) {
|
|
3947
|
+
existing.startedAt = event.timestamp;
|
|
3948
|
+
}
|
|
3949
|
+
if (event.timestamp > existing.lastEventAt) {
|
|
3950
|
+
existing.lastEventAt = event.timestamp;
|
|
3951
|
+
}
|
|
3952
|
+
}
|
|
3953
|
+
}
|
|
3954
|
+
const sessions = Array.from(sessionMap.values()).sort((a, b) => b.lastEventAt.getTime() - a.lastEventAt.getTime());
|
|
3955
|
+
const total = sessions.length;
|
|
3956
|
+
const start = (page - 1) * pageSize;
|
|
3957
|
+
const end = start + pageSize;
|
|
3958
|
+
const paginatedSessions = sessions.slice(start, end);
|
|
3959
|
+
return c.json({
|
|
3960
|
+
sessions: paginatedSessions,
|
|
3961
|
+
total,
|
|
3962
|
+
page,
|
|
3963
|
+
pageSize,
|
|
3964
|
+
hasMore: end < total
|
|
3965
|
+
});
|
|
3966
|
+
} catch (error) {
|
|
3967
|
+
return c.json({ error: error.message }, 500);
|
|
3968
|
+
}
|
|
3969
|
+
});
|
|
3970
|
+
sessionsRouter.get("/:id", async (c) => {
|
|
3971
|
+
const { id } = c.req.param();
|
|
3972
|
+
try {
|
|
3973
|
+
const memoryService = getDefaultMemoryService();
|
|
3974
|
+
await memoryService.initialize();
|
|
3975
|
+
const events = await memoryService.getSessionHistory(id);
|
|
3976
|
+
if (events.length === 0) {
|
|
3977
|
+
return c.json({ error: "Session not found" }, 404);
|
|
3978
|
+
}
|
|
3979
|
+
const session = {
|
|
3980
|
+
id,
|
|
3981
|
+
startedAt: events[0].timestamp,
|
|
3982
|
+
endedAt: events[events.length - 1].timestamp,
|
|
3983
|
+
eventCount: events.length
|
|
3984
|
+
};
|
|
3985
|
+
const eventsByType = {
|
|
3986
|
+
user_prompt: events.filter((e) => e.eventType === "user_prompt").length,
|
|
3987
|
+
agent_response: events.filter((e) => e.eventType === "agent_response").length,
|
|
3988
|
+
tool_observation: events.filter((e) => e.eventType === "tool_observation").length
|
|
3989
|
+
};
|
|
3990
|
+
return c.json({
|
|
3991
|
+
session,
|
|
3992
|
+
events: events.slice(0, 100).map((e) => ({
|
|
3993
|
+
id: e.id,
|
|
3994
|
+
eventType: e.eventType,
|
|
3995
|
+
timestamp: e.timestamp,
|
|
3996
|
+
preview: e.content.slice(0, 200) + (e.content.length > 200 ? "..." : "")
|
|
3997
|
+
})),
|
|
3998
|
+
stats: eventsByType
|
|
3999
|
+
});
|
|
4000
|
+
} catch (error) {
|
|
4001
|
+
return c.json({ error: error.message }, 500);
|
|
4002
|
+
}
|
|
4003
|
+
});
|
|
4004
|
+
|
|
4005
|
+
// src/server/api/events.ts
|
|
4006
|
+
import { Hono as Hono2 } from "hono";
|
|
4007
|
+
var eventsRouter = new Hono2();
|
|
4008
|
+
eventsRouter.get("/", async (c) => {
|
|
4009
|
+
const sessionId = c.req.query("sessionId");
|
|
4010
|
+
const eventType = c.req.query("type");
|
|
4011
|
+
const limit = parseInt(c.req.query("limit") || "100", 10);
|
|
4012
|
+
const offset = parseInt(c.req.query("offset") || "0", 10);
|
|
4013
|
+
try {
|
|
4014
|
+
const memoryService = getDefaultMemoryService();
|
|
4015
|
+
await memoryService.initialize();
|
|
4016
|
+
let events = await memoryService.getRecentEvents(limit + offset + 1e3);
|
|
4017
|
+
if (sessionId) {
|
|
4018
|
+
events = events.filter((e) => e.sessionId === sessionId);
|
|
4019
|
+
}
|
|
4020
|
+
if (eventType) {
|
|
4021
|
+
events = events.filter((e) => e.eventType === eventType);
|
|
4022
|
+
}
|
|
4023
|
+
const total = events.length;
|
|
4024
|
+
events = events.slice(offset, offset + limit);
|
|
4025
|
+
return c.json({
|
|
4026
|
+
events: events.map((e) => ({
|
|
4027
|
+
id: e.id,
|
|
4028
|
+
eventType: e.eventType,
|
|
4029
|
+
timestamp: e.timestamp,
|
|
4030
|
+
sessionId: e.sessionId,
|
|
4031
|
+
preview: e.content.slice(0, 200) + (e.content.length > 200 ? "..." : ""),
|
|
4032
|
+
contentLength: e.content.length
|
|
4033
|
+
})),
|
|
4034
|
+
total,
|
|
4035
|
+
limit,
|
|
4036
|
+
offset,
|
|
4037
|
+
hasMore: offset + limit < total
|
|
4038
|
+
});
|
|
4039
|
+
} catch (error) {
|
|
4040
|
+
return c.json({ error: error.message }, 500);
|
|
4041
|
+
}
|
|
4042
|
+
});
|
|
4043
|
+
eventsRouter.get("/:id", async (c) => {
|
|
4044
|
+
const { id } = c.req.param();
|
|
4045
|
+
try {
|
|
4046
|
+
const memoryService = getDefaultMemoryService();
|
|
4047
|
+
await memoryService.initialize();
|
|
4048
|
+
const recentEvents = await memoryService.getRecentEvents(1e4);
|
|
4049
|
+
const event = recentEvents.find((e) => e.id === id);
|
|
4050
|
+
if (!event) {
|
|
4051
|
+
return c.json({ error: "Event not found" }, 404);
|
|
4052
|
+
}
|
|
4053
|
+
const sessionEvents = recentEvents.filter((e) => e.sessionId === event.sessionId).sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
|
4054
|
+
const eventIndex = sessionEvents.findIndex((e) => e.id === id);
|
|
4055
|
+
const start = Math.max(0, eventIndex - 2);
|
|
4056
|
+
const end = Math.min(sessionEvents.length, eventIndex + 3);
|
|
4057
|
+
const context = sessionEvents.slice(start, end).filter((e) => e.id !== id);
|
|
4058
|
+
return c.json({
|
|
4059
|
+
event: {
|
|
4060
|
+
id: event.id,
|
|
4061
|
+
eventType: event.eventType,
|
|
4062
|
+
timestamp: event.timestamp,
|
|
4063
|
+
sessionId: event.sessionId,
|
|
4064
|
+
content: event.content,
|
|
4065
|
+
metadata: event.metadata
|
|
4066
|
+
},
|
|
4067
|
+
context: context.map((e) => ({
|
|
4068
|
+
id: e.id,
|
|
4069
|
+
eventType: e.eventType,
|
|
4070
|
+
timestamp: e.timestamp,
|
|
4071
|
+
preview: e.content.slice(0, 100) + (e.content.length > 100 ? "..." : "")
|
|
4072
|
+
}))
|
|
4073
|
+
});
|
|
4074
|
+
} catch (error) {
|
|
4075
|
+
return c.json({ error: error.message }, 500);
|
|
4076
|
+
}
|
|
4077
|
+
});
|
|
4078
|
+
|
|
4079
|
+
// src/server/api/search.ts
|
|
4080
|
+
import { Hono as Hono3 } from "hono";
|
|
4081
|
+
var searchRouter = new Hono3();
|
|
4082
|
+
searchRouter.post("/", async (c) => {
|
|
4083
|
+
try {
|
|
4084
|
+
const body = await c.req.json();
|
|
4085
|
+
if (!body.query) {
|
|
4086
|
+
return c.json({ error: "Query is required" }, 400);
|
|
4087
|
+
}
|
|
4088
|
+
const memoryService = getDefaultMemoryService();
|
|
4089
|
+
await memoryService.initialize();
|
|
4090
|
+
const startTime = Date.now();
|
|
4091
|
+
const result = await memoryService.retrieveMemories(body.query, {
|
|
4092
|
+
topK: body.options?.topK ?? 10,
|
|
4093
|
+
minScore: body.options?.minScore ?? 0.7,
|
|
4094
|
+
sessionId: body.options?.sessionId
|
|
4095
|
+
});
|
|
4096
|
+
const searchTime = Date.now() - startTime;
|
|
4097
|
+
return c.json({
|
|
4098
|
+
results: result.memories.map((m) => ({
|
|
4099
|
+
id: m.event.id,
|
|
4100
|
+
eventType: m.event.eventType,
|
|
4101
|
+
timestamp: m.event.timestamp,
|
|
4102
|
+
sessionId: m.event.sessionId,
|
|
4103
|
+
score: m.score,
|
|
4104
|
+
content: m.event.content,
|
|
4105
|
+
preview: m.event.content.slice(0, 200) + (m.event.content.length > 200 ? "..." : ""),
|
|
4106
|
+
context: m.sessionContext
|
|
4107
|
+
})),
|
|
4108
|
+
meta: {
|
|
4109
|
+
totalMatches: result.memories.length,
|
|
4110
|
+
searchTime,
|
|
4111
|
+
confidence: result.matchResult.confidence,
|
|
4112
|
+
totalTokens: result.totalTokens
|
|
4113
|
+
}
|
|
4114
|
+
});
|
|
4115
|
+
} catch (error) {
|
|
4116
|
+
return c.json({ error: error.message }, 500);
|
|
4117
|
+
}
|
|
4118
|
+
});
|
|
4119
|
+
searchRouter.get("/", async (c) => {
|
|
4120
|
+
const query = c.req.query("q");
|
|
4121
|
+
if (!query) {
|
|
4122
|
+
return c.json({ error: 'Query parameter "q" is required' }, 400);
|
|
4123
|
+
}
|
|
4124
|
+
const topK = parseInt(c.req.query("topK") || "5", 10);
|
|
4125
|
+
try {
|
|
4126
|
+
const memoryService = getDefaultMemoryService();
|
|
4127
|
+
await memoryService.initialize();
|
|
4128
|
+
const result = await memoryService.retrieveMemories(query, { topK });
|
|
4129
|
+
return c.json({
|
|
4130
|
+
results: result.memories.map((m) => ({
|
|
4131
|
+
id: m.event.id,
|
|
4132
|
+
eventType: m.event.eventType,
|
|
4133
|
+
timestamp: m.event.timestamp,
|
|
4134
|
+
score: m.score,
|
|
4135
|
+
preview: m.event.content.slice(0, 200) + (m.event.content.length > 200 ? "..." : "")
|
|
4136
|
+
})),
|
|
4137
|
+
meta: {
|
|
4138
|
+
totalMatches: result.memories.length,
|
|
4139
|
+
confidence: result.matchResult.confidence
|
|
4140
|
+
}
|
|
4141
|
+
});
|
|
4142
|
+
} catch (error) {
|
|
4143
|
+
return c.json({ error: error.message }, 500);
|
|
4144
|
+
}
|
|
4145
|
+
});
|
|
4146
|
+
|
|
4147
|
+
// src/server/api/stats.ts
|
|
4148
|
+
import { Hono as Hono4 } from "hono";
|
|
4149
|
+
var statsRouter = new Hono4();
|
|
4150
|
+
statsRouter.get("/shared", async (c) => {
|
|
4151
|
+
try {
|
|
4152
|
+
const memoryService = getDefaultMemoryService();
|
|
4153
|
+
await memoryService.initialize();
|
|
4154
|
+
const sharedStats = await memoryService.getSharedStoreStats();
|
|
4155
|
+
return c.json({
|
|
4156
|
+
troubleshooting: sharedStats?.troubleshooting || 0,
|
|
4157
|
+
bestPractices: sharedStats?.bestPractices || 0,
|
|
4158
|
+
commonErrors: sharedStats?.commonErrors || 0,
|
|
4159
|
+
totalUsageCount: sharedStats?.totalUsageCount || 0,
|
|
4160
|
+
lastUpdated: sharedStats?.lastUpdated || null
|
|
4161
|
+
});
|
|
4162
|
+
} catch (error) {
|
|
4163
|
+
return c.json({
|
|
4164
|
+
troubleshooting: 0,
|
|
4165
|
+
bestPractices: 0,
|
|
4166
|
+
commonErrors: 0,
|
|
4167
|
+
totalUsageCount: 0,
|
|
4168
|
+
lastUpdated: null
|
|
4169
|
+
});
|
|
4170
|
+
}
|
|
4171
|
+
});
|
|
4172
|
+
statsRouter.get("/endless", async (c) => {
|
|
4173
|
+
try {
|
|
4174
|
+
const projectPath = c.req.query("project") || process.cwd();
|
|
4175
|
+
const memoryService = getMemoryServiceForProject(projectPath);
|
|
4176
|
+
await memoryService.initialize();
|
|
4177
|
+
const status = await memoryService.getEndlessModeStatus();
|
|
4178
|
+
return c.json({
|
|
4179
|
+
mode: status.mode,
|
|
4180
|
+
continuityScore: status.continuityScore,
|
|
4181
|
+
workingSetSize: status.workingSetSize,
|
|
4182
|
+
consolidatedCount: status.consolidatedCount,
|
|
4183
|
+
lastConsolidation: status.lastConsolidation?.toISOString() || null
|
|
4184
|
+
});
|
|
4185
|
+
} catch (error) {
|
|
4186
|
+
return c.json({
|
|
4187
|
+
mode: "session",
|
|
4188
|
+
continuityScore: 0,
|
|
4189
|
+
workingSetSize: 0,
|
|
4190
|
+
consolidatedCount: 0,
|
|
4191
|
+
lastConsolidation: null
|
|
4192
|
+
});
|
|
4193
|
+
}
|
|
4194
|
+
});
|
|
4195
|
+
statsRouter.get("/", async (c) => {
|
|
4196
|
+
try {
|
|
4197
|
+
const memoryService = getDefaultMemoryService();
|
|
4198
|
+
await memoryService.initialize();
|
|
4199
|
+
const stats = await memoryService.getStats();
|
|
4200
|
+
const recentEvents = await memoryService.getRecentEvents(1e4);
|
|
4201
|
+
const eventsByType = recentEvents.reduce((acc, e) => {
|
|
4202
|
+
acc[e.eventType] = (acc[e.eventType] || 0) + 1;
|
|
4203
|
+
return acc;
|
|
4204
|
+
}, {});
|
|
4205
|
+
const uniqueSessions = new Set(recentEvents.map((e) => e.sessionId));
|
|
4206
|
+
const now = /* @__PURE__ */ new Date();
|
|
4207
|
+
const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1e3);
|
|
4208
|
+
const eventsByDay = recentEvents.filter((e) => e.timestamp >= sevenDaysAgo).reduce((acc, e) => {
|
|
4209
|
+
const day = e.timestamp.toISOString().split("T")[0];
|
|
4210
|
+
acc[day] = (acc[day] || 0) + 1;
|
|
4211
|
+
return acc;
|
|
4212
|
+
}, {});
|
|
4213
|
+
return c.json({
|
|
4214
|
+
storage: {
|
|
4215
|
+
eventCount: stats.totalEvents,
|
|
4216
|
+
vectorCount: stats.vectorCount
|
|
4217
|
+
},
|
|
4218
|
+
sessions: {
|
|
4219
|
+
total: uniqueSessions.size
|
|
4220
|
+
},
|
|
4221
|
+
eventsByType,
|
|
4222
|
+
activity: {
|
|
4223
|
+
daily: eventsByDay,
|
|
4224
|
+
total7Days: recentEvents.filter((e) => e.timestamp >= sevenDaysAgo).length
|
|
4225
|
+
},
|
|
4226
|
+
memory: {
|
|
4227
|
+
heapUsed: Math.round(process.memoryUsage().heapUsed / 1024 / 1024),
|
|
4228
|
+
heapTotal: Math.round(process.memoryUsage().heapTotal / 1024 / 1024)
|
|
4229
|
+
},
|
|
4230
|
+
levelStats: stats.levelStats
|
|
4231
|
+
});
|
|
4232
|
+
} catch (error) {
|
|
4233
|
+
return c.json({ error: error.message }, 500);
|
|
4234
|
+
}
|
|
4235
|
+
});
|
|
4236
|
+
statsRouter.get("/timeline", async (c) => {
|
|
4237
|
+
const days = parseInt(c.req.query("days") || "7", 10);
|
|
4238
|
+
try {
|
|
4239
|
+
const memoryService = getDefaultMemoryService();
|
|
4240
|
+
await memoryService.initialize();
|
|
4241
|
+
const recentEvents = await memoryService.getRecentEvents(1e4);
|
|
4242
|
+
const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1e3);
|
|
4243
|
+
const filteredEvents = recentEvents.filter((e) => e.timestamp >= cutoff);
|
|
4244
|
+
const daily = filteredEvents.reduce((acc, e) => {
|
|
4245
|
+
const day = e.timestamp.toISOString().split("T")[0];
|
|
4246
|
+
if (!acc[day]) {
|
|
4247
|
+
acc[day] = { date: day, total: 0, prompts: 0, responses: 0, tools: 0 };
|
|
4248
|
+
}
|
|
4249
|
+
acc[day].total++;
|
|
4250
|
+
if (e.eventType === "user_prompt")
|
|
4251
|
+
acc[day].prompts++;
|
|
4252
|
+
if (e.eventType === "agent_response")
|
|
4253
|
+
acc[day].responses++;
|
|
4254
|
+
if (e.eventType === "tool_observation")
|
|
4255
|
+
acc[day].tools++;
|
|
4256
|
+
return acc;
|
|
4257
|
+
}, {});
|
|
4258
|
+
return c.json({
|
|
4259
|
+
days,
|
|
4260
|
+
daily: Object.values(daily).sort((a, b) => a.date.localeCompare(b.date))
|
|
4261
|
+
});
|
|
4262
|
+
} catch (error) {
|
|
4263
|
+
return c.json({ error: error.message }, 500);
|
|
4264
|
+
}
|
|
4265
|
+
});
|
|
4266
|
+
|
|
4267
|
+
// src/server/api/citations.ts
|
|
4268
|
+
import { Hono as Hono5 } from "hono";
|
|
4269
|
+
|
|
4270
|
+
// src/core/citation-generator.ts
|
|
4271
|
+
import { createHash as createHash3 } from "crypto";
|
|
4272
|
+
var ID_LENGTH = 6;
|
|
4273
|
+
var CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
4274
|
+
function generateCitationId(eventId) {
|
|
4275
|
+
const hash = createHash3("sha256").update(eventId).digest();
|
|
4276
|
+
let id = "";
|
|
4277
|
+
for (let i = 0; i < ID_LENGTH; i++) {
|
|
4278
|
+
id += CHARSET[hash[i] % CHARSET.length];
|
|
4279
|
+
}
|
|
4280
|
+
return id;
|
|
4281
|
+
}
|
|
4282
|
+
function parseCitationId(formatted) {
|
|
4283
|
+
const match = formatted.match(/\[?mem:([A-Za-z0-9]{6})\]?/);
|
|
4284
|
+
return match ? match[1] : null;
|
|
4285
|
+
}
|
|
4286
|
+
|
|
4287
|
+
// src/server/api/citations.ts
|
|
4288
|
+
var citationsRouter = new Hono5();
|
|
4289
|
+
citationsRouter.get("/:id", async (c) => {
|
|
4290
|
+
const { id } = c.req.param();
|
|
4291
|
+
const citationId = parseCitationId(id) || id;
|
|
4292
|
+
try {
|
|
4293
|
+
const memoryService = getDefaultMemoryService();
|
|
4294
|
+
await memoryService.initialize();
|
|
4295
|
+
const recentEvents = await memoryService.getRecentEvents(1e4);
|
|
4296
|
+
const event = recentEvents.find((e) => {
|
|
4297
|
+
const eventCitationId = generateCitationId(e.id);
|
|
4298
|
+
return eventCitationId === citationId;
|
|
4299
|
+
});
|
|
4300
|
+
if (!event) {
|
|
4301
|
+
return c.json({ error: "Citation not found" }, 404);
|
|
4302
|
+
}
|
|
4303
|
+
return c.json({
|
|
4304
|
+
citation: {
|
|
4305
|
+
id: citationId,
|
|
4306
|
+
eventId: event.id
|
|
4307
|
+
},
|
|
4308
|
+
event: {
|
|
4309
|
+
id: event.id,
|
|
4310
|
+
eventType: event.eventType,
|
|
4311
|
+
timestamp: event.timestamp,
|
|
4312
|
+
sessionId: event.sessionId,
|
|
4313
|
+
content: event.content,
|
|
4314
|
+
metadata: event.metadata
|
|
4315
|
+
}
|
|
4316
|
+
});
|
|
4317
|
+
} catch (error) {
|
|
4318
|
+
return c.json({ error: error.message }, 500);
|
|
4319
|
+
}
|
|
4320
|
+
});
|
|
4321
|
+
citationsRouter.get("/:id/related", async (c) => {
|
|
4322
|
+
const { id } = c.req.param();
|
|
4323
|
+
const citationId = parseCitationId(id) || id;
|
|
4324
|
+
try {
|
|
4325
|
+
const memoryService = getDefaultMemoryService();
|
|
4326
|
+
await memoryService.initialize();
|
|
4327
|
+
const recentEvents = await memoryService.getRecentEvents(1e4);
|
|
4328
|
+
const event = recentEvents.find((e) => {
|
|
4329
|
+
const eventCitationId = generateCitationId(e.id);
|
|
4330
|
+
return eventCitationId === citationId;
|
|
4331
|
+
});
|
|
4332
|
+
if (!event) {
|
|
4333
|
+
return c.json({ error: "Citation not found" }, 404);
|
|
4334
|
+
}
|
|
4335
|
+
const sessionEvents = recentEvents.filter((e) => e.sessionId === event.sessionId).sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
|
4336
|
+
const eventIndex = sessionEvents.findIndex((e) => e.id === event.id);
|
|
4337
|
+
const prev = eventIndex > 0 ? sessionEvents[eventIndex - 1] : null;
|
|
4338
|
+
const next = eventIndex < sessionEvents.length - 1 ? sessionEvents[eventIndex + 1] : null;
|
|
4339
|
+
return c.json({
|
|
4340
|
+
previous: prev ? {
|
|
4341
|
+
citationId: generateCitationId(prev.id),
|
|
4342
|
+
eventType: prev.eventType,
|
|
4343
|
+
timestamp: prev.timestamp,
|
|
4344
|
+
preview: prev.content.slice(0, 100) + (prev.content.length > 100 ? "..." : "")
|
|
4345
|
+
} : null,
|
|
4346
|
+
next: next ? {
|
|
4347
|
+
citationId: generateCitationId(next.id),
|
|
4348
|
+
eventType: next.eventType,
|
|
4349
|
+
timestamp: next.timestamp,
|
|
4350
|
+
preview: next.content.slice(0, 100) + (next.content.length > 100 ? "..." : "")
|
|
4351
|
+
} : null
|
|
4352
|
+
});
|
|
4353
|
+
} catch (error) {
|
|
4354
|
+
return c.json({ error: error.message }, 500);
|
|
4355
|
+
}
|
|
4356
|
+
});
|
|
4357
|
+
|
|
4358
|
+
// src/server/api/index.ts
|
|
4359
|
+
var apiRouter = new Hono6().route("/sessions", sessionsRouter).route("/events", eventsRouter).route("/search", searchRouter).route("/stats", statsRouter).route("/citations", citationsRouter);
|
|
4360
|
+
export {
|
|
4361
|
+
apiRouter
|
|
4362
|
+
};
|
|
4363
|
+
//# sourceMappingURL=index.js.map
|