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.
- package/.claude/settings.local.json +7 -1
- package/.claude-memory/test.sqlite +0 -0
- package/.history/package_20260202114053.json +49 -0
- package/.history/package_20260202121115.json +49 -0
- package/HANDOFF.md +92 -0
- package/dist/cli/index.js +1257 -74
- package/dist/cli/index.js.map +4 -4
- package/dist/core/index.js +1111 -47
- package/dist/core/index.js.map +4 -4
- package/dist/hooks/post-tool-use.js +5693 -0
- package/dist/hooks/post-tool-use.js.map +7 -0
- package/dist/hooks/session-end.js +1224 -67
- package/dist/hooks/session-end.js.map +4 -4
- package/dist/hooks/session-start.js +1219 -66
- package/dist/hooks/session-start.js.map +4 -4
- package/dist/hooks/stop.js +1224 -67
- package/dist/hooks/stop.js.map +4 -4
- package/dist/hooks/user-prompt-submit.js +1252 -98
- package/dist/hooks/user-prompt-submit.js.map +4 -4
- package/dist/server/api/index.js +1252 -73
- package/dist/server/api/index.js.map +4 -4
- package/dist/server/index.js +1252 -73
- package/dist/server/index.js.map +4 -4
- package/dist/services/memory-service.js +1246 -68
- package/dist/services/memory-service.js.map +4 -4
- package/dist/ui/app.js +304 -0
- package/dist/ui/index.html +195 -1188
- package/dist/ui/style.css +595 -0
- package/package.json +3 -1
- package/scripts/build.ts +2 -0
- package/src/core/event-store.ts +18 -0
- package/src/core/index.ts +3 -0
- package/src/core/retriever.ts +4 -1
- package/src/core/sqlite-event-store.ts +947 -0
- package/src/core/sqlite-wrapper.ts +108 -0
- package/src/core/sync-worker.ts +228 -0
- package/src/core/vector-worker.ts +44 -14
- package/src/hooks/user-prompt-submit.ts +40 -17
- package/src/server/api/stats.ts +37 -7
- package/src/services/memory-service.ts +239 -43
- package/src/ui/app.js +304 -0
- package/src/ui/index.html +195 -1188
- package/src/ui/style.css +595 -0
- package/test_access.js +49 -0
package/dist/server/index.js
CHANGED
|
@@ -715,26 +715,1053 @@ var EventStore = class {
|
|
|
715
715
|
tags: row.tags ? JSON.parse(row.tags) : void 0
|
|
716
716
|
}));
|
|
717
717
|
}
|
|
718
|
+
/**
|
|
719
|
+
* Increment access count for events (stub for compatibility)
|
|
720
|
+
*/
|
|
721
|
+
async incrementAccessCount(eventIds) {
|
|
722
|
+
return Promise.resolve();
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Get most accessed memories (stub for compatibility)
|
|
726
|
+
*/
|
|
727
|
+
async getMostAccessed(limit = 10) {
|
|
728
|
+
return [];
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Close database connection
|
|
732
|
+
*/
|
|
733
|
+
async close() {
|
|
734
|
+
await dbClose(this.db);
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* Convert database row to MemoryEvent
|
|
738
|
+
*/
|
|
739
|
+
rowToEvent(row) {
|
|
740
|
+
return {
|
|
741
|
+
id: row.id,
|
|
742
|
+
eventType: row.event_type,
|
|
743
|
+
sessionId: row.session_id,
|
|
744
|
+
timestamp: toDate(row.timestamp),
|
|
745
|
+
content: row.content,
|
|
746
|
+
canonicalKey: row.canonical_key,
|
|
747
|
+
dedupeKey: row.dedupe_key,
|
|
748
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : void 0
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
};
|
|
752
|
+
|
|
753
|
+
// src/core/sqlite-event-store.ts
|
|
754
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
755
|
+
|
|
756
|
+
// src/core/sqlite-wrapper.ts
|
|
757
|
+
import Database from "better-sqlite3";
|
|
758
|
+
function createSQLiteDatabase(path3, options) {
|
|
759
|
+
const db = new Database(path3, {
|
|
760
|
+
readonly: options?.readonly ?? false
|
|
761
|
+
});
|
|
762
|
+
if (!options?.readonly && (options?.walMode ?? true)) {
|
|
763
|
+
db.pragma("journal_mode = WAL");
|
|
764
|
+
db.pragma("synchronous = NORMAL");
|
|
765
|
+
db.pragma("busy_timeout = 5000");
|
|
766
|
+
}
|
|
767
|
+
return db;
|
|
768
|
+
}
|
|
769
|
+
function sqliteRun(db, sql, params = []) {
|
|
770
|
+
const stmt = db.prepare(sql);
|
|
771
|
+
return stmt.run(...params);
|
|
772
|
+
}
|
|
773
|
+
function sqliteAll(db, sql, params = []) {
|
|
774
|
+
const stmt = db.prepare(sql);
|
|
775
|
+
return stmt.all(...params);
|
|
776
|
+
}
|
|
777
|
+
function sqliteGet(db, sql, params = []) {
|
|
778
|
+
const stmt = db.prepare(sql);
|
|
779
|
+
return stmt.get(...params);
|
|
780
|
+
}
|
|
781
|
+
function sqliteExec(db, sql) {
|
|
782
|
+
db.exec(sql);
|
|
783
|
+
}
|
|
784
|
+
function sqliteClose(db) {
|
|
785
|
+
db.close();
|
|
786
|
+
}
|
|
787
|
+
function toDateFromSQLite(value) {
|
|
788
|
+
if (value instanceof Date)
|
|
789
|
+
return value;
|
|
790
|
+
if (typeof value === "string")
|
|
791
|
+
return new Date(value);
|
|
792
|
+
if (typeof value === "number")
|
|
793
|
+
return new Date(value);
|
|
794
|
+
return new Date(String(value));
|
|
795
|
+
}
|
|
796
|
+
function toSQLiteTimestamp(date) {
|
|
797
|
+
return date.toISOString();
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// src/core/sqlite-event-store.ts
|
|
801
|
+
var SQLiteEventStore = class {
|
|
802
|
+
constructor(dbPath, options) {
|
|
803
|
+
this.dbPath = dbPath;
|
|
804
|
+
this.readOnly = options?.readonly ?? false;
|
|
805
|
+
this.db = createSQLiteDatabase(dbPath, {
|
|
806
|
+
readonly: this.readOnly,
|
|
807
|
+
walMode: !this.readOnly
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
db;
|
|
811
|
+
initialized = false;
|
|
812
|
+
readOnly;
|
|
813
|
+
/**
|
|
814
|
+
* Initialize database schema
|
|
815
|
+
*/
|
|
816
|
+
async initialize() {
|
|
817
|
+
if (this.initialized)
|
|
818
|
+
return;
|
|
819
|
+
if (this.readOnly) {
|
|
820
|
+
this.initialized = true;
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
sqliteExec(this.db, `
|
|
824
|
+
-- L0 EventStore: Single Source of Truth (immutable, append-only)
|
|
825
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
826
|
+
id TEXT PRIMARY KEY,
|
|
827
|
+
event_type TEXT NOT NULL,
|
|
828
|
+
session_id TEXT NOT NULL,
|
|
829
|
+
timestamp TEXT NOT NULL,
|
|
830
|
+
content TEXT NOT NULL,
|
|
831
|
+
canonical_key TEXT NOT NULL,
|
|
832
|
+
dedupe_key TEXT UNIQUE,
|
|
833
|
+
metadata TEXT,
|
|
834
|
+
access_count INTEGER DEFAULT 0,
|
|
835
|
+
last_accessed_at TEXT
|
|
836
|
+
);
|
|
837
|
+
|
|
838
|
+
-- Dedup table for idempotency
|
|
839
|
+
CREATE TABLE IF NOT EXISTS event_dedup (
|
|
840
|
+
dedupe_key TEXT PRIMARY KEY,
|
|
841
|
+
event_id TEXT NOT NULL,
|
|
842
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
843
|
+
);
|
|
844
|
+
|
|
845
|
+
-- Session metadata
|
|
846
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
847
|
+
id TEXT PRIMARY KEY,
|
|
848
|
+
started_at TEXT NOT NULL,
|
|
849
|
+
ended_at TEXT,
|
|
850
|
+
project_path TEXT,
|
|
851
|
+
summary TEXT,
|
|
852
|
+
tags TEXT
|
|
853
|
+
);
|
|
854
|
+
|
|
855
|
+
-- Insights (derived data, rebuildable)
|
|
856
|
+
CREATE TABLE IF NOT EXISTS insights (
|
|
857
|
+
id TEXT PRIMARY KEY,
|
|
858
|
+
insight_type TEXT NOT NULL,
|
|
859
|
+
content TEXT NOT NULL,
|
|
860
|
+
canonical_key TEXT NOT NULL,
|
|
861
|
+
confidence REAL,
|
|
862
|
+
source_events TEXT,
|
|
863
|
+
created_at TEXT,
|
|
864
|
+
last_updated TEXT
|
|
865
|
+
);
|
|
866
|
+
|
|
867
|
+
-- Embedding Outbox (Single-Writer Pattern)
|
|
868
|
+
CREATE TABLE IF NOT EXISTS embedding_outbox (
|
|
869
|
+
id TEXT PRIMARY KEY,
|
|
870
|
+
event_id TEXT NOT NULL,
|
|
871
|
+
content TEXT NOT NULL,
|
|
872
|
+
status TEXT DEFAULT 'pending',
|
|
873
|
+
retry_count INTEGER DEFAULT 0,
|
|
874
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
875
|
+
processed_at TEXT,
|
|
876
|
+
error_message TEXT
|
|
877
|
+
);
|
|
878
|
+
|
|
879
|
+
-- Projection offset tracking
|
|
880
|
+
CREATE TABLE IF NOT EXISTS projection_offsets (
|
|
881
|
+
projection_name TEXT PRIMARY KEY,
|
|
882
|
+
last_event_id TEXT,
|
|
883
|
+
last_timestamp TEXT,
|
|
884
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
885
|
+
);
|
|
886
|
+
|
|
887
|
+
-- Memory level tracking
|
|
888
|
+
CREATE TABLE IF NOT EXISTS memory_levels (
|
|
889
|
+
event_id TEXT PRIMARY KEY,
|
|
890
|
+
level TEXT NOT NULL DEFAULT 'L0',
|
|
891
|
+
promoted_at TEXT DEFAULT (datetime('now'))
|
|
892
|
+
);
|
|
893
|
+
|
|
894
|
+
-- Entries (immutable memory units)
|
|
895
|
+
CREATE TABLE IF NOT EXISTS entries (
|
|
896
|
+
entry_id TEXT PRIMARY KEY,
|
|
897
|
+
created_ts TEXT NOT NULL,
|
|
898
|
+
entry_type TEXT NOT NULL,
|
|
899
|
+
title TEXT NOT NULL,
|
|
900
|
+
content_json TEXT NOT NULL,
|
|
901
|
+
stage TEXT NOT NULL DEFAULT 'raw',
|
|
902
|
+
status TEXT DEFAULT 'active',
|
|
903
|
+
superseded_by TEXT,
|
|
904
|
+
build_id TEXT,
|
|
905
|
+
evidence_json TEXT,
|
|
906
|
+
canonical_key TEXT,
|
|
907
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
908
|
+
);
|
|
909
|
+
|
|
910
|
+
-- Entities (task/condition/artifact)
|
|
911
|
+
CREATE TABLE IF NOT EXISTS entities (
|
|
912
|
+
entity_id TEXT PRIMARY KEY,
|
|
913
|
+
entity_type TEXT NOT NULL,
|
|
914
|
+
canonical_key TEXT NOT NULL,
|
|
915
|
+
title TEXT NOT NULL,
|
|
916
|
+
stage TEXT NOT NULL DEFAULT 'raw',
|
|
917
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
918
|
+
current_json TEXT NOT NULL,
|
|
919
|
+
title_norm TEXT,
|
|
920
|
+
search_text TEXT,
|
|
921
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
922
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
923
|
+
);
|
|
924
|
+
|
|
925
|
+
-- Entity aliases for canonical key lookup
|
|
926
|
+
CREATE TABLE IF NOT EXISTS entity_aliases (
|
|
927
|
+
entity_type TEXT NOT NULL,
|
|
928
|
+
canonical_key TEXT NOT NULL,
|
|
929
|
+
entity_id TEXT NOT NULL,
|
|
930
|
+
is_primary INTEGER DEFAULT 0,
|
|
931
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
932
|
+
PRIMARY KEY(entity_type, canonical_key)
|
|
933
|
+
);
|
|
934
|
+
|
|
935
|
+
-- Edges (relationships between entries/entities)
|
|
936
|
+
CREATE TABLE IF NOT EXISTS edges (
|
|
937
|
+
edge_id TEXT PRIMARY KEY,
|
|
938
|
+
src_type TEXT NOT NULL,
|
|
939
|
+
src_id TEXT NOT NULL,
|
|
940
|
+
rel_type TEXT NOT NULL,
|
|
941
|
+
dst_type TEXT NOT NULL,
|
|
942
|
+
dst_id TEXT NOT NULL,
|
|
943
|
+
meta_json TEXT,
|
|
944
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
945
|
+
);
|
|
946
|
+
|
|
947
|
+
-- Vector Outbox V2 Table
|
|
948
|
+
CREATE TABLE IF NOT EXISTS vector_outbox (
|
|
949
|
+
job_id TEXT PRIMARY KEY,
|
|
950
|
+
item_kind TEXT NOT NULL,
|
|
951
|
+
item_id TEXT NOT NULL,
|
|
952
|
+
embedding_version TEXT NOT NULL,
|
|
953
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
954
|
+
retry_count INTEGER DEFAULT 0,
|
|
955
|
+
error TEXT,
|
|
956
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
957
|
+
updated_at TEXT DEFAULT (datetime('now')),
|
|
958
|
+
UNIQUE(item_kind, item_id, embedding_version)
|
|
959
|
+
);
|
|
960
|
+
|
|
961
|
+
-- Build Runs
|
|
962
|
+
CREATE TABLE IF NOT EXISTS build_runs (
|
|
963
|
+
build_id TEXT PRIMARY KEY,
|
|
964
|
+
started_at TEXT NOT NULL,
|
|
965
|
+
finished_at TEXT,
|
|
966
|
+
extractor_model TEXT NOT NULL,
|
|
967
|
+
extractor_prompt_hash TEXT NOT NULL,
|
|
968
|
+
embedder_model TEXT NOT NULL,
|
|
969
|
+
embedding_version TEXT NOT NULL,
|
|
970
|
+
idris_version TEXT NOT NULL,
|
|
971
|
+
schema_version TEXT NOT NULL,
|
|
972
|
+
status TEXT NOT NULL DEFAULT 'running',
|
|
973
|
+
error TEXT
|
|
974
|
+
);
|
|
975
|
+
|
|
976
|
+
-- Pipeline Metrics
|
|
977
|
+
CREATE TABLE IF NOT EXISTS pipeline_metrics (
|
|
978
|
+
id TEXT PRIMARY KEY,
|
|
979
|
+
ts TEXT NOT NULL,
|
|
980
|
+
stage TEXT NOT NULL,
|
|
981
|
+
latency_ms REAL NOT NULL,
|
|
982
|
+
success INTEGER NOT NULL,
|
|
983
|
+
error TEXT,
|
|
984
|
+
session_id TEXT
|
|
985
|
+
);
|
|
986
|
+
|
|
987
|
+
-- Working Set table (active memory window)
|
|
988
|
+
CREATE TABLE IF NOT EXISTS working_set (
|
|
989
|
+
id TEXT PRIMARY KEY,
|
|
990
|
+
event_id TEXT NOT NULL,
|
|
991
|
+
added_at TEXT DEFAULT (datetime('now')),
|
|
992
|
+
relevance_score REAL DEFAULT 1.0,
|
|
993
|
+
topics TEXT,
|
|
994
|
+
expires_at TEXT
|
|
995
|
+
);
|
|
996
|
+
|
|
997
|
+
-- Consolidated Memories table (long-term integrated memories)
|
|
998
|
+
CREATE TABLE IF NOT EXISTS consolidated_memories (
|
|
999
|
+
memory_id TEXT PRIMARY KEY,
|
|
1000
|
+
summary TEXT NOT NULL,
|
|
1001
|
+
topics TEXT,
|
|
1002
|
+
source_events TEXT,
|
|
1003
|
+
confidence REAL DEFAULT 0.5,
|
|
1004
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
1005
|
+
accessed_at TEXT,
|
|
1006
|
+
access_count INTEGER DEFAULT 0
|
|
1007
|
+
);
|
|
1008
|
+
|
|
1009
|
+
-- Continuity Log table (tracks context transitions)
|
|
1010
|
+
CREATE TABLE IF NOT EXISTS continuity_log (
|
|
1011
|
+
log_id TEXT PRIMARY KEY,
|
|
1012
|
+
from_context_id TEXT,
|
|
1013
|
+
to_context_id TEXT,
|
|
1014
|
+
continuity_score REAL,
|
|
1015
|
+
transition_type TEXT,
|
|
1016
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
1017
|
+
);
|
|
1018
|
+
|
|
1019
|
+
-- Endless Mode Config table
|
|
1020
|
+
CREATE TABLE IF NOT EXISTS endless_config (
|
|
1021
|
+
key TEXT PRIMARY KEY,
|
|
1022
|
+
value TEXT,
|
|
1023
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
1024
|
+
);
|
|
1025
|
+
|
|
1026
|
+
-- Sync position tracking (for SQLite -> DuckDB sync)
|
|
1027
|
+
CREATE TABLE IF NOT EXISTS sync_positions (
|
|
1028
|
+
target_name TEXT PRIMARY KEY,
|
|
1029
|
+
last_event_id TEXT,
|
|
1030
|
+
last_timestamp TEXT,
|
|
1031
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
1032
|
+
);
|
|
1033
|
+
|
|
1034
|
+
-- Create indexes
|
|
1035
|
+
CREATE INDEX IF NOT EXISTS idx_events_session ON events(session_id);
|
|
1036
|
+
CREATE INDEX IF NOT EXISTS idx_events_timestamp ON events(timestamp);
|
|
1037
|
+
CREATE INDEX IF NOT EXISTS idx_entries_type ON entries(entry_type);
|
|
1038
|
+
CREATE INDEX IF NOT EXISTS idx_entries_stage ON entries(stage);
|
|
1039
|
+
CREATE INDEX IF NOT EXISTS idx_entries_canonical ON entries(canonical_key);
|
|
1040
|
+
CREATE INDEX IF NOT EXISTS idx_entities_type_key ON entities(entity_type, canonical_key);
|
|
1041
|
+
CREATE INDEX IF NOT EXISTS idx_entities_status ON entities(status);
|
|
1042
|
+
CREATE INDEX IF NOT EXISTS idx_edges_src ON edges(src_id, rel_type);
|
|
1043
|
+
CREATE INDEX IF NOT EXISTS idx_edges_dst ON edges(dst_id, rel_type);
|
|
1044
|
+
CREATE INDEX IF NOT EXISTS idx_edges_rel ON edges(rel_type);
|
|
1045
|
+
CREATE INDEX IF NOT EXISTS idx_outbox_status ON vector_outbox(status);
|
|
1046
|
+
CREATE INDEX IF NOT EXISTS idx_working_set_expires ON working_set(expires_at);
|
|
1047
|
+
CREATE INDEX IF NOT EXISTS idx_working_set_relevance ON working_set(relevance_score);
|
|
1048
|
+
CREATE INDEX IF NOT EXISTS idx_consolidated_confidence ON consolidated_memories(confidence);
|
|
1049
|
+
CREATE INDEX IF NOT EXISTS idx_continuity_created ON continuity_log(created_at);
|
|
1050
|
+
CREATE INDEX IF NOT EXISTS idx_embedding_outbox_status ON embedding_outbox(status);
|
|
1051
|
+
|
|
1052
|
+
-- FTS5 Full-Text Search for fast keyword search
|
|
1053
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS events_fts USING fts5(
|
|
1054
|
+
content,
|
|
1055
|
+
event_id UNINDEXED,
|
|
1056
|
+
content='events',
|
|
1057
|
+
content_rowid='rowid'
|
|
1058
|
+
);
|
|
1059
|
+
|
|
1060
|
+
-- Triggers to keep FTS in sync with events table
|
|
1061
|
+
CREATE TRIGGER IF NOT EXISTS events_fts_insert AFTER INSERT ON events BEGIN
|
|
1062
|
+
INSERT INTO events_fts(rowid, content, event_id) VALUES (NEW.rowid, NEW.content, NEW.id);
|
|
1063
|
+
END;
|
|
1064
|
+
|
|
1065
|
+
CREATE TRIGGER IF NOT EXISTS events_fts_delete AFTER DELETE ON events BEGIN
|
|
1066
|
+
INSERT INTO events_fts(events_fts, rowid, content, event_id) VALUES('delete', OLD.rowid, OLD.content, OLD.id);
|
|
1067
|
+
END;
|
|
1068
|
+
|
|
1069
|
+
CREATE TRIGGER IF NOT EXISTS events_fts_update AFTER UPDATE ON events BEGIN
|
|
1070
|
+
INSERT INTO events_fts(events_fts, rowid, content, event_id) VALUES('delete', OLD.rowid, OLD.content, OLD.id);
|
|
1071
|
+
INSERT INTO events_fts(rowid, content, event_id) VALUES (NEW.rowid, NEW.content, NEW.id);
|
|
1072
|
+
END;
|
|
1073
|
+
`);
|
|
1074
|
+
const tableInfo = sqliteAll(this.db, "PRAGMA table_info(events)", []);
|
|
1075
|
+
const columnNames = tableInfo.map((col) => col.name);
|
|
1076
|
+
if (!columnNames.includes("access_count")) {
|
|
1077
|
+
try {
|
|
1078
|
+
sqliteExec(this.db, `
|
|
1079
|
+
ALTER TABLE events ADD COLUMN access_count INTEGER DEFAULT 0;
|
|
1080
|
+
`);
|
|
1081
|
+
} catch (err) {
|
|
1082
|
+
console.error("Error adding access_count column:", err);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
if (!columnNames.includes("last_accessed_at")) {
|
|
1086
|
+
try {
|
|
1087
|
+
sqliteExec(this.db, `
|
|
1088
|
+
ALTER TABLE events ADD COLUMN last_accessed_at TEXT;
|
|
1089
|
+
`);
|
|
1090
|
+
} catch (err) {
|
|
1091
|
+
console.error("Error adding last_accessed_at column:", err);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
try {
|
|
1095
|
+
sqliteExec(this.db, `
|
|
1096
|
+
CREATE INDEX IF NOT EXISTS idx_events_access_count ON events(access_count DESC);
|
|
1097
|
+
`);
|
|
1098
|
+
} catch (err) {
|
|
1099
|
+
}
|
|
1100
|
+
try {
|
|
1101
|
+
sqliteExec(this.db, `
|
|
1102
|
+
CREATE INDEX IF NOT EXISTS idx_events_last_accessed ON events(last_accessed_at DESC);
|
|
1103
|
+
`);
|
|
1104
|
+
} catch (err) {
|
|
1105
|
+
}
|
|
1106
|
+
this.initialized = true;
|
|
1107
|
+
}
|
|
1108
|
+
/**
|
|
1109
|
+
* Append event to store (Append-only, Idempotent)
|
|
1110
|
+
*/
|
|
1111
|
+
async append(input) {
|
|
1112
|
+
await this.initialize();
|
|
1113
|
+
const canonicalKey = makeCanonicalKey(input.content);
|
|
1114
|
+
const dedupeKey = makeDedupeKey(input.content, input.sessionId);
|
|
1115
|
+
const existing = sqliteGet(
|
|
1116
|
+
this.db,
|
|
1117
|
+
`SELECT event_id FROM event_dedup WHERE dedupe_key = ?`,
|
|
1118
|
+
[dedupeKey]
|
|
1119
|
+
);
|
|
1120
|
+
if (existing) {
|
|
1121
|
+
return {
|
|
1122
|
+
success: true,
|
|
1123
|
+
eventId: existing.event_id,
|
|
1124
|
+
isDuplicate: true
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
const id = randomUUID2();
|
|
1128
|
+
const timestamp = toSQLiteTimestamp(input.timestamp);
|
|
1129
|
+
try {
|
|
1130
|
+
const insertEvent = this.db.prepare(`
|
|
1131
|
+
INSERT INTO events (id, event_type, session_id, timestamp, content, canonical_key, dedupe_key, metadata)
|
|
1132
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1133
|
+
`);
|
|
1134
|
+
const insertDedup = this.db.prepare(`
|
|
1135
|
+
INSERT INTO event_dedup (dedupe_key, event_id) VALUES (?, ?)
|
|
1136
|
+
`);
|
|
1137
|
+
const insertLevel = this.db.prepare(`
|
|
1138
|
+
INSERT INTO memory_levels (event_id, level) VALUES (?, 'L0')
|
|
1139
|
+
`);
|
|
1140
|
+
const transaction = this.db.transaction(() => {
|
|
1141
|
+
insertEvent.run(
|
|
1142
|
+
id,
|
|
1143
|
+
input.eventType,
|
|
1144
|
+
input.sessionId,
|
|
1145
|
+
timestamp,
|
|
1146
|
+
input.content,
|
|
1147
|
+
canonicalKey,
|
|
1148
|
+
dedupeKey,
|
|
1149
|
+
JSON.stringify(input.metadata || {})
|
|
1150
|
+
);
|
|
1151
|
+
insertDedup.run(dedupeKey, id);
|
|
1152
|
+
insertLevel.run(id);
|
|
1153
|
+
});
|
|
1154
|
+
transaction();
|
|
1155
|
+
return { success: true, eventId: id, isDuplicate: false };
|
|
1156
|
+
} catch (error) {
|
|
1157
|
+
return {
|
|
1158
|
+
success: false,
|
|
1159
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
/**
|
|
1164
|
+
* Get events by session ID
|
|
1165
|
+
*/
|
|
1166
|
+
async getSessionEvents(sessionId) {
|
|
1167
|
+
await this.initialize();
|
|
1168
|
+
const rows = sqliteAll(
|
|
1169
|
+
this.db,
|
|
1170
|
+
`SELECT * FROM events WHERE session_id = ? ORDER BY timestamp ASC`,
|
|
1171
|
+
[sessionId]
|
|
1172
|
+
);
|
|
1173
|
+
return rows.map(this.rowToEvent);
|
|
1174
|
+
}
|
|
1175
|
+
/**
|
|
1176
|
+
* Get recent events
|
|
1177
|
+
*/
|
|
1178
|
+
async getRecentEvents(limit = 100) {
|
|
1179
|
+
await this.initialize();
|
|
1180
|
+
const rows = sqliteAll(
|
|
1181
|
+
this.db,
|
|
1182
|
+
`SELECT * FROM events ORDER BY timestamp DESC LIMIT ?`,
|
|
1183
|
+
[limit]
|
|
1184
|
+
);
|
|
1185
|
+
return rows.map(this.rowToEvent);
|
|
1186
|
+
}
|
|
1187
|
+
/**
|
|
1188
|
+
* Get event by ID
|
|
1189
|
+
*/
|
|
1190
|
+
async getEvent(id) {
|
|
1191
|
+
await this.initialize();
|
|
1192
|
+
const row = sqliteGet(
|
|
1193
|
+
this.db,
|
|
1194
|
+
`SELECT * FROM events WHERE id = ?`,
|
|
1195
|
+
[id]
|
|
1196
|
+
);
|
|
1197
|
+
if (!row)
|
|
1198
|
+
return null;
|
|
1199
|
+
return this.rowToEvent(row);
|
|
1200
|
+
}
|
|
1201
|
+
/**
|
|
1202
|
+
* Get events since a timestamp (for sync)
|
|
1203
|
+
*/
|
|
1204
|
+
async getEventsSince(timestamp, limit = 1e3) {
|
|
1205
|
+
await this.initialize();
|
|
1206
|
+
const rows = sqliteAll(
|
|
1207
|
+
this.db,
|
|
1208
|
+
`SELECT * FROM events WHERE timestamp > ? ORDER BY timestamp ASC LIMIT ?`,
|
|
1209
|
+
[timestamp, limit]
|
|
1210
|
+
);
|
|
1211
|
+
return rows.map(this.rowToEvent);
|
|
1212
|
+
}
|
|
1213
|
+
/**
|
|
1214
|
+
* Create or update session
|
|
1215
|
+
*/
|
|
1216
|
+
async upsertSession(session) {
|
|
1217
|
+
await this.initialize();
|
|
1218
|
+
const existing = sqliteGet(
|
|
1219
|
+
this.db,
|
|
1220
|
+
`SELECT id FROM sessions WHERE id = ?`,
|
|
1221
|
+
[session.id]
|
|
1222
|
+
);
|
|
1223
|
+
if (!existing) {
|
|
1224
|
+
sqliteRun(
|
|
1225
|
+
this.db,
|
|
1226
|
+
`INSERT INTO sessions (id, started_at, project_path, tags)
|
|
1227
|
+
VALUES (?, ?, ?, ?)`,
|
|
1228
|
+
[
|
|
1229
|
+
session.id,
|
|
1230
|
+
toSQLiteTimestamp(session.startedAt || /* @__PURE__ */ new Date()),
|
|
1231
|
+
session.projectPath || null,
|
|
1232
|
+
JSON.stringify(session.tags || [])
|
|
1233
|
+
]
|
|
1234
|
+
);
|
|
1235
|
+
} else {
|
|
1236
|
+
const updates = [];
|
|
1237
|
+
const values = [];
|
|
1238
|
+
if (session.endedAt) {
|
|
1239
|
+
updates.push("ended_at = ?");
|
|
1240
|
+
values.push(toSQLiteTimestamp(session.endedAt));
|
|
1241
|
+
}
|
|
1242
|
+
if (session.summary) {
|
|
1243
|
+
updates.push("summary = ?");
|
|
1244
|
+
values.push(session.summary);
|
|
1245
|
+
}
|
|
1246
|
+
if (session.tags) {
|
|
1247
|
+
updates.push("tags = ?");
|
|
1248
|
+
values.push(JSON.stringify(session.tags));
|
|
1249
|
+
}
|
|
1250
|
+
if (updates.length > 0) {
|
|
1251
|
+
values.push(session.id);
|
|
1252
|
+
sqliteRun(
|
|
1253
|
+
this.db,
|
|
1254
|
+
`UPDATE sessions SET ${updates.join(", ")} WHERE id = ?`,
|
|
1255
|
+
values
|
|
1256
|
+
);
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
/**
|
|
1261
|
+
* Get session by ID
|
|
1262
|
+
*/
|
|
1263
|
+
async getSession(id) {
|
|
1264
|
+
await this.initialize();
|
|
1265
|
+
const row = sqliteGet(
|
|
1266
|
+
this.db,
|
|
1267
|
+
`SELECT * FROM sessions WHERE id = ?`,
|
|
1268
|
+
[id]
|
|
1269
|
+
);
|
|
1270
|
+
if (!row)
|
|
1271
|
+
return null;
|
|
1272
|
+
return {
|
|
1273
|
+
id: row.id,
|
|
1274
|
+
startedAt: toDateFromSQLite(row.started_at),
|
|
1275
|
+
endedAt: row.ended_at ? toDateFromSQLite(row.ended_at) : void 0,
|
|
1276
|
+
projectPath: row.project_path,
|
|
1277
|
+
summary: row.summary,
|
|
1278
|
+
tags: row.tags ? JSON.parse(row.tags) : void 0
|
|
1279
|
+
};
|
|
1280
|
+
}
|
|
1281
|
+
/**
|
|
1282
|
+
* Get all sessions
|
|
1283
|
+
*/
|
|
1284
|
+
async getAllSessions() {
|
|
1285
|
+
await this.initialize();
|
|
1286
|
+
const rows = sqliteAll(
|
|
1287
|
+
this.db,
|
|
1288
|
+
`SELECT * FROM sessions ORDER BY started_at DESC`
|
|
1289
|
+
);
|
|
1290
|
+
return rows.map((row) => ({
|
|
1291
|
+
id: row.id,
|
|
1292
|
+
startedAt: toDateFromSQLite(row.started_at),
|
|
1293
|
+
endedAt: row.ended_at ? toDateFromSQLite(row.ended_at) : void 0,
|
|
1294
|
+
projectPath: row.project_path,
|
|
1295
|
+
summary: row.summary,
|
|
1296
|
+
tags: row.tags ? JSON.parse(row.tags) : void 0
|
|
1297
|
+
}));
|
|
1298
|
+
}
|
|
1299
|
+
/**
|
|
1300
|
+
* Add to embedding outbox
|
|
1301
|
+
*/
|
|
1302
|
+
async enqueueForEmbedding(eventId, content) {
|
|
1303
|
+
await this.initialize();
|
|
1304
|
+
const id = randomUUID2();
|
|
1305
|
+
sqliteRun(
|
|
1306
|
+
this.db,
|
|
1307
|
+
`INSERT INTO embedding_outbox (id, event_id, content, status, retry_count)
|
|
1308
|
+
VALUES (?, ?, ?, 'pending', 0)`,
|
|
1309
|
+
[id, eventId, content]
|
|
1310
|
+
);
|
|
1311
|
+
return id;
|
|
1312
|
+
}
|
|
1313
|
+
/**
|
|
1314
|
+
* Get pending outbox items
|
|
1315
|
+
*/
|
|
1316
|
+
async getPendingOutboxItems(limit = 32) {
|
|
1317
|
+
await this.initialize();
|
|
1318
|
+
const pending = sqliteAll(
|
|
1319
|
+
this.db,
|
|
1320
|
+
`SELECT * FROM embedding_outbox
|
|
1321
|
+
WHERE status = 'pending'
|
|
1322
|
+
ORDER BY created_at
|
|
1323
|
+
LIMIT ?`,
|
|
1324
|
+
[limit]
|
|
1325
|
+
);
|
|
1326
|
+
if (pending.length === 0)
|
|
1327
|
+
return [];
|
|
1328
|
+
const ids = pending.map((r) => r.id);
|
|
1329
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
1330
|
+
sqliteRun(
|
|
1331
|
+
this.db,
|
|
1332
|
+
`UPDATE embedding_outbox SET status = 'processing' WHERE id IN (${placeholders})`,
|
|
1333
|
+
ids
|
|
1334
|
+
);
|
|
1335
|
+
return pending.map((row) => ({
|
|
1336
|
+
id: row.id,
|
|
1337
|
+
eventId: row.event_id,
|
|
1338
|
+
content: row.content,
|
|
1339
|
+
status: "processing",
|
|
1340
|
+
retryCount: row.retry_count,
|
|
1341
|
+
createdAt: toDateFromSQLite(row.created_at),
|
|
1342
|
+
errorMessage: row.error_message
|
|
1343
|
+
}));
|
|
1344
|
+
}
|
|
1345
|
+
/**
|
|
1346
|
+
* Mark outbox items as done
|
|
1347
|
+
*/
|
|
1348
|
+
async completeOutboxItems(ids) {
|
|
1349
|
+
if (ids.length === 0)
|
|
1350
|
+
return;
|
|
1351
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
1352
|
+
sqliteRun(
|
|
1353
|
+
this.db,
|
|
1354
|
+
`DELETE FROM embedding_outbox WHERE id IN (${placeholders})`,
|
|
1355
|
+
ids
|
|
1356
|
+
);
|
|
1357
|
+
}
|
|
1358
|
+
/**
|
|
1359
|
+
* Mark outbox items as failed
|
|
1360
|
+
*/
|
|
1361
|
+
async failOutboxItems(ids, error) {
|
|
1362
|
+
if (ids.length === 0)
|
|
1363
|
+
return;
|
|
1364
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
1365
|
+
sqliteRun(
|
|
1366
|
+
this.db,
|
|
1367
|
+
`UPDATE embedding_outbox
|
|
1368
|
+
SET status = CASE WHEN retry_count >= 3 THEN 'failed' ELSE 'pending' END,
|
|
1369
|
+
retry_count = retry_count + 1,
|
|
1370
|
+
error_message = ?
|
|
1371
|
+
WHERE id IN (${placeholders})`,
|
|
1372
|
+
[error, ...ids]
|
|
1373
|
+
);
|
|
1374
|
+
}
|
|
1375
|
+
/**
|
|
1376
|
+
* Update memory level
|
|
1377
|
+
*/
|
|
1378
|
+
async updateMemoryLevel(eventId, level) {
|
|
1379
|
+
await this.initialize();
|
|
1380
|
+
sqliteRun(
|
|
1381
|
+
this.db,
|
|
1382
|
+
`UPDATE memory_levels SET level = ?, promoted_at = datetime('now') WHERE event_id = ?`,
|
|
1383
|
+
[level, eventId]
|
|
1384
|
+
);
|
|
1385
|
+
}
|
|
1386
|
+
/**
|
|
1387
|
+
* Get memory level statistics
|
|
1388
|
+
*/
|
|
1389
|
+
async getLevelStats() {
|
|
1390
|
+
await this.initialize();
|
|
1391
|
+
const rows = sqliteAll(
|
|
1392
|
+
this.db,
|
|
1393
|
+
`SELECT level, COUNT(*) as count FROM memory_levels GROUP BY level`
|
|
1394
|
+
);
|
|
1395
|
+
return rows;
|
|
1396
|
+
}
|
|
1397
|
+
/**
|
|
1398
|
+
* Get events by memory level
|
|
1399
|
+
*/
|
|
1400
|
+
async getEventsByLevel(level, options) {
|
|
1401
|
+
await this.initialize();
|
|
1402
|
+
const limit = options?.limit || 50;
|
|
1403
|
+
const offset = options?.offset || 0;
|
|
1404
|
+
const rows = sqliteAll(
|
|
1405
|
+
this.db,
|
|
1406
|
+
`SELECT e.* FROM events e
|
|
1407
|
+
INNER JOIN memory_levels ml ON e.id = ml.event_id
|
|
1408
|
+
WHERE ml.level = ?
|
|
1409
|
+
ORDER BY e.timestamp DESC
|
|
1410
|
+
LIMIT ? OFFSET ?`,
|
|
1411
|
+
[level, limit, offset]
|
|
1412
|
+
);
|
|
1413
|
+
return rows.map((row) => this.rowToEvent(row));
|
|
1414
|
+
}
|
|
1415
|
+
/**
|
|
1416
|
+
* Get memory level for a specific event
|
|
1417
|
+
*/
|
|
1418
|
+
async getEventLevel(eventId) {
|
|
1419
|
+
await this.initialize();
|
|
1420
|
+
const row = sqliteGet(
|
|
1421
|
+
this.db,
|
|
1422
|
+
`SELECT level FROM memory_levels WHERE event_id = ?`,
|
|
1423
|
+
[eventId]
|
|
1424
|
+
);
|
|
1425
|
+
return row ? row.level : null;
|
|
1426
|
+
}
|
|
1427
|
+
/**
|
|
1428
|
+
* Get sync position for a target
|
|
1429
|
+
*/
|
|
1430
|
+
async getSyncPosition(targetName) {
|
|
1431
|
+
await this.initialize();
|
|
1432
|
+
const row = sqliteGet(
|
|
1433
|
+
this.db,
|
|
1434
|
+
`SELECT last_event_id, last_timestamp FROM sync_positions WHERE target_name = ?`,
|
|
1435
|
+
[targetName]
|
|
1436
|
+
);
|
|
1437
|
+
return {
|
|
1438
|
+
lastEventId: row?.last_event_id ?? null,
|
|
1439
|
+
lastTimestamp: row?.last_timestamp ?? null
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
/**
|
|
1443
|
+
* Update sync position for a target
|
|
1444
|
+
*/
|
|
1445
|
+
async updateSyncPosition(targetName, lastEventId, lastTimestamp) {
|
|
1446
|
+
await this.initialize();
|
|
1447
|
+
sqliteRun(
|
|
1448
|
+
this.db,
|
|
1449
|
+
`INSERT OR REPLACE INTO sync_positions (target_name, last_event_id, last_timestamp, updated_at)
|
|
1450
|
+
VALUES (?, ?, ?, datetime('now'))`,
|
|
1451
|
+
[targetName, lastEventId, lastTimestamp]
|
|
1452
|
+
);
|
|
1453
|
+
}
|
|
1454
|
+
/**
|
|
1455
|
+
* Get config value for endless mode
|
|
1456
|
+
*/
|
|
1457
|
+
async getEndlessConfig(key) {
|
|
1458
|
+
await this.initialize();
|
|
1459
|
+
const row = sqliteGet(
|
|
1460
|
+
this.db,
|
|
1461
|
+
`SELECT value FROM endless_config WHERE key = ?`,
|
|
1462
|
+
[key]
|
|
1463
|
+
);
|
|
1464
|
+
if (!row)
|
|
1465
|
+
return null;
|
|
1466
|
+
return JSON.parse(row.value);
|
|
1467
|
+
}
|
|
1468
|
+
/**
|
|
1469
|
+
* Set config value for endless mode
|
|
1470
|
+
*/
|
|
1471
|
+
async setEndlessConfig(key, value) {
|
|
1472
|
+
await this.initialize();
|
|
1473
|
+
sqliteRun(
|
|
1474
|
+
this.db,
|
|
1475
|
+
`INSERT OR REPLACE INTO endless_config (key, value, updated_at)
|
|
1476
|
+
VALUES (?, ?, datetime('now'))`,
|
|
1477
|
+
[key, JSON.stringify(value)]
|
|
1478
|
+
);
|
|
1479
|
+
}
|
|
1480
|
+
/**
|
|
1481
|
+
* Increment access count for events
|
|
1482
|
+
*/
|
|
1483
|
+
async incrementAccessCount(eventIds) {
|
|
1484
|
+
if (eventIds.length === 0 || this.readOnly)
|
|
1485
|
+
return;
|
|
1486
|
+
await this.initialize();
|
|
1487
|
+
const placeholders = eventIds.map(() => "?").join(",");
|
|
1488
|
+
const currentTime = toSQLiteTimestamp(/* @__PURE__ */ new Date());
|
|
1489
|
+
sqliteRun(
|
|
1490
|
+
this.db,
|
|
1491
|
+
`UPDATE events
|
|
1492
|
+
SET access_count = access_count + 1,
|
|
1493
|
+
last_accessed_at = ?
|
|
1494
|
+
WHERE id IN (${placeholders})`,
|
|
1495
|
+
[currentTime, ...eventIds]
|
|
1496
|
+
);
|
|
1497
|
+
}
|
|
1498
|
+
/**
|
|
1499
|
+
* Get most accessed memories
|
|
1500
|
+
*/
|
|
1501
|
+
async getMostAccessed(limit = 10) {
|
|
1502
|
+
await this.initialize();
|
|
1503
|
+
const rows = sqliteAll(
|
|
1504
|
+
this.db,
|
|
1505
|
+
`SELECT * FROM events
|
|
1506
|
+
WHERE access_count > 0
|
|
1507
|
+
ORDER BY access_count DESC, last_accessed_at DESC
|
|
1508
|
+
LIMIT ?`,
|
|
1509
|
+
[limit]
|
|
1510
|
+
);
|
|
1511
|
+
return rows.map((row) => this.rowToEvent(row));
|
|
1512
|
+
}
|
|
1513
|
+
/**
|
|
1514
|
+
* Fast keyword search using FTS5
|
|
1515
|
+
* Returns events matching the search query, ranked by relevance
|
|
1516
|
+
*/
|
|
1517
|
+
async keywordSearch(query, limit = 10) {
|
|
1518
|
+
await this.initialize();
|
|
1519
|
+
const searchTerms = query.replace(/['"(){}[\]^~*?:\\/-]/g, " ").split(/\s+/).filter((term) => term.length > 1).map((term) => `"${term}"*`).join(" OR ");
|
|
1520
|
+
if (!searchTerms) {
|
|
1521
|
+
return [];
|
|
1522
|
+
}
|
|
1523
|
+
try {
|
|
1524
|
+
const rows = sqliteAll(
|
|
1525
|
+
this.db,
|
|
1526
|
+
`SELECT e.*, fts.rank
|
|
1527
|
+
FROM events_fts fts
|
|
1528
|
+
JOIN events e ON e.id = fts.event_id
|
|
1529
|
+
WHERE events_fts MATCH ?
|
|
1530
|
+
ORDER BY fts.rank
|
|
1531
|
+
LIMIT ?`,
|
|
1532
|
+
[searchTerms, limit]
|
|
1533
|
+
);
|
|
1534
|
+
return rows.map((row) => ({
|
|
1535
|
+
event: this.rowToEvent(row),
|
|
1536
|
+
rank: row.rank
|
|
1537
|
+
}));
|
|
1538
|
+
} catch (error) {
|
|
1539
|
+
const likePattern = `%${query}%`;
|
|
1540
|
+
const rows = sqliteAll(
|
|
1541
|
+
this.db,
|
|
1542
|
+
`SELECT *, 0 as rank FROM events
|
|
1543
|
+
WHERE content LIKE ?
|
|
1544
|
+
ORDER BY timestamp DESC
|
|
1545
|
+
LIMIT ?`,
|
|
1546
|
+
[likePattern, limit]
|
|
1547
|
+
);
|
|
1548
|
+
return rows.map((row) => ({
|
|
1549
|
+
event: this.rowToEvent(row),
|
|
1550
|
+
rank: 0
|
|
1551
|
+
}));
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
/**
|
|
1555
|
+
* Rebuild FTS index from existing events
|
|
1556
|
+
* Call this once after upgrading to FTS5
|
|
1557
|
+
*/
|
|
1558
|
+
async rebuildFtsIndex() {
|
|
1559
|
+
await this.initialize();
|
|
1560
|
+
const countRow = sqliteGet(this.db, "SELECT COUNT(*) as count FROM events", []);
|
|
1561
|
+
const totalEvents = countRow?.count ?? 0;
|
|
1562
|
+
sqliteExec(this.db, `
|
|
1563
|
+
DELETE FROM events_fts;
|
|
1564
|
+
INSERT INTO events_fts(rowid, content, event_id)
|
|
1565
|
+
SELECT rowid, content, id FROM events;
|
|
1566
|
+
`);
|
|
1567
|
+
return totalEvents;
|
|
1568
|
+
}
|
|
1569
|
+
/**
|
|
1570
|
+
* Get database instance for direct access
|
|
1571
|
+
*/
|
|
1572
|
+
getDatabase() {
|
|
1573
|
+
return this.db;
|
|
1574
|
+
}
|
|
718
1575
|
/**
|
|
719
1576
|
* Close database connection
|
|
720
1577
|
*/
|
|
721
1578
|
async close() {
|
|
722
|
-
|
|
1579
|
+
sqliteClose(this.db);
|
|
723
1580
|
}
|
|
724
1581
|
/**
|
|
725
1582
|
* Convert database row to MemoryEvent
|
|
726
1583
|
*/
|
|
727
1584
|
rowToEvent(row) {
|
|
728
|
-
|
|
1585
|
+
const event = {
|
|
729
1586
|
id: row.id,
|
|
730
1587
|
eventType: row.event_type,
|
|
731
1588
|
sessionId: row.session_id,
|
|
732
|
-
timestamp:
|
|
1589
|
+
timestamp: toDateFromSQLite(row.timestamp),
|
|
733
1590
|
content: row.content,
|
|
734
1591
|
canonicalKey: row.canonical_key,
|
|
735
1592
|
dedupeKey: row.dedupe_key,
|
|
736
1593
|
metadata: row.metadata ? JSON.parse(row.metadata) : void 0
|
|
737
1594
|
};
|
|
1595
|
+
if (row.access_count !== void 0) {
|
|
1596
|
+
event.access_count = row.access_count;
|
|
1597
|
+
}
|
|
1598
|
+
if (row.last_accessed_at !== void 0) {
|
|
1599
|
+
event.last_accessed_at = row.last_accessed_at;
|
|
1600
|
+
}
|
|
1601
|
+
return event;
|
|
1602
|
+
}
|
|
1603
|
+
};
|
|
1604
|
+
|
|
1605
|
+
// src/core/sync-worker.ts
|
|
1606
|
+
var DEFAULT_CONFIG = {
|
|
1607
|
+
intervalMs: 3e4,
|
|
1608
|
+
batchSize: 500,
|
|
1609
|
+
maxRetries: 3,
|
|
1610
|
+
retryDelayMs: 5e3
|
|
1611
|
+
};
|
|
1612
|
+
var SyncWorker = class {
|
|
1613
|
+
constructor(sqliteStore, duckdbStore, config) {
|
|
1614
|
+
this.sqliteStore = sqliteStore;
|
|
1615
|
+
this.duckdbStore = duckdbStore;
|
|
1616
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
1617
|
+
}
|
|
1618
|
+
config;
|
|
1619
|
+
intervalHandle = null;
|
|
1620
|
+
running = false;
|
|
1621
|
+
stats = {
|
|
1622
|
+
lastSyncAt: null,
|
|
1623
|
+
eventsSynced: 0,
|
|
1624
|
+
sessionsSynced: 0,
|
|
1625
|
+
errors: 0,
|
|
1626
|
+
status: "idle"
|
|
1627
|
+
};
|
|
1628
|
+
/**
|
|
1629
|
+
* Start the sync worker
|
|
1630
|
+
*/
|
|
1631
|
+
start() {
|
|
1632
|
+
if (this.running)
|
|
1633
|
+
return;
|
|
1634
|
+
this.running = true;
|
|
1635
|
+
this.stats.status = "idle";
|
|
1636
|
+
this.syncNow().catch((err) => {
|
|
1637
|
+
console.error("[SyncWorker] Initial sync failed:", err);
|
|
1638
|
+
});
|
|
1639
|
+
this.intervalHandle = setInterval(() => {
|
|
1640
|
+
this.syncNow().catch((err) => {
|
|
1641
|
+
console.error("[SyncWorker] Periodic sync failed:", err);
|
|
1642
|
+
});
|
|
1643
|
+
}, this.config.intervalMs);
|
|
1644
|
+
}
|
|
1645
|
+
/**
|
|
1646
|
+
* Stop the sync worker
|
|
1647
|
+
*/
|
|
1648
|
+
stop() {
|
|
1649
|
+
this.running = false;
|
|
1650
|
+
this.stats.status = "stopped";
|
|
1651
|
+
if (this.intervalHandle) {
|
|
1652
|
+
clearInterval(this.intervalHandle);
|
|
1653
|
+
this.intervalHandle = null;
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
/**
|
|
1657
|
+
* Trigger immediate sync
|
|
1658
|
+
*/
|
|
1659
|
+
async syncNow() {
|
|
1660
|
+
if (this.stats.status === "syncing") {
|
|
1661
|
+
return;
|
|
1662
|
+
}
|
|
1663
|
+
this.stats.status = "syncing";
|
|
1664
|
+
try {
|
|
1665
|
+
await this.syncEvents();
|
|
1666
|
+
await this.syncSessions();
|
|
1667
|
+
this.stats.lastSyncAt = /* @__PURE__ */ new Date();
|
|
1668
|
+
this.stats.status = "idle";
|
|
1669
|
+
} catch (error) {
|
|
1670
|
+
this.stats.errors++;
|
|
1671
|
+
this.stats.status = "error";
|
|
1672
|
+
throw error;
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
/**
|
|
1676
|
+
* Sync events from SQLite to DuckDB
|
|
1677
|
+
*/
|
|
1678
|
+
async syncEvents() {
|
|
1679
|
+
const targetName = "duckdb_analytics";
|
|
1680
|
+
const position = await this.sqliteStore.getSyncPosition(targetName);
|
|
1681
|
+
const lastTimestamp = position.lastTimestamp || "1970-01-01T00:00:00.000Z";
|
|
1682
|
+
let hasMore = true;
|
|
1683
|
+
let totalSynced = 0;
|
|
1684
|
+
while (hasMore) {
|
|
1685
|
+
const events = await this.sqliteStore.getEventsSince(lastTimestamp, this.config.batchSize);
|
|
1686
|
+
if (events.length === 0) {
|
|
1687
|
+
hasMore = false;
|
|
1688
|
+
break;
|
|
1689
|
+
}
|
|
1690
|
+
await this.retryWithBackoff(async () => {
|
|
1691
|
+
for (const event of events) {
|
|
1692
|
+
await this.insertEventToDuckDB(event);
|
|
1693
|
+
}
|
|
1694
|
+
});
|
|
1695
|
+
totalSynced += events.length;
|
|
1696
|
+
const lastEvent = events[events.length - 1];
|
|
1697
|
+
await this.sqliteStore.updateSyncPosition(
|
|
1698
|
+
targetName,
|
|
1699
|
+
lastEvent.id,
|
|
1700
|
+
lastEvent.timestamp.toISOString()
|
|
1701
|
+
);
|
|
1702
|
+
hasMore = events.length === this.config.batchSize;
|
|
1703
|
+
}
|
|
1704
|
+
this.stats.eventsSynced += totalSynced;
|
|
1705
|
+
}
|
|
1706
|
+
/**
|
|
1707
|
+
* Sync sessions from SQLite to DuckDB
|
|
1708
|
+
*/
|
|
1709
|
+
async syncSessions() {
|
|
1710
|
+
const sessions = await this.sqliteStore.getAllSessions();
|
|
1711
|
+
for (const session of sessions) {
|
|
1712
|
+
await this.retryWithBackoff(async () => {
|
|
1713
|
+
await this.duckdbStore.upsertSession(session);
|
|
1714
|
+
});
|
|
1715
|
+
}
|
|
1716
|
+
this.stats.sessionsSynced = sessions.length;
|
|
1717
|
+
}
|
|
1718
|
+
/**
|
|
1719
|
+
* Insert a single event into DuckDB
|
|
1720
|
+
*/
|
|
1721
|
+
async insertEventToDuckDB(event) {
|
|
1722
|
+
await this.duckdbStore.append({
|
|
1723
|
+
eventType: event.eventType,
|
|
1724
|
+
sessionId: event.sessionId,
|
|
1725
|
+
timestamp: event.timestamp,
|
|
1726
|
+
content: event.content,
|
|
1727
|
+
metadata: event.metadata
|
|
1728
|
+
});
|
|
1729
|
+
}
|
|
1730
|
+
/**
|
|
1731
|
+
* Retry operation with exponential backoff
|
|
1732
|
+
*/
|
|
1733
|
+
async retryWithBackoff(fn) {
|
|
1734
|
+
let lastError = null;
|
|
1735
|
+
for (let attempt = 0; attempt < this.config.maxRetries; attempt++) {
|
|
1736
|
+
try {
|
|
1737
|
+
return await fn();
|
|
1738
|
+
} catch (error) {
|
|
1739
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
1740
|
+
if (attempt < this.config.maxRetries - 1) {
|
|
1741
|
+
const delay = this.config.retryDelayMs * Math.pow(2, attempt);
|
|
1742
|
+
await this.sleep(delay);
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
throw lastError;
|
|
1747
|
+
}
|
|
1748
|
+
/**
|
|
1749
|
+
* Sleep utility
|
|
1750
|
+
*/
|
|
1751
|
+
sleep(ms) {
|
|
1752
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
1753
|
+
}
|
|
1754
|
+
/**
|
|
1755
|
+
* Get sync statistics
|
|
1756
|
+
*/
|
|
1757
|
+
getStats() {
|
|
1758
|
+
return { ...this.stats };
|
|
1759
|
+
}
|
|
1760
|
+
/**
|
|
1761
|
+
* Check if worker is running
|
|
1762
|
+
*/
|
|
1763
|
+
isRunning() {
|
|
1764
|
+
return this.running;
|
|
738
1765
|
}
|
|
739
1766
|
};
|
|
740
1767
|
|
|
@@ -966,7 +1993,7 @@ function getDefaultEmbedder() {
|
|
|
966
1993
|
}
|
|
967
1994
|
|
|
968
1995
|
// src/core/vector-outbox.ts
|
|
969
|
-
var
|
|
1996
|
+
var DEFAULT_CONFIG2 = {
|
|
970
1997
|
embeddingVersion: "v1",
|
|
971
1998
|
maxRetries: 3,
|
|
972
1999
|
stuckThresholdMs: 5 * 60 * 1e3,
|
|
@@ -975,7 +2002,7 @@ var DEFAULT_CONFIG = {
|
|
|
975
2002
|
};
|
|
976
2003
|
|
|
977
2004
|
// src/core/vector-worker.ts
|
|
978
|
-
var
|
|
2005
|
+
var DEFAULT_CONFIG3 = {
|
|
979
2006
|
batchSize: 32,
|
|
980
2007
|
pollIntervalMs: 1e3,
|
|
981
2008
|
maxRetries: 3
|
|
@@ -986,12 +2013,13 @@ var VectorWorker = class {
|
|
|
986
2013
|
embedder;
|
|
987
2014
|
config;
|
|
988
2015
|
running = false;
|
|
2016
|
+
stopping = false;
|
|
989
2017
|
pollTimeout = null;
|
|
990
2018
|
constructor(eventStore, vectorStore, embedder, config = {}) {
|
|
991
2019
|
this.eventStore = eventStore;
|
|
992
2020
|
this.vectorStore = vectorStore;
|
|
993
2021
|
this.embedder = embedder;
|
|
994
|
-
this.config = { ...
|
|
2022
|
+
this.config = { ...DEFAULT_CONFIG3, ...config };
|
|
995
2023
|
}
|
|
996
2024
|
/**
|
|
997
2025
|
* Start the worker polling loop
|
|
@@ -1000,6 +2028,7 @@ var VectorWorker = class {
|
|
|
1000
2028
|
if (this.running)
|
|
1001
2029
|
return;
|
|
1002
2030
|
this.running = true;
|
|
2031
|
+
this.stopping = false;
|
|
1003
2032
|
this.poll();
|
|
1004
2033
|
}
|
|
1005
2034
|
/**
|
|
@@ -1007,6 +2036,7 @@ var VectorWorker = class {
|
|
|
1007
2036
|
*/
|
|
1008
2037
|
stop() {
|
|
1009
2038
|
this.running = false;
|
|
2039
|
+
this.stopping = true;
|
|
1010
2040
|
if (this.pollTimeout) {
|
|
1011
2041
|
clearTimeout(this.pollTimeout);
|
|
1012
2042
|
this.pollTimeout = null;
|
|
@@ -1056,9 +2086,15 @@ var VectorWorker = class {
|
|
|
1056
2086
|
}
|
|
1057
2087
|
return successful.length;
|
|
1058
2088
|
} catch (error) {
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
2089
|
+
if (!this.stopping) {
|
|
2090
|
+
try {
|
|
2091
|
+
const allIds = items.map((i) => i.id);
|
|
2092
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2093
|
+
await this.eventStore.failOutboxItems(allIds, errorMessage);
|
|
2094
|
+
} catch (failError) {
|
|
2095
|
+
console.warn("Could not mark outbox items as failed (database may be closed)");
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
1062
2098
|
throw error;
|
|
1063
2099
|
}
|
|
1064
2100
|
}
|
|
@@ -1066,14 +2102,18 @@ var VectorWorker = class {
|
|
|
1066
2102
|
* Poll for new items
|
|
1067
2103
|
*/
|
|
1068
2104
|
async poll() {
|
|
1069
|
-
if (!this.running)
|
|
2105
|
+
if (!this.running || this.stopping)
|
|
1070
2106
|
return;
|
|
1071
2107
|
try {
|
|
1072
2108
|
await this.processBatch();
|
|
1073
2109
|
} catch (error) {
|
|
1074
|
-
|
|
2110
|
+
if (!this.stopping) {
|
|
2111
|
+
console.error("Vector worker error:", error);
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
if (this.running && !this.stopping) {
|
|
2115
|
+
this.pollTimeout = setTimeout(() => this.poll(), this.config.pollIntervalMs);
|
|
1075
2116
|
}
|
|
1076
|
-
this.pollTimeout = setTimeout(() => this.poll(), this.config.pollIntervalMs);
|
|
1077
2117
|
}
|
|
1078
2118
|
/**
|
|
1079
2119
|
* Process all pending items (blocking)
|
|
@@ -1100,7 +2140,7 @@ function createVectorWorker(eventStore, vectorStore, embedder, config) {
|
|
|
1100
2140
|
}
|
|
1101
2141
|
|
|
1102
2142
|
// src/core/matcher.ts
|
|
1103
|
-
var
|
|
2143
|
+
var DEFAULT_CONFIG4 = {
|
|
1104
2144
|
weights: {
|
|
1105
2145
|
semanticSimilarity: 0.4,
|
|
1106
2146
|
ftsScore: 0.25,
|
|
@@ -1115,9 +2155,9 @@ var Matcher = class {
|
|
|
1115
2155
|
config;
|
|
1116
2156
|
constructor(config = {}) {
|
|
1117
2157
|
this.config = {
|
|
1118
|
-
...
|
|
2158
|
+
...DEFAULT_CONFIG4,
|
|
1119
2159
|
...config,
|
|
1120
|
-
weights: { ...
|
|
2160
|
+
weights: { ...DEFAULT_CONFIG4.weights, ...config.weights }
|
|
1121
2161
|
};
|
|
1122
2162
|
}
|
|
1123
2163
|
/**
|
|
@@ -1825,7 +2865,7 @@ function createSharedEventStore(dbPath) {
|
|
|
1825
2865
|
}
|
|
1826
2866
|
|
|
1827
2867
|
// src/core/shared-store.ts
|
|
1828
|
-
import { randomUUID as
|
|
2868
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
1829
2869
|
var SharedStore = class {
|
|
1830
2870
|
constructor(sharedEventStore) {
|
|
1831
2871
|
this.sharedEventStore = sharedEventStore;
|
|
@@ -1837,7 +2877,7 @@ var SharedStore = class {
|
|
|
1837
2877
|
* Promote a verified troubleshooting entry to shared storage
|
|
1838
2878
|
*/
|
|
1839
2879
|
async promoteEntry(input) {
|
|
1840
|
-
const entryId =
|
|
2880
|
+
const entryId = randomUUID3();
|
|
1841
2881
|
await dbRun(
|
|
1842
2882
|
this.db,
|
|
1843
2883
|
`INSERT INTO shared_troubleshooting (
|
|
@@ -2202,7 +3242,7 @@ function createSharedVectorStore(dbPath) {
|
|
|
2202
3242
|
}
|
|
2203
3243
|
|
|
2204
3244
|
// src/core/shared-promoter.ts
|
|
2205
|
-
import { randomUUID as
|
|
3245
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
2206
3246
|
var SharedPromoter = class {
|
|
2207
3247
|
constructor(sharedStore, sharedVectorStore, embedder, config) {
|
|
2208
3248
|
this.sharedStore = sharedStore;
|
|
@@ -2270,7 +3310,7 @@ var SharedPromoter = class {
|
|
|
2270
3310
|
const embeddingContent = this.createEmbeddingContent(input);
|
|
2271
3311
|
const embedding = await this.embedder.embed(embeddingContent);
|
|
2272
3312
|
await this.sharedVectorStore.upsert({
|
|
2273
|
-
id:
|
|
3313
|
+
id: randomUUID4(),
|
|
2274
3314
|
entryId,
|
|
2275
3315
|
entryType: "troubleshooting",
|
|
2276
3316
|
content: embeddingContent,
|
|
@@ -2410,7 +3450,7 @@ function createToolObservationEmbedding(toolName, metadata, success) {
|
|
|
2410
3450
|
}
|
|
2411
3451
|
|
|
2412
3452
|
// src/core/working-set-store.ts
|
|
2413
|
-
import { randomUUID as
|
|
3453
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
2414
3454
|
var WorkingSetStore = class {
|
|
2415
3455
|
constructor(eventStore, config) {
|
|
2416
3456
|
this.eventStore = eventStore;
|
|
@@ -2431,7 +3471,7 @@ var WorkingSetStore = class {
|
|
|
2431
3471
|
`INSERT OR REPLACE INTO working_set (id, event_id, added_at, relevance_score, topics, expires_at)
|
|
2432
3472
|
VALUES (?, ?, CURRENT_TIMESTAMP, ?, ?, ?)`,
|
|
2433
3473
|
[
|
|
2434
|
-
|
|
3474
|
+
randomUUID5(),
|
|
2435
3475
|
eventId,
|
|
2436
3476
|
relevanceScore,
|
|
2437
3477
|
JSON.stringify(topics || []),
|
|
@@ -2615,7 +3655,7 @@ function createWorkingSetStore(eventStore, config) {
|
|
|
2615
3655
|
}
|
|
2616
3656
|
|
|
2617
3657
|
// src/core/consolidated-store.ts
|
|
2618
|
-
import { randomUUID as
|
|
3658
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
2619
3659
|
var ConsolidatedStore = class {
|
|
2620
3660
|
constructor(eventStore) {
|
|
2621
3661
|
this.eventStore = eventStore;
|
|
@@ -2627,7 +3667,7 @@ var ConsolidatedStore = class {
|
|
|
2627
3667
|
* Create a new consolidated memory
|
|
2628
3668
|
*/
|
|
2629
3669
|
async create(input) {
|
|
2630
|
-
const memoryId =
|
|
3670
|
+
const memoryId = randomUUID6();
|
|
2631
3671
|
await dbRun(
|
|
2632
3672
|
this.db,
|
|
2633
3673
|
`INSERT INTO consolidated_memories
|
|
@@ -3154,7 +4194,7 @@ function createConsolidationWorker(workingSetStore, consolidatedStore, config) {
|
|
|
3154
4194
|
}
|
|
3155
4195
|
|
|
3156
4196
|
// src/core/continuity-manager.ts
|
|
3157
|
-
import { randomUUID as
|
|
4197
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
3158
4198
|
var ContinuityManager = class {
|
|
3159
4199
|
constructor(eventStore, config) {
|
|
3160
4200
|
this.eventStore = eventStore;
|
|
@@ -3308,7 +4348,7 @@ var ContinuityManager = class {
|
|
|
3308
4348
|
`INSERT INTO continuity_log
|
|
3309
4349
|
(log_id, from_context_id, to_context_id, continuity_score, transition_type, created_at)
|
|
3310
4350
|
VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,
|
|
3311
|
-
[
|
|
4351
|
+
[randomUUID7(), previous.id, current.id, score, type]
|
|
3312
4352
|
);
|
|
3313
4353
|
}
|
|
3314
4354
|
/**
|
|
@@ -3417,7 +4457,7 @@ function createContinuityManager(eventStore, config) {
|
|
|
3417
4457
|
}
|
|
3418
4458
|
|
|
3419
4459
|
// src/core/graduation-worker.ts
|
|
3420
|
-
var
|
|
4460
|
+
var DEFAULT_CONFIG5 = {
|
|
3421
4461
|
evaluationIntervalMs: 3e5,
|
|
3422
4462
|
// 5 minutes
|
|
3423
4463
|
batchSize: 50,
|
|
@@ -3425,7 +4465,7 @@ var DEFAULT_CONFIG4 = {
|
|
|
3425
4465
|
// 1 hour cooldown between evaluations
|
|
3426
4466
|
};
|
|
3427
4467
|
var GraduationWorker = class {
|
|
3428
|
-
constructor(eventStore, graduation, config =
|
|
4468
|
+
constructor(eventStore, graduation, config = DEFAULT_CONFIG5) {
|
|
3429
4469
|
this.eventStore = eventStore;
|
|
3430
4470
|
this.graduation = graduation;
|
|
3431
4471
|
this.config = config;
|
|
@@ -3533,7 +4573,7 @@ function createGraduationWorker(eventStore, graduation, config) {
|
|
|
3533
4573
|
return new GraduationWorker(
|
|
3534
4574
|
eventStore,
|
|
3535
4575
|
graduation,
|
|
3536
|
-
{ ...
|
|
4576
|
+
{ ...DEFAULT_CONFIG5, ...config }
|
|
3537
4577
|
);
|
|
3538
4578
|
}
|
|
3539
4579
|
|
|
@@ -3557,7 +4597,11 @@ function getProjectStoragePath(projectPath) {
|
|
|
3557
4597
|
var REGISTRY_PATH = path.join(os.homedir(), ".claude-code", "memory", "session-registry.json");
|
|
3558
4598
|
var SHARED_STORAGE_PATH = path.join(os.homedir(), ".claude-code", "memory", "shared");
|
|
3559
4599
|
var MemoryService = class {
|
|
3560
|
-
|
|
4600
|
+
// Primary store: SQLite (WAL mode) - for hooks, always available
|
|
4601
|
+
sqliteStore;
|
|
4602
|
+
// Analytics store: DuckDB - for server reads (optional, synced from SQLite)
|
|
4603
|
+
analyticsStore;
|
|
4604
|
+
syncWorker = null;
|
|
3561
4605
|
vectorStore;
|
|
3562
4606
|
embedder;
|
|
3563
4607
|
matcher;
|
|
@@ -3580,25 +4624,49 @@ var MemoryService = class {
|
|
|
3580
4624
|
sharedStoreConfig = null;
|
|
3581
4625
|
projectHash = null;
|
|
3582
4626
|
readOnly;
|
|
4627
|
+
lightweightMode;
|
|
3583
4628
|
constructor(config) {
|
|
3584
4629
|
const storagePath = this.expandPath(config.storagePath);
|
|
3585
4630
|
this.readOnly = config.readOnly ?? false;
|
|
4631
|
+
this.lightweightMode = config.lightweightMode ?? false;
|
|
3586
4632
|
if (!this.readOnly && !fs.existsSync(storagePath)) {
|
|
3587
4633
|
fs.mkdirSync(storagePath, { recursive: true });
|
|
3588
4634
|
}
|
|
3589
4635
|
this.projectHash = config.projectHash || null;
|
|
3590
4636
|
this.sharedStoreConfig = config.sharedStoreConfig ?? { enabled: true };
|
|
3591
|
-
this.
|
|
4637
|
+
this.sqliteStore = new SQLiteEventStore(
|
|
4638
|
+
path.join(storagePath, "events.sqlite"),
|
|
4639
|
+
{ readonly: this.readOnly }
|
|
4640
|
+
);
|
|
4641
|
+
const analyticsEnabled = config.analyticsEnabled ?? this.readOnly;
|
|
4642
|
+
if (!analyticsEnabled) {
|
|
4643
|
+
this.analyticsStore = null;
|
|
4644
|
+
} else if (this.readOnly) {
|
|
4645
|
+
try {
|
|
4646
|
+
this.analyticsStore = new EventStore(
|
|
4647
|
+
path.join(storagePath, "analytics.duckdb"),
|
|
4648
|
+
{ readOnly: true }
|
|
4649
|
+
);
|
|
4650
|
+
} catch {
|
|
4651
|
+
this.analyticsStore = null;
|
|
4652
|
+
}
|
|
4653
|
+
} else {
|
|
4654
|
+
this.analyticsStore = new EventStore(
|
|
4655
|
+
path.join(storagePath, "analytics.duckdb"),
|
|
4656
|
+
{ readOnly: false }
|
|
4657
|
+
);
|
|
4658
|
+
}
|
|
3592
4659
|
this.vectorStore = new VectorStore(path.join(storagePath, "vectors"));
|
|
3593
4660
|
this.embedder = config.embeddingModel ? new Embedder(config.embeddingModel) : getDefaultEmbedder();
|
|
3594
4661
|
this.matcher = getDefaultMatcher();
|
|
3595
4662
|
this.retriever = createRetriever(
|
|
3596
|
-
this.
|
|
4663
|
+
this.sqliteStore,
|
|
4664
|
+
// Interface compatible
|
|
3597
4665
|
this.vectorStore,
|
|
3598
4666
|
this.embedder,
|
|
3599
4667
|
this.matcher
|
|
3600
4668
|
);
|
|
3601
|
-
this.graduation = createGraduationPipeline(this.
|
|
4669
|
+
this.graduation = createGraduationPipeline(this.sqliteStore);
|
|
3602
4670
|
}
|
|
3603
4671
|
/**
|
|
3604
4672
|
* Initialize all components
|
|
@@ -3606,23 +4674,42 @@ var MemoryService = class {
|
|
|
3606
4674
|
async initialize() {
|
|
3607
4675
|
if (this.initialized)
|
|
3608
4676
|
return;
|
|
3609
|
-
await this.
|
|
4677
|
+
await this.sqliteStore.initialize();
|
|
4678
|
+
if (this.lightweightMode) {
|
|
4679
|
+
this.initialized = true;
|
|
4680
|
+
return;
|
|
4681
|
+
}
|
|
4682
|
+
if (this.analyticsStore) {
|
|
4683
|
+
try {
|
|
4684
|
+
await this.analyticsStore.initialize();
|
|
4685
|
+
} catch (error) {
|
|
4686
|
+
console.warn("[MemoryService] Analytics store (DuckDB) initialization failed, using SQLite for reads:", error);
|
|
4687
|
+
}
|
|
4688
|
+
}
|
|
3610
4689
|
await this.vectorStore.initialize();
|
|
3611
4690
|
await this.embedder.initialize();
|
|
3612
4691
|
if (!this.readOnly) {
|
|
3613
4692
|
this.vectorWorker = createVectorWorker(
|
|
3614
|
-
this.
|
|
4693
|
+
this.sqliteStore,
|
|
3615
4694
|
this.vectorStore,
|
|
3616
4695
|
this.embedder
|
|
3617
4696
|
);
|
|
3618
4697
|
this.vectorWorker.start();
|
|
3619
4698
|
this.retriever.setGraduationPipeline(this.graduation);
|
|
3620
4699
|
this.graduationWorker = createGraduationWorker(
|
|
3621
|
-
this.
|
|
4700
|
+
this.sqliteStore,
|
|
3622
4701
|
this.graduation
|
|
3623
4702
|
);
|
|
3624
4703
|
this.graduationWorker.start();
|
|
3625
|
-
|
|
4704
|
+
if (this.analyticsStore) {
|
|
4705
|
+
this.syncWorker = new SyncWorker(
|
|
4706
|
+
this.sqliteStore,
|
|
4707
|
+
this.analyticsStore,
|
|
4708
|
+
{ intervalMs: 3e4, batchSize: 500 }
|
|
4709
|
+
);
|
|
4710
|
+
this.syncWorker.start();
|
|
4711
|
+
}
|
|
4712
|
+
const savedMode = await this.sqliteStore.getEndlessConfig("mode");
|
|
3626
4713
|
if (savedMode === "endless") {
|
|
3627
4714
|
this.endlessMode = "endless";
|
|
3628
4715
|
await this.initializeEndlessMode();
|
|
@@ -3663,7 +4750,7 @@ var MemoryService = class {
|
|
|
3663
4750
|
*/
|
|
3664
4751
|
async startSession(sessionId, projectPath) {
|
|
3665
4752
|
await this.initialize();
|
|
3666
|
-
await this.
|
|
4753
|
+
await this.sqliteStore.upsertSession({
|
|
3667
4754
|
id: sessionId,
|
|
3668
4755
|
startedAt: /* @__PURE__ */ new Date(),
|
|
3669
4756
|
projectPath
|
|
@@ -3674,7 +4761,7 @@ var MemoryService = class {
|
|
|
3674
4761
|
*/
|
|
3675
4762
|
async endSession(sessionId, summary) {
|
|
3676
4763
|
await this.initialize();
|
|
3677
|
-
await this.
|
|
4764
|
+
await this.sqliteStore.upsertSession({
|
|
3678
4765
|
id: sessionId,
|
|
3679
4766
|
endedAt: /* @__PURE__ */ new Date(),
|
|
3680
4767
|
summary
|
|
@@ -3685,7 +4772,7 @@ var MemoryService = class {
|
|
|
3685
4772
|
*/
|
|
3686
4773
|
async storeUserPrompt(sessionId, content, metadata) {
|
|
3687
4774
|
await this.initialize();
|
|
3688
|
-
const result = await this.
|
|
4775
|
+
const result = await this.sqliteStore.append({
|
|
3689
4776
|
eventType: "user_prompt",
|
|
3690
4777
|
sessionId,
|
|
3691
4778
|
timestamp: /* @__PURE__ */ new Date(),
|
|
@@ -3693,7 +4780,7 @@ var MemoryService = class {
|
|
|
3693
4780
|
metadata
|
|
3694
4781
|
});
|
|
3695
4782
|
if (result.success && !result.isDuplicate) {
|
|
3696
|
-
await this.
|
|
4783
|
+
await this.sqliteStore.enqueueForEmbedding(result.eventId, content);
|
|
3697
4784
|
}
|
|
3698
4785
|
return result;
|
|
3699
4786
|
}
|
|
@@ -3702,7 +4789,7 @@ var MemoryService = class {
|
|
|
3702
4789
|
*/
|
|
3703
4790
|
async storeAgentResponse(sessionId, content, metadata) {
|
|
3704
4791
|
await this.initialize();
|
|
3705
|
-
const result = await this.
|
|
4792
|
+
const result = await this.sqliteStore.append({
|
|
3706
4793
|
eventType: "agent_response",
|
|
3707
4794
|
sessionId,
|
|
3708
4795
|
timestamp: /* @__PURE__ */ new Date(),
|
|
@@ -3710,7 +4797,7 @@ var MemoryService = class {
|
|
|
3710
4797
|
metadata
|
|
3711
4798
|
});
|
|
3712
4799
|
if (result.success && !result.isDuplicate) {
|
|
3713
|
-
await this.
|
|
4800
|
+
await this.sqliteStore.enqueueForEmbedding(result.eventId, content);
|
|
3714
4801
|
}
|
|
3715
4802
|
return result;
|
|
3716
4803
|
}
|
|
@@ -3719,14 +4806,14 @@ var MemoryService = class {
|
|
|
3719
4806
|
*/
|
|
3720
4807
|
async storeSessionSummary(sessionId, summary) {
|
|
3721
4808
|
await this.initialize();
|
|
3722
|
-
const result = await this.
|
|
4809
|
+
const result = await this.sqliteStore.append({
|
|
3723
4810
|
eventType: "session_summary",
|
|
3724
4811
|
sessionId,
|
|
3725
4812
|
timestamp: /* @__PURE__ */ new Date(),
|
|
3726
4813
|
content: summary
|
|
3727
4814
|
});
|
|
3728
4815
|
if (result.success && !result.isDuplicate) {
|
|
3729
|
-
await this.
|
|
4816
|
+
await this.sqliteStore.enqueueForEmbedding(result.eventId, summary);
|
|
3730
4817
|
}
|
|
3731
4818
|
return result;
|
|
3732
4819
|
}
|
|
@@ -3736,7 +4823,7 @@ var MemoryService = class {
|
|
|
3736
4823
|
async storeToolObservation(sessionId, payload) {
|
|
3737
4824
|
await this.initialize();
|
|
3738
4825
|
const content = JSON.stringify(payload);
|
|
3739
|
-
const result = await this.
|
|
4826
|
+
const result = await this.sqliteStore.append({
|
|
3740
4827
|
eventType: "tool_observation",
|
|
3741
4828
|
sessionId,
|
|
3742
4829
|
timestamp: /* @__PURE__ */ new Date(),
|
|
@@ -3752,7 +4839,7 @@ var MemoryService = class {
|
|
|
3752
4839
|
payload.metadata || {},
|
|
3753
4840
|
payload.success
|
|
3754
4841
|
);
|
|
3755
|
-
await this.
|
|
4842
|
+
await this.sqliteStore.enqueueForEmbedding(result.eventId, embeddingContent);
|
|
3756
4843
|
}
|
|
3757
4844
|
return result;
|
|
3758
4845
|
}
|
|
@@ -3761,9 +4848,6 @@ var MemoryService = class {
|
|
|
3761
4848
|
*/
|
|
3762
4849
|
async retrieveMemories(query, options) {
|
|
3763
4850
|
await this.initialize();
|
|
3764
|
-
if (this.vectorWorker) {
|
|
3765
|
-
await this.vectorWorker.processAll();
|
|
3766
|
-
}
|
|
3767
4851
|
if (options?.includeShared && this.sharedStore) {
|
|
3768
4852
|
return this.retriever.retrieveUnified(query, {
|
|
3769
4853
|
...options,
|
|
@@ -3773,26 +4857,49 @@ var MemoryService = class {
|
|
|
3773
4857
|
}
|
|
3774
4858
|
return this.retriever.retrieve(query, options);
|
|
3775
4859
|
}
|
|
4860
|
+
/**
|
|
4861
|
+
* Fast keyword search using SQLite FTS5
|
|
4862
|
+
* Much faster than vector search - no embedding model needed
|
|
4863
|
+
*/
|
|
4864
|
+
async keywordSearch(query, options) {
|
|
4865
|
+
await this.initialize();
|
|
4866
|
+
const results = await this.sqliteStore.keywordSearch(query, options?.topK ?? 10);
|
|
4867
|
+
const maxRank = Math.min(...results.map((r) => r.rank), -1e-3);
|
|
4868
|
+
const minRank = Math.max(...results.map((r) => r.rank), -1e3);
|
|
4869
|
+
const rankRange = maxRank - minRank || 1;
|
|
4870
|
+
return results.map((r) => ({
|
|
4871
|
+
event: r.event,
|
|
4872
|
+
score: 1 - (r.rank - minRank) / rankRange
|
|
4873
|
+
// Normalize to 0-1
|
|
4874
|
+
})).filter((r) => !options?.minScore || r.score >= options.minScore);
|
|
4875
|
+
}
|
|
4876
|
+
/**
|
|
4877
|
+
* Rebuild FTS index (call after database upgrade)
|
|
4878
|
+
*/
|
|
4879
|
+
async rebuildFtsIndex() {
|
|
4880
|
+
await this.initialize();
|
|
4881
|
+
return this.sqliteStore.rebuildFtsIndex();
|
|
4882
|
+
}
|
|
3776
4883
|
/**
|
|
3777
4884
|
* Get session history
|
|
3778
4885
|
*/
|
|
3779
4886
|
async getSessionHistory(sessionId) {
|
|
3780
4887
|
await this.initialize();
|
|
3781
|
-
return this.
|
|
4888
|
+
return this.sqliteStore.getSessionEvents(sessionId);
|
|
3782
4889
|
}
|
|
3783
4890
|
/**
|
|
3784
4891
|
* Get recent events
|
|
3785
4892
|
*/
|
|
3786
4893
|
async getRecentEvents(limit = 100) {
|
|
3787
4894
|
await this.initialize();
|
|
3788
|
-
return this.
|
|
4895
|
+
return this.sqliteStore.getRecentEvents(limit);
|
|
3789
4896
|
}
|
|
3790
4897
|
/**
|
|
3791
4898
|
* Get memory statistics
|
|
3792
4899
|
*/
|
|
3793
4900
|
async getStats() {
|
|
3794
4901
|
await this.initialize();
|
|
3795
|
-
const recentEvents = await this.
|
|
4902
|
+
const recentEvents = await this.sqliteStore.getRecentEvents(1e4);
|
|
3796
4903
|
const vectorCount = await this.vectorStore.count();
|
|
3797
4904
|
const levelStats = await this.graduation.getStats();
|
|
3798
4905
|
return {
|
|
@@ -3815,14 +4922,14 @@ var MemoryService = class {
|
|
|
3815
4922
|
*/
|
|
3816
4923
|
async getEventsByLevel(level, options) {
|
|
3817
4924
|
await this.initialize();
|
|
3818
|
-
return this.
|
|
4925
|
+
return this.sqliteStore.getEventsByLevel(level, options);
|
|
3819
4926
|
}
|
|
3820
4927
|
/**
|
|
3821
4928
|
* Get memory level for a specific event
|
|
3822
4929
|
*/
|
|
3823
4930
|
async getEventLevel(eventId) {
|
|
3824
4931
|
await this.initialize();
|
|
3825
|
-
return this.
|
|
4932
|
+
return this.sqliteStore.getEventLevel(eventId);
|
|
3826
4933
|
}
|
|
3827
4934
|
/**
|
|
3828
4935
|
* Format retrieval results as context for Claude
|
|
@@ -3916,21 +5023,21 @@ var MemoryService = class {
|
|
|
3916
5023
|
*/
|
|
3917
5024
|
async initializeEndlessMode() {
|
|
3918
5025
|
const config = await this.getEndlessConfig();
|
|
3919
|
-
this.workingSetStore = createWorkingSetStore(this.
|
|
3920
|
-
this.consolidatedStore = createConsolidatedStore(this.
|
|
5026
|
+
this.workingSetStore = createWorkingSetStore(this.sqliteStore, config);
|
|
5027
|
+
this.consolidatedStore = createConsolidatedStore(this.sqliteStore);
|
|
3921
5028
|
this.consolidationWorker = createConsolidationWorker(
|
|
3922
5029
|
this.workingSetStore,
|
|
3923
5030
|
this.consolidatedStore,
|
|
3924
5031
|
config
|
|
3925
5032
|
);
|
|
3926
|
-
this.continuityManager = createContinuityManager(this.
|
|
5033
|
+
this.continuityManager = createContinuityManager(this.sqliteStore, config);
|
|
3927
5034
|
this.consolidationWorker.start();
|
|
3928
5035
|
}
|
|
3929
5036
|
/**
|
|
3930
5037
|
* Get Endless Mode configuration
|
|
3931
5038
|
*/
|
|
3932
5039
|
async getEndlessConfig() {
|
|
3933
|
-
const savedConfig = await this.
|
|
5040
|
+
const savedConfig = await this.sqliteStore.getEndlessConfig("config");
|
|
3934
5041
|
return savedConfig || this.getDefaultEndlessConfig();
|
|
3935
5042
|
}
|
|
3936
5043
|
/**
|
|
@@ -3939,7 +5046,7 @@ var MemoryService = class {
|
|
|
3939
5046
|
async setEndlessConfig(config) {
|
|
3940
5047
|
const current = await this.getEndlessConfig();
|
|
3941
5048
|
const merged = { ...current, ...config };
|
|
3942
|
-
await this.
|
|
5049
|
+
await this.sqliteStore.setEndlessConfig("config", merged);
|
|
3943
5050
|
}
|
|
3944
5051
|
/**
|
|
3945
5052
|
* Set memory mode (session or endless)
|
|
@@ -3949,7 +5056,7 @@ var MemoryService = class {
|
|
|
3949
5056
|
if (mode === this.endlessMode)
|
|
3950
5057
|
return;
|
|
3951
5058
|
this.endlessMode = mode;
|
|
3952
|
-
await this.
|
|
5059
|
+
await this.sqliteStore.setEndlessConfig("mode", mode);
|
|
3953
5060
|
if (mode === "endless") {
|
|
3954
5061
|
await this.initializeEndlessMode();
|
|
3955
5062
|
} else {
|
|
@@ -4007,12 +5114,49 @@ var MemoryService = class {
|
|
|
4007
5114
|
return this.consolidatedStore.getAll({ limit });
|
|
4008
5115
|
}
|
|
4009
5116
|
/**
|
|
4010
|
-
*
|
|
5117
|
+
* Increment access count for memories that were used in prompts
|
|
5118
|
+
*/
|
|
5119
|
+
async incrementMemoryAccess(eventIds) {
|
|
5120
|
+
if (eventIds.length === 0)
|
|
5121
|
+
return;
|
|
5122
|
+
if (this.sqliteStore) {
|
|
5123
|
+
await this.sqliteStore.incrementAccessCount(eventIds);
|
|
5124
|
+
} else if (this.eventStore) {
|
|
5125
|
+
await this.eventStore.incrementAccessCount(eventIds);
|
|
5126
|
+
}
|
|
5127
|
+
}
|
|
5128
|
+
/**
|
|
5129
|
+
* Get most accessed memories from events
|
|
4011
5130
|
*/
|
|
4012
5131
|
async getMostAccessedMemories(limit = 10) {
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
5132
|
+
console.log("[getMostAccessedMemories] sqliteStore available:", !!this.sqliteStore);
|
|
5133
|
+
if (this.sqliteStore) {
|
|
5134
|
+
const events = await this.sqliteStore.getMostAccessed(limit);
|
|
5135
|
+
console.log("[getMostAccessedMemories] Got events from SQLite:", events.length);
|
|
5136
|
+
return events.map((event) => ({
|
|
5137
|
+
memoryId: event.id,
|
|
5138
|
+
summary: event.content.substring(0, 200) + (event.content.length > 200 ? "..." : ""),
|
|
5139
|
+
topics: [],
|
|
5140
|
+
// Could extract topics from content if needed
|
|
5141
|
+
accessCount: event.access_count || 0,
|
|
5142
|
+
lastAccessed: event.last_accessed_at || null,
|
|
5143
|
+
confidence: 1,
|
|
5144
|
+
createdAt: event.timestamp
|
|
5145
|
+
}));
|
|
5146
|
+
}
|
|
5147
|
+
if (this.consolidatedStore) {
|
|
5148
|
+
const consolidated = await this.consolidatedStore.getMostAccessed(limit);
|
|
5149
|
+
return consolidated.map((m) => ({
|
|
5150
|
+
memoryId: m.memoryId,
|
|
5151
|
+
summary: m.summary,
|
|
5152
|
+
topics: m.topics,
|
|
5153
|
+
accessCount: m.accessCount,
|
|
5154
|
+
lastAccessed: m.accessedAt,
|
|
5155
|
+
confidence: m.confidence,
|
|
5156
|
+
createdAt: m.createdAt
|
|
5157
|
+
}));
|
|
5158
|
+
}
|
|
5159
|
+
return [];
|
|
4016
5160
|
}
|
|
4017
5161
|
/**
|
|
4018
5162
|
* Mark a consolidated memory as accessed
|
|
@@ -4137,10 +5281,16 @@ var MemoryService = class {
|
|
|
4137
5281
|
if (this.vectorWorker) {
|
|
4138
5282
|
this.vectorWorker.stop();
|
|
4139
5283
|
}
|
|
5284
|
+
if (this.syncWorker) {
|
|
5285
|
+
this.syncWorker.stop();
|
|
5286
|
+
}
|
|
4140
5287
|
if (this.sharedEventStore) {
|
|
4141
5288
|
await this.sharedEventStore.close();
|
|
4142
5289
|
}
|
|
4143
|
-
await this.
|
|
5290
|
+
await this.sqliteStore.close();
|
|
5291
|
+
if (this.analyticsStore) {
|
|
5292
|
+
await this.analyticsStore.close();
|
|
5293
|
+
}
|
|
4144
5294
|
}
|
|
4145
5295
|
/**
|
|
4146
5296
|
* Expand ~ to home directory
|
|
@@ -4156,7 +5306,11 @@ var serviceCache = /* @__PURE__ */ new Map();
|
|
|
4156
5306
|
function getReadOnlyMemoryService() {
|
|
4157
5307
|
return new MemoryService({
|
|
4158
5308
|
storagePath: "~/.claude-code/memory",
|
|
4159
|
-
readOnly: true
|
|
5309
|
+
readOnly: true,
|
|
5310
|
+
analyticsEnabled: false,
|
|
5311
|
+
// Use SQLite for reads (WAL supports concurrent readers)
|
|
5312
|
+
sharedStoreConfig: { enabled: false }
|
|
5313
|
+
// Skip shared store for now
|
|
4160
5314
|
});
|
|
4161
5315
|
}
|
|
4162
5316
|
function getMemoryServiceForProject(projectPath, sharedStoreConfig) {
|
|
@@ -4166,7 +5320,10 @@ function getMemoryServiceForProject(projectPath, sharedStoreConfig) {
|
|
|
4166
5320
|
serviceCache.set(hash, new MemoryService({
|
|
4167
5321
|
storagePath,
|
|
4168
5322
|
projectHash: hash,
|
|
4169
|
-
|
|
5323
|
+
// Override shared store config - hooks don't need DuckDB
|
|
5324
|
+
sharedStoreConfig: sharedStoreConfig ?? { enabled: false },
|
|
5325
|
+
analyticsEnabled: false
|
|
5326
|
+
// Hooks don't need DuckDB
|
|
4170
5327
|
}));
|
|
4171
5328
|
}
|
|
4172
5329
|
return serviceCache.get(hash);
|
|
@@ -4462,6 +5619,7 @@ statsRouter.get("/levels/:level", async (c) => {
|
|
|
4462
5619
|
const { level } = c.req.param();
|
|
4463
5620
|
const limit = parseInt(c.req.query("limit") || "20", 10);
|
|
4464
5621
|
const offset = parseInt(c.req.query("offset") || "0", 10);
|
|
5622
|
+
const sort = c.req.query("sort") || "recent";
|
|
4465
5623
|
const validLevels = ["L0", "L1", "L2", "L3", "L4"];
|
|
4466
5624
|
if (!validLevels.includes(level)) {
|
|
4467
5625
|
return c.json({ error: `Invalid level. Must be one of: ${validLevels.join(", ")}` }, 400);
|
|
@@ -4469,9 +5627,27 @@ statsRouter.get("/levels/:level", async (c) => {
|
|
|
4469
5627
|
const memoryService = getReadOnlyMemoryService();
|
|
4470
5628
|
try {
|
|
4471
5629
|
await memoryService.initialize();
|
|
4472
|
-
|
|
5630
|
+
let events = await memoryService.getEventsByLevel(level, { limit: limit * 2, offset });
|
|
4473
5631
|
const stats = await memoryService.getStats();
|
|
4474
5632
|
const levelStat = stats.levelStats.find((s) => s.level === level);
|
|
5633
|
+
if (sort === "accessed") {
|
|
5634
|
+
const sqliteStore = memoryService.sqliteEventStore;
|
|
5635
|
+
if (sqliteStore) {
|
|
5636
|
+
const eventIds = events.map((e) => e.id);
|
|
5637
|
+
const accessedEvents = await sqliteStore.getMostAccessed(1e3);
|
|
5638
|
+
const accessMap = new Map(accessedEvents.map((e) => [e.id, e.access_count || 0]));
|
|
5639
|
+
events = events.map((e) => ({
|
|
5640
|
+
...e,
|
|
5641
|
+
accessCount: accessMap.get(e.id) || 0
|
|
5642
|
+
}));
|
|
5643
|
+
events.sort((a, b) => b.accessCount - a.accessCount);
|
|
5644
|
+
}
|
|
5645
|
+
} else if (sort === "oldest") {
|
|
5646
|
+
events.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
|
5647
|
+
} else {
|
|
5648
|
+
events.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
|
|
5649
|
+
}
|
|
5650
|
+
events = events.slice(0, limit);
|
|
4475
5651
|
return c.json({
|
|
4476
5652
|
level,
|
|
4477
5653
|
events: events.map((e) => ({
|
|
@@ -4480,7 +5656,8 @@ statsRouter.get("/levels/:level", async (c) => {
|
|
|
4480
5656
|
sessionId: e.sessionId,
|
|
4481
5657
|
timestamp: e.timestamp.toISOString(),
|
|
4482
5658
|
content: e.content.slice(0, 500) + (e.content.length > 500 ? "..." : ""),
|
|
4483
|
-
metadata: e.metadata
|
|
5659
|
+
metadata: e.metadata,
|
|
5660
|
+
accessCount: e.accessCount || 0
|
|
4484
5661
|
})),
|
|
4485
5662
|
total: levelStat?.count || 0,
|
|
4486
5663
|
limit,
|
|
@@ -4538,24 +5715,26 @@ statsRouter.get("/", async (c) => {
|
|
|
4538
5715
|
});
|
|
4539
5716
|
statsRouter.get("/most-accessed", async (c) => {
|
|
4540
5717
|
const limit = parseInt(c.req.query("limit") || "10", 10);
|
|
4541
|
-
const
|
|
4542
|
-
const memoryService = getMemoryServiceForProject(projectPath);
|
|
5718
|
+
const memoryService = getReadOnlyMemoryService();
|
|
4543
5719
|
try {
|
|
4544
5720
|
await memoryService.initialize();
|
|
5721
|
+
console.log("[most-accessed] Fetching most accessed memories, limit:", limit);
|
|
4545
5722
|
const memories = await memoryService.getMostAccessedMemories(limit);
|
|
5723
|
+
console.log("[most-accessed] Got memories:", memories.length);
|
|
4546
5724
|
return c.json({
|
|
4547
5725
|
memories: memories.map((m) => ({
|
|
4548
5726
|
memoryId: m.memoryId,
|
|
4549
5727
|
summary: m.summary,
|
|
4550
5728
|
topics: m.topics,
|
|
4551
5729
|
accessCount: m.accessCount,
|
|
4552
|
-
lastAccessed: m.
|
|
5730
|
+
lastAccessed: m.lastAccessed || null,
|
|
4553
5731
|
confidence: m.confidence,
|
|
4554
|
-
createdAt: m.createdAt.toISOString()
|
|
5732
|
+
createdAt: m.createdAt instanceof Date ? m.createdAt.toISOString() : m.createdAt
|
|
4555
5733
|
})),
|
|
4556
5734
|
total: memories.length
|
|
4557
5735
|
});
|
|
4558
5736
|
} catch (error) {
|
|
5737
|
+
console.error("[most-accessed] Error:", error);
|
|
4559
5738
|
return c.json({
|
|
4560
5739
|
memories: [],
|
|
4561
5740
|
total: 0,
|