-
Notifications
You must be signed in to change notification settings - Fork 31
fix: handle cancelled SSE streams #34
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
Open
noamkush
wants to merge
2
commits into
ephraimduncan:main
Choose a base branch
from
noamkush:fix/sse-cancellation
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
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
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
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Setting
closed = truemutes the SSE output but leaves the bridge fully alive: the heartbeat interval started at line 1563 keeps writing every 5s, and the code's own comment at 1499-1501 notes heartbeats otherwise keep the bridge open forever.bridge.onDataalso keeps running, so the exec path at 1464-1471 still registers the session inactiveBridgeseven though the disconnected client will never send the follow-up that resumes it, leaving the entry, timer, and subprocess alive until a same-key retry or a config reload. Please mirror the endStream-error teardown at 1507-1509 here: clearheartbeatTimer, delete theactiveBridgesentry, and callbridge.end().Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Disclaimer: used an llm to research, fix, and write this reply.
You are correct.
closed = truedoes not stop the bridge. I pushed a follow-up commit.But
bridge.end()does not stop the bridge either. It does a half-close. It writes a zero-length message, and this message breaks the stdin loop inh2-bridge.mjs. The loop then callsh2Stream.end()on the request half only. The response half stays open, andh2Stream.on("data")continues to callresetTimeout(). Thus the subprocess stays alive.bridge.onDataalso stays installed. The model sends its tool call, and the exec handler writes theactiveBridgesentry again. The teardown deleted that entry a moment earlier. The map has no TTL, so the new entry stays until a same-key retry or a call tostopProxy().I did not assume this. I replaced
kill()withend()and ran the new test. It failed in the same way as no fix at all.The commit kills the bridge process on cancellation, clears the heartbeat, and removes the
activeBridgesentry only if it still belongs to that bridge. This identity check prevents cancellation from deleting a newer same-key bridge. The endStream error path uses the same teardown, changing that path from a half-close to a hard stop as well.One detail needs a note.
cancel()does nothing if the stream is already closed. The tool-call pause path closes its controller on purpose, but it keeps the bridge inactiveBridgesfor the continuation. An unguarded cancel would stop a bridge that OpenCode must resume. The streams specification does not permit this sequence, but the guard costs nothing, and the failure is silent without it.For the test, I changed the fake H2 backend to hold the Run stream open. A flag controls this behavior, so the non-streaming test still stops. The test aborts the fetch during the stream. It then checks that the upstream Run stream closes. Only the subprocess holds a handle to that stream. Thus a closed stream shows that the process stopped, and not only that it stopped to write.
The last point is optional. I traced the abort path in the Cursor CLI bundle. The client writes a
ConversationAction{cancel_action}on the same bidi stream, waits for that write, and only then sendsRST_STREAM(CANCEL). It never does a half-close, and it never keeps a cancelled stream for a resume. Thus a hard kill is correct, andcancel_actiononly lets the server stop and write a final checkpoint. The repo has all the necessary types, but I did not test this against the live server, so I kept it out of this PR.