Skip to content

Fail fast on empty safeoutputs schema and add Copilot soft-time convergence guard#43057

Merged
pelikhan merged 10 commits into
mainfrom
copilot/aw-failures-fix-15min-timeout
Jul 3, 2026
Merged

Fail fast on empty safeoutputs schema and add Copilot soft-time convergence guard#43057
pelikhan merged 10 commits into
mainfrom
copilot/aw-failures-fix-15min-timeout

Conversation

Copilot AI commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Scheduled Copilot runs were hitting the 15-minute hard step timeout after thrashing, often without emitting any safe-output. A key contributor was intermittent empty safeoutputs tool schema resolution, which left the agent unable to discover safe-output tools and spiraling into retry loops.

  • Bridge/schema hardening (safeoutputs)

    • Added explicit non-empty schema enforcement for safeoutputs in MCP CLI mounting and bridge startup paths.
    • If MCP tools/list returns empty for safeoutputs, the code now re-derives from GH_AW_SAFE_OUTPUTS_TOOLS_PATH (safeoutputs/tools.json).
    • If still empty, execution fails early with a targeted error instead of allowing a silent degraded run.
  • Copilot convergence guard before hard timeout

    • Added a soft-time budget in copilot_harness.cjs derived from GH_AW_TIMEOUT_MINUTES with a pre-timeout buffer.
    • Retry loop exits early once soft budget is reached and emits report_incomplete (reason: infrastructure_error) so runs degrade explicitly instead of being killed by Actions hard timeout.
  • Failure signal quality

    • Empty-safeoutputs-schema now produces a clear startup failure condition.
    • Timeout-adjacent thrash now yields structured incomplete output rather than losing the run without safe-output artifacts.
// Soft guard (before 15m hard cap)
const softTimeoutGuard = buildSoftTimeoutGuard(driverStartTime);
if (softTimeoutGuard && Date.now() >= softTimeoutGuard.softDeadlineMs) {
  emitInfrastructureIncomplete(
    `Copilot harness reached soft retry budget before the ${softTimeoutGuard.timeoutMinutes}-minute step timeout.`
  );
  lastExitCode = 1;
  break;
}

Generated by 👨‍🍳 PR Sous Chef · 6.92 AIC · ⌖ 15.9 AIC · ⊞ 6.4K ·


Generated by 👨‍🍳 PR Sous Chef · 6.07 AIC · ⌖ 7.49 AIC · ⊞ 6.4K ·

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix Copilot CLI 15-min hard timeout issue Fail fast on empty safeoutputs schema and add Copilot soft-time convergence guard Jul 3, 2026
Copilot AI requested a review from pelikhan July 3, 2026 00:25
@pelikhan pelikhan marked this pull request as ready for review July 3, 2026 00:26
Copilot AI review requested due to automatic review settings July 3, 2026 00:26
@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Design Decision Gate 🏗️ completed the design decision gate check.

No ADR enforcement needed: PR does not have the 'implementation' label and has ≤100 new lines of code in business logic directories.

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

⚠️ PR Code Quality Reviewer failed during code quality review.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Hardens scheduled agent runs by (1) preventing silent degraded execution when the safeoutputs tool schema is unexpectedly empty and (2) adding a Copilot harness soft-time budget to stop retry thrash early and emit structured report_incomplete output before the GitHub Actions hard timeout.

Changes:

  • Enforce non-empty safeoutputs schema when mounting MCP servers as CLIs, with fallback recovery from GH_AW_SAFE_OUTPUTS_TOOLS_PATH / generated safeoutputs/tools.json.
  • Enforce non-empty safeoutputs schema in the CLI bridge help-path (fail fast if tools remain empty).
  • Add a Copilot harness soft timeout guard derived from GH_AW_TIMEOUT_MINUTES, with tests for deadline computation.
