fix(sdk): show interrupts raised after a passive thread rejoin - #2813
Merged
Elior Nataf Lackritz (eliornl) merged 3 commits intoSep 9, 2026
Merged
Conversation
🦋 Changeset detectedLatest commit: 105ba45 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: |
After a page refresh mid-run, the controller treated every input.requested it did not already know from the hydrated thread state as replayed history and dropped it. The SSE replays the thread buffer on connect with no marker for where the replay ends, so the interrupt the run then raised never reached the UI until the next reload, while isLoading still dropped to false. Unknown interrupts are now parked, and settled against the server's thread state when a root terminal lifecycle arrives: ids the server lists in tasks[].interrupts are shown, the rest are dropped as history. The state is written slightly after the event streams, so the fetch retries every 500ms for up to 5s, coalesced across a replay burst. A failed fetch keeps retrying until the deadline; only disposal, a thread swap, or a new submit stop the loop.
Elior Nataf Lackritz (eliornl)
force-pushed
the
fix/sdk-passive-rejoin-interrupts
branch
from
September 9, 2026 17:24
eb79e73 to
105ba45
Compare
Hunter Lovell (hntrl)
approved these changes
Sep 9, 2026
Elior Nataf Lackritz (eliornl)
deleted the
fix/sdk-passive-rejoin-interrupts
branch
September 9, 2026 17:38
This was referenced 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>
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>
5 tasks
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
Fixes LG-661. After a page refresh (or a second tab) while a run is in progress, an interrupt the run then raises never shows up in
useStreamuntil the next reload.isLoadingstill drops to false, so the agent looks stuck. Reproduced on a real server with a slow-then-interrupt graph: the original tab shows the interrupt, the refreshed tab showsinterrupts: [].Why it happens
After hydrate, the controller treats every
input.requestedit does not already know fromstate.tasks[].interruptsas replayed history, because the SSE replays the whole thread buffer on connect and the stream has no marker for where replay ends. #2807 parks those unknown interrupts until acheckpointsevent carrying the hydrated checkpoint id arrives, then releases everything newer.That event never arrives:
checkpointsstream mode as aget_state()-shaped snapshot. The server session only forwards a checkpoint envelope with a top-level stringid, so nothing is forwarded. Zerocheckpointsevents on the wire in the reproduction, matching the customer's HAR.checkpointsemission at all (libs/langgraph/src).So the parked interrupts wait forever.
The fix
Decide parked interrupts with the server's thread state instead of a stream marker.
interrupted/completed/failed) arrives with interrupts parked,#settleParkedInterruptsruns the existing#reconcilePendingInterruptsFromServer: ids the server lists as pending are shown, the rest are dropped as history. Payloads come fromstate.tasks[].interrupts, the same source hydrate uses, so a refreshed tab and a fresh load agree by construction.checkpointsbarrier from fix(sdk): appropriately track persisted seq for stream replay #2807. This PR carries the parking list itself; the barrier is gone, so the settle against server state is the only release path.Tests
controller.test.ts:All three fail on
main. The two #2807 tests still pass. Fulllibs/sdkunit suite (794) andlibs/sdk-reactbrowser suite (244) pass.Real-stack check (langgraph-api inmem, langgraph 1.2.6): submit a run, create a second
StreamControlleron the same thread 1s in, let the run interrupt at 4s. Before: refreshed tabinterrupts: []. After: refreshed tab shows the interrupt,respond()from that tab resumes the run, and it completes.Follow-ups (not in this PR)
checkpointschannel is dead on the wire for every current deployment; the server never forwards it because no runtime produces the{id, parent_id, step, source}envelope.onCompletednot firing after a refresh (also reported in the thread):#runLifecycleListenerrequiresisLoadingto be true, and the buffer'srunningevent is trimmed on long runs.Changeset:
@langchain/langgraph-sdkpatch.