Skip to content
26 changes: 14 additions & 12 deletions actions/setup/js/detect_agent_errors.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@
* access to inference (e.g., "Access denied by policy settings").
* - mcp_policy_error: MCP servers were blocked by enterprise/organization
* policy (e.g., "MCP servers were blocked by policy: 'github', 'safeoutputs'").
* - agentic_engine_timeout: The agentic engine process was killed by a
* signal (SIGTERM/SIGKILL/SIGINT), typically due to the step
* timeout-minutes limit being reached.
* - agentic_engine_timeout: A timeout signature was detected in engine logs.
* This includes process termination by signal (SIGTERM/SIGKILL/SIGINT),
* typically due to step timeout-minutes, and SDK idle-timeout messages
* ("Timeout after <n>ms waiting for session.idle").
* - model_not_supported_error: The configured model is invalid or unsupported
* for the selected engine/account (for example unknown model name, model not
* found, or model unavailable for the plan).
Expand Down Expand Up @@ -43,14 +44,15 @@ const INFERENCE_ACCESS_ERROR_PATTERN = /Access denied by policy settings|invalid
// Pattern: MCP servers blocked by enterprise/organization policy
const MCP_POLICY_BLOCKED_PATTERN = /MCP servers were blocked by policy:/;

// Pattern: Agentic engine process killed by signal (timeout).
// When GitHub Actions cancels a step due to timeout-minutes, the runner sends
// SIGINT/SIGTERM/SIGKILL to the process group. The copilot_harness.cjs (and
// other engine wrappers) log the signal in their close handlers:
// [copilot-harness] attempt 1: process closed exitCode=1 signal=SIGTERM ...
// The pattern matches any "signal=SIG(TERM|KILL|INT)" occurrence in the log,
// making it engine-agnostic.
const AGENTIC_ENGINE_TIMEOUT_PATTERN = /signal=SIG(?:TERM|KILL|INT)/;
// Pattern: Agentic engine timeout.
// Covers both timeout signatures observed in engine logs:
// 1) Process killed by signal after step timeout-minutes:
// [copilot-harness] ... process closed exitCode=1 signal=SIGTERM ...
// 2) Copilot SDK idle-timeout while waiting for session.idle:
// [sdk-driver] error: Timeout after 870000ms waiting for session.idle
// The second form can occur even when the driver collected output, and should
// still be classified as a timeout for conclusion/reporting purposes.
const AGENTIC_ENGINE_TIMEOUT_PATTERN = /(?:signal=SIG(?:TERM|KILL|INT)|Timeout after \d+ms waiting for session\.idle)/;
Comment on lines +47 to +55