Show a summary per file
File Description
actions/setup/js/mount_mcp_as_cli.cjs Adds safeoutputs empty-schema recovery + fail-fast behavior when tools remain empty.
actions/setup/js/mount_mcp_as_cli.test.cjs Adds unit tests for safeoutputs fallback recovery and fail-fast behavior.
actions/setup/js/mcp_cli_bridge.cjs Ensures safeoutputs CLI has a non-empty tools schema (fallback + fail-fast).
actions/setup/js/mcp_cli_bridge.test.cjs Adds unit tests covering safeoutputs schema fallback and fail-fast behavior.
actions/setup/js/copilot_harness.cjs Adds a soft timeout guard to exit retry loops early and emit structured incomplete output.
actions/setup/js/copilot_harness.test.cjs Adds tests validating soft timeout guard behavior and deadline calculation.

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 6/6 changed files
  • Comments generated: 1
  • Review effort level: Low

Comment thread actions/setup/js/mcp_cli_bridge.cjs Outdated
Comment on lines +848 to +856
const core = global.core;
const fallbackPath = process.env.GH_AW_SAFE_OUTPUTS_TOOLS_PATH || `${process.env.RUNNER_TEMP}/gh-aw/safeoutputs/tools.json`;
if (fallbackPath && fallbackPath !== toolsFile) {
const fallbackTools = loadTools(fallbackPath);
if (fallbackTools.length > 0) {
core.warning(`[${serverName}] tools cache ${toolsFile} is empty; recovered ${fallbackTools.length} tool(s) from ${fallbackPath}`);
return fallbackTools;
}
}
@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 95/100 — Excellent

Analyzed 6 test(s): 6 design, 0 implementation, 0 violation(s).

📊 Metrics (6 tests)
Metric Value
Analyzed 6 (Go: 0, JS: 6)
✅ Design 6 (100%)
⚠️ Implementation 0 (0%)
Edge/error coverage 5 (83%)
Duplicate clusters 0
Inflation No
🚨 Violations 0
Test File Classification Issues
"returns null when GH_AW_TIMEOUT_MINUTES is missing" copilot_harness.test.cjs design_test / high_value None
"computes a deadline before hard timeout" copilot_harness.test.cjs design_test / high_value Slightly implementation-coupled (hardcodes computed value 820000), but verifies correctness of formula
"recovers empty safeoutputs schema from fallback tools path" mcp_cli_bridge.test.cjs design_test / high_value None
"fails fast when safeoutputs schema is empty" mcp_cli_bridge.test.cjs design_test / high_value None
"recovers empty safeoutputs tools from fallback tools.json" mount_mcp_as_cli.test.cjs design_test / high_value None
"throws when safeoutputs tools remain empty after fallback" mount_mcp_as_cli.test.cjs design_test / high_value None

Verdict

Passed. 0% implementation tests (threshold: 30%). All 6 tests verify user-visible behavioral contracts: soft-timeout guard null-return, deadline computation, safeoutputs schema fail-fast, and fallback recovery with observable side effects (warning emission, thrown error, recovered tool list).

Inflation check: test:prod ratios — copilot_harness 0.44, mcp_cli_bridge 1.24, mount_mcp_as_cli 0.66 — all well below 2:1. ✅

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • awmgmcpg

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "awmgmcpg"

See Network Configuration for more information.

🧪 Test quality analysis by Test Quality Sentinel · 40.9 AIC · ⌖ 15.5 AIC · ⊞ 6.8K ·
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Test Quality Sentinel: 95/100. 0% implementation tests (threshold: 30%). All 6 tests verify behavioral contracts with no violations.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skills-Based Review 🧠

Applied /diagnosing-bugs and /tdd — commenting on testability gaps and a few defensive-coding improvements. No blocking issues; the overall direction is solid.

📋 Key Themes & Highlights

Key Themes

  • Test coverage gaps (most important): buildSoftTimeoutGuard tests use a hardcoded magic number and miss edge cases (0, NaN, sub-buffer timeouts). The loop-level behaviour (guard fires → lastExitCode=1emitInfrastructureIncomplete) is tested only indirectly by the existing harness tests.
  • global.core coupling in mcp_cli_bridge.cjs: Unlike mount_mcp_as_cli.cjs, ensureSafeOutputsTools in the bridge uses global.core without a null guard. This makes it harder to test and can produce a cryptic TypeError in unexpected environments.
  • RUNNER_TEMP not guarded in bridge fallback path: If the env var is unset, the fallback path becomes 'undefined/... and fails silently rather than with an actionable error.
  • Duplicate emitInfrastructureIncomplete call-sites: Two identical message strings; easy to diverge on future edits.

