th-426791: block send while a turn is in flight - #321
Merged
Conversation
A message sent during a running turn did not queue — the daemon spawned a SECOND concurrent turn, the two streamed back interleaved, and each answer landed under the other's prompt. That is the root cause of the swapped responses the agentic bench reproduces, and of the ical/date mess. Both composers now refuse the dispatch instead of racing: - smooth-web: one pure predicate (`canSend`) gates Enter and the send button. The draft is kept, the placeholder and a hint line explain the pause, and Stop (th-3a912a) stays available as the way through. - `th code`: Enter checks before `take_input`, so nothing is lost. Local slash commands and `!shell` stay live; the input box titles itself "Working… send paused" so the swallowed keystroke reads as intent, not breakage. Blocking rather than queueing: a queued message would be composed against a conversation state the user never saw, and firing it silently later is the same surprise the concurrency bug produced. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: 876b146 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
brentrager
enabled auto-merge (squash)
August 2, 2026 21:26
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.
Problem
Sending a message while a turn was already running did not queue behind it. The daemon spawned a second concurrent turn; the two streamed back interleaved and each answer landed under the other's prompt. The agentic conversation bench (#315) reproduces it with plainly swapped responses, and it is the root cause behind the contradictory date/calendar replies.
The Stop button (#316, th-3a912a) gave a way out of a bad turn but deliberately left the footgun in place — its own PR body flagged blocking send as the follow-up. This is that follow-up.
Block, not queue
Send is refused while a turn is in flight. Queueing was considered and rejected: a queued message is composed against a conversation state the user never saw (the turn it was waiting on may have changed everything), and firing it silently later is the same "an answer arrived for a prompt I didn't mean" surprise the concurrency bug produced. Stop is the explicit, already-visible alternative, so blocking removes nothing the user can't get back with one click.
Composers covered
smooth-web (
crates/smooth-web/web)src/turn-guard.ts—canSend({ text, attachments, disabled, turnActive }), a pure predicate with no imports. Enter and the send button both route through it, so there is one place the rule lives (the button already flipped to Stop; Enter was the hole).Big Smooth is working — Stop to interrupt, and once there's a non-empty draft a line under the composer readsTurn in progress — Stop to interrupt, or wait; your draft is kept.It only appears when there's actually a swallowed keystroke to explain, so it isn't noise on every turn.th code(crates/smooth-code) — it had the same free-send behaviour: Enter spawned anotherrun_agent_streamingregardless ofstate.thinking.blocks_second_turn(input, turn_in_flight)inapp.rs, checked beforetake_input()so the draft survives.InputKind::Normalis blocked. Slash commands and!shellrun locally, so/clear,/quit,/helpstay usable —th codehas no interrupt, and blocking everything would leave the user with no way out at all.▶ Working… send pausedwhile thinking.ponytail:comment: an unknown/namethat resolves to a skill also dispatches a turn and isn't blocked, because deciding that needs the skill registry. Corner of a corner.Tests
crates/smooth-web/web/src/turn-guard.test.ts— 4 tests oncanSendvianode --test(pnpm testin the web dir;node --experimental-strip-types, no new dependency and no test framework added). NeededallowImportingTsExtensionsintsconfig.jsonfor the explicit.tsspecifier.crates/smooth-code/src/app.rs—second_turn_guard_tests, 4 tests: idle passes everything, a chat message is blocked mid-turn, local commands stay live mid-turn, an empty draft isn't claimed.Verification
cargo test -p smooai-smooth-code— 321 passed.cargo clippy -p smooai-smooth-code --all-targets— clean, zero warnings.cargo fmt -- --check— clean.pnpm test(web) — 4/4 pass.pnpm typecheck+pnpm build(web) — clean.oxfmtapplied to the three touched/added web files;oxlinton them reports only the pre-existingno-img-elementhits on untouched lines (same three as th-3a912a: Stop button — interrupt a running Big Smooth turn #316).Not runtime-validated against a live daemon; the guard is pure client-side state with no protocol change.
🤖 Generated with Claude Code