@jungjaehoon/mama-core 1.3.3 → 1.4.1

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 (56) hide show
  1. package/README.md +67 -19
  2. package/db/migrations/001-initial-decision-graph.sql +6 -1
  3. package/db/migrations/002-add-error-patterns.sql +1 -1
  4. package/db/migrations/014-add-is-static-column.sql +5 -0
  5. package/db/migrations/015-add-fts5-search.sql +34 -0
  6. package/db/migrations/016-add-memory-v2-columns.sql +14 -0
  7. package/db/migrations/017-create-memory-scopes.sql +10 -0
  8. package/db/migrations/018-create-memory-scope-bindings.sql +15 -0
  9. package/db/migrations/019-create-memory-events.sql +18 -0
  10. package/db/migrations/020-create-audit-findings.sql +18 -0
  11. package/db/migrations/021-create-memory-truth.sql +20 -0
  12. package/db/migrations/022-create-channel-summaries.sql +9 -0
  13. package/db/migrations/023-create-channel-summary-state.sql +9 -0
  14. package/db/migrations/024-add-event-date.sql +9 -0
  15. package/db/migrations/025-extend-kind-check.sql +59 -0
  16. package/dist/config-loader.d.ts +1 -1
  17. package/dist/config-loader.js +3 -3
  18. package/dist/db-adapter/base-adapter.d.ts +2 -2
  19. package/dist/db-adapter/index.js +2 -8
  20. package/dist/db-adapter/node-sqlite-adapter.d.ts +6 -1
  21. package/dist/db-adapter/node-sqlite-adapter.js +99 -45
  22. package/dist/db-manager.d.ts +32 -4
  23. package/dist/db-manager.js +133 -8
  24. package/dist/decision-formatter.d.ts +2 -0
  25. package/dist/embedding-server/mobile/session-manager.js +22 -7
  26. package/dist/embeddings.d.ts +2 -2
  27. package/dist/embeddings.js +2 -2
  28. package/dist/index.d.ts +5 -1
  29. package/dist/index.js +31 -3
  30. package/dist/mama-api.d.ts +67 -2
  31. package/dist/mama-api.js +295 -80
  32. package/dist/memory/api.d.ts +81 -0
  33. package/dist/memory/api.js +972 -0
  34. package/dist/memory/bootstrap-builder.d.ts +10 -0
  35. package/dist/memory/bootstrap-builder.js +66 -0
  36. package/dist/memory/channel-summary-state-store.d.ts +19 -0
  37. package/dist/memory/channel-summary-state-store.js +223 -0
  38. package/dist/memory/channel-summary-store.d.ts +8 -0
  39. package/dist/memory/channel-summary-store.js +41 -0
  40. package/dist/memory/event-store.d.ts +5 -0
  41. package/dist/memory/event-store.js +72 -0
  42. package/dist/memory/evolution-engine.d.ts +22 -0
  43. package/dist/memory/evolution-engine.js +133 -0
  44. package/dist/memory/extraction-prompt.d.ts +4 -0
  45. package/dist/memory/extraction-prompt.js +89 -0
  46. package/dist/memory/finding-store.d.ts +5 -0
  47. package/dist/memory/finding-store.js +75 -0
  48. package/dist/memory/profile-builder.d.ts +3 -0
  49. package/dist/memory/profile-builder.js +23 -0
  50. package/dist/memory/scope-store.d.ts +5 -0
  51. package/dist/memory/scope-store.js +13 -0
  52. package/dist/memory/truth-store.d.ts +11 -0
  53. package/dist/memory/truth-store.js +102 -0
  54. package/dist/memory/types.d.ts +267 -0
  55. package/dist/memory/types.js +83 -0
  56. package/package.json +4 -1
