From 4e09c62181b7af3cf94c2103f58a1d1072948213 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Jun 2026 03:57:25 +0000 Subject: [PATCH 1/3] Add shared non-retry harness guard Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/claude_harness.cjs | 10 +++++ actions/setup/js/codex_harness.cjs | 10 +++++ actions/setup/js/copilot_harness.cjs | 10 +++++ actions/setup/js/harness_retry_guard.cjs | 37 +++++++++++++++++++ actions/setup/js/harness_retry_guard.test.cjs | 29 +++++++++++++++ 5 files changed, 96 insertions(+) create mode 100644 actions/setup/js/harness_retry_guard.cjs create mode 100644 actions/setup/js/harness_retry_guard.test.cjs diff --git a/actions/setup/js/claude_harness.cjs b/actions/setup/js/claude_harness.cjs index 11514a2d866..05c84f660cc 100644 --- a/actions/setup/js/claude_harness.cjs +++ b/actions/setup/js/claude_harness.cjs @@ -47,6 +47,7 @@ const { } = require("./awf_reflect.cjs"); const { emitMissingToolPermissionIssue, hasNoopInSafeOutputs } = require("./safeoutputs_cli.cjs"); const { countPermissionDeniedIssues, hasNumerousPermissionDeniedIssues, extractDeniedCommands, buildMissingToolPermissionIssuePayload } = require("./permission_denied_helpers.cjs"); +const { detectNonRetryableHarnessGuard } = require("./harness_retry_guard.cjs"); // Maximum number of retry attempts after the initial run const MAX_RETRIES = 3; @@ -396,6 +397,15 @@ async function main() { break; } + const nonRetryableGuard = detectNonRetryableHarnessGuard(result.output); + if (nonRetryableGuard.aiCreditsExceeded || nonRetryableGuard.awfAPIProxyBlockingRequests) { + const reasons = []; + if (nonRetryableGuard.aiCreditsExceeded) reasons.push("AI credits budget exceeded"); + if (nonRetryableGuard.awfAPIProxyBlockingRequests) reasons.push("AWF API proxy is blocking requests"); + log(`attempt ${attempt + 1}: ${reasons.join(" and ")} — not retrying (persistent AWF guard condition)`); + break; + } + if (attempt === 0 && isAuthenticationFailed) { log(`attempt ${attempt + 1}: authentication failed — not retrying (first-attempt auth failure is non-retryable)`); break; diff --git a/actions/setup/js/codex_harness.cjs b/actions/setup/js/codex_harness.cjs index f0141485ef2..651290781ff 100644 --- a/actions/setup/js/codex_harness.cjs +++ b/actions/setup/js/codex_harness.cjs @@ -46,6 +46,7 @@ const { } = require("./awf_reflect.cjs"); const { emitMissingToolPermissionIssue, hasNoopInSafeOutputs } = require("./safeoutputs_cli.cjs"); const { countPermissionDeniedIssues, hasNumerousPermissionDeniedIssues, extractDeniedCommands, buildMissingToolPermissionIssuePayload } = require("./permission_denied_helpers.cjs"); +const { detectNonRetryableHarnessGuard } = require("./harness_retry_guard.cjs"); // Maximum number of retry attempts after the initial run const MAX_RETRIES = 3; @@ -425,6 +426,15 @@ async function main() { break; } + const nonRetryableGuard = detectNonRetryableHarnessGuard(result.output); + if (nonRetryableGuard.aiCreditsExceeded || nonRetryableGuard.awfAPIProxyBlockingRequests) { + const reasons = []; + if (nonRetryableGuard.aiCreditsExceeded) reasons.push("AI credits budget exceeded"); + if (nonRetryableGuard.awfAPIProxyBlockingRequests) reasons.push("AWF API proxy is blocking requests"); + log(`attempt ${attempt + 1}: ${reasons.join(" and ")} — not retrying (persistent AWF guard condition)`); + break; + } + if (attempt === 0 && isAuthenticationFailed) { log(`attempt ${attempt + 1}: authentication failed — not retrying (first-attempt auth failure is non-retryable)`); break; diff --git a/actions/setup/js/copilot_harness.cjs b/actions/setup/js/copilot_harness.cjs index 6df8dc4b29a..199599f5e4b 100644 --- a/actions/setup/js/copilot_harness.cjs +++ b/actions/setup/js/copilot_harness.cjs @@ -60,6 +60,7 @@ const { } = require("./awf_reflect.cjs"); const { runSafeOutputsCLI, buildMissingToolAlternatives, emitMissingToolPermissionIssue, emitInfrastructureIncomplete, hasNoopInSafeOutputs } = require("./safeoutputs_cli.cjs"); const { countPermissionDeniedIssues, hasNumerousPermissionDeniedIssues, extractDeniedCommands, buildMissingToolPermissionIssuePayload } = require("./permission_denied_helpers.cjs"); +const { detectNonRetryableHarnessGuard } = require("./harness_retry_guard.cjs"); // Maximum number of retry attempts after the initial run const MAX_RETRIES = 3; @@ -737,6 +738,15 @@ async function main() { break; } + const nonRetryableGuard = detectNonRetryableHarnessGuard(result.output); + if (nonRetryableGuard.aiCreditsExceeded || nonRetryableGuard.awfAPIProxyBlockingRequests) { + const reasons = []; + if (nonRetryableGuard.aiCreditsExceeded) reasons.push("AI credits budget exceeded"); + if (nonRetryableGuard.awfAPIProxyBlockingRequests) reasons.push("AWF API proxy is blocking requests"); + log(`attempt ${attempt + 1}: ${reasons.join(" and ")} — not retrying (persistent AWF guard condition)`); + break; + } + if (attempt === 0 && isAuthenticationFailed) { if (proxyAuthDiagnostic) { log(`attempt ${attempt + 1}: ${proxyAuthDiagnostic} — not retrying (first-attempt auth failure is non-retryable)`); diff --git a/actions/setup/js/harness_retry_guard.cjs b/actions/setup/js/harness_retry_guard.cjs new file mode 100644 index 00000000000..055043c3574 --- /dev/null +++ b/actions/setup/js/harness_retry_guard.cjs @@ -0,0 +1,37 @@ +// @ts-check + +"use strict"; + +const AI_CREDITS_EXCEEDED_PATTERNS = [ + /\bmax[\s_-]*ai[\s_-]*credits[\s_-]*exceeded\b/i, + /\bai[\s_-]*credits[\s_-]*rate[\s_-]*limit[\s_-]*error\b/i, + /ai[\s_-]*credits?.*(?:rate[\s-]*limit|limit exceeded|budget exceeded|exceeded)/i, +]; + +const AWF_API_PROXY_BLOCKING_REQUESTS_PATTERNS = [ + /\bawf\b.*\bapi[\s_-]*proxy\b.*\bblocking requests\b/i, + /\bapi[\s_-]*proxy\b.*\bblocking requests\b/i, + /\bapi[\s_-]*proxy\b.*\bblocked requests?\b/i, + /\bDIFC_FILTERED\b/, +]; + +/** + * Detect retry guard conditions that should stop harness retries immediately. + * @param {string} output + * @returns {{ aiCreditsExceeded: boolean, awfAPIProxyBlockingRequests: boolean }} + */ +function detectNonRetryableHarnessGuard(output) { + const safeOutput = typeof output === "string" ? output : ""; + return { + aiCreditsExceeded: AI_CREDITS_EXCEEDED_PATTERNS.some(pattern => pattern.test(safeOutput)), + awfAPIProxyBlockingRequests: AWF_API_PROXY_BLOCKING_REQUESTS_PATTERNS.some(pattern => pattern.test(safeOutput)), + }; +} + +if (typeof module !== "undefined" && module.exports) { + module.exports = { + detectNonRetryableHarnessGuard, + AI_CREDITS_EXCEEDED_PATTERNS, + AWF_API_PROXY_BLOCKING_REQUESTS_PATTERNS, + }; +} diff --git a/actions/setup/js/harness_retry_guard.test.cjs b/actions/setup/js/harness_retry_guard.test.cjs new file mode 100644 index 00000000000..707c3f402a7 --- /dev/null +++ b/actions/setup/js/harness_retry_guard.test.cjs @@ -0,0 +1,29 @@ +import { describe, expect, it } from "vitest"; + +const { detectNonRetryableHarnessGuard } = require("./harness_retry_guard.cjs"); + +describe("harness_retry_guard.cjs", () => { + it("detects AI credits exceeded markers", () => { + const result = detectNonRetryableHarnessGuard("error: max_ai_credits_exceeded=true"); + expect(result.aiCreditsExceeded).toBe(true); + expect(result.awfAPIProxyBlockingRequests).toBe(false); + }); + + it("detects AWF API proxy blocking request markers", () => { + const result = detectNonRetryableHarnessGuard("awf api proxy is blocking requests for this run"); + expect(result.aiCreditsExceeded).toBe(false); + expect(result.awfAPIProxyBlockingRequests).toBe(true); + }); + + it("detects DIFC filtered proxy block markers", () => { + const result = detectNonRetryableHarnessGuard('{"type":"DIFC_FILTERED","reason":"blocked"}'); + expect(result.aiCreditsExceeded).toBe(false); + expect(result.awfAPIProxyBlockingRequests).toBe(true); + }); + + it("returns false when output has no guard markers", () => { + const result = detectNonRetryableHarnessGuard("transient network timeout"); + expect(result.aiCreditsExceeded).toBe(false); + expect(result.awfAPIProxyBlockingRequests).toBe(false); + }); +}); From 9ca6fa8dca902fc85d72bac8e1db5383f64e277d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Jun 2026 04:01:39 +0000 Subject: [PATCH 2/3] Format shared harness retry guard Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/harness_retry_guard.cjs | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/actions/setup/js/harness_retry_guard.cjs b/actions/setup/js/harness_retry_guard.cjs index 055043c3574..333b34d3ec0 100644 --- a/actions/setup/js/harness_retry_guard.cjs +++ b/actions/setup/js/harness_retry_guard.cjs @@ -2,18 +2,9 @@ "use strict"; -const AI_CREDITS_EXCEEDED_PATTERNS = [ - /\bmax[\s_-]*ai[\s_-]*credits[\s_-]*exceeded\b/i, - /\bai[\s_-]*credits[\s_-]*rate[\s_-]*limit[\s_-]*error\b/i, - /ai[\s_-]*credits?.*(?:rate[\s-]*limit|limit exceeded|budget exceeded|exceeded)/i, -]; +const AI_CREDITS_EXCEEDED_PATTERNS = [/\bmax[\s_-]*ai[\s_-]*credits[\s_-]*exceeded\b/i, /\bai[\s_-]*credits[\s_-]*rate[\s_-]*limit[\s_-]*error\b/i, /ai[\s_-]*credits?.*(?:rate[\s-]*limit|limit exceeded|budget exceeded|exceeded)/i]; -const AWF_API_PROXY_BLOCKING_REQUESTS_PATTERNS = [ - /\bawf\b.*\bapi[\s_-]*proxy\b.*\bblocking requests\b/i, - /\bapi[\s_-]*proxy\b.*\bblocking requests\b/i, - /\bapi[\s_-]*proxy\b.*\bblocked requests?\b/i, - /\bDIFC_FILTERED\b/, -]; +const AWF_API_PROXY_BLOCKING_REQUESTS_PATTERNS = [/\bawf\b.*\bapi[\s_-]*proxy\b.*\bblocking requests\b/i, /\bapi[\s_-]*proxy\b.*\bblocking requests\b/i, /\bapi[\s_-]*proxy\b.*\bblocked requests?\b/i, /\bDIFC_FILTERED\b/]; /** * Detect retry guard conditions that should stop harness retries immediately. From e8c2645e4bfae7b1ed4ec67771ae834cea1f39fb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Jun 2026 05:02:36 +0000 Subject: [PATCH 3/3] Address guardrail review feedback in harness retry logic Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/claude_harness.cjs | 2 +- actions/setup/js/codex_harness.cjs | 2 +- actions/setup/js/copilot_harness.cjs | 2 +- actions/setup/js/harness_retry_guard.cjs | 2 +- actions/setup/js/harness_retry_guard.test.cjs | 40 +++++++++++++++++++ 5 files changed, 44 insertions(+), 4 deletions(-) diff --git a/actions/setup/js/claude_harness.cjs b/actions/setup/js/claude_harness.cjs index 05c84f660cc..b967ded81a1 100644 --- a/actions/setup/js/claude_harness.cjs +++ b/actions/setup/js/claude_harness.cjs @@ -402,7 +402,7 @@ async function main() { const reasons = []; if (nonRetryableGuard.aiCreditsExceeded) reasons.push("AI credits budget exceeded"); if (nonRetryableGuard.awfAPIProxyBlockingRequests) reasons.push("AWF API proxy is blocking requests"); - log(`attempt ${attempt + 1}: ${reasons.join(" and ")} — not retrying (persistent AWF guard condition)`); + log(`attempt ${attempt + 1}: ${reasons.join(" and ")} — not retrying (non-retryable guard condition)`); break; } diff --git a/actions/setup/js/codex_harness.cjs b/actions/setup/js/codex_harness.cjs index 651290781ff..b04855d865d 100644 --- a/actions/setup/js/codex_harness.cjs +++ b/actions/setup/js/codex_harness.cjs @@ -431,7 +431,7 @@ async function main() { const reasons = []; if (nonRetryableGuard.aiCreditsExceeded) reasons.push("AI credits budget exceeded"); if (nonRetryableGuard.awfAPIProxyBlockingRequests) reasons.push("AWF API proxy is blocking requests"); - log(`attempt ${attempt + 1}: ${reasons.join(" and ")} — not retrying (persistent AWF guard condition)`); + log(`attempt ${attempt + 1}: ${reasons.join(" and ")} — not retrying (non-retryable guard condition)`); break; } diff --git a/actions/setup/js/copilot_harness.cjs b/actions/setup/js/copilot_harness.cjs index 199599f5e4b..5176bbf51fa 100644 --- a/actions/setup/js/copilot_harness.cjs +++ b/actions/setup/js/copilot_harness.cjs @@ -743,7 +743,7 @@ async function main() { const reasons = []; if (nonRetryableGuard.aiCreditsExceeded) reasons.push("AI credits budget exceeded"); if (nonRetryableGuard.awfAPIProxyBlockingRequests) reasons.push("AWF API proxy is blocking requests"); - log(`attempt ${attempt + 1}: ${reasons.join(" and ")} — not retrying (persistent AWF guard condition)`); + log(`attempt ${attempt + 1}: ${reasons.join(" and ")} — not retrying (non-retryable guard condition)`); break; } diff --git a/actions/setup/js/harness_retry_guard.cjs b/actions/setup/js/harness_retry_guard.cjs index 333b34d3ec0..c641f11e198 100644 --- a/actions/setup/js/harness_retry_guard.cjs +++ b/actions/setup/js/harness_retry_guard.cjs @@ -8,7 +8,7 @@ const AWF_API_PROXY_BLOCKING_REQUESTS_PATTERNS = [/\bawf\b.*\bapi[\s_-]*proxy\b. /** * Detect retry guard conditions that should stop harness retries immediately. - * @param {string} output + * @param {unknown} output * @returns {{ aiCreditsExceeded: boolean, awfAPIProxyBlockingRequests: boolean }} */ function detectNonRetryableHarnessGuard(output) { diff --git a/actions/setup/js/harness_retry_guard.test.cjs b/actions/setup/js/harness_retry_guard.test.cjs index 707c3f402a7..cca3cc71e07 100644 --- a/actions/setup/js/harness_retry_guard.test.cjs +++ b/actions/setup/js/harness_retry_guard.test.cjs @@ -1,5 +1,9 @@ +// @ts-check + import { describe, expect, it } from "vitest"; +import { createRequire } from "node:module"; +const require = createRequire(import.meta.url); const { detectNonRetryableHarnessGuard } = require("./harness_retry_guard.cjs"); describe("harness_retry_guard.cjs", () => { @@ -9,18 +13,54 @@ describe("harness_retry_guard.cjs", () => { expect(result.awfAPIProxyBlockingRequests).toBe(false); }); + it("detects AI credits rate-limit markers", () => { + const result = detectNonRetryableHarnessGuard("error: ai_credits_rate_limit_error=true"); + expect(result.aiCreditsExceeded).toBe(true); + expect(result.awfAPIProxyBlockingRequests).toBe(false); + }); + + it("detects AI credits budget markers", () => { + const result = detectNonRetryableHarnessGuard("error: ai credits budget exceeded"); + expect(result.aiCreditsExceeded).toBe(true); + expect(result.awfAPIProxyBlockingRequests).toBe(false); + }); + it("detects AWF API proxy blocking request markers", () => { const result = detectNonRetryableHarnessGuard("awf api proxy is blocking requests for this run"); expect(result.aiCreditsExceeded).toBe(false); expect(result.awfAPIProxyBlockingRequests).toBe(true); }); + it("detects API proxy blocking request markers without AWF prefix", () => { + const result = detectNonRetryableHarnessGuard("api-proxy is blocking requests"); + expect(result.aiCreditsExceeded).toBe(false); + expect(result.awfAPIProxyBlockingRequests).toBe(true); + }); + + it("detects API proxy blocked request markers", () => { + const result = detectNonRetryableHarnessGuard("api proxy blocked request"); + expect(result.aiCreditsExceeded).toBe(false); + expect(result.awfAPIProxyBlockingRequests).toBe(true); + }); + it("detects DIFC filtered proxy block markers", () => { const result = detectNonRetryableHarnessGuard('{"type":"DIFC_FILTERED","reason":"blocked"}'); expect(result.aiCreditsExceeded).toBe(false); expect(result.awfAPIProxyBlockingRequests).toBe(true); }); + it("returns false for non-string input", () => { + const result = detectNonRetryableHarnessGuard(null); + expect(result.aiCreditsExceeded).toBe(false); + expect(result.awfAPIProxyBlockingRequests).toBe(false); + }); + + it("detects both flags when output contains both signals", () => { + const result = detectNonRetryableHarnessGuard("max_ai_credits_exceeded=true DIFC_FILTERED"); + expect(result.aiCreditsExceeded).toBe(true); + expect(result.awfAPIProxyBlockingRequests).toBe(true); + }); + it("returns false when output has no guard markers", () => { const result = detectNonRetryableHarnessGuard("transient network timeout"); expect(result.aiCreditsExceeded).toBe(false);