---
title: Sentry Triage Gate
---

# Sentry Triage Gate

## What it is

**What it is.** A decision step that runs between "Omniscio noticed a new Sentry issue"
and "Omniscio spawns a Claude investigation session for it." Before this feature, every
new Sentry issue with auto-spawn on became a full session immediately — and because
Sentry splits one underlying bug into many distinct issue IDs, you'd get a pile of
near-duplicate sessions burning tokens (e.g. four sessions for the same
`Cannot read properties of null` error in one minute). The triage gate decides
**whether each issue is worth a session at all**, and holds the rest in a review
queue.

This page is self-contained: it describes the behavior in prose before pointing at
code, so an outside AI given just this page can answer questions about it.

## Where to find it

### Where you see it

The **Bug Intake** sidebar virtual project (the same place Sentry sources are
configured). Its header carries the triage controls, and its tabs changed:

- **Sources** — your Sentry org/project connections (unchanged).
- **Review** (was "Pending") — issues the gate is **holding**: it didn't auto-spawn
  them, and it's asking you to decide. Each row shows the real error title, level,
  and event count, plus four buttons: **Spawn** (start a session now), **Ignore
  this** (never spawn for this exact Sentry issue again), **Ignore class** (never
  spawn for this kind of error again), and **Dismiss** (a one-off, not a permanent
  rule). If you click **Spawn** on an issue whose error class **already has a live
  investigation running**, Omniscio opens that existing session instead of starting a
  second one — and drops a short inbox note saying it consolidated — so a manual Spawn
  can't accidentally double up on a class you (or the gate) already started.
- **Ignored** (new) — your active ignore rules, each with an **Un-ignore** button.
- **Audit** — the last 500 decisions, now including the new outcomes
  (`ignored` / `consolidated` / `triage`).

## How it behaves

### Held issues also reach the unified inbox (2026-06-03)

You don't have to open Bug Intake to act on a held issue — held Sentry issues now
ALSO appear in the **unified inbox** as cards, **grouped by error type**, so they
reach you on any view (including mobile) the moment the gate holds them. Each card
carries the same actions as the Review tab:

- A **class card** (N issues of one error type) → **Spawn** (one investigation for
  the class), **Always ignore** (permanently mute the whole class), **Dismiss all** (one-off
  dismiss of every issue in the class; confirms first above 5).
- A **single card** (a one-off issue with no class key) → **Spawn**, **Ignore this**,
  **Dismiss**.
- An **overflow card** ("+N more awaiting review") appears past 25 cards and
  deep-links to the Review tab.

Opening a card shows its **detail pane**, where the actions also have **one-key
shortcuts** while the pane is open — **N** Spawn · **T** Always ignore · **G** Ignore
this (only the ones that apply to that card). The keys show **on hover** (each button's
tooltip includes its key), not as an always-on legend. Spawn is the standard **New
session** icon button and its key is **N** — the same key that starts a New session
everywhere else in the app — because spawning an investigation is just opening a new
session for that error. **N** is the only one that starts a paid session, so it's
guarded against an accidental double-press (and Ctrl+Enter is deliberately NOT wired to
it). **Dismiss** is the pane's top-right **Archive** button (it clears every issue in
the group and confirms first above 5), so there is no separate bottom-row Dismiss button
or **D** key.

The cards **self-clear**: every refresh re-queries the held list, so a card vanishes
the instant its issue is spawned, dismissed, ignored, or auto-promoted — there is no
separate "clear" event, and nothing new is stored (the inbox is just a second window
onto the same held rows). It only shows while Bug Intake is enabled. Contract +
invariants: [sentry-triage-contract.md](../../.claude/memory/contracts/sentry-triage-contract.md)
("Inbox Review surface" – `the-inbox-card-self-clears-by-requery` through
`registered-like-any-inbox-source`).

### Issues held by the safety screen

Once the gate decides an issue deserves a session, one more check runs before the agent
starts: the bug intake **safety screen**, a very cheap AI that reads what the agent would be
handed and looks for text trying to give the agent orders. An issue it flags, or cannot check,
shows in Review and as an inbox card of its own with a **Held by safety screen** badge and the
reason. Unlike an ordinary held issue it is never auto-promoted or judged by the AI pass: only
you can start it (**Spawn anyway**) or dismiss it. Spawn anyway still opens an investigation
already running for the same error instead of starting a second one. The Bug intake safety
screen page has the details.

### How the gate decides (cheapest check first, first match wins)

**First, an unconditional safety drop (before any rule below, and even when the master
switch is off).** Omniscio's _own_ background telemetry has three helpers that mirror their
failures to Sentry. When one reports a Firestore permission error (`PERMISSION_DENIED` /
`UNAUTHENTICATED`), that's harmless plumbing config — not an app bug — so Omniscio records it
as `ignored` and never spawns an investigation. Without this, a machine running an older
copy of Omniscio could flood the shared error tracker with that noise, trip the "50+ events =
investigate" rule below, and spawn a **paid** session to investigate Omniscio's own telemetry
(a self-referential loop). A genuine _code_ bug in those helpers (no permission keyword)
still gets investigated, and so does a permission error from anywhere else. See the
contract invariant **`internal-telemetry-noise-is-dropped-pre-gate`**.

For each new Sentry issue, Omniscio runs these rules in order and stops at the first one
that matches:

1. **Ignore list.** If you've ignored this exact issue, or its **class** (the kind
   of error — same code location + title), it's skipped. _(free)_
2. **Consolidate.** If a session investigating this same error class is already
   open, the new issue is held instead of spawning a duplicate. This alone fixes
   the "four identical sessions at once" problem — the first one spawns, the rest
   consolidate. _(free)_
3. **Severity bar — plus evidence it's real.** Sentry assigns each issue a level
   (`fatal` / `error` / `warning` / `info` / `debug`). Issues clearly **below** your
   bar (default `error`) are held for review. At or above the bar, Omniscio also wants
   some evidence the problem is real before spending on an investigation: it spawns if
   the issue is a **regression**, or if it has fired enough times — **twice** for an
   issue above the bar, **50+** for one right at it. Otherwise it's held. _(free)_

   > **Why "above the bar" still needs a second sighting.** Every native crash reaches
   > Sentry marked `fatal`, which is above the default bar — so before this rule, a
   > single crash on ONE person's machine, on any version, started a paid investigation
   > immediately. In one two-week sample, 10 of 15 fatal issues were single-event. A
   > crash that happens once and never again is almost always an environmental blip
   > (the machine was out of memory, the GPU driver hiccuped), which is the same
   > conclusion Omniscio had already reached for crashes reported the other way. Nothing
   > is lost: a held crash still shows up in Review and the inbox, still gets the cheap
   > AI second look, and **auto-promotes to a full session the moment it happens again**.
   > `fatal` still spawns far sooner than `error` — 2 events versus 50 — so severity
   > still means something.
4. **Hold for the AI queue.** Gray-zone issues right at the bar aren't judged on the
   spot — they're held, and a separate AI pass looks at them **every ~15 minutes**
   (see below). The 5-minute poll spends nothing on them. _(free)_

Anything the gate doesn't spawn lands in the **Review** tab — where the AI queue
picks it up.

### The AI queue: gray-zone issues judged every ~15 minutes by Haiku

