fix(issue-17): a decision the wrapper cannot make ends it with 1 (N2)
`exit "$(_decide exit-code ...)"` became `exit ""` when wrapper.py could
not answer (its module or $PY broken): rc 255 on bash 3.2. The other printed
decisions failed open in their own ways: a failed wait-rc returned "" (255),
a failed finalize-begin left finalize-end with an empty --final (argparse
rc 2), and a failed sub-status or finalize-end ended under set -e with the
interpreter's own code (1 for a traceback, 127 for one that cannot run).
Printed decisions now go through _decided <var> <subcommand>, and the
exits through _exit_for <status>: a failed call, an empty answer or a
non-numeric exit code / wait rc prints one ERROR line and exits 1. finalize-
end may legitimately answer nothing (its wrapper gone), so only its rc is
checked. The normal path is unchanged: same answers, exit codes and output.
Left as they are: subscriber-up and stall-watch are answered by rc, so a
failure reads as "no" - the subscriber is then treated as dead (the error
path, whose sub-status/exit-code are now fail-closed) and the notify-only
stall watcher is not started.
Tests (F): the real submit (human job, unreachable broker, 3 s budget) with
a $PY that fails one decision - exit-code, sub-status, finalize-begin,
finalize-end; traceback, rc 127 or no answer - ends with 1 and the ERROR
line; the same run without a failure still ends with 3. cmd_wait with a
failing wait-rc ends with 1. A static guard keeps printed decisions off
raw $(...). At 56d0bfd: exit-code 255/255, finalize-begin 2/2, wait-rc
255/255, sub-status and finalize-end 127 for "gone"; the traceback and
no-answer cases there already ended with 1 (by set -e or by the empty
status mapping to 1) and fail only on the missing ERROR line.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HCGpJXhSZasJLU38ksDghe
This commit is contained in:
1 parent
f19f94099f
commit
a670ef53ee
2 files changed
+118
-14
No files matched your search
@@ -100,6 +100,32 @@ _decide() {
|
||||
PYTHONPATH="$SCRIPT_DIR/..${PYTHONPATH:+:$PYTHONPATH}" "$PY" -m lib_py.delegation.wrapper "$@"
|
||||
}
|
||||
|
||||
# A decision wrapper.py could not make (its module or $PY broken) ends the
|
||||
# wrapper with 1 (fail-closed): never with its own code, 0, or the empty answer
|
||||
# (`exit ""` is 255 on bash 3.2).
|
||||
_decision_failed() {
|
||||
echo "ERROR: the wrapper could not decide '$1' (lib_py.delegation.wrapper: $2); exiting 1 (fail-closed)" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# _decided <var> <subcommand> <args...>: a printed decision into $var.
|
||||
_decided() {
|
||||
local __var="$1" __out __rc=0
|
||||
shift
|
||||
__out="$(_decide "$@")" || __rc=$?
|
||||
[ "$__rc" -eq 0 ] || _decision_failed "$1" "rc $__rc"
|
||||
[ -n "$__out" ] || _decision_failed "$1" "no answer"
|
||||
printf -v "$__var" '%s' "$__out"
|
||||
}
|
||||
|
||||
# _exit_for <status>: exit with the wrapper exit code for a job status.
|
||||
_exit_for() {
|
||||
local code
|
||||
_decided code exit-code "$1"
|
||||
[[ "$code" =~ ^[0-9]+$ ]] || _decision_failed exit-code "answer '$code'"
|
||||
exit "$code"
|
||||
}
|
||||
|
||||
# _pub_cmd <extra...>: the publish command as one shell-quoted string.
|
||||
_pub_cmd() {
|
||||
local s
|
||||
@@ -361,12 +387,12 @@ EOF
|
||||
else
|
||||
echo "ERROR: subscriber died early (pid=$sub_pid, exit=$sub_exit, check $logf)" >&2
|
||||
local mapped_st
|
||||
mapped_st="$(_decide sub-status "$sub_exit" --job "$JOB_ID" --registry-dir "$REGISTRY_DIR")"
|
||||
_decided mapped_st sub-status "$sub_exit" --job "$JOB_ID" --registry-dir "$REGISTRY_DIR"
|
||||
finalize_unfinished "$JOB_ID" "$mapped_st" "$sub_exit" "subscriber"
|
||||
mapped_st="${FINAL_JOB_STATUS:-$mapped_st}"
|
||||
local logs_root="${DELEGATE_JOB_LOGS_DIR:-$WORKDIR/delegate_job_logs}"
|
||||
echo "$logs_root/$JOB_ID"
|
||||
exit "$(_decide exit-code "$mapped_st")"
|
||||
_exit_for "$mapped_st"
|
||||
fi
|
||||
fi
|
||||
sleep 0.2
|
||||
@@ -402,7 +428,7 @@ EOF
|
||||
echo "subscriber output:"; cat "$logf" || true
|
||||
|
||||
local job_status
|
||||
job_status="$(_decide sub-status "$sub_rc" --job "$JOB_ID" --registry-dir "$REGISTRY_DIR")"
|
||||
_decided job_status sub-status "$sub_rc" --job "$JOB_ID" --registry-dir "$REGISTRY_DIR"
|
||||
|
||||
finalize_unfinished "$JOB_ID" "$job_status" "$sub_rc" "direct"
|
||||
job_status="${FINAL_JOB_STATUS:-$job_status}"
|
||||
@@ -411,7 +437,7 @@ EOF
|
||||
# "Audit Logs"). Callers can scrape `tail -n1` to find it.
|
||||
local logs_root="${DELEGATE_JOB_LOGS_DIR:-$WORKDIR/delegate_job_logs}"
|
||||
echo "$logs_root/$JOB_ID"
|
||||
exit "$(_decide exit-code "$job_status")"
|
||||
_exit_for "$job_status"
|
||||
else
|
||||
# Implement loop/discuss orchestrator
|
||||
local iteration=1
|
||||
@@ -533,12 +559,12 @@ EOF
|
||||
else
|
||||
echo "ERROR: subscriber died early (pid=$sub_pid, exit=$sub_exit, check $logf)" >&2
|
||||
local mapped_st
|
||||
mapped_st="$(_decide sub-status "$sub_exit" --job "$JOB_ID" --registry-dir "$REGISTRY_DIR")"
|
||||
_decided mapped_st sub-status "$sub_exit" --job "$JOB_ID" --registry-dir "$REGISTRY_DIR"
|
||||
finalize_unfinished "$JOB_ID" "$mapped_st" "$sub_exit" "subscriber"
|
||||
mapped_st="${FINAL_JOB_STATUS:-$mapped_st}"
|
||||
local logs_root="${DELEGATE_JOB_LOGS_DIR:-$WORKDIR/delegate_job_logs}"
|
||||
echo "$logs_root/$JOB_ID"
|
||||
exit "$(_decide exit-code "$mapped_st")"
|
||||
_exit_for "$mapped_st"
|
||||
fi
|
||||
fi
|
||||
sleep 0.2
|
||||
@@ -571,7 +597,7 @@ EOF
|
||||
echo "subscriber output:"; cat "$logf" || true
|
||||
|
||||
local job_status
|
||||
job_status="$(_decide sub-status "$sub_rc" --job "$JOB_ID" --registry-dir "$REGISTRY_DIR")"
|
||||
_decided job_status sub-status "$sub_rc" --job "$JOB_ID" --registry-dir "$REGISTRY_DIR"
|
||||
|
||||
echo "Job role $display_role finished with status: $job_status"
|
||||
|
||||
@@ -590,7 +616,7 @@ EOF
|
||||
else
|
||||
local logs_root="${DELEGATE_JOB_LOGS_DIR:-$WORKDIR/delegate_job_logs}"
|
||||
echo "$logs_root/$JOB_ID"
|
||||
exit "$(_decide exit-code "$job_status")"
|
||||
_exit_for "$job_status"
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -615,7 +641,7 @@ EOF
|
||||
else
|
||||
local logs_root="${DELEGATE_JOB_LOGS_DIR:-$WORKDIR/delegate_job_logs}"
|
||||
echo "$logs_root/$JOB_ID"
|
||||
exit "$(_decide exit-code "$job_status")"
|
||||
_exit_for "$job_status"
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -805,10 +831,11 @@ _within_deadline() {
|
||||
finalize_unfinished() {
|
||||
local job_id="$1" job_status="$2" sub_rc="$3" phase="${4:-}"
|
||||
local target_sess="${5:-${current_session:-${target_session:-$AGENT_SESSION}}}"
|
||||
local step final evidence_until deadline out
|
||||
read -r step final evidence_until deadline <<<"$(_decide finalize-begin --job "$job_id" \
|
||||
local step final evidence_until deadline out begin end_rc=0
|
||||
_decided begin finalize-begin --job "$job_id" \
|
||||
--registry-dir "$REGISTRY_DIR" --status "$job_status" ${START_TIME:+--start-time "$START_TIME"} \
|
||||
--timeout "$TIMEOUT" --idle-timeout "$IDLE_TIMEOUT" --agent "$AGENT" --session "$target_sess")"
|
||||
--timeout "$TIMEOUT" --idle-timeout "$IDLE_TIMEOUT" --agent "$AGENT" --session "$target_sess"
|
||||
read -r step final evidence_until deadline <<<"$begin"
|
||||
case "$step" in
|
||||
keep) return 0 ;;
|
||||
done) FINAL_JOB_STATUS="$final"; return 0 ;;
|
||||
@@ -826,7 +853,8 @@ finalize_unfinished() {
|
||||
${START_TIME:+--start-time "$START_TIME"} --timeout "$TIMEOUT" --idle-timeout "$IDLE_TIMEOUT" \
|
||||
--sub-rc "$sub_rc" --phase "$phase" --type "$TYPE" --final "$final" --evidence-until "$evidence_until" \
|
||||
--dispatch-token "${_disp_tok:-}" --preflight-reason "${PREFLIGHT_REASON:-}" \
|
||||
--python "$PY" --scripts-dir "$SCRIPT_DIR/scripts" --watch-pid "$$" 3>&1 1>&4 4>&-)"; } 4>&1
|
||||
--python "$PY" --scripts-dir "$SCRIPT_DIR/scripts" --watch-pid "$$" 3>&1 1>&4 4>&-)" || end_rc=$?; } 4>&1
|
||||
[ "$end_rc" -eq 0 ] || _decision_failed finalize-end "rc $end_rc"
|
||||
FINAL_JOB_STATUS="$out"
|
||||
}
|
||||
|
||||
@@ -1039,7 +1067,8 @@ cmd_wait() {
|
||||
fi
|
||||
# Issue #33: either clock is a timeout (rc 2) to callers of `wait`, as before;
|
||||
# the subscriber's own log says which clock it was.
|
||||
rc="$(_decide wait-rc "$rc")"
|
||||
_decided rc wait-rc "$rc"
|
||||
[[ "$rc" =~ ^[0-9]+$ ]] || _decision_failed wait-rc "answer '$rc'"
|
||||
return "$rc"
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user