Allow safe author-lock recovery after the owning MCP session exits #753

Closed
opened 2026-07-18 18:20:34 -05:00 by jcwalker3 · 1 comment
Owner

Summary

A durable author issue lock becomes permanently unusable when the MCP session that created it exits, even though every piece of ownership evidence still matches exactly. There is no sanctioned native path to recover it, and the obvious re-lock path is blocked by a guard that a real PR branch can never satisfy.

Observed failure

Reproduced against PR #750 / issue #749 while attempting a routine branch update.

The durable lock record for issue #749 matches its work exactly:

  • locked issue 749
  • locked branch fix/issue-749-create-issue-bootstrap — the exact head branch of open PR #750
  • locked worktree branches/issue-749-create-issue-bootstrap — a real, registered Git worktree
  • claimant jcwalker3 / profile prgs-author
  • worktree clean; local head, remote branch head, and PR #750 head all e349839fd739e10af2df9c0f429ac487e8ca8b9d

The only thing that changed is that the recording MCP process exited:

  • recorded session_pid / pid = 48545, which is dead (kill -0 fails, no such process)
  • the current MCP server processes carry different PIDs

Consequences observed:

  1. assess_lock_freshness (issue_lock_store.py) returns status="stale", live=false, reason owner pid 48545 is not alive. Note the lease itself had not expired — work_lease.expires_at was still roughly 55 minutes in the future. The lock is non-live purely because of PID death.
  2. gitea_update_pr_branch_by_merge requires a live author lock via verify_lock_for_mutation, and correctly fails closed.
  3. gitea_lock_issue cannot recover the lock from the existing PR worktree. assess_issue_lock_worktree (issue_lock_worktree.py) rejects the worktree because base_equivalent is false, demanding that the worktree HEAD equal a base branch (master/main/dev) SHA before the lock may be taken.

Point 3 is the structural part of the defect: an existing PR branch is necessarily ahead of its base, so it can never be base-equivalent. The branch above is 2 commits ahead of and 1 behind master. Any branch with work on it fails this check by construction, which makes normal re-lock unreachable after a session restart for every PR that already exists.

Why the existing reclaim path does not cover this

assess_expired_lock_reclaim already sanctions reclaiming a non-live lock when the owner process is dead or the worktree is missing. However, assess_same_issue_lease_conflict only consults it inside an is_lease_expired(...) branch. A lock that is stale-by-dead-PID but whose lease has not yet expired never reaches that path, so the existing recovery affordance does not apply to this case.

Non-sanctioned escapes (explicitly rejected)

The following were considered and refused as recovery, because each launders false evidence through a guard:

  • hand-editing the durable lock JSON
  • declaring a false stacked_base_branch / stacked_base_pr to bypass base-equivalence
  • creating a throwaway base-equivalent worktree to satisfy the check while the real work lives elsewhere
  • waiting for the lease TTL to expire purely to unlock a different code path

Required behavior

Provide a native, truthful recovery path for a dead-session durable author lock when ownership evidence still matches exactly, without weakening the guards that protect brand-new issue claims.

Acceptance criteria

AC1 — Recovery may succeed only when all required evidence agrees:

  • same remote, organization, repository, and issue
  • same configured author identity/profile
  • same locked branch
  • same registered worktree path
  • worktree is clean
  • local head equals remote branch head
  • open PR head equals that same head when a PR exists
  • recorded PID is demonstrably dead
  • no live competing author lock or lease exists
  • no conflicting branch claims the issue
  • no different worktree, branch, head, or author is being substituted

AC2 — The recovery must:

  • atomically rebind or replace the dead session ownership
  • record that recovery occurred after session death
  • record the prior and replacement session identity/PID where safe
  • renew the lease using normal policy

AC3 — The recovery must remain fail-closed for:

  • a live prior PID
  • dirty worktrees
  • local/remote/PR head mismatch
  • foreign branch, identity, worktree, or lease conflicts

