@algosuite/vo-mcp 0.2.0-beta.22 → 0.2.0-beta.24

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.
@@ -61,8 +61,130 @@ async function fetchInstallationToken({ req, required = false, readOnly = false,
61
61
  }
62
62
  }
63
63
 
64
+ // ../../scripts/virtual-office/code-runner/control-plane-task-list.mjs
65
+ var PAGE_SIZE = 500;
66
+ async function listAllPrOpenedTasks(request) {
67
+ const tasks = [];
68
+ let beforeCreatedAt = "";
69
+ let beforeId = "";
70
+ for (; ; ) {
71
+ const params = new URLSearchParams({
72
+ status: "pr_opened",
73
+ limit: String(PAGE_SIZE),
74
+ runner_adoption: "1"
75
+ });
76
+ if (beforeCreatedAt) {
77
+ params.set("before_created_at", beforeCreatedAt);
78
+ params.set("before_id", beforeId);
79
+ }
80
+ const res = await request("GET", `/api/v1/code-task?${params}`);
81
+ if (!res.ok) throw new Error(`listPrOpenedTasks failed: HTTP ${res.status}`);
82
+ const json = await res.json();
83
+ const page = Array.isArray(json?.tasks) ? json.tasks : [];
84
+ tasks.push(...page);
85
+ if (page.length < PAGE_SIZE) return tasks;
86
+ const last = page.at(-1);
87
+ if (!last?.created_at || !last?.code_task_id) {
88
+ throw new Error("listPrOpenedTasks pagination cursor missing");
89
+ }
90
+ beforeCreatedAt = last.created_at;
91
+ beforeId = last.code_task_id;
92
+ }
93
+ }
94
+
95
+ // ../../scripts/virtual-office/code-runner/control-plane-resume.mjs
96
+ async function resumeCodeTaskRequest(req, taskId, { automaticRateLimit = false, automaticContinuation = false } = {}, onUnauthorized = () => {
97
+ }) {
98
+ const res = await req(
99
+ "POST",
100
+ `/api/v1/code-task/${encodeURIComponent(taskId)}/resume`,
101
+ automaticRateLimit ? { automatic_rate_limit: true } : automaticContinuation ? { automatic_continuation: true } : {}
102
+ );
103
+ if (res.status === 401) {
104
+ onUnauthorized();
105
+ throw new Error("resume unauthorized (401)");
106
+ }
107
+ if (!res.ok) throw new Error(`resume failed: HTTP ${res.status}`);
108
+ const json = await res.json();
109
+ return json && json.task ? json.task : null;
110
+ }
111
+
112
+ // ../../scripts/virtual-office/code-runner/control-plane-autonomous-admission.mjs
113
+ function makeAutonomousDispatchAdmissionClient(req, timeoutMs, onUnauthorized = () => {
114
+ }) {
115
+ return {
116
+ async reserveAutonomousDispatchBudget({ requestedBudgetUsd, reservationId, occurrenceKey }) {
117
+ const res = await req("POST", "/api/v1/autonomous-dispatch/admission", {
118
+ requested_budget_usd: requestedBudgetUsd,
119
+ reservation_id: reservationId,
120
+ dispatch_occurrence_key: occurrenceKey
121
+ }, { timeoutMs });
122
+ if (res.status === 401) {
123
+ onUnauthorized();
124
+ throw new Error("autonomous dispatch admission unauthorized (401)");
125
+ }
126
+ if (!res.ok) throw new Error(`autonomous dispatch admission failed: HTTP ${res.status}`);
127
+ const body = await res.json();
128
+ return {
129
+ allowed: body?.allowed === true,
130
+ reason: typeof body?.reason === "string" ? body.reason : ""
131
+ };
132
+ },
133
+ async releaseAutonomousDispatchBudget(reservationId) {
134
+ const res = await req("POST", "/api/v1/autonomous-dispatch/reservation/release", {
135
+ reservation_id: reservationId
136
+ }, { timeoutMs });
137
+ if (res.status === 401) {
138
+ onUnauthorized();
139
+ throw new Error("autonomous dispatch release unauthorized (401)");
140
+ }
141
+ if (!res.ok) throw new Error(`autonomous dispatch release failed: HTTP ${res.status}`);
142
+ return true;
143
+ }
144
+ };
145
+ }
146
+
147
+ // ../../scripts/virtual-office/code-runner/control-plane-merge.mjs
148
+ async function mergeVerifiedPrRequest(req, prNumber, automationContext, onUnauthorized) {
149
+ const res = await req(
150
+ "POST",
151
+ "/api/v1/admin/pr/merge",
152
+ { prNumber, automationContext },
153
+ { timeoutMs: 12e4 }
154
+ );
155
+ if (res.status === 401) {
156
+ onUnauthorized();
157
+ throw new Error("gated merge unauthorized (401)");
158
+ }
159
+ const json = await res.json().catch(() => ({}));
160
+ if (res.ok && json?.ok === true) {
161
+ const result = json?.result && typeof json.result === "object" ? json.result : {};
162
+ const status = result.merged === true || result.status === "merged" ? "merged" : result.status === "auto-merge-enabled" || String(result.action || "").includes("auto-merge") ? "queued" : "accepted";
163
+ return {
164
+ status,
165
+ detail: typeof result.detail === "string" ? result.detail : null,
166
+ actionReceiptId: typeof json.action_receipt_id === "string" ? json.action_receipt_id : null
167
+ };
168
+ }
169
+ if (res.status === 503 && json?.error === "verify_unavailable") {
170
+ return { status: "retry", reason: json.reason || "verification unavailable" };
171
+ }
172
+ return {
173
+ status: "blocked",
174
+ reason: json?.reason || json?.message || json?.error || `HTTP ${res.status}`,
175
+ actionReceiptId: typeof json?.action_receipt_id === "string" ? json.action_receipt_id : null
176
+ };
177
+ }
178
+
64
179
  // ../../scripts/virtual-office/code-runner/control-plane-client.mjs
