Repository navigation
agent: memory store lock on file_lock; a lock reads as held from before it is taken - #158
Merged
Merged
Conversation
The memory store's `.memory.lock` was a raw flock outside file_lock: not name-matched, not in the held set, so a model write could rename over it and let two memory mutations run at once. StoreLock is now file_lock::try_lock(Target::File(<memdir>/.memory)): an OFD lock on `.memory.beyond-lock` plus the legacy flock on `.memory.lock` older binaries take, both in the held set and refused by write_atomic. An old binary's `.memory.lock` with no record beside it is matched by name. try_lock now marks each lock file held before it tries to lock it, and a failed attempt drops its marks (a Marks guard, kept by FileLock until after it unlocks), closing the window where a held Itself key read as not held. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JimHGjsfk2Ktm5GxyZJKKk
jaredLunde
added a commit
that referenced
this pull request
Oct 7, 2026
… held mark (#160) Two test gaps from #158's audit, each failing with its mutant: - the_store_lock_and_an_old_binarys_flock_exclude_each_other: an old-binary stand-in's flock on .memory.lock makes StoreLock time out, and while we hold it their non-blocking flock is refused (kills Target::File -> Itself). - a_hard_link_to_a_held_legacy_lock_file_is_not_replaced: a hard link to the held legacy `lock` under a non-lock name is refused by write_atomic while held, written once released (kills dropping the legacy file's mark). Claude-Session: https://claude.ai/code/session_01JimHGjsfk2Ktm5GxyZJKKk Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
These are two follow-ups to #153. Each fix has a test that fails without it; I checked that by reverting each fix with temporary toggles.
1. The memory store's lock now goes through
file_lock.Before this change,
<memdir>/.memory.lockwas a rawflockthat bypassedfile_lock. It wasn't matched by name and wasn't in the held set. A modelwriteoreditcould rename a fresh file over it. The next mutation would then lock a different inode, and two memory mutations could run at once.StoreLockis nowfile_lock::try_lock(Target::File(<memdir>/.memory)), polled for 5s as before. That gives:.memory.beyond-lock, released with an explicit unlock, so a forked child can't keep it;flockon.memory.lock, the one older binaries take, so mixed rollout still excludes both ways.Both files are in the held set, and
write_atomicrefuses both.is_lock_filealso matches an old binary's.memory.lockby name when no record lock file sits beside it. Both files are dotfiles, so the memory listing still hides them.2. A lock reads as held from before it is taken.
try_locknow marks each lock file in the held set before it tries to lock it. A failed attempt drops its marks. The marks live in aMarksguard, andFileLockdrops it only after its ownDrophas unlocked. This closes the microsecond window where a heldItselfkey or a hard-link alias read as not held. A brief false "held" during an acquire is the safe direction..memory.lock/.memory.beyond-lockis refused while the store is locked, and a second mutation still waits (times out)memory::file::tests::the_store_lock_file_is_not_replaced_and_mutations_stay_serializedflockStoreLockfails it: the write replaces the inode, and the second acquire then succeeds.memory.lockwith no record beside it is matched by namefile_lock::tests::a_lock_file_is_one_by_its_name_in_any_case_or_by_where_it_sits.memoryname rulefile_lock::tests::a_lock_file_reads_as_held_from_before_it_is_locked_until_after_it_is_releasedmemory::file::tests::concurrent_mutations_lose_no_updateARCHITECTURE.md (Locks) and the
file_lockmodule doc are updated.Local checks:
mcp_*,serve_*andrun_*suites: 2402 passed.--workspace --all-targets -D warnings,cargo fmt --checkand dprint are clean.🤖 Generated with Claude Code
https://claude.ai/code/session_01JimHGjsfk2Ktm5GxyZJKKk