fix(langgraph): resolve 4 bugs in channels, graph branching, and pregel subgraph traversal#6944
Open
Subham Singhania (Subhamsinghania18) wants to merge 4 commits intolangchain-ai:mainfrom
Conversation
…OperatorAggregate MISSING fast-path When a BinaryOperatorAggregate channel has no default constructor and self.value starts as MISSING, the update() method fast-pathed the first value directly into self.value without inspecting it. This had two observable bugs: 1. An Overwrite(x) wrapper was stored verbatim, so channel.get() returned Overwrite(value=x) instead of x. Graph output was silently wrong. 2. seen_overwrite was never set in the MISSING branch, so passing two Overwrite() values in the same super-step raised no error, violating the documented one-Overwrite-per-super-step contract. The fix moves seen_overwrite initialisation before the MISSING check and runs _get_overwrite() on the first value just like the loop does for all subsequent values. Closes: langchain-ai#6909
…and copy Topic.from_checkpoint() only guarded against MISSING. When a channel key exists in checkpoint['channel_values'] with an explicit None value (observed with the PostgreSQL checkpointer after partial writes), None was stored in self.values. The subsequent call to Topic.copy() then crashed with: AttributeError: 'NoneType' object has no attribute 'copy' This surfaced at runtime inside local_read(fresh=True), typically triggered by a conditional edge or middleware retry, making it a hard crash with no recovery path. Changes: - from_checkpoint: treat None identically to MISSING so self.values always defaults to the empty list defined by __init__. - copy: add a defensive guard (self.values is not None) so even if None leaks in from an unexpected path the method degrades gracefully to an empty list rather than crashing. Closes: langchain-ai#6791
…ath_map When add_conditional_edges() is called with an explicit path_map, the _finish() method blindly looked up every router return value in self.ends. If the router returned '__end__' (END) without a corresponding '__end__': '__end__' entry in the map it raised: KeyError: '__end__' This was inconsistent: when no path_map is supplied, returning END works fine. The fix special-cases r == END so that it passes through directly without a dict lookup, matching the no-path_map behaviour and user expectations. Returning END without an explicit mapping is intuitive — users should not have to enumerate the terminal condition as a dict entry just because they supplied other mappings. Closes: langchain-ai#6770
get_subgraphs() used str.startswith(name) to filter nodes by namespace.
If two sibling nodes share a common name prefix — e.g. 'agent' and
'agent_v2' — and are added in that order, the iteration could match the
shorter name ('agent') against a namespace meant for the longer one
('agent_v2|child'), then strip the wrong number of characters from the
namespace string, producing garbage like 'v2|child' for the recursive
call. The recursion found nothing and the method returned an empty
iterator instead of the expected subgraph.
The fix replaces the startswith check with an exact delimiter-aware test:
namespace == name or namespace.startswith(name + NS_SEP)
This ensures 'agent' only matches the namespace 'agent' or 'agent|...',
never 'agent_v2|...'. NS_SEP is the '|' constant already used
throughout the module.
aget_subgraphs() delegates entirely to get_subgraphs(), so a single
change is sufficient.
Closes: langchain-ai#6924
Joehunk
reviewed
Mar 1, 2026
| # incorrectly match the longer namespace. Fixes #6924. | ||
| if namespace is not None: | ||
| if not namespace.startswith(name): | ||
| if namespace != name and not namespace.startswith(name + NS_SEP): |
There was a problem hiding this comment.
Note, already fixed in #6366 (which also includes a repro test case).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Four independent bug fixes in the core
langgraphpackage:BinaryOperatorAggregate:Overwritenot unwrapped when initial value isMISSINGWhen a channel has no default constructor (
self.value = MISSING), theupdate()fast-path assigned the first value raw without calling_get_overwrite(). This stored theOverwrite(x)wrapper object insteadof
x, producing silently wrong graph output. It also never setseen_overwrite, so passing twoOverwritevalues in the same super-stepraised no error, violating the one-Overwrite-per-super-step contract.
Closes #6909
Topic.copy()crashes withAttributeErrorwhen checkpoint holdsNoneTopic.from_checkpoint()only guarded againstMISSING. When a channelkey existed in
checkpoint["channel_values"]with an explicitNone(observedwith the PostgreSQL saver after partial writes),
Nonewas stored inself.values. The subsequent call toTopic.copy()inlocal_read()thencrashed:
AttributeError: 'NoneType' object has no attribute 'copy'
from_checkpointnow treatsNoneidentically toMISSING.copy()hasan added defensive guard for belt-and-suspenders safety.
Closes #6791
3.
KeyError('__end__')when conditional router returnsENDwithout explicitpath_mapentry_finish()inBranchSpeclooked up every router return value inself.endsunconditionally. If a router returnedENDwithout an explicit"__end__": "__end__"entry in the path map, it raisedKeyError.ENDis now treated as a pass-through, consistent with the no-path-mapbehaviour.
Closes #6770
4.
get_subgraphs()fails when node names share a common string prefixget_subgraphs()usednamespace.startswith(name)to filter nodes. Ifsibling nodes share a prefix (e.g.
"agent"and"agent_v2"), the shortername false-matched the longer namespace, stripped the wrong number of
characters, and passed a garbage string into the recursive call — returning
an empty iterator instead of the expected subgraph.
The fix uses an exact delimiter-aware check:
namespace == name or namespace.startswith(name + "|")Closes #6924
Issue: #6909, #6791, #6770, #6924
Dependencies: None