Reported from a live session: a rerun into the same folder hit a write-scope contention refusal, and the remedy that actually cleared it (release) was not discoverable from the error — it had to be found by reading the agent tool description.
The confirmed mismatch
crates/tui/src/tools/subagent/mod.rs:9580 (the action enum description) says:
release clears write claims whose owner is no longer running — the remediation a write-scope contention refusal names; pass agent_id to clear one, omit it to sweep.
The refusal it refers to (crates/tui/src/tools/subagent/coord/ledger.rs:587) names four remedies, and release is not among them:
write-scope contention with {owner}: requested roots {..}, files {..}, contracts {..}
overlap its writable roots {..}, files {..}, contracts {..}.
Claim disjoint sibling write_roots (for example tmp/scan/worker-a and
tmp/scan/worker-b) or exact_files for each output. A read-only worker uses
write_authority=read_only without a write claim. Otherwise serialize the writers
or use worktree isolation; a nested path inside an existing writable root still
overlaps.
So the description asserts a property of an error message that the error message does not have. That is the minimum defect, and it is not in dispute.
The part that needs a repro before anyone writes a fix
The obvious fix — "have the refusal name release when the blocking owner is inactive" — may be unreachable, because the refusal already cannot fire for an inactive owner. register_claim filters the overlap search on liveness (ledger.rs:563-571):
.find(|existing| {
!existing.isolated_worktree
&& existing.claim.owner != claim.owner
&& owner_is_active(&existing.claim.owner) // <-- here
&& existing.claim.overlaps(&claim)
})
If owner_is_active is false, the stale claim is skipped and no refusal is raised at all. release_stale_claims (ledger.rs:625) clears exactly the complement — owners for which that predicate is false. On this reading the two sets are disjoint and release can never be the remedy for a contention refusal, which would make the description wrong in a stronger sense than "incomplete".
But the reporter observed the refusal and observed release clearing it. Both callers pass the same predicate, built by active_coordination_owners (mod.rs:5127):
self.agents.keys().chain(self.worker_records.keys())
.filter(|id| self.is_live_coordination_owner(id))
That set is built from in-process maps. In a fresh session those maps do not contain owners persisted by a previous process, so a stale owner should read as inactive — which again predicts no refusal.
So one of these is true, and a repro should say which:
- The refusal the reporter hit was raised somewhere other than
ledger.rs:587 (there may be a second contention path — validate_write_scope, mod.rs:4214, is a candidate).
- The stale owner was present in
agents/worker_records and is_live_coordination_owner returned true for a process that no longer exists — a liveness bug, and the real defect.
- The owner id is reconstructed identically on a rerun, so the blocking claim belonged to a live owner, and
release clearing it was incidental rather than causal.
Acceptance
Do not "fix" this by adding release to the refusal text unconditionally — that would point users at a sweep that, on the current liveness filter, cannot clear the claim that just blocked them.
Related: #5562 / SHA-6272 (stale write claims locking sub-agents out), release_stale_write_claims at mod.rs:4200.
Reported from a live session: a rerun into the same folder hit a write-scope contention refusal, and the remedy that actually cleared it (
release) was not discoverable from the error — it had to be found by reading theagenttool description.The confirmed mismatch
crates/tui/src/tools/subagent/mod.rs:9580(theactionenum description) says:The refusal it refers to (
crates/tui/src/tools/subagent/coord/ledger.rs:587) names four remedies, andreleaseis not among them:So the description asserts a property of an error message that the error message does not have. That is the minimum defect, and it is not in dispute.
The part that needs a repro before anyone writes a fix
The obvious fix — "have the refusal name
releasewhen the blocking owner is inactive" — may be unreachable, because the refusal already cannot fire for an inactive owner.register_claimfilters the overlap search on liveness (ledger.rs:563-571):If
owner_is_activeis false, the stale claim is skipped and no refusal is raised at all.release_stale_claims(ledger.rs:625) clears exactly the complement — owners for which that predicate is false. On this reading the two sets are disjoint andreleasecan never be the remedy for a contention refusal, which would make the description wrong in a stronger sense than "incomplete".But the reporter observed the refusal and observed
releaseclearing it. Both callers pass the same predicate, built byactive_coordination_owners(mod.rs:5127):That set is built from in-process maps. In a fresh session those maps do not contain owners persisted by a previous process, so a stale owner should read as inactive — which again predicts no refusal.
So one of these is true, and a repro should say which:
ledger.rs:587(there may be a second contention path —validate_write_scope,mod.rs:4214, is a candidate).agents/worker_recordsandis_live_coordination_ownerreturned true for a process that no longer exists — a liveness bug, and the real defect.releaseclearing it was incidental rather than causal.Acceptance
mod.rs:9580stops asserting that it does.Do not "fix" this by adding
releaseto the refusal text unconditionally — that would point users at a sweep that, on the current liveness filter, cannot clear the claim that just blocked them.Related: #5562 / SHA-6272 (stale write claims locking sub-agents out),
release_stale_write_claimsatmod.rs:4200.