* 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-termin
(options = {})
| 2292 | * @returns {string} Formatted context string, or empty string if no engine failure found |
| 2293 | */ |
| 2294 | function buildEngineFailureContext(options = {}) { |
| 2295 | const suppressEngineRateLimit429 = options.suppressEngineRateLimit429 === true; |
| 2296 | // Derive agent-stdio.log path from the agent output file path (same directory) |
| 2297 | const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; |
| 2298 | const stdioLogPath = agentOutputFile ? path.join(path.dirname(agentOutputFile), "agent-stdio.log") : "/tmp/gh-aw/agent-stdio.log"; |
| 2299 | |
| 2300 | // Include engine ID in failure messages when available (e.g. "copilot", "claude", "codex") |
| 2301 | const engineId = process.env.GH_AW_ENGINE_ID || ""; |
| 2302 | const engineLabel = engineId ? ` \`${engineId}\`` : " AI"; |
| 2303 | |
| 2304 | try { |
| 2305 | if (!fs.existsSync(stdioLogPath)) { |
| 2306 | core.info(`agent-stdio.log not found at ${stdioLogPath}, skipping engine failure context`); |
| 2307 | return ""; |
| 2308 | } |
| 2309 | |
| 2310 | const logContent = fs.readFileSync(stdioLogPath, "utf8"); |
| 2311 | if (!logContent.trim()) { |
| 2312 | return ""; |
| 2313 | } |
| 2314 | |
| 2315 | const lines = logContent.split("\n"); |
| 2316 | |
| 2317 | // Guard: if the agent completed successfully (terminal_reason: "completed"), the job |
| 2318 | // failure was caused by something other than the agent itself (e.g., post-processing |
| 2319 | // or infrastructure). Suppress the engine failure context to avoid false positive labels. |
| 2320 | // Use the already-loaded logContent directly to avoid a redundant file read. |
| 2321 | if (/"terminal_reason"[ ]?:[ ]?"completed"/.test(logContent)) { |
| 2322 | core.info("Agent completed successfully (terminal_reason: completed) — suppressing engine failure context"); |
| 2323 | return ""; |
| 2324 | } |
| 2325 | |
| 2326 | if (!suppressEngineRateLimit429 && (hasEngineRateLimit429Signal(logContent) || hasEngineRateLimit429InOTELMirror())) { |
| 2327 | core.info("Detected engine HTTP 429/rate-limit signal — using dedicated context message"); |
| 2328 | return buildEngineRateLimit429Context(engineLabel); |
| 2329 | } |
| 2330 | |
| 2331 | if (hasEngineMaxRunsExceededSignal(logContent)) { |
| 2332 | core.info("Detected engine max-runs guardrail signal — using dedicated context message"); |
| 2333 | return buildEngineMaxRunsExceededContext(engineLabel); |
| 2334 | } |
| 2335 | |
| 2336 | const errorMessages = new Set(); |
| 2337 | |
| 2338 | for (const line of lines) { |
| 2339 | // Codex / generic CLI: "ERROR: <message>" at the start of a line |
| 2340 | const errorPrefixMatch = line.match(/^ERROR:\s*(.+)$/); |
| 2341 | if (errorPrefixMatch) { |
| 2342 | errorMessages.add(errorPrefixMatch[1].trim()); |
| 2343 | continue; |
| 2344 | } |
| 2345 | |
| 2346 | // Node.js / generic: "Error: <message>" at the start of a line |
| 2347 | const errorCapMatch = line.match(/^Error:\s*(.+)$/); |
| 2348 | if (errorCapMatch) { |
| 2349 | errorMessages.add(errorCapMatch[1].trim()); |
| 2350 | continue; |
| 2351 | } |
no test coverage detected