Skip to content

hard: tenant isolation, RPC timeouts, non-blocking async + tests#2

Merged
tcconnally merged 1 commit into
mainfrom
hard/memory-service-isolation-timeouts
Jun 28, 2026
Merged

hard: tenant isolation, RPC timeouts, non-blocking async + tests#2
tcconnally merged 1 commit into
mainfrom
hard/memory-service-isolation-timeouts

Conversation

@tcconnally

Copy link
Copy Markdown
Contributor

Hardens the outward-facing ADK memory package — the "go-to combo" surface that ADK users actually pip install. Three correctness/robustness bugs + packaging/doc fixes, with the package's first test suite and CI.

🔴 HIGH — fixed

  1. Cross-tenant memory leak. search_memory scoped results by stuffing app_name/user_id into the recall query string — but Mimir OR's query terms together, so a search returned memories belonging to other users and apps (and diluted relevance). The query is now sent clean, and every returned item is post-filtered to the requesting (app_name, user_id) recorded in its body. add_memory now also records app_name/user_id so explicit memories filter correctly. Isolation no longer depends on recall ranking semantics.
  2. No RPC timeout (deadlock). _rpc did a bare blocking stdout.readline() under a lock — a hung mimir subprocess blocked every memory op forever. Now a daemon reader thread pumps stdout into a queue and _rpc waits against a configurable timeout_s (default 30s), with request/response id correlation (skips notifications and stale/other-id replies).
  3. Event-loop blocking. The async methods did blocking subprocess I/O inline, stalling ADK's event loop on every memory call. They now run the blocking RPC via asyncio.to_thread (the existing lock correctly serializes the worker threads).

🟡 Also

  • Send notifications/initialized after the handshake (MCP spec compliance).
  • stderr=subprocess.DEVNULL — the un-drained PIPE could deadlock a chatty server once its pipe buffer filled.
  • UTC timestamps (_format_timestamp was naive local-tz).
  • Declare the typing-extensions dependency — the service imports typing_extensions.override (the stdlib typing.override only exists on 3.12+) but it was undeclared.
  • Add py.typed (PEP 561) — the package ships type hints but exported none.
  • Fix README + module docstring examples that don't run on ADK 2.3.0: Agent has no memory_service field (it goes on Runner); run_async has no agent parameter (the agent is bound on the Runner). Verified against the installed google-adk 2.3.0.

Tests + CI

First test suite for the package (previously zero): a fake Mimir MCP stdio stub that models recall OR-semantics, covering tenant isolation, RPC timeout, id-correlation / notification skipping, and the handshake. New cross-platform test.yml (Linux/Windows/macOS × py3.10/3.12). All 4 tests pass locally; wheel builds with py.typed included. Version 0.2.0 → 0.3.0 (a PyPI republish is a maintainer gate).

🤖 Generated with Claude Code

The published ADK memory service had three correctness/robustness bugs on
the outward-facing combo surface:

1. Cross-tenant LEAK: search_memory stuffed app_name/user_id into the
   recall query string, but Mimir OR's query terms -> other users'/apps'
   memories were returned. Now the query is clean and results are
   post-filtered to the requesting (app_name, user_id), recorded in each
   body (add_memory now stores them too). Guarantees isolation regardless
   of recall ranking semantics.
2. No RPC timeout: _rpc did a bare blocking stdout.readline() under a lock
   -> a hung mimir subprocess deadlocked every memory op forever. Now a
   daemon reader thread + a configurable timeout_s (default 30s) with
   request/response id correlation (skips notifications / stale ids).
3. Event-loop blocking: async methods did blocking subprocess I/O inline.
   They now run it via asyncio.to_thread, so ADK's loop isn't stalled.

Also: send notifications/initialized after handshake (MCP spec);
stderr=DEVNULL (un-drained PIPE deadlock); UTC timestamps; declare the
typing-extensions dep (override backport for 3.10/3.11); add py.typed;
fix README/docstring examples that don't run on ADK 2.3.0
(Agent has no memory_service -> Runner; run_async has no agent arg).

Adds a test suite (fake Mimir MCP stdio stub) + cross-platform CI:
isolation, timeout, id-correlation, handshake. Bump 0.2.0 -> 0.3.0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@tcconnally tcconnally merged commit 6b86dfc into main Jun 28, 2026
6 checks passed
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.

1 participant