@jungjaehoon/mama-core 1.3.2 → 1.4.0

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.
Files changed (50) hide show
  1. package/README.md +67 -19
  2. package/db/migrations/001-initial-decision-graph.sql +5 -0
  3. package/db/migrations/014-add-is-static-column.sql +5 -0
  4. package/db/migrations/015-add-fts5-search.sql +34 -0
  5. package/db/migrations/016-add-memory-v2-columns.sql +14 -0
  6. package/db/migrations/017-create-memory-scopes.sql +10 -0
  7. package/db/migrations/018-create-memory-scope-bindings.sql +15 -0
  8. package/db/migrations/019-create-memory-events.sql +18 -0
  9. package/db/migrations/020-create-audit-findings.sql +18 -0
  10. package/db/migrations/021-create-memory-truth.sql +20 -0
  11. package/db/migrations/022-create-channel-summaries.sql +9 -0
  12. package/db/migrations/023-create-channel-summary-state.sql +9 -0
  13. package/db/migrations/024-add-event-date.sql +9 -0
  14. package/dist/db-adapter/base-adapter.d.ts +1 -1
  15. package/dist/db-adapter/index.js +2 -8
  16. package/dist/db-adapter/node-sqlite-adapter.d.ts +6 -1
  17. package/dist/db-adapter/node-sqlite-adapter.js +99 -45
  18. package/dist/db-manager.d.ts +30 -2
  19. package/dist/db-manager.js +131 -6
  20. package/dist/decision-formatter.d.ts +2 -0
  21. package/dist/embedding-server/mobile/session-manager.js +22 -7
  22. package/dist/index.d.ts +5 -1
  23. package/dist/index.js +31 -3
  24. package/dist/mama-api.d.ts +67 -2
  25. package/dist/mama-api.js +295 -80
  26. package/dist/memory/api.d.ts +81 -0
  27. package/dist/memory/api.js +972 -0
  28. package/dist/memory/bootstrap-builder.d.ts +10 -0
  29. package/dist/memory/bootstrap-builder.js +66 -0
  30. package/dist/memory/channel-summary-state-store.d.ts +19 -0
  31. package/dist/memory/channel-summary-state-store.js +223 -0
  32. package/dist/memory/channel-summary-store.d.ts +8 -0
  33. package/dist/memory/channel-summary-store.js +41 -0
  34. package/dist/memory/event-store.d.ts +5 -0
  35. package/dist/memory/event-store.js +72 -0
  36. package/dist/memory/evolution-engine.d.ts +22 -0
  37. package/dist/memory/evolution-engine.js +133 -0
  38. package/dist/memory/extraction-prompt.d.ts +4 -0
  39. package/dist/memory/extraction-prompt.js +89 -0
  40. package/dist/memory/finding-store.d.ts +5 -0
  41. package/dist/memory/finding-store.js +75 -0
  42. package/dist/memory/profile-builder.d.ts +3 -0
  43. package/dist/memory/profile-builder.js +23 -0
  44. package/dist/memory/scope-store.d.ts +5 -0
  45. package/dist/memory/scope-store.js +13 -0
  46. package/dist/memory/truth-store.d.ts +11 -0
  47. package/dist/memory/truth-store.js +102 -0
  48. package/dist/memory/types.d.ts +267 -0
  49. package/dist/memory/types.js +74 -0
  50. package/package.json +4 -1
package/README.md CHANGED
@@ -74,12 +74,40 @@ const mamaApi = require('@jungjaehoon/mama-core/mama-api');
74
74
  - CRUD operations for decisions
75
75
  - Vector similarity search
76
76
 
77
- ### Core API
77
+ ### Memory API
78
78
 
