Skip to content

fix(core): release every file lock at once, even while a command is starting - #355

Merged
Max17190 merged 1 commit into
mainfrom
release-locks-explicitly
Oct 6, 2026
Merged

Max17190 merged 1 commit into
mainfrom
release-locks-explicitly

Conversation

@Max17190

@Max17190 Max17190 commented Oct 6, 2026 •

Copy link
Copy Markdown
Owner

Why

Open Max guards sessions, the session index, trust.json, and the usage ledger with flock(2) locks. A flock belongs to the open file description, not to one descriptor, and a child that is being spawned (a bash call, a tool, a hook) holds a copy of every descriptor the harness has open until it execs. The session owner and the index lock were released only by closing the file, and trust.lock was too whenever a trust change returned early with an error. So a lock released while a command was starting stayed held by that child:

  • Closing a session and reopening it in that window, from the same process or another one, was refused with "already open in another process".
  • Other processes waiting on the index or trust lock stalled on a child they never started.

The ledger lock already unlocked explicitly, but not when a panic unwound past it.

Summary

  • Add FileLock, a guard that takes the same exclusive flock(2) and calls an explicit unlock when it is dropped, before the file closes. The unlock releases the lock no matter which spawning child still holds a copy of the descriptor.
  • Route every lock through it: the session owner (claim_session), the index lock (lock_index and lock_store), trust::update (refused changes now release too), and both ledger lock sites (with_lock and record_usage).
  • The lock taken and the order locks are taken in are unchanged, so older binaries sharing the data dir still exclude this one and are excluded by it. Nothing retries.
  • Add a test-only hook (PausedSpawn, compiled only in Unix test builds) that runs a real child through run_process and holds it between fork and exec, so tests can release locks inside the window.
  • The raw_flock test helper, which stands in for an older binary, now returns a FileLock too, so a child another test is spawning cannot hold its lock past the drop.
  • No docs, persisted formats, or model-visible prompt bytes change.

Test Plan

  • sessions::tests::a_released_session_reopens_while_a_child_is_spawning: with a child held between fork and exec, a released session reopens from the same core and then from a second core, and a held session still refuses a second owner. Fails on main with the reopen refused as open in another process.
  • sessions::tests::a_released_index_lock_is_free_while_a_child_is_spawning: a dropped index lock can be taken by an independent flock while a child is spawning. Fails on main.
  • trust::tests::a_refused_trust_change_frees_its_lock_while_a_child_is_spawning: a trust change that returns an error releases trust.lock while a child is spawning. Fails on main.
  • cargo test --workspace --locked: passes.
  • cargo +1.97.0 clippy --workspace --all-targets --locked -- -D warnings: passes.

RetriggerConfidence Score: 5/5

Safe to merge based on the changed code and completed checks.

What we checked:

  • Ran the focused session, index, and trust lock tests on the changed revision; all three tests passed. T-Rex
  • Compared pausing behavior during paused spawn and observed that on the parent revision the other Core's attach was refused until exec, while on the changed revision it succeeded before exec, with the index lock remaining available in both runs. T-Rex
  • Executed the persistence lock repro script and ran the three focused tests in disposable worktrees; the parent revision failed the refused-trust lock check while the changed revision passed all three checks. T-Rex
  • Compared the HEAD and HEAD^ paused-child repro and observed that the paused child kept the released session lock, the attach succeeded on HEAD before exec, and the repro exited 0, with no index stall observed. T-Rex
  • Noted that the persistence_lock_repro.py was executed locally and its logs are available locally, with no remote upload performed. T-Rex
Summary

The PR adds an explicit-unlock guard for session, index, trust, and ledger file locks, plus Unix tests for lock release while a child is between fork and exec. A released session could be reopened during that window, and a refused trust change released its lock. Focused checks found no issue requiring a change before merging.

Reviews (1) · Last reviewed commit: "fix(core): release every file lock at on..."

…tarting

Reopening a session right after closing it could be refused as already
open in another process, and other openmax processes could wait on the
session index, trust.lock, or ledger.lock with no visible holder. It
happened whenever a bash call, external tool, or hook was being started
at the moment of release.

Each of these locks is a flock(2), which belongs to the open file
description, and the harness released it only by closing its descriptor.
Those children are spawned through fork and exec (each needs a session of
its own), and between the two a child holds a copy of every descriptor
the harness has open, so the lock stayed held until the child exec'd.

Every lock is now held through a guard that unlocks explicitly before the
file closes: the session owner, the index lock, trust.lock (a refused
change included), and ledger.lock. The lock taken and the order locks are
taken in are unchanged, so older binaries still exclude this one.
@Max17190
Max17190 merged commit d3eacc8 into main Oct 6, 2026
11 checks passed
@Max17190
Max17190 deleted the release-locks-explicitly branch October 10, 2026 21:15
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