@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/agent-auth-probe-cli.mjs +65 -35
- package/dist/autostart-cli.js +62 -46
- package/dist/autostart-cli.js.map +2 -2
- package/dist/cli.js +1336 -238
- package/dist/cli.js.map +4 -4
- package/dist/index.js +1275 -207
- package/dist/index.js.map +4 -4
- package/dist/install-cli.js +56 -42
- package/dist/install-cli.js.map +3 -3
- package/dist/runner-cli.js +802 -355
- package/dist/runner-cli.js.map +4 -4
- package/package.json +1 -1
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
|
|
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 (!
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
title,
|
|
1451
|
-
content
|
|
1452
|
-
};
|
|
1453
|
-
const post = (body) => fetchFn(`${controlPlaneUrl}/api/v1/knowledge/private`, {
|
|
1454
|
-
method: "POST",
|
|
1455
|
-
headers: {
|
|
1456
|
-
authorization: `Bearer ${token}`,
|
|
1457
|
-
"content-type": "application/json"
|
|
1458
|
-
},
|
|
1459
|
-
body: JSON.stringify(body)
|
|
1460
|
-
});
|
|
1461
|
-
let response = await post({
|
|
1462
|
-
...base,
|
|
1463
|
-
provenance: { written_by: "memory-bridge", source_kind: "operator_memory" }
|
|
1464
|
-
});
|
|
1465
|
-
if (response.status === 400) {
|
|
1466
|
-
response = await post(base);
|
|
1467
|
-
}
|
|
1468
|
-
if (response.status >= 200 && response.status < 300) {
|
|
1469
|
-
upserted += 1;
|
|
1470
|
-
} else {
|
|
1471
|
-
const text = await response.text();
|
|
1472
|
-
failures.push(`${fileName}: HTTP ${response.status} ${text.slice(0, 80)}`);
|
|
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 {
|
|
1507
|
-
import {
|
|
1508
|
-
import {
|
|
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
|
|
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
|
-
|
|
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:
|
|
1686
|
-
created:
|
|
1687
|
-
updated:
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
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
|
-
|
|
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
|
|
1738
|
-
import { randomUUID as
|
|
1739
|
-
import { join as
|
|
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
|
|
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
|
-
|
|
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
|
|
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(
|
|
5438
|
-
return entries.length > 0 && entries[0] ?
|
|
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 =
|
|
6562
|
+
const handoff = readFileSync7(handoffPath, "utf8").slice(0, MAX_HANDOFF_BYTES);
|
|
5498
6563
|
const prompt = buildSuccessorPrompt(handoff, rawInput.goal);
|
|
5499
|
-
const
|
|
5500
|
-
|
|
5501
|
-
|
|
5502
|
-
|
|
5503
|
-
|
|
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:
|
|
5508
|
-
// goes via STDIN below, never argv, so the shell never sees it.
|
|
6585
|
+
// Windows: the agent CLIs are .cmd shims — they 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}`,
|
|
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
|
|
5889
|
-
import { dirname as dirname5, isAbsolute, join as
|
|
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
|
|
5893
|
-
import { join as
|
|
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 =
|
|
7041
|
+
const entryPath = join12(skillsDir, entry);
|
|
5947
7042
|
let stat;
|
|
5948
7043
|
try {
|
|
5949
|
-
stat =
|
|
7044
|
+
stat = statSync5(entryPath);
|
|
5950
7045
|
} catch {
|
|
5951
7046
|
continue;
|
|
5952
7047
|
}
|
|
5953
7048
|
if (!stat.isDirectory()) continue;
|
|
5954
|
-
const skillFile =
|
|
7049
|
+
const skillFile = join12(entryPath, "SKILL.md");
|
|
5955
7050
|
let raw;
|
|
5956
7051
|
try {
|
|
5957
|
-
raw =
|
|
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
|
|
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 =
|
|
6004
|
-
if (
|
|
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 ??
|
|
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
|
|
6365
|
-
import { chmodSync as chmodSync3, mkdirSync as
|
|
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
|
-
|
|
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 =
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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@${
|
|
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
|
|
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:
|
|
7565
|
-
const { join:
|
|
7566
|
-
const { homedir:
|
|
7567
|
-
const dir =
|
|
7568
|
-
|
|
7569
|
-
appendFileSync2(
|
|
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)}`);
|