From 1048959cba4a01effce28ece7f3e052d4f200ea9 Mon Sep 17 00:00:00 2001 From: alvinfei Date: Wed, 21 Oct 2026 16:27:13 +0800 Subject: [PATCH] refactor(memhub): recover writes from SDK outcomes Co-authored-by: Cursor --- .../core/src/one_memory/hy_memory_bridge.py | 82 +++++++++++++++++++ .../apps/core/tests/test_hy_memory_bridge.py | 36 ++++++++ 2 files changed, 118 insertions(+) diff --git a/memhub/apps/core/src/one_memory/hy_memory_bridge.py b/memhub/apps/core/src/one_memory/hy_memory_bridge.py index 6db36c8..5f36506 100644 --- a/memhub/apps/core/src/one_memory/hy_memory_bridge.py +++ b/memhub/apps/core/src/one_memory/hy_memory_bridge.py @@ -866,6 +866,25 @@ class HyMemoryBridge: governance. """ client = await self._get_client() + exact_inquiry = getattr(client, "get_write_outcome", None) + if callable(exact_inquiry): + try: + outcome = await _joined_thread(exact_inquiry, request_id) + except Exception as error: + raise HyMemoryBridgeError("hy_memory_outcome_inquiry_failed") from error + if outcome is None: + return () + return await _outputs_from_write_outcome( + client, + outcome, + request_id=request_id, + user_id=user_id, + agent_id=agent_id, + evidence_ids=evidence_ids, + ) + + # Compatibility for the pinned 2.0.4 wheel. Remove after the SDK-first + # authority release is published and MemHub's exact pin is bumped. page_size = 500 outputs: list[HyMemoryOutput] = [] seen: set[str] = set() @@ -2061,6 +2080,69 @@ def _basic_info_memories(raw: object) -> tuple[Mapping[str, object], ...]: return tuple(memory for memory in memories if isinstance(memory, Mapping)) +async def _outputs_from_write_outcome( + client: HyMemoryClientProtocol, + outcome: object, + *, + request_id: str, + user_id: str, + agent_id: str, + evidence_ids: tuple[str, ...], +) -> tuple[HyMemoryOutput, ...]: + if not isinstance(outcome, Mapping): + raise HyMemoryBridgeError("hy_memory_outcome_shape_unknown") + if outcome.get("request_id") not in {None, request_id}: + raise HyMemoryBridgeError("hy_memory_outcome_identity_mismatch") + for key, expected in (("user_id", user_id), ("agent_id", agent_id)): + value = outcome.get(key) + if value is not None and value != expected: + raise HyMemoryBridgeError("hy_memory_outcome_identity_mismatch") + status = outcome.get("status") + if status == "failed": + return () + if status != "succeeded": + raise HyMemoryBridgeError("hy_memory_outcome_not_terminal") + result = outcome.get("result") + if not isinstance(result, Mapping): + raise HyMemoryBridgeError("hy_memory_outcome_shape_unknown") + candidates: list[str] = [] + for key in ("memory_ids", "experience_ids", "fact_ids"): + values = result.get(key) + if isinstance(values, list): + candidates.extend(value for value in values if isinstance(value, str)) + if isinstance(result.get("memory_id"), str): + candidates.append(cast(str, result["memory_id"])) + + outputs: list[HyMemoryOutput] = [] + for memory_id in dict.fromkeys(candidates): + try: + record = await _joined_thread(client.get, memory_id) + except Exception as error: + raise HyMemoryBridgeError("hy_memory_outcome_inquiry_failed") from error + # The singular add memory_id may be the hidden L1 raw node. It is not a + # semantic memory output and public get correctly returns None for it. + if not isinstance(record, Mapping): + continue + if record.get("user_id") != user_id or record.get("agent_id") != agent_id: + raise HyMemoryBridgeError("hy_memory_outcome_identity_mismatch") + content = record.get("content") + if not isinstance(content, str) or not content.strip(): + continue + layer = record.get("layer") + owner = record.get("owner") + outputs.append( + HyMemoryOutput( + memory_id=memory_id, + content=content, + layer=layer if isinstance(layer, str) else None, + owner=owner if owner in {"user", "agent"} else None, + tags=_string_tuple(record.get("tags")), + evidence_ids=evidence_ids, + ) + ) + return tuple(outputs) + + def _sdk_memory_record(raw: object) -> HySdkMemoryRecord: if not isinstance(raw, Mapping): raise HyMemoryBridgeError("hy_memory_record_shape_unknown") diff --git a/memhub/apps/core/tests/test_hy_memory_bridge.py b/memhub/apps/core/tests/test_hy_memory_bridge.py index a276854..de6339d 100644 --- a/memhub/apps/core/tests/test_hy_memory_bridge.py +++ b/memhub/apps/core/tests/test_hy_memory_bridge.py @@ -644,6 +644,8 @@ def test_bridge_writes_evidence_refs_and_uses_source_as_provenance(tmp_path) -> def test_bridge_recovers_only_outputs_with_the_exact_request_identity(tmp_path) -> None: client = FakeHyMemoryClient({"success": True}) + # Pinned SDK 2.0.4 compatibility path: no durable outcome API yet. + client.get_write_outcome = None client.list_result = { "vdb": { "total": 3, @@ -699,6 +701,40 @@ def test_bridge_recovers_only_outputs_with_the_exact_request_identity(tmp_path) assert outputs[0].evidence_ids == ("evidence-1",) +def test_bridge_prefers_exact_sdk_write_outcome_over_library_scan(tmp_path) -> None: + client = FakeHyMemoryClient({"success": True}) + client.write_outcome = { + "request_id": "request-1", + "user_id": "local", + "agent_id": "one-memory", + "status": "succeeded", + "result": {"success": True, "memory_ids": ["memory-1"]}, + } + bridge = HyMemoryBridge( + profile=snapshot( + ProviderKind.OLLAMA, + base_url="http://127.0.0.1:11434", + has_api_key=False, + ), + api_key=None, + data_dir=tmp_path, + client=client, + ) + + outputs = asyncio.run( + bridge.outputs_for_request( + request_id="request-1", + user_id="local", + agent_id="one-memory", + evidence_ids=("evidence-1",), + ) + ) + + assert [output.memory_id for output in outputs] == ["memory-1"] + assert outputs[0].evidence_ids == ("evidence-1",) + assert not any("list_memories" in call for call in client.calls) + + def test_bridge_reads_sdk_memory_lifecycle_without_governance(tmp_path) -> None: client = FakeHyMemoryClient({"success": True}) client.list_result = {