Skip to main content
A stopped or superseded turn can no longer send, react, open a thread, edit or delete — the effect is dropped fail-closed with the detail turn no longer live (stopped or superseded).

Quick Start

1

Run a bot with interrupt mode

Set busy_mode="interrupt" and give the agent the send_message tool — the outbound liveness binding is automatic. No extra config.
2

Scenario — /stop drops an in-flight outbound effect

Send /stop while the agent is about to post a late reply. The send never reaches the chat; the tool call returns a fail-closed result instead.
3

Scenario — mid-send attachment upload halts

Send /stop while a 3-image upload is partway through. Files already uploaded stay; the remaining ones are skipped.

How It Works

Each turn’s BotOutboundMessenger is bound to that turn’s InterruptController. Every outbound verb checks the controller before touching the router. Every detail is the same string: turn no longer live (stopped or superseded).

What Triggers the Drop


Backward Compatibility

No new user-facing config. The binding is automatic whenever a bot runs a turn with an InterruptController (any bot using busy_mode). Without a controller the messenger is unbound and behaves exactly as before. Code that constructs BotOutboundMessenger without liveness= is unchanged, and nothing in the gateway YAML schema moves.

Failure Semantics

If the liveness check itself raises, the messenger treats the turn as live and delivers. A buggy probe can never wedge delivery — it fails open on the probe, never on the effect. This is the same rule as ApprovalRegistry._reject_if_stale on the approval path.
/stop only sets a flag — it does not cancel the running send coroutine. So liveness is re-checked before every attachment upload, and the remaining files are counted as skipped once the turn goes stale.
The messenger reads the turn’s controller flag, which /stop set and nothing clears. Any code still holding that turn’s messenger can never deliver through it again.
A turn that finishes normally never sets its controller flag. Background workers that kept a messenger reference — such as deliver-backed subagents — keep delivering after the turn ends.
Scheduler jobs are meant to outlive the turn that created them and already have principal scoping. They are not bound to turn liveness.

Best Practices

Turn liveness matters most when the latest message should win. In busy_mode="interrupt", a superseding message (or /stop) drops the abandoned turn’s outbound effects so a late reply never lands after the user moved on.
The binding wires itself when agent.interrupt_controller is an InterruptController at the time the turn’s messenger is created:
Without a controller, the messenger stays unbound.
Run a send_message tool call under a turn, issue /stop from another message, and check the returned DeliveryResult:
  • ok is False
  • detail is "turn no longer live (stopped or superseded)"

Bot Run Control

The busy_mode + /stop side that fires the cancel

Approval Turn Liveness

The sibling guarantee on the approval path

Send Message Tool

The agent-callable tool that goes through this messenger

Send Policy

The destination guard that runs before this check

Outbound Media Delivery

The attachment path with the per-upload re-check

Turn Protection Window

Why busy_mode="interrupt" queues during subagents and compaction