Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/patch-fix-infra-lines-in-engine-failure-context.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 27 additions & 1 deletion actions/setup/js/handle_agent_failure.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down Expand Up @@ -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;
Expand All @@ -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`;
Expand Down
90 changes: 90 additions & 0 deletions actions/setup/js/handle_agent_failure.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
26 changes: 26 additions & 0 deletions actions/setup/js/log_parser_shared.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -1650,6 +1675,7 @@ module.exports = {
// Constants
MAX_TOOL_OUTPUT_LENGTH,
MAX_STEP_SUMMARY_SIZE,
AWF_INFRA_LINE_RE,
// Classes
StepSummaryTracker,
// Functions
Expand Down
4 changes: 2 additions & 2 deletions actions/setup/js/parse_copilot_log.cjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// @ts-check
/// <reference types="@actions/github-script" />

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({
Expand Down Expand Up @@ -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+[└│]/;
Expand Down
Loading