Skip to content

Record a stopped investigation when its reader goes away - #54

Merged
shipmill-romamo[bot] merged 2 commits into
mainfrom
fix/44-investigate-stopped
Oct 9, 2026
Merged

shipmill-romamo[bot] merged 2 commits into
mainfrom
fix/44-investigate-stopped

Conversation

@shipmill-romamo

Copy link
Copy Markdown
Contributor

Summary

Since agent investigate streams (#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 status stopped, and the command exits non-zero.

Where the broken pipe surfaces: treaty's stream writer (emit_stream, under contextlib.closing) gets the BrokenPipeError, closes the stream, and the handler's generator gets GeneratorExit at its yield. treaty then ends the run in output_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) catches GeneratorExit and returns the investigation so far, ended stopped (Python 3.13+ close() returns it): steps, turns, tokens, trace, empty answer, no finding, as model-unavailable does
  • agent_investigate (sdk/src/cloudfall/app.py) catches only GeneratorExit, closes the events, saves the record, sets cloudfall.app.stream_stopped, and re-raises; nothing more is written to the dead stdout
  • New exit code 94 INVESTIGATION_STOPPED (after 92 MODEL_UNAVAILABLE and 93 INVESTIGATION_INCOMPLETE), declared on the command. Because treaty gives the handler no say in a closed output's exit code, cloudfall.cli.main/run turn treaty's 0 or 141 into 94 when stream_stopped is set. A stream closed by SIGINT/SIGTERM or a timeout is also recorded stopped but keeps treaty's exit code (130, 143, timeout)
  • agent-investigation.schema.json: stopped added to the status enum (additive)
  • --no-stream is unchanged: the stream is drained before anything is written. The Record the investigation when the model endpoint fails partway #32 path (endpoint fails partway) keeps model-unavailable and exit 92
  • Nothing in cloudfall why or elsewhere matches on the investigation status (only evaluations/nemotron-incident/run.py passes it through), so nothing else needed to describe stopped

Test plan

  • New 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 status stopped, turns 2, the step and its decision on disk, and a stderr with no BrokenPipe or traceback (only treaty's JSON UNTRUSTED_CONTENT warning line). It fails on main with exit 0
  • New test_closing_the_events_returns_the_investigation_so_far_stopped: unit test of the generator's close() return
  • uv run ruff check ., uv run mypy, uv run pytest (438 passed), uv run cloudfall config validate --project config/examples, cloudfall-engine inventory render all pass locally
  • The Ansible syntax checks and ansible-lint could not run in the local session (Ansible refuses non-blocking stdio there); this PR touches no Ansible file, and CI runs them

Fixes #44

🤖 Generated with Claude Code

shipmill-romamo Bot and others added 2 commits October 9, 2026 17:44
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
shipmill-romamo Bot force-pushed the fix/44-investigate-stopped branch from df51e59 to 70017de Compare October 9, 2026 14:47
@shipmill-romamo
shipmill-romamo Bot merged commit aa769e7 into main Oct 9, 2026
1 check passed
@shipmill-romamo
shipmill-romamo Bot deleted the fix/44-investigate-stopped branch October 9, 2026 14:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

agent investigate loses the investigation record when stdout closes mid-run

0 participants