@ours.network/cli 2.0.3 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,7 +4,7 @@ import {
4
4
  explicitApiToken,
5
5
  loadConfig,
6
6
  resolveApiToken
7
- } from "./chunk-FXKFSNKS.js";
7
+ } from "./chunk-DTWZF3ZG.js";
8
8
 
9
9
  // ../../src/boot.ts
10
10
  import { adapt_wrapper } from "@adapt-toolkit/sdk/executables";
@@ -89,13 +89,14 @@ function createStartupProgressReporter(stateDir, opts = {}) {
89
89
  }
90
90
 
91
91
  // ../../src/runtime/env.ts
92
- var VERSION = true ? "3.0.3" : "0.0.0-dev";
92
+ var VERSION = true ? "3.1.0" : "0.0.0-dev";
93
93
  var CONFIG = loadConfig();
94
94
  var STATE_DIR = CONFIG.stateDir;
95
95
  var BROKER_URL = CONFIG.brokerUrl;
96
96
  var TRANSPORT = process.env.OURS_TRANSPORT ?? "http";
97
97
  var PORT = CONFIG.port;
98
98
  var GC_INTERVAL_MS = CONFIG.gcIntervalMs;
99
+ var HISTORY_MAX_BYTES = CONFIG.historyMaxBytes;
99
100
  var API_VISIBILITY = CONFIG.apiVisibility;
100
101
  var startupHeartbeatMs = Number(process.env.OURS_TEST_STARTUP_HEARTBEAT_MS || "") || void 0;
101
102
  var startupProgress = TRANSPORT === "http" ? createStartupProgressReporter(STATE_DIR, { heartbeatMs: startupHeartbeatMs }) : null;
@@ -166,8 +167,8 @@ function locateUnit() {
166
167
  const override = process.env.OURS_UNIT_DIR;
167
168
  const noCap = process.env.OURS_ADVERTISE_MIGRATE === "0";
168
169
  const withNoCap = (dir) => noCap ? [`${dir}-nocap`] : [dir];
169
- const candidates = override ? withNoCap(resolve(override)) : [join2(here, "mufl_code"), join2(here, "..", "mufl_code")].flatMap(withNoCap);
170
- for (const dir of candidates) {
170
+ const candidates2 = override ? withNoCap(resolve(override)) : [join2(here, "mufl_code"), join2(here, "..", "mufl_code")].flatMap(withNoCap);
171
+ for (const dir of candidates2) {
171
172
  if (!fs2.existsSync(dir)) continue;
172
173
  const muflo = fs2.readdirSync(dir).find((f) => f.endsWith(".muflo"));
173
174
  if (muflo) {
@@ -177,7 +178,7 @@ function locateUnit() {
177
178
  }
178
179
  }
179
180
  throw new Error(
180
- `no compiled .muflo packet found (looked in: ${candidates.join(", ")})` + (noCap ? ' \u2014 OURS_ADVERTISE_MIGRATE=0 requires a "<dir>-nocap" packet variant (compiled with $advertise = [core.e2e] only); build it or unset the env.' : "")
181
+ `no compiled .muflo packet found (looked in: ${candidates2.join(", ")})` + (noCap ? ' \u2014 OURS_ADVERTISE_MIGRATE=0 requires a "<dir>-nocap" packet variant (compiled with $advertise = [core.e2e] only); build it or unset the env.' : "")
181
182
  );
182
183
  }
183
184
  var UNIT;
@@ -186,25 +187,702 @@ function setUnit(u) {
186
187
  }
187
188
 
188
189
  // ../../src/identity/model.ts
189
- import { join as join4 } from "node:path";
190
+ import { join as join5 } from "node:path";
190
191
  import { createHash as createHash2 } from "node:crypto";
191
- import * as fs4 from "node:fs";
192
+ import * as fs5 from "node:fs";
192
193
 
193
194
  // ../../src/history.ts
194
195
  import { createHash, randomUUID } from "node:crypto";
196
+ import * as fs4 from "node:fs";
197
+ import { dirname as dirname3, join as join4, resolve as resolve3, sep as sep2 } from "node:path";
198
+ import Database2 from "better-sqlite3";
199
+
200
+ // ../../src/history-retention.ts
195
201
  import * as fs3 from "node:fs";
196
- import { dirname as dirname2, join as join3, resolve as resolve2, sep } from "node:path";
202
+ import { basename, dirname as dirname2, join as join3, resolve as resolve2, sep } from "node:path";
197
203
  import Database from "better-sqlite3";
204
+ var DB_NAME = "history.sqlite3";
205
+ var BLOB_SUBDIR = join3("blobs", "sha256");
206
+ var DELETE_BATCH = 256;
207
+ var VACUUM_PAGES_PER_PASS = 1024;
208
+ var MAX_SYNCHRONOUS_PASSES = 8;
209
+ var LEGACY_VACUUM_LIMIT_BYTES = 128 * 1024 * 1024;
210
+ var SQLITE_MUTATION_UPPER_BOUND = 1024 * 1024;
211
+ var FILE_READ_GRACE_MS = 5 * 60 * 1e3;
212
+ var RETRY_INITIAL_DELAY_MS = 50;
213
+ var RETRY_MAX_DELAY_MS = 5e3;
214
+ var settings = null;
215
+ var approximatePhysicalBytes = 0;
216
+ var running = false;
217
+ var scheduled = null;
218
+ var transientMaintenanceFailure = false;
219
+ var maintenanceRetryDelayMs = RETRY_INITIAL_DELAY_MS;
220
+ var compactionBlocked = /* @__PURE__ */ new Set();
221
+ var inFlightItems = /* @__PURE__ */ new Map();
222
+ var evictedMessages = 0;
223
+ var evictedFiles = 0;
224
+ var orphanedBlobsRemoved = 0;
225
+ var lastRunAtMs = null;
226
+ var cachedStatus = {
227
+ state: "disabled",
228
+ enabled: false,
229
+ max_bytes: 0,
230
+ physical_bytes: 0,
231
+ overflow_bytes: 0,
232
+ protected_logical_bytes: 0,
233
+ eligible_logical_bytes: 0,
234
+ protected_items: 0,
235
+ eligible_items: 0,
236
+ newest_item_retained: false,
237
+ maintenance_pending: false,
238
+ compaction_blocked_identities: [],
239
+ evicted_messages: 0,
240
+ evicted_files: 0,
241
+ orphaned_blobs_removed: 0,
242
+ last_run_at_ms: null
243
+ };
244
+ function isSafeCap(value) {
245
+ return Number.isSafeInteger(value) && value >= 0;
246
+ }
247
+ function withinStateRoot(path) {
248
+ if (!settings) return false;
249
+ const root = settings.stateDir;
250
+ const candidate = resolve2(path);
251
+ return candidate === root || candidate.startsWith(`${root}${sep}`);
252
+ }
253
+ function physicalSize(path, seen) {
254
+ let st;
255
+ try {
256
+ st = fs3.lstatSync(path);
257
+ } catch {
258
+ return 0;
259
+ }
260
+ if (!st.isFile()) return 0;
261
+ const inode = `${String(st.dev)}:${String(st.ino)}`;
262
+ if (st.ino && seen.has(inode)) return 0;
263
+ if (st.ino) seen.add(inode);
264
+ return typeof st.blocks === "number" ? st.blocks * 512 : st.size;
265
+ }
266
+ function walkRegularFiles(root, visit) {
267
+ let entries;
268
+ try {
269
+ entries = fs3.readdirSync(root, { withFileTypes: true });
270
+ } catch {
271
+ return;
272
+ }
273
+ for (const entry of entries) {
274
+ const path = join3(root, entry.name);
275
+ if (entry.isDirectory()) walkRegularFiles(path, visit);
276
+ else if (entry.isFile()) visit(path);
277
+ }
278
+ }
279
+ function databasePaths(stateDir) {
280
+ let entries;
281
+ try {
282
+ entries = fs3.readdirSync(stateDir, { withFileTypes: true });
283
+ } catch {
284
+ return [];
285
+ }
286
+ return entries.filter((entry) => entry.isDirectory()).map((entry) => join3(stateDir, entry.name, DB_NAME)).filter((path) => fs3.existsSync(path)).sort();
287
+ }
288
+ function measureHistoryStorage(stateDir = settings?.stateDir) {
289
+ if (!stateDir) return 0;
290
+ const seen = /* @__PURE__ */ new Set();
291
+ let total = 0;
292
+ for (const dbPath of databasePaths(stateDir)) {
293
+ for (const suffix of ["", "-wal", "-shm"]) total += physicalSize(`${dbPath}${suffix}`, seen);
294
+ walkRegularFiles(join3(dirname2(dbPath), BLOB_SUBDIR), (path) => {
295
+ total += physicalSize(path, seen);
296
+ });
297
+ }
298
+ return total;
299
+ }
300
+ function openMaintenanceDatabase(path) {
301
+ const db = new Database(path);
302
+ db.pragma("busy_timeout = 50");
303
+ db.pragma("foreign_keys = ON");
304
+ return db;
305
+ }
306
+ function isRetryableSqliteError(error) {
307
+ const code = error && typeof error === "object" && "code" in error ? String(error.code ?? "") : "";
308
+ if (code === "SQLITE_BUSY" || code.startsWith("SQLITE_BUSY_") || code === "SQLITE_LOCKED" || code.startsWith("SQLITE_LOCKED_")) return true;
309
+ return /(?:database|table) is locked|database is busy/i.test(String(error));
310
+ }
311
+ function recordMaintenanceError(error, identityName) {
312
+ if (isRetryableSqliteError(error)) transientMaintenanceFailure = true;
313
+ else compactionBlocked.add(identityName);
314
+ }
315
+ function checkpointDatabase(db) {
316
+ const rows = db.pragma("wal_checkpoint(TRUNCATE)");
317
+ if (Number(rows[0]?.busy ?? 0) === 0) return true;
318
+ transientMaintenanceFailure = true;
319
+ return false;
320
+ }
321
+ function prepareHistoryDatabase(path) {
322
+ if (!fs3.existsSync(path)) return;
323
+ const identityName = basename(dirname2(path));
324
+ let db = null;
325
+ try {
326
+ db = openMaintenanceDatabase(path);
327
+ const mode = Number(db.pragma("auto_vacuum", { simple: true }));
328
+ if (mode === 2) {
329
+ compactionBlocked.delete(identityName);
330
+ return;
331
+ }
332
+ if (mode === 1) {
333
+ db.pragma("auto_vacuum = INCREMENTAL");
334
+ compactionBlocked.delete(identityName);
335
+ return;
336
+ }
337
+ const bytes = physicalSize(path, /* @__PURE__ */ new Set());
338
+ if (bytes > LEGACY_VACUUM_LIMIT_BYTES) {
339
+ compactionBlocked.add(identityName);
340
+ return;
341
+ }
342
+ if (!checkpointDatabase(db)) return;
343
+ db.pragma("journal_mode = DELETE");
344
+ db.pragma("auto_vacuum = INCREMENTAL");
345
+ db.exec("VACUUM");
346
+ db.pragma("journal_mode = WAL");
347
+ compactionBlocked.delete(identityName);
348
+ } catch (error) {
349
+ recordMaintenanceError(error, identityName);
350
+ } finally {
351
+ try {
352
+ db?.close();
353
+ } catch {
354
+ }
355
+ }
356
+ }
357
+ function newestItem(dbPath) {
358
+ let db = null;
359
+ try {
360
+ db = openMaintenanceDatabase(dbPath);
361
+ const row = db.prepare(`
362
+ SELECT kind, seq, occurred_at_ms, inserted_at_ms, logical_bytes, eligible FROM (
363
+ SELECT 'message' AS kind, seq, occurred_at_ms, inserted_at_ms,
364
+ length(CAST(body AS BLOB)) + 512 AS logical_bytes,
365
+ CASE WHEN inbox_state = 'read' AND direction = 'in' THEN 1
366
+ WHEN direction = 'out' AND delivery_state IN ('delivered','read') THEN 1 ELSE 0 END AS eligible
367
+ FROM messages
368
+ UNION ALL
369
+ SELECT 'file' AS kind, seq, occurred_at_ms, inserted_at_ms, byte_length + 512 AS logical_bytes,
370
+ CASE WHEN direction = 'in' AND inbox_state = 'read' AND human_read_at_ms <= ? THEN 1
371
+ WHEN direction = 'out' AND delivery_state IN ('delivered','read') THEN 1 ELSE 0 END AS eligible
372
+ FROM files
373
+ ) ORDER BY inserted_at_ms DESC, occurred_at_ms DESC, kind DESC, seq DESC LIMIT 1
374
+ `).get(Date.now() - FILE_READ_GRACE_MS);
375
+ return row ? {
376
+ identityDir: dirname2(dbPath),
377
+ kind: row.kind,
378
+ seq: row.seq,
379
+ occurredAtMs: row.occurred_at_ms,
380
+ insertedAtMs: row.inserted_at_ms,
381
+ logicalBytes: Number(row.logical_bytes),
382
+ eligible: row.eligible === 1
383
+ } : null;
384
+ } catch (error) {
385
+ recordMaintenanceError(error, basename(dirname2(dbPath)));
386
+ return null;
387
+ } finally {
388
+ try {
389
+ db?.close();
390
+ } catch {
391
+ }
392
+ }
393
+ }
394
+ function newer(a, b) {
395
+ if (a.insertedAtMs !== b.insertedAtMs) return a.insertedAtMs > b.insertedAtMs;
396
+ if (a.occurredAtMs !== b.occurredAtMs) return a.occurredAtMs > b.occurredAtMs;
397
+ if (a.identityDir !== b.identityDir) return a.identityDir > b.identityDir;
398
+ if (a.kind !== b.kind) return a.kind > b.kind;
399
+ return a.seq > b.seq;
400
+ }
401
+ function globalNewest(paths) {
402
+ let newest = null;
403
+ for (const path of paths) {
404
+ const item = newestItem(path);
405
+ if (item && (!newest || newer(item, newest))) newest = item;
406
+ }
407
+ return newest;
408
+ }
409
+ function isSameItem(a, b) {
410
+ return !!b && a.identityDir === b.identityDir && a.kind === b.kind && a.seq === b.seq;
411
+ }
412
+ function inFlightKey(identityDir2, kind, wireId) {
413
+ return `${resolve2(identityDir2)}\0${kind}\0${wireId}`;
414
+ }
415
+ function candidates(paths, newest) {
416
+ const result = [];
417
+ for (const dbPath of paths) {
418
+ let db = null;
419
+ try {
420
+ db = openMaintenanceDatabase(dbPath);
421
+ const rows = db.prepare(`
422
+ SELECT kind, seq, wire_id, occurred_at_ms, inserted_at_ms, logical_bytes, blob_relpath FROM (
423
+ SELECT 'message' AS kind, seq, wire_id, occurred_at_ms, inserted_at_ms,
424
+ length(CAST(body AS BLOB)) + 512 AS logical_bytes, NULL AS blob_relpath
425
+ FROM messages WHERE (direction = 'in' AND inbox_state = 'read')
426
+ OR (direction = 'out' AND delivery_state IN ('delivered','read'))
427
+ UNION ALL
428
+ SELECT 'file' AS kind, seq, wire_id, occurred_at_ms, inserted_at_ms,
429
+ byte_length + 512 AS logical_bytes, blob_relpath
430
+ FROM files WHERE (direction = 'in' AND inbox_state = 'read' AND human_read_at_ms <= ?)
431
+ OR (direction = 'out' AND delivery_state IN ('delivered','read'))
432
+ ) ORDER BY occurred_at_ms ASC, inserted_at_ms ASC, kind ASC, seq ASC LIMIT ?
433
+ `).all(Date.now() - FILE_READ_GRACE_MS, DELETE_BATCH + 1);
434
+ for (const row of rows) {
435
+ const item = {
436
+ dbPath,
437
+ identityDir: dirname2(dbPath),
438
+ kind: row.kind,
439
+ seq: row.seq,
440
+ wireId: row.wire_id,
441
+ occurredAtMs: row.occurred_at_ms,
442
+ insertedAtMs: row.inserted_at_ms,
443
+ logicalBytes: Number(row.logical_bytes),
444
+ eligible: true,
445
+ blobRelpath: row.blob_relpath
446
+ };
447
+ if (!isSameItem(item, newest) && !inFlightItems.has(inFlightKey(item.identityDir, item.kind, item.wireId))) {
448
+ result.push(item);
449
+ }
450
+ }
451
+ } catch (error) {
452
+ recordMaintenanceError(error, basename(dirname2(dbPath)));
453
+ } finally {
454
+ try {
455
+ db?.close();
456
+ } catch {
457
+ }
458
+ }
459
+ }
460
+ result.sort((a, b) => a.occurredAtMs - b.occurredAtMs || a.insertedAtMs - b.insertedAtMs || a.identityDir.localeCompare(b.identityDir) || a.kind.localeCompare(b.kind) || a.seq - b.seq);
461
+ return result.slice(0, DELETE_BATCH);
462
+ }
463
+ function safeBlobPath(identityDir2, relpath) {
464
+ const root = resolve2(join3(identityDir2, BLOB_SUBDIR));
465
+ const path = resolve2(identityDir2, relpath);
466
+ return path.startsWith(`${root}${sep}`) ? path : null;
467
+ }
468
+ function pruneEmptyBlobDirs(identityDir2, from) {
469
+ const stop = resolve2(join3(identityDir2, "blobs"));
470
+ let current = resolve2(dirname2(from));
471
+ while (current.startsWith(`${stop}${sep}`)) {
472
+ try {
473
+ fs3.rmdirSync(current);
474
+ } catch {
475
+ break;
476
+ }
477
+ current = dirname2(current);
478
+ }
479
+ }
480
+ function unlinkBlobIfOrphaned(db, identityDir2, relpath) {
481
+ if (db.prepare("SELECT 1 FROM files WHERE blob_relpath = ? LIMIT 1").get(relpath)) return false;
482
+ const path = safeBlobPath(identityDir2, relpath);
483
+ if (!path) return false;
484
+ try {
485
+ const st = fs3.lstatSync(path);
486
+ if (!st.isFile()) return false;
487
+ fs3.unlinkSync(path);
488
+ pruneEmptyBlobDirs(identityDir2, path);
489
+ return true;
490
+ } catch {
491
+ return false;
492
+ }
493
+ }
494
+ function deleteCandidates(items, targetBytes) {
495
+ const selected = [];
496
+ let logical = 0;
497
+ for (const item of items) {
498
+ selected.push(item);
499
+ logical += Math.max(1, item.logicalBytes);
500
+ if (logical >= targetBytes) break;
501
+ }
502
+ const byDatabase = /* @__PURE__ */ new Map();
503
+ for (const item of selected) {
504
+ const rows = byDatabase.get(item.dbPath) ?? [];
505
+ rows.push(item);
506
+ byDatabase.set(item.dbPath, rows);
507
+ }
508
+ let deleted = 0;
509
+ for (const [dbPath, rows] of byDatabase) {
510
+ let db = null;
511
+ try {
512
+ db = openMaintenanceDatabase(dbPath);
513
+ const removedBlobs = [];
514
+ db.transaction(() => {
515
+ const delMessage = db.prepare(`
516
+ DELETE FROM messages WHERE seq = ? AND inbox_state = 'read'
517
+ AND (direction = 'in' OR delivery_state IN ('delivered','read'))
518
+ `);
519
+ const delFile = db.prepare(`
520
+ DELETE FROM files WHERE seq = ? AND (
521
+ (direction = 'in' AND inbox_state = 'read' AND human_read_at_ms <= ?)
522
+ OR (direction = 'out' AND delivery_state IN ('delivered','read'))
523
+ )
524
+ `);
525
+ for (const row of rows) {
526
+ const changes = row.kind === "message" ? delMessage.run(row.seq).changes : delFile.run(row.seq, Date.now() - FILE_READ_GRACE_MS).changes;
527
+ if (changes !== 1) continue;
528
+ deleted += 1;
529
+ if (row.kind === "message") evictedMessages += 1;
530
+ else {
531
+ evictedFiles += 1;
532
+ if (row.blobRelpath) removedBlobs.push(row.blobRelpath);
533
+ }
534
+ }
535
+ })();
536
+ for (const relpath of new Set(removedBlobs)) {
537
+ if (unlinkBlobIfOrphaned(db, dirname2(dbPath), relpath)) orphanedBlobsRemoved += 1;
538
+ }
539
+ checkpointDatabase(db);
540
+ if (Number(db.pragma("auto_vacuum", { simple: true })) === 2) {
541
+ db.pragma(`incremental_vacuum(${VACUUM_PAGES_PER_PASS})`);
542
+ checkpointDatabase(db);
543
+ compactionBlocked.delete(basename(dirname2(dbPath)));
544
+ } else {
545
+ compactionBlocked.add(basename(dirname2(dbPath)));
546
+ }
547
+ } catch (error) {
548
+ recordMaintenanceError(error, basename(dirname2(dbPath)));
549
+ } finally {
550
+ try {
551
+ db?.close();
552
+ } catch {
553
+ }
554
+ }
555
+ }
556
+ return deleted;
557
+ }
558
+ function recoverOrphanedBlobs(dbPath) {
559
+ const identityDir2 = dirname2(dbPath);
560
+ let db = null;
561
+ let removed = 0;
562
+ try {
563
+ db = openMaintenanceDatabase(dbPath);
564
+ const referenced = new Set(
565
+ db.prepare("SELECT DISTINCT blob_relpath FROM files").pluck().all().map((p) => resolve2(identityDir2, p))
566
+ );
567
+ const root = join3(identityDir2, BLOB_SUBDIR);
568
+ const paths = [];
569
+ walkRegularFiles(root, (path) => paths.push(path));
570
+ for (const path of paths) {
571
+ if (referenced.has(resolve2(path))) continue;
572
+ try {
573
+ fs3.unlinkSync(path);
574
+ removed += 1;
575
+ pruneEmptyBlobDirs(identityDir2, path);
576
+ } catch {
577
+ }
578
+ }
579
+ } catch (error) {
580
+ recordMaintenanceError(error, basename(identityDir2));
581
+ } finally {
582
+ try {
583
+ db?.close();
584
+ } catch {
585
+ }
586
+ }
587
+ return removed;
588
+ }
589
+ function logicalStats(paths, newest) {
590
+ const totals = {
591
+ protectedBytes: 0,
592
+ eligibleBytes: 0,
593
+ protectedItems: 0,
594
+ eligibleItems: 0,
595
+ newestRetained: false
596
+ };
597
+ for (const dbPath of paths) {
598
+ let db = null;
599
+ try {
600
+ db = openMaintenanceDatabase(dbPath);
601
+ const row = db.prepare(`
602
+ SELECT
603
+ COALESCE(SUM(CASE WHEN eligible = 1 THEN logical_bytes ELSE 0 END), 0) AS eligible_bytes,
604
+ COALESCE(SUM(CASE WHEN eligible = 0 THEN logical_bytes ELSE 0 END), 0) AS protected_bytes,
605
+ COALESCE(SUM(eligible), 0) AS eligible_items,
606
+ COALESCE(SUM(CASE WHEN eligible = 0 THEN 1 ELSE 0 END), 0) AS protected_items
607
+ FROM (
608
+ SELECT length(CAST(body AS BLOB)) + 512 AS logical_bytes,
609
+ CASE WHEN direction = 'in' AND inbox_state = 'read' THEN 1
610
+ WHEN direction = 'out' AND delivery_state IN ('delivered','read') THEN 1 ELSE 0 END AS eligible
611
+ FROM messages
612
+ UNION ALL
613
+ SELECT byte_length + 512 AS logical_bytes,
614
+ CASE WHEN direction = 'in' AND inbox_state = 'read' AND human_read_at_ms <= ? THEN 1
615
+ WHEN direction = 'out' AND delivery_state IN ('delivered','read') THEN 1 ELSE 0 END AS eligible
616
+ FROM files
617
+ UNION ALL
618
+ SELECT length(CAST(body AS BLOB)) + 512, 0 FROM monitoring_items
619
+ UNION ALL
620
+ SELECT length(CAST(payload AS BLOB)) + 512, 0 FROM control_requests
621
+ )
622
+ `).get(Date.now() - FILE_READ_GRACE_MS);
623
+ totals.eligibleBytes += Number(row.eligible_bytes);
624
+ totals.protectedBytes += Number(row.protected_bytes);
625
+ totals.eligibleItems += Number(row.eligible_items);
626
+ totals.protectedItems += Number(row.protected_items);
627
+ } catch (error) {
628
+ recordMaintenanceError(error, basename(dirname2(dbPath)));
629
+ } finally {
630
+ try {
631
+ db?.close();
632
+ } catch {
633
+ }
634
+ }
635
+ }
636
+ if (newest?.eligible) {
637
+ totals.newestRetained = true;
638
+ totals.eligibleItems = Math.max(0, totals.eligibleItems - 1);
639
+ totals.protectedItems += 1;
640
+ totals.eligibleBytes = Math.max(0, totals.eligibleBytes - newest.logicalBytes);
641
+ totals.protectedBytes += newest.logicalBytes;
642
+ }
643
+ for (const item of inFlightItems.values()) {
644
+ let db = null;
645
+ try {
646
+ db = openMaintenanceDatabase(join3(item.identityDir, DB_NAME));
647
+ const table = item.kind === "file" ? "files" : "messages";
648
+ const sizeExpr = item.kind === "file" ? "byte_length + 512" : "length(CAST(body AS BLOB)) + 512";
649
+ const eligibility = item.kind === "file" ? `((direction = 'in' AND inbox_state = 'read' AND human_read_at_ms <= ?)
650
+ OR (direction = 'out' AND delivery_state IN ('delivered','read')))` : `((direction = 'in' AND inbox_state = 'read')
651
+ OR (direction = 'out' AND delivery_state IN ('delivered','read')))`;
652
+ const row = db.prepare(`
653
+ SELECT seq, ${sizeExpr} AS logical_bytes FROM ${table}
654
+ WHERE wire_id = ? AND ${eligibility}
655
+ `).get(...item.kind === "file" ? [item.wireId, Date.now() - FILE_READ_GRACE_MS] : [item.wireId]);
656
+ if (!row) continue;
657
+ if (isSameItem({ identityDir: item.identityDir, kind: item.kind, seq: row.seq }, newest)) continue;
658
+ const bytes = Number(row.logical_bytes);
659
+ totals.eligibleItems = Math.max(0, totals.eligibleItems - 1);
660
+ totals.protectedItems += 1;
661
+ totals.eligibleBytes = Math.max(0, totals.eligibleBytes - bytes);
662
+ totals.protectedBytes += bytes;
663
+ } catch (error) {
664
+ recordMaintenanceError(error, basename(item.identityDir));
665
+ } finally {
666
+ try {
667
+ db?.close();
668
+ } catch {
669
+ }
670
+ }
671
+ }
672
+ return totals;
673
+ }
674
+ function hasReclaimablePages(paths) {
675
+ let reclaimable = false;
676
+ for (const dbPath of paths) {
677
+ const identityName = basename(dirname2(dbPath));
678
+ let db = null;
679
+ try {
680
+ db = openMaintenanceDatabase(dbPath);
681
+ if (Number(db.pragma("auto_vacuum", { simple: true })) === 2) {
682
+ if (Number(db.pragma("freelist_count", { simple: true })) > 0) reclaimable = true;
683
+ } else {
684
+ compactionBlocked.add(identityName);
685
+ }
686
+ } catch (error) {
687
+ recordMaintenanceError(error, identityName);
688
+ } finally {
689
+ try {
690
+ db?.close();
691
+ } catch {
692
+ }
693
+ }
694
+ }
695
+ return reclaimable;
696
+ }
697
+ function compactDatabases(paths) {
698
+ for (const dbPath of paths) {
699
+ const identityName = basename(dirname2(dbPath));
700
+ let db = null;
701
+ try {
702
+ db = openMaintenanceDatabase(dbPath);
703
+ const firstCheckpoint = checkpointDatabase(db);
704
+ if (Number(db.pragma("auto_vacuum", { simple: true })) === 2) {
705
+ db.pragma(`incremental_vacuum(${VACUUM_PAGES_PER_PASS})`);
706
+ const secondCheckpoint = checkpointDatabase(db);
707
+ if (firstCheckpoint && secondCheckpoint) compactionBlocked.delete(identityName);
708
+ } else {
709
+ compactionBlocked.add(identityName);
710
+ }
711
+ } catch (error) {
712
+ recordMaintenanceError(error, identityName);
713
+ } finally {
714
+ try {
715
+ db?.close();
716
+ } catch {
717
+ }
718
+ }
719
+ }
720
+ }
721
+ function classify(physical, logical, reclaimable) {
722
+ const maxBytes = settings?.maxBytes ?? 0;
723
+ const enabled = maxBytes > 0;
724
+ const overflow = enabled ? Math.max(0, physical - maxBytes) : 0;
725
+ const blocked = [...compactionBlocked].sort();
726
+ let state;
727
+ if (!enabled) state = "disabled";
728
+ else if (overflow === 0) state = "within_cap";
729
+ else if (transientMaintenanceFailure || logical.eligibleItems > 0 || reclaimable) state = "maintenance_pending";
730
+ else if (blocked.length > 0) state = "compaction_blocked";
731
+ else state = "protected_overflow";
732
+ return {
733
+ state,
734
+ enabled,
735
+ max_bytes: maxBytes,
736
+ physical_bytes: physical,
737
+ overflow_bytes: overflow,
738
+ protected_logical_bytes: logical.protectedBytes,
739
+ eligible_logical_bytes: logical.eligibleBytes,
740
+ protected_items: logical.protectedItems,
741
+ eligible_items: logical.eligibleItems,
742
+ newest_item_retained: logical.newestRetained,
743
+ maintenance_pending: state === "maintenance_pending",
744
+ compaction_blocked_identities: blocked,
745
+ evicted_messages: evictedMessages,
746
+ evicted_files: evictedFiles,
747
+ orphaned_blobs_removed: orphanedBlobsRemoved,
748
+ last_run_at_ms: lastRunAtMs
749
+ };
750
+ }
751
+ function pruneMissingBlockedIdentities(paths) {
752
+ const present = new Set(paths.map((path) => basename(dirname2(path))));
753
+ for (const name of compactionBlocked) if (!present.has(name)) compactionBlocked.delete(name);
754
+ }
755
+ function refreshStatus() {
756
+ const paths = settings ? databasePaths(settings.stateDir) : [];
757
+ pruneMissingBlockedIdentities(paths);
758
+ const physical = settings ? measureHistoryStorage(settings.stateDir) : 0;
759
+ approximatePhysicalBytes = physical;
760
+ cachedStatus = classify(physical, logicalStats(paths, globalNewest(paths)), hasReclaimablePages(paths));
761
+ return cachedStatus;
762
+ }
763
+ function scheduleMaintenance() {
764
+ if (scheduled || !settings || settings.maxBytes === 0) return;
765
+ const delayMs = transientMaintenanceFailure ? maintenanceRetryDelayMs : 0;
766
+ maintenanceRetryDelayMs = transientMaintenanceFailure ? Math.min(RETRY_MAX_DELAY_MS, maintenanceRetryDelayMs * 2) : RETRY_INITIAL_DELAY_MS;
767
+ scheduled = setTimeout(() => {
768
+ scheduled = null;
769
+ enforceHistoryStorageLimit(1);
770
+ }, delayMs);
771
+ scheduled.unref?.();
772
+ }
773
+ function initializeHistoryRetention(stateDir, maxBytes) {
774
+ if (!isSafeCap(maxBytes)) throw new Error("historyMaxBytes must be a non-negative safe integer");
775
+ if (scheduled) {
776
+ clearTimeout(scheduled);
777
+ scheduled = null;
778
+ }
779
+ settings = { stateDir: resolve2(stateDir), maxBytes };
780
+ approximatePhysicalBytes = 0;
781
+ running = false;
782
+ transientMaintenanceFailure = false;
783
+ maintenanceRetryDelayMs = RETRY_INITIAL_DELAY_MS;
784
+ compactionBlocked.clear();
785
+ inFlightItems.clear();
786
+ evictedMessages = 0;
787
+ evictedFiles = 0;
788
+ orphanedBlobsRemoved = 0;
789
+ lastRunAtMs = null;
790
+ for (const path of databasePaths(settings.stateDir)) {
791
+ prepareHistoryDatabase(path);
792
+ orphanedBlobsRemoved += recoverOrphanedBlobs(path);
793
+ }
794
+ return enforceHistoryStorageLimit(2);
795
+ }
796
+ function noteHistoryStorageMutation(identityDir2, payloadBytes = 0) {
797
+ if (!settings || settings.maxBytes === 0 || !withinStateRoot(identityDir2)) return;
798
+ approximatePhysicalBytes += Math.max(0, payloadBytes) + SQLITE_MUTATION_UPPER_BOUND;
799
+ if (approximatePhysicalBytes >= settings.maxBytes) {
800
+ const status = enforceHistoryStorageLimit(MAX_SYNCHRONOUS_PASSES);
801
+ if (status.maintenance_pending) scheduleMaintenance();
802
+ }
803
+ }
804
+ function enforceHistoryStorageLimit(maxPasses = MAX_SYNCHRONOUS_PASSES) {
805
+ if (!settings) return cachedStatus;
806
+ if (running) {
807
+ scheduleMaintenance();
808
+ return cachedStatus;
809
+ }
810
+ running = true;
811
+ transientMaintenanceFailure = false;
812
+ try {
813
+ for (let pass = 0; pass < Math.max(1, maxPasses); pass += 1) {
814
+ let physical = measureHistoryStorage(settings.stateDir);
815
+ if (settings.maxBytes === 0 || physical <= settings.maxBytes) break;
816
+ const paths = databasePaths(settings.stateDir);
817
+ compactDatabases(paths);
818
+ const compactedPhysical = measureHistoryStorage(settings.stateDir);
819
+ if (compactedPhysical <= settings.maxBytes) break;
820
+ const compacted = compactedPhysical < physical;
821
+ physical = compactedPhysical;
822
+ const newest = globalNewest(paths);
823
+ const available = candidates(paths, newest);
824
+ if (available.length === 0) {
825
+ if (!compacted) break;
826
+ continue;
827
+ }
828
+ const removed = deleteCandidates(available, physical - settings.maxBytes);
829
+ if (removed === 0) break;
830
+ }
831
+ lastRunAtMs = Date.now();
832
+ const status = refreshStatus();
833
+ if (status.maintenance_pending) scheduleMaintenance();
834
+ return status;
835
+ } finally {
836
+ running = false;
837
+ }
838
+ }
839
+ function getHistoryStorageStatus() {
840
+ if (!settings) return { ...cachedStatus, compaction_blocked_identities: [...cachedStatus.compaction_blocked_identities] };
841
+ const status = refreshStatus();
842
+ return { ...status, compaction_blocked_identities: [...status.compaction_blocked_identities] };
843
+ }
844
+ function stopHistoryRetention() {
845
+ if (scheduled) {
846
+ clearTimeout(scheduled);
847
+ scheduled = null;
848
+ }
849
+ }
850
+ function pinHistoryItems(identityDir2, kind, wireIds) {
851
+ for (const wireId of wireIds) {
852
+ inFlightItems.set(inFlightKey(identityDir2, kind, wireId), { identityDir: identityDir2, kind, wireId });
853
+ }
854
+ }
855
+ function unpinHistoryItems(identityDir2, kind, wireIds) {
856
+ for (const wireId of wireIds) inFlightItems.delete(inFlightKey(identityDir2, kind, wireId));
857
+ }
858
+ function cleanupHistoryBlob(identityDir2, relpath) {
859
+ const dbPath = join3(identityDir2, DB_NAME);
860
+ if (!fs3.existsSync(dbPath)) return false;
861
+ let db = null;
862
+ try {
863
+ db = openMaintenanceDatabase(dbPath);
864
+ const removed = unlinkBlobIfOrphaned(db, identityDir2, relpath);
865
+ if (removed) orphanedBlobsRemoved += 1;
866
+ return removed;
867
+ } finally {
868
+ try {
869
+ db?.close();
870
+ } catch {
871
+ }
872
+ }
873
+ }
874
+
875
+ // ../../src/history.ts
198
876
  var MAX_BATCH = 200;
199
877
  var DEFAULT_BATCH = 50;
200
878
  var MAX_TEXT_BYTES = 2 * 1024 * 1024;
201
879
  var MAX_FILE_BYTES = 8 * 1024 * 1024;
202
- var DB_NAME = "history.sqlite3";
880
+ var DB_NAME2 = "history.sqlite3";
203
881
  var SCHEMA_VERSION = 1;
204
882
  var stores = /* @__PURE__ */ new Map();
205
883
  var health = /* @__PURE__ */ new Map();
206
884
  var outboundFailures = /* @__PURE__ */ new Map();
207
- var historyPath = (id) => join3(id.dir, DB_NAME);
885
+ var historyPath = (id) => join4(id.dir, DB_NAME2);
208
886
  function initialHealth() {
209
887
  return {
210
888
  write_failures: 0,
@@ -328,12 +1006,15 @@ function schema(db) {
328
1006
  function openHistory(id) {
329
1007
  const existing = stores.get(id.dir);
330
1008
  if (existing) return existing;
331
- fs3.mkdirSync(id.dir, { recursive: true, mode: 448 });
332
- fs3.chmodSync(id.dir, 448);
1009
+ fs4.mkdirSync(id.dir, { recursive: true, mode: 448 });
1010
+ fs4.chmodSync(id.dir, 448);
333
1011
  const path = historyPath(id);
334
- const db = new Database(path);
1012
+ const isNew = !fs4.existsSync(path) || fs4.statSync(path).size === 0;
1013
+ if (!isNew) prepareHistoryDatabase(path);
1014
+ const db = new Database2(path);
335
1015
  try {
336
- fs3.chmodSync(path, 384);
1016
+ fs4.chmodSync(path, 384);
1017
+ if (isNew) db.pragma("auto_vacuum = INCREMENTAL");
337
1018
  db.pragma("journal_mode = WAL");
338
1019
  db.pragma("synchronous = NORMAL");
339
1020
  db.pragma("foreign_keys = ON");
@@ -341,7 +1022,7 @@ function openHistory(id) {
341
1022
  schema(db);
342
1023
  for (const suffix of ["", "-wal", "-shm"]) {
343
1024
  const candidate = `${path}${suffix}`;
344
- if (fs3.existsSync(candidate)) fs3.chmodSync(candidate, 384);
1025
+ if (fs4.existsSync(candidate)) fs4.chmodSync(candidate, 384);
345
1026
  }
346
1027
  } catch (error) {
347
1028
  db.close();
@@ -430,36 +1111,36 @@ function fileFromRow(id, row) {
430
1111
  delivery_state: row.delivery_state,
431
1112
  human_read_at_ms: row.human_read_at_ms,
432
1113
  reply_to: reply(row),
433
- blob_path: join3(id.dir, row.blob_relpath),
1114
+ blob_path: join4(id.dir, row.blob_relpath),
434
1115
  kind: mime.startsWith("audio/") ? "voice_message" : "file"
435
1116
  };
436
1117
  }
437
1118
  function writeBlob(id, bytes) {
438
1119
  const sha256 = createHash("sha256").update(bytes).digest("hex");
439
- const relpath = join3("blobs", "sha256", sha256.slice(0, 2), sha256);
440
- const final = join3(id.dir, relpath);
441
- fs3.mkdirSync(dirname2(final), { recursive: true, mode: 448 });
442
- fs3.chmodSync(dirname2(final), 448);
443
- if (!fs3.existsSync(final)) {
444
- const tmp = join3(dirname2(final), `.${sha256}.${process.pid}.${randomUUID()}.tmp`);
1120
+ const relpath = join4("blobs", "sha256", sha256.slice(0, 2), sha256);
1121
+ const final = join4(id.dir, relpath);
1122
+ fs4.mkdirSync(dirname3(final), { recursive: true, mode: 448 });
1123
+ fs4.chmodSync(dirname3(final), 448);
1124
+ if (!fs4.existsSync(final)) {
1125
+ const tmp = join4(dirname3(final), `.${sha256}.${process.pid}.${randomUUID()}.tmp`);
445
1126
  try {
446
- fs3.writeFileSync(tmp, bytes, { flag: "wx", mode: 384 });
447
- fs3.chmodSync(tmp, 384);
1127
+ fs4.writeFileSync(tmp, bytes, { flag: "wx", mode: 384 });
1128
+ fs4.chmodSync(tmp, 384);
448
1129
  try {
449
- fs3.renameSync(tmp, final);
1130
+ fs4.renameSync(tmp, final);
450
1131
  } catch (error) {
451
- if (!fs3.existsSync(final)) throw error;
452
- fs3.rmSync(tmp, { force: true });
1132
+ if (!fs4.existsSync(final)) throw error;
1133
+ fs4.rmSync(tmp, { force: true });
453
1134
  }
454
1135
  } catch (error) {
455
1136
  try {
456
- fs3.rmSync(tmp, { force: true });
1137
+ fs4.rmSync(tmp, { force: true });
457
1138
  } catch {
458
1139
  }
459
1140
  throw error;
460
1141
  }
461
1142
  }
462
- fs3.chmodSync(final, 384);
1143
+ fs4.chmodSync(final, 384);
463
1144
  return { sha256, relpath };
464
1145
  }
465
1146
  function ingestApplicationEvent(id, event) {
@@ -469,7 +1150,7 @@ function ingestApplicationEvent(id, event) {
469
1150
  const inboxState = event.direction === "in" ? event.inboxState ?? "unread" : "read";
470
1151
  const deliveryState = event.direction === "out" ? "sent" : null;
471
1152
  if (event.kind === "message") {
472
- return db.transaction(() => {
1153
+ const inserted2 = db.transaction(() => {
473
1154
  const claimed = db.prepare(`
474
1155
  INSERT INTO wire_items(wire_id, item_kind) VALUES (?, 'message')
475
1156
  ON CONFLICT(wire_id) DO NOTHING
@@ -498,44 +1179,55 @@ function ingestApplicationEvent(id, event) {
498
1179
  );
499
1180
  return true;
500
1181
  })();
1182
+ if (inserted2) noteHistoryStorageMutation(id.dir, Buffer.byteLength(event.body, "utf8"));
1183
+ return inserted2;
501
1184
  }
502
1185
  const duplicate = db.prepare("SELECT 1 FROM wire_items WHERE wire_id = ?").get(event.wireId);
503
1186
  if (duplicate) return false;
504
1187
  const blob = writeBlob(id, event.bytes);
505
- return db.transaction(() => {
506
- const claimed = db.prepare(`
507
- INSERT INTO wire_items(wire_id, item_kind) VALUES (?, 'file')
508
- ON CONFLICT(wire_id) DO NOTHING
509
- `).run(event.wireId);
510
- if (claimed.changes !== 1) return false;
511
- db.prepare(`
512
- INSERT INTO files (
513
- wire_id, peer_cid, peer_name_snapshot, direction, filename, mime,
514
- byte_length, sha256, blob_relpath, occurred_at_ms, reply_to_wire_id,
515
- reply_to_sentence, encryption, inbox_state, delivery_state,
516
- human_read_at_ms, inserted_at_ms
517
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
518
- `).run(
519
- event.wireId,
520
- event.peerCid,
521
- event.peerName,
522
- event.direction,
523
- event.filename,
524
- event.mime || "application/octet-stream",
525
- event.bytes.byteLength,
526
- blob.sha256,
527
- blob.relpath,
528
- event.occurredAtMs,
529
- event.replyToWireId ?? null,
530
- event.replyToSentence ?? null,
531
- event.encryption,
532
- inboxState,
533
- deliveryState,
534
- event.direction === "out" ? event.occurredAtMs : null,
535
- Date.now()
536
- );
537
- return true;
538
- })();
1188
+ let inserted;
1189
+ try {
1190
+ inserted = db.transaction(() => {
1191
+ const claimed = db.prepare(`
1192
+ INSERT INTO wire_items(wire_id, item_kind) VALUES (?, 'file')
1193
+ ON CONFLICT(wire_id) DO NOTHING
1194
+ `).run(event.wireId);
1195
+ if (claimed.changes !== 1) return false;
1196
+ db.prepare(`
1197
+ INSERT INTO files (
1198
+ wire_id, peer_cid, peer_name_snapshot, direction, filename, mime,
1199
+ byte_length, sha256, blob_relpath, occurred_at_ms, reply_to_wire_id,
1200
+ reply_to_sentence, encryption, inbox_state, delivery_state,
1201
+ human_read_at_ms, inserted_at_ms
1202
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1203
+ `).run(
1204
+ event.wireId,
1205
+ event.peerCid,
1206
+ event.peerName,
1207
+ event.direction,
1208
+ event.filename,
1209
+ event.mime || "application/octet-stream",
1210
+ event.bytes.byteLength,
1211
+ blob.sha256,
1212
+ blob.relpath,
1213
+ event.occurredAtMs,
1214
+ event.replyToWireId ?? null,
1215
+ event.replyToSentence ?? null,
1216
+ event.encryption,
1217
+ inboxState,
1218
+ deliveryState,
1219
+ event.direction === "out" ? event.occurredAtMs : null,
1220
+ Date.now()
1221
+ );
1222
+ return true;
1223
+ })();
1224
+ } catch (error) {
1225
+ cleanupHistoryBlob(id.dir, blob.relpath);
1226
+ throw error;
1227
+ }
1228
+ if (!inserted) cleanupHistoryBlob(id.dir, blob.relpath);
1229
+ else noteHistoryStorageMutation(id.dir, event.bytes.byteLength);
1230
+ return inserted;
539
1231
  }
540
1232
  function ingestMonitoringEvent(id, event) {
541
1233
  if (!event.itemId || !event.sourceCid || !event.sourceName || !event.peerCid || !event.peerName) {
@@ -543,7 +1235,7 @@ function ingestMonitoringEvent(id, event) {
543
1235
  }
544
1236
  if (!Number.isSafeInteger(event.occurredAtMs) || event.occurredAtMs < 0) throw new Error("invalid monitoring timestamp");
545
1237
  if (Buffer.byteLength(event.body, "utf8") > MAX_TEXT_BYTES) throw new Error("monitoring body exceeds host limit");
546
- return openHistory(id).prepare(`
1238
+ const inserted = openHistory(id).prepare(`
547
1239
  INSERT INTO monitoring_items (
548
1240
  item_id, source_cid, source_name_snapshot, direction, peer_cid,
549
1241
  peer_name_snapshot, body, occurred_at_ms, queue_state, inserted_at_ms
@@ -560,12 +1252,14 @@ function ingestMonitoringEvent(id, event) {
560
1252
  event.occurredAtMs,
561
1253
  Date.now()
562
1254
  ).changes === 1;
1255
+ if (inserted) noteHistoryStorageMutation(id.dir, Buffer.byteLength(event.body, "utf8"));
1256
+ return inserted;
563
1257
  }
564
1258
  function ingestControlEvent(id, event) {
565
1259
  if (!event.itemId || !event.senderCid || !event.senderName) throw new Error("invalid control event metadata");
566
1260
  if (!Number.isSafeInteger(event.occurredAtMs) || event.occurredAtMs < 0) throw new Error("invalid control timestamp");
567
1261
  if (Buffer.byteLength(event.payload, "utf8") > MAX_TEXT_BYTES) throw new Error("control payload exceeds host limit");
568
- return openHistory(id).prepare(`
1262
+ const inserted = openHistory(id).prepare(`
569
1263
  INSERT INTO control_requests (
570
1264
  item_id, sender_cid, sender_name_snapshot, payload,
571
1265
  occurred_at_ms, queue_state, inserted_at_ms
@@ -579,10 +1273,12 @@ function ingestControlEvent(id, event) {
579
1273
  event.occurredAtMs,
580
1274
  Date.now()
581
1275
  ).changes === 1;
1276
+ if (inserted) noteHistoryStorageMutation(id.dir, Buffer.byteLength(event.payload, "utf8"));
1277
+ return inserted;
582
1278
  }
583
1279
  function promotePendingIntroduction(id, peerCid) {
584
1280
  const db = openHistory(id);
585
- return db.transaction(() => {
1281
+ const changed = db.transaction(() => {
586
1282
  const messages = db.prepare(`
587
1283
  UPDATE messages SET inbox_state = 'unread'
588
1284
  WHERE direction = 'in' AND peer_cid = ? AND inbox_state = 'pending_introduction'
@@ -593,10 +1289,16 @@ function promotePendingIntroduction(id, peerCid) {
593
1289
  `).run(peerCid).changes;
594
1290
  return messages + files;
595
1291
  })();
1292
+ if (changed > 0) noteHistoryStorageMutation(id.dir);
1293
+ return changed;
596
1294
  }
597
1295
  function rejectPendingIntroduction(id, peerCid) {
598
1296
  const db = openHistory(id);
599
- return db.transaction(() => {
1297
+ const blobRelpaths = db.prepare(`
1298
+ SELECT DISTINCT blob_relpath FROM files
1299
+ WHERE direction = 'in' AND peer_cid = ? AND inbox_state = 'pending_introduction'
1300
+ `).pluck().all(peerCid);
1301
+ const changed = db.transaction(() => {
600
1302
  const messages = db.prepare(`
601
1303
  DELETE FROM messages
602
1304
  WHERE direction = 'in' AND peer_cid = ? AND inbox_state = 'pending_introduction'
@@ -607,6 +1309,9 @@ function rejectPendingIntroduction(id, peerCid) {
607
1309
  `).run(peerCid).changes;
608
1310
  return messages + files;
609
1311
  })();
1312
+ for (const relpath of blobRelpaths) cleanupHistoryBlob(id.dir, relpath);
1313
+ if (changed > 0) noteHistoryStorageMutation(id.dir);
1314
+ return changed;
610
1315
  }
611
1316
  function batchLimit(value) {
612
1317
  const limit = value ?? DEFAULT_BATCH;
@@ -640,7 +1345,7 @@ function takeUnreadMessages(id, input = {}) {
640
1345
  const db = openHistory(id);
641
1346
  const limit = batchLimit(input.limit);
642
1347
  const requested = input.wire_ids;
643
- return db.transaction(() => {
1348
+ const result = db.transaction(() => {
644
1349
  let rows;
645
1350
  if (requested !== void 0) {
646
1351
  if (requested.length < 1 || requested.length > MAX_BATCH) {
@@ -676,6 +1381,8 @@ function takeUnreadMessages(id, input = {}) {
676
1381
  `).pluck().get());
677
1382
  return { messages: rows.map(messageFromRow), remaining };
678
1383
  })();
1384
+ if (result.messages.length > 0) noteHistoryStorageMutation(id.dir);
1385
+ return result;
679
1386
  }
680
1387
  function listIncomingFiles(id) {
681
1388
  const rows = openHistory(id).prepare(`
@@ -687,7 +1394,7 @@ function takeUnreadFiles(id, input = {}) {
687
1394
  const db = openHistory(id);
688
1395
  const limit = batchLimit(input.limit);
689
1396
  const requested = input.wire_ids;
690
- return db.transaction(() => {
1397
+ const result = db.transaction(() => {
691
1398
  let rows;
692
1399
  if (requested !== void 0) {
693
1400
  if (requested.length < 1 || requested.length > MAX_BATCH) throw new Error(`wire_ids must contain 1-${MAX_BATCH} items`);
@@ -704,18 +1411,21 @@ function takeUnreadFiles(id, input = {}) {
704
1411
  `).all(limit);
705
1412
  }
706
1413
  const mark = db.prepare(`
707
- UPDATE files SET inbox_state = 'read'
1414
+ UPDATE files SET inbox_state = 'read', human_read_at_ms = COALESCE(human_read_at_ms, ?)
708
1415
  WHERE seq = ? AND direction = 'in' AND inbox_state = 'unread'
709
1416
  `);
710
1417
  for (const row of rows) {
711
- if (mark.run(row.seq).changes !== 1) throw new Error(`file row ${row.seq} changed during read batch`);
1418
+ const readAt = Date.now();
1419
+ if (mark.run(readAt, row.seq).changes !== 1) throw new Error(`file row ${row.seq} changed during read batch`);
712
1420
  row.inbox_state = "read";
1421
+ row.human_read_at_ms = readAt;
713
1422
  }
714
1423
  const remaining = Number(db.prepare(`
715
1424
  SELECT COUNT(*) FROM files WHERE direction = 'in' AND inbox_state = 'unread'
716
1425
  `).pluck().get());
717
1426
  return { files: rows.map((row) => fileFromRow(id, row)), remaining };
718
1427
  })();
1428
+ return result;
719
1429
  }
720
1430
  function historyWhere(query) {
721
1431
  const clauses = [];
@@ -770,10 +1480,10 @@ function getFileHistoryItem(id, wireId) {
770
1480
  }
771
1481
  function resolveStoredFile(id, wireId) {
772
1482
  const item = getFileHistoryItem(id, wireId);
773
- if (!item || !fs3.existsSync(item.blob_path)) return null;
774
- const identityRoot = resolve2(id.dir);
775
- const resolvedPath = resolve2(item.blob_path);
776
- return resolvedPath.startsWith(`${identityRoot}${sep}`) ? resolvedPath : null;
1483
+ if (!item || !fs4.existsSync(item.blob_path)) return null;
1484
+ const identityRoot = resolve3(id.dir);
1485
+ const resolvedPath = resolve3(item.blob_path);
1486
+ return resolvedPath.startsWith(`${identityRoot}${sep2}`) ? resolvedPath : null;
777
1487
  }
778
1488
  function updateDeliveryState(id, peerCid, kind, wireIds) {
779
1489
  const db = openHistory(id);
@@ -791,6 +1501,7 @@ function updateDeliveryState(id, peerCid, kind, wireIds) {
791
1501
  }
792
1502
  });
793
1503
  tx();
1504
+ if (wireIds.length > 0) noteHistoryStorageMutation(id.dir);
794
1505
  }
795
1506
 
796
1507
  // ../../src/constants.ts
@@ -802,6 +1513,7 @@ function setWrapper(w) {
802
1513
  wrapper = w;
803
1514
  }
804
1515
  var identities = /* @__PURE__ */ new Map();
1516
+ var quarantinedIdentities = /* @__PURE__ */ new Map();
805
1517
  var registrar = null;
806
1518
  var registrarAdBlob = null;
807
1519
  function setRegistrar(id) {
@@ -810,15 +1522,15 @@ function setRegistrar(id) {
810
1522
  function setRegistrarAdBlob(blob) {
811
1523
  registrarAdBlob = blob;
812
1524
  }
813
- var identityDir = (name) => join4(STATE_DIR, name);
814
- var keyPath = (dir) => join4(dir, "identity.key");
815
- var dataPath = (dir) => join4(dir, "state_data.bin");
816
- var notifyLogPath = (dir) => join4(dir, "notifications.log");
817
- var unreadPath = (dir) => join4(dir, "unread.json");
818
- var tempMetaPath = (dir) => join4(dir, "temp.json");
1525
+ var identityDir = (name) => join5(STATE_DIR, name);
1526
+ var keyPath = (dir) => join5(dir, "identity.key");
1527
+ var dataPath = (dir) => join5(dir, "state_data.bin");
1528
+ var notifyLogPath = (dir) => join5(dir, "notifications.log");
1529
+ var unreadPath = (dir) => join5(dir, "unread.json");
1530
+ var tempMetaPath = (dir) => join5(dir, "temp.json");
819
1531
  var hashLeaseToken = (token) => createHash2("sha256").update(token).digest("hex");
820
1532
  function writeTempMetaFile(dir, meta) {
821
- fs4.writeFileSync(
1533
+ fs5.writeFileSync(
822
1534
  tempMetaPath(dir),
823
1535
  JSON.stringify({ v: 1, owner: { token_sha256: meta.owner.tokenHash, pid: meta.owner.pid }, created_at: meta.createdAt }),
824
1536
  { mode: 384 }
@@ -826,7 +1538,7 @@ function writeTempMetaFile(dir, meta) {
826
1538
  }
827
1539
  function readTempMetaFile(dir) {
828
1540
  try {
829
- const raw = JSON.parse(fs4.readFileSync(tempMetaPath(dir), "utf8"));
1541
+ const raw = JSON.parse(fs5.readFileSync(tempMetaPath(dir), "utf8"));
830
1542
  if (raw.v !== 1 || typeof raw.owner?.token_sha256 !== "string" || typeof raw.owner?.pid !== "number") return null;
831
1543
  return {
832
1544
  owner: { tokenHash: raw.owner.token_sha256, pid: raw.owner.pid },
@@ -838,33 +1550,33 @@ function readTempMetaFile(dir) {
838
1550
  }
839
1551
  function tightenIdentityPerms() {
840
1552
  for (const name of listPersistedNames()) {
841
- const dir = join4(STATE_DIR, name);
1553
+ const dir = join5(STATE_DIR, name);
842
1554
  try {
843
- fs4.chmodSync(dir, 448);
1555
+ fs5.chmodSync(dir, 448);
844
1556
  } catch (err) {
845
1557
  log(`[${name}] chmod 0700 failed:`, String(err));
846
1558
  }
847
1559
  for (const f of [dataPath(dir), keyPath(dir), historyPath({ dir }), `${historyPath({ dir })}-wal`, `${historyPath({ dir })}-shm`, notifyLogPath(dir), unreadPath(dir)]) {
848
- if (!fs4.existsSync(f)) continue;
1560
+ if (!fs5.existsSync(f)) continue;
849
1561
  try {
850
- fs4.chmodSync(f, 384);
1562
+ fs5.chmodSync(f, 384);
851
1563
  } catch (err) {
852
1564
  log(`[${name}] chmod 0600 ${f} failed:`, String(err));
853
1565
  }
854
1566
  }
855
- const blobs = join4(dir, "blobs");
856
- if (fs4.existsSync(blobs)) {
1567
+ const blobs = join5(dir, "blobs");
1568
+ if (fs5.existsSync(blobs)) {
857
1569
  const tightenTree = (path) => {
858
- for (const entry of fs4.readdirSync(path, { withFileTypes: true })) {
859
- const child = join4(path, entry.name);
1570
+ for (const entry of fs5.readdirSync(path, { withFileTypes: true })) {
1571
+ const child = join5(path, entry.name);
860
1572
  if (entry.isDirectory()) {
861
- fs4.chmodSync(child, 448);
1573
+ fs5.chmodSync(child, 448);
862
1574
  tightenTree(child);
863
- } else if (entry.isFile()) fs4.chmodSync(child, 384);
1575
+ } else if (entry.isFile()) fs5.chmodSync(child, 384);
864
1576
  }
865
1577
  };
866
1578
  try {
867
- fs4.chmodSync(blobs, 448);
1579
+ fs5.chmodSync(blobs, 448);
868
1580
  tightenTree(blobs);
869
1581
  } catch (err) {
870
1582
  log(`[${name}] chmod history blobs failed:`, String(err));
@@ -883,41 +1595,41 @@ function findIdentityFile(id, wireId) {
883
1595
  }
884
1596
  }
885
1597
  function listPersistedNames() {
886
- if (!fs4.existsSync(STATE_DIR)) return [];
887
- return fs4.readdirSync(STATE_DIR, { withFileTypes: true }).filter((d) => d.isDirectory() && fs4.existsSync(keyPath(join4(STATE_DIR, d.name)))).map((d) => d.name);
1598
+ if (!fs5.existsSync(STATE_DIR)) return [];
1599
+ return fs5.readdirSync(STATE_DIR, { withFileTypes: true }).filter((d) => d.isDirectory() && fs5.existsSync(keyPath(join5(STATE_DIR, d.name)))).map((d) => d.name);
888
1600
  }
889
1601
 
890
1602
  // ../../src/identity/lease.ts
891
- import { join as join5 } from "node:path";
892
- import * as fs5 from "node:fs";
1603
+ import { join as join6 } from "node:path";
1604
+ import * as fs6 from "node:fs";
893
1605
  var leases = /* @__PURE__ */ new Map();
894
1606
  var tombstones = /* @__PURE__ */ new Set();
895
1607
  var sessionHeaders = /* @__PURE__ */ new Map();
896
1608
  var outboundRemovalInFlight = /* @__PURE__ */ new Set();
897
- function pidAlive(pid) {
898
- if (!Number.isInteger(pid) || pid <= 0) return false;
1609
+ function pidDefinitelyDead(pid) {
1610
+ if (!Number.isInteger(pid) || pid <= 1) return false;
899
1611
  try {
900
1612
  process.kill(pid, 0);
901
- return true;
1613
+ return false;
902
1614
  } catch (err) {
903
- return err.code === "EPERM";
1615
+ return err.code === "ESRCH";
904
1616
  }
905
1617
  }
906
1618
  function leaseByToken(token) {
907
1619
  for (const l of leases.values()) if (l.token === token) return l;
908
1620
  return void 0;
909
1621
  }
910
- var bindingsSnapshotPath = () => join5(STATE_DIR, "bindings.json");
1622
+ var bindingsSnapshotPath = () => join6(STATE_DIR, "bindings.json");
911
1623
  function persistBindings() {
912
1624
  try {
913
- fs5.mkdirSync(STATE_DIR, { recursive: true });
1625
+ fs6.mkdirSync(STATE_DIR, { recursive: true });
914
1626
  const tmp = `${bindingsSnapshotPath()}.tmp`;
915
- fs5.writeFileSync(tmp, JSON.stringify({
1627
+ fs6.writeFileSync(tmp, JSON.stringify({
916
1628
  pid: process.pid,
917
1629
  bound: [...leases.keys()],
918
1630
  holders: [...leases.values()].map((l) => ({ identity: l.identity, pid: l.pid }))
919
1631
  }));
920
- fs5.renameSync(tmp, bindingsSnapshotPath());
1632
+ fs6.renameSync(tmp, bindingsSnapshotPath());
921
1633
  } catch (err) {
922
1634
  log("failed to persist bindings snapshot:", String(err));
923
1635
  }
@@ -963,19 +1675,19 @@ function bindSession(sid, name) {
963
1675
  }
964
1676
 
965
1677
  // ../../src/identity/hierarchy.ts
966
- import { join as join8 } from "node:path";
967
- import * as fs10 from "node:fs";
1678
+ import { join as join9 } from "node:path";
1679
+ import * as fs11 from "node:fs";
968
1680
 
969
1681
  // ../../src/mufl/tx.ts
970
1682
  import { AdaptObjectLifetime as AdaptObjectLifetime2 } from "@adapt-toolkit/sdk/common";
971
1683
  import { object_to_adapt_value } from "@adapt-toolkit/sdk/wrapper";
972
1684
 
973
1685
  // ../../src/state.ts
974
- import * as fs9 from "node:fs";
1686
+ import * as fs10 from "node:fs";
975
1687
  import { randomBytes as randomBytes2 } from "node:crypto";
976
1688
 
977
1689
  // ../../src/identity/provision.ts
978
- import * as fs8 from "node:fs";
1690
+ import * as fs9 from "node:fs";
979
1691
  import { randomBytes } from "node:crypto";
980
1692
  import { PacketWrapperConfigurator } from "@adapt-toolkit/sdk/wrappers";
981
1693
 
@@ -983,8 +1695,8 @@ import { PacketWrapperConfigurator } from "@adapt-toolkit/sdk/wrappers";
983
1695
  import { AdaptObjectLifetime } from "@adapt-toolkit/sdk/common";
984
1696
 
985
1697
  // ../../src/notify.ts
986
- import { join as join6 } from "node:path";
987
- import * as fs6 from "node:fs";
1698
+ import { join as join7 } from "node:path";
1699
+ import * as fs7 from "node:fs";
988
1700
 
989
1701
  // ../../src/events.ts
990
1702
  var nonEmptyString = (value) => typeof value === "string" && value.length > 0;
@@ -1108,7 +1820,7 @@ function fireNotifyWaiters(name) {
1108
1820
  }
1109
1821
  }
1110
1822
  function waitForNotify(name, ms) {
1111
- return new Promise((resolve3) => {
1823
+ return new Promise((resolve4) => {
1112
1824
  let set = notifyWaiters.get(name);
1113
1825
  if (!set) {
1114
1826
  set = /* @__PURE__ */ new Set();
@@ -1121,7 +1833,7 @@ function waitForNotify(name, ms) {
1121
1833
  set.delete(fn);
1122
1834
  if (set.size === 0) notifyWaiters.delete(name);
1123
1835
  clearTimeout(timer);
1124
- resolve3();
1836
+ resolve4();
1125
1837
  };
1126
1838
  const fn = finish;
1127
1839
  const timer = setTimeout(finish, ms);
@@ -1146,10 +1858,10 @@ function bodyFreeNotifyValue(value) {
1146
1858
  function appendNotifyLog(id, event) {
1147
1859
  const contentFree = bodyFreeNotifyValue(event);
1148
1860
  try {
1149
- fs6.mkdirSync(id.dir, { recursive: true, mode: 448 });
1861
+ fs7.mkdirSync(id.dir, { recursive: true, mode: 448 });
1150
1862
  const path = notifyLogPath(id.dir);
1151
- fs6.appendFileSync(path, JSON.stringify(contentFree) + "\n", { mode: 384 });
1152
- fs6.chmodSync(path, 384);
1863
+ fs7.appendFileSync(path, JSON.stringify(contentFree) + "\n", { mode: 384 });
1864
+ fs7.chmodSync(path, 384);
1153
1865
  } catch (err) {
1154
1866
  log(`[${id.name}] failed to append notifications.log:`, String(err));
1155
1867
  }
@@ -1164,7 +1876,7 @@ var NOTIFY_LONGPOLL_MS = Number(process.env.OURS_NOTIFY_LONGPOLL_MS) > 0 ? Numbe
1164
1876
  var NOTIFY_RECHECK_MS = 250;
1165
1877
  function notifyLogSize(logPath) {
1166
1878
  try {
1167
- return fs6.statSync(logPath).size;
1879
+ return fs7.statSync(logPath).size;
1168
1880
  } catch {
1169
1881
  return 0;
1170
1882
  }
@@ -1174,11 +1886,11 @@ function readNotifyRange(logPath, from, to) {
1174
1886
  const buf = Buffer.alloc(to - from);
1175
1887
  let read = 0;
1176
1888
  try {
1177
- const fd = fs6.openSync(logPath, "r");
1889
+ const fd = fs7.openSync(logPath, "r");
1178
1890
  try {
1179
- read = fs6.readSync(fd, buf, 0, buf.length, from);
1891
+ read = fs7.readSync(fd, buf, 0, buf.length, from);
1180
1892
  } finally {
1181
- fs6.closeSync(fd);
1893
+ fs7.closeSync(fd);
1182
1894
  }
1183
1895
  } catch {
1184
1896
  return { events: [], cursor: from };
@@ -1197,7 +1909,7 @@ function readNotifyRange(logPath, from, to) {
1197
1909
  return { events, cursor: from + lastNl + 1 };
1198
1910
  }
1199
1911
  async function serveNotifications(req, res, name, sinceParam, kindsParam = null) {
1200
- const logPath = notifyLogPath(join6(STATE_DIR, name));
1912
+ const logPath = notifyLogPath(join7(STATE_DIR, name));
1201
1913
  const send = (cursor, events) => {
1202
1914
  if (res.writableEnded) return;
1203
1915
  res.writeHead(200, { "Content-Type": "application/json" });
@@ -1268,11 +1980,11 @@ function refreshUnread(id) {
1268
1980
  files: unreadFiles.length,
1269
1981
  unread_files: unreadFiles.slice(-10)
1270
1982
  };
1271
- fs6.mkdirSync(id.dir, { recursive: true, mode: 448 });
1983
+ fs7.mkdirSync(id.dir, { recursive: true, mode: 448 });
1272
1984
  const tmp = `${unreadPath(id.dir)}.tmp`;
1273
- fs6.writeFileSync(tmp, JSON.stringify(snapshot), { mode: 384 });
1274
- fs6.chmodSync(tmp, 384);
1275
- fs6.renameSync(tmp, unreadPath(id.dir));
1985
+ fs7.writeFileSync(tmp, JSON.stringify(snapshot), { mode: 384 });
1986
+ fs7.chmodSync(tmp, 384);
1987
+ fs7.renameSync(tmp, unreadPath(id.dir));
1276
1988
  } catch (err) {
1277
1989
  log(`[${id.name}] failed to refresh unread snapshot:`, String(err));
1278
1990
  }
@@ -1281,7 +1993,7 @@ function unreadSummary() {
1281
1993
  const out = [];
1282
1994
  let entries = [];
1283
1995
  try {
1284
- entries = fs6.readdirSync(STATE_DIR, { withFileTypes: true });
1996
+ entries = fs7.readdirSync(STATE_DIR, { withFileTypes: true });
1285
1997
  } catch {
1286
1998
  return { identities: out };
1287
1999
  }
@@ -1289,7 +2001,7 @@ function unreadSummary() {
1289
2001
  if (!entry.isDirectory() || validateName(entry.name) !== null) continue;
1290
2002
  let value;
1291
2003
  try {
1292
- value = JSON.parse(fs6.readFileSync(unreadPath(join6(STATE_DIR, entry.name)), "utf8"));
2004
+ value = JSON.parse(fs7.readFileSync(unreadPath(join7(STATE_DIR, entry.name)), "utf8"));
1293
2005
  } catch {
1294
2006
  continue;
1295
2007
  }
@@ -1834,24 +2546,24 @@ function wireHandlers(id, hooks) {
1834
2546
  }
1835
2547
 
1836
2548
  // ../../src/book.ts
1837
- import { join as join7 } from "node:path";
1838
- import * as fs7 from "node:fs";
1839
- var bookDir = () => join7(STATE_DIR, BOOK_DIR_NAME);
1840
- var registrarKeyPath = () => join7(bookDir(), "registrar.key");
1841
- var bookPath = () => join7(bookDir(), "book.json");
2549
+ import { join as join8 } from "node:path";
2550
+ import * as fs8 from "node:fs";
2551
+ var bookDir = () => join8(STATE_DIR, BOOK_DIR_NAME);
2552
+ var registrarKeyPath = () => join8(bookDir(), "registrar.key");
2553
+ var bookPath = () => join8(bookDir(), "book.json");
1842
2554
  function readBook() {
1843
2555
  try {
1844
- const parsed = JSON.parse(fs7.readFileSync(bookPath(), "utf8"));
2556
+ const parsed = JSON.parse(fs8.readFileSync(bookPath(), "utf8"));
1845
2557
  return parsed && typeof parsed.entries === "object" ? parsed.entries : {};
1846
2558
  } catch {
1847
2559
  return {};
1848
2560
  }
1849
2561
  }
1850
2562
  function writeBook(entries) {
1851
- fs7.mkdirSync(bookDir(), { recursive: true });
2563
+ fs8.mkdirSync(bookDir(), { recursive: true });
1852
2564
  const tmp = `${bookPath()}.tmp`;
1853
- fs7.writeFileSync(tmp, JSON.stringify({ v: 1, entries }, null, 2), { mode: 384 });
1854
- fs7.renameSync(tmp, bookPath());
2565
+ fs8.writeFileSync(tmp, JSON.stringify({ v: 1, entries }, null, 2), { mode: 384 });
2566
+ fs8.renameSync(tmp, bookPath());
1855
2567
  }
1856
2568
  function exportAdBlob(id) {
1857
2569
  return withScope(
@@ -1902,6 +2614,85 @@ async function pinRegistrar(id) {
1902
2614
  });
1903
2615
  }
1904
2616
 
2617
+ // ../../src/identity/migration.ts
2618
+ function wasPublished(id) {
2619
+ return Object.values(readBook()).some((entry) => entry.container_id === id.cid);
2620
+ }
2621
+ async function finishRestoredActivation(id) {
2622
+ wrapper.expose_packet(id.cid);
2623
+ identities.set(id.name, id);
2624
+ quarantinedIdentities.delete(id.name);
2625
+ log(`[${id.name}] EXPOSED (routing + broker registration) \u2014 hierarchy reconciled after restore`);
2626
+ try {
2627
+ await contactRestoreSweep(id);
2628
+ } catch (err) {
2629
+ log(`[${id.name}] post-activation contact restore sweep failed:`, String(err));
2630
+ }
2631
+ try {
2632
+ refreshUnread(id);
2633
+ } catch (err) {
2634
+ log(`[${id.name}] post-activation unread refresh failed:`, String(err));
2635
+ }
2636
+ return id;
2637
+ }
2638
+ function quarantine(id, status) {
2639
+ const exposeLocal = wasPublished(id);
2640
+ if (exposeLocal) unpublishFromBook(id);
2641
+ identities.delete(id.name);
2642
+ quarantinedIdentities.set(id.name, { identity: id, status, exposeLocal });
2643
+ log(`[${id.name}] QUARANTINED (${status}) \u2014 management-only, unexposed and unbindable`);
2644
+ return id;
2645
+ }
2646
+ async function reconcileRestoredIdentity(id) {
2647
+ if (rootName === id.name) return finishRestoredActivation(id);
2648
+ const hostRoot = rootName ? identities.get(rootName) : void 0;
2649
+ let info;
2650
+ try {
2651
+ info = describeIdentity(id);
2652
+ } catch (err) {
2653
+ log(`[${id.name}] hierarchy classification failed:`, String(err));
2654
+ return quarantine(id, "migration-failed");
2655
+ }
2656
+ if (hostRoot) {
2657
+ if (info.hasCert && info.roleId !== "" && info.rootCid !== hostRoot.cid) {
2658
+ log(`[${id.name}] preserved imported delegation from root ${info.rootCid.slice(0, 12)}\u2026`);
2659
+ return finishRestoredActivation(id);
2660
+ }
2661
+ try {
2662
+ await delegateRole(hostRoot, id);
2663
+ return finishRestoredActivation(id);
2664
+ } catch (err) {
2665
+ log(`[${id.name}] root delegation during restore failed:`, String(err));
2666
+ return quarantine(id, "migration-failed");
2667
+ }
2668
+ }
2669
+ if (info.hasCert && info.roleId !== "") return finishRestoredActivation(id);
2670
+ return quarantine(id, "awaiting-root");
2671
+ }
2672
+ async function adoptQuarantinedIdentities(root) {
2673
+ const adopted = [];
2674
+ const failed = [];
2675
+ for (const [name, held] of [...quarantinedIdentities]) {
2676
+ try {
2677
+ await delegateRole(root, held.identity);
2678
+ if (held.exposeLocal) await publishToBook(held.identity);
2679
+ await finishRestoredActivation(held.identity);
2680
+ quarantinedIdentities.delete(name);
2681
+ adopted.push(name);
2682
+ } catch (err) {
2683
+ try {
2684
+ unpublishFromBook(held.identity);
2685
+ } catch {
2686
+ }
2687
+ held.status = "migration-failed";
2688
+ quarantinedIdentities.set(name, held);
2689
+ log(`[${name}] quarantine adoption failed under root "${root.name}":`, String(err));
2690
+ failed.push(name);
2691
+ }
2692
+ }
2693
+ return { adopted, failed };
2694
+ }
2695
+
1905
2696
  // ../../src/identity/provision.ts
1906
2697
  function createPacket(name, seed, dir, track = true, signingSecret, deferExposure = false) {
1907
2698
  const config = new PacketWrapperConfigurator();
@@ -1958,7 +2749,7 @@ async function provisionIdentity(name, opts = { exposeLocal: true, localAutoAcce
1958
2749
  let provisioned;
1959
2750
  try {
1960
2751
  const dir = identityDir(name);
1961
- fs8.mkdirSync(dir, { recursive: true, mode: 448 });
2752
+ fs9.mkdirSync(dir, { recursive: true, mode: 448 });
1962
2753
  let tempMeta;
1963
2754
  if (opts.temp) {
1964
2755
  tempMeta = { owner: { tokenHash: opts.temp.tokenHash, pid: opts.temp.pid }, createdAt: Date.now() };
@@ -1969,7 +2760,7 @@ async function provisionIdentity(name, opts = { exposeLocal: true, localAutoAcce
1969
2760
  provisioned = id;
1970
2761
  openHistory(id);
1971
2762
  if (tempMeta) id.temp = tempMeta;
1972
- fs8.writeFileSync(keyPath(dir), exportSigningSecret(id), { mode: 384 });
2763
+ fs9.writeFileSync(keyPath(dir), exportSigningSecret(id), { mode: 384 });
1973
2764
  await withScopeAsync(async (lt) => {
1974
2765
  await mutatingTx(id, "::a2a_messaging::set_my_name", { name }, lt);
1975
2766
  });
@@ -1992,7 +2783,7 @@ async function provisionIdentity(name, opts = { exposeLocal: true, localAutoAcce
1992
2783
  }
1993
2784
  async function restoreIdentity(name) {
1994
2785
  const dir = identityDir(name);
1995
- const secret = fs8.readFileSync(keyPath(dir), "utf8").trim();
2786
+ const secret = fs9.readFileSync(keyPath(dir), "utf8").trim();
1996
2787
  const id = await createPacket(name, "", dir, false, secret, true);
1997
2788
  log(`[${name}] created QUARANTINED (no routing/broker registration, not client-bindable) \u2014 importing state before exposure`);
1998
2789
  const holdMs = Number(process.env.OURS_TEST_RESTORE_HOLD_MS || "") || 0;
@@ -2017,7 +2808,7 @@ async function restoreIdentity(name) {
2017
2808
  if (process.env.OURS_TEST_FORCE_IMPORT_TIMEOUT === "1") {
2018
2809
  throw new Error("timed out waiting for the transaction result (forced by OURS_TEST_FORCE_IMPORT_TIMEOUT)");
2019
2810
  }
2020
- const buf = fs8.readFileSync(dataPath(dir));
2811
+ const buf = fs9.readFileSync(dataPath(dir));
2021
2812
  await withScopeAsync(async (lt) => {
2022
2813
  const adaptData = id.pw.packet.ParseValue(new Uint8Array(buf)).Attach(lt);
2023
2814
  const importTimeoutMs = Number(process.env.OURS_IMPORT_TIMEOUT_MS || "") || void 0;
@@ -2086,33 +2877,30 @@ async function restoreIdentity(name) {
2086
2877
  } catch (err) {
2087
2878
  tearDownUnexposed("history_open", "FAILED (SQLite history unavailable)", err);
2088
2879
  }
2089
- wrapper.expose_packet(id.cid);
2090
- identities.set(name, id);
2091
- log(`[${name}] EXPOSED (routing + broker registration) \u2014 import phase complete`);
2092
- await contactRestoreSweep(id);
2093
- refreshUnread(id);
2094
- return id;
2880
+ const restoredTemp = readTempMetaFile(id.dir);
2881
+ if (restoredTemp) id.temp = restoredTemp;
2882
+ return reconcileRestoredIdentity(id);
2095
2883
  }
2096
2884
 
2097
2885
  // ../../src/state.ts
2098
2886
  async function ensureRegistrar() {
2099
- fs9.mkdirSync(bookDir(), { recursive: true });
2887
+ fs10.mkdirSync(bookDir(), { recursive: true });
2100
2888
  let secret;
2101
2889
  try {
2102
- secret = fs9.readFileSync(registrarKeyPath(), "utf8").trim();
2890
+ secret = fs10.readFileSync(registrarKeyPath(), "utf8").trim();
2103
2891
  } catch {
2104
2892
  }
2105
2893
  const seed = randomBytes2(24).toString("hex");
2106
2894
  setRegistrar(await createPacket(BOOK_DIR_NAME, seed, bookDir(), false, secret));
2107
2895
  if (!secret) {
2108
- fs9.writeFileSync(registrarKeyPath(), exportSigningSecret(registrar), { mode: 384 });
2896
+ fs10.writeFileSync(registrarKeyPath(), exportSigningSecret(registrar), { mode: 384 });
2109
2897
  }
2110
2898
  setRegistrarAdBlob(exportAdBlob(registrar));
2111
2899
  log(`contact-book registrar ready (${registrar.cid})`);
2112
2900
  }
2113
2901
  function hasSavedState(dir) {
2114
2902
  try {
2115
- return fs9.existsSync(dataPath(dir)) && fs9.statSync(dataPath(dir)).size > 0;
2903
+ return fs10.existsSync(dataPath(dir)) && fs10.statSync(dataPath(dir)).size > 0;
2116
2904
  } catch {
2117
2905
  return false;
2118
2906
  }
@@ -2121,38 +2909,38 @@ function saveState(id) {
2121
2909
  const bytes = withScope(
2122
2910
  (lt) => Buffer.from(readonlyTx(id, "::actor::export_state", lt).Serialize())
2123
2911
  );
2124
- fs9.mkdirSync(id.dir, { recursive: true, mode: 448 });
2912
+ fs10.mkdirSync(id.dir, { recursive: true, mode: 448 });
2125
2913
  const final = dataPath(id.dir);
2126
2914
  const tmp = `${final}.tmp`;
2127
2915
  let fd;
2128
2916
  try {
2129
- fd = fs9.openSync(tmp, "w", 384);
2130
- fs9.fchmodSync(fd, 384);
2131
- fs9.writeFileSync(fd, bytes);
2132
- fs9.fsyncSync(fd);
2133
- fs9.closeSync(fd);
2917
+ fd = fs10.openSync(tmp, "w", 384);
2918
+ fs10.fchmodSync(fd, 384);
2919
+ fs10.writeFileSync(fd, bytes);
2920
+ fs10.fsyncSync(fd);
2921
+ fs10.closeSync(fd);
2134
2922
  fd = void 0;
2135
- fs9.renameSync(tmp, final);
2136
- fs9.chmodSync(final, 384);
2923
+ fs10.renameSync(tmp, final);
2924
+ fs10.chmodSync(final, 384);
2137
2925
  try {
2138
- fs9.chmodSync(keyPath(id.dir), 384);
2926
+ fs10.chmodSync(keyPath(id.dir), 384);
2139
2927
  } catch {
2140
2928
  }
2141
- const dirFd = fs9.openSync(id.dir, "r");
2929
+ const dirFd = fs10.openSync(id.dir, "r");
2142
2930
  try {
2143
- fs9.fsyncSync(dirFd);
2931
+ fs10.fsyncSync(dirFd);
2144
2932
  } finally {
2145
- fs9.closeSync(dirFd);
2933
+ fs10.closeSync(dirFd);
2146
2934
  }
2147
2935
  } catch (err) {
2148
2936
  if (fd !== void 0) {
2149
2937
  try {
2150
- fs9.closeSync(fd);
2938
+ fs10.closeSync(fd);
2151
2939
  } catch {
2152
2940
  }
2153
2941
  }
2154
2942
  try {
2155
- fs9.rmSync(tmp, { force: true });
2943
+ fs10.rmSync(tmp, { force: true });
2156
2944
  } catch {
2157
2945
  }
2158
2946
  throw err;
@@ -2264,24 +3052,24 @@ function mutatingTx(id, name, targ, lt, timeoutMs) {
2264
3052
  function setRootName(name) {
2265
3053
  rootName = name;
2266
3054
  }
2267
- var rootMarkerPath = () => join8(STATE_DIR, "root.json");
3055
+ var rootMarkerPath = () => join9(STATE_DIR, "root.json");
2268
3056
  var rootName = null;
2269
3057
  function readRootMarker() {
2270
3058
  try {
2271
- const parsed = JSON.parse(fs10.readFileSync(rootMarkerPath(), "utf8"));
3059
+ const parsed = JSON.parse(fs11.readFileSync(rootMarkerPath(), "utf8"));
2272
3060
  return typeof parsed.name === "string" ? parsed.name : null;
2273
3061
  } catch {
2274
3062
  return null;
2275
3063
  }
2276
3064
  }
2277
3065
  function writeRootMarker(name) {
2278
- fs10.mkdirSync(STATE_DIR, { recursive: true });
3066
+ fs11.mkdirSync(STATE_DIR, { recursive: true });
2279
3067
  const tmp = `${rootMarkerPath()}.tmp`;
2280
- fs10.writeFileSync(tmp, JSON.stringify({ v: 1, name }));
2281
- fs10.renameSync(tmp, rootMarkerPath());
3068
+ fs11.writeFileSync(tmp, JSON.stringify({ v: 1, name }));
3069
+ fs11.renameSync(tmp, rootMarkerPath());
2282
3070
  }
2283
3071
  function clearRootMarker() {
2284
- fs10.rmSync(rootMarkerPath(), { force: true });
3072
+ fs11.rmSync(rootMarkerPath(), { force: true });
2285
3073
  }
2286
3074
  function describeIdentity(id) {
2287
3075
  return withScope((lt) => {
@@ -2345,9 +3133,12 @@ async function establishRoot(id) {
2345
3133
  return { adopted, failed };
2346
3134
  }
2347
3135
 
3136
+ // ../../src/identity/reaper.ts
3137
+ import { join as join10 } from "node:path";
3138
+ import * as fs13 from "node:fs";
3139
+
2348
3140
  // ../../src/identity/lifecycle.ts
2349
- import { join as join9 } from "node:path";
2350
- import * as fs11 from "node:fs";
3141
+ import * as fs12 from "node:fs";
2351
3142
 
2352
3143
  // ../../src/render/adapt-to-json.ts
2353
3144
  import { brotliCompressSync, brotliDecompressSync } from "node:zlib";
@@ -2672,7 +3463,7 @@ function deleteIdentityCompletely(id) {
2672
3463
  clearRootMarker();
2673
3464
  }
2674
3465
  try {
2675
- fs11.rmSync(id.dir, { recursive: true, force: true });
3466
+ fs12.rmSync(id.dir, { recursive: true, force: true });
2676
3467
  reservedNames.delete(id.name);
2677
3468
  } catch (err) {
2678
3469
  return `deleting ${id.dir} failed: ${String(err)}`;
@@ -2724,31 +3515,46 @@ function closeTemporaryIdentity(id, cause) {
2724
3515
  })();
2725
3516
  return t.closing;
2726
3517
  }
2727
- function sweepStaleTempIdentities() {
2728
- for (const id of [...identities.values()]) {
3518
+
3519
+ // ../../src/identity/reaper.ts
3520
+ function sessionReaperIntervalMs(raw = process.env.OURS_SESSION_REAPER_INTERVAL_MS) {
3521
+ if (raw === void 0 || raw.trim() === "") return 5e3;
3522
+ const parsed = Number(raw);
3523
+ if (!Number.isFinite(parsed)) return 5e3;
3524
+ return Math.max(100, Math.trunc(parsed));
3525
+ }
3526
+ async function reapStaleTemporaryIdentities() {
3527
+ const all = [
3528
+ ...identities.values(),
3529
+ ...[...quarantinedIdentities.values()].map((held) => held.identity)
3530
+ ];
3531
+ const closes = [];
3532
+ for (const id of all) {
2729
3533
  const t = id.temp;
2730
- if (!t || t.closing) continue;
2731
- if (pidAlive(t.owner.pid)) continue;
3534
+ if (!t || t.closing || !pidDefinitelyDead(t.owner.pid)) continue;
2732
3535
  const lease = leases.get(id.name);
2733
- if (lease && pidAlive(lease.pid)) continue;
2734
- void closeTemporaryIdentity(id, `stale lease \u2014 owner pid ${t.owner.pid} is dead`).catch(
2735
- (err) => log(`[${id.name}] stale-temp reclaim failed:`, String(err))
3536
+ if (lease && !pidDefinitelyDead(lease.pid)) continue;
3537
+ closes.push(
3538
+ closeTemporaryIdentity(id, `stale lease \u2014 owner pid ${t.owner.pid} is dead`).then(() => {
3539
+ quarantinedIdentities.delete(id.name);
3540
+ }).catch((err) => log(`[${id.name}] stale-temp reclaim failed:`, String(err)))
2736
3541
  );
2737
3542
  }
3543
+ await Promise.all(closes);
2738
3544
  }
2739
- function sweepOrphanTempDirs() {
2740
- if (!fs11.existsSync(STATE_DIR)) return;
2741
- for (const d of fs11.readdirSync(STATE_DIR, { withFileTypes: true })) {
2742
- if (!d.isDirectory() || identities.has(d.name)) continue;
2743
- const dir = join9(STATE_DIR, d.name);
3545
+ async function reapOrphanTemporaryDirectories() {
3546
+ if (!fs13.existsSync(STATE_DIR)) return;
3547
+ for (const d of fs13.readdirSync(STATE_DIR, { withFileTypes: true })) {
3548
+ if (!d.isDirectory() || identities.has(d.name) || quarantinedIdentities.has(d.name)) continue;
3549
+ const dir = join10(STATE_DIR, d.name);
2744
3550
  const meta = readTempMetaFile(dir);
2745
- if (!meta || pidAlive(meta.owner.pid)) continue;
3551
+ if (!meta || !pidDefinitelyDead(meta.owner.pid)) continue;
2746
3552
  try {
2747
- fs11.rmSync(dir, { recursive: true, force: true });
3553
+ fs13.rmSync(dir, { recursive: true, force: true });
2748
3554
  reservedNames.delete(d.name);
2749
- log(`[${d.name}] removed orphaned temporary-identity dir (owner pid ${meta.owner.pid} dead, no live packet)`);
3555
+ log(`[${d.name}] removed orphaned temporary-identity dir (owner pid ${meta.owner.pid} confirmed dead)`);
2750
3556
  } catch (err) {
2751
- log(`[${d.name}] failed to remove orphaned temporary-identity dir:`, String(err));
3557
+ throw new Error(`[${d.name}] failed to remove orphaned temporary-identity dir: ${String(err)}`);
2752
3558
  }
2753
3559
  }
2754
3560
  }
@@ -2859,7 +3665,9 @@ async function bootWrapper() {
2859
3665
  log("failed to start the contact-book registrar (local contact book disabled):", String(err));
2860
3666
  }
2861
3667
  tightenIdentityPerms();
2862
- const names = listPersistedNames();
3668
+ setRootName(readRootMarker());
3669
+ const persistedNames = listPersistedNames();
3670
+ const names = rootName && persistedNames.includes(rootName) ? [rootName, ...persistedNames.filter((name) => name !== rootName)] : persistedNames;
2863
3671
  const fakeRestoreCount = Math.max(0, Number(process.env.OURS_TEST_FAKE_RESTORE_COUNT || "") || 0);
2864
3672
  const fakeRestoreMs = Math.max(0, Number(process.env.OURS_TEST_FAKE_RESTORE_MS || "") || 0);
2865
3673
  const restoreTotal = names.length === 0 && fakeRestoreCount > 0 ? fakeRestoreCount : names.length;
@@ -2945,8 +3753,8 @@ async function bootWrapper() {
2945
3753
  if (refreshedRoles.length > 0) log(`refreshed ${refreshedRoles.length} role delegation cert(s) against the live AD on boot`);
2946
3754
  }
2947
3755
  persistBindings();
2948
- sweepOrphanTempDirs();
2949
- sweepStaleTempIdentities();
3756
+ await reapOrphanTemporaryDirectories();
3757
+ await reapStaleTemporaryIdentities();
2950
3758
  }
2951
3759
 
2952
3760
  export {
@@ -2956,12 +3764,21 @@ export {
2956
3764
  BROKER_URL,
2957
3765
  PORT,
2958
3766
  GC_INTERVAL_MS,
3767
+ HISTORY_MAX_BYTES,
2959
3768
  API_VISIBILITY,
2960
3769
  startupProgress,
2961
3770
  log,
2962
3771
  requireAuth,
2963
3772
  validateName,
3773
+ initializeHistoryRetention,
3774
+ noteHistoryStorageMutation,
3775
+ getHistoryStorageStatus,
3776
+ stopHistoryRetention,
3777
+ pinHistoryItems,
3778
+ unpinHistoryItems,
2964
3779
  consumeOutboundHistoryFailure,
3780
+ openHistory,
3781
+ closeHistory,
2965
3782
  listIncomingMessages,
2966
3783
  takeUnreadMessages,
2967
3784
  listIncomingFiles,
@@ -2972,8 +3789,12 @@ export {
2972
3789
  listFileHistory,
2973
3790
  getFileHistoryItem,
2974
3791
  FILE_SELECTION_CAP,
3792
+ wrapper,
2975
3793
  identities,
3794
+ quarantinedIdentities,
2976
3795
  registrar,
3796
+ identityDir,
3797
+ keyPath,
2977
3798
  hashLeaseToken,
2978
3799
  writeTempMetaFile,
2979
3800
  isSelectableWireId,
@@ -2982,7 +3803,7 @@ export {
2982
3803
  tombstones,
2983
3804
  sessionHeaders,
2984
3805
  outboundRemovalInFlight,
2985
- pidAlive,
3806
+ pidDefinitelyDead,
2986
3807
  leaseByToken,
2987
3808
  persistBindings,
2988
3809
  resolveBound,
@@ -3000,11 +3821,16 @@ export {
3000
3821
  e2eRecoverySweep,
3001
3822
  readBook,
3002
3823
  exportAdBlob,
3824
+ exportSigningSecret,
3003
3825
  publishToBook,
3004
3826
  unpublishFromBook,
3827
+ pinRegistrar,
3828
+ adoptQuarantinedIdentities,
3829
+ createPacket,
3005
3830
  reservedNames,
3006
3831
  provisionIdentity,
3007
3832
  saveState,
3833
+ saveStateFailClosed,
3008
3834
  withScope,
3009
3835
  withScopeAsync,
3010
3836
  readonlyTx,
@@ -3025,7 +3851,8 @@ export {
3025
3851
  decodeWireBin,
3026
3852
  deleteIdentityCompletely,
3027
3853
  closeTemporaryIdentity,
3028
- sweepStaleTempIdentities,
3854
+ sessionReaperIntervalMs,
3855
+ reapStaleTemporaryIdentities,
3029
3856
  envelopeDispatch,
3030
3857
  PROTOCOL_VERSION,
3031
3858
  clusterSweep,