claude-memory-layer 1.0.7 → 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 +10 -1
- package/.claude-memory/test.sqlite +0 -0
- package/.history/package_20260201192048.json +47 -0
- package/.history/package_20260202114053.json +49 -0
- package/HANDOFF.md +92 -0
- package/dist/cli/index.js +1711 -102
- package/dist/cli/index.js.map +4 -4
- package/dist/core/index.js +1257 -84
- 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 +1382 -85
- package/dist/hooks/session-end.js.map +4 -4
- package/dist/hooks/session-start.js +1377 -84
- package/dist/hooks/session-start.js.map +4 -4
- package/dist/hooks/stop.js +1383 -86
- package/dist/hooks/stop.js.map +4 -4
- package/dist/hooks/user-prompt-submit.js +1412 -84
- package/dist/hooks/user-prompt-submit.js.map +4 -4
- package/dist/server/api/index.js +1576 -136
- package/dist/server/api/index.js.map +4 -4
- package/dist/server/index.js +1585 -143
- package/dist/server/index.js.map +4 -4
- package/dist/services/memory-service.js +1392 -84
- package/dist/services/memory-service.js.map +4 -4
- package/dist/ui/app.js +304 -0
- package/dist/ui/index.html +202 -715
- package/dist/ui/style.css +595 -0
- package/package.json +4 -1
- package/scripts/build.ts +5 -2
- package/src/cli/index.ts +226 -0
- package/src/core/db-wrapper.ts +8 -1
- package/src/core/event-store.ts +70 -3
- package/src/core/graduation-worker.ts +171 -0
- package/src/core/graduation.ts +15 -2
- package/src/core/index.ts +4 -0
- package/src/core/retriever.ts +21 -0
- 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/citations.ts +7 -3
- package/src/server/api/events.ts +7 -3
- package/src/server/api/search.ts +7 -3
- package/src/server/api/sessions.ts +7 -3
- package/src/server/api/stats.ts +159 -12
- package/src/server/index.ts +18 -9
- package/src/services/memory-service.ts +263 -46
- package/src/ui/app.js +304 -0
- package/src/ui/index.html +202 -715
- package/src/ui/style.css +595 -0
- package/test_access.js +49 -0
package/dist/cli/index.js
CHANGED
|
@@ -9,6 +9,9 @@ const __dirname = dirname(__filename);
|
|
|
9
9
|
// src/cli/index.ts
|
|
10
10
|
import { Command } from "commander";
|
|
11
11
|
import { exec } from "child_process";
|
|
12
|
+
import * as fs4 from "fs";
|
|
13
|
+
import * as path4 from "path";
|
|
14
|
+
import * as os3 from "os";
|
|
12
15
|
|
|
13
16
|
// src/services/memory-service.ts
|
|
14
17
|
import * as path from "path";
|
|
@@ -71,8 +74,11 @@ function toDate(value) {
|
|
|
71
74
|
return new Date(value);
|
|
72
75
|
return new Date(String(value));
|
|
73
76
|
}
|
|
74
|
-
function createDatabase(
|
|
75
|
-
|
|
77
|
+
function createDatabase(path5, options) {
|
|
78
|
+
if (options?.readOnly) {
|
|
79
|
+
return new duckdb.Database(path5, { access_mode: "READ_ONLY" });
|
|
80
|
+
}
|
|
81
|
+
return new duckdb.Database(path5);
|
|
76
82
|
}
|
|
77
83
|
function dbRun(db, sql, params = []) {
|
|
78
84
|
return new Promise((resolve2, reject) => {
|
|
@@ -125,18 +131,24 @@ function dbClose(db) {
|
|
|
125
131
|
|
|
126
132
|
// src/core/event-store.ts
|
|
127
133
|
var EventStore = class {
|
|
128
|
-
constructor(dbPath) {
|
|
134
|
+
constructor(dbPath, options) {
|
|
129
135
|
this.dbPath = dbPath;
|
|
130
|
-
this.
|
|
136
|
+
this.readOnly = options?.readOnly ?? false;
|
|
137
|
+
this.db = createDatabase(dbPath, { readOnly: this.readOnly });
|
|
131
138
|
}
|
|
132
139
|
db;
|
|
133
140
|
initialized = false;
|
|
141
|
+
readOnly;
|
|
134
142
|
/**
|
|
135
143
|
* Initialize database schema
|
|
136
144
|
*/
|
|
137
145
|
async initialize() {
|
|
138
146
|
if (this.initialized)
|
|
139
147
|
return;
|
|
148
|
+
if (this.readOnly) {
|
|
149
|
+
this.initialized = true;
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
140
152
|
await dbRun(this.db, `
|
|
141
153
|
CREATE TABLE IF NOT EXISTS events (
|
|
142
154
|
id VARCHAR PRIMARY KEY,
|
|
@@ -613,6 +625,36 @@ var EventStore = class {
|
|
|
613
625
|
);
|
|
614
626
|
return rows;
|
|
615
627
|
}
|
|
628
|
+
/**
|
|
629
|
+
* Get events by memory level
|
|
630
|
+
*/
|
|
631
|
+
async getEventsByLevel(level, options) {
|
|
632
|
+
await this.initialize();
|
|
633
|
+
const limit = options?.limit || 50;
|
|
634
|
+
const offset = options?.offset || 0;
|
|
635
|
+
const rows = await dbAll(
|
|
636
|
+
this.db,
|
|
637
|
+
`SELECT e.* FROM events e
|
|
638
|
+
INNER JOIN memory_levels ml ON e.id = ml.event_id
|
|
639
|
+
WHERE ml.level = ?
|
|
640
|
+
ORDER BY e.timestamp DESC
|
|
641
|
+
LIMIT ? OFFSET ?`,
|
|
642
|
+
[level, limit, offset]
|
|
643
|
+
);
|
|
644
|
+
return rows.map((row) => this.rowToEvent(row));
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Get memory level for a specific event
|
|
648
|
+
*/
|
|
649
|
+
async getEventLevel(eventId) {
|
|
650
|
+
await this.initialize();
|
|
651
|
+
const rows = await dbAll(
|
|
652
|
+
this.db,
|
|
653
|
+
`SELECT level FROM memory_levels WHERE event_id = ?`,
|
|
654
|
+
[eventId]
|
|
655
|
+
);
|
|
656
|
+
return rows.length > 0 ? rows[0].level : null;
|
|
657
|
+
}
|
|
616
658
|
// ============================================================
|
|
617
659
|
// Endless Mode Helper Methods
|
|
618
660
|
// ============================================================
|
|
@@ -667,25 +709,974 @@ var EventStore = class {
|
|
|
667
709
|
}));
|
|
668
710
|
}
|
|
669
711
|
/**
|
|
670
|
-
*
|
|
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
|
+
}
|
|
1490
|
+
/**
|
|
1491
|
+
* Close database connection
|
|
1492
|
+
*/
|
|
1493
|
+
async close() {
|
|
1494
|
+
sqliteClose(this.db);
|
|
1495
|
+
}
|
|
1496
|
+
/**
|
|
1497
|
+
* Convert database row to MemoryEvent
|
|
1498
|
+
*/
|
|
1499
|
+
rowToEvent(row) {
|
|
1500
|
+
const event = {
|
|
1501
|
+
id: row.id,
|
|
1502
|
+
eventType: row.event_type,
|
|
1503
|
+
sessionId: row.session_id,
|
|
1504
|
+
timestamp: toDateFromSQLite(row.timestamp),
|
|
1505
|
+
content: row.content,
|
|
1506
|
+
canonicalKey: row.canonical_key,
|
|
1507
|
+
dedupeKey: row.dedupe_key,
|
|
1508
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : void 0
|
|
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
|
|
671
1671
|
*/
|
|
672
|
-
|
|
673
|
-
|
|
1672
|
+
getStats() {
|
|
1673
|
+
return { ...this.stats };
|
|
674
1674
|
}
|
|
675
1675
|
/**
|
|
676
|
-
*
|
|
1676
|
+
* Check if worker is running
|
|
677
1677
|
*/
|
|
678
|
-
|
|
679
|
-
return
|
|
680
|
-
id: row.id,
|
|
681
|
-
eventType: row.event_type,
|
|
682
|
-
sessionId: row.session_id,
|
|
683
|
-
timestamp: toDate(row.timestamp),
|
|
684
|
-
content: row.content,
|
|
685
|
-
canonicalKey: row.canonical_key,
|
|
686
|
-
dedupeKey: row.dedupe_key,
|
|
687
|
-
metadata: row.metadata ? JSON.parse(row.metadata) : void 0
|
|
688
|
-
};
|
|
1678
|
+
isRunning() {
|
|
1679
|
+
return this.running;
|
|
689
1680
|
}
|
|
690
1681
|
};
|
|
691
1682
|
|
|
@@ -917,7 +1908,7 @@ function getDefaultEmbedder() {
|
|
|
917
1908
|
}
|
|
918
1909
|
|
|
919
1910
|
// src/core/vector-outbox.ts
|
|
920
|
-
var
|
|
1911
|
+
var DEFAULT_CONFIG2 = {
|
|
921
1912
|
embeddingVersion: "v1",
|
|
922
1913
|
maxRetries: 3,
|
|
923
1914
|
stuckThresholdMs: 5 * 60 * 1e3,
|
|
@@ -926,7 +1917,7 @@ var DEFAULT_CONFIG = {
|
|
|
926
1917
|
};
|
|
927
1918
|
|
|
928
1919
|
// src/core/vector-worker.ts
|
|
929
|
-
var
|
|
1920
|
+
var DEFAULT_CONFIG3 = {
|
|
930
1921
|
batchSize: 32,
|
|
931
1922
|
pollIntervalMs: 1e3,
|
|
932
1923
|
maxRetries: 3
|
|
@@ -937,12 +1928,13 @@ var VectorWorker = class {
|
|
|
937
1928
|
embedder;
|
|
938
1929
|
config;
|
|
939
1930
|
running = false;
|
|
1931
|
+
stopping = false;
|
|
940
1932
|
pollTimeout = null;
|
|
941
1933
|
constructor(eventStore, vectorStore, embedder, config = {}) {
|
|
942
1934
|
this.eventStore = eventStore;
|
|
943
1935
|
this.vectorStore = vectorStore;
|
|
944
1936
|
this.embedder = embedder;
|
|
945
|
-
this.config = { ...
|
|
1937
|
+
this.config = { ...DEFAULT_CONFIG3, ...config };
|
|
946
1938
|
}
|
|
947
1939
|
/**
|
|
948
1940
|
* Start the worker polling loop
|
|
@@ -951,6 +1943,7 @@ var VectorWorker = class {
|
|
|
951
1943
|
if (this.running)
|
|
952
1944
|
return;
|
|
953
1945
|
this.running = true;
|
|
1946
|
+
this.stopping = false;
|
|
954
1947
|
this.poll();
|
|
955
1948
|
}
|
|
956
1949
|
/**
|
|
@@ -958,6 +1951,7 @@ var VectorWorker = class {
|
|
|
958
1951
|
*/
|
|
959
1952
|
stop() {
|
|
960
1953
|
this.running = false;
|
|
1954
|
+
this.stopping = true;
|
|
961
1955
|
if (this.pollTimeout) {
|
|
962
1956
|
clearTimeout(this.pollTimeout);
|
|
963
1957
|
this.pollTimeout = null;
|
|
@@ -1007,9 +2001,15 @@ var VectorWorker = class {
|
|
|
1007
2001
|
}
|
|
1008
2002
|
return successful.length;
|
|
1009
2003
|
} catch (error) {
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
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
|
+
}
|
|
1013
2013
|
throw error;
|
|
1014
2014
|
}
|
|
1015
2015
|
}
|
|
@@ -1017,14 +2017,18 @@ var VectorWorker = class {
|
|
|
1017
2017
|
* Poll for new items
|
|
1018
2018
|
*/
|
|
1019
2019
|
async poll() {
|
|
1020
|
-
if (!this.running)
|
|
2020
|
+
if (!this.running || this.stopping)
|
|
1021
2021
|
return;
|
|
1022
2022
|
try {
|
|
1023
2023
|
await this.processBatch();
|
|
1024
2024
|
} catch (error) {
|
|
1025
|
-
|
|
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);
|
|
1026
2031
|
}
|
|
1027
|
-
this.pollTimeout = setTimeout(() => this.poll(), this.config.pollIntervalMs);
|
|
1028
2032
|
}
|
|
1029
2033
|
/**
|
|
1030
2034
|
* Process all pending items (blocking)
|
|
@@ -1051,7 +2055,7 @@ function createVectorWorker(eventStore, vectorStore, embedder, config) {
|
|
|
1051
2055
|
}
|
|
1052
2056
|
|
|
1053
2057
|
// src/core/matcher.ts
|
|
1054
|
-
var
|
|
2058
|
+
var DEFAULT_CONFIG4 = {
|
|
1055
2059
|
weights: {
|
|
1056
2060
|
semanticSimilarity: 0.4,
|
|
1057
2061
|
ftsScore: 0.25,
|
|
@@ -1066,9 +2070,9 @@ var Matcher = class {
|
|
|
1066
2070
|
config;
|
|
1067
2071
|
constructor(config = {}) {
|
|
1068
2072
|
this.config = {
|
|
1069
|
-
...
|
|
2073
|
+
...DEFAULT_CONFIG4,
|
|
1070
2074
|
...config,
|
|
1071
|
-
weights: { ...
|
|
2075
|
+
weights: { ...DEFAULT_CONFIG4.weights, ...config.weights }
|
|
1072
2076
|
};
|
|
1073
2077
|
}
|
|
1074
2078
|
/**
|
|
@@ -1195,6 +2199,7 @@ var Retriever = class {
|
|
|
1195
2199
|
matcher;
|
|
1196
2200
|
sharedStore;
|
|
1197
2201
|
sharedVectorStore;
|
|
2202
|
+
graduation;
|
|
1198
2203
|
constructor(eventStore, vectorStore, embedder, matcher, sharedOptions) {
|
|
1199
2204
|
this.eventStore = eventStore;
|
|
1200
2205
|
this.vectorStore = vectorStore;
|
|
@@ -1203,6 +2208,12 @@ var Retriever = class {
|
|
|
1203
2208
|
this.sharedStore = sharedOptions?.sharedStore;
|
|
1204
2209
|
this.sharedVectorStore = sharedOptions?.sharedVectorStore;
|
|
1205
2210
|
}
|
|
2211
|
+
/**
|
|
2212
|
+
* Set graduation pipeline for access tracking
|
|
2213
|
+
*/
|
|
2214
|
+
setGraduationPipeline(graduation) {
|
|
2215
|
+
this.graduation = graduation;
|
|
2216
|
+
}
|
|
1206
2217
|
/**
|
|
1207
2218
|
* Set shared stores after construction
|
|
1208
2219
|
*/
|
|
@@ -1325,6 +2336,13 @@ var Retriever = class {
|
|
|
1325
2336
|
const event = await this.eventStore.getEvent(result.eventId);
|
|
1326
2337
|
if (!event)
|
|
1327
2338
|
continue;
|
|
2339
|
+
if (this.graduation) {
|
|
2340
|
+
this.graduation.recordAccess(
|
|
2341
|
+
event.id,
|
|
2342
|
+
options.sessionId || "unknown",
|
|
2343
|
+
result.score
|
|
2344
|
+
);
|
|
2345
|
+
}
|
|
1328
2346
|
let sessionContext;
|
|
1329
2347
|
if (options.includeSessionContext) {
|
|
1330
2348
|
sessionContext = await this.getSessionContext(event.sessionId, event.id);
|
|
@@ -1446,15 +2464,26 @@ var GraduationPipeline = class {
|
|
|
1446
2464
|
L3toL4: { ...DEFAULT_CRITERIA.L3toL4, ...criteria.L3toL4 }
|
|
1447
2465
|
};
|
|
1448
2466
|
}
|
|
2467
|
+
// Track which sessions have accessed each event
|
|
2468
|
+
sessionAccesses = /* @__PURE__ */ new Map();
|
|
1449
2469
|
/**
|
|
1450
2470
|
* Record an access to an event (used for graduation scoring)
|
|
1451
2471
|
*/
|
|
1452
2472
|
recordAccess(eventId, fromSessionId, confidence = 1) {
|
|
1453
2473
|
const existing = this.metrics.get(eventId);
|
|
2474
|
+
if (!this.sessionAccesses.has(eventId)) {
|
|
2475
|
+
this.sessionAccesses.set(eventId, /* @__PURE__ */ new Set());
|
|
2476
|
+
}
|
|
2477
|
+
const sessions = this.sessionAccesses.get(eventId);
|
|
2478
|
+
const isNewSession = !sessions.has(fromSessionId);
|
|
2479
|
+
sessions.add(fromSessionId);
|
|
1454
2480
|
if (existing) {
|
|
1455
2481
|
existing.accessCount++;
|
|
1456
2482
|
existing.lastAccessed = /* @__PURE__ */ new Date();
|
|
1457
2483
|
existing.confidence = Math.max(existing.confidence, confidence);
|
|
2484
|
+
if (isNewSession && sessions.size > 1) {
|
|
2485
|
+
existing.crossSessionRefs = sessions.size - 1;
|
|
2486
|
+
}
|
|
1458
2487
|
} else {
|
|
1459
2488
|
this.metrics.set(eventId, {
|
|
1460
2489
|
eventId,
|
|
@@ -1751,7 +2780,7 @@ function createSharedEventStore(dbPath) {
|
|
|
1751
2780
|
}
|
|
1752
2781
|
|
|
1753
2782
|
// src/core/shared-store.ts
|
|
1754
|
-
import { randomUUID as
|
|
2783
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
1755
2784
|
var SharedStore = class {
|
|
1756
2785
|
constructor(sharedEventStore) {
|
|
1757
2786
|
this.sharedEventStore = sharedEventStore;
|
|
@@ -1763,7 +2792,7 @@ var SharedStore = class {
|
|
|
1763
2792
|
* Promote a verified troubleshooting entry to shared storage
|
|
1764
2793
|
*/
|
|
1765
2794
|
async promoteEntry(input) {
|
|
1766
|
-
const entryId =
|
|
2795
|
+
const entryId = randomUUID3();
|
|
1767
2796
|
await dbRun(
|
|
1768
2797
|
this.db,
|
|
1769
2798
|
`INSERT INTO shared_troubleshooting (
|
|
@@ -2128,7 +3157,7 @@ function createSharedVectorStore(dbPath) {
|
|
|
2128
3157
|
}
|
|
2129
3158
|
|
|
2130
3159
|
// src/core/shared-promoter.ts
|
|
2131
|
-
import { randomUUID as
|
|
3160
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
2132
3161
|
var SharedPromoter = class {
|
|
2133
3162
|
constructor(sharedStore, sharedVectorStore, embedder, config) {
|
|
2134
3163
|
this.sharedStore = sharedStore;
|
|
@@ -2196,7 +3225,7 @@ var SharedPromoter = class {
|
|
|
2196
3225
|
const embeddingContent = this.createEmbeddingContent(input);
|
|
2197
3226
|
const embedding = await this.embedder.embed(embeddingContent);
|
|
2198
3227
|
await this.sharedVectorStore.upsert({
|
|
2199
|
-
id:
|
|
3228
|
+
id: randomUUID4(),
|
|
2200
3229
|
entryId,
|
|
2201
3230
|
entryType: "troubleshooting",
|
|
2202
3231
|
content: embeddingContent,
|
|
@@ -2336,7 +3365,7 @@ function createToolObservationEmbedding(toolName, metadata, success) {
|
|
|
2336
3365
|
}
|
|
2337
3366
|
|
|
2338
3367
|
// src/core/working-set-store.ts
|
|
2339
|
-
import { randomUUID as
|
|
3368
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
2340
3369
|
var WorkingSetStore = class {
|
|
2341
3370
|
constructor(eventStore, config) {
|
|
2342
3371
|
this.eventStore = eventStore;
|
|
@@ -2357,7 +3386,7 @@ var WorkingSetStore = class {
|
|
|
2357
3386
|
`INSERT OR REPLACE INTO working_set (id, event_id, added_at, relevance_score, topics, expires_at)
|
|
2358
3387
|
VALUES (?, ?, CURRENT_TIMESTAMP, ?, ?, ?)`,
|
|
2359
3388
|
[
|
|
2360
|
-
|
|
3389
|
+
randomUUID5(),
|
|
2361
3390
|
eventId,
|
|
2362
3391
|
relevanceScore,
|
|
2363
3392
|
JSON.stringify(topics || []),
|
|
@@ -2541,7 +3570,7 @@ function createWorkingSetStore(eventStore, config) {
|
|
|
2541
3570
|
}
|
|
2542
3571
|
|
|
2543
3572
|
// src/core/consolidated-store.ts
|
|
2544
|
-
import { randomUUID as
|
|
3573
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
2545
3574
|
var ConsolidatedStore = class {
|
|
2546
3575
|
constructor(eventStore) {
|
|
2547
3576
|
this.eventStore = eventStore;
|
|
@@ -2553,7 +3582,7 @@ var ConsolidatedStore = class {
|
|
|
2553
3582
|
* Create a new consolidated memory
|
|
2554
3583
|
*/
|
|
2555
3584
|
async create(input) {
|
|
2556
|
-
const memoryId =
|
|
3585
|
+
const memoryId = randomUUID6();
|
|
2557
3586
|
await dbRun(
|
|
2558
3587
|
this.db,
|
|
2559
3588
|
`INSERT INTO consolidated_memories
|
|
@@ -3080,7 +4109,7 @@ function createConsolidationWorker(workingSetStore, consolidatedStore, config) {
|
|
|
3080
4109
|
}
|
|
3081
4110
|
|
|
3082
4111
|
// src/core/continuity-manager.ts
|
|
3083
|
-
import { randomUUID as
|
|
4112
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
3084
4113
|
var ContinuityManager = class {
|
|
3085
4114
|
constructor(eventStore, config) {
|
|
3086
4115
|
this.eventStore = eventStore;
|
|
@@ -3234,7 +4263,7 @@ var ContinuityManager = class {
|
|
|
3234
4263
|
`INSERT INTO continuity_log
|
|
3235
4264
|
(log_id, from_context_id, to_context_id, continuity_score, transition_type, created_at)
|
|
3236
4265
|
VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,
|
|
3237
|
-
[
|
|
4266
|
+
[randomUUID7(), previous.id, current.id, score, type]
|
|
3238
4267
|
);
|
|
3239
4268
|
}
|
|
3240
4269
|
/**
|
|
@@ -3342,6 +4371,127 @@ function createContinuityManager(eventStore, config) {
|
|
|
3342
4371
|
return new ContinuityManager(eventStore, config);
|
|
3343
4372
|
}
|
|
3344
4373
|
|
|
4374
|
+
// src/core/graduation-worker.ts
|
|
4375
|
+
var DEFAULT_CONFIG5 = {
|
|
4376
|
+
evaluationIntervalMs: 3e5,
|
|
4377
|
+
// 5 minutes
|
|
4378
|
+
batchSize: 50,
|
|
4379
|
+
cooldownMs: 36e5
|
|
4380
|
+
// 1 hour cooldown between evaluations
|
|
4381
|
+
};
|
|
4382
|
+
var GraduationWorker = class {
|
|
4383
|
+
constructor(eventStore, graduation, config = DEFAULT_CONFIG5) {
|
|
4384
|
+
this.eventStore = eventStore;
|
|
4385
|
+
this.graduation = graduation;
|
|
4386
|
+
this.config = config;
|
|
4387
|
+
}
|
|
4388
|
+
running = false;
|
|
4389
|
+
timeout = null;
|
|
4390
|
+
lastEvaluated = /* @__PURE__ */ new Map();
|
|
4391
|
+
/**
|
|
4392
|
+
* Start the graduation worker
|
|
4393
|
+
*/
|
|
4394
|
+
start() {
|
|
4395
|
+
if (this.running)
|
|
4396
|
+
return;
|
|
4397
|
+
this.running = true;
|
|
4398
|
+
this.scheduleNext();
|
|
4399
|
+
}
|
|
4400
|
+
/**
|
|
4401
|
+
* Stop the graduation worker
|
|
4402
|
+
*/
|
|
4403
|
+
stop() {
|
|
4404
|
+
this.running = false;
|
|
4405
|
+
if (this.timeout) {
|
|
4406
|
+
clearTimeout(this.timeout);
|
|
4407
|
+
this.timeout = null;
|
|
4408
|
+
}
|
|
4409
|
+
}
|
|
4410
|
+
/**
|
|
4411
|
+
* Check if currently running
|
|
4412
|
+
*/
|
|
4413
|
+
isRunning() {
|
|
4414
|
+
return this.running;
|
|
4415
|
+
}
|
|
4416
|
+
/**
|
|
4417
|
+
* Force a graduation evaluation run
|
|
4418
|
+
*/
|
|
4419
|
+
async forceRun() {
|
|
4420
|
+
return await this.runGraduation();
|
|
4421
|
+
}
|
|
4422
|
+
/**
|
|
4423
|
+
* Schedule the next graduation check
|
|
4424
|
+
*/
|
|
4425
|
+
scheduleNext() {
|
|
4426
|
+
if (!this.running)
|
|
4427
|
+
return;
|
|
4428
|
+
this.timeout = setTimeout(
|
|
4429
|
+
() => this.run(),
|
|
4430
|
+
this.config.evaluationIntervalMs
|
|
4431
|
+
);
|
|
4432
|
+
}
|
|
4433
|
+
/**
|
|
4434
|
+
* Run graduation evaluation
|
|
4435
|
+
*/
|
|
4436
|
+
async run() {
|
|
4437
|
+
if (!this.running)
|
|
4438
|
+
return;
|
|
4439
|
+
try {
|
|
4440
|
+
await this.runGraduation();
|
|
4441
|
+
} catch (error) {
|
|
4442
|
+
console.error("Graduation error:", error);
|
|
4443
|
+
}
|
|
4444
|
+
this.scheduleNext();
|
|
4445
|
+
}
|
|
4446
|
+
/**
|
|
4447
|
+
* Perform graduation evaluation across all levels
|
|
4448
|
+
*/
|
|
4449
|
+
async runGraduation() {
|
|
4450
|
+
const result = {
|
|
4451
|
+
evaluated: 0,
|
|
4452
|
+
graduated: 0,
|
|
4453
|
+
byLevel: {}
|
|
4454
|
+
};
|
|
4455
|
+
const levels = ["L0", "L1", "L2", "L3"];
|
|
4456
|
+
const now = Date.now();
|
|
4457
|
+
for (const level of levels) {
|
|
4458
|
+
const events = await this.eventStore.getEventsByLevel(level, {
|
|
4459
|
+
limit: this.config.batchSize
|
|
4460
|
+
});
|
|
4461
|
+
let levelGraduated = 0;
|
|
4462
|
+
for (const event of events) {
|
|
4463
|
+
const lastEval = this.lastEvaluated.get(event.id);
|
|
4464
|
+
if (lastEval && now - lastEval < this.config.cooldownMs) {
|
|
4465
|
+
continue;
|
|
4466
|
+
}
|
|
4467
|
+
result.evaluated++;
|
|
4468
|
+
this.lastEvaluated.set(event.id, now);
|
|
4469
|
+
const gradResult = await this.graduation.evaluateGraduation(event.id, level);
|
|
4470
|
+
if (gradResult.success) {
|
|
4471
|
+
result.graduated++;
|
|
4472
|
+
levelGraduated++;
|
|
4473
|
+
}
|
|
4474
|
+
}
|
|
4475
|
+
if (levelGraduated > 0) {
|
|
4476
|
+
result.byLevel[level] = levelGraduated;
|
|
4477
|
+
}
|
|
4478
|
+
}
|
|
4479
|
+
if (this.lastEvaluated.size > 1e3) {
|
|
4480
|
+
const entries = Array.from(this.lastEvaluated.entries());
|
|
4481
|
+
entries.sort((a, b) => b[1] - a[1]);
|
|
4482
|
+
this.lastEvaluated = new Map(entries.slice(0, 1e3));
|
|
4483
|
+
}
|
|
4484
|
+
return result;
|
|
4485
|
+
}
|
|
4486
|
+
};
|
|
4487
|
+
function createGraduationWorker(eventStore, graduation, config) {
|
|
4488
|
+
return new GraduationWorker(
|
|
4489
|
+
eventStore,
|
|
4490
|
+
graduation,
|
|
4491
|
+
{ ...DEFAULT_CONFIG5, ...config }
|
|
4492
|
+
);
|
|
4493
|
+
}
|
|
4494
|
+
|
|
3345
4495
|
// src/services/memory-service.ts
|
|
3346
4496
|
function normalizePath(projectPath) {
|
|
3347
4497
|
const expanded = projectPath.startsWith("~") ? path.join(os.homedir(), projectPath.slice(1)) : projectPath;
|
|
@@ -3362,13 +4512,18 @@ function getProjectStoragePath(projectPath) {
|
|
|
3362
4512
|
var REGISTRY_PATH = path.join(os.homedir(), ".claude-code", "memory", "session-registry.json");
|
|
3363
4513
|
var SHARED_STORAGE_PATH = path.join(os.homedir(), ".claude-code", "memory", "shared");
|
|
3364
4514
|
var MemoryService = class {
|
|
3365
|
-
|
|
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;
|
|
3366
4520
|
vectorStore;
|
|
3367
4521
|
embedder;
|
|
3368
4522
|
matcher;
|
|
3369
4523
|
retriever;
|
|
3370
4524
|
graduation;
|
|
3371
4525
|
vectorWorker = null;
|
|
4526
|
+
graduationWorker = null;
|
|
3372
4527
|
initialized = false;
|
|
3373
4528
|
// Endless Mode components
|
|
3374
4529
|
workingSetStore = null;
|
|
@@ -3383,24 +4538,48 @@ var MemoryService = class {
|
|
|
3383
4538
|
sharedPromoter = null;
|
|
3384
4539
|
sharedStoreConfig = null;
|
|
3385
4540
|
projectHash = null;
|
|
4541
|
+
readOnly;
|
|
3386
4542
|
constructor(config) {
|
|
3387
4543
|
const storagePath = this.expandPath(config.storagePath);
|
|
3388
|
-
|
|
4544
|
+
this.readOnly = config.readOnly ?? false;
|
|
4545
|
+
if (!this.readOnly && !fs.existsSync(storagePath)) {
|
|
3389
4546
|
fs.mkdirSync(storagePath, { recursive: true });
|
|
3390
4547
|
}
|
|
3391
4548
|
this.projectHash = config.projectHash || null;
|
|
3392
4549
|
this.sharedStoreConfig = config.sharedStoreConfig ?? { enabled: true };
|
|
3393
|
-
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
|
+
}
|
|
3394
4572
|
this.vectorStore = new VectorStore(path.join(storagePath, "vectors"));
|
|
3395
4573
|
this.embedder = config.embeddingModel ? new Embedder(config.embeddingModel) : getDefaultEmbedder();
|
|
3396
4574
|
this.matcher = getDefaultMatcher();
|
|
3397
4575
|
this.retriever = createRetriever(
|
|
3398
|
-
this.
|
|
4576
|
+
this.sqliteStore,
|
|
4577
|
+
// Interface compatible
|
|
3399
4578
|
this.vectorStore,
|
|
3400
4579
|
this.embedder,
|
|
3401
4580
|
this.matcher
|
|
3402
4581
|
);
|
|
3403
|
-
this.graduation = createGraduationPipeline(this.
|
|
4582
|
+
this.graduation = createGraduationPipeline(this.sqliteStore);
|
|
3404
4583
|
}
|
|
3405
4584
|
/**
|
|
3406
4585
|
* Initialize all components
|
|
@@ -3408,22 +4587,45 @@ var MemoryService = class {
|
|
|
3408
4587
|
async initialize() {
|
|
3409
4588
|
if (this.initialized)
|
|
3410
4589
|
return;
|
|
3411
|
-
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
|
+
}
|
|
3412
4598
|
await this.vectorStore.initialize();
|
|
3413
4599
|
await this.embedder.initialize();
|
|
3414
|
-
this.
|
|
3415
|
-
this.
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
this.
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
4600
|
+
if (!this.readOnly) {
|
|
4601
|
+
this.vectorWorker = createVectorWorker(
|
|
4602
|
+
this.sqliteStore,
|
|
4603
|
+
this.vectorStore,
|
|
4604
|
+
this.embedder
|
|
4605
|
+
);
|
|
4606
|
+
this.vectorWorker.start();
|
|
4607
|
+
this.retriever.setGraduationPipeline(this.graduation);
|
|
4608
|
+
this.graduationWorker = createGraduationWorker(
|
|
4609
|
+
this.sqliteStore,
|
|
4610
|
+
this.graduation
|
|
4611
|
+
);
|
|
4612
|
+
this.graduationWorker.start();
|
|
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");
|
|
4622
|
+
if (savedMode === "endless") {
|
|
4623
|
+
this.endlessMode = "endless";
|
|
4624
|
+
await this.initializeEndlessMode();
|
|
4625
|
+
}
|
|
4626
|
+
if (this.sharedStoreConfig?.enabled !== false) {
|
|
4627
|
+
await this.initializeSharedStore();
|
|
4628
|
+
}
|
|
3427
4629
|
}
|
|
3428
4630
|
this.initialized = true;
|
|
3429
4631
|
}
|
|
@@ -3457,7 +4659,7 @@ var MemoryService = class {
|
|
|
3457
4659
|
*/
|
|
3458
4660
|
async startSession(sessionId, projectPath) {
|
|
3459
4661
|
await this.initialize();
|
|
3460
|
-
await this.
|
|
4662
|
+
await this.sqliteStore.upsertSession({
|
|
3461
4663
|
id: sessionId,
|
|
3462
4664
|
startedAt: /* @__PURE__ */ new Date(),
|
|
3463
4665
|
projectPath
|
|
@@ -3468,7 +4670,7 @@ var MemoryService = class {
|
|
|
3468
4670
|
*/
|
|
3469
4671
|
async endSession(sessionId, summary) {
|
|
3470
4672
|
await this.initialize();
|
|
3471
|
-
await this.
|
|
4673
|
+
await this.sqliteStore.upsertSession({
|
|
3472
4674
|
id: sessionId,
|
|
3473
4675
|
endedAt: /* @__PURE__ */ new Date(),
|
|
3474
4676
|
summary
|
|
@@ -3479,7 +4681,7 @@ var MemoryService = class {
|
|
|
3479
4681
|
*/
|
|
3480
4682
|
async storeUserPrompt(sessionId, content, metadata) {
|
|
3481
4683
|
await this.initialize();
|
|
3482
|
-
const result = await this.
|
|
4684
|
+
const result = await this.sqliteStore.append({
|
|
3483
4685
|
eventType: "user_prompt",
|
|
3484
4686
|
sessionId,
|
|
3485
4687
|
timestamp: /* @__PURE__ */ new Date(),
|
|
@@ -3487,7 +4689,7 @@ var MemoryService = class {
|
|
|
3487
4689
|
metadata
|
|
3488
4690
|
});
|
|
3489
4691
|
if (result.success && !result.isDuplicate) {
|
|
3490
|
-
await this.
|
|
4692
|
+
await this.sqliteStore.enqueueForEmbedding(result.eventId, content);
|
|
3491
4693
|
}
|
|
3492
4694
|
return result;
|
|
3493
4695
|
}
|
|
@@ -3496,7 +4698,7 @@ var MemoryService = class {
|
|
|
3496
4698
|
*/
|
|
3497
4699
|
async storeAgentResponse(sessionId, content, metadata) {
|
|
3498
4700
|
await this.initialize();
|
|
3499
|
-
const result = await this.
|
|
4701
|
+
const result = await this.sqliteStore.append({
|
|
3500
4702
|
eventType: "agent_response",
|
|
3501
4703
|
sessionId,
|
|
3502
4704
|
timestamp: /* @__PURE__ */ new Date(),
|
|
@@ -3504,7 +4706,7 @@ var MemoryService = class {
|
|
|
3504
4706
|
metadata
|
|
3505
4707
|
});
|
|
3506
4708
|
if (result.success && !result.isDuplicate) {
|
|
3507
|
-
await this.
|
|
4709
|
+
await this.sqliteStore.enqueueForEmbedding(result.eventId, content);
|
|
3508
4710
|
}
|
|
3509
4711
|
return result;
|
|
3510
4712
|
}
|
|
@@ -3513,14 +4715,14 @@ var MemoryService = class {
|
|
|
3513
4715
|
*/
|
|
3514
4716
|
async storeSessionSummary(sessionId, summary) {
|
|
3515
4717
|
await this.initialize();
|
|
3516
|
-
const result = await this.
|
|
4718
|
+
const result = await this.sqliteStore.append({
|
|
3517
4719
|
eventType: "session_summary",
|
|
3518
4720
|
sessionId,
|
|
3519
4721
|
timestamp: /* @__PURE__ */ new Date(),
|
|
3520
4722
|
content: summary
|
|
3521
4723
|
});
|
|
3522
4724
|
if (result.success && !result.isDuplicate) {
|
|
3523
|
-
await this.
|
|
4725
|
+
await this.sqliteStore.enqueueForEmbedding(result.eventId, summary);
|
|
3524
4726
|
}
|
|
3525
4727
|
return result;
|
|
3526
4728
|
}
|
|
@@ -3530,7 +4732,7 @@ var MemoryService = class {
|
|
|
3530
4732
|
async storeToolObservation(sessionId, payload) {
|
|
3531
4733
|
await this.initialize();
|
|
3532
4734
|
const content = JSON.stringify(payload);
|
|
3533
|
-
const result = await this.
|
|
4735
|
+
const result = await this.sqliteStore.append({
|
|
3534
4736
|
eventType: "tool_observation",
|
|
3535
4737
|
sessionId,
|
|
3536
4738
|
timestamp: /* @__PURE__ */ new Date(),
|
|
@@ -3546,7 +4748,7 @@ var MemoryService = class {
|
|
|
3546
4748
|
payload.metadata || {},
|
|
3547
4749
|
payload.success
|
|
3548
4750
|
);
|
|
3549
|
-
await this.
|
|
4751
|
+
await this.sqliteStore.enqueueForEmbedding(result.eventId, embeddingContent);
|
|
3550
4752
|
}
|
|
3551
4753
|
return result;
|
|
3552
4754
|
}
|
|
@@ -3572,21 +4774,21 @@ var MemoryService = class {
|
|
|
3572
4774
|
*/
|
|
3573
4775
|
async getSessionHistory(sessionId) {
|
|
3574
4776
|
await this.initialize();
|
|
3575
|
-
return this.
|
|
4777
|
+
return this.sqliteStore.getSessionEvents(sessionId);
|
|
3576
4778
|
}
|
|
3577
4779
|
/**
|
|
3578
4780
|
* Get recent events
|
|
3579
4781
|
*/
|
|
3580
4782
|
async getRecentEvents(limit = 100) {
|
|
3581
4783
|
await this.initialize();
|
|
3582
|
-
return this.
|
|
4784
|
+
return this.sqliteStore.getRecentEvents(limit);
|
|
3583
4785
|
}
|
|
3584
4786
|
/**
|
|
3585
4787
|
* Get memory statistics
|
|
3586
4788
|
*/
|
|
3587
4789
|
async getStats() {
|
|
3588
4790
|
await this.initialize();
|
|
3589
|
-
const recentEvents = await this.
|
|
4791
|
+
const recentEvents = await this.sqliteStore.getRecentEvents(1e4);
|
|
3590
4792
|
const vectorCount = await this.vectorStore.count();
|
|
3591
4793
|
const levelStats = await this.graduation.getStats();
|
|
3592
4794
|
return {
|
|
@@ -3604,6 +4806,20 @@ var MemoryService = class {
|
|
|
3604
4806
|
}
|
|
3605
4807
|
return 0;
|
|
3606
4808
|
}
|
|
4809
|
+
/**
|
|
4810
|
+
* Get events by memory level
|
|
4811
|
+
*/
|
|
4812
|
+
async getEventsByLevel(level, options) {
|
|
4813
|
+
await this.initialize();
|
|
4814
|
+
return this.sqliteStore.getEventsByLevel(level, options);
|
|
4815
|
+
}
|
|
4816
|
+
/**
|
|
4817
|
+
* Get memory level for a specific event
|
|
4818
|
+
*/
|
|
4819
|
+
async getEventLevel(eventId) {
|
|
4820
|
+
await this.initialize();
|
|
4821
|
+
return this.sqliteStore.getEventLevel(eventId);
|
|
4822
|
+
}
|
|
3607
4823
|
/**
|
|
3608
4824
|
* Format retrieval results as context for Claude
|
|
3609
4825
|
*/
|
|
@@ -3696,21 +4912,21 @@ var MemoryService = class {
|
|
|
3696
4912
|
*/
|
|
3697
4913
|
async initializeEndlessMode() {
|
|
3698
4914
|
const config = await this.getEndlessConfig();
|
|
3699
|
-
this.workingSetStore = createWorkingSetStore(this.
|
|
3700
|
-
this.consolidatedStore = createConsolidatedStore(this.
|
|
4915
|
+
this.workingSetStore = createWorkingSetStore(this.sqliteStore, config);
|
|
4916
|
+
this.consolidatedStore = createConsolidatedStore(this.sqliteStore);
|
|
3701
4917
|
this.consolidationWorker = createConsolidationWorker(
|
|
3702
4918
|
this.workingSetStore,
|
|
3703
4919
|
this.consolidatedStore,
|
|
3704
4920
|
config
|
|
3705
4921
|
);
|
|
3706
|
-
this.continuityManager = createContinuityManager(this.
|
|
4922
|
+
this.continuityManager = createContinuityManager(this.sqliteStore, config);
|
|
3707
4923
|
this.consolidationWorker.start();
|
|
3708
4924
|
}
|
|
3709
4925
|
/**
|
|
3710
4926
|
* Get Endless Mode configuration
|
|
3711
4927
|
*/
|
|
3712
4928
|
async getEndlessConfig() {
|
|
3713
|
-
const savedConfig = await this.
|
|
4929
|
+
const savedConfig = await this.sqliteStore.getEndlessConfig("config");
|
|
3714
4930
|
return savedConfig || this.getDefaultEndlessConfig();
|
|
3715
4931
|
}
|
|
3716
4932
|
/**
|
|
@@ -3719,7 +4935,7 @@ var MemoryService = class {
|
|
|
3719
4935
|
async setEndlessConfig(config) {
|
|
3720
4936
|
const current = await this.getEndlessConfig();
|
|
3721
4937
|
const merged = { ...current, ...config };
|
|
3722
|
-
await this.
|
|
4938
|
+
await this.sqliteStore.setEndlessConfig("config", merged);
|
|
3723
4939
|
}
|
|
3724
4940
|
/**
|
|
3725
4941
|
* Set memory mode (session or endless)
|
|
@@ -3729,7 +4945,7 @@ var MemoryService = class {
|
|
|
3729
4945
|
if (mode === this.endlessMode)
|
|
3730
4946
|
return;
|
|
3731
4947
|
this.endlessMode = mode;
|
|
3732
|
-
await this.
|
|
4948
|
+
await this.sqliteStore.setEndlessConfig("mode", mode);
|
|
3733
4949
|
if (mode === "endless") {
|
|
3734
4950
|
await this.initializeEndlessMode();
|
|
3735
4951
|
} else {
|
|
@@ -3786,6 +5002,59 @@ var MemoryService = class {
|
|
|
3786
5002
|
return [];
|
|
3787
5003
|
return this.consolidatedStore.getAll({ limit });
|
|
3788
5004
|
}
|
|
5005
|
+
/**
|
|
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
|
|
5019
|
+
*/
|
|
5020
|
+
async getMostAccessedMemories(limit = 10) {
|
|
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 [];
|
|
5049
|
+
}
|
|
5050
|
+
/**
|
|
5051
|
+
* Mark a consolidated memory as accessed
|
|
5052
|
+
*/
|
|
5053
|
+
async markMemoryAccessed(memoryId) {
|
|
5054
|
+
if (!this.consolidatedStore)
|
|
5055
|
+
return;
|
|
5056
|
+
await this.consolidatedStore.markAccessed(memoryId);
|
|
5057
|
+
}
|
|
3789
5058
|
/**
|
|
3790
5059
|
* Calculate continuity score for current context
|
|
3791
5060
|
*/
|
|
@@ -3873,20 +5142,44 @@ var MemoryService = class {
|
|
|
3873
5142
|
}
|
|
3874
5143
|
return parts.join("\n");
|
|
3875
5144
|
}
|
|
5145
|
+
/**
|
|
5146
|
+
* Force a graduation evaluation run
|
|
5147
|
+
*/
|
|
5148
|
+
async forceGraduation() {
|
|
5149
|
+
if (!this.graduationWorker) {
|
|
5150
|
+
return { evaluated: 0, graduated: 0, byLevel: {} };
|
|
5151
|
+
}
|
|
5152
|
+
return this.graduationWorker.forceRun();
|
|
5153
|
+
}
|
|
5154
|
+
/**
|
|
5155
|
+
* Record access to a memory event (for graduation scoring)
|
|
5156
|
+
*/
|
|
5157
|
+
recordMemoryAccess(eventId, sessionId, confidence = 1) {
|
|
5158
|
+
this.graduation.recordAccess(eventId, sessionId, confidence);
|
|
5159
|
+
}
|
|
3876
5160
|
/**
|
|
3877
5161
|
* Shutdown service
|
|
3878
5162
|
*/
|
|
3879
5163
|
async shutdown() {
|
|
5164
|
+
if (this.graduationWorker) {
|
|
5165
|
+
this.graduationWorker.stop();
|
|
5166
|
+
}
|
|
3880
5167
|
if (this.consolidationWorker) {
|
|
3881
5168
|
this.consolidationWorker.stop();
|
|
3882
5169
|
}
|
|
3883
5170
|
if (this.vectorWorker) {
|
|
3884
5171
|
this.vectorWorker.stop();
|
|
3885
5172
|
}
|
|
5173
|
+
if (this.syncWorker) {
|
|
5174
|
+
this.syncWorker.stop();
|
|
5175
|
+
}
|
|
3886
5176
|
if (this.sharedEventStore) {
|
|
3887
5177
|
await this.sharedEventStore.close();
|
|
3888
5178
|
}
|
|
3889
|
-
await this.
|
|
5179
|
+
await this.sqliteStore.close();
|
|
5180
|
+
if (this.analyticsStore) {
|
|
5181
|
+
await this.analyticsStore.close();
|
|
5182
|
+
}
|
|
3890
5183
|
}
|
|
3891
5184
|
/**
|
|
3892
5185
|
* Expand ~ to home directory
|
|
@@ -3903,11 +5196,25 @@ var GLOBAL_KEY = "__global__";
|
|
|
3903
5196
|
function getDefaultMemoryService() {
|
|
3904
5197
|
if (!serviceCache.has(GLOBAL_KEY)) {
|
|
3905
5198
|
serviceCache.set(GLOBAL_KEY, new MemoryService({
|
|
3906
|
-
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
|
|
3907
5204
|
}));
|
|
3908
5205
|
}
|
|
3909
5206
|
return serviceCache.get(GLOBAL_KEY);
|
|
3910
5207
|
}
|
|
5208
|
+
function getReadOnlyMemoryService() {
|
|
5209
|
+
return new MemoryService({
|
|
5210
|
+
storagePath: "~/.claude-code/memory",
|
|
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
|
|
5216
|
+
});
|
|
5217
|
+
}
|
|
3911
5218
|
function getMemoryServiceForProject(projectPath, sharedStoreConfig) {
|
|
3912
5219
|
const hash = hashProjectPath(projectPath);
|
|
3913
5220
|
if (!serviceCache.has(hash)) {
|
|
@@ -3915,7 +5222,10 @@ function getMemoryServiceForProject(projectPath, sharedStoreConfig) {
|
|
|
3915
5222
|
serviceCache.set(hash, new MemoryService({
|
|
3916
5223
|
storagePath,
|
|
3917
5224
|
projectHash: hash,
|
|
3918
|
-
|
|
5225
|
+
// Override shared store config - hooks don't need DuckDB
|
|
5226
|
+
sharedStoreConfig: sharedStoreConfig ?? { enabled: false },
|
|
5227
|
+
analyticsEnabled: false
|
|
5228
|
+
// Hooks don't need DuckDB
|
|
3919
5229
|
}));
|
|
3920
5230
|
}
|
|
3921
5231
|
return serviceCache.get(hash);
|
|
@@ -4163,7 +5473,8 @@ function createSessionHistoryImporter(memoryService) {
|
|
|
4163
5473
|
import { Hono as Hono7 } from "hono";
|
|
4164
5474
|
import { cors } from "hono/cors";
|
|
4165
5475
|
import { logger } from "hono/logger";
|
|
4166
|
-
import {
|
|
5476
|
+
import { serve } from "@hono/node-server";
|
|
5477
|
+
import { serveStatic } from "@hono/node-server/serve-static";
|
|
4167
5478
|
import * as path3 from "path";
|
|
4168
5479
|
import * as fs3 from "fs";
|
|
4169
5480
|
|
|
@@ -4176,8 +5487,8 @@ var sessionsRouter = new Hono();
|
|
|
4176
5487
|
sessionsRouter.get("/", async (c) => {
|
|
4177
5488
|
const page = parseInt(c.req.query("page") || "1", 10);
|
|
4178
5489
|
const pageSize = parseInt(c.req.query("pageSize") || "20", 10);
|
|
5490
|
+
const memoryService = getReadOnlyMemoryService();
|
|
4179
5491
|
try {
|
|
4180
|
-
const memoryService = getDefaultMemoryService();
|
|
4181
5492
|
await memoryService.initialize();
|
|
4182
5493
|
const recentEvents = await memoryService.getRecentEvents(1e3);
|
|
4183
5494
|
const sessionMap = /* @__PURE__ */ new Map();
|
|
@@ -4214,12 +5525,14 @@ sessionsRouter.get("/", async (c) => {
|
|
|
4214
5525
|
});
|
|
4215
5526
|
} catch (error) {
|
|
4216
5527
|
return c.json({ error: error.message }, 500);
|
|
5528
|
+
} finally {
|
|
5529
|
+
await memoryService.shutdown();
|
|
4217
5530
|
}
|
|
4218
5531
|
});
|
|
4219
5532
|
sessionsRouter.get("/:id", async (c) => {
|
|
4220
5533
|
const { id } = c.req.param();
|
|
5534
|
+
const memoryService = getReadOnlyMemoryService();
|
|
4221
5535
|
try {
|
|
4222
|
-
const memoryService = getDefaultMemoryService();
|
|
4223
5536
|
await memoryService.initialize();
|
|
4224
5537
|
const events = await memoryService.getSessionHistory(id);
|
|
4225
5538
|
if (events.length === 0) {
|
|
@@ -4248,6 +5561,8 @@ sessionsRouter.get("/:id", async (c) => {
|
|
|
4248
5561
|
});
|
|
4249
5562
|
} catch (error) {
|
|
4250
5563
|
return c.json({ error: error.message }, 500);
|
|
5564
|
+
} finally {
|
|
5565
|
+
await memoryService.shutdown();
|
|
4251
5566
|
}
|
|
4252
5567
|
});
|
|
4253
5568
|
|
|
@@ -4259,8 +5574,8 @@ eventsRouter.get("/", async (c) => {
|
|
|
4259
5574
|
const eventType = c.req.query("type");
|
|
4260
5575
|
const limit = parseInt(c.req.query("limit") || "100", 10);
|
|
4261
5576
|
const offset = parseInt(c.req.query("offset") || "0", 10);
|
|
5577
|
+
const memoryService = getReadOnlyMemoryService();
|
|
4262
5578
|
try {
|
|
4263
|
-
const memoryService = getDefaultMemoryService();
|
|
4264
5579
|
await memoryService.initialize();
|
|
4265
5580
|
let events = await memoryService.getRecentEvents(limit + offset + 1e3);
|
|
4266
5581
|
if (sessionId) {
|
|
@@ -4287,12 +5602,14 @@ eventsRouter.get("/", async (c) => {
|
|
|
4287
5602
|
});
|
|
4288
5603
|
} catch (error) {
|
|
4289
5604
|
return c.json({ error: error.message }, 500);
|
|
5605
|
+
} finally {
|
|
5606
|
+
await memoryService.shutdown();
|
|
4290
5607
|
}
|
|
4291
5608
|
});
|
|
4292
5609
|
eventsRouter.get("/:id", async (c) => {
|
|
4293
5610
|
const { id } = c.req.param();
|
|
5611
|
+
const memoryService = getReadOnlyMemoryService();
|
|
4294
5612
|
try {
|
|
4295
|
-
const memoryService = getDefaultMemoryService();
|
|
4296
5613
|
await memoryService.initialize();
|
|
4297
5614
|
const recentEvents = await memoryService.getRecentEvents(1e4);
|
|
4298
5615
|
const event = recentEvents.find((e) => e.id === id);
|
|
@@ -4322,6 +5639,8 @@ eventsRouter.get("/:id", async (c) => {
|
|
|
4322
5639
|
});
|
|
4323
5640
|
} catch (error) {
|
|
4324
5641
|
return c.json({ error: error.message }, 500);
|
|
5642
|
+
} finally {
|
|
5643
|
+
await memoryService.shutdown();
|
|
4325
5644
|
}
|
|
4326
5645
|
});
|
|
4327
5646
|
|
|
@@ -4329,12 +5648,12 @@ eventsRouter.get("/:id", async (c) => {
|
|
|
4329
5648
|
import { Hono as Hono3 } from "hono";
|
|
4330
5649
|
var searchRouter = new Hono3();
|
|
4331
5650
|
searchRouter.post("/", async (c) => {
|
|
5651
|
+
const memoryService = getReadOnlyMemoryService();
|
|
4332
5652
|
try {
|
|
4333
5653
|
const body = await c.req.json();
|
|
4334
5654
|
if (!body.query) {
|
|
4335
5655
|
return c.json({ error: "Query is required" }, 400);
|
|
4336
5656
|
}
|
|
4337
|
-
const memoryService = getDefaultMemoryService();
|
|
4338
5657
|
await memoryService.initialize();
|
|
4339
5658
|
const startTime = Date.now();
|
|
4340
5659
|
const result = await memoryService.retrieveMemories(body.query, {
|
|
@@ -4363,6 +5682,8 @@ searchRouter.post("/", async (c) => {
|
|
|
4363
5682
|
});
|
|
4364
5683
|
} catch (error) {
|
|
4365
5684
|
return c.json({ error: error.message }, 500);
|
|
5685
|
+
} finally {
|
|
5686
|
+
await memoryService.shutdown();
|
|
4366
5687
|
}
|
|
4367
5688
|
});
|
|
4368
5689
|
searchRouter.get("/", async (c) => {
|
|
@@ -4371,8 +5692,8 @@ searchRouter.get("/", async (c) => {
|
|
|
4371
5692
|
return c.json({ error: 'Query parameter "q" is required' }, 400);
|
|
4372
5693
|
}
|
|
4373
5694
|
const topK = parseInt(c.req.query("topK") || "5", 10);
|
|
5695
|
+
const memoryService = getReadOnlyMemoryService();
|
|
4374
5696
|
try {
|
|
4375
|
-
const memoryService = getDefaultMemoryService();
|
|
4376
5697
|
await memoryService.initialize();
|
|
4377
5698
|
const result = await memoryService.retrieveMemories(query, { topK });
|
|
4378
5699
|
return c.json({
|
|
@@ -4390,6 +5711,8 @@ searchRouter.get("/", async (c) => {
|
|
|
4390
5711
|
});
|
|
4391
5712
|
} catch (error) {
|
|
4392
5713
|
return c.json({ error: error.message }, 500);
|
|
5714
|
+
} finally {
|
|
5715
|
+
await memoryService.shutdown();
|
|
4393
5716
|
}
|
|
4394
5717
|
});
|
|
4395
5718
|
|
|
@@ -4397,8 +5720,8 @@ searchRouter.get("/", async (c) => {
|
|
|
4397
5720
|
import { Hono as Hono4 } from "hono";
|
|
4398
5721
|
var statsRouter = new Hono4();
|
|
4399
5722
|
statsRouter.get("/shared", async (c) => {
|
|
5723
|
+
const memoryService = getReadOnlyMemoryService();
|
|
4400
5724
|
try {
|
|
4401
|
-
const memoryService = getDefaultMemoryService();
|
|
4402
5725
|
await memoryService.initialize();
|
|
4403
5726
|
const sharedStats = await memoryService.getSharedStoreStats();
|
|
4404
5727
|
return c.json({
|
|
@@ -4416,12 +5739,14 @@ statsRouter.get("/shared", async (c) => {
|
|
|
4416
5739
|
totalUsageCount: 0,
|
|
4417
5740
|
lastUpdated: null
|
|
4418
5741
|
});
|
|
5742
|
+
} finally {
|
|
5743
|
+
await memoryService.shutdown();
|
|
4419
5744
|
}
|
|
4420
5745
|
});
|
|
4421
5746
|
statsRouter.get("/endless", async (c) => {
|
|
5747
|
+
const projectPath = c.req.query("project") || process.cwd();
|
|
5748
|
+
const memoryService = getMemoryServiceForProject(projectPath);
|
|
4422
5749
|
try {
|
|
4423
|
-
const projectPath = c.req.query("project") || process.cwd();
|
|
4424
|
-
const memoryService = getMemoryServiceForProject(projectPath);
|
|
4425
5750
|
await memoryService.initialize();
|
|
4426
5751
|
const status = await memoryService.getEndlessModeStatus();
|
|
4427
5752
|
return c.json({
|
|
@@ -4439,11 +5764,68 @@ statsRouter.get("/endless", async (c) => {
|
|
|
4439
5764
|
consolidatedCount: 0,
|
|
4440
5765
|
lastConsolidation: null
|
|
4441
5766
|
});
|
|
5767
|
+
} finally {
|
|
5768
|
+
await memoryService.shutdown();
|
|
5769
|
+
}
|
|
5770
|
+
});
|
|
5771
|
+
statsRouter.get("/levels/:level", async (c) => {
|
|
5772
|
+
const { level } = c.req.param();
|
|
5773
|
+
const limit = parseInt(c.req.query("limit") || "20", 10);
|
|
5774
|
+
const offset = parseInt(c.req.query("offset") || "0", 10);
|
|
5775
|
+
const sort = c.req.query("sort") || "recent";
|
|
5776
|
+
const validLevels = ["L0", "L1", "L2", "L3", "L4"];
|
|
5777
|
+
if (!validLevels.includes(level)) {
|
|
5778
|
+
return c.json({ error: `Invalid level. Must be one of: ${validLevels.join(", ")}` }, 400);
|
|
5779
|
+
}
|
|
5780
|
+
const memoryService = getReadOnlyMemoryService();
|
|
5781
|
+
try {
|
|
5782
|
+
await memoryService.initialize();
|
|
5783
|
+
let events = await memoryService.getEventsByLevel(level, { limit: limit * 2, offset });
|
|
5784
|
+
const stats = await memoryService.getStats();
|
|
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);
|
|
5804
|
+
return c.json({
|
|
5805
|
+
level,
|
|
5806
|
+
events: events.map((e) => ({
|
|
5807
|
+
id: e.id,
|
|
5808
|
+
eventType: e.eventType,
|
|
5809
|
+
sessionId: e.sessionId,
|
|
5810
|
+
timestamp: e.timestamp.toISOString(),
|
|
5811
|
+
content: e.content.slice(0, 500) + (e.content.length > 500 ? "..." : ""),
|
|
5812
|
+
metadata: e.metadata,
|
|
5813
|
+
accessCount: e.accessCount || 0
|
|
5814
|
+
})),
|
|
5815
|
+
total: levelStat?.count || 0,
|
|
5816
|
+
limit,
|
|
5817
|
+
offset,
|
|
5818
|
+
hasMore: events.length === limit
|
|
5819
|
+
});
|
|
5820
|
+
} catch (error) {
|
|
5821
|
+
return c.json({ error: error.message }, 500);
|
|
5822
|
+
} finally {
|
|
5823
|
+
await memoryService.shutdown();
|
|
4442
5824
|
}
|
|
4443
5825
|
});
|
|
4444
5826
|
statsRouter.get("/", async (c) => {
|
|
5827
|
+
const memoryService = getReadOnlyMemoryService();
|
|
4445
5828
|
try {
|
|
4446
|
-
const memoryService = getDefaultMemoryService();
|
|
4447
5829
|
await memoryService.initialize();
|
|
4448
5830
|
const stats = await memoryService.getStats();
|
|
4449
5831
|
const recentEvents = await memoryService.getRecentEvents(1e4);
|
|
@@ -4480,12 +5862,45 @@ statsRouter.get("/", async (c) => {
|
|
|
4480
5862
|
});
|
|
4481
5863
|
} catch (error) {
|
|
4482
5864
|
return c.json({ error: error.message }, 500);
|
|
5865
|
+
} finally {
|
|
5866
|
+
await memoryService.shutdown();
|
|
5867
|
+
}
|
|
5868
|
+
});
|
|
5869
|
+
statsRouter.get("/most-accessed", async (c) => {
|
|
5870
|
+
const limit = parseInt(c.req.query("limit") || "10", 10);
|
|
5871
|
+
const memoryService = getReadOnlyMemoryService();
|
|
5872
|
+
try {
|
|
5873
|
+
await memoryService.initialize();
|
|
5874
|
+
console.log("[most-accessed] Fetching most accessed memories, limit:", limit);
|
|
5875
|
+
const memories = await memoryService.getMostAccessedMemories(limit);
|
|
5876
|
+
console.log("[most-accessed] Got memories:", memories.length);
|
|
5877
|
+
return c.json({
|
|
5878
|
+
memories: memories.map((m) => ({
|
|
5879
|
+
memoryId: m.memoryId,
|
|
5880
|
+
summary: m.summary,
|
|
5881
|
+
topics: m.topics,
|
|
5882
|
+
accessCount: m.accessCount,
|
|
5883
|
+
lastAccessed: m.lastAccessed || null,
|
|
5884
|
+
confidence: m.confidence,
|
|
5885
|
+
createdAt: m.createdAt instanceof Date ? m.createdAt.toISOString() : m.createdAt
|
|
5886
|
+
})),
|
|
5887
|
+
total: memories.length
|
|
5888
|
+
});
|
|
5889
|
+
} catch (error) {
|
|
5890
|
+
console.error("[most-accessed] Error:", error);
|
|
5891
|
+
return c.json({
|
|
5892
|
+
memories: [],
|
|
5893
|
+
total: 0,
|
|
5894
|
+
error: error.message
|
|
5895
|
+
});
|
|
5896
|
+
} finally {
|
|
5897
|
+
await memoryService.shutdown();
|
|
4483
5898
|
}
|
|
4484
5899
|
});
|
|
4485
5900
|
statsRouter.get("/timeline", async (c) => {
|
|
4486
5901
|
const days = parseInt(c.req.query("days") || "7", 10);
|
|
5902
|
+
const memoryService = getReadOnlyMemoryService();
|
|
4487
5903
|
try {
|
|
4488
|
-
const memoryService = getDefaultMemoryService();
|
|
4489
5904
|
await memoryService.initialize();
|
|
4490
5905
|
const recentEvents = await memoryService.getRecentEvents(1e4);
|
|
4491
5906
|
const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1e3);
|
|
@@ -4510,8 +5925,46 @@ statsRouter.get("/timeline", async (c) => {
|
|
|
4510
5925
|
});
|
|
4511
5926
|
} catch (error) {
|
|
4512
5927
|
return c.json({ error: error.message }, 500);
|
|
5928
|
+
} finally {
|
|
5929
|
+
await memoryService.shutdown();
|
|
5930
|
+
}
|
|
5931
|
+
});
|
|
5932
|
+
statsRouter.post("/graduation/run", async (c) => {
|
|
5933
|
+
const memoryService = getReadOnlyMemoryService();
|
|
5934
|
+
try {
|
|
5935
|
+
await memoryService.initialize();
|
|
5936
|
+
const result = await memoryService.forceGraduation();
|
|
5937
|
+
return c.json({
|
|
5938
|
+
success: true,
|
|
5939
|
+
evaluated: result.evaluated,
|
|
5940
|
+
graduated: result.graduated,
|
|
5941
|
+
byLevel: result.byLevel
|
|
5942
|
+
});
|
|
5943
|
+
} catch (error) {
|
|
5944
|
+
return c.json({
|
|
5945
|
+
success: false,
|
|
5946
|
+
error: error.message
|
|
5947
|
+
}, 500);
|
|
5948
|
+
} finally {
|
|
5949
|
+
await memoryService.shutdown();
|
|
4513
5950
|
}
|
|
4514
5951
|
});
|
|
5952
|
+
statsRouter.get("/graduation", async (c) => {
|
|
5953
|
+
return c.json({
|
|
5954
|
+
criteria: {
|
|
5955
|
+
L0toL1: { minAccessCount: 1, minConfidence: 0.5, minCrossSessionRefs: 0, maxAgeDays: 30 },
|
|
5956
|
+
L1toL2: { minAccessCount: 3, minConfidence: 0.7, minCrossSessionRefs: 1, maxAgeDays: 60 },
|
|
5957
|
+
L2toL3: { minAccessCount: 5, minConfidence: 0.85, minCrossSessionRefs: 2, maxAgeDays: 90 },
|
|
5958
|
+
L3toL4: { minAccessCount: 10, minConfidence: 0.92, minCrossSessionRefs: 3, maxAgeDays: 180 }
|
|
5959
|
+
},
|
|
5960
|
+
description: {
|
|
5961
|
+
accessCount: "Number of times the memory was retrieved/referenced",
|
|
5962
|
+
confidence: "Match confidence score when retrieved (0.0-1.0)",
|
|
5963
|
+
crossSessionRefs: "Number of different sessions that referenced this memory",
|
|
5964
|
+
maxAgeDays: "Maximum days since last access (prevents stale promotion)"
|
|
5965
|
+
}
|
|
5966
|
+
});
|
|
5967
|
+
});
|
|
4515
5968
|
|
|
4516
5969
|
// src/server/api/citations.ts
|
|
4517
5970
|
import { Hono as Hono5 } from "hono";
|
|
@@ -4538,8 +5991,8 @@ var citationsRouter = new Hono5();
|
|
|
4538
5991
|
citationsRouter.get("/:id", async (c) => {
|
|
4539
5992
|
const { id } = c.req.param();
|
|
4540
5993
|
const citationId = parseCitationId(id) || id;
|
|
5994
|
+
const memoryService = getReadOnlyMemoryService();
|
|
4541
5995
|
try {
|
|
4542
|
-
const memoryService = getDefaultMemoryService();
|
|
4543
5996
|
await memoryService.initialize();
|
|
4544
5997
|
const recentEvents = await memoryService.getRecentEvents(1e4);
|
|
4545
5998
|
const event = recentEvents.find((e) => {
|
|
@@ -4565,13 +6018,15 @@ citationsRouter.get("/:id", async (c) => {
|
|
|
4565
6018
|
});
|
|
4566
6019
|
} catch (error) {
|
|
4567
6020
|
return c.json({ error: error.message }, 500);
|
|
6021
|
+
} finally {
|
|
6022
|
+
await memoryService.shutdown();
|
|
4568
6023
|
}
|
|
4569
6024
|
});
|
|
4570
6025
|
citationsRouter.get("/:id/related", async (c) => {
|
|
4571
6026
|
const { id } = c.req.param();
|
|
4572
6027
|
const citationId = parseCitationId(id) || id;
|
|
6028
|
+
const memoryService = getReadOnlyMemoryService();
|
|
4573
6029
|
try {
|
|
4574
|
-
const memoryService = getDefaultMemoryService();
|
|
4575
6030
|
await memoryService.initialize();
|
|
4576
6031
|
const recentEvents = await memoryService.getRecentEvents(1e4);
|
|
4577
6032
|
const event = recentEvents.find((e) => {
|
|
@@ -4601,6 +6056,8 @@ citationsRouter.get("/:id/related", async (c) => {
|
|
|
4601
6056
|
});
|
|
4602
6057
|
} catch (error) {
|
|
4603
6058
|
return c.json({ error: error.message }, 500);
|
|
6059
|
+
} finally {
|
|
6060
|
+
await memoryService.shutdown();
|
|
4604
6061
|
}
|
|
4605
6062
|
});
|
|
4606
6063
|
|
|
@@ -4613,7 +6070,7 @@ app.use("/*", cors());
|
|
|
4613
6070
|
app.use("/*", logger());
|
|
4614
6071
|
app.route("/api", apiRouter);
|
|
4615
6072
|
app.get("/health", (c) => c.json({ status: "ok", timestamp: (/* @__PURE__ */ new Date()).toISOString() }));
|
|
4616
|
-
var uiPath = path3.join(
|
|
6073
|
+
var uiPath = path3.join(__dirname, "../../dist/ui");
|
|
4617
6074
|
if (fs3.existsSync(uiPath)) {
|
|
4618
6075
|
app.use("/*", serveStatic({ root: uiPath }));
|
|
4619
6076
|
}
|
|
@@ -4629,17 +6086,17 @@ function startServer(port = 37777) {
|
|
|
4629
6086
|
if (serverInstance) {
|
|
4630
6087
|
return serverInstance;
|
|
4631
6088
|
}
|
|
4632
|
-
serverInstance =
|
|
4633
|
-
|
|
6089
|
+
serverInstance = serve({
|
|
6090
|
+
fetch: app.fetch,
|
|
4634
6091
|
port,
|
|
4635
|
-
|
|
6092
|
+
hostname: "127.0.0.1"
|
|
4636
6093
|
});
|
|
4637
6094
|
console.log(`\u{1F9E0} Code Memory viewer started at http://localhost:${port}`);
|
|
4638
6095
|
return serverInstance;
|
|
4639
6096
|
}
|
|
4640
6097
|
function stopServer() {
|
|
4641
6098
|
if (serverInstance) {
|
|
4642
|
-
serverInstance.
|
|
6099
|
+
serverInstance.close();
|
|
4643
6100
|
serverInstance = null;
|
|
4644
6101
|
}
|
|
4645
6102
|
}
|
|
@@ -4651,14 +6108,166 @@ async function isServerRunning(port = 37777) {
|
|
|
4651
6108
|
return false;
|
|
4652
6109
|
}
|
|
4653
6110
|
}
|
|
4654
|
-
|
|
6111
|
+
var isMainModule = process.argv[1]?.includes("server/index") || process.argv[1]?.endsWith("server.js");
|
|
6112
|
+
if (isMainModule) {
|
|
4655
6113
|
const port = parseInt(process.env.PORT || "37777", 10);
|
|
4656
6114
|
startServer(port);
|
|
4657
6115
|
}
|
|
4658
6116
|
|
|
4659
6117
|
// src/cli/index.ts
|
|
6118
|
+
var CLAUDE_SETTINGS_PATH = path4.join(os3.homedir(), ".claude", "settings.json");
|
|
6119
|
+
function getPluginPath() {
|
|
6120
|
+
const possiblePaths = [
|
|
6121
|
+
path4.join(__dirname, ".."),
|
|
6122
|
+
// When running from dist/cli
|
|
6123
|
+
path4.join(__dirname, "../..", "dist"),
|
|
6124
|
+
// When running from src
|
|
6125
|
+
path4.join(process.cwd(), "dist")
|
|
6126
|
+
// Current working directory
|
|
6127
|
+
];
|
|
6128
|
+
for (const p of possiblePaths) {
|
|
6129
|
+
const hooksPath = path4.join(p, "hooks", "user-prompt-submit.js");
|
|
6130
|
+
if (fs4.existsSync(hooksPath)) {
|
|
6131
|
+
return p;
|
|
6132
|
+
}
|
|
6133
|
+
}
|
|
6134
|
+
return path4.join(os3.homedir(), ".npm-global", "lib", "node_modules", "claude-memory-layer", "dist");
|
|
6135
|
+
}
|
|
6136
|
+
function loadClaudeSettings() {
|
|
6137
|
+
try {
|
|
6138
|
+
if (fs4.existsSync(CLAUDE_SETTINGS_PATH)) {
|
|
6139
|
+
const content = fs4.readFileSync(CLAUDE_SETTINGS_PATH, "utf-8");
|
|
6140
|
+
return JSON.parse(content);
|
|
6141
|
+
}
|
|
6142
|
+
} catch (error) {
|
|
6143
|
+
console.error("Warning: Could not read existing settings:", error);
|
|
6144
|
+
}
|
|
6145
|
+
return {};
|
|
6146
|
+
}
|
|
6147
|
+
function saveClaudeSettings(settings) {
|
|
6148
|
+
const dir = path4.dirname(CLAUDE_SETTINGS_PATH);
|
|
6149
|
+
if (!fs4.existsSync(dir)) {
|
|
6150
|
+
fs4.mkdirSync(dir, { recursive: true });
|
|
6151
|
+
}
|
|
6152
|
+
const tempPath = CLAUDE_SETTINGS_PATH + ".tmp";
|
|
6153
|
+
fs4.writeFileSync(tempPath, JSON.stringify(settings, null, 2));
|
|
6154
|
+
fs4.renameSync(tempPath, CLAUDE_SETTINGS_PATH);
|
|
6155
|
+
}
|
|
6156
|
+
function getHooksConfig(pluginPath) {
|
|
6157
|
+
return {
|
|
6158
|
+
UserPromptSubmit: [
|
|
6159
|
+
{
|
|
6160
|
+
matcher: "",
|
|
6161
|
+
hooks: [
|
|
6162
|
+
{
|
|
6163
|
+
type: "command",
|
|
6164
|
+
command: `node ${path4.join(pluginPath, "hooks", "user-prompt-submit.js")}`
|
|
6165
|
+
}
|
|
6166
|
+
]
|
|
6167
|
+
}
|
|
6168
|
+
],
|
|
6169
|
+
PostToolUse: [
|
|
6170
|
+
{
|
|
6171
|
+
matcher: "",
|
|
6172
|
+
hooks: [
|
|
6173
|
+
{
|
|
6174
|
+
type: "command",
|
|
6175
|
+
command: `node ${path4.join(pluginPath, "hooks", "post-tool-use.js")}`
|
|
6176
|
+
}
|
|
6177
|
+
]
|
|
6178
|
+
}
|
|
6179
|
+
]
|
|
6180
|
+
};
|
|
6181
|
+
}
|
|
4660
6182
|
var program = new Command();
|
|
4661
6183
|
program.name("claude-memory-layer").description("Claude Code Memory Plugin CLI").version("1.0.0");
|
|
6184
|
+
program.command("install").description("Install hooks into Claude Code settings").option("--path <path>", "Custom plugin path (defaults to auto-detect)").action(async (options) => {
|
|
6185
|
+
try {
|
|
6186
|
+
const pluginPath = options.path || getPluginPath();
|
|
6187
|
+
const userPromptHook = path4.join(pluginPath, "hooks", "user-prompt-submit.js");
|
|
6188
|
+
if (!fs4.existsSync(userPromptHook)) {
|
|
6189
|
+
console.error(`
|
|
6190
|
+
\u274C Hook files not found at: ${pluginPath}`);
|
|
6191
|
+
console.error(' Make sure you have built the plugin with "npm run build"');
|
|
6192
|
+
process.exit(1);
|
|
6193
|
+
}
|
|
6194
|
+
const settings = loadClaudeSettings();
|
|
6195
|
+
const newHooks = getHooksConfig(pluginPath);
|
|
6196
|
+
settings.hooks = {
|
|
6197
|
+
...settings.hooks,
|
|
6198
|
+
...newHooks
|
|
6199
|
+
};
|
|
6200
|
+
saveClaudeSettings(settings);
|
|
6201
|
+
console.log("\n\u2705 Claude Memory Layer installed!\n");
|
|
6202
|
+
console.log("Hooks registered:");
|
|
6203
|
+
console.log(" - UserPromptSubmit: Memory retrieval on user input");
|
|
6204
|
+
console.log(" - PostToolUse: Store tool observations\n");
|
|
6205
|
+
console.log("Plugin path:", pluginPath);
|
|
6206
|
+
console.log("\n\u26A0\uFE0F Restart Claude Code for changes to take effect.\n");
|
|
6207
|
+
console.log("Commands:");
|
|
6208
|
+
console.log(" claude-memory-layer dashboard - Open web dashboard");
|
|
6209
|
+
console.log(" claude-memory-layer search - Search memories");
|
|
6210
|
+
console.log(" claude-memory-layer stats - View statistics");
|
|
6211
|
+
console.log(" claude-memory-layer uninstall - Remove hooks\n");
|
|
6212
|
+
} catch (error) {
|
|
6213
|
+
console.error("Install failed:", error);
|
|
6214
|
+
process.exit(1);
|
|
6215
|
+
}
|
|
6216
|
+
});
|
|
6217
|
+
program.command("uninstall").description("Remove hooks from Claude Code settings").action(async () => {
|
|
6218
|
+
try {
|
|
6219
|
+
const settings = loadClaudeSettings();
|
|
6220
|
+
if (!settings.hooks) {
|
|
6221
|
+
console.log("\n\u{1F4CB} No hooks installed.\n");
|
|
6222
|
+
return;
|
|
6223
|
+
}
|
|
6224
|
+
delete settings.hooks.UserPromptSubmit;
|
|
6225
|
+
delete settings.hooks.PostToolUse;
|
|
6226
|
+
if (Object.keys(settings.hooks).length === 0) {
|
|
6227
|
+
delete settings.hooks;
|
|
6228
|
+
}
|
|
6229
|
+
saveClaudeSettings(settings);
|
|
6230
|
+
console.log("\n\u2705 Claude Memory Layer uninstalled!\n");
|
|
6231
|
+
console.log("Hooks removed from Claude Code settings.");
|
|
6232
|
+
console.log("Your memory data is preserved and can be accessed with:");
|
|
6233
|
+
console.log(" claude-memory-layer dashboard\n");
|
|
6234
|
+
console.log("\u26A0\uFE0F Restart Claude Code for changes to take effect.\n");
|
|
6235
|
+
} catch (error) {
|
|
6236
|
+
console.error("Uninstall failed:", error);
|
|
6237
|
+
process.exit(1);
|
|
6238
|
+
}
|
|
6239
|
+
});
|
|
6240
|
+
program.command("status").description("Check plugin installation status").action(async () => {
|
|
6241
|
+
try {
|
|
6242
|
+
const settings = loadClaudeSettings();
|
|
6243
|
+
const pluginPath = getPluginPath();
|
|
6244
|
+
console.log("\n\u{1F9E0} Claude Memory Layer Status\n");
|
|
6245
|
+
const hasUserPromptHook = settings.hooks?.UserPromptSubmit?.some(
|
|
6246
|
+
(h) => h.hooks?.some((hook) => hook.command?.includes("user-prompt-submit"))
|
|
6247
|
+
);
|
|
6248
|
+
const hasPostToolHook = settings.hooks?.PostToolUse?.some(
|
|
6249
|
+
(h) => h.hooks?.some((hook) => hook.command?.includes("post-tool-use"))
|
|
6250
|
+
);
|
|
6251
|
+
console.log("Hooks:");
|
|
6252
|
+
console.log(` UserPromptSubmit: ${hasUserPromptHook ? "\u2705 Installed" : "\u274C Not installed"}`);
|
|
6253
|
+
console.log(` PostToolUse: ${hasPostToolHook ? "\u2705 Installed" : "\u274C Not installed"}`);
|
|
6254
|
+
const hooksExist = fs4.existsSync(path4.join(pluginPath, "hooks", "user-prompt-submit.js"));
|
|
6255
|
+
console.log(`
|
|
6256
|
+
Plugin files: ${hooksExist ? "\u2705 Found" : "\u274C Not found"}`);
|
|
6257
|
+
console.log(` Path: ${pluginPath}`);
|
|
6258
|
+
const dashboardRunning = await isServerRunning(37777);
|
|
6259
|
+
console.log(`
|
|
6260
|
+
Dashboard: ${dashboardRunning ? "\u2705 Running at http://localhost:37777" : "\u23F9\uFE0F Not running"}`);
|
|
6261
|
+
if (!hasUserPromptHook || !hasPostToolHook) {
|
|
6262
|
+
console.log('\n\u{1F4A1} Run "claude-memory-layer install" to set up hooks.\n');
|
|
6263
|
+
} else {
|
|
6264
|
+
console.log("\n\u2705 Plugin is fully installed and configured.\n");
|
|
6265
|
+
}
|
|
6266
|
+
} catch (error) {
|
|
6267
|
+
console.error("Status check failed:", error);
|
|
6268
|
+
process.exit(1);
|
|
6269
|
+
}
|
|
6270
|
+
});
|
|
4662
6271
|
program.command("search <query>").description("Search memories using semantic search").option("-k, --top-k <number>", "Number of results", "5").option("-s, --min-score <number>", "Minimum similarity score", "0.7").option("--session <id>", "Filter by session ID").option("-p, --project <path>", "Project path (defaults to cwd)").action(async (query, options) => {
|
|
4663
6272
|
const projectPath = options.project || process.cwd();
|
|
4664
6273
|
const service = getMemoryServiceForProject(projectPath);
|