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:
@@ -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 []),
|
||||
|
||||
Reference in New Issue
Block a user