@algosuite/vo-mcp 0.2.0-beta.4 → 0.2.0-beta.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -3
- package/bin/vo-mcp +9 -3
- package/dist/agent-auth-probe-cli.mjs +1718 -0
- package/dist/autostart-cli.js +115 -60
- package/dist/autostart-cli.js.map +2 -2
- package/dist/ci/check-local-pr-overlap.js +107511 -0
- package/dist/cli.js +2392 -340
- package/dist/cli.js.map +4 -4
- package/dist/index.js +2118 -199
- package/dist/index.js.map +4 -4
- package/dist/install-cli.js +361 -345
- package/dist/install-cli.js.map +4 -4
- package/dist/login-cli.js +4 -4
- package/dist/login-cli.js.map +2 -2
- package/dist/pair-cli.js +1 -1
- package/dist/pair-cli.js.map +2 -2
- package/dist/runner-cli.js +14072 -2594
- package/dist/runner-cli.js.map +4 -4
- package/dist/runner-supervisor.js +2628 -0
- package/dist/runner-supervisor.js.map +7 -0
- package/dist/set-key-cli.js +89 -5
- package/dist/set-key-cli.js.map +2 -2
- package/dist/supervisor-credential-helper.js +233 -0
- package/dist/supervisor-credential-helper.js.map +7 -0
- package/dist/thresholds.json +64 -0
- package/dist/update-cli.js +125 -0
- package/dist/update-cli.js.map +7 -0
- package/package.json +5 -3
package/dist/cli.js
CHANGED
|
@@ -1079,6 +1079,42 @@ function toEventPerModelVerdicts(src) {
|
|
|
1079
1079
|
};
|
|
1080
1080
|
});
|
|
1081
1081
|
}
|
|
1082
|
+
function aggregateEventTokenUsage(src, engineUsage) {
|
|
1083
|
+
if (engineUsage !== void 0) {
|
|
1084
|
+
const hasIn = Object.keys(engineUsage.per_model_tokens_in).length > 0;
|
|
1085
|
+
const hasOut = Object.keys(engineUsage.per_model_tokens_out).length > 0;
|
|
1086
|
+
return {
|
|
1087
|
+
per_model_tokens_in: hasIn ? engineUsage.per_model_tokens_in : null,
|
|
1088
|
+
per_model_tokens_out: hasOut ? engineUsage.per_model_tokens_out : null,
|
|
1089
|
+
total_cost_usd: engineUsage.cost_micro_usd === null ? null : engineUsage.cost_micro_usd / 1e6
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
const tokensIn = {};
|
|
1093
|
+
const tokensOut = {};
|
|
1094
|
+
let anyTokensIn = false;
|
|
1095
|
+
let anyTokensOut = false;
|
|
1096
|
+
let costMicroUsd = 0;
|
|
1097
|
+
let anyCost = false;
|
|
1098
|
+
for (const v of src) {
|
|
1099
|
+
if (typeof v.input_tokens === "number") {
|
|
1100
|
+
tokensIn[v.model] = (tokensIn[v.model] ?? 0) + v.input_tokens;
|
|
1101
|
+
anyTokensIn = true;
|
|
1102
|
+
}
|
|
1103
|
+
if (typeof v.output_tokens === "number") {
|
|
1104
|
+
tokensOut[v.model] = (tokensOut[v.model] ?? 0) + v.output_tokens;
|
|
1105
|
+
anyTokensOut = true;
|
|
1106
|
+
}
|
|
1107
|
+
if (typeof v.cost_micro_usd === "number") {
|
|
1108
|
+
costMicroUsd += v.cost_micro_usd;
|
|
1109
|
+
anyCost = true;
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
return {
|
|
1113
|
+
per_model_tokens_in: anyTokensIn ? tokensIn : null,
|
|
1114
|
+
per_model_tokens_out: anyTokensOut ? tokensOut : null,
|
|
1115
|
+
total_cost_usd: anyCost ? costMicroUsd / 1e6 : null
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1082
1118
|
function toEventSynthesizedVerdict(src) {
|
|
1083
1119
|
return {
|
|
1084
1120
|
verdict: src.verdict,
|
|
@@ -1261,6 +1297,7 @@ __export(credential_store_exports, {
|
|
|
1261
1297
|
KEYCHAIN_LOCATION: () => KEYCHAIN_LOCATION,
|
|
1262
1298
|
credentialPath: () => credentialPath,
|
|
1263
1299
|
readStoredCredential: () => readStoredCredential,
|
|
1300
|
+
readStoredCredentialKeychainOnly: () => readStoredCredentialKeychainOnly,
|
|
1264
1301
|
writeStoredCredential: () => writeStoredCredential
|
|
1265
1302
|
});
|
|
1266
1303
|
import { homedir as homedir3 } from "node:os";
|
|
@@ -1319,6 +1356,11 @@ function readStoredCredential(env = process.env, keychain = realKeychain) {
|
|
|
1319
1356
|
}
|
|
1320
1357
|
return readFromFile(env);
|
|
1321
1358
|
}
|
|
1359
|
+
function readStoredCredentialKeychainOnly(env = process.env, keychain = realKeychain) {
|
|
1360
|
+
if (!keychainEnabled(env, keychain)) return null;
|
|
1361
|
+
const raw = keychain.get();
|
|
1362
|
+
return raw ? deserialize(raw) : null;
|
|
1363
|
+
}
|
|
1322
1364
|
function deleteFile(env) {
|
|
1323
1365
|
try {
|
|
1324
1366
|
rmSync(credentialPath(env), { force: true });
|
|
@@ -1368,43 +1410,505 @@ var init_credential_store = __esm({
|
|
|
1368
1410
|
}
|
|
1369
1411
|
});
|
|
1370
1412
|
|
|
1371
|
-
// src/tools/memory/
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1413
|
+
// src/tools/memory/safe-memory-file.ts
|
|
1414
|
+
import { resolve, sep } from "node:path";
|
|
1415
|
+
function isSafeMemoryFileName(fileName) {
|
|
1416
|
+
return fileName.length <= 200 && fileName.trim() === fileName && !fileName.includes("/") && !fileName.includes("\\") && !fileName.includes(":") && SAFE_MEMORY_FILE_RE.test(fileName);
|
|
1417
|
+
}
|
|
1418
|
+
function resolveMemoryFilePath(memoryDir, fileName) {
|
|
1419
|
+
if (!isSafeMemoryFileName(fileName)) {
|
|
1420
|
+
throw new Error(`unsafe memory file_name: ${fileName.slice(0, 80)}`);
|
|
1421
|
+
}
|
|
1422
|
+
const root = resolve(memoryDir);
|
|
1423
|
+
const filePath = resolve(root, fileName);
|
|
1424
|
+
const rootPrefix = root.endsWith(sep) ? root : `${root}${sep}`;
|
|
1425
|
+
if (filePath !== root && !filePath.startsWith(rootPrefix)) {
|
|
1426
|
+
throw new Error(`memory file path escapes memory directory: ${fileName.slice(0, 80)}`);
|
|
1427
|
+
}
|
|
1428
|
+
return filePath;
|
|
1429
|
+
}
|
|
1430
|
+
var SAFE_MEMORY_FILE_RE;
|
|
1431
|
+
var init_safe_memory_file = __esm({
|
|
1432
|
+
"src/tools/memory/safe-memory-file.ts"() {
|
|
1433
|
+
"use strict";
|
|
1434
|
+
SAFE_MEMORY_FILE_RE = /^[A-Za-z0-9][A-Za-z0-9._ -]*\.md$/i;
|
|
1435
|
+
}
|
|
1382
1436
|
});
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
import {
|
|
1386
|
-
function
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1437
|
+
|
|
1438
|
+
// src/tools/memory/sync-lock-liveness.ts
|
|
1439
|
+
import { statSync as statSync4, readFileSync as readFileSync8 } from "node:fs";
|
|
1440
|
+
function defaultIsProcessAlive(pid) {
|
|
1441
|
+
try {
|
|
1442
|
+
process.kill(pid, 0);
|
|
1443
|
+
return true;
|
|
1444
|
+
} catch (err) {
|
|
1445
|
+
return err.code === "EPERM";
|
|
1446
|
+
}
|
|
1392
1447
|
}
|
|
1393
|
-
function
|
|
1394
|
-
|
|
1448
|
+
function toPayload(parsed) {
|
|
1449
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
|
|
1450
|
+
const record = parsed;
|
|
1451
|
+
const token = record["token"];
|
|
1452
|
+
const host = record["hostname"];
|
|
1453
|
+
if (typeof token !== "string" || token.length === 0) return null;
|
|
1454
|
+
const pid = record["pid"];
|
|
1455
|
+
const acquiredAtMs = record["acquiredAtMs"];
|
|
1456
|
+
return {
|
|
1457
|
+
pid: typeof pid === "number" && Number.isInteger(pid) && pid > 0 ? pid : 0,
|
|
1458
|
+
hostname: typeof host === "string" ? host : "",
|
|
1459
|
+
sessionId: typeof record["sessionId"] === "string" ? record["sessionId"] : null,
|
|
1460
|
+
token,
|
|
1461
|
+
acquiredAt: typeof record["acquiredAt"] === "string" ? record["acquiredAt"] : "",
|
|
1462
|
+
acquiredAtMs: typeof acquiredAtMs === "number" && Number.isFinite(acquiredAtMs) ? acquiredAtMs : Number.NaN
|
|
1463
|
+
};
|
|
1395
1464
|
}
|
|
1396
|
-
function
|
|
1397
|
-
|
|
1398
|
-
|
|
1465
|
+
function readLockRecord(path3) {
|
|
1466
|
+
let raw;
|
|
1467
|
+
try {
|
|
1468
|
+
raw = readFileSync8(path3, "utf8");
|
|
1469
|
+
} catch {
|
|
1470
|
+
return null;
|
|
1471
|
+
}
|
|
1472
|
+
try {
|
|
1473
|
+
return { raw, payload: toPayload(JSON.parse(raw)) };
|
|
1474
|
+
} catch {
|
|
1475
|
+
return { raw, payload: null };
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
function lockAgeMs(record, path3, nowMs) {
|
|
1479
|
+
let startedMs = Number.NaN;
|
|
1480
|
+
if (record.payload) {
|
|
1481
|
+
if (Number.isFinite(record.payload.acquiredAtMs)) {
|
|
1482
|
+
startedMs = record.payload.acquiredAtMs;
|
|
1483
|
+
} else if (record.payload.acquiredAt) {
|
|
1484
|
+
startedMs = Date.parse(record.payload.acquiredAt);
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
if (!Number.isFinite(startedMs)) {
|
|
1488
|
+
try {
|
|
1489
|
+
startedMs = statSync4(path3).mtimeMs;
|
|
1490
|
+
} catch {
|
|
1491
|
+
return null;
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
const age = nowMs - startedMs;
|
|
1495
|
+
return Number.isFinite(age) && age >= 0 ? age : null;
|
|
1496
|
+
}
|
|
1497
|
+
function classifyHolderLiveness(record, isProcessAlive, thisHost) {
|
|
1498
|
+
const payload = record.payload;
|
|
1499
|
+
if (payload === null) return "unknown";
|
|
1500
|
+
if (payload.pid <= 0) return "unknown";
|
|
1501
|
+
if (thisHost.length === 0) return "unknown";
|
|
1502
|
+
if (payload.hostname !== thisHost) return "unknown";
|
|
1503
|
+
return isProcessAlive(payload.pid) ? "alive" : "dead";
|
|
1504
|
+
}
|
|
1505
|
+
function isLockAbandoned(record, ageMs, ttlMs, isProcessAlive, thisHost) {
|
|
1506
|
+
const liveness = classifyHolderLiveness(record, isProcessAlive, thisHost);
|
|
1507
|
+
if (liveness === "alive") return false;
|
|
1508
|
+
if (liveness === "dead") return true;
|
|
1509
|
+
return ageMs !== null && ageMs > ttlMs;
|
|
1510
|
+
}
|
|
1511
|
+
var init_sync_lock_liveness = __esm({
|
|
1512
|
+
"src/tools/memory/sync-lock-liveness.ts"() {
|
|
1513
|
+
"use strict";
|
|
1514
|
+
}
|
|
1515
|
+
});
|
|
1516
|
+
|
|
1517
|
+
// src/tools/memory/sync-lock.ts
|
|
1518
|
+
import { closeSync as closeSync2, mkdirSync as mkdirSync5, openSync as openSync3, readFileSync as readFileSync9, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "node:fs";
|
|
1519
|
+
import { hostname } from "node:os";
|
|
1520
|
+
import { join as join8 } from "node:path";
|
|
1521
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
1522
|
+
function positiveOr(value, fallback) {
|
|
1523
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : fallback;
|
|
1524
|
+
}
|
|
1525
|
+
function createExclusive2(path3, contents) {
|
|
1526
|
+
let fd;
|
|
1527
|
+
try {
|
|
1528
|
+
fd = openSync3(path3, "wx");
|
|
1529
|
+
} catch (err) {
|
|
1530
|
+
const code = err.code;
|
|
1531
|
+
return { ok: false, exists: code === "EEXIST", message: err instanceof Error ? err.message : String(err) };
|
|
1532
|
+
}
|
|
1533
|
+
try {
|
|
1534
|
+
writeFileSync4(fd, contents, "utf8");
|
|
1535
|
+
} catch (err) {
|
|
1536
|
+
closeSync2(fd);
|
|
1537
|
+
try {
|
|
1538
|
+
unlinkSync2(path3);
|
|
1539
|
+
} catch {
|
|
1540
|
+
}
|
|
1541
|
+
return { ok: false, exists: false, message: err instanceof Error ? err.message : String(err) };
|
|
1542
|
+
}
|
|
1543
|
+
closeSync2(fd);
|
|
1544
|
+
return { ok: true };
|
|
1545
|
+
}
|
|
1546
|
+
function removeAbandoned(path3, expectedRaw) {
|
|
1547
|
+
let current;
|
|
1548
|
+
try {
|
|
1549
|
+
current = readFileSync9(path3, "utf8");
|
|
1550
|
+
} catch {
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
if (current !== expectedRaw) return;
|
|
1554
|
+
try {
|
|
1555
|
+
unlinkSync2(path3);
|
|
1556
|
+
} catch {
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
function makeRelease(path3, token) {
|
|
1560
|
+
let released = false;
|
|
1561
|
+
return () => {
|
|
1562
|
+
if (released) return;
|
|
1563
|
+
released = true;
|
|
1564
|
+
let raw;
|
|
1565
|
+
try {
|
|
1566
|
+
raw = readFileSync9(path3, "utf8");
|
|
1567
|
+
} catch {
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
let stillOurs;
|
|
1571
|
+
try {
|
|
1572
|
+
stillOurs = toPayload(JSON.parse(raw))?.token === token;
|
|
1573
|
+
} catch {
|
|
1574
|
+
stillOurs = false;
|
|
1575
|
+
}
|
|
1576
|
+
if (!stillOurs) return;
|
|
1577
|
+
try {
|
|
1578
|
+
unlinkSync2(path3);
|
|
1579
|
+
} catch {
|
|
1580
|
+
}
|
|
1581
|
+
};
|
|
1582
|
+
}
|
|
1583
|
+
function describeHolder(record) {
|
|
1584
|
+
const payload = record?.payload;
|
|
1585
|
+
if (!payload) return "an unreadable lock file";
|
|
1586
|
+
return `pid ${payload.pid} on ${payload.hostname || "(unknown host)"} (session ${payload.sessionId ?? "unknown"}, held since ${payload.acquiredAt || "unknown"})`;
|
|
1587
|
+
}
|
|
1588
|
+
async function acquireMemorySyncLock(options) {
|
|
1589
|
+
const waitMs = positiveOr(options.waitMs, DEFAULT_LOCK_WAIT_MS);
|
|
1590
|
+
const ttlMs = positiveOr(options.ttlMs, DEFAULT_LOCK_TTL_MS);
|
|
1591
|
+
const now = options.now ?? Date.now;
|
|
1592
|
+
const sleep = options.sleep ?? ((ms) => new Promise((resolve3) => {
|
|
1593
|
+
setTimeout(resolve3, ms);
|
|
1594
|
+
}));
|
|
1595
|
+
const isProcessAlive = options.isProcessAlive ?? defaultIsProcessAlive;
|
|
1596
|
+
const thisHost = hostname();
|
|
1597
|
+
const path3 = join8(options.memoryDir, MEMORY_SYNC_LOCK_FILE);
|
|
1598
|
+
if (options.createDir === true) mkdirSync5(options.memoryDir, { recursive: true });
|
|
1599
|
+
const deadline = now() + waitMs;
|
|
1600
|
+
let backoffMs = INITIAL_BACKOFF_MS;
|
|
1601
|
+
let tookOverFrom = null;
|
|
1602
|
+
let holderDescription = "another session";
|
|
1603
|
+
for (; ; ) {
|
|
1604
|
+
const acquiredAtMs = now();
|
|
1605
|
+
const payload = {
|
|
1606
|
+
pid: process.pid,
|
|
1607
|
+
hostname: thisHost,
|
|
1608
|
+
sessionId: options.sessionId ?? null,
|
|
1609
|
+
token: randomUUID2(),
|
|
1610
|
+
acquiredAt: new Date(acquiredAtMs).toISOString(),
|
|
1611
|
+
acquiredAtMs
|
|
1612
|
+
};
|
|
1613
|
+
const created = createExclusive2(path3, `${JSON.stringify(payload, null, 2)}
|
|
1614
|
+
`);
|
|
1615
|
+
if (created.ok) {
|
|
1616
|
+
return { path: path3, payload, tookOverFrom, release: makeRelease(path3, payload.token) };
|
|
1617
|
+
}
|
|
1618
|
+
if (!created.exists) {
|
|
1619
|
+
throw new Error(
|
|
1620
|
+
`memory sync lock ${path3} could not be created (${created.message}) \u2014 refusing to sync without exclusion`
|
|
1621
|
+
);
|
|
1622
|
+
}
|
|
1623
|
+
const record = readLockRecord(path3);
|
|
1624
|
+
let reclaimed = false;
|
|
1625
|
+
if (record) {
|
|
1626
|
+
holderDescription = describeHolder(record);
|
|
1627
|
+
const age = lockAgeMs(record, path3, now());
|
|
1628
|
+
if (isLockAbandoned(record, age, ttlMs, isProcessAlive, thisHost)) {
|
|
1629
|
+
tookOverFrom = record.payload;
|
|
1630
|
+
removeAbandoned(path3, record.raw);
|
|
1631
|
+
reclaimed = true;
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
if (now() >= deadline) {
|
|
1635
|
+
throw new Error(
|
|
1636
|
+
`memory sync lock ${path3} is held by ${holderDescription}; waited ${waitMs}ms \u2014 refusing to sync unlocked (concurrent memory writes corrupt the shared index). If that holder is provably gone, delete the lock file.`
|
|
1637
|
+
);
|
|
1638
|
+
}
|
|
1639
|
+
if (reclaimed) backoffMs = INITIAL_BACKOFF_MS;
|
|
1640
|
+
await sleep(Math.max(1, Math.min(backoffMs, deadline - now())));
|
|
1641
|
+
if (!reclaimed) backoffMs = Math.min(MAX_BACKOFF_MS, Math.ceil(backoffMs * BACKOFF_FACTOR));
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
async function withMemorySyncLock(options, fn) {
|
|
1645
|
+
const handle = await acquireMemorySyncLock(options);
|
|
1646
|
+
try {
|
|
1647
|
+
return await fn(handle);
|
|
1648
|
+
} finally {
|
|
1649
|
+
handle.release();
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
var MEMORY_SYNC_LOCK_FILE, DEFAULT_LOCK_TTL_MS, DEFAULT_LOCK_WAIT_MS, INITIAL_BACKOFF_MS, MAX_BACKOFF_MS, BACKOFF_FACTOR;
|
|
1653
|
+
var init_sync_lock = __esm({
|
|
1654
|
+
"src/tools/memory/sync-lock.ts"() {
|
|
1655
|
+
"use strict";
|
|
1656
|
+
init_sync_lock_liveness();
|
|
1657
|
+
init_sync_lock_liveness();
|
|
1658
|
+
MEMORY_SYNC_LOCK_FILE = ".memory-sync.lock";
|
|
1659
|
+
DEFAULT_LOCK_TTL_MS = 15 * 6e4;
|
|
1660
|
+
DEFAULT_LOCK_WAIT_MS = 1e4;
|
|
1661
|
+
INITIAL_BACKOFF_MS = 25;
|
|
1662
|
+
MAX_BACKOFF_MS = 500;
|
|
1663
|
+
BACKOFF_FACTOR = 1.6;
|
|
1664
|
+
}
|
|
1665
|
+
});
|
|
1666
|
+
|
|
1667
|
+
// src/tools/memory/memory-index-merge.ts
|
|
1668
|
+
function isMemoryIndexFile(fileName) {
|
|
1669
|
+
return fileName.toUpperCase() === MEMORY_INDEX_FILE.toUpperCase();
|
|
1670
|
+
}
|
|
1671
|
+
function indexRowKey(line) {
|
|
1672
|
+
const match = INDEX_ROW_RE.exec(line);
|
|
1673
|
+
if (!match) return null;
|
|
1674
|
+
let target = match[1].trim();
|
|
1675
|
+
if (target.startsWith("<") && target.endsWith(">")) target = target.slice(1, -1).trim();
|
|
1676
|
+
target = target.replace(/\s+(["'])[\s\S]*\1$/, "").trim();
|
|
1677
|
+
if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(target)) {
|
|
1678
|
+
target = target.replace(/\\/g, "/").replace(/\/{2,}/g, "/");
|
|
1679
|
+
target = target.replace(/^(?:\.\/)+/, "");
|
|
1680
|
+
}
|
|
1681
|
+
return target.length > 0 ? target.toLowerCase() : null;
|
|
1682
|
+
}
|
|
1683
|
+
function mergeMemoryIndex(localContent, cloudContent) {
|
|
1684
|
+
if (typeof cloudContent !== "string" || cloudContent.trim().length === 0) {
|
|
1685
|
+
return { content: localContent, addedFromCloud: [] };
|
|
1686
|
+
}
|
|
1687
|
+
const eol = localContent.includes("\r\n") ? "\r\n" : "\n";
|
|
1688
|
+
const localLines = localContent.split(/\r?\n/);
|
|
1689
|
+
const localKeys = /* @__PURE__ */ new Set();
|
|
1690
|
+
let lastLocalRowIndex = -1;
|
|
1691
|
+
for (let i = 0; i < localLines.length; i++) {
|
|
1692
|
+
const key = indexRowKey(localLines[i]);
|
|
1693
|
+
if (key === null) continue;
|
|
1694
|
+
localKeys.add(key);
|
|
1695
|
+
lastLocalRowIndex = i;
|
|
1696
|
+
}
|
|
1697
|
+
const addedFromCloud = [];
|
|
1698
|
+
const seenCloudKeys = /* @__PURE__ */ new Set();
|
|
1699
|
+
for (const rawLine of cloudContent.split(/\r?\n/)) {
|
|
1700
|
+
const key = indexRowKey(rawLine);
|
|
1701
|
+
if (key === null) continue;
|
|
1702
|
+
if (localKeys.has(key) || seenCloudKeys.has(key)) continue;
|
|
1703
|
+
seenCloudKeys.add(key);
|
|
1704
|
+
addedFromCloud.push(rawLine.replace(/\r$/, ""));
|
|
1705
|
+
}
|
|
1706
|
+
if (addedFromCloud.length === 0) {
|
|
1707
|
+
return { content: localContent, addedFromCloud: [] };
|
|
1708
|
+
}
|
|
1709
|
+
const merged = lastLocalRowIndex >= 0 ? [...localLines.slice(0, lastLocalRowIndex + 1), ...addedFromCloud, ...localLines.slice(lastLocalRowIndex + 1)] : [...localLines, ...addedFromCloud];
|
|
1710
|
+
return { content: merged.join(eol), addedFromCloud };
|
|
1711
|
+
}
|
|
1712
|
+
var MEMORY_INDEX_FILE, INDEX_ROW_RE;
|
|
1713
|
+
var init_memory_index_merge = __esm({
|
|
1714
|
+
"src/tools/memory/memory-index-merge.ts"() {
|
|
1715
|
+
"use strict";
|
|
1716
|
+
MEMORY_INDEX_FILE = "MEMORY.md";
|
|
1717
|
+
INDEX_ROW_RE = /^\s*[-*]\s+\[[^\]]*\]\(([^)]+)\)/;
|
|
1718
|
+
}
|
|
1719
|
+
});
|
|
1720
|
+
|
|
1721
|
+
// src/tools/memory/bounded-sync.ts
|
|
1722
|
+
function createSyncDeadline(budgetMs = SYNC_DEADLINE_MS, now = Date.now) {
|
|
1723
|
+
const startedAt = now();
|
|
1724
|
+
return {
|
|
1725
|
+
check() {
|
|
1726
|
+
const elapsed = now() - startedAt;
|
|
1727
|
+
if (elapsed > budgetMs) throw new SyncDeadlineExceededError(elapsed, budgetMs);
|
|
1728
|
+
},
|
|
1729
|
+
remainingMs() {
|
|
1730
|
+
return Math.max(0, budgetMs - (now() - startedAt));
|
|
1731
|
+
}
|
|
1732
|
+
};
|
|
1733
|
+
}
|
|
1734
|
+
async function withRequestTimeout(url, run, budgetMs = REQUEST_TIMEOUT_MS) {
|
|
1735
|
+
let timer;
|
|
1736
|
+
try {
|
|
1737
|
+
return await Promise.race([
|
|
1738
|
+
run(),
|
|
1739
|
+
new Promise((_resolve, reject) => {
|
|
1740
|
+
timer = setTimeout(() => reject(new RequestTimeoutError(url, budgetMs)), budgetMs);
|
|
1741
|
+
timer.unref?.();
|
|
1742
|
+
})
|
|
1743
|
+
]);
|
|
1744
|
+
} finally {
|
|
1745
|
+
if (timer) clearTimeout(timer);
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
async function mapWithConcurrency(items, limit, fn) {
|
|
1749
|
+
const results = new Array(items.length);
|
|
1750
|
+
const width = Math.max(1, Math.min(limit, items.length));
|
|
1751
|
+
let next = 0;
|
|
1752
|
+
async function worker() {
|
|
1753
|
+
for (; ; ) {
|
|
1754
|
+
const index = next++;
|
|
1755
|
+
if (index >= items.length) return;
|
|
1756
|
+
try {
|
|
1757
|
+
results[index] = { ok: true, value: await fn(items[index], index) };
|
|
1758
|
+
} catch (error) {
|
|
1759
|
+
results[index] = { ok: false, error };
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
await Promise.all(Array.from({ length: width }, () => worker()));
|
|
1764
|
+
return results;
|
|
1765
|
+
}
|
|
1766
|
+
var REQUEST_TIMEOUT_MS, SYNC_DEADLINE_MS, PUSH_CONCURRENCY, SyncDeadlineExceededError, RequestTimeoutError;
|
|
1767
|
+
var init_bounded_sync = __esm({
|
|
1768
|
+
"src/tools/memory/bounded-sync.ts"() {
|
|
1769
|
+
"use strict";
|
|
1770
|
+
REQUEST_TIMEOUT_MS = 15e3;
|
|
1771
|
+
SYNC_DEADLINE_MS = 12e4;
|
|
1772
|
+
PUSH_CONCURRENCY = 6;
|
|
1773
|
+
SyncDeadlineExceededError = class extends Error {
|
|
1774
|
+
constructor(elapsedMs, budgetMs) {
|
|
1775
|
+
super(
|
|
1776
|
+
`memory sync exceeded its ${budgetMs}ms deadline after ${elapsedMs}ms \u2014 aborting so the lock is released instead of held indefinitely`
|
|
1777
|
+
);
|
|
1778
|
+
this.name = "SyncDeadlineExceededError";
|
|
1779
|
+
}
|
|
1780
|
+
};
|
|
1781
|
+
RequestTimeoutError = class extends Error {
|
|
1782
|
+
constructor(url, budgetMs) {
|
|
1783
|
+
super(`memory sync request to ${url} exceeded ${budgetMs}ms`);
|
|
1784
|
+
this.name = "RequestTimeoutError";
|
|
1785
|
+
}
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1788
|
+
});
|
|
1789
|
+
|
|
1790
|
+
// src/tools/memory/memory-push-cache.ts
|
|
1791
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
1792
|
+
import { readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "node:fs";
|
|
1793
|
+
import { join as join9 } from "node:path";
|
|
1794
|
+
function sha256(content) {
|
|
1795
|
+
return createHash3("sha256").update(content, "utf8").digest("hex");
|
|
1796
|
+
}
|
|
1797
|
+
function statePath(memoryDir) {
|
|
1798
|
+
return join9(memoryDir, MEMORY_SYNC_STATE_FILE);
|
|
1799
|
+
}
|
|
1800
|
+
function readPushCache(memoryDir, controlPlaneUrl) {
|
|
1801
|
+
const empty = { controlPlaneUrl, entries: /* @__PURE__ */ new Map(), knowledgeSweptAtMs: null };
|
|
1802
|
+
let raw;
|
|
1803
|
+
try {
|
|
1804
|
+
raw = readFileSync10(statePath(memoryDir), "utf8");
|
|
1805
|
+
} catch {
|
|
1806
|
+
return empty;
|
|
1807
|
+
}
|
|
1808
|
+
let parsed;
|
|
1809
|
+
try {
|
|
1810
|
+
parsed = JSON.parse(raw);
|
|
1811
|
+
} catch {
|
|
1812
|
+
return empty;
|
|
1813
|
+
}
|
|
1814
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return empty;
|
|
1815
|
+
const obj = parsed;
|
|
1816
|
+
if (obj["version"] !== STATE_VERSION) return empty;
|
|
1817
|
+
if (obj["controlPlaneUrl"] !== controlPlaneUrl) return empty;
|
|
1818
|
+
const files = obj["entries"];
|
|
1819
|
+
if (typeof files !== "object" || files === null || Array.isArray(files)) return empty;
|
|
1820
|
+
const entries = /* @__PURE__ */ new Map();
|
|
1821
|
+
for (const [name, value] of Object.entries(files)) {
|
|
1822
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) continue;
|
|
1823
|
+
const row = value;
|
|
1824
|
+
const memoryHash = typeof row["memoryHash"] === "string" ? row["memoryHash"] : void 0;
|
|
1825
|
+
const knowledgeHash = typeof row["knowledgeHash"] === "string" ? row["knowledgeHash"] : void 0;
|
|
1826
|
+
if (memoryHash === void 0 && knowledgeHash === void 0) continue;
|
|
1827
|
+
entries.set(name, {
|
|
1828
|
+
...memoryHash !== void 0 ? { memoryHash } : {},
|
|
1829
|
+
...knowledgeHash !== void 0 ? { knowledgeHash } : {}
|
|
1830
|
+
});
|
|
1831
|
+
}
|
|
1832
|
+
const sweptAt = obj["knowledgeSweptAtMs"];
|
|
1833
|
+
return {
|
|
1834
|
+
controlPlaneUrl,
|
|
1835
|
+
entries,
|
|
1836
|
+
// An unreadable/absent sweep stamp reads as NEVER SWEPT, which forces a full
|
|
1837
|
+
// sweep — the fail-closed direction (more upserts, never fewer).
|
|
1838
|
+
knowledgeSweptAtMs: typeof sweptAt === "number" && Number.isFinite(sweptAt) ? sweptAt : null
|
|
1839
|
+
};
|
|
1840
|
+
}
|
|
1841
|
+
function writePushCache(memoryDir, cache) {
|
|
1842
|
+
const entries = {};
|
|
1843
|
+
for (const [name, value] of cache.entries) entries[name] = value;
|
|
1844
|
+
try {
|
|
1845
|
+
writeFileSync5(
|
|
1846
|
+
statePath(memoryDir),
|
|
1847
|
+
`${JSON.stringify(
|
|
1848
|
+
{
|
|
1849
|
+
version: STATE_VERSION,
|
|
1850
|
+
controlPlaneUrl: cache.controlPlaneUrl,
|
|
1851
|
+
knowledgeSweptAtMs: cache.knowledgeSweptAtMs,
|
|
1852
|
+
entries
|
|
1853
|
+
},
|
|
1854
|
+
null,
|
|
1855
|
+
2
|
|
1856
|
+
)}
|
|
1857
|
+
`,
|
|
1858
|
+
"utf8"
|
|
1859
|
+
);
|
|
1860
|
+
} catch {
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
function recordMemoryPush(cache, fileName, payloadHash) {
|
|
1864
|
+
cache.entries.set(fileName, { ...cache.entries.get(fileName), memoryHash: payloadHash });
|
|
1865
|
+
}
|
|
1866
|
+
function recordKnowledgePush(cache, fileName, contentHash) {
|
|
1867
|
+
cache.entries.set(fileName, { ...cache.entries.get(fileName), knowledgeHash: contentHash });
|
|
1868
|
+
}
|
|
1869
|
+
function pruneMissing(cache, presentFileNames) {
|
|
1870
|
+
const present = new Set(presentFileNames);
|
|
1871
|
+
for (const name of [...cache.entries.keys()]) {
|
|
1872
|
+
if (!present.has(name)) cache.entries.delete(name);
|
|
1873
|
+
}
|
|
1399
1874
|
}
|
|
1875
|
+
function needsMemoryPush(cache, fileName, payloadHash, serverHasEntry) {
|
|
1876
|
+
if (!serverHasEntry) return true;
|
|
1877
|
+
return cache.entries.get(fileName)?.memoryHash !== payloadHash;
|
|
1878
|
+
}
|
|
1879
|
+
function knowledgeSweepDue(cache, nowMs = Date.now()) {
|
|
1880
|
+
const swept = cache.knowledgeSweptAtMs;
|
|
1881
|
+
if (swept === null || !Number.isFinite(swept)) return true;
|
|
1882
|
+
const age = nowMs - swept;
|
|
1883
|
+
return !(age >= 0 && age < KNOWLEDGE_FULL_SWEEP_MS);
|
|
1884
|
+
}
|
|
1885
|
+
function needsKnowledgePush(cache, fileName, contentHash, sweepDue = false) {
|
|
1886
|
+
if (sweepDue) return true;
|
|
1887
|
+
return cache.entries.get(fileName)?.knowledgeHash !== contentHash;
|
|
1888
|
+
}
|
|
1889
|
+
var MEMORY_SYNC_STATE_FILE, STATE_VERSION, KNOWLEDGE_FULL_SWEEP_MS;
|
|
1890
|
+
var init_memory_push_cache = __esm({
|
|
1891
|
+
"src/tools/memory/memory-push-cache.ts"() {
|
|
1892
|
+
"use strict";
|
|
1893
|
+
MEMORY_SYNC_STATE_FILE = ".memory-sync-state.json";
|
|
1894
|
+
STATE_VERSION = 1;
|
|
1895
|
+
KNOWLEDGE_FULL_SWEEP_MS = 24 * 60 * 6e4;
|
|
1896
|
+
}
|
|
1897
|
+
});
|
|
1898
|
+
|
|
1899
|
+
// src/tools/memory/memory-sync-http.ts
|
|
1900
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync6, readdirSync as readdirSync4 } from "node:fs";
|
|
1400
1901
|
async function pullMemory(controlPlaneUrl, token, memoryDir, fetchFn) {
|
|
1401
1902
|
const url = `${controlPlaneUrl}/api/v1/agent-config/memory/me`;
|
|
1402
|
-
const response = await
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1903
|
+
const response = await withRequestTimeout(
|
|
1904
|
+
url,
|
|
1905
|
+
() => fetchFn(url, {
|
|
1906
|
+
method: "GET",
|
|
1907
|
+
headers: {
|
|
1908
|
+
authorization: `Bearer ${token}`
|
|
1909
|
+
}
|
|
1910
|
+
})
|
|
1911
|
+
);
|
|
1408
1912
|
if (response.status !== 200) {
|
|
1409
1913
|
const text = await response.text();
|
|
1410
1914
|
throw new Error(`GET /api/v1/agent-config/memory/me returned HTTP ${response.status}: ${text.slice(0, 200)}`);
|
|
@@ -1413,117 +1917,392 @@ async function pullMemory(controlPlaneUrl, token, memoryDir, fetchFn) {
|
|
|
1413
1917
|
if (!data.ok || !Array.isArray(data.entries)) {
|
|
1414
1918
|
throw new Error("GET /api/v1/agent-config/memory/me response missing ok=true or entries array");
|
|
1415
1919
|
}
|
|
1416
|
-
|
|
1920
|
+
const writes = data.entries.map((entry) => ({
|
|
1921
|
+
entry,
|
|
1922
|
+
filePath: resolveMemoryFilePath(memoryDir, entry.file_name)
|
|
1923
|
+
}));
|
|
1924
|
+
mkdirSync6(memoryDir, { recursive: true });
|
|
1417
1925
|
const files = [];
|
|
1418
|
-
for (const entry of
|
|
1419
|
-
|
|
1420
|
-
writeFileSync3(filePath, entry.content, "utf8");
|
|
1926
|
+
for (const { entry, filePath } of writes) {
|
|
1927
|
+
writeFileSync6(filePath, entry.content, "utf8");
|
|
1421
1928
|
files.push(entry.file_name);
|
|
1422
1929
|
}
|
|
1423
1930
|
return { pulled: data.entries.length, files };
|
|
1424
1931
|
}
|
|
1425
|
-
|
|
1932
|
+
function listPushableFiles(memoryDir) {
|
|
1933
|
+
return readdirSync4(memoryDir).filter((f) => f.endsWith(".md") && f !== MEMORY_SYNC_LOCK_FILE);
|
|
1934
|
+
}
|
|
1935
|
+
async function uploadOne(item, controlPlaneUrl, token, sessionId, fetchFn, deadline) {
|
|
1936
|
+
deadline.check();
|
|
1937
|
+
if (item.memoryId !== null) {
|
|
1938
|
+
const updateUrl = `${controlPlaneUrl}/api/v1/agent-config/memory/${item.memoryId}`;
|
|
1939
|
+
const updateBody = { content: item.content, session_id: sessionId };
|
|
1940
|
+
const updateResponse = await withRequestTimeout(
|
|
1941
|
+
updateUrl,
|
|
1942
|
+
() => fetchFn(updateUrl, {
|
|
1943
|
+
method: "PUT",
|
|
1944
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
1945
|
+
body: JSON.stringify(updateBody)
|
|
1946
|
+
})
|
|
1947
|
+
);
|
|
1948
|
+
if (updateResponse.status !== 200) {
|
|
1949
|
+
const text = await updateResponse.text();
|
|
1950
|
+
throw new Error(
|
|
1951
|
+
`PUT /api/v1/agent-config/memory/${item.memoryId} returned HTTP ${updateResponse.status}: ${text.slice(0, 200)}`
|
|
1952
|
+
);
|
|
1953
|
+
}
|
|
1954
|
+
const updateData = JSON.parse(await updateResponse.text());
|
|
1955
|
+
if (!updateData.ok) throw new Error(`PUT /api/v1/agent-config/memory/${item.memoryId} returned ok=false`);
|
|
1956
|
+
return "updated";
|
|
1957
|
+
}
|
|
1958
|
+
const createUrl = `${controlPlaneUrl}/api/v1/agent-config/memory/me`;
|
|
1959
|
+
const createBody = {
|
|
1960
|
+
entry_type: item.entryType,
|
|
1961
|
+
file_name: item.fileName,
|
|
1962
|
+
content: item.content,
|
|
1963
|
+
session_id: sessionId
|
|
1964
|
+
};
|
|
1965
|
+
const createResponse = await withRequestTimeout(
|
|
1966
|
+
createUrl,
|
|
1967
|
+
() => fetchFn(createUrl, {
|
|
1968
|
+
method: "POST",
|
|
1969
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
1970
|
+
body: JSON.stringify(createBody)
|
|
1971
|
+
})
|
|
1972
|
+
);
|
|
1973
|
+
if (createResponse.status !== 200 && createResponse.status !== 201) {
|
|
1974
|
+
const text = await createResponse.text();
|
|
1975
|
+
throw new Error(
|
|
1976
|
+
`POST /api/v1/agent-config/memory/me returned HTTP ${createResponse.status}: ${text.slice(0, 200)}`
|
|
1977
|
+
);
|
|
1978
|
+
}
|
|
1979
|
+
const createData = JSON.parse(await createResponse.text());
|
|
1980
|
+
if (!createData.ok) throw new Error("POST /api/v1/agent-config/memory/me returned ok=false");
|
|
1981
|
+
return "created";
|
|
1982
|
+
}
|
|
1983
|
+
async function pushMemory(controlPlaneUrl, token, memoryDir, sessionId, fetchFn, options = {}) {
|
|
1984
|
+
const empty = { pushed: 0, created: 0, updated: 0, skipped: 0, indexRowsPreserved: 0 };
|
|
1426
1985
|
if (!existsSync5(memoryDir)) {
|
|
1427
|
-
return
|
|
1986
|
+
return empty;
|
|
1428
1987
|
}
|
|
1429
|
-
const localFiles =
|
|
1988
|
+
const localFiles = listPushableFiles(memoryDir).map((f) => ({
|
|
1430
1989
|
file_name: f,
|
|
1431
|
-
content:
|
|
1432
|
-
entry_type: f
|
|
1990
|
+
content: readFileSync11(resolveMemoryFilePath(memoryDir, f), "utf8"),
|
|
1991
|
+
entry_type: isMemoryIndexFile(f) ? "index" : "topic"
|
|
1433
1992
|
}));
|
|
1434
1993
|
if (localFiles.length === 0) {
|
|
1435
|
-
return
|
|
1994
|
+
return empty;
|
|
1436
1995
|
}
|
|
1996
|
+
const deadline = options.deadline ?? createSyncDeadline();
|
|
1997
|
+
const cache = options.cache ?? readPushCache(memoryDir, controlPlaneUrl);
|
|
1998
|
+
deadline.check();
|
|
1437
1999
|
const getUrl = `${controlPlaneUrl}/api/v1/agent-config/memory/me`;
|
|
1438
|
-
const getResponse = await
|
|
1439
|
-
|
|
1440
|
-
headers: {
|
|
1441
|
-
|
|
1442
|
-
}
|
|
1443
|
-
});
|
|
2000
|
+
const getResponse = await withRequestTimeout(
|
|
2001
|
+
getUrl,
|
|
2002
|
+
() => fetchFn(getUrl, { method: "GET", headers: { authorization: `Bearer ${token}` } })
|
|
2003
|
+
);
|
|
1444
2004
|
const existingMap = /* @__PURE__ */ new Map();
|
|
1445
2005
|
if (getResponse.status === 200) {
|
|
1446
2006
|
const getData = JSON.parse(await getResponse.text());
|
|
1447
2007
|
if (getData.ok && Array.isArray(getData.entries)) {
|
|
1448
2008
|
for (const entry of getData.entries) {
|
|
1449
|
-
existingMap.set(entry.file_name,
|
|
2009
|
+
existingMap.set(entry.file_name, {
|
|
2010
|
+
memoryId: entry.memory_id,
|
|
2011
|
+
content: typeof entry.content === "string" ? entry.content : ""
|
|
2012
|
+
});
|
|
1450
2013
|
}
|
|
1451
2014
|
}
|
|
1452
2015
|
}
|
|
2016
|
+
const toUpload = [];
|
|
2017
|
+
let skipped = 0;
|
|
2018
|
+
for (const localFile of localFiles) {
|
|
2019
|
+
const existing = existingMap.get(localFile.file_name);
|
|
2020
|
+
let content = localFile.content;
|
|
2021
|
+
let rowsPreserved = 0;
|
|
2022
|
+
if (localFile.entry_type === "index") {
|
|
2023
|
+
const merged = mergeMemoryIndex(localFile.content, existing?.content);
|
|
2024
|
+
content = merged.content;
|
|
2025
|
+
rowsPreserved = merged.addedFromCloud.length;
|
|
2026
|
+
}
|
|
2027
|
+
const payloadHash = sha256(content);
|
|
2028
|
+
if (!needsMemoryPush(cache, localFile.file_name, payloadHash, existing !== void 0)) {
|
|
2029
|
+
skipped++;
|
|
2030
|
+
continue;
|
|
2031
|
+
}
|
|
2032
|
+
toUpload.push({
|
|
2033
|
+
fileName: localFile.file_name,
|
|
2034
|
+
entryType: localFile.entry_type,
|
|
2035
|
+
content,
|
|
2036
|
+
payloadHash,
|
|
2037
|
+
memoryId: existing?.memoryId ?? null,
|
|
2038
|
+
rowsPreserved
|
|
2039
|
+
});
|
|
2040
|
+
}
|
|
2041
|
+
const outcomes = await mapWithConcurrency(
|
|
2042
|
+
toUpload,
|
|
2043
|
+
options.concurrency ?? PUSH_CONCURRENCY,
|
|
2044
|
+
(item) => uploadOne(item, controlPlaneUrl, token, sessionId, fetchFn, deadline)
|
|
2045
|
+
);
|
|
1453
2046
|
let created = 0;
|
|
1454
2047
|
let updated = 0;
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
2048
|
+
let indexRowsPreserved = 0;
|
|
2049
|
+
let firstError;
|
|
2050
|
+
for (let i = 0; i < outcomes.length; i++) {
|
|
2051
|
+
const outcome = outcomes[i];
|
|
2052
|
+
const item = toUpload[i];
|
|
2053
|
+
if (outcome.ok) {
|
|
2054
|
+
if (outcome.value === "created") created++;
|
|
2055
|
+
else updated++;
|
|
2056
|
+
indexRowsPreserved += item.rowsPreserved;
|
|
2057
|
+
recordMemoryPush(cache, item.fileName, item.payloadHash);
|
|
2058
|
+
} else if (firstError === void 0) {
|
|
2059
|
+
firstError = outcome.error;
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
pruneMissing(cache, localFiles.map((f) => f.file_name));
|
|
2063
|
+
if (options.persistCache ?? options.cache === void 0) writePushCache(memoryDir, cache);
|
|
2064
|
+
if (firstError !== void 0) throw firstError;
|
|
2065
|
+
return { pushed: created + updated, created, updated, skipped, indexRowsPreserved };
|
|
2066
|
+
}
|
|
2067
|
+
var init_memory_sync_http = __esm({
|
|
2068
|
+
"src/tools/memory/memory-sync-http.ts"() {
|
|
2069
|
+
"use strict";
|
|
2070
|
+
init_safe_memory_file();
|
|
2071
|
+
init_sync_lock();
|
|
2072
|
+
init_memory_index_merge();
|
|
2073
|
+
init_bounded_sync();
|
|
2074
|
+
init_memory_push_cache();
|
|
2075
|
+
}
|
|
2076
|
+
});
|
|
2077
|
+
|
|
2078
|
+
// src/tools/memory/sync-kill-switch.ts
|
|
2079
|
+
import { existsSync as existsSync6, readFileSync as readFileSync12 } from "node:fs";
|
|
2080
|
+
import { homedir as homedir6 } from "node:os";
|
|
2081
|
+
import { join as join10 } from "node:path";
|
|
2082
|
+
function memorySyncSentinelPath(home) {
|
|
2083
|
+
return join10(home, ".claude", MEMORY_SYNC_DISABLE_SENTINEL);
|
|
2084
|
+
}
|
|
2085
|
+
function isKillSwitchValueOn(raw) {
|
|
2086
|
+
if (raw === void 0 || raw === null) return false;
|
|
2087
|
+
const v = raw.trim().toLowerCase();
|
|
2088
|
+
if (v === "") return false;
|
|
2089
|
+
return !NEGATIONS.has(v);
|
|
2090
|
+
}
|
|
2091
|
+
function clip(raw) {
|
|
2092
|
+
const v = raw.trim();
|
|
2093
|
+
return v.length > MAX_LOGGED_VALUE ? `${v.slice(0, MAX_LOGGED_VALUE)}\u2026` : v;
|
|
2094
|
+
}
|
|
2095
|
+
function evaluateMemorySyncKillSwitch(deps = {}) {
|
|
2096
|
+
const env = deps.env ?? process.env;
|
|
2097
|
+
const home = deps.home ?? homedir6();
|
|
2098
|
+
const fileExists = deps.fileExists ?? existsSync6;
|
|
2099
|
+
const readFile3 = deps.readFile ?? ((p) => readFileSync12(p, "utf8"));
|
|
2100
|
+
const fired = [];
|
|
2101
|
+
const rawEnv = env[MEMORY_SYNC_DISABLE_ENV];
|
|
2102
|
+
if (isKillSwitchValueOn(rawEnv)) {
|
|
2103
|
+
fired.push(`env ${MEMORY_SYNC_DISABLE_ENV}=${clip(rawEnv)}`);
|
|
2104
|
+
}
|
|
2105
|
+
const sentinel = memorySyncSentinelPath(home);
|
|
2106
|
+
let sentinelPresent;
|
|
2107
|
+
try {
|
|
2108
|
+
sentinelPresent = fileExists(sentinel);
|
|
2109
|
+
} catch {
|
|
2110
|
+
sentinelPresent = false;
|
|
2111
|
+
}
|
|
2112
|
+
if (sentinelPresent) {
|
|
2113
|
+
let contents = "";
|
|
2114
|
+
let readable = true;
|
|
2115
|
+
try {
|
|
2116
|
+
contents = readFile3(sentinel);
|
|
2117
|
+
} catch {
|
|
2118
|
+
readable = false;
|
|
2119
|
+
}
|
|
2120
|
+
if (!readable || isKillSwitchValueOn(contents) || contents.trim() === "") {
|
|
2121
|
+
fired.push(`sentinel file ${sentinel}`);
|
|
1509
2122
|
}
|
|
1510
2123
|
}
|
|
1511
|
-
return {
|
|
1512
|
-
}
|
|
1513
|
-
function isNoopSyncReason(reason) {
|
|
1514
|
-
if (!reason) return false;
|
|
1515
|
-
return /not set|No auth configured|Failed to obtain auth token/.test(reason);
|
|
2124
|
+
if (fired.length === 0) return { disabled: false, reason: null };
|
|
2125
|
+
return { disabled: true, reason: `memory sync DISABLED by ${fired.join(" + ")}` };
|
|
1516
2126
|
}
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
2127
|
+
var MEMORY_SYNC_DISABLE_ENV, MEMORY_SYNC_DISABLE_SENTINEL, NEGATIONS, MAX_LOGGED_VALUE;
|
|
2128
|
+
var init_sync_kill_switch = __esm({
|
|
2129
|
+
"src/tools/memory/sync-kill-switch.ts"() {
|
|
2130
|
+
"use strict";
|
|
2131
|
+
MEMORY_SYNC_DISABLE_ENV = "VO_MCP_DISABLE_MEMORY_SYNC";
|
|
2132
|
+
MEMORY_SYNC_DISABLE_SENTINEL = "vo-memory-sync-disabled";
|
|
2133
|
+
NEGATIONS = /* @__PURE__ */ new Set(["0", "false", "no"]);
|
|
2134
|
+
MAX_LOGGED_VALUE = 32;
|
|
2135
|
+
}
|
|
2136
|
+
});
|
|
2137
|
+
|
|
2138
|
+
// src/tools/memory/memory-knowledge-bridge.ts
|
|
2139
|
+
var memory_knowledge_bridge_exports = {};
|
|
2140
|
+
__export(memory_knowledge_bridge_exports, {
|
|
2141
|
+
extractMemoryTitle: () => extractMemoryTitle,
|
|
2142
|
+
upsertMemoryFilesAsKnowledge: () => upsertMemoryFilesAsKnowledge
|
|
2143
|
+
});
|
|
2144
|
+
import { existsSync as existsSync7, readdirSync as readdirSync5, readFileSync as readFileSync13 } from "node:fs";
|
|
2145
|
+
function extractMemoryTitle(fileName, content) {
|
|
2146
|
+
const frontmatter = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
2147
|
+
if (frontmatter) {
|
|
2148
|
+
const description23 = frontmatter[1].match(/^description:\s*(.+)$/m);
|
|
2149
|
+
if (description23 && description23[1].trim()) return description23[1].trim().slice(0, 200);
|
|
2150
|
+
}
|
|
2151
|
+
const heading = content.match(/^#\s+(.+)$/m);
|
|
2152
|
+
if (heading && heading[1].trim()) return heading[1].trim().slice(0, 200);
|
|
2153
|
+
return fileName;
|
|
2154
|
+
}
|
|
2155
|
+
async function upsertMemoryFilesAsKnowledge(options) {
|
|
2156
|
+
const { controlPlaneUrl, token, memoryDir, fetchFn, cache, deadline } = options;
|
|
2157
|
+
let files;
|
|
2158
|
+
try {
|
|
2159
|
+
if (!existsSync7(memoryDir)) {
|
|
2160
|
+
return { attempted: 0, upserted: 0, failed: 0, skipped: 0, failures: [] };
|
|
2161
|
+
}
|
|
2162
|
+
files = readdirSync5(memoryDir).filter(
|
|
2163
|
+
(f) => f.endsWith(".md") && f.toUpperCase() !== "MEMORY.MD"
|
|
2164
|
+
);
|
|
2165
|
+
} catch (err) {
|
|
2166
|
+
return {
|
|
2167
|
+
attempted: 0,
|
|
2168
|
+
upserted: 0,
|
|
2169
|
+
failed: 1,
|
|
2170
|
+
skipped: 0,
|
|
2171
|
+
failures: [`memory dir scan: ${err instanceof Error ? err.message : String(err)}`]
|
|
2172
|
+
};
|
|
2173
|
+
}
|
|
2174
|
+
const sweepDue = cache ? knowledgeSweepDue(cache) : true;
|
|
2175
|
+
const candidates = [];
|
|
2176
|
+
const failures = [];
|
|
2177
|
+
let skipped = 0;
|
|
2178
|
+
for (const fileName of files) {
|
|
2179
|
+
try {
|
|
2180
|
+
const content = readFileSync13(resolveMemoryFilePath(memoryDir, fileName), "utf8");
|
|
2181
|
+
if (content.length > CONTENT_HARD_LIMIT) {
|
|
2182
|
+
failures.push(`${fileName}: ${content.length} chars exceeds the ${CONTENT_HARD_LIMIT} server limit \u2014 split the memory file`);
|
|
2183
|
+
continue;
|
|
2184
|
+
}
|
|
2185
|
+
const hash = sha256(content);
|
|
2186
|
+
if (cache && !needsKnowledgePush(cache, fileName, hash, sweepDue)) {
|
|
2187
|
+
skipped += 1;
|
|
2188
|
+
continue;
|
|
2189
|
+
}
|
|
2190
|
+
candidates.push({ fileName, content, hash });
|
|
2191
|
+
} catch (err) {
|
|
2192
|
+
failures.push(`${fileName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
const url = `${controlPlaneUrl}/api/v1/knowledge/private`;
|
|
2196
|
+
const outcomes = await mapWithConcurrency(candidates, options.concurrency ?? PUSH_CONCURRENCY, async (candidate) => {
|
|
2197
|
+
deadline?.check();
|
|
2198
|
+
const base = {
|
|
2199
|
+
knowledge_class: "memory",
|
|
2200
|
+
source_path: `memory/${candidate.fileName}`,
|
|
2201
|
+
title: extractMemoryTitle(candidate.fileName, candidate.content),
|
|
2202
|
+
content: candidate.content
|
|
2203
|
+
};
|
|
2204
|
+
const post = (body) => withRequestTimeout(url, () => fetchFn(url, {
|
|
2205
|
+
method: "POST",
|
|
2206
|
+
headers: {
|
|
2207
|
+
authorization: `Bearer ${token}`,
|
|
2208
|
+
"content-type": "application/json"
|
|
2209
|
+
},
|
|
2210
|
+
body: JSON.stringify(body)
|
|
2211
|
+
}));
|
|
2212
|
+
let response = await post({
|
|
2213
|
+
...base,
|
|
2214
|
+
provenance: { written_by: "memory-bridge", source_kind: "operator_memory" }
|
|
2215
|
+
});
|
|
2216
|
+
if (response.status === 400) {
|
|
2217
|
+
response = await post(base);
|
|
2218
|
+
}
|
|
2219
|
+
if (response.status >= 200 && response.status < 300) return true;
|
|
2220
|
+
const text = await response.text();
|
|
2221
|
+
throw new Error(`HTTP ${response.status} ${text.slice(0, 80)}`);
|
|
2222
|
+
});
|
|
2223
|
+
let upserted = 0;
|
|
2224
|
+
for (let i = 0; i < outcomes.length; i++) {
|
|
2225
|
+
const outcome = outcomes[i];
|
|
2226
|
+
const candidate = candidates[i];
|
|
2227
|
+
if (outcome.ok) {
|
|
2228
|
+
upserted += 1;
|
|
2229
|
+
if (cache) recordKnowledgePush(cache, candidate.fileName, candidate.hash);
|
|
2230
|
+
} else {
|
|
2231
|
+
const error = outcome.error;
|
|
2232
|
+
failures.push(`${candidate.fileName}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
if (cache && sweepDue && failures.length === 0) {
|
|
2236
|
+
cache.knowledgeSweptAtMs = Date.now();
|
|
2237
|
+
}
|
|
2238
|
+
return {
|
|
2239
|
+
// Every memory file this run considered. `attempted === upserted + skipped
|
|
2240
|
+
// + failed` holds, so a caller can tell "nothing to do" from "nothing ran".
|
|
2241
|
+
attempted: files.length,
|
|
2242
|
+
upserted,
|
|
2243
|
+
failed: failures.length,
|
|
2244
|
+
skipped,
|
|
2245
|
+
failures: failures.slice(0, 5)
|
|
2246
|
+
};
|
|
2247
|
+
}
|
|
2248
|
+
var CONTENT_HARD_LIMIT;
|
|
2249
|
+
var init_memory_knowledge_bridge = __esm({
|
|
2250
|
+
"src/tools/memory/memory-knowledge-bridge.ts"() {
|
|
2251
|
+
"use strict";
|
|
2252
|
+
init_safe_memory_file();
|
|
2253
|
+
init_bounded_sync();
|
|
2254
|
+
init_memory_push_cache();
|
|
2255
|
+
CONTENT_HARD_LIMIT = 5e5;
|
|
2256
|
+
}
|
|
2257
|
+
});
|
|
2258
|
+
|
|
2259
|
+
// src/tools/memory/sync-config.ts
|
|
2260
|
+
var sync_config_exports = {};
|
|
2261
|
+
__export(sync_config_exports, {
|
|
2262
|
+
TOOL_NAME: () => TOOL_NAME22,
|
|
2263
|
+
deriveProjectSlug: () => deriveProjectSlug,
|
|
2264
|
+
description: () => description22,
|
|
2265
|
+
getMemoryDir: () => getMemoryDir,
|
|
2266
|
+
handleSyncConfig: () => handleSyncConfig,
|
|
2267
|
+
inputSchema: () => inputSchema22,
|
|
2268
|
+
isNoopSyncReason: () => isNoopSyncReason,
|
|
2269
|
+
runMemorySync: () => runMemorySync
|
|
2270
|
+
});
|
|
2271
|
+
import { existsSync as existsSync8 } from "node:fs";
|
|
2272
|
+
import { homedir as homedir7 } from "node:os";
|
|
2273
|
+
import { join as join11 } from "node:path";
|
|
2274
|
+
function isToolInput22(v) {
|
|
2275
|
+
if (typeof v !== "object" || v === null) return false;
|
|
2276
|
+
const o = v;
|
|
2277
|
+
if (o["action"] !== "pull" && o["action"] !== "push") return false;
|
|
2278
|
+
if (o["cwd"] !== void 0 && typeof o["cwd"] !== "string") return false;
|
|
2279
|
+
return true;
|
|
2280
|
+
}
|
|
2281
|
+
function deriveProjectSlug(cwd) {
|
|
2282
|
+
return cwd.replace(/([^:\\/])[\\/]+$/, "$1").replace(/\\/g, "/").replace(/^([a-zA-Z]):/, (_m, drive) => `${drive.toUpperCase()}:`).replace(/[^a-zA-Z0-9]/g, "-");
|
|
2283
|
+
}
|
|
2284
|
+
function getMemoryDir(cwd) {
|
|
2285
|
+
const slug = deriveProjectSlug(cwd);
|
|
2286
|
+
return join11(homedir7(), ".claude", "projects", slug, "memory");
|
|
2287
|
+
}
|
|
2288
|
+
function isNoopSyncReason(reason) {
|
|
2289
|
+
if (!reason) return false;
|
|
2290
|
+
return /not set|No auth configured|Failed to obtain auth token|DISABLED by/.test(reason);
|
|
2291
|
+
}
|
|
2292
|
+
async function runMemorySync(action, cwd, sessionId, fetchFn = globalThis.fetch, lockOptions = {}) {
|
|
2293
|
+
const killSwitch = evaluateMemorySyncKillSwitch();
|
|
2294
|
+
if (killSwitch.disabled) {
|
|
2295
|
+
return { synced: false, reason: killSwitch.reason };
|
|
2296
|
+
}
|
|
2297
|
+
const controlPlaneUrl = process.env["VO_CONTROL_PLANE_URL"];
|
|
2298
|
+
if (!controlPlaneUrl) {
|
|
2299
|
+
return { synced: false, reason: "VO_CONTROL_PLANE_URL not set \u2014 cloud mode disabled" };
|
|
2300
|
+
}
|
|
2301
|
+
const { createAuthTokenSourceFromEnv: createAuthTokenSourceFromEnv2 } = await Promise.resolve().then(() => (init_auth_token_source(), auth_token_source_exports));
|
|
2302
|
+
const { readStoredCredential: readStoredCredential2 } = await Promise.resolve().then(() => (init_credential_store(), credential_store_exports));
|
|
2303
|
+
const tokenSource = createAuthTokenSourceFromEnv2(process.env, fetchFn, () => readStoredCredential2(process.env));
|
|
2304
|
+
if (!tokenSource) {
|
|
2305
|
+
return { synced: false, reason: "No auth configured. Run `vo-mcp login` to authenticate as an operator." };
|
|
1527
2306
|
}
|
|
1528
2307
|
const token = await tokenSource.getToken();
|
|
1529
2308
|
if (!token) {
|
|
@@ -1531,20 +2310,79 @@ async function runMemorySync(action, cwd, sessionId, fetchFn = globalThis.fetch)
|
|
|
1531
2310
|
}
|
|
1532
2311
|
const memoryDir = getMemoryDir(cwd);
|
|
1533
2312
|
const baseUrl = controlPlaneUrl.replace(/\/+$/, "");
|
|
1534
|
-
|
|
1535
|
-
if (action === "pull") {
|
|
1536
|
-
const result2 = await pullMemory(baseUrl, token, memoryDir, fetchFn);
|
|
1537
|
-
return { synced: true, action: "pull", pulled: result2.pulled, files: result2.files, memory_dir: memoryDir };
|
|
1538
|
-
}
|
|
1539
|
-
const result = await pushMemory(baseUrl, token, memoryDir, sessionId, fetchFn);
|
|
2313
|
+
if (action === "push" && !existsSync8(memoryDir)) {
|
|
1540
2314
|
return {
|
|
1541
2315
|
synced: true,
|
|
1542
2316
|
action: "push",
|
|
1543
|
-
pushed:
|
|
1544
|
-
created:
|
|
1545
|
-
updated:
|
|
2317
|
+
pushed: 0,
|
|
2318
|
+
created: 0,
|
|
2319
|
+
updated: 0,
|
|
2320
|
+
skipped: 0,
|
|
2321
|
+
index_rows_preserved: 0,
|
|
2322
|
+
knowledge_upserted: 0,
|
|
2323
|
+
knowledge_failed: 0,
|
|
1546
2324
|
memory_dir: memoryDir
|
|
1547
2325
|
};
|
|
2326
|
+
}
|
|
2327
|
+
try {
|
|
2328
|
+
return await withMemorySyncLock({ ...lockOptions, memoryDir, sessionId, createDir: action === "pull" }, async (lock) => {
|
|
2329
|
+
const takeover = lock.tookOverFrom ? { lock_taken_over_from_pid: lock.tookOverFrom.pid } : {};
|
|
2330
|
+
if (action === "pull") {
|
|
2331
|
+
const result2 = await pullMemory(baseUrl, token, memoryDir, fetchFn);
|
|
2332
|
+
return {
|
|
2333
|
+
synced: true,
|
|
2334
|
+
action: "pull",
|
|
2335
|
+
pulled: result2.pulled,
|
|
2336
|
+
files: result2.files,
|
|
2337
|
+
memory_dir: memoryDir,
|
|
2338
|
+
...takeover
|
|
2339
|
+
};
|
|
2340
|
+
}
|
|
2341
|
+
const deadline = createSyncDeadline();
|
|
2342
|
+
const cache = readPushCache(memoryDir, baseUrl);
|
|
2343
|
+
let result;
|
|
2344
|
+
try {
|
|
2345
|
+
result = await pushMemory(baseUrl, token, memoryDir, sessionId, fetchFn, { cache, deadline });
|
|
2346
|
+
} catch (err) {
|
|
2347
|
+
writePushCache(memoryDir, cache);
|
|
2348
|
+
throw err;
|
|
2349
|
+
}
|
|
2350
|
+
let bridge;
|
|
2351
|
+
try {
|
|
2352
|
+
const { upsertMemoryFilesAsKnowledge: upsertMemoryFilesAsKnowledge2 } = await Promise.resolve().then(() => (init_memory_knowledge_bridge(), memory_knowledge_bridge_exports));
|
|
2353
|
+
bridge = await upsertMemoryFilesAsKnowledge2({
|
|
2354
|
+
controlPlaneUrl: baseUrl,
|
|
2355
|
+
token,
|
|
2356
|
+
memoryDir,
|
|
2357
|
+
fetchFn,
|
|
2358
|
+
cache,
|
|
2359
|
+
deadline
|
|
2360
|
+
});
|
|
2361
|
+
} catch (err) {
|
|
2362
|
+
bridge = {
|
|
2363
|
+
upserted: 0,
|
|
2364
|
+
failed: 1,
|
|
2365
|
+
skipped: 0,
|
|
2366
|
+
failures: [`bridge unavailable: ${err instanceof Error ? err.message : String(err)}`]
|
|
2367
|
+
};
|
|
2368
|
+
}
|
|
2369
|
+
writePushCache(memoryDir, cache);
|
|
2370
|
+
return {
|
|
2371
|
+
synced: true,
|
|
2372
|
+
action: "push",
|
|
2373
|
+
pushed: result.pushed,
|
|
2374
|
+
created: result.created,
|
|
2375
|
+
updated: result.updated,
|
|
2376
|
+
skipped: result.skipped,
|
|
2377
|
+
index_rows_preserved: result.indexRowsPreserved,
|
|
2378
|
+
memory_dir: memoryDir,
|
|
2379
|
+
knowledge_upserted: bridge.upserted,
|
|
2380
|
+
knowledge_failed: bridge.failed,
|
|
2381
|
+
knowledge_skipped: bridge.skipped,
|
|
2382
|
+
...bridge.failed > 0 ? { knowledge_failures: bridge.failures } : {},
|
|
2383
|
+
...takeover
|
|
2384
|
+
};
|
|
2385
|
+
});
|
|
1548
2386
|
} catch (err) {
|
|
1549
2387
|
const message = err instanceof Error ? err.message : String(err);
|
|
1550
2388
|
return { synced: false, reason: `Sync failed: ${message}` };
|
|
@@ -1566,6 +2404,11 @@ var init_sync_config = __esm({
|
|
|
1566
2404
|
"src/tools/memory/sync-config.ts"() {
|
|
1567
2405
|
"use strict";
|
|
1568
2406
|
init_common();
|
|
2407
|
+
init_memory_sync_http();
|
|
2408
|
+
init_bounded_sync();
|
|
2409
|
+
init_memory_push_cache();
|
|
2410
|
+
init_sync_lock();
|
|
2411
|
+
init_sync_kill_switch();
|
|
1569
2412
|
TOOL_NAME22 = "vo_sync_config";
|
|
1570
2413
|
inputSchema22 = {
|
|
1571
2414
|
type: "object",
|
|
@@ -1583,19 +2426,19 @@ var init_sync_config = __esm({
|
|
|
1583
2426
|
required: ["action"],
|
|
1584
2427
|
additionalProperties: false
|
|
1585
2428
|
};
|
|
1586
|
-
description22 = "Syncs memory entries between local ~/.claude/projects/<slug>/memory/ and cloud control-plane /api/v1/agent-config/memory/me. Requires operator auth (vo-mcp login). Actions: pull (cloud\u2192local), push (local\u2192cloud). Idempotent; push creates/updates as needed.";
|
|
2429
|
+
description22 = "Syncs memory entries between local ~/.claude/projects/<slug>/memory/ and cloud control-plane /api/v1/agent-config/memory/me. Requires operator auth (vo-mcp login). Actions: pull (cloud\u2192local), push (local\u2192cloud). Idempotent; push creates/updates as needed. Serialized across concurrent sessions by an exclusive lock in the memory dir.";
|
|
1587
2430
|
}
|
|
1588
2431
|
});
|
|
1589
2432
|
|
|
1590
2433
|
// src/cli.ts
|
|
1591
|
-
import { homedir as
|
|
1592
|
-
import { randomUUID as
|
|
1593
|
-
import { join as
|
|
2434
|
+
import { homedir as homedir8, hostname as hostname2 } from "node:os";
|
|
2435
|
+
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
2436
|
+
import { join as join14 } from "node:path";
|
|
1594
2437
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
1595
2438
|
|
|
1596
2439
|
// src/server.ts
|
|
1597
2440
|
init_common();
|
|
1598
|
-
import { randomUUID as
|
|
2441
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
1599
2442
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
1600
2443
|
import {
|
|
1601
2444
|
CallToolRequestSchema,
|
|
@@ -1971,7 +2814,8 @@ async function handleCheckHollowTest(deps, rawInput, signal) {
|
|
|
1971
2814
|
synthesized_verdict: synthForEvent,
|
|
1972
2815
|
consensus_confidence: engineResult.synthesized_verdict.confidence,
|
|
1973
2816
|
duration_ms: engineResult.duration_ms,
|
|
1974
|
-
consensus_engine_version: engineResult.engine_version
|
|
2817
|
+
consensus_engine_version: engineResult.engine_version,
|
|
2818
|
+
...aggregateEventTokenUsage(engineResult.per_model_verdicts, engineResult.token_usage)
|
|
1975
2819
|
};
|
|
1976
2820
|
const payload = {
|
|
1977
2821
|
verdict: engineResult.synthesized_verdict.verdict,
|
|
@@ -2154,7 +2998,8 @@ async function handleVerifyAnswer(deps, rawInput, signal) {
|
|
|
2154
2998
|
synthesized_verdict: synthForEvent,
|
|
2155
2999
|
consensus_confidence: engineResult.synthesized_verdict.confidence,
|
|
2156
3000
|
duration_ms: engineResult.duration_ms,
|
|
2157
|
-
consensus_engine_version: engineResult.engine_version
|
|
3001
|
+
consensus_engine_version: engineResult.engine_version,
|
|
3002
|
+
...aggregateEventTokenUsage(engineResult.per_model_verdicts, engineResult.token_usage)
|
|
2158
3003
|
};
|
|
2159
3004
|
const payload = {
|
|
2160
3005
|
verdict: engineResult.synthesized_verdict.verdict,
|
|
@@ -2163,6 +3008,9 @@ async function handleVerifyAnswer(deps, rawInput, signal) {
|
|
|
2163
3008
|
synthesized_verdict: synthForEvent,
|
|
2164
3009
|
engine_version: engineResult.engine_version,
|
|
2165
3010
|
degraded: engineResult.degraded,
|
|
3011
|
+
...engineResult.quorum_failed === true ? { quorum_failed: true } : {},
|
|
3012
|
+
// The verification receipt (moat decision_id) — the thing an agent pastes as `receipt id: <uuid>`.
|
|
3013
|
+
...engineResult.receipt_id ? { receipt_id: engineResult.receipt_id } : {},
|
|
2166
3014
|
gate_type: gateType,
|
|
2167
3015
|
...kbResult.error !== null ? { kb_unavailable: true } : {},
|
|
2168
3016
|
...kbTruncated > 0 ? { kb_rules_truncated: kbTruncated } : {}
|
|
@@ -2419,7 +3267,8 @@ async function handleConsensusJudgment(deps, rawInput, signal) {
|
|
|
2419
3267
|
duration_ms: engineResult.duration_ms,
|
|
2420
3268
|
consensus_engine_version: engineResult.engine_version,
|
|
2421
3269
|
per_model_verdicts: perModelForEvent,
|
|
2422
|
-
synthesized_verdict: synthForEvent
|
|
3270
|
+
synthesized_verdict: synthForEvent,
|
|
3271
|
+
...aggregateEventTokenUsage(engineResult.per_model_verdicts, engineResult.token_usage)
|
|
2423
3272
|
};
|
|
2424
3273
|
const payload = {
|
|
2425
3274
|
verdict: engineResult.synthesized_verdict.verdict,
|
|
@@ -2428,6 +3277,11 @@ async function handleConsensusJudgment(deps, rawInput, signal) {
|
|
|
2428
3277
|
synthesized_verdict: synthForEvent,
|
|
2429
3278
|
engine_version: engineResult.engine_version,
|
|
2430
3279
|
degraded: engineResult.degraded,
|
|
3280
|
+
...engineResult.quorum_failed === true ? { quorum_failed: true } : {},
|
|
3281
|
+
// The verification receipt (moat decision_id) — the thing an agent pastes as `receipt id: <uuid>`.
|
|
3282
|
+
// NOTE: a content-hash cache hit replays the ORIGINAL call's receipt_id (same claim, same verdict, no new spend) —
|
|
3283
|
+
// a receipt asserts the stage ran for this claim, not one-receipt-per-call.
|
|
3284
|
+
...engineResult.receipt_id ? { receipt_id: engineResult.receipt_id } : {},
|
|
2431
3285
|
gate_type: gateType,
|
|
2432
3286
|
// ─── Consensus-engine feature outputs (additive; 2026-06-13) ─────────────
|
|
2433
3287
|
// Feature 2 (calibrated-confidence) — ON by default; the engine attaches
|
|
@@ -2446,7 +3300,11 @@ async function handleConsensusJudgment(deps, rawInput, signal) {
|
|
|
2446
3300
|
...engineResult.low_confidence_sources !== void 0 ? { low_confidence_sources: engineResult.low_confidence_sources } : {},
|
|
2447
3301
|
// Escalation (from citation grade or human-tiebreak synthesizer).
|
|
2448
3302
|
...engineResult.escalation_required !== void 0 ? { escalation_required: engineResult.escalation_required } : {},
|
|
2449
|
-
...engineResult.escalation_reason !== void 0 ? { escalation_reason: engineResult.escalation_reason } : {}
|
|
3303
|
+
...engineResult.escalation_reason !== void 0 ? { escalation_reason: engineResult.escalation_reason } : {},
|
|
3304
|
+
// Critique-uptake (2026-07-20 red-team fix) — the engine computes this
|
|
3305
|
+
// on every call; this spread closes the gap where the visibility report
|
|
3306
|
+
// was itself silently dropped at the payload boundary.
|
|
3307
|
+
...engineResult.critique_uptake !== void 0 ? { critique_uptake: engineResult.critique_uptake } : {}
|
|
2450
3308
|
};
|
|
2451
3309
|
const envelope = {
|
|
2452
3310
|
tool: TOOL_NAME4,
|
|
@@ -2624,7 +3482,8 @@ async function handleArchitectureReview(deps, rawInput, signal) {
|
|
|
2624
3482
|
synthesized_verdict: synthForEvent,
|
|
2625
3483
|
consensus_confidence: engineResult.synthesized_verdict.confidence,
|
|
2626
3484
|
duration_ms: engineResult.duration_ms,
|
|
2627
|
-
consensus_engine_version: engineResult.engine_version
|
|
3485
|
+
consensus_engine_version: engineResult.engine_version,
|
|
3486
|
+
...aggregateEventTokenUsage(engineResult.per_model_verdicts, engineResult.token_usage)
|
|
2628
3487
|
};
|
|
2629
3488
|
const escalationRequired = engineResult.escalation_required === true || engineResult.escalation_required === void 0 && engineResult.synthesized_verdict.dissent_summary !== null;
|
|
2630
3489
|
const escalationReason = engineResult.escalation_reason ?? engineResult.synthesized_verdict.dissent_summary ?? "";
|
|
@@ -4111,7 +4970,8 @@ Produce the JSON dispatch plan now.`;
|
|
|
4111
4970
|
duration_ms: engineResult.duration_ms,
|
|
4112
4971
|
consensus_engine_version: engineResult.engine_version,
|
|
4113
4972
|
per_model_verdicts: toEventPerModelVerdicts(engineResult.per_model_verdicts),
|
|
4114
|
-
synthesized_verdict: toEventSynthesizedVerdict(engineResult.synthesized_verdict)
|
|
4973
|
+
synthesized_verdict: toEventSynthesizedVerdict(engineResult.synthesized_verdict),
|
|
4974
|
+
...aggregateEventTokenUsage(engineResult.per_model_verdicts, engineResult.token_usage)
|
|
4115
4975
|
};
|
|
4116
4976
|
deps.events.append(enrichedEvent);
|
|
4117
4977
|
return jsonContent(envelope);
|
|
@@ -4324,7 +5184,7 @@ async function buildCloudOrStubResponse(args) {
|
|
|
4324
5184
|
|
|
4325
5185
|
// src/tools/heal/common-heal.ts
|
|
4326
5186
|
init_common();
|
|
4327
|
-
var HEAL_STUB_REASON =
|
|
5187
|
+
var HEAL_STUB_REASON = "cloud mode is not configured on this session, so the admin callable was not reached. The wiring exists (see buildCloudOrStubResponse); sign in with `vo-mcp login` to route this tool to the control plane. Note that /api/v1/admin/* additionally requires a founding-operator credential.";
|
|
4328
5188
|
var HEAL_GATE_TYPE = "admin-action";
|
|
4329
5189
|
|
|
4330
5190
|
// src/tools/heal/trigger-heal.ts
|
|
@@ -4345,7 +5205,7 @@ var inputSchema8 = {
|
|
|
4345
5205
|
},
|
|
4346
5206
|
additionalProperties: false
|
|
4347
5207
|
};
|
|
4348
|
-
var description8 = "Triggers a self-heal pass against open PRs. Optionally scope to a `focus_page` (priority queue for one tester) or omit to fire the auto-process queue. Wraps the `voTriggerHeal` admin Cloud Function.
|
|
5208
|
+
var description8 = "Triggers a self-heal pass against open PRs. Optionally scope to a `focus_page` (priority queue for one tester) or omit to fire the auto-process queue. Wraps the `voTriggerHeal` admin Cloud Function. Requires a founding-operator credential: the control plane's scoped-operator gate refuses `/api/v1/admin/*` for tenant-scoped accounts, which receive HTTP 403 `scoped_operator_forbidden`. Falls back to a clearly-marked `unimplemented` envelope with structured normalized_input when cloud mode is not configured.";
|
|
4349
5209
|
function isToolInput8(v) {
|
|
4350
5210
|
if (typeof v !== "object" || v === null) return false;
|
|
4351
5211
|
const o = v;
|
|
@@ -4403,7 +5263,7 @@ var inputSchema9 = {
|
|
|
4403
5263
|
},
|
|
4404
5264
|
additionalProperties: false
|
|
4405
5265
|
};
|
|
4406
|
-
var description9 = "Retries one or more failed fix attempts by id. Pass `attempt_id` for the single case or `attempt_ids` (up to 50) for the batch case. Wraps `voRetryFixAttempt` / `voRetryFixAttempts` admin Cloud Functions.
|
|
5266
|
+
var description9 = "Retries one or more failed fix attempts by id. Pass `attempt_id` for the single case or `attempt_ids` (up to 50) for the batch case. Wraps `voRetryFixAttempt` / `voRetryFixAttempts` admin Cloud Functions. Requires a founding-operator credential: the control plane's scoped-operator gate refuses `/api/v1/admin/*` for tenant-scoped accounts, which receive HTTP 403 `scoped_operator_forbidden`. Falls back to a clearly-marked `unimplemented` envelope when cloud mode is not configured.";
|
|
4407
5267
|
function isToolInput9(v) {
|
|
4408
5268
|
if (typeof v !== "object" || v === null) return false;
|
|
4409
5269
|
const o = v;
|
|
@@ -4491,7 +5351,7 @@ var inputSchema10 = {
|
|
|
4491
5351
|
required: ["attempt_id"],
|
|
4492
5352
|
additionalProperties: false
|
|
4493
5353
|
};
|
|
4494
|
-
var description10 = "Clears (cancels) a single fix attempt by id. Wraps `voClearFixAttempt` admin Cloud Function.
|
|
5354
|
+
var description10 = "Clears (cancels) a single fix attempt by id. Wraps `voClearFixAttempt` admin Cloud Function. Requires a founding-operator credential: the control plane's scoped-operator gate refuses `/api/v1/admin/*` for tenant-scoped accounts, which receive HTTP 403 `scoped_operator_forbidden`. Falls back to a clearly-marked `unimplemented` envelope when cloud mode is not configured.";
|
|
4495
5355
|
function isToolInput10(v) {
|
|
4496
5356
|
if (typeof v !== "object" || v === null) return false;
|
|
4497
5357
|
const o = v;
|
|
@@ -4539,7 +5399,7 @@ var inputSchema11 = {
|
|
|
4539
5399
|
required: ["run_id"],
|
|
4540
5400
|
additionalProperties: false
|
|
4541
5401
|
};
|
|
4542
|
-
var description11 = "Cancels a running GitHub Actions workflow by run id. Wraps `voStopWorkflow` admin Cloud Function.
|
|
5402
|
+
var description11 = "Cancels a running GitHub Actions workflow by run id. Wraps `voStopWorkflow` admin Cloud Function. Requires a founding-operator credential: the control plane's scoped-operator gate refuses `/api/v1/admin/*` for tenant-scoped accounts, which receive HTTP 403 `scoped_operator_forbidden`. Falls back to a clearly-marked `unimplemented` envelope when cloud mode is not configured.";
|
|
4543
5403
|
function isToolInput11(v) {
|
|
4544
5404
|
if (typeof v !== "object" || v === null) return false;
|
|
4545
5405
|
const o = v;
|
|
@@ -4585,7 +5445,7 @@ var inputSchema12 = {
|
|
|
4585
5445
|
properties: {},
|
|
4586
5446
|
additionalProperties: false
|
|
4587
5447
|
};
|
|
4588
|
-
var description12 = "Returns the current Command Center workflow-runs snapshot (Heal, Manager, Auto-Merge, Deploy on Merge, etc.). Wraps `voGetWorkflowRuns` admin Cloud Function. Read-only diagnostic.
|
|
5448
|
+
var description12 = "Returns the current Command Center workflow-runs snapshot (Heal, Manager, Auto-Merge, Deploy on Merge, etc.). Wraps `voGetWorkflowRuns` admin Cloud Function. Read-only diagnostic. Requires a founding-operator credential: the control plane's scoped-operator gate refuses `/api/v1/admin/*` for tenant-scoped accounts, which receive HTTP 403 `scoped_operator_forbidden`. Falls back to a clearly-marked `unimplemented` envelope when cloud mode is not configured.";
|
|
4589
5449
|
function isToolInput12(v) {
|
|
4590
5450
|
if (typeof v !== "object" || v === null) return false;
|
|
4591
5451
|
return true;
|
|
@@ -4612,7 +5472,7 @@ init_common();
|
|
|
4612
5472
|
|
|
4613
5473
|
// src/tools/pr/common-pr.ts
|
|
4614
5474
|
init_common();
|
|
4615
|
-
var PR_STUB_REASON =
|
|
5475
|
+
var PR_STUB_REASON = "cloud mode is not configured on this session, so the admin callable was not reached. The wiring exists (see buildCloudOrStubResponse); sign in with `vo-mcp login` to route this tool to the control plane. Note that /api/v1/admin/* additionally requires a founding-operator credential.";
|
|
4616
5476
|
var PR_GATE_TYPE = "admin-action";
|
|
4617
5477
|
|
|
4618
5478
|
// src/tools/pr/list-pending-prs.ts
|
|
@@ -4624,7 +5484,7 @@ var inputSchema13 = {
|
|
|
4624
5484
|
properties: {},
|
|
4625
5485
|
additionalProperties: false
|
|
4626
5486
|
};
|
|
4627
|
-
var description13 = "Lists open
|
|
5487
|
+
var description13 = "Lists open AlgoHQ-source pull requests with blocker / source / tester / specialist-context metadata. Read-only diagnostic for Command Center reads. Wraps `voListPendingPRs` admin Cloud Function. Requires a founding-operator credential: the control plane's scoped-operator gate refuses `/api/v1/admin/*` for tenant-scoped accounts, which receive HTTP 403 `scoped_operator_forbidden`. Falls back to a clearly-marked `unimplemented` envelope when cloud mode is not configured.";
|
|
4628
5488
|
function isToolInput13(v) {
|
|
4629
5489
|
return typeof v === "object" && v !== null;
|
|
4630
5490
|
}
|
|
@@ -4661,7 +5521,7 @@ var inputSchema14 = {
|
|
|
4661
5521
|
required: ["pr_number"],
|
|
4662
5522
|
additionalProperties: false
|
|
4663
5523
|
};
|
|
4664
|
-
var description14 = "Approves + merges a single
|
|
5524
|
+
var description14 = "Approves + merges a single AlgoHQ-source pull request by number. Wraps `voMergePR` admin Cloud Function (server-side refuses non-AlgoHQ PRs with permission-denied). Requires a founding-operator credential: the control plane's scoped-operator gate refuses `/api/v1/admin/*` for tenant-scoped accounts, which receive HTTP 403 `scoped_operator_forbidden`. Falls back to a clearly-marked `unimplemented` envelope when cloud mode is not configured.";
|
|
4665
5525
|
function isToolInput14(v) {
|
|
4666
5526
|
if (typeof v !== "object" || v === null) return false;
|
|
4667
5527
|
const o = v;
|
|
@@ -4706,7 +5566,7 @@ var inputSchema15 = {
|
|
|
4706
5566
|
required: ["pr_number"],
|
|
4707
5567
|
additionalProperties: false
|
|
4708
5568
|
};
|
|
4709
|
-
var description15 = "Closes a pull request without merging. No retry dispatched \u2014 use `vo_reject_and_retry` for close+retry. Wraps `voRejectPR` admin Cloud Function.
|
|
5569
|
+
var description15 = "Closes a pull request without merging. No retry dispatched \u2014 use `vo_reject_and_retry` for close+retry. Wraps `voRejectPR` admin Cloud Function. Requires a founding-operator credential: the control plane's scoped-operator gate refuses `/api/v1/admin/*` for tenant-scoped accounts, which receive HTTP 403 `scoped_operator_forbidden`. Falls back to a clearly-marked `unimplemented` envelope when cloud mode is not configured.";
|
|
4710
5570
|
function isToolInput15(v) {
|
|
4711
5571
|
if (typeof v !== "object" || v === null) return false;
|
|
4712
5572
|
const o = v;
|
|
@@ -4745,7 +5605,7 @@ var inputSchema16 = {
|
|
|
4745
5605
|
properties: {},
|
|
4746
5606
|
additionalProperties: false
|
|
4747
5607
|
};
|
|
4748
|
-
var description16 = "Iterates all open
|
|
5608
|
+
var description16 = "Iterates all open AlgoHQ-source pull requests and merges (or arms auto-merge) on each. Returns counts of merged / accepted / total plus per-PR results. Wraps `voApproveAllFixes` admin Cloud Function. Requires a founding-operator credential: the control plane's scoped-operator gate refuses `/api/v1/admin/*` for tenant-scoped accounts, which receive HTTP 403 `scoped_operator_forbidden`. Falls back to a clearly-marked `unimplemented` envelope when cloud mode is not configured.";
|
|
4749
5609
|
function isToolInput16(v) {
|
|
4750
5610
|
return typeof v === "object" && v !== null;
|
|
4751
5611
|
}
|
|
@@ -4780,7 +5640,7 @@ var inputSchema17 = {
|
|
|
4780
5640
|
required: ["pr_number"],
|
|
4781
5641
|
additionalProperties: false
|
|
4782
5642
|
};
|
|
4783
|
-
var description17 = "Closes
|
|
5643
|
+
var description17 = "Closes an AlgoHQ pull request and dispatches a self-heal pass to retry the same focus page. Cloud callable refuses non-AlgoHQ PRs and respects the self-heal kill switch + per-PR retry block. Wraps `voRejectAndRetry` admin Cloud Function. Requires a founding-operator credential: the control plane's scoped-operator gate refuses `/api/v1/admin/*` for tenant-scoped accounts, which receive HTTP 403 `scoped_operator_forbidden`. Falls back to a clearly-marked `unimplemented` envelope when cloud mode is not configured.";
|
|
4784
5644
|
function isToolInput17(v) {
|
|
4785
5645
|
if (typeof v !== "object" || v === null) return false;
|
|
4786
5646
|
const o = v;
|
|
@@ -4857,7 +5717,7 @@ function buildPrompt4(pr, notes) {
|
|
|
4857
5717
|
const lines = [
|
|
4858
5718
|
"You are a release gatekeeper deciding whether a pull request is safe to MERGE.",
|
|
4859
5719
|
"Recommend exactly one of: merge / hold / reject. Be conservative \u2014 this is a high-stakes irreversible action.",
|
|
4860
|
-
"Rules: HOLD if CI is failing/blocked, there is a merge conflict, or the change is a draft. REJECT if the PR is not a legitimate
|
|
5720
|
+
"Rules: HOLD if CI is failing/blocked, there is a merge conflict, or the change is a draft. REJECT if the PR is not a legitimate AlgoHQ-source change or has no clear purpose. MERGE only if it looks complete, scoped, and unblocked.",
|
|
4861
5721
|
"",
|
|
4862
5722
|
`PR #${pr.number}: ${pr.title}`,
|
|
4863
5723
|
`Source: ${pr.source ?? "unknown"}`,
|
|
@@ -4925,7 +5785,7 @@ async function handleReviewMerge(deps, rawInput, signal) {
|
|
|
4925
5785
|
}
|
|
4926
5786
|
if (pr === null) {
|
|
4927
5787
|
return emit(
|
|
4928
|
-
emptyPayload("hold", `PR #${prNumber} is not among open
|
|
5788
|
+
emptyPayload("hold", `PR #${prNumber} is not among open AlgoHQ PRs (already merged/closed, or not an AlgoHQ-source PR).`, null)
|
|
4929
5789
|
);
|
|
4930
5790
|
}
|
|
4931
5791
|
const hasBlocker = pr.blocker !== null && pr.blocker !== "none";
|
|
@@ -4985,7 +5845,8 @@ async function handleReviewMerge(deps, rawInput, signal) {
|
|
|
4985
5845
|
duration_ms: result.duration_ms,
|
|
4986
5846
|
consensus_engine_version: result.engine_version,
|
|
4987
5847
|
per_model_verdicts: perModel,
|
|
4988
|
-
synthesized_verdict: synth
|
|
5848
|
+
synthesized_verdict: synth,
|
|
5849
|
+
...aggregateEventTokenUsage(result.per_model_verdicts, result.token_usage)
|
|
4989
5850
|
});
|
|
4990
5851
|
}
|
|
4991
5852
|
|
|
@@ -5023,6 +5884,8 @@ function suggestedHandoffPath(session_id, isoTimestamp) {
|
|
|
5023
5884
|
}
|
|
5024
5885
|
|
|
5025
5886
|
// src/tools/session/report-session-state.ts
|
|
5887
|
+
init_auth_token_source();
|
|
5888
|
+
init_credential_store();
|
|
5026
5889
|
var TOOL_NAME19 = "vo_report_session_state";
|
|
5027
5890
|
var VALID_AGENT_TYPES = ["claude-code", "codex", "cursor", "continue"];
|
|
5028
5891
|
var MAX_GOAL_CHARS = 500;
|
|
@@ -5073,7 +5936,7 @@ var inputSchema19 = {
|
|
|
5073
5936
|
required: ["operator_id", "session_id", "agent_type", "context_used_pct"],
|
|
5074
5937
|
additionalProperties: false
|
|
5075
5938
|
};
|
|
5076
|
-
var description19 = "Reports per-session context-window utilization to
|
|
5939
|
+
var description19 = "Reports per-session context-window utilization to AlgoHQ and returns a directive: 'continue' (under 70%), 'prepare_handoff' (70-84%), or 'execute_handoff_now' (\u226585%). Implements V1 launch gate #9 (fleet context lifecycle management) per the official AlgoHQ roadmap. Cloud-control-plane mode when VO_CONTROL_PLANE_URL plus a user/scoped HQ credential (or legacy admin token) is available; auto-allocates the session on first report so interactive agents (Claude Code, Cursor, Codex, Continue) appear on the live fleet whiteboard. Stub-local fallback when cloud config is absent or fails. The response shape stays stable across modes (`backend_mode` field in the payload tells the caller which mode produced the verdict).";
|
|
5077
5940
|
function isStringArray2(v, maxItems) {
|
|
5078
5941
|
if (!Array.isArray(v)) return false;
|
|
5079
5942
|
if (v.length > maxItems) return false;
|
|
@@ -5098,14 +5961,39 @@ function isToolInput19(v) {
|
|
|
5098
5961
|
}
|
|
5099
5962
|
return true;
|
|
5100
5963
|
}
|
|
5101
|
-
function
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5964
|
+
async function fetchCloudIdentity(url, token, fetchFn) {
|
|
5965
|
+
try {
|
|
5966
|
+
const response = await fetchFn(`${url}/api/v1/auth/me`, {
|
|
5967
|
+
method: "GET",
|
|
5968
|
+
headers: {
|
|
5969
|
+
"Authorization": `Bearer ${token}`
|
|
5970
|
+
}
|
|
5971
|
+
});
|
|
5972
|
+
if (!response.ok) return null;
|
|
5973
|
+
const data = await response.json();
|
|
5974
|
+
if (!data.ok || !data.provisioned || !data.operator_id || !data.tenant_id) return null;
|
|
5975
|
+
return { operator_id: data.operator_id, tenant_id: data.tenant_id };
|
|
5976
|
+
} catch {
|
|
5977
|
+
return null;
|
|
5978
|
+
}
|
|
5979
|
+
}
|
|
5980
|
+
async function getCloudConfig(fetchFn = fetch) {
|
|
5981
|
+
const url = process.env["VO_CONTROL_PLANE_URL"]?.trim();
|
|
5982
|
+
if (!url) return null;
|
|
5983
|
+
const tokenSource = createAuthTokenSourceFromEnv(
|
|
5984
|
+
process.env,
|
|
5985
|
+
fetchFn,
|
|
5986
|
+
() => readStoredCredential(process.env)
|
|
5987
|
+
);
|
|
5988
|
+
const token = await tokenSource?.getToken();
|
|
5989
|
+
if (!token) return null;
|
|
5990
|
+
const tenant_id = process.env["VO_TENANT_ID"]?.trim();
|
|
5991
|
+
if (tenant_id) return { url, token, tenant_id };
|
|
5992
|
+
const identity = await fetchCloudIdentity(url, token, fetchFn);
|
|
5993
|
+
if (!identity) return null;
|
|
5994
|
+
return { url, token, tenant_id: identity.tenant_id, operator_id: identity.operator_id };
|
|
5107
5995
|
}
|
|
5108
|
-
async function tryCloudReportState(cloud, input) {
|
|
5996
|
+
async function tryCloudReportState(cloud, input, fetchFn = fetch) {
|
|
5109
5997
|
try {
|
|
5110
5998
|
const reportBody = {
|
|
5111
5999
|
context_used_pct: input.context_used_pct
|
|
@@ -5118,7 +6006,7 @@ async function tryCloudReportState(cloud, input) {
|
|
|
5118
6006
|
reportBody["recent_tool_uses"] = input.recent_tool_uses;
|
|
5119
6007
|
}
|
|
5120
6008
|
const reportUrl = `${cloud.url}/api/v1/session/${input.session_id}/report-state`;
|
|
5121
|
-
let response = await
|
|
6009
|
+
let response = await fetchFn(reportUrl, {
|
|
5122
6010
|
method: "POST",
|
|
5123
6011
|
headers: {
|
|
5124
6012
|
"Content-Type": "application/json",
|
|
@@ -5128,7 +6016,7 @@ async function tryCloudReportState(cloud, input) {
|
|
|
5128
6016
|
});
|
|
5129
6017
|
if (response.status === 404) {
|
|
5130
6018
|
const allocateBody = {
|
|
5131
|
-
operator_id: input.operator_id,
|
|
6019
|
+
operator_id: cloud.operator_id ?? input.operator_id,
|
|
5132
6020
|
tenant_id: cloud.tenant_id,
|
|
5133
6021
|
agent_type: input.agent_type,
|
|
5134
6022
|
current_goal: input.current_goal ?? "Interactive session"
|
|
@@ -5137,7 +6025,7 @@ async function tryCloudReportState(cloud, input) {
|
|
|
5137
6025
|
allocateBody["initial_context_used_pct"] = input.context_used_pct;
|
|
5138
6026
|
}
|
|
5139
6027
|
const allocateUrl = `${cloud.url}/api/v1/session`;
|
|
5140
|
-
const allocateResponse = await
|
|
6028
|
+
const allocateResponse = await fetchFn(allocateUrl, {
|
|
5141
6029
|
method: "POST",
|
|
5142
6030
|
headers: {
|
|
5143
6031
|
"Content-Type": "application/json",
|
|
@@ -5148,7 +6036,10 @@ async function tryCloudReportState(cloud, input) {
|
|
|
5148
6036
|
if (!allocateResponse.ok) {
|
|
5149
6037
|
return null;
|
|
5150
6038
|
}
|
|
5151
|
-
|
|
6039
|
+
const allocateData = await allocateResponse.json();
|
|
6040
|
+
const retrySessionId = typeof allocateData.session?.session_id === "string" && allocateData.session.session_id.length > 0 ? allocateData.session.session_id : input.session_id;
|
|
6041
|
+
const retryReportUrl = `${cloud.url}/api/v1/session/${retrySessionId}/report-state`;
|
|
6042
|
+
response = await fetchFn(retryReportUrl, {
|
|
5152
6043
|
method: "POST",
|
|
5153
6044
|
headers: {
|
|
5154
6045
|
"Content-Type": "application/json",
|
|
@@ -5187,7 +6078,7 @@ async function handleReportSessionState(deps, rawInput, _signal) {
|
|
|
5187
6078
|
`invalid input. Required fields: operator_id (non-empty string), session_id (non-empty string), agent_type (one of: ${VALID_AGENT_TYPES.join(" | ")}), context_used_pct (number 0-100). Optional: current_goal (string \u2264${MAX_GOAL_CHARS} chars), recent_files_touched (string[] \u2264${MAX_RECENT_FILES}), recent_tool_uses (string[] \u2264${MAX_RECENT_TOOLS}).`
|
|
5188
6079
|
);
|
|
5189
6080
|
}
|
|
5190
|
-
const cloud = getCloudConfig();
|
|
6081
|
+
const cloud = await getCloudConfig();
|
|
5191
6082
|
if (cloud !== null) {
|
|
5192
6083
|
const cloudPayload = await tryCloudReportState(cloud, rawInput);
|
|
5193
6084
|
if (cloudPayload !== null) {
|
|
@@ -5214,9 +6105,324 @@ async function handleReportSessionState(deps, rawInput, _signal) {
|
|
|
5214
6105
|
// src/tools/session/spawn-successor.ts
|
|
5215
6106
|
init_common();
|
|
5216
6107
|
import { spawn } from "node:child_process";
|
|
6108
|
+
import { homedir as homedir5 } from "node:os";
|
|
6109
|
+
import { join as join7 } from "node:path";
|
|
6110
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync4, openSync as openSync2, readFileSync as readFileSync7, readdirSync as readdirSync3, statSync as statSync3 } from "node:fs";
|
|
6111
|
+
|
|
6112
|
+
// src/swarm/tier-binding.ts
|
|
6113
|
+
var SWARM_TIERS = Object.freeze([
|
|
6114
|
+
"tier1_subscription",
|
|
6115
|
+
"tier1_local",
|
|
6116
|
+
"tier2_user_key",
|
|
6117
|
+
"tier3_platform_key",
|
|
6118
|
+
"refused",
|
|
6119
|
+
"unresolved"
|
|
6120
|
+
]);
|
|
6121
|
+
var TIER_ADMITS_SPAWN = /* @__PURE__ */ new Set([
|
|
6122
|
+
"tier1_subscription",
|
|
6123
|
+
"tier1_local",
|
|
6124
|
+
"tier2_user_key",
|
|
6125
|
+
"tier3_platform_key"
|
|
6126
|
+
]);
|
|
6127
|
+
var SWARM_TIER_BINDING_ENV = "VO_SWARM_TIER_BINDING";
|
|
6128
|
+
var MAX_BOUND_SUBAGENTS = 20;
|
|
6129
|
+
function isPositiveCap(cap) {
|
|
6130
|
+
return typeof cap === "number" && Number.isFinite(cap) && cap > 0;
|
|
6131
|
+
}
|
|
6132
|
+
function unresolvedBinding(swarmId, nowIso, reason) {
|
|
6133
|
+
return {
|
|
6134
|
+
schema_version: 1,
|
|
6135
|
+
swarm_id: swarmId,
|
|
6136
|
+
tier: "unresolved",
|
|
6137
|
+
agent: null,
|
|
6138
|
+
reason,
|
|
6139
|
+
exhausted_agents: [],
|
|
6140
|
+
subagent_budget: 0,
|
|
6141
|
+
spend_cap_usd: null,
|
|
6142
|
+
resolved_at: nowIso
|
|
6143
|
+
};
|
|
6144
|
+
}
|
|
6145
|
+
function serializeSwarmTierBinding(binding) {
|
|
6146
|
+
return JSON.stringify(binding);
|
|
6147
|
+
}
|
|
6148
|
+
function parseSwarmTierBinding(raw, nowIso) {
|
|
6149
|
+
if (typeof raw !== "string" || raw.trim().length === 0) {
|
|
6150
|
+
return unresolvedBinding("", nowIso, "no swarm tier binding present in the environment");
|
|
6151
|
+
}
|
|
6152
|
+
let parsed;
|
|
6153
|
+
try {
|
|
6154
|
+
parsed = JSON.parse(raw);
|
|
6155
|
+
} catch {
|
|
6156
|
+
return unresolvedBinding("", nowIso, "swarm tier binding is not valid JSON");
|
|
6157
|
+
}
|
|
6158
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
6159
|
+
return unresolvedBinding("", nowIso, "swarm tier binding is not an object");
|
|
6160
|
+
}
|
|
6161
|
+
const o = parsed;
|
|
6162
|
+
const swarmId = typeof o["swarm_id"] === "string" ? o["swarm_id"] : "";
|
|
6163
|
+
if (o["schema_version"] !== 1) {
|
|
6164
|
+
return unresolvedBinding(swarmId, nowIso, "swarm tier binding has an unsupported schema_version");
|
|
6165
|
+
}
|
|
6166
|
+
const tier = o["tier"];
|
|
6167
|
+
if (typeof tier !== "string" || !SWARM_TIERS.includes(tier)) {
|
|
6168
|
+
return unresolvedBinding(swarmId, nowIso, "swarm tier binding names an unknown tier");
|
|
6169
|
+
}
|
|
6170
|
+
const budget = o["subagent_budget"];
|
|
6171
|
+
const cap = o["spend_cap_usd"];
|
|
6172
|
+
const capNum = isPositiveCap(cap) ? cap : null;
|
|
6173
|
+
if (tier === "tier3_platform_key" && capNum === null) {
|
|
6174
|
+
return unresolvedBinding(
|
|
6175
|
+
swarmId,
|
|
6176
|
+
nowIso,
|
|
6177
|
+
"inherited tier3_platform_key binding carries no positive numeric spend cap \u2014 refusing an uncapped platform-billed fan-out"
|
|
6178
|
+
);
|
|
6179
|
+
}
|
|
6180
|
+
return {
|
|
6181
|
+
schema_version: 1,
|
|
6182
|
+
swarm_id: swarmId,
|
|
6183
|
+
tier,
|
|
6184
|
+
agent: typeof o["agent"] === "string" ? o["agent"] : null,
|
|
6185
|
+
reason: typeof o["reason"] === "string" ? o["reason"] : "inherited binding carried no reason",
|
|
6186
|
+
exhausted_agents: Array.isArray(o["exhausted_agents"]) ? o["exhausted_agents"].filter((v) => typeof v === "string") : [],
|
|
6187
|
+
subagent_budget: typeof budget === "number" && Number.isFinite(budget) && budget > 0 ? Math.min(Math.floor(budget), MAX_BOUND_SUBAGENTS) : 0,
|
|
6188
|
+
spend_cap_usd: capNum,
|
|
6189
|
+
resolved_at: typeof o["resolved_at"] === "string" ? o["resolved_at"] : nowIso
|
|
6190
|
+
};
|
|
6191
|
+
}
|
|
6192
|
+
function inheritSwarmTierBinding(env, nowIso) {
|
|
6193
|
+
return parseSwarmTierBinding(env[SWARM_TIER_BINDING_ENV], nowIso);
|
|
6194
|
+
}
|
|
6195
|
+
function bindingEnvFragment(binding) {
|
|
6196
|
+
return { [SWARM_TIER_BINDING_ENV]: serializeSwarmTierBinding(binding) };
|
|
6197
|
+
}
|
|
6198
|
+
function childBindingEnvFragment(binding, allocatedCapUsd = null) {
|
|
6199
|
+
return bindingEnvFragment(childBinding(binding, allocatedCapUsd));
|
|
6200
|
+
}
|
|
6201
|
+
function admitSubagentSpawn(binding, spawnsSoFar = 0) {
|
|
6202
|
+
if (!TIER_ADMITS_SPAWN.has(binding.tier)) {
|
|
6203
|
+
return { allowed: false, reason: `tier '${binding.tier}' admits no spawn: ${binding.reason}` };
|
|
6204
|
+
}
|
|
6205
|
+
if (binding.tier === "tier3_platform_key" && !isPositiveCap(binding.spend_cap_usd)) {
|
|
6206
|
+
return {
|
|
6207
|
+
allowed: false,
|
|
6208
|
+
reason: `swarm ${binding.swarm_id} is tier3_platform_key with no positive spend cap \u2014 refusing to spend the platform owner's money uncapped`
|
|
6209
|
+
};
|
|
6210
|
+
}
|
|
6211
|
+
if (!Number.isFinite(spawnsSoFar) || spawnsSoFar < 0) {
|
|
6212
|
+
return { allowed: false, reason: "spawn counter is not a finite non-negative number" };
|
|
6213
|
+
}
|
|
6214
|
+
if (spawnsSoFar >= binding.subagent_budget) {
|
|
6215
|
+
return {
|
|
6216
|
+
allowed: false,
|
|
6217
|
+
reason: `swarm ${binding.swarm_id} exhausted its bound subagent budget (${binding.subagent_budget})`
|
|
6218
|
+
};
|
|
6219
|
+
}
|
|
6220
|
+
return { allowed: true, reason: `admitted under tier '${binding.tier}'` };
|
|
6221
|
+
}
|
|
6222
|
+
function childBinding(binding, allocatedCapUsd = null) {
|
|
6223
|
+
const allocated = isPositiveCap(allocatedCapUsd) ? allocatedCapUsd : null;
|
|
6224
|
+
const parentCap = isPositiveCap(binding.spend_cap_usd) ? binding.spend_cap_usd : null;
|
|
6225
|
+
return {
|
|
6226
|
+
...binding,
|
|
6227
|
+
subagent_budget: Math.max(0, binding.subagent_budget - 1),
|
|
6228
|
+
// A child never carries more than its parent, whatever the ledger says: a
|
|
6229
|
+
// forged or hand-edited pool cannot inflate a descendant above the binding
|
|
6230
|
+
// it descends from.
|
|
6231
|
+
spend_cap_usd: allocated === null || parentCap === null ? null : Math.min(allocated, parentCap)
|
|
6232
|
+
};
|
|
6233
|
+
}
|
|
6234
|
+
function agentBindingRefusal(binding, requestedAgent) {
|
|
6235
|
+
const requested = typeof requestedAgent === "string" ? requestedAgent.trim() : "";
|
|
6236
|
+
if (requested.length === 0) return null;
|
|
6237
|
+
if (binding.agent !== null && requested === binding.agent) return null;
|
|
6238
|
+
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)`;
|
|
6239
|
+
}
|
|
6240
|
+
|
|
6241
|
+
// src/swarm/successor-launch.ts
|
|
6242
|
+
var AGENT_LAUNCH_SHAPES = Object.freeze({
|
|
6243
|
+
claude: {
|
|
6244
|
+
bin: "claude",
|
|
6245
|
+
baseArgs: ["-p", "--permission-mode", "acceptEdits"],
|
|
6246
|
+
enforcesMaxTurns: true,
|
|
6247
|
+
maxTurnsFlag: "--max-turns",
|
|
6248
|
+
windowsShellSafe: true
|
|
6249
|
+
},
|
|
6250
|
+
codex: {
|
|
6251
|
+
bin: "codex",
|
|
6252
|
+
baseArgs: ["exec", "--json", "-c", 'approval_policy="never"', "--sandbox", "workspace-write", "--skip-git-repo-check"],
|
|
6253
|
+
enforcesMaxTurns: false,
|
|
6254
|
+
// `-` makes codex read the prompt from stdin (injection-safe), matching how
|
|
6255
|
+
// codex-runner.mjs already spawns it.
|
|
6256
|
+
trailingArgs: ["-"],
|
|
6257
|
+
// `approval_policy="never"` carries embedded quotes; cmd.exe re-parsing is
|
|
6258
|
+
// unverified, so win32 refuses rather than risking a mangled sandbox flag.
|
|
6259
|
+
windowsShellSafe: false
|
|
6260
|
+
}
|
|
6261
|
+
});
|
|
6262
|
+
function resolveSuccessorLaunch(input) {
|
|
6263
|
+
const agent = typeof input.agent === "string" ? input.agent.trim() : "";
|
|
6264
|
+
if (!agent) {
|
|
6265
|
+
return { ok: false, reason: "no agent bound for this spawn \u2014 refusing rather than defaulting to claude" };
|
|
6266
|
+
}
|
|
6267
|
+
const shape = AGENT_LAUNCH_SHAPES[agent];
|
|
6268
|
+
if (!shape) {
|
|
6269
|
+
const known = Object.keys(AGENT_LAUNCH_SHAPES).join(", ");
|
|
6270
|
+
return {
|
|
6271
|
+
ok: false,
|
|
6272
|
+
reason: `no known headless launch shape for agent '${agent}' (known: ${known}) \u2014 refusing rather than guessing its argv`
|
|
6273
|
+
};
|
|
6274
|
+
}
|
|
6275
|
+
const wantsMaxTurns = Number.isInteger(input.maxTurns) && input.maxTurns > 0;
|
|
6276
|
+
if (wantsMaxTurns && !shape.enforcesMaxTurns) {
|
|
6277
|
+
return {
|
|
6278
|
+
ok: false,
|
|
6279
|
+
reason: `agent '${agent}' cannot enforce a max_turns cap \u2014 refusing rather than spawning it unbounded`
|
|
6280
|
+
};
|
|
6281
|
+
}
|
|
6282
|
+
const platform = input.platform ?? process.platform;
|
|
6283
|
+
if (platform === "win32" && !shape.windowsShellSafe) {
|
|
6284
|
+
return {
|
|
6285
|
+
ok: false,
|
|
6286
|
+
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`
|
|
6287
|
+
};
|
|
6288
|
+
}
|
|
6289
|
+
const args = [...shape.baseArgs];
|
|
6290
|
+
if (wantsMaxTurns && shape.maxTurnsFlag) {
|
|
6291
|
+
args.push(shape.maxTurnsFlag, String(input.maxTurns));
|
|
6292
|
+
}
|
|
6293
|
+
if (shape.trailingArgs) args.push(...shape.trailingArgs);
|
|
6294
|
+
return { ok: true, agent, bin: shape.bin, args };
|
|
6295
|
+
}
|
|
6296
|
+
|
|
6297
|
+
// src/swarm/spawn-ledger.ts
|
|
6298
|
+
import { mkdirSync as mkdirSync3, openSync, closeSync, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
|
|
5217
6299
|
import { homedir as homedir4 } from "node:os";
|
|
5218
6300
|
import { join as join6 } from "node:path";
|
|
5219
|
-
|
|
6301
|
+
var SWARM_LEDGER_DIR_ENV = "VO_SWARM_LEDGER_DIR";
|
|
6302
|
+
function resolveLedgerDir(env) {
|
|
6303
|
+
const override = env[SWARM_LEDGER_DIR_ENV];
|
|
6304
|
+
if (typeof override === "string" && override.trim().length > 0) return override.trim();
|
|
6305
|
+
return join6(homedir4(), ".vo", "swarm-ledger");
|
|
6306
|
+
}
|
|
6307
|
+
function sanitizeSwarmId(raw) {
|
|
6308
|
+
if (typeof raw !== "string") return null;
|
|
6309
|
+
const id = raw.trim();
|
|
6310
|
+
if (id.length === 0 || id.length > 128) return null;
|
|
6311
|
+
if (!/^[A-Za-z0-9._-]+$/u.test(id)) return null;
|
|
6312
|
+
if (id === "." || id === "..") return null;
|
|
6313
|
+
return id;
|
|
6314
|
+
}
|
|
6315
|
+
var CEILING_FILE = "ceiling.json";
|
|
6316
|
+
function createExclusive(path3, contents) {
|
|
6317
|
+
let fd;
|
|
6318
|
+
try {
|
|
6319
|
+
fd = openSync(path3, "wx");
|
|
6320
|
+
} catch {
|
|
6321
|
+
return false;
|
|
6322
|
+
}
|
|
6323
|
+
try {
|
|
6324
|
+
writeFileSync3(fd, contents, "utf8");
|
|
6325
|
+
} finally {
|
|
6326
|
+
closeSync(fd);
|
|
6327
|
+
}
|
|
6328
|
+
return true;
|
|
6329
|
+
}
|
|
6330
|
+
function capToCents(cap) {
|
|
6331
|
+
return isPositiveCap(cap) ? Math.round(cap * 100) : 0;
|
|
6332
|
+
}
|
|
6333
|
+
function readOrRecordLedgerHead(swarmDir, proposedCeiling, proposedCapCents, nowIso) {
|
|
6334
|
+
const path3 = join6(swarmDir, CEILING_FILE);
|
|
6335
|
+
const head = JSON.stringify({
|
|
6336
|
+
ceiling: proposedCeiling,
|
|
6337
|
+
cap_cents: proposedCapCents,
|
|
6338
|
+
recorded_at: nowIso
|
|
6339
|
+
});
|
|
6340
|
+
if (createExclusive(path3, head)) {
|
|
6341
|
+
return { ceiling: proposedCeiling, capCents: proposedCapCents };
|
|
6342
|
+
}
|
|
6343
|
+
let parsed;
|
|
6344
|
+
try {
|
|
6345
|
+
parsed = JSON.parse(readFileSync6(path3, "utf8"));
|
|
6346
|
+
} catch {
|
|
6347
|
+
return null;
|
|
6348
|
+
}
|
|
6349
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
|
|
6350
|
+
const record = parsed;
|
|
6351
|
+
const recorded = record["ceiling"];
|
|
6352
|
+
if (typeof recorded !== "number" || !Number.isFinite(recorded) || recorded < 1) return null;
|
|
6353
|
+
const recordedCap = record["cap_cents"];
|
|
6354
|
+
const capCents = typeof recordedCap === "number" && Number.isFinite(recordedCap) && recordedCap > 0 ? Math.floor(recordedCap) : 0;
|
|
6355
|
+
return { ceiling: Math.min(Math.floor(recorded), MAX_BOUND_SUBAGENTS), capCents };
|
|
6356
|
+
}
|
|
6357
|
+
var claimSpawnSlot = ({ swarmId, proposedCeiling, proposedCapUsd, dir, nowIso }) => {
|
|
6358
|
+
const id = sanitizeSwarmId(swarmId);
|
|
6359
|
+
if (id === null) {
|
|
6360
|
+
return {
|
|
6361
|
+
ok: false,
|
|
6362
|
+
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`
|
|
6363
|
+
};
|
|
6364
|
+
}
|
|
6365
|
+
const proposed = Number.isFinite(proposedCeiling) ? Math.floor(proposedCeiling) : 0;
|
|
6366
|
+
if (proposed < 1) {
|
|
6367
|
+
return { ok: false, reason: `swarm '${id}' proposes a ceiling of ${proposed} \u2014 no allowance to claim` };
|
|
6368
|
+
}
|
|
6369
|
+
const swarmDir = join6(dir, id);
|
|
6370
|
+
try {
|
|
6371
|
+
mkdirSync3(swarmDir, { recursive: true });
|
|
6372
|
+
} catch (err) {
|
|
6373
|
+
return {
|
|
6374
|
+
ok: false,
|
|
6375
|
+
reason: `swarm '${id}' ledger directory is unwritable (${err instanceof Error ? err.message : String(err)}) \u2014 refusing rather than spawning uncounted`
|
|
6376
|
+
};
|
|
6377
|
+
}
|
|
6378
|
+
const wantedCents = capToCents(proposedCapUsd);
|
|
6379
|
+
const head = readOrRecordLedgerHead(swarmDir, Math.min(proposed, MAX_BOUND_SUBAGENTS), wantedCents, nowIso);
|
|
6380
|
+
if (head === null) {
|
|
6381
|
+
return { ok: false, reason: `swarm '${id}' ledger carries no readable ceiling \u2014 refusing rather than spawning uncounted` };
|
|
6382
|
+
}
|
|
6383
|
+
const { ceiling, capCents } = head;
|
|
6384
|
+
const shareCents = capCents > 0 ? Math.floor(capCents / ceiling) : 0;
|
|
6385
|
+
if (wantedCents > 0 && shareCents < 1) {
|
|
6386
|
+
return {
|
|
6387
|
+
ok: false,
|
|
6388
|
+
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`
|
|
6389
|
+
};
|
|
6390
|
+
}
|
|
6391
|
+
for (let slot = 0; slot < ceiling; slot++) {
|
|
6392
|
+
const debitedCents = shareCents;
|
|
6393
|
+
const remainingCents = capCents > 0 ? capCents - (slot + 1) * shareCents : 0;
|
|
6394
|
+
const claimed = createExclusive(
|
|
6395
|
+
join6(swarmDir, `slot-${slot}.json`),
|
|
6396
|
+
JSON.stringify({
|
|
6397
|
+
slot,
|
|
6398
|
+
ceiling,
|
|
6399
|
+
pid: process.pid,
|
|
6400
|
+
claimed_at: nowIso,
|
|
6401
|
+
// The debit record. Durable and atomic with the claim: this file is
|
|
6402
|
+
// created with O_EXCL, so exactly one claimant ever writes this line.
|
|
6403
|
+
cap_cents_pool: capCents,
|
|
6404
|
+
cap_cents_debited: debitedCents,
|
|
6405
|
+
cap_cents_remaining: remainingCents
|
|
6406
|
+
})
|
|
6407
|
+
);
|
|
6408
|
+
if (claimed) {
|
|
6409
|
+
return {
|
|
6410
|
+
ok: true,
|
|
6411
|
+
slot,
|
|
6412
|
+
ceiling,
|
|
6413
|
+
remaining: ceiling - slot - 1,
|
|
6414
|
+
capUsd: debitedCents > 0 ? debitedCents / 100 : null,
|
|
6415
|
+
capRemainingUsd: capCents > 0 ? remainingCents / 100 : null
|
|
6416
|
+
};
|
|
6417
|
+
}
|
|
6418
|
+
}
|
|
6419
|
+
return {
|
|
6420
|
+
ok: false,
|
|
6421
|
+
reason: `swarm '${id}' has spent its whole fan-out ceiling (${ceiling} spawns across every generation) \u2014 refusing`
|
|
6422
|
+
};
|
|
6423
|
+
};
|
|
6424
|
+
|
|
6425
|
+
// src/tools/session/spawn-successor.ts
|
|
5220
6426
|
var TOOL_NAME20 = "vo_spawn_successor";
|
|
5221
6427
|
var MAX_HANDOFF_BYTES = 64e3;
|
|
5222
6428
|
var inputSchema20 = {
|
|
@@ -5237,11 +6443,16 @@ var inputSchema20 = {
|
|
|
5237
6443
|
max_turns: {
|
|
5238
6444
|
type: "number",
|
|
5239
6445
|
description: "Optional --max-turns bound for the successor."
|
|
6446
|
+
},
|
|
6447
|
+
agent: {
|
|
6448
|
+
type: "string",
|
|
6449
|
+
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.`
|
|
5240
6450
|
}
|
|
5241
6451
|
},
|
|
5242
6452
|
required: [],
|
|
5243
6453
|
additionalProperties: false
|
|
5244
6454
|
};
|
|
6455
|
+
var RETIRED_COUNTER_INPUT = "spawns_so_far";
|
|
5245
6456
|
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.";
|
|
5246
6457
|
function isToolInput20(v) {
|
|
5247
6458
|
if (typeof v !== "object" || v === null) return false;
|
|
@@ -5250,12 +6461,18 @@ function isToolInput20(v) {
|
|
|
5250
6461
|
if (o["goal"] !== void 0 && typeof o["goal"] !== "string") return false;
|
|
5251
6462
|
if (o["cwd"] !== void 0 && typeof o["cwd"] !== "string") return false;
|
|
5252
6463
|
if (o["max_turns"] !== void 0 && typeof o["max_turns"] !== "number") return false;
|
|
6464
|
+
if (o["agent"] !== void 0 && typeof o["agent"] !== "string") return false;
|
|
5253
6465
|
return true;
|
|
5254
6466
|
}
|
|
5255
|
-
function
|
|
6467
|
+
function retiredCounterRefusal(v) {
|
|
6468
|
+
if (typeof v !== "object" || v === null) return null;
|
|
6469
|
+
if (!(RETIRED_COUNTER_INPUT in v)) return null;
|
|
6470
|
+
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.`;
|
|
6471
|
+
}
|
|
6472
|
+
function newestHandoff(dir = join7(homedir5(), ".vo", "handoffs")) {
|
|
5256
6473
|
try {
|
|
5257
|
-
const entries = readdirSync3(dir).filter((f) => f.endsWith(".md")).map((f) => ({ f, m: statSync3(
|
|
5258
|
-
return entries.length > 0 && entries[0] ?
|
|
6474
|
+
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);
|
|
6475
|
+
return entries.length > 0 && entries[0] ? join7(dir, entries[0].f) : null;
|
|
5259
6476
|
} catch {
|
|
5260
6477
|
return null;
|
|
5261
6478
|
}
|
|
@@ -5271,7 +6488,7 @@ var MANDATORY_READS = [
|
|
|
5271
6488
|
function buildSuccessorPrompt(handoffMarkdown, goal) {
|
|
5272
6489
|
const reads = MANDATORY_READS.map((r, i) => ` ${i + 1}. ${r}`).join("\n");
|
|
5273
6490
|
const lines = [
|
|
5274
|
-
"You are the SUCCESSOR agent for
|
|
6491
|
+
"You are the SUCCESSOR agent for an AlgoHQ lane. The previous session",
|
|
5275
6492
|
"exhausted its context and wrote the handoff below. Read it fully, verify its",
|
|
5276
6493
|
'"verification needed" items against live state (a handoff is a claim, not',
|
|
5277
6494
|
"evidence \u2014 verify via `git show origin/main:<path>`), then continue the lane.",
|
|
@@ -5282,7 +6499,7 @@ function buildSuccessorPrompt(handoffMarkdown, goal) {
|
|
|
5282
6499
|
"NON-NEGOTIABLES: multi-model consensus verification is the core; test honesty",
|
|
5283
6500
|
"(verified-answer-only, no fake green); verify-before-act + human merge approval;",
|
|
5284
6501
|
"never a full functions-shared deploy; Gen2 only; work in a worktree on your own",
|
|
5285
|
-
"branch; finish line is MERGED + DEPLOYED + LIVE-VERIFIED, and
|
|
6502
|
+
"branch; finish line is MERGED + DEPLOYED + LIVE-VERIFIED, and AlgoHQ changes update",
|
|
5286
6503
|
"the roadmap in the same PR.",
|
|
5287
6504
|
"",
|
|
5288
6505
|
"--- HANDOFF ---",
|
|
@@ -5292,147 +6509,759 @@ function buildSuccessorPrompt(handoffMarkdown, goal) {
|
|
|
5292
6509
|
if (goal && goal.trim().length > 0) lines.push("", `OPERATOR GOAL OVERRIDE: ${goal.trim()}`);
|
|
5293
6510
|
return lines.join("\n");
|
|
5294
6511
|
}
|
|
5295
|
-
function buildSuccessorArgs(maxTurns) {
|
|
5296
|
-
const args = ["-p", "--permission-mode", "acceptEdits"];
|
|
5297
|
-
if (Number.isInteger(maxTurns) && maxTurns > 0) {
|
|
5298
|
-
args.push("--max-turns", String(maxTurns));
|
|
6512
|
+
function buildSuccessorArgs(maxTurns) {
|
|
6513
|
+
const args = ["-p", "--permission-mode", "acceptEdits"];
|
|
6514
|
+
if (Number.isInteger(maxTurns) && maxTurns > 0) {
|
|
6515
|
+
args.push("--max-turns", String(maxTurns));
|
|
6516
|
+
}
|
|
6517
|
+
return args;
|
|
6518
|
+
}
|
|
6519
|
+
function resolveSpawnPlan(env, input, nowIso, platform = process.platform, claim = claimSpawnSlot) {
|
|
6520
|
+
const rawBinding = env[SWARM_TIER_BINDING_ENV];
|
|
6521
|
+
const hasBinding = typeof rawBinding === "string" && rawBinding.trim().length > 0;
|
|
6522
|
+
if (!hasBinding) {
|
|
6523
|
+
const explicit = input.agent?.trim();
|
|
6524
|
+
if (explicit) {
|
|
6525
|
+
const resolved2 = resolveSuccessorLaunch({ agent: explicit, maxTurns: input.max_turns, platform });
|
|
6526
|
+
if (!resolved2.ok) return { ok: false, reason: resolved2.reason, tier: "unbound" };
|
|
6527
|
+
return {
|
|
6528
|
+
ok: true,
|
|
6529
|
+
bin: resolved2.bin,
|
|
6530
|
+
args: resolved2.args,
|
|
6531
|
+
agent: resolved2.agent,
|
|
6532
|
+
tier: "unbound",
|
|
6533
|
+
bound: false,
|
|
6534
|
+
env: {},
|
|
6535
|
+
slot: null,
|
|
6536
|
+
capUsd: null,
|
|
6537
|
+
capRemainingUsd: null
|
|
6538
|
+
};
|
|
6539
|
+
}
|
|
6540
|
+
return {
|
|
6541
|
+
ok: true,
|
|
6542
|
+
bin: "claude",
|
|
6543
|
+
args: buildSuccessorArgs(input.max_turns),
|
|
6544
|
+
agent: "claude",
|
|
6545
|
+
tier: "unbound",
|
|
6546
|
+
bound: false,
|
|
6547
|
+
env: {},
|
|
6548
|
+
slot: null,
|
|
6549
|
+
capUsd: null,
|
|
6550
|
+
capRemainingUsd: null
|
|
6551
|
+
};
|
|
6552
|
+
}
|
|
6553
|
+
const binding = inheritSwarmTierBinding(env, nowIso);
|
|
6554
|
+
const admission = admitSubagentSpawn(binding);
|
|
6555
|
+
if (!admission.allowed) {
|
|
6556
|
+
return { ok: false, reason: admission.reason, tier: binding.tier };
|
|
6557
|
+
}
|
|
6558
|
+
const agentRefusal = agentBindingRefusal(binding, input.agent);
|
|
6559
|
+
if (agentRefusal !== null) return { ok: false, reason: agentRefusal, tier: binding.tier };
|
|
6560
|
+
const resolved = resolveSuccessorLaunch({
|
|
6561
|
+
agent: binding.agent,
|
|
6562
|
+
maxTurns: input.max_turns,
|
|
6563
|
+
platform
|
|
6564
|
+
});
|
|
6565
|
+
if (!resolved.ok) return { ok: false, reason: resolved.reason, tier: binding.tier };
|
|
6566
|
+
const slot = claim({
|
|
6567
|
+
swarmId: binding.swarm_id,
|
|
6568
|
+
proposedCeiling: binding.subagent_budget,
|
|
6569
|
+
// The spend-cap POOL, recorded once per swarm exactly like the ceiling. The
|
|
6570
|
+
// child's cap is DEBITED from it below, not recomputed from this binding.
|
|
6571
|
+
proposedCapUsd: binding.spend_cap_usd,
|
|
6572
|
+
dir: resolveLedgerDir(env),
|
|
6573
|
+
nowIso
|
|
6574
|
+
});
|
|
6575
|
+
if (!slot.ok) return { ok: false, reason: slot.reason, tier: binding.tier };
|
|
6576
|
+
return {
|
|
6577
|
+
ok: true,
|
|
6578
|
+
bin: resolved.bin,
|
|
6579
|
+
args: resolved.args,
|
|
6580
|
+
agent: resolved.agent,
|
|
6581
|
+
tier: binding.tier,
|
|
6582
|
+
bound: true,
|
|
6583
|
+
// Re-export the same TIER with a DECREMENTED budget and the spend cap the
|
|
6584
|
+
// ledger just DEBITED. Exporting the binding verbatim (what this did before
|
|
6585
|
+
// #9312) meant the child re-read the full budget and every generation
|
|
6586
|
+
// restarted at zero. Recomputing the cap from THIS binding (what #9312 did)
|
|
6587
|
+
// bounded a chain but not a tree: three siblings each re-halved the parent's
|
|
6588
|
+
// untouched $50 and walked away with $75 between them.
|
|
6589
|
+
env: childBindingEnvFragment(binding, slot.capUsd),
|
|
6590
|
+
slot: slot.slot,
|
|
6591
|
+
capUsd: slot.capUsd,
|
|
6592
|
+
capRemainingUsd: slot.capRemainingUsd
|
|
6593
|
+
};
|
|
6594
|
+
}
|
|
6595
|
+
async function handleSpawnSuccessor(_deps, rawInput, _signal, spawnImpl = spawn) {
|
|
6596
|
+
const retired = retiredCounterRefusal(rawInput);
|
|
6597
|
+
if (retired !== null) throw invalidParams(TOOL_NAME20, retired);
|
|
6598
|
+
if (!isToolInput20(rawInput)) {
|
|
6599
|
+
throw invalidParams(TOOL_NAME20, "invalid input. Optional: { handoff_path, goal, cwd, max_turns, agent }.");
|
|
6600
|
+
}
|
|
6601
|
+
const handoffPath = rawInput.handoff_path?.trim() || newestHandoff();
|
|
6602
|
+
if (!handoffPath || !existsSync4(handoffPath)) {
|
|
6603
|
+
return jsonContent({
|
|
6604
|
+
tool: TOOL_NAME20,
|
|
6605
|
+
schema_version: 1,
|
|
6606
|
+
payload: {
|
|
6607
|
+
spawned: false,
|
|
6608
|
+
reason: rawInput.handoff_path ? `handoff not found: ${rawInput.handoff_path}` : "no handoff docs in ~/.vo/handoffs \u2014 write one first (the 85% directive does this)"
|
|
6609
|
+
}
|
|
6610
|
+
});
|
|
6611
|
+
}
|
|
6612
|
+
const handoff = readFileSync7(handoffPath, "utf8").slice(0, MAX_HANDOFF_BYTES);
|
|
6613
|
+
const prompt = buildSuccessorPrompt(handoff, rawInput.goal);
|
|
6614
|
+
const plan = resolveSpawnPlan(process.env, rawInput, (/* @__PURE__ */ new Date()).toISOString());
|
|
6615
|
+
if (!plan.ok) {
|
|
6616
|
+
return jsonContent({
|
|
6617
|
+
tool: TOOL_NAME20,
|
|
6618
|
+
schema_version: 1,
|
|
6619
|
+
payload: {
|
|
6620
|
+
spawned: false,
|
|
6621
|
+
reason: `swarm tier binding refused this spawn: ${plan.reason}`,
|
|
6622
|
+
tier: plan.tier,
|
|
6623
|
+
handoff_path: handoffPath
|
|
6624
|
+
}
|
|
6625
|
+
});
|
|
6626
|
+
}
|
|
6627
|
+
const logDir = process.env["VO_MCP_SUCCESSOR_LOG_DIR"]?.trim() || join7(homedir5(), ".vo", "successors");
|
|
6628
|
+
mkdirSync4(logDir, { recursive: true });
|
|
6629
|
+
const logPath = join7(logDir, `successor-${Date.now()}.log`);
|
|
6630
|
+
const logFd = openSync2(logPath, "a");
|
|
6631
|
+
const child = spawnImpl(plan.bin, [...plan.args], {
|
|
6632
|
+
cwd: rawInput.cwd?.trim() || process.cwd(),
|
|
6633
|
+
detached: true,
|
|
6634
|
+
stdio: ["pipe", logFd, logFd],
|
|
6635
|
+
// Windows: the agent CLIs are .cmd shims — they need a shell to resolve.
|
|
6636
|
+
// The prompt goes via STDIN below, never argv, so the shell never sees it.
|
|
6637
|
+
shell: process.platform === "win32",
|
|
6638
|
+
windowsHide: true,
|
|
6639
|
+
// Carry the SAME binding to the child. Without this the successor inherits
|
|
6640
|
+
// no tier and re-resolves its own — which is the split-payer defect one
|
|
6641
|
+
// generation down.
|
|
6642
|
+
...plan.bound ? { env: { ...process.env, ...plan.env } } : {}
|
|
6643
|
+
});
|
|
6644
|
+
let spawnError = null;
|
|
6645
|
+
child.on("error", (e) => {
|
|
6646
|
+
spawnError = e.message;
|
|
6647
|
+
});
|
|
6648
|
+
try {
|
|
6649
|
+
child.stdin.write(prompt);
|
|
6650
|
+
child.stdin.end();
|
|
6651
|
+
} catch {
|
|
6652
|
+
}
|
|
6653
|
+
child.unref();
|
|
6654
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
6655
|
+
return jsonContent({
|
|
6656
|
+
tool: TOOL_NAME20,
|
|
6657
|
+
schema_version: 1,
|
|
6658
|
+
payload: spawnError ? { spawned: false, reason: `spawn failed: ${spawnError}`, agent: plan.agent, tier: plan.tier, handoff_path: handoffPath } : {
|
|
6659
|
+
spawned: true,
|
|
6660
|
+
pid: child.pid ?? null,
|
|
6661
|
+
log_path: logPath,
|
|
6662
|
+
handoff_path: handoffPath,
|
|
6663
|
+
agent: plan.agent,
|
|
6664
|
+
tier: plan.tier,
|
|
6665
|
+
tier_bound: plan.bound,
|
|
6666
|
+
ledger_slot: plan.slot,
|
|
6667
|
+
// The debit, surfaced so an operator can reconcile a fan-out's spend
|
|
6668
|
+
// against the pool without reading the ledger directory by hand.
|
|
6669
|
+
ledger_cap_usd: plan.capUsd,
|
|
6670
|
+
ledger_cap_remaining_usd: plan.capRemainingUsd
|
|
6671
|
+
}
|
|
6672
|
+
});
|
|
6673
|
+
}
|
|
6674
|
+
|
|
6675
|
+
// src/tools/concierge/dispatch.ts
|
|
6676
|
+
init_common();
|
|
6677
|
+
|
|
6678
|
+
// src/tools/concierge/common-concierge.ts
|
|
6679
|
+
var CONCIERGE_STUB_REASON = "cloud mode not active in this MCP runtime \u2014 set VO_CONTROL_PLANE_URL + VO_CONTROL_PLANE_ADMIN_TOKEN to enable. The server-side /api/v1/admin/concierge/dispatch endpoint IS built + deployed (vo-control-plane #5724/#5734); in cloud mode this tool returns the routed pack README + file index.";
|
|
6680
|
+
var CONCIERGE_GATE_TYPE = "concierge-dispatch";
|
|
6681
|
+
var KNOWN_CONCIERGE_PACKS = [
|
|
6682
|
+
"gcp",
|
|
6683
|
+
"firebase",
|
|
6684
|
+
"aws",
|
|
6685
|
+
"cloudflare",
|
|
6686
|
+
"vercel",
|
|
6687
|
+
"netlify",
|
|
6688
|
+
"tax",
|
|
6689
|
+
"hybrid"
|
|
6690
|
+
];
|
|
6691
|
+
function isKnownConciergePack(value) {
|
|
6692
|
+
return typeof value === "string" && KNOWN_CONCIERGE_PACKS.includes(value);
|
|
6693
|
+
}
|
|
6694
|
+
|
|
6695
|
+
// src/tools/concierge/dispatch.ts
|
|
6696
|
+
var TOOL_NAME21 = "vo_concierge_dispatch";
|
|
6697
|
+
var CALLABLE_NAME10 = "voConciergeDispatch";
|
|
6698
|
+
var ADMIN_PATH10 = "/api/v1/admin/concierge/dispatch";
|
|
6699
|
+
var inputSchema21 = {
|
|
6700
|
+
type: "object",
|
|
6701
|
+
properties: {
|
|
6702
|
+
pack: {
|
|
6703
|
+
type: "string",
|
|
6704
|
+
description: `Explicit knowledge pack name. One of: ${KNOWN_CONCIERGE_PACKS.join(", ")}. When provided, overrides tenant-based routing. Omit to let server route by tenant_id.`,
|
|
6705
|
+
enum: [...KNOWN_CONCIERGE_PACKS]
|
|
6706
|
+
},
|
|
6707
|
+
tenant_id: {
|
|
6708
|
+
type: "string",
|
|
6709
|
+
description: "Tenant identifier. When provided (and pack is omitted), the server looks up Tenant.cloud_provider and dispatches to the matching pack. Ignored when pack is also provided."
|
|
6710
|
+
}
|
|
6711
|
+
},
|
|
6712
|
+
additionalProperties: false
|
|
6713
|
+
};
|
|
6714
|
+
var description21 = "Dispatches a provider-scoped knowledge pack (gcp | firebase | aws | cloudflare | vercel | netlify | tax | hybrid). Cross-vendor MCP equivalent of the /vo-concierge Claude-Code slash command. Route explicitly via `pack`, or via tenant.cloud_provider by passing `tenant_id`. Returns the pack's README (`readme_markdown`) + file index. In cloud mode, dispatches via vo-control-plane and returns `verdict: 'pass'` with the pack/directory data; without cloud config, returns `verdict: 'unimplemented'`.";
|
|
6715
|
+
function isToolInput21(v) {
|
|
6716
|
+
if (typeof v !== "object" || v === null) return false;
|
|
6717
|
+
const obj = v;
|
|
6718
|
+
if (obj.pack !== void 0 && typeof obj.pack !== "string") return false;
|
|
6719
|
+
if (obj.tenant_id !== void 0 && typeof obj.tenant_id !== "string") return false;
|
|
6720
|
+
return true;
|
|
6721
|
+
}
|
|
6722
|
+
async function handleConciergeDispatch(deps, rawInput, _signal) {
|
|
6723
|
+
if (!isToolInput21(rawInput)) {
|
|
6724
|
+
throw invalidParams(
|
|
6725
|
+
TOOL_NAME21,
|
|
6726
|
+
"invalid input. Expected { pack?: string, tenant_id?: string }."
|
|
6727
|
+
);
|
|
6728
|
+
}
|
|
6729
|
+
if (rawInput.pack !== void 0 && rawInput.pack !== "" && !isKnownConciergePack(rawInput.pack)) {
|
|
6730
|
+
throw invalidParams(
|
|
6731
|
+
TOOL_NAME21,
|
|
6732
|
+
`unknown pack: ${JSON.stringify(rawInput.pack)}. Known packs: ${KNOWN_CONCIERGE_PACKS.join(", ")}.`
|
|
6733
|
+
);
|
|
6734
|
+
}
|
|
6735
|
+
const normalizedInput = {};
|
|
6736
|
+
if (rawInput.pack) normalizedInput.pack = rawInput.pack;
|
|
6737
|
+
if (rawInput.tenant_id) normalizedInput.tenant_id = rawInput.tenant_id;
|
|
6738
|
+
const cloudBody = {};
|
|
6739
|
+
if (rawInput.pack) cloudBody.pack = rawInput.pack;
|
|
6740
|
+
if (rawInput.tenant_id) cloudBody.tenantId = rawInput.tenant_id;
|
|
6741
|
+
return buildCloudOrStubResponse({
|
|
6742
|
+
toolName: TOOL_NAME21,
|
|
6743
|
+
callableName: CALLABLE_NAME10,
|
|
6744
|
+
adminPath: ADMIN_PATH10,
|
|
6745
|
+
normalizedInput,
|
|
6746
|
+
cloudBody,
|
|
6747
|
+
// Concierge dispatch returns `{ok, mode, routed_from, pack|packs}`,
|
|
6748
|
+
// NOT the callable-proxy `{ok, callable, result}` shape — take the
|
|
6749
|
+
// raw envelope as response_data instead of mandating `.result`.
|
|
6750
|
+
rawEnvelope: true,
|
|
6751
|
+
gateType: CONCIERGE_GATE_TYPE,
|
|
6752
|
+
stubReason: CONCIERGE_STUB_REASON,
|
|
6753
|
+
// Read-only pack fetch — stays live under VO_ADMIN_CALLABLES_READONLY.
|
|
6754
|
+
readOnly: true,
|
|
6755
|
+
deps
|
|
6756
|
+
});
|
|
6757
|
+
}
|
|
6758
|
+
|
|
6759
|
+
// src/server.ts
|
|
6760
|
+
init_sync_config();
|
|
6761
|
+
|
|
6762
|
+
// src/tools/memory/private-knowledge.ts
|
|
6763
|
+
init_common();
|
|
6764
|
+
var UPSERT_TOOL_NAME = "vo_private_knowledge_upsert";
|
|
6765
|
+
var CONTEXT_TOOL_NAME = "vo_private_knowledge_context";
|
|
6766
|
+
var INVALIDATE_TOOL_NAME = "vo_private_knowledge_invalidate";
|
|
6767
|
+
var STALE_TOOL_NAME = "vo_private_knowledge_stale";
|
|
6768
|
+
var KNOWLEDGE_CLASSES = ["memory", "skill", "doctrine", "hook", "command"];
|
|
6769
|
+
var PRECISION_CHAR_BUDGET = 12e3;
|
|
6770
|
+
var upsertInputSchema = {
|
|
6771
|
+
type: "object",
|
|
6772
|
+
properties: {
|
|
6773
|
+
knowledge_class: { type: "string", enum: KNOWLEDGE_CLASSES },
|
|
6774
|
+
source_path: { type: "string", description: "Stable private source identifier; not exposed to other users." },
|
|
6775
|
+
title: { type: "string", description: 'Descriptive, retrieval-friendly title (e.g. "AlgoTax OCR redaction architecture", not "notes") \u2014 retrieval matches on it.' },
|
|
6776
|
+
content: { type: "string", description: "Private knowledge text to store server-side. Keep each entry tight and focused (~1-3 pages, under ~12k chars); split larger corpora into separate entries." }
|
|
6777
|
+
},
|
|
6778
|
+
required: ["knowledge_class", "source_path", "title", "content"],
|
|
6779
|
+
additionalProperties: false
|
|
6780
|
+
};
|
|
6781
|
+
var contextInputSchema = {
|
|
6782
|
+
type: "object",
|
|
6783
|
+
properties: {
|
|
6784
|
+
query: { type: "string" },
|
|
6785
|
+
limit: { type: "number", minimum: 1, maximum: 50 },
|
|
6786
|
+
knowledge_class: { type: "string", enum: KNOWLEDGE_CLASSES }
|
|
6787
|
+
},
|
|
6788
|
+
required: ["query"],
|
|
6789
|
+
additionalProperties: false
|
|
6790
|
+
};
|
|
6791
|
+
var invalidateInputSchema = {
|
|
6792
|
+
type: "object",
|
|
6793
|
+
properties: {
|
|
6794
|
+
knowledge_class: { type: "string", enum: KNOWLEDGE_CLASSES },
|
|
6795
|
+
source_path: { type: "string", minLength: 1, maxLength: 400, description: "Stable private source identifier of the entry to invalidate \u2014 must match the source_path used at upsert." }
|
|
6796
|
+
},
|
|
6797
|
+
required: ["knowledge_class", "source_path"],
|
|
6798
|
+
additionalProperties: false
|
|
6799
|
+
};
|
|
6800
|
+
var staleInputSchema = {
|
|
6801
|
+
type: "object",
|
|
6802
|
+
properties: {
|
|
6803
|
+
days: { type: "number", minimum: 1, maximum: 3650, description: "Window in days (default 90): entries at least this old that were never recalled into an agent context, or not within the window." },
|
|
6804
|
+
limit: { type: "number", minimum: 1, maximum: 500 }
|
|
6805
|
+
},
|
|
6806
|
+
additionalProperties: false
|
|
6807
|
+
};
|
|
6808
|
+
var staleDescription = `The FORGETTING REPORT: lists the authenticated operator\u2019s live private-knowledge entries that are at least N days old and have never been recalled into an agent context (or not within N days). Metadata only. SURFACES ONLY \u2014 never auto-invalidates or auto-merges: two memories that disagree may both have been right in different contexts, so you decide. Act on a candidate deliberately with ${INVALIDATE_TOOL_NAME}; recall counts come from ${CONTEXT_TOOL_NAME} reads that actually placed the entry into returned context.`;
|
|
6809
|
+
var upsertDescription = "Uploads or refreshes the authenticated operator\u2019s private cloud knowledge. Works for Claude, Codex, Cursor, and cowork clients via the same vo-mcp login credential. Returns metadata only, not raw stored content. PRECISION DISCIPLINE: keep each entry tight and focused (~1-3 pages) with a descriptive retrieval-friendly title \u2014 retrieval surfaces whole entries, so small dense entries beat bulk dumps. Split large corpora into focused entries, then run a retrieval self-test via vo_private_knowledge_context before relying on the knowledge.";
|
|
6810
|
+
var contextDescription = "Retrieves prompt-ready private knowledge context for the authenticated operator. Returns snippets/context only; no raw corpus download. Also the retrieval self-test surface: after upserting critical knowledge, query for it here and confirm the entry surfaces before trusting it in downstream work.";
|
|
6811
|
+
var invalidateDescription = `Soft-deletes one private-knowledge entry for the authenticated operator: closes the live entry\u2019s validity window (bi-temporal) so it stops surfacing in retrieval. Never destroys data \u2014 invalidated versions remain queryable server-side via include_invalidated. Identify the entry by the same { knowledge_class, source_path } used at upsert; a not_found response means no live entry matches. After invalidating, self-test via ${CONTEXT_TOOL_NAME} to confirm the entry no longer surfaces.`;
|
|
6812
|
+
function isStaleInput(value) {
|
|
6813
|
+
if (value === void 0 || value === null) return true;
|
|
6814
|
+
if (typeof value !== "object") return false;
|
|
6815
|
+
const input = value;
|
|
6816
|
+
if (input["days"] !== void 0 && typeof input["days"] !== "number") return false;
|
|
6817
|
+
if (input["limit"] !== void 0 && typeof input["limit"] !== "number") return false;
|
|
6818
|
+
return true;
|
|
6819
|
+
}
|
|
6820
|
+
function isKnowledgeClass(value) {
|
|
6821
|
+
return typeof value === "string" && KNOWLEDGE_CLASSES.includes(value);
|
|
6822
|
+
}
|
|
6823
|
+
function isUpsertInput(value) {
|
|
6824
|
+
if (typeof value !== "object" || value === null) return false;
|
|
6825
|
+
const input = value;
|
|
6826
|
+
return isKnowledgeClass(input["knowledge_class"]) && typeof input["source_path"] === "string" && typeof input["title"] === "string" && typeof input["content"] === "string";
|
|
6827
|
+
}
|
|
6828
|
+
function isInvalidateInput(value) {
|
|
6829
|
+
if (typeof value !== "object" || value === null) return false;
|
|
6830
|
+
const input = value;
|
|
6831
|
+
return isKnowledgeClass(input["knowledge_class"]) && typeof input["source_path"] === "string";
|
|
6832
|
+
}
|
|
6833
|
+
function isContextInput(value) {
|
|
6834
|
+
if (typeof value !== "object" || value === null) return false;
|
|
6835
|
+
const input = value;
|
|
6836
|
+
if (typeof input["query"] !== "string") return false;
|
|
6837
|
+
if (input["limit"] !== void 0 && typeof input["limit"] !== "number") return false;
|
|
6838
|
+
if (input["knowledge_class"] !== void 0 && !isKnowledgeClass(input["knowledge_class"])) return false;
|
|
6839
|
+
return true;
|
|
6840
|
+
}
|
|
6841
|
+
async function getCloudAuth(fetchFn) {
|
|
6842
|
+
const controlPlaneUrl = process.env["VO_CONTROL_PLANE_URL"]?.replace(/\/+$/, "");
|
|
6843
|
+
if (!controlPlaneUrl) {
|
|
6844
|
+
return { ok: false, reason: "VO_CONTROL_PLANE_URL not set \u2014 cloud mode disabled" };
|
|
6845
|
+
}
|
|
6846
|
+
const { createAuthTokenSourceFromEnv: createAuthTokenSourceFromEnv2 } = await Promise.resolve().then(() => (init_auth_token_source(), auth_token_source_exports));
|
|
6847
|
+
const { readStoredCredential: readStoredCredential2 } = await Promise.resolve().then(() => (init_credential_store(), credential_store_exports));
|
|
6848
|
+
const tokenSource = createAuthTokenSourceFromEnv2(process.env, fetchFn, () => readStoredCredential2(process.env));
|
|
6849
|
+
if (!tokenSource) return { ok: false, reason: "No auth configured. Run `vo-mcp login`." };
|
|
6850
|
+
const token = await tokenSource.getToken();
|
|
6851
|
+
if (!token) return { ok: false, reason: "Failed to obtain auth token. Run `vo-mcp login` again." };
|
|
6852
|
+
return { ok: true, controlPlaneUrl, token };
|
|
6853
|
+
}
|
|
6854
|
+
async function callPrivateKnowledge(path3, body, fetchFn) {
|
|
6855
|
+
const auth = await getCloudAuth(fetchFn);
|
|
6856
|
+
if (!auth.ok) return { ok: false, reason: auth.reason };
|
|
6857
|
+
const response = await fetchFn(`${auth.controlPlaneUrl}${path3}`, {
|
|
6858
|
+
method: "POST",
|
|
6859
|
+
headers: {
|
|
6860
|
+
authorization: `Bearer ${auth.token}`,
|
|
6861
|
+
"content-type": "application/json"
|
|
6862
|
+
},
|
|
6863
|
+
body: JSON.stringify(body)
|
|
6864
|
+
});
|
|
6865
|
+
const text = await response.text();
|
|
6866
|
+
let parsed;
|
|
6867
|
+
try {
|
|
6868
|
+
parsed = text ? JSON.parse(text) : null;
|
|
6869
|
+
} catch {
|
|
6870
|
+
parsed = null;
|
|
6871
|
+
}
|
|
6872
|
+
if (response.status < 200 || response.status >= 300) {
|
|
6873
|
+
return { ok: false, status: response.status, response: parsed ?? text };
|
|
6874
|
+
}
|
|
6875
|
+
return parsed;
|
|
6876
|
+
}
|
|
6877
|
+
async function handlePrivateKnowledgeUpsert(_deps, rawInput, _signal, fetchFn = globalThis.fetch) {
|
|
6878
|
+
if (!isUpsertInput(rawInput)) {
|
|
6879
|
+
throw invalidParams(UPSERT_TOOL_NAME, "expected { knowledge_class, source_path, title, content }.");
|
|
6880
|
+
}
|
|
6881
|
+
const payload = await callPrivateKnowledge("/api/v1/knowledge/private", rawInput, fetchFn);
|
|
6882
|
+
const envelope = {
|
|
6883
|
+
tool: UPSERT_TOOL_NAME,
|
|
6884
|
+
schema_version: 1,
|
|
6885
|
+
payload
|
|
6886
|
+
};
|
|
6887
|
+
if (rawInput.content.length > PRECISION_CHAR_BUDGET) {
|
|
6888
|
+
envelope.precision_note = `content is ${rawInput.content.length} chars (> ${PRECISION_CHAR_BUDGET}). Tight 1-3 page entries retrieve better \u2014 consider splitting into focused entries, then re-test retrieval via ${CONTEXT_TOOL_NAME}.`;
|
|
6889
|
+
}
|
|
6890
|
+
return jsonContent(envelope);
|
|
6891
|
+
}
|
|
6892
|
+
async function handlePrivateKnowledgeInvalidate(_deps, rawInput, _signal, fetchFn = globalThis.fetch) {
|
|
6893
|
+
if (!isInvalidateInput(rawInput)) {
|
|
6894
|
+
throw invalidParams(INVALIDATE_TOOL_NAME, "expected { knowledge_class, source_path }.");
|
|
6895
|
+
}
|
|
6896
|
+
const payload = await callPrivateKnowledge("/api/v1/knowledge/private/invalidate", rawInput, fetchFn);
|
|
6897
|
+
return jsonContent({ tool: INVALIDATE_TOOL_NAME, schema_version: 1, payload });
|
|
6898
|
+
}
|
|
6899
|
+
async function handlePrivateKnowledgeStale(_deps, rawInput, _signal, fetchFn = globalThis.fetch) {
|
|
6900
|
+
if (!isStaleInput(rawInput)) {
|
|
6901
|
+
throw invalidParams(STALE_TOOL_NAME, "expected { optional days, optional limit }.");
|
|
6902
|
+
}
|
|
6903
|
+
const auth = await getCloudAuth(fetchFn);
|
|
6904
|
+
if (!auth.ok) return jsonContent({ tool: STALE_TOOL_NAME, schema_version: 1, payload: { ok: false, reason: auth.reason } });
|
|
6905
|
+
const params = new URLSearchParams();
|
|
6906
|
+
if (rawInput?.days !== void 0) params.set("days", String(Math.trunc(rawInput.days)));
|
|
6907
|
+
if (rawInput?.limit !== void 0) params.set("limit", String(Math.trunc(rawInput.limit)));
|
|
6908
|
+
const qs = params.toString();
|
|
6909
|
+
const response = await fetchFn(`${auth.controlPlaneUrl}/api/v1/knowledge/private/stale${qs ? `?${qs}` : ""}`, {
|
|
6910
|
+
method: "GET",
|
|
6911
|
+
headers: { authorization: `Bearer ${auth.token}` }
|
|
6912
|
+
});
|
|
6913
|
+
const text = await response.text();
|
|
6914
|
+
let parsed;
|
|
6915
|
+
try {
|
|
6916
|
+
parsed = text ? JSON.parse(text) : null;
|
|
6917
|
+
} catch {
|
|
6918
|
+
parsed = null;
|
|
6919
|
+
}
|
|
6920
|
+
const payload = response.status < 200 || response.status >= 300 ? { ok: false, status: response.status, response: parsed ?? text } : parsed;
|
|
6921
|
+
return jsonContent({ tool: STALE_TOOL_NAME, schema_version: 1, payload });
|
|
6922
|
+
}
|
|
6923
|
+
async function handlePrivateKnowledgeContext(_deps, rawInput, _signal, fetchFn = globalThis.fetch) {
|
|
6924
|
+
if (!isContextInput(rawInput)) {
|
|
6925
|
+
throw invalidParams(CONTEXT_TOOL_NAME, "expected { query, optional limit, optional knowledge_class }.");
|
|
6926
|
+
}
|
|
6927
|
+
const payload = await callPrivateKnowledge("/api/v1/knowledge/private/context", rawInput, fetchFn);
|
|
6928
|
+
return jsonContent({ tool: CONTEXT_TOOL_NAME, schema_version: 1, payload });
|
|
6929
|
+
}
|
|
6930
|
+
|
|
6931
|
+
// src/tools/hq/whiteboard.ts
|
|
6932
|
+
init_auth_token_source();
|
|
6933
|
+
init_credential_store();
|
|
6934
|
+
init_common();
|
|
6935
|
+
var POST_TOOL_NAME = "hq_whiteboard_post";
|
|
6936
|
+
var READ_TOOL_NAME = "hq_whiteboard_read";
|
|
6937
|
+
var postDescription = "Post an append-only coordination note to the live AlgoHQ whiteboard. Uses the scoped credential from vo-mcp login; operator and tenant ownership are derived by the server.";
|
|
6938
|
+
var readDescription = "Read recent coordination notes from the caller's live AlgoHQ whiteboard. Uses the scoped credential from vo-mcp login and cannot widen tenant scope.";
|
|
6939
|
+
var postInputSchema = {
|
|
6940
|
+
type: "object",
|
|
6941
|
+
properties: {
|
|
6942
|
+
from: { type: "string", minLength: 1, maxLength: 100, description: "Agent/session display name." },
|
|
6943
|
+
type: { type: "string", minLength: 1, maxLength: 64, description: "Message kind, such as intent, worklog, blocker, or completion." },
|
|
6944
|
+
content: { type: "string", minLength: 1, maxLength: 500, description: "Short coordination note." },
|
|
6945
|
+
targetAgent: { type: "string", maxLength: 100 },
|
|
6946
|
+
tester: { type: "string", maxLength: 100 },
|
|
6947
|
+
tier: { type: "string", maxLength: 32 }
|
|
6948
|
+
},
|
|
6949
|
+
required: ["from", "type", "content"],
|
|
6950
|
+
additionalProperties: false
|
|
6951
|
+
};
|
|
6952
|
+
var readInputSchema = {
|
|
6953
|
+
type: "object",
|
|
6954
|
+
properties: {
|
|
6955
|
+
limit: { type: "integer", minimum: 1, maximum: 100, default: 25 },
|
|
6956
|
+
since: { type: "string", description: "Optional ISO-8601 lower bound." },
|
|
6957
|
+
type: { type: "string", minLength: 1, maxLength: 64 }
|
|
6958
|
+
},
|
|
6959
|
+
additionalProperties: false
|
|
6960
|
+
};
|
|
6961
|
+
function resolveTimeoutMs() {
|
|
6962
|
+
const parsed = Number(process.env["HQ_WHITEBOARD_TIMEOUT_MS"]);
|
|
6963
|
+
return Number.isFinite(parsed) && parsed >= 10 && parsed <= 12e4 ? parsed : 1e4;
|
|
6964
|
+
}
|
|
6965
|
+
function isRecord(value) {
|
|
6966
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6967
|
+
}
|
|
6968
|
+
function onlyKeys(value, allowed) {
|
|
6969
|
+
return Object.keys(value).every((key) => allowed.includes(key));
|
|
6970
|
+
}
|
|
6971
|
+
function isBoundedString(value, min, max) {
|
|
6972
|
+
return typeof value === "string" && value.trim().length >= min && value.trim().length <= max;
|
|
6973
|
+
}
|
|
6974
|
+
function parsePostInput(value) {
|
|
6975
|
+
if (!isRecord(value) || !onlyKeys(value, ["from", "type", "content", "targetAgent", "tester", "tier"])) return null;
|
|
6976
|
+
if (!isBoundedString(value["from"], 1, 100)) return null;
|
|
6977
|
+
if (!isBoundedString(value["type"], 1, 64) || !/^[a-zA-Z0-9_-]+$/.test(value["type"].trim())) return null;
|
|
6978
|
+
if (!isBoundedString(value["content"], 1, 500)) return null;
|
|
6979
|
+
for (const [key, max] of [["targetAgent", 100], ["tester", 100], ["tier", 32]]) {
|
|
6980
|
+
if (value[key] !== void 0 && !isBoundedString(value[key], 0, max)) return null;
|
|
6981
|
+
}
|
|
6982
|
+
return {
|
|
6983
|
+
from: value["from"].trim(),
|
|
6984
|
+
type: value["type"].trim(),
|
|
6985
|
+
content: value["content"].trim(),
|
|
6986
|
+
...typeof value["targetAgent"] === "string" ? { targetAgent: value["targetAgent"].trim() } : {},
|
|
6987
|
+
...typeof value["tester"] === "string" ? { tester: value["tester"].trim() } : {},
|
|
6988
|
+
...typeof value["tier"] === "string" ? { tier: value["tier"].trim() } : {}
|
|
6989
|
+
};
|
|
6990
|
+
}
|
|
6991
|
+
function parseReadInput(value) {
|
|
6992
|
+
if (!isRecord(value) || !onlyKeys(value, ["limit", "since", "type"])) return null;
|
|
6993
|
+
if (value["limit"] !== void 0 && (!Number.isInteger(value["limit"]) || Number(value["limit"]) < 1 || Number(value["limit"]) > 100)) return null;
|
|
6994
|
+
if (value["since"] !== void 0 && (typeof value["since"] !== "string" || Number.isNaN(Date.parse(value["since"])))) return null;
|
|
6995
|
+
if (value["type"] !== void 0 && !isBoundedString(value["type"], 1, 64)) return null;
|
|
6996
|
+
return {
|
|
6997
|
+
...typeof value["limit"] === "number" ? { limit: value["limit"] } : {},
|
|
6998
|
+
...typeof value["since"] === "string" ? { since: value["since"] } : {},
|
|
6999
|
+
...typeof value["type"] === "string" ? { type: value["type"].trim() } : {}
|
|
7000
|
+
};
|
|
7001
|
+
}
|
|
7002
|
+
async function resolveCloud(fetchFn) {
|
|
7003
|
+
const url = process.env["VO_CONTROL_PLANE_URL"]?.trim().replace(/\/$/, "");
|
|
7004
|
+
if (!url) return null;
|
|
7005
|
+
try {
|
|
7006
|
+
const source = createAuthTokenSourceFromEnv(process.env, fetchFn, () => readStoredCredential(process.env));
|
|
7007
|
+
const token = await source?.getToken();
|
|
7008
|
+
return token ? { url, token } : null;
|
|
7009
|
+
} catch {
|
|
7010
|
+
return null;
|
|
5299
7011
|
}
|
|
5300
|
-
return args;
|
|
5301
7012
|
}
|
|
5302
|
-
async function
|
|
5303
|
-
|
|
5304
|
-
|
|
7013
|
+
async function callWhiteboard(method, bodyOrQuery, signal, fetchFn = fetch) {
|
|
7014
|
+
const cloud = await resolveCloud(fetchFn);
|
|
7015
|
+
if (!cloud) {
|
|
7016
|
+
return {
|
|
7017
|
+
ok: false,
|
|
7018
|
+
error: "hq_whiteboard_not_configured",
|
|
7019
|
+
message: "Set VO_CONTROL_PLANE_URL and run vo-mcp login to install a scoped HQ credential."
|
|
7020
|
+
};
|
|
5305
7021
|
}
|
|
5306
|
-
const
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
|
|
5314
|
-
}
|
|
5315
|
-
});
|
|
7022
|
+
const timeoutSignal = AbortSignal.timeout(resolveTimeoutMs());
|
|
7023
|
+
const requestSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
7024
|
+
const query = new URLSearchParams();
|
|
7025
|
+
if (method === "GET") {
|
|
7026
|
+
const input = bodyOrQuery;
|
|
7027
|
+
query.set("limit", String(input.limit ?? 25));
|
|
7028
|
+
if (input.since) query.set("since", input.since);
|
|
7029
|
+
if (input.type) query.set("type", input.type);
|
|
5316
7030
|
}
|
|
5317
|
-
const handoff = readFileSync6(handoffPath, "utf8").slice(0, MAX_HANDOFF_BYTES);
|
|
5318
|
-
const prompt = buildSuccessorPrompt(handoff, rawInput.goal);
|
|
5319
|
-
const logDir = process.env["VO_MCP_SUCCESSOR_LOG_DIR"]?.trim() || join6(homedir4(), ".vo", "successors");
|
|
5320
|
-
mkdirSync3(logDir, { recursive: true });
|
|
5321
|
-
const logPath = join6(logDir, `successor-${Date.now()}.log`);
|
|
5322
|
-
const logFd = openSync(logPath, "a");
|
|
5323
|
-
const child = spawnImpl("claude", buildSuccessorArgs(rawInput.max_turns), {
|
|
5324
|
-
cwd: rawInput.cwd?.trim() || process.cwd(),
|
|
5325
|
-
detached: true,
|
|
5326
|
-
stdio: ["pipe", logFd, logFd],
|
|
5327
|
-
// Windows: `claude` is a .cmd shim — needs a shell to resolve. The prompt
|
|
5328
|
-
// goes via STDIN below, never argv, so the shell never sees it.
|
|
5329
|
-
shell: process.platform === "win32",
|
|
5330
|
-
windowsHide: true
|
|
5331
|
-
});
|
|
5332
|
-
let spawnError = null;
|
|
5333
|
-
child.on("error", (e) => {
|
|
5334
|
-
spawnError = e.message;
|
|
5335
|
-
});
|
|
5336
7031
|
try {
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
7032
|
+
const response = await fetchFn(
|
|
7033
|
+
`${cloud.url}/api/v1/hq/whiteboard/messages${query.size ? `?${query}` : ""}`,
|
|
7034
|
+
{
|
|
7035
|
+
method,
|
|
7036
|
+
headers: {
|
|
7037
|
+
Authorization: `Bearer ${cloud.token}`,
|
|
7038
|
+
...method === "POST" ? { "Content-Type": "application/json" } : {}
|
|
7039
|
+
},
|
|
7040
|
+
...method === "POST" ? { body: JSON.stringify(bodyOrQuery) } : {},
|
|
7041
|
+
signal: requestSignal
|
|
7042
|
+
}
|
|
7043
|
+
);
|
|
7044
|
+
const text = await response.text();
|
|
7045
|
+
let payload;
|
|
7046
|
+
try {
|
|
7047
|
+
payload = JSON.parse(text);
|
|
7048
|
+
} catch {
|
|
7049
|
+
payload = { ok: false, error: "invalid_response", message: text.slice(0, 200) };
|
|
7050
|
+
}
|
|
7051
|
+
if (!response.ok) {
|
|
7052
|
+
return { ok: false, error: "hq_whiteboard_http_error", status: response.status, response: payload };
|
|
7053
|
+
}
|
|
7054
|
+
return payload;
|
|
7055
|
+
} catch (error) {
|
|
7056
|
+
return {
|
|
7057
|
+
ok: false,
|
|
7058
|
+
error: signal?.aborted ? "cancelled" : timeoutSignal.aborted ? "hq_whiteboard_timeout" : "hq_whiteboard_unreachable",
|
|
7059
|
+
message: error instanceof Error ? error.message.slice(0, 200) : String(error).slice(0, 200)
|
|
7060
|
+
};
|
|
5340
7061
|
}
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
7062
|
+
}
|
|
7063
|
+
async function handleHqWhiteboardPost(_deps, rawInput, signal) {
|
|
7064
|
+
const input = parsePostInput(rawInput);
|
|
7065
|
+
if (!input) throw invalidParams(POST_TOOL_NAME, "requires from, type, and 1-500 character content; unknown fields are rejected");
|
|
7066
|
+
return jsonContent(await callWhiteboard("POST", input, signal));
|
|
7067
|
+
}
|
|
7068
|
+
async function handleHqWhiteboardRead(_deps, rawInput, signal) {
|
|
7069
|
+
const input = parseReadInput(rawInput);
|
|
7070
|
+
if (!input) throw invalidParams(READ_TOOL_NAME, "limit must be 1-100, since must be ISO-8601, and unknown fields are rejected");
|
|
7071
|
+
return jsonContent(await callWhiteboard("GET", input, signal));
|
|
5348
7072
|
}
|
|
5349
7073
|
|
|
5350
|
-
// src/tools/
|
|
5351
|
-
|
|
7074
|
+
// src/tools/skills/skill-corpus.ts
|
|
7075
|
+
import { existsSync as existsSync9, statSync as statSync6 } from "node:fs";
|
|
7076
|
+
import { dirname as dirname5, isAbsolute, join as join13, resolve as resolve2 } from "node:path";
|
|
5352
7077
|
|
|
5353
|
-
// src/
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
var
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
"
|
|
5365
|
-
|
|
5366
|
-
|
|
5367
|
-
|
|
7078
|
+
// ../skill-registry/src/loader.ts
|
|
7079
|
+
import { readdirSync as readdirSync6, readFileSync as readFileSync14, statSync as statSync5 } from "node:fs";
|
|
7080
|
+
import { join as join12 } from "node:path";
|
|
7081
|
+
var InvalidSkillFrontmatterError = class extends Error {
|
|
7082
|
+
constructor(skillFile, reason) {
|
|
7083
|
+
super(`Invalid frontmatter in ${skillFile}: ${reason}`);
|
|
7084
|
+
this.skillFile = skillFile;
|
|
7085
|
+
this.reason = reason;
|
|
7086
|
+
}
|
|
7087
|
+
skillFile;
|
|
7088
|
+
reason;
|
|
7089
|
+
name = "InvalidSkillFrontmatterError";
|
|
7090
|
+
};
|
|
7091
|
+
var FRONTMATTER_DELIMITER = "---";
|
|
7092
|
+
function parseFrontmatter(rawInput, sourcePath) {
|
|
7093
|
+
const raw = rawInput.replace(/\r\n/g, "\n");
|
|
7094
|
+
if (!raw.startsWith(`${FRONTMATTER_DELIMITER}
|
|
7095
|
+
`)) {
|
|
7096
|
+
throw new InvalidSkillFrontmatterError(sourcePath, 'file does not start with frontmatter delimiter "---"');
|
|
7097
|
+
}
|
|
7098
|
+
const afterFirst = raw.slice(FRONTMATTER_DELIMITER.length + 1);
|
|
7099
|
+
const closingIdx = afterFirst.indexOf(`
|
|
7100
|
+
${FRONTMATTER_DELIMITER}
|
|
7101
|
+
`);
|
|
7102
|
+
if (closingIdx === -1) {
|
|
7103
|
+
throw new InvalidSkillFrontmatterError(sourcePath, 'missing closing frontmatter delimiter "---"');
|
|
7104
|
+
}
|
|
7105
|
+
const frontmatterText = afterFirst.slice(0, closingIdx);
|
|
7106
|
+
const body = afterFirst.slice(closingIdx + `
|
|
7107
|
+
${FRONTMATTER_DELIMITER}
|
|
7108
|
+
`.length);
|
|
7109
|
+
let name = "";
|
|
7110
|
+
let description23 = "";
|
|
7111
|
+
for (const line of frontmatterText.split("\n")) {
|
|
7112
|
+
const trimmed = line.trim();
|
|
7113
|
+
if (trimmed.length === 0) continue;
|
|
7114
|
+
const colonIdx = trimmed.indexOf(":");
|
|
7115
|
+
if (colonIdx === -1) continue;
|
|
7116
|
+
const key = trimmed.slice(0, colonIdx).trim();
|
|
7117
|
+
const value = trimmed.slice(colonIdx + 1).trim();
|
|
7118
|
+
if (key === "name") name = value;
|
|
7119
|
+
else if (key === "description") description23 = value;
|
|
7120
|
+
}
|
|
7121
|
+
if (name.length === 0) {
|
|
7122
|
+
throw new InvalidSkillFrontmatterError(sourcePath, 'missing required field "name"');
|
|
7123
|
+
}
|
|
7124
|
+
if (description23.length === 0) {
|
|
7125
|
+
throw new InvalidSkillFrontmatterError(sourcePath, 'missing required field "description"');
|
|
7126
|
+
}
|
|
7127
|
+
return { name, description: description23, body };
|
|
7128
|
+
}
|
|
7129
|
+
function loadSkillsFromDir(skillsDir) {
|
|
7130
|
+
const entries = readdirSync6(skillsDir);
|
|
7131
|
+
const skills = [];
|
|
7132
|
+
for (const entry of entries) {
|
|
7133
|
+
const entryPath = join12(skillsDir, entry);
|
|
7134
|
+
let stat;
|
|
7135
|
+
try {
|
|
7136
|
+
stat = statSync5(entryPath);
|
|
7137
|
+
} catch {
|
|
7138
|
+
continue;
|
|
7139
|
+
}
|
|
7140
|
+
if (!stat.isDirectory()) continue;
|
|
7141
|
+
const skillFile = join12(entryPath, "SKILL.md");
|
|
7142
|
+
let raw;
|
|
7143
|
+
try {
|
|
7144
|
+
raw = readFileSync14(skillFile, "utf8");
|
|
7145
|
+
} catch {
|
|
7146
|
+
continue;
|
|
7147
|
+
}
|
|
7148
|
+
const { name, description: description23, body } = parseFrontmatter(raw, skillFile);
|
|
7149
|
+
skills.push({ name, description: description23, body, sourcePath: skillFile });
|
|
7150
|
+
}
|
|
7151
|
+
return [...skills].sort((a, b) => a.name.localeCompare(b.name));
|
|
5368
7152
|
}
|
|
5369
7153
|
|
|
5370
|
-
// src/tools/
|
|
5371
|
-
|
|
5372
|
-
var
|
|
5373
|
-
var
|
|
5374
|
-
var
|
|
7154
|
+
// src/tools/skills/skill-corpus.ts
|
|
7155
|
+
init_common();
|
|
7156
|
+
var LIST_TOOL_NAME = "vo_skill_list";
|
|
7157
|
+
var GET_TOOL_NAME = "vo_skill_get";
|
|
7158
|
+
var listDescription = "List the Algosuite skill corpus (name + trigger description for every skill). Call once near session start to learn which skills exist; then fetch the full instructions for a relevant skill with vo_skill_get. This is the same corpus Claude Code loads natively from .claude/skills \u2014 served over MCP so every vendor works from identical playbooks. Pass refresh:true to re-scan from disk.";
|
|
7159
|
+
var getDescription = "Fetch the full markdown instructions of one Algosuite skill by name. Follow the returned instructions for the current task the same way a native skill invocation would. Use vo_skill_list to discover skill names.";
|
|
7160
|
+
var listInputSchema = {
|
|
5375
7161
|
type: "object",
|
|
5376
7162
|
properties: {
|
|
5377
|
-
|
|
5378
|
-
type: "
|
|
5379
|
-
description:
|
|
5380
|
-
|
|
5381
|
-
|
|
5382
|
-
|
|
7163
|
+
refresh: {
|
|
7164
|
+
type: "boolean",
|
|
7165
|
+
description: "Re-scan the skills directory instead of using the cached corpus."
|
|
7166
|
+
}
|
|
7167
|
+
},
|
|
7168
|
+
required: []
|
|
7169
|
+
};
|
|
7170
|
+
var getInputSchema = {
|
|
7171
|
+
type: "object",
|
|
7172
|
+
properties: {
|
|
7173
|
+
name: {
|
|
5383
7174
|
type: "string",
|
|
5384
|
-
description: "
|
|
7175
|
+
description: "Skill name exactly as returned by vo_skill_list."
|
|
5385
7176
|
}
|
|
5386
7177
|
},
|
|
5387
|
-
|
|
7178
|
+
required: ["name"]
|
|
5388
7179
|
};
|
|
5389
|
-
var
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
const
|
|
5393
|
-
if (
|
|
5394
|
-
|
|
5395
|
-
|
|
7180
|
+
var MAX_WALK_UP_LEVELS = 8;
|
|
7181
|
+
var cachedCorpus = null;
|
|
7182
|
+
function resolveSkillsDir(env = process.env, startDir = process.cwd()) {
|
|
7183
|
+
const override = env.VO_SKILLS_DIR;
|
|
7184
|
+
if (typeof override === "string" && override.length > 0) {
|
|
7185
|
+
const abs = isAbsolute(override) ? override : resolve2(startDir, override);
|
|
7186
|
+
return existsSync9(abs) && statSync6(abs).isDirectory() ? abs : null;
|
|
7187
|
+
}
|
|
7188
|
+
let dir = resolve2(startDir);
|
|
7189
|
+
for (let i = 0; i < MAX_WALK_UP_LEVELS; i += 1) {
|
|
7190
|
+
const candidate = join13(dir, ".claude", "skills");
|
|
7191
|
+
if (existsSync9(candidate) && statSync6(candidate).isDirectory()) return candidate;
|
|
7192
|
+
const parent = dirname5(dir);
|
|
7193
|
+
if (parent === dir) break;
|
|
7194
|
+
dir = parent;
|
|
7195
|
+
}
|
|
7196
|
+
return null;
|
|
5396
7197
|
}
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
|
|
5400
|
-
|
|
5401
|
-
|
|
5402
|
-
|
|
7198
|
+
function loadCorpus() {
|
|
7199
|
+
const skillsDir = resolveSkillsDir();
|
|
7200
|
+
if (skillsDir === null) {
|
|
7201
|
+
return {
|
|
7202
|
+
skills: [],
|
|
7203
|
+
skillsDir: null,
|
|
7204
|
+
unavailableReason: "No skills directory found. Set VO_SKILLS_DIR or run inside a repo with .claude/skills."
|
|
7205
|
+
};
|
|
5403
7206
|
}
|
|
5404
|
-
|
|
7207
|
+
try {
|
|
7208
|
+
return { skills: loadSkillsFromDir(skillsDir), skillsDir, unavailableReason: null };
|
|
7209
|
+
} catch (err) {
|
|
7210
|
+
const message = err instanceof Error ? `${err.name}: ${err.message}` : String(err);
|
|
7211
|
+
return { skills: [], skillsDir, unavailableReason: message };
|
|
7212
|
+
}
|
|
7213
|
+
}
|
|
7214
|
+
function getCorpus(refresh) {
|
|
7215
|
+
if (refresh || cachedCorpus === null) {
|
|
7216
|
+
cachedCorpus = loadCorpus();
|
|
7217
|
+
}
|
|
7218
|
+
return cachedCorpus;
|
|
7219
|
+
}
|
|
7220
|
+
async function handleSkillList(_deps, rawInput) {
|
|
7221
|
+
const input = rawInput ?? {};
|
|
7222
|
+
const refresh = input.refresh === true;
|
|
7223
|
+
const corpus = getCorpus(refresh);
|
|
7224
|
+
return jsonContent({
|
|
7225
|
+
corpus_available: corpus.unavailableReason === null,
|
|
7226
|
+
skills_dir: corpus.skillsDir,
|
|
7227
|
+
unavailable_reason: corpus.unavailableReason,
|
|
7228
|
+
skill_count: corpus.skills.length,
|
|
7229
|
+
skills: corpus.skills.map((s) => ({ name: s.name, description: s.description }))
|
|
7230
|
+
});
|
|
7231
|
+
}
|
|
7232
|
+
async function handleSkillGet(_deps, rawInput) {
|
|
7233
|
+
const input = rawInput ?? {};
|
|
7234
|
+
if (typeof input.name !== "string" || input.name.trim().length === 0) {
|
|
7235
|
+
throw invalidParams(GET_TOOL_NAME, 'input field "name" (non-empty string) is required');
|
|
7236
|
+
}
|
|
7237
|
+
const requested = input.name.trim();
|
|
7238
|
+
const corpus = getCorpus(false);
|
|
7239
|
+
if (corpus.unavailableReason !== null) {
|
|
7240
|
+
return jsonContent({
|
|
7241
|
+
corpus_available: false,
|
|
7242
|
+
unavailable_reason: corpus.unavailableReason,
|
|
7243
|
+
skill: null
|
|
7244
|
+
});
|
|
7245
|
+
}
|
|
7246
|
+
const skill = corpus.skills.find((s) => s.name === requested);
|
|
7247
|
+
if (skill === void 0) {
|
|
5405
7248
|
throw invalidParams(
|
|
5406
|
-
|
|
5407
|
-
`unknown
|
|
7249
|
+
GET_TOOL_NAME,
|
|
7250
|
+
`unknown skill "${requested}". Known skills: ${corpus.skills.map((s) => s.name).join(", ")}`
|
|
5408
7251
|
);
|
|
5409
7252
|
}
|
|
5410
|
-
|
|
5411
|
-
|
|
5412
|
-
|
|
5413
|
-
|
|
5414
|
-
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
|
|
5418
|
-
callableName: CALLABLE_NAME10,
|
|
5419
|
-
adminPath: ADMIN_PATH10,
|
|
5420
|
-
normalizedInput,
|
|
5421
|
-
cloudBody,
|
|
5422
|
-
// Concierge dispatch returns `{ok, mode, routed_from, pack|packs}`,
|
|
5423
|
-
// NOT the callable-proxy `{ok, callable, result}` shape — take the
|
|
5424
|
-
// raw envelope as response_data instead of mandating `.result`.
|
|
5425
|
-
rawEnvelope: true,
|
|
5426
|
-
gateType: CONCIERGE_GATE_TYPE,
|
|
5427
|
-
stubReason: CONCIERGE_STUB_REASON,
|
|
5428
|
-
// Read-only pack fetch — stays live under VO_ADMIN_CALLABLES_READONLY.
|
|
5429
|
-
readOnly: true,
|
|
5430
|
-
deps
|
|
7253
|
+
return jsonContent({
|
|
7254
|
+
corpus_available: true,
|
|
7255
|
+
skill: {
|
|
7256
|
+
name: skill.name,
|
|
7257
|
+
description: skill.description,
|
|
7258
|
+
instructions: skill.body,
|
|
7259
|
+
source_path: skill.sourcePath
|
|
7260
|
+
}
|
|
5431
7261
|
});
|
|
5432
7262
|
}
|
|
5433
7263
|
|
|
5434
7264
|
// src/server.ts
|
|
5435
|
-
init_sync_config();
|
|
5436
7265
|
function buildToolRegistry() {
|
|
5437
7266
|
return {
|
|
5438
7267
|
[TOOL_NAME]: {
|
|
@@ -5610,11 +7439,75 @@ function buildToolRegistry() {
|
|
|
5610
7439
|
inputSchema: inputSchema22
|
|
5611
7440
|
},
|
|
5612
7441
|
handler: handleSyncConfig
|
|
7442
|
+
},
|
|
7443
|
+
[UPSERT_TOOL_NAME]: {
|
|
7444
|
+
definition: {
|
|
7445
|
+
name: UPSERT_TOOL_NAME,
|
|
7446
|
+
description: upsertDescription,
|
|
7447
|
+
inputSchema: upsertInputSchema
|
|
7448
|
+
},
|
|
7449
|
+
handler: handlePrivateKnowledgeUpsert
|
|
7450
|
+
},
|
|
7451
|
+
[CONTEXT_TOOL_NAME]: {
|
|
7452
|
+
definition: {
|
|
7453
|
+
name: CONTEXT_TOOL_NAME,
|
|
7454
|
+
description: contextDescription,
|
|
7455
|
+
inputSchema: contextInputSchema
|
|
7456
|
+
},
|
|
7457
|
+
handler: handlePrivateKnowledgeContext
|
|
7458
|
+
},
|
|
7459
|
+
[INVALIDATE_TOOL_NAME]: {
|
|
7460
|
+
definition: {
|
|
7461
|
+
name: INVALIDATE_TOOL_NAME,
|
|
7462
|
+
description: invalidateDescription,
|
|
7463
|
+
inputSchema: invalidateInputSchema
|
|
7464
|
+
},
|
|
7465
|
+
handler: handlePrivateKnowledgeInvalidate
|
|
7466
|
+
},
|
|
7467
|
+
[STALE_TOOL_NAME]: {
|
|
7468
|
+
definition: {
|
|
7469
|
+
name: STALE_TOOL_NAME,
|
|
7470
|
+
description: staleDescription,
|
|
7471
|
+
inputSchema: staleInputSchema
|
|
7472
|
+
},
|
|
7473
|
+
handler: handlePrivateKnowledgeStale
|
|
7474
|
+
},
|
|
7475
|
+
[POST_TOOL_NAME]: {
|
|
7476
|
+
definition: {
|
|
7477
|
+
name: POST_TOOL_NAME,
|
|
7478
|
+
description: postDescription,
|
|
7479
|
+
inputSchema: postInputSchema
|
|
7480
|
+
},
|
|
7481
|
+
handler: handleHqWhiteboardPost
|
|
7482
|
+
},
|
|
7483
|
+
[READ_TOOL_NAME]: {
|
|
7484
|
+
definition: {
|
|
7485
|
+
name: READ_TOOL_NAME,
|
|
7486
|
+
description: readDescription,
|
|
7487
|
+
inputSchema: readInputSchema
|
|
7488
|
+
},
|
|
7489
|
+
handler: handleHqWhiteboardRead
|
|
7490
|
+
},
|
|
7491
|
+
[LIST_TOOL_NAME]: {
|
|
7492
|
+
definition: {
|
|
7493
|
+
name: LIST_TOOL_NAME,
|
|
7494
|
+
description: listDescription,
|
|
7495
|
+
inputSchema: listInputSchema
|
|
7496
|
+
},
|
|
7497
|
+
handler: handleSkillList
|
|
7498
|
+
},
|
|
7499
|
+
[GET_TOOL_NAME]: {
|
|
7500
|
+
definition: {
|
|
7501
|
+
name: GET_TOOL_NAME,
|
|
7502
|
+
description: getDescription,
|
|
7503
|
+
inputSchema: getInputSchema
|
|
7504
|
+
},
|
|
7505
|
+
handler: handleSkillGet
|
|
5613
7506
|
}
|
|
5614
7507
|
};
|
|
5615
7508
|
}
|
|
5616
7509
|
function createServer(options) {
|
|
5617
|
-
const sessionId = options.sessionId ??
|
|
7510
|
+
const sessionId = options.sessionId ?? randomUUID3();
|
|
5618
7511
|
const mode = createLocalMode();
|
|
5619
7512
|
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
5620
7513
|
const server = new Server(
|
|
@@ -5663,9 +7556,9 @@ function createServer(options) {
|
|
|
5663
7556
|
}
|
|
5664
7557
|
|
|
5665
7558
|
// src/cache/sqlite-cache.ts
|
|
5666
|
-
import { createHash as
|
|
5667
|
-
import { chmodSync as chmodSync3, mkdirSync as
|
|
5668
|
-
import { dirname as
|
|
7559
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
7560
|
+
import { chmodSync as chmodSync3, mkdirSync as mkdirSync7 } from "node:fs";
|
|
7561
|
+
import { dirname as dirname6 } from "node:path";
|
|
5669
7562
|
import { DatabaseSync } from "node:sqlite";
|
|
5670
7563
|
|
|
5671
7564
|
// src/cache/canonicalize.ts
|
|
@@ -5710,7 +7603,7 @@ function normalizeString(s) {
|
|
|
5710
7603
|
function createSqliteCache(options) {
|
|
5711
7604
|
const fileBacked = options.dbPath !== ":memory:";
|
|
5712
7605
|
if (fileBacked) {
|
|
5713
|
-
|
|
7606
|
+
mkdirSync7(dirname6(options.dbPath), { recursive: true, mode: 448 });
|
|
5714
7607
|
}
|
|
5715
7608
|
const versionNamespace = options.cacheVersionNamespace ?? "";
|
|
5716
7609
|
const db = new DatabaseSync(options.dbPath);
|
|
@@ -5741,7 +7634,7 @@ function createSqliteCache(options) {
|
|
|
5741
7634
|
return {
|
|
5742
7635
|
keyFor(toolName, input, opts) {
|
|
5743
7636
|
const canonical = canonicalize(input, opts);
|
|
5744
|
-
const hash =
|
|
7637
|
+
const hash = createHash4("sha256");
|
|
5745
7638
|
if (versionNamespace.length > 0) {
|
|
5746
7639
|
hash.update(versionNamespace);
|
|
5747
7640
|
hash.update("|");
|
|
@@ -5836,7 +7729,7 @@ function createStubRatchetClient() {
|
|
|
5836
7729
|
let m;
|
|
5837
7730
|
while ((m = pat.regex.exec(req.source)) !== null) {
|
|
5838
7731
|
findings.push({
|
|
5839
|
-
line_excerpt:
|
|
7732
|
+
line_excerpt: clip2(m[0], 80),
|
|
5840
7733
|
severity: pat.severity,
|
|
5841
7734
|
code: pat.code,
|
|
5842
7735
|
message: pat.message
|
|
@@ -5868,7 +7761,7 @@ function createStubRatchetClient() {
|
|
|
5868
7761
|
}
|
|
5869
7762
|
};
|
|
5870
7763
|
}
|
|
5871
|
-
function
|
|
7764
|
+
function clip2(s, n) {
|
|
5872
7765
|
return s.length <= n ? s : s.slice(0, n) + "\u2026";
|
|
5873
7766
|
}
|
|
5874
7767
|
function buildSummary2(args) {
|
|
@@ -5879,7 +7772,7 @@ function buildSummary2(args) {
|
|
|
5879
7772
|
// src/consensus/engine-client.ts
|
|
5880
7773
|
init_events_writer();
|
|
5881
7774
|
init_common();
|
|
5882
|
-
import { randomUUID as
|
|
7775
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
5883
7776
|
|
|
5884
7777
|
// src/consensus/null-client.ts
|
|
5885
7778
|
var NULL_CLIENT_DEFAULT_REASON = "consensus-engine-package-pending";
|
|
@@ -5901,6 +7794,60 @@ function createNullConsensusEngineClient(reason = NULL_CLIENT_DEFAULT_REASON) {
|
|
|
5901
7794
|
};
|
|
5902
7795
|
}
|
|
5903
7796
|
|
|
7797
|
+
// src/consensus/meta-model-caller.ts
|
|
7798
|
+
var META_CONSENSUS_MODEL = "muse-spark-1.1";
|
|
7799
|
+
function createMetaModelCaller(options = {}) {
|
|
7800
|
+
void options;
|
|
7801
|
+
return async function callMetaWithMetrics2() {
|
|
7802
|
+
throw new Error(
|
|
7803
|
+
"Muse Spark direct consensus is disabled. Use an explicit sanitized task capsule through the AlgoSuite Model Firewall."
|
|
7804
|
+
);
|
|
7805
|
+
};
|
|
7806
|
+
}
|
|
7807
|
+
var callMetaWithMetrics = createMetaModelCaller();
|
|
7808
|
+
|
|
7809
|
+
// src/consensus/consensus-panel.ts
|
|
7810
|
+
var VO_MCP_CONSENSUS_PANEL = {
|
|
7811
|
+
// claude-opus-5 (2026-07-24). Opus 4.7 was STRICTLY DOMINATED, not merely old:
|
|
7812
|
+
// Opus 5 is $5/$25 per MTok vs Opus 4.7's $15/$75 — a 3x cost cut on this slot,
|
|
7813
|
+
// corroborated by our own catalog (constants/pricing/sciencePricing.ts prices
|
|
7814
|
+
// claude-opus-5 at 0.010 vs claude-opus-4-7 at 0.030) — AND the same 2026-07-24
|
|
7815
|
+
// release note REMOVED fast mode from Opus 4.7 outright: `speed: "fast"` now
|
|
7816
|
+
// returns an error there rather than degrading, unlike the Opus 4.6 removal.
|
|
7817
|
+
// Verified served: GET /v1/models/claude-opus-5 -> HTTP 200 (2026-07-30).
|
|
7818
|
+
//
|
|
7819
|
+
// Claude-5 API safety checked before this swap: Opus 5 rejects `temperature` /
|
|
7820
|
+
// `top_p` / `top_k` and manual `thinking.budget_tokens` with HTTP 400. Neither
|
|
7821
|
+
// the consensus-engine Anthropic adapter nor functions-shared `callAnthropic`
|
|
7822
|
+
// sends any of them, and buildAdaptiveThinking emits `thinking: {type:'adaptive'}`
|
|
7823
|
+
// (the supported form) — so this swap cannot 400.
|
|
7824
|
+
anthropic: "claude-opus-5",
|
|
7825
|
+
// gpt-5.6-terra (GA 2026-07-09; −20% price cut 2026-07-30). NOTE the real IDs
|
|
7826
|
+
// are tiered — `gpt-5.6-sol` / `-terra` / `-luna`; there is NO bare `gpt-5.6`
|
|
7827
|
+
// alias (verified against the served model list, 2026-07-30). Terra is the
|
|
7828
|
+
// cost/capability balance point and the right default for a judgment panel;
|
|
7829
|
+
// Sol is available if verdict quality ever needs it.
|
|
7830
|
+
openai: "gpt-5.6-terra",
|
|
7831
|
+
// gemini-2.5-FLASH (not -pro): flash accepts the default thinkingBudget=0 from
|
|
7832
|
+
// callGeminiWithMetrics; 2.5-pro REJECTS budget 0 ("only works in thinking mode").
|
|
7833
|
+
// Flash is also ~10x cheaper. 2026-06-02.
|
|
7834
|
+
google: "gemini-2.5-flash",
|
|
7835
|
+
deepseek: "deepseek-chat",
|
|
7836
|
+
// Muse Spark identity is owned by meta-model-caller.ts (single source of
|
|
7837
|
+
// truth for the meta slot); re-exported here so the panel stays complete.
|
|
7838
|
+
meta: META_CONSENSUS_MODEL
|
|
7839
|
+
};
|
|
7840
|
+
function getVoMcpConsensusPanel(panel = VO_MCP_CONSENSUS_PANEL) {
|
|
7841
|
+
for (const [provider, modelId] of Object.entries(panel)) {
|
|
7842
|
+
if (typeof modelId !== "string" || modelId.trim().length === 0) {
|
|
7843
|
+
throw new Error(
|
|
7844
|
+
`getVoMcpConsensusPanel: panel slot "${provider}" has a missing or blank model ID`
|
|
7845
|
+
);
|
|
7846
|
+
}
|
|
7847
|
+
}
|
|
7848
|
+
return panel;
|
|
7849
|
+
}
|
|
7850
|
+
|
|
5904
7851
|
// src/consensus/engine-options.ts
|
|
5905
7852
|
var AGREEMENT_GATE_ENV_VAR = "VO_CONSENSUS_AGREEMENT_GATE";
|
|
5906
7853
|
function isTruthyFlag(raw) {
|
|
@@ -5954,6 +7901,14 @@ function shadowEnabled(env) {
|
|
|
5954
7901
|
const norm = raw.trim().toLowerCase();
|
|
5955
7902
|
return !(norm === "0" || norm === "false" || norm === "no" || norm === "off" || norm === "");
|
|
5956
7903
|
}
|
|
7904
|
+
var MIN_RESPONDERS_ENV_VAR = "VO_CONSENSUS_MIN_RESPONDERS";
|
|
7905
|
+
function resolveMinResponders(env) {
|
|
7906
|
+
const raw = (env ?? {})[MIN_RESPONDERS_ENV_VAR];
|
|
7907
|
+
if (raw === void 0 || raw.trim() === "") return 2;
|
|
7908
|
+
const parsed = Number.parseInt(raw.trim(), 10);
|
|
7909
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return void 0;
|
|
7910
|
+
return parsed;
|
|
7911
|
+
}
|
|
5957
7912
|
function mapShadowSynthesis(s) {
|
|
5958
7913
|
if (s === void 0) return void 0;
|
|
5959
7914
|
return {
|
|
@@ -6103,9 +8058,11 @@ function createEngineConsensusClient(options) {
|
|
|
6103
8058
|
...options.agreement_gate_enabled !== void 0 ? { configEnabled: options.agreement_gate_enabled } : {},
|
|
6104
8059
|
...options.env !== void 0 ? { env: options.env } : {}
|
|
6105
8060
|
});
|
|
8061
|
+
const minResponders = resolveMinResponders(options.env);
|
|
6106
8062
|
const engineOptions = {
|
|
6107
8063
|
panel,
|
|
6108
8064
|
...options.per_model_timeout_ms !== void 0 ? { per_model_timeout_ms: options.per_model_timeout_ms } : {},
|
|
8065
|
+
...minResponders !== void 0 ? { min_responders: minResponders } : {},
|
|
6109
8066
|
...agreementGate !== void 0 ? { agreement_gate: agreementGate } : {},
|
|
6110
8067
|
// Stage A7-shadow: run the adaptive verdict alongside the live one for grading.
|
|
6111
8068
|
// Cheap (pure log-odds over already-fetched verdicts; no extra model calls),
|
|
@@ -6158,8 +8115,13 @@ function createEngineConsensusClient(options) {
|
|
|
6158
8115
|
synthesized_verdict: response.synthesized_verdict,
|
|
6159
8116
|
per_model_verdicts: response.per_model_verdicts,
|
|
6160
8117
|
degraded: response.degraded,
|
|
8118
|
+
...response.quorum_failed === true ? { quorum_failed: true } : {},
|
|
6161
8119
|
duration_ms: response.duration_ms,
|
|
6162
8120
|
engine_version: response.engine_version,
|
|
8121
|
+
// Cumulative cross-round inference usage (B44-3). Absent when no panel
|
|
8122
|
+
// member reported usage; forwarded verbatim — the aggregator prefers it
|
|
8123
|
+
// over summing final-round verdicts (which under-reports deliberation).
|
|
8124
|
+
...response.token_usage !== void 0 ? { token_usage: response.token_usage } : {},
|
|
6163
8125
|
// Phase 2 Lane D-1 — forward escalation signal when present. The
|
|
6164
8126
|
// source-grounded layer's own escalation (from the citation grade)
|
|
6165
8127
|
// takes precedence when set, else the synthesizer's.
|
|
@@ -6169,6 +8131,10 @@ function createEngineConsensusClient(options) {
|
|
|
6169
8131
|
...mapFanOutDiagnostics(response.fan_out_diagnostics) !== void 0 ? { fan_out_diagnostics: mapFanOutDiagnostics(response.fan_out_diagnostics) } : {},
|
|
6170
8132
|
// Stage A7-shadow — adaptive-vs-incumbent comparison (PII-free; present iff shadow on).
|
|
6171
8133
|
...mapShadowSynthesis(response.shadow_synthesis) !== void 0 ? { shadow_synthesis: mapShadowSynthesis(response.shadow_synthesis) } : {},
|
|
8134
|
+
// Critique-uptake (2026-07-20 red-team fix) — verifier-critique
|
|
8135
|
+
// visibility report; previously computed by the engine on every
|
|
8136
|
+
// call but dropped at this boundary.
|
|
8137
|
+
...response.critique_uptake !== void 0 ? { critique_uptake: response.critique_uptake } : {},
|
|
6172
8138
|
// Source-grounded additive outputs (Tier-4 features).
|
|
6173
8139
|
...useSourceGrounded ? { source_grounded: true } : {},
|
|
6174
8140
|
...sourceExtras?.citation_grade !== void 0 ? { citation_grade: sourceExtras.citation_grade } : {},
|
|
@@ -6184,25 +8150,12 @@ function createEngineConsensusClient(options) {
|
|
|
6184
8150
|
}
|
|
6185
8151
|
};
|
|
6186
8152
|
}
|
|
6187
|
-
var DEFAULT_MODELS =
|
|
6188
|
-
// These ids match the strategic-roadmap §4 `newsStandard` / `newsDeep` panel
|
|
6189
|
-
// intent — current production model ids. Per handoff §C-3 these MUST come
|
|
6190
|
-
// from `CONSENSUS_PANELS` in `functions-shared/shared-model-resolvers.ts`
|
|
6191
|
-
// for V1; placeholder defaults here keep Phase 2 Lane A non-blocking.
|
|
6192
|
-
anthropic: "claude-opus-4-7",
|
|
6193
|
-
openai: "gpt-5",
|
|
6194
|
-
// gemini-2.5-FLASH (not -pro): flash accepts the default thinkingBudget=0 from
|
|
6195
|
-
// callGeminiWithMetrics; 2.5-pro REJECTS budget 0 ("only works in thinking mode").
|
|
6196
|
-
// Flash is also ~10x cheaper. 2026-06-02.
|
|
6197
|
-
google: "gemini-2.5-flash",
|
|
6198
|
-
deepseek: "deepseek-chat"
|
|
6199
|
-
};
|
|
8153
|
+
var DEFAULT_MODELS = getVoMcpConsensusPanel();
|
|
6200
8154
|
function probeProviders(env = process.env) {
|
|
6201
8155
|
const out = [];
|
|
6202
8156
|
if ((env["ANTHROPIC_API_KEY"] ?? "").trim().length > 0) out.push("anthropic");
|
|
6203
8157
|
if ((env["OPENAI_API_KEY"] ?? "").trim().length > 0) out.push("openai");
|
|
6204
8158
|
if ((env["GOOGLE_API_KEY"] ?? "").trim().length > 0) out.push("google");
|
|
6205
|
-
if ((env["DEEPSEEK_API_KEY"] ?? "").trim().length > 0) out.push("deepseek");
|
|
6206
8159
|
return out;
|
|
6207
8160
|
}
|
|
6208
8161
|
async function loadFactoryAndCallers(injectedEngine, injectedShared) {
|
|
@@ -6247,14 +8200,12 @@ async function tryCreateEngineConsensusClientFromEnvAsync(options = {}) {
|
|
|
6247
8200
|
const callerByProvider = {
|
|
6248
8201
|
anthropic: loaded.shared.callAnthropicWithMetrics,
|
|
6249
8202
|
openai: loaded.shared.callOpenAIWithMetrics,
|
|
6250
|
-
google: loaded.shared.callGeminiWithMetrics
|
|
6251
|
-
deepseek: loaded.shared.callDeepSeekWithMetrics
|
|
8203
|
+
google: loaded.shared.callGeminiWithMetrics
|
|
6252
8204
|
};
|
|
6253
8205
|
const modelByProvider = {
|
|
6254
8206
|
anthropic: options.models?.anthropic ?? DEFAULT_MODELS.anthropic,
|
|
6255
8207
|
openai: options.models?.openai ?? DEFAULT_MODELS.openai,
|
|
6256
|
-
google: options.models?.google ?? DEFAULT_MODELS.google
|
|
6257
|
-
deepseek: options.models?.deepseek ?? DEFAULT_MODELS.deepseek
|
|
8208
|
+
google: options.models?.google ?? DEFAULT_MODELS.google
|
|
6258
8209
|
};
|
|
6259
8210
|
const panel = [];
|
|
6260
8211
|
for (const p of providers) {
|
|
@@ -6296,7 +8247,7 @@ function tryCreateEngineConsensusClientFromEnv(options = {}) {
|
|
|
6296
8247
|
}
|
|
6297
8248
|
|
|
6298
8249
|
// src/consensus/moat-client.ts
|
|
6299
|
-
import { randomUUID as
|
|
8250
|
+
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
6300
8251
|
|
|
6301
8252
|
// src/consensus/client.ts
|
|
6302
8253
|
var CANCELLED_REASON = "cancelled";
|
|
@@ -6348,7 +8299,7 @@ function createMoatConsensusClient(opts) {
|
|
|
6348
8299
|
request.signal?.addEventListener("abort", onAbort, { once: true });
|
|
6349
8300
|
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
6350
8301
|
const body = JSON.stringify({
|
|
6351
|
-
task_id:
|
|
8302
|
+
task_id: randomUUID5(),
|
|
6352
8303
|
gate_type: request.gate_type,
|
|
6353
8304
|
excerpt: request.prompt,
|
|
6354
8305
|
...request.system_prompt ? { question: request.system_prompt } : {},
|
|
@@ -6377,8 +8328,10 @@ function createMoatConsensusClient(opts) {
|
|
|
6377
8328
|
}
|
|
6378
8329
|
const agreeing = normalizeAgreeing(parsed.models_agreeing);
|
|
6379
8330
|
const reasoning = agreeing !== void 0 ? `${parsed.reason} (${agreeing} models agreeing)` : parsed.reason;
|
|
8331
|
+
const receiptId = typeof parsed.decision_id === "string" && parsed.decision_id.trim().length > 0 ? parsed.decision_id.trim().slice(0, 120) : null;
|
|
6380
8332
|
return {
|
|
6381
8333
|
ok: true,
|
|
8334
|
+
...receiptId ? { receipt_id: receiptId } : {},
|
|
6382
8335
|
synthesized_verdict: {
|
|
6383
8336
|
verdict: parsed.approved ? "pass" : "fail",
|
|
6384
8337
|
confidence: normalizeConfidence(parsed.confidence),
|
|
@@ -6419,6 +8372,73 @@ function tryCreateMoatConsensusClientFromEnv(env = process.env, fetchFn) {
|
|
|
6419
8372
|
});
|
|
6420
8373
|
}
|
|
6421
8374
|
|
|
8375
|
+
// src/consensus/fallback-client.ts
|
|
8376
|
+
var MIN_VALID_LOCAL_VERDICTS = 2;
|
|
8377
|
+
var INSUFFICIENT_LOCAL_VERDICTS_REASON = "local-panel-insufficient-valid-verdicts";
|
|
8378
|
+
function createConsensusFallbackClient(primary, fallback, options = {}) {
|
|
8379
|
+
return {
|
|
8380
|
+
async run(request) {
|
|
8381
|
+
const primaryResult = await primary.run(request);
|
|
8382
|
+
if (request.signal?.aborted || !primaryResult.ok && primaryResult.reason === CANCELLED_REASON) {
|
|
8383
|
+
return primaryResult;
|
|
8384
|
+
}
|
|
8385
|
+
if (primaryResult.ok) {
|
|
8386
|
+
const validVerdicts = primaryResult.per_model_verdicts.filter(
|
|
8387
|
+
(verdict) => verdict.verdict !== "error"
|
|
8388
|
+
);
|
|
8389
|
+
if (validVerdicts.length >= MIN_VALID_LOCAL_VERDICTS) return primaryResult;
|
|
8390
|
+
options.onFallback?.(INSUFFICIENT_LOCAL_VERDICTS_REASON);
|
|
8391
|
+
return fallback.run(request);
|
|
8392
|
+
}
|
|
8393
|
+
options.onFallback?.(primaryResult.reason);
|
|
8394
|
+
return fallback.run(request);
|
|
8395
|
+
}
|
|
8396
|
+
};
|
|
8397
|
+
}
|
|
8398
|
+
|
|
8399
|
+
// src/consensus/local-credential-env.ts
|
|
8400
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
8401
|
+
var require2 = createRequire2(import.meta.url);
|
|
8402
|
+
var KEY_SERVICE = "algosuite-vo";
|
|
8403
|
+
var MOAT_ENTITLEMENT_KEYCHAIN_ACCOUNT = "moat-api-key";
|
|
8404
|
+
var KEYCHAIN_TARGETS = [
|
|
8405
|
+
{ account: "anthropic-api-key", envVar: "ANTHROPIC_API_KEY" },
|
|
8406
|
+
{ account: "openai-api-key", envVar: "OPENAI_API_KEY" },
|
|
8407
|
+
{ account: "meta-api-key", envVar: "MODEL_API_KEY" },
|
|
8408
|
+
// The cloud-consensus entitlement (ADR-002 moat). Read here so an npm-installed
|
|
8409
|
+
// runner — where the local engine package never exists — still has a working
|
|
8410
|
+
// consensus path without an operator plumbing a secret into the daemon's env.
|
|
8411
|
+
{ account: MOAT_ENTITLEMENT_KEYCHAIN_ACCOUNT, envVar: "VO_ENTITLEMENT_TOKEN" }
|
|
8412
|
+
];
|
|
8413
|
+
function loadEntryCtor() {
|
|
8414
|
+
try {
|
|
8415
|
+
return require2("@napi-rs/keyring").Entry ?? null;
|
|
8416
|
+
} catch {
|
|
8417
|
+
return null;
|
|
8418
|
+
}
|
|
8419
|
+
}
|
|
8420
|
+
function readKey(EntryCtor, account) {
|
|
8421
|
+
try {
|
|
8422
|
+
return new EntryCtor(KEY_SERVICE, account).getPassword()?.trim() || null;
|
|
8423
|
+
} catch {
|
|
8424
|
+
return null;
|
|
8425
|
+
}
|
|
8426
|
+
}
|
|
8427
|
+
function withLocalConsensusCredentials(baseEnv = process.env, options = {}) {
|
|
8428
|
+
const env = { ...baseEnv };
|
|
8429
|
+
if (!env.OPENAI_API_KEY?.trim() && env.CODEX_API_KEY?.trim()) {
|
|
8430
|
+
env.OPENAI_API_KEY = env.CODEX_API_KEY;
|
|
8431
|
+
}
|
|
8432
|
+
const EntryCtor = options.EntryCtor === void 0 ? loadEntryCtor() : options.EntryCtor;
|
|
8433
|
+
if (!EntryCtor) return env;
|
|
8434
|
+
for (const target of KEYCHAIN_TARGETS) {
|
|
8435
|
+
if (env[target.envVar]?.trim()) continue;
|
|
8436
|
+
const key = readKey(EntryCtor, target.account);
|
|
8437
|
+
if (key) env[target.envVar] = key;
|
|
8438
|
+
}
|
|
8439
|
+
return env;
|
|
8440
|
+
}
|
|
8441
|
+
|
|
6422
8442
|
// src/cloud/login.ts
|
|
6423
8443
|
init_credential_store();
|
|
6424
8444
|
import { createServer as createServer2 } from "node:http";
|
|
@@ -6453,7 +8473,7 @@ function processCapture(rawBody, expectedState, store) {
|
|
|
6453
8473
|
};
|
|
6454
8474
|
}
|
|
6455
8475
|
function captureHtml() {
|
|
6456
|
-
return `<!doctype html><html><head><meta charset="utf-8"><title>
|
|
8476
|
+
return `<!doctype html><html><head><meta charset="utf-8"><title>AlgoHQ login</title></head>
|
|
6457
8477
|
<body style="font-family:system-ui;max-width:32rem;margin:4rem auto;text-align:center">
|
|
6458
8478
|
<h2 id="m">Completing sign-in\u2026</h2>
|
|
6459
8479
|
<script>
|
|
@@ -6469,7 +8489,7 @@ function captureHtml() {
|
|
|
6469
8489
|
function defaultOpenBrowser(url) {
|
|
6470
8490
|
const platform = process.platform;
|
|
6471
8491
|
if (platform === "win32") {
|
|
6472
|
-
spawn2("
|
|
8492
|
+
spawn2("rundll32", ["url.dll,FileProtocolHandler", url], { detached: true, stdio: "ignore" }).unref();
|
|
6473
8493
|
} else if (platform === "darwin") {
|
|
6474
8494
|
spawn2("open", [url], { detached: true, stdio: "ignore" }).unref();
|
|
6475
8495
|
} else {
|
|
@@ -6484,7 +8504,7 @@ async function runLogin(opts = {}) {
|
|
|
6484
8504
|
const nowIso = opts.nowIso ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
6485
8505
|
const openBrowser = opts.openBrowser ?? defaultOpenBrowser;
|
|
6486
8506
|
const state = randomBytes(32).toString("base64url");
|
|
6487
|
-
return new Promise((
|
|
8507
|
+
return new Promise((resolve3, reject) => {
|
|
6488
8508
|
let settled = false;
|
|
6489
8509
|
const finish = (err, result) => {
|
|
6490
8510
|
if (settled) return;
|
|
@@ -6492,7 +8512,7 @@ async function runLogin(opts = {}) {
|
|
|
6492
8512
|
clearTimeout(timer);
|
|
6493
8513
|
server.close();
|
|
6494
8514
|
if (err) reject(err);
|
|
6495
|
-
else
|
|
8515
|
+
else resolve3(result);
|
|
6496
8516
|
};
|
|
6497
8517
|
const server = createServer2((req, res) => {
|
|
6498
8518
|
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
@@ -6540,7 +8560,7 @@ async function runLogin(opts = {}) {
|
|
|
6540
8560
|
result = { ...capt.email ? { email: capt.email } : {}, credentialPath: path3 };
|
|
6541
8561
|
}
|
|
6542
8562
|
res.writeHead(outcome.httpStatus, { "content-type": "text/html; charset=utf-8" });
|
|
6543
|
-
res.end(outcome.ok ? "<h2>
|
|
8563
|
+
res.end(outcome.ok ? "<h2>AlgoHQ login complete \u2014 you can close this tab.</h2>" : `<h2>Login failed: ${outcome.error}</h2>`);
|
|
6544
8564
|
finish(outcome.ok ? null : new Error(outcome.error ?? "login failed"), result);
|
|
6545
8565
|
})();
|
|
6546
8566
|
});
|
|
@@ -6607,7 +8627,7 @@ init_common();
|
|
|
6607
8627
|
function defaultCacheDbPath() {
|
|
6608
8628
|
const env = process.env["VO_MCP_DB_PATH"];
|
|
6609
8629
|
if (env && env.length > 0) return env;
|
|
6610
|
-
return
|
|
8630
|
+
return join14(homedir8(), ".claude", "vo-mcp-cache.db");
|
|
6611
8631
|
}
|
|
6612
8632
|
async function probeEngineVersion() {
|
|
6613
8633
|
try {
|
|
@@ -6682,11 +8702,25 @@ async function main() {
|
|
|
6682
8702
|
const ratchets = createStubRatchetClient();
|
|
6683
8703
|
const testModule = process.env["VO_MCP_TEST_ENGINE_MODULE"];
|
|
6684
8704
|
const testClient = testModule !== void 0 && testModule.length > 0 ? await loadTestEngineClient(testModule) : null;
|
|
6685
|
-
const
|
|
6686
|
-
|
|
6687
|
-
|
|
8705
|
+
const localEnv = withLocalConsensusCredentials();
|
|
8706
|
+
const localProviders = probeProviders(localEnv);
|
|
8707
|
+
for (const key of ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GOOGLE_API_KEY", "DEEPSEEK_API_KEY", "MODEL_API_KEY"]) {
|
|
8708
|
+
if (!process.env[key] && localEnv[key]) process.env[key] = localEnv[key];
|
|
8709
|
+
}
|
|
8710
|
+
const localConsensus = tryCreateEngineConsensusClientFromEnv({ envSource: localEnv });
|
|
8711
|
+
const cloudConsensus = testClient ? null : tryCreateMoatConsensusClientFromEnv(localEnv);
|
|
8712
|
+
let consensus = testClient ?? localConsensus;
|
|
8713
|
+
if (!testClient && cloudConsensus && localProviders.length >= 2) {
|
|
8714
|
+
console.error(`[vo-mcp] local-first consensus active (${localProviders.join(", ")}); cloud moat is fallback-only`);
|
|
8715
|
+
consensus = createConsensusFallbackClient(localConsensus, cloudConsensus, {
|
|
8716
|
+
onFallback: (reason) => console.error(`[vo-mcp] local consensus unavailable (${reason}); using cloud moat fallback`)
|
|
8717
|
+
});
|
|
8718
|
+
} else if (!testClient && cloudConsensus) {
|
|
8719
|
+
console.error("[vo-mcp] fewer than 2 linked local providers; cloud moat consensus active");
|
|
8720
|
+
consensus = cloudConsensus;
|
|
8721
|
+
} else if (!testClient) {
|
|
8722
|
+
console.error("[vo-mcp] no VO_ENTITLEMENT_TOKEN (env or keychain via `vo-mcp set-key --provider moat`); consensus tools depend on a locally installed engine and will report engine-unavailable on an npm-installed runner");
|
|
6688
8723
|
}
|
|
6689
|
-
const consensus = testClient ?? cloudConsensus ?? tryCreateEngineConsensusClientFromEnv();
|
|
6690
8724
|
let adminCallables = null;
|
|
6691
8725
|
try {
|
|
6692
8726
|
adminCallables = buildAdminCallableClientFromEnv();
|
|
@@ -6722,7 +8756,7 @@ async function main() {
|
|
|
6722
8756
|
}
|
|
6723
8757
|
if (process.argv[2] === "login") {
|
|
6724
8758
|
const controlPlaneUrl = process.env["VO_CONTROL_PLANE_URL"]?.trim();
|
|
6725
|
-
const credentialLabel = `vo-mcp-cli@${
|
|
8759
|
+
const credentialLabel = `vo-mcp-cli@${hostname2()}`.slice(0, 200);
|
|
6726
8760
|
runLogin(
|
|
6727
8761
|
controlPlaneUrl ? {
|
|
6728
8762
|
exchange: (refreshToken, apiKey) => exchangeForVoCredential({ refreshToken, apiKey, controlPlaneUrl, label: credentialLabel })
|
|
@@ -6731,35 +8765,53 @@ if (process.argv[2] === "login") {
|
|
|
6731
8765
|
console.error(`[vo-mcp] login successful${r.email ? ` as ${r.email}` : ""}. Credential stored at ${r.credentialPath}.`);
|
|
6732
8766
|
console.error("[vo-mcp] You can now remove VO_CONTROL_PLANE_ADMIN_TOKEN (the god-token) from your MCP config.");
|
|
6733
8767
|
console.error("[vo-mcp] NOTE: per-user auth requires VO_OPERATOR_ALLOWED_EMAILS (with your email) on the deployed control-plane.");
|
|
6734
|
-
process.exit(0);
|
|
6735
8768
|
}).catch((err) => {
|
|
6736
8769
|
console.error("[vo-mcp] login failed:", err instanceof Error ? err.message : String(err));
|
|
6737
|
-
process.
|
|
8770
|
+
process.exitCode = 1;
|
|
6738
8771
|
});
|
|
6739
8772
|
} else if (process.argv[2] === "sync") {
|
|
6740
8773
|
const action = process.argv[3];
|
|
6741
8774
|
if (action !== "push" && action !== "pull") {
|
|
6742
|
-
console.error("[vo-mcp] usage: vo-mcp sync <push|pull> [--cwd <path>]");
|
|
8775
|
+
console.error("[vo-mcp] usage: vo-mcp sync <push|pull> [--cwd <path>] [--lock-wait-ms <n>]");
|
|
6743
8776
|
process.exit(2);
|
|
6744
8777
|
}
|
|
6745
8778
|
const cwdFlag = process.argv.indexOf("--cwd");
|
|
6746
8779
|
const cwd = cwdFlag >= 0 && typeof process.argv[cwdFlag + 1] === "string" ? process.argv[cwdFlag + 1] : process.cwd();
|
|
6747
|
-
const
|
|
8780
|
+
const waitFlag = process.argv.indexOf("--lock-wait-ms");
|
|
8781
|
+
const parsedWait = waitFlag >= 0 ? Number(process.argv[waitFlag + 1]) : Number.NaN;
|
|
8782
|
+
const lockOptions = Number.isFinite(parsedWait) && parsedWait >= 0 ? { waitMs: parsedWait } : {};
|
|
8783
|
+
const sessionId = randomUUID6();
|
|
8784
|
+
const appendSyncLog = async (line) => {
|
|
8785
|
+
try {
|
|
8786
|
+
const { appendFileSync: appendFileSync2, mkdirSync: mkdirSync8 } = await import("node:fs");
|
|
8787
|
+
const { join: join15 } = await import("node:path");
|
|
8788
|
+
const { homedir: homedir9 } = await import("node:os");
|
|
8789
|
+
const dir = join15(homedir9(), ".claude");
|
|
8790
|
+
mkdirSync8(dir, { recursive: true });
|
|
8791
|
+
appendFileSync2(join15(dir, "vo-mcp-sync.log"), `${line}
|
|
8792
|
+
`, "utf8");
|
|
8793
|
+
} catch {
|
|
8794
|
+
}
|
|
8795
|
+
};
|
|
6748
8796
|
Promise.resolve().then(() => (init_sync_config(), sync_config_exports)).then(async ({ runMemorySync: runMemorySync2, isNoopSyncReason: isNoopSyncReason2 }) => {
|
|
6749
|
-
const r = await runMemorySync2(action, cwd, sessionId);
|
|
8797
|
+
const r = await runMemorySync2(action, cwd, sessionId, void 0, lockOptions);
|
|
8798
|
+
const stamp = `${sessionId} ${action}`;
|
|
6750
8799
|
if (r.synced) {
|
|
6751
8800
|
console.error(`[vo-mcp] sync ${action} ok: ${JSON.stringify(r)}`);
|
|
6752
|
-
|
|
6753
|
-
}
|
|
6754
|
-
if (isNoopSyncReason2(r.reason)) {
|
|
8801
|
+
await appendSyncLog(`ok ${stamp} ${JSON.stringify(r)}`);
|
|
8802
|
+
} else if (isNoopSyncReason2(r.reason)) {
|
|
6755
8803
|
console.error(`[vo-mcp] sync ${action} skipped: ${r.reason}`);
|
|
6756
|
-
|
|
8804
|
+
await appendSyncLog(`skip ${stamp} ${r.reason ?? ""}`);
|
|
8805
|
+
} else {
|
|
8806
|
+
console.error(`[vo-mcp] sync ${action} failed: ${r.reason}`);
|
|
8807
|
+
await appendSyncLog(`FAIL ${stamp} ${r.reason ?? ""}`);
|
|
8808
|
+
process.exitCode = 1;
|
|
6757
8809
|
}
|
|
6758
|
-
|
|
6759
|
-
|
|
6760
|
-
|
|
6761
|
-
|
|
6762
|
-
process.
|
|
8810
|
+
}).catch(async (err) => {
|
|
8811
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
8812
|
+
console.error("[vo-mcp] sync fatal:", message);
|
|
8813
|
+
await appendSyncLog(`FATAL ${sessionId} ${action} ${message}`);
|
|
8814
|
+
process.exitCode = 1;
|
|
6763
8815
|
});
|
|
6764
8816
|
} else {
|
|
6765
8817
|
main().catch((err) => {
|