fix(reset): fence every job registry the reset reads, by its physical directory (#78)

Review round 3 R1 (codex): the registration guard chose the workspace marker
from the path's spelling, so a symlink alias of .mam/jobs or a MAM_JOBS_DIR
registry was not refused. The reset now writes .reset-in-progress into each
registry its open-job check reads (.mam/jobs, MAM_JOBS_DIR, a row's other
control_plane.mam_jobs_dir - the latter only for this workspace's sessions),
each under that registry's lock and before the check, and removes it on exit;
register_job reads the marker inside the registry it writes to. The
precheck's separate lock barrier is subsumed by the fence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
GodopuandClaude Opus 5.5 committed 2026-10-30 05:23:51 +09:00
1 parent 548c6854ea
commit 568385c77a
6 files changed
+186 -75

No files matched your search

+47 -7
View File
@@ -18,11 +18,12 @@ import sqlite3
import subprocess
import sys
import tempfile
from typing import Dict, List, Mapping, Optional, Tuple
from typing import Dict, Iterator, List, Mapping, Optional, Tuple
from lib_py.agents.base import AGENT_SESSION_ENV, POST_DISCOVERED
from lib_py.agents.registry import agent_of_row, get_adapter, own_key
from lib_py.delegation.mqtt_common import registry_lock
from lib_py.delegation.registry import RESET_MARKER
from lib_py.update import open_jobs, row_server
from lib_py.verify_session import verify_session_uuid
@@ -484,6 +485,39 @@ def target_open_jobs(workspace: str, state: dict, env: Mapping[str, str], sessio
return out
def fence_registries(workspace: str, state: dict, env: Mapping[str, str]) -> Iterator[str]:
"""Write RESET_MARKER into every registry open_jobs reads, each under its
registry lock (a registration that read no marker has finished writing),
and yield its real path as it is written. This workspace's .mam/jobs and
MAM_JOBS_DIR (created when absent) refuse every registration; another
control plane's registry named by a row refuses only those addressed to
this registry's rows. The marker copies .mam/reset-in-progress."""
with open(os.path.join(workspace, ".mam", "reset-in-progress"), encoding="utf-8") as fh:
base = "".join(l + "\n" for l in fh.read().splitlines() if l.split("=", 1)[0] in ("pid", "lstart", "started"))
base += "workspace=%s\n" % workspace
rows = state.get("herdr_sessions") or []
sessions = ",".join("herdr:%s" % s["name"] for s in rows if s.get("name"))
plan, seen = [], set()
for d in (os.path.join(workspace, ".mam", "jobs"), env.get("MAM_JOBS_DIR") or ""):
if d:
os.makedirs(d, exist_ok=True)
plan.append((d, base))
for s in rows:
d = (s.get("control_plane") or {}).get("mam_jobs_dir") or ""
if d and os.path.isdir(d):
plan.append((d, base + "sessions=%s\n" % sessions))
for d, text in plan:
if _real(d) in seen:
continue
seen.add(_real(d))
with registry_lock(d):
fd, tmp = tempfile.mkstemp(dir=d, prefix=RESET_MARKER + ".")
with os.fdopen(fd, "w", encoding="utf-8") as fh:
fh.write(text)
os.replace(tmp, os.path.join(d, RESET_MARKER))
yield _real(d)
def target_id_owned(state: dict, session_name: str) -> bool:
"""The row's recorded post-discovered id still verifies (revalidate) as a
conversation of the row's cwd - the check stop makes before it keeps such
@@ -757,6 +791,9 @@ def main(argv: Optional[List[str]] = None) -> int:
s.add_argument("--workspace", required=True)
s.add_argument("--session", required=True)
s = sub.add_parser("fence")
s.add_argument("--workspace", required=True)
s = sub.add_parser("id-owned")
s.add_argument("--workspace", required=True)
s.add_argument("--session", required=True)
@@ -814,12 +851,6 @@ def main(argv: Optional[List[str]] = None) -> int:
return 1 if sel["errors"] else 0
if a.cmd == "precheck":
# A registration that read no reset marker finishes writing under this
# lock before the open jobs are listed (registry.reset_refusal).
jobs_dir = os.path.join(ws, ".mam", "jobs")
if os.path.isdir(jobs_dir):
with registry_lock(jobs_dir):
pass
lines = precheck(ws, state, os.environ)
for line in lines:
print(line)
@@ -852,6 +883,15 @@ def main(argv: Optional[List[str]] = None) -> int:
print(line)
return 1 if lines else 0
if a.cmd == "fence":
try:
for d in fence_registries(ws, state, os.environ):
print(d, flush=True)
except OSError as exc:
print("ERROR: cannot fence the job registries (%s)" % exc, file=sys.stderr)
return 1
return 0
if a.cmd == "id-owned":
return 0 if target_id_owned(state, a.session) else 1