@vaur94/agz-memory 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/core.js CHANGED
@@ -734,7 +734,7 @@ function inspectDatabase(db) {
734
734
  }
735
735
  return { integrity, foreignKeyViolations, schemaVersion, counts };
736
736
  }
737
- function assertHealthyDatabase(db) {
737
+ function assertHealthyDatabase(db, options = {}) {
738
738
  const health = inspectDatabase(db);
739
739
  if (health.integrity !== "ok") {
740
740
  throw new Error(`database integrity check failed: ${health.integrity}`);
@@ -742,7 +742,7 @@ function assertHealthyDatabase(db) {
742
742
  if (health.foreignKeyViolations.length > 0) {
743
743
  throw new Error(`database foreign key check failed: ${health.foreignKeyViolations.length} violation(s)`);
744
744
  }
745
- if (health.schemaVersion === 11)
745
+ if (options.verifySchema !== false && health.schemaVersion === 11)
746
746
  assertSchemaV11(db);
747
747
  return health;
748
748
  }
@@ -2169,7 +2169,7 @@ var V9_V10_COLUMNS = {
2169
2169
  "completed_at"
2170
2170
  ]
2171
2171
  };
2172
- function assertLegacySchemaIdentity(db, version) {
2172
+ function assertLegacySchemaIdentity(db, version, options = {}) {
2173
2173
  if (version < 2 || version > 10)
2174
2174
  throw new Error("unrecognized_database");
2175
2175
  const applicationID = db.query("PRAGMA application_id").get().application_id;
@@ -2177,12 +2177,14 @@ function assertLegacySchemaIdentity(db, version) {
2177
2177
  throw new Error("unrecognized_database");
2178
2178
  if (version === 2 && tableExists(db, "memory_items")) {
2179
2179
  assertV2Identity(db);
2180
- assertHealthyDatabase(db);
2180
+ if (options.verifyHealth)
2181
+ assertHealthyDatabase(db);
2181
2182
  return;
2182
2183
  }
2183
2184
  if (version < 8) {
2184
2185
  assertPreV8Identity(db, version);
2185
- assertHealthyDatabase(db);
2186
+ if (options.verifyHealth)
2187
+ assertHealthyDatabase(db);
2186
2188
  return;
2187
2189
  }
2188
2190
  const states = db.query("SELECT version FROM schema_state").all();
@@ -2207,7 +2209,8 @@ function assertLegacySchemaIdentity(db, version) {
2207
2209
  throw new Error("unrecognized_database");
2208
2210
  }
2209
2211
  }
2210
- assertHealthyDatabase(db);
2212
+ if (options.verifyHealth)
2213
+ assertHealthyDatabase(db);
2211
2214
  if (version === 10)
2212
2215
  assertV10SourceDatabase(db);
2213
2216
  }
@@ -3375,7 +3378,7 @@ function migrateV9ToV10(db) {
3375
3378
  }
3376
3379
 
3377
3380
  // src/version.ts
3378
- var PRODUCT_VERSION = "0.5.0";
3381
+ var PRODUCT_VERSION = "0.5.2";
3379
3382
 
3380
3383
  // src/db.ts
3381
3384
  var DDL = `
@@ -3418,15 +3421,25 @@ CREATE INDEX IF NOT EXISTS note_edges_target_idx ON note_edges(project_id, targe
3418
3421
  CREATE TABLE IF NOT EXISTS schema_state (version INTEGER PRIMARY KEY);
3419
3422
  `;
3420
3423
  var PRE_OPEN_PROBE_TIMEOUT_MS = 5000;
3421
- function openMemoryDatabase(path) {
3424
+ function timeMigrationStage(timing, stage, work) {
3425
+ if (!timing)
3426
+ return work();
3427
+ const started = performance.now();
3428
+ try {
3429
+ return work();
3430
+ } finally {
3431
+ timing.phases.push({ stage, elapsedMs: Math.round((performance.now() - started) * 1000) / 1000 });
3432
+ }
3433
+ }
3434
+ function openMemoryDatabase(path, options = {}) {
3422
3435
  ensureDatabaseParent(path);
3423
- assertSupportedDatabaseBeforeOpen(path);
3436
+ assertSupportedDatabaseBeforeOpen(path, false);
3424
3437
  recoverStaleMaintenanceGate(path, () => assertSupportedDatabaseBeforeOpen(path));
3425
3438
  let lock = acquireMigrationLock(path, SCHEMA_VERSION);
3426
3439
  let lease = acquireDatabaseLease(path);
3427
3440
  let db;
3428
3441
  try {
3429
- assertSupportedDatabaseBeforeOpen(path);
3442
+ assertSupportedDatabaseBeforeOpen(path, false);
3430
3443
  db = openDatabase(path);
3431
3444
  } catch (error) {
3432
3445
  lease.release();
@@ -3448,7 +3461,7 @@ function openMemoryDatabase(path) {
3448
3461
  lease.release();
3449
3462
  lease = undefined;
3450
3463
  lease = acquireDatabaseLease(path);
3451
- assertSupportedDatabaseBeforeOpen(path);
3464
+ assertSupportedDatabaseBeforeOpen(path, false);
3452
3465
  db = openDatabase(path);
3453
3466
  dbOpen = true;
3454
3467
  if (hasApplicationObjects(db)) {
@@ -3498,7 +3511,7 @@ function openMemoryDatabase(path) {
3498
3511
  lease.release();
3499
3512
  lease = undefined;
3500
3513
  lease = acquireDatabaseLease(path);
3501
- assertSupportedDatabaseBeforeOpen(path);
3514
+ assertSupportedDatabaseBeforeOpen(path, false);
3502
3515
  db = openDatabase(path);
3503
3516
  dbOpen = true;
3504
3517
  let migrationVersion = getSchemaVersion(db);
@@ -3526,7 +3539,7 @@ function openMemoryDatabase(path) {
3526
3539
  lease.release();
3527
3540
  lease = undefined;
3528
3541
  maintenance = acquireMaintenanceGate(path);
3529
- assertSupportedDatabaseBeforeOpen(path);
3542
+ timeMigrationStage(options.timing, "source-validation", () => assertSupportedDatabaseBeforeOpen(path, false));
3530
3543
  db = openDatabase(path);
3531
3544
  dbOpen = true;
3532
3545
  migrationVersion = getSchemaVersion(db);
@@ -3556,12 +3569,12 @@ function openMemoryDatabase(path) {
3556
3569
  lease = undefined;
3557
3570
  return opened3;
3558
3571
  }
3559
- backup = createVerifiedBackup(db, path, migrationVersion?.version ?? 2, SCHEMA_VERSION, PRODUCT_VERSION);
3572
+ backup = timeMigrationStage(options.timing, "backup-checkpoint", () => createVerifiedBackup(db, path, migrationVersion?.version ?? 2, SCHEMA_VERSION, PRODUCT_VERSION));
3560
3573
  db.exec("PRAGMA foreign_keys=OFF");
3561
3574
  if (!migrationVersion && hasLegacyV2(db)) {
3562
3575
  db.exec(DDL);
3563
3576
  db.exec("CREATE VIRTUAL TABLE IF NOT EXISTS notes_fts USING fts5(id UNINDEXED, title, summary, content, tokenize='unicode61')");
3564
- migrateFromV2(db, path);
3577
+ timeMigrationStage(options.timing, "v2-import", () => migrateFromV2(db, path));
3565
3578
  } else if (!migrationVersion) {
3566
3579
  db.exec(DDL);
3567
3580
  db.exec("CREATE VIRTUAL TABLE IF NOT EXISTS notes_fts USING fts5(id UNINDEXED, title, summary, content, tokenize='unicode61')");
@@ -3577,24 +3590,25 @@ function openMemoryDatabase(path) {
3577
3590
  }
3578
3591
  let version = getSchemaVersion(db)?.version ?? 8;
3579
3592
  if (version < 9) {
3580
- db.transaction(() => migrateV8ToV9(db))();
3593
+ timeMigrationStage(options.timing, "v8-to-v9", () => db.transaction(() => migrateV8ToV9(db))());
3581
3594
  version = 9;
3582
3595
  }
3583
3596
  if (version < 10) {
3584
- db.transaction(() => migrateV9ToV10(db))();
3597
+ timeMigrationStage(options.timing, "v9-to-v10", () => db.transaction(() => migrateV9ToV10(db))());
3585
3598
  version = 10;
3586
3599
  }
3587
3600
  if (version < SCHEMA_VERSION) {
3588
- db.transaction(() => {
3601
+ timeMigrationStage(options.timing, "v10-to-v11", () => db.transaction(() => {
3589
3602
  db.exec(`PRAGMA application_id = ${APPLICATION_ID}`);
3590
3603
  migrateV10ToV11(db);
3591
- })();
3604
+ })());
3592
3605
  }
3593
3606
  db.exec("PRAGMA foreign_keys=ON");
3594
3607
  if (db.query("PRAGMA foreign_keys").get().foreign_keys !== 1) {
3595
3608
  throw new Error("failed to enable database foreign keys");
3596
3609
  }
3597
- assertHealthyDatabase(db);
3610
+ timeMigrationStage(options.timing, "fingerprint", () => assertSchemaV11(db));
3611
+ timeMigrationStage(options.timing, "deep-health", () => assertHealthyDatabase(db, { verifySchema: false }));
3598
3612
  console.warn(`[agz-memory] migrated to v${SCHEMA_VERSION} (backup: ${backup.manifestPath})`);
3599
3613
  backup = undefined;
3600
3614
  db.close();
@@ -3680,11 +3694,11 @@ function openDatabase(path) {
3680
3694
  throw error;
3681
3695
  }
3682
3696
  }
3683
- function assertSupportedDatabaseBeforeOpen(path) {
3697
+ function assertSupportedDatabaseBeforeOpen(path, verifyHealth = true) {
3684
3698
  const deadline = Date.now() + PRE_OPEN_PROBE_TIMEOUT_MS;
3685
3699
  while (true) {
3686
3700
  try {
3687
- assertSupportedDatabaseBeforeOpenOnce(path);
3701
+ assertSupportedDatabaseBeforeOpenOnce(path, verifyHealth);
3688
3702
  return;
3689
3703
  } catch (error) {
3690
3704
  if (!isSQLiteBusyError(error) || Date.now() >= deadline)
@@ -3693,19 +3707,19 @@ function assertSupportedDatabaseBeforeOpen(path) {
3693
3707
  }
3694
3708
  }
3695
3709
  }
3696
- function assertSupportedDatabaseBeforeOpenOnce(path) {
3710
+ function assertSupportedDatabaseBeforeOpenOnce(path, verifyHealth) {
3697
3711
  assertDatabasePath(path);
3698
3712
  if (!existsSync4(path))
3699
3713
  return;
3700
3714
  const db = new Database3(path, { readonly: true });
3701
3715
  try {
3702
3716
  assertDatabasePath(path);
3703
- assertSupportedDatabase(db);
3717
+ assertSupportedDatabase(db, verifyHealth);
3704
3718
  } finally {
3705
3719
  db.close();
3706
3720
  }
3707
3721
  }
3708
- function assertSupportedDatabase(db) {
3722
+ function assertSupportedDatabase(db, verifyHealth = true) {
3709
3723
  const existingVersion = getSchemaVersion(db);
3710
3724
  if (existingVersion && existingVersion.version > SCHEMA_VERSION) {
3711
3725
  throw new Error(`database schema v${existingVersion.version} is newer than supported v${SCHEMA_VERSION}`);
@@ -3716,7 +3730,7 @@ function assertSupportedDatabase(db) {
3716
3730
  if (!existingVersion) {
3717
3731
  if (!hasLegacyV2(db))
3718
3732
  throw new Error("unrecognized_database");
3719
- assertLegacySchemaIdentity(db, 2);
3733
+ assertLegacySchemaIdentity(db, 2, { verifyHealth });
3720
3734
  return;
3721
3735
  }
3722
3736
  if (existingVersion.version === SCHEMA_VERSION || hasV11Marker) {
@@ -3726,7 +3740,7 @@ function assertSupportedDatabase(db) {
3726
3740
  if (existingVersion.version < 2 || existingVersion.version > 10) {
3727
3741
  throw new Error("unrecognized_database");
3728
3742
  }
3729
- assertLegacySchemaIdentity(db, existingVersion.version);
3743
+ assertLegacySchemaIdentity(db, existingVersion.version, { verifyHealth });
3730
3744
  }
3731
3745
  function assertDatabasePath(path) {
3732
3746
  try {
@@ -4086,13 +4100,341 @@ function takeHead(value, maxCharacters) {
4086
4100
  return value.slice(0, offset);
4087
4101
  }
4088
4102
 
4103
+ // src/config.ts
4104
+ import { homedir } from "os";
4105
+ import { join as join3 } from "path";
4106
+ function resolveConfig(environment = process.env) {
4107
+ const databasePath = environment.OPENCODE_MEMORY_DATABASE_PATH?.trim() || join3(environment.HOME ?? homedir(), ".local", "share", "opencode-memory", "memory.sqlite");
4108
+ const quarantineKeyringPath = environment.OPENCODE_MEMORY_QUARANTINE_KEYRING_PATH?.trim() || `${databasePath}.quarantine-keys`;
4109
+ return { databasePath, quarantineKeyringPath };
4110
+ }
4111
+
4112
+ // src/security/quarantine-key.ts
4113
+ import { createHmac, randomBytes, timingSafeEqual } from "crypto";
4114
+ import {
4115
+ closeSync as closeSync3,
4116
+ constants as constants3,
4117
+ existsSync as existsSync5,
4118
+ fchmodSync,
4119
+ fstatSync as fstatSync2,
4120
+ fsyncSync as fsyncSync3,
4121
+ lstatSync as lstatSync4,
4122
+ mkdirSync as mkdirSync4,
4123
+ openSync as openSync3,
4124
+ readFileSync as readFileSync4,
4125
+ renameSync as renameSync4,
4126
+ rmSync as rmSync3,
4127
+ writeSync as writeSync2
4128
+ } from "fs";
4129
+ import { dirname as dirname3 } from "path";
4130
+ var KEYRING_FORMAT = "agz-memory.quarantine-keyring/1";
4131
+ var KEY_BYTES = 32;
4132
+ var KEY_ID_BYTES = 12;
4133
+ var MAX_KEYRING_BYTES = 64 * 1024;
4134
+ var KEY_MODE = 384;
4135
+ var LOCK_TIMEOUT_MS = 1000;
4136
+ var RETRIES = 20;
4137
+
4138
+ class QuarantineKeyring {
4139
+ path;
4140
+ constructor(path) {
4141
+ this.path = path;
4142
+ }
4143
+ readActiveKey() {
4144
+ return this.keyReference(this.readDocument());
4145
+ }
4146
+ ensureActiveKey() {
4147
+ let lastError;
4148
+ for (let attempt = 0;attempt < RETRIES; attempt++) {
4149
+ try {
4150
+ return this.readActiveKey();
4151
+ } catch (error) {
4152
+ lastError = error;
4153
+ if (!isKeyringError(error, "quarantine_keyring_missing")) {
4154
+ if (attempt + 1 < RETRIES) {
4155
+ pause();
4156
+ continue;
4157
+ }
4158
+ throw error;
4159
+ }
4160
+ }
4161
+ try {
4162
+ this.createInitialKeyring();
4163
+ } catch (error) {
4164
+ lastError = error;
4165
+ if (!isKeyringError(error, "quarantine_keyring_exists"))
4166
+ throw error;
4167
+ }
4168
+ pause();
4169
+ }
4170
+ throw lastError instanceof Error ? lastError : new Error("quarantine_keyring_unavailable");
4171
+ }
4172
+ rotate() {
4173
+ this.assertSupportedPlatform();
4174
+ const lockPath = `${this.path}.lock`;
4175
+ const lock = this.acquireLock(lockPath);
4176
+ try {
4177
+ const document = this.readDocument();
4178
+ const keyID = randomBytes(KEY_ID_BYTES).toString("hex");
4179
+ document.keys[keyID] = randomBytes(KEY_BYTES).toString("base64");
4180
+ document.activeKeyID = keyID;
4181
+ this.writeAtomically(document);
4182
+ return { keyID };
4183
+ } finally {
4184
+ closeSync3(lock);
4185
+ rmSync3(lockPath, { force: true });
4186
+ }
4187
+ }
4188
+ digestSource(source, payloadFingerprint) {
4189
+ const active = this.activeKey(true);
4190
+ return digestSourceWithKey(source, payloadFingerprint, active);
4191
+ }
4192
+ digestExistingSource(source, payloadFingerprint) {
4193
+ const active = this.activeKey(false);
4194
+ return digestSourceWithKey(source, payloadFingerprint, active);
4195
+ }
4196
+ verifySourceDigest(source, payloadFingerprint, keyID, digest) {
4197
+ if (!isPayloadFingerprint(payloadFingerprint) || !/^[0-9a-f]{24}$/.test(keyID) || !/^[0-9a-f]{64}$/.test(digest)) {
4198
+ return false;
4199
+ }
4200
+ const document = this.readDocument();
4201
+ const encoded = document.keys[keyID];
4202
+ if (!encoded)
4203
+ return false;
4204
+ const expected = createHmac("sha256", Buffer.from(encoded, "base64")).update(sourceDigestBytes(source, payloadFingerprint)).digest();
4205
+ return timingSafeEqual(expected, Buffer.from(digest, "hex"));
4206
+ }
4207
+ activeKey(createIfMissing) {
4208
+ if (createIfMissing)
4209
+ this.ensureActiveKey();
4210
+ const document = this.readDocument();
4211
+ const keyID = document.activeKeyID;
4212
+ const encoded = document.keys[keyID];
4213
+ if (!encoded)
4214
+ throw new Error("quarantine_keyring_invalid");
4215
+ return { keyID, key: Buffer.from(encoded, "base64") };
4216
+ }
4217
+ keyReference(document) {
4218
+ if (!document.keys[document.activeKeyID])
4219
+ throw new Error("quarantine_keyring_invalid");
4220
+ return { keyID: document.activeKeyID };
4221
+ }
4222
+ createInitialKeyring() {
4223
+ this.assertSupportedPlatform();
4224
+ this.assertSafeParent();
4225
+ const keyID = randomBytes(KEY_ID_BYTES).toString("hex");
4226
+ const document = {
4227
+ format: KEYRING_FORMAT,
4228
+ activeKeyID: keyID,
4229
+ keys: { [keyID]: randomBytes(KEY_BYTES).toString("base64") }
4230
+ };
4231
+ let fd;
4232
+ try {
4233
+ fd = openSync3(this.path, constants3.O_WRONLY | constants3.O_CREAT | constants3.O_EXCL | constants3.O_NOFOLLOW, KEY_MODE);
4234
+ fchmodSync(fd, KEY_MODE);
4235
+ writeSync2(fd, JSON.stringify(document));
4236
+ fsyncSync3(fd);
4237
+ } catch (error) {
4238
+ if (error.code === "EEXIST") {
4239
+ throw new Error("quarantine_keyring_exists");
4240
+ }
4241
+ throw error;
4242
+ } finally {
4243
+ if (fd !== undefined)
4244
+ closeSync3(fd);
4245
+ }
4246
+ }
4247
+ readDocument() {
4248
+ this.assertSupportedPlatform();
4249
+ let before;
4250
+ try {
4251
+ before = lstatSync4(this.path);
4252
+ } catch (error) {
4253
+ if (error.code === "ENOENT") {
4254
+ throw new Error("quarantine_keyring_missing");
4255
+ }
4256
+ throw new Error("quarantine_keyring_unavailable");
4257
+ }
4258
+ if (before.isSymbolicLink())
4259
+ throw new Error("quarantine_keyring_symlink");
4260
+ if (!before.isFile())
4261
+ throw new Error("quarantine_keyring_not_regular");
4262
+ this.assertPermissions(before.mode);
4263
+ if (before.size <= 0 || before.size > MAX_KEYRING_BYTES) {
4264
+ throw new Error("quarantine_keyring_invalid");
4265
+ }
4266
+ let fd;
4267
+ try {
4268
+ fd = openSync3(this.path, constants3.O_RDONLY | constants3.O_NOFOLLOW);
4269
+ const opened = fstatSync2(fd);
4270
+ if (!opened.isFile() || opened.dev !== before.dev || opened.ino !== before.ino || opened.size <= 0 || opened.size > MAX_KEYRING_BYTES) {
4271
+ throw new Error("quarantine_keyring_toctou");
4272
+ }
4273
+ this.assertPermissions(opened.mode);
4274
+ const bytes = readFileSync4(fd);
4275
+ const after = fstatSync2(fd);
4276
+ if (after.dev !== opened.dev || after.ino !== opened.ino || after.size !== opened.size) {
4277
+ throw new Error("quarantine_keyring_toctou");
4278
+ }
4279
+ return parseKeyring(bytes);
4280
+ } catch (error) {
4281
+ if (error.code === "ELOOP") {
4282
+ throw new Error("quarantine_keyring_symlink");
4283
+ }
4284
+ throw error;
4285
+ } finally {
4286
+ if (fd !== undefined)
4287
+ closeSync3(fd);
4288
+ }
4289
+ }
4290
+ writeAtomically(document) {
4291
+ this.assertSafeParent();
4292
+ const temporary = `${this.path}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`;
4293
+ let fd;
4294
+ try {
4295
+ fd = openSync3(temporary, constants3.O_WRONLY | constants3.O_CREAT | constants3.O_EXCL | constants3.O_NOFOLLOW, KEY_MODE);
4296
+ fchmodSync(fd, KEY_MODE);
4297
+ writeSync2(fd, JSON.stringify(document));
4298
+ fsyncSync3(fd);
4299
+ closeSync3(fd);
4300
+ fd = undefined;
4301
+ renameSync4(temporary, this.path);
4302
+ this.syncParent();
4303
+ } finally {
4304
+ if (fd !== undefined)
4305
+ closeSync3(fd);
4306
+ rmSync3(temporary, { force: true });
4307
+ }
4308
+ }
4309
+ acquireLock(lockPath) {
4310
+ this.assertSafeParent();
4311
+ const deadline = Date.now() + LOCK_TIMEOUT_MS;
4312
+ while (Date.now() < deadline) {
4313
+ try {
4314
+ const fd = openSync3(lockPath, constants3.O_WRONLY | constants3.O_CREAT | constants3.O_EXCL | constants3.O_NOFOLLOW, KEY_MODE);
4315
+ fchmodSync(fd, KEY_MODE);
4316
+ return fd;
4317
+ } catch (error) {
4318
+ if (error.code !== "EEXIST")
4319
+ throw error;
4320
+ pause();
4321
+ }
4322
+ }
4323
+ throw new Error("quarantine_keyring_busy");
4324
+ }
4325
+ assertSafeParent() {
4326
+ const parent = dirname3(this.path);
4327
+ if (!existsSync5(parent))
4328
+ mkdirSync4(parent, { recursive: true, mode: 448 });
4329
+ const stat = lstatSync4(parent);
4330
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
4331
+ throw new Error("quarantine_keyring_parent_unsafe");
4332
+ }
4333
+ }
4334
+ assertSupportedPlatform() {
4335
+ if (process.platform === "win32") {
4336
+ throw new Error("quarantine_keyring_platform_unsupported");
4337
+ }
4338
+ }
4339
+ assertPermissions(mode) {
4340
+ if ((mode & 511) !== KEY_MODE)
4341
+ throw new Error("quarantine_keyring_permissions");
4342
+ }
4343
+ syncParent() {
4344
+ let fd;
4345
+ try {
4346
+ fd = openSync3(dirname3(this.path), constants3.O_RDONLY);
4347
+ fsyncSync3(fd);
4348
+ } catch {} finally {
4349
+ if (fd !== undefined)
4350
+ closeSync3(fd);
4351
+ }
4352
+ }
4353
+ }
4354
+ function digestSourceWithKey(source, payloadFingerprint, active) {
4355
+ if (!isPayloadFingerprint(payloadFingerprint)) {
4356
+ throw new Error("quarantine_payload_fingerprint_invalid");
4357
+ }
4358
+ return {
4359
+ keyID: active.keyID,
4360
+ digest: createHmac("sha256", active.key).update(sourceDigestBytes(source, payloadFingerprint)).digest("hex")
4361
+ };
4362
+ }
4363
+ function parseKeyring(bytes) {
4364
+ let value;
4365
+ try {
4366
+ value = JSON.parse(bytes.toString("utf8"));
4367
+ } catch {
4368
+ throw new Error("quarantine_keyring_invalid");
4369
+ }
4370
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
4371
+ throw new Error("quarantine_keyring_invalid");
4372
+ }
4373
+ const record = value;
4374
+ if (record.format !== KEYRING_FORMAT || typeof record.activeKeyID !== "string" || !record.keys || typeof record.keys !== "object" || Array.isArray(record.keys) || Object.keys(record).length !== 3) {
4375
+ throw new Error("quarantine_keyring_invalid");
4376
+ }
4377
+ const keys = record.keys;
4378
+ if (!/^[0-9a-f]{24}$/.test(record.activeKeyID)) {
4379
+ throw new Error("quarantine_keyring_invalid");
4380
+ }
4381
+ for (const [keyID, encoded] of Object.entries(keys)) {
4382
+ if (!/^[0-9a-f]{24}$/.test(keyID) || typeof encoded !== "string" || !isCanonicalKey(encoded)) {
4383
+ throw new Error("quarantine_keyring_invalid");
4384
+ }
4385
+ }
4386
+ if (typeof keys[record.activeKeyID] !== "string") {
4387
+ throw new Error("quarantine_keyring_invalid");
4388
+ }
4389
+ return {
4390
+ format: KEYRING_FORMAT,
4391
+ activeKeyID: record.activeKeyID,
4392
+ keys
4393
+ };
4394
+ }
4395
+ function isCanonicalKey(value) {
4396
+ const key = Buffer.from(value, "base64");
4397
+ return key.length === KEY_BYTES && key.toString("base64") === value;
4398
+ }
4399
+ function sourceDigestBytes(source, payloadFingerprint) {
4400
+ return Buffer.from(JSON.stringify([
4401
+ "quarantine-source-payload/2",
4402
+ source.schema,
4403
+ source.projectID,
4404
+ source.bindingKey,
4405
+ source.kind,
4406
+ source.source.system,
4407
+ source.source.opencodeVersion,
4408
+ source.source.pluginVersion,
4409
+ source.source.sessionID,
4410
+ source.source.messageID ?? null,
4411
+ source.source.ordinal ?? null,
4412
+ source.source.toolCallID ?? null,
4413
+ payloadFingerprint
4414
+ ]), "utf8");
4415
+ }
4416
+ function isPayloadFingerprint(value) {
4417
+ return /^[0-9a-f]{64}$/.test(value);
4418
+ }
4419
+ function isKeyringError(error, code) {
4420
+ return error instanceof Error && error.message === code;
4421
+ }
4422
+ function pause() {
4423
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
4424
+ }
4425
+
4089
4426
  // src/store/capture.ts
4090
4427
  class CaptureStore {
4091
4428
  db;
4092
4429
  indexBackends;
4093
- constructor(db, indexBackends = []) {
4430
+ quarantineKeyring;
4431
+ constructor(db, indexBackends = [], options = {}) {
4094
4432
  this.db = db;
4095
4433
  this.indexBackends = indexBackends;
4434
+ this.quarantineKeyring = options.quarantineKeyring ?? new QuarantineKeyring(resolveConfig().quarantineKeyringPath);
4435
+ try {
4436
+ this.quarantineKeyring.ensureActiveKey();
4437
+ } catch {}
4096
4438
  }
4097
4439
  bindProject(input) {
4098
4440
  const workspaceID = input.workspaceID ?? "";
@@ -4273,7 +4615,7 @@ class CaptureStore {
4273
4615
  }
4274
4616
  if (!this.binding(parsed.bindingKey, parsed.projectID))
4275
4617
  throw new Error("binding_conflict");
4276
- const prepared = prepareForPersistence(parsed, options.denylist);
4618
+ const prepared = prepareForPersistence(parsed, options.denylist, this.quarantineKeyring);
4277
4619
  const now = Date.now();
4278
4620
  let result = {
4279
4621
  outcome: prepared.quarantined ? "quarantined" : "shadowed",
@@ -4288,13 +4630,13 @@ class CaptureStore {
4288
4630
  generation, created_at, updated_at, processed_at)
4289
4631
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 0, ?, ?, ?)
4290
4632
  ON CONFLICT DO NOTHING
4291
- `).run(prepared.event.idempotencyKey, prepared.event.schema, prepared.event.projectID, prepared.event.bindingKey, prepared.event.kind, prepared.event.source.sessionID, prepared.event.source.messageID ?? null, prepared.event.source.ordinal ?? null, prepared.event.source.toolCallID ?? null, prepared.payload, prepared.payloadHash, REDACTION_POLICY_VERSION, prepared.quarantined ? "quarantined" : "pending", now, now, prepared.quarantined ? now : null);
4633
+ `).run(prepared.event.idempotencyKey, prepared.event.schema, prepared.event.projectID, prepared.event.bindingKey, prepared.event.kind, prepared.event.source.sessionID, prepared.event.source.messageID ?? null, prepared.event.source.ordinal ?? null, prepared.event.source.toolCallID ?? null, prepared.payload, prepared.payloadHash, prepared.redactionVersion, prepared.quarantined ? "quarantined" : "pending", now, now, prepared.quarantined ? now : null);
4292
4634
  if (inserted.changes === 0) {
4293
4635
  const existing = this.db.query(`SELECT contract, project_id, binding_key, event_kind, source_session_id,
4294
4636
  source_message_id, source_ordinal, source_tool_call_id,
4295
4637
  payload_json, payload_hash, redaction_version, note_id
4296
4638
  FROM capture_events WHERE idempotency_key = ?`).get(parsed.idempotencyKey);
4297
- if (!existing || !samePersistedCapture(existing, prepared.event, prepared.payload, prepared.payloadHash)) {
4639
+ if (!existing || !samePersistedCapture(existing, prepared.event, prepared.payload, prepared.payloadHash, prepared.redactionVersion, prepared.payloadFingerprint, this.quarantineKeyring)) {
4298
4640
  throw new Error("idempotency_conflict");
4299
4641
  }
4300
4642
  result = {
@@ -4459,6 +4801,9 @@ class CaptureStore {
4459
4801
  supersedes_id, current_revision, subject_key, content_hash, created_at, updated_at)
4460
4802
  VALUES (?, ?, ?, ?, ?, ?, ?, 0, 'active', ?, 1, ?, ?, ?, ?)
4461
4803
  `).run(id, event.projectID, candidate.kind, candidate.title, candidate.summary, candidate.content, sizeClass, supersedesID, subjectKey, contentHash, now, now);
4804
+ this.db.query(`UPDATE projects
4805
+ SET updated_at = CASE WHEN updated_at >= ? THEN updated_at + 1 ELSE ? END
4806
+ WHERE id = ?`).run(now, now, event.projectID);
4462
4807
  this.recordRevision(event, id, now);
4463
4808
  return id;
4464
4809
  }
@@ -4535,7 +4880,7 @@ class CaptureStore {
4535
4880
  return rows[0];
4536
4881
  }
4537
4882
  }
4538
- function prepareForPersistence(event, denylist) {
4883
+ function prepareForPersistence(event, denylist, quarantineKeyring) {
4539
4884
  const copy = structuredClone(event);
4540
4885
  let replacements = 0;
4541
4886
  let truncated = copy.redaction.truncated;
@@ -4576,11 +4921,28 @@ function prepareForPersistence(event, denylist) {
4576
4921
  truncated
4577
4922
  };
4578
4923
  const validated = parseCaptureEvent(copy);
4924
+ const payloadFingerprint = capturePayloadHash2(validated);
4925
+ let payloadHash = payloadFingerprint;
4926
+ let redactionVersion = REDACTION_POLICY_VERSION;
4927
+ if (quarantined) {
4928
+ let keyID = "unavailable";
4929
+ payloadHash = null;
4930
+ try {
4931
+ const keyed = quarantineKeyring?.digestExistingSource(validated, payloadFingerprint);
4932
+ if (keyed) {
4933
+ keyID = keyed.keyID;
4934
+ payloadHash = keyed.digest;
4935
+ }
4936
+ } catch {}
4937
+ redactionVersion = quarantineRedactionVersion(keyID);
4938
+ }
4579
4939
  const payload = quarantined ? null : JSON.stringify(validated);
4580
4940
  return {
4581
4941
  event: validated,
4582
4942
  payload,
4583
- payloadHash: capturePayloadHash2(validated),
4943
+ payloadHash,
4944
+ redactionVersion,
4945
+ payloadFingerprint,
4584
4946
  quarantined,
4585
4947
  additionalReplacements: replacements
4586
4948
  };
@@ -4654,9 +5016,34 @@ function captureKeyForEvent(event) {
4654
5016
  terminalStatus: event.signal.status
4655
5017
  });
4656
5018
  }
4657
- function samePersistedCapture(existing, event, payload, payloadHash) {
4658
- const retainedLegacyPayload = existing.payload_json === null && existing.payload_hash === null;
4659
- return existing.contract === event.schema && existing.project_id === event.projectID && existing.binding_key === event.bindingKey && existing.event_kind === event.kind && existing.source_session_id === event.source.sessionID && existing.source_message_id === (event.source.messageID ?? null) && existing.source_ordinal === (event.source.ordinal ?? null) && existing.source_tool_call_id === (event.source.toolCallID ?? null) && (retainedLegacyPayload || sameCapturePayload(existing.payload_json, existing.payload_hash, payload, payloadHash) && existing.redaction_version === REDACTION_POLICY_VERSION);
5019
+ function samePersistedCapture(existing, event, payload, payloadHash, redactionVersion, payloadFingerprint, quarantineKeyring) {
5020
+ const retainedLegacyPayload = existing.payload_json === null && existing.payload_hash === null && !isQuarantineRedactionVersion(existing.redaction_version);
5021
+ return existing.contract === event.schema && existing.project_id === event.projectID && existing.binding_key === event.bindingKey && existing.event_kind === event.kind && existing.source_session_id === event.source.sessionID && existing.source_message_id === (event.source.messageID ?? null) && existing.source_ordinal === (event.source.ordinal ?? null) && existing.source_tool_call_id === (event.source.toolCallID ?? null) && (retainedLegacyPayload || sameCapturePayload(existing.payload_json, existing.payload_hash, payload, payloadHash) && existing.redaction_version === redactionVersion || sameKeyedQuarantinedCapture(existing, event, payload, payloadHash, redactionVersion, payloadFingerprint, quarantineKeyring));
5022
+ }
5023
+ function sameKeyedQuarantinedCapture(existing, event, payload, payloadHash, redactionVersion, payloadFingerprint, quarantineKeyring) {
5024
+ if (payload !== null || existing.payload_json !== null || payloadHash === null || existing.payload_hash === null || !isCurrentKeyedQuarantineVersion(existing.redaction_version) || !isCurrentKeyedQuarantineVersion(redactionVersion)) {
5025
+ return false;
5026
+ }
5027
+ const keyID = quarantineKeyID(existing.redaction_version);
5028
+ if (!keyID)
5029
+ return false;
5030
+ try {
5031
+ return quarantineKeyring.verifySourceDigest(event, payloadFingerprint, keyID, existing.payload_hash);
5032
+ } catch {
5033
+ return false;
5034
+ }
5035
+ }
5036
+ function quarantineRedactionVersion(keyID) {
5037
+ return `${REDACTION_POLICY_VERSION};quarantine-key=${keyID};quarantine-digest=2`;
5038
+ }
5039
+ function isQuarantineRedactionVersion(value) {
5040
+ return /^redaction\/1;quarantine-key=(?:[0-9a-f]{24}|unavailable)(?:;quarantine-digest=2)?$/.test(value);
5041
+ }
5042
+ function isCurrentKeyedQuarantineVersion(value) {
5043
+ return /^redaction\/1;quarantine-key=[0-9a-f]{24};quarantine-digest=2$/.test(value);
5044
+ }
5045
+ function quarantineKeyID(value) {
5046
+ return /^redaction\/1;quarantine-key=([0-9a-f]{24});quarantine-digest=2$/.exec(value)?.[1];
4660
5047
  }
4661
5048
  function sameCapturePayload(existingPayload, existingHash, payload, payloadHash) {
4662
5049
  if (existingHash !== null && existingHash === payloadHash)
@@ -4694,6 +5081,172 @@ function derivedHash(note) {
4694
5081
 
4695
5082
  // src/store.ts
4696
5083
  import { randomUUID as randomUUID8 } from "crypto";
5084
+
5085
+ // src/contracts/limits.ts
5086
+ import * as z2 from "zod/v4";
5087
+ var LIMITS = {
5088
+ title: 240,
5089
+ summary: 4096,
5090
+ content: 65536,
5091
+ query: 4096,
5092
+ noteID: 256,
5093
+ batch: 10,
5094
+ requestBytes: 1048576,
5095
+ responseBytes: 1048576,
5096
+ pageSize: 100
5097
+ };
5098
+ function utf8Bytes(value) {
5099
+ return Buffer.byteLength(value, "utf8");
5100
+ }
5101
+ function assertTextLimit(field, value) {
5102
+ const maximum = LIMITS[field];
5103
+ if (utf8Bytes(value) > maximum)
5104
+ throw new RangeError(`${field} exceeds ${maximum} UTF-8 bytes`);
5105
+ }
5106
+ function boundedText(field, description) {
5107
+ return z2.string().superRefine((value, context) => {
5108
+ if (utf8Bytes(value) > LIMITS[field]) {
5109
+ context.addIssue({ code: "custom", message: `${field} exceeds ${LIMITS[field]} UTF-8 bytes` });
5110
+ }
5111
+ }).describe(description);
5112
+ }
5113
+ function assertRequestLimit(value) {
5114
+ if (utf8Bytes(JSON.stringify(value)) > LIMITS.requestBytes) {
5115
+ throw new RangeError(`request exceeds ${LIMITS.requestBytes} UTF-8 bytes`);
5116
+ }
5117
+ }
5118
+
5119
+ // src/contracts/pagination.ts
5120
+ import { createHmac as createHmac2, randomBytes as randomBytes2, timingSafeEqual as timingSafeEqual2 } from "crypto";
5121
+ var CURSOR_VERSION = 1;
5122
+ var CURSOR_KEY = randomBytes2(32);
5123
+ var MAX_CURSOR_BYTES = 2048;
5124
+ function encodeCursor(payload) {
5125
+ const body = Buffer.from(JSON.stringify({ v: CURSOR_VERSION, ...payload }));
5126
+ const signature = createHmac2("sha256", CURSOR_KEY).update(body).digest();
5127
+ return Buffer.concat([body, signature]).toString("base64url");
5128
+ }
5129
+ function decodeCursor(cursor, scope) {
5130
+ let payload;
5131
+ try {
5132
+ if (Buffer.byteLength(cursor, "utf8") > MAX_CURSOR_BYTES)
5133
+ throw new Error;
5134
+ const encoded = Buffer.from(cursor, "base64url");
5135
+ if (encoded.length <= 32)
5136
+ throw new Error;
5137
+ const body = encoded.subarray(0, -32);
5138
+ const signature = encoded.subarray(-32);
5139
+ const expected = createHmac2("sha256", CURSOR_KEY).update(body).digest();
5140
+ if (!timingSafeEqual2(signature, expected))
5141
+ throw new Error;
5142
+ payload = JSON.parse(body.toString("utf8"));
5143
+ } catch {
5144
+ throw new TypeError("invalid_cursor");
5145
+ }
5146
+ if (payload.v !== CURSOR_VERSION || !Number.isSafeInteger(payload.offset) || payload.offset < 0)
5147
+ throw new TypeError("invalid_cursor");
5148
+ if (payload.projectID !== scope.projectID || payload.query !== scope.query) {
5149
+ throw new TypeError("cursor_scope_mismatch");
5150
+ }
5151
+ if (payload.snapshot !== scope.snapshot)
5152
+ throw new TypeError("stale_snapshot");
5153
+ return { offset: payload.offset };
5154
+ }
5155
+ function paginate(items, options) {
5156
+ if (!Number.isSafeInteger(options.limit) || options.limit < 1)
5157
+ throw new RangeError("page limit must be positive");
5158
+ if (options.requestedSnapshot !== undefined && options.requestedSnapshot !== options.snapshot) {
5159
+ throw new TypeError("stale_snapshot");
5160
+ }
5161
+ const offset = options.cursor !== undefined ? decodeCursor(options.cursor, options).offset : 0;
5162
+ const limit = Math.min(options.limit, LIMITS.pageSize);
5163
+ const page = items.slice(offset, offset + limit);
5164
+ const nextOffset = offset + page.length;
5165
+ return {
5166
+ items: page,
5167
+ snapshot: options.snapshot,
5168
+ etag: options.snapshot,
5169
+ ...nextOffset < items.length ? { nextCursor: encodeCursor({ projectID: options.projectID, query: options.query, snapshot: options.snapshot, offset: nextOffset }) } : {}
5170
+ };
5171
+ }
5172
+
5173
+ // src/contracts/mutation.ts
5174
+ function normalizeLegacyMutation(input) {
5175
+ const { id, delete: deleteFlag, confirmation, kind, title, summary, content } = input;
5176
+ const hasEdits = kind !== undefined || title !== undefined || summary !== undefined || content !== undefined;
5177
+ if (deleteFlag) {
5178
+ if (!id)
5179
+ throw new TypeError("id is required for delete");
5180
+ if (hasEdits)
5181
+ throw new TypeError("cannot combine delete with edits");
5182
+ return { operation: "delete", id, confirmation };
5183
+ }
5184
+ if (id) {
5185
+ if (!hasEdits)
5186
+ throw new TypeError("patch requires changes");
5187
+ return { operation: "patch", id, changes: { kind, title, summary, content } };
5188
+ }
5189
+ if (!kind || title === undefined || summary === undefined) {
5190
+ throw new TypeError("create requires kind, title, and summary");
5191
+ }
5192
+ return { operation: "create", kind, title, summary, content };
5193
+ }
5194
+ function isMutationOperation(value) {
5195
+ try {
5196
+ assertStrictMutationOperation(value);
5197
+ return true;
5198
+ } catch {
5199
+ return false;
5200
+ }
5201
+ }
5202
+ function assertStrictMutationOperation(value) {
5203
+ if (!value || typeof value !== "object" || Array.isArray(value))
5204
+ throw new TypeError("invalid mutation");
5205
+ const input = value;
5206
+ if (input.operation !== "create" && input.operation !== "patch" && input.operation !== "delete") {
5207
+ throw new TypeError("invalid mutation operation");
5208
+ }
5209
+ const operation = input.operation;
5210
+ const allowed = {
5211
+ create: ["operation", "kind", "title", "summary", "content"],
5212
+ patch: ["operation", "id", "changes"],
5213
+ delete: ["operation", "id", "confirmation"]
5214
+ };
5215
+ const invalid = Object.keys(input).some((key) => !allowed[operation].includes(key));
5216
+ if (invalid)
5217
+ throw new TypeError("invalid mutation keys");
5218
+ if (input.operation === "create") {
5219
+ if (!KINDS.includes(input.kind) || typeof input.title !== "string" || typeof input.summary !== "string") {
5220
+ throw new TypeError("invalid create mutation");
5221
+ }
5222
+ if (input.content !== undefined && typeof input.content !== "string")
5223
+ throw new TypeError("invalid create mutation");
5224
+ }
5225
+ if (input.operation === "patch") {
5226
+ if (typeof input.id !== "string" || !input.id || !input.changes || typeof input.changes !== "object" || Array.isArray(input.changes)) {
5227
+ throw new TypeError("patch requires changes");
5228
+ }
5229
+ const changes = input.changes;
5230
+ if (Object.values(changes).every((value2) => value2 === undefined))
5231
+ throw new TypeError("patch requires changes");
5232
+ if (Object.keys(changes).some((key) => !["kind", "title", "summary", "content"].includes(key)))
5233
+ throw new TypeError("invalid patch changes");
5234
+ if (changes.kind !== undefined && !KINDS.includes(changes.kind))
5235
+ throw new TypeError("invalid patch changes");
5236
+ for (const key of ["title", "summary", "content"]) {
5237
+ if (changes[key] !== undefined && typeof changes[key] !== "string")
5238
+ throw new TypeError("invalid patch changes");
5239
+ }
5240
+ }
5241
+ if (input.operation === "delete") {
5242
+ if (typeof input.id !== "string" || !input.id)
5243
+ throw new TypeError("id is required for delete");
5244
+ if (input.confirmation !== undefined && typeof input.confirmation !== "string")
5245
+ throw new TypeError("invalid delete confirmation");
5246
+ }
5247
+ }
5248
+
5249
+ // src/store.ts
4697
5250
  class MemoryStore {
4698
5251
  db;
4699
5252
  indexBackends;
@@ -4723,6 +5276,18 @@ class MemoryStore {
4723
5276
  pinnedCount: row.pinned_count
4724
5277
  }));
4725
5278
  }
5279
+ listProjectsPage(limit, cursor, snapshot) {
5280
+ const projects = this.listProjects();
5281
+ const current = hashTuple("project-list-snapshot", 1, projects.flatMap((project) => [
5282
+ project.projectID,
5283
+ project.projectName,
5284
+ project.createdAt,
5285
+ project.updatedAt,
5286
+ project.noteCount,
5287
+ project.pinnedCount
5288
+ ]));
5289
+ return paginate(projects, { projectID: "__projects__", query: "", limit, cursor, snapshot: current, requestedSnapshot: snapshot });
5290
+ }
4726
5291
  createProject(nameValue) {
4727
5292
  const reason = validateProjectName(nameValue);
4728
5293
  if (reason)
@@ -4767,7 +5332,12 @@ class MemoryStore {
4767
5332
  }
4768
5333
  const now = Date.now();
4769
5334
  try {
4770
- const updated = this.immediateTransaction(() => this.db.query("UPDATE projects SET name = ?, normalized_name = ?, updated_at = ? WHERE id = ? RETURNING *").get(name, normalizedName, now, projectID));
5335
+ const updated = this.immediateTransaction(() => this.db.query(`UPDATE projects
5336
+ SET name = ?,
5337
+ normalized_name = ?,
5338
+ updated_at = CASE WHEN updated_at >= ? THEN updated_at + 1 ELSE ? END
5339
+ WHERE id = ?
5340
+ RETURNING *`).get(name, normalizedName, now, now, projectID));
4771
5341
  if (!updated)
4772
5342
  return { ok: false, reason: `project ${projectID} not found` };
4773
5343
  return { ok: true, project: rowToProject(updated) };
@@ -4825,14 +5395,14 @@ class MemoryStore {
4825
5395
  };
4826
5396
  });
4827
5397
  }
4828
- update(projectID, input) {
5398
+ update(projectID, operation) {
4829
5399
  const project = this.getProjectRow(projectID);
4830
5400
  if (!project)
4831
5401
  return { ok: false, reason: `project ${projectID} not found` };
4832
- if (input.delete) {
4833
- if (!input.id)
4834
- return { ok: false, reason: "id is required for delete" };
4835
- const id2 = input.id;
5402
+ assertStrictMutationOperation(operation);
5403
+ if (operation.operation === "delete") {
5404
+ const id2 = operation.id;
5405
+ assertTextLimit("noteID", id2);
4836
5406
  const existing2 = this.getNoteRow(projectID, id2);
4837
5407
  if (!existing2)
4838
5408
  return {
@@ -4849,6 +5419,7 @@ class MemoryStore {
4849
5419
  };
4850
5420
  }
4851
5421
  const now2 = Date.now();
5422
+ this.bumpProjectVersion(projectID, now2);
4852
5423
  for (const backend of this.indexBackends) {
4853
5424
  this.enqueueOutbox(backend, "delete-note", deleted.project_id, deleted.id, deleted.current_revision, null, now2);
4854
5425
  }
@@ -4861,31 +5432,35 @@ class MemoryStore {
4861
5432
  };
4862
5433
  });
4863
5434
  }
4864
- const existing = input.id ? this.getNoteRow(projectID, input.id) : undefined;
4865
- if (input.id && !existing) {
5435
+ if (operation.operation === "patch")
5436
+ assertTextLimit("noteID", operation.id);
5437
+ const existing = operation.operation === "patch" ? this.getNoteRow(projectID, operation.id) : undefined;
5438
+ if (operation.operation === "patch" && !existing) {
4866
5439
  return {
4867
5440
  ok: false,
4868
- reason: `note ${input.id} not found in project ${project.name}`
5441
+ reason: `note ${operation.id} not found in project ${project.name}`
4869
5442
  };
4870
5443
  }
4871
5444
  if (existing && existing.status !== "active") {
4872
5445
  return { ok: false, reason: `note is ${existing.status}` };
4873
5446
  }
4874
- const kindValue = input.kind ?? existing?.kind;
5447
+ const changes = operation.operation === "patch" ? operation.changes : undefined;
5448
+ const kindValue = operation.operation === "create" ? operation.kind : changes?.kind ?? existing?.kind;
4875
5449
  const kind = KINDS.includes(kindValue ?? "") ? kindValue : null;
4876
5450
  if (!kind)
4877
5451
  return { ok: false, reason: `kind must be one of: ${KINDS.join(", ")}` };
4878
- const title = input.title === undefined ? existing?.title ?? "" : input.title.trim();
4879
- const summary = input.summary === undefined ? existing?.summary ?? "" : input.summary.trim();
4880
- const content = input.content === undefined ? existing?.content ?? summary : input.content.trim();
5452
+ const title = operation.operation === "create" ? operation.title.trim() : changes?.title === undefined ? existing?.title ?? "" : changes.title.trim();
5453
+ const summary = operation.operation === "create" ? operation.summary.trim() : changes?.summary === undefined ? existing?.summary ?? "" : changes.summary.trim();
5454
+ const content = operation.operation === "create" ? (operation.content ?? summary).trim() : changes?.content === undefined ? existing?.content ?? summary : changes.content.trim();
4881
5455
  if (!title)
4882
5456
  return { ok: false, reason: "title is required" };
4883
- if (title.length > 240)
4884
- return { ok: false, reason: "title exceeds 240 characters" };
4885
5457
  if (!summary)
4886
5458
  return { ok: false, reason: "summary is required" };
4887
5459
  if (!content)
4888
5460
  return { ok: false, reason: "content is empty" };
5461
+ assertTextLimit("title", title);
5462
+ assertTextLimit("summary", summary);
5463
+ assertTextLimit("content", content);
4889
5464
  const now = Date.now();
4890
5465
  const sizeClass = content.length <= INLINE_LIMIT ? "inline" : "indexed";
4891
5466
  const contentHash = noteContentHash(kind, title, summary, content);
@@ -4920,6 +5495,7 @@ class MemoryStore {
4920
5495
  RETURNING *`).get(kind, title, summary, content, sizeClass, subjectKey, contentHash, now, projectID, existing.id, existing.current_revision);
4921
5496
  if (!updated)
4922
5497
  return;
5498
+ this.bumpProjectVersion(projectID, now);
4923
5499
  this.recordCurrentRevision(updated, "mcp-manual", now);
4924
5500
  const derived = deriveDocument({
4925
5501
  projectID: updated.project_id,
@@ -4965,6 +5541,7 @@ class MemoryStore {
4965
5541
  };
4966
5542
  }
4967
5543
  pin(projectID, id, pinned) {
5544
+ assertTextLimit("noteID", id);
4968
5545
  const project = this.getProjectRow(projectID);
4969
5546
  if (!project)
4970
5547
  return { ok: false, reason: `project ${projectID} not found` };
@@ -4999,6 +5576,7 @@ class MemoryStore {
4999
5576
  RETURNING *`).get(pinned ? 1 : 0, now, projectID, id, note.current_revision);
5000
5577
  if (!updated)
5001
5578
  return;
5579
+ this.bumpProjectVersion(projectID, now);
5002
5580
  this.recordCurrentRevision(updated, "mcp-manual", now);
5003
5581
  const derived = deriveDocument({
5004
5582
  projectID: updated.project_id,
@@ -5043,6 +5621,7 @@ class MemoryStore {
5043
5621
  RETURNING *`).get(id, projectID, kind, title, summary, content, sizeClass, supersedesID, subjectKey, contentHash, now, now);
5044
5622
  if (!note)
5045
5623
  throw new Error(`note ${id} insert returned no row`);
5624
+ this.bumpProjectVersion(projectID, now);
5046
5625
  this.recordCurrentRevision(note, "mcp-manual", now);
5047
5626
  const derived = deriveDocument({
5048
5627
  projectID: note.project_id,
@@ -5062,19 +5641,43 @@ class MemoryStore {
5062
5641
  });
5063
5642
  }
5064
5643
  read(projectID, id) {
5644
+ const page = this.readPage(projectID, id, LIMITS.pageSize);
5645
+ if (!("note" in page))
5646
+ return page;
5647
+ return {
5648
+ note: page.note,
5649
+ edges: page.items,
5650
+ snapshot: page.snapshot,
5651
+ etag: page.etag,
5652
+ nextCursor: page.nextCursor
5653
+ };
5654
+ }
5655
+ readPage(projectID, id, limit, cursor, snapshot) {
5656
+ assertTextLimit("noteID", id);
5065
5657
  const row = this.getNoteRow(projectID, id);
5066
5658
  if (!row)
5067
5659
  return { reason: `note ${id} not found in project ${projectID}` };
5068
5660
  const edges = this.db.query(`SELECT e.id, e.project_id, p.name AS project_name, e.source_id, e.target_id, e.predicate, e.created_at
5069
- FROM note_edges e
5070
- JOIN projects p ON p.id = e.project_id
5071
- WHERE e.project_id = ? AND (e.source_id = ? OR e.target_id = ?)`).all(projectID, id, id);
5661
+ FROM note_edges e
5662
+ JOIN projects p ON p.id = e.project_id
5663
+ WHERE e.project_id = ? AND (e.source_id = ? OR e.target_id = ?)
5664
+ ORDER BY e.created_at, e.id`).all(projectID, id, id);
5665
+ const resultEdges = edges.map(rowToEdge);
5666
+ const current = hashTuple("note-edge-snapshot", 1, [id, row.updated_at, ...resultEdges.flatMap((edge) => [edge.id, edge.projectID, edge.projectName, edge.sourceID, edge.targetID, edge.predicate, edge.createdAt])]);
5072
5667
  return {
5073
5668
  note: rowToNote(row),
5074
- edges: edges.map(rowToEdge)
5669
+ ...paginate(resultEdges, { projectID, query: `edges:${id}`, limit, cursor, snapshot: current, requestedSnapshot: snapshot })
5075
5670
  };
5076
5671
  }
5672
+ listRevisionsPage(projectID, noteID, limit, cursor, snapshot) {
5673
+ assertTextLimit("noteID", noteID);
5674
+ const rows = this.db.query("SELECT revision, created_at FROM note_revisions WHERE project_id = ? AND note_id = ? ORDER BY revision").all(projectID, noteID);
5675
+ const current = hashTuple("note-revision-snapshot", 1, [noteID, ...rows.flatMap((row) => [row.revision, row.created_at])]);
5676
+ return paginate(rows, { projectID, query: `revisions:${noteID}`, limit, cursor, snapshot: current, requestedSnapshot: snapshot });
5677
+ }
5077
5678
  link(projectID, sourceID, targetID, predicate) {
5679
+ assertTextLimit("noteID", sourceID);
5680
+ assertTextLimit("noteID", targetID);
5078
5681
  const project = this.getProjectRow(projectID);
5079
5682
  if (!project)
5080
5683
  return { ok: false, reason: `project ${projectID} not found` };
@@ -5096,24 +5699,64 @@ class MemoryStore {
5096
5699
  if (row.status !== "active")
5097
5700
  return { ok: false, reason: `note ${id} is ${row.status}` };
5098
5701
  }
5099
- this.db.query("INSERT OR IGNORE INTO note_edges (id, project_id, source_id, target_id, predicate, created_at) VALUES (?, ?, ?, ?, ?, ?)").run(randomUUID8(), projectID, sourceID, targetID, predicate, Date.now());
5702
+ this.immediateTransaction(() => {
5703
+ const now = Date.now();
5704
+ const inserted = this.db.query("INSERT OR IGNORE INTO note_edges (id, project_id, source_id, target_id, predicate, created_at) VALUES (?, ?, ?, ?, ?, ?)").run(randomUUID8(), projectID, sourceID, targetID, predicate, now);
5705
+ if (inserted.changes > 0)
5706
+ this.bumpProjectVersion(projectID, now);
5707
+ });
5100
5708
  return { ok: true, projectID, projectName: project.name };
5101
5709
  }
5102
5710
  recall(projectID, query, limit = 10) {
5711
+ return this.recallPage(projectID, query, limit).cards;
5712
+ }
5713
+ recallPage(projectID, query, limit = LIMITS.pageSize, cursor, snapshot) {
5714
+ assertTextLimit("query", query);
5715
+ if (!Number.isSafeInteger(limit) || limit < 1) {
5716
+ return { cards: [], snapshot: hashTuple("recall-snapshot", 1, [projectID, query]), etag: hashTuple("recall-snapshot", 1, [projectID, query]) };
5717
+ }
5718
+ const pageLimit = Math.min(limit, LIMITS.pageSize);
5103
5719
  const tokens = query.split(/\s+/).map((token) => token.trim()).filter(Boolean).slice(0, 12).map((token) => `"${token.replace(/"/g, '""')}"`);
5104
- if (tokens.length === 0)
5105
- return [];
5106
- const matches = this.db.query(`SELECT n.*, p.name AS project_name, bm25(notes_fts) AS rank
5720
+ if (tokens.length === 0) {
5721
+ const current2 = hashTuple("recall-snapshot", 1, [projectID, query]);
5722
+ return { cards: [], snapshot: current2, etag: current2 };
5723
+ }
5724
+ const project = this.getProjectRow(projectID);
5725
+ const current = hashTuple("recall-snapshot", 2, [
5726
+ projectID,
5727
+ query,
5728
+ project?.name ?? "",
5729
+ project?.updated_at ?? 0
5730
+ ]);
5731
+ if (snapshot !== undefined && snapshot !== current)
5732
+ throw new TypeError("stale_snapshot");
5733
+ const offset = cursor !== undefined ? decodeCursor(cursor, { projectID, query, snapshot: current }).offset : 0;
5734
+ const directCount = this.db.query(`SELECT COUNT(*) AS count
5735
+ FROM notes_fts
5736
+ JOIN notes n ON n.rowid = notes_fts.rowid
5737
+ WHERE notes_fts MATCH ? AND n.project_id = ? AND n.status = 'active'`).get(tokens.join(" OR "), projectID).count;
5738
+ const matches = offset < directCount ? this.db.query(`SELECT n.*, p.name AS project_name, bm25(notes_fts) AS rank
5107
5739
  FROM notes_fts
5108
5740
  JOIN notes n ON n.rowid = notes_fts.rowid
5109
5741
  JOIN projects p ON p.id = n.project_id
5110
5742
  WHERE notes_fts MATCH ? AND n.project_id = ? AND n.status = 'active'
5111
- ORDER BY n.pinned DESC, rank
5112
- LIMIT ?`).all(tokens.join(" OR "), projectID, limit);
5743
+ ORDER BY n.pinned DESC, rank, n.id
5744
+ LIMIT ? OFFSET ?`).all(tokens.join(" OR "), projectID, pageLimit, offset) : [];
5113
5745
  const cards = matches.map((row) => toCard(rowToNote(row), "match"));
5114
- const seen = new Set(cards.map((card) => card.id));
5115
- for (const match of matches.slice(0, 5)) {
5116
- const neighbors = this.db.query(`SELECT e.predicate, n.*, p.name AS project_name
5746
+ const directRemaining = Math.max(0, directCount - offset);
5747
+ const neighborOffset = Math.max(0, offset - directCount);
5748
+ if (cards.length < pageLimit && directRemaining <= cards.length) {
5749
+ const firstMatches = this.db.query(`SELECT n.*, p.name AS project_name, bm25(notes_fts) AS rank
5750
+ FROM notes_fts
5751
+ JOIN notes n ON n.rowid = notes_fts.rowid
5752
+ JOIN projects p ON p.id = n.project_id
5753
+ WHERE notes_fts MATCH ? AND n.project_id = ? AND n.status = 'active'
5754
+ ORDER BY n.pinned DESC, rank, n.id
5755
+ LIMIT 5`).all(tokens.join(" OR "), projectID);
5756
+ const neighborsToAppend = [];
5757
+ const seen = new Set;
5758
+ for (const match of firstMatches) {
5759
+ const neighbors = this.db.query(`SELECT e.predicate, n.*, p.name AS project_name
5117
5760
  FROM note_edges e
5118
5761
  JOIN notes n ON n.id = CASE WHEN e.source_id = ? THEN e.target_id ELSE e.source_id END
5119
5762
  JOIN projects p ON p.id = n.project_id
@@ -5121,22 +5764,49 @@ class MemoryStore {
5121
5764
  AND (e.source_id = ? OR e.target_id = ?)
5122
5765
  AND n.project_id = ?
5123
5766
  AND n.status = 'active'
5124
- ORDER BY n.pinned DESC
5767
+ ORDER BY n.pinned DESC, n.updated_at DESC, n.id
5125
5768
  LIMIT 6`).all(match.id, projectID, match.id, match.id, projectID);
5126
- for (const neighbor of neighbors) {
5127
- if (seen.has(neighbor.id))
5128
- continue;
5129
- seen.add(neighbor.id);
5130
- const card = toCard(rowToNote(neighbor), "neighbor");
5131
- card.predicates = [neighbor.predicate];
5132
- cards.push(card);
5769
+ for (const neighbor of neighbors) {
5770
+ if (seen.has(neighbor.id))
5771
+ continue;
5772
+ const isDirectMatch = this.db.query(`SELECT 1
5773
+ FROM notes_fts
5774
+ JOIN notes n ON n.rowid = notes_fts.rowid
5775
+ WHERE notes_fts MATCH ? AND n.project_id = ? AND n.id = ? AND n.status = 'active'`).get(tokens.join(" OR "), projectID, neighbor.id);
5776
+ if (isDirectMatch)
5777
+ continue;
5778
+ seen.add(neighbor.id);
5779
+ const card = toCard(rowToNote(neighbor), "neighbor");
5780
+ card.predicates = [neighbor.predicate];
5781
+ neighborsToAppend.push(card);
5782
+ }
5133
5783
  }
5784
+ cards.push(...neighborsToAppend.slice(neighborOffset, neighborOffset + (pageLimit - cards.length)));
5785
+ const consumedNeighbors = neighborOffset + Math.max(0, cards.length - matches.length);
5786
+ const hasMore2 = offset + matches.length < directCount || consumedNeighbors < neighborsToAppend.length;
5787
+ return {
5788
+ cards,
5789
+ snapshot: current,
5790
+ etag: current,
5791
+ ...hasMore2 ? { nextCursor: encodeCursor({ projectID, query, snapshot: current, offset: offset + cards.length }) } : {}
5792
+ };
5134
5793
  }
5135
- return cards.slice(0, limit + 5);
5794
+ const hasMore = offset + cards.length < directCount;
5795
+ return {
5796
+ cards,
5797
+ snapshot: current,
5798
+ etag: current,
5799
+ ...hasMore ? { nextCursor: encodeCursor({ projectID, query, snapshot: current, offset: offset + cards.length }) } : {}
5800
+ };
5136
5801
  }
5137
5802
  getProjectRow(id) {
5138
5803
  return this.db.query("SELECT * FROM projects WHERE id = ?").get(id);
5139
5804
  }
5805
+ bumpProjectVersion(projectID, now) {
5806
+ this.db.query(`UPDATE projects
5807
+ SET updated_at = CASE WHEN updated_at >= ? THEN updated_at + 1 ELSE ? END
5808
+ WHERE id = ?`).run(now, now, projectID);
5809
+ }
5140
5810
  getProjectByNormalizedName(normalizedName) {
5141
5811
  return this.db.query("SELECT * FROM projects WHERE normalized_name = ?").get(normalizedName);
5142
5812
  }
@@ -5261,6 +5931,8 @@ import { randomUUID as randomUUID9 } from "crypto";
5261
5931
  var LEASE_DURATION_MS = 30000;
5262
5932
  var BACKEND_TIMEOUT_MS = 5000;
5263
5933
  var HEARTBEAT_INTERVAL_MS = 1e4;
5934
+ var TERMINAL_OUTBOX_RETENTION = 1e4;
5935
+ var TERMINAL_OUTBOX_PRUNE_INTERVAL = 100;
5264
5936
 
5265
5937
  class OutboxWorker {
5266
5938
  db;
@@ -5268,11 +5940,21 @@ class OutboxWorker {
5268
5940
  now;
5269
5941
  random;
5270
5942
  workerID = randomUUID9();
5271
- constructor(db, backends, now = Date.now, random = Math.random) {
5943
+ terminalRetention;
5944
+ terminalPruneInterval;
5945
+ terminalTransitions;
5946
+ constructor(db, backends, now = Date.now, random = Math.random, retention = {}) {
5272
5947
  this.db = db;
5273
5948
  this.backends = backends;
5274
5949
  this.now = now;
5275
5950
  this.random = random;
5951
+ this.terminalRetention = retention.terminalRetention ?? TERMINAL_OUTBOX_RETENTION;
5952
+ this.terminalPruneInterval = retention.terminalPruneInterval ?? TERMINAL_OUTBOX_PRUNE_INTERVAL;
5953
+ if (!Number.isSafeInteger(this.terminalRetention) || this.terminalRetention < 1)
5954
+ throw new Error("outbox_terminal_retention_invalid");
5955
+ if (!Number.isSafeInteger(this.terminalPruneInterval) || this.terminalPruneInterval < 1)
5956
+ throw new Error("outbox_terminal_prune_interval_invalid");
5957
+ this.terminalTransitions = this.terminalPruneInterval - 1;
5276
5958
  for (const backend of backends.values()) {
5277
5959
  if (backend.outboxProtocol !== "agz-memory-outbox/1") {
5278
5960
  throw new Error("outbox_backend_fencing_required");
@@ -5377,30 +6059,52 @@ class OutboxWorker {
5377
6059
  return transition === "succeeded" ? outcome : transition;
5378
6060
  }
5379
6061
  succeed(row) {
5380
- const result = this.db.query(`
5381
- UPDATE index_outbox
5382
- SET state = 'succeeded', completed_at = ?, lease_owner = NULL,
5383
- lease_expires_at = NULL, heartbeat_at = NULL, last_error_code = NULL
5384
- WHERE id = ? AND state = 'leased' AND lease_owner = ?
5385
- AND lease_generation = ? AND fence = ?
5386
- `).run(this.now(), row.id, this.workerID, row.lease_generation, row.fence);
5387
- return result.changes === 1 ? "succeeded" : "lost_lease";
6062
+ let changed = false;
6063
+ this.db.transaction(() => {
6064
+ const result = this.db.query(`
6065
+ UPDATE index_outbox
6066
+ SET state = 'succeeded', completed_at = ?, lease_owner = NULL,
6067
+ lease_expires_at = NULL, heartbeat_at = NULL, last_error_code = NULL
6068
+ WHERE id = ? AND state = 'leased' AND lease_owner = ?
6069
+ AND lease_generation = ? AND fence = ?
6070
+ `).run(this.now(), row.id, this.workerID, row.lease_generation, row.fence);
6071
+ changed = result.changes === 1;
6072
+ if (changed)
6073
+ this.pruneTerminalOutbox();
6074
+ })();
6075
+ return changed ? "succeeded" : "lost_lease";
5388
6076
  }
5389
6077
  fail(row, errorCode) {
5390
6078
  const dead = row.attempt_count >= 10;
5391
6079
  const availableAt = this.now() + retryDelay(row.attempt_count, this.random());
5392
- const result = this.db.query(`
5393
- UPDATE index_outbox
5394
- SET state = ?, available_at = ?, lease_owner = NULL,
5395
- lease_expires_at = NULL, heartbeat_at = NULL,
5396
- last_error_code = ?, completed_at = ?
5397
- WHERE id = ? AND state = 'leased' AND lease_owner = ?
5398
- AND lease_generation = ? AND fence = ?
5399
- `).run(dead ? "dead" : "pending", availableAt, errorCode, dead ? this.now() : null, row.id, this.workerID, row.lease_generation, row.fence);
5400
- if (result.changes !== 1)
6080
+ let changed = false;
6081
+ this.db.transaction(() => {
6082
+ const result = this.db.query(`
6083
+ UPDATE index_outbox
6084
+ SET state = ?, available_at = ?, lease_owner = NULL,
6085
+ lease_expires_at = NULL, heartbeat_at = NULL,
6086
+ last_error_code = ?, completed_at = ?
6087
+ WHERE id = ? AND state = 'leased' AND lease_owner = ?
6088
+ AND lease_generation = ? AND fence = ?
6089
+ `).run(dead ? "dead" : "pending", availableAt, errorCode, dead ? this.now() : null, row.id, this.workerID, row.lease_generation, row.fence);
6090
+ changed = result.changes === 1;
6091
+ if (changed && dead)
6092
+ this.pruneTerminalOutbox();
6093
+ })();
6094
+ if (!changed)
5401
6095
  return "lost_lease";
5402
6096
  return dead ? "dead" : "retry";
5403
6097
  }
6098
+ pruneTerminalOutbox() {
6099
+ this.terminalTransitions++;
6100
+ if (this.terminalTransitions < this.terminalPruneInterval)
6101
+ return;
6102
+ this.terminalTransitions = 0;
6103
+ this.db.query(`DELETE FROM index_outbox WHERE id IN (
6104
+ SELECT id FROM index_outbox WHERE state IN ('succeeded', 'dead')
6105
+ ORDER BY completed_at DESC, id DESC LIMIT -1 OFFSET ?
6106
+ )`).run(this.terminalRetention);
6107
+ }
5404
6108
  }
5405
6109
  function exportDocument(note) {
5406
6110
  return deriveDocument({
@@ -5544,6 +6248,7 @@ class RetrievalStore {
5544
6248
  this.backend = backend;
5545
6249
  }
5546
6250
  async retrieve(request) {
6251
+ assertTextLimit("query", request.query);
5547
6252
  const query = request.query.trim();
5548
6253
  const limit = boundedCardLimit(request.limit);
5549
6254
  if (!query || limit === 0 || Date.now() >= request.deadlineAt) {
@@ -5912,6 +6617,46 @@ function escapeText(value) {
5912
6617
  function escapeAttribute(value) {
5913
6618
  return escapeText(value).replaceAll('"', "&quot;").replaceAll("'", "&#39;");
5914
6619
  }
6620
+ // src/contracts/error.ts
6621
+ class MemoryBusinessError extends Error {
6622
+ code;
6623
+ correlationID;
6624
+ cause;
6625
+ constructor(code, message, correlationID = newCorrelationID(), cause) {
6626
+ super(message);
6627
+ this.code = code;
6628
+ this.correlationID = correlationID;
6629
+ this.cause = cause;
6630
+ }
6631
+ }
6632
+ function correlationID() {
6633
+ return newCorrelationID();
6634
+ }
6635
+ function newCorrelationID() {
6636
+ return crypto.randomUUID();
6637
+ }
6638
+ function businessError(code, message, id = correlationID(), cause) {
6639
+ return new MemoryBusinessError(code, message, id, cause);
6640
+ }
6641
+ function toPublicError(error) {
6642
+ return { code: error.code, correlationID: error.correlationID, retryable: error.code === "internal_error", message: error.message };
6643
+ }
6644
+ function asBusinessError(error, id = correlationID()) {
6645
+ if (error instanceof MemoryBusinessError)
6646
+ return error;
6647
+ if (error instanceof RangeError)
6648
+ return businessError("limit_exceeded", error.message, id, error);
6649
+ if (error instanceof TypeError) {
6650
+ if (error.message === "invalid_cursor")
6651
+ return businessError("invalid_cursor", "cursor is invalid", id, error);
6652
+ if (error.message === "cursor_scope_mismatch")
6653
+ return businessError("cursor_scope_mismatch", "cursor does not match this request", id, error);
6654
+ if (error.message === "stale_snapshot")
6655
+ return businessError("stale_cursor", "cursor snapshot is stale", id, error);
6656
+ return businessError("invalid_request", "request is invalid", id, error);
6657
+ }
6658
+ return businessError("internal_error", "memory operation failed", id, error);
6659
+ }
5915
6660
 
5916
6661
  // src/core.ts
5917
6662
  class MemoryCore {
@@ -5937,25 +6682,41 @@ function openMemoryCore(databasePath, options = {}) {
5937
6682
  }
5938
6683
  export {
5939
6684
  validateBackendHits,
6685
+ utf8Bytes,
6686
+ toPublicError,
5940
6687
  redactText,
5941
6688
  projectUserPrompt,
5942
6689
  projectToolSignal,
5943
6690
  projectSessionSummary,
5944
6691
  projectAssistantParts,
5945
6692
  parseCaptureEvent,
6693
+ paginate,
5946
6694
  openMemoryCore,
5947
6695
  normalizeSubjectKey,
6696
+ normalizeLegacyMutation,
6697
+ isMutationOperation,
5948
6698
  formatUntrustedContext,
5949
6699
  extractExplicitUserCandidate,
6700
+ encodeCursor,
6701
+ decodeCursor,
6702
+ correlationID,
5950
6703
  captureIdempotencyKey,
5951
6704
  captureEventSchema,
5952
6705
  canAutoWrite,
6706
+ businessError,
6707
+ boundedText,
6708
+ assertTextLimit,
6709
+ assertStrictMutationOperation,
6710
+ assertRequestLimit,
6711
+ asBusinessError,
5953
6712
  SUPPORTED_OPENCODE_VERSION,
5954
6713
  SCHEMA_VERSION,
5955
6714
  REDACTION_POLICY_VERSION,
5956
6715
  PRODUCT_VERSION,
5957
6716
  PREDICATES,
5958
6717
  MemoryCore,
6718
+ MemoryBusinessError,
6719
+ LIMITS,
5959
6720
  KINDS,
5960
6721
  INLINE_LIMIT,
5961
6722
  EXTRACTOR_VERSION,