@jungjaehoon/mama-core 1.5.0 → 1.6.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.
- package/README.md +48 -10
- package/db/migrations/032-add-memory-provenance-columns.sql +21 -0
- package/db/migrations/033-create-model-runs-and-tool-traces.sql +53 -0
- package/db/migrations/034-add-connector-event-scope-columns.sql +14 -0
- package/db/migrations/035-create-twin-edges.sql +63 -0
- package/db/migrations/036-create-agent-situation-packets.sql +61 -0
- package/dist/agent-graph/alias-write.d.ts +3 -0
- package/dist/agent-graph/alias-write.js +231 -0
- package/dist/agent-graph/entity-resolve.d.ts +3 -0
- package/dist/agent-graph/entity-resolve.js +275 -0
- package/dist/agent-graph/errors.d.ts +4 -0
- package/dist/agent-graph/errors.js +11 -0
- package/dist/agent-graph/graph-query.d.ts +5 -0
- package/dist/agent-graph/graph-query.js +359 -0
- package/dist/agent-graph/index.d.ts +6 -0
- package/dist/agent-graph/index.js +22 -0
- package/dist/agent-graph/types.d.ts +162 -0
- package/dist/agent-graph/types.js +3 -0
- package/dist/agent-situation/builder.d.ts +8 -0
- package/dist/agent-situation/builder.js +239 -0
- package/dist/agent-situation/cache-key.d.ts +4 -0
- package/dist/agent-situation/cache-key.js +64 -0
- package/dist/agent-situation/index.d.ts +7 -0
- package/dist/agent-situation/index.js +28 -0
- package/dist/agent-situation/packet-store.d.ts +27 -0
- package/dist/agent-situation/packet-store.js +228 -0
- package/dist/agent-situation/ranking-policy.d.ts +18 -0
- package/dist/agent-situation/ranking-policy.js +75 -0
- package/dist/agent-situation/source-readers.d.ts +52 -0
- package/dist/agent-situation/source-readers.js +300 -0
- package/dist/agent-situation/types.d.ts +195 -0
- package/dist/agent-situation/types.js +3 -0
- package/dist/cases/search-rollup.d.ts +4 -0
- package/dist/cases/search-rollup.js +84 -0
- package/dist/connectors/event-index.d.ts +1 -0
- package/dist/connectors/event-index.js +19 -4
- package/dist/connectors/raw-query.d.ts +18 -0
- package/dist/connectors/raw-query.js +305 -0
- package/dist/connectors/types.d.ts +40 -0
- package/dist/db-adapter/node-sqlite-adapter.d.ts +6 -0
- package/dist/db-adapter/node-sqlite-adapter.js +131 -0
- package/dist/db-manager.d.ts +8 -1
- package/dist/db-manager.js +53 -5
- package/dist/edges/ref-validation.d.ts +8 -0
- package/dist/edges/ref-validation.js +262 -0
- package/dist/edges/store.d.ts +10 -0
- package/dist/edges/store.js +343 -0
- package/dist/edges/types.d.ts +86 -0
- package/dist/edges/types.js +18 -0
- package/dist/embeddings.js +13 -0
- package/dist/index.d.ts +17 -3
- package/dist/index.js +53 -4
- package/dist/mama-api.d.ts +32 -35
- package/dist/mama-api.js +173 -22
- package/dist/memory/api.d.ts +17 -56
- package/dist/memory/api.js +494 -52
- package/dist/memory/event-store.d.ts +3 -0
- package/dist/memory/event-store.js +21 -2
- package/dist/memory/provenance-audit.d.ts +22 -0
- package/dist/memory/provenance-audit.js +112 -0
- package/dist/memory/provenance-query.d.ts +11 -0
- package/dist/memory/provenance-query.js +193 -0
- package/dist/memory/provenance.d.ts +29 -0
- package/dist/memory/provenance.js +136 -0
- package/dist/memory/scope-backfill.d.ts +17 -0
- package/dist/memory/scope-backfill.js +77 -0
- package/dist/memory/types.d.ts +86 -14
- package/dist/model-runs/store.d.ts +13 -0
- package/dist/model-runs/store.js +457 -0
- package/dist/model-runs/tool-trace-store.d.ts +4 -0
- package/dist/model-runs/tool-trace-store.js +114 -0
- package/dist/model-runs/types.d.ts +69 -0
- package/dist/model-runs/types.js +5 -0
- package/dist/runtime/trusted-provenance.d.ts +3 -0
- package/dist/runtime/trusted-provenance.js +6 -0
- package/dist/search/search-quality.d.ts +36 -0
- package/dist/search/search-quality.js +29 -0
- package/package.json +9 -2
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.searchRaw = searchRaw;
|
|
4
|
+
exports.searchAllRaw = searchAllRaw;
|
|
5
|
+
exports.getRawById = getRawById;
|
|
6
|
+
exports.getRawWindow = getRawWindow;
|
|
7
|
+
const types_js_1 = require("../memory/types.js");
|
|
8
|
+
const event_index_js_1 = require("./event-index.js");
|
|
9
|
+
const DEFAULT_RAW_LIMIT = 25;
|
|
10
|
+
const MAX_RAW_LIMIT = 100;
|
|
11
|
+
const DEFAULT_WINDOW_SIZE = 5;
|
|
12
|
+
const MAX_WINDOW_SIZE = 50;
|
|
13
|
+
const VALID_SCOPE_KINDS = new Set(types_js_1.MEMORY_SCOPE_KINDS);
|
|
14
|
+
function placeholders(values) {
|
|
15
|
+
if (values.length === 0) {
|
|
16
|
+
throw new Error('Cannot build SQL placeholders for an empty list.');
|
|
17
|
+
}
|
|
18
|
+
return values.map(() => '?').join(', ');
|
|
19
|
+
}
|
|
20
|
+
function normalizeLimit(value) {
|
|
21
|
+
if (value === undefined) {
|
|
22
|
+
return DEFAULT_RAW_LIMIT;
|
|
23
|
+
}
|
|
24
|
+
return Math.min(MAX_RAW_LIMIT, Math.max(0, Math.floor(value)));
|
|
25
|
+
}
|
|
26
|
+
function normalizeWindowSize(value) {
|
|
27
|
+
if (value === undefined) {
|
|
28
|
+
return DEFAULT_WINDOW_SIZE;
|
|
29
|
+
}
|
|
30
|
+
return Math.min(MAX_WINDOW_SIZE, Math.max(0, Math.floor(value)));
|
|
31
|
+
}
|
|
32
|
+
function escapeFtsQuery(query) {
|
|
33
|
+
return `"${query.replace(/"/g, '""')}"`;
|
|
34
|
+
}
|
|
35
|
+
function encodeCursor(row) {
|
|
36
|
+
return Buffer.from(JSON.stringify({
|
|
37
|
+
rank: row.rank,
|
|
38
|
+
timestampMs: row.source_timestamp_ms,
|
|
39
|
+
rawId: row.event_index_id,
|
|
40
|
+
}), 'utf8').toString('base64url');
|
|
41
|
+
}
|
|
42
|
+
function decodeCursor(cursor) {
|
|
43
|
+
if (!cursor) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
const parsed = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'));
|
|
48
|
+
if (typeof parsed.rank !== 'number' ||
|
|
49
|
+
!Number.isFinite(parsed.rank) ||
|
|
50
|
+
typeof parsed.timestampMs !== 'number' ||
|
|
51
|
+
!Number.isFinite(parsed.timestampMs) ||
|
|
52
|
+
typeof parsed.rawId !== 'string' ||
|
|
53
|
+
parsed.rawId.length === 0) {
|
|
54
|
+
throw new Error('invalid shape');
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
rank: parsed.rank,
|
|
58
|
+
timestampMs: parsed.timestampMs,
|
|
59
|
+
rawId: parsed.rawId,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
64
|
+
throw new Error(`Invalid raw search cursor: ${message}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function normalizeConnectors(connectors) {
|
|
68
|
+
return [...new Set((connectors ?? []).map((connector) => connector.trim()).filter(Boolean))];
|
|
69
|
+
}
|
|
70
|
+
function normalizeScopes(scopes) {
|
|
71
|
+
return (scopes ?? [])
|
|
72
|
+
.map((scope) => {
|
|
73
|
+
const kind = scope.kind.trim();
|
|
74
|
+
if (!VALID_SCOPE_KINDS.has(kind)) {
|
|
75
|
+
throw new Error(`Invalid raw search scope kind: ${kind}`);
|
|
76
|
+
}
|
|
77
|
+
return { kind: kind, id: scope.id.trim() };
|
|
78
|
+
})
|
|
79
|
+
.filter((scope) => scope.id.length > 0);
|
|
80
|
+
}
|
|
81
|
+
function appendFilters(clauses, params, input, alias = 'e') {
|
|
82
|
+
const connectors = normalizeConnectors(input.connectors);
|
|
83
|
+
if (connectors.length > 0) {
|
|
84
|
+
clauses.push(`${alias}.source_connector IN (${placeholders(connectors)})`);
|
|
85
|
+
params.push(...connectors);
|
|
86
|
+
}
|
|
87
|
+
const scopes = normalizeScopes(input.scopes);
|
|
88
|
+
if (scopes.length > 0) {
|
|
89
|
+
clauses.push(`(${scopes.map(() => `(${alias}.memory_scope_kind = ? AND ${alias}.memory_scope_id = ?)`).join(' OR ')})`);
|
|
90
|
+
for (const scope of scopes) {
|
|
91
|
+
params.push(scope.kind, scope.id);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (input.fromMs !== undefined) {
|
|
95
|
+
clauses.push(`${alias}.source_timestamp_ms >= ?`);
|
|
96
|
+
params.push(input.fromMs);
|
|
97
|
+
}
|
|
98
|
+
if (input.toMs !== undefined) {
|
|
99
|
+
clauses.push(`${alias}.source_timestamp_ms <= ?`);
|
|
100
|
+
params.push(input.toMs);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function appendCursorFilter(clauses, params, cursor) {
|
|
104
|
+
if (!cursor) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
clauses.push(`(rank > ? OR (rank = ? AND source_timestamp_ms < ?) OR ` +
|
|
108
|
+
`(rank = ? AND source_timestamp_ms = ? AND event_index_id > ?))`);
|
|
109
|
+
params.push(cursor.rank, cursor.rank, cursor.timestampMs, cursor.rank, cursor.timestampMs, cursor.rawId);
|
|
110
|
+
}
|
|
111
|
+
function parseMetadata(metadataJson) {
|
|
112
|
+
if (!metadataJson) {
|
|
113
|
+
return {};
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
const parsed = JSON.parse(metadataJson);
|
|
117
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
118
|
+
? parsed
|
|
119
|
+
: {};
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return {};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function contentPreview(content) {
|
|
126
|
+
const compact = content.replace(/\s+/g, ' ').trim();
|
|
127
|
+
return compact.length > 240 ? `${compact.slice(0, 237)}...` : compact;
|
|
128
|
+
}
|
|
129
|
+
function scoreFromRank(rank) {
|
|
130
|
+
return Number.isFinite(rank) ? 1 / (1 + Math.exp(rank)) : 0;
|
|
131
|
+
}
|
|
132
|
+
function timestampToIso(timestampMs) {
|
|
133
|
+
if (timestampMs === null || !Number.isFinite(timestampMs)) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
return new Date(timestampMs).toISOString();
|
|
137
|
+
}
|
|
138
|
+
function toRawHit(row) {
|
|
139
|
+
const record = (0, event_index_js_1.mapConnectorEventIndexRecord)(row);
|
|
140
|
+
return {
|
|
141
|
+
raw_id: record.event_index_id,
|
|
142
|
+
connector: record.source_connector,
|
|
143
|
+
source_id: record.source_id,
|
|
144
|
+
channel_id: record.channel,
|
|
145
|
+
author_label: record.author,
|
|
146
|
+
created_at: timestampToIso(record.event_datetime ?? record.source_timestamp_ms),
|
|
147
|
+
content_preview: contentPreview(record.content),
|
|
148
|
+
score: scoreFromRank(Number(row.rank)),
|
|
149
|
+
source_ref: record.source_locator ?? record.artifact_locator,
|
|
150
|
+
metadata: parseMetadata(record.metadata_json),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function runSearch(adapter, input) {
|
|
154
|
+
const query = input.query.trim();
|
|
155
|
+
if (query.length === 0) {
|
|
156
|
+
return { hits: [], next_cursor: null };
|
|
157
|
+
}
|
|
158
|
+
const limit = normalizeLimit(input.limit);
|
|
159
|
+
if (limit === 0) {
|
|
160
|
+
return { hits: [], next_cursor: null };
|
|
161
|
+
}
|
|
162
|
+
const params = [escapeFtsQuery(query)];
|
|
163
|
+
const clauses = [];
|
|
164
|
+
appendFilters(clauses, params, input);
|
|
165
|
+
const cursor = decodeCursor(input.cursor);
|
|
166
|
+
const outerClauses = [];
|
|
167
|
+
const outerParams = [];
|
|
168
|
+
appendCursorFilter(outerClauses, outerParams, cursor);
|
|
169
|
+
const whereSql = clauses.length > 0 ? `AND ${clauses.join(' AND ')}` : '';
|
|
170
|
+
const cursorSql = outerClauses.length > 0 ? `WHERE ${outerClauses.join(' AND ')}` : '';
|
|
171
|
+
const rows = adapter
|
|
172
|
+
.prepare(`
|
|
173
|
+
WITH ranked AS (
|
|
174
|
+
SELECT e.*, bm25(connector_event_index_fts) AS rank
|
|
175
|
+
FROM connector_event_index_fts
|
|
176
|
+
JOIN connector_event_index e
|
|
177
|
+
ON e.event_index_id = connector_event_index_fts.event_index_id
|
|
178
|
+
WHERE connector_event_index_fts MATCH ?
|
|
179
|
+
${whereSql}
|
|
180
|
+
)
|
|
181
|
+
SELECT *
|
|
182
|
+
FROM ranked
|
|
183
|
+
${cursorSql}
|
|
184
|
+
ORDER BY rank ASC, source_timestamp_ms DESC, event_index_id ASC
|
|
185
|
+
LIMIT ?
|
|
186
|
+
`)
|
|
187
|
+
.all(...params, ...outerParams, limit + 1);
|
|
188
|
+
const pageRows = rows.slice(0, limit);
|
|
189
|
+
const nextRow = rows.length > limit ? pageRows[pageRows.length - 1] : undefined;
|
|
190
|
+
return {
|
|
191
|
+
hits: pageRows.map(toRawHit),
|
|
192
|
+
next_cursor: nextRow
|
|
193
|
+
? encodeCursor({
|
|
194
|
+
rank: Number(nextRow.rank),
|
|
195
|
+
source_timestamp_ms: Number(nextRow.source_timestamp_ms),
|
|
196
|
+
event_index_id: String(nextRow.event_index_id),
|
|
197
|
+
})
|
|
198
|
+
: null,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
function searchRaw(adapter, input) {
|
|
202
|
+
const connectors = normalizeConnectors(input.connectors);
|
|
203
|
+
if (connectors.length !== 1) {
|
|
204
|
+
throw new Error('searchRaw requires exactly one connector filter.');
|
|
205
|
+
}
|
|
206
|
+
return runSearch(adapter, { ...input, connectors });
|
|
207
|
+
}
|
|
208
|
+
function searchAllRaw(adapter, input) {
|
|
209
|
+
return runSearch(adapter, input);
|
|
210
|
+
}
|
|
211
|
+
function getRawById(adapter, rawId, visibility) {
|
|
212
|
+
const params = [rawId];
|
|
213
|
+
const clauses = ['e.event_index_id = ?'];
|
|
214
|
+
appendFilters(clauses, params, { query: '*', ...visibility });
|
|
215
|
+
const row = adapter
|
|
216
|
+
.prepare(`
|
|
217
|
+
SELECT e.*, 0 AS rank
|
|
218
|
+
FROM connector_event_index e
|
|
219
|
+
WHERE ${clauses.join(' AND ')}
|
|
220
|
+
LIMIT 1
|
|
221
|
+
`)
|
|
222
|
+
.get(...params);
|
|
223
|
+
return row ? toRawHit(row) : null;
|
|
224
|
+
}
|
|
225
|
+
function getRawWindow(adapter, rawId, input) {
|
|
226
|
+
const targetParams = [rawId];
|
|
227
|
+
const targetClauses = ['e.event_index_id = ?'];
|
|
228
|
+
appendFilters(targetClauses, targetParams, {
|
|
229
|
+
query: '*',
|
|
230
|
+
connectors: input.connectors,
|
|
231
|
+
scopes: input.scopes,
|
|
232
|
+
});
|
|
233
|
+
const targetRow = adapter
|
|
234
|
+
.prepare(`
|
|
235
|
+
SELECT e.*, 0 AS rank
|
|
236
|
+
FROM connector_event_index e
|
|
237
|
+
WHERE ${targetClauses.join(' AND ')}
|
|
238
|
+
LIMIT 1
|
|
239
|
+
`)
|
|
240
|
+
.get(...targetParams);
|
|
241
|
+
if (!targetRow) {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
const target = (0, event_index_js_1.mapConnectorEventIndexRecord)(targetRow);
|
|
245
|
+
const before = normalizeWindowSize(input.before);
|
|
246
|
+
const after = normalizeWindowSize(input.after);
|
|
247
|
+
const beforeRows = beforeWindowRows(adapter, target, input, before);
|
|
248
|
+
const afterRows = afterWindowRows(adapter, target, input, after);
|
|
249
|
+
const targetHit = toRawHit(targetRow);
|
|
250
|
+
return {
|
|
251
|
+
target: targetHit,
|
|
252
|
+
items: [...beforeRows.reverse().map(toRawHit), targetHit, ...afterRows.map(toRawHit)],
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
function beforeWindowRows(adapter, target, input, limit) {
|
|
256
|
+
if (limit === 0) {
|
|
257
|
+
return [];
|
|
258
|
+
}
|
|
259
|
+
const params = [target.source_connector, target.channel, target.source_timestamp_ms];
|
|
260
|
+
const clauses = [
|
|
261
|
+
'e.source_connector = ?',
|
|
262
|
+
target.channel === null ? 'e.channel IS ?' : 'e.channel = ?',
|
|
263
|
+
'e.source_timestamp_ms < ?',
|
|
264
|
+
];
|
|
265
|
+
appendFilters(clauses, params, {
|
|
266
|
+
query: '*',
|
|
267
|
+
connectors: input.connectors,
|
|
268
|
+
scopes: input.scopes,
|
|
269
|
+
});
|
|
270
|
+
return adapter
|
|
271
|
+
.prepare(`
|
|
272
|
+
SELECT e.*, 0 AS rank
|
|
273
|
+
FROM connector_event_index e
|
|
274
|
+
WHERE ${clauses.join(' AND ')}
|
|
275
|
+
ORDER BY e.source_timestamp_ms DESC, e.event_index_id DESC
|
|
276
|
+
LIMIT ?
|
|
277
|
+
`)
|
|
278
|
+
.all(...params, limit);
|
|
279
|
+
}
|
|
280
|
+
function afterWindowRows(adapter, target, input, limit) {
|
|
281
|
+
if (limit === 0) {
|
|
282
|
+
return [];
|
|
283
|
+
}
|
|
284
|
+
const params = [target.source_connector, target.channel, target.source_timestamp_ms];
|
|
285
|
+
const clauses = [
|
|
286
|
+
'e.source_connector = ?',
|
|
287
|
+
target.channel === null ? 'e.channel IS ?' : 'e.channel = ?',
|
|
288
|
+
'e.source_timestamp_ms > ?',
|
|
289
|
+
];
|
|
290
|
+
appendFilters(clauses, params, {
|
|
291
|
+
query: '*',
|
|
292
|
+
connectors: input.connectors,
|
|
293
|
+
scopes: input.scopes,
|
|
294
|
+
});
|
|
295
|
+
return adapter
|
|
296
|
+
.prepare(`
|
|
297
|
+
SELECT e.*, 0 AS rank
|
|
298
|
+
FROM connector_event_index e
|
|
299
|
+
WHERE ${clauses.join(' AND ')}
|
|
300
|
+
ORDER BY e.source_timestamp_ms ASC, e.event_index_id ASC
|
|
301
|
+
LIMIT ?
|
|
302
|
+
`)
|
|
303
|
+
.all(...params, limit);
|
|
304
|
+
}
|
|
305
|
+
//# sourceMappingURL=raw-query.js.map
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Buffer } from 'node:buffer';
|
|
2
|
+
import type { MemoryScopeRef } from '../memory/types.js';
|
|
2
3
|
export interface ConnectorEventIndexRecord {
|
|
3
4
|
event_index_id: string;
|
|
4
5
|
source_connector: string;
|
|
@@ -12,6 +13,11 @@ export interface ConnectorEventIndexRecord {
|
|
|
12
13
|
event_datetime: number | null;
|
|
13
14
|
event_date: string | null;
|
|
14
15
|
source_timestamp_ms: number;
|
|
16
|
+
source_cursor: string | null;
|
|
17
|
+
tenant_id: string | null;
|
|
18
|
+
project_id: string | null;
|
|
19
|
+
memory_scope_kind: string | null;
|
|
20
|
+
memory_scope_id: string | null;
|
|
15
21
|
metadata_json: string | null;
|
|
16
22
|
artifact_locator: string | null;
|
|
17
23
|
artifact_title: string | null;
|
|
@@ -32,6 +38,11 @@ export interface UpsertConnectorEventIndexInput {
|
|
|
32
38
|
event_datetime?: number | null;
|
|
33
39
|
event_date?: string | null;
|
|
34
40
|
source_timestamp_ms?: number | null;
|
|
41
|
+
source_cursor?: string | null;
|
|
42
|
+
tenant_id?: string | null;
|
|
43
|
+
project_id?: string | null;
|
|
44
|
+
memory_scope_kind?: string | null;
|
|
45
|
+
memory_scope_id?: string | null;
|
|
35
46
|
metadata_json?: string | null;
|
|
36
47
|
metadata?: unknown;
|
|
37
48
|
artifact_locator?: string | null;
|
|
@@ -65,5 +76,34 @@ export interface ConnectorEventSearchHit extends ConnectorEventIndexRecord {
|
|
|
65
76
|
rank: number;
|
|
66
77
|
score: number;
|
|
67
78
|
}
|
|
79
|
+
export interface RawSearchScopeFilter {
|
|
80
|
+
kind: MemoryScopeRef['kind'];
|
|
81
|
+
id: string;
|
|
82
|
+
}
|
|
83
|
+
export interface RawSearchInput {
|
|
84
|
+
query: string;
|
|
85
|
+
connectors?: string[];
|
|
86
|
+
scopes?: RawSearchScopeFilter[];
|
|
87
|
+
fromMs?: number;
|
|
88
|
+
toMs?: number;
|
|
89
|
+
cursor?: string;
|
|
90
|
+
limit?: number;
|
|
91
|
+
}
|
|
92
|
+
export interface RawSearchHit {
|
|
93
|
+
raw_id: string;
|
|
94
|
+
connector: string;
|
|
95
|
+
source_id: string;
|
|
96
|
+
channel_id: string | null;
|
|
97
|
+
author_label: string | null;
|
|
98
|
+
created_at: string | null;
|
|
99
|
+
content_preview: string;
|
|
100
|
+
score: number;
|
|
101
|
+
source_ref: string | null;
|
|
102
|
+
metadata: Record<string, unknown>;
|
|
103
|
+
}
|
|
104
|
+
export interface RawSearchResult {
|
|
105
|
+
hits: RawSearchHit[];
|
|
106
|
+
next_cursor: string | null;
|
|
107
|
+
}
|
|
68
108
|
export type ConnectorEventStalenessStatus = 'healthy' | 'stale-but-warming' | 'warn' | 'unhealthy' | 'never_swept';
|
|
69
109
|
//# sourceMappingURL=types.d.ts.map
|
|
@@ -55,6 +55,12 @@ export declare class NodeSQLiteAdapter extends DatabaseAdapter {
|
|
|
55
55
|
insertEmbedding(rowid: number, embedding: Float32Array | number[]): RunResult | null;
|
|
56
56
|
getLastInsertRowid(): number;
|
|
57
57
|
runMigrations(migrationsDir: string): void;
|
|
58
|
+
private recoverMemoryProvenanceMigration032;
|
|
59
|
+
private assertMigration032Complete;
|
|
60
|
+
private recoverConnectorEventScopeMigration034;
|
|
61
|
+
private assertMigration034Complete;
|
|
62
|
+
private tableColumns;
|
|
63
|
+
private indexExists;
|
|
58
64
|
private migrateFromVssMemories;
|
|
59
65
|
}
|
|
60
66
|
export default NodeSQLiteAdapter;
|
|
@@ -18,6 +18,7 @@ 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
|
+
const SQLITE_IDENTIFIER_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
21
22
|
// Prefer better-sqlite3 (includes FTS5) over node:sqlite (lacks FTS5)
|
|
22
23
|
let BetterSQLite3 = null;
|
|
23
24
|
let DatabaseSync = null;
|
|
@@ -320,6 +321,16 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
|
|
|
320
321
|
catch (err) {
|
|
321
322
|
this.exec('ROLLBACK');
|
|
322
323
|
const message = err instanceof Error ? err.message : String(err);
|
|
324
|
+
if (message.includes('duplicate column') && version === 32) {
|
|
325
|
+
this.recoverMemoryProvenanceMigration032();
|
|
326
|
+
(0, debug_logger_js_1.info)(`[node-sqlite-adapter] Migration ${file} recovered successfully`);
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
if (message.includes('duplicate column') && version === 34) {
|
|
330
|
+
this.recoverConnectorEventScopeMigration034();
|
|
331
|
+
(0, debug_logger_js_1.info)(`[node-sqlite-adapter] Migration ${file} recovered successfully`);
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
323
334
|
if (message.includes('duplicate column')) {
|
|
324
335
|
(0, debug_logger_js_1.warn)(`[node-sqlite-adapter] Migration ${file} skipped (duplicate column - already applied)`);
|
|
325
336
|
this.prepare('INSERT OR IGNORE INTO schema_version (version) VALUES (?)').run(version);
|
|
@@ -345,6 +356,126 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
|
|
|
345
356
|
}
|
|
346
357
|
this.migrateFromVssMemories();
|
|
347
358
|
}
|
|
359
|
+
recoverMemoryProvenanceMigration032() {
|
|
360
|
+
this.transaction(() => {
|
|
361
|
+
const expectedDecisionColumns = [
|
|
362
|
+
'agent_id',
|
|
363
|
+
'model_run_id',
|
|
364
|
+
'envelope_hash',
|
|
365
|
+
'gateway_call_id',
|
|
366
|
+
'source_refs_json',
|
|
367
|
+
'provenance_json',
|
|
368
|
+
];
|
|
369
|
+
const decisionColumns = this.tableColumns('decisions');
|
|
370
|
+
for (const column of expectedDecisionColumns) {
|
|
371
|
+
if (!decisionColumns.has(column)) {
|
|
372
|
+
this.exec(`ALTER TABLE decisions ADD COLUMN ${column} TEXT`);
|
|
373
|
+
decisionColumns.add(column);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
this.exec(`
|
|
377
|
+
CREATE INDEX IF NOT EXISTS idx_decisions_envelope_hash
|
|
378
|
+
ON decisions(envelope_hash)
|
|
379
|
+
`);
|
|
380
|
+
this.exec(`
|
|
381
|
+
CREATE INDEX IF NOT EXISTS idx_decisions_model_run_id
|
|
382
|
+
ON decisions(model_run_id)
|
|
383
|
+
`);
|
|
384
|
+
this.exec(`
|
|
385
|
+
CREATE INDEX IF NOT EXISTS idx_decisions_gateway_call_id
|
|
386
|
+
ON decisions(gateway_call_id)
|
|
387
|
+
`);
|
|
388
|
+
this.exec(`
|
|
389
|
+
CREATE INDEX IF NOT EXISTS idx_memory_events_memory_created
|
|
390
|
+
ON memory_events(memory_id, created_at DESC)
|
|
391
|
+
`);
|
|
392
|
+
this.assertMigration032Complete();
|
|
393
|
+
this.prepare('INSERT OR IGNORE INTO schema_version (version, description) VALUES (?, ?)').run(32, 'Add nullable memory provenance columns');
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
assertMigration032Complete() {
|
|
397
|
+
const decisionColumns = this.tableColumns('decisions');
|
|
398
|
+
for (const column of [
|
|
399
|
+
'agent_id',
|
|
400
|
+
'model_run_id',
|
|
401
|
+
'envelope_hash',
|
|
402
|
+
'gateway_call_id',
|
|
403
|
+
'source_refs_json',
|
|
404
|
+
'provenance_json',
|
|
405
|
+
]) {
|
|
406
|
+
if (!decisionColumns.has(column)) {
|
|
407
|
+
throw new Error(`Migration 032 recovery failed: missing decisions.${column}`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
for (const indexName of [
|
|
411
|
+
'idx_decisions_envelope_hash',
|
|
412
|
+
'idx_decisions_model_run_id',
|
|
413
|
+
'idx_decisions_gateway_call_id',
|
|
414
|
+
'idx_memory_events_memory_created',
|
|
415
|
+
]) {
|
|
416
|
+
if (!this.indexExists(indexName)) {
|
|
417
|
+
throw new Error(`Migration 032 recovery failed: missing index ${indexName}`);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
recoverConnectorEventScopeMigration034() {
|
|
422
|
+
this.transaction(() => {
|
|
423
|
+
const expectedColumns = [
|
|
424
|
+
'source_cursor',
|
|
425
|
+
'tenant_id',
|
|
426
|
+
'project_id',
|
|
427
|
+
'memory_scope_kind',
|
|
428
|
+
'memory_scope_id',
|
|
429
|
+
];
|
|
430
|
+
const columns = this.tableColumns('connector_event_index');
|
|
431
|
+
for (const column of expectedColumns) {
|
|
432
|
+
if (!columns.has(column)) {
|
|
433
|
+
this.exec(`ALTER TABLE connector_event_index ADD COLUMN ${column} TEXT`);
|
|
434
|
+
columns.add(column);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
this.exec(`
|
|
438
|
+
CREATE INDEX IF NOT EXISTS idx_connector_event_scope
|
|
439
|
+
ON connector_event_index(tenant_id, project_id, memory_scope_kind, memory_scope_id)
|
|
440
|
+
`);
|
|
441
|
+
this.exec(`
|
|
442
|
+
CREATE INDEX IF NOT EXISTS idx_connector_event_source_cursor
|
|
443
|
+
ON connector_event_index(source_connector, source_cursor)
|
|
444
|
+
`);
|
|
445
|
+
this.assertMigration034Complete();
|
|
446
|
+
this.prepare('INSERT OR IGNORE INTO schema_version (version, description) VALUES (?, ?)').run(34, 'Add connector event scope columns');
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
assertMigration034Complete() {
|
|
450
|
+
const columns = this.tableColumns('connector_event_index');
|
|
451
|
+
for (const column of [
|
|
452
|
+
'source_cursor',
|
|
453
|
+
'tenant_id',
|
|
454
|
+
'project_id',
|
|
455
|
+
'memory_scope_kind',
|
|
456
|
+
'memory_scope_id',
|
|
457
|
+
]) {
|
|
458
|
+
if (!columns.has(column)) {
|
|
459
|
+
throw new Error(`Migration 034 recovery failed: missing connector_event_index.${column}`);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
for (const indexName of ['idx_connector_event_scope', 'idx_connector_event_source_cursor']) {
|
|
463
|
+
if (!this.indexExists(indexName)) {
|
|
464
|
+
throw new Error(`Migration 034 recovery failed: missing index ${indexName}`);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
tableColumns(tableName) {
|
|
469
|
+
if (!SQLITE_IDENTIFIER_PATTERN.test(tableName)) {
|
|
470
|
+
throw new Error('Invalid SQLite table identifier');
|
|
471
|
+
}
|
|
472
|
+
const rows = this.prepare(`PRAGMA table_info(${tableName})`).all();
|
|
473
|
+
return new Set(rows.map((row) => row.name));
|
|
474
|
+
}
|
|
475
|
+
indexExists(indexName) {
|
|
476
|
+
const row = this.prepare("SELECT name FROM sqlite_master WHERE type='index' AND name = ?").get(indexName);
|
|
477
|
+
return row?.name === indexName;
|
|
478
|
+
}
|
|
348
479
|
migrateFromVssMemories() {
|
|
349
480
|
try {
|
|
350
481
|
const vssTables = this.prepare(`SELECT name FROM sqlite_master WHERE name='vss_memories'`).all();
|
package/dist/db-manager.d.ts
CHANGED
|
@@ -121,6 +121,7 @@ export interface SemanticEdges {
|
|
|
121
121
|
* @returns SQLite database connection
|
|
122
122
|
*/
|
|
123
123
|
export declare function initDB(): Promise<unknown>;
|
|
124
|
+
export declare function assertTestProcessIsNotUsingRealDb(effectivePath?: string, effectivePathSource?: string): void;
|
|
124
125
|
/**
|
|
125
126
|
* Get database connection (singleton pattern)
|
|
126
127
|
*
|
|
@@ -206,6 +207,12 @@ export interface DecisionInput {
|
|
|
206
207
|
event_date?: string | null;
|
|
207
208
|
/** Source event timestamp in milliseconds when known. */
|
|
208
209
|
event_datetime?: number | null;
|
|
210
|
+
agent_id?: string | null;
|
|
211
|
+
model_run_id?: string | null;
|
|
212
|
+
envelope_hash?: string | null;
|
|
213
|
+
gateway_call_id?: string | null;
|
|
214
|
+
source_refs_json?: string | null;
|
|
215
|
+
provenance_json?: string | null;
|
|
209
216
|
}
|
|
210
217
|
/**
|
|
211
218
|
* Insert decision with embedding
|
|
@@ -284,7 +291,7 @@ export declare function resetDBState(options?: {
|
|
|
284
291
|
/**
|
|
285
292
|
* Check if running in test mode
|
|
286
293
|
*
|
|
287
|
-
* Returns true if MAMA_TEST_MODE or
|
|
294
|
+
* Returns true if MAMA_TEST_MODE, VITEST, or NODE_ENV=test env vars are set
|
|
288
295
|
*/
|
|
289
296
|
export declare function isTestMode(): boolean;
|
|
290
297
|
/**
|
package/dist/db-manager.js
CHANGED
|
@@ -23,6 +23,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
23
23
|
};
|
|
24
24
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
25
|
exports.initDB = initDB;
|
|
26
|
+
exports.assertTestProcessIsNotUsingRealDb = assertTestProcessIsNotUsingRealDb;
|
|
26
27
|
exports.getDB = getDB;
|
|
27
28
|
exports.getAdapter = getAdapter;
|
|
28
29
|
exports.closeDB = closeDB;
|
|
@@ -43,6 +44,7 @@ exports.isTestMode = isTestMode;
|
|
|
43
44
|
exports.fts5Search = fts5Search;
|
|
44
45
|
exports.reindexEmbeddings = reindexEmbeddings;
|
|
45
46
|
const path_1 = __importDefault(require("path"));
|
|
47
|
+
const os_1 = __importDefault(require("os"));
|
|
46
48
|
const debug_logger_js_1 = require("./debug-logger.js");
|
|
47
49
|
const progress_indicator_js_1 = require("./progress-indicator.js");
|
|
48
50
|
const index_js_1 = require("./db-adapter/index.js");
|
|
@@ -53,6 +55,7 @@ let isInitialized = false;
|
|
|
53
55
|
let initializingPromise = null; // Single-flight guard for concurrent callers
|
|
54
56
|
// Migration directory (moved to src/db/migrations for M1.2)
|
|
55
57
|
const MIGRATIONS_DIR = path_1.default.join(__dirname, '..', 'db', 'migrations');
|
|
58
|
+
const REAL_USER_DB_PATH = path_1.default.join(os_1.default.homedir(), '.claude', 'mama-memory.db');
|
|
56
59
|
/**
|
|
57
60
|
* Initialize SQLite database adapter and connect
|
|
58
61
|
*
|
|
@@ -65,6 +68,7 @@ const MIGRATIONS_DIR = path_1.default.join(__dirname, '..', 'db', 'migrations');
|
|
|
65
68
|
* @returns SQLite database connection
|
|
66
69
|
*/
|
|
67
70
|
async function initDB() {
|
|
71
|
+
assertTestProcessIsNotUsingRealDb();
|
|
68
72
|
// Already initialized - return immediately
|
|
69
73
|
if (isInitialized) {
|
|
70
74
|
return dbConnection;
|
|
@@ -79,6 +83,7 @@ async function initDB() {
|
|
|
79
83
|
(0, progress_indicator_js_1.logSearching)('Initializing database...');
|
|
80
84
|
// Create SQLite adapter
|
|
81
85
|
dbAdapter = (0, index_js_1.createAdapter)();
|
|
86
|
+
assertTestProcessIsNotUsingRealDb(resolveAdapterDbPath(dbAdapter), 'adapter.getDbPath()');
|
|
82
87
|
// Connect to database
|
|
83
88
|
dbConnection = await dbAdapter.connect();
|
|
84
89
|
// Run migrations (includes 012-create-checkpoints-table.sql)
|
|
@@ -104,6 +109,48 @@ async function initDB() {
|
|
|
104
109
|
})();
|
|
105
110
|
return initializingPromise;
|
|
106
111
|
}
|
|
112
|
+
function assertTestProcessIsNotUsingRealDb(effectivePath, effectivePathSource = 'adapter') {
|
|
113
|
+
if (!isDatabaseBoundaryTestMode()) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const configuredPaths = [
|
|
117
|
+
{ name: 'MAMA_DB_PATH', value: process.env.MAMA_DB_PATH },
|
|
118
|
+
{ name: 'MAMA_DATABASE_PATH', value: process.env.MAMA_DATABASE_PATH },
|
|
119
|
+
];
|
|
120
|
+
if (effectivePath) {
|
|
121
|
+
configuredPaths.push({ name: effectivePathSource, value: effectivePath });
|
|
122
|
+
}
|
|
123
|
+
for (const configuredPath of configuredPaths) {
|
|
124
|
+
if (!configuredPath.value) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
const resolvedPath = path_1.default.resolve(expandHomePath(configuredPath.value));
|
|
128
|
+
if (resolvedPath === path_1.default.resolve(REAL_USER_DB_PATH)) {
|
|
129
|
+
throw new Error(`[db-boundary] Refusing to initDB against real DB ${REAL_USER_DB_PATH} ` +
|
|
130
|
+
`from a test process (${configuredPath.name}=${configuredPath.value}). ` +
|
|
131
|
+
'Set MAMA_DB_PATH to a temporary path before initDB.');
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function resolveAdapterDbPath(adapter) {
|
|
136
|
+
if (typeof adapter.getDbPath === 'function') {
|
|
137
|
+
return adapter.getDbPath();
|
|
138
|
+
}
|
|
139
|
+
return adapter.dbPath;
|
|
140
|
+
}
|
|
141
|
+
function isDatabaseBoundaryTestMode() {
|
|
142
|
+
return isTestMode();
|
|
143
|
+
}
|
|
144
|
+
function expandHomePath(value) {
|
|
145
|
+
const home = os_1.default.homedir();
|
|
146
|
+
if (value === '~') {
|
|
147
|
+
return home;
|
|
148
|
+
}
|
|
149
|
+
if (value.startsWith('~/')) {
|
|
150
|
+
return path_1.default.join(home, value.slice(2));
|
|
151
|
+
}
|
|
152
|
+
return value.replaceAll('${HOME}', home);
|
|
153
|
+
}
|
|
107
154
|
/**
|
|
108
155
|
* Get database connection (singleton pattern)
|
|
109
156
|
*
|
|
@@ -306,14 +353,15 @@ async function insertDecisionWithEmbedding(decision) {
|
|
|
306
353
|
confidence, created_at, updated_at,
|
|
307
354
|
needs_validation, validation_attempts, last_validated_at, usage_count,
|
|
308
355
|
trust_context, usage_success, usage_failure, time_saved,
|
|
309
|
-
evidence, alternatives, risks, event_date, event_datetime
|
|
310
|
-
|
|
356
|
+
evidence, alternatives, risks, event_date, event_datetime,
|
|
357
|
+
agent_id, model_run_id, envelope_hash, gateway_call_id, source_refs_json, provenance_json
|
|
358
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
311
359
|
`);
|
|
312
360
|
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,
|
|
313
361
|
// IMPORTANT: All timestamps are stored in milliseconds (Date.now()).
|
|
314
362
|
// The schema DEFAULT uses unixepoch() (seconds) but is never used
|
|
315
363
|
// since all inserts go through this function which always provides ms.
|
|
316
|
-
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, decision.event_datetime ?? null);
|
|
364
|
+
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, decision.event_datetime ?? null, decision.agent_id ?? null, decision.model_run_id ?? null, decision.envelope_hash ?? null, decision.gateway_call_id ?? null, decision.source_refs_json ?? null, decision.provenance_json ?? null);
|
|
317
365
|
const rowid = Number(insertResult.lastInsertRowid);
|
|
318
366
|
// Insert embedding in same transaction to ensure rowid matching
|
|
319
367
|
(0, debug_logger_js_1.info)(`[db-manager] Vector search enabled: ${adapter.vectorSearchEnabled}`);
|
|
@@ -675,10 +723,10 @@ function resetDBState(options = {}) {
|
|
|
675
723
|
/**
|
|
676
724
|
* Check if running in test mode
|
|
677
725
|
*
|
|
678
|
-
* Returns true if MAMA_TEST_MODE or
|
|
726
|
+
* Returns true if MAMA_TEST_MODE, VITEST, or NODE_ENV=test env vars are set
|
|
679
727
|
*/
|
|
680
728
|
function isTestMode() {
|
|
681
|
-
return
|
|
729
|
+
return Boolean(process.env.MAMA_TEST_MODE || process.env.VITEST || process.env.NODE_ENV === 'test');
|
|
682
730
|
}
|
|
683
731
|
/**
|
|
684
732
|
* FTS5 keyword search on decisions table.
|