fix(delegate): submit also takes the target workspace's update admission (Issue #72, review R1)

Decision 8: delegate submit uses the admission lock of the workspace the
target session belongs to. The wrapper resolved only its own
WORKSPACE_ROOT, so a job could be registered and delivered to an agent of
another installed workspace while that workspace was being updated.

lib_py.update target-workspace resolves a herdr:<name> row's workspace (the
.mam at or above its pane.cwd, else its control_plane.workspace_root);
mam_update_admit takes an fd, and submit holds the caller root on fd 7 and
the target on fd 8 until the record is written. Rowless and remote targets
keep the caller-root gate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
GodopuandClaude Opus 5.5 committed 2026-10-27 08:15:23 +00:00
1 parent 595e34b2d1
commit e3394655b3
4 files changed
+123 -16

No files matched your search

+40
View File
@@ -16,6 +16,11 @@ this module what needs parsing. Standard library only, Python 3.9.
python -m lib_py.update monitor --workspace W
JSON {"pids": [...], "lock": "free"|"held"}: the monitor daemons
daemon.find_running recognises, and whether .mam/monitor.lock is held.
python -m lib_py.update target-workspace --workspace W --agent-session S
The installed workspace a delegate target `herdr:<name>` belongs to
(decision 8), when it is not W: the nearest directory holding a .mam
at or above its row's pane.cwd, else the row's control_plane
workspace_root. Nothing for a rowless or non-herdr target.
python -m lib_py.update backup-registry --workspace W --dest D
Copies the registry into D under the registry's own write lock
(agent-sessions.db.lock, as atomic_yaml takes it): the DB through the
@@ -231,6 +236,31 @@ def precheck(workspace: str, state: dict, herdr: str, env: Mapping[str, str]) ->
return blockers
# --------------------------------------------------------------------------- delegate target
def target_workspace(workspace: str, state: dict, agent_session: str) -> str:
if not agent_session.startswith("herdr:"):
return ""
name = agent_session[len("herdr:"):]
row = next((s for s in state.get("herdr_sessions") or []
if isinstance(s, dict) and s.get("name") == name), None)
if row is None:
return ""
found = ""
cwd = (row.get("pane") or {}).get("cwd") or ""
d = _real(cwd) if cwd else ""
while d and not found:
if os.path.isdir(os.path.join(d, ".mam")):
found = d
elif os.path.dirname(d) == d:
break
d = os.path.dirname(d)
if not found:
root = (row.get("control_plane") or {}).get("workspace_root") or ""
found = _real(root) if root else ""
return "" if found == _real(workspace) else found
# --------------------------------------------------------------------------- monitor
def monitor_state(workspace: str) -> dict:
@@ -288,6 +318,9 @@ def main(argv: Optional[List[str]] = None) -> int:
s.add_argument("--herdr", required=True)
s = sub.add_parser("monitor")
s.add_argument("--workspace", required=True)
s = sub.add_parser("target-workspace")
s.add_argument("--workspace", required=True)
s.add_argument("--agent-session", required=True)
s = sub.add_parser("backup-registry")
s.add_argument("--workspace", required=True)
s.add_argument("--dest", required=True)
@@ -303,6 +336,13 @@ def main(argv: Optional[List[str]] = None) -> int:
for line in lines:
print(line)
return 1 if lines else 0
if a.cmd == "target-workspace":
try:
state = _state()
except ValueError:
state = {}
print(target_workspace(ws, state, a.agent_session))
return 0
if a.cmd == "monitor":
print(json.dumps(monitor_state(ws)))
return 0