There’s a timing problem in confirm-before-execute flows that’s easy to overlook until it causes a duplicate action: the window between when the user confirms and when the action actually dispatches.
In most implementations, the flow looks like this:
- Agent plans an action
- User is shown a confirmation prompt
- User taps confirm
- Action dispatches
The assumption is that step 3 leads directly to step 4. But in practice, there’s a gap — and things can happen in that gap.
This post covers what we call the late confirmation race in StareBrain, and how we handle it without introducing false blocks or silent duplicates.
What Can Go Wrong in the Gap
The most common case: the action was already dispatched by an earlier path before the confirmation came back.
How does that happen? A few ways:
- The user confirmed on one device/session while a background task had already triggered the same action
- A retry fired because the first dispatch timed out, but the timeout was too aggressive — the original dispatch was still in-flight
- The auto-confirm threshold was met (for low-risk actions) at the same time the manual confirm came back
In all three cases, a naive implementation dispatches twice. One dispatch was right. The other is a duplicate.
Checking Terminal State Before Replacement Dispatch
The fix is straightforward: before honoring a confirmation by dispatching, check whether the action is already in a terminal delivery state.
`sealed class ConfirmationOutcome {
data class Dispatched(val actionId: String) : ConfirmationOutcome()
data class AlreadyCompleted(val actionId: String, val state: DeliveryState) : ConfirmationOutcome()
data class Superseded(val actionId: String) : ConfirmationOutcome()
data class Blocked(val actionId: String, val reason: String) : ConfirmationOutcome()
}
class ConfirmationHandler(
private val actionRepository: ActionRepository,
private val dispatcher: ActionDispatcher
) {
fun handleConfirmation(actionId: String): ConfirmationOutcome {
val record = actionRepository.find(actionId)
?: return ConfirmationOutcome.Blocked(actionId, "action record not found")
// Terminal check before dispatch
return when (record.delivery) {
DeliveryState.CONFIRMED -> {
ConfirmationOutcome.AlreadyCompleted(actionId, DeliveryState.CONFIRMED)
}
DeliveryState.FAILED -> {
ConfirmationOutcome.AlreadyCompleted(actionId, DeliveryState.FAILED)
}
DeliveryState.PENDING -> {
// Safe to dispatch — not yet in a terminal state
dispatcher.dispatch(record)
ConfirmationOutcome.Dispatched(actionId)
}
}
}
}`
If the delivery state is already CONFIRMED or FAILED, the confirmation is honored as a no-op. The user’s tap is acknowledged, but no second dispatch fires.
The DENIED_UNRESOLVED Case
The terminal check above handles CONFIRMED and FAILED cleanly. The harder case is DENIED_UNRESOLVED — the action dispatched, but the outcome is unknown.
This is not a terminal state in the sense that it’s resolved, but it is a state where a replacement dispatch is dangerous: if the first dispatch actually landed on the provider side and just failed to confirm back, a second dispatch is a duplicate.
`fun handleConfirmation(actionId: String): ConfirmationOutcome {
val record = actionRepository.find(actionId)
?: return ConfirmationOutcome.Blocked(actionId, "action record not found")
return when {
record.delivery == DeliveryState.CONFIRMED ->
ConfirmationOutcome.AlreadyCompleted(actionId, DeliveryState.CONFIRMED)
record.delivery == DeliveryState.FAILED ->
ConfirmationOutcome.AlreadyCompleted(actionId, DeliveryState.FAILED)
record.actionStatus == ActionStatus.DENIED_UNRESOLVED -> {
// Do NOT dispatch. Surface for reconciliation first.
ConfirmationOutcome.Blocked(
actionId,
"prior dispatch unresolved — reconcile before retry"
)
}
record.delivery == DeliveryState.PENDING ->
dispatcher.dispatch(record).let {
ConfirmationOutcome.Dispatched(actionId)
}
else -> ConfirmationOutcome.Blocked(actionId, "unexpected state")
}
}`
The DENIED_UNRESOLVED case blocks replacement dispatch and routes to reconciliation. The user sees that the previous attempt’s outcome is unknown and needs to be checked before proceeding — not a silent failure, not a silent retry.
Idempotency on the Dispatch Side
The terminal check on confirmation prevents duplicate dispatches from the confirmation handler. But the dispatch path itself should also be idempotent, because the confirmation handler isn’t the only entry point.
`class ActionDispatcher(
private val repository: ActionRepository,
private val provider: ActionProvider
) {
fun dispatch(record: ActionRecord): DispatchResult {
// Idempotency key scoped to action + attempt number
val idempotencyKey = "${record.actionId}:${record.attemptNumber}"
// Re-check delivery state inside dispatch — the terminal check
// in ConfirmationHandler is a fast path, not the only guard
if (record.delivery != DeliveryState.PENDING) {
return DispatchResult.Skipped(record.actionId, "non-pending state at dispatch")
}
return try {
val result = provider.execute(record, idempotencyKey)
repository.markDelivered(record.actionId, result.providerObjectId)
DispatchResult.Succeeded(record.actionId)
} catch (e: TimeoutException) {
repository.markUnresolved(record.actionId)
DispatchResult.Unresolved(record.actionId)
} catch (e: Exception) {
repository.markFailed(record.actionId)
DispatchResult.Failed(record.actionId, e.message)
}
}
}
sealed class DispatchResult {
data class Succeeded(val actionId: String) : DispatchResult()
data class Failed(val actionId: String, val reason: String?) : DispatchResult()
data class Unresolved(val actionId: String) : DispatchResult()
data class Skipped(val actionId: String, val reason: String) : DispatchResult()
}`
The idempotency key scoped to actionId + attemptNumber means a retry on the same attempt doesn’t produce a second provider-side effect. A new attempt gets a new key.
What the User Sees
The goal is that the user never sees a duplicate action and never gets a silent failure. What they see instead:
- Normal path: confirm → action runs → done
- Already completed: confirm acknowledged, brief notice that it already ran
- DENIED_UNRESOLVED: confirm blocked, clear message that the previous attempt’s outcome is unknown and needs to be checked
The third case is the uncomfortable one to surface. But a user who sees “we’re not sure if the last attempt ran” is in a better position than a user who just had a duplicate booking made silently.
What This Closes
The late confirmation race is a narrow window but it’s a real one — especially in mobile contexts where network conditions vary and the same action can be triggered from multiple paths. The terminal check before dispatch, combined with idempotent dispatch, closes both the happy-path duplicate and the unresolved-state duplicate without adding meaningful latency to the normal flow.
Top comments (0)