AC4 — Scope discipline:

  • recovery must not require an existing PR branch to match its base
  • normal base-equivalence requirements are preserved for brand-new issue claims
  • a native sanctioned MCP path is exposed rather than requiring file edits

AC5 — Downstream acceptance: a recovered lock must satisfy gitea_update_pr_branch_by_merge, existing PR mutation ownership verification, and subsequent author handoff and PR update paths.

AC6 — Evidence: structured recovery evidence is recorded for auditing, without exposing secrets.

AC7 — Regression tests cover positive recovery and every rejection condition above, including: dead PID with exact proof succeeds; live prior PID refused; different author/profile refused; different branch refused; different worktree refused; dirty worktree refused; local head differing from remote refused; PR head differing refused; competing live lock or lease refused; multiple candidate branches refused; malformed or incomplete durable lock refused; new-issue base-equivalence still enforced; recovered lock satisfies update-by-merge ownership checks; recovery record preserves truthful provenance; no manual file seeding required.

References

  • #749 / #750 — the work blocked by this defect; PR #750 could not have its branch updated.
  • #601 — introduced lease adopt/release/expire/abandon and assess_expired_lock_reclaim; its reclaim path is gated behind lease expiry and so does not reach the dead-PID-but-unexpired case.
  • #249 — origin of the issue-lock worktree base-equivalence requirement.
  • #484 — stacked-base opt-in; deliberately not usable as a recovery escape here.
  • #702 — stale runtime binding and orphaned durable lease on daemon crash; adjacent, but did not address author-lock re-acquisition.
  • #438verify_lock_for_mutation ownership re-check consuming lock freshness.
## Summary A durable author issue lock becomes permanently unusable when the MCP session that created it exits, even though every piece of ownership evidence still matches exactly. There is no sanctioned native path to recover it, and the obvious re-lock path is blocked by a guard that a real PR branch can never satisfy. ## Observed failure Reproduced against PR #750 / issue #749 while attempting a routine branch update. The durable lock record for issue #749 matches its work exactly: - locked issue `749` - locked branch `fix/issue-749-create-issue-bootstrap` — the exact head branch of open PR #750 - locked worktree `branches/issue-749-create-issue-bootstrap` — a real, registered Git worktree - claimant `jcwalker3` / profile `prgs-author` - worktree clean; local head, remote branch head, and PR #750 head all `e349839fd739e10af2df9c0f429ac487e8ca8b9d` The only thing that changed is that the recording MCP process exited: - recorded `session_pid` / `pid` = `48545`, which is dead (`kill -0` fails, no such process) - the current MCP server processes carry different PIDs Consequences observed: 1. `assess_lock_freshness` (`issue_lock_store.py`) returns `status="stale"`, `live=false`, reason `owner pid 48545 is not alive`. Note the lease itself had **not** expired — `work_lease.expires_at` was still roughly 55 minutes in the future. The lock is non-live purely because of PID death. 2. `gitea_update_pr_branch_by_merge` requires a live author lock via `verify_lock_for_mutation`, and correctly fails closed. 3. `gitea_lock_issue` cannot recover the lock from the existing PR worktree. `assess_issue_lock_worktree` (`issue_lock_worktree.py`) rejects the worktree because `base_equivalent` is `false`, demanding that the worktree HEAD equal a base branch (`master`/`main`/`dev`) SHA before the lock may be taken. Point 3 is the structural part of the defect: **an existing PR branch is necessarily ahead of its base**, so it can never be base-equivalent. The branch above is 2 commits ahead of and 1 behind master. Any branch with work on it fails this check by construction, which makes normal re-lock unreachable after a session restart for every PR that already exists. ### Why the existing reclaim path does not cover this `assess_expired_lock_reclaim` already sanctions reclaiming a non-live lock when the owner process is dead or the worktree is missing. However, `assess_same_issue_lease_conflict` only consults it inside an `is_lease_expired(...)` branch. A lock that is stale-by-dead-PID but whose lease has **not** yet expired never reaches that path, so the existing recovery affordance does not apply to this case. ### Non-sanctioned escapes (explicitly rejected) The following were considered and refused as recovery, because each launders false evidence through a guard: - hand-editing the durable lock JSON - declaring a false `stacked_base_branch` / `stacked_base_pr` to bypass base-equivalence - creating a throwaway base-equivalent worktree to satisfy the check while the real work lives elsewhere - waiting for the lease TTL to expire purely to unlock a different code path ## Required behavior Provide a native, truthful recovery path for a dead-session durable author lock when ownership evidence still matches exactly, without weakening the guards that protect brand-new issue claims. ## Acceptance criteria **AC1 — Recovery may succeed only when all required evidence agrees:** - same remote, organization, repository, and issue - same configured author identity/profile - same locked branch - same registered worktree path - worktree is clean - local head equals remote branch head - open PR head equals that same head when a PR exists - recorded PID is demonstrably dead - no live competing author lock or lease exists - no conflicting branch claims the issue - no different worktree, branch, head, or author is being substituted **AC2 — The recovery must:** - atomically rebind or replace the dead session ownership - record that recovery occurred after session death - record the prior and replacement session identity/PID where safe - renew the lease using normal policy **AC3 — The recovery must remain fail-closed for:** - a live prior PID - dirty worktrees - local/remote/PR head mismatch - foreign branch, identity, worktree, or lease conflicts **AC4 — Scope discipline:** - recovery must not require an existing PR branch to match its base - normal base-equivalence requirements are preserved for brand-new issue claims - a native sanctioned MCP path is exposed rather than requiring file edits **AC5 — Downstream acceptance:** a recovered lock must satisfy `gitea_update_pr_branch_by_merge`, existing PR mutation ownership verification, and subsequent author handoff and PR update paths. **AC6 — Evidence:** structured recovery evidence is recorded for auditing, without exposing secrets. **AC7 — Regression tests** cover positive recovery and every rejection condition above, including: dead PID with exact proof succeeds; live prior PID refused; different author/profile refused; different branch refused; different worktree refused; dirty worktree refused; local head differing from remote refused; PR head differing refused; competing live lock or lease refused; multiple candidate branches refused; malformed or incomplete durable lock refused; new-issue base-equivalence still enforced; recovered lock satisfies update-by-merge ownership checks; recovery record preserves truthful provenance; no manual file seeding required. ## References - **#749 / #750** — the work blocked by this defect; PR #750 could not have its branch updated. - **#601** — introduced lease adopt/release/expire/abandon and `assess_expired_lock_reclaim`; its reclaim path is gated behind lease expiry and so does not reach the dead-PID-but-unexpired case. - **#249** — origin of the issue-lock worktree base-equivalence requirement. - **#484** — stacked-base opt-in; deliberately not usable as a recovery escape here. - **#702** — stale runtime binding and orphaned durable lease on daemon crash; adjacent, but did not address author-lock re-acquisition. - **#438** — `verify_lock_for_mutation` ownership re-check consuming lock freshness.
jcwalker3 added status:pr-open and removed status:ready labels 2026-07-18 18:33:05 -05:00
Author
Owner

[THREAD STATE LEDGER] Issue #753 — implementation pushed; PR #754 opened by author

What is true now:

  • Issue state: open, labels include status:pr-open
  • PR #754 state: open against master
  • Current head SHA: 3edeba4d7f
  • Server-side decision state: no review verdict exists for PR #754
  • Local verdict/state: author work complete; no verdict prepared by this session
  • Latest known validation: full suite 3498 passed, 6 skipped, 2 failed; both failures reproduce identically on pristine master 0425bf9a43

What changed:

What is blocked:

  • Blocker classification: no blocker

Who/what acts next:

Canonical Issue State

STATE:
Issue #753 is open with an implementation pushed and PR #754 opened against master at head 3edeba4d7f. No review verdict exists.

WHO_IS_NEXT:
reviewer

NEXT_ACTION:
Perform a fresh independent native review of PR #754 pinned at head 3edeba4d7f, checking each acceptance criterion AC1 through AC7 in this issue against the diff.

NEXT_PROMPT:

Review PR #754 in Scaled-Tech-Consulting/Gitea-Tools on remote prgs as an independent reviewer in a fresh session bound to prgs-reviewer. Pin head 3edeba4d7f2ae320fb46f76b0e848c952b3cd9ce. Verify AC1 through AC7 of issue #753: that dead-session recovery requires exact agreement on remote, org, repo, issue, author identity and profile, locked branch with the worktree occupying it, registered worktree path, clean worktree, local head matching remote head and open PR head, a demonstrably dead recorded PID, absence of competing live locks or leases, and absence of ambiguous branch claims; that the waiver suppresses only the base-branch match requirement and never cleanliness; that brand-new issue claims retain the full requirement; that a refused assessment withholds the waiver rather than raising, so the pre-existing guard still fails closed; and that recovery provenance is recorded without secrets. Do not alter PR #750 or PR #746.

WHAT_HAPPENED:
The author session verified the defect against the live durable lock for issue #749: the lock matched issue #749, branch fix/issue-749-create-issue-bootstrap, and the registered worktree branches/issue-749-create-issue-bootstrap, with a clean worktree and local head, remote head and PR #750 head all e349839fd7, while recorded pid 48545 was dead and the lease still had roughly 55 minutes remaining. Issue #753 was then created, its lock acquired on a fresh worktree at master, the recovery assessor implemented with regression tests, and PR #754 opened.

WHY:
A durable author lock records the owning MCP session PID. When that process exits the lock is classified stale even inside its lease TTL, so ownership checks fail closed, and re-locking is unreachable because the worktree gate requires the branch to match its base while any branch carrying work is ahead of its base. This change supplies a native recovery path gated on exact durable ownership evidence.

RELATED_PRS:
PR #754 implements this issue. PR #750 and issue #749 supplied the reproduction and were not modified. PR #746 was not modified.

BLOCKERS:
no blocker

VALIDATION:
New suite tests/test_issue_753_dead_pid_lock_recovery.py 33 passed. Issue-lock, adoption, store, provenance, registration, duplicate-gate, worktree and create-issue-guard suites 120 passed. MCP server, commit payloads, handoff ledger, PR ownership and branch cleanup suites 286 passed. Full suite 3498 passed, 6 skipped, 2 failed. The 2 failures are test_issue_702_review_findings_f1_f6.py::TestF1RecoveryBeforeTerminalProbe::test_removed_worktree_recovers_before_probe and test_reconciler_supersession_close.py::TestReconcilerSupersessionMcpTool::test_tool_posts_comment_and_closes_superseded_pr_issue, both reproduced identically in a throwaway detached worktree at pristine master 0425bf9a43, so they are pre-existing. git diff --check clean.

LAST_UPDATED_BY:
author

