Give every selected record a result state

Use a stable record ID and a result state that survives the aggregate banner:

{
  "case-a": { state: "succeeded", attempt: 1, message: "" },
  "case-b": { state: "failed", attempt: 1, message: "Version changed", retryable: false },
  "case-c": { state: "failed", attempt: 2, message: "Temporary error", retryable: true }
}

Useful states are pending, succeeded, failed, and retrying. Do not replace a settled row with a late duplicate result. React's array-state guidance applies here: make an immutable replacement for the one result row rather than mutating a shared array.

Give each attempt its own request identity. When a retry starts, increment or replace that attempt token; accept a result only when its token matches the currently pending attempt. A stable record ID alone cannot distinguish a late result from the original action and a result from the retry.

Derive the banner from the rows

An aggregate is a summary, never the only source of truth. Derive counts from the per-record states and keep failure rows visible beneath it.

Aggregate What the screen must still show
All succeeded Which records completed
Some failed Each failed record, its reason, and its next action
Some pending Which records still have uncertain outcomes
Retry in progress The one record being retried and preserved prior successes

W3C's status-message guidance is a useful review route for announcing a changed result without a surprising focus move. This draft has not been tested in a browser or with assistive technology; make that product-specific review part of the publication gate.

Retry only under a server-defined rule

React's useOptimistic can show an immediate pending projection, but it cannot determine whether a failed server action is safe to repeat. A failure might mean a transient read problem, a version conflict, an authorization change, or an outcome that is still unknown. Retrying all selected records can duplicate a consequential action.

The local P96 model allows retry only for a row marked retryable by the result contract. It verifies that a successful row stays successful during a retry, that a delayed initial-attempt result cannot settle a retry, and that a non-retryable failure does not replay.

Preserve the trusted boundary

The screen must not claim an action was authorized, idempotent, durable, or reversible merely because it rendered a green row. The backend owns those properties and should return enough outcome information for the product's recovery policy. Stop before shipping if a partial failure can be hidden by a success banner or a retry can repeat an action without a server-defined rule.