Repository navigation
Description
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
- In a scratch folder:
printf 'Build with: npm run build\n' > CLAUDE.md && ln -s CLAUDE.md AGENTS.md - Start nanocoder and have the model call
read_fileonAGENTS.md. - Change the target:
printf 'Build with: pnpm build\n' > CLAUDE.md(from bash through the agent, or by hand). - Have the model call
read_fileonAGENTS.mdagain.
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.mddirectly 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:112gets the file stats withlstat(). 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
me-adityaraj8 commentedon Oct 18, 2026
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.
addyCooks commentedon Oct 19, 2026
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.
fix(tools): compare a symlink's target in the read_file repeat-read c…
Merge pull request #1845 from diyaavirmani/fix/read-file-symlink-stub