From d0595aa19ef7813575bfcd2886f5dc9b1bb61422 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Mar 2026 13:00:16 +0000 Subject: [PATCH 1/2] Initial plan From ef224759335bc4ff63f9e0580753813d7311a244 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Mar 2026 13:22:24 +0000 Subject: [PATCH 2/2] fix: include full error message in agent failure issue comments Expand buildEngineFailureContext() to capture more error patterns (Error:, Fatal:, FATAL:, panic:) and fall back to the last 10 non-empty lines of agent-stdio.log when no specific pattern matches. This ensures that agent failures caused by step timeouts or unexpected terminations always include useful context in the failure issue comment, rather than just 'Agent job [run_id](run_url) failed.' Also exports buildEngineFailureContext so it can be unit-tested, and adds comprehensive tests for all patterns. Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> Agent-Logs-Url: https://github.com/github/gh-aw/sessions/1bc80351-f450-45a5-a967-a395c489db7e --- actions/setup/js/handle_agent_failure.cjs | 57 ++++++-- .../setup/js/handle_agent_failure.test.cjs | 133 ++++++++++++++++++ 2 files changed, 180 insertions(+), 10 deletions(-) diff --git a/actions/setup/js/handle_agent_failure.cjs b/actions/setup/js/handle_agent_failure.cjs index 2933768f67e..a0f33d61180 100644 --- a/actions/setup/js/handle_agent_failure.cjs +++ b/actions/setup/js/handle_agent_failure.cjs @@ -688,8 +688,10 @@ function buildAssignCopilotFailureContext(hasAssignCopilotFailures, assignCopilo } /** - * Extract terminal error messages from agent-stdio.log to surface engine failures - * when agent_output.json was not written (e.g. quota exceeded, auth failure). + * Extract terminal error messages from agent-stdio.log to surface engine failures. + * First tries to match known error patterns (ERROR:, Error:, Fatal:, panic:, Reconnecting...). + * Falls back to the last non-empty lines of the log when no patterns match, so that + * even timeout or unexpected-termination failures include the final agent output. * The log file is available in the conclusion job after the agent artifact is downloaded. * @returns {string} Formatted context string, or empty string if no engine failure found */ @@ -720,6 +722,27 @@ function buildEngineFailureContext() { continue; } + // Node.js / generic: "Error: " at the start of a line + const errorCapMatch = line.match(/^Error:\s*(.+)$/); + if (errorCapMatch) { + errorMessages.add(errorCapMatch[1].trim()); + continue; + } + + // Fatal errors: "Fatal: " or "FATAL: " + const fatalMatch = line.match(/^(?:FATAL|Fatal):\s*(.+)$/); + if (fatalMatch) { + errorMessages.add(fatalMatch[1].trim()); + continue; + } + + // Go runtime panic: "panic: " + const panicMatch = line.match(/^panic:\s*(.+)$/); + if (panicMatch) { + errorMessages.add(panicMatch[1].trim()); + continue; + } + // Reconnect-style lines that embed the error reason: "Reconnecting... N/M (reason)" const reconnectMatch = line.match(/^Reconnecting\.\.\.\s+\d+\/\d+\s*\((.+)\)$/); if (reconnectMatch) { @@ -727,17 +750,31 @@ function buildEngineFailureContext() { } } - if (errorMessages.size === 0) { + if (errorMessages.size > 0) { + core.info(`Found ${errorMessages.size} engine error message(s) in agent-stdio.log`); + + let context = "\n**⚠️ Engine Failure**: The AI engine terminated before producing output.\n\n**Error details:**\n"; + for (const message of errorMessages) { + context += `- ${message}\n`; + } + context += "\n"; + return context; + } + + // Fallback: no known error patterns found — include the last non-empty lines so that + // failures caused by timeouts or unexpected terminations still surface useful context. + const TAIL_LINES = 10; + const nonEmptyLines = lines.filter(l => l.trim()); + if (nonEmptyLines.length === 0) { return ""; } - core.info(`Found ${errorMessages.size} engine error message(s) in agent-stdio.log`); + const tailLines = nonEmptyLines.slice(-TAIL_LINES); + core.info(`No specific error patterns found; including last ${tailLines.length} line(s) of agent-stdio.log as fallback`); - let context = "\n**⚠️ Engine Failure**: The AI engine terminated before producing output.\n\n**Error details:**\n"; - for (const message of errorMessages) { - context += `- ${message}\n`; - } - context += "\n"; + let context = "\n**⚠️ Engine Failure**: The AI engine terminated unexpectedly.\n\n**Last agent output:**\n```\n"; + context += tailLines.join("\n"); + context += "\n```\n\n"; return context; } catch (error) { core.info(`Failed to read agent-stdio.log for engine failure context: ${getErrorMessage(error)}`); @@ -1272,4 +1309,4 @@ async function main() { } } -module.exports = { main, buildCodePushFailureContext, buildPushRepoMemoryFailureContext, buildAppTokenMintingFailedContext, buildLockdownCheckFailedContext, buildTimeoutContext, buildAssignCopilotFailureContext }; +module.exports = { main, buildCodePushFailureContext, buildPushRepoMemoryFailureContext, buildAppTokenMintingFailedContext, buildLockdownCheckFailedContext, buildTimeoutContext, buildAssignCopilotFailureContext, buildEngineFailureContext }; diff --git a/actions/setup/js/handle_agent_failure.test.cjs b/actions/setup/js/handle_agent_failure.test.cjs index 9d241395045..e964f9980f0 100644 --- a/actions/setup/js/handle_agent_failure.test.cjs +++ b/actions/setup/js/handle_agent_failure.test.cjs @@ -443,4 +443,137 @@ describe("handle_agent_failure", () => { expect(result).toContain("55"); }); }); + + // ────────────────────────────────────────────────────── + // buildEngineFailureContext + // ────────────────────────────────────────────────────── + + describe("buildEngineFailureContext", () => { + let buildEngineFailureContext; + const fs = require("fs"); + const path = require("path"); + const os = require("os"); + + /** @type {string} */ + let tmpDir; + /** @type {string} */ + let stdioLogPath; + + beforeEach(() => { + vi.resetModules(); + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "aw-test-")); + stdioLogPath = path.join(tmpDir, "agent-stdio.log"); + process.env.GH_AW_AGENT_OUTPUT = path.join(tmpDir, "agent_output.json"); + ({ buildEngineFailureContext } = require("./handle_agent_failure.cjs")); + }); + + afterEach(() => { + delete process.env.GH_AW_AGENT_OUTPUT; + // Clean up temp dir + if (fs.existsSync(tmpDir)) { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); + + it("returns empty string when log file does not exist", () => { + // stdioLogPath not written — file does not exist + expect(buildEngineFailureContext()).toBe(""); + }); + + it("returns empty string when log file is empty", () => { + fs.writeFileSync(stdioLogPath, ""); + expect(buildEngineFailureContext()).toBe(""); + }); + + it("returns empty string when log file contains only whitespace", () => { + fs.writeFileSync(stdioLogPath, " \n\n "); + expect(buildEngineFailureContext()).toBe(""); + }); + + it("detects ERROR: prefix pattern (Codex/generic CLI)", () => { + fs.writeFileSync(stdioLogPath, "ERROR: quota exceeded\n"); + const result = buildEngineFailureContext(); + expect(result).toContain("Engine Failure"); + expect(result).toContain("quota exceeded"); + expect(result).toContain("Error details:"); + }); + + it("detects Error: prefix pattern (Node.js style)", () => { + fs.writeFileSync(stdioLogPath, "Error: connect ECONNREFUSED 127.0.0.1:8080\n"); + const result = buildEngineFailureContext(); + expect(result).toContain("Engine Failure"); + expect(result).toContain("connect ECONNREFUSED 127.0.0.1:8080"); + }); + + it("detects Fatal: prefix pattern", () => { + fs.writeFileSync(stdioLogPath, "Fatal: out of memory\n"); + const result = buildEngineFailureContext(); + expect(result).toContain("Engine Failure"); + expect(result).toContain("out of memory"); + }); + + it("detects FATAL: prefix pattern", () => { + fs.writeFileSync(stdioLogPath, "FATAL: unexpected shutdown\n"); + const result = buildEngineFailureContext(); + expect(result).toContain("Engine Failure"); + expect(result).toContain("unexpected shutdown"); + }); + + it("detects panic: prefix pattern (Go runtime)", () => { + fs.writeFileSync(stdioLogPath, "panic: runtime error: index out of range\n"); + const result = buildEngineFailureContext(); + expect(result).toContain("Engine Failure"); + expect(result).toContain("runtime error: index out of range"); + }); + + it("detects Reconnecting pattern", () => { + fs.writeFileSync(stdioLogPath, "Reconnecting... 1/3 (connection lost)\n"); + const result = buildEngineFailureContext(); + expect(result).toContain("Engine Failure"); + expect(result).toContain("connection lost"); + }); + + it("deduplicates repeated error messages", () => { + fs.writeFileSync(stdioLogPath, "ERROR: quota exceeded\nERROR: quota exceeded\nERROR: quota exceeded\n"); + const result = buildEngineFailureContext(); + const count = (result.match(/quota exceeded/g) || []).length; + expect(count).toBe(1); + }); + + it("collects multiple distinct error messages", () => { + fs.writeFileSync(stdioLogPath, "ERROR: quota exceeded\nERROR: auth failed\n"); + const result = buildEngineFailureContext(); + expect(result).toContain("quota exceeded"); + expect(result).toContain("auth failed"); + }); + + it("falls back to last lines when no known error patterns match", () => { + const logLines = ["Starting agent...", "Running tool: list_branches", '{"branches": ["main"]}', "Running tool: get_file_contents", "Agent interrupted"]; + fs.writeFileSync(stdioLogPath, logLines.join("\n") + "\n"); + const result = buildEngineFailureContext(); + expect(result).toContain("Engine Failure"); + expect(result).toContain("Last agent output"); + expect(result).toContain("Agent interrupted"); + }); + + it("fallback includes at most 10 non-empty lines", () => { + const lines = Array.from({ length: 20 }, (_, i) => `line ${i + 1}`); + fs.writeFileSync(stdioLogPath, lines.join("\n") + "\n"); + const result = buildEngineFailureContext(); + expect(result).toContain("line 20"); + expect(result).toContain("line 11"); + // Lines 1-10 should not appear in the tail + expect(result).not.toContain("line 10\n"); + expect(result).not.toContain("line 1\n"); + }); + + it("fallback ignores empty lines when counting tail", () => { + const lines = ["line 1", "", "line 2", "", "line 3", "", "", "line 4"]; + fs.writeFileSync(stdioLogPath, lines.join("\n") + "\n"); + const result = buildEngineFailureContext(); + expect(result).toContain("Last agent output"); + expect(result).toContain("line 4"); + expect(result).toContain("line 1"); + }); + }); });