Tell a channel that a turn stopped without answering - #1172
Merged
Conversation
A harness that fails mid-turn does not necessarily error its session. An interactive one prints the failure into its viewport and returns to its composer, which from outside is a session sitting at "awaiting input" — the same shape as a turn that finished, differing only in having produced nothing. Both service wait loops only ever concluded on a reply, an errored session, or the delivery TTL, so that case fell through all three and polled for the full half hour before reporting a timeout that had not happened. A live Slack thread spent that half hour showing "Working on it…" while the harness had drawn "stream disconnected before completion" ten minutes in. Watch the session instead: idle, unqueued, not parked at an approval, and appending nothing, continuously, means the turn is over. Redraws do not count as progress or a harness that draws its own cursor would never look stopped, and a turn never seen running gets a longer grace so a slow start is not mistaken for a failure. When one does fail, quote the harness. The error it drew is the useful part — it says whether to send the message again — so it is recovered from the terminal tail and reported instead of a generic timeout. Also give the placeholder an elapsed time past the first minute. One that has said the same words for twenty minutes reads as abandoned, which is the impression the affordance exists to prevent.
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.
What broke
A Slack thread showed
_Working on it…_and a 👀 reaction for half an hour. The session behind it had failed ten minutes in, with this on its screen:That string is drawn by the codex binary; it never becomes anything the daemon can read. The harness printed it, returned to its composer, and the session went to
awaiting_input— notErrored.Both service wait loops conclude on exactly three things: the reply,
SessionState::Errored, or the delivery TTL. A turn that stops without answering is none of them, so it fell through all three:wait_for_explicit_replykept polling for a tagged reply that was never coming.wait_for_finalhas the same hole from the other side — it seesready, finds no assistant reply, sees a state that isn'tErrored, and loops again.Half an hour later the thread got
_The turn ended without an answer: service turn timed out_— naming a timeout that had not happened, while the actual cause had been legible the whole time.What this changes
A turn that has stopped is finished, whether or not it answered. A new
TurnWatchfolds each poll: idle, no queued input, not parked at an approval, and nothing new appended — continuously — means the turn is over. Two things keep it from misfiring:Approval parking is load-bearing and already tracked separately, so a turn waiting on a human keeps reading as waiting.
Failures are reported in the harness's own words.
harness_error.rsrecovers the last error the harness drew from a bounded PTY tail. It treats cursor motion and erasure as line breaks — a TUI does not end lines with newlines, and without that the banner, the composer, and the model footer collapse into one run-on string. The Slack message becomes:Best-effort by construction, and only ever consulted for a turn already known to have failed — a wrong guess costs a slightly-off sentence, never an answer.
The placeholder carries an elapsed time past the first minute (
_Working on it… (4m)_), refreshed once a minute. A message that has not changed in twenty minutes reads as abandoned, which is what the affordance exists to prevent.Effect
A failure that took 30 minutes to report as a timeout now takes ~10 seconds and names the cause.
Specs
0181-a-turn-that-stops-without-answering-is-a-failure(new) — observing the session rather than waiting to be told, and quoting the harness.0178-a-channel-shows-that-a-turn-is-still-running— amended with a fourth rule: the affordance must keep looking alive.Testing
cargo test --workspacegreen (1349 + 608 daemon lib). One unrelated CLI test (smith_tool_expand_collapse_rebuilds_only_retained_suffix) flaked under concurrent build load and passes in isolation.New coverage: the codex banner recovered verbatim from real captured PTY bytes, a multi-byte glyph split across two writes, composer chrome excluded, runaway errors capped; and for the watch — repaints not counting as work, approvals never called failures, queued input not called a stopped turn, the longer never-started grace.
This PR touches
crates/daemononly → the relevant binary isconstruct.🤖 Generated with Claude Code