Positive Highlights

  • ✅ Excellent fail-fast semantics: errors are thrown rather than swallowing failures silently
  • ✅ Two-layer recovery strategy (MCP response empty → fallback file → hard error) is clean and progressive
  • ✅ Both new modules export their helpers for testing, making them properly testable
  • mount_mcp_as_cli.cjs already follows the preferred pattern of injecting core as a parameter — worth bringing mcp_cli_bridge.cjs into line
  • ✅ PR description is thorough and links each code change to a concrete observed failure mode

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · 112.3 AIC · ⌖ 8.16 AIC · ⊞ 6.6K
Comment /matt to run again

Comment thread actions/setup/js/mcp_cli_bridge.cjs Outdated
if (serverName !== SAFEOUTPUTS_SERVER_NAME || tools.length > 0) {
return tools;
}
const core = global.core;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/diagnosing-bugs] global.core is accessed without a null guard; if it is undefined the recovery-warning path throws a cryptic TypeError instead of the intended fail-fast error.

💡 Suggestion

Consider accepting core as an injected parameter — matching the pattern in mount_mcp_as_cli.cjs — rather than accessing global.core directly:

function ensureSafeOutputsTools(tools, serverName, toolsFile, core = global.core) {
  if (serverName !== SAFEOUTPUTS_SERVER_NAME || tools.length > 0) return tools;
  if (!core) throw new Error('[' + serverName + '] global.core is not set');
  // ...
}

This also makes the function independently testable without mutating global.

@copilot please address this.

// Unified retry loop for CLI and driver modes.
// --continue is a CLI concept; in SDK mode retries always restart the session fresh.
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
if (softTimeoutGuard && Date.now() >= softTimeoutGuard.softDeadlineMs) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/diagnosing-bugs] The soft-timeout guard is checked at loop entry and after backoff sleep, but not during a long-running attempt — if a single Copilot invocation runs past the soft deadline, the guard will only fire on the next iteration, leaving fewer than 90 seconds to emit safe-output.

💡 Suggestion

If Copilot invocations can individually outlast the buffer, consider passing an AbortSignal that fires at softDeadlineMs to the subprocess spawn (or using a Promise.race), so the guard can preempt a running attempt rather than waiting for it to complete.

This is an inherent limitation of a polling guard; document the assumption that individual attempts complete within the buffer window.

@copilot please address this.

