@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 +13 -4
- package/dist/{chunk-JSNSTAFD.js → chunk-BDR4JVMF.js} +1 -1
- package/dist/{chunk-NSVHWVEK.js → chunk-MTPXE734.js} +458 -39
- package/dist/{cli-ZEA7WTVW.js → cli-OXKBAUBE.js} +33 -167
- package/dist/cli.js +2 -2
- package/dist/llmnesia.mcpb +0 -0
- package/dist/mcp.js +95 -4
- package/package.json +1 -1
- package/server.json +2 -2
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.
|
|
155
|
-
|
|
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
|
|
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.
|
|
@@ -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-
|
|
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
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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
|
|
1780
|
+
import { dirname as dirname3, join as join6 } from "path";
|
|
1367
1781
|
import { fileURLToPath } from "url";
|
|
1368
1782
|
function defaultSourceDir() {
|
|
1369
|
-
return
|
|
1783
|
+
return dirname3(fileURLToPath(import.meta.url));
|
|
1370
1784
|
}
|
|
1371
1785
|
function runtimeBinDir(home = homedir2()) {
|
|
1372
|
-
return
|
|
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 =
|
|
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 =
|
|
1799
|
+
const parentDir = dirname3(binDir);
|
|
1386
1800
|
const nonce = `${process.pid}-${Date.now()}`;
|
|
1387
|
-
const stagingDir =
|
|
1388
|
-
const backupDir =
|
|
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(
|
|
1809
|
+
await cp(join6(sourceDir, name), join6(stagingDir, name), { recursive: true });
|
|
1396
1810
|
}
|
|
1397
1811
|
await writeFile(
|
|
1398
|
-
|
|
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 =
|
|
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
|
|
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
|
|
1877
|
+
return join7(home, ".llmnesia", "update-state.json");
|
|
1464
1878
|
}
|
|
1465
|
-
async function
|
|
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
|
|
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 =
|
|
1526
|
-
return process.platform === "win32" ?
|
|
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(
|
|
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
|
|
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 ??
|
|
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
|
|
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
|
|
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
|
|
2008
|
+
await writeState2(home, checked);
|
|
1595
2009
|
return { action: "up-to-date", latest };
|
|
1596
2010
|
}
|
|
1597
2011
|
if (state.lastFailedVersion === latest) {
|
|
1598
|
-
await
|
|
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
|
|
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
|
|
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(
|
|
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-
|
|
20
|
+
} from "./chunk-MTPXE734.js";
|
|
18
21
|
import {
|
|
19
22
|
VERSION,
|
|
20
23
|
__commonJS,
|
|
21
24
|
__require,
|
|
22
25
|
__toESM
|
|
23
|
-
} from "./chunk-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
|
75
|
+
var fs = __require("fs");
|
|
73
76
|
function isexe(path, options, cb) {
|
|
74
|
-
|
|
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(
|
|
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
|
|
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
|
|
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 =
|
|
377
|
-
|
|
378
|
-
|
|
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
|
|
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(
|
|
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-
|
|
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-
|
|
144
|
+
const { main } = await import("./cli-OXKBAUBE.js");
|
|
145
145
|
return main();
|
|
146
146
|
}
|
|
147
147
|
bootstrap().then((code) => {
|
package/dist/llmnesia.mcpb
CHANGED
|
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-
|
|
14
|
+
} from "./chunk-MTPXE734.js";
|
|
13
15
|
import {
|
|
14
16
|
VERSION,
|
|
15
17
|
__commonJS,
|
|
16
18
|
__export,
|
|
17
19
|
__toESM
|
|
18
|
-
} from "./chunk-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
12
|
+
"version": "0.2.5",
|
|
13
13
|
"transport": {
|
|
14
14
|
"type": "stdio"
|
|
15
15
|
}
|