diff --git a/actions/setup/js/codex_harness.cjs b/actions/setup/js/codex_harness.cjs index a88d4026525..937fbd3ff91 100644 --- a/actions/setup/js/codex_harness.cjs +++ b/actions/setup/js/codex_harness.cjs @@ -439,10 +439,11 @@ async function main() { } const nonRetryableGuard = detectNonRetryableHarnessGuard(result.output); - if (nonRetryableGuard.aiCreditsExceeded || nonRetryableGuard.awfAPIProxyBlockingRequests) { + if (nonRetryableGuard.aiCreditsExceeded || nonRetryableGuard.awfAPIProxyBlockingRequests || nonRetryableGuard.goalAlreadyActive) { const reasons = []; if (nonRetryableGuard.aiCreditsExceeded) reasons.push("AI credits budget exceeded"); if (nonRetryableGuard.awfAPIProxyBlockingRequests) reasons.push("AWF API proxy is blocking requests"); + if (nonRetryableGuard.goalAlreadyActive) reasons.push("goal is already active for this thread (use update_goal when the current goal is complete)"); log(`attempt ${attempt + 1}: ${reasons.join(" and ")} — not retrying (non-retryable guard condition)`); break; } diff --git a/actions/setup/js/codex_harness.test.cjs b/actions/setup/js/codex_harness.test.cjs index 1b01eb0e7b4..8a00a49e4e4 100644 --- a/actions/setup/js/codex_harness.test.cjs +++ b/actions/setup/js/codex_harness.test.cjs @@ -25,6 +25,7 @@ const { validateCodexOpenAIBaseURLFromReflect, hasNoopInSafeOutputs, } = require("./codex_harness.cjs"); +const { detectNonRetryableHarnessGuard } = require("./harness_retry_guard.cjs"); const agentTempDir = "/tmp/gh-aw/agent"; @@ -412,6 +413,8 @@ env_key = "OPENAI_API_KEY" if (attempt === 0 && isAuthenticationFailedError(result.output)) return false; if (isMissingApiKeyError(result.output)) return false; if (hasNumerousPermissionDeniedIssues(result.output)) return false; + const nonRetryableGuard = detectNonRetryableHarnessGuard(result.output); + if (nonRetryableGuard.aiCreditsExceeded || nonRetryableGuard.awfAPIProxyBlockingRequests || nonRetryableGuard.goalAlreadyActive) return false; const isTransient = RATE_LIMIT_ERROR_PATTERN.test(result.output) || SERVER_ERROR_PATTERN.test(result.output); return attempt < MAX_RETRIES && (result.hasOutput || isTransient); } @@ -461,6 +464,15 @@ env_key = "OPENAI_API_KEY" const result = { exitCode: 1, hasOutput: true, output: "permission denied\npermission denied\npermission denied" }; expect(shouldRetry(result, 0)).toBe(false); }); + + it("does not retry when codex reports an existing active goal", () => { + const result = { + exitCode: 1, + hasOutput: true, + output: "cannot create a new goal because this thread already has a goal; use update_goal only when the existing goal is complete", + }; + expect(shouldRetry(result, 0)).toBe(false); + }); }); describe("noop pre-flight and retry guard", () => { diff --git a/actions/setup/js/harness_retry_guard.cjs b/actions/setup/js/harness_retry_guard.cjs index c641f11e198..042c76be2aa 100644 --- a/actions/setup/js/harness_retry_guard.cjs +++ b/actions/setup/js/harness_retry_guard.cjs @@ -5,17 +5,19 @@ 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 GOAL_ALREADY_ACTIVE_PATTERNS = [/\bthis thread already has a goal\b[\s\S]*?\buse update_goal\b/i]; /** * Detect retry guard conditions that should stop harness retries immediately. * @param {unknown} output - * @returns {{ aiCreditsExceeded: boolean, awfAPIProxyBlockingRequests: boolean }} + * @returns {{ aiCreditsExceeded: boolean, awfAPIProxyBlockingRequests: boolean, goalAlreadyActive: 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)), + goalAlreadyActive: GOAL_ALREADY_ACTIVE_PATTERNS.some(pattern => pattern.test(safeOutput)), }; } @@ -24,5 +26,6 @@ if (typeof module !== "undefined" && module.exports) { detectNonRetryableHarnessGuard, AI_CREDITS_EXCEEDED_PATTERNS, AWF_API_PROXY_BLOCKING_REQUESTS_PATTERNS, + GOAL_ALREADY_ACTIVE_PATTERNS, }; } diff --git a/actions/setup/js/harness_retry_guard.test.cjs b/actions/setup/js/harness_retry_guard.test.cjs index cca3cc71e07..37fd528acb0 100644 --- a/actions/setup/js/harness_retry_guard.test.cjs +++ b/actions/setup/js/harness_retry_guard.test.cjs @@ -53,17 +53,42 @@ describe("harness_retry_guard.cjs", () => { const result = detectNonRetryableHarnessGuard(null); expect(result.aiCreditsExceeded).toBe(false); expect(result.awfAPIProxyBlockingRequests).toBe(false); + expect(result.goalAlreadyActive).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); + expect(result.goalAlreadyActive).toBe(false); }); 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); + expect(result.goalAlreadyActive).toBe(false); + }); + + it("detects goal already active markers", () => { + const result = detectNonRetryableHarnessGuard("cannot create a new goal because this thread already has a goal; use update_goal only when the existing goal is complete"); + expect(result.aiCreditsExceeded).toBe(false); + expect(result.awfAPIProxyBlockingRequests).toBe(false); + expect(result.goalAlreadyActive).toBe(true); + }); + + it("detects goal already active markers across newlines", () => { + const result = detectNonRetryableHarnessGuard("this thread already has a goal\nuse update_goal to update it"); + expect(result.goalAlreadyActive).toBe(true); + }); + + it("does not detect goal active from first phrase alone", () => { + const result = detectNonRetryableHarnessGuard("this thread already has a goal"); + expect(result.goalAlreadyActive).toBe(false); + }); + + it("detects goal already active when embedded in longer output", () => { + const result = detectNonRetryableHarnessGuard("[codex] cannot create a new goal because this thread already has a goal; use update_goal only when the existing goal is complete\nExit code: 1"); + expect(result.goalAlreadyActive).toBe(true); }); });