fix(mcp): let a sanctioned recovery keep its own owning PR (Closes #755) #756
Merged
sysadmin
merged 1 commits from 2026-07-18 21:08:35 -05:00
fix/issue-755-owning-pr-recovery into master
Labels
Clear labels
allocator
anti-stomp
architecture
bug
chore
codex
concurrency
contamination
control-plane
dashboard
database
design
documentation
enhancement
gitea
glitchtip
important
incident
incident-bridge
integration
jenkins
labels
leases
mcp
mcp-health
mcp-menu
multi-project
mutating
nice-to-have
observability
portability
preflight
protected-branch
queue
read-only
reconnect
recovery
refactor
release
reliability
resumable-review
reviewer
roadmap
safety
security
self-hosted
sentry
stale-runtime
status:blocked
status:in-progress
status:pr-open
status:ready
terminal-lock
testing
tracker
type:bug
type:feature
type:feature
type:guardrail
visibility
workflow
workflow-hardening
workflow-hardening
Controller-owned work allocator
Prevent concurrent LLM session stomping
Architecture / structural design
OpenAI Codex client / workflow session surface
Concurrent session safety
Workflow or session contamination incident
MCP control-plane coordination and allocation authority
MCP operational dashboard/queue view
Internal coordination storage (SQLite/Postgres)
Design / investigation, no implementation
Docs / runbooks
New feature or improvement
Gitea MCP workflow
GlitchTip integration
Operational or process incident requiring durable audit trail
Sentry-to-Gitea incident bridging
Integration testing
Jenkins integration
Label taxonomy management
Lease adopt/release/expire lifecycle
MCP server / tooling
MCP namespace and runtime health
MCP menu surface
Work spanning multiple monitoring projects or Gitea repos
Mutating action; requires gating
Observability, metrics, traces, error reporting
Cross-platform / portability
Shared preflight gates before mutation
Protected branch / stable-branch policy concern
Work queue visibility and allocation
Read-only, no mutation
MCP client reconnect/reload recovery path
Recovery paths for stale/foreign leases
Code refactor / restructure
Release / versioning
Reliability / failure handling
Persist and resume prepared review verdicts across sessions
Reviewer workflow tooling
Roadmap / umbrella issue
Safety rails and fail-closed mutation guards
Security / trust boundary
Self-hosted infrastructure integration
Sentry error monitoring integration
Stale backend daemon / runtime-vs-master parity failures
Issue is blocked
Issue is being worked on
Issue has an open pull request
Issue is ready for work
Terminal review lock (#332) path
Tests / test coverage
Issue tracker hygiene / meta
Bug or defect
Feature or enhancement
Feature or enhancement
Safety gate or guardrail
Workflow state visibility for LLMs/operators
Cross-tool workflow
LLM workflow coordination hardening
LLM workflow coordination hardening
No labels
Milestone
No items
No Milestone
Projects
Clear projects
No projects
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: Scaled-Tech-Consulting/Gitea-Tools#756
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #755.
Base:
08f67007c5984d239e1f52684e9e47543ff41785Head:
f858c1d1b20f403a80b57fe524a774687c38bc8bProblem
#753 shipped the dead-session author-lock recovery assessor, but no sanctioned recovery could reach the lock write.
A dead-session lock is, by construction, a lock for work that already has an open PR. The #400 duplicate-work gate blocked unconditionally on any linked open PR, so
gitea_lock_issuecomputedrecovery_sanctionedand then discarded it one gate later:open PR #750 already covers issue #749 (fail closed)Because the recovered lock was never persisted, it stayed non-live, so
_prove_author_ownership_for_prfound no live author lock andgitea_update_pr_branch_by_mergealso failed closed. Both blockers had one cause, so only the first is fixed here.Live reproduction at the time of filing: issue #749, owning PR #750, branch
fix/issue-749-create-issue-bootstrap, heade349839f, clean registered worktree, local == remote == PR head, dead recorded pid, no competing lock/lease/branch, assessor returningRECOVERY_SANCTIONED.Design
issue_lock_recovery.owning_pr_recovery_evidencedistils a granted assessment into the minimum evidence the duplicate gate needs: issue, branch, owning PR number, head.It returns
Noneunless the outcome isRECOVERY_SANCTIONEDand the assessment's own evidence agrees that local head == remote head == PR head. Every field is copied from evidence the assessor built out of durable lock state plus live git/Gitea observation, so a partial or hand-built assessment cannot authorize anything. There is no new caller-facing input anywhere — no tool signature changed.The duplicate gate re-checks that evidence against the live PR list it was handed, independently:
Only that exact self-owned PR is exempt. Any mismatch keeps the pre-existing fail-closed behavior and adds a diagnostic naming which element disagreed.
Untouched: the competing-branch arm, the claim-lease arms, and the
PHASE_COMMIT/PHASE_PUSH/PHASE_CREATE_PRrechecks. With no evidence supplied, the gate behaves exactly as before — the default isNone, so every existing caller is unchanged.Ownership proof needed no change (AC7): once the recovered lock persists under the live session pid,
_prove_author_ownership_for_prmatches it by its existing durable-lock path. AC5's renewed live lease anddead_session_recoveryprovenance are written by the existing #753 code, which this change simply lets execute.Out of scope
The
PHASE_COMMIT/PHASE_PUSH/PHASE_CREATE_PRduplicate rechecks still block on a linked open PR for every author, recovered or not. That is pre-existing #400 behavior outside lock recovery, and #755 does not cover it. Flagging it rather than widening this fix.Files changed
issue_lock_recovery.pyowning_pr_recovery_evidencepure derivationissue_work_duplicate_gate.py_pr_links_issue/_all_linked_open_prs/_assess_owning_pr_exemption; new keyword-onlyrecovered_owning_prgitea_mcp_server.pyrecovery_sanctioned; thread it through_assess_issue_duplicate_gatetests/test_issue_755_owning_pr_recovery.pyResolver / mutation pairing
lock_issue/push_branch/create_prpush_branch(push),create_pr(PR)gitea.branch.push+gitea.pr.create, authorTests
tests/test_issue_755_owning_pr_recovery.py(new, 31 tests) drives the realmcp_server.gitea_lock_issuehandler, not only the pure assessor (AC8):dead_session_recoveryprovenance (prior pid, replacement pid,prior_pid_alive=false, PR number, branch, identity);_prove_author_ownership_for_pr(AC6);Load-bearing check: neutralizing
owning_pr_recovery_evidenceregresses all three success tests to the exact production error above.Results:
tests/— 3529 passed, 6 skipped, 2 failed, 365 subtestsThe 2 failures are pre-existing, reproduced on a pristine detached worktree at
08f67007c5984d239e1f52684e9e47543ff41785(3498 passed, 6 skipped, 2 failed — identical set):tests/test_issue_702_review_findings_f1_f6.py::TestF1RecoveryBeforeTerminalProbe::test_removed_worktree_recovers_before_probetests/test_reconciler_supersession_close.py::TestReconcilerSupersessionMcpTool::test_tool_posts_comment_and_closes_superseded_pr_issuegit diff --check— clean.Safety
Canonical Issue State
STATE: PR #756 open at head f858c1d1b20f403a80b57fe524a774687c38bc8b; author implementation pushed; no review verdict recorded on Gitea at any head
WHO_IS_NEXT: REVIEWER
NEXT_ACTION: Run a fresh independent reviewer session against PR #756 pinned to exact head
f858c1d1b2, acquiring a reviewer PR lease before any review mutationNEXT_PROMPT:
WHAT_HAPPENED: Acquired the author issue lock for #755 from worktree branches/issue-755-owning-pr-recovery; implemented owning_pr_recovery_evidence in issue_lock_recovery.py; taught issue_work_duplicate_gate.py to exempt only the exact self-owned open PR when that server-derived evidence matches the live PR list; threaded the evidence through _assess_issue_duplicate_gate in gitea_mcp_server.py; added tests/test_issue_755_owning_pr_recovery.py (31 tests) driving the real MCP handler; committed
f858c1d1b2, pushed the branch, and opened PR #756. Duplicate-work outcome: duplicate work not prevented — the single linked open PR is this session's own PR #756 for issue #755.WHY: #753 shipped the recovery assessor, but a dead-session lock always belongs to work that already has an open PR, and the #400 duplicate-work gate rejected any linked open PR unconditionally. gitea_lock_issue therefore computed recovery_sanctioned and discarded it one gate later with "open PR #750 already covers issue #749 (fail closed)", leaving the recovered lock unpersisted and non-live, which in turn made gitea_update_pr_branch_by_merge fail its ownership proof.
RELATED_PRS: #756 (this PR, open); #754 / issue #753 (parent implementation, landed on master at
08f67007c5); #750 / issue #749 (the live reproduction, open, untouched by this session); #746 (open, untouched by this session)BLOCKERS: none for PR #756. Dependency note: dead-session recovery for issue #749 / PR #750 stays unavailable until PR #756 lands on master through sanctioned merge tooling and the MCP namespaces restart onto that master SHA.
VALIDATION: Focused suites (755, 753, both duplicate gates, lock registration, provenance) 102 passed; lock/ownership/worktree/handoff suites 172 passed with 16 subtests; full suite tests/ 3529 passed, 6 skipped, 2 failed, 365 subtests; pristine baseline at
08f67007c5gave 3498 passed, 6 skipped, 2 failed with the identical failure set (test_issue_702_review_findings_f1_f6 F1 recovery-before-probe and test_reconciler_supersession_close org/repo forwarding), so both are pre-existing; neutralizing owning_pr_recovery_evidence regresses all three success tests to the exact production error, proving the coverage is load-bearing; git diff --check cleanLAST_UPDATED_BY: jcwalker3 / prgs-author (author session)
[THREAD STATE LEDGER] PR #756 — author implementation pushed, no review verdict exists
What is true now:
f858c1d1b208f67007c5What changed:
What is blocked:
Who/what acts next:
f858c1d1b2, acquiring a reviewer PR lease firstrepo: Scaled-Tech-Consulting/Gitea-Tools
pr: #756
issue: #755
reviewer_identity: sysadmin
profile: prgs-reviewer
session_id: 49447-f17743b05c18
worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr-756
phase: claimed
candidate_head:
f858c1d1b2target_branch: master
target_branch_sha:
08f67007c5last_activity: 2026-07-19T01:58:21Z
expires_at: 2026-07-19T02:08:21Z
blocker: none
Canonical PR State
STATE: PR #756 open at head f858c1d1b20f403a80b57fe524a774687c38bc8b; independent Grok reviewer APPROVED at this exact head; no merge performed
WHO_IS_NEXT: MERGER
NEXT_ACTION: Start a fresh Grok merger session for PR #756 pinned to exact head f858c1d1b20f403a80b57fe524a774687c38bc8b; adopt or acquire merger lease and merge only if all merger gates pass
NEXT_PROMPT:
WHAT_HAPPENED: Independent Grok reviewer session (sysadmin / prgs-reviewer) acquired reviewer lease comment 12718 against worktree branches/review-pr-756, inspected the full diff for #755, validated focused/affected suites (200 passed including 31 new #755 tests), and submitted formal APPROVE pinned to
f858c1d1b2. No merge. PR #750/#746 untouched.WHY: Server-derived owning-PR recovery evidence correctly exempts only the exact self-owned open PR from the #400 duplicate-work gate after RECOVERY_SANCTIONED; mutation path revalidates live PR number/branch/head; competing/mismatched/forged/partial paths fail closed; ordinary duplicate and base-equivalence behavior preserved; recovered lock proves ownership for update-by-merge.
ISSUE: #755
HEAD_SHA:
f858c1d1b2REVIEW_STATUS: APPROVED at exact head
f858c1d1b2by sysadmin (prgs-reviewer)MERGE_READY: true
BLOCKERS: none for PR #756. Dependency note only: #749/#750 recovery remains unavailable until this PR lands on master and MCP namespaces restart onto that master SHA.
VALIDATION: test_issue_755_owning_pr_recovery 31 passed; expanded focused/affected suites (#753 recovery, both duplicate gates, lock adoption/store/worktree/provenance, lock MCP registration, PR ownership, worktree ownership, pr sync) 200 passed; git diff --check clean; py_compile clean; worktree clean at reviewed head; full suite not re-run (author pre-existing 2 failures on pristine master accepted under session rules)
NATIVE_REVIEW_PROOF: transport=native_mcp; entrypoint=mcp_server; token_fingerprint=0d54d79a6d57405b; pid=49447; mode=production
LAST_UPDATED_BY: sysadmin / prgs-reviewer (Grok independent review session)
Independent code review summary
Files: issue_lock_recovery.py, issue_work_duplicate_gate.py, gitea_mcp_server.py, tests/test_issue_755_owning_pr_recovery.py
AC1–AC10: all satisfied. Exemption is server-derived only (owning_pr_recovery_evidence); requires RECOVERY_SANCTIONED plus local==remote==PR head; gate rechecks exactly one linked open PR and matching number/branch/head/issue/locked-branch against the live PR list. Competing PR, multi-PR, wrong head/branch/worktree, dirty tree, live prior PID, foreign issue, forged/partial assessment evidence all fail closed. Default recovered_owning_pr=None preserves pre-#755 callers. Recovered lock is live with truthful dead_session_recovery provenance and satisfies _prove_author_ownership_for_pr. No #749/#750 special cases; no MCP tool signature bypass.
Race/trust boundary: evidence can age between assessment and mutation, but the gate re-lists and revalidates; head move or extra linked PR refuses exemption rather than adopting stale proof.
Verdict: APPROVE
repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #756
issue: #755
reviewer_identity: sysadmin
profile: prgs-reviewer
session_id: 49447-f17743b05c18
worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr-756
phase: released
candidate_head:
f858c1d1b2target_branch: master
target_branch_sha:
08f67007c5last_activity: 2026-07-19T02:01:20Z
expires_at: 2026-07-19T02:11:20Z
blocker: manual-release
repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #756
issue: #755
reviewer_identity: sysadmin
profile: prgs-merger
session_id: 53218-0e7a3db4efab
worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/merge-pr-756
phase: claimed
candidate_head:
f858c1d1b2target_branch: master
target_branch_sha:
08f67007c5last_activity: 2026-07-19T02:08:20Z
expires_at: 2026-07-19T02:18:20Z
blocker: none
Stale #332 review-decision lock cleanup (#594)
Status: APPLIED
sysadminprgs-merger2026-07-19T02:08:35.838401+00:00approveon PR fix(mcp): let a sanctioned recovery keep its own owning PR (Closes #755) (#756)closed(merged=True)043df0f7df220ad67ac1c3fefdcf3897dad9478b1prgs-reviewerManual deletion of session-state files is not the workflow.
This path only clears a lock when the referenced PR is merged/closed.