@hasna/mementos 0.14.80 → 0.14.81
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/dist/cli/index.js +16 -14
- package/dist/db/projects.d.ts +4 -4
- package/dist/db/projects.d.ts.map +1 -1
- package/dist/index.js +250 -19
- package/dist/project-registration/authority.d.ts +5 -1
- package/dist/project-registration/authority.d.ts.map +1 -1
- package/dist/project-registration/http.d.ts +4 -1
- package/dist/project-registration/http.d.ts.map +1 -1
- package/dist/project-registration/index.d.ts +1 -1
- package/dist/project-registration/index.d.ts.map +1 -1
- package/dist/project-registration/types.d.ts +73 -0
- package/dist/project-registration/types.d.ts.map +1 -1
- package/dist/project-registration.js +3261 -62
- package/dist/server/index.js +226 -19
- package/package.json +2 -2
|
@@ -46,6 +46,1138 @@ var __export = (target, all) => {
|
|
|
46
46
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
47
47
|
var __require = import.meta.require;
|
|
48
48
|
|
|
49
|
+
// src/generated/storage-kit/mode.ts
|
|
50
|
+
function normalizeStorageMode(value) {
|
|
51
|
+
const normalized = value.trim().toLowerCase().replace(/-/g, "_");
|
|
52
|
+
if (normalized === "local")
|
|
53
|
+
return { mode: "local", deprecatedAlias: null };
|
|
54
|
+
if (normalized === "cloud")
|
|
55
|
+
return { mode: "cloud", deprecatedAlias: null };
|
|
56
|
+
if (DEPRECATED_STORAGE_MODE_ALIASES.includes(normalized)) {
|
|
57
|
+
return { mode: "cloud", deprecatedAlias: normalized };
|
|
58
|
+
}
|
|
59
|
+
throw new Error(`Unknown storage mode: ${value}. Use local or cloud.`);
|
|
60
|
+
}
|
|
61
|
+
var DEPRECATED_STORAGE_MODE_ALIASES;
|
|
62
|
+
var init_mode = __esm(() => {
|
|
63
|
+
DEPRECATED_STORAGE_MODE_ALIASES = [
|
|
64
|
+
"remote",
|
|
65
|
+
"hybrid",
|
|
66
|
+
"self_hosted"
|
|
67
|
+
];
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// src/storage.ts
|
|
71
|
+
import { Database } from "bun:sqlite";
|
|
72
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
73
|
+
import { homedir } from "os";
|
|
74
|
+
import { join } from "path";
|
|
75
|
+
import { fileURLToPath } from "url";
|
|
76
|
+
import { Worker } from "worker_threads";
|
|
77
|
+
import pg from "pg";
|
|
78
|
+
function markServerContext() {
|
|
79
|
+
_serverContext = true;
|
|
80
|
+
}
|
|
81
|
+
function resetServerContextForTests() {
|
|
82
|
+
if (process.env["NODE_ENV"] !== "test") {
|
|
83
|
+
throw new Error("resetServerContextForTests is only available under NODE_ENV=test");
|
|
84
|
+
}
|
|
85
|
+
_serverContext = false;
|
|
86
|
+
}
|
|
87
|
+
function isServerContext() {
|
|
88
|
+
return _serverContext;
|
|
89
|
+
}
|
|
90
|
+
function normalizeParams(params) {
|
|
91
|
+
const flat = params.length === 1 && Array.isArray(params[0]) ? params[0] : params;
|
|
92
|
+
return flat.map((value) => value === undefined ? null : value);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
class SqliteAdapter {
|
|
96
|
+
db;
|
|
97
|
+
constructor(path) {
|
|
98
|
+
this.db = new Database(path, { create: true });
|
|
99
|
+
this.db.exec("PRAGMA journal_mode = WAL");
|
|
100
|
+
this.db.exec("PRAGMA foreign_keys = ON");
|
|
101
|
+
}
|
|
102
|
+
run(sql, ...params) {
|
|
103
|
+
const result = this.db.prepare(sql).run(...normalizeParams(params));
|
|
104
|
+
return {
|
|
105
|
+
changes: result.changes,
|
|
106
|
+
lastInsertRowid: result.lastInsertRowid
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
get(sql, ...params) {
|
|
110
|
+
return this.db.prepare(sql).get(...normalizeParams(params));
|
|
111
|
+
}
|
|
112
|
+
all(sql, ...params) {
|
|
113
|
+
return this.db.prepare(sql).all(...normalizeParams(params));
|
|
114
|
+
}
|
|
115
|
+
exec(sql) {
|
|
116
|
+
this.db.exec(sql);
|
|
117
|
+
}
|
|
118
|
+
query(sql) {
|
|
119
|
+
return this.db.query(sql);
|
|
120
|
+
}
|
|
121
|
+
prepare(sql) {
|
|
122
|
+
const statement = this.db.prepare(sql);
|
|
123
|
+
return {
|
|
124
|
+
run: (...params) => {
|
|
125
|
+
const result = statement.run(...normalizeParams(params));
|
|
126
|
+
return {
|
|
127
|
+
changes: result.changes,
|
|
128
|
+
lastInsertRowid: result.lastInsertRowid
|
|
129
|
+
};
|
|
130
|
+
},
|
|
131
|
+
get: (...params) => statement.get(...normalizeParams(params)),
|
|
132
|
+
all: (...params) => statement.all(...normalizeParams(params)),
|
|
133
|
+
finalize: () => {
|
|
134
|
+
statement.finalize();
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
close() {
|
|
139
|
+
this.db.close();
|
|
140
|
+
}
|
|
141
|
+
transaction(fn) {
|
|
142
|
+
return this.db.transaction(fn)();
|
|
143
|
+
}
|
|
144
|
+
get raw() {
|
|
145
|
+
return this.db;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function translateSql(sql) {
|
|
149
|
+
let parameterIndex = 0;
|
|
150
|
+
let translated = sql.replace(/\?/g, () => `$${++parameterIndex}`);
|
|
151
|
+
const ISO_FMT = `'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'`;
|
|
152
|
+
translated = translated.replace(/datetime\s*\(\s*'now'\s*\)/gi, `to_char(now() AT TIME ZONE 'UTC', ${ISO_FMT})`);
|
|
153
|
+
translated = translated.replace(/datetime\s*\(\s*'now'\s*,\s*'(-?\d+)\s+(minutes?|hours?|days?|seconds?)'\s*\)/gi, (_match, amount, unit) => {
|
|
154
|
+
const parsed = parseInt(String(amount), 10);
|
|
155
|
+
const absolute = Math.abs(parsed);
|
|
156
|
+
const normalizedUnit = String(unit).toLowerCase().replace(/s$/, "");
|
|
157
|
+
const pluralUnit = absolute === 1 ? normalizedUnit : `${normalizedUnit}s`;
|
|
158
|
+
const op = parsed < 0 ? "-" : "+";
|
|
159
|
+
return `to_char((now() ${op} INTERVAL '${absolute} ${pluralUnit}') AT TIME ZONE 'UTC', ${ISO_FMT})`;
|
|
160
|
+
});
|
|
161
|
+
translated = translated.replace(/COALESCE\s*\(\s*accessed_at\s*,\s*(created_at|updated_at)\s*\)/gi, (_match, col) => `COALESCE(accessed_at, to_char(${col} AT TIME ZONE 'UTC', ${ISO_FMT}))`);
|
|
162
|
+
translated = translated.replace(/lower\s*\(\s*hex\s*\(\s*randomblob\s*\(\s*\d+\s*\)\s*\)\s*\)/gi, "gen_random_uuid()::text");
|
|
163
|
+
translated = translated.replace(/\bIFNULL\s*\(/gi, "COALESCE(");
|
|
164
|
+
translated = translated.replace(/\bINSTR\s*\(/gi, "STRPOS(");
|
|
165
|
+
if (/INSERT\s+OR\s+IGNORE\s+INTO/i.test(translated)) {
|
|
166
|
+
translated = translated.replace(/INSERT\s+OR\s+IGNORE\s+INTO/gi, "INSERT INTO");
|
|
167
|
+
translated = translated.replace(/;?\s*$/, " ON CONFLICT DO NOTHING");
|
|
168
|
+
}
|
|
169
|
+
translated = translated.replace(/INSERT\s+OR\s+REPLACE\s+INTO/gi, "INSERT INTO");
|
|
170
|
+
translated = translated.replace(/COALESCE\s*\(\s*pinned\s*,\s*0\s*\)/gi, "COALESCE(pinned, FALSE)");
|
|
171
|
+
const BOOLEAN_COLUMNS = [
|
|
172
|
+
"pinned",
|
|
173
|
+
"success",
|
|
174
|
+
"is_primary",
|
|
175
|
+
"blocking",
|
|
176
|
+
"enabled",
|
|
177
|
+
"useful",
|
|
178
|
+
"dry_run",
|
|
179
|
+
"applied"
|
|
180
|
+
];
|
|
181
|
+
for (const col of BOOLEAN_COLUMNS) {
|
|
182
|
+
translated = translated.replace(new RegExp(`\\b${col}\\s*=\\s*1\\b`, "gi"), `${col} = TRUE`);
|
|
183
|
+
translated = translated.replace(new RegExp(`\\b${col}\\s*=\\s*0\\b`, "gi"), `${col} = FALSE`);
|
|
184
|
+
}
|
|
185
|
+
return translated;
|
|
186
|
+
}
|
|
187
|
+
function shouldUsePgSsl(connectionString) {
|
|
188
|
+
let params;
|
|
189
|
+
try {
|
|
190
|
+
params = new URL(connectionString).searchParams;
|
|
191
|
+
} catch {
|
|
192
|
+
params = new URLSearchParams(connectionString.split("?", 2)[1] ?? "");
|
|
193
|
+
}
|
|
194
|
+
const ssl = params.get("ssl")?.trim().toLowerCase();
|
|
195
|
+
const sslMode = params.get("sslmode")?.trim().toLowerCase();
|
|
196
|
+
return ["1", "true", "yes", "on", "require"].includes(ssl ?? "") || ["require", "verify-ca", "verify-full"].includes(sslMode ?? "");
|
|
197
|
+
}
|
|
198
|
+
function sslConfigFor(connectionString) {
|
|
199
|
+
if (!shouldUsePgSsl(connectionString))
|
|
200
|
+
return;
|
|
201
|
+
let sslMode;
|
|
202
|
+
try {
|
|
203
|
+
sslMode = new URL(connectionString).searchParams.get("sslmode")?.trim().toLowerCase() ?? undefined;
|
|
204
|
+
} catch {
|
|
205
|
+
sslMode = new URLSearchParams(connectionString.split("?", 2)[1] ?? "").get("sslmode")?.trim().toLowerCase() ?? undefined;
|
|
206
|
+
}
|
|
207
|
+
if (sslMode === "verify-ca" || sslMode === "verify-full") {
|
|
208
|
+
return { rejectUnauthorized: true };
|
|
209
|
+
}
|
|
210
|
+
return { rejectUnauthorized: false };
|
|
211
|
+
}
|
|
212
|
+
function stripSslParams(connectionString) {
|
|
213
|
+
try {
|
|
214
|
+
const url = new URL(connectionString);
|
|
215
|
+
url.searchParams.delete("ssl");
|
|
216
|
+
url.searchParams.delete("sslmode");
|
|
217
|
+
return url.toString();
|
|
218
|
+
} catch {
|
|
219
|
+
return connectionString;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function makePool(connectionString) {
|
|
223
|
+
return new pg.Pool({
|
|
224
|
+
connectionString: stripSslParams(connectionString),
|
|
225
|
+
ssl: sslConfigFor(connectionString)
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
class PgAdapter {
|
|
230
|
+
pool;
|
|
231
|
+
constructor(input) {
|
|
232
|
+
this.pool = typeof input === "string" ? new PgSyncPool(input) : input;
|
|
233
|
+
}
|
|
234
|
+
run(sql, ...params) {
|
|
235
|
+
const result = this.pool.query(translateSql(sql), normalizeParams(params));
|
|
236
|
+
return {
|
|
237
|
+
changes: result.rowCount ?? 0,
|
|
238
|
+
lastInsertRowid: result.rows?.[0]?.id ?? 0
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
get(sql, ...params) {
|
|
242
|
+
const result = this.pool.query(translateSql(sql), normalizeParams(params));
|
|
243
|
+
return result.rows[0] ?? null;
|
|
244
|
+
}
|
|
245
|
+
all(sql, ...params) {
|
|
246
|
+
return this.pool.query(translateSql(sql), normalizeParams(params)).rows;
|
|
247
|
+
}
|
|
248
|
+
exec(sql) {
|
|
249
|
+
this.pool.query(sql, []);
|
|
250
|
+
}
|
|
251
|
+
prepare(sql) {
|
|
252
|
+
return {
|
|
253
|
+
run: (...params) => this.run(sql, ...params),
|
|
254
|
+
get: (...params) => this.get(sql, ...params),
|
|
255
|
+
all: (...params) => this.all(sql, ...params),
|
|
256
|
+
finalize: () => {}
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
query(sql) {
|
|
260
|
+
return this.prepare(sql);
|
|
261
|
+
}
|
|
262
|
+
close() {
|
|
263
|
+
this.pool.end();
|
|
264
|
+
}
|
|
265
|
+
transaction(fn) {
|
|
266
|
+
this.pool.query("BEGIN", []);
|
|
267
|
+
try {
|
|
268
|
+
const value = fn();
|
|
269
|
+
this.pool.query("COMMIT", []);
|
|
270
|
+
return value;
|
|
271
|
+
} catch (error) {
|
|
272
|
+
try {
|
|
273
|
+
this.pool.query("ROLLBACK", []);
|
|
274
|
+
} catch {}
|
|
275
|
+
throw error;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
get raw() {
|
|
279
|
+
return this.pool;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
class PgAdapterAsync {
|
|
284
|
+
pool;
|
|
285
|
+
constructor(input) {
|
|
286
|
+
this.pool = typeof input === "string" ? makePool(input) : input;
|
|
287
|
+
}
|
|
288
|
+
async run(sql, ...params) {
|
|
289
|
+
const result = await this.pool.query(translateSql(sql), normalizeParams(params));
|
|
290
|
+
return {
|
|
291
|
+
changes: result.rowCount ?? 0,
|
|
292
|
+
lastInsertRowid: result.rows?.[0]?.id ?? 0
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
async get(sql, ...params) {
|
|
296
|
+
const result = await this.pool.query(translateSql(sql), normalizeParams(params));
|
|
297
|
+
return result.rows[0] ?? null;
|
|
298
|
+
}
|
|
299
|
+
async all(sql, ...params) {
|
|
300
|
+
const result = await this.pool.query(translateSql(sql), normalizeParams(params));
|
|
301
|
+
return result.rows;
|
|
302
|
+
}
|
|
303
|
+
async exec(sql) {
|
|
304
|
+
await this.pool.query(sql);
|
|
305
|
+
}
|
|
306
|
+
async close() {
|
|
307
|
+
await this.pool.end();
|
|
308
|
+
}
|
|
309
|
+
async transaction(fn) {
|
|
310
|
+
const client = await this.pool.connect();
|
|
311
|
+
try {
|
|
312
|
+
await client.query("BEGIN");
|
|
313
|
+
const value = await fn(client);
|
|
314
|
+
await client.query("COMMIT");
|
|
315
|
+
return value;
|
|
316
|
+
} catch (error) {
|
|
317
|
+
await client.query("ROLLBACK");
|
|
318
|
+
throw error;
|
|
319
|
+
} finally {
|
|
320
|
+
client.release();
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
get raw() {
|
|
324
|
+
return this.pool;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
function readEnv(name) {
|
|
328
|
+
const value = process.env[name]?.trim();
|
|
329
|
+
return value ? value : null;
|
|
330
|
+
}
|
|
331
|
+
function warnDeprecatedStorageMode(alias) {
|
|
332
|
+
if (warnedDeprecatedModes.has(alias))
|
|
333
|
+
return;
|
|
334
|
+
warnedDeprecatedModes.add(alias);
|
|
335
|
+
process.emitWarning(`${MEMENTOS_STORAGE_ENV.mode}="${alias}" is deprecated; use "cloud". ` + `"${alias}" now maps to pure-remote cloud storage. The local<->remote ` + `sync path is deprecated and is not the fleet cutover path.`, { type: "DeprecationWarning", code: "MEMENTOS_STORAGE_MODE_ALIAS" });
|
|
336
|
+
}
|
|
337
|
+
function normalizeStorageMode2(value, source) {
|
|
338
|
+
if (!value || !value.trim())
|
|
339
|
+
return null;
|
|
340
|
+
let normalized;
|
|
341
|
+
try {
|
|
342
|
+
normalized = normalizeStorageMode(value);
|
|
343
|
+
} catch (error) {
|
|
344
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
345
|
+
throw new Error(`mementos: ${source}=${value} is not a valid mode. ${detail}`);
|
|
346
|
+
}
|
|
347
|
+
if (normalized.deprecatedAlias) {
|
|
348
|
+
warnDeprecatedStorageMode(normalized.deprecatedAlias);
|
|
349
|
+
}
|
|
350
|
+
return normalized.mode;
|
|
351
|
+
}
|
|
352
|
+
function readConfigFile() {
|
|
353
|
+
if (!existsSync(STORAGE_CONFIG_PATH)) {
|
|
354
|
+
return {};
|
|
355
|
+
}
|
|
356
|
+
try {
|
|
357
|
+
return JSON.parse(readFileSync(STORAGE_CONFIG_PATH, "utf-8"));
|
|
358
|
+
} catch {
|
|
359
|
+
return {};
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
function getConfigDir() {
|
|
363
|
+
return STORAGE_CONFIG_DIR;
|
|
364
|
+
}
|
|
365
|
+
function getConfigPath() {
|
|
366
|
+
return STORAGE_CONFIG_PATH;
|
|
367
|
+
}
|
|
368
|
+
function getStorageDatabaseEnv() {
|
|
369
|
+
for (const env of DATABASE_ENV_NAMES) {
|
|
370
|
+
if (readEnv(env.name))
|
|
371
|
+
return env;
|
|
372
|
+
}
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
function getStorageEnvName(key) {
|
|
376
|
+
const canonical = MEMENTOS_STORAGE_ENV[key];
|
|
377
|
+
const fallback = MEMENTOS_STORAGE_FALLBACK_ENV[key];
|
|
378
|
+
return readEnv(canonical) || !readEnv(fallback) ? canonical : fallback;
|
|
379
|
+
}
|
|
380
|
+
function getStorageDatabaseUrl() {
|
|
381
|
+
const env = getStorageDatabaseEnv();
|
|
382
|
+
return env ? readEnv(env.name) : null;
|
|
383
|
+
}
|
|
384
|
+
function getStorageDatabaseEnvName() {
|
|
385
|
+
return getStorageEnvName("databaseUrl");
|
|
386
|
+
}
|
|
387
|
+
function getStorageModeOverride() {
|
|
388
|
+
for (const env of MODE_ENV_NAMES) {
|
|
389
|
+
const value = normalizeStorageMode2(readEnv(env.name) ?? undefined, env.name);
|
|
390
|
+
if (value)
|
|
391
|
+
return value;
|
|
392
|
+
}
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
function getStorageConfig() {
|
|
396
|
+
const fileConfig = readConfigFile();
|
|
397
|
+
const modeOverride = getStorageModeOverride();
|
|
398
|
+
const envConnectionString = getConfiguredConnectionString();
|
|
399
|
+
const fileMode = normalizeStorageMode2(fileConfig.mode, `${STORAGE_CONFIG_PATH} "mode"`);
|
|
400
|
+
const merged = {
|
|
401
|
+
...DEFAULT_STORAGE_CONFIG,
|
|
402
|
+
...fileConfig,
|
|
403
|
+
rds: {
|
|
404
|
+
...DEFAULT_STORAGE_CONFIG.rds,
|
|
405
|
+
...fileConfig.rds ?? {}
|
|
406
|
+
},
|
|
407
|
+
sync: {
|
|
408
|
+
...DEFAULT_STORAGE_CONFIG.sync,
|
|
409
|
+
...fileConfig.sync ?? {}
|
|
410
|
+
},
|
|
411
|
+
mode: fileMode ?? DEFAULT_STORAGE_CONFIG.mode
|
|
412
|
+
};
|
|
413
|
+
if (modeOverride) {
|
|
414
|
+
merged.mode = modeOverride;
|
|
415
|
+
} else if (envConnectionString && merged.mode === "local") {
|
|
416
|
+
merged.mode = "cloud";
|
|
417
|
+
}
|
|
418
|
+
return merged;
|
|
419
|
+
}
|
|
420
|
+
function getStorageMode() {
|
|
421
|
+
return getStorageConfig().mode;
|
|
422
|
+
}
|
|
423
|
+
function isSecretQueryParam(key) {
|
|
424
|
+
const normalized = key.trim().toLowerCase();
|
|
425
|
+
return SECRET_QUERY_PARAMS.has(normalized) || /(?:secret|token|password|passphrase|credential|api[_-]?key|apikey|private[_-]?key|auth|session)/i.test(normalized);
|
|
426
|
+
}
|
|
427
|
+
function redactDatabaseUrl(value) {
|
|
428
|
+
if (!value) {
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
try {
|
|
432
|
+
const url = new URL(value);
|
|
433
|
+
if (url.password) {
|
|
434
|
+
url.password = "***";
|
|
435
|
+
}
|
|
436
|
+
for (const key of Array.from(url.searchParams.keys())) {
|
|
437
|
+
if (isSecretQueryParam(key)) {
|
|
438
|
+
url.searchParams.set(key, "***");
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return url.toString();
|
|
442
|
+
} catch {
|
|
443
|
+
return value.replace(/:[^:@/\s]+@/, ":***@").replace(/([?&\s][^=&\s]*(?:secret|token|password|passphrase|credential|api[_-]?key|apikey|private[_-]?key|auth|session)[^=&\s]*=)[^&\s]+/gi, "$1***");
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
function validatePostgresConnectionString(value) {
|
|
447
|
+
const redactedUrl = redactDatabaseUrl(value);
|
|
448
|
+
if (!value) {
|
|
449
|
+
return {
|
|
450
|
+
ok: false,
|
|
451
|
+
redacted_url: redactedUrl,
|
|
452
|
+
issues: ["Missing PostgreSQL/RDS connection string."]
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
let url;
|
|
456
|
+
try {
|
|
457
|
+
url = new URL(value);
|
|
458
|
+
} catch {
|
|
459
|
+
return {
|
|
460
|
+
ok: false,
|
|
461
|
+
redacted_url: redactedUrl,
|
|
462
|
+
issues: ["PostgreSQL/RDS connection string must be a valid postgres:// or postgresql:// URL."]
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
if (url.protocol !== "postgres:" && url.protocol !== "postgresql:") {
|
|
466
|
+
return {
|
|
467
|
+
ok: false,
|
|
468
|
+
redacted_url: redactedUrl,
|
|
469
|
+
issues: ["PostgreSQL/RDS connection string must use postgres:// or postgresql://."]
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
if (!url.hostname) {
|
|
473
|
+
return {
|
|
474
|
+
ok: false,
|
|
475
|
+
redacted_url: redactedUrl,
|
|
476
|
+
issues: ["PostgreSQL/RDS connection string must include a host."]
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
return {
|
|
480
|
+
ok: true,
|
|
481
|
+
redacted_url: redactedUrl,
|
|
482
|
+
issues: []
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
function storageEnvStatus(key) {
|
|
486
|
+
const activeName = getStorageEnvName(key);
|
|
487
|
+
return {
|
|
488
|
+
name: MEMENTOS_STORAGE_ENV[key],
|
|
489
|
+
active_name: activeName,
|
|
490
|
+
configured: readEnv(activeName) !== null
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
function remoteDatabaseConfigStatus(config) {
|
|
494
|
+
const env = getStorageDatabaseEnv();
|
|
495
|
+
const envUrl = env ? readEnv(env.name) : null;
|
|
496
|
+
if (env && envUrl) {
|
|
497
|
+
const validation = validatePostgresConnectionString(envUrl);
|
|
498
|
+
return {
|
|
499
|
+
configured: validation.ok,
|
|
500
|
+
source: "env",
|
|
501
|
+
env_name: env.name,
|
|
502
|
+
redacted_url: validation.redacted_url,
|
|
503
|
+
missing: [],
|
|
504
|
+
issues: validation.issues,
|
|
505
|
+
rds_compatible: validation.ok
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
const missing = [];
|
|
509
|
+
if (!config.rds.host) {
|
|
510
|
+
missing.push("storage.rds.host");
|
|
511
|
+
}
|
|
512
|
+
if (!config.rds.username) {
|
|
513
|
+
missing.push("storage.rds.username");
|
|
514
|
+
}
|
|
515
|
+
if (!readEnv(config.rds.password_env)) {
|
|
516
|
+
missing.push(config.rds.password_env);
|
|
517
|
+
}
|
|
518
|
+
const configured = missing.length === 0;
|
|
519
|
+
const redactedUrl = configured ? `postgres://${config.rds.username}:***@${config.rds.host}:${config.rds.port}/mementos${config.rds.ssl ? "?sslmode=require" : ""}` : null;
|
|
520
|
+
const issues = missing.length === 0 ? [] : [`Missing ${missing.join(", ")}.`];
|
|
521
|
+
return {
|
|
522
|
+
configured,
|
|
523
|
+
source: config.rds.host || config.rds.username ? "config-file" : "none",
|
|
524
|
+
env_name: null,
|
|
525
|
+
redacted_url: redactedUrl,
|
|
526
|
+
missing,
|
|
527
|
+
issues,
|
|
528
|
+
rds_compatible: configured
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
function runtimeKindFor(mode) {
|
|
532
|
+
return mode === "cloud" ? "cloud-postgres" : "local-sqlite";
|
|
533
|
+
}
|
|
534
|
+
function getSafeStorageConfigSummary(config = getStorageConfig()) {
|
|
535
|
+
return {
|
|
536
|
+
mode: config.mode,
|
|
537
|
+
auto_sync_interval_minutes: config.auto_sync_interval_minutes,
|
|
538
|
+
feedback_endpoint_configured: config.feedback_endpoint.trim() !== "",
|
|
539
|
+
sync: { ...config.sync },
|
|
540
|
+
rds: {
|
|
541
|
+
host_configured: config.rds.host.trim() !== "",
|
|
542
|
+
port: config.rds.port,
|
|
543
|
+
username_configured: config.rds.username.trim() !== "",
|
|
544
|
+
password_env: config.rds.password_env,
|
|
545
|
+
password_configured: readEnv(config.rds.password_env) !== null,
|
|
546
|
+
ssl: config.rds.ssl
|
|
547
|
+
}
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
function getStorageStatus() {
|
|
551
|
+
const config = getStorageConfig();
|
|
552
|
+
const mode = config.mode;
|
|
553
|
+
const remoteRequested = mode === "cloud";
|
|
554
|
+
const remote = remoteDatabaseConfigStatus(config);
|
|
555
|
+
const issues = [];
|
|
556
|
+
const warnings = [];
|
|
557
|
+
if (remoteRequested && !remote.configured) {
|
|
558
|
+
issues.push(`Cloud PostgreSQL/RDS storage is requested but not configured. ${remote.issues.join(" ")}`);
|
|
559
|
+
}
|
|
560
|
+
if (mode === "local" && remote.issues.length > 0 && remote.source !== "none") {
|
|
561
|
+
warnings.push(`Cloud PostgreSQL/RDS configuration is present but invalid; cloud runtime stays disabled until fixed. ${remote.issues.join(" ")}`);
|
|
562
|
+
}
|
|
563
|
+
if (mode === "local" && remote.configured) {
|
|
564
|
+
warnings.push("Cloud PostgreSQL/RDS configuration is present, but storage mode is local; cloud runtime stays disabled until mode is cloud.");
|
|
565
|
+
}
|
|
566
|
+
const failClosed = remoteRequested && !remote.configured;
|
|
567
|
+
const runtime = {
|
|
568
|
+
contract: "mementos-cloud-runtime-v1",
|
|
569
|
+
kind: runtimeKindFor(mode),
|
|
570
|
+
fail_closed: failClosed,
|
|
571
|
+
local: {
|
|
572
|
+
adapter: "sqlite",
|
|
573
|
+
primary_runtime: mode === "local",
|
|
574
|
+
data_dir: LOCAL_DATA_DIR,
|
|
575
|
+
config_path: STORAGE_CONFIG_PATH,
|
|
576
|
+
local_file_sync: {
|
|
577
|
+
supported: false,
|
|
578
|
+
reason: "Mementos stores local state in SQLite; it does not sync raw local data files."
|
|
579
|
+
}
|
|
580
|
+
},
|
|
581
|
+
remote: {
|
|
582
|
+
adapter: "postgres",
|
|
583
|
+
purpose: "primary-runtime",
|
|
584
|
+
requested: remoteRequested,
|
|
585
|
+
configured: remote.configured,
|
|
586
|
+
source: remote.source,
|
|
587
|
+
env_name: remote.env_name,
|
|
588
|
+
redacted_url: remote.redacted_url,
|
|
589
|
+
rds_compatible: remote.rds_compatible,
|
|
590
|
+
fail_closed: failClosed,
|
|
591
|
+
missing: remote.missing
|
|
592
|
+
},
|
|
593
|
+
object_storage: {
|
|
594
|
+
s3: {
|
|
595
|
+
supported: false,
|
|
596
|
+
mutation_allowed: false,
|
|
597
|
+
reason: "No S3 object-storage adapter is part of this runtime."
|
|
598
|
+
},
|
|
599
|
+
aws: {
|
|
600
|
+
mutation_allowed: false,
|
|
601
|
+
reason: "Diagnostics do not store sensitive values, change AWS resources, deploy, or mutate production data."
|
|
602
|
+
}
|
|
603
|
+
},
|
|
604
|
+
migrations: {
|
|
605
|
+
target: "postgres-rds-compatible",
|
|
606
|
+
command: "mementos storage migrate",
|
|
607
|
+
dry_run_command: "mementos storage migrate --dry-run",
|
|
608
|
+
configured: remote.configured,
|
|
609
|
+
mutates_remote_on_apply: true,
|
|
610
|
+
requires_approval_for_live_run: true
|
|
611
|
+
}
|
|
612
|
+
};
|
|
613
|
+
return {
|
|
614
|
+
ok: issues.length === 0,
|
|
615
|
+
service: "mementos",
|
|
616
|
+
mode,
|
|
617
|
+
local_default: mode === "local",
|
|
618
|
+
remote_enabled: remoteRequested,
|
|
619
|
+
runtime,
|
|
620
|
+
database: {
|
|
621
|
+
configured: remote.configured,
|
|
622
|
+
redacted_url: remote.redacted_url,
|
|
623
|
+
source: remote.source,
|
|
624
|
+
env_name: remote.env_name,
|
|
625
|
+
rds_compatible: remote.rds_compatible
|
|
626
|
+
},
|
|
627
|
+
tables: MEMENTOS_STORAGE_TABLES,
|
|
628
|
+
env: {
|
|
629
|
+
databaseUrl: storageEnvStatus("databaseUrl"),
|
|
630
|
+
mode: storageEnvStatus("mode")
|
|
631
|
+
},
|
|
632
|
+
issues,
|
|
633
|
+
warnings,
|
|
634
|
+
no_network: true
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
function saveStorageConfig(config) {
|
|
638
|
+
mkdirSync(STORAGE_CONFIG_DIR, { recursive: true });
|
|
639
|
+
writeFileSync(STORAGE_CONFIG_PATH, `${JSON.stringify(config, null, 2)}
|
|
640
|
+
`, "utf-8");
|
|
641
|
+
}
|
|
642
|
+
function getConfiguredConnectionString() {
|
|
643
|
+
return getStorageDatabaseUrl() ?? undefined;
|
|
644
|
+
}
|
|
645
|
+
function getStorageConnectionString(dbName = "mementos") {
|
|
646
|
+
if (!isServerContext()) {
|
|
647
|
+
throw new Error("Refusing to construct an RDS Postgres DSN outside the mementos-serve server. " + "The raw database DSN is NEVER distributed to client machines. " + "Clients must use the self-hosted HTTP API: set HASNA_MEMENTOS_API_URL and " + "HASNA_MEMENTOS_API_KEY (and unset HASNA_MEMENTOS_DATABASE_URL / HASNA_MEMENTOS_STORAGE_MODE).");
|
|
648
|
+
}
|
|
649
|
+
const envConnectionString = getConfiguredConnectionString();
|
|
650
|
+
if (envConnectionString) {
|
|
651
|
+
const validation = validatePostgresConnectionString(envConnectionString);
|
|
652
|
+
if (!validation.ok) {
|
|
653
|
+
throw new Error(`Remote storage database is not configured. ${validation.issues.join(" ")}`);
|
|
654
|
+
}
|
|
655
|
+
return envConnectionString;
|
|
656
|
+
}
|
|
657
|
+
const config = getStorageConfig();
|
|
658
|
+
const { host, port, username, password_env, ssl } = config.rds;
|
|
659
|
+
const missing = [];
|
|
660
|
+
if (!host) {
|
|
661
|
+
missing.push("storage.rds.host");
|
|
662
|
+
}
|
|
663
|
+
if (!username) {
|
|
664
|
+
missing.push("storage.rds.username");
|
|
665
|
+
}
|
|
666
|
+
if (missing.length > 0) {
|
|
667
|
+
throw new Error(`Remote storage database is not configured. Missing ${missing.join(", ")}. Set HASNA_MEMENTOS_DATABASE_URL or configure ~/.hasna/mementos/storage/config.json.`);
|
|
668
|
+
}
|
|
669
|
+
const password = process.env[password_env];
|
|
670
|
+
if (!password) {
|
|
671
|
+
throw new Error(`Remote storage database password is not set. Export ${password_env}.`);
|
|
672
|
+
}
|
|
673
|
+
const sslParam = ssl ? "?sslmode=require" : "";
|
|
674
|
+
return `postgres://${username}:${encodeURIComponent(password)}@${host}:${port}/${dbName}${sslParam}`;
|
|
675
|
+
}
|
|
676
|
+
function isSyncExcludedTable(table) {
|
|
677
|
+
return SYNC_EXCLUDED_TABLE_PATTERNS.some((pattern) => pattern.test(table));
|
|
678
|
+
}
|
|
679
|
+
function listSqliteTables(db) {
|
|
680
|
+
const rows = db.all("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name");
|
|
681
|
+
return rows.map((row) => row.name);
|
|
682
|
+
}
|
|
683
|
+
function ensureSyncMetaTable(db) {
|
|
684
|
+
db.exec(SYNC_META_TABLE_SQL);
|
|
685
|
+
}
|
|
686
|
+
function getSyncMeta(db, table) {
|
|
687
|
+
ensureSyncMetaTable(db);
|
|
688
|
+
return db.get("SELECT table_name, last_synced_at, last_synced_row_count, direction FROM _sync_meta WHERE table_name = ?", table);
|
|
689
|
+
}
|
|
690
|
+
function upsertSyncMeta(db, meta) {
|
|
691
|
+
ensureSyncMetaTable(db);
|
|
692
|
+
const existing = db.get("SELECT table_name FROM _sync_meta WHERE table_name = ?", meta.table_name);
|
|
693
|
+
if (existing) {
|
|
694
|
+
db.run("UPDATE _sync_meta SET last_synced_at = ?, last_synced_row_count = ?, direction = ? WHERE table_name = ?", meta.last_synced_at, meta.last_synced_row_count, meta.direction, meta.table_name);
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
db.run("INSERT INTO _sync_meta (table_name, last_synced_at, last_synced_row_count, direction) VALUES (?, ?, ?, ?)", meta.table_name, meta.last_synced_at, meta.last_synced_row_count, meta.direction);
|
|
698
|
+
}
|
|
699
|
+
function transferRows(target, table, rows, options) {
|
|
700
|
+
const primaryKey = options.primaryKey ?? "id";
|
|
701
|
+
const conflictColumn = options.conflictColumn ?? "updated_at";
|
|
702
|
+
let written = 0;
|
|
703
|
+
let skipped = 0;
|
|
704
|
+
const errors = [];
|
|
705
|
+
if (rows.length === 0) {
|
|
706
|
+
return { written, skipped, errors };
|
|
707
|
+
}
|
|
708
|
+
const columns = Object.keys(rows[0] ?? {});
|
|
709
|
+
if (!columns.includes(primaryKey)) {
|
|
710
|
+
return {
|
|
711
|
+
written,
|
|
712
|
+
skipped,
|
|
713
|
+
errors: [`Table "${table}" has no "${primaryKey}" column; skipping`]
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
const hasConflictColumn = columns.includes(conflictColumn);
|
|
717
|
+
for (const row of rows) {
|
|
718
|
+
try {
|
|
719
|
+
const existing = target.get(`SELECT "${primaryKey}"${hasConflictColumn ? `, "${conflictColumn}"` : ""} FROM "${table}" WHERE "${primaryKey}" = ?`, row[primaryKey]);
|
|
720
|
+
if (existing) {
|
|
721
|
+
if (hasConflictColumn && existing[conflictColumn] && row[conflictColumn]) {
|
|
722
|
+
const existingTime = Date.parse(String(existing[conflictColumn]));
|
|
723
|
+
const incomingTime = Date.parse(String(row[conflictColumn]));
|
|
724
|
+
if (Number.isFinite(existingTime) && Number.isFinite(incomingTime) && existingTime >= incomingTime) {
|
|
725
|
+
skipped++;
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
const updateColumns = columns.filter((column) => column !== primaryKey);
|
|
730
|
+
const setClauses = updateColumns.map((column) => `"${column}" = ?`).join(", ");
|
|
731
|
+
target.run(`UPDATE "${table}" SET ${setClauses} WHERE "${primaryKey}" = ?`, ...updateColumns.map((column) => row[column]), row[primaryKey]);
|
|
732
|
+
} else {
|
|
733
|
+
const placeholders = columns.map(() => "?").join(", ");
|
|
734
|
+
const columnList = columns.map((column) => `"${column}"`).join(", ");
|
|
735
|
+
target.run(`INSERT INTO "${table}" (${columnList}) VALUES (${placeholders})`, ...columns.map((column) => row[column]));
|
|
736
|
+
}
|
|
737
|
+
written++;
|
|
738
|
+
} catch (error) {
|
|
739
|
+
errors.push(`Row ${String(row[primaryKey] ?? "unknown")}: ${error instanceof Error ? error.message : String(error)}`);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
return { written, skipped, errors };
|
|
743
|
+
}
|
|
744
|
+
function incrementalSyncPush(local, remote, tables, options = {}) {
|
|
745
|
+
return runIncrementalSync("push", local, remote, local, tables, options);
|
|
746
|
+
}
|
|
747
|
+
function incrementalSyncPull(remote, local, tables, options = {}) {
|
|
748
|
+
return runIncrementalSync("pull", remote, local, local, tables, options);
|
|
749
|
+
}
|
|
750
|
+
function runIncrementalSync(direction, source, target, metaDb, tables, options) {
|
|
751
|
+
const conflictColumn = options.conflictColumn ?? "updated_at";
|
|
752
|
+
const batchSize = options.batchSize ?? 500;
|
|
753
|
+
const results = [];
|
|
754
|
+
ensureSyncMetaTable(metaDb);
|
|
755
|
+
for (const table of tables) {
|
|
756
|
+
const stat = {
|
|
757
|
+
table,
|
|
758
|
+
total_rows: 0,
|
|
759
|
+
synced_rows: 0,
|
|
760
|
+
skipped_rows: 0,
|
|
761
|
+
errors: [],
|
|
762
|
+
first_sync: false
|
|
763
|
+
};
|
|
764
|
+
try {
|
|
765
|
+
const countResult = source.get(`SELECT COUNT(*) as cnt FROM "${table}"`);
|
|
766
|
+
stat.total_rows = countResult?.cnt ?? 0;
|
|
767
|
+
const meta = getSyncMeta(metaDb, table);
|
|
768
|
+
let rows;
|
|
769
|
+
if (meta?.last_synced_at) {
|
|
770
|
+
try {
|
|
771
|
+
rows = source.all(`SELECT * FROM "${table}" WHERE "${conflictColumn}" > ?`, meta.last_synced_at);
|
|
772
|
+
} catch {
|
|
773
|
+
rows = source.all(`SELECT * FROM "${table}"`);
|
|
774
|
+
stat.first_sync = true;
|
|
775
|
+
}
|
|
776
|
+
} else {
|
|
777
|
+
rows = source.all(`SELECT * FROM "${table}"`);
|
|
778
|
+
stat.first_sync = true;
|
|
779
|
+
}
|
|
780
|
+
for (let offset = 0;offset < rows.length; offset += batchSize) {
|
|
781
|
+
const batch = rows.slice(offset, offset + batchSize);
|
|
782
|
+
const result = transferRows(target, table, batch, options);
|
|
783
|
+
stat.synced_rows += result.written;
|
|
784
|
+
stat.skipped_rows += result.skipped;
|
|
785
|
+
stat.errors.push(...result.errors);
|
|
786
|
+
}
|
|
787
|
+
if (rows.length === 0) {
|
|
788
|
+
stat.skipped_rows = stat.total_rows;
|
|
789
|
+
}
|
|
790
|
+
upsertSyncMeta(metaDb, {
|
|
791
|
+
table_name: table,
|
|
792
|
+
last_synced_at: new Date().toISOString(),
|
|
793
|
+
last_synced_row_count: stat.synced_rows,
|
|
794
|
+
direction
|
|
795
|
+
});
|
|
796
|
+
} catch (error) {
|
|
797
|
+
stat.errors.push(`Table "${table}": ${error instanceof Error ? error.message : String(error)}`);
|
|
798
|
+
}
|
|
799
|
+
results.push(stat);
|
|
800
|
+
}
|
|
801
|
+
return results;
|
|
802
|
+
}
|
|
803
|
+
function getSyncMetaAll(db) {
|
|
804
|
+
ensureSyncMetaTable(db);
|
|
805
|
+
return db.all("SELECT table_name, last_synced_at, last_synced_row_count, direction FROM _sync_meta ORDER BY table_name");
|
|
806
|
+
}
|
|
807
|
+
function getSyncMetaForTable(db, table) {
|
|
808
|
+
return getSyncMeta(db, table);
|
|
809
|
+
}
|
|
810
|
+
function resetSyncMeta(db, table) {
|
|
811
|
+
ensureSyncMetaTable(db);
|
|
812
|
+
db.run("DELETE FROM _sync_meta WHERE table_name = ?", table);
|
|
813
|
+
}
|
|
814
|
+
function resetAllSyncMeta(db) {
|
|
815
|
+
ensureSyncMetaTable(db);
|
|
816
|
+
db.run("DELETE FROM _sync_meta");
|
|
817
|
+
}
|
|
818
|
+
var _serverContext = false, PgSyncPool, MEMENTOS_STORAGE_TABLES, STORAGE_TABLES, MEMENTOS_STORAGE_ENV, MEMENTOS_STORAGE_FALLBACK_ENV, LOCAL_DATA_DIR, DEFAULT_STORAGE_CONFIG, STORAGE_CONFIG_DIR, STORAGE_CONFIG_PATH, DATABASE_ENV_NAMES, MODE_ENV_NAMES, warnedDeprecatedModes, SECRET_QUERY_PARAMS, getMementosStorageStatus, SYNC_EXCLUDED_TABLE_PATTERNS, SYNC_META_TABLE_SQL = `
|
|
819
|
+
CREATE TABLE IF NOT EXISTS _sync_meta (
|
|
820
|
+
table_name TEXT PRIMARY KEY,
|
|
821
|
+
last_synced_at TEXT,
|
|
822
|
+
last_synced_row_count INTEGER DEFAULT 0,
|
|
823
|
+
direction TEXT DEFAULT 'push'
|
|
824
|
+
)`;
|
|
825
|
+
var init_storage = __esm(() => {
|
|
826
|
+
init_mode();
|
|
827
|
+
PgSyncPool = class PgSyncPool {
|
|
828
|
+
worker;
|
|
829
|
+
status;
|
|
830
|
+
data;
|
|
831
|
+
closed = false;
|
|
832
|
+
lastError = null;
|
|
833
|
+
static DATA_BYTES = 128 * 1024 * 1024;
|
|
834
|
+
static QUERY_TIMEOUT_MS = 60000;
|
|
835
|
+
static resolveWorkerPath() {
|
|
836
|
+
const ext = import.meta.url.endsWith(".ts") ? ".ts" : ".js";
|
|
837
|
+
const here = fileURLToPath(new URL(".", import.meta.url));
|
|
838
|
+
const candidates = [
|
|
839
|
+
join(here, `pg-sync-worker${ext}`),
|
|
840
|
+
join(here, "..", `pg-sync-worker${ext}`),
|
|
841
|
+
join(here, "..", "..", `pg-sync-worker${ext}`)
|
|
842
|
+
];
|
|
843
|
+
for (const candidate of candidates) {
|
|
844
|
+
if (existsSync(candidate))
|
|
845
|
+
return candidate;
|
|
846
|
+
}
|
|
847
|
+
return candidates[0];
|
|
848
|
+
}
|
|
849
|
+
constructor(connectionString) {
|
|
850
|
+
const control = new SharedArrayBuffer(8);
|
|
851
|
+
const dataSab = new SharedArrayBuffer(PgSyncPool.DATA_BYTES);
|
|
852
|
+
this.status = new Int32Array(control);
|
|
853
|
+
this.data = new Uint8Array(dataSab);
|
|
854
|
+
this.worker = new Worker(PgSyncPool.resolveWorkerPath(), {
|
|
855
|
+
workerData: {
|
|
856
|
+
dsn: stripSslParams(connectionString),
|
|
857
|
+
ssl: sslConfigFor(connectionString),
|
|
858
|
+
control,
|
|
859
|
+
data: dataSab
|
|
860
|
+
}
|
|
861
|
+
});
|
|
862
|
+
this.worker.unref();
|
|
863
|
+
this.worker.on("error", (err) => {
|
|
864
|
+
this.lastError = err;
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
query(sql, params) {
|
|
868
|
+
if (this.closed)
|
|
869
|
+
throw new Error("PgSyncPool is closed");
|
|
870
|
+
if (this.lastError)
|
|
871
|
+
throw this.lastError;
|
|
872
|
+
Atomics.store(this.status, 0, 0);
|
|
873
|
+
this.worker.postMessage({ sql, params });
|
|
874
|
+
const waitResult = Atomics.wait(this.status, 0, 0, PgSyncPool.QUERY_TIMEOUT_MS);
|
|
875
|
+
const code = Atomics.load(this.status, 0);
|
|
876
|
+
if (code === 0 || waitResult === "timed-out") {
|
|
877
|
+
if (this.lastError)
|
|
878
|
+
throw this.lastError;
|
|
879
|
+
throw new Error("PostgreSQL query timed out after 60s");
|
|
880
|
+
}
|
|
881
|
+
const len = Atomics.load(this.status, 1);
|
|
882
|
+
const payload = JSON.parse(new TextDecoder().decode(this.data.subarray(0, len)));
|
|
883
|
+
if (code === 2) {
|
|
884
|
+
throw new Error(payload.message ?? "PostgreSQL error");
|
|
885
|
+
}
|
|
886
|
+
return payload;
|
|
887
|
+
}
|
|
888
|
+
end() {
|
|
889
|
+
if (this.closed)
|
|
890
|
+
return;
|
|
891
|
+
this.closed = true;
|
|
892
|
+
this.worker.terminate();
|
|
893
|
+
}
|
|
894
|
+
};
|
|
895
|
+
MEMENTOS_STORAGE_TABLES = [
|
|
896
|
+
"projects",
|
|
897
|
+
"agents",
|
|
898
|
+
"machines",
|
|
899
|
+
"sessions",
|
|
900
|
+
"entities",
|
|
901
|
+
"memories",
|
|
902
|
+
"relations",
|
|
903
|
+
"entity_memories",
|
|
904
|
+
"memory_tags",
|
|
905
|
+
"memory_versions",
|
|
906
|
+
"memory_embeddings",
|
|
907
|
+
"tool_events",
|
|
908
|
+
"resource_locks",
|
|
909
|
+
"memory_ratings"
|
|
910
|
+
];
|
|
911
|
+
STORAGE_TABLES = MEMENTOS_STORAGE_TABLES;
|
|
912
|
+
MEMENTOS_STORAGE_ENV = {
|
|
913
|
+
databaseUrl: "HASNA_MEMENTOS_DATABASE_URL",
|
|
914
|
+
mode: "HASNA_MEMENTOS_STORAGE_MODE"
|
|
915
|
+
};
|
|
916
|
+
MEMENTOS_STORAGE_FALLBACK_ENV = {
|
|
917
|
+
databaseUrl: "MEMENTOS_DATABASE_URL",
|
|
918
|
+
mode: "MEMENTOS_STORAGE_MODE"
|
|
919
|
+
};
|
|
920
|
+
LOCAL_DATA_DIR = join(homedir(), ".hasna", "mementos");
|
|
921
|
+
DEFAULT_STORAGE_CONFIG = {
|
|
922
|
+
rds: {
|
|
923
|
+
host: "",
|
|
924
|
+
port: 5432,
|
|
925
|
+
username: "",
|
|
926
|
+
password_env: "MEMENTOS_DATABASE_PASSWORD",
|
|
927
|
+
ssl: true
|
|
928
|
+
},
|
|
929
|
+
mode: "local",
|
|
930
|
+
auto_sync_interval_minutes: 0,
|
|
931
|
+
feedback_endpoint: "",
|
|
932
|
+
sync: {
|
|
933
|
+
schedule_minutes: 0
|
|
934
|
+
}
|
|
935
|
+
};
|
|
936
|
+
STORAGE_CONFIG_DIR = join(LOCAL_DATA_DIR, "storage");
|
|
937
|
+
STORAGE_CONFIG_PATH = join(STORAGE_CONFIG_DIR, "config.json");
|
|
938
|
+
DATABASE_ENV_NAMES = [
|
|
939
|
+
{ name: MEMENTOS_STORAGE_ENV.databaseUrl, deprecated: false },
|
|
940
|
+
{ name: MEMENTOS_STORAGE_FALLBACK_ENV.databaseUrl, deprecated: false }
|
|
941
|
+
];
|
|
942
|
+
MODE_ENV_NAMES = [
|
|
943
|
+
{ name: MEMENTOS_STORAGE_ENV.mode, deprecated: false },
|
|
944
|
+
{ name: MEMENTOS_STORAGE_FALLBACK_ENV.mode, deprecated: false }
|
|
945
|
+
];
|
|
946
|
+
warnedDeprecatedModes = new Set;
|
|
947
|
+
SECRET_QUERY_PARAMS = new Set([
|
|
948
|
+
"password",
|
|
949
|
+
"pass",
|
|
950
|
+
"pwd",
|
|
951
|
+
"token",
|
|
952
|
+
"secret",
|
|
953
|
+
"api_key",
|
|
954
|
+
"apikey"
|
|
955
|
+
]);
|
|
956
|
+
getMementosStorageStatus = getStorageStatus;
|
|
957
|
+
SYNC_EXCLUDED_TABLE_PATTERNS = [
|
|
958
|
+
/^sqlite_/,
|
|
959
|
+
/_fts$/,
|
|
960
|
+
/_fts_/,
|
|
961
|
+
/^_sync_/,
|
|
962
|
+
/^_pg_migrations$/
|
|
963
|
+
];
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
// src/db/api-mode.ts
|
|
967
|
+
import { tmpdir } from "os";
|
|
968
|
+
import { join as join3 } from "path";
|
|
969
|
+
import { writeFileSync as writeFileSync2, unlinkSync } from "fs";
|
|
970
|
+
import { randomUUID } from "crypto";
|
|
971
|
+
function firstEnv(keys) {
|
|
972
|
+
for (const k of keys) {
|
|
973
|
+
const v = process.env[k]?.trim();
|
|
974
|
+
if (v)
|
|
975
|
+
return v;
|
|
976
|
+
}
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
function firstEnvKey(keys) {
|
|
980
|
+
for (const k of keys) {
|
|
981
|
+
if (process.env[k]?.trim())
|
|
982
|
+
return k;
|
|
983
|
+
}
|
|
984
|
+
return null;
|
|
985
|
+
}
|
|
986
|
+
function hasDatabaseUrl() {
|
|
987
|
+
return Boolean(firstEnv(DATABASE_URL_ENV_KEYS));
|
|
988
|
+
}
|
|
989
|
+
function isLoopbackHost(rawHost) {
|
|
990
|
+
const host = rawHost.replace(/^\[/, "").replace(/\]$/, "").toLowerCase();
|
|
991
|
+
return host === "localhost" || host === "::1" || /^127\./.test(host);
|
|
992
|
+
}
|
|
993
|
+
function assertRequestAllowedUnderTest(baseUrl) {
|
|
994
|
+
if (process.env["NODE_ENV"] !== "test")
|
|
995
|
+
return;
|
|
996
|
+
if (process.env[ALLOW_REMOTE_API_IN_TESTS_ENV]?.trim())
|
|
997
|
+
return;
|
|
998
|
+
let host;
|
|
999
|
+
try {
|
|
1000
|
+
host = new URL(baseUrl).hostname;
|
|
1001
|
+
} catch {
|
|
1002
|
+
host = "";
|
|
1003
|
+
}
|
|
1004
|
+
if (host && isLoopbackHost(host))
|
|
1005
|
+
return;
|
|
1006
|
+
throw new Error("api-mode: REFUSING to make a cloud request from a test process \u2014 this would write to or read " + "from the SHARED PRODUCTION memory store, where test fixtures are indistinguishable from real " + `memories.
|
|
1007
|
+
` + ` host : ${host || "(unparseable base URL)"}
|
|
1008
|
+
` + ` how this happens: a selector set at module scope (after the bun test preload ran), or \`bun test\` ` + `invoked from a directory with no bunfig.toml so the preload never loaded.
|
|
1009
|
+
` + " fix : build the child/process env via src/test-support/store-isolation.ts, or point the " + `suite at a loopback stub.
|
|
1010
|
+
` + ` override : set ${ALLOW_REMOTE_API_IN_TESTS_ENV}=1 only for a test that must reach a remote endpoint.`);
|
|
1011
|
+
}
|
|
1012
|
+
function normalizeBase(raw) {
|
|
1013
|
+
let base = raw.trim().replace(/\/+$/, "");
|
|
1014
|
+
if (/\/(v1|api)$/.test(base))
|
|
1015
|
+
return base;
|
|
1016
|
+
return `${base}/v1`;
|
|
1017
|
+
}
|
|
1018
|
+
function assertUnambiguousStoreEnv() {
|
|
1019
|
+
if (firstEnvKey(DB_PATH_ENV_KEYS))
|
|
1020
|
+
return;
|
|
1021
|
+
if (hasDatabaseUrl())
|
|
1022
|
+
return;
|
|
1023
|
+
const urlKey = firstEnvKey(API_URL_ENV_KEYS);
|
|
1024
|
+
const keyKey = firstEnvKey(API_KEY_ENV_KEYS);
|
|
1025
|
+
if (urlKey && !keyKey) {
|
|
1026
|
+
throw new MementosStoreConfigError(`${urlKey} points at the cloud memory store but ${API_KEY_ENV_KEYS[0]} is not set. ` + `Refusing to serve the on-box SQLite store in its place, because it holds a different ` + `dataset. Set ${API_KEY_ENV_KEYS[0]} to reach the cloud store. If you meant to use the ` + `on-box SQLite store, unset ${urlKey} or set ${DB_PATH_ENV_KEYS[0]} explicitly.`);
|
|
1027
|
+
}
|
|
1028
|
+
if (keyKey && !urlKey) {
|
|
1029
|
+
throw new MementosStoreConfigError(`${keyKey} is set but ${API_URL_ENV_KEYS[0]} is not, so the cloud memory store cannot be ` + `reached. Refusing to serve the on-box SQLite store in its place, because it holds a ` + `different dataset. Set ${API_URL_ENV_KEYS[0]} to reach the cloud store. If you meant to ` + `use the on-box SQLite store, unset ${keyKey} or set ${DB_PATH_ENV_KEYS[0]} explicitly.`);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
function getApiConfig() {
|
|
1033
|
+
assertUnambiguousStoreEnv();
|
|
1034
|
+
if (firstEnvKey(DB_PATH_ENV_KEYS))
|
|
1035
|
+
return null;
|
|
1036
|
+
const rawBase = firstEnv(API_URL_ENV_KEYS);
|
|
1037
|
+
const apiKey = firstEnv(API_KEY_ENV_KEYS);
|
|
1038
|
+
if (!rawBase || !apiKey)
|
|
1039
|
+
return null;
|
|
1040
|
+
return { baseUrl: normalizeBase(rawBase), apiKey };
|
|
1041
|
+
}
|
|
1042
|
+
function isApiMode() {
|
|
1043
|
+
if (hasDatabaseUrl())
|
|
1044
|
+
return false;
|
|
1045
|
+
return getApiConfig() !== null;
|
|
1046
|
+
}
|
|
1047
|
+
function apiRequestRaw(method, path, body) {
|
|
1048
|
+
const cfg = getApiConfig();
|
|
1049
|
+
if (!cfg)
|
|
1050
|
+
throw new Error("api-mode: not configured (HASNA_MEMENTOS_API_URL / HASNA_MEMENTOS_API_KEY)");
|
|
1051
|
+
assertRequestAllowedUnderTest(cfg.baseUrl);
|
|
1052
|
+
const url = `${cfg.baseUrl}${path.startsWith("/") ? path : `/${path}`}`;
|
|
1053
|
+
const hasBody = body !== undefined && body !== null;
|
|
1054
|
+
const timeout = process.env["HASNA_MEMENTOS_API_TIMEOUT"] || DEFAULT_TIMEOUT_S;
|
|
1055
|
+
const headerLines = `Authorization: Bearer ${cfg.apiKey}
|
|
1056
|
+
x-api-key: ${cfg.apiKey}
|
|
1057
|
+
`;
|
|
1058
|
+
const args = [
|
|
1059
|
+
"curl",
|
|
1060
|
+
"-sS",
|
|
1061
|
+
"--fail-with-body",
|
|
1062
|
+
"-m",
|
|
1063
|
+
timeout,
|
|
1064
|
+
"-X",
|
|
1065
|
+
method,
|
|
1066
|
+
"-H",
|
|
1067
|
+
"@-",
|
|
1068
|
+
"-H",
|
|
1069
|
+
"Content-Type: application/json",
|
|
1070
|
+
"-H",
|
|
1071
|
+
"Accept: application/json",
|
|
1072
|
+
"-w",
|
|
1073
|
+
"\\n%{http_code}"
|
|
1074
|
+
];
|
|
1075
|
+
let bodyFile;
|
|
1076
|
+
if (hasBody) {
|
|
1077
|
+
bodyFile = join3(tmpdir(), `mem-req-${process.pid}-${randomUUID()}.json`);
|
|
1078
|
+
writeFileSync2(bodyFile, JSON.stringify(body), { mode: 384 });
|
|
1079
|
+
args.push("--data-binary", `@${bodyFile}`);
|
|
1080
|
+
}
|
|
1081
|
+
args.push(url);
|
|
1082
|
+
const childEnv = {};
|
|
1083
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
1084
|
+
if (v === undefined)
|
|
1085
|
+
continue;
|
|
1086
|
+
if (k === "HASNA_MEMENTOS_API_KEY" || k === "MEMENTOS_API_KEY")
|
|
1087
|
+
continue;
|
|
1088
|
+
childEnv[k] = v;
|
|
1089
|
+
}
|
|
1090
|
+
let out = "";
|
|
1091
|
+
let err = "";
|
|
1092
|
+
try {
|
|
1093
|
+
const proc = Bun.spawnSync(args, {
|
|
1094
|
+
stdin: Buffer.from(headerLines),
|
|
1095
|
+
stdout: "pipe",
|
|
1096
|
+
stderr: "pipe",
|
|
1097
|
+
env: childEnv
|
|
1098
|
+
});
|
|
1099
|
+
out = proc.stdout ? new TextDecoder().decode(proc.stdout) : "";
|
|
1100
|
+
err = proc.stderr ? new TextDecoder().decode(proc.stderr) : "";
|
|
1101
|
+
} finally {
|
|
1102
|
+
if (bodyFile) {
|
|
1103
|
+
try {
|
|
1104
|
+
unlinkSync(bodyFile);
|
|
1105
|
+
} catch {}
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
const nl = out.lastIndexOf(`
|
|
1109
|
+
`);
|
|
1110
|
+
const codeStr = nl >= 0 ? out.slice(nl + 1).trim() : "";
|
|
1111
|
+
const respBody = nl >= 0 ? out.slice(0, nl) : out;
|
|
1112
|
+
const status = parseInt(codeStr, 10);
|
|
1113
|
+
if (!Number.isFinite(status) || status === 0) {
|
|
1114
|
+
throw new ApiRequestError(`mementos cloud request failed (${method} ${path}): ${err.trim() || "no HTTP status"}`, 0, respBody);
|
|
1115
|
+
}
|
|
1116
|
+
return { status, body: respBody };
|
|
1117
|
+
}
|
|
1118
|
+
function apiJson(method, path, body, options) {
|
|
1119
|
+
const raw = apiRequestRaw(method, path, body);
|
|
1120
|
+
if (raw.status >= 200 && raw.status < 300) {
|
|
1121
|
+
const data = raw.body.trim() ? JSON.parse(raw.body) : undefined;
|
|
1122
|
+
return { status: raw.status, data };
|
|
1123
|
+
}
|
|
1124
|
+
if (raw.status === 404 && options?.allow404) {
|
|
1125
|
+
return { status: 404, data: undefined };
|
|
1126
|
+
}
|
|
1127
|
+
let msg = `mementos cloud ${method} ${path} \u2192 ${raw.status}`;
|
|
1128
|
+
try {
|
|
1129
|
+
const parsed = JSON.parse(raw.body);
|
|
1130
|
+
if (parsed.error || parsed.message)
|
|
1131
|
+
msg += `: ${parsed.error || parsed.message}`;
|
|
1132
|
+
} catch {
|
|
1133
|
+
if (raw.body.trim())
|
|
1134
|
+
msg += `: ${raw.body.slice(0, 200)}`;
|
|
1135
|
+
}
|
|
1136
|
+
throw new ApiRequestError(msg, raw.status, raw.body);
|
|
1137
|
+
}
|
|
1138
|
+
function toQuery(params) {
|
|
1139
|
+
const sp = new URLSearchParams;
|
|
1140
|
+
for (const [k, v] of Object.entries(params)) {
|
|
1141
|
+
if (v === undefined || v === null || v === "")
|
|
1142
|
+
continue;
|
|
1143
|
+
if (Array.isArray(v)) {
|
|
1144
|
+
if (v.length === 0)
|
|
1145
|
+
continue;
|
|
1146
|
+
sp.set(k, v.join(","));
|
|
1147
|
+
} else if (typeof v === "boolean") {
|
|
1148
|
+
sp.set(k, v ? "true" : "false");
|
|
1149
|
+
} else {
|
|
1150
|
+
sp.set(k, String(v));
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
const s = sp.toString();
|
|
1154
|
+
return s ? `?${s}` : "";
|
|
1155
|
+
}
|
|
1156
|
+
var API_URL_ENV_KEYS, API_KEY_ENV_KEYS, DATABASE_URL_ENV_KEYS, ALLOW_REMOTE_API_IN_TESTS_ENV = "MEMENTOS_ALLOW_REMOTE_API_IN_TESTS", DB_PATH_ENV_KEYS, MementosStoreConfigError, ApiRequestError, DEFAULT_TIMEOUT_S = "45";
|
|
1157
|
+
var init_api_mode = __esm(() => {
|
|
1158
|
+
API_URL_ENV_KEYS = ["HASNA_MEMENTOS_API_URL", "MEMENTOS_API_URL"];
|
|
1159
|
+
API_KEY_ENV_KEYS = ["HASNA_MEMENTOS_API_KEY", "MEMENTOS_API_KEY"];
|
|
1160
|
+
DATABASE_URL_ENV_KEYS = ["HASNA_MEMENTOS_DATABASE_URL", "MEMENTOS_DATABASE_URL"];
|
|
1161
|
+
DB_PATH_ENV_KEYS = ["HASNA_MEMENTOS_DB_PATH", "MEMENTOS_DB_PATH"];
|
|
1162
|
+
MementosStoreConfigError = class MementosStoreConfigError extends Error {
|
|
1163
|
+
code = "MEMENTOS_STORE_CONFIG";
|
|
1164
|
+
constructor(message) {
|
|
1165
|
+
super(message);
|
|
1166
|
+
this.name = "MementosStoreConfigError";
|
|
1167
|
+
}
|
|
1168
|
+
};
|
|
1169
|
+
ApiRequestError = class ApiRequestError extends Error {
|
|
1170
|
+
status;
|
|
1171
|
+
body;
|
|
1172
|
+
constructor(message, status, body) {
|
|
1173
|
+
super(message);
|
|
1174
|
+
this.status = status;
|
|
1175
|
+
this.body = body;
|
|
1176
|
+
this.name = "ApiRequestError";
|
|
1177
|
+
}
|
|
1178
|
+
};
|
|
1179
|
+
});
|
|
1180
|
+
|
|
49
1181
|
// src/project-registration/schema.ts
|
|
50
1182
|
function sqliteMementosProjectRegistrationSchemaSql() {
|
|
51
1183
|
return `
|
|
@@ -319,22 +1451,1375 @@ function postgresMementosProjectGuardedUpdateSchemaSql() {
|
|
|
319
1451
|
`;
|
|
320
1452
|
}
|
|
321
1453
|
|
|
1454
|
+
// src/memory-project-link/schema.ts
|
|
1455
|
+
function sqliteMementosMemoryProjectLinkSchemaSql() {
|
|
1456
|
+
return `
|
|
1457
|
+
CREATE TABLE IF NOT EXISTS mementos_memory_project_link_receipts (
|
|
1458
|
+
receipt_id TEXT PRIMARY KEY,
|
|
1459
|
+
authority TEXT NOT NULL CHECK(authority = 'mementos'),
|
|
1460
|
+
route TEXT NOT NULL CHECK(route = 'mementos.memory-project-link.v1'),
|
|
1461
|
+
package_version TEXT NOT NULL,
|
|
1462
|
+
authority_id TEXT NOT NULL,
|
|
1463
|
+
tenant_id TEXT NOT NULL,
|
|
1464
|
+
corpus_id TEXT NOT NULL,
|
|
1465
|
+
operation_id TEXT NOT NULL,
|
|
1466
|
+
step_id TEXT NOT NULL,
|
|
1467
|
+
direction TEXT NOT NULL CHECK(direction IN ('forward', 'rollback')),
|
|
1468
|
+
idempotency_key TEXT NOT NULL,
|
|
1469
|
+
request_digest TEXT NOT NULL,
|
|
1470
|
+
outcome TEXT NOT NULL CHECK(outcome IN ('accepted', 'no_change')),
|
|
1471
|
+
target_memory_id TEXT NOT NULL,
|
|
1472
|
+
requested_project_id TEXT NOT NULL,
|
|
1473
|
+
expected_memory_version INTEGER NOT NULL,
|
|
1474
|
+
expected_memory_revision TEXT NOT NULL,
|
|
1475
|
+
expected_project_revision TEXT,
|
|
1476
|
+
result_memory_version INTEGER NOT NULL,
|
|
1477
|
+
result_memory_revision TEXT NOT NULL,
|
|
1478
|
+
result_memory_digest TEXT NOT NULL,
|
|
1479
|
+
result_project_revision TEXT,
|
|
1480
|
+
result_project_digest TEXT,
|
|
1481
|
+
accepted_receipt_id TEXT,
|
|
1482
|
+
before_link_json TEXT NOT NULL,
|
|
1483
|
+
after_link_json TEXT NOT NULL,
|
|
1484
|
+
before_project_revision TEXT,
|
|
1485
|
+
before_project_digest TEXT,
|
|
1486
|
+
after_project_revision TEXT,
|
|
1487
|
+
after_project_digest TEXT,
|
|
1488
|
+
created_at TEXT NOT NULL,
|
|
1489
|
+
UNIQUE(authority_id, tenant_id, corpus_id, direction, idempotency_key)
|
|
1490
|
+
);
|
|
1491
|
+
|
|
1492
|
+
CREATE INDEX IF NOT EXISTS idx_mementos_memory_project_link_receipts_target
|
|
1493
|
+
ON mementos_memory_project_link_receipts(
|
|
1494
|
+
authority_id, tenant_id, corpus_id, target_memory_id, created_at
|
|
1495
|
+
);
|
|
1496
|
+
|
|
1497
|
+
CREATE TRIGGER IF NOT EXISTS mementos_memory_project_link_receipts_immutable_update
|
|
1498
|
+
BEFORE UPDATE ON mementos_memory_project_link_receipts
|
|
1499
|
+
BEGIN
|
|
1500
|
+
SELECT RAISE(ABORT, 'mementos memory project link receipts are immutable');
|
|
1501
|
+
END;
|
|
1502
|
+
|
|
1503
|
+
CREATE TRIGGER IF NOT EXISTS mementos_memory_project_link_receipts_immutable_delete
|
|
1504
|
+
BEFORE DELETE ON mementos_memory_project_link_receipts
|
|
1505
|
+
BEGIN
|
|
1506
|
+
SELECT RAISE(ABORT, 'mementos memory project link receipts are immutable');
|
|
1507
|
+
END;
|
|
1508
|
+
`;
|
|
1509
|
+
}
|
|
1510
|
+
function postgresMementosMemoryProjectLinkSchemaSql() {
|
|
1511
|
+
return `
|
|
1512
|
+
CREATE TABLE IF NOT EXISTS mementos_memory_project_link_receipts (
|
|
1513
|
+
receipt_id TEXT PRIMARY KEY,
|
|
1514
|
+
authority TEXT NOT NULL CHECK(authority = 'mementos'),
|
|
1515
|
+
route TEXT NOT NULL CHECK(route = 'mementos.memory-project-link.v1'),
|
|
1516
|
+
package_version TEXT NOT NULL,
|
|
1517
|
+
authority_id TEXT NOT NULL,
|
|
1518
|
+
tenant_id TEXT NOT NULL,
|
|
1519
|
+
corpus_id TEXT NOT NULL,
|
|
1520
|
+
operation_id TEXT NOT NULL,
|
|
1521
|
+
step_id TEXT NOT NULL,
|
|
1522
|
+
direction TEXT NOT NULL CHECK(direction IN ('forward', 'rollback')),
|
|
1523
|
+
idempotency_key TEXT NOT NULL,
|
|
1524
|
+
request_digest TEXT NOT NULL,
|
|
1525
|
+
outcome TEXT NOT NULL CHECK(outcome IN ('accepted', 'no_change')),
|
|
1526
|
+
target_memory_id TEXT NOT NULL,
|
|
1527
|
+
requested_project_id TEXT NOT NULL,
|
|
1528
|
+
expected_memory_version INTEGER NOT NULL,
|
|
1529
|
+
expected_memory_revision TEXT NOT NULL,
|
|
1530
|
+
expected_project_revision TEXT,
|
|
1531
|
+
result_memory_version INTEGER NOT NULL,
|
|
1532
|
+
result_memory_revision TEXT NOT NULL,
|
|
1533
|
+
result_memory_digest TEXT NOT NULL,
|
|
1534
|
+
result_project_revision TEXT,
|
|
1535
|
+
result_project_digest TEXT,
|
|
1536
|
+
accepted_receipt_id TEXT,
|
|
1537
|
+
before_link_json JSONB NOT NULL,
|
|
1538
|
+
after_link_json JSONB NOT NULL,
|
|
1539
|
+
before_project_revision TEXT,
|
|
1540
|
+
before_project_digest TEXT,
|
|
1541
|
+
after_project_revision TEXT,
|
|
1542
|
+
after_project_digest TEXT,
|
|
1543
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
1544
|
+
UNIQUE(authority_id, tenant_id, corpus_id, direction, idempotency_key)
|
|
1545
|
+
);
|
|
1546
|
+
|
|
1547
|
+
CREATE INDEX IF NOT EXISTS idx_mementos_memory_project_link_receipts_target
|
|
1548
|
+
ON mementos_memory_project_link_receipts(
|
|
1549
|
+
authority_id, tenant_id, corpus_id, target_memory_id, created_at
|
|
1550
|
+
);
|
|
1551
|
+
|
|
1552
|
+
CREATE OR REPLACE FUNCTION mementos_memory_project_link_receipts_immutable()
|
|
1553
|
+
RETURNS trigger
|
|
1554
|
+
LANGUAGE plpgsql
|
|
1555
|
+
AS $$
|
|
1556
|
+
BEGIN
|
|
1557
|
+
RAISE EXCEPTION 'mementos memory project link receipts are immutable';
|
|
1558
|
+
END;
|
|
1559
|
+
$$;
|
|
1560
|
+
DROP TRIGGER IF EXISTS mementos_memory_project_link_receipts_immutable
|
|
1561
|
+
ON mementos_memory_project_link_receipts;
|
|
1562
|
+
CREATE TRIGGER mementos_memory_project_link_receipts_immutable
|
|
1563
|
+
BEFORE UPDATE OR DELETE ON mementos_memory_project_link_receipts
|
|
1564
|
+
FOR EACH ROW EXECUTE FUNCTION mementos_memory_project_link_receipts_immutable();
|
|
1565
|
+
`;
|
|
1566
|
+
}
|
|
1567
|
+
var MEMENTOS_MEMORY_PROJECT_LINK_ROUTE = "mementos.memory-project-link.v1", MEMORY_PROJECT_LINK_RECEIPT_COLUMNS;
|
|
1568
|
+
var init_schema = __esm(() => {
|
|
1569
|
+
MEMORY_PROJECT_LINK_RECEIPT_COLUMNS = [
|
|
1570
|
+
"receipt_id",
|
|
1571
|
+
"authority",
|
|
1572
|
+
"route",
|
|
1573
|
+
"package_version",
|
|
1574
|
+
"authority_id",
|
|
1575
|
+
"tenant_id",
|
|
1576
|
+
"corpus_id",
|
|
1577
|
+
"operation_id",
|
|
1578
|
+
"step_id",
|
|
1579
|
+
"direction",
|
|
1580
|
+
"idempotency_key",
|
|
1581
|
+
"request_digest",
|
|
1582
|
+
"outcome",
|
|
1583
|
+
"target_memory_id",
|
|
1584
|
+
"requested_project_id",
|
|
1585
|
+
"expected_memory_version",
|
|
1586
|
+
"expected_memory_revision",
|
|
1587
|
+
"expected_project_revision",
|
|
1588
|
+
"result_memory_version",
|
|
1589
|
+
"result_memory_revision",
|
|
1590
|
+
"result_memory_digest",
|
|
1591
|
+
"result_project_revision",
|
|
1592
|
+
"result_project_digest",
|
|
1593
|
+
"accepted_receipt_id",
|
|
1594
|
+
"before_link_json",
|
|
1595
|
+
"after_link_json",
|
|
1596
|
+
"before_project_revision",
|
|
1597
|
+
"before_project_digest",
|
|
1598
|
+
"after_project_revision",
|
|
1599
|
+
"after_project_digest",
|
|
1600
|
+
"created_at"
|
|
1601
|
+
];
|
|
1602
|
+
});
|
|
1603
|
+
|
|
1604
|
+
// src/db/migrations.ts
|
|
1605
|
+
var MEMORY_VERSION_SNAPSHOT_TRIGGER = `
|
|
1606
|
+
CREATE TRIGGER IF NOT EXISTS memories_version_snapshot
|
|
1607
|
+
BEFORE UPDATE ON memories
|
|
1608
|
+
WHEN NEW.version > OLD.version
|
|
1609
|
+
BEGIN
|
|
1610
|
+
INSERT OR IGNORE INTO memory_versions (
|
|
1611
|
+
id, memory_id, version, value, importance, scope, category, tags,
|
|
1612
|
+
summary, pinned, status, when_to_use, created_at
|
|
1613
|
+
) VALUES (
|
|
1614
|
+
lower(hex(randomblob(4))) || '-' || lower(hex(randomblob(2))) || '-' ||
|
|
1615
|
+
lower(hex(randomblob(2))) || '-' || lower(hex(randomblob(2))) || '-' ||
|
|
1616
|
+
lower(hex(randomblob(6))),
|
|
1617
|
+
OLD.id, OLD.version, OLD.value, OLD.importance, OLD.scope, OLD.category,
|
|
1618
|
+
OLD.tags, OLD.summary, OLD.pinned, OLD.status, OLD.when_to_use,
|
|
1619
|
+
OLD.updated_at
|
|
1620
|
+
);
|
|
1621
|
+
END;
|
|
1622
|
+
`, MIGRATIONS;
|
|
1623
|
+
var init_migrations = __esm(() => {
|
|
1624
|
+
init_schema();
|
|
1625
|
+
MIGRATIONS = [
|
|
1626
|
+
`
|
|
1627
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
1628
|
+
id TEXT PRIMARY KEY,
|
|
1629
|
+
name TEXT NOT NULL,
|
|
1630
|
+
path TEXT UNIQUE NOT NULL,
|
|
1631
|
+
description TEXT,
|
|
1632
|
+
memory_prefix TEXT,
|
|
1633
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1634
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1635
|
+
);
|
|
1636
|
+
|
|
1637
|
+
CREATE TABLE IF NOT EXISTS agents (
|
|
1638
|
+
id TEXT PRIMARY KEY,
|
|
1639
|
+
name TEXT NOT NULL UNIQUE,
|
|
1640
|
+
description TEXT,
|
|
1641
|
+
role TEXT DEFAULT 'agent',
|
|
1642
|
+
metadata TEXT DEFAULT '{}',
|
|
1643
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1644
|
+
last_seen_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1645
|
+
);
|
|
1646
|
+
|
|
1647
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
1648
|
+
id TEXT PRIMARY KEY,
|
|
1649
|
+
key TEXT NOT NULL,
|
|
1650
|
+
value TEXT NOT NULL,
|
|
1651
|
+
category TEXT NOT NULL DEFAULT 'knowledge' CHECK(category IN ('preference', 'fact', 'knowledge', 'history')),
|
|
1652
|
+
scope TEXT NOT NULL DEFAULT 'private' CHECK(scope IN ('global', 'shared', 'private')),
|
|
1653
|
+
summary TEXT,
|
|
1654
|
+
tags TEXT DEFAULT '[]',
|
|
1655
|
+
importance INTEGER NOT NULL DEFAULT 5 CHECK(importance >= 1 AND importance <= 10),
|
|
1656
|
+
source TEXT NOT NULL DEFAULT 'agent' CHECK(source IN ('user', 'agent', 'system', 'auto', 'imported')),
|
|
1657
|
+
status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'archived', 'expired')),
|
|
1658
|
+
pinned INTEGER NOT NULL DEFAULT 0,
|
|
1659
|
+
agent_id TEXT REFERENCES agents(id) ON DELETE SET NULL,
|
|
1660
|
+
project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
|
|
1661
|
+
session_id TEXT,
|
|
1662
|
+
metadata TEXT DEFAULT '{}',
|
|
1663
|
+
access_count INTEGER NOT NULL DEFAULT 0,
|
|
1664
|
+
version INTEGER NOT NULL DEFAULT 1,
|
|
1665
|
+
expires_at TEXT,
|
|
1666
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1667
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1668
|
+
accessed_at TEXT
|
|
1669
|
+
);
|
|
1670
|
+
|
|
1671
|
+
CREATE TABLE IF NOT EXISTS memory_tags (
|
|
1672
|
+
memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
1673
|
+
tag TEXT NOT NULL,
|
|
1674
|
+
PRIMARY KEY (memory_id, tag)
|
|
1675
|
+
);
|
|
1676
|
+
|
|
1677
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
1678
|
+
id TEXT PRIMARY KEY,
|
|
1679
|
+
agent_id TEXT REFERENCES agents(id) ON DELETE SET NULL,
|
|
1680
|
+
project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
|
|
1681
|
+
started_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1682
|
+
last_activity TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1683
|
+
metadata TEXT DEFAULT '{}'
|
|
1684
|
+
);
|
|
1685
|
+
|
|
1686
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_memories_unique_key
|
|
1687
|
+
ON memories(key, scope, COALESCE(agent_id, ''), COALESCE(project_id, ''), COALESCE(session_id, ''));
|
|
1688
|
+
|
|
1689
|
+
CREATE INDEX IF NOT EXISTS idx_memories_key ON memories(key);
|
|
1690
|
+
CREATE INDEX IF NOT EXISTS idx_memories_scope ON memories(scope);
|
|
1691
|
+
CREATE INDEX IF NOT EXISTS idx_memories_category ON memories(category);
|
|
1692
|
+
CREATE INDEX IF NOT EXISTS idx_memories_status ON memories(status);
|
|
1693
|
+
CREATE INDEX IF NOT EXISTS idx_memories_importance ON memories(importance);
|
|
1694
|
+
CREATE INDEX IF NOT EXISTS idx_memories_agent ON memories(agent_id);
|
|
1695
|
+
CREATE INDEX IF NOT EXISTS idx_memories_project ON memories(project_id);
|
|
1696
|
+
CREATE INDEX IF NOT EXISTS idx_memories_session ON memories(session_id);
|
|
1697
|
+
CREATE INDEX IF NOT EXISTS idx_memories_pinned ON memories(pinned);
|
|
1698
|
+
CREATE INDEX IF NOT EXISTS idx_memories_expires ON memories(expires_at);
|
|
1699
|
+
CREATE INDEX IF NOT EXISTS idx_memories_created ON memories(created_at);
|
|
1700
|
+
CREATE INDEX IF NOT EXISTS idx_memory_tags_tag ON memory_tags(tag);
|
|
1701
|
+
CREATE INDEX IF NOT EXISTS idx_memory_tags_memory ON memory_tags(memory_id);
|
|
1702
|
+
CREATE INDEX IF NOT EXISTS idx_agents_name ON agents(name);
|
|
1703
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent_id);
|
|
1704
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id);
|
|
1705
|
+
|
|
1706
|
+
CREATE TABLE IF NOT EXISTS _migrations (
|
|
1707
|
+
id INTEGER PRIMARY KEY,
|
|
1708
|
+
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1709
|
+
);
|
|
1710
|
+
|
|
1711
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (1);
|
|
1712
|
+
`,
|
|
1713
|
+
`
|
|
1714
|
+
CREATE TABLE IF NOT EXISTS memory_versions (
|
|
1715
|
+
id TEXT PRIMARY KEY,
|
|
1716
|
+
memory_id TEXT NOT NULL,
|
|
1717
|
+
version INTEGER NOT NULL,
|
|
1718
|
+
value TEXT NOT NULL,
|
|
1719
|
+
importance INTEGER NOT NULL,
|
|
1720
|
+
scope TEXT NOT NULL,
|
|
1721
|
+
category TEXT NOT NULL,
|
|
1722
|
+
tags TEXT NOT NULL DEFAULT '[]',
|
|
1723
|
+
summary TEXT,
|
|
1724
|
+
pinned INTEGER NOT NULL DEFAULT 0,
|
|
1725
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
1726
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1727
|
+
UNIQUE(memory_id, version)
|
|
1728
|
+
);
|
|
1729
|
+
|
|
1730
|
+
CREATE INDEX IF NOT EXISTS idx_memory_versions_memory ON memory_versions(memory_id);
|
|
1731
|
+
CREATE INDEX IF NOT EXISTS idx_memory_versions_version ON memory_versions(memory_id, version);
|
|
1732
|
+
|
|
1733
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (2);
|
|
1734
|
+
`,
|
|
1735
|
+
`
|
|
1736
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
|
1737
|
+
key, value, summary,
|
|
1738
|
+
content='memories',
|
|
1739
|
+
content_rowid='rowid'
|
|
1740
|
+
);
|
|
1741
|
+
|
|
1742
|
+
CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
|
|
1743
|
+
INSERT INTO memories_fts(rowid, key, value, summary) VALUES (new.rowid, new.key, new.value, new.summary);
|
|
1744
|
+
END;
|
|
1745
|
+
|
|
1746
|
+
CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
|
|
1747
|
+
INSERT INTO memories_fts(memories_fts, rowid, key, value, summary) VALUES('delete', old.rowid, old.key, old.value, old.summary);
|
|
1748
|
+
END;
|
|
1749
|
+
|
|
1750
|
+
CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
|
|
1751
|
+
INSERT INTO memories_fts(memories_fts, rowid, key, value, summary) VALUES('delete', old.rowid, old.key, old.value, old.summary);
|
|
1752
|
+
INSERT INTO memories_fts(rowid, key, value, summary) VALUES (new.rowid, new.key, new.value, new.summary);
|
|
1753
|
+
END;
|
|
1754
|
+
|
|
1755
|
+
INSERT INTO memories_fts(memories_fts) VALUES('rebuild');
|
|
1756
|
+
|
|
1757
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (3);
|
|
1758
|
+
`,
|
|
1759
|
+
`
|
|
1760
|
+
CREATE TABLE IF NOT EXISTS search_history (
|
|
1761
|
+
id TEXT PRIMARY KEY,
|
|
1762
|
+
query TEXT NOT NULL,
|
|
1763
|
+
result_count INTEGER NOT NULL DEFAULT 0,
|
|
1764
|
+
agent_id TEXT,
|
|
1765
|
+
project_id TEXT,
|
|
1766
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1767
|
+
);
|
|
1768
|
+
CREATE INDEX IF NOT EXISTS idx_search_history_query ON search_history(query);
|
|
1769
|
+
CREATE INDEX IF NOT EXISTS idx_search_history_created ON search_history(created_at);
|
|
1770
|
+
|
|
1771
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (4);
|
|
1772
|
+
`,
|
|
1773
|
+
`
|
|
1774
|
+
CREATE TABLE IF NOT EXISTS entities (
|
|
1775
|
+
id TEXT PRIMARY KEY,
|
|
1776
|
+
name TEXT NOT NULL,
|
|
1777
|
+
type TEXT NOT NULL CHECK (type IN ('person','project','tool','concept','file','api','pattern','organization')),
|
|
1778
|
+
description TEXT,
|
|
1779
|
+
metadata TEXT NOT NULL DEFAULT '{}',
|
|
1780
|
+
project_id TEXT,
|
|
1781
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1782
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1783
|
+
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE SET NULL
|
|
1784
|
+
);
|
|
1785
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_entities_unique_name_type_project
|
|
1786
|
+
ON entities(name, type, COALESCE(project_id, ''));
|
|
1787
|
+
CREATE INDEX IF NOT EXISTS idx_entities_name ON entities(name);
|
|
1788
|
+
CREATE INDEX IF NOT EXISTS idx_entities_type ON entities(type);
|
|
1789
|
+
CREATE INDEX IF NOT EXISTS idx_entities_project ON entities(project_id);
|
|
1790
|
+
|
|
1791
|
+
CREATE TABLE IF NOT EXISTS relations (
|
|
1792
|
+
id TEXT PRIMARY KEY,
|
|
1793
|
+
source_entity_id TEXT NOT NULL,
|
|
1794
|
+
target_entity_id TEXT NOT NULL,
|
|
1795
|
+
relation_type TEXT NOT NULL CHECK (relation_type IN ('uses','knows','depends_on','created_by','related_to','contradicts','part_of','implements')),
|
|
1796
|
+
weight REAL NOT NULL DEFAULT 1.0,
|
|
1797
|
+
metadata TEXT NOT NULL DEFAULT '{}',
|
|
1798
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1799
|
+
UNIQUE(source_entity_id, target_entity_id, relation_type),
|
|
1800
|
+
FOREIGN KEY (source_entity_id) REFERENCES entities(id) ON DELETE CASCADE,
|
|
1801
|
+
FOREIGN KEY (target_entity_id) REFERENCES entities(id) ON DELETE CASCADE
|
|
1802
|
+
);
|
|
1803
|
+
CREATE INDEX IF NOT EXISTS idx_relations_source ON relations(source_entity_id);
|
|
1804
|
+
CREATE INDEX IF NOT EXISTS idx_relations_target ON relations(target_entity_id);
|
|
1805
|
+
CREATE INDEX IF NOT EXISTS idx_relations_type ON relations(relation_type);
|
|
1806
|
+
|
|
1807
|
+
CREATE TABLE IF NOT EXISTS entity_memories (
|
|
1808
|
+
entity_id TEXT NOT NULL,
|
|
1809
|
+
memory_id TEXT NOT NULL,
|
|
1810
|
+
role TEXT NOT NULL DEFAULT 'context' CHECK (role IN ('subject','object','context')),
|
|
1811
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1812
|
+
PRIMARY KEY (entity_id, memory_id),
|
|
1813
|
+
FOREIGN KEY (entity_id) REFERENCES entities(id) ON DELETE CASCADE,
|
|
1814
|
+
FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE CASCADE
|
|
1815
|
+
);
|
|
1816
|
+
CREATE INDEX IF NOT EXISTS idx_entity_memories_memory ON entity_memories(memory_id);
|
|
1817
|
+
|
|
1818
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (5);
|
|
1819
|
+
`,
|
|
1820
|
+
`
|
|
1821
|
+
ALTER TABLE agents ADD COLUMN active_project_id TEXT REFERENCES projects(id) ON DELETE SET NULL;
|
|
1822
|
+
CREATE INDEX IF NOT EXISTS idx_agents_active_project ON agents(active_project_id);
|
|
1823
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (6);
|
|
1824
|
+
`,
|
|
1825
|
+
`
|
|
1826
|
+
ALTER TABLE agents ADD COLUMN session_id TEXT;
|
|
1827
|
+
CREATE INDEX IF NOT EXISTS idx_agents_session ON agents(session_id);
|
|
1828
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (7);
|
|
1829
|
+
`,
|
|
1830
|
+
`
|
|
1831
|
+
CREATE TABLE IF NOT EXISTS resource_locks (
|
|
1832
|
+
id TEXT PRIMARY KEY,
|
|
1833
|
+
resource_type TEXT NOT NULL CHECK(resource_type IN ('project', 'memory', 'entity', 'agent', 'connector')),
|
|
1834
|
+
resource_id TEXT NOT NULL,
|
|
1835
|
+
agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
|
|
1836
|
+
lock_type TEXT NOT NULL DEFAULT 'exclusive' CHECK(lock_type IN ('advisory', 'exclusive')),
|
|
1837
|
+
locked_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1838
|
+
expires_at TEXT NOT NULL
|
|
1839
|
+
);
|
|
1840
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_resource_locks_exclusive
|
|
1841
|
+
ON resource_locks(resource_type, resource_id)
|
|
1842
|
+
WHERE lock_type = 'exclusive';
|
|
1843
|
+
CREATE INDEX IF NOT EXISTS idx_resource_locks_agent ON resource_locks(agent_id);
|
|
1844
|
+
CREATE INDEX IF NOT EXISTS idx_resource_locks_expires ON resource_locks(expires_at);
|
|
1845
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (8);
|
|
1846
|
+
`,
|
|
1847
|
+
`
|
|
1848
|
+
ALTER TABLE memories ADD COLUMN recall_count INTEGER NOT NULL DEFAULT 0;
|
|
1849
|
+
CREATE INDEX IF NOT EXISTS idx_memories_recall_count ON memories(recall_count DESC);
|
|
1850
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (9);
|
|
1851
|
+
`,
|
|
1852
|
+
`
|
|
1853
|
+
CREATE TABLE IF NOT EXISTS synthesis_events (
|
|
1854
|
+
id TEXT PRIMARY KEY,
|
|
1855
|
+
event_type TEXT NOT NULL CHECK(event_type IN ('recalled','searched','saved','updated','deleted','injected')),
|
|
1856
|
+
memory_id TEXT,
|
|
1857
|
+
agent_id TEXT,
|
|
1858
|
+
project_id TEXT,
|
|
1859
|
+
session_id TEXT,
|
|
1860
|
+
query TEXT,
|
|
1861
|
+
importance_at_time INTEGER,
|
|
1862
|
+
metadata TEXT NOT NULL DEFAULT '{}',
|
|
1863
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1864
|
+
);
|
|
1865
|
+
CREATE INDEX IF NOT EXISTS idx_synthesis_events_memory ON synthesis_events(memory_id);
|
|
1866
|
+
CREATE INDEX IF NOT EXISTS idx_synthesis_events_project ON synthesis_events(project_id);
|
|
1867
|
+
CREATE INDEX IF NOT EXISTS idx_synthesis_events_type ON synthesis_events(event_type);
|
|
1868
|
+
CREATE INDEX IF NOT EXISTS idx_synthesis_events_created ON synthesis_events(created_at);
|
|
1869
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (11);
|
|
1870
|
+
`,
|
|
1871
|
+
`
|
|
1872
|
+
CREATE TABLE IF NOT EXISTS synthesis_runs (
|
|
1873
|
+
id TEXT PRIMARY KEY,
|
|
1874
|
+
triggered_by TEXT NOT NULL DEFAULT 'manual' CHECK(triggered_by IN ('scheduler','manual','threshold','hook')),
|
|
1875
|
+
project_id TEXT,
|
|
1876
|
+
agent_id TEXT,
|
|
1877
|
+
corpus_size INTEGER NOT NULL DEFAULT 0,
|
|
1878
|
+
proposals_generated INTEGER NOT NULL DEFAULT 0,
|
|
1879
|
+
proposals_accepted INTEGER NOT NULL DEFAULT 0,
|
|
1880
|
+
proposals_rejected INTEGER NOT NULL DEFAULT 0,
|
|
1881
|
+
status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending','running','completed','failed','rolled_back')),
|
|
1882
|
+
error TEXT,
|
|
1883
|
+
started_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1884
|
+
completed_at TEXT
|
|
1885
|
+
);
|
|
1886
|
+
CREATE INDEX IF NOT EXISTS idx_synthesis_runs_project ON synthesis_runs(project_id);
|
|
1887
|
+
CREATE INDEX IF NOT EXISTS idx_synthesis_runs_status ON synthesis_runs(status);
|
|
1888
|
+
CREATE INDEX IF NOT EXISTS idx_synthesis_runs_started ON synthesis_runs(started_at);
|
|
1889
|
+
|
|
1890
|
+
CREATE TABLE IF NOT EXISTS synthesis_proposals (
|
|
1891
|
+
id TEXT PRIMARY KEY,
|
|
1892
|
+
run_id TEXT NOT NULL REFERENCES synthesis_runs(id) ON DELETE CASCADE,
|
|
1893
|
+
proposal_type TEXT NOT NULL CHECK(proposal_type IN ('merge','archive','promote','update_value','add_tag','remove_duplicate')),
|
|
1894
|
+
memory_ids TEXT NOT NULL DEFAULT '[]',
|
|
1895
|
+
target_memory_id TEXT,
|
|
1896
|
+
proposed_changes TEXT NOT NULL DEFAULT '{}',
|
|
1897
|
+
reasoning TEXT,
|
|
1898
|
+
confidence REAL NOT NULL DEFAULT 0.5,
|
|
1899
|
+
status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending','accepted','rejected','rolled_back')),
|
|
1900
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1901
|
+
executed_at TEXT,
|
|
1902
|
+
rollback_data TEXT
|
|
1903
|
+
);
|
|
1904
|
+
CREATE INDEX IF NOT EXISTS idx_synthesis_proposals_run ON synthesis_proposals(run_id);
|
|
1905
|
+
CREATE INDEX IF NOT EXISTS idx_synthesis_proposals_status ON synthesis_proposals(status);
|
|
1906
|
+
CREATE INDEX IF NOT EXISTS idx_synthesis_proposals_type ON synthesis_proposals(proposal_type);
|
|
1907
|
+
|
|
1908
|
+
CREATE TABLE IF NOT EXISTS synthesis_metrics (
|
|
1909
|
+
id TEXT PRIMARY KEY,
|
|
1910
|
+
run_id TEXT NOT NULL REFERENCES synthesis_runs(id) ON DELETE CASCADE,
|
|
1911
|
+
metric_type TEXT NOT NULL,
|
|
1912
|
+
value REAL NOT NULL,
|
|
1913
|
+
baseline REAL,
|
|
1914
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1915
|
+
);
|
|
1916
|
+
CREATE INDEX IF NOT EXISTS idx_synthesis_metrics_run ON synthesis_metrics(run_id);
|
|
1917
|
+
CREATE INDEX IF NOT EXISTS idx_synthesis_metrics_type ON synthesis_metrics(metric_type);
|
|
1918
|
+
|
|
1919
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (12);
|
|
1920
|
+
`,
|
|
1921
|
+
`
|
|
1922
|
+
CREATE TABLE IF NOT EXISTS webhook_hooks (
|
|
1923
|
+
id TEXT PRIMARY KEY,
|
|
1924
|
+
type TEXT NOT NULL,
|
|
1925
|
+
handler_url TEXT NOT NULL,
|
|
1926
|
+
priority INTEGER NOT NULL DEFAULT 50,
|
|
1927
|
+
blocking INTEGER NOT NULL DEFAULT 0,
|
|
1928
|
+
agent_id TEXT,
|
|
1929
|
+
project_id TEXT,
|
|
1930
|
+
description TEXT,
|
|
1931
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
1932
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1933
|
+
invocation_count INTEGER NOT NULL DEFAULT 0,
|
|
1934
|
+
failure_count INTEGER NOT NULL DEFAULT 0
|
|
1935
|
+
);
|
|
1936
|
+
CREATE INDEX IF NOT EXISTS idx_webhook_hooks_type ON webhook_hooks(type);
|
|
1937
|
+
CREATE INDEX IF NOT EXISTS idx_webhook_hooks_enabled ON webhook_hooks(enabled);
|
|
1938
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (10);
|
|
1939
|
+
`,
|
|
1940
|
+
`
|
|
1941
|
+
CREATE TABLE IF NOT EXISTS session_memory_jobs (
|
|
1942
|
+
id TEXT PRIMARY KEY,
|
|
1943
|
+
session_id TEXT NOT NULL,
|
|
1944
|
+
agent_id TEXT,
|
|
1945
|
+
project_id TEXT,
|
|
1946
|
+
source TEXT NOT NULL DEFAULT 'manual' CHECK(source IN ('claude-code','codex','manual','open-sessions')),
|
|
1947
|
+
status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending','processing','completed','failed')),
|
|
1948
|
+
transcript TEXT NOT NULL,
|
|
1949
|
+
chunk_count INTEGER NOT NULL DEFAULT 0,
|
|
1950
|
+
memories_extracted INTEGER NOT NULL DEFAULT 0,
|
|
1951
|
+
error TEXT,
|
|
1952
|
+
metadata TEXT NOT NULL DEFAULT '{}',
|
|
1953
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1954
|
+
started_at TEXT,
|
|
1955
|
+
completed_at TEXT
|
|
1956
|
+
);
|
|
1957
|
+
CREATE INDEX IF NOT EXISTS idx_session_memory_jobs_status ON session_memory_jobs(status);
|
|
1958
|
+
CREATE INDEX IF NOT EXISTS idx_session_memory_jobs_agent ON session_memory_jobs(agent_id);
|
|
1959
|
+
CREATE INDEX IF NOT EXISTS idx_session_memory_jobs_project ON session_memory_jobs(project_id);
|
|
1960
|
+
CREATE INDEX IF NOT EXISTS idx_session_memory_jobs_session ON session_memory_jobs(session_id);
|
|
1961
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (13);
|
|
1962
|
+
`,
|
|
1963
|
+
`
|
|
1964
|
+
ALTER TABLE resource_locks RENAME TO resource_locks_old;
|
|
1965
|
+
CREATE TABLE resource_locks (
|
|
1966
|
+
id TEXT PRIMARY KEY,
|
|
1967
|
+
resource_type TEXT NOT NULL CHECK(resource_type IN ('project', 'memory', 'entity', 'agent', 'connector', 'file')),
|
|
1968
|
+
resource_id TEXT NOT NULL,
|
|
1969
|
+
agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
|
|
1970
|
+
lock_type TEXT NOT NULL DEFAULT 'exclusive' CHECK(lock_type IN ('advisory', 'exclusive')),
|
|
1971
|
+
locked_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1972
|
+
expires_at TEXT NOT NULL
|
|
1973
|
+
);
|
|
1974
|
+
INSERT INTO resource_locks SELECT * FROM resource_locks_old;
|
|
1975
|
+
DROP TABLE resource_locks_old;
|
|
1976
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_resource_locks_exclusive
|
|
1977
|
+
ON resource_locks(resource_type, resource_id)
|
|
1978
|
+
WHERE lock_type = 'exclusive';
|
|
1979
|
+
CREATE INDEX IF NOT EXISTS idx_resource_locks_agent ON resource_locks(agent_id);
|
|
1980
|
+
CREATE INDEX IF NOT EXISTS idx_resource_locks_expires ON resource_locks(expires_at);
|
|
1981
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (14);
|
|
1982
|
+
`,
|
|
1983
|
+
`
|
|
1984
|
+
CREATE TABLE IF NOT EXISTS machines (
|
|
1985
|
+
id TEXT PRIMARY KEY,
|
|
1986
|
+
name TEXT NOT NULL UNIQUE,
|
|
1987
|
+
hostname TEXT NOT NULL,
|
|
1988
|
+
platform TEXT NOT NULL DEFAULT 'unknown',
|
|
1989
|
+
is_primary INTEGER NOT NULL DEFAULT 0,
|
|
1990
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1991
|
+
last_seen_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1992
|
+
);
|
|
1993
|
+
CREATE INDEX IF NOT EXISTS idx_machines_hostname ON machines(hostname);
|
|
1994
|
+
ALTER TABLE memories ADD COLUMN machine_id TEXT REFERENCES machines(id) ON DELETE SET NULL;
|
|
1995
|
+
CREATE INDEX IF NOT EXISTS idx_memories_machine ON memories(machine_id);
|
|
1996
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (15);
|
|
1997
|
+
`,
|
|
1998
|
+
`
|
|
1999
|
+
ALTER TABLE memories ADD COLUMN flag TEXT;
|
|
2000
|
+
CREATE INDEX IF NOT EXISTS idx_memories_flag ON memories(flag);
|
|
2001
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (16);
|
|
2002
|
+
`,
|
|
2003
|
+
`
|
|
2004
|
+
CREATE TABLE IF NOT EXISTS memory_embeddings (
|
|
2005
|
+
memory_id TEXT PRIMARY KEY REFERENCES memories(id) ON DELETE CASCADE,
|
|
2006
|
+
embedding TEXT NOT NULL,
|
|
2007
|
+
model TEXT NOT NULL DEFAULT 'tfidf-512',
|
|
2008
|
+
dimensions INTEGER NOT NULL DEFAULT 512,
|
|
2009
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
2010
|
+
);
|
|
2011
|
+
CREATE INDEX IF NOT EXISTS idx_memory_embeddings_model ON memory_embeddings(model);
|
|
2012
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (17);
|
|
2013
|
+
`,
|
|
2014
|
+
`
|
|
2015
|
+
ALTER TABLE memories ADD COLUMN valid_from TEXT DEFAULT NULL;
|
|
2016
|
+
ALTER TABLE memories ADD COLUMN valid_until TEXT DEFAULT NULL;
|
|
2017
|
+
ALTER TABLE memories ADD COLUMN ingested_at TEXT DEFAULT NULL;
|
|
2018
|
+
CREATE INDEX IF NOT EXISTS idx_memories_valid_from ON memories(valid_from);
|
|
2019
|
+
CREATE INDEX IF NOT EXISTS idx_memories_valid_until ON memories(valid_until);
|
|
2020
|
+
UPDATE memories SET valid_from = created_at, ingested_at = created_at WHERE valid_from IS NULL;
|
|
2021
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (18);
|
|
2022
|
+
`,
|
|
2023
|
+
`
|
|
2024
|
+
PRAGMA foreign_keys = OFF;
|
|
2025
|
+
ALTER TABLE memories RENAME TO memories_old;
|
|
2026
|
+
CREATE TABLE memories (
|
|
2027
|
+
id TEXT PRIMARY KEY,
|
|
2028
|
+
key TEXT NOT NULL,
|
|
2029
|
+
value TEXT NOT NULL,
|
|
2030
|
+
category TEXT NOT NULL DEFAULT 'knowledge' CHECK(category IN ('preference', 'fact', 'knowledge', 'history')),
|
|
2031
|
+
scope TEXT NOT NULL DEFAULT 'private' CHECK(scope IN ('global', 'shared', 'private', 'working')),
|
|
2032
|
+
summary TEXT,
|
|
2033
|
+
tags TEXT DEFAULT '[]',
|
|
2034
|
+
importance INTEGER NOT NULL DEFAULT 5 CHECK(importance >= 1 AND importance <= 10),
|
|
2035
|
+
source TEXT NOT NULL DEFAULT 'agent' CHECK(source IN ('user', 'agent', 'system', 'auto', 'imported')),
|
|
2036
|
+
status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'archived', 'expired')),
|
|
2037
|
+
pinned INTEGER NOT NULL DEFAULT 0,
|
|
2038
|
+
agent_id TEXT REFERENCES agents(id) ON DELETE SET NULL,
|
|
2039
|
+
project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
|
|
2040
|
+
session_id TEXT,
|
|
2041
|
+
machine_id TEXT REFERENCES machines(id) ON DELETE SET NULL,
|
|
2042
|
+
flag TEXT,
|
|
2043
|
+
metadata TEXT DEFAULT '{}',
|
|
2044
|
+
access_count INTEGER NOT NULL DEFAULT 0,
|
|
2045
|
+
recall_count INTEGER NOT NULL DEFAULT 0,
|
|
2046
|
+
version INTEGER NOT NULL DEFAULT 1,
|
|
2047
|
+
expires_at TEXT,
|
|
2048
|
+
valid_from TEXT DEFAULT NULL,
|
|
2049
|
+
valid_until TEXT DEFAULT NULL,
|
|
2050
|
+
ingested_at TEXT DEFAULT NULL,
|
|
2051
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
2052
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
2053
|
+
accessed_at TEXT
|
|
2054
|
+
);
|
|
2055
|
+
INSERT INTO memories SELECT * FROM memories_old;
|
|
2056
|
+
DROP TABLE memories_old;
|
|
2057
|
+
|
|
2058
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_memories_unique_key
|
|
2059
|
+
ON memories(key, scope, COALESCE(agent_id, ''), COALESCE(project_id, ''), COALESCE(session_id, ''));
|
|
2060
|
+
CREATE INDEX IF NOT EXISTS idx_memories_key ON memories(key);
|
|
2061
|
+
CREATE INDEX IF NOT EXISTS idx_memories_scope ON memories(scope);
|
|
2062
|
+
CREATE INDEX IF NOT EXISTS idx_memories_category ON memories(category);
|
|
2063
|
+
CREATE INDEX IF NOT EXISTS idx_memories_status ON memories(status);
|
|
2064
|
+
CREATE INDEX IF NOT EXISTS idx_memories_importance ON memories(importance);
|
|
2065
|
+
CREATE INDEX IF NOT EXISTS idx_memories_agent ON memories(agent_id);
|
|
2066
|
+
CREATE INDEX IF NOT EXISTS idx_memories_project ON memories(project_id);
|
|
2067
|
+
CREATE INDEX IF NOT EXISTS idx_memories_session ON memories(session_id);
|
|
2068
|
+
CREATE INDEX IF NOT EXISTS idx_memories_pinned ON memories(pinned);
|
|
2069
|
+
CREATE INDEX IF NOT EXISTS idx_memories_expires ON memories(expires_at);
|
|
2070
|
+
CREATE INDEX IF NOT EXISTS idx_memories_created ON memories(created_at);
|
|
2071
|
+
CREATE INDEX IF NOT EXISTS idx_memories_machine ON memories(machine_id);
|
|
2072
|
+
CREATE INDEX IF NOT EXISTS idx_memories_flag ON memories(flag);
|
|
2073
|
+
CREATE INDEX IF NOT EXISTS idx_memories_recall_count ON memories(recall_count DESC);
|
|
2074
|
+
CREATE INDEX IF NOT EXISTS idx_memories_valid_from ON memories(valid_from);
|
|
2075
|
+
CREATE INDEX IF NOT EXISTS idx_memories_valid_until ON memories(valid_until);
|
|
2076
|
+
|
|
2077
|
+
INSERT INTO memories_fts(memories_fts) VALUES('rebuild');
|
|
2078
|
+
|
|
2079
|
+
CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
|
|
2080
|
+
INSERT INTO memories_fts(rowid, key, value, summary) VALUES (new.rowid, new.key, new.value, new.summary);
|
|
2081
|
+
END;
|
|
2082
|
+
CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
|
|
2083
|
+
INSERT INTO memories_fts(memories_fts, rowid, key, value, summary) VALUES('delete', old.rowid, old.key, old.value, old.summary);
|
|
2084
|
+
END;
|
|
2085
|
+
CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
|
|
2086
|
+
INSERT INTO memories_fts(memories_fts, rowid, key, value, summary) VALUES('delete', old.rowid, old.key, old.value, old.summary);
|
|
2087
|
+
INSERT INTO memories_fts(rowid, key, value, summary) VALUES (new.rowid, new.key, new.value, new.summary);
|
|
2088
|
+
END;
|
|
2089
|
+
|
|
2090
|
+
PRAGMA foreign_keys = ON;
|
|
2091
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (19);
|
|
2092
|
+
`,
|
|
2093
|
+
`
|
|
2094
|
+
PRAGMA foreign_keys = OFF;
|
|
2095
|
+
ALTER TABLE memories RENAME TO memories_old;
|
|
2096
|
+
CREATE TABLE memories (
|
|
2097
|
+
id TEXT PRIMARY KEY,
|
|
2098
|
+
key TEXT NOT NULL,
|
|
2099
|
+
value TEXT NOT NULL,
|
|
2100
|
+
category TEXT NOT NULL DEFAULT 'knowledge' CHECK(category IN ('preference', 'fact', 'knowledge', 'history', 'procedural', 'resource')),
|
|
2101
|
+
scope TEXT NOT NULL DEFAULT 'private' CHECK(scope IN ('global', 'shared', 'private', 'working')),
|
|
2102
|
+
summary TEXT,
|
|
2103
|
+
tags TEXT DEFAULT '[]',
|
|
2104
|
+
importance INTEGER NOT NULL DEFAULT 5 CHECK(importance >= 1 AND importance <= 10),
|
|
2105
|
+
source TEXT NOT NULL DEFAULT 'agent' CHECK(source IN ('user', 'agent', 'system', 'auto', 'imported')),
|
|
2106
|
+
status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'archived', 'expired')),
|
|
2107
|
+
pinned INTEGER NOT NULL DEFAULT 0,
|
|
2108
|
+
agent_id TEXT REFERENCES agents(id) ON DELETE SET NULL,
|
|
2109
|
+
project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
|
|
2110
|
+
session_id TEXT,
|
|
2111
|
+
machine_id TEXT REFERENCES machines(id) ON DELETE SET NULL,
|
|
2112
|
+
flag TEXT,
|
|
2113
|
+
content_type TEXT NOT NULL DEFAULT 'text' CHECK(content_type IN ('text', 'code', 'image', 'resource')),
|
|
2114
|
+
metadata TEXT DEFAULT '{}',
|
|
2115
|
+
access_count INTEGER NOT NULL DEFAULT 0,
|
|
2116
|
+
recall_count INTEGER NOT NULL DEFAULT 0,
|
|
2117
|
+
version INTEGER NOT NULL DEFAULT 1,
|
|
2118
|
+
expires_at TEXT,
|
|
2119
|
+
valid_from TEXT DEFAULT NULL,
|
|
2120
|
+
valid_until TEXT DEFAULT NULL,
|
|
2121
|
+
ingested_at TEXT DEFAULT NULL,
|
|
2122
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
2123
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
2124
|
+
accessed_at TEXT
|
|
2125
|
+
);
|
|
2126
|
+
INSERT INTO memories (id, key, value, category, scope, summary, tags, importance, source, status, pinned, agent_id, project_id, session_id, machine_id, flag, metadata, access_count, recall_count, version, expires_at, valid_from, valid_until, ingested_at, created_at, updated_at, accessed_at)
|
|
2127
|
+
SELECT id, key, value, category, scope, summary, tags, importance, source, status, pinned, agent_id, project_id, session_id, machine_id, flag, metadata, access_count, recall_count, version, expires_at, valid_from, valid_until, ingested_at, created_at, updated_at, accessed_at FROM memories_old;
|
|
2128
|
+
DROP TABLE memories_old;
|
|
2129
|
+
|
|
2130
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_memories_unique_key
|
|
2131
|
+
ON memories(key, scope, COALESCE(agent_id, ''), COALESCE(project_id, ''), COALESCE(session_id, ''));
|
|
2132
|
+
CREATE INDEX IF NOT EXISTS idx_memories_key ON memories(key);
|
|
2133
|
+
CREATE INDEX IF NOT EXISTS idx_memories_scope ON memories(scope);
|
|
2134
|
+
CREATE INDEX IF NOT EXISTS idx_memories_category ON memories(category);
|
|
2135
|
+
CREATE INDEX IF NOT EXISTS idx_memories_status ON memories(status);
|
|
2136
|
+
CREATE INDEX IF NOT EXISTS idx_memories_importance ON memories(importance);
|
|
2137
|
+
CREATE INDEX IF NOT EXISTS idx_memories_agent ON memories(agent_id);
|
|
2138
|
+
CREATE INDEX IF NOT EXISTS idx_memories_project ON memories(project_id);
|
|
2139
|
+
CREATE INDEX IF NOT EXISTS idx_memories_session ON memories(session_id);
|
|
2140
|
+
CREATE INDEX IF NOT EXISTS idx_memories_pinned ON memories(pinned);
|
|
2141
|
+
CREATE INDEX IF NOT EXISTS idx_memories_expires ON memories(expires_at);
|
|
2142
|
+
CREATE INDEX IF NOT EXISTS idx_memories_created ON memories(created_at);
|
|
2143
|
+
CREATE INDEX IF NOT EXISTS idx_memories_machine ON memories(machine_id);
|
|
2144
|
+
CREATE INDEX IF NOT EXISTS idx_memories_flag ON memories(flag);
|
|
2145
|
+
CREATE INDEX IF NOT EXISTS idx_memories_recall_count ON memories(recall_count DESC);
|
|
2146
|
+
CREATE INDEX IF NOT EXISTS idx_memories_valid_from ON memories(valid_from);
|
|
2147
|
+
CREATE INDEX IF NOT EXISTS idx_memories_valid_until ON memories(valid_until);
|
|
2148
|
+
CREATE INDEX IF NOT EXISTS idx_memories_content_type ON memories(content_type);
|
|
2149
|
+
|
|
2150
|
+
INSERT INTO memories_fts(memories_fts) VALUES('rebuild');
|
|
2151
|
+
|
|
2152
|
+
CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
|
|
2153
|
+
INSERT INTO memories_fts(rowid, key, value, summary) VALUES (new.rowid, new.key, new.value, new.summary);
|
|
2154
|
+
END;
|
|
2155
|
+
CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
|
|
2156
|
+
INSERT INTO memories_fts(memories_fts, rowid, key, value, summary) VALUES('delete', old.rowid, old.key, old.value, old.summary);
|
|
2157
|
+
END;
|
|
2158
|
+
CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
|
|
2159
|
+
INSERT INTO memories_fts(memories_fts, rowid, key, value, summary) VALUES('delete', old.rowid, old.key, old.value, old.summary);
|
|
2160
|
+
INSERT INTO memories_fts(rowid, key, value, summary) VALUES (new.rowid, new.key, new.value, new.summary);
|
|
2161
|
+
END;
|
|
2162
|
+
|
|
2163
|
+
PRAGMA foreign_keys = ON;
|
|
2164
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (20);
|
|
2165
|
+
`,
|
|
2166
|
+
`
|
|
2167
|
+
PRAGMA foreign_keys = OFF;
|
|
2168
|
+
ALTER TABLE relations RENAME TO relations_old;
|
|
2169
|
+
CREATE TABLE relations (
|
|
2170
|
+
id TEXT PRIMARY KEY,
|
|
2171
|
+
source_entity_id TEXT NOT NULL,
|
|
2172
|
+
target_entity_id TEXT NOT NULL,
|
|
2173
|
+
relation_type TEXT NOT NULL CHECK (relation_type IN ('uses','knows','depends_on','created_by','related_to','contradicts','part_of','implements','happened_before','happened_after','caused_by','resulted_in','supersedes','version_of')),
|
|
2174
|
+
weight REAL NOT NULL DEFAULT 1.0,
|
|
2175
|
+
metadata TEXT NOT NULL DEFAULT '{}',
|
|
2176
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
2177
|
+
UNIQUE(source_entity_id, target_entity_id, relation_type),
|
|
2178
|
+
FOREIGN KEY (source_entity_id) REFERENCES entities(id) ON DELETE CASCADE,
|
|
2179
|
+
FOREIGN KEY (target_entity_id) REFERENCES entities(id) ON DELETE CASCADE
|
|
2180
|
+
);
|
|
2181
|
+
INSERT INTO relations SELECT * FROM relations_old;
|
|
2182
|
+
DROP TABLE relations_old;
|
|
2183
|
+
CREATE INDEX IF NOT EXISTS idx_relations_source ON relations(source_entity_id);
|
|
2184
|
+
CREATE INDEX IF NOT EXISTS idx_relations_target ON relations(target_entity_id);
|
|
2185
|
+
CREATE INDEX IF NOT EXISTS idx_relations_type ON relations(relation_type);
|
|
2186
|
+
PRAGMA foreign_keys = ON;
|
|
2187
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (21);
|
|
2188
|
+
`,
|
|
2189
|
+
`
|
|
2190
|
+
CREATE TABLE IF NOT EXISTS memory_audit_log (
|
|
2191
|
+
id TEXT PRIMARY KEY,
|
|
2192
|
+
memory_id TEXT NOT NULL,
|
|
2193
|
+
memory_key TEXT,
|
|
2194
|
+
operation TEXT NOT NULL CHECK(operation IN ('create','update','delete','archive','restore','read')),
|
|
2195
|
+
agent_id TEXT,
|
|
2196
|
+
old_value_hash TEXT,
|
|
2197
|
+
new_value_hash TEXT,
|
|
2198
|
+
changes TEXT DEFAULT '{}',
|
|
2199
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
2200
|
+
);
|
|
2201
|
+
CREATE INDEX IF NOT EXISTS idx_audit_log_memory ON memory_audit_log(memory_id);
|
|
2202
|
+
CREATE INDEX IF NOT EXISTS idx_audit_log_operation ON memory_audit_log(operation);
|
|
2203
|
+
CREATE INDEX IF NOT EXISTS idx_audit_log_agent ON memory_audit_log(agent_id);
|
|
2204
|
+
CREATE INDEX IF NOT EXISTS idx_audit_log_created ON memory_audit_log(created_at);
|
|
2205
|
+
|
|
2206
|
+
CREATE TRIGGER IF NOT EXISTS audit_memory_insert AFTER INSERT ON memories BEGIN
|
|
2207
|
+
INSERT INTO memory_audit_log (id, memory_id, memory_key, operation, agent_id, new_value_hash, created_at)
|
|
2208
|
+
VALUES (hex(randomblob(4)), new.id, new.key, 'create', new.agent_id, hex(randomblob(16)), datetime('now'));
|
|
2209
|
+
END;
|
|
2210
|
+
|
|
2211
|
+
CREATE TRIGGER IF NOT EXISTS audit_memory_update AFTER UPDATE ON memories BEGIN
|
|
2212
|
+
INSERT INTO memory_audit_log (id, memory_id, memory_key, operation, agent_id, old_value_hash, new_value_hash, changes, created_at)
|
|
2213
|
+
VALUES (hex(randomblob(4)), new.id, new.key, 'update', new.agent_id, hex(randomblob(16)), hex(randomblob(16)),
|
|
2214
|
+
json_object('version_from', old.version, 'version_to', new.version, 'importance_from', old.importance, 'importance_to', new.importance),
|
|
2215
|
+
datetime('now'));
|
|
2216
|
+
END;
|
|
2217
|
+
|
|
2218
|
+
CREATE TRIGGER IF NOT EXISTS audit_memory_delete AFTER DELETE ON memories BEGIN
|
|
2219
|
+
INSERT INTO memory_audit_log (id, memory_id, memory_key, operation, agent_id, old_value_hash, created_at)
|
|
2220
|
+
VALUES (hex(randomblob(4)), old.id, old.key, 'delete', old.agent_id, hex(randomblob(16)), datetime('now'));
|
|
2221
|
+
END;
|
|
2222
|
+
|
|
2223
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (22);
|
|
2224
|
+
`,
|
|
2225
|
+
`
|
|
2226
|
+
ALTER TABLE memories ADD COLUMN namespace TEXT DEFAULT NULL;
|
|
2227
|
+
ALTER TABLE memories ADD COLUMN created_by_agent TEXT DEFAULT NULL;
|
|
2228
|
+
ALTER TABLE memories ADD COLUMN updated_by_agent TEXT DEFAULT NULL;
|
|
2229
|
+
CREATE INDEX IF NOT EXISTS idx_memories_namespace ON memories(namespace);
|
|
2230
|
+
CREATE INDEX IF NOT EXISTS idx_memories_created_by ON memories(created_by_agent);
|
|
2231
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (23);
|
|
2232
|
+
`,
|
|
2233
|
+
`
|
|
2234
|
+
ALTER TABLE memories ADD COLUMN trust_score REAL NOT NULL DEFAULT 1.0;
|
|
2235
|
+
CREATE INDEX IF NOT EXISTS idx_memories_trust_score ON memories(trust_score);
|
|
2236
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (24);
|
|
2237
|
+
`,
|
|
2238
|
+
`
|
|
2239
|
+
CREATE TABLE IF NOT EXISTS memory_ratings (
|
|
2240
|
+
id TEXT PRIMARY KEY,
|
|
2241
|
+
memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
2242
|
+
agent_id TEXT,
|
|
2243
|
+
useful INTEGER NOT NULL DEFAULT 1,
|
|
2244
|
+
context TEXT,
|
|
2245
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
2246
|
+
);
|
|
2247
|
+
CREATE INDEX IF NOT EXISTS idx_memory_ratings_memory ON memory_ratings(memory_id);
|
|
2248
|
+
CREATE INDEX IF NOT EXISTS idx_memory_ratings_agent ON memory_ratings(agent_id);
|
|
2249
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (25);
|
|
2250
|
+
`,
|
|
2251
|
+
`
|
|
2252
|
+
CREATE TABLE IF NOT EXISTS memory_acl (
|
|
2253
|
+
id TEXT PRIMARY KEY,
|
|
2254
|
+
agent_id TEXT NOT NULL,
|
|
2255
|
+
key_pattern TEXT NOT NULL,
|
|
2256
|
+
permission TEXT NOT NULL DEFAULT 'readwrite' CHECK(permission IN ('read', 'readwrite', 'admin')),
|
|
2257
|
+
project_id TEXT,
|
|
2258
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
2259
|
+
);
|
|
2260
|
+
CREATE INDEX IF NOT EXISTS idx_memory_acl_agent ON memory_acl(agent_id);
|
|
2261
|
+
CREATE INDEX IF NOT EXISTS idx_memory_acl_project ON memory_acl(project_id);
|
|
2262
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (26);
|
|
2263
|
+
`,
|
|
2264
|
+
`
|
|
2265
|
+
ALTER TABLE memories ADD COLUMN vector_clock TEXT DEFAULT '{}';
|
|
2266
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (27);
|
|
2267
|
+
`,
|
|
2268
|
+
`
|
|
2269
|
+
CREATE TABLE IF NOT EXISTS memory_subscriptions (
|
|
2270
|
+
id TEXT PRIMARY KEY,
|
|
2271
|
+
agent_id TEXT NOT NULL,
|
|
2272
|
+
key_pattern TEXT,
|
|
2273
|
+
tag_pattern TEXT,
|
|
2274
|
+
scope TEXT,
|
|
2275
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
2276
|
+
);
|
|
2277
|
+
CREATE INDEX IF NOT EXISTS idx_memory_subs_agent ON memory_subscriptions(agent_id);
|
|
2278
|
+
CREATE INDEX IF NOT EXISTS idx_memory_subs_key ON memory_subscriptions(key_pattern);
|
|
2279
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (28);
|
|
2280
|
+
`,
|
|
2281
|
+
`
|
|
2282
|
+
PRAGMA foreign_keys = OFF;
|
|
2283
|
+
|
|
2284
|
+
-- Fix memory_tags FK if broken
|
|
2285
|
+
CREATE TABLE IF NOT EXISTS _mt_fix AS SELECT * FROM memory_tags;
|
|
2286
|
+
DROP TABLE IF EXISTS memory_tags;
|
|
2287
|
+
CREATE TABLE memory_tags (
|
|
2288
|
+
memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
2289
|
+
tag TEXT NOT NULL,
|
|
2290
|
+
PRIMARY KEY (memory_id, tag)
|
|
2291
|
+
);
|
|
2292
|
+
INSERT OR IGNORE INTO memory_tags SELECT * FROM _mt_fix;
|
|
2293
|
+
DROP TABLE _mt_fix;
|
|
2294
|
+
CREATE INDEX IF NOT EXISTS idx_memory_tags_tag ON memory_tags(tag);
|
|
2295
|
+
CREATE INDEX IF NOT EXISTS idx_memory_tags_memory ON memory_tags(memory_id);
|
|
2296
|
+
|
|
2297
|
+
-- Fix entity_memories FK if broken
|
|
2298
|
+
CREATE TABLE IF NOT EXISTS _em_fix AS SELECT * FROM entity_memories;
|
|
2299
|
+
DROP TABLE IF EXISTS entity_memories;
|
|
2300
|
+
CREATE TABLE entity_memories (
|
|
2301
|
+
entity_id TEXT NOT NULL,
|
|
2302
|
+
memory_id TEXT NOT NULL,
|
|
2303
|
+
role TEXT NOT NULL DEFAULT 'context' CHECK (role IN ('subject','object','context')),
|
|
2304
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
2305
|
+
PRIMARY KEY (entity_id, memory_id),
|
|
2306
|
+
FOREIGN KEY (entity_id) REFERENCES entities(id) ON DELETE CASCADE,
|
|
2307
|
+
FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE CASCADE
|
|
2308
|
+
);
|
|
2309
|
+
INSERT OR IGNORE INTO entity_memories SELECT * FROM _em_fix;
|
|
2310
|
+
DROP TABLE _em_fix;
|
|
2311
|
+
CREATE INDEX IF NOT EXISTS idx_entity_memories_memory ON entity_memories(memory_id);
|
|
2312
|
+
|
|
2313
|
+
-- Fix memory_embeddings FK if broken
|
|
2314
|
+
CREATE TABLE IF NOT EXISTS _me_fix AS SELECT * FROM memory_embeddings;
|
|
2315
|
+
DROP TABLE IF EXISTS memory_embeddings;
|
|
2316
|
+
CREATE TABLE memory_embeddings (
|
|
2317
|
+
memory_id TEXT PRIMARY KEY REFERENCES memories(id) ON DELETE CASCADE,
|
|
2318
|
+
embedding TEXT NOT NULL,
|
|
2319
|
+
model TEXT NOT NULL DEFAULT 'tfidf-512',
|
|
2320
|
+
dimensions INTEGER NOT NULL DEFAULT 512,
|
|
2321
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
2322
|
+
);
|
|
2323
|
+
INSERT OR IGNORE INTO memory_embeddings SELECT * FROM _me_fix;
|
|
2324
|
+
DROP TABLE _me_fix;
|
|
2325
|
+
CREATE INDEX IF NOT EXISTS idx_memory_embeddings_model ON memory_embeddings(model);
|
|
2326
|
+
|
|
2327
|
+
PRAGMA foreign_keys = ON;
|
|
2328
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (29);
|
|
2329
|
+
`,
|
|
2330
|
+
`
|
|
2331
|
+
ALTER TABLE memories ADD COLUMN when_to_use TEXT DEFAULT NULL;
|
|
2332
|
+
CREATE INDEX IF NOT EXISTS idx_memories_when_to_use ON memories(when_to_use) WHERE when_to_use IS NOT NULL;
|
|
2333
|
+
ALTER TABLE memory_versions ADD COLUMN when_to_use TEXT;
|
|
2334
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (30);
|
|
2335
|
+
`,
|
|
2336
|
+
`
|
|
2337
|
+
CREATE TABLE IF NOT EXISTS tool_events (
|
|
2338
|
+
id TEXT PRIMARY KEY,
|
|
2339
|
+
tool_name TEXT NOT NULL,
|
|
2340
|
+
action TEXT,
|
|
2341
|
+
success INTEGER NOT NULL DEFAULT 1,
|
|
2342
|
+
error_type TEXT CHECK(error_type IS NULL OR error_type IN ('timeout', 'permission', 'not_found', 'syntax', 'rate_limit', 'other')),
|
|
2343
|
+
error_message TEXT,
|
|
2344
|
+
tokens_used INTEGER,
|
|
2345
|
+
latency_ms INTEGER,
|
|
2346
|
+
context TEXT,
|
|
2347
|
+
lesson TEXT,
|
|
2348
|
+
when_to_use TEXT,
|
|
2349
|
+
agent_id TEXT REFERENCES agents(id) ON DELETE SET NULL,
|
|
2350
|
+
project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
|
|
2351
|
+
session_id TEXT,
|
|
2352
|
+
metadata TEXT DEFAULT '{}',
|
|
2353
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
2354
|
+
);
|
|
2355
|
+
CREATE INDEX IF NOT EXISTS idx_tool_events_tool_name ON tool_events(tool_name);
|
|
2356
|
+
CREATE INDEX IF NOT EXISTS idx_tool_events_agent ON tool_events(agent_id);
|
|
2357
|
+
CREATE INDEX IF NOT EXISTS idx_tool_events_project ON tool_events(project_id);
|
|
2358
|
+
CREATE INDEX IF NOT EXISTS idx_tool_events_success ON tool_events(success);
|
|
2359
|
+
CREATE INDEX IF NOT EXISTS idx_tool_events_created ON tool_events(created_at);
|
|
2360
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (31);
|
|
2361
|
+
`,
|
|
2362
|
+
`
|
|
2363
|
+
ALTER TABLE memories ADD COLUMN sequence_group TEXT DEFAULT NULL;
|
|
2364
|
+
ALTER TABLE memories ADD COLUMN sequence_order INTEGER DEFAULT NULL;
|
|
2365
|
+
CREATE INDEX IF NOT EXISTS idx_memories_sequence_group ON memories(sequence_group) WHERE sequence_group IS NOT NULL;
|
|
2366
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (32);
|
|
2367
|
+
`,
|
|
2368
|
+
`
|
|
2369
|
+
CREATE INDEX IF NOT EXISTS idx_machines_primary ON machines(is_primary);
|
|
2370
|
+
CREATE TRIGGER IF NOT EXISTS machines_single_primary_insert
|
|
2371
|
+
AFTER INSERT ON machines
|
|
2372
|
+
WHEN NEW.is_primary = 1
|
|
2373
|
+
BEGIN
|
|
2374
|
+
UPDATE machines
|
|
2375
|
+
SET is_primary = 0,
|
|
2376
|
+
last_seen_at = COALESCE(NEW.last_seen_at, datetime('now'))
|
|
2377
|
+
WHERE id != NEW.id AND is_primary = 1;
|
|
2378
|
+
END;
|
|
2379
|
+
CREATE TRIGGER IF NOT EXISTS machines_single_primary_update
|
|
2380
|
+
AFTER UPDATE OF is_primary ON machines
|
|
2381
|
+
WHEN NEW.is_primary = 1
|
|
2382
|
+
BEGIN
|
|
2383
|
+
UPDATE machines
|
|
2384
|
+
SET is_primary = 0,
|
|
2385
|
+
last_seen_at = COALESCE(NEW.last_seen_at, datetime('now'))
|
|
2386
|
+
WHERE id != NEW.id AND is_primary = 1;
|
|
2387
|
+
END;
|
|
2388
|
+
CREATE TRIGGER IF NOT EXISTS machines_prevent_delete_primary
|
|
2389
|
+
BEFORE DELETE ON machines
|
|
2390
|
+
WHEN OLD.is_primary = 1
|
|
2391
|
+
BEGIN
|
|
2392
|
+
SELECT RAISE(ABORT, 'Primary machine cannot be deleted');
|
|
2393
|
+
END;
|
|
2394
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (33);
|
|
2395
|
+
`,
|
|
2396
|
+
`
|
|
2397
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
2398
|
+
id TEXT PRIMARY KEY,
|
|
2399
|
+
subject TEXT NOT NULL,
|
|
2400
|
+
description TEXT DEFAULT '',
|
|
2401
|
+
status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending', 'in_progress', 'completed', 'failed', 'cancelled')),
|
|
2402
|
+
priority TEXT NOT NULL DEFAULT 'medium' CHECK(priority IN ('critical', 'high', 'medium', 'low')),
|
|
2403
|
+
tags TEXT NOT NULL DEFAULT '[]',
|
|
2404
|
+
assigned_agent_id TEXT REFERENCES agents(id) ON DELETE SET NULL,
|
|
2405
|
+
project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
|
|
2406
|
+
session_id TEXT,
|
|
2407
|
+
parent_task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
|
|
2408
|
+
metadata TEXT NOT NULL DEFAULT '{}',
|
|
2409
|
+
progress REAL NOT NULL DEFAULT 0 CHECK(progress >= 0 AND progress <= 1),
|
|
2410
|
+
due_at TEXT,
|
|
2411
|
+
started_at TEXT,
|
|
2412
|
+
completed_at TEXT,
|
|
2413
|
+
failed_at TEXT,
|
|
2414
|
+
error TEXT,
|
|
2415
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
2416
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
2417
|
+
);
|
|
2418
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
|
|
2419
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_priority ON tasks(priority);
|
|
2420
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_agent ON tasks(assigned_agent_id);
|
|
2421
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_project ON tasks(project_id);
|
|
2422
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_session ON tasks(session_id);
|
|
2423
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_parent ON tasks(parent_task_id);
|
|
2424
|
+
|
|
2425
|
+
CREATE TABLE IF NOT EXISTS task_comments (
|
|
2426
|
+
id TEXT PRIMARY KEY,
|
|
2427
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
2428
|
+
agent_id TEXT REFERENCES agents(id) ON DELETE SET NULL,
|
|
2429
|
+
body TEXT NOT NULL,
|
|
2430
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
2431
|
+
);
|
|
2432
|
+
CREATE INDEX IF NOT EXISTS idx_task_comments_task ON task_comments(task_id);
|
|
2433
|
+
CREATE INDEX IF NOT EXISTS idx_task_comments_agent ON task_comments(agent_id);
|
|
2434
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (34);
|
|
2435
|
+
`,
|
|
2436
|
+
`
|
|
2437
|
+
CREATE TABLE IF NOT EXISTS memory_links (
|
|
2438
|
+
id TEXT PRIMARY KEY,
|
|
2439
|
+
source_memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
2440
|
+
target_memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
2441
|
+
relation_type TEXT NOT NULL CHECK(relation_type IN ('summarizes','merged_from','promotes','reflects_on','supersedes','related_to')),
|
|
2442
|
+
run_id TEXT,
|
|
2443
|
+
metadata TEXT NOT NULL DEFAULT '{}',
|
|
2444
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
2445
|
+
);
|
|
2446
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_memory_links_unique ON memory_links(source_memory_id, target_memory_id, relation_type, COALESCE(run_id, ''));
|
|
2447
|
+
CREATE INDEX IF NOT EXISTS idx_memory_links_source ON memory_links(source_memory_id);
|
|
2448
|
+
CREATE INDEX IF NOT EXISTS idx_memory_links_target ON memory_links(target_memory_id);
|
|
2449
|
+
CREATE INDEX IF NOT EXISTS idx_memory_links_relation ON memory_links(relation_type);
|
|
2450
|
+
CREATE INDEX IF NOT EXISTS idx_memory_links_run ON memory_links(run_id);
|
|
2451
|
+
|
|
2452
|
+
CREATE TABLE IF NOT EXISTS memory_consolidation_runs (
|
|
2453
|
+
id TEXT PRIMARY KEY,
|
|
2454
|
+
scope TEXT,
|
|
2455
|
+
project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
|
|
2456
|
+
agent_id TEXT REFERENCES agents(id) ON DELETE SET NULL,
|
|
2457
|
+
dry_run INTEGER NOT NULL DEFAULT 1,
|
|
2458
|
+
status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending','running','completed','failed')),
|
|
2459
|
+
summary TEXT NOT NULL DEFAULT '{}',
|
|
2460
|
+
error TEXT,
|
|
2461
|
+
started_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
2462
|
+
completed_at TEXT
|
|
2463
|
+
);
|
|
2464
|
+
CREATE INDEX IF NOT EXISTS idx_memory_consolidation_runs_project ON memory_consolidation_runs(project_id);
|
|
2465
|
+
CREATE INDEX IF NOT EXISTS idx_memory_consolidation_runs_agent ON memory_consolidation_runs(agent_id);
|
|
2466
|
+
CREATE INDEX IF NOT EXISTS idx_memory_consolidation_runs_status ON memory_consolidation_runs(status);
|
|
2467
|
+
CREATE INDEX IF NOT EXISTS idx_memory_consolidation_runs_started ON memory_consolidation_runs(started_at);
|
|
2468
|
+
|
|
2469
|
+
CREATE TABLE IF NOT EXISTS memory_consolidation_actions (
|
|
2470
|
+
id TEXT PRIMARY KEY,
|
|
2471
|
+
run_id TEXT NOT NULL REFERENCES memory_consolidation_runs(id) ON DELETE CASCADE,
|
|
2472
|
+
action_type TEXT NOT NULL CHECK(action_type IN ('merge_duplicate','promote_semantic','summarize_cluster','decay_forget')),
|
|
2473
|
+
memory_ids TEXT NOT NULL DEFAULT '[]',
|
|
2474
|
+
target_memory_id TEXT REFERENCES memories(id) ON DELETE SET NULL,
|
|
2475
|
+
created_memory_id TEXT REFERENCES memories(id) ON DELETE SET NULL,
|
|
2476
|
+
reason TEXT NOT NULL,
|
|
2477
|
+
planned_changes TEXT NOT NULL DEFAULT '{}',
|
|
2478
|
+
applied INTEGER NOT NULL DEFAULT 0,
|
|
2479
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
2480
|
+
);
|
|
2481
|
+
CREATE INDEX IF NOT EXISTS idx_memory_consolidation_actions_run ON memory_consolidation_actions(run_id);
|
|
2482
|
+
CREATE INDEX IF NOT EXISTS idx_memory_consolidation_actions_type ON memory_consolidation_actions(action_type);
|
|
2483
|
+
CREATE INDEX IF NOT EXISTS idx_memory_consolidation_actions_target ON memory_consolidation_actions(target_memory_id);
|
|
2484
|
+
|
|
2485
|
+
CREATE TABLE IF NOT EXISTS memory_reflection_runs (
|
|
2486
|
+
id TEXT PRIMARY KEY,
|
|
2487
|
+
on_type TEXT NOT NULL CHECK(on_type IN ('session','task','range')),
|
|
2488
|
+
source TEXT,
|
|
2489
|
+
project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
|
|
2490
|
+
agent_id TEXT REFERENCES agents(id) ON DELETE SET NULL,
|
|
2491
|
+
dry_run INTEGER NOT NULL DEFAULT 1,
|
|
2492
|
+
provider TEXT,
|
|
2493
|
+
model TEXT,
|
|
2494
|
+
status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending','running','completed','failed')),
|
|
2495
|
+
trajectory_memory_ids TEXT NOT NULL DEFAULT '[]',
|
|
2496
|
+
summary TEXT,
|
|
2497
|
+
error TEXT,
|
|
2498
|
+
started_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
2499
|
+
completed_at TEXT
|
|
2500
|
+
);
|
|
2501
|
+
CREATE INDEX IF NOT EXISTS idx_memory_reflection_runs_source ON memory_reflection_runs(on_type, source);
|
|
2502
|
+
CREATE INDEX IF NOT EXISTS idx_memory_reflection_runs_project ON memory_reflection_runs(project_id);
|
|
2503
|
+
CREATE INDEX IF NOT EXISTS idx_memory_reflection_runs_agent ON memory_reflection_runs(agent_id);
|
|
2504
|
+
CREATE INDEX IF NOT EXISTS idx_memory_reflection_runs_status ON memory_reflection_runs(status);
|
|
2505
|
+
|
|
2506
|
+
CREATE TABLE IF NOT EXISTS memory_reflection_lessons (
|
|
2507
|
+
id TEXT PRIMARY KEY,
|
|
2508
|
+
run_id TEXT NOT NULL REFERENCES memory_reflection_runs(id) ON DELETE CASCADE,
|
|
2509
|
+
memory_id TEXT REFERENCES memories(id) ON DELETE SET NULL,
|
|
2510
|
+
kind TEXT NOT NULL CHECK(kind IN ('worked','failed','do_differently')),
|
|
2511
|
+
lesson TEXT NOT NULL,
|
|
2512
|
+
evidence TEXT NOT NULL DEFAULT '[]',
|
|
2513
|
+
importance INTEGER NOT NULL DEFAULT 6 CHECK(importance >= 1 AND importance <= 10),
|
|
2514
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
2515
|
+
);
|
|
2516
|
+
CREATE INDEX IF NOT EXISTS idx_memory_reflection_lessons_run ON memory_reflection_lessons(run_id);
|
|
2517
|
+
CREATE INDEX IF NOT EXISTS idx_memory_reflection_lessons_memory ON memory_reflection_lessons(memory_id);
|
|
2518
|
+
CREATE INDEX IF NOT EXISTS idx_memory_reflection_lessons_kind ON memory_reflection_lessons(kind);
|
|
2519
|
+
|
|
2520
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (35);
|
|
2521
|
+
`,
|
|
2522
|
+
`
|
|
2523
|
+
${MEMORY_VERSION_SNAPSHOT_TRIGGER}
|
|
2524
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (36);
|
|
2525
|
+
`,
|
|
2526
|
+
`
|
|
2527
|
+
DROP TRIGGER IF EXISTS audit_memory_insert;
|
|
2528
|
+
CREATE TRIGGER audit_memory_insert AFTER INSERT ON memories BEGIN
|
|
2529
|
+
INSERT INTO memory_audit_log (id, memory_id, memory_key, operation, agent_id, created_at)
|
|
2530
|
+
VALUES (hex(randomblob(4)), new.id, new.key, 'create', new.agent_id, datetime('now'));
|
|
2531
|
+
END;
|
|
2532
|
+
|
|
2533
|
+
DROP TRIGGER IF EXISTS audit_memory_update;
|
|
2534
|
+
CREATE TRIGGER audit_memory_update AFTER UPDATE ON memories BEGIN
|
|
2535
|
+
INSERT INTO memory_audit_log (id, memory_id, memory_key, operation, agent_id, changes, created_at)
|
|
2536
|
+
VALUES (hex(randomblob(4)), new.id, new.key, 'update', new.agent_id,
|
|
2537
|
+
json_object('version_from', old.version, 'version_to', new.version, 'importance_from', old.importance, 'importance_to', new.importance),
|
|
2538
|
+
datetime('now'));
|
|
2539
|
+
END;
|
|
2540
|
+
|
|
2541
|
+
DROP TRIGGER IF EXISTS audit_memory_delete;
|
|
2542
|
+
CREATE TRIGGER audit_memory_delete AFTER DELETE ON memories BEGIN
|
|
2543
|
+
INSERT INTO memory_audit_log (id, memory_id, memory_key, operation, agent_id, created_at)
|
|
2544
|
+
VALUES (hex(randomblob(4)), old.id, old.key, 'delete', old.agent_id, datetime('now'));
|
|
2545
|
+
END;
|
|
2546
|
+
|
|
2547
|
+
UPDATE memory_audit_log SET old_value_hash = NULL
|
|
2548
|
+
WHERE old_value_hash IS NOT NULL
|
|
2549
|
+
AND length(old_value_hash) = 32
|
|
2550
|
+
AND old_value_hash NOT GLOB '*[^0-9A-F]*';
|
|
2551
|
+
|
|
2552
|
+
UPDATE memory_audit_log SET new_value_hash = NULL
|
|
2553
|
+
WHERE new_value_hash IS NOT NULL
|
|
2554
|
+
AND length(new_value_hash) = 32
|
|
2555
|
+
AND new_value_hash NOT GLOB '*[^0-9A-F]*';
|
|
2556
|
+
|
|
2557
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (37);
|
|
2558
|
+
`,
|
|
2559
|
+
`
|
|
2560
|
+
${sqliteMementosProjectRegistrationSchemaSql()}
|
|
2561
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (38);
|
|
2562
|
+
`,
|
|
2563
|
+
`
|
|
2564
|
+
${sqliteMementosProjectGuardedUpdateSchemaSql()}
|
|
2565
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (39);
|
|
2566
|
+
`,
|
|
2567
|
+
`
|
|
2568
|
+
${sqliteMementosMemoryProjectLinkSchemaSql()}
|
|
2569
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (40);
|
|
2570
|
+
`
|
|
2571
|
+
];
|
|
2572
|
+
});
|
|
2573
|
+
|
|
2574
|
+
// src/db/database.ts
|
|
2575
|
+
var exports_database = {};
|
|
2576
|
+
__export(exports_database, {
|
|
2577
|
+
uuid: () => uuid,
|
|
2578
|
+
shortUuid: () => shortUuid,
|
|
2579
|
+
resolvePartialId: () => resolvePartialId,
|
|
2580
|
+
resetDatabase: () => resetDatabase,
|
|
2581
|
+
now: () => now,
|
|
2582
|
+
getDbPath: () => getDbPath,
|
|
2583
|
+
getDatabase: () => getDatabase,
|
|
2584
|
+
escapeLikePrefix: () => escapeLikePrefix,
|
|
2585
|
+
closeDatabase: () => closeDatabase
|
|
2586
|
+
});
|
|
2587
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, cpSync } from "fs";
|
|
2588
|
+
import { dirname as dirname2, join as join4, resolve } from "path";
|
|
2589
|
+
function isInMemoryDb(path) {
|
|
2590
|
+
return path === ":memory:" || path.startsWith("file::memory:");
|
|
2591
|
+
}
|
|
2592
|
+
function findNearestMementosDb(startDir) {
|
|
2593
|
+
let dir = resolve(startDir);
|
|
2594
|
+
const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
2595
|
+
const legacyHomeDb = resolve(home, ".mementos", "mementos.db");
|
|
2596
|
+
while (true) {
|
|
2597
|
+
const candidate = join4(dir, ".mementos", "mementos.db");
|
|
2598
|
+
if (existsSync2(candidate) && resolve(candidate) !== legacyHomeDb)
|
|
2599
|
+
return candidate;
|
|
2600
|
+
const parent = dirname2(dir);
|
|
2601
|
+
if (parent === dir)
|
|
2602
|
+
break;
|
|
2603
|
+
dir = parent;
|
|
2604
|
+
}
|
|
2605
|
+
return null;
|
|
2606
|
+
}
|
|
2607
|
+
function findGitRoot(startDir) {
|
|
2608
|
+
let dir = resolve(startDir);
|
|
2609
|
+
while (true) {
|
|
2610
|
+
if (existsSync2(join4(dir, ".git")))
|
|
2611
|
+
return dir;
|
|
2612
|
+
const parent = dirname2(dir);
|
|
2613
|
+
if (parent === dir)
|
|
2614
|
+
break;
|
|
2615
|
+
dir = parent;
|
|
2616
|
+
}
|
|
2617
|
+
return null;
|
|
2618
|
+
}
|
|
2619
|
+
function migrateGlobalDir() {
|
|
2620
|
+
const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
2621
|
+
const newDir = join4(home, ".hasna", "mementos");
|
|
2622
|
+
const oldDir = join4(home, ".mementos");
|
|
2623
|
+
if (!existsSync2(newDir) && existsSync2(oldDir)) {
|
|
2624
|
+
mkdirSync2(join4(home, ".hasna"), { recursive: true });
|
|
2625
|
+
cpSync(oldDir, newDir, { recursive: true });
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
function getDbPath() {
|
|
2629
|
+
for (const key of DB_PATH_ENV_KEYS) {
|
|
2630
|
+
const envPath = process.env[key]?.trim();
|
|
2631
|
+
if (envPath)
|
|
2632
|
+
return envPath;
|
|
2633
|
+
}
|
|
2634
|
+
const cwd = process.cwd();
|
|
2635
|
+
const nearest = findNearestMementosDb(cwd);
|
|
2636
|
+
if (nearest)
|
|
2637
|
+
return nearest;
|
|
2638
|
+
if (process.env["MEMENTOS_DB_SCOPE"] === "project") {
|
|
2639
|
+
const gitRoot = findGitRoot(cwd);
|
|
2640
|
+
if (gitRoot) {
|
|
2641
|
+
return join4(gitRoot, ".mementos", "mementos.db");
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
migrateGlobalDir();
|
|
2645
|
+
const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
2646
|
+
return join4(home, ".hasna", "mementos", "mementos.db");
|
|
2647
|
+
}
|
|
2648
|
+
function ensureDir(filePath) {
|
|
2649
|
+
if (isInMemoryDb(filePath))
|
|
2650
|
+
return;
|
|
2651
|
+
const dir = dirname2(resolve(filePath));
|
|
2652
|
+
if (!existsSync2(dir)) {
|
|
2653
|
+
mkdirSync2(dir, { recursive: true });
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
function getCloudDatabase() {
|
|
2657
|
+
if (_pg)
|
|
2658
|
+
return _pg;
|
|
2659
|
+
if (!isServerContext()) {
|
|
2660
|
+
throw new Error("Direct Postgres (cloud) storage is server-only. A client must use the " + "self-hosted HTTP API (HASNA_MEMENTOS_API_URL + HASNA_MEMENTOS_API_KEY), " + "never a database DSN. Unset HASNA_MEMENTOS_DATABASE_URL / HASNA_MEMENTOS_STORAGE_MODE " + "on this machine to use local SQLite, or configure the API client to reach the cloud.");
|
|
2661
|
+
}
|
|
2662
|
+
const connectionString = getStorageConnectionString();
|
|
2663
|
+
_pg = new PgAdapter(connectionString);
|
|
2664
|
+
return _pg;
|
|
2665
|
+
}
|
|
2666
|
+
function getDatabase(dbPath) {
|
|
2667
|
+
if (!dbPath) {
|
|
2668
|
+
if (_pg)
|
|
2669
|
+
return _pg;
|
|
2670
|
+
if (isApiMode()) {
|
|
2671
|
+
throw new Error("mementos is in API mode (HASNA_MEMENTOS_API_URL + HASNA_MEMENTOS_API_KEY set) " + "but this operation tried to open a local SQLite database. That would create a " + "split-brain local island instead of using the shared cloud store. This op must " + "route through the API client (src/db/api-mode.ts); if it needs a server endpoint " + "that does not exist yet, add it to src/server and redeploy. To use local storage " + "instead, unset HASNA_MEMENTOS_API_URL / HASNA_MEMENTOS_API_KEY.");
|
|
2672
|
+
}
|
|
2673
|
+
if (getStorageMode() === "cloud") {
|
|
2674
|
+
return getCloudDatabase();
|
|
2675
|
+
}
|
|
2676
|
+
if (process.env["NODE_ENV"] === "test") {
|
|
2677
|
+
const hasExplicitDbPath = DB_PATH_ENV_KEYS.some((key) => process.env[key]?.trim());
|
|
2678
|
+
if (!hasExplicitDbPath) {
|
|
2679
|
+
throw new Error("REFUSING-UNPINNED-TEST-OPEN: a test process tried to open the default local " + `SQLite store (${getDbPath()}) with no explicit dbPath argument and no ` + `${DB_PATH_ENV_KEYS.join("/")} set. Pass an explicit dbPath (e.g. ":memory:" ` + "or a scratch file), or set one of those env keys to a scratch path, before " + "calling getDatabase(). This guard exists because an unpinned test process " + "would otherwise silently read and write the real, shared, on-disk memory " + "store (todos 57b8b8c5).");
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
2683
|
+
const path = dbPath || getDbPath();
|
|
2684
|
+
if (_db) {
|
|
2685
|
+
if (_dbPath === path)
|
|
2686
|
+
return _db;
|
|
2687
|
+
_db.close();
|
|
2688
|
+
_db = null;
|
|
2689
|
+
}
|
|
2690
|
+
_dbPath = path;
|
|
2691
|
+
ensureDir(path);
|
|
2692
|
+
_db = new SqliteAdapter(path);
|
|
2693
|
+
_db.run("PRAGMA journal_mode = WAL");
|
|
2694
|
+
_db.run("PRAGMA busy_timeout = 5000");
|
|
2695
|
+
_db.run("PRAGMA foreign_keys = ON");
|
|
2696
|
+
_db.run("PRAGMA wal_autocheckpoint = 100");
|
|
2697
|
+
runMigrations(_db);
|
|
2698
|
+
_db.run(`CREATE TABLE IF NOT EXISTS feedback (
|
|
2699
|
+
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
|
|
2700
|
+
message TEXT NOT NULL,
|
|
2701
|
+
email TEXT,
|
|
2702
|
+
category TEXT DEFAULT 'general',
|
|
2703
|
+
version TEXT,
|
|
2704
|
+
machine_id TEXT,
|
|
2705
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
2706
|
+
)`);
|
|
2707
|
+
return _db;
|
|
2708
|
+
}
|
|
2709
|
+
function runMigrations(db) {
|
|
2710
|
+
try {
|
|
2711
|
+
const result = db.query("SELECT MAX(id) as max_id FROM _migrations").get();
|
|
2712
|
+
const currentLevel = result?.max_id ?? 0;
|
|
2713
|
+
for (let i = currentLevel;i < MIGRATIONS.length; i++) {
|
|
2714
|
+
try {
|
|
2715
|
+
applyMigration(db, i);
|
|
2716
|
+
} catch (e) {
|
|
2717
|
+
console.warn(`[mementos] Migration ${i + 1} failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
} catch {
|
|
2721
|
+
for (let i = 0;i < MIGRATIONS.length; i++) {
|
|
2722
|
+
try {
|
|
2723
|
+
applyMigration(db, i);
|
|
2724
|
+
} catch (e) {
|
|
2725
|
+
console.warn(`[mementos] Migration ${i + 1} failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
function applyMigration(db, index) {
|
|
2731
|
+
if (index === 32) {
|
|
2732
|
+
ensureMachinePrimaryColumn(db);
|
|
2733
|
+
}
|
|
2734
|
+
db.exec(MIGRATIONS[index]);
|
|
2735
|
+
}
|
|
2736
|
+
function ensureMachinePrimaryColumn(db) {
|
|
2737
|
+
const columns = db.query("PRAGMA table_info(machines)").all();
|
|
2738
|
+
if (columns.length === 0 || columns.some((column) => column.name === "is_primary")) {
|
|
2739
|
+
return;
|
|
2740
|
+
}
|
|
2741
|
+
db.exec("ALTER TABLE machines ADD COLUMN is_primary INTEGER NOT NULL DEFAULT 0");
|
|
2742
|
+
}
|
|
2743
|
+
function closeDatabase() {
|
|
2744
|
+
if (_db) {
|
|
2745
|
+
_db.close();
|
|
2746
|
+
_db = null;
|
|
2747
|
+
}
|
|
2748
|
+
if (_pg) {
|
|
2749
|
+
_pg.close();
|
|
2750
|
+
_pg = null;
|
|
2751
|
+
}
|
|
2752
|
+
}
|
|
2753
|
+
function resetDatabase() {
|
|
2754
|
+
_db = null;
|
|
2755
|
+
_pg = null;
|
|
2756
|
+
}
|
|
2757
|
+
function now() {
|
|
2758
|
+
return new Date().toISOString();
|
|
2759
|
+
}
|
|
2760
|
+
function uuid() {
|
|
2761
|
+
return crypto.randomUUID();
|
|
2762
|
+
}
|
|
2763
|
+
function shortUuid() {
|
|
2764
|
+
return crypto.randomUUID().slice(0, 8);
|
|
2765
|
+
}
|
|
2766
|
+
function escapeLikePrefix(s) {
|
|
2767
|
+
return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
2768
|
+
}
|
|
2769
|
+
function resolvePartialId(db, table, partialId) {
|
|
2770
|
+
if (!ALLOWED_TABLES.has(table)) {
|
|
2771
|
+
throw new Error(`Invalid table name: ${table}`);
|
|
2772
|
+
}
|
|
2773
|
+
if (partialId === "")
|
|
2774
|
+
return null;
|
|
2775
|
+
if (partialId.length >= 36) {
|
|
2776
|
+
const row = db.query(`SELECT id FROM ${table} WHERE id = ?`).get(partialId);
|
|
2777
|
+
return row?.id ?? null;
|
|
2778
|
+
}
|
|
2779
|
+
const rows = db.query(`SELECT id FROM ${table} WHERE id LIKE ? ESCAPE '\\'`).all(`${escapeLikePrefix(partialId)}%`);
|
|
2780
|
+
if (rows.length === 1) {
|
|
2781
|
+
return rows[0].id;
|
|
2782
|
+
}
|
|
2783
|
+
return null;
|
|
2784
|
+
}
|
|
2785
|
+
var _db = null, _dbPath = null, _pg = null, ALLOWED_TABLES;
|
|
2786
|
+
var init_database = __esm(() => {
|
|
2787
|
+
init_storage();
|
|
2788
|
+
init_api_mode();
|
|
2789
|
+
init_migrations();
|
|
2790
|
+
ALLOWED_TABLES = new Set([
|
|
2791
|
+
"memories",
|
|
2792
|
+
"agents",
|
|
2793
|
+
"entities",
|
|
2794
|
+
"projects",
|
|
2795
|
+
"relations",
|
|
2796
|
+
"memory_audit_log",
|
|
2797
|
+
"locks",
|
|
2798
|
+
"sessions",
|
|
2799
|
+
"session_memory_jobs",
|
|
2800
|
+
"synthesis_runs",
|
|
2801
|
+
"synthesis_proposals",
|
|
2802
|
+
"tool_events",
|
|
2803
|
+
"webhook_hooks"
|
|
2804
|
+
]);
|
|
2805
|
+
});
|
|
2806
|
+
|
|
322
2807
|
// src/project-registration/authority.ts
|
|
323
|
-
import { createHash } from "crypto";
|
|
324
|
-
import { resolve } from "path";
|
|
2808
|
+
import { createHash as createHash2 } from "crypto";
|
|
2809
|
+
import { resolve as resolve2 } from "path";
|
|
325
2810
|
|
|
326
2811
|
// src/lib/package-version.ts
|
|
327
|
-
import { readFileSync } from "fs";
|
|
328
|
-
import { dirname, join } from "path";
|
|
329
|
-
import { fileURLToPath } from "url";
|
|
2812
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
2813
|
+
import { dirname, join as join2 } from "path";
|
|
2814
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
330
2815
|
function getMementosPackageVersion() {
|
|
331
|
-
const here = dirname(
|
|
2816
|
+
const here = dirname(fileURLToPath2(import.meta.url));
|
|
332
2817
|
for (const candidate of [
|
|
333
|
-
|
|
334
|
-
|
|
2818
|
+
join2(here, "..", "..", "package.json"),
|
|
2819
|
+
join2(here, "..", "package.json")
|
|
335
2820
|
]) {
|
|
336
2821
|
try {
|
|
337
|
-
const parsed = JSON.parse(
|
|
2822
|
+
const parsed = JSON.parse(readFileSync2(candidate, "utf8"));
|
|
338
2823
|
if (typeof parsed.version === "string" && parsed.version.trim())
|
|
339
2824
|
return parsed.version;
|
|
340
2825
|
} catch {}
|
|
@@ -342,6 +2827,508 @@ function getMementosPackageVersion() {
|
|
|
342
2827
|
return "0.0.0";
|
|
343
2828
|
}
|
|
344
2829
|
|
|
2830
|
+
// src/db/projects.ts
|
|
2831
|
+
init_database();
|
|
2832
|
+
init_api_mode();
|
|
2833
|
+
import { createHash } from "crypto";
|
|
2834
|
+
|
|
2835
|
+
// src/project-registration/types.ts
|
|
2836
|
+
var MEMENTOS_PROJECT_REGISTRATION_ROUTE = "mementos.project-registration.v1";
|
|
2837
|
+
var MEMENTOS_PROJECT_REGISTRATION_CALLER_ROUTE = "projects.full-registration.v1";
|
|
2838
|
+
var MEMENTOS_PROJECT_REGISTRATION_SCHEMA_VERSION = 1;
|
|
2839
|
+
var MEMENTOS_PROJECT_GUARDED_UPDATE_ROUTE = "mementos.project-guarded-update.v1";
|
|
2840
|
+
|
|
2841
|
+
class MementosProjectRegistrationError extends Error {
|
|
2842
|
+
code;
|
|
2843
|
+
details;
|
|
2844
|
+
constructor(code, message, details = {}) {
|
|
2845
|
+
super(message);
|
|
2846
|
+
this.code = code;
|
|
2847
|
+
this.details = details;
|
|
2848
|
+
this.name = "MementosProjectRegistrationError";
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
|
|
2852
|
+
// src/db/projects.ts
|
|
2853
|
+
function parseProjectRow(row) {
|
|
2854
|
+
return {
|
|
2855
|
+
id: row["id"],
|
|
2856
|
+
name: row["name"],
|
|
2857
|
+
path: row["path"],
|
|
2858
|
+
description: row["description"] || null,
|
|
2859
|
+
memory_prefix: row["memory_prefix"] || null,
|
|
2860
|
+
created_at: row["created_at"],
|
|
2861
|
+
updated_at: row["updated_at"]
|
|
2862
|
+
};
|
|
2863
|
+
}
|
|
2864
|
+
|
|
2865
|
+
class ProjectCollisionError extends Error {
|
|
2866
|
+
field;
|
|
2867
|
+
value;
|
|
2868
|
+
constructor(field, value) {
|
|
2869
|
+
super(`Project ${field} already exists: ${value}`);
|
|
2870
|
+
this.field = field;
|
|
2871
|
+
this.value = value;
|
|
2872
|
+
this.name = "ProjectCollisionError";
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2875
|
+
|
|
2876
|
+
class ProjectGuardedUpdateError extends Error {
|
|
2877
|
+
code;
|
|
2878
|
+
details;
|
|
2879
|
+
constructor(code, message, details = {}) {
|
|
2880
|
+
super(message);
|
|
2881
|
+
this.code = code;
|
|
2882
|
+
this.details = details;
|
|
2883
|
+
this.name = "ProjectGuardedUpdateError";
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
var PROJECT_UPDATE_AUTHORITY = {
|
|
2887
|
+
authority_id: "mementos",
|
|
2888
|
+
tenant_id: "default",
|
|
2889
|
+
corpus_id: "default"
|
|
2890
|
+
};
|
|
2891
|
+
var BOUNDED_IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$/;
|
|
2892
|
+
function canonicalizeProjectUpdateValue(value) {
|
|
2893
|
+
if (Array.isArray(value))
|
|
2894
|
+
return value.map(canonicalizeProjectUpdateValue);
|
|
2895
|
+
if (!value || typeof value !== "object")
|
|
2896
|
+
return value;
|
|
2897
|
+
const output = {};
|
|
2898
|
+
for (const key of Object.keys(value).sort()) {
|
|
2899
|
+
const item = value[key];
|
|
2900
|
+
if (item !== undefined)
|
|
2901
|
+
output[key] = canonicalizeProjectUpdateValue(item);
|
|
2902
|
+
}
|
|
2903
|
+
return output;
|
|
2904
|
+
}
|
|
2905
|
+
function canonicalProjectUpdateJson(value) {
|
|
2906
|
+
return JSON.stringify(canonicalizeProjectUpdateValue(value));
|
|
2907
|
+
}
|
|
2908
|
+
function digestProjectUpdateValue(value) {
|
|
2909
|
+
return createHash("sha256").update(canonicalProjectUpdateJson(value)).digest("hex");
|
|
2910
|
+
}
|
|
2911
|
+
function timestampString(value) {
|
|
2912
|
+
return value instanceof Date ? value.toISOString() : String(value);
|
|
2913
|
+
}
|
|
2914
|
+
function parseProjectJson(value) {
|
|
2915
|
+
const parsed = typeof value === "string" ? JSON.parse(value) : value;
|
|
2916
|
+
return parsed;
|
|
2917
|
+
}
|
|
2918
|
+
function projectUpdateReceiptFromRow(row) {
|
|
2919
|
+
return {
|
|
2920
|
+
receipt_id: String(row["receipt_id"]),
|
|
2921
|
+
authority: "mementos",
|
|
2922
|
+
route: MEMENTOS_PROJECT_GUARDED_UPDATE_ROUTE,
|
|
2923
|
+
package_version: String(row["package_version"]),
|
|
2924
|
+
authority_id: String(row["authority_id"]),
|
|
2925
|
+
tenant_id: String(row["tenant_id"]),
|
|
2926
|
+
corpus_id: String(row["corpus_id"]),
|
|
2927
|
+
operation_id: String(row["operation_id"]),
|
|
2928
|
+
step_id: String(row["step_id"]),
|
|
2929
|
+
direction: row["direction"],
|
|
2930
|
+
idempotency_key: String(row["idempotency_key"]),
|
|
2931
|
+
request_digest: String(row["request_digest"]),
|
|
2932
|
+
outcome: "accepted",
|
|
2933
|
+
target_id: String(row["target_id"]),
|
|
2934
|
+
expected_revision: timestampString(row["expected_revision"]),
|
|
2935
|
+
result_revision: timestampString(row["result_revision"]),
|
|
2936
|
+
result_digest: String(row["result_digest"]),
|
|
2937
|
+
accepted_receipt_id: row["accepted_receipt_id"] === null ? null : String(row["accepted_receipt_id"]),
|
|
2938
|
+
before_project: parseProjectJson(row["before_project_json"]),
|
|
2939
|
+
after_project: parseProjectJson(row["after_project_json"]),
|
|
2940
|
+
created_at: timestampString(row["created_at"])
|
|
2941
|
+
};
|
|
2942
|
+
}
|
|
2943
|
+
function normalizeProjectUpdateInput(input) {
|
|
2944
|
+
const normalized = {};
|
|
2945
|
+
if (input.name !== undefined)
|
|
2946
|
+
normalized.name = input.name.trim();
|
|
2947
|
+
if (input.path !== undefined)
|
|
2948
|
+
normalized.path = input.path.trim();
|
|
2949
|
+
if (input.description !== undefined)
|
|
2950
|
+
normalized.description = input.description;
|
|
2951
|
+
if (input.memory_prefix !== undefined)
|
|
2952
|
+
normalized.memory_prefix = input.memory_prefix;
|
|
2953
|
+
if (Object.keys(normalized).length === 0) {
|
|
2954
|
+
throw new ProjectGuardedUpdateError("PROJECT_UPDATE_INVALID_INPUT", "At least one project field must be provided");
|
|
2955
|
+
}
|
|
2956
|
+
if (normalized.name !== undefined && normalized.name.length === 0) {
|
|
2957
|
+
throw new ProjectGuardedUpdateError("PROJECT_UPDATE_INVALID_INPUT", "Project name cannot be empty");
|
|
2958
|
+
}
|
|
2959
|
+
if (normalized.path !== undefined && normalized.path.length === 0) {
|
|
2960
|
+
throw new ProjectGuardedUpdateError("PROJECT_UPDATE_INVALID_INPUT", "Project path cannot be empty");
|
|
2961
|
+
}
|
|
2962
|
+
return normalized;
|
|
2963
|
+
}
|
|
2964
|
+
function assertProjectUpdateIdentity(identity, expectedIdentity = PROJECT_UPDATE_AUTHORITY) {
|
|
2965
|
+
if (identity.authority_id !== expectedIdentity.authority_id || identity.tenant_id !== expectedIdentity.tenant_id || identity.corpus_id !== expectedIdentity.corpus_id) {
|
|
2966
|
+
throw new ProjectGuardedUpdateError("PROJECT_UPDATE_AUTHORITY_MISMATCH", "guarded project update does not match this authority, tenant, and corpus");
|
|
2967
|
+
}
|
|
2968
|
+
}
|
|
2969
|
+
function assertBoundedIdentifier(value, field) {
|
|
2970
|
+
if (!BOUNDED_IDENTIFIER.test(value)) {
|
|
2971
|
+
throw new ProjectGuardedUpdateError("PROJECT_UPDATE_INVALID_INPUT", `${field} must be an 8-128 character bounded identifier`);
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
function assertProjectUpdateRequest(request, expectedIdentity = PROJECT_UPDATE_AUTHORITY) {
|
|
2975
|
+
assertProjectUpdateIdentity(request, expectedIdentity);
|
|
2976
|
+
assertBoundedIdentifier(request.operation_id, "operation_id");
|
|
2977
|
+
assertBoundedIdentifier(request.step_id, "step_id");
|
|
2978
|
+
assertBoundedIdentifier(request.idempotency_key, "idempotency_key");
|
|
2979
|
+
if (!request.expected_revision || request.expected_revision.length > 128) {
|
|
2980
|
+
throw new ProjectGuardedUpdateError("PROJECT_UPDATE_INVALID_INPUT", "expected_revision is required and must be bounded");
|
|
2981
|
+
}
|
|
2982
|
+
}
|
|
2983
|
+
function getProjectByExactId(id, db) {
|
|
2984
|
+
const row = db.query("SELECT * FROM projects WHERE id = ? LIMIT 1").get(id);
|
|
2985
|
+
return row ? parseProjectRow(row) : null;
|
|
2986
|
+
}
|
|
2987
|
+
function assertNoProjectCollision(id, input, db) {
|
|
2988
|
+
if (input.name !== undefined) {
|
|
2989
|
+
const collision = db.query("SELECT id FROM projects WHERE LOWER(name) = LOWER(?) AND id != ? LIMIT 1").get(input.name, id);
|
|
2990
|
+
if (collision) {
|
|
2991
|
+
throw new ProjectGuardedUpdateError("PROJECT_UPDATE_COLLISION", `Project name already exists: ${input.name}`, { field: "name" });
|
|
2992
|
+
}
|
|
2993
|
+
}
|
|
2994
|
+
if (input.path !== undefined) {
|
|
2995
|
+
const collision = db.query("SELECT id FROM projects WHERE path = ? AND id != ? LIMIT 1").get(input.path, id);
|
|
2996
|
+
if (collision) {
|
|
2997
|
+
throw new ProjectGuardedUpdateError("PROJECT_UPDATE_COLLISION", `Project path already exists: ${input.path}`, { field: "path" });
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
3000
|
+
}
|
|
3001
|
+
function nextProjectRevision(previous) {
|
|
3002
|
+
const candidate = now();
|
|
3003
|
+
const previousMs = Date.parse(previous);
|
|
3004
|
+
const candidateMs = Date.parse(candidate);
|
|
3005
|
+
if (Number.isFinite(previousMs) && Number.isFinite(candidateMs) && candidateMs <= previousMs) {
|
|
3006
|
+
return new Date(previousMs + 1).toISOString();
|
|
3007
|
+
}
|
|
3008
|
+
return candidate;
|
|
3009
|
+
}
|
|
3010
|
+
function projectWithUpdates(project, updates, revision) {
|
|
3011
|
+
return {
|
|
3012
|
+
...project,
|
|
3013
|
+
...updates.name !== undefined ? { name: updates.name } : {},
|
|
3014
|
+
...updates.path !== undefined ? { path: updates.path } : {},
|
|
3015
|
+
...updates.description !== undefined ? { description: updates.description } : {},
|
|
3016
|
+
...updates.memory_prefix !== undefined ? { memory_prefix: updates.memory_prefix } : {},
|
|
3017
|
+
updated_at: revision
|
|
3018
|
+
};
|
|
3019
|
+
}
|
|
3020
|
+
function findProjectUpdateReceiptByKey(db, identity, direction, idempotencyKey) {
|
|
3021
|
+
const row = db.query(`
|
|
3022
|
+
SELECT * FROM mementos_project_update_receipts
|
|
3023
|
+
WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
|
|
3024
|
+
AND direction = ? AND idempotency_key = ?
|
|
3025
|
+
LIMIT 1
|
|
3026
|
+
`).get(identity.authority_id, identity.tenant_id, identity.corpus_id, direction, idempotencyKey);
|
|
3027
|
+
return row ? projectUpdateReceiptFromRow(row) : null;
|
|
3028
|
+
}
|
|
3029
|
+
function findProjectUpdateReceiptById(db, identity, receiptId) {
|
|
3030
|
+
const row = db.query(`
|
|
3031
|
+
SELECT * FROM mementos_project_update_receipts
|
|
3032
|
+
WHERE receipt_id = ? AND authority_id = ? AND tenant_id = ? AND corpus_id = ?
|
|
3033
|
+
LIMIT 1
|
|
3034
|
+
`).get(receiptId, identity.authority_id, identity.tenant_id, identity.corpus_id);
|
|
3035
|
+
return row ? projectUpdateReceiptFromRow(row) : null;
|
|
3036
|
+
}
|
|
3037
|
+
function insertProjectUpdateReceipt(db, receipt) {
|
|
3038
|
+
db.run(`
|
|
3039
|
+
INSERT INTO mementos_project_update_receipts (
|
|
3040
|
+
receipt_id, authority, route, package_version, authority_id, tenant_id,
|
|
3041
|
+
corpus_id, operation_id, step_id, direction, idempotency_key,
|
|
3042
|
+
request_digest, outcome, target_id, expected_revision, result_revision,
|
|
3043
|
+
result_digest, accepted_receipt_id, before_project_json,
|
|
3044
|
+
after_project_json, created_at
|
|
3045
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
3046
|
+
`, [
|
|
3047
|
+
receipt.receipt_id,
|
|
3048
|
+
receipt.authority,
|
|
3049
|
+
receipt.route,
|
|
3050
|
+
receipt.package_version,
|
|
3051
|
+
receipt.authority_id,
|
|
3052
|
+
receipt.tenant_id,
|
|
3053
|
+
receipt.corpus_id,
|
|
3054
|
+
receipt.operation_id,
|
|
3055
|
+
receipt.step_id,
|
|
3056
|
+
receipt.direction,
|
|
3057
|
+
receipt.idempotency_key,
|
|
3058
|
+
receipt.request_digest,
|
|
3059
|
+
receipt.outcome,
|
|
3060
|
+
receipt.target_id,
|
|
3061
|
+
receipt.expected_revision,
|
|
3062
|
+
receipt.result_revision,
|
|
3063
|
+
receipt.result_digest,
|
|
3064
|
+
receipt.accepted_receipt_id,
|
|
3065
|
+
canonicalProjectUpdateJson(receipt.before_project),
|
|
3066
|
+
canonicalProjectUpdateJson(receipt.after_project),
|
|
3067
|
+
receipt.created_at
|
|
3068
|
+
]);
|
|
3069
|
+
}
|
|
3070
|
+
function makeProjectUpdateReceipt(input) {
|
|
3071
|
+
const createdAt = now();
|
|
3072
|
+
const logical = {
|
|
3073
|
+
authority: "mementos",
|
|
3074
|
+
route: MEMENTOS_PROJECT_GUARDED_UPDATE_ROUTE,
|
|
3075
|
+
package_version: getMementosPackageVersion(),
|
|
3076
|
+
authority_id: input.request.authority_id,
|
|
3077
|
+
tenant_id: input.request.tenant_id,
|
|
3078
|
+
corpus_id: input.request.corpus_id,
|
|
3079
|
+
operation_id: input.request.operation_id,
|
|
3080
|
+
step_id: input.request.step_id,
|
|
3081
|
+
direction: input.direction,
|
|
3082
|
+
idempotency_key: input.request.idempotency_key,
|
|
3083
|
+
request_digest: input.request_digest,
|
|
3084
|
+
outcome: "accepted",
|
|
3085
|
+
target_id: input.target_id,
|
|
3086
|
+
expected_revision: input.request.expected_revision,
|
|
3087
|
+
result_revision: input.after_project.updated_at,
|
|
3088
|
+
result_digest: input.result_digest ?? digestProjectUpdateValue(input.after_project),
|
|
3089
|
+
accepted_receipt_id: input.accepted_receipt_id ?? null,
|
|
3090
|
+
before_project: input.before_project,
|
|
3091
|
+
after_project: input.after_project,
|
|
3092
|
+
created_at: createdAt
|
|
3093
|
+
};
|
|
3094
|
+
return {
|
|
3095
|
+
receipt_id: `mpur_${digestProjectUpdateValue(logical).slice(0, 40)}`,
|
|
3096
|
+
...logical
|
|
3097
|
+
};
|
|
3098
|
+
}
|
|
3099
|
+
function registerProject(name, path, description, memoryPrefix, db) {
|
|
3100
|
+
if (!db && isApiMode()) {
|
|
3101
|
+
const { data } = apiJson("POST", "/projects", {
|
|
3102
|
+
name,
|
|
3103
|
+
path,
|
|
3104
|
+
description,
|
|
3105
|
+
memory_prefix: memoryPrefix
|
|
3106
|
+
});
|
|
3107
|
+
return data;
|
|
3108
|
+
}
|
|
3109
|
+
const d = db || getDatabase();
|
|
3110
|
+
const timestamp = now();
|
|
3111
|
+
const existing = d.query("SELECT * FROM projects WHERE path = ?").get(path);
|
|
3112
|
+
if (existing) {
|
|
3113
|
+
const existingId = existing["id"];
|
|
3114
|
+
d.run("UPDATE projects SET updated_at = ? WHERE id = ?", [
|
|
3115
|
+
timestamp,
|
|
3116
|
+
existingId
|
|
3117
|
+
]);
|
|
3118
|
+
return parseProjectRow(existing);
|
|
3119
|
+
}
|
|
3120
|
+
const id = uuid();
|
|
3121
|
+
d.run("INSERT INTO projects (id, name, path, description, memory_prefix, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)", [id, name, path, description || null, memoryPrefix || null, timestamp, timestamp]);
|
|
3122
|
+
return getProject(id, d);
|
|
3123
|
+
}
|
|
3124
|
+
function getProject(idOrPath, db) {
|
|
3125
|
+
if (!db && isApiMode()) {
|
|
3126
|
+
const { status, data } = apiJson("GET", `/projects/${encodeURIComponent(idOrPath)}`, undefined, { allow404: true });
|
|
3127
|
+
if (status === 404 || !data)
|
|
3128
|
+
return null;
|
|
3129
|
+
return data;
|
|
3130
|
+
}
|
|
3131
|
+
const d = db || getDatabase();
|
|
3132
|
+
let row = d.query("SELECT * FROM projects WHERE id = ?").get(idOrPath);
|
|
3133
|
+
if (row)
|
|
3134
|
+
return parseProjectRow(row);
|
|
3135
|
+
row = d.query("SELECT * FROM projects WHERE path = ?").get(idOrPath);
|
|
3136
|
+
if (row)
|
|
3137
|
+
return parseProjectRow(row);
|
|
3138
|
+
row = d.query("SELECT * FROM projects WHERE LOWER(name) = ?").get(idOrPath.toLowerCase());
|
|
3139
|
+
if (row)
|
|
3140
|
+
return parseProjectRow(row);
|
|
3141
|
+
return null;
|
|
3142
|
+
}
|
|
3143
|
+
function listProjects(db) {
|
|
3144
|
+
if (!db && isApiMode()) {
|
|
3145
|
+
const { data } = apiJson("GET", "/projects");
|
|
3146
|
+
return data?.projects ?? [];
|
|
3147
|
+
}
|
|
3148
|
+
const d = db || getDatabase();
|
|
3149
|
+
const rows = d.query("SELECT * FROM projects ORDER BY updated_at DESC").all();
|
|
3150
|
+
return rows.map(parseProjectRow);
|
|
3151
|
+
}
|
|
3152
|
+
function previewProjectUpdate(id, request, db, expectedIdentity = PROJECT_UPDATE_AUTHORITY) {
|
|
3153
|
+
assertProjectUpdateRequest(request, expectedIdentity);
|
|
3154
|
+
const normalized = normalizeProjectUpdateInput(request.updates);
|
|
3155
|
+
if (!db && isApiMode()) {
|
|
3156
|
+
const { data } = apiJson("POST", `/projects/${encodeURIComponent(id)}/guarded-update`, { ...request, updates: normalized, dry_run: true });
|
|
3157
|
+
return data;
|
|
3158
|
+
}
|
|
3159
|
+
const d = db || getDatabase();
|
|
3160
|
+
const project = getProjectByExactId(id, d);
|
|
3161
|
+
if (!project) {
|
|
3162
|
+
throw new ProjectGuardedUpdateError("PROJECT_UPDATE_NOT_FOUND", `Project not found by exact stable ID: ${id}`);
|
|
3163
|
+
}
|
|
3164
|
+
if (project.updated_at !== request.expected_revision) {
|
|
3165
|
+
throw new ProjectGuardedUpdateError("PROJECT_UPDATE_STALE_REVISION", "Project revision changed before the guarded update dry run", { expected_revision: request.expected_revision, current_revision: project.updated_at });
|
|
3166
|
+
}
|
|
3167
|
+
assertNoProjectCollision(id, normalized, d);
|
|
3168
|
+
return {
|
|
3169
|
+
dry_run: true,
|
|
3170
|
+
applied: false,
|
|
3171
|
+
project: projectWithUpdates(project, normalized, project.updated_at),
|
|
3172
|
+
receipt: null
|
|
3173
|
+
};
|
|
3174
|
+
}
|
|
3175
|
+
function applyProjectUpdate(id, request, db, expectedIdentity = PROJECT_UPDATE_AUTHORITY, resultDigestForProject) {
|
|
3176
|
+
assertProjectUpdateRequest(request, expectedIdentity);
|
|
3177
|
+
const normalized = normalizeProjectUpdateInput(request.updates);
|
|
3178
|
+
if (!db && isApiMode()) {
|
|
3179
|
+
const { data } = apiJson("POST", `/projects/${encodeURIComponent(id)}/guarded-update`, { ...request, updates: normalized, dry_run: false });
|
|
3180
|
+
return data;
|
|
3181
|
+
}
|
|
3182
|
+
const d = db || getDatabase();
|
|
3183
|
+
const requestDigest = digestProjectUpdateValue({
|
|
3184
|
+
...request,
|
|
3185
|
+
target_id: id,
|
|
3186
|
+
direction: "forward",
|
|
3187
|
+
updates: normalized
|
|
3188
|
+
});
|
|
3189
|
+
return d.transaction(() => {
|
|
3190
|
+
const prior = findProjectUpdateReceiptByKey(d, request, "forward", request.idempotency_key);
|
|
3191
|
+
if (prior) {
|
|
3192
|
+
if (prior.request_digest !== requestDigest || prior.target_id !== id) {
|
|
3193
|
+
throw new ProjectGuardedUpdateError("PROJECT_UPDATE_IDEMPOTENCY_MISMATCH", "caller idempotency key is already bound to a different request");
|
|
3194
|
+
}
|
|
3195
|
+
const current = getProjectByExactId(id, d);
|
|
3196
|
+
if (!current || canonicalProjectUpdateJson(current) !== canonicalProjectUpdateJson(prior.after_project)) {
|
|
3197
|
+
throw new ProjectGuardedUpdateError("PROJECT_UPDATE_ACCEPTED_TARGET_DRIFTED", "accepted guarded update target drifted after its immutable receipt");
|
|
3198
|
+
}
|
|
3199
|
+
return { dry_run: false, applied: true, project: prior.after_project, receipt: prior };
|
|
3200
|
+
}
|
|
3201
|
+
const before = getProjectByExactId(id, d);
|
|
3202
|
+
if (!before) {
|
|
3203
|
+
throw new ProjectGuardedUpdateError("PROJECT_UPDATE_NOT_FOUND", `Project not found by exact stable ID: ${id}`);
|
|
3204
|
+
}
|
|
3205
|
+
if (before.updated_at !== request.expected_revision) {
|
|
3206
|
+
throw new ProjectGuardedUpdateError("PROJECT_UPDATE_STALE_REVISION", "Project revision changed before the guarded update", { expected_revision: request.expected_revision, current_revision: before.updated_at });
|
|
3207
|
+
}
|
|
3208
|
+
assertNoProjectCollision(id, normalized, d);
|
|
3209
|
+
const revision = nextProjectRevision(before.updated_at);
|
|
3210
|
+
const after = projectWithUpdates(before, normalized, revision);
|
|
3211
|
+
const result = d.run(`
|
|
3212
|
+
UPDATE projects
|
|
3213
|
+
SET name = ?, path = ?, description = ?, memory_prefix = ?, updated_at = ?
|
|
3214
|
+
WHERE id = ? AND updated_at = ?
|
|
3215
|
+
`, [
|
|
3216
|
+
after.name,
|
|
3217
|
+
after.path,
|
|
3218
|
+
after.description,
|
|
3219
|
+
after.memory_prefix,
|
|
3220
|
+
after.updated_at,
|
|
3221
|
+
id,
|
|
3222
|
+
request.expected_revision
|
|
3223
|
+
]);
|
|
3224
|
+
if (result.changes !== 1) {
|
|
3225
|
+
throw new ProjectGuardedUpdateError("PROJECT_UPDATE_STALE_REVISION", "Project compare-and-swap did not update exactly one row");
|
|
3226
|
+
}
|
|
3227
|
+
const readback = getProjectByExactId(id, d);
|
|
3228
|
+
if (!readback || canonicalProjectUpdateJson(readback) !== canonicalProjectUpdateJson(after)) {
|
|
3229
|
+
throw new ProjectGuardedUpdateError("PROJECT_UPDATE_ACCEPTED_TARGET_DRIFTED", "Project guarded update did not read back exactly under the stable ID");
|
|
3230
|
+
}
|
|
3231
|
+
const receipt = makeProjectUpdateReceipt({
|
|
3232
|
+
request: { ...request, updates: normalized },
|
|
3233
|
+
direction: "forward",
|
|
3234
|
+
request_digest: requestDigest,
|
|
3235
|
+
target_id: id,
|
|
3236
|
+
before_project: before,
|
|
3237
|
+
after_project: readback,
|
|
3238
|
+
result_digest: resultDigestForProject?.(readback)
|
|
3239
|
+
});
|
|
3240
|
+
insertProjectUpdateReceipt(d, receipt);
|
|
3241
|
+
return { dry_run: false, applied: true, project: readback, receipt };
|
|
3242
|
+
});
|
|
3243
|
+
}
|
|
3244
|
+
function rollbackProjectUpdate(id, request, db, expectedIdentity = PROJECT_UPDATE_AUTHORITY, resultDigestForProject) {
|
|
3245
|
+
assertProjectUpdateRequest(request, expectedIdentity);
|
|
3246
|
+
assertBoundedIdentifier(request.accepted_receipt_id, "accepted_receipt_id");
|
|
3247
|
+
if (!db && isApiMode()) {
|
|
3248
|
+
const { data } = apiJson("POST", `/projects/${encodeURIComponent(id)}/guarded-rollback`, request);
|
|
3249
|
+
return data;
|
|
3250
|
+
}
|
|
3251
|
+
const d = db || getDatabase();
|
|
3252
|
+
const requestDigest = digestProjectUpdateValue({
|
|
3253
|
+
...request,
|
|
3254
|
+
target_id: id,
|
|
3255
|
+
direction: "rollback"
|
|
3256
|
+
});
|
|
3257
|
+
return d.transaction(() => {
|
|
3258
|
+
const prior = findProjectUpdateReceiptByKey(d, request, "rollback", request.idempotency_key);
|
|
3259
|
+
if (prior) {
|
|
3260
|
+
if (prior.request_digest !== requestDigest || prior.target_id !== id) {
|
|
3261
|
+
throw new ProjectGuardedUpdateError("PROJECT_UPDATE_IDEMPOTENCY_MISMATCH", "caller idempotency key is already bound to a different rollback request");
|
|
3262
|
+
}
|
|
3263
|
+
const current2 = getProjectByExactId(id, d);
|
|
3264
|
+
if (!current2 || canonicalProjectUpdateJson(current2) !== canonicalProjectUpdateJson(prior.after_project)) {
|
|
3265
|
+
throw new ProjectGuardedUpdateError("PROJECT_UPDATE_ACCEPTED_TARGET_DRIFTED", "accepted rollback target drifted after its immutable receipt");
|
|
3266
|
+
}
|
|
3267
|
+
return { dry_run: false, applied: true, project: prior.after_project, receipt: prior };
|
|
3268
|
+
}
|
|
3269
|
+
const accepted = findProjectUpdateReceiptById(d, request, request.accepted_receipt_id);
|
|
3270
|
+
if (!accepted || accepted.direction !== "forward" || accepted.target_id !== id) {
|
|
3271
|
+
throw new ProjectGuardedUpdateError("PROJECT_UPDATE_RECEIPT_NOT_FOUND", "accepted forward update receipt was not found for this exact project");
|
|
3272
|
+
}
|
|
3273
|
+
const current = getProjectByExactId(id, d);
|
|
3274
|
+
if (!current) {
|
|
3275
|
+
throw new ProjectGuardedUpdateError("PROJECT_UPDATE_NOT_FOUND", `Project not found by exact stable ID: ${id}`);
|
|
3276
|
+
}
|
|
3277
|
+
if (current.updated_at !== request.expected_revision || canonicalProjectUpdateJson(current) !== canonicalProjectUpdateJson(accepted.after_project)) {
|
|
3278
|
+
throw new ProjectGuardedUpdateError("PROJECT_UPDATE_STALE_REVISION", "Project no longer matches the accepted forward receipt", { expected_revision: request.expected_revision, current_revision: current.updated_at });
|
|
3279
|
+
}
|
|
3280
|
+
assertNoProjectCollision(id, accepted.before_project, d);
|
|
3281
|
+
const restored = accepted.before_project;
|
|
3282
|
+
const result = d.run(`
|
|
3283
|
+
UPDATE projects
|
|
3284
|
+
SET name = ?, path = ?, description = ?, memory_prefix = ?,
|
|
3285
|
+
created_at = ?, updated_at = ?
|
|
3286
|
+
WHERE id = ? AND updated_at = ?
|
|
3287
|
+
`, [
|
|
3288
|
+
restored.name,
|
|
3289
|
+
restored.path,
|
|
3290
|
+
restored.description,
|
|
3291
|
+
restored.memory_prefix,
|
|
3292
|
+
restored.created_at,
|
|
3293
|
+
restored.updated_at,
|
|
3294
|
+
id,
|
|
3295
|
+
request.expected_revision
|
|
3296
|
+
]);
|
|
3297
|
+
if (result.changes !== 1) {
|
|
3298
|
+
throw new ProjectGuardedUpdateError("PROJECT_UPDATE_STALE_REVISION", "Project rollback compare-and-swap did not update exactly one row");
|
|
3299
|
+
}
|
|
3300
|
+
const readback = getProjectByExactId(id, d);
|
|
3301
|
+
if (!readback || canonicalProjectUpdateJson(readback) !== canonicalProjectUpdateJson(restored)) {
|
|
3302
|
+
throw new ProjectGuardedUpdateError("PROJECT_UPDATE_ACCEPTED_TARGET_DRIFTED", "Project rollback did not restore the exact prior row");
|
|
3303
|
+
}
|
|
3304
|
+
const receipt = makeProjectUpdateReceipt({
|
|
3305
|
+
request,
|
|
3306
|
+
direction: "rollback",
|
|
3307
|
+
request_digest: requestDigest,
|
|
3308
|
+
target_id: id,
|
|
3309
|
+
before_project: current,
|
|
3310
|
+
after_project: readback,
|
|
3311
|
+
result_digest: resultDigestForProject?.(readback),
|
|
3312
|
+
accepted_receipt_id: accepted.receipt_id
|
|
3313
|
+
});
|
|
3314
|
+
insertProjectUpdateReceipt(d, receipt);
|
|
3315
|
+
return { dry_run: false, applied: true, project: readback, receipt };
|
|
3316
|
+
});
|
|
3317
|
+
}
|
|
3318
|
+
function getProjectUpdateReceipt(id, receiptId, identity = PROJECT_UPDATE_AUTHORITY, db, expectedIdentity = PROJECT_UPDATE_AUTHORITY) {
|
|
3319
|
+
assertProjectUpdateIdentity(identity, expectedIdentity);
|
|
3320
|
+
if (!db && isApiMode()) {
|
|
3321
|
+
const { data } = apiJson("POST", `/projects/${encodeURIComponent(id)}/update-receipts/lookup`, { ...identity, receipt_id: receiptId });
|
|
3322
|
+
return data;
|
|
3323
|
+
}
|
|
3324
|
+
const d = db || getDatabase();
|
|
3325
|
+
const receipt = findProjectUpdateReceiptById(d, identity, receiptId);
|
|
3326
|
+
if (!receipt || receipt.target_id !== id) {
|
|
3327
|
+
throw new ProjectGuardedUpdateError("PROJECT_UPDATE_RECEIPT_NOT_FOUND", "immutable project update receipt was not found for this exact project");
|
|
3328
|
+
}
|
|
3329
|
+
return receipt;
|
|
3330
|
+
}
|
|
3331
|
+
|
|
345
3332
|
// src/project-registration/project-references.ts
|
|
346
3333
|
var MEMENTOS_PROJECT_REFERENCE_SURFACES = [
|
|
347
3334
|
{ key: "memories", table: "memories", column: "project_id" },
|
|
@@ -396,23 +3383,6 @@ function deleteMementosProjectIfUnreferenced(db, projectId) {
|
|
|
396
3383
|
return result.changes;
|
|
397
3384
|
}
|
|
398
3385
|
|
|
399
|
-
// src/project-registration/types.ts
|
|
400
|
-
var MEMENTOS_PROJECT_REGISTRATION_ROUTE = "mementos.project-registration.v1";
|
|
401
|
-
var MEMENTOS_PROJECT_REGISTRATION_CALLER_ROUTE = "projects.full-registration.v1";
|
|
402
|
-
var MEMENTOS_PROJECT_REGISTRATION_SCHEMA_VERSION = 1;
|
|
403
|
-
var MEMENTOS_PROJECT_GUARDED_UPDATE_ROUTE = "mementos.project-guarded-update.v1";
|
|
404
|
-
|
|
405
|
-
class MementosProjectRegistrationError extends Error {
|
|
406
|
-
code;
|
|
407
|
-
details;
|
|
408
|
-
constructor(code, message, details = {}) {
|
|
409
|
-
super(message);
|
|
410
|
-
this.code = code;
|
|
411
|
-
this.details = details;
|
|
412
|
-
this.name = "MementosProjectRegistrationError";
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
|
|
416
3386
|
// src/project-registration/authority.ts
|
|
417
3387
|
var WORKSPACE_ID_PATTERN = /^wks_[A-Za-z0-9][A-Za-z0-9_-]{11,}$/;
|
|
418
3388
|
var OPERATION_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$/;
|
|
@@ -447,10 +3417,10 @@ function canonicalize(value) {
|
|
|
447
3417
|
return out;
|
|
448
3418
|
}
|
|
449
3419
|
function digestMementosProjectRegistrationValue(value) {
|
|
450
|
-
return
|
|
3420
|
+
return createHash2("sha256").update(canonicalMementosProjectRegistrationJson(value)).digest("hex");
|
|
451
3421
|
}
|
|
452
3422
|
function digestOwnedPath(path) {
|
|
453
|
-
return
|
|
3423
|
+
return createHash2("sha256").update(path).digest("hex");
|
|
454
3424
|
}
|
|
455
3425
|
function deriveMementosProjectRegistrationIdempotencyKey(input) {
|
|
456
3426
|
return `prk_${digestMementosProjectRegistrationValue({
|
|
@@ -485,9 +3455,9 @@ function assertWithinBounds(value, bounds, startedAt) {
|
|
|
485
3455
|
}
|
|
486
3456
|
return { response_bytes: bytes, elapsed_ms: elapsed };
|
|
487
3457
|
}
|
|
488
|
-
function
|
|
3458
|
+
function withBoundedResponseControl(payload, bounds, startedAt) {
|
|
489
3459
|
const result = {
|
|
490
|
-
|
|
3460
|
+
...payload,
|
|
491
3461
|
response_control: {
|
|
492
3462
|
response_byte_limit: bounds.response_byte_limit,
|
|
493
3463
|
time_budget_ms: bounds.time_budget_ms,
|
|
@@ -510,6 +3480,9 @@ function withResponseControl(receipt, bounds, startedAt) {
|
|
|
510
3480
|
result.response_control.elapsed_ms = measured.elapsed_ms;
|
|
511
3481
|
return result;
|
|
512
3482
|
}
|
|
3483
|
+
function withResponseControl(receipt, bounds, startedAt) {
|
|
3484
|
+
return withBoundedResponseControl({ receipt }, bounds, startedAt);
|
|
3485
|
+
}
|
|
513
3486
|
function requireString(value, field, options = {}) {
|
|
514
3487
|
const min = options.min ?? 1;
|
|
515
3488
|
const max = options.max ?? 512;
|
|
@@ -531,11 +3504,85 @@ function ownedPath(target) {
|
|
|
531
3504
|
}
|
|
532
3505
|
const path = target.withOwnedPath((value) => value);
|
|
533
3506
|
requireString(path, "target path", { max: 4096 });
|
|
534
|
-
if (path !==
|
|
3507
|
+
if (path !== resolve2(path)) {
|
|
535
3508
|
throw new MementosProjectRegistrationError("MEMENTOS_PROJECT_REGISTRATION_INVALID_INPUT", "target path must already be canonical and absolute");
|
|
536
3509
|
}
|
|
537
3510
|
return path;
|
|
538
3511
|
}
|
|
3512
|
+
function guardedAuthorityIdentity(capability) {
|
|
3513
|
+
return {
|
|
3514
|
+
authority_id: capability.authority_id,
|
|
3515
|
+
tenant_id: capability.tenant_id,
|
|
3516
|
+
corpus_id: capability.corpus_id
|
|
3517
|
+
};
|
|
3518
|
+
}
|
|
3519
|
+
function assertGuardedAuthorityRequest(targetId, request, capability) {
|
|
3520
|
+
assertBounds(request);
|
|
3521
|
+
requireString(targetId, "target_id", { min: 8, max: 128, pattern: OPERATION_PATTERN });
|
|
3522
|
+
if (request.authority !== "mementos" || request.authority_route !== MEMENTOS_PROJECT_GUARDED_UPDATE_ROUTE || request.package_version !== capability.package_version || request.authority_id !== capability.authority_id || request.tenant_id !== capability.tenant_id || request.corpus_id !== capability.corpus_id) {
|
|
3523
|
+
throw new MementosProjectRegistrationError("MEMENTOS_PROJECT_REGISTRATION_CAPABILITY_MISMATCH", "guarded project request does not match this authority capability identity");
|
|
3524
|
+
}
|
|
3525
|
+
return guardedAuthorityIdentity(capability);
|
|
3526
|
+
}
|
|
3527
|
+
function assertGuardedOperationFields(request) {
|
|
3528
|
+
requireString(request.operation_id, "operation_id", {
|
|
3529
|
+
min: 8,
|
|
3530
|
+
max: 128,
|
|
3531
|
+
pattern: OPERATION_PATTERN
|
|
3532
|
+
});
|
|
3533
|
+
requireString(request.step_id, "step_id", {
|
|
3534
|
+
min: 8,
|
|
3535
|
+
max: 128,
|
|
3536
|
+
pattern: OPERATION_PATTERN
|
|
3537
|
+
});
|
|
3538
|
+
requireString(request.idempotency_key, "idempotency_key", {
|
|
3539
|
+
min: 8,
|
|
3540
|
+
max: 128,
|
|
3541
|
+
pattern: OPERATION_PATTERN
|
|
3542
|
+
});
|
|
3543
|
+
requireString(request.expected_revision, "expected_revision", { max: 128 });
|
|
3544
|
+
}
|
|
3545
|
+
function assertGuardedUpdateRequest(targetId, request, capability) {
|
|
3546
|
+
const identity = assertGuardedAuthorityRequest(targetId, request, capability);
|
|
3547
|
+
assertGuardedOperationFields(request);
|
|
3548
|
+
if (!request.updates || typeof request.updates !== "object") {
|
|
3549
|
+
throw new MementosProjectRegistrationError("MEMENTOS_PROJECT_REGISTRATION_INVALID_INPUT", "guarded project updates must contain exactly one private path handle");
|
|
3550
|
+
}
|
|
3551
|
+
exactKeys(request.updates, ["path"], "updates");
|
|
3552
|
+
return { identity, path: ownedPath(request.updates.path) };
|
|
3553
|
+
}
|
|
3554
|
+
function publicGuardedUpdateReceipt(receipt, capability) {
|
|
3555
|
+
return {
|
|
3556
|
+
receipt_id: receipt.receipt_id,
|
|
3557
|
+
authority: "mementos",
|
|
3558
|
+
route: MEMENTOS_PROJECT_GUARDED_UPDATE_ROUTE,
|
|
3559
|
+
package_version: capability.package_version,
|
|
3560
|
+
authority_id: capability.authority_id,
|
|
3561
|
+
tenant_id: capability.tenant_id,
|
|
3562
|
+
corpus_id: capability.corpus_id,
|
|
3563
|
+
operation_id: receipt.operation_id,
|
|
3564
|
+
step_id: receipt.step_id,
|
|
3565
|
+
direction: receipt.direction,
|
|
3566
|
+
idempotency_key: receipt.idempotency_key,
|
|
3567
|
+
request_digest: receipt.request_digest,
|
|
3568
|
+
outcome: "accepted",
|
|
3569
|
+
target_id: receipt.target_id,
|
|
3570
|
+
expected_revision: receipt.expected_revision,
|
|
3571
|
+
result_revision: receipt.result_revision,
|
|
3572
|
+
result_digest: receipt.result_digest,
|
|
3573
|
+
accepted_receipt_id: receipt.accepted_receipt_id,
|
|
3574
|
+
created_at: receipt.created_at
|
|
3575
|
+
};
|
|
3576
|
+
}
|
|
3577
|
+
function guardedProjectError(cause) {
|
|
3578
|
+
if (cause instanceof MementosProjectRegistrationError)
|
|
3579
|
+
throw cause;
|
|
3580
|
+
if (cause instanceof ProjectGuardedUpdateError) {
|
|
3581
|
+
const code = cause.code === "PROJECT_UPDATE_INVALID_INPUT" ? "MEMENTOS_PROJECT_REGISTRATION_INVALID_INPUT" : cause.code === "PROJECT_UPDATE_AUTHORITY_MISMATCH" ? "MEMENTOS_PROJECT_REGISTRATION_CAPABILITY_MISMATCH" : cause.code === "PROJECT_UPDATE_NOT_FOUND" ? "MEMENTOS_PROJECT_REGISTRATION_RECORD_NOT_FOUND" : cause.code === "PROJECT_UPDATE_RECEIPT_NOT_FOUND" ? "MEMENTOS_PROJECT_REGISTRATION_RECEIPT_NOT_FOUND" : "MEMENTOS_PROJECT_REGISTRATION_CONFLICT";
|
|
3582
|
+
throw new MementosProjectRegistrationError(code, "guarded project operation was rejected without exposing private project data", { project_update_code: cause.code });
|
|
3583
|
+
}
|
|
3584
|
+
throw new MementosProjectRegistrationError("MEMENTOS_PROJECT_REGISTRATION_ATOMICITY_UNAVAILABLE", "guarded project operation failed before a bounded public result was available");
|
|
3585
|
+
}
|
|
539
3586
|
function normalizedCallDigest(request) {
|
|
540
3587
|
return digestMementosProjectRegistrationValue({
|
|
541
3588
|
authority_route: request.authority_route,
|
|
@@ -562,7 +3609,7 @@ function publicReceipt(row) {
|
|
|
562
3609
|
const { target_selector: _selector, normalized_call_digest: _digest, ...receipt } = row;
|
|
563
3610
|
return receipt;
|
|
564
3611
|
}
|
|
565
|
-
function
|
|
3612
|
+
function timestampString2(value) {
|
|
566
3613
|
return value instanceof Date ? value.toISOString() : String(value);
|
|
567
3614
|
}
|
|
568
3615
|
function receiptFromRow(row) {
|
|
@@ -571,14 +3618,14 @@ function receiptFromRow(row) {
|
|
|
571
3618
|
authority: "mementos",
|
|
572
3619
|
resource_kind: "project",
|
|
573
3620
|
created_by_operation: row["created_by_operation"] === true || Number(row["created_by_operation"]) === 1,
|
|
574
|
-
created_at:
|
|
3621
|
+
created_at: timestampString2(row["created_at"])
|
|
575
3622
|
};
|
|
576
3623
|
}
|
|
577
3624
|
function bindingFromRow(row) {
|
|
578
3625
|
return {
|
|
579
3626
|
...row,
|
|
580
|
-
created_at:
|
|
581
|
-
updated_at:
|
|
3627
|
+
created_at: timestampString2(row["created_at"]),
|
|
3628
|
+
updated_at: timestampString2(row["updated_at"])
|
|
582
3629
|
};
|
|
583
3630
|
}
|
|
584
3631
|
function getStoredReceipt(db, receiptId) {
|
|
@@ -620,7 +3667,7 @@ function getBinding(db, capability, targetSelector) {
|
|
|
620
3667
|
`, capability.authority_id, capability.tenant_id, capability.corpus_id, targetSelector);
|
|
621
3668
|
return row ? bindingFromRow(row) : null;
|
|
622
3669
|
}
|
|
623
|
-
function
|
|
3670
|
+
function getProjectByExactId2(db, id) {
|
|
624
3671
|
const row = db.get("SELECT * FROM projects WHERE id = ? LIMIT 1", id);
|
|
625
3672
|
if (!row)
|
|
626
3673
|
return null;
|
|
@@ -630,13 +3677,13 @@ function getProjectByExactId(db, id) {
|
|
|
630
3677
|
path: String(row["path"]),
|
|
631
3678
|
description: row["description"] === null ? null : String(row["description"]),
|
|
632
3679
|
memory_prefix: row["memory_prefix"] === null ? null : String(row["memory_prefix"]),
|
|
633
|
-
created_at:
|
|
634
|
-
updated_at:
|
|
3680
|
+
created_at: timestampString2(row["created_at"]),
|
|
3681
|
+
updated_at: timestampString2(row["updated_at"])
|
|
635
3682
|
};
|
|
636
3683
|
}
|
|
637
3684
|
function getProjectByPath(db, path) {
|
|
638
3685
|
const row = db.get("SELECT * FROM projects WHERE path = ? LIMIT 1", path);
|
|
639
|
-
return row ?
|
|
3686
|
+
return row ? getProjectByExactId2(db, String(row["id"])) : null;
|
|
640
3687
|
}
|
|
641
3688
|
function projectRecord(db, project, references = mementosProjectReferenceCounts(db, project.id)) {
|
|
642
3689
|
return {
|
|
@@ -665,7 +3712,7 @@ function targetId(capability, selector) {
|
|
|
665
3712
|
function receiptId(input) {
|
|
666
3713
|
return `mmpr_${digestMementosProjectRegistrationValue(input).slice(0, 40)}`;
|
|
667
3714
|
}
|
|
668
|
-
function makeReceipt(request, capability, callDigest,
|
|
3715
|
+
function makeReceipt(request, capability, callDigest, now2, values) {
|
|
669
3716
|
const withoutId = {
|
|
670
3717
|
authority: "mementos",
|
|
671
3718
|
route: MEMENTOS_PROJECT_REGISTRATION_ROUTE,
|
|
@@ -694,7 +3741,7 @@ function makeReceipt(request, capability, callDigest, now, values) {
|
|
|
694
3741
|
return {
|
|
695
3742
|
receipt_id: receiptId(withoutId),
|
|
696
3743
|
...withoutId,
|
|
697
|
-
created_at:
|
|
3744
|
+
created_at: now2
|
|
698
3745
|
};
|
|
699
3746
|
}
|
|
700
3747
|
function insertReceipt(db, receipt) {
|
|
@@ -719,7 +3766,7 @@ function insertReceipt(db, receipt) {
|
|
|
719
3766
|
}
|
|
720
3767
|
return stored;
|
|
721
3768
|
}
|
|
722
|
-
function insertBinding(db, request, callDigest,
|
|
3769
|
+
function insertBinding(db, request, callDigest, now2) {
|
|
723
3770
|
const result = db.run(`
|
|
724
3771
|
INSERT INTO mementos_project_registration_bindings (
|
|
725
3772
|
authority_id, tenant_id, corpus_id, resource_kind, target_selector,
|
|
@@ -730,7 +3777,7 @@ function insertBinding(db, request, callDigest, now) {
|
|
|
730
3777
|
) VALUES (?, ?, ?, 'project', ?, ?, ?, 'forward', ?, ?, ?, ?, 'pending',
|
|
731
3778
|
NULL, NULL, NULL, NULL, NULL, ?, ?)
|
|
732
3779
|
ON CONFLICT DO NOTHING
|
|
733
|
-
`, request.authority_id, request.tenant_id, request.corpus_id, request.target_selector, request.operation_id, request.step_id, request.idempotency_key, request.request_digest, request.precondition_digest, callDigest,
|
|
3780
|
+
`, request.authority_id, request.tenant_id, request.corpus_id, request.target_selector, request.operation_id, request.step_id, request.idempotency_key, request.request_digest, request.precondition_digest, callDigest, now2, now2);
|
|
734
3781
|
return result.changes === 1;
|
|
735
3782
|
}
|
|
736
3783
|
function lockBinding(db, capability, targetSelector) {
|
|
@@ -742,26 +3789,26 @@ function lockBinding(db, capability, targetSelector) {
|
|
|
742
3789
|
`, capability.authority_id, capability.tenant_id, capability.corpus_id, targetSelector);
|
|
743
3790
|
return result.changes === 1;
|
|
744
3791
|
}
|
|
745
|
-
function setBindingAccepted(db, request, receipt,
|
|
3792
|
+
function setBindingAccepted(db, request, receipt, now2) {
|
|
746
3793
|
const result = db.run(`
|
|
747
3794
|
UPDATE mementos_project_registration_bindings
|
|
748
3795
|
SET state = 'accepted', target_id = ?, accepted_receipt_id = ?,
|
|
749
3796
|
result_revision = ?, result_digest = ?, updated_at = ?
|
|
750
3797
|
WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
|
|
751
3798
|
AND resource_kind = 'project' AND target_selector = ? AND state = 'pending'
|
|
752
|
-
`, receipt.target_id, receipt.receipt_id, receipt.result_revision, receipt.result_digest,
|
|
3799
|
+
`, receipt.target_id, receipt.receipt_id, receipt.result_revision, receipt.result_digest, now2, request.authority_id, request.tenant_id, request.corpus_id, request.target_selector);
|
|
753
3800
|
if (result.changes !== 1) {
|
|
754
3801
|
throw new MementosProjectRegistrationError("MEMENTOS_PROJECT_REGISTRATION_CONFLICT", "registration binding was not pending at acceptance");
|
|
755
3802
|
}
|
|
756
3803
|
}
|
|
757
|
-
function setBindingRemoved(db, accepted, inverseReceiptId,
|
|
3804
|
+
function setBindingRemoved(db, accepted, inverseReceiptId, now2) {
|
|
758
3805
|
const result = db.run(`
|
|
759
3806
|
UPDATE mementos_project_registration_bindings
|
|
760
3807
|
SET state = 'removed', removed_receipt_id = ?, updated_at = ?
|
|
761
3808
|
WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
|
|
762
3809
|
AND resource_kind = 'project' AND target_selector = ?
|
|
763
3810
|
AND state = 'accepted' AND accepted_receipt_id = ?
|
|
764
|
-
`, inverseReceiptId,
|
|
3811
|
+
`, inverseReceiptId, now2, accepted.authority_id, accepted.tenant_id, accepted.corpus_id, accepted.target_selector, accepted.receipt_id);
|
|
765
3812
|
if (result.changes !== 1) {
|
|
766
3813
|
throw new MementosProjectRegistrationError("MEMENTOS_PROJECT_REGISTRATION_CONFLICT", "registration binding was not accepted at inverse");
|
|
767
3814
|
}
|
|
@@ -922,6 +3969,7 @@ class PackageOwnedMementosProjectRegistrationAuthority {
|
|
|
922
3969
|
conditional_inverse: true,
|
|
923
3970
|
ambiguous_outcome_reconciliation: true,
|
|
924
3971
|
guarded_update: true,
|
|
3972
|
+
guarded_update_route: MEMENTOS_PROJECT_GUARDED_UPDATE_ROUTE,
|
|
925
3973
|
no_write_dry_run: true,
|
|
926
3974
|
expected_revision_compare_and_swap: true,
|
|
927
3975
|
caller_idempotency: true,
|
|
@@ -956,7 +4004,7 @@ class PackageOwnedMementosProjectRegistrationAuthority {
|
|
|
956
4004
|
if (accepted.normalized_call_digest !== callDigest) {
|
|
957
4005
|
throw new MementosProjectRegistrationError("MEMENTOS_PROJECT_REGISTRATION_IDEMPOTENCY_MISMATCH", "accepted operation/step identity is bound to a different request");
|
|
958
4006
|
}
|
|
959
|
-
const project = accepted.target_id ?
|
|
4007
|
+
const project = accepted.target_id ? getProjectByExactId2(this.db, accepted.target_id) : null;
|
|
960
4008
|
if (!project || project.path !== path) {
|
|
961
4009
|
return this.terminal(request, callDigest, "accepted_target_missing_or_replaced", {
|
|
962
4010
|
target_id: accepted.target_id
|
|
@@ -983,6 +4031,90 @@ class PackageOwnedMementosProjectRegistrationAuthority {
|
|
|
983
4031
|
supported_resources: ["project"]
|
|
984
4032
|
};
|
|
985
4033
|
}
|
|
4034
|
+
boundedGuardedUpdateResult(targetId2, project, receipt, bounds, startedAt) {
|
|
4035
|
+
if (!receipt || project.id !== targetId2 || receipt.target_id !== targetId2) {
|
|
4036
|
+
throw new MementosProjectRegistrationError("MEMENTOS_PROJECT_REGISTRATION_ATOMICITY_UNAVAILABLE", "guarded project operation did not return its exact immutable receipt");
|
|
4037
|
+
}
|
|
4038
|
+
if (project.updated_at !== receipt.result_revision) {
|
|
4039
|
+
throw new MementosProjectRegistrationError("MEMENTOS_PROJECT_REGISTRATION_ATOMICITY_UNAVAILABLE", "guarded project result revision did not match its immutable receipt");
|
|
4040
|
+
}
|
|
4041
|
+
const record = {
|
|
4042
|
+
target_id: targetId2,
|
|
4043
|
+
revision: receipt.result_revision,
|
|
4044
|
+
digest: receipt.result_digest
|
|
4045
|
+
};
|
|
4046
|
+
return withBoundedResponseControl({
|
|
4047
|
+
dry_run: false,
|
|
4048
|
+
applied: true,
|
|
4049
|
+
record,
|
|
4050
|
+
receipt: publicGuardedUpdateReceipt(receipt, this.capabilityValue)
|
|
4051
|
+
}, bounds, startedAt);
|
|
4052
|
+
}
|
|
4053
|
+
async guardedUpdateProject(targetId2, request) {
|
|
4054
|
+
const startedAt = Date.now();
|
|
4055
|
+
const { identity, path } = assertGuardedUpdateRequest(targetId2, request, this.capabilityValue);
|
|
4056
|
+
try {
|
|
4057
|
+
const result = applyProjectUpdate(targetId2, {
|
|
4058
|
+
...identity,
|
|
4059
|
+
operation_id: request.operation_id,
|
|
4060
|
+
step_id: request.step_id,
|
|
4061
|
+
idempotency_key: request.idempotency_key,
|
|
4062
|
+
expected_revision: request.expected_revision,
|
|
4063
|
+
updates: { path }
|
|
4064
|
+
}, this.db, identity, (project) => projectRecord(this.db, project).digest);
|
|
4065
|
+
return this.boundedGuardedUpdateResult(targetId2, result.project, result.receipt, request, startedAt);
|
|
4066
|
+
} catch (cause) {
|
|
4067
|
+
return guardedProjectError(cause);
|
|
4068
|
+
}
|
|
4069
|
+
}
|
|
4070
|
+
async getGuardedProjectUpdateReceipt(targetId2, receiptId2, request) {
|
|
4071
|
+
const startedAt = Date.now();
|
|
4072
|
+
const identity = assertGuardedAuthorityRequest(targetId2, request, this.capabilityValue);
|
|
4073
|
+
requireString(receiptId2, "receipt_id", {
|
|
4074
|
+
min: 8,
|
|
4075
|
+
max: 128,
|
|
4076
|
+
pattern: OPERATION_PATTERN
|
|
4077
|
+
});
|
|
4078
|
+
try {
|
|
4079
|
+
const receipt = getProjectUpdateReceipt(targetId2, receiptId2, identity, this.db, identity);
|
|
4080
|
+
return withBoundedResponseControl({
|
|
4081
|
+
receipt: publicGuardedUpdateReceipt(receipt, this.capabilityValue)
|
|
4082
|
+
}, request, startedAt);
|
|
4083
|
+
} catch (cause) {
|
|
4084
|
+
return guardedProjectError(cause);
|
|
4085
|
+
}
|
|
4086
|
+
}
|
|
4087
|
+
async rollbackGuardedProjectUpdate(targetId2, request) {
|
|
4088
|
+
const startedAt = Date.now();
|
|
4089
|
+
const identity = assertGuardedAuthorityRequest(targetId2, request, this.capabilityValue);
|
|
4090
|
+
assertGuardedOperationFields(request);
|
|
4091
|
+
if (!request.accepted_receipt || typeof request.accepted_receipt !== "object") {
|
|
4092
|
+
throw new MementosProjectRegistrationError("MEMENTOS_PROJECT_REGISTRATION_INVALID_INPUT", "conditional rollback requires the exact sanitized accepted receipt");
|
|
4093
|
+
}
|
|
4094
|
+
requireString(request.accepted_receipt.receipt_id, "accepted_receipt.receipt_id", {
|
|
4095
|
+
min: 8,
|
|
4096
|
+
max: 128,
|
|
4097
|
+
pattern: OPERATION_PATTERN
|
|
4098
|
+
});
|
|
4099
|
+
try {
|
|
4100
|
+
const internalAccepted = getProjectUpdateReceipt(targetId2, request.accepted_receipt.receipt_id, identity, this.db, identity);
|
|
4101
|
+
const accepted = publicGuardedUpdateReceipt(internalAccepted, this.capabilityValue);
|
|
4102
|
+
if (accepted.direction !== "forward" || accepted.target_id !== targetId2 || request.expected_revision !== accepted.result_revision || canonicalMementosProjectRegistrationJson(request.accepted_receipt) !== canonicalMementosProjectRegistrationJson(accepted)) {
|
|
4103
|
+
throw new MementosProjectRegistrationError("MEMENTOS_PROJECT_REGISTRATION_INVALID_INPUT", "conditional rollback receipt does not exactly match the accepted forward update");
|
|
4104
|
+
}
|
|
4105
|
+
const result = rollbackProjectUpdate(targetId2, {
|
|
4106
|
+
...identity,
|
|
4107
|
+
operation_id: request.operation_id,
|
|
4108
|
+
step_id: request.step_id,
|
|
4109
|
+
idempotency_key: request.idempotency_key,
|
|
4110
|
+
expected_revision: request.expected_revision,
|
|
4111
|
+
accepted_receipt_id: accepted.receipt_id
|
|
4112
|
+
}, this.db, identity, (project) => projectRecord(this.db, project).digest);
|
|
4113
|
+
return this.boundedGuardedUpdateResult(targetId2, result.project, result.receipt, request, startedAt);
|
|
4114
|
+
} catch (cause) {
|
|
4115
|
+
return guardedProjectError(cause);
|
|
4116
|
+
}
|
|
4117
|
+
}
|
|
986
4118
|
async create(request) {
|
|
987
4119
|
const startedAt = Date.now();
|
|
988
4120
|
const path = assertForwardRequest(request, this.capabilityValue);
|
|
@@ -1001,14 +4133,14 @@ class PackageOwnedMementosProjectRegistrationAuthority {
|
|
|
1001
4133
|
if (accepted) {
|
|
1002
4134
|
return this.duplicateForwardOrTerminal(request, callDigest, path, accepted);
|
|
1003
4135
|
}
|
|
1004
|
-
const existing = getProjectByPath(this.db, path) ??
|
|
4136
|
+
const existing = getProjectByPath(this.db, path) ?? getProjectByExactId2(this.db, targetId(this.capabilityValue, request.target_selector));
|
|
1005
4137
|
if (existing) {
|
|
1006
4138
|
return this.terminal(request, callDigest, "target_preexists", {
|
|
1007
4139
|
target_id: existing.id
|
|
1008
4140
|
});
|
|
1009
4141
|
}
|
|
1010
|
-
const
|
|
1011
|
-
const insertedBinding = insertBinding(this.db, request, callDigest,
|
|
4142
|
+
const now2 = this.now();
|
|
4143
|
+
const insertedBinding = insertBinding(this.db, request, callDigest, now2);
|
|
1012
4144
|
if (!lockBinding(this.db, this.capabilityValue, request.target_selector)) {
|
|
1013
4145
|
throw new MementosProjectRegistrationError("MEMENTOS_PROJECT_REGISTRATION_CONFLICT", "registration binding disappeared before it could be locked");
|
|
1014
4146
|
}
|
|
@@ -1034,17 +4166,17 @@ class PackageOwnedMementosProjectRegistrationAuthority {
|
|
|
1034
4166
|
INSERT INTO projects (
|
|
1035
4167
|
id, name, path, description, memory_prefix, created_at, updated_at
|
|
1036
4168
|
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1037
|
-
`, id, request.project_name, path, null, request.project_slug,
|
|
4169
|
+
`, id, request.project_name, path, null, request.project_slug, now2, now2);
|
|
1038
4170
|
} catch (cause) {
|
|
1039
4171
|
throw new WriteBoundaryError("before_object_write", cause);
|
|
1040
4172
|
}
|
|
1041
4173
|
this.writeBoundary("after_object_write", request);
|
|
1042
|
-
const project =
|
|
4174
|
+
const project = getProjectByExactId2(this.db, id);
|
|
1043
4175
|
if (!project || project.path !== path) {
|
|
1044
4176
|
throw new MementosProjectRegistrationError("MEMENTOS_PROJECT_REGISTRATION_CONFLICT", "project write did not read back by its full id");
|
|
1045
4177
|
}
|
|
1046
4178
|
const record = projectRecord(this.db, project);
|
|
1047
|
-
const acceptedReceipt = makeReceipt(request, this.capabilityValue, callDigest,
|
|
4179
|
+
const acceptedReceipt = makeReceipt(request, this.capabilityValue, callDigest, now2, {
|
|
1048
4180
|
outcome: "accepted",
|
|
1049
4181
|
target_id: record.target_id,
|
|
1050
4182
|
result_revision: record.revision,
|
|
@@ -1059,7 +4191,7 @@ class PackageOwnedMementosProjectRegistrationAuthority {
|
|
|
1059
4191
|
throw new WriteBoundaryError("before_receipt_write", cause);
|
|
1060
4192
|
}
|
|
1061
4193
|
this.writeBoundary("after_receipt_write", request);
|
|
1062
|
-
setBindingAccepted(this.db, request, inserted,
|
|
4194
|
+
setBindingAccepted(this.db, request, inserted, now2);
|
|
1063
4195
|
return inserted;
|
|
1064
4196
|
});
|
|
1065
4197
|
} catch (error) {
|
|
@@ -1079,12 +4211,12 @@ class PackageOwnedMementosProjectRegistrationAuthority {
|
|
|
1079
4211
|
throw new MementosProjectRegistrationError("MEMENTOS_PROJECT_REGISTRATION_INVALID_INPUT", "resource_kind must be project");
|
|
1080
4212
|
}
|
|
1081
4213
|
requireString(request.target_id, "target_id", {
|
|
1082
|
-
min:
|
|
1083
|
-
max:
|
|
1084
|
-
pattern:
|
|
4214
|
+
min: 8,
|
|
4215
|
+
max: 128,
|
|
4216
|
+
pattern: OPERATION_PATTERN
|
|
1085
4217
|
});
|
|
1086
4218
|
const path = ownedPath(request.target);
|
|
1087
|
-
const project =
|
|
4219
|
+
const project = getProjectByExactId2(this.db, request.target_id);
|
|
1088
4220
|
if (!project || project.path !== path) {
|
|
1089
4221
|
throw new MementosProjectRegistrationError("MEMENTOS_PROJECT_REGISTRATION_RECORD_NOT_FOUND", "exact full-id project readback did not resolve the owned path");
|
|
1090
4222
|
}
|
|
@@ -1153,7 +4285,7 @@ class PackageOwnedMementosProjectRegistrationAuthority {
|
|
|
1153
4285
|
accepted_receipt_id: storedAccepted.receipt_id
|
|
1154
4286
|
});
|
|
1155
4287
|
}
|
|
1156
|
-
const project =
|
|
4288
|
+
const project = getProjectByExactId2(this.db, storedAccepted.target_id);
|
|
1157
4289
|
if (!project) {
|
|
1158
4290
|
return this.terminal(request, callDigest, "target_missing", {
|
|
1159
4291
|
target_id: storedAccepted.target_id,
|
|
@@ -1188,7 +4320,7 @@ class PackageOwnedMementosProjectRegistrationAuthority {
|
|
|
1188
4320
|
throw new WriteBoundaryError("before_object_write", cause);
|
|
1189
4321
|
}
|
|
1190
4322
|
if (deleted !== 1) {
|
|
1191
|
-
const remaining =
|
|
4323
|
+
const remaining = getProjectByExactId2(this.db, storedAccepted.target_id);
|
|
1192
4324
|
if (remaining && hasMementosProjectReferences(mementosProjectReferenceCounts(this.db, storedAccepted.target_id))) {
|
|
1193
4325
|
return this.terminal(request, callDigest, "target_has_dependents", {
|
|
1194
4326
|
target_id: storedAccepted.target_id,
|
|
@@ -1250,7 +4382,7 @@ class PackageOwnedMementosProjectRegistrationAuthority {
|
|
|
1250
4382
|
if (!inverse || inverse.outcome !== "accepted" || inverse.accepted_receipt_id !== accepted.receipt_id || inverse.result_revision !== "absent" || !inverse.result_digest) {
|
|
1251
4383
|
throw new MementosProjectRegistrationError("MEMENTOS_PROJECT_REGISTRATION_RECEIPT_NOT_FOUND", "accepted conditional inverse receipt was not found");
|
|
1252
4384
|
}
|
|
1253
|
-
if (
|
|
4385
|
+
if (getProjectByExactId2(this.db, accepted.target_id)) {
|
|
1254
4386
|
throw new MementosProjectRegistrationError("MEMENTOS_PROJECT_REGISTRATION_CONFLICT", "inverse verification found the accepted target still present");
|
|
1255
4387
|
}
|
|
1256
4388
|
const verification = {
|
|
@@ -1340,6 +4472,31 @@ function fromWireReadRequest(body) {
|
|
|
1340
4472
|
target: new WirePathHandle(canonicalPath)
|
|
1341
4473
|
};
|
|
1342
4474
|
}
|
|
4475
|
+
function fromWireGuardedUpdateRequest(body) {
|
|
4476
|
+
const { target_id: targetId2, updates, ...request } = body;
|
|
4477
|
+
if (typeof targetId2 !== "string") {
|
|
4478
|
+
throw new MementosProjectRegistrationError("MEMENTOS_PROJECT_REGISTRATION_INVALID_INPUT", "target_id is required on the private guarded-update transport");
|
|
4479
|
+
}
|
|
4480
|
+
if (!updates || typeof updates !== "object" || Array.isArray(updates) || typeof updates["path"] !== "string") {
|
|
4481
|
+
throw new MementosProjectRegistrationError("MEMENTOS_PROJECT_REGISTRATION_INVALID_INPUT", "updates.path is required on the private guarded-update transport");
|
|
4482
|
+
}
|
|
4483
|
+
return {
|
|
4484
|
+
targetId: targetId2,
|
|
4485
|
+
request: {
|
|
4486
|
+
...request,
|
|
4487
|
+
updates: {
|
|
4488
|
+
path: new WirePathHandle(String(updates["path"]))
|
|
4489
|
+
}
|
|
4490
|
+
}
|
|
4491
|
+
};
|
|
4492
|
+
}
|
|
4493
|
+
function exactWireIdentifier(body, field) {
|
|
4494
|
+
const value = body[field];
|
|
4495
|
+
if (typeof value !== "string") {
|
|
4496
|
+
throw new MementosProjectRegistrationError("MEMENTOS_PROJECT_REGISTRATION_INVALID_INPUT", `${field} is required on the guarded-update transport`);
|
|
4497
|
+
}
|
|
4498
|
+
return value;
|
|
4499
|
+
}
|
|
1343
4500
|
async function handleMementosProjectRegistrationHttpRequest(request, url, authority, basePath = "/v1/project-registration") {
|
|
1344
4501
|
const path = url.pathname;
|
|
1345
4502
|
if (path !== basePath && !path.startsWith(`${basePath}/`))
|
|
@@ -1374,6 +4531,21 @@ async function handleMementosProjectRegistrationHttpRequest(request, url, author
|
|
|
1374
4531
|
if (action === "verify-inverse") {
|
|
1375
4532
|
return json({ verification: await authority.verifyInverse(fromWireRequest(body)) });
|
|
1376
4533
|
}
|
|
4534
|
+
if (action === "projects/guarded-update") {
|
|
4535
|
+
const guarded = fromWireGuardedUpdateRequest(body);
|
|
4536
|
+
return json(await authority.guardedUpdateProject(guarded.targetId, guarded.request));
|
|
4537
|
+
}
|
|
4538
|
+
if (action === "projects/update-receipts/lookup") {
|
|
4539
|
+
const targetId2 = exactWireIdentifier(body, "target_id");
|
|
4540
|
+
const receiptId2 = exactWireIdentifier(body, "receipt_id");
|
|
4541
|
+
const { target_id: _targetId, receipt_id: _receiptId, ...lookup } = body;
|
|
4542
|
+
return json(await authority.getGuardedProjectUpdateReceipt(targetId2, receiptId2, lookup));
|
|
4543
|
+
}
|
|
4544
|
+
if (action === "projects/guarded-rollback") {
|
|
4545
|
+
const targetId2 = exactWireIdentifier(body, "target_id");
|
|
4546
|
+
const { target_id: _targetId, ...rollback } = body;
|
|
4547
|
+
return json(await authority.rollbackGuardedProjectUpdate(targetId2, rollback));
|
|
4548
|
+
}
|
|
1377
4549
|
return json({
|
|
1378
4550
|
error: "unknown Mementos project-registration route",
|
|
1379
4551
|
code: "MEMENTOS_PROJECT_REGISTRATION_RECEIPT_NOT_FOUND"
|
|
@@ -1406,6 +4578,15 @@ function toWireRequest(request) {
|
|
|
1406
4578
|
canonical_path: extractPath(target)
|
|
1407
4579
|
};
|
|
1408
4580
|
}
|
|
4581
|
+
function toWireGuardedUpdateRequest(targetId2, request) {
|
|
4582
|
+
return {
|
|
4583
|
+
...request,
|
|
4584
|
+
target_id: targetId2,
|
|
4585
|
+
updates: {
|
|
4586
|
+
path: extractPath(request.updates.path)
|
|
4587
|
+
}
|
|
4588
|
+
};
|
|
4589
|
+
}
|
|
1409
4590
|
|
|
1410
4591
|
class MementosProjectRegistrationHttpClient {
|
|
1411
4592
|
authority = "mementos";
|
|
@@ -1467,6 +4648,24 @@ class MementosProjectRegistrationHttpClient {
|
|
|
1467
4648
|
});
|
|
1468
4649
|
return body.verification;
|
|
1469
4650
|
}
|
|
4651
|
+
async guardedUpdateProject(targetId2, request) {
|
|
4652
|
+
return this.request("/projects/guarded-update", {
|
|
4653
|
+
method: "POST",
|
|
4654
|
+
body: JSON.stringify(toWireGuardedUpdateRequest(targetId2, request))
|
|
4655
|
+
});
|
|
4656
|
+
}
|
|
4657
|
+
async getGuardedProjectUpdateReceipt(targetId2, receiptId2, request) {
|
|
4658
|
+
return this.request("/projects/update-receipts/lookup", {
|
|
4659
|
+
method: "POST",
|
|
4660
|
+
body: JSON.stringify({ ...request, target_id: targetId2, receipt_id: receiptId2 })
|
|
4661
|
+
});
|
|
4662
|
+
}
|
|
4663
|
+
async rollbackGuardedProjectUpdate(targetId2, request) {
|
|
4664
|
+
return this.request("/projects/guarded-rollback", {
|
|
4665
|
+
method: "POST",
|
|
4666
|
+
body: JSON.stringify({ ...request, target_id: targetId2 })
|
|
4667
|
+
});
|
|
4668
|
+
}
|
|
1470
4669
|
}
|
|
1471
4670
|
function createMementosProjectRegistrationHttpClient(options) {
|
|
1472
4671
|
return new MementosProjectRegistrationHttpClient(options);
|