Skip to content

Commit c390592

Browse files
Copilotpelikhangithub-actions[bot]
authored
Stop Codex harness retries on active-goal router failures (#39156)
* Initial plan * fix: stop codex retries when thread already has active goal Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> * test: guard codex active-goal error from retry loop Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> * fix: robust multiline goal-already-active pattern and actionable log hint Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> * test: align mock guard, add negative and completeness test cases Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Peli de Halleux <pelikhan@users.noreply.github.com>
1 parent 4b5836e commit c390592

4 files changed

Lines changed: 43 additions & 2 deletions

File tree

actions/setup/js/codex_harness.cjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -439,10 +439,11 @@ async function main() {
439439
}
440440

441441
const nonRetryableGuard = detectNonRetryableHarnessGuard(result.output);
442-
if (nonRetryableGuard.aiCreditsExceeded || nonRetryableGuard.awfAPIProxyBlockingRequests) {
442+
if (nonRetryableGuard.aiCreditsExceeded || nonRetryableGuard.awfAPIProxyBlockingRequests || nonRetryableGuard.goalAlreadyActive) {
443443
const reasons = [];
444444
if (nonRetryableGuard.aiCreditsExceeded) reasons.push("AI credits budget exceeded");
445445
if (nonRetryableGuard.awfAPIProxyBlockingRequests) reasons.push("AWF API proxy is blocking requests");
446+
if (nonRetryableGuard.goalAlreadyActive) reasons.push("goal is already active for this thread (use update_goal when the current goal is complete)");
446447
log(`attempt ${attempt + 1}: ${reasons.join(" and ")} — not retrying (non-retryable guard condition)`);
447448
break;
448449
}

actions/setup/js/codex_harness.test.cjs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const {
2525
validateCodexOpenAIBaseURLFromReflect,
2626
hasNoopInSafeOutputs,
2727
} = require("./codex_harness.cjs");
28+
const { detectNonRetryableHarnessGuard } = require("./harness_retry_guard.cjs");
2829

2930
const agentTempDir = "/tmp/gh-aw/agent";
3031

@@ -412,6 +413,8 @@ env_key = "OPENAI_API_KEY"
412413
if (attempt === 0 && isAuthenticationFailedError(result.output)) return false;
413414
if (isMissingApiKeyError(result.output)) return false;
414415
if (hasNumerousPermissionDeniedIssues(result.output)) return false;
416+
const nonRetryableGuard = detectNonRetryableHarnessGuard(result.output);
417+
if (nonRetryableGuard.aiCreditsExceeded || nonRetryableGuard.awfAPIProxyBlockingRequests || nonRetryableGuard.goalAlreadyActive) return false;
415418
const isTransient = RATE_LIMIT_ERROR_PATTERN.test(result.output) || SERVER_ERROR_PATTERN.test(result.output);
416419
return attempt < MAX_RETRIES && (result.hasOutput || isTransient);
417420
}
@@ -461,6 +464,15 @@ env_key = "OPENAI_API_KEY"
461464
const result = { exitCode: 1, hasOutput: true, output: "permission denied\npermission denied\npermission denied" };
462465
expect(shouldRetry(result, 0)).toBe(false);
463466
});
467+
468+
it("does not retry when codex reports an existing active goal", () => {
469+
const result = {
470+
exitCode: 1,
471+
hasOutput: true,
472+
output: "cannot create a new goal because this thread already has a goal; use update_goal only when the existing goal is complete",
473+
};
474+
expect(shouldRetry(result, 0)).toBe(false);
475+
});
464476
});
465477

466478
describe("noop pre-flight and retry guard", () => {

actions/setup/js/harness_retry_guard.cjs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,19 @@
55
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];
66

77
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/];
8+
const GOAL_ALREADY_ACTIVE_PATTERNS = [/\bthis thread already has a goal\b[\s\S]*?\buse update_goal\b/i];
89

910
/**
1011
* Detect retry guard conditions that should stop harness retries immediately.
1112
* @param {unknown} output
12-
* @returns {{ aiCreditsExceeded: boolean, awfAPIProxyBlockingRequests: boolean }}
13+
* @returns {{ aiCreditsExceeded: boolean, awfAPIProxyBlockingRequests: boolean, goalAlreadyActive: boolean }}
1314
*/
1415
function detectNonRetryableHarnessGuard(output) {
1516
const safeOutput = typeof output === "string" ? output : "";
1617
return {
1718
aiCreditsExceeded: AI_CREDITS_EXCEEDED_PATTERNS.some(pattern => pattern.test(safeOutput)),
1819
awfAPIProxyBlockingRequests: AWF_API_PROXY_BLOCKING_REQUESTS_PATTERNS.some(pattern => pattern.test(safeOutput)),
20+
goalAlreadyActive: GOAL_ALREADY_ACTIVE_PATTERNS.some(pattern => pattern.test(safeOutput)),
1921
};
2022
}
2123

@@ -24,5 +26,6 @@ if (typeof module !== "undefined" && module.exports) {
2426
detectNonRetryableHarnessGuard,
2527
AI_CREDITS_EXCEEDED_PATTERNS,
2628
AWF_API_PROXY_BLOCKING_REQUESTS_PATTERNS,
29+
GOAL_ALREADY_ACTIVE_PATTERNS,
2730
};
2831
}

actions/setup/js/harness_retry_guard.test.cjs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,17 +53,42 @@ describe("harness_retry_guard.cjs", () => {
5353
const result = detectNonRetryableHarnessGuard(null);
5454
expect(result.aiCreditsExceeded).toBe(false);
5555
expect(result.awfAPIProxyBlockingRequests).toBe(false);
56+
expect(result.goalAlreadyActive).toBe(false);
5657
});
5758

5859
it("detects both flags when output contains both signals", () => {
5960
const result = detectNonRetryableHarnessGuard("max_ai_credits_exceeded=true DIFC_FILTERED");
6061
expect(result.aiCreditsExceeded).toBe(true);
6162
expect(result.awfAPIProxyBlockingRequests).toBe(true);
63+
expect(result.goalAlreadyActive).toBe(false);
6264
});
6365

6466
it("returns false when output has no guard markers", () => {
6567
const result = detectNonRetryableHarnessGuard("transient network timeout");
6668
expect(result.aiCreditsExceeded).toBe(false);
6769
expect(result.awfAPIProxyBlockingRequests).toBe(false);
70+
expect(result.goalAlreadyActive).toBe(false);
71+
});
72+
73+
it("detects goal already active markers", () => {
74+
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");
75+
expect(result.aiCreditsExceeded).toBe(false);
76+
expect(result.awfAPIProxyBlockingRequests).toBe(false);
77+
expect(result.goalAlreadyActive).toBe(true);
78+
});
79+
80+
it("detects goal already active markers across newlines", () => {
81+
const result = detectNonRetryableHarnessGuard("this thread already has a goal\nuse update_goal to update it");
82+
expect(result.goalAlreadyActive).toBe(true);
83+
});
84+
85+
it("does not detect goal active from first phrase alone", () => {
86+
const result = detectNonRetryableHarnessGuard("this thread already has a goal");
87+
expect(result.goalAlreadyActive).toBe(false);
88+
});
89+
90+
it("detects goal already active when embedded in longer output", () => {
91+
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");
92+
expect(result.goalAlreadyActive).toBe(true);
6893
});
6994
});

0 commit comments

Comments
 (0)