Repository navigation
fix(langgraph): delete item when store put receives None - #5886
Open
varun-projects wants to merge 1 commit into
Open
varun-projects wants to merge 1 commit into
varun-projects wants to merge 1 commit into
Conversation
LangGraph's BaseStore contract treats a None value passed to put() as a deletion. OpenVikingStore.batch() already dispatched PutOp with a None value to delete(), and the inherited aput() reaches the same path through abatch(), but the sync put() override wrote None straight through the ordinary JSON record path. Before this change, put(namespace, key, None) stored a record whose value was null: get() returned an Item with value None, search() still listed the record, and list_namespaces() kept exposing the namespace. Calling it on a key that did not exist created a new null-valued record instead of doing nothing. The sync put() now delegates the None case to the existing delete(), after the unsupported-TTL guard so that put(..., None, ttl=...) still raises NotImplementedError and stays consistent with aput(). get() now returns None, search() omits the item, the namespace disappears once its last record is removed, and deleting a missing key is a no-op. The value annotation was widened to dict[str, Any] | None to match BaseStore.
varun-projects
force-pushed
the
fix/5821-store-put-none-deletes
branch
from
October 10, 2026 16:53
92f7f60 to
8578bd0
Compare
This branch has not been deployed
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.
Description
OpenVikingStore.put(namespace, key, None)persisted a record with anullvalue instead ofdeleting the item. LangGraph's
BaseStore.put()contract defines aNonevalue as adeletion. The adapter's
batch()already dispatchedPutOpwith aNonevalue todelete(), and the inheritedaput()reaches the same code throughabatch(), but the syncput()override wrote theNonestraight through the ordinary JSON record path.The sync
put()now delegates theNonecase to the existingdelete()method rather thanintroducing a second deletion path. The delegation sits after the existing unsupported-TTL
guard, so
put(..., None, ttl=...)continues to raiseNotImplementedErrorand staysconsistent with
aput(), which raises inBaseStorebefore it ever reachesabatch(). Thevalueannotation was widened todict[str, Any] | Noneto matchBaseStore.The issue was reproduced before the change, not merely inferred from the code: a standalone
script compared
OpenVikingStore(backed by the deterministicInMemoryOpenVikingClientfixture) against
langgraph.store.memory.InMemoryStoreas the reference implementation, andconfirmed the divergence on both an existing and a missing key. No deployed server or model
call was involved.
One correction to the issue's Expected Behavior that reviewers should be aware of:
InMemoryStoredoes not drop a namespace fromlist_namespaces()after deletion. Itkeeps an empty namespace entry behind after any deletion, including an explicit
delete(),as an artifact of its
defaultdictstorage.OpenVikingStorederives namespaces from thestored records, so a deleted item drops its namespace once it is the last record. That matches
this adapter's pre-existing
delete()behaviour and what the issue asks for, so it was leftas is, and the comparison against the reference store is scoped to
get()andsearch().Human Involvement
Related Issue
Fixes #5821
Type of Change
Changes Made
OpenVikingStore.put()now routes aNonevalue to the existingdelete()method andreturns, so
get()yieldsNone,search()omits the item, the namespace is dropped onceits last record is removed, and deleting a missing key is a no-op.
put()valueannotation todict[str, Any] | Noneto match theBaseStoresignature.
test_langgraph_store_round_trip_and_semantic_searchcontract test tocover deletion through
put(None)on both an existing and a missing key.docs/en/agent-integrations/07-langchain-langgraph.mdand itsdocs/zh/translation, whichpreviously did not mention deletion at all.
Testing
Validated with the same commands as the repository's own
langchain-testsCI job, afteruv pip install -e sdk/pythonanduv pip install -e "examples/langchain[langgraph,test,dev]":That count matches this branch's unmodified base, since this extends an existing test rather
than adding one.
The extended assertions were confirmed to guard the regression: with the
store.pychangereverted and the new test kept, the test fails with
AssertionError: assert Item(..., value=None, ...) is None; with the change applied itpasses.
A temporary comparison script against LangGraph's
InMemoryStoreconfirmed the reportedbehaviour before the change and the corrected behaviour after it. It also confirmed that the
aput(),batch(),abatch(), and explicitdelete()paths were already correct andneeded no edits —
OpenVikingStoredoes not overrideaput, so it routesaput -> abatch -> batch -> PutOp(value=None) -> delete().The deletion dispatch runs before the existing
namespace = tuple(namespace)normalization, soI also confirmed that a caller passing a list namespace still deletes correctly:
put(["u", "a"], "k", None)leavesget()returningNoneandsearch()empty.delete()only unpacks the namespace when building URIs, so both sequence types behave identically.
Checklist
Additional Notes
The TTL guard intentionally runs before the deletion dispatch so the sync and async public
methods agree:
BaseStore.aputraises for an unsupported TTL before dispatching, andOpenVikingStore.supports_ttlisFalse, so placing theNonecheck first would have madesync
putdelete silently whereaputraises. BothBaseStore.putandBaseStore.aputlikewise validate
ttlbefore inspectingvalue.A separate pre-existing inconsistency remains out of scope:
batch([PutOp(..., value=None, ttl=...)])deletes without raising, becausebatchcallsdelete()without forwardingttl. Backend-removal error handling for #5774 is being addressed separately in #5777 and isunchanged here.