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