const guard = buildSoftTimeoutGuard(10_000, { GH_AW_TIMEOUT_MINUTES: "15" });
expect(guard).toEqual({
timeoutMinutes: 15,
softDeadlineMs: 820000,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] The test hardcodes softDeadlineMs: 820000 — a magic number derived from 10_000 + (15*60*1000 - 90*1000). If SOFT_TIMEOUT_BUFFER_MS is changed the test will break silently with a misleading failure message.

💡 Suggestion

Express the expected value using the same arithmetic so the test documents intent and tracks refactors automatically:

const EXPECTED_BUFFER_MS = 90 * 1000; // matches SOFT_TIMEOUT_BUFFER_MS
const guard = buildSoftTimeoutGuard(10_000, { GH_AW_TIMEOUT_MINUTES: '15' });
expect(guard.softDeadlineMs).toBe(10_000 + 15 * 60 * 1000 - EXPECTED_BUFFER_MS);

@copilot please address this.


describe("soft timeout guard", () => {
it("returns null when GH_AW_TIMEOUT_MINUTES is missing", () => {
expect(buildSoftTimeoutGuard(1_000, {})).toBeNull();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] The test suite for buildSoftTimeoutGuard is missing edge cases that could cause hard-to-diagnose field failures: GH_AW_TIMEOUT_MINUTES=0, a non-numeric string (e.g. 'NaN'), and a value so small that hardTimeoutMs - SOFT_TIMEOUT_BUFFER_MS < 1000 (the Math.max floor).

💡 Missing tests to add
it('returns null for GH_AW_TIMEOUT_MINUTES=0', () => {
  expect(buildSoftTimeoutGuard(0, { GH_AW_TIMEOUT_MINUTES: '0' })).toBeNull();
});

it('returns null for non-numeric GH_AW_TIMEOUT_MINUTES', () => {
  expect(buildSoftTimeoutGuard(0, { GH_AW_TIMEOUT_MINUTES: 'NaN' })).toBeNull();
});

it('floors softDeadlineMs to at least 1000ms after start when timeout < buffer', () => {
  // 1 minute = 60000ms < SOFT_TIMEOUT_BUFFER_MS (90000ms) → Math.max floor
  const guard = buildSoftTimeoutGuard(0, { GH_AW_TIMEOUT_MINUTES: '1' });
  expect(guard.softDeadlineMs).toBe(1000);
});

@copilot please address this.

Comment thread actions/setup/js/mcp_cli_bridge.cjs Outdated
}
const core = global.core;
const fallbackPath = process.env.GH_AW_SAFE_OUTPUTS_TOOLS_PATH || `${process.env.RUNNER_TEMP}/gh-aw/safeoutputs/tools.json`;
if (fallbackPath && fallbackPath !== toolsFile) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/diagnosing-bugs] The condition if (fallbackPath && fallbackPath !== toolsFile) silently skips recovery when the fallback resolves to the same path as the primary tools file — but if the primary file was empty, the fallback would also be empty, so the skip is correct. However this also means when RUNNER_TEMP is unset, fallbackPath could become 'undefined/gh-aw/safeoutputs/tools.json', which will silently fail to load rather than failing fast.

💡 Suggestion

Guard against an unset RUNNER_TEMP explicitly:

const runnerTemp = process.env.RUNNER_TEMP;
if (!runnerTemp) {
  throw new Error('[' + serverName + '] RUNNER_TEMP is not set; cannot locate fallback tools.json');
}
const fallbackPath = process.env.GH_AW_SAFE_OUTPUTS_TOOLS_PATH || `${runnerTemp}/gh-aw/safeoutputs/tools.json`;

mount_mcp_as_cli.cjs already has a hard RUNNER_TEMP assignment at the top, so the root fix could also be adding that same constant here.

@copilot please address this.

Comment thread actions/setup/js/copilot_harness.cjs Outdated
// --continue is a CLI concept; in SDK mode retries always restart the session fresh.
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
if (softTimeoutGuard && Date.now() >= softTimeoutGuard.softDeadlineMs) {
emitInfrastructureIncomplete(`Copilot harness reached soft retry budget before the ${softTimeoutGuard.timeoutMinutes}-minute step timeout. ` + "Stopping retries early to preserve structured failure output.");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/diagnosing-bugs] emitInfrastructureIncomplete is called twice with identical message strings (one at loop entry, one after backoff sleep). A future wording change would need to be applied in both places.

💡 Suggestion

Extract the message to a constant or a small helper so the two call-sites stay consistent:

function softTimeoutMessage(guard) {
  return `Copilot harness reached soft retry budget before the ${guard.timeoutMinutes}-minute step timeout. Stopping retries early to preserve structured failure output.`;
}

@copilot please address this.

Comment thread actions/setup/js/mount_mcp_as_cli.cjs Outdated
// Query tools from the server using the host-accessible URL (mount step runs on host)
const tools = await fetchMCPTools(url, apiKey, core);
let tools = await fetchMCPTools(url, apiKey, core);
if (name === SAFEOUTPUTS_SERVER_NAME) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/codebase-design] The safeoutputs-specific recovery logic is inlined inside main() via a string comparison (name === SAFEOUTPUTS_SERVER_NAME). This makes main() responsible for knowing which servers need special validation, which will be hard to extend if other servers need similar hardening.

💡 Suggestion

Consider a plugin/callback pattern that lets each server registration declare its own post-fetch validator:

const SERVER_VALIDATORS = {
  safeoutputs: (tools, core) => recoverSafeOutputsToolsIfNeeded(tools, core),
};

// In loop:
const validator = SERVER_VALIDATORS[name];
tools = validator ? validator(tools, core) : tools;

This keeps the main loop free of server-specific knowledge and makes future additions obvious.

@copilot please address this.

});
});

it("recovers empty safeoutputs schema from fallback tools path", () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] The recovery test doesn't assert that a warning was emitted — it only checks the returned tool list. Without asserting on core.warning, a silent regression (e.g. the warning call being removed) would go undetected.

