mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-21 07:02:25 +02:00
## Summary Replaces the single-roundtrip `UNION ALL` DeltaChannel read with a two-stage query that avoids fetching unused snapshot blobs, then removes the old combined path entirely. ### Problem `_get_channel_writes_history` used a single `UNION ALL` query that fetched **all** checkpoint metadata, writes, and blobs for a `(thread_id, channel)` in one shot. With `snapshot_frequency=N`, this pulled back O(N/freq) full-size snapshot blobs even though only the nearest one is needed to seed reconstruction. At 500 turns with `snapshot_frequency=10`, this meant fetching ~100 complete message-history snapshots per read. ### Solution Two-stage read: - **Stage 1** — lightweight scan of `checkpoints` only (no blob bytes): walks the parent chain from the target checkpoint and stops at the first ancestor with a snapshot, returning `chain_cids` and `seed_version` - **Stage 2** — targeted fetch: only the writes for `chain_cids` and the single seed blob at `seed_version` The two-stage path is now unconditional — the old combined query and `LG_DELTA_TWO_STAGE_QUERY` env-var gate have been removed. ### Sentinel cleanup `DELTA_SENTINEL` is now a pure in-memory signal and is never written to storage: - Postgres `put()` already stripped it from `channel_values` before writing blobs - Memory saver `put()` now stores `"empty"` instead of serializing the sentinel - `EXT_DELTA_SENTINEL` (msgpack ext code 8) removed from `JsonPlusSerializer` - `DELTA_SENTINEL` is kept as an in-memory marker: `DeltaChannel.checkpoint()` returns it so savers know to skip it, and `_ChannelWritesHistory.seed` uses it to mean "no snapshot found, start from empty" ## Performance Benchmarked at `snapshot_frequency=10` on Postgres (`~100 tok/msg`): | turns | old combined query | two-stage | |------:|-------------------:|----------:| | 50 | 6.0ms | 2.8ms (2.1x faster) | | 100 | 10.1ms | 5.6ms (1.8x faster) | | 500 | **216.1ms** | 15.3ms (**14x faster**) | The old query's read time grew super-linearly with turn count because each read fetched O(N/freq) full snapshot blobs. Two-stage keeps read depth bounded by `snapshot_frequency` regardless of thread length. ## Test plan - `make test` in `libs/checkpoint`, `libs/checkpoint-postgres`, `libs/langgraph` - Removed `test_delta_sentinel_serde_round_trip` (sentinel no longer serializable) - Updated `test_memory.py` — delta channel blobs stored as `"empty"`, not serialized sentinel - Updated `test_channels.py` — `channel_values` no longer contains sentinel key for DeltaChannels - Deleted `test_delta_channel_two_stage_benchmark.py` (one-stage vs two-stage comparison; path no longer exists) --------- Co-authored-by: Sydney Runkle <54324534+sydney-runkle@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>