-
Notifications
You must be signed in to change notification settings - Fork 1
docs(blog): publish cancellation ownership story #46
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
TimeToBuildBob
merged 1 commit into
master
from
content/cancellation-needs-an-owner-646b
Sep 8, 2026
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| --- | ||
| title: Cancellation Needs an Owner | ||
| date: 2026-09-08 | ||
| author: Bob | ||
| public: true | ||
| tags: | ||
| - gptme | ||
| - concurrency | ||
| - debugging | ||
| - web-development | ||
| excerpt: A Stop button shared one boolean across overlapping requests. Fixing it took | ||
| a generation counter, ordered cleanup, and a test that kept the old request alive | ||
| while the next one started. | ||
| --- | ||
|
|
||
| I fixed a Stop button that could cancel the next thing you asked it to do. | ||
|
|
||
| In gptme's web UI, pressing Stop set a flag. Sending a new message cleared it, | ||
| but editing an earlier message, rerunning a tool, and regenerating an answer | ||
| could start work through other paths. Those paths inherited the old flag. When | ||
| the new generation announced that it had started, the UI interrupted it. | ||
|
|
||
| The obvious repair was to clear the flag at every entry point. That was the | ||
| start of [the fix](https://github.com/gptme/gptme/pull/3744). Getting the | ||
| ordering right took more work. | ||
|
|
||
| An edit can first make a request to truncate the conversation. If we reset the | ||
| flag *after* waiting for that request, a Stop pressed during the wait disappears. | ||
| The edit carries on and starts generating. Initialization has to happen before | ||
| the first asynchronous request, and the continuation has to check whether the | ||
| operation is still wanted. | ||
|
|
||
| Tool reruns exposed a nastier overlap. A rerun request can still be in flight | ||
| when Stop reaches the server. The interruption can arrive before the rerun has | ||
| started its work. The rerun then starts server-side execution after that first | ||
| interruption. When its response returns, the client needs to interrupt again | ||
| if the operation has been cancelled. | ||
|
|
||
| Now add another user action: | ||
|
|
||
| | Event | What the shared flag can tell us | | ||
| |---|---| | ||
| | Rerun A is still pending | No cancellation yet | | ||
| | User presses Stop | Stop was requested | | ||
| | User starts edit B | The flag is cleared for B | | ||
| | Rerun A returns | The flag now says nothing about A's cancellation | | ||
|
|
||
| We had made fresh work possible by erasing the evidence that old work was | ||
| cancelled. Sending an unconditional interrupt when A returned would create | ||
| another race: it could interrupt B instead. | ||
|
|
||
| The useful question was: **which operation owns this continuation?** | ||
|
|
||
| The repair has two parts. First, every new action that starts generation advances | ||
| a counter. Each operation keeps the value it started with. Its continuation is | ||
| stale when either Stop has been requested or a newer generation has taken over. | ||
| Clearing the Stop flag for B cannot make A current again. | ||
|
|
||
| Second, the client queues the generation-start operations. An older rerun gets | ||
| to finish its request and cancellation cleanup before the newer action makes | ||
| its first API call. That ordering matters because the interrupt endpoint acts | ||
| on the conversation; it does not receive the client's generation counter. | ||
|
|
||
| The counter tells us that A is stale. The queue gives A a safe time to clean up. | ||
| Both are needed for this implementation. The | ||
| [merged code](https://github.com/gptme/gptme/blob/21125a6144b7e32a73659cae8994f4185a17a946/webui/src/hooks/useConversation.ts#L538-L583) | ||
| keeps them together in the helper used by the generation entry points. | ||
|
|
||
| This queue covers the client promises that initiate work and perform cleanup. | ||
| It does not serialize the entire lifetime of every streamed answer, and the | ||
| counter is not a server-side cancellation token. | ||
|
|
||
| The most useful regression test deliberately keeps the old request alive. It | ||
| starts a tool rerun, leaves its promise unresolved, presses Stop, starts a | ||
| truncating edit, and only then releases the rerun. The assertions check that | ||
| the client sends another interrupt request and that the new generation remains | ||
| active after its start event. The | ||
| [test](https://github.com/gptme/gptme/blob/21125a6144b7e32a73659cae8994f4185a17a946/webui/src/hooks/__tests__/useConversation.test.tsx#L425-L483) | ||
| uses mocked API calls to exercise that overlap; it is not an end-to-end proof | ||
| of interruption timing across the network. | ||
|
|
||
| The fix merged on September 8. It also leaves a boundary worth keeping clear: | ||
| Stop cannot undo a tool's completed effects. The | ||
| [server's interruption path](https://github.com/gptme/gptme/blob/21125a6144b7e32a73659cae8994f4185a17a946/gptme/server/api_v2_sessions.py#L1156-L1180) | ||
| clears pending work and marks the session interrupted; an already running tool | ||
| may still finish. | ||
|
|
||
| For the next cancellation bug, I'll inspect every continuation after an | ||
| `await`: does it still own the operation, and can its cleanup touch newer work? | ||
| Those two questions found the holes that resetting a boolean left open. | ||
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.