Skip to content

fix: cancel prompt and session-abort requests on stream close; wire doGenerate abort signal#31

Merged
ben-vargas merged 2 commits into
mainfrom
worktree-fix-prompt-fetch-hang
Jun 11, 2026
Merged

fix: cancel prompt and session-abort requests on stream close; wire doGenerate abort signal#31
ben-vargas merged 2 commits into
mainfrom
worktree-fix-prompt-fetch-hang

Conversation

@ben-vargas

Copy link
Copy Markdown
Owner

Follow-up to #30, which fixed the leaked GET /event SSE connection that kept the Node event loop alive after streaming finished. This PR extends the same per-stream AbortController to the remaining unanchored requests and adds the regression coverage #30 shipped without.

Changes

doStream: prompt and session-abort requests cancelled on stream close

The per-stream abort signal from #30 is now also passed to session.prompt and session.abort, so a prompt the server never completes (e.g. after an abort race) cannot pin the event loop, and dispose() tears these requests down along with the SSE subscription. The local controller is renamed eventAbortControllerrequestAbortController to reflect its broader scope; the manager API from #30 (registerEventSubscription) is unchanged.

Prompt results that arrive after an intentional close are now ignored. Without that guard, cancelling the prompt fetch would be mis-reported as an empty-response error stream part, because the SDK resolves with { error } rather than rejecting on fetch abort.

doGenerate: abort signal actually wired

On main, options.abortSignal never reaches the prompt request — aborting generateText leaves the HTTP request pending and the server generating. The signal is now forwarded to the fetch; a caller-initiated abort surfaces as AbortError (previously a generic empty-response error) and best-effort aborts the server-side session to stop generation.

Regression tests (5 new, none existed for #30's behavior)

  • SSE subscription signal is aborted when the stream closes, including the abort-before-iterator.return() ordering fix: abort SSE event subscription on stream close so process can exit #30 relies on (return() can block on a pending read otherwise)
  • In-flight prompt request is cancelled on the user-abort path and after normal stream completion
  • doGenerate mid-flight abort throws AbortError and calls session.abort
  • Manager dispose() aborts registered controllers; unregistered ones are untouched

CHANGELOG

Adds the missing Unreleased entries for both #30 and this change.

Verification

  • npm run ci green: 376/376 tests, typecheck, lint; build clean
  • Live against an external OpenCode server on 127.0.0.1:4096: repeated runs of examples/abort-signal.ts all exit within ~4s of printing "Done." with no leftover ESTABLISHED connections

Context

Live debugging of the original hang (Node inspector attached to a hung process) showed the leaked request was the SSE GET /event — fixed by #30 — while the prompt POST completes once session.abort is processed. The prompt cancellation here is therefore defense-in-depth rather than a reproduced hang; the doGenerate signal wiring is an independent real gap. This also retires the "fix flaky process hang after stream abort" follow-up: its hypothesized prompt-fetch leak has no reproduction independent of the SSE leak, and the hygiene it asked for is implemented and tested here.

…oGenerate abort signal

Follow-up to #30, which fixed the leaked GET /event SSE connection that
kept the Node event loop alive after streaming finished. This extends
the same per-stream AbortController to the remaining unanchored
requests:

- session.prompt and session.abort in doStream now receive the stream's
  abort signal, so a prompt the server never completes (e.g. after an
  abort race) cannot pin the event loop, and dispose() tears these
  requests down along with the SSE subscription. Prompt results that
  arrive after an intentional close are ignored instead of being
  mis-reported as empty-response errors (the SDK resolves with
  { error } rather than rejecting on fetch abort).

- doGenerate now forwards options.abortSignal to the prompt request, so
  aborting generateText cancels the underlying HTTP request instead of
  leaving it pending. A caller-initiated abort surfaces as AbortError
  (previously a generic empty-response error) and best-effort aborts
  the server-side session to stop generation.

Adds the regression tests #30 lacked: SSE signal cancellation on stream
close (including abort-before-iterator.return ordering), prompt-fetch
cancellation on the abort and completion paths, doGenerate mid-flight
abort semantics, and manager dispose() aborting registered controllers.
Also adds the missing CHANGELOG entries for #30 and this change.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b468520ca7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/opencode-language-model.ts Outdated
…ate abort

Codex review on #31 flagged that clients configured with the supported
clientOptions.throwOnError passthrough reject the session.prompt await
on fetch abort instead of resolving a fields-style { error } result, so
the best-effort session.abort in the empty-data path never ran and the
server kept generating after generateText was cancelled.

Hoist the server-side abort into a helper and issue it from both the
rejection path and the { error } path before rethrowing.
@ben-vargas

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 👍

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@ben-vargas
ben-vargas merged commit 51fc0e5 into main Jun 11, 2026
3 checks passed
@ben-vargas
ben-vargas deleted the worktree-fix-prompt-fetch-hang branch June 11, 2026 16:38
@ben-vargas ben-vargas mentioned this pull request Jun 11, 2026
ben-vargas added a commit that referenced this pull request Jun 11, 2026
Rename the Unreleased changelog section to 3.0.6, add PR references to
the #31/#32 entries, and bump the package version.
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.

1 participant