From 4cc11d8c1d042b742f89ff85c611f88f98b689f9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 1 May 2026 20:20:40 +0000 Subject: [PATCH 1/2] Initial plan From b732bfe042c62fa6683adf9c1433760e220cc6a8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 1 May 2026 20:33:07 +0000 Subject: [PATCH 2/2] fix: skip --continue retries for error_max_turns exits in claude_harness.cjs Agent-Logs-Url: https://github.com/github/gh-aw/sessions/1198d3d6-9840-4440-8025-56c6f8d2b0e5 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/claude_harness.cjs | 39 +++++++++++++++++++++++- actions/setup/js/claude_harness.test.cjs | 29 +++++++++++++++++- 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/actions/setup/js/claude_harness.cjs b/actions/setup/js/claude_harness.cjs index b5bcfa256fe..f0b65fe269d 100644 --- a/actions/setup/js/claude_harness.cjs +++ b/actions/setup/js/claude_harness.cjs @@ -63,6 +63,12 @@ const OVERLOADED_ERROR_PATTERN = /overloaded_error|"overloaded"/i; // Pattern to detect Anthropic rate-limit errors (HTTP 429). const RATE_LIMIT_ERROR_PATTERN = /rate_limit_error|429 Too Many Requests/i; +// Pattern to detect a clean max-turns exit from Claude Code. +// Claude Code emits a JSON result object with "subtype":"error_max_turns" when the +// session ends because the turn limit was reached. This is a deterministic terminal +// condition — --continue cannot recover it because no deferred tool marker was written. +const MAX_TURNS_EXIT_PATTERN = /"subtype"\s*:\s*"error_max_turns"/; + /** * Emit a timestamped diagnostic log line to stderr. * All driver messages are prefixed with "[claude-harness]" so they are easy to @@ -92,6 +98,18 @@ function isRateLimitError(output) { return RATE_LIMIT_ERROR_PATTERN.test(output); } +/** + * Determines if the collected output signals a clean max-turns exit. + * When Claude Code hits its turn limit it emits a result object with + * "subtype":"error_max_turns". This is not a transient error — retrying + * with --continue will always fail because no deferred tool marker was written. + * @param {string} output - Collected stdout+stderr from the process + * @returns {boolean} + */ +function isMaxTurnsExit(output) { + return MAX_TURNS_EXIT_PATTERN.test(output); +} + /** * Sleep for a specified duration * @param {number} ms - Duration in milliseconds @@ -341,7 +359,25 @@ async function main() { const isOverloaded = isOverloadedError(result.output); const isRateLimit = isRateLimitError(result.output); - log(`attempt ${attempt + 1} failed:` + ` exitCode=${result.exitCode}` + ` isOverloadedError=${isOverloaded}` + ` isRateLimitError=${isRateLimit}` + ` hasOutput=${result.hasOutput}` + ` retriesRemaining=${MAX_RETRIES - attempt}`); + const isMaxTurns = isMaxTurnsExit(result.output); + log( + `attempt ${attempt + 1} failed:` + + ` exitCode=${result.exitCode}` + + ` isOverloadedError=${isOverloaded}` + + ` isRateLimitError=${isRateLimit}` + + ` isMaxTurnsExit=${isMaxTurns}` + + ` hasOutput=${result.hasOutput}` + + ` retriesRemaining=${MAX_RETRIES - attempt}` + ); + + // max_turns is a deterministic terminal condition: the session ended cleanly after + // exhausting the allowed number of turns. --continue cannot resume it because no + // deferred tool marker was written. Retrying would immediately fail with "No deferred + // tool marker found", wasting time and masking the real exit reason. + if (isMaxTurns) { + log(`attempt ${attempt + 1}: max_turns exit — not retriable via --continue`); + break; + } // Retry when the session was partially executed (has output). // Use --continue so Claude Code can resume from its saved session state. @@ -372,6 +408,7 @@ if (typeof module !== "undefined" && module.exports) { module.exports = { resolveClaudePromptFileArgs, stripPromptFileArgs, + isMaxTurnsExit, }; } diff --git a/actions/setup/js/claude_harness.test.cjs b/actions/setup/js/claude_harness.test.cjs index c551f02a1c1..1c07db71be6 100644 --- a/actions/setup/js/claude_harness.test.cjs +++ b/actions/setup/js/claude_harness.test.cjs @@ -5,7 +5,7 @@ import os from "os"; import path from "path"; const require = createRequire(import.meta.url); -const { resolveClaudePromptFileArgs, stripPromptFileArgs } = require("./claude_harness.cjs"); +const { resolveClaudePromptFileArgs, stripPromptFileArgs, isMaxTurnsExit } = require("./claude_harness.cjs"); describe("claude_harness.cjs", () => { describe("resolveClaudePromptFileArgs", () => { @@ -80,4 +80,31 @@ describe("claude_harness.cjs", () => { expect(result).toEqual(["--print"]); }); }); + + describe("isMaxTurnsExit", () => { + it('returns true for a JSON result with "subtype":"error_max_turns"', () => { + const output = '{"type":"result","subtype":"error_max_turns","is_error":true,"num_turns":13,' + '"terminal_reason":"max_turns","errors":["Reached maximum number of turns (12)"]}'; + expect(isMaxTurnsExit(output)).toBe(true); + }); + + it("returns true when subtype has extra whitespace around the colon", () => { + expect(isMaxTurnsExit('"subtype" : "error_max_turns"')).toBe(true); + }); + + it("returns false for an overloaded_error output", () => { + expect(isMaxTurnsExit('{"type":"error","error":{"type":"overloaded_error","message":"Overloaded"}}')).toBe(false); + }); + + it("returns false for a rate_limit_error output", () => { + expect(isMaxTurnsExit('{"type":"error","error":{"type":"rate_limit_error","message":"429 Too Many Requests"}}')).toBe(false); + }); + + it("returns false for an empty string", () => { + expect(isMaxTurnsExit("")).toBe(false); + }); + + it("returns false for a successful result output", () => { + expect(isMaxTurnsExit('{"type":"result","subtype":"success","is_error":false}')).toBe(false); + }); + }); });