From b6ebd0ada1ab9ec6b01daec35d77bda9c61b246d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Jul 2026 06:21:51 +0000 Subject: [PATCH 1/4] Initial plan From 76329469f56bef24e367d5c72189c7a3870512e5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Jul 2026 06:58:32 +0000 Subject: [PATCH 2/4] fix: detect max consecutive cache misses exceeded and surface dedicated failure context Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/handle_agent_failure.cjs | 33 +++++++ .../setup/js/handle_agent_failure.test.cjs | 90 +++++++++++++++++++ actions/setup/md/max_cache_misses_exceeded.md | 28 ++++++ 3 files changed, 151 insertions(+) create mode 100644 actions/setup/md/max_cache_misses_exceeded.md diff --git a/actions/setup/js/handle_agent_failure.cjs b/actions/setup/js/handle_agent_failure.cjs index c3b9579f7d9..b77bc1610a6 100644 --- a/actions/setup/js/handle_agent_failure.cjs +++ b/actions/setup/js/handle_agent_failure.cjs @@ -43,6 +43,7 @@ const ELLIPSIS_LENGTH = ELLIPSIS.length; const ENGINE_RATE_LIMIT_429_RE = /(?:\b429\b[\s\S]{0,120}(?:too many requests|rate[\s-]*limit)|\brate_limit_(?:error|exceeded)\b|capierror:\s*429|failed to get response from the ai model[\s\S]{0,120}\b429\b|exceeded your rate limit for utility models)/i; const ENGINE_MAX_RUNS_EXCEEDED_RE = /(?:\bmax_runs_exceeded\b|\bmaximum\s+llm\s+invocations\s+exceeded\b)/i; +const ENGINE_MAX_CACHE_MISSES_EXCEEDED_RE = /(?:\bmax_cache_misses_exceeded\b|\bmaximum\s+consecutive\s+cache\s+misses\s+exceeded\b)/i; const ALLOWED_FILES_ERROR_RE = /^(?.*outside the allowed-files list) \((?.+?)\)\. (?Add the files to the allowed-files configuration field or remove them from the (?:patch|bundle)\.)$/; /** @@ -1981,6 +1982,31 @@ function buildEngineMaxRunsExceededContext(engineLabel) { return "\n" + renderPromptTemplate("engine_max_runs_exceeded.md", { engine_label: normalizedEngineLabel }); } +/** + * Detect max consecutive cache misses failures in text payloads. + * Returns true when content includes either the `max_cache_misses_exceeded` error type + * or the "Maximum consecutive cache misses exceeded" message fragment. + * @param {string|null|undefined} content + * @returns {boolean} + */ +function hasEngineMaxCacheMissesExceededSignal(content) { + if (!content) { + return false; + } + return ENGINE_MAX_CACHE_MISSES_EXCEEDED_RE.test(content); +} + +/** + * Build dedicated context for max consecutive cache misses failures. + * Renders the max-cache-misses-exceeded prompt template with the active engine label. + * @param {string} [engineLabel] + * @returns {string} + */ +function buildEngineMaxCacheMissesExceededContext(engineLabel) { + const normalizedEngineLabel = (typeof engineLabel === "string" ? engineLabel : "").trim() || "AI"; + return "\n" + renderPromptTemplate("max_cache_misses_exceeded.md", { engine_label: normalizedEngineLabel }); +} + /** * Read and render token usage from token-usage.jsonl for inclusion in the ET computation table. * Returns null gracefully when files are absent, empty, or unparseable. @@ -2623,6 +2649,11 @@ function buildEngineFailureContext(options = {}) { return buildEngineMaxRunsExceededContext(engineLabel); } + if (hasEngineMaxCacheMissesExceededSignal(logContent)) { + core.info("Detected engine max cache misses signal — using dedicated context message"); + return buildEngineMaxCacheMissesExceededContext(engineLabel); + } + const errorMessages = new Set(); for (const line of lines) { @@ -4081,6 +4112,8 @@ module.exports = { hasEngineRateLimit429InOTELMirror, buildEngineMaxRunsExceededContext, buildEngineRateLimit429Context, + hasEngineMaxCacheMissesExceededSignal, + buildEngineMaxCacheMissesExceededContext, readTokenUsageMarkdown, parseFirewallAuthErrors, parseMaxAICreditsFromAuditLog, diff --git a/actions/setup/js/handle_agent_failure.test.cjs b/actions/setup/js/handle_agent_failure.test.cjs index 22ed02289f6..aaa171e399e 100644 --- a/actions/setup/js/handle_agent_failure.test.cjs +++ b/actions/setup/js/handle_agent_failure.test.cjs @@ -22,6 +22,7 @@ describe("handle_agent_failure", () => { let getActionFailureIssueExpiresHours; const ENGINE_RATE_LIMIT_TEMPLATE = "> [!WARNING]\n> **Engine Rate Limited (HTTP 429)**\n> OTLP telemetry\n> {engine_label}\n"; const ENGINE_MAX_RUNS_EXCEEDED_TEMPLATE = "> [!WARNING]\n> **Engine Max Runs Exceeded**\n> max-runs guardrail\n> {engine_label}\n"; + const ENGINE_MAX_CACHE_MISSES_EXCEEDED_TEMPLATE = "> [!WARNING]\n> **Engine Cache Miss Limit Exceeded**\n> cache misses guardrail\n> {engine_label}\n"; beforeEach(() => { // Provide minimal GitHub Actions globals expected by require-time code @@ -2920,6 +2921,95 @@ describe("handle_agent_failure", () => { }); }); + // ────────────────────────────────────────────────────── + // hasEngineMaxCacheMissesExceededSignal + // ────────────────────────────────────────────────────── + + describe("hasEngineMaxCacheMissesExceededSignal", () => { + let hasEngineMaxCacheMissesExceededSignal; + + beforeEach(() => { + vi.resetModules(); + ({ hasEngineMaxCacheMissesExceededSignal } = require("./handle_agent_failure.cjs")); + }); + + it("returns false for empty-like content", () => { + expect(hasEngineMaxCacheMissesExceededSignal("")).toBe(false); + expect(hasEngineMaxCacheMissesExceededSignal(null)).toBe(false); + expect(hasEngineMaxCacheMissesExceededSignal(undefined)).toBe(false); + }); + + it("returns true when max_cache_misses_exceeded marker is present", () => { + expect(hasEngineMaxCacheMissesExceededSignal('{"error":{"type":"max_cache_misses_exceeded"}}')).toBe(true); + }); + + it("returns true when Maximum consecutive cache misses exceeded text is present", () => { + expect(hasEngineMaxCacheMissesExceededSignal("Maximum consecutive cache misses exceeded (6 / 5).")).toBe(true); + }); + + it("returns true for the exact error seen in production logs", () => { + const logLine = + '2026-07-30T06:14:50.000Z [ERROR] Error in API request: 403 {"error":{"type":"max_cache_misses_exceeded","message":"Maximum consecutive cache misses exceeded (6 / 5).","consecutive_cache_misses":6,"max_cache_misses":5}}'; + expect(hasEngineMaxCacheMissesExceededSignal(logLine)).toBe(true); + }); + + it("returns false for unrelated content", () => { + expect(hasEngineMaxCacheMissesExceededSignal("request failed for unrelated reason")).toBe(false); + }); + }); + + // ────────────────────────────────────────────────────── + // buildEngineMaxCacheMissesExceededContext + // ────────────────────────────────────────────────────── + + describe("buildEngineMaxCacheMissesExceededContext", () => { + let buildEngineMaxCacheMissesExceededContext; + const fs = require("fs"); + const path = require("path"); + const os = require("os"); + + /** @type {string} */ + let tmpDir; + + /** @type {string} */ + let promptsDir; + + beforeEach(() => { + vi.resetModules(); + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "aw-test-cache-misses-")); + promptsDir = path.join(tmpDir, "gh-aw", "prompts"); + fs.mkdirSync(promptsDir, { recursive: true }); + process.env.RUNNER_TEMP = tmpDir; + ({ buildEngineMaxCacheMissesExceededContext } = require("./handle_agent_failure.cjs")); + fs.writeFileSync(path.join(promptsDir, "max_cache_misses_exceeded.md"), ENGINE_MAX_CACHE_MISSES_EXCEEDED_TEMPLATE); + }); + + afterEach(() => { + delete process.env.RUNNER_TEMP; + if (fs.existsSync(tmpDir)) { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); + + it("renders template content for a provided engine label", () => { + const result = buildEngineMaxCacheMissesExceededContext("claude"); + expect(result).toContain("Engine Cache Miss Limit Exceeded"); + expect(result).toContain("cache misses guardrail"); + expect(result).toContain("claude"); + }); + + it("falls back to AI when engine label is empty or whitespace", () => { + expect(buildEngineMaxCacheMissesExceededContext("")).toContain("AI"); + expect(buildEngineMaxCacheMissesExceededContext(" ")).toContain("AI"); + }); + + it("trims leading/trailing whitespace from engine label", () => { + const result = buildEngineMaxCacheMissesExceededContext(" claude "); + expect(result).toContain("claude"); + expect(result).not.toContain(" claude "); + }); + }); + // ────────────────────────────────────────────────────── // buildMCPPolicyErrorContext // ────────────────────────────────────────────────────── diff --git a/actions/setup/md/max_cache_misses_exceeded.md b/actions/setup/md/max_cache_misses_exceeded.md new file mode 100644 index 00000000000..6cbad8be008 --- /dev/null +++ b/actions/setup/md/max_cache_misses_exceeded.md @@ -0,0 +1,28 @@ +> [!WARNING] +> **Engine Cache Miss Limit Exceeded**: The {engine_label} engine hit the provider's consecutive cache miss limit and could not complete this run. + +This signal was detected from engine runtime logs. + +
+What caused this + +The provider enforces a limit on consecutive cache misses. When too many back-to-back requests bypass the prompt cache, the API returns a 403 error and the engine terminates. + +Common causes: + +- Dynamic or frequently changing content in the prompt (e.g. timestamps, run IDs, random values) +- Prompts that are too short for the provider's cache threshold +- High concurrency across workflow runs that share the same cache slot +- A burst of runs before the cache warms up after a deployment or key rotation + +
+ +
+How to remediate + +- **Wait and retry**: Cache misses are often transient. Re-running the workflow after a few minutes is usually sufficient. +- **Stabilize the prompt**: Move volatile values (current date, run ID, etc.) out of the system prompt or early turns and into later user turns so the cacheable prefix stays constant. +- **Reduce run frequency**: If the workflow fires too often, the cache slot may be evicted before reuse. Consider scheduling less aggressively or adding a concurrency group. +- **Review `max-daily-ai-credits`**: A very high run volume can exhaust per-key cache capacity. Check your usage and apply a daily cap if needed. + +
From 74925da30da06be65e922e9e846525ac32f596e7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Jul 2026 07:58:05 +0000 Subject: [PATCH 3/4] fix: add unified max_cache_misses_exceeded detection to detect_agent_errors for all agentic engines MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add MAX_CACHE_MISSES_EXCEEDED_EVENT_TYPE constant and parseMaxCacheMissesExceededFromEventLog() to ai_credits_context.cjs to detect structured max_cache_misses_exceeded events from the AWF API proxy event logs (engine-agnostic guardrail, all engines share the same proxy) - Add MAX_CACHE_MISSES_EXCEEDED_PATTERN regex and isMaxCacheMissesExceededError() to detect_agent_errors.cjs — the unified detection module — covering both the agent stdio log (text pattern) and the AWF API proxy event log (structured events) - Wire maxCacheMissesExceeded into detectErrors() return value, buildOutputLines() (max_cache_misses_exceeded output), and the main() stderr diagnostics - Update handle_agent_failure.cjs to import MAX_CACHE_MISSES_EXCEEDED_PATTERN from detect_agent_errors.cjs instead of maintaining a duplicate regex - Add 20 new tests to detect_agent_errors.test.cjs covering MAX_CACHE_MISSES_EXCEEDED_PATTERN, isMaxCacheMissesExceededError, detectErrors maxCacheMissesExceeded field, and buildOutputLines max_cache_misses_exceeded output Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/ai_credits_context.cjs | 30 ++++++ actions/setup/js/detect_agent_errors.cjs | 44 +++++++- actions/setup/js/detect_agent_errors.test.cjs | 101 ++++++++++++++++++ actions/setup/js/handle_agent_failure.cjs | 6 +- 4 files changed, 176 insertions(+), 5 deletions(-) diff --git a/actions/setup/js/ai_credits_context.cjs b/actions/setup/js/ai_credits_context.cjs index 327b51a7c4f..b2134d2630b 100644 --- a/actions/setup/js/ai_credits_context.cjs +++ b/actions/setup/js/ai_credits_context.cjs @@ -18,6 +18,10 @@ const BUDGET_EXCEEDED_EVENT = "budget_exceeded"; // The literal error type emitted by the AWF API proxy (HTTP 400) when maxAiCredits is active // and the requested model is not in the built-in pricing table. const UNKNOWN_MODEL_AI_CREDITS_TYPE = "unknown_model_ai_credits"; +// The literal error type emitted by the AWF API proxy (HTTP 403) when the consecutive cache +// miss counter reaches the apiProxy.maxCacheMisses limit. Engine-agnostic: all engines share +// the same proxy guardrail. +const MAX_CACHE_MISSES_EXCEEDED_EVENT_TYPE = "max_cache_misses_exceeded"; const MAX_AI_CREDITS_EXCEEDED_STDIO_RE = /maximum ai credits exceeded(?:\s*\((\d+(?:\.\d+)?)\s*\/\s*(\d+(?:\.\d+)?)\))?/i; const DEFAULT_AGENT_STDIO_LOG = "/tmp/gh-aw/agent-stdio.log"; const AGENT_STDIO_LOG_MAX_TAIL = 64 * 1024; // 64 KB — sufficient for any realistic error block @@ -425,6 +429,30 @@ function parseUnknownModelAICreditsAndModelFromAuditLog(auditJsonlPathOverride) ); } +/** + * Detects a `max_cache_misses_exceeded` event from the AWF API proxy event logs. + * The proxy emits this HTTP 403 error when the consecutive cache miss counter reaches + * the configured `apiProxy.maxCacheMisses` limit. Detection is engine-agnostic: + * all agentic engines share the same AWF API proxy guardrail. + * Structured entries emitted by the AWF API proxy look like: + * { "type": "max_cache_misses_exceeded", "consecutive_cache_misses": 6, "max_cache_misses": 5 } + * + * @param {string} [eventLogPathOverride] + * @returns {boolean} + */ +function parseMaxCacheMissesExceededFromEventLog(eventLogPathOverride) { + return iterateJSONLFiles( + resolveUnknownModelAICreditsLogPaths(eventLogPathOverride), + false, + content => content.includes(MAX_CACHE_MISSES_EXCEEDED_EVENT_TYPE), + (acc, entry) => { + if (acc) return true; // already detected, short-circuit + return traverseObjectTree(entry, (_key, value) => value === MAX_CACHE_MISSES_EXCEEDED_EVENT_TYPE) || undefined; + }, + acc => acc + ); +} + /** * Single-pass combined read of the audit log, returning all AI credits fields at once. * Used by resolveAICreditsFailureState to avoid reading the same file twice. @@ -552,5 +580,7 @@ module.exports = { parseMaxAICreditsExceededFromAuditLog, parseUnknownModelAICreditsFromAuditLog, parseUnknownModelAICreditsAndModelFromAuditLog, + parseMaxCacheMissesExceededFromEventLog, resolveAICreditsFailureState, + MAX_CACHE_MISSES_EXCEEDED_EVENT_TYPE, }; diff --git a/actions/setup/js/detect_agent_errors.cjs b/actions/setup/js/detect_agent_errors.cjs index 3edb13952be..99477eb32ed 100644 --- a/actions/setup/js/detect_agent_errors.cjs +++ b/actions/setup/js/detect_agent_errors.cjs @@ -45,7 +45,7 @@ const fs = require("fs"); const { MAX_RUNS_EXCEEDED_PATTERNS, isMaxRunsExceededError } = require("./harness_retry_guard.cjs"); -const { parseUnknownModelAICreditsAndModelFromAuditLog } = require("./ai_credits_context.cjs"); +const { parseUnknownModelAICreditsAndModelFromAuditLog, parseMaxCacheMissesExceededFromEventLog } = require("./ai_credits_context.cjs"); const LOG_FILE = "/tmp/gh-aw/agent-stdio.log"; @@ -121,6 +121,17 @@ function buildCombinedPattern(patterns) { // The pooled per-run invocation budget is saturated — retries cannot make progress. const INVOCATION_CAP_EXCEEDED_PATTERN = buildCombinedPattern(MAX_RUNS_EXCEEDED_PATTERNS); +// Pattern: AWF API proxy consecutive cache miss limit exceeded. +// The AWF API proxy (engine-agnostic) enforces a configurable limit on back-to-back +// requests that miss the prompt cache (apiProxy.maxCacheMisses, default 5). When the +// limit is reached it rejects further requests with HTTP 403 and error type +// "max_cache_misses_exceeded". Two observable forms: +// 1) JSON error type in provider API response: "max_cache_misses_exceeded" +// 2) Human-readable message from SDK wrapper: "Maximum consecutive cache misses exceeded" +// Structured events from the AWF API proxy event log are checked separately via +// parseMaxCacheMissesExceededFromEventLog(). +const MAX_CACHE_MISSES_EXCEEDED_PATTERN = /(?:\bmax_cache_misses_exceeded\b|\bmaximum\s+consecutive\s+cache\s+misses\s+exceeded\b)/i; + /** * Determines if the collected output contains the observed Copilot/CAPI quota exhaustion error. * @param {string} output - Collected stdout+stderr from the process @@ -142,6 +153,17 @@ function isInvocationCapExceededError(output) { return isMaxRunsExceededError(output); } +/** + * Determines if the collected output indicates the AWF API proxy cache miss limit is exceeded. + * Checks the agent stdio log for the text-form signal. The structured AWF API proxy event log + * is checked separately in detectErrors() via parseMaxCacheMissesExceededFromEventLog(). + * @param {string} output - Collected stdout+stderr from the process + * @returns {boolean} + */ +function isMaxCacheMissesExceededError(output) { + return MAX_CACHE_MISSES_EXCEEDED_PATTERN.test(output); +} + /** * Normalize model names to a single safe line for GitHub Actions outputs and issue titles. * @param {string} value @@ -164,7 +186,7 @@ function extractMissingModelPricingModelName(logContent) { /** * Detect known error patterns in a log string and return detection results. * @param {string} logContent - Contents of the agent stdio log - * @returns {{ inferenceAccessError: boolean, mcpPolicyError: boolean, agenticEngineTimeout: boolean, modelNotSupportedError: boolean, http400ResponseError: boolean, capiQuotaExceededError: boolean, invocationCapExceeded: boolean, missingModelPricingError: boolean, missingModelPricingModelName: string }} + * @returns {{ inferenceAccessError: boolean, mcpPolicyError: boolean, agenticEngineTimeout: boolean, modelNotSupportedError: boolean, http400ResponseError: boolean, capiQuotaExceededError: boolean, invocationCapExceeded: boolean, maxCacheMissesExceeded: boolean, missingModelPricingError: boolean, missingModelPricingModelName: string }} */ function detectErrors(logContent) { const missingModelPricingModelName = extractMissingModelPricingModelName(logContent); @@ -176,6 +198,7 @@ function detectErrors(logContent) { http400ResponseError: HTTP_400_RESPONSE_ERROR_PATTERN.test(logContent), capiQuotaExceededError: isCAPIQuotaExceededError(logContent), invocationCapExceeded: isInvocationCapExceededError(logContent), + maxCacheMissesExceeded: isMaxCacheMissesExceededError(logContent), missingModelPricingError: missingModelPricingModelName !== "", missingModelPricingModelName, }; @@ -183,7 +206,7 @@ function detectErrors(logContent) { /** * Build GitHub Actions output lines from detection results. - * @param {{ inferenceAccessError: boolean, mcpPolicyError: boolean, agenticEngineTimeout: boolean, modelNotSupportedError: boolean, http400ResponseError: boolean, capiQuotaExceededError: boolean, invocationCapExceeded: boolean, missingModelPricingError: boolean, missingModelPricingModelName: string }} results + * @param {{ inferenceAccessError: boolean, mcpPolicyError: boolean, agenticEngineTimeout: boolean, modelNotSupportedError: boolean, http400ResponseError: boolean, capiQuotaExceededError: boolean, invocationCapExceeded: boolean, maxCacheMissesExceeded: boolean, missingModelPricingError: boolean, missingModelPricingModelName: string }} results * @returns {string[]} */ function buildOutputLines(results) { @@ -196,6 +219,7 @@ function buildOutputLines(results) { `http_400_response_error=${results.http400ResponseError}`, `capi_quota_exceeded_error=${effectiveCAPIQuotaExceeded}`, `invocation_cap_exceeded=${results.invocationCapExceeded}`, + `max_cache_misses_exceeded=${results.maxCacheMissesExceeded}`, `missing_model_pricing_error=${results.missingModelPricingError}`, `missing_model_pricing_model_name=${results.missingModelPricingModelName}`, ]; @@ -244,8 +268,17 @@ function main() { process.stderr.write(`[detect-agent-errors] Detected missing model pricing from firewall structured log: model "${auditModelName}" has no AI credits pricing configured\n`); } + // Also check the AWF API proxy event logs for the `max_cache_misses_exceeded` structured + // event. This covers all engines since the proxy guardrail fires independently of the + // underlying AI engine. + const eventLogCacheMissesExceeded = parseMaxCacheMissesExceededFromEventLog(); + if (eventLogCacheMissesExceeded && !stdioResults.maxCacheMissesExceeded) { + process.stderr.write("[detect-agent-errors] Detected max cache misses exceeded from AWF API proxy event log\n"); + } + const results = { ...stdioResults, + maxCacheMissesExceeded: stdioResults.maxCacheMissesExceeded || eventLogCacheMissesExceeded, missingModelPricingError: stdioResults.missingModelPricingError || auditMissingPricing, missingModelPricingModelName: stdioResults.missingModelPricingModelName || sanitizeModelName(auditModelName), }; @@ -271,6 +304,9 @@ function main() { if (results.invocationCapExceeded) { process.stderr.write("[detect-agent-errors] Detected invocation cap exhaustion: the pooled per-run LLM invocation budget is fully saturated\n"); } + if (results.maxCacheMissesExceeded) { + process.stderr.write("[detect-agent-errors] Detected max cache misses exceeded: the AWF API proxy consecutive cache miss limit was reached\n"); + } if (results.missingModelPricingError && !auditMissingPricing) { process.stderr.write(`[detect-agent-errors] Detected missing model pricing: model "${results.missingModelPricingModelName}" has no AI credits pricing configured\n`); } @@ -287,6 +323,7 @@ module.exports = { extractMissingModelPricingModelName, isCAPIQuotaExceededError, isInvocationCapExceededError, + isMaxCacheMissesExceededError, INFERENCE_ACCESS_ERROR_PATTERN, MCP_POLICY_BLOCKED_PATTERN, AGENTIC_ENGINE_TIMEOUT_PATTERN, @@ -294,6 +331,7 @@ module.exports = { HTTP_400_RESPONSE_ERROR_PATTERN, CAPI_QUOTA_EXCEEDED_PATTERN, INVOCATION_CAP_EXCEEDED_PATTERN, + MAX_CACHE_MISSES_EXCEEDED_PATTERN, MISSING_MODEL_PRICING_PATTERN, buildOutputLines, }; diff --git a/actions/setup/js/detect_agent_errors.test.cjs b/actions/setup/js/detect_agent_errors.test.cjs index 7633d14d591..1ec00d34e9f 100644 --- a/actions/setup/js/detect_agent_errors.test.cjs +++ b/actions/setup/js/detect_agent_errors.test.cjs @@ -4,6 +4,7 @@ const { detectErrors, isCAPIQuotaExceededError, isInvocationCapExceededError, + isMaxCacheMissesExceededError, INFERENCE_ACCESS_ERROR_PATTERN, MCP_POLICY_BLOCKED_PATTERN, AGENTIC_ENGINE_TIMEOUT_PATTERN, @@ -11,6 +12,7 @@ const { HTTP_400_RESPONSE_ERROR_PATTERN, CAPI_QUOTA_EXCEEDED_PATTERN, INVOCATION_CAP_EXCEEDED_PATTERN, + MAX_CACHE_MISSES_EXCEEDED_PATTERN, MISSING_MODEL_PRICING_PATTERN, extractMissingModelPricingModelName, buildOutputLines, @@ -318,6 +320,7 @@ describe("detect_agent_errors.cjs", () => { expect(result.http400ResponseError).toBe(false); expect(result.capiQuotaExceededError).toBe(false); expect(result.invocationCapExceeded).toBe(false); + expect(result.maxCacheMissesExceeded).toBe(false); expect(result.missingModelPricingError).toBe(false); expect(result.missingModelPricingModelName).toBe(""); }); @@ -484,6 +487,7 @@ describe("detect_agent_errors.cjs", () => { expect(result.http400ResponseError).toBe(false); expect(result.capiQuotaExceededError).toBe(false); expect(result.invocationCapExceeded).toBe(false); + expect(result.maxCacheMissesExceeded).toBe(false); expect(result.missingModelPricingError).toBe(false); }); @@ -515,6 +519,67 @@ commentary" has no AI credits pricing`; expect(result.missingModelPricingError).toBe(false); expect(result.missingModelPricingModelName).toBe(""); }); + + it("detects max cache misses exceeded (JSON error type form)", () => { + const result = detectErrors('{"error":{"type":"max_cache_misses_exceeded","message":"Maximum consecutive cache misses exceeded (6 / 5).","consecutive_cache_misses":6,"max_cache_misses":5}}'); + expect(result.maxCacheMissesExceeded).toBe(true); + expect(result.inferenceAccessError).toBe(false); + expect(result.invocationCapExceeded).toBe(false); + }); + + it("detects max cache misses exceeded (human-readable message form)", () => { + const result = detectErrors("Maximum consecutive cache misses exceeded"); + expect(result.maxCacheMissesExceeded).toBe(true); + expect(result.inferenceAccessError).toBe(false); + expect(result.invocationCapExceeded).toBe(false); + }); + + it("detects max cache misses exceeded in a production log line", () => { + const log = '2026-07-30T06:14:50.000Z [ERROR] Error in API request: 403 {"error":{"type":"max_cache_misses_exceeded","message":"Maximum consecutive cache misses exceeded (6 / 5).","consecutive_cache_misses":6,"max_cache_misses":5}}'; + const result = detectErrors(log); + expect(result.maxCacheMissesExceeded).toBe(true); + }); + + it("does not false-positive on unrelated cache miss content", () => { + const result = detectErrors("Cache miss for key: model-output-xyz"); + expect(result.maxCacheMissesExceeded).toBe(false); + }); + }); + + describe("MAX_CACHE_MISSES_EXCEEDED_PATTERN", () => { + it("matches max_cache_misses_exceeded error type", () => { + expect(MAX_CACHE_MISSES_EXCEEDED_PATTERN.test('{"type":"max_cache_misses_exceeded"}')).toBe(true); + }); + + it("matches Maximum consecutive cache misses exceeded message", () => { + expect(MAX_CACHE_MISSES_EXCEEDED_PATTERN.test("Maximum consecutive cache misses exceeded")).toBe(true); + }); + + it("is case-insensitive", () => { + expect(MAX_CACHE_MISSES_EXCEEDED_PATTERN.test("MAXIMUM CONSECUTIVE CACHE MISSES EXCEEDED")).toBe(true); + }); + + it("does not match unrelated cache miss content", () => { + expect(MAX_CACHE_MISSES_EXCEEDED_PATTERN.test("Cache miss for key: output")).toBe(false); + }); + }); + + describe("isMaxCacheMissesExceededError", () => { + it("returns false for empty input", () => { + expect(isMaxCacheMissesExceededError("")).toBe(false); + }); + + it("detects max_cache_misses_exceeded JSON error type", () => { + expect(isMaxCacheMissesExceededError('{"error":{"type":"max_cache_misses_exceeded"}}')).toBe(true); + }); + + it("detects human-readable message form", () => { + expect(isMaxCacheMissesExceededError("Maximum consecutive cache misses exceeded")).toBe(true); + }); + + it("returns false for unrelated content", () => { + expect(isMaxCacheMissesExceededError("Some unrelated error message")).toBe(false); + }); }); describe("buildOutputLines", () => { @@ -542,6 +607,7 @@ commentary" has no AI credits pricing`; http400ResponseError: false, capiQuotaExceededError: false, invocationCapExceeded: false, + maxCacheMissesExceeded: false, missingModelPricingError: true, missingModelPricingModelName: "claude-opus-5", }); @@ -559,6 +625,7 @@ commentary" has no AI credits pricing`; http400ResponseError: false, capiQuotaExceededError: false, invocationCapExceeded: false, + maxCacheMissesExceeded: false, missingModelPricingError: false, missingModelPricingModelName: "", }); @@ -566,5 +633,39 @@ commentary" has no AI credits pricing`; expect(lines).toContain("missing_model_pricing_error=false"); expect(lines).toContain("missing_model_pricing_model_name="); }); + + it("emits max_cache_misses_exceeded=true when detected", () => { + const lines = buildOutputLines({ + inferenceAccessError: false, + mcpPolicyError: false, + agenticEngineTimeout: false, + modelNotSupportedError: false, + http400ResponseError: false, + capiQuotaExceededError: false, + invocationCapExceeded: false, + maxCacheMissesExceeded: true, + missingModelPricingError: false, + missingModelPricingModelName: "", + }); + + expect(lines).toContain("max_cache_misses_exceeded=true"); + }); + + it("emits max_cache_misses_exceeded=false when not detected", () => { + const lines = buildOutputLines({ + inferenceAccessError: false, + mcpPolicyError: false, + agenticEngineTimeout: false, + modelNotSupportedError: false, + http400ResponseError: false, + capiQuotaExceededError: false, + invocationCapExceeded: false, + maxCacheMissesExceeded: false, + missingModelPricingError: false, + missingModelPricingModelName: "", + }); + + expect(lines).toContain("max_cache_misses_exceeded=false"); + }); }); }); diff --git a/actions/setup/js/handle_agent_failure.cjs b/actions/setup/js/handle_agent_failure.cjs index b77bc1610a6..785104b0536 100644 --- a/actions/setup/js/handle_agent_failure.cjs +++ b/actions/setup/js/handle_agent_failure.cjs @@ -12,6 +12,7 @@ const { formatMissingData, formatMissingTools } = require("./missing_info_format const { generateHistoryUrl } = require("./generate_history_link.cjs"); const { AWF_INFRA_LINE_RE } = require("./log_parser_shared.cjs"); const { resolveFirewallAuditLogPath, resolveAICreditsFailureState, parseMaxAICreditsFromAuditLog, parseAICreditsErrorInfoFromAuditLog, parseUnknownModelAICreditsFromAuditLog } = require("./ai_credits_context.cjs"); +const { MAX_CACHE_MISSES_EXCEEDED_PATTERN } = require("./detect_agent_errors.cjs"); const { formatAICCredits } = require("./daily_aic_workflow_helpers.cjs"); const { formatAIC } = require("./model_costs.cjs"); const { parseBoolTemplatable } = require("./templatable.cjs"); @@ -43,7 +44,6 @@ const ELLIPSIS_LENGTH = ELLIPSIS.length; const ENGINE_RATE_LIMIT_429_RE = /(?:\b429\b[\s\S]{0,120}(?:too many requests|rate[\s-]*limit)|\brate_limit_(?:error|exceeded)\b|capierror:\s*429|failed to get response from the ai model[\s\S]{0,120}\b429\b|exceeded your rate limit for utility models)/i; const ENGINE_MAX_RUNS_EXCEEDED_RE = /(?:\bmax_runs_exceeded\b|\bmaximum\s+llm\s+invocations\s+exceeded\b)/i; -const ENGINE_MAX_CACHE_MISSES_EXCEEDED_RE = /(?:\bmax_cache_misses_exceeded\b|\bmaximum\s+consecutive\s+cache\s+misses\s+exceeded\b)/i; const ALLOWED_FILES_ERROR_RE = /^(?.*outside the allowed-files list) \((?.+?)\)\. (?Add the files to the allowed-files configuration field or remove them from the (?:patch|bundle)\.)$/; /** @@ -1986,6 +1986,8 @@ function buildEngineMaxRunsExceededContext(engineLabel) { * Detect max consecutive cache misses failures in text payloads. * Returns true when content includes either the `max_cache_misses_exceeded` error type * or the "Maximum consecutive cache misses exceeded" message fragment. + * Uses the shared MAX_CACHE_MISSES_EXCEEDED_PATTERN from detect_agent_errors for + * consistency with the unified detection mechanism. * @param {string|null|undefined} content * @returns {boolean} */ @@ -1993,7 +1995,7 @@ function hasEngineMaxCacheMissesExceededSignal(content) { if (!content) { return false; } - return ENGINE_MAX_CACHE_MISSES_EXCEEDED_RE.test(content); + return MAX_CACHE_MISSES_EXCEEDED_PATTERN.test(content); } /** From a0f7e248b961f2d24dfe1a8433d66514ccf52a28 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Jul 2026 09:22:29 +0000 Subject: [PATCH 4/4] fix: wire max cache miss detection through failure handling Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- actions/setup/js/ai_credits_context.test.cjs | 54 +++++++++++++++++++ actions/setup/js/detect_agent_errors.cjs | 2 +- actions/setup/js/handle_agent_failure.cjs | 29 ++++++++-- .../setup/js/handle_agent_failure.test.cjs | 24 +++++++++ actions/setup/md/max_cache_misses_exceeded.md | 6 +-- pkg/workflow/compiler_main_job_helpers.go | 2 + .../notify_comment_conclusion_helpers.go | 4 +- .../TestWasmGolden_AllEngines/claude.golden | 1 + .../TestWasmGolden_AllEngines/codex.golden | 1 + .../TestWasmGolden_AllEngines/copilot.golden | 1 + .../basic-copilot.golden | 1 + .../playwright-cli-mode.golden | 1 + .../smoke-copilot.golden | 1 + .../with-imports.golden | 1 + 14 files changed, 120 insertions(+), 8 deletions(-) diff --git a/actions/setup/js/ai_credits_context.test.cjs b/actions/setup/js/ai_credits_context.test.cjs index 218700f333d..6b7b5890a48 100644 --- a/actions/setup/js/ai_credits_context.test.cjs +++ b/actions/setup/js/ai_credits_context.test.cjs @@ -364,6 +364,60 @@ describe("ai_credits_context parseUnknownModelAICreditsAndModelFromAuditLog", () }); }); +describe("ai_credits_context parseMaxCacheMissesExceededFromEventLog", () => { + let tmpDir; + let parseMaxCacheMissesExceededFromEventLog; + + beforeEach(async () => { + vi.resetModules(); + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "aic-cache-misses-test-")); + delete process.env.GH_AW_AGENT_OUTPUT; + const mod = await import("./ai_credits_context.cjs"); + const exports = mod.default || mod; + parseMaxCacheMissesExceededFromEventLog = exports.parseMaxCacheMissesExceededFromEventLog; + }); + + afterEach(() => { + vi.restoreAllMocks(); + fs.rmSync(tmpDir, { recursive: true, force: true }); + delete process.env.GH_AW_AGENT_OUTPUT; + }); + + function writeEventLog(lines, filename = "event-logs.jsonl") { + const logDir = path.join(tmpDir, "sandbox", "firewall", "logs", "api-proxy-logs"); + fs.mkdirSync(logDir, { recursive: true }); + const logPath = path.join(logDir, filename); + fs.writeFileSync(logPath, lines.map(l => JSON.stringify(l)).join("\n") + "\n", "utf8"); + process.env.GH_AW_AGENT_OUTPUT = path.join(tmpDir, "output.json"); + return logPath; + } + + it("detects max_cache_misses_exceeded in event-logs.jsonl", () => { + writeEventLog([{ type: "max_cache_misses_exceeded", consecutive_cache_misses: 6, max_cache_misses: 5 }]); + expect(parseMaxCacheMissesExceededFromEventLog()).toBe(true); + }); + + it("detects max_cache_misses_exceeded in events.jsonl fallback", () => { + writeEventLog([{ type: "max_cache_misses_exceeded", consecutive_cache_misses: 7, max_cache_misses: 5 }], "events.jsonl"); + expect(parseMaxCacheMissesExceededFromEventLog()).toBe(true); + }); + + it("returns false when no matching event is present", () => { + writeEventLog([{ type: "response", status: 200 }]); + expect(parseMaxCacheMissesExceededFromEventLog()).toBe(false); + }); + + it("returns false for missing event log", () => { + process.env.GH_AW_AGENT_OUTPUT = path.join(tmpDir, "output.json"); + expect(parseMaxCacheMissesExceededFromEventLog("/nonexistent/path/event-logs.jsonl")).toBe(false); + }); + + it("does not detect other error types", () => { + writeEventLog([{ type: "unknown_model_ai_credits" }]); + expect(parseMaxCacheMissesExceededFromEventLog()).toBe(false); + }); +}); + describe("ai_credits_context parseMaxAICreditsFromAuditLog", () => { let tmpDir; /** @type {(path?: string) => string} */ diff --git a/actions/setup/js/detect_agent_errors.cjs b/actions/setup/js/detect_agent_errors.cjs index 99477eb32ed..4c1966f41ef 100644 --- a/actions/setup/js/detect_agent_errors.cjs +++ b/actions/setup/js/detect_agent_errors.cjs @@ -227,7 +227,7 @@ function buildOutputLines(results) { /** * Write GitHub Actions outputs to $GITHUB_OUTPUT. - * @param {{ inferenceAccessError: boolean, mcpPolicyError: boolean, agenticEngineTimeout: boolean, modelNotSupportedError: boolean, http400ResponseError: boolean, capiQuotaExceededError: boolean, invocationCapExceeded: boolean, missingModelPricingError: boolean, missingModelPricingModelName: string }} results + * @param {{ inferenceAccessError: boolean, mcpPolicyError: boolean, agenticEngineTimeout: boolean, modelNotSupportedError: boolean, http400ResponseError: boolean, capiQuotaExceededError: boolean, invocationCapExceeded: boolean, maxCacheMissesExceeded: boolean, missingModelPricingError: boolean, missingModelPricingModelName: string }} results */ function writeOutputs(results) { const outputFile = process.env.GITHUB_OUTPUT; diff --git a/actions/setup/js/handle_agent_failure.cjs b/actions/setup/js/handle_agent_failure.cjs index 785104b0536..9648332e61f 100644 --- a/actions/setup/js/handle_agent_failure.cjs +++ b/actions/setup/js/handle_agent_failure.cjs @@ -11,7 +11,14 @@ const { MAX_SUB_ISSUES, getSubIssueCount } = require("./sub_issue_helpers.cjs"); const { formatMissingData, formatMissingTools } = require("./missing_info_formatter.cjs"); const { generateHistoryUrl } = require("./generate_history_link.cjs"); const { AWF_INFRA_LINE_RE } = require("./log_parser_shared.cjs"); -const { resolveFirewallAuditLogPath, resolveAICreditsFailureState, parseMaxAICreditsFromAuditLog, parseAICreditsErrorInfoFromAuditLog, parseUnknownModelAICreditsFromAuditLog } = require("./ai_credits_context.cjs"); +const { + resolveFirewallAuditLogPath, + resolveAICreditsFailureState, + parseMaxAICreditsFromAuditLog, + parseAICreditsErrorInfoFromAuditLog, + parseUnknownModelAICreditsFromAuditLog, + parseMaxCacheMissesExceededFromEventLog, +} = require("./ai_credits_context.cjs"); const { MAX_CACHE_MISSES_EXCEEDED_PATTERN } = require("./detect_agent_errors.cjs"); const { formatAICCredits } = require("./daily_aic_workflow_helpers.cjs"); const { formatAIC } = require("./model_costs.cjs"); @@ -2611,6 +2618,7 @@ function detectAWFFirewallStartupFailureFromLog() { */ function buildEngineFailureContext(options = {}) { const suppressEngineRateLimit429 = options.suppressEngineRateLimit429 === true; + const maxCacheMissesExceededFromDetection = options.maxCacheMissesExceeded === true; // Derive agent-stdio.log path from the agent output file path (same directory) const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; const stdioLogPath = agentOutputFile ? path.join(path.dirname(agentOutputFile), "agent-stdio.log") : "/tmp/gh-aw/agent-stdio.log"; @@ -2618,15 +2626,24 @@ function buildEngineFailureContext(options = {}) { // Include engine ID in failure messages when available (e.g. "copilot", "claude", "codex") const engineId = process.env.GH_AW_ENGINE_ID || ""; const engineLabel = engineId ? ` \`${engineId}\`` : " AI"; + const hasStructuredMaxCacheMissesSignal = maxCacheMissesExceededFromDetection || parseMaxCacheMissesExceededFromEventLog(); try { if (!fs.existsSync(stdioLogPath)) { + if (hasStructuredMaxCacheMissesSignal) { + core.info("agent-stdio.log not found, but structured max cache misses signal was detected — using dedicated context message"); + return buildEngineMaxCacheMissesExceededContext(engineLabel); + } core.info(`agent-stdio.log not found at ${stdioLogPath}, skipping engine failure context`); return ""; } const logContent = fs.readFileSync(stdioLogPath, "utf8"); if (!logContent.trim()) { + if (hasStructuredMaxCacheMissesSignal) { + core.info("agent-stdio.log is empty, but structured max cache misses signal was detected — using dedicated context message"); + return buildEngineMaxCacheMissesExceededContext(engineLabel); + } return ""; } @@ -2651,7 +2668,7 @@ function buildEngineFailureContext(options = {}) { return buildEngineMaxRunsExceededContext(engineLabel); } - if (hasEngineMaxCacheMissesExceededSignal(logContent)) { + if (hasEngineMaxCacheMissesExceededSignal(logContent) || hasStructuredMaxCacheMissesSignal) { core.info("Detected engine max cache misses signal — using dedicated context message"); return buildEngineMaxCacheMissesExceededContext(engineLabel); } @@ -3102,6 +3119,7 @@ async function main() { const agenticEngineTimeout = process.env.GH_AW_AGENTIC_ENGINE_TIMEOUT === "true"; const modelNotSupportedError = process.env.GH_AW_MODEL_NOT_SUPPORTED_ERROR === "true"; const http400ResponseError = process.env.GH_AW_HTTP_400_RESPONSE_ERROR === "true"; + const maxCacheMissesExceeded = process.env.GH_AW_MAX_CACHE_MISSES_EXCEEDED === "true" && agentConclusion === "failure"; const unknownModelAICreditsFromOutput = process.env.GH_AW_UNKNOWN_MODEL_AI_CREDITS === "true"; const unknownModelAICreditsFromAudit = parseUnknownModelAICreditsFromAuditLog(); const unknownModelAICredits = unknownModelAICreditsFromAudit || (unknownModelAICreditsFromOutput && agentConclusion === "failure"); @@ -3697,7 +3715,12 @@ async function main() { // context is the more actionable signal. // Also suppress when missing-model-pricing is detected: the pricing error is the // root cause and the engine error block would be redundant noise. - const engineFailureContext = shouldBuildEngineFailureContext(agentConclusion, hasToolDenialsExceeded, isTimedOut, missingModelPricingError) ? buildEngineFailureContext({ suppressEngineRateLimit429: maxAICreditsExceeded }) : ""; + const engineFailureContext = shouldBuildEngineFailureContext(agentConclusion, hasToolDenialsExceeded, isTimedOut, missingModelPricingError) + ? buildEngineFailureContext({ + suppressEngineRateLimit429: maxAICreditsExceeded, + maxCacheMissesExceeded, + }) + : ""; // Build timeout context const timeoutContext = buildTimeoutContext(isTimedOut, timeoutMinutes); diff --git a/actions/setup/js/handle_agent_failure.test.cjs b/actions/setup/js/handle_agent_failure.test.cjs index aaa171e399e..d27867e77bb 100644 --- a/actions/setup/js/handle_agent_failure.test.cjs +++ b/actions/setup/js/handle_agent_failure.test.cjs @@ -2287,6 +2287,7 @@ describe("handle_agent_failure", () => { fs.mkdirSync(promptsDir, { recursive: true }); fs.writeFileSync(path.join(promptsDir, "engine_rate_limit_429.md"), ENGINE_RATE_LIMIT_TEMPLATE); fs.writeFileSync(path.join(promptsDir, "engine_max_runs_exceeded.md"), ENGINE_MAX_RUNS_EXCEEDED_TEMPLATE); + fs.writeFileSync(path.join(promptsDir, "max_cache_misses_exceeded.md"), ENGINE_MAX_CACHE_MISSES_EXCEEDED_TEMPLATE); process.env.GH_AW_AGENT_OUTPUT = path.join(tmpDir, "agent_output.json"); process.env.RUNNER_TEMP = tmpDir; ({ buildEngineFailureContext } = require("./handle_agent_failure.cjs")); @@ -2295,6 +2296,7 @@ describe("handle_agent_failure", () => { afterEach(() => { delete process.env.GH_AW_AGENT_OUTPUT; delete process.env.GH_AW_ENGINE_ID; + delete process.env.GH_AW_MAX_CACHE_MISSES_EXCEEDED; delete process.env.GH_AW_OTEL_JSONL_PATH; delete process.env.RUNNER_TEMP; // Clean up temp dir @@ -2351,6 +2353,28 @@ describe("handle_agent_failure", () => { expect(result).not.toContain("Last agent output"); }); + it("returns dedicated context for max-cache-misses failures in stdio logs", () => { + fs.writeFileSync( + stdioLogPath, + '2026-07-30T06:14:50.000Z [ERROR] Error in API request: 403 {"error":{"type":"max_cache_misses_exceeded","message":"Maximum consecutive cache misses exceeded (6 / 5).","consecutive_cache_misses":6,"max_cache_misses":5}}\n' + ); + const result = buildEngineFailureContext(); + expect(result).toContain("Engine Cache Miss Limit Exceeded"); + expect(result).toContain("cache misses guardrail"); + expect(result).not.toContain("Last agent output"); + }); + + it("returns dedicated context when max-cache-misses is only present in structured logs", () => { + const logDir = path.join(tmpDir, "sandbox", "firewall", "logs", "api-proxy-logs"); + fs.mkdirSync(logDir, { recursive: true }); + fs.writeFileSync(path.join(logDir, "event-logs.jsonl"), `${JSON.stringify({ type: "max_cache_misses_exceeded", consecutive_cache_misses: 6, max_cache_misses: 5 })}\n`); + fs.writeFileSync(stdioLogPath, "Agent terminated unexpectedly without clear error details\n"); + process.env.GH_AW_MAX_CACHE_MISSES_EXCEEDED = "true"; + const result = buildEngineFailureContext(); + expect(result).toContain("Engine Cache Miss Limit Exceeded"); + expect(result).not.toContain("Last agent output"); + }); + it("suppresses engine 429 context when max-ai-credits-exceeded takes precedence", () => { fs.writeFileSync(stdioLogPath, "Failed to get response from the AI model; retried 5 times. Last error: CAPIError: 429 429 Sorry, you've exceeded your rate limit for utility models.\n"); const result = buildEngineFailureContext({ suppressEngineRateLimit429: true }); diff --git a/actions/setup/md/max_cache_misses_exceeded.md b/actions/setup/md/max_cache_misses_exceeded.md index 6cbad8be008..232840abd12 100644 --- a/actions/setup/md/max_cache_misses_exceeded.md +++ b/actions/setup/md/max_cache_misses_exceeded.md @@ -1,12 +1,12 @@ > [!WARNING] -> **Engine Cache Miss Limit Exceeded**: The {engine_label} engine hit the provider's consecutive cache miss limit and could not complete this run. +> **Engine Cache Miss Limit Exceeded**: The {engine_label} engine hit the gh-aw API proxy consecutive cache miss guardrail and could not complete this run. -This signal was detected from engine runtime logs. +This signal was detected from engine runtime or AWF API proxy logs.
What caused this -The provider enforces a limit on consecutive cache misses. When too many back-to-back requests bypass the prompt cache, the API returns a 403 error and the engine terminates. +The gh-aw API proxy enforces a per-run `apiProxy.maxCacheMisses` guardrail. When too many back-to-back requests bypass the prompt cache, the proxy returns a 403 `max_cache_misses_exceeded` error and the engine terminates. Common causes: diff --git a/pkg/workflow/compiler_main_job_helpers.go b/pkg/workflow/compiler_main_job_helpers.go index 5139f6bc875..b412a53fec2 100644 --- a/pkg/workflow/compiler_main_job_helpers.go +++ b/pkg/workflow/compiler_main_job_helpers.go @@ -198,6 +198,8 @@ func (c *Compiler) addMainJobEngineErrorOutputs(outputs map[string]string, data compilerMainJobLog.Printf("Added http_400_response_error output (engine=%s, step=%s)", engine.GetID(), constants.DetectAgentErrorsStepID) outputs["invocation_cap_exceeded"] = fmt.Sprintf("${{ %s.invocation_cap_exceeded || 'false' }}", stepRef) compilerMainJobLog.Printf("Added invocation_cap_exceeded output (engine=%s, step=%s)", engine.GetID(), constants.DetectAgentErrorsStepID) + outputs["max_cache_misses_exceeded"] = fmt.Sprintf("${{ %s.max_cache_misses_exceeded || 'false' }}", stepRef) + compilerMainJobLog.Printf("Added max_cache_misses_exceeded output (engine=%s, step=%s)", engine.GetID(), constants.DetectAgentErrorsStepID) outputs["missing_model_pricing_error"] = fmt.Sprintf("${{ %s.missing_model_pricing_error || 'false' }}", stepRef) compilerMainJobLog.Printf("Added missing_model_pricing_error output (engine=%s, step=%s)", engine.GetID(), constants.DetectAgentErrorsStepID) outputs["missing_model_pricing_model_name"] = fmt.Sprintf("${{ %s.missing_model_pricing_model_name || '' }}", stepRef) diff --git a/pkg/workflow/notify_comment_conclusion_helpers.go b/pkg/workflow/notify_comment_conclusion_helpers.go index e2401b69a78..13e6ccd4e97 100644 --- a/pkg/workflow/notify_comment_conclusion_helpers.go +++ b/pkg/workflow/notify_comment_conclusion_helpers.go @@ -247,7 +247,7 @@ func buildAgentFailureEngineDetectionVars(engine CodingAgentEngine, data *Workfl // Pass engine error-detection outputs to the conclusion job when the selected engine // provides a host-runner detect-agent-errors step. // Contract: engines returning a non-empty GetErrorDetectionScriptId() must run - // actions/setup/js/detect_agent_errors.cjs, which emits all six outputs below. + // actions/setup/js/detect_agent_errors.cjs, which emits all outputs below. // These outputs cover: // - inference_access_error: token lacks inference access // - mcp_policy_error: MCP servers blocked by enterprise/organization policy @@ -255,6 +255,7 @@ func buildAgentFailureEngineDetectionVars(engine CodingAgentEngine, data *Workfl // - model_not_supported_error: configured model name is invalid or unavailable // - http_400_response_error: engine returned a generic HTTP 400 Bad Request response // - capi_quota_exceeded_error: Copilot/CAPI quota exhaustion/rate-limit response + // - max_cache_misses_exceeded: AWF API proxy consecutive cache miss guardrail fired var envVars []string if engine.GetErrorDetectionScriptId() != "" { envVars = append(envVars, fmt.Sprintf(" GH_AW_INFERENCE_ACCESS_ERROR: ${{ needs.%s.outputs.inference_access_error }}\n", mainJobName)) @@ -262,6 +263,7 @@ func buildAgentFailureEngineDetectionVars(engine CodingAgentEngine, data *Workfl envVars = append(envVars, fmt.Sprintf(" GH_AW_AGENTIC_ENGINE_TIMEOUT: ${{ needs.%s.outputs.agentic_engine_timeout }}\n", mainJobName)) envVars = append(envVars, fmt.Sprintf(" GH_AW_MODEL_NOT_SUPPORTED_ERROR: ${{ needs.%s.outputs.model_not_supported_error }}\n", mainJobName)) envVars = append(envVars, fmt.Sprintf(" GH_AW_HTTP_400_RESPONSE_ERROR: ${{ needs.%s.outputs.http_400_response_error }}\n", mainJobName)) + envVars = append(envVars, fmt.Sprintf(" GH_AW_MAX_CACHE_MISSES_EXCEEDED: ${{ needs.%s.outputs.max_cache_misses_exceeded }}\n", mainJobName)) envVars = append(envVars, fmt.Sprintf(" GH_AW_MISSING_MODEL_PRICING_ERROR: ${{ needs.%s.outputs.missing_model_pricing_error }}\n", mainJobName)) envVars = append(envVars, fmt.Sprintf(" GH_AW_MISSING_MODEL_PRICING_MODEL_NAME: ${{ needs.%s.outputs.missing_model_pricing_model_name }}\n", mainJobName)) } diff --git a/pkg/workflow/testdata/TestWasmGolden_AllEngines/claude.golden b/pkg/workflow/testdata/TestWasmGolden_AllEngines/claude.golden index 61e90e147af..c21aed832ee 100644 --- a/pkg/workflow/testdata/TestWasmGolden_AllEngines/claude.golden +++ b/pkg/workflow/testdata/TestWasmGolden_AllEngines/claude.golden @@ -337,6 +337,7 @@ jobs: http_400_response_error: ${{ steps.detect-agent-errors.outputs.http_400_response_error || 'false' }} inference_access_error: ${{ steps.detect-agent-errors.outputs.inference_access_error || 'false' }} invocation_cap_exceeded: ${{ steps.detect-agent-errors.outputs.invocation_cap_exceeded || 'false' }} + max_cache_misses_exceeded: ${{ steps.detect-agent-errors.outputs.max_cache_misses_exceeded || 'false' }} mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} missing_model_pricing_error: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_error || 'false' }} missing_model_pricing_model_name: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_model_name || '' }} diff --git a/pkg/workflow/testdata/TestWasmGolden_AllEngines/codex.golden b/pkg/workflow/testdata/TestWasmGolden_AllEngines/codex.golden index 1a67fdc957e..138f8c1bfae 100644 --- a/pkg/workflow/testdata/TestWasmGolden_AllEngines/codex.golden +++ b/pkg/workflow/testdata/TestWasmGolden_AllEngines/codex.golden @@ -338,6 +338,7 @@ jobs: http_400_response_error: ${{ steps.detect-agent-errors.outputs.http_400_response_error || 'false' }} inference_access_error: ${{ steps.detect-agent-errors.outputs.inference_access_error || 'false' }} invocation_cap_exceeded: ${{ steps.detect-agent-errors.outputs.invocation_cap_exceeded || 'false' }} + max_cache_misses_exceeded: ${{ steps.detect-agent-errors.outputs.max_cache_misses_exceeded || 'false' }} mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} missing_model_pricing_error: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_error || 'false' }} missing_model_pricing_model_name: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_model_name || '' }} diff --git a/pkg/workflow/testdata/TestWasmGolden_AllEngines/copilot.golden b/pkg/workflow/testdata/TestWasmGolden_AllEngines/copilot.golden index e3896b30c7c..44d6d372205 100644 --- a/pkg/workflow/testdata/TestWasmGolden_AllEngines/copilot.golden +++ b/pkg/workflow/testdata/TestWasmGolden_AllEngines/copilot.golden @@ -337,6 +337,7 @@ jobs: http_400_response_error: ${{ steps.detect-agent-errors.outputs.http_400_response_error || 'false' }} inference_access_error: ${{ steps.detect-agent-errors.outputs.inference_access_error || 'false' }} invocation_cap_exceeded: ${{ steps.detect-agent-errors.outputs.invocation_cap_exceeded || 'false' }} + max_cache_misses_exceeded: ${{ steps.detect-agent-errors.outputs.max_cache_misses_exceeded || 'false' }} mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} missing_model_pricing_error: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_error || 'false' }} missing_model_pricing_model_name: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_model_name || '' }} diff --git a/pkg/workflow/testdata/TestWasmGolden_CompileFixtures/basic-copilot.golden b/pkg/workflow/testdata/TestWasmGolden_CompileFixtures/basic-copilot.golden index 365414c342f..808d00196a2 100644 --- a/pkg/workflow/testdata/TestWasmGolden_CompileFixtures/basic-copilot.golden +++ b/pkg/workflow/testdata/TestWasmGolden_CompileFixtures/basic-copilot.golden @@ -337,6 +337,7 @@ jobs: http_400_response_error: ${{ steps.detect-agent-errors.outputs.http_400_response_error || 'false' }} inference_access_error: ${{ steps.detect-agent-errors.outputs.inference_access_error || 'false' }} invocation_cap_exceeded: ${{ steps.detect-agent-errors.outputs.invocation_cap_exceeded || 'false' }} + max_cache_misses_exceeded: ${{ steps.detect-agent-errors.outputs.max_cache_misses_exceeded || 'false' }} mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} missing_model_pricing_error: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_error || 'false' }} missing_model_pricing_model_name: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_model_name || '' }} diff --git a/pkg/workflow/testdata/TestWasmGolden_CompileFixtures/playwright-cli-mode.golden b/pkg/workflow/testdata/TestWasmGolden_CompileFixtures/playwright-cli-mode.golden index b62d59586f6..7203909b84e 100644 --- a/pkg/workflow/testdata/TestWasmGolden_CompileFixtures/playwright-cli-mode.golden +++ b/pkg/workflow/testdata/TestWasmGolden_CompileFixtures/playwright-cli-mode.golden @@ -348,6 +348,7 @@ jobs: http_400_response_error: ${{ steps.detect-agent-errors.outputs.http_400_response_error || 'false' }} inference_access_error: ${{ steps.detect-agent-errors.outputs.inference_access_error || 'false' }} invocation_cap_exceeded: ${{ steps.detect-agent-errors.outputs.invocation_cap_exceeded || 'false' }} + max_cache_misses_exceeded: ${{ steps.detect-agent-errors.outputs.max_cache_misses_exceeded || 'false' }} mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} missing_model_pricing_error: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_error || 'false' }} missing_model_pricing_model_name: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_model_name || '' }} diff --git a/pkg/workflow/testdata/TestWasmGolden_CompileFixtures/smoke-copilot.golden b/pkg/workflow/testdata/TestWasmGolden_CompileFixtures/smoke-copilot.golden index c3a7e50ea0b..43348eae603 100644 --- a/pkg/workflow/testdata/TestWasmGolden_CompileFixtures/smoke-copilot.golden +++ b/pkg/workflow/testdata/TestWasmGolden_CompileFixtures/smoke-copilot.golden @@ -459,6 +459,7 @@ jobs: http_400_response_error: ${{ steps.detect-agent-errors.outputs.http_400_response_error || 'false' }} inference_access_error: ${{ steps.detect-agent-errors.outputs.inference_access_error || 'false' }} invocation_cap_exceeded: ${{ steps.detect-agent-errors.outputs.invocation_cap_exceeded || 'false' }} + max_cache_misses_exceeded: ${{ steps.detect-agent-errors.outputs.max_cache_misses_exceeded || 'false' }} mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} missing_model_pricing_error: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_error || 'false' }} missing_model_pricing_model_name: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_model_name || '' }} diff --git a/pkg/workflow/testdata/TestWasmGolden_CompileFixtures/with-imports.golden b/pkg/workflow/testdata/TestWasmGolden_CompileFixtures/with-imports.golden index c149e110815..449603bbfeb 100644 --- a/pkg/workflow/testdata/TestWasmGolden_CompileFixtures/with-imports.golden +++ b/pkg/workflow/testdata/TestWasmGolden_CompileFixtures/with-imports.golden @@ -338,6 +338,7 @@ jobs: http_400_response_error: ${{ steps.detect-agent-errors.outputs.http_400_response_error || 'false' }} inference_access_error: ${{ steps.detect-agent-errors.outputs.inference_access_error || 'false' }} invocation_cap_exceeded: ${{ steps.detect-agent-errors.outputs.invocation_cap_exceeded || 'false' }} + max_cache_misses_exceeded: ${{ steps.detect-agent-errors.outputs.max_cache_misses_exceeded || 'false' }} mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} missing_model_pricing_error: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_error || 'false' }} missing_model_pricing_model_name: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_model_name || '' }}