@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.
- package/README.md +67 -19
- package/db/migrations/001-initial-decision-graph.sql +6 -1
- package/db/migrations/002-add-error-patterns.sql +1 -1
- package/db/migrations/014-add-is-static-column.sql +5 -0
- package/db/migrations/015-add-fts5-search.sql +34 -0
- package/db/migrations/016-add-memory-v2-columns.sql +14 -0
- package/db/migrations/017-create-memory-scopes.sql +10 -0
- package/db/migrations/018-create-memory-scope-bindings.sql +15 -0
- package/db/migrations/019-create-memory-events.sql +18 -0
- package/db/migrations/020-create-audit-findings.sql +18 -0
- package/db/migrations/021-create-memory-truth.sql +20 -0
- package/db/migrations/022-create-channel-summaries.sql +9 -0
- package/db/migrations/023-create-channel-summary-state.sql +9 -0
- package/db/migrations/024-add-event-date.sql +9 -0
- package/db/migrations/025-extend-kind-check.sql +59 -0
- package/dist/config-loader.d.ts +1 -1
- package/dist/config-loader.js +3 -3
- package/dist/db-adapter/base-adapter.d.ts +2 -2
- package/dist/db-adapter/index.js +2 -8
- package/dist/db-adapter/node-sqlite-adapter.d.ts +6 -1
- package/dist/db-adapter/node-sqlite-adapter.js +99 -45
- package/dist/db-manager.d.ts +32 -4
- package/dist/db-manager.js +133 -8
- package/dist/decision-formatter.d.ts +2 -0
- package/dist/embedding-server/mobile/session-manager.js +22 -7
- package/dist/embeddings.d.ts +2 -2
- package/dist/embeddings.js +2 -2
- package/dist/index.d.ts +5 -1
- package/dist/index.js +31 -3
- package/dist/mama-api.d.ts +67 -2
- package/dist/mama-api.js +295 -80
- package/dist/memory/api.d.ts +81 -0
- package/dist/memory/api.js +972 -0
- package/dist/memory/bootstrap-builder.d.ts +10 -0
- package/dist/memory/bootstrap-builder.js +66 -0
- package/dist/memory/channel-summary-state-store.d.ts +19 -0
- package/dist/memory/channel-summary-state-store.js +223 -0
- package/dist/memory/channel-summary-store.d.ts +8 -0
- package/dist/memory/channel-summary-store.js +41 -0
- package/dist/memory/event-store.d.ts +5 -0
- package/dist/memory/event-store.js +72 -0
- package/dist/memory/evolution-engine.d.ts +22 -0
- package/dist/memory/evolution-engine.js +133 -0
- package/dist/memory/extraction-prompt.d.ts +4 -0
- package/dist/memory/extraction-prompt.js +89 -0
- package/dist/memory/finding-store.d.ts +5 -0
- package/dist/memory/finding-store.js +75 -0
- package/dist/memory/profile-builder.d.ts +3 -0
- package/dist/memory/profile-builder.js +23 -0
- package/dist/memory/scope-store.d.ts +5 -0
- package/dist/memory/scope-store.js +13 -0
- package/dist/memory/truth-store.d.ts +11 -0
- package/dist/memory/truth-store.js +102 -0
- package/dist/memory/types.d.ts +267 -0
- package/dist/memory/types.js +83 -0
- 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
|
-
|
|
26
|
+
const bs3 = require('better-sqlite3');
|
|
27
|
+
BetterSQLite3 = 'default' in bs3 ? bs3.default : bs3;
|
|
25
28
|
}
|
|
26
29
|
catch {
|
|
27
|
-
|
|
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
|
-
|
|
94
|
-
|
|
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)(`[
|
|
124
|
+
(0, debug_logger_js_1.info)(`[sqlite-adapter] Created database directory: ${dbDir}`);
|
|
101
125
|
}
|
|
102
|
-
const database = new
|
|
126
|
+
const database = new Driver(dbPath);
|
|
103
127
|
this.db = new NodeSQLiteConnection(database);
|
|
104
|
-
(0, debug_logger_js_1.info)(`[
|
|
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)('[
|
|
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
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
181
|
-
const
|
|
182
|
-
if (!
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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()) {
|
package/dist/db-manager.d.ts
CHANGED
|
@@ -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 -
|
|
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 (
|
|
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
|
package/dist/db-manager.js
CHANGED
|
@@ -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 -
|
|
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 (
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
|
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.
|
|
169
|
-
createdAt: row.
|
|
170
|
-
lastActive: row.
|
|
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,
|
package/dist/embeddings.d.ts
CHANGED
|
@@ -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-
|
|
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
|
|
46
|
+
* @returns 1024-dim enhanced embedding
|
|
47
47
|
*/
|
|
48
48
|
export declare function generateEnhancedEmbedding(decision: DecisionForEmbedding): Promise<Float32Array>;
|
|
49
49
|
/**
|
package/dist/embeddings.js
CHANGED
|
@@ -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-
|
|
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
|
|
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';
|