65
180
  var cachedFirebaseToken = null;
181
+ var ClaimAuthorityChangedError = class extends Error {
182
+ constructor() {
183
+ super("code-task claim authority changed");
184
+ this.name = "ClaimAuthorityChangedError";
185
+ this.code = "code_task_claim_authority_changed";
186
+ }
187
+ };
66
188
  async function resolveBearer(env) {
67
189
  const adminToken = env.VO_CONTROL_PLANE_ADMIN_TOKEN;
68
190
  if (adminToken) return adminToken;
@@ -84,12 +206,16 @@ function createControlPlaneClient({
84
206
  heartbeatTimeoutMs = Math.min(
85
207
  Math.max(Number(env.VO_CODE_RUNNER_HEARTBEAT_TIMEOUT_MS) || 15e3, 1e3),
86
208
  6e4
87
- )
209
+ ),
210
+ taskRequestTimeoutMs = Math.min(
211
+ Math.max(Number(env.VO_CODE_RUNNER_TASK_REQUEST_TIMEOUT_MS) || 5e3, 100),
212
+ 6e4
213
+ ),
214
+ runnerId: runnerId2,
215
+ runnerInstanceId
88
216
  } = {}) {
89
217
  const resolvedBaseUrl = baseUrl ?? env.VO_CONTROL_PLANE_URL ?? "";
90
- if (!resolvedBaseUrl) {
91
- throw new Error("VO_CONTROL_PLANE_URL is required for the code-runner daemon");
92
- }
218
+ if (!resolvedBaseUrl) throw new Error("VO_CONTROL_PLANE_URL is required for the code-runner daemon");
93
219
  const root = resolvedBaseUrl.replace(/\/+$/, "");
94
220
  async function req(method, path2, body, { timeoutMs } = {}) {
95
221
  const bearer = await resolveBearer(env);
@@ -117,7 +243,15 @@ function createControlPlaneClient({
117
243
  clearTimeout(timeoutId);
118
244
  }
119
245
  }
246
+ const taskReq = (method, path2, body, options = {}) => req(method, path2, body, { timeoutMs: taskRequestTimeoutMs, ...options });
120
247
  return {
248
+ ...makeAutonomousDispatchAdmissionClient(
249
+ req,
250
+ taskRequestTimeoutMs,
251
+ () => {
252
+ cachedFirebaseToken = null;
253
+ }
254
+ ),
121
255
  /**
122
256
  * Claim the next pending task. Returns the task or null (empty queue).
123
257
  * `repos` (optional `owner/name` list) and `operatorIds` (optional
@@ -125,17 +259,20 @@ function createControlPlaneClient({
125
259
  * serves — the control-plane filters by both (logical AND), so another
126
260
  * operator's task never lands on (or bills) this machine.
127
261
  */
128
- async claim(runnerId2, repos, operatorIds, session = {}) {
129
- const body = { runner_id: runnerId2 };
262
+ async claim(runnerId3, repos, operatorIds, session = {}) {
263
+ const body = { runner_id: runnerId3 };
130
264
  if (Array.isArray(repos) && repos.length > 0) body.repos = repos;
131
265
  if (Array.isArray(operatorIds) && operatorIds.length > 0) body.operator_ids = operatorIds;
132
- if (session.runnerInstanceId) body.runner_instance_id = session.runnerInstanceId;
266
+ if (session.runnerInstanceId) {
267
+ body.runner_instance_id = session.runnerInstanceId;
268
+ body.runner_progress_protocol_version = 2;
269
+ }
133
270
  if (session.runnerInstanceId && session.reconcileStale) body.reconcile_stale = true;
134
271
  if (session.defaultAgent) body.default_agent = session.defaultAgent;
135
272
  if (Array.isArray(session.availableAgents)) {
136
273
  body.available_agents = session.availableAgents.filter((entry) => entry?.installed === true && entry?.authenticated === true).map((entry) => entry.agent);
137
274
  }
138
- const res = await req("POST", "/api/v1/code-task/claim", body);
275
+ const res = await taskReq("POST", "/api/v1/code-task/claim", body);
139
276
  if (res.status === 401) {
140
277
  cachedFirebaseToken = null;
141
278
  throw new Error("claim unauthorized (401)");
@@ -149,11 +286,19 @@ function createControlPlaneClient({
149
286
  * Server derives operator/tenant from the daemon's authenticated principal.
150
287
  * Returns the created task, or throws on a non-2xx response.
151
288
  */
152
- async enqueueCodeTask({ repo, prompt, max_budget_usd, max_turns }) {
289
+ async enqueueCodeTask({ repo, prompt, max_budget_usd, max_turns, dispatch_mode, tier, agent, model, dispatch_occurrence_key, autonomous_reservation_id, on_behalf_of_operator_id, repair_pr_number, repair_kind, repair_head_sha, repair_chain }) {
153
290
  const body = { repo, prompt };
154
291
  if (typeof max_budget_usd === "number") body.max_budget_usd = max_budget_usd;
155
292
  if (typeof max_turns === "number") body.max_turns = max_turns;
156
- const res = await req("POST", "/api/v1/code-task", body);
293
+ for (const [key, value] of Object.entries({ dispatch_mode, tier, agent, model, repair_kind, repair_head_sha })) {
294
+ if (value) body[key] = value;
295
+ }
296
+ if (dispatch_occurrence_key) body.dispatch_occurrence_key = dispatch_occurrence_key;
297
+ if (autonomous_reservation_id) body.autonomous_reservation_id = autonomous_reservation_id;
298
+ if (on_behalf_of_operator_id) body.on_behalf_of_operator_id = on_behalf_of_operator_id;
299
+ if (Number.isInteger(repair_pr_number) && repair_pr_number > 0) body.repair_pr_number = repair_pr_number;
300
+ if (repair_chain) body.repair_chain = repair_chain;
301
+ const res = await taskReq("POST", "/api/v1/code-task", body);
157
302
  if (res.status === 401) {
158
303
  cachedFirebaseToken = null;
159
304
  throw new Error("enqueue unauthorized (401)");
@@ -166,15 +311,10 @@ function createControlPlaneClient({
166
311
  * Resume a failed/cancelled/max-turn partial code-task. The PR watcher uses
167
312
  * this after the runner opens a partial draft PR and CI is no longer pending.
168
313
  */
169
- async resumeCodeTask(taskId) {
170
- const res = await req("POST", `/api/v1/code-task/${encodeURIComponent(taskId)}/resume`, {});
171
- if (res.status === 401) {
314
+ async resumeCodeTask(taskId, { automaticRateLimit = false, automaticContinuation = false } = {}) {
315
+ return resumeCodeTaskRequest(taskReq, taskId, { automaticRateLimit, automaticContinuation }, () => {
172
316
  cachedFirebaseToken = null;
173
- throw new Error("resume unauthorized (401)");
174
- }
175
- if (!res.ok) throw new Error(`resume failed: HTTP ${res.status}`);
176
- const json = await res.json();
177
- return json && json.task ? json.task : null;
317
+ });
178
318
  },
179
319
  /**
180
320
  * Send a CI-green PR through the production verify-before-act merge route.
@@ -182,29 +322,14 @@ function createControlPlaneClient({
182
322
  * consensus, records a receipt, and direct-merges only the inspected SHA.
183
323
  */
184
324
  async mergeVerifiedPr(prNumber, automationContext) {
185
- const res = await req("POST", "/api/v1/admin/pr/merge", { prNumber, automationContext });
186
- if (res.status === 401) {
187
- cachedFirebaseToken = null;
188
- throw new Error("gated merge unauthorized (401)");
189
- }
190
- const json = await res.json().catch(() => ({}));
191
- if (res.ok && json?.ok === true) {
192
- const result = json?.result && typeof json.result === "object" ? json.result : {};
193
- const status = result.merged === true || result.status === "merged" ? "merged" : result.status === "auto-merge-enabled" || String(result.action || "").includes("auto-merge") ? "queued" : "accepted";
194
- return {
195
- status,
196
- detail: typeof result.detail === "string" ? result.detail : null,
197
- actionReceiptId: typeof json.action_receipt_id === "string" ? json.action_receipt_id : null
198
- };
199
- }
200
- if (res.status === 503 && json?.error === "verify_unavailable") {
201
- return { status: "retry", reason: json.reason || "verification unavailable" };
202
- }
203
- return {
204
- status: "blocked",
205
- reason: json?.reason || json?.message || json?.error || `HTTP ${res.status}`,
206
- actionReceiptId: typeof json?.action_receipt_id === "string" ? json.action_receipt_id : null
207
- };
325
+ return mergeVerifiedPrRequest(
326
+ req,
327
+ prNumber,
328
+ automationContext,
329
+ () => {
330
+ cachedFirebaseToken = null;
331
+ }
332
+ );
208
333
  },
209
334
  /**
210
335
  * Append progress / set terminal status. Returns
@@ -212,23 +337,37 @@ function createControlPlaneClient({
212
337
  * { terminal: true } — task already terminal (operator cancelled): STOP
213
338
  */
214
339
  async postProgress(taskId, patch) {
215
- const res = await req("PATCH", `/api/v1/code-task/${taskId}/progress`, patch);
216
- if (res.status === 409) return { terminal: true };
340
+ const progress = {
341
+ ...patch,
342
+ ...patch.runner_id ? {} : runnerId2 ? { runner_id: runnerId2 } : {},
343
+ ...patch.runner_instance_id ? {} : runnerInstanceId ? { runner_instance_id: runnerInstanceId } : {}
344
+ };
345
+ const res = await taskReq("PATCH", `/api/v1/code-task/${taskId}/progress`, progress);
346
+ if (res.status === 409) {
347
+ const conflict = await res.json().catch(() => ({}));
348
+ if (conflict?.error === "code_task_claim_authority_changed") {
349
+ throw new ClaimAuthorityChangedError();
350
+ }
351
+ return { terminal: true };
352
+ }
217
353
  if (res.status === 404) return { terminal: true, missing: true };
218
354
  if (!res.ok) throw new Error(`progress failed: HTTP ${res.status}`);
219
355
  const json = await res.json();
220
356
  return { task: json && json.task };
221
357
  },
222
358
  async getTask(taskId) {
223
- const res = await req("GET", `/api/v1/code-task/${taskId}`);
359
+ const res = await taskReq("GET", `/api/v1/code-task/${taskId}`);
224
360
  if (res.status === 404) return null;
225
361
  if (!res.ok) throw new Error(`getTask failed: HTTP ${res.status}`);
226
362
  const json = await res.json();
227
363
  return json ? json.task : null;
228
364
  },
365
+ async listPrOpenedTasks() {
366
+ return listAllPrOpenedTasks(taskReq);
367
+ },
229
368
  async downloadTaskAttachment(taskId, attachmentId) {
230
369
  const path2 = `/api/v1/code-task/${encodeURIComponent(taskId)}/attachment/${encodeURIComponent(attachmentId)}`;
231
- const res = await req("GET", path2);
370
+ const res = await taskReq("GET", path2);
232
371
  if (res.status === 401) cachedFirebaseToken = null;
233
372
  if (!res.ok) throw new Error(`attachment download failed: HTTP ${res.status}`);
234
373
  return Buffer.from(await res.arrayBuffer());
@@ -236,7 +375,7 @@ function createControlPlaneClient({
236
375
  async getTaskKnowledgeContext(taskId, { query } = {}) {
237
376
  const body = {};
238
377
  if (typeof query === "string" && query.trim()) body.query = query;
239
- const res = await req("POST", `/api/v1/code-task/${encodeURIComponent(taskId)}/knowledge-context`, body);
378
+ const res = await taskReq("POST", `/api/v1/code-task/${encodeURIComponent(taskId)}/knowledge-context`, body);
240
379
  if (res.status === 401) {
241
380
  cachedFirebaseToken = null;
242
381
  throw new Error("knowledge-context unauthorized (401)");
@@ -255,10 +394,10 @@ function createControlPlaneClient({
255
394
  * `tokens` = { input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens }.
256
395
  * Optional: `claudeWeeklyPct` (number) + `claudeWeeklyResetsAt` (ISO string | null).
257
396
  */
258
- async postWeeklyTokens({ operatorId, runnerId: runnerId2, tokens, claudeWeeklyPct, claudeWeeklyResetsAt }) {
397
+ async postWeeklyTokens({ operatorId, runnerId: runnerId3, tokens, claudeWeeklyPct, claudeWeeklyResetsAt }) {
259
398
  const body = {
260
399
  operator_id: operatorId,
261
- runner_id: runnerId2,
400
+ runner_id: runnerId3,
262
401
  input_tokens: tokens.input_tokens,
263
402
  output_tokens: tokens.output_tokens,
264
403
  cache_creation_tokens: tokens.cache_creation_tokens,
@@ -270,7 +409,7 @@ function createControlPlaneClient({
270
409
  if (claudeWeeklyResetsAt !== void 0) {
271
410
  body.claude_weekly_resets_at = claudeWeeklyResetsAt;
272
411
  }
273
- const res = await req("POST", "/api/v1/weekly-tokens", body);
412
+ const res = await taskReq("POST", "/api/v1/weekly-tokens", body);
274
413
  if (res.status === 401) {
275
414
  cachedFirebaseToken = null;
276
415
  throw new Error("weekly-tokens unauthorized (401)");
@@ -283,9 +422,9 @@ function createControlPlaneClient({
283
422
  * authenticated operator so the web shows a TRUE "runner online" signal.
284
423
  * Best-effort caller; throws on 401/non-ok so the daemon can log + retry.
285
424
  */
286
- async postHeartbeat({ runnerId: runnerId2, runnerInstanceId, operatorId, uptimeSec, activeTasks, maxConcurrency, effectiveConcurrency, measuredTaskSlots, measuredCpuSlots, measuredMemorySlots, version, daemonVersion, defaultAgent, supervisorInstanceId: supervisorInstanceId2, supervisorVersion: supervisorVersion2, supervisorCapabilities, servedRepos, servedOperators, availableAgents, accountUsage, availableLocalModels }) {
287
- const body = { runner_id: runnerId2 };
288
- if (runnerInstanceId) body.runner_instance_id = runnerInstanceId;
425
+ 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 }) {
426
+ const body = { runner_id: runnerId3 };
427
+ if (runnerInstanceId2) body.runner_instance_id = runnerInstanceId2;
289
428
  if (operatorId) body.operator_id = operatorId;
290
429
  if (typeof uptimeSec === "number") body.uptime_sec = uptimeSec;
291
430
  if (typeof activeTasks === "number") body.active_tasks = activeTasks;
@@ -322,7 +461,17 @@ function createControlPlaneClient({
322
461
  cachedFirebaseToken = null;
323
462
  throw new Error("heartbeat unauthorized (401)");
324
463
  }
325
- if (!res.ok) throw new Error(`heartbeat failed: HTTP ${res.status}`);
464
+ if (!res.ok) {
465
+ let detail = "";
466
+ try {
467
+ const body2 = await res.json();
468
+ if (Array.isArray(body2?.issue_paths) && body2.issue_paths.length > 0) {
469
+ detail = ` (rejected fields: ${body2.issue_paths.join(", ")})`;
470
+ }
471
+ } catch {
472
+ }
473
+ throw new Error(`heartbeat failed: HTTP ${res.status}${detail}`);
474
+ }
326
475
  return res.json();
327
476
  },
328
477
  async getRunnerStatus({ operatorId } = {}) {
@@ -338,13 +487,13 @@ function createControlPlaneClient({
338
487
  const body = await res.json();
339
488
  return Array.isArray(body?.runners) ? body.runners : [];
340
489
  },
341
- async pollRunnerControl({ runnerId: runnerId2, operatorId, supervisorInstanceId: supervisorInstanceId2, supervisorVersion: supervisorVersion2, capabilities }) {
342
- const body = { runner_id: runnerId2 };
490
+ async pollRunnerControl({ runnerId: runnerId3, operatorId, supervisorInstanceId: supervisorInstanceId2, supervisorVersion: supervisorVersion2, capabilities }) {
491
+ const body = { runner_id: runnerId3 };
343
492
  if (operatorId) body.operator_id = operatorId;
344
493
  if (supervisorInstanceId2) body.supervisor_instance_id = supervisorInstanceId2;
345
494
  if (supervisorVersion2) body.supervisor_version = supervisorVersion2;
346
495
  if (Array.isArray(capabilities) && capabilities.length > 0) body.capabilities = capabilities;
347
- const res = await req("POST", "/api/v1/runner/control/poll", body);
496
+ const res = await taskReq("POST", "/api/v1/runner/control/poll", body);
348
497
  if (res.status === 401) {
349
498
  cachedFirebaseToken = null;
350
499
  throw new Error("runner control poll unauthorized (401)");
@@ -354,14 +503,14 @@ function createControlPlaneClient({
354
503
  const action = json?.action;
355
504
  return action && typeof action.action_id === "string" && action.action_id ? { ...action, actionId: action.action_id } : null;
356
505
  },
357
- async completeRunnerControl(actionId, { runnerId: runnerId2, operatorId, supervisorInstanceId: supervisorInstanceId2, supervisorVersion: supervisorVersion2, capabilities, status, detail }) {
358
- const body = { runner_id: runnerId2, status };
506
+ async completeRunnerControl(actionId, { runnerId: runnerId3, operatorId, supervisorInstanceId: supervisorInstanceId2, supervisorVersion: supervisorVersion2, capabilities, status, detail }) {
507
+ const body = { runner_id: runnerId3, status };
359
508
  if (operatorId) body.operator_id = operatorId;
360
509
  if (supervisorInstanceId2) body.supervisor_instance_id = supervisorInstanceId2;
361
510
  if (supervisorVersion2) body.supervisor_version = supervisorVersion2;
362
511
  if (Array.isArray(capabilities) && capabilities.length > 0) body.capabilities = capabilities;
363
512
  if (detail) body.detail = detail;
364
- const res = await req("POST", `/api/v1/runner/control/${encodeURIComponent(actionId)}/complete`, body);
513
+ const res = await taskReq("POST", `/api/v1/runner/control/${encodeURIComponent(actionId)}/complete`, body);
365
514
  if (res.status === 401) {
366
515
  cachedFirebaseToken = null;
367
516
  throw new Error("runner control completion unauthorized (401)");
@@ -372,7 +521,7 @@ function createControlPlaneClient({
372
521
  },
373
522
  /** Mint a GitHub App installation token — see installation-token.mjs. */
374
523
  async getInstallationToken({ required = false, readOnly = false, repo = null } = {}) {
375
- return fetchInstallationToken({ req, required, readOnly, repo });
524
+ return fetchInstallationToken({ req: taskReq, required, readOnly, repo });
376
525
  },
377
526
  /**
378
527
  * Read the operator's dispatch-mode config (Fast→Ultracode effort setting).
@@ -381,7 +530,7 @@ function createControlPlaneClient({
381
530
  */
382
531
  async getDispatchMode() {
383
532
  try {
384
- const res = await req("GET", "/api/v1/dispatch-mode-config");
533
+ const res = await taskReq("GET", "/api/v1/dispatch-mode-config");
385
534
  if (!res.ok) return "standard";
386
535
  const json = await res.json();
387
536
  return json?.dispatchMode || "standard";
@@ -2217,8 +2366,9 @@ async function main() {
2217
2366
  let child = null;
2218
2367
  let stopping = false;
2219
2368
  let handling = false;
2369
+ let degraded = false;
2220
2370
  const respawn = () => {
2221
- if (!stopping && !handling && (!child || child.exitCode !== null)) child = launchChild();
2371
+ if (!stopping && !degraded && !handling && (!child || child.exitCode !== null)) child = launchChild();
2222
2372
  };
2223
2373
  const launchChild = () => {
2224
2374
  const next = spawnChild(childEnv);
@@ -2241,8 +2391,11 @@ async function main() {
2241
2391
  launchPreviousChild: (entry) => spawnPreviousChild(entry, childEnv),
2242
2392
  log: (message) => console.error(`[vo-runner supervisor] ${message}`)
2243
2393
  })) {
2244
- stopping = true;
2245
- return;
2394
+ degraded = true;
2395
+ process.exitCode = 1;
2396
+ await stopChild(child);
2397
+ child = null;
2398
+ console.error("[vo-runner supervisor] activation FAILED \u2014 entering degraded mode: not serving tasks, still polling for remote control actions");
2246
2399
  }
2247
2400
  const shutdown = async () => {
2248
2401
  if (stopping) return;
@@ -2308,6 +2461,7 @@ async function main() {
2308
2461
  child = launchChild();
2309
2462
  await sleep(CHILD_START_MS);
2310
2463
  if (child.exitCode !== null || !await waitForLocalRunner(child)) result.ok = false;
2464
+ else degraded = false;
2311
2465
  await client.completeRunnerControl(action.actionId, {
2312
2466
  runnerId,
2313
2467
  ...operatorId ? { operatorId } : {},