claude-memory-layer 1.0.8 → 1.0.10

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.
Files changed (44) hide show
  1. package/.claude/settings.local.json +7 -1
  2. package/.claude-memory/test.sqlite +0 -0
  3. package/.history/package_20260202114053.json +49 -0
  4. package/.history/package_20260202121115.json +49 -0
  5. package/HANDOFF.md +92 -0
  6. package/dist/cli/index.js +1257 -74
  7. package/dist/cli/index.js.map +4 -4
  8. package/dist/core/index.js +1111 -47
  9. package/dist/core/index.js.map +4 -4
  10. package/dist/hooks/post-tool-use.js +5693 -0
  11. package/dist/hooks/post-tool-use.js.map +7 -0
  12. package/dist/hooks/session-end.js +1224 -67
  13. package/dist/hooks/session-end.js.map +4 -4
  14. package/dist/hooks/session-start.js +1219 -66
  15. package/dist/hooks/session-start.js.map +4 -4
  16. package/dist/hooks/stop.js +1224 -67
  17. package/dist/hooks/stop.js.map +4 -4
  18. package/dist/hooks/user-prompt-submit.js +1252 -98
  19. package/dist/hooks/user-prompt-submit.js.map +4 -4
  20. package/dist/server/api/index.js +1252 -73
  21. package/dist/server/api/index.js.map +4 -4
  22. package/dist/server/index.js +1252 -73
  23. package/dist/server/index.js.map +4 -4
  24. package/dist/services/memory-service.js +1246 -68
  25. package/dist/services/memory-service.js.map +4 -4
  26. package/dist/ui/app.js +304 -0
  27. package/dist/ui/index.html +195 -1188
  28. package/dist/ui/style.css +595 -0
  29. package/package.json +3 -1
  30. package/scripts/build.ts +2 -0
  31. package/src/core/event-store.ts +18 -0
  32. package/src/core/index.ts +3 -0
  33. package/src/core/retriever.ts +4 -1
  34. package/src/core/sqlite-event-store.ts +947 -0
  35. package/src/core/sqlite-wrapper.ts +108 -0
  36. package/src/core/sync-worker.ts +228 -0
  37. package/src/core/vector-worker.ts +44 -14
  38. package/src/hooks/user-prompt-submit.ts +40 -17
  39. package/src/server/api/stats.ts +37 -7
  40. package/src/services/memory-service.ts +239 -43
  41. package/src/ui/app.js +304 -0
  42. package/src/ui/index.html +195 -1188
  43. package/src/ui/style.css +595 -0
  44. package/test_access.js +49 -0
@@ -706,26 +706,1053 @@ var EventStore = class {
706
706
  tags: row.tags ? JSON.parse(row.tags) : void 0
707
707
  }));
708
708
  }
