Rio
adr/0094-stale-buffers.mdRaw Markdown · GitHub ↗

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.