Skip to content

fix(a2a): cooperative cancellation checkpoints in execute_a2a_task (#16174) - #16498

Merged
mrveiss merged 2 commits into
mainfrom
issue-16174-a2a-cancel-cooperative
Sep 13, 2026
Merged

mrveiss merged 2 commits into
mainfrom
issue-16174-a2a-cancel-cooperative

Conversation

@mrveiss

@mrveiss mrveiss commented Sep 12, 2026 •

Copy link
Copy Markdown
Owner

Thinking Path

TaskManager.cancel_task is synchronous and advisory — it flips a state in Redis and returns; execute_a2a_task runs as a fire-and-forget BackgroundTasks coroutine that never observed this, so a cancelled task's executor kept running to completion and its work-claim could only lapse at TTL, never release. Releasing the claim immediately on cancel was considered and rejected: it would free a scope while the work holding it was still writing, which is worse than not releasing at all. The fix has to be cooperative — the executor itself has to check for cancellation and stop — and since a single orchestrator call in flight can't be interrupted, the checkpoints have to sit at the boundaries between steps, not inside one.

What Changed

  • Added _abort_if_cancelled and three checkpoints inside _execute_claimed (autobot-backend/a2a/task_executor.py) — before scrub, before calling the orchestrator, and after it returns — each checking whether the task has moved to CANCELLED.
  • hold_scopes's finally releases the claim as soon as the function returns, at whichever checkpoint that is.
  • scope_enforcement.py's stop-predicate renewal path is untouched except a docstring update reflecting the new state; it remains the backstop for an executor wedged before any checkpoint.
  • New tests: test_already_cancelled_before_the_first_checkpoint_does_nothing, test_cancelled_before_orchestration_skips_the_orchestrator_call, test_scope_stays_held_while_the_orchestrator_call_is_still_running, test_cancelled_during_orchestration_performs_no_further_work_and_releases, test_never_cancelled_still_completes_normally (autobot-backend/a2a/task_executor_cancellation_test.py).

Verification

Static only, per the owner's no-local-execution rule — no test suite was run against this branch locally; CI and the pre-push hook are the authority.

  • flake8 --select=F401,F821,E501 over every touched file — clean.
  • Mutation argument (from the diff, not run locally): deleting any one of the three if await _abort_if_cancelled(...): return blocks removes exactly the behaviour the paired test checks. Removing the first ("before scrub") checkpoint is the one the other three tests cannot catch — they all cancel inside a mocked scrub_outbound/process_request side effect, i.e. after the first checkpoint has already passed, so deleting it would leave all three green. test_already_cancelled_before_the_first_checkpoint_does_nothing closes that gap: it flips the fake manager to CANCELLED from inside update_state's first call (the WORKING transition execute_a2a_task makes unconditionally, before any checkpoint), then asserts scrub_outbound and orchestrator.process_request are never called, no artifact is stored, and the claim is released. Removing the "before orchestration" checkpoint would let orchestrator.process_request run even though tm.cancel() already fired inside scrub_outbound's mock, failing orchestrator.process_request.assert_not_called() in the second test; removing "after orchestration" would let evaluate_task_output/_store_response_artifacts run after cancellation, failing the empty-artifacts assertion in the third. test_scope_stays_held_while_the_orchestrator_call_is_still_running and test_never_cancelled_still_completes_normally are checkpoint-independent by design (a true-either-way property and the no-cancellation contrast).
  • Pre-commit hooks ran and passed on commit; the pre-push hook ran pytest on the new test file and passed before the push went through ([pre-push OK] pytest: all relevant tests pass, 5/5).

Risks

Low. The checkpoints only ever stop the next step from starting — they cannot interrupt work already in flight, so no behaviour changes for a task that is never cancelled (proven by the contrast test). #15950's cancellation criterion is left unticked here deliberately; a PR cannot verify itself against merged code.

Considered and declined the optional step-list refactor: the three checkpoints interleave with three structurally different operations (scrub, an orchestrator call, artifact storage/eval), not a uniform sequence — folding them into a list would add a layer of indirection without actually shrinking what each step does, for a function that already passes the repo's function-length hook at its current size.

Model Used

Claude Sonnet 5 (claude-sonnet-5)

Issue Link

Closes #16174

Single-issue rationale

Split from #16173 deliberately: different files, no overlap, and materially different risk (a mechanical extraction vs. a lifecycle-semantics change affecting every A2A task) — the batching rule's own exception for independent-or-different-risk changes.

Changelog fragment

  • Added changelog/unreleased/16174-a2a-cancel-cooperative.md

Checklist

  • Code follows AutoBot patterns from CLAUDE.md
  • Tests added or updated
  • Documentation updated if behavior changed (N/A — no user-facing behavior changed)
  • Pre-commit hooks pass
  • PR targets main
  • No secrets or credentials in the diff

…16174)

Cancelling an A2A task previously only flipped a Redis state -- the
executor kept running to completion and its work-claim could only lapse
at TTL, never release, because releasing early would free a scope while
the work holding it was still writing.

execute_a2a_task now checks for cancellation at three defined points
(before scrub, before orchestration, after orchestration) and stops there
instead of continuing, so hold_scopes' `finally` releases the claim as
soon as the next checkpoint sees the cancellation. The stop-predicate
renewal path in scope_enforcement.py stays as the backstop for an
executor wedged before any checkpoint.
@coderabbitai

coderabbitai Bot commented Sep 12, 2026 •

Copy link
Copy Markdown
Contributor

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: fa06bfed-5334-40c3-a02a-3330ceb0f8d6


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

AutoBot Phase Validation Results

System Maturity: 96.5%

Phase Status:

