diff --git a/actions/setup/js/claude_harness.cjs b/actions/setup/js/claude_harness.cjs index 11514a2d866..b967ded81a1 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 (non-retryable 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..b04855d865d 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 (non-retryable 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..5176bbf51fa 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 (non-retryable 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..c641f11e198 --- /dev/null +++ b/actions/setup/js/harness_retry_guard.cjs @@ -0,0 +1,28 @@ +// @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 {unknown} 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..cca3cc71e07 --- /dev/null +++ b/actions/setup/js/harness_retry_guard.test.cjs @@ -0,0 +1,69 @@ +// @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", () => { + 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 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); + expect(result.awfAPIProxyBlockingRequests).toBe(false); + }); +});