fix(sdk): recover from a server-side thread stream drop instead of freezing - #2812
Merged
Merged
Conversation
🦋 Changeset detectedLatest commit: 1f07fe5 The changes in this PR will be included in the next version bump. This PR includes changesets to release 5 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
@langchain/langgraph-checkpoint
@langchain/langgraph-checkpoint-mongodb
@langchain/langgraph-checkpoint-postgres
@langchain/langgraph-checkpoint-redis
@langchain/langgraph-checkpoint-sqlite
@langchain/langgraph-checkpoint-validation
create-langgraph
@langchain/langgraph-api
@langchain/langgraph-cli
@langchain/langgraph
@langchain/langgraph-cua
@langchain/langgraph-supervisor
@langchain/langgraph-swarm
@langchain/langgraph-ui
@langchain/langgraph-sdk
@langchain/angular
@langchain/react
@langchain/svelte
@langchain/vue
commit: |
Hunter Lovell (hntrl)
force-pushed
the
fix/sdk-reconnect-on-clean-close
branch
from
September 9, 2026 17:37
9dbb437 to
dfe8b4a
Compare
Hunter Lovell (hntrl)
previously approved these changes
Sep 9, 2026
…eezing The protocol SSE transport treated a clean close of the thread event stream as the end of the thread. The stream is open-ended, so a clean close only happens when the server's upstream consumer died or it is restarting; the client stayed silent, never reconnected, and the UI froze mid-run. The transport now reconnects on a clean close, with the same backoff and attempt budget as a dropped connection, and a connection that delivered events resets that budget so long-lived pages survive repeated deploys. maxReconnectAttempts: 0 keeps the end-on-close behavior. Unsolicited server error frames (no command id) were dropped because command routing found no pending request. ThreadStream now exposes them, and a shared stream that exhausts its reconnects, through onError; the StreamController sets stream.error, clears isLoading, and settles the in-flight submit() as failed.
Hunter Lovell (hntrl)
force-pushed
the
fix/sdk-reconnect-on-clean-close
branch
from
September 9, 2026 17:46
dfe8b4a to
1f07fe5
Compare
Hunter Lovell (hntrl)
approved these changes
Sep 9, 2026
Hunter Lovell (hntrl)
pushed a commit
that referenced
this pull request
Sep 9, 2026
This PR was opened by the [Changesets release](https://github.com/changesets/action) GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ `main` is currently in **pre mode** so this branch has prereleases rather than normal releases. If you want to exit prereleases, run `changeset pre exit` on `main`.⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ # Releases ## @langchain/langgraph-sdk@1.10.3-rc.2 ### Patch Changes - [#2813](#2813) [`4fc118f`](4fc118f) Thanks [@eliornl](https://github.com/eliornl)! - fix(sdk): show interrupts raised after a passive thread rejoin After a page refresh mid-run, `useStream` filtered every interrupt it did not already know from the hydrated thread state as replayed history, and waited for a `checkpoints` event to lift that filter. Current runtimes never emit that event and the replay buffer trims it on long runs, so interrupts raised after the refresh never appeared until the next reload. Unknown interrupts are now settled against the server's thread state when the run reaches a terminal lifecycle: the ones the server lists as pending are shown, the rest are dropped as history. - [#2812](#2812) [`db4bdad`](db4bdad) Thanks [@eliornl](https://github.com/eliornl)! - fix(sdk): recover from a server-side thread stream drop instead of freezing The protocol SSE transport now reconnects when the server closes the event stream cleanly. The thread stream is open-ended, so a clean close only happens when the server's own upstream consumer died or it is restarting; before, the client treated it as the end of the thread and the UI froze mid-run with no error. A connection that delivered events also resets the reconnect budget, so long-lived pages survive repeated deploys. `maxReconnectAttempts: 0` keeps the old end-on-close behavior. Unsolicited server error frames (no command id) and a shared stream that gives up reconnecting now reach `stream.error`: `ThreadStream.onError` exposes them, `useStream` sets `error`, clears `isLoading`, and settles the in-flight `submit()` as failed. ## @langchain/angular@1.0.36-rc.2 ### Patch Changes - Updated dependencies [[`4fc118f`](4fc118f), [`db4bdad`](db4bdad)]: - @langchain/langgraph-sdk@1.10.3-rc.2 ## @langchain/react@1.0.36-rc.2 ### Patch Changes - Updated dependencies [[`4fc118f`](4fc118f), [`db4bdad`](db4bdad)]: - @langchain/langgraph-sdk@1.10.3-rc.2 ## @langchain/svelte@1.0.36-rc.2 ### Patch Changes - Updated dependencies [[`4fc118f`](4fc118f), [`db4bdad`](db4bdad)]: - @langchain/langgraph-sdk@1.10.3-rc.2 ## @langchain/vue@1.0.36-rc.2 ### Patch Changes - Updated dependencies [[`4fc118f`](4fc118f), [`db4bdad`](db4bdad)]: - @langchain/langgraph-sdk@1.10.3-rc.2 Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This was referenced Sep 11, 2026
Hunter Lovell (hntrl)
pushed a commit
that referenced
this pull request
Sep 12, 2026
This PR was opened by the [Changesets release](https://github.com/changesets/action) GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated. # Releases ## @langchain/langgraph-sdk@1.11.0 ### Minor Changes - [#2831](#2831) [`3234c69`](3234c69) Thanks [@ramon-langchain](https://github.com/ramon-langchain)! - Expose connection lifecycle callbacks for built-in streaming transports. `onConnected` runs after the initial SSE or WebSocket connection becomes usable and after every successful reconnect. Its payload distinguishes an `initial` connection from a `reconnected` connection and includes the reconnect attempt number. `onReconnect` now also receives the scheduled `delayMs`, allowing applications to display accurate retry state before the next connection attempt. React, Vue, Svelte, and Angular stream hooks forward both callbacks. ### Patch Changes - [#2831](#2831) [`3234c69`](3234c69) Thanks [@ramon-langchain](https://github.com/ramon-langchain)! - Recover cleanly when a thread's root event stream terminates unexpectedly. If the stream fails or closes while a run is active, the controller now records the transport error and clears `isLoading` rather than leaving the UI in a permanently running state. Once the failed pump settles, a later submission can start a fresh root subscription without recreating the thread stream or replaying the failed command. - [#2809](#2809) [`11a4535`](11a4535) Thanks [@hntrl](https://github.com/hntrl)! - fix(sdk): appropriately track persisted seq for stream replay Sequences weren't being appropriately attributed when rehydrating the page (e.g. on refresh). This meant we'd lose stream information on `useStream` on reloads. This has been fixed by adding a lookup step to determine what the most appropriate sequence index is to track in the event stream.:x - [#2762](#2762) [`2fab6fd`](2fab6fd) Thanks [@JessYanCoding](https://github.com/JessYanCoding)! - Send `checkpoint_id` in the `runs.stream()` request body, so a `checkpointId` passed to `client.runs.stream()` forks from the requested checkpoint instead of being silently dropped. Matches `runs.create()` and `runs.wait()`, which already send it. - [#2813](#2813) [`4fc118f`](4fc118f) Thanks [@eliornl](https://github.com/eliornl)! - fix(sdk): show interrupts raised after a passive thread rejoin After a page refresh mid-run, `useStream` filtered every interrupt it did not already know from the hydrated thread state as replayed history, and waited for a `checkpoints` event to lift that filter. Current runtimes never emit that event and the replay buffer trims it on long runs, so interrupts raised after the refresh never appeared until the next reload. Unknown interrupts are now settled against the server's thread state when the run reaches a terminal lifecycle: the ones the server lists as pending are shown, the rest are dropped as history. - [#2812](#2812) [`db4bdad`](db4bdad) Thanks [@eliornl](https://github.com/eliornl)! - fix(sdk): recover from a server-side thread stream drop instead of freezing The protocol SSE transport now reconnects when the server closes the event stream cleanly. The thread stream is open-ended, so a clean close only happens when the server's own upstream consumer died or it is restarting; before, the client treated it as the end of the thread and the UI froze mid-run with no error. A connection that delivered events also resets the reconnect budget, so long-lived pages survive repeated deploys. `maxReconnectAttempts: 0` keeps the old end-on-close behavior. Unsolicited server error frames (no command id) and a shared stream that gives up reconnecting now reach `stream.error`: `ThreadStream.onError` exposes them, `useStream` sets `error`, clears `isLoading`, and settles the in-flight `submit()` as failed. - [#2808](#2808) [`55fa26b`](55fa26b) Thanks [@hntrl](https://github.com/hntrl)! - fix(sdk): coalesce locally resolved interrupts when resolving interrupts using `useStream`, there was a case where we prioritized the remote state values (which we lookup in React Strict mode on every page transition) over the local interrupt responses we know we've responded with. This has since been fixed to first prioritize the local cache of interrupts, resolved against the remote state values when a run hits a terminal event ## @langchain/angular@1.1.0 ### Minor Changes - [#2831](#2831) [`3234c69`](3234c69) Thanks [@ramon-langchain](https://github.com/ramon-langchain)! - Expose connection lifecycle callbacks for built-in streaming transports. `onConnected` runs after the initial SSE or WebSocket connection becomes usable and after every successful reconnect. Its payload distinguishes an `initial` connection from a `reconnected` connection and includes the reconnect attempt number. `onReconnect` now also receives the scheduled `delayMs`, allowing applications to display accurate retry state before the next connection attempt. React, Vue, Svelte, and Angular stream hooks forward both callbacks. ### Patch Changes - Updated dependencies [[`3234c69`](3234c69), [`3234c69`](3234c69), [`11a4535`](11a4535), [`2fab6fd`](2fab6fd), [`4fc118f`](4fc118f), [`db4bdad`](db4bdad), [`55fa26b`](55fa26b)]: - @langchain/langgraph-sdk@1.11.0 ## @langchain/react@1.1.0 ### Minor Changes - [#2831](#2831) [`3234c69`](3234c69) Thanks [@ramon-langchain](https://github.com/ramon-langchain)! - Expose connection lifecycle callbacks for built-in streaming transports. `onConnected` runs after the initial SSE or WebSocket connection becomes usable and after every successful reconnect. Its payload distinguishes an `initial` connection from a `reconnected` connection and includes the reconnect attempt number. `onReconnect` now also receives the scheduled `delayMs`, allowing applications to display accurate retry state before the next connection attempt. React, Vue, Svelte, and Angular stream hooks forward both callbacks. ### Patch Changes - Updated dependencies [[`3234c69`](3234c69), [`3234c69`](3234c69), [`11a4535`](11a4535), [`2fab6fd`](2fab6fd), [`4fc118f`](4fc118f), [`db4bdad`](db4bdad), [`55fa26b`](55fa26b)]: - @langchain/langgraph-sdk@1.11.0 ## @langchain/svelte@1.1.0 ### Minor Changes - [#2831](#2831) [`3234c69`](3234c69) Thanks [@ramon-langchain](https://github.com/ramon-langchain)! - Expose connection lifecycle callbacks for built-in streaming transports. `onConnected` runs after the initial SSE or WebSocket connection becomes usable and after every successful reconnect. Its payload distinguishes an `initial` connection from a `reconnected` connection and includes the reconnect attempt number. `onReconnect` now also receives the scheduled `delayMs`, allowing applications to display accurate retry state before the next connection attempt. React, Vue, Svelte, and Angular stream hooks forward both callbacks. ### Patch Changes - Updated dependencies [[`3234c69`](3234c69), [`3234c69`](3234c69), [`11a4535`](11a4535), [`2fab6fd`](2fab6fd), [`4fc118f`](4fc118f), [`db4bdad`](db4bdad), [`55fa26b`](55fa26b)]: - @langchain/langgraph-sdk@1.11.0 ## @langchain/vue@1.1.0 ### Minor Changes - [#2831](#2831) [`3234c69`](3234c69) Thanks [@ramon-langchain](https://github.com/ramon-langchain)! - Expose connection lifecycle callbacks for built-in streaming transports. `onConnected` runs after the initial SSE or WebSocket connection becomes usable and after every successful reconnect. Its payload distinguishes an `initial` connection from a `reconnected` connection and includes the reconnect attempt number. `onReconnect` now also receives the scheduled `delayMs`, allowing applications to display accurate retry state before the next connection attempt. React, Vue, Svelte, and Angular stream hooks forward both callbacks. ### Patch Changes - Updated dependencies [[`3234c69`](3234c69), [`3234c69`](3234c69), [`11a4535`](11a4535), [`2fab6fd`](2fab6fd), [`4fc118f`](4fc118f), [`db4bdad`](db4bdad), [`55fa26b`](55fa26b)]: - @langchain/langgraph-sdk@1.11.0 ## @langchain/langgraph-api@1.4.6 ### Patch Changes - [#2751](#2751) [`0a9611a`](0a9611a) Thanks [@t3s7r](https://github.com/t3s7r)! - fix(api): `run.start` with input on a cancelled thread no longer folds the input into `Command(resume)`. A cancelled run shares the "interrupted" status with a genuine `interrupt()` pause, but has no pending interrupt to consume the resume value, so the submitted message was silently dropped. The input-vs-resume decision now keys on whether the thread actually has pending interrupts (both in the protocol service and the embed protocol). - Updated dependencies []: - @langchain/langgraph-ui@1.4.6 ## @langchain/langgraph-cli@1.4.6 ### Patch Changes - Updated dependencies [[`0a9611a`](0a9611a)]: - @langchain/langgraph-api@1.4.6 ## @langchain/langgraph@1.4.15 ### Patch Changes - [#2794](#2794) [`83a4b62`](83a4b62) Thanks [@hntrl](https://github.com/hntrl)! - feat(langgraph): add per-node `tracePolicy` input/output processors and `omitPayload` Transform the payloads recorded on a node's own trace run while retaining its span and timing. Processors receive raw values and fall back to the original payload if they throw. Graph state, root runs, and child runs remain unchanged when processors do not mutate their arguments. Matches Python's callback-level behavior: transforms also affect chain events and message streaming, so omitting outputs can suppress messages returned directly by nodes and omitting inputs can affect message deduplication. - Updated dependencies [[`3234c69`](3234c69), [`3234c69`](3234c69), [`11a4535`](11a4535), [`2fab6fd`](2fab6fd), [`4fc118f`](4fc118f), [`db4bdad`](db4bdad), [`55fa26b`](55fa26b)]: - @langchain/langgraph-sdk@1.11.0 ## @langchain/langgraph-cua@1.0.3 No changes in this release. ## @langchain/langgraph-supervisor@1.1.2 No changes in this release. ## @langchain/langgraph-swarm@1.0.3 No changes in this release. ## @langchain/langgraph-ui@1.4.6 No changes in this release. Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This was referenced Sep 16, 2026
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.
Summary
When the server closed the protocol SSE stream cleanly, the transport treated it as the end of the thread: no reconnect, no error, and a
useStreamUI froze mid-run withisLoadingstuck and nothing instream.error. The thread stream is open-ended by contract, so a clean close only ever means the server's upstream consumer died or the server is restarting. The transport now reconnects on it, and stream-level failures reachstream.error.Details
ProtocolSseTransportAdapter: a clean close of the event stream takes the same path as a dropped connection (backoff,onReconnect,maxReconnectAttempts).maxReconnectAttempts: 0keeps the old end-on-close behavior. A connection that delivered at least one event resets the attempt counter, so a page that lives through several deploys does not die on the fifth one.libs/langgraph-api/src/api/protocol.mts,experimental/embed/protocol.mts) hold the SSE open until the client aborts, and the Python server ends it only when its thread consumer dies, so no server uses a clean close to mean "done".ThreadStream.onError(listener): unsolicited error frames (id: null) were dropped because command routing found no pending request for them. They are now delivered toonError, as is a shared stream that exhausts its reconnects. Subscriptions stay open on an error frame so the reconnected stream keeps feeding the run.StreamController: on a thread error it setserror, clearsisLoading, and settles the in-flightsubmit()as failed.Tests
transport/http.test.ts: reconnects on a clean close; resets the reconnect budget after a connection delivered events; still ends on a clean close withmaxReconnectAttempts: 0.shared-stream.test.ts: an unsolicited error frame reachesonErroras aProtocolErrorand leaves the stream open; a failed shared stream reachesonError.controller.test.ts: a thread error lands onrootStore.error, clearsisLoading, and settlessubmit().libs/sdkunit suite (797) and thelibs/sdk-reactbrowser suite (244) pass.Validated on the real stack (langgraph-api postgres runtime, core server killed with
kill -9mid-run): without the fix the client got two events and then silence; with the fix it gotProtocolError unknown_error "Thread event stream failed: ...", reconnected, and received the rest of the run throughlifecycle: completed.Changeset:
@langchain/langgraph-sdkpatch.