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

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.
@@ -2318,6 +2318,157 @@ function resolveSupervisorChildEntry({
2318
2318
  };
2319
2319
  }
2320
2320
 
2321
+ // src/runner/update-drain-gate.mjs
2322
+ var DEFAULT_DRAIN_CAP_MS = 45 * 60 * 1e3;
2323
+ var DEFAULT_DRAIN_CHECK_MS = 3e4;
2324
+ var MAX_DRAIN_CAP_MS = 6 * 60 * 60 * 1e3;
2325
+ var MIN_DRAIN_CHECK_MS = 1e3;
2326
+ var MAX_DRAIN_CHECK_MS = 5 * 60 * 1e3;
2327
+ var UPDATE_RESTART_ERROR_MESSAGE = "runner update restart after drain timeout";
2328
+ var UPDATE_RESTART_RESULT = "runner_update_restart";
2329
+ var sleepMs = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
2330
+ function positiveNumber(raw) {
2331
+ if (raw === void 0 || raw === null || String(raw).trim() === "") return null;
2332
+ const value = Number(raw);
2333
+ return Number.isFinite(value) && value >= 0 ? value : null;
2334
+ }
2335
+ function resolveDrainCapMs(env = {}) {
2336
+ const ms = positiveNumber(env.VO_RUNNER_UPDATE_DRAIN_CAP_MS);
2337
+ if (ms !== null) return Math.min(ms, MAX_DRAIN_CAP_MS);
2338
+ const minutes = positiveNumber(env.VO_RUNNER_UPDATE_DRAIN_CAP_MIN);
2339
+ if (minutes !== null) return Math.min(minutes * 6e4, MAX_DRAIN_CAP_MS);
2340
+ return DEFAULT_DRAIN_CAP_MS;
2341
+ }
2342
+ function resolveDrainCheckMs(env = {}) {
2343
+ const ms = positiveNumber(env.VO_RUNNER_UPDATE_DRAIN_CHECK_MS);
2344
+ if (ms === null) return DEFAULT_DRAIN_CHECK_MS;
2345
+ return Math.min(Math.max(ms, MIN_DRAIN_CHECK_MS), MAX_DRAIN_CHECK_MS);
2346
+ }
2347
+ function readActiveTasks(status, { childRunning = true } = {}) {
2348
+ if (!childRunning) {
2349
+ return { count: 0, ids: [], known: true, runnerId: null, runnerInstanceId: null };
2350
+ }
2351
+ if (!status || status.ok === false) {
2352
+ return { count: 0, ids: [], known: false, runnerId: null, runnerInstanceId: null };
2353
+ }
2354
+ const ids = Array.isArray(status.activeTaskIds) ? status.activeTaskIds.map((id) => String(id)).filter(Boolean) : [];
2355
+ const reported = Number(status.activeTasks);
2356
+ const count = Number.isFinite(reported) && reported >= 0 ? reported : ids.length;
2357
+ return {
2358
+ count: Math.max(count, ids.length),
2359
+ ids,
2360
+ known: true,
2361
+ runnerId: status.runnerId ? String(status.runnerId) : null,
2362
+ runnerInstanceId: status.runnerInstanceId ? String(status.runnerInstanceId) : null
2363
+ };
2364
+ }
2365
+ function decideDrainStep({ active, elapsedMs, capMs }) {
2366
+ const busy = active.known ? active.count : "unknown";
2367
+ if (active.known && active.count === 0) return { action: "proceed", busy };
2368
+ if (elapsedMs >= capMs) return { action: "cap", busy };
2369
+ return { action: "wait", busy };
2370
+ }
2371
+ function describeBusy(active) {
2372
+ if (!active.known) return "status unreadable from a running child (assumed busy)";
2373
+ const ids = active.ids.length > 0 ? ` [${active.ids.join(", ")}]` : " [ids unavailable]";
2374
+ return `${active.count} active task(s)${ids}`;
2375
+ }
2376
+ async function markCappedTasks({ active, failInFlightTask, log }) {
2377
+ if (active.ids.length === 0) {
2378
+ if (active.count > 0 || !active.known) {
2379
+ log(`update drain cap reached with ${describeBusy(active)} \u2014 cannot annotate tasks the runner did not name`);
2380
+ }
2381
+ return [];
2382
+ }
2383
+ const marked = [];
2384
+ for (const taskId of active.ids) {
2385
+ try {
2386
+ await failInFlightTask(taskId, {
2387
+ message: UPDATE_RESTART_ERROR_MESSAGE,
2388
+ result: UPDATE_RESTART_RESULT,
2389
+ runnerId: active.runnerId,
2390
+ runnerInstanceId: active.runnerInstanceId
2391
+ });
2392
+ marked.push(taskId);
2393
+ } catch (error) {
2394
+ log(`could not mark task ${taskId} before update restart: ${error instanceof Error ? error.message : String(error)}`);
2395
+ }
2396
+ }
2397
+ return marked;
2398
+ }
2399
+ async function awaitRunnerUpdateDrain({
2400
+ readStatus,
2401
+ isChildRunning = () => true,
2402
+ failInFlightTask = async () => {
2403
+ },
2404
+ drainable = false,
2405
+ env = {},
2406
+ now = Date.now,
2407
+ sleep: sleep2 = sleepMs,
2408
+ log = () => {
2409
+ }
2410
+ } = {}) {
2411
+ const capMs = resolveDrainCapMs(env);
2412
+ const checkMs = resolveDrainCheckMs(env);
2413
+ const startedAt = now();
2414
+ let checks = 0;
2415
+ let lastReported = null;
2416
+ const snapshot = async () => {
2417
+ checks += 1;
2418
+ const childRunning = Boolean(isChildRunning());
2419
+ let status;
2420
+ try {
2421
+ status = await readStatus();
2422
+ } catch {
2423
+ status = null;
2424
+ }
2425
+ return readActiveTasks(status, { childRunning });
2426
+ };
2427
+ if (!drainable) {
2428
+ const active = await snapshot();
2429
+ if (active.known && active.count > 0) {
2430
+ return {
2431
+ proceed: false,
2432
+ detail: `deferred safely: ${active.count} active task(s); retry when the runner is idle`,
2433
+ capped: false,
2434
+ waitedMs: 0,
2435
+ checks,
2436
+ markedTaskIds: []
2437
+ };
2438
+ }
2439
+ return { proceed: true, detail: "runner idle", capped: false, waitedMs: 0, checks, markedTaskIds: [] };
2440
+ }
2441
+ for (; ; ) {
2442
+ const active = await snapshot();
2443
+ const elapsedMs = now() - startedAt;
2444
+ const step = decideDrainStep({ active, elapsedMs, capMs });
2445
+ if (step.action === "proceed") {
2446
+ if (lastReported !== null) {
2447
+ log(`update drain complete after ${Math.round(elapsedMs / 1e3)}s \u2014 runner idle, applying staged update restart`);
2448
+ }
2449
+ return { proceed: true, detail: "runner idle", capped: false, waitedMs: elapsedMs, checks, markedTaskIds: [] };
2450
+ }
2451
+ if (step.action === "cap") {
2452
+ 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`);
2453
+ const markedTaskIds = await markCappedTasks({ active, failInFlightTask, log });
2454
+ return {
2455
+ proceed: true,
2456
+ capped: true,
2457
+ detail: `update restart applied after drain cap; marked ${markedTaskIds.length} task(s) as "${UPDATE_RESTART_ERROR_MESSAGE}"`,
2458
+ waitedMs: elapsedMs,
2459
+ checks,
2460
+ markedTaskIds
2461
+ };
2462
+ }
2463
+ const fingerprint = `${step.busy}:${active.ids.join(",")}`;
2464
+ if (fingerprint !== lastReported) {
2465
+ lastReported = fingerprint;
2466
+ log(`deferring staged update restart \u2014 ${describeBusy(active)}; re-checking every ${Math.round(checkMs / 1e3)}s until idle (cap ${Math.round(capMs / 1e3)}s)`);
2467
+ }
2468
+ await sleep2(Math.max(1, Math.min(checkMs, capMs - elapsedMs)));
2469
+ }
2470
+ }
2471
+
2321
2472
  // src/runner/supervisor-child-env.mjs
2322
2473
  import { hostname as systemHostname } from "node:os";
2323
2474
 
@@ -3408,22 +3559,24 @@ async function main() {
3408
3559
  }
3409
3560
  deferredControlRecoveryGeneration = null;
3410
3561
  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
- });
3562
+ const controlIdentity = { runnerId, ...operatorId ? { operatorId } : {}, ...supervisorControlIdentity };
3563
+ const bundledAction = action.kind === "update" || action.kind === "reinstall";
3564
+ const drain = await awaitRunnerUpdateDrain({
3565
+ readStatus: localStatus,
3566
+ drainable: bundledAction,
3567
+ env: process.env,
3568
+ isChildRunning: () => supervisorChildIsRunning(child),
3569
+ 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 } : {} }),
3570
+ log: (message) => console.warn(`[vo-runner supervisor] ${message}`)
3571
+ });
3572
+ if (!drain.proceed) {
3573
+ await client.completeRunnerControl(action.actionId, { ...controlIdentity, status: "failed", detail: drain.detail });
3420
3574
  handling = false;
3421
3575
  respawn();
3422
3576
  continue;
3423
3577
  }
3424
3578
  controlRecoveryFence.markBeforeStop(child);
3425
3579
  await stopChildExpectedly(child);
3426
- const bundledAction = action.kind === "update" || action.kind === "reinstall";
3427
3580
  const result = bundledAction ? stageAndActivateBundledUpdate({
3428
3581
  runtimeRoot,
3429
3582
  packageSpec: `@algosuite/vo-mcp@${action.desired_package_version}`,
@@ -3445,6 +3598,7 @@ async function main() {
3445
3598
  log: (message) => console.warn(`[vo-runner supervisor] ${message}`)
3446
3599
  });
3447
3600
  if (result.ok && result.handoff) {
3601
+ if (drain.capped) console.warn(`[vo-runner supervisor] ${drain.detail}`);
3448
3602
  console.warn(`[vo-runner supervisor] activated ${result.active.version}; exiting for new-process attestation`);
3449
3603
  return;
3450
3604
  }
@@ -3456,9 +3610,7 @@ async function main() {
3456
3610
  const relaunchedHealthy = recoveryAuthorized ? await ensureHealthyChildAfterControl() : false;
3457
3611
  if (!relaunchedHealthy) result.ok = false;
3458
3612
  await client.completeRunnerControl(action.actionId, {
3459
- runnerId,
3460
- ...operatorId ? { operatorId } : {},
3461
- ...supervisorControlIdentity,
3613
+ ...controlIdentity,
3462
3614
  status: result.ok ? "succeeded" : "failed",
3463
3615
  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
3616
  });