Skip to content

fix(server): keep terminal task states final across replicas - #1391

Draft
mykytanetipa wants to merge 1 commit into
mainfrom
multiserver/fix-terminal-state-final
Draft

mykytanetipa wants to merge 1 commit into
mainfrom
multiserver/fix-terminal-state-final

Conversation

@mykytanetipa

Copy link
Copy Markdown
Collaborator

In cluster mode, a task that another replica finished could be moved out of its terminal state by the replica still running its agent. Found while working on #1324.

Example:

sequenceDiagram
    participant C as Client
    participant B as Replica B
    participant S as Shared store
    participant A as Replica A (runs agent)
    Note over A: agent has queued artifact + COMPLETED
    C->>B: CancelTask(T)
    B->>S: save CANCELED (v3)
    A->>S: save artifact (expects v2)
    S-->>A: conflict
    A->>S: reload T, gets CANCELED (v3)
    A->>S: save artifact + COMPLETED (expects v3)
    S-->>A: accepted
    Note over S: T is COMPLETED, the client was told CANCELED
Loading

Fix: a terminal state is now final. VersionedDatabaseTaskStore refuses any write on top of a terminal task, not only CANCELED writes, and the VersionedTaskStore contract says so. On the consumer side, once a reload shows that another writer finished the task, A drops the remaining events from its executor instead of applying them, which also covers a custom VersionedTaskStore that does not have the check. In the example the last save is refused and T stays CANCELED.

Tested: multi-replica tests for a remote cancel followed by a stale burst of events, with and without the store check; full suite and Postgres/MySQL tests pass.

When another replica cancels a task (or otherwise finishes it) while the
executing replica's agent is still running, the executing replica's next
save conflicts and it reloads the terminal state. Events its agent had
already enqueued were still applied afterwards: the reload refreshed the
version, so the next save passed the compare-and-swap and overwrote the
terminal state (e.g. CANCELED became COMPLETED).

- VersionedDatabaseTaskStore refuses any write on top of a terminal task,
  not only CANCELED writes. The VersionedTaskStore contract says so.
- The consumer stops applying a stale executor's events once a reload
  shows another writer finished the task.

The in-memory test store now copies tasks in and out like a database and
follows the same rule; it can also model a store without the rule, to
check the consumer side on its own.
@mykytanetipa
mykytanetipa added this pull request to stack #1393 October 27, 2026 12:55
@github-actions

Copy link
Copy Markdown

🧪 Code Coverage (vs main)

⬇️ Download Full Report

Base PR Delta
src/a2a/server/agent_execution/active_task.py 95.81% 95.92% 🟢 +0.11%
src/a2a/server/cluster/database_task_store.py 99.30% 99.32% 🟢 +0.02%
Total 93.16% 93.18% 🟢 +0.01%

Generated by coverage-comment.yml

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.

1 participant