Repository navigation
feat(message_history): add async methods to MessageHistory - #816
Merged
Merged
Conversation
vishal-bala
requested changes
Nov 5, 2026
MessageHistory had no async path, so reading or writing chat history from an async agent loop blocked the event loop on every Redis call. Add aget_recent, astore, aadd_messages, aadd_message, acount, adrop, aclear, adisconnect and an aindex property. The async index is built on first use from the sync SearchIndex's connection settings, the same way SemanticRouter does it. Query building and message construction move into helpers shared by the sync and async paths so the two cannot drift.
…eats The caveats that matter before the first async call -- which clients convert to async, loop binding, and adisconnect -- were only on aget_recent, so a caller who reads astore or aadd_messages first never saw them. Each short async docstring now points there.
Seed the other session through astore, block every sync round trip by patching execute_command and pipeline on the sync client, and pin a tool message's tool_call_id and metadata through an async write, adrop(id), the reused async index and adisconnect closing the converted client. adrop now goes through AsyncSearchIndex.drop_keys rather than the private client, and the connection caveat drops SSL and unix sockets, which convert since redis#792.
Yarmoluk
force-pushed
the
feat/message-history-async
branch
from
November 5, 2026 11:54
8fc3ce8 to
c1b5d75
Compare
Contributor
Author
|
Thanks. Rebased onto
|
vishal-bala
self-requested a review
November 5, 2026 13:16
vishal-bala
approved these changes
Nov 6, 2026
| assert standard_history.aindex is None | ||
| # adisconnect closed the converted client's connections. | ||
| assert pool is not None | ||
| connections = pool._available_connections + list(pool._in_use_connections) |
Collaborator
There was a problem hiding this comment.
Minor: _available_connections and _in_use_connections are private to redis-py's pool, so an internal rename there would break this assert without anything in redisvl changing. Not blocking, but if there's a public way to check the client was closed, it'd be sturdier.
|
🚀 PR was released in |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Branch:
feat/message-history-async(forkYarmoluk/redis-vl-python), rebased ontomainat506b961(includes #792 and #785).Commits:
82f91e4feat,93657d7docs,c1b5d75test. Label:auto:minor.Summary
Follow-up to #784, which gave
SemanticRouteraroute/aroute_many.MessageHistoryhad the same gap: an agent running on asyncio blocked its event loop on every history read or write. This PR adds the async twins --aget_recent,astore,aadd_messages,aadd_message,acount,adrop,aclear,adisconnectand anaindexproperty -- using the same pattern #784 landed:AsyncSearchIndexis built lazily on the first async call, from the sync index's connection settings. A history built fromredis_urlgets its own async connection from that URL andconnection_kwargs; a history given a syncredis_clientgets an async client converted from that client's connection settings byRedisConnectionFactory.sync_to_async_redis, owned by the async index, soadisconnectcloses only the converted client and the caller's sync client is never touched or rebuilt.AsyncSearchIndex; none reaches into a Redis client directly (adropdeletes withaindex.drop_keys(aindex.key(id)))._recent_query,_count_query) and message construction (_chat_messages,_exchange) move into helpers shared by the sync and async paths, so the two cannot drift.redis_urlinstead); the async client is bound to the loop that made the first call, so calladisconnectbefore switching loops.No sync behaviour changes.
SemanticMessageHistoryis not covered here (see Notes).Reviewer Brief
Start here:
redisvl/extensions/message_history/message_history.py,_get_async_index()(line 418). Every async method goes through it, and it decides whether the async index connects from the URL the sync index owns or from a client converted from the caller's sync client; it is the same code asSemanticRouter._get_async_index.Where it could fail
message_history.py:432: a caller-supplied Sentinel or Cluster client, or one with a sync-only setting such asssl_validate_ocsp=True, raisesValueErrorfromsync_to_async_redisat the first async call; theaget_recentdocstring names Sentinel and Cluster but, like the router's, not the sync-only-setting case.message_history.py:154:adropremoves the key withUNLINK(viadrop_keys) where syncdropusesDEL; both are@keyspace @write, but an ACL that grants+delby name and not+unlinkwould allowdropand denyadrop(UNLINK, DEL).message_history.py:449: the async client belongs to the loop that made the first call; a secondasyncio.run()on the same history withoutadisconnectin between fails, the same contractSemanticRouterhas shipped since feat(router): add async aroute and aroute_many to SemanticRouter #784.Not verified: Redis Cluster and Sentinel (no cluster tests run locally or in CI); the TLS and unix-socket conversion paths were not exercised end to end through
MessageHistory(they are covered by #792's own tests);connection_kwargsandlib_nameforwarding on the URL path is not asserted by a test.Beyond CI: each of these mutations makes
test_async_message_history_matches_syncfail:astoreignoringsession_tag,adropgoing through the sync client,adrop(id)ignoringid, the async index rebuilt per call, andadisconnectnot closing the converted client. The two new integration tests pass locally againstredis:8.4via testcontainers.Since the last brief: rebased onto
506b961(#792 now converts TLS and unix-socket clients, so the caveat narrows to Sentinel and Cluster);adropdeletes throughaindex.drop_keysinstead of the async client; the sync-equivalence test now blocks the sync client'sexecute_commandandpipeline, seeds a second session withastore, and pinstool_call_id/metadata,adrop(id), async-index reuse andadisconnectclosing the converted pool.Reviewed
c1b5d75· Claude Code (claude-opus-5-5) · lenses: correctness, security, design, tests, docs, Redis behaviourTest plan
Run on the rebased branch at
c1b5d75, macOS,uv, Docker running (testcontainers startsredis:8.4):The 3 failures and 53 errors are the identical set on
mainat506b961: every one isModuleNotFoundError: sentence_transformers(the HuggingFace vectorizer is not installed locally), inSemanticMessageHistorytests this PR does not touch.test_async_message_history_matches_synctakes its expected values from the sync methods, then patchesexecute_commandandpipelineon the sync index's Redis client to raise, so any async method that made a blocking call would fail it.test_async_message_history_with_redis_urlchecks that a URL-built history builds its async index from that URL rather than converting the client.Notes
SemanticMessageHistorygets no async methods in this PR. Itsget_relevantembeds through the vectorizer, which brings in theaembedcaveats feat(router): add async aroute and aroute_many to SemanticRouter #784 documented for the router; it is a separate change with its own brief.adelete: dropping the index is setup/teardown rather than a per-turn call, and feat(router): add async aroute and aroute_many to SemanticRouter #784 drew the same line for the router. No async form of themessagesproperty either, since a property cannot be awaited;aget_recentwith a largetop_kis the async read.adisconnectcontract rather than the rebuild-on-new-loop fix(vectorize): use Cohere's async client for aembed and aembed_many #785 added for Cohere's async client. The async index owns a connection pool, and rebuilding it silently on a loop change would leak the previous pool; an explicitadisconnectkeeps the two extensions consistent and the close visible._get_async_index,aindexandadisconnectmatchSemanticRouter's line for line; they are kept as two copies rather than a shared helper, since extracting one would mean changing the merged router code in this PR.count/acountkeep the sync method's unannotated signature rather than adding-> intto one twin only.Note
Medium Risk
New async Redis connection lifecycle (lazy index, loop binding, client conversion) could misbehave for Sentinel/Cluster/unix-socket setups; sync API and data paths are unchanged.
Overview
Adds async counterparts to
MessageHistoryread/write/clear/drop operations (aget_recent,astore,aadd_messages,aadd_message,acount,adrop,aclear, plusadisconnectand anaindexproperty) so asyncio agents can use history without blocking the event loop.Async work goes through a lazily created
AsyncSearchIndexthat reuses the sync index’s schema and connection settings: URL-owned clients open a separate async connection fromredis_url; caller-supplied sync clients get a derived async client viaRedisConnectionFactory.sync_to_async_redis, closed only byadisconnect. Docstrings document loop binding and that Sentinel/Cluster sync clients needredis_urlinstead.Sync behavior is preserved by extracting shared helpers (
_recent_query,_recent_result,_count_query,_exchange,_chat_messages) so sync and async paths share query and message construction. Two integration tests assert parity with sync (including a monkeypatch that fails if async calls hit the sync Redis client) and cover theredis_urlasync-index path.Reviewed by Cursor Bugbot for commit c1b5d75. Bugbot is set up for automated code reviews on this repo. Configure here.