@llmnesia/mcp 0.2.4 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -151,15 +151,24 @@ dropped to save space.
151
151
  Current builds discard a clean browser-to-corpus transport payload after both
152
152
  the canonical conversation and active index have accepted it. Malformed input
153
153
  is kept under `inbox/quarantine/` for inspection. Older releases archived every
154
- successful payload and may also have left versioned indexes behind. Preview the
155
- exact recoverable space without writing anything:
154
+ successful payload and may also have left versioned indexes behind.
155
+
156
+ No maintenance command is required. After an upgrade, the MCP service waits
157
+ through a three-day compatibility window, then reclaims the historical files
158
+ automatically while the service is ready and idle. Work is single-process,
159
+ resumable, yields between small batches, and retries later without interrupting
160
+ search if any verification or filesystem step fails. Quarantine is never
161
+ removed. Set `LLMNESIA_NO_AUTO_MAINTENANCE=1` only if you need to opt out.
162
+
163
+ For support or an immediate attended cleanup, preview the exact recoverable
164
+ space without writing anything:
156
165
 
157
166
  ```bash
158
167
  npx -y @llmnesia/mcp@latest compact --dry-run
159
168
  ```
160
169
 
161
- To apply it, fully quit connected desktop AI/MCP clients, then run `compact`
162
- without `--dry-run`. It asks for confirmation, verifies every compressed
170
+ To apply it immediately, fully quit connected desktop AI/MCP clients, then run
171
+ `compact` without `--dry-run`. It asks for confirmation, verifies every compressed
163
172
  conversation by reading it back before removing the legacy JSON, builds the
164
173
  current index before deleting older derived versions, and never removes
165
174
  quarantined input.
@@ -38,7 +38,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
38
38
  ));
39
39
 
40
40
  // src/version.ts
41
- var VERSION = true ? "0.2.4" : "0.0.0-dev";
41
+ var VERSION = true ? "0.2.5" : "0.0.0-dev";
42
42
 
43
43
  export {
44
44
  __require,
@@ -3,7 +3,7 @@ import { createRequire as __llmnesiaCreateRequire } from "node:module";
3
3
  const require = __llmnesiaCreateRequire(import.meta.url);
4
4
  import {
5
5
  VERSION
6
- } from "./chunk-JSNSTAFD.js";
6
+ } from "./chunk-BDR4JVMF.js";
7
7
 
8
8
  // src/config.ts
9
9
  import { readFileSync } from "fs";
