feat: update admission gate - entries refuse while an update runs (Issue #72, decision 8, U2)

The update skill publishes .mam/update-in-progress (pid + lstart, the
loop_lock.sh rules) while it holds .mam/update-admission.lock exclusively.
create, resume, run_loop.sh and delegate submit (decision 8) and stop and
orc-onboard (U2: a rollback would restore the registry without their write)
take that lock shared (lib.sh mam_update_admit, fd 7), refuse while the marker
is live or unknown, and hold the lock until their record is written. A stale
marker refuses nothing. lib_py.pane_lock acquire gains --shared and
--wait-env (MAM_UPDATE_ADMISSION_WAIT_S, default 120); create's wrapper path
closes fd 7 for the long-lived child. The exit-1 rows of create/resume/stop
name the refusal; skill_code_blocks.json pins follow the shifted lines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
GodopuandClaude Opus 5.5 committed 2026-10-27 04:06:42 +00:00
1 parent 179473e5d5
commit 6359d65999
13 files changed
+311 -25

No files matched your search

+52
View File
@@ -3247,3 +3247,55 @@ mam_control_plane_prefix() {
[ -n "${1:-}" ] || return 0
PYTHONPATH="$SKILL_DIR:${PYTHONPATH:-}" "$(_delegate_py_bin)" -m lib_py.control_plane prefix "$1"
}
# ---------------------------------------------------------------------------
# Issue #72 (decision 8): the update admission gate.
#
# The update skill publishes .mam/update-in-progress (pid + lstart, loop_lock.sh
# rules) while it holds .mam/update-admission.lock exclusively, then runs its
# pre-check. Every entry that starts something the pre-check must see - create,
# resume, run_loop.sh, delegate submit - or writes the registry - stop,
# orc-onboard - takes that lock shared on fd 7, refuses while the marker is
# `live` or `unknown`, and keeps fd 7 until its record is written
# (mam_update_admit_release). An update therefore either waits for that record
# and sees it, or publishes first and the entry refuses. The lock is a BSD
# flock taken by lib_py.pane_lock on the open fd: it ends when every process
# holding fd 7 has ended, however that ends, so nothing long-lived may inherit
# fd 7 (`7>&-` on such a child, as pane_lock's fd 9). The wait is bounded by
# MAM_UPDATE_ADMISSION_WAIT_S (default 120).
#
# mam_update_admit [workspace]: rc 0 admitted (fd 7 held), 1 refused (stderr).
mam_update_admit() {
local root="${1:-$WORKSPACE_ROOT}"
local lock="$root/.mam/update-admission.lock" marker="$root/.mam/update-in-progress" st
if ! { mkdir -p "$root/.mam" && exec 7>>"$lock"; }; then
echo "ERROR: cannot open the update admission lock $lock" >&2
return 1
fi
case "$(PYTHONPATH="$SKILL_DIR:${PYTHONPATH:-}" python3 -m lib_py.pane_lock acquire --fd 7 --shared --wait-env MAM_UPDATE_ADMISSION_WAIT_S)" in
locked) ;;
timeout)
exec 7>&-
echo "ERROR: an update of $root holds its admission lock (MAM_UPDATE_ADMISSION_WAIT_S passed); try again when it has finished" >&2
return 1 ;;
*)
exec 7>&-
echo "ERROR: the update admission lock $lock could not be decided (lib_py.pane_lock)" >&2
return 1 ;;
esac
[ -f "$marker" ] || return 0
declare -F _mam_lock_holder_state >/dev/null || source "$SKILL_DIR/multi-agent-mux-loop/scripts/loop_lock.sh"
st="$(_mam_lock_holder_state "$marker")"
case "$st" in
stale)
echo "NOTE: stale update marker $marker (its update is gone); ignored" >&2
return 0 ;;
esac
exec 7>&-
echo "ERROR: an update of $root is in progress (marker $marker, holder $st); try again when it has finished" >&2
return 1
}
mam_update_admit_release() {
exec 7>&-
}