diff --git a/.changeset/patch-fix-infra-lines-in-engine-failure-context.md b/.changeset/patch-fix-infra-lines-in-engine-failure-context.md new file mode 100644 index 00000000000..334244d91ee --- /dev/null +++ b/.changeset/patch-fix-infra-lines-in-engine-failure-context.md @@ -0,0 +1,5 @@ +--- +"gh-aw": patch +--- + +Fix `buildEngineFailureContext` to filter AWF infrastructure lines (container lifecycle, firewall wrapper) from the fallback "last agent output" display. When the agent-stdio.log contains only infrastructure lines — a pattern characteristic of transient startup failures (service unavailable, rate limiting) — a dedicated message is now shown instead of confusing container shutdown noise. diff --git a/actions/setup/js/handle_agent_failure.cjs b/actions/setup/js/handle_agent_failure.cjs index abe2a8a0c9f..36df2bdc23c 100644 --- a/actions/setup/js/handle_agent_failure.cjs +++ b/actions/setup/js/handle_agent_failure.cjs @@ -10,6 +10,7 @@ const { createExpirationLine, generateFooterWithExpiration } = require("./epheme const { MAX_SUB_ISSUES, getSubIssueCount } = require("./sub_issue_helpers.cjs"); const { formatMissingData } = require("./missing_info_formatter.cjs"); const { generateHistoryUrl } = require("./generate_history_link.cjs"); +const { AWF_INFRA_LINE_RE } = require("./log_parser_shared.cjs"); const fs = require("fs"); const path = require("path"); @@ -836,6 +837,14 @@ function buildEngineFailureContext() { return context; } + // AWF infrastructure lines written by the firewall/container wrapper — not produced by + // the engine itself. They must be filtered out of the fallback tail so the failure + // context surfaces actual agent output rather than container lifecycle noise + // (e.g. "Container awf-squid Removed", "[WARN] Command completed with exit code: 1", + // "Process exiting with code: 1"). Shared constant from log_parser_shared.cjs keeps the + // pattern in sync with parse_copilot_log.cjs. + const INFRA_LINE_RE = AWF_INFRA_LINE_RE; + // 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; @@ -844,7 +853,24 @@ function buildEngineFailureContext() { return ""; } - const tailLines = nonEmptyLines.slice(-TAIL_LINES); + // Exclude AWF infrastructure lines so the fallback displays only actual engine output. + const agentLines = nonEmptyLines.filter(l => !INFRA_LINE_RE.test(l)); + + if (agentLines.length === 0) { + // The log contains only AWF infrastructure lines — the engine exited before producing + // any substantive output. This pattern is characteristic of a transient startup failure + // (e.g., API service unavailable, rate-limiting, token not yet provisioned). + core.info("agent-stdio.log contains only infrastructure lines — engine likely failed at startup (possible transient failure)"); + const recurringFailureGuidance = + process.env.GH_AW_ENGINE_ID === "copilot" + ? "If this failure recurs, check the GitHub Copilot status page and review the firewall audit logs.\n\n" + : "If this failure recurs, check the provider status page (if available) and review the firewall audit logs.\n\n"; + let context = `\n**⚠️ Engine Failure**: The${engineLabel} engine terminated before producing output.\n\n`; + context += "The engine exited immediately without producing any output. This often indicates a transient infrastructure issue (e.g., service unavailable, API rate limiting). " + recurringFailureGuidance; + return context; + } + + const tailLines = agentLines.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${engineLabel} engine terminated unexpectedly.\n\n**Last agent output:**\n\`\`\`\n`; diff --git a/actions/setup/js/handle_agent_failure.test.cjs b/actions/setup/js/handle_agent_failure.test.cjs index d8e165f580a..8e71a677788 100644 --- a/actions/setup/js/handle_agent_failure.test.cjs +++ b/actions/setup/js/handle_agent_failure.test.cjs @@ -590,6 +590,96 @@ describe("handle_agent_failure", () => { expect(result).toContain("line 1"); }); + it("shows startup-failure message when log contains only AWF infrastructure lines", () => { + // This is the exact pattern from the Apr 8 systemic failure incident: + // containers stop cleanly, engine exits with code 1, no substantive output produced. + const infraLines = [ + " Container awf-squid Removing", + " Container awf-squid Removed", + "[SUCCESS] Containers stopped successfully", + "[INFO] Agent session state preserved at: /tmp/awf-agent-session-state-abc123", + "[INFO] API proxy logs available at: /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs", + "[WARN] Command completed with exit code: 1", + "Process exiting with code: 1", + ]; + fs.writeFileSync(stdioLogPath, infraLines.join("\n") + "\n"); + const result = buildEngineFailureContext(); + expect(result).toContain("Engine Failure"); + expect(result).toContain("terminated before producing output"); + expect(result).toContain("transient infrastructure issue"); + // Infrastructure lines should NOT appear as "Last agent output" + expect(result).not.toContain("Last agent output"); + expect(result).not.toContain("awf-squid"); + expect(result).not.toContain("Command completed with exit code"); + expect(result).not.toContain("Process exiting with code"); + }); + + it("filters infrastructure lines from fallback tail when mixed with real agent output", () => { + // Real agent output followed by AWF infrastructure shutdown lines. + // Only the real agent output should appear in the fallback. + const logLines = [ + "Starting agent...", + "● list_files", + " └ Found 12 files", + " Container awf-squid Removing", + " Container awf-squid Removed", + "[SUCCESS] Containers stopped successfully", + "[WARN] Command completed with exit code: 1", + "Process exiting with code: 1", + ]; + fs.writeFileSync(stdioLogPath, logLines.join("\n") + "\n"); + const result = buildEngineFailureContext(); + expect(result).toContain("Last agent output"); + expect(result).toContain("Starting agent"); + expect(result).toContain("Found 12 files"); + // Infrastructure lines must be excluded from the displayed output + expect(result).not.toContain("awf-squid"); + expect(result).not.toContain("Command completed with exit code"); + expect(result).not.toContain("Process exiting with code"); + }); + + it("includes [entrypoint] and [health-check] infra lines in the infra filter", () => { + // AWF container scripts emit lowercase [entrypoint] and [health-check] prefixes. + // The INFRA_LINE_RE pattern is intentionally case-sensitive and matches exactly + // the casing produced by each AWF component (consistent with parse_copilot_log.cjs). + const lines = ["[entrypoint] Starting firewall...", "[health-check] Proxy ready", "[INFO] API proxy logs available at: /tmp/gh-aw/logs", "Process exiting with code: 1"]; + fs.writeFileSync(stdioLogPath, lines.join("\n") + "\n"); + const result = buildEngineFailureContext(); + expect(result).toContain("Engine Failure"); + expect(result).toContain("terminated before producing output"); + // None of the infra lines should appear + expect(result).not.toContain("entrypoint"); + expect(result).not.toContain("health-check"); + expect(result).not.toContain("API proxy"); + }); + + it("includes engine ID in startup-failure message", () => { + process.env.GH_AW_ENGINE_ID = "copilot"; + vi.resetModules(); + ({ buildEngineFailureContext } = require("./handle_agent_failure.cjs")); + const infraLines = ["[WARN] Command completed with exit code: 1", "Process exiting with code: 1"]; + fs.writeFileSync(stdioLogPath, infraLines.join("\n") + "\n"); + const result = buildEngineFailureContext(); + expect(result).toContain("`copilot` engine"); + expect(result).toContain("terminated before producing output"); + // Copilot-specific status page guidance + expect(result).toContain("GitHub Copilot status page"); + }); + + it("shows provider-agnostic status page guidance for non-copilot engines", () => { + process.env.GH_AW_ENGINE_ID = "claude"; + vi.resetModules(); + ({ buildEngineFailureContext } = require("./handle_agent_failure.cjs")); + const infraLines = ["[WARN] Command completed with exit code: 1", "Process exiting with code: 1"]; + fs.writeFileSync(stdioLogPath, infraLines.join("\n") + "\n"); + const result = buildEngineFailureContext(); + expect(result).toContain("`claude` engine"); + expect(result).toContain("terminated before producing output"); + // Generic guidance for non-copilot engines + expect(result).toContain("provider status page"); + expect(result).not.toContain("GitHub Copilot status page"); + }); + it("includes engine ID in failure message when GH_AW_ENGINE_ID is set", () => { process.env.GH_AW_ENGINE_ID = "copilot"; vi.resetModules(); diff --git a/actions/setup/js/log_parser_shared.cjs b/actions/setup/js/log_parser_shared.cjs index e7a23fd410d..e1050de49c3 100644 --- a/actions/setup/js/log_parser_shared.cjs +++ b/actions/setup/js/log_parser_shared.cjs @@ -43,6 +43,31 @@ const MAX_AGENT_TEXT_LENGTH = 2000; */ const SIZE_LIMIT_WARNING = "\n\n⚠️ *Step summary size limit reached. Additional content truncated.*\n\n"; +/** + * Matches AWF infrastructure lines written by the firewall/container wrapper. + * These lines are produced by the AWF infrastructure (container lifecycle, firewall proxy) + * rather than by the engine itself, and must be excluded when analysing agent output. + * + * Examples of matched lines: + * - [INFO] API proxy logs available at: … + * - [WARN] Command completed with exit code: 1 + * - [SUCCESS] Containers stopped successfully + * - [ERROR] … + * - [entrypoint] Starting firewall… (lowercase — container script convention) + * - [health-check] Proxy ready (lowercase — container script convention) + * - Container awf-squid Removed (Docker Compose lifecycle output) + * - Network … Removed + * - Process exiting with code: 1 (AWF wrapper exit line) + * + * Note: INFO/WARN/SUCCESS/ERROR are uppercase (AWF wrapper convention); entrypoint and + * health-check are lowercase (container script convention). Mixed casing is intentional + * and reflects the actual output produced by different AWF components. + * + * Used by parse_copilot_log.cjs (parsePrettyPrintFormat) and handle_agent_failure.cjs + * (buildEngineFailureContext) to strip infrastructure noise from engine log analysis. + */ +const AWF_INFRA_LINE_RE = /^\[(INFO|WARN|SUCCESS|ERROR|entrypoint|health-check)\]|^ (?:Container|Network|Volume) |^Process exiting with code:/; + /** * Tracks the size of content being added to a step summary. * Used to prevent exceeding GitHub Actions step summary size limits. @@ -1650,6 +1675,7 @@ module.exports = { // Constants MAX_TOOL_OUTPUT_LENGTH, MAX_STEP_SUMMARY_SIZE, + AWF_INFRA_LINE_RE, // Classes StepSummaryTracker, // Functions diff --git a/actions/setup/js/parse_copilot_log.cjs b/actions/setup/js/parse_copilot_log.cjs index 46350a3324f..89fa9681387 100644 --- a/actions/setup/js/parse_copilot_log.cjs +++ b/actions/setup/js/parse_copilot_log.cjs @@ -1,7 +1,7 @@ // @ts-check /// -const { createEngineLogParser, generateConversationMarkdown, generateInformationSection, formatInitializationSummary, formatToolUse, parseLogEntries } = require("./log_parser_shared.cjs"); +const { createEngineLogParser, generateConversationMarkdown, generateInformationSection, formatInitializationSummary, formatToolUse, parseLogEntries, AWF_INFRA_LINE_RE } = require("./log_parser_shared.cjs"); const { ERR_PARSE } = require("./error_codes.cjs"); const main = createEngineLogParser({ @@ -153,7 +153,7 @@ function parsePrettyPrintFormat(logContent) { return []; } - const INFRA_LINE_RE = /^\[(INFO|WARN|SUCCESS|ERROR|entrypoint|health-check)\]|^ (?:Container|Network|Volume) |^Process exiting with code:/; + const INFRA_LINE_RE = AWF_INFRA_LINE_RE; const FAILED_TOOL_RE = /^✗\s+(\S+)/; const SUCCESS_TOOL_RE = /^(?:●|✓)\s+(\S+)/; const CONTINUATION_RE = /^\s+[└│]/;