@algosuite/vo-mcp 0.2.0-beta.55 → 0.2.0-beta.56
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/runner-cli.js +363 -72
- package/dist/runner-cli.js.map +4 -4
- package/dist/runner-supervisor.js +66 -13
- package/dist/runner-supervisor.js.map +4 -4
- package/package.json +1 -1
package/dist/runner-cli.js
CHANGED
|
@@ -1178,8 +1178,8 @@ async function walkManagedRoots(ownership, options, onLink) {
|
|
|
1178
1178
|
const normalized = normalizeOwnership(ownership);
|
|
1179
1179
|
let scannedEntries = 0;
|
|
1180
1180
|
for (const rootPath of normalized.managedRoots) {
|
|
1181
|
-
const
|
|
1182
|
-
if (!
|
|
1181
|
+
const present2 = await assertManagedRoot(rootPath, normalized.worktreeRoot, fsApi);
|
|
1182
|
+
if (!present2) continue;
|
|
1183
1183
|
const stack = [rootPath];
|
|
1184
1184
|
while (stack.length > 0) {
|
|
1185
1185
|
const current = stack.pop();
|
|
@@ -3497,8 +3497,9 @@ async function mergeVerifiedPrRequest(req, prNumber, automationContext, onUnauth
|
|
|
3497
3497
|
actionReceiptId: typeof json.action_receipt_id === "string" ? json.action_receipt_id : null
|
|
3498
3498
|
};
|
|
3499
3499
|
}
|
|
3500
|
-
|
|
3501
|
-
|
|
3500
|
+
const captureStoreRetry = json?.action_status === "not_attempted" && (json?.error === "capture_preflight_unavailable" || json?.error === "decision_outcome_intent_failed");
|
|
3501
|
+
if (res.status === 503 && (json?.error === "verify_unavailable" || json?.error === "merge_unavailable" || captureStoreRetry)) {
|
|
3502
|
+
return { status: "retry", reason: json.reason || json.message || json.error || "verification unavailable" };
|
|
3502
3503
|
}
|
|
3503
3504
|
return {
|
|
3504
3505
|
status: "blocked",
|
|
@@ -3604,6 +3605,64 @@ var init_claim_gate_notice = __esm({
|
|
|
3604
3605
|
}
|
|
3605
3606
|
});
|
|
3606
3607
|
|
|
3608
|
+
// ../../scripts/virtual-office/code-runner/control-plane-knowledge-context.mjs
|
|
3609
|
+
function isRetryableStatus(status) {
|
|
3610
|
+
return status >= 500 && status <= 599;
|
|
3611
|
+
}
|
|
3612
|
+
function defaultSleep(ms) {
|
|
3613
|
+
return new Promise((resolve3) => {
|
|
3614
|
+
setTimeout(resolve3, ms);
|
|
3615
|
+
});
|
|
3616
|
+
}
|
|
3617
|
+
async function getTaskKnowledgeContextRequest(req, taskId, { query } = {}, {
|
|
3618
|
+
taskRequestTimeoutMs,
|
|
3619
|
+
invalidateToken = () => {
|
|
3620
|
+
},
|
|
3621
|
+
sleep: sleep3 = defaultSleep,
|
|
3622
|
+
log: log2 = () => {
|
|
3623
|
+
}
|
|
3624
|
+
} = {}) {
|
|
3625
|
+
const body = {};
|
|
3626
|
+
if (typeof query === "string" && query.trim()) body.query = query;
|
|
3627
|
+
const path22 = `/api/v1/code-task/${encodeURIComponent(taskId)}/knowledge-context`;
|
|
3628
|
+
const timeoutMs = Math.max(Number(taskRequestTimeoutMs) || 0, MIN_KNOWLEDGE_CONTEXT_TIMEOUT_MS);
|
|
3629
|
+
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt += 1) {
|
|
3630
|
+
let res;
|
|
3631
|
+
let cause;
|
|
3632
|
+
try {
|
|
3633
|
+
res = await req("POST", path22, body, { timeoutMs });
|
|
3634
|
+
} catch (err) {
|
|
3635
|
+
cause = err;
|
|
3636
|
+
}
|
|
3637
|
+
if (!cause) {
|
|
3638
|
+
if (res.status === 401) {
|
|
3639
|
+
invalidateToken();
|
|
3640
|
+
throw new Error("knowledge-context unauthorized (401)");
|
|
3641
|
+
}
|
|
3642
|
+
if (res.status === 404) return null;
|
|
3643
|
+
if (res.ok) return res.json();
|
|
3644
|
+
if (!isRetryableStatus(res.status)) {
|
|
3645
|
+
throw new Error(`knowledge-context failed: HTTP ${res.status}`);
|
|
3646
|
+
}
|
|
3647
|
+
cause = new Error(`knowledge-context failed: HTTP ${res.status}`);
|
|
3648
|
+
}
|
|
3649
|
+
if (attempt === MAX_ATTEMPTS) throw cause;
|
|
3650
|
+
const delayMs = RETRY_DELAYS_MS[attempt - 1];
|
|
3651
|
+
log2(`knowledge-context attempt ${attempt}/${MAX_ATTEMPTS} failed (${cause.message}); retrying in ${delayMs}ms`);
|
|
3652
|
+
await sleep3(delayMs);
|
|
3653
|
+
}
|
|
3654
|
+
throw new Error("knowledge-context retry loop exited unexpectedly");
|
|
3655
|
+
}
|
|
3656
|
+
var MIN_KNOWLEDGE_CONTEXT_TIMEOUT_MS, RETRY_DELAYS_MS, MAX_ATTEMPTS;
|
|
3657
|
+
var init_control_plane_knowledge_context = __esm({
|
|
3658
|
+
"../../scripts/virtual-office/code-runner/control-plane-knowledge-context.mjs"() {
|
|
3659
|
+
"use strict";
|
|
3660
|
+
MIN_KNOWLEDGE_CONTEXT_TIMEOUT_MS = 15e3;
|
|
3661
|
+
RETRY_DELAYS_MS = [2e3, 6e3];
|
|
3662
|
+
MAX_ATTEMPTS = RETRY_DELAYS_MS.length + 1;
|
|
3663
|
+
}
|
|
3664
|
+
});
|
|
3665
|
+
|
|
3607
3666
|
// src/runner/control-plane-auth-stub.mjs
|
|
3608
3667
|
var control_plane_auth_stub_exports = {};
|
|
3609
3668
|
__export(control_plane_auth_stub_exports, {
|
|
@@ -3647,7 +3706,8 @@ function createControlPlaneClient({
|
|
|
3647
3706
|
6e4
|
|
3648
3707
|
),
|
|
3649
3708
|
runnerId,
|
|
3650
|
-
runnerInstanceId
|
|
3709
|
+
runnerInstanceId,
|
|
3710
|
+
sleep: sleep3
|
|
3651
3711
|
} = {}) {
|
|
3652
3712
|
const resolvedBaseUrl = baseUrl ?? env2.VO_CONTROL_PLANE_URL ?? "";
|
|
3653
3713
|
if (!resolvedBaseUrl) throw new Error("VO_CONTROL_PLANE_URL is required for the code-runner daemon");
|
|
@@ -3828,17 +3888,16 @@ function createControlPlaneClient({
|
|
|
3828
3888
|
if (!res.ok) throw new Error(`attachment download failed: HTTP ${res.status}`);
|
|
3829
3889
|
return Buffer.from(await res.arrayBuffer());
|
|
3830
3890
|
},
|
|
3891
|
+
// Raised per-attempt timeout (>=15s) + bounded retry — see control-plane-knowledge-context.mjs.
|
|
3831
3892
|
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();
|
|
3893
|
+
return getTaskKnowledgeContextRequest(req, taskId, { query }, {
|
|
3894
|
+
taskRequestTimeoutMs,
|
|
3895
|
+
invalidateToken: () => {
|
|
3896
|
+
cachedFirebaseToken = null;
|
|
3897
|
+
},
|
|
3898
|
+
sleep: sleep3,
|
|
3899
|
+
log: (m) => console.warn(`[code-runner ${(/* @__PURE__ */ new Date()).toISOString()}] ${m}`)
|
|
3900
|
+
});
|
|
3842
3901
|
},
|
|
3843
3902
|
/** Weekly Claude token usage report — see control-plane-weekly-tokens.mjs. */
|
|
3844
3903
|
async postWeeklyTokens(report) {
|
|
@@ -3992,6 +4051,7 @@ var init_control_plane_client = __esm({
|
|
|
3992
4051
|
init_control_plane_weekly_tokens();
|
|
3993
4052
|
init_control_plane_telemetry_relay();
|
|
3994
4053
|
init_claim_gate_notice();
|
|
4054
|
+
init_control_plane_knowledge_context();
|
|
3995
4055
|
cachedFirebaseToken = null;
|
|
3996
4056
|
ClaimAuthorityChangedError = class extends Error {
|
|
3997
4057
|
constructor() {
|
|
@@ -8876,7 +8936,7 @@ function selectDueEntries({ entries = [], now, alreadyDispatched = /* @__PURE__
|
|
|
8876
8936
|
if (!code_task_id) continue;
|
|
8877
8937
|
if (seen.has(code_task_id)) continue;
|
|
8878
8938
|
const stableAttempts = Number(attemptsByKey[stableTaskKey(e)] || 0);
|
|
8879
|
-
if (Math.max(typeof attempts === "number" ? attempts : 0, stableAttempts) >=
|
|
8939
|
+
if (Math.max(typeof attempts === "number" ? attempts : 0, stableAttempts) >= MAX_ATTEMPTS2) {
|
|
8880
8940
|
exhausted.push(e);
|
|
8881
8941
|
continue;
|
|
8882
8942
|
}
|
|
@@ -8912,11 +8972,11 @@ function reconcileQueue({ entries = [], dispatchedIds = /* @__PURE__ */ new Set(
|
|
|
8912
8972
|
return true;
|
|
8913
8973
|
});
|
|
8914
8974
|
}
|
|
8915
|
-
var
|
|
8975
|
+
var MAX_ATTEMPTS2, MAX_DISPATCH_PER_RUN, NULL_RESUME_AFTER_BACKOFF_MS;
|
|
8916
8976
|
var init_rate_limit_resume_scheduler_core = __esm({
|
|
8917
8977
|
"../../scripts/virtual-office/code-runner/rate-limit-resume-scheduler-core.mjs"() {
|
|
8918
8978
|
"use strict";
|
|
8919
|
-
|
|
8979
|
+
MAX_ATTEMPTS2 = 3;
|
|
8920
8980
|
MAX_DISPATCH_PER_RUN = 10;
|
|
8921
8981
|
NULL_RESUME_AFTER_BACKOFF_MS = 15 * 60 * 1e3;
|
|
8922
8982
|
}
|
|
@@ -8943,7 +9003,7 @@ function bumpAttempts(store, key, sourceTaskId, nowIso) {
|
|
|
8943
9003
|
store[key] = {
|
|
8944
9004
|
count: (typeof prior.count === "number" ? prior.count : 0) + 1,
|
|
8945
9005
|
lastSeen: nowIso,
|
|
8946
|
-
resumedTaskIds: [...resumedTaskIds, sourceTaskId].slice(-
|
|
9006
|
+
resumedTaskIds: [...resumedTaskIds, sourceTaskId].slice(-MAX_ATTEMPTS2)
|
|
8947
9007
|
};
|
|
8948
9008
|
}
|
|
8949
9009
|
function pruneAttemptsStore(store, nowIso) {
|
|
@@ -8995,7 +9055,7 @@ async function runLockedScheduler({
|
|
|
8995
9055
|
await writeResumeAttempts(attemptsPath, pruneAttemptsStore(attemptsStore, nowIso));
|
|
8996
9056
|
await writeResumeQueue(queuePath, kept);
|
|
8997
9057
|
if (exhausted.length > 0) {
|
|
8998
|
-
log2(`WARN: ${exhausted.length} task(s) reached the ${
|
|
9058
|
+
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
9059
|
}
|
|
9000
9060
|
if (due.length >= MAX_DISPATCH_PER_RUN) {
|
|
9001
9061
|
log2(`WARN: hit the per-run dispatch cap (${MAX_DISPATCH_PER_RUN}); more tasks remain queued`);
|
|
@@ -10446,39 +10506,6 @@ var init_pr_watcher_failure_confirmation = __esm({
|
|
|
10446
10506
|
}
|
|
10447
10507
|
});
|
|
10448
10508
|
|
|
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
10509
|
// ../../scripts/virtual-office/code-runner/error-message.mjs
|
|
10483
10510
|
function boundedErrorMessage(error, maxLength = 240) {
|
|
10484
10511
|
try {
|
|
@@ -10513,9 +10540,52 @@ function mergeEnqueueActive(entry, nowMs = Date.now()) {
|
|
|
10513
10540
|
if (!at) return true;
|
|
10514
10541
|
return nowMs - at < MERGE_ENQUEUE_TTL_MS;
|
|
10515
10542
|
}
|
|
10543
|
+
function migrateLegacyMergeState(entry, nowMs = Date.now()) {
|
|
10544
|
+
if (!entry) return false;
|
|
10545
|
+
if (entry.mergeEnqueued === true && !(Number(entry.mergeEnqueuedAt) > 0)) {
|
|
10546
|
+
entry.mergeEnqueuedAt = nowMs;
|
|
10547
|
+
return true;
|
|
10548
|
+
}
|
|
10549
|
+
const exhausted = entry.mergeRetryExhausted === true || (Number(entry.mergeAttempts) || 0) >= 3 || (Number(entry.mergeErrors) || 0) >= 3;
|
|
10550
|
+
if (entry.mergeTerminal === true || mergeEnqueueActive(entry, nowMs) || !exhausted) return false;
|
|
10551
|
+
entry.mergeRetryExhausted = true;
|
|
10552
|
+
entry.mergeTerminal = true;
|
|
10553
|
+
entry.mergeBlockReason ||= "Legacy watcher state exhausted 3 automatic merge attempts without a terminal result.";
|
|
10554
|
+
return true;
|
|
10555
|
+
}
|
|
10516
10556
|
function isTerminalResumeRefusal(error) {
|
|
10517
10557
|
return typeof error?.code === "string" && TERMINAL_RESUME_REFUSALS.includes(error.code);
|
|
10518
10558
|
}
|
|
10559
|
+
async function reportPendingMergeExhaustion({
|
|
10560
|
+
entry,
|
|
10561
|
+
now,
|
|
10562
|
+
reportBlocker,
|
|
10563
|
+
taskId,
|
|
10564
|
+
prNumber,
|
|
10565
|
+
log: log2 = () => {
|
|
10566
|
+
}
|
|
10567
|
+
}) {
|
|
10568
|
+
if (!entry.mergeRetryExhausted || entry.mergeEscalatedAt || typeof reportBlocker !== "function") return false;
|
|
10569
|
+
const lastError = boundedErrorMessage(entry.mergeRetryReason || entry.mergeBlockReason);
|
|
10570
|
+
try {
|
|
10571
|
+
await reportBlocker({
|
|
10572
|
+
taskId,
|
|
10573
|
+
message: `PR #${prNumber} automatic merge verification exhausted its bounded retry budget; operator review is required. Last error: ${lastError}`
|
|
10574
|
+
});
|
|
10575
|
+
entry.mergeEscalatedAt = now();
|
|
10576
|
+
return true;
|
|
10577
|
+
} catch (reportError) {
|
|
10578
|
+
log2(`watch: pr #${prNumber} merge escalation failed; terminal state retained: ${boundedErrorMessage(reportError)}`);
|
|
10579
|
+
return false;
|
|
10580
|
+
}
|
|
10581
|
+
}
|
|
10582
|
+
async function scheduleMergeRetry(input) {
|
|
10583
|
+
await scheduleCoordinationRetry({ ...input, kind: "merge" });
|
|
10584
|
+
return {
|
|
10585
|
+
terminal: input.entry.mergeTerminal === true,
|
|
10586
|
+
escalated: await reportPendingMergeExhaustion(input)
|
|
10587
|
+
};
|
|
10588
|
+
}
|
|
10519
10589
|
async function scheduleCoordinationRetry({
|
|
10520
10590
|
entry,
|
|
10521
10591
|
kind,
|
|
@@ -10534,10 +10604,21 @@ async function scheduleCoordinationRetry({
|
|
|
10534
10604
|
log2(`watch: pr #${prNumber} automatic continuation refused by the plane (${error.code}); no retry \u2014 operator may resume explicitly with a larger cap`);
|
|
10535
10605
|
return;
|
|
10536
10606
|
}
|
|
10537
|
-
const errorsKey = kind === "resume" ? "resumeErrors" : "enqueueErrors";
|
|
10538
|
-
const attemptsKey = kind === "resume" ? "resumeAttempts" : "fixAttempts";
|
|
10607
|
+
const errorsKey = kind === "resume" ? "resumeErrors" : kind === "merge" ? "mergeErrors" : "enqueueErrors";
|
|
10608
|
+
const attemptsKey = kind === "resume" ? "resumeAttempts" : kind === "merge" ? "mergeAttempts" : "fixAttempts";
|
|
10539
10609
|
entry[errorsKey] = (entry[errorsKey] || 0) + 1;
|
|
10540
|
-
|
|
10610
|
+
if (kind === "merge") {
|
|
10611
|
+
entry.mergeRetryReason = boundedErrorMessage(error);
|
|
10612
|
+
if (entry[errorsKey] >= 3) {
|
|
10613
|
+
entry.mergeTerminal = true;
|
|
10614
|
+
entry.mergeRetryExhausted = true;
|
|
10615
|
+
entry.mergeBlockReason = `Automatic merge verification remained unavailable after 3 attempts: ${entry.mergeRetryReason}`;
|
|
10616
|
+
delete entry.nextRetryAt;
|
|
10617
|
+
log2(`watch: pr #${prNumber} merge retry cap reached \u2014 terminal and operator review required`);
|
|
10618
|
+
return;
|
|
10619
|
+
}
|
|
10620
|
+
}
|
|
10621
|
+
if (kind !== "merge") entry[attemptsKey] = Math.max(0, (entry[attemptsKey] || 1) - 1);
|
|
10541
10622
|
const delay2 = Math.min(MAX_BACKOFF_MS2, 3e4 * 2 ** Math.min(7, entry[errorsKey] - 1));
|
|
10542
10623
|
entry.nextRetryAt = now() + delay2;
|
|
10543
10624
|
if (entry[errorsKey] >= 3 && !entry.coordinationEscalatedAt && typeof reportBlocker === "function") {
|
|
@@ -10646,16 +10727,87 @@ var init_watcher_state = __esm({
|
|
|
10646
10727
|
function normalizedRepo(repo) {
|
|
10647
10728
|
return String(repo || "").trim().toLowerCase();
|
|
10648
10729
|
}
|
|
10730
|
+
function present(value) {
|
|
10731
|
+
return value !== null && value !== void 0 && value !== "";
|
|
10732
|
+
}
|
|
10649
10733
|
function watcherKey(state, repo, prNumber) {
|
|
10734
|
+
const targetRepo = normalizedRepo(repo);
|
|
10735
|
+
for (const [key, entry] of Object.entries(state)) {
|
|
10736
|
+
if (watcherPrNumber(key, entry) !== Number(prNumber)) continue;
|
|
10737
|
+
if (normalizedRepo(entry?.repo) === targetRepo) return key;
|
|
10738
|
+
}
|
|
10650
10739
|
const legacy = String(prNumber);
|
|
10651
10740
|
const existing = state[legacy];
|
|
10652
|
-
if (!existing
|
|
10653
|
-
return `${
|
|
10741
|
+
if (!existing) return legacy;
|
|
10742
|
+
return `${targetRepo}#${prNumber}`;
|
|
10654
10743
|
}
|
|
10655
10744
|
function watcherPrNumber(stateKey, entry) {
|
|
10656
10745
|
const value = Number(entry?.prNumber ?? String(stateKey).split("#").at(-1));
|
|
10657
10746
|
return Number.isInteger(value) && value > 0 ? value : null;
|
|
10658
10747
|
}
|
|
10748
|
+
function collapseDuplicateWatcherEntries(state) {
|
|
10749
|
+
const keeperBySubject = /* @__PURE__ */ new Map();
|
|
10750
|
+
let collapsed = 0;
|
|
10751
|
+
for (const [key, entry] of Object.entries(state)) {
|
|
10752
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
|
|
10753
|
+
const repo = normalizedRepo(entry.repo);
|
|
10754
|
+
const prNumber = watcherPrNumber(key, entry);
|
|
10755
|
+
if (!repo || !prNumber) continue;
|
|
10756
|
+
const subject = `${repo}#${prNumber}`;
|
|
10757
|
+
const keeperKey = keeperBySubject.get(subject);
|
|
10758
|
+
if (!keeperKey) {
|
|
10759
|
+
keeperBySubject.set(subject, key);
|
|
10760
|
+
continue;
|
|
10761
|
+
}
|
|
10762
|
+
const keeper = state[keeperKey];
|
|
10763
|
+
let identityConflict = false;
|
|
10764
|
+
for (const field of ["taskId", "operatorId", "tenantId"]) {
|
|
10765
|
+
const current = keeper[field];
|
|
10766
|
+
const duplicate = entry[field];
|
|
10767
|
+
if (!present(current) && present(duplicate)) keeper[field] = duplicate;
|
|
10768
|
+
else if (present(current) && present(duplicate) && current !== duplicate) identityConflict = true;
|
|
10769
|
+
}
|
|
10770
|
+
if (keeper.operatorId === "admin" && present(keeper.tenantId)) identityConflict = true;
|
|
10771
|
+
for (const field of ["fixAttempts", "mergeAttempts", "mergeErrors", "resumeAttempts"]) {
|
|
10772
|
+
const current = Number(keeper[field]);
|
|
10773
|
+
const duplicate = Number(entry[field]);
|
|
10774
|
+
const total = (Number.isSafeInteger(current) && current > 0 ? current : 0) + (Number.isSafeInteger(duplicate) && duplicate > 0 ? duplicate : 0);
|
|
10775
|
+
keeper[field] = Math.min(Number.MAX_SAFE_INTEGER, total);
|
|
10776
|
+
}
|
|
10777
|
+
for (const [field, value] of Object.entries(entry)) {
|
|
10778
|
+
if (["taskId", "operatorId", "tenantId"].includes(field)) continue;
|
|
10779
|
+
if (!present(keeper[field]) && present(value)) keeper[field] = value;
|
|
10780
|
+
}
|
|
10781
|
+
if (entry.allowFixDispatch === false) keeper.allowFixDispatch = false;
|
|
10782
|
+
const continuationExhausted = keeper.continuationExhausted === true || entry.continuationExhausted === true;
|
|
10783
|
+
keeper.continuationExhausted = continuationExhausted;
|
|
10784
|
+
keeper.needsContinuation = !continuationExhausted && (keeper.needsContinuation === true || entry.needsContinuation === true);
|
|
10785
|
+
if (entry.mergeEnqueued === true) {
|
|
10786
|
+
keeper.mergeEnqueued = true;
|
|
10787
|
+
if ((Number(entry.mergeEnqueuedAt) || 0) > (Number(keeper.mergeEnqueuedAt) || 0)) {
|
|
10788
|
+
keeper.mergeEnqueuedAt = entry.mergeEnqueuedAt;
|
|
10789
|
+
keeper.mergeActionReceiptId = entry.mergeActionReceiptId ?? null;
|
|
10790
|
+
}
|
|
10791
|
+
}
|
|
10792
|
+
if (entry.mergeTerminal === true && keeper.mergeTerminal !== true) {
|
|
10793
|
+
keeper.mergeTerminal = true;
|
|
10794
|
+
keeper.mergeBlockReason = entry.mergeBlockReason || "verification refused";
|
|
10795
|
+
}
|
|
10796
|
+
if ((Number(keeper.mergeErrors) || 0) >= 3) {
|
|
10797
|
+
keeper.mergeRetryExhausted = true;
|
|
10798
|
+
keeper.mergeTerminal = true;
|
|
10799
|
+
keeper.mergeBlockReason ||= `Duplicate watcher history exhausted ${keeper.mergeErrors} automatic merge retries.`;
|
|
10800
|
+
}
|
|
10801
|
+
if (identityConflict) {
|
|
10802
|
+
keeper.automationContextConflict = true;
|
|
10803
|
+
keeper.mergeTerminal = true;
|
|
10804
|
+
keeper.mergeBlockReason = "Duplicate watcher entries disagree on automation attribution.";
|
|
10805
|
+
}
|
|
10806
|
+
delete state[key];
|
|
10807
|
+
collapsed += 1;
|
|
10808
|
+
}
|
|
10809
|
+
return collapsed;
|
|
10810
|
+
}
|
|
10659
10811
|
function deleteWatcherEntry(state, repo, prNumber) {
|
|
10660
10812
|
const targetRepo = normalizedRepo(repo);
|
|
10661
10813
|
for (const [key, entry] of Object.entries(state)) {
|
|
@@ -10670,6 +10822,46 @@ var init_watcher_key = __esm({
|
|
|
10670
10822
|
}
|
|
10671
10823
|
});
|
|
10672
10824
|
|
|
10825
|
+
// ../../scripts/virtual-office/code-runner/watcher-merge-authority.mjs
|
|
10826
|
+
function nonEmptyString(value) {
|
|
10827
|
+
return typeof value === "string" && value.length > 0;
|
|
10828
|
+
}
|
|
10829
|
+
function resolveWatcherMergeAuthority(entry = {}) {
|
|
10830
|
+
if (entry.automationContextConflict === true) {
|
|
10831
|
+
return {
|
|
10832
|
+
kind: "blocked",
|
|
10833
|
+
reason: "Watcher automation attribution conflicts with the durable code task."
|
|
10834
|
+
};
|
|
10835
|
+
}
|
|
10836
|
+
if (String(entry.repo || "").trim().toLowerCase() !== CANONICAL_WATCHER_MERGE_REPO) {
|
|
10837
|
+
return {
|
|
10838
|
+
kind: "blocked",
|
|
10839
|
+
reason: "Watcher merge target is not the canonical Nexus repository."
|
|
10840
|
+
};
|
|
10841
|
+
}
|
|
10842
|
+
const taskId = entry.taskId;
|
|
10843
|
+
const operatorId = entry.operatorId;
|
|
10844
|
+
const tenantId = entry.tenantId;
|
|
10845
|
+
if (nonEmptyString(taskId) && operatorId === "admin" && (tenantId === null || tenantId === void 0)) {
|
|
10846
|
+
return { kind: "legacy-admin", context: void 0 };
|
|
10847
|
+
}
|
|
10848
|
+
if (nonEmptyString(taskId) && nonEmptyString(operatorId) && nonEmptyString(tenantId)) {
|
|
10849
|
+
return { kind: "scoped", context: { taskId, operatorId, tenantId } };
|
|
10850
|
+
}
|
|
10851
|
+
return {
|
|
10852
|
+
kind: "blocked",
|
|
10853
|
+
reason: INCOMPLETE_WATCHER_AUTHORITY_REASON
|
|
10854
|
+
};
|
|
10855
|
+
}
|
|
10856
|
+
var INCOMPLETE_WATCHER_AUTHORITY_REASON, CANONICAL_WATCHER_MERGE_REPO;
|
|
10857
|
+
var init_watcher_merge_authority = __esm({
|
|
10858
|
+
"../../scripts/virtual-office/code-runner/watcher-merge-authority.mjs"() {
|
|
10859
|
+
"use strict";
|
|
10860
|
+
INCOMPLETE_WATCHER_AUTHORITY_REASON = "Watcher automation attribution is incomplete; merge was not attempted.";
|
|
10861
|
+
CANONICAL_WATCHER_MERGE_REPO = "algosuite-ai/nexus";
|
|
10862
|
+
}
|
|
10863
|
+
});
|
|
10864
|
+
|
|
10673
10865
|
// ../../scripts/virtual-office/code-runner/watcher-adoption.mjs
|
|
10674
10866
|
async function adoptPrOpenedTasks(tasks, {
|
|
10675
10867
|
stateFile,
|
|
@@ -10690,7 +10882,38 @@ async function adoptPrOpenedTasks(tasks, {
|
|
|
10690
10882
|
if (repos.size > 0 && !repos.has(task.repo.toLowerCase())) continue;
|
|
10691
10883
|
if (operators.size > 0 && !operators.has(task.operator_id)) continue;
|
|
10692
10884
|
const key = watcherKey(state, task.repo, task.pr_number);
|
|
10693
|
-
|
|
10885
|
+
const existing = state[key];
|
|
10886
|
+
if (existing) {
|
|
10887
|
+
let changed = false;
|
|
10888
|
+
let conflict = false;
|
|
10889
|
+
for (const [field, durable] of [
|
|
10890
|
+
["taskId", task.code_task_id],
|
|
10891
|
+
["operatorId", task.operator_id],
|
|
10892
|
+
["tenantId", task.tenant_id]
|
|
10893
|
+
]) {
|
|
10894
|
+
const comparable = durable === null || typeof durable === "string" && durable.length > 0;
|
|
10895
|
+
if (!comparable) continue;
|
|
10896
|
+
const current = existing[field];
|
|
10897
|
+
if ((current === null || current === void 0 || current === "") && durable !== null) {
|
|
10898
|
+
existing[field] = durable;
|
|
10899
|
+
changed = true;
|
|
10900
|
+
} else if (current !== null && current !== void 0 && current !== "" && current !== durable) {
|
|
10901
|
+
conflict = true;
|
|
10902
|
+
}
|
|
10903
|
+
}
|
|
10904
|
+
if (conflict && existing.automationContextConflict !== true) {
|
|
10905
|
+
existing.automationContextConflict = true;
|
|
10906
|
+
changed = true;
|
|
10907
|
+
}
|
|
10908
|
+
if (!conflict && existing.mergeTerminal === true && existing.mergeBlockReason === INCOMPLETE_WATCHER_AUTHORITY_REASON && resolveWatcherMergeAuthority(existing).kind !== "blocked") {
|
|
10909
|
+
delete existing.mergeTerminal;
|
|
10910
|
+
delete existing.mergeBlockReason;
|
|
10911
|
+
existing.mergeAttempts = 0;
|
|
10912
|
+
changed = true;
|
|
10913
|
+
}
|
|
10914
|
+
if (changed) adopted += 1;
|
|
10915
|
+
continue;
|
|
10916
|
+
}
|
|
10694
10917
|
const partial = String(task.result || "").includes(PARTIAL_PR_CONTINUATION_MARKER);
|
|
10695
10918
|
const repairChain = task.repair_chain ?? {
|
|
10696
10919
|
root_pr_number: task.repair_pr_number ?? task.pr_number,
|
|
@@ -10726,6 +10949,7 @@ var init_watcher_adoption = __esm({
|
|
|
10726
10949
|
init_watcher_state();
|
|
10727
10950
|
init_partial_pr_continuation();
|
|
10728
10951
|
init_watcher_key();
|
|
10952
|
+
init_watcher_merge_authority();
|
|
10729
10953
|
}
|
|
10730
10954
|
});
|
|
10731
10955
|
|
|
@@ -10757,6 +10981,39 @@ var init_watcher_github_token = __esm({
|
|
|
10757
10981
|
}
|
|
10758
10982
|
});
|
|
10759
10983
|
|
|
10984
|
+
// ../../scripts/virtual-office/code-runner/superseded-pr-source.mjs
|
|
10985
|
+
function supersededSourcePrNumber(prompt) {
|
|
10986
|
+
const text = String(prompt || "");
|
|
10987
|
+
if (text.includes(CI_FIX_MARKER)) {
|
|
10988
|
+
const match = text.match(/\bPR:\s*#(\d+)\b/u);
|
|
10989
|
+
return match ? Number(match[1]) : null;
|
|
10990
|
+
}
|
|
10991
|
+
const repairMatch = text.match(/^REPAIR MISSION:\s*PR\s+#(\d+)\b/iu);
|
|
10992
|
+
if (repairMatch) return Number(repairMatch[1]);
|
|
10993
|
+
const recoverySupersedeMatch = text.match(
|
|
10994
|
+
/^VO_RECOVERY_FROM_CODE_TASK:\s*[0-9a-f-]{36}\n\nSupersede draft PR #(\d+) from a fresh current origin\/main branch\b/iu
|
|
10995
|
+
);
|
|
10996
|
+
if (recoverySupersedeMatch) return Number(recoverySupersedeMatch[1]);
|
|
10997
|
+
const restoredContextMatch = text.match(
|
|
10998
|
+
/^(?: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
|
|
10999
|
+
);
|
|
11000
|
+
if (restoredContextMatch) {
|
|
11001
|
+
return restoredContextMatch[1] === restoredContextMatch[2] ? Number(restoredContextMatch[1]) : null;
|
|
11002
|
+
}
|
|
11003
|
+
const continuationMatch = text.match(
|
|
11004
|
+
/^(?: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
|
|
11005
|
+
);
|
|
11006
|
+
if (!continuationMatch || continuationMatch[1] !== continuationMatch[2]) return null;
|
|
11007
|
+
return Number(continuationMatch[1]);
|
|
11008
|
+
}
|
|
11009
|
+
var CI_FIX_MARKER;
|
|
11010
|
+
var init_superseded_pr_source = __esm({
|
|
11011
|
+
"../../scripts/virtual-office/code-runner/superseded-pr-source.mjs"() {
|
|
11012
|
+
"use strict";
|
|
11013
|
+
CI_FIX_MARKER = "[VO-CI-FIX]";
|
|
11014
|
+
}
|
|
11015
|
+
});
|
|
11016
|
+
|
|
10760
11017
|
// ../../scripts/virtual-office/code-runner/ci-fix-prompt.mjs
|
|
10761
11018
|
function buildCiFixPrompt({ prNumber, repo, branch, headSha, failedChecks, prPatch, failedLogs }) {
|
|
10762
11019
|
return [
|
|
@@ -11038,6 +11295,7 @@ async function runWatchCycleUnlocked({
|
|
|
11038
11295
|
stateFile = DEFAULT_STATE_FILE
|
|
11039
11296
|
}) {
|
|
11040
11297
|
const state = await readWatcherState(stateFile);
|
|
11298
|
+
collapseDuplicateWatcherEntries(state);
|
|
11041
11299
|
const prNumbers = Object.keys(state);
|
|
11042
11300
|
let checked = 0;
|
|
11043
11301
|
let fixed = 0;
|
|
@@ -11063,6 +11321,7 @@ async function runWatchCycleUnlocked({
|
|
|
11063
11321
|
}
|
|
11064
11322
|
checked += 1;
|
|
11065
11323
|
const pr = parsePrCiStatus(view);
|
|
11324
|
+
migrateLegacyMergeState(entry, now());
|
|
11066
11325
|
entry.lastCi = pr.ci;
|
|
11067
11326
|
const confirmation = observeRepairFailure(entry, pr, now());
|
|
11068
11327
|
const proposedAction = decideWatchAction(
|
|
@@ -11133,10 +11392,30 @@ async function runWatchCycleUnlocked({
|
|
|
11133
11392
|
mergeBlocked += 1;
|
|
11134
11393
|
log2(`watch: pr #${prNumber} verification blocked merge: ${entry.mergeBlockReason}`);
|
|
11135
11394
|
} else {
|
|
11136
|
-
|
|
11395
|
+
const retry = await scheduleMergeRetry({
|
|
11396
|
+
entry,
|
|
11397
|
+
now,
|
|
11398
|
+
reportBlocker,
|
|
11399
|
+
taskId: entry.taskId,
|
|
11400
|
+
prNumber,
|
|
11401
|
+
error: new Error(outcome.reason || "verification unavailable"),
|
|
11402
|
+
log: log2
|
|
11403
|
+
});
|
|
11404
|
+
if (retry.terminal) mergeBlocked += 1;
|
|
11405
|
+
if (retry.escalated) escalated += 1;
|
|
11137
11406
|
}
|
|
11138
11407
|
} catch (err) {
|
|
11139
|
-
|
|
11408
|
+
const retry = await scheduleMergeRetry({
|
|
11409
|
+
entry,
|
|
11410
|
+
now,
|
|
11411
|
+
reportBlocker,
|
|
11412
|
+
taskId: entry.taskId,
|
|
11413
|
+
prNumber,
|
|
11414
|
+
error: err,
|
|
11415
|
+
log: log2
|
|
11416
|
+
});
|
|
11417
|
+
if (retry.terminal) mergeBlocked += 1;
|
|
11418
|
+
if (retry.escalated) escalated += 1;
|
|
11140
11419
|
}
|
|
11141
11420
|
} else if (action === "fix") {
|
|
11142
11421
|
entry.fixAttempts = (entry.fixAttempts || 0) + 1;
|
|
@@ -11169,6 +11448,7 @@ async function runWatchCycleUnlocked({
|
|
|
11169
11448
|
}
|
|
11170
11449
|
} else {
|
|
11171
11450
|
entry.lastCheckedAt = now();
|
|
11451
|
+
if (await reportPendingMergeExhaustion({ entry, now, reportBlocker, taskId: entry.taskId, prNumber, log: log2 })) escalated += 1;
|
|
11172
11452
|
const repairCapped = pr.ci === "failing" && ((entry.fixAttempts || 0) >= maxFixAttempts || entry.allowFixDispatch === false);
|
|
11173
11453
|
if ((repairCapped || entry.continuationExhausted) && !entry.escalatedAt) {
|
|
11174
11454
|
try {
|
|
@@ -11226,7 +11506,7 @@ function makeWatchRunner({
|
|
|
11226
11506
|
repairChainMax,
|
|
11227
11507
|
repairBudgetUsd
|
|
11228
11508
|
});
|
|
11229
|
-
if (adopted > 0) log2(`watch: adopted ${adopted} open task PR(s) from the control plane`);
|
|
11509
|
+
if (adopted > 0) log2(`watch: adopted or reconciled ${adopted} open task PR(s) from the control plane`);
|
|
11230
11510
|
return runWatchCycle({
|
|
11231
11511
|
viewPr: watchView,
|
|
11232
11512
|
enqueueFix: async ({ prNumber, repo, branch, headSha, failedChecks, failedCheckLinks, repairChain, operatorId }) => {
|
|
@@ -11268,13 +11548,10 @@ function makeWatchRunner({
|
|
|
11268
11548
|
return result;
|
|
11269
11549
|
},
|
|
11270
11550
|
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);
|
|
11551
|
+
const authority = resolveWatcherMergeAuthority(entry);
|
|
11552
|
+
if (authority.kind === "blocked")
|
|
11553
|
+
return Promise.resolve({ status: "blocked", reason: authority.reason });
|
|
11554
|
+
return client.mergeVerifiedPr(prNumber, authority.context);
|
|
11278
11555
|
},
|
|
11279
11556
|
log: log2,
|
|
11280
11557
|
maxFixAttempts,
|
|
@@ -11291,7 +11568,6 @@ var init_pr_watcher = __esm({
|
|
|
11291
11568
|
init_check_run_latest();
|
|
11292
11569
|
init_ci_repair_evidence();
|
|
11293
11570
|
init_pr_watcher_failure_confirmation();
|
|
11294
|
-
init_superseded_pr_source();
|
|
11295
11571
|
init_watcher_coordination();
|
|
11296
11572
|
init_watcher_adoption();
|
|
11297
11573
|
init_watcher_key();
|
|
@@ -11300,6 +11576,7 @@ var init_pr_watcher = __esm({
|
|
|
11300
11576
|
init_pr_watcher_github();
|
|
11301
11577
|
init_enqueue_autonomous_code_task();
|
|
11302
11578
|
init_watcher_state();
|
|
11579
|
+
init_watcher_merge_authority();
|
|
11303
11580
|
init_superseded_pr_source();
|
|
11304
11581
|
init_ci_fix_prompt();
|
|
11305
11582
|
DEFAULT_STATE_FILE = join14(homedir9(), ".vo", "dispatched-prs.json");
|
|
@@ -13110,9 +13387,16 @@ function makeSafeProgress(log2) {
|
|
|
13110
13387
|
function runnerStagePatch(stage, message, extra = {}) {
|
|
13111
13388
|
return { stage, message, ...extra };
|
|
13112
13389
|
}
|
|
13390
|
+
function preservedReceiptLines(run, slicedBodyText) {
|
|
13391
|
+
const full = `${String(run?.summary || "")}
|
|
13392
|
+
${String(run?.lastAgentMessage || "")}`;
|
|
13393
|
+
const found = [...new Set((full.match(RECEIPT_LINE_RE) || []).map((line) => line.trim()))];
|
|
13394
|
+
const missing = found.filter((line) => !String(slicedBodyText || "").includes(line));
|
|
13395
|
+
return missing.length ? ["", "### Consensus evidence (preserved past truncation)", "", ...missing.map((line) => redactSecrets(line))] : [];
|
|
13396
|
+
}
|
|
13113
13397
|
function buildPrBody(task, run, files, { armAutoMerge = false } = {}) {
|
|
13114
13398
|
const governedStakes = matchGovernedStakes({ prompt: String(task.prompt || "") });
|
|
13115
|
-
|
|
13399
|
+
const slicedSections = [
|
|
13116
13400
|
"## AlgoHQ Command Center \u2014 Code-from-Anywhere task",
|
|
13117
13401
|
"",
|
|
13118
13402
|
`- **Task:** \`${task.code_task_id}\``,
|
|
@@ -13135,7 +13419,12 @@ function buildPrBody(task, run, files, { armAutoMerge = false } = {}) {
|
|
|
13135
13419
|
"",
|
|
13136
13420
|
// A budget/turn-capped run ends with no assistant text (summary = the bare
|
|
13137
13421
|
// 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), ""] : []
|
|
13422
|
+
...run.lastAgentMessage ? ["### Last agent message before the cap", "", redactSecrets(String(run.lastAgentMessage)).slice(0, 2e3), ""] : []
|
|
13423
|
+
];
|
|
13424
|
+
return [
|
|
13425
|
+
...slicedSections,
|
|
13426
|
+
// Receipt lines the slices dropped — the gate reads only this body.
|
|
13427
|
+
...preservedReceiptLines(run, slicedSections.join("\n")),
|
|
13139
13428
|
"---",
|
|
13140
13429
|
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
13430
|
].filter((l) => l !== "").join("\n");
|
|
@@ -13153,11 +13442,13 @@ async function mintRunnerGithubTokens({ client, taskId, log: log2, repo = null,
|
|
|
13153
13442
|
if (!agentReadToken) log2(`task ${taskId}: no GitHub read access for the agent (${reason}); it cannot read a private repo`);
|
|
13154
13443
|
return { publishToken, agentReadToken };
|
|
13155
13444
|
}
|
|
13445
|
+
var RECEIPT_LINE_RE;
|
|
13156
13446
|
var init_task_helpers = __esm({
|
|
13157
13447
|
"../../scripts/virtual-office/code-runner/task-helpers.mjs"() {
|
|
13158
13448
|
"use strict";
|
|
13159
13449
|
init_redact_tokens();
|
|
13160
13450
|
init_methodology_composer();
|
|
13451
|
+
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
13452
|
}
|
|
13162
13453
|
});
|
|
13163
13454
|
|