@algosuite/vo-mcp 0.2.0-beta.29 → 0.2.0-beta.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-auth-probe-cli.mjs +187 -6
- package/dist/autostart-cli.js +62 -46
- package/dist/autostart-cli.js.map +2 -2
- package/dist/cli.js +952 -229
- package/dist/cli.js.map +4 -4
- package/dist/index.js +891 -198
- package/dist/index.js.map +4 -4
- package/dist/install-cli.js +56 -42
- package/dist/install-cli.js.map +3 -3
- package/dist/runner-cli.js +1403 -381
- package/dist/runner-cli.js.map +4 -4
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -1079,6 +1079,42 @@ function toEventPerModelVerdicts(src) {
|
|
|
1079
1079
|
};
|
|
1080
1080
|
});
|
|
1081
1081
|
}
|
|
1082
|
+
function aggregateEventTokenUsage(src, engineUsage) {
|
|
1083
|
+
if (engineUsage !== void 0) {
|
|
1084
|
+
const hasIn = Object.keys(engineUsage.per_model_tokens_in).length > 0;
|
|
1085
|
+
const hasOut = Object.keys(engineUsage.per_model_tokens_out).length > 0;
|
|
1086
|
+
return {
|
|
1087
|
+
per_model_tokens_in: hasIn ? engineUsage.per_model_tokens_in : null,
|
|
1088
|
+
per_model_tokens_out: hasOut ? engineUsage.per_model_tokens_out : null,
|
|
1089
|
+
total_cost_usd: engineUsage.cost_micro_usd === null ? null : engineUsage.cost_micro_usd / 1e6
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
const tokensIn = {};
|
|
1093
|
+
const tokensOut = {};
|
|
1094
|
+
let anyTokensIn = false;
|
|
1095
|
+
let anyTokensOut = false;
|
|
1096
|
+
let costMicroUsd = 0;
|
|
1097
|
+
let anyCost = false;
|
|
1098
|
+
for (const v of src) {
|
|
1099
|
+
if (typeof v.input_tokens === "number") {
|
|
1100
|
+
tokensIn[v.model] = (tokensIn[v.model] ?? 0) + v.input_tokens;
|
|
1101
|
+
anyTokensIn = true;
|
|
1102
|
+
}
|
|
1103
|
+
if (typeof v.output_tokens === "number") {
|
|
1104
|
+
tokensOut[v.model] = (tokensOut[v.model] ?? 0) + v.output_tokens;
|
|
1105
|
+
anyTokensOut = true;
|
|
1106
|
+
}
|
|
1107
|
+
if (typeof v.cost_micro_usd === "number") {
|
|
1108
|
+
costMicroUsd += v.cost_micro_usd;
|
|
1109
|
+
anyCost = true;
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
return {
|
|
1113
|
+
per_model_tokens_in: anyTokensIn ? tokensIn : null,
|
|
1114
|
+
per_model_tokens_out: anyTokensOut ? tokensOut : null,
|
|
1115
|
+
total_cost_usd: anyCost ? costMicroUsd / 1e6 : null
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1082
1118
|
function toEventSynthesizedVerdict(src) {
|
|
1083
1119
|
return {
|
|
1084
1120
|
verdict: src.verdict,
|
|
@@ -1399,13 +1435,713 @@ var init_safe_memory_file = __esm({
|
|
|
1399
1435
|
}
|
|
1400
1436
|
});
|
|
1401
1437
|
|
|
1438
|
+
// src/tools/memory/sync-lock-liveness.ts
|
|
1439
|
+
import { statSync as statSync4, readFileSync as readFileSync8 } from "node:fs";
|
|
1440
|
+
function defaultIsProcessAlive(pid) {
|
|
1441
|
+
try {
|
|
1442
|
+
process.kill(pid, 0);
|
|
1443
|
+
return true;
|
|
1444
|
+
} catch (err) {
|
|
1445
|
+
return err.code === "EPERM";
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
function toPayload(parsed) {
|
|
1449
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
|
|
1450
|
+
const record = parsed;
|
|
1451
|
+
const token = record["token"];
|
|
1452
|
+
const host = record["hostname"];
|
|
1453
|
+
if (typeof token !== "string" || token.length === 0) return null;
|
|
1454
|
+
const pid = record["pid"];
|
|
1455
|
+
const acquiredAtMs = record["acquiredAtMs"];
|
|
1456
|
+
return {
|
|
1457
|
+
pid: typeof pid === "number" && Number.isInteger(pid) && pid > 0 ? pid : 0,
|
|
1458
|
+
hostname: typeof host === "string" ? host : "",
|
|
1459
|
+
sessionId: typeof record["sessionId"] === "string" ? record["sessionId"] : null,
|
|
1460
|
+
token,
|
|
1461
|
+
acquiredAt: typeof record["acquiredAt"] === "string" ? record["acquiredAt"] : "",
|
|
1462
|
+
acquiredAtMs: typeof acquiredAtMs === "number" && Number.isFinite(acquiredAtMs) ? acquiredAtMs : Number.NaN
|
|
1463
|
+
};
|
|
1464
|
+
}
|
|
1465
|
+
function readLockRecord(path3) {
|
|
1466
|
+
let raw;
|
|
1467
|
+
try {
|
|
1468
|
+
raw = readFileSync8(path3, "utf8");
|
|
1469
|
+
} catch {
|
|
1470
|
+
return null;
|
|
1471
|
+
}
|
|
1472
|
+
try {
|
|
1473
|
+
return { raw, payload: toPayload(JSON.parse(raw)) };
|
|
1474
|
+
} catch {
|
|
1475
|
+
return { raw, payload: null };
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
function lockAgeMs(record, path3, nowMs) {
|
|
1479
|
+
let startedMs = Number.NaN;
|
|
1480
|
+
if (record.payload) {
|
|
1481
|
+
if (Number.isFinite(record.payload.acquiredAtMs)) {
|
|
1482
|
+
startedMs = record.payload.acquiredAtMs;
|
|
1483
|
+
} else if (record.payload.acquiredAt) {
|
|
1484
|
+
startedMs = Date.parse(record.payload.acquiredAt);
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
if (!Number.isFinite(startedMs)) {
|
|
1488
|
+
try {
|
|
1489
|
+
startedMs = statSync4(path3).mtimeMs;
|
|
1490
|
+
} catch {
|
|
1491
|
+
return null;
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
const age = nowMs - startedMs;
|
|
1495
|
+
return Number.isFinite(age) && age >= 0 ? age : null;
|
|
1496
|
+
}
|
|
1497
|
+
function classifyHolderLiveness(record, isProcessAlive, thisHost) {
|
|
1498
|
+
const payload = record.payload;
|
|
1499
|
+
if (payload === null) return "unknown";
|
|
1500
|
+
if (payload.pid <= 0) return "unknown";
|
|
1501
|
+
if (thisHost.length === 0) return "unknown";
|
|
1502
|
+
if (payload.hostname !== thisHost) return "unknown";
|
|
1503
|
+
return isProcessAlive(payload.pid) ? "alive" : "dead";
|
|
1504
|
+
}
|
|
1505
|
+
function isLockAbandoned(record, ageMs, ttlMs, isProcessAlive, thisHost) {
|
|
1506
|
+
const liveness = classifyHolderLiveness(record, isProcessAlive, thisHost);
|
|
1507
|
+
if (liveness === "alive") return false;
|
|
1508
|
+
if (liveness === "dead") return true;
|
|
1509
|
+
return ageMs !== null && ageMs > ttlMs;
|
|
1510
|
+
}
|
|
1511
|
+
var init_sync_lock_liveness = __esm({
|
|
1512
|
+
"src/tools/memory/sync-lock-liveness.ts"() {
|
|
1513
|
+
"use strict";
|
|
1514
|
+
}
|
|
1515
|
+
});
|
|
1516
|
+
|
|
1517
|
+
// src/tools/memory/sync-lock.ts
|
|
1518
|
+
import { closeSync as closeSync2, mkdirSync as mkdirSync5, openSync as openSync3, readFileSync as readFileSync9, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "node:fs";
|
|
1519
|
+
import { hostname } from "node:os";
|
|
1520
|
+
import { join as join8 } from "node:path";
|
|
1521
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
1522
|
+
function positiveOr(value, fallback) {
|
|
1523
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : fallback;
|
|
1524
|
+
}
|
|
1525
|
+
function createExclusive2(path3, contents) {
|
|
1526
|
+
let fd;
|
|
1527
|
+
try {
|
|
1528
|
+
fd = openSync3(path3, "wx");
|
|
1529
|
+
} catch (err) {
|
|
1530
|
+
const code = err.code;
|
|
1531
|
+
return { ok: false, exists: code === "EEXIST", message: err instanceof Error ? err.message : String(err) };
|
|
1532
|
+
}
|
|
1533
|
+
try {
|
|
1534
|
+
writeFileSync4(fd, contents, "utf8");
|
|
1535
|
+
} catch (err) {
|
|
1536
|
+
closeSync2(fd);
|
|
1537
|
+
try {
|
|
1538
|
+
unlinkSync2(path3);
|
|
1539
|
+
} catch {
|
|
1540
|
+
}
|
|
1541
|
+
return { ok: false, exists: false, message: err instanceof Error ? err.message : String(err) };
|
|
1542
|
+
}
|
|
1543
|
+
closeSync2(fd);
|
|
1544
|
+
return { ok: true };
|
|
1545
|
+
}
|
|
1546
|
+
function removeAbandoned(path3, expectedRaw) {
|
|
1547
|
+
let current;
|
|
1548
|
+
try {
|
|
1549
|
+
current = readFileSync9(path3, "utf8");
|
|
1550
|
+
} catch {
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
if (current !== expectedRaw) return;
|
|
1554
|
+
try {
|
|
1555
|
+
unlinkSync2(path3);
|
|
1556
|
+
} catch {
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
function makeRelease(path3, token) {
|
|
1560
|
+
let released = false;
|
|
1561
|
+
return () => {
|
|
1562
|
+
if (released) return;
|
|
1563
|
+
released = true;
|
|
1564
|
+
let raw;
|
|
1565
|
+
try {
|
|
1566
|
+
raw = readFileSync9(path3, "utf8");
|
|
1567
|
+
} catch {
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
let stillOurs;
|
|
1571
|
+
try {
|
|
1572
|
+
stillOurs = toPayload(JSON.parse(raw))?.token === token;
|
|
1573
|
+
} catch {
|
|
1574
|
+
stillOurs = false;
|
|
1575
|
+
}
|
|
1576
|
+
if (!stillOurs) return;
|
|
1577
|
+
try {
|
|
1578
|
+
unlinkSync2(path3);
|
|
1579
|
+
} catch {
|
|
1580
|
+
}
|
|
1581
|
+
};
|
|
1582
|
+
}
|
|
1583
|
+
function describeHolder(record) {
|
|
1584
|
+
const payload = record?.payload;
|
|
1585
|
+
if (!payload) return "an unreadable lock file";
|
|
1586
|
+
return `pid ${payload.pid} on ${payload.hostname || "(unknown host)"} (session ${payload.sessionId ?? "unknown"}, held since ${payload.acquiredAt || "unknown"})`;
|
|
1587
|
+
}
|
|
1588
|
+
async function acquireMemorySyncLock(options) {
|
|
1589
|
+
const waitMs = positiveOr(options.waitMs, DEFAULT_LOCK_WAIT_MS);
|
|
1590
|
+
const ttlMs = positiveOr(options.ttlMs, DEFAULT_LOCK_TTL_MS);
|
|
1591
|
+
const now = options.now ?? Date.now;
|
|
1592
|
+
const sleep = options.sleep ?? ((ms) => new Promise((resolve3) => {
|
|
1593
|
+
setTimeout(resolve3, ms);
|
|
1594
|
+
}));
|
|
1595
|
+
const isProcessAlive = options.isProcessAlive ?? defaultIsProcessAlive;
|
|
1596
|
+
const thisHost = hostname();
|
|
1597
|
+
const path3 = join8(options.memoryDir, MEMORY_SYNC_LOCK_FILE);
|
|
1598
|
+
if (options.createDir === true) mkdirSync5(options.memoryDir, { recursive: true });
|
|
1599
|
+
const deadline = now() + waitMs;
|
|
1600
|
+
let backoffMs = INITIAL_BACKOFF_MS;
|
|
1601
|
+
let tookOverFrom = null;
|
|
1602
|
+
let holderDescription = "another session";
|
|
1603
|
+
for (; ; ) {
|
|
1604
|
+
const acquiredAtMs = now();
|
|
1605
|
+
const payload = {
|
|
1606
|
+
pid: process.pid,
|
|
1607
|
+
hostname: thisHost,
|
|
1608
|
+
sessionId: options.sessionId ?? null,
|
|
1609
|
+
token: randomUUID2(),
|
|
1610
|
+
acquiredAt: new Date(acquiredAtMs).toISOString(),
|
|
1611
|
+
acquiredAtMs
|
|
1612
|
+
};
|
|
1613
|
+
const created = createExclusive2(path3, `${JSON.stringify(payload, null, 2)}
|
|
1614
|
+
`);
|
|
1615
|
+
if (created.ok) {
|
|
1616
|
+
return { path: path3, payload, tookOverFrom, release: makeRelease(path3, payload.token) };
|
|
1617
|
+
}
|
|
1618
|
+
if (!created.exists) {
|
|
1619
|
+
throw new Error(
|
|
1620
|
+
`memory sync lock ${path3} could not be created (${created.message}) \u2014 refusing to sync without exclusion`
|
|
1621
|
+
);
|
|
1622
|
+
}
|
|
1623
|
+
const record = readLockRecord(path3);
|
|
1624
|
+
let reclaimed = false;
|
|
1625
|
+
if (record) {
|
|
1626
|
+
holderDescription = describeHolder(record);
|
|
1627
|
+
const age = lockAgeMs(record, path3, now());
|
|
1628
|
+
if (isLockAbandoned(record, age, ttlMs, isProcessAlive, thisHost)) {
|
|
1629
|
+
tookOverFrom = record.payload;
|
|
1630
|
+
removeAbandoned(path3, record.raw);
|
|
1631
|
+
reclaimed = true;
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
if (now() >= deadline) {
|
|
1635
|
+
throw new Error(
|
|
1636
|
+
`memory sync lock ${path3} is held by ${holderDescription}; waited ${waitMs}ms \u2014 refusing to sync unlocked (concurrent memory writes corrupt the shared index). If that holder is provably gone, delete the lock file.`
|
|
1637
|
+
);
|
|
1638
|
+
}
|
|
1639
|
+
if (reclaimed) backoffMs = INITIAL_BACKOFF_MS;
|
|
1640
|
+
await sleep(Math.max(1, Math.min(backoffMs, deadline - now())));
|
|
1641
|
+
if (!reclaimed) backoffMs = Math.min(MAX_BACKOFF_MS, Math.ceil(backoffMs * BACKOFF_FACTOR));
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
async function withMemorySyncLock(options, fn) {
|
|
1645
|
+
const handle = await acquireMemorySyncLock(options);
|
|
1646
|
+
try {
|
|
1647
|
+
return await fn(handle);
|
|
1648
|
+
} finally {
|
|
1649
|
+
handle.release();
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
var MEMORY_SYNC_LOCK_FILE, DEFAULT_LOCK_TTL_MS, DEFAULT_LOCK_WAIT_MS, INITIAL_BACKOFF_MS, MAX_BACKOFF_MS, BACKOFF_FACTOR;
|
|
1653
|
+
var init_sync_lock = __esm({
|
|
1654
|
+
"src/tools/memory/sync-lock.ts"() {
|
|
1655
|
+
"use strict";
|
|
1656
|
+
init_sync_lock_liveness();
|
|
1657
|
+
init_sync_lock_liveness();
|
|
1658
|
+
MEMORY_SYNC_LOCK_FILE = ".memory-sync.lock";
|
|
1659
|
+
DEFAULT_LOCK_TTL_MS = 15 * 6e4;
|
|
1660
|
+
DEFAULT_LOCK_WAIT_MS = 1e4;
|
|
1661
|
+
INITIAL_BACKOFF_MS = 25;
|
|
1662
|
+
MAX_BACKOFF_MS = 500;
|
|
1663
|
+
BACKOFF_FACTOR = 1.6;
|
|
1664
|
+
}
|
|
1665
|
+
});
|
|
1666
|
+
|
|
1667
|
+
// src/tools/memory/memory-index-merge.ts
|
|
1668
|
+
function isMemoryIndexFile(fileName) {
|
|
1669
|
+
return fileName.toUpperCase() === MEMORY_INDEX_FILE.toUpperCase();
|
|
1670
|
+
}
|
|
1671
|
+
function indexRowKey(line) {
|
|
1672
|
+
const match = INDEX_ROW_RE.exec(line);
|
|
1673
|
+
if (!match) return null;
|
|
1674
|
+
let target = match[1].trim();
|
|
1675
|
+
if (target.startsWith("<") && target.endsWith(">")) target = target.slice(1, -1).trim();
|
|
1676
|
+
target = target.replace(/\s+(["'])[\s\S]*\1$/, "").trim();
|
|
1677
|
+
if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(target)) {
|
|
1678
|
+
target = target.replace(/\\/g, "/").replace(/\/{2,}/g, "/");
|
|
1679
|
+
target = target.replace(/^(?:\.\/)+/, "");
|
|
1680
|
+
}
|
|
1681
|
+
return target.length > 0 ? target.toLowerCase() : null;
|
|
1682
|
+
}
|
|
1683
|
+
function mergeMemoryIndex(localContent, cloudContent) {
|
|
1684
|
+
if (typeof cloudContent !== "string" || cloudContent.trim().length === 0) {
|
|
1685
|
+
return { content: localContent, addedFromCloud: [] };
|
|
1686
|
+
}
|
|
1687
|
+
const eol = localContent.includes("\r\n") ? "\r\n" : "\n";
|
|
1688
|
+
const localLines = localContent.split(/\r?\n/);
|
|
1689
|
+
const localKeys = /* @__PURE__ */ new Set();
|
|
1690
|
+
let lastLocalRowIndex = -1;
|
|
1691
|
+
for (let i = 0; i < localLines.length; i++) {
|
|
1692
|
+
const key = indexRowKey(localLines[i]);
|
|
1693
|
+
if (key === null) continue;
|
|
1694
|
+
localKeys.add(key);
|
|
1695
|
+
lastLocalRowIndex = i;
|
|
1696
|
+
}
|
|
1697
|
+
const addedFromCloud = [];
|
|
1698
|
+
const seenCloudKeys = /* @__PURE__ */ new Set();
|
|
1699
|
+
for (const rawLine of cloudContent.split(/\r?\n/)) {
|
|
1700
|
+
const key = indexRowKey(rawLine);
|
|
1701
|
+
if (key === null) continue;
|
|
1702
|
+
if (localKeys.has(key) || seenCloudKeys.has(key)) continue;
|
|
1703
|
+
seenCloudKeys.add(key);
|
|
1704
|
+
addedFromCloud.push(rawLine.replace(/\r$/, ""));
|
|
1705
|
+
}
|
|
1706
|
+
if (addedFromCloud.length === 0) {
|
|
1707
|
+
return { content: localContent, addedFromCloud: [] };
|
|
1708
|
+
}
|
|
1709
|
+
const merged = lastLocalRowIndex >= 0 ? [...localLines.slice(0, lastLocalRowIndex + 1), ...addedFromCloud, ...localLines.slice(lastLocalRowIndex + 1)] : [...localLines, ...addedFromCloud];
|
|
1710
|
+
return { content: merged.join(eol), addedFromCloud };
|
|
1711
|
+
}
|
|
1712
|
+
var MEMORY_INDEX_FILE, INDEX_ROW_RE;
|
|
1713
|
+
var init_memory_index_merge = __esm({
|
|
1714
|
+
"src/tools/memory/memory-index-merge.ts"() {
|
|
1715
|
+
"use strict";
|
|
1716
|
+
MEMORY_INDEX_FILE = "MEMORY.md";
|
|
1717
|
+
INDEX_ROW_RE = /^\s*[-*]\s+\[[^\]]*\]\(([^)]+)\)/;
|
|
1718
|
+
}
|
|
1719
|
+
});
|
|
1720
|
+
|
|
1721
|
+
// src/tools/memory/bounded-sync.ts
|
|
1722
|
+
function createSyncDeadline(budgetMs = SYNC_DEADLINE_MS, now = Date.now) {
|
|
1723
|
+
const startedAt = now();
|
|
1724
|
+
return {
|
|
1725
|
+
check() {
|
|
1726
|
+
const elapsed = now() - startedAt;
|
|
1727
|
+
if (elapsed > budgetMs) throw new SyncDeadlineExceededError(elapsed, budgetMs);
|
|
1728
|
+
},
|
|
1729
|
+
remainingMs() {
|
|
1730
|
+
return Math.max(0, budgetMs - (now() - startedAt));
|
|
1731
|
+
}
|
|
1732
|
+
};
|
|
1733
|
+
}
|
|
1734
|
+
async function withRequestTimeout(url, run, budgetMs = REQUEST_TIMEOUT_MS) {
|
|
1735
|
+
let timer;
|
|
1736
|
+
try {
|
|
1737
|
+
return await Promise.race([
|
|
1738
|
+
run(),
|
|
1739
|
+
new Promise((_resolve, reject) => {
|
|
1740
|
+
timer = setTimeout(() => reject(new RequestTimeoutError(url, budgetMs)), budgetMs);
|
|
1741
|
+
timer.unref?.();
|
|
1742
|
+
})
|
|
1743
|
+
]);
|
|
1744
|
+
} finally {
|
|
1745
|
+
if (timer) clearTimeout(timer);
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
async function mapWithConcurrency(items, limit, fn) {
|
|
1749
|
+
const results = new Array(items.length);
|
|
1750
|
+
const width = Math.max(1, Math.min(limit, items.length));
|
|
1751
|
+
let next = 0;
|
|
1752
|
+
async function worker() {
|
|
1753
|
+
for (; ; ) {
|
|
1754
|
+
const index = next++;
|
|
1755
|
+
if (index >= items.length) return;
|
|
1756
|
+
try {
|
|
1757
|
+
results[index] = { ok: true, value: await fn(items[index], index) };
|
|
1758
|
+
} catch (error) {
|
|
1759
|
+
results[index] = { ok: false, error };
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
await Promise.all(Array.from({ length: width }, () => worker()));
|
|
1764
|
+
return results;
|
|
1765
|
+
}
|
|
1766
|
+
var REQUEST_TIMEOUT_MS, SYNC_DEADLINE_MS, PUSH_CONCURRENCY, SyncDeadlineExceededError, RequestTimeoutError;
|
|
1767
|
+
var init_bounded_sync = __esm({
|
|
1768
|
+
"src/tools/memory/bounded-sync.ts"() {
|
|
1769
|
+
"use strict";
|
|
1770
|
+
REQUEST_TIMEOUT_MS = 15e3;
|
|
1771
|
+
SYNC_DEADLINE_MS = 12e4;
|
|
1772
|
+
PUSH_CONCURRENCY = 6;
|
|
1773
|
+
SyncDeadlineExceededError = class extends Error {
|
|
1774
|
+
constructor(elapsedMs, budgetMs) {
|
|
1775
|
+
super(
|
|
1776
|
+
`memory sync exceeded its ${budgetMs}ms deadline after ${elapsedMs}ms \u2014 aborting so the lock is released instead of held indefinitely`
|
|
1777
|
+
);
|
|
1778
|
+
this.name = "SyncDeadlineExceededError";
|
|
1779
|
+
}
|
|
1780
|
+
};
|
|
1781
|
+
RequestTimeoutError = class extends Error {
|
|
1782
|
+
constructor(url, budgetMs) {
|
|
1783
|
+
super(`memory sync request to ${url} exceeded ${budgetMs}ms`);
|
|
1784
|
+
this.name = "RequestTimeoutError";
|
|
1785
|
+
}
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1788
|
+
});
|
|
1789
|
+
|
|
1790
|
+
// src/tools/memory/memory-push-cache.ts
|
|
1791
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
1792
|
+
import { readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "node:fs";
|
|
1793
|
+
import { join as join9 } from "node:path";
|
|
1794
|
+
function sha256(content) {
|
|
1795
|
+
return createHash3("sha256").update(content, "utf8").digest("hex");
|
|
1796
|
+
}
|
|
1797
|
+
function statePath(memoryDir) {
|
|
1798
|
+
return join9(memoryDir, MEMORY_SYNC_STATE_FILE);
|
|
1799
|
+
}
|
|
1800
|
+
function readPushCache(memoryDir, controlPlaneUrl) {
|
|
1801
|
+
const empty = { controlPlaneUrl, entries: /* @__PURE__ */ new Map(), knowledgeSweptAtMs: null };
|
|
1802
|
+
let raw;
|
|
1803
|
+
try {
|
|
1804
|
+
raw = readFileSync10(statePath(memoryDir), "utf8");
|
|
1805
|
+
} catch {
|
|
1806
|
+
return empty;
|
|
1807
|
+
}
|
|
1808
|
+
let parsed;
|
|
1809
|
+
try {
|
|
1810
|
+
parsed = JSON.parse(raw);
|
|
1811
|
+
} catch {
|
|
1812
|
+
return empty;
|
|
1813
|
+
}
|
|
1814
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return empty;
|
|
1815
|
+
const obj = parsed;
|
|
1816
|
+
if (obj["version"] !== STATE_VERSION) return empty;
|
|
1817
|
+
if (obj["controlPlaneUrl"] !== controlPlaneUrl) return empty;
|
|
1818
|
+
const files = obj["entries"];
|
|
1819
|
+
if (typeof files !== "object" || files === null || Array.isArray(files)) return empty;
|
|
1820
|
+
const entries = /* @__PURE__ */ new Map();
|
|
1821
|
+
for (const [name, value] of Object.entries(files)) {
|
|
1822
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) continue;
|
|
1823
|
+
const row = value;
|
|
1824
|
+
const memoryHash = typeof row["memoryHash"] === "string" ? row["memoryHash"] : void 0;
|
|
1825
|
+
const knowledgeHash = typeof row["knowledgeHash"] === "string" ? row["knowledgeHash"] : void 0;
|
|
1826
|
+
if (memoryHash === void 0 && knowledgeHash === void 0) continue;
|
|
1827
|
+
entries.set(name, {
|
|
1828
|
+
...memoryHash !== void 0 ? { memoryHash } : {},
|
|
1829
|
+
...knowledgeHash !== void 0 ? { knowledgeHash } : {}
|
|
1830
|
+
});
|
|
1831
|
+
}
|
|
1832
|
+
const sweptAt = obj["knowledgeSweptAtMs"];
|
|
1833
|
+
return {
|
|
1834
|
+
controlPlaneUrl,
|
|
1835
|
+
entries,
|
|
1836
|
+
// An unreadable/absent sweep stamp reads as NEVER SWEPT, which forces a full
|
|
1837
|
+
// sweep — the fail-closed direction (more upserts, never fewer).
|
|
1838
|
+
knowledgeSweptAtMs: typeof sweptAt === "number" && Number.isFinite(sweptAt) ? sweptAt : null
|
|
1839
|
+
};
|
|
1840
|
+
}
|
|
1841
|
+
function writePushCache(memoryDir, cache) {
|
|
1842
|
+
const entries = {};
|
|
1843
|
+
for (const [name, value] of cache.entries) entries[name] = value;
|
|
1844
|
+
try {
|
|
1845
|
+
writeFileSync5(
|
|
1846
|
+
statePath(memoryDir),
|
|
1847
|
+
`${JSON.stringify(
|
|
1848
|
+
{
|
|
1849
|
+
version: STATE_VERSION,
|
|
1850
|
+
controlPlaneUrl: cache.controlPlaneUrl,
|
|
1851
|
+
knowledgeSweptAtMs: cache.knowledgeSweptAtMs,
|
|
1852
|
+
entries
|
|
1853
|
+
},
|
|
1854
|
+
null,
|
|
1855
|
+
2
|
|
1856
|
+
)}
|
|
1857
|
+
`,
|
|
1858
|
+
"utf8"
|
|
1859
|
+
);
|
|
1860
|
+
} catch {
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
function recordMemoryPush(cache, fileName, payloadHash) {
|
|
1864
|
+
cache.entries.set(fileName, { ...cache.entries.get(fileName), memoryHash: payloadHash });
|
|
1865
|
+
}
|
|
1866
|
+
function recordKnowledgePush(cache, fileName, contentHash) {
|
|
1867
|
+
cache.entries.set(fileName, { ...cache.entries.get(fileName), knowledgeHash: contentHash });
|
|
1868
|
+
}
|
|
1869
|
+
function pruneMissing(cache, presentFileNames) {
|
|
1870
|
+
const present = new Set(presentFileNames);
|
|
1871
|
+
for (const name of [...cache.entries.keys()]) {
|
|
1872
|
+
if (!present.has(name)) cache.entries.delete(name);
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
function needsMemoryPush(cache, fileName, payloadHash, serverHasEntry) {
|
|
1876
|
+
if (!serverHasEntry) return true;
|
|
1877
|
+
return cache.entries.get(fileName)?.memoryHash !== payloadHash;
|
|
1878
|
+
}
|
|
1879
|
+
function knowledgeSweepDue(cache, nowMs = Date.now()) {
|
|
1880
|
+
const swept = cache.knowledgeSweptAtMs;
|
|
1881
|
+
if (swept === null || !Number.isFinite(swept)) return true;
|
|
1882
|
+
const age = nowMs - swept;
|
|
1883
|
+
return !(age >= 0 && age < KNOWLEDGE_FULL_SWEEP_MS);
|
|
1884
|
+
}
|
|
1885
|
+
function needsKnowledgePush(cache, fileName, contentHash, sweepDue = false) {
|
|
1886
|
+
if (sweepDue) return true;
|
|
1887
|
+
return cache.entries.get(fileName)?.knowledgeHash !== contentHash;
|
|
1888
|
+
}
|
|
1889
|
+
var MEMORY_SYNC_STATE_FILE, STATE_VERSION, KNOWLEDGE_FULL_SWEEP_MS;
|
|
1890
|
+
var init_memory_push_cache = __esm({
|
|
1891
|
+
"src/tools/memory/memory-push-cache.ts"() {
|
|
1892
|
+
"use strict";
|
|
1893
|
+
MEMORY_SYNC_STATE_FILE = ".memory-sync-state.json";
|
|
1894
|
+
STATE_VERSION = 1;
|
|
1895
|
+
KNOWLEDGE_FULL_SWEEP_MS = 24 * 60 * 6e4;
|
|
1896
|
+
}
|
|
1897
|
+
});
|
|
1898
|
+
|
|
1899
|
+
// src/tools/memory/memory-sync-http.ts
|
|
1900
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync6, readdirSync as readdirSync4 } from "node:fs";
|
|
1901
|
+
async function pullMemory(controlPlaneUrl, token, memoryDir, fetchFn) {
|
|
1902
|
+
const url = `${controlPlaneUrl}/api/v1/agent-config/memory/me`;
|
|
1903
|
+
const response = await withRequestTimeout(
|
|
1904
|
+
url,
|
|
1905
|
+
() => fetchFn(url, {
|
|
1906
|
+
method: "GET",
|
|
1907
|
+
headers: {
|
|
1908
|
+
authorization: `Bearer ${token}`
|
|
1909
|
+
}
|
|
1910
|
+
})
|
|
1911
|
+
);
|
|
1912
|
+
if (response.status !== 200) {
|
|
1913
|
+
const text = await response.text();
|
|
1914
|
+
throw new Error(`GET /api/v1/agent-config/memory/me returned HTTP ${response.status}: ${text.slice(0, 200)}`);
|
|
1915
|
+
}
|
|
1916
|
+
const data = JSON.parse(await response.text());
|
|
1917
|
+
if (!data.ok || !Array.isArray(data.entries)) {
|
|
1918
|
+
throw new Error("GET /api/v1/agent-config/memory/me response missing ok=true or entries array");
|
|
1919
|
+
}
|
|
1920
|
+
const writes = data.entries.map((entry) => ({
|
|
1921
|
+
entry,
|
|
1922
|
+
filePath: resolveMemoryFilePath(memoryDir, entry.file_name)
|
|
1923
|
+
}));
|
|
1924
|
+
mkdirSync6(memoryDir, { recursive: true });
|
|
1925
|
+
const files = [];
|
|
1926
|
+
for (const { entry, filePath } of writes) {
|
|
1927
|
+
writeFileSync6(filePath, entry.content, "utf8");
|
|
1928
|
+
files.push(entry.file_name);
|
|
1929
|
+
}
|
|
1930
|
+
return { pulled: data.entries.length, files };
|
|
1931
|
+
}
|
|
1932
|
+
function listPushableFiles(memoryDir) {
|
|
1933
|
+
return readdirSync4(memoryDir).filter((f) => f.endsWith(".md") && f !== MEMORY_SYNC_LOCK_FILE);
|
|
1934
|
+
}
|
|
1935
|
+
async function uploadOne(item, controlPlaneUrl, token, sessionId, fetchFn, deadline) {
|
|
1936
|
+
deadline.check();
|
|
1937
|
+
if (item.memoryId !== null) {
|
|
1938
|
+
const updateUrl = `${controlPlaneUrl}/api/v1/agent-config/memory/${item.memoryId}`;
|
|
1939
|
+
const updateBody = { content: item.content, session_id: sessionId };
|
|
1940
|
+
const updateResponse = await withRequestTimeout(
|
|
1941
|
+
updateUrl,
|
|
1942
|
+
() => fetchFn(updateUrl, {
|
|
1943
|
+
method: "PUT",
|
|
1944
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
1945
|
+
body: JSON.stringify(updateBody)
|
|
1946
|
+
})
|
|
1947
|
+
);
|
|
1948
|
+
if (updateResponse.status !== 200) {
|
|
1949
|
+
const text = await updateResponse.text();
|
|
1950
|
+
throw new Error(
|
|
1951
|
+
`PUT /api/v1/agent-config/memory/${item.memoryId} returned HTTP ${updateResponse.status}: ${text.slice(0, 200)}`
|
|
1952
|
+
);
|
|
1953
|
+
}
|
|
1954
|
+
const updateData = JSON.parse(await updateResponse.text());
|
|
1955
|
+
if (!updateData.ok) throw new Error(`PUT /api/v1/agent-config/memory/${item.memoryId} returned ok=false`);
|
|
1956
|
+
return "updated";
|
|
1957
|
+
}
|
|
1958
|
+
const createUrl = `${controlPlaneUrl}/api/v1/agent-config/memory/me`;
|
|
1959
|
+
const createBody = {
|
|
1960
|
+
entry_type: item.entryType,
|
|
1961
|
+
file_name: item.fileName,
|
|
1962
|
+
content: item.content,
|
|
1963
|
+
session_id: sessionId
|
|
1964
|
+
};
|
|
1965
|
+
const createResponse = await withRequestTimeout(
|
|
1966
|
+
createUrl,
|
|
1967
|
+
() => fetchFn(createUrl, {
|
|
1968
|
+
method: "POST",
|
|
1969
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
1970
|
+
body: JSON.stringify(createBody)
|
|
1971
|
+
})
|
|
1972
|
+
);
|
|
1973
|
+
if (createResponse.status !== 200 && createResponse.status !== 201) {
|
|
1974
|
+
const text = await createResponse.text();
|
|
1975
|
+
throw new Error(
|
|
1976
|
+
`POST /api/v1/agent-config/memory/me returned HTTP ${createResponse.status}: ${text.slice(0, 200)}`
|
|
1977
|
+
);
|
|
1978
|
+
}
|
|
1979
|
+
const createData = JSON.parse(await createResponse.text());
|
|
1980
|
+
if (!createData.ok) throw new Error("POST /api/v1/agent-config/memory/me returned ok=false");
|
|
1981
|
+
return "created";
|
|
1982
|
+
}
|
|
1983
|
+
async function pushMemory(controlPlaneUrl, token, memoryDir, sessionId, fetchFn, options = {}) {
|
|
1984
|
+
const empty = { pushed: 0, created: 0, updated: 0, skipped: 0, indexRowsPreserved: 0 };
|
|
1985
|
+
if (!existsSync5(memoryDir)) {
|
|
1986
|
+
return empty;
|
|
1987
|
+
}
|
|
1988
|
+
const localFiles = listPushableFiles(memoryDir).map((f) => ({
|
|
1989
|
+
file_name: f,
|
|
1990
|
+
content: readFileSync11(resolveMemoryFilePath(memoryDir, f), "utf8"),
|
|
1991
|
+
entry_type: isMemoryIndexFile(f) ? "index" : "topic"
|
|
1992
|
+
}));
|
|
1993
|
+
if (localFiles.length === 0) {
|
|
1994
|
+
return empty;
|
|
1995
|
+
}
|
|
1996
|
+
const deadline = options.deadline ?? createSyncDeadline();
|
|
1997
|
+
const cache = options.cache ?? readPushCache(memoryDir, controlPlaneUrl);
|
|
1998
|
+
deadline.check();
|
|
1999
|
+
const getUrl = `${controlPlaneUrl}/api/v1/agent-config/memory/me`;
|
|
2000
|
+
const getResponse = await withRequestTimeout(
|
|
2001
|
+
getUrl,
|
|
2002
|
+
() => fetchFn(getUrl, { method: "GET", headers: { authorization: `Bearer ${token}` } })
|
|
2003
|
+
);
|
|
2004
|
+
const existingMap = /* @__PURE__ */ new Map();
|
|
2005
|
+
if (getResponse.status === 200) {
|
|
2006
|
+
const getData = JSON.parse(await getResponse.text());
|
|
2007
|
+
if (getData.ok && Array.isArray(getData.entries)) {
|
|
2008
|
+
for (const entry of getData.entries) {
|
|
2009
|
+
existingMap.set(entry.file_name, {
|
|
2010
|
+
memoryId: entry.memory_id,
|
|
2011
|
+
content: typeof entry.content === "string" ? entry.content : ""
|
|
2012
|
+
});
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
const toUpload = [];
|
|
2017
|
+
let skipped = 0;
|
|
2018
|
+
for (const localFile of localFiles) {
|
|
2019
|
+
const existing = existingMap.get(localFile.file_name);
|
|
2020
|
+
let content = localFile.content;
|
|
2021
|
+
let rowsPreserved = 0;
|
|
2022
|
+
if (localFile.entry_type === "index") {
|
|
2023
|
+
const merged = mergeMemoryIndex(localFile.content, existing?.content);
|
|
2024
|
+
content = merged.content;
|
|
2025
|
+
rowsPreserved = merged.addedFromCloud.length;
|
|
2026
|
+
}
|
|
2027
|
+
const payloadHash = sha256(content);
|
|
2028
|
+
if (!needsMemoryPush(cache, localFile.file_name, payloadHash, existing !== void 0)) {
|
|
2029
|
+
skipped++;
|
|
2030
|
+
continue;
|
|
2031
|
+
}
|
|
2032
|
+
toUpload.push({
|
|
2033
|
+
fileName: localFile.file_name,
|
|
2034
|
+
entryType: localFile.entry_type,
|
|
2035
|
+
content,
|
|
2036
|
+
payloadHash,
|
|
2037
|
+
memoryId: existing?.memoryId ?? null,
|
|
2038
|
+
rowsPreserved
|
|
2039
|
+
});
|
|
2040
|
+
}
|
|
2041
|
+
const outcomes = await mapWithConcurrency(
|
|
2042
|
+
toUpload,
|
|
2043
|
+
options.concurrency ?? PUSH_CONCURRENCY,
|
|
2044
|
+
(item) => uploadOne(item, controlPlaneUrl, token, sessionId, fetchFn, deadline)
|
|
2045
|
+
);
|
|
2046
|
+
let created = 0;
|
|
2047
|
+
let updated = 0;
|
|
2048
|
+
let indexRowsPreserved = 0;
|
|
2049
|
+
let firstError;
|
|
2050
|
+
for (let i = 0; i < outcomes.length; i++) {
|
|
2051
|
+
const outcome = outcomes[i];
|
|
2052
|
+
const item = toUpload[i];
|
|
2053
|
+
if (outcome.ok) {
|
|
2054
|
+
if (outcome.value === "created") created++;
|
|
2055
|
+
else updated++;
|
|
2056
|
+
indexRowsPreserved += item.rowsPreserved;
|
|
2057
|
+
recordMemoryPush(cache, item.fileName, item.payloadHash);
|
|
2058
|
+
} else if (firstError === void 0) {
|
|
2059
|
+
firstError = outcome.error;
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
pruneMissing(cache, localFiles.map((f) => f.file_name));
|
|
2063
|
+
if (options.persistCache ?? options.cache === void 0) writePushCache(memoryDir, cache);
|
|
2064
|
+
if (firstError !== void 0) throw firstError;
|
|
2065
|
+
return { pushed: created + updated, created, updated, skipped, indexRowsPreserved };
|
|
2066
|
+
}
|
|
2067
|
+
var init_memory_sync_http = __esm({
|
|
2068
|
+
"src/tools/memory/memory-sync-http.ts"() {
|
|
2069
|
+
"use strict";
|
|
2070
|
+
init_safe_memory_file();
|
|
2071
|
+
init_sync_lock();
|
|
2072
|
+
init_memory_index_merge();
|
|
2073
|
+
init_bounded_sync();
|
|
2074
|
+
init_memory_push_cache();
|
|
2075
|
+
}
|
|
2076
|
+
});
|
|
2077
|
+
|
|
2078
|
+
// src/tools/memory/sync-kill-switch.ts
|
|
2079
|
+
import { existsSync as existsSync6, readFileSync as readFileSync12 } from "node:fs";
|
|
2080
|
+
import { homedir as homedir6 } from "node:os";
|
|
2081
|
+
import { join as join10 } from "node:path";
|
|
2082
|
+
function memorySyncSentinelPath(home) {
|
|
2083
|
+
return join10(home, ".claude", MEMORY_SYNC_DISABLE_SENTINEL);
|
|
2084
|
+
}
|
|
2085
|
+
function isKillSwitchValueOn(raw) {
|
|
2086
|
+
if (raw === void 0 || raw === null) return false;
|
|
2087
|
+
const v = raw.trim().toLowerCase();
|
|
2088
|
+
if (v === "") return false;
|
|
2089
|
+
return !NEGATIONS.has(v);
|
|
2090
|
+
}
|
|
2091
|
+
function clip(raw) {
|
|
2092
|
+
const v = raw.trim();
|
|
2093
|
+
return v.length > MAX_LOGGED_VALUE ? `${v.slice(0, MAX_LOGGED_VALUE)}\u2026` : v;
|
|
2094
|
+
}
|
|
2095
|
+
function evaluateMemorySyncKillSwitch(deps = {}) {
|
|
2096
|
+
const env = deps.env ?? process.env;
|
|
2097
|
+
const home = deps.home ?? homedir6();
|
|
2098
|
+
const fileExists = deps.fileExists ?? existsSync6;
|
|
2099
|
+
const readFile3 = deps.readFile ?? ((p) => readFileSync12(p, "utf8"));
|
|
2100
|
+
const fired = [];
|
|
2101
|
+
const rawEnv = env[MEMORY_SYNC_DISABLE_ENV];
|
|
2102
|
+
if (isKillSwitchValueOn(rawEnv)) {
|
|
2103
|
+
fired.push(`env ${MEMORY_SYNC_DISABLE_ENV}=${clip(rawEnv)}`);
|
|
2104
|
+
}
|
|
2105
|
+
const sentinel = memorySyncSentinelPath(home);
|
|
2106
|
+
let sentinelPresent;
|
|
2107
|
+
try {
|
|
2108
|
+
sentinelPresent = fileExists(sentinel);
|
|
2109
|
+
} catch {
|
|
2110
|
+
sentinelPresent = false;
|
|
2111
|
+
}
|
|
2112
|
+
if (sentinelPresent) {
|
|
2113
|
+
let contents = "";
|
|
2114
|
+
let readable = true;
|
|
2115
|
+
try {
|
|
2116
|
+
contents = readFile3(sentinel);
|
|
2117
|
+
} catch {
|
|
2118
|
+
readable = false;
|
|
2119
|
+
}
|
|
2120
|
+
if (!readable || isKillSwitchValueOn(contents) || contents.trim() === "") {
|
|
2121
|
+
fired.push(`sentinel file ${sentinel}`);
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
if (fired.length === 0) return { disabled: false, reason: null };
|
|
2125
|
+
return { disabled: true, reason: `memory sync DISABLED by ${fired.join(" + ")}` };
|
|
2126
|
+
}
|
|
2127
|
+
var MEMORY_SYNC_DISABLE_ENV, MEMORY_SYNC_DISABLE_SENTINEL, NEGATIONS, MAX_LOGGED_VALUE;
|
|
2128
|
+
var init_sync_kill_switch = __esm({
|
|
2129
|
+
"src/tools/memory/sync-kill-switch.ts"() {
|
|
2130
|
+
"use strict";
|
|
2131
|
+
MEMORY_SYNC_DISABLE_ENV = "VO_MCP_DISABLE_MEMORY_SYNC";
|
|
2132
|
+
MEMORY_SYNC_DISABLE_SENTINEL = "vo-memory-sync-disabled";
|
|
2133
|
+
NEGATIONS = /* @__PURE__ */ new Set(["0", "false", "no"]);
|
|
2134
|
+
MAX_LOGGED_VALUE = 32;
|
|
2135
|
+
}
|
|
2136
|
+
});
|
|
2137
|
+
|
|
1402
2138
|
// src/tools/memory/memory-knowledge-bridge.ts
|
|
1403
2139
|
var memory_knowledge_bridge_exports = {};
|
|
1404
2140
|
__export(memory_knowledge_bridge_exports, {
|
|
1405
2141
|
extractMemoryTitle: () => extractMemoryTitle,
|
|
1406
2142
|
upsertMemoryFilesAsKnowledge: () => upsertMemoryFilesAsKnowledge
|
|
1407
2143
|
});
|
|
1408
|
-
import { existsSync as
|
|
2144
|
+
import { existsSync as existsSync7, readdirSync as readdirSync5, readFileSync as readFileSync13 } from "node:fs";
|
|
1409
2145
|
function extractMemoryTitle(fileName, content) {
|
|
1410
2146
|
const frontmatter = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
1411
2147
|
if (frontmatter) {
|
|
@@ -1417,13 +2153,13 @@ function extractMemoryTitle(fileName, content) {
|
|
|
1417
2153
|
return fileName;
|
|
1418
2154
|
}
|
|
1419
2155
|
async function upsertMemoryFilesAsKnowledge(options) {
|
|
1420
|
-
const { controlPlaneUrl, token, memoryDir, fetchFn } = options;
|
|
2156
|
+
const { controlPlaneUrl, token, memoryDir, fetchFn, cache, deadline } = options;
|
|
1421
2157
|
let files;
|
|
1422
2158
|
try {
|
|
1423
|
-
if (!
|
|
1424
|
-
return { attempted: 0, upserted: 0, failed: 0, failures: [] };
|
|
2159
|
+
if (!existsSync7(memoryDir)) {
|
|
2160
|
+
return { attempted: 0, upserted: 0, failed: 0, skipped: 0, failures: [] };
|
|
1425
2161
|
}
|
|
1426
|
-
files =
|
|
2162
|
+
files = readdirSync5(memoryDir).filter(
|
|
1427
2163
|
(f) => f.endsWith(".md") && f.toUpperCase() !== "MEMORY.MD"
|
|
1428
2164
|
);
|
|
1429
2165
|
} catch (err) {
|
|
@@ -1431,54 +2167,81 @@ async function upsertMemoryFilesAsKnowledge(options) {
|
|
|
1431
2167
|
attempted: 0,
|
|
1432
2168
|
upserted: 0,
|
|
1433
2169
|
failed: 1,
|
|
2170
|
+
skipped: 0,
|
|
1434
2171
|
failures: [`memory dir scan: ${err instanceof Error ? err.message : String(err)}`]
|
|
1435
2172
|
};
|
|
1436
2173
|
}
|
|
1437
|
-
|
|
2174
|
+
const sweepDue = cache ? knowledgeSweepDue(cache) : true;
|
|
2175
|
+
const candidates = [];
|
|
1438
2176
|
const failures = [];
|
|
2177
|
+
let skipped = 0;
|
|
1439
2178
|
for (const fileName of files) {
|
|
1440
2179
|
try {
|
|
1441
|
-
const content =
|
|
2180
|
+
const content = readFileSync13(resolveMemoryFilePath(memoryDir, fileName), "utf8");
|
|
1442
2181
|
if (content.length > CONTENT_HARD_LIMIT) {
|
|
1443
2182
|
failures.push(`${fileName}: ${content.length} chars exceeds the ${CONTENT_HARD_LIMIT} server limit \u2014 split the memory file`);
|
|
1444
2183
|
continue;
|
|
1445
2184
|
}
|
|
1446
|
-
const
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
title,
|
|
1451
|
-
content
|
|
1452
|
-
};
|
|
1453
|
-
const post = (body) => fetchFn(`${controlPlaneUrl}/api/v1/knowledge/private`, {
|
|
1454
|
-
method: "POST",
|
|
1455
|
-
headers: {
|
|
1456
|
-
authorization: `Bearer ${token}`,
|
|
1457
|
-
"content-type": "application/json"
|
|
1458
|
-
},
|
|
1459
|
-
body: JSON.stringify(body)
|
|
1460
|
-
});
|
|
1461
|
-
let response = await post({
|
|
1462
|
-
...base,
|
|
1463
|
-
provenance: { written_by: "memory-bridge", source_kind: "operator_memory" }
|
|
1464
|
-
});
|
|
1465
|
-
if (response.status === 400) {
|
|
1466
|
-
response = await post(base);
|
|
1467
|
-
}
|
|
1468
|
-
if (response.status >= 200 && response.status < 300) {
|
|
1469
|
-
upserted += 1;
|
|
1470
|
-
} else {
|
|
1471
|
-
const text = await response.text();
|
|
1472
|
-
failures.push(`${fileName}: HTTP ${response.status} ${text.slice(0, 80)}`);
|
|
2185
|
+
const hash = sha256(content);
|
|
2186
|
+
if (cache && !needsKnowledgePush(cache, fileName, hash, sweepDue)) {
|
|
2187
|
+
skipped += 1;
|
|
2188
|
+
continue;
|
|
1473
2189
|
}
|
|
2190
|
+
candidates.push({ fileName, content, hash });
|
|
1474
2191
|
} catch (err) {
|
|
1475
2192
|
failures.push(`${fileName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1476
2193
|
}
|
|
1477
2194
|
}
|
|
2195
|
+
const url = `${controlPlaneUrl}/api/v1/knowledge/private`;
|
|
2196
|
+
const outcomes = await mapWithConcurrency(candidates, options.concurrency ?? PUSH_CONCURRENCY, async (candidate) => {
|
|
2197
|
+
deadline?.check();
|
|
2198
|
+
const base = {
|
|
2199
|
+
knowledge_class: "memory",
|
|
2200
|
+
source_path: `memory/${candidate.fileName}`,
|
|
2201
|
+
title: extractMemoryTitle(candidate.fileName, candidate.content),
|
|
2202
|
+
content: candidate.content
|
|
2203
|
+
};
|
|
2204
|
+
const post = (body) => withRequestTimeout(url, () => fetchFn(url, {
|
|
2205
|
+
method: "POST",
|
|
2206
|
+
headers: {
|
|
2207
|
+
authorization: `Bearer ${token}`,
|
|
2208
|
+
"content-type": "application/json"
|
|
2209
|
+
},
|
|
2210
|
+
body: JSON.stringify(body)
|
|
2211
|
+
}));
|
|
2212
|
+
let response = await post({
|
|
2213
|
+
...base,
|
|
2214
|
+
provenance: { written_by: "memory-bridge", source_kind: "operator_memory" }
|
|
2215
|
+
});
|
|
2216
|
+
if (response.status === 400) {
|
|
2217
|
+
response = await post(base);
|
|
2218
|
+
}
|
|
2219
|
+
if (response.status >= 200 && response.status < 300) return true;
|
|
2220
|
+
const text = await response.text();
|
|
2221
|
+
throw new Error(`HTTP ${response.status} ${text.slice(0, 80)}`);
|
|
2222
|
+
});
|
|
2223
|
+
let upserted = 0;
|
|
2224
|
+
for (let i = 0; i < outcomes.length; i++) {
|
|
2225
|
+
const outcome = outcomes[i];
|
|
2226
|
+
const candidate = candidates[i];
|
|
2227
|
+
if (outcome.ok) {
|
|
2228
|
+
upserted += 1;
|
|
2229
|
+
if (cache) recordKnowledgePush(cache, candidate.fileName, candidate.hash);
|
|
2230
|
+
} else {
|
|
2231
|
+
const error = outcome.error;
|
|
2232
|
+
failures.push(`${candidate.fileName}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
if (cache && sweepDue && failures.length === 0) {
|
|
2236
|
+
cache.knowledgeSweptAtMs = Date.now();
|
|
2237
|
+
}
|
|
1478
2238
|
return {
|
|
2239
|
+
// Every memory file this run considered. `attempted === upserted + skipped
|
|
2240
|
+
// + failed` holds, so a caller can tell "nothing to do" from "nothing ran".
|
|
1479
2241
|
attempted: files.length,
|
|
1480
2242
|
upserted,
|
|
1481
2243
|
failed: failures.length,
|
|
2244
|
+
skipped,
|
|
1482
2245
|
failures: failures.slice(0, 5)
|
|
1483
2246
|
};
|
|
1484
2247
|
}
|
|
@@ -1487,6 +2250,8 @@ var init_memory_knowledge_bridge = __esm({
|
|
|
1487
2250
|
"src/tools/memory/memory-knowledge-bridge.ts"() {
|
|
1488
2251
|
"use strict";
|
|
1489
2252
|
init_safe_memory_file();
|
|
2253
|
+
init_bounded_sync();
|
|
2254
|
+
init_memory_push_cache();
|
|
1490
2255
|
CONTENT_HARD_LIMIT = 5e5;
|
|
1491
2256
|
}
|
|
1492
2257
|
});
|
|
@@ -1503,9 +2268,9 @@ __export(sync_config_exports, {
|
|
|
1503
2268
|
isNoopSyncReason: () => isNoopSyncReason,
|
|
1504
2269
|
runMemorySync: () => runMemorySync
|
|
1505
2270
|
});
|
|
1506
|
-
import {
|
|
1507
|
-
import {
|
|
1508
|
-
import {
|
|
2271
|
+
import { existsSync as existsSync8 } from "node:fs";
|
|
2272
|
+
import { homedir as homedir7 } from "node:os";
|
|
2273
|
+
import { join as join11 } from "node:path";
|
|
1509
2274
|
function isToolInput22(v) {
|
|
1510
2275
|
if (typeof v !== "object" || v === null) return false;
|
|
1511
2276
|
const o = v;
|
|
@@ -1518,129 +2283,17 @@ function deriveProjectSlug(cwd) {
|
|
|
1518
2283
|
}
|
|
1519
2284
|
function getMemoryDir(cwd) {
|
|
1520
2285
|
const slug = deriveProjectSlug(cwd);
|
|
1521
|
-
return
|
|
1522
|
-
}
|
|
1523
|
-
async function pullMemory(controlPlaneUrl, token, memoryDir, fetchFn) {
|
|
1524
|
-
const url = `${controlPlaneUrl}/api/v1/agent-config/memory/me`;
|
|
1525
|
-
const response = await fetchFn(url, {
|
|
1526
|
-
method: "GET",
|
|
1527
|
-
headers: {
|
|
1528
|
-
authorization: `Bearer ${token}`
|
|
1529
|
-
}
|
|
1530
|
-
});
|
|
1531
|
-
if (response.status !== 200) {
|
|
1532
|
-
const text = await response.text();
|
|
1533
|
-
throw new Error(`GET /api/v1/agent-config/memory/me returned HTTP ${response.status}: ${text.slice(0, 200)}`);
|
|
1534
|
-
}
|
|
1535
|
-
const data = JSON.parse(await response.text());
|
|
1536
|
-
if (!data.ok || !Array.isArray(data.entries)) {
|
|
1537
|
-
throw new Error("GET /api/v1/agent-config/memory/me response missing ok=true or entries array");
|
|
1538
|
-
}
|
|
1539
|
-
const writes = data.entries.map((entry) => ({
|
|
1540
|
-
entry,
|
|
1541
|
-
filePath: resolveMemoryFilePath(memoryDir, entry.file_name)
|
|
1542
|
-
}));
|
|
1543
|
-
mkdirSync5(memoryDir, { recursive: true });
|
|
1544
|
-
const files = [];
|
|
1545
|
-
for (const { entry, filePath } of writes) {
|
|
1546
|
-
writeFileSync4(filePath, entry.content, "utf8");
|
|
1547
|
-
files.push(entry.file_name);
|
|
1548
|
-
}
|
|
1549
|
-
return { pulled: data.entries.length, files };
|
|
1550
|
-
}
|
|
1551
|
-
async function pushMemory(controlPlaneUrl, token, memoryDir, sessionId, fetchFn) {
|
|
1552
|
-
if (!existsSync6(memoryDir)) {
|
|
1553
|
-
return { pushed: 0, created: 0, updated: 0 };
|
|
1554
|
-
}
|
|
1555
|
-
const localFiles = readdirSync5(memoryDir).filter((f) => f.endsWith(".md")).map((f) => ({
|
|
1556
|
-
file_name: f,
|
|
1557
|
-
content: readFileSync9(resolveMemoryFilePath(memoryDir, f), "utf8"),
|
|
1558
|
-
entry_type: f === "MEMORY.md" ? "index" : "topic"
|
|
1559
|
-
}));
|
|
1560
|
-
if (localFiles.length === 0) {
|
|
1561
|
-
return { pushed: 0, created: 0, updated: 0 };
|
|
1562
|
-
}
|
|
1563
|
-
const getUrl = `${controlPlaneUrl}/api/v1/agent-config/memory/me`;
|
|
1564
|
-
const getResponse = await fetchFn(getUrl, {
|
|
1565
|
-
method: "GET",
|
|
1566
|
-
headers: {
|
|
1567
|
-
authorization: `Bearer ${token}`
|
|
1568
|
-
}
|
|
1569
|
-
});
|
|
1570
|
-
const existingMap = /* @__PURE__ */ new Map();
|
|
1571
|
-
if (getResponse.status === 200) {
|
|
1572
|
-
const getData = JSON.parse(await getResponse.text());
|
|
1573
|
-
if (getData.ok && Array.isArray(getData.entries)) {
|
|
1574
|
-
for (const entry of getData.entries) {
|
|
1575
|
-
existingMap.set(entry.file_name, entry.memory_id);
|
|
1576
|
-
}
|
|
1577
|
-
}
|
|
1578
|
-
}
|
|
1579
|
-
let created = 0;
|
|
1580
|
-
let updated = 0;
|
|
1581
|
-
for (const localFile of localFiles) {
|
|
1582
|
-
const memoryId = existingMap.get(localFile.file_name);
|
|
1583
|
-
if (memoryId) {
|
|
1584
|
-
const updateUrl = `${controlPlaneUrl}/api/v1/agent-config/memory/${memoryId}`;
|
|
1585
|
-
const updateBody = {
|
|
1586
|
-
content: localFile.content,
|
|
1587
|
-
session_id: sessionId
|
|
1588
|
-
};
|
|
1589
|
-
const updateResponse = await fetchFn(updateUrl, {
|
|
1590
|
-
method: "PUT",
|
|
1591
|
-
headers: {
|
|
1592
|
-
authorization: `Bearer ${token}`,
|
|
1593
|
-
"content-type": "application/json"
|
|
1594
|
-
},
|
|
1595
|
-
body: JSON.stringify(updateBody)
|
|
1596
|
-
});
|
|
1597
|
-
if (updateResponse.status !== 200) {
|
|
1598
|
-
const text = await updateResponse.text();
|
|
1599
|
-
throw new Error(
|
|
1600
|
-
`PUT /api/v1/agent-config/memory/${memoryId} returned HTTP ${updateResponse.status}: ${text.slice(0, 200)}`
|
|
1601
|
-
);
|
|
1602
|
-
}
|
|
1603
|
-
const updateData = JSON.parse(await updateResponse.text());
|
|
1604
|
-
if (!updateData.ok) {
|
|
1605
|
-
throw new Error(`PUT /api/v1/agent-config/memory/${memoryId} returned ok=false`);
|
|
1606
|
-
}
|
|
1607
|
-
updated++;
|
|
1608
|
-
} else {
|
|
1609
|
-
const createUrl = `${controlPlaneUrl}/api/v1/agent-config/memory/me`;
|
|
1610
|
-
const createBody = {
|
|
1611
|
-
entry_type: localFile.entry_type,
|
|
1612
|
-
file_name: localFile.file_name,
|
|
1613
|
-
content: localFile.content,
|
|
1614
|
-
session_id: sessionId
|
|
1615
|
-
};
|
|
1616
|
-
const createResponse = await fetchFn(createUrl, {
|
|
1617
|
-
method: "POST",
|
|
1618
|
-
headers: {
|
|
1619
|
-
authorization: `Bearer ${token}`,
|
|
1620
|
-
"content-type": "application/json"
|
|
1621
|
-
},
|
|
1622
|
-
body: JSON.stringify(createBody)
|
|
1623
|
-
});
|
|
1624
|
-
if (createResponse.status !== 200 && createResponse.status !== 201) {
|
|
1625
|
-
const text = await createResponse.text();
|
|
1626
|
-
throw new Error(
|
|
1627
|
-
`POST /api/v1/agent-config/memory/me returned HTTP ${createResponse.status}: ${text.slice(0, 200)}`
|
|
1628
|
-
);
|
|
1629
|
-
}
|
|
1630
|
-
const createData = JSON.parse(await createResponse.text());
|
|
1631
|
-
if (!createData.ok) {
|
|
1632
|
-
throw new Error("POST /api/v1/agent-config/memory/me returned ok=false");
|
|
1633
|
-
}
|
|
1634
|
-
created++;
|
|
1635
|
-
}
|
|
1636
|
-
}
|
|
1637
|
-
return { pushed: localFiles.length, created, updated };
|
|
2286
|
+
return join11(homedir7(), ".claude", "projects", slug, "memory");
|
|
1638
2287
|
}
|
|
1639
2288
|
function isNoopSyncReason(reason) {
|
|
1640
2289
|
if (!reason) return false;
|
|
1641
|
-
return /not set|No auth configured|Failed to obtain auth token/.test(reason);
|
|
2290
|
+
return /not set|No auth configured|Failed to obtain auth token|DISABLED by/.test(reason);
|
|
1642
2291
|
}
|
|
1643
|
-
async function runMemorySync(action, cwd, sessionId, fetchFn = globalThis.fetch) {
|
|
2292
|
+
async function runMemorySync(action, cwd, sessionId, fetchFn = globalThis.fetch, lockOptions = {}) {
|
|
2293
|
+
const killSwitch = evaluateMemorySyncKillSwitch();
|
|
2294
|
+
if (killSwitch.disabled) {
|
|
2295
|
+
return { synced: false, reason: killSwitch.reason };
|
|
2296
|
+
}
|
|
1644
2297
|
const controlPlaneUrl = process.env["VO_CONTROL_PLANE_URL"];
|
|
1645
2298
|
if (!controlPlaneUrl) {
|
|
1646
2299
|
return { synced: false, reason: "VO_CONTROL_PLANE_URL not set \u2014 cloud mode disabled" };
|
|
@@ -1657,39 +2310,79 @@ async function runMemorySync(action, cwd, sessionId, fetchFn = globalThis.fetch)
|
|
|
1657
2310
|
}
|
|
1658
2311
|
const memoryDir = getMemoryDir(cwd);
|
|
1659
2312
|
const baseUrl = controlPlaneUrl.replace(/\/+$/, "");
|
|
1660
|
-
|
|
1661
|
-
if (action === "pull") {
|
|
1662
|
-
const result2 = await pullMemory(baseUrl, token, memoryDir, fetchFn);
|
|
1663
|
-
return { synced: true, action: "pull", pulled: result2.pulled, files: result2.files, memory_dir: memoryDir };
|
|
1664
|
-
}
|
|
1665
|
-
const result = await pushMemory(baseUrl, token, memoryDir, sessionId, fetchFn);
|
|
1666
|
-
let bridge = { upserted: 0, failed: 0, failures: [] };
|
|
1667
|
-
try {
|
|
1668
|
-
const { upsertMemoryFilesAsKnowledge: upsertMemoryFilesAsKnowledge2 } = await Promise.resolve().then(() => (init_memory_knowledge_bridge(), memory_knowledge_bridge_exports));
|
|
1669
|
-
bridge = await upsertMemoryFilesAsKnowledge2({
|
|
1670
|
-
controlPlaneUrl: baseUrl,
|
|
1671
|
-
token,
|
|
1672
|
-
memoryDir,
|
|
1673
|
-
fetchFn
|
|
1674
|
-
});
|
|
1675
|
-
} catch (err) {
|
|
1676
|
-
bridge = {
|
|
1677
|
-
upserted: 0,
|
|
1678
|
-
failed: 1,
|
|
1679
|
-
failures: [`bridge unavailable: ${err instanceof Error ? err.message : String(err)}`]
|
|
1680
|
-
};
|
|
1681
|
-
}
|
|
2313
|
+
if (action === "push" && !existsSync8(memoryDir)) {
|
|
1682
2314
|
return {
|
|
1683
2315
|
synced: true,
|
|
1684
2316
|
action: "push",
|
|
1685
|
-
pushed:
|
|
1686
|
-
created:
|
|
1687
|
-
updated:
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
2317
|
+
pushed: 0,
|
|
2318
|
+
created: 0,
|
|
2319
|
+
updated: 0,
|
|
2320
|
+
skipped: 0,
|
|
2321
|
+
index_rows_preserved: 0,
|
|
2322
|
+
knowledge_upserted: 0,
|
|
2323
|
+
knowledge_failed: 0,
|
|
2324
|
+
memory_dir: memoryDir
|
|
1692
2325
|
};
|
|
2326
|
+
}
|
|
2327
|
+
try {
|
|
2328
|
+
return await withMemorySyncLock({ ...lockOptions, memoryDir, sessionId, createDir: action === "pull" }, async (lock) => {
|
|
2329
|
+
const takeover = lock.tookOverFrom ? { lock_taken_over_from_pid: lock.tookOverFrom.pid } : {};
|
|
2330
|
+
if (action === "pull") {
|
|
2331
|
+
const result2 = await pullMemory(baseUrl, token, memoryDir, fetchFn);
|
|
2332
|
+
return {
|
|
2333
|
+
synced: true,
|
|
2334
|
+
action: "pull",
|
|
2335
|
+
pulled: result2.pulled,
|
|
2336
|
+
files: result2.files,
|
|
2337
|
+
memory_dir: memoryDir,
|
|
2338
|
+
...takeover
|
|
2339
|
+
};
|
|
2340
|
+
}
|
|
2341
|
+
const deadline = createSyncDeadline();
|
|
2342
|
+
const cache = readPushCache(memoryDir, baseUrl);
|
|
2343
|
+
let result;
|
|
2344
|
+
try {
|
|
2345
|
+
result = await pushMemory(baseUrl, token, memoryDir, sessionId, fetchFn, { cache, deadline });
|
|
2346
|
+
} catch (err) {
|
|
2347
|
+
writePushCache(memoryDir, cache);
|
|
2348
|
+
throw err;
|
|
2349
|
+
}
|
|
2350
|
+
let bridge;
|
|
2351
|
+
try {
|
|
2352
|
+
const { upsertMemoryFilesAsKnowledge: upsertMemoryFilesAsKnowledge2 } = await Promise.resolve().then(() => (init_memory_knowledge_bridge(), memory_knowledge_bridge_exports));
|
|
2353
|
+
bridge = await upsertMemoryFilesAsKnowledge2({
|
|
2354
|
+
controlPlaneUrl: baseUrl,
|
|
2355
|
+
token,
|
|
2356
|
+
memoryDir,
|
|
2357
|
+
fetchFn,
|
|
2358
|
+
cache,
|
|
2359
|
+
deadline
|
|
2360
|
+
});
|
|
2361
|
+
} catch (err) {
|
|
2362
|
+
bridge = {
|
|
2363
|
+
upserted: 0,
|
|
2364
|
+
failed: 1,
|
|
2365
|
+
skipped: 0,
|
|
2366
|
+
failures: [`bridge unavailable: ${err instanceof Error ? err.message : String(err)}`]
|
|
2367
|
+
};
|
|
2368
|
+
}
|
|
2369
|
+
writePushCache(memoryDir, cache);
|
|
2370
|
+
return {
|
|
2371
|
+
synced: true,
|
|
2372
|
+
action: "push",
|
|
2373
|
+
pushed: result.pushed,
|
|
2374
|
+
created: result.created,
|
|
2375
|
+
updated: result.updated,
|
|
2376
|
+
skipped: result.skipped,
|
|
2377
|
+
index_rows_preserved: result.indexRowsPreserved,
|
|
2378
|
+
memory_dir: memoryDir,
|
|
2379
|
+
knowledge_upserted: bridge.upserted,
|
|
2380
|
+
knowledge_failed: bridge.failed,
|
|
2381
|
+
knowledge_skipped: bridge.skipped,
|
|
2382
|
+
...bridge.failed > 0 ? { knowledge_failures: bridge.failures } : {},
|
|
2383
|
+
...takeover
|
|
2384
|
+
};
|
|
2385
|
+
});
|
|
1693
2386
|
} catch (err) {
|
|
1694
2387
|
const message = err instanceof Error ? err.message : String(err);
|
|
1695
2388
|
return { synced: false, reason: `Sync failed: ${message}` };
|
|
@@ -1711,7 +2404,11 @@ var init_sync_config = __esm({
|
|
|
1711
2404
|
"src/tools/memory/sync-config.ts"() {
|
|
1712
2405
|
"use strict";
|
|
1713
2406
|
init_common();
|
|
1714
|
-
|
|
2407
|
+
init_memory_sync_http();
|
|
2408
|
+
init_bounded_sync();
|
|
2409
|
+
init_memory_push_cache();
|
|
2410
|
+
init_sync_lock();
|
|
2411
|
+
init_sync_kill_switch();
|
|
1715
2412
|
TOOL_NAME22 = "vo_sync_config";
|
|
1716
2413
|
inputSchema22 = {
|
|
1717
2414
|
type: "object",
|
|
@@ -1729,19 +2426,19 @@ var init_sync_config = __esm({
|
|
|
1729
2426
|
required: ["action"],
|
|
1730
2427
|
additionalProperties: false
|
|
1731
2428
|
};
|
|
1732
|
-
description22 = "Syncs memory entries between local ~/.claude/projects/<slug>/memory/ and cloud control-plane /api/v1/agent-config/memory/me. Requires operator auth (vo-mcp login). Actions: pull (cloud\u2192local), push (local\u2192cloud). Idempotent; push creates/updates as needed.";
|
|
2429
|
+
description22 = "Syncs memory entries between local ~/.claude/projects/<slug>/memory/ and cloud control-plane /api/v1/agent-config/memory/me. Requires operator auth (vo-mcp login). Actions: pull (cloud\u2192local), push (local\u2192cloud). Idempotent; push creates/updates as needed. Serialized across concurrent sessions by an exclusive lock in the memory dir.";
|
|
1733
2430
|
}
|
|
1734
2431
|
});
|
|
1735
2432
|
|
|
1736
2433
|
// src/cli.ts
|
|
1737
|
-
import { homedir as
|
|
1738
|
-
import { randomUUID as
|
|
1739
|
-
import { join as
|
|
2434
|
+
import { homedir as homedir8, hostname as hostname2 } from "node:os";
|
|
2435
|
+
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
2436
|
+
import { join as join14 } from "node:path";
|
|
1740
2437
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
1741
2438
|
|
|
1742
2439
|
// src/server.ts
|
|
1743
2440
|
init_common();
|
|
1744
|
-
import { randomUUID as
|
|
2441
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
1745
2442
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
1746
2443
|
import {
|
|
1747
2444
|
CallToolRequestSchema,
|
|
@@ -2117,7 +2814,8 @@ async function handleCheckHollowTest(deps, rawInput, signal) {
|
|
|
2117
2814
|
synthesized_verdict: synthForEvent,
|
|
2118
2815
|
consensus_confidence: engineResult.synthesized_verdict.confidence,
|
|
2119
2816
|
duration_ms: engineResult.duration_ms,
|
|
2120
|
-
consensus_engine_version: engineResult.engine_version
|
|
2817
|
+
consensus_engine_version: engineResult.engine_version,
|
|
2818
|
+
...aggregateEventTokenUsage(engineResult.per_model_verdicts, engineResult.token_usage)
|
|
2121
2819
|
};
|
|
2122
2820
|
const payload = {
|
|
2123
2821
|
verdict: engineResult.synthesized_verdict.verdict,
|
|
@@ -2300,7 +2998,8 @@ async function handleVerifyAnswer(deps, rawInput, signal) {
|
|
|
2300
2998
|
synthesized_verdict: synthForEvent,
|
|
2301
2999
|
consensus_confidence: engineResult.synthesized_verdict.confidence,
|
|
2302
3000
|
duration_ms: engineResult.duration_ms,
|
|
2303
|
-
consensus_engine_version: engineResult.engine_version
|
|
3001
|
+
consensus_engine_version: engineResult.engine_version,
|
|
3002
|
+
...aggregateEventTokenUsage(engineResult.per_model_verdicts, engineResult.token_usage)
|
|
2304
3003
|
};
|
|
2305
3004
|
const payload = {
|
|
2306
3005
|
verdict: engineResult.synthesized_verdict.verdict,
|
|
@@ -2309,6 +3008,7 @@ async function handleVerifyAnswer(deps, rawInput, signal) {
|
|
|
2309
3008
|
synthesized_verdict: synthForEvent,
|
|
2310
3009
|
engine_version: engineResult.engine_version,
|
|
2311
3010
|
degraded: engineResult.degraded,
|
|
3011
|
+
...engineResult.quorum_failed === true ? { quorum_failed: true } : {},
|
|
2312
3012
|
gate_type: gateType,
|
|
2313
3013
|
...kbResult.error !== null ? { kb_unavailable: true } : {},
|
|
2314
3014
|
...kbTruncated > 0 ? { kb_rules_truncated: kbTruncated } : {}
|
|
@@ -2565,7 +3265,8 @@ async function handleConsensusJudgment(deps, rawInput, signal) {
|
|
|
2565
3265
|
duration_ms: engineResult.duration_ms,
|
|
2566
3266
|
consensus_engine_version: engineResult.engine_version,
|
|
2567
3267
|
per_model_verdicts: perModelForEvent,
|
|
2568
|
-
synthesized_verdict: synthForEvent
|
|
3268
|
+
synthesized_verdict: synthForEvent,
|
|
3269
|
+
...aggregateEventTokenUsage(engineResult.per_model_verdicts, engineResult.token_usage)
|
|
2569
3270
|
};
|
|
2570
3271
|
const payload = {
|
|
2571
3272
|
verdict: engineResult.synthesized_verdict.verdict,
|
|
@@ -2574,6 +3275,7 @@ async function handleConsensusJudgment(deps, rawInput, signal) {
|
|
|
2574
3275
|
synthesized_verdict: synthForEvent,
|
|
2575
3276
|
engine_version: engineResult.engine_version,
|
|
2576
3277
|
degraded: engineResult.degraded,
|
|
3278
|
+
...engineResult.quorum_failed === true ? { quorum_failed: true } : {},
|
|
2577
3279
|
gate_type: gateType,
|
|
2578
3280
|
// ─── Consensus-engine feature outputs (additive; 2026-06-13) ─────────────
|
|
2579
3281
|
// Feature 2 (calibrated-confidence) — ON by default; the engine attaches
|
|
@@ -2774,7 +3476,8 @@ async function handleArchitectureReview(deps, rawInput, signal) {
|
|
|
2774
3476
|
synthesized_verdict: synthForEvent,
|
|
2775
3477
|
consensus_confidence: engineResult.synthesized_verdict.confidence,
|
|
2776
3478
|
duration_ms: engineResult.duration_ms,
|
|
2777
|
-
consensus_engine_version: engineResult.engine_version
|
|
3479
|
+
consensus_engine_version: engineResult.engine_version,
|
|
3480
|
+
...aggregateEventTokenUsage(engineResult.per_model_verdicts, engineResult.token_usage)
|
|
2778
3481
|
};
|
|
2779
3482
|
const escalationRequired = engineResult.escalation_required === true || engineResult.escalation_required === void 0 && engineResult.synthesized_verdict.dissent_summary !== null;
|
|
2780
3483
|
const escalationReason = engineResult.escalation_reason ?? engineResult.synthesized_verdict.dissent_summary ?? "";
|
|
@@ -4261,7 +4964,8 @@ Produce the JSON dispatch plan now.`;
|
|
|
4261
4964
|
duration_ms: engineResult.duration_ms,
|
|
4262
4965
|
consensus_engine_version: engineResult.engine_version,
|
|
4263
4966
|
per_model_verdicts: toEventPerModelVerdicts(engineResult.per_model_verdicts),
|
|
4264
|
-
synthesized_verdict: toEventSynthesizedVerdict(engineResult.synthesized_verdict)
|
|
4967
|
+
synthesized_verdict: toEventSynthesizedVerdict(engineResult.synthesized_verdict),
|
|
4968
|
+
...aggregateEventTokenUsage(engineResult.per_model_verdicts, engineResult.token_usage)
|
|
4265
4969
|
};
|
|
4266
4970
|
deps.events.append(enrichedEvent);
|
|
4267
4971
|
return jsonContent(envelope);
|
|
@@ -5135,7 +5839,8 @@ async function handleReviewMerge(deps, rawInput, signal) {
|
|
|
5135
5839
|
duration_ms: result.duration_ms,
|
|
5136
5840
|
consensus_engine_version: result.engine_version,
|
|
5137
5841
|
per_model_verdicts: perModel,
|
|
5138
|
-
synthesized_verdict: synth
|
|
5842
|
+
synthesized_verdict: synth,
|
|
5843
|
+
...aggregateEventTokenUsage(result.per_model_verdicts, result.token_usage)
|
|
5139
5844
|
});
|
|
5140
5845
|
}
|
|
5141
5846
|
|
|
@@ -6319,12 +7024,12 @@ async function handleHqWhiteboardRead(_deps, rawInput, signal) {
|
|
|
6319
7024
|
}
|
|
6320
7025
|
|
|
6321
7026
|
// src/tools/skills/skill-corpus.ts
|
|
6322
|
-
import { existsSync as
|
|
6323
|
-
import { dirname as dirname5, isAbsolute, join as
|
|
7027
|
+
import { existsSync as existsSync9, statSync as statSync6 } from "node:fs";
|
|
7028
|
+
import { dirname as dirname5, isAbsolute, join as join13, resolve as resolve2 } from "node:path";
|
|
6324
7029
|
|
|
6325
7030
|
// ../skill-registry/src/loader.ts
|
|
6326
|
-
import { readdirSync as readdirSync6, readFileSync as
|
|
6327
|
-
import { join as
|
|
7031
|
+
import { readdirSync as readdirSync6, readFileSync as readFileSync14, statSync as statSync5 } from "node:fs";
|
|
7032
|
+
import { join as join12 } from "node:path";
|
|
6328
7033
|
var InvalidSkillFrontmatterError = class extends Error {
|
|
6329
7034
|
constructor(skillFile, reason) {
|
|
6330
7035
|
super(`Invalid frontmatter in ${skillFile}: ${reason}`);
|
|
@@ -6377,18 +7082,18 @@ function loadSkillsFromDir(skillsDir) {
|
|
|
6377
7082
|
const entries = readdirSync6(skillsDir);
|
|
6378
7083
|
const skills = [];
|
|
6379
7084
|
for (const entry of entries) {
|
|
6380
|
-
const entryPath =
|
|
7085
|
+
const entryPath = join12(skillsDir, entry);
|
|
6381
7086
|
let stat;
|
|
6382
7087
|
try {
|
|
6383
|
-
stat =
|
|
7088
|
+
stat = statSync5(entryPath);
|
|
6384
7089
|
} catch {
|
|
6385
7090
|
continue;
|
|
6386
7091
|
}
|
|
6387
7092
|
if (!stat.isDirectory()) continue;
|
|
6388
|
-
const skillFile =
|
|
7093
|
+
const skillFile = join12(entryPath, "SKILL.md");
|
|
6389
7094
|
let raw;
|
|
6390
7095
|
try {
|
|
6391
|
-
raw =
|
|
7096
|
+
raw = readFileSync14(skillFile, "utf8");
|
|
6392
7097
|
} catch {
|
|
6393
7098
|
continue;
|
|
6394
7099
|
}
|
|
@@ -6430,12 +7135,12 @@ function resolveSkillsDir(env = process.env, startDir = process.cwd()) {
|
|
|
6430
7135
|
const override = env.VO_SKILLS_DIR;
|
|
6431
7136
|
if (typeof override === "string" && override.length > 0) {
|
|
6432
7137
|
const abs = isAbsolute(override) ? override : resolve2(startDir, override);
|
|
6433
|
-
return
|
|
7138
|
+
return existsSync9(abs) && statSync6(abs).isDirectory() ? abs : null;
|
|
6434
7139
|
}
|
|
6435
7140
|
let dir = resolve2(startDir);
|
|
6436
7141
|
for (let i = 0; i < MAX_WALK_UP_LEVELS; i += 1) {
|
|
6437
|
-
const candidate =
|
|
6438
|
-
if (
|
|
7142
|
+
const candidate = join13(dir, ".claude", "skills");
|
|
7143
|
+
if (existsSync9(candidate) && statSync6(candidate).isDirectory()) return candidate;
|
|
6439
7144
|
const parent = dirname5(dir);
|
|
6440
7145
|
if (parent === dir) break;
|
|
6441
7146
|
dir = parent;
|
|
@@ -6746,7 +7451,7 @@ function buildToolRegistry() {
|
|
|
6746
7451
|
};
|
|
6747
7452
|
}
|
|
6748
7453
|
function createServer(options) {
|
|
6749
|
-
const sessionId = options.sessionId ??
|
|
7454
|
+
const sessionId = options.sessionId ?? randomUUID3();
|
|
6750
7455
|
const mode = createLocalMode();
|
|
6751
7456
|
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
6752
7457
|
const server = new Server(
|
|
@@ -6795,8 +7500,8 @@ function createServer(options) {
|
|
|
6795
7500
|
}
|
|
6796
7501
|
|
|
6797
7502
|
// src/cache/sqlite-cache.ts
|
|
6798
|
-
import { createHash as
|
|
6799
|
-
import { chmodSync as chmodSync3, mkdirSync as
|
|
7503
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
7504
|
+
import { chmodSync as chmodSync3, mkdirSync as mkdirSync7 } from "node:fs";
|
|
6800
7505
|
import { dirname as dirname6 } from "node:path";
|
|
6801
7506
|
import { DatabaseSync } from "node:sqlite";
|
|
6802
7507
|
|
|
@@ -6842,7 +7547,7 @@ function normalizeString(s) {
|
|
|
6842
7547
|
function createSqliteCache(options) {
|
|
6843
7548
|
const fileBacked = options.dbPath !== ":memory:";
|
|
6844
7549
|
if (fileBacked) {
|
|
6845
|
-
|
|
7550
|
+
mkdirSync7(dirname6(options.dbPath), { recursive: true, mode: 448 });
|
|
6846
7551
|
}
|
|
6847
7552
|
const versionNamespace = options.cacheVersionNamespace ?? "";
|
|
6848
7553
|
const db = new DatabaseSync(options.dbPath);
|
|
@@ -6873,7 +7578,7 @@ function createSqliteCache(options) {
|
|
|
6873
7578
|
return {
|
|
6874
7579
|
keyFor(toolName, input, opts) {
|
|
6875
7580
|
const canonical = canonicalize(input, opts);
|
|
6876
|
-
const hash =
|
|
7581
|
+
const hash = createHash4("sha256");
|
|
6877
7582
|
if (versionNamespace.length > 0) {
|
|
6878
7583
|
hash.update(versionNamespace);
|
|
6879
7584
|
hash.update("|");
|
|
@@ -6968,7 +7673,7 @@ function createStubRatchetClient() {
|
|
|
6968
7673
|
let m;
|
|
6969
7674
|
while ((m = pat.regex.exec(req.source)) !== null) {
|
|
6970
7675
|
findings.push({
|
|
6971
|
-
line_excerpt:
|
|
7676
|
+
line_excerpt: clip2(m[0], 80),
|
|
6972
7677
|
severity: pat.severity,
|
|
6973
7678
|
code: pat.code,
|
|
6974
7679
|
message: pat.message
|
|
@@ -7000,7 +7705,7 @@ function createStubRatchetClient() {
|
|
|
7000
7705
|
}
|
|
7001
7706
|
};
|
|
7002
7707
|
}
|
|
7003
|
-
function
|
|
7708
|
+
function clip2(s, n) {
|
|
7004
7709
|
return s.length <= n ? s : s.slice(0, n) + "\u2026";
|
|
7005
7710
|
}
|
|
7006
7711
|
function buildSummary2(args) {
|
|
@@ -7011,7 +7716,7 @@ function buildSummary2(args) {
|
|
|
7011
7716
|
// src/consensus/engine-client.ts
|
|
7012
7717
|
init_events_writer();
|
|
7013
7718
|
init_common();
|
|
7014
|
-
import { randomUUID as
|
|
7719
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
7015
7720
|
|
|
7016
7721
|
// src/consensus/null-client.ts
|
|
7017
7722
|
var NULL_CLIENT_DEFAULT_REASON = "consensus-engine-package-pending";
|
|
@@ -7140,6 +7845,14 @@ function shadowEnabled(env) {
|
|
|
7140
7845
|
const norm = raw.trim().toLowerCase();
|
|
7141
7846
|
return !(norm === "0" || norm === "false" || norm === "no" || norm === "off" || norm === "");
|
|
7142
7847
|
}
|
|
7848
|
+
var MIN_RESPONDERS_ENV_VAR = "VO_CONSENSUS_MIN_RESPONDERS";
|
|
7849
|
+
function resolveMinResponders(env) {
|
|
7850
|
+
const raw = (env ?? {})[MIN_RESPONDERS_ENV_VAR];
|
|
7851
|
+
if (raw === void 0 || raw.trim() === "") return 2;
|
|
7852
|
+
const parsed = Number.parseInt(raw.trim(), 10);
|
|
7853
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return void 0;
|
|
7854
|
+
return parsed;
|
|
7855
|
+
}
|
|
7143
7856
|
function mapShadowSynthesis(s) {
|
|
7144
7857
|
if (s === void 0) return void 0;
|
|
7145
7858
|
return {
|
|
@@ -7289,9 +8002,11 @@ function createEngineConsensusClient(options) {
|
|
|
7289
8002
|
...options.agreement_gate_enabled !== void 0 ? { configEnabled: options.agreement_gate_enabled } : {},
|
|
7290
8003
|
...options.env !== void 0 ? { env: options.env } : {}
|
|
7291
8004
|
});
|
|
8005
|
+
const minResponders = resolveMinResponders(options.env);
|
|
7292
8006
|
const engineOptions = {
|
|
7293
8007
|
panel,
|
|
7294
8008
|
...options.per_model_timeout_ms !== void 0 ? { per_model_timeout_ms: options.per_model_timeout_ms } : {},
|
|
8009
|
+
...minResponders !== void 0 ? { min_responders: minResponders } : {},
|
|
7295
8010
|
...agreementGate !== void 0 ? { agreement_gate: agreementGate } : {},
|
|
7296
8011
|
// Stage A7-shadow: run the adaptive verdict alongside the live one for grading.
|
|
7297
8012
|
// Cheap (pure log-odds over already-fetched verdicts; no extra model calls),
|
|
@@ -7344,8 +8059,13 @@ function createEngineConsensusClient(options) {
|
|
|
7344
8059
|
synthesized_verdict: response.synthesized_verdict,
|
|
7345
8060
|
per_model_verdicts: response.per_model_verdicts,
|
|
7346
8061
|
degraded: response.degraded,
|
|
8062
|
+
...response.quorum_failed === true ? { quorum_failed: true } : {},
|
|
7347
8063
|
duration_ms: response.duration_ms,
|
|
7348
8064
|
engine_version: response.engine_version,
|
|
8065
|
+
// Cumulative cross-round inference usage (B44-3). Absent when no panel
|
|
8066
|
+
// member reported usage; forwarded verbatim — the aggregator prefers it
|
|
8067
|
+
// over summing final-round verdicts (which under-reports deliberation).
|
|
8068
|
+
...response.token_usage !== void 0 ? { token_usage: response.token_usage } : {},
|
|
7349
8069
|
// Phase 2 Lane D-1 — forward escalation signal when present. The
|
|
7350
8070
|
// source-grounded layer's own escalation (from the citation grade)
|
|
7351
8071
|
// takes precedence when set, else the synthesizer's.
|
|
@@ -7471,7 +8191,7 @@ function tryCreateEngineConsensusClientFromEnv(options = {}) {
|
|
|
7471
8191
|
}
|
|
7472
8192
|
|
|
7473
8193
|
// src/consensus/moat-client.ts
|
|
7474
|
-
import { randomUUID as
|
|
8194
|
+
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
7475
8195
|
|
|
7476
8196
|
// src/consensus/client.ts
|
|
7477
8197
|
var CANCELLED_REASON = "cancelled";
|
|
@@ -7523,7 +8243,7 @@ function createMoatConsensusClient(opts) {
|
|
|
7523
8243
|
request.signal?.addEventListener("abort", onAbort, { once: true });
|
|
7524
8244
|
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
7525
8245
|
const body = JSON.stringify({
|
|
7526
|
-
task_id:
|
|
8246
|
+
task_id: randomUUID5(),
|
|
7527
8247
|
gate_type: request.gate_type,
|
|
7528
8248
|
excerpt: request.prompt,
|
|
7529
8249
|
...request.system_prompt ? { question: request.system_prompt } : {},
|
|
@@ -7844,7 +8564,7 @@ init_common();
|
|
|
7844
8564
|
function defaultCacheDbPath() {
|
|
7845
8565
|
const env = process.env["VO_MCP_DB_PATH"];
|
|
7846
8566
|
if (env && env.length > 0) return env;
|
|
7847
|
-
return
|
|
8567
|
+
return join14(homedir8(), ".claude", "vo-mcp-cache.db");
|
|
7848
8568
|
}
|
|
7849
8569
|
async function probeEngineVersion() {
|
|
7850
8570
|
try {
|
|
@@ -7971,7 +8691,7 @@ async function main() {
|
|
|
7971
8691
|
}
|
|
7972
8692
|
if (process.argv[2] === "login") {
|
|
7973
8693
|
const controlPlaneUrl = process.env["VO_CONTROL_PLANE_URL"]?.trim();
|
|
7974
|
-
const credentialLabel = `vo-mcp-cli@${
|
|
8694
|
+
const credentialLabel = `vo-mcp-cli@${hostname2()}`.slice(0, 200);
|
|
7975
8695
|
runLogin(
|
|
7976
8696
|
controlPlaneUrl ? {
|
|
7977
8697
|
exchange: (refreshToken, apiKey) => exchangeForVoCredential({ refreshToken, apiKey, controlPlaneUrl, label: credentialLabel })
|
|
@@ -7987,26 +8707,29 @@ if (process.argv[2] === "login") {
|
|
|
7987
8707
|
} else if (process.argv[2] === "sync") {
|
|
7988
8708
|
const action = process.argv[3];
|
|
7989
8709
|
if (action !== "push" && action !== "pull") {
|
|
7990
|
-
console.error("[vo-mcp] usage: vo-mcp sync <push|pull> [--cwd <path>]");
|
|
8710
|
+
console.error("[vo-mcp] usage: vo-mcp sync <push|pull> [--cwd <path>] [--lock-wait-ms <n>]");
|
|
7991
8711
|
process.exit(2);
|
|
7992
8712
|
}
|
|
7993
8713
|
const cwdFlag = process.argv.indexOf("--cwd");
|
|
7994
8714
|
const cwd = cwdFlag >= 0 && typeof process.argv[cwdFlag + 1] === "string" ? process.argv[cwdFlag + 1] : process.cwd();
|
|
7995
|
-
const
|
|
8715
|
+
const waitFlag = process.argv.indexOf("--lock-wait-ms");
|
|
8716
|
+
const parsedWait = waitFlag >= 0 ? Number(process.argv[waitFlag + 1]) : Number.NaN;
|
|
8717
|
+
const lockOptions = Number.isFinite(parsedWait) && parsedWait >= 0 ? { waitMs: parsedWait } : {};
|
|
8718
|
+
const sessionId = randomUUID6();
|
|
7996
8719
|
const appendSyncLog = async (line) => {
|
|
7997
8720
|
try {
|
|
7998
|
-
const { appendFileSync: appendFileSync2, mkdirSync:
|
|
7999
|
-
const { join:
|
|
8000
|
-
const { homedir:
|
|
8001
|
-
const dir =
|
|
8002
|
-
|
|
8003
|
-
appendFileSync2(
|
|
8721
|
+
const { appendFileSync: appendFileSync2, mkdirSync: mkdirSync8 } = await import("node:fs");
|
|
8722
|
+
const { join: join15 } = await import("node:path");
|
|
8723
|
+
const { homedir: homedir9 } = await import("node:os");
|
|
8724
|
+
const dir = join15(homedir9(), ".claude");
|
|
8725
|
+
mkdirSync8(dir, { recursive: true });
|
|
8726
|
+
appendFileSync2(join15(dir, "vo-mcp-sync.log"), `${line}
|
|
8004
8727
|
`, "utf8");
|
|
8005
8728
|
} catch {
|
|
8006
8729
|
}
|
|
8007
8730
|
};
|
|
8008
8731
|
Promise.resolve().then(() => (init_sync_config(), sync_config_exports)).then(async ({ runMemorySync: runMemorySync2, isNoopSyncReason: isNoopSyncReason2 }) => {
|
|
8009
|
-
const r = await runMemorySync2(action, cwd, sessionId);
|
|
8732
|
+
const r = await runMemorySync2(action, cwd, sessionId, void 0, lockOptions);
|
|
8010
8733
|
const stamp = `${sessionId} ${action}`;
|
|
8011
8734
|
if (r.synced) {
|
|
8012
8735
|
console.error(`[vo-mcp] sync ${action} ok: ${JSON.stringify(r)}`);
|