@ours.network/cli 2.0.4 → 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.4" : "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
@@ -811,15 +1522,15 @@ function setRegistrar(id) {
811
1522
  function setRegistrarAdBlob(blob) {
812
1523
  registrarAdBlob = blob;
813
1524
  }
814
- var identityDir = (name) => join4(STATE_DIR, name);
815
- var keyPath = (dir) => join4(dir, "identity.key");
816
- var dataPath = (dir) => join4(dir, "state_data.bin");
817
- var notifyLogPath = (dir) => join4(dir, "notifications.log");
818
- var unreadPath = (dir) => join4(dir, "unread.json");
819
- 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");
820
1531
  var hashLeaseToken = (token) => createHash2("sha256").update(token).digest("hex");
821
1532
  function writeTempMetaFile(dir, meta) {
822
- fs4.writeFileSync(
1533
+ fs5.writeFileSync(
823
1534
  tempMetaPath(dir),
824
1535
  JSON.stringify({ v: 1, owner: { token_sha256: meta.owner.tokenHash, pid: meta.owner.pid }, created_at: meta.createdAt }),
825
1536
  { mode: 384 }
@@ -827,7 +1538,7 @@ function writeTempMetaFile(dir, meta) {
827
1538
  }
828
1539
  function readTempMetaFile(dir) {
829
1540
  try {
830
- const raw = JSON.parse(fs4.readFileSync(tempMetaPath(dir), "utf8"));
1541
+ const raw = JSON.parse(fs5.readFileSync(tempMetaPath(dir), "utf8"));
831
1542
  if (raw.v !== 1 || typeof raw.owner?.token_sha256 !== "string" || typeof raw.owner?.pid !== "number") return null;
832
1543
  return {
833
1544
  owner: { tokenHash: raw.owner.token_sha256, pid: raw.owner.pid },
@@ -839,33 +1550,33 @@ function readTempMetaFile(dir) {
839
1550
  }
840
1551
  function tightenIdentityPerms() {
841
1552
  for (const name of listPersistedNames()) {
842
- const dir = join4(STATE_DIR, name);
1553
+ const dir = join5(STATE_DIR, name);
843
1554
  try {
844
- fs4.chmodSync(dir, 448);
1555
+ fs5.chmodSync(dir, 448);
845
1556
  } catch (err) {
846
1557
  log(`[${name}] chmod 0700 failed:`, String(err));
847
1558
  }
848
1559
  for (const f of [dataPath(dir), keyPath(dir), historyPath({ dir }), `${historyPath({ dir })}-wal`, `${historyPath({ dir })}-shm`, notifyLogPath(dir), unreadPath(dir)]) {
849
- if (!fs4.existsSync(f)) continue;
1560
+ if (!fs5.existsSync(f)) continue;
850
1561
  try {
851
- fs4.chmodSync(f, 384);
1562
+ fs5.chmodSync(f, 384);
852
1563
  } catch (err) {
853
1564
  log(`[${name}] chmod 0600 ${f} failed:`, String(err));
854
1565
  }
855
1566
  }
856
- const blobs = join4(dir, "blobs");
857
- if (fs4.existsSync(blobs)) {
1567
+ const blobs = join5(dir, "blobs");
1568
+ if (fs5.existsSync(blobs)) {
858
1569
  const tightenTree = (path) => {
859
- for (const entry of fs4.readdirSync(path, { withFileTypes: true })) {
860
- const child = join4(path, entry.name);
1570
+ for (const entry of fs5.readdirSync(path, { withFileTypes: true })) {
1571
+ const child = join5(path, entry.name);
861
1572
  if (entry.isDirectory()) {
862
- fs4.chmodSync(child, 448);
1573
+ fs5.chmodSync(child, 448);
863
1574
  tightenTree(child);
864
- } else if (entry.isFile()) fs4.chmodSync(child, 384);
1575
+ } else if (entry.isFile()) fs5.chmodSync(child, 384);
865
1576
  }
866
1577
  };
867
1578
  try {
868
- fs4.chmodSync(blobs, 448);
1579
+ fs5.chmodSync(blobs, 448);
869
1580
  tightenTree(blobs);
870
1581
  } catch (err) {
871
1582
  log(`[${name}] chmod history blobs failed:`, String(err));
@@ -884,13 +1595,13 @@ function findIdentityFile(id, wireId) {
884
1595
  }
885
1596
  }
886
1597
  function listPersistedNames() {
887
- if (!fs4.existsSync(STATE_DIR)) return [];
888
- 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);
889
1600
  }
890
1601
 
891
1602
  // ../../src/identity/lease.ts
892
- import { join as join5 } from "node:path";
893
- import * as fs5 from "node:fs";
1603
+ import { join as join6 } from "node:path";
1604
+ import * as fs6 from "node:fs";
894
1605
  var leases = /* @__PURE__ */ new Map();
895
1606
  var tombstones = /* @__PURE__ */ new Set();
896
1607
  var sessionHeaders = /* @__PURE__ */ new Map();
@@ -908,17 +1619,17 @@ function leaseByToken(token) {
908
1619
  for (const l of leases.values()) if (l.token === token) return l;
909
1620
  return void 0;
910
1621
  }
911
- var bindingsSnapshotPath = () => join5(STATE_DIR, "bindings.json");
1622
+ var bindingsSnapshotPath = () => join6(STATE_DIR, "bindings.json");
912
1623
  function persistBindings() {
913
1624
  try {
914
- fs5.mkdirSync(STATE_DIR, { recursive: true });
1625
+ fs6.mkdirSync(STATE_DIR, { recursive: true });
915
1626
  const tmp = `${bindingsSnapshotPath()}.tmp`;
916
- fs5.writeFileSync(tmp, JSON.stringify({
1627
+ fs6.writeFileSync(tmp, JSON.stringify({
917
1628
  pid: process.pid,
918
1629
  bound: [...leases.keys()],
919
1630
  holders: [...leases.values()].map((l) => ({ identity: l.identity, pid: l.pid }))
920
1631
  }));
921
- fs5.renameSync(tmp, bindingsSnapshotPath());
1632
+ fs6.renameSync(tmp, bindingsSnapshotPath());
922
1633
  } catch (err) {
923
1634
  log("failed to persist bindings snapshot:", String(err));
924
1635
  }
@@ -964,19 +1675,19 @@ function bindSession(sid, name) {
964
1675
  }
965
1676
 
966
1677
  // ../../src/identity/hierarchy.ts
967
- import { join as join8 } from "node:path";
968
- import * as fs10 from "node:fs";
1678
+ import { join as join9 } from "node:path";
1679
+ import * as fs11 from "node:fs";
969
1680
 
970
1681
  // ../../src/mufl/tx.ts
971
1682
  import { AdaptObjectLifetime as AdaptObjectLifetime2 } from "@adapt-toolkit/sdk/common";
972
1683
  import { object_to_adapt_value } from "@adapt-toolkit/sdk/wrapper";
973
1684
 
974
1685
  // ../../src/state.ts
975
- import * as fs9 from "node:fs";
1686
+ import * as fs10 from "node:fs";
976
1687
  import { randomBytes as randomBytes2 } from "node:crypto";
977
1688
 
978
1689
  // ../../src/identity/provision.ts
979
- import * as fs8 from "node:fs";
1690
+ import * as fs9 from "node:fs";
980
1691
  import { randomBytes } from "node:crypto";
981
1692
  import { PacketWrapperConfigurator } from "@adapt-toolkit/sdk/wrappers";
982
1693
 
@@ -984,8 +1695,8 @@ import { PacketWrapperConfigurator } from "@adapt-toolkit/sdk/wrappers";
984
1695
  import { AdaptObjectLifetime } from "@adapt-toolkit/sdk/common";
985
1696
 
986
1697
  // ../../src/notify.ts
987
- import { join as join6 } from "node:path";
988
- import * as fs6 from "node:fs";
1698
+ import { join as join7 } from "node:path";
1699
+ import * as fs7 from "node:fs";
989
1700
 
990
1701
  // ../../src/events.ts
991
1702
  var nonEmptyString = (value) => typeof value === "string" && value.length > 0;
@@ -1109,7 +1820,7 @@ function fireNotifyWaiters(name) {
1109
1820
  }
1110
1821
  }
1111
1822
  function waitForNotify(name, ms) {
1112
- return new Promise((resolve3) => {
1823
+ return new Promise((resolve4) => {
1113
1824
  let set = notifyWaiters.get(name);
1114
1825
  if (!set) {
1115
1826
  set = /* @__PURE__ */ new Set();
@@ -1122,7 +1833,7 @@ function waitForNotify(name, ms) {
1122
1833
  set.delete(fn);
1123
1834
  if (set.size === 0) notifyWaiters.delete(name);
1124
1835
  clearTimeout(timer);
1125
- resolve3();
1836
+ resolve4();
1126
1837
  };
1127
1838
  const fn = finish;
1128
1839
  const timer = setTimeout(finish, ms);
@@ -1147,10 +1858,10 @@ function bodyFreeNotifyValue(value) {
1147
1858
  function appendNotifyLog(id, event) {
1148
1859
  const contentFree = bodyFreeNotifyValue(event);
1149
1860
  try {
1150
- fs6.mkdirSync(id.dir, { recursive: true, mode: 448 });
1861
+ fs7.mkdirSync(id.dir, { recursive: true, mode: 448 });
1151
1862
  const path = notifyLogPath(id.dir);
1152
- fs6.appendFileSync(path, JSON.stringify(contentFree) + "\n", { mode: 384 });
1153
- fs6.chmodSync(path, 384);
1863
+ fs7.appendFileSync(path, JSON.stringify(contentFree) + "\n", { mode: 384 });
1864
+ fs7.chmodSync(path, 384);
1154
1865
  } catch (err) {
1155
1866
  log(`[${id.name}] failed to append notifications.log:`, String(err));
1156
1867
  }
@@ -1165,7 +1876,7 @@ var NOTIFY_LONGPOLL_MS = Number(process.env.OURS_NOTIFY_LONGPOLL_MS) > 0 ? Numbe
1165
1876
  var NOTIFY_RECHECK_MS = 250;
1166
1877
  function notifyLogSize(logPath) {
1167
1878
  try {
1168
- return fs6.statSync(logPath).size;
1879
+ return fs7.statSync(logPath).size;
1169
1880
  } catch {
1170
1881
  return 0;
1171
1882
  }
@@ -1175,11 +1886,11 @@ function readNotifyRange(logPath, from, to) {
1175
1886
  const buf = Buffer.alloc(to - from);
1176
1887
  let read = 0;
1177
1888
  try {
1178
- const fd = fs6.openSync(logPath, "r");
1889
+ const fd = fs7.openSync(logPath, "r");
1179
1890
  try {
1180
- read = fs6.readSync(fd, buf, 0, buf.length, from);
1891
+ read = fs7.readSync(fd, buf, 0, buf.length, from);
1181
1892
  } finally {
1182
- fs6.closeSync(fd);
1893
+ fs7.closeSync(fd);
1183
1894
  }
1184
1895
  } catch {
1185
1896
  return { events: [], cursor: from };
@@ -1198,7 +1909,7 @@ function readNotifyRange(logPath, from, to) {
1198
1909
  return { events, cursor: from + lastNl + 1 };
1199
1910
  }
1200
1911
  async function serveNotifications(req, res, name, sinceParam, kindsParam = null) {
1201
- const logPath = notifyLogPath(join6(STATE_DIR, name));
1912
+ const logPath = notifyLogPath(join7(STATE_DIR, name));
1202
1913
  const send = (cursor, events) => {
1203
1914
  if (res.writableEnded) return;
1204
1915
  res.writeHead(200, { "Content-Type": "application/json" });
@@ -1269,11 +1980,11 @@ function refreshUnread(id) {
1269
1980
  files: unreadFiles.length,
1270
1981
  unread_files: unreadFiles.slice(-10)
1271
1982
  };
1272
- fs6.mkdirSync(id.dir, { recursive: true, mode: 448 });
1983
+ fs7.mkdirSync(id.dir, { recursive: true, mode: 448 });
1273
1984
  const tmp = `${unreadPath(id.dir)}.tmp`;
1274
- fs6.writeFileSync(tmp, JSON.stringify(snapshot), { mode: 384 });
1275
- fs6.chmodSync(tmp, 384);
1276
- 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));
1277
1988
  } catch (err) {
1278
1989
  log(`[${id.name}] failed to refresh unread snapshot:`, String(err));
1279
1990
  }
@@ -1282,7 +1993,7 @@ function unreadSummary() {
1282
1993
  const out = [];
1283
1994
  let entries = [];
1284
1995
  try {
1285
- entries = fs6.readdirSync(STATE_DIR, { withFileTypes: true });
1996
+ entries = fs7.readdirSync(STATE_DIR, { withFileTypes: true });
1286
1997
  } catch {
1287
1998
  return { identities: out };
1288
1999
  }
@@ -1290,7 +2001,7 @@ function unreadSummary() {
1290
2001
  if (!entry.isDirectory() || validateName(entry.name) !== null) continue;
1291
2002
  let value;
1292
2003
  try {
1293
- 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"));
1294
2005
  } catch {
1295
2006
  continue;
1296
2007
  }
@@ -1835,24 +2546,24 @@ function wireHandlers(id, hooks) {
1835
2546
  }
1836
2547
 
1837
2548
  // ../../src/book.ts
1838
- import { join as join7 } from "node:path";
1839
- import * as fs7 from "node:fs";
1840
- var bookDir = () => join7(STATE_DIR, BOOK_DIR_NAME);
1841
- var registrarKeyPath = () => join7(bookDir(), "registrar.key");
1842
- 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");
1843
2554
  function readBook() {
1844
2555
  try {
1845
- const parsed = JSON.parse(fs7.readFileSync(bookPath(), "utf8"));
2556
+ const parsed = JSON.parse(fs8.readFileSync(bookPath(), "utf8"));
1846
2557
  return parsed && typeof parsed.entries === "object" ? parsed.entries : {};
1847
2558
  } catch {
1848
2559
  return {};
1849
2560
  }
1850
2561
  }
1851
2562
  function writeBook(entries) {
1852
- fs7.mkdirSync(bookDir(), { recursive: true });
2563
+ fs8.mkdirSync(bookDir(), { recursive: true });
1853
2564
  const tmp = `${bookPath()}.tmp`;
1854
- fs7.writeFileSync(tmp, JSON.stringify({ v: 1, entries }, null, 2), { mode: 384 });
1855
- fs7.renameSync(tmp, bookPath());
2565
+ fs8.writeFileSync(tmp, JSON.stringify({ v: 1, entries }, null, 2), { mode: 384 });
2566
+ fs8.renameSync(tmp, bookPath());
1856
2567
  }
1857
2568
  function exportAdBlob(id) {
1858
2569
  return withScope(
@@ -2038,7 +2749,7 @@ async function provisionIdentity(name, opts = { exposeLocal: true, localAutoAcce
2038
2749
  let provisioned;
2039
2750
  try {
2040
2751
  const dir = identityDir(name);
2041
- fs8.mkdirSync(dir, { recursive: true, mode: 448 });
2752
+ fs9.mkdirSync(dir, { recursive: true, mode: 448 });
2042
2753
  let tempMeta;
2043
2754
  if (opts.temp) {
2044
2755
  tempMeta = { owner: { tokenHash: opts.temp.tokenHash, pid: opts.temp.pid }, createdAt: Date.now() };
@@ -2049,7 +2760,7 @@ async function provisionIdentity(name, opts = { exposeLocal: true, localAutoAcce
2049
2760
  provisioned = id;
2050
2761
  openHistory(id);
2051
2762
  if (tempMeta) id.temp = tempMeta;
2052
- fs8.writeFileSync(keyPath(dir), exportSigningSecret(id), { mode: 384 });
2763
+ fs9.writeFileSync(keyPath(dir), exportSigningSecret(id), { mode: 384 });
2053
2764
  await withScopeAsync(async (lt) => {
2054
2765
  await mutatingTx(id, "::a2a_messaging::set_my_name", { name }, lt);
2055
2766
  });
@@ -2072,7 +2783,7 @@ async function provisionIdentity(name, opts = { exposeLocal: true, localAutoAcce
2072
2783
  }
2073
2784
  async function restoreIdentity(name) {
2074
2785
  const dir = identityDir(name);
2075
- const secret = fs8.readFileSync(keyPath(dir), "utf8").trim();
2786
+ const secret = fs9.readFileSync(keyPath(dir), "utf8").trim();
2076
2787
  const id = await createPacket(name, "", dir, false, secret, true);
2077
2788
  log(`[${name}] created QUARANTINED (no routing/broker registration, not client-bindable) \u2014 importing state before exposure`);
2078
2789
  const holdMs = Number(process.env.OURS_TEST_RESTORE_HOLD_MS || "") || 0;
@@ -2097,7 +2808,7 @@ async function restoreIdentity(name) {
2097
2808
  if (process.env.OURS_TEST_FORCE_IMPORT_TIMEOUT === "1") {
2098
2809
  throw new Error("timed out waiting for the transaction result (forced by OURS_TEST_FORCE_IMPORT_TIMEOUT)");
2099
2810
  }
2100
- const buf = fs8.readFileSync(dataPath(dir));
2811
+ const buf = fs9.readFileSync(dataPath(dir));
2101
2812
  await withScopeAsync(async (lt) => {
2102
2813
  const adaptData = id.pw.packet.ParseValue(new Uint8Array(buf)).Attach(lt);
2103
2814
  const importTimeoutMs = Number(process.env.OURS_IMPORT_TIMEOUT_MS || "") || void 0;
@@ -2173,23 +2884,23 @@ async function restoreIdentity(name) {
2173
2884
 
2174
2885
  // ../../src/state.ts
2175
2886
  async function ensureRegistrar() {
2176
- fs9.mkdirSync(bookDir(), { recursive: true });
2887
+ fs10.mkdirSync(bookDir(), { recursive: true });
2177
2888
  let secret;
2178
2889
  try {
2179
- secret = fs9.readFileSync(registrarKeyPath(), "utf8").trim();
2890
+ secret = fs10.readFileSync(registrarKeyPath(), "utf8").trim();
2180
2891
  } catch {
2181
2892
  }
2182
2893
  const seed = randomBytes2(24).toString("hex");
2183
2894
  setRegistrar(await createPacket(BOOK_DIR_NAME, seed, bookDir(), false, secret));
2184
2895
  if (!secret) {
2185
- fs9.writeFileSync(registrarKeyPath(), exportSigningSecret(registrar), { mode: 384 });
2896
+ fs10.writeFileSync(registrarKeyPath(), exportSigningSecret(registrar), { mode: 384 });
2186
2897
  }
2187
2898
  setRegistrarAdBlob(exportAdBlob(registrar));
2188
2899
  log(`contact-book registrar ready (${registrar.cid})`);
2189
2900
  }
2190
2901
  function hasSavedState(dir) {
2191
2902
  try {
2192
- return fs9.existsSync(dataPath(dir)) && fs9.statSync(dataPath(dir)).size > 0;
2903
+ return fs10.existsSync(dataPath(dir)) && fs10.statSync(dataPath(dir)).size > 0;
2193
2904
  } catch {
2194
2905
  return false;
2195
2906
  }
@@ -2198,38 +2909,38 @@ function saveState(id) {
2198
2909
  const bytes = withScope(
2199
2910
  (lt) => Buffer.from(readonlyTx(id, "::actor::export_state", lt).Serialize())
2200
2911
  );
2201
- fs9.mkdirSync(id.dir, { recursive: true, mode: 448 });
2912
+ fs10.mkdirSync(id.dir, { recursive: true, mode: 448 });
2202
2913
  const final = dataPath(id.dir);
2203
2914
  const tmp = `${final}.tmp`;
2204
2915
  let fd;
2205
2916
  try {
2206
- fd = fs9.openSync(tmp, "w", 384);
2207
- fs9.fchmodSync(fd, 384);
2208
- fs9.writeFileSync(fd, bytes);
2209
- fs9.fsyncSync(fd);
2210
- 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);
2211
2922
  fd = void 0;
2212
- fs9.renameSync(tmp, final);
2213
- fs9.chmodSync(final, 384);
2923
+ fs10.renameSync(tmp, final);
2924
+ fs10.chmodSync(final, 384);
2214
2925
  try {
2215
- fs9.chmodSync(keyPath(id.dir), 384);
2926
+ fs10.chmodSync(keyPath(id.dir), 384);
2216
2927
  } catch {
2217
2928
  }
2218
- const dirFd = fs9.openSync(id.dir, "r");
2929
+ const dirFd = fs10.openSync(id.dir, "r");
2219
2930
  try {
2220
- fs9.fsyncSync(dirFd);
2931
+ fs10.fsyncSync(dirFd);
2221
2932
  } finally {
2222
- fs9.closeSync(dirFd);
2933
+ fs10.closeSync(dirFd);
2223
2934
  }
2224
2935
  } catch (err) {
2225
2936
  if (fd !== void 0) {
2226
2937
  try {
2227
- fs9.closeSync(fd);
2938
+ fs10.closeSync(fd);
2228
2939
  } catch {
2229
2940
  }
2230
2941
  }
2231
2942
  try {
2232
- fs9.rmSync(tmp, { force: true });
2943
+ fs10.rmSync(tmp, { force: true });
2233
2944
  } catch {
2234
2945
  }
2235
2946
  throw err;
@@ -2341,24 +3052,24 @@ function mutatingTx(id, name, targ, lt, timeoutMs) {
2341
3052
  function setRootName(name) {
2342
3053
  rootName = name;
2343
3054
  }
2344
- var rootMarkerPath = () => join8(STATE_DIR, "root.json");
3055
+ var rootMarkerPath = () => join9(STATE_DIR, "root.json");
2345
3056
  var rootName = null;
2346
3057
  function readRootMarker() {
2347
3058
  try {
2348
- const parsed = JSON.parse(fs10.readFileSync(rootMarkerPath(), "utf8"));
3059
+ const parsed = JSON.parse(fs11.readFileSync(rootMarkerPath(), "utf8"));
2349
3060
  return typeof parsed.name === "string" ? parsed.name : null;
2350
3061
  } catch {
2351
3062
  return null;
2352
3063
  }
2353
3064
  }
2354
3065
  function writeRootMarker(name) {
2355
- fs10.mkdirSync(STATE_DIR, { recursive: true });
3066
+ fs11.mkdirSync(STATE_DIR, { recursive: true });
2356
3067
  const tmp = `${rootMarkerPath()}.tmp`;
2357
- fs10.writeFileSync(tmp, JSON.stringify({ v: 1, name }));
2358
- fs10.renameSync(tmp, rootMarkerPath());
3068
+ fs11.writeFileSync(tmp, JSON.stringify({ v: 1, name }));
3069
+ fs11.renameSync(tmp, rootMarkerPath());
2359
3070
  }
2360
3071
  function clearRootMarker() {
2361
- fs10.rmSync(rootMarkerPath(), { force: true });
3072
+ fs11.rmSync(rootMarkerPath(), { force: true });
2362
3073
  }
2363
3074
  function describeIdentity(id) {
2364
3075
  return withScope((lt) => {
@@ -2423,11 +3134,11 @@ async function establishRoot(id) {
2423
3134
  }
2424
3135
 
2425
3136
  // ../../src/identity/reaper.ts
2426
- import { join as join9 } from "node:path";
2427
- import * as fs12 from "node:fs";
3137
+ import { join as join10 } from "node:path";
3138
+ import * as fs13 from "node:fs";
2428
3139
 
2429
3140
  // ../../src/identity/lifecycle.ts
2430
- import * as fs11 from "node:fs";
3141
+ import * as fs12 from "node:fs";
2431
3142
 
2432
3143
  // ../../src/render/adapt-to-json.ts
2433
3144
  import { brotliCompressSync, brotliDecompressSync } from "node:zlib";
@@ -2752,7 +3463,7 @@ function deleteIdentityCompletely(id) {
2752
3463
  clearRootMarker();
2753
3464
  }
2754
3465
  try {
2755
- fs11.rmSync(id.dir, { recursive: true, force: true });
3466
+ fs12.rmSync(id.dir, { recursive: true, force: true });
2756
3467
  reservedNames.delete(id.name);
2757
3468
  } catch (err) {
2758
3469
  return `deleting ${id.dir} failed: ${String(err)}`;
@@ -2832,14 +3543,14 @@ async function reapStaleTemporaryIdentities() {
2832
3543
  await Promise.all(closes);
2833
3544
  }
2834
3545
  async function reapOrphanTemporaryDirectories() {
2835
- if (!fs12.existsSync(STATE_DIR)) return;
2836
- for (const d of fs12.readdirSync(STATE_DIR, { withFileTypes: true })) {
3546
+ if (!fs13.existsSync(STATE_DIR)) return;
3547
+ for (const d of fs13.readdirSync(STATE_DIR, { withFileTypes: true })) {
2837
3548
  if (!d.isDirectory() || identities.has(d.name) || quarantinedIdentities.has(d.name)) continue;
2838
- const dir = join9(STATE_DIR, d.name);
3549
+ const dir = join10(STATE_DIR, d.name);
2839
3550
  const meta = readTempMetaFile(dir);
2840
3551
  if (!meta || !pidDefinitelyDead(meta.owner.pid)) continue;
2841
3552
  try {
2842
- fs12.rmSync(dir, { recursive: true, force: true });
3553
+ fs13.rmSync(dir, { recursive: true, force: true });
2843
3554
  reservedNames.delete(d.name);
2844
3555
  log(`[${d.name}] removed orphaned temporary-identity dir (owner pid ${meta.owner.pid} confirmed dead)`);
2845
3556
  } catch (err) {
@@ -3053,11 +3764,18 @@ export {
3053
3764
  BROKER_URL,
3054
3765
  PORT,
3055
3766
  GC_INTERVAL_MS,
3767
+ HISTORY_MAX_BYTES,
3056
3768
  API_VISIBILITY,
3057
3769
  startupProgress,
3058
3770
  log,
3059
3771
  requireAuth,
3060
3772
  validateName,
3773
+ initializeHistoryRetention,
3774
+ noteHistoryStorageMutation,
3775
+ getHistoryStorageStatus,
3776
+ stopHistoryRetention,
3777
+ pinHistoryItems,
3778
+ unpinHistoryItems,
3061
3779
  consumeOutboundHistoryFailure,
3062
3780
  openHistory,
3063
3781
  closeHistory,