@algosuite/vo-mcp 0.2.0-beta.28 → 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 readFileSync7 } 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 = readFileSync7(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 homedir5 } from "node:os";
1507
- import { join as join7 } from "node:path";
1508
- import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync8, writeFileSync as writeFileSync3, 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 join7(homedir5(), ".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
- mkdirSync4(memoryDir, { recursive: true });
1544
- const files = [];
1545
- for (const { entry, filePath } of writes) {
1546
- writeFileSync3(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: readFileSync8(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 homedir6, hostname } from "node:os";
1738
- import { randomUUID as randomUUID5 } from "node:crypto";
1739
- import { join as join10 } 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,
@@ -5394,9 +6055,324 @@ async function handleReportSessionState(deps, rawInput, _signal) {
5394
6055
  // src/tools/session/spawn-successor.ts
5395
6056
  init_common();
5396
6057
  import { spawn } from "node:child_process";
6058
+ import { homedir as homedir5 } from "node:os";
6059
+ import { join as join7 } from "node:path";
6060
+ import { existsSync as existsSync4, mkdirSync as mkdirSync4, openSync as openSync2, readFileSync as readFileSync7, readdirSync as readdirSync3, statSync as statSync3 } from "node:fs";
6061
+
6062
+ // src/swarm/tier-binding.ts
6063
+ var SWARM_TIERS = Object.freeze([
6064
+ "tier1_subscription",
6065
+ "tier1_local",
6066
+ "tier2_user_key",
6067
+ "tier3_platform_key",
6068
+ "refused",
6069
+ "unresolved"
6070
+ ]);
6071
+ var TIER_ADMITS_SPAWN = /* @__PURE__ */ new Set([
6072
+ "tier1_subscription",
6073
+ "tier1_local",
6074
+ "tier2_user_key",
6075
+ "tier3_platform_key"
6076
+ ]);
6077
+ var SWARM_TIER_BINDING_ENV = "VO_SWARM_TIER_BINDING";
6078
+ var MAX_BOUND_SUBAGENTS = 20;
6079
+ function isPositiveCap(cap) {
6080
+ return typeof cap === "number" && Number.isFinite(cap) && cap > 0;
6081
+ }
6082
+ function unresolvedBinding(swarmId, nowIso, reason) {
6083
+ return {
6084
+ schema_version: 1,
6085
+ swarm_id: swarmId,
6086
+ tier: "unresolved",
6087
+ agent: null,
6088
+ reason,
6089
+ exhausted_agents: [],
6090
+ subagent_budget: 0,
6091
+ spend_cap_usd: null,
6092
+ resolved_at: nowIso
6093
+ };
6094
+ }
6095
+ function serializeSwarmTierBinding(binding) {
6096
+ return JSON.stringify(binding);
6097
+ }
6098
+ function parseSwarmTierBinding(raw, nowIso) {
6099
+ if (typeof raw !== "string" || raw.trim().length === 0) {
6100
+ return unresolvedBinding("", nowIso, "no swarm tier binding present in the environment");
6101
+ }
6102
+ let parsed;
6103
+ try {
6104
+ parsed = JSON.parse(raw);
6105
+ } catch {
6106
+ return unresolvedBinding("", nowIso, "swarm tier binding is not valid JSON");
6107
+ }
6108
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
6109
+ return unresolvedBinding("", nowIso, "swarm tier binding is not an object");
6110
+ }
6111
+ const o = parsed;
6112
+ const swarmId = typeof o["swarm_id"] === "string" ? o["swarm_id"] : "";
6113
+ if (o["schema_version"] !== 1) {
6114
+ return unresolvedBinding(swarmId, nowIso, "swarm tier binding has an unsupported schema_version");
6115
+ }
6116
+ const tier = o["tier"];
6117
+ if (typeof tier !== "string" || !SWARM_TIERS.includes(tier)) {
6118
+ return unresolvedBinding(swarmId, nowIso, "swarm tier binding names an unknown tier");
6119
+ }
6120
+ const budget = o["subagent_budget"];
6121
+ const cap = o["spend_cap_usd"];
6122
+ const capNum = isPositiveCap(cap) ? cap : null;
6123
+ if (tier === "tier3_platform_key" && capNum === null) {
6124
+ return unresolvedBinding(
6125
+ swarmId,
6126
+ nowIso,
6127
+ "inherited tier3_platform_key binding carries no positive numeric spend cap \u2014 refusing an uncapped platform-billed fan-out"
6128
+ );
6129
+ }
6130
+ return {
6131
+ schema_version: 1,
6132
+ swarm_id: swarmId,
6133
+ tier,
6134
+ agent: typeof o["agent"] === "string" ? o["agent"] : null,
6135
+ reason: typeof o["reason"] === "string" ? o["reason"] : "inherited binding carried no reason",
6136
+ exhausted_agents: Array.isArray(o["exhausted_agents"]) ? o["exhausted_agents"].filter((v) => typeof v === "string") : [],
6137
+ subagent_budget: typeof budget === "number" && Number.isFinite(budget) && budget > 0 ? Math.min(Math.floor(budget), MAX_BOUND_SUBAGENTS) : 0,
6138
+ spend_cap_usd: capNum,
6139
+ resolved_at: typeof o["resolved_at"] === "string" ? o["resolved_at"] : nowIso
6140
+ };
6141
+ }
6142
+ function inheritSwarmTierBinding(env, nowIso) {
6143
+ return parseSwarmTierBinding(env[SWARM_TIER_BINDING_ENV], nowIso);
6144
+ }
6145
+ function bindingEnvFragment(binding) {
6146
+ return { [SWARM_TIER_BINDING_ENV]: serializeSwarmTierBinding(binding) };
6147
+ }
6148
+ function childBindingEnvFragment(binding, allocatedCapUsd = null) {
6149
+ return bindingEnvFragment(childBinding(binding, allocatedCapUsd));
6150
+ }
6151
+ function admitSubagentSpawn(binding, spawnsSoFar = 0) {
6152
+ if (!TIER_ADMITS_SPAWN.has(binding.tier)) {
6153
+ return { allowed: false, reason: `tier '${binding.tier}' admits no spawn: ${binding.reason}` };
6154
+ }
6155
+ if (binding.tier === "tier3_platform_key" && !isPositiveCap(binding.spend_cap_usd)) {
6156
+ return {
6157
+ allowed: false,
6158
+ reason: `swarm ${binding.swarm_id} is tier3_platform_key with no positive spend cap \u2014 refusing to spend the platform owner's money uncapped`
6159
+ };
6160
+ }
6161
+ if (!Number.isFinite(spawnsSoFar) || spawnsSoFar < 0) {
6162
+ return { allowed: false, reason: "spawn counter is not a finite non-negative number" };
6163
+ }
6164
+ if (spawnsSoFar >= binding.subagent_budget) {
6165
+ return {
6166
+ allowed: false,
6167
+ reason: `swarm ${binding.swarm_id} exhausted its bound subagent budget (${binding.subagent_budget})`
6168
+ };
6169
+ }
6170
+ return { allowed: true, reason: `admitted under tier '${binding.tier}'` };
6171
+ }
6172
+ function childBinding(binding, allocatedCapUsd = null) {
6173
+ const allocated = isPositiveCap(allocatedCapUsd) ? allocatedCapUsd : null;
6174
+ const parentCap = isPositiveCap(binding.spend_cap_usd) ? binding.spend_cap_usd : null;
6175
+ return {
6176
+ ...binding,
6177
+ subagent_budget: Math.max(0, binding.subagent_budget - 1),
6178
+ // A child never carries more than its parent, whatever the ledger says: a
6179
+ // forged or hand-edited pool cannot inflate a descendant above the binding
6180
+ // it descends from.
6181
+ spend_cap_usd: allocated === null || parentCap === null ? null : Math.min(allocated, parentCap)
6182
+ };
6183
+ }
6184
+ function agentBindingRefusal(binding, requestedAgent) {
6185
+ const requested = typeof requestedAgent === "string" ? requestedAgent.trim() : "";
6186
+ if (requested.length === 0) return null;
6187
+ if (binding.agent !== null && requested === binding.agent) return null;
6188
+ return `swarm '${binding.swarm_id}' is bound to agent '${binding.agent ?? "none"}' under tier '${binding.tier}'; a caller-supplied agent '${requested}' would move this fan-out onto a different payer \u2014 refusing (the tier is decided once, at admission, and an inherited binding cannot be renegotiated)`;
6189
+ }
6190
+
6191
+ // src/swarm/successor-launch.ts
6192
+ var AGENT_LAUNCH_SHAPES = Object.freeze({
6193
+ claude: {
6194
+ bin: "claude",
6195
+ baseArgs: ["-p", "--permission-mode", "acceptEdits"],
6196
+ enforcesMaxTurns: true,
6197
+ maxTurnsFlag: "--max-turns",
6198
+ windowsShellSafe: true
6199
+ },
6200
+ codex: {
6201
+ bin: "codex",
6202
+ baseArgs: ["exec", "--json", "-c", 'approval_policy="never"', "--sandbox", "workspace-write", "--skip-git-repo-check"],
6203
+ enforcesMaxTurns: false,
6204
+ // `-` makes codex read the prompt from stdin (injection-safe), matching how
6205
+ // codex-runner.mjs already spawns it.
6206
+ trailingArgs: ["-"],
6207
+ // `approval_policy="never"` carries embedded quotes; cmd.exe re-parsing is
6208
+ // unverified, so win32 refuses rather than risking a mangled sandbox flag.
6209
+ windowsShellSafe: false
6210
+ }
6211
+ });
6212
+ function resolveSuccessorLaunch(input) {
6213
+ const agent = typeof input.agent === "string" ? input.agent.trim() : "";
6214
+ if (!agent) {
6215
+ return { ok: false, reason: "no agent bound for this spawn \u2014 refusing rather than defaulting to claude" };
6216
+ }
6217
+ const shape = AGENT_LAUNCH_SHAPES[agent];
6218
+ if (!shape) {
6219
+ const known = Object.keys(AGENT_LAUNCH_SHAPES).join(", ");
6220
+ return {
6221
+ ok: false,
6222
+ reason: `no known headless launch shape for agent '${agent}' (known: ${known}) \u2014 refusing rather than guessing its argv`
6223
+ };
6224
+ }
6225
+ const wantsMaxTurns = Number.isInteger(input.maxTurns) && input.maxTurns > 0;
6226
+ if (wantsMaxTurns && !shape.enforcesMaxTurns) {
6227
+ return {
6228
+ ok: false,
6229
+ reason: `agent '${agent}' cannot enforce a max_turns cap \u2014 refusing rather than spawning it unbounded`
6230
+ };
6231
+ }
6232
+ const platform = input.platform ?? process.platform;
6233
+ if (platform === "win32" && !shape.windowsShellSafe) {
6234
+ return {
6235
+ ok: false,
6236
+ reason: `agent '${agent}' has an argv whose behaviour under Windows cmd.exe re-parsing is unverified \u2014 refusing rather than emitting a command line that may mean something else`
6237
+ };
6238
+ }
6239
+ const args = [...shape.baseArgs];
6240
+ if (wantsMaxTurns && shape.maxTurnsFlag) {
6241
+ args.push(shape.maxTurnsFlag, String(input.maxTurns));
6242
+ }
6243
+ if (shape.trailingArgs) args.push(...shape.trailingArgs);
6244
+ return { ok: true, agent, bin: shape.bin, args };
6245
+ }
6246
+
6247
+ // src/swarm/spawn-ledger.ts
6248
+ import { mkdirSync as mkdirSync3, openSync, closeSync, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
5397
6249
  import { homedir as homedir4 } from "node:os";
5398
6250
  import { join as join6 } from "node:path";
5399
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, openSync, readFileSync as readFileSync6, readdirSync as readdirSync3, statSync as statSync3 } from "node:fs";
6251
+ var SWARM_LEDGER_DIR_ENV = "VO_SWARM_LEDGER_DIR";
6252
+ function resolveLedgerDir(env) {
6253
+ const override = env[SWARM_LEDGER_DIR_ENV];
6254
+ if (typeof override === "string" && override.trim().length > 0) return override.trim();
6255
+ return join6(homedir4(), ".vo", "swarm-ledger");
6256
+ }
6257
+ function sanitizeSwarmId(raw) {
6258
+ if (typeof raw !== "string") return null;
6259
+ const id = raw.trim();
6260
+ if (id.length === 0 || id.length > 128) return null;
6261
+ if (!/^[A-Za-z0-9._-]+$/u.test(id)) return null;
6262
+ if (id === "." || id === "..") return null;
6263
+ return id;
6264
+ }
6265
+ var CEILING_FILE = "ceiling.json";
6266
+ function createExclusive(path3, contents) {
6267
+ let fd;
6268
+ try {
6269
+ fd = openSync(path3, "wx");
6270
+ } catch {
6271
+ return false;
6272
+ }
6273
+ try {
6274
+ writeFileSync3(fd, contents, "utf8");
6275
+ } finally {
6276
+ closeSync(fd);
6277
+ }
6278
+ return true;
6279
+ }
6280
+ function capToCents(cap) {
6281
+ return isPositiveCap(cap) ? Math.round(cap * 100) : 0;
6282
+ }
6283
+ function readOrRecordLedgerHead(swarmDir, proposedCeiling, proposedCapCents, nowIso) {
6284
+ const path3 = join6(swarmDir, CEILING_FILE);
6285
+ const head = JSON.stringify({
6286
+ ceiling: proposedCeiling,
6287
+ cap_cents: proposedCapCents,
6288
+ recorded_at: nowIso
6289
+ });
6290
+ if (createExclusive(path3, head)) {
6291
+ return { ceiling: proposedCeiling, capCents: proposedCapCents };
6292
+ }
6293
+ let parsed;
6294
+ try {
6295
+ parsed = JSON.parse(readFileSync6(path3, "utf8"));
6296
+ } catch {
6297
+ return null;
6298
+ }
6299
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
6300
+ const record = parsed;
6301
+ const recorded = record["ceiling"];
6302
+ if (typeof recorded !== "number" || !Number.isFinite(recorded) || recorded < 1) return null;
6303
+ const recordedCap = record["cap_cents"];
6304
+ const capCents = typeof recordedCap === "number" && Number.isFinite(recordedCap) && recordedCap > 0 ? Math.floor(recordedCap) : 0;
6305
+ return { ceiling: Math.min(Math.floor(recorded), MAX_BOUND_SUBAGENTS), capCents };
6306
+ }
6307
+ var claimSpawnSlot = ({ swarmId, proposedCeiling, proposedCapUsd, dir, nowIso }) => {
6308
+ const id = sanitizeSwarmId(swarmId);
6309
+ if (id === null) {
6310
+ return {
6311
+ ok: false,
6312
+ reason: `swarm id ${JSON.stringify(swarmId)} is absent or unusable as a ledger key \u2014 refusing a spawn that cannot be counted against a fan-out ceiling`
6313
+ };
6314
+ }
6315
+ const proposed = Number.isFinite(proposedCeiling) ? Math.floor(proposedCeiling) : 0;
6316
+ if (proposed < 1) {
6317
+ return { ok: false, reason: `swarm '${id}' proposes a ceiling of ${proposed} \u2014 no allowance to claim` };
6318
+ }
6319
+ const swarmDir = join6(dir, id);
6320
+ try {
6321
+ mkdirSync3(swarmDir, { recursive: true });
6322
+ } catch (err) {
6323
+ return {
6324
+ ok: false,
6325
+ reason: `swarm '${id}' ledger directory is unwritable (${err instanceof Error ? err.message : String(err)}) \u2014 refusing rather than spawning uncounted`
6326
+ };
6327
+ }
6328
+ const wantedCents = capToCents(proposedCapUsd);
6329
+ const head = readOrRecordLedgerHead(swarmDir, Math.min(proposed, MAX_BOUND_SUBAGENTS), wantedCents, nowIso);
6330
+ if (head === null) {
6331
+ return { ok: false, reason: `swarm '${id}' ledger carries no readable ceiling \u2014 refusing rather than spawning uncounted` };
6332
+ }
6333
+ const { ceiling, capCents } = head;
6334
+ const shareCents = capCents > 0 ? Math.floor(capCents / ceiling) : 0;
6335
+ if (wantedCents > 0 && shareCents < 1) {
6336
+ return {
6337
+ ok: false,
6338
+ reason: `swarm '${id}' has no spend allowance left to debit (recorded pool $${(capCents / 100).toFixed(2)} across a ceiling of ${ceiling} leaves under one cent per spawn) \u2014 refusing a platform-billed spawn it cannot fund`
6339
+ };
6340
+ }
6341
+ for (let slot = 0; slot < ceiling; slot++) {
6342
+ const debitedCents = shareCents;
6343
+ const remainingCents = capCents > 0 ? capCents - (slot + 1) * shareCents : 0;
6344
+ const claimed = createExclusive(
6345
+ join6(swarmDir, `slot-${slot}.json`),
6346
+ JSON.stringify({
6347
+ slot,
6348
+ ceiling,
6349
+ pid: process.pid,
6350
+ claimed_at: nowIso,
6351
+ // The debit record. Durable and atomic with the claim: this file is
6352
+ // created with O_EXCL, so exactly one claimant ever writes this line.
6353
+ cap_cents_pool: capCents,
6354
+ cap_cents_debited: debitedCents,
6355
+ cap_cents_remaining: remainingCents
6356
+ })
6357
+ );
6358
+ if (claimed) {
6359
+ return {
6360
+ ok: true,
6361
+ slot,
6362
+ ceiling,
6363
+ remaining: ceiling - slot - 1,
6364
+ capUsd: debitedCents > 0 ? debitedCents / 100 : null,
6365
+ capRemainingUsd: capCents > 0 ? remainingCents / 100 : null
6366
+ };
6367
+ }
6368
+ }
6369
+ return {
6370
+ ok: false,
6371
+ reason: `swarm '${id}' has spent its whole fan-out ceiling (${ceiling} spawns across every generation) \u2014 refusing`
6372
+ };
6373
+ };
6374
+
6375
+ // src/tools/session/spawn-successor.ts
5400
6376
  var TOOL_NAME20 = "vo_spawn_successor";
5401
6377
  var MAX_HANDOFF_BYTES = 64e3;
5402
6378
  var inputSchema20 = {
@@ -5417,11 +6393,16 @@ var inputSchema20 = {
5417
6393
  max_turns: {
5418
6394
  type: "number",
5419
6395
  description: "Optional --max-turns bound for the successor."
6396
+ },
6397
+ agent: {
6398
+ type: "string",
6399
+ description: `Which agent to spawn ('claude' | 'codex'). Normally omitted: the agent comes from the swarm tier binding inherited via ${SWARM_TIER_BINDING_ENV}. When a binding IS inherited this may only RESTATE the bound agent \u2014 an agent that contradicts the binding is REFUSED, because a different agent is a different payer and the payer was decided once, at admission.`
5420
6400
  }
5421
6401
  },
5422
6402
  required: [],
5423
6403
  additionalProperties: false
5424
6404
  };
6405
+ var RETIRED_COUNTER_INPUT = "spawns_so_far";
5425
6406
  var description20 = "Mode B auto-handoff (roadmap \xA73.4): spawn a DETACHED headless `claude -p` successor with a handoff doc pre-injected into its prompt. Defaults to the newest handoff in ~/.vo/handoffs/. Returns {spawned, pid, log_path, handoff_path}. The successor works under the same gates as any session (ADR-001: verify-before-act, human merge approval) \u2014 this tool never fires autonomously.";
5426
6407
  function isToolInput20(v) {
5427
6408
  if (typeof v !== "object" || v === null) return false;
@@ -5430,12 +6411,18 @@ function isToolInput20(v) {
5430
6411
  if (o["goal"] !== void 0 && typeof o["goal"] !== "string") return false;
5431
6412
  if (o["cwd"] !== void 0 && typeof o["cwd"] !== "string") return false;
5432
6413
  if (o["max_turns"] !== void 0 && typeof o["max_turns"] !== "number") return false;
6414
+ if (o["agent"] !== void 0 && typeof o["agent"] !== "string") return false;
5433
6415
  return true;
5434
6416
  }
5435
- function newestHandoff(dir = join6(homedir4(), ".vo", "handoffs")) {
6417
+ function retiredCounterRefusal(v) {
6418
+ if (typeof v !== "object" || v === null) return null;
6419
+ if (!(RETIRED_COUNTER_INPUT in v)) return null;
6420
+ return `\`${RETIRED_COUNTER_INPUT}\` is no longer accepted: a spawn counter supplied by the process being bounded bounds nothing, and an absent one read as zero. The fan-out ceiling is now enforced by the durable per-swarm spawn ledger; remove the field.`;
6421
+ }
6422
+ function newestHandoff(dir = join7(homedir5(), ".vo", "handoffs")) {
5436
6423
  try {
5437
- const entries = readdirSync3(dir).filter((f) => f.endsWith(".md")).map((f) => ({ f, m: statSync3(join6(dir, f)).mtimeMs })).sort((a, b) => b.m - a.m);
5438
- return entries.length > 0 && entries[0] ? join6(dir, entries[0].f) : null;
6424
+ const entries = readdirSync3(dir).filter((f) => f.endsWith(".md")).map((f) => ({ f, m: statSync3(join7(dir, f)).mtimeMs })).sort((a, b) => b.m - a.m);
6425
+ return entries.length > 0 && entries[0] ? join7(dir, entries[0].f) : null;
5439
6426
  } catch {
5440
6427
  return null;
5441
6428
  }
@@ -5479,9 +6466,87 @@ function buildSuccessorArgs(maxTurns) {
5479
6466
  }
5480
6467
  return args;
5481
6468
  }
6469
+ function resolveSpawnPlan(env, input, nowIso, platform = process.platform, claim = claimSpawnSlot) {
6470
+ const rawBinding = env[SWARM_TIER_BINDING_ENV];
6471
+ const hasBinding = typeof rawBinding === "string" && rawBinding.trim().length > 0;
6472
+ if (!hasBinding) {
6473
+ const explicit = input.agent?.trim();
6474
+ if (explicit) {
6475
+ const resolved2 = resolveSuccessorLaunch({ agent: explicit, maxTurns: input.max_turns, platform });
6476
+ if (!resolved2.ok) return { ok: false, reason: resolved2.reason, tier: "unbound" };
6477
+ return {
6478
+ ok: true,
6479
+ bin: resolved2.bin,
6480
+ args: resolved2.args,
6481
+ agent: resolved2.agent,
6482
+ tier: "unbound",
6483
+ bound: false,
6484
+ env: {},
6485
+ slot: null,
6486
+ capUsd: null,
6487
+ capRemainingUsd: null
6488
+ };
6489
+ }
6490
+ return {
6491
+ ok: true,
6492
+ bin: "claude",
6493
+ args: buildSuccessorArgs(input.max_turns),
6494
+ agent: "claude",
6495
+ tier: "unbound",
6496
+ bound: false,
6497
+ env: {},
6498
+ slot: null,
6499
+ capUsd: null,
6500
+ capRemainingUsd: null
6501
+ };
6502
+ }
6503
+ const binding = inheritSwarmTierBinding(env, nowIso);
6504
+ const admission = admitSubagentSpawn(binding);
6505
+ if (!admission.allowed) {
6506
+ return { ok: false, reason: admission.reason, tier: binding.tier };
6507
+ }
6508
+ const agentRefusal = agentBindingRefusal(binding, input.agent);
6509
+ if (agentRefusal !== null) return { ok: false, reason: agentRefusal, tier: binding.tier };
6510
+ const resolved = resolveSuccessorLaunch({
6511
+ agent: binding.agent,
6512
+ maxTurns: input.max_turns,
6513
+ platform
6514
+ });
6515
+ if (!resolved.ok) return { ok: false, reason: resolved.reason, tier: binding.tier };
6516
+ const slot = claim({
6517
+ swarmId: binding.swarm_id,
6518
+ proposedCeiling: binding.subagent_budget,
6519
+ // The spend-cap POOL, recorded once per swarm exactly like the ceiling. The
6520
+ // child's cap is DEBITED from it below, not recomputed from this binding.
6521
+ proposedCapUsd: binding.spend_cap_usd,
6522
+ dir: resolveLedgerDir(env),
6523
+ nowIso
6524
+ });
6525
+ if (!slot.ok) return { ok: false, reason: slot.reason, tier: binding.tier };
6526
+ return {
6527
+ ok: true,
6528
+ bin: resolved.bin,
6529
+ args: resolved.args,
6530
+ agent: resolved.agent,
6531
+ tier: binding.tier,
6532
+ bound: true,
6533
+ // Re-export the same TIER with a DECREMENTED budget and the spend cap the
6534
+ // ledger just DEBITED. Exporting the binding verbatim (what this did before
6535
+ // #9312) meant the child re-read the full budget and every generation
6536
+ // restarted at zero. Recomputing the cap from THIS binding (what #9312 did)
6537
+ // bounded a chain but not a tree: three siblings each re-halved the parent's
6538
+ // untouched $50 and walked away with $75 between them.
6539
+ env: childBindingEnvFragment(binding, slot.capUsd),
6540
+ slot: slot.slot,
6541
+ capUsd: slot.capUsd,
6542
+ capRemainingUsd: slot.capRemainingUsd
6543
+ };
6544
+ }
5482
6545
  async function handleSpawnSuccessor(_deps, rawInput, _signal, spawnImpl = spawn) {
6546
+ const retired = retiredCounterRefusal(rawInput);
6547
+ if (retired !== null) throw invalidParams(TOOL_NAME20, retired);
5483
6548
  if (!isToolInput20(rawInput)) {
5484
- throw invalidParams(TOOL_NAME20, "invalid input. Optional: { handoff_path, goal, cwd, max_turns }.");
6549
+ throw invalidParams(TOOL_NAME20, "invalid input. Optional: { handoff_path, goal, cwd, max_turns, agent }.");
5485
6550
  }
5486
6551
  const handoffPath = rawInput.handoff_path?.trim() || newestHandoff();
5487
6552
  if (!handoffPath || !existsSync4(handoffPath)) {
@@ -5494,20 +6559,37 @@ async function handleSpawnSuccessor(_deps, rawInput, _signal, spawnImpl = spawn)
5494
6559
  }
5495
6560
  });
5496
6561
  }
5497
- const handoff = readFileSync6(handoffPath, "utf8").slice(0, MAX_HANDOFF_BYTES);
6562
+ const handoff = readFileSync7(handoffPath, "utf8").slice(0, MAX_HANDOFF_BYTES);
5498
6563
  const prompt = buildSuccessorPrompt(handoff, rawInput.goal);
5499
- const logDir = process.env["VO_MCP_SUCCESSOR_LOG_DIR"]?.trim() || join6(homedir4(), ".vo", "successors");
5500
- mkdirSync3(logDir, { recursive: true });
5501
- const logPath = join6(logDir, `successor-${Date.now()}.log`);
5502
- const logFd = openSync(logPath, "a");
5503
- const child = spawnImpl("claude", buildSuccessorArgs(rawInput.max_turns), {
6564
+ const plan = resolveSpawnPlan(process.env, rawInput, (/* @__PURE__ */ new Date()).toISOString());
6565
+ if (!plan.ok) {
6566
+ return jsonContent({
6567
+ tool: TOOL_NAME20,
6568
+ schema_version: 1,
6569
+ payload: {
6570
+ spawned: false,
6571
+ reason: `swarm tier binding refused this spawn: ${plan.reason}`,
6572
+ tier: plan.tier,
6573
+ handoff_path: handoffPath
6574
+ }
6575
+ });
6576
+ }
6577
+ const logDir = process.env["VO_MCP_SUCCESSOR_LOG_DIR"]?.trim() || join7(homedir5(), ".vo", "successors");
6578
+ mkdirSync4(logDir, { recursive: true });
6579
+ const logPath = join7(logDir, `successor-${Date.now()}.log`);
6580
+ const logFd = openSync2(logPath, "a");
6581
+ const child = spawnImpl(plan.bin, [...plan.args], {
5504
6582
  cwd: rawInput.cwd?.trim() || process.cwd(),
5505
6583
  detached: true,
5506
6584
  stdio: ["pipe", logFd, logFd],
5507
- // Windows: `claude` is a .cmd shimneeds a shell to resolve. The prompt
5508
- // goes via STDIN below, never argv, so the shell never sees it.
6585
+ // Windows: the agent CLIs are .cmd shimsthey need a shell to resolve.
6586
+ // The prompt goes via STDIN below, never argv, so the shell never sees it.
5509
6587
  shell: process.platform === "win32",
5510
- windowsHide: true
6588
+ windowsHide: true,
6589
+ // Carry the SAME binding to the child. Without this the successor inherits
6590
+ // no tier and re-resolves its own — which is the split-payer defect one
6591
+ // generation down.
6592
+ ...plan.bound ? { env: { ...process.env, ...plan.env } } : {}
5511
6593
  });
5512
6594
  let spawnError = null;
5513
6595
  child.on("error", (e) => {
@@ -5523,7 +6605,20 @@ async function handleSpawnSuccessor(_deps, rawInput, _signal, spawnImpl = spawn)
5523
6605
  return jsonContent({
5524
6606
  tool: TOOL_NAME20,
5525
6607
  schema_version: 1,
5526
- payload: spawnError ? { spawned: false, reason: `spawn failed: ${spawnError}`, handoff_path: handoffPath } : { spawned: true, pid: child.pid ?? null, log_path: logPath, handoff_path: handoffPath }
6608
+ payload: spawnError ? { spawned: false, reason: `spawn failed: ${spawnError}`, agent: plan.agent, tier: plan.tier, handoff_path: handoffPath } : {
6609
+ spawned: true,
6610
+ pid: child.pid ?? null,
6611
+ log_path: logPath,
6612
+ handoff_path: handoffPath,
6613
+ agent: plan.agent,
6614
+ tier: plan.tier,
6615
+ tier_bound: plan.bound,
6616
+ ledger_slot: plan.slot,
6617
+ // The debit, surfaced so an operator can reconcile a fan-out's spend
6618
+ // against the pool without reading the ledger directory by hand.
6619
+ ledger_cap_usd: plan.capUsd,
6620
+ ledger_cap_remaining_usd: plan.capRemainingUsd
6621
+ }
5527
6622
  });
5528
6623
  }
5529
6624
 
@@ -5885,12 +6980,12 @@ async function handleHqWhiteboardRead(_deps, rawInput, signal) {
5885
6980
  }
5886
6981
 
5887
6982
  // src/tools/skills/skill-corpus.ts
5888
- import { existsSync as existsSync7, statSync as statSync5 } from "node:fs";
5889
- import { dirname as dirname5, isAbsolute, join as join9, 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";
5890
6985
 
5891
6986
  // ../skill-registry/src/loader.ts
5892
- import { readdirSync as readdirSync6, readFileSync as readFileSync9, statSync as statSync4 } from "node:fs";
5893
- import { join as join8 } 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";
5894
6989
  var InvalidSkillFrontmatterError = class extends Error {
5895
6990
  constructor(skillFile, reason) {
5896
6991
  super(`Invalid frontmatter in ${skillFile}: ${reason}`);
@@ -5943,18 +7038,18 @@ function loadSkillsFromDir(skillsDir) {
5943
7038
  const entries = readdirSync6(skillsDir);
5944
7039
  const skills = [];
5945
7040
  for (const entry of entries) {
5946
- const entryPath = join8(skillsDir, entry);
7041
+ const entryPath = join12(skillsDir, entry);
5947
7042
  let stat;
5948
7043
  try {
5949
- stat = statSync4(entryPath);
7044
+ stat = statSync5(entryPath);
5950
7045
  } catch {
5951
7046
  continue;
5952
7047
  }
5953
7048
  if (!stat.isDirectory()) continue;
5954
- const skillFile = join8(entryPath, "SKILL.md");
7049
+ const skillFile = join12(entryPath, "SKILL.md");
5955
7050
  let raw;
5956
7051
  try {
5957
- raw = readFileSync9(skillFile, "utf8");
7052
+ raw = readFileSync14(skillFile, "utf8");
5958
7053
  } catch {
5959
7054
  continue;
5960
7055
  }
@@ -5996,12 +7091,12 @@ function resolveSkillsDir(env = process.env, startDir = process.cwd()) {
5996
7091
  const override = env.VO_SKILLS_DIR;
5997
7092
  if (typeof override === "string" && override.length > 0) {
5998
7093
  const abs = isAbsolute(override) ? override : resolve2(startDir, override);
5999
- return existsSync7(abs) && statSync5(abs).isDirectory() ? abs : null;
7094
+ return existsSync9(abs) && statSync6(abs).isDirectory() ? abs : null;
6000
7095
  }
6001
7096
  let dir = resolve2(startDir);
6002
7097
  for (let i = 0; i < MAX_WALK_UP_LEVELS; i += 1) {
6003
- const candidate = join9(dir, ".claude", "skills");
6004
- if (existsSync7(candidate) && statSync5(candidate).isDirectory()) return candidate;
7098
+ const candidate = join13(dir, ".claude", "skills");
7099
+ if (existsSync9(candidate) && statSync6(candidate).isDirectory()) return candidate;
6005
7100
  const parent = dirname5(dir);
6006
7101
  if (parent === dir) break;
6007
7102
  dir = parent;
@@ -6312,7 +7407,7 @@ function buildToolRegistry() {
6312
7407
  };
6313
7408
  }
6314
7409
  function createServer(options) {
6315
- const sessionId = options.sessionId ?? randomUUID2();
7410
+ const sessionId = options.sessionId ?? randomUUID3();
6316
7411
  const mode = createLocalMode();
6317
7412
  const now = options.now ?? (() => /* @__PURE__ */ new Date());
6318
7413
  const server = new Server(
@@ -6361,8 +7456,8 @@ function createServer(options) {
6361
7456
  }
6362
7457
 
6363
7458
  // src/cache/sqlite-cache.ts
6364
- import { createHash as createHash3 } from "node:crypto";
6365
- import { chmodSync as chmodSync3, mkdirSync as mkdirSync5 } from "node:fs";
7459
+ import { createHash as createHash4 } from "node:crypto";
7460
+ import { chmodSync as chmodSync3, mkdirSync as mkdirSync7 } from "node:fs";
6366
7461
  import { dirname as dirname6 } from "node:path";
6367
7462
  import { DatabaseSync } from "node:sqlite";
6368
7463
 
@@ -6408,7 +7503,7 @@ function normalizeString(s) {
6408
7503
  function createSqliteCache(options) {
6409
7504
  const fileBacked = options.dbPath !== ":memory:";
6410
7505
  if (fileBacked) {
6411
- mkdirSync5(dirname6(options.dbPath), { recursive: true, mode: 448 });
7506
+ mkdirSync7(dirname6(options.dbPath), { recursive: true, mode: 448 });
6412
7507
  }
6413
7508
  const versionNamespace = options.cacheVersionNamespace ?? "";
6414
7509
  const db = new DatabaseSync(options.dbPath);
@@ -6439,7 +7534,7 @@ function createSqliteCache(options) {
6439
7534
  return {
6440
7535
  keyFor(toolName, input, opts) {
6441
7536
  const canonical = canonicalize(input, opts);
6442
- const hash = createHash3("sha256");
7537
+ const hash = createHash4("sha256");
6443
7538
  if (versionNamespace.length > 0) {
6444
7539
  hash.update(versionNamespace);
6445
7540
  hash.update("|");
@@ -6534,7 +7629,7 @@ function createStubRatchetClient() {
6534
7629
  let m;
6535
7630
  while ((m = pat.regex.exec(req.source)) !== null) {
6536
7631
  findings.push({
6537
- line_excerpt: clip(m[0], 80),
7632
+ line_excerpt: clip2(m[0], 80),
6538
7633
  severity: pat.severity,
6539
7634
  code: pat.code,
6540
7635
  message: pat.message
@@ -6566,7 +7661,7 @@ function createStubRatchetClient() {
6566
7661
  }
6567
7662
  };
6568
7663
  }
6569
- function clip(s, n) {
7664
+ function clip2(s, n) {
6570
7665
  return s.length <= n ? s : s.slice(0, n) + "\u2026";
6571
7666
  }
6572
7667
  function buildSummary2(args) {
@@ -6577,7 +7672,7 @@ function buildSummary2(args) {
6577
7672
  // src/consensus/engine-client.ts
6578
7673
  init_events_writer();
6579
7674
  init_common();
6580
- import { randomUUID as randomUUID3 } from "node:crypto";
7675
+ import { randomUUID as randomUUID4 } from "node:crypto";
6581
7676
 
6582
7677
  // src/consensus/null-client.ts
6583
7678
  var NULL_CLIENT_DEFAULT_REASON = "consensus-engine-package-pending";
@@ -7037,7 +8132,7 @@ function tryCreateEngineConsensusClientFromEnv(options = {}) {
7037
8132
  }
7038
8133
 
7039
8134
  // src/consensus/moat-client.ts
7040
- import { randomUUID as randomUUID4 } from "node:crypto";
8135
+ import { randomUUID as randomUUID5 } from "node:crypto";
7041
8136
 
7042
8137
  // src/consensus/client.ts
7043
8138
  var CANCELLED_REASON = "cancelled";
@@ -7089,7 +8184,7 @@ function createMoatConsensusClient(opts) {
7089
8184
  request.signal?.addEventListener("abort", onAbort, { once: true });
7090
8185
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
7091
8186
  const body = JSON.stringify({
7092
- task_id: randomUUID4(),
8187
+ task_id: randomUUID5(),
7093
8188
  gate_type: request.gate_type,
7094
8189
  excerpt: request.prompt,
7095
8190
  ...request.system_prompt ? { question: request.system_prompt } : {},
@@ -7410,7 +8505,7 @@ init_common();
7410
8505
  function defaultCacheDbPath() {
7411
8506
  const env = process.env["VO_MCP_DB_PATH"];
7412
8507
  if (env && env.length > 0) return env;
7413
- return join10(homedir6(), ".claude", "vo-mcp-cache.db");
8508
+ return join14(homedir8(), ".claude", "vo-mcp-cache.db");
7414
8509
  }
7415
8510
  async function probeEngineVersion() {
7416
8511
  try {
@@ -7537,7 +8632,7 @@ async function main() {
7537
8632
  }
7538
8633
  if (process.argv[2] === "login") {
7539
8634
  const controlPlaneUrl = process.env["VO_CONTROL_PLANE_URL"]?.trim();
7540
- const credentialLabel = `vo-mcp-cli@${hostname()}`.slice(0, 200);
8635
+ const credentialLabel = `vo-mcp-cli@${hostname2()}`.slice(0, 200);
7541
8636
  runLogin(
7542
8637
  controlPlaneUrl ? {
7543
8638
  exchange: (refreshToken, apiKey) => exchangeForVoCredential({ refreshToken, apiKey, controlPlaneUrl, label: credentialLabel })
@@ -7553,26 +8648,29 @@ if (process.argv[2] === "login") {
7553
8648
  } else if (process.argv[2] === "sync") {
7554
8649
  const action = process.argv[3];
7555
8650
  if (action !== "push" && action !== "pull") {
7556
- 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>]");
7557
8652
  process.exit(2);
7558
8653
  }
7559
8654
  const cwdFlag = process.argv.indexOf("--cwd");
7560
8655
  const cwd = cwdFlag >= 0 && typeof process.argv[cwdFlag + 1] === "string" ? process.argv[cwdFlag + 1] : process.cwd();
7561
- 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();
7562
8660
  const appendSyncLog = async (line) => {
7563
8661
  try {
7564
- const { appendFileSync: appendFileSync2, mkdirSync: mkdirSync6 } = await import("node:fs");
7565
- const { join: join11 } = await import("node:path");
7566
- const { homedir: homedir7 } = await import("node:os");
7567
- const dir = join11(homedir7(), ".claude");
7568
- mkdirSync6(dir, { recursive: true });
7569
- appendFileSync2(join11(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}
7570
8668
  `, "utf8");
7571
8669
  } catch {
7572
8670
  }
7573
8671
  };
7574
8672
  Promise.resolve().then(() => (init_sync_config(), sync_config_exports)).then(async ({ runMemorySync: runMemorySync2, isNoopSyncReason: isNoopSyncReason2 }) => {
7575
- const r = await runMemorySync2(action, cwd, sessionId);
8673
+ const r = await runMemorySync2(action, cwd, sessionId, void 0, lockOptions);
7576
8674
  const stamp = `${sessionId} ${action}`;
7577
8675
  if (r.synced) {
7578
8676
  console.error(`[vo-mcp] sync ${action} ok: ${JSON.stringify(r)}`);