Compare commits

...
Author SHA1 Message Date
Christian Bromann 88635b4413 fix(langgraph): treat DeltaChannel Overwrite as a hard reset in update()
DeltaChannel.update() kept writes ordered before an Overwrite within the
same super-step, while replay_writes() dropped them, so reconstructing a
thread from a checkpoint produced different state than the live run. Align
update() with replay_writes() (drop everything up to and including the
overwrite) so reload reproduces live state.
2026-06-01 12:38:33 -07:00
Nick HollonandGitHub 83dd61feac release(langgraph): 1.2.3 (#7945) 2026-06-01 14:51:31 -04:00
6 changed files with 43 additions and 6 deletions
+6 -1
View File
@@ -177,7 +177,12 @@ class DeltaChannel(Generic[Value], BaseChannel[Any, Any, Any]):
if overwrite_value is not None
else self.typ()
)
remaining = [v for i, v in enumerate(values) if i != overwrite_idx]
# Treat Overwrite as a hard reset: drop everything up to and
# including the overwrite, keeping only writes that follow it. This
# mirrors replay_writes so reconstruction from a checkpoint
# reproduces the live state even when a plain write precedes the
# Overwrite in the same super-step.
remaining = list(values[overwrite_idx + 1 :])
self.value = self.reducer(base, remaining) if remaining else base
return True
base = self.typ() if self.value is MISSING else self.value
+2 -2
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "langgraph"
version = "1.2.2"
version = "1.2.3"
description = "Building stateful, multi-actor applications with LLMs"
authors = []
requires-python = ">=3.10"
@@ -26,7 +26,7 @@ classifiers = [
dependencies = [
"langchain-core>=1.4.0,<2",
"langgraph-checkpoint>=4.1.0,<5.0.0",
"langgraph-sdk>=0.4.1,<0.5.0",
"langgraph-sdk>=0.4.2,<0.5.0",
"langgraph-prebuilt>=1.1.0,<1.2.0",
"xxhash>=3.5.0",
"pydantic>=2.7.4",
+32
View File
@@ -186,6 +186,38 @@ def test_delta_channel_overwrite() -> None:
assert ch.get()[0].content == "new"
def test_delta_channel_overwrite_after_plain_write_in_one_step() -> None:
"""Regression: a plain write ordered BEFORE an Overwrite in the same
super-step must reconstruct identically to the live state.
Overwrite is a hard reset: update() and replay_writes() both drop
everything up to and including the overwrite, keeping only writes that
follow it. Mirrors the JS end-state (channels/delta.ts).
"""
def list_reducer(state: list, writes: list) -> list:
out = list(state)
for w in writes:
out.extend(w)
return out
# Live: a single super-step receives [1] then Overwrite([50]).
live = DeltaChannel(list_reducer, list).from_checkpoint(MISSING)
live.update([[1], Overwrite([50])])
assert live.get() == [50]
# Reload: the same two writes are replayed from the checkpoint.
replayed = DeltaChannel(list_reducer, list).from_checkpoint(MISSING)
replayed.replay_writes(
[
("t1", "messages", [1]),
("t2", "messages", Overwrite([50])),
]
)
# Invariant: reconstructed state must equal live state.
assert replayed.get() == live.get()
def test_delta_channel_remove_message_and_replay() -> None:
"""RemoveMessage must round-trip correctly when writes are replayed."""
spec = DeltaChannel(_messages_delta_reducer, list)
+1 -1
View File
@@ -1382,7 +1382,7 @@ wheels = [
[[package]]
name = "langgraph"
version = "1.2.2"
version = "1.2.3"
source = { editable = "." }
dependencies = [
{ name = "langchain-core" },
+1 -1
View File
@@ -285,7 +285,7 @@ wheels = [
[[package]]
name = "langgraph"
version = "1.2.2"
version = "1.2.3"
source = { editable = "../langgraph" }
dependencies = [
{ name = "langchain-core" },
+1 -1
View File
@@ -298,7 +298,7 @@ wheels = [
[[package]]
name = "langgraph"
version = "1.2.2"
version = "1.2.3"
source = { editable = "../langgraph" }
dependencies = [
{ name = "langchain-core" },