79
- - **mama-api** - High-level API interface
79
+ - **memory/api** - Scoped memory operations
80
+ - `saveMemory(input)` - Save typed memory with scopes and optional event_date (preference, fact, decision, lesson, constraint)
81
+ - `recallMemory(query, options)` - Truth-aware recall with scope filtering
82
+ - `buildProfile(scopes)` - Build memory profile (static/dynamic/evidence)
83
+ - `ingestMemory(input)` - Ingest raw content as memory
84
+ - `ingestConversation(input)` - Decompose conversations into typed memory units via optional LLM extraction
85
+ - `evolveMemory(input)` - Resolve graph edges between memories
86
+ - `buildMemoryBootstrap(params)` - Build memory agent bootstrap context
87
+ - `createAuditAck(input)` - Create audit acknowledgment
88
+ - `recordMemoryAudit(input)` - Record channel audit with state management
89
+
90
+ - **memory/truth-store** - Truth projection layer
91
+ - `projectMemoryTruth(row)` - Write truth projection
92
+ - `queryRelevantTruth(params)` - Query current truth with scope/query filtering
93
+ - `queryTruthByTopic(topic)` - Get truth rows for a topic
94
+
95
+ - **memory/evolution-engine** - Graph edge resolution
96
+ - `resolveMemoryEvolution(input)` - Determine supersedes/builds_on edges
97
+
98
+ - **memory/extraction-prompt** - LLM extraction for conversation ingestion
99
+ - `buildExtractionPrompt(messages)` - Build structured prompt for memory extraction
100
+ - `parseExtractionResponse(response)` - Parse LLM JSON response into typed units
101
+
102
+ - **memory/channel-summary-state-store** - Channel state management
103
+ - `recordChannelAudit(input)` - Accumulate audit outcomes into channel state
104
+
105
+ ### Core API (Legacy)
106
+
107
+ - **mama-api** - High-level API interface (wraps memory API)
80
108
  - `save(decision)` - Save decision
81
109
  - `recall(topic)` - Retrieve decision history
82
- - `suggest(query)` - Semantic search
110
+ - `suggest(query)` - Semantic search with hybrid FTS5 + vector + recency
83
111
  - `updateOutcome(id, outcome)` - Update decision outcome
84
112
 
85
113
  - **decision-tracker** - Decision graph management
@@ -125,12 +153,15 @@ pnpm test:watch
125
153
 
126
154
  ## Test Coverage
127
155
 
128
- - 35 unit tests
156
+ - 72 unit tests across 17 test files
129
157
  - 100% passing
130
158
  - Tests cover:
131
- - Config loader
132
- - Database initialization
133
- - Module exports
159
+ - Config loader, database initialization, module exports
160
+ - Memory API (save, recall, profile)
161
+ - Truth store, evolution engine, scope schema
162
+ - Channel summary, channel summary state
163
+ - Event store, finding store, bootstrap builder
164
+ - Legacy shim compatibility
134
165
 
135
166
  ## Architecture
136
167
 
@@ -139,23 +170,40 @@ MAMA Core uses CommonJS modules and is designed to be shared across multiple pac
139
170
  ```
140
171
  packages/mama-core/
141
172
  ├── src/
142
- │ ├── index.js # Main exports
143
- │ ├── embeddings.js # Embedding generation
144
- │ ├── db-manager.js # Database management
145
- │ ├── mama-api.js # High-level API
146
- │ └── db-adapter/ # Database adapter
147
- ├── db/migrations/ # SQLite migrations
148
- └── tests/ # Unit tests
173
+ │ ├── index.ts # Main exports
174
+ │ ├── embeddings.ts # Embedding generation
175
+ │ ├── db-manager.ts # Database management
176
+ │ ├── mama-api.ts # High-level API (wraps memory API)
177
+ │ ├── db-adapter/ # Database adapter (SQLite)
178
+ │ └── memory/ # Memory infrastructure
179
+ │ ├── types.ts # MemoryRecord, MemoryScopeRef, RecallBundle, etc.
180
+ │ ├── api.ts # saveMemory, recallMemory, buildProfile, etc.
181
+ │ ├── truth-store.ts # Truth projection layer
182
+ │ ├── extraction-prompt.ts # LLM extraction prompt + parser
183
+ │ ├── evolution-engine.ts # Graph edge resolution
184
+ │ ├── scope-store.ts # Scope management
185
+ │ ├── event-store.ts # Audit event persistence
186
+ │ ├── finding-store.ts # Audit finding persistence
187
+ │ ├── channel-summary-store.ts # Channel summaries
188
+ │ ├── channel-summary-state-store.ts # Channel state reducer
189
+ │ ├── bootstrap-builder.ts # Memory agent bootstrap
190
+ │ └── profile-builder.ts # Profile classification
191
+ ├── db/migrations/ # SQLite migrations (001-023)
192
+ └── tests/ # 16 test files, 59 tests
149
193
  ```
