Skip to content

fix: make cancelling a turn actually stop it, and end turns that hang - #3985

Merged
taciturnaxolotl merged 6 commits into
mainfrom
fix/cancel-stops-stream
Nov 7, 2026
Merged

taciturnaxolotl merged 6 commits into
mainfrom
fix/cancel-stops-stream

Conversation

@taciturnaxolotl

@taciturnaxolotl taciturnaxolotl commented Sep 28, 2026 •

Copy link
Copy Markdown
Member

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-timeout to 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

@taciturnaxolotl
taciturnaxolotl force-pushed the fix/cancel-stops-stream branch 2 times, most recently from 33622b0 to 34bfd5a Compare October 5, 2026 21:15
@taciturnaxolotl
taciturnaxolotl force-pushed the fix/cancel-stops-stream branch 2 times, most recently from 14f4501 to 0cc389e Compare October 10, 2026 04:57
@taciturnaxolotl
taciturnaxolotl force-pushed the fix/cancel-stops-stream branch 9 times, most recently from 0817dfd to 5b83d10 Compare October 20, 2026 12:00
@taciturnaxolotl
taciturnaxolotl force-pushed the fix/cancel-stops-stream branch 17 times, most recently from e82a2dc to e8ba5b1 Compare October 28, 2026 19:41
@taciturnaxolotl
taciturnaxolotl force-pushed the fix/cancel-stops-stream branch 8 times, most recently from 63cf4c8 to 5ec30e9 Compare November 6, 2026 18:42
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
taciturnaxolotl force-pushed the fix/cancel-stops-stream branch from 5ec30e9 to be06016 Compare November 7, 2026 20:21
@taciturnaxolotl
taciturnaxolotl merged commit 7b8eea5 into main Nov 7, 2026
13 of 14 checks passed
@taciturnaxolotl
taciturnaxolotl deleted the fix/cancel-stops-stream branch November 7, 2026 22:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants