Skip to content

agent: memory store lock on file_lock; a lock reads as held from before it is taken - #158

Merged
jaredLunde merged 1 commit into
mainfrom
jared/memory-lock
Oct 7, 2026
Merged

jaredLunde merged 1 commit into
mainfrom
jared/memory-lock

Conversation

@jaredLunde

Copy link
Copy Markdown
Contributor

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.lock was a raw flock that bypassed file_lock. It wasn't matched by name and wasn't in the held set. A model write or edit could rename a fresh file over it. The next mutation would then lock a different inode, and two memory mutations could run at once.

StoreLock is now file_lock::try_lock(Target::File(<memdir>/.memory)), polled for 5s as before. That gives:

  • an OFD lock on .memory.beyond-lock, released with an explicit unlock, so a forked child can't keep it;
  • the legacy flock on .memory.lock, the one older binaries take, so mixed rollout still excludes both ways.

Both files are in the held set, and write_atomic refuses both. is_lock_file also matches an old binary's .memory.lock by 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_lock now marks each lock file in the held set before it tries to lock it. A failed attempt drops its marks. The marks live in a Marks guard, and FileLock drops it only after its own Drop has unlocked. This closes the microsecond window where a held Itself key or a hard-link alias read as not held. A brief false "held" during an acquire is the safe direction.

Claim Test Fails without the fix (checked)
A model write to .memory.lock / .memory.beyond-lock is 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_serialized The old raw-flock StoreLock fails it: the write replaces the inode, and the second acquire then succeeds
An old binary's .memory.lock with no record beside it is matched by name file_lock::tests::a_lock_file_is_one_by_its_name_in_any_case_or_by_where_it_sits Fails without the .memory name rule
A lock reads as held between the open and the lock, and a failed attempt removes its mark file_lock::tests::a_lock_file_reads_as_held_from_before_it_is_locked_until_after_it_is_released Fails when the lock is marked only after it is taken, and when a failed attempt keeps its mark
16 concurrent backend mutations lose no update memory::file::tests::concurrent_mutations_lose_no_update A general serialization guard. It passes with the old lock too, since that also serialized until its file was replaced

ARCHITECTURE.md (Locks) and the file_lock module doc are updated.

Local checks:

  • lib units plus all mcp_*, serve_* and run_* suites: 2402 passed.
  • clippy --workspace --all-targets -D warnings, cargo fmt --check and dprint are clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JimHGjsfk2Ktm5GxyZJKKk

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
jaredLunde merged commit 3bee93f into main Oct 7, 2026
21 checks passed
@jaredLunde
jaredLunde deleted the jared/memory-lock branch October 7, 2026 13:33
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>
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