ADR-0094: Buffers detect changes to their files
- Status: Accepted
- Date: 2026-09-10
- Deciders: jka
- Decision: D94
Context
fs.changed (ADR-0047) refreshed the panes but never the buffers. After an external
write (a git pull, an agent write, rio's own discard) a tab kept showing the old text,
and a later save wrote it back over the new content: silent data loss one keystroke away.
Decision
rio announces its own writes. git.discard and git.discard_all emit fs.changed
for every path they touch, including both names of a reverted rename.
Detection runs in the core. With a remote core the file is on the server, so a
frontend's file mtime would describe the wrong machine. The buffer's disk identity is
modification time plus size, recorded on open and after save. It is recorded after
reading, so a write landing during the read makes the buffer look stale rather than
current.
Three list-based operations, because one external write can affect every open tab:
buffers.stale {}returns{stale: [{buffer, path, gone}]}.buffers.reload {buffers}re-reads each file, replaces the buffer text, re-detects encoding and line endings, and records the new identity. Failures are reported per buffer, never raised for the batch.buffers.stamp {buffers}records "I have seen this version" without changing text.
A reload replaces the text through the ordinary edit path, as one undo step that
also ends any typing run, and emits one buffer.changed. No frontend text code is
needed: the event updates whichever view shows the buffer.
The frontend decides what to do, because the decision depends on whether the buffer is modified, which is frontend state (ADR-0022). The default button is always the choice that loses nothing:
- Unmodified and still present: reload silently.
- Modified and changed on disk: ask once for the whole set, default No (keep my edits). No records the version so the question is not repeated; a further change asks again.
- Deleted on disk: "has been deleted on disk. Keep it open in the editor? Saving it later will recreate the file." Default Yes. Keeping it marks the buffer modified; declining closes the tab. This acknowledgement is kept in the GUI, since a deleted file has nothing to stat.
Triggers: a burst of fs.changed events, and the application regaining focus, the
same two triggers as the files pane. A check never runs while another core call is in
flight (it defers and retries), and it cannot re-enter through a dialog's event loop. A
burst of events is debounced into one refresh with a 60 ms timer; an idle callback would
split a burst read line by line from the channel.
Consequences
- Stale buffers can no longer overwrite newer files unnoticed.
- A same-second, same-length rewrite is not detected; a content hash is the upgrade path if it matters.
- There is still no live file watching. Changes made outside rio are noticed on the next focus return.