From 0ea0c53e892ca4cb4ecc67eb7cb8dd11d6a8c8cd Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Mon, 14 Sep 2026 21:16:05 -0700 Subject: [PATCH] fix: record a known exit status when the reader's wait() raises The early-EOF reaper made _finish_reader return without publishing when wait() raises, so the session stays tracked for later reconciliation. That is right for the pipe path (_reconcile_local_exit can still reap via session.process), but PTY sessions have no session.process: when ptyprocess.wait raises (waitpid ECHILD after isalive() already reaped the child) the exitstatus is known, yet poll() reported "running" forever. Only leave the session tracked when exit_code() is still None; otherwise record the known status and finish as before. Review finding: PTY session whose pty.wait raises stays in _running forever (fail-open regression vs main) --- tests/tools/test_process_registry.py | 20 ++++++++++++++++++++ tools/process_registry.py | 8 ++++++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/tests/tools/test_process_registry.py b/tests/tools/test_process_registry.py index 9bda3a1656..d057a36c1e 100644 --- a/tests/tools/test_process_registry.py +++ b/tests/tools/test_process_registry.py @@ -361,6 +361,26 @@ def test_failed_reader_wait_does_not_publish_false_completion(registry, monkeypa assert moved == [] +def test_failed_reader_wait_still_records_known_exit_status(registry, monkeypatch): + """A PTY child reaped by isalive() has its status; a raising wait must not lose it.""" + session = _make_session(sid="proc_pty_wait_failed") + moved = [] + monkeypatch.setattr(registry, "_move_to_finished", lambda _s: moved.append(_s.id)) + + registry._finish_reader( + session, + MagicMock(decode=MagicMock(return_value="")), + lambda _text: None, + "PTY", + MagicMock(side_effect=OSError("waitpid ECHILD")), + lambda: 9, + ) + + assert session.exited is True + assert session.exit_code == 9 + assert moved == [session.id] + + # ========================================================================= # Incremental UTF-8 decoding across chunk boundaries # (ported from openclaw/openclaw#112325) diff --git a/tools/process_registry.py b/tools/process_registry.py index 3eb2c4d3f4..8330437b45 100644 --- a/tools/process_registry.py +++ b/tools/process_registry.py @@ -1178,8 +1178,12 @@ class ProcessRegistry(ProcessCheckpointMixin): try: wait() except Exception as e: - logger.warning("%s wait failed; leaving process tracked: %s", label, e) - return + # A PTY child reaped by isalive() already has its exitstatus; only an + # unknown status must stay tracked for later reconciliation. + if exit_code() is None: + logger.warning("%s wait failed; leaving process tracked: %s", label, e) + return + logger.warning("%s wait failed; recording known exit status: %s", label, e) self._finish_exited(session, exit_code()) @staticmethod