@@ -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()) {
@@ -26,8 +26,9 @@ export interface DatabaseAdapter {
26
26
  prepare: (sql: string) => PreparedStatement;
27
27
  transaction: <T>(fn: () => T) => T;
28
28
  insertEmbedding: (rowid: number, embedding: Float32Array | number[]) => void;
29
- vectorSearch: (embedding: Float32Array | number[], limit: number) => Promise<VectorSearchResult[] | null>;
29
+ vectorSearch: (embedding: Float32Array | number[], limit: number, topicPrefix?: string) => Promise<VectorSearchResult[] | null> | VectorSearchResult[] | null;
30
30
  vectorSearchEnabled: boolean;
31
+ reloadVectorCache?: () => void;
31
32
  getDbPath?: () => string;
32
33
  dbPath?: string;
33
34
  constructor: {
@@ -54,6 +55,8 @@ export interface DecisionRecord {
54
55
  created_at: number;
55
56
  updated_at?: number;
56
57
  edges?: DecisionEdgeRow[];
58
+ /** ISO 8601 date when the event actually occurred. Null if not set. */
59
+ event_date?: string | null;
57
60
  }
58
61
  export interface OutcomeData {
59
62
  outcome?: string | null;
@@ -67,6 +70,7 @@ export interface VectorSearchParams {
67
70
  limit?: number;
68
71
  threshold?: number;
69
72
  timeWindow?: number;
73
+ includeSuperseded?: boolean;
70
74
  }
71
75
  export interface DecisionEdgeRow {
72
76
  from_id: string;
@@ -140,13 +144,19 @@ export declare function getAdapter(): DatabaseAdapter;
140
144
  * Call this on process exit
141
145
  */
142
146
  export declare function closeDB(): Promise<void>;
147
+ export declare function ensureMemoryScope(kind: string, externalId: string): Promise<string>;
148
+ export declare function bindMemoryToScope(memoryId: string, scopeId: string, isPrimary?: boolean): Promise<void>;
149
+ export declare function listScopesForMemory(memoryId: string): Promise<Array<{
150
+ kind: string;
151
+ id: string;
152
+ }>>;
143
153
  /**
144
154
  * Insert embedding into vector search table
145
155
  *
146
156
  * Stores embedding in the embeddings table for vector similarity search
147
157
  *
148
158
  * @param decisionRowid - SQLite rowid
149
- * @param embedding - 384-dim embedding vector
159
+ * @param embedding - 1024-dim embedding vector
150
160
  */
151
161
  export declare function insertEmbedding(decisionRowid: number, embedding: Float32Array | number[]): Promise<void>;
152
162
  /**
@@ -154,12 +164,12 @@ export declare function insertEmbedding(decisionRowid: number, embedding: Float3
154
164
  *
155
165
  * Returns empty array if vector search not available (no keyword fallback)
156
166
  *
157
- * @param queryEmbedding - Query embedding (384-dim)
167
+ * @param queryEmbedding - Query embedding (1024-dim)
158
168
  * @param limit - Max results to return (default: 5)
159
169
  * @param threshold - Minimum similarity threshold (default: 0.7)
160
170
  * @returns Array of decisions with similarity scores, or empty array
161
171
  */
162
- export declare function vectorSearch(queryEmbedding: Float32Array | number[], limit?: number, threshold?: number): Promise<DecisionRecord[]>;
172
+ export declare function vectorSearch(queryEmbedding: Float32Array | number[], limit?: number, threshold?: number, topicPrefix?: string): Promise<DecisionRecord[]>;
163
173
  /**
164
174
  * Decision input for storage
165
175
  */
@@ -190,6 +200,8 @@ export interface DecisionInput {
190
200
  evidence?: string | null;
191
201
  alternatives?: string | null;
192
202
  risks?: string | null;
203
+ /** ISO 8601 date string for when the event actually occurred (e.g. "2023-01-15") */
204
+ event_date?: string | null;
193
205
  }
194
206
  /**
195
207
  * Insert decision with embedding
@@ -271,4 +283,20 @@ export declare function resetDBState(options?: {
271
283
  * Returns true if MAMA_TEST_MODE or VITEST env vars are set
272
284
  */
273
285
  export declare function isTestMode(): boolean;
286
+ /**
287
+ * FTS5 keyword search on decisions table.
288
+ * Returns matching decision IDs with BM25 rank scores.
289
+ */
290
+ export declare function fts5Search(query: string, limit?: number): Promise<{
291
+ id: string;
292
+ rank: number;
293
+ }[]>;
294
+ /**
295
+ * Re-generate all embeddings using the currently configured model.
296
+ * Call this after changing the embedding model in config.json.
297
+ *
298
+ * @param onProgress - Optional callback with (completed, total) counts
299
+ * @returns Number of embeddings regenerated
300
+ */
301
+ export declare function reindexEmbeddings(onProgress?: (completed: number, total: number) => void): Promise<number>;
274
302
  //# sourceMappingURL=db-manager.d.ts.map
@@ -26,6 +26,9 @@ exports.initDB = initDB;
26
26
  exports.getDB = getDB;
27
27
  exports.getAdapter = getAdapter;
28
28
  exports.closeDB = closeDB;
29
+ exports.ensureMemoryScope = ensureMemoryScope;
30
+ exports.bindMemoryToScope = bindMemoryToScope;
31
+ exports.listScopesForMemory = listScopesForMemory;
29
32
  exports.insertEmbedding = insertEmbedding;
30
33
  exports.vectorSearch = vectorSearch;
31
34
  exports.insertDecisionWithEmbedding = insertDecisionWithEmbedding;
@@ -37,6 +40,8 @@ exports.getPreparedStmt = getPreparedStmt;
37
40
  exports.getDbPath = getDbPath;
38
41
  exports.resetDBState = resetDBState;
39
42
  exports.isTestMode = isTestMode;
43
+ exports.fts5Search = fts5Search;
44
+ exports.reindexEmbeddings = reindexEmbeddings;
40
45
  const path_1 = __importDefault(require("path"));
41
46
  const debug_logger_js_1 = require("./debug-logger.js");
42
47
  const progress_indicator_js_1 = require("./progress-indicator.js");
@@ -78,6 +83,10 @@ async function initDB() {
78
83
  dbConnection = await dbAdapter.connect();
79
84
  // Run migrations (includes 012-create-checkpoints-table.sql)
80
85
  await dbAdapter.runMigrations(MIGRATIONS_DIR);
86
+ // Reload vector cache after migrations (new tables/rows may now exist)
87
+ if (typeof dbAdapter.reloadVectorCache === 'function') {
88
+ dbAdapter.reloadVectorCache();
89
+ }
81
90
  isInitialized = true;
82
91
  (0, debug_logger_js_1.info)(`[db-manager] Database initialized (${dbAdapter.constructor.name})`);
83
92
  (0, progress_indicator_js_1.logComplete)('Database ready');
@@ -124,6 +133,9 @@ function getAdapter() {
124
133
  }
125
134
  return dbAdapter;
126
135
  }
136
+ function buildMemoryScopeId(kind, externalId) {
137
+ return `scope_${kind}_${Buffer.from(externalId).toString('base64url')}`;
138
+ }
127
139
  /**
128
140
  * Close database connection
129
141
  *
@@ -139,13 +151,45 @@ async function closeDB() {
139
151
  (0, debug_logger_js_1.info)('[db-manager] Database connection closed');
140
152
  }
141
153
  }
154
+ async function ensureMemoryScope(kind, externalId) {
155
+ const adapter = getAdapter();
156
+ const id = buildMemoryScopeId(kind, externalId);
157
+ adapter
158
+ .prepare(`
159
+ INSERT OR IGNORE INTO memory_scopes (id, kind, external_id)
160
+ VALUES (?, ?, ?)
161
+ `)
162
+ .run(id, kind, externalId);
163
+ return id;
164
+ }
165
+ async function bindMemoryToScope(memoryId, scopeId, isPrimary = false) {
166
+ const adapter = getAdapter();
167
+ adapter
168
+ .prepare(`
169
+ INSERT OR REPLACE INTO memory_scope_bindings (memory_id, scope_id, is_primary)
170
+ VALUES (?, ?, ?)
171
+ `)
172
+ .run(memoryId, scopeId, isPrimary ? 1 : 0);
173
+ }
174
+ async function listScopesForMemory(memoryId) {
175
+ const adapter = getAdapter();
176
+ return adapter
177
+ .prepare(`
178
+ SELECT ms.kind, ms.external_id AS id
179
+ FROM memory_scope_bindings msb
180
+ JOIN memory_scopes ms ON ms.id = msb.scope_id
181
+ WHERE msb.memory_id = ?
182
+ ORDER BY msb.is_primary DESC, ms.created_at ASC
183
+ `)
184
+ .all(memoryId);
185
+ }
142
186
  /**
143
187
  * Insert embedding into vector search table
144
188
  *
145
189
  * Stores embedding in the embeddings table for vector similarity search
146
190
  *
147
191
  * @param decisionRowid - SQLite rowid
148
- * @param embedding - 384-dim embedding vector
192
+ * @param embedding - 1024-dim embedding vector
149
193
  */
150
194
  async function insertEmbedding(decisionRowid, embedding) {
151
195
  const adapter = getAdapter();
@@ -163,16 +207,16 @@ async function insertEmbedding(decisionRowid, embedding) {
163
207
  *
164
208
  * Returns empty array if vector search not available (no keyword fallback)
165
209
  *
166
- * @param queryEmbedding - Query embedding (384-dim)
210
+ * @param queryEmbedding - Query embedding (1024-dim)
167
211
  * @param limit - Max results to return (default: 5)
168
212
  * @param threshold - Minimum similarity threshold (default: 0.7)
169
213
  * @returns Array of decisions with similarity scores, or empty array
170
214
  */
171
- async function vectorSearch(queryEmbedding, limit = 5, threshold = 0.7) {
215
+ async function vectorSearch(queryEmbedding, limit = 5, threshold = 0.7, topicPrefix) {
172
216
  const adapter = getAdapter();
173
217
  try {
174
- // Brute-force cosine similarity over all embeddings
175
- const results = await adapter.vectorSearch(queryEmbedding, limit * 3);
218
+ // Brute-force cosine similarity over all embeddings (with optional topic pre-filter)
219
+ const results = await adapter.vectorSearch(queryEmbedding, limit * 3, topicPrefix);
176
220
  if (!results || results.length === 0) {
177
221
  return []; // No keyword fallback - fast fail
178
222
  }
@@ -216,6 +260,13 @@ async function vectorSearch(queryEmbedding, limit = 5, threshold = 0.7) {
216
260
  async function insertDecisionWithEmbedding(decision) {
217
261
  const adapter = getAdapter();
218
262
  const { generateEnhancedEmbedding } = await import('./embeddings.js');
263
+ // Validate event_date before any DB operations
264
+ if (decision.event_date !== null && decision.event_date !== undefined) {
265
+ const d = decision.event_date;
266
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(d) || isNaN(new Date(d).getTime())) {
267
+ throw new Error(`Invalid event_date: must be ISO 8601 YYYY-MM-DD (got: ${d})`);
268
+ }
269
+ }
219
270
  try {
220
271
  // Generate embedding BEFORE transaction (required for SQLite's sync transaction)
221
272
  // Note: Redact topic for privacy - only log length
@@ -248,14 +299,14 @@ async function insertDecisionWithEmbedding(decision) {
248
299
  confidence, created_at, updated_at,
249
300
  needs_validation, validation_attempts, last_validated_at, usage_count,
250
301
  trust_context, usage_success, usage_failure, time_saved,
251
- evidence, alternatives, risks
252
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
302
+ evidence, alternatives, risks, event_date
303
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
253
304
  `);
254
305
  const insertResult = stmt.run(decision.id, decision.topic, decision.decision, decision.reasoning || null, decision.outcome || null, decision.failure_reason || null, decision.limitation || null, decision.user_involvement || null, decision.session_id || null, decision.supersedes || null, decision.superseded_by || null, decision.refined_from ? JSON.stringify(decision.refined_from) : null, decision.confidence !== undefined ? decision.confidence : 0.5,
255
306
  // IMPORTANT: All timestamps are stored in milliseconds (Date.now()).
256
307
  // The schema DEFAULT uses unixepoch() (seconds) but is never used
257
308
  // since all inserts go through this function which always provides ms.
258
- decision.created_at || Date.now(), decision.updated_at || Date.now(), decision.needs_validation !== undefined ? decision.needs_validation : 0, decision.validation_attempts || 0, decision.last_validated_at || null, decision.usage_count || 0, decision.trust_context || null, decision.usage_success || 0, decision.usage_failure || 0, decision.time_saved || 0, decision.evidence || null, decision.alternatives || null, decision.risks || null);
309
+ decision.created_at || Date.now(), decision.updated_at || Date.now(), decision.needs_validation !== undefined ? decision.needs_validation : 0, decision.validation_attempts || 0, decision.last_validated_at || null, decision.usage_count || 0, decision.trust_context || null, decision.usage_success || 0, decision.usage_failure || 0, decision.time_saved || 0, decision.evidence || null, decision.alternatives || null, decision.risks || null, decision.event_date || null);
259
310
  const rowid = Number(insertResult.lastInsertRowid);
260
311
  // Insert embedding in same transaction to ensure rowid matching
261
312
  (0, debug_logger_js_1.info)(`[db-manager] Vector search enabled: ${adapter.vectorSearchEnabled}`);
@@ -485,6 +536,10 @@ async function queryVectorSearch(params) {
485
536
  if (similarity < threshold) {
486
537
  continue;
487
538
  }
539
+ // Filter out superseded decisions unless explicitly requested
540
+ if (!params.includeSuperseded && decision.superseded_by) {
541
+ continue;
542
+ }
488
543
  results.push({
489
544
  ...decision,
490
545
  similarity,
@@ -618,4 +673,74 @@ function resetDBState(options = {}) {
618
673
  function isTestMode() {
619
674
  return !!(process.env.MAMA_TEST_MODE || process.env.VITEST);
620
675
  }
676
+ /**
677
+ * FTS5 keyword search on decisions table.
678
+ * Returns matching decision IDs with BM25 rank scores.
679
+ */
680
+ async function fts5Search(query, limit = 10) {
681
+ const adapter = getAdapter();
682
+ // Check if FTS5 table exists (swallow errors — table may not exist yet)
683
+ let tableCheck;
684
+ try {
685
+ tableCheck = adapter
686
+ .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='decisions_fts'")
687
+ .get();
688
+ }
689
+ catch {
690
+ return [];
691
+ }
692
+ if (!tableCheck)
693
+ return [];
694
+ // Query execution — let errors propagate to the caller
695
+ const stmt = adapter.prepare(`
696
+ SELECT d.id, rank
697
+ FROM decisions_fts
698
+ JOIN decisions d ON decisions_fts.rowid = d.rowid
699
+ WHERE decisions_fts MATCH ?
700
+ ORDER BY rank
701
+ LIMIT ?
702
+ `);
703
+ return stmt.all(query, limit);
704
+ }
705
+ /**
706
+ * Re-generate all embeddings using the currently configured model.
707
+ * Call this after changing the embedding model in config.json.
708
+ *
709
+ * @param onProgress - Optional callback with (completed, total) counts
710
+ * @returns Number of embeddings regenerated
711
+ */
712
+ async function reindexEmbeddings(onProgress) {
713
+ const adapter = getAdapter();
714
+ const { generateEnhancedEmbedding } = await import('./embeddings.js');
715
+ const decisions = adapter
716
+ .prepare('SELECT rowid, topic, decision, reasoning, outcome, confidence FROM decisions')
717
+ .all();
718
+ if (typeof adapter.insertEmbedding !== 'function') {
719
+ throw new Error('Embedding insertion is not available on this adapter');
720
+ }
721
+ const total = decisions.length;
722
+ let completed = 0;
723
+ for (const row of decisions) {
724
+ try {
725
+ const embedding = await generateEnhancedEmbedding({
726
+ topic: row.topic,
727
+ decision: row.decision,
728
+ reasoning: row.reasoning || undefined,
729
+ outcome: row.outcome || undefined,
730
+ confidence: row.confidence ?? undefined,
731
+ });
732
+ adapter.insertEmbedding(row.rowid, embedding);
733
+ completed++;
734
+ if (onProgress && completed % 50 === 0) {
735
+ onProgress(completed, total);
736
+ }
737
+ }
738
+ catch (e) {
739
+ (0, debug_logger_js_1.error)(`[db-manager] Failed to reindex rowid ${row.rowid}: ${e}`);
740
+ }
741
+ }
742
+ onProgress?.(completed, total);
743
+ (0, debug_logger_js_1.info)(`[db-manager] Reindexed ${completed}/${total} embeddings`);
744
+ return completed;
745
+ }
621
746
  //# sourceMappingURL=db-manager.js.map
@@ -36,6 +36,8 @@ export interface DecisionForFormat {
36
36
  evidence?: string | string[] | unknown;
37
37
  alternatives?: string | string[] | unknown;
38
38
  risks?: string;
39
+ /** ISO 8601 date when the event actually occurred. Null if not set. */
40
+ event_date?: string | null;
39
41
  }
40
42
  /**
41
43
  * Trust context object
@@ -31,7 +31,7 @@ exports.DEFAULT_DB_PATH = process.env.MAMA_DB_PATH || path_1.default.join(os_1.d
31
31
  exports.CREATE_SESSIONS_TABLE = `
32
32
  CREATE TABLE IF NOT EXISTS sessions (
33
33
  id TEXT PRIMARY KEY,
34
- project_dir TEXT NOT NULL,
34
+ project_path TEXT,
35
35
  created_at TEXT DEFAULT (datetime('now')),
36
36
  last_active TEXT DEFAULT (datetime('now')),
37
37
  status TEXT DEFAULT 'active',
@@ -83,6 +83,21 @@ class SessionManager {
83
83
  // Create sessions table if not exists
84
84
  this.db.exec(exports.CREATE_SESSIONS_TABLE);
85
85
  this.db.exec(CREATE_STATUS_INDEX);
86
+ // Add missing columns for older databases
87
+ for (const col of [
88
+ 'pid INTEGER',
89
+ 'client_id TEXT',
90
+ "status TEXT DEFAULT 'active'",
91
+ 'last_active TEXT',
92
+ 'created_at TEXT',
93
+ ]) {
94
+ try {
95
+ this.db.exec(`ALTER TABLE sessions ADD COLUMN ${col}`);
96
+ }
97
+ catch {
98
+ // Column already exists
99
+ }
100
+ }
86
101
  // Mark any orphaned 'active' sessions as 'terminated'
87
102
  // (from previous server crashes)
88
103
  this.db
@@ -121,7 +136,7 @@ class SessionManager {
121
136
  await daemon.spawn();
122
137
  // Insert into database
123
138
  const stmt = this.db.prepare(`
124
- INSERT INTO sessions (id, project_dir, status, pid)
139
+ INSERT INTO sessions (id, project_path, status, pid)
125
140
  VALUES (?, ?, 'active', ?)
126
141
  `);
127
142
  stmt.run(sessionId, projectDir, daemon.getPid());
@@ -154,10 +169,10 @@ class SessionManager {
154
169
  await this.initDB();
155
170
  }
156
171
  const stmt = this.db.prepare(`
157
- SELECT id, project_dir, started_at, last_active_at, status, pid, client_id
172
+ SELECT id, project_path, created_at, last_active, status, pid, client_id
158
173
  FROM sessions
159
174
  WHERE status = 'active'
160
- ORDER BY started_at DESC
175
+ ORDER BY created_at DESC
161
176
  `);
162
177
  const rows = stmt.all();
163
178
  // Enrich with in-memory data
@@ -165,9 +180,9 @@ class SessionManager {
165
180
  const memSession = this.sessions.get(row.id);
166
181
  return {
167
182
  id: row.id,
168
- projectDir: row.project_dir,
169
- createdAt: row.started_at,
170
- lastActive: row.last_active_at,
183
+ projectDir: row.project_path ?? '',
184
+ createdAt: row.created_at,
185
+ lastActive: row.last_active,
171
186
  status: row.status,
172
187
  pid: row.pid,
173
188
  clientId: row.client_id,
@@ -2,7 +2,7 @@
2
2
  * MAMA (Memory-Augmented MCP Architecture) - Embedding Generation
3
3
  *
4
4
  * Story M1.4: Configurable embedding model selection
5
- * Generates embeddings using configurable model (default: multilingual-e5-small)
5
+ * Generates embeddings using configurable model (default: multilingual-e5-large)
6
6
  * Supports: Korean-English cross-lingual similarity, enhanced metadata
7
7
  *
8
8
  * @module embeddings
@@ -43,7 +43,7 @@ export declare function generateEmbedding(text: string): Promise<Float32Array>;
43
43
  * Inspired by A-mem: Content + Metadata for richer semantic representation
44
44
  *
45
45
  * @param decision - Decision object
46
- * @returns 384-dim enhanced embedding
46
+ * @returns 1024-dim enhanced embedding
47
47
  */
48
48
  export declare function generateEnhancedEmbedding(decision: DecisionForEmbedding): Promise<Float32Array>;
49
49
  /**
@@ -3,7 +3,7 @@
3
3
  * MAMA (Memory-Augmented MCP Architecture) - Embedding Generation
4
4
  *
5
5
  * Story M1.4: Configurable embedding model selection
6
- * Generates embeddings using configurable model (default: multilingual-e5-small)
6
+ * Generates embeddings using configurable model (default: multilingual-e5-large)
7
7
  * Supports: Korean-English cross-lingual similarity, enhanced metadata
8
8
  *
9
9
  * @module embeddings
@@ -142,7 +142,7 @@ async function generateEmbedding(text) {
142
142
  * Inspired by A-mem: Content + Metadata for richer semantic representation
143
143
  *
144
144
  * @param decision - Decision object
145
- * @returns 384-dim enhanced embedding
145
+ * @returns 1024-dim enhanced embedding
146
146
  */
147
147
  async function generateEnhancedEmbedding(decision) {
148
148
  // Construct enriched text representation with narrative fields (Story 2.2)
package/dist/index.d.ts CHANGED
@@ -10,11 +10,15 @@
10
10
  export { generateEmbedding, generateEnhancedEmbedding, generateBatchEmbeddings, cosineSimilarity, embeddingCache, EMBEDDING_DIM, MODEL_NAME, } from './embeddings.js';
11
11
  export { EmbeddingCache } from './embedding-cache.js';
12
12
  export { getEmbeddingFromServer, getServerStatus, isServerRunning, getServerPort, DEFAULT_PORT, HOST, TIMEOUT_MS, type ServerStatus, } from './embedding-client.js';
13
- export { initDB, getDB, getAdapter, closeDB, insertEmbedding, vectorSearch, queryVectorSearch, insertDecisionWithEmbedding, queryDecisionGraph, querySemanticEdges, updateDecisionOutcome, getPreparedStmt, getDbPath, type DatabaseAdapter as DBManagerAdapter, type PreparedStatement, type DecisionRecord, type OutcomeData, type VectorSearchParams, type SemanticEdges, type SemanticEdgeItem, type DecisionInput, } from './db-manager.js';
13
+ export { initDB, getDB, getAdapter, closeDB, insertEmbedding, vectorSearch, queryVectorSearch, insertDecisionWithEmbedding, queryDecisionGraph, querySemanticEdges, updateDecisionOutcome, getPreparedStmt, getDbPath, fts5Search, reindexEmbeddings, type DatabaseAdapter as DBManagerAdapter, type PreparedStatement, type DecisionRecord, type OutcomeData, type VectorSearchParams, type SemanticEdges, type SemanticEdgeItem, type DecisionInput, } from './db-manager.js';
14
14
  export { createAdapter, DatabaseAdapter, SQLiteAdapter, type AdapterConfig, type Statement, type VectorSearchResult, type RunResult, } from './db-adapter/index.js';
15
15
  export { getSessionDecisions, incrementUsageSuccess, incrementUsageFailure, getDecisionById, traverseDecisionChain, DB_PATH, DB_DIR, LEGACY_DB_PATH, DEFAULT_DB_PATH, } from './memory-store.js';
16
16
  import mama from './mama-api.js';
17
17
  export { mama };
18
+ export { MEMORY_SCOPE_KINDS, MEMORY_KINDS, MEMORY_STATUSES, MEMORY_EDGE_TYPES, createEmptyRecallBundle, type MemoryScopeKind, type MemoryKind, type MemoryStatus, type MemoryEdgeType, type MemoryScopeRef, type MemoryRecord, type MemoryEdge, type ProfileSnapshot, type RecallBundle, type ConversationMessage, type IngestConversationInput, type ExtractedMemoryUnit, type IngestConversationResult, } from './memory/types.js';
19
+ export { saveMemory, recallMemory, buildProfile, ingestMemory, evolveMemory, buildMemoryBootstrap, createAuditAck, recordMemoryAudit, ingestConversation, setExtractionFn, upsertChannelSummary, getChannelSummary, } from './memory/api.js';
20
+ export { buildExtractionPrompt, parseExtractionResponse } from './memory/extraction-prompt.js';
21
+ export { listRecentMemoryEvents } from './memory/event-store.js';
18
22
  export { loadConfig, getModelName, getEmbeddingDim, getCacheDir, updateConfig, getConfigPath, DEFAULT_CONFIG, type MAMAConfig, type ConfigUpdates, } from './config-loader.js';
19
23
  export { calculateRelevance, selectTopDecisions, formatTopNContext, testRelevanceScoring, type DecisionWithEmbedding, type QueryContext, type FormattedContext, type TestResult, } from './relevance-scorer.js';
20
24
  export { learnDecision, generateDecisionId, getPreviousDecision, createEdge, createSupersedesEdge, markSuperseded, calculateCombinedConfidence, detectRefinement, parseReasoningForRelationships, createEdgesFromReasoning, getSupersededChainDepth, updateConfidence, VALID_EDGE_TYPES, type EdgeType, type DecisionDetection, type ToolExecution, type SessionContext, type LearnDecisionResult, type ParsedRelationship, type EvidenceItem, } from './decision-tracker.js';