@@ -63,6 +63,14 @@ var CorpusPaths = class {
63
63
  get metaFile() {
64
64
  return join2(this.root, "meta.json");
65
65
  }
66
+ /** Content-free state for automatic storage maintenance. */
67
+ get maintenanceStateFile() {
68
+ return join2(this.root, "maintenance-v1.json");
69
+ }
70
+ /** Cross-process lease preventing several MCP clients compacting together. */
71
+ get maintenanceLockFile() {
72
+ return join2(this.root, ".maintenance-v1.lock");
73
+ }
66
74
  get inboxDir() {
67
75
  return join2(this.root, "inbox");
68
76
  }
@@ -228,6 +236,8 @@ var DIR_MODE = 448;
228
236
  var FILE_MODE = 384;
229
237
  var gzip = promisify(gzipCallback);
230
238
  var gunzip = promisify(gunzipCallback);
239
+ var MAINTENANCE_WRITE_WAIT_MS = 2e3;
240
+ var MAINTENANCE_WRITE_POLL_MS = 50;
231
241
  var Corpus = class {
232
242
  paths;
233
243
  // Serialize writes within this process so concurrent upserts (inbox drain +
@@ -291,6 +301,7 @@ var Corpus = class {
291
301
  */
292
302
  async upsert(record) {
293
303
  return this.enqueueWrite(async () => {
304
+ await waitForMaintenanceLease(this.paths.maintenanceLockFile);
294
305
  const canonical = canonicalizeConversation(record);
295
306
  const { conversation } = canonical;
296
307
  const file = this.paths.compressedConversationFile(conversation.platform, conversation.docId);
@@ -473,6 +484,7 @@ var Corpus = class {
473
484
  */
474
485
  async migrateLegacyConversations(options = {}) {
475
486
  const dryRun = options.dryRun === true;
487
+ const yieldEvery = Math.max(0, Math.floor(options.yieldEvery ?? 0));
476
488
  const before = await this.conversationStorageStats();
477
489
  const report = {
478
490
  ...before,
@@ -495,18 +507,22 @@ var Corpus = class {
495
507
  const legacyFile = join3(dir, name);
496
508
  const compressedFile = join3(dir, `${name}.gz`);
497
509
  try {
498
- const legacy = await readPlainConversationIfExists(legacyFile);
499
- if (!legacy) throw new Error("legacy JSON could not be parsed");
500
- const compressed = await readCompressedConversationIfExists(compressedFile);
501
- const winner = pickNewerConversation(compressed, legacy) ?? legacy;
502
- const serialized = serializeConversation(canonicalizeConversation(winner));
503
- const compressedBytes = await gzip(Buffer.from(serialized, "utf8"), { level: 9 });
504
- report.projectedCompressedBytes -= await fileSizeIfExists(compressedFile);
505
- report.projectedCompressedBytes += compressedBytes.byteLength;
506
- if (!dryRun) {
507
- await writeVerifiedCompressedConversationAtomic(compressedFile, compressedBytes, serialized);
508
- await fs.rm(legacyFile);
509
- }
510
+ const migrateOne = async () => {
511
+ const legacy = await readPlainConversationIfExists(legacyFile);
512
+ if (!legacy) throw new Error("legacy JSON could not be parsed");
513
+ const compressed = await readCompressedConversationIfExists(compressedFile);
514
+ const winner = pickNewerConversation(compressed, legacy) ?? legacy;
515
+ const serialized = serializeConversation(canonicalizeConversation(winner));
516
+ const compressedBytes = await gzip(Buffer.from(serialized, "utf8"), { level: 9 });
517
+ report.projectedCompressedBytes -= await fileSizeIfExists(compressedFile);
518
+ report.projectedCompressedBytes += compressedBytes.byteLength;
519
+ if (!dryRun) {
520
+ await writeVerifiedCompressedConversationAtomic(compressedFile, compressedBytes, serialized);
521
+ await fs.rm(legacyFile);
522
+ }
523
+ };
524
+ if (dryRun) await migrateOne();
525
+ else await this.enqueueWrite(migrateOne);
510
526
  report.migrated += 1;
511
527
  report.legacyRemoved += 1;
512
528
  } catch (error) {
@@ -515,6 +531,9 @@ var Corpus = class {
515
531
  error: error instanceof Error ? error.message : String(error)
516
532
  });
517
533
  }
534
+ if (yieldEvery > 0 && (report.migrated + report.failures.length) % yieldEvery === 0) {
535
+ await yieldToEventLoop();
536
+ }
518
537
  }
519
538
  }
520
539
  if (!dryRun && report.failures.length === 0) {
@@ -558,6 +577,23 @@ var Corpus = class {
558
577
  );
559
578
  }
560
579
  };
580
+ function yieldToEventLoop() {
581
+ return new Promise((resolve2) => setImmediate(resolve2));
582
+ }
583
+ async function waitForMaintenanceLease(file) {
584
+ const deadline = Date.now() + MAINTENANCE_WRITE_WAIT_MS;
585
+ while (true) {
586
+ try {
587
+ await fs.access(file);
588
+ } catch {
589
+ return;
590
+ }
591
+ if (Date.now() >= deadline) {
592
+ throw new Error("automatic corpus maintenance is in progress; retry this write shortly");
593
+ }
594
+ await new Promise((resolve2) => setTimeout(resolve2, MAINTENANCE_WRITE_POLL_MS));
595
+ }
596
+ }
561
597
  async function readJsonIfExists(file) {
562
598
  try {
563
599
  const raw = await fs.readFile(file, "utf8");
@@ -1359,43 +1395,421 @@ function normalizeMatchRole(role) {
1359
1395
  return role === "assistant" || role === "user" || role === "system" ? role : "unknown";
1360
1396
  }
1361
1397
 
1398
+ // src/storageMaintenance.ts
1399
+ import { promises as fs4 } from "fs";
1400
+ import { join as join5 } from "path";
1401
+ async function inspectCorpusStorage(corpus) {
1402
+ const conversations = await corpus.conversationStorageStats();
1403
+ const processed = await directoryFiles(corpus.paths.processedDir);
1404
+ const quarantine = await directoryFiles(corpus.paths.quarantineDir);
1405
+ const activeNames = [
1406
+ corpus.paths.indexDbFile,
1407
+ `${corpus.paths.indexDbFile}-wal`,
1408
+ `${corpus.paths.indexDbFile}-shm`
1409
+ ];
1410
+ const activeIndex = await explicitFiles(activeNames);
1411
+ const staleNames = await staleIndexFiles(corpus.paths.indexDir);
1412
+ const stalePaths = staleNames.flatMap((name) => {
1413
+ const base = join5(corpus.paths.indexDir, name);
1414
+ return [base, `${base}-wal`, `${base}-shm`];
1415
+ });
1416
+ const staleIndexes = await explicitFiles(stalePaths);
1417
+ const root = await directoryFiles(corpus.paths.root);
1418
+ const classifiedBytes = conversations.legacyBytes + conversations.compressedBytes + processed.bytes + quarantine.bytes + activeIndex.bytes + staleIndexes.bytes;
1419
+ const classifiedFiles = conversations.legacyFiles + conversations.compressedFiles + processed.files + quarantine.files + activeIndex.files + staleIndexes.files;
1420
+ const other = {
1421
+ files: Math.max(0, root.files - classifiedFiles),
1422
+ bytes: Math.max(0, root.bytes - classifiedBytes)
1423
+ };
1424
+ return {
1425
+ root: corpus.paths.root,
1426
+ conversations,
1427
+ processed,
1428
+ quarantine,
1429
+ activeIndex,
1430
+ staleIndexes,
1431
+ other,
1432
+ totalBytes: root.bytes,
1433
+ immediatelyReclaimableBytes: processed.bytes + staleIndexes.bytes
1434
+ };
1435
+ }
1436
+ async function compactCorpus(corpus, options = {}) {
1437
+ const dryRun = options.dryRun === true;
1438
+ const before = await inspectCorpusStorage(corpus);
1439
+ if (dryRun) {
1440
+ const compression = await corpus.migrateLegacyConversations({ dryRun: true });
1441
+ const compressionSavings = Math.max(
1442
+ 0,
1443
+ compression.legacyBytes + compression.compressedBytes - compression.projectedCompressedBytes
1444
+ );
1445
+ return {
1446
+ dryRun: true,
1447
+ before,
1448
+ compression,
1449
+ processedRemoved: before.processed,
1450
+ staleIndexesRemoved: before.staleIndexes,
1451
+ after: null,
1452
+ reclaimedBytes: before.immediatelyReclaimableBytes + compressionSavings
1453
+ };
1454
+ }
1455
+ await corpus.init();
1456
+ const processedRemoved = await removeDirectoryFiles(corpus.paths.processedDir);
1457
+ const index = options.index ?? await openSearchIndex(corpus);
1458
+ try {
1459
+ index.compact();
1460
+ } finally {
1461
+ if (!options.index) index.close();
1462
+ }
1463
+ const appliedCompression = await corpus.migrateLegacyConversations({
1464
+ yieldEvery: options.yieldEvery
1465
+ });
1466
+ if (appliedCompression.failures.length > 0) {
1467
+ throw new Error(
1468
+ `Corpus compression left ${appliedCompression.failures.length} legacy file(s) untouched; stale indexes were preserved. Run compact again after fixing the reported files.`
1469
+ );
1470
+ }
1471
+ const staleIndexesRemoved = await removeStaleIndexes(corpus);
1472
+ const after = await inspectCorpusStorage(corpus);
1473
+ return {
1474
+ dryRun: false,
1475
+ before,
1476
+ compression: appliedCompression,
1477
+ processedRemoved,
1478
+ staleIndexesRemoved,
1479
+ after,
1480
+ reclaimedBytes: Math.max(0, before.totalBytes - after.totalBytes)
1481
+ };
1482
+ }
1483
+ async function removeStaleIndexes(corpus) {
1484
+ const names = await staleIndexFiles(corpus.paths.indexDir);
1485
+ const paths = names.flatMap((name) => {
1486
+ const base = join5(corpus.paths.indexDir, name);
1487
+ return [base, `${base}-wal`, `${base}-shm`];
1488
+ });
1489
+ const before = await explicitFiles(paths);
1490
+ for (const name of names) {
1491
+ await deleteIndexDb(join5(corpus.paths.indexDir, name));
1492
+ }
1493
+ return before;
1494
+ }
1495
+ async function removeDirectoryFiles(dir) {
1496
+ const before = await directoryFiles(dir);
1497
+ await removeFilesRecursively(dir);
1498
+ return before;
1499
+ }
1500
+ async function removeFilesRecursively(dir) {
1501
+ let entries;
1502
+ try {
1503
+ entries = await fs4.readdir(dir, { withFileTypes: true });
1504
+ } catch {
1505
+ return;
1506
+ }
1507
+ for (const entry of entries) {
1508
+ const path = join5(dir, entry.name);
1509
+ if (entry.isDirectory()) await removeFilesRecursively(path);
1510
+ else if (entry.isFile()) await fs4.rm(path, { force: true });
1511
+ }
1512
+ }
1513
+ async function explicitFiles(files) {
1514
+ let count = 0;
1515
+ let bytes = 0;
1516
+ for (const file of files) {
1517
+ try {
1518
+ const stat = await fs4.stat(file);
1519
+ if (stat.isFile()) {
1520
+ count += 1;
1521
+ bytes += stat.size;
1522
+ }
1523
+ } catch {
1524
+ }
1525
+ }
1526
+ return { files: count, bytes };
1527
+ }
1528
+ async function directoryFiles(dir) {
1529
+ let entries;
1530
+ try {
1531
+ entries = await fs4.readdir(dir, { withFileTypes: true });
1532
+ } catch {
1533
+ return { files: 0, bytes: 0 };
1534
+ }
1535
+ let files = 0;
1536
+ let bytes = 0;
1537
+ for (const entry of entries) {
1538
+ const path = join5(dir, entry.name);
1539
+ if (entry.isDirectory()) {
1540
+ const nested = await directoryFiles(path);
1541
+ files += nested.files;
1542
+ bytes += nested.bytes;
1543
+ } else if (entry.isFile()) {
1544
+ files += 1;
1545
+ bytes += (await fs4.stat(path)).size;
1546
+ }
1547
+ }
1548
+ return { files, bytes };
1549
+ }
1550
+
1551
+ // src/autoMaintenance.ts
1552
+ import { constants as fsConstants, promises as fs5 } from "fs";
1553
+ import { dirname as dirname2 } from "path";
1554
+ var FILE_MODE2 = 384;
1555
+ var STATE_SCHEMA_VERSION = 1;
1556
+ var FORMAT_KEY = `corpus-${CORPUS_FORMAT_VERSION}-index-${INDEX_FORMAT_VERSION}`;
1557
+ var AUTO_MAINTENANCE_GRACE_MS = 3 * 24 * 60 * 60 * 1e3;
1558
+ var AUTO_MAINTENANCE_RECHECK_MS = 7 * 24 * 60 * 60 * 1e3;
1559
+ var AUTO_MAINTENANCE_RETRY_MS = 24 * 60 * 60 * 1e3;
1560
+ var AUTO_MAINTENANCE_LOCK_STALE_MS = 6 * 60 * 60 * 1e3;
1561
+ var AUTO_MAINTENANCE_YIELD_EVERY = 25;
1562
+ var AUTO_MAINTENANCE_SETTLE_MS = 1e3;
1563
+ function isAutomaticMaintenanceDisabled(env = process.env) {
1564
+ const raw = env.LLMNESIA_NO_AUTO_MAINTENANCE;
1565
+ return raw === "1" || raw === "true";
1566
+ }
1567
+ async function runAutomaticMaintenance(corpus, index, options = {}) {
1568
+ const env = options.env ?? process.env;
1569
+ if (isAutomaticMaintenanceDisabled(env)) return { action: "disabled" };
1570
+ await corpus.init();
1571
+ const now = options.now ?? Date.now;
1572
+ const at = now();
1573
+ const graceMs = options.graceMs ?? AUTO_MAINTENANCE_GRACE_MS;
1574
+ const recheckMs = options.recheckMs ?? AUTO_MAINTENANCE_RECHECK_MS;
1575
+ const retryMs = options.retryMs ?? AUTO_MAINTENANCE_RETRY_MS;
1576
+ const state = await readState(corpus);
1577
+ if (state?.formatKey === FORMAT_KEY && state.status === "complete" && state.completedAt !== void 0 && at - state.completedAt < recheckMs) {
1578
+ return { action: "recently-checked", completedAt: state.completedAt };
1579
+ }
1580
+ if (state?.formatKey === FORMAT_KEY && state.nextEligibleAt !== void 0 && at < state.nextEligibleAt) {
1581
+ return { action: "deferred", until: state.nextEligibleAt };
1582
+ }
1583
+ const before = await inspectCorpusStorage(corpus);
1584
+ if (!hasWork(before)) {
1585
+ await writeState(corpus, {
1586
+ schemaVersion: STATE_SCHEMA_VERSION,
1587
+ formatKey: FORMAT_KEY,
1588
+ status: "complete",
1589
+ firstSeenAt: state?.formatKey === FORMAT_KEY ? state.firstSeenAt : at,
1590
+ completedAt: at,
1591
+ beforeBytes: before.totalBytes,
1592
+ afterBytes: before.totalBytes,
1593
+ reclaimedBytes: 0
1594
+ });
1595
+ return { action: "no-work" };
1596
+ }
1597
+ const firstSeenAt = state?.formatKey === FORMAT_KEY ? state.firstSeenAt : at;
1598
+ const eligibleAt = firstSeenAt + Math.max(0, graceMs);
1599
+ if (at < eligibleAt) {
1600
+ await writeState(corpus, {
1601
+ schemaVersion: STATE_SCHEMA_VERSION,
1602
+ formatKey: FORMAT_KEY,
1603
+ status: "waiting",
1604
+ firstSeenAt,
1605
+ nextEligibleAt: eligibleAt,
1606
+ beforeBytes: before.totalBytes
1607
+ });
1608
+ return { action: "deferred", until: eligibleAt };
1609
+ }
1610
+ const lease = await acquireLease(
1611
+ corpus,
1612
+ at,
1613
+ options.lockStaleMs ?? AUTO_MAINTENANCE_LOCK_STALE_MS
1614
+ );
1615
+ if (!lease) return { action: "locked" };
1616
+ try {
1617
+ await delay(options.settleMs ?? AUTO_MAINTENANCE_SETTLE_MS);
1618
+ const lockedBefore = await inspectCorpusStorage(corpus);
1619
+ if (!hasWork(lockedBefore)) {
1620
+ await writeState(corpus, {
1621
+ schemaVersion: STATE_SCHEMA_VERSION,
1622
+ formatKey: FORMAT_KEY,
1623
+ status: "complete",
1624
+ firstSeenAt,
1625
+ completedAt: now(),
1626
+ beforeBytes: lockedBefore.totalBytes,
1627
+ afterBytes: lockedBefore.totalBytes,
1628
+ reclaimedBytes: 0
1629
+ });
1630
+ return { action: "no-work" };
1631
+ }
1632
+ await writeState(corpus, {
1633
+ schemaVersion: STATE_SCHEMA_VERSION,
1634
+ formatKey: FORMAT_KEY,
1635
+ status: "running",
1636
+ firstSeenAt,
1637
+ lastAttemptAt: at,
1638
+ beforeBytes: lockedBefore.totalBytes
1639
+ });
1640
+ try {
1641
+ const report = await compactCorpus(corpus, {
1642
+ index,
1643
+ yieldEvery: options.yieldEvery ?? AUTO_MAINTENANCE_YIELD_EVERY
1644
+ });
1645
+ const completedAt = now();
1646
+ await writeState(corpus, {
1647
+ schemaVersion: STATE_SCHEMA_VERSION,
1648
+ formatKey: FORMAT_KEY,
1649
+ status: "complete",
1650
+ firstSeenAt,
1651
+ lastAttemptAt: at,
1652
+ completedAt,
1653
+ beforeBytes: report.before.totalBytes,
1654
+ afterBytes: report.after?.totalBytes ?? report.before.totalBytes,
1655
+ reclaimedBytes: report.reclaimedBytes
1656
+ });
1657
+ return { action: "compacted", report };
1658
+ } catch (error) {
1659
+ const message = error instanceof Error ? error.message : String(error);
1660
+ const retryAt = now() + Math.max(0, retryMs);
1661
+ await writeState(corpus, {
1662
+ schemaVersion: STATE_SCHEMA_VERSION,
1663
+ formatKey: FORMAT_KEY,
1664
+ status: "failed",
1665
+ firstSeenAt,
1666
+ lastAttemptAt: at,
1667
+ nextEligibleAt: retryAt,
1668
+ beforeBytes: lockedBefore.totalBytes,
1669
+ lastError: message
1670
+ });
1671
+ return { action: "failed", error: message, retryAt };
1672
+ }
1673
+ } finally {
1674
+ await lease.release();
1675
+ }
1676
+ }
1677
+ async function readAutomaticMaintenanceState(corpus) {
1678
+ return readState(corpus);
1679
+ }
1680
+ function hasWork(report) {
1681
+ return report.conversations.legacyFiles > 0 || report.processed.files > 0 || report.staleIndexes.files > 0;
1682
+ }
1683
+ async function readState(corpus) {
1684
+ try {
1685
+ const parsed = JSON.parse(
1686
+ await fs5.readFile(corpus.paths.maintenanceStateFile, "utf8")
1687
+ );
1688
+ return parsed?.schemaVersion === STATE_SCHEMA_VERSION ? parsed : null;
1689
+ } catch {
1690
+ return null;
1691
+ }
1692
+ }
1693
+ async function writeState(corpus, state) {
1694
+ const file = corpus.paths.maintenanceStateFile;
1695
+ const temp = `${file}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
1696
+ await fs5.mkdir(dirname2(file), { recursive: true });
1697
+ try {
1698
+ const handle = await fs5.open(
1699
+ temp,
1700
+ fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_WRONLY,
1701
+ FILE_MODE2
1702
+ );
1703
+ try {
1704
+ await handle.writeFile(`${JSON.stringify(state, null, 2)}
1705
+ `, "utf8");
1706
+ } finally {
1707
+ await handle.close();
1708
+ }
1709
+ await fs5.rename(temp, file);
1710
+ } finally {
1711
+ await fs5.rm(temp, { force: true }).catch(() => {
1712
+ });
1713
+ }
1714
+ }
1715
+ async function acquireLease(corpus, now, staleMs) {
1716
+ const file = corpus.paths.maintenanceLockFile;
1717
+ const token = `${process.pid}-${now}-${Math.random().toString(16).slice(2)}`;
1718
+ for (let attempt = 0; attempt < 2; attempt += 1) {
1719
+ try {
1720
+ const handle = await fs5.open(file, "wx", FILE_MODE2);
1721
+ try {
1722
+ await handle.writeFile(`${JSON.stringify({ token, pid: process.pid, startedAt: now })}
1723
+ `, "utf8");
1724
+ } finally {
1725
+ await handle.close();
1726
+ }
1727
+ return {
1728
+ token,
1729
+ release: async () => {
1730
+ try {
1731
+ const current = JSON.parse(await fs5.readFile(file, "utf8"));
1732
+ if (current.token === token) await fs5.rm(file, { force: true });
1733
+ } catch {
1734
+ }
1735
+ }
1736
+ };
1737
+ } catch (error) {
1738
+ if (!isAlreadyExists(error)) throw error;
1739
+ const existing = await readLease(file);
1740
+ const alive = existing?.pid ? isProcessAlive(existing.pid) : false;
1741
+ const age = existing?.startedAt ? now - existing.startedAt : Number.POSITIVE_INFINITY;
1742
+ if (alive && age < staleMs) return null;
1743
+ const stale = `${file}.stale-${token}`;
1744
+ try {
1745
+ await fs5.rename(file, stale);
1746
+ await fs5.rm(stale, { force: true });
1747
+ } catch {
1748
+ return null;
1749
+ }
1750
+ }
1751
+ }
1752
+ return null;
1753
+ }
1754
+ async function readLease(file) {
1755
+ try {
1756
+ return JSON.parse(await fs5.readFile(file, "utf8"));
1757
+ } catch {
1758
+ return null;
1759
+ }
1760
+ }
1761
+ function isProcessAlive(pid) {
1762
+ try {
1763
+ process.kill(pid, 0);
1764
+ return true;
1765
+ } catch (error) {
1766
+ return error.code === "EPERM";
1767
+ }
1768
+ }
1769
+ function isAlreadyExists(error) {
1770
+ return error.code === "EEXIST";
1771
+ }
1772
+ function delay(ms) {
1773
+ return ms > 0 ? new Promise((resolve2) => setTimeout(resolve2, ms)) : Promise.resolve();
1774
+ }
1775
+
1362
1776
  // src/runtimeInstall.ts
1363
1777
  import { cp, mkdir, readdir as readdir2, rename, rm, writeFile } from "fs/promises";
1364
1778
  import { existsSync } from "fs";
1365
1779
  import { homedir as homedir2 } from "os";
1366
- import { dirname as dirname2, join as join5 } from "path";
1780
+ import { dirname as dirname3, join as join6 } from "path";
1367
1781
  import { fileURLToPath } from "url";
1368
1782
  function defaultSourceDir() {
1369
- return dirname2(fileURLToPath(import.meta.url));
1783
+ return dirname3(fileURLToPath(import.meta.url));
1370
1784
  }
1371
1785
  function runtimeBinDir(home = homedir2()) {
1372
- return join5(home, ".llmnesia", "bin");
1786
+ return join6(home, ".llmnesia", "bin");
1373
1787
  }
1374
1788
  async function installRuntime(options = {}) {
1375
1789
  const home = options.home ?? homedir2();
1376
1790
  const sourceDir = options.sourceDir ?? defaultSourceDir();
1377
1791
  const nodePath = options.nodePath ?? process.execPath;
1378
1792
  const binDir = runtimeBinDir(home);
1379
- const sourceEntry = join5(sourceDir, "cli.js");
1793
+ const sourceEntry = join6(sourceDir, "cli.js");
1380
1794
  if (!existsSync(sourceEntry)) {
1381
1795
  throw new Error(
1382
1796
  `Cannot locate the built runtime: no cli.js in ${sourceDir}. Run this from an installed package (npx -y @llmnesia/mcp@latest install) or build first (npm run build).`
1383
1797
  );
1384
1798
  }
1385
- const parentDir = dirname2(binDir);
1799
+ const parentDir = dirname3(binDir);
1386
1800
  const nonce = `${process.pid}-${Date.now()}`;
1387
- const stagingDir = join5(parentDir, `.bin-install-${nonce}`);
1388
- const backupDir = join5(parentDir, `.bin-backup-${nonce}`);
1801
+ const stagingDir = join6(parentDir, `.bin-install-${nonce}`);
1802
+ const backupDir = join6(parentDir, `.bin-backup-${nonce}`);
1389
1803
  await mkdir(parentDir, { recursive: true });
1390
1804
  await rm(stagingDir, { recursive: true, force: true });
1391
1805
  await rm(backupDir, { recursive: true, force: true });
1392
1806
  await mkdir(stagingDir, { recursive: true });
1393
1807
  try {
1394
1808
  for (const name of await readdir2(sourceDir)) {
1395
- await cp(join5(sourceDir, name), join5(stagingDir, name), { recursive: true });
1809
+ await cp(join6(sourceDir, name), join6(stagingDir, name), { recursive: true });
1396
1810
  }
1397
1811
  await writeFile(
1398
- join5(stagingDir, "package.json"),
1812
+ join6(stagingDir, "package.json"),
1399
1813
  `${JSON.stringify(
1400
1814
  {
1401
1815
  name: "llmnesia-mcp-runtime",
@@ -1429,7 +1843,7 @@ async function installRuntime(options = {}) {
1429
1843
  } finally {
1430
1844
  await rm(stagingDir, { recursive: true, force: true });
1431
1845
  }
1432
- const entry = join5(binDir, "cli.js");
1846
+ const entry = join6(binDir, "cli.js");
1433
1847
  return {
1434
1848
  binDir,
1435
1849
  entry,
@@ -1450,7 +1864,7 @@ async function uninstallRuntime(home = homedir2()) {
1450
1864
  // src/selfUpdate.ts
1451
1865
  import { spawn } from "child_process";
1452
1866
  import { mkdtemp, readFile, rm as rm2, writeFile as writeFile2 } from "fs/promises";
1453
- import { dirname as dirname3, join as join6 } from "path";
1867
+ import { dirname as dirname4, join as join7 } from "path";
1454
1868
  import { homedir as homedir3, tmpdir } from "os";
1455
1869
  import { fileURLToPath as fileURLToPath2 } from "url";
1456
1870
  var PACKAGE_NAME = "@llmnesia/mcp";
@@ -1460,9 +1874,9 @@ var REGISTRY_TIMEOUT_MS = 5e3;
1460
1874
  var INSTALL_TIMEOUT_MS = 12e4;
1461
1875
  var UPDATE_CHECK_DELAY_MS = 3e4;
1462
1876
  function stateFile(home) {
1463
- return join6(home, ".llmnesia", "update-state.json");
1877
+ return join7(home, ".llmnesia", "update-state.json");
1464
1878
  }
1465
- async function readState(home) {
1879
+ async function readState2(home) {
1466
1880
  try {
1467
1881
  const parsed = JSON.parse(await readFile(stateFile(home), "utf8"));
1468
1882
  return typeof parsed === "object" && parsed !== null ? parsed : {};
@@ -1470,7 +1884,7 @@ async function readState(home) {
1470
1884
  return {};
1471
1885
  }
1472
1886
  }
1473
- async function writeState(home, state) {
1887
+ async function writeState2(home, state) {
1474
1888
  try {
1475
1889
  await writeFile2(stateFile(home), `${JSON.stringify(state, null, 2)}
1476
1890
  `, "utf8");
@@ -1522,11 +1936,11 @@ async function fetchLatestFromRegistry() {
1522
1936
  }
1523
1937
  }
1524
1938
  function npmCommand() {
1525
- const binDir = dirname3(process.execPath);
1526
- return process.platform === "win32" ? join6(binDir, "npm.cmd") : join6(binDir, "npm");
1939
+ const binDir = dirname4(process.execPath);
1940
+ return process.platform === "win32" ? join7(binDir, "npm.cmd") : join7(binDir, "npm");
1527
1941
  }
1528
1942
  async function npmDownload(version) {
1529
- const prefix = await mkdtemp(join6(tmpdir(), "llmnesia-update-"));
1943
+ const prefix = await mkdtemp(join7(tmpdir(), "llmnesia-update-"));
1530
1944
  await new Promise((resolve2, reject) => {
1531
1945
  const child = spawn(
1532
1946
  npmCommand(),
@@ -1561,7 +1975,7 @@ async function npmDownload(version) {
1561
1975
  }
1562
1976
  });
1563
1977
  });
1564
- return join6(prefix, "node_modules", PACKAGE_NAME, "dist");
1978
+ return join7(prefix, "node_modules", PACKAGE_NAME, "dist");
1565
1979
  }
1566
1980
  async function maybeSelfUpdate(options = {}) {
1567
1981
  const env = options.env ?? process.env;
@@ -1570,7 +1984,7 @@ async function maybeSelfUpdate(options = {}) {
1570
1984
  }
1571
1985
  const home = options.home ?? homedir3();
1572
1986
  const currentVersion = options.currentVersion ?? VERSION;
1573
- const runningDir = options.runningDir ?? dirname3(fileURLToPath2(import.meta.url));
1987
+ const runningDir = options.runningDir ?? dirname4(fileURLToPath2(import.meta.url));
1574
1988
  if (runningDir !== runtimeBinDir(home)) {
1575
1989
  return { action: "not-installed-runtime" };
1576
1990
  }
@@ -1578,7 +1992,7 @@ async function maybeSelfUpdate(options = {}) {
1578
1992
  return { action: "not-a-release-build", currentVersion };
1579
1993
  }
1580
1994
  const now = options.now ?? Date.now;
1581
- const state = await readState(home);
1995
+ const state = await readState2(home);
1582
1996
  if (state.lastCheckedAt !== void 0 && now() - state.lastCheckedAt < UPDATE_CHECK_INTERVAL_MS) {
1583
1997
  return { action: "throttled" };
1584
1998
  }
@@ -1586,30 +2000,30 @@ async function maybeSelfUpdate(options = {}) {
1586
2000
  try {
1587
2001
  latest = await (options.fetchLatestVersion ?? fetchLatestFromRegistry)();
1588
2002
  } catch (err) {
1589
- await writeState(home, { ...state, lastCheckedAt: now() });
2003
+ await writeState2(home, { ...state, lastCheckedAt: now() });
1590
2004
  return { action: "check-failed", error: err instanceof Error ? err.message : String(err) };
1591
2005
  }
1592
2006
  const checked = { ...state, lastCheckedAt: now(), lastSeenVersion: latest };
1593
2007
  if (!isStableRelease(latest) || compareVersions(latest, currentVersion) <= 0) {
1594
- await writeState(home, checked);
2008
+ await writeState2(home, checked);
1595
2009
  return { action: "up-to-date", latest };
1596
2010
  }
1597
2011
  if (state.lastFailedVersion === latest) {
1598
- await writeState(home, checked);
2012
+ await writeState2(home, checked);
1599
2013
  return { action: "skipped-failed-before", latest };
1600
2014
  }
1601
2015
  let sourceDir;
1602
2016
  try {
1603
2017
  sourceDir = await (options.downloadPackage ?? npmDownload)(latest);
1604
2018
  await installRuntime({ home, sourceDir, version: latest });
1605
- await writeState(home, { ...checked, lastUpdatedTo: latest, lastFailedVersion: void 0 });
2019
+ await writeState2(home, { ...checked, lastUpdatedTo: latest, lastFailedVersion: void 0 });
1606
2020
  return { action: "updated", from: currentVersion, to: latest };
1607
2021
  } catch (err) {
1608
- await writeState(home, { ...checked, lastFailedVersion: latest });
2022
+ await writeState2(home, { ...checked, lastFailedVersion: latest });
1609
2023
  return { action: "update-failed", latest, error: err instanceof Error ? err.message : String(err) };
1610
2024
  } finally {
1611
2025
  if (sourceDir) {
1612
- await rm2(join6(sourceDir, "..", "..", ".."), { recursive: true, force: true }).catch(() => {
2026
+ await rm2(join7(sourceDir, "..", "..", ".."), { recursive: true, force: true }).catch(() => {
1613
2027
  });
1614
2028
  }
1615
2029
  }
@@ -1658,6 +2072,11 @@ export {
1658
2072
  SearchIndex,
1659
2073
  openSearchIndex,
1660
2074
  deleteIndexDb,
2075
+ inspectCorpusStorage,
2076
+ compactCorpus,
2077
+ AUTO_MAINTENANCE_RECHECK_MS,
2078
+ runAutomaticMaintenance,
2079
+ readAutomaticMaintenanceState,
1661
2080
  runtimeBinDir,
1662
2081
  installRuntime,
1663
2082
  uninstallRuntime,
@@ -4,23 +4,26 @@ const require = __llmnesiaCreateRequire(import.meta.url);
4
4
  import {
5
5
  Corpus,
6
6
  SearchIndex,
7
+ compactCorpus,
7
8
  deleteIndexDb,
8
9
  drainInbox,
9
10
  ingestFile,
11
+ inspectCorpusStorage,
10
12
  installRuntime,
11
13
  isSelfUpdateDisabled,
12
14
  openSearchIndex,
15
+ readAutomaticMaintenanceState,
13
16
  resolveCorpusDir,
14
17
  runtimeBinDir,
15
18
  staleIndexFiles,
16
19
  uninstallRuntime
17
- } from "./chunk-NSVHWVEK.js";
20
+ } from "./chunk-MTPXE734.js";
18
21
  import {
19
22
  VERSION,
20
23
  __commonJS,
21
24
  __require,
22
25
  __toESM
23
- } from "./chunk-JSNSTAFD.js";
26
+ } from "./chunk-BDR4JVMF.js";
24
27
 
25
28
  // ../node_modules/isexe/windows.js
26
29
  var require_windows = __commonJS({
@@ -28,7 +31,7 @@ var require_windows = __commonJS({
28
31
  "use strict";
29
32
  module.exports = isexe;
30
33
  isexe.sync = sync;
31
- var fs2 = __require("fs");
34
+ var fs = __require("fs");
32
35
  function checkPathExt(path, options) {
33
36
  var pathext = options.pathExt !== void 0 ? options.pathExt : process.env.PATHEXT;
34
37
  if (!pathext) {
@@ -53,12 +56,12 @@ var require_windows = __commonJS({
53
56
  return checkPathExt(path, options);
54
57
  }
55
58
  function isexe(path, options, cb) {
56
- fs2.stat(path, function(er, stat6) {
59
+ fs.stat(path, function(er, stat6) {
57
60
  cb(er, er ? false : checkStat(stat6, path, options));
58
61
  });
59
62
  }
60
63
  function sync(path, options) {
61
- return checkStat(fs2.statSync(path), path, options);
64
+ return checkStat(fs.statSync(path), path, options);
62
65
  }
63
66
  }
64
67
  });
@@ -69,14 +72,14 @@ var require_mode = __commonJS({
69
72
  "use strict";
70
73
  module.exports = isexe;
71
74
  isexe.sync = sync;
72
- var fs2 = __require("fs");
75
+ var fs = __require("fs");
73
76
  function isexe(path, options, cb) {
74
- fs2.stat(path, function(er, stat6) {
77
+ fs.stat(path, function(er, stat6) {
75
78
  cb(er, er ? false : checkStat(stat6, options));
76
79
  });
77
80
  }
78
81
  function sync(path, options) {
79
- return checkStat(fs2.statSync(path), options);
82
+ return checkStat(fs.statSync(path), options);
80
83
  }
81
84
  function checkStat(stat6, options) {
82
85
  return stat6.isFile() && checkMode(stat6, options);
@@ -101,7 +104,7 @@ var require_mode = __commonJS({
101
104
  var require_isexe = __commonJS({
102
105
  "../node_modules/isexe/index.js"(exports, module) {
103
106
  "use strict";
104
- var fs2 = __require("fs");
107
+ var fs = __require("fs");
105
108
  var core;
106
109
  if (process.platform === "win32" || global.TESTING_WINDOWS) {
107
110
  core = require_windows();
@@ -366,16 +369,16 @@ var require_shebang_command = __commonJS({
366
369
  var require_readShebang = __commonJS({
367
370
  "../node_modules/cross-spawn/lib/util/readShebang.js"(exports, module) {
368
371
  "use strict";
369
- var fs2 = __require("fs");
372
+ var fs = __require("fs");
370
373
  var shebangCommand = require_shebang_command();
371
374
  function readShebang(command) {
372
375
  const size = 150;
373
376
  const buffer = Buffer.alloc(size);
374
377
  let fd;
375
378
  try {
376
- fd = fs2.openSync(command, "r");
377
- fs2.readSync(fd, buffer, 0, size, 0);
378
- fs2.closeSync(fd);
379
+ fd = fs.openSync(command, "r");
380
+ fs.readSync(fd, buffer, 0, size, 0);
381
+ fs.closeSync(fd);
379
382
  } catch (e) {
380
383
  }
381
384
  return shebangCommand(buffer.toString());
@@ -525,7 +528,7 @@ var require_cross_spawn = __commonJS({
525
528
 
526
529
  // src/cli.ts
527
530
  import { createInterface } from "readline/promises";
528
- import { join as join7 } from "path";
531
+ import { join as join6 } from "path";
529
532
 
530
533
  // ../packages/native-host/install.mjs
531
534
  import { execFile } from "child_process";
@@ -3749,157 +3752,6 @@ function formatDoctorReport(report) {
3749
3752
  return report.checks.map((c) => `${icon[c.status]} ${c.name}: ${c.detail}`).join("\n");
3750
3753
  }
3751
3754
 
3752
- // src/storageMaintenance.ts
3753
- import { promises as fs } from "fs";
3754
- import { join as join6 } from "path";
3755
- async function inspectCorpusStorage(corpus) {
3756
- const conversations = await corpus.conversationStorageStats();
3757
- const processed = await directoryFiles(corpus.paths.processedDir);
3758
- const quarantine = await directoryFiles(corpus.paths.quarantineDir);
3759
- const activeNames = [
3760
- corpus.paths.indexDbFile,
3761
- `${corpus.paths.indexDbFile}-wal`,
3762
- `${corpus.paths.indexDbFile}-shm`
3763
- ];
3764
- const activeIndex = await explicitFiles(activeNames);
3765
- const staleNames = await staleIndexFiles(corpus.paths.indexDir);
3766
- const stalePaths = staleNames.flatMap((name) => {
3767
- const base = join6(corpus.paths.indexDir, name);
3768
- return [base, `${base}-wal`, `${base}-shm`];
3769
- });
3770
- const staleIndexes = await explicitFiles(stalePaths);
3771
- const root = await directoryFiles(corpus.paths.root);
3772
- const classifiedBytes = conversations.legacyBytes + conversations.compressedBytes + processed.bytes + quarantine.bytes + activeIndex.bytes + staleIndexes.bytes;
3773
- const classifiedFiles = conversations.legacyFiles + conversations.compressedFiles + processed.files + quarantine.files + activeIndex.files + staleIndexes.files;
3774
- const other = {
3775
- files: Math.max(0, root.files - classifiedFiles),
3776
- bytes: Math.max(0, root.bytes - classifiedBytes)
3777
- };
3778
- return {
3779
- root: corpus.paths.root,
3780
- conversations,
3781
- processed,
3782
- quarantine,
3783
- activeIndex,
3784
- staleIndexes,
3785
- other,
3786
- totalBytes: root.bytes,
3787
- immediatelyReclaimableBytes: processed.bytes + staleIndexes.bytes
3788
- };
3789
- }
3790
- async function compactCorpus(corpus, options = {}) {
3791
- const dryRun = options.dryRun === true;
3792
- const before = await inspectCorpusStorage(corpus);
3793
- if (dryRun) {
3794
- const compression = await corpus.migrateLegacyConversations({ dryRun: true });
3795
- const compressionSavings = Math.max(
3796
- 0,
3797
- compression.legacyBytes + compression.compressedBytes - compression.projectedCompressedBytes
3798
- );
3799
- return {
3800
- dryRun: true,
3801
- before,
3802
- compression,
3803
- processedRemoved: before.processed,
3804
- staleIndexesRemoved: before.staleIndexes,
3805
- after: null,
3806
- reclaimedBytes: before.immediatelyReclaimableBytes + compressionSavings
3807
- };
3808
- }
3809
- await corpus.init();
3810
- const processedRemoved = await removeDirectoryFiles(corpus.paths.processedDir);
3811
- const index = await openSearchIndex(corpus);
3812
- try {
3813
- index.compact();
3814
- } finally {
3815
- index.close();
3816
- }
3817
- const appliedCompression = await corpus.migrateLegacyConversations();
3818
- if (appliedCompression.failures.length > 0) {
3819
- throw new Error(
3820
- `Corpus compression left ${appliedCompression.failures.length} legacy file(s) untouched; stale indexes were preserved. Run compact again after fixing the reported files.`
3821
- );
3822
- }
3823
- const staleIndexesRemoved = await removeStaleIndexes(corpus);
3824
- const after = await inspectCorpusStorage(corpus);
3825
- return {
3826
- dryRun: false,
3827
- before,
3828
- compression: appliedCompression,
3829
- processedRemoved,
3830
- staleIndexesRemoved,
3831
- after,
3832
- reclaimedBytes: Math.max(0, before.totalBytes - after.totalBytes)
3833
- };
3834
- }
3835
- async function removeStaleIndexes(corpus) {
3836
- const names = await staleIndexFiles(corpus.paths.indexDir);
3837
- const paths2 = names.flatMap((name) => {
3838
- const base = join6(corpus.paths.indexDir, name);
3839
- return [base, `${base}-wal`, `${base}-shm`];
3840
- });
3841
- const before = await explicitFiles(paths2);
3842
- for (const name of names) {
3843
- await deleteIndexDb(join6(corpus.paths.indexDir, name));
3844
- }
3845
- return before;
3846
- }
3847
- async function removeDirectoryFiles(dir) {
3848
- const before = await directoryFiles(dir);
3849
- await removeFilesRecursively(dir);
3850
- return before;
3851
- }
3852
- async function removeFilesRecursively(dir) {
3853
- let entries;
3854
- try {
3855
- entries = await fs.readdir(dir, { withFileTypes: true });
3856
- } catch {
3857
- return;
3858
- }
3859
- for (const entry of entries) {
3860
- const path = join6(dir, entry.name);
3861
- if (entry.isDirectory()) await removeFilesRecursively(path);
3862
- else if (entry.isFile()) await fs.rm(path, { force: true });
3863
- }
3864
- }
3865
- async function explicitFiles(files) {
3866
- let count = 0;
3867
- let bytes = 0;
3868
- for (const file of files) {
3869
- try {
3870
- const stat6 = await fs.stat(file);
3871
- if (stat6.isFile()) {
3872
- count += 1;
3873
- bytes += stat6.size;
3874
- }
3875
- } catch {
3876
- }
3877
- }
3878
- return { files: count, bytes };
3879
- }
3880
- async function directoryFiles(dir) {
3881
- let entries;
3882
- try {
3883
- entries = await fs.readdir(dir, { withFileTypes: true });
3884
- } catch {
3885
- return { files: 0, bytes: 0 };
3886
- }
3887
- let files = 0;
3888
- let bytes = 0;
3889
- for (const entry of entries) {
3890
- const path = join6(dir, entry.name);
3891
- if (entry.isDirectory()) {
3892
- const nested = await directoryFiles(path);
3893
- files += nested.files;
3894
- bytes += nested.bytes;
3895
- } else if (entry.isFile()) {
3896
- files += 1;
3897
- bytes += (await fs.stat(path)).size;
3898
- }
3899
- }
3900
- return { files, bytes };
3901
- }
3902
-
3903
3755
  // src/cli.ts
3904
3756
  var USAGE = `llmnesia-mcp ${VERSION}
3905
3757
 
@@ -4168,6 +4020,7 @@ async function main() {
4168
4020
  case "stats": {
4169
4021
  const stats = await corpus.stats();
4170
4022
  const storage = await inspectCorpusStorage(corpus);
4023
+ const maintenance = await readAutomaticMaintenanceState(corpus);
4171
4024
  const platforms = Object.entries(stats.byPlatform).sort((a, b) => b[1] - a[1]).map(([p, n]) => ` ${p}: ${n}`).join("\n");
4172
4025
  process.stdout.write(
4173
4026
  `Corpus: ${root}
@@ -4181,7 +4034,8 @@ ${platforms}
4181
4034
  processed: ${formatBytes(storage.processed.bytes)} (${storage.processed.files} reclaimable files)
4182
4035
  stale indexes: ${formatBytes(storage.staleIndexes.bytes)}
4183
4036
  quarantine: ${formatBytes(storage.quarantine.bytes)}
4184
- `
4037
+ ` + (maintenance ? ` maintenance: ${formatMaintenanceState(maintenance)}
4038
+ ` : "")
4185
4039
  );
4186
4040
  return 0;
4187
4041
  }
@@ -4220,7 +4074,7 @@ Corpus now uses ${formatBytes(result.after?.totalBytes ?? 0)}.
4220
4074
  }
4221
4075
  const stale = await staleIndexFiles(corpus.paths.indexDir);
4222
4076
  for (const name of stale) {
4223
- await deleteIndexDb(join7(corpus.paths.indexDir, name));
4077
+ await deleteIndexDb(join6(corpus.paths.indexDir, name));
4224
4078
  }
4225
4079
  if (stale.length > 0) {
4226
4080
  process.stdout.write(
@@ -4287,6 +4141,18 @@ function formatBytes(bytes) {
4287
4141
  }
4288
4142
  return `${value.toFixed(value >= 10 ? 1 : 2)} ${unit}`;
4289
4143
  }
4144
+ function formatMaintenanceState(state) {
4145
+ if (state.status === "complete") {
4146
+ return `complete${state.completedAt ? ` (${new Date(state.completedAt).toISOString()})` : ""}`;
4147
+ }
4148
+ if (state.status === "waiting" && state.nextEligibleAt) {
4149
+ return `automatic cleanup scheduled after ${new Date(state.nextEligibleAt).toISOString()}`;
4150
+ }
4151
+ if (state.status === "failed" && state.nextEligibleAt) {
4152
+ return `will retry after ${new Date(state.nextEligibleAt).toISOString()}`;
4153
+ }
4154
+ return state.status;
4155
+ }
4290
4156
  function formatCompactPreview(result) {
4291
4157
  const { before, compression } = result;
4292
4158
  const compressionSavings = Math.max(
package/dist/cli.js CHANGED
@@ -3,7 +3,7 @@ import { createRequire as __llmnesiaCreateRequire } from "node:module";
3
3
  const require = __llmnesiaCreateRequire(import.meta.url);
4
4
  import {
5
5
  VERSION
6
- } from "./chunk-JSNSTAFD.js";
6
+ } from "./chunk-BDR4JVMF.js";
7
7
 
8
8
  // src/nodeDownload.ts
9
9
  var NODE_DOWNLOAD_URLS = {
@@ -141,7 +141,7 @@ async function bootstrap() {
141
141
  await guideUnsupportedNodeVersion(process.versions.node);
142
142
  return 1;
143
143
  }
144
- const { main } = await import("./cli-ZEA7WTVW.js");
144
+ const { main } = await import("./cli-OXKBAUBE.js");
145
145
  return main();
146
146
  }
147
147
  bootstrap().then((code) => {
Binary file
package/dist/mcp.js CHANGED
@@ -2,20 +2,22 @@
2
2
  import { createRequire as __llmnesiaCreateRequire } from "node:module";
3
3
  const require = __llmnesiaCreateRequire(import.meta.url);
4
4
  import {
5
+ AUTO_MAINTENANCE_RECHECK_MS,
5
6
  Corpus,
6
7
  describeOutcome,
7
8
  drainInbox,
8
9
  openSearchIndex,
9
10
  pendingInboxFiles,
10
11
  resolveCorpusDir,
12
+ runAutomaticMaintenance,
11
13
  scheduleSelfUpdate
12
- } from "./chunk-NSVHWVEK.js";
14
+ } from "./chunk-MTPXE734.js";
13
15
  import {
14
16
  VERSION,
15
17
  __commonJS,
16
18
  __export,
17
19
  __toESM
18
- } from "./chunk-JSNSTAFD.js";
20
+ } from "./chunk-BDR4JVMF.js";
19
21
 
20
22
  // ../node_modules/ajv/dist/compile/codegen/code.js
21
23
  var require_code = __commonJS({
@@ -21393,6 +21395,9 @@ function describeIssues(error2) {
21393
21395
 
21394
21396
  // src/service.ts
21395
21397
  var STALE_AFTER_HOURS = 48;
21398
+ var AUTO_MAINTENANCE_START_DELAY_MS = 6e4;
21399
+ var AUTO_MAINTENANCE_IDLE_MS = 3e4;
21400
+ var AUTO_MAINTENANCE_BUSY_RETRY_MS = 3e4;
21396
21401
  var CompanionService = class _CompanionService {
21397
21402
  constructor(corpus, index, onDrainError, memoryWriteCap) {
21398
21403
  this.corpus = corpus;
@@ -21409,6 +21414,10 @@ var CompanionService = class _CompanionService {
21409
21414
  refreshChain = Promise.resolve();
21410
21415
  /** Memories written since this process started; the per-session cap counts these. */
21411
21416
  memoriesWritten = 0;
21417
+ activeOperations = 0;
21418
+ lastOperationAt = Date.now();
21419
+ maintenanceTimer;
21420
+ closed = false;
21412
21421
  static async open(root, options = {}) {
21413
21422
  const corpus = new Corpus(root);
21414
21423
  const index = await openSearchIndex(corpus);
@@ -21419,8 +21428,71 @@ var CompanionService = class _CompanionService {
21419
21428
  options.memoryWriteCap ?? DEFAULT_MEMORY_WRITE_CAP
21420
21429
  );
21421
21430
  await service.refresh();
21431
+ if (options.automaticMaintenance) {
21432
+ service.scheduleAutomaticMaintenance(options.automaticMaintenance);
21433
+ }
21422
21434
  return service;
21423
21435
  }
21436
+ /** Let the MCP wrapper expose activity to the idle-only maintenance scheduler. */
21437
+ async runWithActivity(operation) {
21438
+ this.activeOperations += 1;
21439
+ this.lastOperationAt = Date.now();
21440
+ try {
21441
+ return await operation();
21442
+ } finally {
21443
+ this.activeOperations -= 1;
21444
+ this.lastOperationAt = Date.now();
21445
+ }
21446
+ }
21447
+ scheduleAutomaticMaintenance(options) {
21448
+ const {
21449
+ delayMs = AUTO_MAINTENANCE_START_DELAY_MS,
21450
+ idleMs = AUTO_MAINTENANCE_IDLE_MS,
21451
+ busyRetryMs = AUTO_MAINTENANCE_BUSY_RETRY_MS,
21452
+ onOutcome,
21453
+ ...runOptions
21454
+ } = options;
21455
+ const schedule = (delay) => {
21456
+ if (this.closed) return;
21457
+ this.maintenanceTimer = setTimeout(
21458
+ attempt,
21459
+ Math.max(0, Math.min(delay, 2147483647))
21460
+ );
21461
+ this.maintenanceTimer.unref?.();
21462
+ };
21463
+ const attempt = () => {
21464
+ this.maintenanceTimer = void 0;
21465
+ if (this.closed) return;
21466
+ const idleFor = Date.now() - this.lastOperationAt;
21467
+ if (this.activeOperations > 0 || idleFor < idleMs) {
21468
+ schedule(Math.max(busyRetryMs, idleMs - idleFor));
21469
+ return;
21470
+ }
21471
+ void runAutomaticMaintenance(this.corpus, this.index, runOptions).then(
21472
+ (outcome) => {
21473
+ onOutcome?.(outcome);
21474
+ const current = Date.now();
21475
+ if (outcome.action === "deferred") schedule(outcome.until - current);
21476
+ else if (outcome.action === "failed") schedule(outcome.retryAt - current);
21477
+ else if (outcome.action === "locked") schedule(busyRetryMs);
21478
+ else if (outcome.action === "recently-checked") {
21479
+ schedule(outcome.completedAt + (runOptions.recheckMs ?? AUTO_MAINTENANCE_RECHECK_MS) - current);
21480
+ } else if (outcome.action === "compacted" || outcome.action === "no-work") {
21481
+ schedule(runOptions.recheckMs ?? AUTO_MAINTENANCE_RECHECK_MS);
21482
+ }
21483
+ },
21484
+ (error2) => {
21485
+ onOutcome?.({
21486
+ action: "failed",
21487
+ error: error2 instanceof Error ? error2.message : String(error2),
21488
+ retryAt: Date.now() + busyRetryMs
21489
+ });
21490
+ schedule(busyRetryMs);
21491
+ }
21492
+ );
21493
+ };
21494
+ schedule(delayMs);
21495
+ }
21424
21496
  /**
21425
21497
  * Ingest whatever is waiting in `inbox/`. Never throws: a corrupt or
21426
21498
  * unreadable delta is reported and skipped rather than failing the read.
@@ -21547,6 +21619,9 @@ var CompanionService = class _CompanionService {
21547
21619
  };
21548
21620
  }
21549
21621
  close() {
21622
+ this.closed = true;
21623
+ if (this.maintenanceTimer) clearTimeout(this.maintenanceTimer);
21624
+ this.maintenanceTimer = void 0;
21550
21625
  this.index.close();
21551
21626
  }
21552
21627
  };
@@ -21636,12 +21711,28 @@ async function runServe() {
21636
21711
  onDrainError: (error2) => process.stderr.write(
21637
21712
  `llmnesia: inbox drain failed, serving the existing index: ${error2 instanceof Error ? error2.message : String(error2)}
21638
21713
  `
21639
- )
21714
+ ),
21715
+ automaticMaintenance: {
21716
+ onOutcome: (outcome) => {
21717
+ if (outcome.action === "failed") {
21718
+ process.stderr.write(
21719
+ `llmnesia: automatic storage maintenance will retry later: ${outcome.error}
21720
+ `
21721
+ );
21722
+ } else if (outcome.action === "compacted" && outcome.report.reclaimedBytes > 0) {
21723
+ process.stderr.write(
21724
+ `llmnesia: automatic storage maintenance reclaimed ${outcome.report.reclaimedBytes} bytes
21725
+ `
21726
+ );
21727
+ }
21728
+ }
21729
+ }
21640
21730
  })
21641
21731
  );
21642
21732
  const withService = async (operation) => {
21643
21733
  try {
21644
- return await operation(await service.get(5e3));
21734
+ const ready = await service.get(5e3);
21735
+ return await ready.runWithActivity(() => operation(ready));
21645
21736
  } catch (err) {
21646
21737
  if (err instanceof ResourcePreparingError) {
21647
21738
  return errorResult("LLMnesia is still preparing the local conversation index. Retry this tool shortly.");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llmnesia/mcp",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "comment:mcpName": "The MCP Registry's ownership marker: it must equal server.json's `name`, and the namespace must match how you authenticate with mcp-publisher. `com.llmnesia/...` requires DNS auth (a TXT record on llmnesia.com) — see docs/MCP_INSTALLER_BOARD.md.",
package/server.json CHANGED
@@ -3,13 +3,13 @@
3
3
  "name": "com.llmnesia/mcp",
4
4
  "title": "LLMnesia",
5
5
  "description": "Search your AI chat history from Claude, Cursor, Codex, and other MCP clients.",
6
- "version": "0.2.4",
6
+ "version": "0.2.5",
7
7
  "packages": [
8
8
  {
9
9
  "registryType": "npm",
10
10
  "registryBaseUrl": "https://registry.npmjs.org",
11
11
  "identifier": "@llmnesia/mcp",
12
- "version": "0.2.4",
12
+ "version": "0.2.5",
13
13
  "transport": {
14
14
  "type": "stdio"
15
15
  }