mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-24 00:22:25 +02:00
## Summary Fix time travel (replay and fork) for graphs with interrupts and subgraphs. ## Problem Two issues with replaying/forking from earlier checkpoints: 1. **Stale interrupt values during replay** — Replays incorrectly reused cached `RESUME` values from prior `interrupt()` calls, so interrupts silently returned stale answers instead of re-firing. 2. **Wrong subgraph state during time travel** — Subgraphs always loaded their **latest** checkpoint instead of the one corresponding to the parent's historical state. This caused subgraphs to skip execution or produce incorrect results during replay/fork. ## Changes Code changes span `libs/langgraph/langgraph/pregel/_loop.py`, `libs/langgraph/langgraph/_internal/_constants.py`, and a new `libs/langgraph/langgraph/_internal/_replay.py` module: - **Strip stale `RESUME` writes on replay** — During replays, cached `RESUME` writes are filtered out so `interrupt()` re-fires instead of returning old values. Genuine resumes (`Command(resume=...)`) preserve these writes. - **Rename `skip_done_tasks` → `is_replaying`** — Clearer naming for the flag that tracks whether the current run is replaying from a specific checkpoint. - **New `ReplayState` class (`_replay.py`)** — Encapsulates subgraph checkpoint loading during time-travel. Tracks a parent checkpoint ID upper bound and which subgraph namespaces have already loaded their pre-replay checkpoint. On the first visit to a subgraph namespace, it loads the latest checkpoint created *before* the replay point (via `checkpointer.list(..., before=...)` with `limit=1`). On subsequent visits (e.g. the same subgraph in a later loop iteration), it falls back to normal latest-checkpoint loading. The task-id suffix is stripped from namespaces so the same logical subgraph is recognized across loop iterations. - **New `CONFIG_KEY_REPLAY_STATE` config key** — The parent graph creates a `ReplayState` instance and passes it to subgraphs via config. For forks (`source=update`), the replay state uses the fork's parent checkpoint ID since the fork was created after the subgraph's original checkpoints. The single `ReplayState` instance is shared by reference across all derived configs within one parent execution. - **Subgraph checkpoint loading in `__enter__`/`__aenter__`** — When a subgraph detects a `ReplayState` in its config, it delegates checkpoint loading to `ReplayState.get_checkpoint`/`aget_checkpoint` instead of using the default `get_tuple`. It also clears `CONFIG_KEY_RESUMING` so `_first` re-applies input and recreates ephemeral routing channels. ## Tests New test files `test_time_travel.py` (~2500 lines) and `test_time_travel_async.py` (~2200 lines) covering: - Replay and fork with interrupts (single and multiple) - Replay and fork for graphs with and without subgraphs - Correct subgraph checkpoint restoration during parent time travel - `get_state` with subgraph state during replay