fix(chat): end a turn when the agent stops speaking, not once it has spoken - #4
Merged
Conversation
…spoken A turn ended two seconds after silence, measured from the first reply of any kind and never re-measured. One flag was set on the first substantive event and never cleared, so everything after it ran on the two-second timeout — including a tool call, where silence is what a running call looks like. An agent that said "placing the call now" and then placed it was cut off from its own result. The flag is replaced by a question asked of every event: was the last thing to arrive the agent speaking? Text, a document, a form, a result, a restriction — those start the two-second clock. Anything else clears it and the wait stays long. `session:tool_status` moves out of that set for the same reason it caused the bug: it reports what the agent is doing, not what it says. ## Where the rule stops Nothing else ends a turn, so a turn whose last event is neither speech nor a settled state waits indefinitely. Three plain turns recorded against a live server each ended on `session:text`, and a turn that ran a task ended on a settled state, but nothing guarantees either. `turn_timeout` bounds a turn in wall-clock seconds and keeps whatever arrived before the deadline; it defaults to none, so no existing caller changes behaviour. A test states the boundary rather than leaving it to be discovered. ## Session states `credits_exhausted` and `task_paused` now end a turn. Both stop on the account rather than on the agent, so nothing further arrives from the session itself. `task_stale` is dropped: the server has no such state. Staleness is `is_stale` on the session object, read over REST — the two were conflated. ## Tests The flow tests closed a turn by sending `session:input_state`, which left the supported scope in the previous change and no longer ends anything. They now close on a settled `session:state`, which is explicit rather than timing-based. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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
A turn ended two seconds after silence, measured from the first reply of any kind and never re-measured. The check is now asked of every event: was the last thing to arrive the agent speaking?
This also adds
turn_timeout, a wall-clock bound on one turn, and corrects the set of session states that end one.The defect
received_agent_responsewas set by the first substantive event and never cleared. From that point the whole turn ran on the two-second timeout, including the part where the agent is working rather than talking — and silence is exactly what a running tool call looks like.The caller was cut off from the result of the operation it had just been told about.
The rule
session:text,session:text_part,session:rich_content,session:form_to_user,session:task_finished,session:restrictionsession:tool_statusmoves out of the first set for the same reason it caused the defect: it reports what the agent is doing, not what it says.Where the rule stops
Nothing else ends a turn, so a turn whose last event is neither speech nor a settled state waits indefinitely.
Three plain turns recorded against a live server each ended on
session:text, and a turn that ran a task ended on a settled state. Neither is guaranteed.turn_timeoutbounds a turn in wall-clock seconds and keeps whatever arrived before the deadline:It defaults to
None, so no existing caller changes behaviour. A test states this boundary rather than leaving it to be discovered.The name deliberately describes the bound rather than the mechanism, so it survives a change of mechanism. The two internal timeouts stay internal.
Session states
credits_exhaustedandtask_pausednow end a turn. Both stop on the account rather than on the agent, so nothing further arrives from the session itself.task_staleis dropped. The server has no such state — its session states areinit,chat,task_ready,task_processing,task_interrupted(deprecated),task_finished,task_cancelled,task_pausedandcredits_exhausted. Staleness isis_staleon the session object over REST; the two were conflated.Tests
The flow tests closed a turn by sending
session:input_state, which left the supported scope in #3 and no longer ends anything. They now close on a settledsession:state— explicit rather than timing-based, and no longer dependent on an unsupported event.Four tests cover the new rule: a turn that ends on speech, a running tool that is not mistaken for a finished turn, a session stopped on its balance, and the unbounded case with and without
turn_timeout.Verification
ruff checkclean;python -m buildandtwine checkpass.session:textcomes from three recorded local sessions, not from reading the server.🤖 Generated with Claude Code