From 46dcdb92ac97bd8d4980347ae7e1bfb3f2b30188 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 13 Jun 2026 22:02:38 +0000 Subject: [PATCH 1/5] Initial plan From 9a18c3ddfa9fb5c6dccc883c5a84b4d4ddd5bc4b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 13 Jun 2026 22:15:19 +0000 Subject: [PATCH 2/5] fix: stop codex retries when thread already has active goal Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/codex_harness.cjs | 3 ++- actions/setup/js/codex_harness.test.cjs | 11 +++++++++++ actions/setup/js/harness_retry_guard.cjs | 5 ++++- actions/setup/js/harness_retry_guard.test.cjs | 8 ++++++++ 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/actions/setup/js/codex_harness.cjs b/actions/setup/js/codex_harness.cjs index a88d4026525..4b109a198a3 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"); 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..a63058b86f4 100644 --- a/actions/setup/js/codex_harness.test.cjs +++ b/actions/setup/js/codex_harness.test.cjs @@ -409,9 +409,11 @@ env_key = "OPENAI_API_KEY" if (result.exitCode === 0) return false; const RATE_LIMIT_ERROR_PATTERN = /rate_limit_exceeded|429 Too Many Requests|RateLimitError/i; const SERVER_ERROR_PATTERN = /InternalServerError|ServiceUnavailableError|500 Internal Server Error|503 Service Unavailable/i; + const GOAL_ALREADY_ACTIVE_PATTERN = /cannot create a new goal because this thread already has a goal|this thread already has a goal|use update_goal only when the existing goal is complete/i; if (attempt === 0 && isAuthenticationFailedError(result.output)) return false; if (isMissingApiKeyError(result.output)) return false; if (hasNumerousPermissionDeniedIssues(result.output)) return false; + if (GOAL_ALREADY_ACTIVE_PATTERN.test(result.output)) 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 +463,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..ab2be227920 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 = [/\bcannot create a new goal because this thread already has a goal\b/i, /\bthis thread already has a goal\b/i, /\buse update_goal only when the existing goal is complete\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..1677fedf80f 100644 --- a/actions/setup/js/harness_retry_guard.test.cjs +++ b/actions/setup/js/harness_retry_guard.test.cjs @@ -65,5 +65,13 @@ describe("harness_retry_guard.cjs", () => { 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); }); }); From b6c554f0a32464ce96cda864f2b564894c223119 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 13 Jun 2026 22:22:55 +0000 Subject: [PATCH 3/5] test: guard codex active-goal error from retry loop Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/codex_harness.test.cjs | 4 ++-- actions/setup/js/harness_retry_guard.cjs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/actions/setup/js/codex_harness.test.cjs b/actions/setup/js/codex_harness.test.cjs index a63058b86f4..bc568e55d80 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"; @@ -409,11 +410,10 @@ env_key = "OPENAI_API_KEY" if (result.exitCode === 0) return false; const RATE_LIMIT_ERROR_PATTERN = /rate_limit_exceeded|429 Too Many Requests|RateLimitError/i; const SERVER_ERROR_PATTERN = /InternalServerError|ServiceUnavailableError|500 Internal Server Error|503 Service Unavailable/i; - const GOAL_ALREADY_ACTIVE_PATTERN = /cannot create a new goal because this thread already has a goal|this thread already has a goal|use update_goal only when the existing goal is complete/i; if (attempt === 0 && isAuthenticationFailedError(result.output)) return false; if (isMissingApiKeyError(result.output)) return false; if (hasNumerousPermissionDeniedIssues(result.output)) return false; - if (GOAL_ALREADY_ACTIVE_PATTERN.test(result.output)) return false; + if (detectNonRetryableHarnessGuard(result.output).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); } diff --git a/actions/setup/js/harness_retry_guard.cjs b/actions/setup/js/harness_retry_guard.cjs index ab2be227920..bd029eb2242 100644 --- a/actions/setup/js/harness_retry_guard.cjs +++ b/actions/setup/js/harness_retry_guard.cjs @@ -5,7 +5,7 @@ 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 = [/\bcannot create a new goal because this thread already has a goal\b/i, /\bthis thread already has a goal\b/i, /\buse update_goal only when the existing goal is complete\b/i]; +const GOAL_ALREADY_ACTIVE_PATTERNS = [/\bthis thread already has a goal\b.*\buse update_goal only when the existing goal is complete\b/i]; /** * Detect retry guard conditions that should stop harness retries immediately. From 8e32314296075d5b37dd0105f4b3362b4572e1e5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 13 Jun 2026 23:10:31 +0000 Subject: [PATCH 4/5] fix: robust multiline goal-already-active pattern and actionable log hint Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/codex_harness.cjs | 2 +- actions/setup/js/harness_retry_guard.cjs | 2 +- actions/setup/js/harness_retry_guard.test.cjs | 5 +++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/actions/setup/js/codex_harness.cjs b/actions/setup/js/codex_harness.cjs index 4b109a198a3..937fbd3ff91 100644 --- a/actions/setup/js/codex_harness.cjs +++ b/actions/setup/js/codex_harness.cjs @@ -443,7 +443,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"); - if (nonRetryableGuard.goalAlreadyActive) reasons.push("goal is already active for this thread"); + 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/harness_retry_guard.cjs b/actions/setup/js/harness_retry_guard.cjs index bd029eb2242..042c76be2aa 100644 --- a/actions/setup/js/harness_retry_guard.cjs +++ b/actions/setup/js/harness_retry_guard.cjs @@ -5,7 +5,7 @@ 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.*\buse update_goal only when the existing goal is complete\b/i]; +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. diff --git a/actions/setup/js/harness_retry_guard.test.cjs b/actions/setup/js/harness_retry_guard.test.cjs index 1677fedf80f..39a8b70995d 100644 --- a/actions/setup/js/harness_retry_guard.test.cjs +++ b/actions/setup/js/harness_retry_guard.test.cjs @@ -74,4 +74,9 @@ describe("harness_retry_guard.cjs", () => { 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); + }); }); From 8f77cb11d3663e1b787340e57f34c522c4a988e5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 13 Jun 2026 23:12:13 +0000 Subject: [PATCH 5/5] test: align mock guard, add negative and completeness test cases Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/codex_harness.test.cjs | 3 ++- actions/setup/js/harness_retry_guard.test.cjs | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/actions/setup/js/codex_harness.test.cjs b/actions/setup/js/codex_harness.test.cjs index bc568e55d80..8a00a49e4e4 100644 --- a/actions/setup/js/codex_harness.test.cjs +++ b/actions/setup/js/codex_harness.test.cjs @@ -413,7 +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; - if (detectNonRetryableHarnessGuard(result.output).goalAlreadyActive) 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); } diff --git a/actions/setup/js/harness_retry_guard.test.cjs b/actions/setup/js/harness_retry_guard.test.cjs index 39a8b70995d..37fd528acb0 100644 --- a/actions/setup/js/harness_retry_guard.test.cjs +++ b/actions/setup/js/harness_retry_guard.test.cjs @@ -53,12 +53,14 @@ 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", () => { @@ -79,4 +81,14 @@ describe("harness_retry_guard.cjs", () => { 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); + }); });