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/server/index.js
CHANGED
|
@@ -715,26 +715,975 @@ var EventStore = class {
|
|
|
715
715
|
tags: row.tags ? JSON.parse(row.tags) : void 0
|
|
716
716
|
}));
|
|
717
717
|
}
|
|
718
|
+
/**
|
|
719
|
+
* Increment access count for events (stub for compatibility)
|
|
720
|
+
*/
|
|
721
|
+
async incrementAccessCount(eventIds) {
|
|
722
|
+
return Promise.resolve();
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Get most accessed memories (stub for compatibility)
|
|
726
|
+
*/
|
|
727
|
+
async getMostAccessed(limit = 10) {
|
|
728
|
+
return [];
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Close database connection
|
|
732
|
+
*/
|
|
733
|
+
async close() {
|
|
734
|
+
await dbClose(this.db);
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* Convert database row to MemoryEvent
|
|
738
|
+
*/
|
|
739
|
+
rowToEvent(row) {
|
|
740
|
+
return {
|
|
741
|
+
id: row.id,
|
|
742
|
+
eventType: row.event_type,
|
|
743
|
+
sessionId: row.session_id,
|
|
744
|
+
timestamp: toDate(row.timestamp),
|
|
745
|
+
content: row.content,
|
|
746
|
+
canonicalKey: row.canonical_key,
|
|
747
|
+
dedupeKey: row.dedupe_key,
|
|
748
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : void 0
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
};
|
|
752
|
+
|
|
753
|
+
// src/core/sqlite-event-store.ts
|
|
754
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
755
|
+
|
|
756
|
+
// src/core/sqlite-wrapper.ts
|
|
757
|
+
import Database from "better-sqlite3";
|
|
758
|
+
function createSQLiteDatabase(path3, options) {
|
|
759
|
+
const db = new Database(path3, {
|
|
760
|
+
readonly: options?.readonly ?? false
|
|
761
|
+
});
|
|
762
|
+
if (!options?.readonly && (options?.walMode ?? true)) {
|
|
763
|
+
db.pragma("journal_mode = WAL");
|
|
764
|
+
db.pragma("synchronous = NORMAL");
|
|
765
|
+
db.pragma("busy_timeout = 5000");
|
|
766
|
+
}
|
|
767
|
+
return db;
|
|
768
|
+
}
|
|
769
|
+
function sqliteRun(db, sql, params = []) {
|
|
770
|
+
const stmt = db.prepare(sql);
|
|
771
|
+
return stmt.run(...params);
|
|
772
|
+
}
|
|
773
|
+
function sqliteAll(db, sql, params = []) {
|
|
774
|
+
const stmt = db.prepare(sql);
|
|
775
|
+
return stmt.all(...params);
|
|
776
|
+
}
|
|
777
|
+
function sqliteGet(db, sql, params = []) {
|
|
778
|
+
const stmt = db.prepare(sql);
|
|
779
|
+
return stmt.get(...params);
|
|
780
|
+
}
|
|
781
|
+
function sqliteExec(db, sql) {
|
|
782
|
+
db.exec(sql);
|
|
783
|
+
}
|
|
784
|
+
function sqliteClose(db) {
|
|
785
|
+
db.close();
|
|
786
|
+
}
|
|
787
|
+
function toDateFromSQLite(value) {
|
|
788
|
+
if (value instanceof Date)
|
|
789
|
+
return value;
|
|
790
|
+
if (typeof value === "string")
|
|
791
|
+
return new Date(value);
|
|
792
|
+
if (typeof value === "number")
|
|
793
|
+
return new Date(value);
|
|
794
|
+
return new Date(String(value));
|
|
795
|
+
}
|
|
796
|
+
function toSQLiteTimestamp(date) {
|
|
797
|
+
return date.toISOString();
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// src/core/sqlite-event-store.ts
|
|
801
|
+
var SQLiteEventStore = class {
|
|
802
|
+
constructor(dbPath, options) {
|
|
803
|
+
this.dbPath = dbPath;
|
|
804
|
+
this.readOnly = options?.readonly ?? false;
|
|
805
|
+
this.db = createSQLiteDatabase(dbPath, {
|
|
806
|
+
readonly: this.readOnly,
|
|
807
|
+
walMode: !this.readOnly
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
db;
|
|
811
|
+
initialized = false;
|
|
812
|
+
readOnly;
|
|
813
|
+
/**
|
|
814
|
+
* Initialize database schema
|
|
815
|
+
*/
|
|
816
|
+
async initialize() {
|
|
817
|
+
if (this.initialized)
|
|
818
|
+
return;
|
|
819
|
+
if (this.readOnly) {
|
|
820
|
+
this.initialized = true;
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
sqliteExec(this.db, `
|
|
824
|
+
-- L0 EventStore: Single Source of Truth (immutable, append-only)
|
|
825
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
826
|
+
id TEXT PRIMARY KEY,
|
|
827
|
+
event_type TEXT NOT NULL,
|
|
828
|
+
session_id TEXT NOT NULL,
|
|
829
|
+
timestamp TEXT NOT NULL,
|
|
830
|
+
content TEXT NOT NULL,
|
|
831
|
+
canonical_key TEXT NOT NULL,
|
|
832
|
+
dedupe_key TEXT UNIQUE,
|
|
833
|
+
metadata TEXT,
|
|
834
|
+
access_count INTEGER DEFAULT 0,
|
|
835
|
+
last_accessed_at TEXT
|
|
836
|
+
);
|
|
837
|
+
|
|
838
|
+
-- Dedup table for idempotency
|
|
839
|
+
CREATE TABLE IF NOT EXISTS event_dedup (
|
|
840
|
+
dedupe_key TEXT PRIMARY KEY,
|
|
841
|
+
event_id TEXT NOT NULL,
|
|
842
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
843
|
+
);
|
|
844
|
+
|
|
845
|
+
-- Session metadata
|
|
846
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
847
|
+
id TEXT PRIMARY KEY,
|
|
848
|
+
started_at TEXT NOT NULL,
|
|
849
|
+
ended_at TEXT,
|
|
850
|
+
project_path TEXT,
|
|
851
|
+
summary TEXT,
|
|
852
|
+
tags TEXT
|
|
853
|
+
);
|
|
854
|
+
|
|
855
|
+
-- Insights (derived data, rebuildable)
|
|
856
|
+
CREATE TABLE IF NOT EXISTS insights (
|
|
857
|
+
id TEXT PRIMARY KEY,
|
|
858
|
+
insight_type TEXT NOT NULL,
|
|
859
|
+
content TEXT NOT NULL,
|
|
860
|
+
canonical_key TEXT NOT NULL,
|
|
861
|
+
confidence REAL,
|
|
862
|
+
source_events TEXT,
|
|
863
|
+
created_at TEXT,
|
|
864
|
+
last_updated TEXT
|
|
865
|
+
);
|
|
866
|
+
|
|
867
|
+
-- Embedding Outbox (Single-Writer Pattern)
|
|
868
|
+
CREATE TABLE IF NOT EXISTS embedding_outbox (
|
|
869
|
+
id TEXT PRIMARY KEY,
|
|
870
|
+
event_id TEXT NOT NULL,
|
|
871
|
+
content TEXT NOT NULL,
|
|
872
|
+
status TEXT DEFAULT 'pending',
|
|
873
|
+
retry_count INTEGER DEFAULT 0,
|
|
874
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
875
|
+
processed_at TEXT,
|
|
876
|
+
error_message TEXT
|
|
877
|
+
);
|
|
878
|
+
|
|
879
|
+
-- Projection offset tracking
|
|
880
|
+
CREATE TABLE IF NOT EXISTS projection_offsets (
|
|
881
|
+
projection_name TEXT PRIMARY KEY,
|
|
882
|
+
last_event_id TEXT,
|
|
883
|
+
last_timestamp TEXT,
|
|
884
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
885
|
+
);
|
|
886
|
+
|
|
887
|
+
-- Memory level tracking
|
|
888
|
+
CREATE TABLE IF NOT EXISTS memory_levels (
|
|
889
|
+
event_id TEXT PRIMARY KEY,
|
|
890
|
+
level TEXT NOT NULL DEFAULT 'L0',
|
|
891
|
+
promoted_at TEXT DEFAULT (datetime('now'))
|
|
892
|
+
);
|
|
893
|
+
|
|
894
|
+
-- Entries (immutable memory units)
|
|
895
|
+
CREATE TABLE IF NOT EXISTS entries (
|
|
896
|
+
entry_id TEXT PRIMARY KEY,
|
|
897
|
+
created_ts TEXT NOT NULL,
|
|
898
|
+
entry_type TEXT NOT NULL,
|
|
899
|
+
title TEXT NOT NULL,
|
|
900
|
+
content_json TEXT NOT NULL,
|
|
901
|
+
stage TEXT NOT NULL DEFAULT 'raw',
|
|
902
|
+
status TEXT DEFAULT 'active',
|
|
903
|
+
superseded_by TEXT,
|
|
904
|
+
build_id TEXT,
|
|
905
|
+
evidence_json TEXT,
|
|
906
|
+
canonical_key TEXT,
|
|
907
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
908
|
+
);
|
|
909
|
+
|
|
910
|
+
-- Entities (task/condition/artifact)
|
|
911
|
+
CREATE TABLE IF NOT EXISTS entities (
|
|
912
|
+
entity_id TEXT PRIMARY KEY,
|
|
913
|
+
entity_type TEXT NOT NULL,
|
|
914
|
+
canonical_key TEXT NOT NULL,
|
|
915
|
+
title TEXT NOT NULL,
|
|
916
|
+
stage TEXT NOT NULL DEFAULT 'raw',
|
|
917
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
918
|
+
current_json TEXT NOT NULL,
|
|
919
|
+
title_norm TEXT,
|
|
920
|
+
search_text TEXT,
|
|
921
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
922
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
923
|
+
);
|
|
924
|
+
|
|
925
|
+
-- Entity aliases for canonical key lookup
|
|
926
|
+
CREATE TABLE IF NOT EXISTS entity_aliases (
|
|
927
|
+
entity_type TEXT NOT NULL,
|
|
928
|
+
canonical_key TEXT NOT NULL,
|
|
929
|
+
entity_id TEXT NOT NULL,
|
|
930
|
+
is_primary INTEGER DEFAULT 0,
|
|
931
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
932
|
+
PRIMARY KEY(entity_type, canonical_key)
|
|
933
|
+
);
|
|
934
|
+
|
|
935
|
+
-- Edges (relationships between entries/entities)
|
|
936
|
+
CREATE TABLE IF NOT EXISTS edges (
|
|
937
|
+
edge_id TEXT PRIMARY KEY,
|
|
938
|
+
src_type TEXT NOT NULL,
|
|
939
|
+
src_id TEXT NOT NULL,
|
|
940
|
+
rel_type TEXT NOT NULL,
|
|
941
|
+
dst_type TEXT NOT NULL,
|
|
942
|
+
dst_id TEXT NOT NULL,
|
|
943
|
+
meta_json TEXT,
|
|
944
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
945
|
+
);
|
|
946
|
+
|
|
947
|
+
-- Vector Outbox V2 Table
|
|
948
|
+
CREATE TABLE IF NOT EXISTS vector_outbox (
|
|
949
|
+
job_id TEXT PRIMARY KEY,
|
|
950
|
+
item_kind TEXT NOT NULL,
|
|
951
|
+
item_id TEXT NOT NULL,
|
|
952
|
+
embedding_version TEXT NOT NULL,
|
|
953
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
954
|
+
retry_count INTEGER DEFAULT 0,
|
|
955
|
+
error TEXT,
|
|
956
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
957
|
+
updated_at TEXT DEFAULT (datetime('now')),
|
|
958
|
+
UNIQUE(item_kind, item_id, embedding_version)
|
|
959
|
+
);
|
|
960
|
+
|
|
961
|
+
-- Build Runs
|
|
962
|
+
CREATE TABLE IF NOT EXISTS build_runs (
|
|
963
|
+
build_id TEXT PRIMARY KEY,
|
|
964
|
+
started_at TEXT NOT NULL,
|
|
965
|
+
finished_at TEXT,
|
|
966
|
+
extractor_model TEXT NOT NULL,
|
|
967
|
+
extractor_prompt_hash TEXT NOT NULL,
|
|
968
|
+
embedder_model TEXT NOT NULL,
|
|
969
|
+
embedding_version TEXT NOT NULL,
|
|
970
|
+
idris_version TEXT NOT NULL,
|
|
971
|
+
schema_version TEXT NOT NULL,
|
|
972
|
+
status TEXT NOT NULL DEFAULT 'running',
|
|
973
|
+
error TEXT
|
|
974
|
+
);
|
|
975
|
+
|
|
976
|
+
-- Pipeline Metrics
|
|
977
|
+
CREATE TABLE IF NOT EXISTS pipeline_metrics (
|
|
978
|
+
id TEXT PRIMARY KEY,
|
|
979
|
+
ts TEXT NOT NULL,
|
|
980
|
+
stage TEXT NOT NULL,
|
|
981
|
+
latency_ms REAL NOT NULL,
|
|
982
|
+
success INTEGER NOT NULL,
|
|
983
|
+
error TEXT,
|
|
984
|
+
session_id TEXT
|
|
985
|
+
);
|
|
986
|
+
|
|
987
|
+
-- Working Set table (active memory window)
|
|
988
|
+
CREATE TABLE IF NOT EXISTS working_set (
|
|
989
|
+
id TEXT PRIMARY KEY,
|
|
990
|
+
event_id TEXT NOT NULL,
|
|
991
|
+
added_at TEXT DEFAULT (datetime('now')),
|
|
992
|
+
relevance_score REAL DEFAULT 1.0,
|
|
993
|
+
topics TEXT,
|
|
994
|
+
expires_at TEXT
|
|
995
|
+
);
|
|
996
|
+
|
|
997
|
+
-- Consolidated Memories table (long-term integrated memories)
|
|
998
|
+
CREATE TABLE IF NOT EXISTS consolidated_memories (
|
|
999
|
+
memory_id TEXT PRIMARY KEY,
|
|
1000
|
+
summary TEXT NOT NULL,
|
|
1001
|
+
topics TEXT,
|
|
1002
|
+
source_events TEXT,
|
|
1003
|
+
confidence REAL DEFAULT 0.5,
|
|
1004
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
1005
|
+
accessed_at TEXT,
|
|
1006
|
+
access_count INTEGER DEFAULT 0
|
|
1007
|
+
);
|
|
1008
|
+
|
|
1009
|
+
-- Continuity Log table (tracks context transitions)
|
|
1010
|
+
CREATE TABLE IF NOT EXISTS continuity_log (
|
|
1011
|
+
log_id TEXT PRIMARY KEY,
|
|
1012
|
+
from_context_id TEXT,
|
|
1013
|
+
to_context_id TEXT,
|
|
1014
|
+
continuity_score REAL,
|
|
1015
|
+
transition_type TEXT,
|
|
1016
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
1017
|
+
);
|
|
1018
|
+
|
|
1019
|
+
-- Endless Mode Config table
|
|
1020
|
+
CREATE TABLE IF NOT EXISTS endless_config (
|
|
1021
|
+
key TEXT PRIMARY KEY,
|
|
1022
|
+
value TEXT,
|
|
1023
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
1024
|
+
);
|
|
1025
|
+
|
|
1026
|
+
-- Sync position tracking (for SQLite -> DuckDB sync)
|
|
1027
|
+
CREATE TABLE IF NOT EXISTS sync_positions (
|
|
1028
|
+
target_name TEXT PRIMARY KEY,
|
|
1029
|
+
last_event_id TEXT,
|
|
1030
|
+
last_timestamp TEXT,
|
|
1031
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
1032
|
+
);
|
|
1033
|
+
|
|
1034
|
+
-- Create indexes
|
|
1035
|
+
CREATE INDEX IF NOT EXISTS idx_events_session ON events(session_id);
|
|
1036
|
+
CREATE INDEX IF NOT EXISTS idx_events_timestamp ON events(timestamp);
|
|
1037
|
+
CREATE INDEX IF NOT EXISTS idx_entries_type ON entries(entry_type);
|
|
1038
|
+
CREATE INDEX IF NOT EXISTS idx_entries_stage ON entries(stage);
|
|
1039
|
+
CREATE INDEX IF NOT EXISTS idx_entries_canonical ON entries(canonical_key);
|
|
1040
|
+
CREATE INDEX IF NOT EXISTS idx_entities_type_key ON entities(entity_type, canonical_key);
|
|
1041
|
+
CREATE INDEX IF NOT EXISTS idx_entities_status ON entities(status);
|
|
1042
|
+
CREATE INDEX IF NOT EXISTS idx_edges_src ON edges(src_id, rel_type);
|
|
1043
|
+
CREATE INDEX IF NOT EXISTS idx_edges_dst ON edges(dst_id, rel_type);
|
|
1044
|
+
CREATE INDEX IF NOT EXISTS idx_edges_rel ON edges(rel_type);
|
|
1045
|
+
CREATE INDEX IF NOT EXISTS idx_outbox_status ON vector_outbox(status);
|
|
1046
|
+
CREATE INDEX IF NOT EXISTS idx_working_set_expires ON working_set(expires_at);
|
|
1047
|
+
CREATE INDEX IF NOT EXISTS idx_working_set_relevance ON working_set(relevance_score);
|
|
1048
|
+
CREATE INDEX IF NOT EXISTS idx_consolidated_confidence ON consolidated_memories(confidence);
|
|
1049
|
+
CREATE INDEX IF NOT EXISTS idx_continuity_created ON continuity_log(created_at);
|
|
1050
|
+
CREATE INDEX IF NOT EXISTS idx_embedding_outbox_status ON embedding_outbox(status);
|
|
1051
|
+
`);
|
|
1052
|
+
const tableInfo = sqliteAll(this.db, "PRAGMA table_info(events)", []);
|
|
1053
|
+
const columnNames = tableInfo.map((col) => col.name);
|
|
1054
|
+
if (!columnNames.includes("access_count")) {
|
|
1055
|
+
try {
|
|
1056
|
+
sqliteExec(this.db, `
|
|
1057
|
+
ALTER TABLE events ADD COLUMN access_count INTEGER DEFAULT 0;
|
|
1058
|
+
`);
|
|
1059
|
+
} catch (err) {
|
|
1060
|
+
console.error("Error adding access_count column:", err);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
if (!columnNames.includes("last_accessed_at")) {
|
|
1064
|
+
try {
|
|
1065
|
+
sqliteExec(this.db, `
|
|
1066
|
+
ALTER TABLE events ADD COLUMN last_accessed_at TEXT;
|
|
1067
|
+
`);
|
|
1068
|
+
} catch (err) {
|
|
1069
|
+
console.error("Error adding last_accessed_at column:", err);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
try {
|
|
1073
|
+
sqliteExec(this.db, `
|
|
1074
|
+
CREATE INDEX IF NOT EXISTS idx_events_access_count ON events(access_count DESC);
|
|
1075
|
+
`);
|
|
1076
|
+
} catch (err) {
|
|
1077
|
+
}
|
|
1078
|
+
try {
|
|
1079
|
+
sqliteExec(this.db, `
|
|
1080
|
+
CREATE INDEX IF NOT EXISTS idx_events_last_accessed ON events(last_accessed_at DESC);
|
|
1081
|
+
`);
|
|
1082
|
+
} catch (err) {
|
|
1083
|
+
}
|
|
1084
|
+
this.initialized = true;
|
|
1085
|
+
}
|
|
1086
|
+
/**
|
|
1087
|
+
* Append event to store (Append-only, Idempotent)
|
|
1088
|
+
*/
|
|
1089
|
+
async append(input) {
|
|
1090
|
+
await this.initialize();
|
|
1091
|
+
const canonicalKey = makeCanonicalKey(input.content);
|
|
1092
|
+
const dedupeKey = makeDedupeKey(input.content, input.sessionId);
|
|
1093
|
+
const existing = sqliteGet(
|
|
1094
|
+
this.db,
|
|
1095
|
+
`SELECT event_id FROM event_dedup WHERE dedupe_key = ?`,
|
|
1096
|
+
[dedupeKey]
|
|
1097
|
+
);
|
|
1098
|
+
if (existing) {
|
|
1099
|
+
return {
|
|
1100
|
+
success: true,
|
|
1101
|
+
eventId: existing.event_id,
|
|
1102
|
+
isDuplicate: true
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
const id = randomUUID2();
|
|
1106
|
+
const timestamp = toSQLiteTimestamp(input.timestamp);
|
|
1107
|
+
try {
|
|
1108
|
+
const insertEvent = this.db.prepare(`
|
|
1109
|
+
INSERT INTO events (id, event_type, session_id, timestamp, content, canonical_key, dedupe_key, metadata)
|
|
1110
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1111
|
+
`);
|
|
1112
|
+
const insertDedup = this.db.prepare(`
|
|
1113
|
+
INSERT INTO event_dedup (dedupe_key, event_id) VALUES (?, ?)
|
|
1114
|
+
`);
|
|
1115
|
+
const insertLevel = this.db.prepare(`
|
|
1116
|
+
INSERT INTO memory_levels (event_id, level) VALUES (?, 'L0')
|
|
1117
|
+
`);
|
|
1118
|
+
const transaction = this.db.transaction(() => {
|
|
1119
|
+
insertEvent.run(
|
|
1120
|
+
id,
|
|
1121
|
+
input.eventType,
|
|
1122
|
+
input.sessionId,
|
|
1123
|
+
timestamp,
|
|
1124
|
+
input.content,
|
|
1125
|
+
canonicalKey,
|
|
1126
|
+
dedupeKey,
|
|
1127
|
+
JSON.stringify(input.metadata || {})
|
|
1128
|
+
);
|
|
1129
|
+
insertDedup.run(dedupeKey, id);
|
|
1130
|
+
insertLevel.run(id);
|
|
1131
|
+
});
|
|
1132
|
+
transaction();
|
|
1133
|
+
return { success: true, eventId: id, isDuplicate: false };
|
|
1134
|
+
} catch (error) {
|
|
1135
|
+
return {
|
|
1136
|
+
success: false,
|
|
1137
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
/**
|
|
1142
|
+
* Get events by session ID
|
|
1143
|
+
*/
|
|
1144
|
+
async getSessionEvents(sessionId) {
|
|
1145
|
+
await this.initialize();
|
|
1146
|
+
const rows = sqliteAll(
|
|
1147
|
+
this.db,
|
|
1148
|
+
`SELECT * FROM events WHERE session_id = ? ORDER BY timestamp ASC`,
|
|
1149
|
+
[sessionId]
|
|
1150
|
+
);
|
|
1151
|
+
return rows.map(this.rowToEvent);
|
|
1152
|
+
}
|
|
1153
|
+
/**
|
|
1154
|
+
* Get recent events
|
|
1155
|
+
*/
|
|
1156
|
+
async getRecentEvents(limit = 100) {
|
|
1157
|
+
await this.initialize();
|
|
1158
|
+
const rows = sqliteAll(
|
|
1159
|
+
this.db,
|
|
1160
|
+
`SELECT * FROM events ORDER BY timestamp DESC LIMIT ?`,
|
|
1161
|
+
[limit]
|
|
1162
|
+
);
|
|
1163
|
+
return rows.map(this.rowToEvent);
|
|
1164
|
+
}
|
|
1165
|
+
/**
|
|
1166
|
+
* Get event by ID
|
|
1167
|
+
*/
|
|
1168
|
+
async getEvent(id) {
|
|
1169
|
+
await this.initialize();
|
|
1170
|
+
const row = sqliteGet(
|
|
1171
|
+
this.db,
|
|
1172
|
+
`SELECT * FROM events WHERE id = ?`,
|
|
1173
|
+
[id]
|
|
1174
|
+
);
|
|
1175
|
+
if (!row)
|
|
1176
|
+
return null;
|
|
1177
|
+
return this.rowToEvent(row);
|
|
1178
|
+
}
|
|
1179
|
+
/**
|
|
1180
|
+
* Get events since a timestamp (for sync)
|
|
1181
|
+
*/
|
|
1182
|
+
async getEventsSince(timestamp, limit = 1e3) {
|
|
1183
|
+
await this.initialize();
|
|
1184
|
+
const rows = sqliteAll(
|
|
1185
|
+
this.db,
|
|
1186
|
+
`SELECT * FROM events WHERE timestamp > ? ORDER BY timestamp ASC LIMIT ?`,
|
|
1187
|
+
[timestamp, limit]
|
|
1188
|
+
);
|
|
1189
|
+
return rows.map(this.rowToEvent);
|
|
1190
|
+
}
|
|
1191
|
+
/**
|
|
1192
|
+
* Create or update session
|
|
1193
|
+
*/
|
|
1194
|
+
async upsertSession(session) {
|
|
1195
|
+
await this.initialize();
|
|
1196
|
+
const existing = sqliteGet(
|
|
1197
|
+
this.db,
|
|
1198
|
+
`SELECT id FROM sessions WHERE id = ?`,
|
|
1199
|
+
[session.id]
|
|
1200
|
+
);
|
|
1201
|
+
if (!existing) {
|
|
1202
|
+
sqliteRun(
|
|
1203
|
+
this.db,
|
|
1204
|
+
`INSERT INTO sessions (id, started_at, project_path, tags)
|
|
1205
|
+
VALUES (?, ?, ?, ?)`,
|
|
1206
|
+
[
|
|
1207
|
+
session.id,
|
|
1208
|
+
toSQLiteTimestamp(session.startedAt || /* @__PURE__ */ new Date()),
|
|
1209
|
+
session.projectPath || null,
|
|
1210
|
+
JSON.stringify(session.tags || [])
|
|
1211
|
+
]
|
|
1212
|
+
);
|
|
1213
|
+
} else {
|
|
1214
|
+
const updates = [];
|
|
1215
|
+
const values = [];
|
|
1216
|
+
if (session.endedAt) {
|
|
1217
|
+
updates.push("ended_at = ?");
|
|
1218
|
+
values.push(toSQLiteTimestamp(session.endedAt));
|
|
1219
|
+
}
|
|
1220
|
+
if (session.summary) {
|
|
1221
|
+
updates.push("summary = ?");
|
|
1222
|
+
values.push(session.summary);
|
|
1223
|
+
}
|
|
1224
|
+
if (session.tags) {
|
|
1225
|
+
updates.push("tags = ?");
|
|
1226
|
+
values.push(JSON.stringify(session.tags));
|
|
1227
|
+
}
|
|
1228
|
+
if (updates.length > 0) {
|
|
1229
|
+
values.push(session.id);
|
|
1230
|
+
sqliteRun(
|
|
1231
|
+
this.db,
|
|
1232
|
+
`UPDATE sessions SET ${updates.join(", ")} WHERE id = ?`,
|
|
1233
|
+
values
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
/**
|
|
1239
|
+
* Get session by ID
|
|
1240
|
+
*/
|
|
1241
|
+
async getSession(id) {
|
|
1242
|
+
await this.initialize();
|
|
1243
|
+
const row = sqliteGet(
|
|
1244
|
+
this.db,
|
|
1245
|
+
`SELECT * FROM sessions WHERE id = ?`,
|
|
1246
|
+
[id]
|
|
1247
|
+
);
|
|
1248
|
+
if (!row)
|
|
1249
|
+
return null;
|
|
1250
|
+
return {
|
|
1251
|
+
id: row.id,
|
|
1252
|
+
startedAt: toDateFromSQLite(row.started_at),
|
|
1253
|
+
endedAt: row.ended_at ? toDateFromSQLite(row.ended_at) : void 0,
|
|
1254
|
+
projectPath: row.project_path,
|
|
1255
|
+
summary: row.summary,
|
|
1256
|
+
tags: row.tags ? JSON.parse(row.tags) : void 0
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
/**
|
|
1260
|
+
* Get all sessions
|
|
1261
|
+
*/
|
|
1262
|
+
async getAllSessions() {
|
|
1263
|
+
await this.initialize();
|
|
1264
|
+
const rows = sqliteAll(
|
|
1265
|
+
this.db,
|
|
1266
|
+
`SELECT * FROM sessions ORDER BY started_at DESC`
|
|
1267
|
+
);
|
|
1268
|
+
return rows.map((row) => ({
|
|
1269
|
+
id: row.id,
|
|
1270
|
+
startedAt: toDateFromSQLite(row.started_at),
|
|
1271
|
+
endedAt: row.ended_at ? toDateFromSQLite(row.ended_at) : void 0,
|
|
1272
|
+
projectPath: row.project_path,
|
|
1273
|
+
summary: row.summary,
|
|
1274
|
+
tags: row.tags ? JSON.parse(row.tags) : void 0
|
|
1275
|
+
}));
|
|
1276
|
+
}
|
|
1277
|
+
/**
|
|
1278
|
+
* Add to embedding outbox
|
|
1279
|
+
*/
|
|
1280
|
+
async enqueueForEmbedding(eventId, content) {
|
|
1281
|
+
await this.initialize();
|
|
1282
|
+
const id = randomUUID2();
|
|
1283
|
+
sqliteRun(
|
|
1284
|
+
this.db,
|
|
1285
|
+
`INSERT INTO embedding_outbox (id, event_id, content, status, retry_count)
|
|
1286
|
+
VALUES (?, ?, ?, 'pending', 0)`,
|
|
1287
|
+
[id, eventId, content]
|
|
1288
|
+
);
|
|
1289
|
+
return id;
|
|
1290
|
+
}
|
|
1291
|
+
/**
|
|
1292
|
+
* Get pending outbox items
|
|
1293
|
+
*/
|
|
1294
|
+
async getPendingOutboxItems(limit = 32) {
|
|
1295
|
+
await this.initialize();
|
|
1296
|
+
const pending = sqliteAll(
|
|
1297
|
+
this.db,
|
|
1298
|
+
`SELECT * FROM embedding_outbox
|
|
1299
|
+
WHERE status = 'pending'
|
|
1300
|
+
ORDER BY created_at
|
|
1301
|
+
LIMIT ?`,
|
|
1302
|
+
[limit]
|
|
1303
|
+
);
|
|
1304
|
+
if (pending.length === 0)
|
|
1305
|
+
return [];
|
|
1306
|
+
const ids = pending.map((r) => r.id);
|
|
1307
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
1308
|
+
sqliteRun(
|
|
1309
|
+
this.db,
|
|
1310
|
+
`UPDATE embedding_outbox SET status = 'processing' WHERE id IN (${placeholders})`,
|
|
1311
|
+
ids
|
|
1312
|
+
);
|
|
1313
|
+
return pending.map((row) => ({
|
|
1314
|
+
id: row.id,
|
|
1315
|
+
eventId: row.event_id,
|
|
1316
|
+
content: row.content,
|
|
1317
|
+
status: "processing",
|
|
1318
|
+
retryCount: row.retry_count,
|
|
1319
|
+
createdAt: toDateFromSQLite(row.created_at),
|
|
1320
|
+
errorMessage: row.error_message
|
|
1321
|
+
}));
|
|
1322
|
+
}
|
|
1323
|
+
/**
|
|
1324
|
+
* Mark outbox items as done
|
|
1325
|
+
*/
|
|
1326
|
+
async completeOutboxItems(ids) {
|
|
1327
|
+
if (ids.length === 0)
|
|
1328
|
+
return;
|
|
1329
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
1330
|
+
sqliteRun(
|
|
1331
|
+
this.db,
|
|
1332
|
+
`DELETE FROM embedding_outbox WHERE id IN (${placeholders})`,
|
|
1333
|
+
ids
|
|
1334
|
+
);
|
|
1335
|
+
}
|
|
1336
|
+
/**
|
|
1337
|
+
* Mark outbox items as failed
|
|
1338
|
+
*/
|
|
1339
|
+
async failOutboxItems(ids, error) {
|
|
1340
|
+
if (ids.length === 0)
|
|
1341
|
+
return;
|
|
1342
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
1343
|
+
sqliteRun(
|
|
1344
|
+
this.db,
|
|
1345
|
+
`UPDATE embedding_outbox
|
|
1346
|
+
SET status = CASE WHEN retry_count >= 3 THEN 'failed' ELSE 'pending' END,
|
|
1347
|
+
retry_count = retry_count + 1,
|
|
1348
|
+
error_message = ?
|
|
1349
|
+
WHERE id IN (${placeholders})`,
|
|
1350
|
+
[error, ...ids]
|
|
1351
|
+
);
|
|
1352
|
+
}
|
|
1353
|
+
/**
|
|
1354
|
+
* Update memory level
|
|
1355
|
+
*/
|
|
1356
|
+
async updateMemoryLevel(eventId, level) {
|
|
1357
|
+
await this.initialize();
|
|
1358
|
+
sqliteRun(
|
|
1359
|
+
this.db,
|
|
1360
|
+
`UPDATE memory_levels SET level = ?, promoted_at = datetime('now') WHERE event_id = ?`,
|
|
1361
|
+
[level, eventId]
|
|
1362
|
+
);
|
|
1363
|
+
}
|
|
1364
|
+
/**
|
|
1365
|
+
* Get memory level statistics
|
|
1366
|
+
*/
|
|
1367
|
+
async getLevelStats() {
|
|
1368
|
+
await this.initialize();
|
|
1369
|
+
const rows = sqliteAll(
|
|
1370
|
+
this.db,
|
|
1371
|
+
`SELECT level, COUNT(*) as count FROM memory_levels GROUP BY level`
|
|
1372
|
+
);
|
|
1373
|
+
return rows;
|
|
1374
|
+
}
|
|
1375
|
+
/**
|
|
1376
|
+
* Get events by memory level
|
|
1377
|
+
*/
|
|
1378
|
+
async getEventsByLevel(level, options) {
|
|
1379
|
+
await this.initialize();
|
|
1380
|
+
const limit = options?.limit || 50;
|
|
1381
|
+
const offset = options?.offset || 0;
|
|
1382
|
+
const rows = sqliteAll(
|
|
1383
|
+
this.db,
|
|
1384
|
+
`SELECT e.* FROM events e
|
|
1385
|
+
INNER JOIN memory_levels ml ON e.id = ml.event_id
|
|
1386
|
+
WHERE ml.level = ?
|
|
1387
|
+
ORDER BY e.timestamp DESC
|
|
1388
|
+
LIMIT ? OFFSET ?`,
|
|
1389
|
+
[level, limit, offset]
|
|
1390
|
+
);
|
|
1391
|
+
return rows.map((row) => this.rowToEvent(row));
|
|
1392
|
+
}
|
|
1393
|
+
/**
|
|
1394
|
+
* Get memory level for a specific event
|
|
1395
|
+
*/
|
|
1396
|
+
async getEventLevel(eventId) {
|
|
1397
|
+
await this.initialize();
|
|
1398
|
+
const row = sqliteGet(
|
|
1399
|
+
this.db,
|
|
1400
|
+
`SELECT level FROM memory_levels WHERE event_id = ?`,
|
|
1401
|
+
[eventId]
|
|
1402
|
+
);
|
|
1403
|
+
return row ? row.level : null;
|
|
1404
|
+
}
|
|
1405
|
+
/**
|
|
1406
|
+
* Get sync position for a target
|
|
1407
|
+
*/
|
|
1408
|
+
async getSyncPosition(targetName) {
|
|
1409
|
+
await this.initialize();
|
|
1410
|
+
const row = sqliteGet(
|
|
1411
|
+
this.db,
|
|
1412
|
+
`SELECT last_event_id, last_timestamp FROM sync_positions WHERE target_name = ?`,
|
|
1413
|
+
[targetName]
|
|
1414
|
+
);
|
|
1415
|
+
return {
|
|
1416
|
+
lastEventId: row?.last_event_id ?? null,
|
|
1417
|
+
lastTimestamp: row?.last_timestamp ?? null
|
|
1418
|
+
};
|
|
1419
|
+
}
|
|
1420
|
+
/**
|
|
1421
|
+
* Update sync position for a target
|
|
1422
|
+
*/
|
|
1423
|
+
async updateSyncPosition(targetName, lastEventId, lastTimestamp) {
|
|
1424
|
+
await this.initialize();
|
|
1425
|
+
sqliteRun(
|
|
1426
|
+
this.db,
|
|
1427
|
+
`INSERT OR REPLACE INTO sync_positions (target_name, last_event_id, last_timestamp, updated_at)
|
|
1428
|
+
VALUES (?, ?, ?, datetime('now'))`,
|
|
1429
|
+
[targetName, lastEventId, lastTimestamp]
|
|
1430
|
+
);
|
|
1431
|
+
}
|
|
1432
|
+
/**
|
|
1433
|
+
* Get config value for endless mode
|
|
1434
|
+
*/
|
|
1435
|
+
async getEndlessConfig(key) {
|
|
1436
|
+
await this.initialize();
|
|
1437
|
+
const row = sqliteGet(
|
|
1438
|
+
this.db,
|
|
1439
|
+
`SELECT value FROM endless_config WHERE key = ?`,
|
|
1440
|
+
[key]
|
|
1441
|
+
);
|
|
1442
|
+
if (!row)
|
|
1443
|
+
return null;
|
|
1444
|
+
return JSON.parse(row.value);
|
|
1445
|
+
}
|
|
1446
|
+
/**
|
|
1447
|
+
* Set config value for endless mode
|
|
1448
|
+
*/
|
|
1449
|
+
async setEndlessConfig(key, value) {
|
|
1450
|
+
await this.initialize();
|
|
1451
|
+
sqliteRun(
|
|
1452
|
+
this.db,
|
|
1453
|
+
`INSERT OR REPLACE INTO endless_config (key, value, updated_at)
|
|
1454
|
+
VALUES (?, ?, datetime('now'))`,
|
|
1455
|
+
[key, JSON.stringify(value)]
|
|
1456
|
+
);
|
|
1457
|
+
}
|
|
1458
|
+
/**
|
|
1459
|
+
* Increment access count for events
|
|
1460
|
+
*/
|
|
1461
|
+
async incrementAccessCount(eventIds) {
|
|
1462
|
+
if (eventIds.length === 0 || this.readOnly)
|
|
1463
|
+
return;
|
|
1464
|
+
await this.initialize();
|
|
1465
|
+
const placeholders = eventIds.map(() => "?").join(",");
|
|
1466
|
+
const currentTime = toSQLiteTimestamp(/* @__PURE__ */ new Date());
|
|
1467
|
+
sqliteRun(
|
|
1468
|
+
this.db,
|
|
1469
|
+
`UPDATE events
|
|
1470
|
+
SET access_count = access_count + 1,
|
|
1471
|
+
last_accessed_at = ?
|
|
1472
|
+
WHERE id IN (${placeholders})`,
|
|
1473
|
+
[currentTime, ...eventIds]
|
|
1474
|
+
);
|
|
1475
|
+
}
|
|
1476
|
+
/**
|
|
1477
|
+
* Get most accessed memories
|
|
1478
|
+
*/
|
|
1479
|
+
async getMostAccessed(limit = 10) {
|
|
1480
|
+
await this.initialize();
|
|
1481
|
+
const rows = sqliteAll(
|
|
1482
|
+
this.db,
|
|
1483
|
+
`SELECT * FROM events
|
|
1484
|
+
WHERE access_count > 0
|
|
1485
|
+
ORDER BY access_count DESC, last_accessed_at DESC
|
|
1486
|
+
LIMIT ?`,
|
|
1487
|
+
[limit]
|
|
1488
|
+
);
|
|
1489
|
+
return rows.map((row) => this.rowToEvent(row));
|
|
1490
|
+
}
|
|
1491
|
+
/**
|
|
1492
|
+
* Get database instance for direct access
|
|
1493
|
+
*/
|
|
1494
|
+
getDatabase() {
|
|
1495
|
+
return this.db;
|
|
1496
|
+
}
|
|
718
1497
|
/**
|
|
719
1498
|
* Close database connection
|
|
720
1499
|
*/
|
|
721
1500
|
async close() {
|
|
722
|
-
|
|
1501
|
+
sqliteClose(this.db);
|
|
723
1502
|
}
|
|
724
1503
|
/**
|
|
725
1504
|
* Convert database row to MemoryEvent
|
|
726
1505
|
*/
|
|
727
1506
|
rowToEvent(row) {
|
|
728
|
-
|
|
1507
|
+
const event = {
|
|
729
1508
|
id: row.id,
|
|
730
1509
|
eventType: row.event_type,
|
|
731
1510
|
sessionId: row.session_id,
|
|
732
|
-
timestamp:
|
|
1511
|
+
timestamp: toDateFromSQLite(row.timestamp),
|
|
733
1512
|
content: row.content,
|
|
734
1513
|
canonicalKey: row.canonical_key,
|
|
735
1514
|
dedupeKey: row.dedupe_key,
|
|
736
1515
|
metadata: row.metadata ? JSON.parse(row.metadata) : void 0
|
|
737
1516
|
};
|
|
1517
|
+
if (row.access_count !== void 0) {
|
|
1518
|
+
event.access_count = row.access_count;
|
|
1519
|
+
}
|
|
1520
|
+
if (row.last_accessed_at !== void 0) {
|
|
1521
|
+
event.last_accessed_at = row.last_accessed_at;
|
|
1522
|
+
}
|
|
1523
|
+
return event;
|
|
1524
|
+
}
|
|
1525
|
+
};
|
|
1526
|
+
|
|
1527
|
+
// src/core/sync-worker.ts
|
|
1528
|
+
var DEFAULT_CONFIG = {
|
|
1529
|
+
intervalMs: 3e4,
|
|
1530
|
+
batchSize: 500,
|
|
1531
|
+
maxRetries: 3,
|
|
1532
|
+
retryDelayMs: 5e3
|
|
1533
|
+
};
|
|
1534
|
+
var SyncWorker = class {
|
|
1535
|
+
constructor(sqliteStore, duckdbStore, config) {
|
|
1536
|
+
this.sqliteStore = sqliteStore;
|
|
1537
|
+
this.duckdbStore = duckdbStore;
|
|
1538
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
1539
|
+
}
|
|
1540
|
+
config;
|
|
1541
|
+
intervalHandle = null;
|
|
1542
|
+
running = false;
|
|
1543
|
+
stats = {
|
|
1544
|
+
lastSyncAt: null,
|
|
1545
|
+
eventsSynced: 0,
|
|
1546
|
+
sessionsSynced: 0,
|
|
1547
|
+
errors: 0,
|
|
1548
|
+
status: "idle"
|
|
1549
|
+
};
|
|
1550
|
+
/**
|
|
1551
|
+
* Start the sync worker
|
|
1552
|
+
*/
|
|
1553
|
+
start() {
|
|
1554
|
+
if (this.running)
|
|
1555
|
+
return;
|
|
1556
|
+
this.running = true;
|
|
1557
|
+
this.stats.status = "idle";
|
|
1558
|
+
this.syncNow().catch((err) => {
|
|
1559
|
+
console.error("[SyncWorker] Initial sync failed:", err);
|
|
1560
|
+
});
|
|
1561
|
+
this.intervalHandle = setInterval(() => {
|
|
1562
|
+
this.syncNow().catch((err) => {
|
|
1563
|
+
console.error("[SyncWorker] Periodic sync failed:", err);
|
|
1564
|
+
});
|
|
1565
|
+
}, this.config.intervalMs);
|
|
1566
|
+
}
|
|
1567
|
+
/**
|
|
1568
|
+
* Stop the sync worker
|
|
1569
|
+
*/
|
|
1570
|
+
stop() {
|
|
1571
|
+
this.running = false;
|
|
1572
|
+
this.stats.status = "stopped";
|
|
1573
|
+
if (this.intervalHandle) {
|
|
1574
|
+
clearInterval(this.intervalHandle);
|
|
1575
|
+
this.intervalHandle = null;
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
/**
|
|
1579
|
+
* Trigger immediate sync
|
|
1580
|
+
*/
|
|
1581
|
+
async syncNow() {
|
|
1582
|
+
if (this.stats.status === "syncing") {
|
|
1583
|
+
return;
|
|
1584
|
+
}
|
|
1585
|
+
this.stats.status = "syncing";
|
|
1586
|
+
try {
|
|
1587
|
+
await this.syncEvents();
|
|
1588
|
+
await this.syncSessions();
|
|
1589
|
+
this.stats.lastSyncAt = /* @__PURE__ */ new Date();
|
|
1590
|
+
this.stats.status = "idle";
|
|
1591
|
+
} catch (error) {
|
|
1592
|
+
this.stats.errors++;
|
|
1593
|
+
this.stats.status = "error";
|
|
1594
|
+
throw error;
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
/**
|
|
1598
|
+
* Sync events from SQLite to DuckDB
|
|
1599
|
+
*/
|
|
1600
|
+
async syncEvents() {
|
|
1601
|
+
const targetName = "duckdb_analytics";
|
|
1602
|
+
const position = await this.sqliteStore.getSyncPosition(targetName);
|
|
1603
|
+
const lastTimestamp = position.lastTimestamp || "1970-01-01T00:00:00.000Z";
|
|
1604
|
+
let hasMore = true;
|
|
1605
|
+
let totalSynced = 0;
|
|
1606
|
+
while (hasMore) {
|
|
1607
|
+
const events = await this.sqliteStore.getEventsSince(lastTimestamp, this.config.batchSize);
|
|
1608
|
+
if (events.length === 0) {
|
|
1609
|
+
hasMore = false;
|
|
1610
|
+
break;
|
|
1611
|
+
}
|
|
1612
|
+
await this.retryWithBackoff(async () => {
|
|
1613
|
+
for (const event of events) {
|
|
1614
|
+
await this.insertEventToDuckDB(event);
|
|
1615
|
+
}
|
|
1616
|
+
});
|
|
1617
|
+
totalSynced += events.length;
|
|
1618
|
+
const lastEvent = events[events.length - 1];
|
|
1619
|
+
await this.sqliteStore.updateSyncPosition(
|
|
1620
|
+
targetName,
|
|
1621
|
+
lastEvent.id,
|
|
1622
|
+
lastEvent.timestamp.toISOString()
|
|
1623
|
+
);
|
|
1624
|
+
hasMore = events.length === this.config.batchSize;
|
|
1625
|
+
}
|
|
1626
|
+
this.stats.eventsSynced += totalSynced;
|
|
1627
|
+
}
|
|
1628
|
+
/**
|
|
1629
|
+
* Sync sessions from SQLite to DuckDB
|
|
1630
|
+
*/
|
|
1631
|
+
async syncSessions() {
|
|
1632
|
+
const sessions = await this.sqliteStore.getAllSessions();
|
|
1633
|
+
for (const session of sessions) {
|
|
1634
|
+
await this.retryWithBackoff(async () => {
|
|
1635
|
+
await this.duckdbStore.upsertSession(session);
|
|
1636
|
+
});
|
|
1637
|
+
}
|
|
1638
|
+
this.stats.sessionsSynced = sessions.length;
|
|
1639
|
+
}
|
|
1640
|
+
/**
|
|
1641
|
+
* Insert a single event into DuckDB
|
|
1642
|
+
*/
|
|
1643
|
+
async insertEventToDuckDB(event) {
|
|
1644
|
+
await this.duckdbStore.append({
|
|
1645
|
+
eventType: event.eventType,
|
|
1646
|
+
sessionId: event.sessionId,
|
|
1647
|
+
timestamp: event.timestamp,
|
|
1648
|
+
content: event.content,
|
|
1649
|
+
metadata: event.metadata
|
|
1650
|
+
});
|
|
1651
|
+
}
|
|
1652
|
+
/**
|
|
1653
|
+
* Retry operation with exponential backoff
|
|
1654
|
+
*/
|
|
1655
|
+
async retryWithBackoff(fn) {
|
|
1656
|
+
let lastError = null;
|
|
1657
|
+
for (let attempt = 0; attempt < this.config.maxRetries; attempt++) {
|
|
1658
|
+
try {
|
|
1659
|
+
return await fn();
|
|
1660
|
+
} catch (error) {
|
|
1661
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
1662
|
+
if (attempt < this.config.maxRetries - 1) {
|
|
1663
|
+
const delay = this.config.retryDelayMs * Math.pow(2, attempt);
|
|
1664
|
+
await this.sleep(delay);
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
throw lastError;
|
|
1669
|
+
}
|
|
1670
|
+
/**
|
|
1671
|
+
* Sleep utility
|
|
1672
|
+
*/
|
|
1673
|
+
sleep(ms) {
|
|
1674
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
1675
|
+
}
|
|
1676
|
+
/**
|
|
1677
|
+
* Get sync statistics
|
|
1678
|
+
*/
|
|
1679
|
+
getStats() {
|
|
1680
|
+
return { ...this.stats };
|
|
1681
|
+
}
|
|
1682
|
+
/**
|
|
1683
|
+
* Check if worker is running
|
|
1684
|
+
*/
|
|
1685
|
+
isRunning() {
|
|
1686
|
+
return this.running;
|
|
738
1687
|
}
|
|
739
1688
|
};
|
|
740
1689
|
|
|
@@ -966,7 +1915,7 @@ function getDefaultEmbedder() {
|
|
|
966
1915
|
}
|
|
967
1916
|
|
|
968
1917
|
// src/core/vector-outbox.ts
|
|
969
|
-
var
|
|
1918
|
+
var DEFAULT_CONFIG2 = {
|
|
970
1919
|
embeddingVersion: "v1",
|
|
971
1920
|
maxRetries: 3,
|
|
972
1921
|
stuckThresholdMs: 5 * 60 * 1e3,
|
|
@@ -975,7 +1924,7 @@ var DEFAULT_CONFIG = {
|
|
|
975
1924
|
};
|
|
976
1925
|
|
|
977
1926
|
// src/core/vector-worker.ts
|
|
978
|
-
var
|
|
1927
|
+
var DEFAULT_CONFIG3 = {
|
|
979
1928
|
batchSize: 32,
|
|
980
1929
|
pollIntervalMs: 1e3,
|
|
981
1930
|
maxRetries: 3
|
|
@@ -986,12 +1935,13 @@ var VectorWorker = class {
|
|
|
986
1935
|
embedder;
|
|
987
1936
|
config;
|
|
988
1937
|
running = false;
|
|
1938
|
+
stopping = false;
|
|
989
1939
|
pollTimeout = null;
|
|
990
1940
|
constructor(eventStore, vectorStore, embedder, config = {}) {
|
|
991
1941
|
this.eventStore = eventStore;
|
|
992
1942
|
this.vectorStore = vectorStore;
|
|
993
1943
|
this.embedder = embedder;
|
|
994
|
-
this.config = { ...
|
|
1944
|
+
this.config = { ...DEFAULT_CONFIG3, ...config };
|
|
995
1945
|
}
|
|
996
1946
|
/**
|
|
997
1947
|
* Start the worker polling loop
|
|
@@ -1000,6 +1950,7 @@ var VectorWorker = class {
|
|
|
1000
1950
|
if (this.running)
|
|
1001
1951
|
return;
|
|
1002
1952
|
this.running = true;
|
|
1953
|
+
this.stopping = false;
|
|
1003
1954
|
this.poll();
|
|
1004
1955
|
}
|
|
1005
1956
|
/**
|
|
@@ -1007,6 +1958,7 @@ var VectorWorker = class {
|
|
|
1007
1958
|
*/
|
|
1008
1959
|
stop() {
|
|
1009
1960
|
this.running = false;
|
|
1961
|
+
this.stopping = true;
|
|
1010
1962
|
if (this.pollTimeout) {
|
|
1011
1963
|
clearTimeout(this.pollTimeout);
|
|
1012
1964
|
this.pollTimeout = null;
|
|
@@ -1056,9 +2008,15 @@ var VectorWorker = class {
|
|
|
1056
2008
|
}
|
|
1057
2009
|
return successful.length;
|
|
1058
2010
|
} catch (error) {
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
2011
|
+
if (!this.stopping) {
|
|
2012
|
+
try {
|
|
2013
|
+
const allIds = items.map((i) => i.id);
|
|
2014
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2015
|
+
await this.eventStore.failOutboxItems(allIds, errorMessage);
|
|
2016
|
+
} catch (failError) {
|
|
2017
|
+
console.warn("Could not mark outbox items as failed (database may be closed)");
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
1062
2020
|
throw error;
|
|
1063
2021
|
}
|
|
1064
2022
|
}
|
|
@@ -1066,14 +2024,18 @@ var VectorWorker = class {
|
|
|
1066
2024
|
* Poll for new items
|
|
1067
2025
|
*/
|
|
1068
2026
|
async poll() {
|
|
1069
|
-
if (!this.running)
|
|
2027
|
+
if (!this.running || this.stopping)
|
|
1070
2028
|
return;
|
|
1071
2029
|
try {
|
|
1072
2030
|
await this.processBatch();
|
|
1073
2031
|
} catch (error) {
|
|
1074
|
-
|
|
2032
|
+
if (!this.stopping) {
|
|
2033
|
+
console.error("Vector worker error:", error);
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
if (this.running && !this.stopping) {
|
|
2037
|
+
this.pollTimeout = setTimeout(() => this.poll(), this.config.pollIntervalMs);
|
|
1075
2038
|
}
|
|
1076
|
-
this.pollTimeout = setTimeout(() => this.poll(), this.config.pollIntervalMs);
|
|
1077
2039
|
}
|
|
1078
2040
|
/**
|
|
1079
2041
|
* Process all pending items (blocking)
|
|
@@ -1100,7 +2062,7 @@ function createVectorWorker(eventStore, vectorStore, embedder, config) {
|
|
|
1100
2062
|
}
|
|
1101
2063
|
|
|
1102
2064
|
// src/core/matcher.ts
|
|
1103
|
-
var
|
|
2065
|
+
var DEFAULT_CONFIG4 = {
|
|
1104
2066
|
weights: {
|
|
1105
2067
|
semanticSimilarity: 0.4,
|
|
1106
2068
|
ftsScore: 0.25,
|
|
@@ -1115,9 +2077,9 @@ var Matcher = class {
|
|
|
1115
2077
|
config;
|
|
1116
2078
|
constructor(config = {}) {
|
|
1117
2079
|
this.config = {
|
|
1118
|
-
...
|
|
2080
|
+
...DEFAULT_CONFIG4,
|
|
1119
2081
|
...config,
|
|
1120
|
-
weights: { ...
|
|
2082
|
+
weights: { ...DEFAULT_CONFIG4.weights, ...config.weights }
|
|
1121
2083
|
};
|
|
1122
2084
|
}
|
|
1123
2085
|
/**
|
|
@@ -1825,7 +2787,7 @@ function createSharedEventStore(dbPath) {
|
|
|
1825
2787
|
}
|
|
1826
2788
|
|
|
1827
2789
|
// src/core/shared-store.ts
|
|
1828
|
-
import { randomUUID as
|
|
2790
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
1829
2791
|
var SharedStore = class {
|
|
1830
2792
|
constructor(sharedEventStore) {
|
|
1831
2793
|
this.sharedEventStore = sharedEventStore;
|
|
@@ -1837,7 +2799,7 @@ var SharedStore = class {
|
|
|
1837
2799
|
* Promote a verified troubleshooting entry to shared storage
|
|
1838
2800
|
*/
|
|
1839
2801
|
async promoteEntry(input) {
|
|
1840
|
-
const entryId =
|
|
2802
|
+
const entryId = randomUUID3();
|
|
1841
2803
|
await dbRun(
|
|
1842
2804
|
this.db,
|
|
1843
2805
|
`INSERT INTO shared_troubleshooting (
|
|
@@ -2202,7 +3164,7 @@ function createSharedVectorStore(dbPath) {
|
|
|
2202
3164
|
}
|
|
2203
3165
|
|
|
2204
3166
|
// src/core/shared-promoter.ts
|
|
2205
|
-
import { randomUUID as
|
|
3167
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
2206
3168
|
var SharedPromoter = class {
|
|
2207
3169
|
constructor(sharedStore, sharedVectorStore, embedder, config) {
|
|
2208
3170
|
this.sharedStore = sharedStore;
|
|
@@ -2270,7 +3232,7 @@ var SharedPromoter = class {
|
|
|
2270
3232
|
const embeddingContent = this.createEmbeddingContent(input);
|
|
2271
3233
|
const embedding = await this.embedder.embed(embeddingContent);
|
|
2272
3234
|
await this.sharedVectorStore.upsert({
|
|
2273
|
-
id:
|
|
3235
|
+
id: randomUUID4(),
|
|
2274
3236
|
entryId,
|
|
2275
3237
|
entryType: "troubleshooting",
|
|
2276
3238
|
content: embeddingContent,
|
|
@@ -2410,7 +3372,7 @@ function createToolObservationEmbedding(toolName, metadata, success) {
|
|
|
2410
3372
|
}
|
|
2411
3373
|
|
|
2412
3374
|
// src/core/working-set-store.ts
|
|
2413
|
-
import { randomUUID as
|
|
3375
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
2414
3376
|
var WorkingSetStore = class {
|
|
2415
3377
|
constructor(eventStore, config) {
|
|
2416
3378
|
this.eventStore = eventStore;
|
|
@@ -2431,7 +3393,7 @@ var WorkingSetStore = class {
|
|
|
2431
3393
|
`INSERT OR REPLACE INTO working_set (id, event_id, added_at, relevance_score, topics, expires_at)
|
|
2432
3394
|
VALUES (?, ?, CURRENT_TIMESTAMP, ?, ?, ?)`,
|
|
2433
3395
|
[
|
|
2434
|
-
|
|
3396
|
+
randomUUID5(),
|
|
2435
3397
|
eventId,
|
|
2436
3398
|
relevanceScore,
|
|
2437
3399
|
JSON.stringify(topics || []),
|
|
@@ -2615,7 +3577,7 @@ function createWorkingSetStore(eventStore, config) {
|
|
|
2615
3577
|
}
|
|
2616
3578
|
|
|
2617
3579
|
// src/core/consolidated-store.ts
|
|
2618
|
-
import { randomUUID as
|
|
3580
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
2619
3581
|
var ConsolidatedStore = class {
|
|
2620
3582
|
constructor(eventStore) {
|
|
2621
3583
|
this.eventStore = eventStore;
|
|
@@ -2627,7 +3589,7 @@ var ConsolidatedStore = class {
|
|
|
2627
3589
|
* Create a new consolidated memory
|
|
2628
3590
|
*/
|
|
2629
3591
|
async create(input) {
|
|
2630
|
-
const memoryId =
|
|
3592
|
+
const memoryId = randomUUID6();
|
|
2631
3593
|
await dbRun(
|
|
2632
3594
|
this.db,
|
|
2633
3595
|
`INSERT INTO consolidated_memories
|
|
@@ -3154,7 +4116,7 @@ function createConsolidationWorker(workingSetStore, consolidatedStore, config) {
|
|
|
3154
4116
|
}
|
|
3155
4117
|
|
|
3156
4118
|
// src/core/continuity-manager.ts
|
|
3157
|
-
import { randomUUID as
|
|
4119
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
3158
4120
|
var ContinuityManager = class {
|
|
3159
4121
|
constructor(eventStore, config) {
|
|
3160
4122
|
this.eventStore = eventStore;
|
|
@@ -3308,7 +4270,7 @@ var ContinuityManager = class {
|
|
|
3308
4270
|
`INSERT INTO continuity_log
|
|
3309
4271
|
(log_id, from_context_id, to_context_id, continuity_score, transition_type, created_at)
|
|
3310
4272
|
VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,
|
|
3311
|
-
[
|
|
4273
|
+
[randomUUID7(), previous.id, current.id, score, type]
|
|
3312
4274
|
);
|
|
3313
4275
|
}
|
|
3314
4276
|
/**
|
|
@@ -3417,7 +4379,7 @@ function createContinuityManager(eventStore, config) {
|
|
|
3417
4379
|
}
|
|
3418
4380
|
|
|
3419
4381
|
// src/core/graduation-worker.ts
|
|
3420
|
-
var
|
|
4382
|
+
var DEFAULT_CONFIG5 = {
|
|
3421
4383
|
evaluationIntervalMs: 3e5,
|
|
3422
4384
|
// 5 minutes
|
|
3423
4385
|
batchSize: 50,
|
|
@@ -3425,7 +4387,7 @@ var DEFAULT_CONFIG4 = {
|
|
|
3425
4387
|
// 1 hour cooldown between evaluations
|
|
3426
4388
|
};
|
|
3427
4389
|
var GraduationWorker = class {
|
|
3428
|
-
constructor(eventStore, graduation, config =
|
|
4390
|
+
constructor(eventStore, graduation, config = DEFAULT_CONFIG5) {
|
|
3429
4391
|
this.eventStore = eventStore;
|
|
3430
4392
|
this.graduation = graduation;
|
|
3431
4393
|
this.config = config;
|
|
@@ -3533,7 +4495,7 @@ function createGraduationWorker(eventStore, graduation, config) {
|
|
|
3533
4495
|
return new GraduationWorker(
|
|
3534
4496
|
eventStore,
|
|
3535
4497
|
graduation,
|
|
3536
|
-
{ ...
|
|
4498
|
+
{ ...DEFAULT_CONFIG5, ...config }
|
|
3537
4499
|
);
|
|
3538
4500
|
}
|
|
3539
4501
|
|
|
@@ -3557,7 +4519,11 @@ function getProjectStoragePath(projectPath) {
|
|
|
3557
4519
|
var REGISTRY_PATH = path.join(os.homedir(), ".claude-code", "memory", "session-registry.json");
|
|
3558
4520
|
var SHARED_STORAGE_PATH = path.join(os.homedir(), ".claude-code", "memory", "shared");
|
|
3559
4521
|
var MemoryService = class {
|
|
3560
|
-
|
|
4522
|
+
// Primary store: SQLite (WAL mode) - for hooks, always available
|
|
4523
|
+
sqliteStore;
|
|
4524
|
+
// Analytics store: DuckDB - for server reads (optional, synced from SQLite)
|
|
4525
|
+
analyticsStore;
|
|
4526
|
+
syncWorker = null;
|
|
3561
4527
|
vectorStore;
|
|
3562
4528
|
embedder;
|
|
3563
4529
|
matcher;
|
|
@@ -3588,17 +4554,39 @@ var MemoryService = class {
|
|
|
3588
4554
|
}
|
|
3589
4555
|
this.projectHash = config.projectHash || null;
|
|
3590
4556
|
this.sharedStoreConfig = config.sharedStoreConfig ?? { enabled: true };
|
|
3591
|
-
this.
|
|
4557
|
+
this.sqliteStore = new SQLiteEventStore(
|
|
4558
|
+
path.join(storagePath, "events.sqlite"),
|
|
4559
|
+
{ readonly: this.readOnly }
|
|
4560
|
+
);
|
|
4561
|
+
const analyticsEnabled = config.analyticsEnabled ?? this.readOnly;
|
|
4562
|
+
if (!analyticsEnabled) {
|
|
4563
|
+
this.analyticsStore = null;
|
|
4564
|
+
} else if (this.readOnly) {
|
|
4565
|
+
try {
|
|
4566
|
+
this.analyticsStore = new EventStore(
|
|
4567
|
+
path.join(storagePath, "analytics.duckdb"),
|
|
4568
|
+
{ readOnly: true }
|
|
4569
|
+
);
|
|
4570
|
+
} catch {
|
|
4571
|
+
this.analyticsStore = null;
|
|
4572
|
+
}
|
|
4573
|
+
} else {
|
|
4574
|
+
this.analyticsStore = new EventStore(
|
|
4575
|
+
path.join(storagePath, "analytics.duckdb"),
|
|
4576
|
+
{ readOnly: false }
|
|
4577
|
+
);
|
|
4578
|
+
}
|
|
3592
4579
|
this.vectorStore = new VectorStore(path.join(storagePath, "vectors"));
|
|
3593
4580
|
this.embedder = config.embeddingModel ? new Embedder(config.embeddingModel) : getDefaultEmbedder();
|
|
3594
4581
|
this.matcher = getDefaultMatcher();
|
|
3595
4582
|
this.retriever = createRetriever(
|
|
3596
|
-
this.
|
|
4583
|
+
this.sqliteStore,
|
|
4584
|
+
// Interface compatible
|
|
3597
4585
|
this.vectorStore,
|
|
3598
4586
|
this.embedder,
|
|
3599
4587
|
this.matcher
|
|
3600
4588
|
);
|
|
3601
|
-
this.graduation = createGraduationPipeline(this.
|
|
4589
|
+
this.graduation = createGraduationPipeline(this.sqliteStore);
|
|
3602
4590
|
}
|
|
3603
4591
|
/**
|
|
3604
4592
|
* Initialize all components
|
|
@@ -3606,23 +4594,38 @@ var MemoryService = class {
|
|
|
3606
4594
|
async initialize() {
|
|
3607
4595
|
if (this.initialized)
|
|
3608
4596
|
return;
|
|
3609
|
-
await this.
|
|
4597
|
+
await this.sqliteStore.initialize();
|
|
4598
|
+
if (this.analyticsStore) {
|
|
4599
|
+
try {
|
|
4600
|
+
await this.analyticsStore.initialize();
|
|
4601
|
+
} catch (error) {
|
|
4602
|
+
console.warn("[MemoryService] Analytics store (DuckDB) initialization failed, using SQLite for reads:", error);
|
|
4603
|
+
}
|
|
4604
|
+
}
|
|
3610
4605
|
await this.vectorStore.initialize();
|
|
3611
4606
|
await this.embedder.initialize();
|
|
3612
4607
|
if (!this.readOnly) {
|
|
3613
4608
|
this.vectorWorker = createVectorWorker(
|
|
3614
|
-
this.
|
|
4609
|
+
this.sqliteStore,
|
|
3615
4610
|
this.vectorStore,
|
|
3616
4611
|
this.embedder
|
|
3617
4612
|
);
|
|
3618
4613
|
this.vectorWorker.start();
|
|
3619
4614
|
this.retriever.setGraduationPipeline(this.graduation);
|
|
3620
4615
|
this.graduationWorker = createGraduationWorker(
|
|
3621
|
-
this.
|
|
4616
|
+
this.sqliteStore,
|
|
3622
4617
|
this.graduation
|
|
3623
4618
|
);
|
|
3624
4619
|
this.graduationWorker.start();
|
|
3625
|
-
|
|
4620
|
+
if (this.analyticsStore) {
|
|
4621
|
+
this.syncWorker = new SyncWorker(
|
|
4622
|
+
this.sqliteStore,
|
|
4623
|
+
this.analyticsStore,
|
|
4624
|
+
{ intervalMs: 3e4, batchSize: 500 }
|
|
4625
|
+
);
|
|
4626
|
+
this.syncWorker.start();
|
|
4627
|
+
}
|
|
4628
|
+
const savedMode = await this.sqliteStore.getEndlessConfig("mode");
|
|
3626
4629
|
if (savedMode === "endless") {
|
|
3627
4630
|
this.endlessMode = "endless";
|
|
3628
4631
|
await this.initializeEndlessMode();
|
|
@@ -3663,7 +4666,7 @@ var MemoryService = class {
|
|
|
3663
4666
|
*/
|
|
3664
4667
|
async startSession(sessionId, projectPath) {
|
|
3665
4668
|
await this.initialize();
|
|
3666
|
-
await this.
|
|
4669
|
+
await this.sqliteStore.upsertSession({
|
|
3667
4670
|
id: sessionId,
|
|
3668
4671
|
startedAt: /* @__PURE__ */ new Date(),
|
|
3669
4672
|
projectPath
|
|
@@ -3674,7 +4677,7 @@ var MemoryService = class {
|
|
|
3674
4677
|
*/
|
|
3675
4678
|
async endSession(sessionId, summary) {
|
|
3676
4679
|
await this.initialize();
|
|
3677
|
-
await this.
|
|
4680
|
+
await this.sqliteStore.upsertSession({
|
|
3678
4681
|
id: sessionId,
|
|
3679
4682
|
endedAt: /* @__PURE__ */ new Date(),
|
|
3680
4683
|
summary
|
|
@@ -3685,7 +4688,7 @@ var MemoryService = class {
|
|
|
3685
4688
|
*/
|
|
3686
4689
|
async storeUserPrompt(sessionId, content, metadata) {
|
|
3687
4690
|
await this.initialize();
|
|
3688
|
-
const result = await this.
|
|
4691
|
+
const result = await this.sqliteStore.append({
|
|
3689
4692
|
eventType: "user_prompt",
|
|
3690
4693
|
sessionId,
|
|
3691
4694
|
timestamp: /* @__PURE__ */ new Date(),
|
|
@@ -3693,7 +4696,7 @@ var MemoryService = class {
|
|
|
3693
4696
|
metadata
|
|
3694
4697
|
});
|
|
3695
4698
|
if (result.success && !result.isDuplicate) {
|
|
3696
|
-
await this.
|
|
4699
|
+
await this.sqliteStore.enqueueForEmbedding(result.eventId, content);
|
|
3697
4700
|
}
|
|
3698
4701
|
return result;
|
|
3699
4702
|
}
|
|
@@ -3702,7 +4705,7 @@ var MemoryService = class {
|
|
|
3702
4705
|
*/
|
|
3703
4706
|
async storeAgentResponse(sessionId, content, metadata) {
|
|
3704
4707
|
await this.initialize();
|
|
3705
|
-
const result = await this.
|
|
4708
|
+
const result = await this.sqliteStore.append({
|
|
3706
4709
|
eventType: "agent_response",
|
|
3707
4710
|
sessionId,
|
|
3708
4711
|
timestamp: /* @__PURE__ */ new Date(),
|
|
@@ -3710,7 +4713,7 @@ var MemoryService = class {
|
|
|
3710
4713
|
metadata
|
|
3711
4714
|
});
|
|
3712
4715
|
if (result.success && !result.isDuplicate) {
|
|
3713
|
-
await this.
|
|
4716
|
+
await this.sqliteStore.enqueueForEmbedding(result.eventId, content);
|
|
3714
4717
|
}
|
|
3715
4718
|
return result;
|
|
3716
4719
|
}
|
|
@@ -3719,14 +4722,14 @@ var MemoryService = class {
|
|
|
3719
4722
|
*/
|
|
3720
4723
|
async storeSessionSummary(sessionId, summary) {
|
|
3721
4724
|
await this.initialize();
|
|
3722
|
-
const result = await this.
|
|
4725
|
+
const result = await this.sqliteStore.append({
|
|
3723
4726
|
eventType: "session_summary",
|
|
3724
4727
|
sessionId,
|
|
3725
4728
|
timestamp: /* @__PURE__ */ new Date(),
|
|
3726
4729
|
content: summary
|
|
3727
4730
|
});
|
|
3728
4731
|
if (result.success && !result.isDuplicate) {
|
|
3729
|
-
await this.
|
|
4732
|
+
await this.sqliteStore.enqueueForEmbedding(result.eventId, summary);
|
|
3730
4733
|
}
|
|
3731
4734
|
return result;
|
|
3732
4735
|
}
|
|
@@ -3736,7 +4739,7 @@ var MemoryService = class {
|
|
|
3736
4739
|
async storeToolObservation(sessionId, payload) {
|
|
3737
4740
|
await this.initialize();
|
|
3738
4741
|
const content = JSON.stringify(payload);
|
|
3739
|
-
const result = await this.
|
|
4742
|
+
const result = await this.sqliteStore.append({
|
|
3740
4743
|
eventType: "tool_observation",
|
|
3741
4744
|
sessionId,
|
|
3742
4745
|
timestamp: /* @__PURE__ */ new Date(),
|
|
@@ -3752,7 +4755,7 @@ var MemoryService = class {
|
|
|
3752
4755
|
payload.metadata || {},
|
|
3753
4756
|
payload.success
|
|
3754
4757
|
);
|
|
3755
|
-
await this.
|
|
4758
|
+
await this.sqliteStore.enqueueForEmbedding(result.eventId, embeddingContent);
|
|
3756
4759
|
}
|
|
3757
4760
|
return result;
|
|
3758
4761
|
}
|
|
@@ -3778,21 +4781,21 @@ var MemoryService = class {
|
|
|
3778
4781
|
*/
|
|
3779
4782
|
async getSessionHistory(sessionId) {
|
|
3780
4783
|
await this.initialize();
|
|
3781
|
-
return this.
|
|
4784
|
+
return this.sqliteStore.getSessionEvents(sessionId);
|
|
3782
4785
|
}
|
|
3783
4786
|
/**
|
|
3784
4787
|
* Get recent events
|
|
3785
4788
|
*/
|
|
3786
4789
|
async getRecentEvents(limit = 100) {
|
|
3787
4790
|
await this.initialize();
|
|
3788
|
-
return this.
|
|
4791
|
+
return this.sqliteStore.getRecentEvents(limit);
|
|
3789
4792
|
}
|
|
3790
4793
|
/**
|
|
3791
4794
|
* Get memory statistics
|
|
3792
4795
|
*/
|
|
3793
4796
|
async getStats() {
|
|
3794
4797
|
await this.initialize();
|
|
3795
|
-
const recentEvents = await this.
|
|
4798
|
+
const recentEvents = await this.sqliteStore.getRecentEvents(1e4);
|
|
3796
4799
|
const vectorCount = await this.vectorStore.count();
|
|
3797
4800
|
const levelStats = await this.graduation.getStats();
|
|
3798
4801
|
return {
|
|
@@ -3815,14 +4818,14 @@ var MemoryService = class {
|
|
|
3815
4818
|
*/
|
|
3816
4819
|
async getEventsByLevel(level, options) {
|
|
3817
4820
|
await this.initialize();
|
|
3818
|
-
return this.
|
|
4821
|
+
return this.sqliteStore.getEventsByLevel(level, options);
|
|
3819
4822
|
}
|
|
3820
4823
|
/**
|
|
3821
4824
|
* Get memory level for a specific event
|
|
3822
4825
|
*/
|
|
3823
4826
|
async getEventLevel(eventId) {
|
|
3824
4827
|
await this.initialize();
|
|
3825
|
-
return this.
|
|
4828
|
+
return this.sqliteStore.getEventLevel(eventId);
|
|
3826
4829
|
}
|
|
3827
4830
|
/**
|
|
3828
4831
|
* Format retrieval results as context for Claude
|
|
@@ -3916,21 +4919,21 @@ var MemoryService = class {
|
|
|
3916
4919
|
*/
|
|
3917
4920
|
async initializeEndlessMode() {
|
|
3918
4921
|
const config = await this.getEndlessConfig();
|
|
3919
|
-
this.workingSetStore = createWorkingSetStore(this.
|
|
3920
|
-
this.consolidatedStore = createConsolidatedStore(this.
|
|
4922
|
+
this.workingSetStore = createWorkingSetStore(this.sqliteStore, config);
|
|
4923
|
+
this.consolidatedStore = createConsolidatedStore(this.sqliteStore);
|
|
3921
4924
|
this.consolidationWorker = createConsolidationWorker(
|
|
3922
4925
|
this.workingSetStore,
|
|
3923
4926
|
this.consolidatedStore,
|
|
3924
4927
|
config
|
|
3925
4928
|
);
|
|
3926
|
-
this.continuityManager = createContinuityManager(this.
|
|
4929
|
+
this.continuityManager = createContinuityManager(this.sqliteStore, config);
|
|
3927
4930
|
this.consolidationWorker.start();
|
|
3928
4931
|
}
|
|
3929
4932
|
/**
|
|
3930
4933
|
* Get Endless Mode configuration
|
|
3931
4934
|
*/
|
|
3932
4935
|
async getEndlessConfig() {
|
|
3933
|
-
const savedConfig = await this.
|
|
4936
|
+
const savedConfig = await this.sqliteStore.getEndlessConfig("config");
|
|
3934
4937
|
return savedConfig || this.getDefaultEndlessConfig();
|
|
3935
4938
|
}
|
|
3936
4939
|
/**
|
|
@@ -3939,7 +4942,7 @@ var MemoryService = class {
|
|
|
3939
4942
|
async setEndlessConfig(config) {
|
|
3940
4943
|
const current = await this.getEndlessConfig();
|
|
3941
4944
|
const merged = { ...current, ...config };
|
|
3942
|
-
await this.
|
|
4945
|
+
await this.sqliteStore.setEndlessConfig("config", merged);
|
|
3943
4946
|
}
|
|
3944
4947
|
/**
|
|
3945
4948
|
* Set memory mode (session or endless)
|
|
@@ -3949,7 +4952,7 @@ var MemoryService = class {
|
|
|
3949
4952
|
if (mode === this.endlessMode)
|
|
3950
4953
|
return;
|
|
3951
4954
|
this.endlessMode = mode;
|
|
3952
|
-
await this.
|
|
4955
|
+
await this.sqliteStore.setEndlessConfig("mode", mode);
|
|
3953
4956
|
if (mode === "endless") {
|
|
3954
4957
|
await this.initializeEndlessMode();
|
|
3955
4958
|
} else {
|
|
@@ -4007,12 +5010,49 @@ var MemoryService = class {
|
|
|
4007
5010
|
return this.consolidatedStore.getAll({ limit });
|
|
4008
5011
|
}
|
|
4009
5012
|
/**
|
|
4010
|
-
*
|
|
5013
|
+
* Increment access count for memories that were used in prompts
|
|
5014
|
+
*/
|
|
5015
|
+
async incrementMemoryAccess(eventIds) {
|
|
5016
|
+
if (eventIds.length === 0)
|
|
5017
|
+
return;
|
|
5018
|
+
if (this.sqliteStore) {
|
|
5019
|
+
await this.sqliteStore.incrementAccessCount(eventIds);
|
|
5020
|
+
} else if (this.eventStore) {
|
|
5021
|
+
await this.eventStore.incrementAccessCount(eventIds);
|
|
5022
|
+
}
|
|
5023
|
+
}
|
|
5024
|
+
/**
|
|
5025
|
+
* Get most accessed memories from events
|
|
4011
5026
|
*/
|
|
4012
5027
|
async getMostAccessedMemories(limit = 10) {
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
5028
|
+
console.log("[getMostAccessedMemories] sqliteStore available:", !!this.sqliteStore);
|
|
5029
|
+
if (this.sqliteStore) {
|
|
5030
|
+
const events = await this.sqliteStore.getMostAccessed(limit);
|
|
5031
|
+
console.log("[getMostAccessedMemories] Got events from SQLite:", events.length);
|
|
5032
|
+
return events.map((event) => ({
|
|
5033
|
+
memoryId: event.id,
|
|
5034
|
+
summary: event.content.substring(0, 200) + (event.content.length > 200 ? "..." : ""),
|
|
5035
|
+
topics: [],
|
|
5036
|
+
// Could extract topics from content if needed
|
|
5037
|
+
accessCount: event.access_count || 0,
|
|
5038
|
+
lastAccessed: event.last_accessed_at || null,
|
|
5039
|
+
confidence: 1,
|
|
5040
|
+
createdAt: event.timestamp
|
|
5041
|
+
}));
|
|
5042
|
+
}
|
|
5043
|
+
if (this.consolidatedStore) {
|
|
5044
|
+
const consolidated = await this.consolidatedStore.getMostAccessed(limit);
|
|
5045
|
+
return consolidated.map((m) => ({
|
|
5046
|
+
memoryId: m.memoryId,
|
|
5047
|
+
summary: m.summary,
|
|
5048
|
+
topics: m.topics,
|
|
5049
|
+
accessCount: m.accessCount,
|
|
5050
|
+
lastAccessed: m.accessedAt,
|
|
5051
|
+
confidence: m.confidence,
|
|
5052
|
+
createdAt: m.createdAt
|
|
5053
|
+
}));
|
|
5054
|
+
}
|
|
5055
|
+
return [];
|
|
4016
5056
|
}
|
|
4017
5057
|
/**
|
|
4018
5058
|
* Mark a consolidated memory as accessed
|
|
@@ -4137,10 +5177,16 @@ var MemoryService = class {
|
|
|
4137
5177
|
if (this.vectorWorker) {
|
|
4138
5178
|
this.vectorWorker.stop();
|
|
4139
5179
|
}
|
|
5180
|
+
if (this.syncWorker) {
|
|
5181
|
+
this.syncWorker.stop();
|
|
5182
|
+
}
|
|
4140
5183
|
if (this.sharedEventStore) {
|
|
4141
5184
|
await this.sharedEventStore.close();
|
|
4142
5185
|
}
|
|
4143
|
-
await this.
|
|
5186
|
+
await this.sqliteStore.close();
|
|
5187
|
+
if (this.analyticsStore) {
|
|
5188
|
+
await this.analyticsStore.close();
|
|
5189
|
+
}
|
|
4144
5190
|
}
|
|
4145
5191
|
/**
|
|
4146
5192
|
* Expand ~ to home directory
|
|
@@ -4156,7 +5202,11 @@ var serviceCache = /* @__PURE__ */ new Map();
|
|
|
4156
5202
|
function getReadOnlyMemoryService() {
|
|
4157
5203
|
return new MemoryService({
|
|
4158
5204
|
storagePath: "~/.claude-code/memory",
|
|
4159
|
-
readOnly: true
|
|
5205
|
+
readOnly: true,
|
|
5206
|
+
analyticsEnabled: false,
|
|
5207
|
+
// Use SQLite for reads (WAL supports concurrent readers)
|
|
5208
|
+
sharedStoreConfig: { enabled: false }
|
|
5209
|
+
// Skip shared store for now
|
|
4160
5210
|
});
|
|
4161
5211
|
}
|
|
4162
5212
|
function getMemoryServiceForProject(projectPath, sharedStoreConfig) {
|
|
@@ -4166,7 +5216,10 @@ function getMemoryServiceForProject(projectPath, sharedStoreConfig) {
|
|
|
4166
5216
|
serviceCache.set(hash, new MemoryService({
|
|
4167
5217
|
storagePath,
|
|
4168
5218
|
projectHash: hash,
|
|
4169
|
-
|
|
5219
|
+
// Override shared store config - hooks don't need DuckDB
|
|
5220
|
+
sharedStoreConfig: sharedStoreConfig ?? { enabled: false },
|
|
5221
|
+
analyticsEnabled: false
|
|
5222
|
+
// Hooks don't need DuckDB
|
|
4170
5223
|
}));
|
|
4171
5224
|
}
|
|
4172
5225
|
return serviceCache.get(hash);
|
|
@@ -4462,6 +5515,7 @@ statsRouter.get("/levels/:level", async (c) => {
|
|
|
4462
5515
|
const { level } = c.req.param();
|
|
4463
5516
|
const limit = parseInt(c.req.query("limit") || "20", 10);
|
|
4464
5517
|
const offset = parseInt(c.req.query("offset") || "0", 10);
|
|
5518
|
+
const sort = c.req.query("sort") || "recent";
|
|
4465
5519
|
const validLevels = ["L0", "L1", "L2", "L3", "L4"];
|
|
4466
5520
|
if (!validLevels.includes(level)) {
|
|
4467
5521
|
return c.json({ error: `Invalid level. Must be one of: ${validLevels.join(", ")}` }, 400);
|
|
@@ -4469,9 +5523,27 @@ statsRouter.get("/levels/:level", async (c) => {
|
|
|
4469
5523
|
const memoryService = getReadOnlyMemoryService();
|
|
4470
5524
|
try {
|
|
4471
5525
|
await memoryService.initialize();
|
|
4472
|
-
|
|
5526
|
+
let events = await memoryService.getEventsByLevel(level, { limit: limit * 2, offset });
|
|
4473
5527
|
const stats = await memoryService.getStats();
|
|
4474
5528
|
const levelStat = stats.levelStats.find((s) => s.level === level);
|
|
5529
|
+
if (sort === "accessed") {
|
|
5530
|
+
const sqliteStore = memoryService.sqliteEventStore;
|
|
5531
|
+
if (sqliteStore) {
|
|
5532
|
+
const eventIds = events.map((e) => e.id);
|
|
5533
|
+
const accessedEvents = await sqliteStore.getMostAccessed(1e3);
|
|
5534
|
+
const accessMap = new Map(accessedEvents.map((e) => [e.id, e.access_count || 0]));
|
|
5535
|
+
events = events.map((e) => ({
|
|
5536
|
+
...e,
|
|
5537
|
+
accessCount: accessMap.get(e.id) || 0
|
|
5538
|
+
}));
|
|
5539
|
+
events.sort((a, b) => b.accessCount - a.accessCount);
|
|
5540
|
+
}
|
|
5541
|
+
} else if (sort === "oldest") {
|
|
5542
|
+
events.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
|
5543
|
+
} else {
|
|
5544
|
+
events.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
|
|
5545
|
+
}
|
|
5546
|
+
events = events.slice(0, limit);
|
|
4475
5547
|
return c.json({
|
|
4476
5548
|
level,
|
|
4477
5549
|
events: events.map((e) => ({
|
|
@@ -4480,7 +5552,8 @@ statsRouter.get("/levels/:level", async (c) => {
|
|
|
4480
5552
|
sessionId: e.sessionId,
|
|
4481
5553
|
timestamp: e.timestamp.toISOString(),
|
|
4482
5554
|
content: e.content.slice(0, 500) + (e.content.length > 500 ? "..." : ""),
|
|
4483
|
-
metadata: e.metadata
|
|
5555
|
+
metadata: e.metadata,
|
|
5556
|
+
accessCount: e.accessCount || 0
|
|
4484
5557
|
})),
|
|
4485
5558
|
total: levelStat?.count || 0,
|
|
4486
5559
|
limit,
|
|
@@ -4538,24 +5611,26 @@ statsRouter.get("/", async (c) => {
|
|
|
4538
5611
|
});
|
|
4539
5612
|
statsRouter.get("/most-accessed", async (c) => {
|
|
4540
5613
|
const limit = parseInt(c.req.query("limit") || "10", 10);
|
|
4541
|
-
const
|
|
4542
|
-
const memoryService = getMemoryServiceForProject(projectPath);
|
|
5614
|
+
const memoryService = getReadOnlyMemoryService();
|
|
4543
5615
|
try {
|
|
4544
5616
|
await memoryService.initialize();
|
|
5617
|
+
console.log("[most-accessed] Fetching most accessed memories, limit:", limit);
|
|
4545
5618
|
const memories = await memoryService.getMostAccessedMemories(limit);
|
|
5619
|
+
console.log("[most-accessed] Got memories:", memories.length);
|
|
4546
5620
|
return c.json({
|
|
4547
5621
|
memories: memories.map((m) => ({
|
|
4548
5622
|
memoryId: m.memoryId,
|
|
4549
5623
|
summary: m.summary,
|
|
4550
5624
|
topics: m.topics,
|
|
4551
5625
|
accessCount: m.accessCount,
|
|
4552
|
-
lastAccessed: m.
|
|
5626
|
+
lastAccessed: m.lastAccessed || null,
|
|
4553
5627
|
confidence: m.confidence,
|
|
4554
|
-
createdAt: m.createdAt.toISOString()
|
|
5628
|
+
createdAt: m.createdAt instanceof Date ? m.createdAt.toISOString() : m.createdAt
|
|
4555
5629
|
})),
|
|
4556
5630
|
total: memories.length
|
|
4557
5631
|
});
|
|
4558
5632
|
} catch (error) {
|
|
5633
|
+
console.error("[most-accessed] Error:", error);
|
|
4559
5634
|
return c.json({
|
|
4560
5635
|
memories: [],
|
|
4561
5636
|
total: 0,
|