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