150
194
 
151
195
  ## Migration Files
152
196
 
153
- Database migrations are included in `db/migrations/`:
197
+ Database migrations are included in `db/migrations/` (001-023):
154
198
 
155
- - 001-initial-decision-graph.sql
156
- - 002-add-error-patterns.sql
157
- - 003-add-validation-fields.sql
158
- - (and more...)
199
+ - 001-013: Core schema (decisions, embeddings, graph edges)
200
+ - 014: Add is_static column
201
+ - 015: FTS5 full-text search index
202
+ - 016: Memory kind/status/summary columns
203
+ - 017-018: Memory scopes and scope bindings
204
+ - 019-020: Memory events and audit findings
205
+ - 021: Memory truth projection
206
+ - 022-023: Channel summaries and state
159
207
 
160
208
  ## License
161
209
 
@@ -100,7 +100,12 @@ CREATE TABLE IF NOT EXISTS sessions (
100
100
  -- Timestamps
101
101
  started_at INTEGER DEFAULT (unixepoch() * 1000),
102
102
  last_active_at INTEGER DEFAULT (unixepoch() * 1000),
103
+ last_active TEXT,
104
+ created_at TEXT DEFAULT (datetime('now')),
103
105
  ended_at INTEGER,
106
+ status TEXT DEFAULT 'active',
107
+ pid INTEGER,
108
+ client_id TEXT,
104
109
 
105
110
  CHECK (action_count >= 0),
106
111
  CHECK (decision_count >= 0)
@@ -0,0 +1,5 @@
1
+ -- Add is_static column for user profile (long-term preferences vs current work)
2
+ ALTER TABLE decisions ADD COLUMN is_static INTEGER DEFAULT 0;
3
+
4
+ INSERT OR IGNORE INTO schema_version (version, description)
5
+ VALUES (14, 'Add is_static column for user profile preferences');
@@ -0,0 +1,34 @@
1
+ -- FTS5 virtual table for keyword search
2
+ CREATE VIRTUAL TABLE IF NOT EXISTS decisions_fts USING fts5(
3
+ topic, decision, reasoning,
4
+ content='decisions',
5
+ content_rowid='rowid'
6
+ );
7
+
8
+ -- Sync triggers for external content table
9
+ CREATE TRIGGER IF NOT EXISTS decisions_ai AFTER INSERT ON decisions BEGIN
10
+ INSERT INTO decisions_fts(rowid, topic, decision, reasoning)
11
+ VALUES (new.rowid, new.topic, new.decision, new.reasoning);
12
+ END;
13
+
14
+ CREATE TRIGGER IF NOT EXISTS decisions_ad BEFORE DELETE ON decisions BEGIN
15
+ INSERT INTO decisions_fts(decisions_fts, rowid, topic, decision, reasoning)
16
+ VALUES('delete', old.rowid, old.topic, old.decision, old.reasoning);
17
+ END;
18
+
19
+ CREATE TRIGGER IF NOT EXISTS decisions_au BEFORE UPDATE ON decisions BEGIN
20
+ INSERT INTO decisions_fts(decisions_fts, rowid, topic, decision, reasoning)
21
+ VALUES('delete', old.rowid, old.topic, old.decision, old.reasoning);
22
+ END;
23
+
24
+ CREATE TRIGGER IF NOT EXISTS decisions_au2 AFTER UPDATE ON decisions BEGIN
25
+ INSERT INTO decisions_fts(rowid, topic, decision, reasoning)
26
+ VALUES (new.rowid, new.topic, new.decision, new.reasoning);
27
+ END;
28
+
29
+ -- Backfill existing decisions into FTS index
30
+ INSERT INTO decisions_fts(rowid, topic, decision, reasoning)
31
+ SELECT rowid, topic, decision, reasoning FROM decisions;
32
+
33
+ INSERT OR IGNORE INTO schema_version (version, description)
34
+ VALUES (15, 'Add FTS5 keyword search with sync triggers');
@@ -0,0 +1,14 @@
1
+ ALTER TABLE decisions ADD COLUMN kind TEXT DEFAULT 'decision'
2
+ CHECK (kind IN ('decision', 'preference', 'constraint', 'lesson', 'fact'));
3
+
4
+ ALTER TABLE decisions ADD COLUMN status TEXT DEFAULT 'active'
5
+ CHECK (status IN ('active', 'superseded', 'contradicted', 'stale'));
6
+
7
+ ALTER TABLE decisions ADD COLUMN summary TEXT;
8
+
9
+ UPDATE decisions
10
+ SET summary = decision
11
+ WHERE summary IS NULL;
12
+
13
+ INSERT OR IGNORE INTO schema_version (version, description)
14
+ VALUES (16, 'Add memory v2 kind/status/summary columns');
@@ -0,0 +1,10 @@
1
+ CREATE TABLE IF NOT EXISTS memory_scopes (
2
+ id TEXT PRIMARY KEY,
3
+ kind TEXT NOT NULL CHECK (kind IN ('global', 'user', 'channel', 'project')),
4
+ external_id TEXT NOT NULL,
5
+ created_at INTEGER DEFAULT (unixepoch() * 1000),
6
+ UNIQUE(kind, external_id)
7
+ );
8
+
9
+ INSERT OR IGNORE INTO schema_version (version, description)
10
+ VALUES (17, 'Create memory scopes table');
@@ -0,0 +1,15 @@
1
+ CREATE TABLE IF NOT EXISTS memory_scope_bindings (
2
+ memory_id TEXT NOT NULL,
3
+ scope_id TEXT NOT NULL,
4
+ is_primary INTEGER DEFAULT 0 CHECK (is_primary IN (0, 1)),
5
+ created_at INTEGER DEFAULT (unixepoch() * 1000),
6
+ PRIMARY KEY (memory_id, scope_id),
7
+ FOREIGN KEY (memory_id) REFERENCES decisions(id) ON DELETE CASCADE,
8
+ FOREIGN KEY (scope_id) REFERENCES memory_scopes(id) ON DELETE CASCADE
9
+ );
10
+
11
+ CREATE INDEX IF NOT EXISTS idx_memory_scope_bindings_scope_id
12
+ ON memory_scope_bindings(scope_id);
13
+
14
+ INSERT OR IGNORE INTO schema_version (version, description)
15
+ VALUES (18, 'Create memory scope bindings table');
@@ -0,0 +1,18 @@
1
+ CREATE TABLE IF NOT EXISTS memory_events (
2
+ event_id TEXT PRIMARY KEY,
3
+ event_type TEXT NOT NULL,
4
+ actor TEXT NOT NULL,
5
+ source_turn_id TEXT,
6
+ memory_id TEXT,
7
+ topic TEXT,
8
+ scope_refs TEXT NOT NULL,
9
+ evidence_refs TEXT,
10
+ reason TEXT,
11
+ created_at INTEGER NOT NULL
12
+ );
13
+
14
+ CREATE INDEX IF NOT EXISTS idx_memory_events_topic_created_at
15
+ ON memory_events(topic, created_at DESC);
16
+
17
+ INSERT OR IGNORE INTO schema_version (version, description)
18
+ VALUES (19, 'Create memory events table');
@@ -0,0 +1,18 @@
1
+ CREATE TABLE IF NOT EXISTS audit_findings (
2
+ finding_id TEXT PRIMARY KEY,
3
+ kind TEXT NOT NULL,
4
+ severity TEXT NOT NULL,
5
+ summary TEXT NOT NULL,
6
+ evidence_refs TEXT NOT NULL,
7
+ affected_memory_ids TEXT NOT NULL,
8
+ recommended_action TEXT NOT NULL,
9
+ status TEXT NOT NULL DEFAULT 'open',
10
+ created_at INTEGER NOT NULL,
11
+ resolved_at INTEGER
12
+ );
13
+
14
+ CREATE INDEX IF NOT EXISTS idx_audit_findings_status_created_at
15
+ ON audit_findings(status, created_at DESC);
16
+
17
+ INSERT OR IGNORE INTO schema_version (version, description)
18
+ VALUES (20, 'Create audit findings table');
@@ -0,0 +1,20 @@
1
+ CREATE TABLE IF NOT EXISTS memory_truth (
2
+ memory_id TEXT PRIMARY KEY,
3
+ topic TEXT NOT NULL,
4
+ truth_status TEXT NOT NULL,
5
+ effective_summary TEXT NOT NULL,
6
+ effective_details TEXT NOT NULL,
7
+ trust_score REAL NOT NULL,
8
+ scope_refs TEXT NOT NULL,
9
+ supporting_event_ids TEXT NOT NULL,
10
+ superseded_by TEXT,
11
+ contradicted_by TEXT,
12
+ created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000),
13
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
14
+ );
15
+
16
+ CREATE INDEX IF NOT EXISTS idx_memory_truth_topic_updated_at
17
+ ON memory_truth(topic, updated_at DESC);
18
+
19
+ INSERT OR IGNORE INTO schema_version (version, description)
20
+ VALUES (21, 'Create memory truth projection table');
@@ -0,0 +1,9 @@
1
+ CREATE TABLE IF NOT EXISTS channel_summaries (
2
+ channel_key TEXT PRIMARY KEY,
3
+ summary_markdown TEXT NOT NULL,
4
+ delta_hash TEXT,
5
+ updated_at INTEGER NOT NULL
6
+ );
7
+
8
+ INSERT OR IGNORE INTO schema_version (version, description)
9
+ VALUES (22, 'Create channel summaries table');
@@ -0,0 +1,9 @@
1
+ CREATE TABLE IF NOT EXISTS channel_summary_state (
2
+ channel_key TEXT PRIMARY KEY,
3
+ state_json TEXT NOT NULL,
4
+ state_hash TEXT NOT NULL,
5
+ updated_at INTEGER NOT NULL
6
+ );
7
+
8
+ INSERT OR IGNORE INTO schema_version (version, description)
9
+ VALUES (23, 'Create channel summary state table');
@@ -0,0 +1,9 @@
1
+ -- Migration 024: Add event_date column to decisions table
2
+ -- event_date stores the ISO 8601 date when the event actually occurred
3
+ -- (distinct from created_at which is ingestion time)
4
+ ALTER TABLE decisions ADD COLUMN event_date TEXT;
5
+
6
+ CREATE INDEX IF NOT EXISTS idx_decisions_event_date ON decisions(event_date);
7
+
8
+ INSERT OR IGNORE INTO schema_version (version, description)
9
+ VALUES (24, 'Add event_date column for temporal tracking');
@@ -47,7 +47,7 @@ export declare abstract class DatabaseAdapter {
47
47
  * @param limit - Max results
48
48
  * @returns Search results with distance
49
49
  */
50
- abstract vectorSearch(embedding: Float32Array | number[], limit: number): VectorSearchResult[] | null;
50
+ abstract vectorSearch(embedding: Float32Array | number[], limit: number, topicPrefix?: string): VectorSearchResult[] | null;
51
51
  /**
52
52
  * Insert vector embedding
53
53
  * @param rowid - Decision rowid
@@ -24,14 +24,8 @@ Object.defineProperty(exports, "DatabaseAdapter", { enumerable: true, get: funct
24
24
  * @returns Configured SQLite adapter instance
25
25
  */
26
26
  function createAdapter(config = {}) {
27
- const configuredDriver = process.env.MAMA_SQLITE_DRIVER;
28
- if (configuredDriver &&
29
- configuredDriver !== 'node-sqlite' &&
30
- configuredDriver !== 'node:sqlite' &&
31
- configuredDriver !== 'auto') {
32
- throw new Error(`Unsupported SQLite driver "${configuredDriver}". MAMA now requires node:sqlite.`);
33
- }
34
- (0, debug_logger_js_1.info)('[db-adapter] Using node:sqlite adapter');
27
+ // SQLiteAdapter (extends NodeSQLiteAdapter) auto-detects: better-sqlite3 (preferred, FTS5) → node:sqlite (fallback)
28
+ (0, debug_logger_js_1.info)('[db-adapter] Creating SQLite adapter (auto-detect driver)');
35
29
  const dbPath = config.dbPath || process.env.MAMA_DB_PATH;
36
30
  return new sqlite_adapter_js_1.SQLiteAdapter({ dbPath });
37
31
  }
@@ -24,6 +24,7 @@ interface NodeSQLiteStatementLike {
24
24
  declare class NodeSQLiteConnection {
25
25
  private db;
26
26
  private connected;
27
+ private isBetterSQLite3;
27
28
  constructor(db: NodeSQLiteDatabaseLike);
28
29
  prepare(sql: string): NodeSQLiteStatementLike;
29
30
  exec(sql: string): void;
@@ -37,16 +38,20 @@ export declare class NodeSQLiteAdapter extends DatabaseAdapter {
37
38
  private config;
38
39
  private db;
39
40
  private _vectorSearchEnabled;
41
+ private vectorCache;
42
+ private topicCache;
40
43
  constructor(config?: SQLiteAdapterConfig);
41
44
  get vectorSearchEnabled(): boolean;
42
45
  getDbPath(): string;
43
46
  connect(): NodeSQLiteConnection;
47
+ reloadVectorCache(): void;
48
+ private loadVectorCache;
44
49
  disconnect(): void;
45
50
  isConnected(): boolean;
46
51
  prepare(sql: string): Statement;
47
52
  exec(sql: string): void;
48
53
  transaction<T>(fn: () => T): T;
49
- vectorSearch(embedding: Float32Array | number[], limit?: number): VectorSearchResult[] | null;
54
+ vectorSearch(embedding: Float32Array | number[], limit?: number, topicPrefix?: string): VectorSearchResult[] | null;
50
55
  insertEmbedding(rowid: number, embedding: Float32Array | number[]): RunResult | null;
51
56
  getLastInsertRowid(): number;
52
57
  runMigrations(migrationsDir: string): void;
@@ -18,19 +18,34 @@ const debug_logger_js_1 = require("../debug-logger.js");
18
18
  const embeddings_js_1 = require("../embeddings.js");
19
19
  const LEGACY_DB_PATH = path_1.default.join(os_1.default.homedir(), '.spinelift', 'memories.db');
20
20
  const DEFAULT_DB_PATH = path_1.default.join(os_1.default.homedir(), '.claude', 'mama-memory.db');
21
+ // Prefer better-sqlite3 (includes FTS5) over node:sqlite (lacks FTS5)
22
+ let BetterSQLite3 = null;
21
23
  let DatabaseSync = null;
22
24
  try {
23
25
  // eslint-disable-next-line @typescript-eslint/no-require-imports
24
- ({ DatabaseSync } = require('node:sqlite'));
26
+ const bs3 = require('better-sqlite3');
27
+ BetterSQLite3 = 'default' in bs3 ? bs3.default : bs3;
25
28
  }
26
29
  catch {
27
- DatabaseSync = null;
30
+ BetterSQLite3 = null;
31
+ }
32
+ if (!BetterSQLite3) {
33
+ try {
34
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
35
+ ({ DatabaseSync } = require('node:sqlite'));
36
+ }
37
+ catch {
38
+ DatabaseSync = null;
39
+ }
28
40
  }
29
41
  class NodeSQLiteConnection {
30
42
  db;
31
43
  connected = true;
44
+ isBetterSQLite3;
32
45
  constructor(db) {
33
46
  this.db = db;
47
+ // better-sqlite3 databases have a native .pragma() method
48
+ this.isBetterSQLite3 = typeof db.pragma === 'function';
34
49
  }
35
50
  prepare(sql) {
36
51
  return this.db.prepare(sql);
@@ -40,6 +55,11 @@ class NodeSQLiteConnection {
40
55
  }
41
56
  pragma(sql, options) {
42
57
  const query = sql.trim().replace(/^PRAGMA\s+/i, '');
58
+ if (this.isBetterSQLite3) {
59
+ // better-sqlite3 has a native pragma method that handles both read and write pragmas
60
+ return this.db.pragma(query, options);
61
+ }
62
+ // node:sqlite fallback: use prepare()
43
63
  const stmt = this.db.prepare(`PRAGMA ${query}`);
44
64
  if (options?.simple) {
45
65
  const row = stmt.get();
@@ -65,6 +85,8 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
65
85
  config;
66
86
  db = null;
67
87
  _vectorSearchEnabled = true;
88
+ vectorCache = new Map();
89
+ topicCache = new Map();
68
90
  constructor(config = {}) {
69
91
  super();
70
92
  this.config = config;
@@ -90,27 +112,67 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
90
112
  if (this.db) {
91
113
  return this.db;
92
114
  }
93
- if (!DatabaseSync) {
94
- throw new Error('node:sqlite is not available in this Node.js runtime. Use Node 22.13+.');
115
+ const Driver = BetterSQLite3 || DatabaseSync;
116
+ if (!Driver) {
117
+ throw new Error('No SQLite driver available. Install better-sqlite3 or use Node 22.13+ (node:sqlite).');
95
118
  }
119
+ const driverName = BetterSQLite3 ? 'better-sqlite3' : 'node:sqlite';
96
120
  const dbPath = this.getDbPath();
97
121
  const dbDir = path_1.default.dirname(dbPath);
98
122
  if (!fs_1.default.existsSync(dbDir)) {
99
123
  fs_1.default.mkdirSync(dbDir, { recursive: true });
100
- (0, debug_logger_js_1.info)(`[node-sqlite-adapter] Created database directory: ${dbDir}`);
124
+ (0, debug_logger_js_1.info)(`[sqlite-adapter] Created database directory: ${dbDir}`);
101
125
  }
102
- const database = new DatabaseSync(dbPath);
126
+ const database = new Driver(dbPath);
103
127
  this.db = new NodeSQLiteConnection(database);
104
- (0, debug_logger_js_1.info)(`[node-sqlite-adapter] Opened database at: ${dbPath}`);
128
+ (0, debug_logger_js_1.info)(`[sqlite-adapter] Opened database at: ${dbPath} (driver: ${driverName})`);
105
129
  this.db.pragma('journal_mode = WAL');
106
130
  this.db.pragma('busy_timeout = 5000');
107
131
  this.db.pragma('synchronous = NORMAL');
108
132
  this.db.pragma('cache_size = -64000');
109
133
  this.db.pragma('temp_store = MEMORY');
110
134
  this.db.pragma('foreign_keys = ON');
111
- (0, debug_logger_js_1.info)('[node-sqlite-adapter] Vector search: pure TS cosine similarity (no native extensions)');
135
+ (0, debug_logger_js_1.info)('[sqlite-adapter] Vector search: pure TS cosine similarity (no native extensions)');
136
+ this.loadVectorCache();
112
137
  return this.db;
113
138
  }
139
+ reloadVectorCache() {
140
+ this.loadVectorCache();
141
+ }
142
+ loadVectorCache() {
143
+ if (!this.db)
144
+ return;
145
+ const tableCheck = this.db
146
+ .prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='embeddings'`)
147
+ .all();
148
+ if (tableCheck.length === 0) {
149
+ this.vectorCache.clear();
150
+ this.topicCache.clear();
151
+ return;
152
+ }
153
+ const start = Date.now();
154
+ const rows = this.db.prepare('SELECT rowid, embedding FROM embeddings').all();
155
+ const CACHE_WARN_THRESHOLD = 100_000;
156
+ this.vectorCache.clear();
157
+ for (const row of rows) {
158
+ const vec = bytesToVector(row.embedding);
159
+ if (vec) {
160
+ this.vectorCache.set(row.rowid, vec);
161
+ }
162
+ }
163
+ // Load topic cache for scoped vector search
164
+ this.topicCache.clear();
165
+ const topicRows = this.db.prepare('SELECT rowid, topic FROM decisions').all();
166
+ for (const row of topicRows) {
167
+ this.topicCache.set(row.rowid, row.topic);
168
+ }
169
+ const count = this.vectorCache.size;
170
+ const elapsed = Date.now() - start;
171
+ (0, debug_logger_js_1.info)(`[node-sqlite-adapter] Vector cache loaded: ${count} embeddings in ${elapsed}ms`);
172
+ if (count > CACHE_WARN_THRESHOLD) {
173
+ (0, debug_logger_js_1.warn)(`[node-sqlite-adapter] Vector cache holds ${count} embeddings — consider LRU eviction or on-demand loading for large datasets`);
174
+ }
175
+ }
114
176
  disconnect() {
115
177
  if (this.db) {
116
178
  this.db.close();
@@ -157,53 +219,39 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
157
219
  throw error;
158
220
  }
159
221
  }
160
- vectorSearch(embedding, limit = 5) {
222
+ vectorSearch(embedding, limit = 5, topicPrefix) {
161
223
  if (!this.isConnected()) {
162
224
  throw new Error('Database not connected');
163
225
  }
164
226
  const queryVector = embedding instanceof Float32Array ? embedding : Float32Array.from(embedding);
165
- const tableCheck = this.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='embeddings'`).all();
166
- if (tableCheck.length === 0) {
167
- throw new Error('Embeddings table missing');
168
- }
169
227
  const effectiveLimit = Math.max(limit, 1);
170
- const batchSize = 500;
171
228
  const bestMatches = [];
172
- let offset = 0;
173
- let hasMoreRows = true;
174
- while (hasMoreRows) {
175
- const rows = this.prepare('SELECT rowid, embedding FROM embeddings LIMIT ? OFFSET ?').all(batchSize, offset);
176
- if (rows.length === 0) {
177
- hasMoreRows = false;
229
+ let minScore = -Infinity;
230
+ for (const [rowid, candidate] of this.vectorCache) {
231
+ if (candidate.length !== queryVector.length)
178
232
  continue;
179
- }
180
- for (const row of rows) {
181
- const candidate = bytesToVector(row.embedding);
182
- if (!candidate)
183
- continue;
184
- if (candidate.length !== queryVector.length) {
185
- (0, debug_logger_js_1.warn)(`Skipping rowid ${row.rowid}: dimension mismatch (${candidate.length} vs ${queryVector.length})`);
233
+ // Pre-filter by topic prefix before computing similarity
234
+ if (topicPrefix) {
235
+ const topic = this.topicCache.get(rowid);
236
+ if (!topic || !topic.startsWith(topicPrefix))
186
237
  continue;
187
- }
188
- const similarity = (0, embeddings_js_1.cosineSimilarity)(candidate, queryVector);
189
- const scoredRow = {
190
- rowid: row.rowid,
191
- similarity,
192
- distance: 1 - similarity,
193
- };
194
- if (bestMatches.length < effectiveLimit) {
195
- bestMatches.push(scoredRow);
196
- bestMatches.sort((a, b) => (b.similarity ?? 0) - (a.similarity ?? 0));
197
- continue;
198
- }
199
- const weakestMatch = bestMatches[bestMatches.length - 1];
200
- if ((scoredRow.similarity ?? 0) > (weakestMatch.similarity ?? 0)) {
201
- bestMatches.pop();
202
- bestMatches.push(scoredRow);
238
+ }
239
+ const similarity = (0, embeddings_js_1.cosineSimilarity)(candidate, queryVector);
240
+ if (bestMatches.length < effectiveLimit) {
241
+ bestMatches.push({ rowid, similarity, distance: 1 - similarity });
242
+ if (bestMatches.length === effectiveLimit) {
203
243
  bestMatches.sort((a, b) => (b.similarity ?? 0) - (a.similarity ?? 0));
244
+ minScore = bestMatches[bestMatches.length - 1].similarity ?? -Infinity;
204
245
  }
205
246
  }
206
- offset += rows.length;
247
+ else if (similarity > minScore) {
248
+ bestMatches[bestMatches.length - 1] = { rowid, similarity, distance: 1 - similarity };
249
+ bestMatches.sort((a, b) => (b.similarity ?? 0) - (a.similarity ?? 0));
250
+ minScore = bestMatches[bestMatches.length - 1].similarity ?? -Infinity;
251
+ }
252
+ }
253
+ if (bestMatches.length < effectiveLimit) {
254
+ bestMatches.sort((a, b) => (b.similarity ?? 0) - (a.similarity ?? 0));
207
255
  }
208
256
  return bestMatches;
209
257
  }
@@ -217,7 +265,13 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
217
265
  INSERT OR REPLACE INTO embeddings(rowid, embedding)
218
266
  VALUES (?, ?)
219
267
  `);
220
- return stmt.run(rowid, buffer);
268
+ const result = stmt.run(rowid, buffer);
269
+ // Keep in-memory caches in sync
270
+ this.vectorCache.set(rowid, vec);
271
+ const topicRow = this.prepare('SELECT topic FROM decisions WHERE rowid = ?').get(rowid);
272
+ if (topicRow)
273
+ this.topicCache.set(rowid, topicRow.topic);
274
+ return result;
221
275
  }
222
276
  getLastInsertRowid() {
223
277
  if (!this.isConnected()) {