Skip to content

feat(files): report files that change under the agent; refuse stale rewrites - #223

Merged
fuseraft merged 2 commits into
mainfrom
feature/file-change-tracking
Nov 13, 2026
Merged

fuseraft merged 2 commits into
mainfrom
feature/file-change-tracking

Conversation

@fuseraft

@fuseraft fuseraft commented Nov 13, 2026 •

Copy link
Copy Markdown
Owner

An agent works from the copy of a file it last read, but the file can change under it: a shell command it ran (sed, a formatter), the user's editor, or another process. This change tells the agent when that happens, and stops it from rewriting a file from an out-of-date copy. It replaces #221 (closed) and leaves out that PR's secret masking.

Change notices

FileLedger keeps track of the files the agent has read or written with the file tools. For each file it stores the size, the time and a hash, and no content. Before and after every tool call, the tracked files are checked, and a changed file is named in that tool's result:

[fuseraft: files you've seen changed while shell_run ran]
1. `config.txt` changed.
Your copies of these are out of date. Re-read before editing; write_file on them is refused until you do.
  • A change found before a call was made outside fuseraft. One found after a call happened while that tool ran, and the notice names the tool.
  • In the REPL, a change made while the agent waited for the user (an edit in their editor, say) is noted in a message right after theirs.
  • Each change is reported once, by name only, for up to 10 files. The agent re-reads what it needs with read_file, so a file with long lines (JSONL, minified code) can't flood the context.
  • A sub-agent's changes are reported to its parent, on the call that ran the sub-agent. /clear, /switch, /resume and a new serve task forget the tracked files.

A file's modified time moves in clock ticks (about 15 ms on Windows), so a same-size edit made right after a read can leave the size and time unchanged. A time from the last 2 seconds isn't trusted: until it's older, the file is compared by content. Windows CI caught this.

Stale rewrites are refused

write_file on a file that changed since the agent last read or wrote it fails with WRITE BLOCKED, because content built from the old copy would undo the change. Reading the file again lifts the block. patch_file still works, because it matches the file as it is now.

Orchestration

A file a tool changed without naming it (a sed through shell_run) is recorded in FilesWritten/FilesDeleted in the change log. RequireAllFilesWritten and the other validators that read the log now see it like a write_file.

files.trackedFiles

Each tracked file costs one lookup before and after every tool call, and on a network share each lookup is a round trip. fuseraft settings set files.trackedFiles <0–5000> sets how many of the most recently seen files are tracked. The default is 50, and 0 turns tracking off, along with the notices and the stale-write refusal. The setting is in a new files section because it applies to both the REPL and fuseraft run. settings show lists it under "Files", and the VS Code and web settings panels get it from the catalog.

Live check (Claude Sonnet 5.5, JSON bridge)

  • I edited config.txt between two messages. The note after the next message named the file, and the model re-read it before answering.
  • The model ran sed -i on a file it had read. The shell_run result named the file.
  • I appended a 6,000-character JSONL line to a file the model had read. The note named the file, and none of the line entered the context.
  • After another outside change, a write_file without re-reading was refused with WRITE BLOCKED, and the file was left as it was.
  • With files.trackedFiles set to 0, no notices appeared anywhere in the session.

Tests

New test files: FileLedgerTests, FileChangeNoticeTests, AgentMiddlewareBuilderChangeReportingTests and FileSystemPluginStaleWriteTests. ChangeTrackerTests gains cases for tool-changed files in the change log.

…ewrites

FileLedger keeps track of the files the agent has read or written with
the file tools (size, time and hash). Before and after each tool call,
a file that changed is named in that tool's result: changed outside
fuseraft, or while the tool ran. In the REPL, changes made while the
agent waited are noted after the user's message. Names only: the agent
re-reads what it needs, so long-line files can't flood the context.

write_file on a file that changed since the agent last saw it is
refused until it reads the file again. In orchestration, a file a tool
changed this way counts as written or deleted in the change log.

files.trackedFiles (a new `files` section, default 50, shared by the
REPL and fuseraft run) sets how many recent files are tracked; 0 turns
tracking off, which matters on network shares.
A file's modified time moves in clock ticks (about 15 ms on Windows), so
a same-size edit right after the read kept the size and time the check
had recorded, and the change was missed. Windows CI caught it. A time
from the last 2 seconds is no longer trusted: until it's older, the
file is compared by content.
@fuseraft
fuseraft merged commit e6c56f0 into main Nov 13, 2026
9 checks passed
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.

1 participant