// Pattern: Configured model is invalid or unavailable.
// Covers common engine/provider variants:
Expand Down Expand Up @@ -148,7 +150,7 @@ function main() {
process.stderr.write("[detect-agent-errors] Detected MCP policy error in agent log\n");
}
if (results.agenticEngineTimeout) {
process.stderr.write("[detect-agent-errors] Detected timeout: engine process was killed by signal (step timeout-minutes likely exceeded)\n");
process.stderr.write("[detect-agent-errors] Detected agentic engine timeout signature in agent log\n");
}
if (results.modelNotSupportedError) {
process.stderr.write("[detect-agent-errors] Detected model configuration error: configured model is invalid or unavailable for this engine/account\n");
Expand Down
32 changes: 32 additions & 0 deletions actions/setup/js/detect_agent_errors.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,17 @@ describe("detect_agent_errors.cjs", () => {
expect(AGENTIC_ENGINE_TIMEOUT_PATTERN.test(log)).toBe(true);
});

it("matches SDK session.idle timeout signature", () => {
const log = "[copilot-sdk-driver] [sdk-driver] error: Timeout after 870000ms waiting for session.idle";
expect(AGENTIC_ENGINE_TIMEOUT_PATTERN.test(log)).toBe(true);
});

it("does not match timeouts waiting for states other than session.idle", () => {
expect(AGENTIC_ENGINE_TIMEOUT_PATTERN.test("Timeout after 5000ms waiting for session.connected")).toBe(false);
expect(AGENTIC_ENGINE_TIMEOUT_PATTERN.test("Timeout after 5000ms waiting for session_idle")).toBe(false);
expect(AGENTIC_ENGINE_TIMEOUT_PATTERN.test("Timeout after 5000ms waiting for network response")).toBe(false);
});

it("does not match regular exit without signal", () => {
const log = "[copilot-harness] 2026-04-12T04:56:28.000Z attempt 1: process closed exitCode=1 duration=5m 3s stdout=1234B stderr=567B hasOutput=true";
expect(AGENTIC_ENGINE_TIMEOUT_PATTERN.test(log)).toBe(false);
Expand Down Expand Up @@ -321,6 +332,27 @@ describe("detect_agent_errors.cjs", () => {
expect(result.capiQuotaExceededError).toBe(false);
});

it("detects SDK session.idle timeout", () => {
const result = detectErrors("[copilot-sdk-driver] [sdk-driver] error: Timeout after 870000ms waiting for session.idle");
expect(result.inferenceAccessError).toBe(false);
expect(result.mcpPolicyError).toBe(false);
expect(result.agenticEngineTimeout).toBe(true);
expect(result.modelNotSupportedError).toBe(false);
expect(result.http400ResponseError).toBe(false);
expect(result.capiQuotaExceededError).toBe(false);
});

it("detects SDK session.idle timeout alongside other errors", () => {
const log = "Access denied by policy settings\n[sdk-driver] info: retrying request\n[copilot-sdk-driver] error: Timeout after 870000ms waiting for session.idle";
const result = detectErrors(log);
expect(result.inferenceAccessError).toBe(true);
expect(result.mcpPolicyError).toBe(false);
expect(result.agenticEngineTimeout).toBe(true);
expect(result.modelNotSupportedError).toBe(false);
expect(result.http400ResponseError).toBe(false);
expect(result.capiQuotaExceededError).toBe(false);
});

it("returns false for unrelated log content", () => {
const result = detectErrors("CAPIError: 400 Bad Request\nSome normal output");
expect(result.inferenceAccessError).toBe(false);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# ADR-43413: Classify SDK Session-Idle Timeouts as Agentic Engine Timeout

**Date**: 2026-07-05
**Status**: Accepted
**Deciders**: @pelikhan, @copilot

---

### Context

The `detect_agent_errors` module (`actions/setup/js/detect_agent_errors.cjs`) classifies Copilot engine log output into typed error categories consumed by downstream workflows via `steps.detect-agent-errors.outputs.agentic_engine_timeout`. The original `AGENTIC_ENGINE_TIMEOUT_PATTERN` only matched signal-based termination (`signal=SIGTERM|SIGKILL|SIGINT`), which occurs when GitHub Actions' `timeout-minutes` cancels a step. A second timeout form—the Copilot SDK emitting `Timeout after <n>ms waiting for session.idle` during long runs—was not matched, causing those failures to go unclassified and skewing failure accounting in all consuming workflows.

### Decision

We will extend `AGENTIC_ENGINE_TIMEOUT_PATTERN` with an alternation to also match the SDK idle-timeout log signature (`Timeout after \d+ms waiting for session.idle`). Both timeout signatures are unified in a single regex so all timeout forms produce `agenticEngineTimeout: true` without introducing a new output flag or secondary detection path. The comment block above the constant is updated to document both covered cases.

### Alternatives Considered

#### Alternative 1: Add a Separate `sdkIdleTimeout` Output Flag

Introduce a distinct `sdkIdleTimeout` boolean alongside `agenticEngineTimeout`, keeping the two timeout kinds separately classifiable by consumers. This would allow finer-grained downstream handling but requires every consuming workflow to add a new condition for a case that semantically is still "the run timed out," adding consumer coupling with no practical benefit for current use cases.

#### Alternative 2: Handle Idle-Timeout Upstream in the SDK Driver

Remap the SDK's `session.idle` error to a signal exit before it reaches the log output seen by `detect_agent_errors` (e.g., by wrapping it in the driver's close handler). This avoids changing the classifier but pushes responsibility into the engine driver layer, is harder to test in isolation, and would not retroactively fix the misclassification in logs already emitted by deployed versions of the driver.

### Consequences

#### Positive
- Downstream workflows consuming `agentic_engine_timeout` now receive the correct flag for SDK idle-timeout runs, eliminating silent misclassification.
- Timeout detection remains centralized in a single module; no consuming workflow changes are required.

#### Negative
- The regex alternation slightly increases pattern complexity; reviewers must understand both timeout forms to audit the pattern correctly.
- If the Copilot SDK changes the format of its idle-timeout message in a future version, the pattern will silently stop matching, requiring ongoing maintenance awareness.

#### Neutral
- Regression tests are added for both the direct `AGENTIC_ENGINE_TIMEOUT_PATTERN.test()` match and the `detectErrors()` return value, establishing explicit coverage for this timeout signature.

---
Loading