fix(reset): never take another live reset's fence, and fence absent row registries (#78)

Review round 4 (codex):
- R1: fence_registries reads an existing marker under the registry lock and
  refuses the reset, changing nothing, when another live (or undecidable)
  reset holds it - registries may be shared between workspaces; only a stale
  marker is replaced, and the reset's exit removes only its own fences.
- R2: the registries to fence come from update.jobs_dirs, the enumeration
  open_jobs now uses too, whether or not they exist; an absent row registry
  is created and fenced (a registration would create it), and one that cannot
  be created refuses the reset.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
GodopuandClaude Opus 5.5 committed 2026-10-30 08:21:40 +09:00
1 parent 568385c77a
commit b49314c3d0
6 files changed
+213 -74

No files matched your search

+27 -14
View File
@@ -94,32 +94,45 @@ def _lstart(pid: str) -> str:
return " ".join(out.split())
def reset_refusal(registry_dir: str, agent_session: str) -> Optional[str]:
"""Why registering a job for ``agent_session`` in ``registry_dir`` is
refused, or None: a live reset fenced this registry (for that session) and
the caller is not its own create. Holder states as loop_lock.sh decides
them: a dead or reused pid is stale; ps failing on this process is unknown
(refused)."""
def reset_marker(registry_dir: str) -> Tuple[str, Dict[str, str]]:
"""(holder state, fields) of the registry's RESET_MARKER: "absent", or as
loop_lock.sh decides - "stale" (dead or reused pid), "live", "unknown" (ps
fails on this process; a marker that cannot be read is unknown too)."""
marker = os.path.join(registry_dir, RESET_MARKER)
try:
with open(marker, encoding="utf-8") as fh:
fields = dict(line.split("=", 1) for line in fh.read().splitlines() if "=" in line)
except FileNotFoundError:
return None
return "absent", {}
except (OSError, ValueError) as exc:
return "the reset marker %s cannot be read (%s)" % (marker, exc)
if "sessions" in fields and agent_session not in fields["sessions"].split(","):
return None
return "unknown", {"error": str(exc)}
pid = fields.get("pid", "")
if not pid.isdigit() or os.environ.get(RESET_OWNER_ENV) == pid:
return None
if not pid.isdigit():
return "stale", fields
if not _lstart(str(os.getpid())):
return "cannot tell whether the reset in %s (pid %s) is live (ps unavailable?)" % (marker, pid)
return "unknown", fields
cur = _lstart(pid)
if not cur or (fields.get("lstart") and cur != fields["lstart"]):
return "stale", fields
return "live", fields
def reset_refusal(registry_dir: str, agent_session: str) -> Optional[str]:
"""Why registering a job for ``agent_session`` in ``registry_dir`` is
refused, or None: a live (or undecidable) reset fenced this registry, for
that session, and the caller is not its own create."""
state, fields = reset_marker(registry_dir)
if state in ("absent", "stale"):
return None
if "sessions" in fields and agent_session not in fields["sessions"].split(","):
return None
if fields.get("pid") and os.environ.get(RESET_OWNER_ENV) == fields["pid"]:
return None
if state == "unknown":
return "cannot tell whether the reset fencing %s is live (%s)" % (
os.path.join(registry_dir, RESET_MARKER), fields.get("error") or "ps unavailable?")
return ("a reset of %s is in progress (pid %s); its agents are being purged and recreated;"
" register the job when it has finished" % (fields.get("workspace", "?"), pid))
" register the job when it has finished" % (fields.get("workspace", "?"), fields["pid"]))
def register_job(