@algosuite/vo-mcp 0.2.0-beta.28 → 0.2.0-beta.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -318,13 +318,191 @@ var init_safe_memory_file = __esm({
318
318
  }
319
319
  });
320
320
 
321
+ // src/tools/memory/bounded-sync.ts
322
+ function createSyncDeadline(budgetMs = SYNC_DEADLINE_MS, now = Date.now) {
323
+ const startedAt = now();
324
+ return {
325
+ check() {
326
+ const elapsed = now() - startedAt;
327
+ if (elapsed > budgetMs) throw new SyncDeadlineExceededError(elapsed, budgetMs);
328
+ },
329
+ remainingMs() {
330
+ return Math.max(0, budgetMs - (now() - startedAt));
331
+ }
332
+ };
333
+ }
334
+ async function withRequestTimeout(url, run, budgetMs = REQUEST_TIMEOUT_MS) {
335
+ let timer;
336
+ try {
337
+ return await Promise.race([
338
+ run(),
339
+ new Promise((_resolve, reject) => {
340
+ timer = setTimeout(() => reject(new RequestTimeoutError(url, budgetMs)), budgetMs);
341
+ timer.unref?.();
342
+ })
343
+ ]);
344
+ } finally {
345
+ if (timer) clearTimeout(timer);
346
+ }
347
+ }
348
+ async function mapWithConcurrency(items, limit, fn) {
349
+ const results = new Array(items.length);
350
+ const width = Math.max(1, Math.min(limit, items.length));
351
+ let next = 0;
352
+ async function worker() {
353
+ for (; ; ) {
354
+ const index = next++;
355
+ if (index >= items.length) return;
356
+ try {
357
+ results[index] = { ok: true, value: await fn(items[index], index) };
358
+ } catch (error) {
359
+ results[index] = { ok: false, error };
360
+ }
361
+ }
362
+ }
363
+ await Promise.all(Array.from({ length: width }, () => worker()));
364
+ return results;
365
+ }
366
+ var REQUEST_TIMEOUT_MS, SYNC_DEADLINE_MS, PUSH_CONCURRENCY, SyncDeadlineExceededError, RequestTimeoutError;
367
+ var init_bounded_sync = __esm({
368
+ "src/tools/memory/bounded-sync.ts"() {
369
+ "use strict";
370
+ REQUEST_TIMEOUT_MS = 15e3;
371
+ SYNC_DEADLINE_MS = 12e4;
372
+ PUSH_CONCURRENCY = 6;
373
+ SyncDeadlineExceededError = class extends Error {
374
+ constructor(elapsedMs, budgetMs) {
375
+ super(
376
+ `memory sync exceeded its ${budgetMs}ms deadline after ${elapsedMs}ms \u2014 aborting so the lock is released instead of held indefinitely`
377
+ );
378
+ this.name = "SyncDeadlineExceededError";
379
+ }
380
+ };
381
+ RequestTimeoutError = class extends Error {
382
+ constructor(url, budgetMs) {
383
+ super(`memory sync request to ${url} exceeded ${budgetMs}ms`);
384
+ this.name = "RequestTimeoutError";
385
+ }
386
+ };
387
+ }
388
+ });
389
+
390
+ // src/tools/memory/memory-push-cache.ts
391
+ import { createHash as createHash3 } from "node:crypto";
392
+ import { readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "node:fs";
393
+ import { join as join9 } from "node:path";
394
+ function sha256(content) {
395
+ return createHash3("sha256").update(content, "utf8").digest("hex");
396
+ }
397
+ function statePath(memoryDir) {
398
+ return join9(memoryDir, MEMORY_SYNC_STATE_FILE);
399
+ }
400
+ function readPushCache(memoryDir, controlPlaneUrl) {
401
+ const empty = { controlPlaneUrl, entries: /* @__PURE__ */ new Map(), knowledgeSweptAtMs: null };
402
+ let raw;
403
+ try {
404
+ raw = readFileSync10(statePath(memoryDir), "utf8");
405
+ } catch {
406
+ return empty;
407
+ }
408
+ let parsed;
409
+ try {
410
+ parsed = JSON.parse(raw);
411
+ } catch {
412
+ return empty;
413
+ }
414
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return empty;
415
+ const obj = parsed;
416
+ if (obj["version"] !== STATE_VERSION) return empty;
417
+ if (obj["controlPlaneUrl"] !== controlPlaneUrl) return empty;
418
+ const files = obj["entries"];
419
+ if (typeof files !== "object" || files === null || Array.isArray(files)) return empty;
420
+ const entries = /* @__PURE__ */ new Map();
421
+ for (const [name, value] of Object.entries(files)) {
422
+ if (typeof value !== "object" || value === null || Array.isArray(value)) continue;
423
+ const row = value;
424
+ const memoryHash = typeof row["memoryHash"] === "string" ? row["memoryHash"] : void 0;
425
+ const knowledgeHash = typeof row["knowledgeHash"] === "string" ? row["knowledgeHash"] : void 0;
426
+ if (memoryHash === void 0 && knowledgeHash === void 0) continue;
427
+ entries.set(name, {
428
+ ...memoryHash !== void 0 ? { memoryHash } : {},
429
+ ...knowledgeHash !== void 0 ? { knowledgeHash } : {}
430
+ });
431
+ }
432
+ const sweptAt = obj["knowledgeSweptAtMs"];
433
+ return {
434
+ controlPlaneUrl,
435
+ entries,
436
+ // An unreadable/absent sweep stamp reads as NEVER SWEPT, which forces a full
437
+ // sweep — the fail-closed direction (more upserts, never fewer).
438
+ knowledgeSweptAtMs: typeof sweptAt === "number" && Number.isFinite(sweptAt) ? sweptAt : null
439
+ };
440
+ }
441
+ function writePushCache(memoryDir, cache) {
442
+ const entries = {};
443
+ for (const [name, value] of cache.entries) entries[name] = value;
444
+ try {
445
+ writeFileSync5(
446
+ statePath(memoryDir),
447
+ `${JSON.stringify(
448
+ {
449
+ version: STATE_VERSION,
450
+ controlPlaneUrl: cache.controlPlaneUrl,
451
+ knowledgeSweptAtMs: cache.knowledgeSweptAtMs,
452
+ entries
453
+ },
454
+ null,
455
+ 2
456
+ )}
457
+ `,
458
+ "utf8"
459
+ );
460
+ } catch {
461
+ }
462
+ }
463
+ function recordMemoryPush(cache, fileName, payloadHash) {
464
+ cache.entries.set(fileName, { ...cache.entries.get(fileName), memoryHash: payloadHash });
465
+ }
466
+ function recordKnowledgePush(cache, fileName, contentHash) {
467
+ cache.entries.set(fileName, { ...cache.entries.get(fileName), knowledgeHash: contentHash });
468
+ }
469
+ function pruneMissing(cache, presentFileNames) {
470
+ const present = new Set(presentFileNames);
471
+ for (const name of [...cache.entries.keys()]) {
472
+ if (!present.has(name)) cache.entries.delete(name);
473
+ }
474
+ }
475
+ function needsMemoryPush(cache, fileName, payloadHash, serverHasEntry) {
476
+ if (!serverHasEntry) return true;
477
+ return cache.entries.get(fileName)?.memoryHash !== payloadHash;
478
+ }
479
+ function knowledgeSweepDue(cache, nowMs = Date.now()) {
480
+ const swept = cache.knowledgeSweptAtMs;
481
+ if (swept === null || !Number.isFinite(swept)) return true;
482
+ const age = nowMs - swept;
483
+ return !(age >= 0 && age < KNOWLEDGE_FULL_SWEEP_MS);
484
+ }
485
+ function needsKnowledgePush(cache, fileName, contentHash, sweepDue = false) {
486
+ if (sweepDue) return true;
487
+ return cache.entries.get(fileName)?.knowledgeHash !== contentHash;
488
+ }
489
+ var MEMORY_SYNC_STATE_FILE, STATE_VERSION, KNOWLEDGE_FULL_SWEEP_MS;
490
+ var init_memory_push_cache = __esm({
491
+ "src/tools/memory/memory-push-cache.ts"() {
492
+ "use strict";
493
+ MEMORY_SYNC_STATE_FILE = ".memory-sync-state.json";
494
+ STATE_VERSION = 1;
495
+ KNOWLEDGE_FULL_SWEEP_MS = 24 * 60 * 6e4;
496
+ }
497
+ });
498
+
321
499
  // src/tools/memory/memory-knowledge-bridge.ts
322
500
  var memory_knowledge_bridge_exports = {};
323
501
  __export(memory_knowledge_bridge_exports, {
324
502
  extractMemoryTitle: () => extractMemoryTitle,
325
503
  upsertMemoryFilesAsKnowledge: () => upsertMemoryFilesAsKnowledge
326
504
  });
327
- import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as readFileSync7 } from "node:fs";
505
+ import { existsSync as existsSync7, readdirSync as readdirSync5, readFileSync as readFileSync13 } from "node:fs";
328
506
  function extractMemoryTitle(fileName, content) {
329
507
  const frontmatter = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
330
508
  if (frontmatter) {
@@ -336,13 +514,13 @@ function extractMemoryTitle(fileName, content) {
336
514
  return fileName;
337
515
  }
338
516
  async function upsertMemoryFilesAsKnowledge(options) {
339
- const { controlPlaneUrl, token, memoryDir, fetchFn } = options;
517
+ const { controlPlaneUrl, token, memoryDir, fetchFn, cache, deadline } = options;
340
518
  let files;
341
519
  try {
342
- if (!existsSync5(memoryDir)) {
343
- return { attempted: 0, upserted: 0, failed: 0, failures: [] };
520
+ if (!existsSync7(memoryDir)) {
521
+ return { attempted: 0, upserted: 0, failed: 0, skipped: 0, failures: [] };
344
522
  }
345
- files = readdirSync4(memoryDir).filter(
523
+ files = readdirSync5(memoryDir).filter(
346
524
  (f) => f.endsWith(".md") && f.toUpperCase() !== "MEMORY.MD"
347
525
  );
348
526
  } catch (err) {
@@ -350,54 +528,81 @@ async function upsertMemoryFilesAsKnowledge(options) {
350
528
  attempted: 0,
351
529
  upserted: 0,
352
530
  failed: 1,
531
+ skipped: 0,
353
532
  failures: [`memory dir scan: ${err instanceof Error ? err.message : String(err)}`]
354
533
  };
355
534
  }
356
- let upserted = 0;
535
+ const sweepDue = cache ? knowledgeSweepDue(cache) : true;
536
+ const candidates = [];
357
537
  const failures = [];
538
+ let skipped = 0;
358
539
  for (const fileName of files) {
359
540
  try {
360
- const content = readFileSync7(resolveMemoryFilePath(memoryDir, fileName), "utf8");
541
+ const content = readFileSync13(resolveMemoryFilePath(memoryDir, fileName), "utf8");
361
542
  if (content.length > CONTENT_HARD_LIMIT) {
362
543
  failures.push(`${fileName}: ${content.length} chars exceeds the ${CONTENT_HARD_LIMIT} server limit \u2014 split the memory file`);
363
544
  continue;
364
545
  }
365
- const title = extractMemoryTitle(fileName, content);
366
- const base = {
367
- knowledge_class: "memory",
368
- source_path: `memory/${fileName}`,
369
- title,
370
- content
371
- };
372
- const post = (body) => fetchFn(`${controlPlaneUrl}/api/v1/knowledge/private`, {
373
- method: "POST",
374
- headers: {
375
- authorization: `Bearer ${token}`,
376
- "content-type": "application/json"
377
- },
378
- body: JSON.stringify(body)
379
- });
380
- let response = await post({
381
- ...base,
382
- provenance: { written_by: "memory-bridge", source_kind: "operator_memory" }
383
- });
384
- if (response.status === 400) {
385
- response = await post(base);
386
- }
387
- if (response.status >= 200 && response.status < 300) {
388
- upserted += 1;
389
- } else {
390
- const text = await response.text();
391
- failures.push(`${fileName}: HTTP ${response.status} ${text.slice(0, 80)}`);
546
+ const hash = sha256(content);
547
+ if (cache && !needsKnowledgePush(cache, fileName, hash, sweepDue)) {
548
+ skipped += 1;
549
+ continue;
392
550
  }
551
+ candidates.push({ fileName, content, hash });
393
552
  } catch (err) {
394
553
  failures.push(`${fileName}: ${err instanceof Error ? err.message : String(err)}`);
395
554
  }
396
555
  }
556
+ const url = `${controlPlaneUrl}/api/v1/knowledge/private`;
557
+ const outcomes = await mapWithConcurrency(candidates, options.concurrency ?? PUSH_CONCURRENCY, async (candidate) => {
558
+ deadline?.check();
559
+ const base = {
560
+ knowledge_class: "memory",
561
+ source_path: `memory/${candidate.fileName}`,
562
+ title: extractMemoryTitle(candidate.fileName, candidate.content),
563
+ content: candidate.content
564
+ };
565
+ const post = (body) => withRequestTimeout(url, () => fetchFn(url, {
566
+ method: "POST",
567
+ headers: {
568
+ authorization: `Bearer ${token}`,
569
+ "content-type": "application/json"
570
+ },
571
+ body: JSON.stringify(body)
572
+ }));
573
+ let response = await post({
574
+ ...base,
575
+ provenance: { written_by: "memory-bridge", source_kind: "operator_memory" }
576
+ });
577
+ if (response.status === 400) {
578
+ response = await post(base);
579
+ }
580
+ if (response.status >= 200 && response.status < 300) return true;
581
+ const text = await response.text();
582
+ throw new Error(`HTTP ${response.status} ${text.slice(0, 80)}`);
583
+ });
584
+ let upserted = 0;
585
+ for (let i = 0; i < outcomes.length; i++) {
586
+ const outcome = outcomes[i];
587
+ const candidate = candidates[i];
588
+ if (outcome.ok) {
589
+ upserted += 1;
590
+ if (cache) recordKnowledgePush(cache, candidate.fileName, candidate.hash);
591
+ } else {
592
+ const error = outcome.error;
593
+ failures.push(`${candidate.fileName}: ${error instanceof Error ? error.message : String(error)}`);
594
+ }
595
+ }
596
+ if (cache && sweepDue && failures.length === 0) {
597
+ cache.knowledgeSweptAtMs = Date.now();
598
+ }
397
599
  return {
600
+ // Every memory file this run considered. `attempted === upserted + skipped
601
+ // + failed` holds, so a caller can tell "nothing to do" from "nothing ran".
398
602
  attempted: files.length,
399
603
  upserted,
400
604
  failed: failures.length,
605
+ skipped,
401
606
  failures: failures.slice(0, 5)
402
607
  };
403
608
  }
@@ -406,12 +611,14 @@ var init_memory_knowledge_bridge = __esm({
406
611
  "src/tools/memory/memory-knowledge-bridge.ts"() {
407
612
  "use strict";
408
613
  init_safe_memory_file();
614
+ init_bounded_sync();
615
+ init_memory_push_cache();
409
616
  CONTENT_HARD_LIMIT = 5e5;
410
617
  }
411
618
  });
412
619
 
413
620
  // src/server.ts
414
- import { randomUUID as randomUUID2 } from "node:crypto";
621
+ import { randomUUID as randomUUID3 } from "node:crypto";
415
622
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
416
623
  import {
417
624
  CallToolRequestSchema,
@@ -4843,9 +5050,324 @@ async function handleReportSessionState(deps, rawInput, _signal) {
4843
5050
 
4844
5051
  // src/tools/session/spawn-successor.ts
4845
5052
  import { spawn } from "node:child_process";
5053
+ import { homedir as homedir5 } from "node:os";
5054
+ import { join as join7 } from "node:path";
5055
+ import { existsSync as existsSync4, mkdirSync as mkdirSync4, openSync as openSync2, readFileSync as readFileSync7, readdirSync as readdirSync3, statSync as statSync3 } from "node:fs";
5056
+
5057
+ // src/swarm/tier-binding.ts
5058
+ var SWARM_TIERS = Object.freeze([
5059
+ "tier1_subscription",
5060
+ "tier1_local",
5061
+ "tier2_user_key",
5062
+ "tier3_platform_key",
5063
+ "refused",
5064
+ "unresolved"
5065
+ ]);
5066
+ var TIER_ADMITS_SPAWN = /* @__PURE__ */ new Set([
5067
+ "tier1_subscription",
5068
+ "tier1_local",
5069
+ "tier2_user_key",
5070
+ "tier3_platform_key"
5071
+ ]);
5072
+ var SWARM_TIER_BINDING_ENV = "VO_SWARM_TIER_BINDING";
5073
+ var MAX_BOUND_SUBAGENTS = 20;
5074
+ function isPositiveCap(cap) {
5075
+ return typeof cap === "number" && Number.isFinite(cap) && cap > 0;
5076
+ }
5077
+ function unresolvedBinding(swarmId, nowIso, reason) {
5078
+ return {
5079
+ schema_version: 1,
5080
+ swarm_id: swarmId,
5081
+ tier: "unresolved",
5082
+ agent: null,
5083
+ reason,
5084
+ exhausted_agents: [],
5085
+ subagent_budget: 0,
5086
+ spend_cap_usd: null,
5087
+ resolved_at: nowIso
5088
+ };
5089
+ }
5090
+ function serializeSwarmTierBinding(binding) {
5091
+ return JSON.stringify(binding);
5092
+ }
5093
+ function parseSwarmTierBinding(raw, nowIso) {
5094
+ if (typeof raw !== "string" || raw.trim().length === 0) {
5095
+ return unresolvedBinding("", nowIso, "no swarm tier binding present in the environment");
5096
+ }
5097
+ let parsed;
5098
+ try {
5099
+ parsed = JSON.parse(raw);
5100
+ } catch {
5101
+ return unresolvedBinding("", nowIso, "swarm tier binding is not valid JSON");
5102
+ }
5103
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
5104
+ return unresolvedBinding("", nowIso, "swarm tier binding is not an object");
5105
+ }
5106
+ const o = parsed;
5107
+ const swarmId = typeof o["swarm_id"] === "string" ? o["swarm_id"] : "";
5108
+ if (o["schema_version"] !== 1) {
5109
+ return unresolvedBinding(swarmId, nowIso, "swarm tier binding has an unsupported schema_version");
5110
+ }
5111
+ const tier = o["tier"];
5112
+ if (typeof tier !== "string" || !SWARM_TIERS.includes(tier)) {
5113
+ return unresolvedBinding(swarmId, nowIso, "swarm tier binding names an unknown tier");
5114
+ }
5115
+ const budget = o["subagent_budget"];
5116
+ const cap = o["spend_cap_usd"];
5117
+ const capNum = isPositiveCap(cap) ? cap : null;
5118
+ if (tier === "tier3_platform_key" && capNum === null) {
5119
+ return unresolvedBinding(
5120
+ swarmId,
5121
+ nowIso,
5122
+ "inherited tier3_platform_key binding carries no positive numeric spend cap \u2014 refusing an uncapped platform-billed fan-out"
5123
+ );
5124
+ }
5125
+ return {
5126
+ schema_version: 1,
5127
+ swarm_id: swarmId,
5128
+ tier,
5129
+ agent: typeof o["agent"] === "string" ? o["agent"] : null,
5130
+ reason: typeof o["reason"] === "string" ? o["reason"] : "inherited binding carried no reason",
5131
+ exhausted_agents: Array.isArray(o["exhausted_agents"]) ? o["exhausted_agents"].filter((v) => typeof v === "string") : [],
5132
+ subagent_budget: typeof budget === "number" && Number.isFinite(budget) && budget > 0 ? Math.min(Math.floor(budget), MAX_BOUND_SUBAGENTS) : 0,
5133
+ spend_cap_usd: capNum,
5134
+ resolved_at: typeof o["resolved_at"] === "string" ? o["resolved_at"] : nowIso
5135
+ };
5136
+ }
5137
+ function inheritSwarmTierBinding(env, nowIso) {
5138
+ return parseSwarmTierBinding(env[SWARM_TIER_BINDING_ENV], nowIso);
5139
+ }
5140
+ function bindingEnvFragment(binding) {
5141
+ return { [SWARM_TIER_BINDING_ENV]: serializeSwarmTierBinding(binding) };
5142
+ }
5143
+ function childBindingEnvFragment(binding, allocatedCapUsd = null) {
5144
+ return bindingEnvFragment(childBinding(binding, allocatedCapUsd));
5145
+ }
5146
+ function admitSubagentSpawn(binding, spawnsSoFar = 0) {
5147
+ if (!TIER_ADMITS_SPAWN.has(binding.tier)) {
5148
+ return { allowed: false, reason: `tier '${binding.tier}' admits no spawn: ${binding.reason}` };
5149
+ }
5150
+ if (binding.tier === "tier3_platform_key" && !isPositiveCap(binding.spend_cap_usd)) {
5151
+ return {
5152
+ allowed: false,
5153
+ reason: `swarm ${binding.swarm_id} is tier3_platform_key with no positive spend cap \u2014 refusing to spend the platform owner's money uncapped`
5154
+ };
5155
+ }
5156
+ if (!Number.isFinite(spawnsSoFar) || spawnsSoFar < 0) {
5157
+ return { allowed: false, reason: "spawn counter is not a finite non-negative number" };
5158
+ }
5159
+ if (spawnsSoFar >= binding.subagent_budget) {
5160
+ return {
5161
+ allowed: false,
5162
+ reason: `swarm ${binding.swarm_id} exhausted its bound subagent budget (${binding.subagent_budget})`
5163
+ };
5164
+ }
5165
+ return { allowed: true, reason: `admitted under tier '${binding.tier}'` };
5166
+ }
5167
+ function childBinding(binding, allocatedCapUsd = null) {
5168
+ const allocated = isPositiveCap(allocatedCapUsd) ? allocatedCapUsd : null;
5169
+ const parentCap = isPositiveCap(binding.spend_cap_usd) ? binding.spend_cap_usd : null;
5170
+ return {
5171
+ ...binding,
5172
+ subagent_budget: Math.max(0, binding.subagent_budget - 1),
5173
+ // A child never carries more than its parent, whatever the ledger says: a
5174
+ // forged or hand-edited pool cannot inflate a descendant above the binding
5175
+ // it descends from.
5176
+ spend_cap_usd: allocated === null || parentCap === null ? null : Math.min(allocated, parentCap)
5177
+ };
5178
+ }
5179
+ function agentBindingRefusal(binding, requestedAgent) {
5180
+ const requested = typeof requestedAgent === "string" ? requestedAgent.trim() : "";
5181
+ if (requested.length === 0) return null;
5182
+ if (binding.agent !== null && requested === binding.agent) return null;
5183
+ 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)`;
5184
+ }
5185
+
5186
+ // src/swarm/successor-launch.ts
5187
+ var AGENT_LAUNCH_SHAPES = Object.freeze({
5188
+ claude: {
5189
+ bin: "claude",
5190
+ baseArgs: ["-p", "--permission-mode", "acceptEdits"],
5191
+ enforcesMaxTurns: true,
5192
+ maxTurnsFlag: "--max-turns",
5193
+ windowsShellSafe: true
5194
+ },
5195
+ codex: {
5196
+ bin: "codex",
5197
+ baseArgs: ["exec", "--json", "-c", 'approval_policy="never"', "--sandbox", "workspace-write", "--skip-git-repo-check"],
5198
+ enforcesMaxTurns: false,
5199
+ // `-` makes codex read the prompt from stdin (injection-safe), matching how
5200
+ // codex-runner.mjs already spawns it.
5201
+ trailingArgs: ["-"],
5202
+ // `approval_policy="never"` carries embedded quotes; cmd.exe re-parsing is
5203
+ // unverified, so win32 refuses rather than risking a mangled sandbox flag.
5204
+ windowsShellSafe: false
5205
+ }
5206
+ });
5207
+ function resolveSuccessorLaunch(input) {
5208
+ const agent = typeof input.agent === "string" ? input.agent.trim() : "";
5209
+ if (!agent) {
5210
+ return { ok: false, reason: "no agent bound for this spawn \u2014 refusing rather than defaulting to claude" };
5211
+ }
5212
+ const shape = AGENT_LAUNCH_SHAPES[agent];
5213
+ if (!shape) {
5214
+ const known = Object.keys(AGENT_LAUNCH_SHAPES).join(", ");
5215
+ return {
5216
+ ok: false,
5217
+ reason: `no known headless launch shape for agent '${agent}' (known: ${known}) \u2014 refusing rather than guessing its argv`
5218
+ };
5219
+ }
5220
+ const wantsMaxTurns = Number.isInteger(input.maxTurns) && input.maxTurns > 0;
5221
+ if (wantsMaxTurns && !shape.enforcesMaxTurns) {
5222
+ return {
5223
+ ok: false,
5224
+ reason: `agent '${agent}' cannot enforce a max_turns cap \u2014 refusing rather than spawning it unbounded`
5225
+ };
5226
+ }
5227
+ const platform = input.platform ?? process.platform;
5228
+ if (platform === "win32" && !shape.windowsShellSafe) {
5229
+ return {
5230
+ ok: false,
5231
+ 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`
5232
+ };
5233
+ }
5234
+ const args = [...shape.baseArgs];
5235
+ if (wantsMaxTurns && shape.maxTurnsFlag) {
5236
+ args.push(shape.maxTurnsFlag, String(input.maxTurns));
5237
+ }
5238
+ if (shape.trailingArgs) args.push(...shape.trailingArgs);
5239
+ return { ok: true, agent, bin: shape.bin, args };
5240
+ }
5241
+
5242
+ // src/swarm/spawn-ledger.ts
5243
+ import { mkdirSync as mkdirSync3, openSync, closeSync, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
4846
5244
  import { homedir as homedir4 } from "node:os";
4847
5245
  import { join as join6 } from "node:path";
4848
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, openSync, readFileSync as readFileSync6, readdirSync as readdirSync3, statSync as statSync3 } from "node:fs";
5246
+ var SWARM_LEDGER_DIR_ENV = "VO_SWARM_LEDGER_DIR";
5247
+ function resolveLedgerDir(env) {
5248
+ const override = env[SWARM_LEDGER_DIR_ENV];
5249
+ if (typeof override === "string" && override.trim().length > 0) return override.trim();
5250
+ return join6(homedir4(), ".vo", "swarm-ledger");
5251
+ }
5252
+ function sanitizeSwarmId(raw) {
5253
+ if (typeof raw !== "string") return null;
5254
+ const id = raw.trim();
5255
+ if (id.length === 0 || id.length > 128) return null;
5256
+ if (!/^[A-Za-z0-9._-]+$/u.test(id)) return null;
5257
+ if (id === "." || id === "..") return null;
5258
+ return id;
5259
+ }
5260
+ var CEILING_FILE = "ceiling.json";
5261
+ function createExclusive(path3, contents) {
5262
+ let fd;
5263
+ try {
5264
+ fd = openSync(path3, "wx");
5265
+ } catch {
5266
+ return false;
5267
+ }
5268
+ try {
5269
+ writeFileSync3(fd, contents, "utf8");
5270
+ } finally {
5271
+ closeSync(fd);
5272
+ }
5273
+ return true;
5274
+ }
5275
+ function capToCents(cap) {
5276
+ return isPositiveCap(cap) ? Math.round(cap * 100) : 0;
5277
+ }
5278
+ function readOrRecordLedgerHead(swarmDir, proposedCeiling, proposedCapCents, nowIso) {
5279
+ const path3 = join6(swarmDir, CEILING_FILE);
5280
+ const head = JSON.stringify({
5281
+ ceiling: proposedCeiling,
5282
+ cap_cents: proposedCapCents,
5283
+ recorded_at: nowIso
5284
+ });
5285
+ if (createExclusive(path3, head)) {
5286
+ return { ceiling: proposedCeiling, capCents: proposedCapCents };
5287
+ }
5288
+ let parsed;
5289
+ try {
5290
+ parsed = JSON.parse(readFileSync6(path3, "utf8"));
5291
+ } catch {
5292
+ return null;
5293
+ }
5294
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
5295
+ const record = parsed;
5296
+ const recorded = record["ceiling"];
5297
+ if (typeof recorded !== "number" || !Number.isFinite(recorded) || recorded < 1) return null;
5298
+ const recordedCap = record["cap_cents"];
5299
+ const capCents = typeof recordedCap === "number" && Number.isFinite(recordedCap) && recordedCap > 0 ? Math.floor(recordedCap) : 0;
5300
+ return { ceiling: Math.min(Math.floor(recorded), MAX_BOUND_SUBAGENTS), capCents };
5301
+ }
5302
+ var claimSpawnSlot = ({ swarmId, proposedCeiling, proposedCapUsd, dir, nowIso }) => {
5303
+ const id = sanitizeSwarmId(swarmId);
5304
+ if (id === null) {
5305
+ return {
5306
+ ok: false,
5307
+ 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`
5308
+ };
5309
+ }
5310
+ const proposed = Number.isFinite(proposedCeiling) ? Math.floor(proposedCeiling) : 0;
5311
+ if (proposed < 1) {
5312
+ return { ok: false, reason: `swarm '${id}' proposes a ceiling of ${proposed} \u2014 no allowance to claim` };
5313
+ }
5314
+ const swarmDir = join6(dir, id);
5315
+ try {
5316
+ mkdirSync3(swarmDir, { recursive: true });
5317
+ } catch (err) {
5318
+ return {
5319
+ ok: false,
5320
+ reason: `swarm '${id}' ledger directory is unwritable (${err instanceof Error ? err.message : String(err)}) \u2014 refusing rather than spawning uncounted`
5321
+ };
5322
+ }
5323
+ const wantedCents = capToCents(proposedCapUsd);
5324
+ const head = readOrRecordLedgerHead(swarmDir, Math.min(proposed, MAX_BOUND_SUBAGENTS), wantedCents, nowIso);
5325
+ if (head === null) {
5326
+ return { ok: false, reason: `swarm '${id}' ledger carries no readable ceiling \u2014 refusing rather than spawning uncounted` };
5327
+ }
5328
+ const { ceiling, capCents } = head;
5329
+ const shareCents = capCents > 0 ? Math.floor(capCents / ceiling) : 0;
5330
+ if (wantedCents > 0 && shareCents < 1) {
5331
+ return {
5332
+ ok: false,
5333
+ 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`
5334
+ };
5335
+ }
5336
+ for (let slot = 0; slot < ceiling; slot++) {
5337
+ const debitedCents = shareCents;
5338
+ const remainingCents = capCents > 0 ? capCents - (slot + 1) * shareCents : 0;
5339
+ const claimed = createExclusive(
5340
+ join6(swarmDir, `slot-${slot}.json`),
5341
+ JSON.stringify({
5342
+ slot,
5343
+ ceiling,
5344
+ pid: process.pid,
5345
+ claimed_at: nowIso,
5346
+ // The debit record. Durable and atomic with the claim: this file is
5347
+ // created with O_EXCL, so exactly one claimant ever writes this line.
5348
+ cap_cents_pool: capCents,
5349
+ cap_cents_debited: debitedCents,
5350
+ cap_cents_remaining: remainingCents
5351
+ })
5352
+ );
5353
+ if (claimed) {
5354
+ return {
5355
+ ok: true,
5356
+ slot,
5357
+ ceiling,
5358
+ remaining: ceiling - slot - 1,
5359
+ capUsd: debitedCents > 0 ? debitedCents / 100 : null,
5360
+ capRemainingUsd: capCents > 0 ? remainingCents / 100 : null
5361
+ };
5362
+ }
5363
+ }
5364
+ return {
5365
+ ok: false,
5366
+ reason: `swarm '${id}' has spent its whole fan-out ceiling (${ceiling} spawns across every generation) \u2014 refusing`
5367
+ };
5368
+ };
5369
+
5370
+ // src/tools/session/spawn-successor.ts
4849
5371
  var TOOL_NAME20 = "vo_spawn_successor";
4850
5372
  var MAX_HANDOFF_BYTES = 64e3;
4851
5373
  var inputSchema20 = {
@@ -4866,11 +5388,16 @@ var inputSchema20 = {
4866
5388
  max_turns: {
4867
5389
  type: "number",
4868
5390
  description: "Optional --max-turns bound for the successor."
5391
+ },
5392
+ agent: {
5393
+ type: "string",
5394
+ 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.`
4869
5395
  }
4870
5396
  },
4871
5397
  required: [],
4872
5398
  additionalProperties: false
4873
5399
  };
5400
+ var RETIRED_COUNTER_INPUT = "spawns_so_far";
4874
5401
  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.";
4875
5402
  function isToolInput20(v) {
4876
5403
  if (typeof v !== "object" || v === null) return false;
@@ -4879,12 +5406,18 @@ function isToolInput20(v) {
4879
5406
  if (o["goal"] !== void 0 && typeof o["goal"] !== "string") return false;
4880
5407
  if (o["cwd"] !== void 0 && typeof o["cwd"] !== "string") return false;
4881
5408
  if (o["max_turns"] !== void 0 && typeof o["max_turns"] !== "number") return false;
5409
+ if (o["agent"] !== void 0 && typeof o["agent"] !== "string") return false;
4882
5410
  return true;
4883
5411
  }
4884
- function newestHandoff(dir = join6(homedir4(), ".vo", "handoffs")) {
5412
+ function retiredCounterRefusal(v) {
5413
+ if (typeof v !== "object" || v === null) return null;
5414
+ if (!(RETIRED_COUNTER_INPUT in v)) return null;
5415
+ 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.`;
5416
+ }
5417
+ function newestHandoff(dir = join7(homedir5(), ".vo", "handoffs")) {
4885
5418
  try {
4886
- const entries = readdirSync3(dir).filter((f) => f.endsWith(".md")).map((f) => ({ f, m: statSync3(join6(dir, f)).mtimeMs })).sort((a, b) => b.m - a.m);
4887
- return entries.length > 0 && entries[0] ? join6(dir, entries[0].f) : null;
5419
+ const entries = readdirSync3(dir).filter((f) => f.endsWith(".md")).map((f) => ({ f, m: statSync3(join7(dir, f)).mtimeMs })).sort((a, b) => b.m - a.m);
5420
+ return entries.length > 0 && entries[0] ? join7(dir, entries[0].f) : null;
4888
5421
  } catch {
4889
5422
  return null;
4890
5423
  }
@@ -4928,9 +5461,87 @@ function buildSuccessorArgs(maxTurns) {
4928
5461
  }
4929
5462
  return args;
4930
5463
  }
5464
+ function resolveSpawnPlan(env, input, nowIso, platform = process.platform, claim = claimSpawnSlot) {
5465
+ const rawBinding = env[SWARM_TIER_BINDING_ENV];
5466
+ const hasBinding = typeof rawBinding === "string" && rawBinding.trim().length > 0;
5467
+ if (!hasBinding) {
5468
+ const explicit = input.agent?.trim();
5469
+ if (explicit) {
5470
+ const resolved2 = resolveSuccessorLaunch({ agent: explicit, maxTurns: input.max_turns, platform });
5471
+ if (!resolved2.ok) return { ok: false, reason: resolved2.reason, tier: "unbound" };
5472
+ return {
5473
+ ok: true,
5474
+ bin: resolved2.bin,
5475
+ args: resolved2.args,
5476
+ agent: resolved2.agent,
5477
+ tier: "unbound",
5478
+ bound: false,
5479
+ env: {},
5480
+ slot: null,
5481
+ capUsd: null,
5482
+ capRemainingUsd: null
5483
+ };
5484
+ }
5485
+ return {
5486
+ ok: true,
5487
+ bin: "claude",
5488
+ args: buildSuccessorArgs(input.max_turns),
5489
+ agent: "claude",
5490
+ tier: "unbound",
5491
+ bound: false,
5492
+ env: {},
5493
+ slot: null,
5494
+ capUsd: null,
5495
+ capRemainingUsd: null
5496
+ };
5497
+ }
5498
+ const binding = inheritSwarmTierBinding(env, nowIso);
5499
+ const admission = admitSubagentSpawn(binding);
5500
+ if (!admission.allowed) {
5501
+ return { ok: false, reason: admission.reason, tier: binding.tier };
5502
+ }
5503
+ const agentRefusal = agentBindingRefusal(binding, input.agent);
5504
+ if (agentRefusal !== null) return { ok: false, reason: agentRefusal, tier: binding.tier };
5505
+ const resolved = resolveSuccessorLaunch({
5506
+ agent: binding.agent,
5507
+ maxTurns: input.max_turns,
5508
+ platform
5509
+ });
5510
+ if (!resolved.ok) return { ok: false, reason: resolved.reason, tier: binding.tier };
5511
+ const slot = claim({
5512
+ swarmId: binding.swarm_id,
5513
+ proposedCeiling: binding.subagent_budget,
5514
+ // The spend-cap POOL, recorded once per swarm exactly like the ceiling. The
5515
+ // child's cap is DEBITED from it below, not recomputed from this binding.
5516
+ proposedCapUsd: binding.spend_cap_usd,
5517
+ dir: resolveLedgerDir(env),
5518
+ nowIso
5519
+ });
5520
+ if (!slot.ok) return { ok: false, reason: slot.reason, tier: binding.tier };
5521
+ return {
5522
+ ok: true,
5523
+ bin: resolved.bin,
5524
+ args: resolved.args,
5525
+ agent: resolved.agent,
5526
+ tier: binding.tier,
5527
+ bound: true,
5528
+ // Re-export the same TIER with a DECREMENTED budget and the spend cap the
5529
+ // ledger just DEBITED. Exporting the binding verbatim (what this did before
5530
+ // #9312) meant the child re-read the full budget and every generation
5531
+ // restarted at zero. Recomputing the cap from THIS binding (what #9312 did)
5532
+ // bounded a chain but not a tree: three siblings each re-halved the parent's
5533
+ // untouched $50 and walked away with $75 between them.
5534
+ env: childBindingEnvFragment(binding, slot.capUsd),
5535
+ slot: slot.slot,
5536
+ capUsd: slot.capUsd,
5537
+ capRemainingUsd: slot.capRemainingUsd
5538
+ };
5539
+ }
4931
5540
  async function handleSpawnSuccessor(_deps, rawInput, _signal, spawnImpl = spawn) {
5541
+ const retired = retiredCounterRefusal(rawInput);
5542
+ if (retired !== null) throw invalidParams(TOOL_NAME20, retired);
4932
5543
  if (!isToolInput20(rawInput)) {
4933
- throw invalidParams(TOOL_NAME20, "invalid input. Optional: { handoff_path, goal, cwd, max_turns }.");
5544
+ throw invalidParams(TOOL_NAME20, "invalid input. Optional: { handoff_path, goal, cwd, max_turns, agent }.");
4934
5545
  }
4935
5546
  const handoffPath = rawInput.handoff_path?.trim() || newestHandoff();
4936
5547
  if (!handoffPath || !existsSync4(handoffPath)) {
@@ -4943,20 +5554,37 @@ async function handleSpawnSuccessor(_deps, rawInput, _signal, spawnImpl = spawn)
4943
5554
  }
4944
5555
  });
4945
5556
  }
4946
- const handoff = readFileSync6(handoffPath, "utf8").slice(0, MAX_HANDOFF_BYTES);
5557
+ const handoff = readFileSync7(handoffPath, "utf8").slice(0, MAX_HANDOFF_BYTES);
4947
5558
  const prompt = buildSuccessorPrompt(handoff, rawInput.goal);
4948
- const logDir = process.env["VO_MCP_SUCCESSOR_LOG_DIR"]?.trim() || join6(homedir4(), ".vo", "successors");
4949
- mkdirSync3(logDir, { recursive: true });
4950
- const logPath = join6(logDir, `successor-${Date.now()}.log`);
4951
- const logFd = openSync(logPath, "a");
4952
- const child = spawnImpl("claude", buildSuccessorArgs(rawInput.max_turns), {
5559
+ const plan = resolveSpawnPlan(process.env, rawInput, (/* @__PURE__ */ new Date()).toISOString());
5560
+ if (!plan.ok) {
5561
+ return jsonContent({
5562
+ tool: TOOL_NAME20,
5563
+ schema_version: 1,
5564
+ payload: {
5565
+ spawned: false,
5566
+ reason: `swarm tier binding refused this spawn: ${plan.reason}`,
5567
+ tier: plan.tier,
5568
+ handoff_path: handoffPath
5569
+ }
5570
+ });
5571
+ }
5572
+ const logDir = process.env["VO_MCP_SUCCESSOR_LOG_DIR"]?.trim() || join7(homedir5(), ".vo", "successors");
5573
+ mkdirSync4(logDir, { recursive: true });
5574
+ const logPath = join7(logDir, `successor-${Date.now()}.log`);
5575
+ const logFd = openSync2(logPath, "a");
5576
+ const child = spawnImpl(plan.bin, [...plan.args], {
4953
5577
  cwd: rawInput.cwd?.trim() || process.cwd(),
4954
5578
  detached: true,
4955
5579
  stdio: ["pipe", logFd, logFd],
4956
- // Windows: `claude` is a .cmd shimneeds a shell to resolve. The prompt
4957
- // goes via STDIN below, never argv, so the shell never sees it.
5580
+ // Windows: the agent CLIs are .cmd shimsthey need a shell to resolve.
5581
+ // The prompt goes via STDIN below, never argv, so the shell never sees it.
4958
5582
  shell: process.platform === "win32",
4959
- windowsHide: true
5583
+ windowsHide: true,
5584
+ // Carry the SAME binding to the child. Without this the successor inherits
5585
+ // no tier and re-resolves its own — which is the split-payer defect one
5586
+ // generation down.
5587
+ ...plan.bound ? { env: { ...process.env, ...plan.env } } : {}
4960
5588
  });
4961
5589
  let spawnError = null;
4962
5590
  child.on("error", (e) => {
@@ -4972,7 +5600,20 @@ async function handleSpawnSuccessor(_deps, rawInput, _signal, spawnImpl = spawn)
4972
5600
  return jsonContent({
4973
5601
  tool: TOOL_NAME20,
4974
5602
  schema_version: 1,
4975
- payload: spawnError ? { spawned: false, reason: `spawn failed: ${spawnError}`, handoff_path: handoffPath } : { spawned: true, pid: child.pid ?? null, log_path: logPath, handoff_path: handoffPath }
5603
+ payload: spawnError ? { spawned: false, reason: `spawn failed: ${spawnError}`, agent: plan.agent, tier: plan.tier, handoff_path: handoffPath } : {
5604
+ spawned: true,
5605
+ pid: child.pid ?? null,
5606
+ log_path: logPath,
5607
+ handoff_path: handoffPath,
5608
+ agent: plan.agent,
5609
+ tier: plan.tier,
5610
+ tier_bound: plan.bound,
5611
+ ledger_slot: plan.slot,
5612
+ // The debit, surfaced so an operator can reconcile a fan-out's spend
5613
+ // against the pool without reading the ledger directory by hand.
5614
+ ledger_cap_usd: plan.capUsd,
5615
+ ledger_cap_remaining_usd: plan.capRemainingUsd
5616
+ }
4976
5617
  });
4977
5618
  }
4978
5619
 
@@ -5058,50 +5699,294 @@ async function handleConciergeDispatch(deps, rawInput, _signal) {
5058
5699
  }
5059
5700
 
5060
5701
  // src/tools/memory/sync-config.ts
5061
- import { homedir as homedir5 } from "node:os";
5062
- import { join as join7 } from "node:path";
5063
- import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync8, writeFileSync as writeFileSync3, readdirSync as readdirSync5 } from "node:fs";
5702
+ import { existsSync as existsSync8 } from "node:fs";
5703
+ import { homedir as homedir7 } from "node:os";
5704
+ import { join as join11 } from "node:path";
5705
+
5706
+ // src/tools/memory/memory-sync-http.ts
5064
5707
  init_safe_memory_file();
5065
- var TOOL_NAME22 = "vo_sync_config";
5066
- var inputSchema22 = {
5067
- type: "object",
5068
- properties: {
5069
- action: {
5070
- type: "string",
5071
- enum: ["pull", "push"],
5072
- description: "pull: download cloud memory to local files. push: upload local files to cloud."
5073
- },
5074
- cwd: {
5075
- type: "string",
5076
- description: "Working directory to derive project slug from (default: process.cwd())."
5708
+ import { existsSync as existsSync5, mkdirSync as mkdirSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync6, readdirSync as readdirSync4 } from "node:fs";
5709
+
5710
+ // src/tools/memory/sync-lock.ts
5711
+ import { closeSync as closeSync2, mkdirSync as mkdirSync5, openSync as openSync3, readFileSync as readFileSync9, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "node:fs";
5712
+ import { hostname } from "node:os";
5713
+ import { join as join8 } from "node:path";
5714
+ import { randomUUID as randomUUID2 } from "node:crypto";
5715
+
5716
+ // src/tools/memory/sync-lock-liveness.ts
5717
+ import { statSync as statSync4, readFileSync as readFileSync8 } from "node:fs";
5718
+ function defaultIsProcessAlive(pid) {
5719
+ try {
5720
+ process.kill(pid, 0);
5721
+ return true;
5722
+ } catch (err) {
5723
+ return err.code === "EPERM";
5724
+ }
5725
+ }
5726
+ function toPayload(parsed) {
5727
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
5728
+ const record = parsed;
5729
+ const token = record["token"];
5730
+ const host = record["hostname"];
5731
+ if (typeof token !== "string" || token.length === 0) return null;
5732
+ const pid = record["pid"];
5733
+ const acquiredAtMs = record["acquiredAtMs"];
5734
+ return {
5735
+ pid: typeof pid === "number" && Number.isInteger(pid) && pid > 0 ? pid : 0,
5736
+ hostname: typeof host === "string" ? host : "",
5737
+ sessionId: typeof record["sessionId"] === "string" ? record["sessionId"] : null,
5738
+ token,
5739
+ acquiredAt: typeof record["acquiredAt"] === "string" ? record["acquiredAt"] : "",
5740
+ acquiredAtMs: typeof acquiredAtMs === "number" && Number.isFinite(acquiredAtMs) ? acquiredAtMs : Number.NaN
5741
+ };
5742
+ }
5743
+ function readLockRecord(path3) {
5744
+ let raw;
5745
+ try {
5746
+ raw = readFileSync8(path3, "utf8");
5747
+ } catch {
5748
+ return null;
5749
+ }
5750
+ try {
5751
+ return { raw, payload: toPayload(JSON.parse(raw)) };
5752
+ } catch {
5753
+ return { raw, payload: null };
5754
+ }
5755
+ }
5756
+ function lockAgeMs(record, path3, nowMs) {
5757
+ let startedMs = Number.NaN;
5758
+ if (record.payload) {
5759
+ if (Number.isFinite(record.payload.acquiredAtMs)) {
5760
+ startedMs = record.payload.acquiredAtMs;
5761
+ } else if (record.payload.acquiredAt) {
5762
+ startedMs = Date.parse(record.payload.acquiredAt);
5077
5763
  }
5078
- },
5079
- required: ["action"],
5080
- additionalProperties: false
5081
- };
5082
- var description22 = "Syncs memory entries between local ~/.claude/projects/<slug>/memory/ and cloud control-plane /api/v1/agent-config/memory/me. Requires operator auth (vo-mcp login). Actions: pull (cloud\u2192local), push (local\u2192cloud). Idempotent; push creates/updates as needed.";
5083
- function isToolInput22(v) {
5084
- if (typeof v !== "object" || v === null) return false;
5085
- const o = v;
5086
- if (o["action"] !== "pull" && o["action"] !== "push") return false;
5087
- if (o["cwd"] !== void 0 && typeof o["cwd"] !== "string") return false;
5088
- return true;
5764
+ }
5765
+ if (!Number.isFinite(startedMs)) {
5766
+ try {
5767
+ startedMs = statSync4(path3).mtimeMs;
5768
+ } catch {
5769
+ return null;
5770
+ }
5771
+ }
5772
+ const age = nowMs - startedMs;
5773
+ return Number.isFinite(age) && age >= 0 ? age : null;
5089
5774
  }
5090
- function deriveProjectSlug(cwd) {
5091
- return cwd.replace(/([^:\\/])[\\/]+$/, "$1").replace(/\\/g, "/").replace(/^([a-zA-Z]):/, (_m, drive) => `${drive.toUpperCase()}:`).replace(/[^a-zA-Z0-9]/g, "-");
5775
+ function classifyHolderLiveness(record, isProcessAlive, thisHost) {
5776
+ const payload = record.payload;
5777
+ if (payload === null) return "unknown";
5778
+ if (payload.pid <= 0) return "unknown";
5779
+ if (thisHost.length === 0) return "unknown";
5780
+ if (payload.hostname !== thisHost) return "unknown";
5781
+ return isProcessAlive(payload.pid) ? "alive" : "dead";
5092
5782
  }
5093
- function getMemoryDir(cwd) {
5094
- const slug = deriveProjectSlug(cwd);
5095
- return join7(homedir5(), ".claude", "projects", slug, "memory");
5783
+ function isLockAbandoned(record, ageMs, ttlMs, isProcessAlive, thisHost) {
5784
+ const liveness = classifyHolderLiveness(record, isProcessAlive, thisHost);
5785
+ if (liveness === "alive") return false;
5786
+ if (liveness === "dead") return true;
5787
+ return ageMs !== null && ageMs > ttlMs;
5096
5788
  }
5789
+
5790
+ // src/tools/memory/sync-lock.ts
5791
+ var MEMORY_SYNC_LOCK_FILE = ".memory-sync.lock";
5792
+ var DEFAULT_LOCK_TTL_MS = 15 * 6e4;
5793
+ var DEFAULT_LOCK_WAIT_MS = 1e4;
5794
+ var INITIAL_BACKOFF_MS = 25;
5795
+ var MAX_BACKOFF_MS = 500;
5796
+ var BACKOFF_FACTOR = 1.6;
5797
+ function positiveOr(value, fallback) {
5798
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : fallback;
5799
+ }
5800
+ function createExclusive2(path3, contents) {
5801
+ let fd;
5802
+ try {
5803
+ fd = openSync3(path3, "wx");
5804
+ } catch (err) {
5805
+ const code = err.code;
5806
+ return { ok: false, exists: code === "EEXIST", message: err instanceof Error ? err.message : String(err) };
5807
+ }
5808
+ try {
5809
+ writeFileSync4(fd, contents, "utf8");
5810
+ } catch (err) {
5811
+ closeSync2(fd);
5812
+ try {
5813
+ unlinkSync2(path3);
5814
+ } catch {
5815
+ }
5816
+ return { ok: false, exists: false, message: err instanceof Error ? err.message : String(err) };
5817
+ }
5818
+ closeSync2(fd);
5819
+ return { ok: true };
5820
+ }
5821
+ function removeAbandoned(path3, expectedRaw) {
5822
+ let current;
5823
+ try {
5824
+ current = readFileSync9(path3, "utf8");
5825
+ } catch {
5826
+ return;
5827
+ }
5828
+ if (current !== expectedRaw) return;
5829
+ try {
5830
+ unlinkSync2(path3);
5831
+ } catch {
5832
+ }
5833
+ }
5834
+ function makeRelease(path3, token) {
5835
+ let released = false;
5836
+ return () => {
5837
+ if (released) return;
5838
+ released = true;
5839
+ let raw;
5840
+ try {
5841
+ raw = readFileSync9(path3, "utf8");
5842
+ } catch {
5843
+ return;
5844
+ }
5845
+ let stillOurs;
5846
+ try {
5847
+ stillOurs = toPayload(JSON.parse(raw))?.token === token;
5848
+ } catch {
5849
+ stillOurs = false;
5850
+ }
5851
+ if (!stillOurs) return;
5852
+ try {
5853
+ unlinkSync2(path3);
5854
+ } catch {
5855
+ }
5856
+ };
5857
+ }
5858
+ function describeHolder(record) {
5859
+ const payload = record?.payload;
5860
+ if (!payload) return "an unreadable lock file";
5861
+ return `pid ${payload.pid} on ${payload.hostname || "(unknown host)"} (session ${payload.sessionId ?? "unknown"}, held since ${payload.acquiredAt || "unknown"})`;
5862
+ }
5863
+ async function acquireMemorySyncLock(options) {
5864
+ const waitMs = positiveOr(options.waitMs, DEFAULT_LOCK_WAIT_MS);
5865
+ const ttlMs = positiveOr(options.ttlMs, DEFAULT_LOCK_TTL_MS);
5866
+ const now = options.now ?? Date.now;
5867
+ const sleep = options.sleep ?? ((ms) => new Promise((resolve3) => {
5868
+ setTimeout(resolve3, ms);
5869
+ }));
5870
+ const isProcessAlive = options.isProcessAlive ?? defaultIsProcessAlive;
5871
+ const thisHost = hostname();
5872
+ const path3 = join8(options.memoryDir, MEMORY_SYNC_LOCK_FILE);
5873
+ if (options.createDir === true) mkdirSync5(options.memoryDir, { recursive: true });
5874
+ const deadline = now() + waitMs;
5875
+ let backoffMs = INITIAL_BACKOFF_MS;
5876
+ let tookOverFrom = null;
5877
+ let holderDescription = "another session";
5878
+ for (; ; ) {
5879
+ const acquiredAtMs = now();
5880
+ const payload = {
5881
+ pid: process.pid,
5882
+ hostname: thisHost,
5883
+ sessionId: options.sessionId ?? null,
5884
+ token: randomUUID2(),
5885
+ acquiredAt: new Date(acquiredAtMs).toISOString(),
5886
+ acquiredAtMs
5887
+ };
5888
+ const created = createExclusive2(path3, `${JSON.stringify(payload, null, 2)}
5889
+ `);
5890
+ if (created.ok) {
5891
+ return { path: path3, payload, tookOverFrom, release: makeRelease(path3, payload.token) };
5892
+ }
5893
+ if (!created.exists) {
5894
+ throw new Error(
5895
+ `memory sync lock ${path3} could not be created (${created.message}) \u2014 refusing to sync without exclusion`
5896
+ );
5897
+ }
5898
+ const record = readLockRecord(path3);
5899
+ let reclaimed = false;
5900
+ if (record) {
5901
+ holderDescription = describeHolder(record);
5902
+ const age = lockAgeMs(record, path3, now());
5903
+ if (isLockAbandoned(record, age, ttlMs, isProcessAlive, thisHost)) {
5904
+ tookOverFrom = record.payload;
5905
+ removeAbandoned(path3, record.raw);
5906
+ reclaimed = true;
5907
+ }
5908
+ }
5909
+ if (now() >= deadline) {
5910
+ throw new Error(
5911
+ `memory sync lock ${path3} is held by ${holderDescription}; waited ${waitMs}ms \u2014 refusing to sync unlocked (concurrent memory writes corrupt the shared index). If that holder is provably gone, delete the lock file.`
5912
+ );
5913
+ }
5914
+ if (reclaimed) backoffMs = INITIAL_BACKOFF_MS;
5915
+ await sleep(Math.max(1, Math.min(backoffMs, deadline - now())));
5916
+ if (!reclaimed) backoffMs = Math.min(MAX_BACKOFF_MS, Math.ceil(backoffMs * BACKOFF_FACTOR));
5917
+ }
5918
+ }
5919
+ async function withMemorySyncLock(options, fn) {
5920
+ const handle = await acquireMemorySyncLock(options);
5921
+ try {
5922
+ return await fn(handle);
5923
+ } finally {
5924
+ handle.release();
5925
+ }
5926
+ }
5927
+
5928
+ // src/tools/memory/memory-index-merge.ts
5929
+ var MEMORY_INDEX_FILE = "MEMORY.md";
5930
+ function isMemoryIndexFile(fileName) {
5931
+ return fileName.toUpperCase() === MEMORY_INDEX_FILE.toUpperCase();
5932
+ }
5933
+ var INDEX_ROW_RE = /^\s*[-*]\s+\[[^\]]*\]\(([^)]+)\)/;
5934
+ function indexRowKey(line) {
5935
+ const match = INDEX_ROW_RE.exec(line);
5936
+ if (!match) return null;
5937
+ let target = match[1].trim();
5938
+ if (target.startsWith("<") && target.endsWith(">")) target = target.slice(1, -1).trim();
5939
+ target = target.replace(/\s+(["'])[\s\S]*\1$/, "").trim();
5940
+ if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(target)) {
5941
+ target = target.replace(/\\/g, "/").replace(/\/{2,}/g, "/");
5942
+ target = target.replace(/^(?:\.\/)+/, "");
5943
+ }
5944
+ return target.length > 0 ? target.toLowerCase() : null;
5945
+ }
5946
+ function mergeMemoryIndex(localContent, cloudContent) {
5947
+ if (typeof cloudContent !== "string" || cloudContent.trim().length === 0) {
5948
+ return { content: localContent, addedFromCloud: [] };
5949
+ }
5950
+ const eol = localContent.includes("\r\n") ? "\r\n" : "\n";
5951
+ const localLines = localContent.split(/\r?\n/);
5952
+ const localKeys = /* @__PURE__ */ new Set();
5953
+ let lastLocalRowIndex = -1;
5954
+ for (let i = 0; i < localLines.length; i++) {
5955
+ const key = indexRowKey(localLines[i]);
5956
+ if (key === null) continue;
5957
+ localKeys.add(key);
5958
+ lastLocalRowIndex = i;
5959
+ }
5960
+ const addedFromCloud = [];
5961
+ const seenCloudKeys = /* @__PURE__ */ new Set();
5962
+ for (const rawLine of cloudContent.split(/\r?\n/)) {
5963
+ const key = indexRowKey(rawLine);
5964
+ if (key === null) continue;
5965
+ if (localKeys.has(key) || seenCloudKeys.has(key)) continue;
5966
+ seenCloudKeys.add(key);
5967
+ addedFromCloud.push(rawLine.replace(/\r$/, ""));
5968
+ }
5969
+ if (addedFromCloud.length === 0) {
5970
+ return { content: localContent, addedFromCloud: [] };
5971
+ }
5972
+ const merged = lastLocalRowIndex >= 0 ? [...localLines.slice(0, lastLocalRowIndex + 1), ...addedFromCloud, ...localLines.slice(lastLocalRowIndex + 1)] : [...localLines, ...addedFromCloud];
5973
+ return { content: merged.join(eol), addedFromCloud };
5974
+ }
5975
+
5976
+ // src/tools/memory/memory-sync-http.ts
5977
+ init_bounded_sync();
5978
+ init_memory_push_cache();
5097
5979
  async function pullMemory(controlPlaneUrl, token, memoryDir, fetchFn) {
5098
5980
  const url = `${controlPlaneUrl}/api/v1/agent-config/memory/me`;
5099
- const response = await fetchFn(url, {
5100
- method: "GET",
5101
- headers: {
5102
- authorization: `Bearer ${token}`
5103
- }
5104
- });
5981
+ const response = await withRequestTimeout(
5982
+ url,
5983
+ () => fetchFn(url, {
5984
+ method: "GET",
5985
+ headers: {
5986
+ authorization: `Bearer ${token}`
5987
+ }
5988
+ })
5989
+ );
5105
5990
  if (response.status !== 200) {
5106
5991
  const text = await response.text();
5107
5992
  throw new Error(`GET /api/v1/agent-config/memory/me returned HTTP ${response.status}: ${text.slice(0, 200)}`);
@@ -5114,103 +5999,246 @@ async function pullMemory(controlPlaneUrl, token, memoryDir, fetchFn) {
5114
5999
  entry,
5115
6000
  filePath: resolveMemoryFilePath(memoryDir, entry.file_name)
5116
6001
  }));
5117
- mkdirSync4(memoryDir, { recursive: true });
6002
+ mkdirSync6(memoryDir, { recursive: true });
5118
6003
  const files = [];
5119
6004
  for (const { entry, filePath } of writes) {
5120
- writeFileSync3(filePath, entry.content, "utf8");
6005
+ writeFileSync6(filePath, entry.content, "utf8");
5121
6006
  files.push(entry.file_name);
5122
6007
  }
5123
6008
  return { pulled: data.entries.length, files };
5124
6009
  }
5125
- async function pushMemory(controlPlaneUrl, token, memoryDir, sessionId, fetchFn) {
5126
- if (!existsSync6(memoryDir)) {
5127
- return { pushed: 0, created: 0, updated: 0 };
6010
+ function listPushableFiles(memoryDir) {
6011
+ return readdirSync4(memoryDir).filter((f) => f.endsWith(".md") && f !== MEMORY_SYNC_LOCK_FILE);
6012
+ }
6013
+ async function uploadOne(item, controlPlaneUrl, token, sessionId, fetchFn, deadline) {
6014
+ deadline.check();
6015
+ if (item.memoryId !== null) {
6016
+ const updateUrl = `${controlPlaneUrl}/api/v1/agent-config/memory/${item.memoryId}`;
6017
+ const updateBody = { content: item.content, session_id: sessionId };
6018
+ const updateResponse = await withRequestTimeout(
6019
+ updateUrl,
6020
+ () => fetchFn(updateUrl, {
6021
+ method: "PUT",
6022
+ headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
6023
+ body: JSON.stringify(updateBody)
6024
+ })
6025
+ );
6026
+ if (updateResponse.status !== 200) {
6027
+ const text = await updateResponse.text();
6028
+ throw new Error(
6029
+ `PUT /api/v1/agent-config/memory/${item.memoryId} returned HTTP ${updateResponse.status}: ${text.slice(0, 200)}`
6030
+ );
6031
+ }
6032
+ const updateData = JSON.parse(await updateResponse.text());
6033
+ if (!updateData.ok) throw new Error(`PUT /api/v1/agent-config/memory/${item.memoryId} returned ok=false`);
6034
+ return "updated";
6035
+ }
6036
+ const createUrl = `${controlPlaneUrl}/api/v1/agent-config/memory/me`;
6037
+ const createBody = {
6038
+ entry_type: item.entryType,
6039
+ file_name: item.fileName,
6040
+ content: item.content,
6041
+ session_id: sessionId
6042
+ };
6043
+ const createResponse = await withRequestTimeout(
6044
+ createUrl,
6045
+ () => fetchFn(createUrl, {
6046
+ method: "POST",
6047
+ headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
6048
+ body: JSON.stringify(createBody)
6049
+ })
6050
+ );
6051
+ if (createResponse.status !== 200 && createResponse.status !== 201) {
6052
+ const text = await createResponse.text();
6053
+ throw new Error(
6054
+ `POST /api/v1/agent-config/memory/me returned HTTP ${createResponse.status}: ${text.slice(0, 200)}`
6055
+ );
6056
+ }
6057
+ const createData = JSON.parse(await createResponse.text());
6058
+ if (!createData.ok) throw new Error("POST /api/v1/agent-config/memory/me returned ok=false");
6059
+ return "created";
6060
+ }
6061
+ async function pushMemory(controlPlaneUrl, token, memoryDir, sessionId, fetchFn, options = {}) {
6062
+ const empty = { pushed: 0, created: 0, updated: 0, skipped: 0, indexRowsPreserved: 0 };
6063
+ if (!existsSync5(memoryDir)) {
6064
+ return empty;
5128
6065
  }
5129
- const localFiles = readdirSync5(memoryDir).filter((f) => f.endsWith(".md")).map((f) => ({
6066
+ const localFiles = listPushableFiles(memoryDir).map((f) => ({
5130
6067
  file_name: f,
5131
- content: readFileSync8(resolveMemoryFilePath(memoryDir, f), "utf8"),
5132
- entry_type: f === "MEMORY.md" ? "index" : "topic"
6068
+ content: readFileSync11(resolveMemoryFilePath(memoryDir, f), "utf8"),
6069
+ entry_type: isMemoryIndexFile(f) ? "index" : "topic"
5133
6070
  }));
5134
6071
  if (localFiles.length === 0) {
5135
- return { pushed: 0, created: 0, updated: 0 };
6072
+ return empty;
5136
6073
  }
6074
+ const deadline = options.deadline ?? createSyncDeadline();
6075
+ const cache = options.cache ?? readPushCache(memoryDir, controlPlaneUrl);
6076
+ deadline.check();
5137
6077
  const getUrl = `${controlPlaneUrl}/api/v1/agent-config/memory/me`;
5138
- const getResponse = await fetchFn(getUrl, {
5139
- method: "GET",
5140
- headers: {
5141
- authorization: `Bearer ${token}`
5142
- }
5143
- });
6078
+ const getResponse = await withRequestTimeout(
6079
+ getUrl,
6080
+ () => fetchFn(getUrl, { method: "GET", headers: { authorization: `Bearer ${token}` } })
6081
+ );
5144
6082
  const existingMap = /* @__PURE__ */ new Map();
5145
6083
  if (getResponse.status === 200) {
5146
6084
  const getData = JSON.parse(await getResponse.text());
5147
6085
  if (getData.ok && Array.isArray(getData.entries)) {
5148
6086
  for (const entry of getData.entries) {
5149
- existingMap.set(entry.file_name, entry.memory_id);
6087
+ existingMap.set(entry.file_name, {
6088
+ memoryId: entry.memory_id,
6089
+ content: typeof entry.content === "string" ? entry.content : ""
6090
+ });
5150
6091
  }
5151
6092
  }
5152
6093
  }
6094
+ const toUpload = [];
6095
+ let skipped = 0;
6096
+ for (const localFile of localFiles) {
6097
+ const existing = existingMap.get(localFile.file_name);
6098
+ let content = localFile.content;
6099
+ let rowsPreserved = 0;
6100
+ if (localFile.entry_type === "index") {
6101
+ const merged = mergeMemoryIndex(localFile.content, existing?.content);
6102
+ content = merged.content;
6103
+ rowsPreserved = merged.addedFromCloud.length;
6104
+ }
6105
+ const payloadHash = sha256(content);
6106
+ if (!needsMemoryPush(cache, localFile.file_name, payloadHash, existing !== void 0)) {
6107
+ skipped++;
6108
+ continue;
6109
+ }
6110
+ toUpload.push({
6111
+ fileName: localFile.file_name,
6112
+ entryType: localFile.entry_type,
6113
+ content,
6114
+ payloadHash,
6115
+ memoryId: existing?.memoryId ?? null,
6116
+ rowsPreserved
6117
+ });
6118
+ }
6119
+ const outcomes = await mapWithConcurrency(
6120
+ toUpload,
6121
+ options.concurrency ?? PUSH_CONCURRENCY,
6122
+ (item) => uploadOne(item, controlPlaneUrl, token, sessionId, fetchFn, deadline)
6123
+ );
5153
6124
  let created = 0;
5154
6125
  let updated = 0;
5155
- for (const localFile of localFiles) {
5156
- const memoryId = existingMap.get(localFile.file_name);
5157
- if (memoryId) {
5158
- const updateUrl = `${controlPlaneUrl}/api/v1/agent-config/memory/${memoryId}`;
5159
- const updateBody = {
5160
- content: localFile.content,
5161
- session_id: sessionId
5162
- };
5163
- const updateResponse = await fetchFn(updateUrl, {
5164
- method: "PUT",
5165
- headers: {
5166
- authorization: `Bearer ${token}`,
5167
- "content-type": "application/json"
5168
- },
5169
- body: JSON.stringify(updateBody)
5170
- });
5171
- if (updateResponse.status !== 200) {
5172
- const text = await updateResponse.text();
5173
- throw new Error(
5174
- `PUT /api/v1/agent-config/memory/${memoryId} returned HTTP ${updateResponse.status}: ${text.slice(0, 200)}`
5175
- );
5176
- }
5177
- const updateData = JSON.parse(await updateResponse.text());
5178
- if (!updateData.ok) {
5179
- throw new Error(`PUT /api/v1/agent-config/memory/${memoryId} returned ok=false`);
5180
- }
5181
- updated++;
5182
- } else {
5183
- const createUrl = `${controlPlaneUrl}/api/v1/agent-config/memory/me`;
5184
- const createBody = {
5185
- entry_type: localFile.entry_type,
5186
- file_name: localFile.file_name,
5187
- content: localFile.content,
5188
- session_id: sessionId
5189
- };
5190
- const createResponse = await fetchFn(createUrl, {
5191
- method: "POST",
5192
- headers: {
5193
- authorization: `Bearer ${token}`,
5194
- "content-type": "application/json"
5195
- },
5196
- body: JSON.stringify(createBody)
5197
- });
5198
- if (createResponse.status !== 200 && createResponse.status !== 201) {
5199
- const text = await createResponse.text();
5200
- throw new Error(
5201
- `POST /api/v1/agent-config/memory/me returned HTTP ${createResponse.status}: ${text.slice(0, 200)}`
5202
- );
5203
- }
5204
- const createData = JSON.parse(await createResponse.text());
5205
- if (!createData.ok) {
5206
- throw new Error("POST /api/v1/agent-config/memory/me returned ok=false");
5207
- }
5208
- created++;
6126
+ let indexRowsPreserved = 0;
6127
+ let firstError;
6128
+ for (let i = 0; i < outcomes.length; i++) {
6129
+ const outcome = outcomes[i];
6130
+ const item = toUpload[i];
6131
+ if (outcome.ok) {
6132
+ if (outcome.value === "created") created++;
6133
+ else updated++;
6134
+ indexRowsPreserved += item.rowsPreserved;
6135
+ recordMemoryPush(cache, item.fileName, item.payloadHash);
6136
+ } else if (firstError === void 0) {
6137
+ firstError = outcome.error;
6138
+ }
6139
+ }
6140
+ pruneMissing(cache, localFiles.map((f) => f.file_name));
6141
+ if (options.persistCache ?? options.cache === void 0) writePushCache(memoryDir, cache);
6142
+ if (firstError !== void 0) throw firstError;
6143
+ return { pushed: created + updated, created, updated, skipped, indexRowsPreserved };
6144
+ }
6145
+
6146
+ // src/tools/memory/sync-config.ts
6147
+ init_bounded_sync();
6148
+ init_memory_push_cache();
6149
+
6150
+ // src/tools/memory/sync-kill-switch.ts
6151
+ import { existsSync as existsSync6, readFileSync as readFileSync12 } from "node:fs";
6152
+ import { homedir as homedir6 } from "node:os";
6153
+ import { join as join10 } from "node:path";
6154
+ var MEMORY_SYNC_DISABLE_ENV = "VO_MCP_DISABLE_MEMORY_SYNC";
6155
+ var MEMORY_SYNC_DISABLE_SENTINEL = "vo-memory-sync-disabled";
6156
+ var NEGATIONS = /* @__PURE__ */ new Set(["0", "false", "no"]);
6157
+ var MAX_LOGGED_VALUE = 32;
6158
+ function memorySyncSentinelPath(home) {
6159
+ return join10(home, ".claude", MEMORY_SYNC_DISABLE_SENTINEL);
6160
+ }
6161
+ function isKillSwitchValueOn(raw) {
6162
+ if (raw === void 0 || raw === null) return false;
6163
+ const v = raw.trim().toLowerCase();
6164
+ if (v === "") return false;
6165
+ return !NEGATIONS.has(v);
6166
+ }
6167
+ function clip(raw) {
6168
+ const v = raw.trim();
6169
+ return v.length > MAX_LOGGED_VALUE ? `${v.slice(0, MAX_LOGGED_VALUE)}\u2026` : v;
6170
+ }
6171
+ function evaluateMemorySyncKillSwitch(deps = {}) {
6172
+ const env = deps.env ?? process.env;
6173
+ const home = deps.home ?? homedir6();
6174
+ const fileExists = deps.fileExists ?? existsSync6;
6175
+ const readFile3 = deps.readFile ?? ((p) => readFileSync12(p, "utf8"));
6176
+ const fired = [];
6177
+ const rawEnv = env[MEMORY_SYNC_DISABLE_ENV];
6178
+ if (isKillSwitchValueOn(rawEnv)) {
6179
+ fired.push(`env ${MEMORY_SYNC_DISABLE_ENV}=${clip(rawEnv)}`);
6180
+ }
6181
+ const sentinel = memorySyncSentinelPath(home);
6182
+ let sentinelPresent;
6183
+ try {
6184
+ sentinelPresent = fileExists(sentinel);
6185
+ } catch {
6186
+ sentinelPresent = false;
6187
+ }
6188
+ if (sentinelPresent) {
6189
+ let contents = "";
6190
+ let readable = true;
6191
+ try {
6192
+ contents = readFile3(sentinel);
6193
+ } catch {
6194
+ readable = false;
6195
+ }
6196
+ if (!readable || isKillSwitchValueOn(contents) || contents.trim() === "") {
6197
+ fired.push(`sentinel file ${sentinel}`);
5209
6198
  }
5210
6199
  }
5211
- return { pushed: localFiles.length, created, updated };
6200
+ if (fired.length === 0) return { disabled: false, reason: null };
6201
+ return { disabled: true, reason: `memory sync DISABLED by ${fired.join(" + ")}` };
5212
6202
  }
5213
- async function runMemorySync(action, cwd, sessionId, fetchFn = globalThis.fetch) {
6203
+
6204
+ // src/tools/memory/sync-config.ts
6205
+ var TOOL_NAME22 = "vo_sync_config";
6206
+ var inputSchema22 = {
6207
+ type: "object",
6208
+ properties: {
6209
+ action: {
6210
+ type: "string",
6211
+ enum: ["pull", "push"],
6212
+ description: "pull: download cloud memory to local files. push: upload local files to cloud."
6213
+ },
6214
+ cwd: {
6215
+ type: "string",
6216
+ description: "Working directory to derive project slug from (default: process.cwd())."
6217
+ }
6218
+ },
6219
+ required: ["action"],
6220
+ additionalProperties: false
6221
+ };
6222
+ var description22 = "Syncs memory entries between local ~/.claude/projects/<slug>/memory/ and cloud control-plane /api/v1/agent-config/memory/me. Requires operator auth (vo-mcp login). Actions: pull (cloud\u2192local), push (local\u2192cloud). Idempotent; push creates/updates as needed. Serialized across concurrent sessions by an exclusive lock in the memory dir.";
6223
+ function isToolInput22(v) {
6224
+ if (typeof v !== "object" || v === null) return false;
6225
+ const o = v;
6226
+ if (o["action"] !== "pull" && o["action"] !== "push") return false;
6227
+ if (o["cwd"] !== void 0 && typeof o["cwd"] !== "string") return false;
6228
+ return true;
6229
+ }
6230
+ function deriveProjectSlug(cwd) {
6231
+ return cwd.replace(/([^:\\/])[\\/]+$/, "$1").replace(/\\/g, "/").replace(/^([a-zA-Z]):/, (_m, drive) => `${drive.toUpperCase()}:`).replace(/[^a-zA-Z0-9]/g, "-");
6232
+ }
6233
+ function getMemoryDir(cwd) {
6234
+ const slug = deriveProjectSlug(cwd);
6235
+ return join11(homedir7(), ".claude", "projects", slug, "memory");
6236
+ }
6237
+ async function runMemorySync(action, cwd, sessionId, fetchFn = globalThis.fetch, lockOptions = {}) {
6238
+ const killSwitch = evaluateMemorySyncKillSwitch();
6239
+ if (killSwitch.disabled) {
6240
+ return { synced: false, reason: killSwitch.reason };
6241
+ }
5214
6242
  const controlPlaneUrl = process.env["VO_CONTROL_PLANE_URL"];
5215
6243
  if (!controlPlaneUrl) {
5216
6244
  return { synced: false, reason: "VO_CONTROL_PLANE_URL not set \u2014 cloud mode disabled" };
@@ -5227,39 +6255,79 @@ async function runMemorySync(action, cwd, sessionId, fetchFn = globalThis.fetch)
5227
6255
  }
5228
6256
  const memoryDir = getMemoryDir(cwd);
5229
6257
  const baseUrl = controlPlaneUrl.replace(/\/+$/, "");
5230
- try {
5231
- if (action === "pull") {
5232
- const result2 = await pullMemory(baseUrl, token, memoryDir, fetchFn);
5233
- return { synced: true, action: "pull", pulled: result2.pulled, files: result2.files, memory_dir: memoryDir };
5234
- }
5235
- const result = await pushMemory(baseUrl, token, memoryDir, sessionId, fetchFn);
5236
- let bridge = { upserted: 0, failed: 0, failures: [] };
5237
- try {
5238
- const { upsertMemoryFilesAsKnowledge: upsertMemoryFilesAsKnowledge2 } = await Promise.resolve().then(() => (init_memory_knowledge_bridge(), memory_knowledge_bridge_exports));
5239
- bridge = await upsertMemoryFilesAsKnowledge2({
5240
- controlPlaneUrl: baseUrl,
5241
- token,
5242
- memoryDir,
5243
- fetchFn
5244
- });
5245
- } catch (err) {
5246
- bridge = {
5247
- upserted: 0,
5248
- failed: 1,
5249
- failures: [`bridge unavailable: ${err instanceof Error ? err.message : String(err)}`]
5250
- };
5251
- }
6258
+ if (action === "push" && !existsSync8(memoryDir)) {
5252
6259
  return {
5253
6260
  synced: true,
5254
6261
  action: "push",
5255
- pushed: result.pushed,
5256
- created: result.created,
5257
- updated: result.updated,
5258
- memory_dir: memoryDir,
5259
- knowledge_upserted: bridge.upserted,
5260
- knowledge_failed: bridge.failed,
5261
- ...bridge.failed > 0 ? { knowledge_failures: bridge.failures } : {}
6262
+ pushed: 0,
6263
+ created: 0,
6264
+ updated: 0,
6265
+ skipped: 0,
6266
+ index_rows_preserved: 0,
6267
+ knowledge_upserted: 0,
6268
+ knowledge_failed: 0,
6269
+ memory_dir: memoryDir
5262
6270
  };
6271
+ }
6272
+ try {
6273
+ return await withMemorySyncLock({ ...lockOptions, memoryDir, sessionId, createDir: action === "pull" }, async (lock) => {
6274
+ const takeover = lock.tookOverFrom ? { lock_taken_over_from_pid: lock.tookOverFrom.pid } : {};
6275
+ if (action === "pull") {
6276
+ const result2 = await pullMemory(baseUrl, token, memoryDir, fetchFn);
6277
+ return {
6278
+ synced: true,
6279
+ action: "pull",
6280
+ pulled: result2.pulled,
6281
+ files: result2.files,
6282
+ memory_dir: memoryDir,
6283
+ ...takeover
6284
+ };
6285
+ }
6286
+ const deadline = createSyncDeadline();
6287
+ const cache = readPushCache(memoryDir, baseUrl);
6288
+ let result;
6289
+ try {
6290
+ result = await pushMemory(baseUrl, token, memoryDir, sessionId, fetchFn, { cache, deadline });
6291
+ } catch (err) {
6292
+ writePushCache(memoryDir, cache);
6293
+ throw err;
6294
+ }
6295
+ let bridge;
6296
+ try {
6297
+ const { upsertMemoryFilesAsKnowledge: upsertMemoryFilesAsKnowledge2 } = await Promise.resolve().then(() => (init_memory_knowledge_bridge(), memory_knowledge_bridge_exports));
6298
+ bridge = await upsertMemoryFilesAsKnowledge2({
6299
+ controlPlaneUrl: baseUrl,
6300
+ token,
6301
+ memoryDir,
6302
+ fetchFn,
6303
+ cache,
6304
+ deadline
6305
+ });
6306
+ } catch (err) {
6307
+ bridge = {
6308
+ upserted: 0,
6309
+ failed: 1,
6310
+ skipped: 0,
6311
+ failures: [`bridge unavailable: ${err instanceof Error ? err.message : String(err)}`]
6312
+ };
6313
+ }
6314
+ writePushCache(memoryDir, cache);
6315
+ return {
6316
+ synced: true,
6317
+ action: "push",
6318
+ pushed: result.pushed,
6319
+ created: result.created,
6320
+ updated: result.updated,
6321
+ skipped: result.skipped,
6322
+ index_rows_preserved: result.indexRowsPreserved,
6323
+ memory_dir: memoryDir,
6324
+ knowledge_upserted: bridge.upserted,
6325
+ knowledge_failed: bridge.failed,
6326
+ knowledge_skipped: bridge.skipped,
6327
+ ...bridge.failed > 0 ? { knowledge_failures: bridge.failures } : {},
6328
+ ...takeover
6329
+ };
6330
+ });
5263
6331
  } catch (err) {
5264
6332
  const message = err instanceof Error ? err.message : String(err);
5265
6333
  return { synced: false, reason: `Sync failed: ${message}` };
@@ -5546,12 +6614,12 @@ async function handleHqWhiteboardRead(_deps, rawInput, signal) {
5546
6614
  }
5547
6615
 
5548
6616
  // src/tools/skills/skill-corpus.ts
5549
- import { existsSync as existsSync7, statSync as statSync5 } from "node:fs";
5550
- import { dirname as dirname5, isAbsolute, join as join9, resolve as resolve2 } from "node:path";
6617
+ import { existsSync as existsSync9, statSync as statSync6 } from "node:fs";
6618
+ import { dirname as dirname5, isAbsolute, join as join13, resolve as resolve2 } from "node:path";
5551
6619
 
5552
6620
  // ../skill-registry/src/loader.ts
5553
- import { readdirSync as readdirSync6, readFileSync as readFileSync9, statSync as statSync4 } from "node:fs";
5554
- import { join as join8 } from "node:path";
6621
+ import { readdirSync as readdirSync6, readFileSync as readFileSync14, statSync as statSync5 } from "node:fs";
6622
+ import { join as join12 } from "node:path";
5555
6623
  var InvalidSkillFrontmatterError = class extends Error {
5556
6624
  constructor(skillFile, reason) {
5557
6625
  super(`Invalid frontmatter in ${skillFile}: ${reason}`);
@@ -5604,18 +6672,18 @@ function loadSkillsFromDir(skillsDir) {
5604
6672
  const entries = readdirSync6(skillsDir);
5605
6673
  const skills = [];
5606
6674
  for (const entry of entries) {
5607
- const entryPath = join8(skillsDir, entry);
6675
+ const entryPath = join12(skillsDir, entry);
5608
6676
  let stat;
5609
6677
  try {
5610
- stat = statSync4(entryPath);
6678
+ stat = statSync5(entryPath);
5611
6679
  } catch {
5612
6680
  continue;
5613
6681
  }
5614
6682
  if (!stat.isDirectory()) continue;
5615
- const skillFile = join8(entryPath, "SKILL.md");
6683
+ const skillFile = join12(entryPath, "SKILL.md");
5616
6684
  let raw;
5617
6685
  try {
5618
- raw = readFileSync9(skillFile, "utf8");
6686
+ raw = readFileSync14(skillFile, "utf8");
5619
6687
  } catch {
5620
6688
  continue;
5621
6689
  }
@@ -5656,12 +6724,12 @@ function resolveSkillsDir(env = process.env, startDir = process.cwd()) {
5656
6724
  const override = env.VO_SKILLS_DIR;
5657
6725
  if (typeof override === "string" && override.length > 0) {
5658
6726
  const abs = isAbsolute(override) ? override : resolve2(startDir, override);
5659
- return existsSync7(abs) && statSync5(abs).isDirectory() ? abs : null;
6727
+ return existsSync9(abs) && statSync6(abs).isDirectory() ? abs : null;
5660
6728
  }
5661
6729
  let dir = resolve2(startDir);
5662
6730
  for (let i = 0; i < MAX_WALK_UP_LEVELS; i += 1) {
5663
- const candidate = join9(dir, ".claude", "skills");
5664
- if (existsSync7(candidate) && statSync5(candidate).isDirectory()) return candidate;
6731
+ const candidate = join13(dir, ".claude", "skills");
6732
+ if (existsSync9(candidate) && statSync6(candidate).isDirectory()) return candidate;
5665
6733
  const parent = dirname5(dir);
5666
6734
  if (parent === dir) break;
5667
6735
  dir = parent;
@@ -5972,7 +7040,7 @@ function buildToolRegistry() {
5972
7040
  };
5973
7041
  }
5974
7042
  function createServer(options) {
5975
- const sessionId = options.sessionId ?? randomUUID2();
7043
+ const sessionId = options.sessionId ?? randomUUID3();
5976
7044
  const mode = createLocalMode();
5977
7045
  const now = options.now ?? (() => /* @__PURE__ */ new Date());
5978
7046
  const server = new Server(
@@ -6024,8 +7092,8 @@ function listToolNames() {
6024
7092
  }
6025
7093
 
6026
7094
  // src/cache/sqlite-cache.ts
6027
- import { createHash as createHash3 } from "node:crypto";
6028
- import { chmodSync as chmodSync3, mkdirSync as mkdirSync5 } from "node:fs";
7095
+ import { createHash as createHash4 } from "node:crypto";
7096
+ import { chmodSync as chmodSync3, mkdirSync as mkdirSync7 } from "node:fs";
6029
7097
  import { dirname as dirname6 } from "node:path";
6030
7098
  import { DatabaseSync } from "node:sqlite";
6031
7099
 
@@ -6071,7 +7139,7 @@ function normalizeString(s) {
6071
7139
  function createSqliteCache(options) {
6072
7140
  const fileBacked = options.dbPath !== ":memory:";
6073
7141
  if (fileBacked) {
6074
- mkdirSync5(dirname6(options.dbPath), { recursive: true, mode: 448 });
7142
+ mkdirSync7(dirname6(options.dbPath), { recursive: true, mode: 448 });
6075
7143
  }
6076
7144
  const versionNamespace = options.cacheVersionNamespace ?? "";
6077
7145
  const db = new DatabaseSync(options.dbPath);
@@ -6102,7 +7170,7 @@ function createSqliteCache(options) {
6102
7170
  return {
6103
7171
  keyFor(toolName, input, opts) {
6104
7172
  const canonical = canonicalize(input, opts);
6105
- const hash = createHash3("sha256");
7173
+ const hash = createHash4("sha256");
6106
7174
  if (versionNamespace.length > 0) {
6107
7175
  hash.update(versionNamespace);
6108
7176
  hash.update("|");
@@ -6194,7 +7262,7 @@ function createStubRatchetClient() {
6194
7262
  let m;
6195
7263
  while ((m = pat.regex.exec(req.source)) !== null) {
6196
7264
  findings.push({
6197
- line_excerpt: clip(m[0], 80),
7265
+ line_excerpt: clip2(m[0], 80),
6198
7266
  severity: pat.severity,
6199
7267
  code: pat.code,
6200
7268
  message: pat.message
@@ -6226,7 +7294,7 @@ function createStubRatchetClient() {
6226
7294
  }
6227
7295
  };
6228
7296
  }
6229
- function clip(s, n) {
7297
+ function clip2(s, n) {
6230
7298
  return s.length <= n ? s : s.slice(0, n) + "\u2026";
6231
7299
  }
6232
7300
  function buildSummary2(args) {
@@ -6255,7 +7323,7 @@ function createNullConsensusEngineClient(reason = NULL_CLIENT_DEFAULT_REASON) {
6255
7323
  }
6256
7324
 
6257
7325
  // src/consensus/engine-client.ts
6258
- import { randomUUID as randomUUID3 } from "node:crypto";
7326
+ import { randomUUID as randomUUID4 } from "node:crypto";
6259
7327
 
6260
7328
  // src/consensus/meta-model-caller.ts
6261
7329
  var META_CONSENSUS_MODEL = "muse-spark-1.1";