Skip to content

fix(models): hold an exclusive OS lock on the destination claim for the transfer - #856

Open
mattmillerai wants to merge 1 commit into
mainfrom
matt/be-9923-held-claim-lock
Open

fix(models): hold an exclusive OS lock on the destination claim for the transfer#856
mattmillerai wants to merge 1 commit into
mainfrom
matt/be-9923-held-claim-lock

Conversation

@mattmillerai

Copy link
Copy Markdown
Collaborator

ELI-5

Two comfy model download --background commands must never write the same file at once, so the first one to arrive "claims" the destination. Until now that claim was a note on disk saying download abc123 owns this, and anyone else who came along had to look up abc123's record and guess whether it was still alive. Guessing was the problem: a worker that had been asked to cancel but had not actually died yet, or one whose record simply could not be read for a moment, both looked dead — so a second download would take the destination away from a worker that was still busy writing to it, and both would stream into the same file.

Now the worker doesn't leave a note, it holds the door shut. It takes a real exclusive OS lock on the claim file and keeps it for the whole transfer. Nobody has to guess whether it is alive: if the lock is held, it is alive. And if the worker is kill -9'd, the operating system drops the lock for it, so the next download gets the destination straight away instead of waiting out a timeout.

Description

Destination ownership was derived on every access — read the claim's download_id, resolve it to a state record, judge whether that record looks live. This makes it held: an exclusive OS file lock (flock on POSIX, msvcrt.locking on Windows) taken on the claim file and kept for the transfer's lifetime by the process moving the bytes. A held lock is the liveness proof, so no record is consulted and nothing is inferred.

Three windows that derived ownership left open, all closed:

  • Read-then-unlink in release_claim. It compared the recorded owner and then unlinked, so a claim that changed hands between the compare and the unlink was deleted out from under its new owner. release_claim is gone; nothing unlinks on release at all.
  • Terminal-record-but-live-worker, and transient read failure, in _claim_holder. download-cancel writes state.status = "cancelled" unconditionally even when stop_worker returns false (models.py:1968, with error set to "worker may still be running; partial file left in place"), and a record that momentarily fails to read came back indistinguishable from a record that is gone. Both made a live claim read stale. That worker now still holds its lock, so the refusal stands regardless of what its record says or whether the record can be read at all.
  • The once-only stale-clear retry. Clearing a stale claim and re-creating it were two steps another submitter could interleave with, which is the only reason a for attempt in (1, 2) loop existed. The steal now happens in one step under the lock, so the loop and its model_download_claim_unclearable error code are both deleted.

The primitive

