Skip to content

fix(langchain): align Store deletion, validation and removal errors - #6324

Merged
t0saki merged 6 commits into
volcengine:mainfrom
ehz0ah:fix/langgraph-store-writes
Oct 20, 2026
Merged

t0saki merged 6 commits into
volcengine:mainfrom
ehz0ah:fix/langgraph-store-writes

Conversation

@ehz0ah

@ehz0ah ehz0ah commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

Description

Align OpenVikingStore mutations with their public LangGraph contracts:

  • On current main, synchronous put(namespace, key, None) raises TypeError and leaves the item in place. It now delegates to the existing deletion path, including removal of the search projection. Missing keys remain a no-op.
  • Invalid namespaces can collapse onto valid record URIs: ("users", "", "ada") can overwrite ("users", "ada"). Sync put() now reuses LangGraph's namespace validator before writing or dispatching deletion, matching the inherited async validation.
  • Removal previously suppressed every exception. Only missing-target errors are now ignored; permission, availability and timeout failures reach callers of deletion and index=False updates, including async and batch entrypoints.

Record and projection removal remain separate operations. A partial failure is reported, and callers can retry after resolving it. Existing URI encoding, Mapping support and unsupported-TTL behavior are preserved.

Human Involvement

  • A human participated in the implementation or review loop
  • This PR was generated entirely by AI agents without human participation in the loop

Related Issue

Fixes #5821
Fixes #5774
Fixes #6050

Consolidates the overlapping deletion fixes in #5823, #5886 and #6022, plus the related mutation fixes in #5777 and #6051.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Refactoring (no functional changes)
  • Performance improvement
  • Test update

Changes Made

  • Keep the runtime changes in the existing Store owner, reusing delete(), is_not_found_error() and LangGraph's validator.
  • Consolidate contributed tests in the adapter's existing integration test module. Cover invalid put(None) calls, existing/missing keys, index overrides, ordinary values, TTL rejection, independent items, partial cleanup, explicit retry and batch interruption.
  • Document deletion and error behavior in both integration guides.

Testing

  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • I have tested this on the following platforms:
    • Linux
    • macOS
    • Windows
python -m pytest -q -rs --noconftest -p no:cacheprovider -o addopts='' \
  integrations/frameworks/langchain/tests tests/unit/test_langchain_package_boundary.py
ruff check integrations/frameworks/langchain/src integrations/frameworks/langchain/tests \
  tests/unit/test_langchain_package_boundary.py
ruff format --check integrations/frameworks/langchain/src integrations/frameworks/langchain/tests \
  tests/unit/test_langchain_package_boundary.py
mypy --config-file integrations/frameworks/langchain/pyproject.toml \
  integrations/frameworks/langchain/src/langchain_openviking
uv build --offline --sdist --wheel integrations/frameworks/langchain --out-dir "$DIST"
python -m twine check "$DIST"/*
  • Python 3.12, LangChain 1.4.6 / core 1.6.12 / LangGraph 1.2.16: 481 passed, 1 skipped.
  • Python 3.10, LangChain / core / LangGraph 1.0.0: 480 passed, 2 skipped.
  • The 90 Store contract cases produce 59 failures / 31 passes against main 59ef89730, and all 90 pass here. The bugs were also reproduced independently through public Store calls before applying the commits.
  • Ruff lint/format, mypy for all 15 source modules, wheel/sdist builds and Twine checks pass.
  • Fresh base and LangGraph-extra installed-wheel smoke tests and dependency checks pass. All 15 wheel Python modules match source. The sdist's bundled suite against the installed wheel, outside the checkout: 476 passed, 1 skipped.
  • Public Store calls through the actual SDK and a controlled localhost HTTP fixture: 40 cases pass, both from source and the installed wheel. This covers all six deletion entrypoints, missing targets, HTTP 403/503 propagation, partial-removal retry, deindexing failures and invalid namespaces rejected without an HTTP request.

Skips are the optional SQLite checkpointer and, on Python 3.10, asyncio.timeout. Existing dependency deprecation warnings remain. The HTTP fixture uses deterministic storage and controlled errors; it does not validate a deployed backend, authorization policy, vector service or external model.

Checklist

  • My code follows the project's coding style
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • Any dependent changes have been merged and published

Screenshots (if applicable)

Not applicable.

Additional Notes

Original commits were cherry-picked with -x, preserving their authorship:

Overlapping deletion code and tests are consolidated. Test relocation, additional contract coverage and documentation updates are a separate signed-off commit by Hao Zhe. The source PRs remain open until this combined fix lands.

ktz03 and others added 6 commits October 20, 2026 12:17
(cherry picked from commit 9cc5841)
Signed-off-by: Hao Zhe <[email protected]>
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.

(cherry picked from commit 8578bd0)
Signed-off-by: Hao Zhe <[email protected]>
…olcengine#5821)

LangGraph's BaseStore.put() defines value=None as a deletion signal. The
OpenViking adapter's batch / async paths already routed None through
self.delete(...), but the sync put() override wrote the None through the
ordinary JSON storage path, persisting a record whose value is JSON null.

After the fix, put(namespace, key, None) delegates to delete(namespace, key)
and returns, matching the public BaseStore contract:

- get() returns None for the deleted key.
- search() no longer returns the deleted item.
- list_namespaces() no longer keeps the deleted item's namespace.
- Deleting a missing key is a no-op (no null-valued record is created).

The value type annotation is widened to "Mapping[str, Any] | None" to
match the call sites batch already uses (op.value is forwarded as-is).

(cherry picked from commit 564f8f1)
Signed-off-by: Hao Zhe <[email protected]>
(cherry picked from commit d9a8c82)
Signed-off-by: Hao Zhe <[email protected]>
Keep contributed deletion coverage in the adapter suite and remove overlapping assertions. Exercise sync, async and batch removal failures, partial cleanup and retry, invalid put(None) namespaces, independent items and unsupported TTL. Document partial removal behavior.

Signed-off-by: Hao Zhe <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

5 participants