Escalation and fallbacks
A single phone call is a weak reachability guarantee: you miss it, you are somewhere only SMS or chat works, or you want a retry before Ony.ai gives up. The escalation ladder (ADR 0009) adds a resiliency layer that retries and falls back across channels, without weakening the two guarantees: the call is the high-signal apex, and a decision is always a signed, action-bound verdict the agent enforces, never a notification it can ignore.
Every channel is a decision-capture front end onto one core. Whether you press a keypad digit, tap a
Telegram button, or reply to an SMS, it writes the same single Decision for the handoff.
The backup ring (no flag needed)
Section titled “The backup ring (no flag needed)”Even with the full ladder off, Ony.ai can do more than one call. Configure an org backup number and
the out-of-box behavior becomes a single primary call plus one backup-contact attempt: if the primary
leg goes unanswered, Ony.ai rings the backup once. Set it in the dashboard under team settings; the number
is OTP-verified before it is stored, so Ony.ai never rings an unverified backup. This is a single-shot
ring, never a loop, and it needs no ONY_ESCALATION_ENABLED.
The escalation ladder
Section titled “The escalation ladder”The full ladder is behind a flag and off by default:
ONY_ESCALATION_ENABLED=trueAn escalation policy is an ordered list of steps. Each step is { channel, wait_seconds, repeat }:
deliver on channel, wait wait_seconds for a decision, retry up to repeat times, then advance to
the next step. When the steps exhaust with no decision, the handoff expires and the agent fails
closed (defers or blocks) exactly as it would without escalation. Escalation extends the reachability
window, never the authority.
Each org gets a default policy, seeded on first use and editable per org: call you, retrying about
two minutes apart, then fall back to a single SMS (call wait 120s repeat 2 -> sms wait 180s repeat 1). When the ladder runs, its call retries replace the single-shot backup ring above, so you never get
double-dialed.
Timing is durable: each run stores its next_attempt_at, and a lightweight in-process periodic tick
advances every run whose timer is due. State lives in the database, so a run survives a worker restart.
There is no external job broker.
Channels
Section titled “Channels”| Channel | Can decide? | How it delivers |
|---|---|---|
| Call | Yes | The same outbound voice call, keypad approval |
| SMS | Yes | A reply-to-decide text (“Reply 1 to approve or 2 to deny”) |
| Telegram | Yes | An interactive Approve / Deny message with buttons |
| Slack | Yes | An Approve / Deny message with buttons, sent as a DM |
| Webhook | No | Log-only stub (see below) |
The first channel to capture an approve or deny wins: it writes the one signed verdict, the run
transitions to decided, and in-flight attempts are cancelled best-effort. Late arrivals hit the
unique-decision constraint and are ignored cleanly.
Identity assurance is gated by risk
Section titled “Identity assurance is gated by risk”A keypad-PIN’d call is strong identity; a chat button tap is weaker. Ony.ai maps (channel, risk) to
whether a step may capture a decision or only notify. For the most sensitive actions, only
high-assurance channels (a call plus PIN) can approve; weaker channels notify and can pause escalation
but cannot decide. When a step can only notify, the SMS and chat messages drop the approve/deny
controls and say “answer the call or approve in the dashboard.” This mirrors the voice policy:
CRITICAL actions and production deploys are dashboard-only and cannot be approved from any remote
channel. See the risk and policy model.
What the channels never carry
Section titled “What the channels never carry”Voice, SMS, and chat carry only the server-derived action and risk tier plus a redacted gist, never
the raw client title, command, diff, or details payload. Chat messages can include a short preview
and a dashboard deep link for the full payload. See the threat model.
Configuring the chat channels
Section titled “Configuring the chat channels”Telegram and Slack are bring-your-own-bot in the OSS core. Ony.ai DMs only the device owner on these channels (a chat identity is a personal link, so an approvable button is never broadcast to someone else), and the user must first link their chat account to Ony.ai.
# TelegramONY_TELEGRAM_BOT_TOKEN= # from @BotFatherONY_TELEGRAM_BOT_USERNAME= # without the @; builds the account-linking deep linkONY_TELEGRAM_WEBHOOK_SECRET= # required in production when the bot token is set
# SlackONY_SLACK_BOT_TOKEN=ONY_SLACK_SIGNING_SECRET= # verifies X-Slack-Signature; required in production when the token is setThe inbound button callbacks are re-validated server-side against handoff ownership, so a crafted callback for someone else’s handoff is rejected. In production the boot guard refuses to start if a channel that can send has no verification secret configured.
Open-core split
Section titled “Open-core split”The engine (policy, step, and run models, the run state machine, the next_attempt_at scheduler, the
channel-adapter contract, and the built-in call, SMS, and mock adapters) is open-core. Branded managed
channels (a hosted Slack app, a hosted Telegram bot, and the SMS number pool) and per-risk-tier
policies are part of the premium ony_cloud module. In the OSS core you get the same ladder by
bringing your own bot tokens and carrier.
Related
Section titled “Related”- Carrier setup - the SMS and voice channels need a carrier (and US SMS needs 10DLC approval).
- Configuration reference - every escalation and channel variable.
- The /ony modes - when Ony.ai gates an action in the first place.