@algosuite/vo-mcp 0.2.0-beta.7 → 0.2.0-beta.71
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 +29 -3
- package/bin/vo-mcp +6 -3
- package/dist/agent-auth-probe-cli.mjs +1754 -0
- package/dist/autostart-cli.js +118 -60
- package/dist/autostart-cli.js.map +1 -2
- package/dist/ci/check-local-pr-overlap.js +107644 -0
- package/dist/cli.js +2905 -515
- package/dist/cli.js.map +3 -4
- package/dist/index.js +2495 -370
- package/dist/index.js.map +3 -4
- package/dist/install-cli.js +660 -115
- package/dist/install-cli.js.map +3 -4
- package/dist/login-cli.js +4 -4
- package/dist/login-cli.js.map +1 -2
- package/dist/pair-cli.js +1 -1
- package/dist/pair-cli.js.map +1 -2
- package/dist/runner-cli.js +14594 -3254
- package/dist/runner-cli.js.map +3 -4
- package/dist/runner-supervisor.js +3780 -0
- package/dist/runner-supervisor.js.map +6 -0
- package/dist/set-key-cli.js +82 -8
- package/dist/set-key-cli.js.map +1 -2
- package/dist/supervisor-credential-helper.js +233 -0
- package/dist/supervisor-credential-helper.js.map +6 -0
- package/dist/thresholds.json +64 -0
- package/dist/update-cli.js +75 -12
- package/dist/update-cli.js.map +3 -4
- package/package.json +5 -3
package/dist/cli.js
CHANGED
|
@@ -203,24 +203,24 @@ function defaultOverridePath() {
|
|
|
203
203
|
return join2(homedir(), ".claude", "vo-arch-defaults.local.json");
|
|
204
204
|
}
|
|
205
205
|
function loadTenantOverride(opts = {}) {
|
|
206
|
-
const
|
|
207
|
-
if (!existsSync2(
|
|
206
|
+
const path4 = opts.path ?? defaultOverridePath();
|
|
207
|
+
if (!existsSync2(path4)) {
|
|
208
208
|
return { override: null, source_path: null };
|
|
209
209
|
}
|
|
210
|
-
const raw = readFileSync2(
|
|
210
|
+
const raw = readFileSync2(path4, "utf8");
|
|
211
211
|
let parsed;
|
|
212
212
|
try {
|
|
213
213
|
parsed = JSON.parse(raw);
|
|
214
214
|
} catch (err) {
|
|
215
215
|
const m = err instanceof Error ? err.message : String(err);
|
|
216
|
-
throw new Error(`vo-arch-defaults: invalid JSON in override ${
|
|
216
|
+
throw new Error(`vo-arch-defaults: invalid JSON in override ${path4}: ${m}`, { cause: err });
|
|
217
217
|
}
|
|
218
218
|
try {
|
|
219
219
|
const override = parseOverride(parsed);
|
|
220
|
-
return { override, source_path:
|
|
220
|
+
return { override, source_path: path4 };
|
|
221
221
|
} catch (err) {
|
|
222
222
|
const m = err instanceof Error ? err.message : String(err);
|
|
223
|
-
throw new Error(`vo-arch-defaults: override schema validation failed for ${
|
|
223
|
+
throw new Error(`vo-arch-defaults: override schema validation failed for ${path4}: ${m}`, { cause: err });
|
|
224
224
|
}
|
|
225
225
|
}
|
|
226
226
|
var init_load_override = __esm({
|
|
@@ -371,9 +371,9 @@ function globToRegExp(glob) {
|
|
|
371
371
|
}
|
|
372
372
|
return new RegExp("^" + out + "$");
|
|
373
373
|
}
|
|
374
|
-
function matchesAnyGlob(
|
|
374
|
+
function matchesAnyGlob(path4, globs) {
|
|
375
375
|
for (const g of globs) {
|
|
376
|
-
if (globToRegExp(g).test(
|
|
376
|
+
if (globToRegExp(g).test(path4)) return true;
|
|
377
377
|
}
|
|
378
378
|
return false;
|
|
379
379
|
}
|
|
@@ -1023,7 +1023,16 @@ function assertWithinByteCap(toolName, fieldName, value, maxBytes) {
|
|
|
1023
1023
|
);
|
|
1024
1024
|
}
|
|
1025
1025
|
}
|
|
1026
|
+
function subjectFromEnv(env = process.env) {
|
|
1027
|
+
const code_task_id = (env["VO_CODE_TASK_ID"] ?? "").trim().slice(0, 120);
|
|
1028
|
+
const repo = (env["VO_CODE_TASK_REPO"] ?? "").trim();
|
|
1029
|
+
const subject = {};
|
|
1030
|
+
if (code_task_id) subject.code_task_id = code_task_id;
|
|
1031
|
+
if (repo && REPO_RE.test(repo) && repo.length <= 200) subject.repo = repo;
|
|
1032
|
+
return subject.code_task_id || subject.repo ? subject : null;
|
|
1033
|
+
}
|
|
1026
1034
|
function buildBaseEvent(args) {
|
|
1035
|
+
const subject = args.subject === void 0 ? subjectFromEnv() : args.subject;
|
|
1027
1036
|
return {
|
|
1028
1037
|
schema_version: 1,
|
|
1029
1038
|
event_id: args.eventId ?? randomUUID(),
|
|
@@ -1049,7 +1058,12 @@ function buildBaseEvent(args) {
|
|
|
1049
1058
|
downstream_outcome: null,
|
|
1050
1059
|
vo_mcp_version: VO_MCP_VERSION,
|
|
1051
1060
|
consensus_engine_version: null,
|
|
1052
|
-
cache_hit: false
|
|
1061
|
+
cache_hit: false,
|
|
1062
|
+
// OMIT the key when there is no subject (rather than `subject: null`): the
|
|
1063
|
+
// ingest schema is `.strict()`, so an event with no subject stays valid on a
|
|
1064
|
+
// sink that has not learned the field yet — only subject-carrying events
|
|
1065
|
+
// depend on the sink being current (deploy ordering: sink before producer).
|
|
1066
|
+
...subject ? { subject } : {}
|
|
1053
1067
|
};
|
|
1054
1068
|
}
|
|
1055
1069
|
function jsonContent(value) {
|
|
@@ -1079,6 +1093,52 @@ function toEventPerModelVerdicts(src) {
|
|
|
1079
1093
|
};
|
|
1080
1094
|
});
|
|
1081
1095
|
}
|
|
1096
|
+
function aggregateEventTokenUsage(src, engineUsage) {
|
|
1097
|
+
if (engineUsage !== void 0) {
|
|
1098
|
+
const hasIn = Object.keys(engineUsage.per_model_tokens_in).length > 0;
|
|
1099
|
+
const hasOut = Object.keys(engineUsage.per_model_tokens_out).length > 0;
|
|
1100
|
+
return {
|
|
1101
|
+
per_model_tokens_in: hasIn ? engineUsage.per_model_tokens_in : null,
|
|
1102
|
+
per_model_tokens_out: hasOut ? engineUsage.per_model_tokens_out : null,
|
|
1103
|
+
total_cost_usd: engineUsage.cost_micro_usd === null ? null : engineUsage.cost_micro_usd / 1e6
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
const tokensIn = {};
|
|
1107
|
+
const tokensOut = {};
|
|
1108
|
+
let anyTokensIn = false;
|
|
1109
|
+
let anyTokensOut = false;
|
|
1110
|
+
let costMicroUsd = 0;
|
|
1111
|
+
let anyCost = false;
|
|
1112
|
+
for (const v of src) {
|
|
1113
|
+
if (typeof v.input_tokens === "number") {
|
|
1114
|
+
tokensIn[v.model] = (tokensIn[v.model] ?? 0) + v.input_tokens;
|
|
1115
|
+
anyTokensIn = true;
|
|
1116
|
+
}
|
|
1117
|
+
if (typeof v.output_tokens === "number") {
|
|
1118
|
+
tokensOut[v.model] = (tokensOut[v.model] ?? 0) + v.output_tokens;
|
|
1119
|
+
anyTokensOut = true;
|
|
1120
|
+
}
|
|
1121
|
+
if (typeof v.cost_micro_usd === "number") {
|
|
1122
|
+
costMicroUsd += v.cost_micro_usd;
|
|
1123
|
+
anyCost = true;
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
return {
|
|
1127
|
+
per_model_tokens_in: anyTokensIn ? tokensIn : null,
|
|
1128
|
+
per_model_tokens_out: anyTokensOut ? tokensOut : null,
|
|
1129
|
+
total_cost_usd: anyCost ? costMicroUsd / 1e6 : null
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
function trajectoryFromEngine(result, known = {}) {
|
|
1133
|
+
const rounds = typeof result.token_usage?.rounds_counted === "number" && Number.isInteger(result.token_usage.rounds_counted) && result.token_usage.rounds_counted >= 0 ? result.token_usage.rounds_counted : null;
|
|
1134
|
+
const called = result.fan_out_diagnostics?.models_called;
|
|
1135
|
+
const turns = typeof called === "number" && Number.isInteger(called) && called >= 0 ? called : rounds !== null && rounds > 0 ? result.per_model_verdicts.length * rounds : null;
|
|
1136
|
+
const sourceGrounded = result.citation_grade !== void 0 || result.low_confidence_sources !== void 0;
|
|
1137
|
+
const tool_calls = typeof known.tool_calls === "number" && Number.isInteger(known.tool_calls) && known.tool_calls >= 0 ? known.tool_calls : sourceGrounded ? null : 0;
|
|
1138
|
+
if (rounds === null && turns === null && tool_calls === null) return {};
|
|
1139
|
+
const trajectory = { rounds, turns, tool_calls };
|
|
1140
|
+
return { trajectory };
|
|
1141
|
+
}
|
|
1082
1142
|
function toEventSynthesizedVerdict(src) {
|
|
1083
1143
|
return {
|
|
1084
1144
|
verdict: src.verdict,
|
|
@@ -1086,12 +1146,13 @@ function toEventSynthesizedVerdict(src) {
|
|
|
1086
1146
|
reasoning_excerpt: sanitizeExcerpt(src.reasoning_excerpt)
|
|
1087
1147
|
};
|
|
1088
1148
|
}
|
|
1089
|
-
var VO_MCP_VERSION;
|
|
1149
|
+
var VO_MCP_VERSION, REPO_RE;
|
|
1090
1150
|
var init_common = __esm({
|
|
1091
1151
|
"src/tools/common.ts"() {
|
|
1092
1152
|
"use strict";
|
|
1093
1153
|
init_events_writer();
|
|
1094
1154
|
VO_MCP_VERSION = readVoMcpVersion();
|
|
1155
|
+
REPO_RE = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u;
|
|
1095
1156
|
}
|
|
1096
1157
|
});
|
|
1097
1158
|
|
|
@@ -1261,6 +1322,7 @@ __export(credential_store_exports, {
|
|
|
1261
1322
|
KEYCHAIN_LOCATION: () => KEYCHAIN_LOCATION,
|
|
1262
1323
|
credentialPath: () => credentialPath,
|
|
1263
1324
|
readStoredCredential: () => readStoredCredential,
|
|
1325
|
+
readStoredCredentialKeychainOnly: () => readStoredCredentialKeychainOnly,
|
|
1264
1326
|
writeStoredCredential: () => writeStoredCredential
|
|
1265
1327
|
});
|
|
1266
1328
|
import { homedir as homedir3 } from "node:os";
|
|
@@ -1319,6 +1381,11 @@ function readStoredCredential(env = process.env, keychain = realKeychain) {
|
|
|
1319
1381
|
}
|
|
1320
1382
|
return readFromFile(env);
|
|
1321
1383
|
}
|
|
1384
|
+
function readStoredCredentialKeychainOnly(env = process.env, keychain = realKeychain) {
|
|
1385
|
+
if (!keychainEnabled(env, keychain)) return null;
|
|
1386
|
+
const raw = keychain.get();
|
|
1387
|
+
return raw ? deserialize(raw) : null;
|
|
1388
|
+
}
|
|
1322
1389
|
function deleteFile(env) {
|
|
1323
1390
|
try {
|
|
1324
1391
|
rmSync(credentialPath(env), { force: true });
|
|
@@ -1393,43 +1460,480 @@ var init_safe_memory_file = __esm({
|
|
|
1393
1460
|
}
|
|
1394
1461
|
});
|
|
1395
1462
|
|
|
1396
|
-
// src/tools/memory/sync-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1463
|
+
// src/tools/memory/sync-lock-liveness.ts
|
|
1464
|
+
import { statSync as statSync5, readFileSync as readFileSync8 } from "node:fs";
|
|
1465
|
+
function defaultIsProcessAlive(pid) {
|
|
1466
|
+
try {
|
|
1467
|
+
process.kill(pid, 0);
|
|
1468
|
+
return true;
|
|
1469
|
+
} catch (err) {
|
|
1470
|
+
return err.code === "EPERM";
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
function toPayload(parsed) {
|
|
1474
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
|
|
1475
|
+
const record = parsed;
|
|
1476
|
+
const token = record["token"];
|
|
1477
|
+
const host = record["hostname"];
|
|
1478
|
+
if (typeof token !== "string" || token.length === 0) return null;
|
|
1479
|
+
const pid = record["pid"];
|
|
1480
|
+
const acquiredAtMs = record["acquiredAtMs"];
|
|
1481
|
+
return {
|
|
1482
|
+
pid: typeof pid === "number" && Number.isInteger(pid) && pid > 0 ? pid : 0,
|
|
1483
|
+
hostname: typeof host === "string" ? host : "",
|
|
1484
|
+
sessionId: typeof record["sessionId"] === "string" ? record["sessionId"] : null,
|
|
1485
|
+
token,
|
|
1486
|
+
acquiredAt: typeof record["acquiredAt"] === "string" ? record["acquiredAt"] : "",
|
|
1487
|
+
acquiredAtMs: typeof acquiredAtMs === "number" && Number.isFinite(acquiredAtMs) ? acquiredAtMs : Number.NaN
|
|
1488
|
+
};
|
|
1489
|
+
}
|
|
1490
|
+
function readLockRecord(path4) {
|
|
1491
|
+
let raw;
|
|
1492
|
+
try {
|
|
1493
|
+
raw = readFileSync8(path4, "utf8");
|
|
1494
|
+
} catch {
|
|
1495
|
+
return null;
|
|
1496
|
+
}
|
|
1497
|
+
try {
|
|
1498
|
+
return { raw, payload: toPayload(JSON.parse(raw)) };
|
|
1499
|
+
} catch {
|
|
1500
|
+
return { raw, payload: null };
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
function lockAgeMs(record, path4, nowMs) {
|
|
1504
|
+
let startedMs = Number.NaN;
|
|
1505
|
+
if (record.payload) {
|
|
1506
|
+
if (Number.isFinite(record.payload.acquiredAtMs)) {
|
|
1507
|
+
startedMs = record.payload.acquiredAtMs;
|
|
1508
|
+
} else if (record.payload.acquiredAt) {
|
|
1509
|
+
startedMs = Date.parse(record.payload.acquiredAt);
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
if (!Number.isFinite(startedMs)) {
|
|
1513
|
+
try {
|
|
1514
|
+
startedMs = statSync5(path4).mtimeMs;
|
|
1515
|
+
} catch {
|
|
1516
|
+
return null;
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
const age = nowMs - startedMs;
|
|
1520
|
+
return Number.isFinite(age) && age >= 0 ? age : null;
|
|
1521
|
+
}
|
|
1522
|
+
function classifyHolderLiveness(record, isProcessAlive, thisHost) {
|
|
1523
|
+
const payload = record.payload;
|
|
1524
|
+
if (payload === null) return "unknown";
|
|
1525
|
+
if (payload.pid <= 0) return "unknown";
|
|
1526
|
+
if (thisHost.length === 0) return "unknown";
|
|
1527
|
+
if (payload.hostname !== thisHost) return "unknown";
|
|
1528
|
+
return isProcessAlive(payload.pid) ? "alive" : "dead";
|
|
1529
|
+
}
|
|
1530
|
+
function isLockAbandoned(record, ageMs, ttlMs, isProcessAlive, thisHost) {
|
|
1531
|
+
const liveness = classifyHolderLiveness(record, isProcessAlive, thisHost);
|
|
1532
|
+
if (liveness === "alive") return false;
|
|
1533
|
+
if (liveness === "dead") return true;
|
|
1534
|
+
return ageMs !== null && ageMs > ttlMs;
|
|
1535
|
+
}
|
|
1536
|
+
var init_sync_lock_liveness = __esm({
|
|
1537
|
+
"src/tools/memory/sync-lock-liveness.ts"() {
|
|
1538
|
+
"use strict";
|
|
1539
|
+
}
|
|
1407
1540
|
});
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
import {
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
return true;
|
|
1541
|
+
|
|
1542
|
+
// src/tools/memory/sync-lock.ts
|
|
1543
|
+
import { closeSync as closeSync3, mkdirSync as mkdirSync5, openSync as openSync3, readFileSync as readFileSync9, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "node:fs";
|
|
1544
|
+
import { hostname } from "node:os";
|
|
1545
|
+
import { join as join8 } from "node:path";
|
|
1546
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
1547
|
+
function positiveOr(value, fallback) {
|
|
1548
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : fallback;
|
|
1417
1549
|
}
|
|
1418
|
-
function
|
|
1419
|
-
|
|
1550
|
+
function createExclusive2(path4, contents) {
|
|
1551
|
+
let fd;
|
|
1552
|
+
try {
|
|
1553
|
+
fd = openSync3(path4, "wx");
|
|
1554
|
+
} catch (err) {
|
|
1555
|
+
const code = err.code;
|
|
1556
|
+
return { ok: false, exists: code === "EEXIST", message: err instanceof Error ? err.message : String(err) };
|
|
1557
|
+
}
|
|
1558
|
+
try {
|
|
1559
|
+
writeFileSync4(fd, contents, "utf8");
|
|
1560
|
+
} catch (err) {
|
|
1561
|
+
closeSync3(fd);
|
|
1562
|
+
try {
|
|
1563
|
+
unlinkSync2(path4);
|
|
1564
|
+
} catch {
|
|
1565
|
+
}
|
|
1566
|
+
return { ok: false, exists: false, message: err instanceof Error ? err.message : String(err) };
|
|
1567
|
+
}
|
|
1568
|
+
closeSync3(fd);
|
|
1569
|
+
return { ok: true };
|
|
1420
1570
|
}
|
|
1421
|
-
function
|
|
1422
|
-
|
|
1423
|
-
|
|
1571
|
+
function removeAbandoned(path4, expectedRaw) {
|
|
1572
|
+
let current;
|
|
1573
|
+
try {
|
|
1574
|
+
current = readFileSync9(path4, "utf8");
|
|
1575
|
+
} catch {
|
|
1576
|
+
return;
|
|
1577
|
+
}
|
|
1578
|
+
if (current !== expectedRaw) return;
|
|
1579
|
+
try {
|
|
1580
|
+
unlinkSync2(path4);
|
|
1581
|
+
} catch {
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
function makeRelease(path4, token) {
|
|
1585
|
+
let released = false;
|
|
1586
|
+
return () => {
|
|
1587
|
+
if (released) return;
|
|
1588
|
+
released = true;
|
|
1589
|
+
let raw;
|
|
1590
|
+
try {
|
|
1591
|
+
raw = readFileSync9(path4, "utf8");
|
|
1592
|
+
} catch {
|
|
1593
|
+
return;
|
|
1594
|
+
}
|
|
1595
|
+
let stillOurs;
|
|
1596
|
+
try {
|
|
1597
|
+
stillOurs = toPayload(JSON.parse(raw))?.token === token;
|
|
1598
|
+
} catch {
|
|
1599
|
+
stillOurs = false;
|
|
1600
|
+
}
|
|
1601
|
+
if (!stillOurs) return;
|
|
1602
|
+
try {
|
|
1603
|
+
unlinkSync2(path4);
|
|
1604
|
+
} catch {
|
|
1605
|
+
}
|
|
1606
|
+
};
|
|
1607
|
+
}
|
|
1608
|
+
function describeHolder(record) {
|
|
1609
|
+
const payload = record?.payload;
|
|
1610
|
+
if (!payload) return "an unreadable lock file";
|
|
1611
|
+
return `pid ${payload.pid} on ${payload.hostname || "(unknown host)"} (session ${payload.sessionId ?? "unknown"}, held since ${payload.acquiredAt || "unknown"})`;
|
|
1612
|
+
}
|
|
1613
|
+
async function acquireMemorySyncLock(options) {
|
|
1614
|
+
const waitMs = positiveOr(options.waitMs, DEFAULT_LOCK_WAIT_MS);
|
|
1615
|
+
const ttlMs = positiveOr(options.ttlMs, DEFAULT_LOCK_TTL_MS);
|
|
1616
|
+
const now = options.now ?? Date.now;
|
|
1617
|
+
const sleep = options.sleep ?? ((ms) => new Promise((resolve3) => {
|
|
1618
|
+
setTimeout(resolve3, ms);
|
|
1619
|
+
}));
|
|
1620
|
+
const isProcessAlive = options.isProcessAlive ?? defaultIsProcessAlive;
|
|
1621
|
+
const thisHost = hostname();
|
|
1622
|
+
const path4 = join8(options.memoryDir, MEMORY_SYNC_LOCK_FILE);
|
|
1623
|
+
if (options.createDir === true) mkdirSync5(options.memoryDir, { recursive: true });
|
|
1624
|
+
const deadline = now() + waitMs;
|
|
1625
|
+
let backoffMs = INITIAL_BACKOFF_MS;
|
|
1626
|
+
let tookOverFrom = null;
|
|
1627
|
+
let holderDescription = "another session";
|
|
1628
|
+
for (; ; ) {
|
|
1629
|
+
const acquiredAtMs = now();
|
|
1630
|
+
const payload = {
|
|
1631
|
+
pid: process.pid,
|
|
1632
|
+
hostname: thisHost,
|
|
1633
|
+
sessionId: options.sessionId ?? null,
|
|
1634
|
+
token: randomUUID2(),
|
|
1635
|
+
acquiredAt: new Date(acquiredAtMs).toISOString(),
|
|
1636
|
+
acquiredAtMs
|
|
1637
|
+
};
|
|
1638
|
+
const created = createExclusive2(path4, `${JSON.stringify(payload, null, 2)}
|
|
1639
|
+
`);
|
|
1640
|
+
if (created.ok) {
|
|
1641
|
+
return { path: path4, payload, tookOverFrom, release: makeRelease(path4, payload.token) };
|
|
1642
|
+
}
|
|
1643
|
+
if (!created.exists) {
|
|
1644
|
+
throw new Error(
|
|
1645
|
+
`memory sync lock ${path4} could not be created (${created.message}) \u2014 refusing to sync without exclusion`
|
|
1646
|
+
);
|
|
1647
|
+
}
|
|
1648
|
+
const record = readLockRecord(path4);
|
|
1649
|
+
let reclaimed = false;
|
|
1650
|
+
if (record) {
|
|
1651
|
+
holderDescription = describeHolder(record);
|
|
1652
|
+
const age = lockAgeMs(record, path4, now());
|
|
1653
|
+
if (isLockAbandoned(record, age, ttlMs, isProcessAlive, thisHost)) {
|
|
1654
|
+
tookOverFrom = record.payload;
|
|
1655
|
+
removeAbandoned(path4, record.raw);
|
|
1656
|
+
reclaimed = true;
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
if (now() >= deadline) {
|
|
1660
|
+
throw new Error(
|
|
1661
|
+
`memory sync lock ${path4} 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.`
|
|
1662
|
+
);
|
|
1663
|
+
}
|
|
1664
|
+
if (reclaimed) backoffMs = INITIAL_BACKOFF_MS;
|
|
1665
|
+
await sleep(Math.max(1, Math.min(backoffMs, deadline - now())));
|
|
1666
|
+
if (!reclaimed) backoffMs = Math.min(MAX_BACKOFF_MS, Math.ceil(backoffMs * BACKOFF_FACTOR));
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
async function withMemorySyncLock(options, fn) {
|
|
1670
|
+
const handle = await acquireMemorySyncLock(options);
|
|
1671
|
+
try {
|
|
1672
|
+
return await fn(handle);
|
|
1673
|
+
} finally {
|
|
1674
|
+
handle.release();
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
var MEMORY_SYNC_LOCK_FILE, DEFAULT_LOCK_TTL_MS, DEFAULT_LOCK_WAIT_MS, INITIAL_BACKOFF_MS, MAX_BACKOFF_MS, BACKOFF_FACTOR;
|
|
1678
|
+
var init_sync_lock = __esm({
|
|
1679
|
+
"src/tools/memory/sync-lock.ts"() {
|
|
1680
|
+
"use strict";
|
|
1681
|
+
init_sync_lock_liveness();
|
|
1682
|
+
init_sync_lock_liveness();
|
|
1683
|
+
MEMORY_SYNC_LOCK_FILE = ".memory-sync.lock";
|
|
1684
|
+
DEFAULT_LOCK_TTL_MS = 15 * 6e4;
|
|
1685
|
+
DEFAULT_LOCK_WAIT_MS = 1e4;
|
|
1686
|
+
INITIAL_BACKOFF_MS = 25;
|
|
1687
|
+
MAX_BACKOFF_MS = 500;
|
|
1688
|
+
BACKOFF_FACTOR = 1.6;
|
|
1689
|
+
}
|
|
1690
|
+
});
|
|
1691
|
+
|
|
1692
|
+
// src/tools/memory/memory-index-merge.ts
|
|
1693
|
+
function isMemoryIndexFile(fileName) {
|
|
1694
|
+
return fileName.toUpperCase() === MEMORY_INDEX_FILE.toUpperCase();
|
|
1695
|
+
}
|
|
1696
|
+
function indexRowKey(line) {
|
|
1697
|
+
const match = INDEX_ROW_RE.exec(line);
|
|
1698
|
+
if (!match) return null;
|
|
1699
|
+
let target = match[1].trim();
|
|
1700
|
+
if (target.startsWith("<") && target.endsWith(">")) target = target.slice(1, -1).trim();
|
|
1701
|
+
target = target.replace(/\s+(["'])[\s\S]*\1$/, "").trim();
|
|
1702
|
+
if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(target)) {
|
|
1703
|
+
target = target.replace(/\\/g, "/").replace(/\/{2,}/g, "/");
|
|
1704
|
+
target = target.replace(/^(?:\.\/)+/, "");
|
|
1705
|
+
}
|
|
1706
|
+
return target.length > 0 ? target.toLowerCase() : null;
|
|
1707
|
+
}
|
|
1708
|
+
function mergeMemoryIndex(localContent, cloudContent) {
|
|
1709
|
+
if (typeof cloudContent !== "string" || cloudContent.trim().length === 0) {
|
|
1710
|
+
return { content: localContent, addedFromCloud: [] };
|
|
1711
|
+
}
|
|
1712
|
+
const eol = localContent.includes("\r\n") ? "\r\n" : "\n";
|
|
1713
|
+
const localLines = localContent.split(/\r?\n/);
|
|
1714
|
+
const localKeys = /* @__PURE__ */ new Set();
|
|
1715
|
+
let lastLocalRowIndex = -1;
|
|
1716
|
+
for (let i = 0; i < localLines.length; i++) {
|
|
1717
|
+
const key = indexRowKey(localLines[i]);
|
|
1718
|
+
if (key === null) continue;
|
|
1719
|
+
localKeys.add(key);
|
|
1720
|
+
lastLocalRowIndex = i;
|
|
1721
|
+
}
|
|
1722
|
+
const addedFromCloud = [];
|
|
1723
|
+
const seenCloudKeys = /* @__PURE__ */ new Set();
|
|
1724
|
+
for (const rawLine of cloudContent.split(/\r?\n/)) {
|
|
1725
|
+
const key = indexRowKey(rawLine);
|
|
1726
|
+
if (key === null) continue;
|
|
1727
|
+
if (localKeys.has(key) || seenCloudKeys.has(key)) continue;
|
|
1728
|
+
seenCloudKeys.add(key);
|
|
1729
|
+
addedFromCloud.push(rawLine.replace(/\r$/, ""));
|
|
1730
|
+
}
|
|
1731
|
+
if (addedFromCloud.length === 0) {
|
|
1732
|
+
return { content: localContent, addedFromCloud: [] };
|
|
1733
|
+
}
|
|
1734
|
+
const merged = lastLocalRowIndex >= 0 ? [...localLines.slice(0, lastLocalRowIndex + 1), ...addedFromCloud, ...localLines.slice(lastLocalRowIndex + 1)] : [...localLines, ...addedFromCloud];
|
|
1735
|
+
return { content: merged.join(eol), addedFromCloud };
|
|
1736
|
+
}
|
|
1737
|
+
var MEMORY_INDEX_FILE, INDEX_ROW_RE;
|
|
1738
|
+
var init_memory_index_merge = __esm({
|
|
1739
|
+
"src/tools/memory/memory-index-merge.ts"() {
|
|
1740
|
+
"use strict";
|
|
1741
|
+
MEMORY_INDEX_FILE = "MEMORY.md";
|
|
1742
|
+
INDEX_ROW_RE = /^\s*[-*]\s+\[[^\]]*\]\(([^)]+)\)/;
|
|
1743
|
+
}
|
|
1744
|
+
});
|
|
1745
|
+
|
|
1746
|
+
// src/tools/memory/bounded-sync.ts
|
|
1747
|
+
function createSyncDeadline(budgetMs = SYNC_DEADLINE_MS, now = Date.now) {
|
|
1748
|
+
const startedAt = now();
|
|
1749
|
+
return {
|
|
1750
|
+
check() {
|
|
1751
|
+
const elapsed = now() - startedAt;
|
|
1752
|
+
if (elapsed > budgetMs) throw new SyncDeadlineExceededError(elapsed, budgetMs);
|
|
1753
|
+
},
|
|
1754
|
+
remainingMs() {
|
|
1755
|
+
return Math.max(0, budgetMs - (now() - startedAt));
|
|
1756
|
+
}
|
|
1757
|
+
};
|
|
1758
|
+
}
|
|
1759
|
+
async function withRequestTimeout(url, run, budgetMs = REQUEST_TIMEOUT_MS) {
|
|
1760
|
+
let timer;
|
|
1761
|
+
try {
|
|
1762
|
+
return await Promise.race([
|
|
1763
|
+
run(),
|
|
1764
|
+
new Promise((_resolve, reject) => {
|
|
1765
|
+
timer = setTimeout(() => reject(new RequestTimeoutError(url, budgetMs)), budgetMs);
|
|
1766
|
+
timer.unref?.();
|
|
1767
|
+
})
|
|
1768
|
+
]);
|
|
1769
|
+
} finally {
|
|
1770
|
+
if (timer) clearTimeout(timer);
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
async function mapWithConcurrency(items, limit, fn) {
|
|
1774
|
+
const results = new Array(items.length);
|
|
1775
|
+
const width = Math.max(1, Math.min(limit, items.length));
|
|
1776
|
+
let next = 0;
|
|
1777
|
+
async function worker() {
|
|
1778
|
+
for (; ; ) {
|
|
1779
|
+
const index = next++;
|
|
1780
|
+
if (index >= items.length) return;
|
|
1781
|
+
try {
|
|
1782
|
+
results[index] = { ok: true, value: await fn(items[index], index) };
|
|
1783
|
+
} catch (error) {
|
|
1784
|
+
results[index] = { ok: false, error };
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
await Promise.all(Array.from({ length: width }, () => worker()));
|
|
1789
|
+
return results;
|
|
1790
|
+
}
|
|
1791
|
+
var REQUEST_TIMEOUT_MS, SYNC_DEADLINE_MS, PUSH_CONCURRENCY, SyncDeadlineExceededError, RequestTimeoutError;
|
|
1792
|
+
var init_bounded_sync = __esm({
|
|
1793
|
+
"src/tools/memory/bounded-sync.ts"() {
|
|
1794
|
+
"use strict";
|
|
1795
|
+
REQUEST_TIMEOUT_MS = 15e3;
|
|
1796
|
+
SYNC_DEADLINE_MS = 12e4;
|
|
1797
|
+
PUSH_CONCURRENCY = 6;
|
|
1798
|
+
SyncDeadlineExceededError = class extends Error {
|
|
1799
|
+
constructor(elapsedMs, budgetMs) {
|
|
1800
|
+
super(
|
|
1801
|
+
`memory sync exceeded its ${budgetMs}ms deadline after ${elapsedMs}ms \u2014 aborting so the lock is released instead of held indefinitely`
|
|
1802
|
+
);
|
|
1803
|
+
this.name = "SyncDeadlineExceededError";
|
|
1804
|
+
}
|
|
1805
|
+
};
|
|
1806
|
+
RequestTimeoutError = class extends Error {
|
|
1807
|
+
constructor(url, budgetMs) {
|
|
1808
|
+
super(`memory sync request to ${url} exceeded ${budgetMs}ms`);
|
|
1809
|
+
this.name = "RequestTimeoutError";
|
|
1810
|
+
}
|
|
1811
|
+
};
|
|
1812
|
+
}
|
|
1813
|
+
});
|
|
1814
|
+
|
|
1815
|
+
// src/tools/memory/memory-push-cache.ts
|
|
1816
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
1817
|
+
import { readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "node:fs";
|
|
1818
|
+
import { join as join9 } from "node:path";
|
|
1819
|
+
function sha256(content) {
|
|
1820
|
+
return createHash3("sha256").update(content, "utf8").digest("hex");
|
|
1821
|
+
}
|
|
1822
|
+
function statePath(memoryDir) {
|
|
1823
|
+
return join9(memoryDir, MEMORY_SYNC_STATE_FILE);
|
|
1824
|
+
}
|
|
1825
|
+
function readPushCache(memoryDir, controlPlaneUrl) {
|
|
1826
|
+
const empty = { controlPlaneUrl, entries: /* @__PURE__ */ new Map(), knowledgeSweptAtMs: null };
|
|
1827
|
+
let raw;
|
|
1828
|
+
try {
|
|
1829
|
+
raw = readFileSync10(statePath(memoryDir), "utf8");
|
|
1830
|
+
} catch {
|
|
1831
|
+
return empty;
|
|
1832
|
+
}
|
|
1833
|
+
let parsed;
|
|
1834
|
+
try {
|
|
1835
|
+
parsed = JSON.parse(raw);
|
|
1836
|
+
} catch {
|
|
1837
|
+
return empty;
|
|
1838
|
+
}
|
|
1839
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return empty;
|
|
1840
|
+
const obj = parsed;
|
|
1841
|
+
if (obj["version"] !== STATE_VERSION) return empty;
|
|
1842
|
+
if (obj["controlPlaneUrl"] !== controlPlaneUrl) return empty;
|
|
1843
|
+
const files = obj["entries"];
|
|
1844
|
+
if (typeof files !== "object" || files === null || Array.isArray(files)) return empty;
|
|
1845
|
+
const entries = /* @__PURE__ */ new Map();
|
|
1846
|
+
for (const [name, value] of Object.entries(files)) {
|
|
1847
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) continue;
|
|
1848
|
+
const row = value;
|
|
1849
|
+
const memoryHash = typeof row["memoryHash"] === "string" ? row["memoryHash"] : void 0;
|
|
1850
|
+
const knowledgeHash = typeof row["knowledgeHash"] === "string" ? row["knowledgeHash"] : void 0;
|
|
1851
|
+
if (memoryHash === void 0 && knowledgeHash === void 0) continue;
|
|
1852
|
+
entries.set(name, {
|
|
1853
|
+
...memoryHash !== void 0 ? { memoryHash } : {},
|
|
1854
|
+
...knowledgeHash !== void 0 ? { knowledgeHash } : {}
|
|
1855
|
+
});
|
|
1856
|
+
}
|
|
1857
|
+
const sweptAt = obj["knowledgeSweptAtMs"];
|
|
1858
|
+
return {
|
|
1859
|
+
controlPlaneUrl,
|
|
1860
|
+
entries,
|
|
1861
|
+
// An unreadable/absent sweep stamp reads as NEVER SWEPT, which forces a full
|
|
1862
|
+
// sweep — the fail-closed direction (more upserts, never fewer).
|
|
1863
|
+
knowledgeSweptAtMs: typeof sweptAt === "number" && Number.isFinite(sweptAt) ? sweptAt : null
|
|
1864
|
+
};
|
|
1865
|
+
}
|
|
1866
|
+
function writePushCache(memoryDir, cache) {
|
|
1867
|
+
const entries = {};
|
|
1868
|
+
for (const [name, value] of cache.entries) entries[name] = value;
|
|
1869
|
+
try {
|
|
1870
|
+
writeFileSync5(
|
|
1871
|
+
statePath(memoryDir),
|
|
1872
|
+
`${JSON.stringify(
|
|
1873
|
+
{
|
|
1874
|
+
version: STATE_VERSION,
|
|
1875
|
+
controlPlaneUrl: cache.controlPlaneUrl,
|
|
1876
|
+
knowledgeSweptAtMs: cache.knowledgeSweptAtMs,
|
|
1877
|
+
entries
|
|
1878
|
+
},
|
|
1879
|
+
null,
|
|
1880
|
+
2
|
|
1881
|
+
)}
|
|
1882
|
+
`,
|
|
1883
|
+
"utf8"
|
|
1884
|
+
);
|
|
1885
|
+
} catch {
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
function recordMemoryPush(cache, fileName, payloadHash) {
|
|
1889
|
+
cache.entries.set(fileName, { ...cache.entries.get(fileName), memoryHash: payloadHash });
|
|
1424
1890
|
}
|
|
1891
|
+
function recordKnowledgePush(cache, fileName, contentHash) {
|
|
1892
|
+
cache.entries.set(fileName, { ...cache.entries.get(fileName), knowledgeHash: contentHash });
|
|
1893
|
+
}
|
|
1894
|
+
function pruneMissing(cache, presentFileNames) {
|
|
1895
|
+
const present = new Set(presentFileNames);
|
|
1896
|
+
for (const name of [...cache.entries.keys()]) {
|
|
1897
|
+
if (!present.has(name)) cache.entries.delete(name);
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
function needsMemoryPush(cache, fileName, payloadHash, serverHasEntry) {
|
|
1901
|
+
if (!serverHasEntry) return true;
|
|
1902
|
+
return cache.entries.get(fileName)?.memoryHash !== payloadHash;
|
|
1903
|
+
}
|
|
1904
|
+
function knowledgeSweepDue(cache, nowMs = Date.now()) {
|
|
1905
|
+
const swept = cache.knowledgeSweptAtMs;
|
|
1906
|
+
if (swept === null || !Number.isFinite(swept)) return true;
|
|
1907
|
+
const age = nowMs - swept;
|
|
1908
|
+
return !(age >= 0 && age < KNOWLEDGE_FULL_SWEEP_MS);
|
|
1909
|
+
}
|
|
1910
|
+
function needsKnowledgePush(cache, fileName, contentHash, sweepDue = false) {
|
|
1911
|
+
if (sweepDue) return true;
|
|
1912
|
+
return cache.entries.get(fileName)?.knowledgeHash !== contentHash;
|
|
1913
|
+
}
|
|
1914
|
+
var MEMORY_SYNC_STATE_FILE, STATE_VERSION, KNOWLEDGE_FULL_SWEEP_MS;
|
|
1915
|
+
var init_memory_push_cache = __esm({
|
|
1916
|
+
"src/tools/memory/memory-push-cache.ts"() {
|
|
1917
|
+
"use strict";
|
|
1918
|
+
MEMORY_SYNC_STATE_FILE = ".memory-sync-state.json";
|
|
1919
|
+
STATE_VERSION = 1;
|
|
1920
|
+
KNOWLEDGE_FULL_SWEEP_MS = 24 * 60 * 6e4;
|
|
1921
|
+
}
|
|
1922
|
+
});
|
|
1923
|
+
|
|
1924
|
+
// src/tools/memory/memory-sync-http.ts
|
|
1925
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync6, readdirSync as readdirSync4 } from "node:fs";
|
|
1425
1926
|
async function pullMemory(controlPlaneUrl, token, memoryDir, fetchFn) {
|
|
1426
1927
|
const url = `${controlPlaneUrl}/api/v1/agent-config/memory/me`;
|
|
1427
|
-
const response = await
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1928
|
+
const response = await withRequestTimeout(
|
|
1929
|
+
url,
|
|
1930
|
+
() => fetchFn(url, {
|
|
1931
|
+
method: "GET",
|
|
1932
|
+
headers: {
|
|
1933
|
+
authorization: `Bearer ${token}`
|
|
1934
|
+
}
|
|
1935
|
+
})
|
|
1936
|
+
);
|
|
1433
1937
|
if (response.status !== 200) {
|
|
1434
1938
|
const text = await response.text();
|
|
1435
1939
|
throw new Error(`GET /api/v1/agent-config/memory/me returned HTTP ${response.status}: ${text.slice(0, 200)}`);
|
|
@@ -1442,137 +1946,468 @@ async function pullMemory(controlPlaneUrl, token, memoryDir, fetchFn) {
|
|
|
1442
1946
|
entry,
|
|
1443
1947
|
filePath: resolveMemoryFilePath(memoryDir, entry.file_name)
|
|
1444
1948
|
}));
|
|
1445
|
-
|
|
1949
|
+
mkdirSync6(memoryDir, { recursive: true });
|
|
1446
1950
|
const files = [];
|
|
1447
1951
|
for (const { entry, filePath } of writes) {
|
|
1448
|
-
|
|
1952
|
+
writeFileSync6(filePath, entry.content, "utf8");
|
|
1449
1953
|
files.push(entry.file_name);
|
|
1450
1954
|
}
|
|
1451
1955
|
return { pulled: data.entries.length, files };
|
|
1452
1956
|
}
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1957
|
+
function listPushableFiles(memoryDir) {
|
|
1958
|
+
return readdirSync4(memoryDir).filter((f) => f.endsWith(".md") && f !== MEMORY_SYNC_LOCK_FILE);
|
|
1959
|
+
}
|
|
1960
|
+
async function uploadOne(item, controlPlaneUrl, token, sessionId, fetchFn, deadline) {
|
|
1961
|
+
deadline.check();
|
|
1962
|
+
if (item.memoryId !== null) {
|
|
1963
|
+
const updateUrl = `${controlPlaneUrl}/api/v1/agent-config/memory/${item.memoryId}`;
|
|
1964
|
+
const updateBody = { content: item.content, session_id: sessionId };
|
|
1965
|
+
const updateResponse = await withRequestTimeout(
|
|
1966
|
+
updateUrl,
|
|
1967
|
+
() => fetchFn(updateUrl, {
|
|
1968
|
+
method: "PUT",
|
|
1969
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
1970
|
+
body: JSON.stringify(updateBody)
|
|
1971
|
+
})
|
|
1972
|
+
);
|
|
1973
|
+
if (updateResponse.status !== 200) {
|
|
1974
|
+
const text = await updateResponse.text();
|
|
1975
|
+
throw new Error(
|
|
1976
|
+
`PUT /api/v1/agent-config/memory/${item.memoryId} returned HTTP ${updateResponse.status}: ${text.slice(0, 200)}`
|
|
1977
|
+
);
|
|
1978
|
+
}
|
|
1979
|
+
const updateData = JSON.parse(await updateResponse.text());
|
|
1980
|
+
if (!updateData.ok) throw new Error(`PUT /api/v1/agent-config/memory/${item.memoryId} returned ok=false`);
|
|
1981
|
+
return "updated";
|
|
1982
|
+
}
|
|
1983
|
+
const createUrl = `${controlPlaneUrl}/api/v1/agent-config/memory/me`;
|
|
1984
|
+
const createBody = {
|
|
1985
|
+
entry_type: item.entryType,
|
|
1986
|
+
file_name: item.fileName,
|
|
1987
|
+
content: item.content,
|
|
1988
|
+
session_id: sessionId
|
|
1989
|
+
};
|
|
1990
|
+
const createResponse = await withRequestTimeout(
|
|
1991
|
+
createUrl,
|
|
1992
|
+
() => fetchFn(createUrl, {
|
|
1993
|
+
method: "POST",
|
|
1994
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
1995
|
+
body: JSON.stringify(createBody)
|
|
1996
|
+
})
|
|
1997
|
+
);
|
|
1998
|
+
if (createResponse.status !== 200 && createResponse.status !== 201) {
|
|
1999
|
+
const text = await createResponse.text();
|
|
2000
|
+
throw new Error(
|
|
2001
|
+
`POST /api/v1/agent-config/memory/me returned HTTP ${createResponse.status}: ${text.slice(0, 200)}`
|
|
2002
|
+
);
|
|
2003
|
+
}
|
|
2004
|
+
const createData = JSON.parse(await createResponse.text());
|
|
2005
|
+
if (!createData.ok) throw new Error("POST /api/v1/agent-config/memory/me returned ok=false");
|
|
2006
|
+
return "created";
|
|
2007
|
+
}
|
|
2008
|
+
async function pushMemory(controlPlaneUrl, token, memoryDir, sessionId, fetchFn, options = {}) {
|
|
2009
|
+
const empty = { pushed: 0, created: 0, updated: 0, skipped: 0, indexRowsPreserved: 0 };
|
|
2010
|
+
if (!existsSync6(memoryDir)) {
|
|
2011
|
+
return empty;
|
|
1456
2012
|
}
|
|
1457
|
-
const localFiles =
|
|
2013
|
+
const localFiles = listPushableFiles(memoryDir).map((f) => ({
|
|
1458
2014
|
file_name: f,
|
|
1459
|
-
content:
|
|
1460
|
-
entry_type: f
|
|
2015
|
+
content: readFileSync11(resolveMemoryFilePath(memoryDir, f), "utf8"),
|
|
2016
|
+
entry_type: isMemoryIndexFile(f) ? "index" : "topic"
|
|
1461
2017
|
}));
|
|
1462
2018
|
if (localFiles.length === 0) {
|
|
1463
|
-
return
|
|
2019
|
+
return empty;
|
|
1464
2020
|
}
|
|
2021
|
+
const deadline = options.deadline ?? createSyncDeadline();
|
|
2022
|
+
const cache = options.cache ?? readPushCache(memoryDir, controlPlaneUrl);
|
|
2023
|
+
deadline.check();
|
|
1465
2024
|
const getUrl = `${controlPlaneUrl}/api/v1/agent-config/memory/me`;
|
|
1466
|
-
const getResponse = await
|
|
1467
|
-
|
|
1468
|
-
headers: {
|
|
1469
|
-
|
|
1470
|
-
}
|
|
1471
|
-
});
|
|
2025
|
+
const getResponse = await withRequestTimeout(
|
|
2026
|
+
getUrl,
|
|
2027
|
+
() => fetchFn(getUrl, { method: "GET", headers: { authorization: `Bearer ${token}` } })
|
|
2028
|
+
);
|
|
1472
2029
|
const existingMap = /* @__PURE__ */ new Map();
|
|
1473
2030
|
if (getResponse.status === 200) {
|
|
1474
2031
|
const getData = JSON.parse(await getResponse.text());
|
|
1475
2032
|
if (getData.ok && Array.isArray(getData.entries)) {
|
|
1476
2033
|
for (const entry of getData.entries) {
|
|
1477
|
-
existingMap.set(entry.file_name,
|
|
2034
|
+
existingMap.set(entry.file_name, {
|
|
2035
|
+
memoryId: entry.memory_id,
|
|
2036
|
+
content: typeof entry.content === "string" ? entry.content : ""
|
|
2037
|
+
});
|
|
1478
2038
|
}
|
|
1479
2039
|
}
|
|
1480
2040
|
}
|
|
1481
|
-
|
|
1482
|
-
let
|
|
2041
|
+
const toUpload = [];
|
|
2042
|
+
let skipped = 0;
|
|
1483
2043
|
for (const localFile of localFiles) {
|
|
1484
|
-
const
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
},
|
|
1497
|
-
body: JSON.stringify(updateBody)
|
|
1498
|
-
});
|
|
1499
|
-
if (updateResponse.status !== 200) {
|
|
1500
|
-
const text = await updateResponse.text();
|
|
1501
|
-
throw new Error(
|
|
1502
|
-
`PUT /api/v1/agent-config/memory/${memoryId} returned HTTP ${updateResponse.status}: ${text.slice(0, 200)}`
|
|
1503
|
-
);
|
|
1504
|
-
}
|
|
1505
|
-
const updateData = JSON.parse(await updateResponse.text());
|
|
1506
|
-
if (!updateData.ok) {
|
|
1507
|
-
throw new Error(`PUT /api/v1/agent-config/memory/${memoryId} returned ok=false`);
|
|
1508
|
-
}
|
|
1509
|
-
updated++;
|
|
1510
|
-
} else {
|
|
1511
|
-
const createUrl = `${controlPlaneUrl}/api/v1/agent-config/memory/me`;
|
|
1512
|
-
const createBody = {
|
|
1513
|
-
entry_type: localFile.entry_type,
|
|
1514
|
-
file_name: localFile.file_name,
|
|
1515
|
-
content: localFile.content,
|
|
1516
|
-
session_id: sessionId
|
|
1517
|
-
};
|
|
1518
|
-
const createResponse = await fetchFn(createUrl, {
|
|
1519
|
-
method: "POST",
|
|
1520
|
-
headers: {
|
|
1521
|
-
authorization: `Bearer ${token}`,
|
|
1522
|
-
"content-type": "application/json"
|
|
1523
|
-
},
|
|
1524
|
-
body: JSON.stringify(createBody)
|
|
1525
|
-
});
|
|
1526
|
-
if (createResponse.status !== 200 && createResponse.status !== 201) {
|
|
1527
|
-
const text = await createResponse.text();
|
|
1528
|
-
throw new Error(
|
|
1529
|
-
`POST /api/v1/agent-config/memory/me returned HTTP ${createResponse.status}: ${text.slice(0, 200)}`
|
|
1530
|
-
);
|
|
1531
|
-
}
|
|
1532
|
-
const createData = JSON.parse(await createResponse.text());
|
|
1533
|
-
if (!createData.ok) {
|
|
1534
|
-
throw new Error("POST /api/v1/agent-config/memory/me returned ok=false");
|
|
1535
|
-
}
|
|
1536
|
-
created++;
|
|
2044
|
+
const existing = existingMap.get(localFile.file_name);
|
|
2045
|
+
let content = localFile.content;
|
|
2046
|
+
let rowsPreserved = 0;
|
|
2047
|
+
if (localFile.entry_type === "index") {
|
|
2048
|
+
const merged = mergeMemoryIndex(localFile.content, existing?.content);
|
|
2049
|
+
content = merged.content;
|
|
2050
|
+
rowsPreserved = merged.addedFromCloud.length;
|
|
2051
|
+
}
|
|
2052
|
+
const payloadHash = sha256(content);
|
|
2053
|
+
if (!needsMemoryPush(cache, localFile.file_name, payloadHash, existing !== void 0)) {
|
|
2054
|
+
skipped++;
|
|
2055
|
+
continue;
|
|
1537
2056
|
}
|
|
2057
|
+
toUpload.push({
|
|
2058
|
+
fileName: localFile.file_name,
|
|
2059
|
+
entryType: localFile.entry_type,
|
|
2060
|
+
content,
|
|
2061
|
+
payloadHash,
|
|
2062
|
+
memoryId: existing?.memoryId ?? null,
|
|
2063
|
+
rowsPreserved
|
|
2064
|
+
});
|
|
1538
2065
|
}
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
2066
|
+
const outcomes = await mapWithConcurrency(
|
|
2067
|
+
toUpload,
|
|
2068
|
+
options.concurrency ?? PUSH_CONCURRENCY,
|
|
2069
|
+
(item) => uploadOne(item, controlPlaneUrl, token, sessionId, fetchFn, deadline)
|
|
2070
|
+
);
|
|
2071
|
+
let created = 0;
|
|
2072
|
+
let updated = 0;
|
|
2073
|
+
let indexRowsPreserved = 0;
|
|
2074
|
+
let firstError;
|
|
2075
|
+
for (let i = 0; i < outcomes.length; i++) {
|
|
2076
|
+
const outcome = outcomes[i];
|
|
2077
|
+
const item = toUpload[i];
|
|
2078
|
+
if (outcome.ok) {
|
|
2079
|
+
if (outcome.value === "created") created++;
|
|
2080
|
+
else updated++;
|
|
2081
|
+
indexRowsPreserved += item.rowsPreserved;
|
|
2082
|
+
recordMemoryPush(cache, item.fileName, item.payloadHash);
|
|
2083
|
+
} else if (firstError === void 0) {
|
|
2084
|
+
firstError = outcome.error;
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
pruneMissing(cache, localFiles.map((f) => f.file_name));
|
|
2088
|
+
if (options.persistCache ?? options.cache === void 0) writePushCache(memoryDir, cache);
|
|
2089
|
+
if (firstError !== void 0) throw firstError;
|
|
2090
|
+
return { pushed: created + updated, created, updated, skipped, indexRowsPreserved };
|
|
2091
|
+
}
|
|
2092
|
+
var init_memory_sync_http = __esm({
|
|
2093
|
+
"src/tools/memory/memory-sync-http.ts"() {
|
|
2094
|
+
"use strict";
|
|
2095
|
+
init_safe_memory_file();
|
|
2096
|
+
init_sync_lock();
|
|
2097
|
+
init_memory_index_merge();
|
|
2098
|
+
init_bounded_sync();
|
|
2099
|
+
init_memory_push_cache();
|
|
1559
2100
|
}
|
|
1560
|
-
|
|
1561
|
-
|
|
2101
|
+
});
|
|
2102
|
+
|
|
2103
|
+
// src/tools/memory/sync-kill-switch.ts
|
|
2104
|
+
import { existsSync as existsSync7, readFileSync as readFileSync12 } from "node:fs";
|
|
2105
|
+
import { homedir as homedir6 } from "node:os";
|
|
2106
|
+
import { join as join10 } from "node:path";
|
|
2107
|
+
function memorySyncSentinelPath(home) {
|
|
2108
|
+
return join10(home, ".claude", MEMORY_SYNC_DISABLE_SENTINEL);
|
|
2109
|
+
}
|
|
2110
|
+
function isKillSwitchValueOn(raw) {
|
|
2111
|
+
if (raw === void 0 || raw === null) return false;
|
|
2112
|
+
const v = raw.trim().toLowerCase();
|
|
2113
|
+
if (v === "") return false;
|
|
2114
|
+
return !NEGATIONS.has(v);
|
|
2115
|
+
}
|
|
2116
|
+
function clip(raw) {
|
|
2117
|
+
const v = raw.trim();
|
|
2118
|
+
return v.length > MAX_LOGGED_VALUE ? `${v.slice(0, MAX_LOGGED_VALUE)}\u2026` : v;
|
|
2119
|
+
}
|
|
2120
|
+
function evaluateMemorySyncKillSwitch(deps = {}) {
|
|
2121
|
+
const env = deps.env ?? process.env;
|
|
2122
|
+
const home = deps.home ?? homedir6();
|
|
2123
|
+
const fileExists = deps.fileExists ?? existsSync7;
|
|
2124
|
+
const readFile3 = deps.readFile ?? ((p) => readFileSync12(p, "utf8"));
|
|
2125
|
+
const fired = [];
|
|
2126
|
+
const rawEnv = env[MEMORY_SYNC_DISABLE_ENV];
|
|
2127
|
+
if (isKillSwitchValueOn(rawEnv)) {
|
|
2128
|
+
fired.push(`env ${MEMORY_SYNC_DISABLE_ENV}=${clip(rawEnv)}`);
|
|
2129
|
+
}
|
|
2130
|
+
const sentinel = memorySyncSentinelPath(home);
|
|
2131
|
+
let sentinelPresent;
|
|
1562
2132
|
try {
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
2133
|
+
sentinelPresent = fileExists(sentinel);
|
|
2134
|
+
} catch {
|
|
2135
|
+
sentinelPresent = false;
|
|
2136
|
+
}
|
|
2137
|
+
if (sentinelPresent) {
|
|
2138
|
+
let contents = "";
|
|
2139
|
+
let readable = true;
|
|
2140
|
+
try {
|
|
2141
|
+
contents = readFile3(sentinel);
|
|
2142
|
+
} catch {
|
|
2143
|
+
readable = false;
|
|
1566
2144
|
}
|
|
1567
|
-
|
|
2145
|
+
if (!readable || isKillSwitchValueOn(contents) || contents.trim() === "") {
|
|
2146
|
+
fired.push(`sentinel file ${sentinel}`);
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
if (fired.length === 0) return { disabled: false, reason: null };
|
|
2150
|
+
return { disabled: true, reason: `memory sync DISABLED by ${fired.join(" + ")}` };
|
|
2151
|
+
}
|
|
2152
|
+
var MEMORY_SYNC_DISABLE_ENV, MEMORY_SYNC_DISABLE_SENTINEL, NEGATIONS, MAX_LOGGED_VALUE;
|
|
2153
|
+
var init_sync_kill_switch = __esm({
|
|
2154
|
+
"src/tools/memory/sync-kill-switch.ts"() {
|
|
2155
|
+
"use strict";
|
|
2156
|
+
MEMORY_SYNC_DISABLE_ENV = "VO_MCP_DISABLE_MEMORY_SYNC";
|
|
2157
|
+
MEMORY_SYNC_DISABLE_SENTINEL = "vo-memory-sync-disabled";
|
|
2158
|
+
NEGATIONS = /* @__PURE__ */ new Set(["0", "false", "no"]);
|
|
2159
|
+
MAX_LOGGED_VALUE = 32;
|
|
2160
|
+
}
|
|
2161
|
+
});
|
|
2162
|
+
|
|
2163
|
+
// src/tools/memory/memory-knowledge-bridge.ts
|
|
2164
|
+
var memory_knowledge_bridge_exports = {};
|
|
2165
|
+
__export(memory_knowledge_bridge_exports, {
|
|
2166
|
+
extractMemoryTitle: () => extractMemoryTitle,
|
|
2167
|
+
upsertMemoryFilesAsKnowledge: () => upsertMemoryFilesAsKnowledge
|
|
2168
|
+
});
|
|
2169
|
+
import { existsSync as existsSync8, readdirSync as readdirSync5, readFileSync as readFileSync13 } from "node:fs";
|
|
2170
|
+
function extractMemoryTitle(fileName, content) {
|
|
2171
|
+
const frontmatter = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
2172
|
+
if (frontmatter) {
|
|
2173
|
+
const description24 = frontmatter[1].match(/^description:\s*(.+)$/m);
|
|
2174
|
+
if (description24 && description24[1].trim()) return description24[1].trim().slice(0, 200);
|
|
2175
|
+
}
|
|
2176
|
+
const heading = content.match(/^#\s+(.+)$/m);
|
|
2177
|
+
if (heading && heading[1].trim()) return heading[1].trim().slice(0, 200);
|
|
2178
|
+
return fileName;
|
|
2179
|
+
}
|
|
2180
|
+
async function upsertMemoryFilesAsKnowledge(options) {
|
|
2181
|
+
const { controlPlaneUrl, token, memoryDir, fetchFn, cache, deadline } = options;
|
|
2182
|
+
let files;
|
|
2183
|
+
try {
|
|
2184
|
+
if (!existsSync8(memoryDir)) {
|
|
2185
|
+
return { attempted: 0, upserted: 0, failed: 0, skipped: 0, failures: [] };
|
|
2186
|
+
}
|
|
2187
|
+
files = readdirSync5(memoryDir).filter(
|
|
2188
|
+
(f) => f.endsWith(".md") && f.toUpperCase() !== "MEMORY.MD"
|
|
2189
|
+
);
|
|
2190
|
+
} catch (err) {
|
|
2191
|
+
return {
|
|
2192
|
+
attempted: 0,
|
|
2193
|
+
upserted: 0,
|
|
2194
|
+
failed: 1,
|
|
2195
|
+
skipped: 0,
|
|
2196
|
+
failures: [`memory dir scan: ${err instanceof Error ? err.message : String(err)}`]
|
|
2197
|
+
};
|
|
2198
|
+
}
|
|
2199
|
+
const sweepDue = cache ? knowledgeSweepDue(cache) : true;
|
|
2200
|
+
const candidates = [];
|
|
2201
|
+
const failures = [];
|
|
2202
|
+
let skipped = 0;
|
|
2203
|
+
for (const fileName of files) {
|
|
2204
|
+
try {
|
|
2205
|
+
const content = readFileSync13(resolveMemoryFilePath(memoryDir, fileName), "utf8");
|
|
2206
|
+
if (content.length > CONTENT_HARD_LIMIT) {
|
|
2207
|
+
failures.push(`${fileName}: ${content.length} chars exceeds the ${CONTENT_HARD_LIMIT} server limit \u2014 split the memory file`);
|
|
2208
|
+
continue;
|
|
2209
|
+
}
|
|
2210
|
+
const hash = sha256(content);
|
|
2211
|
+
if (cache && !needsKnowledgePush(cache, fileName, hash, sweepDue)) {
|
|
2212
|
+
skipped += 1;
|
|
2213
|
+
continue;
|
|
2214
|
+
}
|
|
2215
|
+
candidates.push({ fileName, content, hash });
|
|
2216
|
+
} catch (err) {
|
|
2217
|
+
failures.push(`${fileName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
const url = `${controlPlaneUrl}/api/v1/knowledge/private`;
|
|
2221
|
+
const outcomes = await mapWithConcurrency(candidates, options.concurrency ?? PUSH_CONCURRENCY, async (candidate) => {
|
|
2222
|
+
deadline?.check();
|
|
2223
|
+
const base = {
|
|
2224
|
+
knowledge_class: "memory",
|
|
2225
|
+
source_path: `memory/${candidate.fileName}`,
|
|
2226
|
+
title: extractMemoryTitle(candidate.fileName, candidate.content),
|
|
2227
|
+
content: candidate.content
|
|
2228
|
+
};
|
|
2229
|
+
const post = (body) => withRequestTimeout(url, () => fetchFn(url, {
|
|
2230
|
+
method: "POST",
|
|
2231
|
+
headers: {
|
|
2232
|
+
authorization: `Bearer ${token}`,
|
|
2233
|
+
"content-type": "application/json"
|
|
2234
|
+
},
|
|
2235
|
+
body: JSON.stringify(body)
|
|
2236
|
+
}));
|
|
2237
|
+
let response = await post({
|
|
2238
|
+
...base,
|
|
2239
|
+
provenance: { written_by: "memory-bridge", source_kind: "operator_memory" }
|
|
2240
|
+
});
|
|
2241
|
+
if (response.status === 400) {
|
|
2242
|
+
response = await post(base);
|
|
2243
|
+
}
|
|
2244
|
+
if (response.status >= 200 && response.status < 300) return true;
|
|
2245
|
+
const text = await response.text();
|
|
2246
|
+
throw new Error(`HTTP ${response.status} ${text.slice(0, 80)}`);
|
|
2247
|
+
});
|
|
2248
|
+
let upserted = 0;
|
|
2249
|
+
for (let i = 0; i < outcomes.length; i++) {
|
|
2250
|
+
const outcome = outcomes[i];
|
|
2251
|
+
const candidate = candidates[i];
|
|
2252
|
+
if (outcome.ok) {
|
|
2253
|
+
upserted += 1;
|
|
2254
|
+
if (cache) recordKnowledgePush(cache, candidate.fileName, candidate.hash);
|
|
2255
|
+
} else {
|
|
2256
|
+
const error = outcome.error;
|
|
2257
|
+
failures.push(`${candidate.fileName}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
if (cache && sweepDue && failures.length === 0) {
|
|
2261
|
+
cache.knowledgeSweptAtMs = Date.now();
|
|
2262
|
+
}
|
|
2263
|
+
return {
|
|
2264
|
+
// Every memory file this run considered. `attempted === upserted + skipped
|
|
2265
|
+
// + failed` holds, so a caller can tell "nothing to do" from "nothing ran".
|
|
2266
|
+
attempted: files.length,
|
|
2267
|
+
upserted,
|
|
2268
|
+
failed: failures.length,
|
|
2269
|
+
skipped,
|
|
2270
|
+
failures: failures.slice(0, 5)
|
|
2271
|
+
};
|
|
2272
|
+
}
|
|
2273
|
+
var CONTENT_HARD_LIMIT;
|
|
2274
|
+
var init_memory_knowledge_bridge = __esm({
|
|
2275
|
+
"src/tools/memory/memory-knowledge-bridge.ts"() {
|
|
2276
|
+
"use strict";
|
|
2277
|
+
init_safe_memory_file();
|
|
2278
|
+
init_bounded_sync();
|
|
2279
|
+
init_memory_push_cache();
|
|
2280
|
+
CONTENT_HARD_LIMIT = 5e5;
|
|
2281
|
+
}
|
|
2282
|
+
});
|
|
2283
|
+
|
|
2284
|
+
// src/tools/memory/sync-config.ts
|
|
2285
|
+
var sync_config_exports = {};
|
|
2286
|
+
__export(sync_config_exports, {
|
|
2287
|
+
TOOL_NAME: () => TOOL_NAME23,
|
|
2288
|
+
deriveProjectSlug: () => deriveProjectSlug,
|
|
2289
|
+
description: () => description23,
|
|
2290
|
+
getMemoryDir: () => getMemoryDir,
|
|
2291
|
+
handleSyncConfig: () => handleSyncConfig,
|
|
2292
|
+
inputSchema: () => inputSchema23,
|
|
2293
|
+
isNoopSyncReason: () => isNoopSyncReason,
|
|
2294
|
+
runMemorySync: () => runMemorySync
|
|
2295
|
+
});
|
|
2296
|
+
import { existsSync as existsSync9 } from "node:fs";
|
|
2297
|
+
import { homedir as homedir7 } from "node:os";
|
|
2298
|
+
import { join as join11 } from "node:path";
|
|
2299
|
+
function isToolInput22(v) {
|
|
2300
|
+
if (typeof v !== "object" || v === null) return false;
|
|
2301
|
+
const o = v;
|
|
2302
|
+
if (o["action"] !== "pull" && o["action"] !== "push") return false;
|
|
2303
|
+
if (o["cwd"] !== void 0 && typeof o["cwd"] !== "string") return false;
|
|
2304
|
+
return true;
|
|
2305
|
+
}
|
|
2306
|
+
function deriveProjectSlug(cwd) {
|
|
2307
|
+
return cwd.replace(/([^:\\/])[\\/]+$/, "$1").replace(/\\/g, "/").replace(/^([a-zA-Z]):/, (_m, drive) => `${drive.toUpperCase()}:`).replace(/[^a-zA-Z0-9]/g, "-");
|
|
2308
|
+
}
|
|
2309
|
+
function getMemoryDir(cwd) {
|
|
2310
|
+
const slug = deriveProjectSlug(cwd);
|
|
2311
|
+
return join11(homedir7(), ".claude", "projects", slug, "memory");
|
|
2312
|
+
}
|
|
2313
|
+
function isNoopSyncReason(reason) {
|
|
2314
|
+
if (!reason) return false;
|
|
2315
|
+
return /not set|No auth configured|Failed to obtain auth token|DISABLED by/.test(reason);
|
|
2316
|
+
}
|
|
2317
|
+
async function runMemorySync(action, cwd, sessionId, fetchFn = globalThis.fetch, lockOptions = {}) {
|
|
2318
|
+
const killSwitch = evaluateMemorySyncKillSwitch();
|
|
2319
|
+
if (killSwitch.disabled) {
|
|
2320
|
+
return { synced: false, reason: killSwitch.reason };
|
|
2321
|
+
}
|
|
2322
|
+
const controlPlaneUrl = process.env["VO_CONTROL_PLANE_URL"];
|
|
2323
|
+
if (!controlPlaneUrl) {
|
|
2324
|
+
return { synced: false, reason: "VO_CONTROL_PLANE_URL not set \u2014 cloud mode disabled" };
|
|
2325
|
+
}
|
|
2326
|
+
const { createAuthTokenSourceFromEnv: createAuthTokenSourceFromEnv2 } = await Promise.resolve().then(() => (init_auth_token_source(), auth_token_source_exports));
|
|
2327
|
+
const { readStoredCredential: readStoredCredential2 } = await Promise.resolve().then(() => (init_credential_store(), credential_store_exports));
|
|
2328
|
+
const tokenSource = createAuthTokenSourceFromEnv2(process.env, fetchFn, () => readStoredCredential2(process.env));
|
|
2329
|
+
if (!tokenSource) {
|
|
2330
|
+
return { synced: false, reason: "No auth configured. Run `vo-mcp login` to authenticate as an operator." };
|
|
2331
|
+
}
|
|
2332
|
+
const token = await tokenSource.getToken();
|
|
2333
|
+
if (!token) {
|
|
2334
|
+
return { synced: false, reason: "Failed to obtain auth token. Run `vo-mcp login` to re-authenticate." };
|
|
2335
|
+
}
|
|
2336
|
+
const memoryDir = getMemoryDir(cwd);
|
|
2337
|
+
const baseUrl = controlPlaneUrl.replace(/\/+$/, "");
|
|
2338
|
+
if (action === "push" && !existsSync9(memoryDir)) {
|
|
1568
2339
|
return {
|
|
1569
2340
|
synced: true,
|
|
1570
2341
|
action: "push",
|
|
1571
|
-
pushed:
|
|
1572
|
-
created:
|
|
1573
|
-
updated:
|
|
2342
|
+
pushed: 0,
|
|
2343
|
+
created: 0,
|
|
2344
|
+
updated: 0,
|
|
2345
|
+
skipped: 0,
|
|
2346
|
+
index_rows_preserved: 0,
|
|
2347
|
+
knowledge_upserted: 0,
|
|
2348
|
+
knowledge_failed: 0,
|
|
1574
2349
|
memory_dir: memoryDir
|
|
1575
2350
|
};
|
|
2351
|
+
}
|
|
2352
|
+
try {
|
|
2353
|
+
return await withMemorySyncLock({ ...lockOptions, memoryDir, sessionId, createDir: action === "pull" }, async (lock) => {
|
|
2354
|
+
const takeover = lock.tookOverFrom ? { lock_taken_over_from_pid: lock.tookOverFrom.pid } : {};
|
|
2355
|
+
if (action === "pull") {
|
|
2356
|
+
const result2 = await pullMemory(baseUrl, token, memoryDir, fetchFn);
|
|
2357
|
+
return {
|
|
2358
|
+
synced: true,
|
|
2359
|
+
action: "pull",
|
|
2360
|
+
pulled: result2.pulled,
|
|
2361
|
+
files: result2.files,
|
|
2362
|
+
memory_dir: memoryDir,
|
|
2363
|
+
...takeover
|
|
2364
|
+
};
|
|
2365
|
+
}
|
|
2366
|
+
const deadline = createSyncDeadline();
|
|
2367
|
+
const cache = readPushCache(memoryDir, baseUrl);
|
|
2368
|
+
let result;
|
|
2369
|
+
try {
|
|
2370
|
+
result = await pushMemory(baseUrl, token, memoryDir, sessionId, fetchFn, { cache, deadline });
|
|
2371
|
+
} catch (err) {
|
|
2372
|
+
writePushCache(memoryDir, cache);
|
|
2373
|
+
throw err;
|
|
2374
|
+
}
|
|
2375
|
+
let bridge;
|
|
2376
|
+
try {
|
|
2377
|
+
const { upsertMemoryFilesAsKnowledge: upsertMemoryFilesAsKnowledge2 } = await Promise.resolve().then(() => (init_memory_knowledge_bridge(), memory_knowledge_bridge_exports));
|
|
2378
|
+
bridge = await upsertMemoryFilesAsKnowledge2({
|
|
2379
|
+
controlPlaneUrl: baseUrl,
|
|
2380
|
+
token,
|
|
2381
|
+
memoryDir,
|
|
2382
|
+
fetchFn,
|
|
2383
|
+
cache,
|
|
2384
|
+
deadline
|
|
2385
|
+
});
|
|
2386
|
+
} catch (err) {
|
|
2387
|
+
bridge = {
|
|
2388
|
+
upserted: 0,
|
|
2389
|
+
failed: 1,
|
|
2390
|
+
skipped: 0,
|
|
2391
|
+
failures: [`bridge unavailable: ${err instanceof Error ? err.message : String(err)}`]
|
|
2392
|
+
};
|
|
2393
|
+
}
|
|
2394
|
+
writePushCache(memoryDir, cache);
|
|
2395
|
+
return {
|
|
2396
|
+
synced: true,
|
|
2397
|
+
action: "push",
|
|
2398
|
+
pushed: result.pushed,
|
|
2399
|
+
created: result.created,
|
|
2400
|
+
updated: result.updated,
|
|
2401
|
+
skipped: result.skipped,
|
|
2402
|
+
index_rows_preserved: result.indexRowsPreserved,
|
|
2403
|
+
memory_dir: memoryDir,
|
|
2404
|
+
knowledge_upserted: bridge.upserted,
|
|
2405
|
+
knowledge_failed: bridge.failed,
|
|
2406
|
+
knowledge_skipped: bridge.skipped,
|
|
2407
|
+
...bridge.failed > 0 ? { knowledge_failures: bridge.failures } : {},
|
|
2408
|
+
...takeover
|
|
2409
|
+
};
|
|
2410
|
+
});
|
|
1576
2411
|
} catch (err) {
|
|
1577
2412
|
const message = err instanceof Error ? err.message : String(err);
|
|
1578
2413
|
return { synced: false, reason: `Sync failed: ${message}` };
|
|
@@ -1581,22 +2416,26 @@ async function runMemorySync(action, cwd, sessionId, fetchFn = globalThis.fetch)
|
|
|
1581
2416
|
async function handleSyncConfig(deps, rawInput, _signal, fetchFn = globalThis.fetch) {
|
|
1582
2417
|
if (!isToolInput22(rawInput)) {
|
|
1583
2418
|
throw invalidParams(
|
|
1584
|
-
|
|
2419
|
+
TOOL_NAME23,
|
|
1585
2420
|
'invalid input. Required: { action: "pull" | "push" }. Optional: { cwd: "<path>" }.'
|
|
1586
2421
|
);
|
|
1587
2422
|
}
|
|
1588
2423
|
const cwd = rawInput.cwd?.trim() || process.cwd();
|
|
1589
2424
|
const result = await runMemorySync(rawInput.action, cwd, deps.session.sessionId, fetchFn);
|
|
1590
|
-
return jsonContent({ tool:
|
|
2425
|
+
return jsonContent({ tool: TOOL_NAME23, schema_version: 1, payload: result });
|
|
1591
2426
|
}
|
|
1592
|
-
var
|
|
2427
|
+
var TOOL_NAME23, inputSchema23, description23;
|
|
1593
2428
|
var init_sync_config = __esm({
|
|
1594
2429
|
"src/tools/memory/sync-config.ts"() {
|
|
1595
2430
|
"use strict";
|
|
1596
2431
|
init_common();
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
2432
|
+
init_memory_sync_http();
|
|
2433
|
+
init_bounded_sync();
|
|
2434
|
+
init_memory_push_cache();
|
|
2435
|
+
init_sync_lock();
|
|
2436
|
+
init_sync_kill_switch();
|
|
2437
|
+
TOOL_NAME23 = "vo_sync_config";
|
|
2438
|
+
inputSchema23 = {
|
|
1600
2439
|
type: "object",
|
|
1601
2440
|
properties: {
|
|
1602
2441
|
action: {
|
|
@@ -1612,19 +2451,19 @@ var init_sync_config = __esm({
|
|
|
1612
2451
|
required: ["action"],
|
|
1613
2452
|
additionalProperties: false
|
|
1614
2453
|
};
|
|
1615
|
-
|
|
2454
|
+
description23 = "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.";
|
|
1616
2455
|
}
|
|
1617
2456
|
});
|
|
1618
2457
|
|
|
1619
2458
|
// src/cli.ts
|
|
1620
|
-
import { homedir as
|
|
1621
|
-
import { randomUUID as
|
|
1622
|
-
import { join as
|
|
2459
|
+
import { homedir as homedir9, hostname as hostname2 } from "node:os";
|
|
2460
|
+
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
2461
|
+
import { join as join15 } from "node:path";
|
|
1623
2462
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
1624
2463
|
|
|
1625
2464
|
// src/server.ts
|
|
1626
2465
|
init_common();
|
|
1627
|
-
import { randomUUID as
|
|
2466
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
1628
2467
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
1629
2468
|
import {
|
|
1630
2469
|
CallToolRequestSchema,
|
|
@@ -2000,7 +2839,9 @@ async function handleCheckHollowTest(deps, rawInput, signal) {
|
|
|
2000
2839
|
synthesized_verdict: synthForEvent,
|
|
2001
2840
|
consensus_confidence: engineResult.synthesized_verdict.confidence,
|
|
2002
2841
|
duration_ms: engineResult.duration_ms,
|
|
2003
|
-
consensus_engine_version: engineResult.engine_version
|
|
2842
|
+
consensus_engine_version: engineResult.engine_version,
|
|
2843
|
+
...aggregateEventTokenUsage(engineResult.per_model_verdicts, engineResult.token_usage),
|
|
2844
|
+
...trajectoryFromEngine(engineResult)
|
|
2004
2845
|
};
|
|
2005
2846
|
const payload = {
|
|
2006
2847
|
verdict: engineResult.synthesized_verdict.verdict,
|
|
@@ -2183,7 +3024,9 @@ async function handleVerifyAnswer(deps, rawInput, signal) {
|
|
|
2183
3024
|
synthesized_verdict: synthForEvent,
|
|
2184
3025
|
consensus_confidence: engineResult.synthesized_verdict.confidence,
|
|
2185
3026
|
duration_ms: engineResult.duration_ms,
|
|
2186
|
-
consensus_engine_version: engineResult.engine_version
|
|
3027
|
+
consensus_engine_version: engineResult.engine_version,
|
|
3028
|
+
...aggregateEventTokenUsage(engineResult.per_model_verdicts, engineResult.token_usage),
|
|
3029
|
+
...trajectoryFromEngine(engineResult)
|
|
2187
3030
|
};
|
|
2188
3031
|
const payload = {
|
|
2189
3032
|
verdict: engineResult.synthesized_verdict.verdict,
|
|
@@ -2192,6 +3035,9 @@ async function handleVerifyAnswer(deps, rawInput, signal) {
|
|
|
2192
3035
|
synthesized_verdict: synthForEvent,
|
|
2193
3036
|
engine_version: engineResult.engine_version,
|
|
2194
3037
|
degraded: engineResult.degraded,
|
|
3038
|
+
...engineResult.quorum_failed === true ? { quorum_failed: true } : {},
|
|
3039
|
+
// The verification receipt (moat decision_id) — the thing an agent pastes as `receipt id: <uuid>`.
|
|
3040
|
+
...engineResult.receipt_id ? { receipt_id: engineResult.receipt_id } : {},
|
|
2195
3041
|
gate_type: gateType,
|
|
2196
3042
|
...kbResult.error !== null ? { kb_unavailable: true } : {},
|
|
2197
3043
|
...kbTruncated > 0 ? { kb_rules_truncated: kbTruncated } : {}
|
|
@@ -2448,7 +3294,9 @@ async function handleConsensusJudgment(deps, rawInput, signal) {
|
|
|
2448
3294
|
duration_ms: engineResult.duration_ms,
|
|
2449
3295
|
consensus_engine_version: engineResult.engine_version,
|
|
2450
3296
|
per_model_verdicts: perModelForEvent,
|
|
2451
|
-
synthesized_verdict: synthForEvent
|
|
3297
|
+
synthesized_verdict: synthForEvent,
|
|
3298
|
+
...aggregateEventTokenUsage(engineResult.per_model_verdicts, engineResult.token_usage),
|
|
3299
|
+
...trajectoryFromEngine(engineResult)
|
|
2452
3300
|
};
|
|
2453
3301
|
const payload = {
|
|
2454
3302
|
verdict: engineResult.synthesized_verdict.verdict,
|
|
@@ -2457,6 +3305,11 @@ async function handleConsensusJudgment(deps, rawInput, signal) {
|
|
|
2457
3305
|
synthesized_verdict: synthForEvent,
|
|
2458
3306
|
engine_version: engineResult.engine_version,
|
|
2459
3307
|
degraded: engineResult.degraded,
|
|
3308
|
+
...engineResult.quorum_failed === true ? { quorum_failed: true } : {},
|
|
3309
|
+
// The verification receipt (moat decision_id) — the thing an agent pastes as `receipt id: <uuid>`.
|
|
3310
|
+
// NOTE: a content-hash cache hit replays the ORIGINAL call's receipt_id (same claim, same verdict, no new spend) —
|
|
3311
|
+
// a receipt asserts the stage ran for this claim, not one-receipt-per-call.
|
|
3312
|
+
...engineResult.receipt_id ? { receipt_id: engineResult.receipt_id } : {},
|
|
2460
3313
|
gate_type: gateType,
|
|
2461
3314
|
// ─── Consensus-engine feature outputs (additive; 2026-06-13) ─────────────
|
|
2462
3315
|
// Feature 2 (calibrated-confidence) — ON by default; the engine attaches
|
|
@@ -2475,7 +3328,11 @@ async function handleConsensusJudgment(deps, rawInput, signal) {
|
|
|
2475
3328
|
...engineResult.low_confidence_sources !== void 0 ? { low_confidence_sources: engineResult.low_confidence_sources } : {},
|
|
2476
3329
|
// Escalation (from citation grade or human-tiebreak synthesizer).
|
|
2477
3330
|
...engineResult.escalation_required !== void 0 ? { escalation_required: engineResult.escalation_required } : {},
|
|
2478
|
-
...engineResult.escalation_reason !== void 0 ? { escalation_reason: engineResult.escalation_reason } : {}
|
|
3331
|
+
...engineResult.escalation_reason !== void 0 ? { escalation_reason: engineResult.escalation_reason } : {},
|
|
3332
|
+
// Critique-uptake (2026-07-20 red-team fix) — the engine computes this
|
|
3333
|
+
// on every call; this spread closes the gap where the visibility report
|
|
3334
|
+
// was itself silently dropped at the payload boundary.
|
|
3335
|
+
...engineResult.critique_uptake !== void 0 ? { critique_uptake: engineResult.critique_uptake } : {}
|
|
2479
3336
|
};
|
|
2480
3337
|
const envelope = {
|
|
2481
3338
|
tool: TOOL_NAME4,
|
|
@@ -2653,7 +3510,9 @@ async function handleArchitectureReview(deps, rawInput, signal) {
|
|
|
2653
3510
|
synthesized_verdict: synthForEvent,
|
|
2654
3511
|
consensus_confidence: engineResult.synthesized_verdict.confidence,
|
|
2655
3512
|
duration_ms: engineResult.duration_ms,
|
|
2656
|
-
consensus_engine_version: engineResult.engine_version
|
|
3513
|
+
consensus_engine_version: engineResult.engine_version,
|
|
3514
|
+
...aggregateEventTokenUsage(engineResult.per_model_verdicts, engineResult.token_usage),
|
|
3515
|
+
...trajectoryFromEngine(engineResult)
|
|
2657
3516
|
};
|
|
2658
3517
|
const escalationRequired = engineResult.escalation_required === true || engineResult.escalation_required === void 0 && engineResult.synthesized_verdict.dissent_summary !== null;
|
|
2659
3518
|
const escalationReason = engineResult.escalation_reason ?? engineResult.synthesized_verdict.dissent_summary ?? "";
|
|
@@ -4140,7 +4999,9 @@ Produce the JSON dispatch plan now.`;
|
|
|
4140
4999
|
duration_ms: engineResult.duration_ms,
|
|
4141
5000
|
consensus_engine_version: engineResult.engine_version,
|
|
4142
5001
|
per_model_verdicts: toEventPerModelVerdicts(engineResult.per_model_verdicts),
|
|
4143
|
-
synthesized_verdict: toEventSynthesizedVerdict(engineResult.synthesized_verdict)
|
|
5002
|
+
synthesized_verdict: toEventSynthesizedVerdict(engineResult.synthesized_verdict),
|
|
5003
|
+
...aggregateEventTokenUsage(engineResult.per_model_verdicts, engineResult.token_usage),
|
|
5004
|
+
...trajectoryFromEngine(engineResult)
|
|
4144
5005
|
};
|
|
4145
5006
|
deps.events.append(enrichedEvent);
|
|
4146
5007
|
return jsonContent(envelope);
|
|
@@ -4153,15 +5014,27 @@ init_common();
|
|
|
4153
5014
|
init_auth_token_source();
|
|
4154
5015
|
init_credential_store();
|
|
4155
5016
|
var AdminCallableError = class extends Error {
|
|
4156
|
-
constructor(status,
|
|
5017
|
+
constructor(status, path4, message, body) {
|
|
4157
5018
|
super(message);
|
|
4158
5019
|
this.status = status;
|
|
4159
|
-
this.path =
|
|
5020
|
+
this.path = path4;
|
|
5021
|
+
this.body = body;
|
|
4160
5022
|
this.name = "AdminCallableError";
|
|
4161
5023
|
}
|
|
4162
5024
|
status;
|
|
4163
5025
|
path;
|
|
5026
|
+
body;
|
|
4164
5027
|
};
|
|
5028
|
+
function parseJsonObject(text) {
|
|
5029
|
+
try {
|
|
5030
|
+
const parsed = JSON.parse(text);
|
|
5031
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
5032
|
+
return parsed;
|
|
5033
|
+
}
|
|
5034
|
+
} catch {
|
|
5035
|
+
}
|
|
5036
|
+
return void 0;
|
|
5037
|
+
}
|
|
4165
5038
|
var HttpAdminCallableClient = class {
|
|
4166
5039
|
baseUrl;
|
|
4167
5040
|
tokenSource;
|
|
@@ -4183,35 +5056,39 @@ var HttpAdminCallableClient = class {
|
|
|
4183
5056
|
this.fetchFn = config.fetchFn ?? globalThis.fetch;
|
|
4184
5057
|
this.readOnly = config.readOnly ?? false;
|
|
4185
5058
|
}
|
|
4186
|
-
async invoke(
|
|
4187
|
-
if (!
|
|
5059
|
+
async invoke(path4, body, opts) {
|
|
5060
|
+
if (!path4.startsWith("/")) {
|
|
4188
5061
|
throw new Error(
|
|
4189
|
-
`HttpAdminCallableClient.invoke: path must start with '/', got '${
|
|
5062
|
+
`HttpAdminCallableClient.invoke: path must start with '/', got '${path4}'`
|
|
4190
5063
|
);
|
|
4191
5064
|
}
|
|
4192
5065
|
const token = await this.tokenSource.getToken();
|
|
4193
5066
|
if (!token) {
|
|
4194
5067
|
throw new AdminCallableError(
|
|
4195
5068
|
401,
|
|
4196
|
-
|
|
4197
|
-
`admin proxy ${
|
|
5069
|
+
path4,
|
|
5070
|
+
`admin proxy ${path4}: no auth token available (check VO_USER_REFRESH_TOKEN / VO_USER_ID_TOKEN / VO_CONTROL_PLANE_ADMIN_TOKEN)`
|
|
4198
5071
|
);
|
|
4199
5072
|
}
|
|
4200
|
-
const
|
|
4201
|
-
const
|
|
4202
|
-
|
|
5073
|
+
const method = opts?.method ?? "POST";
|
|
5074
|
+
const queryString = opts?.query ? new URLSearchParams(Object.entries(opts.query)).toString() : "";
|
|
5075
|
+
const url = `${this.baseUrl}${path4}${queryString ? `?${queryString}` : ""}`;
|
|
5076
|
+
const init = method === "GET" ? { method, headers: { "authorization": `Bearer ${token}` } } : {
|
|
5077
|
+
method,
|
|
4203
5078
|
headers: {
|
|
4204
5079
|
"authorization": `Bearer ${token}`,
|
|
4205
5080
|
"content-type": "application/json"
|
|
4206
5081
|
},
|
|
4207
5082
|
body: JSON.stringify(body)
|
|
4208
|
-
}
|
|
5083
|
+
};
|
|
5084
|
+
const response = await this.fetchFn(url, init);
|
|
4209
5085
|
const text = await response.text();
|
|
4210
5086
|
if (response.status < 200 || response.status >= 300) {
|
|
4211
5087
|
throw new AdminCallableError(
|
|
4212
5088
|
response.status,
|
|
4213
|
-
|
|
4214
|
-
`admin proxy ${
|
|
5089
|
+
path4,
|
|
5090
|
+
`admin proxy ${path4} returned HTTP ${response.status}: ${text.slice(0, 200)}`,
|
|
5091
|
+
parseJsonObject(text)
|
|
4215
5092
|
);
|
|
4216
5093
|
}
|
|
4217
5094
|
let parsed;
|
|
@@ -4220,23 +5097,24 @@ var HttpAdminCallableClient = class {
|
|
|
4220
5097
|
} catch {
|
|
4221
5098
|
throw new AdminCallableError(
|
|
4222
5099
|
response.status,
|
|
4223
|
-
|
|
4224
|
-
`admin proxy ${
|
|
5100
|
+
path4,
|
|
5101
|
+
`admin proxy ${path4} returned non-JSON body`
|
|
4225
5102
|
);
|
|
4226
5103
|
}
|
|
4227
5104
|
if (typeof parsed !== "object" || parsed === null) {
|
|
4228
5105
|
throw new AdminCallableError(
|
|
4229
5106
|
response.status,
|
|
4230
|
-
|
|
4231
|
-
`admin proxy ${
|
|
5107
|
+
path4,
|
|
5108
|
+
`admin proxy ${path4} response not an object`
|
|
4232
5109
|
);
|
|
4233
5110
|
}
|
|
4234
5111
|
const obj = parsed;
|
|
4235
5112
|
if (obj["ok"] !== true) {
|
|
4236
5113
|
throw new AdminCallableError(
|
|
4237
5114
|
response.status,
|
|
4238
|
-
|
|
4239
|
-
`admin proxy ${
|
|
5115
|
+
path4,
|
|
5116
|
+
`admin proxy ${path4} returned ok=false: ${JSON.stringify(obj).slice(0, 200)}`,
|
|
5117
|
+
obj
|
|
4240
5118
|
);
|
|
4241
5119
|
}
|
|
4242
5120
|
if (opts?.rawEnvelope) {
|
|
@@ -4249,8 +5127,8 @@ var HttpAdminCallableClient = class {
|
|
|
4249
5127
|
if (!("result" in obj)) {
|
|
4250
5128
|
throw new AdminCallableError(
|
|
4251
5129
|
response.status,
|
|
4252
|
-
|
|
4253
|
-
`admin proxy ${
|
|
5130
|
+
path4,
|
|
5131
|
+
`admin proxy ${path4} response missing .result field`
|
|
4254
5132
|
);
|
|
4255
5133
|
}
|
|
4256
5134
|
return obj["result"];
|
|
@@ -4310,11 +5188,12 @@ async function buildCloudOrStubResponse(args) {
|
|
|
4310
5188
|
args.deps.events.append(event);
|
|
4311
5189
|
return jsonContent(envelope);
|
|
4312
5190
|
}
|
|
5191
|
+
const invokeOptions = args.rawEnvelope || args.invokeOptions ? { ...args.rawEnvelope ? { rawEnvelope: true } : {}, ...args.invokeOptions } : void 0;
|
|
4313
5192
|
try {
|
|
4314
5193
|
const result = await args.deps.adminCallables.invoke(
|
|
4315
5194
|
args.adminPath,
|
|
4316
5195
|
args.cloudBody ?? args.normalizedInput,
|
|
4317
|
-
|
|
5196
|
+
invokeOptions
|
|
4318
5197
|
);
|
|
4319
5198
|
const payload = {
|
|
4320
5199
|
verdict: "pass",
|
|
@@ -4334,11 +5213,13 @@ async function buildCloudOrStubResponse(args) {
|
|
|
4334
5213
|
} catch (err) {
|
|
4335
5214
|
const status = err instanceof AdminCallableError ? err.status : void 0;
|
|
4336
5215
|
const message = err instanceof Error ? err.message : String(err);
|
|
5216
|
+
const errorBody = err instanceof AdminCallableError && err.body !== void 0 ? err.body : void 0;
|
|
4337
5217
|
const payload = {
|
|
4338
5218
|
verdict: "fail",
|
|
4339
5219
|
reason: status !== void 0 ? `vo-control-plane returned HTTP ${status}: ${message.slice(0, 300)}` : `cloud invocation failed: ${message.slice(0, 300)}`,
|
|
4340
5220
|
callable: args.callableName,
|
|
4341
|
-
normalized_input: args.normalizedInput
|
|
5221
|
+
normalized_input: args.normalizedInput,
|
|
5222
|
+
...errorBody ? { response_data: errorBody } : {}
|
|
4342
5223
|
};
|
|
4343
5224
|
const envelope = {
|
|
4344
5225
|
tool: args.toolName,
|
|
@@ -4353,7 +5234,7 @@ async function buildCloudOrStubResponse(args) {
|
|
|
4353
5234
|
|
|
4354
5235
|
// src/tools/heal/common-heal.ts
|
|
4355
5236
|
init_common();
|
|
4356
|
-
var HEAL_STUB_REASON =
|
|
5237
|
+
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.";
|
|
4357
5238
|
var HEAL_GATE_TYPE = "admin-action";
|
|
4358
5239
|
|
|
4359
5240
|
// src/tools/heal/trigger-heal.ts
|
|
@@ -4374,7 +5255,7 @@ var inputSchema8 = {
|
|
|
4374
5255
|
},
|
|
4375
5256
|
additionalProperties: false
|
|
4376
5257
|
};
|
|
4377
|
-
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.
|
|
5258
|
+
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.";
|
|
4378
5259
|
function isToolInput8(v) {
|
|
4379
5260
|
if (typeof v !== "object" || v === null) return false;
|
|
4380
5261
|
const o = v;
|
|
@@ -4432,7 +5313,7 @@ var inputSchema9 = {
|
|
|
4432
5313
|
},
|
|
4433
5314
|
additionalProperties: false
|
|
4434
5315
|
};
|
|
4435
|
-
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.
|
|
5316
|
+
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.";
|
|
4436
5317
|
function isToolInput9(v) {
|
|
4437
5318
|
if (typeof v !== "object" || v === null) return false;
|
|
4438
5319
|
const o = v;
|
|
@@ -4520,7 +5401,7 @@ var inputSchema10 = {
|
|
|
4520
5401
|
required: ["attempt_id"],
|
|
4521
5402
|
additionalProperties: false
|
|
4522
5403
|
};
|
|
4523
|
-
var description10 = "Clears (cancels) a single fix attempt by id. Wraps `voClearFixAttempt` admin Cloud Function.
|
|
5404
|
+
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.";
|
|
4524
5405
|
function isToolInput10(v) {
|
|
4525
5406
|
if (typeof v !== "object" || v === null) return false;
|
|
4526
5407
|
const o = v;
|
|
@@ -4568,7 +5449,7 @@ var inputSchema11 = {
|
|
|
4568
5449
|
required: ["run_id"],
|
|
4569
5450
|
additionalProperties: false
|
|
4570
5451
|
};
|
|
4571
|
-
var description11 = "Cancels a running GitHub Actions workflow by run id. Wraps `voStopWorkflow` admin Cloud Function.
|
|
5452
|
+
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.";
|
|
4572
5453
|
function isToolInput11(v) {
|
|
4573
5454
|
if (typeof v !== "object" || v === null) return false;
|
|
4574
5455
|
const o = v;
|
|
@@ -4614,7 +5495,7 @@ var inputSchema12 = {
|
|
|
4614
5495
|
properties: {},
|
|
4615
5496
|
additionalProperties: false
|
|
4616
5497
|
};
|
|
4617
|
-
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.
|
|
5498
|
+
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.";
|
|
4618
5499
|
function isToolInput12(v) {
|
|
4619
5500
|
if (typeof v !== "object" || v === null) return false;
|
|
4620
5501
|
return true;
|
|
@@ -4641,7 +5522,7 @@ init_common();
|
|
|
4641
5522
|
|
|
4642
5523
|
// src/tools/pr/common-pr.ts
|
|
4643
5524
|
init_common();
|
|
4644
|
-
var PR_STUB_REASON =
|
|
5525
|
+
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.";
|
|
4645
5526
|
var PR_GATE_TYPE = "admin-action";
|
|
4646
5527
|
|
|
4647
5528
|
// src/tools/pr/list-pending-prs.ts
|
|
@@ -4653,7 +5534,7 @@ var inputSchema13 = {
|
|
|
4653
5534
|
properties: {},
|
|
4654
5535
|
additionalProperties: false
|
|
4655
5536
|
};
|
|
4656
|
-
var description13 = "Lists open
|
|
5537
|
+
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.";
|
|
4657
5538
|
function isToolInput13(v) {
|
|
4658
5539
|
return typeof v === "object" && v !== null;
|
|
4659
5540
|
}
|
|
@@ -4690,7 +5571,7 @@ var inputSchema14 = {
|
|
|
4690
5571
|
required: ["pr_number"],
|
|
4691
5572
|
additionalProperties: false
|
|
4692
5573
|
};
|
|
4693
|
-
var description14 = "Approves + merges a single
|
|
5574
|
+
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.";
|
|
4694
5575
|
function isToolInput14(v) {
|
|
4695
5576
|
if (typeof v !== "object" || v === null) return false;
|
|
4696
5577
|
const o = v;
|
|
@@ -4735,7 +5616,7 @@ var inputSchema15 = {
|
|
|
4735
5616
|
required: ["pr_number"],
|
|
4736
5617
|
additionalProperties: false
|
|
4737
5618
|
};
|
|
4738
|
-
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.
|
|
5619
|
+
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.";
|
|
4739
5620
|
function isToolInput15(v) {
|
|
4740
5621
|
if (typeof v !== "object" || v === null) return false;
|
|
4741
5622
|
const o = v;
|
|
@@ -4774,7 +5655,7 @@ var inputSchema16 = {
|
|
|
4774
5655
|
properties: {},
|
|
4775
5656
|
additionalProperties: false
|
|
4776
5657
|
};
|
|
4777
|
-
var description16 = "Iterates all open
|
|
5658
|
+
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.";
|
|
4778
5659
|
function isToolInput16(v) {
|
|
4779
5660
|
return typeof v === "object" && v !== null;
|
|
4780
5661
|
}
|
|
@@ -4809,7 +5690,7 @@ var inputSchema17 = {
|
|
|
4809
5690
|
required: ["pr_number"],
|
|
4810
5691
|
additionalProperties: false
|
|
4811
5692
|
};
|
|
4812
|
-
var description17 = "Closes
|
|
5693
|
+
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.";
|
|
4813
5694
|
function isToolInput17(v) {
|
|
4814
5695
|
if (typeof v !== "object" || v === null) return false;
|
|
4815
5696
|
const o = v;
|
|
@@ -4842,6 +5723,7 @@ async function handleRejectAndRetry(deps, rawInput, _signal) {
|
|
|
4842
5723
|
init_common();
|
|
4843
5724
|
var TOOL_NAME18 = "vo_review_merge";
|
|
4844
5725
|
var LIST_PATH = "/api/v1/admin/pr/list";
|
|
5726
|
+
var DEFAULT_REVIEW_REPO = "Algosuite-ai/Nexus";
|
|
4845
5727
|
var ENGINE_GATE = "final-deep-verify";
|
|
4846
5728
|
var EVENT_GATE = "merge-review";
|
|
4847
5729
|
var UNAVAILABLE_REASON = "vo_review_merge needs cloud mode to fetch PR context \u2014 set VO_CONTROL_PLANE_URL + VO_CONTROL_PLANE_ADMIN_TOKEN in the MCP env. (It is read-only; it never merges.)";
|
|
@@ -4886,7 +5768,7 @@ function buildPrompt4(pr, notes) {
|
|
|
4886
5768
|
const lines = [
|
|
4887
5769
|
"You are a release gatekeeper deciding whether a pull request is safe to MERGE.",
|
|
4888
5770
|
"Recommend exactly one of: merge / hold / reject. Be conservative \u2014 this is a high-stakes irreversible action.",
|
|
4889
|
-
"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
|
|
5771
|
+
"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.",
|
|
4890
5772
|
"",
|
|
4891
5773
|
`PR #${pr.number}: ${pr.title}`,
|
|
4892
5774
|
`Source: ${pr.source ?? "unknown"}`,
|
|
@@ -4906,6 +5788,11 @@ async function handleReviewMerge(deps, rawInput, signal) {
|
|
|
4906
5788
|
if (rawInput.notes !== void 0) normalizedInput.notes = rawInput.notes;
|
|
4907
5789
|
const inputJson = JSON.stringify(normalizedInput);
|
|
4908
5790
|
const key = deps.cache.keyFor(TOOL_NAME18, normalizedInput);
|
|
5791
|
+
const subject = {
|
|
5792
|
+
...subjectFromEnv() ?? {},
|
|
5793
|
+
repo: subjectFromEnv()?.repo ?? DEFAULT_REVIEW_REPO,
|
|
5794
|
+
pr_number: prNumber
|
|
5795
|
+
};
|
|
4909
5796
|
const baseEvent = buildBaseEvent({
|
|
4910
5797
|
tool: TOOL_NAME18,
|
|
4911
5798
|
gateType: EVENT_GATE,
|
|
@@ -4913,7 +5800,8 @@ async function handleReviewMerge(deps, rawInput, signal) {
|
|
|
4913
5800
|
inputExcerpt: inputJson.slice(0, 300),
|
|
4914
5801
|
inputSizeBytes: bytesOf(inputJson),
|
|
4915
5802
|
session: deps.session,
|
|
4916
|
-
now: deps.now()
|
|
5803
|
+
now: deps.now(),
|
|
5804
|
+
subject
|
|
4917
5805
|
});
|
|
4918
5806
|
const emit = (payload2, eventExtra) => {
|
|
4919
5807
|
deps.events.append(eventExtra ? { ...baseEvent, ...eventExtra } : baseEvent);
|
|
@@ -4954,7 +5842,7 @@ async function handleReviewMerge(deps, rawInput, signal) {
|
|
|
4954
5842
|
}
|
|
4955
5843
|
if (pr === null) {
|
|
4956
5844
|
return emit(
|
|
4957
|
-
emptyPayload("hold", `PR #${prNumber} is not among open
|
|
5845
|
+
emptyPayload("hold", `PR #${prNumber} is not among open AlgoHQ PRs (already merged/closed, or not an AlgoHQ-source PR).`, null)
|
|
4958
5846
|
);
|
|
4959
5847
|
}
|
|
4960
5848
|
const hasBlocker = pr.blocker !== null && pr.blocker !== "none";
|
|
@@ -5014,7 +5902,139 @@ async function handleReviewMerge(deps, rawInput, signal) {
|
|
|
5014
5902
|
duration_ms: result.duration_ms,
|
|
5015
5903
|
consensus_engine_version: result.engine_version,
|
|
5016
5904
|
per_model_verdicts: perModel,
|
|
5017
|
-
synthesized_verdict: synth
|
|
5905
|
+
synthesized_verdict: synth,
|
|
5906
|
+
...aggregateEventTokenUsage(result.per_model_verdicts, result.token_usage),
|
|
5907
|
+
...trajectoryFromEngine(result)
|
|
5908
|
+
});
|
|
5909
|
+
}
|
|
5910
|
+
|
|
5911
|
+
// src/tools/runner/prepared-job-mode.ts
|
|
5912
|
+
init_common();
|
|
5913
|
+
var TOOL_NAME19 = "vo_prepared_job_mode";
|
|
5914
|
+
var CALLABLE_NAME10 = "GET|POST /api/v1/runner/prepared-job-mode";
|
|
5915
|
+
var PLANE_PATH = "/api/v1/runner/prepared-job-mode";
|
|
5916
|
+
var PREPARED_JOB_MODE_GATE_TYPE = "admin-action";
|
|
5917
|
+
var PREPARED_JOB_MODE_STUB_REASON = "cloud mode is not configured on this session, so vo-control-plane was not reached. Sign in with `vo-mcp login` to route this tool to the plane. This route is NOT under /api/v1/admin/*, so a tenant-scoped operator credential is the CORRECT principal for it \u2014 the target operator is derived from that principal, never from this input.";
|
|
5918
|
+
var PREPARED_JOB_MODES = ["off", "shadow", "prepared"];
|
|
5919
|
+
var MIN_REASON_CHARS = 10;
|
|
5920
|
+
var MAX_REASON_CHARS = 500;
|
|
5921
|
+
var MAX_RUNNER_ID_CHARS = 100;
|
|
5922
|
+
var inputSchema19 = {
|
|
5923
|
+
type: "object",
|
|
5924
|
+
properties: {
|
|
5925
|
+
action: {
|
|
5926
|
+
type: "string",
|
|
5927
|
+
enum: ["get", "set"],
|
|
5928
|
+
description: "Required. 'get' reads the delivered mode (read-only; stays live under VO_ADMIN_CALLABLES_READONLY). 'set' writes it (a write; gated to the stub in read-only mode)."
|
|
5929
|
+
},
|
|
5930
|
+
runner_id: {
|
|
5931
|
+
type: "string",
|
|
5932
|
+
minLength: 1,
|
|
5933
|
+
maxLength: MAX_RUNNER_ID_CHARS,
|
|
5934
|
+
description: "Required. Runner to read/configure, e.g. 'vo-code-runner-JacksPC'. Must belong to the authenticated operator \u2014 the plane scopes by principal, so another operator's runner simply reads as unset."
|
|
5935
|
+
},
|
|
5936
|
+
prepared_job_mode: {
|
|
5937
|
+
type: "string",
|
|
5938
|
+
enum: [...PREPARED_JOB_MODES],
|
|
5939
|
+
description: "Required for action='set'. 'off' = the runner's machine-local env rules; 'shadow' = compare the plane's prepared job without consuming it. 'prepared' is recognized but REFUSED by the plane with 409 prepared_mode_not_flippable (ADR-004 11.1c owns the flip)."
|
|
5940
|
+
},
|
|
5941
|
+
expected_revision: {
|
|
5942
|
+
type: "integer",
|
|
5943
|
+
minimum: 0,
|
|
5944
|
+
description: "Required for action='set'. Optimistic-concurrency revision read from a prior 'get' (use 0 when no config exists). A stale value returns 409 stale_prepared_job_revision carrying the authoritative current_revision."
|
|
5945
|
+
},
|
|
5946
|
+
reason: {
|
|
5947
|
+
type: "string",
|
|
5948
|
+
minLength: MIN_REASON_CHARS,
|
|
5949
|
+
maxLength: MAX_REASON_CHARS,
|
|
5950
|
+
description: `Required for action='set'. Audit reason stored on the config-change record; at least ${MIN_REASON_CHARS} characters after trimming.`
|
|
5951
|
+
}
|
|
5952
|
+
},
|
|
5953
|
+
required: ["action", "runner_id"],
|
|
5954
|
+
additionalProperties: false
|
|
5955
|
+
};
|
|
5956
|
+
var description19 = "Gets or sets a paired runner's plane-delivered prepared-job mode (ADR-004 \xA7 11.1) via vo-control-plane GET/POST /api/v1/runner/prepared-job-mode. action='get' returns the stored config ({prepared_job_mode, revision, reason, updated_at, updated_by}, or null when unset); action='set' writes it with optimistic concurrency and returns the new config plus an audit_id. THE TARGET OPERATOR IS DERIVED FROM THE AUTHENTICATED PRINCIPAL, never from this input \u2014 an admin token therefore writes under operator 'admin' and does NOT reach a scoped operator's runner, so use the operator credential from `vo-mcp login`. 'prepared' is refused by the plane with 409 prepared_mode_not_flippable; 'get' is read-only while 'set' is a write and is gated to a stub under VO_ADMIN_CALLABLES_READONLY. Falls back to a clearly-marked `unimplemented` envelope when cloud mode is not configured.";
|
|
5957
|
+
var SHAPE_HINT = `invalid input. Shape: { action: 'get' | 'set', runner_id: non-empty string \u2264${MAX_RUNNER_ID_CHARS} chars }. For action='set' also required: prepared_job_mode ('${PREPARED_JOB_MODES.join("' | '")}'), expected_revision (integer \u2265 0), reason (string \u2265${MIN_REASON_CHARS} chars after trim). Those three fields are NOT accepted with action='get'.`;
|
|
5958
|
+
function parseInput(v) {
|
|
5959
|
+
if (typeof v !== "object" || v === null || Array.isArray(v)) return null;
|
|
5960
|
+
const o = v;
|
|
5961
|
+
const action = o["action"];
|
|
5962
|
+
if (action !== "get" && action !== "set") return null;
|
|
5963
|
+
const rawRunnerId = o["runner_id"];
|
|
5964
|
+
if (typeof rawRunnerId !== "string") return null;
|
|
5965
|
+
const runner_id = rawRunnerId.trim();
|
|
5966
|
+
if (runner_id.length === 0 || runner_id.length > MAX_RUNNER_ID_CHARS) return null;
|
|
5967
|
+
const setOnly = ["prepared_job_mode", "expected_revision", "reason"];
|
|
5968
|
+
if (action === "get") {
|
|
5969
|
+
if (setOnly.some((key) => o[key] !== void 0)) return null;
|
|
5970
|
+
return { action, runner_id };
|
|
5971
|
+
}
|
|
5972
|
+
const mode = o["prepared_job_mode"];
|
|
5973
|
+
if (typeof mode !== "string") return null;
|
|
5974
|
+
if (!PREPARED_JOB_MODES.includes(mode)) return null;
|
|
5975
|
+
const revision = o["expected_revision"];
|
|
5976
|
+
if (typeof revision !== "number") return null;
|
|
5977
|
+
if (!Number.isInteger(revision) || revision < 0) return null;
|
|
5978
|
+
const rawReason = o["reason"];
|
|
5979
|
+
if (typeof rawReason !== "string") return null;
|
|
5980
|
+
const reason = rawReason.trim();
|
|
5981
|
+
if (reason.length < MIN_REASON_CHARS || reason.length > MAX_REASON_CHARS) return null;
|
|
5982
|
+
return {
|
|
5983
|
+
action,
|
|
5984
|
+
runner_id,
|
|
5985
|
+
prepared_job_mode: mode,
|
|
5986
|
+
expected_revision: revision,
|
|
5987
|
+
reason
|
|
5988
|
+
};
|
|
5989
|
+
}
|
|
5990
|
+
async function handlePreparedJobMode(deps, rawInput, _signal) {
|
|
5991
|
+
const input = parseInput(rawInput);
|
|
5992
|
+
if (!input) {
|
|
5993
|
+
throw invalidParams(TOOL_NAME19, SHAPE_HINT);
|
|
5994
|
+
}
|
|
5995
|
+
if (input.action === "get") {
|
|
5996
|
+
return buildCloudOrStubResponse({
|
|
5997
|
+
toolName: TOOL_NAME19,
|
|
5998
|
+
callableName: CALLABLE_NAME10,
|
|
5999
|
+
adminPath: PLANE_PATH,
|
|
6000
|
+
normalizedInput: { action: "get", runner_id: input.runner_id },
|
|
6001
|
+
// A GET carries no body; the runner id rides the query string.
|
|
6002
|
+
cloudBody: {},
|
|
6003
|
+
invokeOptions: { method: "GET", query: { runner_id: input.runner_id } },
|
|
6004
|
+
// The plane answers `{ok, config}` — not the `{ok, callable, result}`
|
|
6005
|
+
// admin-proxy envelope — so take the whole object with `ok` stripped.
|
|
6006
|
+
rawEnvelope: true,
|
|
6007
|
+
gateType: PREPARED_JOB_MODE_GATE_TYPE,
|
|
6008
|
+
stubReason: PREPARED_JOB_MODE_STUB_REASON,
|
|
6009
|
+
readOnly: true,
|
|
6010
|
+
deps
|
|
6011
|
+
});
|
|
6012
|
+
}
|
|
6013
|
+
return buildCloudOrStubResponse({
|
|
6014
|
+
toolName: TOOL_NAME19,
|
|
6015
|
+
callableName: CALLABLE_NAME10,
|
|
6016
|
+
adminPath: PLANE_PATH,
|
|
6017
|
+
normalizedInput: {
|
|
6018
|
+
action: "set",
|
|
6019
|
+
runner_id: input.runner_id,
|
|
6020
|
+
prepared_job_mode: input.prepared_job_mode,
|
|
6021
|
+
expected_revision: input.expected_revision,
|
|
6022
|
+
reason: input.reason
|
|
6023
|
+
},
|
|
6024
|
+
// Exactly the four fields of `updateRunnerPreparedJobConfigInputSchema`
|
|
6025
|
+
// (.strict()) — no `operator_id`, which the route would ignore anyway.
|
|
6026
|
+
cloudBody: {
|
|
6027
|
+
runner_id: input.runner_id,
|
|
6028
|
+
prepared_job_mode: input.prepared_job_mode,
|
|
6029
|
+
expected_revision: input.expected_revision,
|
|
6030
|
+
reason: input.reason
|
|
6031
|
+
},
|
|
6032
|
+
rawEnvelope: true,
|
|
6033
|
+
gateType: PREPARED_JOB_MODE_GATE_TYPE,
|
|
6034
|
+
stubReason: PREPARED_JOB_MODE_STUB_REASON,
|
|
6035
|
+
// A write: stays gated behind VO_ADMIN_CALLABLES_READONLY.
|
|
6036
|
+
readOnly: false,
|
|
6037
|
+
deps
|
|
5018
6038
|
});
|
|
5019
6039
|
}
|
|
5020
6040
|
|
|
@@ -5054,12 +6074,12 @@ function suggestedHandoffPath(session_id, isoTimestamp) {
|
|
|
5054
6074
|
// src/tools/session/report-session-state.ts
|
|
5055
6075
|
init_auth_token_source();
|
|
5056
6076
|
init_credential_store();
|
|
5057
|
-
var
|
|
6077
|
+
var TOOL_NAME20 = "vo_report_session_state";
|
|
5058
6078
|
var VALID_AGENT_TYPES = ["claude-code", "codex", "cursor", "continue"];
|
|
5059
6079
|
var MAX_GOAL_CHARS = 500;
|
|
5060
6080
|
var MAX_RECENT_FILES = 20;
|
|
5061
6081
|
var MAX_RECENT_TOOLS = 50;
|
|
5062
|
-
var
|
|
6082
|
+
var inputSchema20 = {
|
|
5063
6083
|
type: "object",
|
|
5064
6084
|
properties: {
|
|
5065
6085
|
operator_id: {
|
|
@@ -5104,7 +6124,7 @@ var inputSchema19 = {
|
|
|
5104
6124
|
required: ["operator_id", "session_id", "agent_type", "context_used_pct"],
|
|
5105
6125
|
additionalProperties: false
|
|
5106
6126
|
};
|
|
5107
|
-
var
|
|
6127
|
+
var description20 = "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).";
|
|
5108
6128
|
function isStringArray2(v, maxItems) {
|
|
5109
6129
|
if (!Array.isArray(v)) return false;
|
|
5110
6130
|
if (v.length > maxItems) return false;
|
|
@@ -5242,7 +6262,7 @@ async function tryCloudReportState(cloud, input, fetchFn = fetch) {
|
|
|
5242
6262
|
async function handleReportSessionState(deps, rawInput, _signal) {
|
|
5243
6263
|
if (!isToolInput19(rawInput)) {
|
|
5244
6264
|
throw invalidParams(
|
|
5245
|
-
|
|
6265
|
+
TOOL_NAME20,
|
|
5246
6266
|
`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}).`
|
|
5247
6267
|
);
|
|
5248
6268
|
}
|
|
@@ -5273,125 +6293,770 @@ async function handleReportSessionState(deps, rawInput, _signal) {
|
|
|
5273
6293
|
// src/tools/session/spawn-successor.ts
|
|
5274
6294
|
init_common();
|
|
5275
6295
|
import { spawn } from "node:child_process";
|
|
5276
|
-
import { homedir as
|
|
5277
|
-
import { join as
|
|
5278
|
-
import { existsSync as
|
|
5279
|
-
|
|
5280
|
-
|
|
5281
|
-
var
|
|
5282
|
-
|
|
5283
|
-
|
|
5284
|
-
|
|
5285
|
-
|
|
5286
|
-
|
|
5287
|
-
|
|
5288
|
-
|
|
5289
|
-
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
|
|
5297
|
-
|
|
5298
|
-
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
|
|
5311
|
-
|
|
5312
|
-
return true;
|
|
6296
|
+
import { homedir as homedir5 } from "node:os";
|
|
6297
|
+
import { join as join7 } from "node:path";
|
|
6298
|
+
import { closeSync as closeSync2, existsSync as existsSync5, mkdirSync as mkdirSync4, openSync as openSync2, readFileSync as readFileSync7, readdirSync as readdirSync3, statSync as statSync4 } from "node:fs";
|
|
6299
|
+
|
|
6300
|
+
// src/swarm/tier-binding.ts
|
|
6301
|
+
var SWARM_TIERS = Object.freeze([
|
|
6302
|
+
"tier1_subscription",
|
|
6303
|
+
"tier1_local",
|
|
6304
|
+
"tier2_user_key",
|
|
6305
|
+
"tier3_platform_key",
|
|
6306
|
+
"refused",
|
|
6307
|
+
"unresolved"
|
|
6308
|
+
]);
|
|
6309
|
+
var TIER_ADMITS_SPAWN = /* @__PURE__ */ new Set([
|
|
6310
|
+
"tier1_subscription",
|
|
6311
|
+
"tier1_local",
|
|
6312
|
+
"tier2_user_key",
|
|
6313
|
+
"tier3_platform_key"
|
|
6314
|
+
]);
|
|
6315
|
+
var SWARM_TIER_BINDING_ENV = "VO_SWARM_TIER_BINDING";
|
|
6316
|
+
var MAX_BOUND_SUBAGENTS = 20;
|
|
6317
|
+
function isPositiveCap(cap) {
|
|
6318
|
+
return typeof cap === "number" && Number.isFinite(cap) && cap > 0;
|
|
6319
|
+
}
|
|
6320
|
+
function unresolvedBinding(swarmId, nowIso, reason) {
|
|
6321
|
+
return {
|
|
6322
|
+
schema_version: 1,
|
|
6323
|
+
swarm_id: swarmId,
|
|
6324
|
+
tier: "unresolved",
|
|
6325
|
+
agent: null,
|
|
6326
|
+
reason,
|
|
6327
|
+
exhausted_agents: [],
|
|
6328
|
+
subagent_budget: 0,
|
|
6329
|
+
spend_cap_usd: null,
|
|
6330
|
+
resolved_at: nowIso
|
|
6331
|
+
};
|
|
5313
6332
|
}
|
|
5314
|
-
function
|
|
6333
|
+
function serializeSwarmTierBinding(binding) {
|
|
6334
|
+
return JSON.stringify(binding);
|
|
6335
|
+
}
|
|
6336
|
+
function parseSwarmTierBinding(raw, nowIso) {
|
|
6337
|
+
if (typeof raw !== "string" || raw.trim().length === 0) {
|
|
6338
|
+
return unresolvedBinding("", nowIso, "no swarm tier binding present in the environment");
|
|
6339
|
+
}
|
|
6340
|
+
let parsed;
|
|
5315
6341
|
try {
|
|
5316
|
-
|
|
5317
|
-
return entries.length > 0 && entries[0] ? join6(dir, entries[0].f) : null;
|
|
6342
|
+
parsed = JSON.parse(raw);
|
|
5318
6343
|
} catch {
|
|
5319
|
-
return
|
|
6344
|
+
return unresolvedBinding("", nowIso, "swarm tier binding is not valid JSON");
|
|
6345
|
+
}
|
|
6346
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
6347
|
+
return unresolvedBinding("", nowIso, "swarm tier binding is not an object");
|
|
6348
|
+
}
|
|
6349
|
+
const o = parsed;
|
|
6350
|
+
const swarmId = typeof o["swarm_id"] === "string" ? o["swarm_id"] : "";
|
|
6351
|
+
if (o["schema_version"] !== 1) {
|
|
6352
|
+
return unresolvedBinding(swarmId, nowIso, "swarm tier binding has an unsupported schema_version");
|
|
6353
|
+
}
|
|
6354
|
+
const tier = o["tier"];
|
|
6355
|
+
if (typeof tier !== "string" || !SWARM_TIERS.includes(tier)) {
|
|
6356
|
+
return unresolvedBinding(swarmId, nowIso, "swarm tier binding names an unknown tier");
|
|
6357
|
+
}
|
|
6358
|
+
const budget = o["subagent_budget"];
|
|
6359
|
+
const cap = o["spend_cap_usd"];
|
|
6360
|
+
const capNum = isPositiveCap(cap) ? cap : null;
|
|
6361
|
+
if (tier === "tier3_platform_key" && capNum === null) {
|
|
6362
|
+
return unresolvedBinding(
|
|
6363
|
+
swarmId,
|
|
6364
|
+
nowIso,
|
|
6365
|
+
"inherited tier3_platform_key binding carries no positive numeric spend cap \u2014 refusing an uncapped platform-billed fan-out"
|
|
6366
|
+
);
|
|
5320
6367
|
}
|
|
6368
|
+
return {
|
|
6369
|
+
schema_version: 1,
|
|
6370
|
+
swarm_id: swarmId,
|
|
6371
|
+
tier,
|
|
6372
|
+
agent: typeof o["agent"] === "string" ? o["agent"] : null,
|
|
6373
|
+
reason: typeof o["reason"] === "string" ? o["reason"] : "inherited binding carried no reason",
|
|
6374
|
+
exhausted_agents: Array.isArray(o["exhausted_agents"]) ? o["exhausted_agents"].filter((v) => typeof v === "string") : [],
|
|
6375
|
+
subagent_budget: typeof budget === "number" && Number.isFinite(budget) && budget > 0 ? Math.min(Math.floor(budget), MAX_BOUND_SUBAGENTS) : 0,
|
|
6376
|
+
spend_cap_usd: capNum,
|
|
6377
|
+
resolved_at: typeof o["resolved_at"] === "string" ? o["resolved_at"] : nowIso
|
|
6378
|
+
};
|
|
5321
6379
|
}
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
"docs/current/virtual-office-agent-charter.md, -operating-model.md, -test-architect.md",
|
|
5325
|
-
"docs/current/evidence-grounded-consensus-testing.md",
|
|
5326
|
-
"docs/vo/ADR-001-* (verify + sign, human approves merge; no autonomous bot-merge / headless triggers) + docs/vo/vo-adr-002-two-plane-moat.md",
|
|
5327
|
-
"docs/vo/vo-roadmap-2026-05-26.md (read the Change log tail for current state)",
|
|
5328
|
-
"the operator memory index ~/.claude/projects/C--Users-greyl/memory/MEMORY.md"
|
|
5329
|
-
];
|
|
5330
|
-
function buildSuccessorPrompt(handoffMarkdown, goal) {
|
|
5331
|
-
const reads = MANDATORY_READS.map((r, i) => ` ${i + 1}. ${r}`).join("\n");
|
|
5332
|
-
const lines = [
|
|
5333
|
-
"You are the SUCCESSOR agent for a Virtual Office lane. The previous session",
|
|
5334
|
-
"exhausted its context and wrote the handoff below. Read it fully, verify its",
|
|
5335
|
-
'"verification needed" items against live state (a handoff is a claim, not',
|
|
5336
|
-
"evidence \u2014 verify via `git show origin/main:<path>`), then continue the lane.",
|
|
5337
|
-
"",
|
|
5338
|
-
"MANDATORY READS before writing any code (NOT all auto-loaded \u2014 open them):",
|
|
5339
|
-
reads,
|
|
5340
|
-
"",
|
|
5341
|
-
"NON-NEGOTIABLES: multi-model consensus verification is the core; test honesty",
|
|
5342
|
-
"(verified-answer-only, no fake green); verify-before-act + human merge approval;",
|
|
5343
|
-
"never a full functions-shared deploy; Gen2 only; work in a worktree on your own",
|
|
5344
|
-
"branch; finish line is MERGED + DEPLOYED + LIVE-VERIFIED, and VO changes update",
|
|
5345
|
-
"the roadmap in the same PR.",
|
|
5346
|
-
"",
|
|
5347
|
-
"--- HANDOFF ---",
|
|
5348
|
-
handoffMarkdown,
|
|
5349
|
-
"--- END HANDOFF ---"
|
|
5350
|
-
];
|
|
5351
|
-
if (goal && goal.trim().length > 0) lines.push("", `OPERATOR GOAL OVERRIDE: ${goal.trim()}`);
|
|
5352
|
-
return lines.join("\n");
|
|
6380
|
+
function inheritSwarmTierBinding(env, nowIso) {
|
|
6381
|
+
return parseSwarmTierBinding(env[SWARM_TIER_BINDING_ENV], nowIso);
|
|
5353
6382
|
}
|
|
5354
|
-
function
|
|
5355
|
-
|
|
5356
|
-
if (Number.isInteger(maxTurns) && maxTurns > 0) {
|
|
5357
|
-
args.push("--max-turns", String(maxTurns));
|
|
5358
|
-
}
|
|
5359
|
-
return args;
|
|
6383
|
+
function bindingEnvFragment(binding) {
|
|
6384
|
+
return { [SWARM_TIER_BINDING_ENV]: serializeSwarmTierBinding(binding) };
|
|
5360
6385
|
}
|
|
5361
|
-
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
|
|
5367
|
-
return jsonContent({
|
|
5368
|
-
tool: TOOL_NAME20,
|
|
5369
|
-
schema_version: 1,
|
|
5370
|
-
payload: {
|
|
5371
|
-
spawned: false,
|
|
5372
|
-
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)"
|
|
5373
|
-
}
|
|
5374
|
-
});
|
|
6386
|
+
function childBindingEnvFragment(binding, allocatedCapUsd = null) {
|
|
6387
|
+
return bindingEnvFragment(childBinding(binding, allocatedCapUsd));
|
|
6388
|
+
}
|
|
6389
|
+
function admitSubagentSpawn(binding, spawnsSoFar = 0) {
|
|
6390
|
+
if (!TIER_ADMITS_SPAWN.has(binding.tier)) {
|
|
6391
|
+
return { allowed: false, reason: `tier '${binding.tier}' admits no spawn: ${binding.reason}` };
|
|
5375
6392
|
}
|
|
5376
|
-
|
|
6393
|
+
if (binding.tier === "tier3_platform_key" && !isPositiveCap(binding.spend_cap_usd)) {
|
|
6394
|
+
return {
|
|
6395
|
+
allowed: false,
|
|
6396
|
+
reason: `swarm ${binding.swarm_id} is tier3_platform_key with no positive spend cap \u2014 refusing to spend the platform owner's money uncapped`
|
|
6397
|
+
};
|
|
6398
|
+
}
|
|
6399
|
+
if (!Number.isFinite(spawnsSoFar) || spawnsSoFar < 0) {
|
|
6400
|
+
return { allowed: false, reason: "spawn counter is not a finite non-negative number" };
|
|
6401
|
+
}
|
|
6402
|
+
if (spawnsSoFar >= binding.subagent_budget) {
|
|
6403
|
+
return {
|
|
6404
|
+
allowed: false,
|
|
6405
|
+
reason: `swarm ${binding.swarm_id} exhausted its bound subagent budget (${binding.subagent_budget})`
|
|
6406
|
+
};
|
|
6407
|
+
}
|
|
6408
|
+
return { allowed: true, reason: `admitted under tier '${binding.tier}'` };
|
|
6409
|
+
}
|
|
6410
|
+
function childBinding(binding, allocatedCapUsd = null) {
|
|
6411
|
+
const allocated = isPositiveCap(allocatedCapUsd) ? allocatedCapUsd : null;
|
|
6412
|
+
const parentCap = isPositiveCap(binding.spend_cap_usd) ? binding.spend_cap_usd : null;
|
|
6413
|
+
return {
|
|
6414
|
+
...binding,
|
|
6415
|
+
subagent_budget: Math.max(0, binding.subagent_budget - 1),
|
|
6416
|
+
// A child never carries more than its parent, whatever the ledger says: a
|
|
6417
|
+
// forged or hand-edited pool cannot inflate a descendant above the binding
|
|
6418
|
+
// it descends from.
|
|
6419
|
+
spend_cap_usd: allocated === null || parentCap === null ? null : Math.min(allocated, parentCap)
|
|
6420
|
+
};
|
|
6421
|
+
}
|
|
6422
|
+
function agentBindingRefusal(binding, requestedAgent) {
|
|
6423
|
+
const requested = typeof requestedAgent === "string" ? requestedAgent.trim() : "";
|
|
6424
|
+
if (requested.length === 0) return null;
|
|
6425
|
+
if (binding.agent !== null && requested === binding.agent) return null;
|
|
6426
|
+
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)`;
|
|
6427
|
+
}
|
|
6428
|
+
|
|
6429
|
+
// src/swarm/successor-launch.ts
|
|
6430
|
+
var AGENT_LAUNCH_SHAPES = Object.freeze({
|
|
6431
|
+
claude: {
|
|
6432
|
+
bin: "claude",
|
|
6433
|
+
baseArgs: ["-p", "--permission-mode", "acceptEdits"],
|
|
6434
|
+
enforcesMaxTurns: true,
|
|
6435
|
+
maxTurnsFlag: "--max-turns",
|
|
6436
|
+
windowsShellSafe: true
|
|
6437
|
+
},
|
|
6438
|
+
codex: {
|
|
6439
|
+
bin: "codex",
|
|
6440
|
+
baseArgs: ["exec", "--json", "-c", 'approval_policy="never"', "--sandbox", "workspace-write", "--skip-git-repo-check"],
|
|
6441
|
+
enforcesMaxTurns: false,
|
|
6442
|
+
// `-` makes codex read the prompt from stdin (injection-safe), matching how
|
|
6443
|
+
// codex-runner.mjs already spawns it.
|
|
6444
|
+
trailingArgs: ["-"],
|
|
6445
|
+
// `approval_policy="never"` carries embedded quotes; cmd.exe re-parsing is
|
|
6446
|
+
// unverified, so win32 refuses rather than risking a mangled sandbox flag.
|
|
6447
|
+
windowsShellSafe: false
|
|
6448
|
+
}
|
|
6449
|
+
});
|
|
6450
|
+
function resolveSuccessorLaunch(input) {
|
|
6451
|
+
const agent = typeof input.agent === "string" ? input.agent.trim() : "";
|
|
6452
|
+
if (!agent) {
|
|
6453
|
+
return { ok: false, reason: "no agent bound for this spawn \u2014 refusing rather than defaulting to claude" };
|
|
6454
|
+
}
|
|
6455
|
+
const shape = AGENT_LAUNCH_SHAPES[agent];
|
|
6456
|
+
if (!shape) {
|
|
6457
|
+
const known = Object.keys(AGENT_LAUNCH_SHAPES).join(", ");
|
|
6458
|
+
return {
|
|
6459
|
+
ok: false,
|
|
6460
|
+
reason: `no known headless launch shape for agent '${agent}' (known: ${known}) \u2014 refusing rather than guessing its argv`
|
|
6461
|
+
};
|
|
6462
|
+
}
|
|
6463
|
+
const wantsMaxTurns = Number.isInteger(input.maxTurns) && input.maxTurns > 0;
|
|
6464
|
+
if (wantsMaxTurns && !shape.enforcesMaxTurns) {
|
|
6465
|
+
return {
|
|
6466
|
+
ok: false,
|
|
6467
|
+
reason: `agent '${agent}' cannot enforce a max_turns cap \u2014 refusing rather than spawning it unbounded`
|
|
6468
|
+
};
|
|
6469
|
+
}
|
|
6470
|
+
const platform = input.platform ?? process.platform;
|
|
6471
|
+
if (platform === "win32" && !shape.windowsShellSafe) {
|
|
6472
|
+
return {
|
|
6473
|
+
ok: false,
|
|
6474
|
+
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`
|
|
6475
|
+
};
|
|
6476
|
+
}
|
|
6477
|
+
const args = [...shape.baseArgs];
|
|
6478
|
+
if (wantsMaxTurns && shape.maxTurnsFlag) {
|
|
6479
|
+
args.push(shape.maxTurnsFlag, String(input.maxTurns));
|
|
6480
|
+
}
|
|
6481
|
+
if (shape.trailingArgs) args.push(...shape.trailingArgs);
|
|
6482
|
+
return { ok: true, agent, bin: shape.bin, args };
|
|
6483
|
+
}
|
|
6484
|
+
|
|
6485
|
+
// src/swarm/spawn-ledger.ts
|
|
6486
|
+
import { mkdirSync as mkdirSync3, openSync, closeSync, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
|
|
6487
|
+
import { homedir as homedir4 } from "node:os";
|
|
6488
|
+
import { join as join6 } from "node:path";
|
|
6489
|
+
var SWARM_LEDGER_DIR_ENV = "VO_SWARM_LEDGER_DIR";
|
|
6490
|
+
function resolveLedgerDir(env) {
|
|
6491
|
+
const override = env[SWARM_LEDGER_DIR_ENV];
|
|
6492
|
+
if (typeof override === "string" && override.trim().length > 0) return override.trim();
|
|
6493
|
+
return join6(homedir4(), ".vo", "swarm-ledger");
|
|
6494
|
+
}
|
|
6495
|
+
function sanitizeSwarmId(raw) {
|
|
6496
|
+
if (typeof raw !== "string") return null;
|
|
6497
|
+
const id = raw.trim();
|
|
6498
|
+
if (id.length === 0 || id.length > 128) return null;
|
|
6499
|
+
if (!/^[A-Za-z0-9._-]+$/u.test(id)) return null;
|
|
6500
|
+
if (id === "." || id === "..") return null;
|
|
6501
|
+
return id;
|
|
6502
|
+
}
|
|
6503
|
+
var CEILING_FILE = "ceiling.json";
|
|
6504
|
+
function createExclusive(path4, contents) {
|
|
6505
|
+
let fd;
|
|
6506
|
+
try {
|
|
6507
|
+
fd = openSync(path4, "wx");
|
|
6508
|
+
} catch {
|
|
6509
|
+
return false;
|
|
6510
|
+
}
|
|
6511
|
+
try {
|
|
6512
|
+
writeFileSync3(fd, contents, "utf8");
|
|
6513
|
+
} finally {
|
|
6514
|
+
closeSync(fd);
|
|
6515
|
+
}
|
|
6516
|
+
return true;
|
|
6517
|
+
}
|
|
6518
|
+
function capToCents(cap) {
|
|
6519
|
+
return isPositiveCap(cap) ? Math.round(cap * 100) : 0;
|
|
6520
|
+
}
|
|
6521
|
+
function readOrRecordLedgerHead(swarmDir, proposedCeiling, proposedCapCents, nowIso) {
|
|
6522
|
+
const path4 = join6(swarmDir, CEILING_FILE);
|
|
6523
|
+
const head = JSON.stringify({
|
|
6524
|
+
ceiling: proposedCeiling,
|
|
6525
|
+
cap_cents: proposedCapCents,
|
|
6526
|
+
recorded_at: nowIso
|
|
6527
|
+
});
|
|
6528
|
+
if (createExclusive(path4, head)) {
|
|
6529
|
+
return { ceiling: proposedCeiling, capCents: proposedCapCents };
|
|
6530
|
+
}
|
|
6531
|
+
let parsed;
|
|
6532
|
+
try {
|
|
6533
|
+
parsed = JSON.parse(readFileSync6(path4, "utf8"));
|
|
6534
|
+
} catch {
|
|
6535
|
+
return null;
|
|
6536
|
+
}
|
|
6537
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
|
|
6538
|
+
const record = parsed;
|
|
6539
|
+
const recorded = record["ceiling"];
|
|
6540
|
+
if (typeof recorded !== "number" || !Number.isFinite(recorded) || recorded < 1) return null;
|
|
6541
|
+
const recordedCap = record["cap_cents"];
|
|
6542
|
+
const capCents = typeof recordedCap === "number" && Number.isFinite(recordedCap) && recordedCap > 0 ? Math.floor(recordedCap) : 0;
|
|
6543
|
+
return { ceiling: Math.min(Math.floor(recorded), MAX_BOUND_SUBAGENTS), capCents };
|
|
6544
|
+
}
|
|
6545
|
+
var claimSpawnSlot = ({ swarmId, proposedCeiling, proposedCapUsd, dir, nowIso }) => {
|
|
6546
|
+
const id = sanitizeSwarmId(swarmId);
|
|
6547
|
+
if (id === null) {
|
|
6548
|
+
return {
|
|
6549
|
+
ok: false,
|
|
6550
|
+
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`
|
|
6551
|
+
};
|
|
6552
|
+
}
|
|
6553
|
+
const proposed = Number.isFinite(proposedCeiling) ? Math.floor(proposedCeiling) : 0;
|
|
6554
|
+
if (proposed < 1) {
|
|
6555
|
+
return { ok: false, reason: `swarm '${id}' proposes a ceiling of ${proposed} \u2014 no allowance to claim` };
|
|
6556
|
+
}
|
|
6557
|
+
const swarmDir = join6(dir, id);
|
|
6558
|
+
try {
|
|
6559
|
+
mkdirSync3(swarmDir, { recursive: true });
|
|
6560
|
+
} catch (err) {
|
|
6561
|
+
return {
|
|
6562
|
+
ok: false,
|
|
6563
|
+
reason: `swarm '${id}' ledger directory is unwritable (${err instanceof Error ? err.message : String(err)}) \u2014 refusing rather than spawning uncounted`
|
|
6564
|
+
};
|
|
6565
|
+
}
|
|
6566
|
+
const wantedCents = capToCents(proposedCapUsd);
|
|
6567
|
+
const head = readOrRecordLedgerHead(swarmDir, Math.min(proposed, MAX_BOUND_SUBAGENTS), wantedCents, nowIso);
|
|
6568
|
+
if (head === null) {
|
|
6569
|
+
return { ok: false, reason: `swarm '${id}' ledger carries no readable ceiling \u2014 refusing rather than spawning uncounted` };
|
|
6570
|
+
}
|
|
6571
|
+
const { ceiling, capCents } = head;
|
|
6572
|
+
const shareCents = capCents > 0 ? Math.floor(capCents / ceiling) : 0;
|
|
6573
|
+
if (wantedCents > 0 && shareCents < 1) {
|
|
6574
|
+
return {
|
|
6575
|
+
ok: false,
|
|
6576
|
+
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`
|
|
6577
|
+
};
|
|
6578
|
+
}
|
|
6579
|
+
for (let slot = 0; slot < ceiling; slot++) {
|
|
6580
|
+
const debitedCents = shareCents;
|
|
6581
|
+
const remainingCents = capCents > 0 ? capCents - (slot + 1) * shareCents : 0;
|
|
6582
|
+
const claimed = createExclusive(
|
|
6583
|
+
join6(swarmDir, `slot-${slot}.json`),
|
|
6584
|
+
JSON.stringify({
|
|
6585
|
+
slot,
|
|
6586
|
+
ceiling,
|
|
6587
|
+
pid: process.pid,
|
|
6588
|
+
claimed_at: nowIso,
|
|
6589
|
+
// The debit record. Durable and atomic with the claim: this file is
|
|
6590
|
+
// created with O_EXCL, so exactly one claimant ever writes this line.
|
|
6591
|
+
cap_cents_pool: capCents,
|
|
6592
|
+
cap_cents_debited: debitedCents,
|
|
6593
|
+
cap_cents_remaining: remainingCents
|
|
6594
|
+
})
|
|
6595
|
+
);
|
|
6596
|
+
if (claimed) {
|
|
6597
|
+
return {
|
|
6598
|
+
ok: true,
|
|
6599
|
+
slot,
|
|
6600
|
+
ceiling,
|
|
6601
|
+
remaining: ceiling - slot - 1,
|
|
6602
|
+
capUsd: debitedCents > 0 ? debitedCents / 100 : null,
|
|
6603
|
+
capRemainingUsd: capCents > 0 ? remainingCents / 100 : null
|
|
6604
|
+
};
|
|
6605
|
+
}
|
|
6606
|
+
}
|
|
6607
|
+
return {
|
|
6608
|
+
ok: false,
|
|
6609
|
+
reason: `swarm '${id}' has spent its whole fan-out ceiling (${ceiling} spawns across every generation) \u2014 refusing`
|
|
6610
|
+
};
|
|
6611
|
+
};
|
|
6612
|
+
|
|
6613
|
+
// src/swarm/spawn-plan.ts
|
|
6614
|
+
function resolveSpawnPlan(env, input, nowIso, platform = process.platform, claim = claimSpawnSlot) {
|
|
6615
|
+
const rawBinding = env[SWARM_TIER_BINDING_ENV];
|
|
6616
|
+
const hasBinding = typeof rawBinding === "string" && rawBinding.trim().length > 0;
|
|
6617
|
+
if (!hasBinding) {
|
|
6618
|
+
const explicit = input.agent?.trim();
|
|
6619
|
+
const resolved2 = resolveSuccessorLaunch({ agent: explicit || "claude", maxTurns: input.max_turns, platform });
|
|
6620
|
+
if (!resolved2.ok) return { ok: false, reason: resolved2.reason, tier: "unbound" };
|
|
6621
|
+
return {
|
|
6622
|
+
ok: true,
|
|
6623
|
+
bin: resolved2.bin,
|
|
6624
|
+
args: resolved2.args,
|
|
6625
|
+
agent: resolved2.agent,
|
|
6626
|
+
tier: "unbound",
|
|
6627
|
+
bound: false,
|
|
6628
|
+
env: {},
|
|
6629
|
+
slot: null,
|
|
6630
|
+
capUsd: null,
|
|
6631
|
+
capRemainingUsd: null
|
|
6632
|
+
};
|
|
6633
|
+
}
|
|
6634
|
+
const binding = inheritSwarmTierBinding(env, nowIso);
|
|
6635
|
+
const admission = admitSubagentSpawn(binding);
|
|
6636
|
+
if (!admission.allowed) {
|
|
6637
|
+
return { ok: false, reason: admission.reason, tier: binding.tier };
|
|
6638
|
+
}
|
|
6639
|
+
const agentRefusal = agentBindingRefusal(binding, input.agent);
|
|
6640
|
+
if (agentRefusal !== null) return { ok: false, reason: agentRefusal, tier: binding.tier };
|
|
6641
|
+
const resolved = resolveSuccessorLaunch({
|
|
6642
|
+
agent: binding.agent,
|
|
6643
|
+
maxTurns: input.max_turns,
|
|
6644
|
+
platform
|
|
6645
|
+
});
|
|
6646
|
+
if (!resolved.ok) return { ok: false, reason: resolved.reason, tier: binding.tier };
|
|
6647
|
+
const slot = claim({
|
|
6648
|
+
swarmId: binding.swarm_id,
|
|
6649
|
+
proposedCeiling: binding.subagent_budget,
|
|
6650
|
+
// The spend-cap POOL, recorded once per swarm exactly like the ceiling. The
|
|
6651
|
+
// child's cap is DEBITED from it below, not recomputed from this binding.
|
|
6652
|
+
proposedCapUsd: binding.spend_cap_usd,
|
|
6653
|
+
dir: resolveLedgerDir(env),
|
|
6654
|
+
nowIso
|
|
6655
|
+
});
|
|
6656
|
+
if (!slot.ok) return { ok: false, reason: slot.reason, tier: binding.tier };
|
|
6657
|
+
return {
|
|
6658
|
+
ok: true,
|
|
6659
|
+
bin: resolved.bin,
|
|
6660
|
+
args: resolved.args,
|
|
6661
|
+
agent: resolved.agent,
|
|
6662
|
+
tier: binding.tier,
|
|
6663
|
+
bound: true,
|
|
6664
|
+
// Re-export the same TIER with a DECREMENTED budget and the spend cap the
|
|
6665
|
+
// ledger just DEBITED. Exporting the binding verbatim (what this did before
|
|
6666
|
+
// #9312) meant the child re-read the full budget and every generation
|
|
6667
|
+
// restarted at zero. Recomputing the cap from THIS binding (what #9312 did)
|
|
6668
|
+
// bounded a chain but not a tree: three siblings each re-halved the parent's
|
|
6669
|
+
// untouched $50 and walked away with $75 between them.
|
|
6670
|
+
env: childBindingEnvFragment(binding, slot.capUsd),
|
|
6671
|
+
slot: slot.slot,
|
|
6672
|
+
capUsd: slot.capUsd,
|
|
6673
|
+
capRemainingUsd: slot.capRemainingUsd
|
|
6674
|
+
};
|
|
6675
|
+
}
|
|
6676
|
+
|
|
6677
|
+
// ../../scripts/virtual-office/code-runner/windows-claude-launch.mjs
|
|
6678
|
+
import { existsSync as existsSync4, realpathSync } from "node:fs";
|
|
6679
|
+
import { win32 as path3 } from "node:path";
|
|
6680
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
6681
|
+
var NATIVE_CLAUDE_PARTS = [
|
|
6682
|
+
"node_modules",
|
|
6683
|
+
"@anthropic-ai",
|
|
6684
|
+
"claude-code",
|
|
6685
|
+
"bin",
|
|
6686
|
+
"claude.exe"
|
|
6687
|
+
];
|
|
6688
|
+
function pathValue(env) {
|
|
6689
|
+
for (const key of ["Path", "PATH", "path"]) {
|
|
6690
|
+
if (typeof env?.[key] === "string") return env[key];
|
|
6691
|
+
}
|
|
6692
|
+
return "";
|
|
6693
|
+
}
|
|
6694
|
+
function cleanPathSegment(value) {
|
|
6695
|
+
const trimmed = String(value || "").trim();
|
|
6696
|
+
return trimmed.startsWith('"') && trimmed.endsWith('"') ? trimmed.slice(1, -1) : trimmed;
|
|
6697
|
+
}
|
|
6698
|
+
function envValue(env, name) {
|
|
6699
|
+
const exact = env?.[name];
|
|
6700
|
+
if (typeof exact === "string") return exact.trim();
|
|
6701
|
+
const key = Object.keys(env || {}).find((candidate) => candidate.toLowerCase() === name.toLowerCase());
|
|
6702
|
+
return typeof env?.[key] === "string" ? env[key].trim() : "";
|
|
6703
|
+
}
|
|
6704
|
+
function userClaudeCandidates(bin, env) {
|
|
6705
|
+
if (!/^claude(?:\.(?:exe|cmd|ps1))?$/iu.test(bin)) return [];
|
|
6706
|
+
const userProfile = envValue(env, "USERPROFILE");
|
|
6707
|
+
const appData = envValue(env, "APPDATA") || (userProfile ? path3.join(userProfile, "AppData", "Roaming") : "");
|
|
6708
|
+
const localAppData = envValue(env, "LOCALAPPDATA") || (userProfile ? path3.join(userProfile, "AppData", "Local") : "");
|
|
6709
|
+
const candidates = [];
|
|
6710
|
+
if (appData) {
|
|
6711
|
+
const npmBin = path3.join(appData, "npm");
|
|
6712
|
+
candidates.push(
|
|
6713
|
+
path3.join(npmBin, "claude.exe"),
|
|
6714
|
+
path3.join(npmBin, "claude.cmd"),
|
|
6715
|
+
path3.join(npmBin, "claude.ps1"),
|
|
6716
|
+
path3.join(npmBin, "claude"),
|
|
6717
|
+
path3.join(npmBin, ...NATIVE_CLAUDE_PARTS)
|
|
6718
|
+
);
|
|
6719
|
+
}
|
|
6720
|
+
if (userProfile) candidates.push(path3.join(userProfile, ".local", "bin", "claude.exe"));
|
|
6721
|
+
if (localAppData) {
|
|
6722
|
+
candidates.push(
|
|
6723
|
+
path3.join(localAppData, "Microsoft", "WinGet", "Links", "claude.exe"),
|
|
6724
|
+
path3.join(localAppData, "Microsoft", "WindowsApps", "claude.exe")
|
|
6725
|
+
);
|
|
6726
|
+
}
|
|
6727
|
+
return candidates;
|
|
6728
|
+
}
|
|
6729
|
+
function pathCandidates(bin, env) {
|
|
6730
|
+
if (path3.isAbsolute(bin) || /[\\/]/u.test(bin)) {
|
|
6731
|
+
return [path3.resolve(bin)];
|
|
6732
|
+
}
|
|
6733
|
+
const extension = path3.extname(bin);
|
|
6734
|
+
const fromPath = pathValue(env).split(";").map(cleanPathSegment).filter(Boolean).flatMap((directory) => extension ? [path3.join(directory, bin)] : [
|
|
6735
|
+
path3.join(directory, `${bin}.exe`),
|
|
6736
|
+
path3.join(directory, `${bin}.cmd`),
|
|
6737
|
+
path3.join(directory, `${bin}.ps1`),
|
|
6738
|
+
path3.join(directory, bin)
|
|
6739
|
+
]);
|
|
6740
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6741
|
+
return [...fromPath, ...userClaudeCandidates(bin, env)].filter((candidate) => {
|
|
6742
|
+
const key = candidate.toLowerCase();
|
|
6743
|
+
if (seen.has(key)) return false;
|
|
6744
|
+
seen.add(key);
|
|
6745
|
+
return true;
|
|
6746
|
+
});
|
|
6747
|
+
}
|
|
6748
|
+
function canonicalExistingPath(candidate, exists, canonicalize2) {
|
|
6749
|
+
if (!exists(candidate)) return null;
|
|
6750
|
+
try {
|
|
6751
|
+
return canonicalize2(candidate);
|
|
6752
|
+
} catch {
|
|
6753
|
+
return null;
|
|
6754
|
+
}
|
|
6755
|
+
}
|
|
6756
|
+
function resolveWindowsClaudeExecutable({
|
|
6757
|
+
bin = "claude",
|
|
6758
|
+
env = process.env,
|
|
6759
|
+
exists = existsSync4,
|
|
6760
|
+
canonicalize: canonicalize2 = realpathSync
|
|
6761
|
+
} = {}) {
|
|
6762
|
+
const requested = String(bin || "").trim();
|
|
6763
|
+
if (!requested || requested.includes("\0")) {
|
|
6764
|
+
throw new TypeError("Claude executable must be a non-empty path without NUL bytes");
|
|
6765
|
+
}
|
|
6766
|
+
for (const candidate of pathCandidates(requested, env)) {
|
|
6767
|
+
const found = canonicalExistingPath(candidate, exists, canonicalize2);
|
|
6768
|
+
if (!found) continue;
|
|
6769
|
+
if (path3.extname(found).toLowerCase() === ".exe") return found;
|
|
6770
|
+
const native = path3.join(path3.dirname(found), ...NATIVE_CLAUDE_PARTS);
|
|
6771
|
+
const resolvedNative = canonicalExistingPath(native, exists, canonicalize2);
|
|
6772
|
+
if (resolvedNative) return resolvedNative;
|
|
6773
|
+
}
|
|
6774
|
+
const error = new Error(
|
|
6775
|
+
`Could not resolve a native claude.exe for "${requested}". Install or update Claude Code with the native Windows installer (recommended) or npm install -g @anthropic-ai/claude-code; the HQ runner will not execute a shell-only .cmd/.ps1 shim.`
|
|
6776
|
+
);
|
|
6777
|
+
error.code = "ENOENT";
|
|
6778
|
+
throw error;
|
|
6779
|
+
}
|
|
6780
|
+
|
|
6781
|
+
// src/swarm/successor-windows-exe.ts
|
|
6782
|
+
var resolveWindowsClaudeExe = (bin, env) => resolveWindowsClaudeExecutable({ bin, env });
|
|
6783
|
+
function resolveNativeWindowsExecutable(bin, env = process.env, resolve3 = resolveWindowsClaudeExe) {
|
|
6784
|
+
try {
|
|
6785
|
+
return { ok: true, bin: resolve3(bin, env) };
|
|
6786
|
+
} catch (error) {
|
|
6787
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
6788
|
+
return {
|
|
6789
|
+
ok: false,
|
|
6790
|
+
reason: `could not resolve a native Windows executable for '${bin}': ${message}`
|
|
6791
|
+
};
|
|
6792
|
+
}
|
|
6793
|
+
}
|
|
6794
|
+
|
|
6795
|
+
// src/swarm/successor-liveness.ts
|
|
6796
|
+
import { statSync as statSync3 } from "node:fs";
|
|
6797
|
+
var DEFAULT_EARLY_EXIT_SEC = 10;
|
|
6798
|
+
var DEFAULT_NO_OUTPUT_SEC = 0;
|
|
6799
|
+
var DEFAULT_POLL_MS = 200;
|
|
6800
|
+
var KILL_ESCALATION_MS = 2e3;
|
|
6801
|
+
function positiveSeconds(raw, fallback) {
|
|
6802
|
+
if (raw === void 0) return fallback;
|
|
6803
|
+
const n = Number(raw.trim());
|
|
6804
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
6805
|
+
}
|
|
6806
|
+
function resolveLivenessConfigFromEnv(env = process.env, explicitOverride = {}) {
|
|
6807
|
+
const earlyExitMs = explicitOverride.earlyExitMs ?? positiveSeconds(env["VO_MCP_SUCCESSOR_EXIT_CHECK_SEC"], DEFAULT_EARLY_EXIT_SEC) * 1e3;
|
|
6808
|
+
const noOutputMs = explicitOverride.noOutputMs ?? positiveSeconds(env["VO_MCP_SUCCESSOR_OUTPUT_CHECK_SEC"], DEFAULT_NO_OUTPUT_SEC) * 1e3;
|
|
6809
|
+
return { earlyExitMs, noOutputMs };
|
|
6810
|
+
}
|
|
6811
|
+
function defaultStatLogBytes(path4) {
|
|
6812
|
+
try {
|
|
6813
|
+
return statSync3(path4).size;
|
|
6814
|
+
} catch {
|
|
6815
|
+
return 0;
|
|
6816
|
+
}
|
|
6817
|
+
}
|
|
6818
|
+
function defaultKillChild(child) {
|
|
6819
|
+
try {
|
|
6820
|
+
child.kill("SIGTERM");
|
|
6821
|
+
} catch {
|
|
6822
|
+
}
|
|
6823
|
+
const escalation = setTimeout(() => {
|
|
6824
|
+
try {
|
|
6825
|
+
child.kill("SIGKILL");
|
|
6826
|
+
} catch {
|
|
6827
|
+
}
|
|
6828
|
+
}, KILL_ESCALATION_MS);
|
|
6829
|
+
escalation.unref();
|
|
6830
|
+
}
|
|
6831
|
+
function checkSuccessorLiveness(child, logPath, config, deps = {}) {
|
|
6832
|
+
const statLogBytes = deps.statLogBytes ?? defaultStatLogBytes;
|
|
6833
|
+
const now = deps.now ?? Date.now;
|
|
6834
|
+
const pollIntervalMs = deps.pollIntervalMs ?? DEFAULT_POLL_MS;
|
|
6835
|
+
const killChild = deps.killChild ?? defaultKillChild;
|
|
6836
|
+
const outputGateEnabled = Number.isFinite(config.noOutputMs) && config.noOutputMs > 0;
|
|
6837
|
+
const startedAt = now();
|
|
6838
|
+
return new Promise((resolve3) => {
|
|
6839
|
+
let settled = false;
|
|
6840
|
+
let pollTimer = null;
|
|
6841
|
+
const onExit = (code, signal) => {
|
|
6842
|
+
const elapsedMs = now() - startedAt;
|
|
6843
|
+
finish({
|
|
6844
|
+
ok: false,
|
|
6845
|
+
reason: `child_exited_early: exit code ${code ?? "null"} signal ${signal ?? "none"} after ${elapsedMs}ms`,
|
|
6846
|
+
detail: { exitCode: code, signal, elapsedMs }
|
|
6847
|
+
});
|
|
6848
|
+
};
|
|
6849
|
+
const cleanup = () => {
|
|
6850
|
+
if (pollTimer !== null) clearInterval(pollTimer);
|
|
6851
|
+
child.off?.("exit", onExit);
|
|
6852
|
+
};
|
|
6853
|
+
const finish = (result) => {
|
|
6854
|
+
if (settled) return;
|
|
6855
|
+
settled = true;
|
|
6856
|
+
cleanup();
|
|
6857
|
+
if (!result.ok) {
|
|
6858
|
+
try {
|
|
6859
|
+
killChild(child);
|
|
6860
|
+
} catch {
|
|
6861
|
+
}
|
|
6862
|
+
}
|
|
6863
|
+
resolve3(result);
|
|
6864
|
+
};
|
|
6865
|
+
if (typeof child.exitCode === "number" || typeof child.signalCode === "string" && child.signalCode.length > 0) {
|
|
6866
|
+
finish({
|
|
6867
|
+
ok: false,
|
|
6868
|
+
reason: `child_exited_early: exit code ${child.exitCode ?? "null"} signal ${child.signalCode ?? "none"} before the liveness watch attached`,
|
|
6869
|
+
detail: { exitCode: child.exitCode ?? null, signal: child.signalCode ?? null, elapsedMs: 0 }
|
|
6870
|
+
});
|
|
6871
|
+
return;
|
|
6872
|
+
}
|
|
6873
|
+
child.on("exit", onExit);
|
|
6874
|
+
const tick = () => {
|
|
6875
|
+
if (settled) return;
|
|
6876
|
+
const elapsedMs = now() - startedAt;
|
|
6877
|
+
const outputSeen = outputGateEnabled ? statLogBytes(logPath) > 0 : true;
|
|
6878
|
+
if (outputSeen && elapsedMs >= config.earlyExitMs) {
|
|
6879
|
+
finish({ ok: true });
|
|
6880
|
+
return;
|
|
6881
|
+
}
|
|
6882
|
+
if (outputGateEnabled && !outputSeen && elapsedMs >= config.noOutputMs) {
|
|
6883
|
+
finish({
|
|
6884
|
+
ok: false,
|
|
6885
|
+
reason: `no_output: log stayed empty for ${elapsedMs}ms (limit ${config.noOutputMs}ms)`,
|
|
6886
|
+
detail: { elapsedMs, logPath }
|
|
6887
|
+
});
|
|
6888
|
+
}
|
|
6889
|
+
};
|
|
6890
|
+
pollTimer = setInterval(tick, pollIntervalMs);
|
|
6891
|
+
tick();
|
|
6892
|
+
});
|
|
6893
|
+
}
|
|
6894
|
+
|
|
6895
|
+
// src/tools/session/spawn-successor.ts
|
|
6896
|
+
var TOOL_NAME21 = "vo_spawn_successor";
|
|
6897
|
+
var MAX_HANDOFF_BYTES = 64e3;
|
|
6898
|
+
var inputSchema21 = {
|
|
6899
|
+
type: "object",
|
|
6900
|
+
properties: {
|
|
6901
|
+
handoff_path: {
|
|
6902
|
+
type: "string",
|
|
6903
|
+
description: "Path to the handoff doc to pre-inject. Default: the newest .md in ~/.vo/handoffs/."
|
|
6904
|
+
},
|
|
6905
|
+
goal: {
|
|
6906
|
+
type: "string",
|
|
6907
|
+
description: "Optional one-line goal override appended after the handoff."
|
|
6908
|
+
},
|
|
6909
|
+
cwd: {
|
|
6910
|
+
type: "string",
|
|
6911
|
+
description: "Working directory for the successor (default: the repo the handoff names, else process cwd)."
|
|
6912
|
+
},
|
|
6913
|
+
max_turns: {
|
|
6914
|
+
type: "number",
|
|
6915
|
+
description: "Optional --max-turns bound for the successor."
|
|
6916
|
+
},
|
|
6917
|
+
agent: {
|
|
6918
|
+
type: "string",
|
|
6919
|
+
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.`
|
|
6920
|
+
}
|
|
6921
|
+
},
|
|
6922
|
+
required: [],
|
|
6923
|
+
additionalProperties: false
|
|
6924
|
+
};
|
|
6925
|
+
var RETIRED_COUNTER_INPUT = "spawns_so_far";
|
|
6926
|
+
var description21 = "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/. Verifies the child is actually alive (survives an early-exit window; an optional log-output window is off by default) before reporting success. 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.";
|
|
6927
|
+
function isToolInput20(v) {
|
|
6928
|
+
if (typeof v !== "object" || v === null) return false;
|
|
6929
|
+
const o = v;
|
|
6930
|
+
if (o["handoff_path"] !== void 0 && typeof o["handoff_path"] !== "string") return false;
|
|
6931
|
+
if (o["goal"] !== void 0 && typeof o["goal"] !== "string") return false;
|
|
6932
|
+
if (o["cwd"] !== void 0 && typeof o["cwd"] !== "string") return false;
|
|
6933
|
+
if (o["max_turns"] !== void 0 && typeof o["max_turns"] !== "number") return false;
|
|
6934
|
+
if (o["agent"] !== void 0 && typeof o["agent"] !== "string") return false;
|
|
6935
|
+
return true;
|
|
6936
|
+
}
|
|
6937
|
+
function retiredCounterRefusal(v) {
|
|
6938
|
+
if (typeof v !== "object" || v === null) return null;
|
|
6939
|
+
if (!(RETIRED_COUNTER_INPUT in v)) return null;
|
|
6940
|
+
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.`;
|
|
6941
|
+
}
|
|
6942
|
+
function newestHandoff(dir = join7(homedir5(), ".vo", "handoffs")) {
|
|
6943
|
+
try {
|
|
6944
|
+
const entries = readdirSync3(dir).filter((f) => f.endsWith(".md")).map((f) => ({ f, m: statSync4(join7(dir, f)).mtimeMs })).sort((a, b) => b.m - a.m);
|
|
6945
|
+
return entries.length > 0 && entries[0] ? join7(dir, entries[0].f) : null;
|
|
6946
|
+
} catch {
|
|
6947
|
+
return null;
|
|
6948
|
+
}
|
|
6949
|
+
}
|
|
6950
|
+
var MANDATORY_READS = [
|
|
6951
|
+
"CLAUDE.md + AGENTS.md + README.md (repo root)",
|
|
6952
|
+
"docs/current/virtual-office-agent-charter.md, -operating-model.md, -test-architect.md",
|
|
6953
|
+
"docs/current/evidence-grounded-consensus-testing.md",
|
|
6954
|
+
"docs/vo/ADR-001-* (verify + sign, human approves merge; no autonomous bot-merge / headless triggers) + docs/vo/vo-adr-002-two-plane-moat.md",
|
|
6955
|
+
"docs/vo/vo-roadmap-2026-05-26.md (read the Change log tail for current state)",
|
|
6956
|
+
"the operator memory index ~/.claude/projects/C--Users-greyl/memory/MEMORY.md"
|
|
6957
|
+
];
|
|
6958
|
+
function buildSuccessorPrompt(handoffMarkdown, goal) {
|
|
6959
|
+
const reads = MANDATORY_READS.map((r, i) => ` ${i + 1}. ${r}`).join("\n");
|
|
6960
|
+
const lines = [
|
|
6961
|
+
"You are the SUCCESSOR agent for an AlgoHQ lane. The previous session",
|
|
6962
|
+
"exhausted its context and wrote the handoff below. Read it fully, verify its",
|
|
6963
|
+
'"verification needed" items against live state (a handoff is a claim, not',
|
|
6964
|
+
"evidence \u2014 verify via `git show origin/main:<path>`), then continue the lane.",
|
|
6965
|
+
"",
|
|
6966
|
+
"MANDATORY READS before writing any code (NOT all auto-loaded \u2014 open them):",
|
|
6967
|
+
reads,
|
|
6968
|
+
"",
|
|
6969
|
+
"NON-NEGOTIABLES: multi-model consensus verification is the core; test honesty",
|
|
6970
|
+
"(verified-answer-only, no fake green); verify-before-act + human merge approval;",
|
|
6971
|
+
"never a full functions-shared deploy; Gen2 only; work in a worktree on your own",
|
|
6972
|
+
"branch; finish line is MERGED + DEPLOYED + LIVE-VERIFIED, and AlgoHQ changes update",
|
|
6973
|
+
"the roadmap in the same PR.",
|
|
6974
|
+
"",
|
|
6975
|
+
"--- HANDOFF ---",
|
|
6976
|
+
handoffMarkdown,
|
|
6977
|
+
"--- END HANDOFF ---"
|
|
6978
|
+
];
|
|
6979
|
+
if (goal && goal.trim().length > 0) lines.push("", `OPERATOR GOAL OVERRIDE: ${goal.trim()}`);
|
|
6980
|
+
return lines.join("\n");
|
|
6981
|
+
}
|
|
6982
|
+
async function handleSpawnSuccessor(_deps, rawInput, _signal, spawnImpl = spawn, overrides = {}) {
|
|
6983
|
+
const retired = retiredCounterRefusal(rawInput);
|
|
6984
|
+
if (retired !== null) throw invalidParams(TOOL_NAME21, retired);
|
|
6985
|
+
if (!isToolInput20(rawInput)) {
|
|
6986
|
+
throw invalidParams(TOOL_NAME21, "invalid input. Optional: { handoff_path, goal, cwd, max_turns, agent }.");
|
|
6987
|
+
}
|
|
6988
|
+
const handoffPath = rawInput.handoff_path?.trim() || newestHandoff();
|
|
6989
|
+
if (!handoffPath || !existsSync5(handoffPath)) {
|
|
6990
|
+
return jsonContent({
|
|
6991
|
+
tool: TOOL_NAME21,
|
|
6992
|
+
schema_version: 1,
|
|
6993
|
+
payload: {
|
|
6994
|
+
spawned: false,
|
|
6995
|
+
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)"
|
|
6996
|
+
}
|
|
6997
|
+
});
|
|
6998
|
+
}
|
|
6999
|
+
const handoff = readFileSync7(handoffPath, "utf8").slice(0, MAX_HANDOFF_BYTES);
|
|
5377
7000
|
const prompt = buildSuccessorPrompt(handoff, rawInput.goal);
|
|
5378
|
-
const
|
|
5379
|
-
|
|
5380
|
-
|
|
5381
|
-
|
|
5382
|
-
|
|
7001
|
+
const plan = resolveSpawnPlan(process.env, rawInput, (/* @__PURE__ */ new Date()).toISOString());
|
|
7002
|
+
if (!plan.ok) {
|
|
7003
|
+
return jsonContent({
|
|
7004
|
+
tool: TOOL_NAME21,
|
|
7005
|
+
schema_version: 1,
|
|
7006
|
+
payload: {
|
|
7007
|
+
spawned: false,
|
|
7008
|
+
reason: `swarm tier binding refused this spawn: ${plan.reason}`,
|
|
7009
|
+
tier: plan.tier,
|
|
7010
|
+
handoff_path: handoffPath
|
|
7011
|
+
}
|
|
7012
|
+
});
|
|
7013
|
+
}
|
|
7014
|
+
const platform = overrides.platform ?? process.platform;
|
|
7015
|
+
let resolvedBin = plan.bin;
|
|
7016
|
+
if (platform === "win32") {
|
|
7017
|
+
const resolution = resolveNativeWindowsExecutable(plan.bin, process.env, overrides.resolveWindowsExecutable);
|
|
7018
|
+
if (!resolution.ok) {
|
|
7019
|
+
return jsonContent({
|
|
7020
|
+
tool: TOOL_NAME21,
|
|
7021
|
+
schema_version: 1,
|
|
7022
|
+
payload: {
|
|
7023
|
+
spawned: false,
|
|
7024
|
+
reason: resolution.reason,
|
|
7025
|
+
agent: plan.agent,
|
|
7026
|
+
tier: plan.tier,
|
|
7027
|
+
handoff_path: handoffPath
|
|
7028
|
+
}
|
|
7029
|
+
});
|
|
7030
|
+
}
|
|
7031
|
+
resolvedBin = resolution.bin;
|
|
7032
|
+
}
|
|
7033
|
+
const logDir = process.env["VO_MCP_SUCCESSOR_LOG_DIR"]?.trim() || join7(homedir5(), ".vo", "successors");
|
|
7034
|
+
mkdirSync4(logDir, { recursive: true });
|
|
7035
|
+
const logPath = join7(logDir, `successor-${Date.now()}.log`);
|
|
7036
|
+
const logFd = openSync2(logPath, "a");
|
|
7037
|
+
const child = spawnImpl(resolvedBin, [...plan.args], {
|
|
5383
7038
|
cwd: rawInput.cwd?.trim() || process.cwd(),
|
|
5384
7039
|
detached: true,
|
|
5385
7040
|
stdio: ["pipe", logFd, logFd],
|
|
5386
|
-
//
|
|
5387
|
-
//
|
|
5388
|
-
|
|
5389
|
-
|
|
7041
|
+
// Never a shell: `resolvedBin` is either the bare platform-neutral name
|
|
7042
|
+
// (POSIX, resolved by the OS via PATH + shebang) or the native win32 exe
|
|
7043
|
+
// resolved above — routing either through cmd.exe/sh is the extra layer
|
|
7044
|
+
// a detached, unref'd child can lose silently (2026-08-16 incident).
|
|
7045
|
+
shell: false,
|
|
7046
|
+
windowsHide: true,
|
|
7047
|
+
windowsVerbatimArguments: false,
|
|
7048
|
+
// Carry the SAME binding to the child. Without this the successor inherits
|
|
7049
|
+
// no tier and re-resolves its own — which is the split-payer defect one
|
|
7050
|
+
// generation down.
|
|
7051
|
+
...plan.bound ? { env: { ...process.env, ...plan.env } } : {}
|
|
5390
7052
|
});
|
|
7053
|
+
closeSync2(logFd);
|
|
5391
7054
|
let spawnError = null;
|
|
5392
7055
|
child.on("error", (e) => {
|
|
5393
7056
|
spawnError = e.message;
|
|
5394
7057
|
});
|
|
7058
|
+
child.stdin.on?.("error", () => {
|
|
7059
|
+
});
|
|
5395
7060
|
try {
|
|
5396
7061
|
child.stdin.write(prompt);
|
|
5397
7062
|
child.stdin.end();
|
|
@@ -5399,10 +7064,56 @@ async function handleSpawnSuccessor(_deps, rawInput, _signal, spawnImpl = spawn)
|
|
|
5399
7064
|
}
|
|
5400
7065
|
child.unref();
|
|
5401
7066
|
await new Promise((r) => setTimeout(r, 150));
|
|
7067
|
+
if (spawnError) {
|
|
7068
|
+
return jsonContent({
|
|
7069
|
+
tool: TOOL_NAME21,
|
|
7070
|
+
schema_version: 1,
|
|
7071
|
+
payload: {
|
|
7072
|
+
spawned: false,
|
|
7073
|
+
reason: `spawn failed: ${spawnError}`,
|
|
7074
|
+
agent: plan.agent,
|
|
7075
|
+
tier: plan.tier,
|
|
7076
|
+
handoff_path: handoffPath
|
|
7077
|
+
}
|
|
7078
|
+
});
|
|
7079
|
+
}
|
|
7080
|
+
const checkLiveness = overrides.checkLiveness ?? ((c, p) => checkSuccessorLiveness(c, p, resolveLivenessConfigFromEnv(process.env, overrides.livenessConfig), overrides.livenessDeps));
|
|
7081
|
+
const liveness = await checkLiveness(child, logPath);
|
|
7082
|
+
if (!liveness.ok) {
|
|
7083
|
+
return jsonContent({
|
|
7084
|
+
tool: TOOL_NAME21,
|
|
7085
|
+
schema_version: 1,
|
|
7086
|
+
payload: {
|
|
7087
|
+
spawned: false,
|
|
7088
|
+
reason: liveness.reason,
|
|
7089
|
+
pid: child.pid ?? null,
|
|
7090
|
+
log_path: logPath,
|
|
7091
|
+
agent: plan.agent,
|
|
7092
|
+
tier: plan.tier,
|
|
7093
|
+
handoff_path: handoffPath
|
|
7094
|
+
}
|
|
7095
|
+
});
|
|
7096
|
+
}
|
|
5402
7097
|
return jsonContent({
|
|
5403
|
-
tool:
|
|
7098
|
+
tool: TOOL_NAME21,
|
|
5404
7099
|
schema_version: 1,
|
|
5405
|
-
payload:
|
|
7100
|
+
payload: {
|
|
7101
|
+
spawned: true,
|
|
7102
|
+
pid: child.pid ?? null,
|
|
7103
|
+
log_path: logPath,
|
|
7104
|
+
handoff_path: handoffPath,
|
|
7105
|
+
agent: plan.agent,
|
|
7106
|
+
tier: plan.tier,
|
|
7107
|
+
tier_bound: plan.bound,
|
|
7108
|
+
ledger_slot: plan.slot,
|
|
7109
|
+
// The debit, surfaced so an operator can reconcile a fan-out's spend
|
|
7110
|
+
// against the pool without reading the ledger directory by hand.
|
|
7111
|
+
ledger_cap_usd: plan.capUsd,
|
|
7112
|
+
ledger_cap_remaining_usd: plan.capRemainingUsd,
|
|
7113
|
+
// Additive (2026-08-17): true only once the child survived the
|
|
7114
|
+
// early-exit window (and the output window, when that gate is enabled).
|
|
7115
|
+
verified_alive: true
|
|
7116
|
+
}
|
|
5406
7117
|
});
|
|
5407
7118
|
}
|
|
5408
7119
|
|
|
@@ -5427,10 +7138,10 @@ function isKnownConciergePack(value) {
|
|
|
5427
7138
|
}
|
|
5428
7139
|
|
|
5429
7140
|
// src/tools/concierge/dispatch.ts
|
|
5430
|
-
var
|
|
5431
|
-
var
|
|
7141
|
+
var TOOL_NAME22 = "vo_concierge_dispatch";
|
|
7142
|
+
var CALLABLE_NAME11 = "voConciergeDispatch";
|
|
5432
7143
|
var ADMIN_PATH10 = "/api/v1/admin/concierge/dispatch";
|
|
5433
|
-
var
|
|
7144
|
+
var inputSchema22 = {
|
|
5434
7145
|
type: "object",
|
|
5435
7146
|
properties: {
|
|
5436
7147
|
pack: {
|
|
@@ -5445,7 +7156,7 @@ var inputSchema21 = {
|
|
|
5445
7156
|
},
|
|
5446
7157
|
additionalProperties: false
|
|
5447
7158
|
};
|
|
5448
|
-
var
|
|
7159
|
+
var description22 = "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'`.";
|
|
5449
7160
|
function isToolInput21(v) {
|
|
5450
7161
|
if (typeof v !== "object" || v === null) return false;
|
|
5451
7162
|
const obj = v;
|
|
@@ -5456,13 +7167,13 @@ function isToolInput21(v) {
|
|
|
5456
7167
|
async function handleConciergeDispatch(deps, rawInput, _signal) {
|
|
5457
7168
|
if (!isToolInput21(rawInput)) {
|
|
5458
7169
|
throw invalidParams(
|
|
5459
|
-
|
|
7170
|
+
TOOL_NAME22,
|
|
5460
7171
|
"invalid input. Expected { pack?: string, tenant_id?: string }."
|
|
5461
7172
|
);
|
|
5462
7173
|
}
|
|
5463
7174
|
if (rawInput.pack !== void 0 && rawInput.pack !== "" && !isKnownConciergePack(rawInput.pack)) {
|
|
5464
7175
|
throw invalidParams(
|
|
5465
|
-
|
|
7176
|
+
TOOL_NAME22,
|
|
5466
7177
|
`unknown pack: ${JSON.stringify(rawInput.pack)}. Known packs: ${KNOWN_CONCIERGE_PACKS.join(", ")}.`
|
|
5467
7178
|
);
|
|
5468
7179
|
}
|
|
@@ -5473,8 +7184,8 @@ async function handleConciergeDispatch(deps, rawInput, _signal) {
|
|
|
5473
7184
|
if (rawInput.pack) cloudBody.pack = rawInput.pack;
|
|
5474
7185
|
if (rawInput.tenant_id) cloudBody.tenantId = rawInput.tenant_id;
|
|
5475
7186
|
return buildCloudOrStubResponse({
|
|
5476
|
-
toolName:
|
|
5477
|
-
callableName:
|
|
7187
|
+
toolName: TOOL_NAME22,
|
|
7188
|
+
callableName: CALLABLE_NAME11,
|
|
5478
7189
|
adminPath: ADMIN_PATH10,
|
|
5479
7190
|
normalizedInput,
|
|
5480
7191
|
cloudBody,
|
|
@@ -5490,106 +7201,509 @@ async function handleConciergeDispatch(deps, rawInput, _signal) {
|
|
|
5490
7201
|
});
|
|
5491
7202
|
}
|
|
5492
7203
|
|
|
5493
|
-
// src/server.ts
|
|
5494
|
-
init_sync_config();
|
|
5495
|
-
|
|
5496
|
-
// src/tools/memory/private-knowledge.ts
|
|
7204
|
+
// src/server.ts
|
|
7205
|
+
init_sync_config();
|
|
7206
|
+
|
|
7207
|
+
// src/tools/memory/private-knowledge.ts
|
|
7208
|
+
init_common();
|
|
7209
|
+
var UPSERT_TOOL_NAME = "vo_private_knowledge_upsert";
|
|
7210
|
+
var CONTEXT_TOOL_NAME = "vo_private_knowledge_context";
|
|
7211
|
+
var INVALIDATE_TOOL_NAME = "vo_private_knowledge_invalidate";
|
|
7212
|
+
var STALE_TOOL_NAME = "vo_private_knowledge_stale";
|
|
7213
|
+
var KNOWLEDGE_CLASSES = ["memory", "skill", "doctrine", "hook", "command"];
|
|
7214
|
+
var PRECISION_CHAR_BUDGET = 12e3;
|
|
7215
|
+
var upsertInputSchema = {
|
|
7216
|
+
type: "object",
|
|
7217
|
+
properties: {
|
|
7218
|
+
knowledge_class: { type: "string", enum: KNOWLEDGE_CLASSES },
|
|
7219
|
+
source_path: { type: "string", description: "Stable private source identifier; not exposed to other users." },
|
|
7220
|
+
title: { type: "string", description: 'Descriptive, retrieval-friendly title (e.g. "AlgoTax OCR redaction architecture", not "notes") \u2014 retrieval matches on it.' },
|
|
7221
|
+
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." }
|
|
7222
|
+
},
|
|
7223
|
+
required: ["knowledge_class", "source_path", "title", "content"],
|
|
7224
|
+
additionalProperties: false
|
|
7225
|
+
};
|
|
7226
|
+
var contextInputSchema = {
|
|
7227
|
+
type: "object",
|
|
7228
|
+
properties: {
|
|
7229
|
+
query: { type: "string" },
|
|
7230
|
+
limit: { type: "number", minimum: 1, maximum: 50 },
|
|
7231
|
+
knowledge_class: { type: "string", enum: KNOWLEDGE_CLASSES }
|
|
7232
|
+
},
|
|
7233
|
+
required: ["query"],
|
|
7234
|
+
additionalProperties: false
|
|
7235
|
+
};
|
|
7236
|
+
var invalidateInputSchema = {
|
|
7237
|
+
type: "object",
|
|
7238
|
+
properties: {
|
|
7239
|
+
knowledge_class: { type: "string", enum: KNOWLEDGE_CLASSES },
|
|
7240
|
+
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." }
|
|
7241
|
+
},
|
|
7242
|
+
required: ["knowledge_class", "source_path"],
|
|
7243
|
+
additionalProperties: false
|
|
7244
|
+
};
|
|
7245
|
+
var staleInputSchema = {
|
|
7246
|
+
type: "object",
|
|
7247
|
+
properties: {
|
|
7248
|
+
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." },
|
|
7249
|
+
limit: { type: "number", minimum: 1, maximum: 500 }
|
|
7250
|
+
},
|
|
7251
|
+
additionalProperties: false
|
|
7252
|
+
};
|
|
7253
|
+
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.`;
|
|
7254
|
+
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.";
|
|
7255
|
+
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.";
|
|
7256
|
+
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.`;
|
|
7257
|
+
function isStaleInput(value) {
|
|
7258
|
+
if (value === void 0 || value === null) return true;
|
|
7259
|
+
if (typeof value !== "object") return false;
|
|
7260
|
+
const input = value;
|
|
7261
|
+
if (input["days"] !== void 0 && typeof input["days"] !== "number") return false;
|
|
7262
|
+
if (input["limit"] !== void 0 && typeof input["limit"] !== "number") return false;
|
|
7263
|
+
return true;
|
|
7264
|
+
}
|
|
7265
|
+
function isKnowledgeClass(value) {
|
|
7266
|
+
return typeof value === "string" && KNOWLEDGE_CLASSES.includes(value);
|
|
7267
|
+
}
|
|
7268
|
+
function isUpsertInput(value) {
|
|
7269
|
+
if (typeof value !== "object" || value === null) return false;
|
|
7270
|
+
const input = value;
|
|
7271
|
+
return isKnowledgeClass(input["knowledge_class"]) && typeof input["source_path"] === "string" && typeof input["title"] === "string" && typeof input["content"] === "string";
|
|
7272
|
+
}
|
|
7273
|
+
function isInvalidateInput(value) {
|
|
7274
|
+
if (typeof value !== "object" || value === null) return false;
|
|
7275
|
+
const input = value;
|
|
7276
|
+
return isKnowledgeClass(input["knowledge_class"]) && typeof input["source_path"] === "string";
|
|
7277
|
+
}
|
|
7278
|
+
function isContextInput(value) {
|
|
7279
|
+
if (typeof value !== "object" || value === null) return false;
|
|
7280
|
+
const input = value;
|
|
7281
|
+
if (typeof input["query"] !== "string") return false;
|
|
7282
|
+
if (input["limit"] !== void 0 && typeof input["limit"] !== "number") return false;
|
|
7283
|
+
if (input["knowledge_class"] !== void 0 && !isKnowledgeClass(input["knowledge_class"])) return false;
|
|
7284
|
+
return true;
|
|
7285
|
+
}
|
|
7286
|
+
async function getCloudAuth(fetchFn) {
|
|
7287
|
+
const controlPlaneUrl = process.env["VO_CONTROL_PLANE_URL"]?.replace(/\/+$/, "");
|
|
7288
|
+
if (!controlPlaneUrl) {
|
|
7289
|
+
return { ok: false, reason: "VO_CONTROL_PLANE_URL not set \u2014 cloud mode disabled" };
|
|
7290
|
+
}
|
|
7291
|
+
const { createAuthTokenSourceFromEnv: createAuthTokenSourceFromEnv2 } = await Promise.resolve().then(() => (init_auth_token_source(), auth_token_source_exports));
|
|
7292
|
+
const { readStoredCredential: readStoredCredential2 } = await Promise.resolve().then(() => (init_credential_store(), credential_store_exports));
|
|
7293
|
+
const tokenSource = createAuthTokenSourceFromEnv2(process.env, fetchFn, () => readStoredCredential2(process.env));
|
|
7294
|
+
if (!tokenSource) return { ok: false, reason: "No auth configured. Run `vo-mcp login`." };
|
|
7295
|
+
const token = await tokenSource.getToken();
|
|
7296
|
+
if (!token) return { ok: false, reason: "Failed to obtain auth token. Run `vo-mcp login` again." };
|
|
7297
|
+
return { ok: true, controlPlaneUrl, token };
|
|
7298
|
+
}
|
|
7299
|
+
async function callPrivateKnowledge(path4, body, fetchFn) {
|
|
7300
|
+
const auth = await getCloudAuth(fetchFn);
|
|
7301
|
+
if (!auth.ok) return { ok: false, reason: auth.reason };
|
|
7302
|
+
const response = await fetchFn(`${auth.controlPlaneUrl}${path4}`, {
|
|
7303
|
+
method: "POST",
|
|
7304
|
+
headers: {
|
|
7305
|
+
authorization: `Bearer ${auth.token}`,
|
|
7306
|
+
"content-type": "application/json"
|
|
7307
|
+
},
|
|
7308
|
+
body: JSON.stringify(body)
|
|
7309
|
+
});
|
|
7310
|
+
const text = await response.text();
|
|
7311
|
+
let parsed;
|
|
7312
|
+
try {
|
|
7313
|
+
parsed = text ? JSON.parse(text) : null;
|
|
7314
|
+
} catch {
|
|
7315
|
+
parsed = null;
|
|
7316
|
+
}
|
|
7317
|
+
if (response.status < 200 || response.status >= 300) {
|
|
7318
|
+
return { ok: false, status: response.status, response: parsed ?? text };
|
|
7319
|
+
}
|
|
7320
|
+
return parsed;
|
|
7321
|
+
}
|
|
7322
|
+
async function handlePrivateKnowledgeUpsert(_deps, rawInput, _signal, fetchFn = globalThis.fetch) {
|
|
7323
|
+
if (!isUpsertInput(rawInput)) {
|
|
7324
|
+
throw invalidParams(UPSERT_TOOL_NAME, "expected { knowledge_class, source_path, title, content }.");
|
|
7325
|
+
}
|
|
7326
|
+
const payload = await callPrivateKnowledge("/api/v1/knowledge/private", rawInput, fetchFn);
|
|
7327
|
+
const envelope = {
|
|
7328
|
+
tool: UPSERT_TOOL_NAME,
|
|
7329
|
+
schema_version: 1,
|
|
7330
|
+
payload
|
|
7331
|
+
};
|
|
7332
|
+
if (rawInput.content.length > PRECISION_CHAR_BUDGET) {
|
|
7333
|
+
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}.`;
|
|
7334
|
+
}
|
|
7335
|
+
return jsonContent(envelope);
|
|
7336
|
+
}
|
|
7337
|
+
async function handlePrivateKnowledgeInvalidate(_deps, rawInput, _signal, fetchFn = globalThis.fetch) {
|
|
7338
|
+
if (!isInvalidateInput(rawInput)) {
|
|
7339
|
+
throw invalidParams(INVALIDATE_TOOL_NAME, "expected { knowledge_class, source_path }.");
|
|
7340
|
+
}
|
|
7341
|
+
const payload = await callPrivateKnowledge("/api/v1/knowledge/private/invalidate", rawInput, fetchFn);
|
|
7342
|
+
return jsonContent({ tool: INVALIDATE_TOOL_NAME, schema_version: 1, payload });
|
|
7343
|
+
}
|
|
7344
|
+
async function handlePrivateKnowledgeStale(_deps, rawInput, _signal, fetchFn = globalThis.fetch) {
|
|
7345
|
+
if (!isStaleInput(rawInput)) {
|
|
7346
|
+
throw invalidParams(STALE_TOOL_NAME, "expected { optional days, optional limit }.");
|
|
7347
|
+
}
|
|
7348
|
+
const auth = await getCloudAuth(fetchFn);
|
|
7349
|
+
if (!auth.ok) return jsonContent({ tool: STALE_TOOL_NAME, schema_version: 1, payload: { ok: false, reason: auth.reason } });
|
|
7350
|
+
const params = new URLSearchParams();
|
|
7351
|
+
if (rawInput?.days !== void 0) params.set("days", String(Math.trunc(rawInput.days)));
|
|
7352
|
+
if (rawInput?.limit !== void 0) params.set("limit", String(Math.trunc(rawInput.limit)));
|
|
7353
|
+
const qs = params.toString();
|
|
7354
|
+
const response = await fetchFn(`${auth.controlPlaneUrl}/api/v1/knowledge/private/stale${qs ? `?${qs}` : ""}`, {
|
|
7355
|
+
method: "GET",
|
|
7356
|
+
headers: { authorization: `Bearer ${auth.token}` }
|
|
7357
|
+
});
|
|
7358
|
+
const text = await response.text();
|
|
7359
|
+
let parsed;
|
|
7360
|
+
try {
|
|
7361
|
+
parsed = text ? JSON.parse(text) : null;
|
|
7362
|
+
} catch {
|
|
7363
|
+
parsed = null;
|
|
7364
|
+
}
|
|
7365
|
+
const payload = response.status < 200 || response.status >= 300 ? { ok: false, status: response.status, response: parsed ?? text } : parsed;
|
|
7366
|
+
return jsonContent({ tool: STALE_TOOL_NAME, schema_version: 1, payload });
|
|
7367
|
+
}
|
|
7368
|
+
async function handlePrivateKnowledgeContext(_deps, rawInput, _signal, fetchFn = globalThis.fetch) {
|
|
7369
|
+
if (!isContextInput(rawInput)) {
|
|
7370
|
+
throw invalidParams(CONTEXT_TOOL_NAME, "expected { query, optional limit, optional knowledge_class }.");
|
|
7371
|
+
}
|
|
7372
|
+
const payload = await callPrivateKnowledge("/api/v1/knowledge/private/context", rawInput, fetchFn);
|
|
7373
|
+
return jsonContent({ tool: CONTEXT_TOOL_NAME, schema_version: 1, payload });
|
|
7374
|
+
}
|
|
7375
|
+
|
|
7376
|
+
// src/tools/hq/whiteboard.ts
|
|
7377
|
+
init_auth_token_source();
|
|
7378
|
+
init_credential_store();
|
|
7379
|
+
init_common();
|
|
7380
|
+
var POST_TOOL_NAME = "hq_whiteboard_post";
|
|
7381
|
+
var READ_TOOL_NAME = "hq_whiteboard_read";
|
|
7382
|
+
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.";
|
|
7383
|
+
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.";
|
|
7384
|
+
var postInputSchema = {
|
|
7385
|
+
type: "object",
|
|
7386
|
+
properties: {
|
|
7387
|
+
from: { type: "string", minLength: 1, maxLength: 100, description: "Agent/session display name." },
|
|
7388
|
+
type: { type: "string", minLength: 1, maxLength: 64, description: "Message kind, such as intent, worklog, blocker, or completion." },
|
|
7389
|
+
content: { type: "string", minLength: 1, maxLength: 500, description: "Short coordination note." },
|
|
7390
|
+
targetAgent: { type: "string", maxLength: 100 },
|
|
7391
|
+
tester: { type: "string", maxLength: 100 },
|
|
7392
|
+
tier: { type: "string", maxLength: 32 }
|
|
7393
|
+
},
|
|
7394
|
+
required: ["from", "type", "content"],
|
|
7395
|
+
additionalProperties: false
|
|
7396
|
+
};
|
|
7397
|
+
var readInputSchema = {
|
|
7398
|
+
type: "object",
|
|
7399
|
+
properties: {
|
|
7400
|
+
limit: { type: "integer", minimum: 1, maximum: 100, default: 25 },
|
|
7401
|
+
since: { type: "string", description: "Optional ISO-8601 lower bound." },
|
|
7402
|
+
type: { type: "string", minLength: 1, maxLength: 64 }
|
|
7403
|
+
},
|
|
7404
|
+
additionalProperties: false
|
|
7405
|
+
};
|
|
7406
|
+
function resolveTimeoutMs() {
|
|
7407
|
+
const parsed = Number(process.env["HQ_WHITEBOARD_TIMEOUT_MS"]);
|
|
7408
|
+
return Number.isFinite(parsed) && parsed >= 10 && parsed <= 12e4 ? parsed : 1e4;
|
|
7409
|
+
}
|
|
7410
|
+
function isRecord(value) {
|
|
7411
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7412
|
+
}
|
|
7413
|
+
function onlyKeys(value, allowed) {
|
|
7414
|
+
return Object.keys(value).every((key) => allowed.includes(key));
|
|
7415
|
+
}
|
|
7416
|
+
function isBoundedString(value, min, max) {
|
|
7417
|
+
return typeof value === "string" && value.trim().length >= min && value.trim().length <= max;
|
|
7418
|
+
}
|
|
7419
|
+
function parsePostInput(value) {
|
|
7420
|
+
if (!isRecord(value) || !onlyKeys(value, ["from", "type", "content", "targetAgent", "tester", "tier"])) return null;
|
|
7421
|
+
if (!isBoundedString(value["from"], 1, 100)) return null;
|
|
7422
|
+
if (!isBoundedString(value["type"], 1, 64) || !/^[a-zA-Z0-9_-]+$/.test(value["type"].trim())) return null;
|
|
7423
|
+
if (!isBoundedString(value["content"], 1, 500)) return null;
|
|
7424
|
+
for (const [key, max] of [["targetAgent", 100], ["tester", 100], ["tier", 32]]) {
|
|
7425
|
+
if (value[key] !== void 0 && !isBoundedString(value[key], 0, max)) return null;
|
|
7426
|
+
}
|
|
7427
|
+
return {
|
|
7428
|
+
from: value["from"].trim(),
|
|
7429
|
+
type: value["type"].trim(),
|
|
7430
|
+
content: value["content"].trim(),
|
|
7431
|
+
...typeof value["targetAgent"] === "string" ? { targetAgent: value["targetAgent"].trim() } : {},
|
|
7432
|
+
...typeof value["tester"] === "string" ? { tester: value["tester"].trim() } : {},
|
|
7433
|
+
...typeof value["tier"] === "string" ? { tier: value["tier"].trim() } : {}
|
|
7434
|
+
};
|
|
7435
|
+
}
|
|
7436
|
+
function parseReadInput(value) {
|
|
7437
|
+
if (!isRecord(value) || !onlyKeys(value, ["limit", "since", "type"])) return null;
|
|
7438
|
+
if (value["limit"] !== void 0 && (!Number.isInteger(value["limit"]) || Number(value["limit"]) < 1 || Number(value["limit"]) > 100)) return null;
|
|
7439
|
+
if (value["since"] !== void 0 && (typeof value["since"] !== "string" || Number.isNaN(Date.parse(value["since"])))) return null;
|
|
7440
|
+
if (value["type"] !== void 0 && !isBoundedString(value["type"], 1, 64)) return null;
|
|
7441
|
+
return {
|
|
7442
|
+
...typeof value["limit"] === "number" ? { limit: value["limit"] } : {},
|
|
7443
|
+
...typeof value["since"] === "string" ? { since: value["since"] } : {},
|
|
7444
|
+
...typeof value["type"] === "string" ? { type: value["type"].trim() } : {}
|
|
7445
|
+
};
|
|
7446
|
+
}
|
|
7447
|
+
async function resolveCloud(fetchFn) {
|
|
7448
|
+
const url = process.env["VO_CONTROL_PLANE_URL"]?.trim().replace(/\/$/, "");
|
|
7449
|
+
if (!url) return null;
|
|
7450
|
+
try {
|
|
7451
|
+
const source = createAuthTokenSourceFromEnv(process.env, fetchFn, () => readStoredCredential(process.env));
|
|
7452
|
+
const token = await source?.getToken();
|
|
7453
|
+
return token ? { url, token } : null;
|
|
7454
|
+
} catch {
|
|
7455
|
+
return null;
|
|
7456
|
+
}
|
|
7457
|
+
}
|
|
7458
|
+
async function callWhiteboard(method, bodyOrQuery, signal, fetchFn = fetch) {
|
|
7459
|
+
const cloud = await resolveCloud(fetchFn);
|
|
7460
|
+
if (!cloud) {
|
|
7461
|
+
return {
|
|
7462
|
+
ok: false,
|
|
7463
|
+
error: "hq_whiteboard_not_configured",
|
|
7464
|
+
message: "Set VO_CONTROL_PLANE_URL and run vo-mcp login to install a scoped HQ credential."
|
|
7465
|
+
};
|
|
7466
|
+
}
|
|
7467
|
+
const timeoutSignal = AbortSignal.timeout(resolveTimeoutMs());
|
|
7468
|
+
const requestSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
7469
|
+
const query = new URLSearchParams();
|
|
7470
|
+
if (method === "GET") {
|
|
7471
|
+
const input = bodyOrQuery;
|
|
7472
|
+
query.set("limit", String(input.limit ?? 25));
|
|
7473
|
+
if (input.since) query.set("since", input.since);
|
|
7474
|
+
if (input.type) query.set("type", input.type);
|
|
7475
|
+
}
|
|
7476
|
+
try {
|
|
7477
|
+
const response = await fetchFn(
|
|
7478
|
+
`${cloud.url}/api/v1/hq/whiteboard/messages${query.size ? `?${query}` : ""}`,
|
|
7479
|
+
{
|
|
7480
|
+
method,
|
|
7481
|
+
headers: {
|
|
7482
|
+
Authorization: `Bearer ${cloud.token}`,
|
|
7483
|
+
...method === "POST" ? { "Content-Type": "application/json" } : {}
|
|
7484
|
+
},
|
|
7485
|
+
...method === "POST" ? { body: JSON.stringify(bodyOrQuery) } : {},
|
|
7486
|
+
signal: requestSignal
|
|
7487
|
+
}
|
|
7488
|
+
);
|
|
7489
|
+
const text = await response.text();
|
|
7490
|
+
let payload;
|
|
7491
|
+
try {
|
|
7492
|
+
payload = JSON.parse(text);
|
|
7493
|
+
} catch {
|
|
7494
|
+
payload = { ok: false, error: "invalid_response", message: text.slice(0, 200) };
|
|
7495
|
+
}
|
|
7496
|
+
if (!response.ok) {
|
|
7497
|
+
return { ok: false, error: "hq_whiteboard_http_error", status: response.status, response: payload };
|
|
7498
|
+
}
|
|
7499
|
+
return payload;
|
|
7500
|
+
} catch (error) {
|
|
7501
|
+
return {
|
|
7502
|
+
ok: false,
|
|
7503
|
+
error: signal?.aborted ? "cancelled" : timeoutSignal.aborted ? "hq_whiteboard_timeout" : "hq_whiteboard_unreachable",
|
|
7504
|
+
message: error instanceof Error ? error.message.slice(0, 200) : String(error).slice(0, 200)
|
|
7505
|
+
};
|
|
7506
|
+
}
|
|
7507
|
+
}
|
|
7508
|
+
async function handleHqWhiteboardPost(_deps, rawInput, signal) {
|
|
7509
|
+
const input = parsePostInput(rawInput);
|
|
7510
|
+
if (!input) throw invalidParams(POST_TOOL_NAME, "requires from, type, and 1-500 character content; unknown fields are rejected");
|
|
7511
|
+
return jsonContent(await callWhiteboard("POST", input, signal));
|
|
7512
|
+
}
|
|
7513
|
+
async function handleHqWhiteboardRead(_deps, rawInput, signal) {
|
|
7514
|
+
const input = parseReadInput(rawInput);
|
|
7515
|
+
if (!input) throw invalidParams(READ_TOOL_NAME, "limit must be 1-100, since must be ISO-8601, and unknown fields are rejected");
|
|
7516
|
+
return jsonContent(await callWhiteboard("GET", input, signal));
|
|
7517
|
+
}
|
|
7518
|
+
|
|
7519
|
+
// src/tools/skills/skill-corpus.ts
|
|
7520
|
+
import { existsSync as existsSync10, statSync as statSync7 } from "node:fs";
|
|
7521
|
+
import { dirname as dirname5, isAbsolute, join as join13, resolve as resolve2 } from "node:path";
|
|
7522
|
+
|
|
7523
|
+
// ../skill-registry/src/loader.ts
|
|
7524
|
+
import { readdirSync as readdirSync6, readFileSync as readFileSync14, statSync as statSync6 } from "node:fs";
|
|
7525
|
+
import { join as join12 } from "node:path";
|
|
7526
|
+
var InvalidSkillFrontmatterError = class extends Error {
|
|
7527
|
+
constructor(skillFile, reason) {
|
|
7528
|
+
super(`Invalid frontmatter in ${skillFile}: ${reason}`);
|
|
7529
|
+
this.skillFile = skillFile;
|
|
7530
|
+
this.reason = reason;
|
|
7531
|
+
}
|
|
7532
|
+
skillFile;
|
|
7533
|
+
reason;
|
|
7534
|
+
name = "InvalidSkillFrontmatterError";
|
|
7535
|
+
};
|
|
7536
|
+
var FRONTMATTER_DELIMITER = "---";
|
|
7537
|
+
function parseFrontmatter(rawInput, sourcePath) {
|
|
7538
|
+
const raw = rawInput.replace(/\r\n/g, "\n");
|
|
7539
|
+
if (!raw.startsWith(`${FRONTMATTER_DELIMITER}
|
|
7540
|
+
`)) {
|
|
7541
|
+
throw new InvalidSkillFrontmatterError(sourcePath, 'file does not start with frontmatter delimiter "---"');
|
|
7542
|
+
}
|
|
7543
|
+
const afterFirst = raw.slice(FRONTMATTER_DELIMITER.length + 1);
|
|
7544
|
+
const closingIdx = afterFirst.indexOf(`
|
|
7545
|
+
${FRONTMATTER_DELIMITER}
|
|
7546
|
+
`);
|
|
7547
|
+
if (closingIdx === -1) {
|
|
7548
|
+
throw new InvalidSkillFrontmatterError(sourcePath, 'missing closing frontmatter delimiter "---"');
|
|
7549
|
+
}
|
|
7550
|
+
const frontmatterText = afterFirst.slice(0, closingIdx);
|
|
7551
|
+
const body = afterFirst.slice(closingIdx + `
|
|
7552
|
+
${FRONTMATTER_DELIMITER}
|
|
7553
|
+
`.length);
|
|
7554
|
+
let name = "";
|
|
7555
|
+
let description24 = "";
|
|
7556
|
+
for (const line of frontmatterText.split("\n")) {
|
|
7557
|
+
const trimmed = line.trim();
|
|
7558
|
+
if (trimmed.length === 0) continue;
|
|
7559
|
+
const colonIdx = trimmed.indexOf(":");
|
|
7560
|
+
if (colonIdx === -1) continue;
|
|
7561
|
+
const key = trimmed.slice(0, colonIdx).trim();
|
|
7562
|
+
const value = trimmed.slice(colonIdx + 1).trim();
|
|
7563
|
+
if (key === "name") name = value;
|
|
7564
|
+
else if (key === "description") description24 = value;
|
|
7565
|
+
}
|
|
7566
|
+
if (name.length === 0) {
|
|
7567
|
+
throw new InvalidSkillFrontmatterError(sourcePath, 'missing required field "name"');
|
|
7568
|
+
}
|
|
7569
|
+
if (description24.length === 0) {
|
|
7570
|
+
throw new InvalidSkillFrontmatterError(sourcePath, 'missing required field "description"');
|
|
7571
|
+
}
|
|
7572
|
+
return { name, description: description24, body };
|
|
7573
|
+
}
|
|
7574
|
+
function loadSkillsFromDir(skillsDir) {
|
|
7575
|
+
const entries = readdirSync6(skillsDir);
|
|
7576
|
+
const skills = [];
|
|
7577
|
+
for (const entry of entries) {
|
|
7578
|
+
const entryPath = join12(skillsDir, entry);
|
|
7579
|
+
let stat;
|
|
7580
|
+
try {
|
|
7581
|
+
stat = statSync6(entryPath);
|
|
7582
|
+
} catch {
|
|
7583
|
+
continue;
|
|
7584
|
+
}
|
|
7585
|
+
if (!stat.isDirectory()) continue;
|
|
7586
|
+
const skillFile = join12(entryPath, "SKILL.md");
|
|
7587
|
+
let raw;
|
|
7588
|
+
try {
|
|
7589
|
+
raw = readFileSync14(skillFile, "utf8");
|
|
7590
|
+
} catch {
|
|
7591
|
+
continue;
|
|
7592
|
+
}
|
|
7593
|
+
const { name, description: description24, body } = parseFrontmatter(raw, skillFile);
|
|
7594
|
+
skills.push({ name, description: description24, body, sourcePath: skillFile });
|
|
7595
|
+
}
|
|
7596
|
+
return [...skills].sort((a, b) => a.name.localeCompare(b.name));
|
|
7597
|
+
}
|
|
7598
|
+
|
|
7599
|
+
// src/tools/skills/skill-corpus.ts
|
|
5497
7600
|
init_common();
|
|
5498
|
-
var
|
|
5499
|
-
var
|
|
5500
|
-
var
|
|
5501
|
-
var
|
|
5502
|
-
var
|
|
7601
|
+
var LIST_TOOL_NAME = "vo_skill_list";
|
|
7602
|
+
var GET_TOOL_NAME = "vo_skill_get";
|
|
7603
|
+
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.";
|
|
7604
|
+
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.";
|
|
7605
|
+
var listInputSchema = {
|
|
5503
7606
|
type: "object",
|
|
5504
7607
|
properties: {
|
|
5505
|
-
|
|
5506
|
-
|
|
5507
|
-
|
|
5508
|
-
|
|
7608
|
+
refresh: {
|
|
7609
|
+
type: "boolean",
|
|
7610
|
+
description: "Re-scan the skills directory instead of using the cached corpus."
|
|
7611
|
+
}
|
|
5509
7612
|
},
|
|
5510
|
-
required: [
|
|
5511
|
-
additionalProperties: false
|
|
7613
|
+
required: []
|
|
5512
7614
|
};
|
|
5513
|
-
var
|
|
7615
|
+
var getInputSchema = {
|
|
5514
7616
|
type: "object",
|
|
5515
7617
|
properties: {
|
|
5516
|
-
|
|
5517
|
-
|
|
5518
|
-
|
|
7618
|
+
name: {
|
|
7619
|
+
type: "string",
|
|
7620
|
+
description: "Skill name exactly as returned by vo_skill_list."
|
|
7621
|
+
}
|
|
5519
7622
|
},
|
|
5520
|
-
required: ["
|
|
5521
|
-
additionalProperties: false
|
|
7623
|
+
required: ["name"]
|
|
5522
7624
|
};
|
|
5523
|
-
var
|
|
5524
|
-
var
|
|
5525
|
-
function
|
|
5526
|
-
|
|
5527
|
-
|
|
5528
|
-
|
|
5529
|
-
|
|
5530
|
-
|
|
5531
|
-
|
|
7625
|
+
var MAX_WALK_UP_LEVELS = 8;
|
|
7626
|
+
var cachedCorpus = null;
|
|
7627
|
+
function resolveSkillsDir(env = process.env, startDir = process.cwd()) {
|
|
7628
|
+
const override = env.VO_SKILLS_DIR;
|
|
7629
|
+
if (typeof override === "string" && override.length > 0) {
|
|
7630
|
+
const abs = isAbsolute(override) ? override : resolve2(startDir, override);
|
|
7631
|
+
return existsSync10(abs) && statSync7(abs).isDirectory() ? abs : null;
|
|
7632
|
+
}
|
|
7633
|
+
let dir = resolve2(startDir);
|
|
7634
|
+
for (let i = 0; i < MAX_WALK_UP_LEVELS; i += 1) {
|
|
7635
|
+
const candidate = join13(dir, ".claude", "skills");
|
|
7636
|
+
if (existsSync10(candidate) && statSync7(candidate).isDirectory()) return candidate;
|
|
7637
|
+
const parent = dirname5(dir);
|
|
7638
|
+
if (parent === dir) break;
|
|
7639
|
+
dir = parent;
|
|
7640
|
+
}
|
|
7641
|
+
return null;
|
|
5532
7642
|
}
|
|
5533
|
-
function
|
|
5534
|
-
|
|
5535
|
-
|
|
5536
|
-
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
|
|
7643
|
+
function loadCorpus() {
|
|
7644
|
+
const skillsDir = resolveSkillsDir();
|
|
7645
|
+
if (skillsDir === null) {
|
|
7646
|
+
return {
|
|
7647
|
+
skills: [],
|
|
7648
|
+
skillsDir: null,
|
|
7649
|
+
unavailableReason: "No skills directory found. Set VO_SKILLS_DIR or run inside a repo with .claude/skills."
|
|
7650
|
+
};
|
|
7651
|
+
}
|
|
7652
|
+
try {
|
|
7653
|
+
return { skills: loadSkillsFromDir(skillsDir), skillsDir, unavailableReason: null };
|
|
7654
|
+
} catch (err) {
|
|
7655
|
+
const message = err instanceof Error ? `${err.name}: ${err.message}` : String(err);
|
|
7656
|
+
return { skills: [], skillsDir, unavailableReason: message };
|
|
7657
|
+
}
|
|
5540
7658
|
}
|
|
5541
|
-
|
|
5542
|
-
|
|
5543
|
-
|
|
5544
|
-
return { ok: false, reason: "VO_CONTROL_PLANE_URL not set \u2014 cloud mode disabled" };
|
|
7659
|
+
function getCorpus(refresh) {
|
|
7660
|
+
if (refresh || cachedCorpus === null) {
|
|
7661
|
+
cachedCorpus = loadCorpus();
|
|
5545
7662
|
}
|
|
5546
|
-
|
|
5547
|
-
const { readStoredCredential: readStoredCredential2 } = await Promise.resolve().then(() => (init_credential_store(), credential_store_exports));
|
|
5548
|
-
const tokenSource = createAuthTokenSourceFromEnv2(process.env, fetchFn, () => readStoredCredential2(process.env));
|
|
5549
|
-
if (!tokenSource) return { ok: false, reason: "No auth configured. Run `vo-mcp login`." };
|
|
5550
|
-
const token = await tokenSource.getToken();
|
|
5551
|
-
if (!token) return { ok: false, reason: "Failed to obtain auth token. Run `vo-mcp login` again." };
|
|
5552
|
-
return { ok: true, controlPlaneUrl, token };
|
|
7663
|
+
return cachedCorpus;
|
|
5553
7664
|
}
|
|
5554
|
-
async function
|
|
5555
|
-
const
|
|
5556
|
-
|
|
5557
|
-
const
|
|
5558
|
-
|
|
5559
|
-
|
|
5560
|
-
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
7665
|
+
async function handleSkillList(_deps, rawInput) {
|
|
7666
|
+
const input = rawInput ?? {};
|
|
7667
|
+
const refresh = input.refresh === true;
|
|
7668
|
+
const corpus = getCorpus(refresh);
|
|
7669
|
+
return jsonContent({
|
|
7670
|
+
corpus_available: corpus.unavailableReason === null,
|
|
7671
|
+
skills_dir: corpus.skillsDir,
|
|
7672
|
+
unavailable_reason: corpus.unavailableReason,
|
|
7673
|
+
skill_count: corpus.skills.length,
|
|
7674
|
+
skills: corpus.skills.map((s) => ({ name: s.name, description: s.description }))
|
|
5564
7675
|
});
|
|
5565
|
-
const text = await response.text();
|
|
5566
|
-
const parsed = text ? JSON.parse(text) : null;
|
|
5567
|
-
if (response.status < 200 || response.status >= 300) {
|
|
5568
|
-
return { ok: false, status: response.status, response: parsed ?? text };
|
|
5569
|
-
}
|
|
5570
|
-
return parsed;
|
|
5571
7676
|
}
|
|
5572
|
-
async function
|
|
5573
|
-
|
|
5574
|
-
|
|
7677
|
+
async function handleSkillGet(_deps, rawInput) {
|
|
7678
|
+
const input = rawInput ?? {};
|
|
7679
|
+
if (typeof input.name !== "string" || input.name.trim().length === 0) {
|
|
7680
|
+
throw invalidParams(GET_TOOL_NAME, 'input field "name" (non-empty string) is required');
|
|
5575
7681
|
}
|
|
5576
|
-
const
|
|
5577
|
-
const
|
|
5578
|
-
|
|
5579
|
-
|
|
5580
|
-
|
|
5581
|
-
|
|
5582
|
-
|
|
5583
|
-
|
|
7682
|
+
const requested = input.name.trim();
|
|
7683
|
+
const corpus = getCorpus(false);
|
|
7684
|
+
if (corpus.unavailableReason !== null) {
|
|
7685
|
+
return jsonContent({
|
|
7686
|
+
corpus_available: false,
|
|
7687
|
+
unavailable_reason: corpus.unavailableReason,
|
|
7688
|
+
skill: null
|
|
7689
|
+
});
|
|
5584
7690
|
}
|
|
5585
|
-
|
|
5586
|
-
|
|
5587
|
-
|
|
5588
|
-
|
|
5589
|
-
|
|
7691
|
+
const skill = corpus.skills.find((s) => s.name === requested);
|
|
7692
|
+
if (skill === void 0) {
|
|
7693
|
+
throw invalidParams(
|
|
7694
|
+
GET_TOOL_NAME,
|
|
7695
|
+
`unknown skill "${requested}". Known skills: ${corpus.skills.map((s) => s.name).join(", ")}`
|
|
7696
|
+
);
|
|
5590
7697
|
}
|
|
5591
|
-
|
|
5592
|
-
|
|
7698
|
+
return jsonContent({
|
|
7699
|
+
corpus_available: true,
|
|
7700
|
+
skill: {
|
|
7701
|
+
name: skill.name,
|
|
7702
|
+
description: skill.description,
|
|
7703
|
+
instructions: skill.body,
|
|
7704
|
+
source_path: skill.sourcePath
|
|
7705
|
+
}
|
|
7706
|
+
});
|
|
5593
7707
|
}
|
|
5594
7708
|
|
|
5595
7709
|
// src/server.ts
|
|
@@ -5745,7 +7859,7 @@ function buildToolRegistry() {
|
|
|
5745
7859
|
description: description19,
|
|
5746
7860
|
inputSchema: inputSchema19
|
|
5747
7861
|
},
|
|
5748
|
-
handler:
|
|
7862
|
+
handler: handlePreparedJobMode
|
|
5749
7863
|
},
|
|
5750
7864
|
[TOOL_NAME20]: {
|
|
5751
7865
|
definition: {
|
|
@@ -5753,7 +7867,7 @@ function buildToolRegistry() {
|
|
|
5753
7867
|
description: description20,
|
|
5754
7868
|
inputSchema: inputSchema20
|
|
5755
7869
|
},
|
|
5756
|
-
handler:
|
|
7870
|
+
handler: handleReportSessionState
|
|
5757
7871
|
},
|
|
5758
7872
|
[TOOL_NAME21]: {
|
|
5759
7873
|
definition: {
|
|
@@ -5761,7 +7875,7 @@ function buildToolRegistry() {
|
|
|
5761
7875
|
description: description21,
|
|
5762
7876
|
inputSchema: inputSchema21
|
|
5763
7877
|
},
|
|
5764
|
-
handler:
|
|
7878
|
+
handler: handleSpawnSuccessor
|
|
5765
7879
|
},
|
|
5766
7880
|
[TOOL_NAME22]: {
|
|
5767
7881
|
definition: {
|
|
@@ -5769,6 +7883,14 @@ function buildToolRegistry() {
|
|
|
5769
7883
|
description: description22,
|
|
5770
7884
|
inputSchema: inputSchema22
|
|
5771
7885
|
},
|
|
7886
|
+
handler: handleConciergeDispatch
|
|
7887
|
+
},
|
|
7888
|
+
[TOOL_NAME23]: {
|
|
7889
|
+
definition: {
|
|
7890
|
+
name: TOOL_NAME23,
|
|
7891
|
+
description: description23,
|
|
7892
|
+
inputSchema: inputSchema23
|
|
7893
|
+
},
|
|
5772
7894
|
handler: handleSyncConfig
|
|
5773
7895
|
},
|
|
5774
7896
|
[UPSERT_TOOL_NAME]: {
|
|
@@ -5786,11 +7908,59 @@ function buildToolRegistry() {
|
|
|
5786
7908
|
inputSchema: contextInputSchema
|
|
5787
7909
|
},
|
|
5788
7910
|
handler: handlePrivateKnowledgeContext
|
|
7911
|
+
},
|
|
7912
|
+
[INVALIDATE_TOOL_NAME]: {
|
|
7913
|
+
definition: {
|
|
7914
|
+
name: INVALIDATE_TOOL_NAME,
|
|
7915
|
+
description: invalidateDescription,
|
|
7916
|
+
inputSchema: invalidateInputSchema
|
|
7917
|
+
},
|
|
7918
|
+
handler: handlePrivateKnowledgeInvalidate
|
|
7919
|
+
},
|
|
7920
|
+
[STALE_TOOL_NAME]: {
|
|
7921
|
+
definition: {
|
|
7922
|
+
name: STALE_TOOL_NAME,
|
|
7923
|
+
description: staleDescription,
|
|
7924
|
+
inputSchema: staleInputSchema
|
|
7925
|
+
},
|
|
7926
|
+
handler: handlePrivateKnowledgeStale
|
|
7927
|
+
},
|
|
7928
|
+
[POST_TOOL_NAME]: {
|
|
7929
|
+
definition: {
|
|
7930
|
+
name: POST_TOOL_NAME,
|
|
7931
|
+
description: postDescription,
|
|
7932
|
+
inputSchema: postInputSchema
|
|
7933
|
+
},
|
|
7934
|
+
handler: handleHqWhiteboardPost
|
|
7935
|
+
},
|
|
7936
|
+
[READ_TOOL_NAME]: {
|
|
7937
|
+
definition: {
|
|
7938
|
+
name: READ_TOOL_NAME,
|
|
7939
|
+
description: readDescription,
|
|
7940
|
+
inputSchema: readInputSchema
|
|
7941
|
+
},
|
|
7942
|
+
handler: handleHqWhiteboardRead
|
|
7943
|
+
},
|
|
7944
|
+
[LIST_TOOL_NAME]: {
|
|
7945
|
+
definition: {
|
|
7946
|
+
name: LIST_TOOL_NAME,
|
|
7947
|
+
description: listDescription,
|
|
7948
|
+
inputSchema: listInputSchema
|
|
7949
|
+
},
|
|
7950
|
+
handler: handleSkillList
|
|
7951
|
+
},
|
|
7952
|
+
[GET_TOOL_NAME]: {
|
|
7953
|
+
definition: {
|
|
7954
|
+
name: GET_TOOL_NAME,
|
|
7955
|
+
description: getDescription,
|
|
7956
|
+
inputSchema: getInputSchema
|
|
7957
|
+
},
|
|
7958
|
+
handler: handleSkillGet
|
|
5789
7959
|
}
|
|
5790
7960
|
};
|
|
5791
7961
|
}
|
|
5792
7962
|
function createServer(options) {
|
|
5793
|
-
const sessionId = options.sessionId ??
|
|
7963
|
+
const sessionId = options.sessionId ?? randomUUID3();
|
|
5794
7964
|
const mode = createLocalMode();
|
|
5795
7965
|
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
5796
7966
|
const server = new Server(
|
|
@@ -5839,9 +8009,9 @@ function createServer(options) {
|
|
|
5839
8009
|
}
|
|
5840
8010
|
|
|
5841
8011
|
// src/cache/sqlite-cache.ts
|
|
5842
|
-
import { createHash as
|
|
5843
|
-
import { chmodSync as chmodSync3, mkdirSync as
|
|
5844
|
-
import { dirname as
|
|
8012
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
8013
|
+
import { chmodSync as chmodSync3, mkdirSync as mkdirSync7 } from "node:fs";
|
|
8014
|
+
import { dirname as dirname6 } from "node:path";
|
|
5845
8015
|
import { DatabaseSync } from "node:sqlite";
|
|
5846
8016
|
|
|
5847
8017
|
// src/cache/canonicalize.ts
|
|
@@ -5886,7 +8056,7 @@ function normalizeString(s) {
|
|
|
5886
8056
|
function createSqliteCache(options) {
|
|
5887
8057
|
const fileBacked = options.dbPath !== ":memory:";
|
|
5888
8058
|
if (fileBacked) {
|
|
5889
|
-
|
|
8059
|
+
mkdirSync7(dirname6(options.dbPath), { recursive: true, mode: 448 });
|
|
5890
8060
|
}
|
|
5891
8061
|
const versionNamespace = options.cacheVersionNamespace ?? "";
|
|
5892
8062
|
const db = new DatabaseSync(options.dbPath);
|
|
@@ -5917,7 +8087,7 @@ function createSqliteCache(options) {
|
|
|
5917
8087
|
return {
|
|
5918
8088
|
keyFor(toolName, input, opts) {
|
|
5919
8089
|
const canonical = canonicalize(input, opts);
|
|
5920
|
-
const hash =
|
|
8090
|
+
const hash = createHash4("sha256");
|
|
5921
8091
|
if (versionNamespace.length > 0) {
|
|
5922
8092
|
hash.update(versionNamespace);
|
|
5923
8093
|
hash.update("|");
|
|
@@ -6012,7 +8182,7 @@ function createStubRatchetClient() {
|
|
|
6012
8182
|
let m;
|
|
6013
8183
|
while ((m = pat.regex.exec(req.source)) !== null) {
|
|
6014
8184
|
findings.push({
|
|
6015
|
-
line_excerpt:
|
|
8185
|
+
line_excerpt: clip2(m[0], 80),
|
|
6016
8186
|
severity: pat.severity,
|
|
6017
8187
|
code: pat.code,
|
|
6018
8188
|
message: pat.message
|
|
@@ -6044,7 +8214,7 @@ function createStubRatchetClient() {
|
|
|
6044
8214
|
}
|
|
6045
8215
|
};
|
|
6046
8216
|
}
|
|
6047
|
-
function
|
|
8217
|
+
function clip2(s, n) {
|
|
6048
8218
|
return s.length <= n ? s : s.slice(0, n) + "\u2026";
|
|
6049
8219
|
}
|
|
6050
8220
|
function buildSummary2(args) {
|
|
@@ -6055,7 +8225,7 @@ function buildSummary2(args) {
|
|
|
6055
8225
|
// src/consensus/engine-client.ts
|
|
6056
8226
|
init_events_writer();
|
|
6057
8227
|
init_common();
|
|
6058
|
-
import { randomUUID as
|
|
8228
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
6059
8229
|
|
|
6060
8230
|
// src/consensus/null-client.ts
|
|
6061
8231
|
var NULL_CLIENT_DEFAULT_REASON = "consensus-engine-package-pending";
|
|
@@ -6078,63 +8248,59 @@ function createNullConsensusEngineClient(reason = NULL_CLIENT_DEFAULT_REASON) {
|
|
|
6078
8248
|
}
|
|
6079
8249
|
|
|
6080
8250
|
// src/consensus/meta-model-caller.ts
|
|
6081
|
-
var META_MODEL_API_BASE_URL = "https://api.meta.ai/v1";
|
|
6082
8251
|
var META_CONSENSUS_MODEL = "muse-spark-1.1";
|
|
6083
|
-
var META_MODEL_API_KEY_ENV = "MODEL_API_KEY";
|
|
6084
|
-
var META_MODEL_API_KEY_ALIAS = "META_API";
|
|
6085
|
-
function resolveMetaKey(env) {
|
|
6086
|
-
return String(env[META_MODEL_API_KEY_ENV] || env[META_MODEL_API_KEY_ALIAS] || "").trim();
|
|
6087
|
-
}
|
|
6088
|
-
function positiveMaxTokens(value) {
|
|
6089
|
-
const parsed = Math.floor(Number(value));
|
|
6090
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : 2048;
|
|
6091
|
-
}
|
|
6092
8252
|
function createMetaModelCaller(options = {}) {
|
|
6093
|
-
|
|
6094
|
-
|
|
6095
|
-
|
|
6096
|
-
|
|
6097
|
-
|
|
6098
|
-
if (!key) throw new Error(`Missing ${META_MODEL_API_KEY_ENV} for Meta Model API`);
|
|
6099
|
-
const messages = [
|
|
6100
|
-
...systemPrompt ? [{ role: "system", content: systemPrompt }] : [],
|
|
6101
|
-
{ role: "user", content: prompt }
|
|
6102
|
-
];
|
|
6103
|
-
const response = await fetchImpl(`${META_MODEL_API_BASE_URL}/chat/completions`, {
|
|
6104
|
-
method: "POST",
|
|
6105
|
-
headers: {
|
|
6106
|
-
Authorization: `Bearer ${key}`,
|
|
6107
|
-
"Content-Type": "application/json"
|
|
6108
|
-
},
|
|
6109
|
-
body: JSON.stringify({
|
|
6110
|
-
model: model || META_CONSENSUS_MODEL,
|
|
6111
|
-
messages,
|
|
6112
|
-
max_tokens: positiveMaxTokens(maxTokens),
|
|
6113
|
-
reasoning_effort: reasoningEffort
|
|
6114
|
-
}),
|
|
6115
|
-
signal
|
|
6116
|
-
});
|
|
6117
|
-
const payload = await response.json();
|
|
6118
|
-
if (!response.ok) {
|
|
6119
|
-
const message = String(payload.error?.message || response.statusText || "request failed").slice(0, 500);
|
|
6120
|
-
throw Object.assign(new Error(`Meta Model API ${response.status}: ${message}`), { status: response.status });
|
|
6121
|
-
}
|
|
6122
|
-
const content = payload.choices?.[0]?.message?.content;
|
|
6123
|
-
if (typeof content !== "string" || !content.trim()) {
|
|
6124
|
-
throw new Error(`Meta Model API returned no text (finish=${payload.choices?.[0]?.finish_reason || "unknown"})`);
|
|
6125
|
-
}
|
|
6126
|
-
const inputTokens = Number(payload.usage?.prompt_tokens || 0);
|
|
6127
|
-
const outputTokens = Number(payload.usage?.completion_tokens || 0);
|
|
6128
|
-
return {
|
|
6129
|
-
content,
|
|
6130
|
-
inputTokens,
|
|
6131
|
-
outputTokens,
|
|
6132
|
-
totalTokens: Number(payload.usage?.total_tokens || inputTokens + outputTokens)
|
|
6133
|
-
};
|
|
8253
|
+
void options;
|
|
8254
|
+
return async function callMetaWithMetrics2() {
|
|
8255
|
+
throw new Error(
|
|
8256
|
+
"Muse Spark direct consensus is disabled. Use an explicit sanitized task capsule through the AlgoSuite Model Firewall."
|
|
8257
|
+
);
|
|
6134
8258
|
};
|
|
6135
8259
|
}
|
|
6136
8260
|
var callMetaWithMetrics = createMetaModelCaller();
|
|
6137
8261
|
|
|
8262
|
+
// src/consensus/consensus-panel.ts
|
|
8263
|
+
var VO_MCP_CONSENSUS_PANEL = {
|
|
8264
|
+
// claude-opus-5 (2026-07-24). Opus 4.7 was STRICTLY DOMINATED, not merely old:
|
|
8265
|
+
// Opus 5 is $5/$25 per MTok vs Opus 4.7's $15/$75 — a 3x cost cut on this slot,
|
|
8266
|
+
// corroborated by our own catalog (constants/pricing/sciencePricing.ts prices
|
|
8267
|
+
// claude-opus-5 at 0.010 vs claude-opus-4-7 at 0.030) — AND the same 2026-07-24
|
|
8268
|
+
// release note REMOVED fast mode from Opus 4.7 outright: `speed: "fast"` now
|
|
8269
|
+
// returns an error there rather than degrading, unlike the Opus 4.6 removal.
|
|
8270
|
+
// Verified served: GET /v1/models/claude-opus-5 -> HTTP 200 (2026-07-30).
|
|
8271
|
+
//
|
|
8272
|
+
// Claude-5 API safety checked before this swap: Opus 5 rejects `temperature` /
|
|
8273
|
+
// `top_p` / `top_k` and manual `thinking.budget_tokens` with HTTP 400. Neither
|
|
8274
|
+
// the consensus-engine Anthropic adapter nor functions-shared `callAnthropic`
|
|
8275
|
+
// sends any of them, and buildAdaptiveThinking emits `thinking: {type:'adaptive'}`
|
|
8276
|
+
// (the supported form) — so this swap cannot 400.
|
|
8277
|
+
anthropic: "claude-opus-5",
|
|
8278
|
+
// gpt-5.6-terra (GA 2026-07-09; −20% price cut 2026-07-30). NOTE the real IDs
|
|
8279
|
+
// are tiered — `gpt-5.6-sol` / `-terra` / `-luna`; there is NO bare `gpt-5.6`
|
|
8280
|
+
// alias (verified against the served model list, 2026-07-30). Terra is the
|
|
8281
|
+
// cost/capability balance point and the right default for a judgment panel;
|
|
8282
|
+
// Sol is available if verdict quality ever needs it.
|
|
8283
|
+
openai: "gpt-5.6-terra",
|
|
8284
|
+
// gemini-2.5-FLASH (not -pro): flash accepts the default thinkingBudget=0 from
|
|
8285
|
+
// callGeminiWithMetrics; 2.5-pro REJECTS budget 0 ("only works in thinking mode").
|
|
8286
|
+
// Flash is also ~10x cheaper. 2026-06-02.
|
|
8287
|
+
google: "gemini-2.5-flash",
|
|
8288
|
+
deepseek: "deepseek-chat",
|
|
8289
|
+
// Muse Spark identity is owned by meta-model-caller.ts (single source of
|
|
8290
|
+
// truth for the meta slot); re-exported here so the panel stays complete.
|
|
8291
|
+
meta: META_CONSENSUS_MODEL
|
|
8292
|
+
};
|
|
8293
|
+
function getVoMcpConsensusPanel(panel = VO_MCP_CONSENSUS_PANEL) {
|
|
8294
|
+
for (const [provider, modelId] of Object.entries(panel)) {
|
|
8295
|
+
if (typeof modelId !== "string" || modelId.trim().length === 0) {
|
|
8296
|
+
throw new Error(
|
|
8297
|
+
`getVoMcpConsensusPanel: panel slot "${provider}" has a missing or blank model ID`
|
|
8298
|
+
);
|
|
8299
|
+
}
|
|
8300
|
+
}
|
|
8301
|
+
return panel;
|
|
8302
|
+
}
|
|
8303
|
+
|
|
6138
8304
|
// src/consensus/engine-options.ts
|
|
6139
8305
|
var AGREEMENT_GATE_ENV_VAR = "VO_CONSENSUS_AGREEMENT_GATE";
|
|
6140
8306
|
function isTruthyFlag(raw) {
|
|
@@ -6188,6 +8354,14 @@ function shadowEnabled(env) {
|
|
|
6188
8354
|
const norm = raw.trim().toLowerCase();
|
|
6189
8355
|
return !(norm === "0" || norm === "false" || norm === "no" || norm === "off" || norm === "");
|
|
6190
8356
|
}
|
|
8357
|
+
var MIN_RESPONDERS_ENV_VAR = "VO_CONSENSUS_MIN_RESPONDERS";
|
|
8358
|
+
function resolveMinResponders(env) {
|
|
8359
|
+
const raw = (env ?? {})[MIN_RESPONDERS_ENV_VAR];
|
|
8360
|
+
if (raw === void 0 || raw.trim() === "") return 2;
|
|
8361
|
+
const parsed = Number.parseInt(raw.trim(), 10);
|
|
8362
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return void 0;
|
|
8363
|
+
return parsed;
|
|
8364
|
+
}
|
|
6191
8365
|
function mapShadowSynthesis(s) {
|
|
6192
8366
|
if (s === void 0) return void 0;
|
|
6193
8367
|
return {
|
|
@@ -6337,9 +8511,11 @@ function createEngineConsensusClient(options) {
|
|
|
6337
8511
|
...options.agreement_gate_enabled !== void 0 ? { configEnabled: options.agreement_gate_enabled } : {},
|
|
6338
8512
|
...options.env !== void 0 ? { env: options.env } : {}
|
|
6339
8513
|
});
|
|
8514
|
+
const minResponders = resolveMinResponders(options.env);
|
|
6340
8515
|
const engineOptions = {
|
|
6341
8516
|
panel,
|
|
6342
8517
|
...options.per_model_timeout_ms !== void 0 ? { per_model_timeout_ms: options.per_model_timeout_ms } : {},
|
|
8518
|
+
...minResponders !== void 0 ? { min_responders: minResponders } : {},
|
|
6343
8519
|
...agreementGate !== void 0 ? { agreement_gate: agreementGate } : {},
|
|
6344
8520
|
// Stage A7-shadow: run the adaptive verdict alongside the live one for grading.
|
|
6345
8521
|
// Cheap (pure log-odds over already-fetched verdicts; no extra model calls),
|
|
@@ -6348,7 +8524,7 @@ function createEngineConsensusClient(options) {
|
|
|
6348
8524
|
shadow_synthesis: { enabled: shadowEnabled(options.env) }
|
|
6349
8525
|
};
|
|
6350
8526
|
const sources = request.source_urls;
|
|
6351
|
-
const useSourceGrounded = sources !== void 0 && sources.length > 0
|
|
8527
|
+
const useSourceGrounded = sources !== void 0 && sources.length > 0;
|
|
6352
8528
|
let response;
|
|
6353
8529
|
let sourceExtras;
|
|
6354
8530
|
if (useSourceGrounded) {
|
|
@@ -6392,8 +8568,13 @@ function createEngineConsensusClient(options) {
|
|
|
6392
8568
|
synthesized_verdict: response.synthesized_verdict,
|
|
6393
8569
|
per_model_verdicts: response.per_model_verdicts,
|
|
6394
8570
|
degraded: response.degraded,
|
|
8571
|
+
...response.quorum_failed === true ? { quorum_failed: true } : {},
|
|
6395
8572
|
duration_ms: response.duration_ms,
|
|
6396
8573
|
engine_version: response.engine_version,
|
|
8574
|
+
// Cumulative cross-round inference usage (B44-3). Absent when no panel
|
|
8575
|
+
// member reported usage; forwarded verbatim — the aggregator prefers it
|
|
8576
|
+
// over summing final-round verdicts (which under-reports deliberation).
|
|
8577
|
+
...response.token_usage !== void 0 ? { token_usage: response.token_usage } : {},
|
|
6397
8578
|
// Phase 2 Lane D-1 — forward escalation signal when present. The
|
|
6398
8579
|
// source-grounded layer's own escalation (from the citation grade)
|
|
6399
8580
|
// takes precedence when set, else the synthesizer's.
|
|
@@ -6403,6 +8584,10 @@ function createEngineConsensusClient(options) {
|
|
|
6403
8584
|
...mapFanOutDiagnostics(response.fan_out_diagnostics) !== void 0 ? { fan_out_diagnostics: mapFanOutDiagnostics(response.fan_out_diagnostics) } : {},
|
|
6404
8585
|
// Stage A7-shadow — adaptive-vs-incumbent comparison (PII-free; present iff shadow on).
|
|
6405
8586
|
...mapShadowSynthesis(response.shadow_synthesis) !== void 0 ? { shadow_synthesis: mapShadowSynthesis(response.shadow_synthesis) } : {},
|
|
8587
|
+
// Critique-uptake (2026-07-20 red-team fix) — verifier-critique
|
|
8588
|
+
// visibility report; previously computed by the engine on every
|
|
8589
|
+
// call but dropped at this boundary.
|
|
8590
|
+
...response.critique_uptake !== void 0 ? { critique_uptake: response.critique_uptake } : {},
|
|
6406
8591
|
// Source-grounded additive outputs (Tier-4 features).
|
|
6407
8592
|
...useSourceGrounded ? { source_grounded: true } : {},
|
|
6408
8593
|
...sourceExtras?.citation_grade !== void 0 ? { citation_grade: sourceExtras.citation_grade } : {},
|
|
@@ -6418,27 +8603,12 @@ function createEngineConsensusClient(options) {
|
|
|
6418
8603
|
}
|
|
6419
8604
|
};
|
|
6420
8605
|
}
|
|
6421
|
-
var DEFAULT_MODELS =
|
|
6422
|
-
// These ids match the strategic-roadmap §4 `newsStandard` / `newsDeep` panel
|
|
6423
|
-
// intent — current production model ids. Per handoff §C-3 these MUST come
|
|
6424
|
-
// from `CONSENSUS_PANELS` in `functions-shared/shared-model-resolvers.ts`
|
|
6425
|
-
// for V1; placeholder defaults here keep Phase 2 Lane A non-blocking.
|
|
6426
|
-
anthropic: "claude-opus-4-7",
|
|
6427
|
-
openai: "gpt-5",
|
|
6428
|
-
// gemini-2.5-FLASH (not -pro): flash accepts the default thinkingBudget=0 from
|
|
6429
|
-
// callGeminiWithMetrics; 2.5-pro REJECTS budget 0 ("only works in thinking mode").
|
|
6430
|
-
// Flash is also ~10x cheaper. 2026-06-02.
|
|
6431
|
-
google: "gemini-2.5-flash",
|
|
6432
|
-
deepseek: "deepseek-chat",
|
|
6433
|
-
meta: META_CONSENSUS_MODEL
|
|
6434
|
-
};
|
|
8606
|
+
var DEFAULT_MODELS = getVoMcpConsensusPanel();
|
|
6435
8607
|
function probeProviders(env = process.env) {
|
|
6436
8608
|
const out = [];
|
|
6437
8609
|
if ((env["ANTHROPIC_API_KEY"] ?? "").trim().length > 0) out.push("anthropic");
|
|
6438
8610
|
if ((env["OPENAI_API_KEY"] ?? "").trim().length > 0) out.push("openai");
|
|
6439
8611
|
if ((env["GOOGLE_API_KEY"] ?? "").trim().length > 0) out.push("google");
|
|
6440
|
-
if ((env["DEEPSEEK_API_KEY"] ?? "").trim().length > 0) out.push("deepseek");
|
|
6441
|
-
if ((env[META_MODEL_API_KEY_ENV] ?? "").trim().length > 0 || (env[META_MODEL_API_KEY_ALIAS] ?? "").trim().length > 0) out.push("meta");
|
|
6442
8612
|
return out;
|
|
6443
8613
|
}
|
|
6444
8614
|
async function loadFactoryAndCallers(injectedEngine, injectedShared) {
|
|
@@ -6483,25 +8653,20 @@ async function tryCreateEngineConsensusClientFromEnvAsync(options = {}) {
|
|
|
6483
8653
|
const callerByProvider = {
|
|
6484
8654
|
anthropic: loaded.shared.callAnthropicWithMetrics,
|
|
6485
8655
|
openai: loaded.shared.callOpenAIWithMetrics,
|
|
6486
|
-
google: loaded.shared.callGeminiWithMetrics
|
|
6487
|
-
deepseek: loaded.shared.callDeepSeekWithMetrics,
|
|
6488
|
-
meta: options.metaCaller ?? callMetaWithMetrics
|
|
8656
|
+
google: loaded.shared.callGeminiWithMetrics
|
|
6489
8657
|
};
|
|
6490
8658
|
const modelByProvider = {
|
|
6491
8659
|
anthropic: options.models?.anthropic ?? DEFAULT_MODELS.anthropic,
|
|
6492
8660
|
openai: options.models?.openai ?? DEFAULT_MODELS.openai,
|
|
6493
|
-
google: options.models?.google ?? DEFAULT_MODELS.google
|
|
6494
|
-
deepseek: options.models?.deepseek ?? DEFAULT_MODELS.deepseek,
|
|
6495
|
-
meta: options.models?.meta ?? DEFAULT_MODELS.meta
|
|
8661
|
+
google: options.models?.google ?? DEFAULT_MODELS.google
|
|
6496
8662
|
};
|
|
6497
|
-
const adapterEnv = !(env[META_MODEL_API_KEY_ENV] ?? "").trim() && (env[META_MODEL_API_KEY_ALIAS] ?? "").trim() ? { ...env, [META_MODEL_API_KEY_ENV]: env[META_MODEL_API_KEY_ALIAS] } : env;
|
|
6498
8663
|
const panel = [];
|
|
6499
8664
|
for (const p of providers) {
|
|
6500
8665
|
try {
|
|
6501
8666
|
const adapter = loaded.engine.createAdapter(p, {
|
|
6502
8667
|
model: modelByProvider[p],
|
|
6503
8668
|
caller: callerByProvider[p],
|
|
6504
|
-
envSource:
|
|
8669
|
+
envSource: env
|
|
6505
8670
|
});
|
|
6506
8671
|
panel.push(adapter);
|
|
6507
8672
|
} catch {
|
|
@@ -6535,10 +8700,13 @@ function tryCreateEngineConsensusClientFromEnv(options = {}) {
|
|
|
6535
8700
|
}
|
|
6536
8701
|
|
|
6537
8702
|
// src/consensus/moat-client.ts
|
|
6538
|
-
import { randomUUID as
|
|
8703
|
+
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
6539
8704
|
|
|
6540
8705
|
// src/consensus/client.ts
|
|
6541
8706
|
var CANCELLED_REASON = "cancelled";
|
|
8707
|
+
function hasLocalConsensusQuorum(result) {
|
|
8708
|
+
return result.execution_source !== "cloud" && result.quorum_failed !== true && result.per_model_verdicts.filter((verdict) => verdict.verdict !== "error").length >= 2;
|
|
8709
|
+
}
|
|
6542
8710
|
|
|
6543
8711
|
// src/consensus/moat-client.ts
|
|
6544
8712
|
var DEFAULT_VO_MOAT_ENDPOINT = "https://vo-moat-plane-620873201932.us-central1.run.app";
|
|
@@ -6554,7 +8722,9 @@ var MOAT_CLIENT_REASONS = {
|
|
|
6554
8722
|
/** Network failure / local timeout (not the caller's cancellation). */
|
|
6555
8723
|
NETWORK: "consensus-cloud-network-error",
|
|
6556
8724
|
/** Excerpt exceeds the server's 50KB cap — refused locally with a clear reason. */
|
|
6557
|
-
EXCERPT_TOO_LARGE: "consensus-cloud-excerpt-too-large"
|
|
8725
|
+
EXCERPT_TOO_LARGE: "consensus-cloud-excerpt-too-large",
|
|
8726
|
+
/** Cloud v1 cannot retrieve or grade caller-supplied authoritative sources. */
|
|
8727
|
+
SOURCE_GROUNDING_UNSUPPORTED: "consensus-cloud-source-grounding-unsupported"
|
|
6558
8728
|
};
|
|
6559
8729
|
function isVerdict(v) {
|
|
6560
8730
|
if (!v || typeof v !== "object") return false;
|
|
@@ -6578,6 +8748,9 @@ function createMoatConsensusClient(opts) {
|
|
|
6578
8748
|
return {
|
|
6579
8749
|
async run(request) {
|
|
6580
8750
|
if (request.signal?.aborted) return { ok: false, reason: CANCELLED_REASON };
|
|
8751
|
+
if (request.source_urls !== void 0 && request.source_urls.length > 0) {
|
|
8752
|
+
return { ok: false, reason: MOAT_CLIENT_REASONS.SOURCE_GROUNDING_UNSUPPORTED };
|
|
8753
|
+
}
|
|
6581
8754
|
if (Buffer.byteLength(request.prompt, "utf8") > MOAT_EXCERPT_MAX_BYTES) {
|
|
6582
8755
|
return { ok: false, reason: MOAT_CLIENT_REASONS.EXCERPT_TOO_LARGE };
|
|
6583
8756
|
}
|
|
@@ -6587,7 +8760,7 @@ function createMoatConsensusClient(opts) {
|
|
|
6587
8760
|
request.signal?.addEventListener("abort", onAbort, { once: true });
|
|
6588
8761
|
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
6589
8762
|
const body = JSON.stringify({
|
|
6590
|
-
task_id:
|
|
8763
|
+
task_id: randomUUID5(),
|
|
6591
8764
|
gate_type: request.gate_type,
|
|
6592
8765
|
excerpt: request.prompt,
|
|
6593
8766
|
...request.system_prompt ? { question: request.system_prompt } : {},
|
|
@@ -6616,8 +8789,11 @@ function createMoatConsensusClient(opts) {
|
|
|
6616
8789
|
}
|
|
6617
8790
|
const agreeing = normalizeAgreeing(parsed.models_agreeing);
|
|
6618
8791
|
const reasoning = agreeing !== void 0 ? `${parsed.reason} (${agreeing} models agreeing)` : parsed.reason;
|
|
8792
|
+
const receiptId = typeof parsed.decision_id === "string" && parsed.decision_id.trim().length > 0 ? parsed.decision_id.trim().slice(0, 120) : null;
|
|
6619
8793
|
return {
|
|
6620
8794
|
ok: true,
|
|
8795
|
+
execution_source: "cloud",
|
|
8796
|
+
...receiptId ? { receipt_id: receiptId } : {},
|
|
6621
8797
|
synthesized_verdict: {
|
|
6622
8798
|
verdict: parsed.approved ? "pass" : "fail",
|
|
6623
8799
|
confidence: normalizeConfidence(parsed.confidence),
|
|
@@ -6658,6 +8834,178 @@ function tryCreateMoatConsensusClientFromEnv(env = process.env, fetchFn) {
|
|
|
6658
8834
|
});
|
|
6659
8835
|
}
|
|
6660
8836
|
|
|
8837
|
+
// src/consensus/fallback-client.ts
|
|
8838
|
+
var INSUFFICIENT_LOCAL_VERDICTS_REASON = "local-panel-insufficient-valid-verdicts";
|
|
8839
|
+
function createConsensusFallbackClient(primary, fallback, options = {}) {
|
|
8840
|
+
return {
|
|
8841
|
+
async run(request) {
|
|
8842
|
+
const primaryResult = await primary.run(request);
|
|
8843
|
+
if (request.signal?.aborted || !primaryResult.ok && primaryResult.reason === CANCELLED_REASON) {
|
|
8844
|
+
return primaryResult;
|
|
8845
|
+
}
|
|
8846
|
+
if (primaryResult.ok) {
|
|
8847
|
+
if (primaryResult.execution_source === "cloud" || hasLocalConsensusQuorum(primaryResult)) {
|
|
8848
|
+
return primaryResult;
|
|
8849
|
+
}
|
|
8850
|
+
options.onFallback?.(INSUFFICIENT_LOCAL_VERDICTS_REASON);
|
|
8851
|
+
return fallback.run(request);
|
|
8852
|
+
}
|
|
8853
|
+
options.onFallback?.(primaryResult.reason);
|
|
8854
|
+
return fallback.run(request);
|
|
8855
|
+
}
|
|
8856
|
+
};
|
|
8857
|
+
}
|
|
8858
|
+
|
|
8859
|
+
// src/consensus/shadow-client.ts
|
|
8860
|
+
import { appendFileSync as appendFileSync2, chmodSync as chmodSync4, mkdirSync as mkdirSync8, renameSync, statSync as statSync8 } from "node:fs";
|
|
8861
|
+
import { homedir as homedir8 } from "node:os";
|
|
8862
|
+
import { dirname as dirname7, join as join14 } from "node:path";
|
|
8863
|
+
var DEFAULT_SHADOW_TIMEOUT_MS = 2e4;
|
|
8864
|
+
var SHADOW_RECEIPT_MAX_BYTES = 20 * 1024 * 1024;
|
|
8865
|
+
function defaultShadowReceiptPath(env = process.env) {
|
|
8866
|
+
const p = (env["VO_MCP_MOAT_SHADOW_PATH"] ?? "").trim();
|
|
8867
|
+
return p || join14(homedir8(), ".claude", "vo-mcp-moat-shadow.jsonl");
|
|
8868
|
+
}
|
|
8869
|
+
function appendShadowReceipt(receipt, path4 = defaultShadowReceiptPath()) {
|
|
8870
|
+
try {
|
|
8871
|
+
mkdirSync8(dirname7(path4), { recursive: true, mode: 448 });
|
|
8872
|
+
try {
|
|
8873
|
+
if (statSync8(path4).size > SHADOW_RECEIPT_MAX_BYTES) renameSync(path4, `${path4}.1`);
|
|
8874
|
+
} catch {
|
|
8875
|
+
}
|
|
8876
|
+
appendFileSync2(path4, `${JSON.stringify(receipt)}
|
|
8877
|
+
`, "utf8");
|
|
8878
|
+
try {
|
|
8879
|
+
chmodSync4(path4, 384);
|
|
8880
|
+
} catch {
|
|
8881
|
+
}
|
|
8882
|
+
} catch {
|
|
8883
|
+
}
|
|
8884
|
+
}
|
|
8885
|
+
function summarize(result) {
|
|
8886
|
+
if (result.ok) {
|
|
8887
|
+
return {
|
|
8888
|
+
ok: true,
|
|
8889
|
+
verdict: result.synthesized_verdict.verdict,
|
|
8890
|
+
confidence: result.synthesized_verdict.confidence,
|
|
8891
|
+
...result.receipt_id ? { receipt_id: result.receipt_id } : {}
|
|
8892
|
+
};
|
|
8893
|
+
}
|
|
8894
|
+
return { ok: false, reason: result.reason };
|
|
8895
|
+
}
|
|
8896
|
+
async function runMoat(shadow, request, timeoutMs, linkCallerSignal) {
|
|
8897
|
+
const callerSignal = linkCallerSignal ? request.signal : void 0;
|
|
8898
|
+
if (callerSignal?.aborted) return { ok: false, reason: CANCELLED_REASON };
|
|
8899
|
+
const controller = new AbortController();
|
|
8900
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
8901
|
+
const onCallerAbort = () => controller.abort();
|
|
8902
|
+
callerSignal?.addEventListener("abort", onCallerAbort, { once: true });
|
|
8903
|
+
try {
|
|
8904
|
+
const { signal: _ignored, ...rest } = request;
|
|
8905
|
+
void _ignored;
|
|
8906
|
+
return await shadow.run({ ...rest, signal: controller.signal });
|
|
8907
|
+
} catch (err) {
|
|
8908
|
+
return { ok: false, reason: `moat-shadow-threw: ${err instanceof Error ? err.message : String(err)}`.slice(0, 200) };
|
|
8909
|
+
} finally {
|
|
8910
|
+
clearTimeout(timer);
|
|
8911
|
+
callerSignal?.removeEventListener("abort", onCallerAbort);
|
|
8912
|
+
}
|
|
8913
|
+
}
|
|
8914
|
+
function createConsensusShadowClient(primary, shadow, options = {}) {
|
|
8915
|
+
const authoritative = options.authoritative === true;
|
|
8916
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_SHADOW_TIMEOUT_MS;
|
|
8917
|
+
const now = options.now ?? (() => Date.now());
|
|
8918
|
+
const random = options.random ?? Math.random;
|
|
8919
|
+
const samplePct = Math.min(100, Math.max(0, options.samplePct ?? 100));
|
|
8920
|
+
const onReceipt = options.onReceipt ?? ((r) => appendShadowReceipt(r));
|
|
8921
|
+
function receiptFor(request, local, moat, startedAt) {
|
|
8922
|
+
const l = summarize(local);
|
|
8923
|
+
const m = summarize(moat);
|
|
8924
|
+
return {
|
|
8925
|
+
ts: new Date(now()).toISOString(),
|
|
8926
|
+
gate_type: request.gate_type,
|
|
8927
|
+
mode: authoritative ? "authoritative" : "shadow",
|
|
8928
|
+
local: l,
|
|
8929
|
+
moat: m,
|
|
8930
|
+
agree: l.ok && m.ok ? l.verdict === m.verdict : null,
|
|
8931
|
+
duration_ms: Math.max(0, now() - startedAt)
|
|
8932
|
+
};
|
|
8933
|
+
}
|
|
8934
|
+
return {
|
|
8935
|
+
async run(request) {
|
|
8936
|
+
const startedAt = now();
|
|
8937
|
+
const primaryResult = await primary.run(request);
|
|
8938
|
+
if (request.signal?.aborted || !primaryResult.ok && primaryResult.reason === CANCELLED_REASON) {
|
|
8939
|
+
return primaryResult;
|
|
8940
|
+
}
|
|
8941
|
+
if (!primaryResult.ok || primaryResult.receipt_id || !hasLocalConsensusQuorum(primaryResult)) {
|
|
8942
|
+
return primaryResult;
|
|
8943
|
+
}
|
|
8944
|
+
if (authoritative) {
|
|
8945
|
+
const moatResult = await runMoat(shadow, request, timeoutMs, true);
|
|
8946
|
+
try {
|
|
8947
|
+
onReceipt(receiptFor(request, primaryResult, moatResult, startedAt));
|
|
8948
|
+
} catch {
|
|
8949
|
+
}
|
|
8950
|
+
return moatResult.ok ? moatResult : primaryResult;
|
|
8951
|
+
}
|
|
8952
|
+
if (samplePct < 100 && random() * 100 >= samplePct) return primaryResult;
|
|
8953
|
+
void runMoat(shadow, request, timeoutMs, false).then((moatResult) => {
|
|
8954
|
+
try {
|
|
8955
|
+
onReceipt(receiptFor(request, primaryResult, moatResult, startedAt));
|
|
8956
|
+
} catch {
|
|
8957
|
+
}
|
|
8958
|
+
});
|
|
8959
|
+
return primaryResult;
|
|
8960
|
+
}
|
|
8961
|
+
};
|
|
8962
|
+
}
|
|
8963
|
+
|
|
8964
|
+
// src/consensus/local-credential-env.ts
|
|
8965
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
8966
|
+
var require2 = createRequire2(import.meta.url);
|
|
8967
|
+
var KEY_SERVICE = "algosuite-vo";
|
|
8968
|
+
var MOAT_ENTITLEMENT_KEYCHAIN_ACCOUNT = "moat-api-key";
|
|
8969
|
+
var KEYCHAIN_TARGETS = [
|
|
8970
|
+
{ account: "anthropic-api-key", envVar: "ANTHROPIC_API_KEY" },
|
|
8971
|
+
{ account: "openai-api-key", envVar: "OPENAI_API_KEY" },
|
|
8972
|
+
{ account: "meta-api-key", envVar: "MODEL_API_KEY" },
|
|
8973
|
+
// The cloud-consensus entitlement (ADR-002 moat). Read here so an npm-installed
|
|
8974
|
+
// runner — where the local engine package never exists — still has a working
|
|
8975
|
+
// consensus path without an operator plumbing a secret into the daemon's env.
|
|
8976
|
+
{ account: MOAT_ENTITLEMENT_KEYCHAIN_ACCOUNT, envVar: "VO_ENTITLEMENT_TOKEN" }
|
|
8977
|
+
];
|
|
8978
|
+
function loadEntryCtor() {
|
|
8979
|
+
try {
|
|
8980
|
+
return require2("@napi-rs/keyring").Entry ?? null;
|
|
8981
|
+
} catch {
|
|
8982
|
+
return null;
|
|
8983
|
+
}
|
|
8984
|
+
}
|
|
8985
|
+
function readKey(EntryCtor, account) {
|
|
8986
|
+
try {
|
|
8987
|
+
return new EntryCtor(KEY_SERVICE, account).getPassword()?.trim() || null;
|
|
8988
|
+
} catch {
|
|
8989
|
+
return null;
|
|
8990
|
+
}
|
|
8991
|
+
}
|
|
8992
|
+
var SKIP_KEYCHAIN_ENV = "VO_MCP_SKIP_KEYCHAIN";
|
|
8993
|
+
function withLocalConsensusCredentials(baseEnv = process.env, options = {}) {
|
|
8994
|
+
const env = { ...baseEnv };
|
|
8995
|
+
if (!env.OPENAI_API_KEY?.trim() && env.CODEX_API_KEY?.trim()) {
|
|
8996
|
+
env.OPENAI_API_KEY = env.CODEX_API_KEY;
|
|
8997
|
+
}
|
|
8998
|
+
if (env[SKIP_KEYCHAIN_ENV]?.trim() === "1") return env;
|
|
8999
|
+
const EntryCtor = options.EntryCtor === void 0 ? loadEntryCtor() : options.EntryCtor;
|
|
9000
|
+
if (!EntryCtor) return env;
|
|
9001
|
+
for (const target of KEYCHAIN_TARGETS) {
|
|
9002
|
+
if (env[target.envVar]?.trim()) continue;
|
|
9003
|
+
const key = readKey(EntryCtor, target.account);
|
|
9004
|
+
if (key) env[target.envVar] = key;
|
|
9005
|
+
}
|
|
9006
|
+
return env;
|
|
9007
|
+
}
|
|
9008
|
+
|
|
6661
9009
|
// src/cloud/login.ts
|
|
6662
9010
|
init_credential_store();
|
|
6663
9011
|
import { createServer as createServer2 } from "node:http";
|
|
@@ -6683,16 +9031,16 @@ function processCapture(rawBody, expectedState, store) {
|
|
|
6683
9031
|
return { ok: false, httpStatus: 400, error: "login response missing refresh_token / api_key" };
|
|
6684
9032
|
}
|
|
6685
9033
|
const email = typeof data.email === "string" && data.email.trim() ? data.email.trim() : void 0;
|
|
6686
|
-
const
|
|
9034
|
+
const path4 = store({ refresh_token: refresh, api_key: apiKey, ...email ? { email } : {} });
|
|
6687
9035
|
return {
|
|
6688
9036
|
ok: true,
|
|
6689
9037
|
httpStatus: 200,
|
|
6690
|
-
result: { ...email ? { email } : {}, credentialPath:
|
|
9038
|
+
result: { ...email ? { email } : {}, credentialPath: path4 },
|
|
6691
9039
|
captured: { refresh_token: refresh, api_key: apiKey, ...email ? { email } : {} }
|
|
6692
9040
|
};
|
|
6693
9041
|
}
|
|
6694
9042
|
function captureHtml() {
|
|
6695
|
-
return `<!doctype html><html><head><meta charset="utf-8"><title>
|
|
9043
|
+
return `<!doctype html><html><head><meta charset="utf-8"><title>AlgoHQ login</title></head>
|
|
6696
9044
|
<body style="font-family:system-ui;max-width:32rem;margin:4rem auto;text-align:center">
|
|
6697
9045
|
<h2 id="m">Completing sign-in\u2026</h2>
|
|
6698
9046
|
<script>
|
|
@@ -6708,7 +9056,7 @@ function captureHtml() {
|
|
|
6708
9056
|
function defaultOpenBrowser(url) {
|
|
6709
9057
|
const platform = process.platform;
|
|
6710
9058
|
if (platform === "win32") {
|
|
6711
|
-
spawn2("
|
|
9059
|
+
spawn2("rundll32", ["url.dll,FileProtocolHandler", url], { detached: true, stdio: "ignore" }).unref();
|
|
6712
9060
|
} else if (platform === "darwin") {
|
|
6713
9061
|
spawn2("open", [url], { detached: true, stdio: "ignore" }).unref();
|
|
6714
9062
|
} else {
|
|
@@ -6723,7 +9071,7 @@ async function runLogin(opts = {}) {
|
|
|
6723
9071
|
const nowIso = opts.nowIso ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
6724
9072
|
const openBrowser = opts.openBrowser ?? defaultOpenBrowser;
|
|
6725
9073
|
const state = randomBytes(32).toString("base64url");
|
|
6726
|
-
return new Promise((
|
|
9074
|
+
return new Promise((resolve3, reject) => {
|
|
6727
9075
|
let settled = false;
|
|
6728
9076
|
const finish = (err, result) => {
|
|
6729
9077
|
if (settled) return;
|
|
@@ -6731,7 +9079,7 @@ async function runLogin(opts = {}) {
|
|
|
6731
9079
|
clearTimeout(timer);
|
|
6732
9080
|
server.close();
|
|
6733
9081
|
if (err) reject(err);
|
|
6734
|
-
else
|
|
9082
|
+
else resolve3(result);
|
|
6735
9083
|
};
|
|
6736
9084
|
const server = createServer2((req, res) => {
|
|
6737
9085
|
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
@@ -6775,11 +9123,11 @@ async function runLogin(opts = {}) {
|
|
|
6775
9123
|
}
|
|
6776
9124
|
} catch {
|
|
6777
9125
|
}
|
|
6778
|
-
const
|
|
6779
|
-
result = { ...capt.email ? { email: capt.email } : {}, credentialPath:
|
|
9126
|
+
const path4 = writeNow(cred);
|
|
9127
|
+
result = { ...capt.email ? { email: capt.email } : {}, credentialPath: path4 };
|
|
6780
9128
|
}
|
|
6781
9129
|
res.writeHead(outcome.httpStatus, { "content-type": "text/html; charset=utf-8" });
|
|
6782
|
-
res.end(outcome.ok ? "<h2>
|
|
9130
|
+
res.end(outcome.ok ? "<h2>AlgoHQ login complete \u2014 you can close this tab.</h2>" : `<h2>Login failed: ${outcome.error}</h2>`);
|
|
6783
9131
|
finish(outcome.ok ? null : new Error(outcome.error ?? "login failed"), result);
|
|
6784
9132
|
})();
|
|
6785
9133
|
});
|
|
@@ -6846,7 +9194,7 @@ init_common();
|
|
|
6846
9194
|
function defaultCacheDbPath() {
|
|
6847
9195
|
const env = process.env["VO_MCP_DB_PATH"];
|
|
6848
9196
|
if (env && env.length > 0) return env;
|
|
6849
|
-
return
|
|
9197
|
+
return join15(homedir9(), ".claude", "vo-mcp-cache.db");
|
|
6850
9198
|
}
|
|
6851
9199
|
async function probeEngineVersion() {
|
|
6852
9200
|
try {
|
|
@@ -6921,11 +9269,35 @@ async function main() {
|
|
|
6921
9269
|
const ratchets = createStubRatchetClient();
|
|
6922
9270
|
const testModule = process.env["VO_MCP_TEST_ENGINE_MODULE"];
|
|
6923
9271
|
const testClient = testModule !== void 0 && testModule.length > 0 ? await loadTestEngineClient(testModule) : null;
|
|
6924
|
-
const
|
|
6925
|
-
|
|
6926
|
-
|
|
9272
|
+
const localEnv = withLocalConsensusCredentials();
|
|
9273
|
+
const localProviders = probeProviders(localEnv);
|
|
9274
|
+
for (const key of ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GOOGLE_API_KEY", "DEEPSEEK_API_KEY", "MODEL_API_KEY"]) {
|
|
9275
|
+
if (!process.env[key] && localEnv[key]) process.env[key] = localEnv[key];
|
|
9276
|
+
}
|
|
9277
|
+
const localConsensus = tryCreateEngineConsensusClientFromEnv({ envSource: localEnv });
|
|
9278
|
+
const cloudConsensus = testClient ? null : tryCreateMoatConsensusClientFromEnv(localEnv);
|
|
9279
|
+
let consensus = testClient ?? localConsensus;
|
|
9280
|
+
if (!testClient && cloudConsensus && localProviders.length >= 2) {
|
|
9281
|
+
console.error(`[vo-mcp] local-first consensus active (${localProviders.join(", ")}); cloud moat is fallback-only`);
|
|
9282
|
+
consensus = createConsensusFallbackClient(localConsensus, cloudConsensus, {
|
|
9283
|
+
onFallback: (reason) => console.error(`[vo-mcp] local consensus unavailable (${reason}); using cloud moat fallback`)
|
|
9284
|
+
});
|
|
9285
|
+
const moatAuthoritative = process.env["VO_CONSENSUS_MOAT_AUTHORITATIVE"] === "1";
|
|
9286
|
+
if (moatAuthoritative || process.env["VO_CONSENSUS_MOAT_SHADOW"] === "1") {
|
|
9287
|
+
const pctRaw = Number(process.env["VO_CONSENSUS_MOAT_SHADOW_PCT"] ?? "100");
|
|
9288
|
+
const samplePct = Number.isFinite(pctRaw) ? Math.min(100, Math.max(0, pctRaw)) : 100;
|
|
9289
|
+
const shadowedLocal = createConsensusShadowClient(localConsensus, cloudConsensus, { authoritative: moatAuthoritative, samplePct });
|
|
9290
|
+
consensus = createConsensusFallbackClient(shadowedLocal, cloudConsensus, {
|
|
9291
|
+
onFallback: (reason) => console.error(`[vo-mcp] local consensus unavailable (${reason}); using cloud moat fallback`)
|
|
9292
|
+
});
|
|
9293
|
+
console.error(`[vo-mcp] moat ${moatAuthoritative ? "AUTHORITATIVE" : `SHADOW (${samplePct}%)`} active \u2014 local verdicts also reach the moat verify path (receipts: ${defaultShadowReceiptPath()})`);
|
|
9294
|
+
}
|
|
9295
|
+
} else if (!testClient && cloudConsensus) {
|
|
9296
|
+
console.error("[vo-mcp] fewer than 2 linked local providers; cloud moat consensus active");
|
|
9297
|
+
consensus = cloudConsensus;
|
|
9298
|
+
} else if (!testClient) {
|
|
9299
|
+
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");
|
|
6927
9300
|
}
|
|
6928
|
-
const consensus = testClient ?? cloudConsensus ?? tryCreateEngineConsensusClientFromEnv();
|
|
6929
9301
|
let adminCallables = null;
|
|
6930
9302
|
try {
|
|
6931
9303
|
adminCallables = buildAdminCallableClientFromEnv();
|
|
@@ -6961,7 +9333,7 @@ async function main() {
|
|
|
6961
9333
|
}
|
|
6962
9334
|
if (process.argv[2] === "login") {
|
|
6963
9335
|
const controlPlaneUrl = process.env["VO_CONTROL_PLANE_URL"]?.trim();
|
|
6964
|
-
const credentialLabel = `vo-mcp-cli@${
|
|
9336
|
+
const credentialLabel = `vo-mcp-cli@${hostname2()}`.slice(0, 200);
|
|
6965
9337
|
runLogin(
|
|
6966
9338
|
controlPlaneUrl ? {
|
|
6967
9339
|
exchange: (refreshToken, apiKey) => exchangeForVoCredential({ refreshToken, apiKey, controlPlaneUrl, label: credentialLabel })
|
|
@@ -6970,35 +9342,53 @@ if (process.argv[2] === "login") {
|
|
|
6970
9342
|
console.error(`[vo-mcp] login successful${r.email ? ` as ${r.email}` : ""}. Credential stored at ${r.credentialPath}.`);
|
|
6971
9343
|
console.error("[vo-mcp] You can now remove VO_CONTROL_PLANE_ADMIN_TOKEN (the god-token) from your MCP config.");
|
|
6972
9344
|
console.error("[vo-mcp] NOTE: per-user auth requires VO_OPERATOR_ALLOWED_EMAILS (with your email) on the deployed control-plane.");
|
|
6973
|
-
process.exit(0);
|
|
6974
9345
|
}).catch((err) => {
|
|
6975
9346
|
console.error("[vo-mcp] login failed:", err instanceof Error ? err.message : String(err));
|
|
6976
|
-
process.
|
|
9347
|
+
process.exitCode = 1;
|
|
6977
9348
|
});
|
|
6978
9349
|
} else if (process.argv[2] === "sync") {
|
|
6979
9350
|
const action = process.argv[3];
|
|
6980
9351
|
if (action !== "push" && action !== "pull") {
|
|
6981
|
-
console.error("[vo-mcp] usage: vo-mcp sync <push|pull> [--cwd <path>]");
|
|
9352
|
+
console.error("[vo-mcp] usage: vo-mcp sync <push|pull> [--cwd <path>] [--lock-wait-ms <n>]");
|
|
6982
9353
|
process.exit(2);
|
|
6983
9354
|
}
|
|
6984
9355
|
const cwdFlag = process.argv.indexOf("--cwd");
|
|
6985
9356
|
const cwd = cwdFlag >= 0 && typeof process.argv[cwdFlag + 1] === "string" ? process.argv[cwdFlag + 1] : process.cwd();
|
|
6986
|
-
const
|
|
9357
|
+
const waitFlag = process.argv.indexOf("--lock-wait-ms");
|
|
9358
|
+
const parsedWait = waitFlag >= 0 ? Number(process.argv[waitFlag + 1]) : Number.NaN;
|
|
9359
|
+
const lockOptions = Number.isFinite(parsedWait) && parsedWait >= 0 ? { waitMs: parsedWait } : {};
|
|
9360
|
+
const sessionId = randomUUID6();
|
|
9361
|
+
const appendSyncLog = async (line) => {
|
|
9362
|
+
try {
|
|
9363
|
+
const { appendFileSync: appendFileSync3, mkdirSync: mkdirSync9 } = await import("node:fs");
|
|
9364
|
+
const { join: join16 } = await import("node:path");
|
|
9365
|
+
const { homedir: homedir10 } = await import("node:os");
|
|
9366
|
+
const dir = join16(homedir10(), ".claude");
|
|
9367
|
+
mkdirSync9(dir, { recursive: true });
|
|
9368
|
+
appendFileSync3(join16(dir, "vo-mcp-sync.log"), `${line}
|
|
9369
|
+
`, "utf8");
|
|
9370
|
+
} catch {
|
|
9371
|
+
}
|
|
9372
|
+
};
|
|
6987
9373
|
Promise.resolve().then(() => (init_sync_config(), sync_config_exports)).then(async ({ runMemorySync: runMemorySync2, isNoopSyncReason: isNoopSyncReason2 }) => {
|
|
6988
|
-
const r = await runMemorySync2(action, cwd, sessionId);
|
|
9374
|
+
const r = await runMemorySync2(action, cwd, sessionId, void 0, lockOptions);
|
|
9375
|
+
const stamp = `${sessionId} ${action}`;
|
|
6989
9376
|
if (r.synced) {
|
|
6990
9377
|
console.error(`[vo-mcp] sync ${action} ok: ${JSON.stringify(r)}`);
|
|
6991
|
-
|
|
6992
|
-
}
|
|
6993
|
-
if (isNoopSyncReason2(r.reason)) {
|
|
9378
|
+
await appendSyncLog(`ok ${stamp} ${JSON.stringify(r)}`);
|
|
9379
|
+
} else if (isNoopSyncReason2(r.reason)) {
|
|
6994
9380
|
console.error(`[vo-mcp] sync ${action} skipped: ${r.reason}`);
|
|
6995
|
-
|
|
9381
|
+
await appendSyncLog(`skip ${stamp} ${r.reason ?? ""}`);
|
|
9382
|
+
} else {
|
|
9383
|
+
console.error(`[vo-mcp] sync ${action} failed: ${r.reason}`);
|
|
9384
|
+
await appendSyncLog(`FAIL ${stamp} ${r.reason ?? ""}`);
|
|
9385
|
+
process.exitCode = 1;
|
|
6996
9386
|
}
|
|
6997
|
-
|
|
6998
|
-
|
|
6999
|
-
|
|
7000
|
-
|
|
7001
|
-
process.
|
|
9387
|
+
}).catch(async (err) => {
|
|
9388
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
9389
|
+
console.error("[vo-mcp] sync fatal:", message);
|
|
9390
|
+
await appendSyncLog(`FATAL ${sessionId} ${action} ${message}`);
|
|
9391
|
+
process.exitCode = 1;
|
|
7002
9392
|
});
|
|
7003
9393
|
} else {
|
|
7004
9394
|
main().catch((err) => {
|