@algosuite/vo-mcp 0.2.0-beta.50 → 0.2.0-beta.52
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +432 -175
- package/dist/cli.js.map +4 -4
- package/dist/index.js +406 -149
- package/dist/index.js.map +4 -4
- package/dist/runner-cli.js +112 -6
- package/dist/runner-cli.js.map +4 -4
- package/dist/runner-supervisor.js +45 -3
- package/dist/runner-supervisor.js.map +3 -3
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -502,7 +502,7 @@ __export(memory_knowledge_bridge_exports, {
|
|
|
502
502
|
extractMemoryTitle: () => extractMemoryTitle,
|
|
503
503
|
upsertMemoryFilesAsKnowledge: () => upsertMemoryFilesAsKnowledge
|
|
504
504
|
});
|
|
505
|
-
import { existsSync as
|
|
505
|
+
import { existsSync as existsSync8, readdirSync as readdirSync5, readFileSync as readFileSync13 } from "node:fs";
|
|
506
506
|
function extractMemoryTitle(fileName, content) {
|
|
507
507
|
const frontmatter = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
508
508
|
if (frontmatter) {
|
|
@@ -517,7 +517,7 @@ async function upsertMemoryFilesAsKnowledge(options) {
|
|
|
517
517
|
const { controlPlaneUrl, token, memoryDir, fetchFn, cache, deadline } = options;
|
|
518
518
|
let files;
|
|
519
519
|
try {
|
|
520
|
-
if (!
|
|
520
|
+
if (!existsSync8(memoryDir)) {
|
|
521
521
|
return { attempted: 0, upserted: 0, failed: 0, skipped: 0, failures: [] };
|
|
522
522
|
}
|
|
523
523
|
files = readdirSync5(memoryDir).filter(
|
|
@@ -807,24 +807,24 @@ function defaultOverridePath() {
|
|
|
807
807
|
return join2(homedir(), ".claude", "vo-arch-defaults.local.json");
|
|
808
808
|
}
|
|
809
809
|
function loadTenantOverride(opts = {}) {
|
|
810
|
-
const
|
|
811
|
-
if (!existsSync2(
|
|
810
|
+
const path4 = opts.path ?? defaultOverridePath();
|
|
811
|
+
if (!existsSync2(path4)) {
|
|
812
812
|
return { override: null, source_path: null };
|
|
813
813
|
}
|
|
814
|
-
const raw = readFileSync2(
|
|
814
|
+
const raw = readFileSync2(path4, "utf8");
|
|
815
815
|
let parsed;
|
|
816
816
|
try {
|
|
817
817
|
parsed = JSON.parse(raw);
|
|
818
818
|
} catch (err) {
|
|
819
819
|
const m = err instanceof Error ? err.message : String(err);
|
|
820
|
-
throw new Error(`vo-arch-defaults: invalid JSON in override ${
|
|
820
|
+
throw new Error(`vo-arch-defaults: invalid JSON in override ${path4}: ${m}`, { cause: err });
|
|
821
821
|
}
|
|
822
822
|
try {
|
|
823
823
|
const override = parseOverride(parsed);
|
|
824
|
-
return { override, source_path:
|
|
824
|
+
return { override, source_path: path4 };
|
|
825
825
|
} catch (err) {
|
|
826
826
|
const m = err instanceof Error ? err.message : String(err);
|
|
827
|
-
throw new Error(`vo-arch-defaults: override schema validation failed for ${
|
|
827
|
+
throw new Error(`vo-arch-defaults: override schema validation failed for ${path4}: ${m}`, { cause: err });
|
|
828
828
|
}
|
|
829
829
|
}
|
|
830
830
|
|
|
@@ -954,9 +954,9 @@ function globToRegExp(glob) {
|
|
|
954
954
|
}
|
|
955
955
|
return new RegExp("^" + out + "$");
|
|
956
956
|
}
|
|
957
|
-
function matchesAnyGlob(
|
|
957
|
+
function matchesAnyGlob(path4, globs) {
|
|
958
958
|
for (const g of globs) {
|
|
959
|
-
if (globToRegExp(g).test(
|
|
959
|
+
if (globToRegExp(g).test(path4)) return true;
|
|
960
960
|
}
|
|
961
961
|
return false;
|
|
962
962
|
}
|
|
@@ -4142,10 +4142,10 @@ Produce the JSON dispatch plan now.`;
|
|
|
4142
4142
|
init_auth_token_source();
|
|
4143
4143
|
init_credential_store();
|
|
4144
4144
|
var AdminCallableError = class extends Error {
|
|
4145
|
-
constructor(status,
|
|
4145
|
+
constructor(status, path4, message) {
|
|
4146
4146
|
super(message);
|
|
4147
4147
|
this.status = status;
|
|
4148
|
-
this.path =
|
|
4148
|
+
this.path = path4;
|
|
4149
4149
|
this.name = "AdminCallableError";
|
|
4150
4150
|
}
|
|
4151
4151
|
status;
|
|
@@ -5140,7 +5140,7 @@ async function handleReportSessionState(deps, rawInput, _signal) {
|
|
|
5140
5140
|
import { spawn } from "node:child_process";
|
|
5141
5141
|
import { homedir as homedir5 } from "node:os";
|
|
5142
5142
|
import { join as join7 } from "node:path";
|
|
5143
|
-
import { existsSync as
|
|
5143
|
+
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";
|
|
5144
5144
|
|
|
5145
5145
|
// src/swarm/tier-binding.ts
|
|
5146
5146
|
var SWARM_TIERS = Object.freeze([
|
|
@@ -5346,10 +5346,10 @@ function sanitizeSwarmId(raw) {
|
|
|
5346
5346
|
return id;
|
|
5347
5347
|
}
|
|
5348
5348
|
var CEILING_FILE = "ceiling.json";
|
|
5349
|
-
function createExclusive(
|
|
5349
|
+
function createExclusive(path4, contents) {
|
|
5350
5350
|
let fd;
|
|
5351
5351
|
try {
|
|
5352
|
-
fd = openSync(
|
|
5352
|
+
fd = openSync(path4, "wx");
|
|
5353
5353
|
} catch {
|
|
5354
5354
|
return false;
|
|
5355
5355
|
}
|
|
@@ -5364,18 +5364,18 @@ function capToCents(cap) {
|
|
|
5364
5364
|
return isPositiveCap(cap) ? Math.round(cap * 100) : 0;
|
|
5365
5365
|
}
|
|
5366
5366
|
function readOrRecordLedgerHead(swarmDir, proposedCeiling, proposedCapCents, nowIso) {
|
|
5367
|
-
const
|
|
5367
|
+
const path4 = join6(swarmDir, CEILING_FILE);
|
|
5368
5368
|
const head = JSON.stringify({
|
|
5369
5369
|
ceiling: proposedCeiling,
|
|
5370
5370
|
cap_cents: proposedCapCents,
|
|
5371
5371
|
recorded_at: nowIso
|
|
5372
5372
|
});
|
|
5373
|
-
if (createExclusive(
|
|
5373
|
+
if (createExclusive(path4, head)) {
|
|
5374
5374
|
return { ceiling: proposedCeiling, capCents: proposedCapCents };
|
|
5375
5375
|
}
|
|
5376
5376
|
let parsed;
|
|
5377
5377
|
try {
|
|
5378
|
-
parsed = JSON.parse(readFileSync6(
|
|
5378
|
+
parsed = JSON.parse(readFileSync6(path4, "utf8"));
|
|
5379
5379
|
} catch {
|
|
5380
5380
|
return null;
|
|
5381
5381
|
}
|
|
@@ -5455,6 +5455,288 @@ var claimSpawnSlot = ({ swarmId, proposedCeiling, proposedCapUsd, dir, nowIso })
|
|
|
5455
5455
|
};
|
|
5456
5456
|
};
|
|
5457
5457
|
|
|
5458
|
+
// src/swarm/spawn-plan.ts
|
|
5459
|
+
function resolveSpawnPlan(env, input, nowIso, platform = process.platform, claim = claimSpawnSlot) {
|
|
5460
|
+
const rawBinding = env[SWARM_TIER_BINDING_ENV];
|
|
5461
|
+
const hasBinding = typeof rawBinding === "string" && rawBinding.trim().length > 0;
|
|
5462
|
+
if (!hasBinding) {
|
|
5463
|
+
const explicit = input.agent?.trim();
|
|
5464
|
+
const resolved2 = resolveSuccessorLaunch({ agent: explicit || "claude", maxTurns: input.max_turns, platform });
|
|
5465
|
+
if (!resolved2.ok) return { ok: false, reason: resolved2.reason, tier: "unbound" };
|
|
5466
|
+
return {
|
|
5467
|
+
ok: true,
|
|
5468
|
+
bin: resolved2.bin,
|
|
5469
|
+
args: resolved2.args,
|
|
5470
|
+
agent: resolved2.agent,
|
|
5471
|
+
tier: "unbound",
|
|
5472
|
+
bound: false,
|
|
5473
|
+
env: {},
|
|
5474
|
+
slot: null,
|
|
5475
|
+
capUsd: null,
|
|
5476
|
+
capRemainingUsd: null
|
|
5477
|
+
};
|
|
5478
|
+
}
|
|
5479
|
+
const binding = inheritSwarmTierBinding(env, nowIso);
|
|
5480
|
+
const admission = admitSubagentSpawn(binding);
|
|
5481
|
+
if (!admission.allowed) {
|
|
5482
|
+
return { ok: false, reason: admission.reason, tier: binding.tier };
|
|
5483
|
+
}
|
|
5484
|
+
const agentRefusal = agentBindingRefusal(binding, input.agent);
|
|
5485
|
+
if (agentRefusal !== null) return { ok: false, reason: agentRefusal, tier: binding.tier };
|
|
5486
|
+
const resolved = resolveSuccessorLaunch({
|
|
5487
|
+
agent: binding.agent,
|
|
5488
|
+
maxTurns: input.max_turns,
|
|
5489
|
+
platform
|
|
5490
|
+
});
|
|
5491
|
+
if (!resolved.ok) return { ok: false, reason: resolved.reason, tier: binding.tier };
|
|
5492
|
+
const slot = claim({
|
|
5493
|
+
swarmId: binding.swarm_id,
|
|
5494
|
+
proposedCeiling: binding.subagent_budget,
|
|
5495
|
+
// The spend-cap POOL, recorded once per swarm exactly like the ceiling. The
|
|
5496
|
+
// child's cap is DEBITED from it below, not recomputed from this binding.
|
|
5497
|
+
proposedCapUsd: binding.spend_cap_usd,
|
|
5498
|
+
dir: resolveLedgerDir(env),
|
|
5499
|
+
nowIso
|
|
5500
|
+
});
|
|
5501
|
+
if (!slot.ok) return { ok: false, reason: slot.reason, tier: binding.tier };
|
|
5502
|
+
return {
|
|
5503
|
+
ok: true,
|
|
5504
|
+
bin: resolved.bin,
|
|
5505
|
+
args: resolved.args,
|
|
5506
|
+
agent: resolved.agent,
|
|
5507
|
+
tier: binding.tier,
|
|
5508
|
+
bound: true,
|
|
5509
|
+
// Re-export the same TIER with a DECREMENTED budget and the spend cap the
|
|
5510
|
+
// ledger just DEBITED. Exporting the binding verbatim (what this did before
|
|
5511
|
+
// #9312) meant the child re-read the full budget and every generation
|
|
5512
|
+
// restarted at zero. Recomputing the cap from THIS binding (what #9312 did)
|
|
5513
|
+
// bounded a chain but not a tree: three siblings each re-halved the parent's
|
|
5514
|
+
// untouched $50 and walked away with $75 between them.
|
|
5515
|
+
env: childBindingEnvFragment(binding, slot.capUsd),
|
|
5516
|
+
slot: slot.slot,
|
|
5517
|
+
capUsd: slot.capUsd,
|
|
5518
|
+
capRemainingUsd: slot.capRemainingUsd
|
|
5519
|
+
};
|
|
5520
|
+
}
|
|
5521
|
+
|
|
5522
|
+
// ../../scripts/virtual-office/code-runner/windows-claude-launch.mjs
|
|
5523
|
+
import { existsSync as existsSync4, realpathSync } from "node:fs";
|
|
5524
|
+
import { win32 as path3 } from "node:path";
|
|
5525
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
5526
|
+
var NATIVE_CLAUDE_PARTS = [
|
|
5527
|
+
"node_modules",
|
|
5528
|
+
"@anthropic-ai",
|
|
5529
|
+
"claude-code",
|
|
5530
|
+
"bin",
|
|
5531
|
+
"claude.exe"
|
|
5532
|
+
];
|
|
5533
|
+
function pathValue(env) {
|
|
5534
|
+
for (const key of ["Path", "PATH", "path"]) {
|
|
5535
|
+
if (typeof env?.[key] === "string") return env[key];
|
|
5536
|
+
}
|
|
5537
|
+
return "";
|
|
5538
|
+
}
|
|
5539
|
+
function cleanPathSegment(value) {
|
|
5540
|
+
const trimmed = String(value || "").trim();
|
|
5541
|
+
return trimmed.startsWith('"') && trimmed.endsWith('"') ? trimmed.slice(1, -1) : trimmed;
|
|
5542
|
+
}
|
|
5543
|
+
function envValue(env, name) {
|
|
5544
|
+
const exact = env?.[name];
|
|
5545
|
+
if (typeof exact === "string") return exact.trim();
|
|
5546
|
+
const key = Object.keys(env || {}).find((candidate) => candidate.toLowerCase() === name.toLowerCase());
|
|
5547
|
+
return typeof env?.[key] === "string" ? env[key].trim() : "";
|
|
5548
|
+
}
|
|
5549
|
+
function userClaudeCandidates(bin, env) {
|
|
5550
|
+
if (!/^claude(?:\.(?:exe|cmd|ps1))?$/iu.test(bin)) return [];
|
|
5551
|
+
const userProfile = envValue(env, "USERPROFILE");
|
|
5552
|
+
const appData = envValue(env, "APPDATA") || (userProfile ? path3.join(userProfile, "AppData", "Roaming") : "");
|
|
5553
|
+
const localAppData = envValue(env, "LOCALAPPDATA") || (userProfile ? path3.join(userProfile, "AppData", "Local") : "");
|
|
5554
|
+
const candidates = [];
|
|
5555
|
+
if (appData) {
|
|
5556
|
+
const npmBin = path3.join(appData, "npm");
|
|
5557
|
+
candidates.push(
|
|
5558
|
+
path3.join(npmBin, "claude.exe"),
|
|
5559
|
+
path3.join(npmBin, "claude.cmd"),
|
|
5560
|
+
path3.join(npmBin, "claude.ps1"),
|
|
5561
|
+
path3.join(npmBin, "claude"),
|
|
5562
|
+
path3.join(npmBin, ...NATIVE_CLAUDE_PARTS)
|
|
5563
|
+
);
|
|
5564
|
+
}
|
|
5565
|
+
if (userProfile) candidates.push(path3.join(userProfile, ".local", "bin", "claude.exe"));
|
|
5566
|
+
if (localAppData) {
|
|
5567
|
+
candidates.push(
|
|
5568
|
+
path3.join(localAppData, "Microsoft", "WinGet", "Links", "claude.exe"),
|
|
5569
|
+
path3.join(localAppData, "Microsoft", "WindowsApps", "claude.exe")
|
|
5570
|
+
);
|
|
5571
|
+
}
|
|
5572
|
+
return candidates;
|
|
5573
|
+
}
|
|
5574
|
+
function pathCandidates(bin, env) {
|
|
5575
|
+
if (path3.isAbsolute(bin) || /[\\/]/u.test(bin)) {
|
|
5576
|
+
return [path3.resolve(bin)];
|
|
5577
|
+
}
|
|
5578
|
+
const extension = path3.extname(bin);
|
|
5579
|
+
const fromPath = pathValue(env).split(";").map(cleanPathSegment).filter(Boolean).flatMap((directory) => extension ? [path3.join(directory, bin)] : [
|
|
5580
|
+
path3.join(directory, `${bin}.exe`),
|
|
5581
|
+
path3.join(directory, `${bin}.cmd`),
|
|
5582
|
+
path3.join(directory, `${bin}.ps1`),
|
|
5583
|
+
path3.join(directory, bin)
|
|
5584
|
+
]);
|
|
5585
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5586
|
+
return [...fromPath, ...userClaudeCandidates(bin, env)].filter((candidate) => {
|
|
5587
|
+
const key = candidate.toLowerCase();
|
|
5588
|
+
if (seen.has(key)) return false;
|
|
5589
|
+
seen.add(key);
|
|
5590
|
+
return true;
|
|
5591
|
+
});
|
|
5592
|
+
}
|
|
5593
|
+
function canonicalExistingPath(candidate, exists, canonicalize2) {
|
|
5594
|
+
if (!exists(candidate)) return null;
|
|
5595
|
+
try {
|
|
5596
|
+
return canonicalize2(candidate);
|
|
5597
|
+
} catch {
|
|
5598
|
+
return null;
|
|
5599
|
+
}
|
|
5600
|
+
}
|
|
5601
|
+
function resolveWindowsClaudeExecutable({
|
|
5602
|
+
bin = "claude",
|
|
5603
|
+
env = process.env,
|
|
5604
|
+
exists = existsSync4,
|
|
5605
|
+
canonicalize: canonicalize2 = realpathSync
|
|
5606
|
+
} = {}) {
|
|
5607
|
+
const requested = String(bin || "").trim();
|
|
5608
|
+
if (!requested || requested.includes("\0")) {
|
|
5609
|
+
throw new TypeError("Claude executable must be a non-empty path without NUL bytes");
|
|
5610
|
+
}
|
|
5611
|
+
for (const candidate of pathCandidates(requested, env)) {
|
|
5612
|
+
const found = canonicalExistingPath(candidate, exists, canonicalize2);
|
|
5613
|
+
if (!found) continue;
|
|
5614
|
+
if (path3.extname(found).toLowerCase() === ".exe") return found;
|
|
5615
|
+
const native = path3.join(path3.dirname(found), ...NATIVE_CLAUDE_PARTS);
|
|
5616
|
+
const resolvedNative = canonicalExistingPath(native, exists, canonicalize2);
|
|
5617
|
+
if (resolvedNative) return resolvedNative;
|
|
5618
|
+
}
|
|
5619
|
+
const error = new Error(
|
|
5620
|
+
`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.`
|
|
5621
|
+
);
|
|
5622
|
+
error.code = "ENOENT";
|
|
5623
|
+
throw error;
|
|
5624
|
+
}
|
|
5625
|
+
|
|
5626
|
+
// src/swarm/successor-windows-exe.ts
|
|
5627
|
+
var resolveWindowsClaudeExe = (bin, env) => resolveWindowsClaudeExecutable({ bin, env });
|
|
5628
|
+
function resolveNativeWindowsExecutable(bin, env = process.env, resolve3 = resolveWindowsClaudeExe) {
|
|
5629
|
+
try {
|
|
5630
|
+
return { ok: true, bin: resolve3(bin, env) };
|
|
5631
|
+
} catch (error) {
|
|
5632
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5633
|
+
return {
|
|
5634
|
+
ok: false,
|
|
5635
|
+
reason: `could not resolve a native Windows executable for '${bin}': ${message}`
|
|
5636
|
+
};
|
|
5637
|
+
}
|
|
5638
|
+
}
|
|
5639
|
+
|
|
5640
|
+
// src/swarm/successor-liveness.ts
|
|
5641
|
+
import { statSync as statSync3 } from "node:fs";
|
|
5642
|
+
var DEFAULT_EARLY_EXIT_SEC = 10;
|
|
5643
|
+
var DEFAULT_NO_OUTPUT_SEC = 0;
|
|
5644
|
+
var DEFAULT_POLL_MS = 200;
|
|
5645
|
+
var KILL_ESCALATION_MS = 2e3;
|
|
5646
|
+
function positiveSeconds(raw, fallback) {
|
|
5647
|
+
if (raw === void 0) return fallback;
|
|
5648
|
+
const n = Number(raw.trim());
|
|
5649
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
5650
|
+
}
|
|
5651
|
+
function resolveLivenessConfigFromEnv(env = process.env, explicitOverride = {}) {
|
|
5652
|
+
const earlyExitMs = explicitOverride.earlyExitMs ?? positiveSeconds(env["VO_MCP_SUCCESSOR_EXIT_CHECK_SEC"], DEFAULT_EARLY_EXIT_SEC) * 1e3;
|
|
5653
|
+
const noOutputMs = explicitOverride.noOutputMs ?? positiveSeconds(env["VO_MCP_SUCCESSOR_OUTPUT_CHECK_SEC"], DEFAULT_NO_OUTPUT_SEC) * 1e3;
|
|
5654
|
+
return { earlyExitMs, noOutputMs };
|
|
5655
|
+
}
|
|
5656
|
+
function defaultStatLogBytes(path4) {
|
|
5657
|
+
try {
|
|
5658
|
+
return statSync3(path4).size;
|
|
5659
|
+
} catch {
|
|
5660
|
+
return 0;
|
|
5661
|
+
}
|
|
5662
|
+
}
|
|
5663
|
+
function defaultKillChild(child) {
|
|
5664
|
+
try {
|
|
5665
|
+
child.kill("SIGTERM");
|
|
5666
|
+
} catch {
|
|
5667
|
+
}
|
|
5668
|
+
const escalation = setTimeout(() => {
|
|
5669
|
+
try {
|
|
5670
|
+
child.kill("SIGKILL");
|
|
5671
|
+
} catch {
|
|
5672
|
+
}
|
|
5673
|
+
}, KILL_ESCALATION_MS);
|
|
5674
|
+
escalation.unref();
|
|
5675
|
+
}
|
|
5676
|
+
function checkSuccessorLiveness(child, logPath, config, deps = {}) {
|
|
5677
|
+
const statLogBytes = deps.statLogBytes ?? defaultStatLogBytes;
|
|
5678
|
+
const now = deps.now ?? Date.now;
|
|
5679
|
+
const pollIntervalMs = deps.pollIntervalMs ?? DEFAULT_POLL_MS;
|
|
5680
|
+
const killChild = deps.killChild ?? defaultKillChild;
|
|
5681
|
+
const outputGateEnabled = Number.isFinite(config.noOutputMs) && config.noOutputMs > 0;
|
|
5682
|
+
const startedAt = now();
|
|
5683
|
+
return new Promise((resolve3) => {
|
|
5684
|
+
let settled = false;
|
|
5685
|
+
let pollTimer = null;
|
|
5686
|
+
const onExit = (code, signal) => {
|
|
5687
|
+
const elapsedMs = now() - startedAt;
|
|
5688
|
+
finish({
|
|
5689
|
+
ok: false,
|
|
5690
|
+
reason: `child_exited_early: exit code ${code ?? "null"} signal ${signal ?? "none"} after ${elapsedMs}ms`,
|
|
5691
|
+
detail: { exitCode: code, signal, elapsedMs }
|
|
5692
|
+
});
|
|
5693
|
+
};
|
|
5694
|
+
const cleanup = () => {
|
|
5695
|
+
if (pollTimer !== null) clearInterval(pollTimer);
|
|
5696
|
+
child.off?.("exit", onExit);
|
|
5697
|
+
};
|
|
5698
|
+
const finish = (result) => {
|
|
5699
|
+
if (settled) return;
|
|
5700
|
+
settled = true;
|
|
5701
|
+
cleanup();
|
|
5702
|
+
if (!result.ok) {
|
|
5703
|
+
try {
|
|
5704
|
+
killChild(child);
|
|
5705
|
+
} catch {
|
|
5706
|
+
}
|
|
5707
|
+
}
|
|
5708
|
+
resolve3(result);
|
|
5709
|
+
};
|
|
5710
|
+
if (typeof child.exitCode === "number" || typeof child.signalCode === "string" && child.signalCode.length > 0) {
|
|
5711
|
+
finish({
|
|
5712
|
+
ok: false,
|
|
5713
|
+
reason: `child_exited_early: exit code ${child.exitCode ?? "null"} signal ${child.signalCode ?? "none"} before the liveness watch attached`,
|
|
5714
|
+
detail: { exitCode: child.exitCode ?? null, signal: child.signalCode ?? null, elapsedMs: 0 }
|
|
5715
|
+
});
|
|
5716
|
+
return;
|
|
5717
|
+
}
|
|
5718
|
+
child.on("exit", onExit);
|
|
5719
|
+
const tick = () => {
|
|
5720
|
+
if (settled) return;
|
|
5721
|
+
const elapsedMs = now() - startedAt;
|
|
5722
|
+
const outputSeen = outputGateEnabled ? statLogBytes(logPath) > 0 : true;
|
|
5723
|
+
if (outputSeen && elapsedMs >= config.earlyExitMs) {
|
|
5724
|
+
finish({ ok: true });
|
|
5725
|
+
return;
|
|
5726
|
+
}
|
|
5727
|
+
if (outputGateEnabled && !outputSeen && elapsedMs >= config.noOutputMs) {
|
|
5728
|
+
finish({
|
|
5729
|
+
ok: false,
|
|
5730
|
+
reason: `no_output: log stayed empty for ${elapsedMs}ms (limit ${config.noOutputMs}ms)`,
|
|
5731
|
+
detail: { elapsedMs, logPath }
|
|
5732
|
+
});
|
|
5733
|
+
}
|
|
5734
|
+
};
|
|
5735
|
+
pollTimer = setInterval(tick, pollIntervalMs);
|
|
5736
|
+
tick();
|
|
5737
|
+
});
|
|
5738
|
+
}
|
|
5739
|
+
|
|
5458
5740
|
// src/tools/session/spawn-successor.ts
|
|
5459
5741
|
var TOOL_NAME20 = "vo_spawn_successor";
|
|
5460
5742
|
var MAX_HANDOFF_BYTES = 64e3;
|
|
@@ -5486,7 +5768,7 @@ var inputSchema20 = {
|
|
|
5486
5768
|
additionalProperties: false
|
|
5487
5769
|
};
|
|
5488
5770
|
var RETIRED_COUNTER_INPUT = "spawns_so_far";
|
|
5489
|
-
var description20 = "Mode B auto-handoff (roadmap \xA73.4): spawn a DETACHED headless `claude -p` successor with a handoff doc pre-injected into its prompt. Defaults to the newest handoff in ~/.vo/handoffs/. Returns {spawned, pid, log_path, handoff_path}. The successor works under the same gates as any session (ADR-001: verify-before-act, human merge approval) \u2014 this tool never fires autonomously.";
|
|
5771
|
+
var description20 = "Mode B auto-handoff (roadmap \xA73.4): spawn a DETACHED headless `claude -p` successor with a handoff doc pre-injected into its prompt. Defaults to the newest handoff in ~/.vo/handoffs/. 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.";
|
|
5490
5772
|
function isToolInput20(v) {
|
|
5491
5773
|
if (typeof v !== "object" || v === null) return false;
|
|
5492
5774
|
const o = v;
|
|
@@ -5504,7 +5786,7 @@ function retiredCounterRefusal(v) {
|
|
|
5504
5786
|
}
|
|
5505
5787
|
function newestHandoff(dir = join7(homedir5(), ".vo", "handoffs")) {
|
|
5506
5788
|
try {
|
|
5507
|
-
const entries = readdirSync3(dir).filter((f) => f.endsWith(".md")).map((f) => ({ f, m:
|
|
5789
|
+
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);
|
|
5508
5790
|
return entries.length > 0 && entries[0] ? join7(dir, entries[0].f) : null;
|
|
5509
5791
|
} catch {
|
|
5510
5792
|
return null;
|
|
@@ -5542,97 +5824,14 @@ function buildSuccessorPrompt(handoffMarkdown, goal) {
|
|
|
5542
5824
|
if (goal && goal.trim().length > 0) lines.push("", `OPERATOR GOAL OVERRIDE: ${goal.trim()}`);
|
|
5543
5825
|
return lines.join("\n");
|
|
5544
5826
|
}
|
|
5545
|
-
function
|
|
5546
|
-
const args = ["-p", "--permission-mode", "acceptEdits"];
|
|
5547
|
-
if (Number.isInteger(maxTurns) && maxTurns > 0) {
|
|
5548
|
-
args.push("--max-turns", String(maxTurns));
|
|
5549
|
-
}
|
|
5550
|
-
return args;
|
|
5551
|
-
}
|
|
5552
|
-
function resolveSpawnPlan(env, input, nowIso, platform = process.platform, claim = claimSpawnSlot) {
|
|
5553
|
-
const rawBinding = env[SWARM_TIER_BINDING_ENV];
|
|
5554
|
-
const hasBinding = typeof rawBinding === "string" && rawBinding.trim().length > 0;
|
|
5555
|
-
if (!hasBinding) {
|
|
5556
|
-
const explicit = input.agent?.trim();
|
|
5557
|
-
if (explicit) {
|
|
5558
|
-
const resolved2 = resolveSuccessorLaunch({ agent: explicit, maxTurns: input.max_turns, platform });
|
|
5559
|
-
if (!resolved2.ok) return { ok: false, reason: resolved2.reason, tier: "unbound" };
|
|
5560
|
-
return {
|
|
5561
|
-
ok: true,
|
|
5562
|
-
bin: resolved2.bin,
|
|
5563
|
-
args: resolved2.args,
|
|
5564
|
-
agent: resolved2.agent,
|
|
5565
|
-
tier: "unbound",
|
|
5566
|
-
bound: false,
|
|
5567
|
-
env: {},
|
|
5568
|
-
slot: null,
|
|
5569
|
-
capUsd: null,
|
|
5570
|
-
capRemainingUsd: null
|
|
5571
|
-
};
|
|
5572
|
-
}
|
|
5573
|
-
return {
|
|
5574
|
-
ok: true,
|
|
5575
|
-
bin: "claude",
|
|
5576
|
-
args: buildSuccessorArgs(input.max_turns),
|
|
5577
|
-
agent: "claude",
|
|
5578
|
-
tier: "unbound",
|
|
5579
|
-
bound: false,
|
|
5580
|
-
env: {},
|
|
5581
|
-
slot: null,
|
|
5582
|
-
capUsd: null,
|
|
5583
|
-
capRemainingUsd: null
|
|
5584
|
-
};
|
|
5585
|
-
}
|
|
5586
|
-
const binding = inheritSwarmTierBinding(env, nowIso);
|
|
5587
|
-
const admission = admitSubagentSpawn(binding);
|
|
5588
|
-
if (!admission.allowed) {
|
|
5589
|
-
return { ok: false, reason: admission.reason, tier: binding.tier };
|
|
5590
|
-
}
|
|
5591
|
-
const agentRefusal = agentBindingRefusal(binding, input.agent);
|
|
5592
|
-
if (agentRefusal !== null) return { ok: false, reason: agentRefusal, tier: binding.tier };
|
|
5593
|
-
const resolved = resolveSuccessorLaunch({
|
|
5594
|
-
agent: binding.agent,
|
|
5595
|
-
maxTurns: input.max_turns,
|
|
5596
|
-
platform
|
|
5597
|
-
});
|
|
5598
|
-
if (!resolved.ok) return { ok: false, reason: resolved.reason, tier: binding.tier };
|
|
5599
|
-
const slot = claim({
|
|
5600
|
-
swarmId: binding.swarm_id,
|
|
5601
|
-
proposedCeiling: binding.subagent_budget,
|
|
5602
|
-
// The spend-cap POOL, recorded once per swarm exactly like the ceiling. The
|
|
5603
|
-
// child's cap is DEBITED from it below, not recomputed from this binding.
|
|
5604
|
-
proposedCapUsd: binding.spend_cap_usd,
|
|
5605
|
-
dir: resolveLedgerDir(env),
|
|
5606
|
-
nowIso
|
|
5607
|
-
});
|
|
5608
|
-
if (!slot.ok) return { ok: false, reason: slot.reason, tier: binding.tier };
|
|
5609
|
-
return {
|
|
5610
|
-
ok: true,
|
|
5611
|
-
bin: resolved.bin,
|
|
5612
|
-
args: resolved.args,
|
|
5613
|
-
agent: resolved.agent,
|
|
5614
|
-
tier: binding.tier,
|
|
5615
|
-
bound: true,
|
|
5616
|
-
// Re-export the same TIER with a DECREMENTED budget and the spend cap the
|
|
5617
|
-
// ledger just DEBITED. Exporting the binding verbatim (what this did before
|
|
5618
|
-
// #9312) meant the child re-read the full budget and every generation
|
|
5619
|
-
// restarted at zero. Recomputing the cap from THIS binding (what #9312 did)
|
|
5620
|
-
// bounded a chain but not a tree: three siblings each re-halved the parent's
|
|
5621
|
-
// untouched $50 and walked away with $75 between them.
|
|
5622
|
-
env: childBindingEnvFragment(binding, slot.capUsd),
|
|
5623
|
-
slot: slot.slot,
|
|
5624
|
-
capUsd: slot.capUsd,
|
|
5625
|
-
capRemainingUsd: slot.capRemainingUsd
|
|
5626
|
-
};
|
|
5627
|
-
}
|
|
5628
|
-
async function handleSpawnSuccessor(_deps, rawInput, _signal, spawnImpl = spawn) {
|
|
5827
|
+
async function handleSpawnSuccessor(_deps, rawInput, _signal, spawnImpl = spawn, overrides = {}) {
|
|
5629
5828
|
const retired = retiredCounterRefusal(rawInput);
|
|
5630
5829
|
if (retired !== null) throw invalidParams(TOOL_NAME20, retired);
|
|
5631
5830
|
if (!isToolInput20(rawInput)) {
|
|
5632
5831
|
throw invalidParams(TOOL_NAME20, "invalid input. Optional: { handoff_path, goal, cwd, max_turns, agent }.");
|
|
5633
5832
|
}
|
|
5634
5833
|
const handoffPath = rawInput.handoff_path?.trim() || newestHandoff();
|
|
5635
|
-
if (!handoffPath || !
|
|
5834
|
+
if (!handoffPath || !existsSync5(handoffPath)) {
|
|
5636
5835
|
return jsonContent({
|
|
5637
5836
|
tool: TOOL_NAME20,
|
|
5638
5837
|
schema_version: 1,
|
|
@@ -5657,27 +5856,52 @@ async function handleSpawnSuccessor(_deps, rawInput, _signal, spawnImpl = spawn)
|
|
|
5657
5856
|
}
|
|
5658
5857
|
});
|
|
5659
5858
|
}
|
|
5859
|
+
const platform = overrides.platform ?? process.platform;
|
|
5860
|
+
let resolvedBin = plan.bin;
|
|
5861
|
+
if (platform === "win32") {
|
|
5862
|
+
const resolution = resolveNativeWindowsExecutable(plan.bin, process.env, overrides.resolveWindowsExecutable);
|
|
5863
|
+
if (!resolution.ok) {
|
|
5864
|
+
return jsonContent({
|
|
5865
|
+
tool: TOOL_NAME20,
|
|
5866
|
+
schema_version: 1,
|
|
5867
|
+
payload: {
|
|
5868
|
+
spawned: false,
|
|
5869
|
+
reason: resolution.reason,
|
|
5870
|
+
agent: plan.agent,
|
|
5871
|
+
tier: plan.tier,
|
|
5872
|
+
handoff_path: handoffPath
|
|
5873
|
+
}
|
|
5874
|
+
});
|
|
5875
|
+
}
|
|
5876
|
+
resolvedBin = resolution.bin;
|
|
5877
|
+
}
|
|
5660
5878
|
const logDir = process.env["VO_MCP_SUCCESSOR_LOG_DIR"]?.trim() || join7(homedir5(), ".vo", "successors");
|
|
5661
5879
|
mkdirSync4(logDir, { recursive: true });
|
|
5662
5880
|
const logPath = join7(logDir, `successor-${Date.now()}.log`);
|
|
5663
5881
|
const logFd = openSync2(logPath, "a");
|
|
5664
|
-
const child = spawnImpl(
|
|
5882
|
+
const child = spawnImpl(resolvedBin, [...plan.args], {
|
|
5665
5883
|
cwd: rawInput.cwd?.trim() || process.cwd(),
|
|
5666
5884
|
detached: true,
|
|
5667
5885
|
stdio: ["pipe", logFd, logFd],
|
|
5668
|
-
//
|
|
5669
|
-
//
|
|
5670
|
-
|
|
5886
|
+
// Never a shell: `resolvedBin` is either the bare platform-neutral name
|
|
5887
|
+
// (POSIX, resolved by the OS via PATH + shebang) or the native win32 exe
|
|
5888
|
+
// resolved above — routing either through cmd.exe/sh is the extra layer
|
|
5889
|
+
// a detached, unref'd child can lose silently (2026-08-16 incident).
|
|
5890
|
+
shell: false,
|
|
5671
5891
|
windowsHide: true,
|
|
5892
|
+
windowsVerbatimArguments: false,
|
|
5672
5893
|
// Carry the SAME binding to the child. Without this the successor inherits
|
|
5673
5894
|
// no tier and re-resolves its own — which is the split-payer defect one
|
|
5674
5895
|
// generation down.
|
|
5675
5896
|
...plan.bound ? { env: { ...process.env, ...plan.env } } : {}
|
|
5676
5897
|
});
|
|
5898
|
+
closeSync2(logFd);
|
|
5677
5899
|
let spawnError = null;
|
|
5678
5900
|
child.on("error", (e) => {
|
|
5679
5901
|
spawnError = e.message;
|
|
5680
5902
|
});
|
|
5903
|
+
child.stdin.on?.("error", () => {
|
|
5904
|
+
});
|
|
5681
5905
|
try {
|
|
5682
5906
|
child.stdin.write(prompt);
|
|
5683
5907
|
child.stdin.end();
|
|
@@ -5685,10 +5909,40 @@ async function handleSpawnSuccessor(_deps, rawInput, _signal, spawnImpl = spawn)
|
|
|
5685
5909
|
}
|
|
5686
5910
|
child.unref();
|
|
5687
5911
|
await new Promise((r) => setTimeout(r, 150));
|
|
5912
|
+
if (spawnError) {
|
|
5913
|
+
return jsonContent({
|
|
5914
|
+
tool: TOOL_NAME20,
|
|
5915
|
+
schema_version: 1,
|
|
5916
|
+
payload: {
|
|
5917
|
+
spawned: false,
|
|
5918
|
+
reason: `spawn failed: ${spawnError}`,
|
|
5919
|
+
agent: plan.agent,
|
|
5920
|
+
tier: plan.tier,
|
|
5921
|
+
handoff_path: handoffPath
|
|
5922
|
+
}
|
|
5923
|
+
});
|
|
5924
|
+
}
|
|
5925
|
+
const checkLiveness = overrides.checkLiveness ?? ((c, p) => checkSuccessorLiveness(c, p, resolveLivenessConfigFromEnv(process.env, overrides.livenessConfig), overrides.livenessDeps));
|
|
5926
|
+
const liveness = await checkLiveness(child, logPath);
|
|
5927
|
+
if (!liveness.ok) {
|
|
5928
|
+
return jsonContent({
|
|
5929
|
+
tool: TOOL_NAME20,
|
|
5930
|
+
schema_version: 1,
|
|
5931
|
+
payload: {
|
|
5932
|
+
spawned: false,
|
|
5933
|
+
reason: liveness.reason,
|
|
5934
|
+
pid: child.pid ?? null,
|
|
5935
|
+
log_path: logPath,
|
|
5936
|
+
agent: plan.agent,
|
|
5937
|
+
tier: plan.tier,
|
|
5938
|
+
handoff_path: handoffPath
|
|
5939
|
+
}
|
|
5940
|
+
});
|
|
5941
|
+
}
|
|
5688
5942
|
return jsonContent({
|
|
5689
5943
|
tool: TOOL_NAME20,
|
|
5690
5944
|
schema_version: 1,
|
|
5691
|
-
payload:
|
|
5945
|
+
payload: {
|
|
5692
5946
|
spawned: true,
|
|
5693
5947
|
pid: child.pid ?? null,
|
|
5694
5948
|
log_path: logPath,
|
|
@@ -5700,7 +5954,10 @@ async function handleSpawnSuccessor(_deps, rawInput, _signal, spawnImpl = spawn)
|
|
|
5700
5954
|
// The debit, surfaced so an operator can reconcile a fan-out's spend
|
|
5701
5955
|
// against the pool without reading the ledger directory by hand.
|
|
5702
5956
|
ledger_cap_usd: plan.capUsd,
|
|
5703
|
-
ledger_cap_remaining_usd: plan.capRemainingUsd
|
|
5957
|
+
ledger_cap_remaining_usd: plan.capRemainingUsd,
|
|
5958
|
+
// Additive (2026-08-17): true only once the child survived the
|
|
5959
|
+
// early-exit window (and the output window, when that gate is enabled).
|
|
5960
|
+
verified_alive: true
|
|
5704
5961
|
}
|
|
5705
5962
|
});
|
|
5706
5963
|
}
|
|
@@ -5787,22 +6044,22 @@ async function handleConciergeDispatch(deps, rawInput, _signal) {
|
|
|
5787
6044
|
}
|
|
5788
6045
|
|
|
5789
6046
|
// src/tools/memory/sync-config.ts
|
|
5790
|
-
import { existsSync as
|
|
6047
|
+
import { existsSync as existsSync9 } from "node:fs";
|
|
5791
6048
|
import { homedir as homedir7 } from "node:os";
|
|
5792
6049
|
import { join as join11 } from "node:path";
|
|
5793
6050
|
|
|
5794
6051
|
// src/tools/memory/memory-sync-http.ts
|
|
5795
6052
|
init_safe_memory_file();
|
|
5796
|
-
import { existsSync as
|
|
6053
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync6, readdirSync as readdirSync4 } from "node:fs";
|
|
5797
6054
|
|
|
5798
6055
|
// src/tools/memory/sync-lock.ts
|
|
5799
|
-
import { closeSync as
|
|
6056
|
+
import { closeSync as closeSync3, mkdirSync as mkdirSync5, openSync as openSync3, readFileSync as readFileSync9, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "node:fs";
|
|
5800
6057
|
import { hostname } from "node:os";
|
|
5801
6058
|
import { join as join8 } from "node:path";
|
|
5802
6059
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
5803
6060
|
|
|
5804
6061
|
// src/tools/memory/sync-lock-liveness.ts
|
|
5805
|
-
import { statSync as
|
|
6062
|
+
import { statSync as statSync5, readFileSync as readFileSync8 } from "node:fs";
|
|
5806
6063
|
function defaultIsProcessAlive(pid) {
|
|
5807
6064
|
try {
|
|
5808
6065
|
process.kill(pid, 0);
|
|
@@ -5828,10 +6085,10 @@ function toPayload(parsed) {
|
|
|
5828
6085
|
acquiredAtMs: typeof acquiredAtMs === "number" && Number.isFinite(acquiredAtMs) ? acquiredAtMs : Number.NaN
|
|
5829
6086
|
};
|
|
5830
6087
|
}
|
|
5831
|
-
function readLockRecord(
|
|
6088
|
+
function readLockRecord(path4) {
|
|
5832
6089
|
let raw;
|
|
5833
6090
|
try {
|
|
5834
|
-
raw = readFileSync8(
|
|
6091
|
+
raw = readFileSync8(path4, "utf8");
|
|
5835
6092
|
} catch {
|
|
5836
6093
|
return null;
|
|
5837
6094
|
}
|
|
@@ -5841,7 +6098,7 @@ function readLockRecord(path3) {
|
|
|
5841
6098
|
return { raw, payload: null };
|
|
5842
6099
|
}
|
|
5843
6100
|
}
|
|
5844
|
-
function lockAgeMs(record,
|
|
6101
|
+
function lockAgeMs(record, path4, nowMs) {
|
|
5845
6102
|
let startedMs = Number.NaN;
|
|
5846
6103
|
if (record.payload) {
|
|
5847
6104
|
if (Number.isFinite(record.payload.acquiredAtMs)) {
|
|
@@ -5852,7 +6109,7 @@ function lockAgeMs(record, path3, nowMs) {
|
|
|
5852
6109
|
}
|
|
5853
6110
|
if (!Number.isFinite(startedMs)) {
|
|
5854
6111
|
try {
|
|
5855
|
-
startedMs =
|
|
6112
|
+
startedMs = statSync5(path4).mtimeMs;
|
|
5856
6113
|
} catch {
|
|
5857
6114
|
return null;
|
|
5858
6115
|
}
|
|
@@ -5885,10 +6142,10 @@ var BACKOFF_FACTOR = 1.6;
|
|
|
5885
6142
|
function positiveOr(value, fallback) {
|
|
5886
6143
|
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : fallback;
|
|
5887
6144
|
}
|
|
5888
|
-
function createExclusive2(
|
|
6145
|
+
function createExclusive2(path4, contents) {
|
|
5889
6146
|
let fd;
|
|
5890
6147
|
try {
|
|
5891
|
-
fd = openSync3(
|
|
6148
|
+
fd = openSync3(path4, "wx");
|
|
5892
6149
|
} catch (err) {
|
|
5893
6150
|
const code = err.code;
|
|
5894
6151
|
return { ok: false, exists: code === "EEXIST", message: err instanceof Error ? err.message : String(err) };
|
|
@@ -5896,37 +6153,37 @@ function createExclusive2(path3, contents) {
|
|
|
5896
6153
|
try {
|
|
5897
6154
|
writeFileSync4(fd, contents, "utf8");
|
|
5898
6155
|
} catch (err) {
|
|
5899
|
-
|
|
6156
|
+
closeSync3(fd);
|
|
5900
6157
|
try {
|
|
5901
|
-
unlinkSync2(
|
|
6158
|
+
unlinkSync2(path4);
|
|
5902
6159
|
} catch {
|
|
5903
6160
|
}
|
|
5904
6161
|
return { ok: false, exists: false, message: err instanceof Error ? err.message : String(err) };
|
|
5905
6162
|
}
|
|
5906
|
-
|
|
6163
|
+
closeSync3(fd);
|
|
5907
6164
|
return { ok: true };
|
|
5908
6165
|
}
|
|
5909
|
-
function removeAbandoned(
|
|
6166
|
+
function removeAbandoned(path4, expectedRaw) {
|
|
5910
6167
|
let current;
|
|
5911
6168
|
try {
|
|
5912
|
-
current = readFileSync9(
|
|
6169
|
+
current = readFileSync9(path4, "utf8");
|
|
5913
6170
|
} catch {
|
|
5914
6171
|
return;
|
|
5915
6172
|
}
|
|
5916
6173
|
if (current !== expectedRaw) return;
|
|
5917
6174
|
try {
|
|
5918
|
-
unlinkSync2(
|
|
6175
|
+
unlinkSync2(path4);
|
|
5919
6176
|
} catch {
|
|
5920
6177
|
}
|
|
5921
6178
|
}
|
|
5922
|
-
function makeRelease(
|
|
6179
|
+
function makeRelease(path4, token) {
|
|
5923
6180
|
let released = false;
|
|
5924
6181
|
return () => {
|
|
5925
6182
|
if (released) return;
|
|
5926
6183
|
released = true;
|
|
5927
6184
|
let raw;
|
|
5928
6185
|
try {
|
|
5929
|
-
raw = readFileSync9(
|
|
6186
|
+
raw = readFileSync9(path4, "utf8");
|
|
5930
6187
|
} catch {
|
|
5931
6188
|
return;
|
|
5932
6189
|
}
|
|
@@ -5938,7 +6195,7 @@ function makeRelease(path3, token) {
|
|
|
5938
6195
|
}
|
|
5939
6196
|
if (!stillOurs) return;
|
|
5940
6197
|
try {
|
|
5941
|
-
unlinkSync2(
|
|
6198
|
+
unlinkSync2(path4);
|
|
5942
6199
|
} catch {
|
|
5943
6200
|
}
|
|
5944
6201
|
};
|
|
@@ -5957,7 +6214,7 @@ async function acquireMemorySyncLock(options) {
|
|
|
5957
6214
|
}));
|
|
5958
6215
|
const isProcessAlive = options.isProcessAlive ?? defaultIsProcessAlive;
|
|
5959
6216
|
const thisHost = hostname();
|
|
5960
|
-
const
|
|
6217
|
+
const path4 = join8(options.memoryDir, MEMORY_SYNC_LOCK_FILE);
|
|
5961
6218
|
if (options.createDir === true) mkdirSync5(options.memoryDir, { recursive: true });
|
|
5962
6219
|
const deadline = now() + waitMs;
|
|
5963
6220
|
let backoffMs = INITIAL_BACKOFF_MS;
|
|
@@ -5973,30 +6230,30 @@ async function acquireMemorySyncLock(options) {
|
|
|
5973
6230
|
acquiredAt: new Date(acquiredAtMs).toISOString(),
|
|
5974
6231
|
acquiredAtMs
|
|
5975
6232
|
};
|
|
5976
|
-
const created = createExclusive2(
|
|
6233
|
+
const created = createExclusive2(path4, `${JSON.stringify(payload, null, 2)}
|
|
5977
6234
|
`);
|
|
5978
6235
|
if (created.ok) {
|
|
5979
|
-
return { path:
|
|
6236
|
+
return { path: path4, payload, tookOverFrom, release: makeRelease(path4, payload.token) };
|
|
5980
6237
|
}
|
|
5981
6238
|
if (!created.exists) {
|
|
5982
6239
|
throw new Error(
|
|
5983
|
-
`memory sync lock ${
|
|
6240
|
+
`memory sync lock ${path4} could not be created (${created.message}) \u2014 refusing to sync without exclusion`
|
|
5984
6241
|
);
|
|
5985
6242
|
}
|
|
5986
|
-
const record = readLockRecord(
|
|
6243
|
+
const record = readLockRecord(path4);
|
|
5987
6244
|
let reclaimed = false;
|
|
5988
6245
|
if (record) {
|
|
5989
6246
|
holderDescription = describeHolder(record);
|
|
5990
|
-
const age = lockAgeMs(record,
|
|
6247
|
+
const age = lockAgeMs(record, path4, now());
|
|
5991
6248
|
if (isLockAbandoned(record, age, ttlMs, isProcessAlive, thisHost)) {
|
|
5992
6249
|
tookOverFrom = record.payload;
|
|
5993
|
-
removeAbandoned(
|
|
6250
|
+
removeAbandoned(path4, record.raw);
|
|
5994
6251
|
reclaimed = true;
|
|
5995
6252
|
}
|
|
5996
6253
|
}
|
|
5997
6254
|
if (now() >= deadline) {
|
|
5998
6255
|
throw new Error(
|
|
5999
|
-
`memory sync lock ${
|
|
6256
|
+
`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.`
|
|
6000
6257
|
);
|
|
6001
6258
|
}
|
|
6002
6259
|
if (reclaimed) backoffMs = INITIAL_BACKOFF_MS;
|
|
@@ -6148,7 +6405,7 @@ async function uploadOne(item, controlPlaneUrl, token, sessionId, fetchFn, deadl
|
|
|
6148
6405
|
}
|
|
6149
6406
|
async function pushMemory(controlPlaneUrl, token, memoryDir, sessionId, fetchFn, options = {}) {
|
|
6150
6407
|
const empty = { pushed: 0, created: 0, updated: 0, skipped: 0, indexRowsPreserved: 0 };
|
|
6151
|
-
if (!
|
|
6408
|
+
if (!existsSync6(memoryDir)) {
|
|
6152
6409
|
return empty;
|
|
6153
6410
|
}
|
|
6154
6411
|
const localFiles = listPushableFiles(memoryDir).map((f) => ({
|
|
@@ -6236,7 +6493,7 @@ init_bounded_sync();
|
|
|
6236
6493
|
init_memory_push_cache();
|
|
6237
6494
|
|
|
6238
6495
|
// src/tools/memory/sync-kill-switch.ts
|
|
6239
|
-
import { existsSync as
|
|
6496
|
+
import { existsSync as existsSync7, readFileSync as readFileSync12 } from "node:fs";
|
|
6240
6497
|
import { homedir as homedir6 } from "node:os";
|
|
6241
6498
|
import { join as join10 } from "node:path";
|
|
6242
6499
|
var MEMORY_SYNC_DISABLE_ENV = "VO_MCP_DISABLE_MEMORY_SYNC";
|
|
@@ -6259,7 +6516,7 @@ function clip(raw) {
|
|
|
6259
6516
|
function evaluateMemorySyncKillSwitch(deps = {}) {
|
|
6260
6517
|
const env = deps.env ?? process.env;
|
|
6261
6518
|
const home = deps.home ?? homedir6();
|
|
6262
|
-
const fileExists = deps.fileExists ??
|
|
6519
|
+
const fileExists = deps.fileExists ?? existsSync7;
|
|
6263
6520
|
const readFile3 = deps.readFile ?? ((p) => readFileSync12(p, "utf8"));
|
|
6264
6521
|
const fired = [];
|
|
6265
6522
|
const rawEnv = env[MEMORY_SYNC_DISABLE_ENV];
|
|
@@ -6343,7 +6600,7 @@ async function runMemorySync(action, cwd, sessionId, fetchFn = globalThis.fetch,
|
|
|
6343
6600
|
}
|
|
6344
6601
|
const memoryDir = getMemoryDir(cwd);
|
|
6345
6602
|
const baseUrl = controlPlaneUrl.replace(/\/+$/, "");
|
|
6346
|
-
if (action === "push" && !
|
|
6603
|
+
if (action === "push" && !existsSync9(memoryDir)) {
|
|
6347
6604
|
return {
|
|
6348
6605
|
synced: true,
|
|
6349
6606
|
action: "push",
|
|
@@ -6524,10 +6781,10 @@ async function getCloudAuth(fetchFn) {
|
|
|
6524
6781
|
if (!token) return { ok: false, reason: "Failed to obtain auth token. Run `vo-mcp login` again." };
|
|
6525
6782
|
return { ok: true, controlPlaneUrl, token };
|
|
6526
6783
|
}
|
|
6527
|
-
async function callPrivateKnowledge(
|
|
6784
|
+
async function callPrivateKnowledge(path4, body, fetchFn) {
|
|
6528
6785
|
const auth = await getCloudAuth(fetchFn);
|
|
6529
6786
|
if (!auth.ok) return { ok: false, reason: auth.reason };
|
|
6530
|
-
const response = await fetchFn(`${auth.controlPlaneUrl}${
|
|
6787
|
+
const response = await fetchFn(`${auth.controlPlaneUrl}${path4}`, {
|
|
6531
6788
|
method: "POST",
|
|
6532
6789
|
headers: {
|
|
6533
6790
|
authorization: `Bearer ${auth.token}`,
|
|
@@ -6744,11 +7001,11 @@ async function handleHqWhiteboardRead(_deps, rawInput, signal) {
|
|
|
6744
7001
|
}
|
|
6745
7002
|
|
|
6746
7003
|
// src/tools/skills/skill-corpus.ts
|
|
6747
|
-
import { existsSync as
|
|
7004
|
+
import { existsSync as existsSync10, statSync as statSync7 } from "node:fs";
|
|
6748
7005
|
import { dirname as dirname5, isAbsolute, join as join13, resolve as resolve2 } from "node:path";
|
|
6749
7006
|
|
|
6750
7007
|
// ../skill-registry/src/loader.ts
|
|
6751
|
-
import { readdirSync as readdirSync6, readFileSync as readFileSync14, statSync as
|
|
7008
|
+
import { readdirSync as readdirSync6, readFileSync as readFileSync14, statSync as statSync6 } from "node:fs";
|
|
6752
7009
|
import { join as join12 } from "node:path";
|
|
6753
7010
|
var InvalidSkillFrontmatterError = class extends Error {
|
|
6754
7011
|
constructor(skillFile, reason) {
|
|
@@ -6805,7 +7062,7 @@ function loadSkillsFromDir(skillsDir) {
|
|
|
6805
7062
|
const entryPath = join12(skillsDir, entry);
|
|
6806
7063
|
let stat;
|
|
6807
7064
|
try {
|
|
6808
|
-
stat =
|
|
7065
|
+
stat = statSync6(entryPath);
|
|
6809
7066
|
} catch {
|
|
6810
7067
|
continue;
|
|
6811
7068
|
}
|
|
@@ -6854,12 +7111,12 @@ function resolveSkillsDir(env = process.env, startDir = process.cwd()) {
|
|
|
6854
7111
|
const override = env.VO_SKILLS_DIR;
|
|
6855
7112
|
if (typeof override === "string" && override.length > 0) {
|
|
6856
7113
|
const abs = isAbsolute(override) ? override : resolve2(startDir, override);
|
|
6857
|
-
return
|
|
7114
|
+
return existsSync10(abs) && statSync7(abs).isDirectory() ? abs : null;
|
|
6858
7115
|
}
|
|
6859
7116
|
let dir = resolve2(startDir);
|
|
6860
7117
|
for (let i = 0; i < MAX_WALK_UP_LEVELS; i += 1) {
|
|
6861
7118
|
const candidate = join13(dir, ".claude", "skills");
|
|
6862
|
-
if (
|
|
7119
|
+
if (existsSync10(candidate) && statSync7(candidate).isDirectory()) return candidate;
|
|
6863
7120
|
const parent = dirname5(dir);
|
|
6864
7121
|
if (parent === dir) break;
|
|
6865
7122
|
dir = parent;
|