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:
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",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in new issue
Block a user