[THREAD STATE LEDGER] Issue #753 — implementation pushed; PR #754 opened by author What is true now: - Issue state: open, labels include status:pr-open - PR #754 state: open against master - Current head SHA: 3edeba4d7f2ae320fb46f76b0e848c952b3cd9ce - Server-side decision state: no review verdict exists for PR #754 - Local verdict/state: author work complete; no verdict prepared by this session - Latest known validation: full suite 3498 passed, 6 skipped, 2 failed; both failures reproduce identically on pristine master 0425bf9a4325a25b72739fba81604463e3e5e363 What changed: - Created issue #753 from a temporary registered pre-issue worktree, which was then removed - Acquired the durable author lock for #753 on branch fix/issue-753-dead-pid-author-lock-recovery - Added issue_lock_recovery.py, wired it into gitea_lock_issue, threaded recovery_sanctioned through assess_issue_lock_worktree, exposed branch_carries_issue_marker - Committed 3edeba4d7f2ae320fb46f76b0e848c952b3cd9ce and pushed the branch - Opened PR #754 What is blocked: - Blocker classification: no blocker Who/what acts next: - Next actor: reviewer - Required action: perform a fresh independent native review of PR #754 at head 3edeba4d7f2ae320fb46f76b0e848c952b3cd9ce - Do not do: reuse this author session, and do not alter PR #750 or PR #746 - Resume from: PR #754 diff and the acceptance criteria in this issue ## Canonical Issue State STATE: Issue #753 is open with an implementation pushed and PR #754 opened against master at head 3edeba4d7f2ae320fb46f76b0e848c952b3cd9ce. No review verdict exists. WHO_IS_NEXT: reviewer NEXT_ACTION: Perform a fresh independent native review of PR #754 pinned at head 3edeba4d7f2ae320fb46f76b0e848c952b3cd9ce, checking each acceptance criterion AC1 through AC7 in this issue against the diff. NEXT_PROMPT: ```text Review PR #754 in Scaled-Tech-Consulting/Gitea-Tools on remote prgs as an independent reviewer in a fresh session bound to prgs-reviewer. Pin head 3edeba4d7f2ae320fb46f76b0e848c952b3cd9ce. Verify AC1 through AC7 of issue #753: that dead-session recovery requires exact agreement on remote, org, repo, issue, author identity and profile, locked branch with the worktree occupying it, registered worktree path, clean worktree, local head matching remote head and open PR head, a demonstrably dead recorded PID, absence of competing live locks or leases, and absence of ambiguous branch claims; that the waiver suppresses only the base-branch match requirement and never cleanliness; that brand-new issue claims retain the full requirement; that a refused assessment withholds the waiver rather than raising, so the pre-existing guard still fails closed; and that recovery provenance is recorded without secrets. Do not alter PR #750 or PR #746. ``` WHAT_HAPPENED: The author session verified the defect against the live durable lock for issue #749: the lock matched issue #749, branch fix/issue-749-create-issue-bootstrap, and the registered worktree branches/issue-749-create-issue-bootstrap, with a clean worktree and local head, remote head and PR #750 head all e349839fd739e10af2df9c0f429ac487e8ca8b9d, while recorded pid 48545 was dead and the lease still had roughly 55 minutes remaining. Issue #753 was then created, its lock acquired on a fresh worktree at master, the recovery assessor implemented with regression tests, and PR #754 opened. WHY: A durable author lock records the owning MCP session PID. When that process exits the lock is classified stale even inside its lease TTL, so ownership checks fail closed, and re-locking is unreachable because the worktree gate requires the branch to match its base while any branch carrying work is ahead of its base. This change supplies a native recovery path gated on exact durable ownership evidence. RELATED_PRS: PR #754 implements this issue. PR #750 and issue #749 supplied the reproduction and were not modified. PR #746 was not modified. BLOCKERS: no blocker VALIDATION: New suite tests/test_issue_753_dead_pid_lock_recovery.py 33 passed. Issue-lock, adoption, store, provenance, registration, duplicate-gate, worktree and create-issue-guard suites 120 passed. MCP server, commit payloads, handoff ledger, PR ownership and branch cleanup suites 286 passed. Full suite 3498 passed, 6 skipped, 2 failed. The 2 failures are test_issue_702_review_findings_f1_f6.py::TestF1RecoveryBeforeTerminalProbe::test_removed_worktree_recovers_before_probe and test_reconciler_supersession_close.py::TestReconcilerSupersessionMcpTool::test_tool_posts_comment_and_closes_superseded_pr_issue, both reproduced identically in a throwaway detached worktree at pristine master 0425bf9a4325a25b72739fba81604463e3e5e363, so they are pre-existing. git diff --check clean. LAST_UPDATED_BY: author
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Scaled-Tech-Consulting/Gitea-Tools#753