Visitar URL original
fix(sessions): refresh stale callers on duplicate event delivery by galleonli · Pull Request #7454 · google/adk-python · GitHub
Skip to content

fix(sessions): refresh stale callers on duplicate event delivery - #7454

Closed
galleonli wants to merge 2 commits into
google:mainfrom
galleonli:fix/session-broadcast-refresh
Closed

galleonli wants to merge 2 commits into
google:mainfrom
galleonli:fix/session-broadcast-refresh

Conversation

@galleonli

@galleonli galleonli commented Oct 8, 2026 •

Copy link
Copy Markdown

Link to Issue or Description of Change

Problem: When two callers hold copies of the same in-memory session, broadcasting the same event to both updates only the first caller. append_event finds the event in canonical storage and returns before updating the second caller's state and history. A running agent can therefore continue with stale state even though the append succeeded.

This is a follow-up to the event deduplication introduced for #5723. The existing tests cover repeated delivery to one reference; this change covers delivery to separate references.

Steps to reproduce:

  1. Create a session with counter=0 and retrieve a second copy before appending any event.
  2. Append an event with state_delta={"counter": 1} to the first copy.
  3. Append the same event, or an equal copy, to the second copy.

Expected: both callers see counter=1 and the event remains stored once. Observed: the second caller still has counter=0 and an empty event list. The new Runner regression reproduces the same behavior with two concurrent invocations and no model calls.

Environment: Reproduced with google-adk 2.11.0 and the source at ea656b03d3f3c6dd041ba8e3f65d612017592c42, Python 3.12.3, macOS arm64. Model/LiteLLM: N/A. Reproducible without credentials or network services.

Solution: If storage already contains the event but the caller does not, refresh the caller from a copied storage snapshot with merged app/user state. Refreshing the snapshot, rather than replaying the old delta, preserves newer values and event order. Update the caller's containers in place to preserve existing references and local temporary state. An event already present in both references remains a no-op, and canonical storage is unchanged.

Testing Plan

Unit Tests: Added six parameterized session cases covering both copy modes, same-object/equal-copy delivery, scoped state, temporary state, copy isolation, idempotence, and late delivery after a newer update. All six fail on the original implementation and pass with the patch.

python -m pytest -q tests/unittests/sessions --tb=short --disable-warnings
543 passed, 1 xfailed

python -m pytest -q tests/unittests/test_runners.py tests/unittests/runners --tb=short --disable-warnings
275 passed, 1 skipped, 3 xfailed

End-to-End Runner Validation: test_concurrent_invocations_receive_a_shared_state_event uses an asyncio.Barrier to ensure both invocations have loaded their session before a custom BaseAgent yields the shared event. It exercises the actual Runner append path without mocking the session service or calling an LLM.

python -m pytest -q tests/unittests/test_runners.py -k concurrent_invocations_receive_a_shared_state_event
Original implementation: fails with observed counters [1, 0]
Patched implementation: both counters are 1; one broadcast event is stored

The Runner regression is included in the Runner suite count above. The full repository suite, GPU execution, and performance benchmarks were not run.

Changed-file pre-commit hooks passed. mypy --follow-imports=silent src/google/adk/sessions/in_memory_session_service.py and git diff --check passed.

Compatibility recheck (2026-10-08): The results above were recorded for the initial fix, cddc380c. After upstream changed the shared session and Runner tests, merged main at 7d56ef84 into this branch as ed94ef16. The session-service implementation is unchanged. On the merged source, the combined session and Runner suites passed:

python -m pytest -q tests/unittests/sessions tests/unittests/test_runners.py tests/unittests/runners --tb=short --disable-warnings --show-capture=no
826 passed, 1 skipped, 4 xfailed

Changed-file pre-commit hooks, the single-file mypy check, and the diff check passed again. This was a Python 3.12.3 CPU run in the existing environment; the full multi-version tox suite was not run.

Checklist

  • Added regression tests that fail without the fix.
  • Relevant session and Runner tests pass locally.
  • Verified the actual Runner flow on CPU.
  • Added a comment explaining why the current snapshot is used instead of replaying the old delta.
  • Full repository unit test suite run locally.

No new public API or dependency changes.

@google-cla

google-cla Bot commented Oct 8, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@DeanChensj

DeanChensj commented Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

Hi @galleonli, thank you very much for taking the time to investigate this and for putting together such a thorough PR and test suite!

After reviewing the end-to-end session lifecycle across all ADK session backends, we realized that the inline comment added in InMemorySessionService.append_event from #5815 ("when the orchestrator broadcasts a shared-state delta to several concurrent session references") was itself misleading and does not reflect a supported ADK execution pattern:

  1. Within a single Runner.run_async invocation (including ParallelAgent, graph workflows, and parallel tool calls), all concurrent branches share a single InvocationContext.session reference and serialize append_event calls through the runner's event queue.
  2. Across separate concurrent invocations, ADK does not support multiple writers concurrently mutating separate in-memory Session copies of the same session_id or broadcasting a single Event instance (with the same event.id) across them. On persistent backends (DatabaseSessionService, SqliteSessionService, FirestoreSessionService), appending from a stale Session copy raises StaleSessionError, and inserting a duplicate event.id violates the primary key constraint.
  3. For sharing state across sessions, ADK provides app: and user: scoped state prefixes, which automatically propagate via a single append_event call without re-delivering the same event to multiple session objects. Callers holding an older Session snapshot that want to observe external updates should call get_session() rather than relying on duplicate append_event delivery to refresh state.

We sincerely apologize that the existing comment in in_memory_session_service.py pointed you down this path—we will clean up that comment on our side so it doesn't mislead future contributors. Thanks again for your effort and contribution to ADK!

@DeanChensj DeanChensj closed this Oct 8, 2026
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.

2 participants