Skip to content

fix(server): ignore replayed interrupted snapshot in blocking send - #1447

Open
trleo wants to merge 2 commits into
a2aproject:mainfrom
trleo:fix/a2a-1409-stale-interrupted-snapshot
Open

trleo wants to merge 2 commits into
a2aproject:mainfrom
trleo:fix/a2a-1409-stale-interrupted-snapshot

Conversation

@trleo

@trleo trleo commented Nov 11, 2026

Copy link
Copy Markdown

On a blocking continuation, the handler now ignores the first event when it is an interrupted-state Task snapshot replayed at turn start, so a stale INPUT_REQUIRED snapshot no longer ends the wait.

The turn's real outcome is returned normally once the executor reports it, or via the existing get_task() fallback if the turn ends without one.

Fixes #1409

@trleo
trleo requested a review from a team as a code owner November 11, 2026 12:19
@github-actions

Copy link
Copy Markdown

🧪 Code Coverage (vs main)

⬇️ Download Full Report

Base PR Delta
src/a2a/server/request_handlers/default_request_handler_v2.py 88.19% 89.15% 🟢 +0.96%
Total 93.49% 93.52% 🟢 +0.03%

Generated by coverage-comment.yml

@ishymko
ishymko requested review from a team and removed request for a team November 12, 2026 10:27

@1AbdullahButt 1AbdullahButt left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fixes the #1409 repro, but the check is wider than "replayed snapshot". It also swallows a fresh interrupted state that the executor reports as its first event.

The issue: the first interrupted event is skipped even when it's new. The subscription uses replace_status_update_with_task=True (default_request_handler_v2.py:490), so a TaskStatusUpdateEvent from updater.requires_auth() or updater.requires_input() arrives as a Task. You can't tell it apart from a re-enqueued context.current_task. And is_first_event is true on any request, not only on continuations. Two cases change:

  • Out-of-band auth. AgentExecutor's docstring (agent_executor.py:42-48) says that for out-of-band auth the agent publishes TASK_STATE_AUTH_REQUIRED and "should not return from execute()" until auth finishes. If that is the executor's first event, as with an executor that doesn't enqueue a Task first (the handler creates the task via create_task_if_missing=True), the event is skipped. The blocking call then waits for the whole auth round-trip, or forever if auth never happens, instead of returning AUTH_REQUIRED. The spec says a blocking send waits until the task reaches a terminal or interrupted state (A2A spec §3.2.2, Execution Mode).
  • Input re-validation on a continuation. The user answers "Friday", and the executor immediately asks again with requires_input() and returns. That event is skipped. The result still comes back correct through the get_task() fallback after the producer finishes, so this case is only slower, not wrong.

Suggested fix: skip only an event that matches the inherited state. request_context.current_task is the stored task at turn start. A replayed snapshot carries the same status (including its timestamp). TaskUpdater.update_status stamps a new timestamp (task_updater.py:84-96), so a fresh status will differ:

prior_status = (
    request_context.current_task.status
    if request_context.current_task is not None
    else None
)
...
if (
    not params.configuration.return_immediately
    and is_first_event
    and prior_status is not None
    and event.status == prior_status
):
    continue

Two tests would pin this. In the first, the first turn ends INPUT_REQUIRED, and the second turn's first event is a fresh requires_input() followed by a return; the result should be INPUT_REQUIRED from the loop, not the fallback. In the second, a new task's first event is requires_auth() and the executor then waits on an asyncio.Event; the blocking call should return AUTH_REQUIRED without the event being set.

Other notes:

  • git merge-tree against main conflicts in tests/server/request_handlers/test_default_request_handler_v2.py (the branch is 5 commits behind), so it needs a rebase.
  • #1298 also deals with executors re-enqueuing context.current_task (the history side, in EventConsumer._handle_initial_task). The two don't touch the same files, but they're worth reviewing together.

I didn't run the tests.

Reviewed with help from Claude Code.

@Iwaniukooo11 Iwaniukooo11 self-assigned this Nov 13, 2026

This branch has not been deployed

No deployments
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.

[Bug]: Blocking continuation returns stale INPUT_REQUIRED when executor emits current task

3 participants