mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-10-21 15:31:43 +02:00
## Summary Replaces `durability="exit"`'s blanket force-snapshot of every `DeltaChannel` with proper write persistence that honors per-channel `snapshot_frequency`, plus closes two latent bugs the force-snapshot was masking. Before: every exit-mode run wrote a full `_DeltaSnapshot` blob for every delta channel, even when the channel had zero updates this run and was nowhere near its `snapshot_frequency`. After: the same count-based decision used by `durability="sync"`/`"async"` applies — channels at or above `snapshot_frequency` snapshot; channels below it persist their accumulated writes via a lazy "stub" anchor; untouched channels write nothing. ## What changed **Core redesign** (`pregel/_loop.py`, `pregel/_checkpoint.py`) - Drop `force_delta_snapshot` from `create_checkpoint` and `_should_snapshot_delta`. - Add `decide_delta_snapshots(channels, counts)` pure helper used by both `create_checkpoint` and the new exit-mode peek-ahead path. - Add `_exit_delta_writes` accumulator: every delta-channel write produced during a `durability="exit"` run (input writes from `_first` + per-superstep writes captured before `pending_writes.clear()` in `after_tick`) is collected into this list. - Add `_put_exit_delta_writes` (sync + async): runs from `_suppress_interrupt` BEFORE `_put_checkpoint(exiting=True)`. Filters out channels that will snapshot, then persists remaining writes to `checkpoint_writes` under an anchor parent. The anchor is the existing saved parent on resumed runs, or a lazily-created empty stub on first runs. - Visibility ordering: stub put goes onto `_put_checkpoint_fut` (becomes the next put's `prev`); exit-write futures go onto `_delta_write_futs`. The existing `_checkpointer_put_after_previous` already drains both before calling `saver.put`, so `final_checkpoint` is structurally guaranteed to land last — readers never see a partial view. **Latent bugs fixed (previously masked by force-snapshot)** - **Sync drain race**: `SyncPregelLoop` now initializes `_delta_write_futs = []` in `__enter__` and drains it in sync `_checkpointer_put_after_previous` before `put`, mirroring the async version. Without this, a multi-worker `BackgroundExecutor` could publish a checkpoint before the writes that produced it. - **Count double-bump in exit mode**: in `_put_checkpoint`, `delta_updates_since_snapshot` was being incremented twice for the last superstep — once by the intermediate `after_tick` call, once by `_suppress_interrupt`. Force-snapshot used to reset all counts to 0 so this never persisted; without it, snapshots would fire one superstep early after every exit-mode run. Fixed by gating the count-bump behind `not exiting`. **Pre-existing input-durability gap** - In the plain (non-Command) input path of `_first`, delta-channel input writes are now persisted via `put_writes` (mirroring the Command path), so sub-frequency inputs survive a `get_state` on resumed runs in `sync`/`async` durability. Note: first-run `sync`/`async` still has the same gap (writes orphan on the synthetic-empty parent id). That's flagged as a follow-up — out of scope for this PR. ## Test plan - Existing `tests/test_pregel.py` and `tests/test_pregel_async.py` pass unchanged. - Existing `tests/test_channels.py` (29 tests) and `tests/test_delta_channel_migration.py` pass unchanged. - New `tests/test_exit_delta_persistence.py` (11 tests) covers: - **Write-path**: zero-write exit (no stub), all-snapshot first run (no stub), sub-freq first run (single shared stub), sub-freq resumed run (anchor on saved parent), sync-vs-exit count parity, mixed snapshot/non-snapshot channels, snapshot fires at frequency. - **Read-path**: K-run replay chain reads correctly across stub→saved-parent transition; metadata `delta_updates_since_snapshot` round-trips correctly; mixed sync/exit durability alternation produces correct final state; snapshot+tail-deltas combination reads correctly. - `make format && make lint && make test` in `libs/langgraph/`. --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Sydney Runkle <sydneymarierunkle@gmail.com>