From de3cb2fee412429088493bc5ad17f9dadb725be9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Jun 2026 04:48:24 +0000 Subject: [PATCH 1/5] Initial plan From 89f2e16dade01588abb7357cadc847fe1f24c73b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Jun 2026 05:52:33 +0000 Subject: [PATCH 2/5] Classify copilot harness failures Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/copilot_harness.cjs | 106 ++++++++++++++++++ actions/setup/js/copilot_harness.test.cjs | 43 ++++++- actions/setup/js/detect_agent_errors.cjs | 2 +- actions/setup/js/detect_agent_errors.test.cjs | 5 +- 4 files changed, 151 insertions(+), 5 deletions(-) diff --git a/actions/setup/js/copilot_harness.cjs b/actions/setup/js/copilot_harness.cjs index a9cc3fc912b..b735b7e9212 100644 --- a/actions/setup/js/copilot_harness.cjs +++ b/actions/setup/js/copilot_harness.cjs @@ -78,6 +78,8 @@ const MAX_SCHEDULED_EXIT2_RETRIES = 1; const PROMPT_FILE_INLINE_THRESHOLD_BYTES = 100 * 1024; const PROMPT_FILE_INLINE_THRESHOLD_LABEL = "100KB"; const MAX_ENV_VAR_PREVIEW_LENGTH = 120; +const OUTPUT_TAIL_MAX_CHARS = 600; +const OUTPUT_TAIL_MAX_LINES = 12; // Pattern to detect transient CAPIError 400 in copilot output const CAPI_ERROR_400_PATTERN = /CAPIError:\s*400/; @@ -112,6 +114,10 @@ const AUTHENTICATION_FAILED_PATTERN = /Authentication failed(?:\s*\(Request ID:[ const INFERENCE_ACCESS_ERROR_PATTERN = /Access denied by policy settings|invalid access to inference/; // Pattern: Agentic engine process killed by signal (timeout) const AGENTIC_ENGINE_TIMEOUT_PATTERN = /signal=SIG(?:TERM|KILL|INT)/; +// Pattern: Copilot SDK driver timed out waiting for the session to become idle. +const SDK_SESSION_IDLE_TIMEOUT_PATTERN = /Timeout after \d+ms waiting for session\.idle/; +// Pattern: MCP gateway shutdown surfaced in agent output. +const MCP_GATEWAY_SHUTDOWN_PATTERN = /Gateway shutdown initiated/; // Pattern to detect null-type tool_call error that poisons conversation history. // Matches the Copilot API 400 error: @@ -260,6 +266,80 @@ function isAuthenticationFailedError(output) { return AUTHENTICATION_FAILED_PATTERN.test(output); } +/** + * Determines if the collected output contains a Copilot SDK session.idle timeout. + * @param {string} output + * @returns {boolean} + */ +function isSDKSessionIdleTimeoutError(output) { + return SDK_SESSION_IDLE_TIMEOUT_PATTERN.test(output); +} + +/** + * Determines if the collected output contains an MCP gateway shutdown message. + * @param {string} output + * @returns {boolean} + */ +function isMCPGatewayShutdownError(output) { + return MCP_GATEWAY_SHUTDOWN_PATTERN.test(output); +} + +/** + * Extract a compact tail preview from combined process output for failure logs. + * @param {string} output + * @param {{ maxChars?: number, maxLines?: number }} [options] + * @returns {string} + */ +function extractOutputTail(output, options) { + if (typeof output !== "string" || !output) return ""; + const maxChars = options?.maxChars ?? OUTPUT_TAIL_MAX_CHARS; + const maxLines = options?.maxLines ?? OUTPUT_TAIL_MAX_LINES; + const normalized = output.replace(/\0/g, "").replace(/\r\n/g, "\n").trim(); + if (!normalized) return ""; + const tailLines = normalized + .split("\n") + .map(line => line.trimEnd()) + .filter(Boolean) + .slice(-maxLines); + if (tailLines.length === 0) return ""; + let tail = tailLines.join("\n"); + if (tail.length > maxChars) { + tail = `…${tail.slice(-(maxChars - 1))}`; + } + return tail; +} + +/** + * Classify a failed Copilot attempt into a short, named failure class. + * @param {{ + * hasOutput: boolean, + * isAuthErr?: boolean, + * isAuthenticationFailed?: boolean, + * isCAPIError?: boolean, + * isMCPGatewayShutdown?: boolean, + * isMCPPolicy?: boolean, + * isModelNotSupported?: boolean, + * isNullTypeToolCall?: boolean, + * isQuotaExceeded?: boolean, + * isSDKSessionIdleTimeout?: boolean, + * hasNumerousPermissionDenied?: boolean, + * }} detection + * @returns {string} + */ +function classifyCopilotFailure(detection) { + if (detection.isQuotaExceeded) return "capi_quota_exceeded"; + if (detection.isMCPPolicy) return "mcp_policy_blocked"; + if (detection.isModelNotSupported) return "model_not_supported"; + if (detection.isNullTypeToolCall) return "null_type_tool_call"; + if (detection.isAuthErr) return "no_auth_info"; + if (detection.isAuthenticationFailed) return "authentication_failed"; + if (detection.hasNumerousPermissionDenied) return "permission_denied"; + if (detection.isSDKSessionIdleTimeout) return "sdk_session_idle_timeout"; + if (detection.isMCPGatewayShutdown) return "mcp_gateway_shutdown"; + if (detection.isCAPIError) return "capi_error_400"; + return detection.hasOutput ? "partial_execution" : "no_output"; +} + /** * Extract provider auth failure details from Copilot output when available. * @param {string} output @@ -714,16 +794,35 @@ async function main() { const isAuthenticationFailed = isAuthenticationFailedError(result.output); const proxyAuthDiagnostic = buildCopilotProxyAuthFailureDiagnostic(result.output, process.env); const isNullTypeToolCall = isNullTypeToolCallError(result.output); + const isSDKSessionIdleTimeout = isSDKSessionIdleTimeoutError(result.output); + const isMCPGatewayShutdown = isMCPGatewayShutdownError(result.output); const permissionDeniedCount = countPermissionDeniedIssues(result.output); const hasNumerousPermissionDenied = hasNumerousPermissionDeniedIssues(result.output); + const failureClass = classifyCopilotFailure({ + hasOutput: result.hasOutput, + isAuthErr, + isAuthenticationFailed, + isCAPIError, + isMCPGatewayShutdown, + isMCPPolicy, + isModelNotSupported, + isNullTypeToolCall, + isQuotaExceeded, + isSDKSessionIdleTimeout, + hasNumerousPermissionDenied, + }); + const outputTail = extractOutputTail(result.output); log( `attempt ${attempt + 1} failed:` + ` exitCode=${result.exitCode}` + + ` failureClass=${failureClass}` + ` isCAPIError400=${isCAPIError}` + ` isCAPIQuotaExceededError=${isQuotaExceeded}` + ` isMCPPolicyError=${isMCPPolicy}` + ` isModelNotSupportedError=${isModelNotSupported}` + ` isNullTypeToolCallError=${isNullTypeToolCall}` + + ` isSDKSessionIdleTimeoutError=${isSDKSessionIdleTimeout}` + + ` isMCPGatewayShutdownError=${isMCPGatewayShutdown}` + ` isAuthError=${isAuthErr}` + ` isAuthenticationFailedError=${isAuthenticationFailed}` + ` permissionDeniedCount=${permissionDeniedCount}` + @@ -731,6 +830,9 @@ async function main() { ` hasOutput=${result.hasOutput}` + ` retriesRemaining=${MAX_RETRIES - attempt}` ); + if (outputTail) { + log(`attempt ${attempt + 1}: outputTail=${JSON.stringify(outputTail)}`); + } // If a noop was written to safe-outputs during the failed run, the agent determined // there was nothing to do (or the user indicated so before the agent ran). Retrying @@ -908,11 +1010,15 @@ if (typeof module !== "undefined" && module.exports) { resolveCopilotSDKCustomProviderFromReflect, countPermissionDeniedIssues, detectCopilotErrors, + classifyCopilotFailure, + extractOutputTail, hasNumerousPermissionDeniedIssues, INFERENCE_ACCESS_ERROR_PATTERN, AGENTIC_ENGINE_TIMEOUT_PATTERN, buildMissingToolPermissionIssuePayload, isAuthenticationFailedError, + isMCPGatewayShutdownError, + isSDKSessionIdleTimeoutError, startCopilotSDKServer, stopCopilotSDKServer, waitForCopilotSDKServer, diff --git a/actions/setup/js/copilot_harness.test.cjs b/actions/setup/js/copilot_harness.test.cjs index 23f9d586271..374711a8d3a 100644 --- a/actions/setup/js/copilot_harness.test.cjs +++ b/actions/setup/js/copilot_harness.test.cjs @@ -13,6 +13,7 @@ const { buildCopilotSDKEnv, isCopilotSDKEnabled } = require("./process_runner.cj const { appendSafeOutputLine, buildMissingToolPermissionIssuePayload, + classifyCopilotFailure, buildMissingToolAlternatives, buildInfrastructureIncompletePayload, buildCopilotProxyAuthFailureDiagnostic, @@ -21,6 +22,7 @@ const { detectCopilotErrors, emitInfrastructureIncomplete, emitMissingToolPermissionIssue, + extractOutputTail, extractDeniedCommands, hasNumerousPermissionDeniedIssues, hasNoopInSafeOutputs, @@ -28,6 +30,7 @@ const { AGENTIC_ENGINE_TIMEOUT_PATTERN, isDetectionPhase, isAuthenticationFailedError, + isMCPGatewayShutdownError, isModelAvailableInReflectData, isModelAvailableInReflectFile, resolveCopilotSDKCustomProviderFromReflect, @@ -38,6 +41,7 @@ const { generateCopilotConnectionToken, GEMINI_MODEL_NAME_PREFIX, isCAPIQuotaExceededError, + isSDKSessionIdleTimeoutError, PROMPT_FILE_INLINE_THRESHOLD_BYTES, resolvePromptFileArgs, writeCopilotOutputs, @@ -81,8 +85,9 @@ describe("copilot_harness.cjs", () => { expect(isCAPIQuotaExceededError("CAPIError: 400 Bad Request")).toBe(false); }); - it("does not match generic 429 output without the observed quota-exceeded message", () => { - expect(isCAPIQuotaExceededError("CAPIError: 429 Too Many Requests")).toBe(false); + it("matches Copilot/CAPI 429 Too Many Requests output", () => { + expect(isCAPIQuotaExceededError("CAPIError: 429 Too Many Requests")).toBe(true); + expect(isCAPIQuotaExceededError("Last error: CAPIError: Too Many Requests")).toBe(true); }); it("does not match unrelated errors", () => { @@ -199,6 +204,16 @@ describe("copilot_harness.cjs", () => { expect(shouldRetry(result, 0)).toBe(false); }); + it("does not retry Copilot/CAPI Too Many Requests output", () => { + const result = { + exitCode: 1, + hasOutput: true, + output: "Failed to get response from the AI model; retried 5 times. Last error: CAPIError: Too Many Requests", + }; + + expect(shouldRetry(result, 0)).toBe(false); + }); + it("still retries generic partial-execution errors with output", () => { const result = { exitCode: 1, @@ -239,6 +254,30 @@ describe("copilot_harness.cjs", () => { expect(shouldRetry(result, 1, true, 1)).toBe(false); }); + describe("failure classification helpers", () => { + it("classifies Copilot SDK session.idle timeouts distinctly", () => { + const output = "[copilot-sdk-driver] Timeout after 60000ms waiting for session.idle"; + expect(isSDKSessionIdleTimeoutError(output)).toBe(true); + expect(classifyCopilotFailure({ hasOutput: true, isSDKSessionIdleTimeout: true })).toBe("sdk_session_idle_timeout"); + }); + + it("classifies MCP gateway shutdown distinctly when present in output", () => { + const output = 'Response: {"message":"Gateway shutdown initiated","serversTerminated":2,"status":"closed"}'; + expect(isMCPGatewayShutdownError(output)).toBe(true); + expect(classifyCopilotFailure({ hasOutput: true, isMCPGatewayShutdown: true })).toBe("mcp_gateway_shutdown"); + }); + + it("extracts a compact tail preview from large output", () => { + const tail = extractOutputTail(["line 1", "line 2", "line 3", "line 4"].join("\n"), { maxLines: 2, maxChars: 20 }); + expect(tail).toBe("line 3\nline 4"); + }); + + it("truncates very large output tails from the front", () => { + const tail = extractOutputTail(`prefix\n${"x".repeat(40)}`, { maxLines: 5, maxChars: 16 }); + expect(tail).toBe(`…${"x".repeat(15)}`); + }); + }); + it("does not claim a retry when already at max retry attempt", () => { const result = { exitCode: 2, hasOutput: false }; expect(shouldRetry(result, MAX_RETRIES, true, 0)).toBe(false); diff --git a/actions/setup/js/detect_agent_errors.cjs b/actions/setup/js/detect_agent_errors.cjs index c38099221c5..bd4a4efb7e4 100644 --- a/actions/setup/js/detect_agent_errors.cjs +++ b/actions/setup/js/detect_agent_errors.cjs @@ -60,7 +60,7 @@ const MODEL_NOT_SUPPORTED_PATTERN = // Pattern: Copilot/CAPI quota exhaustion. // Matches the observed error: "CAPIError: 429 429 quota exceeded". // Quota exhaustion is a persistent, non-retryable condition. -const CAPI_QUOTA_EXCEEDED_PATTERN = /CAPIError:\s*429\s+429\s+quota exceeded/i; +const CAPI_QUOTA_EXCEEDED_PATTERN = /CAPIError:\s*(?:429\s+)?(?:429\s+quota exceeded|Too Many Requests)/i; /** * Determines if the collected output contains the observed Copilot/CAPI quota exhaustion error. diff --git a/actions/setup/js/detect_agent_errors.test.cjs b/actions/setup/js/detect_agent_errors.test.cjs index ee50ef4d28c..fd706389082 100644 --- a/actions/setup/js/detect_agent_errors.test.cjs +++ b/actions/setup/js/detect_agent_errors.test.cjs @@ -146,8 +146,9 @@ describe("detect_agent_errors.cjs", () => { expect(isCAPIQuotaExceededError("CAPIError: 429 429 QUOTA EXCEEDED")).toBe(true); }); - it("does not match other CAPIError 429 messages", () => { - expect(isCAPIQuotaExceededError("CAPIError: 429 Too Many Requests")).toBe(false); + it("matches Copilot/CAPI Too Many Requests output", () => { + expect(isCAPIQuotaExceededError("CAPIError: 429 Too Many Requests")).toBe(true); + expect(isCAPIQuotaExceededError("Last error: CAPIError: Too Many Requests")).toBe(true); }); it("does not match CAPIError 400", () => { From 4aa981ce020f146e9b2738c0a6b572f7bd37df89 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Jun 2026 06:40:54 +0000 Subject: [PATCH 3/5] Address review feedback: clarify CAPI quota comment and rename isCAPIError to isTransientCAPIError Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/copilot_harness.cjs | 6 +++--- actions/setup/js/detect_agent_errors.cjs | 4 +++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/actions/setup/js/copilot_harness.cjs b/actions/setup/js/copilot_harness.cjs index b735b7e9212..364985df2f8 100644 --- a/actions/setup/js/copilot_harness.cjs +++ b/actions/setup/js/copilot_harness.cjs @@ -315,7 +315,7 @@ function extractOutputTail(output, options) { * hasOutput: boolean, * isAuthErr?: boolean, * isAuthenticationFailed?: boolean, - * isCAPIError?: boolean, + * isTransientCAPIError?: boolean, * isMCPGatewayShutdown?: boolean, * isMCPPolicy?: boolean, * isModelNotSupported?: boolean, @@ -336,7 +336,7 @@ function classifyCopilotFailure(detection) { if (detection.hasNumerousPermissionDenied) return "permission_denied"; if (detection.isSDKSessionIdleTimeout) return "sdk_session_idle_timeout"; if (detection.isMCPGatewayShutdown) return "mcp_gateway_shutdown"; - if (detection.isCAPIError) return "capi_error_400"; + if (detection.isTransientCAPIError) return "capi_error_400"; return detection.hasOutput ? "partial_execution" : "no_output"; } @@ -802,7 +802,7 @@ async function main() { hasOutput: result.hasOutput, isAuthErr, isAuthenticationFailed, - isCAPIError, + isTransientCAPIError: isCAPIError, isMCPGatewayShutdown, isMCPPolicy, isModelNotSupported, diff --git a/actions/setup/js/detect_agent_errors.cjs b/actions/setup/js/detect_agent_errors.cjs index bd4a4efb7e4..297e2379e7c 100644 --- a/actions/setup/js/detect_agent_errors.cjs +++ b/actions/setup/js/detect_agent_errors.cjs @@ -58,7 +58,9 @@ const MODEL_NOT_SUPPORTED_PATTERN = /(?:The requested model is not supported|invalid model(?:\s+name)?\s+['"`]?[a-z0-9._:/@-]+['"`]?(?=(?:\s*$|\s*[\n\r.,;:!?)]))|unknown model\s+['"`]?[a-z0-9._:/@-]+['"`]?(?=(?:\s*$|\s*[\n\r.,;:!?)]))|model(?:\s+name)?\s+['"`]?[a-z0-9._:/@-]+['"`]?\s+(?:is\s+)?(?:not found|does not exist|not supported|not available|unavailable))/i; // Pattern: Copilot/CAPI quota exhaustion. -// Matches the observed error: "CAPIError: 429 429 quota exceeded". +// Matches observed errors: +// - "CAPIError: 429 429 quota exceeded" +// - "CAPIError: Too Many Requests" // Quota exhaustion is a persistent, non-retryable condition. const CAPI_QUOTA_EXCEEDED_PATTERN = /CAPIError:\s*(?:429\s+)?(?:429\s+quota exceeded|Too Many Requests)/i; From 502d97b528975a99eeead172b74f7eb0733edf66 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Jun 2026 11:25:20 +0000 Subject: [PATCH 4/5] Address GitHub Actions bot review: tighten MCP pattern, reorder failure priority, fix bare-CR/slice-0, improve comments, add regression tests Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/copilot_harness.cjs | 13 ++++-- actions/setup/js/copilot_harness.test.cjs | 49 +++++++++++++++++++++++ actions/setup/js/detect_agent_errors.cjs | 17 +++++--- 3 files changed, 69 insertions(+), 10 deletions(-) diff --git a/actions/setup/js/copilot_harness.cjs b/actions/setup/js/copilot_harness.cjs index 364985df2f8..1ad69fcd398 100644 --- a/actions/setup/js/copilot_harness.cjs +++ b/actions/setup/js/copilot_harness.cjs @@ -117,7 +117,10 @@ const AGENTIC_ENGINE_TIMEOUT_PATTERN = /signal=SIG(?:TERM|KILL|INT)/; // Pattern: Copilot SDK driver timed out waiting for the session to become idle. const SDK_SESSION_IDLE_TIMEOUT_PATTERN = /Timeout after \d+ms waiting for session\.idle/; // Pattern: MCP gateway shutdown surfaced in agent output. -const MCP_GATEWAY_SHUTDOWN_PATTERN = /Gateway shutdown initiated/; +// Anchored to the JSON "message" key emitted by the MCP gateway driver to +// avoid false positives from any process that logs "Gateway shutdown initiated" +// as plain text. +const MCP_GATEWAY_SHUTDOWN_PATTERN = /"message"\s*:\s*"Gateway shutdown initiated"/; // Pattern to detect null-type tool_call error that poisons conversation history. // Matches the Copilot API 400 error: @@ -294,8 +297,9 @@ function extractOutputTail(output, options) { if (typeof output !== "string" || !output) return ""; const maxChars = options?.maxChars ?? OUTPUT_TAIL_MAX_CHARS; const maxLines = options?.maxLines ?? OUTPUT_TAIL_MAX_LINES; - const normalized = output.replace(/\0/g, "").replace(/\r\n/g, "\n").trim(); + const normalized = output.replace(/\0/g, "").replace(/\r\n/g, "\n").replace(/\r/g, "\n").trim(); if (!normalized) return ""; + // filter(Boolean) removes blank lines; maxLines therefore counts non-empty lines. const tailLines = normalized .split("\n") .map(line => line.trimEnd()) @@ -304,7 +308,8 @@ function extractOutputTail(output, options) { if (tailLines.length === 0) return ""; let tail = tailLines.join("\n"); if (tail.length > maxChars) { - tail = `…${tail.slice(-(maxChars - 1))}`; + const keep = maxChars - 1; + tail = keep > 0 ? `…${tail.slice(-keep)}` : "…"; } return tail; } @@ -333,9 +338,9 @@ function classifyCopilotFailure(detection) { if (detection.isNullTypeToolCall) return "null_type_tool_call"; if (detection.isAuthErr) return "no_auth_info"; if (detection.isAuthenticationFailed) return "authentication_failed"; - if (detection.hasNumerousPermissionDenied) return "permission_denied"; if (detection.isSDKSessionIdleTimeout) return "sdk_session_idle_timeout"; if (detection.isMCPGatewayShutdown) return "mcp_gateway_shutdown"; + if (detection.hasNumerousPermissionDenied) return "permission_denied"; if (detection.isTransientCAPIError) return "capi_error_400"; return detection.hasOutput ? "partial_execution" : "no_output"; } diff --git a/actions/setup/js/copilot_harness.test.cjs b/actions/setup/js/copilot_harness.test.cjs index 374711a8d3a..29ec757029d 100644 --- a/actions/setup/js/copilot_harness.test.cjs +++ b/actions/setup/js/copilot_harness.test.cjs @@ -267,6 +267,55 @@ describe("copilot_harness.cjs", () => { expect(classifyCopilotFailure({ hasOutput: true, isMCPGatewayShutdown: true })).toBe("mcp_gateway_shutdown"); }); + it("sdk_session_idle_timeout outranks permission_denied in failure classification", () => { + // Both flags set — the more specific signal must win. + expect(classifyCopilotFailure({ hasOutput: true, isSDKSessionIdleTimeout: true, hasNumerousPermissionDenied: true })).toBe("sdk_session_idle_timeout"); + }); + + it("mcp_gateway_shutdown outranks permission_denied in failure classification", () => { + // Both flags set — the more specific signal must win. + expect(classifyCopilotFailure({ hasOutput: true, isMCPGatewayShutdown: true, hasNumerousPermissionDenied: true })).toBe("mcp_gateway_shutdown"); + }); + + it("retries sdk_session_idle_timeout as partial execution (shouldRetry)", () => { + // sdk_session_idle_timeout is not a quota/permission blocker; the harness should retry. + const result = { + exitCode: 1, + hasOutput: true, + output: "[copilot-sdk-driver] Timeout after 60000ms waiting for session.idle", + }; + const MAX_RETRIES = 3; + const shouldRetryLocal = (r, attempt) => { + if (r.exitCode === 0) return false; + if (hasNumerousPermissionDeniedIssues(r.output)) return false; + if (isCAPIQuotaExceededError(r.output)) return false; + return attempt < MAX_RETRIES && r.hasOutput; + }; + expect(shouldRetryLocal(result, 0)).toBe(true); + }); + + it("retries mcp_gateway_shutdown as partial execution (shouldRetry)", () => { + // mcp_gateway_shutdown is not a quota/permission blocker; the harness should retry. + const result = { + exitCode: 1, + hasOutput: true, + output: '{"message":"Gateway shutdown initiated","serversTerminated":1,"status":"closed"}', + }; + const MAX_RETRIES = 3; + const shouldRetryLocal = (r, attempt) => { + if (r.exitCode === 0) return false; + if (hasNumerousPermissionDeniedIssues(r.output)) return false; + if (isCAPIQuotaExceededError(r.output)) return false; + return attempt < MAX_RETRIES && r.hasOutput; + }; + expect(shouldRetryLocal(result, 0)).toBe(true); + }); + + it("extractOutputTail never exceeds maxChars even when maxChars is 1", () => { + const tail = extractOutputTail("abc", { maxLines: 5, maxChars: 1 }); + expect(tail.length).toBeLessThanOrEqual(1); + }); + it("extracts a compact tail preview from large output", () => { const tail = extractOutputTail(["line 1", "line 2", "line 3", "line 4"].join("\n"), { maxLines: 2, maxChars: 20 }); expect(tail).toBe("line 3\nline 4"); diff --git a/actions/setup/js/detect_agent_errors.cjs b/actions/setup/js/detect_agent_errors.cjs index 297e2379e7c..b1f9b33cb0e 100644 --- a/actions/setup/js/detect_agent_errors.cjs +++ b/actions/setup/js/detect_agent_errors.cjs @@ -17,7 +17,10 @@ * for the selected engine/account (for example unknown model name, model not * found, or model unavailable for the plan). * - capi_quota_exceeded_error: The Copilot CAPI quota has been exhausted - * (e.g., "CAPIError: 429 429 quota exceeded"). + * or rate-limited (e.g., "CAPIError: 429 429 quota exceeded", + * "CAPIError: Too Many Requests"). All matched forms are treated as + * non-retryable because the Copilot SDK has already retried internally + * before surfacing the error. * * This replaces the individual bash scripts (detect_inference_access_error.sh, * detect_mcp_policy_error.sh) with a single JavaScript step. @@ -57,11 +60,13 @@ const AGENTIC_ENGINE_TIMEOUT_PATTERN = /signal=SIG(?:TERM|KILL|INT)/; const MODEL_NOT_SUPPORTED_PATTERN = /(?:The requested model is not supported|invalid model(?:\s+name)?\s+['"`]?[a-z0-9._:/@-]+['"`]?(?=(?:\s*$|\s*[\n\r.,;:!?)]))|unknown model\s+['"`]?[a-z0-9._:/@-]+['"`]?(?=(?:\s*$|\s*[\n\r.,;:!?)]))|model(?:\s+name)?\s+['"`]?[a-z0-9._:/@-]+['"`]?\s+(?:is\s+)?(?:not found|does not exist|not supported|not available|unavailable))/i; -// Pattern: Copilot/CAPI quota exhaustion. -// Matches observed errors: -// - "CAPIError: 429 429 quota exceeded" -// - "CAPIError: Too Many Requests" -// Quota exhaustion is a persistent, non-retryable condition. +// Pattern: Copilot/CAPI quota exhaustion and rate-limit responses. +// Matches all observed forms: +// "CAPIError: 429 429 quota exceeded" (original observed form) +// "CAPIError: 429 Too Many Requests" (HTTP 429 form) +// "CAPIError: Too Many Requests" (no status code in message) +// All forms are treated as non-retryable; the Copilot SDK has already retried +// internally before surfacing this error (evidenced by "retried 5 times" context). const CAPI_QUOTA_EXCEEDED_PATTERN = /CAPIError:\s*(?:429\s+)?(?:429\s+quota exceeded|Too Many Requests)/i; /** From d83ab10f3548850eca4fc0aa3c88711c008d87d9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Jun 2026 11:26:17 +0000 Subject: [PATCH 5/5] Clarify filter(Boolean) comment in extractOutputTail Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/copilot_harness.cjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actions/setup/js/copilot_harness.cjs b/actions/setup/js/copilot_harness.cjs index 1ad69fcd398..ddbb8fee9db 100644 --- a/actions/setup/js/copilot_harness.cjs +++ b/actions/setup/js/copilot_harness.cjs @@ -299,7 +299,7 @@ function extractOutputTail(output, options) { const maxLines = options?.maxLines ?? OUTPUT_TAIL_MAX_LINES; const normalized = output.replace(/\0/g, "").replace(/\r\n/g, "\n").replace(/\r/g, "\n").trim(); if (!normalized) return ""; - // filter(Boolean) removes blank lines; maxLines therefore counts non-empty lines. + // filter(Boolean) removes empty strings from blank lines after trimEnd(); maxLines therefore counts non-empty lines. const tailLines = normalized .split("\n") .map(line => line.trimEnd())