The Agent Read Your Note 40 Seconds Ago — It's About to Write What It Read
Vi-style editors have shown this warning since the 1980s: W11: Warning: File "%s" has changed since editing started.1 The editor detected that the file on disk diverged from the snapshot it loaded, stopped, and asked what to do. Your AI agent probably did not.
An agent reads a Markdown file, reasons for 30 to 90 seconds, then writes the complete file back. If you edited the same note in that window, your edit is gone — replaced by the agent's output built on its stale read. No conflict. No warning. No trace. This is a textbook lost update, applied to plain files, by a system with no built-in concurrency guard at all.
This post explains why the anomaly is structurally inevitable for agents working on plain files, how a 43-year-old concurrency model predicts exactly the gap they fall into, and three repair patterns you can add today — in ascending strictness: a stat-check before write, an advisory flock, and an atomic rename.
What is a lost update, and why does every agent produce one?
A lost update is the oldest anomaly in concurrent data access. Transaction T1 reads a value. Transaction T2 reads the same value, modifies it, and writes it back. T1 then writes its own version — based on its stale read — and T2's update disappears silently, with no error surfaced by the system that allowed it.
Berenson et al. formalized this as anomaly P4 in their 1995 critique of ANSI SQL isolation levels; the definition has held for three decades.2 For a plain file the story is identical: the agent is T1, your editor is T2, and the file is the shared value. The agent loads the file, pauses to reason, then calls its write tool. If you saved an edit during that pause, your change is overwritten.
The window is not short. A single reasoning step — an LLM call that plans the edit, validates the output, and settles on the final text — commonly takes 30 to 90 seconds for a note of moderate length. In an active writing session that is time for several saves. Every one is invisible to an agent that is not watching the file.
The three-phase model that explains the gap
In 1981, H. T. Kung and J. T. Robinson published "On Optimistic Methods for Concurrency Control" in ACM Transactions on Database Systems.3 They described a three-phase structure: a read phase, in which a transaction executes and caches its writes locally; a validation phase, in which it checks whether a conflict occurred before committing; and a write phase, in which it applies cached changes to shared state only if validation passed.3 Their framing of the optimism is worth quoting directly from the abstract: "The methods used are 'optimistic' in the sense that they rely mainly on transaction backup as a control mechanism, 'hoping' that conflicts between transactions will not occur."4
A database that implements OCC still runs the validation step. It compares what it read at the start of the transaction against the current state of shared data. If anything changed, the transaction rolls back and the write never lands on stale data.
An agent calling its write-file tool collapses all three phases into one. It reads. It reasons. It writes. The validation phase — the entire architectural point of optimistic concurrency — is simply absent. The agent is optimistic without the corresponding check. This is not a bug in any specific framework. It is the structural reality of plain-file writes: the filesystem offers no automatic snapshot comparison, no rollback, no conflict detection. The application has to build those things. Most agent tool implementations do not.
Figure 1 — OCC three-phase model versus agent plain-file write. A database implementing optimistic concurrency control runs all three phases — Read, Validate, Write — rolling back on any conflict detected in the validate step. An agent calling its write-file tool collapses the three phases into two: it reads at T0, reasons for 30–90 seconds, then writes at T1 with no validate step between them.
flowchart LR
subgraph OCC ["OCC (database)"]
R1[Read] --> V[Validate]
V -->|pass| W1[Write]
V -->|conflict| RB[Rollback]
end
subgraph Agent ["Agent (plain file)"]
R2["Read (T0)"] --> Rz["Reason (30–90 s)"]
Rz --> W2["Write (T1)"]
end
The naive approach, and why it fails
The obvious first reaction is that the problem is rare. Agent edits are quick; users are slow; collisions must be uncommon. This reasoning fails on two counts. First, it assumes the agent and the human are taking turns. In practice an agent is often triggered by a save event — exactly the moment the human is most actively editing. Second, even low-probability silent corruption is unacceptable for notes. A database that silently dropped a write once a month would be considered broken. The bar for personal knowledge files is the same.
The second naive approach is to use file locking. Most POSIX environments offer two locking APIs: flock(2) and fcntl(2). Both can be acquired before writing and released after. But the Linux man page for flock(2) is explicit in its NOTES section: "flock() places advisory locks only; given suitable permissions on a file, a process is free to ignore the use of flock() and perform I/O on the file."5 An advisory lock works only when every cooperating process checks for and respects it. A text editor that has never heard of your agent's locking convention will not check. The lock signals intent; it does not block anyone who does not listen.
The question of which kind of write operation an agent should perform — append versus full-file overwrite — is a separate problem addressed in Let Your AI Agent Append, Not Overwrite. This post addresses the read-to-write window regardless of what the write does.
Three repair patterns in ascending strictness
The patterns below form a ladder. Start at pattern 1 for minimum friction; climb as your workflow demands stronger guarantees.
Pattern 1 — Stat-check before write
The cheapest guard records the file's metadata at read time and compares it immediately before writing. The st_mtime field in a POSIX inode is the file's last modification timestamp. The Linux man page for inode(7) defines it precisely: "This is the file's last modification timestamp. It is changed by file modifications, for example, by mknod(2), truncate(2), utime(2), and write(2) (of more than zero bytes)."6 If st_mtime or st_size changed between read and write, a concurrent modification occurred.
This is exactly what vi-style editors have done since the 1980s. Vim's source code in src/fileio.c assigns the W11 message string when it detects that the file changed since the buffer was loaded.1 The stat-check is the oldest file-change detection available, and it requires no kernel support beyond stat(2).
import os
def safe_write(path: str, new_content: str) -> None:
stat_at_read = os.stat(path)
# ... agent reasoning happens here ...
stat_before_write = os.stat(path)
if (stat_before_write.st_mtime != stat_at_read.st_mtime or
stat_before_write.st_size != stat_at_read.st_size):
raise RuntimeError(
f"File '{path}' was modified between read and write. "
"Refusing to overwrite."
)
with open(path, "w") as f:
f.write(new_content)
The guard adds two os.stat() calls — negligible cost. What it does not provide is resolution: on a conflict the agent needs a strategy (retry, merge, surface the conflict to the user). Pattern 1 detects the anomaly; it does not resolve it.
Pattern 2 — Advisory flock before the read
Wrapping the entire read-reason-write cycle in an flock(2) advisory lock tells any other cooperating process to wait. If your editor and your agent both acquire a shared/exclusive lock, the editor will block while the agent holds the write lock.
import fcntl
def locked_write(path: str, new_content: str) -> None:
with open(path, "r+") as f:
fcntl.flock(f, fcntl.LOCK_EX) # exclusive advisory lock
original = f.read()
# ... agent reasoning happens here ...
f.seek(0)
f.write(new_content)
f.truncate()
fcntl.flock(f, fcntl.LOCK_UN)
The critical constraint is the word "advisory." The man page for flock(2) states that a process is "free to ignore the use of flock() and perform I/O on the file."5 A text editor opened by a user who is unaware of your locking convention will write without acquiring the lock. Pattern 2 is stronger than Pattern 1 in a controlled environment where all writers cooperate. It offers no protection against non-cooperating writers — which is why §13 of this post exists.
Pattern 3 — Atomic rename
Pattern 3 eliminates torn writes entirely. Instead of writing to the target file directly, write to a temporary file in the same directory and then rename it over the target.
The Linux man page for rename(2) specifies: "If newpath already exists, it will be atomically replaced, so that there is no point at which another process attempting to access newpath will find it missing."7 The POSIX specification confirms the same guarantee: "That specification requires that the action of the function be atomic."8 The Python standard library exposes this through os.replace(), documenting: "the renaming will be an atomic operation (this is a POSIX requirement)."9
import os, tempfile
def atomic_write(path: str, new_content: str) -> None:
dir_ = os.path.dirname(os.path.abspath(path))
with tempfile.NamedTemporaryFile(
mode="w",
dir=dir_, # same filesystem as target — required for atomic rename
delete=False,
suffix=".tmp"
) as tmp:
tmp.write(new_content)
tmp_path = tmp.name
os.replace(tmp_path, path) # atomic on POSIX
Two caveats: the temp file must be on the same filesystem as the target (cross-filesystem renames are non-atomic and may raise OSError). And the atomic rename still does not detect a concurrent modification — it guarantees a clean commit, not a conflict-free one. Combine Pattern 3 with Pattern 1 to both detect interference and write cleanly when the path is clear.
This pattern is how most database write-ahead logs and safe-write utilities operate at the OS layer. For any agent writing to a Markdown vault, it is the right floor.
When this connects to sync engines and MVCC
This post's problem space is deliberately narrow: a single machine, a single process window, no sync engine on either side. That scope matters, because solutions scale differently once network actors enter the picture.
When two devices with a sync engine both edit the same note, the conflict is handled by the engine — typically through operational transforms or a last-write-wins policy with merge metadata. That scenario is covered in When Two Devices Edit the Same Note.
When a database engine is present, multi-version concurrency control provides snapshot isolation at the storage layer — each reader sees a consistent snapshot, and conflicts are detected at commit time. That mechanism is examined in MVCC in an Embedded SQL Database.
Plain Markdown files sit below both of these. There is no sync engine intercepting the agent's write. There is no MVCC layer taking a snapshot. The only concurrency protection available is what you build into the tool yourself.
Advisory locking signals cooperative intent — it does not block anyone
Advisory locking — whether flock(2) or fcntl(2) — does not block a process that never calls the locking API. The man page is explicit and the behavior is by design. Linux removed support for mandatory file locking (previously available via the -o mand mount option) as of kernel 5.15, released October 2021. Advisory is now the only mechanism available for plain files on Linux.
What this means in practice: Pattern 2 protects against a second cooperating agent that also calls flock. It does not protect against a text editor, a sync daemon, or any other process that writes without acquiring the lock.
The honest summary of all three patterns is this: they make the lost update detectable and recoverable, not impossible. Pattern 1 detects modification before the write lands. Pattern 2 prevents a cooperating second agent from writing concurrently. Pattern 3 ensures that if the write proceeds it lands cleanly, never in a torn intermediate state. None of the three prevents a determined or unaware process from writing at any moment.
If your threat model includes non-cooperating writers — a cloud sync client, a backup tool, a user's editor — the only complete protection is a layer above the file: a lock file by convention that all parties respect, a database with real isolation, or an operational transform protocol. For most agent-assisted Markdown workflows, Pattern 1 combined with Pattern 3 is the right practical floor.
Frequently Asked Questions
Why does my AI agent overwrite my changes to a file? The agent read the file at T0, spent 30 to 90 seconds reasoning, then wrote the complete file at T1 without checking what changed in between. This is the lost-update anomaly: the agent's write is based on a stale snapshot of the file. The filesystem provides no built-in mechanism to detect or prevent this, and most agent tool implementations do not add one.
How do I stop an AI agent from overwriting my edits?
Record st_mtime and st_size when the agent reads the file, re-check them immediately before the write, and raise an error if they changed. For a cleaner commit, combine this with an atomic rename using os.replace(tmp, path). Advisory locking with flock also helps in cooperative multi-agent environments but does not protect against editors that ignore the lock.
Is flock advisory or mandatory on Linux?
flock(2) places advisory locks only. Any process with sufficient permissions can write to the file without calling flock at all. Linux mandatory file locking via the -o mand mount option was removed in kernel 5.15. Advisory is the only mechanism available for plain files on current Linux kernels.
What is a lost update in concurrency control? T1 reads a value. T2 reads and writes it. T1 then writes based on its stale read, discarding T2's change. No error is signaled; the last writer wins. Formally classified as anomaly P4 by Berenson et al. (1995),2 it is the simplest concurrency failure and the one most likely to silently corrupt plain-file data under concurrent access.
What does "atomic write" mean for a file?
Writing to a temp file in the same directory and then calling os.replace(tmp, path) guarantees that any reader sees either the old content or the complete new content — never a partial state. POSIX requires rename() to be atomic.8 The Python os.replace() documentation states: "the renaming will be an atomic operation (this is a POSIX requirement)."9
Can two processes write the same file at the same time?
Yes. On a plain POSIX filesystem nothing prevents concurrent writes without explicit locking. The last write(2) call to complete wins. If both writers write the full file, the result is simply the later writer's content. If the writes overlap at the byte level, the result may be interleaved and corrupted.
How does Vim detect that a file changed on disk?
Vim polls st_mtime and st_size via stat() and emits W11 if they differ from the values recorded when the buffer was loaded.1 The warning string is: W11: Warning: File "%s" has changed since editing started. It is the same stat-check described in Pattern 1 of this post — implemented in a text editor more than three decades ago.
The vi-style editor learned to ask before overwriting in the 1980s. The agent writing to your notes today can learn the same thing in three lines of code.
If you keep your notes in a local Markdown vault and work with AI agents in that context, mnmnote.com is built for exactly the kind of plain-file, local-first workflow this post describes.
Footnotes
-
Vim project,
src/fileio.c. Warning string:mesg = _("W11: Warning: File \"%s\" has changed since editing started"). Available at: https://raw.githubusercontent.com/vim/vim/master/src/fileio.c (grep-confirmed verbatim, 2026-08-09). ↩ ↩2 ↩3 -
H. Berenson, P. Bernstein, J. Gray, J. Melton, E. O'Neil, P. O'Neil, "A Critique of ANSI SQL Isolation Levels," SIGMOD Record 24(2), 1995, pp. 1–10. DOI: 10.1145/223784.223785. (P4 "Lost Update" anomaly. Paraphrase; ACM DL 403 to WebFetch.) ↩ ↩2
-
H. T. Kung, J. T. Robinson, "On Optimistic Methods for Concurrency Control," ACM Transactions on Database Systems 6(2), June 1981, pp. 213–226. DOI: 10.1145/319566.319567. (Cite 1981 TODS, not 1979 VLDB. Three-phase model confirmed via DBLP
journals/tods/KungR81+ Semantic Scholar; paraphrase per BC2.) ↩ ↩2 -
H. T. Kung, J. T. Robinson, ibid., abstract. "The methods used are 'optimistic' in the sense that they rely mainly on transaction backup as a control mechanism, 'hoping' that conflicts between transactions will not occur." Confirmed verbatim via Semantic Scholar: https://www.semanticscholar.org/paper/On-optimistic-methods-for-concurrency-control-Kung-Robinson/b9b2e39c26f9870491bb770e4608fcd197d34edb ↩
-
Linux man-pages,
flock(2), NOTES section. "flock() places advisory locks only; given suitable permissions on a file, a process is free to ignore the use of flock() and perform I/O on the file." https://man7.org/linux/man-pages/man2/flock.2.html (grep-confirmed verbatim, 2026-08-09). ↩ ↩2 -
Linux man-pages,
inode(7), modification timestamp section. "This is the file's last modification timestamp. It is changed by file modifications, for example, by mknod(2), truncate(2), utime(2), and write(2) (of more than zero bytes)." https://man7.org/linux/man-pages/man7/inode.7.html (grep-confirmed verbatim, 2026-08-09). ↩ -
Linux man-pages,
rename(2), DESCRIPTION. "If newpath already exists, it will be atomically replaced, so that there is no point at which another process attempting to access newpath will find it missing." https://man7.org/linux/man-pages/man2/rename.2.html (grep-confirmed verbatim, 2026-08-09). ↩ -
The Open Group, POSIX.1-2017,
rename(), RATIONALE. "That specification requires that the action of the function be atomic." https://pubs.opengroup.org/onlinepubs/9699919799/functions/rename.html (grep-confirmed verbatim, 2026-08-09). ↩ ↩2 -
Python Software Foundation, Python 3 Library Reference,
os.replace(). "the renaming will be an atomic operation (this is a POSIX requirement)." https://docs.python.org/3/library/os.html#os.replace (grep-confirmed verbatim, 2026-08-09). ↩ ↩2