A non-numeric MAM_READY_EMPTY_GIVEUP, SKS_EMPTY_GIVEUP, SKS_IDLE_RETRIES,
SKS_IDLE_RETRY_BACKOFF_S, MAM_LOCK_STEAL_RETRIES, MAM_DIFF_MAX_BYTES,
MAM_DIFF_MAX_LINES or RECONCILE_POLL_INTERVAL was a `[` error or a sleep
error at the point of use (PR #43 review claude R3). They now go through
mam_int_knob: decimal, a warning and the default for a bad value. Minimums
are in test_issue26_ints.py INT_KNOBS: 1 for the give-up counts (0 acts as
1), the diff caps (0 truncates every diff) and the poll interval (0 is a
busy loop); 0 for the retry counts (one attempt) and the retry backoff.
SKS_QUIESCENT_INTERVAL stays raw: its default is 0.5 s and sleep's
fraction is its meaning; NOT_ARITHMETIC says so.
loop_lock.sh and diff_collect.sh also run without lib.sh (the Python runner
and the tests source them alone), so mam_int / mam_int_knob move verbatim
to lib_int.sh, which lib.sh sources and those two source when the function
is missing. remove.sh's no-manifest fallback list names the new file
(install copies every file under .agents/skills). test_issue21_loop_b l15,
which builds a control root holding only diff_collect.sh, copies it too.
Tests: the table guard now reads every shell file; scripts sourced alone
read their knobs as decimal; _pane_quiescent with SKS_EMPTY_GIVEUP=abc
gives up with a warning and no `[` error (14 of the new cases fail at
0051fd6).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HCGpJXhSZasJLU38ksDghe
172 lines
4.0 KiB
Bash
Executable File
172 lines
4.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# loop_lock.sh — Atomic race-free loop lock acquisition and marker ownership (O-2)
|
|
# Manages .mam/loop-guard-active with atomic hard links and pid+lstart identity validation.
|
|
|
|
# This file is also sourced without lib.sh (tests); the integer rule is lib_int.sh's.
|
|
# shellcheck source=../../lib_int.sh
|
|
declare -F mam_int_knob >/dev/null || source "$(dirname "${BASH_SOURCE[0]}")/../../lib_int.sh"
|
|
MAM_LOCK_STEAL_RETRIES="$(mam_int_knob MAM_LOCK_STEAL_RETRIES 3 0)"
|
|
|
|
mam_lstart() {
|
|
local pid="${1:-$$}"
|
|
ps -p "$pid" -o lstart= 2>/dev/null | tr -s ' ' | sed 's/^ *//;s/ *$//'
|
|
}
|
|
|
|
_mam_lock_field() {
|
|
local file="$1" key="$2"
|
|
if [ ! -f "$file" ]; then
|
|
return 1
|
|
fi
|
|
grep "^${key}=" "$file" 2>/dev/null | head -n 1 | cut -d'=' -f2- || true
|
|
}
|
|
|
|
_mam_lock_holder_state() {
|
|
local marker="$1"
|
|
if [ ! -f "$marker" ]; then
|
|
printf 'stale'
|
|
return 0
|
|
fi
|
|
|
|
local rec_pid rec_lstart cur_lstart own_lstart
|
|
rec_pid=$(_mam_lock_field "$marker" "pid")
|
|
rec_lstart=$(_mam_lock_field "$marker" "lstart")
|
|
|
|
if [ -z "$rec_pid" ] || ! [[ "$rec_pid" =~ ^[0-9]+$ ]]; then
|
|
printf 'stale'
|
|
return 0
|
|
fi
|
|
|
|
own_lstart=$(mam_lstart "$$")
|
|
if [ -z "$own_lstart" ]; then
|
|
printf 'unknown'
|
|
return 0
|
|
fi
|
|
|
|
cur_lstart=$(mam_lstart "$rec_pid")
|
|
if [ -z "$cur_lstart" ]; then
|
|
printf 'stale'
|
|
return 0
|
|
fi
|
|
|
|
if [ -n "$rec_lstart" ]; then
|
|
if [ "$cur_lstart" != "$rec_lstart" ]; then
|
|
printf 'stale'
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
printf 'live'
|
|
return 0
|
|
}
|
|
|
|
_mam_lock_publish() {
|
|
local marker="$1"
|
|
local parent_dir
|
|
parent_dir=$(dirname "$marker")
|
|
mkdir -p "$parent_dir" 2>/dev/null || true
|
|
|
|
local tmp_marker="${marker}.tmp.${$}.${RANDOM:-0}"
|
|
local lstart_val
|
|
lstart_val=$(mam_lstart "$$")
|
|
|
|
printf 'pid=%s\nlstart=%s\nstarted=%s\n' \
|
|
"$$" "$lstart_val" "$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date)" > "$tmp_marker"
|
|
|
|
local rc=0
|
|
ln "$tmp_marker" "$marker" 2>/dev/null || rc=$?
|
|
rm -f "$tmp_marker" 2>/dev/null || true
|
|
return $rc
|
|
}
|
|
|
|
mam_acquire_loop_lock() {
|
|
local marker="${1:-}"
|
|
if [ -z "$marker" ]; then
|
|
return 2
|
|
fi
|
|
|
|
local steal_lock="${marker}.steal"
|
|
local retries=0
|
|
|
|
while [ "$retries" -le "$MAM_LOCK_STEAL_RETRIES" ]; do
|
|
# Upgrade path: clean up legacy directory at steal_lock if present
|
|
if [ -d "$steal_lock" ]; then
|
|
rmdir "$steal_lock" 2>/dev/null || true
|
|
fi
|
|
|
|
# Try publishing primary marker
|
|
if _mam_lock_publish "$marker"; then
|
|
return 0
|
|
fi
|
|
|
|
# Check primary marker holder state
|
|
local state
|
|
state=$(_mam_lock_holder_state "$marker")
|
|
if [ "$state" = "live" ]; then
|
|
return 3
|
|
fi
|
|
if [ "$state" = "unknown" ]; then
|
|
return 4
|
|
fi
|
|
|
|
# Primary marker is stale! Try publishing steal_lock
|
|
if _mam_lock_publish "$steal_lock"; then
|
|
if [ "$(_mam_lock_holder_state "$marker")" = "stale" ]; then
|
|
rm -f "$marker" 2>/dev/null || true
|
|
fi
|
|
rm -f "$steal_lock" 2>/dev/null || true
|
|
if _mam_lock_publish "$marker"; then
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
# Check steal_lock holder state
|
|
local steal_state
|
|
steal_state=$(_mam_lock_holder_state "$steal_lock")
|
|
case "$steal_state" in
|
|
live)
|
|
return 3
|
|
;;
|
|
unknown)
|
|
return 4
|
|
;;
|
|
stale)
|
|
rm -f "$steal_lock" 2>/dev/null || true
|
|
retries=$((retries + 1))
|
|
continue
|
|
;;
|
|
esac
|
|
|
|
sleep 1
|
|
retries=$((retries + 1))
|
|
done
|
|
|
|
return 3
|
|
}
|
|
|
|
mam_release_loop_lock() {
|
|
local marker="${1:-}"
|
|
if [ -z "$marker" ] || [ ! -f "$marker" ]; then
|
|
return 0
|
|
fi
|
|
|
|
local rec_pid rec_lstart cur_lstart
|
|
rec_pid=$(_mam_lock_field "$marker" "pid")
|
|
rec_lstart=$(_mam_lock_field "$marker" "lstart")
|
|
cur_lstart=$(mam_lstart "$$")
|
|
|
|
if [ "$rec_pid" = "$$" ] && [ "$rec_lstart" = "$cur_lstart" ]; then
|
|
rm -f "$marker" 2>/dev/null || true
|
|
return 0
|
|
fi
|
|
|
|
return 1
|
|
}
|
|
|
|
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
|
|
case "${1:-}" in
|
|
acquire) mam_acquire_loop_lock "${2:-}" ;;
|
|
release) mam_release_loop_lock "${2:-}" ;;
|
|
*) echo "Usage: $0 {acquire|release} <marker-path>" ; exit 1 ;;
|
|
esac
|
|
fi
|