Repository navigation
Record a stopped investigation when its reader goes away - #54
Merged
Merged
Conversation
Since agent investigate streams (PR #39), a reader closing stdout mid-run left the decisions the model proposed without the investigation that made them, and the command exited 0: treaty closes the handler's generator on a broken pipe and ends a stream that delivered an event with exit 0. investigation_events now returns the investigation so far, ended with the new status stopped, when it is closed, and the handler saves it before re-raising GeneratorExit, so nothing more reaches the dead stdout. treaty gives the handler no say in the exit code of a closed output, so the handler sets cloudfall.app.stream_stopped and cloudfall.cli turns treaty's 0 or 141 into 94 INVESTIGATION_STOPPED. A signal or a timeout keeps its own exit code; --no-stream is unchanged. Fixes #44 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A SIGINT while agent investigate waits on the model exits 130 with no investigation record: treaty defers closing the stream until the handler's next() returns, and the process exits first. The CHANGELOG said such a run is recorded stopped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
shipmill-romamo
Bot
force-pushed
the
fix/44-investigate-stopped
branch
from
October 9, 2026 14:47
df51e59 to
70017de
Compare
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.
Summary
Since
agent investigatestreams (#39), a reader that closes stdout mid-run (cloudfall agent investigate ... | head -2) left the decisions the model proposed without the investigation that made them, and the command exited 0. This implements D-3: the record is saved with a new statusstopped, and the command exits non-zero.Where the broken pipe surfaces: treaty's stream writer (
emit_stream, undercontextlib.closing) gets theBrokenPipeError, closes the stream, and the handler's generator getsGeneratorExitat itsyield. treaty then ends the run inoutput_closed(): exit 0 when an event was already delivered, 141 before any, with no hook for the handler.investigation_events(sdk/src/cloudfall/investigation.py) catchesGeneratorExitand returns the investigation so far, endedstopped(Python 3.13+close()returns it): steps, turns, tokens, trace, emptyanswer, nofinding, asmodel-unavailabledoesagent_investigate(sdk/src/cloudfall/app.py) catches onlyGeneratorExit, closes the events, saves the record, setscloudfall.app.stream_stopped, and re-raises; nothing more is written to the dead stdoutINVESTIGATION_STOPPED(after 92MODEL_UNAVAILABLEand 93INVESTIGATION_INCOMPLETE), declared on the command. Because treaty gives the handler no say in a closed output's exit code,cloudfall.cli.main/runturn treaty's 0 or 141 into 94 whenstream_stoppedis set. A stream closed by SIGINT/SIGTERM or a timeout is also recordedstoppedbut keeps treaty's exit code (130, 143, timeout)agent-investigation.schema.json:stoppedadded to the status enum (additive)--no-streamis unchanged: the stream is drained before anything is written. The Record the investigation when the model endpoint fails partway #32 path (endpoint fails partway) keepsmodel-unavailableand exit 92cloudfall whyor elsewhere matches on the investigation status (onlyevaluations/nemotron-incident/run.pypasses it through), so nothing else needed to describestoppedTest plan
test_a_reader_closing_stdout_mid_run_leaves_the_record_stopped: runs the real CLI (cloudfall.cli:run) in a subprocess against a local fake model endpoint; the reader takes the first turn and its call, then closes stdout, and only then does the endpoint answer turn 2. Checks exit 94, a saved record with statusstopped, turns 2, the step and its decision on disk, and a stderr with noBrokenPipeor traceback (only treaty's JSONUNTRUSTED_CONTENTwarning line). It fails onmainwith exit 0test_closing_the_events_returns_the_investigation_so_far_stopped: unit test of the generator'sclose()returnuv run ruff check .,uv run mypy,uv run pytest(438 passed),uv run cloudfall config validate --project config/examples,cloudfall-engine inventory renderall pass locallyansible-lintcould not run in the local session (Ansible refuses non-blocking stdio there); this PR touches no Ansible file, and CI runs themFixes #44
🤖 Generated with Claude Code