langchain-ai / langchain-ai/langgraph
Topic/NamedBarrierValue from_checkpoint aliases the checkpoint container instead of copying
- Dominant language
- Python
- Stars
- 41.8k
- Forks
- 7.1k
- Avg merge
- 23h 7m
- Merged PRs (30d)
- 30
Description
### Description
`Topic.from_checkpoint` and `NamedBarrierValue.from_checkpoint` assign the checkpoint's container directly to the new channel instead of copying it:
```python
# topic.py
empty.values = checkpoint
# named_barrier_value.py
empty.seen = checkpoint
```
`BaseChannel.from_checkpoint` documents that "If the checkpoint contains complex data structures, they should be copied", and the channels' own `copy()` methods do copy (`self.values.copy()`, `self.seen.copy()`), but `from_checkpoint` does not. So two channels restored from the same checkpoint object share the same list/set, and an in-place update to one mutates the checkpoint and the sibling.
### Reproduction
```python
from langgraph.channels.topic import Topic
source = Topic(str, accumulate=True)
source.update(["a", "b"])
checkpoint = source.checkpoint()
one = Topic(str, accumulate=True).from_checkpoint(checkpoint)
two = Topic(str, accumulate=True).from_checkpoint(checkpoint)
one.update(["c"])
print(two.get()) # ['a', 'b', 'c'] -> expected ['a', 'b']
print(checkpoint) # ['a', 'b', 'c'] -> checkpoint was mutated
```
`NamedBarrierValue` (and `NamedBarrierValueAfterFinish`) behave the same way with their `seen` set. This matters when a single loaded checkpoint is reused across restores (replay / forking a thread): an `accumulate=True` topic or a barrier value can retroactively corrupt the persisted checkpoint.
### Expected
`from_checkpoint` should copy the container, like `copy()` already does, so restored channels are independent of the checkpoint and of each other.
### System info
langgraph main; the affected code is in `langgraph/channels/topic.py` and `langgraph/channels/named_barrier_value.py`.
Contributor guide
Research direction
Start with langgraph/channels/topic.py and langgraph/channels/named_barrier_value.py, then review BaseChannel.from_checkpoint and the channels' copy() methods. Reproduce the issue with the Topic example and add or run focused tests for Topic and NamedBarrierValue, including NamedBarrierValueAfterFinish. Done means restored channels are independent from the checkpoint and from one another.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- backend
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 78/100