lock_claim(path, *, blocking_timeout=0) -> ClaimLock | None in download_state.py:

  • Opens O_CREAT without O_EXCL — the claim file is a lock target that outlives every holder, so it usually already exists and creating it decides nothing. What decides is the lock.
  • Re-stat guard. Between the open and the lock, the file can be unlinked and replaced, leaving us holding a real lock on an orphan inode. After locking, os.fstat(fd) is compared against os.stat(path); a differing (st_dev, st_ino) — or a vanished path — means we lost that race, so the fd is closed and reopened, bounded at 3 attempts. Windows skips the guard (inode identity is unreliable there) and pays for it by never unlinking a claim file at all, which removes the race rather than detecting it.
  • Returns None for contention, and raises OSError for a filesystem that cannot lock — the two must not be confused, because only the first means somebody owns the destination.
  • read_payload() / write_payload() work in place on the locked descriptor (lseek + ftruncate + write). Never temp+rename: a rename swaps the inode the lock lives on. Payload shape is unchanged ({download_id, dest, created_at}, still no url — fix(models): close the background download race with an atomic O_EXCL destination claim #777's credential rationale stands).
  • release() closes the fd and does not unlink. Unlinking a file while holding its lock leaves every descriptor already opened on that path guarding an orphan inode, so two holders could each hold a genuine lock on a different inode for one destination. A claim is ~100 bytes keyed by destination hash, so the directory is bounded by distinct destinations; prune bounds it further by unlinking only claims it can both lock and prove dead (POSIX only).

Submit and worker

The submitter locks non-blocking, arbitrates, stamps its id, releases, then spawns. The worker re-takes the lock with a short blocking window (5s — a competitor may hold it for the ~100 byte read/rewrite it takes to inspect), verifies the payload names it, and then holds it for the whole transfer. A payload naming a different download means it lost the destination during the spawn gap: it exits refused without touching dest, writing its record terminal exactly as the submit-side withdraw path does.

The gap between the submitter's release and the worker's lock is the one thing a bare lock cannot arbitrate, so the payload closes it (_spawn_gap_holder): a payload naming an active record younger than STARTUP_GRACE_S is a submit whose worker has not booted yet, and the destination is its own. Past the grace, a genuinely live download would be holding the lock — so having got there proves it is not, and refusing on the payload alone would resurrect the derived-ownership guess this change removes.

_enforce_claim and its OSError degradation are both kept: it is still the foreground path's only guard.

Judgment calls

  • _spawn_gap_holder narrows an existing check, so per Chesterton's Fence it is called out explicitly. The old _claim_holder treated any active-after-reconcile record as live; the new one additionally requires it to be younger than STARTUP_GRACE_S. The two can only disagree when the lock is not held, and under this design a live worker always holds it — so the narrowed branch is reachable only in the spawn gap, which is exactly what it exists to arbitrate. This is the shape the ticket specifies, and the reasoning is written into the function's docstring rather than only here.
  • A worker that finds the claim unstamped adopts it rather than refusing. Nobody can own a destination whose lock we are holding, so an empty payload means only that nothing stamped it (a submitter that degraded past write_payload on ENOSPC, or a sweep that removed an unowned file our own O_CREAT then re-made). Refusing there would turn a recoverable submit-side hiccup into a download that never runs.
  • _sweep_claims unlinks under the lock, which ClaimLock.release()'s docstring warns against. That is deliberate and POSIX-only: it is precisely the case the re-stat guard exists to absorb, and the guard exists only on POSIX. On Windows the sweep skips .claim files entirely.

Provenance

  • Authored by: agent-work loop
  • Verified: pytest (full suite): 7413 passed, 39 skipped, 1 failed — the single failure, tests/comfy_cli/test_http.py::test_an_unloadable_supplement_falls_through_to_the_platform_roots, reproduces identically on a clean origin/main worktree (an environment-dependent CA-bundle cert count, assert 182 == 145) and is unrelated to this change; pytest tests/comfy_cli/command/test_model_download_background.py: 236 passed, 1 skipped (the skip is the Windows-only msvcrt test); ruff check .: all checks passed; ruff format --check .: 447 files already formatted.
  • Deviations: none against the specified plan; the scoped-out items are under Residual below.

Residual

Not fixed here, and actionable on their own:

  • The msvcrt / Windows locking path is entirely unexercised — by this change and by CI. Its unit test carries skipif(sys.platform != 'win32') exactly as specified, so it does not run on the Linux/macOS machine this was written and verified on. It will not run in CI either: .github/workflows/pytest.yml is runs-on: ubuntu-latest, and .github/workflows/test-windows.yml pip install pytest but never invokes it — it is a comfy install / comfy launch smoke test. So every Windows-specific claim in this change is reasoned, not measured: that msvcrt.locking(fd, LK_NBLCK, 1) after seek(0) reports contention as EACCES; that ftruncate to zero is permitted on a byte-0 range locked by the same handle; that a locked byte range makes another process's unlocked read_bytes() fail (which the refusal path relies on to fall back to an id-less message); and that the winerror set {1, 50} is what a volume with no byte-range locking returns. Worth either running the unit suite on the Windows runner or exercising these four behaviours by hand on Windows before relying on them.
  • The foreground path is untouched and still uses derived ownership. A plain comfy model download writes no claim file and is guarded only by _enforce_claim's advisory re-scan, so a foreground transfer and a background worker can still both write one destination. This is the ticket's stated non-goal and belongs to the follow-on ticket that converts the foreground path.
  • Network filesystems are best effort. flock over NFS/SMB is advisory at best and may be a local-only lock, so two machines sharing a mount can still both win. Strictly no worse than the pid-based liveness it replaces (which was meaningless across machines), and documented as such in the module comment — but it is not fixed, and cross-machine correctness was an explicit non-goal.
  • The cross-workspace residual is unchanged. Claims live under get_workspace(), and --relative-path is only expanduser-ed, so two invocations run from different workspaces against one destination still consult different claim directories and neither sees the other. Pre-existing, carried forward from fix(models): close the background download race with an atomic O_EXCL destination claim #777.
  • A submitter that dies between writing its record and spawning its worker still wedges its destination for up to STARTUP_GRACE_S (60s). This is the residual the startup grace has always had; the held lock does not remove it, because in that window there is no process left to hold anything.
  • Named artifacts I could not read. The ticket cites a parent epic and the read-only spike issue whose findings comment it says it was built from; their bodies were not available to me, so the root-cause narrative above was re-derived from the code rather than checked against that evidence. I verified the download-cancel leg directly against live source (models.py:1968) and the other two legs against the pre-change release_claim / _claim_holder bodies, but I cannot confirm the spike found nothing further.
  • Provenance of the starting point. A prior interrupted attempt had left an uncommitted working tree on this branch. Rather than assume it, I reviewed the whole diff line by line against the plan, re-derived the root causes from source, and re-ran the full suite, lint and format from scratch; the commit is the reviewed result. Flagging it because "it already passed its own tests" was not evidence I treated as sufficient, and a reviewer should not either.

…he transfer

Ownership of a background download's destination was re-derived on every
access: a submitter read the claim file's `download_id`, resolved it to a
state record, and asked whether that record looked live. Deriving left three
windows open.

* `release_claim` compared the recorded owner and then unlinked, so a claim
  that changed hands between the two steps was deleted out from under its new
  owner.
* `_claim_holder` read a record to judge liveness, so both a record that reads
  terminal while its worker is demonstrably still running (`download-cancel`
  writes `cancelled` even when `stop_worker` fails) and a transient failure to
  read that record at all made a live claim look stale — and the destination
  was stolen from a worker mid-transfer.
* Clearing a stale claim and re-creating it were two separate steps, so the
  steal needed a once-only retry that another submitter could interleave with.

Ownership is now *held*: the process performing the transfer takes an
exclusive OS lock (`flock` on POSIX, `msvcrt.locking` on Windows) on the
claim file and keeps it for the whole transfer. A held lock is the liveness
proof, so no record has to be consulted and no liveness has to be guessed. A
SIGKILLed worker's lock is dropped by the kernel, so the next submitter
acquires immediately — no reconcile, no grace wait, no sweeper.

New `ClaimLock` primitive in `download_state`: `lock_claim` opens `O_CREAT`
(no `O_EXCL` — the file is a lock target that outlives every holder), locks
non-blocking, then re-stats `fstat(fd)` against `stat(path)` so a lock taken
on an inode that was unlinked underneath us is discarded and retried. The
payload is read and rewritten in place on the locked descriptor, never via
temp+rename, because a rename swaps the inode the lock lives on. `release()`
closes the fd and deliberately does not unlink: a claim is ~100 bytes keyed by
destination hash, and unlinking under a lock is how two holders end up
guarding two different inodes for one destination. `prune` bounds the
directory instead, unlinking only claims it can both lock and prove dead
(POSIX only — Windows has no reliable inode identity, so it never unlinks).

The submit path takes the lock non-blocking, arbitrates, stamps its id and
releases before spawning; the worker re-takes it with a short blocking window
and holds it for the transfer. The gap between those two is the one thing a
bare lock cannot arbitrate, so the payload closes it: a payload naming an
active record younger than `STARTUP_GRACE_S` is a submit whose worker has not
booted yet and the destination is its own.

`acquire_claim`, `release_claim` and `_claim_holder` are removed with no
callers left, and `model_download_claim_unclearable` goes with the
clear-then-retry loop that was its only source. The `OSError` degradation to
`_enforce_claim` on a filesystem that cannot lock is kept, as is
`_enforce_claim` itself — it remains the foreground path's only guard.
@mattmillerai mattmillerai added the agent-coded PR authored by the agent-work loop label Sep 6, 2026
@mattmillerai
mattmillerai marked this pull request as ready for review September 6, 2026 00:01
@coderabbitai

coderabbitai Bot commented Sep 6, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Team

Run ID: 22487e51-ceeb-4b16-8df9-fb13dc9b061d

📥 Commits

Reviewing files that changed from the base of the PR and between 0343f50 and 8e1d15e.

📒 Files selected for processing (4)
  • comfy_cli/command/models/models.py
  • comfy_cli/download_state.py
  • comfy_cli/error_codes.py
  • tests/comfy_cli/command/test_model_download_background.py

Included review availability: Your plan provides up to 10 included reviews per hour; 3 remain after this review.


📝 Walkthrough

Walkthrough

Background downloads now use persistent OS locks for destination claims. Workers retain ownership during transfers, update claim payloads in place, handle spawn gaps and lock failures, and preserve claim files for later sweeping. Tests cover cross-process arbitration, lifecycle failures, platform behavior, and cleanup.

Changes

Destination claim locking

Layer / File(s) Summary
Cross-platform claim lock primitive
comfy_cli/download_state.py
ClaimLock replaces atomic claim creation and owner-checked unlinking. It supports bounded locking, in-place payload updates, advisory reads, descriptor release, and platform-specific behavior.
Background download arbitration
comfy_cli/command/models/models.py
Submission stamps the claim before worker startup. Workers reacquire and hold the lock through transfers, detect ownership loss, handle spawn failures, and degrade when locking is unavailable.
Claim cleanup and refusal paths
comfy_cli/download_state.py, comfy_cli/error_codes.py, comfy_cli/command/models/models.py
Sweeping and pruning preserve persistent claim files. Owned payloads are cleared under lock. Contested claims use the consolidated model_download_claim_contested error.
Claim lock lifecycle validation
tests/comfy_cli/command/test_model_download_background.py
Tests cover process races, stale and orphan claims, worker termination, spawn failures, payload writes, platform locking, degradation, terminal cleanup, and pruning.

Sequence Diagram(s)

sequenceDiagram
  participant BackgroundSubmitter
  participant ClaimLock
  participant DownloadWorker
  participant Destination
  BackgroundSubmitter->>ClaimLock: acquire and stamp destination claim
  BackgroundSubmitter->>DownloadWorker: spawn worker
  DownloadWorker->>ClaimLock: reacquire claim lock
  DownloadWorker->>ClaimLock: retain lock during transfer
  DownloadWorker->>Destination: write downloaded model
  DownloadWorker->>ClaimLock: clear or update payload
  DownloadWorker->>ClaimLock: release descriptor
Loading

Suggested reviewers: annehe9, skishore23

Merge Risk: ⚪ Minimal · up to 8e1d1

Background downloads now retain OS-level destination locks throughout transfers, with covered contention and cleanup behavior. No actionable merge-blocking risk is currently identified.

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch matt/be-9923-held-claim-lock
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch matt/be-9923-held-claim-lock

Comment @coderabbitai help to get the list of available commands.

@coderabbitai
coderabbitai Bot requested review from annehe9 and skishore23 September 6, 2026 00:02
@mattmillerai mattmillerai added the cursor-review Request Cursor bot review label Sep 6, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Cursor Review — Consolidated panel

Triggered by @mattmillerai.

Found 10 finding(s).

Severity Count
🟠 High 1
🟡 Medium 5
🟢 Low 4

Panel: 5/6 reviewers contributed findings.

Reviewers that did not contribute: gpt-5.6-sol-max:edge-case (error)

_enforce_claim(state, dest)
return

if lock is None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High — The submit path takes the claim lock with the default blocking_timeout=0 and treats any None as proof that a live owner has the destination, but non-owners now take the same lock: prune() runs at the head of every background submit (and of every comfy model downloads), and _sweep_claims locks every claim file in the workspace, holding each one across read_payload(), reconcile() (which stats dest) and worker_alive() (a psutil query). A submit to destination X can therefore be hard-refused with model_download_claim_contested because an unrelated submit — or an agent polling downloads — is sweeping X's claim at that instant. Give the submit path the same short blocking window the worker gets, and/or shorten the sweep's hold to the payload read with the unlink re-validated under a second short lock.

Raised by 3 of 6 reviewers (claude-opus-5-thinking-max edge-case, gpt-5.6-sol-max adversarial, kimi-k3-high adversarial).

path = Path(path)
deadline = time.monotonic() + max(0.0, float(blocking_timeout))
for _ in range(CLAIM_LOCK_RESTAT_ATTEMPTS):
fd = os.open(str(path), os.O_CREAT | os.O_RDWR | _O_BINARY, STATE_FILE_MODE)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Mediumlock_claim opens the deterministic claim path with O_CREAT | O_RDWR and no O_NOFOLLOW, and never checks that the descriptor is a regular file before os.fchmod, os.ftruncate and the payload write. A symlink planted at claims/<sha>.claim therefore becomes an arbitrary-file truncate-and-overwrite, and a FIFO or device node makes read_payload's loop hang; claims_dir's 0700 chmod is best-effort and suppressed, so a pre-existing directory the user does not own stays reachable. The old os.link publish failed closed with EEXIST on anything already at the path, so this is a new capability — add O_NOFOLLOW on POSIX plus an S_ISREG(os.fstat(fd).st_mode) check before truncating.

Raised by 3 of 6 reviewers (gpt-5.6-sol-max adversarial, claude-opus-5-thinking-max adversarial, kimi-k3-high adversarial).

payload = lock.read_payload()
holder_id = payload.get("download_id") if payload is not None else None
if holder_id is not None and holder_id != state.id:
holder = _spawn_gap_holder(holder_id)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium_spawn_gap_holder runs while the claim lock is held and is far more than the "~100 byte read and rewrite" that WORKER_CLAIM_LOCK_TIMEOUT_S is explicitly sized against: it reads another state file, runs _reconciled (stat on the destination plus a psutil query) and persists a status correction. The write_payload failure branch below is worse, calling _enforce_claim, which list_alls and reconciles the whole state directory under the same held lock. On a slow or hung destination mount this exceeds the worker's 5 s window, so the legitimately-owning worker concludes it lost its own destination and writes failed with "another download claimed this destination first"; release the lock before doing this work.

Raised by 2 of 6 reviewers (claude-opus-5-thinking-max edge-case, claude-opus-5-thinking-max adversarial).

# download that never runs. Stamp it and carry on.
payload = claim_lock.read_payload() if claim_lock is not None else None
owner = payload.get("download_id") if payload is not None else None
claim_lost = claim_lock is None or (owner is not None and owner != state.id)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium — The worker treats any payload naming a different id as decisive, with no liveness or staleness check on that owner — unlike the submit side, which routes the same question through _spawn_gap_holder (active after reconcile and younger than the grace). Payloads are never blanked on release and never swept on Windows, and a submitter that degraded past write_payload never re-stamps, so a payload naming a long-dead download is a normal state: the worker reads it, marks its own record failed with "another download claimed this destination first", and a download that should have run never does. Mirror _spawn_gap_holder's liveness test here so the worker adopts the destination instead of refusing.

Raised by 2 of 6 reviewers (kimi-k3-high edge-case, kimi-k3-high adversarial).

# means no lock was taken and the caller has to degrade rather than conclude
# somebody else owns the destination.
_LOCK_CONTENDED_ERRNOS = frozenset(
getattr(errno, name) for name in ("EACCES", "EAGAIN", "EWOULDBLOCK", "EDEADLK", "EDEADLOCK") if hasattr(errno, name)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium_LOCK_CONTENDED_ERRNOS includes EACCES unconditionally, but EACCES is only the contention signal for msvcrt.locking on Windows — POSIX flock reports contention as EWOULDBLOCK/EAGAIN. A genuine EACCES (a mount or LSM that denies the lock outright) is therefore silently reported as "somebody else holds this", so _acquire_dest_claim takes the lock is None branch and refuses every submit to that destination with model_download_claim_contested forever, rather than degrading to the advisory guard with a warning the way ENOLCK does. Gate EACCES and EDEADLOCK behind sys.platform == "win32".

Raised by 2 of 6 reviewers (claude-opus-5-thinking-max adversarial, kimi-k3-high adversarial).

if holder is not None:
return _in_flight_failure(holder, dest)
named = f" ({holder_id})" if holder_id else ""
return _download_failure(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium_refuse_held_claim resolves the payload's id to a record and unconditionally emits model_download_in_flight from it with no liveness check, but the lock and the payload are independent: a worker writes its completed record before releasing the lock, and a non-owner (_sweep_claims) can hold the lock over a payload naming a long-finished download. Both cases produce "A background download (id) is already writing to " with details.status set to completed/failed plus a hint to cancel a download that has already finished, contradicting the documented contract of that code. A terminal status on the resolved record should fall through to the model_download_claim_contested wording instead.

Raised by 1 of 6 reviewers (claude-opus-5-thinking-max edge-case).

# Every attempt raced a concurrent unlink. Reporting "held" rather than
# "acquired" is the safe direction: the caller refuses instead of letting a
# second transfer into a destination whose ownership we could not settle.
return None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Low — Exhausting CLAIM_LOCK_RESTAT_ATTEMPTS returns None, which is indistinguishable from "held" to every caller: the submitter refuses, and _download_worker sets claim_lost and writes a terminal failed record for a download nobody actually contested. Since _sweep_claims is the only thing that unlinks claims and it runs from prune() on every submit and every downloads call, a few unlucky interleavings of internal bookkeeping can kill a legitimate transfer. Note also that deadline is computed once before the retry loop, so a caller that asked for a blocking window can reach attempt 2 or 3 with zero budget; a distinct signal (or a raised OSError, which callers already degrade on) would let callers tell "contested" from "could not settle".

Raised by 2 of 6 reviewers (claude-opus-5-thinking-max edge-case, claude-opus-5-thinking-max adversarial).

os.lseek(fd, 0, os.SEEK_SET)
chunks: list[bytes] = []
while True:
block = os.read(fd, 65536)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Lowread_payload slurps to EOF with an unbounded while True: os.read(fd, 65536) loop and then duplicates the result via b"".join(chunks), with no size cap on a payload that is supposed to be ~100 bytes. A corrupt or planted oversized claim can exhaust memory while the destination lock is held, wedging every submit and prune for that claim; a cheap byte cap that bails to None bounds the allocation for free.

Raised by 2 of 6 reviewers (gpt-5.6-sol-max adversarial, claude-opus-5-thinking-max adversarial).

path.unlink()
continue
if not path.name.endswith(".claim"):
if not path.name.endswith(".claim") or sys.platform == "win32":

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Low — The or sys.platform == "win32" short-circuit makes the sweep skip every .claim file on Windows, and ClaimLock.release never unlinks one, so claims/ grows by one file per distinct destination for the life of the workspace — the same unbounded growth prune's docstring says this sweep exists to prevent — while _sweep_claims keeps materialising sorted(base.iterdir()) on every prune(), i.e. every submit and every downloads call. A Windows-safe reclaim (age-based, or moving the file aside under the lock instead of unlinking) would keep the stated bound.

Raised by 2 of 6 reviewers (gpt-5.6-sol-max adversarial, claude-opus-5-thinking-max adversarial).

fd, self._fd = self._fd, None
if fd is None:
return
with contextlib.suppress(OSError):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Low — On Windows the msvcrt.locking(fd, LK_NBLCK, 1) byte-range lock is never explicitly released — release() only closes the descriptor, and Windows documents unlock-on-close timing as depending on available system resources. The normal spawn-gap handoff (submitter releases, worker immediately re-locks) can therefore observe spurious contention, surfacing as a bogus model_download_claim_contested refusal or a worker claim_lost on a destination that is actually free. An explicit msvcrt.locking(fd, LK_UNLCK, 1) at offset 0 before os.close would make release deterministic.

Raised by 1 of 6 reviewers (kimi-k3-high edge-case).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent-coded PR authored by the agent-work loop cursor-review Request Cursor bot review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant