fix(reset): fence every job registry the reset reads, by its physical directory (#78)

Review round 3 R1 (codex): the registration guard chose the workspace marker
from the path's spelling, so a symlink alias of .mam/jobs or a MAM_JOBS_DIR
registry was not refused. The reset now writes .reset-in-progress into each
registry its open-job check reads (.mam/jobs, MAM_JOBS_DIR, a row's other
control_plane.mam_jobs_dir - the latter only for this workspace's sessions),
each under that registry's lock and before the check, and removes it on exit;
register_job reads the marker inside the registry it writes to. The
precheck's separate lock barrier is subsumed by the fence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
GodopuandClaude Opus 5.5 committed 2026-10-30 05:23:51 +09:00
1 parent 548c6854ea
commit 568385c77a
6 files changed
+186 -75

No files matched your search

@@ -92,9 +92,8 @@ _refuse_locked() {
# The update admission lock is taken exclusively, as update.sh does, while the
# reset publishes its markers: a delegate submit admitted earlier has written
# its job record (the precheck below sees it), and one admitted later sees
# .mam/reset-in-progress and refuses. Any other registration in .mam/jobs
# (registry.py register, the delegation API) reads the marker under the
# registry lock, which the precheck takes before it lists the open jobs.
# .mam/reset-in-progress and refuses. Any other registration (registry.py
# register, the delegation API) is fenced below.
mkdir -p "$MAM"
exec 7>>"$MAM/update-admission.lock"
case "$("$PY" -m lib_py.pane_lock acquire --fd 7 --wait-env MAM_UPDATE_ADMISSION_WAIT_S)" in
@@ -115,7 +114,12 @@ case "$_acquire_rc" in
3) _refuse_locked "a review loop or another reset is running (pid $(_mam_lock_field "$MARKER" "pid" 2>/dev/null || echo "unknown")); wait for it to end" ;;
*) _refuse_locked "cannot determine whether a review loop or another reset is running (ps unavailable?)" ;;
esac
FENCED=()
_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
mam_release_loop_lock "$RESET_MARKER" || true
mam_release_loop_lock "$MARKER" || true
}
@@ -140,6 +144,21 @@ if ! STATE_JSON="$(load_state_json strict)"; then
exit 1
fi
# Every registry the precheck reads gets the reset marker under its own lock
# (lib_py.reset fence): a registration that read none is written by then, any
# later one is refused, by whatever path it names that registry.
fence_out=""
fence_rc=0
fence_out="$(MAM_STATE_JSON="$STATE_JSON" "$PY" -m lib_py.reset fence --workspace "$WS")" || fence_rc=$?
while IFS= read -r line; do
[ -n "$line" ] && FENCED+=("$line")
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
exit 1
fi
pre_out=""
pre_rc=0
pre_out="$(MAM_STATE_JSON="$STATE_JSON" "$PY" -m lib_py.reset precheck --workspace "$WS")" || pre_rc=$?