/stop (or supersedes the turn with a new message in busy_mode="interrupt"), any approval the turn was waiting on is cancelled β the bot replies immediately instead of hanging, and a late Allow tap for that stopped turn is dropped.
Quick Start
1
Run a bot with interrupt mode and presentation approval
Set
busy_mode="interrupt" and approval="presentation" β the turn-liveness wiring is automatic. No extra config.2
Scenario A β /stop unblocks a parked turn
Trigger a tool that needs approval, then send The awaiting tool call resolves with
/stop before you tap Allow. The turn unwinds immediately instead of hanging on the approval wait.reason="cancelled" β the bot replies rather than parking indefinitely.3
Scenario B β a late Allow tap is dropped
Tap Allow on that stopped turnβs card after The resolution is dropped fail-closed and recorded with
/stop. Nothing happens: no stale tool runs, no stale reply lands.reason="superseded" in the audit trail.No new user-facing config is introduced. The binding is wired automatically when a bot uses
busy_mode with SessionRunControl (for example Telegram with busy_mode="interrupt"). Requests that carry no session_id/run_generation behave exactly as before.How It Works
A gateway approval request can optionally bind to the turn liveness of the originating run viasession_id + run_generation. When the run is stopped or superseded, the gateway fail-closes any pending future and drops any resolution that arrives afterwards.
User Interaction Flow
What Triggers the Cancel
busy_mode="steer" folds the new message into the same turn, and busy_mode="queue" preserves the running turn β neither fires on_supersede, so a pending approval on that turn stays live. busy_mode="interrupt" (and /stop) is the path that cancels.
Backward Compatibility
No new user-facing config, and a no-op for unbound requests. Existing single-turn flows are unchanged.
- The binding is automatic when a bot uses
busy_modewithSessionRunControl(e.g. Telegram withbusy_mode="interrupt"). - Approvals not stamped with
session_id/run_generationβ anything outside the bot β gateway path today β are always considered live and keep behaving exactly as before. - Todayβs single-turn / no-supersede flows behave identically; the cancel path only runs when a turn is actually stopped or superseded.
Failure Semantics
TOCTOU guard β /stop racing register()
TOCTOU guard β /stop racing register()
If
/stop marks the generation superseded before the toolβs register() call lands, register() denies immediately with reason="superseded" β it never parks the caller on a future that no cancel batch will ever complete.Non-integer run_generation β fail-open to unbound
Non-integer run_generation β fail-open to unbound
An un-coercible
run_generation logs a warning and registers the approval unbound rather than becoming silently un-cancellable. The request still resolves normally (fail-open to unbound, not fail-closed to stuck).Newer generation stays live
Newer generation stays live
Cancelling generation N does not affect generation N+1 on the same session. A fresh turn started after the stop is unaffected and resolves normally.
Callback failures are swallowed
Callback failures are swallowed
The
on_supersede callback is best-effort. Any exception is logged and swallowed (see _notify_supersede) so /stop and interrupt handling are never broken by a missing gateway package or manager.Best Practices
Prefer busy_mode='interrupt' for the strongest benefit
Prefer busy_mode='interrupt' for the strongest benefit
Turn-liveness cancellation matters most when the latest message should win. In
busy_mode="interrupt", a superseding message (or /stop) cancels the abandoned turnβs pending approval so the bot never hangs on a decision for a turn the user already replaced.Why busy_mode='queue' does not cancel a running turn's approval
Why busy_mode='queue' does not cancel a running turn's approval
Queue mode preserves the current turn on purpose β that is the point. A follow-up is parked for the next turn, so the running turnβs pending approval stays live and can still be resolved normally.
Custom integrators embedding SessionRunControl
Custom integrators embedding SessionRunControl
Pass Without the callback, the old silent-hang behaviour returns.
on_supersede=make_approval_supersede_callback() (or an explicit ExecApprovalManager) to opt in:Verifying the fix locally
Verifying the fix locally
Run the bot with
busy_mode="interrupt", trigger an approval, and issue /stop:- The awaiting tool call must unwind with
reason="cancelled". - A late reviewer Allow for the stopped turn must be a no-op with
reason="superseded"in the audit trail.
Related
Bot Run Control
The
busy_mode + /stop side that fires the supersede callbackGateway Approval Durability
The persistence side β pending approvals that survive a restart
Telegram Durable Approval
The transport side β Allow/Deny buttons on chat
Gateway Scoped Approvals
Per-request reviewer custody on the same manager

