Skip to content

fix(acp): recognise structured ENOENT codes and broader not-found phrasings#26439

Open
frozename wants to merge 2 commits intogoogle-gemini:mainfrom
frozename:fix/acp-enoent-detection
Open

fix(acp): recognise structured ENOENT codes and broader not-found phrasings#26439
frozename wants to merge 2 commits intogoogle-gemini:mainfrom
frozename:fix/acp-enoent-detection

Conversation

@frozename
Copy link
Copy Markdown

@frozename frozename commented May 4, 2026

Problem

AcpFileSystemService.normalizeFileSystemError classifies a read failure as
file-not-found by string-matching the message against four exact substrings:

errorMessage.includes('Resource not found') ||
errorMessage.includes('ENOENT') ||
errorMessage.includes('does not exist') ||
errorMessage.includes('No such file')

JSON-RPC error responses also carry a structured code field, but it was
ignored. Common phrasings that don't match any of those four substrings —
notably not_found (snake_case) and file not found (no capitalisation, no
"No such") — fall through to the generic throw err branch.

The downstream effect is severe. The write-file tool calls readTextFile
first to check existence, and a non-ENOENT-shaped error short-circuits the
call site with error: { code: FILE_WRITE_FAILURE } rather than
fileExists: false (see
packages/core/src/tools/write-file.ts).
The agent's reasoning loop then commonly hallucinates "the tool succeeded"
and reports success in the final stop_reason: end_turn response — a
silent failure pattern that's hard to detect from outside the CLI.

Fix

This PR makes normalizeFileSystemError:

  1. Prefer the structured signal: when the rejection carries code: 'ENOENT',
    recognise it as ENOENT regardless of message wording. Cheapest signal,
    matches what well-behaved JSON-RPC servers should emit.
  2. Relax the legacy substring matcher: also accept not_found and
    file not found phrasings. These are common in JSON-RPC servers in the
    wild and appear in the agent-client-protocol ecosystem in particular.

The existing four substrings are preserved for backward compatibility.

Tests added

In packages/cli/src/acp/acpFileSystemService.test.ts:

  • it.each over the two new message variants (not_found snake_case and
    file not found lowercase).
  • A separate test for the structured-code path: server throws Object.assign(new Error('opaque server message'), { code: 'ENOENT' }) and the matcher recognises it.

npm run test:ci -- src/acp/acpFileSystemService13 passed (3 new + 10 existing).

How this surfaced

Discovered while running gemini-cli in ACP mode against a third-party stdio
JSON-RPC server. The server raised
fs/read_text_file not_found: file not found: <path> for a non-existent
file. None of the four current substrings matched, so every gemini-cli
write to that worktree silently produced no file changes. After this patch
a single-file write smoke against the same server goes from ~9 minutes
with three server restarts (recovered by chance) to ~36 seconds clean.

Compatibility

  • Behaviour for the four pre-existing substrings is unchanged.
  • The two new substrings (not_found, file not found) and the structured
    code === 'ENOENT' path are strictly additive — they only convert
    errors that previously fell through to throw err into ENOENT-coded
    errors, never the reverse. No risk of regression for existing servers
    that already produce one of the four recognised messages.

Fixes #26448.

…asings

AcpFileSystemService.normalizeFileSystemError classified read failures as
file-not-found by checking the message against four exact substrings:
'Resource not found', 'ENOENT', 'does not exist', 'No such file'. JSON-RPC
error responses also carry a structured 'code' field, but it was ignored —
and common phrasings like 'not_found' (snake_case) or 'file not found' (no
capitalisation) didn't match any substring.

The downstream effect is severe: the write-file tool calls readTextFile
first to check existence; a non-ENOENT-shaped error short-circuits the
write attempt with 'unreadable file' rather than 'file is new'. The agent
reasoning loop then commonly hallucinates 'the tool succeeded' and reports
success in the final stop_reason: end_turn response — a silent failure
pattern that's hard to detect from outside the CLI.

This patch:
- prefers the structured `err.code === 'ENOENT'` signal when present;
- relaxes the legacy substring matcher to also accept 'not_found' and
  'file not found' variants.

Discovered while investigating gemini-cli ACP write reliability against a
local stdio JSON-RPC server (penumbra/agentchat). Server emits
'fs/read_text_file not_found: file not found: <path>' for missing files —
neither the original four substrings catch it, so every gemini-cli write
silently wrote nothing. After this patch a single-file write smoke against
the same server goes from 9m19s with 3 server restarts (recovered by
chance) to 36s clean.

Test added: 3 new it-block paths covering snake_case 'not_found', the
'file not found' phrasing, and a structured `code: 'ENOENT'` ACP error.
@frozename frozename requested a review from a team as a code owner May 4, 2026 14:36
@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request improves the robustness of file system error normalization within the ACP service. By introducing support for structured error codes and broadening the range of recognized error message phrasings, it prevents silent failures where file-not-found conditions were previously misclassified, ensuring that downstream tools correctly identify missing files.

Highlights

  • Structured Error Handling: Updated the file system service to prioritize checking for a structured 'code: ENOENT' property in error objects before falling back to string matching.
  • Expanded Error Matching: Added support for 'not_found' and 'file not found' substrings to the legacy error message matcher to accommodate more JSON-RPC server variations.
  • Improved Test Coverage: Added new test cases to verify that both the structured error code and the newly supported message variants are correctly normalized to ENOENT.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@google-cla
Copy link
Copy Markdown

google-cla Bot commented May 4, 2026

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request enhances error normalization in AcpFileSystemService by introducing support for structured error objects containing an ENOENT code and expanding substring matching to include 'not_found' and 'file not found' patterns. Unit tests were added to cover these new cases. Feedback suggests refining the structured error handling to correctly extract the error message, ensuring detailed errors are logged for debugging, and utilizing acp.RequestError with specific JSON-RPC codes to align with repository standards.

Comment on lines +43 to +50
if (err && typeof err === 'object' && 'code' in err) {
const code = (err as { code?: unknown }).code;
if (code === 'ENOENT') {
const newErr = new Error(errorMessage) as NodeJS.ErrnoException;
newErr.code = 'ENOENT';
throw newErr;
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

If err is a plain object, errorMessage will be initialized as '[object Object]' at line 37. When a structured error is detected, we should extract the message property. Additionally, per repository rules, we should log the detailed error for debugging and throw acp.RequestError with an appropriate JSON-RPC error code instead of a generic Error.

Suggested change
if (err && typeof err === 'object' && 'code' in err) {
const code = (err as { code?: unknown }).code;
if (code === 'ENOENT') {
const newErr = new Error(errorMessage) as NodeJS.ErrnoException;
newErr.code = 'ENOENT';
throw newErr;
}
}
if (err && typeof err === 'object' && 'code' in err) {
const errObj = err as { code?: unknown; message?: unknown };
console.error('Detailed error:', err);
if (errObj.code === 'ENOENT') {
const msg = typeof errObj.message === 'string' ? errObj.message : errorMessage;
throw new acp.RequestError(-32602, msg);
}
}
References
  1. When catching exceptions, log the detailed error for debugging instead of providing only a generic error message.
  2. When handling JSON-RPC requests, throw acp.RequestError with an appropriate JSON-RPC error code instead of a generic Error for invalid parameters.

…ject

JSON-RPC clients commonly surface error responses as plain objects
(`{ code, message, data }`) rather than Error instances. The previous
`String(err)` fallback collapsed these to '[object Object]', losing the
diagnostic. Resolve a useful message by reading `err.message` when the
rejection is a plain object that has it, before falling back to String().

Adds a test for the non-Error structured-error case.
@frozename
Copy link
Copy Markdown
Author

@gemini-code-assist Thanks — pushed b67c5d6 addressing the valid kernel of your suggestion.

Applied: the message-extraction fix. String(err) on a plain object collapses to '[object Object]' and loses the diagnostic, so normalizeFileSystemError now reads err.message first when the rejection is a plain (non-Error) object. Added a test for that case (should preserve the message when a structured ENOENT error is a plain (non-Error) object).

Pushed back on: swapping the throw to acp.RequestError(-32602, msg). Two reasons:

  1. Contract. The function's contract is to throw an Error whose code field is 'ENOENT' (a NodeJS.ErrnoException-shaped error). Every existing caller — including getCorrectedFileContent in packages/core/src/tools/write-file.ts:117 — checks for err.code === 'ENOENT' via isNodeError(err). Throwing an acp.RequestError would break those callers and turn this from "file not found, treat as new file" into a write-blocking failure (regressing the same bug class this PR fixes).
  2. Wrong code anyway. -32602 is JSON-RPC "Invalid params". ENOENT isn't an invalid-params condition — the request was well-formed, the file just doesn't exist. The existing convention in this file is to remap upstream errors to POSIX-shaped NodeJS.ErrnoException for downstream tool compatibility, and that's what this PR preserves.

I left out the console.error('Detailed error:', err) line: this method is on the hot path of every readTextFile/writeTextFile call (existence checks etc.), so an unconditional log would be noisy. Happy to add a debug-level log if there's a Logger instance available in this class — let me know if you'd prefer that.

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Thank you for the detailed explanation and for addressing the message-extraction issue. Your reasoning regarding the contract of normalizeFileSystemError and the interpretation of JSON-RPC error codes is sound. Maintaining compatibility with existing callers that expect NodeJS.ErrnoException-shaped errors is indeed the correct approach here. The decision to avoid unconditional logging on the hot path is also appreciated. The updated implementation and tests look solid.

@gemini-cli gemini-cli Bot added the status/need-issue Pull requests that need to have an associated issue. label May 4, 2026
@gemini-cli gemini-cli Bot removed the status/need-issue Pull requests that need to have an associated issue. label May 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(acp): AcpFileSystemService.normalizeFileSystemError misses structured ENOENT codes and common not-found phrasings

1 participant