claude-memory-layer 1.0.11 → 1.0.13
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/AGENTS.md +60 -0
- package/README.md +166 -2
- package/bootstrap-kb/decisions/decisions.md +244 -0
- package/bootstrap-kb/glossary/glossary.md +46 -0
- package/bootstrap-kb/modules/.claude-plugin.md +22 -0
- package/bootstrap-kb/modules/agents.md.md +15 -0
- package/bootstrap-kb/modules/claude.md.md +15 -0
- package/bootstrap-kb/modules/context.md.md +15 -0
- package/bootstrap-kb/modules/docs.md +18 -0
- package/bootstrap-kb/modules/handoff.md.md +15 -0
- package/bootstrap-kb/modules/package-lock.json.md +15 -0
- package/bootstrap-kb/modules/package.json.md +15 -0
- package/bootstrap-kb/modules/plan.md.md +15 -0
- package/bootstrap-kb/modules/readme.md.md +15 -0
- package/bootstrap-kb/modules/scripts.md +26 -0
- package/bootstrap-kb/modules/spec.md.md +15 -0
- package/bootstrap-kb/modules/specs.md +20 -0
- package/bootstrap-kb/modules/src.md +51 -0
- package/bootstrap-kb/modules/tests.md +42 -0
- package/bootstrap-kb/modules/tsconfig.json.md +15 -0
- package/bootstrap-kb/modules/vitest.config.ts.md +15 -0
- package/bootstrap-kb/overview/overview.md +40 -0
- package/bootstrap-kb/sources/manifest.json +950 -0
- package/bootstrap-kb/sources/manifest.md +227 -0
- package/bootstrap-kb/timeline/timeline.md +57 -0
- package/d.sh +3 -0
- package/deploy.sh +3 -0
- package/dist/cli/index.js +2389 -286
- package/dist/cli/index.js.map +4 -4
- package/dist/core/index.js +1017 -132
- package/dist/core/index.js.map +4 -4
- package/dist/hooks/post-tool-use.js +1347 -202
- package/dist/hooks/post-tool-use.js.map +4 -4
- package/dist/hooks/session-end.js +1339 -194
- package/dist/hooks/session-end.js.map +4 -4
- package/dist/hooks/session-start.js +1343 -198
- package/dist/hooks/session-start.js.map +4 -4
- package/dist/hooks/stop.js +1351 -206
- package/dist/hooks/stop.js.map +4 -4
- package/dist/hooks/user-prompt-submit.js +1347 -202
- package/dist/hooks/user-prompt-submit.js.map +4 -4
- package/dist/server/api/index.js +1436 -211
- package/dist/server/api/index.js.map +4 -4
- package/dist/server/index.js +1445 -220
- package/dist/server/index.js.map +4 -4
- package/dist/services/memory-service.js +1345 -199
- package/dist/services/memory-service.js.map +4 -4
- package/dist/ui/app.js +69 -2
- package/dist/ui/index.html +8 -0
- package/docs/MCP_MEMORY_SERVICE_COMPARATIVE_REVIEW.md +271 -0
- package/docs/MEMU_ADOPTION.md +40 -0
- package/memory/.claude-plugin/commands/2026-02-25.md +263 -0
- package/memory/_index.md +405 -0
- package/memory/default/uncategorized/2026-02-25.md +4839 -0
- package/memory/specs/20260207-dashboard-upgrade/2026-02-25.md +142 -0
- package/memory/specs/citations-system/2026-02-25.md +1121 -0
- package/memory/specs/endless-mode/2026-02-25.md +1392 -0
- package/memory/specs/entity-edge-model/2026-02-25.md +1263 -0
- package/memory/specs/evidence-aligner-v2/2026-02-25.md +1028 -0
- package/memory/specs/mcp-desktop-integration/2026-02-25.md +1334 -0
- package/memory/specs/post-tool-use-hook/2026-02-25.md +1164 -0
- package/memory/specs/private-tags/2026-02-25.md +1057 -0
- package/memory/specs/progressive-disclosure/2026-02-25.md +1436 -0
- package/memory/specs/task-entity-system/2026-02-25.md +924 -0
- package/memory/specs/vector-outbox-v2/2026-02-25.md +1510 -0
- package/memory/specs/web-viewer-ui/2026-02-25.md +1709 -0
- package/package.json +2 -1
- package/scripts/build.ts +6 -0
- package/scripts/bump-patch-version.sh +18 -0
- package/src/cli/index.ts +281 -2
- package/src/core/consolidated-store.ts +63 -1
- package/src/core/consolidation-worker.ts +115 -6
- package/src/core/event-store.ts +14 -0
- package/src/core/index.ts +1 -0
- package/src/core/ingest-interceptor.ts +80 -0
- package/src/core/markdown-mirror.ts +70 -0
- package/src/core/md-mirror.ts +92 -0
- package/src/core/mongo-sync-config.ts +165 -0
- package/src/core/mongo-sync-worker.ts +381 -0
- package/src/core/retriever.ts +540 -150
- package/src/core/sqlite-event-store.ts +350 -1
- package/src/core/tag-taxonomy.ts +51 -0
- package/src/core/types.ts +28 -0
- package/src/server/api/health.ts +53 -0
- package/src/server/api/index.ts +3 -1
- package/src/server/api/stats.ts +46 -1
- package/src/services/bootstrap-organizer.ts +443 -0
- package/src/services/codex-session-history-importer.ts +474 -0
- package/src/services/memory-service.ts +373 -68
- package/src/services/session-history-importer.ts +53 -25
- package/src/ui/app.js +69 -2
- package/src/ui/index.html +8 -0
- package/tests/bootstrap-organizer.test.ts +111 -0
- package/tests/consolidation-worker.test.ts +75 -0
- package/tests/ingest-interceptor.test.ts +38 -0
- package/tests/markdown-mirror.test.ts +85 -0
- package/tests/md-mirror.test.ts +50 -0
- package/tests/retriever-fallback-chain.test.ts +223 -0
- package/tests/retriever-strategy-scope.test.ts +97 -0
- package/tests/retriever.memu-adoption.test.ts +122 -0
- package/tests/sqlite-event-store-replication.test.ts +92 -0
|
@@ -24,15 +24,17 @@ import {
|
|
|
24
24
|
type SQLiteDatabase,
|
|
25
25
|
type SQLiteOptions
|
|
26
26
|
} from './sqlite-wrapper.js';
|
|
27
|
+
import { MarkdownMirror } from './markdown-mirror.js';
|
|
27
28
|
|
|
28
29
|
export interface SQLiteEventStoreOptions extends SQLiteOptions {
|
|
29
|
-
|
|
30
|
+
markdownMirrorRoot?: string;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
export class SQLiteEventStore {
|
|
33
34
|
private db: SQLiteDatabase;
|
|
34
35
|
private initialized = false;
|
|
35
36
|
private readonly readOnly: boolean;
|
|
37
|
+
private readonly markdownMirror: MarkdownMirror | null;
|
|
36
38
|
|
|
37
39
|
constructor(private dbPath: string, options?: SQLiteEventStoreOptions) {
|
|
38
40
|
this.readOnly = options?.readonly ?? false;
|
|
@@ -40,6 +42,9 @@ export class SQLiteEventStore {
|
|
|
40
42
|
readonly: this.readOnly,
|
|
41
43
|
walMode: !this.readOnly
|
|
42
44
|
});
|
|
45
|
+
this.markdownMirror = this.readOnly || !options?.markdownMirrorRoot
|
|
46
|
+
? null
|
|
47
|
+
: new MarkdownMirror(options.markdownMirrorRoot);
|
|
43
48
|
}
|
|
44
49
|
|
|
45
50
|
/**
|
|
@@ -251,6 +256,17 @@ export class SQLiteEventStore {
|
|
|
251
256
|
created_at TEXT DEFAULT (datetime('now'))
|
|
252
257
|
);
|
|
253
258
|
|
|
259
|
+
-- Consolidated Rules table (long-term stable memory)
|
|
260
|
+
CREATE TABLE IF NOT EXISTS consolidated_rules (
|
|
261
|
+
rule_id TEXT PRIMARY KEY,
|
|
262
|
+
rule TEXT NOT NULL,
|
|
263
|
+
topics TEXT,
|
|
264
|
+
source_memory_ids TEXT,
|
|
265
|
+
source_events TEXT,
|
|
266
|
+
confidence REAL DEFAULT 0.5,
|
|
267
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
268
|
+
);
|
|
269
|
+
|
|
254
270
|
-- Endless Mode Config table
|
|
255
271
|
CREATE TABLE IF NOT EXISTS endless_config (
|
|
256
272
|
key TEXT PRIMARY KEY,
|
|
@@ -275,6 +291,24 @@ export class SQLiteEventStore {
|
|
|
275
291
|
measured_at TEXT
|
|
276
292
|
);
|
|
277
293
|
|
|
294
|
+
-- Retrieval trace log (query -> candidates -> selected for context)
|
|
295
|
+
CREATE TABLE IF NOT EXISTS retrieval_traces (
|
|
296
|
+
trace_id TEXT PRIMARY KEY,
|
|
297
|
+
session_id TEXT,
|
|
298
|
+
project_hash TEXT,
|
|
299
|
+
query_text TEXT NOT NULL,
|
|
300
|
+
strategy TEXT,
|
|
301
|
+
candidate_event_ids TEXT,
|
|
302
|
+
selected_event_ids TEXT,
|
|
303
|
+
candidate_details_json TEXT,
|
|
304
|
+
selected_details_json TEXT,
|
|
305
|
+
candidate_count INTEGER DEFAULT 0,
|
|
306
|
+
selected_count INTEGER DEFAULT 0,
|
|
307
|
+
confidence TEXT,
|
|
308
|
+
fallback_trace TEXT,
|
|
309
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
310
|
+
);
|
|
311
|
+
|
|
278
312
|
-- Sync position tracking (for SQLite -> DuckDB sync)
|
|
279
313
|
CREATE TABLE IF NOT EXISTS sync_positions (
|
|
280
314
|
target_name TEXT PRIMARY KEY,
|
|
@@ -299,10 +333,14 @@ export class SQLiteEventStore {
|
|
|
299
333
|
CREATE INDEX IF NOT EXISTS idx_working_set_relevance ON working_set(relevance_score);
|
|
300
334
|
CREATE INDEX IF NOT EXISTS idx_consolidated_confidence ON consolidated_memories(confidence);
|
|
301
335
|
CREATE INDEX IF NOT EXISTS idx_continuity_created ON continuity_log(created_at);
|
|
336
|
+
CREATE INDEX IF NOT EXISTS idx_consolidated_rules_confidence ON consolidated_rules(confidence);
|
|
302
337
|
CREATE INDEX IF NOT EXISTS idx_embedding_outbox_status ON embedding_outbox(status);
|
|
303
338
|
CREATE INDEX IF NOT EXISTS idx_helpfulness_event ON memory_helpfulness(event_id);
|
|
304
339
|
CREATE INDEX IF NOT EXISTS idx_helpfulness_session ON memory_helpfulness(session_id);
|
|
305
340
|
CREATE INDEX IF NOT EXISTS idx_helpfulness_score ON memory_helpfulness(helpfulness_score DESC);
|
|
341
|
+
CREATE INDEX IF NOT EXISTS idx_retrieval_traces_created_at ON retrieval_traces(created_at DESC);
|
|
342
|
+
CREATE INDEX IF NOT EXISTS idx_retrieval_traces_project_hash ON retrieval_traces(project_hash);
|
|
343
|
+
CREATE INDEX IF NOT EXISTS idx_retrieval_traces_session_id ON retrieval_traces(session_id);
|
|
306
344
|
|
|
307
345
|
-- FTS5 Full-Text Search for fast keyword search
|
|
308
346
|
CREATE VIRTUAL TABLE IF NOT EXISTS events_fts USING fts5(
|
|
@@ -327,6 +365,19 @@ export class SQLiteEventStore {
|
|
|
327
365
|
END;
|
|
328
366
|
`);
|
|
329
367
|
|
|
368
|
+
|
|
369
|
+
// Best-effort forward migration for retrieval trace detail column
|
|
370
|
+
try {
|
|
371
|
+
sqliteExec(this.db, `ALTER TABLE retrieval_traces ADD COLUMN selected_details_json TEXT;`);
|
|
372
|
+
} catch {
|
|
373
|
+
// column may already exist
|
|
374
|
+
}
|
|
375
|
+
try {
|
|
376
|
+
sqliteExec(this.db, `ALTER TABLE retrieval_traces ADD COLUMN candidate_details_json TEXT;`);
|
|
377
|
+
} catch {
|
|
378
|
+
// column may already exist
|
|
379
|
+
}
|
|
380
|
+
|
|
330
381
|
// Migrate existing events table to add new columns if they don't exist
|
|
331
382
|
// Check if columns exist before trying to add them
|
|
332
383
|
const tableInfo = sqliteAll(this.db, "PRAGMA table_info(events)", []);
|
|
@@ -455,6 +506,22 @@ export class SQLiteEventStore {
|
|
|
455
506
|
|
|
456
507
|
transaction();
|
|
457
508
|
|
|
509
|
+
if (this.markdownMirror) {
|
|
510
|
+
const event: MemoryEvent = {
|
|
511
|
+
id,
|
|
512
|
+
eventType: input.eventType,
|
|
513
|
+
sessionId: input.sessionId,
|
|
514
|
+
timestamp: input.timestamp,
|
|
515
|
+
content: input.content,
|
|
516
|
+
canonicalKey,
|
|
517
|
+
dedupeKey,
|
|
518
|
+
metadata
|
|
519
|
+
};
|
|
520
|
+
this.markdownMirror.append(event).catch((err) => {
|
|
521
|
+
console.warn('[SQLiteEventStore] markdown mirror append failed:', err);
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
|
|
458
525
|
return { success: true, eventId: id, isDuplicate: false };
|
|
459
526
|
} catch (error) {
|
|
460
527
|
return {
|
|
@@ -525,6 +592,114 @@ export class SQLiteEventStore {
|
|
|
525
592
|
return rows.map(this.rowToEvent);
|
|
526
593
|
}
|
|
527
594
|
|
|
595
|
+
/**
|
|
596
|
+
* Get events since a SQLite rowid (for robust incremental replication).
|
|
597
|
+
* Rowid is monotonic for append-only tables, independent of client timestamps.
|
|
598
|
+
*/
|
|
599
|
+
async getEventsSinceRowid(
|
|
600
|
+
lastRowid: number,
|
|
601
|
+
limit: number = 1000
|
|
602
|
+
): Promise<Array<{ rowid: number; event: MemoryEvent }>> {
|
|
603
|
+
await this.initialize();
|
|
604
|
+
|
|
605
|
+
const rows = sqliteAll<Record<string, unknown>>(
|
|
606
|
+
this.db,
|
|
607
|
+
`SELECT rowid as _rowid, * FROM events WHERE rowid > ? ORDER BY rowid ASC LIMIT ?`,
|
|
608
|
+
[lastRowid, limit]
|
|
609
|
+
);
|
|
610
|
+
|
|
611
|
+
return rows.map(row => ({
|
|
612
|
+
rowid: row._rowid as number,
|
|
613
|
+
event: this.rowToEvent(row)
|
|
614
|
+
}));
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Import events with fixed IDs (used for cross-machine replication).
|
|
619
|
+
* Idempotent: skips if event id or dedupeKey already exists.
|
|
620
|
+
*
|
|
621
|
+
* NOTE: This bypasses the append() id generation to preserve stable IDs.
|
|
622
|
+
*/
|
|
623
|
+
async importEvents(events: MemoryEvent[]): Promise<{ inserted: number; skipped: number }> {
|
|
624
|
+
if (events.length === 0) return { inserted: 0, skipped: 0 };
|
|
625
|
+
if (this.readOnly) return { inserted: 0, skipped: events.length };
|
|
626
|
+
|
|
627
|
+
await this.initialize();
|
|
628
|
+
|
|
629
|
+
const getById = this.db.prepare(`SELECT id FROM events WHERE id = ?`);
|
|
630
|
+
const getByDedupe = this.db.prepare(`SELECT event_id FROM event_dedup WHERE dedupe_key = ?`);
|
|
631
|
+
|
|
632
|
+
const insertEvent = this.db.prepare(`
|
|
633
|
+
INSERT INTO events (id, event_type, session_id, timestamp, content, canonical_key, dedupe_key, metadata, turn_id)
|
|
634
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
635
|
+
`);
|
|
636
|
+
|
|
637
|
+
const insertDedup = this.db.prepare(`
|
|
638
|
+
INSERT INTO event_dedup (dedupe_key, event_id) VALUES (?, ?)
|
|
639
|
+
`);
|
|
640
|
+
|
|
641
|
+
const insertLevel = this.db.prepare(`
|
|
642
|
+
INSERT INTO memory_levels (event_id, level) VALUES (?, 'L0')
|
|
643
|
+
`);
|
|
644
|
+
|
|
645
|
+
let inserted = 0;
|
|
646
|
+
let skipped = 0;
|
|
647
|
+
const insertedEvents: MemoryEvent[] = [];
|
|
648
|
+
|
|
649
|
+
const tx = this.db.transaction((batch: MemoryEvent[]) => {
|
|
650
|
+
for (const ev of batch) {
|
|
651
|
+
// Skip if already present by id
|
|
652
|
+
const existingById = getById.get(ev.id) as { id: string } | undefined;
|
|
653
|
+
if (existingById) {
|
|
654
|
+
skipped++;
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const canonicalKey = ev.canonicalKey || makeCanonicalKey(ev.content);
|
|
659
|
+
const dedupeKey = ev.dedupeKey || makeDedupeKey(ev.content, ev.sessionId);
|
|
660
|
+
|
|
661
|
+
// Skip if already present by dedupe key
|
|
662
|
+
const existingByDedupe = getByDedupe.get(dedupeKey) as { event_id: string } | undefined;
|
|
663
|
+
if (existingByDedupe) {
|
|
664
|
+
skipped++;
|
|
665
|
+
continue;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
const metadata = ev.metadata || {};
|
|
669
|
+
const turnId = (metadata as any).turnId as string | undefined;
|
|
670
|
+
|
|
671
|
+
insertEvent.run(
|
|
672
|
+
ev.id,
|
|
673
|
+
ev.eventType,
|
|
674
|
+
ev.sessionId,
|
|
675
|
+
toSQLiteTimestamp(ev.timestamp),
|
|
676
|
+
ev.content,
|
|
677
|
+
canonicalKey,
|
|
678
|
+
dedupeKey,
|
|
679
|
+
JSON.stringify(metadata),
|
|
680
|
+
turnId ?? null
|
|
681
|
+
);
|
|
682
|
+
|
|
683
|
+
insertDedup.run(dedupeKey, ev.id);
|
|
684
|
+
insertLevel.run(ev.id);
|
|
685
|
+
inserted++;
|
|
686
|
+
insertedEvents.push(ev);
|
|
687
|
+
}
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
tx(events);
|
|
691
|
+
|
|
692
|
+
if (this.markdownMirror && insertedEvents.length > 0) {
|
|
693
|
+
for (const ev of insertedEvents) {
|
|
694
|
+
this.markdownMirror.append(ev).catch((err) => {
|
|
695
|
+
console.warn('[SQLiteEventStore] markdown mirror append failed:', err);
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
return { inserted, skipped };
|
|
701
|
+
}
|
|
702
|
+
|
|
528
703
|
/**
|
|
529
704
|
* Create or update session
|
|
530
705
|
*/
|
|
@@ -708,6 +883,43 @@ export class SQLiteEventStore {
|
|
|
708
883
|
);
|
|
709
884
|
}
|
|
710
885
|
|
|
886
|
+
|
|
887
|
+
/**
|
|
888
|
+
* Get embedding/vector outbox health statistics
|
|
889
|
+
*/
|
|
890
|
+
async getOutboxStats(): Promise<{
|
|
891
|
+
embedding: { pending: number; processing: number; failed: number; total: number };
|
|
892
|
+
vector: { pending: number; processing: number; failed: number; total: number };
|
|
893
|
+
}> {
|
|
894
|
+
await this.initialize();
|
|
895
|
+
|
|
896
|
+
const embeddingRows = sqliteAll<{ status: string; count: number }>(
|
|
897
|
+
this.db,
|
|
898
|
+
`SELECT status, COUNT(*) as count FROM embedding_outbox GROUP BY status`
|
|
899
|
+
);
|
|
900
|
+
const vectorRows = sqliteAll<{ status: string; count: number }>(
|
|
901
|
+
this.db,
|
|
902
|
+
`SELECT status, COUNT(*) as count FROM vector_outbox GROUP BY status`
|
|
903
|
+
);
|
|
904
|
+
|
|
905
|
+
const fromRows = (rows: Array<{ status: string; count: number }>) => {
|
|
906
|
+
const out = { pending: 0, processing: 0, failed: 0, total: 0 };
|
|
907
|
+
for (const row of rows) {
|
|
908
|
+
const key = row.status as 'pending' | 'processing' | 'failed' | 'done';
|
|
909
|
+
if (key === 'pending' || key === 'processing' || key === 'failed') {
|
|
910
|
+
out[key] += row.count;
|
|
911
|
+
}
|
|
912
|
+
out.total += row.count;
|
|
913
|
+
}
|
|
914
|
+
return out;
|
|
915
|
+
};
|
|
916
|
+
|
|
917
|
+
return {
|
|
918
|
+
embedding: fromRows(embeddingRows),
|
|
919
|
+
vector: fromRows(vectorRows)
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
|
|
711
923
|
/**
|
|
712
924
|
* Update memory level
|
|
713
925
|
*/
|
|
@@ -1151,6 +1363,143 @@ export class SQLiteEventStore {
|
|
|
1151
1363
|
return this.db;
|
|
1152
1364
|
}
|
|
1153
1365
|
|
|
1366
|
+
|
|
1367
|
+
async recordRetrievalTrace(input: {
|
|
1368
|
+
sessionId?: string;
|
|
1369
|
+
projectHash?: string;
|
|
1370
|
+
queryText: string;
|
|
1371
|
+
strategy?: string;
|
|
1372
|
+
candidateEventIds: string[];
|
|
1373
|
+
selectedEventIds: string[];
|
|
1374
|
+
candidateDetails?: Array<{
|
|
1375
|
+
eventId: string;
|
|
1376
|
+
score: number;
|
|
1377
|
+
semanticScore?: number;
|
|
1378
|
+
lexicalScore?: number;
|
|
1379
|
+
recencyScore?: number;
|
|
1380
|
+
}>;
|
|
1381
|
+
selectedDetails?: Array<{
|
|
1382
|
+
eventId: string;
|
|
1383
|
+
score: number;
|
|
1384
|
+
semanticScore?: number;
|
|
1385
|
+
lexicalScore?: number;
|
|
1386
|
+
recencyScore?: number;
|
|
1387
|
+
}>;
|
|
1388
|
+
confidence?: string;
|
|
1389
|
+
fallbackTrace?: string[];
|
|
1390
|
+
}): Promise<void> {
|
|
1391
|
+
await this.initialize();
|
|
1392
|
+
|
|
1393
|
+
const traceId = randomUUID();
|
|
1394
|
+
sqliteRun(
|
|
1395
|
+
this.db,
|
|
1396
|
+
`INSERT INTO retrieval_traces (
|
|
1397
|
+
trace_id, session_id, project_hash, query_text, strategy,
|
|
1398
|
+
candidate_event_ids, selected_event_ids, candidate_details_json, selected_details_json,
|
|
1399
|
+
candidate_count, selected_count, confidence, fallback_trace
|
|
1400
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1401
|
+
[
|
|
1402
|
+
traceId,
|
|
1403
|
+
input.sessionId || null,
|
|
1404
|
+
input.projectHash || null,
|
|
1405
|
+
input.queryText,
|
|
1406
|
+
input.strategy || null,
|
|
1407
|
+
JSON.stringify(input.candidateEventIds || []),
|
|
1408
|
+
JSON.stringify(input.selectedEventIds || []),
|
|
1409
|
+
JSON.stringify(input.candidateDetails || []),
|
|
1410
|
+
JSON.stringify(input.selectedDetails || []),
|
|
1411
|
+
(input.candidateEventIds || []).length,
|
|
1412
|
+
(input.selectedEventIds || []).length,
|
|
1413
|
+
input.confidence || null,
|
|
1414
|
+
JSON.stringify(input.fallbackTrace || [])
|
|
1415
|
+
]
|
|
1416
|
+
);
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
async getRecentRetrievalTraces(limit: number = 50): Promise<Array<{
|
|
1420
|
+
traceId: string;
|
|
1421
|
+
sessionId?: string;
|
|
1422
|
+
projectHash?: string;
|
|
1423
|
+
queryText: string;
|
|
1424
|
+
strategy?: string;
|
|
1425
|
+
candidateEventIds: string[];
|
|
1426
|
+
selectedEventIds: string[];
|
|
1427
|
+
candidateDetails: Array<{
|
|
1428
|
+
eventId: string;
|
|
1429
|
+
score: number;
|
|
1430
|
+
semanticScore?: number;
|
|
1431
|
+
lexicalScore?: number;
|
|
1432
|
+
recencyScore?: number;
|
|
1433
|
+
}>;
|
|
1434
|
+
selectedDetails: Array<{
|
|
1435
|
+
eventId: string;
|
|
1436
|
+
score: number;
|
|
1437
|
+
semanticScore?: number;
|
|
1438
|
+
lexicalScore?: number;
|
|
1439
|
+
recencyScore?: number;
|
|
1440
|
+
}>;
|
|
1441
|
+
candidateCount: number;
|
|
1442
|
+
selectedCount: number;
|
|
1443
|
+
confidence?: string;
|
|
1444
|
+
fallbackTrace: string[];
|
|
1445
|
+
createdAt: Date;
|
|
1446
|
+
}>> {
|
|
1447
|
+
await this.initialize();
|
|
1448
|
+
|
|
1449
|
+
const rows = sqliteAll<Record<string, unknown>>(
|
|
1450
|
+
this.db,
|
|
1451
|
+
`SELECT * FROM retrieval_traces ORDER BY created_at DESC LIMIT ?`,
|
|
1452
|
+
[limit]
|
|
1453
|
+
);
|
|
1454
|
+
|
|
1455
|
+
return rows.map((row) => ({
|
|
1456
|
+
traceId: row.trace_id as string,
|
|
1457
|
+
sessionId: (row.session_id as string) || undefined,
|
|
1458
|
+
projectHash: (row.project_hash as string) || undefined,
|
|
1459
|
+
queryText: row.query_text as string,
|
|
1460
|
+
strategy: (row.strategy as string) || undefined,
|
|
1461
|
+
candidateEventIds: row.candidate_event_ids ? JSON.parse(row.candidate_event_ids as string) : [],
|
|
1462
|
+
selectedEventIds: row.selected_event_ids ? JSON.parse(row.selected_event_ids as string) : [],
|
|
1463
|
+
candidateDetails: row.candidate_details_json ? JSON.parse(row.candidate_details_json as string) : [],
|
|
1464
|
+
selectedDetails: row.selected_details_json ? JSON.parse(row.selected_details_json as string) : [],
|
|
1465
|
+
candidateCount: Number(row.candidate_count || 0),
|
|
1466
|
+
selectedCount: Number(row.selected_count || 0),
|
|
1467
|
+
confidence: (row.confidence as string) || undefined,
|
|
1468
|
+
fallbackTrace: row.fallback_trace ? JSON.parse(row.fallback_trace as string) : [],
|
|
1469
|
+
createdAt: toDateFromSQLite(row.created_at),
|
|
1470
|
+
}));
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
async getRetrievalTraceStats(): Promise<{
|
|
1474
|
+
totalQueries: number;
|
|
1475
|
+
avgCandidateCount: number;
|
|
1476
|
+
avgSelectedCount: number;
|
|
1477
|
+
selectionRate: number;
|
|
1478
|
+
}> {
|
|
1479
|
+
await this.initialize();
|
|
1480
|
+
|
|
1481
|
+
const row = sqliteGet<Record<string, unknown>>(
|
|
1482
|
+
this.db,
|
|
1483
|
+
`SELECT
|
|
1484
|
+
COUNT(*) as total_queries,
|
|
1485
|
+
AVG(candidate_count) as avg_candidate_count,
|
|
1486
|
+
AVG(selected_count) as avg_selected_count,
|
|
1487
|
+
CASE
|
|
1488
|
+
WHEN SUM(candidate_count) > 0 THEN (SUM(selected_count) * 1.0 / SUM(candidate_count))
|
|
1489
|
+
ELSE 0
|
|
1490
|
+
END as selection_rate
|
|
1491
|
+
FROM retrieval_traces`,
|
|
1492
|
+
[]
|
|
1493
|
+
);
|
|
1494
|
+
|
|
1495
|
+
return {
|
|
1496
|
+
totalQueries: Number(row?.total_queries || 0),
|
|
1497
|
+
avgCandidateCount: Number(row?.avg_candidate_count || 0),
|
|
1498
|
+
avgSelectedCount: Number(row?.avg_selected_count || 0),
|
|
1499
|
+
selectionRate: Number(row?.selection_rate || 0),
|
|
1500
|
+
};
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1154
1503
|
/**
|
|
1155
1504
|
* Close database connection
|
|
1156
1505
|
*/
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export const TAG_NAMESPACES = {
|
|
2
|
+
SYSTEM: 'sys:',
|
|
3
|
+
QUALITY: 'q:',
|
|
4
|
+
PROJECT: 'proj:',
|
|
5
|
+
TOPIC: 'topic:',
|
|
6
|
+
TEMPORAL: 't:',
|
|
7
|
+
USER: 'user:',
|
|
8
|
+
AGENT: 'agent:'
|
|
9
|
+
} as const;
|
|
10
|
+
|
|
11
|
+
export const VALID_TAG_NAMESPACES = new Set<string>(Object.values(TAG_NAMESPACES));
|
|
12
|
+
|
|
13
|
+
export function parseTag(tag: string): { namespace?: string; value: string } {
|
|
14
|
+
const value = (tag || '').trim();
|
|
15
|
+
const idx = value.indexOf(':');
|
|
16
|
+
if (idx <= 0) return { value };
|
|
17
|
+
|
|
18
|
+
const namespace = `${value.slice(0, idx)}:`;
|
|
19
|
+
const tagValue = value.slice(idx + 1);
|
|
20
|
+
if (!tagValue) return { value };
|
|
21
|
+
|
|
22
|
+
return { namespace, value: tagValue };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function validateTag(tag: string): boolean {
|
|
26
|
+
const normalized = (tag || '').trim();
|
|
27
|
+
if (!normalized) return false;
|
|
28
|
+
|
|
29
|
+
const { namespace } = parseTag(normalized);
|
|
30
|
+
if (!namespace) return true; // backward compatibility for legacy tags
|
|
31
|
+
return VALID_TAG_NAMESPACES.has(namespace);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function withNamespace(value: string, namespace: string): string {
|
|
35
|
+
const clean = parseTag(value).value.trim();
|
|
36
|
+
return `${namespace}${clean}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function normalizeTags(tags: unknown): string[] {
|
|
40
|
+
if (!Array.isArray(tags)) return [];
|
|
41
|
+
|
|
42
|
+
const dedup = new Set<string>();
|
|
43
|
+
for (const item of tags) {
|
|
44
|
+
if (typeof item !== 'string') continue;
|
|
45
|
+
const normalized = item.trim();
|
|
46
|
+
if (!validateTag(normalized)) continue;
|
|
47
|
+
dedup.add(normalized);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return [...dedup];
|
|
51
|
+
}
|
package/src/core/types.ts
CHANGED
|
@@ -818,6 +818,34 @@ export interface ConsolidatedMemoryInput {
|
|
|
818
818
|
confidence: number;
|
|
819
819
|
}
|
|
820
820
|
|
|
821
|
+
// Long-term Rule (promoted from stable summaries)
|
|
822
|
+
export const ConsolidationRuleSchema = z.object({
|
|
823
|
+
ruleId: z.string(),
|
|
824
|
+
rule: z.string(),
|
|
825
|
+
topics: z.array(z.string()),
|
|
826
|
+
sourceMemoryIds: z.array(z.string()),
|
|
827
|
+
sourceEvents: z.array(z.string()),
|
|
828
|
+
confidence: z.number(),
|
|
829
|
+
createdAt: z.date()
|
|
830
|
+
});
|
|
831
|
+
export type ConsolidationRule = z.infer<typeof ConsolidationRuleSchema>;
|
|
832
|
+
|
|
833
|
+
export interface ConsolidationRuleInput {
|
|
834
|
+
rule: string;
|
|
835
|
+
topics: string[];
|
|
836
|
+
sourceMemoryIds: string[];
|
|
837
|
+
sourceEvents: string[];
|
|
838
|
+
confidence: number;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
export interface ConsolidationCostQualityReport {
|
|
842
|
+
beforeTokenEstimate: number;
|
|
843
|
+
afterTokenEstimate: number;
|
|
844
|
+
reductionRatio: number;
|
|
845
|
+
qualityGuardPassed: boolean;
|
|
846
|
+
details: string;
|
|
847
|
+
}
|
|
848
|
+
|
|
821
849
|
// Event Group (for consolidation)
|
|
822
850
|
export interface EventGroup {
|
|
823
851
|
topics: string[];
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Health API
|
|
3
|
+
* Operational health checks including outbox backlog/failures
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Hono } from 'hono';
|
|
7
|
+
import { getServiceFromQuery } from './utils.js';
|
|
8
|
+
|
|
9
|
+
export const healthRouter = new Hono();
|
|
10
|
+
|
|
11
|
+
// GET /api/health
|
|
12
|
+
healthRouter.get('/', async (c) => {
|
|
13
|
+
const memoryService = getServiceFromQuery(c);
|
|
14
|
+
try {
|
|
15
|
+
await memoryService.initialize();
|
|
16
|
+
|
|
17
|
+
const [stats, outbox] = await Promise.all([
|
|
18
|
+
memoryService.getStats(),
|
|
19
|
+
memoryService.getOutboxStats()
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
const outboxPending = outbox.embedding.pending + outbox.vector.pending;
|
|
23
|
+
const outboxFailed = outbox.embedding.failed + outbox.vector.failed;
|
|
24
|
+
|
|
25
|
+
const status = outboxFailed > 0 ? 'needs-attention' : 'ok';
|
|
26
|
+
|
|
27
|
+
return c.json({
|
|
28
|
+
status,
|
|
29
|
+
timestamp: new Date().toISOString(),
|
|
30
|
+
storage: {
|
|
31
|
+
totalEvents: stats.totalEvents,
|
|
32
|
+
vectorCount: stats.vectorCount
|
|
33
|
+
},
|
|
34
|
+
outbox: {
|
|
35
|
+
embedding: outbox.embedding,
|
|
36
|
+
vector: outbox.vector,
|
|
37
|
+
totals: {
|
|
38
|
+
pending: outboxPending,
|
|
39
|
+
failed: outboxFailed
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
levelStats: stats.levelStats
|
|
43
|
+
});
|
|
44
|
+
} catch (error) {
|
|
45
|
+
return c.json({
|
|
46
|
+
status: 'error',
|
|
47
|
+
timestamp: new Date().toISOString(),
|
|
48
|
+
error: (error as Error).message
|
|
49
|
+
}, 500);
|
|
50
|
+
} finally {
|
|
51
|
+
await memoryService.shutdown();
|
|
52
|
+
}
|
|
53
|
+
});
|
package/src/server/api/index.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { citationsRouter } from './citations.js';
|
|
|
12
12
|
import { turnsRouter } from './turns.js';
|
|
13
13
|
import { projectsRouter } from './projects.js';
|
|
14
14
|
import { chatRouter } from './chat.js';
|
|
15
|
+
import { healthRouter } from './health.js';
|
|
15
16
|
|
|
16
17
|
export const apiRouter = new Hono()
|
|
17
18
|
.route('/sessions', sessionsRouter)
|
|
@@ -21,4 +22,5 @@ export const apiRouter = new Hono()
|
|
|
21
22
|
.route('/citations', citationsRouter)
|
|
22
23
|
.route('/turns', turnsRouter)
|
|
23
24
|
.route('/projects', projectsRouter)
|
|
24
|
-
.route('/chat', chatRouter)
|
|
25
|
+
.route('/chat', chatRouter)
|
|
26
|
+
.route('/health', healthRouter);
|
package/src/server/api/stats.ts
CHANGED
|
@@ -158,6 +158,8 @@ statsRouter.get('/', async (c) => {
|
|
|
158
158
|
return acc;
|
|
159
159
|
}, {} as Record<string, number>);
|
|
160
160
|
|
|
161
|
+
const retrievalTrace = await memoryService.getRetrievalTraceStats();
|
|
162
|
+
|
|
161
163
|
return c.json({
|
|
162
164
|
storage: {
|
|
163
165
|
eventCount: stats.totalEvents,
|
|
@@ -175,7 +177,8 @@ statsRouter.get('/', async (c) => {
|
|
|
175
177
|
heapUsed: Math.round(process.memoryUsage().heapUsed / 1024 / 1024),
|
|
176
178
|
heapTotal: Math.round(process.memoryUsage().heapTotal / 1024 / 1024)
|
|
177
179
|
},
|
|
178
|
-
levelStats: stats.levelStats
|
|
180
|
+
levelStats: stats.levelStats,
|
|
181
|
+
retrievalTrace
|
|
179
182
|
});
|
|
180
183
|
} catch (error) {
|
|
181
184
|
return c.json({ error: (error as Error).message }, 500);
|
|
@@ -291,6 +294,48 @@ statsRouter.get('/helpfulness', async (c) => {
|
|
|
291
294
|
}
|
|
292
295
|
});
|
|
293
296
|
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
// GET /api/stats/retrieval-traces - Get recent retrieval traces (query -> selected context)
|
|
300
|
+
statsRouter.get('/retrieval-traces', async (c) => {
|
|
301
|
+
const limit = parseInt(c.req.query('limit') || '50', 10);
|
|
302
|
+
const memoryService = getServiceFromQuery(c);
|
|
303
|
+
|
|
304
|
+
try {
|
|
305
|
+
await memoryService.initialize();
|
|
306
|
+
const traces = await memoryService.getRecentRetrievalTraces(limit);
|
|
307
|
+
const traceStats = await memoryService.getRetrievalTraceStats();
|
|
308
|
+
|
|
309
|
+
return c.json({
|
|
310
|
+
stats: traceStats,
|
|
311
|
+
traces: traces.map((t) => ({
|
|
312
|
+
traceId: t.traceId,
|
|
313
|
+
sessionId: t.sessionId || null,
|
|
314
|
+
projectHash: t.projectHash || null,
|
|
315
|
+
queryText: t.queryText,
|
|
316
|
+
strategy: t.strategy || null,
|
|
317
|
+
candidateEventIds: t.candidateEventIds,
|
|
318
|
+
selectedEventIds: t.selectedEventIds,
|
|
319
|
+
candidateDetails: t.candidateDetails || [],
|
|
320
|
+
selectedDetails: t.selectedDetails || [],
|
|
321
|
+
candidateCount: t.candidateCount,
|
|
322
|
+
selectedCount: t.selectedCount,
|
|
323
|
+
confidence: t.confidence || null,
|
|
324
|
+
fallbackTrace: t.fallbackTrace,
|
|
325
|
+
createdAt: t.createdAt.toISOString()
|
|
326
|
+
}))
|
|
327
|
+
});
|
|
328
|
+
} catch (error) {
|
|
329
|
+
return c.json({
|
|
330
|
+
stats: { totalQueries: 0, avgCandidateCount: 0, avgSelectedCount: 0, selectionRate: 0 },
|
|
331
|
+
traces: [],
|
|
332
|
+
error: (error as Error).message
|
|
333
|
+
}, 500);
|
|
334
|
+
} finally {
|
|
335
|
+
await memoryService.shutdown();
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
|
|
294
339
|
// POST /api/stats/graduation/run - Force graduation evaluation
|
|
295
340
|
statsRouter.post('/graduation/run', async (c) => {
|
|
296
341
|
const memoryService = getServiceFromQuery(c);
|