Plan
docs/superpowers/specs/2026-08-19-locks-subsystem-design.md#L126
Unlock by path resolves ids from the current snapshot. A resolution that upstream then answers 404 or 409 triggers one snapshot refresh and one retry for that item only, because a stale snapshot can name a lock id that has since been released and reissued.
The spec's As built section doesn't record a departure from this.
What exists today
LockFanOut.RunOneAsync (GitLfsCache/Locks/LockFanOut.cs:130-160) resolves a path to an id from the snapshot. It refreshes only when the path is missing from the snapshot. SendWithRetriesAsync retries only on throttling (429, or 403 with Retry-After), so a 404 or 409 for a stale id goes back to the client as ok:false immediately.
What's missing
Suppose the path's lock was released and re-taken under a new id after the snapshot was built. The unlock is sent to the old id, upstream answers 404 or 409, and the user's unlock fails even though the lock exists. The spec asks for:
- one snapshot refresh on that answer,
- re-resolving the path,
- one retry.
This applies only to items the proxy resolved from a path. Ids the client supplied are never retried.
Acceptance criteria
- A path-resolved unlock whose first attempt returns 404 or 409 refreshes once, re-resolves, and retries once. If the retry also fails, the item reports upstream's status.
- An unlock by a client-supplied id is never retried on 404/409.
- The retry refresh goes through the same single-flight and limiter as the other resolution refreshes.
- Tests cover both the retry path and the no-retry path for client-supplied ids.
Dependencies
Coordinate with #63. It restructures unlock-by-path resolution into one refresh per request, and this retry should use that same mechanism.
Plan
docs/superpowers/specs/2026-08-19-locks-subsystem-design.md#L126The spec's As built section doesn't record a departure from this.
What exists today
LockFanOut.RunOneAsync(GitLfsCache/Locks/LockFanOut.cs:130-160) resolves a path to an id from the snapshot. It refreshes only when the path is missing from the snapshot.SendWithRetriesAsyncretries only on throttling (429, or 403 withRetry-After), so a 404 or 409 for a stale id goes back to the client asok:falseimmediately.What's missing
Suppose the path's lock was released and re-taken under a new id after the snapshot was built. The unlock is sent to the old id, upstream answers 404 or 409, and the user's unlock fails even though the lock exists. The spec asks for:
This applies only to items the proxy resolved from a path. Ids the client supplied are never retried.
Acceptance criteria
Dependencies
Coordinate with #63. It restructures unlock-by-path resolution into one refresh per request, and this retry should use that same mechanism.