An unhandled daemon crash skips teardown: the durable reviewer lease
comment survives, the in-memory session lease dies, and the
auto-reconnected daemon inherits a stale GITEA_ACTIVE_WORKTREE from its
parent environment (PR #701 / branches/review-pr-654 incident).
AC2 — safe clear/re-bind of stale bindings:
- new stale_binding_recovery.py: classifies the active env binding
(corroborated / unverified_inherited / provably_stale_missing_path /
superseded_by_session_lease) and produces a fail-closed recovery plan;
only provably stale bindings are clear-eligible
- gitea_resolve_task_capability and daemon boot run the sanctioned
recovery (durable audit via session state); runtime context surfaces
the classification read-only
- namespace_workspace_binding.resolve_namespace_workspace(verify_paths=…)
demotes env-bound worktrees whose path no longer exists; mutation and
runtime-context resolution always verify
AC3 — recoverable lease cleanup after unexpected exit:
- durable session-lease shadow (KIND_REVIEWER_SESSION_LEASE) written on
sanctioned record/heartbeat, removed on sanctioned clear; a crash
leaves provable orphan evidence (owner pid + session id)
- assess_crashed_session_lease_orphan derives tri-state
owner_process_alive: only a dead recorded owner pid proves exit; PID
liveness is never ownership proof
- diagnose/cleanup wrappers consume the shadow instead of passing
owner_process_alive=None
- new classification orphaned_expired_superseded_head: an expired
superseded-head lease with no terminal review stops being a permanent
ambiguous/wait; post-expiry it unlocks fresh acquisition, and guarded
cleanup becomes eligible only with owner-exit evidence + clean/absent
worktree + controller authorization + confirmation. Pre-expiry
behavior is unchanged (fail-closed wait, no steal).
Validation: tests/test_issue_702_stale_binding_lease_recovery.py (29
tests covering lost in-session leases, old-head leases, runtime/worktree
mismatch, managed reconnect, safe expiry); full suite 2665 passed,
6 skipped, 161 subtests.
Co-Authored-By: Claude Fable 5 <[email protected]>
Add fail-closed assessment and MCP tool for expired/superseded-head
comment-backed reviewer leases that lack a control-plane lease_id.
- assess_obsolete_reviewer_comment_lease_cleanup eligibility matrix
- diagnose classifications: foreign_active_current_head,
foreign_expired_current_head, foreign_completed_superseded_head,
foreign_expired_superseded_head, orphaned_owner_missing,
ambiguous_conflicting_evidence
- gitea_cleanup_obsolete_reviewer_comment_lease with confirmation
CLEANUP OBSOLETE REVIEWER LEASE <n> and controller_recovery_authorized
- preserve audit history via append-only phase=released marker
- never repoint, transfer validation, use PID ownership, or steal
active current-head foreign leases
- regression for PR #688 lease comment 10749 after recorded expiry
find_active_reviewer_lease walked past terminal markers and re-armed
older claimed leases after gitea_release_reviewer_pr_lease. Treat the
newest marker as authoritative (append-only ledger).
Closes#577
Replace manual in-process _SESSION_LEASE seeding with an auditable
gitea_adopt_merger_pr_lease tool that posts durable adoption proof and
records sanctioned session provenance. Mutation gates now reject bare
record_session_lease calls without provenance from acquire/adopt/heartbeat.
Closes#536
Refuse merge-oriented reviewer-lease acquisition/adoption on an already
merged/closed PR, and add a read-first cleanup path for a lingering moot lease.
- reviewer_pr_lease.assess_acquire_lease: new pr_merged_or_closed flag; fail
closed with a post_merge_moot reason and no lease body when the PR already
merged/closed.
- reviewer_pr_lease.assess_post_merge_moot_lease: pure assessment. Only treats a
lease as moot when the PR is merged/closed; never proposes touching an active
lease on an open PR; provides a terminal phase:released (blocker:
post-merge-moot) body to neutralise the moot lease append-only; idempotent
once the released marker is the newest entry.
- gitea_acquire_reviewer_pr_lease: fetch live PR state, pass the flag, surface
post_merge_moot on refusal (no comment posted).
- gitea_cleanup_post_merge_moot_lease: new read-first tool. Reports merged
state, merge_commit_sha, linked-issue closure, lease-moot, cleanup
performed/skipped, and no_merge_or_adoption. apply=True posts the released
marker only when merged/closed with an active lease; refuses on open PRs.
- tests/test_post_merge_moot_lease.py: 10 tests covering acquire refusal on
merged PR (no post), safe/idempotent cleanup, and open-PR foreign-lease
protection.
No relaxation of #407 reviewer-lease or #16 gated-merge gates for open PRs.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Structured PR-thread leases with acquire/heartbeat MCP tools and
mutation-time enforcement so reviewers cannot race the same PR queue.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>