Skip to content

fix(langmem-vectorize): search() crashes on empty namespace prefix with no query - #51

Closed
mittalpk wants to merge 1 commit into
cloudflare:mainfrom
mittalpk:fix/langmem-search-empty-namespace-query
Closed

mittalpk wants to merge 1 commit into
cloudflare:mainfrom
mittalpk:fix/langmem-search-empty-namespace-query

Conversation

@mittalpk

@mittalpk mittalpk commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

CloudflareVectorizeBaseStore.search() only assigns docs_and_scores when query is truthy, or when query is falsy and namespace_prefix_str is truthy. If both are empty — store.search(()), listing everything with no query, a documented valid LangGraph BaseStore usage — neither branch runs, and the code falls through to for doc, score in docs_and_scores:, raising UnboundLocalError.

An empty namespace prefix is a valid "search everything" request (every namespace string starts with ""), so the dummy-query fetch now runs unconditionally whenever no query is given, and the existing namespace-prefix filtering loop below handles the rest.

Added tests/unit_tests/test_search_empty_namespace.py. Confirmed the crash reproduces on unpatched code (git stash) with the exact UnboundLocalError, and passes after the fix. ruff check and mypy clean.

…th no query

docs_and_scores was only assigned when namespace_prefix_str was truthy,
so store.search(()) -- listing everything with no query, a documented
valid LangGraph BaseStore usage -- raised UnboundLocalError instead of
returning results. An empty prefix is a valid 'search everything'
request since every namespace starts with the empty string; the dummy-
query fetch now runs unconditionally when no query is given.
CollierKing added a commit that referenced this pull request Aug 18, 2026
…add new deepseek models (#53)

* fix: decode base64 metadata blobs and fix sync list() filtering

get_tuple/aget_tuple/list/alist read the checkpoints.metadata BLOB
column without base64-decoding it first, so every returned
CheckpointMetadata silently fell back to {"step": -2} (or an empty
dict in list/alist) instead of the real stored metadata. Fixed via a
shared decode_metadata_blob helper used by all four call sites across
CloudflareD1Saver, AsyncCloudflareD1Saver, since none of this had
integration test coverage before now.

Also fixes CloudflareD1Saver.list() (sync) building its own ad-hoc
WHERE clause that only supported exact-match filtering on
thread_id/checkpoint_id, silently dropping every other filter key and
never referencing the before parameter at all, unlike alist() (which
already delegated to search_where() correctly). Rewrote list() to use
search_where(), and fixed a related bug in search_where()'s metadata
predicate builder where the bool branch was checked after the int
branch and was therefore unreachable (bool is a subclass of int in
Python), so boolean filter values were passed through as raw
True/False instead of the 1/0 SQLite's json_extract() returns.

Adds REST API integration tests against live D1 (previously this
package had none, for either the sync or async saver).

Co-authored-by: Praveen Mittal <pkmittal28@gmail.com>

* feat: add WorkerCloudflareD1Saver for Cloudflare Python Workers

Adds a checkpoint saver for use inside Cloudflare Python Workers,
talking to D1 through the native Worker binding (env.DB) via
sqlalchemy-cloudflare-d1's WorkerConnection instead of the REST API --
no network round-trip to the Cloudflare API, no API token. Installable
via the new optional worker extra:
pip install 'langgraph-checkpoint-cloudflare-d1[worker]'

Implements both the native async methods (for graph.ainvoke/astream)
and, mirroring sqlalchemy-cloudflare-d1's SyncWorkerConnection,
synchronous methods bridged via pyodide.ffi.run_sync(), for calling
the saver directly outside of a compiled graph. graph.invoke() (sync)
itself still can't run inside a Worker with any checkpointer attached
-- LangGraph's sync Pregel loop always submits checkpoint writes to a
real ThreadPoolExecutor, which Workers/Pyodide can't create.

Adds examples/workers/, a Python Worker example exercising the saver
end to end, including a full StateGraph compiled with it as its
checkpointer, and REST/Worker integration test coverage mirroring the
split already used in sqlalchemy-cloudflare-d1 and langchain-cloudflare.

* fix(langmem-vectorize): search() crashes on empty namespace prefix with no query

docs_and_scores was only assigned when namespace_prefix_str was truthy,
so store.search(()) -- listing everything with no query, a documented
valid LangGraph BaseStore usage -- raised UnboundLocalError instead of
returning results. An empty prefix is a valid 'search everything'
request since every namespace starts with the empty string; the dummy-
query fetch now runs unconditionally when no query is given.

* fix(langchain-cloudflare): Vectorize/index bugs found building langmem Worker support

- add_documents/aadd_documents ignored explicit ids in some code paths,
  silently falling back to generated ones
- _poll_mutation_status/_apoll_mutation_status had no timeout, so a single
  stuck API response could hang indefinitely
- _index_is_ready checked fields the /info endpoint never returns, so
  create_index(wait=True) could never detect readiness
- integration tests now poll for Vectorize's eventual-consistency window
  after writes instead of asserting immediately

* feat(langmem-cloudflare-vectorize): add Python Worker support + REST/Worker integration tests

Extends CloudflareVectorizeBaseStore with the same binding/Worker pattern
already established in langchain-cloudflare and
langgraph-checkpoint-cloudflare-d1: binding=/d1_binding= constructor params,
real async implementations (aget/aput/adelete/asearch/abatch), and sync
methods that bridge to them via pyodide.ffi.run_sync() inside a Worker.

Also gives the package REST + Worker integration test coverage it didn't
have before (examples/workers/, tests/integration_tests/,
tests/worker_tests/), matching the other two libraries' test structure.

* fix(langchain-cloudflare): harden REST client layer

Reliability fixes found via full integration suite validation:
- streamed chunk usage included a per-chunk "neurons" float that
  langchain_core's chunk merge can't combine across chunks, crashing any
  stream longer than one token
- mistral-small-3.1 and gpt-oss don't support tool_choice reliably, so
  tool-calling-based structured output silently returned None; routed both
  through json_schema instead, with a max_tokens floor for reasoning
  models so the reasoning phase can't exhaust the budget before answering
- CloudflareWorkersAIReranker's REST calls had no request timeout at all

Also unifies AI Gateway routing with the standard Workers AI endpoint
(chat_models.py, embeddings.py, rerankers.py): AI Gateway no longer uses a
separate gateway.ai.cloudflare.com host, routing instead via a
cf-aig-gateway-id header on the same endpoint everything else uses --
see https://blog.cloudflare.com/workers-ai-gateway-unification/

* test(langchain-cloudflare): mark known-flaky models for retry, drop deprecated kimi-k2.5

kimi-k2.5 is deprecated (planned_deprecation_date already past per the
Workers AI models API) and superseded by kimi-k2.6, already in the model
matrix and covering the same capabilities.

gemma-4-26b-a4b-it is not deprecated, just intermittently slow (200s+ on
some calls, passing in seconds on immediate retry) -- adds
pytest-rerunfailures and wraps it with a rerun marker instead of letting
the whole suite fail on model-specific, not code, flakiness.

* chore: enforce E501 line-length lint consistently across all packages

langchain-cloudflare enforced ruff's E501 while
langgraph-checkpoint-cloudflare-d1 and langmem-cloudflare-vectorize both
ignored it, so the same lint gate meant different things per package.
Standardized on enforcing it everywhere and rewrapped the resulting
violations (docstrings, log/error f-strings, and SQL statement strings)
without changing behavior.

* chore: bump versions and update changelog for this session's changes

langchain-cloudflare (0.3.6 -> 0.3.7): Vectorize/chat-model/reranker
fixes and the AI Gateway unification.
langmem-cloudflare-vectorize (0.1.1 -> 0.1.2): Worker binding support,
the PR #51 empty-namespace search fix, and the with_cloudflare_embeddings
index_name propagation fix. Backfilled a changelog section for this
package, which previously had none.

* fix(reranker): _process_response never actually sorts results by score

Every docstring on this reranker (rerank(), arerank(), and
_process_response() itself) promises results "sorted by relevance
score (descending)", but the implementation just appended results in
whatever order the API response listed them -- no sort() call
anywhere. Nothing in the test suite exercised this at all; the only
existing tests cover credential validation.

Added the sort and two tests covering both an out-of-order response
and an already-sorted one.

* feat(langchain-cloudflare): add DeepSeek V4 Pro and Flash models

Adds @cf/deepseek-ai/deepseek-v4-pro-0813 and
@cf/deepseek-ai/deepseek-v4-flash-0731 to the tested and
example-supported model lists. Both are the first Workers AI models
with a full 1,048,576-token context window and support thinking mode
(reasoning_content) and function calling; deepseek-v4-pro additionally
supports vision (image input), deepseek-v4-flash is text/audio only.
Both require the Workers Paid plan or prepaid AI Gateway credits.

Verified live before assuming standard reasoning-model behavior:
tool_choice="required" returns clean tool_calls with empty content,
and reasoning shows up under the existing reasoning_content field --
so both models use the existing _REASONING_BEHAVIOR entry rather than
a bespoke one.

* test(langchain-cloudflare): mark gpt-oss-120b as known-flaky (structured output)

gpt-oss-120b intermittently produces runaway/degenerate output on
structured-output requests -- a JSON field filled with thousands of
repeated "-001" fragments (~20KB+), sometimes large/slow enough to
trip a 500 instead of returning. Confirmed live by hitting the raw
endpoint directly, repeatedly, outside pytest: 2 of 3 calls reproduced
it, 1 returned a normal response. Not a code bug -- a live
model-quality/stability issue on Cloudflare's platform for this model,
same category as the existing gemma-4-26b-a4b-it handling.

Wrapped with the existing _model_param()/FLAKY_MODELS rerun mechanism
in both integration test files rather than adding a second one.

* test(langchain-cloudflare): mark llama-3.3-70b structured-output tests as flaky

llama-3.3-70b-instruct-fp8-fast intermittently returns zero chunks when
streaming a structured-output response ("Stream returned no chunks --
possible API issue" from langchain_tests itself), while the equivalent
non-streaming invoke() call succeeds. A live API/model reliability
issue, not a code bug -- confirmed pre-existing by running the same
tests against a clean checkout with none of this session's changes,
and intermittent (a different subset fails each run, never all).

This file has no existing FLAKY_MODELS/_model_param() infrastructure
to hook into (it's a fixed single-model LangChain conformance suite,
not a parametrized model list), so the affected inherited test methods
are overridden here just to attach pytest.mark.flaky(reruns=2,
reruns_delay=5), re-applying the base class's parametrize decorator
where present so all schema_type variants still run.

Verified live: test_structured_output[json_schema] and
test_structured_output_async[json_schema] both hit the failure and
were correctly retried to a pass by the new marker.

* fix: CI lint failures (ruff version drift + missing dev-group dependency)

CI failed across every lint job and the pre-commit job on first run,
since none of this branch's code had ever actually gone through CI
before this PR. Two distinct root causes, neither reproducible via
the local venvs used earlier in this branch's history:

1. uv.lock is gitignored repo-wide, so both CI and any local machine
   re-resolve ruff fresh against PyPI on every sync with no pinning.
   Local venvs across all three packages had drifted to older ruff
   (0.14.10-0.15.18) than what CI resolves (0.16.x), which changed
   markdown-formatting and default-rule-selection behavior. Refreshed
   all three packages' environments to the current resolution and
   applied the resulting formatting fixes (langchain-cloudflare's and
   langmem-cloudflare-vectorize's READMEs, a langmem Worker test's
   line wrapping) plus two newly-caught pre-existing lint violations
   in root-level scripts (typing.Dict -> dict, an intentional blind
   except needing an explicit noqa).

2. langgraph-checkpoint-cloudflare-d1's worker.py imports
   sqlalchemy-cloudflare-d1, which was only declared in the `test`
   dependency group, not `dev` -- CI's lint job syncs `--group dev`
   only, so mypy couldn't resolve the import. Added it to `dev` too.

Verified by simulating CI's exact per-job sync target (dev-only for
lint, dev+test for test) in a clean venv, not just re-running the
combined local dev environment.

* fix: stop gitignoring uv.lock for the 4 CI-relevant packages

uv.lock was gitignored repo-wide with no stated rationale (added as an
aside in an unrelated commit). Without a committed lockfile, every
`uv sync` -- local or CI -- re-resolves dependencies fresh against
PyPI, so a local venv resolved months ago silently drifts from
whatever CI gets on its next run. This is what caused PR #53's lint
failures: local ruff was 3-4 releases behind what CI installed.

Un-ignore and commit the lockfile for root and the three libs/*
packages CI's _lint.yml/_test.yml actually run against, pinned to the
versions already verified passing. Leave examples/workers/uv.lock
gitignored -- those are pywrangler-managed Pyodide resolutions, never
touched by CI, and already known to need manual re-vendoring per
DEVELOPMENT.md, so pinning them doesn't prevent a real CI failure the
way the package-level locks do.

---------

Co-authored-by: Collier King <collier@example.com>
Co-authored-by: Praveen Mittal <pkmittal28@gmail.com>
@CollierKing

Copy link
Copy Markdown
Collaborator

Thank you for this — store.search(()) is a documented valid usage, and an UnboundLocalError there instead of just returning everything is a nasty one to hit in production.

This fix has been incorporated into #53, which just merged into main (commit 9437e4c). Closing this in favor of that — appreciate you tracking it down!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants