Skip to main content

Exceptions

A queue of failures and anomalies surfaced as first-class, triageable items rather than log lines.

Overview

The platform files an exception whenever something needs a human's attention — an orchestration run that failed after exhausting retries, a guardrail tripwire that aborted an action, or an approval that expired without a decision. Each item carries a severity, structured detail, and provenance links, and moves through an open → acknowledged → resolved triage lifecycle. Repeated identical failures fold into one item with an occurrence count, so a hot failure loop never floods the queue.

Exceptions are auto-filed by the platform (or filed explicitly as manual); there is no public create endpoint. They are read, acknowledged, and resolved through the API, and an exceptions.created webhook fires on the first occurrence so alerting is push, not poll.

See the Permissions Reference for the IAM action strings for this module.

Data Model

ExceptionItem

FieldTypeDescription
idstringPublic ID, exc_ prefix
project_idstringOwning project
statusstringopen, acknowledged, resolved
severitystringinfo, warning, critical
kindstringrun_failed, guardrail_tripwire, approval_expired, quota_unpriced, event_trigger_loop, chain_limit, manual
titlestringHuman-readable one-line summary
detailobject | nullStructured context (tool, error, guardrail version)
occurrence_countintegerTimes this exact failure was observed while open
last_seen_atstringTimestamp of the most recent occurrence
orchestration_run_idstring | nullOriginating orchestration run
node_idstring | nullOriginating node id within the run's graph
agent_idstring | nullAssociated agent
guardrail_versionstring | null<guardrailId>@<version> for a guardrail_tripwire item
acknowledged_bystring | nullAcknowledging user's public ID
resolved_bystring | nullResolving user's public ID
resolution_notestring | nullOptional note recorded at resolution
created_at / updated_atstringTimestamps

Key Concepts

Severity

Severity is keyed to actionability, not raw "badness". Each kind has a default a producer can override:

KindDefault severityWhy
run_failedcriticalA run died after exhausting retries — needs intervention
guardrail_tripwirewarningThe guard worked as designed; also a feedback-loop signal
approval_expiredwarningFail-safe missed SLA — the action never ran
quota_unpricedwarningA cost cap is measuring less than it caps; needs a config fix, not incident response
event_trigger_loopwarningThe causation guard stopped a self-feeding event trigger; the wiring still needs a human
chain_limitwarningA continuation chain spent its generation budget — the guard stopped it, and an agent that cannot terminate on its own still needs a human
manualwarningAuthor-chosen

Occurrence dedup

Repeated identical failures fold into one open item rather than filing duplicates: a partial unique index keys at most one open exception per dedup key, and each recurrence bumps occurrence_count and last_seen_at (only the first emits exceptions.created). A resolved item frees the key, so a recurrence after resolution opens a fresh exception. manual items are never deduped.

Triage lifecycle

An item is open when filed. Acknowledge it (acknowledged) to signal someone is on it — distinct from resolve (resolved, "fixed"), which records the resolver and an optional note. A resolved item is terminal: acknowledging or resolving it again returns 409 EXCEPTION_ALREADY_RESOLVED.

Producers

Exceptions are filed by subscribing to platform events, so producers stay decoupled: run_failed rides the existing orchestration_runs.failed event, approval_expired rides approvals.expired, and guardrail_tripwire rides a dedicated guardrail.tripwire event emitted from the guardrail dispatch path. Every filing is fire-and-forget — it never disturbs the producer.

event_trigger_loop is filed by the event-trigger dispatcher when a trigger refuses to extend the causal chain that reached it — because the chain already names that trigger, or because it has run past the depth cap. It is deduped on the trigger and the reason, so a loop that keeps re-arriving is one triage item whose occurrence_count reads as how often it was refused; detail carries the chain and the event name, which is the only place that wiring is visible (the events themselves are not persisted).

chain_limit is filed when a continuation chain is refused for spending its generation budget. It rides a dedicated generations.chain_limit event and is deduped on the chain's root generation, which is the one id every refusal in a chain shares: an over-budget chain is refused once per resumption, so keying on the refused hop would file one item per occurrence of exactly the runaway this reports. detail carries the root, the initiator that asked for the refused turn, the chain's size, the budget it hit, and limit_sourceagent when the agent's own max_chain_generations refused it, project when the project's max_chain_generations did, platform when the deployment's ceiling did, so the number alone does not leave you guessing which knob to turn.

This is the signal that a chain stopped growing. The refusal itself is recorded on a trace and returned to a caller that is usually a background sweep with nothing left to hand it to, so without the exception a runaway would be bounded but still reach nobody until the bill arrived.

quota_unpriced is the exception to the event-driven pattern: it is filed inline from the quota pre-generation check, which is the only place that knows a cost cap just evaluated against a window whose usage was not fully priced. A window that priced nothing and one that priced only part of its usage file the same item — the fix is the same price rows, named in the item's unpriced_rows — and it is deduped on the quota rather than the window, so one degraded cap is one triage item and occurrence_count reads as the number of generations that ran under it. The check fails open, so a filing error can never block a generation.

Examples

# List open, critical exceptions in a project
soat list-exceptions --project-id proj_01 --status open --severity critical

# Triage one
soat acknowledge-exception --exception-id exc_01
soat resolve-exception --exception-id exc_01 --note "Root cause fixed; reran the pipeline."