fix(memory/hindsight): honor configured retain_async on manual hindsight_retain - #34529
fix(memory/hindsight): honor configured retain_async on manual hindsight_retain#34529spiky02plateau wants to merge 1 commit into
Conversation
…ght_retain The manual `hindsight_retain` tool path in `HindsightMemoryProvider.handle_tool_call` built its retain kwargs without passing `retain_async`, so `_build_retain_kwargs` left it out and `client.aretain(**kwargs)` fell back to the client default (`retain_async=False`). The tool therefore ran the retain *server-sync* — inconsistent with both the provider default (`retain_async=True`) and the gateway auto-retain path (`_do_retain` / flush-on-switch), which pass the configured flag to `aretain_batch`. Practical effect: a manual `hindsight_retain` call blocks the turn while the server performs synchronous extraction (~15-40s under a slow extraction LLM), and is fragile to daemon lifecycle — an in-flight sync retain can be cancelled if the daemon is torn down, surfacing as a 500 (`CancelledError: Task cancelled, timeout graceful shutdown exceeded`). Fix: pass `retain_async=self._retain_async` from the tool path, mirroring the auto-retain path. Covers all profiles. Tool response unchanged. Adds a regression test asserting the configured async flag reaches `aretain`.
teknium1
left a comment
There was a problem hiding this comment.
Thanks for identifying the manual-retain configuration gap. The defect remains on current main, but the provider was refactored after this branch.
Problems
- Current
hindsight_retaincallsaretain_batch, notaretain, and removesretain_asyncfrom the item before the call (plugins/memory/hindsight/__init__.py:1715-1722). The configured value must instead be passed as a call-levelaretain_batch(..., retain_async=self._retain_async)argument, as automatic retain does atplugins/memory/hindsight/__init__.py:1681-1686. - The added test asserts against
p._client.aretain(tests/plugins/memory/test_hindsight_provider.py:480in this PR), while current main's manual path usesp._client.aretain_batch.
Suggested changes
- Rework the patch and test for the current
aretain_batchcontract; test bothretain_async=TrueandFalseon the manual path.
Automated hermes-sweeper review.
| content, | ||
| context=context, | ||
| tags=args.get("tags"), | ||
| retain_async=self._retain_async, |
There was a problem hiding this comment.
This branch predates the aretain_batch refactor. On current main, the manual path removes retain_async from the item and calls aretain_batch; pass retain_async=self._retain_async to that call instead of adding it to _build_retain_kwargs.
| "hindsight_retain", {"content": "user likes dark mode"} | ||
| )) | ||
| assert result["result"] == "Memory stored successfully." | ||
| assert p._client.aretain.call_args.kwargs["retain_async"] is True |
There was a problem hiding this comment.
Current main's manual handler calls aretain_batch, so this regression should assert p._client.aretain_batch.call_args.kwargs["retain_async"] instead.
Duplicate of #22836 (earliest-open canonical of the hindsight |
Problem
The manual
hindsight_retaintool path inHindsightMemoryProvider.handle_tool_callbuilds its retain kwargs without passingretain_async:_build_retain_kwargsonly insertsretain_asyncinto the dict when the arg is notNone(it defaults toNone), so the key is omitted andclient.aretain(**kwargs)falls back to the client defaultretain_async=False— running the retain server-sync.That's inconsistent with the rest of the provider:
self._retain_async = config.get("retain_async", True)(i.e.True), and_do_retainand the flush-on-switch path) both pass the configured flag toaretain_batch(..., retain_async=retain_async_flag).So only the manual tool runs synchronously.
Impact
A manual
hindsight_retainblocks the turn while the server performs synchronous fact extraction (~15-40s under a slow/loaded extraction LLM).It's fragile to daemon lifecycle: an in-flight sync retain can be cancelled if the daemon is torn down, surfacing to the client as a bare
500. The server-side traceback in that case is:In a long-lived gateway the same defect manifests as "the manual tool blocks the turn / occasionally errors," rather than a 500.
Fix
Pass
retain_async=self._retain_asyncfrom the tool path, mirroring the auto-retain path. The tool response is unchanged. Applies to all profiles.Test
Adds
test_manual_retain_uses_configured_async_mode, asserting the configured async flag reachesaretain. Full file suite passes: