Skip to content

[Bug] read_file reports a symlinked file as unchanged after its target changes (AGENTS.md -> CLAUDE.md) #1834

Closed

Description

@diyaavirmani

Description

The repeat-read stub in read_file (added in #1359 and #1678) checks a symlink's own mtime and size instead of the file it points to. So when a symlinked file changes, the next read returns "already in context, unchanged since last read", and the model never sees the new content.

This hits a very common setup: AGENTS.md as a symlink to CLAUDE.md. If CLAUDE.md changes during a session (a bash edit, a formatter, a git pull, or you editing it yourself), the model keeps working from the old version and is told it's up to date.

Environment

  • OS: macOS (Darwin 25.6.0, x86_64)
  • Node version: 22.23.3
  • Nanocoder version: main at 0925b881. Not in v1.31.0, which doesn't have the stub yet.
  • Provider: a local OpenAI-compatible test server with scripted responses
  • Model: n/a (scripted)

Steps to Reproduce

  1. In a scratch folder: printf 'Build with: npm run build\n' > CLAUDE.md && ln -s CLAUDE.md AGENTS.md
  2. Start nanocoder and have the model call read_file on AGENTS.md.
  3. Change the target: printf 'Build with: pnpm build\n' > CLAUDE.md (from bash through the agent, or by hand).
  4. Have the model call read_file on AGENTS.md again.

Expected Behavior

The second read returns the new content, because the file changed.

Actual Behavior

The second read returns the stub [file: AGENTS.md ... already in context, unchanged since last read (2 lines, 9 bytes). pass start_line/end_line to read the body again.], so the model only ever saw npm run build.

The "9 bytes" is the size of the symlink itself (the text CLAUDE.md), not of the file.

Controls:

  • Reading CLAUDE.md directly returns the new content.
  • On v1.31.0, both paths return the new content.

Logs/Screenshots

What I think is going on:

  • source/tools/read-file.tsx:112 gets the file stats with lstat(). For a symlink that's the link's own mtime and size, and those don't change when the target changes.
  • Those stats go to matchReadContent (source/utils/read-tracker.ts:118-137), which compares only mtime and size, so it always matches.
  • The content cache uses fs.stat() (source/utils/file-cache.ts:73), which follows the link, so the two checks disagree.

Additional Context

Proposed fix (happy to adjust): use the stats of the resolved target for the stub check (stat instead of lstat, or key the tracker on the real path), or skip the stub for symlinks. Regular files keep the token savings.

For tests, I'd add a spec that reads a symlink, changes its target, reads again and expects the new content, plus one that confirms an unchanged regular file still gets the stub.

  • I have searched existing issues to ensure this is not a duplicate
  • I can reproduce this issue consistently
  • This issue occurs with the latest version of nanocoder (it's on main; v1.31.0 doesn't have the stub feature)

@addyCooks could you take a look when you have a moment? I'd like to work on this one, so please assign it to me if that works for you.

Activity

  1. me-adityaraj8 commented on Oct 18, 2026

    @me-adityaraj8

    Hi, can I take this? The stub check uses lstat() instead of stat(), so it sees the symlink, not the target. I'll fix that and add a test.

  2. added
    area:toolsTool implementations and tool-calling
    bugSomething isn't working
    and removed
    needs-triageQueued for automated issue triage
    on Oct 18, 2026
  3. addyCooks commented on Oct 19, 2026

    @addyCooks
    Collaborator

    Thanks @diyaavirmani. Confirmed from the code: read_file.tsx:112 uses lstat(), so for a symlink the stub check in read-tracker.ts compares the link's own mtime/size, which never change when the target does. The content cache uses stat(), so the two checks disagree.

    Your proposed fix is right: use the stats of the resolved target (stat or realpath) for the stub check, and keep lstat on the metadata_only path where reporting the link is intentional. Please also cover a dangling symlink in the tests.

    Go ahead and open a PR. I'll assign this to you.

    @me-adityaraj8 thanks for the offer, but the reporter asked first,
    so this one is taken.

  4. added a commit that references this issue on Oct 19, 2026
    89bce15
  5. added a commit that references this issue on Oct 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:toolsTool implementations and tool-callingbugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions