fix(langgraph): replay task results on nested resume - #2679
Open
Serhiy Bzhezytskyy (serhiy-bzhezytskyy) wants to merge 1 commit into
Open
Conversation
A subgraph's loop receives `checkpoint_id` as a key in its `configurable`, so `skipDoneTasks` was false for every nested run and the recorded pending writes were never applied to the replayed tasks. A `task()` that completed before an `interrupt()` therefore executed its body again on resume — a duplicated side effect, since `task()` is the wrapper for the non-idempotent step. The standalone case was unaffected. Gate applying pending writes on the existing `isTimeTraveling` predicate, which `_first()` already computes and which distinguishes replaying a specific checkpoint from resuming an interrupt. `skipDoneTasks` is left untouched, so the subgraph time-travel behaviour it guards is unchanged.
🦋 Changeset detectedLatest commit: 6060c05 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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: |
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 #2667
A
task()that completed before aninterrupt()records its result as a pending write, so resuming should reuse it. It does when the graph runs standalone. Composed as a subgraph, the body runs again on every resume — one duplicated side effect per resume, andtask()is the wrapper for the non-idempotent step.Root cause
A subgraph's loop receives
checkpoint_idas a key in itsconfigurable, andskipDoneTaskstests for the key rather than a value:So it is
falseon every nested run and the two sites that hand a recorded result to a task never fire —_matchWrites()for a pushed Call task, and the pending-writes loop intick().task.writesstays empty, the short-circuit inrunner.tsthat returns a recordedRETURNwrite cannot run, and the body executes again.The predicate itself is load-bearing. Python fixed the same bug in langchain-ai/langgraph#6161 by conditioning the derivation on resuming inside a nested graph; ported here, that shape fails the
Direct-to-subgraph time travelgroup intime_travel.test.ts, and making the check test the value instead of the key fails seven tests in the same file.Changes
pregel/loop.ts: gate applying pending writes onisTimeTraveling— the predicate_first()already computes and already uses to drop staleRESUMEwrites — instead ofskipDoneTasks, whose derivation is untouched.isReplayingstill derives from it, so it still readstruefor every nested run.tests/nested-resume-task-replay.test.ts: standalone resume (the control), nested resume, and an explicitcheckpoint_idreplay, which must still re-run the task.Test plan
expected 2 to be 1with the change stashedpnpm lint,pnpm format:checkand thelanggraph-coresuite pass locally, including the time-travel suites@langchain/langgraphReproduces on
@langchain/langgraph1.4.9, onmain, and on Pythonlanggraph1.2.10 — langchain-ai/langgraph#6161 fixed it there, and the later rename ofskip_done_taskstois_replayingdropped the nested-resume condition.