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
@@ -701,26 +701,1053 @@ var EventStore = class {
701
701
  tags: row.tags ? JSON.parse(row.tags) : void 0
702
702
  }));
703
703
  }
704
+ /**
705
+ * Increment access count for events (stub for compatibility)
706
+ */
707
+ async incrementAccessCount(eventIds) {
708
+ return Promise.resolve();
709
+ }
710
+ /**
711
+ * Get most accessed memories (stub for compatibility)
712
+ */
713
+ async getMostAccessed(limit = 10) {
714
+ return [];
715
+ }
716
+ /**
717
+ * Close database connection
718
+ */
719
+ async close() {
720
+ await dbClose(this.db);
721
+ }
722
+ /**
723
+ * Convert database row to MemoryEvent
724
+ */
725
+ rowToEvent(row) {
726
+ return {
727
+ id: row.id,
728
+ eventType: row.event_type,
729
+ sessionId: row.session_id,
730
+ timestamp: toDate(row.timestamp),
731
+ content: row.content,
732
+ canonicalKey: row.canonical_key,
733
+ dedupeKey: row.dedupe_key,
734
+ metadata: row.metadata ? JSON.parse(row.metadata) : void 0
735
+ };
736
+ }
737
+ };
738
+
739
+ // src/core/sqlite-event-store.ts
740
+ import { randomUUID as randomUUID2 } from "crypto";
741
+
742
+ // src/core/sqlite-wrapper.ts
743
+ import Database from "better-sqlite3";
744
+ function createSQLiteDatabase(path2, options) {
745
+ const db = new Database(path2, {
746
+ readonly: options?.readonly ?? false
747
+ });
748
+ if (!options?.readonly && (options?.walMode ?? true)) {
749
+ db.pragma("journal_mode = WAL");
750
+ db.pragma("synchronous = NORMAL");
751
+ db.pragma("busy_timeout = 5000");
752
+ }
753
+ return db;
754
+ }
755
+ function sqliteRun(db, sql, params = []) {
756
+ const stmt = db.prepare(sql);
757
+ return stmt.run(...params);
758
+ }
759
+ function sqliteAll(db, sql, params = []) {
760
+ const stmt = db.prepare(sql);
761
+ return stmt.all(...params);
762
+ }
763
+ function sqliteGet(db, sql, params = []) {
764
+ const stmt = db.prepare(sql);
765
+ return stmt.get(...params);
766
+ }
767
+ function sqliteExec(db, sql) {
768
+ db.exec(sql);
769
+ }
770
+ function sqliteClose(db) {
771
+ db.close();
772
+ }
773
+ function toDateFromSQLite(value) {
774
+ if (value instanceof Date)
775
+ return value;
776
+ if (typeof value === "string")
777
+ return new Date(value);
778
+ if (typeof value === "number")
779
+ return new Date(value);
780
+ return new Date(String(value));
781
+ }
782
+ function toSQLiteTimestamp(date) {
783
+ return date.toISOString();
784
+ }
785
+
786
+ // src/core/sqlite-event-store.ts
787
+ var SQLiteEventStore = class {
788
+ constructor(dbPath, options) {
789
+ this.dbPath = dbPath;
790
+ this.readOnly = options?.readonly ?? false;
791
+ this.db = createSQLiteDatabase(dbPath, {
792
+ readonly: this.readOnly,
793
+ walMode: !this.readOnly
794
+ });
795
+ }
796
+ db;
797
+ initialized = false;
798
+ readOnly;
799
+ /**
800
+ * Initialize database schema
801
+ */
802
+ async initialize() {
803
+ if (this.initialized)
804
+ return;
805
+ if (this.readOnly) {
806
+ this.initialized = true;
807
+ return;
808
+ }
809
+ sqliteExec(this.db, `
810
+ -- L0 EventStore: Single Source of Truth (immutable, append-only)
811
+ CREATE TABLE IF NOT EXISTS events (
812
+ id TEXT PRIMARY KEY,
813
+ event_type TEXT NOT NULL,
814
+ session_id TEXT NOT NULL,
815
+ timestamp TEXT NOT NULL,
816
+ content TEXT NOT NULL,
817
+ canonical_key TEXT NOT NULL,
818
+ dedupe_key TEXT UNIQUE,
819
+ metadata TEXT,
820
+ access_count INTEGER DEFAULT 0,
821
+ last_accessed_at TEXT
822
+ );
823
+
824
+ -- Dedup table for idempotency
825
+ CREATE TABLE IF NOT EXISTS event_dedup (
826
+ dedupe_key TEXT PRIMARY KEY,
827
+ event_id TEXT NOT NULL,
828
+ created_at TEXT DEFAULT (datetime('now'))
829
+ );
830
+
831
+ -- Session metadata
832
+ CREATE TABLE IF NOT EXISTS sessions (
833
+ id TEXT PRIMARY KEY,
834
+ started_at TEXT NOT NULL,
835
+ ended_at TEXT,
836
+ project_path TEXT,
837
+ summary TEXT,
838
+ tags TEXT
839
+ );
840
+
841
+ -- Insights (derived data, rebuildable)
842
+ CREATE TABLE IF NOT EXISTS insights (
843
+ id TEXT PRIMARY KEY,
844
+ insight_type TEXT NOT NULL,
845
+ content TEXT NOT NULL,
846
+ canonical_key TEXT NOT NULL,
847
+ confidence REAL,
848
+ source_events TEXT,
849
+ created_at TEXT,
850
+ last_updated TEXT
851
+ );
852
+
853
+ -- Embedding Outbox (Single-Writer Pattern)
854
+ CREATE TABLE IF NOT EXISTS embedding_outbox (
855
+ id TEXT PRIMARY KEY,
856
+ event_id TEXT NOT NULL,
857
+ content TEXT NOT NULL,
858
+ status TEXT DEFAULT 'pending',
859
+ retry_count INTEGER DEFAULT 0,
860
+ created_at TEXT DEFAULT (datetime('now')),
861
+ processed_at TEXT,
862
+ error_message TEXT
863
+ );
864
+
865
+ -- Projection offset tracking
866
+ CREATE TABLE IF NOT EXISTS projection_offsets (
867
+ projection_name TEXT PRIMARY KEY,
868
+ last_event_id TEXT,
869
+ last_timestamp TEXT,
870
+ updated_at TEXT DEFAULT (datetime('now'))
871
+ );
872
+
873
+ -- Memory level tracking
874
+ CREATE TABLE IF NOT EXISTS memory_levels (
875
+ event_id TEXT PRIMARY KEY,
876
+ level TEXT NOT NULL DEFAULT 'L0',
877
+ promoted_at TEXT DEFAULT (datetime('now'))
878
+ );
879
+
880
+ -- Entries (immutable memory units)
881
+ CREATE TABLE IF NOT EXISTS entries (
882
+ entry_id TEXT PRIMARY KEY,
883
+ created_ts TEXT NOT NULL,
884
+ entry_type TEXT NOT NULL,
885
+ title TEXT NOT NULL,
886
+ content_json TEXT NOT NULL,
887
+ stage TEXT NOT NULL DEFAULT 'raw',
888
+ status TEXT DEFAULT 'active',
889
+ superseded_by TEXT,
890
+ build_id TEXT,
891
+ evidence_json TEXT,
892
+ canonical_key TEXT,
893
+ created_at TEXT DEFAULT (datetime('now'))
894
+ );
895
+
896
+ -- Entities (task/condition/artifact)
897
+ CREATE TABLE IF NOT EXISTS entities (
898
+ entity_id TEXT PRIMARY KEY,
899
+ entity_type TEXT NOT NULL,
900
+ canonical_key TEXT NOT NULL,
901
+ title TEXT NOT NULL,
902
+ stage TEXT NOT NULL DEFAULT 'raw',
903
+ status TEXT NOT NULL DEFAULT 'active',
904
+ current_json TEXT NOT NULL,
905
+ title_norm TEXT,
906
+ search_text TEXT,
907
+ created_at TEXT DEFAULT (datetime('now')),
908
+ updated_at TEXT DEFAULT (datetime('now'))
909
+ );
910
+
911
+ -- Entity aliases for canonical key lookup
912
+ CREATE TABLE IF NOT EXISTS entity_aliases (
913
+ entity_type TEXT NOT NULL,
914
+ canonical_key TEXT NOT NULL,
915
+ entity_id TEXT NOT NULL,
916
+ is_primary INTEGER DEFAULT 0,
917
+ created_at TEXT DEFAULT (datetime('now')),
918
+ PRIMARY KEY(entity_type, canonical_key)
919
+ );
920
+
921
+ -- Edges (relationships between entries/entities)
922
+ CREATE TABLE IF NOT EXISTS edges (
923
+ edge_id TEXT PRIMARY KEY,
924
+ src_type TEXT NOT NULL,
925
+ src_id TEXT NOT NULL,
926
+ rel_type TEXT NOT NULL,
927
+ dst_type TEXT NOT NULL,
928
+ dst_id TEXT NOT NULL,
929
+ meta_json TEXT,
930
+ created_at TEXT DEFAULT (datetime('now'))
931
+ );
932
+
933
+ -- Vector Outbox V2 Table
934
+ CREATE TABLE IF NOT EXISTS vector_outbox (
935
+ job_id TEXT PRIMARY KEY,
936
+ item_kind TEXT NOT NULL,
937
+ item_id TEXT NOT NULL,
938
+ embedding_version TEXT NOT NULL,
939
+ status TEXT NOT NULL DEFAULT 'pending',
940
+ retry_count INTEGER DEFAULT 0,
941
+ error TEXT,
942
+ created_at TEXT DEFAULT (datetime('now')),
943
+ updated_at TEXT DEFAULT (datetime('now')),
944
+ UNIQUE(item_kind, item_id, embedding_version)
945
+ );
946
+
947
+ -- Build Runs
948
+ CREATE TABLE IF NOT EXISTS build_runs (
949
+ build_id TEXT PRIMARY KEY,
950
+ started_at TEXT NOT NULL,
951
+ finished_at TEXT,
952
+ extractor_model TEXT NOT NULL,
953
+ extractor_prompt_hash TEXT NOT NULL,
954
+ embedder_model TEXT NOT NULL,
955
+ embedding_version TEXT NOT NULL,
956
+ idris_version TEXT NOT NULL,
957
+ schema_version TEXT NOT NULL,
958
+ status TEXT NOT NULL DEFAULT 'running',
959
+ error TEXT
960
+ );
961
+
962
+ -- Pipeline Metrics
963
+ CREATE TABLE IF NOT EXISTS pipeline_metrics (
964
+ id TEXT PRIMARY KEY,
965
+ ts TEXT NOT NULL,
966
+ stage TEXT NOT NULL,
967
+ latency_ms REAL NOT NULL,
968
+ success INTEGER NOT NULL,
969
+ error TEXT,
970
+ session_id TEXT
971
+ );
972
+
973
+ -- Working Set table (active memory window)
974
+ CREATE TABLE IF NOT EXISTS working_set (
975
+ id TEXT PRIMARY KEY,
976
+ event_id TEXT NOT NULL,
977
+ added_at TEXT DEFAULT (datetime('now')),
978
+ relevance_score REAL DEFAULT 1.0,
979
+ topics TEXT,
980
+ expires_at TEXT
981
+ );
982
+
983
+ -- Consolidated Memories table (long-term integrated memories)
984
+ CREATE TABLE IF NOT EXISTS consolidated_memories (
985
+ memory_id TEXT PRIMARY KEY,
986
+ summary TEXT NOT NULL,
987
+ topics TEXT,
988
+ source_events TEXT,
989
+ confidence REAL DEFAULT 0.5,
990
+ created_at TEXT DEFAULT (datetime('now')),
991
+ accessed_at TEXT,
992
+ access_count INTEGER DEFAULT 0
993
+ );
994
+
995
+ -- Continuity Log table (tracks context transitions)
996
+ CREATE TABLE IF NOT EXISTS continuity_log (
997
+ log_id TEXT PRIMARY KEY,
998
+ from_context_id TEXT,
999
+ to_context_id TEXT,
1000
+ continuity_score REAL,
1001
+ transition_type TEXT,
1002
+ created_at TEXT DEFAULT (datetime('now'))
1003
+ );
1004
+
1005
+ -- Endless Mode Config table
1006
+ CREATE TABLE IF NOT EXISTS endless_config (
1007
+ key TEXT PRIMARY KEY,
1008
+ value TEXT,
1009
+ updated_at TEXT DEFAULT (datetime('now'))
1010
+ );
1011
+
1012
+ -- Sync position tracking (for SQLite -> DuckDB sync)
1013
+ CREATE TABLE IF NOT EXISTS sync_positions (
1014
+ target_name TEXT PRIMARY KEY,
1015
+ last_event_id TEXT,
1016
+ last_timestamp TEXT,
1017
+ updated_at TEXT DEFAULT (datetime('now'))
1018
+ );
1019
+
1020
+ -- Create indexes
1021
+ CREATE INDEX IF NOT EXISTS idx_events_session ON events(session_id);
1022
+ CREATE INDEX IF NOT EXISTS idx_events_timestamp ON events(timestamp);
1023
+ CREATE INDEX IF NOT EXISTS idx_entries_type ON entries(entry_type);
1024
+ CREATE INDEX IF NOT EXISTS idx_entries_stage ON entries(stage);
1025
+ CREATE INDEX IF NOT EXISTS idx_entries_canonical ON entries(canonical_key);
1026
+ CREATE INDEX IF NOT EXISTS idx_entities_type_key ON entities(entity_type, canonical_key);
1027
+ CREATE INDEX IF NOT EXISTS idx_entities_status ON entities(status);
1028
+ CREATE INDEX IF NOT EXISTS idx_edges_src ON edges(src_id, rel_type);
1029
+ CREATE INDEX IF NOT EXISTS idx_edges_dst ON edges(dst_id, rel_type);
1030
+ CREATE INDEX IF NOT EXISTS idx_edges_rel ON edges(rel_type);
1031
+ CREATE INDEX IF NOT EXISTS idx_outbox_status ON vector_outbox(status);
1032
+ CREATE INDEX IF NOT EXISTS idx_working_set_expires ON working_set(expires_at);
1033
+ CREATE INDEX IF NOT EXISTS idx_working_set_relevance ON working_set(relevance_score);
1034
+ CREATE INDEX IF NOT EXISTS idx_consolidated_confidence ON consolidated_memories(confidence);
1035
+ CREATE INDEX IF NOT EXISTS idx_continuity_created ON continuity_log(created_at);
1036
+ CREATE INDEX IF NOT EXISTS idx_embedding_outbox_status ON embedding_outbox(status);
1037
+
1038
+ -- FTS5 Full-Text Search for fast keyword search
1039
+ CREATE VIRTUAL TABLE IF NOT EXISTS events_fts USING fts5(
1040
+ content,
1041
+ event_id UNINDEXED,
1042
+ content='events',
1043
+ content_rowid='rowid'
1044
+ );
1045
+
1046
+ -- Triggers to keep FTS in sync with events table
1047
+ CREATE TRIGGER IF NOT EXISTS events_fts_insert AFTER INSERT ON events BEGIN
1048
+ INSERT INTO events_fts(rowid, content, event_id) VALUES (NEW.rowid, NEW.content, NEW.id);
1049
+ END;
1050
+
1051
+ CREATE TRIGGER IF NOT EXISTS events_fts_delete AFTER DELETE ON events BEGIN
1052
+ INSERT INTO events_fts(events_fts, rowid, content, event_id) VALUES('delete', OLD.rowid, OLD.content, OLD.id);
1053
+ END;
1054
+
1055
+ CREATE TRIGGER IF NOT EXISTS events_fts_update AFTER UPDATE ON events BEGIN
1056
+ INSERT INTO events_fts(events_fts, rowid, content, event_id) VALUES('delete', OLD.rowid, OLD.content, OLD.id);
1057
+ INSERT INTO events_fts(rowid, content, event_id) VALUES (NEW.rowid, NEW.content, NEW.id);
1058
+ END;
1059
+ `);
1060
+ const tableInfo = sqliteAll(this.db, "PRAGMA table_info(events)", []);
1061
+ const columnNames = tableInfo.map((col) => col.name);
1062
+ if (!columnNames.includes("access_count")) {
1063
+ try {
1064
+ sqliteExec(this.db, `
1065
+ ALTER TABLE events ADD COLUMN access_count INTEGER DEFAULT 0;
1066
+ `);
1067
+ } catch (err) {
1068
+ console.error("Error adding access_count column:", err);
1069
+ }
1070
+ }
1071
+ if (!columnNames.includes("last_accessed_at")) {
1072
+ try {
1073
+ sqliteExec(this.db, `
1074
+ ALTER TABLE events ADD COLUMN last_accessed_at TEXT;
1075
+ `);
1076
+ } catch (err) {
1077
+ console.error("Error adding last_accessed_at column:", err);
1078
+ }
1079
+ }
1080
+ try {
1081
+ sqliteExec(this.db, `
1082
+ CREATE INDEX IF NOT EXISTS idx_events_access_count ON events(access_count DESC);
1083
+ `);
1084
+ } catch (err) {
1085
+ }
1086
+ try {
1087
+ sqliteExec(this.db, `
1088
+ CREATE INDEX IF NOT EXISTS idx_events_last_accessed ON events(last_accessed_at DESC);
1089
+ `);
1090
+ } catch (err) {
1091
+ }
1092
+ this.initialized = true;
1093
+ }
1094
+ /**
1095
+ * Append event to store (Append-only, Idempotent)
1096
+ */
1097
+ async append(input) {
1098
+ await this.initialize();
1099
+ const canonicalKey = makeCanonicalKey(input.content);
1100
+ const dedupeKey = makeDedupeKey(input.content, input.sessionId);
1101
+ const existing = sqliteGet(
1102
+ this.db,
1103
+ `SELECT event_id FROM event_dedup WHERE dedupe_key = ?`,
1104
+ [dedupeKey]
1105
+ );
1106
+ if (existing) {
1107
+ return {
1108
+ success: true,
1109
+ eventId: existing.event_id,
1110
+ isDuplicate: true
1111
+ };
1112
+ }
1113
+ const id = randomUUID2();
1114
+ const timestamp = toSQLiteTimestamp(input.timestamp);
1115
+ try {
1116
+ const insertEvent = this.db.prepare(`
1117
+ INSERT INTO events (id, event_type, session_id, timestamp, content, canonical_key, dedupe_key, metadata)
1118
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1119
+ `);
1120
+ const insertDedup = this.db.prepare(`
1121
+ INSERT INTO event_dedup (dedupe_key, event_id) VALUES (?, ?)
1122
+ `);
1123
+ const insertLevel = this.db.prepare(`
1124
+ INSERT INTO memory_levels (event_id, level) VALUES (?, 'L0')
1125
+ `);
1126
+ const transaction = this.db.transaction(() => {
1127
+ insertEvent.run(
1128
+ id,
1129
+ input.eventType,
1130
+ input.sessionId,
1131
+ timestamp,
1132
+ input.content,
1133
+ canonicalKey,
1134
+ dedupeKey,
1135
+ JSON.stringify(input.metadata || {})
1136
+ );
1137
+ insertDedup.run(dedupeKey, id);
1138
+ insertLevel.run(id);
1139
+ });
1140
+ transaction();
1141
+ return { success: true, eventId: id, isDuplicate: false };
1142
+ } catch (error) {
1143
+ return {
1144
+ success: false,
1145
+ error: error instanceof Error ? error.message : String(error)
1146
+ };
1147
+ }
1148
+ }
1149
+ /**
1150
+ * Get events by session ID
1151
+ */
1152
+ async getSessionEvents(sessionId) {
1153
+ await this.initialize();
1154
+ const rows = sqliteAll(
1155
+ this.db,
1156
+ `SELECT * FROM events WHERE session_id = ? ORDER BY timestamp ASC`,
1157
+ [sessionId]
1158
+ );
1159
+ return rows.map(this.rowToEvent);
1160
+ }
1161
+ /**
1162
+ * Get recent events
1163
+ */
1164
+ async getRecentEvents(limit = 100) {
1165
+ await this.initialize();
1166
+ const rows = sqliteAll(
1167
+ this.db,
1168
+ `SELECT * FROM events ORDER BY timestamp DESC LIMIT ?`,
1169
+ [limit]
1170
+ );
1171
+ return rows.map(this.rowToEvent);
1172
+ }
1173
+ /**
1174
+ * Get event by ID
1175
+ */
1176
+ async getEvent(id) {
1177
+ await this.initialize();
1178
+ const row = sqliteGet(
1179
+ this.db,
1180
+ `SELECT * FROM events WHERE id = ?`,
1181
+ [id]
1182
+ );
1183
+ if (!row)
1184
+ return null;
1185
+ return this.rowToEvent(row);
1186
+ }
1187
+ /**
1188
+ * Get events since a timestamp (for sync)
1189
+ */
1190
+ async getEventsSince(timestamp, limit = 1e3) {
1191
+ await this.initialize();
1192
+ const rows = sqliteAll(
1193
+ this.db,
1194
+ `SELECT * FROM events WHERE timestamp > ? ORDER BY timestamp ASC LIMIT ?`,
1195
+ [timestamp, limit]
1196
+ );
1197
+ return rows.map(this.rowToEvent);
1198
+ }
1199
+ /**
1200
+ * Create or update session
1201
+ */
1202
+ async upsertSession(session) {
1203
+ await this.initialize();
1204
+ const existing = sqliteGet(
1205
+ this.db,
1206
+ `SELECT id FROM sessions WHERE id = ?`,
1207
+ [session.id]
1208
+ );
1209
+ if (!existing) {
1210
+ sqliteRun(
1211
+ this.db,
1212
+ `INSERT INTO sessions (id, started_at, project_path, tags)
1213
+ VALUES (?, ?, ?, ?)`,
1214
+ [
1215
+ session.id,
1216
+ toSQLiteTimestamp(session.startedAt || /* @__PURE__ */ new Date()),
1217
+ session.projectPath || null,
1218
+ JSON.stringify(session.tags || [])
1219
+ ]
1220
+ );
1221
+ } else {
1222
+ const updates = [];
1223
+ const values = [];
1224
+ if (session.endedAt) {
1225
+ updates.push("ended_at = ?");
1226
+ values.push(toSQLiteTimestamp(session.endedAt));
1227
+ }
1228
+ if (session.summary) {
1229
+ updates.push("summary = ?");
1230
+ values.push(session.summary);
1231
+ }
1232
+ if (session.tags) {
1233
+ updates.push("tags = ?");
1234
+ values.push(JSON.stringify(session.tags));
1235
+ }
1236
+ if (updates.length > 0) {
1237
+ values.push(session.id);
1238
+ sqliteRun(
1239
+ this.db,
1240
+ `UPDATE sessions SET ${updates.join(", ")} WHERE id = ?`,
1241
+ values
1242
+ );
1243
+ }
1244
+ }
1245
+ }
1246
+ /**
1247
+ * Get session by ID
1248
+ */
1249
+ async getSession(id) {
1250
+ await this.initialize();
1251
+ const row = sqliteGet(
1252
+ this.db,
1253
+ `SELECT * FROM sessions WHERE id = ?`,
1254
+ [id]
1255
+ );
1256
+ if (!row)
1257
+ return null;
1258
+ return {
1259
+ id: row.id,
1260
+ startedAt: toDateFromSQLite(row.started_at),
1261
+ endedAt: row.ended_at ? toDateFromSQLite(row.ended_at) : void 0,
1262
+ projectPath: row.project_path,
1263
+ summary: row.summary,
1264
+ tags: row.tags ? JSON.parse(row.tags) : void 0
1265
+ };
1266
+ }
1267
+ /**
1268
+ * Get all sessions
1269
+ */
1270
+ async getAllSessions() {
1271
+ await this.initialize();
1272
+ const rows = sqliteAll(
1273
+ this.db,
1274
+ `SELECT * FROM sessions ORDER BY started_at DESC`
1275
+ );
1276
+ return rows.map((row) => ({
1277
+ id: row.id,
1278
+ startedAt: toDateFromSQLite(row.started_at),
1279
+ endedAt: row.ended_at ? toDateFromSQLite(row.ended_at) : void 0,
1280
+ projectPath: row.project_path,
1281
+ summary: row.summary,
1282
+ tags: row.tags ? JSON.parse(row.tags) : void 0
1283
+ }));
1284
+ }
1285
+ /**
1286
+ * Add to embedding outbox
1287
+ */
1288
+ async enqueueForEmbedding(eventId, content) {
1289
+ await this.initialize();
1290
+ const id = randomUUID2();
1291
+ sqliteRun(
1292
+ this.db,
1293
+ `INSERT INTO embedding_outbox (id, event_id, content, status, retry_count)
1294
+ VALUES (?, ?, ?, 'pending', 0)`,
1295
+ [id, eventId, content]
1296
+ );
1297
+ return id;
1298
+ }
1299
+ /**
1300
+ * Get pending outbox items
1301
+ */
1302
+ async getPendingOutboxItems(limit = 32) {
1303
+ await this.initialize();
1304
+ const pending = sqliteAll(
1305
+ this.db,
1306
+ `SELECT * FROM embedding_outbox
1307
+ WHERE status = 'pending'
1308
+ ORDER BY created_at
1309
+ LIMIT ?`,
1310
+ [limit]
1311
+ );
1312
+ if (pending.length === 0)
1313
+ return [];
1314
+ const ids = pending.map((r) => r.id);
1315
+ const placeholders = ids.map(() => "?").join(",");
1316
+ sqliteRun(
1317
+ this.db,
1318
+ `UPDATE embedding_outbox SET status = 'processing' WHERE id IN (${placeholders})`,
1319
+ ids
1320
+ );
1321
+ return pending.map((row) => ({
1322
+ id: row.id,
1323
+ eventId: row.event_id,
1324
+ content: row.content,
1325
+ status: "processing",
1326
+ retryCount: row.retry_count,
1327
+ createdAt: toDateFromSQLite(row.created_at),
1328
+ errorMessage: row.error_message
1329
+ }));
1330
+ }
1331
+ /**
1332
+ * Mark outbox items as done
1333
+ */
1334
+ async completeOutboxItems(ids) {
1335
+ if (ids.length === 0)
1336
+ return;
1337
+ const placeholders = ids.map(() => "?").join(",");
1338
+ sqliteRun(
1339
+ this.db,
1340
+ `DELETE FROM embedding_outbox WHERE id IN (${placeholders})`,
1341
+ ids
1342
+ );
1343
+ }
1344
+ /**
1345
+ * Mark outbox items as failed
1346
+ */
1347
+ async failOutboxItems(ids, error) {
1348
+ if (ids.length === 0)
1349
+ return;
1350
+ const placeholders = ids.map(() => "?").join(",");
1351
+ sqliteRun(
1352
+ this.db,
1353
+ `UPDATE embedding_outbox
1354
+ SET status = CASE WHEN retry_count >= 3 THEN 'failed' ELSE 'pending' END,
1355
+ retry_count = retry_count + 1,
1356
+ error_message = ?
1357
+ WHERE id IN (${placeholders})`,
1358
+ [error, ...ids]
1359
+ );
1360
+ }
1361
+ /**
1362
+ * Update memory level
1363
+ */
1364
+ async updateMemoryLevel(eventId, level) {
1365
+ await this.initialize();
1366
+ sqliteRun(
1367
+ this.db,
1368
+ `UPDATE memory_levels SET level = ?, promoted_at = datetime('now') WHERE event_id = ?`,
1369
+ [level, eventId]
1370
+ );
1371
+ }
1372
+ /**
1373
+ * Get memory level statistics
1374
+ */
1375
+ async getLevelStats() {
1376
+ await this.initialize();
1377
+ const rows = sqliteAll(
1378
+ this.db,
1379
+ `SELECT level, COUNT(*) as count FROM memory_levels GROUP BY level`
1380
+ );
1381
+ return rows;
1382
+ }
1383
+ /**
1384
+ * Get events by memory level
1385
+ */
1386
+ async getEventsByLevel(level, options) {
1387
+ await this.initialize();
1388
+ const limit = options?.limit || 50;
1389
+ const offset = options?.offset || 0;
1390
+ const rows = sqliteAll(
1391
+ this.db,
1392
+ `SELECT e.* FROM events e
1393
+ INNER JOIN memory_levels ml ON e.id = ml.event_id
1394
+ WHERE ml.level = ?
1395
+ ORDER BY e.timestamp DESC
1396
+ LIMIT ? OFFSET ?`,
1397
+ [level, limit, offset]
1398
+ );
1399
+ return rows.map((row) => this.rowToEvent(row));
1400
+ }
1401
+ /**
1402
+ * Get memory level for a specific event
1403
+ */
1404
+ async getEventLevel(eventId) {
1405
+ await this.initialize();
1406
+ const row = sqliteGet(
1407
+ this.db,
1408
+ `SELECT level FROM memory_levels WHERE event_id = ?`,
1409
+ [eventId]
1410
+ );
1411
+ return row ? row.level : null;
1412
+ }
1413
+ /**
1414
+ * Get sync position for a target
1415
+ */
1416
+ async getSyncPosition(targetName) {
1417
+ await this.initialize();
1418
+ const row = sqliteGet(
1419
+ this.db,
1420
+ `SELECT last_event_id, last_timestamp FROM sync_positions WHERE target_name = ?`,
1421
+ [targetName]
1422
+ );
1423
+ return {
1424
+ lastEventId: row?.last_event_id ?? null,
1425
+ lastTimestamp: row?.last_timestamp ?? null
1426
+ };
1427
+ }
1428
+ /**
1429
+ * Update sync position for a target
1430
+ */
1431
+ async updateSyncPosition(targetName, lastEventId, lastTimestamp) {
1432
+ await this.initialize();
1433
+ sqliteRun(
1434
+ this.db,
1435
+ `INSERT OR REPLACE INTO sync_positions (target_name, last_event_id, last_timestamp, updated_at)
1436
+ VALUES (?, ?, ?, datetime('now'))`,
1437
+ [targetName, lastEventId, lastTimestamp]
1438
+ );
1439
+ }
1440
+ /**
1441
+ * Get config value for endless mode
1442
+ */
1443
+ async getEndlessConfig(key) {
1444
+ await this.initialize();
1445
+ const row = sqliteGet(
1446
+ this.db,
1447
+ `SELECT value FROM endless_config WHERE key = ?`,
1448
+ [key]
1449
+ );
1450
+ if (!row)
1451
+ return null;
1452
+ return JSON.parse(row.value);
1453
+ }
1454
+ /**
1455
+ * Set config value for endless mode
1456
+ */
1457
+ async setEndlessConfig(key, value) {
1458
+ await this.initialize();
1459
+ sqliteRun(
1460
+ this.db,
1461
+ `INSERT OR REPLACE INTO endless_config (key, value, updated_at)
1462
+ VALUES (?, ?, datetime('now'))`,
1463
+ [key, JSON.stringify(value)]
1464
+ );
1465
+ }
1466
+ /**
1467
+ * Increment access count for events
1468
+ */
1469
+ async incrementAccessCount(eventIds) {
1470
+ if (eventIds.length === 0 || this.readOnly)
1471
+ return;
1472
+ await this.initialize();
1473
+ const placeholders = eventIds.map(() => "?").join(",");
1474
+ const currentTime = toSQLiteTimestamp(/* @__PURE__ */ new Date());
1475
+ sqliteRun(
1476
+ this.db,
1477
+ `UPDATE events
1478
+ SET access_count = access_count + 1,
1479
+ last_accessed_at = ?
1480
+ WHERE id IN (${placeholders})`,
1481
+ [currentTime, ...eventIds]
1482
+ );
1483
+ }
1484
+ /**
1485
+ * Get most accessed memories
1486
+ */
1487
+ async getMostAccessed(limit = 10) {
1488
+ await this.initialize();
1489
+ const rows = sqliteAll(
1490
+ this.db,
1491
+ `SELECT * FROM events
1492
+ WHERE access_count > 0
1493
+ ORDER BY access_count DESC, last_accessed_at DESC
1494
+ LIMIT ?`,
1495
+ [limit]
1496
+ );
1497
+ return rows.map((row) => this.rowToEvent(row));
1498
+ }
1499
+ /**
1500
+ * Fast keyword search using FTS5
1501
+ * Returns events matching the search query, ranked by relevance
1502
+ */
1503
+ async keywordSearch(query, limit = 10) {
1504
+ await this.initialize();
1505
+ const searchTerms = query.replace(/['"(){}[\]^~*?:\\/-]/g, " ").split(/\s+/).filter((term) => term.length > 1).map((term) => `"${term}"*`).join(" OR ");
1506
+ if (!searchTerms) {
1507
+ return [];
1508
+ }
1509
+ try {
1510
+ const rows = sqliteAll(
1511
+ this.db,
1512
+ `SELECT e.*, fts.rank
1513
+ FROM events_fts fts
1514
+ JOIN events e ON e.id = fts.event_id
1515
+ WHERE events_fts MATCH ?
1516
+ ORDER BY fts.rank
1517
+ LIMIT ?`,
1518
+ [searchTerms, limit]
1519
+ );
1520
+ return rows.map((row) => ({
1521
+ event: this.rowToEvent(row),
1522
+ rank: row.rank
1523
+ }));
1524
+ } catch (error) {
1525
+ const likePattern = `%${query}%`;
1526
+ const rows = sqliteAll(
1527
+ this.db,
1528
+ `SELECT *, 0 as rank FROM events
1529
+ WHERE content LIKE ?
1530
+ ORDER BY timestamp DESC
1531
+ LIMIT ?`,
1532
+ [likePattern, limit]
1533
+ );
1534
+ return rows.map((row) => ({
1535
+ event: this.rowToEvent(row),
1536
+ rank: 0
1537
+ }));
1538
+ }
1539
+ }
1540
+ /**
1541
+ * Rebuild FTS index from existing events
1542
+ * Call this once after upgrading to FTS5
1543
+ */
1544
+ async rebuildFtsIndex() {
1545
+ await this.initialize();
1546
+ const countRow = sqliteGet(this.db, "SELECT COUNT(*) as count FROM events", []);
1547
+ const totalEvents = countRow?.count ?? 0;
1548
+ sqliteExec(this.db, `
1549
+ DELETE FROM events_fts;
1550
+ INSERT INTO events_fts(rowid, content, event_id)
1551
+ SELECT rowid, content, id FROM events;
1552
+ `);
1553
+ return totalEvents;
1554
+ }
1555
+ /**
1556
+ * Get database instance for direct access
1557
+ */
1558
+ getDatabase() {
1559
+ return this.db;
1560
+ }
704
1561
  /**
705
1562
  * Close database connection
706
1563
  */
707
1564
  async close() {
708
- await dbClose(this.db);
1565
+ sqliteClose(this.db);
709
1566
  }
710
1567
  /**
711
1568
  * Convert database row to MemoryEvent
712
1569
  */
713
1570
  rowToEvent(row) {
714
- return {
1571
+ const event = {
715
1572
  id: row.id,
716
1573
  eventType: row.event_type,
717
1574
  sessionId: row.session_id,
718
- timestamp: toDate(row.timestamp),
1575
+ timestamp: toDateFromSQLite(row.timestamp),
719
1576
  content: row.content,
720
1577
  canonicalKey: row.canonical_key,
721
1578
  dedupeKey: row.dedupe_key,
722
1579
  metadata: row.metadata ? JSON.parse(row.metadata) : void 0
723
1580
  };
1581
+ if (row.access_count !== void 0) {
1582
+ event.access_count = row.access_count;
1583
+ }
1584
+ if (row.last_accessed_at !== void 0) {
1585
+ event.last_accessed_at = row.last_accessed_at;
1586
+ }
1587
+ return event;
1588
+ }
1589
+ };
1590
+
1591
+ // src/core/sync-worker.ts
1592
+ var DEFAULT_CONFIG = {
1593
+ intervalMs: 3e4,
1594
+ batchSize: 500,
1595
+ maxRetries: 3,
1596
+ retryDelayMs: 5e3
1597
+ };
1598
+ var SyncWorker = class {
1599
+ constructor(sqliteStore, duckdbStore, config) {
1600
+ this.sqliteStore = sqliteStore;
1601
+ this.duckdbStore = duckdbStore;
1602
+ this.config = { ...DEFAULT_CONFIG, ...config };
1603
+ }
1604
+ config;
1605
+ intervalHandle = null;
1606
+ running = false;
1607
+ stats = {
1608
+ lastSyncAt: null,
1609
+ eventsSynced: 0,
1610
+ sessionsSynced: 0,
1611
+ errors: 0,
1612
+ status: "idle"
1613
+ };
1614
+ /**
1615
+ * Start the sync worker
1616
+ */
1617
+ start() {
1618
+ if (this.running)
1619
+ return;
1620
+ this.running = true;
1621
+ this.stats.status = "idle";
1622
+ this.syncNow().catch((err) => {
1623
+ console.error("[SyncWorker] Initial sync failed:", err);
1624
+ });
1625
+ this.intervalHandle = setInterval(() => {
1626
+ this.syncNow().catch((err) => {
1627
+ console.error("[SyncWorker] Periodic sync failed:", err);
1628
+ });
1629
+ }, this.config.intervalMs);
1630
+ }
1631
+ /**
1632
+ * Stop the sync worker
1633
+ */
1634
+ stop() {
1635
+ this.running = false;
1636
+ this.stats.status = "stopped";
1637
+ if (this.intervalHandle) {
1638
+ clearInterval(this.intervalHandle);
1639
+ this.intervalHandle = null;
1640
+ }
1641
+ }
1642
+ /**
1643
+ * Trigger immediate sync
1644
+ */
1645
+ async syncNow() {
1646
+ if (this.stats.status === "syncing") {
1647
+ return;
1648
+ }
1649
+ this.stats.status = "syncing";
1650
+ try {
1651
+ await this.syncEvents();
1652
+ await this.syncSessions();
1653
+ this.stats.lastSyncAt = /* @__PURE__ */ new Date();
1654
+ this.stats.status = "idle";
1655
+ } catch (error) {
1656
+ this.stats.errors++;
1657
+ this.stats.status = "error";
1658
+ throw error;
1659
+ }
1660
+ }
1661
+ /**
1662
+ * Sync events from SQLite to DuckDB
1663
+ */
1664
+ async syncEvents() {
1665
+ const targetName = "duckdb_analytics";
1666
+ const position = await this.sqliteStore.getSyncPosition(targetName);
1667
+ const lastTimestamp = position.lastTimestamp || "1970-01-01T00:00:00.000Z";
1668
+ let hasMore = true;
1669
+ let totalSynced = 0;
1670
+ while (hasMore) {
1671
+ const events = await this.sqliteStore.getEventsSince(lastTimestamp, this.config.batchSize);
1672
+ if (events.length === 0) {
1673
+ hasMore = false;
1674
+ break;
1675
+ }
1676
+ await this.retryWithBackoff(async () => {
1677
+ for (const event of events) {
1678
+ await this.insertEventToDuckDB(event);
1679
+ }
1680
+ });
1681
+ totalSynced += events.length;
1682
+ const lastEvent = events[events.length - 1];
1683
+ await this.sqliteStore.updateSyncPosition(
1684
+ targetName,
1685
+ lastEvent.id,
1686
+ lastEvent.timestamp.toISOString()
1687
+ );
1688
+ hasMore = events.length === this.config.batchSize;
1689
+ }
1690
+ this.stats.eventsSynced += totalSynced;
1691
+ }
1692
+ /**
1693
+ * Sync sessions from SQLite to DuckDB
1694
+ */
1695
+ async syncSessions() {
1696
+ const sessions = await this.sqliteStore.getAllSessions();
1697
+ for (const session of sessions) {
1698
+ await this.retryWithBackoff(async () => {
1699
+ await this.duckdbStore.upsertSession(session);
1700
+ });
1701
+ }
1702
+ this.stats.sessionsSynced = sessions.length;
1703
+ }
1704
+ /**
1705
+ * Insert a single event into DuckDB
1706
+ */
1707
+ async insertEventToDuckDB(event) {
1708
+ await this.duckdbStore.append({
1709
+ eventType: event.eventType,
1710
+ sessionId: event.sessionId,
1711
+ timestamp: event.timestamp,
1712
+ content: event.content,
1713
+ metadata: event.metadata
1714
+ });
1715
+ }
1716
+ /**
1717
+ * Retry operation with exponential backoff
1718
+ */
1719
+ async retryWithBackoff(fn) {
1720
+ let lastError = null;
1721
+ for (let attempt = 0; attempt < this.config.maxRetries; attempt++) {
1722
+ try {
1723
+ return await fn();
1724
+ } catch (error) {
1725
+ lastError = error instanceof Error ? error : new Error(String(error));
1726
+ if (attempt < this.config.maxRetries - 1) {
1727
+ const delay = this.config.retryDelayMs * Math.pow(2, attempt);
1728
+ await this.sleep(delay);
1729
+ }
1730
+ }
1731
+ }
1732
+ throw lastError;
1733
+ }
1734
+ /**
1735
+ * Sleep utility
1736
+ */
1737
+ sleep(ms) {
1738
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
1739
+ }
1740
+ /**
1741
+ * Get sync statistics
1742
+ */
1743
+ getStats() {
1744
+ return { ...this.stats };
1745
+ }
1746
+ /**
1747
+ * Check if worker is running
1748
+ */
1749
+ isRunning() {
1750
+ return this.running;
724
1751
  }
725
1752
  };
726
1753
 
@@ -952,7 +1979,7 @@ function getDefaultEmbedder() {
952
1979
  }
953
1980
 
954
1981
  // src/core/vector-outbox.ts
955
- var DEFAULT_CONFIG = {
1982
+ var DEFAULT_CONFIG2 = {
956
1983
  embeddingVersion: "v1",
957
1984
  maxRetries: 3,
958
1985
  stuckThresholdMs: 5 * 60 * 1e3,
@@ -961,7 +1988,7 @@ var DEFAULT_CONFIG = {
961
1988
  };
962
1989
 
963
1990
  // src/core/vector-worker.ts
964
- var DEFAULT_CONFIG2 = {
1991
+ var DEFAULT_CONFIG3 = {
965
1992
  batchSize: 32,
966
1993
  pollIntervalMs: 1e3,
967
1994
  maxRetries: 3
@@ -972,12 +1999,13 @@ var VectorWorker = class {
972
1999
  embedder;
973
2000
  config;
974
2001
  running = false;
2002
+ stopping = false;
975
2003
  pollTimeout = null;
976
2004
  constructor(eventStore, vectorStore, embedder, config = {}) {
977
2005
  this.eventStore = eventStore;
978
2006
  this.vectorStore = vectorStore;
979
2007
  this.embedder = embedder;
980
- this.config = { ...DEFAULT_CONFIG2, ...config };
2008
+ this.config = { ...DEFAULT_CONFIG3, ...config };
981
2009
  }
982
2010
  /**
983
2011
  * Start the worker polling loop
@@ -986,6 +2014,7 @@ var VectorWorker = class {
986
2014
  if (this.running)
987
2015
  return;
988
2016
  this.running = true;
2017
+ this.stopping = false;
989
2018
  this.poll();
990
2019
  }
991
2020
  /**
@@ -993,6 +2022,7 @@ var VectorWorker = class {
993
2022
  */
994
2023
  stop() {
995
2024
  this.running = false;
2025
+ this.stopping = true;
996
2026
  if (this.pollTimeout) {
997
2027
  clearTimeout(this.pollTimeout);
998
2028
  this.pollTimeout = null;
@@ -1042,9 +2072,15 @@ var VectorWorker = class {
1042
2072
  }
1043
2073
  return successful.length;
1044
2074
  } catch (error) {
1045
- const allIds = items.map((i) => i.id);
1046
- const errorMessage = error instanceof Error ? error.message : String(error);
1047
- await this.eventStore.failOutboxItems(allIds, errorMessage);
2075
+ if (!this.stopping) {
2076
+ try {
2077
+ const allIds = items.map((i) => i.id);
2078
+ const errorMessage = error instanceof Error ? error.message : String(error);
2079
+ await this.eventStore.failOutboxItems(allIds, errorMessage);
2080
+ } catch (failError) {
2081
+ console.warn("Could not mark outbox items as failed (database may be closed)");
2082
+ }
2083
+ }
1048
2084
  throw error;
1049
2085
  }
1050
2086
  }
@@ -1052,14 +2088,18 @@ var VectorWorker = class {
1052
2088
  * Poll for new items
1053
2089
  */
1054
2090
  async poll() {
1055
- if (!this.running)
2091
+ if (!this.running || this.stopping)
1056
2092
  return;
1057
2093
  try {
1058
2094
  await this.processBatch();
1059
2095
  } catch (error) {
1060
- console.error("Vector worker error:", error);
2096
+ if (!this.stopping) {
2097
+ console.error("Vector worker error:", error);
2098
+ }
2099
+ }
2100
+ if (this.running && !this.stopping) {
2101
+ this.pollTimeout = setTimeout(() => this.poll(), this.config.pollIntervalMs);
1061
2102
  }
1062
- this.pollTimeout = setTimeout(() => this.poll(), this.config.pollIntervalMs);
1063
2103
  }
1064
2104
  /**
1065
2105
  * Process all pending items (blocking)
@@ -1086,7 +2126,7 @@ function createVectorWorker(eventStore, vectorStore, embedder, config) {
1086
2126
  }
1087
2127
 
1088
2128
  // src/core/matcher.ts
1089
- var DEFAULT_CONFIG3 = {
2129
+ var DEFAULT_CONFIG4 = {
1090
2130
  weights: {
1091
2131
  semanticSimilarity: 0.4,
1092
2132
  ftsScore: 0.25,
@@ -1101,9 +2141,9 @@ var Matcher = class {
1101
2141
  config;
1102
2142
  constructor(config = {}) {
1103
2143
  this.config = {
1104
- ...DEFAULT_CONFIG3,
2144
+ ...DEFAULT_CONFIG4,
1105
2145
  ...config,
1106
- weights: { ...DEFAULT_CONFIG3.weights, ...config.weights }
2146
+ weights: { ...DEFAULT_CONFIG4.weights, ...config.weights }
1107
2147
  };
1108
2148
  }
1109
2149
  /**
@@ -1811,7 +2851,7 @@ function createSharedEventStore(dbPath) {
1811
2851
  }
1812
2852
 
1813
2853
  // src/core/shared-store.ts
1814
- import { randomUUID as randomUUID2 } from "crypto";
2854
+ import { randomUUID as randomUUID3 } from "crypto";
1815
2855
  var SharedStore = class {
1816
2856
  constructor(sharedEventStore) {
1817
2857
  this.sharedEventStore = sharedEventStore;
@@ -1823,7 +2863,7 @@ var SharedStore = class {
1823
2863
  * Promote a verified troubleshooting entry to shared storage
1824
2864
  */
1825
2865
  async promoteEntry(input) {
1826
- const entryId = randomUUID2();
2866
+ const entryId = randomUUID3();
1827
2867
  await dbRun(
1828
2868
  this.db,
1829
2869
  `INSERT INTO shared_troubleshooting (
@@ -2188,7 +3228,7 @@ function createSharedVectorStore(dbPath) {
2188
3228
  }
2189
3229
 
2190
3230
  // src/core/shared-promoter.ts
2191
- import { randomUUID as randomUUID3 } from "crypto";
3231
+ import { randomUUID as randomUUID4 } from "crypto";
2192
3232
  var SharedPromoter = class {
2193
3233
  constructor(sharedStore, sharedVectorStore, embedder, config) {
2194
3234
  this.sharedStore = sharedStore;
@@ -2256,7 +3296,7 @@ var SharedPromoter = class {
2256
3296
  const embeddingContent = this.createEmbeddingContent(input);
2257
3297
  const embedding = await this.embedder.embed(embeddingContent);
2258
3298
  await this.sharedVectorStore.upsert({
2259
- id: randomUUID3(),
3299
+ id: randomUUID4(),
2260
3300
  entryId,
2261
3301
  entryType: "troubleshooting",
2262
3302
  content: embeddingContent,
@@ -2396,7 +3436,7 @@ function createToolObservationEmbedding(toolName, metadata, success) {
2396
3436
  }
2397
3437
 
2398
3438
  // src/core/working-set-store.ts
2399
- import { randomUUID as randomUUID4 } from "crypto";
3439
+ import { randomUUID as randomUUID5 } from "crypto";
2400
3440
  var WorkingSetStore = class {
2401
3441
  constructor(eventStore, config) {
2402
3442
  this.eventStore = eventStore;
@@ -2417,7 +3457,7 @@ var WorkingSetStore = class {
2417
3457
  `INSERT OR REPLACE INTO working_set (id, event_id, added_at, relevance_score, topics, expires_at)
2418
3458
  VALUES (?, ?, CURRENT_TIMESTAMP, ?, ?, ?)`,
2419
3459
  [
2420
- randomUUID4(),
3460
+ randomUUID5(),
2421
3461
  eventId,
2422
3462
  relevanceScore,
2423
3463
  JSON.stringify(topics || []),
@@ -2601,7 +3641,7 @@ function createWorkingSetStore(eventStore, config) {
2601
3641
  }
2602
3642
 
2603
3643
  // src/core/consolidated-store.ts
2604
- import { randomUUID as randomUUID5 } from "crypto";
3644
+ import { randomUUID as randomUUID6 } from "crypto";
2605
3645
  var ConsolidatedStore = class {
2606
3646
  constructor(eventStore) {
2607
3647
  this.eventStore = eventStore;
@@ -2613,7 +3653,7 @@ var ConsolidatedStore = class {
2613
3653
  * Create a new consolidated memory
2614
3654
  */
2615
3655
  async create(input) {
2616
- const memoryId = randomUUID5();
3656
+ const memoryId = randomUUID6();
2617
3657
  await dbRun(
2618
3658
  this.db,
2619
3659
  `INSERT INTO consolidated_memories
@@ -3140,7 +4180,7 @@ function createConsolidationWorker(workingSetStore, consolidatedStore, config) {
3140
4180
  }
3141
4181
 
3142
4182
  // src/core/continuity-manager.ts
3143
- import { randomUUID as randomUUID6 } from "crypto";
4183
+ import { randomUUID as randomUUID7 } from "crypto";
3144
4184
  var ContinuityManager = class {
3145
4185
  constructor(eventStore, config) {
3146
4186
  this.eventStore = eventStore;
@@ -3294,7 +4334,7 @@ var ContinuityManager = class {
3294
4334
  `INSERT INTO continuity_log
3295
4335
  (log_id, from_context_id, to_context_id, continuity_score, transition_type, created_at)
3296
4336
  VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,
3297
- [randomUUID6(), previous.id, current.id, score, type]
4337
+ [randomUUID7(), previous.id, current.id, score, type]
3298
4338
  );
3299
4339
  }
3300
4340
  /**
@@ -3403,7 +4443,7 @@ function createContinuityManager(eventStore, config) {
3403
4443
  }
3404
4444
 
3405
4445
  // src/core/graduation-worker.ts
3406
- var DEFAULT_CONFIG4 = {
4446
+ var DEFAULT_CONFIG5 = {
3407
4447
  evaluationIntervalMs: 3e5,
3408
4448
  // 5 minutes
3409
4449
  batchSize: 50,
@@ -3411,7 +4451,7 @@ var DEFAULT_CONFIG4 = {
3411
4451
  // 1 hour cooldown between evaluations
3412
4452
  };
3413
4453
  var GraduationWorker = class {
3414
- constructor(eventStore, graduation, config = DEFAULT_CONFIG4) {
4454
+ constructor(eventStore, graduation, config = DEFAULT_CONFIG5) {
3415
4455
  this.eventStore = eventStore;
3416
4456
  this.graduation = graduation;
3417
4457
  this.config = config;
@@ -3519,7 +4559,7 @@ function createGraduationWorker(eventStore, graduation, config) {
3519
4559
  return new GraduationWorker(
3520
4560
  eventStore,
3521
4561
  graduation,
3522
- { ...DEFAULT_CONFIG4, ...config }
4562
+ { ...DEFAULT_CONFIG5, ...config }
3523
4563
  );
3524
4564
  }
3525
4565
 
@@ -3579,7 +4619,11 @@ function registerSession(sessionId, projectPath) {
3579
4619
  saveSessionRegistry(registry);
3580
4620
  }
3581
4621
  var MemoryService = class {
3582
- eventStore;
4622
+ // Primary store: SQLite (WAL mode) - for hooks, always available
4623
+ sqliteStore;
4624
+ // Analytics store: DuckDB - for server reads (optional, synced from SQLite)
4625
+ analyticsStore;
4626
+ syncWorker = null;
3583
4627
  vectorStore;
3584
4628
  embedder;
3585
4629
  matcher;
@@ -3602,25 +4646,49 @@ var MemoryService = class {
3602
4646
  sharedStoreConfig = null;
3603
4647
  projectHash = null;
3604
4648
  readOnly;
4649
+ lightweightMode;
3605
4650
  constructor(config) {
3606
4651
  const storagePath = this.expandPath(config.storagePath);
3607
4652
  this.readOnly = config.readOnly ?? false;
4653
+ this.lightweightMode = config.lightweightMode ?? false;
3608
4654
  if (!this.readOnly && !fs.existsSync(storagePath)) {
3609
4655
  fs.mkdirSync(storagePath, { recursive: true });
3610
4656
  }
3611
4657
  this.projectHash = config.projectHash || null;
3612
4658
  this.sharedStoreConfig = config.sharedStoreConfig ?? { enabled: true };
3613
- this.eventStore = new EventStore(path.join(storagePath, "events.duckdb"), { readOnly: this.readOnly });
4659
+ this.sqliteStore = new SQLiteEventStore(
4660
+ path.join(storagePath, "events.sqlite"),
4661
+ { readonly: this.readOnly }
4662
+ );
4663
+ const analyticsEnabled = config.analyticsEnabled ?? this.readOnly;
4664
+ if (!analyticsEnabled) {
4665
+ this.analyticsStore = null;
4666
+ } else if (this.readOnly) {
4667
+ try {
4668
+ this.analyticsStore = new EventStore(
4669
+ path.join(storagePath, "analytics.duckdb"),
4670
+ { readOnly: true }
4671
+ );
4672
+ } catch {
4673
+ this.analyticsStore = null;
4674
+ }
4675
+ } else {
4676
+ this.analyticsStore = new EventStore(
4677
+ path.join(storagePath, "analytics.duckdb"),
4678
+ { readOnly: false }
4679
+ );
4680
+ }
3614
4681
  this.vectorStore = new VectorStore(path.join(storagePath, "vectors"));
3615
4682
  this.embedder = config.embeddingModel ? new Embedder(config.embeddingModel) : getDefaultEmbedder();
3616
4683
  this.matcher = getDefaultMatcher();
3617
4684
  this.retriever = createRetriever(
3618
- this.eventStore,
4685
+ this.sqliteStore,
4686
+ // Interface compatible
3619
4687
  this.vectorStore,
3620
4688
  this.embedder,
3621
4689
  this.matcher
3622
4690
  );
3623
- this.graduation = createGraduationPipeline(this.eventStore);
4691
+ this.graduation = createGraduationPipeline(this.sqliteStore);
3624
4692
  }
3625
4693
  /**
3626
4694
  * Initialize all components
@@ -3628,23 +4696,42 @@ var MemoryService = class {
3628
4696
  async initialize() {
3629
4697
  if (this.initialized)
3630
4698
  return;
3631
- await this.eventStore.initialize();
4699
+ await this.sqliteStore.initialize();
4700
+ if (this.lightweightMode) {
4701
+ this.initialized = true;
4702
+ return;
4703
+ }
4704
+ if (this.analyticsStore) {
4705
+ try {
4706
+ await this.analyticsStore.initialize();
4707
+ } catch (error) {
4708
+ console.warn("[MemoryService] Analytics store (DuckDB) initialization failed, using SQLite for reads:", error);
4709
+ }
4710
+ }
3632
4711
  await this.vectorStore.initialize();
3633
4712
  await this.embedder.initialize();
3634
4713
  if (!this.readOnly) {
3635
4714
  this.vectorWorker = createVectorWorker(
3636
- this.eventStore,
4715
+ this.sqliteStore,
3637
4716
  this.vectorStore,
3638
4717
  this.embedder
3639
4718
  );
3640
4719
  this.vectorWorker.start();
3641
4720
  this.retriever.setGraduationPipeline(this.graduation);
3642
4721
  this.graduationWorker = createGraduationWorker(
3643
- this.eventStore,
4722
+ this.sqliteStore,
3644
4723
  this.graduation
3645
4724
  );
3646
4725
  this.graduationWorker.start();
3647
- const savedMode = await this.eventStore.getEndlessConfig("mode");
4726
+ if (this.analyticsStore) {
4727
+ this.syncWorker = new SyncWorker(
4728
+ this.sqliteStore,
4729
+ this.analyticsStore,
4730
+ { intervalMs: 3e4, batchSize: 500 }
4731
+ );
4732
+ this.syncWorker.start();
4733
+ }
4734
+ const savedMode = await this.sqliteStore.getEndlessConfig("mode");
3648
4735
  if (savedMode === "endless") {
3649
4736
  this.endlessMode = "endless";
3650
4737
  await this.initializeEndlessMode();
@@ -3685,7 +4772,7 @@ var MemoryService = class {
3685
4772
  */
3686
4773
  async startSession(sessionId, projectPath) {
3687
4774
  await this.initialize();
3688
- await this.eventStore.upsertSession({
4775
+ await this.sqliteStore.upsertSession({
3689
4776
  id: sessionId,
3690
4777
  startedAt: /* @__PURE__ */ new Date(),
3691
4778
  projectPath
@@ -3696,7 +4783,7 @@ var MemoryService = class {
3696
4783
  */
3697
4784
  async endSession(sessionId, summary) {
3698
4785
  await this.initialize();
3699
- await this.eventStore.upsertSession({
4786
+ await this.sqliteStore.upsertSession({
3700
4787
  id: sessionId,
3701
4788
  endedAt: /* @__PURE__ */ new Date(),
3702
4789
  summary
@@ -3707,7 +4794,7 @@ var MemoryService = class {
3707
4794
  */
3708
4795
  async storeUserPrompt(sessionId, content, metadata) {
3709
4796
  await this.initialize();
3710
- const result = await this.eventStore.append({
4797
+ const result = await this.sqliteStore.append({
3711
4798
  eventType: "user_prompt",
3712
4799
  sessionId,
3713
4800
  timestamp: /* @__PURE__ */ new Date(),
@@ -3715,7 +4802,7 @@ var MemoryService = class {
3715
4802
  metadata
3716
4803
  });
3717
4804
  if (result.success && !result.isDuplicate) {
3718
- await this.eventStore.enqueueForEmbedding(result.eventId, content);
4805
+ await this.sqliteStore.enqueueForEmbedding(result.eventId, content);
3719
4806
  }
3720
4807
  return result;
3721
4808
  }
@@ -3724,7 +4811,7 @@ var MemoryService = class {
3724
4811
  */
3725
4812
  async storeAgentResponse(sessionId, content, metadata) {
3726
4813
  await this.initialize();
3727
- const result = await this.eventStore.append({
4814
+ const result = await this.sqliteStore.append({
3728
4815
  eventType: "agent_response",
3729
4816
  sessionId,
3730
4817
  timestamp: /* @__PURE__ */ new Date(),
@@ -3732,7 +4819,7 @@ var MemoryService = class {
3732
4819
  metadata
3733
4820
  });
3734
4821
  if (result.success && !result.isDuplicate) {
3735
- await this.eventStore.enqueueForEmbedding(result.eventId, content);
4822
+ await this.sqliteStore.enqueueForEmbedding(result.eventId, content);
3736
4823
  }
3737
4824
  return result;
3738
4825
  }
@@ -3741,14 +4828,14 @@ var MemoryService = class {
3741
4828
  */
3742
4829
  async storeSessionSummary(sessionId, summary) {
3743
4830
  await this.initialize();
3744
- const result = await this.eventStore.append({
4831
+ const result = await this.sqliteStore.append({
3745
4832
  eventType: "session_summary",
3746
4833
  sessionId,
3747
4834
  timestamp: /* @__PURE__ */ new Date(),
3748
4835
  content: summary
3749
4836
  });
3750
4837
  if (result.success && !result.isDuplicate) {
3751
- await this.eventStore.enqueueForEmbedding(result.eventId, summary);
4838
+ await this.sqliteStore.enqueueForEmbedding(result.eventId, summary);
3752
4839
  }
3753
4840
  return result;
3754
4841
  }
@@ -3758,7 +4845,7 @@ var MemoryService = class {
3758
4845
  async storeToolObservation(sessionId, payload) {
3759
4846
  await this.initialize();
3760
4847
  const content = JSON.stringify(payload);
3761
- const result = await this.eventStore.append({
4848
+ const result = await this.sqliteStore.append({
3762
4849
  eventType: "tool_observation",
3763
4850
  sessionId,
3764
4851
  timestamp: /* @__PURE__ */ new Date(),
@@ -3774,7 +4861,7 @@ var MemoryService = class {
3774
4861
  payload.metadata || {},
3775
4862
  payload.success
3776
4863
  );
3777
- await this.eventStore.enqueueForEmbedding(result.eventId, embeddingContent);
4864
+ await this.sqliteStore.enqueueForEmbedding(result.eventId, embeddingContent);
3778
4865
  }
3779
4866
  return result;
3780
4867
  }
@@ -3783,9 +4870,6 @@ var MemoryService = class {
3783
4870
  */
3784
4871
  async retrieveMemories(query, options) {
3785
4872
  await this.initialize();
3786
- if (this.vectorWorker) {
3787
- await this.vectorWorker.processAll();
3788
- }
3789
4873
  if (options?.includeShared && this.sharedStore) {
3790
4874
  return this.retriever.retrieveUnified(query, {
3791
4875
  ...options,
@@ -3795,26 +4879,49 @@ var MemoryService = class {
3795
4879
  }
3796
4880
  return this.retriever.retrieve(query, options);
3797
4881
  }
4882
+ /**
4883
+ * Fast keyword search using SQLite FTS5
4884
+ * Much faster than vector search - no embedding model needed
4885
+ */
4886
+ async keywordSearch(query, options) {
4887
+ await this.initialize();
4888
+ const results = await this.sqliteStore.keywordSearch(query, options?.topK ?? 10);
4889
+ const maxRank = Math.min(...results.map((r) => r.rank), -1e-3);
4890
+ const minRank = Math.max(...results.map((r) => r.rank), -1e3);
4891
+ const rankRange = maxRank - minRank || 1;
4892
+ return results.map((r) => ({
4893
+ event: r.event,
4894
+ score: 1 - (r.rank - minRank) / rankRange
4895
+ // Normalize to 0-1
4896
+ })).filter((r) => !options?.minScore || r.score >= options.minScore);
4897
+ }
4898
+ /**
4899
+ * Rebuild FTS index (call after database upgrade)
4900
+ */
4901
+ async rebuildFtsIndex() {
4902
+ await this.initialize();
4903
+ return this.sqliteStore.rebuildFtsIndex();
4904
+ }
3798
4905
  /**
3799
4906
  * Get session history
3800
4907
  */
3801
4908
  async getSessionHistory(sessionId) {
3802
4909
  await this.initialize();
3803
- return this.eventStore.getSessionEvents(sessionId);
4910
+ return this.sqliteStore.getSessionEvents(sessionId);
3804
4911
  }
3805
4912
  /**
3806
4913
  * Get recent events
3807
4914
  */
3808
4915
  async getRecentEvents(limit = 100) {
3809
4916
  await this.initialize();
3810
- return this.eventStore.getRecentEvents(limit);
4917
+ return this.sqliteStore.getRecentEvents(limit);
3811
4918
  }
3812
4919
  /**
3813
4920
  * Get memory statistics
3814
4921
  */
3815
4922
  async getStats() {
3816
4923
  await this.initialize();
3817
- const recentEvents = await this.eventStore.getRecentEvents(1e4);
4924
+ const recentEvents = await this.sqliteStore.getRecentEvents(1e4);
3818
4925
  const vectorCount = await this.vectorStore.count();
3819
4926
  const levelStats = await this.graduation.getStats();
3820
4927
  return {
@@ -3837,14 +4944,14 @@ var MemoryService = class {
3837
4944
  */
3838
4945
  async getEventsByLevel(level, options) {
3839
4946
  await this.initialize();
3840
- return this.eventStore.getEventsByLevel(level, options);
4947
+ return this.sqliteStore.getEventsByLevel(level, options);
3841
4948
  }
3842
4949
  /**
3843
4950
  * Get memory level for a specific event
3844
4951
  */
3845
4952
  async getEventLevel(eventId) {
3846
4953
  await this.initialize();
3847
- return this.eventStore.getEventLevel(eventId);
4954
+ return this.sqliteStore.getEventLevel(eventId);
3848
4955
  }
3849
4956
  /**
3850
4957
  * Format retrieval results as context for Claude
@@ -3938,21 +5045,21 @@ var MemoryService = class {
3938
5045
  */
3939
5046
  async initializeEndlessMode() {
3940
5047
  const config = await this.getEndlessConfig();
3941
- this.workingSetStore = createWorkingSetStore(this.eventStore, config);
3942
- this.consolidatedStore = createConsolidatedStore(this.eventStore);
5048
+ this.workingSetStore = createWorkingSetStore(this.sqliteStore, config);
5049
+ this.consolidatedStore = createConsolidatedStore(this.sqliteStore);
3943
5050
  this.consolidationWorker = createConsolidationWorker(
3944
5051
  this.workingSetStore,
3945
5052
  this.consolidatedStore,
3946
5053
  config
3947
5054
  );
3948
- this.continuityManager = createContinuityManager(this.eventStore, config);
5055
+ this.continuityManager = createContinuityManager(this.sqliteStore, config);
3949
5056
  this.consolidationWorker.start();
3950
5057
  }
3951
5058
  /**
3952
5059
  * Get Endless Mode configuration
3953
5060
  */
3954
5061
  async getEndlessConfig() {
3955
- const savedConfig = await this.eventStore.getEndlessConfig("config");
5062
+ const savedConfig = await this.sqliteStore.getEndlessConfig("config");
3956
5063
  return savedConfig || this.getDefaultEndlessConfig();
3957
5064
  }
3958
5065
  /**
@@ -3961,7 +5068,7 @@ var MemoryService = class {
3961
5068
  async setEndlessConfig(config) {
3962
5069
  const current = await this.getEndlessConfig();
3963
5070
  const merged = { ...current, ...config };
3964
- await this.eventStore.setEndlessConfig("config", merged);
5071
+ await this.sqliteStore.setEndlessConfig("config", merged);
3965
5072
  }
3966
5073
  /**
3967
5074
  * Set memory mode (session or endless)
@@ -3971,7 +5078,7 @@ var MemoryService = class {
3971
5078
  if (mode === this.endlessMode)
3972
5079
  return;
3973
5080
  this.endlessMode = mode;
3974
- await this.eventStore.setEndlessConfig("mode", mode);
5081
+ await this.sqliteStore.setEndlessConfig("mode", mode);
3975
5082
  if (mode === "endless") {
3976
5083
  await this.initializeEndlessMode();
3977
5084
  } else {
@@ -4029,12 +5136,49 @@ var MemoryService = class {
4029
5136
  return this.consolidatedStore.getAll({ limit });
4030
5137
  }
4031
5138
  /**
4032
- * Get most accessed consolidated memories
5139
+ * Increment access count for memories that were used in prompts
5140
+ */
5141
+ async incrementMemoryAccess(eventIds) {
5142
+ if (eventIds.length === 0)
5143
+ return;
5144
+ if (this.sqliteStore) {
5145
+ await this.sqliteStore.incrementAccessCount(eventIds);
5146
+ } else if (this.eventStore) {
5147
+ await this.eventStore.incrementAccessCount(eventIds);
5148
+ }
5149
+ }
5150
+ /**
5151
+ * Get most accessed memories from events
4033
5152
  */
4034
5153
  async getMostAccessedMemories(limit = 10) {
4035
- if (!this.consolidatedStore)
4036
- return [];
4037
- return this.consolidatedStore.getMostAccessed(limit);
5154
+ console.log("[getMostAccessedMemories] sqliteStore available:", !!this.sqliteStore);
5155
+ if (this.sqliteStore) {
5156
+ const events = await this.sqliteStore.getMostAccessed(limit);
5157
+ console.log("[getMostAccessedMemories] Got events from SQLite:", events.length);
5158
+ return events.map((event) => ({
5159
+ memoryId: event.id,
5160
+ summary: event.content.substring(0, 200) + (event.content.length > 200 ? "..." : ""),
5161
+ topics: [],
5162
+ // Could extract topics from content if needed
5163
+ accessCount: event.access_count || 0,
5164
+ lastAccessed: event.last_accessed_at || null,
5165
+ confidence: 1,
5166
+ createdAt: event.timestamp
5167
+ }));
5168
+ }
5169
+ if (this.consolidatedStore) {
5170
+ const consolidated = await this.consolidatedStore.getMostAccessed(limit);
5171
+ return consolidated.map((m) => ({
5172
+ memoryId: m.memoryId,
5173
+ summary: m.summary,
5174
+ topics: m.topics,
5175
+ accessCount: m.accessCount,
5176
+ lastAccessed: m.accessedAt,
5177
+ confidence: m.confidence,
5178
+ createdAt: m.createdAt
5179
+ }));
5180
+ }
5181
+ return [];
4038
5182
  }
4039
5183
  /**
4040
5184
  * Mark a consolidated memory as accessed
@@ -4159,10 +5303,16 @@ var MemoryService = class {
4159
5303
  if (this.vectorWorker) {
4160
5304
  this.vectorWorker.stop();
4161
5305
  }
5306
+ if (this.syncWorker) {
5307
+ this.syncWorker.stop();
5308
+ }
4162
5309
  if (this.sharedEventStore) {
4163
5310
  await this.sharedEventStore.close();
4164
5311
  }
4165
- await this.eventStore.close();
5312
+ await this.sqliteStore.close();
5313
+ if (this.analyticsStore) {
5314
+ await this.analyticsStore.close();
5315
+ }
4166
5316
  }
4167
5317
  /**
4168
5318
  * Expand ~ to home directory
@@ -4182,7 +5332,10 @@ function getMemoryServiceForProject(projectPath, sharedStoreConfig) {
4182
5332
  serviceCache.set(hash, new MemoryService({
4183
5333
  storagePath,
4184
5334
  projectHash: hash,
4185
- sharedStoreConfig
5335
+ // Override shared store config - hooks don't need DuckDB
5336
+ sharedStoreConfig: sharedStoreConfig ?? { enabled: false },
5337
+ analyticsEnabled: false
5338
+ // Hooks don't need DuckDB
4186
5339
  }));
4187
5340
  }
4188
5341
  return serviceCache.get(hash);