fix(issue-17): a failed yes/no decision is not a "no" (review codex R1)

subscriber-up and stall-watch answered only by exit code, so a call that
could not run (module error rc 1, missing interpreter 127) read as "no":
with a subscriber that had ended 0, the error path mapped 0 to completed
and the submit ended with 0; a failed stall-watch silently skipped the
watcher.

Both now take --print: the answer is printed as yes/no and the exit code
is 0 for either. The wrapper asks with --print through _decided_yes, so a
nonzero exit, no answer or anything else is _decision_failed (ERROR line,
exit 1). Without --print the rc contract is unchanged (yes 0, no 1).

Tests: subscriber-up (direct and loop/discuss call sites) and stall-watch,
each failing by traceback, rc 127 or no answer with a subscriber that
ended 0, end the submit with 1 and the ERROR line - at 8241bd5 eight of the
nine ended with 0 (the loop/no-answer one ended with 1 through the loop's
own result and failed only on the ERROR line); yes and no keep their
branches in both paths; --print unit table; the static guard forbids an
rc-read yes/no decision.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HCGpJXhSZasJLU38ksDghe
This commit is contained in:
GodopuandClaude Opus 5.5 committed 2026-10-08 09:26:41 +09:00
1 parent 541c3febe8
commit 65b78b6ec4
3 files changed
+77 -13

No files matched your search

+19 -5
View File
@@ -5,12 +5,16 @@ injects; everything it used to decide in bash is here, and it branches only on
what a subcommand prints or returns:
sub-status <rc> --job J --registry-dir R subscriber rc -> job status (was _map_sub_rc)
subscriber-up <rc> exit 0: a subscriber that ended before
subscriber-up <rc> [--print] exit 0: a subscriber that ended before
SUBSCRIBED still lets the job proceed
exit-code <status> the wrapper's exit code (was _rc_for, with
MAM_DELEGATE_FAIL_CLOSED=0 -> 0)
wait-rc <rc> `wait`'s rc (either clock is a timeout, 2)
stall-watch <session> <agent> exit 0: start the stall watcher (Issue #30)
stall-watch <session> <agent> [--print] exit 0: start the stall watcher (Issue #30)
The two yes/no decisions answer by exit code (0 yes, 1 no); with --print they
print `yes`/`no` and exit 0, so a failure to decide (any other exit code, or
no answer) cannot pass for "no". The wrapper uses --print.
finalize-begin / finalize-end ... finalize_unfinished, split around the one
herdr diagnostic probe bash still runs
@@ -294,12 +298,15 @@ def _parser() -> argparse.ArgumentParser:
s.add_argument("rc", type=int)
s.add_argument("--job", required=True)
s.add_argument("--registry-dir", required=True)
sub.add_parser("subscriber-up").add_argument("rc", type=int)
s = sub.add_parser("subscriber-up")
s.add_argument("rc", type=int)
s.add_argument("--print", action="store_true", help="print yes/no and exit 0")
sub.add_parser("exit-code").add_argument("status")
sub.add_parser("wait-rc").add_argument("rc", type=int)
s = sub.add_parser("stall-watch")
s.add_argument("session")
s.add_argument("agent")
s.add_argument("--print", action="store_true", help="print yes/no and exit 0")
for name in ("finalize-begin", "finalize-end"):
s = sub.add_parser(name)
s.add_argument("--job", required=True)
@@ -325,18 +332,25 @@ def _parser() -> argparse.ArgumentParser:
return p
def _yes_no(answer: bool, printed: bool) -> int:
if printed:
print("yes" if answer else "no")
return 0
return 0 if answer else 1
def main(argv: Optional[List[str]] = None) -> int:
a = _parser().parse_args(argv)
if a.cmd == "sub-status":
print(sub_status(a.rc, a.registry_dir, a.job))
elif a.cmd == "subscriber-up":
return 0 if subscriber_up(a.rc) else 1
return _yes_no(subscriber_up(a.rc), a.print)
elif a.cmd == "exit-code":
print(exit_code(a.status))
elif a.cmd == "wait-rc":
print(wait_rc(a.rc))
elif a.cmd == "stall-watch":
return 0 if stall_watch(a.session, a.agent) else 1
return _yes_no(stall_watch(a.session, a.agent), a.print)
elif a.cmd == "finalize-begin":
print(finalize_begin(a))
else:
@@ -118,6 +118,18 @@ _decided() {
printf -v "$__var" '%s' "$__out"
}
# _decided_yes <subcommand> <args...>: a yes/no decision (asked with --print,
# so "no" is an answer and a failed call is not): returns 0 for yes, 1 for no.
_decided_yes() {
local yn
_decided yn "$@" --print
case "$yn" in
yes) return 0 ;;
no) return 1 ;;
esac
_decision_failed "$1" "answer '$yn'"
}
# _exit_for <status>: exit with the wrapper exit code for a job status.
_exit_for() {
local code
@@ -381,7 +393,7 @@ EOF
else
local sub_exit=0
wait "$sub_pid" 2>/dev/null || sub_exit=$?
if _decide subscriber-up "$sub_exit"; then
if _decided_yes subscriber-up "$sub_exit"; then
sub_ready=1
break
else
@@ -553,7 +565,7 @@ EOF
else
local sub_exit=0
wait "$sub_pid" 2>/dev/null || sub_exit=$?
if _decide subscriber-up "$sub_exit"; then
if _decided_yes subscriber-up "$sub_exit"; then
sub_ready=1
break
else
@@ -715,7 +727,7 @@ reap_subscriber() {
start_stall_watch() {
local session="$1" agent="$2" tok="${3:-}" att="${4:-}"
STALL_PID=""
_decide stall-watch "$session" "$agent" || return 0
_decided_yes stall-watch "$session" "$agent" || return 0
local args=(--job "$JOB_ID" --registry-dir "$REGISTRY_DIR" --target "${session#herdr:}"
--agent "$agent" --herdr-session "${HERDR_SESSION_NAME:-}" --watch-pid "$$")
[ -n "$tok" ] && args+=(--expect-dispatch-token "$tok")