fix(session): keep KIND_DECISION_LOCK durable past generic 4h TTL (Closes #720)

Terminal review-decision ledgers are recovery-critical provenance, not
disposable session cache. A generic four-hour TTL previously made
fresh_review_on_current_head_allowed unreachable after age expiry on open
PRs (PR #616 / review 443 reproduction).

- Classify KIND_DECISION_LOCK as RECOVERY_CRITICAL so load/mark_final work
  after >4h without hand-editing session-state files
- Stamp kind + recovery_critical on save for compatibility
- Add inspect_state_envelope so assessment reports on-disk evidence instead
  of silent "no lock" when TTL would hide non-critical kinds
- Surface disk_inspect on stale decision-lock cleanup assessment

Preserves same-head #332 hard-stop, #620 head-scoped fresh review, #594
moot cleanup, and #709 irrecoverable authorization (no ordinary-profile
permission grant).
This commit is contained in:
2026-07-16 15:47:52 -04:00
parent 77746b08fc
commit 80f59b334e
3 changed files with 668 additions and 3 deletions
+44
View File
@@ -3344,6 +3344,11 @@ def _save_review_decision_lock(data):
)
payload["session_profile_lock"] = binding["session_profile_lock"]
payload["profile_identity"] = binding["profile_identity"]
# #720: durable decision locks are recovery-critical terminal provenance,
# not generic TTL session cache. Stamp kind + recovery_critical for
# pre-existing readers and identity_match_reasons flag-based exempt.
payload["kind"] = mcp_session_state.KIND_DECISION_LOCK
payload["recovery_critical"] = True
if binding.get("remote") and not payload.get("remote"):
payload["remote"] = binding["remote"]
# #695 AC6: stamp native transport provenance on durable decision locks.
@@ -5027,6 +5032,12 @@ def gitea_cleanup_stale_review_decision_lock(
binding = _decision_lock_binding()
active_identity = binding.get("profile_identity")
lock = _load_review_decision_lock()
# #720: when normal load yields no lock, inspect disk so assessment does not
# silently report "absent" while an expired/non-critical envelope remains.
disk_inspect = mcp_session_state.inspect_state_envelope(
kind=mcp_session_state.KIND_DECISION_LOCK,
profile_identity=active_identity,
)
last = stale_review_decision_lock.last_terminal_mutation(lock)
pr_live = None
pr_lookup_error = None
@@ -5048,6 +5059,26 @@ def gitea_cleanup_stale_review_decision_lock(
pr_lookup_error=pr_lookup_error,
active_profile_identity=active_identity,
)
if lock is None and disk_inspect.get("on_disk"):
assessment = dict(assessment)
assessment["reasons"] = list(assessment.get("reasons") or []) + [
"decision-lock file is present on disk but not loadable via normal "
f"TTL/identity gates: {disk_inspect.get('summary')} "
"(do not rm session-state files; #720)"
]
assessment["disk_inspect"] = {
k: disk_inspect.get(k)
for k in (
"on_disk",
"has_payload",
"age_hours",
"age_exceeds_default_ttl",
"recovery_critical",
"ttl_exempt",
"would_ttl_reject",
"summary",
)
}
# Optional pin: refuse apply against a different terminal PR than expected.
if (
@@ -5102,6 +5133,19 @@ def gitea_cleanup_stale_review_decision_lock(
"pr_merged_or_closed": assessment.get("pr_merged_or_closed"),
"merge_commit_sha": assessment.get("merge_commit_sha"),
"lock_summary": assessment.get("lock_summary"),
"disk_inspect": assessment.get("disk_inspect") or {
k: disk_inspect.get(k)
for k in (
"on_disk",
"has_payload",
"age_hours",
"age_exceeds_default_ttl",
"recovery_critical",
"ttl_exempt",
"would_ttl_reject",
"summary",
)
},
"audit": audit,
"audit_comment_id": None,
"reasons": list(assessment.get("reasons") or []),