💡 Suggestion

Capture the warning side-effect in the test. Because ensureSafeOutputsTools currently reads global.core directly, you'd need to either inject core as a parameter (see the first comment) or assert on global.core.warning calls:

const warnings = [];
global.core = { warning: msg => warnings.push(msg) };
const recovered = ensureSafeOutputsTools([], 'safeoutputs', path.join(tempDir, 'empty.json'));
expect(warnings).toHaveLength(1);
expect(warnings[0]).toMatch(/recovered 1 tool/);

@copilot please address this.

// This prevents a broken --continue recovery from resurrecting --continue on the next attempt.
let continueDisabledPermanently = false;
const driverStartTime = Date.now();
const softTimeoutGuard = buildSoftTimeoutGuard(driverStartTime);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] There are no integration/behaviour tests verifying that the soft-timeout guard actually causes the retry loop to break with lastExitCode = 1. The unit test only validates the guard's deadline arithmetic — the loop integration (that a guard hit exits the loop and calls emitInfrastructureIncomplete) is untested.

💡 Suggestion

Add a test that mocks Date.now to return a value past the soft deadline and verifies the process exits with code 1 and that the incomplete output was emitted. This is the most important regression test for the feature's observable behaviour.

@copilot please address this.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: Fail fast on empty safeoutputs schema + soft-time convergence guard

Overall this is a solid reliability fix. The two mechanisms (schema fail-fast and soft-time budget) address the root causes of silent timeout failures cleanly, and the test coverage is good.

Three non-blocking suggestions are attached as inline comments:

  1. Hardcoded magic number in testsoftDeadlineMs: 820000 in the soft-timeout guard test silently breaks if SOFT_TIMEOUT_BUFFER_MS changes.
  2. global.core null-dereferenceensureSafeOutputsTools in mcp_cli_bridge.cjs accesses global.core without a null guard; a defensive fallback would protect untested call sites.
  3. Duplicated soft-timeout emit — the two identical emitInfrastructureIncomplete blocks (before attempt and after backoff sleep) could be extracted into a shared helper to avoid drift.

None of these are blocking — the logic is correct and the tests pass. Addressing them would improve maintainability.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · 129.7 AIC · ⌖ 6.85 AIC · ⊞ 4.9K

