claude-memory-layer 1.0.8 → 1.0.9
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/HANDOFF.md +92 -0
- package/dist/cli/index.js +1150 -71
- package/dist/cli/index.js.map +4 -4
- package/dist/core/index.js +1033 -47
- package/dist/core/index.js.map +4 -4
- package/dist/hooks/post-tool-use.js +5589 -0
- package/dist/hooks/post-tool-use.js.map +7 -0
- package/dist/hooks/session-end.js +1117 -64
- package/dist/hooks/session-end.js.map +4 -4
- package/dist/hooks/session-start.js +1112 -63
- package/dist/hooks/session-start.js.map +4 -4
- package/dist/hooks/stop.js +1117 -64
- package/dist/hooks/stop.js.map +4 -4
- package/dist/hooks/user-prompt-submit.js +1151 -67
- package/dist/hooks/user-prompt-submit.js.map +4 -4
- package/dist/server/api/index.js +1145 -70
- package/dist/server/api/index.js.map +4 -4
- package/dist/server/index.js +1145 -70
- package/dist/server/index.js.map +4 -4
- package/dist/services/memory-service.js +1122 -65
- 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 +849 -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 +53 -4
- package/src/server/api/stats.ts +37 -7
- package/src/services/memory-service.ts +168 -39
- 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,975 @@ 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
|
+
const tableInfo = sqliteAll(this.db, "PRAGMA table_info(events)", []);
|
|
1046
|
+
const columnNames = tableInfo.map((col) => col.name);
|
|
1047
|
+
if (!columnNames.includes("access_count")) {
|
|
1048
|
+
try {
|
|
1049
|
+
sqliteExec(this.db, `
|
|
1050
|
+
ALTER TABLE events ADD COLUMN access_count INTEGER DEFAULT 0;
|
|
1051
|
+
`);
|
|
1052
|
+
} catch (err) {
|
|
1053
|
+
console.error("Error adding access_count column:", err);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
if (!columnNames.includes("last_accessed_at")) {
|
|
1057
|
+
try {
|
|
1058
|
+
sqliteExec(this.db, `
|
|
1059
|
+
ALTER TABLE events ADD COLUMN last_accessed_at TEXT;
|
|
1060
|
+
`);
|
|
1061
|
+
} catch (err) {
|
|
1062
|
+
console.error("Error adding last_accessed_at column:", err);
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
try {
|
|
1066
|
+
sqliteExec(this.db, `
|
|
1067
|
+
CREATE INDEX IF NOT EXISTS idx_events_access_count ON events(access_count DESC);
|
|
1068
|
+
`);
|
|
1069
|
+
} catch (err) {
|
|
1070
|
+
}
|
|
1071
|
+
try {
|
|
1072
|
+
sqliteExec(this.db, `
|
|
1073
|
+
CREATE INDEX IF NOT EXISTS idx_events_last_accessed ON events(last_accessed_at DESC);
|
|
1074
|
+
`);
|
|
1075
|
+
} catch (err) {
|
|
1076
|
+
}
|
|
1077
|
+
this.initialized = true;
|
|
1078
|
+
}
|
|
1079
|
+
/**
|
|
1080
|
+
* Append event to store (Append-only, Idempotent)
|
|
1081
|
+
*/
|
|
1082
|
+
async append(input) {
|
|
1083
|
+
await this.initialize();
|
|
1084
|
+
const canonicalKey = makeCanonicalKey(input.content);
|
|
1085
|
+
const dedupeKey = makeDedupeKey(input.content, input.sessionId);
|
|
1086
|
+
const existing = sqliteGet(
|
|
1087
|
+
this.db,
|
|
1088
|
+
`SELECT event_id FROM event_dedup WHERE dedupe_key = ?`,
|
|
1089
|
+
[dedupeKey]
|
|
1090
|
+
);
|
|
1091
|
+
if (existing) {
|
|
1092
|
+
return {
|
|
1093
|
+
success: true,
|
|
1094
|
+
eventId: existing.event_id,
|
|
1095
|
+
isDuplicate: true
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
const id = randomUUID2();
|
|
1099
|
+
const timestamp = toSQLiteTimestamp(input.timestamp);
|
|
1100
|
+
try {
|
|
1101
|
+
const insertEvent = this.db.prepare(`
|
|
1102
|
+
INSERT INTO events (id, event_type, session_id, timestamp, content, canonical_key, dedupe_key, metadata)
|
|
1103
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1104
|
+
`);
|
|
1105
|
+
const insertDedup = this.db.prepare(`
|
|
1106
|
+
INSERT INTO event_dedup (dedupe_key, event_id) VALUES (?, ?)
|
|
1107
|
+
`);
|
|
1108
|
+
const insertLevel = this.db.prepare(`
|
|
1109
|
+
INSERT INTO memory_levels (event_id, level) VALUES (?, 'L0')
|
|
1110
|
+
`);
|
|
1111
|
+
const transaction = this.db.transaction(() => {
|
|
1112
|
+
insertEvent.run(
|
|
1113
|
+
id,
|
|
1114
|
+
input.eventType,
|
|
1115
|
+
input.sessionId,
|
|
1116
|
+
timestamp,
|
|
1117
|
+
input.content,
|
|
1118
|
+
canonicalKey,
|
|
1119
|
+
dedupeKey,
|
|
1120
|
+
JSON.stringify(input.metadata || {})
|
|
1121
|
+
);
|
|
1122
|
+
insertDedup.run(dedupeKey, id);
|
|
1123
|
+
insertLevel.run(id);
|
|
1124
|
+
});
|
|
1125
|
+
transaction();
|
|
1126
|
+
return { success: true, eventId: id, isDuplicate: false };
|
|
1127
|
+
} catch (error) {
|
|
1128
|
+
return {
|
|
1129
|
+
success: false,
|
|
1130
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1131
|
+
};
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
/**
|
|
1135
|
+
* Get events by session ID
|
|
1136
|
+
*/
|
|
1137
|
+
async getSessionEvents(sessionId) {
|
|
1138
|
+
await this.initialize();
|
|
1139
|
+
const rows = sqliteAll(
|
|
1140
|
+
this.db,
|
|
1141
|
+
`SELECT * FROM events WHERE session_id = ? ORDER BY timestamp ASC`,
|
|
1142
|
+
[sessionId]
|
|
1143
|
+
);
|
|
1144
|
+
return rows.map(this.rowToEvent);
|
|
1145
|
+
}
|
|
1146
|
+
/**
|
|
1147
|
+
* Get recent events
|
|
1148
|
+
*/
|
|
1149
|
+
async getRecentEvents(limit = 100) {
|
|
1150
|
+
await this.initialize();
|
|
1151
|
+
const rows = sqliteAll(
|
|
1152
|
+
this.db,
|
|
1153
|
+
`SELECT * FROM events ORDER BY timestamp DESC LIMIT ?`,
|
|
1154
|
+
[limit]
|
|
1155
|
+
);
|
|
1156
|
+
return rows.map(this.rowToEvent);
|
|
1157
|
+
}
|
|
1158
|
+
/**
|
|
1159
|
+
* Get event by ID
|
|
1160
|
+
*/
|
|
1161
|
+
async getEvent(id) {
|
|
1162
|
+
await this.initialize();
|
|
1163
|
+
const row = sqliteGet(
|
|
1164
|
+
this.db,
|
|
1165
|
+
`SELECT * FROM events WHERE id = ?`,
|
|
1166
|
+
[id]
|
|
1167
|
+
);
|
|
1168
|
+
if (!row)
|
|
1169
|
+
return null;
|
|
1170
|
+
return this.rowToEvent(row);
|
|
1171
|
+
}
|
|
1172
|
+
/**
|
|
1173
|
+
* Get events since a timestamp (for sync)
|
|
1174
|
+
*/
|
|
1175
|
+
async getEventsSince(timestamp, limit = 1e3) {
|
|
1176
|
+
await this.initialize();
|
|
1177
|
+
const rows = sqliteAll(
|
|
1178
|
+
this.db,
|
|
1179
|
+
`SELECT * FROM events WHERE timestamp > ? ORDER BY timestamp ASC LIMIT ?`,
|
|
1180
|
+
[timestamp, limit]
|
|
1181
|
+
);
|
|
1182
|
+
return rows.map(this.rowToEvent);
|
|
1183
|
+
}
|
|
1184
|
+
/**
|
|
1185
|
+
* Create or update session
|
|
1186
|
+
*/
|
|
1187
|
+
async upsertSession(session) {
|
|
1188
|
+
await this.initialize();
|
|
1189
|
+
const existing = sqliteGet(
|
|
1190
|
+
this.db,
|
|
1191
|
+
`SELECT id FROM sessions WHERE id = ?`,
|
|
1192
|
+
[session.id]
|
|
1193
|
+
);
|
|
1194
|
+
if (!existing) {
|
|
1195
|
+
sqliteRun(
|
|
1196
|
+
this.db,
|
|
1197
|
+
`INSERT INTO sessions (id, started_at, project_path, tags)
|
|
1198
|
+
VALUES (?, ?, ?, ?)`,
|
|
1199
|
+
[
|
|
1200
|
+
session.id,
|
|
1201
|
+
toSQLiteTimestamp(session.startedAt || /* @__PURE__ */ new Date()),
|
|
1202
|
+
session.projectPath || null,
|
|
1203
|
+
JSON.stringify(session.tags || [])
|
|
1204
|
+
]
|
|
1205
|
+
);
|
|
1206
|
+
} else {
|
|
1207
|
+
const updates = [];
|
|
1208
|
+
const values = [];
|
|
1209
|
+
if (session.endedAt) {
|
|
1210
|
+
updates.push("ended_at = ?");
|
|
1211
|
+
values.push(toSQLiteTimestamp(session.endedAt));
|
|
1212
|
+
}
|
|
1213
|
+
if (session.summary) {
|
|
1214
|
+
updates.push("summary = ?");
|
|
1215
|
+
values.push(session.summary);
|
|
1216
|
+
}
|
|
1217
|
+
if (session.tags) {
|
|
1218
|
+
updates.push("tags = ?");
|
|
1219
|
+
values.push(JSON.stringify(session.tags));
|
|
1220
|
+
}
|
|
1221
|
+
if (updates.length > 0) {
|
|
1222
|
+
values.push(session.id);
|
|
1223
|
+
sqliteRun(
|
|
1224
|
+
this.db,
|
|
1225
|
+
`UPDATE sessions SET ${updates.join(", ")} WHERE id = ?`,
|
|
1226
|
+
values
|
|
1227
|
+
);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
/**
|
|
1232
|
+
* Get session by ID
|
|
1233
|
+
*/
|
|
1234
|
+
async getSession(id) {
|
|
1235
|
+
await this.initialize();
|
|
1236
|
+
const row = sqliteGet(
|
|
1237
|
+
this.db,
|
|
1238
|
+
`SELECT * FROM sessions WHERE id = ?`,
|
|
1239
|
+
[id]
|
|
1240
|
+
);
|
|
1241
|
+
if (!row)
|
|
1242
|
+
return null;
|
|
1243
|
+
return {
|
|
1244
|
+
id: row.id,
|
|
1245
|
+
startedAt: toDateFromSQLite(row.started_at),
|
|
1246
|
+
endedAt: row.ended_at ? toDateFromSQLite(row.ended_at) : void 0,
|
|
1247
|
+
projectPath: row.project_path,
|
|
1248
|
+
summary: row.summary,
|
|
1249
|
+
tags: row.tags ? JSON.parse(row.tags) : void 0
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
/**
|
|
1253
|
+
* Get all sessions
|
|
1254
|
+
*/
|
|
1255
|
+
async getAllSessions() {
|
|
1256
|
+
await this.initialize();
|
|
1257
|
+
const rows = sqliteAll(
|
|
1258
|
+
this.db,
|
|
1259
|
+
`SELECT * FROM sessions ORDER BY started_at DESC`
|
|
1260
|
+
);
|
|
1261
|
+
return rows.map((row) => ({
|
|
1262
|
+
id: row.id,
|
|
1263
|
+
startedAt: toDateFromSQLite(row.started_at),
|
|
1264
|
+
endedAt: row.ended_at ? toDateFromSQLite(row.ended_at) : void 0,
|
|
1265
|
+
projectPath: row.project_path,
|
|
1266
|
+
summary: row.summary,
|
|
1267
|
+
tags: row.tags ? JSON.parse(row.tags) : void 0
|
|
1268
|
+
}));
|
|
1269
|
+
}
|
|
1270
|
+
/**
|
|
1271
|
+
* Add to embedding outbox
|
|
1272
|
+
*/
|
|
1273
|
+
async enqueueForEmbedding(eventId, content) {
|
|
1274
|
+
await this.initialize();
|
|
1275
|
+
const id = randomUUID2();
|
|
1276
|
+
sqliteRun(
|
|
1277
|
+
this.db,
|
|
1278
|
+
`INSERT INTO embedding_outbox (id, event_id, content, status, retry_count)
|
|
1279
|
+
VALUES (?, ?, ?, 'pending', 0)`,
|
|
1280
|
+
[id, eventId, content]
|
|
1281
|
+
);
|
|
1282
|
+
return id;
|
|
1283
|
+
}
|
|
1284
|
+
/**
|
|
1285
|
+
* Get pending outbox items
|
|
1286
|
+
*/
|
|
1287
|
+
async getPendingOutboxItems(limit = 32) {
|
|
1288
|
+
await this.initialize();
|
|
1289
|
+
const pending = sqliteAll(
|
|
1290
|
+
this.db,
|
|
1291
|
+
`SELECT * FROM embedding_outbox
|
|
1292
|
+
WHERE status = 'pending'
|
|
1293
|
+
ORDER BY created_at
|
|
1294
|
+
LIMIT ?`,
|
|
1295
|
+
[limit]
|
|
1296
|
+
);
|
|
1297
|
+
if (pending.length === 0)
|
|
1298
|
+
return [];
|
|
1299
|
+
const ids = pending.map((r) => r.id);
|
|
1300
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
1301
|
+
sqliteRun(
|
|
1302
|
+
this.db,
|
|
1303
|
+
`UPDATE embedding_outbox SET status = 'processing' WHERE id IN (${placeholders})`,
|
|
1304
|
+
ids
|
|
1305
|
+
);
|
|
1306
|
+
return pending.map((row) => ({
|
|
1307
|
+
id: row.id,
|
|
1308
|
+
eventId: row.event_id,
|
|
1309
|
+
content: row.content,
|
|
1310
|
+
status: "processing",
|
|
1311
|
+
retryCount: row.retry_count,
|
|
1312
|
+
createdAt: toDateFromSQLite(row.created_at),
|
|
1313
|
+
errorMessage: row.error_message
|
|
1314
|
+
}));
|
|
1315
|
+
}
|
|
1316
|
+
/**
|
|
1317
|
+
* Mark outbox items as done
|
|
1318
|
+
*/
|
|
1319
|
+
async completeOutboxItems(ids) {
|
|
1320
|
+
if (ids.length === 0)
|
|
1321
|
+
return;
|
|
1322
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
1323
|
+
sqliteRun(
|
|
1324
|
+
this.db,
|
|
1325
|
+
`DELETE FROM embedding_outbox WHERE id IN (${placeholders})`,
|
|
1326
|
+
ids
|
|
1327
|
+
);
|
|
1328
|
+
}
|
|
1329
|
+
/**
|
|
1330
|
+
* Mark outbox items as failed
|
|
1331
|
+
*/
|
|
1332
|
+
async failOutboxItems(ids, error) {
|
|
1333
|
+
if (ids.length === 0)
|
|
1334
|
+
return;
|
|
1335
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
1336
|
+
sqliteRun(
|
|
1337
|
+
this.db,
|
|
1338
|
+
`UPDATE embedding_outbox
|
|
1339
|
+
SET status = CASE WHEN retry_count >= 3 THEN 'failed' ELSE 'pending' END,
|
|
1340
|
+
retry_count = retry_count + 1,
|
|
1341
|
+
error_message = ?
|
|
1342
|
+
WHERE id IN (${placeholders})`,
|
|
1343
|
+
[error, ...ids]
|
|
1344
|
+
);
|
|
1345
|
+
}
|
|
1346
|
+
/**
|
|
1347
|
+
* Update memory level
|
|
1348
|
+
*/
|
|
1349
|
+
async updateMemoryLevel(eventId, level) {
|
|
1350
|
+
await this.initialize();
|
|
1351
|
+
sqliteRun(
|
|
1352
|
+
this.db,
|
|
1353
|
+
`UPDATE memory_levels SET level = ?, promoted_at = datetime('now') WHERE event_id = ?`,
|
|
1354
|
+
[level, eventId]
|
|
1355
|
+
);
|
|
1356
|
+
}
|
|
1357
|
+
/**
|
|
1358
|
+
* Get memory level statistics
|
|
1359
|
+
*/
|
|
1360
|
+
async getLevelStats() {
|
|
1361
|
+
await this.initialize();
|
|
1362
|
+
const rows = sqliteAll(
|
|
1363
|
+
this.db,
|
|
1364
|
+
`SELECT level, COUNT(*) as count FROM memory_levels GROUP BY level`
|
|
1365
|
+
);
|
|
1366
|
+
return rows;
|
|
1367
|
+
}
|
|
1368
|
+
/**
|
|
1369
|
+
* Get events by memory level
|
|
1370
|
+
*/
|
|
1371
|
+
async getEventsByLevel(level, options) {
|
|
1372
|
+
await this.initialize();
|
|
1373
|
+
const limit = options?.limit || 50;
|
|
1374
|
+
const offset = options?.offset || 0;
|
|
1375
|
+
const rows = sqliteAll(
|
|
1376
|
+
this.db,
|
|
1377
|
+
`SELECT e.* FROM events e
|
|
1378
|
+
INNER JOIN memory_levels ml ON e.id = ml.event_id
|
|
1379
|
+
WHERE ml.level = ?
|
|
1380
|
+
ORDER BY e.timestamp DESC
|
|
1381
|
+
LIMIT ? OFFSET ?`,
|
|
1382
|
+
[level, limit, offset]
|
|
1383
|
+
);
|
|
1384
|
+
return rows.map((row) => this.rowToEvent(row));
|
|
1385
|
+
}
|
|
1386
|
+
/**
|
|
1387
|
+
* Get memory level for a specific event
|
|
1388
|
+
*/
|
|
1389
|
+
async getEventLevel(eventId) {
|
|
1390
|
+
await this.initialize();
|
|
1391
|
+
const row = sqliteGet(
|
|
1392
|
+
this.db,
|
|
1393
|
+
`SELECT level FROM memory_levels WHERE event_id = ?`,
|
|
1394
|
+
[eventId]
|
|
1395
|
+
);
|
|
1396
|
+
return row ? row.level : null;
|
|
1397
|
+
}
|
|
1398
|
+
/**
|
|
1399
|
+
* Get sync position for a target
|
|
1400
|
+
*/
|
|
1401
|
+
async getSyncPosition(targetName) {
|
|
1402
|
+
await this.initialize();
|
|
1403
|
+
const row = sqliteGet(
|
|
1404
|
+
this.db,
|
|
1405
|
+
`SELECT last_event_id, last_timestamp FROM sync_positions WHERE target_name = ?`,
|
|
1406
|
+
[targetName]
|
|
1407
|
+
);
|
|
1408
|
+
return {
|
|
1409
|
+
lastEventId: row?.last_event_id ?? null,
|
|
1410
|
+
lastTimestamp: row?.last_timestamp ?? null
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1413
|
+
/**
|
|
1414
|
+
* Update sync position for a target
|
|
1415
|
+
*/
|
|
1416
|
+
async updateSyncPosition(targetName, lastEventId, lastTimestamp) {
|
|
1417
|
+
await this.initialize();
|
|
1418
|
+
sqliteRun(
|
|
1419
|
+
this.db,
|
|
1420
|
+
`INSERT OR REPLACE INTO sync_positions (target_name, last_event_id, last_timestamp, updated_at)
|
|
1421
|
+
VALUES (?, ?, ?, datetime('now'))`,
|
|
1422
|
+
[targetName, lastEventId, lastTimestamp]
|
|
1423
|
+
);
|
|
1424
|
+
}
|
|
1425
|
+
/**
|
|
1426
|
+
* Get config value for endless mode
|
|
1427
|
+
*/
|
|
1428
|
+
async getEndlessConfig(key) {
|
|
1429
|
+
await this.initialize();
|
|
1430
|
+
const row = sqliteGet(
|
|
1431
|
+
this.db,
|
|
1432
|
+
`SELECT value FROM endless_config WHERE key = ?`,
|
|
1433
|
+
[key]
|
|
1434
|
+
);
|
|
1435
|
+
if (!row)
|
|
1436
|
+
return null;
|
|
1437
|
+
return JSON.parse(row.value);
|
|
1438
|
+
}
|
|
1439
|
+
/**
|
|
1440
|
+
* Set config value for endless mode
|
|
1441
|
+
*/
|
|
1442
|
+
async setEndlessConfig(key, value) {
|
|
1443
|
+
await this.initialize();
|
|
1444
|
+
sqliteRun(
|
|
1445
|
+
this.db,
|
|
1446
|
+
`INSERT OR REPLACE INTO endless_config (key, value, updated_at)
|
|
1447
|
+
VALUES (?, ?, datetime('now'))`,
|
|
1448
|
+
[key, JSON.stringify(value)]
|
|
1449
|
+
);
|
|
1450
|
+
}
|
|
1451
|
+
/**
|
|
1452
|
+
* Increment access count for events
|
|
1453
|
+
*/
|
|
1454
|
+
async incrementAccessCount(eventIds) {
|
|
1455
|
+
if (eventIds.length === 0 || this.readOnly)
|
|
1456
|
+
return;
|
|
1457
|
+
await this.initialize();
|
|
1458
|
+
const placeholders = eventIds.map(() => "?").join(",");
|
|
1459
|
+
const currentTime = toSQLiteTimestamp(/* @__PURE__ */ new Date());
|
|
1460
|
+
sqliteRun(
|
|
1461
|
+
this.db,
|
|
1462
|
+
`UPDATE events
|
|
1463
|
+
SET access_count = access_count + 1,
|
|
1464
|
+
last_accessed_at = ?
|
|
1465
|
+
WHERE id IN (${placeholders})`,
|
|
1466
|
+
[currentTime, ...eventIds]
|
|
1467
|
+
);
|
|
1468
|
+
}
|
|
1469
|
+
/**
|
|
1470
|
+
* Get most accessed memories
|
|
1471
|
+
*/
|
|
1472
|
+
async getMostAccessed(limit = 10) {
|
|
1473
|
+
await this.initialize();
|
|
1474
|
+
const rows = sqliteAll(
|
|
1475
|
+
this.db,
|
|
1476
|
+
`SELECT * FROM events
|
|
1477
|
+
WHERE access_count > 0
|
|
1478
|
+
ORDER BY access_count DESC, last_accessed_at DESC
|
|
1479
|
+
LIMIT ?`,
|
|
1480
|
+
[limit]
|
|
1481
|
+
);
|
|
1482
|
+
return rows.map((row) => this.rowToEvent(row));
|
|
1483
|
+
}
|
|
1484
|
+
/**
|
|
1485
|
+
* Get database instance for direct access
|
|
1486
|
+
*/
|
|
1487
|
+
getDatabase() {
|
|
1488
|
+
return this.db;
|
|
1489
|
+
}
|
|
711
1490
|
/**
|
|
712
1491
|
* Close database connection
|
|
713
1492
|
*/
|
|
714
1493
|
async close() {
|
|
715
|
-
|
|
1494
|
+
sqliteClose(this.db);
|
|
716
1495
|
}
|
|
717
1496
|
/**
|
|
718
1497
|
* Convert database row to MemoryEvent
|
|
719
1498
|
*/
|
|
720
1499
|
rowToEvent(row) {
|
|
721
|
-
|
|
1500
|
+
const event = {
|
|
722
1501
|
id: row.id,
|
|
723
1502
|
eventType: row.event_type,
|
|
724
1503
|
sessionId: row.session_id,
|
|
725
|
-
timestamp:
|
|
1504
|
+
timestamp: toDateFromSQLite(row.timestamp),
|
|
726
1505
|
content: row.content,
|
|
727
1506
|
canonicalKey: row.canonical_key,
|
|
728
1507
|
dedupeKey: row.dedupe_key,
|
|
729
1508
|
metadata: row.metadata ? JSON.parse(row.metadata) : void 0
|
|
730
1509
|
};
|
|
1510
|
+
if (row.access_count !== void 0) {
|
|
1511
|
+
event.access_count = row.access_count;
|
|
1512
|
+
}
|
|
1513
|
+
if (row.last_accessed_at !== void 0) {
|
|
1514
|
+
event.last_accessed_at = row.last_accessed_at;
|
|
1515
|
+
}
|
|
1516
|
+
return event;
|
|
1517
|
+
}
|
|
1518
|
+
};
|
|
1519
|
+
|
|
1520
|
+
// src/core/sync-worker.ts
|
|
1521
|
+
var DEFAULT_CONFIG = {
|
|
1522
|
+
intervalMs: 3e4,
|
|
1523
|
+
batchSize: 500,
|
|
1524
|
+
maxRetries: 3,
|
|
1525
|
+
retryDelayMs: 5e3
|
|
1526
|
+
};
|
|
1527
|
+
var SyncWorker = class {
|
|
1528
|
+
constructor(sqliteStore, duckdbStore, config) {
|
|
1529
|
+
this.sqliteStore = sqliteStore;
|
|
1530
|
+
this.duckdbStore = duckdbStore;
|
|
1531
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
1532
|
+
}
|
|
1533
|
+
config;
|
|
1534
|
+
intervalHandle = null;
|
|
1535
|
+
running = false;
|
|
1536
|
+
stats = {
|
|
1537
|
+
lastSyncAt: null,
|
|
1538
|
+
eventsSynced: 0,
|
|
1539
|
+
sessionsSynced: 0,
|
|
1540
|
+
errors: 0,
|
|
1541
|
+
status: "idle"
|
|
1542
|
+
};
|
|
1543
|
+
/**
|
|
1544
|
+
* Start the sync worker
|
|
1545
|
+
*/
|
|
1546
|
+
start() {
|
|
1547
|
+
if (this.running)
|
|
1548
|
+
return;
|
|
1549
|
+
this.running = true;
|
|
1550
|
+
this.stats.status = "idle";
|
|
1551
|
+
this.syncNow().catch((err) => {
|
|
1552
|
+
console.error("[SyncWorker] Initial sync failed:", err);
|
|
1553
|
+
});
|
|
1554
|
+
this.intervalHandle = setInterval(() => {
|
|
1555
|
+
this.syncNow().catch((err) => {
|
|
1556
|
+
console.error("[SyncWorker] Periodic sync failed:", err);
|
|
1557
|
+
});
|
|
1558
|
+
}, this.config.intervalMs);
|
|
1559
|
+
}
|
|
1560
|
+
/**
|
|
1561
|
+
* Stop the sync worker
|
|
1562
|
+
*/
|
|
1563
|
+
stop() {
|
|
1564
|
+
this.running = false;
|
|
1565
|
+
this.stats.status = "stopped";
|
|
1566
|
+
if (this.intervalHandle) {
|
|
1567
|
+
clearInterval(this.intervalHandle);
|
|
1568
|
+
this.intervalHandle = null;
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
/**
|
|
1572
|
+
* Trigger immediate sync
|
|
1573
|
+
*/
|
|
1574
|
+
async syncNow() {
|
|
1575
|
+
if (this.stats.status === "syncing") {
|
|
1576
|
+
return;
|
|
1577
|
+
}
|
|
1578
|
+
this.stats.status = "syncing";
|
|
1579
|
+
try {
|
|
1580
|
+
await this.syncEvents();
|
|
1581
|
+
await this.syncSessions();
|
|
1582
|
+
this.stats.lastSyncAt = /* @__PURE__ */ new Date();
|
|
1583
|
+
this.stats.status = "idle";
|
|
1584
|
+
} catch (error) {
|
|
1585
|
+
this.stats.errors++;
|
|
1586
|
+
this.stats.status = "error";
|
|
1587
|
+
throw error;
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
/**
|
|
1591
|
+
* Sync events from SQLite to DuckDB
|
|
1592
|
+
*/
|
|
1593
|
+
async syncEvents() {
|
|
1594
|
+
const targetName = "duckdb_analytics";
|
|
1595
|
+
const position = await this.sqliteStore.getSyncPosition(targetName);
|
|
1596
|
+
const lastTimestamp = position.lastTimestamp || "1970-01-01T00:00:00.000Z";
|
|
1597
|
+
let hasMore = true;
|
|
1598
|
+
let totalSynced = 0;
|
|
1599
|
+
while (hasMore) {
|
|
1600
|
+
const events = await this.sqliteStore.getEventsSince(lastTimestamp, this.config.batchSize);
|
|
1601
|
+
if (events.length === 0) {
|
|
1602
|
+
hasMore = false;
|
|
1603
|
+
break;
|
|
1604
|
+
}
|
|
1605
|
+
await this.retryWithBackoff(async () => {
|
|
1606
|
+
for (const event of events) {
|
|
1607
|
+
await this.insertEventToDuckDB(event);
|
|
1608
|
+
}
|
|
1609
|
+
});
|
|
1610
|
+
totalSynced += events.length;
|
|
1611
|
+
const lastEvent = events[events.length - 1];
|
|
1612
|
+
await this.sqliteStore.updateSyncPosition(
|
|
1613
|
+
targetName,
|
|
1614
|
+
lastEvent.id,
|
|
1615
|
+
lastEvent.timestamp.toISOString()
|
|
1616
|
+
);
|
|
1617
|
+
hasMore = events.length === this.config.batchSize;
|
|
1618
|
+
}
|
|
1619
|
+
this.stats.eventsSynced += totalSynced;
|
|
1620
|
+
}
|
|
1621
|
+
/**
|
|
1622
|
+
* Sync sessions from SQLite to DuckDB
|
|
1623
|
+
*/
|
|
1624
|
+
async syncSessions() {
|
|
1625
|
+
const sessions = await this.sqliteStore.getAllSessions();
|
|
1626
|
+
for (const session of sessions) {
|
|
1627
|
+
await this.retryWithBackoff(async () => {
|
|
1628
|
+
await this.duckdbStore.upsertSession(session);
|
|
1629
|
+
});
|
|
1630
|
+
}
|
|
1631
|
+
this.stats.sessionsSynced = sessions.length;
|
|
1632
|
+
}
|
|
1633
|
+
/**
|
|
1634
|
+
* Insert a single event into DuckDB
|
|
1635
|
+
*/
|
|
1636
|
+
async insertEventToDuckDB(event) {
|
|
1637
|
+
await this.duckdbStore.append({
|
|
1638
|
+
eventType: event.eventType,
|
|
1639
|
+
sessionId: event.sessionId,
|
|
1640
|
+
timestamp: event.timestamp,
|
|
1641
|
+
content: event.content,
|
|
1642
|
+
metadata: event.metadata
|
|
1643
|
+
});
|
|
1644
|
+
}
|
|
1645
|
+
/**
|
|
1646
|
+
* Retry operation with exponential backoff
|
|
1647
|
+
*/
|
|
1648
|
+
async retryWithBackoff(fn) {
|
|
1649
|
+
let lastError = null;
|
|
1650
|
+
for (let attempt = 0; attempt < this.config.maxRetries; attempt++) {
|
|
1651
|
+
try {
|
|
1652
|
+
return await fn();
|
|
1653
|
+
} catch (error) {
|
|
1654
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
1655
|
+
if (attempt < this.config.maxRetries - 1) {
|
|
1656
|
+
const delay = this.config.retryDelayMs * Math.pow(2, attempt);
|
|
1657
|
+
await this.sleep(delay);
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
throw lastError;
|
|
1662
|
+
}
|
|
1663
|
+
/**
|
|
1664
|
+
* Sleep utility
|
|
1665
|
+
*/
|
|
1666
|
+
sleep(ms) {
|
|
1667
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
1668
|
+
}
|
|
1669
|
+
/**
|
|
1670
|
+
* Get sync statistics
|
|
1671
|
+
*/
|
|
1672
|
+
getStats() {
|
|
1673
|
+
return { ...this.stats };
|
|
1674
|
+
}
|
|
1675
|
+
/**
|
|
1676
|
+
* Check if worker is running
|
|
1677
|
+
*/
|
|
1678
|
+
isRunning() {
|
|
1679
|
+
return this.running;
|
|
731
1680
|
}
|
|
732
1681
|
};
|
|
733
1682
|
|
|
@@ -959,7 +1908,7 @@ function getDefaultEmbedder() {
|
|
|
959
1908
|
}
|
|
960
1909
|
|
|
961
1910
|
// src/core/vector-outbox.ts
|
|
962
|
-
var
|
|
1911
|
+
var DEFAULT_CONFIG2 = {
|
|
963
1912
|
embeddingVersion: "v1",
|
|
964
1913
|
maxRetries: 3,
|
|
965
1914
|
stuckThresholdMs: 5 * 60 * 1e3,
|
|
@@ -968,7 +1917,7 @@ var DEFAULT_CONFIG = {
|
|
|
968
1917
|
};
|
|
969
1918
|
|
|
970
1919
|
// src/core/vector-worker.ts
|
|
971
|
-
var
|
|
1920
|
+
var DEFAULT_CONFIG3 = {
|
|
972
1921
|
batchSize: 32,
|
|
973
1922
|
pollIntervalMs: 1e3,
|
|
974
1923
|
maxRetries: 3
|
|
@@ -979,12 +1928,13 @@ var VectorWorker = class {
|
|
|
979
1928
|
embedder;
|
|
980
1929
|
config;
|
|
981
1930
|
running = false;
|
|
1931
|
+
stopping = false;
|
|
982
1932
|
pollTimeout = null;
|
|
983
1933
|
constructor(eventStore, vectorStore, embedder, config = {}) {
|
|
984
1934
|
this.eventStore = eventStore;
|
|
985
1935
|
this.vectorStore = vectorStore;
|
|
986
1936
|
this.embedder = embedder;
|
|
987
|
-
this.config = { ...
|
|
1937
|
+
this.config = { ...DEFAULT_CONFIG3, ...config };
|
|
988
1938
|
}
|
|
989
1939
|
/**
|
|
990
1940
|
* Start the worker polling loop
|
|
@@ -993,6 +1943,7 @@ var VectorWorker = class {
|
|
|
993
1943
|
if (this.running)
|
|
994
1944
|
return;
|
|
995
1945
|
this.running = true;
|
|
1946
|
+
this.stopping = false;
|
|
996
1947
|
this.poll();
|
|
997
1948
|
}
|
|
998
1949
|
/**
|
|
@@ -1000,6 +1951,7 @@ var VectorWorker = class {
|
|
|
1000
1951
|
*/
|
|
1001
1952
|
stop() {
|
|
1002
1953
|
this.running = false;
|
|
1954
|
+
this.stopping = true;
|
|
1003
1955
|
if (this.pollTimeout) {
|
|
1004
1956
|
clearTimeout(this.pollTimeout);
|
|
1005
1957
|
this.pollTimeout = null;
|
|
@@ -1049,9 +2001,15 @@ var VectorWorker = class {
|
|
|
1049
2001
|
}
|
|
1050
2002
|
return successful.length;
|
|
1051
2003
|
} catch (error) {
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
2004
|
+
if (!this.stopping) {
|
|
2005
|
+
try {
|
|
2006
|
+
const allIds = items.map((i) => i.id);
|
|
2007
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2008
|
+
await this.eventStore.failOutboxItems(allIds, errorMessage);
|
|
2009
|
+
} catch (failError) {
|
|
2010
|
+
console.warn("Could not mark outbox items as failed (database may be closed)");
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
1055
2013
|
throw error;
|
|
1056
2014
|
}
|
|
1057
2015
|
}
|
|
@@ -1059,14 +2017,18 @@ var VectorWorker = class {
|
|
|
1059
2017
|
* Poll for new items
|
|
1060
2018
|
*/
|
|
1061
2019
|
async poll() {
|
|
1062
|
-
if (!this.running)
|
|
2020
|
+
if (!this.running || this.stopping)
|
|
1063
2021
|
return;
|
|
1064
2022
|
try {
|
|
1065
2023
|
await this.processBatch();
|
|
1066
2024
|
} catch (error) {
|
|
1067
|
-
|
|
2025
|
+
if (!this.stopping) {
|
|
2026
|
+
console.error("Vector worker error:", error);
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
if (this.running && !this.stopping) {
|
|
2030
|
+
this.pollTimeout = setTimeout(() => this.poll(), this.config.pollIntervalMs);
|
|
1068
2031
|
}
|
|
1069
|
-
this.pollTimeout = setTimeout(() => this.poll(), this.config.pollIntervalMs);
|
|
1070
2032
|
}
|
|
1071
2033
|
/**
|
|
1072
2034
|
* Process all pending items (blocking)
|
|
@@ -1093,7 +2055,7 @@ function createVectorWorker(eventStore, vectorStore, embedder, config) {
|
|
|
1093
2055
|
}
|
|
1094
2056
|
|
|
1095
2057
|
// src/core/matcher.ts
|
|
1096
|
-
var
|
|
2058
|
+
var DEFAULT_CONFIG4 = {
|
|
1097
2059
|
weights: {
|
|
1098
2060
|
semanticSimilarity: 0.4,
|
|
1099
2061
|
ftsScore: 0.25,
|
|
@@ -1108,9 +2070,9 @@ var Matcher = class {
|
|
|
1108
2070
|
config;
|
|
1109
2071
|
constructor(config = {}) {
|
|
1110
2072
|
this.config = {
|
|
1111
|
-
...
|
|
2073
|
+
...DEFAULT_CONFIG4,
|
|
1112
2074
|
...config,
|
|
1113
|
-
weights: { ...
|
|
2075
|
+
weights: { ...DEFAULT_CONFIG4.weights, ...config.weights }
|
|
1114
2076
|
};
|
|
1115
2077
|
}
|
|
1116
2078
|
/**
|
|
@@ -1818,7 +2780,7 @@ function createSharedEventStore(dbPath) {
|
|
|
1818
2780
|
}
|
|
1819
2781
|
|
|
1820
2782
|
// src/core/shared-store.ts
|
|
1821
|
-
import { randomUUID as
|
|
2783
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
1822
2784
|
var SharedStore = class {
|
|
1823
2785
|
constructor(sharedEventStore) {
|
|
1824
2786
|
this.sharedEventStore = sharedEventStore;
|
|
@@ -1830,7 +2792,7 @@ var SharedStore = class {
|
|
|
1830
2792
|
* Promote a verified troubleshooting entry to shared storage
|
|
1831
2793
|
*/
|
|
1832
2794
|
async promoteEntry(input) {
|
|
1833
|
-
const entryId =
|
|
2795
|
+
const entryId = randomUUID3();
|
|
1834
2796
|
await dbRun(
|
|
1835
2797
|
this.db,
|
|
1836
2798
|
`INSERT INTO shared_troubleshooting (
|
|
@@ -2195,7 +3157,7 @@ function createSharedVectorStore(dbPath) {
|
|
|
2195
3157
|
}
|
|
2196
3158
|
|
|
2197
3159
|
// src/core/shared-promoter.ts
|
|
2198
|
-
import { randomUUID as
|
|
3160
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
2199
3161
|
var SharedPromoter = class {
|
|
2200
3162
|
constructor(sharedStore, sharedVectorStore, embedder, config) {
|
|
2201
3163
|
this.sharedStore = sharedStore;
|
|
@@ -2263,7 +3225,7 @@ var SharedPromoter = class {
|
|
|
2263
3225
|
const embeddingContent = this.createEmbeddingContent(input);
|
|
2264
3226
|
const embedding = await this.embedder.embed(embeddingContent);
|
|
2265
3227
|
await this.sharedVectorStore.upsert({
|
|
2266
|
-
id:
|
|
3228
|
+
id: randomUUID4(),
|
|
2267
3229
|
entryId,
|
|
2268
3230
|
entryType: "troubleshooting",
|
|
2269
3231
|
content: embeddingContent,
|
|
@@ -2403,7 +3365,7 @@ function createToolObservationEmbedding(toolName, metadata, success) {
|
|
|
2403
3365
|
}
|
|
2404
3366
|
|
|
2405
3367
|
// src/core/working-set-store.ts
|
|
2406
|
-
import { randomUUID as
|
|
3368
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
2407
3369
|
var WorkingSetStore = class {
|
|
2408
3370
|
constructor(eventStore, config) {
|
|
2409
3371
|
this.eventStore = eventStore;
|
|
@@ -2424,7 +3386,7 @@ var WorkingSetStore = class {
|
|
|
2424
3386
|
`INSERT OR REPLACE INTO working_set (id, event_id, added_at, relevance_score, topics, expires_at)
|
|
2425
3387
|
VALUES (?, ?, CURRENT_TIMESTAMP, ?, ?, ?)`,
|
|
2426
3388
|
[
|
|
2427
|
-
|
|
3389
|
+
randomUUID5(),
|
|
2428
3390
|
eventId,
|
|
2429
3391
|
relevanceScore,
|
|
2430
3392
|
JSON.stringify(topics || []),
|
|
@@ -2608,7 +3570,7 @@ function createWorkingSetStore(eventStore, config) {
|
|
|
2608
3570
|
}
|
|
2609
3571
|
|
|
2610
3572
|
// src/core/consolidated-store.ts
|
|
2611
|
-
import { randomUUID as
|
|
3573
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
2612
3574
|
var ConsolidatedStore = class {
|
|
2613
3575
|
constructor(eventStore) {
|
|
2614
3576
|
this.eventStore = eventStore;
|
|
@@ -2620,7 +3582,7 @@ var ConsolidatedStore = class {
|
|
|
2620
3582
|
* Create a new consolidated memory
|
|
2621
3583
|
*/
|
|
2622
3584
|
async create(input) {
|
|
2623
|
-
const memoryId =
|
|
3585
|
+
const memoryId = randomUUID6();
|
|
2624
3586
|
await dbRun(
|
|
2625
3587
|
this.db,
|
|
2626
3588
|
`INSERT INTO consolidated_memories
|
|
@@ -3147,7 +4109,7 @@ function createConsolidationWorker(workingSetStore, consolidatedStore, config) {
|
|
|
3147
4109
|
}
|
|
3148
4110
|
|
|
3149
4111
|
// src/core/continuity-manager.ts
|
|
3150
|
-
import { randomUUID as
|
|
4112
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
3151
4113
|
var ContinuityManager = class {
|
|
3152
4114
|
constructor(eventStore, config) {
|
|
3153
4115
|
this.eventStore = eventStore;
|
|
@@ -3301,7 +4263,7 @@ var ContinuityManager = class {
|
|
|
3301
4263
|
`INSERT INTO continuity_log
|
|
3302
4264
|
(log_id, from_context_id, to_context_id, continuity_score, transition_type, created_at)
|
|
3303
4265
|
VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,
|
|
3304
|
-
[
|
|
4266
|
+
[randomUUID7(), previous.id, current.id, score, type]
|
|
3305
4267
|
);
|
|
3306
4268
|
}
|
|
3307
4269
|
/**
|
|
@@ -3410,7 +4372,7 @@ function createContinuityManager(eventStore, config) {
|
|
|
3410
4372
|
}
|
|
3411
4373
|
|
|
3412
4374
|
// src/core/graduation-worker.ts
|
|
3413
|
-
var
|
|
4375
|
+
var DEFAULT_CONFIG5 = {
|
|
3414
4376
|
evaluationIntervalMs: 3e5,
|
|
3415
4377
|
// 5 minutes
|
|
3416
4378
|
batchSize: 50,
|
|
@@ -3418,7 +4380,7 @@ var DEFAULT_CONFIG4 = {
|
|
|
3418
4380
|
// 1 hour cooldown between evaluations
|
|
3419
4381
|
};
|
|
3420
4382
|
var GraduationWorker = class {
|
|
3421
|
-
constructor(eventStore, graduation, config =
|
|
4383
|
+
constructor(eventStore, graduation, config = DEFAULT_CONFIG5) {
|
|
3422
4384
|
this.eventStore = eventStore;
|
|
3423
4385
|
this.graduation = graduation;
|
|
3424
4386
|
this.config = config;
|
|
@@ -3526,7 +4488,7 @@ function createGraduationWorker(eventStore, graduation, config) {
|
|
|
3526
4488
|
return new GraduationWorker(
|
|
3527
4489
|
eventStore,
|
|
3528
4490
|
graduation,
|
|
3529
|
-
{ ...
|
|
4491
|
+
{ ...DEFAULT_CONFIG5, ...config }
|
|
3530
4492
|
);
|
|
3531
4493
|
}
|
|
3532
4494
|
|
|
@@ -3550,7 +4512,11 @@ function getProjectStoragePath(projectPath) {
|
|
|
3550
4512
|
var REGISTRY_PATH = path.join(os.homedir(), ".claude-code", "memory", "session-registry.json");
|
|
3551
4513
|
var SHARED_STORAGE_PATH = path.join(os.homedir(), ".claude-code", "memory", "shared");
|
|
3552
4514
|
var MemoryService = class {
|
|
3553
|
-
|
|
4515
|
+
// Primary store: SQLite (WAL mode) - for hooks, always available
|
|
4516
|
+
sqliteStore;
|
|
4517
|
+
// Analytics store: DuckDB - for server reads (optional, synced from SQLite)
|
|
4518
|
+
analyticsStore;
|
|
4519
|
+
syncWorker = null;
|
|
3554
4520
|
vectorStore;
|
|
3555
4521
|
embedder;
|
|
3556
4522
|
matcher;
|
|
@@ -3581,17 +4547,39 @@ var MemoryService = class {
|
|
|
3581
4547
|
}
|
|
3582
4548
|
this.projectHash = config.projectHash || null;
|
|
3583
4549
|
this.sharedStoreConfig = config.sharedStoreConfig ?? { enabled: true };
|
|
3584
|
-
this.
|
|
4550
|
+
this.sqliteStore = new SQLiteEventStore(
|
|
4551
|
+
path.join(storagePath, "events.sqlite"),
|
|
4552
|
+
{ readonly: this.readOnly }
|
|
4553
|
+
);
|
|
4554
|
+
const analyticsEnabled = config.analyticsEnabled ?? this.readOnly;
|
|
4555
|
+
if (!analyticsEnabled) {
|
|
4556
|
+
this.analyticsStore = null;
|
|
4557
|
+
} else if (this.readOnly) {
|
|
4558
|
+
try {
|
|
4559
|
+
this.analyticsStore = new EventStore(
|
|
4560
|
+
path.join(storagePath, "analytics.duckdb"),
|
|
4561
|
+
{ readOnly: true }
|
|
4562
|
+
);
|
|
4563
|
+
} catch {
|
|
4564
|
+
this.analyticsStore = null;
|
|
4565
|
+
}
|
|
4566
|
+
} else {
|
|
4567
|
+
this.analyticsStore = new EventStore(
|
|
4568
|
+
path.join(storagePath, "analytics.duckdb"),
|
|
4569
|
+
{ readOnly: false }
|
|
4570
|
+
);
|
|
4571
|
+
}
|
|
3585
4572
|
this.vectorStore = new VectorStore(path.join(storagePath, "vectors"));
|
|
3586
4573
|
this.embedder = config.embeddingModel ? new Embedder(config.embeddingModel) : getDefaultEmbedder();
|
|
3587
4574
|
this.matcher = getDefaultMatcher();
|
|
3588
4575
|
this.retriever = createRetriever(
|
|
3589
|
-
this.
|
|
4576
|
+
this.sqliteStore,
|
|
4577
|
+
// Interface compatible
|
|
3590
4578
|
this.vectorStore,
|
|
3591
4579
|
this.embedder,
|
|
3592
4580
|
this.matcher
|
|
3593
4581
|
);
|
|
3594
|
-
this.graduation = createGraduationPipeline(this.
|
|
4582
|
+
this.graduation = createGraduationPipeline(this.sqliteStore);
|
|
3595
4583
|
}
|
|
3596
4584
|
/**
|
|
3597
4585
|
* Initialize all components
|
|
@@ -3599,23 +4587,38 @@ var MemoryService = class {
|
|
|
3599
4587
|
async initialize() {
|
|
3600
4588
|
if (this.initialized)
|
|
3601
4589
|
return;
|
|
3602
|
-
await this.
|
|
4590
|
+
await this.sqliteStore.initialize();
|
|
4591
|
+
if (this.analyticsStore) {
|
|
4592
|
+
try {
|
|
4593
|
+
await this.analyticsStore.initialize();
|
|
4594
|
+
} catch (error) {
|
|
4595
|
+
console.warn("[MemoryService] Analytics store (DuckDB) initialization failed, using SQLite for reads:", error);
|
|
4596
|
+
}
|
|
4597
|
+
}
|
|
3603
4598
|
await this.vectorStore.initialize();
|
|
3604
4599
|
await this.embedder.initialize();
|
|
3605
4600
|
if (!this.readOnly) {
|
|
3606
4601
|
this.vectorWorker = createVectorWorker(
|
|
3607
|
-
this.
|
|
4602
|
+
this.sqliteStore,
|
|
3608
4603
|
this.vectorStore,
|
|
3609
4604
|
this.embedder
|
|
3610
4605
|
);
|
|
3611
4606
|
this.vectorWorker.start();
|
|
3612
4607
|
this.retriever.setGraduationPipeline(this.graduation);
|
|
3613
4608
|
this.graduationWorker = createGraduationWorker(
|
|
3614
|
-
this.
|
|
4609
|
+
this.sqliteStore,
|
|
3615
4610
|
this.graduation
|
|
3616
4611
|
);
|
|
3617
4612
|
this.graduationWorker.start();
|
|
3618
|
-
|
|
4613
|
+
if (this.analyticsStore) {
|
|
4614
|
+
this.syncWorker = new SyncWorker(
|
|
4615
|
+
this.sqliteStore,
|
|
4616
|
+
this.analyticsStore,
|
|
4617
|
+
{ intervalMs: 3e4, batchSize: 500 }
|
|
4618
|
+
);
|
|
4619
|
+
this.syncWorker.start();
|
|
4620
|
+
}
|
|
4621
|
+
const savedMode = await this.sqliteStore.getEndlessConfig("mode");
|
|
3619
4622
|
if (savedMode === "endless") {
|
|
3620
4623
|
this.endlessMode = "endless";
|
|
3621
4624
|
await this.initializeEndlessMode();
|
|
@@ -3656,7 +4659,7 @@ var MemoryService = class {
|
|
|
3656
4659
|
*/
|
|
3657
4660
|
async startSession(sessionId, projectPath) {
|
|
3658
4661
|
await this.initialize();
|
|
3659
|
-
await this.
|
|
4662
|
+
await this.sqliteStore.upsertSession({
|
|
3660
4663
|
id: sessionId,
|
|
3661
4664
|
startedAt: /* @__PURE__ */ new Date(),
|
|
3662
4665
|
projectPath
|
|
@@ -3667,7 +4670,7 @@ var MemoryService = class {
|
|
|
3667
4670
|
*/
|
|
3668
4671
|
async endSession(sessionId, summary) {
|
|
3669
4672
|
await this.initialize();
|
|
3670
|
-
await this.
|
|
4673
|
+
await this.sqliteStore.upsertSession({
|
|
3671
4674
|
id: sessionId,
|
|
3672
4675
|
endedAt: /* @__PURE__ */ new Date(),
|
|
3673
4676
|
summary
|
|
@@ -3678,7 +4681,7 @@ var MemoryService = class {
|
|
|
3678
4681
|
*/
|
|
3679
4682
|
async storeUserPrompt(sessionId, content, metadata) {
|
|
3680
4683
|
await this.initialize();
|
|
3681
|
-
const result = await this.
|
|
4684
|
+
const result = await this.sqliteStore.append({
|
|
3682
4685
|
eventType: "user_prompt",
|
|
3683
4686
|
sessionId,
|
|
3684
4687
|
timestamp: /* @__PURE__ */ new Date(),
|
|
@@ -3686,7 +4689,7 @@ var MemoryService = class {
|
|
|
3686
4689
|
metadata
|
|
3687
4690
|
});
|
|
3688
4691
|
if (result.success && !result.isDuplicate) {
|
|
3689
|
-
await this.
|
|
4692
|
+
await this.sqliteStore.enqueueForEmbedding(result.eventId, content);
|
|
3690
4693
|
}
|
|
3691
4694
|
return result;
|
|
3692
4695
|
}
|
|
@@ -3695,7 +4698,7 @@ var MemoryService = class {
|
|
|
3695
4698
|
*/
|
|
3696
4699
|
async storeAgentResponse(sessionId, content, metadata) {
|
|
3697
4700
|
await this.initialize();
|
|
3698
|
-
const result = await this.
|
|
4701
|
+
const result = await this.sqliteStore.append({
|
|
3699
4702
|
eventType: "agent_response",
|
|
3700
4703
|
sessionId,
|
|
3701
4704
|
timestamp: /* @__PURE__ */ new Date(),
|
|
@@ -3703,7 +4706,7 @@ var MemoryService = class {
|
|
|
3703
4706
|
metadata
|
|
3704
4707
|
});
|
|
3705
4708
|
if (result.success && !result.isDuplicate) {
|
|
3706
|
-
await this.
|
|
4709
|
+
await this.sqliteStore.enqueueForEmbedding(result.eventId, content);
|
|
3707
4710
|
}
|
|
3708
4711
|
return result;
|
|
3709
4712
|
}
|
|
@@ -3712,14 +4715,14 @@ var MemoryService = class {
|
|
|
3712
4715
|
*/
|
|
3713
4716
|
async storeSessionSummary(sessionId, summary) {
|
|
3714
4717
|
await this.initialize();
|
|
3715
|
-
const result = await this.
|
|
4718
|
+
const result = await this.sqliteStore.append({
|
|
3716
4719
|
eventType: "session_summary",
|
|
3717
4720
|
sessionId,
|
|
3718
4721
|
timestamp: /* @__PURE__ */ new Date(),
|
|
3719
4722
|
content: summary
|
|
3720
4723
|
});
|
|
3721
4724
|
if (result.success && !result.isDuplicate) {
|
|
3722
|
-
await this.
|
|
4725
|
+
await this.sqliteStore.enqueueForEmbedding(result.eventId, summary);
|
|
3723
4726
|
}
|
|
3724
4727
|
return result;
|
|
3725
4728
|
}
|
|
@@ -3729,7 +4732,7 @@ var MemoryService = class {
|
|
|
3729
4732
|
async storeToolObservation(sessionId, payload) {
|
|
3730
4733
|
await this.initialize();
|
|
3731
4734
|
const content = JSON.stringify(payload);
|
|
3732
|
-
const result = await this.
|
|
4735
|
+
const result = await this.sqliteStore.append({
|
|
3733
4736
|
eventType: "tool_observation",
|
|
3734
4737
|
sessionId,
|
|
3735
4738
|
timestamp: /* @__PURE__ */ new Date(),
|
|
@@ -3745,7 +4748,7 @@ var MemoryService = class {
|
|
|
3745
4748
|
payload.metadata || {},
|
|
3746
4749
|
payload.success
|
|
3747
4750
|
);
|
|
3748
|
-
await this.
|
|
4751
|
+
await this.sqliteStore.enqueueForEmbedding(result.eventId, embeddingContent);
|
|
3749
4752
|
}
|
|
3750
4753
|
return result;
|
|
3751
4754
|
}
|
|
@@ -3771,21 +4774,21 @@ var MemoryService = class {
|
|
|
3771
4774
|
*/
|
|
3772
4775
|
async getSessionHistory(sessionId) {
|
|
3773
4776
|
await this.initialize();
|
|
3774
|
-
return this.
|
|
4777
|
+
return this.sqliteStore.getSessionEvents(sessionId);
|
|
3775
4778
|
}
|
|
3776
4779
|
/**
|
|
3777
4780
|
* Get recent events
|
|
3778
4781
|
*/
|
|
3779
4782
|
async getRecentEvents(limit = 100) {
|
|
3780
4783
|
await this.initialize();
|
|
3781
|
-
return this.
|
|
4784
|
+
return this.sqliteStore.getRecentEvents(limit);
|
|
3782
4785
|
}
|
|
3783
4786
|
/**
|
|
3784
4787
|
* Get memory statistics
|
|
3785
4788
|
*/
|
|
3786
4789
|
async getStats() {
|
|
3787
4790
|
await this.initialize();
|
|
3788
|
-
const recentEvents = await this.
|
|
4791
|
+
const recentEvents = await this.sqliteStore.getRecentEvents(1e4);
|
|
3789
4792
|
const vectorCount = await this.vectorStore.count();
|
|
3790
4793
|
const levelStats = await this.graduation.getStats();
|
|
3791
4794
|
return {
|
|
@@ -3808,14 +4811,14 @@ var MemoryService = class {
|
|
|
3808
4811
|
*/
|
|
3809
4812
|
async getEventsByLevel(level, options) {
|
|
3810
4813
|
await this.initialize();
|
|
3811
|
-
return this.
|
|
4814
|
+
return this.sqliteStore.getEventsByLevel(level, options);
|
|
3812
4815
|
}
|
|
3813
4816
|
/**
|
|
3814
4817
|
* Get memory level for a specific event
|
|
3815
4818
|
*/
|
|
3816
4819
|
async getEventLevel(eventId) {
|
|
3817
4820
|
await this.initialize();
|
|
3818
|
-
return this.
|
|
4821
|
+
return this.sqliteStore.getEventLevel(eventId);
|
|
3819
4822
|
}
|
|
3820
4823
|
/**
|
|
3821
4824
|
* Format retrieval results as context for Claude
|
|
@@ -3909,21 +4912,21 @@ var MemoryService = class {
|
|
|
3909
4912
|
*/
|
|
3910
4913
|
async initializeEndlessMode() {
|
|
3911
4914
|
const config = await this.getEndlessConfig();
|
|
3912
|
-
this.workingSetStore = createWorkingSetStore(this.
|
|
3913
|
-
this.consolidatedStore = createConsolidatedStore(this.
|
|
4915
|
+
this.workingSetStore = createWorkingSetStore(this.sqliteStore, config);
|
|
4916
|
+
this.consolidatedStore = createConsolidatedStore(this.sqliteStore);
|
|
3914
4917
|
this.consolidationWorker = createConsolidationWorker(
|
|
3915
4918
|
this.workingSetStore,
|
|
3916
4919
|
this.consolidatedStore,
|
|
3917
4920
|
config
|
|
3918
4921
|
);
|
|
3919
|
-
this.continuityManager = createContinuityManager(this.
|
|
4922
|
+
this.continuityManager = createContinuityManager(this.sqliteStore, config);
|
|
3920
4923
|
this.consolidationWorker.start();
|
|
3921
4924
|
}
|
|
3922
4925
|
/**
|
|
3923
4926
|
* Get Endless Mode configuration
|
|
3924
4927
|
*/
|
|
3925
4928
|
async getEndlessConfig() {
|
|
3926
|
-
const savedConfig = await this.
|
|
4929
|
+
const savedConfig = await this.sqliteStore.getEndlessConfig("config");
|
|
3927
4930
|
return savedConfig || this.getDefaultEndlessConfig();
|
|
3928
4931
|
}
|
|
3929
4932
|
/**
|
|
@@ -3932,7 +4935,7 @@ var MemoryService = class {
|
|
|
3932
4935
|
async setEndlessConfig(config) {
|
|
3933
4936
|
const current = await this.getEndlessConfig();
|
|
3934
4937
|
const merged = { ...current, ...config };
|
|
3935
|
-
await this.
|
|
4938
|
+
await this.sqliteStore.setEndlessConfig("config", merged);
|
|
3936
4939
|
}
|
|
3937
4940
|
/**
|
|
3938
4941
|
* Set memory mode (session or endless)
|
|
@@ -3942,7 +4945,7 @@ var MemoryService = class {
|
|
|
3942
4945
|
if (mode === this.endlessMode)
|
|
3943
4946
|
return;
|
|
3944
4947
|
this.endlessMode = mode;
|
|
3945
|
-
await this.
|
|
4948
|
+
await this.sqliteStore.setEndlessConfig("mode", mode);
|
|
3946
4949
|
if (mode === "endless") {
|
|
3947
4950
|
await this.initializeEndlessMode();
|
|
3948
4951
|
} else {
|
|
@@ -4000,12 +5003,49 @@ var MemoryService = class {
|
|
|
4000
5003
|
return this.consolidatedStore.getAll({ limit });
|
|
4001
5004
|
}
|
|
4002
5005
|
/**
|
|
4003
|
-
*
|
|
5006
|
+
* Increment access count for memories that were used in prompts
|
|
5007
|
+
*/
|
|
5008
|
+
async incrementMemoryAccess(eventIds) {
|
|
5009
|
+
if (eventIds.length === 0)
|
|
5010
|
+
return;
|
|
5011
|
+
if (this.sqliteStore) {
|
|
5012
|
+
await this.sqliteStore.incrementAccessCount(eventIds);
|
|
5013
|
+
} else if (this.eventStore) {
|
|
5014
|
+
await this.eventStore.incrementAccessCount(eventIds);
|
|
5015
|
+
}
|
|
5016
|
+
}
|
|
5017
|
+
/**
|
|
5018
|
+
* Get most accessed memories from events
|
|
4004
5019
|
*/
|
|
4005
5020
|
async getMostAccessedMemories(limit = 10) {
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
5021
|
+
console.log("[getMostAccessedMemories] sqliteStore available:", !!this.sqliteStore);
|
|
5022
|
+
if (this.sqliteStore) {
|
|
5023
|
+
const events = await this.sqliteStore.getMostAccessed(limit);
|
|
5024
|
+
console.log("[getMostAccessedMemories] Got events from SQLite:", events.length);
|
|
5025
|
+
return events.map((event) => ({
|
|
5026
|
+
memoryId: event.id,
|
|
5027
|
+
summary: event.content.substring(0, 200) + (event.content.length > 200 ? "..." : ""),
|
|
5028
|
+
topics: [],
|
|
5029
|
+
// Could extract topics from content if needed
|
|
5030
|
+
accessCount: event.access_count || 0,
|
|
5031
|
+
lastAccessed: event.last_accessed_at || null,
|
|
5032
|
+
confidence: 1,
|
|
5033
|
+
createdAt: event.timestamp
|
|
5034
|
+
}));
|
|
5035
|
+
}
|
|
5036
|
+
if (this.consolidatedStore) {
|
|
5037
|
+
const consolidated = await this.consolidatedStore.getMostAccessed(limit);
|
|
5038
|
+
return consolidated.map((m) => ({
|
|
5039
|
+
memoryId: m.memoryId,
|
|
5040
|
+
summary: m.summary,
|
|
5041
|
+
topics: m.topics,
|
|
5042
|
+
accessCount: m.accessCount,
|
|
5043
|
+
lastAccessed: m.accessedAt,
|
|
5044
|
+
confidence: m.confidence,
|
|
5045
|
+
createdAt: m.createdAt
|
|
5046
|
+
}));
|
|
5047
|
+
}
|
|
5048
|
+
return [];
|
|
4009
5049
|
}
|
|
4010
5050
|
/**
|
|
4011
5051
|
* Mark a consolidated memory as accessed
|
|
@@ -4130,10 +5170,16 @@ var MemoryService = class {
|
|
|
4130
5170
|
if (this.vectorWorker) {
|
|
4131
5171
|
this.vectorWorker.stop();
|
|
4132
5172
|
}
|
|
5173
|
+
if (this.syncWorker) {
|
|
5174
|
+
this.syncWorker.stop();
|
|
5175
|
+
}
|
|
4133
5176
|
if (this.sharedEventStore) {
|
|
4134
5177
|
await this.sharedEventStore.close();
|
|
4135
5178
|
}
|
|
4136
|
-
await this.
|
|
5179
|
+
await this.sqliteStore.close();
|
|
5180
|
+
if (this.analyticsStore) {
|
|
5181
|
+
await this.analyticsStore.close();
|
|
5182
|
+
}
|
|
4137
5183
|
}
|
|
4138
5184
|
/**
|
|
4139
5185
|
* Expand ~ to home directory
|
|
@@ -4150,7 +5196,11 @@ var GLOBAL_KEY = "__global__";
|
|
|
4150
5196
|
function getDefaultMemoryService() {
|
|
4151
5197
|
if (!serviceCache.has(GLOBAL_KEY)) {
|
|
4152
5198
|
serviceCache.set(GLOBAL_KEY, new MemoryService({
|
|
4153
|
-
storagePath: "~/.claude-code/memory"
|
|
5199
|
+
storagePath: "~/.claude-code/memory",
|
|
5200
|
+
analyticsEnabled: false,
|
|
5201
|
+
// Hooks don't need DuckDB
|
|
5202
|
+
sharedStoreConfig: { enabled: false }
|
|
5203
|
+
// Shared store uses DuckDB too
|
|
4154
5204
|
}));
|
|
4155
5205
|
}
|
|
4156
5206
|
return serviceCache.get(GLOBAL_KEY);
|
|
@@ -4158,7 +5208,11 @@ function getDefaultMemoryService() {
|
|
|
4158
5208
|
function getReadOnlyMemoryService() {
|
|
4159
5209
|
return new MemoryService({
|
|
4160
5210
|
storagePath: "~/.claude-code/memory",
|
|
4161
|
-
readOnly: true
|
|
5211
|
+
readOnly: true,
|
|
5212
|
+
analyticsEnabled: false,
|
|
5213
|
+
// Use SQLite for reads (WAL supports concurrent readers)
|
|
5214
|
+
sharedStoreConfig: { enabled: false }
|
|
5215
|
+
// Skip shared store for now
|
|
4162
5216
|
});
|
|
4163
5217
|
}
|
|
4164
5218
|
function getMemoryServiceForProject(projectPath, sharedStoreConfig) {
|
|
@@ -4168,7 +5222,10 @@ function getMemoryServiceForProject(projectPath, sharedStoreConfig) {
|
|
|
4168
5222
|
serviceCache.set(hash, new MemoryService({
|
|
4169
5223
|
storagePath,
|
|
4170
5224
|
projectHash: hash,
|
|
4171
|
-
|
|
5225
|
+
// Override shared store config - hooks don't need DuckDB
|
|
5226
|
+
sharedStoreConfig: sharedStoreConfig ?? { enabled: false },
|
|
5227
|
+
analyticsEnabled: false
|
|
5228
|
+
// Hooks don't need DuckDB
|
|
4172
5229
|
}));
|
|
4173
5230
|
}
|
|
4174
5231
|
return serviceCache.get(hash);
|
|
@@ -4715,6 +5772,7 @@ statsRouter.get("/levels/:level", async (c) => {
|
|
|
4715
5772
|
const { level } = c.req.param();
|
|
4716
5773
|
const limit = parseInt(c.req.query("limit") || "20", 10);
|
|
4717
5774
|
const offset = parseInt(c.req.query("offset") || "0", 10);
|
|
5775
|
+
const sort = c.req.query("sort") || "recent";
|
|
4718
5776
|
const validLevels = ["L0", "L1", "L2", "L3", "L4"];
|
|
4719
5777
|
if (!validLevels.includes(level)) {
|
|
4720
5778
|
return c.json({ error: `Invalid level. Must be one of: ${validLevels.join(", ")}` }, 400);
|
|
@@ -4722,9 +5780,27 @@ statsRouter.get("/levels/:level", async (c) => {
|
|
|
4722
5780
|
const memoryService = getReadOnlyMemoryService();
|
|
4723
5781
|
try {
|
|
4724
5782
|
await memoryService.initialize();
|
|
4725
|
-
|
|
5783
|
+
let events = await memoryService.getEventsByLevel(level, { limit: limit * 2, offset });
|
|
4726
5784
|
const stats = await memoryService.getStats();
|
|
4727
5785
|
const levelStat = stats.levelStats.find((s) => s.level === level);
|
|
5786
|
+
if (sort === "accessed") {
|
|
5787
|
+
const sqliteStore = memoryService.sqliteEventStore;
|
|
5788
|
+
if (sqliteStore) {
|
|
5789
|
+
const eventIds = events.map((e) => e.id);
|
|
5790
|
+
const accessedEvents = await sqliteStore.getMostAccessed(1e3);
|
|
5791
|
+
const accessMap = new Map(accessedEvents.map((e) => [e.id, e.access_count || 0]));
|
|
5792
|
+
events = events.map((e) => ({
|
|
5793
|
+
...e,
|
|
5794
|
+
accessCount: accessMap.get(e.id) || 0
|
|
5795
|
+
}));
|
|
5796
|
+
events.sort((a, b) => b.accessCount - a.accessCount);
|
|
5797
|
+
}
|
|
5798
|
+
} else if (sort === "oldest") {
|
|
5799
|
+
events.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
|
5800
|
+
} else {
|
|
5801
|
+
events.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
|
|
5802
|
+
}
|
|
5803
|
+
events = events.slice(0, limit);
|
|
4728
5804
|
return c.json({
|
|
4729
5805
|
level,
|
|
4730
5806
|
events: events.map((e) => ({
|
|
@@ -4733,7 +5809,8 @@ statsRouter.get("/levels/:level", async (c) => {
|
|
|
4733
5809
|
sessionId: e.sessionId,
|
|
4734
5810
|
timestamp: e.timestamp.toISOString(),
|
|
4735
5811
|
content: e.content.slice(0, 500) + (e.content.length > 500 ? "..." : ""),
|
|
4736
|
-
metadata: e.metadata
|
|
5812
|
+
metadata: e.metadata,
|
|
5813
|
+
accessCount: e.accessCount || 0
|
|
4737
5814
|
})),
|
|
4738
5815
|
total: levelStat?.count || 0,
|
|
4739
5816
|
limit,
|
|
@@ -4791,24 +5868,26 @@ statsRouter.get("/", async (c) => {
|
|
|
4791
5868
|
});
|
|
4792
5869
|
statsRouter.get("/most-accessed", async (c) => {
|
|
4793
5870
|
const limit = parseInt(c.req.query("limit") || "10", 10);
|
|
4794
|
-
const
|
|
4795
|
-
const memoryService = getMemoryServiceForProject(projectPath);
|
|
5871
|
+
const memoryService = getReadOnlyMemoryService();
|
|
4796
5872
|
try {
|
|
4797
5873
|
await memoryService.initialize();
|
|
5874
|
+
console.log("[most-accessed] Fetching most accessed memories, limit:", limit);
|
|
4798
5875
|
const memories = await memoryService.getMostAccessedMemories(limit);
|
|
5876
|
+
console.log("[most-accessed] Got memories:", memories.length);
|
|
4799
5877
|
return c.json({
|
|
4800
5878
|
memories: memories.map((m) => ({
|
|
4801
5879
|
memoryId: m.memoryId,
|
|
4802
5880
|
summary: m.summary,
|
|
4803
5881
|
topics: m.topics,
|
|
4804
5882
|
accessCount: m.accessCount,
|
|
4805
|
-
lastAccessed: m.
|
|
5883
|
+
lastAccessed: m.lastAccessed || null,
|
|
4806
5884
|
confidence: m.confidence,
|
|
4807
|
-
createdAt: m.createdAt.toISOString()
|
|
5885
|
+
createdAt: m.createdAt instanceof Date ? m.createdAt.toISOString() : m.createdAt
|
|
4808
5886
|
})),
|
|
4809
5887
|
total: memories.length
|
|
4810
5888
|
});
|
|
4811
5889
|
} catch (error) {
|
|
5890
|
+
console.error("[most-accessed] Error:", error);
|
|
4812
5891
|
return c.json({
|
|
4813
5892
|
memories: [],
|
|
4814
5893
|
total: 0,
|