feat(issue-50): the delegate-job wrapper waits by the registry before it injects (C)
Reproduction first (herdr 0.9.3, a real claude, one-off server, job 51ccd800): after a job that left a background subagent running (`← 1 agent` on screen for 5 min), herdr reported `working` only while the job ran and `idle` ~10 s after `completed`; a second submit with a 20 s idle wait was injected, not cancelled. The 2026-10-04 observation (0.9.1, 10+ min of `working`) does not reproduce on 0.9.3, so the herdr idle wait stays and the registry wait runs before it (plan §4.3 (나)). - lib_py/delegation/wrapper.py: `open-jobs` (diagnostic) and `wait-open-jobs`. An open job of the session is live when the #18 contract says so: `running` with an event in events.log within idle_timeout_sec; `pending` with a dispatch signal (dispatch_wait_at, else created_at) within ack_timeout_sec. A dead record is reported (`stale open job <id> ignored (...)`) and never waited for. The waiter signals every 10 s by merging dispatch_wait_at into its own pending record through the registry's transition boundary (status untouched, no event), so later waiters see it alive; a waiter whose wrapper is gone stops and its signal goes stale (Creator's challenge, job bf8863e1). Release is FIFO by (created_at, job_id): `clear` when no live running job remains and this job is the earliest live pending one; `busy <ids>` at the bound min(MAM_DISPATCH_WAIT_S, remaining wall clock). - the wrapper: _registry_wait after the preflight in both dispatch paths; `busy` closes the job as cancelled/session_busy (phase registry_wait), exit 5 - the existing contract. - tests/test_issue50_dispatch.py: the open-job table (status x age x signal x spelling), FIFO and the tie, the signal as a registry merge, the bound, a dead wrapper, finalize-end's new phase, both call sites. - tests/test_issue50_dispatch_herdr.py: a real herdr 0.9.3 server per test (short mkdtemp HOME, fake claude the server reports idle, the real publish path over the suite's broker): C1 wait-then-run, C2 the bound, C3 a dead running record, C4 the loop runner's first job behind an onboarding, C5 a session without a row, C7 two waiters in FIFO order with no cancellation, C8 a killed waiter skipped once stale; and three concurrent `create --onboard` / `worktree_prepare.sh --onboard` in one server (one server process survives the shims' concurrent bootstrap, so no bootstrap lock is needed). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
c29c5b2142
commit
e9efabf74b
4 files changed
+1021
-4
No files matched your search
@@ -354,8 +354,11 @@ EOF
|
||||
exit 5
|
||||
fi
|
||||
|
||||
# 2) TUI idle handshake
|
||||
# 2) The registry, then the TUI idle handshake (Issue #50: the session's
|
||||
# open jobs - an onboarding still running, a job sent by hand - end
|
||||
# first, in order; herdr's agent_status is the screen, read after)
|
||||
if [ "$AGENT" != "human" ] && ! _is_remote "$AGENT_SESSION"; then
|
||||
_registry_wait "$JOB_ID" "$AGENT_SESSION"
|
||||
local idle_rc=0
|
||||
wait_for_agent_idle "$AGENT_SESSION" || idle_rc=$?
|
||||
export _SKS_IDLE_RC="$idle_rc"
|
||||
@@ -515,10 +518,11 @@ EOF
|
||||
exit 5
|
||||
fi
|
||||
|
||||
# Idle wait
|
||||
# Registry wait (Issue #50), then idle wait
|
||||
local cur_agent="$AGENT"
|
||||
[ "$_phase" = "reviewer" ] && cur_agent="$REVIEWER"
|
||||
if [ "$cur_agent" != "human" ] && ! _is_remote "$current_session"; then
|
||||
_registry_wait "$JOB_ID" "$current_session"
|
||||
local idle_rc=0
|
||||
wait_for_agent_idle "$current_session" || idle_rc=$?
|
||||
export _SKS_IDLE_RC="$idle_rc"
|
||||
@@ -835,6 +839,30 @@ _within_deadline() {
|
||||
wait "$pid"
|
||||
}
|
||||
|
||||
# _registry_wait <job_id> <session> (Issue #50)
|
||||
# Waits, by the registry alone, until no live open job of <session> remains
|
||||
# and this job is the earliest pending one (lib_py.delegation.wrapper
|
||||
# wait-open-jobs: FIFO, the waiter signals on its own record, a dead record is
|
||||
# ignored). A wait that runs out (MAM_DISPATCH_WAIT_S, default 600 s, bounded
|
||||
# by this job's wall clock) closes this job as cancelled/session_busy and
|
||||
# exits 5, like the preflight and the idle wait.
|
||||
_registry_wait() {
|
||||
local job_id="$1" sess="$2" res
|
||||
_decided res wait-open-jobs --job "$job_id" --agent-session "$sess" --registry-dir "$REGISTRY_DIR" \
|
||||
--wait-s "$(mam_int_knob MAM_DISPATCH_WAIT_S 600 0)" ${START_TIME:+--start-time "$START_TIME"} \
|
||||
--timeout "$TIMEOUT" --watch-pid "$$"
|
||||
case "$res" in
|
||||
clear) return 0 ;;
|
||||
busy*)
|
||||
echo "ERROR: session '$sess' still has open jobs in the registry (${res#busy }); job $job_id is not dispatched" >&2
|
||||
finalize_unfinished "$job_id" "error" "$SUB_RC_ERROR" "registry_wait" "$sess"
|
||||
local logs_root="${DELEGATE_JOB_LOGS_DIR:-$WORKDIR/delegate_job_logs}"
|
||||
echo "$logs_root/$job_id"
|
||||
exit 5 ;;
|
||||
esac
|
||||
_decision_failed wait-open-jobs "answer '$res'"
|
||||
}
|
||||
|
||||
# finalize_unfinished <job_id> <status> <sub_rc> [phase] [session]
|
||||
# Closes a job that did not complete (Issue #33 labels, ISSUE-14/18 disk grace,
|
||||
# Issue #34 cancel reasons) and sets FINAL_JOB_STATUS. The decisions, the disk
|
||||
|
||||
Reference in new issue
Block a user