@vaur94/agz-memory 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/admin.js CHANGED
@@ -2,14 +2,15 @@
2
2
  // @bun
3
3
 
4
4
  // src/admin/index.ts
5
- import { createHash as createHash7 } from "crypto";
5
+ import { createHash as createHash8 } from "crypto";
6
6
  import { Database as Database4 } from "bun:sqlite";
7
7
  import {
8
8
  existsSync as existsSync5,
9
- lstatSync as lstatSync4,
9
+ lstatSync as lstatSync5,
10
10
  readdirSync as readdirSync2,
11
- readFileSync as readFileSync4,
12
- rmSync as rmSync3
11
+ readFileSync as readFileSync5,
12
+ realpathSync,
13
+ rmSync as rmSync4
13
14
  } from "fs";
14
15
  import { basename as basename3, dirname as dirname3, resolve as resolve3 } from "path";
15
16
 
@@ -18,7 +19,8 @@ import { homedir } from "os";
18
19
  import { join } from "path";
19
20
  function resolveConfig(environment = process.env) {
20
21
  const databasePath = environment.OPENCODE_MEMORY_DATABASE_PATH?.trim() || join(environment.HOME ?? homedir(), ".local", "share", "opencode-memory", "memory.sqlite");
21
- return { databasePath };
22
+ const quarantineKeyringPath = environment.OPENCODE_MEMORY_QUARANTINE_KEYRING_PATH?.trim() || `${databasePath}.quarantine-keys`;
23
+ return { databasePath, quarantineKeyringPath };
22
24
  }
23
25
 
24
26
  // src/db.ts
@@ -746,7 +748,7 @@ function inspectDatabase(db) {
746
748
  }
747
749
  return { integrity, foreignKeyViolations, schemaVersion, counts };
748
750
  }
749
- function assertHealthyDatabase(db) {
751
+ function assertHealthyDatabase(db, options = {}) {
750
752
  const health = inspectDatabase(db);
751
753
  if (health.integrity !== "ok") {
752
754
  throw new Error(`database integrity check failed: ${health.integrity}`);
@@ -754,7 +756,7 @@ function assertHealthyDatabase(db) {
754
756
  if (health.foreignKeyViolations.length > 0) {
755
757
  throw new Error(`database foreign key check failed: ${health.foreignKeyViolations.length} violation(s)`);
756
758
  }
757
- if (health.schemaVersion === 11)
759
+ if (options.verifySchema !== false && health.schemaVersion === 11)
758
760
  assertSchemaV11(db);
759
761
  return health;
760
762
  }
@@ -2180,7 +2182,7 @@ var V9_V10_COLUMNS = {
2180
2182
  "completed_at"
2181
2183
  ]
2182
2184
  };
2183
- function assertLegacySchemaIdentity(db, version) {
2185
+ function assertLegacySchemaIdentity(db, version, options = {}) {
2184
2186
  if (version < 2 || version > 10)
2185
2187
  throw new Error("unrecognized_database");
2186
2188
  const applicationID = db.query("PRAGMA application_id").get().application_id;
@@ -2188,12 +2190,14 @@ function assertLegacySchemaIdentity(db, version) {
2188
2190
  throw new Error("unrecognized_database");
2189
2191
  if (version === 2 && tableExists(db, "memory_items")) {
2190
2192
  assertV2Identity(db);
2191
- assertHealthyDatabase(db);
2193
+ if (options.verifyHealth)
2194
+ assertHealthyDatabase(db);
2192
2195
  return;
2193
2196
  }
2194
2197
  if (version < 8) {
2195
2198
  assertPreV8Identity(db, version);
2196
- assertHealthyDatabase(db);
2199
+ if (options.verifyHealth)
2200
+ assertHealthyDatabase(db);
2197
2201
  return;
2198
2202
  }
2199
2203
  const states = db.query("SELECT version FROM schema_state").all();
@@ -2218,7 +2222,8 @@ function assertLegacySchemaIdentity(db, version) {
2218
2222
  throw new Error("unrecognized_database");
2219
2223
  }
2220
2224
  }
2221
- assertHealthyDatabase(db);
2225
+ if (options.verifyHealth)
2226
+ assertHealthyDatabase(db);
2222
2227
  if (version === 10)
2223
2228
  assertV10SourceDatabase(db);
2224
2229
  }
@@ -3416,7 +3421,7 @@ function migrateV9ToV10(db) {
3416
3421
  }
3417
3422
 
3418
3423
  // src/version.ts
3419
- var PRODUCT_VERSION = "0.5.0";
3424
+ var PRODUCT_VERSION = "0.5.1";
3420
3425
 
3421
3426
  // src/db.ts
3422
3427
  var DDL = `
@@ -3459,15 +3464,25 @@ CREATE INDEX IF NOT EXISTS note_edges_target_idx ON note_edges(project_id, targe
3459
3464
  CREATE TABLE IF NOT EXISTS schema_state (version INTEGER PRIMARY KEY);
3460
3465
  `;
3461
3466
  var PRE_OPEN_PROBE_TIMEOUT_MS = 5000;
3462
- function openMemoryDatabase(path) {
3467
+ function timeMigrationStage(timing, stage, work) {
3468
+ if (!timing)
3469
+ return work();
3470
+ const started = performance.now();
3471
+ try {
3472
+ return work();
3473
+ } finally {
3474
+ timing.phases.push({ stage, elapsedMs: Math.round((performance.now() - started) * 1000) / 1000 });
3475
+ }
3476
+ }
3477
+ function openMemoryDatabase(path, options = {}) {
3463
3478
  ensureDatabaseParent(path);
3464
- assertSupportedDatabaseBeforeOpen(path);
3479
+ assertSupportedDatabaseBeforeOpen(path, false);
3465
3480
  recoverStaleMaintenanceGate(path, () => assertSupportedDatabaseBeforeOpen(path));
3466
3481
  let lock = acquireMigrationLock(path, SCHEMA_VERSION);
3467
3482
  let lease = acquireDatabaseLease(path);
3468
3483
  let db;
3469
3484
  try {
3470
- assertSupportedDatabaseBeforeOpen(path);
3485
+ assertSupportedDatabaseBeforeOpen(path, false);
3471
3486
  db = openDatabase(path);
3472
3487
  } catch (error) {
3473
3488
  lease.release();
@@ -3489,7 +3504,7 @@ function openMemoryDatabase(path) {
3489
3504
  lease.release();
3490
3505
  lease = undefined;
3491
3506
  lease = acquireDatabaseLease(path);
3492
- assertSupportedDatabaseBeforeOpen(path);
3507
+ assertSupportedDatabaseBeforeOpen(path, false);
3493
3508
  db = openDatabase(path);
3494
3509
  dbOpen = true;
3495
3510
  if (hasApplicationObjects(db)) {
@@ -3539,7 +3554,7 @@ function openMemoryDatabase(path) {
3539
3554
  lease.release();
3540
3555
  lease = undefined;
3541
3556
  lease = acquireDatabaseLease(path);
3542
- assertSupportedDatabaseBeforeOpen(path);
3557
+ assertSupportedDatabaseBeforeOpen(path, false);
3543
3558
  db = openDatabase(path);
3544
3559
  dbOpen = true;
3545
3560
  let migrationVersion = getSchemaVersion(db);
@@ -3567,7 +3582,7 @@ function openMemoryDatabase(path) {
3567
3582
  lease.release();
3568
3583
  lease = undefined;
3569
3584
  maintenance = acquireMaintenanceGate(path);
3570
- assertSupportedDatabaseBeforeOpen(path);
3585
+ timeMigrationStage(options.timing, "source-validation", () => assertSupportedDatabaseBeforeOpen(path, false));
3571
3586
  db = openDatabase(path);
3572
3587
  dbOpen = true;
3573
3588
  migrationVersion = getSchemaVersion(db);
@@ -3597,12 +3612,12 @@ function openMemoryDatabase(path) {
3597
3612
  lease = undefined;
3598
3613
  return opened3;
3599
3614
  }
3600
- backup = createVerifiedBackup(db, path, migrationVersion?.version ?? 2, SCHEMA_VERSION, PRODUCT_VERSION);
3615
+ backup = timeMigrationStage(options.timing, "backup-checkpoint", () => createVerifiedBackup(db, path, migrationVersion?.version ?? 2, SCHEMA_VERSION, PRODUCT_VERSION));
3601
3616
  db.exec("PRAGMA foreign_keys=OFF");
3602
3617
  if (!migrationVersion && hasLegacyV2(db)) {
3603
3618
  db.exec(DDL);
3604
3619
  db.exec("CREATE VIRTUAL TABLE IF NOT EXISTS notes_fts USING fts5(id UNINDEXED, title, summary, content, tokenize='unicode61')");
3605
- migrateFromV2(db, path);
3620
+ timeMigrationStage(options.timing, "v2-import", () => migrateFromV2(db, path));
3606
3621
  } else if (!migrationVersion) {
3607
3622
  db.exec(DDL);
3608
3623
  db.exec("CREATE VIRTUAL TABLE IF NOT EXISTS notes_fts USING fts5(id UNINDEXED, title, summary, content, tokenize='unicode61')");
@@ -3618,24 +3633,25 @@ function openMemoryDatabase(path) {
3618
3633
  }
3619
3634
  let version = getSchemaVersion(db)?.version ?? 8;
3620
3635
  if (version < 9) {
3621
- db.transaction(() => migrateV8ToV9(db))();
3636
+ timeMigrationStage(options.timing, "v8-to-v9", () => db.transaction(() => migrateV8ToV9(db))());
3622
3637
  version = 9;
3623
3638
  }
3624
3639
  if (version < 10) {
3625
- db.transaction(() => migrateV9ToV10(db))();
3640
+ timeMigrationStage(options.timing, "v9-to-v10", () => db.transaction(() => migrateV9ToV10(db))());
3626
3641
  version = 10;
3627
3642
  }
3628
3643
  if (version < SCHEMA_VERSION) {
3629
- db.transaction(() => {
3644
+ timeMigrationStage(options.timing, "v10-to-v11", () => db.transaction(() => {
3630
3645
  db.exec(`PRAGMA application_id = ${APPLICATION_ID}`);
3631
3646
  migrateV10ToV11(db);
3632
- })();
3647
+ })());
3633
3648
  }
3634
3649
  db.exec("PRAGMA foreign_keys=ON");
3635
3650
  if (db.query("PRAGMA foreign_keys").get().foreign_keys !== 1) {
3636
3651
  throw new Error("failed to enable database foreign keys");
3637
3652
  }
3638
- assertHealthyDatabase(db);
3653
+ timeMigrationStage(options.timing, "fingerprint", () => assertSchemaV11(db));
3654
+ timeMigrationStage(options.timing, "deep-health", () => assertHealthyDatabase(db, { verifySchema: false }));
3639
3655
  console.warn(`[agz-memory] migrated to v${SCHEMA_VERSION} (backup: ${backup.manifestPath})`);
3640
3656
  backup = undefined;
3641
3657
  db.close();
@@ -3743,11 +3759,11 @@ function openDatabase(path) {
3743
3759
  throw error;
3744
3760
  }
3745
3761
  }
3746
- function assertSupportedDatabaseBeforeOpen(path) {
3762
+ function assertSupportedDatabaseBeforeOpen(path, verifyHealth = true) {
3747
3763
  const deadline = Date.now() + PRE_OPEN_PROBE_TIMEOUT_MS;
3748
3764
  while (true) {
3749
3765
  try {
3750
- assertSupportedDatabaseBeforeOpenOnce(path);
3766
+ assertSupportedDatabaseBeforeOpenOnce(path, verifyHealth);
3751
3767
  return;
3752
3768
  } catch (error) {
3753
3769
  if (!isSQLiteBusyError(error) || Date.now() >= deadline)
@@ -3756,19 +3772,19 @@ function assertSupportedDatabaseBeforeOpen(path) {
3756
3772
  }
3757
3773
  }
3758
3774
  }
3759
- function assertSupportedDatabaseBeforeOpenOnce(path) {
3775
+ function assertSupportedDatabaseBeforeOpenOnce(path, verifyHealth) {
3760
3776
  assertDatabasePath(path);
3761
3777
  if (!existsSync4(path))
3762
3778
  return;
3763
3779
  const db = new Database3(path, { readonly: true });
3764
3780
  try {
3765
3781
  assertDatabasePath(path);
3766
- assertSupportedDatabase(db);
3782
+ assertSupportedDatabase(db, verifyHealth);
3767
3783
  } finally {
3768
3784
  db.close();
3769
3785
  }
3770
3786
  }
3771
- function assertSupportedDatabase(db) {
3787
+ function assertSupportedDatabase(db, verifyHealth = true) {
3772
3788
  const existingVersion = getSchemaVersion(db);
3773
3789
  if (existingVersion && existingVersion.version > SCHEMA_VERSION) {
3774
3790
  throw new Error(`database schema v${existingVersion.version} is newer than supported v${SCHEMA_VERSION}`);
@@ -3779,7 +3795,7 @@ function assertSupportedDatabase(db) {
3779
3795
  if (!existingVersion) {
3780
3796
  if (!hasLegacyV2(db))
3781
3797
  throw new Error("unrecognized_database");
3782
- assertLegacySchemaIdentity(db, 2);
3798
+ assertLegacySchemaIdentity(db, 2, { verifyHealth });
3783
3799
  return;
3784
3800
  }
3785
3801
  if (existingVersion.version === SCHEMA_VERSION || hasV11Marker) {
@@ -3789,7 +3805,7 @@ function assertSupportedDatabase(db) {
3789
3805
  if (existingVersion.version < 2 || existingVersion.version > 10) {
3790
3806
  throw new Error("unrecognized_database");
3791
3807
  }
3792
- assertLegacySchemaIdentity(db, existingVersion.version);
3808
+ assertLegacySchemaIdentity(db, existingVersion.version, { verifyHealth });
3793
3809
  }
3794
3810
  function assertDatabasePath(path) {
3795
3811
  try {
@@ -4274,17 +4290,550 @@ function derivedHashMismatches(db) {
4274
4290
  return mismatches;
4275
4291
  }
4276
4292
 
4293
+ // src/admin/quarantine.ts
4294
+ var KEYED_VERSION = /^redaction\/1;quarantine-key=([0-9a-f]{24});quarantine-digest=2$/;
4295
+ var UNAVAILABLE_VERSION = /^redaction\/1;quarantine-key=unavailable;quarantine-digest=2$/;
4296
+ function quarantinePrivacyReport(db) {
4297
+ const rows = db.query(`SELECT redaction_version, COUNT(*) AS count
4298
+ FROM capture_events
4299
+ WHERE state = 'quarantined'
4300
+ GROUP BY redaction_version`).all();
4301
+ let quarantinedEvents = 0;
4302
+ let keyedEvents = 0;
4303
+ let unavailableKeyEvents = 0;
4304
+ let legacyOrUnknownEvents = 0;
4305
+ const keyIDs = new Set;
4306
+ for (const row of rows) {
4307
+ quarantinedEvents += row.count;
4308
+ const keyed = KEYED_VERSION.exec(row.redaction_version);
4309
+ if (keyed) {
4310
+ keyedEvents += row.count;
4311
+ keyIDs.add(keyed[1]);
4312
+ } else if (UNAVAILABLE_VERSION.test(row.redaction_version)) {
4313
+ unavailableKeyEvents += row.count;
4314
+ } else {
4315
+ legacyOrUnknownEvents += row.count;
4316
+ }
4317
+ }
4318
+ return {
4319
+ quarantinedEvents,
4320
+ keyedEvents,
4321
+ unavailableKeyEvents,
4322
+ legacyOrUnknownEvents,
4323
+ keyIDs: [...keyIDs].sort(),
4324
+ digest: {
4325
+ algorithm: "HMAC-SHA256",
4326
+ input: "quarantine-source-identity-and-redacted-payload/2",
4327
+ storage: "capture_events.payload_hash",
4328
+ keyID: "capture_events.redaction_version quarantine-key suffix"
4329
+ }
4330
+ };
4331
+ }
4332
+
4333
+ // src/admin/reindex.ts
4334
+ import { spawnSync } from "child_process";
4335
+ import { createHash as createHash7, randomUUID as randomUUID7 } from "crypto";
4336
+ import {
4337
+ closeSync as closeSync3,
4338
+ constants as constants3,
4339
+ fsyncSync as fsyncSync3,
4340
+ fstatSync as fstatSync2,
4341
+ lstatSync as lstatSync4,
4342
+ mkdirSync as mkdirSync4,
4343
+ openSync as openSync3,
4344
+ readFileSync as readFileSync4,
4345
+ renameSync as renameSync4,
4346
+ rmSync as rmSync3,
4347
+ writeSync as writeSync2
4348
+ } from "fs";
4349
+ import { hostname as hostname3 } from "os";
4350
+ import { join as join4 } from "path";
4351
+ var MAX_BATCH_SIZE = 500;
4352
+ function classifyReindexOwner(owner, localHostname, processAlive, currentProcessStart) {
4353
+ if (!isOwnerMetadata(owner) || owner.hostname !== localHostname)
4354
+ return "unverifiable";
4355
+ if (!processAlive)
4356
+ return "stale";
4357
+ if (!currentProcessStart)
4358
+ return "unverifiable";
4359
+ return currentProcessStart === owner.processStart ? "live" : "stale";
4360
+ }
4361
+ function runResumableReindex(path, databaseID, backend, batchSize, maxBatches, testOptions = {}) {
4362
+ if (!Number.isSafeInteger(batchSize) || batchSize < 1 || batchSize > MAX_BATCH_SIZE) {
4363
+ throw new Error(`reindex --batch-size must be 1..${MAX_BATCH_SIZE}`);
4364
+ }
4365
+ if (maxBatches !== undefined && (!Number.isSafeInteger(maxBatches) || maxBatches < 1 || maxBatches > 1e4)) {
4366
+ throw new Error("reindex --max-batches must be 1..10000");
4367
+ }
4368
+ const databaseFile = captureDatabaseFile(path);
4369
+ const directory = openSidecarDirectory(path);
4370
+ const file = join4(directory.path, `${createHash7("sha256").update(backend).digest("hex")}.json`);
4371
+ const release = acquireOwnerLock(file, directory, testOptions);
4372
+ try {
4373
+ testOptions.afterOwnerLockAcquired?.();
4374
+ const existing = readState(file, directory, path, databaseID, backend);
4375
+ let state = existing ?? createState(path, databaseID, backend, databaseFile);
4376
+ if (!existing)
4377
+ writeState(file, directory, state);
4378
+ let batches = 0;
4379
+ while (true) {
4380
+ const result = runBatch(path, state, batchSize, testOptions);
4381
+ state = result.state;
4382
+ batches++;
4383
+ if (result.done) {
4384
+ removePrivateFile(file, directory, "reindex state file", true);
4385
+ return {
4386
+ backend,
4387
+ generation: state.generation,
4388
+ purges: state.purges,
4389
+ queued: state.queued,
4390
+ quarantined: state.quarantined,
4391
+ resumed: existing !== undefined
4392
+ };
4393
+ }
4394
+ testOptions.afterCommitBeforeStateWrite?.();
4395
+ writeState(file, directory, state);
4396
+ if (maxBatches !== undefined && batches >= maxBatches) {
4397
+ return {
4398
+ backend,
4399
+ generation: state.generation,
4400
+ purges: state.purges,
4401
+ queued: state.queued,
4402
+ quarantined: state.quarantined,
4403
+ incomplete: true,
4404
+ resumed: existing !== undefined
4405
+ };
4406
+ }
4407
+ }
4408
+ } finally {
4409
+ try {
4410
+ release();
4411
+ } finally {
4412
+ closeSync3(directory.fd);
4413
+ }
4414
+ }
4415
+ }
4416
+ function acquireOwnerLock(stateFile, directory, testOptions) {
4417
+ const lock = `${stateFile}.lock`;
4418
+ assertReindexOwnerLivenessSupported();
4419
+ const processStart = readReindexProcessStartMarker(process.pid);
4420
+ if (!processStart)
4421
+ throw new Error(`reindex owner liveness is unavailable on ${process.platform}`);
4422
+ const owner = {
4423
+ ownerID: randomUUID7(),
4424
+ pid: process.pid,
4425
+ processStart,
4426
+ hostname: hostname3(),
4427
+ createdAt: Date.now()
4428
+ };
4429
+ for (let attempt = 0;attempt < 3; attempt++) {
4430
+ let fd;
4431
+ try {
4432
+ assertSidecarDirectory(directory);
4433
+ fd = openSync3(lock, constants3.O_WRONLY | constants3.O_CREAT | constants3.O_EXCL | constants3.O_NOFOLLOW, 384);
4434
+ writeSync2(fd, `${JSON.stringify(owner)}
4435
+ `);
4436
+ fsyncSync3(fd);
4437
+ closeSync3(fd);
4438
+ fd = undefined;
4439
+ assertSidecarDirectory(directory);
4440
+ return () => {
4441
+ const current = readPrivateJsonFile(lock, directory, "reindex owner lock", false);
4442
+ if (!current || !isOwnerMetadata(current.value) || current.value.ownerID !== owner.ownerID) {
4443
+ throw new Error("reindex owner lock changed before release");
4444
+ }
4445
+ removeOwnedPrivateFile(lock, directory, "reindex owner lock", current.identity, owner.ownerID);
4446
+ };
4447
+ } catch (error) {
4448
+ if (fd !== undefined)
4449
+ closeSync3(fd);
4450
+ if (!hasCode(error, "EEXIST") || attempt === 2)
4451
+ throw new Error("reindex already owned");
4452
+ const observed = readPrivateJsonFile(lock, directory, "reindex owner lock", false);
4453
+ if (!observed || !isOwnerMetadata(observed.value))
4454
+ throw new Error("reindex owner lock is unverifiable");
4455
+ const current = observed.value;
4456
+ const pid = current.pid;
4457
+ let alive = false;
4458
+ try {
4459
+ process.kill(pid, 0);
4460
+ alive = true;
4461
+ } catch (probe) {
4462
+ if (!hasCode(probe, "ESRCH"))
4463
+ throw new Error("reindex owner lock is unverifiable");
4464
+ }
4465
+ const status = classifyReindexOwner(current, hostname3(), alive, alive ? readReindexProcessStartMarker(pid) : null);
4466
+ if (status === "live")
4467
+ throw new Error("reindex already owned");
4468
+ if (status === "unverifiable")
4469
+ throw new Error("reindex owner lock is unverifiable");
4470
+ testOptions.beforeStaleLockTakeover?.();
4471
+ quarantineStaleLock(lock, directory, observed.identity, current, owner);
4472
+ }
4473
+ }
4474
+ throw new Error("reindex already owned");
4475
+ }
4476
+ function readReindexProcessStartMarker(pid, platform3 = process.platform, readers = {}) {
4477
+ if (platform3 === "linux") {
4478
+ try {
4479
+ const value = "readLinuxStat" in readers ? readers.readLinuxStat?.(pid) : readFileSync4(`/proc/${pid}/stat`, "utf8");
4480
+ if (!value)
4481
+ return null;
4482
+ const close = value.lastIndexOf(")");
4483
+ if (close < 0)
4484
+ return null;
4485
+ const start = value.slice(close + 1).trim().split(/\s+/)[19];
4486
+ return start ? `linux:${start}` : null;
4487
+ } catch {
4488
+ return null;
4489
+ }
4490
+ }
4491
+ if (platform3 === "darwin") {
4492
+ try {
4493
+ const output = "readMacOSPs" in readers ? readers.readMacOSPs?.(pid) : readMacOSProcessStart(pid);
4494
+ return output ? `macos:${output}` : null;
4495
+ } catch {
4496
+ return null;
4497
+ }
4498
+ }
4499
+ return null;
4500
+ }
4501
+ function assertReindexOwnerLivenessSupported(platform3 = process.platform) {
4502
+ if (platform3 !== "linux" && platform3 !== "darwin") {
4503
+ throw new Error(`reindex owner liveness is unsupported on ${platform3}`);
4504
+ }
4505
+ }
4506
+ function readMacOSProcessStart(pid) {
4507
+ const result = spawnSync("/bin/ps", ["-o", "lstart=", "-p", String(pid)], { encoding: "utf8" });
4508
+ if (result.status !== 0 || typeof result.stdout !== "string")
4509
+ return null;
4510
+ const marker = result.stdout.trim();
4511
+ return marker || null;
4512
+ }
4513
+ function createState(path, databaseID, backend, databaseFile) {
4514
+ preflightDatabaseIdentity(path, databaseID, databaseFile);
4515
+ const opened = openMemoryDatabase(path);
4516
+ try {
4517
+ assertDatabaseIdentity(path, opened.db, databaseID, databaseFile);
4518
+ const generation = opened.db.query("SELECT COALESCE(MAX(generation), 0) + 1 AS value FROM index_outbox WHERE backend = ?").get(backend).value;
4519
+ return {
4520
+ databaseID,
4521
+ databaseFile,
4522
+ backend,
4523
+ generation,
4524
+ phase: "purges",
4525
+ projectID: null,
4526
+ noteID: null,
4527
+ startedAt: Date.now(),
4528
+ snapshot: randomUUID7(),
4529
+ purges: 0,
4530
+ queued: 0,
4531
+ quarantined: 0
4532
+ };
4533
+ } finally {
4534
+ opened.close();
4535
+ }
4536
+ }
4537
+ function runBatch(path, state, limit, testOptions) {
4538
+ preflightDatabaseIdentity(path, state.databaseID, state.databaseFile);
4539
+ const opened = openMemoryDatabase(path);
4540
+ try {
4541
+ const next = { ...state };
4542
+ testOptions.beforeBatchDatabaseIdentity?.();
4543
+ assertDatabaseIdentity(path, opened.db, state.databaseID, state.databaseFile);
4544
+ opened.db.exec("BEGIN IMMEDIATE");
4545
+ try {
4546
+ if (next.phase === "purges") {
4547
+ const rows = opened.db.query("SELECT id FROM projects WHERE id > ? ORDER BY id LIMIT ?").all(next.projectID ?? "", limit + 1);
4548
+ for (const row of rows.slice(0, limit)) {
4549
+ insert(opened.db, next, "purge-project", row.id, null, null, null);
4550
+ next.purges++;
4551
+ next.projectID = row.id;
4552
+ }
4553
+ if (rows.length <= limit) {
4554
+ next.phase = "notes";
4555
+ next.projectID = null;
4556
+ next.noteID = null;
4557
+ }
4558
+ } else {
4559
+ const rows = opened.db.query("SELECT id, project_id, current_revision, kind, title, summary, content FROM notes WHERE status = 'active' AND (project_id > ? OR (project_id = ? AND id > ?)) ORDER BY project_id, id LIMIT ?").all(next.projectID ?? "", next.projectID ?? "", next.noteID ?? "", limit + 1);
4560
+ for (const row of rows.slice(0, limit)) {
4561
+ next.projectID = row.project_id;
4562
+ next.noteID = row.id;
4563
+ const document = deriveDocument({ projectID: row.project_id, noteID: row.id, revision: row.current_revision, kind: row.kind, title: row.title, summary: row.summary, content: row.content });
4564
+ if (!document) {
4565
+ next.quarantined++;
4566
+ continue;
4567
+ }
4568
+ insert(opened.db, next, "upsert-note", row.project_id, row.id, row.current_revision, document.contentHash);
4569
+ next.queued++;
4570
+ }
4571
+ if (rows.length <= limit) {
4572
+ opened.db.exec("COMMIT");
4573
+ return { state: next, done: true };
4574
+ }
4575
+ }
4576
+ opened.db.exec("COMMIT");
4577
+ return { state: next, done: false };
4578
+ } catch (error) {
4579
+ try {
4580
+ opened.db.exec("ROLLBACK");
4581
+ } catch {}
4582
+ throw error;
4583
+ }
4584
+ } finally {
4585
+ opened.close();
4586
+ }
4587
+ }
4588
+ function captureDatabaseFile(path) {
4589
+ const stat = regularFile(path, "database");
4590
+ return { dev: stat.dev, ino: stat.ino };
4591
+ }
4592
+ function assertDatabaseIdentity(path, db, databaseID, expectedFile) {
4593
+ const stat = regularFile(path, "database");
4594
+ if (!sameFile(stat, expectedFile))
4595
+ throw new Error("reindex database file changed");
4596
+ const row = db.query("SELECT database_id FROM agz_meta WHERE id = 1").get();
4597
+ if (row?.database_id !== databaseID)
4598
+ throw new Error("reindex database identity changed");
4599
+ }
4600
+ function preflightDatabaseIdentity(path, databaseID, expectedFile) {
4601
+ if (!sameFile(regularFile(path, "database"), expectedFile)) {
4602
+ throw new Error("reindex database file changed; remove state and restart reindex");
4603
+ }
4604
+ const opened = openReadOnlyMemoryDatabase(path);
4605
+ try {
4606
+ assertDatabaseIdentity(path, opened.db, databaseID, expectedFile);
4607
+ } finally {
4608
+ opened.close();
4609
+ }
4610
+ if (!sameFile(regularFile(path, "database"), expectedFile)) {
4611
+ throw new Error("reindex database file changed; remove state and restart reindex");
4612
+ }
4613
+ }
4614
+ function insert(db, state, operation, projectID, noteID, revision, contentHash) {
4615
+ const key = hashTuple("outbox-operation", 2, [state.backend, operation, projectID, noteID, revision, contentHash, state.generation]);
4616
+ if (db.query("SELECT 1 FROM index_outbox WHERE operation_key = ?").get(key))
4617
+ return;
4618
+ db.query("INSERT INTO index_outbox (backend, operation_key, operation, project_id, note_id, revision, content_hash, generation, lease_generation, fence, state, attempt_count, available_at, heartbeat_at, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, 0, 'pending', 0, ?, NULL, ?)").run(state.backend, key, operation, projectID, noteID, revision, contentHash, state.generation, Date.now(), Date.now());
4619
+ }
4620
+ function openSidecarDirectory(databasePath) {
4621
+ if (process.platform === "win32" || !constants3.O_DIRECTORY || !constants3.O_NOFOLLOW) {
4622
+ throw new Error("reindex sidecar directory security is unsupported on this platform");
4623
+ }
4624
+ const path = `${databasePath}.reindex`;
4625
+ try {
4626
+ mkdirSync4(path, { mode: 448 });
4627
+ } catch (error) {
4628
+ if (!hasCode(error, "EEXIST"))
4629
+ throw error;
4630
+ }
4631
+ const before = lstatSync4(path);
4632
+ if (!before.isDirectory() || before.isSymbolicLink() || (before.mode & 63) !== 0) {
4633
+ throw new Error("reindex state directory is unsafe");
4634
+ }
4635
+ const fd = openSync3(path, constants3.O_RDONLY | constants3.O_DIRECTORY | constants3.O_NOFOLLOW);
4636
+ try {
4637
+ const opened = fstatSync2(fd);
4638
+ if (!sameFile(before, opened))
4639
+ throw new Error("reindex state directory changed while opening");
4640
+ const directory = { path, fd, dev: opened.dev, ino: opened.ino };
4641
+ assertSidecarDirectory(directory);
4642
+ return directory;
4643
+ } catch (error) {
4644
+ closeSync3(fd);
4645
+ throw error;
4646
+ }
4647
+ }
4648
+ function assertSidecarDirectory(directory) {
4649
+ const opened = fstatSync2(directory.fd);
4650
+ const current = lstatSync4(directory.path);
4651
+ if (!opened.isDirectory() || !current.isDirectory() || current.isSymbolicLink() || !sameFile(opened, directory) || !sameFile(current, directory) || (current.mode & 63) !== 0) {
4652
+ throw new Error("reindex state directory changed");
4653
+ }
4654
+ }
4655
+ function readState(path, directory, databasePath, databaseID, backend) {
4656
+ const parsed = readPrivateJson(path, directory, "reindex state file", true);
4657
+ if (parsed === undefined)
4658
+ return;
4659
+ if (!parsed || typeof parsed !== "object")
4660
+ throw new Error("reindex state is invalid");
4661
+ const state = parsed;
4662
+ if (state.databaseID !== databaseID || state.backend !== backend || !isFileIdentity(state.databaseFile) || !Number.isSafeInteger(state.generation) || state.generation < 1 || state.phase !== "purges" && state.phase !== "notes" || !Number.isSafeInteger(state.startedAt) || typeof state.snapshot !== "string" || !/^[0-9a-f-]{36}$/i.test(state.snapshot) || !nullableString(state.projectID) || !nullableString(state.noteID) || ![state.purges, state.queued, state.quarantined].every((value) => Number.isSafeInteger(value) && value >= 0)) {
4663
+ throw new Error("reindex state is invalid");
4664
+ }
4665
+ if (!sameFile(regularFile(databasePath, "database"), state.databaseFile)) {
4666
+ throw new Error("reindex state is stale; remove state and restart reindex");
4667
+ }
4668
+ return state;
4669
+ }
4670
+ function readPrivateJson(path, directory, label, optional) {
4671
+ return readPrivateJsonFile(path, directory, label, optional)?.value;
4672
+ }
4673
+ function readPrivateJsonFile(path, directory, label, optional) {
4674
+ let fd;
4675
+ try {
4676
+ assertSidecarDirectory(directory);
4677
+ const before = regularFile(path, label);
4678
+ fd = openSync3(path, constants3.O_RDONLY | constants3.O_NOFOLLOW);
4679
+ const opened = fstatSync2(fd);
4680
+ if (!sameFile(before, opened))
4681
+ throw new Error(`${label} changed while opening`);
4682
+ const raw = readFileSync4(fd, "utf8");
4683
+ const after = regularFile(path, label);
4684
+ if (!sameFile(after, opened))
4685
+ throw new Error(`${label} changed while reading`);
4686
+ assertSidecarDirectory(directory);
4687
+ return { value: JSON.parse(raw), identity: { dev: opened.dev, ino: opened.ino } };
4688
+ } catch (error) {
4689
+ if (optional && hasCode(error, "ENOENT")) {
4690
+ assertSidecarDirectory(directory);
4691
+ return;
4692
+ }
4693
+ throw error;
4694
+ } finally {
4695
+ if (fd !== undefined)
4696
+ closeSync3(fd);
4697
+ }
4698
+ }
4699
+ function quarantineStaleLock(lock, directory, observedIdentity, observedOwner, contender) {
4700
+ const takeover = `${lock}.takeover`;
4701
+ let takeoverFd;
4702
+ let takeoverIdentity;
4703
+ try {
4704
+ assertSidecarDirectory(directory);
4705
+ takeoverFd = openSync3(takeover, constants3.O_WRONLY | constants3.O_CREAT | constants3.O_EXCL | constants3.O_NOFOLLOW, 384);
4706
+ const takeoverStat = fstatSync2(takeoverFd);
4707
+ takeoverIdentity = { dev: takeoverStat.dev, ino: takeoverStat.ino };
4708
+ writeSync2(takeoverFd, `${JSON.stringify(contender)}
4709
+ `);
4710
+ fsyncSync3(takeoverFd);
4711
+ closeSync3(takeoverFd);
4712
+ takeoverFd = undefined;
4713
+ const current = readPrivateJsonFile(lock, directory, "reindex owner lock", true);
4714
+ if (!current || !sameFile(current.identity, observedIdentity) || !isOwnerMetadata(current.value) || current.value.ownerID !== observedOwner.ownerID) {
4715
+ return;
4716
+ }
4717
+ const quarantine = `${lock}.${contender.ownerID}.stale`;
4718
+ assertReplacementTargetSafe(quarantine, "reindex stale lock quarantine");
4719
+ renameSync4(lock, quarantine);
4720
+ const claimed = readPrivateJsonFile(quarantine, directory, "reindex stale lock quarantine", false);
4721
+ if (!claimed || !sameFile(claimed.identity, observedIdentity) || !isOwnerMetadata(claimed.value) || claimed.value.ownerID !== observedOwner.ownerID) {
4722
+ throw new Error("reindex owner lock changed during stale takeover");
4723
+ }
4724
+ removeOwnedPrivateFile(quarantine, directory, "reindex stale lock quarantine", observedIdentity, observedOwner.ownerID);
4725
+ } catch (error) {
4726
+ if (hasCode(error, "EEXIST"))
4727
+ throw new Error("reindex already owned");
4728
+ throw error;
4729
+ } finally {
4730
+ if (takeoverFd !== undefined)
4731
+ closeSync3(takeoverFd);
4732
+ if (takeoverIdentity) {
4733
+ removeOwnedPrivateFile(takeover, directory, "reindex stale takeover lock", takeoverIdentity, contender.ownerID);
4734
+ }
4735
+ }
4736
+ }
4737
+ function removeOwnedPrivateFile(path, directory, label, expectedIdentity, expectedOwnerID) {
4738
+ const current = readPrivateJsonFile(path, directory, label, false);
4739
+ if (!current || !sameFile(current.identity, expectedIdentity) || !isOwnerMetadata(current.value) || current.value.ownerID !== expectedOwnerID) {
4740
+ throw new Error(`${label} changed before removal`);
4741
+ }
4742
+ removePrivateFile(path, directory, label, false);
4743
+ }
4744
+ function writeState(path, directory, state) {
4745
+ const temporary = `${path}.${randomUUID7()}.tmp`;
4746
+ let fd;
4747
+ let created = false;
4748
+ try {
4749
+ assertSidecarDirectory(directory);
4750
+ fd = openSync3(temporary, constants3.O_WRONLY | constants3.O_CREAT | constants3.O_EXCL | constants3.O_NOFOLLOW, 384);
4751
+ created = true;
4752
+ writeSync2(fd, `${JSON.stringify(state)}
4753
+ `);
4754
+ fsyncSync3(fd);
4755
+ closeSync3(fd);
4756
+ fd = undefined;
4757
+ regularFile(temporary, "reindex temporary state file");
4758
+ assertSidecarDirectory(directory);
4759
+ assertReplacementTargetSafe(path, "reindex state file");
4760
+ renameSync4(temporary, path);
4761
+ created = false;
4762
+ assertSidecarDirectory(directory);
4763
+ fsyncSync3(directory.fd);
4764
+ } finally {
4765
+ if (fd !== undefined)
4766
+ closeSync3(fd);
4767
+ if (created)
4768
+ removePrivateFile(temporary, directory, "reindex temporary state file", true);
4769
+ }
4770
+ }
4771
+ function removePrivateFile(path, directory, label, optional) {
4772
+ try {
4773
+ assertSidecarDirectory(directory);
4774
+ regularFile(path, label);
4775
+ rmSync3(path);
4776
+ assertSidecarDirectory(directory);
4777
+ fsyncSync3(directory.fd);
4778
+ } catch (error) {
4779
+ if (optional && hasCode(error, "ENOENT")) {
4780
+ assertSidecarDirectory(directory);
4781
+ return;
4782
+ }
4783
+ throw error;
4784
+ }
4785
+ }
4786
+ function regularFile(path, label) {
4787
+ const stat = lstatSync4(path);
4788
+ if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 63) !== 0)
4789
+ throw new Error(`${label} is unsafe`);
4790
+ return stat;
4791
+ }
4792
+ function assertReplacementTargetSafe(path, label) {
4793
+ try {
4794
+ regularFile(path, label);
4795
+ } catch (error) {
4796
+ if (hasCode(error, "ENOENT"))
4797
+ return;
4798
+ throw error;
4799
+ }
4800
+ }
4801
+ function sameFile(left, right) {
4802
+ return left.dev === right.dev && left.ino === right.ino;
4803
+ }
4804
+ function isFileIdentity(value) {
4805
+ return Boolean(value && typeof value === "object" && Number.isSafeInteger(value.dev) && Number.isSafeInteger(value.ino));
4806
+ }
4807
+ function nullableString(value) {
4808
+ return value === null || typeof value === "string";
4809
+ }
4810
+ function hasCode(error, code) {
4811
+ return Boolean(error && typeof error === "object" && "code" in error && error.code === code);
4812
+ }
4813
+ function isOwnerMetadata(value) {
4814
+ return Boolean(value && typeof value === "object" && typeof value.ownerID === "string" && value.ownerID && Number.isSafeInteger(value.pid) && value.pid > 0 && typeof value.processStart === "string" && value.processStart && typeof value.hostname === "string" && value.hostname && Number.isSafeInteger(value.createdAt));
4815
+ }
4816
+
4277
4817
  // src/admin/index.ts
4278
4818
  async function runAdmin(argv = process.argv.slice(2)) {
4279
- const databasePath = resolveConfig().databasePath;
4280
- const [command, subcommand] = argv;
4281
- if (!command)
4282
- throw new Error("admin command is required");
4819
+ const parsed = parseAdminArguments(argv);
4820
+ const configuredDatabasePath = resolveConfig().databasePath;
4821
+ const database = parsed.command === "init" ? undefined : requireExistingDatabase(configuredDatabasePath, parsed.databaseID);
4822
+ const databasePath = database?.path ?? configuredDatabasePath;
4823
+ const [command, subcommand] = [parsed.command, parsed.subcommand];
4824
+ if (command === "init") {
4825
+ const opened = openMemoryDatabase(databasePath);
4826
+ try {
4827
+ return withDatabaseIdentity({ initialized: true }, databaseIdentity(opened.db, databasePath));
4828
+ } finally {
4829
+ opened.close();
4830
+ }
4831
+ }
4283
4832
  if (command === "doctor") {
4284
4833
  requireExistingDatabase(databasePath);
4285
4834
  const opened = openReadOnlyMemoryDatabase(databasePath);
4286
4835
  try {
4287
- return doctorDatabase(opened.db);
4836
+ return withDatabaseIdentity(doctorDatabase(opened.db), database);
4288
4837
  } finally {
4289
4838
  opened.close();
4290
4839
  }
@@ -4295,7 +4844,7 @@ async function runAdmin(argv = process.argv.slice(2)) {
4295
4844
  const db = new Database4(databasePath);
4296
4845
  try {
4297
4846
  const version = schemaVersion(db);
4298
- return createVerifiedBackup(db, databasePath, version, version, PRODUCT_VERSION);
4847
+ return withDatabaseIdentity(createVerifiedBackup(db, databasePath, version, version, PRODUCT_VERSION), database);
4299
4848
  } finally {
4300
4849
  db.close();
4301
4850
  }
@@ -4307,7 +4856,7 @@ async function runAdmin(argv = process.argv.slice(2)) {
4307
4856
  }
4308
4857
  const opened = openMemoryDatabase(databasePath);
4309
4858
  try {
4310
- return doctorDatabase(opened.db);
4859
+ return withDatabaseIdentity(doctorDatabase(opened.db), database);
4311
4860
  } finally {
4312
4861
  opened.close();
4313
4862
  }
@@ -4321,7 +4870,7 @@ async function runAdmin(argv = process.argv.slice(2)) {
4321
4870
  const confirmation = option(argv, "--confirm");
4322
4871
  const expectedHash = option(argv, "--sha256");
4323
4872
  if (!confirmation || !expectedHash) {
4324
- return { dryRun: true, manifest: verified.manifest, targetPath: databasePath };
4873
+ return withDatabaseIdentity({ dryRun: true, manifest: verified.manifest, targetPath: databasePath }, database);
4325
4874
  }
4326
4875
  if (expectedHash !== verified.manifest.sha256)
4327
4876
  throw new Error("restore manifest hash mismatch");
@@ -4339,7 +4888,7 @@ async function runAdmin(argv = process.argv.slice(2)) {
4339
4888
  }
4340
4889
  return withExclusiveMaintenance(databasePath, verified.manifest.sourceSchema, (maintenance) => {
4341
4890
  const preservedPath = restoreVerifiedBackup(manifestPath, databasePath, confirmation, maintenance, expectedHash);
4342
- return { restored: true, preservedPath, manifest: verified.manifest };
4891
+ return withDatabaseIdentity({ restored: true, preservedPath, manifest: verified.manifest }, database);
4343
4892
  }, recovery);
4344
4893
  }
4345
4894
  if (command === "unlock") {
@@ -4348,72 +4897,20 @@ async function runAdmin(argv = process.argv.slice(2)) {
4348
4897
  if (!ownerID || !confirmation)
4349
4898
  throw new Error("unlock requires --owner and --confirm");
4350
4899
  breakMigrationLock(databasePath, ownerID, confirmation);
4351
- return { unlocked: true, ownerID };
4900
+ return withDatabaseIdentity({ unlocked: true, ownerID }, database);
4352
4901
  }
4353
4902
  if (command === "reindex") {
4354
4903
  const backend = option(argv, "--backend");
4355
4904
  if (!backend || !/^[a-z0-9][a-z0-9._-]{0,79}$/i.test(backend)) {
4356
4905
  throw new Error("reindex requires a valid --backend");
4357
4906
  }
4358
- const opened = openMemoryDatabase(databasePath);
4359
- try {
4360
- const now = Date.now();
4361
- let queued = 0;
4362
- let purges = 0;
4363
- const quarantined = {};
4364
- let generation = 0;
4365
- const insert = opened.db.query(`
4366
- INSERT INTO index_outbox
4367
- (backend, operation_key, operation, project_id, note_id, revision, content_hash,
4368
- generation, lease_generation, fence, state, attempt_count, available_at,
4369
- heartbeat_at, created_at)
4370
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, 0, 'pending', 0, ?, NULL, ?)
4371
- `);
4372
- opened.db.exec("BEGIN IMMEDIATE");
4373
- try {
4374
- generation = opened.db.query("SELECT COALESCE(MAX(generation), 0) + 1 AS generation FROM index_outbox WHERE backend = ?").get(backend).generation;
4375
- const projects = opened.db.query("SELECT id FROM projects ORDER BY id").all();
4376
- const notes = opened.db.query("SELECT * FROM notes WHERE status = 'active' ORDER BY project_id, id").all();
4377
- for (const project of projects) {
4378
- const operation = "purge-project";
4379
- const operationKey = outboxOperationKey(backend, operation, project.id, null, null, null, generation);
4380
- purges += insert.run(backend, operationKey, operation, project.id, null, null, null, generation, now, now).changes;
4381
- }
4382
- for (const note of notes) {
4383
- const document = deriveDocument({
4384
- projectID: note.project_id,
4385
- noteID: note.id,
4386
- revision: note.current_revision,
4387
- kind: note.kind,
4388
- title: note.title,
4389
- summary: note.summary,
4390
- content: note.content
4391
- });
4392
- if (!document) {
4393
- quarantined.derived_document_unavailable = (quarantined.derived_document_unavailable ?? 0) + 1;
4394
- continue;
4395
- }
4396
- const operation = "upsert-note";
4397
- const operationKey = outboxOperationKey(backend, operation, note.project_id, note.id, note.current_revision, document.contentHash, generation);
4398
- queued += insert.run(backend, operationKey, operation, note.project_id, note.id, note.current_revision, document.contentHash, generation, now, now).changes;
4399
- }
4400
- opened.db.exec("COMMIT");
4401
- } catch (error) {
4402
- try {
4403
- opened.db.exec("ROLLBACK");
4404
- } catch {}
4405
- throw error;
4406
- }
4407
- return { backend, generation, purges, queued, quarantined };
4408
- } finally {
4409
- opened.close();
4410
- }
4907
+ return withDatabaseIdentity(runResumableReindex(databasePath, database.databaseID, backend, Number(option(argv, "--batch-size") ?? 100), option(argv, "--max-batches") === undefined ? undefined : Number(option(argv, "--max-batches"))), database);
4411
4908
  }
4412
4909
  if (command === "outbox" && subcommand === "status") {
4413
- return withDatabase(databasePath, (db) => ({
4910
+ return withDatabase(databasePath, (db) => withDatabaseIdentity({
4414
4911
  states: db.query("SELECT state, COUNT(*) AS count FROM index_outbox GROUP BY state ORDER BY state").all(),
4415
4912
  oldestPendingAt: db.query("SELECT MIN(created_at) AS value FROM index_outbox WHERE state IN ('pending','leased')").get().value
4416
- }));
4913
+ }, database));
4417
4914
  }
4418
4915
  if (command === "outbox" && subcommand === "retry") {
4419
4916
  const id = Number(argv[2]);
@@ -4423,26 +4920,28 @@ async function runAdmin(argv = process.argv.slice(2)) {
4423
4920
  const result = db.query(`UPDATE index_outbox
4424
4921
  SET state = 'pending', available_at = ?, lease_owner = NULL,
4425
4922
  lease_expires_at = NULL, heartbeat_at = NULL,
4426
- completed_at = NULL, last_error_code = NULL
4427
- WHERE id = ? AND state = 'dead'`).run(Date.now(), id);
4428
- return { id, retried: result.changes === 1 };
4923
+ completed_at = NULL, last_error_code = NULL, attempt_count = 0
4924
+ WHERE id = ? AND state = 'dead'`).run(Date.now(), id);
4925
+ pruneTerminalOutbox(db);
4926
+ return withDatabaseIdentity({ id, retried: result.changes === 1 }, database);
4429
4927
  });
4430
4928
  }
4431
4929
  if (command === "capture" && subcommand === "status") {
4432
- return withDatabase(databasePath, (db) => ({
4930
+ return withDatabase(databasePath, (db) => withDatabaseIdentity({
4433
4931
  events: db.query("SELECT state, COUNT(*) AS count FROM capture_events GROUP BY state ORDER BY state").all(),
4434
- checkpoints: db.query("SELECT state, COUNT(*) AS count FROM capture_checkpoints GROUP BY state ORDER BY state").all()
4435
- }));
4932
+ checkpoints: db.query("SELECT state, COUNT(*) AS count FROM capture_checkpoints GROUP BY state ORDER BY state").all(),
4933
+ quarantinePrivacy: quarantinePrivacyReport(db)
4934
+ }, database));
4436
4935
  }
4437
4936
  if (command === "backup" && subcommand === "prune") {
4438
4937
  return withExclusiveMaintenance(databasePath, SCHEMA_VERSION, () => {
4439
4938
  const entries = backupEntries(databasePath);
4440
4939
  const root = resolve3(`${databasePath}.backup`);
4441
- const digest = createHash7("sha256").update(`${resolve3(databasePath)}\x00${root}
4940
+ const digest = createHash8("sha256").update(`${resolve3(databasePath)}\x00${root}
4442
4941
  ${entries.map((entry) => `${basename3(entry.manifest)}\x00${basename3(entry.database)}\x00${entry.sha256}\x00${entry.size}\x00${entry.manifestHash}`).join(`
4443
4942
  `)}`).digest("hex");
4444
4943
  if (option(argv, "--confirm") !== "DELETE_VERIFIED_BACKUPS") {
4445
- return { dryRun: true, digest, backups: entries };
4944
+ return withDatabaseIdentity({ dryRun: true, digest, backups: entries }, database);
4446
4945
  }
4447
4946
  if (option(argv, "--digest") !== digest)
4448
4947
  throw new Error("backup prune digest mismatch");
@@ -4454,14 +4953,59 @@ ${entries.map((entry) => `${basename3(entry.manifest)}\x00${basename3(entry.data
4454
4953
  return current;
4455
4954
  });
4456
4955
  for (const current of currentEntries) {
4457
- rmSync3(current.database, { force: true });
4458
- rmSync3(current.manifest, { force: true });
4956
+ rmSync4(current.database, { force: true });
4957
+ rmSync4(current.manifest, { force: true });
4459
4958
  }
4460
- return { deleted: entries.length, digest };
4959
+ return withDatabaseIdentity({ deleted: entries.length, digest }, database);
4461
4960
  });
4462
4961
  }
4463
4962
  throw new Error(`unknown admin command: ${argv.join(" ")}`);
4464
4963
  }
4964
+ var TERMINAL_OUTBOX_RETENTION = 1e4;
4965
+ function parseAdminArguments(argv) {
4966
+ if (argv.length === 0)
4967
+ throw new Error("admin command is required");
4968
+ const [command, subcommand] = argv;
4969
+ const forms = {
4970
+ init: { positional: 0, flags: ["--database-id"] },
4971
+ doctor: { positional: 0, flags: ["--database-id"] },
4972
+ backup: { positional: subcommand === "prune" ? 1 : 0, flags: subcommand === "prune" ? ["--confirm", "--digest", "--database-id"] : ["--database-id"] },
4973
+ upgrade: { positional: 0, flags: ["--to", "--database-id"] },
4974
+ restore: { positional: 1, flags: ["--confirm", "--sha256", "--maintenance-owner", "--maintenance-confirm", "--database-id"] },
4975
+ unlock: { positional: 0, flags: ["--owner", "--confirm", "--database-id"] },
4976
+ reindex: { positional: 0, flags: ["--backend", "--batch-size", "--max-batches", "--database-id"] },
4977
+ outbox: { positional: subcommand === "retry" ? 2 : subcommand === "status" ? 1 : -1, flags: ["--database-id"] },
4978
+ capture: { positional: subcommand === "status" ? 1 : -1, flags: ["--database-id"] }
4979
+ };
4980
+ const form = forms[command];
4981
+ if (!form || form.positional < 0)
4982
+ throw new Error(`unknown admin command: ${argv.join(" ")}`);
4983
+ let positional = command === "restore" ? 2 : subcommand ? 2 : 1;
4984
+ if (command === "backup" && subcommand !== "prune")
4985
+ positional = 1;
4986
+ if (command === "init" || command === "doctor" || command === "upgrade" || command === "unlock" || command === "reindex")
4987
+ positional = 1;
4988
+ if (command === "outbox" && subcommand === "retry")
4989
+ positional = 3;
4990
+ const seen = new Set;
4991
+ for (let index = positional;index < argv.length; index += 2) {
4992
+ const flag = argv[index];
4993
+ const value = argv[index + 1];
4994
+ if (!flag?.startsWith("--") || value === undefined || value.startsWith("--")) {
4995
+ throw new Error("admin arguments must use unique --flag value pairs");
4996
+ }
4997
+ if (!form.flags.includes(flag) || seen.has(flag))
4998
+ throw new Error(`invalid or duplicate admin argument: ${flag}`);
4999
+ seen.add(flag);
5000
+ }
5001
+ if (command === "restore" && (!subcommand || subcommand.startsWith("--"))) {
5002
+ throw new Error("restore manifest path is required");
5003
+ }
5004
+ if ((command === "outbox" || command === "capture") && !subcommand) {
5005
+ throw new Error(`unknown admin command: ${argv.join(" ")}`);
5006
+ }
5007
+ return { command, subcommand, databaseID: option(argv, "--database-id") };
5008
+ }
4465
5009
  function withDatabase(databasePath, action) {
4466
5010
  const opened = openMemoryDatabase(databasePath);
4467
5011
  try {
@@ -4487,9 +5031,39 @@ function option(argv, name) {
4487
5031
  const index = argv.indexOf(name);
4488
5032
  return index >= 0 ? argv[index + 1] : undefined;
4489
5033
  }
4490
- function requireExistingDatabase(databasePath) {
5034
+ function requireExistingDatabase(databasePath, expectedID) {
4491
5035
  if (!existsSync5(databasePath))
4492
- throw new Error(`database does not exist: ${databasePath}`);
5036
+ throw new Error("database does not exist");
5037
+ const stat = lstatSync5(databasePath);
5038
+ if (!stat.isFile() || stat.isSymbolicLink())
5039
+ throw new Error("database must be a regular file");
5040
+ const canonicalPath = realpathSync(databasePath);
5041
+ if (canonicalPath !== resolve3(databasePath))
5042
+ throw new Error("database path must be canonical");
5043
+ const opened = openReadOnlyMemoryDatabase(canonicalPath);
5044
+ try {
5045
+ const identity = databaseIdentity(opened.db, canonicalPath);
5046
+ if (expectedID !== undefined && expectedID !== identity.databaseID)
5047
+ throw new Error("database id mismatch");
5048
+ return identity;
5049
+ } finally {
5050
+ opened.close();
5051
+ }
5052
+ }
5053
+ function databaseIdentity(db, databasePath) {
5054
+ const row = db.query("SELECT database_id FROM agz_meta WHERE id = 1").get();
5055
+ if (!row?.database_id)
5056
+ throw new Error("database identity is missing");
5057
+ return { path: realpathSync(databasePath), databaseID: row.database_id };
5058
+ }
5059
+ function withDatabaseIdentity(result, identity) {
5060
+ return { ...result, databasePath: identity.path, databaseID: identity.databaseID };
5061
+ }
5062
+ function pruneTerminalOutbox(db) {
5063
+ db.query(`DELETE FROM index_outbox WHERE id IN (
5064
+ SELECT id FROM index_outbox WHERE state IN ('succeeded','dead')
5065
+ ORDER BY completed_at DESC, id DESC LIMIT -1 OFFSET ?
5066
+ )`).run(TERMINAL_OUTBOX_RETENTION);
4493
5067
  }
4494
5068
  function readSchemaVersion(databasePath) {
4495
5069
  const db = new Database4(databasePath, { readonly: true });
@@ -4521,10 +5095,10 @@ function verifiedBackupEntry(root, manifest) {
4521
5095
  const candidate = resolve3(manifest);
4522
5096
  if (dirname3(candidate) !== root)
4523
5097
  throw new Error("backup manifest escaped the backup directory");
4524
- const stat = lstatSync4(candidate);
5098
+ const stat = lstatSync5(candidate);
4525
5099
  if (!stat.isFile() || stat.isSymbolicLink())
4526
5100
  throw new Error("backup manifest must be a regular file");
4527
- const bytes = readFileSync4(candidate);
5101
+ const bytes = readFileSync5(candidate);
4528
5102
  const verified = verifyBackupManifest(candidate);
4529
5103
  if (dirname3(verified.databasePath) !== root) {
4530
5104
  throw new Error("backup database escaped the backup directory");
@@ -4534,20 +5108,9 @@ function verifiedBackupEntry(root, manifest) {
4534
5108
  database: verified.databasePath,
4535
5109
  sha256: verified.manifest.sha256,
4536
5110
  size: verified.manifest.size,
4537
- manifestHash: createHash7("sha256").update(bytes).digest("hex")
5111
+ manifestHash: createHash8("sha256").update(bytes).digest("hex")
4538
5112
  };
4539
5113
  }
4540
- function outboxOperationKey(backend, operation, projectID, noteID, revision, contentHash, generation) {
4541
- return hashTuple("outbox-operation", 2, [
4542
- backend,
4543
- operation,
4544
- projectID,
4545
- noteID,
4546
- revision,
4547
- contentHash,
4548
- generation
4549
- ]);
4550
- }
4551
5114
  if (import.meta.main) {
4552
5115
  try {
4553
5116
  const result = await runAdmin();