From 2c9cd3efedc351a0f8f34059726ca4b3bda4088c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 30 Jan 2026 18:15:17 +0000 Subject: [PATCH 1/5] Initial plan From 87bb7ff0a9ba1227759a3a02230b99d2a6eebd2e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 30 Jan 2026 18:18:08 +0000 Subject: [PATCH 2/5] Add retry logic with exponential backoff for fetch operations Co-authored-by: KushagraAgarwal525 <67466389+KushagraAgarwal525@users.noreply.github.com> --- package-lock.json | 236 +--------------------------- packages/cli/src/commands/deploy.ts | 72 ++++++++- 2 files changed, 67 insertions(+), 241 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8a5f76d..bad635e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1258,6 +1258,7 @@ }, "examples/gpt-slack": { "version": "1.0.0", + "extraneous": true, "dependencies": { "@leanmcp/core": "^0.3.10", "@leanmcp/ui": "*", @@ -1274,237 +1275,6 @@ "typescript": "^5.6.3" } }, - "examples/gpt-slack/node_modules/@leanmcp/cli": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/@leanmcp/cli/-/cli-0.4.5.tgz", - "integrity": "sha512-Sn5zi0VrnJpiwoSIcbDszfWdbk4YyyAoC6vMDT9xzMUZL8AYAo2qPIVWleZocqYLzzLLWf1bWHjIJ2Xb9KNhMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/prompts": "^7.0.1", - "@vitejs/plugin-react": "^4.3.0", - "archiver": "^7.0.1", - "autoprefixer": "^10.4.16", - "chalk": "^5.3.0", - "chokidar": "^4.0.0", - "commander": "^12.0.0", - "fs-extra": "^11.2.0", - "glob": "^11.0.0", - "ora": "^8.1.0", - "postcss": "^8.4.32", - "tailwindcss": "^3.4.0", - "vite": "^5.4.0", - "vite-plugin-singlefile": "^2.3.0" - }, - "bin": { - "leanmcp": "bin/leanmcp.js" - } - }, - "examples/gpt-slack/node_modules/@leanmcp/core": { - "version": "0.3.19", - "resolved": "https://registry.npmjs.org/@leanmcp/core/-/core-0.3.19.tgz", - "integrity": "sha512-67VWhhOcX3xvO6AseFAlrBBUYNrKurf2Dt6fRY90UznVeH+cYeByAZ/1LcfQYgeGJIxQWOAXQ3FRtD7KdgN+RA==", - "license": "MIT", - "dependencies": { - "@modelcontextprotocol/sdk": "^1.0.0", - "ajv": "^8.12.0", - "chokidar": "^4.0.0", - "dotenv": "^16.3.1", - "reflect-metadata": "^0.2.1" - }, - "peerDependencies": { - "@leanmcp/auth": "^0.4.0", - "cors": "^2.8.5", - "express": "^5.0.0" - }, - "peerDependenciesMeta": { - "@leanmcp/auth": { - "optional": true - }, - "cors": { - "optional": true - }, - "express": { - "optional": true - } - } - }, - "examples/gpt-slack/node_modules/@leanmcp/core/node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "examples/gpt-slack/node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "examples/gpt-slack/node_modules/dotenv": { - "version": "17.2.3", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", - "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "examples/gpt-slack/node_modules/glob": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", - "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "foreground-child": "^3.3.1", - "jackspeak": "^4.1.1", - "minimatch": "^10.1.1", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^2.0.0" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "examples/gpt-slack/node_modules/jiti": { - "version": "1.21.7", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", - "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", - "dev": true, - "license": "MIT", - "bin": { - "jiti": "bin/jiti.js" - } - }, - "examples/gpt-slack/node_modules/minimatch": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", - "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "examples/gpt-slack/node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "examples/gpt-slack/node_modules/tailwindcss": { - "version": "3.4.19", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", - "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.6.0", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.3.2", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.21.7", - "lilconfig": "^3.1.3", - "micromatch": "^4.0.8", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.1.1", - "postcss": "^8.4.47", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", - "postcss-nested": "^6.2.0", - "postcss-selector-parser": "^6.1.2", - "resolve": "^1.22.8", - "sucrase": "^3.35.0" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "examples/gpt-slack/node_modules/tailwindcss/node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "examples/gpt-slack/node_modules/tailwindcss/node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "examples/leanmcp-auth": { "version": "1.0.0", "dependencies": { @@ -11872,10 +11642,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gpt-slack": { - "resolved": "examples/gpt-slack", - "link": true - }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", diff --git a/packages/cli/src/commands/deploy.ts b/packages/cli/src/commands/deploy.ts index 85c8840..1b0e112 100644 --- a/packages/cli/src/commands/deploy.ts +++ b/packages/cli/src/commands/deploy.ts @@ -45,6 +45,60 @@ async function debugFetch(url: string, options: RequestInit = {}): Promise Promise, + options: { + maxRetries?: number; + initialDelay?: number; + maxDelay?: number; + operation?: string; + } = {} +): Promise { + const maxRetries = options.maxRetries ?? 15; + const initialDelay = options.initialDelay ?? 1000; + const maxDelay = options.maxDelay ?? 10000; + const operation = options.operation ?? 'Fetch'; + + let lastError: Error | null = null; + + for (let attempt = 0; attempt <= maxRetries; attempt++) { + try { + const response = await fetchFn(); + + // Clear the retry message if any was shown + if (attempt > 0) { + process.stdout.write('\r' + ' '.repeat(80) + '\r'); + } + + return response; + } catch (error) { + lastError = error instanceof Error ? error : new Error(String(error)); + + if (attempt < maxRetries) { + // Calculate delay with exponential backoff + const delay = Math.min(initialDelay * Math.pow(2, attempt), maxDelay); + + // Show retry message with in-place update + const message = `${operation} failed. Retrying... (${attempt + 1}/${maxRetries})`; + process.stdout.write('\r' + chalk.yellow(message)); + + debug(`Retry ${attempt + 1}/${maxRetries}: ${lastError.message}, waiting ${delay}ms`); + + await new Promise((resolve) => setTimeout(resolve, delay)); + } + } + } + + // Clear the retry message + process.stdout.write('\r' + ' '.repeat(80) + '\r'); + + throw new Error(`${operation} failed after ${maxRetries} retries: ${lastError?.message || 'Unknown error'}`); +} + // API endpoints (relative to base URL) const API_ENDPOINTS = { // Projects @@ -171,9 +225,12 @@ async function waitForBuild( let attempts = 0; while (attempts < maxAttempts) { - const response = await debugFetch(`${apiUrl}${API_ENDPOINTS.getBuild}/${buildId}`, { - headers: { Authorization: `Bearer ${apiKey}` }, - }); + const response = await fetchWithRetry( + () => debugFetch(`${apiUrl}${API_ENDPOINTS.getBuild}/${buildId}`, { + headers: { Authorization: `Bearer ${apiKey}` }, + }), + { operation: 'Build status check' } + ); if (!response.ok) { throw new Error(`Failed to get build status: ${response.statusText}`); @@ -215,9 +272,12 @@ async function waitForDeployment( let attempts = 0; while (attempts < maxAttempts) { - const response = await debugFetch(`${apiUrl}${API_ENDPOINTS.getDeployment}/${deploymentId}`, { - headers: { Authorization: `Bearer ${apiKey}` }, - }); + const response = await fetchWithRetry( + () => debugFetch(`${apiUrl}${API_ENDPOINTS.getDeployment}/${deploymentId}`, { + headers: { Authorization: `Bearer ${apiKey}` }, + }), + { operation: 'Deployment status check' } + ); if (!response.ok) { throw new Error(`Failed to get deployment status: ${response.statusText}`); From 7c66006fead96343a36e7b73757d90e9945437ff Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 30 Jan 2026 18:19:02 +0000 Subject: [PATCH 3/5] Improve retry logic to work with ora spinners Co-authored-by: KushagraAgarwal525 <67466389+KushagraAgarwal525@users.noreply.github.com> --- packages/cli/src/commands/deploy.ts | 59 ++++++++++++++++------------- 1 file changed, 33 insertions(+), 26 deletions(-) diff --git a/packages/cli/src/commands/deploy.ts b/packages/cli/src/commands/deploy.ts index 1b0e112..ca8884f 100644 --- a/packages/cli/src/commands/deploy.ts +++ b/packages/cli/src/commands/deploy.ts @@ -56,47 +56,52 @@ async function fetchWithRetry( initialDelay?: number; maxDelay?: number; operation?: string; + spinner?: ReturnType; } = {} ): Promise { const maxRetries = options.maxRetries ?? 15; const initialDelay = options.initialDelay ?? 1000; const maxDelay = options.maxDelay ?? 10000; const operation = options.operation ?? 'Fetch'; + const spinner = options.spinner; let lastError: Error | null = null; - + for (let attempt = 0; attempt <= maxRetries; attempt++) { try { const response = await fetchFn(); - - // Clear the retry message if any was shown - if (attempt > 0) { - process.stdout.write('\r' + ' '.repeat(80) + '\r'); - } - + return response; } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); - + if (attempt < maxRetries) { // Calculate delay with exponential backoff const delay = Math.min(initialDelay * Math.pow(2, attempt), maxDelay); - - // Show retry message with in-place update + + // Show retry message - either via spinner or stdout const message = `${operation} failed. Retrying... (${attempt + 1}/${maxRetries})`; - process.stdout.write('\r' + chalk.yellow(message)); - + if (spinner) { + spinner.text = message; + } else { + process.stdout.write('\r' + chalk.yellow(message)); + } + debug(`Retry ${attempt + 1}/${maxRetries}: ${lastError.message}, waiting ${delay}ms`); - + await new Promise((resolve) => setTimeout(resolve, delay)); } } } - - // Clear the retry message - process.stdout.write('\r' + ' '.repeat(80) + '\r'); - - throw new Error(`${operation} failed after ${maxRetries} retries: ${lastError?.message || 'Unknown error'}`); + + // Clear the retry message if not using spinner + if (!spinner) { + process.stdout.write('\r' + ' '.repeat(80) + '\r'); + } + + throw new Error( + `${operation} failed after ${maxRetries} retries: ${lastError?.message || 'Unknown error'}` + ); } // API endpoints (relative to base URL) @@ -226,10 +231,11 @@ async function waitForBuild( while (attempts < maxAttempts) { const response = await fetchWithRetry( - () => debugFetch(`${apiUrl}${API_ENDPOINTS.getBuild}/${buildId}`, { - headers: { Authorization: `Bearer ${apiKey}` }, - }), - { operation: 'Build status check' } + () => + debugFetch(`${apiUrl}${API_ENDPOINTS.getBuild}/${buildId}`, { + headers: { Authorization: `Bearer ${apiKey}` }, + }), + { operation: 'Build status check', spinner } ); if (!response.ok) { @@ -273,10 +279,11 @@ async function waitForDeployment( while (attempts < maxAttempts) { const response = await fetchWithRetry( - () => debugFetch(`${apiUrl}${API_ENDPOINTS.getDeployment}/${deploymentId}`, { - headers: { Authorization: `Bearer ${apiKey}` }, - }), - { operation: 'Deployment status check' } + () => + debugFetch(`${apiUrl}${API_ENDPOINTS.getDeployment}/${deploymentId}`, { + headers: { Authorization: `Bearer ${apiKey}` }, + }), + { operation: 'Deployment status check', spinner } ); if (!response.ok) { From 920cd8edc27c3174779e150452b134a9296d76ad Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 30 Jan 2026 18:21:51 +0000 Subject: [PATCH 4/5] Address code review feedback: fix retry logic, improve docs, reduce nested retries Co-authored-by: KushagraAgarwal525 <67466389+KushagraAgarwal525@users.noreply.github.com> --- packages/cli/src/commands/deploy.ts | 41 +++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/packages/cli/src/commands/deploy.ts b/packages/cli/src/commands/deploy.ts index ca8884f..c66c18e 100644 --- a/packages/cli/src/commands/deploy.ts +++ b/packages/cli/src/commands/deploy.ts @@ -48,6 +48,23 @@ async function debugFetch(url: string, options: RequestInit = {}): Promise Promise, @@ -57,6 +74,7 @@ async function fetchWithRetry( maxDelay?: number; operation?: string; spinner?: ReturnType; + retryOnHttpErrors?: boolean; } = {} ): Promise { const maxRetries = options.maxRetries ?? 15; @@ -64,30 +82,37 @@ async function fetchWithRetry( const maxDelay = options.maxDelay ?? 10000; const operation = options.operation ?? 'Fetch'; const spinner = options.spinner; + const retryOnHttpErrors = options.retryOnHttpErrors ?? false; let lastError: Error | null = null; - for (let attempt = 0; attempt <= maxRetries; attempt++) { + // Initial attempt + retries + for (let attempt = 0; attempt < maxRetries; attempt++) { try { const response = await fetchFn(); + // Check if we should retry on HTTP errors + if (retryOnHttpErrors && !response.ok && response.status >= 500) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + return response; } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); - if (attempt < maxRetries) { + if (attempt < maxRetries - 1) { // Calculate delay with exponential backoff const delay = Math.min(initialDelay * Math.pow(2, attempt), maxDelay); // Show retry message - either via spinner or stdout - const message = `${operation} failed. Retrying... (${attempt + 1}/${maxRetries})`; + const message = `${operation} failed. Retrying... (${attempt + 1}/${maxRetries - 1})`; if (spinner) { spinner.text = message; } else { process.stdout.write('\r' + chalk.yellow(message)); } - debug(`Retry ${attempt + 1}/${maxRetries}: ${lastError.message}, waiting ${delay}ms`); + debug(`Retry ${attempt + 1}/${maxRetries - 1}: ${lastError.message}, waiting ${delay}ms`); await new Promise((resolve) => setTimeout(resolve, delay)); } @@ -96,11 +121,11 @@ async function fetchWithRetry( // Clear the retry message if not using spinner if (!spinner) { - process.stdout.write('\r' + ' '.repeat(80) + '\r'); + process.stdout.write('\x1b[2K\r'); // ANSI escape to clear entire line } throw new Error( - `${operation} failed after ${maxRetries} retries: ${lastError?.message || 'Unknown error'}` + `${operation} failed after ${maxRetries} attempts: ${lastError?.message || 'Unknown error'}` ); } @@ -235,7 +260,7 @@ async function waitForBuild( debugFetch(`${apiUrl}${API_ENDPOINTS.getBuild}/${buildId}`, { headers: { Authorization: `Bearer ${apiKey}` }, }), - { operation: 'Build status check', spinner } + { operation: 'Build status check', spinner, maxRetries: 3 } ); if (!response.ok) { @@ -283,7 +308,7 @@ async function waitForDeployment( debugFetch(`${apiUrl}${API_ENDPOINTS.getDeployment}/${deploymentId}`, { headers: { Authorization: `Bearer ${apiKey}` }, }), - { operation: 'Deployment status check', spinner } + { operation: 'Deployment status check', spinner, maxRetries: 3 } ); if (!response.ok) { From 0d2f6de058f4ab85f6d9d591f0c36a15a547cce0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 30 Jan 2026 18:23:58 +0000 Subject: [PATCH 5/5] Fix retry logic: correct loop condition and counter display Co-authored-by: KushagraAgarwal525 <67466389+KushagraAgarwal525@users.noreply.github.com> --- packages/cli/src/commands/deploy.ts | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/cli/src/commands/deploy.ts b/packages/cli/src/commands/deploy.ts index c66c18e..4bb9f46 100644 --- a/packages/cli/src/commands/deploy.ts +++ b/packages/cli/src/commands/deploy.ts @@ -48,23 +48,23 @@ async function debugFetch(url: string, options: RequestInit = {}): Promise Promise, @@ -86,8 +86,8 @@ async function fetchWithRetry( let lastError: Error | null = null; - // Initial attempt + retries - for (let attempt = 0; attempt < maxRetries; attempt++) { + // Initial attempt (0) + retries (1 to maxRetries) + for (let attempt = 0; attempt <= maxRetries; attempt++) { try { const response = await fetchFn(); @@ -100,19 +100,19 @@ async function fetchWithRetry( } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); - if (attempt < maxRetries - 1) { + if (attempt < maxRetries) { // Calculate delay with exponential backoff const delay = Math.min(initialDelay * Math.pow(2, attempt), maxDelay); // Show retry message - either via spinner or stdout - const message = `${operation} failed. Retrying... (${attempt + 1}/${maxRetries - 1})`; + const message = `${operation} failed. Retrying... (${attempt + 1}/${maxRetries})`; if (spinner) { spinner.text = message; } else { process.stdout.write('\r' + chalk.yellow(message)); } - debug(`Retry ${attempt + 1}/${maxRetries - 1}: ${lastError.message}, waiting ${delay}ms`); + debug(`Retry ${attempt + 1}/${maxRetries}: ${lastError.message}, waiting ${delay}ms`); await new Promise((resolve) => setTimeout(resolve, delay)); } @@ -125,7 +125,7 @@ async function fetchWithRetry( } throw new Error( - `${operation} failed after ${maxRetries} attempts: ${lastError?.message || 'Unknown error'}` + `${operation} failed after ${maxRetries + 1} attempts (1 initial + ${maxRetries} retries): ${lastError?.message || 'Unknown error'}` ); }