@algosuite/vo-mcp 0.2.0-beta.55 → 0.2.0-beta.57
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/dist/autostart-cli.js +6 -3
- package/dist/autostart-cli.js.map +2 -2
- package/dist/install-cli.js +6 -3
- package/dist/install-cli.js.map +2 -2
- package/dist/runner-cli.js +649 -127
- package/dist/runner-cli.js.map +4 -4
- package/dist/runner-supervisor.js +871 -88
- package/dist/runner-supervisor.js.map +4 -4
- package/package.json +1 -1
package/dist/runner-cli.js
CHANGED
|
@@ -675,12 +675,15 @@ function installWindowsAutostart(runnerCommand, log2, env2) {
|
|
|
675
675
|
log2(`\u26A0 Could not remove legacy launcher ${legacyCmdPath} (${error instanceof Error ? error.message : String(error)}); remove it manually to avoid a double start`);
|
|
676
676
|
}
|
|
677
677
|
}
|
|
678
|
-
const
|
|
678
|
+
const runnerConsoleCommand = `cmd /c ${runnerCommand} >> "%USERPROFILE%\\.claude\\vo-runner.log" 2>&1`.replace(/"/g, '""');
|
|
679
679
|
const launcherContent = `' Auto-start launcher for vo-mcp runner
|
|
680
680
|
' Created by vo-mcp autostart installer
|
|
681
681
|
' Keepalive supervisor: restarts the runner if it exits (parity with launchd
|
|
682
682
|
' KeepAlive on macOS and systemd Restart=on-failure on Linux).
|
|
683
|
-
'
|
|
683
|
+
' Runs the runner in a MINIMIZED (style 7), never a hidden (style 0) window:
|
|
684
|
+
' hidden script-host exec trips Defender's PowhidSubExec.B heuristic and gets
|
|
685
|
+
' blocked at logon. To stop: create %USERPROFILE%\\.claude\\vo-runner.stop, or
|
|
686
|
+
' end wscript.exe.
|
|
684
687
|
Dim sh, fso, stopFile, backoff, startedAt, ranMs
|
|
685
688
|
Set sh = CreateObject("WScript.Shell")
|
|
686
689
|
Set fso = CreateObject("Scripting.FileSystemObject")
|
|
@@ -694,7 +697,7 @@ Do
|
|
|
694
697
|
WScript.Quit 0
|
|
695
698
|
End If
|
|
696
699
|
startedAt = Timer
|
|
697
|
-
sh.Run "${
|
|
700
|
+
sh.Run "${runnerConsoleCommand}", 7, True
|
|
698
701
|
ranMs = (Timer - startedAt) * 1000
|
|
699
702
|
If ranMs < 0 Then ranMs = ${WINDOWS_HEALTHY_RUN_MS}
|
|
700
703
|
If ranMs >= ${WINDOWS_HEALTHY_RUN_MS} Then
|
|
@@ -1178,8 +1181,8 @@ async function walkManagedRoots(ownership, options, onLink) {
|
|
|
1178
1181
|
const normalized = normalizeOwnership(ownership);
|
|
1179
1182
|
let scannedEntries = 0;
|
|
1180
1183
|
for (const rootPath of normalized.managedRoots) {
|
|
1181
|
-
const
|
|
1182
|
-
if (!
|
|
1184
|
+
const present2 = await assertManagedRoot(rootPath, normalized.worktreeRoot, fsApi);
|
|
1185
|
+
if (!present2) continue;
|
|
1183
1186
|
const stack = [rootPath];
|
|
1184
1187
|
while (stack.length > 0) {
|
|
1185
1188
|
const current = stack.pop();
|
|
@@ -1999,12 +2002,12 @@ var init_pnpm_materialize = __esm({
|
|
|
1999
2002
|
});
|
|
2000
2003
|
|
|
2001
2004
|
// src/runner/pnpm-hydration.mjs
|
|
2002
|
-
import { createHash } from "node:crypto";
|
|
2005
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
2003
2006
|
import fs3 from "node:fs";
|
|
2004
2007
|
import fsp5 from "node:fs/promises";
|
|
2005
2008
|
import path5 from "node:path";
|
|
2006
2009
|
function hashText(text) {
|
|
2007
|
-
return
|
|
2010
|
+
return createHash2("sha256").update(String(text)).digest("hex");
|
|
2008
2011
|
}
|
|
2009
2012
|
function statePath(root) {
|
|
2010
2013
|
return path5.join(root, ".agent-worktrees", "runner-pnpm-hydration.json");
|
|
@@ -2314,7 +2317,7 @@ var init_pnpm_hydration = __esm({
|
|
|
2314
2317
|
});
|
|
2315
2318
|
|
|
2316
2319
|
// src/runner/worktree-paths.mjs
|
|
2317
|
-
import { createHash as
|
|
2320
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
2318
2321
|
import path6 from "node:path";
|
|
2319
2322
|
function samePath2(left, right) {
|
|
2320
2323
|
const a = path6.resolve(left);
|
|
@@ -2332,7 +2335,7 @@ function worktreePoolForRoot(root, { clonesRootDir = process.env.VO_CODE_RUNNER_
|
|
|
2332
2335
|
return path6.join(canonicalRoot, ".agent-worktrees");
|
|
2333
2336
|
}
|
|
2334
2337
|
function worktreeDirForName(root, worktreeName, options = {}) {
|
|
2335
|
-
const leaf =
|
|
2338
|
+
const leaf = createHash3("sha256").update(String(worktreeName)).digest("hex").slice(0, 16);
|
|
2336
2339
|
return path6.join(worktreePoolForRoot(root, options), leaf);
|
|
2337
2340
|
}
|
|
2338
2341
|
function recoveryLedgerPathForRoot(root, options = {}) {
|
|
@@ -3497,8 +3500,9 @@ async function mergeVerifiedPrRequest(req, prNumber, automationContext, onUnauth
|
|
|
3497
3500
|
actionReceiptId: typeof json.action_receipt_id === "string" ? json.action_receipt_id : null
|
|
3498
3501
|
};
|
|
3499
3502
|
}
|
|
3500
|
-
|
|
3501
|
-
|
|
3503
|
+
const captureStoreRetry = json?.action_status === "not_attempted" && (json?.error === "capture_preflight_unavailable" || json?.error === "decision_outcome_intent_failed");
|
|
3504
|
+
if (res.status === 503 && (json?.error === "verify_unavailable" || json?.error === "merge_unavailable" || captureStoreRetry)) {
|
|
3505
|
+
return { status: "retry", reason: json.reason || json.message || json.error || "verification unavailable" };
|
|
3502
3506
|
}
|
|
3503
3507
|
return {
|
|
3504
3508
|
status: "blocked",
|
|
@@ -3604,6 +3608,64 @@ var init_claim_gate_notice = __esm({
|
|
|
3604
3608
|
}
|
|
3605
3609
|
});
|
|
3606
3610
|
|
|
3611
|
+
// ../../scripts/virtual-office/code-runner/control-plane-knowledge-context.mjs
|
|
3612
|
+
function isRetryableStatus(status) {
|
|
3613
|
+
return status >= 500 && status <= 599;
|
|
3614
|
+
}
|
|
3615
|
+
function defaultSleep(ms) {
|
|
3616
|
+
return new Promise((resolve3) => {
|
|
3617
|
+
setTimeout(resolve3, ms);
|
|
3618
|
+
});
|
|
3619
|
+
}
|
|
3620
|
+
async function getTaskKnowledgeContextRequest(req, taskId, { query } = {}, {
|
|
3621
|
+
taskRequestTimeoutMs,
|
|
3622
|
+
invalidateToken = () => {
|
|
3623
|
+
},
|
|
3624
|
+
sleep: sleep3 = defaultSleep,
|
|
3625
|
+
log: log2 = () => {
|
|
3626
|
+
}
|
|
3627
|
+
} = {}) {
|
|
3628
|
+
const body = {};
|
|
3629
|
+
if (typeof query === "string" && query.trim()) body.query = query;
|
|
3630
|
+
const path22 = `/api/v1/code-task/${encodeURIComponent(taskId)}/knowledge-context`;
|
|
3631
|
+
const timeoutMs = Math.max(Number(taskRequestTimeoutMs) || 0, MIN_KNOWLEDGE_CONTEXT_TIMEOUT_MS);
|
|
3632
|
+
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt += 1) {
|
|
3633
|
+
let res;
|
|
3634
|
+
let cause;
|
|
3635
|
+
try {
|
|
3636
|
+
res = await req("POST", path22, body, { timeoutMs });
|
|
3637
|
+
} catch (err) {
|
|
3638
|
+
cause = err;
|
|
3639
|
+
}
|
|
3640
|
+
if (!cause) {
|
|
3641
|
+
if (res.status === 401) {
|
|
3642
|
+
invalidateToken();
|
|
3643
|
+
throw new Error("knowledge-context unauthorized (401)");
|
|
3644
|
+
}
|
|
3645
|
+
if (res.status === 404) return null;
|
|
3646
|
+
if (res.ok) return res.json();
|
|
3647
|
+
if (!isRetryableStatus(res.status)) {
|
|
3648
|
+
throw new Error(`knowledge-context failed: HTTP ${res.status}`);
|
|
3649
|
+
}
|
|
3650
|
+
cause = new Error(`knowledge-context failed: HTTP ${res.status}`);
|
|
3651
|
+
}
|
|
3652
|
+
if (attempt === MAX_ATTEMPTS) throw cause;
|
|
3653
|
+
const delayMs = RETRY_DELAYS_MS[attempt - 1];
|
|
3654
|
+
log2(`knowledge-context attempt ${attempt}/${MAX_ATTEMPTS} failed (${cause.message}); retrying in ${delayMs}ms`);
|
|
3655
|
+
await sleep3(delayMs);
|
|
3656
|
+
}
|
|
3657
|
+
throw new Error("knowledge-context retry loop exited unexpectedly");
|
|
3658
|
+
}
|
|
3659
|
+
var MIN_KNOWLEDGE_CONTEXT_TIMEOUT_MS, RETRY_DELAYS_MS, MAX_ATTEMPTS;
|
|
3660
|
+
var init_control_plane_knowledge_context = __esm({
|
|
3661
|
+
"../../scripts/virtual-office/code-runner/control-plane-knowledge-context.mjs"() {
|
|
3662
|
+
"use strict";
|
|
3663
|
+
MIN_KNOWLEDGE_CONTEXT_TIMEOUT_MS = 15e3;
|
|
3664
|
+
RETRY_DELAYS_MS = [2e3, 6e3];
|
|
3665
|
+
MAX_ATTEMPTS = RETRY_DELAYS_MS.length + 1;
|
|
3666
|
+
}
|
|
3667
|
+
});
|
|
3668
|
+
|
|
3607
3669
|
// src/runner/control-plane-auth-stub.mjs
|
|
3608
3670
|
var control_plane_auth_stub_exports = {};
|
|
3609
3671
|
__export(control_plane_auth_stub_exports, {
|
|
@@ -3647,7 +3709,8 @@ function createControlPlaneClient({
|
|
|
3647
3709
|
6e4
|
|
3648
3710
|
),
|
|
3649
3711
|
runnerId,
|
|
3650
|
-
runnerInstanceId
|
|
3712
|
+
runnerInstanceId,
|
|
3713
|
+
sleep: sleep3
|
|
3651
3714
|
} = {}) {
|
|
3652
3715
|
const resolvedBaseUrl = baseUrl ?? env2.VO_CONTROL_PLANE_URL ?? "";
|
|
3653
3716
|
if (!resolvedBaseUrl) throw new Error("VO_CONTROL_PLANE_URL is required for the code-runner daemon");
|
|
@@ -3828,17 +3891,16 @@ function createControlPlaneClient({
|
|
|
3828
3891
|
if (!res.ok) throw new Error(`attachment download failed: HTTP ${res.status}`);
|
|
3829
3892
|
return Buffer.from(await res.arrayBuffer());
|
|
3830
3893
|
},
|
|
3894
|
+
// Raised per-attempt timeout (>=15s) + bounded retry — see control-plane-knowledge-context.mjs.
|
|
3831
3895
|
async getTaskKnowledgeContext(taskId, { query } = {}) {
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
if (!res.ok) throw new Error(`knowledge-context failed: HTTP ${res.status}`);
|
|
3841
|
-
return res.json();
|
|
3896
|
+
return getTaskKnowledgeContextRequest(req, taskId, { query }, {
|
|
3897
|
+
taskRequestTimeoutMs,
|
|
3898
|
+
invalidateToken: () => {
|
|
3899
|
+
cachedFirebaseToken = null;
|
|
3900
|
+
},
|
|
3901
|
+
sleep: sleep3,
|
|
3902
|
+
log: (m) => console.warn(`[code-runner ${(/* @__PURE__ */ new Date()).toISOString()}] ${m}`)
|
|
3903
|
+
});
|
|
3842
3904
|
},
|
|
3843
3905
|
/** Weekly Claude token usage report — see control-plane-weekly-tokens.mjs. */
|
|
3844
3906
|
async postWeeklyTokens(report) {
|
|
@@ -3992,6 +4054,7 @@ var init_control_plane_client = __esm({
|
|
|
3992
4054
|
init_control_plane_weekly_tokens();
|
|
3993
4055
|
init_control_plane_telemetry_relay();
|
|
3994
4056
|
init_claim_gate_notice();
|
|
4057
|
+
init_control_plane_knowledge_context();
|
|
3995
4058
|
cachedFirebaseToken = null;
|
|
3996
4059
|
ClaimAuthorityChangedError = class extends Error {
|
|
3997
4060
|
constructor() {
|
|
@@ -8606,7 +8669,7 @@ var init_task_prompt = __esm({
|
|
|
8606
8669
|
});
|
|
8607
8670
|
|
|
8608
8671
|
// ../../scripts/virtual-office/code-runner/task-attachments.mjs
|
|
8609
|
-
import { createHash as
|
|
8672
|
+
import { createHash as createHash4, randomUUID as randomUUID3 } from "node:crypto";
|
|
8610
8673
|
import { chmod, mkdir, mkdtemp, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
8611
8674
|
import os2 from "node:os";
|
|
8612
8675
|
import path16 from "node:path";
|
|
@@ -8630,7 +8693,7 @@ async function createAttachmentDirectory(taskId, tempRoot) {
|
|
|
8630
8693
|
const root = path16.resolve(tempRoot);
|
|
8631
8694
|
await mkdir(root, { recursive: true });
|
|
8632
8695
|
const directory = await mkdtemp(path16.join(root, `${DIRECTORY_PREFIX}${safeTaskToken(taskId)}-`));
|
|
8633
|
-
const marker = JSON.stringify({ owner: MARKER_OWNER, token:
|
|
8696
|
+
const marker = JSON.stringify({ owner: MARKER_OWNER, token: randomUUID3(), directory: path16.basename(directory), created_at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
8634
8697
|
await writeFile(path16.join(directory, MARKER_FILE), marker, { encoding: "utf8", mode: 384 });
|
|
8635
8698
|
return { directory, marker, tempRoot: root };
|
|
8636
8699
|
}
|
|
@@ -8706,7 +8769,7 @@ async function materializeTaskAttachments(client, task, { tempRoot = os2.tmpdir(
|
|
|
8706
8769
|
const content = await client.downloadTaskAttachment(task.code_task_id, ref.attachment_id);
|
|
8707
8770
|
if (!Buffer.isBuffer(content)) throw new Error(`attachment ${ref.attachment_id} did not return binary content`);
|
|
8708
8771
|
if (content.byteLength !== ref.size_bytes) throw new Error(`attachment ${ref.attachment_id} size mismatch`);
|
|
8709
|
-
const sha256 =
|
|
8772
|
+
const sha256 = createHash4("sha256").update(content).digest("hex");
|
|
8710
8773
|
if (sha256 !== ref.sha256) throw new Error(`attachment ${ref.attachment_id} sha256 mismatch`);
|
|
8711
8774
|
const name = sanitizeTaskAttachmentName(ref.name, index);
|
|
8712
8775
|
const filePath = path16.join(state.directory, name);
|
|
@@ -8737,9 +8800,9 @@ var init_task_attachments = __esm({
|
|
|
8737
8800
|
import { homedir as homedir7 } from "node:os";
|
|
8738
8801
|
import { join as join11 } from "node:path";
|
|
8739
8802
|
import { readdir as readdir2, readFile as readFile2, unlink, writeFile as writeFile2 } from "node:fs/promises";
|
|
8740
|
-
import { createHash as
|
|
8803
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
8741
8804
|
function deriveUuid(seed) {
|
|
8742
|
-
const h =
|
|
8805
|
+
const h = createHash5("sha256").update(seed).digest("hex");
|
|
8743
8806
|
return `${h.slice(0, 8)}-${h.slice(8, 12)}-5${h.slice(13, 16)}-${(parseInt(h.slice(16, 18), 16) & 63 | 128).toString(16)}${h.slice(18, 20)}-${h.slice(20, 32)}`;
|
|
8744
8807
|
}
|
|
8745
8808
|
function spoolToCloud(record, ids) {
|
|
@@ -8876,7 +8939,7 @@ function selectDueEntries({ entries = [], now, alreadyDispatched = /* @__PURE__
|
|
|
8876
8939
|
if (!code_task_id) continue;
|
|
8877
8940
|
if (seen.has(code_task_id)) continue;
|
|
8878
8941
|
const stableAttempts = Number(attemptsByKey[stableTaskKey(e)] || 0);
|
|
8879
|
-
if (Math.max(typeof attempts === "number" ? attempts : 0, stableAttempts) >=
|
|
8942
|
+
if (Math.max(typeof attempts === "number" ? attempts : 0, stableAttempts) >= MAX_ATTEMPTS2) {
|
|
8880
8943
|
exhausted.push(e);
|
|
8881
8944
|
continue;
|
|
8882
8945
|
}
|
|
@@ -8912,11 +8975,11 @@ function reconcileQueue({ entries = [], dispatchedIds = /* @__PURE__ */ new Set(
|
|
|
8912
8975
|
return true;
|
|
8913
8976
|
});
|
|
8914
8977
|
}
|
|
8915
|
-
var
|
|
8978
|
+
var MAX_ATTEMPTS2, MAX_DISPATCH_PER_RUN, NULL_RESUME_AFTER_BACKOFF_MS;
|
|
8916
8979
|
var init_rate_limit_resume_scheduler_core = __esm({
|
|
8917
8980
|
"../../scripts/virtual-office/code-runner/rate-limit-resume-scheduler-core.mjs"() {
|
|
8918
8981
|
"use strict";
|
|
8919
|
-
|
|
8982
|
+
MAX_ATTEMPTS2 = 3;
|
|
8920
8983
|
MAX_DISPATCH_PER_RUN = 10;
|
|
8921
8984
|
NULL_RESUME_AFTER_BACKOFF_MS = 15 * 60 * 1e3;
|
|
8922
8985
|
}
|
|
@@ -8943,7 +9006,7 @@ function bumpAttempts(store, key, sourceTaskId, nowIso) {
|
|
|
8943
9006
|
store[key] = {
|
|
8944
9007
|
count: (typeof prior.count === "number" ? prior.count : 0) + 1,
|
|
8945
9008
|
lastSeen: nowIso,
|
|
8946
|
-
resumedTaskIds: [...resumedTaskIds, sourceTaskId].slice(-
|
|
9009
|
+
resumedTaskIds: [...resumedTaskIds, sourceTaskId].slice(-MAX_ATTEMPTS2)
|
|
8947
9010
|
};
|
|
8948
9011
|
}
|
|
8949
9012
|
function pruneAttemptsStore(store, nowIso) {
|
|
@@ -8995,7 +9058,7 @@ async function runLockedScheduler({
|
|
|
8995
9058
|
await writeResumeAttempts(attemptsPath, pruneAttemptsStore(attemptsStore, nowIso));
|
|
8996
9059
|
await writeResumeQueue(queuePath, kept);
|
|
8997
9060
|
if (exhausted.length > 0) {
|
|
8998
|
-
log2(`WARN: ${exhausted.length} task(s) reached the ${
|
|
9061
|
+
log2(`WARN: ${exhausted.length} task(s) reached the ${MAX_ATTEMPTS2}-continuation spend ceiling and remain queued for operator review: ${exhausted.map((entry) => entry.code_task_id).join(", ")}`);
|
|
8999
9062
|
}
|
|
9000
9063
|
if (due.length >= MAX_DISPATCH_PER_RUN) {
|
|
9001
9064
|
log2(`WARN: hit the per-run dispatch cap (${MAX_DISPATCH_PER_RUN}); more tasks remain queued`);
|
|
@@ -10446,39 +10509,6 @@ var init_pr_watcher_failure_confirmation = __esm({
|
|
|
10446
10509
|
}
|
|
10447
10510
|
});
|
|
10448
10511
|
|
|
10449
|
-
// ../../scripts/virtual-office/code-runner/superseded-pr-source.mjs
|
|
10450
|
-
function supersededSourcePrNumber(prompt) {
|
|
10451
|
-
const text = String(prompt || "");
|
|
10452
|
-
if (text.includes(CI_FIX_MARKER)) {
|
|
10453
|
-
const match = text.match(/\bPR:\s*#(\d+)\b/u);
|
|
10454
|
-
return match ? Number(match[1]) : null;
|
|
10455
|
-
}
|
|
10456
|
-
const repairMatch = text.match(/^REPAIR MISSION:\s*PR\s+#(\d+)\b/iu);
|
|
10457
|
-
if (repairMatch) return Number(repairMatch[1]);
|
|
10458
|
-
const recoverySupersedeMatch = text.match(
|
|
10459
|
-
/^VO_RECOVERY_FROM_CODE_TASK:\s*[0-9a-f-]{36}\n\nSupersede draft PR #(\d+) from a fresh current origin\/main branch\b/iu
|
|
10460
|
-
);
|
|
10461
|
-
if (recoverySupersedeMatch) return Number(recoverySupersedeMatch[1]);
|
|
10462
|
-
const restoredContextMatch = text.match(
|
|
10463
|
-
/^(?:The previous run reached its max-turn cap after opening a partial draft PR\.|The previous run left a draft PR\.)\nThe HQ runner will restore the existing draft PR context before you start \(draft PR https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/(\d+), PR #(\d+)\)\. Continue that work and finish it\./u
|
|
10464
|
-
);
|
|
10465
|
-
if (restoredContextMatch) {
|
|
10466
|
-
return restoredContextMatch[1] === restoredContextMatch[2] ? Number(restoredContextMatch[1]) : null;
|
|
10467
|
-
}
|
|
10468
|
-
const continuationMatch = text.match(
|
|
10469
|
-
/^(?:The previous run reached its max-turn cap after opening a partial draft PR\.|The previous run left a draft PR\.)\nContinue the work already started on the branch for PR #(\d+) \(draft PR https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/(\d+)\); check it out and finish it\./u
|
|
10470
|
-
);
|
|
10471
|
-
if (!continuationMatch || continuationMatch[1] !== continuationMatch[2]) return null;
|
|
10472
|
-
return Number(continuationMatch[1]);
|
|
10473
|
-
}
|
|
10474
|
-
var CI_FIX_MARKER;
|
|
10475
|
-
var init_superseded_pr_source = __esm({
|
|
10476
|
-
"../../scripts/virtual-office/code-runner/superseded-pr-source.mjs"() {
|
|
10477
|
-
"use strict";
|
|
10478
|
-
CI_FIX_MARKER = "[VO-CI-FIX]";
|
|
10479
|
-
}
|
|
10480
|
-
});
|
|
10481
|
-
|
|
10482
10512
|
// ../../scripts/virtual-office/code-runner/error-message.mjs
|
|
10483
10513
|
function boundedErrorMessage(error, maxLength = 240) {
|
|
10484
10514
|
try {
|
|
@@ -10494,7 +10524,7 @@ var init_error_message = __esm({
|
|
|
10494
10524
|
});
|
|
10495
10525
|
|
|
10496
10526
|
// ../../scripts/virtual-office/code-runner/watcher-coordination.mjs
|
|
10497
|
-
import { createHash as
|
|
10527
|
+
import { createHash as createHash6 } from "node:crypto";
|
|
10498
10528
|
function ciFixOccurrenceKey({ repo, prNumber, headSha, repairAttempt }) {
|
|
10499
10529
|
const occurrence = JSON.stringify([
|
|
10500
10530
|
String(repo).toLowerCase(),
|
|
@@ -10502,7 +10532,7 @@ function ciFixOccurrenceKey({ repo, prNumber, headSha, repairAttempt }) {
|
|
|
10502
10532
|
String(headSha).toLowerCase(),
|
|
10503
10533
|
Number(repairAttempt)
|
|
10504
10534
|
]);
|
|
10505
|
-
return `ci-fix:v1:${
|
|
10535
|
+
return `ci-fix:v1:${createHash6("sha256").update(occurrence).digest("hex")}`;
|
|
10506
10536
|
}
|
|
10507
10537
|
function coordinationRetryDue(entry, nowMs) {
|
|
10508
10538
|
return !entry.nextRetryAt || nowMs >= entry.nextRetryAt;
|
|
@@ -10513,9 +10543,52 @@ function mergeEnqueueActive(entry, nowMs = Date.now()) {
|
|
|
10513
10543
|
if (!at) return true;
|
|
10514
10544
|
return nowMs - at < MERGE_ENQUEUE_TTL_MS;
|
|
10515
10545
|
}
|
|
10546
|
+
function migrateLegacyMergeState(entry, nowMs = Date.now()) {
|
|
10547
|
+
if (!entry) return false;
|
|
10548
|
+
if (entry.mergeEnqueued === true && !(Number(entry.mergeEnqueuedAt) > 0)) {
|
|
10549
|
+
entry.mergeEnqueuedAt = nowMs;
|
|
10550
|
+
return true;
|
|
10551
|
+
}
|
|
10552
|
+
const exhausted = entry.mergeRetryExhausted === true || (Number(entry.mergeAttempts) || 0) >= 3 || (Number(entry.mergeErrors) || 0) >= 3;
|
|
10553
|
+
if (entry.mergeTerminal === true || mergeEnqueueActive(entry, nowMs) || !exhausted) return false;
|
|
10554
|
+
entry.mergeRetryExhausted = true;
|
|
10555
|
+
entry.mergeTerminal = true;
|
|
10556
|
+
entry.mergeBlockReason ||= "Legacy watcher state exhausted 3 automatic merge attempts without a terminal result.";
|
|
10557
|
+
return true;
|
|
10558
|
+
}
|
|
10516
10559
|
function isTerminalResumeRefusal(error) {
|
|
10517
10560
|
return typeof error?.code === "string" && TERMINAL_RESUME_REFUSALS.includes(error.code);
|
|
10518
10561
|
}
|
|
10562
|
+
async function reportPendingMergeExhaustion({
|
|
10563
|
+
entry,
|
|
10564
|
+
now,
|
|
10565
|
+
reportBlocker,
|
|
10566
|
+
taskId,
|
|
10567
|
+
prNumber,
|
|
10568
|
+
log: log2 = () => {
|
|
10569
|
+
}
|
|
10570
|
+
}) {
|
|
10571
|
+
if (!entry.mergeRetryExhausted || entry.mergeEscalatedAt || typeof reportBlocker !== "function") return false;
|
|
10572
|
+
const lastError = boundedErrorMessage(entry.mergeRetryReason || entry.mergeBlockReason);
|
|
10573
|
+
try {
|
|
10574
|
+
await reportBlocker({
|
|
10575
|
+
taskId,
|
|
10576
|
+
message: `PR #${prNumber} automatic merge verification exhausted its bounded retry budget; operator review is required. Last error: ${lastError}`
|
|
10577
|
+
});
|
|
10578
|
+
entry.mergeEscalatedAt = now();
|
|
10579
|
+
return true;
|
|
10580
|
+
} catch (reportError) {
|
|
10581
|
+
log2(`watch: pr #${prNumber} merge escalation failed; terminal state retained: ${boundedErrorMessage(reportError)}`);
|
|
10582
|
+
return false;
|
|
10583
|
+
}
|
|
10584
|
+
}
|
|
10585
|
+
async function scheduleMergeRetry(input) {
|
|
10586
|
+
await scheduleCoordinationRetry({ ...input, kind: "merge" });
|
|
10587
|
+
return {
|
|
10588
|
+
terminal: input.entry.mergeTerminal === true,
|
|
10589
|
+
escalated: await reportPendingMergeExhaustion(input)
|
|
10590
|
+
};
|
|
10591
|
+
}
|
|
10519
10592
|
async function scheduleCoordinationRetry({
|
|
10520
10593
|
entry,
|
|
10521
10594
|
kind,
|
|
@@ -10534,10 +10607,21 @@ async function scheduleCoordinationRetry({
|
|
|
10534
10607
|
log2(`watch: pr #${prNumber} automatic continuation refused by the plane (${error.code}); no retry \u2014 operator may resume explicitly with a larger cap`);
|
|
10535
10608
|
return;
|
|
10536
10609
|
}
|
|
10537
|
-
const errorsKey = kind === "resume" ? "resumeErrors" : "enqueueErrors";
|
|
10538
|
-
const attemptsKey = kind === "resume" ? "resumeAttempts" : "fixAttempts";
|
|
10610
|
+
const errorsKey = kind === "resume" ? "resumeErrors" : kind === "merge" ? "mergeErrors" : "enqueueErrors";
|
|
10611
|
+
const attemptsKey = kind === "resume" ? "resumeAttempts" : kind === "merge" ? "mergeAttempts" : "fixAttempts";
|
|
10539
10612
|
entry[errorsKey] = (entry[errorsKey] || 0) + 1;
|
|
10540
|
-
|
|
10613
|
+
if (kind === "merge") {
|
|
10614
|
+
entry.mergeRetryReason = boundedErrorMessage(error);
|
|
10615
|
+
if (entry[errorsKey] >= 3) {
|
|
10616
|
+
entry.mergeTerminal = true;
|
|
10617
|
+
entry.mergeRetryExhausted = true;
|
|
10618
|
+
entry.mergeBlockReason = `Automatic merge verification remained unavailable after 3 attempts: ${entry.mergeRetryReason}`;
|
|
10619
|
+
delete entry.nextRetryAt;
|
|
10620
|
+
log2(`watch: pr #${prNumber} merge retry cap reached \u2014 terminal and operator review required`);
|
|
10621
|
+
return;
|
|
10622
|
+
}
|
|
10623
|
+
}
|
|
10624
|
+
if (kind !== "merge") entry[attemptsKey] = Math.max(0, (entry[attemptsKey] || 1) - 1);
|
|
10541
10625
|
const delay2 = Math.min(MAX_BACKOFF_MS2, 3e4 * 2 ** Math.min(7, entry[errorsKey] - 1));
|
|
10542
10626
|
entry.nextRetryAt = now() + delay2;
|
|
10543
10627
|
if (entry[errorsKey] >= 3 && !entry.coordinationEscalatedAt && typeof reportBlocker === "function") {
|
|
@@ -10578,7 +10662,7 @@ var init_watcher_coordination = __esm({
|
|
|
10578
10662
|
});
|
|
10579
10663
|
|
|
10580
10664
|
// ../../scripts/virtual-office/code-runner/watcher-state.mjs
|
|
10581
|
-
import { randomUUID as
|
|
10665
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
10582
10666
|
import { mkdir as mkdir3, open as open2, readFile as readFile4, rename, unlink as unlink2 } from "node:fs/promises";
|
|
10583
10667
|
import { dirname as dirname10 } from "node:path";
|
|
10584
10668
|
async function readWatcherState(stateFile) {
|
|
@@ -10598,7 +10682,7 @@ async function readWatcherState(stateFile) {
|
|
|
10598
10682
|
async function writeWatcherState(stateFile, state) {
|
|
10599
10683
|
const directory = dirname10(stateFile);
|
|
10600
10684
|
await mkdir3(directory, { recursive: true });
|
|
10601
|
-
const temp = `${stateFile}.${process.pid}.${
|
|
10685
|
+
const temp = `${stateFile}.${process.pid}.${randomUUID4()}.tmp`;
|
|
10602
10686
|
let handle;
|
|
10603
10687
|
try {
|
|
10604
10688
|
handle = await open2(temp, "wx");
|
|
@@ -10646,16 +10730,87 @@ var init_watcher_state = __esm({
|
|
|
10646
10730
|
function normalizedRepo(repo) {
|
|
10647
10731
|
return String(repo || "").trim().toLowerCase();
|
|
10648
10732
|
}
|
|
10733
|
+
function present(value) {
|
|
10734
|
+
return value !== null && value !== void 0 && value !== "";
|
|
10735
|
+
}
|
|
10649
10736
|
function watcherKey(state, repo, prNumber) {
|
|
10737
|
+
const targetRepo = normalizedRepo(repo);
|
|
10738
|
+
for (const [key, entry] of Object.entries(state)) {
|
|
10739
|
+
if (watcherPrNumber(key, entry) !== Number(prNumber)) continue;
|
|
10740
|
+
if (normalizedRepo(entry?.repo) === targetRepo) return key;
|
|
10741
|
+
}
|
|
10650
10742
|
const legacy = String(prNumber);
|
|
10651
10743
|
const existing = state[legacy];
|
|
10652
|
-
if (!existing
|
|
10653
|
-
return `${
|
|
10744
|
+
if (!existing) return legacy;
|
|
10745
|
+
return `${targetRepo}#${prNumber}`;
|
|
10654
10746
|
}
|
|
10655
10747
|
function watcherPrNumber(stateKey, entry) {
|
|
10656
10748
|
const value = Number(entry?.prNumber ?? String(stateKey).split("#").at(-1));
|
|
10657
10749
|
return Number.isInteger(value) && value > 0 ? value : null;
|
|
10658
10750
|
}
|
|
10751
|
+
function collapseDuplicateWatcherEntries(state) {
|
|
10752
|
+
const keeperBySubject = /* @__PURE__ */ new Map();
|
|
10753
|
+
let collapsed = 0;
|
|
10754
|
+
for (const [key, entry] of Object.entries(state)) {
|
|
10755
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
|
|
10756
|
+
const repo = normalizedRepo(entry.repo);
|
|
10757
|
+
const prNumber = watcherPrNumber(key, entry);
|
|
10758
|
+
if (!repo || !prNumber) continue;
|
|
10759
|
+
const subject = `${repo}#${prNumber}`;
|
|
10760
|
+
const keeperKey = keeperBySubject.get(subject);
|
|
10761
|
+
if (!keeperKey) {
|
|
10762
|
+
keeperBySubject.set(subject, key);
|
|
10763
|
+
continue;
|
|
10764
|
+
}
|
|
10765
|
+
const keeper = state[keeperKey];
|
|
10766
|
+
let identityConflict = false;
|
|
10767
|
+
for (const field of ["taskId", "operatorId", "tenantId"]) {
|
|
10768
|
+
const current = keeper[field];
|
|
10769
|
+
const duplicate = entry[field];
|
|
10770
|
+
if (!present(current) && present(duplicate)) keeper[field] = duplicate;
|
|
10771
|
+
else if (present(current) && present(duplicate) && current !== duplicate) identityConflict = true;
|
|
10772
|
+
}
|
|
10773
|
+
if (keeper.operatorId === "admin" && present(keeper.tenantId)) identityConflict = true;
|
|
10774
|
+
for (const field of ["fixAttempts", "mergeAttempts", "mergeErrors", "resumeAttempts"]) {
|
|
10775
|
+
const current = Number(keeper[field]);
|
|
10776
|
+
const duplicate = Number(entry[field]);
|
|
10777
|
+
const total = (Number.isSafeInteger(current) && current > 0 ? current : 0) + (Number.isSafeInteger(duplicate) && duplicate > 0 ? duplicate : 0);
|
|
10778
|
+
keeper[field] = Math.min(Number.MAX_SAFE_INTEGER, total);
|
|
10779
|
+
}
|
|
10780
|
+
for (const [field, value] of Object.entries(entry)) {
|
|
10781
|
+
if (["taskId", "operatorId", "tenantId"].includes(field)) continue;
|
|
10782
|
+
if (!present(keeper[field]) && present(value)) keeper[field] = value;
|
|
10783
|
+
}
|
|
10784
|
+
if (entry.allowFixDispatch === false) keeper.allowFixDispatch = false;
|
|
10785
|
+
const continuationExhausted = keeper.continuationExhausted === true || entry.continuationExhausted === true;
|
|
10786
|
+
keeper.continuationExhausted = continuationExhausted;
|
|
10787
|
+
keeper.needsContinuation = !continuationExhausted && (keeper.needsContinuation === true || entry.needsContinuation === true);
|
|
10788
|
+
if (entry.mergeEnqueued === true) {
|
|
10789
|
+
keeper.mergeEnqueued = true;
|
|
10790
|
+
if ((Number(entry.mergeEnqueuedAt) || 0) > (Number(keeper.mergeEnqueuedAt) || 0)) {
|
|
10791
|
+
keeper.mergeEnqueuedAt = entry.mergeEnqueuedAt;
|
|
10792
|
+
keeper.mergeActionReceiptId = entry.mergeActionReceiptId ?? null;
|
|
10793
|
+
}
|
|
10794
|
+
}
|
|
10795
|
+
if (entry.mergeTerminal === true && keeper.mergeTerminal !== true) {
|
|
10796
|
+
keeper.mergeTerminal = true;
|
|
10797
|
+
keeper.mergeBlockReason = entry.mergeBlockReason || "verification refused";
|
|
10798
|
+
}
|
|
10799
|
+
if ((Number(keeper.mergeErrors) || 0) >= 3) {
|
|
10800
|
+
keeper.mergeRetryExhausted = true;
|
|
10801
|
+
keeper.mergeTerminal = true;
|
|
10802
|
+
keeper.mergeBlockReason ||= `Duplicate watcher history exhausted ${keeper.mergeErrors} automatic merge retries.`;
|
|
10803
|
+
}
|
|
10804
|
+
if (identityConflict) {
|
|
10805
|
+
keeper.automationContextConflict = true;
|
|
10806
|
+
keeper.mergeTerminal = true;
|
|
10807
|
+
keeper.mergeBlockReason = "Duplicate watcher entries disagree on automation attribution.";
|
|
10808
|
+
}
|
|
10809
|
+
delete state[key];
|
|
10810
|
+
collapsed += 1;
|
|
10811
|
+
}
|
|
10812
|
+
return collapsed;
|
|
10813
|
+
}
|
|
10659
10814
|
function deleteWatcherEntry(state, repo, prNumber) {
|
|
10660
10815
|
const targetRepo = normalizedRepo(repo);
|
|
10661
10816
|
for (const [key, entry] of Object.entries(state)) {
|
|
@@ -10670,6 +10825,46 @@ var init_watcher_key = __esm({
|
|
|
10670
10825
|
}
|
|
10671
10826
|
});
|
|
10672
10827
|
|
|
10828
|
+
// ../../scripts/virtual-office/code-runner/watcher-merge-authority.mjs
|
|
10829
|
+
function nonEmptyString(value) {
|
|
10830
|
+
return typeof value === "string" && value.length > 0;
|
|
10831
|
+
}
|
|
10832
|
+
function resolveWatcherMergeAuthority(entry = {}) {
|
|
10833
|
+
if (entry.automationContextConflict === true) {
|
|
10834
|
+
return {
|
|
10835
|
+
kind: "blocked",
|
|
10836
|
+
reason: "Watcher automation attribution conflicts with the durable code task."
|
|
10837
|
+
};
|
|
10838
|
+
}
|
|
10839
|
+
if (String(entry.repo || "").trim().toLowerCase() !== CANONICAL_WATCHER_MERGE_REPO) {
|
|
10840
|
+
return {
|
|
10841
|
+
kind: "blocked",
|
|
10842
|
+
reason: "Watcher merge target is not the canonical Nexus repository."
|
|
10843
|
+
};
|
|
10844
|
+
}
|
|
10845
|
+
const taskId = entry.taskId;
|
|
10846
|
+
const operatorId = entry.operatorId;
|
|
10847
|
+
const tenantId = entry.tenantId;
|
|
10848
|
+
if (nonEmptyString(taskId) && operatorId === "admin" && (tenantId === null || tenantId === void 0)) {
|
|
10849
|
+
return { kind: "legacy-admin", context: void 0 };
|
|
10850
|
+
}
|
|
10851
|
+
if (nonEmptyString(taskId) && nonEmptyString(operatorId) && nonEmptyString(tenantId)) {
|
|
10852
|
+
return { kind: "scoped", context: { taskId, operatorId, tenantId } };
|
|
10853
|
+
}
|
|
10854
|
+
return {
|
|
10855
|
+
kind: "blocked",
|
|
10856
|
+
reason: INCOMPLETE_WATCHER_AUTHORITY_REASON
|
|
10857
|
+
};
|
|
10858
|
+
}
|
|
10859
|
+
var INCOMPLETE_WATCHER_AUTHORITY_REASON, CANONICAL_WATCHER_MERGE_REPO;
|
|
10860
|
+
var init_watcher_merge_authority = __esm({
|
|
10861
|
+
"../../scripts/virtual-office/code-runner/watcher-merge-authority.mjs"() {
|
|
10862
|
+
"use strict";
|
|
10863
|
+
INCOMPLETE_WATCHER_AUTHORITY_REASON = "Watcher automation attribution is incomplete; merge was not attempted.";
|
|
10864
|
+
CANONICAL_WATCHER_MERGE_REPO = "algosuite-ai/nexus";
|
|
10865
|
+
}
|
|
10866
|
+
});
|
|
10867
|
+
|
|
10673
10868
|
// ../../scripts/virtual-office/code-runner/watcher-adoption.mjs
|
|
10674
10869
|
async function adoptPrOpenedTasks(tasks, {
|
|
10675
10870
|
stateFile,
|
|
@@ -10690,7 +10885,38 @@ async function adoptPrOpenedTasks(tasks, {
|
|
|
10690
10885
|
if (repos.size > 0 && !repos.has(task.repo.toLowerCase())) continue;
|
|
10691
10886
|
if (operators.size > 0 && !operators.has(task.operator_id)) continue;
|
|
10692
10887
|
const key = watcherKey(state, task.repo, task.pr_number);
|
|
10693
|
-
|
|
10888
|
+
const existing = state[key];
|
|
10889
|
+
if (existing) {
|
|
10890
|
+
let changed = false;
|
|
10891
|
+
let conflict = false;
|
|
10892
|
+
for (const [field, durable] of [
|
|
10893
|
+
["taskId", task.code_task_id],
|
|
10894
|
+
["operatorId", task.operator_id],
|
|
10895
|
+
["tenantId", task.tenant_id]
|
|
10896
|
+
]) {
|
|
10897
|
+
const comparable = durable === null || typeof durable === "string" && durable.length > 0;
|
|
10898
|
+
if (!comparable) continue;
|
|
10899
|
+
const current = existing[field];
|
|
10900
|
+
if ((current === null || current === void 0 || current === "") && durable !== null) {
|
|
10901
|
+
existing[field] = durable;
|
|
10902
|
+
changed = true;
|
|
10903
|
+
} else if (current !== null && current !== void 0 && current !== "" && current !== durable) {
|
|
10904
|
+
conflict = true;
|
|
10905
|
+
}
|
|
10906
|
+
}
|
|
10907
|
+
if (conflict && existing.automationContextConflict !== true) {
|
|
10908
|
+
existing.automationContextConflict = true;
|
|
10909
|
+
changed = true;
|
|
10910
|
+
}
|
|
10911
|
+
if (!conflict && existing.mergeTerminal === true && existing.mergeBlockReason === INCOMPLETE_WATCHER_AUTHORITY_REASON && resolveWatcherMergeAuthority(existing).kind !== "blocked") {
|
|
10912
|
+
delete existing.mergeTerminal;
|
|
10913
|
+
delete existing.mergeBlockReason;
|
|
10914
|
+
existing.mergeAttempts = 0;
|
|
10915
|
+
changed = true;
|
|
10916
|
+
}
|
|
10917
|
+
if (changed) adopted += 1;
|
|
10918
|
+
continue;
|
|
10919
|
+
}
|
|
10694
10920
|
const partial = String(task.result || "").includes(PARTIAL_PR_CONTINUATION_MARKER);
|
|
10695
10921
|
const repairChain = task.repair_chain ?? {
|
|
10696
10922
|
root_pr_number: task.repair_pr_number ?? task.pr_number,
|
|
@@ -10726,6 +10952,7 @@ var init_watcher_adoption = __esm({
|
|
|
10726
10952
|
init_watcher_state();
|
|
10727
10953
|
init_partial_pr_continuation();
|
|
10728
10954
|
init_watcher_key();
|
|
10955
|
+
init_watcher_merge_authority();
|
|
10729
10956
|
}
|
|
10730
10957
|
});
|
|
10731
10958
|
|
|
@@ -10757,6 +10984,39 @@ var init_watcher_github_token = __esm({
|
|
|
10757
10984
|
}
|
|
10758
10985
|
});
|
|
10759
10986
|
|
|
10987
|
+
// ../../scripts/virtual-office/code-runner/superseded-pr-source.mjs
|
|
10988
|
+
function supersededSourcePrNumber(prompt) {
|
|
10989
|
+
const text = String(prompt || "");
|
|
10990
|
+
if (text.includes(CI_FIX_MARKER)) {
|
|
10991
|
+
const match = text.match(/\bPR:\s*#(\d+)\b/u);
|
|
10992
|
+
return match ? Number(match[1]) : null;
|
|
10993
|
+
}
|
|
10994
|
+
const repairMatch = text.match(/^REPAIR MISSION:\s*PR\s+#(\d+)\b/iu);
|
|
10995
|
+
if (repairMatch) return Number(repairMatch[1]);
|
|
10996
|
+
const recoverySupersedeMatch = text.match(
|
|
10997
|
+
/^VO_RECOVERY_FROM_CODE_TASK:\s*[0-9a-f-]{36}\n\nSupersede draft PR #(\d+) from a fresh current origin\/main branch\b/iu
|
|
10998
|
+
);
|
|
10999
|
+
if (recoverySupersedeMatch) return Number(recoverySupersedeMatch[1]);
|
|
11000
|
+
const restoredContextMatch = text.match(
|
|
11001
|
+
/^(?:The previous run reached its max-turn cap after opening a partial draft PR\.|The previous run left a draft PR\.)\nThe HQ runner will restore the existing draft PR context before you start \(draft PR https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/(\d+), PR #(\d+)\)\. Continue that work and finish it\./u
|
|
11002
|
+
);
|
|
11003
|
+
if (restoredContextMatch) {
|
|
11004
|
+
return restoredContextMatch[1] === restoredContextMatch[2] ? Number(restoredContextMatch[1]) : null;
|
|
11005
|
+
}
|
|
11006
|
+
const continuationMatch = text.match(
|
|
11007
|
+
/^(?:The previous run reached its max-turn cap after opening a partial draft PR\.|The previous run left a draft PR\.)\nContinue the work already started on the branch for PR #(\d+) \(draft PR https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/(\d+)\); check it out and finish it\./u
|
|
11008
|
+
);
|
|
11009
|
+
if (!continuationMatch || continuationMatch[1] !== continuationMatch[2]) return null;
|
|
11010
|
+
return Number(continuationMatch[1]);
|
|
11011
|
+
}
|
|
11012
|
+
var CI_FIX_MARKER;
|
|
11013
|
+
var init_superseded_pr_source = __esm({
|
|
11014
|
+
"../../scripts/virtual-office/code-runner/superseded-pr-source.mjs"() {
|
|
11015
|
+
"use strict";
|
|
11016
|
+
CI_FIX_MARKER = "[VO-CI-FIX]";
|
|
11017
|
+
}
|
|
11018
|
+
});
|
|
11019
|
+
|
|
10760
11020
|
// ../../scripts/virtual-office/code-runner/ci-fix-prompt.mjs
|
|
10761
11021
|
function buildCiFixPrompt({ prNumber, repo, branch, headSha, failedChecks, prPatch, failedLogs }) {
|
|
10762
11022
|
return [
|
|
@@ -10900,7 +11160,7 @@ var init_pr_watcher_github = __esm({
|
|
|
10900
11160
|
});
|
|
10901
11161
|
|
|
10902
11162
|
// ../../scripts/virtual-office/code-runner/enqueue-autonomous-code-task.mjs
|
|
10903
|
-
import { randomUUID as
|
|
11163
|
+
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
10904
11164
|
function isDefiniteRefusal(err) {
|
|
10905
11165
|
const status = Number(err?.status);
|
|
10906
11166
|
if (Number.isFinite(status) && status >= 400 && status < 500) return true;
|
|
@@ -10919,7 +11179,7 @@ async function enqueueAutonomousCodeTask(client, task, log2 = () => {
|
|
|
10919
11179
|
if (typeof client?.reserveAutonomousDispatchBudget !== "function" || typeof client?.releaseAutonomousDispatchBudget !== "function") {
|
|
10920
11180
|
throw new Error("autonomous dispatch admission client unavailable");
|
|
10921
11181
|
}
|
|
10922
|
-
const reservationId =
|
|
11182
|
+
const reservationId = randomUUID5();
|
|
10923
11183
|
const admission = await client.reserveAutonomousDispatchBudget({
|
|
10924
11184
|
requestedBudgetUsd,
|
|
10925
11185
|
reservationId,
|
|
@@ -11038,6 +11298,7 @@ async function runWatchCycleUnlocked({
|
|
|
11038
11298
|
stateFile = DEFAULT_STATE_FILE
|
|
11039
11299
|
}) {
|
|
11040
11300
|
const state = await readWatcherState(stateFile);
|
|
11301
|
+
collapseDuplicateWatcherEntries(state);
|
|
11041
11302
|
const prNumbers = Object.keys(state);
|
|
11042
11303
|
let checked = 0;
|
|
11043
11304
|
let fixed = 0;
|
|
@@ -11063,6 +11324,7 @@ async function runWatchCycleUnlocked({
|
|
|
11063
11324
|
}
|
|
11064
11325
|
checked += 1;
|
|
11065
11326
|
const pr = parsePrCiStatus(view);
|
|
11327
|
+
migrateLegacyMergeState(entry, now());
|
|
11066
11328
|
entry.lastCi = pr.ci;
|
|
11067
11329
|
const confirmation = observeRepairFailure(entry, pr, now());
|
|
11068
11330
|
const proposedAction = decideWatchAction(
|
|
@@ -11133,10 +11395,30 @@ async function runWatchCycleUnlocked({
|
|
|
11133
11395
|
mergeBlocked += 1;
|
|
11134
11396
|
log2(`watch: pr #${prNumber} verification blocked merge: ${entry.mergeBlockReason}`);
|
|
11135
11397
|
} else {
|
|
11136
|
-
|
|
11398
|
+
const retry = await scheduleMergeRetry({
|
|
11399
|
+
entry,
|
|
11400
|
+
now,
|
|
11401
|
+
reportBlocker,
|
|
11402
|
+
taskId: entry.taskId,
|
|
11403
|
+
prNumber,
|
|
11404
|
+
error: new Error(outcome.reason || "verification unavailable"),
|
|
11405
|
+
log: log2
|
|
11406
|
+
});
|
|
11407
|
+
if (retry.terminal) mergeBlocked += 1;
|
|
11408
|
+
if (retry.escalated) escalated += 1;
|
|
11137
11409
|
}
|
|
11138
11410
|
} catch (err) {
|
|
11139
|
-
|
|
11411
|
+
const retry = await scheduleMergeRetry({
|
|
11412
|
+
entry,
|
|
11413
|
+
now,
|
|
11414
|
+
reportBlocker,
|
|
11415
|
+
taskId: entry.taskId,
|
|
11416
|
+
prNumber,
|
|
11417
|
+
error: err,
|
|
11418
|
+
log: log2
|
|
11419
|
+
});
|
|
11420
|
+
if (retry.terminal) mergeBlocked += 1;
|
|
11421
|
+
if (retry.escalated) escalated += 1;
|
|
11140
11422
|
}
|
|
11141
11423
|
} else if (action === "fix") {
|
|
11142
11424
|
entry.fixAttempts = (entry.fixAttempts || 0) + 1;
|
|
@@ -11169,6 +11451,7 @@ async function runWatchCycleUnlocked({
|
|
|
11169
11451
|
}
|
|
11170
11452
|
} else {
|
|
11171
11453
|
entry.lastCheckedAt = now();
|
|
11454
|
+
if (await reportPendingMergeExhaustion({ entry, now, reportBlocker, taskId: entry.taskId, prNumber, log: log2 })) escalated += 1;
|
|
11172
11455
|
const repairCapped = pr.ci === "failing" && ((entry.fixAttempts || 0) >= maxFixAttempts || entry.allowFixDispatch === false);
|
|
11173
11456
|
if ((repairCapped || entry.continuationExhausted) && !entry.escalatedAt) {
|
|
11174
11457
|
try {
|
|
@@ -11226,7 +11509,7 @@ function makeWatchRunner({
|
|
|
11226
11509
|
repairChainMax,
|
|
11227
11510
|
repairBudgetUsd
|
|
11228
11511
|
});
|
|
11229
|
-
if (adopted > 0) log2(`watch: adopted ${adopted} open task PR(s) from the control plane`);
|
|
11512
|
+
if (adopted > 0) log2(`watch: adopted or reconciled ${adopted} open task PR(s) from the control plane`);
|
|
11230
11513
|
return runWatchCycle({
|
|
11231
11514
|
viewPr: watchView,
|
|
11232
11515
|
enqueueFix: async ({ prNumber, repo, branch, headSha, failedChecks, failedCheckLinks, repairChain, operatorId }) => {
|
|
@@ -11268,13 +11551,10 @@ function makeWatchRunner({
|
|
|
11268
11551
|
return result;
|
|
11269
11552
|
},
|
|
11270
11553
|
mergePr: (prNumber, entry) => {
|
|
11271
|
-
const
|
|
11272
|
-
|
|
11273
|
-
|
|
11274
|
-
|
|
11275
|
-
};
|
|
11276
|
-
const complete = Object.values(context).every((value) => typeof value === "string" && value.length > 0);
|
|
11277
|
-
return client.mergeVerifiedPr(prNumber, complete ? context : void 0);
|
|
11554
|
+
const authority = resolveWatcherMergeAuthority(entry);
|
|
11555
|
+
if (authority.kind === "blocked")
|
|
11556
|
+
return Promise.resolve({ status: "blocked", reason: authority.reason });
|
|
11557
|
+
return client.mergeVerifiedPr(prNumber, authority.context);
|
|
11278
11558
|
},
|
|
11279
11559
|
log: log2,
|
|
11280
11560
|
maxFixAttempts,
|
|
@@ -11291,7 +11571,6 @@ var init_pr_watcher = __esm({
|
|
|
11291
11571
|
init_check_run_latest();
|
|
11292
11572
|
init_ci_repair_evidence();
|
|
11293
11573
|
init_pr_watcher_failure_confirmation();
|
|
11294
|
-
init_superseded_pr_source();
|
|
11295
11574
|
init_watcher_coordination();
|
|
11296
11575
|
init_watcher_adoption();
|
|
11297
11576
|
init_watcher_key();
|
|
@@ -11300,6 +11579,7 @@ var init_pr_watcher = __esm({
|
|
|
11300
11579
|
init_pr_watcher_github();
|
|
11301
11580
|
init_enqueue_autonomous_code_task();
|
|
11302
11581
|
init_watcher_state();
|
|
11582
|
+
init_watcher_merge_authority();
|
|
11303
11583
|
init_superseded_pr_source();
|
|
11304
11584
|
init_ci_fix_prompt();
|
|
11305
11585
|
DEFAULT_STATE_FILE = join14(homedir9(), ".vo", "dispatched-prs.json");
|
|
@@ -11733,7 +12013,7 @@ var init_effort_mode_config = __esm({
|
|
|
11733
12013
|
});
|
|
11734
12014
|
|
|
11735
12015
|
// ../../scripts/virtual-office/model-registry.mjs
|
|
11736
|
-
import { randomUUID as
|
|
12016
|
+
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
11737
12017
|
import fs11 from "node:fs";
|
|
11738
12018
|
import os4 from "node:os";
|
|
11739
12019
|
import path18 from "node:path";
|
|
@@ -11744,7 +12024,7 @@ function userCacheRoot() {
|
|
|
11744
12024
|
if (home) return path18.join(home, ".claude");
|
|
11745
12025
|
} catch {
|
|
11746
12026
|
}
|
|
11747
|
-
return path18.join(os4.tmpdir(), `vo-model-registry-${
|
|
12027
|
+
return path18.join(os4.tmpdir(), `vo-model-registry-${randomUUID6()}`);
|
|
11748
12028
|
}
|
|
11749
12029
|
function resolveCacheBaseDir(env2 = process.env, moduleDir = __dirname) {
|
|
11750
12030
|
if (env2.VO_MODEL_REGISTRY_CACHE_DIR) return env2.VO_MODEL_REGISTRY_CACHE_DIR;
|
|
@@ -13110,9 +13390,16 @@ function makeSafeProgress(log2) {
|
|
|
13110
13390
|
function runnerStagePatch(stage, message, extra = {}) {
|
|
13111
13391
|
return { stage, message, ...extra };
|
|
13112
13392
|
}
|
|
13393
|
+
function preservedReceiptLines(run, slicedBodyText) {
|
|
13394
|
+
const full = `${String(run?.summary || "")}
|
|
13395
|
+
${String(run?.lastAgentMessage || "")}`;
|
|
13396
|
+
const found = [...new Set((full.match(RECEIPT_LINE_RE) || []).map((line) => line.trim()))];
|
|
13397
|
+
const missing = found.filter((line) => !String(slicedBodyText || "").includes(line));
|
|
13398
|
+
return missing.length ? ["", "### Consensus evidence (preserved past truncation)", "", ...missing.map((line) => redactSecrets(line))] : [];
|
|
13399
|
+
}
|
|
13113
13400
|
function buildPrBody(task, run, files, { armAutoMerge = false } = {}) {
|
|
13114
13401
|
const governedStakes = matchGovernedStakes({ prompt: String(task.prompt || "") });
|
|
13115
|
-
|
|
13402
|
+
const slicedSections = [
|
|
13116
13403
|
"## AlgoHQ Command Center \u2014 Code-from-Anywhere task",
|
|
13117
13404
|
"",
|
|
13118
13405
|
`- **Task:** \`${task.code_task_id}\``,
|
|
@@ -13135,7 +13422,12 @@ function buildPrBody(task, run, files, { armAutoMerge = false } = {}) {
|
|
|
13135
13422
|
"",
|
|
13136
13423
|
// A budget/turn-capped run ends with no assistant text (summary = the bare
|
|
13137
13424
|
// subtype); its LAST message is the honest report the operator needs.
|
|
13138
|
-
...run.lastAgentMessage ? ["### Last agent message before the cap", "", redactSecrets(String(run.lastAgentMessage)).slice(0, 2e3), ""] : []
|
|
13425
|
+
...run.lastAgentMessage ? ["### Last agent message before the cap", "", redactSecrets(String(run.lastAgentMessage)).slice(0, 2e3), ""] : []
|
|
13426
|
+
];
|
|
13427
|
+
return [
|
|
13428
|
+
...slicedSections,
|
|
13429
|
+
// Receipt lines the slices dropped — the gate reads only this body.
|
|
13430
|
+
...preservedReceiptLines(run, slicedSections.join("\n")),
|
|
13139
13431
|
"---",
|
|
13140
13432
|
armAutoMerge ? "_Opened by the AlgoHQ code-runner daemon. After CI passes, the watcher must obtain a durable consensus receipt and merge the exact verified SHA._" : "_Opened by the AlgoHQ code-runner daemon. This PR awaits the verify-before-act gate / operator review \u2014 it is NOT auto-merged._"
|
|
13141
13433
|
].filter((l) => l !== "").join("\n");
|
|
@@ -13153,11 +13445,13 @@ async function mintRunnerGithubTokens({ client, taskId, log: log2, repo = null,
|
|
|
13153
13445
|
if (!agentReadToken) log2(`task ${taskId}: no GitHub read access for the agent (${reason}); it cannot read a private repo`);
|
|
13154
13446
|
return { publishToken, agentReadToken };
|
|
13155
13447
|
}
|
|
13448
|
+
var RECEIPT_LINE_RE;
|
|
13156
13449
|
var init_task_helpers = __esm({
|
|
13157
13450
|
"../../scripts/virtual-office/code-runner/task-helpers.mjs"() {
|
|
13158
13451
|
"use strict";
|
|
13159
13452
|
init_redact_tokens();
|
|
13160
13453
|
init_methodology_composer();
|
|
13454
|
+
RECEIPT_LINE_RE = /receipt id:\s*[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/giu;
|
|
13161
13455
|
}
|
|
13162
13456
|
});
|
|
13163
13457
|
|
|
@@ -15022,7 +15316,7 @@ var init_detached_economics_spool = __esm({
|
|
|
15022
15316
|
});
|
|
15023
15317
|
|
|
15024
15318
|
// ../../scripts/virtual-office/code-runner/killed-run-outcome.mjs
|
|
15025
|
-
import { randomUUID as
|
|
15319
|
+
import { randomUUID as randomUUID7 } from "node:crypto";
|
|
15026
15320
|
async function handleKilledRun({
|
|
15027
15321
|
client,
|
|
15028
15322
|
id,
|
|
@@ -15059,7 +15353,7 @@ async function handleKilledRun({
|
|
|
15059
15353
|
};
|
|
15060
15354
|
}
|
|
15061
15355
|
if (reason === "claim_authority_changed") {
|
|
15062
|
-
const occurrenceId =
|
|
15356
|
+
const occurrenceId = randomUUID7();
|
|
15063
15357
|
const economics = {
|
|
15064
15358
|
occurrence_id: occurrenceId,
|
|
15065
15359
|
runner_id: runnerId,
|
|
@@ -15359,7 +15653,7 @@ var code_runner_daemon_exports = {};
|
|
|
15359
15653
|
__export(code_runner_daemon_exports, {
|
|
15360
15654
|
main: () => main
|
|
15361
15655
|
});
|
|
15362
|
-
import { randomUUID as
|
|
15656
|
+
import { randomUUID as randomUUID8 } from "node:crypto";
|
|
15363
15657
|
import { fileURLToPath as fileURLToPath8 } from "node:url";
|
|
15364
15658
|
function log(msg) {
|
|
15365
15659
|
console.log(`[code-runner ${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}`);
|
|
@@ -15592,7 +15886,7 @@ Closes #${publicationTarget.supersedesPrNumber}` : "";
|
|
|
15592
15886
|
async function main({ env: env2 = process.env, once: once2 = false } = {}) {
|
|
15593
15887
|
const cfg = loadCodeRunnerConfig(env2, { log });
|
|
15594
15888
|
await sweepStaleTaskAttachmentDirectories().catch((error) => log(`stale attachment cleanup failed: ${error.message}`));
|
|
15595
|
-
const runnerInstanceId =
|
|
15889
|
+
const runnerInstanceId = randomUUID8();
|
|
15596
15890
|
const client = createControlPlaneClient({
|
|
15597
15891
|
env: env2,
|
|
15598
15892
|
runnerId: cfg.runnerId,
|
|
@@ -15804,8 +16098,163 @@ var init_code_runner_daemon = __esm({
|
|
|
15804
16098
|
import { createRequire as createRequire4 } from "node:module";
|
|
15805
16099
|
|
|
15806
16100
|
// src/runner-readiness.mjs
|
|
15807
|
-
|
|
15808
|
-
|
|
16101
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
16102
|
+
var RUNNER_GITHUB_READINESS_TIMEOUT_MS = 5e4;
|
|
16103
|
+
var RUNNER_IDENTITY_READINESS_TIMEOUT_MS = 1e4;
|
|
16104
|
+
var RUNNER_READINESS_MAX_REPOSITORIES = 4;
|
|
16105
|
+
var RUNNER_READINESS_MAX_TIMER_DELAY_MS = 2147e6;
|
|
16106
|
+
var RUNNER_READINESS_TRANSIENT_MAX_RETRY_AFTER_MS = 3e5;
|
|
16107
|
+
var RUNNER_READINESS_IPC_ACK_TIMEOUT_MS = 5e3;
|
|
16108
|
+
var RUNNER_READINESS_DEFERRAL_MESSAGE_TYPE = "vo-runner-readiness-deferred-v1";
|
|
16109
|
+
var RUNNER_READINESS_DEFERRAL_ACK_TYPE = "vo-runner-readiness-deferred-ack-v1";
|
|
16110
|
+
var RUNNER_REPOSITORY = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u;
|
|
16111
|
+
function runnerRepositoryScopeFromEnv(env2 = {}) {
|
|
16112
|
+
return String(env2.VO_CODE_RUNNER_REPOS || "").split(/[\s,]+/u).map((repo) => repo.trim()).filter(Boolean);
|
|
16113
|
+
}
|
|
16114
|
+
function normalizeRunnerRepositoryScope(repositories) {
|
|
16115
|
+
if (!Array.isArray(repositories) || repositories.length < 1) {
|
|
16116
|
+
throw new Error("VO_CODE_RUNNER_REPOS must name at least one owner/name repository");
|
|
16117
|
+
}
|
|
16118
|
+
if (repositories.length > RUNNER_READINESS_MAX_REPOSITORIES) {
|
|
16119
|
+
throw new Error(`VO_CODE_RUNNER_REPOS supports at most ${RUNNER_READINESS_MAX_REPOSITORIES} repositories`);
|
|
16120
|
+
}
|
|
16121
|
+
const normalized = repositories.map((repo) => {
|
|
16122
|
+
const [owner, name] = typeof repo === "string" ? repo.split("/") : [];
|
|
16123
|
+
if (typeof repo !== "string" || repo.length > 140 || !RUNNER_REPOSITORY.test(repo) || owner === "." || owner === ".." || name === "." || name === "..") {
|
|
16124
|
+
throw new Error("VO_CODE_RUNNER_REPOS entries must be canonical owner/name repositories");
|
|
16125
|
+
}
|
|
16126
|
+
return repo.toLowerCase();
|
|
16127
|
+
});
|
|
16128
|
+
if (new Set(normalized).size !== normalized.length) {
|
|
16129
|
+
throw new Error("VO_CODE_RUNNER_REPOS entries must be unique");
|
|
16130
|
+
}
|
|
16131
|
+
const owners = new Set(normalized.map((repo) => repo.split("/")[0]));
|
|
16132
|
+
if (owners.size !== 1) {
|
|
16133
|
+
throw new Error("VO_CODE_RUNNER_REPOS entries must share one owner");
|
|
16134
|
+
}
|
|
16135
|
+
return Object.freeze(normalized.sort());
|
|
16136
|
+
}
|
|
16137
|
+
function runnerRepositoryScopeDigest(repositories) {
|
|
16138
|
+
const scope = normalizeRunnerRepositoryScope(repositories);
|
|
16139
|
+
return createHash("sha256").update(JSON.stringify({
|
|
16140
|
+
version: 1,
|
|
16141
|
+
repositories: scope
|
|
16142
|
+
}), "utf8").digest("hex");
|
|
16143
|
+
}
|
|
16144
|
+
function runnerReadinessRetryDelayMs(readiness) {
|
|
16145
|
+
if (readiness?.paired !== true || readiness?.githubReady !== false) return null;
|
|
16146
|
+
if (Number.isFinite(readiness.retryAfterMs) && readiness.retryAfterMs > 0) {
|
|
16147
|
+
return Math.ceil(readiness.retryAfterMs);
|
|
16148
|
+
}
|
|
16149
|
+
return null;
|
|
16150
|
+
}
|
|
16151
|
+
function runnerReadinessFailureDisposition(readiness, delivery = null) {
|
|
16152
|
+
const retryAfterMs = runnerReadinessRetryDelayMs(readiness);
|
|
16153
|
+
if (retryAfterMs === null) {
|
|
16154
|
+
return Object.freeze({ action: "exit", exitCode: 1, retryAfterMs: null });
|
|
16155
|
+
}
|
|
16156
|
+
if (delivery?.attempted === true) {
|
|
16157
|
+
return Object.freeze({
|
|
16158
|
+
action: "exit",
|
|
16159
|
+
exitCode: delivery.delivered === true ? 75 : 1,
|
|
16160
|
+
retryAfterMs
|
|
16161
|
+
});
|
|
16162
|
+
}
|
|
16163
|
+
return Object.freeze({ action: "wait", exitCode: null, retryAfterMs });
|
|
16164
|
+
}
|
|
16165
|
+
async function notifySupervisorReadinessDeferral({
|
|
16166
|
+
processLike,
|
|
16167
|
+
retryAfterMs,
|
|
16168
|
+
error = null,
|
|
16169
|
+
ackTimeoutMs = RUNNER_READINESS_IPC_ACK_TIMEOUT_MS
|
|
16170
|
+
}) {
|
|
16171
|
+
if (processLike?.connected !== true || typeof processLike.send !== "function") {
|
|
16172
|
+
return Object.freeze({ attempted: false, delivered: false });
|
|
16173
|
+
}
|
|
16174
|
+
if (typeof processLike.on !== "function" || typeof processLike.off !== "function") {
|
|
16175
|
+
return Object.freeze({ attempted: true, delivered: false });
|
|
16176
|
+
}
|
|
16177
|
+
const nonce = randomUUID();
|
|
16178
|
+
const delivered = await new Promise((resolve3) => {
|
|
16179
|
+
let settled = false;
|
|
16180
|
+
const onMessage = (message) => {
|
|
16181
|
+
if (message?.type === RUNNER_READINESS_DEFERRAL_ACK_TYPE && message.nonce === nonce) {
|
|
16182
|
+
finish(true);
|
|
16183
|
+
}
|
|
16184
|
+
};
|
|
16185
|
+
const onDisconnect = () => finish(false);
|
|
16186
|
+
const finish = (value) => {
|
|
16187
|
+
if (settled) return;
|
|
16188
|
+
settled = true;
|
|
16189
|
+
clearTimeout(timer);
|
|
16190
|
+
processLike.off("message", onMessage);
|
|
16191
|
+
processLike.off("disconnect", onDisconnect);
|
|
16192
|
+
resolve3(value);
|
|
16193
|
+
};
|
|
16194
|
+
const timer = setTimeout(() => finish(false), ackTimeoutMs);
|
|
16195
|
+
processLike.on("message", onMessage);
|
|
16196
|
+
processLike.on("disconnect", onDisconnect);
|
|
16197
|
+
try {
|
|
16198
|
+
processLike.send({
|
|
16199
|
+
type: RUNNER_READINESS_DEFERRAL_MESSAGE_TYPE,
|
|
16200
|
+
nonce,
|
|
16201
|
+
retryAfterMs,
|
|
16202
|
+
error
|
|
16203
|
+
}, (sendError) => {
|
|
16204
|
+
if (sendError) finish(false);
|
|
16205
|
+
});
|
|
16206
|
+
} catch {
|
|
16207
|
+
finish(false);
|
|
16208
|
+
}
|
|
16209
|
+
});
|
|
16210
|
+
return Object.freeze({ attempted: true, delivered });
|
|
16211
|
+
}
|
|
16212
|
+
async function waitForRunnerReadinessRetry(delayMs, {
|
|
16213
|
+
nowMs = () => Date.now(),
|
|
16214
|
+
wait: wait3 = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms))
|
|
16215
|
+
} = {}) {
|
|
16216
|
+
if (!Number.isFinite(delayMs) || delayMs <= 0) {
|
|
16217
|
+
throw new TypeError("runner readiness retry delay is invalid");
|
|
16218
|
+
}
|
|
16219
|
+
const deadline = nowMs() + Math.ceil(delayMs);
|
|
16220
|
+
while (nowMs() < deadline) {
|
|
16221
|
+
await wait3(Math.min(RUNNER_READINESS_MAX_TIMER_DELAY_MS, deadline - nowMs()));
|
|
16222
|
+
}
|
|
16223
|
+
}
|
|
16224
|
+
function failed({
|
|
16225
|
+
paired = false,
|
|
16226
|
+
operatorId = null,
|
|
16227
|
+
tenantId = null,
|
|
16228
|
+
githubReady = null,
|
|
16229
|
+
retryAfterMs = null,
|
|
16230
|
+
error,
|
|
16231
|
+
message
|
|
16232
|
+
}) {
|
|
16233
|
+
return {
|
|
16234
|
+
ok: false,
|
|
16235
|
+
paired,
|
|
16236
|
+
operatorId,
|
|
16237
|
+
tenantId,
|
|
16238
|
+
githubReady,
|
|
16239
|
+
...Number.isFinite(retryAfterMs) && retryAfterMs > 0 ? { retryAfterMs } : {},
|
|
16240
|
+
error,
|
|
16241
|
+
message
|
|
16242
|
+
};
|
|
16243
|
+
}
|
|
16244
|
+
function responseRetryAfterMs(response, body) {
|
|
16245
|
+
const transientReadinessFailure = response.status === 503 && body?.error === "github_installation_readiness_retryable";
|
|
16246
|
+
if (response.status !== 429 && !transientReadinessFailure) return null;
|
|
16247
|
+
const bound = (retryAfterMs) => transientReadinessFailure ? Math.min(RUNNER_READINESS_TRANSIENT_MAX_RETRY_AFTER_MS, retryAfterMs) : retryAfterMs;
|
|
16248
|
+
const value = response.headers?.get?.("retry-after")?.trim() ?? "";
|
|
16249
|
+
const seconds = Number(value);
|
|
16250
|
+
if (value && Number.isFinite(seconds) && seconds >= 0) {
|
|
16251
|
+
return bound(Math.max(1e3, Math.ceil(seconds * 1e3)));
|
|
16252
|
+
}
|
|
16253
|
+
const at = value ? Date.parse(value) : Number.NaN;
|
|
16254
|
+
if (Number.isFinite(at)) {
|
|
16255
|
+
return bound(Math.max(1e3, Math.ceil(at - Date.now())));
|
|
16256
|
+
}
|
|
16257
|
+
return bound(6e4);
|
|
15809
16258
|
}
|
|
15810
16259
|
async function responseBody(response) {
|
|
15811
16260
|
try {
|
|
@@ -15818,11 +16267,21 @@ async function responseBody(response) {
|
|
|
15818
16267
|
function serverMessage(body, fallback) {
|
|
15819
16268
|
return typeof body.message === "string" && body.message.trim() ? body.message.trim() : fallback;
|
|
15820
16269
|
}
|
|
15821
|
-
async function
|
|
16270
|
+
async function fetchJsonWithTimeout(fetchImpl, url, init, timeoutMs) {
|
|
15822
16271
|
const controller = new AbortController();
|
|
15823
|
-
|
|
16272
|
+
let timer;
|
|
16273
|
+
const timeout = new Promise((_, reject) => {
|
|
16274
|
+
timer = setTimeout(() => {
|
|
16275
|
+
controller.abort();
|
|
16276
|
+
reject(new Error(`request aborted after ${timeoutMs}ms`));
|
|
16277
|
+
}, timeoutMs);
|
|
16278
|
+
});
|
|
15824
16279
|
try {
|
|
15825
|
-
|
|
16280
|
+
const requestAndBody = (async () => {
|
|
16281
|
+
const response = await fetchImpl(url, { ...init, signal: controller.signal });
|
|
16282
|
+
return { response, body: await responseBody(response) };
|
|
16283
|
+
})();
|
|
16284
|
+
return await Promise.race([requestAndBody, timeout]);
|
|
15826
16285
|
} finally {
|
|
15827
16286
|
clearTimeout(timer);
|
|
15828
16287
|
}
|
|
@@ -15832,18 +16291,24 @@ async function probeRunnerReadiness({
|
|
|
15832
16291
|
token: token2,
|
|
15833
16292
|
fetchImpl = fetch,
|
|
15834
16293
|
requireGithub = false,
|
|
15835
|
-
|
|
16294
|
+
repositories = [],
|
|
16295
|
+
timeoutMs = RUNNER_IDENTITY_READINESS_TIMEOUT_MS,
|
|
16296
|
+
// The server performs at most five sequential App-JWT GETs (installation plus
|
|
16297
|
+
// every one of at most four configured repositories), each bounded at 8s.
|
|
16298
|
+
// Keep a transport margin while preventing a stuck proof from hanging startup.
|
|
16299
|
+
githubTimeoutMs = RUNNER_GITHUB_READINESS_TIMEOUT_MS
|
|
15836
16300
|
}) {
|
|
15837
16301
|
const base = controlPlaneUrl2.replace(/\/+$/u, "");
|
|
15838
16302
|
const headers = { authorization: `Bearer ${token2}` };
|
|
15839
16303
|
let identityResponse;
|
|
16304
|
+
let identity;
|
|
15840
16305
|
try {
|
|
15841
|
-
identityResponse = await
|
|
16306
|
+
({ response: identityResponse, body: identity } = await fetchJsonWithTimeout(
|
|
15842
16307
|
fetchImpl,
|
|
15843
16308
|
`${base}/api/v1/auth/me`,
|
|
15844
16309
|
{ headers },
|
|
15845
16310
|
timeoutMs
|
|
15846
|
-
);
|
|
16311
|
+
));
|
|
15847
16312
|
} catch (error) {
|
|
15848
16313
|
const detail = error instanceof Error ? error.message : String(error);
|
|
15849
16314
|
return failed({
|
|
@@ -15851,7 +16316,6 @@ async function probeRunnerReadiness({
|
|
|
15851
16316
|
message: `AlgoHQ could not be reached: ${detail}`
|
|
15852
16317
|
});
|
|
15853
16318
|
}
|
|
15854
|
-
const identity = await responseBody(identityResponse);
|
|
15855
16319
|
if (!identityResponse.ok) {
|
|
15856
16320
|
return failed({
|
|
15857
16321
|
error: "credential_rejected",
|
|
@@ -15877,18 +16341,33 @@ async function probeRunnerReadiness({
|
|
|
15877
16341
|
message: "Paired to AlgoHQ."
|
|
15878
16342
|
};
|
|
15879
16343
|
}
|
|
16344
|
+
let repositoryScope = null;
|
|
16345
|
+
try {
|
|
16346
|
+
if (!Array.isArray(repositories)) {
|
|
16347
|
+
throw new Error("VO_CODE_RUNNER_REPOS must be a repository array");
|
|
16348
|
+
}
|
|
16349
|
+
if (repositories.length > 0) repositoryScope = normalizeRunnerRepositoryScope(repositories);
|
|
16350
|
+
} catch (error) {
|
|
16351
|
+
return failed({
|
|
16352
|
+
paired: true,
|
|
16353
|
+
operatorId,
|
|
16354
|
+
tenantId,
|
|
16355
|
+
githubReady: false,
|
|
16356
|
+
error: "github_repository_scope_invalid",
|
|
16357
|
+
message: error instanceof Error ? error.message : String(error)
|
|
16358
|
+
});
|
|
16359
|
+
}
|
|
15880
16360
|
let githubResponse;
|
|
16361
|
+
let github;
|
|
15881
16362
|
try {
|
|
15882
|
-
|
|
16363
|
+
const readinessUrl = new URL(`${base}/api/v1/github/installation-readiness`);
|
|
16364
|
+
for (const repo of repositoryScope ?? []) readinessUrl.searchParams.append("repo", repo);
|
|
16365
|
+
({ response: githubResponse, body: github } = await fetchJsonWithTimeout(
|
|
15883
16366
|
fetchImpl,
|
|
15884
|
-
|
|
15885
|
-
{
|
|
15886
|
-
|
|
15887
|
-
|
|
15888
|
-
body: "{}"
|
|
15889
|
-
},
|
|
15890
|
-
timeoutMs
|
|
15891
|
-
);
|
|
16367
|
+
readinessUrl.toString(),
|
|
16368
|
+
{ headers },
|
|
16369
|
+
githubTimeoutMs
|
|
16370
|
+
));
|
|
15892
16371
|
} catch (error) {
|
|
15893
16372
|
const detail = error instanceof Error ? error.message : String(error);
|
|
15894
16373
|
return failed({
|
|
@@ -15900,14 +16379,25 @@ async function probeRunnerReadiness({
|
|
|
15900
16379
|
message: `GitHub publication readiness could not be checked: ${detail}`
|
|
15901
16380
|
});
|
|
15902
16381
|
}
|
|
15903
|
-
const
|
|
15904
|
-
|
|
16382
|
+
const repositorySelection = github.repository_selection;
|
|
16383
|
+
const repositoriesVerified = github.repositories_verified;
|
|
16384
|
+
const returnedScope = github.repository_scope;
|
|
16385
|
+
let normalizedReturnedScope = null;
|
|
16386
|
+
try {
|
|
16387
|
+
normalizedReturnedScope = normalizeRunnerRepositoryScope(returnedScope);
|
|
16388
|
+
} catch {
|
|
16389
|
+
}
|
|
16390
|
+
const effectiveScope = repositoryScope ?? normalizedReturnedScope;
|
|
16391
|
+
const sourceVerified = repositoryScope === null ? github.repository_scope_source === "persisted_installation_singleton" && normalizedReturnedScope?.length === 1 : github.repository_scope_source === "runner_config";
|
|
16392
|
+
const repositoryScopeVerified = effectiveScope !== null && normalizedReturnedScope !== null && Array.isArray(returnedScope) && returnedScope.length === normalizedReturnedScope.length && returnedScope.every((repo, index) => repo === normalizedReturnedScope[index]) && normalizedReturnedScope.length === effectiveScope.length && normalizedReturnedScope.every((repo, index) => repo === effectiveScope[index]) && github.repository_scope_sha256 === runnerRepositoryScopeDigest(effectiveScope) && repositoriesVerified === effectiveScope.length && sourceVerified && (repositorySelection === "all" || repositorySelection === "selected");
|
|
16393
|
+
if (!githubResponse.ok || github.configured !== true || github.verified !== true || github.publication_ready !== true || !repositoryScopeVerified) {
|
|
15905
16394
|
const error = typeof github.error === "string" && github.error ? github.error : "github_not_ready";
|
|
15906
16395
|
return failed({
|
|
15907
16396
|
paired: true,
|
|
15908
16397
|
operatorId,
|
|
15909
16398
|
tenantId,
|
|
15910
16399
|
githubReady: false,
|
|
16400
|
+
retryAfterMs: responseRetryAfterMs(githubResponse, github),
|
|
15911
16401
|
error,
|
|
15912
16402
|
message: serverMessage(github, `GitHub publication preflight failed (HTTP ${githubResponse.status}).`)
|
|
15913
16403
|
});
|
|
@@ -15918,8 +16408,10 @@ async function probeRunnerReadiness({
|
|
|
15918
16408
|
operatorId,
|
|
15919
16409
|
tenantId,
|
|
15920
16410
|
githubReady: true,
|
|
16411
|
+
repositoryScope: effectiveScope,
|
|
16412
|
+
repositoryScopeSource: github.repository_scope_source,
|
|
15921
16413
|
error: null,
|
|
15922
|
-
message:
|
|
16414
|
+
message: `Paired with a verified Algosuite GitHub App installation (${repositoriesVerified} repos).`
|
|
15923
16415
|
};
|
|
15924
16416
|
}
|
|
15925
16417
|
function pairedOperatorScope(readiness) {
|
|
@@ -15930,7 +16422,7 @@ function pairedOperatorScope(readiness) {
|
|
|
15930
16422
|
import { closeSync, existsSync, mkdirSync, openSync, unlinkSync } from "node:fs";
|
|
15931
16423
|
import { homedir } from "node:os";
|
|
15932
16424
|
import { posix, win32 } from "node:path";
|
|
15933
|
-
import { randomUUID } from "node:crypto";
|
|
16425
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
15934
16426
|
var APP_IDENTIFIER = "ai.algosuite.vo-runner";
|
|
15935
16427
|
var CLONES_DIR = "clones";
|
|
15936
16428
|
function pathsFor(platform4) {
|
|
@@ -16000,7 +16492,7 @@ function runnerWorkingDirectory({ repoRoot: repoRoot2, clonesRoot: clonesRoot2 }
|
|
|
16000
16492
|
}
|
|
16001
16493
|
function assertWritableRunnerDirectory(root) {
|
|
16002
16494
|
mkdirSync(root, { recursive: true });
|
|
16003
|
-
const probe = pathsFor(process.platform).join(root, `.vo-runner-write-probe-${process.pid}-${
|
|
16495
|
+
const probe = pathsFor(process.platform).join(root, `.vo-runner-write-probe-${process.pid}-${randomUUID2()}`);
|
|
16004
16496
|
let handle;
|
|
16005
16497
|
try {
|
|
16006
16498
|
handle = openSync(probe, "wx", 384);
|
|
@@ -16120,21 +16612,51 @@ if (!token) {
|
|
|
16120
16612
|
}
|
|
16121
16613
|
var controlPlaneUrl = process.env.VO_CONTROL_PLANE_URL || DEFAULT_CONTROL_PLANE_URL3;
|
|
16122
16614
|
var pairedOperatorId = null;
|
|
16123
|
-
|
|
16124
|
-
const
|
|
16125
|
-
|
|
16126
|
-
|
|
16127
|
-
|
|
16615
|
+
async function deferRunnerReadinessRetry(readiness) {
|
|
16616
|
+
const initial = runnerReadinessFailureDisposition(readiness);
|
|
16617
|
+
if (initial.action === "exit") return initial;
|
|
16618
|
+
const { retryAfterMs } = initial;
|
|
16619
|
+
console.error(
|
|
16620
|
+
`[vo-mcp runner] GitHub readiness is temporarily unavailable; retrying after ${retryAfterMs}ms: ${readiness.message}`
|
|
16621
|
+
);
|
|
16622
|
+
const delivery = await notifySupervisorReadinessDeferral({
|
|
16623
|
+
processLike: process,
|
|
16624
|
+
retryAfterMs,
|
|
16625
|
+
error: readiness.error
|
|
16128
16626
|
});
|
|
16129
|
-
if (
|
|
16130
|
-
|
|
16131
|
-
|
|
16627
|
+
if (delivery.attempted) {
|
|
16628
|
+
const disposition = runnerReadinessFailureDisposition(readiness, delivery);
|
|
16629
|
+
if (disposition.exitCode === 1) {
|
|
16630
|
+
console.error("[vo-mcp runner] Supervisor readiness deferral could not be delivered; failing closed.");
|
|
16631
|
+
}
|
|
16632
|
+
return disposition;
|
|
16633
|
+
}
|
|
16634
|
+
await waitForRunnerReadinessRetry(retryAfterMs);
|
|
16635
|
+
return Object.freeze({ action: "retry", exitCode: null, retryAfterMs });
|
|
16636
|
+
}
|
|
16637
|
+
if (!explicitAdminToken) {
|
|
16638
|
+
let readiness;
|
|
16639
|
+
while (true) {
|
|
16640
|
+
readiness = await probeRunnerReadiness({
|
|
16641
|
+
controlPlaneUrl,
|
|
16642
|
+
token,
|
|
16643
|
+
requireGithub: true,
|
|
16644
|
+
repositories: runnerRepositoryScopeFromEnv(process.env)
|
|
16645
|
+
});
|
|
16646
|
+
if (readiness.ok) break;
|
|
16647
|
+
const disposition = await deferRunnerReadinessRetry(readiness);
|
|
16648
|
+
if (disposition.action === "retry") continue;
|
|
16649
|
+
if (disposition.retryAfterMs === null) {
|
|
16650
|
+
console.error(`[vo-mcp runner] Readiness check failed: ${readiness.message}`);
|
|
16651
|
+
}
|
|
16652
|
+
process.exit(disposition.exitCode);
|
|
16132
16653
|
}
|
|
16133
16654
|
pairedOperatorId = pairedOperatorScope(readiness);
|
|
16134
16655
|
if (!pairedOperatorId) {
|
|
16135
16656
|
console.error("[vo-mcp runner] Readiness check failed: paired operator scope is missing. Pair this computer again.");
|
|
16136
16657
|
process.exit(1);
|
|
16137
16658
|
}
|
|
16659
|
+
process.env.VO_CODE_RUNNER_REPOS = readiness.repositoryScope.join(",");
|
|
16138
16660
|
}
|
|
16139
16661
|
var rootConfig;
|
|
16140
16662
|
try {
|