@algosuite/vo-mcp 0.2.0-beta.29 → 0.2.0-beta.30

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/cli.js CHANGED
@@ -1399,13 +1399,713 @@ var init_safe_memory_file = __esm({
1399
1399
  }
1400
1400
  });
1401
1401
 
1402
+ // src/tools/memory/sync-lock-liveness.ts
1403
+ import { statSync as statSync4, readFileSync as readFileSync8 } from "node:fs";
1404
+ function defaultIsProcessAlive(pid) {
1405
+ try {
1406
+ process.kill(pid, 0);
1407
+ return true;
1408
+ } catch (err) {
1409
+ return err.code === "EPERM";
1410
+ }
1411
+ }
1412
+ function toPayload(parsed) {
1413
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
1414
+ const record = parsed;
1415
+ const token = record["token"];
1416
+ const host = record["hostname"];
1417
+ if (typeof token !== "string" || token.length === 0) return null;
1418
+ const pid = record["pid"];
1419
+ const acquiredAtMs = record["acquiredAtMs"];
1420
+ return {
1421
+ pid: typeof pid === "number" && Number.isInteger(pid) && pid > 0 ? pid : 0,
1422
+ hostname: typeof host === "string" ? host : "",
1423
+ sessionId: typeof record["sessionId"] === "string" ? record["sessionId"] : null,
1424
+ token,
1425
+ acquiredAt: typeof record["acquiredAt"] === "string" ? record["acquiredAt"] : "",
1426
+ acquiredAtMs: typeof acquiredAtMs === "number" && Number.isFinite(acquiredAtMs) ? acquiredAtMs : Number.NaN
1427
+ };
1428
+ }
1429
+ function readLockRecord(path3) {
1430
+ let raw;
1431
+ try {
1432
+ raw = readFileSync8(path3, "utf8");
1433
+ } catch {
1434
+ return null;
1435
+ }
1436
+ try {
1437
+ return { raw, payload: toPayload(JSON.parse(raw)) };
1438
+ } catch {
1439
+ return { raw, payload: null };
1440
+ }
1441
+ }
1442
+ function lockAgeMs(record, path3, nowMs) {
1443
+ let startedMs = Number.NaN;
1444
+ if (record.payload) {
1445
+ if (Number.isFinite(record.payload.acquiredAtMs)) {
1446
+ startedMs = record.payload.acquiredAtMs;
1447
+ } else if (record.payload.acquiredAt) {
1448
+ startedMs = Date.parse(record.payload.acquiredAt);
1449
+ }
1450
+ }
1451
+ if (!Number.isFinite(startedMs)) {
1452
+ try {
1453
+ startedMs = statSync4(path3).mtimeMs;
1454
+ } catch {
1455
+ return null;
1456
+ }
1457
+ }
1458
+ const age = nowMs - startedMs;
1459
+ return Number.isFinite(age) && age >= 0 ? age : null;
1460
+ }
1461
+ function classifyHolderLiveness(record, isProcessAlive, thisHost) {
1462
+ const payload = record.payload;
1463
+ if (payload === null) return "unknown";
1464
+ if (payload.pid <= 0) return "unknown";
1465
+ if (thisHost.length === 0) return "unknown";
1466
+ if (payload.hostname !== thisHost) return "unknown";
1467
+ return isProcessAlive(payload.pid) ? "alive" : "dead";
1468
+ }
1469
+ function isLockAbandoned(record, ageMs, ttlMs, isProcessAlive, thisHost) {
1470
+ const liveness = classifyHolderLiveness(record, isProcessAlive, thisHost);
1471
+ if (liveness === "alive") return false;
1472
+ if (liveness === "dead") return true;
1473
+ return ageMs !== null && ageMs > ttlMs;
1474
+ }
1475
+ var init_sync_lock_liveness = __esm({
1476
+ "src/tools/memory/sync-lock-liveness.ts"() {
1477
+ "use strict";
1478
+ }
1479
+ });
1480
+
1481
+ // src/tools/memory/sync-lock.ts
1482
+ import { closeSync as closeSync2, mkdirSync as mkdirSync5, openSync as openSync3, readFileSync as readFileSync9, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "node:fs";
1483
+ import { hostname } from "node:os";
1484
+ import { join as join8 } from "node:path";
1485
+ import { randomUUID as randomUUID2 } from "node:crypto";
1486
+ function positiveOr(value, fallback) {
1487
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : fallback;
1488
+ }
1489
+ function createExclusive2(path3, contents) {
1490
+ let fd;
1491
+ try {
1492
+ fd = openSync3(path3, "wx");
1493
+ } catch (err) {
1494
+ const code = err.code;
1495
+ return { ok: false, exists: code === "EEXIST", message: err instanceof Error ? err.message : String(err) };
1496
+ }
1497
+ try {
1498
+ writeFileSync4(fd, contents, "utf8");
1499
+ } catch (err) {
1500
+ closeSync2(fd);
1501
+ try {
1502
+ unlinkSync2(path3);
1503
+ } catch {
1504
+ }
1505
+ return { ok: false, exists: false, message: err instanceof Error ? err.message : String(err) };
1506
+ }
1507
+ closeSync2(fd);
1508
+ return { ok: true };
1509
+ }
1510
+ function removeAbandoned(path3, expectedRaw) {
1511
+ let current;
1512
+ try {
1513
+ current = readFileSync9(path3, "utf8");
1514
+ } catch {
1515
+ return;
1516
+ }
1517
+ if (current !== expectedRaw) return;
1518
+ try {
1519
+ unlinkSync2(path3);
1520
+ } catch {
1521
+ }
1522
+ }
1523
+ function makeRelease(path3, token) {
1524
+ let released = false;
1525
+ return () => {
1526
+ if (released) return;
1527
+ released = true;
1528
+ let raw;
1529
+ try {
1530
+ raw = readFileSync9(path3, "utf8");
1531
+ } catch {
1532
+ return;
1533
+ }
1534
+ let stillOurs;
1535
+ try {
1536
+ stillOurs = toPayload(JSON.parse(raw))?.token === token;
1537
+ } catch {
1538
+ stillOurs = false;
1539
+ }
1540
+ if (!stillOurs) return;
1541
+ try {
1542
+ unlinkSync2(path3);
1543
+ } catch {
1544
+ }
1545
+ };
1546
+ }
1547
+ function describeHolder(record) {
1548
+ const payload = record?.payload;
1549
+ if (!payload) return "an unreadable lock file";
1550
+ return `pid ${payload.pid} on ${payload.hostname || "(unknown host)"} (session ${payload.sessionId ?? "unknown"}, held since ${payload.acquiredAt || "unknown"})`;
1551
+ }
1552
+ async function acquireMemorySyncLock(options) {
1553
+ const waitMs = positiveOr(options.waitMs, DEFAULT_LOCK_WAIT_MS);
1554
+ const ttlMs = positiveOr(options.ttlMs, DEFAULT_LOCK_TTL_MS);
1555
+ const now = options.now ?? Date.now;
1556
+ const sleep = options.sleep ?? ((ms) => new Promise((resolve3) => {
1557
+ setTimeout(resolve3, ms);
1558
+ }));
1559
+ const isProcessAlive = options.isProcessAlive ?? defaultIsProcessAlive;
1560
+ const thisHost = hostname();
1561
+ const path3 = join8(options.memoryDir, MEMORY_SYNC_LOCK_FILE);
1562
+ if (options.createDir === true) mkdirSync5(options.memoryDir, { recursive: true });
1563
+ const deadline = now() + waitMs;
1564
+ let backoffMs = INITIAL_BACKOFF_MS;
1565
+ let tookOverFrom = null;
1566
+ let holderDescription = "another session";
1567
+ for (; ; ) {
1568
+ const acquiredAtMs = now();
1569
+ const payload = {
1570
+ pid: process.pid,
1571
+ hostname: thisHost,
1572
+ sessionId: options.sessionId ?? null,
1573
+ token: randomUUID2(),
1574
+ acquiredAt: new Date(acquiredAtMs).toISOString(),
1575
+ acquiredAtMs
1576
+ };
1577
+ const created = createExclusive2(path3, `${JSON.stringify(payload, null, 2)}
1578
+ `);
1579
+ if (created.ok) {
1580
+ return { path: path3, payload, tookOverFrom, release: makeRelease(path3, payload.token) };
1581
+ }
1582
+ if (!created.exists) {
1583
+ throw new Error(
1584
+ `memory sync lock ${path3} could not be created (${created.message}) \u2014 refusing to sync without exclusion`
1585
+ );
1586
+ }
1587
+ const record = readLockRecord(path3);
1588
+ let reclaimed = false;
1589
+ if (record) {
1590
+ holderDescription = describeHolder(record);
1591
+ const age = lockAgeMs(record, path3, now());
1592
+ if (isLockAbandoned(record, age, ttlMs, isProcessAlive, thisHost)) {
1593
+ tookOverFrom = record.payload;
1594
+ removeAbandoned(path3, record.raw);
1595
+ reclaimed = true;
1596
+ }
1597
+ }
1598
+ if (now() >= deadline) {
1599
+ throw new Error(
1600
+ `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.`
1601
+ );
1602
+ }
1603
+ if (reclaimed) backoffMs = INITIAL_BACKOFF_MS;
1604
+ await sleep(Math.max(1, Math.min(backoffMs, deadline - now())));
1605
+ if (!reclaimed) backoffMs = Math.min(MAX_BACKOFF_MS, Math.ceil(backoffMs * BACKOFF_FACTOR));
1606
+ }
1607
+ }
1608
+ async function withMemorySyncLock(options, fn) {
1609
+ const handle = await acquireMemorySyncLock(options);
1610
+ try {
1611
+ return await fn(handle);
1612
+ } finally {
1613
+ handle.release();
1614
+ }
1615
+ }
1616
+ var MEMORY_SYNC_LOCK_FILE, DEFAULT_LOCK_TTL_MS, DEFAULT_LOCK_WAIT_MS, INITIAL_BACKOFF_MS, MAX_BACKOFF_MS, BACKOFF_FACTOR;
1617
+ var init_sync_lock = __esm({
1618
+ "src/tools/memory/sync-lock.ts"() {
1619
+ "use strict";
1620
+ init_sync_lock_liveness();
1621
+ init_sync_lock_liveness();
1622
+ MEMORY_SYNC_LOCK_FILE = ".memory-sync.lock";
1623
+ DEFAULT_LOCK_TTL_MS = 15 * 6e4;
1624
+ DEFAULT_LOCK_WAIT_MS = 1e4;
1625
+ INITIAL_BACKOFF_MS = 25;
1626
+ MAX_BACKOFF_MS = 500;
1627
+ BACKOFF_FACTOR = 1.6;
1628
+ }
1629
+ });
1630
+
1631
+ // src/tools/memory/memory-index-merge.ts
1632
+ function isMemoryIndexFile(fileName) {
1633
+ return fileName.toUpperCase() === MEMORY_INDEX_FILE.toUpperCase();
1634
+ }
1635
+ function indexRowKey(line) {
1636
+ const match = INDEX_ROW_RE.exec(line);
1637
+ if (!match) return null;
1638
+ let target = match[1].trim();
1639
+ if (target.startsWith("<") && target.endsWith(">")) target = target.slice(1, -1).trim();
1640
+ target = target.replace(/\s+(["'])[\s\S]*\1$/, "").trim();
1641
+ if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(target)) {
1642
+ target = target.replace(/\\/g, "/").replace(/\/{2,}/g, "/");
1643
+ target = target.replace(/^(?:\.\/)+/, "");
1644
+ }
1645
+ return target.length > 0 ? target.toLowerCase() : null;
1646
+ }
1647
+ function mergeMemoryIndex(localContent, cloudContent) {
1648
+ if (typeof cloudContent !== "string" || cloudContent.trim().length === 0) {
1649
+ return { content: localContent, addedFromCloud: [] };
1650
+ }
1651
+ const eol = localContent.includes("\r\n") ? "\r\n" : "\n";
1652
+ const localLines = localContent.split(/\r?\n/);
1653
+ const localKeys = /* @__PURE__ */ new Set();
1654
+ let lastLocalRowIndex = -1;
1655
+ for (let i = 0; i < localLines.length; i++) {
1656
+ const key = indexRowKey(localLines[i]);
1657
+ if (key === null) continue;
1658
+ localKeys.add(key);
1659
+ lastLocalRowIndex = i;
1660
+ }
1661
+ const addedFromCloud = [];
1662
+ const seenCloudKeys = /* @__PURE__ */ new Set();
1663
+ for (const rawLine of cloudContent.split(/\r?\n/)) {
1664
+ const key = indexRowKey(rawLine);
1665
+ if (key === null) continue;
1666
+ if (localKeys.has(key) || seenCloudKeys.has(key)) continue;
1667
+ seenCloudKeys.add(key);
1668
+ addedFromCloud.push(rawLine.replace(/\r$/, ""));
1669
+ }
1670
+ if (addedFromCloud.length === 0) {
1671
+ return { content: localContent, addedFromCloud: [] };
1672
+ }
1673
+ const merged = lastLocalRowIndex >= 0 ? [...localLines.slice(0, lastLocalRowIndex + 1), ...addedFromCloud, ...localLines.slice(lastLocalRowIndex + 1)] : [...localLines, ...addedFromCloud];
1674
+ return { content: merged.join(eol), addedFromCloud };
1675
+ }
1676
+ var MEMORY_INDEX_FILE, INDEX_ROW_RE;
1677
+ var init_memory_index_merge = __esm({
1678
+ "src/tools/memory/memory-index-merge.ts"() {
1679
+ "use strict";
1680
+ MEMORY_INDEX_FILE = "MEMORY.md";
1681
+ INDEX_ROW_RE = /^\s*[-*]\s+\[[^\]]*\]\(([^)]+)\)/;
1682
+ }
1683
+ });
1684
+
1685
+ // src/tools/memory/bounded-sync.ts
1686
+ function createSyncDeadline(budgetMs = SYNC_DEADLINE_MS, now = Date.now) {
1687
+ const startedAt = now();
1688
+ return {
1689
+ check() {
1690
+ const elapsed = now() - startedAt;
1691
+ if (elapsed > budgetMs) throw new SyncDeadlineExceededError(elapsed, budgetMs);
1692
+ },
1693
+ remainingMs() {
1694
+ return Math.max(0, budgetMs - (now() - startedAt));
1695
+ }
1696
+ };
1697
+ }
1698
+ async function withRequestTimeout(url, run, budgetMs = REQUEST_TIMEOUT_MS) {
1699
+ let timer;
1700
+ try {
1701
+ return await Promise.race([
1702
+ run(),
1703
+ new Promise((_resolve, reject) => {
1704
+ timer = setTimeout(() => reject(new RequestTimeoutError(url, budgetMs)), budgetMs);
1705
+ timer.unref?.();
1706
+ })
1707
+ ]);
1708
+ } finally {
1709
+ if (timer) clearTimeout(timer);
1710
+ }
1711
+ }
1712
+ async function mapWithConcurrency(items, limit, fn) {
1713
+ const results = new Array(items.length);
1714
+ const width = Math.max(1, Math.min(limit, items.length));
1715
+ let next = 0;
1716
+ async function worker() {
1717
+ for (; ; ) {
1718
+ const index = next++;
1719
+ if (index >= items.length) return;
1720
+ try {
1721
+ results[index] = { ok: true, value: await fn(items[index], index) };
1722
+ } catch (error) {
1723
+ results[index] = { ok: false, error };
1724
+ }
1725
+ }
1726
+ }
1727
+ await Promise.all(Array.from({ length: width }, () => worker()));
1728
+ return results;
1729
+ }
1730
+ var REQUEST_TIMEOUT_MS, SYNC_DEADLINE_MS, PUSH_CONCURRENCY, SyncDeadlineExceededError, RequestTimeoutError;
1731
+ var init_bounded_sync = __esm({
1732
+ "src/tools/memory/bounded-sync.ts"() {
1733
+ "use strict";
1734
+ REQUEST_TIMEOUT_MS = 15e3;
1735
+ SYNC_DEADLINE_MS = 12e4;
1736
+ PUSH_CONCURRENCY = 6;
1737
+ SyncDeadlineExceededError = class extends Error {
1738
+ constructor(elapsedMs, budgetMs) {
1739
+ super(
1740
+ `memory sync exceeded its ${budgetMs}ms deadline after ${elapsedMs}ms \u2014 aborting so the lock is released instead of held indefinitely`
1741
+ );
1742
+ this.name = "SyncDeadlineExceededError";
1743
+ }
1744
+ };
1745
+ RequestTimeoutError = class extends Error {
1746
+ constructor(url, budgetMs) {
1747
+ super(`memory sync request to ${url} exceeded ${budgetMs}ms`);
1748
+ this.name = "RequestTimeoutError";
1749
+ }
1750
+ };
1751
+ }
1752
+ });
1753
+
1754
+ // src/tools/memory/memory-push-cache.ts
1755
+ import { createHash as createHash3 } from "node:crypto";
1756
+ import { readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "node:fs";
1757
+ import { join as join9 } from "node:path";
1758
+ function sha256(content) {
1759
+ return createHash3("sha256").update(content, "utf8").digest("hex");
1760
+ }
1761
+ function statePath(memoryDir) {
1762
+ return join9(memoryDir, MEMORY_SYNC_STATE_FILE);
1763
+ }
1764
+ function readPushCache(memoryDir, controlPlaneUrl) {
1765
+ const empty = { controlPlaneUrl, entries: /* @__PURE__ */ new Map(), knowledgeSweptAtMs: null };
1766
+ let raw;
1767
+ try {
1768
+ raw = readFileSync10(statePath(memoryDir), "utf8");
1769
+ } catch {
1770
+ return empty;
1771
+ }
1772
+ let parsed;
1773
+ try {
1774
+ parsed = JSON.parse(raw);
1775
+ } catch {
1776
+ return empty;
1777
+ }
1778
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return empty;
1779
+ const obj = parsed;
1780
+ if (obj["version"] !== STATE_VERSION) return empty;
1781
+ if (obj["controlPlaneUrl"] !== controlPlaneUrl) return empty;
1782
+ const files = obj["entries"];
1783
+ if (typeof files !== "object" || files === null || Array.isArray(files)) return empty;
1784
+ const entries = /* @__PURE__ */ new Map();
1785
+ for (const [name, value] of Object.entries(files)) {
1786
+ if (typeof value !== "object" || value === null || Array.isArray(value)) continue;
1787
+ const row = value;
1788
+ const memoryHash = typeof row["memoryHash"] === "string" ? row["memoryHash"] : void 0;
1789
+ const knowledgeHash = typeof row["knowledgeHash"] === "string" ? row["knowledgeHash"] : void 0;
1790
+ if (memoryHash === void 0 && knowledgeHash === void 0) continue;
1791
+ entries.set(name, {
1792
+ ...memoryHash !== void 0 ? { memoryHash } : {},
1793
+ ...knowledgeHash !== void 0 ? { knowledgeHash } : {}
1794
+ });
1795
+ }
1796
+ const sweptAt = obj["knowledgeSweptAtMs"];
1797
+ return {
1798
+ controlPlaneUrl,
1799
+ entries,
1800
+ // An unreadable/absent sweep stamp reads as NEVER SWEPT, which forces a full
1801
+ // sweep — the fail-closed direction (more upserts, never fewer).
1802
+ knowledgeSweptAtMs: typeof sweptAt === "number" && Number.isFinite(sweptAt) ? sweptAt : null
1803
+ };
1804
+ }
1805
+ function writePushCache(memoryDir, cache) {
1806
+ const entries = {};
1807
+ for (const [name, value] of cache.entries) entries[name] = value;
1808
+ try {
1809
+ writeFileSync5(
1810
+ statePath(memoryDir),
1811
+ `${JSON.stringify(
1812
+ {
1813
+ version: STATE_VERSION,
1814
+ controlPlaneUrl: cache.controlPlaneUrl,
1815
+ knowledgeSweptAtMs: cache.knowledgeSweptAtMs,
1816
+ entries
1817
+ },
1818
+ null,
1819
+ 2
1820
+ )}
1821
+ `,
1822
+ "utf8"
1823
+ );
1824
+ } catch {
1825
+ }
1826
+ }
1827
+ function recordMemoryPush(cache, fileName, payloadHash) {
1828
+ cache.entries.set(fileName, { ...cache.entries.get(fileName), memoryHash: payloadHash });
1829
+ }
1830
+ function recordKnowledgePush(cache, fileName, contentHash) {
1831
+ cache.entries.set(fileName, { ...cache.entries.get(fileName), knowledgeHash: contentHash });
1832
+ }
1833
+ function pruneMissing(cache, presentFileNames) {
1834
+ const present = new Set(presentFileNames);
1835
+ for (const name of [...cache.entries.keys()]) {
1836
+ if (!present.has(name)) cache.entries.delete(name);
1837
+ }
1838
+ }
1839
+ function needsMemoryPush(cache, fileName, payloadHash, serverHasEntry) {
1840
+ if (!serverHasEntry) return true;
1841
+ return cache.entries.get(fileName)?.memoryHash !== payloadHash;
1842
+ }
1843
+ function knowledgeSweepDue(cache, nowMs = Date.now()) {
1844
+ const swept = cache.knowledgeSweptAtMs;
1845
+ if (swept === null || !Number.isFinite(swept)) return true;
1846
+ const age = nowMs - swept;
1847
+ return !(age >= 0 && age < KNOWLEDGE_FULL_SWEEP_MS);
1848
+ }
1849
+ function needsKnowledgePush(cache, fileName, contentHash, sweepDue = false) {
1850
+ if (sweepDue) return true;
1851
+ return cache.entries.get(fileName)?.knowledgeHash !== contentHash;
1852
+ }
1853
+ var MEMORY_SYNC_STATE_FILE, STATE_VERSION, KNOWLEDGE_FULL_SWEEP_MS;
1854
+ var init_memory_push_cache = __esm({
1855
+ "src/tools/memory/memory-push-cache.ts"() {
1856
+ "use strict";
1857
+ MEMORY_SYNC_STATE_FILE = ".memory-sync-state.json";
1858
+ STATE_VERSION = 1;
1859
+ KNOWLEDGE_FULL_SWEEP_MS = 24 * 60 * 6e4;
1860
+ }
1861
+ });
1862
+
1863
+ // src/tools/memory/memory-sync-http.ts
1864
+ import { existsSync as existsSync5, mkdirSync as mkdirSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync6, readdirSync as readdirSync4 } from "node:fs";
1865
+ async function pullMemory(controlPlaneUrl, token, memoryDir, fetchFn) {
1866
+ const url = `${controlPlaneUrl}/api/v1/agent-config/memory/me`;
1867
+ const response = await withRequestTimeout(
1868
+ url,
1869
+ () => fetchFn(url, {
1870
+ method: "GET",
1871
+ headers: {
1872
+ authorization: `Bearer ${token}`
1873
+ }
1874
+ })
1875
+ );
1876
+ if (response.status !== 200) {
1877
+ const text = await response.text();
1878
+ throw new Error(`GET /api/v1/agent-config/memory/me returned HTTP ${response.status}: ${text.slice(0, 200)}`);
1879
+ }
1880
+ const data = JSON.parse(await response.text());
1881
+ if (!data.ok || !Array.isArray(data.entries)) {
1882
+ throw new Error("GET /api/v1/agent-config/memory/me response missing ok=true or entries array");
1883
+ }
1884
+ const writes = data.entries.map((entry) => ({
1885
+ entry,
1886
+ filePath: resolveMemoryFilePath(memoryDir, entry.file_name)
1887
+ }));
1888
+ mkdirSync6(memoryDir, { recursive: true });
1889
+ const files = [];
1890
+ for (const { entry, filePath } of writes) {
1891
+ writeFileSync6(filePath, entry.content, "utf8");
1892
+ files.push(entry.file_name);
1893
+ }
1894
+ return { pulled: data.entries.length, files };
1895
+ }
1896
+ function listPushableFiles(memoryDir) {
1897
+ return readdirSync4(memoryDir).filter((f) => f.endsWith(".md") && f !== MEMORY_SYNC_LOCK_FILE);
1898
+ }
1899
+ async function uploadOne(item, controlPlaneUrl, token, sessionId, fetchFn, deadline) {
1900
+ deadline.check();
1901
+ if (item.memoryId !== null) {
1902
+ const updateUrl = `${controlPlaneUrl}/api/v1/agent-config/memory/${item.memoryId}`;
1903
+ const updateBody = { content: item.content, session_id: sessionId };
1904
+ const updateResponse = await withRequestTimeout(
1905
+ updateUrl,
1906
+ () => fetchFn(updateUrl, {
1907
+ method: "PUT",
1908
+ headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
1909
+ body: JSON.stringify(updateBody)
1910
+ })
1911
+ );
1912
+ if (updateResponse.status !== 200) {
1913
+ const text = await updateResponse.text();
1914
+ throw new Error(
1915
+ `PUT /api/v1/agent-config/memory/${item.memoryId} returned HTTP ${updateResponse.status}: ${text.slice(0, 200)}`
1916
+ );
1917
+ }
1918
+ const updateData = JSON.parse(await updateResponse.text());
1919
+ if (!updateData.ok) throw new Error(`PUT /api/v1/agent-config/memory/${item.memoryId} returned ok=false`);
1920
+ return "updated";
1921
+ }
1922
+ const createUrl = `${controlPlaneUrl}/api/v1/agent-config/memory/me`;
1923
+ const createBody = {
1924
+ entry_type: item.entryType,
1925
+ file_name: item.fileName,
1926
+ content: item.content,
1927
+ session_id: sessionId
1928
+ };
1929
+ const createResponse = await withRequestTimeout(
1930
+ createUrl,
1931
+ () => fetchFn(createUrl, {
1932
+ method: "POST",
1933
+ headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
1934
+ body: JSON.stringify(createBody)
1935
+ })
1936
+ );
1937
+ if (createResponse.status !== 200 && createResponse.status !== 201) {
1938
+ const text = await createResponse.text();
1939
+ throw new Error(
1940
+ `POST /api/v1/agent-config/memory/me returned HTTP ${createResponse.status}: ${text.slice(0, 200)}`
1941
+ );
1942
+ }
1943
+ const createData = JSON.parse(await createResponse.text());
1944
+ if (!createData.ok) throw new Error("POST /api/v1/agent-config/memory/me returned ok=false");
1945
+ return "created";
1946
+ }
1947
+ async function pushMemory(controlPlaneUrl, token, memoryDir, sessionId, fetchFn, options = {}) {
1948
+ const empty = { pushed: 0, created: 0, updated: 0, skipped: 0, indexRowsPreserved: 0 };
1949
+ if (!existsSync5(memoryDir)) {
1950
+ return empty;
1951
+ }
1952
+ const localFiles = listPushableFiles(memoryDir).map((f) => ({
1953
+ file_name: f,
1954
+ content: readFileSync11(resolveMemoryFilePath(memoryDir, f), "utf8"),
1955
+ entry_type: isMemoryIndexFile(f) ? "index" : "topic"
1956
+ }));
1957
+ if (localFiles.length === 0) {
1958
+ return empty;
1959
+ }
1960
+ const deadline = options.deadline ?? createSyncDeadline();
1961
+ const cache = options.cache ?? readPushCache(memoryDir, controlPlaneUrl);
1962
+ deadline.check();
1963
+ const getUrl = `${controlPlaneUrl}/api/v1/agent-config/memory/me`;
1964
+ const getResponse = await withRequestTimeout(
1965
+ getUrl,
1966
+ () => fetchFn(getUrl, { method: "GET", headers: { authorization: `Bearer ${token}` } })
1967
+ );
1968
+ const existingMap = /* @__PURE__ */ new Map();
1969
+ if (getResponse.status === 200) {
1970
+ const getData = JSON.parse(await getResponse.text());
1971
+ if (getData.ok && Array.isArray(getData.entries)) {
1972
+ for (const entry of getData.entries) {
1973
+ existingMap.set(entry.file_name, {
1974
+ memoryId: entry.memory_id,
1975
+ content: typeof entry.content === "string" ? entry.content : ""
1976
+ });
1977
+ }
1978
+ }
1979
+ }
1980
+ const toUpload = [];
1981
+ let skipped = 0;
1982
+ for (const localFile of localFiles) {
1983
+ const existing = existingMap.get(localFile.file_name);
1984
+ let content = localFile.content;
1985
+ let rowsPreserved = 0;
1986
+ if (localFile.entry_type === "index") {
1987
+ const merged = mergeMemoryIndex(localFile.content, existing?.content);
1988
+ content = merged.content;
1989
+ rowsPreserved = merged.addedFromCloud.length;
1990
+ }
1991
+ const payloadHash = sha256(content);
1992
+ if (!needsMemoryPush(cache, localFile.file_name, payloadHash, existing !== void 0)) {
1993
+ skipped++;
1994
+ continue;
1995
+ }
1996
+ toUpload.push({
1997
+ fileName: localFile.file_name,
1998
+ entryType: localFile.entry_type,
1999
+ content,
2000
+ payloadHash,
2001
+ memoryId: existing?.memoryId ?? null,
2002
+ rowsPreserved
2003
+ });
2004
+ }
2005
+ const outcomes = await mapWithConcurrency(
2006
+ toUpload,
2007
+ options.concurrency ?? PUSH_CONCURRENCY,
2008
+ (item) => uploadOne(item, controlPlaneUrl, token, sessionId, fetchFn, deadline)
2009
+ );
2010
+ let created = 0;
2011
+ let updated = 0;
2012
+ let indexRowsPreserved = 0;
2013
+ let firstError;
2014
+ for (let i = 0; i < outcomes.length; i++) {
2015
+ const outcome = outcomes[i];
2016
+ const item = toUpload[i];
2017
+ if (outcome.ok) {
2018
+ if (outcome.value === "created") created++;
2019
+ else updated++;
2020
+ indexRowsPreserved += item.rowsPreserved;
2021
+ recordMemoryPush(cache, item.fileName, item.payloadHash);
2022
+ } else if (firstError === void 0) {
2023
+ firstError = outcome.error;
2024
+ }
2025
+ }
2026
+ pruneMissing(cache, localFiles.map((f) => f.file_name));
2027
+ if (options.persistCache ?? options.cache === void 0) writePushCache(memoryDir, cache);
2028
+ if (firstError !== void 0) throw firstError;
2029
+ return { pushed: created + updated, created, updated, skipped, indexRowsPreserved };
2030
+ }
2031
+ var init_memory_sync_http = __esm({
2032
+ "src/tools/memory/memory-sync-http.ts"() {
2033
+ "use strict";
2034
+ init_safe_memory_file();
2035
+ init_sync_lock();
2036
+ init_memory_index_merge();
2037
+ init_bounded_sync();
2038
+ init_memory_push_cache();
2039
+ }
2040
+ });
2041
+
2042
+ // src/tools/memory/sync-kill-switch.ts
2043
+ import { existsSync as existsSync6, readFileSync as readFileSync12 } from "node:fs";
2044
+ import { homedir as homedir6 } from "node:os";
2045
+ import { join as join10 } from "node:path";
2046
+ function memorySyncSentinelPath(home) {
2047
+ return join10(home, ".claude", MEMORY_SYNC_DISABLE_SENTINEL);
2048
+ }
2049
+ function isKillSwitchValueOn(raw) {
2050
+ if (raw === void 0 || raw === null) return false;
2051
+ const v = raw.trim().toLowerCase();
2052
+ if (v === "") return false;
2053
+ return !NEGATIONS.has(v);
2054
+ }
2055
+ function clip(raw) {
2056
+ const v = raw.trim();
2057
+ return v.length > MAX_LOGGED_VALUE ? `${v.slice(0, MAX_LOGGED_VALUE)}\u2026` : v;
2058
+ }
2059
+ function evaluateMemorySyncKillSwitch(deps = {}) {
2060
+ const env = deps.env ?? process.env;
2061
+ const home = deps.home ?? homedir6();
2062
+ const fileExists = deps.fileExists ?? existsSync6;
2063
+ const readFile3 = deps.readFile ?? ((p) => readFileSync12(p, "utf8"));
2064
+ const fired = [];
2065
+ const rawEnv = env[MEMORY_SYNC_DISABLE_ENV];
2066
+ if (isKillSwitchValueOn(rawEnv)) {
2067
+ fired.push(`env ${MEMORY_SYNC_DISABLE_ENV}=${clip(rawEnv)}`);
2068
+ }
2069
+ const sentinel = memorySyncSentinelPath(home);
2070
+ let sentinelPresent;
2071
+ try {
2072
+ sentinelPresent = fileExists(sentinel);
2073
+ } catch {
2074
+ sentinelPresent = false;
2075
+ }
2076
+ if (sentinelPresent) {
2077
+ let contents = "";
2078
+ let readable = true;
2079
+ try {
2080
+ contents = readFile3(sentinel);
2081
+ } catch {
2082
+ readable = false;
2083
+ }
2084
+ if (!readable || isKillSwitchValueOn(contents) || contents.trim() === "") {
2085
+ fired.push(`sentinel file ${sentinel}`);
2086
+ }
2087
+ }
2088
+ if (fired.length === 0) return { disabled: false, reason: null };
2089
+ return { disabled: true, reason: `memory sync DISABLED by ${fired.join(" + ")}` };
2090
+ }
2091
+ var MEMORY_SYNC_DISABLE_ENV, MEMORY_SYNC_DISABLE_SENTINEL, NEGATIONS, MAX_LOGGED_VALUE;
2092
+ var init_sync_kill_switch = __esm({
2093
+ "src/tools/memory/sync-kill-switch.ts"() {
2094
+ "use strict";
2095
+ MEMORY_SYNC_DISABLE_ENV = "VO_MCP_DISABLE_MEMORY_SYNC";
2096
+ MEMORY_SYNC_DISABLE_SENTINEL = "vo-memory-sync-disabled";
2097
+ NEGATIONS = /* @__PURE__ */ new Set(["0", "false", "no"]);
2098
+ MAX_LOGGED_VALUE = 32;
2099
+ }
2100
+ });
2101
+
1402
2102
  // src/tools/memory/memory-knowledge-bridge.ts
1403
2103
  var memory_knowledge_bridge_exports = {};
1404
2104
  __export(memory_knowledge_bridge_exports, {
1405
2105
  extractMemoryTitle: () => extractMemoryTitle,
1406
2106
  upsertMemoryFilesAsKnowledge: () => upsertMemoryFilesAsKnowledge
1407
2107
  });
1408
- import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as readFileSync8 } from "node:fs";
2108
+ import { existsSync as existsSync7, readdirSync as readdirSync5, readFileSync as readFileSync13 } from "node:fs";
1409
2109
  function extractMemoryTitle(fileName, content) {
1410
2110
  const frontmatter = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
1411
2111
  if (frontmatter) {
@@ -1417,13 +2117,13 @@ function extractMemoryTitle(fileName, content) {
1417
2117
  return fileName;
1418
2118
  }
1419
2119
  async function upsertMemoryFilesAsKnowledge(options) {
1420
- const { controlPlaneUrl, token, memoryDir, fetchFn } = options;
2120
+ const { controlPlaneUrl, token, memoryDir, fetchFn, cache, deadline } = options;
1421
2121
  let files;
1422
2122
  try {
1423
- if (!existsSync5(memoryDir)) {
1424
- return { attempted: 0, upserted: 0, failed: 0, failures: [] };
2123
+ if (!existsSync7(memoryDir)) {
2124
+ return { attempted: 0, upserted: 0, failed: 0, skipped: 0, failures: [] };
1425
2125
  }
1426
- files = readdirSync4(memoryDir).filter(
2126
+ files = readdirSync5(memoryDir).filter(
1427
2127
  (f) => f.endsWith(".md") && f.toUpperCase() !== "MEMORY.MD"
1428
2128
  );
1429
2129
  } catch (err) {
@@ -1431,54 +2131,81 @@ async function upsertMemoryFilesAsKnowledge(options) {
1431
2131
  attempted: 0,
1432
2132
  upserted: 0,
1433
2133
  failed: 1,
2134
+ skipped: 0,
1434
2135
  failures: [`memory dir scan: ${err instanceof Error ? err.message : String(err)}`]
1435
2136
  };
1436
2137
  }
1437
- let upserted = 0;
2138
+ const sweepDue = cache ? knowledgeSweepDue(cache) : true;
2139
+ const candidates = [];
1438
2140
  const failures = [];
2141
+ let skipped = 0;
1439
2142
  for (const fileName of files) {
1440
2143
  try {
1441
- const content = readFileSync8(resolveMemoryFilePath(memoryDir, fileName), "utf8");
2144
+ const content = readFileSync13(resolveMemoryFilePath(memoryDir, fileName), "utf8");
1442
2145
  if (content.length > CONTENT_HARD_LIMIT) {
1443
2146
  failures.push(`${fileName}: ${content.length} chars exceeds the ${CONTENT_HARD_LIMIT} server limit \u2014 split the memory file`);
1444
2147
  continue;
1445
2148
  }
1446
- const title = extractMemoryTitle(fileName, content);
1447
- const base = {
1448
- knowledge_class: "memory",
1449
- source_path: `memory/${fileName}`,
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)}`);
2149
+ const hash = sha256(content);
2150
+ if (cache && !needsKnowledgePush(cache, fileName, hash, sweepDue)) {
2151
+ skipped += 1;
2152
+ continue;
1473
2153
  }
2154
+ candidates.push({ fileName, content, hash });
1474
2155
  } catch (err) {
1475
2156
  failures.push(`${fileName}: ${err instanceof Error ? err.message : String(err)}`);
1476
2157
  }
1477
2158
  }
2159
+ const url = `${controlPlaneUrl}/api/v1/knowledge/private`;
2160
+ const outcomes = await mapWithConcurrency(candidates, options.concurrency ?? PUSH_CONCURRENCY, async (candidate) => {
2161
+ deadline?.check();
2162
+ const base = {
2163
+ knowledge_class: "memory",
2164
+ source_path: `memory/${candidate.fileName}`,
2165
+ title: extractMemoryTitle(candidate.fileName, candidate.content),
2166
+ content: candidate.content
2167
+ };
2168
+ const post = (body) => withRequestTimeout(url, () => fetchFn(url, {
2169
+ method: "POST",
2170
+ headers: {
2171
+ authorization: `Bearer ${token}`,
2172
+ "content-type": "application/json"
2173
+ },
2174
+ body: JSON.stringify(body)
2175
+ }));
2176
+ let response = await post({
2177
+ ...base,
2178
+ provenance: { written_by: "memory-bridge", source_kind: "operator_memory" }
2179
+ });
2180
+ if (response.status === 400) {
2181
+ response = await post(base);
2182
+ }
2183
+ if (response.status >= 200 && response.status < 300) return true;
2184
+ const text = await response.text();
2185
+ throw new Error(`HTTP ${response.status} ${text.slice(0, 80)}`);
2186
+ });
2187
+ let upserted = 0;
2188
+ for (let i = 0; i < outcomes.length; i++) {
2189
+ const outcome = outcomes[i];
2190
+ const candidate = candidates[i];
2191
+ if (outcome.ok) {
2192
+ upserted += 1;
2193
+ if (cache) recordKnowledgePush(cache, candidate.fileName, candidate.hash);
2194
+ } else {
2195
+ const error = outcome.error;
2196
+ failures.push(`${candidate.fileName}: ${error instanceof Error ? error.message : String(error)}`);
2197
+ }
2198
+ }
2199
+ if (cache && sweepDue && failures.length === 0) {
2200
+ cache.knowledgeSweptAtMs = Date.now();
2201
+ }
1478
2202
  return {
2203
+ // Every memory file this run considered. `attempted === upserted + skipped
2204
+ // + failed` holds, so a caller can tell "nothing to do" from "nothing ran".
1479
2205
  attempted: files.length,
1480
2206
  upserted,
1481
2207
  failed: failures.length,
2208
+ skipped,
1482
2209
  failures: failures.slice(0, 5)
1483
2210
  };
1484
2211
  }
@@ -1487,6 +2214,8 @@ var init_memory_knowledge_bridge = __esm({
1487
2214
  "src/tools/memory/memory-knowledge-bridge.ts"() {
1488
2215
  "use strict";
1489
2216
  init_safe_memory_file();
2217
+ init_bounded_sync();
2218
+ init_memory_push_cache();
1490
2219
  CONTENT_HARD_LIMIT = 5e5;
1491
2220
  }
1492
2221
  });
@@ -1503,9 +2232,9 @@ __export(sync_config_exports, {
1503
2232
  isNoopSyncReason: () => isNoopSyncReason,
1504
2233
  runMemorySync: () => runMemorySync
1505
2234
  });
1506
- import { homedir as homedir6 } from "node:os";
1507
- import { join as join8 } from "node:path";
1508
- import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync4, readdirSync as readdirSync5 } from "node:fs";
2235
+ import { existsSync as existsSync8 } from "node:fs";
2236
+ import { homedir as homedir7 } from "node:os";
2237
+ import { join as join11 } from "node:path";
1509
2238
  function isToolInput22(v) {
1510
2239
  if (typeof v !== "object" || v === null) return false;
1511
2240
  const o = v;
@@ -1518,129 +2247,17 @@ function deriveProjectSlug(cwd) {
1518
2247
  }
1519
2248
  function getMemoryDir(cwd) {
1520
2249
  const slug = deriveProjectSlug(cwd);
1521
- return join8(homedir6(), ".claude", "projects", slug, "memory");
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 };
2250
+ return join11(homedir7(), ".claude", "projects", slug, "memory");
1638
2251
  }
1639
2252
  function isNoopSyncReason(reason) {
1640
2253
  if (!reason) return false;
1641
- return /not set|No auth configured|Failed to obtain auth token/.test(reason);
2254
+ return /not set|No auth configured|Failed to obtain auth token|DISABLED by/.test(reason);
1642
2255
  }
1643
- async function runMemorySync(action, cwd, sessionId, fetchFn = globalThis.fetch) {
2256
+ async function runMemorySync(action, cwd, sessionId, fetchFn = globalThis.fetch, lockOptions = {}) {
2257
+ const killSwitch = evaluateMemorySyncKillSwitch();
2258
+ if (killSwitch.disabled) {
2259
+ return { synced: false, reason: killSwitch.reason };
2260
+ }
1644
2261
  const controlPlaneUrl = process.env["VO_CONTROL_PLANE_URL"];
1645
2262
  if (!controlPlaneUrl) {
1646
2263
  return { synced: false, reason: "VO_CONTROL_PLANE_URL not set \u2014 cloud mode disabled" };
@@ -1657,39 +2274,79 @@ async function runMemorySync(action, cwd, sessionId, fetchFn = globalThis.fetch)
1657
2274
  }
1658
2275
  const memoryDir = getMemoryDir(cwd);
1659
2276
  const baseUrl = controlPlaneUrl.replace(/\/+$/, "");
1660
- try {
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
- }
2277
+ if (action === "push" && !existsSync8(memoryDir)) {
1682
2278
  return {
1683
2279
  synced: true,
1684
2280
  action: "push",
1685
- pushed: result.pushed,
1686
- created: result.created,
1687
- updated: result.updated,
1688
- memory_dir: memoryDir,
1689
- knowledge_upserted: bridge.upserted,
1690
- knowledge_failed: bridge.failed,
1691
- ...bridge.failed > 0 ? { knowledge_failures: bridge.failures } : {}
2281
+ pushed: 0,
2282
+ created: 0,
2283
+ updated: 0,
2284
+ skipped: 0,
2285
+ index_rows_preserved: 0,
2286
+ knowledge_upserted: 0,
2287
+ knowledge_failed: 0,
2288
+ memory_dir: memoryDir
1692
2289
  };
2290
+ }
2291
+ try {
2292
+ return await withMemorySyncLock({ ...lockOptions, memoryDir, sessionId, createDir: action === "pull" }, async (lock) => {
2293
+ const takeover = lock.tookOverFrom ? { lock_taken_over_from_pid: lock.tookOverFrom.pid } : {};
2294
+ if (action === "pull") {
2295
+ const result2 = await pullMemory(baseUrl, token, memoryDir, fetchFn);
2296
+ return {
2297
+ synced: true,
2298
+ action: "pull",
2299
+ pulled: result2.pulled,
2300
+ files: result2.files,
2301
+ memory_dir: memoryDir,
2302
+ ...takeover
2303
+ };
2304
+ }
2305
+ const deadline = createSyncDeadline();
2306
+ const cache = readPushCache(memoryDir, baseUrl);
2307
+ let result;
2308
+ try {
2309
+ result = await pushMemory(baseUrl, token, memoryDir, sessionId, fetchFn, { cache, deadline });
2310
+ } catch (err) {
2311
+ writePushCache(memoryDir, cache);
2312
+ throw err;
2313
+ }
2314
+ let bridge;
2315
+ try {
2316
+ const { upsertMemoryFilesAsKnowledge: upsertMemoryFilesAsKnowledge2 } = await Promise.resolve().then(() => (init_memory_knowledge_bridge(), memory_knowledge_bridge_exports));
2317
+ bridge = await upsertMemoryFilesAsKnowledge2({
2318
+ controlPlaneUrl: baseUrl,
2319
+ token,
2320
+ memoryDir,
2321
+ fetchFn,
2322
+ cache,
2323
+ deadline
2324
+ });
2325
+ } catch (err) {
2326
+ bridge = {
2327
+ upserted: 0,
2328
+ failed: 1,
2329
+ skipped: 0,
2330
+ failures: [`bridge unavailable: ${err instanceof Error ? err.message : String(err)}`]
2331
+ };
2332
+ }
2333
+ writePushCache(memoryDir, cache);
2334
+ return {
2335
+ synced: true,
2336
+ action: "push",
2337
+ pushed: result.pushed,
2338
+ created: result.created,
2339
+ updated: result.updated,
2340
+ skipped: result.skipped,
2341
+ index_rows_preserved: result.indexRowsPreserved,
2342
+ memory_dir: memoryDir,
2343
+ knowledge_upserted: bridge.upserted,
2344
+ knowledge_failed: bridge.failed,
2345
+ knowledge_skipped: bridge.skipped,
2346
+ ...bridge.failed > 0 ? { knowledge_failures: bridge.failures } : {},
2347
+ ...takeover
2348
+ };
2349
+ });
1693
2350
  } catch (err) {
1694
2351
  const message = err instanceof Error ? err.message : String(err);
1695
2352
  return { synced: false, reason: `Sync failed: ${message}` };
@@ -1711,7 +2368,11 @@ var init_sync_config = __esm({
1711
2368
  "src/tools/memory/sync-config.ts"() {
1712
2369
  "use strict";
1713
2370
  init_common();
1714
- init_safe_memory_file();
2371
+ init_memory_sync_http();
2372
+ init_bounded_sync();
2373
+ init_memory_push_cache();
2374
+ init_sync_lock();
2375
+ init_sync_kill_switch();
1715
2376
  TOOL_NAME22 = "vo_sync_config";
1716
2377
  inputSchema22 = {
1717
2378
  type: "object",
@@ -1729,19 +2390,19 @@ var init_sync_config = __esm({
1729
2390
  required: ["action"],
1730
2391
  additionalProperties: false
1731
2392
  };
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.";
2393
+ 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
2394
  }
1734
2395
  });
1735
2396
 
1736
2397
  // src/cli.ts
1737
- import { homedir as homedir7, hostname } from "node:os";
1738
- import { randomUUID as randomUUID5 } from "node:crypto";
1739
- import { join as join11 } from "node:path";
2398
+ import { homedir as homedir8, hostname as hostname2 } from "node:os";
2399
+ import { randomUUID as randomUUID6 } from "node:crypto";
2400
+ import { join as join14 } from "node:path";
1740
2401
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
1741
2402
 
1742
2403
  // src/server.ts
1743
2404
  init_common();
1744
- import { randomUUID as randomUUID2 } from "node:crypto";
2405
+ import { randomUUID as randomUUID3 } from "node:crypto";
1745
2406
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
1746
2407
  import {
1747
2408
  CallToolRequestSchema,
@@ -6319,12 +6980,12 @@ async function handleHqWhiteboardRead(_deps, rawInput, signal) {
6319
6980
  }
6320
6981
 
6321
6982
  // src/tools/skills/skill-corpus.ts
6322
- import { existsSync as existsSync7, statSync as statSync5 } from "node:fs";
6323
- import { dirname as dirname5, isAbsolute, join as join10, resolve as resolve2 } from "node:path";
6983
+ import { existsSync as existsSync9, statSync as statSync6 } from "node:fs";
6984
+ import { dirname as dirname5, isAbsolute, join as join13, resolve as resolve2 } from "node:path";
6324
6985
 
6325
6986
  // ../skill-registry/src/loader.ts
6326
- import { readdirSync as readdirSync6, readFileSync as readFileSync10, statSync as statSync4 } from "node:fs";
6327
- import { join as join9 } from "node:path";
6987
+ import { readdirSync as readdirSync6, readFileSync as readFileSync14, statSync as statSync5 } from "node:fs";
6988
+ import { join as join12 } from "node:path";
6328
6989
  var InvalidSkillFrontmatterError = class extends Error {
6329
6990
  constructor(skillFile, reason) {
6330
6991
  super(`Invalid frontmatter in ${skillFile}: ${reason}`);
@@ -6377,18 +7038,18 @@ function loadSkillsFromDir(skillsDir) {
6377
7038
  const entries = readdirSync6(skillsDir);
6378
7039
  const skills = [];
6379
7040
  for (const entry of entries) {
6380
- const entryPath = join9(skillsDir, entry);
7041
+ const entryPath = join12(skillsDir, entry);
6381
7042
  let stat;
6382
7043
  try {
6383
- stat = statSync4(entryPath);
7044
+ stat = statSync5(entryPath);
6384
7045
  } catch {
6385
7046
  continue;
6386
7047
  }
6387
7048
  if (!stat.isDirectory()) continue;
6388
- const skillFile = join9(entryPath, "SKILL.md");
7049
+ const skillFile = join12(entryPath, "SKILL.md");
6389
7050
  let raw;
6390
7051
  try {
6391
- raw = readFileSync10(skillFile, "utf8");
7052
+ raw = readFileSync14(skillFile, "utf8");
6392
7053
  } catch {
6393
7054
  continue;
6394
7055
  }
@@ -6430,12 +7091,12 @@ function resolveSkillsDir(env = process.env, startDir = process.cwd()) {
6430
7091
  const override = env.VO_SKILLS_DIR;
6431
7092
  if (typeof override === "string" && override.length > 0) {
6432
7093
  const abs = isAbsolute(override) ? override : resolve2(startDir, override);
6433
- return existsSync7(abs) && statSync5(abs).isDirectory() ? abs : null;
7094
+ return existsSync9(abs) && statSync6(abs).isDirectory() ? abs : null;
6434
7095
  }
6435
7096
  let dir = resolve2(startDir);
6436
7097
  for (let i = 0; i < MAX_WALK_UP_LEVELS; i += 1) {
6437
- const candidate = join10(dir, ".claude", "skills");
6438
- if (existsSync7(candidate) && statSync5(candidate).isDirectory()) return candidate;
7098
+ const candidate = join13(dir, ".claude", "skills");
7099
+ if (existsSync9(candidate) && statSync6(candidate).isDirectory()) return candidate;
6439
7100
  const parent = dirname5(dir);
6440
7101
  if (parent === dir) break;
6441
7102
  dir = parent;
@@ -6746,7 +7407,7 @@ function buildToolRegistry() {
6746
7407
  };
6747
7408
  }
6748
7409
  function createServer(options) {
6749
- const sessionId = options.sessionId ?? randomUUID2();
7410
+ const sessionId = options.sessionId ?? randomUUID3();
6750
7411
  const mode = createLocalMode();
6751
7412
  const now = options.now ?? (() => /* @__PURE__ */ new Date());
6752
7413
  const server = new Server(
@@ -6795,8 +7456,8 @@ function createServer(options) {
6795
7456
  }
6796
7457
 
6797
7458
  // src/cache/sqlite-cache.ts
6798
- import { createHash as createHash3 } from "node:crypto";
6799
- import { chmodSync as chmodSync3, mkdirSync as mkdirSync6 } from "node:fs";
7459
+ import { createHash as createHash4 } from "node:crypto";
7460
+ import { chmodSync as chmodSync3, mkdirSync as mkdirSync7 } from "node:fs";
6800
7461
  import { dirname as dirname6 } from "node:path";
6801
7462
  import { DatabaseSync } from "node:sqlite";
6802
7463
 
@@ -6842,7 +7503,7 @@ function normalizeString(s) {
6842
7503
  function createSqliteCache(options) {
6843
7504
  const fileBacked = options.dbPath !== ":memory:";
6844
7505
  if (fileBacked) {
6845
- mkdirSync6(dirname6(options.dbPath), { recursive: true, mode: 448 });
7506
+ mkdirSync7(dirname6(options.dbPath), { recursive: true, mode: 448 });
6846
7507
  }
6847
7508
  const versionNamespace = options.cacheVersionNamespace ?? "";
6848
7509
  const db = new DatabaseSync(options.dbPath);
@@ -6873,7 +7534,7 @@ function createSqliteCache(options) {
6873
7534
  return {
6874
7535
  keyFor(toolName, input, opts) {
6875
7536
  const canonical = canonicalize(input, opts);
6876
- const hash = createHash3("sha256");
7537
+ const hash = createHash4("sha256");
6877
7538
  if (versionNamespace.length > 0) {
6878
7539
  hash.update(versionNamespace);
6879
7540
  hash.update("|");
@@ -6968,7 +7629,7 @@ function createStubRatchetClient() {
6968
7629
  let m;
6969
7630
  while ((m = pat.regex.exec(req.source)) !== null) {
6970
7631
  findings.push({
6971
- line_excerpt: clip(m[0], 80),
7632
+ line_excerpt: clip2(m[0], 80),
6972
7633
  severity: pat.severity,
6973
7634
  code: pat.code,
6974
7635
  message: pat.message
@@ -7000,7 +7661,7 @@ function createStubRatchetClient() {
7000
7661
  }
7001
7662
  };
7002
7663
  }
7003
- function clip(s, n) {
7664
+ function clip2(s, n) {
7004
7665
  return s.length <= n ? s : s.slice(0, n) + "\u2026";
7005
7666
  }
7006
7667
  function buildSummary2(args) {
@@ -7011,7 +7672,7 @@ function buildSummary2(args) {
7011
7672
  // src/consensus/engine-client.ts
7012
7673
  init_events_writer();
7013
7674
  init_common();
7014
- import { randomUUID as randomUUID3 } from "node:crypto";
7675
+ import { randomUUID as randomUUID4 } from "node:crypto";
7015
7676
 
7016
7677
  // src/consensus/null-client.ts
7017
7678
  var NULL_CLIENT_DEFAULT_REASON = "consensus-engine-package-pending";
@@ -7471,7 +8132,7 @@ function tryCreateEngineConsensusClientFromEnv(options = {}) {
7471
8132
  }
7472
8133
 
7473
8134
  // src/consensus/moat-client.ts
7474
- import { randomUUID as randomUUID4 } from "node:crypto";
8135
+ import { randomUUID as randomUUID5 } from "node:crypto";
7475
8136
 
7476
8137
  // src/consensus/client.ts
7477
8138
  var CANCELLED_REASON = "cancelled";
@@ -7523,7 +8184,7 @@ function createMoatConsensusClient(opts) {
7523
8184
  request.signal?.addEventListener("abort", onAbort, { once: true });
7524
8185
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
7525
8186
  const body = JSON.stringify({
7526
- task_id: randomUUID4(),
8187
+ task_id: randomUUID5(),
7527
8188
  gate_type: request.gate_type,
7528
8189
  excerpt: request.prompt,
7529
8190
  ...request.system_prompt ? { question: request.system_prompt } : {},
@@ -7844,7 +8505,7 @@ init_common();
7844
8505
  function defaultCacheDbPath() {
7845
8506
  const env = process.env["VO_MCP_DB_PATH"];
7846
8507
  if (env && env.length > 0) return env;
7847
- return join11(homedir7(), ".claude", "vo-mcp-cache.db");
8508
+ return join14(homedir8(), ".claude", "vo-mcp-cache.db");
7848
8509
  }
7849
8510
  async function probeEngineVersion() {
7850
8511
  try {
@@ -7971,7 +8632,7 @@ async function main() {
7971
8632
  }
7972
8633
  if (process.argv[2] === "login") {
7973
8634
  const controlPlaneUrl = process.env["VO_CONTROL_PLANE_URL"]?.trim();
7974
- const credentialLabel = `vo-mcp-cli@${hostname()}`.slice(0, 200);
8635
+ const credentialLabel = `vo-mcp-cli@${hostname2()}`.slice(0, 200);
7975
8636
  runLogin(
7976
8637
  controlPlaneUrl ? {
7977
8638
  exchange: (refreshToken, apiKey) => exchangeForVoCredential({ refreshToken, apiKey, controlPlaneUrl, label: credentialLabel })
@@ -7987,26 +8648,29 @@ if (process.argv[2] === "login") {
7987
8648
  } else if (process.argv[2] === "sync") {
7988
8649
  const action = process.argv[3];
7989
8650
  if (action !== "push" && action !== "pull") {
7990
- console.error("[vo-mcp] usage: vo-mcp sync <push|pull> [--cwd <path>]");
8651
+ console.error("[vo-mcp] usage: vo-mcp sync <push|pull> [--cwd <path>] [--lock-wait-ms <n>]");
7991
8652
  process.exit(2);
7992
8653
  }
7993
8654
  const cwdFlag = process.argv.indexOf("--cwd");
7994
8655
  const cwd = cwdFlag >= 0 && typeof process.argv[cwdFlag + 1] === "string" ? process.argv[cwdFlag + 1] : process.cwd();
7995
- const sessionId = randomUUID5();
8656
+ const waitFlag = process.argv.indexOf("--lock-wait-ms");
8657
+ const parsedWait = waitFlag >= 0 ? Number(process.argv[waitFlag + 1]) : Number.NaN;
8658
+ const lockOptions = Number.isFinite(parsedWait) && parsedWait >= 0 ? { waitMs: parsedWait } : {};
8659
+ const sessionId = randomUUID6();
7996
8660
  const appendSyncLog = async (line) => {
7997
8661
  try {
7998
- const { appendFileSync: appendFileSync2, mkdirSync: mkdirSync7 } = await import("node:fs");
7999
- const { join: join12 } = await import("node:path");
8000
- const { homedir: homedir8 } = await import("node:os");
8001
- const dir = join12(homedir8(), ".claude");
8002
- mkdirSync7(dir, { recursive: true });
8003
- appendFileSync2(join12(dir, "vo-mcp-sync.log"), `${line}
8662
+ const { appendFileSync: appendFileSync2, mkdirSync: mkdirSync8 } = await import("node:fs");
8663
+ const { join: join15 } = await import("node:path");
8664
+ const { homedir: homedir9 } = await import("node:os");
8665
+ const dir = join15(homedir9(), ".claude");
8666
+ mkdirSync8(dir, { recursive: true });
8667
+ appendFileSync2(join15(dir, "vo-mcp-sync.log"), `${line}
8004
8668
  `, "utf8");
8005
8669
  } catch {
8006
8670
  }
8007
8671
  };
8008
8672
  Promise.resolve().then(() => (init_sync_config(), sync_config_exports)).then(async ({ runMemorySync: runMemorySync2, isNoopSyncReason: isNoopSyncReason2 }) => {
8009
- const r = await runMemorySync2(action, cwd, sessionId);
8673
+ const r = await runMemorySync2(action, cwd, sessionId, void 0, lockOptions);
8010
8674
  const stamp = `${sessionId} ${action}`;
8011
8675
  if (r.synced) {
8012
8676
  console.error(`[vo-mcp] sync ${action} ok: ${JSON.stringify(r)}`);