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:
1 parent
568385c77a
commit
b49314c3d0
6 files changed
+213
-74
No files matched your search
@@ -23,7 +23,7 @@ bash .agents/skills/multi-agent-mux-reset/scripts/reset.sh --yes [--dry-run]
|
||||
| Targets | Every registry row with `status: running` and `realpath(pane.cwd) == realpath(WS)`. Rows with any other status, worktree rows, and `orchestrator_uuids` are skipped and listed with their reason. |
|
||||
| Confirmation | `[y/N]` prompt; without a terminal `-y`/`--yes` is required (exits `3` otherwise). `--dry-run` inspects without `--yes` and changes nothing. |
|
||||
| Prechecks | Refuses before any change when an open job exists (prints the `registry.py ... close --request cancelled` command), when `.mam/loop-guard-active` or `.mam/update-in-progress` is held, when a target row lacks a valid agent or role, when a running `claude`/`grok` target has no recorded conversation id (the monitor had not pinned one; record it, or stop/purge it by hand first), when `.mam/reset-spec.json` is invalid, or when the caller itself is one of the targets. |
|
||||
| Lock & concurrency | Takes the update admission lock exclusively, acquires `.mam/loop-guard-active` and publishes `.mam/reset-in-progress`, then releases the admission lock and runs the prechecks. `run_loop.sh`, `update.sh` and another reset refuse on the loop marker. The reset then writes `.reset-in-progress` into every job registry its open-job check reads (`.mam/jobs`, `MAM_JOBS_DIR`, and a row's other `control_plane.mam_jobs_dir`), each under that registry's lock, before the check. A registration (`multi-agent-mux-delegate-job submit`, `registry.py register`, the registry API) into a fenced registry - by any path to that directory - refuses; in another control plane's registry only for this workspace's sessions. The onboarding jobs of the reset's own `create_session.sh` are let through. |
|
||||
| Lock & concurrency | Takes the update admission lock exclusively, acquires `.mam/loop-guard-active` and publishes `.mam/reset-in-progress`, then releases the admission lock and runs the prechecks. `run_loop.sh`, `update.sh` and another reset refuse on the loop marker. The reset then writes `.reset-in-progress` into every job registry its open-job check reads (`.mam/jobs`, `MAM_JOBS_DIR`, and a row's other `control_plane.mam_jobs_dir`; created when absent), each under that registry's lock, before the check. It refuses, changing nothing, when a registry shared with another workspace is already fenced by that workspace's live reset; it never replaces or removes another reset's marker. A registration (`multi-agent-mux-delegate-job submit`, `registry.py register`, the registry API) into a fenced registry - by any path to that directory - refuses; in another control plane's registry only for this workspace's sessions. The onboarding jobs of the reset's own `create_session.sh` are let through. |
|
||||
| Spec file & rerun | Writes `.mam/reset-spec.json` atomically before the first stop/purge and removes it only when all targets reach `done`. Rerunning resumes from the spec file without re-purging already recreated agents (`created` or `done`). |
|
||||
| Sequence per target | Sequential (never parallel): ⓪ re-checks that its row still has the spec's agent and herdr server and that no job addressed to it is open, ① normal `stop_session.sh --reason reset` (a post-discovered agent's id is captured onto the row while herdr is live; `status: stopped`), ② checks that the row's id is the one recorded before the stop and, for a post-discovered agent, that it still verifies as a conversation of the row's folder, ③ `stop_session.sh --reason reset --purge-conversation --yes` (using the row's recorded ID, never a CLI UUID), then `create_session.sh` with the original name, agent, role, `--herdr-session`, and `--herdr-workspace` (onboarding enabled by default). |
|
||||
| When skipped (exit 4) | The target is skipped and the reset continues to the next agent when step ⓪ finds a changed row (`row changed`) or an open job (`job open`) - nothing was changed for it -, or when step ② finds no id (`no conversation id recorded`) or an id other than the recorded one or one that does not verify for the row's folder (`id not verified`), or step ③ exits `4` (`purge refused`, in-use gate) - it is stopped but resumable (its row keeps its id) and its conversation files are untouched. Rerun once the cause is gone, or resume it. |
|
||||
|
||||
@@ -150,12 +150,16 @@ fi
|
||||
fence_out=""
|
||||
fence_rc=0
|
||||
fence_out="$(MAM_STATE_JSON="$STATE_JSON" "$PY" -m lib_py.reset fence --workspace "$WS")" || fence_rc=$?
|
||||
fence_refusal=""
|
||||
while IFS= read -r line; do
|
||||
[ -n "$line" ] && FENCED+=("$line")
|
||||
case "$line" in
|
||||
"fenced "*) FENCED+=("${line#fenced }") ;;
|
||||
"refused "*) fence_refusal="${line#refused }" ;;
|
||||
esac
|
||||
done <<< "$fence_out"
|
||||
if [ "$fence_rc" -ne 0 ]; then
|
||||
echo "ERROR: reset refused; nothing was changed:" >&2
|
||||
echo " - the job registries could not be fenced against new registrations (lib_py.reset fence rc $fence_rc)" >&2
|
||||
echo " - ${fence_refusal:-the job registries could not be fenced against new registrations (lib_py.reset fence rc $fence_rc)}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
Reference in new issue
Block a user