fix(environments): surface SDK backend errors instead of a silent exit 1 - #152
Conversation
_ThreadedProcessHandle stored the exec exception on self._error and wrote nothing to the stdout pipe. self._error is never read anywhere in the codebase, so a dead Modal sandbox, connection reset, or coroutine timeout reached the agent as a bare 'exit 1' with EMPTY output, indistinguishable from a command that legitimately failed. Modal-backed kanban lanes responded by parking cards and asking a human to 'reset the execution backend'. The failures are transient: since the 2026-07-24 migration, 36 distinct tasks blocked with a 'Modal unavailable' message and later completed on a retry, ~141 minutes burned. Write the exception into the same pipe the success path uses, prefixed [backend error]. No API change, no new config. Tests assert all three paths: backend exception is now visible, successful output is byte-identical, and a genuine non-zero command exit is NOT relabelled as a backend error. Verified the new test FAILS against the unpatched code (assert '' != ''). Patch note: ~/.hermes/plans/hermes-patches/modal-exec-error-visibility.md
Self-reviewAuthor reviewing his own PR, so treat the endorsement accordingly. I did try to break it rather than confirm it. Verdict: ship itThe change is 3 lines of behavior in the What I verified rather than assumed
Honest limitations
One correction to my own PR descriptionThe description cites a dev run that burned 214.7 hours (~$82). That number is wrong and I should not have shipped it. Checking That does not change this PR's rationale (the 36-tasks-recovered-on-retry figure comes from run outcomes, not durations), but it does mean DB run durations on this board are not a safe proxy for wall-clock compute wherever run rows can leak. Same caveat applies to #153, where I've flagged it more seriously. Not blocking, worth knowing
|
Modal/SDK backend errors were invisible to the agent
Branch:
fix/modal-exec-error-visibilityFile:
tools/environments/base.py(_ThreadedProcessHandle._worker)Date: 2026-07-25
Symptom
Modal-backed kanban lanes kept parking tasks with messages like:
Eric's read of the board was that cards were "exiting the wrong way."
Root cause
_ThreadedProcessHandleis the adapter every SDK-backed environment (Modal,Daytona) runs commands through. Its worker did this:
The exception was stored on
self._errorand never read anywhere in thecodebase (verified: the only two references are the declaration and this
assignment). Nothing was written to the stdout pipe, so the drain thread
collected an empty string.
Net effect: a dead sandbox, a connection reset, or a coroutine timeout reached
the agent as a bare
exit 1with empty output — byte-for-byteindistinguishable from a command that legitimately failed. With no error text
to reason about, the worker concluded the whole backend was down and blocked
the card asking a human to reset it.
Why this was expensive
The failures are transient. Same query, three attempts: two succeeded, one
hung. Task
t_fbe02103blocked four consecutive times with "Modal shellunavailable" and then completed normally on the fifth run, 23 minutes of
real work.
Since the 2026-07-24 migration: 24 runs blocked this way, ~141 minutes of
wall-clock burned, and 36 distinct tasks that blocked on a "Modal
unavailable" message later completed on a retry. Every one of those was a
human ping for a problem that resolved itself.
Fix
Write the exception into the same stdout pipe the success path uses, prefixed
[backend error]:Three lines of behavior, no API change, no new config. The agent now sees
[backend error] RuntimeError: modal sandbox died: connection resetand canretry instead of escalating.
Verification
rc=1, stdout=''.rc=1, stdout='[backend error] RuntimeError: modal sandbox died: connection reset'.test_backend_exception_is_written_to_stdoutfails withassert '' != '', proving the test catches the real bug rather than just passing.rc=0, exact stdout preserved).rc=2,"backend error"absent) — a failinglsmust not be mislabelled as infrastructure.569 passed, 12 skippedacross environment/terminal/modal/daytona tests.test_concurrent_writes_never_tear_the_snapshot) fails identically on unmodifiedlive-config; confirmed not caused by this change.Scope note
This does NOT fix the underlying Modal flakiness (sandboxes do intermittently
die). It makes the failure legible so the agent can retry and so the next
person debugging this reads a cause instead of guessing. The flakiness itself
is worth a separate look — the timeout path in
modal.py::_run_bashwrapsworker.run_coroutine(..., timeout=timeout + 30)and a hang there is whatsurfaces as the empty exit 1.