Repository navigation
fix(eval): #537 F01 — mot17.py --processes child failure fails the launch - #632
Merged
Merged
Conversation
A child that exited non-zero used to be printed as ❌ while the parent computed metrics from the remaining output and exited 0 (#538 D1-F01). The launcher moves to saccade.perception.eval.sequence_launcher so CPU tests can import it (mot17.py loads TensorRT at import). It is fail-fast: the first failure (non-zero exit, signal death, or a child that cannot be started) stops the launch; pending sequences are not started, running children get SIGTERM then SIGKILL after a grace period, and all are reaped. Each original failure keeps its sequence, code, stdout and stderr; launcher-stopped children are reported as terminated. The parent skips metrics and exits with the original failure's code (128+N for signal N, 127 when it could not be started); KeyboardInterrupt reaps the children and exits 130. SIGTERM to the parent is not handled. The exporter's mot17.py anchor statements are unchanged; the producer wiring contract now pins the launcher call and the exit-before-metrics order. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found on the GPU child_failure_probe: both children failed by themselves almost at once; the second one's thread had already set terminated_at and reported it as "terminated by the launcher (code 1)", hiding its own NoSectionError failure. Only a death by the launcher's own SIGTERM or SIGKILL is TERMINATED now; any other exit keeps the child's own status. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Records the fail-fast launch contract, exit codes (own code, 128+N, 127 start failure, 130 interrupt), the CPU fault-injection tests, 13 mutants, the before/after GPU child_failure_probe and the limits (parent SIGTERM unhandled). #538 D1-F01 row links to it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex r1 (NOT-READY) found, each confirmed with real children: - a worker exception (invalid UTF-8 under text=True) neither aborted the launch nor kept the failure: output is now read as bytes and decoded with replacement, and any error while supervising a child fails that sequence (launcher_error, exit 70 = EX_SOFTWARE), aborts the launch and kills and reaps the child; - "terminated" was decided from the return code alone, so a child that SIGKILLed itself in its SIGTERM handler looked stopped by the launcher: only a death by a signal the launcher actually sent counts now; - the abort check and Popen could interleave with another worker's failure, so a sequence started after the abort: both now run under one lock, and spawned_at / failed_at are taken under it; - the producer wiring contract was textual (`if False:` passed): it now checks the guard, its exit and the KeyboardInterrupt handler by AST; - the interrupt test could read an empty pid file: pid files are written atomically and an autouse teardown SIGKILLs this test's leftover children (matched by command line). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RVti7Gf7paqtEzERmZaCx
Launcher errors (exit 70), the spawn lock, signal ownership for "terminated", the AST wiring contract, 21 test cases, 19 + 5 mutants and the GPU probe on 4e917ca. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RVti7Gf7paqtEzERmZaCx
Codex r2 (NOT-READY), each reproduced with real children: - a signal was recorded as sent even when Popen.send_signal's own poll reaped the child and sent nothing: it is recorded only if the child was still unreaped after the call; - an exception from command_for, or a non-OSError from Popen, escaped the worker without aborting: both now fail the sequence as a launcher error (exit 70) and abort; OSError stays a start failure (127); - holding the spawn lock while building the command let a later failure win the lock and take precedence (127 instead of 5): the command is built outside the lock, and failures are ordered by when they were observed (failed_at) rather than when they set the abort (abort_at); - the wiring contract searched the AST recursively, so an exit nested under `if False:` passed: only direct statements count now; - the spawn-race test assumed B starts before A fails: it now forces both interleavings (slow Popen, slow command) and checks each outcome. Also pins that a pending sequence builds no command after the abort. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RVti7Gf7paqtEzERmZaCx
Codex r3 (NOT-READY): - a child's exit was only seen at pipe EOF, so a descendant holding its stdout/stderr (e.g. torch compile workers) hid the exit and let a later failure decide the code: output now goes to temporary files and the child is supervised with wait(); - the spawn message could raise (BrokenPipeError) outside every guard: one guard now covers the whole worker; any error kills and reaps its child and aborts the launch as a launcher error (70); - tests: the spawn-race "command" scenario accepts both sound outcomes; the signal-death and start-failure tests synchronise A on B; the grace test measures SIGTERM->SIGKILL >= grace_s; new cases for a descendant holding the output and for a failing spawn message. Not changed (documented in §25): a child that handles the launcher's SIGTERM, cleans up and re-raises SIGTERM is counted as stopped by the launcher; that is the usual graceful-shutdown idiom, and a death by the same signal for an unrelated reason after the launcher sent it cannot be told apart. It only affects how an already-aborted child is labelled. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RVti7Gf7paqtEzERmZaCx
Codex r4 (NOT-READY): - capture-file creation (e.g. EMFILE) ran outside the worker guard: the files are now created inside it, so the error aborts as a launcher error; - a failure was timestamped and aborted only after its output was read, so a slow or large read let a later failure win: classify, timestamp and abort first, then read; - reading the capture with seek(0)+read moved the file offset shared with every descendant that inherited it, so a late writer could overwrite the child's own output: read with os.pread from offset 0; - the wiring contract accepted a launcher call nested under a condition: the try body must be exactly the `results = ...run_sequence_children(` assignment; - the grace test measured the launcher's return: it now records the actual send_signal times. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RVti7Gf7paqtEzERmZaCx
Codex r5 (NOT-READY): - a capture read error after a child's failure was observed (e.g. EIO on stderr) went to the worker guard and replaced the child's failure with a launcher error (70): collecting output now never raises; the read error is reported inside that stream and the child's code, timestamp and readable output are kept; - the grace period started before SIGTERM was actually sent, so a slow send shortened it: it is timestamped after the send; the grace test slows the SIGTERM send by 1.3 s; - the wiring contract accepted `run_sequence_children(...)[:0]`: the assignment value must be exactly that call. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RVti7Gf7paqtEzERmZaCx
Codex r6 (NOT-READY): - reading a capture followed a moving EOF, so a descendant that kept appending blocked collection after the child was reaped: reads stop at the size seen on entry; - a launcher error was timestamped and aborted only after a possibly slow kill/reap, so a later child failure won: it is recorded and aborts first, then the child is cleaned up; - a read error discarded the bytes already read: the readable prefix is kept and the error marker appended (_read itself never raises now); - the wiring contract did not check the launcher's arguments (`seq_list[:0]` passed): seq_list, max_workers=args.processes and env=child_env are pinned; - the signal-reap test assumed B starts before A fails: A waits for B. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RVti7Gf7paqtEzERmZaCx
…x r7) Codex r7 (NOT-READY): - printing the captured child output to a stdout whose reader went away raised BrokenPipeError before sys.exit(exit_code), so the parent exited 1 instead of the original code: the code is decided first, then sequence_launcher.report()/report_interrupt() print best effort and point a broken stream at /dev/null (so the interpreter's flush at exit cannot turn it into 120 either); the guard body is just sys.exit(exit_code); - the wiring contract now pins the command-builder lambda, the report call between the decision and the guard, and the interrupt handler body; - NIT: the precedence test makes B fail only after A has exited. Test hardening found while stress-testing (4 concurrent runs): the interrupt harness inherited SIGINT as ignored when pytest itself was started in the background, so the KeyboardInterrupt never happened; it installs Python's default SIGINT handler (a foreground run). The launcher-error test now has B fail only once A is running. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RVti7Gf7paqtEzERmZaCx
…x r8) Codex r8 was READY-WITH-CHANGES with two test gaps: dropping the explicit env in Popen (losing parent_claim_environ) passed, and a child exiting 0 by itself after the abort could have been labelled terminated. A real child now echoes an env var supplied only through `env=`, and the voluntary-exit test runs with codes 0 (OK) and 4 (FAILED). Both targeted mutants are caught. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RVti7Gf7paqtEzERmZaCx
Fail-fast contract after Codex r1-r8: spawn lock, signal ownership for "terminated", file capture read with pread up to the size on entry, worker-wide guard (launcher errors, exit 70), failures recorded before output collection and cleanup, best-effort reporting after the exit code is decided, exit-code table; 39 CPU cases, 36 + 10 mutants, the GPU failure probe (4 repeats) and the unchanged success path at fc93509; the review history and the one deliberately unchanged item. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RVti7Gf7paqtEzERmZaCx
Codex r9 (NOT-READY): a parent started with `1>&-` or `2>&-` has sys.stdout / sys.stderr None; the unconditional flush in the best-effort reporting raised AttributeError and the parent exited 1 instead of the decided 5 (or 130). Reporting now absorbs any exception, not only OSError, and a stream that cannot be flushed (None included) is skipped or pointed at /dev/null. Tests run real harnesses with fd 1 or fd 2 closed, and with the reader of both pipes gone; the NITs widen two scheduling margins in the grace and precedence tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RVti7Gf7paqtEzERmZaCx
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RVti7Gf7paqtEzERmZaCx
…im (Codex r10) Codex r10 was READY-WITH-CHANGES with two MINORs: no test checked what is printed on failure (dropping the child's stdout/stderr from describe() passed), and §25 claimed the interleaving tests use no fixed delays while the precedence test keeps a 1.5 s margin and the grace test an 8 s upper bound. Both addressed; the describe mutant is caught. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RVti7Gf7paqtEzERmZaCx
Fresh capture on the implementation head 1ae9f1f (clean tree, own build/runtime_identity in the capture worktree, --run-probe --require-complete under the gpu0 lease): - implementation: 4ad5ae2c7f2a7d2c -> 11204b8a3246374f; 317 -> 318 files: src/saccade/perception/eval/sequence_launcher.py added (the fail-fast --processes launcher), scripts/eval/mot17.py changed (calls it). - decision_surface, environment, identity_semantics, runtime_inputs: recomputed, unchanged. - probe: rerun, not reused; digest 2dabed0b... unchanged, build witness unchanged. Probe equality is not an equivalence claim; equivalence stays unproven. The outgoing canonical is archived as runtime_identity.2026-10-25.4ad5ae2c7f2a7d2c.json (byte-identical to origin/main's, sha256 a522eb76...). check_runtime_identity_staleness.py --mode attested exits 0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RVti7Gf7paqtEzERmZaCx
This was referenced Oct 25, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
#537 F01:
mot17.py --processes子程序失敗不再回傳 exit 0來源:#538 D1 的 D1-F01(owner 裁決:#537 擁有修復契約)。完整契約與證據:
docs/architecture/failure_degradation_537.md§25。修復前(
main@4380c2fa,GPU 重現)D1 的
child_failure_probe(兩個 sequence 的seqinfo.ini沒有[Sequence]):兩個子程序都failed with code 1(NoSectionError),parent 印Completed all sequences後 exit 0。行為(fail-fast,
src/saccade/perception/eval/sequence_launcher.py)pread讀到進入時的大小為止,讀取錯誤保留已讀部分。只有死於 launcher 實際送出的 signal 才標terminated。KeyboardInterrupt回收子程序後 → 130。launcher 放在
src/是因為mot17.pyimport 時載入 TensorRT,CPU CI 無法 import。exporter 依賴的入口敘述未變(test_resolved_shipping_config.py通過)。驗證
tests/unit/eval/test_sequence_launcher_537.py,44 cases,真實子程序;每案確認沒有未回收的直接子程序):涵蓋要求的六種情境,加上多個子程序同時失敗時最早者決定 exit code且不留下孤兒,以及 Codex 審查找到的各種競態與錯誤路徑。4 個 pytest 同時執行也穩定。test_mot17_producer_wiring.py以 AST 檢查 launcher 區塊的直接敘述(呼叫與參數、中斷 exit 130、先決定 code 再回報、if exit_code != 0: sys.exit(exit_code)、metrics 最後)。mot17.pymutant 全部攔住;worktree 檔案 SHA-256 前後相同。.venv/build//models/,只換 checkout):fc935095:exit 1,兩個子程序各自保留NoSectionError,沒有誤標terminated,不算 metrics;連續 4 次相同。--processes 2跑MOT17-09/10-SDP,before/after 都 exit 0,結果檔逐位元組相同。fc935095..1ae9f1f6只改了回報方式、測試與文件。terminated;這不影響 exit code,也不遮住原始失敗。明細見 §25「審查過程的修正」。1ae9f1f6:6107 passed,只有 4 個 lagging-identity 測試失敗(test_runtime_identity_staleness×2、test_research_lock×2),由 docs(runtime): republish the runtime coordinate for #537 F01 (stacked on #632) #633 修正。Runtime identity
mot17.py與sequence_launcher.py是decision_relevant;依 runbook §3.2 由 stacked #633 原子落地。implementation4ad5ae2c…→11204b8a…(317 → 318 檔),其他軸不動,probe 重跑 digest 相同,equivalence=unproven。不在範圍
tracking 數值、online qualification、F06(另開 PR)、#538 的任何修復、
mot17_all_sdp.py/run_eval_concurrent的失敗傳播。證據(repo 外)
~/.local/state/saccade/evidence/537-f01/20261025T145150/(README.md、SHA256SUMS)🤖 Generated with Claude Code
https://claude.ai/code/session_019RVti7Gf7paqtEzERmZaCx