fix(create): one pane claim at a time per herdr server; a create keeps the pane the monitor registered for it
Three `create --onboard` at once into one folder (the control root, so no control-plane prefix reaches the pane and the shim reuses an idle pane): the shim's new-session chooses a pane - an idle one in a workspace on the same cwd, a split, a fresh workspace's root pane - and then claims it with `agent start`; two shims could choose the pane one of them had just made, and the later start failed with agent_pane_busy (one create in three). mam_herdr now runs every new-session call holding one lock per server: a flock on fd 9, opened for the block and taken by lib_py.pane_lock, which also bounds the wait (MAM_PANE_CLAIM_WAIT_S, default 120). It is released with the block or with the process, however it ends. The shim heredoc is unchanged (S11). Other servers' creates are not held. Serialising the claims exposed a second race: a create's end-of-run `reconcile.sh --once` adopted the pane of another create that had not yet written its row, and that create refused its own name as already running (exit 4). Such a row - registered by the monitor, no job, for a pane created after this create began - is now replaced, not refused. Tests: real herdr - shim-level and create-level three-way races into one folder (the real herdr's pane-creating replies held back so the window is hit on every run; both fail on the base with the orchestrator's error), the different-folder test kept; pane_lock unit tests (per-server file, timeout, kill -9 holder, handover, knob); the adoption rule and its three refusals; shim contract h26 (held lock: no pane calls). The herdr box gets a ~/.zshrc: without it every first `agent start` typed into zsh's first-run menu and timed out at 30 s. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
30c25f193c
commit
7844ae2c5f
9 files changed
+480
-27
No files matched your search
@@ -1486,6 +1486,54 @@ _herdr_scope_is_pane_inherited() {
|
||||
[ -n "${HERDR_WORKSPACE_ID:-}" ] && [ -n "$pane" ] && [ "${pane%%:*}" = "$HERDR_WORKSPACE_ID" ]
|
||||
}
|
||||
|
||||
# Issue #50 follow-up: one pane claim at a time per herdr server.
|
||||
#
|
||||
# The shim's new-session chooses a pane - an idle one in a workspace on the
|
||||
# same cwd (BUG-4), a split of one, or the root pane of a fresh workspace - and
|
||||
# then claims it with `agent start`. Two shims on one server (several creates
|
||||
# at once into one folder) could choose the same idle pane in between, and the
|
||||
# later `agent start` failed with agent_pane_busy. mam_herdr runs the whole
|
||||
# new-session call holding one lock per server: a flock on fd 9, opened for
|
||||
# the block below and taken by lib_py.pane_lock (which also bounds the wait,
|
||||
# MAM_PANE_CLAIM_WAIT_S). It ends with the block or with this process, however
|
||||
# that ends, so a dead holder leaves nothing behind; the file is the target
|
||||
# server's, so other servers' creates are not held. The lock lives here, not
|
||||
# in the shim heredoc, which stays byte-identical (test_issue23_24 S11).
|
||||
#
|
||||
# _mam_pane_claim_target <herdr argv...>: prints the server a new-session call
|
||||
# targets ("" for the default one); rc 1 for any other call.
|
||||
_mam_pane_claim_target() {
|
||||
local sess="${HERDR_SESSION_NAME:-}"
|
||||
while [ "${1:-}" = "-L" ]; do sess="${2:-}"; shift 2 || return 1; done
|
||||
[ "${1:-}" = "new-session" ] || return 1
|
||||
[ "$sess" = "default" ] && sess=""
|
||||
printf '%s\n' "$sess"
|
||||
}
|
||||
|
||||
# _mam_with_pane_claim <server> <cmd...>: run <cmd> holding <server>'s pane lock.
|
||||
_mam_with_pane_claim() {
|
||||
local sess="$1" f=""
|
||||
shift
|
||||
f=$(PYTHONPATH="$SKILL_DIR:${PYTHONPATH:-}" python3 -m lib_py.pane_lock path --session "$sess") || f=""
|
||||
if [ -z "$f" ]; then
|
||||
echo "ERROR: no pane lock path for herdr session ${sess:-default} (lib_py.pane_lock); not placing this session" >&2
|
||||
return 1
|
||||
fi
|
||||
mkdir -p "$(dirname "$f")"
|
||||
{
|
||||
case "$(PYTHONPATH="$SKILL_DIR:${PYTHONPATH:-}" python3 -m lib_py.pane_lock acquire --fd 9)" in
|
||||
locked) ;;
|
||||
timeout)
|
||||
echo "ERROR: another pane claim on herdr session ${sess:-default} did not end within MAM_PANE_CLAIM_WAIT_S; not placing this session" >&2
|
||||
return 1 ;;
|
||||
*)
|
||||
echo "ERROR: the pane lock of herdr session ${sess:-default} could not be decided (lib_py.pane_lock); not placing this session" >&2
|
||||
return 1 ;;
|
||||
esac
|
||||
"$@"
|
||||
} 9>>"$f"
|
||||
}
|
||||
|
||||
mam_herdr() {
|
||||
_init_herdr_isolation
|
||||
# Built as an argv array and decided inside `if`: this runs under
|
||||
@@ -1503,6 +1551,11 @@ mam_herdr() {
|
||||
echo "mam_herdr: '$1' looks up by name; the caller pane's own workspace ${HERDR_WORKSPACE_ID} is not a scope" >&2
|
||||
fi
|
||||
fi
|
||||
local _claim_sess=""
|
||||
if _claim_sess=$(_mam_pane_claim_target "$@"); then
|
||||
_mam_with_pane_claim "$_claim_sess" ${_scope_strip[@]+"${_scope_strip[@]}"} "$WORKSPACE_ROOT/.mam/shim/herdr" "$@"
|
||||
return
|
||||
fi
|
||||
${_scope_strip[@]+"${_scope_strip[@]}"} "$WORKSPACE_ROOT/.mam/shim/herdr" "$@"
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user