Repository navigation
fix: make cancelling a turn actually stop it, and end turns that hang - #3985
Merged
Merged
Conversation
taciturnaxolotl
force-pushed
the
fix/cancel-stops-stream
branch
2 times, most recently
from
October 5, 2026 21:15
33622b0 to
34bfd5a
Compare
taciturnaxolotl
force-pushed
the
fix/cancel-stops-stream
branch
2 times, most recently
from
October 10, 2026 04:57
14f4501 to
0cc389e
Compare
taciturnaxolotl
force-pushed
the
fix/cancel-stops-stream
branch
9 times, most recently
from
October 20, 2026 12:00
0817dfd to
5b83d10
Compare
taciturnaxolotl
force-pushed
the
fix/cancel-stops-stream
branch
17 times, most recently
from
October 28, 2026 19:41
e82a2dc to
e8ba5b1
Compare
taciturnaxolotl
force-pushed
the
fix/cancel-stops-stream
branch
6 times, most recently
from
October 30, 2026 03:58
c2e8298 to
1b207ba
Compare
andreynering
approved these changes
Oct 30, 2026
taciturnaxolotl
force-pushed
the
fix/cancel-stops-stream
branch
8 times, most recently
from
November 6, 2026 18:42
63cf4c8 to
5ec30e9
Compare
Cancellation and the request timeout now share one stream guard, timeouts are retried through fantasy's explicit retryable-error contract, a tool cut off by a cancelled turn is recorded as interrupted, and a tool that finished before the cancel keeps its output in the chat. 💘 Generated with Crush Assisted-by: Crush:claude-opus-5-5
taciturnaxolotl
force-pushed
the
fix/cancel-stops-stream
branch
from
November 7, 2026 20:21
5ec30e9 to
be06016
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.
Two ways a turn could fail to end, and one change to how timeouts are retried.
Cancelling didn't always cancel. Waiting on the network, a cancel works. But once a provider has flushed its response into memory there's nothing left to interrupt, so the turn keeps draining the backlog (thousands of chunks, for a large tool call) while you watch. Every request now goes through one guard that stops the stream at the first part after a cancel. It does this on every model, whether or not a request timeout is set, and the stream always ends by saying why it stopped. Stopping quietly looked like a stream that finished normally, which left the half-written tool call spinning forever. A tool cut off by the cancel is recorded as interrupted and shows as cancelled rather than as a red error. Tools that had already finished keep their output.
A silent connection hung forever. Providers send content-free keepalives while a model thinks, and honoring those is what keeps a long think from being cut off as a dead connection. The catch is that they reset the idle timer indefinitely, so a connection that stays open and never produces anything (a sleeping laptop, from this side) never times out. Keepalive-only streams now run against their own budget of ten times the request timeout (20 minutes at the default). The error says whether the model went quiet or the connection died, instead of blaming the model for both.
Timeouts are now retried. A request timeout used to look like a deliberate abort, so it was never retried. It now tells the retry loop directly that it is worth retrying, using a new contract in fantasy. That covers a non-streaming request that runs past its deadline, a stream that goes silent, and a stream that sends only keepalives. Silence usually means the network dropped, which is worth another try. But it also means a request that genuinely keeps timing out takes up to the retry count times longer to give up. Setting
request-timeoutto 0 still disables the timeouts. Cancellation keeps working either way.Depends on charmbracelet/fantasy#438. This PR pins fantasy to a pseudo-version of that branch, which also brings in fantasy's latest changes and its go 1.27.3 requirement. Once #438 is released, the pin should move to the tag before this merges.
💘 Generated with Crush