fix(reset,stop): warn before a reset purges, skip and prune stale registries, refuse a guessed purge

reset.sh without --yes says the conversations are deleted for good before it
asks; without a terminal the refusal says so too. A registry that cannot be
created is skipped, since nothing can be registered there, and the directories
the fence created are removed on exit when still empty. stop --purge-conversation
refuses a conversation the row does not record instead of deleting the folder's
newest one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
This commit is contained in:
GodopuandClaude Opus 5.5 committed 2026-10-31 07:56:07 +09:00
1 parent 12e140322d
commit 022c656052
9 files changed
+201 -28

No files matched your search

@@ -154,6 +154,13 @@ def decide_stop(state: Dict[str, Any], name: str, agent: str, cwd: str,
"purge_uuid": "",
"note": f"{found} is also recorded by {others}; registry needs fixing",
}
if purge and found and found != own:
return {
"captured": "",
"status": "purge-unrecorded",
"purge_uuid": "",
"note": f"{found} is this folder's newest conversation, not an id this row records",
}
return {
"captured": found,
"status": "resolved" if found else "no-candidate",
+41 -6
View File
@@ -22,7 +22,7 @@ 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.mqtt_common import LOCK_FILENAME, registry_lock
from lib_py.delegation.registry import RESET_MARKER, reset_marker
from lib_py.update import jobs_dirs, open_jobs, row_server
from lib_py.verify_session import verify_session_uuid
@@ -497,7 +497,10 @@ def fence_registries(workspace: str, state: dict, env: Mapping[str, str]) -> Ite
fenced by another live (or undecidable) reset - registries may be shared
between workspaces; its marker is left as it is. This workspace's own
registries refuse every registration; another control plane's only those
addressed to this registry's rows. The marker copies .mam/reset-in-progress."""
addressed to this registry's rows. The marker copies .mam/reset-in-progress.
Yields ("created", path) per directory level it makes, deepest first, then
("fenced", real path). A registry that cannot be created is skipped: nothing
can be registered there."""
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"))
me = dict(l.split("=", 1) for l in base.splitlines()).get("pid", "")
@@ -505,7 +508,17 @@ def fence_registries(workspace: str, state: dict, env: Mapping[str, str]) -> Ite
rows = state.get("herdr_sessions") or []
sessions = ",".join("herdr:%s" % s["name"] for s in rows if s.get("name"))
for d, addressed_only in jobs_dirs(workspace, state, env):
os.makedirs(d, exist_ok=True)
missing = []
p = os.path.abspath(d)
while not os.path.lexists(p):
missing.append(p)
p = os.path.dirname(p)
try:
os.makedirs(d, exist_ok=True)
except OSError:
continue
for p in missing:
yield "created", p
with registry_lock(d):
held, fields = reset_marker(d)
if held in ("live", "unknown") and fields.get("pid") != me:
@@ -516,7 +529,21 @@ def fence_registries(workspace: str, state: dict, env: Mapping[str, str]) -> Ite
with os.fdopen(fd, "w", encoding="utf-8") as fh:
fh.write(base + ("sessions=%s\n" % sessions if addressed_only else ""))
os.replace(tmp, os.path.join(d, RESET_MARKER))
yield _real(d)
yield "fenced", _real(d)
def prune_created(paths: List[str]) -> None:
"""Remove the directories fence_registries created, deepest first, each only
while nothing but its registry lock file is left in it."""
for d in paths:
try:
if os.path.isfile(os.path.join(d, LOCK_FILENAME)):
with registry_lock(d):
if os.listdir(d) == [LOCK_FILENAME]:
os.unlink(os.path.join(d, LOCK_FILENAME))
os.rmdir(d)
except OSError:
continue
def target_id_owned(state: dict, session_name: str) -> bool:
@@ -795,6 +822,10 @@ def main(argv: Optional[List[str]] = None) -> int:
s = sub.add_parser("fence")
s.add_argument("--workspace", required=True)
s = sub.add_parser("prune-created")
s.add_argument("--workspace", required=True)
s.add_argument("paths", nargs="*")
s = sub.add_parser("id-owned")
s.add_argument("--workspace", required=True)
s.add_argument("--session", required=True)
@@ -886,8 +917,8 @@ def main(argv: Optional[List[str]] = None) -> int:
if a.cmd == "fence":
try:
for d in fence_registries(ws, state, os.environ):
print("fenced %s" % d, flush=True)
for kind, d in fence_registries(ws, state, os.environ):
print("%s %s" % (kind, d), flush=True)
except FenceBusy as exc:
print("refused %s" % exc)
return 1
@@ -896,6 +927,10 @@ def main(argv: Optional[List[str]] = None) -> int:
return 1
return 0
if a.cmd == "prune-created":
prune_created(a.paths)
return 0
if a.cmd == "id-owned":
return 0 if target_id_owned(state, a.session) else 1