From 9ecdcc641b12893637e42e15e1aced7b5eb1428e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Jun 2026 15:50:01 +0000 Subject: [PATCH 1/6] Initial plan From ee6ebde6798f4357d45b53cc48588aa59f9b4dc8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Jun 2026 15:55:06 +0000 Subject: [PATCH 2/6] fix: harden screenshot navigation retries --- .../run-code-blocks/directives/screenshot.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/lib/plugins/run-code-blocks/directives/screenshot.ts b/src/lib/plugins/run-code-blocks/directives/screenshot.ts index 5103357..f277150 100644 --- a/src/lib/plugins/run-code-blocks/directives/screenshot.ts +++ b/src/lib/plugins/run-code-blocks/directives/screenshot.ts @@ -57,11 +57,12 @@ function compile(steps: string, path: `${string}.png`, args: Args): string { let script = [ `const puppeteer = require('puppeteer'); +const NAVIGATION_TIMEOUT = 180000; async function main() { let browser = await puppeteer.launch(); let page = await browser.newPage(); - page.setDefaultNavigationTimeout(120000); + page.setDefaultNavigationTimeout(NAVIGATION_TIMEOUT); await page.setViewport(${js(viewport)}); ` ]; @@ -94,10 +95,18 @@ async function main() { case 'visit': script.push(` for (let _attempt = 0; _attempt < 3; _attempt++) {`); script.push(` try {`); - script.push(` await page.goto(${js(params[0])}, { waitUntil: 'domcontentloaded', timeout: 120000 });`); + script.push(` await page.goto(${js(params[0])}, { waitUntil: 'domcontentloaded', timeout: NAVIGATION_TIMEOUT });`); script.push(` break;`); script.push(` } catch (e) {`); - script.push(` if (_attempt === 2) throw e;`); + script.push(` let shouldRetry =`); + script.push(` String(e).includes('Navigation timeout') ||`); + script.push(` String(e).includes('net::ERR_CONNECTION_REFUSED') ||`); + script.push(` String(e).includes('net::ERR_CONNECTION_RESET');`); + script.push(` if (_attempt === 2 || !shouldRetry) throw e;`); + script.push(` await page.close().catch(() => {});`); + script.push(` page = await browser.newPage();`); + script.push(` page.setDefaultNavigationTimeout(NAVIGATION_TIMEOUT);`); + script.push(` await page.setViewport(${js(viewport)});`); script.push(` await new Promise(r => setTimeout(r, 2000));`); script.push(` }`); script.push(` }`); From 7aafe69709291e21c6bfa2766fac0f38abca7189 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Jun 2026 15:56:23 +0000 Subject: [PATCH 3/6] refactor: improve screenshot retry error handling --- .../run-code-blocks/directives/screenshot.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/lib/plugins/run-code-blocks/directives/screenshot.ts b/src/lib/plugins/run-code-blocks/directives/screenshot.ts index f277150..36d5be6 100644 --- a/src/lib/plugins/run-code-blocks/directives/screenshot.ts +++ b/src/lib/plugins/run-code-blocks/directives/screenshot.ts @@ -98,12 +98,18 @@ async function main() { script.push(` await page.goto(${js(params[0])}, { waitUntil: 'domcontentloaded', timeout: NAVIGATION_TIMEOUT });`); script.push(` break;`); script.push(` } catch (e) {`); + script.push(` let message = e instanceof Error ? e.message : String(e);`); script.push(` let shouldRetry =`); - script.push(` String(e).includes('Navigation timeout') ||`); - script.push(` String(e).includes('net::ERR_CONNECTION_REFUSED') ||`); - script.push(` String(e).includes('net::ERR_CONNECTION_RESET');`); + script.push(` message.includes('Navigation timeout') ||`); + script.push(` message.includes('net::ERR_CONNECTION_REFUSED') ||`); + script.push(` message.includes('net::ERR_CONNECTION_RESET');`); script.push(` if (_attempt === 2 || !shouldRetry) throw e;`); - script.push(` await page.close().catch(() => {});`); + script.push(` try {`); + script.push(` await page.close();`); + script.push(` } catch (closeError) {`); + script.push(` let closeMessage = closeError instanceof Error ? closeError.message : String(closeError);`); + script.push(` if (!closeMessage.includes('Target closed')) throw closeError;`); + script.push(` }`); script.push(` page = await browser.newPage();`); script.push(` page.setDefaultNavigationTimeout(NAVIGATION_TIMEOUT);`); script.push(` await page.setViewport(${js(viewport)});`); From 1f93e8af536c2a4fb5043e084364ccef8fb3ff76 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Jun 2026 15:57:18 +0000 Subject: [PATCH 4/6] chore: centralize retryable screenshot navigation errors --- .../plugins/run-code-blocks/directives/screenshot.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/lib/plugins/run-code-blocks/directives/screenshot.ts b/src/lib/plugins/run-code-blocks/directives/screenshot.ts index 36d5be6..cc029f2 100644 --- a/src/lib/plugins/run-code-blocks/directives/screenshot.ts +++ b/src/lib/plugins/run-code-blocks/directives/screenshot.ts @@ -58,6 +58,11 @@ function compile(steps: string, path: `${string}.png`, args: Args): string { let script = [ `const puppeteer = require('puppeteer'); const NAVIGATION_TIMEOUT = 180000; +const RETRYABLE_NAVIGATION_ERRORS = [ + 'Navigation timeout', + 'net::ERR_CONNECTION_REFUSED', + 'net::ERR_CONNECTION_RESET' +]; async function main() { let browser = await puppeteer.launch(); @@ -99,10 +104,7 @@ async function main() { script.push(` break;`); script.push(` } catch (e) {`); script.push(` let message = e instanceof Error ? e.message : String(e);`); - script.push(` let shouldRetry =`); - script.push(` message.includes('Navigation timeout') ||`); - script.push(` message.includes('net::ERR_CONNECTION_REFUSED') ||`); - script.push(` message.includes('net::ERR_CONNECTION_RESET');`); + script.push(` let shouldRetry = RETRYABLE_NAVIGATION_ERRORS.some(pattern => message.includes(pattern));`); script.push(` if (_attempt === 2 || !shouldRetry) throw e;`); script.push(` try {`); script.push(` await page.close();`); From 185b2c9bcd5ca75db25e9ce8e46db64047d2b5f8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Jun 2026 16:37:30 +0000 Subject: [PATCH 5/6] Harden local server readiness and navigation retries --- .../run-code-blocks/directives/screenshot.ts | 15 ++- .../directives/server/start.ts | 93 ++++++++++++++++++- 2 files changed, 103 insertions(+), 5 deletions(-) diff --git a/src/lib/plugins/run-code-blocks/directives/screenshot.ts b/src/lib/plugins/run-code-blocks/directives/screenshot.ts index cc029f2..082aa9a 100644 --- a/src/lib/plugins/run-code-blocks/directives/screenshot.ts +++ b/src/lib/plugins/run-code-blocks/directives/screenshot.ts @@ -58,12 +58,19 @@ function compile(steps: string, path: `${string}.png`, args: Args): string { let script = [ `const puppeteer = require('puppeteer'); const NAVIGATION_TIMEOUT = 180000; +const MAX_NAVIGATION_RETRIES = 8; const RETRYABLE_NAVIGATION_ERRORS = [ 'Navigation timeout', 'net::ERR_CONNECTION_REFUSED', - 'net::ERR_CONNECTION_RESET' + 'net::ERR_CONNECTION_RESET', + 'net::ERR_ABORTED', + 'ERR_HTTP_RESPONSE_CODE_FAILURE' ]; +function retryDelay(attempt) { + return Math.min(500 * Math.pow(2, attempt), 5000); +} + async function main() { let browser = await puppeteer.launch(); let page = await browser.newPage(); @@ -98,14 +105,14 @@ async function main() { script.push(` await page.evaluate(${js(params[0])});`); break; case 'visit': - script.push(` for (let _attempt = 0; _attempt < 3; _attempt++) {`); + script.push(` for (let _attempt = 0; _attempt < MAX_NAVIGATION_RETRIES; _attempt++) {`); script.push(` try {`); script.push(` await page.goto(${js(params[0])}, { waitUntil: 'domcontentloaded', timeout: NAVIGATION_TIMEOUT });`); script.push(` break;`); script.push(` } catch (e) {`); script.push(` let message = e instanceof Error ? e.message : String(e);`); script.push(` let shouldRetry = RETRYABLE_NAVIGATION_ERRORS.some(pattern => message.includes(pattern));`); - script.push(` if (_attempt === 2 || !shouldRetry) throw e;`); + script.push(` if (_attempt === MAX_NAVIGATION_RETRIES - 1 || !shouldRetry) throw e;`); script.push(` try {`); script.push(` await page.close();`); script.push(` } catch (closeError) {`); @@ -115,7 +122,7 @@ async function main() { script.push(` page = await browser.newPage();`); script.push(` page.setDefaultNavigationTimeout(NAVIGATION_TIMEOUT);`); script.push(` await page.setViewport(${js(viewport)});`); - script.push(` await new Promise(r => setTimeout(r, 2000));`); + script.push(` await new Promise(r => setTimeout(r, retryDelay(_attempt)));`); script.push(` }`); script.push(` }`); break; diff --git a/src/lib/plugins/run-code-blocks/directives/server/start.ts b/src/lib/plugins/run-code-blocks/directives/server/start.ts index 89e95eb..71f41b3 100644 --- a/src/lib/plugins/run-code-blocks/directives/server/start.ts +++ b/src/lib/plugins/run-code-blocks/directives/server/start.ts @@ -1,4 +1,5 @@ import { Code } from 'mdast'; +import { createConnection } from 'net'; import { join } from 'path'; import { Option, assert } from 'ts-std'; import { parseCommand } from '../../commands'; @@ -6,6 +7,9 @@ import Options from '../../options'; import parseArgs, { ToBool, optional } from '../../parse-args'; import Servers from '../../servers'; +const DEFAULT_READY_TIMEOUT = 30000; +const READY_RETRY_INTERVAL = 500; + interface Args { id?: string; lang?: string; @@ -17,6 +21,64 @@ interface Args { captureOutput?: boolean; } +function extractURL(expect: Option): Option { + if (!expect) { + return null; + } + + let match = expect.match(/https?:\/\/[^\s"']+/); + + if (match) { + return match[0]!; + } else { + return null; + } +} + +async function waitForServerReady(url: string, timeout: number): Promise { + let parsed = new URL(url); + let host = parsed.hostname; + let port = parsed.port ? Number(parsed.port) : (parsed.protocol === 'https:' ? 443 : 80); + let startedAt = Date.now(); + let lastError: Option = null; + + while (Date.now() - startedAt < timeout) { + try { + await new Promise((resolve, reject) => { + let socket = createConnection({ host, port }); + + socket.once('connect', () => { + socket.end(); + resolve(); + }); + + socket.once('error', e => { + socket.destroy(); + + if (e instanceof Error) { + reject(e); + } else { + reject(new Error(String(e))); + } + }); + }); + + return; + } catch (e) { + if (e instanceof Error) { + lastError = e; + } else { + lastError = new Error(String(e)); + } + } + + await new Promise(resolve => setTimeout(resolve, READY_RETRY_INTERVAL)); + } + + let details = lastError ? ` Last error: ${lastError.message}` : ''; + throw new Error(`Timed out while waiting for ${url} to accept connections after ${timeout}ms.${details}`); +} + export default async function startServer(node: Code, options: Options, servers: Servers): Promise> { let args = parseArgs(node, [ optional('id', String), @@ -72,7 +134,36 @@ export default async function startServer(node: Code, options: Options, servers: output.push(`$ ${display}`); } - let stdout = await server.start(args.expect); + let stdout = await server.start(args.expect, args.timeout); + + let readyURL = extractURL(args.expect); + + if (readyURL) { + let timeout = args.timeout || DEFAULT_READY_TIMEOUT; + + try { + await waitForServerReady(readyURL, timeout); + } catch (e) { + await server.kill(); + + let message = (e instanceof Error) ? e.message : String(e); + + throw new Error( +`${message} + +====== STDOUT ====== + +${server.stdout || '(No output)'} + +====== STDERR ====== + +${server.stderr || '(No output)'} + +==================== +` + ); + } + } if (args.captureOutput && stdout) { output.push(stdout); From 23b07d1a2ed42f2ea35229bcf07a9283c269ed0c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Jun 2026 16:40:51 +0000 Subject: [PATCH 6/6] Improve server startup readiness checks and navigation retries --- src/lib/plugins/run-code-blocks/directives/screenshot.ts | 2 +- .../plugins/run-code-blocks/directives/server/start.ts | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/lib/plugins/run-code-blocks/directives/screenshot.ts b/src/lib/plugins/run-code-blocks/directives/screenshot.ts index 082aa9a..49c6b26 100644 --- a/src/lib/plugins/run-code-blocks/directives/screenshot.ts +++ b/src/lib/plugins/run-code-blocks/directives/screenshot.ts @@ -58,7 +58,7 @@ function compile(steps: string, path: `${string}.png`, args: Args): string { let script = [ `const puppeteer = require('puppeteer'); const NAVIGATION_TIMEOUT = 180000; -const MAX_NAVIGATION_RETRIES = 8; +const MAX_NAVIGATION_RETRIES = 6; const RETRYABLE_NAVIGATION_ERRORS = [ 'Navigation timeout', 'net::ERR_CONNECTION_REFUSED', diff --git a/src/lib/plugins/run-code-blocks/directives/server/start.ts b/src/lib/plugins/run-code-blocks/directives/server/start.ts index 71f41b3..dc79dd5 100644 --- a/src/lib/plugins/run-code-blocks/directives/server/start.ts +++ b/src/lib/plugins/run-code-blocks/directives/server/start.ts @@ -21,7 +21,7 @@ interface Args { captureOutput?: boolean; } -function extractURL(expect: Option): Option { +function extractURL(expect: Option | undefined): Option { if (!expect) { return null; } @@ -29,7 +29,11 @@ function extractURL(expect: Option): Option { let match = expect.match(/https?:\/\/[^\s"']+/); if (match) { - return match[0]!; + try { + return new URL(match[0]!).toString(); + } catch { + return null; + } } else { return null; }