@algosuite/vo-mcp 0.2.0-beta.29 → 0.2.0-beta.33

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 readFileSync8 } 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 = readFileSync8(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,
@@ -1339,6 +1546,42 @@ function toEventPerModelVerdicts(src) {
1339
1546
  };
1340
1547
  });
1341
1548
  }
1549
+ function aggregateEventTokenUsage(src, engineUsage) {
1550
+ if (engineUsage !== void 0) {
1551
+ const hasIn = Object.keys(engineUsage.per_model_tokens_in).length > 0;
1552
+ const hasOut = Object.keys(engineUsage.per_model_tokens_out).length > 0;
1553
+ return {
1554
+ per_model_tokens_in: hasIn ? engineUsage.per_model_tokens_in : null,
1555
+ per_model_tokens_out: hasOut ? engineUsage.per_model_tokens_out : null,
1556
+ total_cost_usd: engineUsage.cost_micro_usd === null ? null : engineUsage.cost_micro_usd / 1e6
1557
+ };
1558
+ }
1559
+ const tokensIn = {};
1560
+ const tokensOut = {};
1561
+ let anyTokensIn = false;
1562
+ let anyTokensOut = false;
1563
+ let costMicroUsd = 0;
1564
+ let anyCost = false;
1565
+ for (const v of src) {
1566
+ if (typeof v.input_tokens === "number") {
1567
+ tokensIn[v.model] = (tokensIn[v.model] ?? 0) + v.input_tokens;
1568
+ anyTokensIn = true;
1569
+ }
1570
+ if (typeof v.output_tokens === "number") {
1571
+ tokensOut[v.model] = (tokensOut[v.model] ?? 0) + v.output_tokens;
1572
+ anyTokensOut = true;
1573
+ }
1574
+ if (typeof v.cost_micro_usd === "number") {
1575
+ costMicroUsd += v.cost_micro_usd;
1576
+ anyCost = true;
1577
+ }
1578
+ }
1579
+ return {
1580
+ per_model_tokens_in: anyTokensIn ? tokensIn : null,
1581
+ per_model_tokens_out: anyTokensOut ? tokensOut : null,
1582
+ total_cost_usd: anyCost ? costMicroUsd / 1e6 : null
1583
+ };
1584
+ }
1342
1585
  function toEventSynthesizedVerdict(src) {
1343
1586
  return {
1344
1587
  verdict: src.verdict,
@@ -1710,7 +1953,8 @@ async function handleCheckHollowTest(deps, rawInput, signal) {
1710
1953
  synthesized_verdict: synthForEvent,
1711
1954
  consensus_confidence: engineResult.synthesized_verdict.confidence,
1712
1955
  duration_ms: engineResult.duration_ms,
1713
- consensus_engine_version: engineResult.engine_version
1956
+ consensus_engine_version: engineResult.engine_version,
1957
+ ...aggregateEventTokenUsage(engineResult.per_model_verdicts, engineResult.token_usage)
1714
1958
  };
1715
1959
  const payload = {
1716
1960
  verdict: engineResult.synthesized_verdict.verdict,
@@ -1892,7 +2136,8 @@ async function handleVerifyAnswer(deps, rawInput, signal) {
1892
2136
  synthesized_verdict: synthForEvent,
1893
2137
  consensus_confidence: engineResult.synthesized_verdict.confidence,
1894
2138
  duration_ms: engineResult.duration_ms,
1895
- consensus_engine_version: engineResult.engine_version
2139
+ consensus_engine_version: engineResult.engine_version,
2140
+ ...aggregateEventTokenUsage(engineResult.per_model_verdicts, engineResult.token_usage)
1896
2141
  };
1897
2142
  const payload = {
1898
2143
  verdict: engineResult.synthesized_verdict.verdict,
@@ -1901,6 +2146,7 @@ async function handleVerifyAnswer(deps, rawInput, signal) {
1901
2146
  synthesized_verdict: synthForEvent,
1902
2147
  engine_version: engineResult.engine_version,
1903
2148
  degraded: engineResult.degraded,
2149
+ ...engineResult.quorum_failed === true ? { quorum_failed: true } : {},
1904
2150
  gate_type: gateType,
1905
2151
  ...kbResult.error !== null ? { kb_unavailable: true } : {},
1906
2152
  ...kbTruncated > 0 ? { kb_rules_truncated: kbTruncated } : {}
@@ -2154,7 +2400,8 @@ async function handleConsensusJudgment(deps, rawInput, signal) {
2154
2400
  duration_ms: engineResult.duration_ms,
2155
2401
  consensus_engine_version: engineResult.engine_version,
2156
2402
  per_model_verdicts: perModelForEvent,
2157
- synthesized_verdict: synthForEvent
2403
+ synthesized_verdict: synthForEvent,
2404
+ ...aggregateEventTokenUsage(engineResult.per_model_verdicts, engineResult.token_usage)
2158
2405
  };
2159
2406
  const payload = {
2160
2407
  verdict: engineResult.synthesized_verdict.verdict,
@@ -2163,6 +2410,7 @@ async function handleConsensusJudgment(deps, rawInput, signal) {
2163
2410
  synthesized_verdict: synthForEvent,
2164
2411
  engine_version: engineResult.engine_version,
2165
2412
  degraded: engineResult.degraded,
2413
+ ...engineResult.quorum_failed === true ? { quorum_failed: true } : {},
2166
2414
  gate_type: gateType,
2167
2415
  // ─── Consensus-engine feature outputs (additive; 2026-06-13) ─────────────
2168
2416
  // Feature 2 (calibrated-confidence) — ON by default; the engine attaches
@@ -2361,7 +2609,8 @@ async function handleArchitectureReview(deps, rawInput, signal) {
2361
2609
  synthesized_verdict: synthForEvent,
2362
2610
  consensus_confidence: engineResult.synthesized_verdict.confidence,
2363
2611
  duration_ms: engineResult.duration_ms,
2364
- consensus_engine_version: engineResult.engine_version
2612
+ consensus_engine_version: engineResult.engine_version,
2613
+ ...aggregateEventTokenUsage(engineResult.per_model_verdicts, engineResult.token_usage)
2365
2614
  };
2366
2615
  const escalationRequired = engineResult.escalation_required === true || engineResult.escalation_required === void 0 && engineResult.synthesized_verdict.dissent_summary !== null;
2367
2616
  const escalationReason = engineResult.escalation_reason ?? engineResult.synthesized_verdict.dissent_summary ?? "";
@@ -3846,7 +4095,8 @@ Produce the JSON dispatch plan now.`;
3846
4095
  duration_ms: engineResult.duration_ms,
3847
4096
  consensus_engine_version: engineResult.engine_version,
3848
4097
  per_model_verdicts: toEventPerModelVerdicts(engineResult.per_model_verdicts),
3849
- synthesized_verdict: toEventSynthesizedVerdict(engineResult.synthesized_verdict)
4098
+ synthesized_verdict: toEventSynthesizedVerdict(engineResult.synthesized_verdict),
4099
+ ...aggregateEventTokenUsage(engineResult.per_model_verdicts, engineResult.token_usage)
3850
4100
  };
3851
4101
  deps.events.append(enrichedEvent);
3852
4102
  return jsonContent(envelope);
@@ -4588,7 +4838,8 @@ async function handleReviewMerge(deps, rawInput, signal) {
4588
4838
  duration_ms: result.duration_ms,
4589
4839
  consensus_engine_version: result.engine_version,
4590
4840
  per_model_verdicts: perModel,
4591
- synthesized_verdict: synth
4841
+ synthesized_verdict: synth,
4842
+ ...aggregateEventTokenUsage(result.per_model_verdicts, result.token_usage)
4592
4843
  });
4593
4844
  }
4594
4845
 
@@ -5492,50 +5743,294 @@ async function handleConciergeDispatch(deps, rawInput, _signal) {
5492
5743
  }
5493
5744
 
5494
5745
  // src/tools/memory/sync-config.ts
5495
- import { homedir as homedir6 } from "node:os";
5496
- import { join as join8 } from "node:path";
5497
- import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync4, readdirSync as readdirSync5 } from "node:fs";
5746
+ import { existsSync as existsSync8 } from "node:fs";
5747
+ import { homedir as homedir7 } from "node:os";
5748
+ import { join as join11 } from "node:path";
5749
+
5750
+ // src/tools/memory/memory-sync-http.ts
5498
5751
  init_safe_memory_file();
5499
- var TOOL_NAME22 = "vo_sync_config";
5500
- var inputSchema22 = {
5501
- type: "object",
5502
- properties: {
5503
- action: {
5504
- type: "string",
5505
- enum: ["pull", "push"],
5506
- description: "pull: download cloud memory to local files. push: upload local files to cloud."
5507
- },
5508
- cwd: {
5509
- type: "string",
5510
- description: "Working directory to derive project slug from (default: process.cwd())."
5752
+ import { existsSync as existsSync5, mkdirSync as mkdirSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync6, readdirSync as readdirSync4 } from "node:fs";
5753
+
5754
+ // src/tools/memory/sync-lock.ts
5755
+ import { closeSync as closeSync2, mkdirSync as mkdirSync5, openSync as openSync3, readFileSync as readFileSync9, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "node:fs";
5756
+ import { hostname } from "node:os";
5757
+ import { join as join8 } from "node:path";
5758
+ import { randomUUID as randomUUID2 } from "node:crypto";
5759
+
5760
+ // src/tools/memory/sync-lock-liveness.ts
5761
+ import { statSync as statSync4, readFileSync as readFileSync8 } from "node:fs";
5762
+ function defaultIsProcessAlive(pid) {
5763
+ try {
5764
+ process.kill(pid, 0);
5765
+ return true;
5766
+ } catch (err) {
5767
+ return err.code === "EPERM";
5768
+ }
5769
+ }
5770
+ function toPayload(parsed) {
5771
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
5772
+ const record = parsed;
5773
+ const token = record["token"];
5774
+ const host = record["hostname"];
5775
+ if (typeof token !== "string" || token.length === 0) return null;
5776
+ const pid = record["pid"];
5777
+ const acquiredAtMs = record["acquiredAtMs"];
5778
+ return {
5779
+ pid: typeof pid === "number" && Number.isInteger(pid) && pid > 0 ? pid : 0,
5780
+ hostname: typeof host === "string" ? host : "",
5781
+ sessionId: typeof record["sessionId"] === "string" ? record["sessionId"] : null,
5782
+ token,
5783
+ acquiredAt: typeof record["acquiredAt"] === "string" ? record["acquiredAt"] : "",
5784
+ acquiredAtMs: typeof acquiredAtMs === "number" && Number.isFinite(acquiredAtMs) ? acquiredAtMs : Number.NaN
5785
+ };
5786
+ }
5787
+ function readLockRecord(path3) {
5788
+ let raw;
5789
+ try {
5790
+ raw = readFileSync8(path3, "utf8");
5791
+ } catch {
5792
+ return null;
5793
+ }
5794
+ try {
5795
+ return { raw, payload: toPayload(JSON.parse(raw)) };
5796
+ } catch {
5797
+ return { raw, payload: null };
5798
+ }
5799
+ }
5800
+ function lockAgeMs(record, path3, nowMs) {
5801
+ let startedMs = Number.NaN;
5802
+ if (record.payload) {
5803
+ if (Number.isFinite(record.payload.acquiredAtMs)) {
5804
+ startedMs = record.payload.acquiredAtMs;
5805
+ } else if (record.payload.acquiredAt) {
5806
+ startedMs = Date.parse(record.payload.acquiredAt);
5511
5807
  }
5512
- },
5513
- required: ["action"],
5514
- additionalProperties: false
5515
- };
5516
- 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.";
5517
- function isToolInput22(v) {
5518
- if (typeof v !== "object" || v === null) return false;
5519
- const o = v;
5520
- if (o["action"] !== "pull" && o["action"] !== "push") return false;
5521
- if (o["cwd"] !== void 0 && typeof o["cwd"] !== "string") return false;
5522
- return true;
5808
+ }
5809
+ if (!Number.isFinite(startedMs)) {
5810
+ try {
5811
+ startedMs = statSync4(path3).mtimeMs;
5812
+ } catch {
5813
+ return null;
5814
+ }
5815
+ }
5816
+ const age = nowMs - startedMs;
5817
+ return Number.isFinite(age) && age >= 0 ? age : null;
5523
5818
  }
5524
- function deriveProjectSlug(cwd) {
5525
- return cwd.replace(/([^:\\/])[\\/]+$/, "$1").replace(/\\/g, "/").replace(/^([a-zA-Z]):/, (_m, drive) => `${drive.toUpperCase()}:`).replace(/[^a-zA-Z0-9]/g, "-");
5819
+ function classifyHolderLiveness(record, isProcessAlive, thisHost) {
5820
+ const payload = record.payload;
5821
+ if (payload === null) return "unknown";
5822
+ if (payload.pid <= 0) return "unknown";
5823
+ if (thisHost.length === 0) return "unknown";
5824
+ if (payload.hostname !== thisHost) return "unknown";
5825
+ return isProcessAlive(payload.pid) ? "alive" : "dead";
5526
5826
  }
5527
- function getMemoryDir(cwd) {
5528
- const slug = deriveProjectSlug(cwd);
5529
- return join8(homedir6(), ".claude", "projects", slug, "memory");
5827
+ function isLockAbandoned(record, ageMs, ttlMs, isProcessAlive, thisHost) {
5828
+ const liveness = classifyHolderLiveness(record, isProcessAlive, thisHost);
5829
+ if (liveness === "alive") return false;
5830
+ if (liveness === "dead") return true;
5831
+ return ageMs !== null && ageMs > ttlMs;
5530
5832
  }
5833
+
5834
+ // src/tools/memory/sync-lock.ts
5835
+ var MEMORY_SYNC_LOCK_FILE = ".memory-sync.lock";
5836
+ var DEFAULT_LOCK_TTL_MS = 15 * 6e4;
5837
+ var DEFAULT_LOCK_WAIT_MS = 1e4;
5838
+ var INITIAL_BACKOFF_MS = 25;
5839
+ var MAX_BACKOFF_MS = 500;
5840
+ var BACKOFF_FACTOR = 1.6;
5841
+ function positiveOr(value, fallback) {
5842
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : fallback;
5843
+ }
5844
+ function createExclusive2(path3, contents) {
5845
+ let fd;
5846
+ try {
5847
+ fd = openSync3(path3, "wx");
5848
+ } catch (err) {
5849
+ const code = err.code;
5850
+ return { ok: false, exists: code === "EEXIST", message: err instanceof Error ? err.message : String(err) };
5851
+ }
5852
+ try {
5853
+ writeFileSync4(fd, contents, "utf8");
5854
+ } catch (err) {
5855
+ closeSync2(fd);
5856
+ try {
5857
+ unlinkSync2(path3);
5858
+ } catch {
5859
+ }
5860
+ return { ok: false, exists: false, message: err instanceof Error ? err.message : String(err) };
5861
+ }
5862
+ closeSync2(fd);
5863
+ return { ok: true };
5864
+ }
5865
+ function removeAbandoned(path3, expectedRaw) {
5866
+ let current;
5867
+ try {
5868
+ current = readFileSync9(path3, "utf8");
5869
+ } catch {
5870
+ return;
5871
+ }
5872
+ if (current !== expectedRaw) return;
5873
+ try {
5874
+ unlinkSync2(path3);
5875
+ } catch {
5876
+ }
5877
+ }
5878
+ function makeRelease(path3, token) {
5879
+ let released = false;
5880
+ return () => {
5881
+ if (released) return;
5882
+ released = true;
5883
+ let raw;
5884
+ try {
5885
+ raw = readFileSync9(path3, "utf8");
5886
+ } catch {
5887
+ return;
5888
+ }
5889
+ let stillOurs;
5890
+ try {
5891
+ stillOurs = toPayload(JSON.parse(raw))?.token === token;
5892
+ } catch {
5893
+ stillOurs = false;
5894
+ }
5895
+ if (!stillOurs) return;
5896
+ try {
5897
+ unlinkSync2(path3);
5898
+ } catch {
5899
+ }
5900
+ };
5901
+ }
5902
+ function describeHolder(record) {
5903
+ const payload = record?.payload;
5904
+ if (!payload) return "an unreadable lock file";
5905
+ return `pid ${payload.pid} on ${payload.hostname || "(unknown host)"} (session ${payload.sessionId ?? "unknown"}, held since ${payload.acquiredAt || "unknown"})`;
5906
+ }
5907
+ async function acquireMemorySyncLock(options) {
5908
+ const waitMs = positiveOr(options.waitMs, DEFAULT_LOCK_WAIT_MS);
5909
+ const ttlMs = positiveOr(options.ttlMs, DEFAULT_LOCK_TTL_MS);
5910
+ const now = options.now ?? Date.now;
5911
+ const sleep = options.sleep ?? ((ms) => new Promise((resolve3) => {
5912
+ setTimeout(resolve3, ms);
5913
+ }));
5914
+ const isProcessAlive = options.isProcessAlive ?? defaultIsProcessAlive;
5915
+ const thisHost = hostname();
5916
+ const path3 = join8(options.memoryDir, MEMORY_SYNC_LOCK_FILE);
5917
+ if (options.createDir === true) mkdirSync5(options.memoryDir, { recursive: true });
5918
+ const deadline = now() + waitMs;
5919
+ let backoffMs = INITIAL_BACKOFF_MS;
5920
+ let tookOverFrom = null;
5921
+ let holderDescription = "another session";
5922
+ for (; ; ) {
5923
+ const acquiredAtMs = now();
5924
+ const payload = {
5925
+ pid: process.pid,
5926
+ hostname: thisHost,
5927
+ sessionId: options.sessionId ?? null,
5928
+ token: randomUUID2(),
5929
+ acquiredAt: new Date(acquiredAtMs).toISOString(),
5930
+ acquiredAtMs
5931
+ };
5932
+ const created = createExclusive2(path3, `${JSON.stringify(payload, null, 2)}
5933
+ `);
5934
+ if (created.ok) {
5935
+ return { path: path3, payload, tookOverFrom, release: makeRelease(path3, payload.token) };
5936
+ }
5937
+ if (!created.exists) {
5938
+ throw new Error(
5939
+ `memory sync lock ${path3} could not be created (${created.message}) \u2014 refusing to sync without exclusion`
5940
+ );
5941
+ }
5942
+ const record = readLockRecord(path3);
5943
+ let reclaimed = false;
5944
+ if (record) {
5945
+ holderDescription = describeHolder(record);
5946
+ const age = lockAgeMs(record, path3, now());
5947
+ if (isLockAbandoned(record, age, ttlMs, isProcessAlive, thisHost)) {
5948
+ tookOverFrom = record.payload;
5949
+ removeAbandoned(path3, record.raw);
5950
+ reclaimed = true;
5951
+ }
5952
+ }
5953
+ if (now() >= deadline) {
5954
+ throw new Error(
5955
+ `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.`
5956
+ );
5957
+ }
5958
+ if (reclaimed) backoffMs = INITIAL_BACKOFF_MS;
5959
+ await sleep(Math.max(1, Math.min(backoffMs, deadline - now())));
5960
+ if (!reclaimed) backoffMs = Math.min(MAX_BACKOFF_MS, Math.ceil(backoffMs * BACKOFF_FACTOR));
5961
+ }
5962
+ }
5963
+ async function withMemorySyncLock(options, fn) {
5964
+ const handle = await acquireMemorySyncLock(options);
5965
+ try {
5966
+ return await fn(handle);
5967
+ } finally {
5968
+ handle.release();
5969
+ }
5970
+ }
5971
+
5972
+ // src/tools/memory/memory-index-merge.ts
5973
+ var MEMORY_INDEX_FILE = "MEMORY.md";
5974
+ function isMemoryIndexFile(fileName) {
5975
+ return fileName.toUpperCase() === MEMORY_INDEX_FILE.toUpperCase();
5976
+ }
5977
+ var INDEX_ROW_RE = /^\s*[-*]\s+\[[^\]]*\]\(([^)]+)\)/;
5978
+ function indexRowKey(line) {
5979
+ const match = INDEX_ROW_RE.exec(line);
5980
+ if (!match) return null;
5981
+ let target = match[1].trim();
5982
+ if (target.startsWith("<") && target.endsWith(">")) target = target.slice(1, -1).trim();
5983
+ target = target.replace(/\s+(["'])[\s\S]*\1$/, "").trim();
5984
+ if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(target)) {
5985
+ target = target.replace(/\\/g, "/").replace(/\/{2,}/g, "/");
5986
+ target = target.replace(/^(?:\.\/)+/, "");
5987
+ }
5988
+ return target.length > 0 ? target.toLowerCase() : null;
5989
+ }
5990
+ function mergeMemoryIndex(localContent, cloudContent) {
5991
+ if (typeof cloudContent !== "string" || cloudContent.trim().length === 0) {
5992
+ return { content: localContent, addedFromCloud: [] };
5993
+ }
5994
+ const eol = localContent.includes("\r\n") ? "\r\n" : "\n";
5995
+ const localLines = localContent.split(/\r?\n/);
5996
+ const localKeys = /* @__PURE__ */ new Set();
5997
+ let lastLocalRowIndex = -1;
5998
+ for (let i = 0; i < localLines.length; i++) {
5999
+ const key = indexRowKey(localLines[i]);
6000
+ if (key === null) continue;
6001
+ localKeys.add(key);
6002
+ lastLocalRowIndex = i;
6003
+ }
6004
+ const addedFromCloud = [];
6005
+ const seenCloudKeys = /* @__PURE__ */ new Set();
6006
+ for (const rawLine of cloudContent.split(/\r?\n/)) {
6007
+ const key = indexRowKey(rawLine);
6008
+ if (key === null) continue;
6009
+ if (localKeys.has(key) || seenCloudKeys.has(key)) continue;
6010
+ seenCloudKeys.add(key);
6011
+ addedFromCloud.push(rawLine.replace(/\r$/, ""));
6012
+ }
6013
+ if (addedFromCloud.length === 0) {
6014
+ return { content: localContent, addedFromCloud: [] };
6015
+ }
6016
+ const merged = lastLocalRowIndex >= 0 ? [...localLines.slice(0, lastLocalRowIndex + 1), ...addedFromCloud, ...localLines.slice(lastLocalRowIndex + 1)] : [...localLines, ...addedFromCloud];
6017
+ return { content: merged.join(eol), addedFromCloud };
6018
+ }
6019
+
6020
+ // src/tools/memory/memory-sync-http.ts
6021
+ init_bounded_sync();
6022
+ init_memory_push_cache();
5531
6023
  async function pullMemory(controlPlaneUrl, token, memoryDir, fetchFn) {
5532
6024
  const url = `${controlPlaneUrl}/api/v1/agent-config/memory/me`;
5533
- const response = await fetchFn(url, {
5534
- method: "GET",
5535
- headers: {
5536
- authorization: `Bearer ${token}`
5537
- }
5538
- });
6025
+ const response = await withRequestTimeout(
6026
+ url,
6027
+ () => fetchFn(url, {
6028
+ method: "GET",
6029
+ headers: {
6030
+ authorization: `Bearer ${token}`
6031
+ }
6032
+ })
6033
+ );
5539
6034
  if (response.status !== 200) {
5540
6035
  const text = await response.text();
5541
6036
  throw new Error(`GET /api/v1/agent-config/memory/me returned HTTP ${response.status}: ${text.slice(0, 200)}`);
@@ -5548,103 +6043,246 @@ async function pullMemory(controlPlaneUrl, token, memoryDir, fetchFn) {
5548
6043
  entry,
5549
6044
  filePath: resolveMemoryFilePath(memoryDir, entry.file_name)
5550
6045
  }));
5551
- mkdirSync5(memoryDir, { recursive: true });
6046
+ mkdirSync6(memoryDir, { recursive: true });
5552
6047
  const files = [];
5553
6048
  for (const { entry, filePath } of writes) {
5554
- writeFileSync4(filePath, entry.content, "utf8");
6049
+ writeFileSync6(filePath, entry.content, "utf8");
5555
6050
  files.push(entry.file_name);
5556
6051
  }
5557
6052
  return { pulled: data.entries.length, files };
5558
6053
  }
5559
- async function pushMemory(controlPlaneUrl, token, memoryDir, sessionId, fetchFn) {
5560
- if (!existsSync6(memoryDir)) {
5561
- return { pushed: 0, created: 0, updated: 0 };
6054
+ function listPushableFiles(memoryDir) {
6055
+ return readdirSync4(memoryDir).filter((f) => f.endsWith(".md") && f !== MEMORY_SYNC_LOCK_FILE);
6056
+ }
6057
+ async function uploadOne(item, controlPlaneUrl, token, sessionId, fetchFn, deadline) {
6058
+ deadline.check();
6059
+ if (item.memoryId !== null) {
6060
+ const updateUrl = `${controlPlaneUrl}/api/v1/agent-config/memory/${item.memoryId}`;
6061
+ const updateBody = { content: item.content, session_id: sessionId };
6062
+ const updateResponse = await withRequestTimeout(
6063
+ updateUrl,
6064
+ () => fetchFn(updateUrl, {
6065
+ method: "PUT",
6066
+ headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
6067
+ body: JSON.stringify(updateBody)
6068
+ })
6069
+ );
6070
+ if (updateResponse.status !== 200) {
6071
+ const text = await updateResponse.text();
6072
+ throw new Error(
6073
+ `PUT /api/v1/agent-config/memory/${item.memoryId} returned HTTP ${updateResponse.status}: ${text.slice(0, 200)}`
6074
+ );
6075
+ }
6076
+ const updateData = JSON.parse(await updateResponse.text());
6077
+ if (!updateData.ok) throw new Error(`PUT /api/v1/agent-config/memory/${item.memoryId} returned ok=false`);
6078
+ return "updated";
6079
+ }
6080
+ const createUrl = `${controlPlaneUrl}/api/v1/agent-config/memory/me`;
6081
+ const createBody = {
6082
+ entry_type: item.entryType,
6083
+ file_name: item.fileName,
6084
+ content: item.content,
6085
+ session_id: sessionId
6086
+ };
6087
+ const createResponse = await withRequestTimeout(
6088
+ createUrl,
6089
+ () => fetchFn(createUrl, {
6090
+ method: "POST",
6091
+ headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
6092
+ body: JSON.stringify(createBody)
6093
+ })
6094
+ );
6095
+ if (createResponse.status !== 200 && createResponse.status !== 201) {
6096
+ const text = await createResponse.text();
6097
+ throw new Error(
6098
+ `POST /api/v1/agent-config/memory/me returned HTTP ${createResponse.status}: ${text.slice(0, 200)}`
6099
+ );
6100
+ }
6101
+ const createData = JSON.parse(await createResponse.text());
6102
+ if (!createData.ok) throw new Error("POST /api/v1/agent-config/memory/me returned ok=false");
6103
+ return "created";
6104
+ }
6105
+ async function pushMemory(controlPlaneUrl, token, memoryDir, sessionId, fetchFn, options = {}) {
6106
+ const empty = { pushed: 0, created: 0, updated: 0, skipped: 0, indexRowsPreserved: 0 };
6107
+ if (!existsSync5(memoryDir)) {
6108
+ return empty;
5562
6109
  }
5563
- const localFiles = readdirSync5(memoryDir).filter((f) => f.endsWith(".md")).map((f) => ({
6110
+ const localFiles = listPushableFiles(memoryDir).map((f) => ({
5564
6111
  file_name: f,
5565
- content: readFileSync9(resolveMemoryFilePath(memoryDir, f), "utf8"),
5566
- entry_type: f === "MEMORY.md" ? "index" : "topic"
6112
+ content: readFileSync11(resolveMemoryFilePath(memoryDir, f), "utf8"),
6113
+ entry_type: isMemoryIndexFile(f) ? "index" : "topic"
5567
6114
  }));
5568
6115
  if (localFiles.length === 0) {
5569
- return { pushed: 0, created: 0, updated: 0 };
6116
+ return empty;
5570
6117
  }
6118
+ const deadline = options.deadline ?? createSyncDeadline();
6119
+ const cache = options.cache ?? readPushCache(memoryDir, controlPlaneUrl);
6120
+ deadline.check();
5571
6121
  const getUrl = `${controlPlaneUrl}/api/v1/agent-config/memory/me`;
5572
- const getResponse = await fetchFn(getUrl, {
5573
- method: "GET",
5574
- headers: {
5575
- authorization: `Bearer ${token}`
5576
- }
5577
- });
6122
+ const getResponse = await withRequestTimeout(
6123
+ getUrl,
6124
+ () => fetchFn(getUrl, { method: "GET", headers: { authorization: `Bearer ${token}` } })
6125
+ );
5578
6126
  const existingMap = /* @__PURE__ */ new Map();
5579
6127
  if (getResponse.status === 200) {
5580
6128
  const getData = JSON.parse(await getResponse.text());
5581
6129
  if (getData.ok && Array.isArray(getData.entries)) {
5582
6130
  for (const entry of getData.entries) {
5583
- existingMap.set(entry.file_name, entry.memory_id);
6131
+ existingMap.set(entry.file_name, {
6132
+ memoryId: entry.memory_id,
6133
+ content: typeof entry.content === "string" ? entry.content : ""
6134
+ });
5584
6135
  }
5585
6136
  }
5586
6137
  }
6138
+ const toUpload = [];
6139
+ let skipped = 0;
6140
+ for (const localFile of localFiles) {
6141
+ const existing = existingMap.get(localFile.file_name);
6142
+ let content = localFile.content;
6143
+ let rowsPreserved = 0;
6144
+ if (localFile.entry_type === "index") {
6145
+ const merged = mergeMemoryIndex(localFile.content, existing?.content);
6146
+ content = merged.content;
6147
+ rowsPreserved = merged.addedFromCloud.length;
6148
+ }
6149
+ const payloadHash = sha256(content);
6150
+ if (!needsMemoryPush(cache, localFile.file_name, payloadHash, existing !== void 0)) {
6151
+ skipped++;
6152
+ continue;
6153
+ }
6154
+ toUpload.push({
6155
+ fileName: localFile.file_name,
6156
+ entryType: localFile.entry_type,
6157
+ content,
6158
+ payloadHash,
6159
+ memoryId: existing?.memoryId ?? null,
6160
+ rowsPreserved
6161
+ });
6162
+ }
6163
+ const outcomes = await mapWithConcurrency(
6164
+ toUpload,
6165
+ options.concurrency ?? PUSH_CONCURRENCY,
6166
+ (item) => uploadOne(item, controlPlaneUrl, token, sessionId, fetchFn, deadline)
6167
+ );
5587
6168
  let created = 0;
5588
6169
  let updated = 0;
5589
- for (const localFile of localFiles) {
5590
- const memoryId = existingMap.get(localFile.file_name);
5591
- if (memoryId) {
5592
- const updateUrl = `${controlPlaneUrl}/api/v1/agent-config/memory/${memoryId}`;
5593
- const updateBody = {
5594
- content: localFile.content,
5595
- session_id: sessionId
5596
- };
5597
- const updateResponse = await fetchFn(updateUrl, {
5598
- method: "PUT",
5599
- headers: {
5600
- authorization: `Bearer ${token}`,
5601
- "content-type": "application/json"
5602
- },
5603
- body: JSON.stringify(updateBody)
5604
- });
5605
- if (updateResponse.status !== 200) {
5606
- const text = await updateResponse.text();
5607
- throw new Error(
5608
- `PUT /api/v1/agent-config/memory/${memoryId} returned HTTP ${updateResponse.status}: ${text.slice(0, 200)}`
5609
- );
5610
- }
5611
- const updateData = JSON.parse(await updateResponse.text());
5612
- if (!updateData.ok) {
5613
- throw new Error(`PUT /api/v1/agent-config/memory/${memoryId} returned ok=false`);
5614
- }
5615
- updated++;
5616
- } else {
5617
- const createUrl = `${controlPlaneUrl}/api/v1/agent-config/memory/me`;
5618
- const createBody = {
5619
- entry_type: localFile.entry_type,
5620
- file_name: localFile.file_name,
5621
- content: localFile.content,
5622
- session_id: sessionId
5623
- };
5624
- const createResponse = await fetchFn(createUrl, {
5625
- method: "POST",
5626
- headers: {
5627
- authorization: `Bearer ${token}`,
5628
- "content-type": "application/json"
5629
- },
5630
- body: JSON.stringify(createBody)
5631
- });
5632
- if (createResponse.status !== 200 && createResponse.status !== 201) {
5633
- const text = await createResponse.text();
5634
- throw new Error(
5635
- `POST /api/v1/agent-config/memory/me returned HTTP ${createResponse.status}: ${text.slice(0, 200)}`
5636
- );
5637
- }
5638
- const createData = JSON.parse(await createResponse.text());
5639
- if (!createData.ok) {
5640
- throw new Error("POST /api/v1/agent-config/memory/me returned ok=false");
5641
- }
5642
- created++;
6170
+ let indexRowsPreserved = 0;
6171
+ let firstError;
6172
+ for (let i = 0; i < outcomes.length; i++) {
6173
+ const outcome = outcomes[i];
6174
+ const item = toUpload[i];
6175
+ if (outcome.ok) {
6176
+ if (outcome.value === "created") created++;
6177
+ else updated++;
6178
+ indexRowsPreserved += item.rowsPreserved;
6179
+ recordMemoryPush(cache, item.fileName, item.payloadHash);
6180
+ } else if (firstError === void 0) {
6181
+ firstError = outcome.error;
6182
+ }
6183
+ }
6184
+ pruneMissing(cache, localFiles.map((f) => f.file_name));
6185
+ if (options.persistCache ?? options.cache === void 0) writePushCache(memoryDir, cache);
6186
+ if (firstError !== void 0) throw firstError;
6187
+ return { pushed: created + updated, created, updated, skipped, indexRowsPreserved };
6188
+ }
6189
+
6190
+ // src/tools/memory/sync-config.ts
6191
+ init_bounded_sync();
6192
+ init_memory_push_cache();
6193
+
6194
+ // src/tools/memory/sync-kill-switch.ts
6195
+ import { existsSync as existsSync6, readFileSync as readFileSync12 } from "node:fs";
6196
+ import { homedir as homedir6 } from "node:os";
6197
+ import { join as join10 } from "node:path";
6198
+ var MEMORY_SYNC_DISABLE_ENV = "VO_MCP_DISABLE_MEMORY_SYNC";
6199
+ var MEMORY_SYNC_DISABLE_SENTINEL = "vo-memory-sync-disabled";
6200
+ var NEGATIONS = /* @__PURE__ */ new Set(["0", "false", "no"]);
6201
+ var MAX_LOGGED_VALUE = 32;
6202
+ function memorySyncSentinelPath(home) {
6203
+ return join10(home, ".claude", MEMORY_SYNC_DISABLE_SENTINEL);
6204
+ }
6205
+ function isKillSwitchValueOn(raw) {
6206
+ if (raw === void 0 || raw === null) return false;
6207
+ const v = raw.trim().toLowerCase();
6208
+ if (v === "") return false;
6209
+ return !NEGATIONS.has(v);
6210
+ }
6211
+ function clip(raw) {
6212
+ const v = raw.trim();
6213
+ return v.length > MAX_LOGGED_VALUE ? `${v.slice(0, MAX_LOGGED_VALUE)}\u2026` : v;
6214
+ }
6215
+ function evaluateMemorySyncKillSwitch(deps = {}) {
6216
+ const env = deps.env ?? process.env;
6217
+ const home = deps.home ?? homedir6();
6218
+ const fileExists = deps.fileExists ?? existsSync6;
6219
+ const readFile3 = deps.readFile ?? ((p) => readFileSync12(p, "utf8"));
6220
+ const fired = [];
6221
+ const rawEnv = env[MEMORY_SYNC_DISABLE_ENV];
6222
+ if (isKillSwitchValueOn(rawEnv)) {
6223
+ fired.push(`env ${MEMORY_SYNC_DISABLE_ENV}=${clip(rawEnv)}`);
6224
+ }
6225
+ const sentinel = memorySyncSentinelPath(home);
6226
+ let sentinelPresent;
6227
+ try {
6228
+ sentinelPresent = fileExists(sentinel);
6229
+ } catch {
6230
+ sentinelPresent = false;
6231
+ }
6232
+ if (sentinelPresent) {
6233
+ let contents = "";
6234
+ let readable = true;
6235
+ try {
6236
+ contents = readFile3(sentinel);
6237
+ } catch {
6238
+ readable = false;
6239
+ }
6240
+ if (!readable || isKillSwitchValueOn(contents) || contents.trim() === "") {
6241
+ fired.push(`sentinel file ${sentinel}`);
5643
6242
  }
5644
6243
  }
5645
- return { pushed: localFiles.length, created, updated };
6244
+ if (fired.length === 0) return { disabled: false, reason: null };
6245
+ return { disabled: true, reason: `memory sync DISABLED by ${fired.join(" + ")}` };
5646
6246
  }
5647
- async function runMemorySync(action, cwd, sessionId, fetchFn = globalThis.fetch) {
6247
+
6248
+ // src/tools/memory/sync-config.ts
6249
+ var TOOL_NAME22 = "vo_sync_config";
6250
+ var inputSchema22 = {
6251
+ type: "object",
6252
+ properties: {
6253
+ action: {
6254
+ type: "string",
6255
+ enum: ["pull", "push"],
6256
+ description: "pull: download cloud memory to local files. push: upload local files to cloud."
6257
+ },
6258
+ cwd: {
6259
+ type: "string",
6260
+ description: "Working directory to derive project slug from (default: process.cwd())."
6261
+ }
6262
+ },
6263
+ required: ["action"],
6264
+ additionalProperties: false
6265
+ };
6266
+ 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.";
6267
+ function isToolInput22(v) {
6268
+ if (typeof v !== "object" || v === null) return false;
6269
+ const o = v;
6270
+ if (o["action"] !== "pull" && o["action"] !== "push") return false;
6271
+ if (o["cwd"] !== void 0 && typeof o["cwd"] !== "string") return false;
6272
+ return true;
6273
+ }
6274
+ function deriveProjectSlug(cwd) {
6275
+ return cwd.replace(/([^:\\/])[\\/]+$/, "$1").replace(/\\/g, "/").replace(/^([a-zA-Z]):/, (_m, drive) => `${drive.toUpperCase()}:`).replace(/[^a-zA-Z0-9]/g, "-");
6276
+ }
6277
+ function getMemoryDir(cwd) {
6278
+ const slug = deriveProjectSlug(cwd);
6279
+ return join11(homedir7(), ".claude", "projects", slug, "memory");
6280
+ }
6281
+ async function runMemorySync(action, cwd, sessionId, fetchFn = globalThis.fetch, lockOptions = {}) {
6282
+ const killSwitch = evaluateMemorySyncKillSwitch();
6283
+ if (killSwitch.disabled) {
6284
+ return { synced: false, reason: killSwitch.reason };
6285
+ }
5648
6286
  const controlPlaneUrl = process.env["VO_CONTROL_PLANE_URL"];
5649
6287
  if (!controlPlaneUrl) {
5650
6288
  return { synced: false, reason: "VO_CONTROL_PLANE_URL not set \u2014 cloud mode disabled" };
@@ -5661,39 +6299,79 @@ async function runMemorySync(action, cwd, sessionId, fetchFn = globalThis.fetch)
5661
6299
  }
5662
6300
  const memoryDir = getMemoryDir(cwd);
5663
6301
  const baseUrl = controlPlaneUrl.replace(/\/+$/, "");
5664
- try {
5665
- if (action === "pull") {
5666
- const result2 = await pullMemory(baseUrl, token, memoryDir, fetchFn);
5667
- return { synced: true, action: "pull", pulled: result2.pulled, files: result2.files, memory_dir: memoryDir };
5668
- }
5669
- const result = await pushMemory(baseUrl, token, memoryDir, sessionId, fetchFn);
5670
- let bridge = { upserted: 0, failed: 0, failures: [] };
5671
- try {
5672
- const { upsertMemoryFilesAsKnowledge: upsertMemoryFilesAsKnowledge2 } = await Promise.resolve().then(() => (init_memory_knowledge_bridge(), memory_knowledge_bridge_exports));
5673
- bridge = await upsertMemoryFilesAsKnowledge2({
5674
- controlPlaneUrl: baseUrl,
5675
- token,
5676
- memoryDir,
5677
- fetchFn
5678
- });
5679
- } catch (err) {
5680
- bridge = {
5681
- upserted: 0,
5682
- failed: 1,
5683
- failures: [`bridge unavailable: ${err instanceof Error ? err.message : String(err)}`]
5684
- };
5685
- }
6302
+ if (action === "push" && !existsSync8(memoryDir)) {
5686
6303
  return {
5687
6304
  synced: true,
5688
6305
  action: "push",
5689
- pushed: result.pushed,
5690
- created: result.created,
5691
- updated: result.updated,
5692
- memory_dir: memoryDir,
5693
- knowledge_upserted: bridge.upserted,
5694
- knowledge_failed: bridge.failed,
5695
- ...bridge.failed > 0 ? { knowledge_failures: bridge.failures } : {}
6306
+ pushed: 0,
6307
+ created: 0,
6308
+ updated: 0,
6309
+ skipped: 0,
6310
+ index_rows_preserved: 0,
6311
+ knowledge_upserted: 0,
6312
+ knowledge_failed: 0,
6313
+ memory_dir: memoryDir
5696
6314
  };
6315
+ }
6316
+ try {
6317
+ return await withMemorySyncLock({ ...lockOptions, memoryDir, sessionId, createDir: action === "pull" }, async (lock) => {
6318
+ const takeover = lock.tookOverFrom ? { lock_taken_over_from_pid: lock.tookOverFrom.pid } : {};
6319
+ if (action === "pull") {
6320
+ const result2 = await pullMemory(baseUrl, token, memoryDir, fetchFn);
6321
+ return {
6322
+ synced: true,
6323
+ action: "pull",
6324
+ pulled: result2.pulled,
6325
+ files: result2.files,
6326
+ memory_dir: memoryDir,
6327
+ ...takeover
6328
+ };
6329
+ }
6330
+ const deadline = createSyncDeadline();
6331
+ const cache = readPushCache(memoryDir, baseUrl);
6332
+ let result;
6333
+ try {
6334
+ result = await pushMemory(baseUrl, token, memoryDir, sessionId, fetchFn, { cache, deadline });
6335
+ } catch (err) {
6336
+ writePushCache(memoryDir, cache);
6337
+ throw err;
6338
+ }
6339
+ let bridge;
6340
+ try {
6341
+ const { upsertMemoryFilesAsKnowledge: upsertMemoryFilesAsKnowledge2 } = await Promise.resolve().then(() => (init_memory_knowledge_bridge(), memory_knowledge_bridge_exports));
6342
+ bridge = await upsertMemoryFilesAsKnowledge2({
6343
+ controlPlaneUrl: baseUrl,
6344
+ token,
6345
+ memoryDir,
6346
+ fetchFn,
6347
+ cache,
6348
+ deadline
6349
+ });
6350
+ } catch (err) {
6351
+ bridge = {
6352
+ upserted: 0,
6353
+ failed: 1,
6354
+ skipped: 0,
6355
+ failures: [`bridge unavailable: ${err instanceof Error ? err.message : String(err)}`]
6356
+ };
6357
+ }
6358
+ writePushCache(memoryDir, cache);
6359
+ return {
6360
+ synced: true,
6361
+ action: "push",
6362
+ pushed: result.pushed,
6363
+ created: result.created,
6364
+ updated: result.updated,
6365
+ skipped: result.skipped,
6366
+ index_rows_preserved: result.indexRowsPreserved,
6367
+ memory_dir: memoryDir,
6368
+ knowledge_upserted: bridge.upserted,
6369
+ knowledge_failed: bridge.failed,
6370
+ knowledge_skipped: bridge.skipped,
6371
+ ...bridge.failed > 0 ? { knowledge_failures: bridge.failures } : {},
6372
+ ...takeover
6373
+ };
6374
+ });
5697
6375
  } catch (err) {
5698
6376
  const message = err instanceof Error ? err.message : String(err);
5699
6377
  return { synced: false, reason: `Sync failed: ${message}` };
@@ -5980,12 +6658,12 @@ async function handleHqWhiteboardRead(_deps, rawInput, signal) {
5980
6658
  }
5981
6659
 
5982
6660
  // src/tools/skills/skill-corpus.ts
5983
- import { existsSync as existsSync7, statSync as statSync5 } from "node:fs";
5984
- import { dirname as dirname5, isAbsolute, join as join10, resolve as resolve2 } from "node:path";
6661
+ import { existsSync as existsSync9, statSync as statSync6 } from "node:fs";
6662
+ import { dirname as dirname5, isAbsolute, join as join13, resolve as resolve2 } from "node:path";
5985
6663
 
5986
6664
  // ../skill-registry/src/loader.ts
5987
- import { readdirSync as readdirSync6, readFileSync as readFileSync10, statSync as statSync4 } from "node:fs";
5988
- import { join as join9 } from "node:path";
6665
+ import { readdirSync as readdirSync6, readFileSync as readFileSync14, statSync as statSync5 } from "node:fs";
6666
+ import { join as join12 } from "node:path";
5989
6667
  var InvalidSkillFrontmatterError = class extends Error {
5990
6668
  constructor(skillFile, reason) {
5991
6669
  super(`Invalid frontmatter in ${skillFile}: ${reason}`);
@@ -6038,18 +6716,18 @@ function loadSkillsFromDir(skillsDir) {
6038
6716
  const entries = readdirSync6(skillsDir);
6039
6717
  const skills = [];
6040
6718
  for (const entry of entries) {
6041
- const entryPath = join9(skillsDir, entry);
6719
+ const entryPath = join12(skillsDir, entry);
6042
6720
  let stat;
6043
6721
  try {
6044
- stat = statSync4(entryPath);
6722
+ stat = statSync5(entryPath);
6045
6723
  } catch {
6046
6724
  continue;
6047
6725
  }
6048
6726
  if (!stat.isDirectory()) continue;
6049
- const skillFile = join9(entryPath, "SKILL.md");
6727
+ const skillFile = join12(entryPath, "SKILL.md");
6050
6728
  let raw;
6051
6729
  try {
6052
- raw = readFileSync10(skillFile, "utf8");
6730
+ raw = readFileSync14(skillFile, "utf8");
6053
6731
  } catch {
6054
6732
  continue;
6055
6733
  }
@@ -6090,12 +6768,12 @@ function resolveSkillsDir(env = process.env, startDir = process.cwd()) {
6090
6768
  const override = env.VO_SKILLS_DIR;
6091
6769
  if (typeof override === "string" && override.length > 0) {
6092
6770
  const abs = isAbsolute(override) ? override : resolve2(startDir, override);
6093
- return existsSync7(abs) && statSync5(abs).isDirectory() ? abs : null;
6771
+ return existsSync9(abs) && statSync6(abs).isDirectory() ? abs : null;
6094
6772
  }
6095
6773
  let dir = resolve2(startDir);
6096
6774
  for (let i = 0; i < MAX_WALK_UP_LEVELS; i += 1) {
6097
- const candidate = join10(dir, ".claude", "skills");
6098
- if (existsSync7(candidate) && statSync5(candidate).isDirectory()) return candidate;
6775
+ const candidate = join13(dir, ".claude", "skills");
6776
+ if (existsSync9(candidate) && statSync6(candidate).isDirectory()) return candidate;
6099
6777
  const parent = dirname5(dir);
6100
6778
  if (parent === dir) break;
6101
6779
  dir = parent;
@@ -6406,7 +7084,7 @@ function buildToolRegistry() {
6406
7084
  };
6407
7085
  }
6408
7086
  function createServer(options) {
6409
- const sessionId = options.sessionId ?? randomUUID2();
7087
+ const sessionId = options.sessionId ?? randomUUID3();
6410
7088
  const mode = createLocalMode();
6411
7089
  const now = options.now ?? (() => /* @__PURE__ */ new Date());
6412
7090
  const server = new Server(
@@ -6458,8 +7136,8 @@ function listToolNames() {
6458
7136
  }
6459
7137
 
6460
7138
  // src/cache/sqlite-cache.ts
6461
- import { createHash as createHash3 } from "node:crypto";
6462
- import { chmodSync as chmodSync3, mkdirSync as mkdirSync6 } from "node:fs";
7139
+ import { createHash as createHash4 } from "node:crypto";
7140
+ import { chmodSync as chmodSync3, mkdirSync as mkdirSync7 } from "node:fs";
6463
7141
  import { dirname as dirname6 } from "node:path";
6464
7142
  import { DatabaseSync } from "node:sqlite";
6465
7143
 
@@ -6505,7 +7183,7 @@ function normalizeString(s) {
6505
7183
  function createSqliteCache(options) {
6506
7184
  const fileBacked = options.dbPath !== ":memory:";
6507
7185
  if (fileBacked) {
6508
- mkdirSync6(dirname6(options.dbPath), { recursive: true, mode: 448 });
7186
+ mkdirSync7(dirname6(options.dbPath), { recursive: true, mode: 448 });
6509
7187
  }
6510
7188
  const versionNamespace = options.cacheVersionNamespace ?? "";
6511
7189
  const db = new DatabaseSync(options.dbPath);
@@ -6536,7 +7214,7 @@ function createSqliteCache(options) {
6536
7214
  return {
6537
7215
  keyFor(toolName, input, opts) {
6538
7216
  const canonical = canonicalize(input, opts);
6539
- const hash = createHash3("sha256");
7217
+ const hash = createHash4("sha256");
6540
7218
  if (versionNamespace.length > 0) {
6541
7219
  hash.update(versionNamespace);
6542
7220
  hash.update("|");
@@ -6628,7 +7306,7 @@ function createStubRatchetClient() {
6628
7306
  let m;
6629
7307
  while ((m = pat.regex.exec(req.source)) !== null) {
6630
7308
  findings.push({
6631
- line_excerpt: clip(m[0], 80),
7309
+ line_excerpt: clip2(m[0], 80),
6632
7310
  severity: pat.severity,
6633
7311
  code: pat.code,
6634
7312
  message: pat.message
@@ -6660,7 +7338,7 @@ function createStubRatchetClient() {
6660
7338
  }
6661
7339
  };
6662
7340
  }
6663
- function clip(s, n) {
7341
+ function clip2(s, n) {
6664
7342
  return s.length <= n ? s : s.slice(0, n) + "\u2026";
6665
7343
  }
6666
7344
  function buildSummary2(args) {
@@ -6689,7 +7367,7 @@ function createNullConsensusEngineClient(reason = NULL_CLIENT_DEFAULT_REASON) {
6689
7367
  }
6690
7368
 
6691
7369
  // src/consensus/engine-client.ts
6692
- import { randomUUID as randomUUID3 } from "node:crypto";
7370
+ import { randomUUID as randomUUID4 } from "node:crypto";
6693
7371
 
6694
7372
  // src/consensus/meta-model-caller.ts
6695
7373
  var META_CONSENSUS_MODEL = "muse-spark-1.1";
@@ -6798,6 +7476,14 @@ function shadowEnabled(env) {
6798
7476
  const norm = raw.trim().toLowerCase();
6799
7477
  return !(norm === "0" || norm === "false" || norm === "no" || norm === "off" || norm === "");
6800
7478
  }
7479
+ var MIN_RESPONDERS_ENV_VAR = "VO_CONSENSUS_MIN_RESPONDERS";
7480
+ function resolveMinResponders(env) {
7481
+ const raw = (env ?? {})[MIN_RESPONDERS_ENV_VAR];
7482
+ if (raw === void 0 || raw.trim() === "") return 2;
7483
+ const parsed = Number.parseInt(raw.trim(), 10);
7484
+ if (!Number.isFinite(parsed) || parsed <= 0) return void 0;
7485
+ return parsed;
7486
+ }
6801
7487
  function mapShadowSynthesis(s) {
6802
7488
  if (s === void 0) return void 0;
6803
7489
  return {
@@ -6947,9 +7633,11 @@ function createEngineConsensusClient(options) {
6947
7633
  ...options.agreement_gate_enabled !== void 0 ? { configEnabled: options.agreement_gate_enabled } : {},
6948
7634
  ...options.env !== void 0 ? { env: options.env } : {}
6949
7635
  });
7636
+ const minResponders = resolveMinResponders(options.env);
6950
7637
  const engineOptions = {
6951
7638
  panel,
6952
7639
  ...options.per_model_timeout_ms !== void 0 ? { per_model_timeout_ms: options.per_model_timeout_ms } : {},
7640
+ ...minResponders !== void 0 ? { min_responders: minResponders } : {},
6953
7641
  ...agreementGate !== void 0 ? { agreement_gate: agreementGate } : {},
6954
7642
  // Stage A7-shadow: run the adaptive verdict alongside the live one for grading.
6955
7643
  // Cheap (pure log-odds over already-fetched verdicts; no extra model calls),
@@ -7002,8 +7690,13 @@ function createEngineConsensusClient(options) {
7002
7690
  synthesized_verdict: response.synthesized_verdict,
7003
7691
  per_model_verdicts: response.per_model_verdicts,
7004
7692
  degraded: response.degraded,
7693
+ ...response.quorum_failed === true ? { quorum_failed: true } : {},
7005
7694
  duration_ms: response.duration_ms,
7006
7695
  engine_version: response.engine_version,
7696
+ // Cumulative cross-round inference usage (B44-3). Absent when no panel
7697
+ // member reported usage; forwarded verbatim — the aggregator prefers it
7698
+ // over summing final-round verdicts (which under-reports deliberation).
7699
+ ...response.token_usage !== void 0 ? { token_usage: response.token_usage } : {},
7007
7700
  // Phase 2 Lane D-1 — forward escalation signal when present. The
7008
7701
  // source-grounded layer's own escalation (from the citation grade)
7009
7702
  // takes precedence when set, else the synthesizer's.