PASS Phase 1: Core Infrastructure: 100.0%
PASS Phase 2: Knowledge Base and Memory: 100.0%
IN PROGRESS Phase 3: LLM Integration: 66.7%
PASS Phase 4: Security and Authentication: 100.0%
PASS Phase 5: Agent Orchestration: 100.0%
PASS Phase 6: Enhanced UI/UX: 100.0%
PASS Phase 7: Testing and Validation: 100.0%
PASS Phase 8: Advanced Features: 100.0%
PASS Phase 9: Multi-Modal AI: 100.0%
PASS Phase 10: Production Readiness: 100.0%

Recommendations:

  • 🟡 MEDIUM: Phase 3: LLM Integration requires attention (66.7% complete): Review and implement
  • ✅ System is production-ready - consider advanced features and scaling: Review and implement

@github-actions

Copy link
Copy Markdown
Contributor

✅ SSOT Configuration Compliance: Passing

🎉 No new hardcoded values of either class — ssot and other both block.

Known backlog in pipeline-scripts/hardcoded_values_baseline.txt is suppressed and tracked in #14371.

@github-actions

Copy link
Copy Markdown
Contributor

Notice: 29 open PRs — past the runaway threshold (25)

There is no PR queue limit, and this is not a request to defer this PR. Work proceeds one issue at a time without a cap on open PRs; review capacity is the constraint.

This notice only means the count is high enough to be worth a glance for a runaway — something opening PRs in a loop, or a merge pipeline that has stalled so nothing is draining.

Currently open:

If the queue is draining normally, ignore this. Otherwise:

  1. Check whether CI is dispatching at all — see the ci-dispatch-watchdog status on these PRs
  2. Merge the ones whose CI has finished and review has passed: gh pr merge <number> --squash --delete-branch
  3. Look for a loop opening near-identical PRs

Warn-only runaway detector — .github/workflows/pr-queue-gate.yml. It never blocks a merge.

@mrveiss

mrveiss commented Sep 12, 2026

Copy link
Copy Markdown
Owner Author

Independent review: changes requested, one blocking gap.

Severity Where Evidence Fix
HIGH a2a/task_executor.py, the "before scrub" checkpoint All four new tests start the task SUBMITTED and cancel it only inside the scrub_outbound or process_request side effect, which is after this checkpoint has already evaluated. Traced with the checkpoint deleted: all four still pass, so it's present but unverified. The Verification section argues mutation coverage only for the other two Add a test with an already-cancelled task (_FakeTaskManager(initial=TaskState.CANCELLED)). Assert scrub_outbound and process_request are never called, no artifact is stored, and the claim is released

Verified sound:

  • hold_scopes' finally releases scopes on every checkpoint return.
  • Checkpoints only observe the CANCELLED status that cancel_task wrote; none rewrites it.
  • The after-orchestration checkpoint sits strictly between process_request and _store_response_artifacts.
  • Closing refs are exactly [16174].

Non-blocking:

  • _execute_claimed grows from 61 to 76 lines. Folding the three checkpoints into a step list would keep it small.
  • cancel_task never publishes a WebSocket event. That's pre-existing, and a separate issue if the UI needs it.

Review finding: every existing test cancels mid-run, inside
scrub_outbound or process_request's side effect, so the first
checkpoint (before either runs) could be deleted and all four would
stay green. Adds a case starting _FakeTaskManager already CANCELLED,
asserting scrub_outbound and process_request are never called, no
artifact is stored, and the claim is released.
@github-actions

Copy link
Copy Markdown
Contributor

AutoBot Phase Validation Results

System Maturity: 96.5%

Phase Status:

PASS Phase 1: Core Infrastructure: 100.0%
PASS Phase 2: Knowledge Base and Memory: 100.0%
IN PROGRESS Phase 3: LLM Integration: 66.7%
PASS Phase 4: Security and Authentication: 100.0%
PASS Phase 5: Agent Orchestration: 100.0%
PASS Phase 6: Enhanced UI/UX: 100.0%
PASS Phase 7: Testing and Validation: 100.0%
PASS Phase 8: Advanced Features: 100.0%
PASS Phase 9: Multi-Modal AI: 100.0%
PASS Phase 10: Production Readiness: 100.0%

Recommendations:

  • 🟡 MEDIUM: Phase 3: LLM Integration requires attention (66.7% complete): Review and implement
  • ✅ System is production-ready - consider advanced features and scaling: Review and implement

@mrveiss

mrveiss commented Sep 12, 2026

Copy link
Copy Markdown
Owner Author

Delta 18b9e7f4a..c7e4b375f: verified, approved. One file, +37/−0. test_already_cancelled_before_the_first_checkpoint_does_nothing flips the fake manager to CANCELLED only on the WORKING update_state call, the one call before the "before scrub" checkpoint. Traced with that checkpoint deleted: execution reaches scrub_outbound, and scrub.assert_not_called() fails. The mock records the call before its side effect raises, so the executor's broad except can't hide it. hold_scopes' stop predicate isn't checked at acquisition, so nothing else short-circuits the path. The assertions check effects: no scrub, no orchestration, no artifacts, the claim released. The four existing tests are byte-identical. Closing refs are exactly [16174], plain and userLinkedOnly. The declined step-list refactor was non-blocking, and the reasoning in the body is fine. It merges on a settled green.

@mrveiss
mrveiss merged commit c6bed51 into main Sep 13, 2026
74 of 78 checks passed
@mrveiss
mrveiss deleted the issue-16174-a2a-cancel-cooperative branch September 13, 2026 13:36
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.

Cancelling an A2A task does not stop its executor, so claims can only lapse rather than release

1 participant