@algosuite/vo-mcp 0.2.0-beta.4 → 0.2.0-beta.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -3
- package/bin/vo-mcp +9 -3
- package/dist/agent-auth-probe-cli.mjs +1707 -0
- package/dist/autostart-cli.js +115 -60
- package/dist/autostart-cli.js.map +2 -2
- package/dist/ci/check-local-pr-overlap.js +107511 -0
- package/dist/cli.js +2384 -339
- package/dist/cli.js.map +4 -4
- package/dist/index.js +2118 -199
- package/dist/index.js.map +4 -4
- package/dist/install-cli.js +361 -345
- package/dist/install-cli.js.map +4 -4
- package/dist/login-cli.js +4 -4
- package/dist/login-cli.js.map +2 -2
- package/dist/pair-cli.js +1 -1
- package/dist/pair-cli.js.map +2 -2
- package/dist/runner-cli.js +13987 -2596
- package/dist/runner-cli.js.map +4 -4
- package/dist/runner-supervisor.js +2617 -0
- package/dist/runner-supervisor.js.map +7 -0
- package/dist/set-key-cli.js +81 -5
- package/dist/set-key-cli.js.map +2 -2
- package/dist/supervisor-credential-helper.js +233 -0
- package/dist/supervisor-credential-helper.js.map +7 -0
- package/dist/thresholds.json +64 -0
- package/dist/update-cli.js +125 -0
- package/dist/update-cli.js.map +7 -0
- package/package.json +5 -3
package/dist/index.js
CHANGED
|
@@ -180,6 +180,7 @@ __export(credential_store_exports, {
|
|
|
180
180
|
KEYCHAIN_LOCATION: () => KEYCHAIN_LOCATION,
|
|
181
181
|
credentialPath: () => credentialPath,
|
|
182
182
|
readStoredCredential: () => readStoredCredential,
|
|
183
|
+
readStoredCredentialKeychainOnly: () => readStoredCredentialKeychainOnly,
|
|
183
184
|
writeStoredCredential: () => writeStoredCredential
|
|
184
185
|
});
|
|
185
186
|
import { homedir as homedir3 } from "node:os";
|
|
@@ -238,6 +239,11 @@ function readStoredCredential(env = process.env, keychain = realKeychain) {
|
|
|
238
239
|
}
|
|
239
240
|
return readFromFile(env);
|
|
240
241
|
}
|
|
242
|
+
function readStoredCredentialKeychainOnly(env = process.env, keychain = realKeychain) {
|
|
243
|
+
if (!keychainEnabled(env, keychain)) return null;
|
|
244
|
+
const raw = keychain.get();
|
|
245
|
+
return raw ? deserialize(raw) : null;
|
|
246
|
+
}
|
|
241
247
|
function deleteFile(env) {
|
|
242
248
|
try {
|
|
243
249
|
rmSync(credentialPath(env), { force: true });
|
|
@@ -287,8 +293,332 @@ var init_credential_store = __esm({
|
|
|
287
293
|
}
|
|
288
294
|
});
|
|
289
295
|
|
|
296
|
+
// src/tools/memory/safe-memory-file.ts
|
|
297
|
+
import { resolve, sep } from "node:path";
|
|
298
|
+
function isSafeMemoryFileName(fileName) {
|
|
299
|
+
return fileName.length <= 200 && fileName.trim() === fileName && !fileName.includes("/") && !fileName.includes("\\") && !fileName.includes(":") && SAFE_MEMORY_FILE_RE.test(fileName);
|
|
300
|
+
}
|
|
301
|
+
function resolveMemoryFilePath(memoryDir, fileName) {
|
|
302
|
+
if (!isSafeMemoryFileName(fileName)) {
|
|
303
|
+
throw new Error(`unsafe memory file_name: ${fileName.slice(0, 80)}`);
|
|
304
|
+
}
|
|
305
|
+
const root = resolve(memoryDir);
|
|
306
|
+
const filePath = resolve(root, fileName);
|
|
307
|
+
const rootPrefix = root.endsWith(sep) ? root : `${root}${sep}`;
|
|
308
|
+
if (filePath !== root && !filePath.startsWith(rootPrefix)) {
|
|
309
|
+
throw new Error(`memory file path escapes memory directory: ${fileName.slice(0, 80)}`);
|
|
310
|
+
}
|
|
311
|
+
return filePath;
|
|
312
|
+
}
|
|
313
|
+
var SAFE_MEMORY_FILE_RE;
|
|
314
|
+
var init_safe_memory_file = __esm({
|
|
315
|
+
"src/tools/memory/safe-memory-file.ts"() {
|
|
316
|
+
"use strict";
|
|
317
|
+
SAFE_MEMORY_FILE_RE = /^[A-Za-z0-9][A-Za-z0-9._ -]*\.md$/i;
|
|
318
|
+
}
|
|
319
|
+
});
|
|
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
|
+
|
|
499
|
+
// src/tools/memory/memory-knowledge-bridge.ts
|
|
500
|
+
var memory_knowledge_bridge_exports = {};
|
|
501
|
+
__export(memory_knowledge_bridge_exports, {
|
|
502
|
+
extractMemoryTitle: () => extractMemoryTitle,
|
|
503
|
+
upsertMemoryFilesAsKnowledge: () => upsertMemoryFilesAsKnowledge
|
|
504
|
+
});
|
|
505
|
+
import { existsSync as existsSync7, readdirSync as readdirSync5, readFileSync as readFileSync13 } from "node:fs";
|
|
506
|
+
function extractMemoryTitle(fileName, content) {
|
|
507
|
+
const frontmatter = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
508
|
+
if (frontmatter) {
|
|
509
|
+
const description23 = frontmatter[1].match(/^description:\s*(.+)$/m);
|
|
510
|
+
if (description23 && description23[1].trim()) return description23[1].trim().slice(0, 200);
|
|
511
|
+
}
|
|
512
|
+
const heading = content.match(/^#\s+(.+)$/m);
|
|
513
|
+
if (heading && heading[1].trim()) return heading[1].trim().slice(0, 200);
|
|
514
|
+
return fileName;
|
|
515
|
+
}
|
|
516
|
+
async function upsertMemoryFilesAsKnowledge(options) {
|
|
517
|
+
const { controlPlaneUrl, token, memoryDir, fetchFn, cache, deadline } = options;
|
|
518
|
+
let files;
|
|
519
|
+
try {
|
|
520
|
+
if (!existsSync7(memoryDir)) {
|
|
521
|
+
return { attempted: 0, upserted: 0, failed: 0, skipped: 0, failures: [] };
|
|
522
|
+
}
|
|
523
|
+
files = readdirSync5(memoryDir).filter(
|
|
524
|
+
(f) => f.endsWith(".md") && f.toUpperCase() !== "MEMORY.MD"
|
|
525
|
+
);
|
|
526
|
+
} catch (err) {
|
|
527
|
+
return {
|
|
528
|
+
attempted: 0,
|
|
529
|
+
upserted: 0,
|
|
530
|
+
failed: 1,
|
|
531
|
+
skipped: 0,
|
|
532
|
+
failures: [`memory dir scan: ${err instanceof Error ? err.message : String(err)}`]
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
const sweepDue = cache ? knowledgeSweepDue(cache) : true;
|
|
536
|
+
const candidates = [];
|
|
537
|
+
const failures = [];
|
|
538
|
+
let skipped = 0;
|
|
539
|
+
for (const fileName of files) {
|
|
540
|
+
try {
|
|
541
|
+
const content = readFileSync13(resolveMemoryFilePath(memoryDir, fileName), "utf8");
|
|
542
|
+
if (content.length > CONTENT_HARD_LIMIT) {
|
|
543
|
+
failures.push(`${fileName}: ${content.length} chars exceeds the ${CONTENT_HARD_LIMIT} server limit \u2014 split the memory file`);
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
const hash = sha256(content);
|
|
547
|
+
if (cache && !needsKnowledgePush(cache, fileName, hash, sweepDue)) {
|
|
548
|
+
skipped += 1;
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
candidates.push({ fileName, content, hash });
|
|
552
|
+
} catch (err) {
|
|
553
|
+
failures.push(`${fileName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
554
|
+
}
|
|
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
|
+
}
|
|
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".
|
|
602
|
+
attempted: files.length,
|
|
603
|
+
upserted,
|
|
604
|
+
failed: failures.length,
|
|
605
|
+
skipped,
|
|
606
|
+
failures: failures.slice(0, 5)
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
var CONTENT_HARD_LIMIT;
|
|
610
|
+
var init_memory_knowledge_bridge = __esm({
|
|
611
|
+
"src/tools/memory/memory-knowledge-bridge.ts"() {
|
|
612
|
+
"use strict";
|
|
613
|
+
init_safe_memory_file();
|
|
614
|
+
init_bounded_sync();
|
|
615
|
+
init_memory_push_cache();
|
|
616
|
+
CONTENT_HARD_LIMIT = 5e5;
|
|
617
|
+
}
|
|
618
|
+
});
|
|
619
|
+
|
|
290
620
|
// src/server.ts
|
|
291
|
-
import { randomUUID as
|
|
621
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
292
622
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
293
623
|
import {
|
|
294
624
|
CallToolRequestSchema,
|
|
@@ -1216,6 +1546,42 @@ function toEventPerModelVerdicts(src) {
|
|
|
1216
1546
|
};
|
|
1217
1547
|
});
|
|
1218
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
|
+
}
|
|
1219
1585
|
function toEventSynthesizedVerdict(src) {
|
|
1220
1586
|
return {
|
|
1221
1587
|
verdict: src.verdict,
|
|
@@ -1587,7 +1953,8 @@ async function handleCheckHollowTest(deps, rawInput, signal) {
|
|
|
1587
1953
|
synthesized_verdict: synthForEvent,
|
|
1588
1954
|
consensus_confidence: engineResult.synthesized_verdict.confidence,
|
|
1589
1955
|
duration_ms: engineResult.duration_ms,
|
|
1590
|
-
consensus_engine_version: engineResult.engine_version
|
|
1956
|
+
consensus_engine_version: engineResult.engine_version,
|
|
1957
|
+
...aggregateEventTokenUsage(engineResult.per_model_verdicts, engineResult.token_usage)
|
|
1591
1958
|
};
|
|
1592
1959
|
const payload = {
|
|
1593
1960
|
verdict: engineResult.synthesized_verdict.verdict,
|
|
@@ -1769,7 +2136,8 @@ async function handleVerifyAnswer(deps, rawInput, signal) {
|
|
|
1769
2136
|
synthesized_verdict: synthForEvent,
|
|
1770
2137
|
consensus_confidence: engineResult.synthesized_verdict.confidence,
|
|
1771
2138
|
duration_ms: engineResult.duration_ms,
|
|
1772
|
-
consensus_engine_version: engineResult.engine_version
|
|
2139
|
+
consensus_engine_version: engineResult.engine_version,
|
|
2140
|
+
...aggregateEventTokenUsage(engineResult.per_model_verdicts, engineResult.token_usage)
|
|
1773
2141
|
};
|
|
1774
2142
|
const payload = {
|
|
1775
2143
|
verdict: engineResult.synthesized_verdict.verdict,
|
|
@@ -1778,6 +2146,9 @@ async function handleVerifyAnswer(deps, rawInput, signal) {
|
|
|
1778
2146
|
synthesized_verdict: synthForEvent,
|
|
1779
2147
|
engine_version: engineResult.engine_version,
|
|
1780
2148
|
degraded: engineResult.degraded,
|
|
2149
|
+
...engineResult.quorum_failed === true ? { quorum_failed: true } : {},
|
|
2150
|
+
// The verification receipt (moat decision_id) — the thing an agent pastes as `receipt id: <uuid>`.
|
|
2151
|
+
...engineResult.receipt_id ? { receipt_id: engineResult.receipt_id } : {},
|
|
1781
2152
|
gate_type: gateType,
|
|
1782
2153
|
...kbResult.error !== null ? { kb_unavailable: true } : {},
|
|
1783
2154
|
...kbTruncated > 0 ? { kb_rules_truncated: kbTruncated } : {}
|
|
@@ -2031,7 +2402,8 @@ async function handleConsensusJudgment(deps, rawInput, signal) {
|
|
|
2031
2402
|
duration_ms: engineResult.duration_ms,
|
|
2032
2403
|
consensus_engine_version: engineResult.engine_version,
|
|
2033
2404
|
per_model_verdicts: perModelForEvent,
|
|
2034
|
-
synthesized_verdict: synthForEvent
|
|
2405
|
+
synthesized_verdict: synthForEvent,
|
|
2406
|
+
...aggregateEventTokenUsage(engineResult.per_model_verdicts, engineResult.token_usage)
|
|
2035
2407
|
};
|
|
2036
2408
|
const payload = {
|
|
2037
2409
|
verdict: engineResult.synthesized_verdict.verdict,
|
|
@@ -2040,6 +2412,11 @@ async function handleConsensusJudgment(deps, rawInput, signal) {
|
|
|
2040
2412
|
synthesized_verdict: synthForEvent,
|
|
2041
2413
|
engine_version: engineResult.engine_version,
|
|
2042
2414
|
degraded: engineResult.degraded,
|
|
2415
|
+
...engineResult.quorum_failed === true ? { quorum_failed: true } : {},
|
|
2416
|
+
// The verification receipt (moat decision_id) — the thing an agent pastes as `receipt id: <uuid>`.
|
|
2417
|
+
// NOTE: a content-hash cache hit replays the ORIGINAL call's receipt_id (same claim, same verdict, no new spend) —
|
|
2418
|
+
// a receipt asserts the stage ran for this claim, not one-receipt-per-call.
|
|
2419
|
+
...engineResult.receipt_id ? { receipt_id: engineResult.receipt_id } : {},
|
|
2043
2420
|
gate_type: gateType,
|
|
2044
2421
|
// ─── Consensus-engine feature outputs (additive; 2026-06-13) ─────────────
|
|
2045
2422
|
// Feature 2 (calibrated-confidence) — ON by default; the engine attaches
|
|
@@ -2058,7 +2435,11 @@ async function handleConsensusJudgment(deps, rawInput, signal) {
|
|
|
2058
2435
|
...engineResult.low_confidence_sources !== void 0 ? { low_confidence_sources: engineResult.low_confidence_sources } : {},
|
|
2059
2436
|
// Escalation (from citation grade or human-tiebreak synthesizer).
|
|
2060
2437
|
...engineResult.escalation_required !== void 0 ? { escalation_required: engineResult.escalation_required } : {},
|
|
2061
|
-
...engineResult.escalation_reason !== void 0 ? { escalation_reason: engineResult.escalation_reason } : {}
|
|
2438
|
+
...engineResult.escalation_reason !== void 0 ? { escalation_reason: engineResult.escalation_reason } : {},
|
|
2439
|
+
// Critique-uptake (2026-07-20 red-team fix) — the engine computes this
|
|
2440
|
+
// on every call; this spread closes the gap where the visibility report
|
|
2441
|
+
// was itself silently dropped at the payload boundary.
|
|
2442
|
+
...engineResult.critique_uptake !== void 0 ? { critique_uptake: engineResult.critique_uptake } : {}
|
|
2062
2443
|
};
|
|
2063
2444
|
const envelope = {
|
|
2064
2445
|
tool: TOOL_NAME4,
|
|
@@ -2234,7 +2615,8 @@ async function handleArchitectureReview(deps, rawInput, signal) {
|
|
|
2234
2615
|
synthesized_verdict: synthForEvent,
|
|
2235
2616
|
consensus_confidence: engineResult.synthesized_verdict.confidence,
|
|
2236
2617
|
duration_ms: engineResult.duration_ms,
|
|
2237
|
-
consensus_engine_version: engineResult.engine_version
|
|
2618
|
+
consensus_engine_version: engineResult.engine_version,
|
|
2619
|
+
...aggregateEventTokenUsage(engineResult.per_model_verdicts, engineResult.token_usage)
|
|
2238
2620
|
};
|
|
2239
2621
|
const escalationRequired = engineResult.escalation_required === true || engineResult.escalation_required === void 0 && engineResult.synthesized_verdict.dissent_summary !== null;
|
|
2240
2622
|
const escalationReason = engineResult.escalation_reason ?? engineResult.synthesized_verdict.dissent_summary ?? "";
|
|
@@ -3719,7 +4101,8 @@ Produce the JSON dispatch plan now.`;
|
|
|
3719
4101
|
duration_ms: engineResult.duration_ms,
|
|
3720
4102
|
consensus_engine_version: engineResult.engine_version,
|
|
3721
4103
|
per_model_verdicts: toEventPerModelVerdicts(engineResult.per_model_verdicts),
|
|
3722
|
-
synthesized_verdict: toEventSynthesizedVerdict(engineResult.synthesized_verdict)
|
|
4104
|
+
synthesized_verdict: toEventSynthesizedVerdict(engineResult.synthesized_verdict),
|
|
4105
|
+
...aggregateEventTokenUsage(engineResult.per_model_verdicts, engineResult.token_usage)
|
|
3723
4106
|
};
|
|
3724
4107
|
deps.events.append(enrichedEvent);
|
|
3725
4108
|
return jsonContent(envelope);
|
|
@@ -3813,7 +4196,7 @@ async function buildCloudOrStubResponse(args) {
|
|
|
3813
4196
|
}
|
|
3814
4197
|
|
|
3815
4198
|
// src/tools/heal/common-heal.ts
|
|
3816
|
-
var HEAL_STUB_REASON =
|
|
4199
|
+
var HEAL_STUB_REASON = "cloud mode is not configured on this session, so the admin callable was not reached. The wiring exists (see buildCloudOrStubResponse); sign in with `vo-mcp login` to route this tool to the control plane. Note that /api/v1/admin/* additionally requires a founding-operator credential.";
|
|
3817
4200
|
var HEAL_GATE_TYPE = "admin-action";
|
|
3818
4201
|
|
|
3819
4202
|
// src/tools/heal/trigger-heal.ts
|
|
@@ -3834,7 +4217,7 @@ var inputSchema8 = {
|
|
|
3834
4217
|
},
|
|
3835
4218
|
additionalProperties: false
|
|
3836
4219
|
};
|
|
3837
|
-
var description8 = "Triggers a self-heal pass against open PRs. Optionally scope to a `focus_page` (priority queue for one tester) or omit to fire the auto-process queue. Wraps the `voTriggerHeal` admin Cloud Function.
|
|
4220
|
+
var description8 = "Triggers a self-heal pass against open PRs. Optionally scope to a `focus_page` (priority queue for one tester) or omit to fire the auto-process queue. Wraps the `voTriggerHeal` admin Cloud Function. Requires a founding-operator credential: the control plane's scoped-operator gate refuses `/api/v1/admin/*` for tenant-scoped accounts, which receive HTTP 403 `scoped_operator_forbidden`. Falls back to a clearly-marked `unimplemented` envelope with structured normalized_input when cloud mode is not configured.";
|
|
3838
4221
|
function isToolInput8(v) {
|
|
3839
4222
|
if (typeof v !== "object" || v === null) return false;
|
|
3840
4223
|
const o = v;
|
|
@@ -3891,7 +4274,7 @@ var inputSchema9 = {
|
|
|
3891
4274
|
},
|
|
3892
4275
|
additionalProperties: false
|
|
3893
4276
|
};
|
|
3894
|
-
var description9 = "Retries one or more failed fix attempts by id. Pass `attempt_id` for the single case or `attempt_ids` (up to 50) for the batch case. Wraps `voRetryFixAttempt` / `voRetryFixAttempts` admin Cloud Functions.
|
|
4277
|
+
var description9 = "Retries one or more failed fix attempts by id. Pass `attempt_id` for the single case or `attempt_ids` (up to 50) for the batch case. Wraps `voRetryFixAttempt` / `voRetryFixAttempts` admin Cloud Functions. Requires a founding-operator credential: the control plane's scoped-operator gate refuses `/api/v1/admin/*` for tenant-scoped accounts, which receive HTTP 403 `scoped_operator_forbidden`. Falls back to a clearly-marked `unimplemented` envelope when cloud mode is not configured.";
|
|
3895
4278
|
function isToolInput9(v) {
|
|
3896
4279
|
if (typeof v !== "object" || v === null) return false;
|
|
3897
4280
|
const o = v;
|
|
@@ -3978,7 +4361,7 @@ var inputSchema10 = {
|
|
|
3978
4361
|
required: ["attempt_id"],
|
|
3979
4362
|
additionalProperties: false
|
|
3980
4363
|
};
|
|
3981
|
-
var description10 = "Clears (cancels) a single fix attempt by id. Wraps `voClearFixAttempt` admin Cloud Function.
|
|
4364
|
+
var description10 = "Clears (cancels) a single fix attempt by id. Wraps `voClearFixAttempt` admin Cloud Function. Requires a founding-operator credential: the control plane's scoped-operator gate refuses `/api/v1/admin/*` for tenant-scoped accounts, which receive HTTP 403 `scoped_operator_forbidden`. Falls back to a clearly-marked `unimplemented` envelope when cloud mode is not configured.";
|
|
3982
4365
|
function isToolInput10(v) {
|
|
3983
4366
|
if (typeof v !== "object" || v === null) return false;
|
|
3984
4367
|
const o = v;
|
|
@@ -4025,7 +4408,7 @@ var inputSchema11 = {
|
|
|
4025
4408
|
required: ["run_id"],
|
|
4026
4409
|
additionalProperties: false
|
|
4027
4410
|
};
|
|
4028
|
-
var description11 = "Cancels a running GitHub Actions workflow by run id. Wraps `voStopWorkflow` admin Cloud Function.
|
|
4411
|
+
var description11 = "Cancels a running GitHub Actions workflow by run id. Wraps `voStopWorkflow` admin Cloud Function. Requires a founding-operator credential: the control plane's scoped-operator gate refuses `/api/v1/admin/*` for tenant-scoped accounts, which receive HTTP 403 `scoped_operator_forbidden`. Falls back to a clearly-marked `unimplemented` envelope when cloud mode is not configured.";
|
|
4029
4412
|
function isToolInput11(v) {
|
|
4030
4413
|
if (typeof v !== "object" || v === null) return false;
|
|
4031
4414
|
const o = v;
|
|
@@ -4070,7 +4453,7 @@ var inputSchema12 = {
|
|
|
4070
4453
|
properties: {},
|
|
4071
4454
|
additionalProperties: false
|
|
4072
4455
|
};
|
|
4073
|
-
var description12 = "Returns the current Command Center workflow-runs snapshot (Heal, Manager, Auto-Merge, Deploy on Merge, etc.). Wraps `voGetWorkflowRuns` admin Cloud Function. Read-only diagnostic.
|
|
4456
|
+
var description12 = "Returns the current Command Center workflow-runs snapshot (Heal, Manager, Auto-Merge, Deploy on Merge, etc.). Wraps `voGetWorkflowRuns` admin Cloud Function. Read-only diagnostic. Requires a founding-operator credential: the control plane's scoped-operator gate refuses `/api/v1/admin/*` for tenant-scoped accounts, which receive HTTP 403 `scoped_operator_forbidden`. Falls back to a clearly-marked `unimplemented` envelope when cloud mode is not configured.";
|
|
4074
4457
|
function isToolInput12(v) {
|
|
4075
4458
|
if (typeof v !== "object" || v === null) return false;
|
|
4076
4459
|
return true;
|
|
@@ -4093,7 +4476,7 @@ async function handleGetWorkflowRuns(deps, rawInput, _signal) {
|
|
|
4093
4476
|
}
|
|
4094
4477
|
|
|
4095
4478
|
// src/tools/pr/common-pr.ts
|
|
4096
|
-
var PR_STUB_REASON =
|
|
4479
|
+
var PR_STUB_REASON = "cloud mode is not configured on this session, so the admin callable was not reached. The wiring exists (see buildCloudOrStubResponse); sign in with `vo-mcp login` to route this tool to the control plane. Note that /api/v1/admin/* additionally requires a founding-operator credential.";
|
|
4097
4480
|
var PR_GATE_TYPE = "admin-action";
|
|
4098
4481
|
|
|
4099
4482
|
// src/tools/pr/list-pending-prs.ts
|
|
@@ -4105,7 +4488,7 @@ var inputSchema13 = {
|
|
|
4105
4488
|
properties: {},
|
|
4106
4489
|
additionalProperties: false
|
|
4107
4490
|
};
|
|
4108
|
-
var description13 = "Lists open
|
|
4491
|
+
var description13 = "Lists open AlgoHQ-source pull requests with blocker / source / tester / specialist-context metadata. Read-only diagnostic for Command Center reads. Wraps `voListPendingPRs` admin Cloud Function. Requires a founding-operator credential: the control plane's scoped-operator gate refuses `/api/v1/admin/*` for tenant-scoped accounts, which receive HTTP 403 `scoped_operator_forbidden`. Falls back to a clearly-marked `unimplemented` envelope when cloud mode is not configured.";
|
|
4109
4492
|
function isToolInput13(v) {
|
|
4110
4493
|
return typeof v === "object" && v !== null;
|
|
4111
4494
|
}
|
|
@@ -4141,7 +4524,7 @@ var inputSchema14 = {
|
|
|
4141
4524
|
required: ["pr_number"],
|
|
4142
4525
|
additionalProperties: false
|
|
4143
4526
|
};
|
|
4144
|
-
var description14 = "Approves + merges a single
|
|
4527
|
+
var description14 = "Approves + merges a single AlgoHQ-source pull request by number. Wraps `voMergePR` admin Cloud Function (server-side refuses non-AlgoHQ PRs with permission-denied). Requires a founding-operator credential: the control plane's scoped-operator gate refuses `/api/v1/admin/*` for tenant-scoped accounts, which receive HTTP 403 `scoped_operator_forbidden`. Falls back to a clearly-marked `unimplemented` envelope when cloud mode is not configured.";
|
|
4145
4528
|
function isToolInput14(v) {
|
|
4146
4529
|
if (typeof v !== "object" || v === null) return false;
|
|
4147
4530
|
const o = v;
|
|
@@ -4185,7 +4568,7 @@ var inputSchema15 = {
|
|
|
4185
4568
|
required: ["pr_number"],
|
|
4186
4569
|
additionalProperties: false
|
|
4187
4570
|
};
|
|
4188
|
-
var description15 = "Closes a pull request without merging. No retry dispatched \u2014 use `vo_reject_and_retry` for close+retry. Wraps `voRejectPR` admin Cloud Function.
|
|
4571
|
+
var description15 = "Closes a pull request without merging. No retry dispatched \u2014 use `vo_reject_and_retry` for close+retry. Wraps `voRejectPR` admin Cloud Function. Requires a founding-operator credential: the control plane's scoped-operator gate refuses `/api/v1/admin/*` for tenant-scoped accounts, which receive HTTP 403 `scoped_operator_forbidden`. Falls back to a clearly-marked `unimplemented` envelope when cloud mode is not configured.";
|
|
4189
4572
|
function isToolInput15(v) {
|
|
4190
4573
|
if (typeof v !== "object" || v === null) return false;
|
|
4191
4574
|
const o = v;
|
|
@@ -4223,7 +4606,7 @@ var inputSchema16 = {
|
|
|
4223
4606
|
properties: {},
|
|
4224
4607
|
additionalProperties: false
|
|
4225
4608
|
};
|
|
4226
|
-
var description16 = "Iterates all open
|
|
4609
|
+
var description16 = "Iterates all open AlgoHQ-source pull requests and merges (or arms auto-merge) on each. Returns counts of merged / accepted / total plus per-PR results. Wraps `voApproveAllFixes` admin Cloud Function. Requires a founding-operator credential: the control plane's scoped-operator gate refuses `/api/v1/admin/*` for tenant-scoped accounts, which receive HTTP 403 `scoped_operator_forbidden`. Falls back to a clearly-marked `unimplemented` envelope when cloud mode is not configured.";
|
|
4227
4610
|
function isToolInput16(v) {
|
|
4228
4611
|
return typeof v === "object" && v !== null;
|
|
4229
4612
|
}
|
|
@@ -4257,7 +4640,7 @@ var inputSchema17 = {
|
|
|
4257
4640
|
required: ["pr_number"],
|
|
4258
4641
|
additionalProperties: false
|
|
4259
4642
|
};
|
|
4260
|
-
var description17 = "Closes
|
|
4643
|
+
var description17 = "Closes an AlgoHQ pull request and dispatches a self-heal pass to retry the same focus page. Cloud callable refuses non-AlgoHQ PRs and respects the self-heal kill switch + per-PR retry block. Wraps `voRejectAndRetry` admin Cloud Function. Requires a founding-operator credential: the control plane's scoped-operator gate refuses `/api/v1/admin/*` for tenant-scoped accounts, which receive HTTP 403 `scoped_operator_forbidden`. Falls back to a clearly-marked `unimplemented` envelope when cloud mode is not configured.";
|
|
4261
4644
|
function isToolInput17(v) {
|
|
4262
4645
|
if (typeof v !== "object" || v === null) return false;
|
|
4263
4646
|
const o = v;
|
|
@@ -4333,7 +4716,7 @@ function buildPrompt4(pr, notes) {
|
|
|
4333
4716
|
const lines = [
|
|
4334
4717
|
"You are a release gatekeeper deciding whether a pull request is safe to MERGE.",
|
|
4335
4718
|
"Recommend exactly one of: merge / hold / reject. Be conservative \u2014 this is a high-stakes irreversible action.",
|
|
4336
|
-
"Rules: HOLD if CI is failing/blocked, there is a merge conflict, or the change is a draft. REJECT if the PR is not a legitimate
|
|
4719
|
+
"Rules: HOLD if CI is failing/blocked, there is a merge conflict, or the change is a draft. REJECT if the PR is not a legitimate AlgoHQ-source change or has no clear purpose. MERGE only if it looks complete, scoped, and unblocked.",
|
|
4337
4720
|
"",
|
|
4338
4721
|
`PR #${pr.number}: ${pr.title}`,
|
|
4339
4722
|
`Source: ${pr.source ?? "unknown"}`,
|
|
@@ -4401,7 +4784,7 @@ async function handleReviewMerge(deps, rawInput, signal) {
|
|
|
4401
4784
|
}
|
|
4402
4785
|
if (pr === null) {
|
|
4403
4786
|
return emit(
|
|
4404
|
-
emptyPayload("hold", `PR #${prNumber} is not among open
|
|
4787
|
+
emptyPayload("hold", `PR #${prNumber} is not among open AlgoHQ PRs (already merged/closed, or not an AlgoHQ-source PR).`, null)
|
|
4405
4788
|
);
|
|
4406
4789
|
}
|
|
4407
4790
|
const hasBlocker = pr.blocker !== null && pr.blocker !== "none";
|
|
@@ -4461,7 +4844,8 @@ async function handleReviewMerge(deps, rawInput, signal) {
|
|
|
4461
4844
|
duration_ms: result.duration_ms,
|
|
4462
4845
|
consensus_engine_version: result.engine_version,
|
|
4463
4846
|
per_model_verdicts: perModel,
|
|
4464
|
-
synthesized_verdict: synth
|
|
4847
|
+
synthesized_verdict: synth,
|
|
4848
|
+
...aggregateEventTokenUsage(result.per_model_verdicts, result.token_usage)
|
|
4465
4849
|
});
|
|
4466
4850
|
}
|
|
4467
4851
|
|
|
@@ -4496,6 +4880,8 @@ function suggestedHandoffPath(session_id, isoTimestamp) {
|
|
|
4496
4880
|
}
|
|
4497
4881
|
|
|
4498
4882
|
// src/tools/session/report-session-state.ts
|
|
4883
|
+
init_auth_token_source();
|
|
4884
|
+
init_credential_store();
|
|
4499
4885
|
var TOOL_NAME19 = "vo_report_session_state";
|
|
4500
4886
|
var VALID_AGENT_TYPES = ["claude-code", "codex", "cursor", "continue"];
|
|
4501
4887
|
var MAX_GOAL_CHARS = 500;
|
|
@@ -4546,7 +4932,7 @@ var inputSchema19 = {
|
|
|
4546
4932
|
required: ["operator_id", "session_id", "agent_type", "context_used_pct"],
|
|
4547
4933
|
additionalProperties: false
|
|
4548
4934
|
};
|
|
4549
|
-
var description19 = "Reports per-session context-window utilization to
|
|
4935
|
+
var description19 = "Reports per-session context-window utilization to AlgoHQ and returns a directive: 'continue' (under 70%), 'prepare_handoff' (70-84%), or 'execute_handoff_now' (\u226585%). Implements V1 launch gate #9 (fleet context lifecycle management) per the official AlgoHQ roadmap. Cloud-control-plane mode when VO_CONTROL_PLANE_URL plus a user/scoped HQ credential (or legacy admin token) is available; auto-allocates the session on first report so interactive agents (Claude Code, Cursor, Codex, Continue) appear on the live fleet whiteboard. Stub-local fallback when cloud config is absent or fails. The response shape stays stable across modes (`backend_mode` field in the payload tells the caller which mode produced the verdict).";
|
|
4550
4936
|
function isStringArray2(v, maxItems) {
|
|
4551
4937
|
if (!Array.isArray(v)) return false;
|
|
4552
4938
|
if (v.length > maxItems) return false;
|
|
@@ -4571,14 +4957,39 @@ function isToolInput19(v) {
|
|
|
4571
4957
|
}
|
|
4572
4958
|
return true;
|
|
4573
4959
|
}
|
|
4574
|
-
function
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4960
|
+
async function fetchCloudIdentity(url, token, fetchFn) {
|
|
4961
|
+
try {
|
|
4962
|
+
const response = await fetchFn(`${url}/api/v1/auth/me`, {
|
|
4963
|
+
method: "GET",
|
|
4964
|
+
headers: {
|
|
4965
|
+
"Authorization": `Bearer ${token}`
|
|
4966
|
+
}
|
|
4967
|
+
});
|
|
4968
|
+
if (!response.ok) return null;
|
|
4969
|
+
const data = await response.json();
|
|
4970
|
+
if (!data.ok || !data.provisioned || !data.operator_id || !data.tenant_id) return null;
|
|
4971
|
+
return { operator_id: data.operator_id, tenant_id: data.tenant_id };
|
|
4972
|
+
} catch {
|
|
4973
|
+
return null;
|
|
4974
|
+
}
|
|
4580
4975
|
}
|
|
4581
|
-
async function
|
|
4976
|
+
async function getCloudConfig(fetchFn = fetch) {
|
|
4977
|
+
const url = process.env["VO_CONTROL_PLANE_URL"]?.trim();
|
|
4978
|
+
if (!url) return null;
|
|
4979
|
+
const tokenSource = createAuthTokenSourceFromEnv(
|
|
4980
|
+
process.env,
|
|
4981
|
+
fetchFn,
|
|
4982
|
+
() => readStoredCredential(process.env)
|
|
4983
|
+
);
|
|
4984
|
+
const token = await tokenSource?.getToken();
|
|
4985
|
+
if (!token) return null;
|
|
4986
|
+
const tenant_id = process.env["VO_TENANT_ID"]?.trim();
|
|
4987
|
+
if (tenant_id) return { url, token, tenant_id };
|
|
4988
|
+
const identity = await fetchCloudIdentity(url, token, fetchFn);
|
|
4989
|
+
if (!identity) return null;
|
|
4990
|
+
return { url, token, tenant_id: identity.tenant_id, operator_id: identity.operator_id };
|
|
4991
|
+
}
|
|
4992
|
+
async function tryCloudReportState(cloud, input, fetchFn = fetch) {
|
|
4582
4993
|
try {
|
|
4583
4994
|
const reportBody = {
|
|
4584
4995
|
context_used_pct: input.context_used_pct
|
|
@@ -4591,7 +5002,7 @@ async function tryCloudReportState(cloud, input) {
|
|
|
4591
5002
|
reportBody["recent_tool_uses"] = input.recent_tool_uses;
|
|
4592
5003
|
}
|
|
4593
5004
|
const reportUrl = `${cloud.url}/api/v1/session/${input.session_id}/report-state`;
|
|
4594
|
-
let response = await
|
|
5005
|
+
let response = await fetchFn(reportUrl, {
|
|
4595
5006
|
method: "POST",
|
|
4596
5007
|
headers: {
|
|
4597
5008
|
"Content-Type": "application/json",
|
|
@@ -4601,7 +5012,7 @@ async function tryCloudReportState(cloud, input) {
|
|
|
4601
5012
|
});
|
|
4602
5013
|
if (response.status === 404) {
|
|
4603
5014
|
const allocateBody = {
|
|
4604
|
-
operator_id: input.operator_id,
|
|
5015
|
+
operator_id: cloud.operator_id ?? input.operator_id,
|
|
4605
5016
|
tenant_id: cloud.tenant_id,
|
|
4606
5017
|
agent_type: input.agent_type,
|
|
4607
5018
|
current_goal: input.current_goal ?? "Interactive session"
|
|
@@ -4610,7 +5021,7 @@ async function tryCloudReportState(cloud, input) {
|
|
|
4610
5021
|
allocateBody["initial_context_used_pct"] = input.context_used_pct;
|
|
4611
5022
|
}
|
|
4612
5023
|
const allocateUrl = `${cloud.url}/api/v1/session`;
|
|
4613
|
-
const allocateResponse = await
|
|
5024
|
+
const allocateResponse = await fetchFn(allocateUrl, {
|
|
4614
5025
|
method: "POST",
|
|
4615
5026
|
headers: {
|
|
4616
5027
|
"Content-Type": "application/json",
|
|
@@ -4621,7 +5032,10 @@ async function tryCloudReportState(cloud, input) {
|
|
|
4621
5032
|
if (!allocateResponse.ok) {
|
|
4622
5033
|
return null;
|
|
4623
5034
|
}
|
|
4624
|
-
|
|
5035
|
+
const allocateData = await allocateResponse.json();
|
|
5036
|
+
const retrySessionId = typeof allocateData.session?.session_id === "string" && allocateData.session.session_id.length > 0 ? allocateData.session.session_id : input.session_id;
|
|
5037
|
+
const retryReportUrl = `${cloud.url}/api/v1/session/${retrySessionId}/report-state`;
|
|
5038
|
+
response = await fetchFn(retryReportUrl, {
|
|
4625
5039
|
method: "POST",
|
|
4626
5040
|
headers: {
|
|
4627
5041
|
"Content-Type": "application/json",
|
|
@@ -4660,7 +5074,7 @@ async function handleReportSessionState(deps, rawInput, _signal) {
|
|
|
4660
5074
|
`invalid input. Required fields: operator_id (non-empty string), session_id (non-empty string), agent_type (one of: ${VALID_AGENT_TYPES.join(" | ")}), context_used_pct (number 0-100). Optional: current_goal (string \u2264${MAX_GOAL_CHARS} chars), recent_files_touched (string[] \u2264${MAX_RECENT_FILES}), recent_tool_uses (string[] \u2264${MAX_RECENT_TOOLS}).`
|
|
4661
5075
|
);
|
|
4662
5076
|
}
|
|
4663
|
-
const cloud = getCloudConfig();
|
|
5077
|
+
const cloud = await getCloudConfig();
|
|
4664
5078
|
if (cloud !== null) {
|
|
4665
5079
|
const cloudPayload = await tryCloudReportState(cloud, rawInput);
|
|
4666
5080
|
if (cloudPayload !== null) {
|
|
@@ -4686,9 +5100,324 @@ async function handleReportSessionState(deps, rawInput, _signal) {
|
|
|
4686
5100
|
|
|
4687
5101
|
// src/tools/session/spawn-successor.ts
|
|
4688
5102
|
import { spawn } from "node:child_process";
|
|
5103
|
+
import { homedir as homedir5 } from "node:os";
|
|
5104
|
+
import { join as join7 } from "node:path";
|
|
5105
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync4, openSync as openSync2, readFileSync as readFileSync7, readdirSync as readdirSync3, statSync as statSync3 } from "node:fs";
|
|
5106
|
+
|
|
5107
|
+
// src/swarm/tier-binding.ts
|
|
5108
|
+
var SWARM_TIERS = Object.freeze([
|
|
5109
|
+
"tier1_subscription",
|
|
5110
|
+
"tier1_local",
|
|
5111
|
+
"tier2_user_key",
|
|
5112
|
+
"tier3_platform_key",
|
|
5113
|
+
"refused",
|
|
5114
|
+
"unresolved"
|
|
5115
|
+
]);
|
|
5116
|
+
var TIER_ADMITS_SPAWN = /* @__PURE__ */ new Set([
|
|
5117
|
+
"tier1_subscription",
|
|
5118
|
+
"tier1_local",
|
|
5119
|
+
"tier2_user_key",
|
|
5120
|
+
"tier3_platform_key"
|
|
5121
|
+
]);
|
|
5122
|
+
var SWARM_TIER_BINDING_ENV = "VO_SWARM_TIER_BINDING";
|
|
5123
|
+
var MAX_BOUND_SUBAGENTS = 20;
|
|
5124
|
+
function isPositiveCap(cap) {
|
|
5125
|
+
return typeof cap === "number" && Number.isFinite(cap) && cap > 0;
|
|
5126
|
+
}
|
|
5127
|
+
function unresolvedBinding(swarmId, nowIso, reason) {
|
|
5128
|
+
return {
|
|
5129
|
+
schema_version: 1,
|
|
5130
|
+
swarm_id: swarmId,
|
|
5131
|
+
tier: "unresolved",
|
|
5132
|
+
agent: null,
|
|
5133
|
+
reason,
|
|
5134
|
+
exhausted_agents: [],
|
|
5135
|
+
subagent_budget: 0,
|
|
5136
|
+
spend_cap_usd: null,
|
|
5137
|
+
resolved_at: nowIso
|
|
5138
|
+
};
|
|
5139
|
+
}
|
|
5140
|
+
function serializeSwarmTierBinding(binding) {
|
|
5141
|
+
return JSON.stringify(binding);
|
|
5142
|
+
}
|
|
5143
|
+
function parseSwarmTierBinding(raw, nowIso) {
|
|
5144
|
+
if (typeof raw !== "string" || raw.trim().length === 0) {
|
|
5145
|
+
return unresolvedBinding("", nowIso, "no swarm tier binding present in the environment");
|
|
5146
|
+
}
|
|
5147
|
+
let parsed;
|
|
5148
|
+
try {
|
|
5149
|
+
parsed = JSON.parse(raw);
|
|
5150
|
+
} catch {
|
|
5151
|
+
return unresolvedBinding("", nowIso, "swarm tier binding is not valid JSON");
|
|
5152
|
+
}
|
|
5153
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
5154
|
+
return unresolvedBinding("", nowIso, "swarm tier binding is not an object");
|
|
5155
|
+
}
|
|
5156
|
+
const o = parsed;
|
|
5157
|
+
const swarmId = typeof o["swarm_id"] === "string" ? o["swarm_id"] : "";
|
|
5158
|
+
if (o["schema_version"] !== 1) {
|
|
5159
|
+
return unresolvedBinding(swarmId, nowIso, "swarm tier binding has an unsupported schema_version");
|
|
5160
|
+
}
|
|
5161
|
+
const tier = o["tier"];
|
|
5162
|
+
if (typeof tier !== "string" || !SWARM_TIERS.includes(tier)) {
|
|
5163
|
+
return unresolvedBinding(swarmId, nowIso, "swarm tier binding names an unknown tier");
|
|
5164
|
+
}
|
|
5165
|
+
const budget = o["subagent_budget"];
|
|
5166
|
+
const cap = o["spend_cap_usd"];
|
|
5167
|
+
const capNum = isPositiveCap(cap) ? cap : null;
|
|
5168
|
+
if (tier === "tier3_platform_key" && capNum === null) {
|
|
5169
|
+
return unresolvedBinding(
|
|
5170
|
+
swarmId,
|
|
5171
|
+
nowIso,
|
|
5172
|
+
"inherited tier3_platform_key binding carries no positive numeric spend cap \u2014 refusing an uncapped platform-billed fan-out"
|
|
5173
|
+
);
|
|
5174
|
+
}
|
|
5175
|
+
return {
|
|
5176
|
+
schema_version: 1,
|
|
5177
|
+
swarm_id: swarmId,
|
|
5178
|
+
tier,
|
|
5179
|
+
agent: typeof o["agent"] === "string" ? o["agent"] : null,
|
|
5180
|
+
reason: typeof o["reason"] === "string" ? o["reason"] : "inherited binding carried no reason",
|
|
5181
|
+
exhausted_agents: Array.isArray(o["exhausted_agents"]) ? o["exhausted_agents"].filter((v) => typeof v === "string") : [],
|
|
5182
|
+
subagent_budget: typeof budget === "number" && Number.isFinite(budget) && budget > 0 ? Math.min(Math.floor(budget), MAX_BOUND_SUBAGENTS) : 0,
|
|
5183
|
+
spend_cap_usd: capNum,
|
|
5184
|
+
resolved_at: typeof o["resolved_at"] === "string" ? o["resolved_at"] : nowIso
|
|
5185
|
+
};
|
|
5186
|
+
}
|
|
5187
|
+
function inheritSwarmTierBinding(env, nowIso) {
|
|
5188
|
+
return parseSwarmTierBinding(env[SWARM_TIER_BINDING_ENV], nowIso);
|
|
5189
|
+
}
|
|
5190
|
+
function bindingEnvFragment(binding) {
|
|
5191
|
+
return { [SWARM_TIER_BINDING_ENV]: serializeSwarmTierBinding(binding) };
|
|
5192
|
+
}
|
|
5193
|
+
function childBindingEnvFragment(binding, allocatedCapUsd = null) {
|
|
5194
|
+
return bindingEnvFragment(childBinding(binding, allocatedCapUsd));
|
|
5195
|
+
}
|
|
5196
|
+
function admitSubagentSpawn(binding, spawnsSoFar = 0) {
|
|
5197
|
+
if (!TIER_ADMITS_SPAWN.has(binding.tier)) {
|
|
5198
|
+
return { allowed: false, reason: `tier '${binding.tier}' admits no spawn: ${binding.reason}` };
|
|
5199
|
+
}
|
|
5200
|
+
if (binding.tier === "tier3_platform_key" && !isPositiveCap(binding.spend_cap_usd)) {
|
|
5201
|
+
return {
|
|
5202
|
+
allowed: false,
|
|
5203
|
+
reason: `swarm ${binding.swarm_id} is tier3_platform_key with no positive spend cap \u2014 refusing to spend the platform owner's money uncapped`
|
|
5204
|
+
};
|
|
5205
|
+
}
|
|
5206
|
+
if (!Number.isFinite(spawnsSoFar) || spawnsSoFar < 0) {
|
|
5207
|
+
return { allowed: false, reason: "spawn counter is not a finite non-negative number" };
|
|
5208
|
+
}
|
|
5209
|
+
if (spawnsSoFar >= binding.subagent_budget) {
|
|
5210
|
+
return {
|
|
5211
|
+
allowed: false,
|
|
5212
|
+
reason: `swarm ${binding.swarm_id} exhausted its bound subagent budget (${binding.subagent_budget})`
|
|
5213
|
+
};
|
|
5214
|
+
}
|
|
5215
|
+
return { allowed: true, reason: `admitted under tier '${binding.tier}'` };
|
|
5216
|
+
}
|
|
5217
|
+
function childBinding(binding, allocatedCapUsd = null) {
|
|
5218
|
+
const allocated = isPositiveCap(allocatedCapUsd) ? allocatedCapUsd : null;
|
|
5219
|
+
const parentCap = isPositiveCap(binding.spend_cap_usd) ? binding.spend_cap_usd : null;
|
|
5220
|
+
return {
|
|
5221
|
+
...binding,
|
|
5222
|
+
subagent_budget: Math.max(0, binding.subagent_budget - 1),
|
|
5223
|
+
// A child never carries more than its parent, whatever the ledger says: a
|
|
5224
|
+
// forged or hand-edited pool cannot inflate a descendant above the binding
|
|
5225
|
+
// it descends from.
|
|
5226
|
+
spend_cap_usd: allocated === null || parentCap === null ? null : Math.min(allocated, parentCap)
|
|
5227
|
+
};
|
|
5228
|
+
}
|
|
5229
|
+
function agentBindingRefusal(binding, requestedAgent) {
|
|
5230
|
+
const requested = typeof requestedAgent === "string" ? requestedAgent.trim() : "";
|
|
5231
|
+
if (requested.length === 0) return null;
|
|
5232
|
+
if (binding.agent !== null && requested === binding.agent) return null;
|
|
5233
|
+
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)`;
|
|
5234
|
+
}
|
|
5235
|
+
|
|
5236
|
+
// src/swarm/successor-launch.ts
|
|
5237
|
+
var AGENT_LAUNCH_SHAPES = Object.freeze({
|
|
5238
|
+
claude: {
|
|
5239
|
+
bin: "claude",
|
|
5240
|
+
baseArgs: ["-p", "--permission-mode", "acceptEdits"],
|
|
5241
|
+
enforcesMaxTurns: true,
|
|
5242
|
+
maxTurnsFlag: "--max-turns",
|
|
5243
|
+
windowsShellSafe: true
|
|
5244
|
+
},
|
|
5245
|
+
codex: {
|
|
5246
|
+
bin: "codex",
|
|
5247
|
+
baseArgs: ["exec", "--json", "-c", 'approval_policy="never"', "--sandbox", "workspace-write", "--skip-git-repo-check"],
|
|
5248
|
+
enforcesMaxTurns: false,
|
|
5249
|
+
// `-` makes codex read the prompt from stdin (injection-safe), matching how
|
|
5250
|
+
// codex-runner.mjs already spawns it.
|
|
5251
|
+
trailingArgs: ["-"],
|
|
5252
|
+
// `approval_policy="never"` carries embedded quotes; cmd.exe re-parsing is
|
|
5253
|
+
// unverified, so win32 refuses rather than risking a mangled sandbox flag.
|
|
5254
|
+
windowsShellSafe: false
|
|
5255
|
+
}
|
|
5256
|
+
});
|
|
5257
|
+
function resolveSuccessorLaunch(input) {
|
|
5258
|
+
const agent = typeof input.agent === "string" ? input.agent.trim() : "";
|
|
5259
|
+
if (!agent) {
|
|
5260
|
+
return { ok: false, reason: "no agent bound for this spawn \u2014 refusing rather than defaulting to claude" };
|
|
5261
|
+
}
|
|
5262
|
+
const shape = AGENT_LAUNCH_SHAPES[agent];
|
|
5263
|
+
if (!shape) {
|
|
5264
|
+
const known = Object.keys(AGENT_LAUNCH_SHAPES).join(", ");
|
|
5265
|
+
return {
|
|
5266
|
+
ok: false,
|
|
5267
|
+
reason: `no known headless launch shape for agent '${agent}' (known: ${known}) \u2014 refusing rather than guessing its argv`
|
|
5268
|
+
};
|
|
5269
|
+
}
|
|
5270
|
+
const wantsMaxTurns = Number.isInteger(input.maxTurns) && input.maxTurns > 0;
|
|
5271
|
+
if (wantsMaxTurns && !shape.enforcesMaxTurns) {
|
|
5272
|
+
return {
|
|
5273
|
+
ok: false,
|
|
5274
|
+
reason: `agent '${agent}' cannot enforce a max_turns cap \u2014 refusing rather than spawning it unbounded`
|
|
5275
|
+
};
|
|
5276
|
+
}
|
|
5277
|
+
const platform = input.platform ?? process.platform;
|
|
5278
|
+
if (platform === "win32" && !shape.windowsShellSafe) {
|
|
5279
|
+
return {
|
|
5280
|
+
ok: false,
|
|
5281
|
+
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`
|
|
5282
|
+
};
|
|
5283
|
+
}
|
|
5284
|
+
const args = [...shape.baseArgs];
|
|
5285
|
+
if (wantsMaxTurns && shape.maxTurnsFlag) {
|
|
5286
|
+
args.push(shape.maxTurnsFlag, String(input.maxTurns));
|
|
5287
|
+
}
|
|
5288
|
+
if (shape.trailingArgs) args.push(...shape.trailingArgs);
|
|
5289
|
+
return { ok: true, agent, bin: shape.bin, args };
|
|
5290
|
+
}
|
|
5291
|
+
|
|
5292
|
+
// src/swarm/spawn-ledger.ts
|
|
5293
|
+
import { mkdirSync as mkdirSync3, openSync, closeSync, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
|
|
4689
5294
|
import { homedir as homedir4 } from "node:os";
|
|
4690
5295
|
import { join as join6 } from "node:path";
|
|
4691
|
-
|
|
5296
|
+
var SWARM_LEDGER_DIR_ENV = "VO_SWARM_LEDGER_DIR";
|
|
5297
|
+
function resolveLedgerDir(env) {
|
|
5298
|
+
const override = env[SWARM_LEDGER_DIR_ENV];
|
|
5299
|
+
if (typeof override === "string" && override.trim().length > 0) return override.trim();
|
|
5300
|
+
return join6(homedir4(), ".vo", "swarm-ledger");
|
|
5301
|
+
}
|
|
5302
|
+
function sanitizeSwarmId(raw) {
|
|
5303
|
+
if (typeof raw !== "string") return null;
|
|
5304
|
+
const id = raw.trim();
|
|
5305
|
+
if (id.length === 0 || id.length > 128) return null;
|
|
5306
|
+
if (!/^[A-Za-z0-9._-]+$/u.test(id)) return null;
|
|
5307
|
+
if (id === "." || id === "..") return null;
|
|
5308
|
+
return id;
|
|
5309
|
+
}
|
|
5310
|
+
var CEILING_FILE = "ceiling.json";
|
|
5311
|
+
function createExclusive(path3, contents) {
|
|
5312
|
+
let fd;
|
|
5313
|
+
try {
|
|
5314
|
+
fd = openSync(path3, "wx");
|
|
5315
|
+
} catch {
|
|
5316
|
+
return false;
|
|
5317
|
+
}
|
|
5318
|
+
try {
|
|
5319
|
+
writeFileSync3(fd, contents, "utf8");
|
|
5320
|
+
} finally {
|
|
5321
|
+
closeSync(fd);
|
|
5322
|
+
}
|
|
5323
|
+
return true;
|
|
5324
|
+
}
|
|
5325
|
+
function capToCents(cap) {
|
|
5326
|
+
return isPositiveCap(cap) ? Math.round(cap * 100) : 0;
|
|
5327
|
+
}
|
|
5328
|
+
function readOrRecordLedgerHead(swarmDir, proposedCeiling, proposedCapCents, nowIso) {
|
|
5329
|
+
const path3 = join6(swarmDir, CEILING_FILE);
|
|
5330
|
+
const head = JSON.stringify({
|
|
5331
|
+
ceiling: proposedCeiling,
|
|
5332
|
+
cap_cents: proposedCapCents,
|
|
5333
|
+
recorded_at: nowIso
|
|
5334
|
+
});
|
|
5335
|
+
if (createExclusive(path3, head)) {
|
|
5336
|
+
return { ceiling: proposedCeiling, capCents: proposedCapCents };
|
|
5337
|
+
}
|
|
5338
|
+
let parsed;
|
|
5339
|
+
try {
|
|
5340
|
+
parsed = JSON.parse(readFileSync6(path3, "utf8"));
|
|
5341
|
+
} catch {
|
|
5342
|
+
return null;
|
|
5343
|
+
}
|
|
5344
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
|
|
5345
|
+
const record = parsed;
|
|
5346
|
+
const recorded = record["ceiling"];
|
|
5347
|
+
if (typeof recorded !== "number" || !Number.isFinite(recorded) || recorded < 1) return null;
|
|
5348
|
+
const recordedCap = record["cap_cents"];
|
|
5349
|
+
const capCents = typeof recordedCap === "number" && Number.isFinite(recordedCap) && recordedCap > 0 ? Math.floor(recordedCap) : 0;
|
|
5350
|
+
return { ceiling: Math.min(Math.floor(recorded), MAX_BOUND_SUBAGENTS), capCents };
|
|
5351
|
+
}
|
|
5352
|
+
var claimSpawnSlot = ({ swarmId, proposedCeiling, proposedCapUsd, dir, nowIso }) => {
|
|
5353
|
+
const id = sanitizeSwarmId(swarmId);
|
|
5354
|
+
if (id === null) {
|
|
5355
|
+
return {
|
|
5356
|
+
ok: false,
|
|
5357
|
+
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`
|
|
5358
|
+
};
|
|
5359
|
+
}
|
|
5360
|
+
const proposed = Number.isFinite(proposedCeiling) ? Math.floor(proposedCeiling) : 0;
|
|
5361
|
+
if (proposed < 1) {
|
|
5362
|
+
return { ok: false, reason: `swarm '${id}' proposes a ceiling of ${proposed} \u2014 no allowance to claim` };
|
|
5363
|
+
}
|
|
5364
|
+
const swarmDir = join6(dir, id);
|
|
5365
|
+
try {
|
|
5366
|
+
mkdirSync3(swarmDir, { recursive: true });
|
|
5367
|
+
} catch (err) {
|
|
5368
|
+
return {
|
|
5369
|
+
ok: false,
|
|
5370
|
+
reason: `swarm '${id}' ledger directory is unwritable (${err instanceof Error ? err.message : String(err)}) \u2014 refusing rather than spawning uncounted`
|
|
5371
|
+
};
|
|
5372
|
+
}
|
|
5373
|
+
const wantedCents = capToCents(proposedCapUsd);
|
|
5374
|
+
const head = readOrRecordLedgerHead(swarmDir, Math.min(proposed, MAX_BOUND_SUBAGENTS), wantedCents, nowIso);
|
|
5375
|
+
if (head === null) {
|
|
5376
|
+
return { ok: false, reason: `swarm '${id}' ledger carries no readable ceiling \u2014 refusing rather than spawning uncounted` };
|
|
5377
|
+
}
|
|
5378
|
+
const { ceiling, capCents } = head;
|
|
5379
|
+
const shareCents = capCents > 0 ? Math.floor(capCents / ceiling) : 0;
|
|
5380
|
+
if (wantedCents > 0 && shareCents < 1) {
|
|
5381
|
+
return {
|
|
5382
|
+
ok: false,
|
|
5383
|
+
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`
|
|
5384
|
+
};
|
|
5385
|
+
}
|
|
5386
|
+
for (let slot = 0; slot < ceiling; slot++) {
|
|
5387
|
+
const debitedCents = shareCents;
|
|
5388
|
+
const remainingCents = capCents > 0 ? capCents - (slot + 1) * shareCents : 0;
|
|
5389
|
+
const claimed = createExclusive(
|
|
5390
|
+
join6(swarmDir, `slot-${slot}.json`),
|
|
5391
|
+
JSON.stringify({
|
|
5392
|
+
slot,
|
|
5393
|
+
ceiling,
|
|
5394
|
+
pid: process.pid,
|
|
5395
|
+
claimed_at: nowIso,
|
|
5396
|
+
// The debit record. Durable and atomic with the claim: this file is
|
|
5397
|
+
// created with O_EXCL, so exactly one claimant ever writes this line.
|
|
5398
|
+
cap_cents_pool: capCents,
|
|
5399
|
+
cap_cents_debited: debitedCents,
|
|
5400
|
+
cap_cents_remaining: remainingCents
|
|
5401
|
+
})
|
|
5402
|
+
);
|
|
5403
|
+
if (claimed) {
|
|
5404
|
+
return {
|
|
5405
|
+
ok: true,
|
|
5406
|
+
slot,
|
|
5407
|
+
ceiling,
|
|
5408
|
+
remaining: ceiling - slot - 1,
|
|
5409
|
+
capUsd: debitedCents > 0 ? debitedCents / 100 : null,
|
|
5410
|
+
capRemainingUsd: capCents > 0 ? remainingCents / 100 : null
|
|
5411
|
+
};
|
|
5412
|
+
}
|
|
5413
|
+
}
|
|
5414
|
+
return {
|
|
5415
|
+
ok: false,
|
|
5416
|
+
reason: `swarm '${id}' has spent its whole fan-out ceiling (${ceiling} spawns across every generation) \u2014 refusing`
|
|
5417
|
+
};
|
|
5418
|
+
};
|
|
5419
|
+
|
|
5420
|
+
// src/tools/session/spawn-successor.ts
|
|
4692
5421
|
var TOOL_NAME20 = "vo_spawn_successor";
|
|
4693
5422
|
var MAX_HANDOFF_BYTES = 64e3;
|
|
4694
5423
|
var inputSchema20 = {
|
|
@@ -4709,11 +5438,16 @@ var inputSchema20 = {
|
|
|
4709
5438
|
max_turns: {
|
|
4710
5439
|
type: "number",
|
|
4711
5440
|
description: "Optional --max-turns bound for the successor."
|
|
5441
|
+
},
|
|
5442
|
+
agent: {
|
|
5443
|
+
type: "string",
|
|
5444
|
+
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.`
|
|
4712
5445
|
}
|
|
4713
5446
|
},
|
|
4714
5447
|
required: [],
|
|
4715
5448
|
additionalProperties: false
|
|
4716
5449
|
};
|
|
5450
|
+
var RETIRED_COUNTER_INPUT = "spawns_so_far";
|
|
4717
5451
|
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.";
|
|
4718
5452
|
function isToolInput20(v) {
|
|
4719
5453
|
if (typeof v !== "object" || v === null) return false;
|
|
@@ -4722,12 +5456,18 @@ function isToolInput20(v) {
|
|
|
4722
5456
|
if (o["goal"] !== void 0 && typeof o["goal"] !== "string") return false;
|
|
4723
5457
|
if (o["cwd"] !== void 0 && typeof o["cwd"] !== "string") return false;
|
|
4724
5458
|
if (o["max_turns"] !== void 0 && typeof o["max_turns"] !== "number") return false;
|
|
5459
|
+
if (o["agent"] !== void 0 && typeof o["agent"] !== "string") return false;
|
|
4725
5460
|
return true;
|
|
4726
5461
|
}
|
|
4727
|
-
function
|
|
5462
|
+
function retiredCounterRefusal(v) {
|
|
5463
|
+
if (typeof v !== "object" || v === null) return null;
|
|
5464
|
+
if (!(RETIRED_COUNTER_INPUT in v)) return null;
|
|
5465
|
+
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.`;
|
|
5466
|
+
}
|
|
5467
|
+
function newestHandoff(dir = join7(homedir5(), ".vo", "handoffs")) {
|
|
4728
5468
|
try {
|
|
4729
|
-
const entries = readdirSync3(dir).filter((f) => f.endsWith(".md")).map((f) => ({ f, m: statSync3(
|
|
4730
|
-
return entries.length > 0 && entries[0] ?
|
|
5469
|
+
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);
|
|
5470
|
+
return entries.length > 0 && entries[0] ? join7(dir, entries[0].f) : null;
|
|
4731
5471
|
} catch {
|
|
4732
5472
|
return null;
|
|
4733
5473
|
}
|
|
@@ -4743,7 +5483,7 @@ var MANDATORY_READS = [
|
|
|
4743
5483
|
function buildSuccessorPrompt(handoffMarkdown, goal) {
|
|
4744
5484
|
const reads = MANDATORY_READS.map((r, i) => ` ${i + 1}. ${r}`).join("\n");
|
|
4745
5485
|
const lines = [
|
|
4746
|
-
"You are the SUCCESSOR agent for
|
|
5486
|
+
"You are the SUCCESSOR agent for an AlgoHQ lane. The previous session",
|
|
4747
5487
|
"exhausted its context and wrote the handoff below. Read it fully, verify its",
|
|
4748
5488
|
'"verification needed" items against live state (a handoff is a claim, not',
|
|
4749
5489
|
"evidence \u2014 verify via `git show origin/main:<path>`), then continue the lane.",
|
|
@@ -4754,7 +5494,7 @@ function buildSuccessorPrompt(handoffMarkdown, goal) {
|
|
|
4754
5494
|
"NON-NEGOTIABLES: multi-model consensus verification is the core; test honesty",
|
|
4755
5495
|
"(verified-answer-only, no fake green); verify-before-act + human merge approval;",
|
|
4756
5496
|
"never a full functions-shared deploy; Gen2 only; work in a worktree on your own",
|
|
4757
|
-
"branch; finish line is MERGED + DEPLOYED + LIVE-VERIFIED, and
|
|
5497
|
+
"branch; finish line is MERGED + DEPLOYED + LIVE-VERIFIED, and AlgoHQ changes update",
|
|
4758
5498
|
"the roadmap in the same PR.",
|
|
4759
5499
|
"",
|
|
4760
5500
|
"--- HANDOFF ---",
|
|
@@ -4771,9 +5511,87 @@ function buildSuccessorArgs(maxTurns) {
|
|
|
4771
5511
|
}
|
|
4772
5512
|
return args;
|
|
4773
5513
|
}
|
|
5514
|
+
function resolveSpawnPlan(env, input, nowIso, platform = process.platform, claim = claimSpawnSlot) {
|
|
5515
|
+
const rawBinding = env[SWARM_TIER_BINDING_ENV];
|
|
5516
|
+
const hasBinding = typeof rawBinding === "string" && rawBinding.trim().length > 0;
|
|
5517
|
+
if (!hasBinding) {
|
|
5518
|
+
const explicit = input.agent?.trim();
|
|
5519
|
+
if (explicit) {
|
|
5520
|
+
const resolved2 = resolveSuccessorLaunch({ agent: explicit, maxTurns: input.max_turns, platform });
|
|
5521
|
+
if (!resolved2.ok) return { ok: false, reason: resolved2.reason, tier: "unbound" };
|
|
5522
|
+
return {
|
|
5523
|
+
ok: true,
|
|
5524
|
+
bin: resolved2.bin,
|
|
5525
|
+
args: resolved2.args,
|
|
5526
|
+
agent: resolved2.agent,
|
|
5527
|
+
tier: "unbound",
|
|
5528
|
+
bound: false,
|
|
5529
|
+
env: {},
|
|
5530
|
+
slot: null,
|
|
5531
|
+
capUsd: null,
|
|
5532
|
+
capRemainingUsd: null
|
|
5533
|
+
};
|
|
5534
|
+
}
|
|
5535
|
+
return {
|
|
5536
|
+
ok: true,
|
|
5537
|
+
bin: "claude",
|
|
5538
|
+
args: buildSuccessorArgs(input.max_turns),
|
|
5539
|
+
agent: "claude",
|
|
5540
|
+
tier: "unbound",
|
|
5541
|
+
bound: false,
|
|
5542
|
+
env: {},
|
|
5543
|
+
slot: null,
|
|
5544
|
+
capUsd: null,
|
|
5545
|
+
capRemainingUsd: null
|
|
5546
|
+
};
|
|
5547
|
+
}
|
|
5548
|
+
const binding = inheritSwarmTierBinding(env, nowIso);
|
|
5549
|
+
const admission = admitSubagentSpawn(binding);
|
|
5550
|
+
if (!admission.allowed) {
|
|
5551
|
+
return { ok: false, reason: admission.reason, tier: binding.tier };
|
|
5552
|
+
}
|
|
5553
|
+
const agentRefusal = agentBindingRefusal(binding, input.agent);
|
|
5554
|
+
if (agentRefusal !== null) return { ok: false, reason: agentRefusal, tier: binding.tier };
|
|
5555
|
+
const resolved = resolveSuccessorLaunch({
|
|
5556
|
+
agent: binding.agent,
|
|
5557
|
+
maxTurns: input.max_turns,
|
|
5558
|
+
platform
|
|
5559
|
+
});
|
|
5560
|
+
if (!resolved.ok) return { ok: false, reason: resolved.reason, tier: binding.tier };
|
|
5561
|
+
const slot = claim({
|
|
5562
|
+
swarmId: binding.swarm_id,
|
|
5563
|
+
proposedCeiling: binding.subagent_budget,
|
|
5564
|
+
// The spend-cap POOL, recorded once per swarm exactly like the ceiling. The
|
|
5565
|
+
// child's cap is DEBITED from it below, not recomputed from this binding.
|
|
5566
|
+
proposedCapUsd: binding.spend_cap_usd,
|
|
5567
|
+
dir: resolveLedgerDir(env),
|
|
5568
|
+
nowIso
|
|
5569
|
+
});
|
|
5570
|
+
if (!slot.ok) return { ok: false, reason: slot.reason, tier: binding.tier };
|
|
5571
|
+
return {
|
|
5572
|
+
ok: true,
|
|
5573
|
+
bin: resolved.bin,
|
|
5574
|
+
args: resolved.args,
|
|
5575
|
+
agent: resolved.agent,
|
|
5576
|
+
tier: binding.tier,
|
|
5577
|
+
bound: true,
|
|
5578
|
+
// Re-export the same TIER with a DECREMENTED budget and the spend cap the
|
|
5579
|
+
// ledger just DEBITED. Exporting the binding verbatim (what this did before
|
|
5580
|
+
// #9312) meant the child re-read the full budget and every generation
|
|
5581
|
+
// restarted at zero. Recomputing the cap from THIS binding (what #9312 did)
|
|
5582
|
+
// bounded a chain but not a tree: three siblings each re-halved the parent's
|
|
5583
|
+
// untouched $50 and walked away with $75 between them.
|
|
5584
|
+
env: childBindingEnvFragment(binding, slot.capUsd),
|
|
5585
|
+
slot: slot.slot,
|
|
5586
|
+
capUsd: slot.capUsd,
|
|
5587
|
+
capRemainingUsd: slot.capRemainingUsd
|
|
5588
|
+
};
|
|
5589
|
+
}
|
|
4774
5590
|
async function handleSpawnSuccessor(_deps, rawInput, _signal, spawnImpl = spawn) {
|
|
5591
|
+
const retired = retiredCounterRefusal(rawInput);
|
|
5592
|
+
if (retired !== null) throw invalidParams(TOOL_NAME20, retired);
|
|
4775
5593
|
if (!isToolInput20(rawInput)) {
|
|
4776
|
-
throw invalidParams(TOOL_NAME20, "invalid input. Optional: { handoff_path, goal, cwd, max_turns }.");
|
|
5594
|
+
throw invalidParams(TOOL_NAME20, "invalid input. Optional: { handoff_path, goal, cwd, max_turns, agent }.");
|
|
4777
5595
|
}
|
|
4778
5596
|
const handoffPath = rawInput.handoff_path?.trim() || newestHandoff();
|
|
4779
5597
|
if (!handoffPath || !existsSync4(handoffPath)) {
|
|
@@ -4786,20 +5604,37 @@ async function handleSpawnSuccessor(_deps, rawInput, _signal, spawnImpl = spawn)
|
|
|
4786
5604
|
}
|
|
4787
5605
|
});
|
|
4788
5606
|
}
|
|
4789
|
-
const handoff =
|
|
5607
|
+
const handoff = readFileSync7(handoffPath, "utf8").slice(0, MAX_HANDOFF_BYTES);
|
|
4790
5608
|
const prompt = buildSuccessorPrompt(handoff, rawInput.goal);
|
|
4791
|
-
const
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
5609
|
+
const plan = resolveSpawnPlan(process.env, rawInput, (/* @__PURE__ */ new Date()).toISOString());
|
|
5610
|
+
if (!plan.ok) {
|
|
5611
|
+
return jsonContent({
|
|
5612
|
+
tool: TOOL_NAME20,
|
|
5613
|
+
schema_version: 1,
|
|
5614
|
+
payload: {
|
|
5615
|
+
spawned: false,
|
|
5616
|
+
reason: `swarm tier binding refused this spawn: ${plan.reason}`,
|
|
5617
|
+
tier: plan.tier,
|
|
5618
|
+
handoff_path: handoffPath
|
|
5619
|
+
}
|
|
5620
|
+
});
|
|
5621
|
+
}
|
|
5622
|
+
const logDir = process.env["VO_MCP_SUCCESSOR_LOG_DIR"]?.trim() || join7(homedir5(), ".vo", "successors");
|
|
5623
|
+
mkdirSync4(logDir, { recursive: true });
|
|
5624
|
+
const logPath = join7(logDir, `successor-${Date.now()}.log`);
|
|
5625
|
+
const logFd = openSync2(logPath, "a");
|
|
5626
|
+
const child = spawnImpl(plan.bin, [...plan.args], {
|
|
4796
5627
|
cwd: rawInput.cwd?.trim() || process.cwd(),
|
|
4797
5628
|
detached: true,
|
|
4798
5629
|
stdio: ["pipe", logFd, logFd],
|
|
4799
|
-
// Windows:
|
|
4800
|
-
// goes via STDIN below, never argv, so the shell never sees it.
|
|
5630
|
+
// Windows: the agent CLIs are .cmd shims — they need a shell to resolve.
|
|
5631
|
+
// The prompt goes via STDIN below, never argv, so the shell never sees it.
|
|
4801
5632
|
shell: process.platform === "win32",
|
|
4802
|
-
windowsHide: true
|
|
5633
|
+
windowsHide: true,
|
|
5634
|
+
// Carry the SAME binding to the child. Without this the successor inherits
|
|
5635
|
+
// no tier and re-resolves its own — which is the split-payer defect one
|
|
5636
|
+
// generation down.
|
|
5637
|
+
...plan.bound ? { env: { ...process.env, ...plan.env } } : {}
|
|
4803
5638
|
});
|
|
4804
5639
|
let spawnError = null;
|
|
4805
5640
|
child.on("error", (e) => {
|
|
@@ -4815,7 +5650,20 @@ async function handleSpawnSuccessor(_deps, rawInput, _signal, spawnImpl = spawn)
|
|
|
4815
5650
|
return jsonContent({
|
|
4816
5651
|
tool: TOOL_NAME20,
|
|
4817
5652
|
schema_version: 1,
|
|
4818
|
-
payload: spawnError ? { spawned: false, reason: `spawn failed: ${spawnError}`,
|
|
5653
|
+
payload: spawnError ? { spawned: false, reason: `spawn failed: ${spawnError}`, agent: plan.agent, tier: plan.tier, handoff_path: handoffPath } : {
|
|
5654
|
+
spawned: true,
|
|
5655
|
+
pid: child.pid ?? null,
|
|
5656
|
+
log_path: logPath,
|
|
5657
|
+
handoff_path: handoffPath,
|
|
5658
|
+
agent: plan.agent,
|
|
5659
|
+
tier: plan.tier,
|
|
5660
|
+
tier_bound: plan.bound,
|
|
5661
|
+
ledger_slot: plan.slot,
|
|
5662
|
+
// The debit, surfaced so an operator can reconcile a fan-out's spend
|
|
5663
|
+
// against the pool without reading the ledger directory by hand.
|
|
5664
|
+
ledger_cap_usd: plan.capUsd,
|
|
5665
|
+
ledger_cap_remaining_usd: plan.capRemainingUsd
|
|
5666
|
+
}
|
|
4819
5667
|
});
|
|
4820
5668
|
}
|
|
4821
5669
|
|
|
@@ -4901,49 +5749,294 @@ async function handleConciergeDispatch(deps, rawInput, _signal) {
|
|
|
4901
5749
|
}
|
|
4902
5750
|
|
|
4903
5751
|
// src/tools/memory/sync-config.ts
|
|
4904
|
-
import {
|
|
4905
|
-
import {
|
|
4906
|
-
import {
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
const o = v;
|
|
4928
|
-
if (o["action"] !== "pull" && o["action"] !== "push") return false;
|
|
4929
|
-
if (o["cwd"] !== void 0 && typeof o["cwd"] !== "string") return false;
|
|
4930
|
-
return true;
|
|
5752
|
+
import { existsSync as existsSync8 } from "node:fs";
|
|
5753
|
+
import { homedir as homedir7 } from "node:os";
|
|
5754
|
+
import { join as join11 } from "node:path";
|
|
5755
|
+
|
|
5756
|
+
// src/tools/memory/memory-sync-http.ts
|
|
5757
|
+
init_safe_memory_file();
|
|
5758
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync6, readdirSync as readdirSync4 } from "node:fs";
|
|
5759
|
+
|
|
5760
|
+
// src/tools/memory/sync-lock.ts
|
|
5761
|
+
import { closeSync as closeSync2, mkdirSync as mkdirSync5, openSync as openSync3, readFileSync as readFileSync9, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "node:fs";
|
|
5762
|
+
import { hostname } from "node:os";
|
|
5763
|
+
import { join as join8 } from "node:path";
|
|
5764
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
5765
|
+
|
|
5766
|
+
// src/tools/memory/sync-lock-liveness.ts
|
|
5767
|
+
import { statSync as statSync4, readFileSync as readFileSync8 } from "node:fs";
|
|
5768
|
+
function defaultIsProcessAlive(pid) {
|
|
5769
|
+
try {
|
|
5770
|
+
process.kill(pid, 0);
|
|
5771
|
+
return true;
|
|
5772
|
+
} catch (err) {
|
|
5773
|
+
return err.code === "EPERM";
|
|
5774
|
+
}
|
|
4931
5775
|
}
|
|
4932
|
-
function
|
|
4933
|
-
|
|
5776
|
+
function toPayload(parsed) {
|
|
5777
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
|
|
5778
|
+
const record = parsed;
|
|
5779
|
+
const token = record["token"];
|
|
5780
|
+
const host = record["hostname"];
|
|
5781
|
+
if (typeof token !== "string" || token.length === 0) return null;
|
|
5782
|
+
const pid = record["pid"];
|
|
5783
|
+
const acquiredAtMs = record["acquiredAtMs"];
|
|
5784
|
+
return {
|
|
5785
|
+
pid: typeof pid === "number" && Number.isInteger(pid) && pid > 0 ? pid : 0,
|
|
5786
|
+
hostname: typeof host === "string" ? host : "",
|
|
5787
|
+
sessionId: typeof record["sessionId"] === "string" ? record["sessionId"] : null,
|
|
5788
|
+
token,
|
|
5789
|
+
acquiredAt: typeof record["acquiredAt"] === "string" ? record["acquiredAt"] : "",
|
|
5790
|
+
acquiredAtMs: typeof acquiredAtMs === "number" && Number.isFinite(acquiredAtMs) ? acquiredAtMs : Number.NaN
|
|
5791
|
+
};
|
|
4934
5792
|
}
|
|
4935
|
-
function
|
|
4936
|
-
|
|
4937
|
-
|
|
5793
|
+
function readLockRecord(path3) {
|
|
5794
|
+
let raw;
|
|
5795
|
+
try {
|
|
5796
|
+
raw = readFileSync8(path3, "utf8");
|
|
5797
|
+
} catch {
|
|
5798
|
+
return null;
|
|
5799
|
+
}
|
|
5800
|
+
try {
|
|
5801
|
+
return { raw, payload: toPayload(JSON.parse(raw)) };
|
|
5802
|
+
} catch {
|
|
5803
|
+
return { raw, payload: null };
|
|
5804
|
+
}
|
|
5805
|
+
}
|
|
5806
|
+
function lockAgeMs(record, path3, nowMs) {
|
|
5807
|
+
let startedMs = Number.NaN;
|
|
5808
|
+
if (record.payload) {
|
|
5809
|
+
if (Number.isFinite(record.payload.acquiredAtMs)) {
|
|
5810
|
+
startedMs = record.payload.acquiredAtMs;
|
|
5811
|
+
} else if (record.payload.acquiredAt) {
|
|
5812
|
+
startedMs = Date.parse(record.payload.acquiredAt);
|
|
5813
|
+
}
|
|
5814
|
+
}
|
|
5815
|
+
if (!Number.isFinite(startedMs)) {
|
|
5816
|
+
try {
|
|
5817
|
+
startedMs = statSync4(path3).mtimeMs;
|
|
5818
|
+
} catch {
|
|
5819
|
+
return null;
|
|
5820
|
+
}
|
|
5821
|
+
}
|
|
5822
|
+
const age = nowMs - startedMs;
|
|
5823
|
+
return Number.isFinite(age) && age >= 0 ? age : null;
|
|
5824
|
+
}
|
|
5825
|
+
function classifyHolderLiveness(record, isProcessAlive, thisHost) {
|
|
5826
|
+
const payload = record.payload;
|
|
5827
|
+
if (payload === null) return "unknown";
|
|
5828
|
+
if (payload.pid <= 0) return "unknown";
|
|
5829
|
+
if (thisHost.length === 0) return "unknown";
|
|
5830
|
+
if (payload.hostname !== thisHost) return "unknown";
|
|
5831
|
+
return isProcessAlive(payload.pid) ? "alive" : "dead";
|
|
5832
|
+
}
|
|
5833
|
+
function isLockAbandoned(record, ageMs, ttlMs, isProcessAlive, thisHost) {
|
|
5834
|
+
const liveness = classifyHolderLiveness(record, isProcessAlive, thisHost);
|
|
5835
|
+
if (liveness === "alive") return false;
|
|
5836
|
+
if (liveness === "dead") return true;
|
|
5837
|
+
return ageMs !== null && ageMs > ttlMs;
|
|
5838
|
+
}
|
|
5839
|
+
|
|
5840
|
+
// src/tools/memory/sync-lock.ts
|
|
5841
|
+
var MEMORY_SYNC_LOCK_FILE = ".memory-sync.lock";
|
|
5842
|
+
var DEFAULT_LOCK_TTL_MS = 15 * 6e4;
|
|
5843
|
+
var DEFAULT_LOCK_WAIT_MS = 1e4;
|
|
5844
|
+
var INITIAL_BACKOFF_MS = 25;
|
|
5845
|
+
var MAX_BACKOFF_MS = 500;
|
|
5846
|
+
var BACKOFF_FACTOR = 1.6;
|
|
5847
|
+
function positiveOr(value, fallback) {
|
|
5848
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : fallback;
|
|
5849
|
+
}
|
|
5850
|
+
function createExclusive2(path3, contents) {
|
|
5851
|
+
let fd;
|
|
5852
|
+
try {
|
|
5853
|
+
fd = openSync3(path3, "wx");
|
|
5854
|
+
} catch (err) {
|
|
5855
|
+
const code = err.code;
|
|
5856
|
+
return { ok: false, exists: code === "EEXIST", message: err instanceof Error ? err.message : String(err) };
|
|
5857
|
+
}
|
|
5858
|
+
try {
|
|
5859
|
+
writeFileSync4(fd, contents, "utf8");
|
|
5860
|
+
} catch (err) {
|
|
5861
|
+
closeSync2(fd);
|
|
5862
|
+
try {
|
|
5863
|
+
unlinkSync2(path3);
|
|
5864
|
+
} catch {
|
|
5865
|
+
}
|
|
5866
|
+
return { ok: false, exists: false, message: err instanceof Error ? err.message : String(err) };
|
|
5867
|
+
}
|
|
5868
|
+
closeSync2(fd);
|
|
5869
|
+
return { ok: true };
|
|
5870
|
+
}
|
|
5871
|
+
function removeAbandoned(path3, expectedRaw) {
|
|
5872
|
+
let current;
|
|
5873
|
+
try {
|
|
5874
|
+
current = readFileSync9(path3, "utf8");
|
|
5875
|
+
} catch {
|
|
5876
|
+
return;
|
|
5877
|
+
}
|
|
5878
|
+
if (current !== expectedRaw) return;
|
|
5879
|
+
try {
|
|
5880
|
+
unlinkSync2(path3);
|
|
5881
|
+
} catch {
|
|
5882
|
+
}
|
|
4938
5883
|
}
|
|
5884
|
+
function makeRelease(path3, token) {
|
|
5885
|
+
let released = false;
|
|
5886
|
+
return () => {
|
|
5887
|
+
if (released) return;
|
|
5888
|
+
released = true;
|
|
5889
|
+
let raw;
|
|
5890
|
+
try {
|
|
5891
|
+
raw = readFileSync9(path3, "utf8");
|
|
5892
|
+
} catch {
|
|
5893
|
+
return;
|
|
5894
|
+
}
|
|
5895
|
+
let stillOurs;
|
|
5896
|
+
try {
|
|
5897
|
+
stillOurs = toPayload(JSON.parse(raw))?.token === token;
|
|
5898
|
+
} catch {
|
|
5899
|
+
stillOurs = false;
|
|
5900
|
+
}
|
|
5901
|
+
if (!stillOurs) return;
|
|
5902
|
+
try {
|
|
5903
|
+
unlinkSync2(path3);
|
|
5904
|
+
} catch {
|
|
5905
|
+
}
|
|
5906
|
+
};
|
|
5907
|
+
}
|
|
5908
|
+
function describeHolder(record) {
|
|
5909
|
+
const payload = record?.payload;
|
|
5910
|
+
if (!payload) return "an unreadable lock file";
|
|
5911
|
+
return `pid ${payload.pid} on ${payload.hostname || "(unknown host)"} (session ${payload.sessionId ?? "unknown"}, held since ${payload.acquiredAt || "unknown"})`;
|
|
5912
|
+
}
|
|
5913
|
+
async function acquireMemorySyncLock(options) {
|
|
5914
|
+
const waitMs = positiveOr(options.waitMs, DEFAULT_LOCK_WAIT_MS);
|
|
5915
|
+
const ttlMs = positiveOr(options.ttlMs, DEFAULT_LOCK_TTL_MS);
|
|
5916
|
+
const now = options.now ?? Date.now;
|
|
5917
|
+
const sleep = options.sleep ?? ((ms) => new Promise((resolve3) => {
|
|
5918
|
+
setTimeout(resolve3, ms);
|
|
5919
|
+
}));
|
|
5920
|
+
const isProcessAlive = options.isProcessAlive ?? defaultIsProcessAlive;
|
|
5921
|
+
const thisHost = hostname();
|
|
5922
|
+
const path3 = join8(options.memoryDir, MEMORY_SYNC_LOCK_FILE);
|
|
5923
|
+
if (options.createDir === true) mkdirSync5(options.memoryDir, { recursive: true });
|
|
5924
|
+
const deadline = now() + waitMs;
|
|
5925
|
+
let backoffMs = INITIAL_BACKOFF_MS;
|
|
5926
|
+
let tookOverFrom = null;
|
|
5927
|
+
let holderDescription = "another session";
|
|
5928
|
+
for (; ; ) {
|
|
5929
|
+
const acquiredAtMs = now();
|
|
5930
|
+
const payload = {
|
|
5931
|
+
pid: process.pid,
|
|
5932
|
+
hostname: thisHost,
|
|
5933
|
+
sessionId: options.sessionId ?? null,
|
|
5934
|
+
token: randomUUID2(),
|
|
5935
|
+
acquiredAt: new Date(acquiredAtMs).toISOString(),
|
|
5936
|
+
acquiredAtMs
|
|
5937
|
+
};
|
|
5938
|
+
const created = createExclusive2(path3, `${JSON.stringify(payload, null, 2)}
|
|
5939
|
+
`);
|
|
5940
|
+
if (created.ok) {
|
|
5941
|
+
return { path: path3, payload, tookOverFrom, release: makeRelease(path3, payload.token) };
|
|
5942
|
+
}
|
|
5943
|
+
if (!created.exists) {
|
|
5944
|
+
throw new Error(
|
|
5945
|
+
`memory sync lock ${path3} could not be created (${created.message}) \u2014 refusing to sync without exclusion`
|
|
5946
|
+
);
|
|
5947
|
+
}
|
|
5948
|
+
const record = readLockRecord(path3);
|
|
5949
|
+
let reclaimed = false;
|
|
5950
|
+
if (record) {
|
|
5951
|
+
holderDescription = describeHolder(record);
|
|
5952
|
+
const age = lockAgeMs(record, path3, now());
|
|
5953
|
+
if (isLockAbandoned(record, age, ttlMs, isProcessAlive, thisHost)) {
|
|
5954
|
+
tookOverFrom = record.payload;
|
|
5955
|
+
removeAbandoned(path3, record.raw);
|
|
5956
|
+
reclaimed = true;
|
|
5957
|
+
}
|
|
5958
|
+
}
|
|
5959
|
+
if (now() >= deadline) {
|
|
5960
|
+
throw new Error(
|
|
5961
|
+
`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.`
|
|
5962
|
+
);
|
|
5963
|
+
}
|
|
5964
|
+
if (reclaimed) backoffMs = INITIAL_BACKOFF_MS;
|
|
5965
|
+
await sleep(Math.max(1, Math.min(backoffMs, deadline - now())));
|
|
5966
|
+
if (!reclaimed) backoffMs = Math.min(MAX_BACKOFF_MS, Math.ceil(backoffMs * BACKOFF_FACTOR));
|
|
5967
|
+
}
|
|
5968
|
+
}
|
|
5969
|
+
async function withMemorySyncLock(options, fn) {
|
|
5970
|
+
const handle = await acquireMemorySyncLock(options);
|
|
5971
|
+
try {
|
|
5972
|
+
return await fn(handle);
|
|
5973
|
+
} finally {
|
|
5974
|
+
handle.release();
|
|
5975
|
+
}
|
|
5976
|
+
}
|
|
5977
|
+
|
|
5978
|
+
// src/tools/memory/memory-index-merge.ts
|
|
5979
|
+
var MEMORY_INDEX_FILE = "MEMORY.md";
|
|
5980
|
+
function isMemoryIndexFile(fileName) {
|
|
5981
|
+
return fileName.toUpperCase() === MEMORY_INDEX_FILE.toUpperCase();
|
|
5982
|
+
}
|
|
5983
|
+
var INDEX_ROW_RE = /^\s*[-*]\s+\[[^\]]*\]\(([^)]+)\)/;
|
|
5984
|
+
function indexRowKey(line) {
|
|
5985
|
+
const match = INDEX_ROW_RE.exec(line);
|
|
5986
|
+
if (!match) return null;
|
|
5987
|
+
let target = match[1].trim();
|
|
5988
|
+
if (target.startsWith("<") && target.endsWith(">")) target = target.slice(1, -1).trim();
|
|
5989
|
+
target = target.replace(/\s+(["'])[\s\S]*\1$/, "").trim();
|
|
5990
|
+
if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(target)) {
|
|
5991
|
+
target = target.replace(/\\/g, "/").replace(/\/{2,}/g, "/");
|
|
5992
|
+
target = target.replace(/^(?:\.\/)+/, "");
|
|
5993
|
+
}
|
|
5994
|
+
return target.length > 0 ? target.toLowerCase() : null;
|
|
5995
|
+
}
|
|
5996
|
+
function mergeMemoryIndex(localContent, cloudContent) {
|
|
5997
|
+
if (typeof cloudContent !== "string" || cloudContent.trim().length === 0) {
|
|
5998
|
+
return { content: localContent, addedFromCloud: [] };
|
|
5999
|
+
}
|
|
6000
|
+
const eol = localContent.includes("\r\n") ? "\r\n" : "\n";
|
|
6001
|
+
const localLines = localContent.split(/\r?\n/);
|
|
6002
|
+
const localKeys = /* @__PURE__ */ new Set();
|
|
6003
|
+
let lastLocalRowIndex = -1;
|
|
6004
|
+
for (let i = 0; i < localLines.length; i++) {
|
|
6005
|
+
const key = indexRowKey(localLines[i]);
|
|
6006
|
+
if (key === null) continue;
|
|
6007
|
+
localKeys.add(key);
|
|
6008
|
+
lastLocalRowIndex = i;
|
|
6009
|
+
}
|
|
6010
|
+
const addedFromCloud = [];
|
|
6011
|
+
const seenCloudKeys = /* @__PURE__ */ new Set();
|
|
6012
|
+
for (const rawLine of cloudContent.split(/\r?\n/)) {
|
|
6013
|
+
const key = indexRowKey(rawLine);
|
|
6014
|
+
if (key === null) continue;
|
|
6015
|
+
if (localKeys.has(key) || seenCloudKeys.has(key)) continue;
|
|
6016
|
+
seenCloudKeys.add(key);
|
|
6017
|
+
addedFromCloud.push(rawLine.replace(/\r$/, ""));
|
|
6018
|
+
}
|
|
6019
|
+
if (addedFromCloud.length === 0) {
|
|
6020
|
+
return { content: localContent, addedFromCloud: [] };
|
|
6021
|
+
}
|
|
6022
|
+
const merged = lastLocalRowIndex >= 0 ? [...localLines.slice(0, lastLocalRowIndex + 1), ...addedFromCloud, ...localLines.slice(lastLocalRowIndex + 1)] : [...localLines, ...addedFromCloud];
|
|
6023
|
+
return { content: merged.join(eol), addedFromCloud };
|
|
6024
|
+
}
|
|
6025
|
+
|
|
6026
|
+
// src/tools/memory/memory-sync-http.ts
|
|
6027
|
+
init_bounded_sync();
|
|
6028
|
+
init_memory_push_cache();
|
|
4939
6029
|
async function pullMemory(controlPlaneUrl, token, memoryDir, fetchFn) {
|
|
4940
6030
|
const url = `${controlPlaneUrl}/api/v1/agent-config/memory/me`;
|
|
4941
|
-
const response = await
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
6031
|
+
const response = await withRequestTimeout(
|
|
6032
|
+
url,
|
|
6033
|
+
() => fetchFn(url, {
|
|
6034
|
+
method: "GET",
|
|
6035
|
+
headers: {
|
|
6036
|
+
authorization: `Bearer ${token}`
|
|
6037
|
+
}
|
|
6038
|
+
})
|
|
6039
|
+
);
|
|
4947
6040
|
if (response.status !== 200) {
|
|
4948
6041
|
const text = await response.text();
|
|
4949
6042
|
throw new Error(`GET /api/v1/agent-config/memory/me returned HTTP ${response.status}: ${text.slice(0, 200)}`);
|
|
@@ -4952,104 +6045,250 @@ async function pullMemory(controlPlaneUrl, token, memoryDir, fetchFn) {
|
|
|
4952
6045
|
if (!data.ok || !Array.isArray(data.entries)) {
|
|
4953
6046
|
throw new Error("GET /api/v1/agent-config/memory/me response missing ok=true or entries array");
|
|
4954
6047
|
}
|
|
4955
|
-
|
|
6048
|
+
const writes = data.entries.map((entry) => ({
|
|
6049
|
+
entry,
|
|
6050
|
+
filePath: resolveMemoryFilePath(memoryDir, entry.file_name)
|
|
6051
|
+
}));
|
|
6052
|
+
mkdirSync6(memoryDir, { recursive: true });
|
|
4956
6053
|
const files = [];
|
|
4957
|
-
for (const entry of
|
|
4958
|
-
|
|
4959
|
-
writeFileSync3(filePath, entry.content, "utf8");
|
|
6054
|
+
for (const { entry, filePath } of writes) {
|
|
6055
|
+
writeFileSync6(filePath, entry.content, "utf8");
|
|
4960
6056
|
files.push(entry.file_name);
|
|
4961
6057
|
}
|
|
4962
6058
|
return { pulled: data.entries.length, files };
|
|
4963
6059
|
}
|
|
4964
|
-
|
|
6060
|
+
function listPushableFiles(memoryDir) {
|
|
6061
|
+
return readdirSync4(memoryDir).filter((f) => f.endsWith(".md") && f !== MEMORY_SYNC_LOCK_FILE);
|
|
6062
|
+
}
|
|
6063
|
+
async function uploadOne(item, controlPlaneUrl, token, sessionId, fetchFn, deadline) {
|
|
6064
|
+
deadline.check();
|
|
6065
|
+
if (item.memoryId !== null) {
|
|
6066
|
+
const updateUrl = `${controlPlaneUrl}/api/v1/agent-config/memory/${item.memoryId}`;
|
|
6067
|
+
const updateBody = { content: item.content, session_id: sessionId };
|
|
6068
|
+
const updateResponse = await withRequestTimeout(
|
|
6069
|
+
updateUrl,
|
|
6070
|
+
() => fetchFn(updateUrl, {
|
|
6071
|
+
method: "PUT",
|
|
6072
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
6073
|
+
body: JSON.stringify(updateBody)
|
|
6074
|
+
})
|
|
6075
|
+
);
|
|
6076
|
+
if (updateResponse.status !== 200) {
|
|
6077
|
+
const text = await updateResponse.text();
|
|
6078
|
+
throw new Error(
|
|
6079
|
+
`PUT /api/v1/agent-config/memory/${item.memoryId} returned HTTP ${updateResponse.status}: ${text.slice(0, 200)}`
|
|
6080
|
+
);
|
|
6081
|
+
}
|
|
6082
|
+
const updateData = JSON.parse(await updateResponse.text());
|
|
6083
|
+
if (!updateData.ok) throw new Error(`PUT /api/v1/agent-config/memory/${item.memoryId} returned ok=false`);
|
|
6084
|
+
return "updated";
|
|
6085
|
+
}
|
|
6086
|
+
const createUrl = `${controlPlaneUrl}/api/v1/agent-config/memory/me`;
|
|
6087
|
+
const createBody = {
|
|
6088
|
+
entry_type: item.entryType,
|
|
6089
|
+
file_name: item.fileName,
|
|
6090
|
+
content: item.content,
|
|
6091
|
+
session_id: sessionId
|
|
6092
|
+
};
|
|
6093
|
+
const createResponse = await withRequestTimeout(
|
|
6094
|
+
createUrl,
|
|
6095
|
+
() => fetchFn(createUrl, {
|
|
6096
|
+
method: "POST",
|
|
6097
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
6098
|
+
body: JSON.stringify(createBody)
|
|
6099
|
+
})
|
|
6100
|
+
);
|
|
6101
|
+
if (createResponse.status !== 200 && createResponse.status !== 201) {
|
|
6102
|
+
const text = await createResponse.text();
|
|
6103
|
+
throw new Error(
|
|
6104
|
+
`POST /api/v1/agent-config/memory/me returned HTTP ${createResponse.status}: ${text.slice(0, 200)}`
|
|
6105
|
+
);
|
|
6106
|
+
}
|
|
6107
|
+
const createData = JSON.parse(await createResponse.text());
|
|
6108
|
+
if (!createData.ok) throw new Error("POST /api/v1/agent-config/memory/me returned ok=false");
|
|
6109
|
+
return "created";
|
|
6110
|
+
}
|
|
6111
|
+
async function pushMemory(controlPlaneUrl, token, memoryDir, sessionId, fetchFn, options = {}) {
|
|
6112
|
+
const empty = { pushed: 0, created: 0, updated: 0, skipped: 0, indexRowsPreserved: 0 };
|
|
4965
6113
|
if (!existsSync5(memoryDir)) {
|
|
4966
|
-
return
|
|
6114
|
+
return empty;
|
|
4967
6115
|
}
|
|
4968
|
-
const localFiles =
|
|
6116
|
+
const localFiles = listPushableFiles(memoryDir).map((f) => ({
|
|
4969
6117
|
file_name: f,
|
|
4970
|
-
content:
|
|
4971
|
-
entry_type: f
|
|
6118
|
+
content: readFileSync11(resolveMemoryFilePath(memoryDir, f), "utf8"),
|
|
6119
|
+
entry_type: isMemoryIndexFile(f) ? "index" : "topic"
|
|
4972
6120
|
}));
|
|
4973
6121
|
if (localFiles.length === 0) {
|
|
4974
|
-
return
|
|
6122
|
+
return empty;
|
|
4975
6123
|
}
|
|
6124
|
+
const deadline = options.deadline ?? createSyncDeadline();
|
|
6125
|
+
const cache = options.cache ?? readPushCache(memoryDir, controlPlaneUrl);
|
|
6126
|
+
deadline.check();
|
|
4976
6127
|
const getUrl = `${controlPlaneUrl}/api/v1/agent-config/memory/me`;
|
|
4977
|
-
const getResponse = await
|
|
4978
|
-
|
|
4979
|
-
headers: {
|
|
4980
|
-
|
|
4981
|
-
}
|
|
4982
|
-
});
|
|
6128
|
+
const getResponse = await withRequestTimeout(
|
|
6129
|
+
getUrl,
|
|
6130
|
+
() => fetchFn(getUrl, { method: "GET", headers: { authorization: `Bearer ${token}` } })
|
|
6131
|
+
);
|
|
4983
6132
|
const existingMap = /* @__PURE__ */ new Map();
|
|
4984
6133
|
if (getResponse.status === 200) {
|
|
4985
6134
|
const getData = JSON.parse(await getResponse.text());
|
|
4986
6135
|
if (getData.ok && Array.isArray(getData.entries)) {
|
|
4987
6136
|
for (const entry of getData.entries) {
|
|
4988
|
-
existingMap.set(entry.file_name,
|
|
6137
|
+
existingMap.set(entry.file_name, {
|
|
6138
|
+
memoryId: entry.memory_id,
|
|
6139
|
+
content: typeof entry.content === "string" ? entry.content : ""
|
|
6140
|
+
});
|
|
4989
6141
|
}
|
|
4990
6142
|
}
|
|
4991
6143
|
}
|
|
6144
|
+
const toUpload = [];
|
|
6145
|
+
let skipped = 0;
|
|
6146
|
+
for (const localFile of localFiles) {
|
|
6147
|
+
const existing = existingMap.get(localFile.file_name);
|
|
6148
|
+
let content = localFile.content;
|
|
6149
|
+
let rowsPreserved = 0;
|
|
6150
|
+
if (localFile.entry_type === "index") {
|
|
6151
|
+
const merged = mergeMemoryIndex(localFile.content, existing?.content);
|
|
6152
|
+
content = merged.content;
|
|
6153
|
+
rowsPreserved = merged.addedFromCloud.length;
|
|
6154
|
+
}
|
|
6155
|
+
const payloadHash = sha256(content);
|
|
6156
|
+
if (!needsMemoryPush(cache, localFile.file_name, payloadHash, existing !== void 0)) {
|
|
6157
|
+
skipped++;
|
|
6158
|
+
continue;
|
|
6159
|
+
}
|
|
6160
|
+
toUpload.push({
|
|
6161
|
+
fileName: localFile.file_name,
|
|
6162
|
+
entryType: localFile.entry_type,
|
|
6163
|
+
content,
|
|
6164
|
+
payloadHash,
|
|
6165
|
+
memoryId: existing?.memoryId ?? null,
|
|
6166
|
+
rowsPreserved
|
|
6167
|
+
});
|
|
6168
|
+
}
|
|
6169
|
+
const outcomes = await mapWithConcurrency(
|
|
6170
|
+
toUpload,
|
|
6171
|
+
options.concurrency ?? PUSH_CONCURRENCY,
|
|
6172
|
+
(item) => uploadOne(item, controlPlaneUrl, token, sessionId, fetchFn, deadline)
|
|
6173
|
+
);
|
|
4992
6174
|
let created = 0;
|
|
4993
6175
|
let updated = 0;
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
"content-type": "application/json"
|
|
5007
|
-
},
|
|
5008
|
-
body: JSON.stringify(updateBody)
|
|
5009
|
-
});
|
|
5010
|
-
if (updateResponse.status !== 200) {
|
|
5011
|
-
const text = await updateResponse.text();
|
|
5012
|
-
throw new Error(
|
|
5013
|
-
`PUT /api/v1/agent-config/memory/${memoryId} returned HTTP ${updateResponse.status}: ${text.slice(0, 200)}`
|
|
5014
|
-
);
|
|
5015
|
-
}
|
|
5016
|
-
const updateData = JSON.parse(await updateResponse.text());
|
|
5017
|
-
if (!updateData.ok) {
|
|
5018
|
-
throw new Error(`PUT /api/v1/agent-config/memory/${memoryId} returned ok=false`);
|
|
5019
|
-
}
|
|
5020
|
-
updated++;
|
|
5021
|
-
} else {
|
|
5022
|
-
const createUrl = `${controlPlaneUrl}/api/v1/agent-config/memory/me`;
|
|
5023
|
-
const createBody = {
|
|
5024
|
-
entry_type: localFile.entry_type,
|
|
5025
|
-
file_name: localFile.file_name,
|
|
5026
|
-
content: localFile.content,
|
|
5027
|
-
session_id: sessionId
|
|
5028
|
-
};
|
|
5029
|
-
const createResponse = await fetchFn(createUrl, {
|
|
5030
|
-
method: "POST",
|
|
5031
|
-
headers: {
|
|
5032
|
-
authorization: `Bearer ${token}`,
|
|
5033
|
-
"content-type": "application/json"
|
|
5034
|
-
},
|
|
5035
|
-
body: JSON.stringify(createBody)
|
|
5036
|
-
});
|
|
5037
|
-
if (createResponse.status !== 200 && createResponse.status !== 201) {
|
|
5038
|
-
const text = await createResponse.text();
|
|
5039
|
-
throw new Error(
|
|
5040
|
-
`POST /api/v1/agent-config/memory/me returned HTTP ${createResponse.status}: ${text.slice(0, 200)}`
|
|
5041
|
-
);
|
|
5042
|
-
}
|
|
5043
|
-
const createData = JSON.parse(await createResponse.text());
|
|
5044
|
-
if (!createData.ok) {
|
|
5045
|
-
throw new Error("POST /api/v1/agent-config/memory/me returned ok=false");
|
|
5046
|
-
}
|
|
5047
|
-
created++;
|
|
6176
|
+
let indexRowsPreserved = 0;
|
|
6177
|
+
let firstError;
|
|
6178
|
+
for (let i = 0; i < outcomes.length; i++) {
|
|
6179
|
+
const outcome = outcomes[i];
|
|
6180
|
+
const item = toUpload[i];
|
|
6181
|
+
if (outcome.ok) {
|
|
6182
|
+
if (outcome.value === "created") created++;
|
|
6183
|
+
else updated++;
|
|
6184
|
+
indexRowsPreserved += item.rowsPreserved;
|
|
6185
|
+
recordMemoryPush(cache, item.fileName, item.payloadHash);
|
|
6186
|
+
} else if (firstError === void 0) {
|
|
6187
|
+
firstError = outcome.error;
|
|
5048
6188
|
}
|
|
5049
6189
|
}
|
|
5050
|
-
|
|
6190
|
+
pruneMissing(cache, localFiles.map((f) => f.file_name));
|
|
6191
|
+
if (options.persistCache ?? options.cache === void 0) writePushCache(memoryDir, cache);
|
|
6192
|
+
if (firstError !== void 0) throw firstError;
|
|
6193
|
+
return { pushed: created + updated, created, updated, skipped, indexRowsPreserved };
|
|
5051
6194
|
}
|
|
5052
|
-
|
|
6195
|
+
|
|
6196
|
+
// src/tools/memory/sync-config.ts
|
|
6197
|
+
init_bounded_sync();
|
|
6198
|
+
init_memory_push_cache();
|
|
6199
|
+
|
|
6200
|
+
// src/tools/memory/sync-kill-switch.ts
|
|
6201
|
+
import { existsSync as existsSync6, readFileSync as readFileSync12 } from "node:fs";
|
|
6202
|
+
import { homedir as homedir6 } from "node:os";
|
|
6203
|
+
import { join as join10 } from "node:path";
|
|
6204
|
+
var MEMORY_SYNC_DISABLE_ENV = "VO_MCP_DISABLE_MEMORY_SYNC";
|
|
6205
|
+
var MEMORY_SYNC_DISABLE_SENTINEL = "vo-memory-sync-disabled";
|
|
6206
|
+
var NEGATIONS = /* @__PURE__ */ new Set(["0", "false", "no"]);
|
|
6207
|
+
var MAX_LOGGED_VALUE = 32;
|
|
6208
|
+
function memorySyncSentinelPath(home) {
|
|
6209
|
+
return join10(home, ".claude", MEMORY_SYNC_DISABLE_SENTINEL);
|
|
6210
|
+
}
|
|
6211
|
+
function isKillSwitchValueOn(raw) {
|
|
6212
|
+
if (raw === void 0 || raw === null) return false;
|
|
6213
|
+
const v = raw.trim().toLowerCase();
|
|
6214
|
+
if (v === "") return false;
|
|
6215
|
+
return !NEGATIONS.has(v);
|
|
6216
|
+
}
|
|
6217
|
+
function clip(raw) {
|
|
6218
|
+
const v = raw.trim();
|
|
6219
|
+
return v.length > MAX_LOGGED_VALUE ? `${v.slice(0, MAX_LOGGED_VALUE)}\u2026` : v;
|
|
6220
|
+
}
|
|
6221
|
+
function evaluateMemorySyncKillSwitch(deps = {}) {
|
|
6222
|
+
const env = deps.env ?? process.env;
|
|
6223
|
+
const home = deps.home ?? homedir6();
|
|
6224
|
+
const fileExists = deps.fileExists ?? existsSync6;
|
|
6225
|
+
const readFile3 = deps.readFile ?? ((p) => readFileSync12(p, "utf8"));
|
|
6226
|
+
const fired = [];
|
|
6227
|
+
const rawEnv = env[MEMORY_SYNC_DISABLE_ENV];
|
|
6228
|
+
if (isKillSwitchValueOn(rawEnv)) {
|
|
6229
|
+
fired.push(`env ${MEMORY_SYNC_DISABLE_ENV}=${clip(rawEnv)}`);
|
|
6230
|
+
}
|
|
6231
|
+
const sentinel = memorySyncSentinelPath(home);
|
|
6232
|
+
let sentinelPresent;
|
|
6233
|
+
try {
|
|
6234
|
+
sentinelPresent = fileExists(sentinel);
|
|
6235
|
+
} catch {
|
|
6236
|
+
sentinelPresent = false;
|
|
6237
|
+
}
|
|
6238
|
+
if (sentinelPresent) {
|
|
6239
|
+
let contents = "";
|
|
6240
|
+
let readable = true;
|
|
6241
|
+
try {
|
|
6242
|
+
contents = readFile3(sentinel);
|
|
6243
|
+
} catch {
|
|
6244
|
+
readable = false;
|
|
6245
|
+
}
|
|
6246
|
+
if (!readable || isKillSwitchValueOn(contents) || contents.trim() === "") {
|
|
6247
|
+
fired.push(`sentinel file ${sentinel}`);
|
|
6248
|
+
}
|
|
6249
|
+
}
|
|
6250
|
+
if (fired.length === 0) return { disabled: false, reason: null };
|
|
6251
|
+
return { disabled: true, reason: `memory sync DISABLED by ${fired.join(" + ")}` };
|
|
6252
|
+
}
|
|
6253
|
+
|
|
6254
|
+
// src/tools/memory/sync-config.ts
|
|
6255
|
+
var TOOL_NAME22 = "vo_sync_config";
|
|
6256
|
+
var inputSchema22 = {
|
|
6257
|
+
type: "object",
|
|
6258
|
+
properties: {
|
|
6259
|
+
action: {
|
|
6260
|
+
type: "string",
|
|
6261
|
+
enum: ["pull", "push"],
|
|
6262
|
+
description: "pull: download cloud memory to local files. push: upload local files to cloud."
|
|
6263
|
+
},
|
|
6264
|
+
cwd: {
|
|
6265
|
+
type: "string",
|
|
6266
|
+
description: "Working directory to derive project slug from (default: process.cwd())."
|
|
6267
|
+
}
|
|
6268
|
+
},
|
|
6269
|
+
required: ["action"],
|
|
6270
|
+
additionalProperties: false
|
|
6271
|
+
};
|
|
6272
|
+
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.";
|
|
6273
|
+
function isToolInput22(v) {
|
|
6274
|
+
if (typeof v !== "object" || v === null) return false;
|
|
6275
|
+
const o = v;
|
|
6276
|
+
if (o["action"] !== "pull" && o["action"] !== "push") return false;
|
|
6277
|
+
if (o["cwd"] !== void 0 && typeof o["cwd"] !== "string") return false;
|
|
6278
|
+
return true;
|
|
6279
|
+
}
|
|
6280
|
+
function deriveProjectSlug(cwd) {
|
|
6281
|
+
return cwd.replace(/([^:\\/])[\\/]+$/, "$1").replace(/\\/g, "/").replace(/^([a-zA-Z]):/, (_m, drive) => `${drive.toUpperCase()}:`).replace(/[^a-zA-Z0-9]/g, "-");
|
|
6282
|
+
}
|
|
6283
|
+
function getMemoryDir(cwd) {
|
|
6284
|
+
const slug = deriveProjectSlug(cwd);
|
|
6285
|
+
return join11(homedir7(), ".claude", "projects", slug, "memory");
|
|
6286
|
+
}
|
|
6287
|
+
async function runMemorySync(action, cwd, sessionId, fetchFn = globalThis.fetch, lockOptions = {}) {
|
|
6288
|
+
const killSwitch = evaluateMemorySyncKillSwitch();
|
|
6289
|
+
if (killSwitch.disabled) {
|
|
6290
|
+
return { synced: false, reason: killSwitch.reason };
|
|
6291
|
+
}
|
|
5053
6292
|
const controlPlaneUrl = process.env["VO_CONTROL_PLANE_URL"];
|
|
5054
6293
|
if (!controlPlaneUrl) {
|
|
5055
6294
|
return { synced: false, reason: "VO_CONTROL_PLANE_URL not set \u2014 cloud mode disabled" };
|
|
@@ -5066,20 +6305,79 @@ async function runMemorySync(action, cwd, sessionId, fetchFn = globalThis.fetch)
|
|
|
5066
6305
|
}
|
|
5067
6306
|
const memoryDir = getMemoryDir(cwd);
|
|
5068
6307
|
const baseUrl = controlPlaneUrl.replace(/\/+$/, "");
|
|
5069
|
-
|
|
5070
|
-
if (action === "pull") {
|
|
5071
|
-
const result2 = await pullMemory(baseUrl, token, memoryDir, fetchFn);
|
|
5072
|
-
return { synced: true, action: "pull", pulled: result2.pulled, files: result2.files, memory_dir: memoryDir };
|
|
5073
|
-
}
|
|
5074
|
-
const result = await pushMemory(baseUrl, token, memoryDir, sessionId, fetchFn);
|
|
6308
|
+
if (action === "push" && !existsSync8(memoryDir)) {
|
|
5075
6309
|
return {
|
|
5076
6310
|
synced: true,
|
|
5077
6311
|
action: "push",
|
|
5078
|
-
pushed:
|
|
5079
|
-
created:
|
|
5080
|
-
updated:
|
|
6312
|
+
pushed: 0,
|
|
6313
|
+
created: 0,
|
|
6314
|
+
updated: 0,
|
|
6315
|
+
skipped: 0,
|
|
6316
|
+
index_rows_preserved: 0,
|
|
6317
|
+
knowledge_upserted: 0,
|
|
6318
|
+
knowledge_failed: 0,
|
|
5081
6319
|
memory_dir: memoryDir
|
|
5082
6320
|
};
|
|
6321
|
+
}
|
|
6322
|
+
try {
|
|
6323
|
+
return await withMemorySyncLock({ ...lockOptions, memoryDir, sessionId, createDir: action === "pull" }, async (lock) => {
|
|
6324
|
+
const takeover = lock.tookOverFrom ? { lock_taken_over_from_pid: lock.tookOverFrom.pid } : {};
|
|
6325
|
+
if (action === "pull") {
|
|
6326
|
+
const result2 = await pullMemory(baseUrl, token, memoryDir, fetchFn);
|
|
6327
|
+
return {
|
|
6328
|
+
synced: true,
|
|
6329
|
+
action: "pull",
|
|
6330
|
+
pulled: result2.pulled,
|
|
6331
|
+
files: result2.files,
|
|
6332
|
+
memory_dir: memoryDir,
|
|
6333
|
+
...takeover
|
|
6334
|
+
};
|
|
6335
|
+
}
|
|
6336
|
+
const deadline = createSyncDeadline();
|
|
6337
|
+
const cache = readPushCache(memoryDir, baseUrl);
|
|
6338
|
+
let result;
|
|
6339
|
+
try {
|
|
6340
|
+
result = await pushMemory(baseUrl, token, memoryDir, sessionId, fetchFn, { cache, deadline });
|
|
6341
|
+
} catch (err) {
|
|
6342
|
+
writePushCache(memoryDir, cache);
|
|
6343
|
+
throw err;
|
|
6344
|
+
}
|
|
6345
|
+
let bridge;
|
|
6346
|
+
try {
|
|
6347
|
+
const { upsertMemoryFilesAsKnowledge: upsertMemoryFilesAsKnowledge2 } = await Promise.resolve().then(() => (init_memory_knowledge_bridge(), memory_knowledge_bridge_exports));
|
|
6348
|
+
bridge = await upsertMemoryFilesAsKnowledge2({
|
|
6349
|
+
controlPlaneUrl: baseUrl,
|
|
6350
|
+
token,
|
|
6351
|
+
memoryDir,
|
|
6352
|
+
fetchFn,
|
|
6353
|
+
cache,
|
|
6354
|
+
deadline
|
|
6355
|
+
});
|
|
6356
|
+
} catch (err) {
|
|
6357
|
+
bridge = {
|
|
6358
|
+
upserted: 0,
|
|
6359
|
+
failed: 1,
|
|
6360
|
+
skipped: 0,
|
|
6361
|
+
failures: [`bridge unavailable: ${err instanceof Error ? err.message : String(err)}`]
|
|
6362
|
+
};
|
|
6363
|
+
}
|
|
6364
|
+
writePushCache(memoryDir, cache);
|
|
6365
|
+
return {
|
|
6366
|
+
synced: true,
|
|
6367
|
+
action: "push",
|
|
6368
|
+
pushed: result.pushed,
|
|
6369
|
+
created: result.created,
|
|
6370
|
+
updated: result.updated,
|
|
6371
|
+
skipped: result.skipped,
|
|
6372
|
+
index_rows_preserved: result.indexRowsPreserved,
|
|
6373
|
+
memory_dir: memoryDir,
|
|
6374
|
+
knowledge_upserted: bridge.upserted,
|
|
6375
|
+
knowledge_failed: bridge.failed,
|
|
6376
|
+
knowledge_skipped: bridge.skipped,
|
|
6377
|
+
...bridge.failed > 0 ? { knowledge_failures: bridge.failures } : {},
|
|
6378
|
+
...takeover
|
|
6379
|
+
};
|
|
6380
|
+
});
|
|
5083
6381
|
} catch (err) {
|
|
5084
6382
|
const message = err instanceof Error ? err.message : String(err);
|
|
5085
6383
|
return { synced: false, reason: `Sync failed: ${message}` };
|
|
@@ -5097,6 +6395,505 @@ async function handleSyncConfig(deps, rawInput, _signal, fetchFn = globalThis.fe
|
|
|
5097
6395
|
return jsonContent({ tool: TOOL_NAME22, schema_version: 1, payload: result });
|
|
5098
6396
|
}
|
|
5099
6397
|
|
|
6398
|
+
// src/tools/memory/private-knowledge.ts
|
|
6399
|
+
var UPSERT_TOOL_NAME = "vo_private_knowledge_upsert";
|
|
6400
|
+
var CONTEXT_TOOL_NAME = "vo_private_knowledge_context";
|
|
6401
|
+
var INVALIDATE_TOOL_NAME = "vo_private_knowledge_invalidate";
|
|
6402
|
+
var STALE_TOOL_NAME = "vo_private_knowledge_stale";
|
|
6403
|
+
var KNOWLEDGE_CLASSES = ["memory", "skill", "doctrine", "hook", "command"];
|
|
6404
|
+
var PRECISION_CHAR_BUDGET = 12e3;
|
|
6405
|
+
var upsertInputSchema = {
|
|
6406
|
+
type: "object",
|
|
6407
|
+
properties: {
|
|
6408
|
+
knowledge_class: { type: "string", enum: KNOWLEDGE_CLASSES },
|
|
6409
|
+
source_path: { type: "string", description: "Stable private source identifier; not exposed to other users." },
|
|
6410
|
+
title: { type: "string", description: 'Descriptive, retrieval-friendly title (e.g. "AlgoTax OCR redaction architecture", not "notes") \u2014 retrieval matches on it.' },
|
|
6411
|
+
content: { type: "string", description: "Private knowledge text to store server-side. Keep each entry tight and focused (~1-3 pages, under ~12k chars); split larger corpora into separate entries." }
|
|
6412
|
+
},
|
|
6413
|
+
required: ["knowledge_class", "source_path", "title", "content"],
|
|
6414
|
+
additionalProperties: false
|
|
6415
|
+
};
|
|
6416
|
+
var contextInputSchema = {
|
|
6417
|
+
type: "object",
|
|
6418
|
+
properties: {
|
|
6419
|
+
query: { type: "string" },
|
|
6420
|
+
limit: { type: "number", minimum: 1, maximum: 50 },
|
|
6421
|
+
knowledge_class: { type: "string", enum: KNOWLEDGE_CLASSES }
|
|
6422
|
+
},
|
|
6423
|
+
required: ["query"],
|
|
6424
|
+
additionalProperties: false
|
|
6425
|
+
};
|
|
6426
|
+
var invalidateInputSchema = {
|
|
6427
|
+
type: "object",
|
|
6428
|
+
properties: {
|
|
6429
|
+
knowledge_class: { type: "string", enum: KNOWLEDGE_CLASSES },
|
|
6430
|
+
source_path: { type: "string", minLength: 1, maxLength: 400, description: "Stable private source identifier of the entry to invalidate \u2014 must match the source_path used at upsert." }
|
|
6431
|
+
},
|
|
6432
|
+
required: ["knowledge_class", "source_path"],
|
|
6433
|
+
additionalProperties: false
|
|
6434
|
+
};
|
|
6435
|
+
var staleInputSchema = {
|
|
6436
|
+
type: "object",
|
|
6437
|
+
properties: {
|
|
6438
|
+
days: { type: "number", minimum: 1, maximum: 3650, description: "Window in days (default 90): entries at least this old that were never recalled into an agent context, or not within the window." },
|
|
6439
|
+
limit: { type: "number", minimum: 1, maximum: 500 }
|
|
6440
|
+
},
|
|
6441
|
+
additionalProperties: false
|
|
6442
|
+
};
|
|
6443
|
+
var staleDescription = `The FORGETTING REPORT: lists the authenticated operator\u2019s live private-knowledge entries that are at least N days old and have never been recalled into an agent context (or not within N days). Metadata only. SURFACES ONLY \u2014 never auto-invalidates or auto-merges: two memories that disagree may both have been right in different contexts, so you decide. Act on a candidate deliberately with ${INVALIDATE_TOOL_NAME}; recall counts come from ${CONTEXT_TOOL_NAME} reads that actually placed the entry into returned context.`;
|
|
6444
|
+
var upsertDescription = "Uploads or refreshes the authenticated operator\u2019s private cloud knowledge. Works for Claude, Codex, Cursor, and cowork clients via the same vo-mcp login credential. Returns metadata only, not raw stored content. PRECISION DISCIPLINE: keep each entry tight and focused (~1-3 pages) with a descriptive retrieval-friendly title \u2014 retrieval surfaces whole entries, so small dense entries beat bulk dumps. Split large corpora into focused entries, then run a retrieval self-test via vo_private_knowledge_context before relying on the knowledge.";
|
|
6445
|
+
var contextDescription = "Retrieves prompt-ready private knowledge context for the authenticated operator. Returns snippets/context only; no raw corpus download. Also the retrieval self-test surface: after upserting critical knowledge, query for it here and confirm the entry surfaces before trusting it in downstream work.";
|
|
6446
|
+
var invalidateDescription = `Soft-deletes one private-knowledge entry for the authenticated operator: closes the live entry\u2019s validity window (bi-temporal) so it stops surfacing in retrieval. Never destroys data \u2014 invalidated versions remain queryable server-side via include_invalidated. Identify the entry by the same { knowledge_class, source_path } used at upsert; a not_found response means no live entry matches. After invalidating, self-test via ${CONTEXT_TOOL_NAME} to confirm the entry no longer surfaces.`;
|
|
6447
|
+
function isStaleInput(value) {
|
|
6448
|
+
if (value === void 0 || value === null) return true;
|
|
6449
|
+
if (typeof value !== "object") return false;
|
|
6450
|
+
const input = value;
|
|
6451
|
+
if (input["days"] !== void 0 && typeof input["days"] !== "number") return false;
|
|
6452
|
+
if (input["limit"] !== void 0 && typeof input["limit"] !== "number") return false;
|
|
6453
|
+
return true;
|
|
6454
|
+
}
|
|
6455
|
+
function isKnowledgeClass(value) {
|
|
6456
|
+
return typeof value === "string" && KNOWLEDGE_CLASSES.includes(value);
|
|
6457
|
+
}
|
|
6458
|
+
function isUpsertInput(value) {
|
|
6459
|
+
if (typeof value !== "object" || value === null) return false;
|
|
6460
|
+
const input = value;
|
|
6461
|
+
return isKnowledgeClass(input["knowledge_class"]) && typeof input["source_path"] === "string" && typeof input["title"] === "string" && typeof input["content"] === "string";
|
|
6462
|
+
}
|
|
6463
|
+
function isInvalidateInput(value) {
|
|
6464
|
+
if (typeof value !== "object" || value === null) return false;
|
|
6465
|
+
const input = value;
|
|
6466
|
+
return isKnowledgeClass(input["knowledge_class"]) && typeof input["source_path"] === "string";
|
|
6467
|
+
}
|
|
6468
|
+
function isContextInput(value) {
|
|
6469
|
+
if (typeof value !== "object" || value === null) return false;
|
|
6470
|
+
const input = value;
|
|
6471
|
+
if (typeof input["query"] !== "string") return false;
|
|
6472
|
+
if (input["limit"] !== void 0 && typeof input["limit"] !== "number") return false;
|
|
6473
|
+
if (input["knowledge_class"] !== void 0 && !isKnowledgeClass(input["knowledge_class"])) return false;
|
|
6474
|
+
return true;
|
|
6475
|
+
}
|
|
6476
|
+
async function getCloudAuth(fetchFn) {
|
|
6477
|
+
const controlPlaneUrl = process.env["VO_CONTROL_PLANE_URL"]?.replace(/\/+$/, "");
|
|
6478
|
+
if (!controlPlaneUrl) {
|
|
6479
|
+
return { ok: false, reason: "VO_CONTROL_PLANE_URL not set \u2014 cloud mode disabled" };
|
|
6480
|
+
}
|
|
6481
|
+
const { createAuthTokenSourceFromEnv: createAuthTokenSourceFromEnv2 } = await Promise.resolve().then(() => (init_auth_token_source(), auth_token_source_exports));
|
|
6482
|
+
const { readStoredCredential: readStoredCredential2 } = await Promise.resolve().then(() => (init_credential_store(), credential_store_exports));
|
|
6483
|
+
const tokenSource = createAuthTokenSourceFromEnv2(process.env, fetchFn, () => readStoredCredential2(process.env));
|
|
6484
|
+
if (!tokenSource) return { ok: false, reason: "No auth configured. Run `vo-mcp login`." };
|
|
6485
|
+
const token = await tokenSource.getToken();
|
|
6486
|
+
if (!token) return { ok: false, reason: "Failed to obtain auth token. Run `vo-mcp login` again." };
|
|
6487
|
+
return { ok: true, controlPlaneUrl, token };
|
|
6488
|
+
}
|
|
6489
|
+
async function callPrivateKnowledge(path3, body, fetchFn) {
|
|
6490
|
+
const auth = await getCloudAuth(fetchFn);
|
|
6491
|
+
if (!auth.ok) return { ok: false, reason: auth.reason };
|
|
6492
|
+
const response = await fetchFn(`${auth.controlPlaneUrl}${path3}`, {
|
|
6493
|
+
method: "POST",
|
|
6494
|
+
headers: {
|
|
6495
|
+
authorization: `Bearer ${auth.token}`,
|
|
6496
|
+
"content-type": "application/json"
|
|
6497
|
+
},
|
|
6498
|
+
body: JSON.stringify(body)
|
|
6499
|
+
});
|
|
6500
|
+
const text = await response.text();
|
|
6501
|
+
let parsed;
|
|
6502
|
+
try {
|
|
6503
|
+
parsed = text ? JSON.parse(text) : null;
|
|
6504
|
+
} catch {
|
|
6505
|
+
parsed = null;
|
|
6506
|
+
}
|
|
6507
|
+
if (response.status < 200 || response.status >= 300) {
|
|
6508
|
+
return { ok: false, status: response.status, response: parsed ?? text };
|
|
6509
|
+
}
|
|
6510
|
+
return parsed;
|
|
6511
|
+
}
|
|
6512
|
+
async function handlePrivateKnowledgeUpsert(_deps, rawInput, _signal, fetchFn = globalThis.fetch) {
|
|
6513
|
+
if (!isUpsertInput(rawInput)) {
|
|
6514
|
+
throw invalidParams(UPSERT_TOOL_NAME, "expected { knowledge_class, source_path, title, content }.");
|
|
6515
|
+
}
|
|
6516
|
+
const payload = await callPrivateKnowledge("/api/v1/knowledge/private", rawInput, fetchFn);
|
|
6517
|
+
const envelope = {
|
|
6518
|
+
tool: UPSERT_TOOL_NAME,
|
|
6519
|
+
schema_version: 1,
|
|
6520
|
+
payload
|
|
6521
|
+
};
|
|
6522
|
+
if (rawInput.content.length > PRECISION_CHAR_BUDGET) {
|
|
6523
|
+
envelope.precision_note = `content is ${rawInput.content.length} chars (> ${PRECISION_CHAR_BUDGET}). Tight 1-3 page entries retrieve better \u2014 consider splitting into focused entries, then re-test retrieval via ${CONTEXT_TOOL_NAME}.`;
|
|
6524
|
+
}
|
|
6525
|
+
return jsonContent(envelope);
|
|
6526
|
+
}
|
|
6527
|
+
async function handlePrivateKnowledgeInvalidate(_deps, rawInput, _signal, fetchFn = globalThis.fetch) {
|
|
6528
|
+
if (!isInvalidateInput(rawInput)) {
|
|
6529
|
+
throw invalidParams(INVALIDATE_TOOL_NAME, "expected { knowledge_class, source_path }.");
|
|
6530
|
+
}
|
|
6531
|
+
const payload = await callPrivateKnowledge("/api/v1/knowledge/private/invalidate", rawInput, fetchFn);
|
|
6532
|
+
return jsonContent({ tool: INVALIDATE_TOOL_NAME, schema_version: 1, payload });
|
|
6533
|
+
}
|
|
6534
|
+
async function handlePrivateKnowledgeStale(_deps, rawInput, _signal, fetchFn = globalThis.fetch) {
|
|
6535
|
+
if (!isStaleInput(rawInput)) {
|
|
6536
|
+
throw invalidParams(STALE_TOOL_NAME, "expected { optional days, optional limit }.");
|
|
6537
|
+
}
|
|
6538
|
+
const auth = await getCloudAuth(fetchFn);
|
|
6539
|
+
if (!auth.ok) return jsonContent({ tool: STALE_TOOL_NAME, schema_version: 1, payload: { ok: false, reason: auth.reason } });
|
|
6540
|
+
const params = new URLSearchParams();
|
|
6541
|
+
if (rawInput?.days !== void 0) params.set("days", String(Math.trunc(rawInput.days)));
|
|
6542
|
+
if (rawInput?.limit !== void 0) params.set("limit", String(Math.trunc(rawInput.limit)));
|
|
6543
|
+
const qs = params.toString();
|
|
6544
|
+
const response = await fetchFn(`${auth.controlPlaneUrl}/api/v1/knowledge/private/stale${qs ? `?${qs}` : ""}`, {
|
|
6545
|
+
method: "GET",
|
|
6546
|
+
headers: { authorization: `Bearer ${auth.token}` }
|
|
6547
|
+
});
|
|
6548
|
+
const text = await response.text();
|
|
6549
|
+
let parsed;
|
|
6550
|
+
try {
|
|
6551
|
+
parsed = text ? JSON.parse(text) : null;
|
|
6552
|
+
} catch {
|
|
6553
|
+
parsed = null;
|
|
6554
|
+
}
|
|
6555
|
+
const payload = response.status < 200 || response.status >= 300 ? { ok: false, status: response.status, response: parsed ?? text } : parsed;
|
|
6556
|
+
return jsonContent({ tool: STALE_TOOL_NAME, schema_version: 1, payload });
|
|
6557
|
+
}
|
|
6558
|
+
async function handlePrivateKnowledgeContext(_deps, rawInput, _signal, fetchFn = globalThis.fetch) {
|
|
6559
|
+
if (!isContextInput(rawInput)) {
|
|
6560
|
+
throw invalidParams(CONTEXT_TOOL_NAME, "expected { query, optional limit, optional knowledge_class }.");
|
|
6561
|
+
}
|
|
6562
|
+
const payload = await callPrivateKnowledge("/api/v1/knowledge/private/context", rawInput, fetchFn);
|
|
6563
|
+
return jsonContent({ tool: CONTEXT_TOOL_NAME, schema_version: 1, payload });
|
|
6564
|
+
}
|
|
6565
|
+
|
|
6566
|
+
// src/tools/hq/whiteboard.ts
|
|
6567
|
+
init_auth_token_source();
|
|
6568
|
+
init_credential_store();
|
|
6569
|
+
var POST_TOOL_NAME = "hq_whiteboard_post";
|
|
6570
|
+
var READ_TOOL_NAME = "hq_whiteboard_read";
|
|
6571
|
+
var postDescription = "Post an append-only coordination note to the live AlgoHQ whiteboard. Uses the scoped credential from vo-mcp login; operator and tenant ownership are derived by the server.";
|
|
6572
|
+
var readDescription = "Read recent coordination notes from the caller's live AlgoHQ whiteboard. Uses the scoped credential from vo-mcp login and cannot widen tenant scope.";
|
|
6573
|
+
var postInputSchema = {
|
|
6574
|
+
type: "object",
|
|
6575
|
+
properties: {
|
|
6576
|
+
from: { type: "string", minLength: 1, maxLength: 100, description: "Agent/session display name." },
|
|
6577
|
+
type: { type: "string", minLength: 1, maxLength: 64, description: "Message kind, such as intent, worklog, blocker, or completion." },
|
|
6578
|
+
content: { type: "string", minLength: 1, maxLength: 500, description: "Short coordination note." },
|
|
6579
|
+
targetAgent: { type: "string", maxLength: 100 },
|
|
6580
|
+
tester: { type: "string", maxLength: 100 },
|
|
6581
|
+
tier: { type: "string", maxLength: 32 }
|
|
6582
|
+
},
|
|
6583
|
+
required: ["from", "type", "content"],
|
|
6584
|
+
additionalProperties: false
|
|
6585
|
+
};
|
|
6586
|
+
var readInputSchema = {
|
|
6587
|
+
type: "object",
|
|
6588
|
+
properties: {
|
|
6589
|
+
limit: { type: "integer", minimum: 1, maximum: 100, default: 25 },
|
|
6590
|
+
since: { type: "string", description: "Optional ISO-8601 lower bound." },
|
|
6591
|
+
type: { type: "string", minLength: 1, maxLength: 64 }
|
|
6592
|
+
},
|
|
6593
|
+
additionalProperties: false
|
|
6594
|
+
};
|
|
6595
|
+
function resolveTimeoutMs() {
|
|
6596
|
+
const parsed = Number(process.env["HQ_WHITEBOARD_TIMEOUT_MS"]);
|
|
6597
|
+
return Number.isFinite(parsed) && parsed >= 10 && parsed <= 12e4 ? parsed : 1e4;
|
|
6598
|
+
}
|
|
6599
|
+
function isRecord(value) {
|
|
6600
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6601
|
+
}
|
|
6602
|
+
function onlyKeys(value, allowed) {
|
|
6603
|
+
return Object.keys(value).every((key) => allowed.includes(key));
|
|
6604
|
+
}
|
|
6605
|
+
function isBoundedString(value, min, max) {
|
|
6606
|
+
return typeof value === "string" && value.trim().length >= min && value.trim().length <= max;
|
|
6607
|
+
}
|
|
6608
|
+
function parsePostInput(value) {
|
|
6609
|
+
if (!isRecord(value) || !onlyKeys(value, ["from", "type", "content", "targetAgent", "tester", "tier"])) return null;
|
|
6610
|
+
if (!isBoundedString(value["from"], 1, 100)) return null;
|
|
6611
|
+
if (!isBoundedString(value["type"], 1, 64) || !/^[a-zA-Z0-9_-]+$/.test(value["type"].trim())) return null;
|
|
6612
|
+
if (!isBoundedString(value["content"], 1, 500)) return null;
|
|
6613
|
+
for (const [key, max] of [["targetAgent", 100], ["tester", 100], ["tier", 32]]) {
|
|
6614
|
+
if (value[key] !== void 0 && !isBoundedString(value[key], 0, max)) return null;
|
|
6615
|
+
}
|
|
6616
|
+
return {
|
|
6617
|
+
from: value["from"].trim(),
|
|
6618
|
+
type: value["type"].trim(),
|
|
6619
|
+
content: value["content"].trim(),
|
|
6620
|
+
...typeof value["targetAgent"] === "string" ? { targetAgent: value["targetAgent"].trim() } : {},
|
|
6621
|
+
...typeof value["tester"] === "string" ? { tester: value["tester"].trim() } : {},
|
|
6622
|
+
...typeof value["tier"] === "string" ? { tier: value["tier"].trim() } : {}
|
|
6623
|
+
};
|
|
6624
|
+
}
|
|
6625
|
+
function parseReadInput(value) {
|
|
6626
|
+
if (!isRecord(value) || !onlyKeys(value, ["limit", "since", "type"])) return null;
|
|
6627
|
+
if (value["limit"] !== void 0 && (!Number.isInteger(value["limit"]) || Number(value["limit"]) < 1 || Number(value["limit"]) > 100)) return null;
|
|
6628
|
+
if (value["since"] !== void 0 && (typeof value["since"] !== "string" || Number.isNaN(Date.parse(value["since"])))) return null;
|
|
6629
|
+
if (value["type"] !== void 0 && !isBoundedString(value["type"], 1, 64)) return null;
|
|
6630
|
+
return {
|
|
6631
|
+
...typeof value["limit"] === "number" ? { limit: value["limit"] } : {},
|
|
6632
|
+
...typeof value["since"] === "string" ? { since: value["since"] } : {},
|
|
6633
|
+
...typeof value["type"] === "string" ? { type: value["type"].trim() } : {}
|
|
6634
|
+
};
|
|
6635
|
+
}
|
|
6636
|
+
async function resolveCloud(fetchFn) {
|
|
6637
|
+
const url = process.env["VO_CONTROL_PLANE_URL"]?.trim().replace(/\/$/, "");
|
|
6638
|
+
if (!url) return null;
|
|
6639
|
+
try {
|
|
6640
|
+
const source = createAuthTokenSourceFromEnv(process.env, fetchFn, () => readStoredCredential(process.env));
|
|
6641
|
+
const token = await source?.getToken();
|
|
6642
|
+
return token ? { url, token } : null;
|
|
6643
|
+
} catch {
|
|
6644
|
+
return null;
|
|
6645
|
+
}
|
|
6646
|
+
}
|
|
6647
|
+
async function callWhiteboard(method, bodyOrQuery, signal, fetchFn = fetch) {
|
|
6648
|
+
const cloud = await resolveCloud(fetchFn);
|
|
6649
|
+
if (!cloud) {
|
|
6650
|
+
return {
|
|
6651
|
+
ok: false,
|
|
6652
|
+
error: "hq_whiteboard_not_configured",
|
|
6653
|
+
message: "Set VO_CONTROL_PLANE_URL and run vo-mcp login to install a scoped HQ credential."
|
|
6654
|
+
};
|
|
6655
|
+
}
|
|
6656
|
+
const timeoutSignal = AbortSignal.timeout(resolveTimeoutMs());
|
|
6657
|
+
const requestSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
6658
|
+
const query = new URLSearchParams();
|
|
6659
|
+
if (method === "GET") {
|
|
6660
|
+
const input = bodyOrQuery;
|
|
6661
|
+
query.set("limit", String(input.limit ?? 25));
|
|
6662
|
+
if (input.since) query.set("since", input.since);
|
|
6663
|
+
if (input.type) query.set("type", input.type);
|
|
6664
|
+
}
|
|
6665
|
+
try {
|
|
6666
|
+
const response = await fetchFn(
|
|
6667
|
+
`${cloud.url}/api/v1/hq/whiteboard/messages${query.size ? `?${query}` : ""}`,
|
|
6668
|
+
{
|
|
6669
|
+
method,
|
|
6670
|
+
headers: {
|
|
6671
|
+
Authorization: `Bearer ${cloud.token}`,
|
|
6672
|
+
...method === "POST" ? { "Content-Type": "application/json" } : {}
|
|
6673
|
+
},
|
|
6674
|
+
...method === "POST" ? { body: JSON.stringify(bodyOrQuery) } : {},
|
|
6675
|
+
signal: requestSignal
|
|
6676
|
+
}
|
|
6677
|
+
);
|
|
6678
|
+
const text = await response.text();
|
|
6679
|
+
let payload;
|
|
6680
|
+
try {
|
|
6681
|
+
payload = JSON.parse(text);
|
|
6682
|
+
} catch {
|
|
6683
|
+
payload = { ok: false, error: "invalid_response", message: text.slice(0, 200) };
|
|
6684
|
+
}
|
|
6685
|
+
if (!response.ok) {
|
|
6686
|
+
return { ok: false, error: "hq_whiteboard_http_error", status: response.status, response: payload };
|
|
6687
|
+
}
|
|
6688
|
+
return payload;
|
|
6689
|
+
} catch (error) {
|
|
6690
|
+
return {
|
|
6691
|
+
ok: false,
|
|
6692
|
+
error: signal?.aborted ? "cancelled" : timeoutSignal.aborted ? "hq_whiteboard_timeout" : "hq_whiteboard_unreachable",
|
|
6693
|
+
message: error instanceof Error ? error.message.slice(0, 200) : String(error).slice(0, 200)
|
|
6694
|
+
};
|
|
6695
|
+
}
|
|
6696
|
+
}
|
|
6697
|
+
async function handleHqWhiteboardPost(_deps, rawInput, signal) {
|
|
6698
|
+
const input = parsePostInput(rawInput);
|
|
6699
|
+
if (!input) throw invalidParams(POST_TOOL_NAME, "requires from, type, and 1-500 character content; unknown fields are rejected");
|
|
6700
|
+
return jsonContent(await callWhiteboard("POST", input, signal));
|
|
6701
|
+
}
|
|
6702
|
+
async function handleHqWhiteboardRead(_deps, rawInput, signal) {
|
|
6703
|
+
const input = parseReadInput(rawInput);
|
|
6704
|
+
if (!input) throw invalidParams(READ_TOOL_NAME, "limit must be 1-100, since must be ISO-8601, and unknown fields are rejected");
|
|
6705
|
+
return jsonContent(await callWhiteboard("GET", input, signal));
|
|
6706
|
+
}
|
|
6707
|
+
|
|
6708
|
+
// src/tools/skills/skill-corpus.ts
|
|
6709
|
+
import { existsSync as existsSync9, statSync as statSync6 } from "node:fs";
|
|
6710
|
+
import { dirname as dirname5, isAbsolute, join as join13, resolve as resolve2 } from "node:path";
|
|
6711
|
+
|
|
6712
|
+
// ../skill-registry/src/loader.ts
|
|
6713
|
+
import { readdirSync as readdirSync6, readFileSync as readFileSync14, statSync as statSync5 } from "node:fs";
|
|
6714
|
+
import { join as join12 } from "node:path";
|
|
6715
|
+
var InvalidSkillFrontmatterError = class extends Error {
|
|
6716
|
+
constructor(skillFile, reason) {
|
|
6717
|
+
super(`Invalid frontmatter in ${skillFile}: ${reason}`);
|
|
6718
|
+
this.skillFile = skillFile;
|
|
6719
|
+
this.reason = reason;
|
|
6720
|
+
}
|
|
6721
|
+
skillFile;
|
|
6722
|
+
reason;
|
|
6723
|
+
name = "InvalidSkillFrontmatterError";
|
|
6724
|
+
};
|
|
6725
|
+
var FRONTMATTER_DELIMITER = "---";
|
|
6726
|
+
function parseFrontmatter(rawInput, sourcePath) {
|
|
6727
|
+
const raw = rawInput.replace(/\r\n/g, "\n");
|
|
6728
|
+
if (!raw.startsWith(`${FRONTMATTER_DELIMITER}
|
|
6729
|
+
`)) {
|
|
6730
|
+
throw new InvalidSkillFrontmatterError(sourcePath, 'file does not start with frontmatter delimiter "---"');
|
|
6731
|
+
}
|
|
6732
|
+
const afterFirst = raw.slice(FRONTMATTER_DELIMITER.length + 1);
|
|
6733
|
+
const closingIdx = afterFirst.indexOf(`
|
|
6734
|
+
${FRONTMATTER_DELIMITER}
|
|
6735
|
+
`);
|
|
6736
|
+
if (closingIdx === -1) {
|
|
6737
|
+
throw new InvalidSkillFrontmatterError(sourcePath, 'missing closing frontmatter delimiter "---"');
|
|
6738
|
+
}
|
|
6739
|
+
const frontmatterText = afterFirst.slice(0, closingIdx);
|
|
6740
|
+
const body = afterFirst.slice(closingIdx + `
|
|
6741
|
+
${FRONTMATTER_DELIMITER}
|
|
6742
|
+
`.length);
|
|
6743
|
+
let name = "";
|
|
6744
|
+
let description23 = "";
|
|
6745
|
+
for (const line of frontmatterText.split("\n")) {
|
|
6746
|
+
const trimmed = line.trim();
|
|
6747
|
+
if (trimmed.length === 0) continue;
|
|
6748
|
+
const colonIdx = trimmed.indexOf(":");
|
|
6749
|
+
if (colonIdx === -1) continue;
|
|
6750
|
+
const key = trimmed.slice(0, colonIdx).trim();
|
|
6751
|
+
const value = trimmed.slice(colonIdx + 1).trim();
|
|
6752
|
+
if (key === "name") name = value;
|
|
6753
|
+
else if (key === "description") description23 = value;
|
|
6754
|
+
}
|
|
6755
|
+
if (name.length === 0) {
|
|
6756
|
+
throw new InvalidSkillFrontmatterError(sourcePath, 'missing required field "name"');
|
|
6757
|
+
}
|
|
6758
|
+
if (description23.length === 0) {
|
|
6759
|
+
throw new InvalidSkillFrontmatterError(sourcePath, 'missing required field "description"');
|
|
6760
|
+
}
|
|
6761
|
+
return { name, description: description23, body };
|
|
6762
|
+
}
|
|
6763
|
+
function loadSkillsFromDir(skillsDir) {
|
|
6764
|
+
const entries = readdirSync6(skillsDir);
|
|
6765
|
+
const skills = [];
|
|
6766
|
+
for (const entry of entries) {
|
|
6767
|
+
const entryPath = join12(skillsDir, entry);
|
|
6768
|
+
let stat;
|
|
6769
|
+
try {
|
|
6770
|
+
stat = statSync5(entryPath);
|
|
6771
|
+
} catch {
|
|
6772
|
+
continue;
|
|
6773
|
+
}
|
|
6774
|
+
if (!stat.isDirectory()) continue;
|
|
6775
|
+
const skillFile = join12(entryPath, "SKILL.md");
|
|
6776
|
+
let raw;
|
|
6777
|
+
try {
|
|
6778
|
+
raw = readFileSync14(skillFile, "utf8");
|
|
6779
|
+
} catch {
|
|
6780
|
+
continue;
|
|
6781
|
+
}
|
|
6782
|
+
const { name, description: description23, body } = parseFrontmatter(raw, skillFile);
|
|
6783
|
+
skills.push({ name, description: description23, body, sourcePath: skillFile });
|
|
6784
|
+
}
|
|
6785
|
+
return [...skills].sort((a, b) => a.name.localeCompare(b.name));
|
|
6786
|
+
}
|
|
6787
|
+
|
|
6788
|
+
// src/tools/skills/skill-corpus.ts
|
|
6789
|
+
var LIST_TOOL_NAME = "vo_skill_list";
|
|
6790
|
+
var GET_TOOL_NAME = "vo_skill_get";
|
|
6791
|
+
var listDescription = "List the Algosuite skill corpus (name + trigger description for every skill). Call once near session start to learn which skills exist; then fetch the full instructions for a relevant skill with vo_skill_get. This is the same corpus Claude Code loads natively from .claude/skills \u2014 served over MCP so every vendor works from identical playbooks. Pass refresh:true to re-scan from disk.";
|
|
6792
|
+
var getDescription = "Fetch the full markdown instructions of one Algosuite skill by name. Follow the returned instructions for the current task the same way a native skill invocation would. Use vo_skill_list to discover skill names.";
|
|
6793
|
+
var listInputSchema = {
|
|
6794
|
+
type: "object",
|
|
6795
|
+
properties: {
|
|
6796
|
+
refresh: {
|
|
6797
|
+
type: "boolean",
|
|
6798
|
+
description: "Re-scan the skills directory instead of using the cached corpus."
|
|
6799
|
+
}
|
|
6800
|
+
},
|
|
6801
|
+
required: []
|
|
6802
|
+
};
|
|
6803
|
+
var getInputSchema = {
|
|
6804
|
+
type: "object",
|
|
6805
|
+
properties: {
|
|
6806
|
+
name: {
|
|
6807
|
+
type: "string",
|
|
6808
|
+
description: "Skill name exactly as returned by vo_skill_list."
|
|
6809
|
+
}
|
|
6810
|
+
},
|
|
6811
|
+
required: ["name"]
|
|
6812
|
+
};
|
|
6813
|
+
var MAX_WALK_UP_LEVELS = 8;
|
|
6814
|
+
var cachedCorpus = null;
|
|
6815
|
+
function resolveSkillsDir(env = process.env, startDir = process.cwd()) {
|
|
6816
|
+
const override = env.VO_SKILLS_DIR;
|
|
6817
|
+
if (typeof override === "string" && override.length > 0) {
|
|
6818
|
+
const abs = isAbsolute(override) ? override : resolve2(startDir, override);
|
|
6819
|
+
return existsSync9(abs) && statSync6(abs).isDirectory() ? abs : null;
|
|
6820
|
+
}
|
|
6821
|
+
let dir = resolve2(startDir);
|
|
6822
|
+
for (let i = 0; i < MAX_WALK_UP_LEVELS; i += 1) {
|
|
6823
|
+
const candidate = join13(dir, ".claude", "skills");
|
|
6824
|
+
if (existsSync9(candidate) && statSync6(candidate).isDirectory()) return candidate;
|
|
6825
|
+
const parent = dirname5(dir);
|
|
6826
|
+
if (parent === dir) break;
|
|
6827
|
+
dir = parent;
|
|
6828
|
+
}
|
|
6829
|
+
return null;
|
|
6830
|
+
}
|
|
6831
|
+
function loadCorpus() {
|
|
6832
|
+
const skillsDir = resolveSkillsDir();
|
|
6833
|
+
if (skillsDir === null) {
|
|
6834
|
+
return {
|
|
6835
|
+
skills: [],
|
|
6836
|
+
skillsDir: null,
|
|
6837
|
+
unavailableReason: "No skills directory found. Set VO_SKILLS_DIR or run inside a repo with .claude/skills."
|
|
6838
|
+
};
|
|
6839
|
+
}
|
|
6840
|
+
try {
|
|
6841
|
+
return { skills: loadSkillsFromDir(skillsDir), skillsDir, unavailableReason: null };
|
|
6842
|
+
} catch (err) {
|
|
6843
|
+
const message = err instanceof Error ? `${err.name}: ${err.message}` : String(err);
|
|
6844
|
+
return { skills: [], skillsDir, unavailableReason: message };
|
|
6845
|
+
}
|
|
6846
|
+
}
|
|
6847
|
+
function getCorpus(refresh) {
|
|
6848
|
+
if (refresh || cachedCorpus === null) {
|
|
6849
|
+
cachedCorpus = loadCorpus();
|
|
6850
|
+
}
|
|
6851
|
+
return cachedCorpus;
|
|
6852
|
+
}
|
|
6853
|
+
async function handleSkillList(_deps, rawInput) {
|
|
6854
|
+
const input = rawInput ?? {};
|
|
6855
|
+
const refresh = input.refresh === true;
|
|
6856
|
+
const corpus = getCorpus(refresh);
|
|
6857
|
+
return jsonContent({
|
|
6858
|
+
corpus_available: corpus.unavailableReason === null,
|
|
6859
|
+
skills_dir: corpus.skillsDir,
|
|
6860
|
+
unavailable_reason: corpus.unavailableReason,
|
|
6861
|
+
skill_count: corpus.skills.length,
|
|
6862
|
+
skills: corpus.skills.map((s) => ({ name: s.name, description: s.description }))
|
|
6863
|
+
});
|
|
6864
|
+
}
|
|
6865
|
+
async function handleSkillGet(_deps, rawInput) {
|
|
6866
|
+
const input = rawInput ?? {};
|
|
6867
|
+
if (typeof input.name !== "string" || input.name.trim().length === 0) {
|
|
6868
|
+
throw invalidParams(GET_TOOL_NAME, 'input field "name" (non-empty string) is required');
|
|
6869
|
+
}
|
|
6870
|
+
const requested = input.name.trim();
|
|
6871
|
+
const corpus = getCorpus(false);
|
|
6872
|
+
if (corpus.unavailableReason !== null) {
|
|
6873
|
+
return jsonContent({
|
|
6874
|
+
corpus_available: false,
|
|
6875
|
+
unavailable_reason: corpus.unavailableReason,
|
|
6876
|
+
skill: null
|
|
6877
|
+
});
|
|
6878
|
+
}
|
|
6879
|
+
const skill = corpus.skills.find((s) => s.name === requested);
|
|
6880
|
+
if (skill === void 0) {
|
|
6881
|
+
throw invalidParams(
|
|
6882
|
+
GET_TOOL_NAME,
|
|
6883
|
+
`unknown skill "${requested}". Known skills: ${corpus.skills.map((s) => s.name).join(", ")}`
|
|
6884
|
+
);
|
|
6885
|
+
}
|
|
6886
|
+
return jsonContent({
|
|
6887
|
+
corpus_available: true,
|
|
6888
|
+
skill: {
|
|
6889
|
+
name: skill.name,
|
|
6890
|
+
description: skill.description,
|
|
6891
|
+
instructions: skill.body,
|
|
6892
|
+
source_path: skill.sourcePath
|
|
6893
|
+
}
|
|
6894
|
+
});
|
|
6895
|
+
}
|
|
6896
|
+
|
|
5100
6897
|
// src/server.ts
|
|
5101
6898
|
function buildToolRegistry() {
|
|
5102
6899
|
return {
|
|
@@ -5275,11 +7072,75 @@ function buildToolRegistry() {
|
|
|
5275
7072
|
inputSchema: inputSchema22
|
|
5276
7073
|
},
|
|
5277
7074
|
handler: handleSyncConfig
|
|
7075
|
+
},
|
|
7076
|
+
[UPSERT_TOOL_NAME]: {
|
|
7077
|
+
definition: {
|
|
7078
|
+
name: UPSERT_TOOL_NAME,
|
|
7079
|
+
description: upsertDescription,
|
|
7080
|
+
inputSchema: upsertInputSchema
|
|
7081
|
+
},
|
|
7082
|
+
handler: handlePrivateKnowledgeUpsert
|
|
7083
|
+
},
|
|
7084
|
+
[CONTEXT_TOOL_NAME]: {
|
|
7085
|
+
definition: {
|
|
7086
|
+
name: CONTEXT_TOOL_NAME,
|
|
7087
|
+
description: contextDescription,
|
|
7088
|
+
inputSchema: contextInputSchema
|
|
7089
|
+
},
|
|
7090
|
+
handler: handlePrivateKnowledgeContext
|
|
7091
|
+
},
|
|
7092
|
+
[INVALIDATE_TOOL_NAME]: {
|
|
7093
|
+
definition: {
|
|
7094
|
+
name: INVALIDATE_TOOL_NAME,
|
|
7095
|
+
description: invalidateDescription,
|
|
7096
|
+
inputSchema: invalidateInputSchema
|
|
7097
|
+
},
|
|
7098
|
+
handler: handlePrivateKnowledgeInvalidate
|
|
7099
|
+
},
|
|
7100
|
+
[STALE_TOOL_NAME]: {
|
|
7101
|
+
definition: {
|
|
7102
|
+
name: STALE_TOOL_NAME,
|
|
7103
|
+
description: staleDescription,
|
|
7104
|
+
inputSchema: staleInputSchema
|
|
7105
|
+
},
|
|
7106
|
+
handler: handlePrivateKnowledgeStale
|
|
7107
|
+
},
|
|
7108
|
+
[POST_TOOL_NAME]: {
|
|
7109
|
+
definition: {
|
|
7110
|
+
name: POST_TOOL_NAME,
|
|
7111
|
+
description: postDescription,
|
|
7112
|
+
inputSchema: postInputSchema
|
|
7113
|
+
},
|
|
7114
|
+
handler: handleHqWhiteboardPost
|
|
7115
|
+
},
|
|
7116
|
+
[READ_TOOL_NAME]: {
|
|
7117
|
+
definition: {
|
|
7118
|
+
name: READ_TOOL_NAME,
|
|
7119
|
+
description: readDescription,
|
|
7120
|
+
inputSchema: readInputSchema
|
|
7121
|
+
},
|
|
7122
|
+
handler: handleHqWhiteboardRead
|
|
7123
|
+
},
|
|
7124
|
+
[LIST_TOOL_NAME]: {
|
|
7125
|
+
definition: {
|
|
7126
|
+
name: LIST_TOOL_NAME,
|
|
7127
|
+
description: listDescription,
|
|
7128
|
+
inputSchema: listInputSchema
|
|
7129
|
+
},
|
|
7130
|
+
handler: handleSkillList
|
|
7131
|
+
},
|
|
7132
|
+
[GET_TOOL_NAME]: {
|
|
7133
|
+
definition: {
|
|
7134
|
+
name: GET_TOOL_NAME,
|
|
7135
|
+
description: getDescription,
|
|
7136
|
+
inputSchema: getInputSchema
|
|
7137
|
+
},
|
|
7138
|
+
handler: handleSkillGet
|
|
5278
7139
|
}
|
|
5279
7140
|
};
|
|
5280
7141
|
}
|
|
5281
7142
|
function createServer(options) {
|
|
5282
|
-
const sessionId = options.sessionId ??
|
|
7143
|
+
const sessionId = options.sessionId ?? randomUUID3();
|
|
5283
7144
|
const mode = createLocalMode();
|
|
5284
7145
|
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
5285
7146
|
const server = new Server(
|
|
@@ -5331,9 +7192,9 @@ function listToolNames() {
|
|
|
5331
7192
|
}
|
|
5332
7193
|
|
|
5333
7194
|
// src/cache/sqlite-cache.ts
|
|
5334
|
-
import { createHash as
|
|
5335
|
-
import { chmodSync as chmodSync3, mkdirSync as
|
|
5336
|
-
import { dirname as
|
|
7195
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
7196
|
+
import { chmodSync as chmodSync3, mkdirSync as mkdirSync7 } from "node:fs";
|
|
7197
|
+
import { dirname as dirname6 } from "node:path";
|
|
5337
7198
|
import { DatabaseSync } from "node:sqlite";
|
|
5338
7199
|
|
|
5339
7200
|
// src/cache/canonicalize.ts
|
|
@@ -5378,7 +7239,7 @@ function normalizeString(s) {
|
|
|
5378
7239
|
function createSqliteCache(options) {
|
|
5379
7240
|
const fileBacked = options.dbPath !== ":memory:";
|
|
5380
7241
|
if (fileBacked) {
|
|
5381
|
-
|
|
7242
|
+
mkdirSync7(dirname6(options.dbPath), { recursive: true, mode: 448 });
|
|
5382
7243
|
}
|
|
5383
7244
|
const versionNamespace = options.cacheVersionNamespace ?? "";
|
|
5384
7245
|
const db = new DatabaseSync(options.dbPath);
|
|
@@ -5409,7 +7270,7 @@ function createSqliteCache(options) {
|
|
|
5409
7270
|
return {
|
|
5410
7271
|
keyFor(toolName, input, opts) {
|
|
5411
7272
|
const canonical = canonicalize(input, opts);
|
|
5412
|
-
const hash =
|
|
7273
|
+
const hash = createHash4("sha256");
|
|
5413
7274
|
if (versionNamespace.length > 0) {
|
|
5414
7275
|
hash.update(versionNamespace);
|
|
5415
7276
|
hash.update("|");
|
|
@@ -5501,7 +7362,7 @@ function createStubRatchetClient() {
|
|
|
5501
7362
|
let m;
|
|
5502
7363
|
while ((m = pat.regex.exec(req.source)) !== null) {
|
|
5503
7364
|
findings.push({
|
|
5504
|
-
line_excerpt:
|
|
7365
|
+
line_excerpt: clip2(m[0], 80),
|
|
5505
7366
|
severity: pat.severity,
|
|
5506
7367
|
code: pat.code,
|
|
5507
7368
|
message: pat.message
|
|
@@ -5533,7 +7394,7 @@ function createStubRatchetClient() {
|
|
|
5533
7394
|
}
|
|
5534
7395
|
};
|
|
5535
7396
|
}
|
|
5536
|
-
function
|
|
7397
|
+
function clip2(s, n) {
|
|
5537
7398
|
return s.length <= n ? s : s.slice(0, n) + "\u2026";
|
|
5538
7399
|
}
|
|
5539
7400
|
function buildSummary2(args) {
|
|
@@ -5562,7 +7423,61 @@ function createNullConsensusEngineClient(reason = NULL_CLIENT_DEFAULT_REASON) {
|
|
|
5562
7423
|
}
|
|
5563
7424
|
|
|
5564
7425
|
// src/consensus/engine-client.ts
|
|
5565
|
-
import { randomUUID as
|
|
7426
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
7427
|
+
|
|
7428
|
+
// src/consensus/meta-model-caller.ts
|
|
7429
|
+
var META_CONSENSUS_MODEL = "muse-spark-1.1";
|
|
7430
|
+
function createMetaModelCaller(options = {}) {
|
|
7431
|
+
void options;
|
|
7432
|
+
return async function callMetaWithMetrics2() {
|
|
7433
|
+
throw new Error(
|
|
7434
|
+
"Muse Spark direct consensus is disabled. Use an explicit sanitized task capsule through the AlgoSuite Model Firewall."
|
|
7435
|
+
);
|
|
7436
|
+
};
|
|
7437
|
+
}
|
|
7438
|
+
var callMetaWithMetrics = createMetaModelCaller();
|
|
7439
|
+
|
|
7440
|
+
// src/consensus/consensus-panel.ts
|
|
7441
|
+
var VO_MCP_CONSENSUS_PANEL = {
|
|
7442
|
+
// claude-opus-5 (2026-07-24). Opus 4.7 was STRICTLY DOMINATED, not merely old:
|
|
7443
|
+
// Opus 5 is $5/$25 per MTok vs Opus 4.7's $15/$75 — a 3x cost cut on this slot,
|
|
7444
|
+
// corroborated by our own catalog (constants/pricing/sciencePricing.ts prices
|
|
7445
|
+
// claude-opus-5 at 0.010 vs claude-opus-4-7 at 0.030) — AND the same 2026-07-24
|
|
7446
|
+
// release note REMOVED fast mode from Opus 4.7 outright: `speed: "fast"` now
|
|
7447
|
+
// returns an error there rather than degrading, unlike the Opus 4.6 removal.
|
|
7448
|
+
// Verified served: GET /v1/models/claude-opus-5 -> HTTP 200 (2026-07-30).
|
|
7449
|
+
//
|
|
7450
|
+
// Claude-5 API safety checked before this swap: Opus 5 rejects `temperature` /
|
|
7451
|
+
// `top_p` / `top_k` and manual `thinking.budget_tokens` with HTTP 400. Neither
|
|
7452
|
+
// the consensus-engine Anthropic adapter nor functions-shared `callAnthropic`
|
|
7453
|
+
// sends any of them, and buildAdaptiveThinking emits `thinking: {type:'adaptive'}`
|
|
7454
|
+
// (the supported form) — so this swap cannot 400.
|
|
7455
|
+
anthropic: "claude-opus-5",
|
|
7456
|
+
// gpt-5.6-terra (GA 2026-07-09; −20% price cut 2026-07-30). NOTE the real IDs
|
|
7457
|
+
// are tiered — `gpt-5.6-sol` / `-terra` / `-luna`; there is NO bare `gpt-5.6`
|
|
7458
|
+
// alias (verified against the served model list, 2026-07-30). Terra is the
|
|
7459
|
+
// cost/capability balance point and the right default for a judgment panel;
|
|
7460
|
+
// Sol is available if verdict quality ever needs it.
|
|
7461
|
+
openai: "gpt-5.6-terra",
|
|
7462
|
+
// gemini-2.5-FLASH (not -pro): flash accepts the default thinkingBudget=0 from
|
|
7463
|
+
// callGeminiWithMetrics; 2.5-pro REJECTS budget 0 ("only works in thinking mode").
|
|
7464
|
+
// Flash is also ~10x cheaper. 2026-06-02.
|
|
7465
|
+
google: "gemini-2.5-flash",
|
|
7466
|
+
deepseek: "deepseek-chat",
|
|
7467
|
+
// Muse Spark identity is owned by meta-model-caller.ts (single source of
|
|
7468
|
+
// truth for the meta slot); re-exported here so the panel stays complete.
|
|
7469
|
+
meta: META_CONSENSUS_MODEL
|
|
7470
|
+
};
|
|
7471
|
+
function getVoMcpConsensusPanel(panel = VO_MCP_CONSENSUS_PANEL) {
|
|
7472
|
+
for (const [provider, modelId] of Object.entries(panel)) {
|
|
7473
|
+
if (typeof modelId !== "string" || modelId.trim().length === 0) {
|
|
7474
|
+
throw new Error(
|
|
7475
|
+
`getVoMcpConsensusPanel: panel slot "${provider}" has a missing or blank model ID`
|
|
7476
|
+
);
|
|
7477
|
+
}
|
|
7478
|
+
}
|
|
7479
|
+
return panel;
|
|
7480
|
+
}
|
|
5566
7481
|
|
|
5567
7482
|
// src/consensus/engine-options.ts
|
|
5568
7483
|
var AGREEMENT_GATE_ENV_VAR = "VO_CONSENSUS_AGREEMENT_GATE";
|
|
@@ -5617,6 +7532,14 @@ function shadowEnabled(env) {
|
|
|
5617
7532
|
const norm = raw.trim().toLowerCase();
|
|
5618
7533
|
return !(norm === "0" || norm === "false" || norm === "no" || norm === "off" || norm === "");
|
|
5619
7534
|
}
|
|
7535
|
+
var MIN_RESPONDERS_ENV_VAR = "VO_CONSENSUS_MIN_RESPONDERS";
|
|
7536
|
+
function resolveMinResponders(env) {
|
|
7537
|
+
const raw = (env ?? {})[MIN_RESPONDERS_ENV_VAR];
|
|
7538
|
+
if (raw === void 0 || raw.trim() === "") return 2;
|
|
7539
|
+
const parsed = Number.parseInt(raw.trim(), 10);
|
|
7540
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return void 0;
|
|
7541
|
+
return parsed;
|
|
7542
|
+
}
|
|
5620
7543
|
function mapShadowSynthesis(s) {
|
|
5621
7544
|
if (s === void 0) return void 0;
|
|
5622
7545
|
return {
|
|
@@ -5766,9 +7689,11 @@ function createEngineConsensusClient(options) {
|
|
|
5766
7689
|
...options.agreement_gate_enabled !== void 0 ? { configEnabled: options.agreement_gate_enabled } : {},
|
|
5767
7690
|
...options.env !== void 0 ? { env: options.env } : {}
|
|
5768
7691
|
});
|
|
7692
|
+
const minResponders = resolveMinResponders(options.env);
|
|
5769
7693
|
const engineOptions = {
|
|
5770
7694
|
panel,
|
|
5771
7695
|
...options.per_model_timeout_ms !== void 0 ? { per_model_timeout_ms: options.per_model_timeout_ms } : {},
|
|
7696
|
+
...minResponders !== void 0 ? { min_responders: minResponders } : {},
|
|
5772
7697
|
...agreementGate !== void 0 ? { agreement_gate: agreementGate } : {},
|
|
5773
7698
|
// Stage A7-shadow: run the adaptive verdict alongside the live one for grading.
|
|
5774
7699
|
// Cheap (pure log-odds over already-fetched verdicts; no extra model calls),
|
|
@@ -5821,8 +7746,13 @@ function createEngineConsensusClient(options) {
|
|
|
5821
7746
|
synthesized_verdict: response.synthesized_verdict,
|
|
5822
7747
|
per_model_verdicts: response.per_model_verdicts,
|
|
5823
7748
|
degraded: response.degraded,
|
|
7749
|
+
...response.quorum_failed === true ? { quorum_failed: true } : {},
|
|
5824
7750
|
duration_ms: response.duration_ms,
|
|
5825
7751
|
engine_version: response.engine_version,
|
|
7752
|
+
// Cumulative cross-round inference usage (B44-3). Absent when no panel
|
|
7753
|
+
// member reported usage; forwarded verbatim — the aggregator prefers it
|
|
7754
|
+
// over summing final-round verdicts (which under-reports deliberation).
|
|
7755
|
+
...response.token_usage !== void 0 ? { token_usage: response.token_usage } : {},
|
|
5826
7756
|
// Phase 2 Lane D-1 — forward escalation signal when present. The
|
|
5827
7757
|
// source-grounded layer's own escalation (from the citation grade)
|
|
5828
7758
|
// takes precedence when set, else the synthesizer's.
|
|
@@ -5832,6 +7762,10 @@ function createEngineConsensusClient(options) {
|
|
|
5832
7762
|
...mapFanOutDiagnostics(response.fan_out_diagnostics) !== void 0 ? { fan_out_diagnostics: mapFanOutDiagnostics(response.fan_out_diagnostics) } : {},
|
|
5833
7763
|
// Stage A7-shadow — adaptive-vs-incumbent comparison (PII-free; present iff shadow on).
|
|
5834
7764
|
...mapShadowSynthesis(response.shadow_synthesis) !== void 0 ? { shadow_synthesis: mapShadowSynthesis(response.shadow_synthesis) } : {},
|
|
7765
|
+
// Critique-uptake (2026-07-20 red-team fix) — verifier-critique
|
|
7766
|
+
// visibility report; previously computed by the engine on every
|
|
7767
|
+
// call but dropped at this boundary.
|
|
7768
|
+
...response.critique_uptake !== void 0 ? { critique_uptake: response.critique_uptake } : {},
|
|
5835
7769
|
// Source-grounded additive outputs (Tier-4 features).
|
|
5836
7770
|
...useSourceGrounded ? { source_grounded: true } : {},
|
|
5837
7771
|
...sourceExtras?.citation_grade !== void 0 ? { citation_grade: sourceExtras.citation_grade } : {},
|
|
@@ -5847,25 +7781,12 @@ function createEngineConsensusClient(options) {
|
|
|
5847
7781
|
}
|
|
5848
7782
|
};
|
|
5849
7783
|
}
|
|
5850
|
-
var DEFAULT_MODELS =
|
|
5851
|
-
// These ids match the strategic-roadmap §4 `newsStandard` / `newsDeep` panel
|
|
5852
|
-
// intent — current production model ids. Per handoff §C-3 these MUST come
|
|
5853
|
-
// from `CONSENSUS_PANELS` in `functions-shared/shared-model-resolvers.ts`
|
|
5854
|
-
// for V1; placeholder defaults here keep Phase 2 Lane A non-blocking.
|
|
5855
|
-
anthropic: "claude-opus-4-7",
|
|
5856
|
-
openai: "gpt-5",
|
|
5857
|
-
// gemini-2.5-FLASH (not -pro): flash accepts the default thinkingBudget=0 from
|
|
5858
|
-
// callGeminiWithMetrics; 2.5-pro REJECTS budget 0 ("only works in thinking mode").
|
|
5859
|
-
// Flash is also ~10x cheaper. 2026-06-02.
|
|
5860
|
-
google: "gemini-2.5-flash",
|
|
5861
|
-
deepseek: "deepseek-chat"
|
|
5862
|
-
};
|
|
7784
|
+
var DEFAULT_MODELS = getVoMcpConsensusPanel();
|
|
5863
7785
|
function probeProviders(env = process.env) {
|
|
5864
7786
|
const out = [];
|
|
5865
7787
|
if ((env["ANTHROPIC_API_KEY"] ?? "").trim().length > 0) out.push("anthropic");
|
|
5866
7788
|
if ((env["OPENAI_API_KEY"] ?? "").trim().length > 0) out.push("openai");
|
|
5867
7789
|
if ((env["GOOGLE_API_KEY"] ?? "").trim().length > 0) out.push("google");
|
|
5868
|
-
if ((env["DEEPSEEK_API_KEY"] ?? "").trim().length > 0) out.push("deepseek");
|
|
5869
7790
|
return out;
|
|
5870
7791
|
}
|
|
5871
7792
|
async function loadFactoryAndCallers(injectedEngine, injectedShared) {
|
|
@@ -5910,14 +7831,12 @@ async function tryCreateEngineConsensusClientFromEnvAsync(options = {}) {
|
|
|
5910
7831
|
const callerByProvider = {
|
|
5911
7832
|
anthropic: loaded.shared.callAnthropicWithMetrics,
|
|
5912
7833
|
openai: loaded.shared.callOpenAIWithMetrics,
|
|
5913
|
-
google: loaded.shared.callGeminiWithMetrics
|
|
5914
|
-
deepseek: loaded.shared.callDeepSeekWithMetrics
|
|
7834
|
+
google: loaded.shared.callGeminiWithMetrics
|
|
5915
7835
|
};
|
|
5916
7836
|
const modelByProvider = {
|
|
5917
7837
|
anthropic: options.models?.anthropic ?? DEFAULT_MODELS.anthropic,
|
|
5918
7838
|
openai: options.models?.openai ?? DEFAULT_MODELS.openai,
|
|
5919
|
-
google: options.models?.google ?? DEFAULT_MODELS.google
|
|
5920
|
-
deepseek: options.models?.deepseek ?? DEFAULT_MODELS.deepseek
|
|
7839
|
+
google: options.models?.google ?? DEFAULT_MODELS.google
|
|
5921
7840
|
};
|
|
5922
7841
|
const panel = [];
|
|
5923
7842
|
for (const p of providers) {
|