From b72b9df04b2803bd1e7fb59e28d526eda827735b Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 20 Mar 2026 22:30:33 +0000
Subject: [PATCH 1/2] feat: extract agent timeout text to markdown template
 file

Move the hardcoded timeout message from buildTimeoutContext() in
handle_agent_failure.cjs into actions/setup/md/agent_timeout.md.

The new template uses {current_minutes} and {suggested_minutes}
placeholders rendered via renderTemplate(), following the same
pattern used by buildInferenceAccessErrorContext() and
buildLockdownCheckFailedContext().

The setup.sh script already copies all md/*.md files to
${RUNNER_TEMP}/gh-aw/prompts/ at runtime, so the file is
automatically available to the conclusion job.

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Agent-Logs-Url: https://github.com/github/gh-aw/sessions/2718c9d9-d8d3-4899-8818-0818d6bde90a
---
 actions/setup/js/handle_agent_failure.cjs | 13 +++----------
 actions/setup/md/agent_timeout.md         |  9 +++++++++
 2 files changed, 12 insertions(+), 10 deletions(-)
 create mode 100644 actions/setup/md/agent_timeout.md

diff --git a/actions/setup/js/handle_agent_failure.cjs b/actions/setup/js/handle_agent_failure.cjs
index ed5797d7274..30116d4f2bb 100644
--- a/actions/setup/js/handle_agent_failure.cjs
+++ b/actions/setup/js/handle_agent_failure.cjs
@@ -608,16 +608,9 @@ function buildTimeoutContext(isTimedOut, timeoutMinutes) {
   const currentMinutes = parseInt(timeoutMinutes || "20", 10);
   const suggestedMinutes = currentMinutes + 10;
 
-  let ctx = "\n**⏱️ Agent Timed Out**: The agent job exceeded the maximum allowed execution time";
-  ctx += ` (${currentMinutes} minutes).`;
-  ctx += "\n\nTo increase the timeout, add or update the `timeout-minutes` setting in your workflow's frontmatter:\n\n";
-  ctx += "```yaml\n";
-  ctx += "---\n";
-  ctx += `timeout-minutes: ${suggestedMinutes}\n`;
-  ctx += "---\n";
-  ctx += "```\n\n";
-
-  return ctx;
+  const templatePath = `${process.env.RUNNER_TEMP}/gh-aw/prompts/agent_timeout.md`;
+  const template = fs.readFileSync(templatePath, "utf8");
+  return "\n" + renderTemplate(template, { current_minutes: currentMinutes, suggested_minutes: suggestedMinutes });
 }
 
 /**
diff --git a/actions/setup/md/agent_timeout.md b/actions/setup/md/agent_timeout.md
new file mode 100644
index 00000000000..c8f1860bf06
--- /dev/null
+++ b/actions/setup/md/agent_timeout.md
@@ -0,0 +1,9 @@
+**⏱️ Agent Timed Out**: The agent job exceeded the maximum allowed execution time ({current_minutes} minutes).
+
+To increase the timeout, add or update the `timeout-minutes` setting in your workflow's frontmatter:
+
+```yaml
+---
+timeout-minutes: {suggested_minutes}
+---
+```

From ad31df1d923b14eb7166c30fd3c615a94103bc39 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 21 Mar 2026 05:05:21 +0000
Subject: [PATCH 2/2] test: export buildTimeoutContext and add unit tests

Export buildTimeoutContext from handle_agent_failure.cjs so it can be
tested directly, following the same pattern as other exported context
builders.

Add unit tests that stub fs.readFileSync for agent_timeout.md and
assert correct rendering of {current_minutes} and {suggested_minutes}
placeholders, including the default-minutes fallback and the +10
suggestion logic.

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Agent-Logs-Url: https://github.com/github/gh-aw/sessions/991ce53a-a7f5-4337-845b-de6f685d5d0f
---
 actions/setup/js/handle_agent_failure.cjs     |  2 +-
 .../setup/js/handle_agent_failure.test.cjs    | 53 +++++++++++++++++++
 2 files changed, 54 insertions(+), 1 deletion(-)

diff --git a/actions/setup/js/handle_agent_failure.cjs b/actions/setup/js/handle_agent_failure.cjs
index 30116d4f2bb..50ad555e417 100644
--- a/actions/setup/js/handle_agent_failure.cjs
+++ b/actions/setup/js/handle_agent_failure.cjs
@@ -1162,4 +1162,4 @@ async function main() {
   }
 }
 
-module.exports = { main, buildCodePushFailureContext, buildPushRepoMemoryFailureContext, buildAppTokenMintingFailedContext, buildLockdownCheckFailedContext };
+module.exports = { main, buildCodePushFailureContext, buildPushRepoMemoryFailureContext, buildAppTokenMintingFailedContext, buildLockdownCheckFailedContext, buildTimeoutContext };
diff --git a/actions/setup/js/handle_agent_failure.test.cjs b/actions/setup/js/handle_agent_failure.test.cjs
index e6a253bad8e..86d56fdf210 100644
--- a/actions/setup/js/handle_agent_failure.test.cjs
+++ b/actions/setup/js/handle_agent_failure.test.cjs
@@ -389,4 +389,57 @@ describe("handle_agent_failure", () => {
       expect(result).toContain("gh aw compile --strict");
     });
   });
+
+  // ──────────────────────────────────────────────────────
+  // buildTimeoutContext
+  // ──────────────────────────────────────────────────────
+
+  describe("buildTimeoutContext", () => {
+    let buildTimeoutContext;
+    const fs = require("fs");
+    const path = require("path");
+    const templateContent = fs.readFileSync(path.join(__dirname, "../md/agent_timeout.md"), "utf8");
+    const originalReadFileSync = fs.readFileSync.bind(fs);
+
+    beforeEach(() => {
+      vi.resetModules();
+      // Stub readFileSync so the runtime path resolves to the source-tree template
+      fs.readFileSync = (filePath, encoding) => {
+        if (typeof filePath === "string" && filePath.includes("agent_timeout.md")) {
+          return templateContent;
+        }
+        return originalReadFileSync(filePath, encoding);
+      };
+      ({ buildTimeoutContext } = require("./handle_agent_failure.cjs"));
+    });
+
+    afterEach(() => {
+      fs.readFileSync = originalReadFileSync;
+    });
+
+    it("returns empty string when not timed out", () => {
+      expect(buildTimeoutContext(false, "20")).toBe("");
+      expect(buildTimeoutContext(false, "")).toBe("");
+    });
+
+    it("returns formatted error message when timed out", () => {
+      const result = buildTimeoutContext(true, "20");
+      expect(result).toContain("Agent Timed Out");
+      expect(result).toContain("20");
+      expect(result).toContain("30");
+      expect(result).toContain("timeout-minutes");
+    });
+
+    it("uses default of 20 minutes when timeoutMinutes is empty", () => {
+      const result = buildTimeoutContext(true, "");
+      expect(result).toContain("20");
+      expect(result).toContain("30");
+    });
+
+    it("suggests current + 10 minutes", () => {
+      const result = buildTimeoutContext(true, "45");
+      expect(result).toContain("45");
+      expect(result).toContain("55");
+    });
+  });
 });
