@algosuite/vo-mcp 0.2.0-beta.57 → 0.2.0-beta.59

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.
@@ -340,6 +340,87 @@ async function getTaskKnowledgeContextRequest(req, taskId, { query } = {}, {
340
340
  throw new Error("knowledge-context retry loop exited unexpectedly");
341
341
  }
342
342
 
343
+ // ../../scripts/virtual-office/code-runner/control-plane-prepared-job.mjs
344
+ var PREPARED_JOB_ENV_QUERY_KEYS = [
345
+ "VO_CODE_RUNNER_NO_WEB",
346
+ "VO_CODE_RUNNER_NO_WORKFLOW",
347
+ "VO_CODE_RUNNER_NO_CONSENSUS",
348
+ "VO_CODE_RUNNER_PERMISSION_MODE",
349
+ "VO_CODE_RUNNER_DEFAULT_BUDGET_USD",
350
+ "VO_CODE_RUNNER_META_REASONING_EFFORT",
351
+ "VO_CODE_RUNNER_ALLOW_MISSING_KNOWLEDGE_CONTEXT",
352
+ "VO_ENABLE_CONTEXT7"
353
+ ];
354
+ var PREPARED_JOB_ENV_VALUE_MAX = 64;
355
+ function preparedJobQuery({ agent = "claude", env = {} } = {}) {
356
+ const params = new URLSearchParams();
357
+ params.set("agent", String(agent));
358
+ const sent = [];
359
+ const dropped = [];
360
+ for (const key of PREPARED_JOB_ENV_QUERY_KEYS) {
361
+ const raw = env?.[key];
362
+ if (typeof raw !== "string" || raw.length === 0) continue;
363
+ if (raw.length > PREPARED_JOB_ENV_VALUE_MAX) {
364
+ dropped.push(key);
365
+ continue;
366
+ }
367
+ params.set(key, raw);
368
+ sent.push(key);
369
+ }
370
+ return { query: params.toString(), sent, dropped };
371
+ }
372
+ async function refusalCode(res) {
373
+ try {
374
+ const body = await res.json();
375
+ return typeof body?.error === "string" && body.error ? body.error : null;
376
+ } catch {
377
+ return null;
378
+ }
379
+ }
380
+ async function getPreparedJobRequest(req, taskId, options = {}, invalidateToken = () => {
381
+ }) {
382
+ const { agent = "claude", env = {}, timeoutMs = 15e3 } = options;
383
+ const { query, sent, dropped } = preparedJobQuery({ agent, env });
384
+ const envMeta = { envSent: sent, envDropped: dropped };
385
+ if (typeof taskId !== "string" || taskId.length === 0) {
386
+ return { ok: false, reason: "missing_task_id", status: 0, ...envMeta };
387
+ }
388
+ const path2 = `/api/v1/code-task/${encodeURIComponent(taskId)}/prepared-job?${query}`;
389
+ let res;
390
+ try {
391
+ res = await req("GET", path2, void 0, { timeoutMs });
392
+ } catch (err) {
393
+ return { ok: false, reason: `transport: ${err?.message || String(err)}`, status: 0, ...envMeta };
394
+ }
395
+ if (res?.status === 401) {
396
+ try {
397
+ invalidateToken();
398
+ } catch {
399
+ }
400
+ return { ok: false, reason: "unauthorized", status: 401, ...envMeta };
401
+ }
402
+ if (!res?.ok) {
403
+ const code = await refusalCode(res);
404
+ return { ok: false, reason: code || `http_${res?.status ?? "unknown"}`, status: res?.status ?? 0, ...envMeta };
405
+ }
406
+ let body;
407
+ try {
408
+ body = await res.json();
409
+ } catch (err) {
410
+ return { ok: false, reason: `unreadable_body: ${err?.message || String(err)}`, status: res.status, ...envMeta };
411
+ }
412
+ const job = body?.prepared_job;
413
+ if (!job || typeof job !== "object") {
414
+ return { ok: false, reason: "no_prepared_job_in_body", status: res.status, ...envMeta };
415
+ }
416
+ return {
417
+ ok: true,
418
+ job,
419
+ composition: body?.composition && typeof body.composition === "object" ? body.composition : {},
420
+ ...envMeta
421
+ };
422
+ }
423
+
343
424
  // ../../scripts/virtual-office/code-runner/control-plane-client.mjs
344
425
  var cachedFirebaseToken = null;
345
426
  var ClaimAuthorityChangedError = class extends Error {
@@ -569,6 +650,10 @@ function createControlPlaneClient({
569
650
  log: (m) => console.warn(`[code-runner ${(/* @__PURE__ */ new Date()).toISOString()}] ${m}`)
570
651
  });
571
652
  },
653
+ /** ADR-004 § 11.1b: the plane's prepared job, for SHADOW comparison. Never throws. */
654
+ getPreparedJob: (taskId, options) => getPreparedJobRequest(req, taskId, options, () => {
655
+ cachedFirebaseToken = null;
656
+ }),
572
657
  /** Weekly Claude token usage report — see control-plane-weekly-tokens.mjs. */
573
658
  async postWeeklyTokens(report) {
574
659
  return postWeeklyTokensRequest(taskReq, report, () => {
@@ -590,8 +675,8 @@ function createControlPlaneClient({
590
675
  * authenticated operator so the web shows a TRUE "runner online" signal.
591
676
  * Best-effort caller; throws on 401/non-ok so the daemon can log + retry.
592
677
  */
593
- async postHeartbeat({ runnerId: runnerId3, runnerInstanceId: runnerInstanceId2, operatorId, uptimeSec, activeTasks, maxConcurrency, effectiveConcurrency, measuredTaskSlots, measuredCpuSlots, measuredMemorySlots, version, daemonVersion, defaultAgent, supervisorInstanceId: supervisorInstanceId2, supervisorVersion: supervisorVersion2, supervisorCapabilities, servedRepos, servedOperators, availableAgents, accountUsage, availableLocalModels }) {
594
- const body = { runner_id: runnerId3 };
678
+ async postHeartbeat({ runnerId: runnerId3, runnerInstanceId: runnerInstanceId2, operatorId, uptimeSec, activeTasks, maxConcurrency, effectiveConcurrency, measuredTaskSlots, measuredCpuSlots, measuredMemorySlots, version, daemonVersion, defaultAgent, supervisorInstanceId: supervisorInstanceId2, supervisorVersion: supervisorVersion2, supervisorCapabilities, servedRepos, servedOperators, availableAgents, accountUsage, availableLocalModels, prepared_job_shadow: preparedJobShadow }) {
679
+ const body = { runner_id: runnerId3, ...preparedJobShadow ? { prepared_job_shadow: preparedJobShadow } : {} };
595
680
  if (runnerInstanceId2) body.runner_instance_id = runnerInstanceId2;
596
681
  if (operatorId) body.operator_id = operatorId;
597
682
  if (typeof uptimeSec === "number") body.uptime_sec = uptimeSec;
@@ -2318,6 +2403,157 @@ function resolveSupervisorChildEntry({
2318
2403
  };
2319
2404
  }
2320
2405
 
2406
+ // src/runner/update-drain-gate.mjs
2407
+ var DEFAULT_DRAIN_CAP_MS = 45 * 60 * 1e3;
2408
+ var DEFAULT_DRAIN_CHECK_MS = 3e4;
2409
+ var MAX_DRAIN_CAP_MS = 6 * 60 * 60 * 1e3;
2410
+ var MIN_DRAIN_CHECK_MS = 1e3;
2411
+ var MAX_DRAIN_CHECK_MS = 5 * 60 * 1e3;
2412
+ var UPDATE_RESTART_ERROR_MESSAGE = "runner update restart after drain timeout";
2413
+ var UPDATE_RESTART_RESULT = "runner_update_restart";
2414
+ var sleepMs = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
2415
+ function positiveNumber(raw) {
2416
+ if (raw === void 0 || raw === null || String(raw).trim() === "") return null;
2417
+ const value = Number(raw);
2418
+ return Number.isFinite(value) && value >= 0 ? value : null;
2419
+ }
2420
+ function resolveDrainCapMs(env = {}) {
2421
+ const ms = positiveNumber(env.VO_RUNNER_UPDATE_DRAIN_CAP_MS);
2422
+ if (ms !== null) return Math.min(ms, MAX_DRAIN_CAP_MS);
2423
+ const minutes = positiveNumber(env.VO_RUNNER_UPDATE_DRAIN_CAP_MIN);
2424
+ if (minutes !== null) return Math.min(minutes * 6e4, MAX_DRAIN_CAP_MS);
2425
+ return DEFAULT_DRAIN_CAP_MS;
2426
+ }
2427
+ function resolveDrainCheckMs(env = {}) {
2428
+ const ms = positiveNumber(env.VO_RUNNER_UPDATE_DRAIN_CHECK_MS);
2429
+ if (ms === null) return DEFAULT_DRAIN_CHECK_MS;
2430
+ return Math.min(Math.max(ms, MIN_DRAIN_CHECK_MS), MAX_DRAIN_CHECK_MS);
2431
+ }
2432
+ function readActiveTasks(status, { childRunning = true } = {}) {
2433
+ if (!childRunning) {
2434
+ return { count: 0, ids: [], known: true, runnerId: null, runnerInstanceId: null };
2435
+ }
2436
+ if (!status || status.ok === false) {
2437
+ return { count: 0, ids: [], known: false, runnerId: null, runnerInstanceId: null };
2438
+ }
2439
+ const ids = Array.isArray(status.activeTaskIds) ? status.activeTaskIds.map((id) => String(id)).filter(Boolean) : [];
2440
+ const reported = Number(status.activeTasks);
2441
+ const count = Number.isFinite(reported) && reported >= 0 ? reported : ids.length;
2442
+ return {
2443
+ count: Math.max(count, ids.length),
2444
+ ids,
2445
+ known: true,
2446
+ runnerId: status.runnerId ? String(status.runnerId) : null,
2447
+ runnerInstanceId: status.runnerInstanceId ? String(status.runnerInstanceId) : null
2448
+ };
2449
+ }
2450
+ function decideDrainStep({ active, elapsedMs, capMs }) {
2451
+ const busy = active.known ? active.count : "unknown";
2452
+ if (active.known && active.count === 0) return { action: "proceed", busy };
2453
+ if (elapsedMs >= capMs) return { action: "cap", busy };
2454
+ return { action: "wait", busy };
2455
+ }
2456
+ function describeBusy(active) {
2457
+ if (!active.known) return "status unreadable from a running child (assumed busy)";
2458
+ const ids = active.ids.length > 0 ? ` [${active.ids.join(", ")}]` : " [ids unavailable]";
2459
+ return `${active.count} active task(s)${ids}`;
2460
+ }
2461
+ async function markCappedTasks({ active, failInFlightTask, log }) {
2462
+ if (active.ids.length === 0) {
2463
+ if (active.count > 0 || !active.known) {
2464
+ log(`update drain cap reached with ${describeBusy(active)} \u2014 cannot annotate tasks the runner did not name`);
2465
+ }
2466
+ return [];
2467
+ }
2468
+ const marked = [];
2469
+ for (const taskId of active.ids) {
2470
+ try {
2471
+ await failInFlightTask(taskId, {
2472
+ message: UPDATE_RESTART_ERROR_MESSAGE,
2473
+ result: UPDATE_RESTART_RESULT,
2474
+ runnerId: active.runnerId,
2475
+ runnerInstanceId: active.runnerInstanceId
2476
+ });
2477
+ marked.push(taskId);
2478
+ } catch (error) {
2479
+ log(`could not mark task ${taskId} before update restart: ${error instanceof Error ? error.message : String(error)}`);
2480
+ }
2481
+ }
2482
+ return marked;
2483
+ }
2484
+ async function awaitRunnerUpdateDrain({
2485
+ readStatus,
2486
+ isChildRunning = () => true,
2487
+ failInFlightTask = async () => {
2488
+ },
2489
+ drainable = false,
2490
+ env = {},
2491
+ now = Date.now,
2492
+ sleep: sleep2 = sleepMs,
2493
+ log = () => {
2494
+ }
2495
+ } = {}) {
2496
+ const capMs = resolveDrainCapMs(env);
2497
+ const checkMs = resolveDrainCheckMs(env);
2498
+ const startedAt = now();
2499
+ let checks = 0;
2500
+ let lastReported = null;
2501
+ const snapshot = async () => {
2502
+ checks += 1;
2503
+ const childRunning = Boolean(isChildRunning());
2504
+ let status;
2505
+ try {
2506
+ status = await readStatus();
2507
+ } catch {
2508
+ status = null;
2509
+ }
2510
+ return readActiveTasks(status, { childRunning });
2511
+ };
2512
+ if (!drainable) {
2513
+ const active = await snapshot();
2514
+ if (active.known && active.count > 0) {
2515
+ return {
2516
+ proceed: false,
2517
+ detail: `deferred safely: ${active.count} active task(s); retry when the runner is idle`,
2518
+ capped: false,
2519
+ waitedMs: 0,
2520
+ checks,
2521
+ markedTaskIds: []
2522
+ };
2523
+ }
2524
+ return { proceed: true, detail: "runner idle", capped: false, waitedMs: 0, checks, markedTaskIds: [] };
2525
+ }
2526
+ for (; ; ) {
2527
+ const active = await snapshot();
2528
+ const elapsedMs = now() - startedAt;
2529
+ const step = decideDrainStep({ active, elapsedMs, capMs });
2530
+ if (step.action === "proceed") {
2531
+ if (lastReported !== null) {
2532
+ log(`update drain complete after ${Math.round(elapsedMs / 1e3)}s \u2014 runner idle, applying staged update restart`);
2533
+ }
2534
+ return { proceed: true, detail: "runner idle", capped: false, waitedMs: elapsedMs, checks, markedTaskIds: [] };
2535
+ }
2536
+ if (step.action === "cap") {
2537
+ log(`update drain cap reached after ${Math.round(elapsedMs / 1e3)}s (cap ${Math.round(capMs / 1e3)}s) \u2014 ${describeBusy(active)}; marking task(s) before restart`);
2538
+ const markedTaskIds = await markCappedTasks({ active, failInFlightTask, log });
2539
+ return {
2540
+ proceed: true,
2541
+ capped: true,
2542
+ detail: `update restart applied after drain cap; marked ${markedTaskIds.length} task(s) as "${UPDATE_RESTART_ERROR_MESSAGE}"`,
2543
+ waitedMs: elapsedMs,
2544
+ checks,
2545
+ markedTaskIds
2546
+ };
2547
+ }
2548
+ const fingerprint = `${step.busy}:${active.ids.join(",")}`;
2549
+ if (fingerprint !== lastReported) {
2550
+ lastReported = fingerprint;
2551
+ log(`deferring staged update restart \u2014 ${describeBusy(active)}; re-checking every ${Math.round(checkMs / 1e3)}s until idle (cap ${Math.round(capMs / 1e3)}s)`);
2552
+ }
2553
+ await sleep2(Math.max(1, Math.min(checkMs, capMs - elapsedMs)));
2554
+ }
2555
+ }
2556
+
2321
2557
  // src/runner/supervisor-child-env.mjs
2322
2558
  import { hostname as systemHostname } from "node:os";
2323
2559
 
@@ -3408,22 +3644,24 @@ async function main() {
3408
3644
  }
3409
3645
  deferredControlRecoveryGeneration = null;
3410
3646
  handling = true;
3411
- const beforeStop = await localStatus();
3412
- if (beforeStop && Number(beforeStop.activeTasks || 0) > 0) {
3413
- await client.completeRunnerControl(action.actionId, {
3414
- runnerId,
3415
- ...operatorId ? { operatorId } : {},
3416
- ...supervisorControlIdentity,
3417
- status: "failed",
3418
- detail: `deferred safely: ${beforeStop.activeTasks} active task(s); retry when the runner is idle`
3419
- });
3647
+ const controlIdentity = { runnerId, ...operatorId ? { operatorId } : {}, ...supervisorControlIdentity };
3648
+ const bundledAction = action.kind === "update" || action.kind === "reinstall";
3649
+ const drain = await awaitRunnerUpdateDrain({
3650
+ readStatus: localStatus,
3651
+ drainable: bundledAction,
3652
+ env: process.env,
3653
+ isChildRunning: () => supervisorChildIsRunning(child),
3654
+ failInFlightTask: (taskId, patch) => client.postProgress(taskId, { status: "failed", message: patch.message, result: patch.result, ...patch.runnerId ? { runner_id: patch.runnerId } : {}, ...patch.runnerInstanceId ? { runner_instance_id: patch.runnerInstanceId } : {} }),
3655
+ log: (message) => console.warn(`[vo-runner supervisor] ${message}`)
3656
+ });
3657
+ if (!drain.proceed) {
3658
+ await client.completeRunnerControl(action.actionId, { ...controlIdentity, status: "failed", detail: drain.detail });
3420
3659
  handling = false;
3421
3660
  respawn();
3422
3661
  continue;
3423
3662
  }
3424
3663
  controlRecoveryFence.markBeforeStop(child);
3425
3664
  await stopChildExpectedly(child);
3426
- const bundledAction = action.kind === "update" || action.kind === "reinstall";
3427
3665
  const result = bundledAction ? stageAndActivateBundledUpdate({
3428
3666
  runtimeRoot,
3429
3667
  packageSpec: `@algosuite/vo-mcp@${action.desired_package_version}`,
@@ -3445,6 +3683,7 @@ async function main() {
3445
3683
  log: (message) => console.warn(`[vo-runner supervisor] ${message}`)
3446
3684
  });
3447
3685
  if (result.ok && result.handoff) {
3686
+ if (drain.capped) console.warn(`[vo-runner supervisor] ${drain.detail}`);
3448
3687
  console.warn(`[vo-runner supervisor] activated ${result.active.version}; exiting for new-process attestation`);
3449
3688
  return;
3450
3689
  }
@@ -3456,9 +3695,7 @@ async function main() {
3456
3695
  const relaunchedHealthy = recoveryAuthorized ? await ensureHealthyChildAfterControl() : false;
3457
3696
  if (!relaunchedHealthy) result.ok = false;
3458
3697
  await client.completeRunnerControl(action.actionId, {
3459
- runnerId,
3460
- ...operatorId ? { operatorId } : {},
3461
- ...supervisorControlIdentity,
3698
+ ...controlIdentity,
3462
3699
  status: result.ok ? "succeeded" : "failed",
3463
3700
  detail: result.ok ? result.detail ? `${result.detail}; runner ${packageVersion()} reconnected`.slice(0, 1e3) : `runner ${packageVersion()} reconnected` : `maintenance exited ${result.status}${result.detail ? `: ${result.detail}` : ""}${relaunchedHealthy ? "" : "; runner relaunch did not become healthy"}`
3464
3701
  });