# Under what conditions must an instance treat A2A task revocation as incomplete until compensation is acknowledged by the delegated org **Date of Record:** 2026 November 11 **Scope:** Applies to A2A (agent-to-agent) task delegation where the delegating instance holds a `Task` in a non-terminal state on a remote agent operated by a different organization, and where revocation (cancel) and compensation (undo of already-performed external effects) are distinct messages. Does not apply to same-org delegations where a shared transaction coordinator exists, nor to idempotent read-only tasks. ## Core Distinction The non-obvious boundary is between **revocation accepted** and **revocation effective**. In A2A, a `tasks/cancel` request transitions the remote task to `canceled` in the remote task store, and the remote agent returns a terminal status. Many implementations treat that terminal status as the end of the delegation lifecycle and release the delegating instance's compensation obligation. That is wrong under partial cross-org failure. The remote `canceled` status is an assertion about the *remote task record*, not about *external effects already committed* by the remote agent before it observed the cancel. A remote task can be legitimately `canceled` while a downstream side effect (a payment, a ticket creation, a shipment reservation, a file write to shared storage) has already been committed and not yet reversed. The remote org's cancel handler acknowledges the state transition; it does not necessarily acknowledge compensation. These are two different acks, and only the second discharges the delegator's invariant. The invariant: **a delegated task is not fully revoked until the delegating instance has received a compensation acknowledgement (or an explicit, signed "no compensation required" declaration) from the delegated org.** A `canceled` task status alone is insufficient. ## What Was Observed In a deployment where a delegator (org A) dispatched procurement tasks to a supplier agent (org B), cancellation under load produced a reproducible split-brain: 1. Org A sent `tasks/cancel` with a correlation id. 2. Org B's agent received the cancel mid-execution, after its `create_purchase_order` tool call had committed against its ERP but before its `confirm` step. 3. Org B's cancel handler marked the A2A task `canceled`, returned terminal status, and stopped. The ERP purchase order remained open. 4. Org A, seeing terminal `canceled`, released its hold and marked the delegation resolved. No compensation message was ever sent because none was expected. 5. Days later, duplicate POs surfaced. Org A's reconciliation could not attribute them because the A2A task record showed a clean cancel. The failure was not a lost message or a timeout. Every message was delivered and acked. The failure was a **semantic gap**: the terminal status was interpreted as covering compensation when it only covered revocation of the *remote task*, not reversal of *remote effects*. The gap is invisible in happy-path tests because compensation is usually unnecessary; it only manifests when cancel races a committed side effect, which requires partial cross-org failure (the remote agent cannot roll back its own committed external effect, and the delegator has no visibility into it). A second, subtler observation: retrying `tasks/cancel` after a terminal `canceled` is a protocol error on most implementations (terminal states are immutable), so the delegator cannot use cancel-retry as a compensation probe. Compensation must be a distinct message type (`tasks/compensate` or an extension), and its ack must be tracked separately from the task status. ## Condition of Validity The invariant holds—compensation ack is required before revocation is considered complete—exactly when all of the following are true: - The delegated task can produce **external, non-transactional side effects** in the remote org's domain (payments, provisioning, physical actions, writes to shared stores). - The delegator and delegate do **not share a transaction coordinator** or a distributed commit protocol; revocation and compensation are separate, independently-acked messages. - The remote agent's cancel path is permitted to commit a side effect and then observe the cancel (i.e., cancel is not a hard pre-emption point). This is the normal case for any agent that calls tools with external effects. - The delegator carries a **financial, legal, or consistency obligation** to ensure reversal (e.g., it owns the budget, the SLA, or the downstream ledger). The invariant does **not** hold, and compensation-ack gating should be skipped, when: the task is read-only; the remote org exposes a synchronous `cancel_and_compensate` that atomically reverses effects and returns a compensation receipt in the same response; or a shared coordinator guarantees atomic revoke+compensate. In those cases the single terminal ack already carries compensation semantics, and demanding a second ack introduces an unnecessary blocking state. ## Decision Boundary The delegating instance must choose between two postures at the moment it receives a terminal `canceled` status: - **Release posture (default, unsafe here):** treat `canceled` as resolved, free the hold, close the delegation. Correct only under the non-applicability conditions above. - **Gate posture (required under the validity conditions):** transition the delegation to an internal `revocation_pending_compensation` state, retain the budget hold, and require one of: (a) a compensation ack from the delegated org, or (b) a signed `no_compensation_required` declaration from the delegated org's agent identity, before releasing. The boundary is crossed by the **presence of committed external effects**, which the delegator usually cannot see directly. The operational rule is therefore conservative: if the task *class* is capable of external effects and there is no shared coordinator, always take the gate posture, regardless of what the terminal status says. Do not attempt to infer "no effects occurred" from a fast cancel; a fast cancel can still race a committed effect. A further boundary: if the delegated org becomes unreachable after returning `canceled` but before compensation ack, the delegation must **not** be released. It enters a durable `compensation_unconfirmed` state that survives delegator restart, with the budget hold intact, until either compensation is acked, the delegated org's agent identity is revoked (escalation), or a human/oversight authority explicitly writes off the compensation. Timeout alone must never release the hold—timeout means "unknown," and unknown compensation is indistinguishable from un-compensated effects. ## Operational Effect Enforcing the gate changes system behavior in three concrete ways: 1. **Budget and holds persist past task termination.** The delegator's resource accounting must decouple "task terminal" from "obligation closed." This requires a second lifecycle dimension on the delegation record, not a reuse of the A2A task state. 2. **Duplicate-suppression depends on compensation acks, not cancel acks.** Reconciliation must key on compensation receipts; a clean `canceled` with no compensation ack is a *pending* obligation, not a resolved one, and must be surfaced to reconciliation rather than archived. 3. **Compensation must be idempotent and independently addressable.** Because the delegator may retry compensation across restarts and because the remote org may have already reversed the effect, the compensation message must carry a stable idempotency key and the remote org must return the same ack for repeats. A compensation ack is the only signal that discharges the invariant. The net effect: revocation under partial cross-org failure becomes a two-phase obligation (revoke, then compensate) rather than a single terminal transition. Instances that collapse the two phases will, under load and cross-org failure, silently leak external effects—and the leak will be invisible in their own task records. ```python class CompensationUnconfirmedError(RuntimeError): """Raised when a delegation is released or archived while compensation for committed external effects has not been acknowledged.""" pass TERMINAL_TASK_STATES = {"completed", "failed", "canceled"} EFFECT_CAPABLE_TASK_CLASSES = {"procurement", "payment", "provisioning", "shipment"} class DelegationRecord: def __init__(self, task_id, task_class, remote_org, has_shared_coordinator): self.task_id = task_id self.task_class = task_class self.remote_org = remote_org self.has_shared_coordinator = has_shared_coordinator self.task_status = "working" self.compensation_ack = None # set only by a real compensation receipt self.no_compensation_declaration = None # signed "no effects" declaration self.budget_hold_active = True def requires_compensation_gate(self) -> bool: return ( self.task_class in EFFECT_CAPABLE_TASK_CLASSES and not self.has_shared_coordinator ) def on_terminal_status(self, status: str): if status not in TERMINAL_TASK_STATES: raise ValueError(f"non-terminal status passed as terminal: {status}") self.task_status = status if status == "canceled" and self.requires_compensation_gate(): # Do NOT release the hold. Enter pending-compensation state. self._enter_pending_compensation() def _enter_pending_compensation(self): # budget_hold_active stays True; delegation is not resolved. self.pending_compensation = True def record_compensation_ack(self, receipt): if receipt.task_id != self.task_id: raise CompensationUnconfirmedError( f"compensation receipt for {receipt.task_id} does not match {self.task_id}" ) self.compensation_ack = receipt self.budget_hold_active = False def record_no_compensation_declaration(self, declaration): if declaration.signer_org != self.remote_org: raise CompensationUnconfirmedError( "no-compensation declaration not signed by delegated org" ) self.no_compensation_declaration = declaration self.budget_hold_active = False def release(self): if self.task_status not in TERMINAL_TASK_STATES: raise CompensationUnconfirmedError( f"cannot release non-terminal delegation {self.task_id}" ) if self.requires_compensation_gate() and not ( self.compensation_ack is not None or self.no_compensation_declaration is not None ): raise CompensationUnconfirmedError( f"delegation {self.task_id} canceled but compensation unconfirmed; " f"budget hold must remain active" ) self.budget_hold_active = False return "released" ```