const guard = buildSoftTimeoutGuard(10_000, { GH_AW_TIMEOUT_MINUTES: "15" });
expect(guard).toEqual({
timeoutMinutes: 15,
softDeadlineMs: 820000,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The expected value 820000 is a magic number derived from driverStartTime(10_000) + hardTimeoutMs(15×60×1000) − SOFT_TIMEOUT_BUFFER_MS(90×1000). Since SOFT_TIMEOUT_BUFFER_MS is not exported, a future change to the buffer will silently break this test with an opaque mismatch.\n\nConsider either:\n1. Exporting SOFT_TIMEOUT_BUFFER_MS and computing the expected value inline: driverStartTime + 15 * 60 * 1000 - SOFT_TIMEOUT_BUFFER_MS\n2. Or replacing the exact match with range assertions.\n\n@copilot please address this.

Comment thread actions/setup/js/mcp_cli_bridge.cjs Outdated
if (serverName !== SAFEOUTPUTS_SERVER_NAME || tools.length > 0) {
return tools;
}
const core = global.core;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ensureSafeOutputsTools uses global.core directly after the early-return guard, which means any call for a non-safeoutputs server returns safely, but a call for safeoutputs with an empty tools array will dereference global.core — this will throw a TypeError: Cannot read properties of undefined if the function is called before global.core is set (e.g., in unit tests that don't set up the global, or in a path where bridge is invoked before core initialisation).\n\nThe test in mcp_cli_bridge.test.cjs sets global.core in beforeEach, so test coverage is fine, but adding a guard like const core = global.core ?? { warning: () => {} } would make the function defensively safe.\n\n@copilot please address this.

Comment thread actions/setup/js/copilot_harness.cjs Outdated
// --continue is a CLI concept; in SDK mode retries always restart the session fresh.
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
if (softTimeoutGuard && Date.now() >= softTimeoutGuard.softDeadlineMs) {
emitInfrastructureIncomplete(`Copilot harness reached soft retry budget before the ${softTimeoutGuard.timeoutMinutes}-minute step timeout. ` + "Stopping retries early to preserve structured failure output.");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are two identical calls to emitInfrastructureIncomplete + break (one at line ~897 before the attempt, one at line ~912 after backoff sleep). Both share the same message string. If the message ever changes, it needs to be updated in two places.\n\nConsider extracting this into a small helper or labelled-break to deduplicate:\n\njs\nfunction triggerSoftTimeout(guard) {\n emitInfrastructureIncomplete(\n `Copilot harness reached soft retry budget before the ${guard.timeoutMinutes}-minute step timeout. ` +\n "Stopping retries early to preserve structured failure output."\n );\n log(`soft-timeout guard reached: timeoutMinutes=${guard.timeoutMinutes} bufferMs=${SOFT_TIMEOUT_BUFFER_MS}`);\n lastExitCode = 1;\n}\n\n\n@copilot please address this.

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot please run the pr-finisher skill, and rerun checks once the branch is up to date.

Generated by 👨‍🍳 PR Sous Chef · 6.92 AIC · ⌖ 15.9 AIC · ⊞ 6.4K ·

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Triage Result

Field Value
Category bug
Risk High
Priority Score 90/100
Score Breakdown Impact: 45 · Urgency: 28 · Quality: 17
Recommended Action fast_track

Rationale: Critical reliability fix for systemic scheduled-run timeouts. Fails fast on empty safeoutputs schema and adds soft-time convergence guard before the 15-min hard cap. High impact (prevents silent run losses), high urgency (active prod issue), 6 focused files. Prioritize for expedited human review.

Generated by 🔧 PR Triage Agent · 98.6 AIC · ⌖ 9.89 AIC · ⊞ 5.5K ·

Copilot AI commented Jul 3, 2026

Copy link
Copy Markdown
Contributor Author

@copilot please run the pr-finisher skill, and rerun checks once the branch is up to date.

...

Fixed in c75f85censureSafeOutputsTools now derives runnerTemp from toolsFile as a fallback when RUNNER_TEMP is unset, using path.resolve(path.dirname(toolsFile), "../../..") to compute the base. This avoids the undefined/gh-aw/safeoutputs/tools.json path that made recovery silently fail. Local validation (make fmt, make lint, make test-unit) all pass. CI needs a human re-trigger to confirm.

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Great work on this one! 🎉 The fix targets a real and painful failure mode — scheduled runs hitting the hard 15-minute step cap without producing any safe-output artifacts is exactly the kind of silent failure that's hard to diagnose after the fact.

The PR looks ready for review:

  • ✅ Clear root-cause description with a precise fix for the empty safeoutputs schema case
  • ✅ Soft-time budget guard in copilot_harness.cjs ensures degraded-but-structured output instead of a hard kill
  • ✅ All three changed modules (copilot_harness, mcp_cli_bridge, mount_mcp_as_cli) have corresponding test file updates
  • ✅ No new dependencies introduced
  • ✅ Tightly scoped to the actions/setup/js/ layer

This looks aligned and well-executed. 🚀

Generated by ✅ Contribution Check · 120.9 AIC · ⌖ 10.8 AIC · ⊞ 6.3K ·

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot please run the pr-finisher skill, address the remaining review feedback, and rerun checks once the branch is up to date.

Outstanding review follow-ups:

  • github-actions suggested improving buildSoftTimeoutGuard test coverage for edge cases like 0, NaN, and sub-buffer timeouts.
  • github-actions suggested guarding global.core access in mcp_cli_bridge.cjs to avoid null-dereference paths.
  • github-actions suggested deduplicating the repeated emitInfrastructureIncomplete soft-timeout block to avoid drift.

Run: https://github.com/github/gh-aw/actions/runs/28632917404

Generated by 👨‍🍳 PR Sous Chef · 25 AIC · ⌖ 9.73 AIC · ⊞ 6.4K ·

…, deduplicate soft-timeout block

Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>

Copilot AI commented Jul 3, 2026

Copy link
Copy Markdown
Contributor Author

@copilot please run the pr-finisher skill, address the remaining review feedback, and rerun checks once the branch is up to date.

Outstanding review follow-ups:...

Addressed in ce1a77d:

  • buildSoftTimeoutGuard edge cases — added tests for "0", "-5", "NaN", "abc" (all → null) and a sub-buffer 1-minute timeout (clamps softDeadlineMs to start + 1000ms).
  • global.core guardensureSafeOutputsTools now uses global.core?.warning(...) so the fallback path doesn't throw if global.core is uninitialized.
  • Deduplication — extracted emitSoftTimeoutSignal(guard, context) helper; the two identical emitInfrastructureIncomplete + log blocks in the retry loop are replaced with single-line calls that preserve their distinct context strings.

make fmt, make lint, make test-unit all pass. CI needs a human re-trigger.

@pelikhan

pelikhan commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

@copilot refactor as a general purpose harness helper/functionality that all harnesses can reuse

…se across harnesses

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

Copilot AI commented Jul 3, 2026

Copy link
Copy Markdown
Contributor Author

@copilot refactor as a general purpose harness helper/functionality that all harnesses can reuse

Done in the latest commit. SOFT_TIMEOUT_BUFFER_MS, buildSoftTimeoutGuard, and emitSoftTimeoutSignal are now in harness_retry_guard.cjs. emitSoftTimeoutSignal accepts a harnessName string and a logFn callback so any harness can call it with its own label and logger. The buildSoftTimeoutGuard tests moved to harness_retry_guard.test.cjs as well.

@pelikhan

pelikhan commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

@copilot apply soft timeouts to other agentic engines harnesses

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot please run the pr-finisher skill, address unresolved review comments, and rerun checks once the branch is up to date.

Generated by 👨‍🍳 PR Sous Chef · 6.07 AIC · ⌖ 7.49 AIC · ⊞ 6.4K ·

Copilot AI and others added 2 commits July 3, 2026 04:23
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

Copilot AI commented Jul 3, 2026

Copy link
Copy Markdown
Contributor Author

@copilot apply soft timeouts to other agentic engines harnesses

Done — claude_harness.cjs and codex_harness.cjs now both import buildSoftTimeoutGuard and emitSoftTimeoutSignal from harness_retry_guard.cjs and apply the same two-point guard pattern as the Copilot harness: once at the top of the retry loop (before each attempt) and once after the backoff sleep. Both harnesses emit report_incomplete (reason: infrastructure_error) and exit with lastExitCode = 1 when the soft deadline is reached.

…g assertion, soft-timeout test

Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>

Copilot AI commented Jul 3, 2026

Copy link
Copy Markdown
Contributor Author

@copilot please run the pr-finisher skill, address unresolved review comments, and rerun checks once the branch is up to date.

...

Done — addressed in the latest commit:

  • Polling guard documentation: added a comment in all three harnesses (copilot_harness.cjs, claude_harness.cjs, codex_harness.cjs) near the buildSoftTimeoutGuard call explaining that the guard is polled between retry iterations and does not preempt a running attempt.
  • SERVER_VALIDATORS refactor: mount_mcp_as_cli.cjs now uses a SERVER_VALIDATORS registry (keyed by server name) instead of if (name === SAFEOUTPUTS_SERVER_NAME). The table is exported for testing.
  • core.warning assertion: mcp_cli_bridge.test.cjs recovery test now asserts expect(global.core.warning).toHaveBeenCalledWith(expect.stringContaining("recovered")).
  • Soft-timeout integration test: copilot_harness.test.cjs has a new test in the "noop pre-flight and retry guard" describe block — uses a stub that sleeps 1.5 s then exits 1 with GH_AW_TIMEOUT_MINUTES=0.001; asserts callCount === 1, status === 1, and stderr contains "soft-timeout guard reached".

make fmt, make lint, make test-unit all pass. CI needs a human re-trigger.

@pelikhan pelikhan merged commit fce3a83 into main Jul 3, 2026
14 checks passed
@pelikhan pelikhan deleted the copilot/aw-failures-fix-15min-timeout branch July 3, 2026 05:11
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

🎉 This pull request is included in a new release.

Release: v0.82.3

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

4 participants