From 6aa0afba682b0308fbcace31f66fb532978d4dca Mon Sep 17 00:00:00 2001 From: Elior Nataf Lackritz Date: Fri, 9 Oct 2026 20:59:20 -0400 Subject: [PATCH] fix(checkpoint): default InMemorySaver delta history to the latest checkpoint (#9257) `InMemorySaver.get_delta_channel_history` looked its target up by `config["configurable"].get("checkpoint_id", "")`, so a config without a `checkpoint_id` matched no checkpoint, and every channel came back with no seed and no writes, which a `DeltaChannel` reads as empty. The base implementation, `PostgresSaver` and `SqliteSaver` read from the thread's latest checkpoint instead, as `get_tuple` does. A missing id now resolves to the namespace's newest checkpoint id, with the same `max` that `InMemorySaver.get_tuple` uses. Graphs always pass a checkpoint id here, so only direct callers of the saver API were affected, and a call with an id does the same work as before. Thanks @Hotragn, who pinned this down on #8242, where @longquanzheng's branch had already fixed it. JS twin: langchain-ai/langgraphjs#2979 ## Tests - `test_get_channel_writes_without_checkpoint_id_reads_the_latest` fails on main (`{'messages': {'writes': []}}`). - The `checkpoint`, `checkpoint-conformance`, `checkpoint-sqlite`, `checkpoint-postgres` (Postgres 16), `prebuilt` and `langgraph` suites pass, the last two without their Redis cases. The DeltaChannel conformance suite also passes against `InMemorySaver`; CI skips that run in `libs/checkpoint`, where the conformance package isn't installed, so the new test lives in `test_memory.py`. --- .../langgraph/checkpoint/memory/__init__.py | 2 +- libs/checkpoint/tests/test_memory.py | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/libs/checkpoint/langgraph/checkpoint/memory/__init__.py b/libs/checkpoint/langgraph/checkpoint/memory/__init__.py index 82361b539..ff6f7b1ca 100644 --- a/libs/checkpoint/langgraph/checkpoint/memory/__init__.py +++ b/libs/checkpoint/langgraph/checkpoint/memory/__init__.py @@ -170,8 +170,8 @@ class InMemorySaver( return {} thread_id = config["configurable"]["thread_id"] checkpoint_ns = config["configurable"].get("checkpoint_ns", "") - checkpoint_id = config["configurable"].get("checkpoint_id", "") ns_storage = self.storage.get(thread_id, {}).get(checkpoint_ns, {}) + checkpoint_id = get_checkpoint_id(config) or max(ns_storage, default="") chain: list[str] = [] target_entry = ns_storage.get(checkpoint_id) diff --git a/libs/checkpoint/tests/test_memory.py b/libs/checkpoint/tests/test_memory.py index 51d10a5dd..7c350ab96 100644 --- a/libs/checkpoint/tests/test_memory.py +++ b/libs/checkpoint/tests/test_memory.py @@ -420,6 +420,19 @@ class TestInMemorySaverDeltaChannel: assert "seed" not in result assert result["writes"] == [] + def test_get_channel_writes_without_checkpoint_id_reads_the_latest(self) -> None: + saver = InMemorySaver() + thread: RunnableConfig = { + "configurable": {"thread_id": "t1", "checkpoint_ns": ""} + } + parent = saver.put(thread, empty_checkpoint(), {}, {}) + saver.put_writes(parent, [("messages", "hi")], "task1") + saver.put(parent, empty_checkpoint(), {}, {}) + + result = saver.get_delta_channel_history(config=thread, channels=["messages"]) + + assert result == {"messages": {"writes": [("task1", "messages", "hi")]}} + class TestBaseFallbackGetChannelWrites: """Exercises the `BaseCheckpointSaver.get_delta_channel_history` default