Rather than calling AI on every 5-minute poll, Omniscio batches it. Roughly every 15
minutes it takes the gray-zone (borderline) issues it's been holding and asks
**Haiku** (a fast, cheap model) about each _kind_ of error once — spawn / hold /
ignore. A "spawn" recommendation starts **one** session for that error class (any
other held issues of the same class fold into it, so you don't get duplicates);
"hold" leaves it in Review; "ignore" leaves it in Review **with a suggestion** that
you mute the class (it never mutes anything on its own — you confirm).

### Auto-promote (held issues that get worse)

A held issue isn't forgotten. Every poll, Omniscio re-checks the issues it's holding
against fresh Sentry data and promotes it to a full session automatically when either
of two things happens:

- its event count crosses your **auto-promote threshold** (default 100 events); or
- it now **clears the spawn bar** — which includes a held above-bar crash that has
  simply **happened a second time** (rule 3 above).

Setting the threshold to `0` turns off the **count** half only. The second half keeps
working, so a crash that recurs is always picked up — that is what stops the
"second sighting" rule from ever parking a real, repeating crash forever.

### The settings (Bug Intake header, when the gate is on)

| Control             | Setting key                         | Default | What it does                                                                    |
| ------------------- | ----------------------------------- | ------- | ------------------------------------------------------------------------------- |
| **Triage gate**     | `sentryTriageEnabled`               | on      | Master switch. Off = every issue auto-spawns (pre-triage behavior).             |
| **Consolidate**     | `sentryTriageConsolidateByClass`    | on      | Hold issues whose error class already has a live session.                       |
| **Spawn bar**       | `sentryTriageAutoSpawnMinLevel`     | `error` | Severity at/above which issues auto-spawn.                                      |
| **(AI on/off)**     | `sentryTriageAiSecondLookEnabled`   | on      | The ~15-min Haiku queue that judges gray-zone issues.                           |
| **(AI cap)**        | `sentryTriageAiDailyCapUsd`         | `$0.25` | Per-day spend ceiling for the AI queue; at the cap, gray-zone issues just hold. |
| **Auto-promote at** | `sentryTriageAutoPromoteEventCount` | `100`   | Event count that auto-promotes a held issue (`0` = off).                        |

All six are read fresh on every poll, so toggling them takes effect without a
restart. With the master switch off, the gate is a pure passthrough — behavior is
exactly as it was before this feature, **except** the unconditional telemetry-noise
safety drop above, which always applies (it runs before the gate, not inside it).

### What the AI queue costs you, and how it's bounded

- It only looks at the **gray-zone (borderline)** issues — not every issue.
- It runs **about once every 15 minutes**, not on every poll, and is **deduplicated
  to one call per error class per run** — so cost scales with how many _distinct
  kinds_ of error you have, not how many issues, and not how often Omniscio polls.
- The model is **Haiku** (via OpenRouter), with a second model (Qwen) as an automatic
  backup only if Haiku errors.
- Spend is recorded to Omniscio's cost log under the source `sentry-triage-ai` and capped
  per day. Once the cap is hit, gray-zone issues simply hold (no spend, no surprise
  spawns).
- It requires an **API-key account** (the login/OAuth path can't make these calls).
  With no API-key account, the queue is off and gray-zone issues hold.
- It **never spawns on its own uncertainty**: any failure (cap hit, no account,
  bad response, network error) falls back to holding the issue for you.

### Ignore: "this one" vs "everything like this"

- **Ignore this** keys on the exact Sentry issue ID. A brand-new clone of the same
  bug (a different ID) would still come through.
- **Ignore class** keys on the error's **class** — its normalized code location +
  title — so future occurrences of the same kind of error are all suppressed. This
  is the one that stops recurring noise. It's disabled for native crashes that have
  no usable class key (use "Ignore this" there).
- Ignoring is reversible: the **Ignored** tab's **Un-ignore** restores future
  spawning. Ignoring an issue also immediately clears any matching rows already
  sitting in Review.

### What the spawned investigation receives

When the gate spawns a session, the investigator's prompt now includes the issue's
**actual stack trace and recent breadcrumbs** — fetched from Sentry's latest event
at spawn time, not just the title, level, and a Sentry link. Secrets and user paths
are scrubbed out first. The fetch is fail-safe: if Sentry can't be reached, the
session still starts with the metadata it always had (it's never blocked).

For Omniscio's **own** crashes (the `jls-trading-co` / `mission-control` Sentry project),
the investigator is additionally pointed at the on-disk crash logs in
`<userData>/logs` (`main.log`, `heartbeat-final.log`, `diagnostic-report.json`) —
the same forensic evidence Omniscio's crash email carries. That pointer appears only for
Omniscio's own crash source, since an investigator can't read another project's machine
logs. This is what lets an Omniscio crash captured by Sentry become a full investigation
with the real stack trace _and_ the local logs, instead of just a one-line title.

### Relationship to the existing duplicate detection

This is a **new, earlier** layer. The triage gate decides _whether to spawn_; the
older 24h duplicate detection ("Dedup pre-check") runs _after_, on issues that do
spawn, and hints the agent that a session might be a duplicate. They're independent
and both on by default. See [bug-report-intake.md](bug-report-intake.md) for the
duplicate-detection side.

## For agents

### Code pointers (for agents with repo access)

- Pure decision engine: `decideTriage` / `classifyBySeverity` / `computeSentryClassKey`
  in `src/main/services/intake/intake-triage.ts`.
- Gate wiring + auto-promote pass + the 15-min AI drain (Phase D): `pollSentrySource`
  in `src/main/services/intake/intake-sentry-service.ts`; the drain cadence
  (`aiSweepAt` / `AI_QUEUE_DRAIN_INTERVAL_MS`) is in `intake-poll-scheduler.ts`.
- Unconditional pre-gate telemetry-noise drop + its shared prefix/token source of truth
  (also consumed by sentry-init's `beforeSend` emit-side drop, so the two never drift):
  `textMatchesInternalTelemetryPermissionFailure` in
  `src/shared/sentry-internal-telemetry-noise.ts`, applied in `pollSentrySource` before
  the triage gate.
- AI queue evaluator: `evaluateSentryTriageAi` + `makeTriageQueueAiRunner` (Haiku
  primary) in `src/main/services/intake/intake-triage-evaluator.ts`.
- Ignore rules: `intake_ignore_rules` table + `src/main/db/queries-intake-ignore.ts`.
- UI: `src/renderer/src/features/bug-intake/BugIntakeView.tsx`.
- Schema: dated ledger migration (`triage_class_key` + `triage_meta` columns, `intake_ignore_rules`
  table).
- **Invariants + safe-change rules:** `.claude/memory/contracts/sentry-triage-contract.md`.

## Related

[bug-report-intake.md](bug-report-intake.md) covers the older duplicate detection that runs after the gate, on issues that do spawn, and hints the investigating agent that a session may be a duplicate. To find anything else this library holds, start from [INDEX.md](INDEX.md).
