fix(author): bootstrap recovery for dirty orphaned issue worktrees (#860)
Add an explicit recovery operation for same-claimant dirty registered worktrees under malformed PID-less durable locks, with crash-safe journals, dirty byte preservation, path-level conflict detection, and live session binding. PID-less locks are never treated as live merely because expiry is absent. Closes #860 Co-Authored-By: Grok 4.5 (xAI) <[email protected]>
This commit is contained in:
+34
-3
@@ -380,7 +380,16 @@ def assess_lock_freshness(
|
||||
pid = lock_data.get("session_pid")
|
||||
if pid is None:
|
||||
pid = lock_data.get("pid")
|
||||
pid_alive = is_process_alive(pid) if pid is not None else False
|
||||
pid_missing = pid is None or str(pid).strip() == ""
|
||||
try:
|
||||
pid_int = int(pid) if not pid_missing else None
|
||||
if pid_int is not None and pid_int <= 0:
|
||||
pid_missing = True
|
||||
pid_int = None
|
||||
except (TypeError, ValueError):
|
||||
pid_missing = True
|
||||
pid_int = None
|
||||
pid_alive = is_process_alive(pid_int) if pid_int is not None else False
|
||||
|
||||
if expires_at and expires_at <= current:
|
||||
return {
|
||||
@@ -389,15 +398,36 @@ def assess_lock_freshness(
|
||||
"stale": True,
|
||||
"reason": f"lease expired at {expires_at.isoformat()}",
|
||||
"pid_alive": pid_alive,
|
||||
"pid_missing": pid_missing,
|
||||
}
|
||||
|
||||
if pid is not None and not pid_alive:
|
||||
# #860: a PID-less lock must never be considered live merely because
|
||||
# expiration / heartbeat fields are absent. Missing PID is insufficient
|
||||
# evidence of a live owner; treat as malformed/stale so recovery routes
|
||||
# can evaluate corroborating pins instead of blocking on a false live flag.
|
||||
if pid_missing:
|
||||
return {
|
||||
"status": "malformed",
|
||||
"live": False,
|
||||
"stale": True,
|
||||
"reason": (
|
||||
"lock has no usable session pid; cannot prove live ownership "
|
||||
"(PID-less locks are never live by missing expiry alone)"
|
||||
),
|
||||
"pid_alive": False,
|
||||
"pid_missing": True,
|
||||
"heartbeat_at": heartbeat_at.isoformat() if heartbeat_at else None,
|
||||
"expires_at": expires_at.isoformat() if expires_at else None,
|
||||
}
|
||||
|
||||
if pid_int is not None and not pid_alive:
|
||||
return {
|
||||
"status": "stale",
|
||||
"live": False,
|
||||
"stale": True,
|
||||
"reason": f"owner pid {pid} is not alive",
|
||||
"reason": f"owner pid {pid_int} is not alive",
|
||||
"pid_alive": False,
|
||||
"pid_missing": False,
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -406,6 +436,7 @@ def assess_lock_freshness(
|
||||
"stale": False,
|
||||
"reason": "lock heartbeat and lease are fresh",
|
||||
"pid_alive": pid_alive,
|
||||
"pid_missing": False,
|
||||
"heartbeat_at": heartbeat_at.isoformat() if heartbeat_at else None,
|
||||
"expires_at": expires_at.isoformat() if expires_at else None,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user