Skip to content

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.

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 full ladder is behind a flag and off by default:

ONY_ESCALATION_ENABLED=true

An 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.

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.

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.

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.

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.

# Telegram
ONY_TELEGRAM_BOT_TOKEN= # from @BotFather
ONY_TELEGRAM_BOT_USERNAME= # without the @; builds the account-linking deep link
ONY_TELEGRAM_WEBHOOK_SECRET= # required in production when the bot token is set
# Slack
ONY_SLACK_BOT_TOKEN=
ONY_SLACK_SIGNING_SECRET= # verifies X-Slack-Signature; required in production when the token is set

The 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.

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.