@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.
@@ -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(fileURLToPath(import.meta.url));
2816
+ const here = dirname(fileURLToPath2(import.meta.url));
332
2817
  for (const candidate of [
333
- join(here, "..", "..", "package.json"),
334
- join(here, "..", "package.json")
2818
+ join2(here, "..", "..", "package.json"),
2819
+ join2(here, "..", "package.json")
335
2820
  ]) {
336
2821
  try {
337
- const parsed = JSON.parse(readFileSync(candidate, "utf8"));
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 createHash("sha256").update(canonicalMementosProjectRegistrationJson(value)).digest("hex");
3420
+ return createHash2("sha256").update(canonicalMementosProjectRegistrationJson(value)).digest("hex");
451
3421
  }
452
3422
  function digestOwnedPath(path) {
453
- return createHash("sha256").update(path).digest("hex");
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 withResponseControl(receipt, bounds, startedAt) {
3458
+ function withBoundedResponseControl(payload, bounds, startedAt) {
489
3459
  const result = {
490
- receipt,
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 !== resolve(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 timestampString(value) {
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: timestampString(row["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: timestampString(row["created_at"]),
581
- updated_at: timestampString(row["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 getProjectByExactId(db, id) {
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: timestampString(row["created_at"]),
634
- updated_at: timestampString(row["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 ? getProjectByExactId(db, String(row["id"])) : null;
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, now, values) {
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: now
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, now) {
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, now, now);
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, now) {
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, now, request.authority_id, request.tenant_id, request.corpus_id, request.target_selector);
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, now) {
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, now, accepted.authority_id, accepted.tenant_id, accepted.corpus_id, accepted.target_selector, accepted.receipt_id);
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 ? getProjectByExactId(this.db, accepted.target_id) : null;
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) ?? getProjectByExactId(this.db, targetId(this.capabilityValue, request.target_selector));
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 now = this.now();
1011
- const insertedBinding = insertBinding(this.db, request, callDigest, now);
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, now, now);
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 = getProjectByExactId(this.db, id);
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, now, {
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, now);
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: 51,
1083
- max: 51,
1084
- pattern: PROJECT_ID_PATTERN
4214
+ min: 8,
4215
+ max: 128,
4216
+ pattern: OPERATION_PATTERN
1085
4217
  });
1086
4218
  const path = ownedPath(request.target);
1087
- const project = getProjectByExactId(this.db, request.target_id);
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 = getProjectByExactId(this.db, storedAccepted.target_id);
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 = getProjectByExactId(this.db, storedAccepted.target_id);
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 (getProjectByExactId(this.db, accepted.target_id)) {
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);