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
@@ -21,9 +21,9 @@ bash .agents/skills/multi-agent-mux-reset/scripts/reset.sh --yes [--dry-run]
|
||||
| Item | Behaviour |
|
||||
|---|---|
|
||||
| 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. |
|
||||
| Confirmation | Without `-y`/`--yes` it warns that the conversations are deleted for good and asks `[y/N]`; without a terminal it cannot ask, so `-y`/`--yes` is required (exits `3` otherwise). `--dry-run` inspects without `--yes` and changes nothing. An agent has no terminal here: show the user the `--dry-run` targets, say the deletion cannot be undone, and pass `--yes` only after they confirm. |
|
||||
| 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`; 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. |
|
||||
| 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, skipped when it cannot be created, since nothing can be registered there), each under that registry's lock, before the check, and on exit removes the directories it created if they are still empty. 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. |
|
||||
|
||||
@@ -36,7 +36,7 @@ while [ $# -gt 0 ]; do
|
||||
done
|
||||
|
||||
if [ "$DRY_RUN" != "1" ] && [ "$YES" != "1" ] && [ ! -t 0 ]; then
|
||||
echo "ERROR: not a terminal; run with -y/--yes (or --dry-run to inspect)" >&2
|
||||
echo "ERROR: not a terminal, so the reset cannot ask; it permanently deletes conversations: run with -y/--yes once that is confirmed (or --dry-run to inspect)" >&2
|
||||
exit 3
|
||||
fi
|
||||
|
||||
@@ -115,11 +115,13 @@ case "$_acquire_rc" in
|
||||
*) _refuse_locked "cannot determine whether a review loop or another reset is running (ps unavailable?)" ;;
|
||||
esac
|
||||
FENCED=()
|
||||
CREATED=()
|
||||
_release_reset_lock() {
|
||||
local d
|
||||
for d in ${FENCED[@]+"${FENCED[@]}"}; do
|
||||
[ "$(_mam_lock_field "$d/.reset-in-progress" pid 2>/dev/null || true)" != "$$" ] || rm -f "$d/.reset-in-progress"
|
||||
done
|
||||
[ "${#CREATED[@]}" -eq 0 ] || MAM_STATE_JSON="$STATE_JSON" "$PY" -m lib_py.reset prune-created --workspace "$WS" "${CREATED[@]}" || true
|
||||
mam_release_loop_lock "$RESET_MARKER" || true
|
||||
mam_release_loop_lock "$MARKER" || true
|
||||
}
|
||||
@@ -154,6 +156,7 @@ fence_refusal=""
|
||||
while IFS= read -r line; do
|
||||
case "$line" in
|
||||
"fenced "*) FENCED+=("${line#fenced }") ;;
|
||||
"created "*) CREATED+=("${line#created }") ;;
|
||||
"refused "*) fence_refusal="${line#refused }" ;;
|
||||
esac
|
||||
done <<< "$fence_out"
|
||||
@@ -174,16 +177,19 @@ if [ "$pre_rc" -ne 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
_confirm_purge() {
|
||||
local ans=""
|
||||
echo "WARNING: this permanently deletes the conversations of the agents to reset above; they cannot be recovered."
|
||||
read -r -p "$1 [y/N]: " ans || ans=""
|
||||
case "$ans" in
|
||||
y|Y|yes|Yes|YES) ;;
|
||||
*) echo "Reset cancelled; nothing was changed"; exit 0 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
if [ -f "$SPEC_FILE" ]; then
|
||||
MAM_STATE_JSON="$STATE_JSON" "$PY" -m lib_py.reset dry-run --workspace "$WS" || exit 1
|
||||
if [ "$YES" != "1" ]; then
|
||||
ans=""
|
||||
read -r -p "Resume reset from $SPEC_FILE? [y/N]: " ans || ans=""
|
||||
case "$ans" in
|
||||
y|Y|yes|Yes|YES) ;;
|
||||
*) echo "Reset cancelled; nothing was changed"; exit 0 ;;
|
||||
esac
|
||||
fi
|
||||
[ "$YES" = "1" ] || _confirm_purge "Really resume the reset from $SPEC_FILE?"
|
||||
else
|
||||
PLAN_JSON="$(MAM_STATE_JSON="$STATE_JSON" "$PY" -m lib_py.reset plan --workspace "$WS")" || exit 1
|
||||
TARGET_COUNT="$(printf '%s' "$PLAN_JSON" | "$PY" -c 'import json,sys; print(len(json.load(sys.stdin).get("targets", [])))')"
|
||||
@@ -192,14 +198,7 @@ else
|
||||
echo "nothing to reset"
|
||||
exit 0
|
||||
fi
|
||||
if [ "$YES" != "1" ]; then
|
||||
ans=""
|
||||
read -r -p "Purge and recreate $TARGET_COUNT agents? [y/N]: " ans || ans=""
|
||||
case "$ans" in
|
||||
y|Y|yes|Yes|YES) ;;
|
||||
*) echo "Reset cancelled; nothing was changed"; exit 0 ;;
|
||||
esac
|
||||
fi
|
||||
[ "$YES" = "1" ] || _confirm_purge "Really purge and recreate $TARGET_COUNT agents?"
|
||||
printf '%s' "$PLAN_JSON" | "$PY" -m lib_py.reset write-spec --workspace "$WS" || exit 1
|
||||
fi
|
||||
|
||||
|
||||
Reference in new issue
Block a user