From 72ac8456168554e35287d1c232d25eec92708135 Mon Sep 17 00:00:00 2001 From: codesheepchen Date: Tue, 20 Oct 2026 10:41:23 +0800 Subject: [PATCH 1/6] fix(langchain): resolve framework relocation conflicts Merge upstream main while preserving both separator-key regressions and upstream mapping-value tests. Use the relocated package in the boundary check. Validation: 404 tests passed, 1 skipped (optional SQLite checkpointer unavailable); Ruff, mypy, build and twine checks passed. (cherry picked from commit a5752a0d36aad1b581e311925e6b74c92e3778ee) --- .../src/langchain_openviking/store.py | 23 +++- .../tests/unit/test_langchain_integration.py | 104 ++++++++++++++++++ 2 files changed, 124 insertions(+), 3 deletions(-) diff --git a/integrations/frameworks/langchain/src/langchain_openviking/store.py b/integrations/frameworks/langchain/src/langchain_openviking/store.py index e185e942b1..f8671d3434 100644 --- a/integrations/frameworks/langchain/src/langchain_openviking/store.py +++ b/integrations/frameworks/langchain/src/langchain_openviking/store.py @@ -67,6 +67,9 @@ class OpenVikingStore(BaseStore): markdown projection under ``/index`` gives OpenViking semantic retrieval a compact document to index for query-based ``search`` calls. Removal ignores missing targets and propagates other backend errors. + Every key remains one URI path segment. Encoded slash and backslash + separators use adapter-level escape tokens because OpenViking rejects + ``%2F`` and ``%5C`` within a URI segment. Args: root_uri: Base URI for the store. Defaults to the ``viking://~`` home alias, @@ -444,10 +447,10 @@ def _remove(self, uri: str) -> None: logger.debug("OpenVikingStore remove ignored missing URI", exc_info=True) def _data_uri(self, namespace: tuple[str, ...], key: str) -> str: - return f"{self._data_prefix_uri(namespace)}/{_segment(key)}.json" + return _record_uri(self._data_prefix_uri(namespace), key, ".json") def _index_uri(self, namespace: tuple[str, ...], key: str) -> str: - return f"{self._index_prefix_uri(namespace)}/{_segment(key)}.md" + return _record_uri(self._index_prefix_uri(namespace), key, ".md") def _data_prefix_uri(self, namespace: tuple[str, ...]) -> str: return _join_uri(self.root_uri, "data", *namespace) @@ -508,6 +511,20 @@ def _join_uri(root: str, *segments: str) -> str: return root.rstrip("/") if not suffix else f"{root.rstrip('/')}/{suffix}" +def _record_uri(prefix: str, key: str, suffix: str) -> str: + return f"{prefix}/{_encode_key_segment(key)}{suffix}" + + +def _encode_key_segment(key: str) -> str: + # quote(..., safe="") encodes a literal "!" as "%21", leaving bare "!" + # available as an unambiguous adapter escape marker. + return _segment(key).replace("%2F", "!s").replace("%5C", "!b") + + +def _decode_key_segment(segment: str) -> str: + return unquote(segment.replace("!s", "%2F").replace("!b", "%5C")) + + def _parse_canonicalized_record_uri( *, root_uri: str, @@ -559,7 +576,7 @@ def _parse_record_parts(parts: list[str]) -> tuple[tuple[str, ...], str] | None: if not parts or not parts[-1]: return None namespace = tuple(unquote(part) for part in parts[:-1]) - key = unquote(parts[-1]) + key = _decode_key_segment(parts[-1]) return namespace, key diff --git a/integrations/frameworks/langchain/tests/unit/test_langchain_integration.py b/integrations/frameworks/langchain/tests/unit/test_langchain_integration.py index 861055851d..66c4b33677 100644 --- a/integrations/frameworks/langchain/tests/unit/test_langchain_integration.py +++ b/integrations/frameworks/langchain/tests/unit/test_langchain_integration.py @@ -1805,6 +1805,110 @@ def test_langgraph_store_unsupported_ttl_still_rejects_before_mutation( assert store.get(namespace, "item").value == original +def test_langgraph_store_round_trips_separator_keys_as_single_uri_segments(): + client = InMemoryOpenVikingClient() + store = OpenVikingStore(client=client) + namespace = ("filesystem", "user-a") + cases = { + "profile": "profile", + "/docs/readme.md": "!sdocs!sreadme.md", + "notes/todo.md": "notes!stodo.md", + "/": "!s", + "/docs/": "!sdocs!s", + "docs//readme.md": "docs!s!sreadme.md", + r"docs\readme.md": "docs!breadme.md", + "!sAGENTS.md": "%21sAGENTS.md", + "!bAGENTS.md": "%21bAGENTS.md", + "%2F": "%252F", + ".": ".", + "..": "..", + "中/文": "%E4%B8%AD!s%E6%96%87", + } + + for key, encoded in cases.items(): + policy = "cobalt deployment" if key == "/docs/readme.md" else "amber checklist" + store.put(namespace, key, {"key": key, "policy": policy}) + data_uri = f"viking://~/memories/langgraph_store/data/filesystem/user-a/{encoded}.json" + index_uri = f"viking://~/memories/langgraph_store/index/filesystem/user-a/{encoded}.md" + assert data_uri in client.records + assert index_uri in client.records + + assert all("%2F" not in uri for uri in client.records) + + for key in cases: + item = store.get(namespace, key) + assert item is not None + assert item.key == key + assert item.value["key"] == key + + assert {item.key for item in store.search(namespace, limit=100)} == set(cases) + semantic = store.search(namespace, query="cobalt", limit=5) + assert [item.key for item in semantic] == ["/docs/readme.md"] + assert store.list_namespaces(prefix=("filesystem",)) == [namespace] + + store.delete(namespace, "/docs/readme.md") + assert ( + "viking://~/memories/langgraph_store/data/filesystem/user-a/!sdocs!sreadme.md.json" + ) not in client.records + assert ( + "viking://~/memories/langgraph_store/index/filesystem/user-a/!sdocs!sreadme.md.md" + ) not in client.records + + +def test_langgraph_store_distinguishes_separator_escapes_from_literal_markers(): + client = InMemoryOpenVikingClient() + store = OpenVikingStore(client=client) + namespace = ("filesystem", "user-a") + cases = { + "/AGENTS.md": "!sAGENTS.md", + "!sAGENTS.md": "%21sAGENTS.md", + r"\AGENTS.md": "!bAGENTS.md", + "!bAGENTS.md": "%21bAGENTS.md", + } + + for key in cases: + store.put(namespace, key, {"key": key}) + + expected_data_uris = { + f"viking://~/memories/langgraph_store/data/filesystem/user-a/{encoded}.json" + for encoded in cases.values() + } + assert expected_data_uris.issubset(client.records) + assert len(expected_data_uris) == len(cases) + + for key in cases: + item = store.get(namespace, key) + assert item is not None + assert item.key == key + assert item.value["key"] == key + + +def test_langgraph_store_keeps_namespace_and_key_identity_collision_free(): + client = InMemoryOpenVikingClient() + store = OpenVikingStore(client=client) + marker = "__openviking_path_key_v1__" + path_identity = (("filesystem", "user-a"), "/docs") + namespace_identity = (("filesystem", "user-a", marker, "abs"), "docs") + + store.put(*path_identity, {"identity": "path-key"}) + store.put(*namespace_identity, {"identity": "namespace"}) + + path_uri = "viking://~/memories/langgraph_store/data/filesystem/user-a/!sdocs.json" + namespace_uri = ( + "viking://~/memories/langgraph_store/data/filesystem/user-a/" + "__openviking_path_key_v1__/abs/docs.json" + ) + assert path_uri in client.records + assert namespace_uri in client.records + assert path_uri != namespace_uri + assert store.get(*path_identity).value["identity"] == "path-key" + assert store.get(*namespace_identity).value["identity"] == "namespace" + assert store.list_namespaces(prefix=("filesystem",)) == [ + ("filesystem", "user-a"), + ("filesystem", "user-a", marker, "abs"), + ] + + @pytest.mark.parametrize("use_batch", [False, True]) def test_langgraph_store_accepts_mapping_values(use_batch): store = OpenVikingStore(client=InMemoryOpenVikingClient()) From 21a6dd500d0bbc02e4cac316be4e02107b79ce08 Mon Sep 17 00:00:00 2001 From: ydflow <314143294+ydflow@users.noreply.github.com> Date: Thu, 29 Oct 2026 22:49:15 +0800 Subject: [PATCH 2/6] fix(langchain): propagate Store directory listing failures (cherry picked from commit dac457efd2f9c49e70fc4efbe76a720915730576) --- .../src/langchain_openviking/store.py | 4 +- .../frameworks/langchain/tests/test_store.py | 46 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/integrations/frameworks/langchain/src/langchain_openviking/store.py b/integrations/frameworks/langchain/src/langchain_openviking/store.py index f8671d3434..528d580e01 100644 --- a/integrations/frameworks/langchain/src/langchain_openviking/store.py +++ b/integrations/frameworks/langchain/src/langchain_openviking/store.py @@ -361,7 +361,9 @@ def _all_data_uris(self, namespace_prefix: tuple[str, ...]) -> list[str]: for pattern in ("*.json", "**/*.json"): try: result = call_openviking(self._client(), "glob", pattern=pattern, uri=base_uri) - except Exception: + except Exception as exc: + if not is_not_found_error(exc): + raise continue for uri in _extract_uris(result): if uri not in seen: diff --git a/integrations/frameworks/langchain/tests/test_store.py b/integrations/frameworks/langchain/tests/test_store.py index 024d8a8e43..f53f84d971 100644 --- a/integrations/frameworks/langchain/tests/test_store.py +++ b/integrations/frameworks/langchain/tests/test_store.py @@ -123,6 +123,52 @@ def test_store_retains_explicit_missing_record_behavior( assert result is None +@pytest.mark.parametrize("asynchronous", [False, True]) +@pytest.mark.parametrize("operation", ["search", "list_namespaces"]) +@pytest.mark.parametrize("failed_pattern", ["*.json", "**/*.json"]) +@pytest.mark.parametrize( + "error", + [UnavailableError("test listing"), PermissionDeniedError("test listing"), TimeoutError()], +) +def test_store_propagates_listing_errors( + monkeypatch, operation, error, failed_pattern, asynchronous +): + client = InMemoryOpenVikingClient() + store = OpenVikingStore(client=client) + store.put(("memory",), "note", {"version": 1}) + original_records = dict(client.records) + original_glob = client.glob + + def glob(pattern, uri): + if pattern == failed_pattern: + raise error + return original_glob(pattern, uri) + + monkeypatch.setattr(client, "glob", glob) + method = getattr(store, ("a" if asynchronous else "") + operation) + args = (("memory",),) if operation == "search" else () + with pytest.raises(type(error)) as caught: + result = method(*args) + if asynchronous: + asyncio.run(result) + assert caught.value.args == error.args + assert client.records == original_records + + +@pytest.mark.parametrize("asynchronous", [False, True]) +@pytest.mark.parametrize("operation", ["search", "list_namespaces"]) +@pytest.mark.parametrize( + "error", [FileNotFoundError("missing root"), NotFoundError("missing root")] +) +def test_store_returns_empty_for_missing_listing_root(monkeypatch, operation, error, asynchronous): + client = InMemoryOpenVikingClient() + store = OpenVikingStore(client=client) + monkeypatch.setattr(client, "glob", Mock(side_effect=error)) + method = getattr(store, ("a" if asynchronous else "") + operation) + result = method(("memory",)) if operation == "search" else method() + assert (asyncio.run(result) if asynchronous else result) == [] + + NAMESPACES = ( ("users", "ada", "prefs"), ("users", "grace", "prefs"), From 48bf061f900ee24bd93e553a96373f00c59400a3 Mon Sep 17 00:00:00 2001 From: stefanielizeth73-art Date: Tue, 3 Nov 2026 20:01:30 +0800 Subject: [PATCH 3/6] fix(langchain): skip projection for empty Store index fields (cherry picked from commit 50b5286bc813b8e30c45470c3fd107b15f516bfb) --- .../07-langchain-langgraph.md | 2 ++ .../07-langchain-langgraph.md | 3 ++ .../src/langchain_openviking/store.py | 2 +- .../frameworks/langchain/tests/test_store.py | 28 ++++++++++++++++++- 4 files changed, 33 insertions(+), 2 deletions(-) diff --git a/docs/en/agent-integrations/07-langchain-langgraph.md b/docs/en/agent-integrations/07-langchain-langgraph.md index 52c83d5273..11622d3f22 100644 --- a/docs/en/agent-integrations/07-langchain-langgraph.md +++ b/docs/en/agent-integrations/07-langchain-langgraph.md @@ -281,6 +281,8 @@ when `root_uri` uses a peer home alias such as `viking://~/peers/support/memorie Filter keys `eq`, `ne`, `gt`, `gte`, `lt`, `lte` and `in` are reserved operator aliases at every nesting level. Use `index=["items[*].text"]` to index selected array fields while retaining the complete stored value. Existing index projections are updated when the item is written again. +An empty field list (`index=[]`), including a constructor default, removes the projection +like `index=False`; the value remains available to `get()` and nonsemantic search. ### LangGraph middleware diff --git a/docs/zh/agent-integrations/07-langchain-langgraph.md b/docs/zh/agent-integrations/07-langchain-langgraph.md index 9de3f185ee..247ac5faa5 100644 --- a/docs/zh/agent-integrations/07-langchain-langgraph.md +++ b/docs/zh/agent-integrations/07-langchain-langgraph.md @@ -269,6 +269,9 @@ store.close() 使用 `index=["items[*].text"]` 可以只索引数组中的指定字段,同时保留完整的存储值。 已有条目的索引会在再次写入该条目时更新。 +空字段列表(`index=[]`)与 `index=False` 一样移除索引投影,构造时的默认值也遵循此规则; +完整值仍可通过 `get()` 和非语义搜索读取。 + ### LangGraph middleware ```python diff --git a/integrations/frameworks/langchain/src/langchain_openviking/store.py b/integrations/frameworks/langchain/src/langchain_openviking/store.py index 528d580e01..03b196c7bd 100644 --- a/integrations/frameworks/langchain/src/langchain_openviking/store.py +++ b/integrations/frameworks/langchain/src/langchain_openviking/store.py @@ -233,7 +233,7 @@ def put( effective_index = self.index if index is None else index index_uri = self._index_uri(namespace, key) - if effective_index is False: + if effective_index is False or effective_index == []: self._remove(index_uri) return self._write(index_uri, self._index_document(record, effective_index)) diff --git a/integrations/frameworks/langchain/tests/test_store.py b/integrations/frameworks/langchain/tests/test_store.py index f53f84d971..45f27ed98c 100644 --- a/integrations/frameworks/langchain/tests/test_store.py +++ b/integrations/frameworks/langchain/tests/test_store.py @@ -260,7 +260,12 @@ def test_store_keeps_existing_nonpositive_namespace_depth_behavior(stores, max_d ) -def test_store_keeps_namespace_validation_and_persisted_values(stores): +@pytest.mark.parametrize("operation", ["put", "aput", "batch", "abatch"]) +@pytest.mark.parametrize("use_default", [False, True]) +@pytest.mark.parametrize("existing", [False, True]) +def test_store_keeps_namespace_validation_and_persisted_values( + stores, operation, use_default, existing +): store, _ = stores with pytest.raises(InvalidNamespaceError, match="Namespace cannot be empty"): store.put((), "root", {"number": -1}) @@ -268,6 +273,27 @@ def test_store_keeps_namespace_validation_and_persisted_values(stores): for number, namespace in enumerate(NAMESPACES): assert store.get(namespace, "item").value == {"number": number} + client = InMemoryOpenVikingClient() + store = OpenVikingStore(client=client, index=[] if use_default else None) + namespace, key = ("memory",), "excluded-note" + if existing: + store.put(namespace, key, {"text": "old"}, index=["text"]) + value = {"text": "still stored"} + index: list[str] | None = None if use_default else [] + if operation == "put": + store.put(namespace, key, value, index=index) + elif operation == "aput": + asyncio.run(store.aput(namespace, key, value, index=index)) + else: + ops = [PutOp(namespace, key, value, index=index)] + asyncio.run(store.abatch(ops)) if operation == "abatch" else store.batch(ops) + + assert store.get(namespace, key).value == value + assert [item.key for item in store.search(namespace)] == [key] + assert not any("/index/" in uri for uri in client.records) + assert store.search(namespace, query=key) == [] + + @pytest.mark.asyncio @pytest.mark.parametrize("owned", [False, True]) From 4afa4bc69df70f8fe474bdc6f5d4d3cc92990f79 Mon Sep 17 00:00:00 2001 From: stefanielizeth73-art Date: Tue, 3 Nov 2026 20:01:31 +0800 Subject: [PATCH 4/6] fix(langchain): preserve Store index write failures (cherry picked from commit 20a4da131678fbc31b6e4629e85d671b4d051059) --- .../07-langchain-langgraph.md | 4 + .../07-langchain-langgraph.md | 3 + .../src/langchain_openviking/store.py | 13 +-- .../frameworks/langchain/tests/test_store.py | 86 +++++++++++++++---- 4 files changed, 81 insertions(+), 25 deletions(-) diff --git a/docs/en/agent-integrations/07-langchain-langgraph.md b/docs/en/agent-integrations/07-langchain-langgraph.md index 11622d3f22..aeed0f930a 100644 --- a/docs/en/agent-integrations/07-langchain-langgraph.md +++ b/docs/en/agent-integrations/07-langchain-langgraph.md @@ -274,6 +274,10 @@ succeeds, resolve the reported error and retry the operation to finish cleanup. Record reads return `None` only for a missing record; permission, backend, and invalid-JSON errors propagate, and `put()` does not overwrite a record after a failed read. +Index creation falls back to replacement only for an explicit already-existing target. +Other index-write errors propagate. The JSON record is written before its projection, +so a failed projection can leave the stored value updated; these writes are not transactional. + `list_namespaces(prefix=("users", "*"))` matches one namespace label per `*`, including when `root_uri` uses a peer home alias such as `viking://~/peers/support/memories/langgraph_store`. diff --git a/docs/zh/agent-integrations/07-langchain-langgraph.md b/docs/zh/agent-integrations/07-langchain-langgraph.md index 247ac5faa5..0962e737f7 100644 --- a/docs/zh/agent-integrations/07-langchain-langgraph.md +++ b/docs/zh/agent-integrations/07-langchain-langgraph.md @@ -261,6 +261,9 @@ store.close() 读取记录时,仅记录不存在才返回 `None`;权限、后端和 JSON 解析错误会向调用方抛出, `put()` 不会在读取失败后覆盖记录。 +索引创建仅在明确返回目标已存在时改用替换写入;其他索引写入错误向调用方传播。 +JSON 记录先于索引投影写入,因此投影失败时存储值可能已更新;两次写入不是事务操作。 + `list_namespaces(prefix=("users", "*"))` 中的每个 `*` 匹配一层命名空间, 也适用于 `viking://~/peers/support/memories/langgraph_store` 这样的 peer 主目录别名。 diff --git a/integrations/frameworks/langchain/src/langchain_openviking/store.py b/integrations/frameworks/langchain/src/langchain_openviking/store.py index 03b196c7bd..d9d987f597 100644 --- a/integrations/frameworks/langchain/src/langchain_openviking/store.py +++ b/integrations/frameworks/langchain/src/langchain_openviking/store.py @@ -413,12 +413,13 @@ def _write(self, uri: str, content: str) -> None: try: self._write_create(uri, content) return - except Exception as create_exc: - try: - self._write_replace(uri, content) - return - except Exception: - raise create_exc + except Exception as exc: + if ( + not isinstance(exc, FileExistsError) + and getattr(exc, "code", None) != "ALREADY_EXISTS" + ): + raise + self._write_replace(uri, content) def _write_create(self, uri: str, content: str) -> None: call_openviking( diff --git a/integrations/frameworks/langchain/tests/test_store.py b/integrations/frameworks/langchain/tests/test_store.py index 45f27ed98c..56e5283d02 100644 --- a/integrations/frameworks/langchain/tests/test_store.py +++ b/integrations/frameworks/langchain/tests/test_store.py @@ -3,7 +3,6 @@ import asyncio import json -from datetime import datetime, timezone from unittest.mock import Mock import openviking_sdk @@ -16,31 +15,80 @@ PutOp, ) from langgraph.store.memory import InMemoryStore -from openviking_sdk.errors import NotFoundError, PermissionDeniedError, UnavailableError +from openviking_sdk.errors import ( + AlreadyExistsError, + NotFoundError, + PermissionDeniedError, + UnavailableError, +) from langchain_openviking.store import OpenVikingStore from langchain_openviking.testing import InMemoryOpenVikingClient -def test_write_record_preserves_created_at_when_create_is_an_upsert_alias(): - store = object.__new__(OpenVikingStore) - created_at = "2026-01-01T00:00:00+00:00" - store._read_record = Mock(return_value={"created_at": created_at}) - store._write_create = Mock() - store._write_replace = Mock() - - record = store._write_record( - "viking://user/default/langgraph/data/ns/key.json", - ("ns",), - "key", - {"version": 2}, - datetime(2026, 1, 2, tzinfo=timezone.utc), +@pytest.mark.parametrize("asynchronous", [False, True]) +@pytest.mark.parametrize( + "create_error,replace_error", + [ + (None, None), + (FileExistsError("index exists"), None), + (AlreadyExistsError("index"), None), + (UnavailableError("index writer"), None), + (PermissionDeniedError("index denied"), None), + (TimeoutError("index timed out"), None), + (FileExistsError("index exists"), PermissionDeniedError("replace denied")), + (AlreadyExistsError("index"), UnavailableError("replace writer")), + ], +) +def test_store_preserves_created_at_and_reports_index_write_errors( + create_error, replace_error, asynchronous +): + class Client(InMemoryOpenVikingClient): + fail = False + + def write( + self, uri: str, content: str, mode: str = "replace", wait: bool = False, **kwargs + ): + if self.fail and "/index/" in uri: + error = create_error if mode == "create" else replace_error + if error is not None: + raise error + if mode == "create" and uri in self.records: + mode = "replace" # Current server create behavior is an upsert. + return super().write(uri, content, mode=mode, wait=wait, **kwargs) + + client = Client() + store = OpenVikingStore(client=client) + store.put(("memory",), "note", {"version": 1}) + before = store.get(("memory",), "note") + old_index = {uri: text for uri, text in client.records.items() if "/index/" in uri} + client.fail = True + conflict = isinstance(create_error, FileExistsError) or isinstance( + create_error, AlreadyExistsError ) + expected_error = replace_error if conflict else create_error + + def update(): + if asynchronous: + asyncio.run(store.aput(("memory",), "note", {"version": 2})) + else: + store.put(("memory",), "note", {"version": 2}) - assert record["created_at"] == created_at - assert record["updated_at"] == "2026-01-02T00:00:00+00:00" - store._write_create.assert_not_called() - store._write_replace.assert_called_once() + if expected_error is None: + update() + else: + with pytest.raises(type(expected_error)) as caught: + update() + assert caught.value is expected_error + assert {uri: text for uri, text in client.records.items() if "/index/" in uri} == old_index + after = store.get(("memory",), "note") + assert after.value == {"version": 2} + assert after.created_at == before.created_at + if expected_error is None: + assert any( + '"version": 2' in text for uri, text in client.records.items() if "/index/" in uri + ) + assert [item.key for item in store.search(("memory",), query='"version": 2')] == ["note"] async def _read_or_update(store, operation, asynchronous): From 2b398254b5bd07b668435793e2f06934b052bcba Mon Sep 17 00:00:00 2001 From: Hao Zhe Date: Wed, 4 Nov 2026 13:01:53 +0800 Subject: [PATCH 5/6] test(langchain): assert stable async Store timeout errors Signed-off-by: Hao Zhe --- integrations/frameworks/langchain/tests/test_store.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/integrations/frameworks/langchain/tests/test_store.py b/integrations/frameworks/langchain/tests/test_store.py index 56e5283d02..526e94f3f7 100644 --- a/integrations/frameworks/langchain/tests/test_store.py +++ b/integrations/frameworks/langchain/tests/test_store.py @@ -79,7 +79,10 @@ def update(): else: with pytest.raises(type(expected_error)) as caught: update() - assert caught.value is expected_error + assert type(caught.value) is type(expected_error) + assert caught.value.args == expected_error.args + assert str(caught.value) == str(expected_error) + assert getattr(caught.value, "code", None) == getattr(expected_error, "code", None) assert {uri: text for uri, text in client.records.items() if "/index/" in uri} == old_index after = store.get(("memory",), "note") assert after.value == {"version": 2} @@ -342,7 +345,6 @@ def test_store_keeps_namespace_validation_and_persisted_values( assert store.search(namespace, query=key) == [] - @pytest.mark.asyncio @pytest.mark.parametrize("owned", [False, True]) @pytest.mark.parametrize("initialized", [False, True]) From d544a8b5b3dd5631f26b7780ddc2eac4cebe12ff Mon Sep 17 00:00:00 2001 From: Hao Zhe Date: Fri, 6 Nov 2026 13:24:53 +0800 Subject: [PATCH 6/6] docs(langchain): note propagated Store discovery errors Signed-off-by: Hao Zhe --- docs/en/about/02-changelog.md | 3 +++ docs/zh/about/02-changelog.md | 2 ++ 2 files changed, 5 insertions(+) diff --git a/docs/en/about/02-changelog.md b/docs/en/about/02-changelog.md index 70e4620229..ee1c878659 100644 --- a/docs/en/about/02-changelog.md +++ b/docs/en/about/02-changelog.md @@ -7,6 +7,9 @@ This changelog is automatically generated from [GitHub Releases](https://github. ## Unreleased +- **LangGraph Store discovery errors**: Non-semantic `search()` and + `list_namespaces()` now propagate backend listing failures, including permission + and availability errors. Explicitly missing namespace roots still return empty results. - **Legacy session migration removed (breaking change)**: Removed the legacy implementation, `ov admin migrate` (including `--cleanup`), and the corresponding Python, TypeScript, and Go SDK methods. The ROOT-only `POST /api/v1/admin/migrate` diff --git a/docs/zh/about/02-changelog.md b/docs/zh/about/02-changelog.md index 1d2a1cf160..ea0092de03 100644 --- a/docs/zh/about/02-changelog.md +++ b/docs/zh/about/02-changelog.md @@ -7,6 +7,8 @@ OpenViking 的所有重要变更都将记录在此文件中。 ## 未发布 +- **LangGraph Store 发现错误**:非语义 `search()` 和 `list_namespaces()` 现在会传播 + 后端文件列表查询失败,包括权限和服务不可用错误。明确不存在的命名空间根路径仍返回空结果。 - **移除旧会话迁移(不兼容变更)**:删除旧迁移实现、`ov admin migrate`(含 `--cleanup`) 及 Python、TypeScript、Go SDK 的对应方法。保留仅 ROOT 可调用的 `POST /api/v1/admin/migrate` 入口,当前返回 `400 / INVALID_ARGUMENT`,不创建任务。