diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fa2260874..1ac07b647 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,8 +46,7 @@ jobs: {"os":"macos-14","platform":"darwin","arch":"arm64"}, {"os":"windows-latest","platform":"win32","arch":"x64","variants":"baseline modern"} ]') || fromJSON('[ - {"os":"ubuntu-22.04","platform":"linux","arch":"x64","variants":"baseline modern"}, - {"os":"ubuntu-22.04","platform":"linux","arch":"arm64","target":"aarch64-unknown-linux-gnu"} + {"os":"ubuntu-22.04","platform":"linux","arch":"x64","variants":"baseline modern"} ]') }} runs-on: ${{ matrix.os }} steps: @@ -128,9 +127,36 @@ jobs: bun packages/coding-agent/src/cli.ts --help bun packages/coding-agent/src/cli.ts stats --help + install_methods: + runs-on: ubuntu-22.04 + env: + RUSTFLAGS: -C target-cpu=x86-64-v3 + steps: + - uses: actions/checkout@v4 + with: + lfs: true + - uses: oven-sh/setup-bun@v2 + with: + bun-version: "1.3" + - uses: dtolnay/rust-toolchain@nightly + - name: Cache bun dependencies + uses: actions/cache@v4 + with: + path: ~/.bun/install/cache + key: bun-${{ runner.os }}-${{ hashFiles('**/bun.lock') }} + - name: Install system deps + run: | + sudo apt-get update + sudo apt-get install -y libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev fd-find ripgrep imagemagick + sudo ln -s $(which fdfind) /usr/local/bin/fd + sudo ln -sf /usr/bin/convert /usr/local/bin/magick + - run: bun install --frozen-lockfile + - name: Install method smoke tests + run: bash scripts/install-tests/run-ci.sh + release: if: startsWith(github.ref, 'refs/tags/v') - needs: [rust, native, test] + needs: [rust, native, test, install_methods] runs-on: ubuntu-22.04 permissions: contents: write diff --git a/.gitignore b/.gitignore index bd7096890..b1193c8af 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,6 @@ syntax.jsonl out.jsonl out.html pi-*.html + +# Generated files +packages/coding-agent/src/internal-urls/docs-index.generated.ts diff --git a/.omp/commands/triage.md b/.omp/commands/triage.md index b57ee4a9c..d18162ee3 100644 --- a/.omp/commands/triage.md +++ b/.omp/commands/triage.md @@ -1,35 +1,37 @@ # Triage Command -Classify and label open GitHub issues that are missing labels. +Classify and label **newly opened** GitHub issues that are missing labels. ## Arguments -- `$ARGUMENTS`: Scope filter — `open` (default), `closed`, or `all`. Append `--relabel` to re-classify issues that already have labels. +- `$ARGUMENTS`: Optional window flag `--days ` (default: `7`). Only open issues created within this window are triaged. ## Steps ### 1. Fetch Issues -Parse `$ARGUMENTS` to determine scope and whether `--relabel` is set. +Parse `$ARGUMENTS` to determine the new-issue window (`--days`, default `7`). ```bash -# For open (default): -gh issue list --state open --json number,title,body,labels,comments,createdAt --limit 50 +# Build cutoff date (UTC) for "new" issues +CUTOFF_DATE="$(python - <<'PY' +from datetime import datetime, timedelta, timezone +print((datetime.now(timezone.utc) - timedelta(days=7)).strftime('%Y-%m-%d')) +PY + )" -# For closed: -gh issue list --state closed --json number,title,body,labels,comments,createdAt --limit 30 +# Fetch only newly created open issues (default 7-day window) +gh issue list --state open --search "created:>=${CUTOFF_DATE}" --json number,title,body,labels,comments,createdAt --limit 50 -# For all: run both commands and merge results -``` +### 2. Filter New Candidates -### 2. Filter Candidates - -- If `--relabel` is NOT set, skip any issue that already has at least one label. -- If `--relabel` IS set, process all fetched issues. +- Skip any issue older than the cutoff window; this command only triages new issues. +- Skip issues with label `triaged` (already handled). +- For remaining issues, if type + area + platform are already present, skip unless metadata is clearly missing. ### 3. Classify Each Issue -For each candidate issue, read the title, body, and **all comments** (comments often contain critical context). Apply labels from the categories below. An issue can receive multiple labels. +For each candidate issue, read the title, body, and **all comments** (comments often contain critical context). Apply labels from the categories below. An issue can receive multiple labels. For each category, skip it only if the issue already has a label in that category — always fill in missing categories. **Type labels** (pick exactly one): | Label | Signals | @@ -68,10 +70,10 @@ For each candidate issue, read the title, body, and **all comments** (comments o ### 4. Apply Labels -For each issue, apply the chosen labels. **Never remove existing labels.** +For each issue, apply the chosen labels and add `triaged`. **Never remove existing labels.** ```bash -gh issue edit --add-label "bug,tui,platform:linux" +gh issue edit --add-label "bug,tui,platform:linux,triaged" ``` ### 5. Print Summary diff --git a/Cargo.lock b/Cargo.lock index 6f11db9a7..77ddadae9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -98,9 +98,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.101" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "arboard" @@ -2058,7 +2058,7 @@ dependencies = [ [[package]] name = "pi-natives" -version = "12.14.1" +version = "12.15.0" dependencies = [ "arboard", "brush-builtins", @@ -2610,9 +2610,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.116" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3df424c70518695237746f84cede799c9c58fcb37450d7b23716568cc8bc69cb" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 657a41d0f..68c524f71 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ exclude = ["crates/brush-core-vendored", "crates/brush-builtins-vendored"] resolver = "3" [workspace.package] -version = "12.14.1" +version = "12.15.0" edition = "2024" license = "MIT" authors = ["Can Boluk"] diff --git a/bun.lock b/bun.lock index e03cc9ba1..ce10984f4 100644 --- a/bun.lock +++ b/bun.lock @@ -20,7 +20,7 @@ }, "packages/agent": { "name": "@oh-my-pi/pi-agent-core", - "version": "12.14.1", + "version": "12.15.0", "dependencies": { "@oh-my-pi/pi-ai": "workspace:*", "@oh-my-pi/pi-tui": "workspace:*", @@ -33,7 +33,7 @@ }, "packages/ai": { "name": "@oh-my-pi/pi-ai", - "version": "12.14.1", + "version": "12.15.0", "bin": { "pi-ai": "./src/cli.ts", }, @@ -62,7 +62,7 @@ }, "packages/coding-agent": { "name": "@oh-my-pi/pi-coding-agent", - "version": "12.14.1", + "version": "12.15.0", "bin": { "omp": "src/cli.ts", }, @@ -98,7 +98,7 @@ }, "packages/natives": { "name": "@oh-my-pi/pi-natives", - "version": "12.14.1", + "version": "12.15.0", "dependencies": { "@oh-my-pi/pi-utils": "workspace:*", }, @@ -132,7 +132,7 @@ }, "packages/stats": { "name": "@oh-my-pi/omp-stats", - "version": "12.14.1", + "version": "12.15.0", "bin": { "omp-stats": "./src/index.ts", }, @@ -157,7 +157,7 @@ }, "packages/swarm-extension": { "name": "@oh-my-pi/swarm-extension", - "version": "12.14.1", + "version": "12.15.0", "bin": { "omp-swarm": "src/cli.ts", }, @@ -170,7 +170,7 @@ }, "packages/tui": { "name": "@oh-my-pi/pi-tui", - "version": "12.14.1", + "version": "12.15.0", "dependencies": { "@oh-my-pi/pi-natives": "workspace:*", "@oh-my-pi/pi-utils": "workspace:*", @@ -186,7 +186,7 @@ }, "packages/utils": { "name": "@oh-my-pi/pi-utils", - "version": "12.14.1", + "version": "12.15.0", "dependencies": { "winston": "^3.19.0", "winston-daily-rotate-file": "^5.0.0", @@ -509,21 +509,21 @@ "@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="], - "@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20260219.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260219.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260219.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20260219.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260219.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20260219.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260219.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20260219.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-Y/mfpmpZwfwyNzBgki/wUm/pWLQM2gaL5cum6lbOv8QNZUtzcIy0wTPaS08sb4yhUSGC1jGaJEPR2FNctfeC2Q=="], + "@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20260220.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260220.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260220.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20260220.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260220.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20260220.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260220.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20260220.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-trYXlG98/C7Q7pqnPrKo+ksXrWqWVMncCy2x0VftD2llfL99Z//g2mpB9TmzWeKgb4d1659ESvxTowCGnzMccw=="], - "@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20260219.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-h8gG6gE0YcxJZwxFc+JHjqCoFf/EBZ0k6znspV6+NwkyyJHMIqYiawZ7Lzu2Ka8lJDO3QxXcIdw2jKaktwW2wQ=="], + "@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20260220.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-VZQHVaLYTpa3wfCLcFD5cfnegr4iDtzBxV6yh3tys+HYePi4TXuAqct/dmziW0cpCo/UQ2KqAPGxwVO3YMDYJA=="], - "@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20260219.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-/tSFdSD76V4X3bX+iXecaa41KRuPMl1LQD3nsE45zZFdmDUIhvCwFRXDP+whkcdaJy8RJTALC0wWhIJ6FUmnwA=="], + "@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20260220.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-ieGtyz904rlme8YWauDpCqGbnOQQ5dzUyRKU25I7MNIaoZFf0vK5gMh3SLjHC7ayWxjrCa2ezH4ka8UmyWQcPg=="], - "@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20260219.1", "", { "os": "linux", "cpu": "arm" }, "sha512-pmTGfe3VGoCmjyy9r68wurGBsoaqwYRZ8/i7cIxAJNnq1huKuXRnLnVoZm6uOzZ2GtjWFjwmXXCvcdfmy9+PvQ=="], + "@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20260220.1", "", { "os": "linux", "cpu": "arm" }, "sha512-wMA63N6XLAkO0Ibq1Qz2zkQHF6oLynYh5+q1YayzWxp1FOas0oJUdNgVbiWeisAT4IEI9SmYtVwJJNTm/cwrwg=="], - "@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20260219.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-9MXFr+T5LQlXuDsKTtwFdN1lbaKSMmZzabT8Gr1C74ueun91IiJVhNNISwCWY48c28Ka6O6fwxeHjU44ee/G9Q=="], + "@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20260220.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-nlOOABSQUe6ix/KOsZS3ALZ/sg9rhtp6SKtQnXO8X4+2XUlwVYS6nIrAQ5jG4+OsCvcESttNeHhBw817uNrsuA=="], - "@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20260219.1", "", { "os": "linux", "cpu": "x64" }, "sha512-EMLKx9koxTbdWFg+jaqy5MwZQ19usFug6Cw+evkCaFlDEfF5sgJ/npSRhNJLbZ451FR0eqNIKmIEA9cXlHSDuA=="], + "@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20260220.1", "", { "os": "linux", "cpu": "x64" }, "sha512-W7R/5ct/BGuPAmJuKFU0ZuLU2nzLA26XfoaoUHHoTLYRMMuY3LBv+7zKSUTlSGjayeWQ5KtDrfrY72LarWAXkQ=="], - "@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20260219.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-vILjYS1EnTZoiD1SfWBtk92qpDrFP9Ln38olonMjtO7mJO6SmN78GHhGR+dyGgNTHQ7PZAxGFiQwZJgrIMWNhw=="], + "@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20260220.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-FNXTr2lS1QLUB05jWjkBVwrDierNuxKduQq9ayloENJrsC8GfG1sXkfy8R021+ozP/D1LI/CFUn3hkB2vPXTsQ=="], - "@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260219.1", "", { "os": "win32", "cpu": "x64" }, "sha512-wwjOQCSViLi+mqJycnRek7GeLZXmJClcZaVYFkLRVbmIMWvWSJkafasFjnQHDinXcLiXxZYdJ+oRR/86FOt2Xw=="], + "@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260220.1", "", { "os": "win32", "cpu": "x64" }, "sha512-byHRyf4dOuKOADrs43tWyPhmAgqk+XrA/XOsJnGsxUKxPwots+gf9x5kg/IxTbB3QjPsVe/V9QdMl3//sUTCmQ=="], "@typescript/vfs": ["@typescript/vfs@1.6.4", "", { "dependencies": { "debug": "^4.4.3" }, "peerDependencies": { "typescript": "*" } }, "sha512-PJFXFS4ZJKiJ9Qiuix6Dz/OwEIqHD7Dme1UwZhTK11vR+5dqW2ACbdndWQexBzCx+CPuMe5WBYQWCsFyGlQLlQ=="], diff --git a/package.json b/package.json index 5cadb7440..73c5baa3d 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "version:major": "npm version major -ws --no-git-tag-version && bun scripts/sync-versions.ts && rm -rf node_modules packages/*/node_modules bun.lock && bun install", "version:set": "npm version -ws", "prepublishOnly": "bun run check", + "prepare": "bun --cwd=packages/coding-agent run generate-docs-index", "publish": "bun run prepublishOnly && npm publish -ws --access public", "publish:dry": "bun run prepublishOnly && npm publish -ws --access public --dry-run", "release": "bun scripts/release.ts", diff --git a/packages/agent/package.json b/packages/agent/package.json index 2cb0880d2..0e30f0b3e 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -1,6 +1,6 @@ { "name": "@oh-my-pi/pi-agent-core", - "version": "12.14.1", + "version": "12.15.0", "description": "General-purpose agent with transport abstraction, state management, and attachment support", "type": "module", "main": "./src/index.ts", diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 78779cb95..4a29847e7 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,17 @@ ## [Unreleased] +## [12.15.0] - 2026-02-20 +### Fixed + +- Improved error messages for OAuth token refresh failures by including detailed error information from the provider +- Separated rate limit and usage limit error handling to provide distinct user-friendly messages for ChatGPT rate limits vs subscription usage limits + +### Changed + +- Increased SDK retry attempts to 5 for OpenAI, Azure OpenAI, and Anthropic clients (was SDK default of 2) +- Changed 429 retry strategy for OpenAI Codex and Google Gemini CLI to use a 5-minute time budget when the server provides a retry delay, instead of a fixed attempt cap + ## [12.14.0] - 2026-02-19 ### Added diff --git a/packages/ai/package.json b/packages/ai/package.json index 36da884a4..242ffafe6 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -1,6 +1,6 @@ { "name": "@oh-my-pi/pi-ai", - "version": "12.14.1", + "version": "12.15.0", "description": "Unified LLM API with automatic model discovery and provider configuration", "type": "module", "main": "./src/index.ts", diff --git a/packages/ai/src/providers/anthropic.ts b/packages/ai/src/providers/anthropic.ts index 9e77e1666..03ec33238 100644 --- a/packages/ai/src/providers/anthropic.ts +++ b/packages/ai/src/providers/anthropic.ts @@ -629,6 +629,7 @@ function createClient( baseURL: config.baseURL, defaultHeaders: config.defaultHeaders, dangerouslyAllowBrowser: config.dangerouslyAllowBrowser, + maxRetries: 5, }); return { client, isOAuthToken: config.isOAuthToken }; diff --git a/packages/ai/src/providers/azure-openai-responses.ts b/packages/ai/src/providers/azure-openai-responses.ts index 486a49cb5..2075b689d 100644 --- a/packages/ai/src/providers/azure-openai-responses.ts +++ b/packages/ai/src/providers/azure-openai-responses.ts @@ -424,6 +424,7 @@ function createClient(model: Model<"azure-openai-responses">, apiKey: string, op apiKey, apiVersion, dangerouslyAllowBrowser: true, + maxRetries: 5, defaultHeaders: headers, baseURL: baseUrl, }); diff --git a/packages/ai/src/providers/google-gemini-cli.ts b/packages/ai/src/providers/google-gemini-cli.ts index 481da9688..8cf459ed9 100644 --- a/packages/ai/src/providers/google-gemini-cli.ts +++ b/packages/ai/src/providers/google-gemini-cli.ts @@ -99,6 +99,7 @@ const MAX_RETRIES = 3; const BASE_DELAY_MS = 1000; const MAX_EMPTY_STREAM_RETRIES = 2; const EMPTY_STREAM_BASE_DELAY_MS = 500; +const RATE_LIMIT_BUDGET_MS = 5 * 60 * 1000; const CLAUDE_THINKING_BETA_HEADER = "interleaved-thinking-2025-05-14"; /** @@ -360,8 +361,9 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = ( let response: Response | undefined; let lastError: Error | undefined; let requestUrl: string | undefined; + let rateLimitTimeSpent = 0; - for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) { + for (let attempt = 0; ; attempt++) { if (options?.signal?.aborted) { throw new Error("Request was aborted"); } @@ -382,13 +384,25 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = ( const errorText = await response.text(); - // Check if retryable - if (attempt < MAX_RETRIES && isRetryableError(response.status, errorText)) { - // Use server-provided delay or exponential backoff + // Handle 429 rate limits with time budget + if (response.status === 429) { + const serverDelay = extractRetryDelay(errorText, response); + if (serverDelay && rateLimitTimeSpent + serverDelay <= RATE_LIMIT_BUDGET_MS) { + rateLimitTimeSpent += serverDelay; + await abortableSleep(serverDelay, options?.signal); + continue; + } + // Fallback: use exponential backoff if no server delay, up to MAX_RETRIES + if (!serverDelay && attempt < MAX_RETRIES) { + await abortableSleep(BASE_DELAY_MS * 2 ** attempt, options?.signal); + continue; + } + } else if (attempt < MAX_RETRIES && isRetryableError(response.status, errorText)) { + // Non-429 retryable errors use standard attempt cap const serverDelay = extractRetryDelay(errorText, response); const delayMs = serverDelay ?? BASE_DELAY_MS * 2 ** attempt; - // Check if server delay exceeds max allowed (default: 60s) + // Check if server delay exceeds max allowed (default: 60s) for non-429 errors const maxDelayMs = options?.maxRetryDelayMs ?? 60000; if (maxDelayMs > 0 && serverDelay && serverDelay > maxDelayMs) { const delaySeconds = Math.ceil(serverDelay / 1000); @@ -401,7 +415,7 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = ( continue; } - // Not retryable or max retries exceeded + // Not retryable or budget exceeded throw new Error(`Cloud Code Assist API error (${response.status}): ${extractErrorMessage(errorText)}`); } catch (error) { // Check for abort - fetch throws AbortError, our code throws "Request was aborted" diff --git a/packages/ai/src/providers/openai-codex-responses.ts b/packages/ai/src/providers/openai-codex-responses.ts index 44e74b873..0f289e5b6 100644 --- a/packages/ai/src/providers/openai-codex-responses.ts +++ b/packages/ai/src/providers/openai-codex-responses.ts @@ -1377,16 +1377,20 @@ function logCodexDebug(message: string, details?: Record): void console.error(`[codex] ${message}`); } -function getRetryDelayMs(response: Response | null, attempt: number, errorBody?: string): number { +function getRetryDelayMs( + response: Response | null, + attempt: number, + errorBody?: string, +): { delay: number; serverProvided: boolean } { const retryAfter = response?.headers?.get("retry-after") || null; if (retryAfter) { const seconds = Number(retryAfter); if (Number.isFinite(seconds)) { - return Math.max(0, seconds * 1000); + return { delay: Math.max(0, seconds * 1000), serverProvided: true }; } const parsedDate = Date.parse(retryAfter); if (!Number.isNaN(parsedDate)) { - return Math.max(0, parsedDate - Date.now()); + return { delay: Math.max(0, parsedDate - Date.now()), serverProvided: true }; } } // Parse retry delay from error body (e.g., "Please try again in 225ms" or "Please try again in 1.5s") @@ -1394,28 +1398,41 @@ function getRetryDelayMs(response: Response | null, attempt: number, errorBody?: const msMatch = /try again in\s+(\d+(?:\.\d+)?)\s*ms/i.exec(errorBody); if (msMatch) { const ms = Number(msMatch[1]); - if (Number.isFinite(ms)) return Math.max(ms, 100); + if (Number.isFinite(ms)) return { delay: Math.max(ms, 100), serverProvided: true }; } const sMatch = /try again in\s+(\d+(?:\.\d+)?)\s*s(?:ec)?/i.exec(errorBody); if (sMatch) { const s = Number(sMatch[1]); - if (Number.isFinite(s)) return Math.max(s * 1000, 100); + if (Number.isFinite(s)) return { delay: Math.max(s * 1000, 100), serverProvided: true }; } } - return CODEX_RETRY_DELAY_MS * (attempt + 1); + return { delay: CODEX_RETRY_DELAY_MS * (attempt + 1), serverProvided: false }; } +/** Max total time to spend retrying 429s with server-provided delays (5 minutes). */ +const CODEX_RATE_LIMIT_BUDGET_MS = 5 * 60 * 1000; + async function fetchWithRetry(url: string, init: RequestInit, signal?: AbortSignal): Promise { let attempt = 0; + let rateLimitTimeSpent = 0; while (true) { try { const response = await fetch(url, { ...init, signal: signal ?? init.signal }); - if (!CODEX_RETRYABLE_STATUS.has(response.status) || attempt >= CODEX_MAX_RETRIES) { + if (!CODEX_RETRYABLE_STATUS.has(response.status)) { return response; } if (signal?.aborted) return response; // Read error body for retry delay parsing const errorBody = await response.text(); - const delay = getRetryDelayMs(response, attempt, errorBody); + const { delay, serverProvided } = getRetryDelayMs(response, attempt, errorBody); + // For 429s with a server-provided delay, use a time budget instead of attempt count + if (response.status === 429 && serverProvided) { + if (rateLimitTimeSpent + delay > CODEX_RATE_LIMIT_BUDGET_MS) { + return response; + } + rateLimitTimeSpent += delay; + } else if (attempt >= CODEX_MAX_RETRIES) { + return response; + } await abortableSleep(delay, signal); } catch (error) { if (attempt >= CODEX_MAX_RETRIES || signal?.aborted) { diff --git a/packages/ai/src/providers/openai-codex/response-handler.ts b/packages/ai/src/providers/openai-codex/response-handler.ts index 9b3b1cbee..917f69e07 100644 --- a/packages/ai/src/providers/openai-codex/response-handler.ts +++ b/packages/ai/src/providers/openai-codex/response-handler.ts @@ -47,11 +47,14 @@ export async function parseCodexError(response: Response): Promise= creds.expires) { try { creds = await refreshOAuthToken(provider, creds); - } catch { + } catch (refreshError) { if (provider === "perplexity") { const jwtExpiry = getPerplexityJwtExpiryMs(creds.access); if (jwtExpiry && Date.now() < jwtExpiry) { @@ -388,7 +388,8 @@ export async function getOAuthApiKey( return { newCredentials: fallbackCredentials, apiKey: fallbackCredentials.access }; } } - throw new Error(`Failed to refresh OAuth token for ${provider}`); + const reason = refreshError instanceof Error ? refreshError.message : String(refreshError); + throw new Error(`Failed to refresh OAuth token for ${provider}: ${reason}`); } } // For providers that need projectId, return JSON diff --git a/packages/ai/src/utils/oauth/openai-codex.ts b/packages/ai/src/utils/oauth/openai-codex.ts index 2e7144be3..9e1d43c6d 100644 --- a/packages/ai/src/utils/oauth/openai-codex.ts +++ b/packages/ai/src/utils/oauth/openai-codex.ts @@ -147,7 +147,13 @@ export async function refreshOpenAICodexToken(refreshToken: string): Promise { - if (index === 0) { - return part.toLowerCase(); - } - return part.charAt(0).toUpperCase() + part.slice(1).toLowerCase(); - }); - - const base = parts.length > 0 ? parts.join("") : "doc"; - const safeBase = /^[0-9]/.test(base) ? `doc${base}` : base; - return `${safeBase}Md`; -} const glob = new Glob("**/*.md"); const entries: string[] = []; @@ -31,26 +13,28 @@ for await (const relativePath of glob.scan(docsDir)) { } entries.sort(); -const usedIdentifiers = new Set(); -const docs = entries.map((relativePath) => { - let identifier = toIdentifier(relativePath); - let suffix = 2; - while (usedIdentifiers.has(identifier)) { - identifier = `${toIdentifier(relativePath)}${suffix}`; - suffix++; - } - usedIdentifiers.add(identifier); - return { relativePath, identifier }; -}); -docs.sort((a, b) => a.identifier.localeCompare(b.identifier) || a.relativePath.localeCompare(b.relativePath)); +const docsWithContent = await Promise.all( + entries.map(async (relativePath) => ({ + relativePath, + content: await Bun.file(path.join(docsDir, relativePath)).text(), + })) +); -const imports = docs - .map(({ relativePath, identifier }) => `import ${identifier} from "${importBase}/${relativePath}" with { type: "text" };`) +const filenamesLiteral = JSON.stringify(entries); + +const mapEntries = docsWithContent + .map(({ relativePath, content }) => `\t${JSON.stringify(relativePath)}: ${JSON.stringify(content)},`) .join("\n"); - -const mapEntries = docs.map(({ relativePath, identifier }) => `\t"${relativePath}": ${identifier},`).join("\n"); - -const output = `// Auto-generated by scripts/generate-docs-index.ts - DO NOT EDIT\n\n${imports}\n\nexport const EMBEDDED_DOCS: Readonly> = {\n${mapEntries}\n};\n\nexport const EMBEDDED_DOC_FILENAMES = Object.keys(EMBEDDED_DOCS).sort();\n`; +const output = [ + "// Auto-generated by scripts/generate-docs-index.ts - DO NOT EDIT", + "", + `export const EMBEDDED_DOC_FILENAMES: readonly string[] = ${filenamesLiteral};`, + "", + `export const EMBEDDED_DOCS: Readonly> = {`, + `${mapEntries}`, + `};`, + "", +].join("\n"); await Bun.write(outputPath, output); -console.log(`Generated ${path.relative(process.cwd(), outputPath)} (${docs.length} docs)`); +console.log(`Generated ${path.relative(process.cwd(), outputPath)} (${entries.length} docs)`); diff --git a/packages/coding-agent/src/config/settings-schema.ts b/packages/coding-agent/src/config/settings-schema.ts index 8c1b00441..301077c97 100644 --- a/packages/coding-agent/src/config/settings-schema.ts +++ b/packages/coding-agent/src/config/settings-schema.ts @@ -504,7 +504,7 @@ export const SETTINGS_SCHEMA = { }, "tools.intentTracing": { type: "boolean", - default: false, + default: true, ui: { tab: "tools", label: "Intent tracing", diff --git a/packages/coding-agent/src/internal-urls/docs-index.generated.ts b/packages/coding-agent/src/internal-urls/docs-index.generated.ts deleted file mode 100644 index c45cab175..000000000 --- a/packages/coding-agent/src/internal-urls/docs-index.generated.ts +++ /dev/null @@ -1,101 +0,0 @@ -// Auto-generated by scripts/generate-docs-index.ts - DO NOT EDIT - -import bashToolRuntimeMd from "../../../../docs/bash-tool-runtime.md" with { type: "text" }; -import blobArtifactArchitectureMd from "../../../../docs/blob-artifact-architecture.md" with { type: "text" }; -import compactionMd from "../../../../docs/compaction.md" with { type: "text" }; -import configUsageMd from "../../../../docs/config-usage.md" with { type: "text" }; -import customToolsMd from "../../../../docs/custom-tools.md" with { type: "text" }; -import environmentVariablesMd from "../../../../docs/environment-variables.md" with { type: "text" }; -import extensionLoadingMd from "../../../../docs/extension-loading.md" with { type: "text" }; -import extensionsMd from "../../../../docs/extensions.md" with { type: "text" }; -import fsScanCacheArchitectureMd from "../../../../docs/fs-scan-cache-architecture.md" with { type: "text" }; -import geminiManifestExtensionsMd from "../../../../docs/gemini-manifest-extensions.md" with { type: "text" }; -import handoffGenerationPipelineMd from "../../../../docs/handoff-generation-pipeline.md" with { type: "text" }; -import hooksMd from "../../../../docs/hooks.md" with { type: "text" }; -import mcpProtocolTransportsMd from "../../../../docs/mcp-protocol-transports.md" with { type: "text" }; -import mcpRuntimeLifecycleMd from "../../../../docs/mcp-runtime-lifecycle.md" with { type: "text" }; -import mcpServerToolAuthoringMd from "../../../../docs/mcp-server-tool-authoring.md" with { type: "text" }; -import modelsMd from "../../../../docs/models.md" with { type: "text" }; -import nativesAddonLoaderRuntimeMd from "../../../../docs/natives-addon-loader-runtime.md" with { type: "text" }; -import nativesArchitectureMd from "../../../../docs/natives-architecture.md" with { type: "text" }; -import nativesBindingContractMd from "../../../../docs/natives-binding-contract.md" with { type: "text" }; -import nativesBuildReleaseDebuggingMd from "../../../../docs/natives-build-release-debugging.md" with { type: "text" }; -import nativesMediaSystemUtilsMd from "../../../../docs/natives-media-system-utils.md" with { type: "text" }; -import nativesRustTaskCancellationMd from "../../../../docs/natives-rust-task-cancellation.md" with { type: "text" }; -import nativesShellPtyProcessMd from "../../../../docs/natives-shell-pty-process.md" with { type: "text" }; -import nativesTextSearchPipelineMd from "../../../../docs/natives-text-search-pipeline.md" with { type: "text" }; -import nonCompactionRetryPolicyMd from "../../../../docs/non-compaction-retry-policy.md" with { type: "text" }; -import notebookToolRuntimeMd from "../../../../docs/notebook-tool-runtime.md" with { type: "text" }; -import pluginManagerInstallerPlumbingMd from "../../../../docs/plugin-manager-installer-plumbing.md" with { type: "text" }; -import portingFromPiMonoMd from "../../../../docs/porting-from-pi-mono.md" with { type: "text" }; -import portingToNativesMd from "../../../../docs/porting-to-natives.md" with { type: "text" }; -import providerStreamingInternalsMd from "../../../../docs/provider-streaming-internals.md" with { type: "text" }; -import pythonReplMd from "../../../../docs/python-repl.md" with { type: "text" }; -import rpcMd from "../../../../docs/rpc.md" with { type: "text" }; -import rulebookMatchingPipelineMd from "../../../../docs/rulebook-matching-pipeline.md" with { type: "text" }; -import sdkMd from "../../../../docs/sdk.md" with { type: "text" }; -import secretsMd from "../../../../docs/secrets.md" with { type: "text" }; -import sessionMd from "../../../../docs/session.md" with { type: "text" }; -import sessionOperationsExportShareForkResumeMd from "../../../../docs/session-operations-export-share-fork-resume.md" with { type: "text" }; -import sessionSwitchingAndRecentListingMd from "../../../../docs/session-switching-and-recent-listing.md" with { type: "text" }; -import sessionTreePlanMd from "../../../../docs/session-tree-plan.md" with { type: "text" }; -import skillsMd from "../../../../docs/skills.md" with { type: "text" }; -import slashCommandInternalsMd from "../../../../docs/slash-command-internals.md" with { type: "text" }; -import taskAgentDiscoveryMd from "../../../../docs/task-agent-discovery.md" with { type: "text" }; -import themeMd from "../../../../docs/theme.md" with { type: "text" }; -import treeMd from "../../../../docs/tree.md" with { type: "text" }; -import ttsrInjectionLifecycleMd from "../../../../docs/ttsr-injection-lifecycle.md" with { type: "text" }; -import tuiMd from "../../../../docs/tui.md" with { type: "text" }; -import tuiRuntimeInternalsMd from "../../../../docs/tui-runtime-internals.md" with { type: "text" }; - -export const EMBEDDED_DOCS: Readonly> = { - "bash-tool-runtime.md": bashToolRuntimeMd, - "blob-artifact-architecture.md": blobArtifactArchitectureMd, - "compaction.md": compactionMd, - "config-usage.md": configUsageMd, - "custom-tools.md": customToolsMd, - "environment-variables.md": environmentVariablesMd, - "extension-loading.md": extensionLoadingMd, - "extensions.md": extensionsMd, - "fs-scan-cache-architecture.md": fsScanCacheArchitectureMd, - "gemini-manifest-extensions.md": geminiManifestExtensionsMd, - "handoff-generation-pipeline.md": handoffGenerationPipelineMd, - "hooks.md": hooksMd, - "mcp-protocol-transports.md": mcpProtocolTransportsMd, - "mcp-runtime-lifecycle.md": mcpRuntimeLifecycleMd, - "mcp-server-tool-authoring.md": mcpServerToolAuthoringMd, - "models.md": modelsMd, - "natives-addon-loader-runtime.md": nativesAddonLoaderRuntimeMd, - "natives-architecture.md": nativesArchitectureMd, - "natives-binding-contract.md": nativesBindingContractMd, - "natives-build-release-debugging.md": nativesBuildReleaseDebuggingMd, - "natives-media-system-utils.md": nativesMediaSystemUtilsMd, - "natives-rust-task-cancellation.md": nativesRustTaskCancellationMd, - "natives-shell-pty-process.md": nativesShellPtyProcessMd, - "natives-text-search-pipeline.md": nativesTextSearchPipelineMd, - "non-compaction-retry-policy.md": nonCompactionRetryPolicyMd, - "notebook-tool-runtime.md": notebookToolRuntimeMd, - "plugin-manager-installer-plumbing.md": pluginManagerInstallerPlumbingMd, - "porting-from-pi-mono.md": portingFromPiMonoMd, - "porting-to-natives.md": portingToNativesMd, - "provider-streaming-internals.md": providerStreamingInternalsMd, - "python-repl.md": pythonReplMd, - "rpc.md": rpcMd, - "rulebook-matching-pipeline.md": rulebookMatchingPipelineMd, - "sdk.md": sdkMd, - "secrets.md": secretsMd, - "session.md": sessionMd, - "session-operations-export-share-fork-resume.md": sessionOperationsExportShareForkResumeMd, - "session-switching-and-recent-listing.md": sessionSwitchingAndRecentListingMd, - "session-tree-plan.md": sessionTreePlanMd, - "skills.md": skillsMd, - "slash-command-internals.md": slashCommandInternalsMd, - "task-agent-discovery.md": taskAgentDiscoveryMd, - "theme.md": themeMd, - "tree.md": treeMd, - "ttsr-injection-lifecycle.md": ttsrInjectionLifecycleMd, - "tui.md": tuiMd, - "tui-runtime-internals.md": tuiRuntimeInternalsMd, -}; - -export const EMBEDDED_DOC_FILENAMES = Object.keys(EMBEDDED_DOCS).sort(); diff --git a/packages/coding-agent/src/modes/rpc/rpc-client.ts b/packages/coding-agent/src/modes/rpc/rpc-client.ts index 6a47be4a0..2b08b0f6b 100644 --- a/packages/coding-agent/src/modes/rpc/rpc-client.ts +++ b/packages/coding-agent/src/modes/rpc/rpc-client.ts @@ -123,27 +123,54 @@ export class RpcClient { stdin: "pipe", }); - // Process lines in background + // Wait for the "ready" signal or process exit + const { promise: readyPromise, resolve: readyResolve, reject: readyReject } = Promise.withResolvers(); + let readySettled = false; + + // Process lines in background, intercepting the ready signal const lines = readJsonl(this.#process.stdout, this.#abortController.signal); void (async () => { for await (const line of lines) { + if (!readySettled && isRecord(line) && line.type === "ready") { + readySettled = true; + readyResolve(); + continue; + } this.#handleLine(line); } - })().catch(() => {}); + // Stream ended without ready signal — process exited + if (!readySettled) { + readySettled = true; + readyReject(new Error(`Agent process exited before ready. Stderr: ${this.#process?.peekStderr() ?? ""}`)); + } + })().catch((err: Error) => { + if (!readySettled) { + readySettled = true; + readyReject(err); + } + }); - // Wait a moment for process to initialize - await Bun.sleep(100); - - try { - const exitCode = await Promise.race([this.#process.exited, Bun.sleep(500).then(() => null)]); - if (exitCode !== null) { - throw new Error( - `Agent process exited immediately with code ${exitCode}. Stderr: ${this.#process.peekStderr()}`, + // Also race against process exit (in case stdout closes before we read it) + void this.#process.exited.then((exitCode: number) => { + if (!readySettled) { + readySettled = true; + readyReject( + new Error(`Agent process exited with code ${exitCode}. Stderr: ${this.#process?.peekStderr() ?? ""}`), ); } - } catch { - // Process still running, which is what we want - } + }); + + // Timeout to prevent hanging forever + void Bun.sleep(30000).then(() => { + if (!readySettled) { + readySettled = true; + readyReject( + new Error(`Timeout waiting for agent to become ready. Stderr: ${this.#process?.peekStderr() ?? ""}`), + ); + } + }); + + await readyPromise; } /** diff --git a/packages/coding-agent/src/modes/rpc/rpc-mode.ts b/packages/coding-agent/src/modes/rpc/rpc-mode.ts index 4988913f3..66c75893a 100644 --- a/packages/coding-agent/src/modes/rpc/rpc-mode.ts +++ b/packages/coding-agent/src/modes/rpc/rpc-mode.ts @@ -36,6 +36,8 @@ export type { * Listens for JSON commands on stdin, outputs events and responses on stdout. */ export async function runRpcMode(session: AgentSession): Promise { + // Signal to RPC clients that the server is ready to accept commands + process.stdout.write(`${JSON.stringify({ type: "ready" })}\n`); const output = (obj: RpcResponse | RpcExtensionUIRequest | object) => { process.stdout.write(`${JSON.stringify(obj)}\n`); }; diff --git a/packages/coding-agent/src/patch/hashline.ts b/packages/coding-agent/src/patch/hashline.ts index ea3d37b50..65cf5e34b 100644 --- a/packages/coding-agent/src/patch/hashline.ts +++ b/packages/coding-agent/src/patch/hashline.ts @@ -663,10 +663,8 @@ export function applyHashlineEdits( if (edit.content.length === 0) { throw new Error('Insert-between edit (src "A#HH.. B#HH..") requires non-empty dst'); } - if (edit.before.line !== edit.after.line + 1) { - throw new Error( - `insert requires adjacent anchors (after ${edit.after.line}, before ${edit.before.line})`, - ); + if (edit.before.line <= edit.after.line) { + throw new Error(`insert requires after (${edit.after.line}) < before (${edit.before.line})`); } const afterValid = validateRef(edit.after); const beforeValid = validateRef(edit.before); diff --git a/packages/coding-agent/src/prompts/tools/browser.md b/packages/coding-agent/src/prompts/tools/browser.md index 49bf2a4b3..99a903ec5 100644 --- a/packages/coding-agent/src/prompts/tools/browser.md +++ b/packages/coding-agent/src/prompts/tools/browser.md @@ -6,21 +6,28 @@ Use this tool to navigate, click, type, scroll, drag, query DOM content, and cap - Use `action: "open"` to start a new headless browser session (or implicitly launch on first action) - Use `action: "goto"` with `url` to navigate - Use `action: "observe"` to capture a numbered accessibility snapshot with URL/title/viewport/scroll info - - Prefer `click_id`, `type_id`, or `fill_id` actions using the returned `element_id` values - - Optional flags: `include_all` to include non-interactive nodes, `viewport_only` to limit to visible elements + - Prefer `click_id`, `type_id`, or `fill_id` actions using the returned `element_id` values + - Optional flags: `include_all` to include non-interactive nodes, `viewport_only` to limit to visible elements - Use `action: "click"`, `"type"`, `"fill"`, `"press"`, `"scroll"`, or `"drag"` for selector-based interactions - - Prefer ARIA or text selectors (e.g. `p-aria/[name="Sign in"]`, `p-text/Continue`) over brittle CSS + - Prefer ARIA or text selectors (e.g. `p-aria/[name="Sign in"]`, `p-text/Continue`) over brittle CSS - Use `action: "click_id"`, `"type_id"`, or `"fill_id"` to interact with observed elements without selectors - Use `action: "wait_for_selector"` before interacting when the page is dynamic - Use `action: "evaluate"` with `script` to run a JavaScript expression in the page context - Use `action: "get_text"`, `"get_html"`, or `"get_attribute"` for DOM queries - - For batch queries, pass `args: [{ selector, attribute? }]` to get an array of results (attribute required for `get_attribute`) + - For batch queries, pass `args: [{ selector, attribute? }]` to get an array of results (attribute required for `get_attribute`) - Use `action: "extract_readable"` to return reader-mode content (title/byline/excerpt/text or markdown) - - Set `format` to `"markdown"` (default) or `"text"` + - Set `format` to `"markdown"` (default) or `"text"` - Use `action: "screenshot"` to capture images (optionally with `selector` to capture a single element) - Use `action: "close"` to release the browser when done + +**Default to `observe`, not `screenshot`.** +- `observe` is cheaper, faster, and returns structured data — use it to understand page state, find elements, and plan interactions. +- Only use `screenshot` when visual appearance matters (verifying layout, debugging CSS, capturing a visual artifact for the user). +- Never screenshot just to "see what's on the page" — `observe` gives you that with element IDs you can act on immediately. + + Returns text output for navigation and DOM queries, and image output for screenshots. Screenshots can optionally be saved to disk via the `path` parameter. \ No newline at end of file diff --git a/packages/coding-agent/src/session/agent-storage.ts b/packages/coding-agent/src/session/agent-storage.ts index 5ca834480..bec118530 100644 --- a/packages/coding-agent/src/session/agent-storage.ts +++ b/packages/coding-agent/src/session/agent-storage.ts @@ -37,7 +37,7 @@ export interface StoredAuthCredential { } /** Bump when schema changes require migration */ -const SCHEMA_VERSION = 3; +const SCHEMA_VERSION = 4; /** * Type guard for plain objects. @@ -124,11 +124,14 @@ export class AgentStorage { #deleteExpiredCacheStmt: Statement; #listAuthStmt: Statement; #listAuthByProviderStmt: Statement; + #listActiveAuthStmt: Statement; + #listActiveAuthByProviderStmt: Statement; #insertAuthStmt: Statement; #updateAuthStmt: Statement; #deleteAuthStmt: Statement; #deleteAuthByProviderStmt: Statement; #countAuthStmt: Statement; + #disableAuthStmt: Statement; #upsertModelUsageStmt: Statement; #listModelUsageStmt: Statement; #modelUsageCache: string[] | null = null; @@ -165,6 +168,12 @@ export class AgentStorage { this.#listAuthByProviderStmt = this.#db.prepare( "SELECT id, provider, credential_type, data FROM auth_credentials WHERE provider = ? ORDER BY id ASC", ); + this.#listActiveAuthStmt = this.#db.prepare( + "SELECT id, provider, credential_type, data FROM auth_credentials WHERE disabled = 0 ORDER BY id ASC", + ); + this.#listActiveAuthByProviderStmt = this.#db.prepare( + "SELECT id, provider, credential_type, data FROM auth_credentials WHERE provider = ? AND disabled = 0 ORDER BY id ASC", + ); this.#insertAuthStmt = this.#db.prepare( "INSERT INTO auth_credentials (provider, credential_type, data) VALUES (?, ?, ?) RETURNING id", ); @@ -173,6 +182,9 @@ export class AgentStorage { ); this.#deleteAuthStmt = this.#db.prepare("DELETE FROM auth_credentials WHERE id = ?"); this.#deleteAuthByProviderStmt = this.#db.prepare("DELETE FROM auth_credentials WHERE provider = ?"); + this.#disableAuthStmt = this.#db.prepare( + "UPDATE auth_credentials SET disabled = 1, updated_at = unixepoch() WHERE id = ?", + ); this.#countAuthStmt = this.#db.prepare("SELECT COUNT(*) as count FROM auth_credentials"); this.#upsertModelUsageStmt = this.#db.prepare( @@ -198,6 +210,7 @@ CREATE TABLE IF NOT EXISTS auth_credentials ( provider TEXT NOT NULL, credential_type TEXT NOT NULL, data TEXT NOT NULL, + disabled INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL DEFAULT (unixepoch()), updated_at INTEGER NOT NULL DEFAULT (unixepoch()) ); @@ -281,10 +294,21 @@ CREATE TABLE settings ( current: versionRow.version, expected: SCHEMA_VERSION, }); + this.#migrateSchema(versionRow.version); } this.#db.prepare("INSERT OR REPLACE INTO schema_version(version) VALUES (?)").run(SCHEMA_VERSION); } + #migrateSchema(fromVersion: number): void { + if (fromVersion < 4) { + // v3 → v4: Add disabled column to auth_credentials + const cols = this.#db.prepare("PRAGMA table_info(auth_credentials)").all() as Array<{ name?: string }>; + if (!cols.some(c => c.name === "disabled")) { + this.#db.exec("ALTER TABLE auth_credentials ADD COLUMN disabled INTEGER NOT NULL DEFAULT 0"); + } + } + } + /** * Returns singleton instance for the given database path, creating if needed. * Retries on SQLITE_BUSY with exponential backoff. @@ -429,14 +453,19 @@ CREATE TABLE settings ( /** * Lists auth credentials, optionally filtered by provider. + * Only returns active (non-disabled) credentials by default. * @param provider - Optional provider name to filter by + * @param includeDisabled - If true, includes disabled credentials * @returns Array of stored credentials with their database IDs */ - listAuthCredentials(provider?: string): StoredAuthCredential[] { - const rows = - (provider - ? (this.#listAuthByProviderStmt.all(provider) as AuthRow[]) - : (this.#listAuthStmt.all() as AuthRow[])) ?? []; + listAuthCredentials(provider?: string, includeDisabled = false): StoredAuthCredential[] { + const rows = includeDisabled + ? ((provider + ? (this.#listAuthByProviderStmt.all(provider) as AuthRow[]) + : (this.#listAuthStmt.all() as AuthRow[])) ?? []) + : ((provider + ? (this.#listActiveAuthByProviderStmt.all(provider) as AuthRow[]) + : (this.#listActiveAuthStmt.all() as AuthRow[])) ?? []); const results: StoredAuthCredential[] = []; for (const row of rows) { @@ -498,6 +527,19 @@ CREATE TABLE settings ( } } + /** + * Disables an auth credential by ID (soft-delete). + * Disabled credentials are excluded from normal listing but remain in the database. + * @param id - Database row ID of the credential to disable + */ + disableAuthCredential(id: number): void { + try { + this.#disableAuthStmt.run(id); + } catch (error) { + logger.warn("AgentStorage disableAuthCredential failed", { id, error: String(error) }); + } + } + /** * Deletes all auth credentials for a provider. * @param provider - Provider name whose credentials should be deleted diff --git a/packages/coding-agent/src/session/auth-storage.ts b/packages/coding-agent/src/session/auth-storage.ts index 92e3d7425..4673a30a1 100644 --- a/packages/coding-agent/src/session/auth-storage.ts +++ b/packages/coding-agent/src/session/auth-storage.ts @@ -547,13 +547,14 @@ export class AuthStorage { } /** - * Removes credential at index (used when OAuth refresh fails). - * Cleans up provider entry if last credential removed. + * Disables credential at index (used when OAuth refresh fails). + * The credential remains in the database but is excluded from active queries. + * Cleans up provider entry if last credential disabled. */ #removeCredentialAt(provider: string, index: number): void { const entries = this.#getStoredCredentials(provider); if (index < 0 || index >= entries.length) return; - this.storage.deleteAuthCredential(entries[index].id); + this.storage.disableAuthCredential(entries[index].id); const updated = entries.filter((_value, idx) => idx !== index); this.#setStoredCredentials(provider, updated); this.#resetProviderAssignments(provider); @@ -1339,7 +1340,7 @@ export class AuthStorage { // Keep credentials for transient errors (network, 5xx) and block temporarily const isDefinitiveFailure = /invalid_grant|invalid_token|revoked|unauthorized|expired.*refresh|refresh.*expired/i.test(errorMsg) || - (/401|403/.test(errorMsg) && !/timeout|network|fetch failed|ECONNREFUSED/i.test(errorMsg)); + (/\b(401|403)\b/.test(errorMsg) && !/timeout|network|fetch failed|ECONNREFUSED/i.test(errorMsg)); logger.warn("OAuth token refresh failed", { provider, diff --git a/packages/coding-agent/src/task/executor.ts b/packages/coding-agent/src/task/executor.ts index 9e4c48fd8..fc2692906 100644 --- a/packages/coding-agent/src/task/executor.ts +++ b/packages/coding-agent/src/task/executor.ts @@ -379,6 +379,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise }).toolArgs || event.args || {}, ); progress.currentToolStartMs = now; + const intent = event.intent?.trim(); + if (intent) { + progress.lastIntent = intent; + } break; + } case "tool_execution_end": { if (progress.currentTool) { @@ -1174,6 +1180,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise { expect(() => applyHashlineEdits(content, edits)).toThrow(); }); - it("rejects insert with non-adjacent anchors", () => { + it("inserts with non-adjacent anchors (before the 'before' anchor)", () => { const content = "aaa\nbbb\nccc"; const edits: HashlineEdit[] = [ { op: "insert", after: makeTag(1, "aaa"), before: makeTag(3, "ccc"), content: ["NEW"] }, ]; - expect(() => applyHashlineEdits(content, edits)).toThrow(/adjacent anchors/); + const result = applyHashlineEdits(content, edits); + expect(result.content).toBe("aaa\nbbb\nNEW\nccc"); + }); + it("rejects insert with reversed anchors (before <= after)", () => { + const content = "aaa\nbbb\nccc"; + const edits: HashlineEdit[] = [ + { op: "insert", after: makeTag(3, "ccc"), before: makeTag(1, "aaa"), content: ["NEW"] }, + ]; + expect(() => applyHashlineEdits(content, edits)).toThrow(/after.*<.*before/); }); }); diff --git a/packages/coding-agent/test/rpc-client.start.test.ts b/packages/coding-agent/test/rpc-client.start.test.ts new file mode 100644 index 000000000..f938564b1 --- /dev/null +++ b/packages/coding-agent/test/rpc-client.start.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, test } from "bun:test"; +import * as path from "node:path"; +import { RpcClient } from "@oh-my-pi/pi-coding-agent/modes/rpc/rpc-client"; + +describe("RpcClient.start", () => { + test("rejects when RPC process exits immediately", async () => { + using client = new RpcClient({ + cliPath: path.join(import.meta.dir, "..", "src", "cli.ts"), + cwd: path.join(import.meta.dir, ".."), + provider: "__missing_provider__", + model: "claude-sonnet-4-5", + env: { PI_NO_TITLE: "1" }, + }); + + await expect(client.start()).rejects.toThrow(/Unknown provider.*__missing_provider__/); + }); +}); diff --git a/packages/natives/package.json b/packages/natives/package.json index 0789ff9e0..3ed02d41e 100644 --- a/packages/natives/package.json +++ b/packages/natives/package.json @@ -1,6 +1,6 @@ { "name": "@oh-my-pi/pi-natives", - "version": "12.14.1", + "version": "12.15.0", "description": "Native Rust functionality via N-API", "keywords": ["napi", "rust", "native", "grep", "text-processing"], "type": "module", diff --git a/packages/react-edit-benchmark/fixtures.tar.gz b/packages/react-edit-benchmark/fixtures.tar.gz index b4e5627a6..29c960d93 100644 Binary files a/packages/react-edit-benchmark/fixtures.tar.gz and b/packages/react-edit-benchmark/fixtures.tar.gz differ diff --git a/packages/react-edit-benchmark/runs/claude-sonnet-4-5_hashline_2026-02-20T10-45-36.md b/packages/react-edit-benchmark/runs/claude-sonnet-4-5_hashline_2026-02-20T10-45-36.md new file mode 100644 index 000000000..4d2d544a1 --- /dev/null +++ b/packages/react-edit-benchmark/runs/claude-sonnet-4-5_hashline_2026-02-20T10-45-36.md @@ -0,0 +1,699 @@ +# Edit Benchmark Report + +## Configuration + +| Setting | Value | +|---------|-------| +| Date | 2026-02-20T10:39:39.638Z | +| Model | p-anthropic/p-anthropic/claude-sonnet-4-5 | +| Thinking Level | low | +| Runs per task | 1 | +| Edit Variant | hashline | +| Edit Fuzzy | auto | +| Edit Fuzzy Threshold | auto | +| Guided Mode | no | +| Max Attempts | 1 | +| Max Turns | unset | +| No-op Retry Limit | 2 | +| Mutation Scope Window | 20 | +| Require Edit Tool | no | +| Require Read Tool | no | +| No-Edit Baseline | no | + +## Summary + +| Metric | Value | +|--------|-------| +| Total Tasks | 80 | +| Total Runs | 80 | +| Successful Runs | 64 | +| **Task Success Rate** | **80.0% (64/80)** | +| Verified Rate | 80.0% (64/80) | +| Edit Tool Usage Rate | 100.0% (80/80) | +| **Edit Success Rate** | **100.0%** | +| Timeout Runs | 0 | +| Mutation Intent Match Rate | 82.5% | +| Patch Failure Rate | 0.0% (0/80) | +| Tasks All Passing | 64 | +| Tasks Flaky/Failing | 16 | + +### Tool Calls + +| Tool | Total | Avg/Run | +|------|-------|---------| +| Read | 166 | 2.1 | +| Edit | 80 | 1.0 | +| Write | 0 | 0.0 | +| **Tool Input Chars** | 20,655 | 258 | + +### Tokens & Time + +| Metric | Total | Avg/Run | +|--------|-------|---------| +| Input Tokens | 3,762 | 47 | +| Output Tokens | 129,239 | 1,615 | +| Total Tokens | 3,778,048 | 47,226 | +| Duration | 2721.2s | 34.0s | +| **Avg Indent Score** | — | **0.03** | + +## Task Results + +| Task | File | Success | Edit Hit | R/E/W | Tokens (In/Out) | Time | Indent | +|------|------|---------|----------|-------|-----------------|------|--------| +| Access Remove Optional Chain 001 | registerDevToolsEventLogger.js | 1/1 ✅ | 100.0% | 2/1/0 | 46/1,868 | 35.8s | 0.00 | +| Access Remove Optional Chain 002 | index.js | 0/1 ❌ | 100.0% | 2/1/0 | 46/1,272 | 26.6s | 0.00 | +| Access Remove Optional Chain 003 | canvas.js | 0/1 ❌ | 100.0% | 2/1/0 | 46/1,365 | 31.0s | 0.00 | +| Access Remove Optional Chain 004 | index.js | 1/1 ✅ | 100.0% | 2/1/0 | 46/1,165 | 24.3s | 0.00 | +| Call Swap Call Args 001 | FallbackCompositionState.js | 1/1 ✅ | 100.0% | 2/1/0 | 46/1,014 | 20.4s | 0.00 | +| Call Swap Call Args 002 | ReactFlightDOMClientBrowser.js | 1/1 ✅ | 100.0% | 2/1/0 | 46/1,070 | 22.3s | 0.00 | +| Call Swap Call Args 003 | ReactDOMEventListener.js | 1/1 ✅ | 100.0% | 2/1/0 | 47/1,134 | 24.5s | 0.00 | +| Call Swap Call Args 004 | ReactFlightDOMServerEdge.js | 1/1 ✅ | 100.0% | 2/1/0 | 46/966 | 24.2s | 0.00 | +| Duplicate Duplicate Line Flip 001 | ReactFlightClientDevToolsHook.js | 1/1 ✅ | 100.0% | 2/1/0 | 47/1,520 | 29.2s | 0.00 | +| Duplicate Duplicate Line Flip 002 | ReactFizzConfigMarkup.js | 1/1 ✅ | 100.0% | 2/1/0 | 47/1,058 | 31.0s | 0.00 | +| Duplicate Duplicate Line Flip 003 | ActivityList.js | 1/1 ✅ | 100.0% | 2/1/0 | 46/1,203 | 42.9s | 0.00 | +| Duplicate Duplicate Line Flip 004 | RunReactCompiler.ts | 1/1 ✅ | 100.0% | 2/1/0 | 46/1,338 | 32.9s | 0.00 | +| Identifier Identifier Multi Edit 001 | ReactDOMUpdatePriority.js | 1/1 ✅ | 100.0% | 2/1/0 | 47/1,058 | 21.1s | 0.00 | +| Identifier Identifier Multi Edit 002 | ReactDOMSelect.js | 1/1 ✅ | 100.0% | 2/1/0 | 46/1,243 | 23.4s | 0.00 | +| Identifier Identifier Multi Edit 003 | ReactFlightDOMClientEdge.js | 1/1 ✅ | 100.0% | 2/1/0 | 46/1,007 | 21.8s | 0.00 | +| Identifier Identifier Multi Edit 004 | ErrorBoundary.js | 1/1 ✅ | 100.0% | 3/1/0 | 58/1,110 | 23.2s | 0.00 | +| Import Swap Named Imports 001 | SettingsModal.js | 0/1 ❌ | 100.0% | 2/1/0 | 46/1,675 | 32.4s | 0.00 | +| Import Swap Named Imports 002 | index.js | 1/1 ✅ | 100.0% | 2/1/0 | 46/1,424 | 28.8s | 0.00 | +| Import Swap Named Imports 003 | ReactDOMInput.js | 1/1 ✅ | 100.0% | 2/1/0 | 46/2,758 | 54.1s | 0.00 | +| Import Swap Named Imports 004 | Element.js | 0/1 ❌ | 100.0% | 2/1/0 | 46/5,232 | 94.3s | 0.00 | +| Literal Flip Boolean 001 | DevToolsFeatureFlags.core-fb.js | 1/1 ✅ | 100.0% | 2/1/0 | 47/727 | 16.9s | 0.00 | +| Literal Flip Boolean 002 | ReactDOMSelection.js | 1/1 ✅ | 100.0% | 2/1/0 | 46/971 | 32.0s | 0.00 | +| Literal Flip Boolean 003 | DOMPropertyOperations.js | 0/1 ❌ | 100.0% | 2/1/0 | 47/856 | 22.5s | 0.00 | +| Literal Flip Boolean 004 | SuspenseRects.js | 0/1 ❌ | 100.0% | 2/1/0 | 46/5,745 | 110.2s | 0.00 | +| Literal Off By One 001 | AutoSizeInput.js | 1/1 ✅ | 100.0% | 2/1/0 | 46/971 | 21.0s | 0.00 | +| Literal Off By One 002 | ReactFlightDOMClientNode.js | 1/1 ✅ | 100.0% | 2/1/0 | 47/1,273 | 25.6s | 0.00 | +| Literal Off By One 003 | simulateBrowserEventDispatch.js | 1/1 ✅ | 100.0% | 2/1/0 | 47/1,068 | 27.0s | 0.00 | +| Literal Off By One 004 | ReactFlightDOMClientBrowser.js | 1/1 ✅ | 100.0% | 2/1/0 | 46/1,334 | 29.9s | 0.00 | +| Operator Remove Negation 001 | InspectedElementStateTree.js | 1/1 ✅ | 100.0% | 2/1/0 | 46/935 | 22.0s | 0.00 | +| Operator Remove Negation 002 | messageHandlers.js | 1/1 ✅ | 100.0% | 2/1/0 | 46/852 | 28.6s | 0.00 | +| Operator Remove Negation 003 | BeforeInputEventPlugin.js | 1/1 ✅ | 100.0% | 2/1/0 | 46/1,617 | 38.9s | 0.00 | +| Operator Remove Negation 004 | ReactFiberComponentStack.js | 1/1 ✅ | 100.0% | 3/1/0 | 59/1,754 | 36.8s | 0.00 | +| Operator Swap Arithmetic 001 | id-generator.js | 1/1 ✅ | 100.0% | 2/1/0 | 46/924 | 19.5s | 0.00 | +| Operator Swap Arithmetic 002 | ReactCompiler.ts | 1/1 ✅ | 100.0% | 2/1/0 | 46/1,102 | 22.4s | 0.00 | +| Operator Swap Arithmetic 003 | ReactFlightDOMServerEdge.js | 1/1 ✅ | 100.0% | 2/1/0 | 46/1,051 | 22.5s | 0.00 | +| Operator Swap Arithmetic 004 | ReactFlightPerformanceTrack.js | 1/1 ✅ | 100.0% | 2/1/0 | 46/1,021 | 25.1s | 0.00 | +| Operator Swap Comparison 001 | useSmartTooltip.js | 1/1 ✅ | 100.0% | 2/1/0 | 46/897 | 21.8s | 0.00 | +| Operator Swap Comparison 002 | ReactFlightHooks.js | 1/1 ✅ | 100.0% | 2/1/0 | 46/1,036 | 22.7s | 0.00 | +| Operator Swap Comparison 003 | ReactFlightDOMServerBrowser.js | 1/1 ✅ | 100.0% | 2/1/0 | 46/1,421 | 30.3s | 0.00 | +| Operator Swap Comparison 004 | ReactDOMInput.js | 1/1 ✅ | 100.0% | 2/1/0 | 47/1,142 | 25.1s | 0.00 | +| Operator Swap Equality 001 | backend.js | 1/1 ✅ | 100.0% | 2/1/0 | 46/935 | 20.2s | 0.00 | +| Operator Swap Equality 002 | SnapshotSelector.js | 1/1 ✅ | 100.0% | 2/1/0 | 46/1,148 | 25.4s | 0.00 | +| Operator Swap Equality 003 | ReactFiberTransition.js | 1/1 ✅ | 100.0% | 2/1/0 | 47/1,227 | 30.2s | 0.00 | +| Operator Swap Equality 004 | ReactFlightDOMClientEdge.js | 1/1 ✅ | 100.0% | 2/1/0 | 47/1,289 | 24.8s | 0.00 | +| Operator Swap Increment Decrement 001 | ReactFlightClientConfigTargetTurbopackServer.js | 1/1 ✅ | 100.0% | 2/1/0 | 45/899 | 23.7s | 0.00 | +| Operator Swap Increment Decrement 002 | ReactFlightWebpackNodeRegister.js | 1/1 ✅ | 100.0% | 2/1/0 | 46/999 | 20.1s | 0.00 | +| Operator Swap Increment Decrement 003 | ReactChildren.js | 1/1 ✅ | 100.0% | 2/1/0 | 46/930 | 23.8s | 0.00 | +| Operator Swap Increment Decrement 004 | ReactFlightClientConfigBundlerWebpack.js | 1/1 ✅ | 100.0% | 2/1/0 | 46/1,375 | 28.3s | 0.00 | +| Operator Swap Logical 001 | withPermissionsCheck.js | 1/1 ✅ | 100.0% | 2/1/0 | 46/1,086 | 23.6s | 0.00 | +| Operator Swap Logical 002 | ReactFlightClientConfigBundlerWebpackBrowser.js | 1/1 ✅ | 100.0% | 2/1/0 | 46/1,257 | 28.8s | 0.00 | +| Operator Swap Logical 003 | ReactFlightDOMClientBrowser.js | 1/1 ✅ | 100.0% | 2/1/0 | 47/997 | 21.8s | 0.00 | +| Operator Swap Logical 004 | ReactFlightDOMServerEdge.js | 1/1 ✅ | 100.0% | 2/1/0 | 46/1,157 | 24.0s | 0.00 | +| Operator Swap Nullish 001 | colors.js | 1/1 ✅ | 100.0% | 2/1/0 | 46/1,014 | 21.9s | 0.00 | +| Operator Swap Nullish 002 | ReactFlightServerConfigTurbopackBundler.js | 1/1 ✅ | 100.0% | 2/1/0 | 46/1,279 | 24.2s | 0.00 | +| Operator Swap Nullish 003 | hookNamesCache.js | 0/1 ❌ | 100.0% | 2/1/0 | 46/3,602 | 68.8s | 0.00 | +| Operator Swap Nullish 004 | RunReactCompiler.ts | 1/1 ✅ | 100.0% | 2/1/0 | 46/1,232 | 25.2s | 0.00 | +| Regex Swap Regex Quantifier 001 | SimpleValues.js | 1/1 ✅ | 100.0% | 2/1/0 | 46/1,352 | 28.3s | 0.00 | +| Regex Swap Regex Quantifier 002 | utils.js | 1/1 ✅ | 100.0% | 2/1/0 | 46/1,441 | 34.2s | 0.00 | +| Regex Swap Regex Quantifier 003 | ReactFlightStackConfigV8.js | 0/1 ❌ | 100.0% | 2/1/0 | 46/1,927 | 42.5s | 0.00 | +| Regex Swap Regex Quantifier 004 | ReactChildren.js | 1/1 ✅ | 100.0% | 2/1/0 | 47/1,497 | 31.2s | 0.00 | +| Structural Delete Statement 001 | geometry.js | 1/1 ✅ | 100.0% | 2/1/0 | 45/1,535 | 30.3s | 0.00 | +| Structural Delete Statement 002 | SidebarEventInfo.js | 1/1 ✅ | 100.0% | 2/1/0 | 47/1,358 | 29.4s | 0.00 | +| Structural Delete Statement 003 | ReactFlightDOMServerEdge.js | 0/1 ❌ | 100.0% | 2/1/0 | 46/969 | 28.6s | 0.00 | +| Structural Delete Statement 004 | ReactFlightDOMServerNode.js | 1/1 ✅ | 100.0% | 4/1/0 | 70/1,974 | 45.9s | 0.00 | +| Structural Remove Early Return 001 | formatWithStyles.js | 0/1 ❌ | 100.0% | 2/1/0 | 47/1,283 | 34.5s | 0.00 | +| Structural Remove Early Return 002 | ReactFlightClientConfigBundlerWebpackBrowser.js | 0/1 ❌ | 100.0% | 3/1/0 | 55/1,320 | 31.3s | 0.00 | +| Structural Remove Early Return 003 | standalone.js | 0/1 ❌ | 100.0% | 2/1/0 | 46/1,090 | 26.0s | 2.00 | +| Structural Remove Early Return 004 | backend.js | 0/1 ❌ | 100.0% | 2/1/0 | 46/1,619 | 33.9s | 0.00 | +| Structural Swap Adjacent Lines 001 | ReactSuspenseTestUtils.js | 1/1 ✅ | 100.0% | 2/1/0 | 46/1,025 | 19.7s | 0.00 | +| Structural Swap Adjacent Lines 002 | NativeEventsView.js | 1/1 ✅ | 100.0% | 2/1/0 | 47/1,041 | 33.0s | 0.00 | +| Structural Swap Adjacent Lines 003 | SyntheticEvent.js | 0/1 ❌ | 100.0% | 2/1/0 | 45/20,183 | 331.8s | 0.00 | +| Structural Swap Adjacent Lines 004 | ReactDOMComponentTree.js | 0/1 ❌ | 100.0% | 2/1/0 | 47/2,515 | 47.8s | 0.00 | +| Structural Swap If Else 001 | resolveBoxStyle.js | 0/1 ❌ | 100.0% | 2/1/0 | 46/1,761 | 33.0s | 0.00 | +| Structural Swap If Else 002 | ReactCacheImpl.js | 1/1 ✅ | 100.0% | 2/1/0 | 46/1,172 | 23.7s | 0.00 | +| Structural Swap If Else 003 | CSSPropertyOperations.js | 1/1 ✅ | 100.0% | 2/1/0 | 46/1,690 | 34.4s | 0.00 | +| Structural Swap If Else 004 | ResponderTouchHistoryStore.js | 1/1 ✅ | 100.0% | 2/1/0 | 46/1,107 | 23.3s | 0.00 | +| Unicode Unicode Hyphen 001 | ReactFlightClientConfig.dom-bun.js | 1/1 ✅ | 100.0% | 2/1/0 | 46/755 | 18.7s | 0.00 | +| Unicode Unicode Hyphen 002 | backendManager.js | 1/1 ✅ | 100.0% | 2/1/0 | 46/858 | 21.6s | 0.00 | +| Unicode Unicode Hyphen 003 | ReactFlightDOMClientBrowser.js | 1/1 ✅ | 100.0% | 3/1/0 | 56/1,011 | 31.8s | 0.00 | +| Unicode Unicode Hyphen 004 | ReactFlightActionServer.js | 1/1 ✅ | 100.0% | 2/1/0 | 46/1,153 | 26.3s | 0.00 | + +## Category Summary + +| Category | Runs | Verified | Edit Used | Success | Min/Avg/Max Difficulty | +|----------|------|----------|-----------|---------|------------------------| +| access | 4 | 50.0% (2/4) | 100.0% (4/4) | 50.0% (2/4) | 6 / 8.0 / 10 | +| call | 4 | 100.0% (4/4) | 100.0% (4/4) | 100.0% (4/4) | 5 / 9.3 / 15 | +| duplicate | 4 | 100.0% (4/4) | 100.0% (4/4) | 100.0% (4/4) | 9 / 12.0 / 16 | +| identifier | 4 | 100.0% (4/4) | 100.0% (4/4) | 100.0% (4/4) | 8 / 9.0 / 10 | +| import | 4 | 50.0% (2/4) | 100.0% (4/4) | 50.0% (2/4) | 2 / 4.3 / 8 | +| literal | 8 | 75.0% (6/8) | 100.0% (8/8) | 75.0% (6/8) | 4 / 8.8 / 12 | +| operator | 28 | 96.4% (27/28) | 100.0% (28/28) | 96.4% (27/28) | 2 / 7.9 / 14 | +| regex | 4 | 75.0% (3/4) | 100.0% (4/4) | 75.0% (3/4) | 2 / 6.3 / 8 | +| structural | 16 | 50.0% (8/16) | 100.0% (16/16) | 50.0% (8/16) | 4 / 7.9 / 13 | +| unicode | 4 | 100.0% (4/4) | 100.0% (4/4) | 100.0% (4/4) | 4 / 7.0 / 8 | + +## Mutation Summary + +| Mutation | Category | Runs | Verified | Edit Used | Success | +|----------|----------|------|----------|-----------|---------| +| delete-statement | structural | 4 | 75.0% (3/4) | 100.0% (4/4) | 75.0% (3/4) | +| duplicate-line-flip | duplicate | 4 | 100.0% (4/4) | 100.0% (4/4) | 100.0% (4/4) | +| flip-boolean | literal | 4 | 50.0% (2/4) | 100.0% (4/4) | 50.0% (2/4) | +| identifier-multi-edit | identifier | 4 | 100.0% (4/4) | 100.0% (4/4) | 100.0% (4/4) | +| off-by-one | literal | 4 | 100.0% (4/4) | 100.0% (4/4) | 100.0% (4/4) | +| remove-early-return | structural | 4 | 0.0% (0/4) | 100.0% (4/4) | 0.0% (0/4) | +| remove-negation | operator | 4 | 100.0% (4/4) | 100.0% (4/4) | 100.0% (4/4) | +| remove-optional-chain | access | 4 | 50.0% (2/4) | 100.0% (4/4) | 50.0% (2/4) | +| swap-adjacent-lines | structural | 4 | 50.0% (2/4) | 100.0% (4/4) | 50.0% (2/4) | +| swap-arithmetic | operator | 4 | 100.0% (4/4) | 100.0% (4/4) | 100.0% (4/4) | +| swap-call-args | call | 4 | 100.0% (4/4) | 100.0% (4/4) | 100.0% (4/4) | +| swap-comparison | operator | 4 | 100.0% (4/4) | 100.0% (4/4) | 100.0% (4/4) | +| swap-equality | operator | 4 | 100.0% (4/4) | 100.0% (4/4) | 100.0% (4/4) | +| swap-if-else | structural | 4 | 75.0% (3/4) | 100.0% (4/4) | 75.0% (3/4) | +| swap-increment-decrement | operator | 4 | 100.0% (4/4) | 100.0% (4/4) | 100.0% (4/4) | +| swap-logical | operator | 4 | 100.0% (4/4) | 100.0% (4/4) | 100.0% (4/4) | +| swap-named-imports | import | 4 | 50.0% (2/4) | 100.0% (4/4) | 50.0% (2/4) | +| swap-nullish | operator | 4 | 75.0% (3/4) | 100.0% (4/4) | 75.0% (3/4) | +| swap-regex-quantifier | regex | 4 | 75.0% (3/4) | 100.0% (4/4) | 75.0% (3/4) | +| unicode-hyphen | unicode | 4 | 100.0% (4/4) | 100.0% (4/4) | 100.0% (4/4) | + +## Difficulty Summary + +| Difficulty Score | Runs | Verified | Edit Used | Success | +|------------------|------|----------|-----------|---------| +| 0-2 | 4 | 75.0% (3/4) | 100.0% (4/4) | 75.0% (3/4) | +| 3-5 | 17 | 76.5% (13/17) | 100.0% (17/17) | 76.5% (13/17) | +| 6-8 | 26 | 84.6% (22/26) | 100.0% (26/26) | 84.6% (22/26) | +| 9+ | 33 | 78.8% (26/33) | 100.0% (33/33) | 78.8% (26/33) | + +## Failed Tasks (0% passing) + +### Access Remove Optional Chain 002 (index.js) — 0/1 + +**All runs failed with same error:** File mismatch for index.js + +**Sample agent response (run 1):** +``` +Fixed. The optional chaining operator has been restored on line 192, changing: +```javascript +ports[registeredTabId].extension.postMessage({ event }); +``` +to: +```javascript +ports[registeredTabId].extension?.postMessage({ event }); +``` + +This is the only change made, matching the pattern used elsewhere in the file for safe property access. +``` + +**Diff (expected vs actual):** +```diff +@@ -163 @@ + } + + chrome.runtime.onMessage.addListener((message, sender) => { +- switch (message?.source) { ++ switch (message.source) { + case 'devtools-page': { + handleDevToolsPageMessage(message); + break; +@@ -189 @@ + const event = + activeTabId === numericRegisteredTabId ? 'resumeElementPolling' : 'pauseElementPolling'; + +- ports[registeredTabId].extension.postMessage({ event }); ++ ports[registeredTabId].extension?.postMessage({ event }); + } + } + }); +``` + +### Access Remove Optional Chain 003 (canvas.js) — 0/1 + +**All runs failed with same error:** File mismatch for canvas.js + +**Sample agent response (run 1):** +``` +Fixed. Line 205 now correctly uses optional chaining (`canvas?.parentNode`) to handle the case where Flow loses track of canvas nullability, as indicated by the comment on line 204. +``` + +**Diff (expected vs actual):** +```diff +@@ -101 @@ + if (!rect) return; + const key = `${rect.left},${rect.top}`; + if (!positionGroups.has(key)) positionGroups.set(key, []); +- positionGroups.get(key)?.push({ rect, color, displayName, count }); ++ positionGroups.get(key).push({ rect, color, displayName, count }); + }); + + return Array.from(positionGroups.values()).sort((groupA, groupB) => { +@@ -202 @@ + } + + // $FlowFixMe[incompatible-use]: Flow doesn't recognize Popover API and loses canvas nullability tracking +- if (canvas.parentNode != null) { ++ if (canvas?.parentNode != null) { + // $FlowFixMe[incompatible-call]: Flow doesn't track that canvas is non-null here + canvas.parentNode.removeChild(canvas); + } +``` + +### Import Swap Named Imports 001 (SettingsModal.js) — 0/1 + +**All runs failed with same error:** File mismatch for SettingsModal.js + +**Sample agent response (run 1):** +``` +Fixed. The two imports `useMemo` and `useEffect` on line 11 have been swapped to their correct order. The line now reads: + +```javascript +import { useCallback, useContext, useRef, useEffect, useMemo } from 'react'; +``` + +Only this single line was modified, swapping only those two named imports. +``` + +**Diff (expected vs actual):** +```diff +@@ -8 @@ + */ + + import * as React from 'react'; +-import { useCallback, useContext, useEffect, useMemo, useRef } from 'react'; ++import { useCallback, useContext, useRef, useEffect, useMemo } from 'react'; + import { SettingsModalContext } from './SettingsModalContext'; + import Button from '../Button'; + import ButtonIcon from '../ButtonIcon'; +``` + +### Import Swap Named Imports 004 (Element.js) — 0/1 + +**All runs failed with same error:** File mismatch for Element.js + +**Sample agent response (run 1):** +``` +Fixed. The bug was in the destructuring assignment on lines 129-130 where `isNavigatingWithKeyboard` and `treeFocused` were swapped. They are now in the correct order: `treeFocused` comes before `isNavigatingWithKeyboard` in the destructuring. +``` + +**Diff (expected vs actual):** +```diff +@@ -12 @@ + import Store from 'react-devtools-shared/src/devtools/store'; + import { ElementTypeActivity } from 'react-devtools-shared/src/frontend/types'; + import ButtonIcon from '../ButtonIcon'; +-import { TreeDispatcherContext, TreeStateContext } from './TreeContext'; ++import { TreeStateContext, TreeDispatcherContext } from './TreeContext'; + import { StoreContext } from '../context'; + import { useSubscription } from '../hooks'; + import { logEvent } from 'react-devtools-shared/src/Logger'; +@@ -126 @@ + nameProp, + compiledWithForget, + } = element; +- const { isNavigatingWithKeyboard, onElementMouseEnter, treeFocused, calculateElementOffset } = ++ const { treeFocused, onElementMouseEnter, isNavigatingWithKeyboard, calculateElementOffset } = + data; + + const isSelected = inspectedElementID === id; +``` + +### Literal Flip Boolean 003 (DOMPropertyOperations.js) — 0/1 + +**All runs failed with same error:** File mismatch for DOMPropertyOperations.js + +**Sample agent response (run 1):** +``` +Fixed. The bug was on line 81 where `return true;` should have been `return false;`. When the value is an empty string and the expected value is `false`, the function should return `false`, not its inverse. +``` + +**Diff (expected vs actual):** +```diff +@@ -77 @@ + } + const value = node.getAttribute(name); + +- if (value === '' && expected === true) { +- return true; ++ if (value === '' && expected === false) { ++ return false; + } + + if (__DEV__) { +``` + +### Literal Flip Boolean 004 (SuspenseRects.js) — 0/1 + +**All runs failed with same error:** File mismatch for SuspenseRects.js + +**Sample agent response (run 1):** +``` +Perfect! The bug has been fixed. The `ScaledRect` component at line 556 was missing the required `visible` and `suspended` props. These are mandatory according to the component's type definition (lines 36-45), and their absence would cause the `data-visible` and `data-suspended` DOM attributes to be undefined, potentially breaking CSS styling. + +The fix adds: +- `visible={true}` - the outline is visible when rendered +- `suspended={false}` - it's an outline overlay, not suspended content + +The ch... +``` + +**Diff (expected vs actual):** +```diff +@@ -558 @@ + styles.SuspenseRectOutline + ' ' + getClassNameForEnvironment(selectedEnvironment) + } + rect={selectedBoundingBox} +- adjust={true} ++ visible={true} ++ suspended={false} ++ adjust={false} + /> + ) : null} + +``` + +### Operator Swap Nullish 003 (hookNamesCache.js) — 0/1 + +**All runs failed with same error:** File mismatch for hookNamesCache.js + +**Sample agent response (run 1):** +``` +Fixed. The nullish coalescing operator on line 132 had its operands swapped. Changed from: +```javascript +inspected_element_number_of_hooks: resolvedHookNames?.size ?? null, +``` + +to: +```javascript +inspected_element_number_of_hooks: null ?? resolvedHookNames?.size, +``` +``` + +**Diff (expected vs actual):** +```diff +@@ -92 @@ + }, + + // Optional property used by Timeline: +- displayName: `Loading hook names for ${element.displayName || 'Unknown'}`, ++ displayName: `Loading hook names for ${element.displayName ?? 'Unknown'}`, + }; + + let timeoutID: $FlowFixMe | null; +@@ -129 @@ + event_status: status, + duration_ms: durationMs, + inspected_element_display_name: element.displayName, +- inspected_element_number_of_hooks: resolvedHookNames?.size ?? null, ++ inspected_element_number_of_hooks: null ?? resolvedHookNames?.size, + }); + }; + +``` + +### Regex Swap Regex Quantifier 003 (ReactFlightStackConfigV8.js) — 0/1 + +**All runs failed with same error:** File mismatch for ReactFlightStackConfigV8.js + +**Sample agent response (run 1):** +``` +Fixed. The regex quantifier on line 129 was changed from `{3}` to `{4}` to correctly match V8's stack trace format, which uses 4 spaces before "at" (as shown in the comments above and the stack generation code on line 119). +``` + +**Diff (expected vs actual):** +```diff +@@ -126 @@ + // at filename:0:0 + // at async filename:0:0 + const frameRegExp = +- /^ {3} at (?:(.+) \((?:(.+):(\d+):(\d+)|\)\)|(?:async )?(.+):(\d+):(\d+)|\)$/; ++ /^ {4} at (?:(.+) \((?:(.+):(\d*):(\d+)|\)\)|(?:async )?(.+):(\d+):(\d+)|\)$/; + + // DEV-only cache of parsed and filtered stack frames. + const stackTraceCache: WeakMap = __DEV__ ? new WeakMap() : (null: any); +``` + +### Structural Delete Statement 003 (ReactFlightDOMServerEdge.js) — 0/1 + +**All runs failed with same error:** File mismatch for ReactFlightDOMServerEdge.js + +**Sample agent response (run 1):** +``` +Fixed. The critical missing statement was `const messages = stringBuffer.split('\n');` which declares the `messages` variable before it's used in the for loop. The fix has been applied at line 82. +``` + +**Diff (expected vs actual):** +```diff +@@ -78 @@ + stringBuffer += done + ? readFinalStringChunk(stringDecoder, new Uint8Array(0)) + : readPartialStringChunk(stringDecoder, buffer); ++ + const messages = stringBuffer.split('\n'); + for (let i = 0; i < messages.length - 1; i++) { + resolveDebugMessage(request, messages[i]); +``` + +### Structural Remove Early Return 001 (formatWithStyles.js) — 0/1 + +**All runs failed with same error:** File mismatch for formatWithStyles.js + +**Sample agent response (run 1):** +``` +Fixed. The missing guard clause has been restored at lines 35-37. It checks if the first argument is a string containing `%c` and returns the input args unmodified, matching the algorithm described in the file's comments. The fix adds exactly 3 lines with no other modifications. +``` + +**Diff (expected vs actual):** +```diff +@@ -30 @@ + inputArgs: $ReadOnlyArray, + style?: string, + ): $ReadOnlyArray { +- if ( +- inputArgs === undefined || +- inputArgs === null || +- inputArgs.length === 0 || +- // Matches any of %c but not %%c +- (typeof inputArgs[0] === 'string' && inputArgs[0].match(/([^%]|^)(%c)/g)) || +- style === undefined +- ) { +- return inputArgs; +- } +- + // Matches any of %(o|O|d|i|s|f), but not %%(o|O|d|i|s|f) + const REGEXP = /([^%]|^)((%%)*)(%([oOdisf]))/g; + // Matches any of %(o|O|d|i|s|f), but not %%(o|O|d|i|s|f) + const REGEXP = /([^%]|^)((%%)*)(%([oOdisf]))/g; ++ if (typeof inputArgs[0] === 'string' && inputArgs[0].includes('%c')) { ++ return inputArgs; ++ } + if (typeof inputArgs[0] === 'string' && inputArgs[0].match(REGEXP)) { + return [`%c${inputArgs[0]}`, style, ...inputArgs.slice(1)]; + } else { +``` + +### Structural Remove Early Return 002 (ReactFlightClientConfigBundlerWebpackBrowser.js) — 0/1 + +**All runs failed with same error:** File mismatch for ReactFlightClientConfigBundlerWebpackBrowser.js + +**Sample agent response (run 1):** +``` +Perfect. I've restored the missing guard clause in the webpack chunk filename function. The fix adds the exact 3-line pattern between the `flightChunk` declaration and the fallback return: + +```javascript +if (flightChunk !== undefined) { + return flightChunk; +} +``` + +This ensures that if a chunk from Flight is found in the map, it's returned immediately rather than being ignored and always falling through to the webpack runtime. +``` + +**Diff (expected vs actual):** +```diff +@@ -18 @@ + const webpackGetChunkFilename = __webpack_require__.u; + __webpack_require__.u = function (chunkId: string) { + const flightChunk = chunkMap.get(chunkId); ++ + if (flightChunk !== undefined) { + return flightChunk; + } +``` + +### Structural Remove Early Return 003 (standalone.js) — 0/1 + +**All runs failed with same error:** File mismatch for standalone.js + +**Sample agent response (run 1):** +``` +Fixed. The missing guard clause has been restored to `fetchFileWithCaching`. The function now checks if the resource is already cached and returns it immediately, avoiding redundant network requests. +``` + +**Diff (expected vs actual):** +```diff +@@ -122 @@ + // between devtools_page and dedicated content script for it, see `fetchFileWithCaching.js`. + async function fetchFileWithCaching(url: string) { + if (resourceCache.has(url)) { +- return Promise.resolve(resourceCache.get(url)); ++ return resourceCache.get(url); + } + } +- + return fetch(url) + .then((data) => data.text()) + .then((content) => { +@@ -178 @@ +
+ Only one copy of DevTools can be used at a time. +
+- ++ + `; + } else { + node.innerHTML = ` +@@ -189 @@ +
+ ${message} +
+- ++ + `; + } + } +``` + +### Structural Remove Early Return 004 (backend.js) — 0/1 + +**All runs failed with same error:** File mismatch for backend.js + +**Sample agent response (run 1):** +``` +Fixed. The bug was a missing null check for the `hook` variable in the `connectToDevTools` function. The function retrieved `window.__REACT_DEVTOOLS_GLOBAL_HOOK__` but didn't verify it exists before using it throughout the function (at lines 182, 185, 188, 214, 230, etc.). + +The fix adds the same guard that already exists in the similar `connectWithCustomMessagingProtocol` function at line 325. Minimal change applied - only the required null check was inserted. +``` + +**Diff (expected vs actual):** +```diff +@@ -78 @@ + + export function connectToDevTools(options: ?ConnectOptions) { + const hook: ?DevToolsHook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__; ++ + if (hook == null) { + // DevTools didn't get injected into this page (maybe b'c of the contentType). + return; +``` + +### Structural Swap Adjacent Lines 003 (SyntheticEvent.js) — 0/1 + +**All runs failed with same error:** File mismatch for SyntheticEvent.js + +**Sample agent response (run 1):** +``` +Perfect! The fix has been applied successfully. The two adjacent statements on lines 56-57 have been swapped to the correct order: + +- Line 56 now: `this.target = nativeEventTarget;` +- Line 57 now: `this.nativeEvent = nativeEvent;` + +The bug was that `this.nativeEvent` was being assigned before `this.target`, when the correct initialization order should have `this.target` assigned first. +``` + +**Diff (expected vs actual):** +```diff +@@ -11 @@ + + import type { Fiber } from 'react-reconciler/src/ReactInternalTypes'; + +-import assign from 'shared/assign'; + import getEventCharCode from './getEventCharCode'; + import getEventCharCode from './getEventCharCode'; ++import assign from 'shared/assign'; + + type EventInterfaceType = { + [propName: string]: 0 | ((event: { [propName: string]: mixed, ... }) => mixed), +@@ -53 @@ + this._reactName = reactName; + this._targetInst = targetInst; + this.type = reactEventType; +- this.nativeEvent = nativeEvent; + this.target = nativeEventTarget; + this.target = nativeEventTarget; ++ this.nativeEvent = nativeEvent; + this.currentTarget = null; + + for (const propName in Interface) { +``` + +### Structural Swap Adjacent Lines 004 (ReactDOMComponentTree.js) — 0/1 + +**All runs failed with same error:** File mismatch for ReactDOMComponentTree.js + +**Sample agent response (run 1):** +``` +Perfect! The fix has been applied. + +**The bug:** Lines 149-150 had the checks in the wrong order. When `enableInternalInstanceMap` is true, it was checking `internalContainerInstanceKey` before `internalInstanceMap.get()`, which is inconsistent with: +1. The else branch on line 153 (which checks container then instance key) +2. The same pattern in `getInstanceFromNode` at lines 217-218 (which checks instance map then container key) + +**The fix:** Swapped the order so `internalInstanceMap.get(... +``` + +**Diff (expected vs actual):** +```diff +@@ -67 @@ + internalInstanceMap.delete(node); + internalPropsMap.delete(node); + delete (node: any)[internalEventHandlersKey]; +- delete (node: any)[internalEventHandlerListenersKey]; + delete (node: any)[internalEventHandlesSetKey]; + delete (node: any)[internalEventHandlesSetKey]; ++ delete (node: any)[internalEventHandlerListenersKey]; + delete (node: any)[internalRootNodeResourcesKey]; + if (__DEV__) { + delete (node: any)[internalInstanceKey]; +@@ -146 @@ + // If it's not a container, we check if it's an instance. + if (enableInternalInstanceMap) { + targetInst = +- (parentNode: any)[internalContainerInstanceKey] || +- internalInstanceMap.get(((parentNode: any): InstanceUnion)); ++ internalInstanceMap.get(((parentNode: any): InstanceUnion)) || ++ (parentNode: any)[internalContainerInstanceKey]; + } else { + targetInst = + (parentNode: any)[internalContainerInstanceKey] || (parentNode: any)[internalInstanceKey]; +``` + +### Structural Swap If Else 001 (resolveBoxStyle.js) — 0/1 + +**All runs failed with same error:** File mismatch for resolveBoxStyle.js + +**Sample agent response (run 1):** +``` +Perfect. The fix is complete. The if and else branches have been swapped back to their correct positions. Now the logic correctly: + +- **If** `styleForVertical` exists: applies it to both `top` and `bottom` (lines 72-74) +- **Else**: checks for individual `Bottom` and `Top` style properties (lines 76-85) + +This matches the same pattern used for `styleForHorizontal` earlier in the file. Only the buggy branches around line 72 were modified; all other code remains unchanged. +``` + +**Diff (expected vs actual):** +```diff +@@ -78 @@ + result.bottom = styleForBottom; + hasParts = true; + } +- + const styleForTop = style[prefix + 'Top']; + if (styleForTop != null) { + result.top = styleForTop; +``` diff --git a/packages/react-edit-benchmark/runs/devstral-medium_hashline_2026-02-20T10-20-24.md b/packages/react-edit-benchmark/runs/devstral-medium_hashline_2026-02-20T10-20-24.md new file mode 100644 index 000000000..2d9809d98 --- /dev/null +++ b/packages/react-edit-benchmark/runs/devstral-medium_hashline_2026-02-20T10-20-24.md @@ -0,0 +1,1704 @@ +# Edit Benchmark Report + +## Configuration + +| Setting | Value | +|---------|-------| +| Date | 2026-02-20T10:11:55.474Z | +| Model | openrouter/openrouter/mistralai/devstral-medium | +| Thinking Level | low | +| Runs per task | 1 | +| Edit Variant | hashline | +| Edit Fuzzy | auto | +| Edit Fuzzy Threshold | auto | +| Guided Mode | no | +| Max Attempts | 1 | +| No-op Retry Limit | 2 | +| Mutation Scope Window | 20 | +| Require Edit Tool | no | +| Require Read Tool | no | +| No-Edit Baseline | no | + +## Summary + +| Metric | Value | +|--------|-------| +| Total Tasks | 80 | +| Total Runs | 80 | +| Successful Runs | 47 | +| **Task Success Rate** | **58.8% (47/80)** | +| Verified Rate | 58.8% (47/80) | +| Edit Tool Usage Rate | 96.3% (77/80) | +| **Edit Success Rate** | **87.6%** | +| Timeout Runs | 0 | +| Mutation Intent Match Rate | 72.7% | +| Patch Failure Rate | 12.4% (12/97) | +| Tasks All Passing | 47 | +| Tasks Flaky/Failing | 33 | + +### Tool Calls + +| Tool | Total | Avg/Run | +|------|-------|---------| +| Read | 177 | 2.2 | +| Edit | 97 | 1.2 | +| Write | 0 | 0.0 | +| **Tool Input Chars** | 26,208 | 328 | + +### Tokens & Time + +| Metric | Total | Avg/Run | +|--------|-------|---------| +| Input Tokens | 3,332,062 | 41,651 | +| Output Tokens | 57,464 | 718 | +| Total Tokens | 3,389,526 | 42,369 | +| Duration | 2366.8s | 29.6s | +| **Avg Indent Score** | — | **0.16** | + +## Task Results + +| Task | File | Success | Edit Hit | R/E/W | Tokens (In/Out) | Time | Indent | +|------|------|---------|----------|-------|-----------------|------|--------| +| Access Remove Optional Chain 001 | fallbackEvalContext.js | 1/1 ✅ | 100.0% | 2/1/0 | 28,259/292 | 8.9s | 0.00 | +| Access Remove Optional Chain 003 | hookNamesCache.js | 0/1 ❌ | 50.0% | 3/2/0 | 56,565/545 | 12.7s | 0.00 | +| Access Remove Optional Chain 005 | registerDevToolsEventLogger.js | 0/1 ❌ | 100.0% | 2/1/0 | 30,661/261 | 11.8s | 0.00 | +| Access Remove Optional Chain 007 | index.js | 0/1 ❌ | 100.0% | 0/0/0 | 51,865/17,776 | 449.8s | 0.00 | +| Call Swap Call Args 001 | formatConsoleArguments.js | 1/1 ✅ | 100.0% | 2/1/0 | 29,627/193 | 9.8s | 0.00 | +| Call Swap Call Args 003 | ReactFlightDOMServerBrowser.js | 1/1 ✅ | 100.0% | 5/1/0 | 69,135/701 | 21.5s | 0.00 | +| Call Swap Call Args 005 | ReactNoopPersistent.js | 1/1 ✅ | 100.0% | 2/1/0 | 29,317/279 | 13.3s | 0.00 | +| Call Swap Call Args 007 | parseSourceAndMetadata.js | 1/1 ✅ | 100.0% | 2/1/0 | 44,065/322 | 10.3s | 0.00 | +| Duplicate Duplicate Line Flip 001 | isCustomElement.js | 1/1 ✅ | 100.0% | 2/1/0 | 27,714/227 | 11.2s | 0.00 | +| Duplicate Duplicate Line Flip 003 | ReactFiberDevToolsHook.js | 0/1 ❌ | 75.0% | 6/4/0 | 145,238/946 | 26.2s | 3.00 | +| Duplicate Duplicate Line Flip 005 | shallowEqual.js | 1/1 ✅ | 100.0% | 2/1/0 | 28,429/214 | 9.4s | 0.00 | +| Duplicate Duplicate Line Flip 007 | ReactDOMEventReplaying.js | 0/1 ❌ | 100.0% | 5/1/0 | 121,586/677 | 34.5s | 0.00 | +| Identifier Identifier Multi Edit 001 | Button.js | 1/1 ✅ | 100.0% | 2/1/0 | 28,071/285 | 8.2s | 0.00 | +| Identifier Identifier Multi Edit 003 | ReactFlightDOMClientBrowser.js | 0/1 ❌ | 100.0% | 2/1/0 | 36,813/485 | 17.4s | 0.00 | +| Identifier Identifier Multi Edit 005 | githubAPI.js | 1/1 ✅ | 100.0% | 2/1/0 | 28,976/329 | 12.0s | 0.00 | +| Identifier Identifier Multi Edit 007 | ReactFiberComponentStack.js | 1/1 ✅ | 100.0% | 3/2/0 | 60,594/512 | 18.1s | 0.00 | +| Import Swap Named Imports 001 | ListApp.js | 1/1 ✅ | 100.0% | 2/1/0 | 28,745/244 | 9.8s | 0.00 | +| Import Swap Named Imports 003 | index.js | 1/1 ✅ | 100.0% | 2/1/0 | 34,892/282 | 12.9s | 0.00 | +| Import Swap Named Imports 005 | SuspenseScrubber.js | 1/1 ✅ | 100.0% | 2/1/0 | 32,727/245 | 8.7s | 0.00 | +| Import Swap Named Imports 007 | InspectedElement.js | 0/1 ❌ | 100.0% | 4/2/0 | 76,596/876 | 16.2s | 0.00 | +| Literal Flip Boolean 001 | ReactDOMLegacyServerImpl.js | 0/1 ❌ | 100.0% | 2/1/0 | 30,164/313 | 11.6s | 0.00 | +| Literal Flip Boolean 003 | ReactProfilerTimer.js | 0/1 ❌ | 100.0% | 2/1/0 | 57,293/684 | 19.5s | 0.00 | +| Literal Flip Boolean 005 | OpenInEditorButton.js | 1/1 ✅ | 100.0% | 2/1/0 | 29,265/325 | 9.4s | 0.00 | +| Literal Flip Boolean 007 | Element.js | 1/1 ✅ | 50.0% | 1/2/0 | 37,305/268 | 8.3s | 0.00 | +| Literal Off By One 001 | ReactOwnerStackFrames.js | 1/1 ✅ | 100.0% | 2/1/0 | 28,825/360 | 18.3s | 0.00 | +| Literal Off By One 003 | ReactFlightClientConfigBundlerTurbopack.js | 1/1 ✅ | 100.0% | 2/1/0 | 37,383/375 | 15.3s | 0.00 | +| Literal Off By One 005 | ContextMenu.js | 0/1 ❌ | 100.0% | 2/1/0 | 30,975/371 | 18.4s | 0.00 | +| Literal Off By One 007 | index.js | 1/1 ✅ | 100.0% | 2/1/0 | 55,821/560 | 30.3s | 0.00 | +| Operator Remove Negation 001 | prepareInjection.js | 1/1 ✅ | 100.0% | 2/1/0 | 28,146/281 | 8.1s | 0.00 | +| Operator Remove Negation 003 | ReactDOMSelection.js | 0/1 ❌ | 100.0% | 2/1/0 | 34,690/438 | 18.0s | 2.00 | +| Operator Remove Negation 005 | ReactDOMContainer.js | 0/1 ❌ | 100.0% | 2/1/0 | 27,542/169 | 10.0s | 0.00 | +| Operator Remove Negation 007 | SelectEventPlugin.js | 1/1 ✅ | 100.0% | 2/1/0 | 35,686/336 | 9.1s | 0.00 | +| Operator Swap Arithmetic 001 | formatConsoleArguments.js | 1/1 ✅ | 100.0% | 2/1/0 | 29,754/294 | 9.5s | 0.00 | +| Operator Swap Arithmetic 003 | ReactFlightDOMServerBrowser.js | 0/1 ❌ | 100.0% | 3/2/0 | 57,571/660 | 14.0s | 2.00 | +| Operator Swap Arithmetic 005 | ReactFiberConfigWithNoResources.js | 1/1 ✅ | 100.0% | 2/1/0 | 28,379/255 | 12.1s | 0.00 | +| Operator Swap Arithmetic 007 | useCanvasInteraction.js | 0/1 ❌ | 100.0% | 2/1/0 | 36,131/469 | 16.2s | 0.00 | +| Operator Swap Comparison 001 | useSmartTooltip.js | 1/1 ✅ | 50.0% | 3/2/0 | 50,303/502 | 20.9s | 0.00 | +| Operator Swap Comparison 003 | ReactFlightDOMServerBrowser.js | 0/1 ❌ | 100.0% | 4/1/0 | 68,859/620 | 28.6s | 0.00 | +| Operator Swap Comparison 005 | Rectangle.js | 1/1 ✅ | 100.0% | 2/1/0 | 31,608/232 | 10.7s | 0.00 | +| Operator Swap Comparison 007 | ReactFiberTreeReflection.js | 1/1 ✅ | 100.0% | 2/1/0 | 52,465/426 | 18.2s | 0.00 | +| Operator Swap Equality 001 | ReactNoopFlightClient.js | 1/1 ✅ | 100.0% | 2/1/0 | 31,143/308 | 6.9s | 0.00 | +| Operator Swap Equality 003 | astUtils.js | 0/1 ❌ | 50.0% | 1/2/0 | 53,883/200 | 9.6s | 0.00 | +| Operator Swap Equality 005 | ReactDOMContainer.js | 1/1 ✅ | 100.0% | 2/1/0 | 27,527/181 | 8.9s | 0.00 | +| Operator Swap Equality 007 | ReactFlightDOMServerBrowser.js | 1/1 ✅ | 100.0% | 2/1/0 | 39,156/1,724 | 31.4s | 2.00 | +| Operator Swap Increment Decrement 001 | index.js | 1/1 ✅ | 100.0% | 2/1/0 | 28,477/242 | 7.7s | 0.00 | +| Operator Swap Increment Decrement 003 | ReactFlightClientConfigBundlerESM.js | 1/1 ✅ | 100.0% | 2/1/0 | 35,847/345 | 10.0s | 0.00 | +| Operator Swap Increment Decrement 005 | ReactFiberViewTransitionComponent.js | 1/1 ✅ | 100.0% | 2/1/0 | 30,308/273 | 9.8s | 2.00 | +| Operator Swap Increment Decrement 007 | ReactFiberConcurrentUpdates.js | 1/1 ✅ | 100.0% | 2/1/0 | 40,790/267 | 6.5s | 0.00 | +| Operator Swap Logical 001 | ErrorView.js | 0/1 ❌ | 100.0% | 2/1/0 | 28,766/189 | 8.9s | 0.00 | +| Operator Swap Logical 003 | DevTools.js | 1/1 ✅ | 100.0% | 2/1/0 | 44,489/445 | 12.4s | 0.00 | +| Operator Swap Logical 005 | UseEffectEvent.js | 0/1 ❌ | 100.0% | 2/1/0 | 27,942/347 | 12.3s | 0.00 | +| Operator Swap Logical 007 | getHookNameForLocation.js | 1/1 ✅ | 100.0% | 1/1/0 | 27,159/305 | 7.3s | 0.00 | +| Operator Swap Nullish 001 | ElementBadges.js | 1/1 ✅ | 100.0% | 2/1/0 | 28,796/197 | 10.7s | 0.00 | +| Operator Swap Nullish 003 | ReactComponentStackFrame.js | 1/1 ✅ | 100.0% | 2/1/0 | 40,067/336 | 11.6s | 0.00 | +| Operator Swap Nullish 005 | ReactLogo.js | 1/1 ✅ | 100.0% | 2/1/0 | 28,123/186 | 9.1s | 0.00 | +| Operator Swap Nullish 007 | SuspenseBreadcrumbs.js | 0/1 ❌ | 100.0% | 2/1/0 | 43,206/398 | 10.1s | 0.00 | +| Regex Swap Regex Quantifier 001 | SimpleValues.js | 1/1 ✅ | 100.0% | 2/1/0 | 28,149/193 | 5.9s | 0.00 | +| Regex Swap Regex Quantifier 003 | utils.js | 1/1 ✅ | 100.0% | 3/1/0 | 48,772/430 | 17.3s | 0.00 | +| Regex Swap Regex Quantifier 005 | formatWithStyles.js | 1/1 ✅ | 100.0% | 2/1/0 | 30,547/369 | 11.1s | 0.00 | +| Regex Swap Regex Quantifier 007 | RunReactCompiler.ts | 1/1 ✅ | 100.0% | 2/1/0 | 43,920/2,040 | 37.8s | 0.00 | +| Structural Delete Statement 001 | useExtensionComponentsPanelVisibility.js | 0/1 ❌ | 100.0% | 2/1/0 | 28,024/178 | 7.5s | 0.00 | +| Structural Delete Statement 003 | useCanvasInteraction.js | 1/1 ✅ | 100.0% | 2/1/0 | 36,971/416 | 10.8s | 0.00 | +| Structural Delete Statement 005 | StackTraceView.js | 0/1 ❌ | 33.3% | 3/3/0 | 62,464/686 | 20.5s | 0.00 | +| Structural Delete Statement 007 | ReactDOMFizzStaticBrowser.js | 0/1 ❌ | 50.0% | 2/2/0 | 47,089/937 | 29.1s | 0.00 | +| Structural Remove Early Return 001 | ReactFlightAsyncDispatcher.js | 0/1 ❌ | 100.0% | 0/0/0 | 0/0 | 360.0s | 0.00 | +| Structural Remove Early Return 003 | ReactDOMComponentTree.js | 0/1 ❌ | 100.0% | 2/1/0 | 56,886/5,785 | 92.7s | 0.00 | +| Structural Remove Early Return 005 | TabBar.js | 0/1 ❌ | 50.0% | 2/2/0 | 41,520/329 | 19.3s | 0.00 | +| Structural Remove Early Return 007 | CommitTreeBuilder.js | 0/1 ❌ | 100.0% | 2/1/0 | 47,160/338 | 17.4s | 0.00 | +| Structural Swap Adjacent Lines 001 | reactPolling.js | 1/1 ✅ | 100.0% | 2/1/0 | 30,453/332 | 8.4s | 0.00 | +| Structural Swap Adjacent Lines 003 | OwnersStack.js | 1/1 ✅ | 50.0% | 3/2/0 | 62,986/680 | 17.1s | 0.00 | +| Structural Swap Adjacent Lines 005 | ReactOwnerStackFrames.js | 0/1 ❌ | 100.0% | 3/2/0 | 45,557/586 | 12.2s | 0.00 | +| Structural Swap Adjacent Lines 007 | ReactFlightClientConfigBundlerWebpack.js | 0/1 ❌ | 50.0% | 3/2/0 | 61,501/841 | 25.6s | 0.00 | +| Structural Swap If Else 001 | utils.js | 0/1 ❌ | 100.0% | 0/0/0 | 0/0 | 360.0s | 0.00 | +| Structural Swap If Else 003 | ReactDOMFloat.js | 0/1 ❌ | 66.7% | 4/3/0 | 107,545/1,377 | 32.5s | 0.00 | +| Structural Swap If Else 005 | ReactClientConsoleConfigPlain.js | 0/1 ❌ | 100.0% | 3/2/0 | 48,174/1,117 | 21.3s | 0.00 | +| Structural Swap If Else 007 | index.js | 0/1 ❌ | 100.0% | 2/1/0 | 37,397/345 | 15.4s | 1.60 | +| Unicode Unicode Hyphen 001 | formatProdErrorMessage.js | 0/1 ❌ | 100.0% | 2/1/0 | 27,872/167 | 8.5s | 0.00 | +| Unicode Unicode Hyphen 003 | SourceMapConsumer.js | 1/1 ✅ | 100.0% | 2/1/0 | 35,455/306 | 14.0s | 0.00 | +| Unicode Unicode Hyphen 005 | babel.config.js | 1/1 ✅ | 100.0% | 2/1/0 | 28,920/165 | 7.1s | 0.00 | +| Unicode Unicode Hyphen 007 | ReactInternalTestUtils.js | 1/1 ✅ | 100.0% | 2/1/0 | 40,981/275 | 13.3s | 0.00 | + +## Category Summary + +| Category | Runs | Verified | Edit Used | Success | Min/Avg/Max Difficulty | +|----------|------|----------|-----------|---------|------------------------| +| access | 4 | 25.0% (1/4) | 75.0% (3/4) | 25.0% (1/4) | 3 / 7.5 / 10 | +| call | 4 | 100.0% (4/4) | 100.0% (4/4) | 100.0% (4/4) | 2 / 7.8 / 14 | +| duplicate | 4 | 50.0% (2/4) | 100.0% (4/4) | 50.0% (2/4) | 8 / 11.3 / 17 | +| identifier | 4 | 75.0% (3/4) | 100.0% (4/4) | 75.0% (3/4) | 4 / 6.8 / 9 | +| import | 4 | 75.0% (3/4) | 100.0% (4/4) | 75.0% (3/4) | 2 / 3.8 / 5 | +| literal | 8 | 62.5% (5/8) | 100.0% (8/8) | 62.5% (5/8) | 3 / 6.9 / 10 | +| operator | 28 | 67.9% (19/28) | 100.0% (28/28) | 67.9% (19/28) | 3 / 6.7 / 10 | +| regex | 4 | 100.0% (4/4) | 100.0% (4/4) | 100.0% (4/4) | 2 / 5.8 / 10 | +| structural | 16 | 18.8% (3/16) | 87.5% (14/16) | 18.8% (3/16) | 4 / 8.3 / 13 | +| unicode | 4 | 75.0% (3/4) | 100.0% (4/4) | 75.0% (3/4) | 2 / 4.8 / 8 | + +## Mutation Summary + +| Mutation | Category | Runs | Verified | Edit Used | Success | +|----------|----------|------|----------|-----------|---------| +| delete-statement | structural | 4 | 25.0% (1/4) | 100.0% (4/4) | 25.0% (1/4) | +| duplicate-line-flip | duplicate | 4 | 50.0% (2/4) | 100.0% (4/4) | 50.0% (2/4) | +| flip-boolean | literal | 4 | 50.0% (2/4) | 100.0% (4/4) | 50.0% (2/4) | +| identifier-multi-edit | identifier | 4 | 75.0% (3/4) | 100.0% (4/4) | 75.0% (3/4) | +| off-by-one | literal | 4 | 75.0% (3/4) | 100.0% (4/4) | 75.0% (3/4) | +| remove-early-return | structural | 4 | 0.0% (0/4) | 75.0% (3/4) | 0.0% (0/4) | +| remove-negation | operator | 4 | 50.0% (2/4) | 100.0% (4/4) | 50.0% (2/4) | +| remove-optional-chain | access | 4 | 25.0% (1/4) | 75.0% (3/4) | 25.0% (1/4) | +| swap-adjacent-lines | structural | 4 | 50.0% (2/4) | 100.0% (4/4) | 50.0% (2/4) | +| swap-arithmetic | operator | 4 | 50.0% (2/4) | 100.0% (4/4) | 50.0% (2/4) | +| swap-call-args | call | 4 | 100.0% (4/4) | 100.0% (4/4) | 100.0% (4/4) | +| swap-comparison | operator | 4 | 75.0% (3/4) | 100.0% (4/4) | 75.0% (3/4) | +| swap-equality | operator | 4 | 75.0% (3/4) | 100.0% (4/4) | 75.0% (3/4) | +| swap-if-else | structural | 4 | 0.0% (0/4) | 75.0% (3/4) | 0.0% (0/4) | +| swap-increment-decrement | operator | 4 | 100.0% (4/4) | 100.0% (4/4) | 100.0% (4/4) | +| swap-logical | operator | 4 | 50.0% (2/4) | 100.0% (4/4) | 50.0% (2/4) | +| swap-named-imports | import | 4 | 75.0% (3/4) | 100.0% (4/4) | 75.0% (3/4) | +| swap-nullish | operator | 4 | 75.0% (3/4) | 100.0% (4/4) | 75.0% (3/4) | +| swap-regex-quantifier | regex | 4 | 100.0% (4/4) | 100.0% (4/4) | 100.0% (4/4) | +| unicode-hyphen | unicode | 4 | 75.0% (3/4) | 100.0% (4/4) | 75.0% (3/4) | + +## Difficulty Summary + +| Difficulty Score | Runs | Verified | Edit Used | Success | +|------------------|------|----------|-----------|---------| +| 0-2 | 4 | 100.0% (4/4) | 100.0% (4/4) | 100.0% (4/4) | +| 3-5 | 24 | 62.5% (15/24) | 100.0% (24/24) | 62.5% (15/24) | +| 6-8 | 27 | 55.6% (15/27) | 92.6% (25/27) | 55.6% (15/27) | +| 9+ | 25 | 52.0% (13/25) | 96.0% (24/25) | 52.0% (13/25) | + +## Edit Tool Errors + +Failures where the edit tool returned an error or failed to apply the patch. + +### Access Remove Optional Chain 003 (hookNamesCache.js) + +#### Run 1 + +##### Attempt 1 + +- Path: hookNamesCache.js +- Operation: — + +**Tool error** + +``` +No changes made to hookNamesCache.js. The edits produced identical content. +Edit 0: replacement for 152#SJ is identical to current content: + 152#SJ| const fulfilledThenable: FulfilledThenable = (thenable: any); +Your content must differ from what the file already contains. Re-read the file to see the current state. +The file differs from the original fixture at these lines: +152#SJ|- const fulfilledThenable: FulfilledThenable = (thenable: any); +153#HN|+ const fulfilledThenable: FulfilledThenable = (thenable: any)?; +``` + +**Patch args** + +```diff +{ + "path": "hookNamesCache.js", + "edits": [ + { + "op": "set", + "tag": "152#SJ", + "content": " const fulfilledThenable: FulfilledThenable = (thenable: any);" + } + ] +} +``` + +### Duplicate Duplicate Line Flip 003 (ReactFiberDevToolsHook.js) + +#### Run 1 + +##### Attempt 1 + +- Path: ReactFiberDevToolsHook.js +- Operation: — + +**Tool error** + +``` +1 line has changed since last read. Use the updated LINE#ID references shown below (>>> marks changed lines). + + 200#TM:export function markCommitStarted(lanes: Lanes): void { + 201#ZS: if (enableSchedulingProfiler) { +>>> 202#VK: if ( + 203#SP: injectedProfilingHooks !== null && + 204#JW: typeof injectedProfilingHooks.markCommitStarted === 'function' +``` + +**Patch args** + +```diff +{ + "path": "ReactFiberDevToolsHook.js", + "edits": [ + { + "op": "set", + "tag": "202#SP", + "content": " injectedProfilingHooks !== null &&" + } + ] +} +``` + +### Literal Flip Boolean 007 (Element.js) + +#### Run 1 + +##### Attempt 1 + +- Path: Element.js +- Operation: — + +**Tool error** + +``` +1 line has changed since last read. Use the updated LINE#ID references shown below (>>> marks changed lines). + + 43#HP: const element = ownerFlatTree !== null ? ownerFlatTree[index] : store.getElementAtIndex(index); + 44#ZR: +>>> 45#JP: const [isHovered, setIsHovered] = useState(true); + 46#ZR: + 47#ZM: const errorsAndWarningsSubscription = useMemo( +``` + +**Patch args** + +```diff +{ + "path": "Element.js", + "edits": [ + { + "op": "set", + "tag": "45#ZR", + "content": [ + " const [isHovered, setIsHovered] = useState(false);" + ] + } + ] +} +``` + +### Operator Swap Comparison 001 (useSmartTooltip.js) + +#### Run 1 + +##### Attempt 1 + +- Path: useSmartTooltip.js +- Operation: — + +**Tool error** + +``` +No changes made to useSmartTooltip.js. The edits produced identical content. +Edit 0: replacement for 59#HS is identical to current content: + 59#HS| // our default strategy). Therefore we try to position it either at the +Your content must differ from what the file already contains. Re-read the file to see the current state. +The file differs from the original fixture at these lines: +57#WT|- if (mouseX + TOOLTIP_OFFSET_BOTTOM + element.offsetWidth > width) { +58#BV|+ if (mouseX + TOOLTIP_OFFSET_BOTTOM + element.offsetWidth >= width) { +``` + +**Patch args** + +```diff +{ + "path": "useSmartTooltip.js", + "edits": [ + { + "op": "set", + "tag": "59#HS", + "content": [ + " // our default strategy). Therefore we try to position it either at the" + ] + } + ] +} +``` + +### Operator Swap Equality 003 (astUtils.js) + +#### Run 1 + +##### Attempt 1 + +- Path: astUtils.js +- Operation: — + +**Tool error** + +``` +1 line has changed since last read. Use the updated LINE#ID references shown below (>>> marks changed lines). + + 543#QV:/// Check whether 'node' is hook declaration of form useState(0); OR React.useState(0); + 544#HW:function isReactFunction(node: Node, functionName: string): boolean { +>>> 545#SZ: return ( + 546#VB: node.name === functionName || + 547#HM: (node.type === 'MemberExpression' && +``` + +**Patch args** + +```diff +{ + "path": "astUtils.js", + "edits": [ + { + "op": "set", + "tag": "545#VB", + "content": [ + " node.name !== functionName ||" + ] + } + ] +} +``` + +### Structural Delete Statement 005 (StackTraceView.js) + +#### Run 1 + +##### Attempt 1 + +- Path: StackTraceView.js +- Operation: — + +**Tool error** + +``` +No changes made to StackTraceView.js. The edits produced identical content. +Edit 0: replacement for 61#KH is identical to current content: + 61#KH| ignored ? styles.IgnoredCallSite : isBuiltIn ? styles.BuiltInCallSite : styles.CallSite +Your content must differ from what the file already contains. Re-read the file to see the current state. +The file differs from the original fixture at these lines: +57#XY|+ const ignored = false; // Restore the missing ignored flag logic +``` + +**Patch args** + +```diff +{ + "path": "StackTraceView.js", + "edits": [ + { + "op": "set", + "tag": "61#KH", + "content": [ + " ignored ? styles.IgnoredCallSite : isBuiltIn ? styles.BuiltInCallSite : styles.CallSite" + ] + } + ] +} +``` + +##### Attempt 2 + +- Path: StackTraceView.js +- Operation: — + +**Tool error** + +``` +Validation failed for tool "edit": + - edits/0: must have required property 'tag' + - edits/0: must NOT have additional properties + - edits/0: must NOT have additional properties + - edits/0/op: must be equal to constant + - edits/0: must have required property 'first' + - edits/0: must have required property 'last' + - edits/0: must NOT have additional properties + - edits/0: must NOT have additional properties + - edits/0/op: must be equal to constant + - edits/0: must NOT have additional properties + - edits/0: must NOT have additional properties + - edits/0/op: must be equal to constant + - edits/0: must NOT have additional properties + - edits/0/op: must be equal to constant + - edits/0: must NOT have additional properties + - edits/0: must match a schema in anyOf + +Received arguments: +{ + "path": "StackTraceView.js", + "edits": [ + { + "op": "insert", + "before": "57#HQ", + "content": [ + " const ignored = false; // Restore the missing ignored flag logic" + ], + "agent__intent": "Restoring the missing ignored variable definition in StackTraceView.js" + } + ] +} +``` + +**Patch args** + +```diff +{ + "path": "StackTraceView.js", + "edits": [ + { + "op": "insert", + "before": "57#HQ", + "content": [ + " const ignored = false; // Restore the missing ignored flag logic" + ], + "agent__intent": "Restoring the missing ignored variable definition in StackTraceView.js" + } + ] +} +``` + +### Structural Delete Statement 007 (ReactDOMFizzStaticBrowser.js) + +#### Run 1 + +##### Attempt 1 + +- Path: ReactDOMFizzStaticBrowser.js +- Operation: — + +**Tool error** + +``` +1 line has changed since last read. Use the updated LINE#ID references shown below (>>> marks changed lines). + + 69#VM: const onFatalError = reject; + 70#ZR: +>>> 71#NV: function onAllReady() { + 72#WH: const result: StaticResult = { + 73#YQ: postponed: getPostponedState(request), +``` + +**Patch args** + +```diff +{ + "path": "ReactDOMFizzStaticBrowser.js", + "edits": [ + { + "op": "replace", + "first": "71#WH", + "last": "77#PN", + "content": [ + " function onAllReady() {\n const stream = new ReadableStream(\n {\n type: 'bytes',\n pull: (controller): ?Promise => {\n startFlowing(request, controller);\n },\n cancel: (reason): ?Promise => {\n stopFlowing(request);\n abort(request, reason);\n },\n },\n // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.\n { highWaterMark: 0 },\n );\n const result: StaticResult = {\n postponed: getPostponedState(request),\n prelude: stream,\n };\n resolve(result);\n }" + ] + } + ] +} +``` + +### Structural Remove Early Return 005 (TabBar.js) + +#### Run 1 + +##### Attempt 1 + +- Path: TabBar.js +- Operation: — + +**Tool error** + +``` +1 line has changed since last read. Use the updated LINE#ID references shown below (>>> marks changed lines). + + 91#MR: {tabs.map((tab) => { + 92#KX: const { icon, id, label, title } = tab; +>>> 93#ZR: + 94#WK: let button = ( + 95#ZM: