revert(sdk): appropriately track persisted seq for stream replay (#2807) - #2814
Merged
Merged
Conversation
|
Elior Nataf Lackritz (eliornl)
approved these changes
Sep 9, 2026
@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: |
Elior Nataf Lackritz (eliornl)
added a commit
that referenced
this pull request
Sep 9, 2026
## 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 `useStream` until the next reload. `isLoading` still 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 shows `interrupts: []`. ## Why it happens After hydrate, the controller treats every `input.requested` it does not already know from `state.tasks[].interrupts` as 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 a `checkpoints` event carrying the hydrated checkpoint id arrives, then releases everything newer. That event never arrives: - LangGraph Python emits the `checkpoints` stream mode as a `get_state()`-shaped snapshot. The server session only forwards a checkpoint envelope with a top-level string `id`, so nothing is forwarded. Zero `checkpoints` events on the wire in the reproduction, matching the customer's HAR. - LangGraph.js has no `checkpoints` emission at all (`libs/langgraph/src`). - The Redis replay buffer trims by age (120s), so on a long node the matching event is gone before the refresh even when it did exist. So the parked interrupts wait forever. ## The fix Decide parked interrupts with the server's thread state instead of a stream marker. - Unknown interrupts are parked as before, now whether or not hydrate returned a checkpoint id (the release no longer needs it). - When a root terminal lifecycle (`interrupted` / `completed` / `failed`) arrives with interrupts parked, `#settleParkedInterrupts` runs the existing `#reconcilePendingInterruptsFromServer`: ids the server lists as pending are shown, the rest are dropped as history. Payloads come from `state.tasks[].interrupts`, the same source hydrate uses, so a refreshed tab and a fresh load agree by construction. - The server commits the interrupt to thread state slightly after it streams the event (the server has its own 5s settle poll for this), so the fetch retries every 500ms for up to 5s. One loop runs at a time, so a replay burst with many old terminals costs one loop, not one per event. The common path (no parked interrupts) makes no extra calls. - Rebased on #2814, which reverts the `checkpoints` barrier from #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`: - shows an interrupt raised after a passive rejoin without a checkpoints event - waits for the server to commit the interrupt before showing it (second fetch returns it) - drops replayed interrupts the server no longer lists All three fail on `main`. The two #2807 tests still pass. Full `libs/sdk` unit suite (794) and `libs/sdk-react` browser suite (244) pass. Real-stack check (langgraph-api inmem, langgraph 1.2.6): submit a run, create a second `StreamController` on the same thread 1s in, let the run interrupt at 4s. Before: refreshed tab `interrupts: []`. After: refreshed tab shows the interrupt, `respond()` from that tab resumes the run, and it completes. ## Follow-ups (not in this PR) - The `checkpoints` channel 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. - `onCompleted` not firing after a refresh (also reported in the thread): `#runLifecycleListener` requires `isLoading` to be true, and the buffer's `running` event is trimmed on long runs. Changeset: `@langchain/langgraph-sdk` patch. --------- Co-authored-by: Hunter Lovell <hunter@hntrl.io>
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.
This reverts commit dfa4332.