@algosuite/vo-mcp 0.2.0-beta.10 → 0.2.0-beta.12

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.
@@ -2125,22 +2125,41 @@ async function resolveBearer(env2) {
2125
2125
  function createControlPlaneClient({
2126
2126
  baseUrl = process.env.VO_CONTROL_PLANE_URL || "",
2127
2127
  env: env2 = process.env,
2128
- fetchImpl = fetch
2128
+ fetchImpl = fetch,
2129
+ heartbeatTimeoutMs = Math.min(
2130
+ Math.max(Number(env2.VO_CODE_RUNNER_HEARTBEAT_TIMEOUT_MS) || 15e3, 1e3),
2131
+ 6e4
2132
+ )
2129
2133
  } = {}) {
2130
2134
  if (!baseUrl) {
2131
2135
  throw new Error("VO_CONTROL_PLANE_URL is required for the code-runner daemon");
2132
2136
  }
2133
2137
  const root = baseUrl.replace(/\/+$/, "");
2134
- async function req(method, path16, body) {
2138
+ async function req(method, path16, body, { timeoutMs } = {}) {
2135
2139
  const bearer = await resolveBearer(env2);
2136
- return fetchImpl(`${root}${path16}`, {
2140
+ const controller = timeoutMs ? new AbortController() : null;
2141
+ let timeoutId;
2142
+ const request = Promise.resolve(fetchImpl(`${root}${path16}`, {
2137
2143
  method,
2138
2144
  headers: {
2139
2145
  "content-type": "application/json",
2140
2146
  authorization: `Bearer ${bearer}`
2141
2147
  },
2142
- body: body === void 0 ? void 0 : JSON.stringify(body)
2148
+ body: body === void 0 ? void 0 : JSON.stringify(body),
2149
+ ...controller ? { signal: controller.signal } : {}
2150
+ }));
2151
+ if (!timeoutMs) return request;
2152
+ const timeout = new Promise((_, reject) => {
2153
+ timeoutId = setTimeout(() => {
2154
+ controller.abort();
2155
+ reject(new Error(`control-plane ${path16} timed out after ${timeoutMs}ms`));
2156
+ }, timeoutMs);
2143
2157
  });
2158
+ try {
2159
+ return await Promise.race([request, timeout]);
2160
+ } finally {
2161
+ clearTimeout(timeoutId);
2162
+ }
2144
2163
  }
2145
2164
  return {
2146
2165
  /**
@@ -2156,6 +2175,10 @@ function createControlPlaneClient({
2156
2175
  if (Array.isArray(operatorIds) && operatorIds.length > 0) body.operator_ids = operatorIds;
2157
2176
  if (session.runnerInstanceId) body.runner_instance_id = session.runnerInstanceId;
2158
2177
  if (session.runnerInstanceId && session.reconcileStale) body.reconcile_stale = true;
2178
+ if (session.defaultAgent) body.default_agent = session.defaultAgent;
2179
+ if (Array.isArray(session.availableAgents)) {
2180
+ body.available_agents = session.availableAgents.filter((entry) => entry?.installed === true && entry?.authenticated === true).map((entry) => entry.agent);
2181
+ }
2159
2182
  const res = await req("POST", "/api/v1/code-task/claim", body);
2160
2183
  if (res.status === 401) {
2161
2184
  cachedFirebaseToken = null;
@@ -2299,13 +2322,20 @@ function createControlPlaneClient({
2299
2322
  * authenticated operator so the web shows a TRUE "runner online" signal.
2300
2323
  * Best-effort caller; throws on 401/non-ok so the daemon can log + retry.
2301
2324
  */
2302
- async postHeartbeat({ runnerId, operatorId, uptimeSec, activeTasks, version, daemonVersion, servedRepos, servedOperators, availableAgents, accountUsage }) {
2325
+ async postHeartbeat({ runnerId, runnerInstanceId, operatorId, uptimeSec, activeTasks, version, daemonVersion, defaultAgent, supervisorInstanceId, supervisorVersion, supervisorCapabilities, servedRepos, servedOperators, availableAgents, accountUsage }) {
2303
2326
  const body = { runner_id: runnerId };
2327
+ if (runnerInstanceId) body.runner_instance_id = runnerInstanceId;
2304
2328
  if (operatorId) body.operator_id = operatorId;
2305
2329
  if (typeof uptimeSec === "number") body.uptime_sec = uptimeSec;
2306
2330
  if (typeof activeTasks === "number") body.active_tasks = activeTasks;
2307
2331
  if (version) body.version = version;
2308
2332
  if (daemonVersion) body.daemon_version = daemonVersion;
2333
+ if (defaultAgent) body.default_agent = defaultAgent;
2334
+ if (supervisorInstanceId) body.supervisor_instance_id = supervisorInstanceId;
2335
+ if (supervisorVersion) body.supervisor_version = supervisorVersion;
2336
+ if (Array.isArray(supervisorCapabilities) && supervisorCapabilities.length > 0) {
2337
+ body.supervisor_capabilities = supervisorCapabilities;
2338
+ }
2309
2339
  if (Array.isArray(servedRepos) && servedRepos.length > 0) body.served_repos = servedRepos;
2310
2340
  if (Array.isArray(servedOperators) && servedOperators.length > 0) {
2311
2341
  body.served_operator_ids = servedOperators;
@@ -2316,7 +2346,9 @@ function createControlPlaneClient({
2316
2346
  if (Array.isArray(accountUsage) && accountUsage.length > 0) {
2317
2347
  body.account_usage = accountUsage;
2318
2348
  }
2319
- const res = await req("POST", "/api/v1/runner/heartbeat", body);
2349
+ const res = await req("POST", "/api/v1/runner/heartbeat", body, {
2350
+ timeoutMs: heartbeatTimeoutMs
2351
+ });
2320
2352
  if (res.status === 401) {
2321
2353
  cachedFirebaseToken = null;
2322
2354
  throw new Error("heartbeat unauthorized (401)");
@@ -2324,10 +2356,27 @@ function createControlPlaneClient({
2324
2356
  if (!res.ok) throw new Error(`heartbeat failed: HTTP ${res.status}`);
2325
2357
  return true;
2326
2358
  },
2359
+ /** Read the server-authoritative heartbeat ledger without mutating it. */
2360
+ async getRunnerStatus({ operatorId } = {}) {
2361
+ const query = operatorId ? `?operator_id=${encodeURIComponent(operatorId)}` : "";
2362
+ const res = await req("GET", `/api/v1/runner/status${query}`, void 0, {
2363
+ timeoutMs: heartbeatTimeoutMs
2364
+ });
2365
+ if (res.status === 401) {
2366
+ cachedFirebaseToken = null;
2367
+ throw new Error("runner status unauthorized (401)");
2368
+ }
2369
+ if (!res.ok) throw new Error(`runner status failed: HTTP ${res.status}`);
2370
+ const body = await res.json();
2371
+ return Array.isArray(body?.runners) ? body.runners : [];
2372
+ },
2327
2373
  /** Poll one authenticated runner's durable Mission Control action queue. */
2328
- async pollRunnerControl({ runnerId, operatorId }) {
2374
+ async pollRunnerControl({ runnerId, operatorId, supervisorInstanceId, supervisorVersion, capabilities }) {
2329
2375
  const body = { runner_id: runnerId };
2330
2376
  if (operatorId) body.operator_id = operatorId;
2377
+ if (supervisorInstanceId) body.supervisor_instance_id = supervisorInstanceId;
2378
+ if (supervisorVersion) body.supervisor_version = supervisorVersion;
2379
+ if (Array.isArray(capabilities) && capabilities.length > 0) body.capabilities = capabilities;
2331
2380
  const res = await req("POST", "/api/v1/runner/control/poll", body);
2332
2381
  if (res.status === 401) {
2333
2382
  cachedFirebaseToken = null;
@@ -2339,9 +2388,12 @@ function createControlPlaneClient({
2339
2388
  return action && typeof action.action_id === "string" && action.action_id ? { ...action, actionId: action.action_id } : null;
2340
2389
  },
2341
2390
  /** Acknowledge a maintenance action after the host has restarted the child. */
2342
- async completeRunnerControl(actionId, { runnerId, operatorId, status, detail }) {
2391
+ async completeRunnerControl(actionId, { runnerId, operatorId, supervisorInstanceId, supervisorVersion, capabilities, status, detail }) {
2343
2392
  const body = { runner_id: runnerId, status };
2344
2393
  if (operatorId) body.operator_id = operatorId;
2394
+ if (supervisorInstanceId) body.supervisor_instance_id = supervisorInstanceId;
2395
+ if (supervisorVersion) body.supervisor_version = supervisorVersion;
2396
+ if (Array.isArray(capabilities) && capabilities.length > 0) body.capabilities = capabilities;
2345
2397
  if (detail) body.detail = detail;
2346
2398
  const res = await req("POST", `/api/v1/runner/control/${encodeURIComponent(actionId)}/complete`, body);
2347
2399
  if (res.status === 401) {
@@ -2423,17 +2475,55 @@ function cleanPathSegment(value) {
2423
2475
  const trimmed = String(value || "").trim();
2424
2476
  return trimmed.startsWith('"') && trimmed.endsWith('"') ? trimmed.slice(1, -1) : trimmed;
2425
2477
  }
2478
+ function envValue(env2, name) {
2479
+ const exact = env2?.[name];
2480
+ if (typeof exact === "string") return exact.trim();
2481
+ const key = Object.keys(env2 || {}).find((candidate) => candidate.toLowerCase() === name.toLowerCase());
2482
+ return typeof env2?.[key] === "string" ? env2[key].trim() : "";
2483
+ }
2484
+ function userClaudeCandidates(bin, env2) {
2485
+ if (!/^claude(?:\.(?:exe|cmd|ps1))?$/iu.test(bin)) return [];
2486
+ const userProfile = envValue(env2, "USERPROFILE");
2487
+ const appData = envValue(env2, "APPDATA") || (userProfile ? path9.join(userProfile, "AppData", "Roaming") : "");
2488
+ const localAppData = envValue(env2, "LOCALAPPDATA") || (userProfile ? path9.join(userProfile, "AppData", "Local") : "");
2489
+ const candidates = [];
2490
+ if (appData) {
2491
+ const npmBin = path9.join(appData, "npm");
2492
+ candidates.push(
2493
+ path9.join(npmBin, "claude.exe"),
2494
+ path9.join(npmBin, "claude.cmd"),
2495
+ path9.join(npmBin, "claude.ps1"),
2496
+ path9.join(npmBin, "claude"),
2497
+ path9.join(npmBin, ...NATIVE_CLAUDE_PARTS)
2498
+ );
2499
+ }
2500
+ if (userProfile) candidates.push(path9.join(userProfile, ".local", "bin", "claude.exe"));
2501
+ if (localAppData) {
2502
+ candidates.push(
2503
+ path9.join(localAppData, "Microsoft", "WinGet", "Links", "claude.exe"),
2504
+ path9.join(localAppData, "Microsoft", "WindowsApps", "claude.exe")
2505
+ );
2506
+ }
2507
+ return candidates;
2508
+ }
2426
2509
  function pathCandidates(bin, env2) {
2427
2510
  if (path9.isAbsolute(bin) || /[\\/]/u.test(bin)) {
2428
2511
  return [path9.resolve(bin)];
2429
2512
  }
2430
2513
  const extension = path9.extname(bin);
2431
- return pathValue(env2).split(";").map(cleanPathSegment).filter(Boolean).flatMap((directory) => extension ? [path9.join(directory, bin)] : [
2514
+ const fromPath = pathValue(env2).split(";").map(cleanPathSegment).filter(Boolean).flatMap((directory) => extension ? [path9.join(directory, bin)] : [
2432
2515
  path9.join(directory, `${bin}.exe`),
2433
2516
  path9.join(directory, `${bin}.cmd`),
2434
2517
  path9.join(directory, `${bin}.ps1`),
2435
2518
  path9.join(directory, bin)
2436
2519
  ]);
2520
+ const seen = /* @__PURE__ */ new Set();
2521
+ return [...fromPath, ...userClaudeCandidates(bin, env2)].filter((candidate) => {
2522
+ const key = candidate.toLowerCase();
2523
+ if (seen.has(key)) return false;
2524
+ seen.add(key);
2525
+ return true;
2526
+ });
2437
2527
  }
2438
2528
  function canonicalExistingPath(candidate, exists, canonicalize) {
2439
2529
  if (!exists(candidate)) return null;
@@ -2462,7 +2552,7 @@ function resolveWindowsClaudeExecutable({
2462
2552
  if (resolvedNative) return resolvedNative;
2463
2553
  }
2464
2554
  const error = new Error(
2465
- `Could not resolve a native claude.exe for "${requested}". Update Claude Code with npm install -g @anthropic-ai/claude-code; the HQ runner will not execute a shell-only .cmd/.ps1 shim.`
2555
+ `Could not resolve a native claude.exe for "${requested}". Install or update Claude Code with the native Windows installer (recommended) or npm install -g @anthropic-ai/claude-code; the HQ runner will not execute a shell-only .cmd/.ps1 shim.`
2466
2556
  );
2467
2557
  error.code = "ENOENT";
2468
2558
  throw error;
@@ -4779,7 +4869,7 @@ var init_dispatch_onboarding = __esm({
4779
4869
  "docs/current/virtual-office-operating-model.md",
4780
4870
  "docs/current/virtual-office-test-architect.md",
4781
4871
  "docs/current/evidence-grounded-consensus-testing.md",
4782
- "docs/vo/ADR-001-verification-oracle-not-orchestrator-2026-05-29.md (VO verifies + signs; human approves merges; NO autonomous bot-merge / headless triggers)",
4872
+ "docs/vo/ADR-001-verification-oracle-not-orchestrator-2026-05-29.md (AlgoHQ verifies + signs; human approves merges; NO autonomous bot-merge / headless triggers)",
4783
4873
  "docs/vo/vo-adr-002-two-plane-moat.md (fat secret server / thin dumb client)",
4784
4874
  "docs/vo/vo-roadmap-2026-05-26.md (the live roadmap \u2014 read its Change log tail for current state)",
4785
4875
  "the nearest scoped CLAUDE.md for any directory you edit",
@@ -5240,6 +5330,7 @@ function makeLoopTicks({
5240
5330
  env: env2,
5241
5331
  log: log3,
5242
5332
  getActive,
5333
+ runnerInstanceId,
5243
5334
  // Cached agent-availability provider (agent-availability.mjs); returns null
5244
5335
  // until the first probe completes — the heartbeat simply omits the field.
5245
5336
  getAgentAvailability = () => null,
@@ -5251,9 +5342,49 @@ function makeLoopTicks({
5251
5342
  }) {
5252
5343
  let lastSessionForward = 0;
5253
5344
  let lastHeartbeat = 0;
5345
+ let availabilityWasReady = false;
5346
+ const heartbeatState = /* @__PURE__ */ new Map();
5254
5347
  let lastResumeSchedule = 0;
5255
5348
  let resumeRunning = false;
5349
+ function enqueueHeartbeat(payload) {
5350
+ const key = payload.operatorId || "";
5351
+ let state = heartbeatState.get(key);
5352
+ if (!state) {
5353
+ state = { running: false, pending: null };
5354
+ heartbeatState.set(key, state);
5355
+ }
5356
+ return new Promise((resolve2) => {
5357
+ if (state.running) {
5358
+ if (state.pending) state.pending.waiters.push(resolve2);
5359
+ else state.pending = { payload, waiters: [resolve2] };
5360
+ state.pending.payload = payload;
5361
+ return;
5362
+ }
5363
+ const launch = (nextPayload, waiters) => {
5364
+ state.running = true;
5365
+ let request;
5366
+ try {
5367
+ request = Promise.resolve(client.postHeartbeat(nextPayload));
5368
+ } catch (error) {
5369
+ request = Promise.reject(error);
5370
+ }
5371
+ request.catch((e) => log3(`heartbeat failed: ${e.message}`)).finally(() => {
5372
+ for (const done of waiters) done();
5373
+ if (state.pending) {
5374
+ const pending = state.pending;
5375
+ state.pending = null;
5376
+ launch(pending.payload, pending.waiters);
5377
+ } else {
5378
+ state.running = false;
5379
+ heartbeatState.delete(key);
5380
+ }
5381
+ });
5382
+ };
5383
+ launch(payload, [resolve2]);
5384
+ });
5385
+ }
5256
5386
  return function tick() {
5387
+ const heartbeatCompletions = [];
5257
5388
  const now = nowFn();
5258
5389
  if (cfg.sessionForwardSec > 0 && now - lastSessionForward >= cfg.sessionForwardSec * 1e3) {
5259
5390
  lastSessionForward = now;
@@ -5264,18 +5395,29 @@ function makeLoopTicks({
5264
5395
  }).catch(() => {
5265
5396
  });
5266
5397
  }
5267
- if (now - lastHeartbeat >= HEARTBEAT_MS) {
5398
+ const availableAgents = getAgentAvailability();
5399
+ const availabilityReady = Array.isArray(availableAgents);
5400
+ const availabilityJustBecameReady = availabilityReady && !availabilityWasReady;
5401
+ availabilityWasReady = availabilityReady;
5402
+ if (now - lastHeartbeat >= HEARTBEAT_MS || availabilityJustBecameReady) {
5268
5403
  lastHeartbeat = now;
5269
5404
  const servedRepos = Array.isArray(cfg.servedRepos) ? cfg.servedRepos.slice(0, 100) : [];
5270
5405
  const servedOperators = Array.isArray(cfg.servedOperators) ? cfg.servedOperators.slice(0, 100) : [];
5271
- const availableAgents = getAgentAvailability();
5272
5406
  const accountUsage = getAccountUsage();
5273
5407
  const version = String(env2.VO_CODE_RUNNER_VERSION || "").trim().slice(0, 40);
5274
5408
  const daemonVersion = String(env2.VO_CODE_RUNNER_DAEMON_VERSION || "").trim().slice(0, 40);
5409
+ const supervisorInstanceId = String(env2.VO_RUNNER_SUPERVISOR_INSTANCE_ID || "").trim();
5410
+ const supervisorVersion = String(env2.VO_RUNNER_SUPERVISOR_VERSION || "").trim().slice(0, 40);
5411
+ const supervisorCapabilities = String(env2.VO_RUNNER_SUPERVISOR_CAPABILITIES || "").split(",").map((value) => value.trim()).filter(Boolean).slice(0, 8);
5275
5412
  const baseHeartbeat = {
5276
5413
  runnerId: cfg.runnerId,
5414
+ ...runnerInstanceId ? { runnerInstanceId } : {},
5277
5415
  ...version ? { version } : {},
5278
5416
  ...daemonVersion ? { daemonVersion } : {},
5417
+ ...cfg.agent ? { defaultAgent: cfg.agent } : {},
5418
+ ...supervisorInstanceId ? { supervisorInstanceId } : {},
5419
+ ...supervisorVersion ? { supervisorVersion } : {},
5420
+ ...supervisorCapabilities.length > 0 ? { supervisorCapabilities } : {},
5279
5421
  ...servedRepos.length > 0 ? { servedRepos } : {},
5280
5422
  ...servedOperators.length > 0 ? { servedOperators } : {},
5281
5423
  ...Array.isArray(availableAgents) && availableAgents.length > 0 ? { availableAgents } : {},
@@ -5285,7 +5427,10 @@ function makeLoopTicks({
5285
5427
  };
5286
5428
  const operatorIds = servedOperators.length > 0 ? servedOperators : [void 0];
5287
5429
  for (const operatorId of operatorIds) {
5288
- client.postHeartbeat({ ...baseHeartbeat, ...operatorId ? { operatorId } : {} }).catch((e) => log3(`heartbeat failed: ${e.message}`));
5430
+ heartbeatCompletions.push(enqueueHeartbeat({
5431
+ ...baseHeartbeat,
5432
+ ...operatorId ? { operatorId } : {}
5433
+ }));
5289
5434
  }
5290
5435
  }
5291
5436
  const resumeSec = Number(env2.VO_RESUME_SCHEDULE_SEC) > 0 ? Number(env2.VO_RESUME_SCHEDULE_SEC) : DEFAULT_RESUME_SCHEDULE_SEC;
@@ -5296,6 +5441,7 @@ function makeLoopTicks({
5296
5441
  resumeRunning = false;
5297
5442
  });
5298
5443
  }
5444
+ return Promise.all(heartbeatCompletions).then(() => void 0);
5299
5445
  };
5300
5446
  }
5301
5447
  var HEARTBEAT_MS, DEFAULT_RESUME_SCHEDULE_SEC;
@@ -5310,6 +5456,10 @@ var init_loop_ticks = __esm({
5310
5456
  });
5311
5457
 
5312
5458
  // ../../scripts/virtual-office/code-runner/agent-availability.mjs
5459
+ function resolveAgentClaimContext(provider, defaultAgent) {
5460
+ const availableAgents = provider.get();
5461
+ return Array.isArray(availableAgents) ? { availableAgents, defaultAgent } : null;
5462
+ }
5313
5463
  async function collectAgentAvailability({
5314
5464
  agents = listAgents(),
5315
5465
  runnerFor = (agent) => resolveRunner({ VO_CODE_RUNNER_AGENT: agent }).runner
@@ -6085,7 +6235,7 @@ function startControlServer({ port, getStatus, requestStop, allowedOrigin, log:
6085
6235
  server.unref?.();
6086
6236
  return server;
6087
6237
  }
6088
- function startDaemonControl({ cfg, requestStop, getActiveCount, isRunning, startedAt, log: log3 = () => {
6238
+ function startDaemonControl({ cfg, runnerInstanceId, requestStop, getActiveCount, isRunning, startedAt, log: log3 = () => {
6089
6239
  }, onDuplicate = null }) {
6090
6240
  if (!cfg.controlEnabled) return null;
6091
6241
  return startControlServer({
@@ -6097,6 +6247,7 @@ function startDaemonControl({ cfg, requestStop, getActiveCount, isRunning, start
6097
6247
  running: isRunning(),
6098
6248
  pid: process.pid,
6099
6249
  runnerId: cfg.runnerId,
6250
+ runnerInstanceId,
6100
6251
  servedRepos: cfg.servedRepos,
6101
6252
  servedOperators: cfg.servedOperators,
6102
6253
  watchEnabled: cfg.watchEnabled,
@@ -7626,7 +7777,7 @@ function reportsBlocker(summary) {
7626
7777
  const text = String(summary || "");
7627
7778
  const explicit = explicitTaskOutcome(text);
7628
7779
  if (explicit) return explicit === "BLOCKED" || explicit === "FAILED";
7629
- return /\btask remains\s+(?:\*\*)?BLOCKED\b/i.test(text) || /(?:^|\n)\s*(?:#{1,6}\s*)?(?:host-recovery\s+)?(?:result|outcome)\s*:\s*(?:\*\*)?BLOCKED\b/im.test(text);
7780
+ return /\btask remains\s+(?:\*\*)?BLOCKED\b/i.test(text) || /(?:^|\n)\s*(?:#{1,6}\s*)?(?:\*\*)?(?:host-recovery\s+)?(?:result|outcome|status)\s*:\s*(?:\*\*)?BLOCKED\b/im.test(text);
7630
7781
  }
7631
7782
  function isMaxTurnExhaustion(run = {}, maxTurns) {
7632
7783
  const summary = String(run.summary || "").trim().toLowerCase();
@@ -7902,6 +8053,7 @@ async function main({ env: env2 = process.env, once: once2 = false } = {}) {
7902
8053
  const startedAt = Date.now();
7903
8054
  const controlServer = startDaemonControl({
7904
8055
  cfg,
8056
+ runnerInstanceId,
7905
8057
  requestStop: () => stop("web-control"),
7906
8058
  getActiveCount: () => active,
7907
8059
  isRunning: () => !stopping,
@@ -7929,18 +8081,24 @@ async function main({ env: env2 = process.env, once: once2 = false } = {}) {
7929
8081
  const watchCoordinator = makeWatchCycleCoordinator({ runWatch, log: log2, intervalMs: cfg.watchIntervalSec * 1e3 });
7930
8082
  const agentAvailability = makeAgentAvailabilityProvider({ onError: (e) => log2(`agent probe failed: ${e.message}`) });
7931
8083
  const accountUsage = makeAccountUsageProvider();
7932
- const loopTick = makeLoopTicks({ client, cfg, env: env2, log: log2, getActive: () => active, getAgentAvailability: () => agentAvailability.get(), getAccountUsage: () => accountUsage.get() });
8084
+ const loopTick = makeLoopTicks({ client, cfg, env: env2, log: log2, getActive: () => active, runnerInstanceId, getAgentAvailability: () => agentAvailability.get(), getAccountUsage: () => accountUsage.get() });
7933
8085
  const backoff = makeReconnectBackoff({ baseMs: cfg.pollSec * 1e3, log: log2 });
7934
8086
  while (!stopping) {
7935
- loopTick();
8087
+ const heartbeatCompletion = loopTick();
7936
8088
  if (cfg.watchEnabled) watchCoordinator.start();
7937
8089
  if (active >= cfg.maxConcurrency) {
7938
8090
  await sleep2(cfg.pollSec * 1e3);
7939
8091
  continue;
7940
8092
  }
8093
+ const claimAgents = resolveAgentClaimContext(agentAvailability, cfg.agent);
8094
+ if (!claimAgents) {
8095
+ await sleep2(cfg.pollSec * 1e3);
8096
+ continue;
8097
+ }
8098
+ await heartbeatCompletion;
7941
8099
  let task;
7942
8100
  try {
7943
- task = await client.claim(cfg.runnerId, cfg.servedRepos, cfg.servedOperators, { runnerInstanceId, reconcileStale });
8101
+ task = await client.claim(cfg.runnerId, cfg.servedRepos, cfg.servedOperators, { runnerInstanceId, reconcileStale, ...claimAgents });
7944
8102
  reconcileStale = false;
7945
8103
  backoff.onSuccess();
7946
8104
  } catch (err) {