fix(author): refresh durable linked-issue lock head after update_pr_branch_by_merge; recover merge-sync-drifted dead-session locks #871

Closed
opened 2026-07-24 01:31:52 -05:00 by jcwalker3 · 0 comments
Owner

Summary

gitea_update_pr_branch_by_merge advances a PR's remote head (native Gitea POST /pulls/{n}/update?style=merge) but never advances the linked durable issue lock's recorded head (head_sha). After a subsequent owning-session death, the drifted lock can no longer be recovered by gitea_lock_issue, and no further synchronization is possible — the PR is stranded behind master.

This is a control-plane lifecycle defect, not a source defect in any single PR.

Confirmed production reproduction (do NOT mutate as part of this fix)

  • Blocked PR: #866, linked issue #855, branch fix/issue-855-pr-scoped-merged-cleanup.
  • Original implementation head: 24c52abf6b80e7ed562294371eee9fe8620b686e.
  • First synchronization advanced the remote PR head to 499b87c48218cf03a3b1f34a5d8a17549d820564.
  • Current master needing a second sync: 89657a06c4cc8d73917e4436e326d3755cff0a40.
  • Durable Issue #855 lock still records 24c52ab…; local author worktree stayed clean at 24c52ab….
  • Owning PID is dead.
  • gitea_lock_issue dead-session recovery refuses: the recorded/local head (24c52ab) is an ancestor of the live PR head (499b87c), i.e. the remote moved forward via merge-sync while the recorded head did not. The existing published-owning-PR recovery only proves the reverse relation (local equal to, or a strict descendant of, the remote head, #753/#768). No relation covers "remote advanced past the recorded head via a sanctioned merge-based branch sync."
  • gitea_update_pr_branch_by_merge correctly refuses a new sync because no live author issue lock can be proven.
  • Review #537 remains REQUEST_CHANGES with stale base as its only blocker.

Root cause (with file:line)

  1. Write-side gapgitea_mcp_server.py gitea_update_pr_branch_by_merge (~L18875-18928): after the successful remote update and new_head re-read, it returns success/performed=True and invalidates approvals/leases (pr_sync_status.assess_post_update_head_transition) but never refreshes the durable linked-issue lock head_sha from the exact former head to new_head. The lock's recorded head is left stale.
  2. Read-side gapissue_lock_recovery.assess_dead_session_lock_recovery published-owning-PR path (issue_lock_recovery.py L454-476) and its ancestry probe in _evaluate_issue_lock_recovery (gitea_mcp_server.py L2440-2446) only observe remote-is-ancestor-of-local. There is no relation, and no ancestry probe, for local/recorded-is-ancestor-of-remote via merge-sync.

Required behavior

A. Write-side durable-lock head refresh (prevents future drift).

  • On a successful sync, refresh the durable linked-issue lock's recorded head from the exact expected PR head to the complete resulting PR head.
  • Preserve and verify repository, issue, PR, branch, worktree, identity, profile, role, and session ownership.
  • Use compare-and-swap / exact-old-head protection (never overwrite a lock whose provenance or head changed concurrently).
  • Read-after-write must prove the durable lock records the complete new head.
  • A remote-head advance followed by a durable-lock refresh failure must be reported as an explicit partial lifecycle failure — the tool must NOT report a fully successful synchronization.

B. Read-side sanctioned recovery for an already-drifted dead-session published_owning_pr lock.
Recovery may accept an older recorded head ONLY when authoritative evidence proves ALL of:

  • same repository, linked issue, exact PR, and exact branch;
  • the recorded head is a strict ancestor of the live PR head;
  • the live head's provenance is compatible with a sanctioned merge-based branch synchronization;
  • the former owner process/session is dead;
  • the worktree is clean;
  • no live competing claimant, author lock, reviewer lease, merger lease, or conflicting workflow mutation exists;
  • all required remote and local evidence is complete and current.

Unknown, missing, malformed, contradictory, stale, truncated, or unavailable evidence blocks recovery. Arbitrary head mismatch, unrelated commit, rewritten history, rebase, force-push, branch mismatch, dirty worktree, or live foreign session stays protected. Do not weaken exact-head, branch-protection, parity, workspace, identity, role, or mutation-safety gates. Do not authorize manual cache-file editing or out-of-band lock seeding.

Acceptance criteria (regression tests)

  1. First base sync updates the durable lock to the resulting head.
  2. A later master advance permits a second sanctioned sync.
  3. Process death between syncs permits guarded recovery.
  4. The exact shape recorded=24c52ab…, live PR=499b87c… is recoverable only with proven ancestry + merge-sync provenance.
  5. Remote mutation succeeds but durable refresh fails → partial lifecycle failure (never reported as full success).
  6. CAS detects a concurrent lock-head change and fails closed.
  7. Non-ancestor recorded heads fail closed.
  8. Rewritten / force-pushed PR history fails closed.
  9. Wrong issue, PR, branch, repository, identity, profile, role, or session fails closed.
  10. Live or ambiguous owners remain protected.
  11. Dirty worktrees remain protected.
  12. Incomplete inventory / remote-read failure remains protected.
  13. Competing claimants block recovery.
  14. Normal issue locking and unpublished-branch publication remain unchanged.
  15. Author/reviewer/merger/reconciler role boundaries remain unchanged.
  16. gitea_update_pr_branch_by_merge retains exact PR-head and base-head pinning.
  17. No regression to existing dead-session recovery and worktree-bootstrap suites.

Out of scope / protected

Do not mutate PR #866, Issue #855, their branch, worktree, durable lock, reviews, or leases. Do not manually repair #855's durable lock. Use isolated temporary repositories/fixtures for mutation tests.

## Summary `gitea_update_pr_branch_by_merge` advances a PR's **remote** head (native Gitea `POST /pulls/{n}/update?style=merge`) but never advances the linked **durable issue lock's recorded head** (`head_sha`). After a subsequent owning-session death, the drifted lock can no longer be recovered by `gitea_lock_issue`, and no further synchronization is possible — the PR is stranded behind master. This is a **control-plane lifecycle defect**, not a source defect in any single PR. ## Confirmed production reproduction (do NOT mutate as part of this fix) * Blocked PR: **#866**, linked issue **#855**, branch `fix/issue-855-pr-scoped-merged-cleanup`. * Original implementation head: `24c52abf6b80e7ed562294371eee9fe8620b686e`. * First synchronization advanced the **remote** PR head to `499b87c48218cf03a3b1f34a5d8a17549d820564`. * Current master needing a second sync: `89657a06c4cc8d73917e4436e326d3755cff0a40`. * Durable Issue #855 lock still records `24c52ab…`; local author worktree stayed clean at `24c52ab…`. * Owning PID is dead. * `gitea_lock_issue` dead-session recovery refuses: the recorded/local head (`24c52ab`) is an **ancestor** of the live PR head (`499b87c`), i.e. the remote moved *forward* via merge-sync while the recorded head did not. The existing published-owning-PR recovery only proves the reverse relation (local equal to, or a strict descendant of, the remote head, #753/#768). No relation covers "remote advanced past the recorded head via a sanctioned merge-based branch sync." * `gitea_update_pr_branch_by_merge` correctly refuses a *new* sync because no live author issue lock can be proven. * Review #537 remains REQUEST_CHANGES with stale base as its only blocker. ## Root cause (with file:line) 1. **Write-side gap** — `gitea_mcp_server.py` `gitea_update_pr_branch_by_merge` (~L18875-18928): after the successful remote update and `new_head` re-read, it returns `success/performed=True` and invalidates approvals/leases (`pr_sync_status.assess_post_update_head_transition`) but **never refreshes the durable linked-issue lock `head_sha`** from the exact former head to `new_head`. The lock's recorded head is left stale. 2. **Read-side gap** — `issue_lock_recovery.assess_dead_session_lock_recovery` published-owning-PR path (`issue_lock_recovery.py` L454-476) and its ancestry probe in `_evaluate_issue_lock_recovery` (`gitea_mcp_server.py` L2440-2446) only observe *remote-is-ancestor-of-local*. There is no relation, and no ancestry probe, for *local/recorded-is-ancestor-of-remote via merge-sync*. ## Required behavior **A. Write-side durable-lock head refresh (prevents future drift).** * On a successful sync, refresh the durable linked-issue lock's recorded head from the exact expected PR head to the complete resulting PR head. * Preserve and verify repository, issue, PR, branch, worktree, identity, profile, role, and session ownership. * Use compare-and-swap / exact-old-head protection (never overwrite a lock whose provenance or head changed concurrently). * Read-after-write must prove the durable lock records the complete new head. * A remote-head advance followed by a durable-lock refresh failure must be reported as an explicit **partial lifecycle failure** — the tool must NOT report a fully successful synchronization. **B. Read-side sanctioned recovery for an already-drifted dead-session `published_owning_pr` lock.** Recovery may accept an older recorded head ONLY when authoritative evidence proves ALL of: * same repository, linked issue, exact PR, and exact branch; * the recorded head is a strict **ancestor** of the live PR head; * the live head's provenance is compatible with a sanctioned merge-based branch synchronization; * the former owner process/session is dead; * the worktree is clean; * no live competing claimant, author lock, reviewer lease, merger lease, or conflicting workflow mutation exists; * all required remote and local evidence is complete and current. Unknown, missing, malformed, contradictory, stale, truncated, or unavailable evidence blocks recovery. Arbitrary head mismatch, unrelated commit, rewritten history, rebase, force-push, branch mismatch, dirty worktree, or live foreign session stays protected. Do not weaken exact-head, branch-protection, parity, workspace, identity, role, or mutation-safety gates. Do not authorize manual cache-file editing or out-of-band lock seeding. ## Acceptance criteria (regression tests) 1. First base sync updates the durable lock to the resulting head. 2. A later master advance permits a second sanctioned sync. 3. Process death between syncs permits guarded recovery. 4. The exact shape `recorded=24c52ab…`, live PR=`499b87c…` is recoverable only with proven ancestry + merge-sync provenance. 5. Remote mutation succeeds but durable refresh fails → partial lifecycle failure (never reported as full success). 6. CAS detects a concurrent lock-head change and fails closed. 7. Non-ancestor recorded heads fail closed. 8. Rewritten / force-pushed PR history fails closed. 9. Wrong issue, PR, branch, repository, identity, profile, role, or session fails closed. 10. Live or ambiguous owners remain protected. 11. Dirty worktrees remain protected. 12. Incomplete inventory / remote-read failure remains protected. 13. Competing claimants block recovery. 14. Normal issue locking and unpublished-branch publication remain unchanged. 15. Author/reviewer/merger/reconciler role boundaries remain unchanged. 16. `gitea_update_pr_branch_by_merge` retains exact PR-head and base-head pinning. 17. No regression to existing dead-session recovery and worktree-bootstrap suites. ## Out of scope / protected Do not mutate PR #866, Issue #855, their branch, worktree, durable lock, reviews, or leases. Do not manually repair #855's durable lock. Use isolated temporary repositories/fixtures for mutation tests.
jcwalker3 added the mcpworkflow-hardeningrecoverysafetytype:bug labels 2026-07-24 01:31:53 -05:00
jcwalker3 added the status:pr-open label 2026-07-24 02:09:04 -05:00
sysadmin removed the status:pr-open label 2026-07-24 03:04:26 -05:00
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#871