fix(agents): a turn that failed closes on a failed rather than on nothing - #39
Open
LukeLIN-web wants to merge 1 commit into
Open
fix(agents): a turn that failed closes on a failed rather than on nothing#39LukeLIN-web wants to merge 1 commit into
failed rather than on nothing#39LukeLIN-web wants to merge 1 commit into
Conversation
LukeLIN-web
force-pushed
the
fix/a-failed-turn-says-so
branch
from
September 3, 2026 15:40
c259a86 to
a4e2846
Compare
…othing A turn ends on exactly one event -- `result` where it landed, `failed` where it did not. That is what `Event` says and what the reference documents, and dsh is the only backend it was true of: everywhere else a turn that failed by raising closed on neither, so whatever was watching the agent saw `begins`, and then `ends`, with nothing between them. Which is precisely what it sees when a turn answers with nothing. So a round that failed and a round with nothing to say were shown as the same thing -- and `suppress=True`, which every loop is written with, hands the flow the same `""` or `None` for both, so the flow cannot tell them apart either. An agent whose every turn failed then leaves no mark on anything that reads the turns: a turn that failed is never asked for a session id, so the run's record names no session for it and a trace of that run has nothing of it to collect. Two agents declared and one of them in the trace, and a reviewer whose CLI was never signed in reading exactly like a reviewer that agreed -- for as long as the loop is left going. d410372 put the reason on stderr for a run nothing is watching. This is the same reason for a run something is watching, which reads the turns rather than stderr: the interface, the status column, and anything else hung on the agent. Said once, since a backend that closed the turn on a `failed` of its own must not be made to say it twice. What `suppress` answers with does not change, so every flow written against it goes on working. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qm4L3SmRKKPB5WaX8GX62f
LukeLIN-web
force-pushed
the
fix/a-failed-turn-says-so
branch
from
September 3, 2026 15:44
a4e2846 to
3b1e356
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.
Rebased onto
mainat25c9bd3.d410372("a suppressed failed turn leaves its reason onstderr") landed the half of this that a run nothing is watching needed. This is the other
half: the run something is watching.
What happens now
Eventsays a turn closes on exactly one of two things:docs/reference/agents.mdsays the same. dsh is the only backend it is actually true of.Everywhere else a turn fails by raising out of
_stream, and_turninglets that through to itsfinally— so a watcher getsbegins, thenends, and nothing between them.That is byte-for-byte what a watcher sees when a turn answers with nothing. A round that failed
and a round with nothing to say are shown as the same thing, and
suppress=True— which everyloop is written with — hands the flow the same
""orNonefor both, so the flow cannot tellthem apart either.
Everything that reads a run reads the turns rather than stderr: the interface, the status column,
anything hung on
agent.watch().d410372reaches none of those, because it fires only when_watchersis empty.Why it matters
Nothing else records it either. A turn that failed is never asked for a session id, so the
session is never opened, so the epic writes no
openedfor that agent andhmz trace collecthas nothing of it to gather.
An agent whose every turn failed is therefore invisible in every direction at once: the flow is
handed nothing, the watcher is shown nothing, the run's record names nothing, the trace has
nothing.
Found on
official/rlar, whose stopping condition is the reviewer's judgement rather than abudget:
The reviewer's CLI was not signed in. Every review turn failed, every round got
None, theprompt went round unchanged, and the run's own journal declared two agents and carried
openedfor one of them. The actor kept working, unreviewed, until somebody stopped it by hand.
The failure the adapter raised on, reproduced — it reads this correctly; the signal was lost
above it:
The change
SessionBase._turningsays the failure before it raises it — afailedevent carryingFailed.__str__, which already carries the CLI's own sentence alongside the exit status:begins/endsbracket, in place of theresulta turn that landed ends on;failedof its own is not made to sayit twice;
tests/agents/test_dsh.pypins that for dsh and still passes.No stderr write is added —
d410372has that, and duplicating it would print the reason twice.Nothing about what
suppressanswers with changes, so every flow written against it goes onworking.
Stoppedis not aCalledProcessErrorand is not caught here; anUnrecoverableisone, and is now said on its way past.
Docs: the
suppressparagraph indocs/reference/agents.md, the one indocs/user/stopping.md,and a new
docs/user/troubleshooting.mdentry — One of my agents is not in the trace at all —since "two agents declared, one in the trace" reads as a broken trace and is not one.
Not in this change
official/rlar. That flow lives inhumanfia/flowverse. A loop held to ajudgement rather than to a ceiling is still a loop with no ceiling; this only makes the silence
audible.
Noneundersuppress— an answer that was not the shape itwas asked for — still says nothing to a watcher. That one is a turn that landed: it closes on
result, and the shape is read outside the bracket, so saying it belongs where the answer isread rather than here. Happy to fold it in if you would rather have both.
Checks
uv run pre-commit run --all-filesanduv run pytestboth pass. No flow-facing API changed, sohumanfia/flowverseneeds no companion change.🤖 Generated with Claude Code
https://claude.ai/code/session_01Qm4L3SmRKKPB5WaX8GX62f