709
+ /**
710
+ * Increment access count for events (stub for compatibility)
711
+ */
712
+ async incrementAccessCount(eventIds) {
713
+ return Promise.resolve();
714
+ }
715
+ /**
716
+ * Get most accessed memories (stub for compatibility)
717
+ */
718
+ async getMostAccessed(limit = 10) {
719
+ return [];
720
+ }
721
+ /**
722
+ * Close database connection
723
+ */
724
+ async close() {
725
+ await dbClose(this.db);
726
+ }
727
+ /**
728
+ * Convert database row to MemoryEvent
729
+ */
730
+ rowToEvent(row) {
731
+ return {
732
+ id: row.id,
733
+ eventType: row.event_type,
734
+ sessionId: row.session_id,
735
+ timestamp: toDate(row.timestamp),
736
+ content: row.content,
737
+ canonicalKey: row.canonical_key,
738
+ dedupeKey: row.dedupe_key,
739
+ metadata: row.metadata ? JSON.parse(row.metadata) : void 0
740
+ };
741
+ }
742
+ };
743
+
744
+ // src/core/sqlite-event-store.ts
745
+ import { randomUUID as randomUUID2 } from "crypto";
746
+
747
+ // src/core/sqlite-wrapper.ts
748
+ import Database from "better-sqlite3";
749
+ function createSQLiteDatabase(path2, options) {
750
+ const db = new Database(path2, {
751
+ readonly: options?.readonly ?? false
752
+ });
753
+ if (!options?.readonly && (options?.walMode ?? true)) {
754
+ db.pragma("journal_mode = WAL");
755
+ db.pragma("synchronous = NORMAL");
756
+ db.pragma("busy_timeout = 5000");
757
+ }
758
+ return db;
759
+ }
760
+ function sqliteRun(db, sql, params = []) {
761
+ const stmt = db.prepare(sql);
762
+ return stmt.run(...params);
763
+ }
764
+ function sqliteAll(db, sql, params = []) {
765
+ const stmt = db.prepare(sql);
766
+ return stmt.all(...params);
767
+ }
768
+ function sqliteGet(db, sql, params = []) {
769
+ const stmt = db.prepare(sql);
770
+ return stmt.get(...params);
771
+ }
772
+ function sqliteExec(db, sql) {
773
+ db.exec(sql);
774
+ }
775
+ function sqliteClose(db) {
776
+ db.close();
777
+ }
778
+ function toDateFromSQLite(value) {
779
+ if (value instanceof Date)
780
+ return value;
781
+ if (typeof value === "string")
782
+ return new Date(value);
783
+ if (typeof value === "number")
784
+ return new Date(value);
785
+ return new Date(String(value));
786
+ }
787
+ function toSQLiteTimestamp(date) {
788
+ return date.toISOString();
789
+ }
790
+
791
+ // src/core/sqlite-event-store.ts
792
+ var SQLiteEventStore = class {
793
+ constructor(dbPath, options) {
794
+ this.dbPath = dbPath;
795
+ this.readOnly = options?.readonly ?? false;
796
+ this.db = createSQLiteDatabase(dbPath, {
797
+ readonly: this.readOnly,
798
+ walMode: !this.readOnly
799
+ });
800
+ }
801
+ db;
802
+ initialized = false;
803
+ readOnly;
804
+ /**
805
+ * Initialize database schema
806
+ */
807
+ async initialize() {
808
+ if (this.initialized)
809
+ return;
810
+ if (this.readOnly) {
811
+ this.initialized = true;
812
+ return;
813
+ }
814
+ sqliteExec(this.db, `
815
+ -- L0 EventStore: Single Source of Truth (immutable, append-only)
816
+ CREATE TABLE IF NOT EXISTS events (
817
+ id TEXT PRIMARY KEY,
818
+ event_type TEXT NOT NULL,
819
+ session_id TEXT NOT NULL,
820
+ timestamp TEXT NOT NULL,
821
+ content TEXT NOT NULL,
822
+ canonical_key TEXT NOT NULL,
823
+ dedupe_key TEXT UNIQUE,
824
+ metadata TEXT,
825
+ access_count INTEGER DEFAULT 0,
826
+ last_accessed_at TEXT
827
+ );
828
+
829
+ -- Dedup table for idempotency
830
+ CREATE TABLE IF NOT EXISTS event_dedup (
831
+ dedupe_key TEXT PRIMARY KEY,
832
+ event_id TEXT NOT NULL,
833
+ created_at TEXT DEFAULT (datetime('now'))
834
+ );
835
+
836
+ -- Session metadata
837
+ CREATE TABLE IF NOT EXISTS sessions (
838
+ id TEXT PRIMARY KEY,
839
+ started_at TEXT NOT NULL,
840
+ ended_at TEXT,
841
+ project_path TEXT,
842
+ summary TEXT,
843
+ tags TEXT
844
+ );
845
+
846
+ -- Insights (derived data, rebuildable)
847
+ CREATE TABLE IF NOT EXISTS insights (
848
+ id TEXT PRIMARY KEY,
849
+ insight_type TEXT NOT NULL,
850
+ content TEXT NOT NULL,
851
+ canonical_key TEXT NOT NULL,
852
+ confidence REAL,
853
+ source_events TEXT,
854
+ created_at TEXT,
855
+ last_updated TEXT
856
+ );
857
+
858
+ -- Embedding Outbox (Single-Writer Pattern)
859
+ CREATE TABLE IF NOT EXISTS embedding_outbox (
860
+ id TEXT PRIMARY KEY,
861
+ event_id TEXT NOT NULL,
862
+ content TEXT NOT NULL,
863
+ status TEXT DEFAULT 'pending',
864
+ retry_count INTEGER DEFAULT 0,
865
+ created_at TEXT DEFAULT (datetime('now')),
866
+ processed_at TEXT,
867
+ error_message TEXT
868
+ );
869
+
870
+ -- Projection offset tracking
871
+ CREATE TABLE IF NOT EXISTS projection_offsets (
872
+ projection_name TEXT PRIMARY KEY,
873
+ last_event_id TEXT,
874
+ last_timestamp TEXT,
875
+ updated_at TEXT DEFAULT (datetime('now'))
876
+ );
877
+
878
+ -- Memory level tracking
879
+ CREATE TABLE IF NOT EXISTS memory_levels (
880
+ event_id TEXT PRIMARY KEY,
881
+ level TEXT NOT NULL DEFAULT 'L0',
882
+ promoted_at TEXT DEFAULT (datetime('now'))
883
+ );
884
+
885
+ -- Entries (immutable memory units)
886
+ CREATE TABLE IF NOT EXISTS entries (
887
+ entry_id TEXT PRIMARY KEY,
888
+ created_ts TEXT NOT NULL,
889
+ entry_type TEXT NOT NULL,
890
+ title TEXT NOT NULL,
891
+ content_json TEXT NOT NULL,
892
+ stage TEXT NOT NULL DEFAULT 'raw',
893
+ status TEXT DEFAULT 'active',
894
+ superseded_by TEXT,
895
+ build_id TEXT,
896
+ evidence_json TEXT,
897
+ canonical_key TEXT,
898
+ created_at TEXT DEFAULT (datetime('now'))
899
+ );
900
+
901
+ -- Entities (task/condition/artifact)
902
+ CREATE TABLE IF NOT EXISTS entities (
903
+ entity_id TEXT PRIMARY KEY,
904
+ entity_type TEXT NOT NULL,
905
+ canonical_key TEXT NOT NULL,
906
+ title TEXT NOT NULL,
907
+ stage TEXT NOT NULL DEFAULT 'raw',
908
+ status TEXT NOT NULL DEFAULT 'active',
909
+ current_json TEXT NOT NULL,
910
+ title_norm TEXT,
911
+ search_text TEXT,
912
+ created_at TEXT DEFAULT (datetime('now')),
913
+ updated_at TEXT DEFAULT (datetime('now'))
914
+ );
915
+
916
+ -- Entity aliases for canonical key lookup
917
+ CREATE TABLE IF NOT EXISTS entity_aliases (
918
+ entity_type TEXT NOT NULL,
919
+ canonical_key TEXT NOT NULL,
920
+ entity_id TEXT NOT NULL,
921
+ is_primary INTEGER DEFAULT 0,
922
+ created_at TEXT DEFAULT (datetime('now')),
923
+ PRIMARY KEY(entity_type, canonical_key)
924
+ );
925
+
926
+ -- Edges (relationships between entries/entities)
927
+ CREATE TABLE IF NOT EXISTS edges (
928
+ edge_id TEXT PRIMARY KEY,
929
+ src_type TEXT NOT NULL,
930
+ src_id TEXT NOT NULL,
931
+ rel_type TEXT NOT NULL,
932
+ dst_type TEXT NOT NULL,
933
+ dst_id TEXT NOT NULL,
934
+ meta_json TEXT,
935
+ created_at TEXT DEFAULT (datetime('now'))
936
+ );
937
+
938
+ -- Vector Outbox V2 Table
939
+ CREATE TABLE IF NOT EXISTS vector_outbox (
940
+ job_id TEXT PRIMARY KEY,
941
+ item_kind TEXT NOT NULL,
942
+ item_id TEXT NOT NULL,
943
+ embedding_version TEXT NOT NULL,
944
+ status TEXT NOT NULL DEFAULT 'pending',
945
+ retry_count INTEGER DEFAULT 0,
946
+ error TEXT,
947
+ created_at TEXT DEFAULT (datetime('now')),
948
+ updated_at TEXT DEFAULT (datetime('now')),
949
+ UNIQUE(item_kind, item_id, embedding_version)
950
+ );
951
+
952
+ -- Build Runs
953
+ CREATE TABLE IF NOT EXISTS build_runs (
954
+ build_id TEXT PRIMARY KEY,
955
+ started_at TEXT NOT NULL,
956
+ finished_at TEXT,
957
+ extractor_model TEXT NOT NULL,
958
+ extractor_prompt_hash TEXT NOT NULL,
959
+ embedder_model TEXT NOT NULL,
960
+ embedding_version TEXT NOT NULL,
961
+ idris_version TEXT NOT NULL,
962
+ schema_version TEXT NOT NULL,
963
+ status TEXT NOT NULL DEFAULT 'running',
964
+ error TEXT
965
+ );
966
+
967
+ -- Pipeline Metrics
968
+ CREATE TABLE IF NOT EXISTS pipeline_metrics (
969
+ id TEXT PRIMARY KEY,
970
+ ts TEXT NOT NULL,
971
+ stage TEXT NOT NULL,
972
+ latency_ms REAL NOT NULL,
973
+ success INTEGER NOT NULL,
974
+ error TEXT,
975
+ session_id TEXT
976
+ );
977
+
978
+ -- Working Set table (active memory window)
979
+ CREATE TABLE IF NOT EXISTS working_set (
980
+ id TEXT PRIMARY KEY,
981
+ event_id TEXT NOT NULL,
982
+ added_at TEXT DEFAULT (datetime('now')),
983
+ relevance_score REAL DEFAULT 1.0,
984
+ topics TEXT,
985
+ expires_at TEXT
986
+ );
987
+
988
+ -- Consolidated Memories table (long-term integrated memories)
989
+ CREATE TABLE IF NOT EXISTS consolidated_memories (
990
+ memory_id TEXT PRIMARY KEY,
991
+ summary TEXT NOT NULL,
992
+ topics TEXT,
993
+ source_events TEXT,
994
+ confidence REAL DEFAULT 0.5,
995
+ created_at TEXT DEFAULT (datetime('now')),
996
+ accessed_at TEXT,
997
+ access_count INTEGER DEFAULT 0
998
+ );
999
+
1000
+ -- Continuity Log table (tracks context transitions)
1001
+ CREATE TABLE IF NOT EXISTS continuity_log (
1002
+ log_id TEXT PRIMARY KEY,
1003
+ from_context_id TEXT,
1004
+ to_context_id TEXT,
1005
+ continuity_score REAL,
1006
+ transition_type TEXT,
1007
+ created_at TEXT DEFAULT (datetime('now'))
1008
+ );
1009
+
1010
+ -- Endless Mode Config table
1011
+ CREATE TABLE IF NOT EXISTS endless_config (
1012
+ key TEXT PRIMARY KEY,
1013
+ value TEXT,
1014
+ updated_at TEXT DEFAULT (datetime('now'))
1015
+ );
1016
+
1017
+ -- Sync position tracking (for SQLite -> DuckDB sync)
1018
+ CREATE TABLE IF NOT EXISTS sync_positions (
1019
+ target_name TEXT PRIMARY KEY,
1020
+ last_event_id TEXT,
1021
+ last_timestamp TEXT,
1022
+ updated_at TEXT DEFAULT (datetime('now'))
1023
+ );
1024
+
1025
+ -- Create indexes
1026
+ CREATE INDEX IF NOT EXISTS idx_events_session ON events(session_id);
1027
+ CREATE INDEX IF NOT EXISTS idx_events_timestamp ON events(timestamp);
1028
+ CREATE INDEX IF NOT EXISTS idx_entries_type ON entries(entry_type);
1029
+ CREATE INDEX IF NOT EXISTS idx_entries_stage ON entries(stage);
1030
+ CREATE INDEX IF NOT EXISTS idx_entries_canonical ON entries(canonical_key);
1031
+ CREATE INDEX IF NOT EXISTS idx_entities_type_key ON entities(entity_type, canonical_key);
1032
+ CREATE INDEX IF NOT EXISTS idx_entities_status ON entities(status);
1033
+ CREATE INDEX IF NOT EXISTS idx_edges_src ON edges(src_id, rel_type);
1034
+ CREATE INDEX IF NOT EXISTS idx_edges_dst ON edges(dst_id, rel_type);
1035
+ CREATE INDEX IF NOT EXISTS idx_edges_rel ON edges(rel_type);
1036
+ CREATE INDEX IF NOT EXISTS idx_outbox_status ON vector_outbox(status);
1037
+ CREATE INDEX IF NOT EXISTS idx_working_set_expires ON working_set(expires_at);
1038
+ CREATE INDEX IF NOT EXISTS idx_working_set_relevance ON working_set(relevance_score);
1039
+ CREATE INDEX IF NOT EXISTS idx_consolidated_confidence ON consolidated_memories(confidence);
1040
+ CREATE INDEX IF NOT EXISTS idx_continuity_created ON continuity_log(created_at);
1041
+ CREATE INDEX IF NOT EXISTS idx_embedding_outbox_status ON embedding_outbox(status);
1042
+
1043
+ -- FTS5 Full-Text Search for fast keyword search
1044
+ CREATE VIRTUAL TABLE IF NOT EXISTS events_fts USING fts5(
1045
+ content,
1046
+ event_id UNINDEXED,
1047
+ content='events',
1048
+ content_rowid='rowid'
1049
+ );
1050
+
1051
+ -- Triggers to keep FTS in sync with events table
1052
+ CREATE TRIGGER IF NOT EXISTS events_fts_insert AFTER INSERT ON events BEGIN
1053
+ INSERT INTO events_fts(rowid, content, event_id) VALUES (NEW.rowid, NEW.content, NEW.id);
1054
+ END;
1055
+
1056
+ CREATE TRIGGER IF NOT EXISTS events_fts_delete AFTER DELETE ON events BEGIN
1057
+ INSERT INTO events_fts(events_fts, rowid, content, event_id) VALUES('delete', OLD.rowid, OLD.content, OLD.id);
1058
+ END;
1059
+
1060
+ CREATE TRIGGER IF NOT EXISTS events_fts_update AFTER UPDATE ON events BEGIN
1061
+ INSERT INTO events_fts(events_fts, rowid, content, event_id) VALUES('delete', OLD.rowid, OLD.content, OLD.id);
1062
+ INSERT INTO events_fts(rowid, content, event_id) VALUES (NEW.rowid, NEW.content, NEW.id);
1063
+ END;
1064
+ `);
1065
+ const tableInfo = sqliteAll(this.db, "PRAGMA table_info(events)", []);
1066
+ const columnNames = tableInfo.map((col) => col.name);
1067
+ if (!columnNames.includes("access_count")) {
1068
+ try {
1069
+ sqliteExec(this.db, `
1070
+ ALTER TABLE events ADD COLUMN access_count INTEGER DEFAULT 0;
1071
+ `);
1072
+ } catch (err) {
1073
+ console.error("Error adding access_count column:", err);
1074
+ }
1075
+ }
1076
+ if (!columnNames.includes("last_accessed_at")) {
1077
+ try {
1078
+ sqliteExec(this.db, `
1079
+ ALTER TABLE events ADD COLUMN last_accessed_at TEXT;
1080
+ `);
1081
+ } catch (err) {
1082
+ console.error("Error adding last_accessed_at column:", err);
1083
+ }
1084
+ }
1085
+ try {
1086
+ sqliteExec(this.db, `
1087
+ CREATE INDEX IF NOT EXISTS idx_events_access_count ON events(access_count DESC);
1088
+ `);
1089
+ } catch (err) {
1090
+ }
1091
+ try {
1092
+ sqliteExec(this.db, `
1093
+ CREATE INDEX IF NOT EXISTS idx_events_last_accessed ON events(last_accessed_at DESC);
1094
+ `);
1095
+ } catch (err) {
1096
+ }
1097
+ this.initialized = true;
1098
+ }
1099
+ /**
1100
+ * Append event to store (Append-only, Idempotent)
1101
+ */
1102
+ async append(input) {
1103
+ await this.initialize();
1104
+ const canonicalKey = makeCanonicalKey(input.content);
1105
+ const dedupeKey = makeDedupeKey(input.content, input.sessionId);
1106
+ const existing = sqliteGet(
1107
+ this.db,
1108
+ `SELECT event_id FROM event_dedup WHERE dedupe_key = ?`,
1109
+ [dedupeKey]
1110
+ );
1111
+ if (existing) {
1112
+ return {
1113
+ success: true,
1114
+ eventId: existing.event_id,
1115
+ isDuplicate: true
1116
+ };
1117
+ }
1118
+ const id = randomUUID2();
1119
+ const timestamp = toSQLiteTimestamp(input.timestamp);
1120
+ try {
1121
+ const insertEvent = this.db.prepare(`
1122
+ INSERT INTO events (id, event_type, session_id, timestamp, content, canonical_key, dedupe_key, metadata)
1123
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1124
+ `);
1125
+ const insertDedup = this.db.prepare(`
1126
+ INSERT INTO event_dedup (dedupe_key, event_id) VALUES (?, ?)
1127
+ `);
1128
+ const insertLevel = this.db.prepare(`
1129
+ INSERT INTO memory_levels (event_id, level) VALUES (?, 'L0')
1130
+ `);
1131
+ const transaction = this.db.transaction(() => {
1132
+ insertEvent.run(
1133
+ id,
1134
+ input.eventType,
1135
+ input.sessionId,
1136
+ timestamp,
1137
+ input.content,
1138
+ canonicalKey,
1139
+ dedupeKey,
1140
+ JSON.stringify(input.metadata || {})
1141
+ );
1142
+ insertDedup.run(dedupeKey, id);
1143
+ insertLevel.run(id);
1144
+ });
1145
+ transaction();
1146
+ return { success: true, eventId: id, isDuplicate: false };
1147
+ } catch (error) {
1148
+ return {
1149
+ success: false,
1150
+ error: error instanceof Error ? error.message : String(error)
1151
+ };
1152
+ }
1153
+ }
1154
+ /**
1155
+ * Get events by session ID
1156
+ */
1157
+ async getSessionEvents(sessionId) {
1158
+ await this.initialize();
1159
+ const rows = sqliteAll(
1160
+ this.db,
1161
+ `SELECT * FROM events WHERE session_id = ? ORDER BY timestamp ASC`,
1162
+ [sessionId]
1163
+ );
1164
+ return rows.map(this.rowToEvent);
1165
+ }
1166
+ /**
1167
+ * Get recent events
1168
+ */
1169
+ async getRecentEvents(limit = 100) {
1170
+ await this.initialize();
1171
+ const rows = sqliteAll(
1172
+ this.db,
1173
+ `SELECT * FROM events ORDER BY timestamp DESC LIMIT ?`,
1174
+ [limit]
1175
+ );
1176
+ return rows.map(this.rowToEvent);
1177
+ }
1178
+ /**
1179
+ * Get event by ID
1180
+ */
1181
+ async getEvent(id) {
1182
+ await this.initialize();
1183
+ const row = sqliteGet(
1184
+ this.db,
1185
+ `SELECT * FROM events WHERE id = ?`,
1186
+ [id]
1187
+ );
1188
+ if (!row)
1189
+ return null;
1190
+ return this.rowToEvent(row);
1191
+ }
1192
+ /**
1193
+ * Get events since a timestamp (for sync)
1194
+ */
1195
+ async getEventsSince(timestamp, limit = 1e3) {
1196
+ await this.initialize();
1197
+ const rows = sqliteAll(
1198
+ this.db,
1199
+ `SELECT * FROM events WHERE timestamp > ? ORDER BY timestamp ASC LIMIT ?`,
1200
+ [timestamp, limit]
1201
+ );
1202
+ return rows.map(this.rowToEvent);
1203
+ }
1204
+ /**
1205
+ * Create or update session
1206
+ */
1207
+ async upsertSession(session) {
1208
+ await this.initialize();
1209
+ const existing = sqliteGet(
1210
+ this.db,
1211
+ `SELECT id FROM sessions WHERE id = ?`,
1212
+ [session.id]
1213
+ );
1214
+ if (!existing) {
1215
+ sqliteRun(
1216
+ this.db,
1217
+ `INSERT INTO sessions (id, started_at, project_path, tags)
1218
+ VALUES (?, ?, ?, ?)`,
1219
+ [
1220
+ session.id,
1221
+ toSQLiteTimestamp(session.startedAt || /* @__PURE__ */ new Date()),
1222
+ session.projectPath || null,
1223
+ JSON.stringify(session.tags || [])
1224
+ ]
1225
+ );
1226
+ } else {
1227
+ const updates = [];
1228
+ const values = [];
1229
+ if (session.endedAt) {
1230
+ updates.push("ended_at = ?");
1231
+ values.push(toSQLiteTimestamp(session.endedAt));
1232
+ }
1233
+ if (session.summary) {
1234
+ updates.push("summary = ?");
1235
+ values.push(session.summary);
1236
+ }
1237
+ if (session.tags) {
1238
+ updates.push("tags = ?");
1239
+ values.push(JSON.stringify(session.tags));
1240
+ }
1241
+ if (updates.length > 0) {
1242
+ values.push(session.id);
1243
+ sqliteRun(
1244
+ this.db,
1245
+ `UPDATE sessions SET ${updates.join(", ")} WHERE id = ?`,
1246
+ values
1247
+ );
1248
+ }
1249
+ }
1250
+ }
1251
+ /**
1252
+ * Get session by ID
1253
+ */
1254
+ async getSession(id) {
1255
+ await this.initialize();
1256
+ const row = sqliteGet(
1257
+ this.db,
1258
+ `SELECT * FROM sessions WHERE id = ?`,
1259
+ [id]
1260
+ );
1261
+ if (!row)
1262
+ return null;
1263
+ return {
1264
+ id: row.id,
1265
+ startedAt: toDateFromSQLite(row.started_at),
1266
+ endedAt: row.ended_at ? toDateFromSQLite(row.ended_at) : void 0,
1267
+ projectPath: row.project_path,
1268
+ summary: row.summary,
1269
+ tags: row.tags ? JSON.parse(row.tags) : void 0
1270
+ };
1271
+ }
1272
+ /**
1273
+ * Get all sessions
1274
+ */
1275
+ async getAllSessions() {
1276
+ await this.initialize();
1277
+ const rows = sqliteAll(
1278
+ this.db,
1279
+ `SELECT * FROM sessions ORDER BY started_at DESC`
1280
+ );
1281
+ return rows.map((row) => ({
1282
+ id: row.id,
1283
+ startedAt: toDateFromSQLite(row.started_at),
1284
+ endedAt: row.ended_at ? toDateFromSQLite(row.ended_at) : void 0,
1285
+ projectPath: row.project_path,
1286
+ summary: row.summary,
1287
+ tags: row.tags ? JSON.parse(row.tags) : void 0
1288
+ }));
1289
+ }
1290
+ /**
1291
+ * Add to embedding outbox
1292
+ */
1293
+ async enqueueForEmbedding(eventId, content) {
1294
+ await this.initialize();
1295
+ const id = randomUUID2();
1296
+ sqliteRun(
1297
+ this.db,
1298
+ `INSERT INTO embedding_outbox (id, event_id, content, status, retry_count)
1299
+ VALUES (?, ?, ?, 'pending', 0)`,
1300
+ [id, eventId, content]
1301
+ );
1302
+ return id;
1303
+ }
1304
+ /**
1305
+ * Get pending outbox items
1306
+ */
1307
+ async getPendingOutboxItems(limit = 32) {
1308
+ await this.initialize();
1309
+ const pending = sqliteAll(
1310
+ this.db,
1311
+ `SELECT * FROM embedding_outbox
1312
+ WHERE status = 'pending'
1313
+ ORDER BY created_at
1314
+ LIMIT ?`,
1315
+ [limit]
1316
+ );
1317
+ if (pending.length === 0)
1318
+ return [];
1319
+ const ids = pending.map((r) => r.id);
1320
+ const placeholders = ids.map(() => "?").join(",");
1321
+ sqliteRun(
1322
+ this.db,
1323
+ `UPDATE embedding_outbox SET status = 'processing' WHERE id IN (${placeholders})`,
1324
+ ids
1325
+ );
1326
+ return pending.map((row) => ({
1327
+ id: row.id,
1328
+ eventId: row.event_id,
1329
+ content: row.content,
1330
+ status: "processing",
1331
+ retryCount: row.retry_count,
1332
+ createdAt: toDateFromSQLite(row.created_at),
1333
+ errorMessage: row.error_message
1334
+ }));
1335
+ }
1336
+ /**
1337
+ * Mark outbox items as done
1338
+ */
1339
+ async completeOutboxItems(ids) {
1340
+ if (ids.length === 0)
1341
+ return;
1342
+ const placeholders = ids.map(() => "?").join(",");
1343
+ sqliteRun(
1344
+ this.db,
1345
+ `DELETE FROM embedding_outbox WHERE id IN (${placeholders})`,
1346
+ ids
1347
+ );
1348
+ }
1349
+ /**
1350
+ * Mark outbox items as failed
1351
+ */
1352
+ async failOutboxItems(ids, error) {
1353
+ if (ids.length === 0)
1354
+ return;
1355
+ const placeholders = ids.map(() => "?").join(",");
1356
+ sqliteRun(
1357
+ this.db,
1358
+ `UPDATE embedding_outbox
1359
+ SET status = CASE WHEN retry_count >= 3 THEN 'failed' ELSE 'pending' END,
1360
+ retry_count = retry_count + 1,
1361
+ error_message = ?
1362
+ WHERE id IN (${placeholders})`,
1363
+ [error, ...ids]
1364
+ );
1365
+ }
1366
+ /**
1367
+ * Update memory level
1368
+ */
1369
+ async updateMemoryLevel(eventId, level) {
1370
+ await this.initialize();
1371
+ sqliteRun(
1372
+ this.db,
1373
+ `UPDATE memory_levels SET level = ?, promoted_at = datetime('now') WHERE event_id = ?`,
1374
+ [level, eventId]
1375
+ );
1376
+ }
1377
+ /**
1378
+ * Get memory level statistics
1379
+ */
1380
+ async getLevelStats() {
1381
+ await this.initialize();
1382
+ const rows = sqliteAll(
1383
+ this.db,
1384
+ `SELECT level, COUNT(*) as count FROM memory_levels GROUP BY level`
1385
+ );
1386
+ return rows;
1387
+ }
1388
+ /**
1389
+ * Get events by memory level
1390
+ */
1391
+ async getEventsByLevel(level, options) {
1392
+ await this.initialize();
1393
+ const limit = options?.limit || 50;
1394
+ const offset = options?.offset || 0;
1395
+ const rows = sqliteAll(
1396
+ this.db,
1397
+ `SELECT e.* FROM events e
1398
+ INNER JOIN memory_levels ml ON e.id = ml.event_id
1399
+ WHERE ml.level = ?
1400
+ ORDER BY e.timestamp DESC
1401
+ LIMIT ? OFFSET ?`,
1402
+ [level, limit, offset]
1403
+ );
1404
+ return rows.map((row) => this.rowToEvent(row));
1405
+ }
1406
+ /**
1407
+ * Get memory level for a specific event
1408
+ */
1409
+ async getEventLevel(eventId) {
1410
+ await this.initialize();
1411
+ const row = sqliteGet(
1412
+ this.db,
1413
+ `SELECT level FROM memory_levels WHERE event_id = ?`,
1414
+ [eventId]
1415
+ );
1416
+ return row ? row.level : null;
1417
+ }
1418
+ /**
1419
+ * Get sync position for a target
1420
+ */
1421
+ async getSyncPosition(targetName) {
1422
+ await this.initialize();
1423
+ const row = sqliteGet(
1424
+ this.db,
1425
+ `SELECT last_event_id, last_timestamp FROM sync_positions WHERE target_name = ?`,
1426
+ [targetName]
1427
+ );
1428
+ return {
1429
+ lastEventId: row?.last_event_id ?? null,
1430
+ lastTimestamp: row?.last_timestamp ?? null
1431
+ };
1432
+ }
1433
+ /**
1434
+ * Update sync position for a target
1435
+ */
1436
+ async updateSyncPosition(targetName, lastEventId, lastTimestamp) {
1437
+ await this.initialize();
1438
+ sqliteRun(
1439
+ this.db,
1440
+ `INSERT OR REPLACE INTO sync_positions (target_name, last_event_id, last_timestamp, updated_at)
1441
+ VALUES (?, ?, ?, datetime('now'))`,
1442
+ [targetName, lastEventId, lastTimestamp]
1443
+ );
1444
+ }
1445
+ /**
1446
+ * Get config value for endless mode
1447
+ */
1448
+ async getEndlessConfig(key) {
1449
+ await this.initialize();
1450
+ const row = sqliteGet(
1451
+ this.db,
1452
+ `SELECT value FROM endless_config WHERE key = ?`,
1453
+ [key]
1454
+ );
1455
+ if (!row)
1456
+ return null;
1457
+ return JSON.parse(row.value);
1458
+ }
1459
+ /**
1460
+ * Set config value for endless mode
1461
+ */
1462
+ async setEndlessConfig(key, value) {
1463
+ await this.initialize();
1464
+ sqliteRun(
1465
+ this.db,
1466
+ `INSERT OR REPLACE INTO endless_config (key, value, updated_at)
1467
+ VALUES (?, ?, datetime('now'))`,
1468
+ [key, JSON.stringify(value)]
1469
+ );
1470
+ }
1471
+ /**
1472
+ * Increment access count for events
1473
+ */
1474
+ async incrementAccessCount(eventIds) {
1475
+ if (eventIds.length === 0 || this.readOnly)
1476
+ return;
1477
+ await this.initialize();
1478
+ const placeholders = eventIds.map(() => "?").join(",");
1479
+ const currentTime = toSQLiteTimestamp(/* @__PURE__ */ new Date());
1480
+ sqliteRun(
1481
+ this.db,
1482
+ `UPDATE events
1483
+ SET access_count = access_count + 1,
1484
+ last_accessed_at = ?
1485
+ WHERE id IN (${placeholders})`,
1486
+ [currentTime, ...eventIds]
1487
+ );
1488
+ }
1489
+ /**
1490
+ * Get most accessed memories
1491
+ */
1492
+ async getMostAccessed(limit = 10) {
1493
+ await this.initialize();
1494
+ const rows = sqliteAll(
1495
+ this.db,
1496
+ `SELECT * FROM events
1497
+ WHERE access_count > 0
1498
+ ORDER BY access_count DESC, last_accessed_at DESC
1499
+ LIMIT ?`,
1500
+ [limit]
1501
+ );
1502
+ return rows.map((row) => this.rowToEvent(row));
1503
+ }
1504
+ /**
1505
+ * Fast keyword search using FTS5
1506
+ * Returns events matching the search query, ranked by relevance
1507
+ */
1508
+ async keywordSearch(query, limit = 10) {
1509
+ await this.initialize();
1510
+ const searchTerms = query.replace(/['"(){}[\]^~*?:\\/-]/g, " ").split(/\s+/).filter((term) => term.length > 1).map((term) => `"${term}"*`).join(" OR ");
1511
+ if (!searchTerms) {
1512
+ return [];
1513
+ }
1514
+ try {
1515
+ const rows = sqliteAll(
1516
+ this.db,
1517
+ `SELECT e.*, fts.rank
1518
+ FROM events_fts fts
1519
+ JOIN events e ON e.id = fts.event_id
1520
+ WHERE events_fts MATCH ?
1521
+ ORDER BY fts.rank
1522
+ LIMIT ?`,
1523
+ [searchTerms, limit]
1524
+ );
1525
+ return rows.map((row) => ({
1526
+ event: this.rowToEvent(row),
1527
+ rank: row.rank
1528
+ }));
1529
+ } catch (error) {
1530
+ const likePattern = `%${query}%`;
1531
+ const rows = sqliteAll(
1532
+ this.db,
1533
+ `SELECT *, 0 as rank FROM events
1534
+ WHERE content LIKE ?
1535
+ ORDER BY timestamp DESC
1536
+ LIMIT ?`,
1537
+ [likePattern, limit]
1538
+ );
1539
+ return rows.map((row) => ({
1540
+ event: this.rowToEvent(row),
1541
+ rank: 0
1542
+ }));
1543
+ }
1544
+ }
1545
+ /**
1546
+ * Rebuild FTS index from existing events
1547
+ * Call this once after upgrading to FTS5
1548
+ */
1549
+ async rebuildFtsIndex() {
1550
+ await this.initialize();
1551
+ const countRow = sqliteGet(this.db, "SELECT COUNT(*) as count FROM events", []);
1552
+ const totalEvents = countRow?.count ?? 0;
1553
+ sqliteExec(this.db, `
1554
+ DELETE FROM events_fts;
1555
+ INSERT INTO events_fts(rowid, content, event_id)
1556
+ SELECT rowid, content, id FROM events;
1557
+ `);
1558
+ return totalEvents;
1559
+ }
1560
+ /**
1561
+ * Get database instance for direct access
1562
+ */
1563
+ getDatabase() {
1564
+ return this.db;
1565
+ }
709
1566
  /**
710
1567
  * Close database connection
711
1568
  */
712
1569
  async close() {
713
- await dbClose(this.db);
1570
+ sqliteClose(this.db);
714
1571
  }
715
1572
  /**
716
1573
  * Convert database row to MemoryEvent
717
1574
  */
718
1575
  rowToEvent(row) {
719
- return {
1576
+ const event = {
720
1577
  id: row.id,
721
1578
  eventType: row.event_type,
722
1579
  sessionId: row.session_id,
723
- timestamp: toDate(row.timestamp),
1580
+ timestamp: toDateFromSQLite(row.timestamp),
724
1581
  content: row.content,
725
1582
  canonicalKey: row.canonical_key,
726
1583
  dedupeKey: row.dedupe_key,
727
1584
  metadata: row.metadata ? JSON.parse(row.metadata) : void 0
728
1585
  };
1586
+ if (row.access_count !== void 0) {
1587
+ event.access_count = row.access_count;
1588
+ }
1589
+ if (row.last_accessed_at !== void 0) {
1590
+ event.last_accessed_at = row.last_accessed_at;
1591
+ }
1592
+ return event;
1593
+ }
1594
+ };
1595
+
1596
+ // src/core/sync-worker.ts
1597
+ var DEFAULT_CONFIG = {
1598
+ intervalMs: 3e4,
1599
+ batchSize: 500,
1600
+ maxRetries: 3,
1601
+ retryDelayMs: 5e3
1602
+ };
1603
+ var SyncWorker = class {
1604
+ constructor(sqliteStore, duckdbStore, config) {
1605
+ this.sqliteStore = sqliteStore;
1606
+ this.duckdbStore = duckdbStore;
1607
+ this.config = { ...DEFAULT_CONFIG, ...config };
1608
+ }
1609
+ config;
1610
+ intervalHandle = null;
1611
+ running = false;
1612
+ stats = {
1613
+ lastSyncAt: null,
1614
+ eventsSynced: 0,
1615
+ sessionsSynced: 0,
1616
+ errors: 0,
1617
+ status: "idle"
1618
+ };
1619
+ /**
1620
+ * Start the sync worker
1621
+ */
1622
+ start() {
1623
+ if (this.running)
1624
+ return;
1625
+ this.running = true;
1626
+ this.stats.status = "idle";
1627
+ this.syncNow().catch((err) => {
1628
+ console.error("[SyncWorker] Initial sync failed:", err);
1629
+ });
1630
+ this.intervalHandle = setInterval(() => {
1631
+ this.syncNow().catch((err) => {
1632
+ console.error("[SyncWorker] Periodic sync failed:", err);
1633
+ });
1634
+ }, this.config.intervalMs);
1635
+ }
1636
+ /**
1637
+ * Stop the sync worker
1638
+ */
1639
+ stop() {
1640
+ this.running = false;
1641
+ this.stats.status = "stopped";
1642
+ if (this.intervalHandle) {
1643
+ clearInterval(this.intervalHandle);
1644
+ this.intervalHandle = null;
1645
+ }
1646
+ }
1647
+ /**
1648
+ * Trigger immediate sync
1649
+ */
1650
+ async syncNow() {
1651
+ if (this.stats.status === "syncing") {
1652
+ return;
1653
+ }
1654
+ this.stats.status = "syncing";
1655
+ try {
1656
+ await this.syncEvents();
1657
+ await this.syncSessions();
1658
+ this.stats.lastSyncAt = /* @__PURE__ */ new Date();
1659
+ this.stats.status = "idle";
1660
+ } catch (error) {
1661
+ this.stats.errors++;
1662
+ this.stats.status = "error";
1663
+ throw error;
1664
+ }
1665
+ }
1666
+ /**
1667
+ * Sync events from SQLite to DuckDB
1668
+ */
1669
+ async syncEvents() {
1670
+ const targetName = "duckdb_analytics";
1671
+ const position = await this.sqliteStore.getSyncPosition(targetName);
1672
+ const lastTimestamp = position.lastTimestamp || "1970-01-01T00:00:00.000Z";
1673
+ let hasMore = true;
1674
+ let totalSynced = 0;
1675
+ while (hasMore) {
1676
+ const events = await this.sqliteStore.getEventsSince(lastTimestamp, this.config.batchSize);
1677
+ if (events.length === 0) {
1678
+ hasMore = false;
1679
+ break;
1680
+ }
1681
+ await this.retryWithBackoff(async () => {
1682
+ for (const event of events) {
1683
+ await this.insertEventToDuckDB(event);
1684
+ }
1685
+ });
1686
+ totalSynced += events.length;
1687
+ const lastEvent = events[events.length - 1];
1688
+ await this.sqliteStore.updateSyncPosition(
1689
+ targetName,
1690
+ lastEvent.id,
1691
+ lastEvent.timestamp.toISOString()
1692
+ );
1693
+ hasMore = events.length === this.config.batchSize;
1694
+ }
1695
+ this.stats.eventsSynced += totalSynced;
1696
+ }
1697
+ /**
1698
+ * Sync sessions from SQLite to DuckDB
1699
+ */
1700
+ async syncSessions() {
1701
+ const sessions = await this.sqliteStore.getAllSessions();
1702
+ for (const session of sessions) {
1703
+ await this.retryWithBackoff(async () => {
1704
+ await this.duckdbStore.upsertSession(session);
1705
+ });
1706
+ }
1707
+ this.stats.sessionsSynced = sessions.length;
1708
+ }
1709
+ /**
1710
+ * Insert a single event into DuckDB
1711
+ */
1712
+ async insertEventToDuckDB(event) {
1713
+ await this.duckdbStore.append({
1714
+ eventType: event.eventType,
1715
+ sessionId: event.sessionId,
1716
+ timestamp: event.timestamp,
1717
+ content: event.content,
1718
+ metadata: event.metadata
1719
+ });
1720
+ }
1721
+ /**
1722
+ * Retry operation with exponential backoff
1723
+ */
1724
+ async retryWithBackoff(fn) {
1725
+ let lastError = null;
1726
+ for (let attempt = 0; attempt < this.config.maxRetries; attempt++) {
1727
+ try {
1728
+ return await fn();
1729
+ } catch (error) {
1730
+ lastError = error instanceof Error ? error : new Error(String(error));
1731
+ if (attempt < this.config.maxRetries - 1) {
1732
+ const delay = this.config.retryDelayMs * Math.pow(2, attempt);
1733
+ await this.sleep(delay);
1734
+ }
1735
+ }
1736
+ }
1737
+ throw lastError;
1738
+ }
1739
+ /**
1740
+ * Sleep utility
1741
+ */
1742
+ sleep(ms) {
1743
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
1744
+ }
1745
+ /**
1746
+ * Get sync statistics
1747
+ */
1748
+ getStats() {
1749
+ return { ...this.stats };
1750
+ }
1751
+ /**
1752
+ * Check if worker is running
1753
+ */
1754
+ isRunning() {
1755
+ return this.running;
729
1756
  }
730
1757
  };
731
1758
 
@@ -957,7 +1984,7 @@ function getDefaultEmbedder() {
957
1984
  }
958
1985
 
959
1986
  // src/core/vector-outbox.ts
960
- var DEFAULT_CONFIG = {
1987
+ var DEFAULT_CONFIG2 = {
961
1988
  embeddingVersion: "v1",
962
1989
  maxRetries: 3,
963
1990
  stuckThresholdMs: 5 * 60 * 1e3,
@@ -966,7 +1993,7 @@ var DEFAULT_CONFIG = {
966
1993
  };
967
1994
 
968
1995
  // src/core/vector-worker.ts
969
- var DEFAULT_CONFIG2 = {
1996
+ var DEFAULT_CONFIG3 = {
970
1997
  batchSize: 32,
971
1998
  pollIntervalMs: 1e3,
972
1999
  maxRetries: 3
@@ -977,12 +2004,13 @@ var VectorWorker = class {
977
2004
  embedder;
978
2005
  config;
979
2006
  running = false;
2007
+ stopping = false;
980
2008
  pollTimeout = null;
981
2009
  constructor(eventStore, vectorStore, embedder, config = {}) {
982
2010
  this.eventStore = eventStore;
983
2011
  this.vectorStore = vectorStore;
984
2012
  this.embedder = embedder;
985
- this.config = { ...DEFAULT_CONFIG2, ...config };
2013
+ this.config = { ...DEFAULT_CONFIG3, ...config };
986
2014
  }
987
2015
  /**
988
2016
  * Start the worker polling loop
@@ -991,6 +2019,7 @@ var VectorWorker = class {
991
2019
  if (this.running)
992
2020
  return;
993
2021
  this.running = true;
2022
+ this.stopping = false;
994
2023
  this.poll();
995
2024
  }
996
2025
  /**
@@ -998,6 +2027,7 @@ var VectorWorker = class {
998
2027
  */
999
2028
  stop() {
1000
2029
  this.running = false;
2030
+ this.stopping = true;
1001
2031
  if (this.pollTimeout) {
1002
2032
  clearTimeout(this.pollTimeout);
1003
2033
  this.pollTimeout = null;
@@ -1047,9 +2077,15 @@ var VectorWorker = class {
1047
2077
  }
1048
2078
  return successful.length;
1049
2079
  } catch (error) {
1050
- const allIds = items.map((i) => i.id);
1051
- const errorMessage = error instanceof Error ? error.message : String(error);
1052
- await this.eventStore.failOutboxItems(allIds, errorMessage);
2080
+ if (!this.stopping) {
2081
+ try {
2082
+ const allIds = items.map((i) => i.id);
2083
+ const errorMessage = error instanceof Error ? error.message : String(error);
2084
+ await this.eventStore.failOutboxItems(allIds, errorMessage);
2085
+ } catch (failError) {
2086
+ console.warn("Could not mark outbox items as failed (database may be closed)");
2087
+ }
2088
+ }
1053
2089
  throw error;
1054
2090
  }
1055
2091
  }
@@ -1057,14 +2093,18 @@ var VectorWorker = class {
1057
2093
  * Poll for new items
1058
2094
  */
1059
2095
  async poll() {
1060
- if (!this.running)
2096
+ if (!this.running || this.stopping)
1061
2097
  return;
1062
2098
  try {
1063
2099
  await this.processBatch();
1064
2100
  } catch (error) {
1065
- console.error("Vector worker error:", error);
2101
+ if (!this.stopping) {
2102
+ console.error("Vector worker error:", error);
2103
+ }
2104
+ }
2105
+ if (this.running && !this.stopping) {
2106
+ this.pollTimeout = setTimeout(() => this.poll(), this.config.pollIntervalMs);
1066
2107
  }
1067
- this.pollTimeout = setTimeout(() => this.poll(), this.config.pollIntervalMs);
1068
2108
  }
1069
2109
  /**
1070
2110
  * Process all pending items (blocking)
@@ -1091,7 +2131,7 @@ function createVectorWorker(eventStore, vectorStore, embedder, config) {
1091
2131
  }
1092
2132
 
1093
2133
  // src/core/matcher.ts
1094
- var DEFAULT_CONFIG3 = {
2134
+ var DEFAULT_CONFIG4 = {
1095
2135
  weights: {
1096
2136
  semanticSimilarity: 0.4,
1097
2137
  ftsScore: 0.25,
@@ -1106,9 +2146,9 @@ var Matcher = class {
1106
2146
  config;
1107
2147
  constructor(config = {}) {
1108
2148
  this.config = {
1109
- ...DEFAULT_CONFIG3,
2149
+ ...DEFAULT_CONFIG4,
1110
2150
  ...config,
1111
- weights: { ...DEFAULT_CONFIG3.weights, ...config.weights }
2151
+ weights: { ...DEFAULT_CONFIG4.weights, ...config.weights }
1112
2152
  };
1113
2153
  }
1114
2154
  /**
@@ -1816,7 +2856,7 @@ function createSharedEventStore(dbPath) {
1816
2856
  }
1817
2857
 
1818
2858
  // src/core/shared-store.ts
1819
- import { randomUUID as randomUUID2 } from "crypto";
2859
+ import { randomUUID as randomUUID3 } from "crypto";
1820
2860
  var SharedStore = class {
1821
2861
  constructor(sharedEventStore) {
1822
2862
  this.sharedEventStore = sharedEventStore;
@@ -1828,7 +2868,7 @@ var SharedStore = class {
1828
2868
  * Promote a verified troubleshooting entry to shared storage
1829
2869
  */
1830
2870
  async promoteEntry(input) {
1831
- const entryId = randomUUID2();
2871
+ const entryId = randomUUID3();
1832
2872
  await dbRun(
1833
2873
  this.db,
1834
2874
  `INSERT INTO shared_troubleshooting (
@@ -2193,7 +3233,7 @@ function createSharedVectorStore(dbPath) {
2193
3233
  }
2194
3234
 
2195
3235
  // src/core/shared-promoter.ts
2196
- import { randomUUID as randomUUID3 } from "crypto";
3236
+ import { randomUUID as randomUUID4 } from "crypto";
2197
3237
  var SharedPromoter = class {
2198
3238
  constructor(sharedStore, sharedVectorStore, embedder, config) {
2199
3239
  this.sharedStore = sharedStore;
@@ -2261,7 +3301,7 @@ var SharedPromoter = class {
2261
3301
  const embeddingContent = this.createEmbeddingContent(input);
2262
3302
  const embedding = await this.embedder.embed(embeddingContent);
2263
3303
  await this.sharedVectorStore.upsert({
2264
- id: randomUUID3(),
3304
+ id: randomUUID4(),
2265
3305
  entryId,
2266
3306
  entryType: "troubleshooting",
2267
3307
  content: embeddingContent,
@@ -2401,7 +3441,7 @@ function createToolObservationEmbedding(toolName, metadata, success) {
2401
3441
  }
2402
3442
 
2403
3443
  // src/core/working-set-store.ts
2404
- import { randomUUID as randomUUID4 } from "crypto";
3444
+ import { randomUUID as randomUUID5 } from "crypto";
2405
3445
  var WorkingSetStore = class {
2406
3446
  constructor(eventStore, config) {
2407
3447
  this.eventStore = eventStore;
@@ -2422,7 +3462,7 @@ var WorkingSetStore = class {
2422
3462
  `INSERT OR REPLACE INTO working_set (id, event_id, added_at, relevance_score, topics, expires_at)
2423
3463
  VALUES (?, ?, CURRENT_TIMESTAMP, ?, ?, ?)`,
2424
3464
  [
2425
- randomUUID4(),
3465
+ randomUUID5(),
2426
3466
  eventId,
2427
3467
  relevanceScore,
2428
3468
  JSON.stringify(topics || []),
@@ -2606,7 +3646,7 @@ function createWorkingSetStore(eventStore, config) {
2606
3646
  }
2607
3647
 
2608
3648
  // src/core/consolidated-store.ts
2609
- import { randomUUID as randomUUID5 } from "crypto";
3649
+ import { randomUUID as randomUUID6 } from "crypto";
2610
3650
  var ConsolidatedStore = class {
2611
3651
  constructor(eventStore) {
2612
3652
  this.eventStore = eventStore;
@@ -2618,7 +3658,7 @@ var ConsolidatedStore = class {
2618
3658
  * Create a new consolidated memory
2619
3659
  */
2620
3660
  async create(input) {
2621
- const memoryId = randomUUID5();
3661
+ const memoryId = randomUUID6();
2622
3662
  await dbRun(
2623
3663
  this.db,
2624
3664
  `INSERT INTO consolidated_memories
@@ -3145,7 +4185,7 @@ function createConsolidationWorker(workingSetStore, consolidatedStore, config) {
3145
4185
  }
3146
4186
 
3147
4187
  // src/core/continuity-manager.ts
3148
- import { randomUUID as randomUUID6 } from "crypto";
4188
+ import { randomUUID as randomUUID7 } from "crypto";
3149
4189
  var ContinuityManager = class {
3150
4190
  constructor(eventStore, config) {
3151
4191
  this.eventStore = eventStore;
@@ -3299,7 +4339,7 @@ var ContinuityManager = class {
3299
4339
  `INSERT INTO continuity_log
3300
4340
  (log_id, from_context_id, to_context_id, continuity_score, transition_type, created_at)
3301
4341
  VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,
3302
- [randomUUID6(), previous.id, current.id, score, type]
4342
+ [randomUUID7(), previous.id, current.id, score, type]
3303
4343
  );
3304
4344
  }
3305
4345
  /**
@@ -3408,7 +4448,7 @@ function createContinuityManager(eventStore, config) {
3408
4448
  }
3409
4449
 
3410
4450
  // src/core/graduation-worker.ts
3411
- var DEFAULT_CONFIG4 = {
4451
+ var DEFAULT_CONFIG5 = {
3412
4452
  evaluationIntervalMs: 3e5,
3413
4453
  // 5 minutes
3414
4454
  batchSize: 50,
@@ -3416,7 +4456,7 @@ var DEFAULT_CONFIG4 = {
3416
4456
  // 1 hour cooldown between evaluations
3417
4457
  };
3418
4458
  var GraduationWorker = class {
3419
- constructor(eventStore, graduation, config = DEFAULT_CONFIG4) {
4459
+ constructor(eventStore, graduation, config = DEFAULT_CONFIG5) {
3420
4460
  this.eventStore = eventStore;
3421
4461
  this.graduation = graduation;
3422
4462
  this.config = config;
@@ -3524,7 +4564,7 @@ function createGraduationWorker(eventStore, graduation, config) {
3524
4564
  return new GraduationWorker(
3525
4565
  eventStore,
3526
4566
  graduation,
3527
- { ...DEFAULT_CONFIG4, ...config }
4567
+ { ...DEFAULT_CONFIG5, ...config }
3528
4568
  );
3529
4569
  }
3530
4570
 
@@ -3548,7 +4588,11 @@ function getProjectStoragePath(projectPath) {
3548
4588
  var REGISTRY_PATH = path.join(os.homedir(), ".claude-code", "memory", "session-registry.json");
3549
4589
  var SHARED_STORAGE_PATH = path.join(os.homedir(), ".claude-code", "memory", "shared");
3550
4590
  var MemoryService = class {
3551
- eventStore;
4591
+ // Primary store: SQLite (WAL mode) - for hooks, always available
4592
+ sqliteStore;
4593
+ // Analytics store: DuckDB - for server reads (optional, synced from SQLite)
4594
+ analyticsStore;
4595
+ syncWorker = null;
3552
4596
  vectorStore;
3553
4597
  embedder;
3554
4598
  matcher;
@@ -3571,25 +4615,49 @@ var MemoryService = class {
3571
4615
  sharedStoreConfig = null;
3572
4616
  projectHash = null;
3573
4617
  readOnly;
4618
+ lightweightMode;
3574
4619
  constructor(config) {
3575
4620
  const storagePath = this.expandPath(config.storagePath);
3576
4621
  this.readOnly = config.readOnly ?? false;
4622
+ this.lightweightMode = config.lightweightMode ?? false;
3577
4623
  if (!this.readOnly && !fs.existsSync(storagePath)) {
3578
4624
  fs.mkdirSync(storagePath, { recursive: true });
3579
4625
  }
3580
4626
  this.projectHash = config.projectHash || null;
3581
4627
  this.sharedStoreConfig = config.sharedStoreConfig ?? { enabled: true };
3582
- this.eventStore = new EventStore(path.join(storagePath, "events.duckdb"), { readOnly: this.readOnly });
4628
+ this.sqliteStore = new SQLiteEventStore(
4629
+ path.join(storagePath, "events.sqlite"),
4630
+ { readonly: this.readOnly }
4631
+ );
4632
+ const analyticsEnabled = config.analyticsEnabled ?? this.readOnly;
4633
+ if (!analyticsEnabled) {
4634
+ this.analyticsStore = null;
4635
+ } else if (this.readOnly) {
4636
+ try {
4637
+ this.analyticsStore = new EventStore(
4638
+ path.join(storagePath, "analytics.duckdb"),
4639
+ { readOnly: true }
4640
+ );
4641
+ } catch {
4642
+ this.analyticsStore = null;
4643
+ }
4644
+ } else {
4645
+ this.analyticsStore = new EventStore(
4646
+ path.join(storagePath, "analytics.duckdb"),
4647
+ { readOnly: false }
4648
+ );
4649
+ }
3583
4650
  this.vectorStore = new VectorStore(path.join(storagePath, "vectors"));
3584
4651
  this.embedder = config.embeddingModel ? new Embedder(config.embeddingModel) : getDefaultEmbedder();
3585
4652
  this.matcher = getDefaultMatcher();
3586
4653
  this.retriever = createRetriever(
3587
- this.eventStore,
4654
+ this.sqliteStore,
4655
+ // Interface compatible
3588
4656
  this.vectorStore,
3589
4657
  this.embedder,
3590
4658
  this.matcher
3591
4659
  );
3592
- this.graduation = createGraduationPipeline(this.eventStore);
4660
+ this.graduation = createGraduationPipeline(this.sqliteStore);
3593
4661
  }
3594
4662
  /**
3595
4663
  * Initialize all components
@@ -3597,23 +4665,42 @@ var MemoryService = class {
3597
4665
  async initialize() {
3598
4666
  if (this.initialized)
3599
4667
  return;
3600
- await this.eventStore.initialize();
4668
+ await this.sqliteStore.initialize();
4669
+ if (this.lightweightMode) {
4670
+ this.initialized = true;
4671
+ return;
4672
+ }
4673
+ if (this.analyticsStore) {
4674
+ try {
4675
+ await this.analyticsStore.initialize();
4676
+ } catch (error) {
4677
+ console.warn("[MemoryService] Analytics store (DuckDB) initialization failed, using SQLite for reads:", error);
4678
+ }
4679
+ }
3601
4680
  await this.vectorStore.initialize();
3602
4681
  await this.embedder.initialize();
3603
4682
  if (!this.readOnly) {
3604
4683
  this.vectorWorker = createVectorWorker(
3605
- this.eventStore,
4684
+ this.sqliteStore,
3606
4685
  this.vectorStore,
3607
4686
  this.embedder
3608
4687
  );
3609
4688
  this.vectorWorker.start();
3610
4689
  this.retriever.setGraduationPipeline(this.graduation);
3611
4690
  this.graduationWorker = createGraduationWorker(
3612
- this.eventStore,
4691
+ this.sqliteStore,
3613
4692
  this.graduation
3614
4693
  );
3615
4694
  this.graduationWorker.start();
3616
- const savedMode = await this.eventStore.getEndlessConfig("mode");
4695
+ if (this.analyticsStore) {
4696
+ this.syncWorker = new SyncWorker(
4697
+ this.sqliteStore,
4698
+ this.analyticsStore,
4699
+ { intervalMs: 3e4, batchSize: 500 }
4700
+ );
4701
+ this.syncWorker.start();
4702
+ }
4703
+ const savedMode = await this.sqliteStore.getEndlessConfig("mode");
3617
4704
  if (savedMode === "endless") {
3618
4705
  this.endlessMode = "endless";
3619
4706
  await this.initializeEndlessMode();
@@ -3654,7 +4741,7 @@ var MemoryService = class {
3654
4741
  */
3655
4742
  async startSession(sessionId, projectPath) {
3656
4743
  await this.initialize();
3657
- await this.eventStore.upsertSession({
4744
+ await this.sqliteStore.upsertSession({
3658
4745
  id: sessionId,
3659
4746
  startedAt: /* @__PURE__ */ new Date(),
3660
4747
  projectPath
@@ -3665,7 +4752,7 @@ var MemoryService = class {
3665
4752
  */
3666
4753
  async endSession(sessionId, summary) {
3667
4754
  await this.initialize();
3668
- await this.eventStore.upsertSession({
4755
+ await this.sqliteStore.upsertSession({
3669
4756
  id: sessionId,
3670
4757
  endedAt: /* @__PURE__ */ new Date(),
3671
4758
  summary
@@ -3676,7 +4763,7 @@ var MemoryService = class {
3676
4763
  */
3677
4764
  async storeUserPrompt(sessionId, content, metadata) {
3678
4765
  await this.initialize();
3679
- const result = await this.eventStore.append({
4766
+ const result = await this.sqliteStore.append({
3680
4767
  eventType: "user_prompt",
3681
4768
  sessionId,
3682
4769
  timestamp: /* @__PURE__ */ new Date(),
@@ -3684,7 +4771,7 @@ var MemoryService = class {
3684
4771
  metadata
3685
4772
  });
3686
4773
  if (result.success && !result.isDuplicate) {
3687
- await this.eventStore.enqueueForEmbedding(result.eventId, content);
4774
+ await this.sqliteStore.enqueueForEmbedding(result.eventId, content);
3688
4775
  }
3689
4776
  return result;
3690
4777
  }
@@ -3693,7 +4780,7 @@ var MemoryService = class {
3693
4780
  */
3694
4781
  async storeAgentResponse(sessionId, content, metadata) {
3695
4782
  await this.initialize();
3696
- const result = await this.eventStore.append({
4783
+ const result = await this.sqliteStore.append({
3697
4784
  eventType: "agent_response",
3698
4785
  sessionId,
3699
4786
  timestamp: /* @__PURE__ */ new Date(),
@@ -3701,7 +4788,7 @@ var MemoryService = class {
3701
4788
  metadata
3702
4789
  });
3703
4790
  if (result.success && !result.isDuplicate) {
3704
- await this.eventStore.enqueueForEmbedding(result.eventId, content);
4791
+ await this.sqliteStore.enqueueForEmbedding(result.eventId, content);
3705
4792
  }
3706
4793
  return result;
3707
4794
  }
@@ -3710,14 +4797,14 @@ var MemoryService = class {
3710
4797
  */
3711
4798
  async storeSessionSummary(sessionId, summary) {
3712
4799
  await this.initialize();
3713
- const result = await this.eventStore.append({
4800
+ const result = await this.sqliteStore.append({
3714
4801
  eventType: "session_summary",
3715
4802
  sessionId,
3716
4803
  timestamp: /* @__PURE__ */ new Date(),
3717
4804
  content: summary
3718
4805
  });
3719
4806
  if (result.success && !result.isDuplicate) {
3720
- await this.eventStore.enqueueForEmbedding(result.eventId, summary);
4807
+ await this.sqliteStore.enqueueForEmbedding(result.eventId, summary);
3721
4808
  }
3722
4809
  return result;
3723
4810
  }
@@ -3727,7 +4814,7 @@ var MemoryService = class {
3727
4814
  async storeToolObservation(sessionId, payload) {
3728
4815
  await this.initialize();
3729
4816
  const content = JSON.stringify(payload);
3730
- const result = await this.eventStore.append({
4817
+ const result = await this.sqliteStore.append({
3731
4818
  eventType: "tool_observation",
3732
4819
  sessionId,
3733
4820
  timestamp: /* @__PURE__ */ new Date(),
@@ -3743,7 +4830,7 @@ var MemoryService = class {
3743
4830
  payload.metadata || {},
3744
4831
  payload.success
3745
4832
  );
3746
- await this.eventStore.enqueueForEmbedding(result.eventId, embeddingContent);
4833
+ await this.sqliteStore.enqueueForEmbedding(result.eventId, embeddingContent);
3747
4834
  }
3748
4835
  return result;
3749
4836
  }
@@ -3752,9 +4839,6 @@ var MemoryService = class {
3752
4839
  */
3753
4840
  async retrieveMemories(query, options) {
3754
4841
  await this.initialize();
3755
- if (this.vectorWorker) {
3756
- await this.vectorWorker.processAll();
3757
- }
3758
4842
  if (options?.includeShared && this.sharedStore) {
3759
4843
  return this.retriever.retrieveUnified(query, {
3760
4844
  ...options,
@@ -3764,26 +4848,49 @@ var MemoryService = class {
3764
4848
  }
3765
4849
  return this.retriever.retrieve(query, options);
3766
4850
  }
4851
+ /**
4852
+ * Fast keyword search using SQLite FTS5
4853
+ * Much faster than vector search - no embedding model needed
4854
+ */
4855
+ async keywordSearch(query, options) {
4856
+ await this.initialize();
4857
+ const results = await this.sqliteStore.keywordSearch(query, options?.topK ?? 10);
4858
+ const maxRank = Math.min(...results.map((r) => r.rank), -1e-3);
4859
+ const minRank = Math.max(...results.map((r) => r.rank), -1e3);
4860
+ const rankRange = maxRank - minRank || 1;
4861
+ return results.map((r) => ({
4862
+ event: r.event,
4863
+ score: 1 - (r.rank - minRank) / rankRange
4864
+ // Normalize to 0-1
4865
+ })).filter((r) => !options?.minScore || r.score >= options.minScore);
4866
+ }
4867
+ /**
4868
+ * Rebuild FTS index (call after database upgrade)
4869
+ */
4870
+ async rebuildFtsIndex() {
4871
+ await this.initialize();
4872
+ return this.sqliteStore.rebuildFtsIndex();
4873
+ }
3767
4874
  /**
3768
4875
  * Get session history
3769
4876
  */
3770
4877
  async getSessionHistory(sessionId) {
3771
4878
  await this.initialize();
3772
- return this.eventStore.getSessionEvents(sessionId);
4879
+ return this.sqliteStore.getSessionEvents(sessionId);
3773
4880
  }
3774
4881
  /**
3775
4882
  * Get recent events
3776
4883
  */
3777
4884
  async getRecentEvents(limit = 100) {
3778
4885
  await this.initialize();
3779
- return this.eventStore.getRecentEvents(limit);
4886
+ return this.sqliteStore.getRecentEvents(limit);
3780
4887
  }
3781
4888
  /**
3782
4889
  * Get memory statistics
3783
4890
  */
3784
4891
  async getStats() {
3785
4892
  await this.initialize();
3786
- const recentEvents = await this.eventStore.getRecentEvents(1e4);
4893
+ const recentEvents = await this.sqliteStore.getRecentEvents(1e4);
3787
4894
  const vectorCount = await this.vectorStore.count();
3788
4895
  const levelStats = await this.graduation.getStats();
3789
4896
  return {
@@ -3806,14 +4913,14 @@ var MemoryService = class {
3806
4913
  */
3807
4914
  async getEventsByLevel(level, options) {
3808
4915
  await this.initialize();
3809
- return this.eventStore.getEventsByLevel(level, options);
4916
+ return this.sqliteStore.getEventsByLevel(level, options);
3810
4917
  }
3811
4918
  /**
3812
4919
  * Get memory level for a specific event
3813
4920
  */
3814
4921
  async getEventLevel(eventId) {
3815
4922
  await this.initialize();
3816
- return this.eventStore.getEventLevel(eventId);
4923
+ return this.sqliteStore.getEventLevel(eventId);
3817
4924
  }
3818
4925
  /**
3819
4926
  * Format retrieval results as context for Claude
@@ -3907,21 +5014,21 @@ var MemoryService = class {
3907
5014
  */
3908
5015
  async initializeEndlessMode() {
3909
5016
  const config = await this.getEndlessConfig();
3910
- this.workingSetStore = createWorkingSetStore(this.eventStore, config);
3911
- this.consolidatedStore = createConsolidatedStore(this.eventStore);
5017
+ this.workingSetStore = createWorkingSetStore(this.sqliteStore, config);
5018
+ this.consolidatedStore = createConsolidatedStore(this.sqliteStore);
3912
5019
  this.consolidationWorker = createConsolidationWorker(
3913
5020
  this.workingSetStore,
3914
5021
  this.consolidatedStore,
3915
5022
  config
3916
5023
  );
3917
- this.continuityManager = createContinuityManager(this.eventStore, config);
5024
+ this.continuityManager = createContinuityManager(this.sqliteStore, config);
3918
5025
  this.consolidationWorker.start();
3919
5026
  }
3920
5027
  /**
3921
5028
  * Get Endless Mode configuration
3922
5029
  */
3923
5030
  async getEndlessConfig() {
3924
- const savedConfig = await this.eventStore.getEndlessConfig("config");
5031
+ const savedConfig = await this.sqliteStore.getEndlessConfig("config");
3925
5032
  return savedConfig || this.getDefaultEndlessConfig();
3926
5033
  }
3927
5034
  /**
@@ -3930,7 +5037,7 @@ var MemoryService = class {
3930
5037
  async setEndlessConfig(config) {
3931
5038
  const current = await this.getEndlessConfig();
3932
5039
  const merged = { ...current, ...config };
3933
- await this.eventStore.setEndlessConfig("config", merged);
5040
+ await this.sqliteStore.setEndlessConfig("config", merged);
3934
5041
  }
3935
5042
  /**
3936
5043
  * Set memory mode (session or endless)
@@ -3940,7 +5047,7 @@ var MemoryService = class {
3940
5047
  if (mode === this.endlessMode)
3941
5048
  return;
3942
5049
  this.endlessMode = mode;
3943
- await this.eventStore.setEndlessConfig("mode", mode);
5050
+ await this.sqliteStore.setEndlessConfig("mode", mode);
3944
5051
  if (mode === "endless") {
3945
5052
  await this.initializeEndlessMode();
3946
5053
  } else {
@@ -3998,12 +5105,49 @@ var MemoryService = class {
3998
5105
  return this.consolidatedStore.getAll({ limit });
3999
5106
  }
4000
5107
  /**
4001
- * Get most accessed consolidated memories
5108
+ * Increment access count for memories that were used in prompts
5109
+ */
5110
+ async incrementMemoryAccess(eventIds) {
5111
+ if (eventIds.length === 0)
5112
+ return;
5113
+ if (this.sqliteStore) {
5114
+ await this.sqliteStore.incrementAccessCount(eventIds);
5115
+ } else if (this.eventStore) {
5116
+ await this.eventStore.incrementAccessCount(eventIds);
5117
+ }
5118
+ }
5119
+ /**
5120
+ * Get most accessed memories from events
4002
5121
  */
4003
5122
  async getMostAccessedMemories(limit = 10) {
4004
- if (!this.consolidatedStore)
4005
- return [];
4006
- return this.consolidatedStore.getMostAccessed(limit);
5123
+ console.log("[getMostAccessedMemories] sqliteStore available:", !!this.sqliteStore);
5124
+ if (this.sqliteStore) {
5125
+ const events = await this.sqliteStore.getMostAccessed(limit);
5126
+ console.log("[getMostAccessedMemories] Got events from SQLite:", events.length);
5127
+ return events.map((event) => ({
5128
+ memoryId: event.id,
5129
+ summary: event.content.substring(0, 200) + (event.content.length > 200 ? "..." : ""),
5130
+ topics: [],
5131
+ // Could extract topics from content if needed
5132
+ accessCount: event.access_count || 0,
5133
+ lastAccessed: event.last_accessed_at || null,
5134
+ confidence: 1,
5135
+ createdAt: event.timestamp
5136
+ }));
5137
+ }
5138
+ if (this.consolidatedStore) {
5139
+ const consolidated = await this.consolidatedStore.getMostAccessed(limit);
5140
+ return consolidated.map((m) => ({
5141
+ memoryId: m.memoryId,
5142
+ summary: m.summary,
5143
+ topics: m.topics,
5144
+ accessCount: m.accessCount,
5145
+ lastAccessed: m.accessedAt,
5146
+ confidence: m.confidence,
5147
+ createdAt: m.createdAt
5148
+ }));
5149
+ }
5150
+ return [];
4007
5151
  }
4008
5152
  /**
4009
5153
  * Mark a consolidated memory as accessed
@@ -4128,10 +5272,16 @@ var MemoryService = class {
4128
5272
  if (this.vectorWorker) {
4129
5273
  this.vectorWorker.stop();
4130
5274
  }
5275
+ if (this.syncWorker) {
5276
+ this.syncWorker.stop();
5277
+ }
4131
5278
  if (this.sharedEventStore) {
4132
5279
  await this.sharedEventStore.close();
4133
5280
  }
4134
- await this.eventStore.close();
5281
+ await this.sqliteStore.close();
5282
+ if (this.analyticsStore) {
5283
+ await this.analyticsStore.close();
5284
+ }
4135
5285
  }
4136
5286
  /**
4137
5287
  * Expand ~ to home directory
@@ -4147,7 +5297,11 @@ var serviceCache = /* @__PURE__ */ new Map();
4147
5297
  function getReadOnlyMemoryService() {
4148
5298
  return new MemoryService({
4149
5299
  storagePath: "~/.claude-code/memory",
4150
- readOnly: true
5300
+ readOnly: true,
5301
+ analyticsEnabled: false,
5302
+ // Use SQLite for reads (WAL supports concurrent readers)
5303
+ sharedStoreConfig: { enabled: false }
5304
+ // Skip shared store for now
4151
5305
  });
4152
5306
  }
4153
5307
  function getMemoryServiceForProject(projectPath, sharedStoreConfig) {
@@ -4157,7 +5311,10 @@ function getMemoryServiceForProject(projectPath, sharedStoreConfig) {
4157
5311
  serviceCache.set(hash, new MemoryService({
4158
5312
  storagePath,
4159
5313
  projectHash: hash,
4160
- sharedStoreConfig
5314
+ // Override shared store config - hooks don't need DuckDB
5315
+ sharedStoreConfig: sharedStoreConfig ?? { enabled: false },
5316
+ analyticsEnabled: false
5317
+ // Hooks don't need DuckDB
4161
5318
  }));
4162
5319
  }
4163
5320
  return serviceCache.get(hash);
@@ -4453,6 +5610,7 @@ statsRouter.get("/levels/:level", async (c) => {
4453
5610
  const { level } = c.req.param();
4454
5611
  const limit = parseInt(c.req.query("limit") || "20", 10);
4455
5612
  const offset = parseInt(c.req.query("offset") || "0", 10);
5613
+ const sort = c.req.query("sort") || "recent";
4456
5614
  const validLevels = ["L0", "L1", "L2", "L3", "L4"];
4457
5615
  if (!validLevels.includes(level)) {
4458
5616
  return c.json({ error: `Invalid level. Must be one of: ${validLevels.join(", ")}` }, 400);
@@ -4460,9 +5618,27 @@ statsRouter.get("/levels/:level", async (c) => {
4460
5618
  const memoryService = getReadOnlyMemoryService();
4461
5619
  try {
4462
5620
  await memoryService.initialize();
4463
- const events = await memoryService.getEventsByLevel(level, { limit, offset });
5621
+ let events = await memoryService.getEventsByLevel(level, { limit: limit * 2, offset });
4464
5622
  const stats = await memoryService.getStats();
4465
5623
  const levelStat = stats.levelStats.find((s) => s.level === level);
5624
+ if (sort === "accessed") {
5625
+ const sqliteStore = memoryService.sqliteEventStore;
5626
+ if (sqliteStore) {
5627
+ const eventIds = events.map((e) => e.id);
5628
+ const accessedEvents = await sqliteStore.getMostAccessed(1e3);
5629
+ const accessMap = new Map(accessedEvents.map((e) => [e.id, e.access_count || 0]));
5630
+ events = events.map((e) => ({
5631
+ ...e,
5632
+ accessCount: accessMap.get(e.id) || 0
5633
+ }));
5634
+ events.sort((a, b) => b.accessCount - a.accessCount);
5635
+ }
5636
+ } else if (sort === "oldest") {
5637
+ events.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
5638
+ } else {
5639
+ events.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
5640
+ }
5641
+ events = events.slice(0, limit);
4466
5642
  return c.json({
4467
5643
  level,
4468
5644
  events: events.map((e) => ({
@@ -4471,7 +5647,8 @@ statsRouter.get("/levels/:level", async (c) => {
4471
5647
  sessionId: e.sessionId,
4472
5648
  timestamp: e.timestamp.toISOString(),
4473
5649
  content: e.content.slice(0, 500) + (e.content.length > 500 ? "..." : ""),
4474
- metadata: e.metadata
5650
+ metadata: e.metadata,
5651
+ accessCount: e.accessCount || 0
4475
5652
  })),
4476
5653
  total: levelStat?.count || 0,
4477
5654
  limit,
@@ -4529,24 +5706,26 @@ statsRouter.get("/", async (c) => {
4529
5706
  });
4530
5707
  statsRouter.get("/most-accessed", async (c) => {
4531
5708
  const limit = parseInt(c.req.query("limit") || "10", 10);
4532
- const projectPath = c.req.query("project") || process.cwd();
4533
- const memoryService = getMemoryServiceForProject(projectPath);
5709
+ const memoryService = getReadOnlyMemoryService();
4534
5710
  try {
4535
5711
  await memoryService.initialize();
5712
+ console.log("[most-accessed] Fetching most accessed memories, limit:", limit);
4536
5713
  const memories = await memoryService.getMostAccessedMemories(limit);
5714
+ console.log("[most-accessed] Got memories:", memories.length);
4537
5715
  return c.json({
4538
5716
  memories: memories.map((m) => ({
4539
5717
  memoryId: m.memoryId,
4540
5718
  summary: m.summary,
4541
5719
  topics: m.topics,
4542
5720
  accessCount: m.accessCount,
4543
- lastAccessed: m.accessedAt?.toISOString() || null,
5721
+ lastAccessed: m.lastAccessed || null,
4544
5722
  confidence: m.confidence,
4545
- createdAt: m.createdAt.toISOString()
5723
+ createdAt: m.createdAt instanceof Date ? m.createdAt.toISOString() : m.createdAt
4546
5724
  })),
4547
5725
  total: memories.length
4548
5726
  });
4549
5727
  } catch (error) {
5728
+ console.error("[most-accessed] Error:", error);
4550
5729
  return c.json({
4551
5730
  memories: [],
4552
5731
  total: 0,