---
title: Inbox alerts (how an agent gets your attention) (part 3)
---

# Inbox alerts (how an agent gets your attention) (part 3)

## What it is

This is part 3 of the [Inbox alerts](inbox-alerts.md) page. It covers what happens around an alert rather than what one looks like: how a dismissed problem is kept quiet, where dismissed alerts go, the ways an alert can start work for you, and the daily digest that summarises the lot.

## Where to find it

Mostly the **Alerts** screen, reached from the inbox. It is laid out like any inbox: the alert list — including the recently dismissed ones — sits in a sidebar on the left, and the alert you pick opens on the right while the list stays in view, so you can move from one to the next. The daily digest arrives in the inbox as its own item.

## How it behaves

### Re-raise cooldown (a dismissed problem alert stays quiet for 24 h by default)

Deduplication keeps a _still-open_ alert to one row; a **re-raise cooldown** stops a _dismissed_ problem alert from bouncing straight back. Once you archive a problem/advisory alert, the **same** one (same `dedupKey`) cannot reappear for **24 hours** — enforced once at the shared `createAlert` chokepoint, so every problem alert inherits it, including ones added later. Within the window a fresh raise is silently suppressed (nothing new in the inbox, no notification); past it, the alert can surface again. 24 h is the **default**, not the only value — a handful of cards are deliberately slower (see "Cards that wait longer" below).

This generalizes the hand-rolled once-a-day caps the low-memory and AI-cost cards already carried into one floor beneath them — it never fights those (they already cap at 24 h) and it covers the ones that had no cap of their own (prompted by the **"low disk space"** card, which re-appeared every 15 minutes after each dismissal).

**A small set of persistent-infrastructure cards get a _shorter_ floor instead.** A condition that stays broken for days — a stalled auto-lander, a stale test baseline, a red production deploy pipeline — is exactly what a day-long silence _hides_: the card is swept up in a routine bulk-dismiss of the inbox and cannot come back until the next day, long after the thing it warns about has stopped being a surprise. Those families re-raise every **6 hours**. The clock is anchored to the card's **creation**, not the dismissal, so a dismissal buys at most `floor − (dismissed − created)`: measured on 2026-09-14, a deploy card 7.8 h old when dismissed was then silent for a further **16.2 h** on the 24 h floor, and re-raises on the next producer tick on the 6 h floor. (A card already older than the floor when dismissed is not suppressed by it at all — it can return immediately. That is independent of the floor's length and is why the length is the lever that matters here.) The exact per-card value is derived, never hand-maintained — the `re-raise` column in [ALERT_CATALOG.generated.md](../ALERT_CATALOG.generated.md) is the one authority; the auto-lander rebuilds it after every land (`npm run alerts:catalog` is the same run by hand), so a producer that changes that value shows up there rather than arriving as a second copy here. A branch never regenerates or commits it.

**Cards that list branches stay quiet for every branch you have already dismissed.** The auto-lander's grouped cards ("N branches need a hand to finish landing", "a ready branch is blocked by a safety check", the stuck card) name their branches. Inside the 6-hour window such a card comes back early only for a branch that was on **none** of the cards you dismissed in that window (`I26-member`). The "branches are parked" card is plain visibility and takes the ordinary 6-hour quiet. Until 2026-09-23 the check looked only at your most recent dismissal, so archiving one card could instantly bring back the branches from the card before it — 49 of that day's 96 give-up cards reappeared within 2 minutes of an archive.

**The "needs a hand to finish landing" card is never raised for a branch that is already being handled.** A branch that is back in the landing queue — tagged ready at the commit it is on — is left to the lander, however many times it was handed back before. A branch a live session is working (yours, its author's, or the lander's rescue — even mid-rebase) holds the card off while that session stays active; only one that has gone silent escalates. Until 2026-09-24 the card judged all three from stale bookkeeping, and 21 of the 40 branches it had given up on since 09-22 went on to land.

**A branch whose automatic rescues have run out is set aside quietly, not carded.** Each stuck branch gets up to three automatic rescue sessions — two, plus one last attempt that waits for a free slot and a calm machine rather than being skipped. If all three end without landing it and nothing is still working on it, the lander marks the branch `blocked` with the reason written on the branch, shows it in the Dev Pipeline panel's couldn't-land list, and raises no inbox card (owner decision 2026-09-24). The commits and the worktree are untouched, the daily lifecycle report counts it as handled, and running `/ready-to-merge` on it again puts it back in the queue. It is never set aside while a check is still running on it or while it is tagged ready at its current commit. If the `blocked` tag cannot be written, the card appears as before, and the lander switch `lander-giveup-set-aside` (`AMC_DISABLE_LANDER_GIVEUP_SET_ASIDE=1`) brings the card back for every spent branch.

It is scoped to genuine _problem_ alerts and never swallows real content:

- **Message / content alerts are exempt.** A **team-chat "new message"** notice reuses one row per channel, so a second message must never be suppressed — those are exempted centrally (`isReRaiseThrottleExemptDedupKey`). Drip items are exempt too (their own surface). Reused-key _status_ alerts (a broken RSS feed, a failing poll) are **not** exempt — for those, "at most once a day" is exactly what you want.
- **Urgent alerts bypass it.** A genuine spend **runaway** bypasses both the AI-cost card's own daily cap and the global cooldown (`bypassThrottle`), so a real money emergency always surfaces — unless you have turned AI cost alerts off, which silences the runaway as well ([ai-spend-alerts.md](ai-spend-alerts.md)).
- **You can turn it off.** **Settings → Notifications → Limit repeat problem alerts** (`problemAlertReRaiseThrottleEnabled`, on by default). Off → a dismissed problem alert can re-appear on its producer's own cadence; the per-card caps (low-memory, spend) still apply.

Like the per-card caps, the cooldown reads the most-recent prior card's timestamp straight from the database (counting a card you already dismissed), so it survives an app restart and a dismiss-then-spike the same day. See [inbox-alert-contract.md](../../.claude/memory/contracts/inbox-alert-contract.md) (I26).

### Cards that wait longer than 24 h

A few cards report a **standing situation** rather than an evolving incident — something you either fix once or knowingly accept. Their producers re-check on a schedule you don't control (every app launch, settings change or boot scan), so the 24 h default would turn each one into a daily nag about a decision you already made — and a card you dismiss on sight is a card you have stopped reading. These get a longer floor in the **central cadence registry** ([alert-reraise-throttle.ts](../../src/shared/alert-reraise-throttle.ts)):

| Card                                                                  | Waits       | Why                                                                                    |
| --------------------------------------------------------------------- | ----------- | -------------------------------------------------------------------------------------- |
| **"Backups are on the same disk as your data"** (`backup-colocated`)  | **90 days** | Re-raised each launch and backup-settings change; backup posture is a standing choice. |
| **"Mobile Access is on"** (`tunnel-public-exposure`)                  | 30 days     | Re-raised each boot auto-start and tunnel re-heal, for a feature you chose to turn on. |
| **"Monthly AI allowance used"** (`pooled-ai-monthly-limit`)           | 30 days     | The allowance is monthly, so the fact cannot change again until it resets.             |
| **"{engine} may be getting too many skills"** (`skill-budget-over:*`) | 30 days     | The scan re-runs ~60 s after every boot; the card itself is titled "Monthly reminder". |
| The watching-agent leverage recap                                     | 7 days      | Meant to land at most once a week.                                                     |

Two things this is **not**. It is not permanent silence — each card returns on its own cadence for as long as the situation lasts. And it never delays the **all-clear**: fixing the underlying thing (enabling an off-machine backup, turning Mobile Access off) clears the card immediately, because clearing archives the row directly and never goes through the cooldown.

A longer floor also takes that card out of the "a problem alert keeps quietly recurring" meta-notice — for these, being suppressed is the cadence working, not a hidden recurrence.

### Recently-dismissed view (the Alerts screen, last 7 days)

The dedicated **"Agent Alerts" screen** (the Alerts virtual-project row, gated by
`alertsSidebarEnabled`) keeps its list in a sidebar, like every inbox-style integration
([AlertsSidebar.tsx](../../src/renderer/src/features/alerts/AlertsSidebar.tsx), the registry's
`sidebarComponent`). The list shows your active alerts at the top — each with a **Dismiss** button
(revealed on hover) that archives it in place, without opening it — and, below them, a collapsed
**"Recently dismissed (N)"** section: agent alerts you archived within the last
**`DISMISSED_ALERT_WINDOW_DAYS` = 7** days ([alert-types.ts](../../src/shared/alert-types.ts)),
muted (theme-safe `text-surface-500`), each with a **Restore** button that un-dismisses it
(reusing the `ALERT_UNARCHIVE` chokepoint).

That sidebar carries **two tabs — "Alerts" and "Sessions"**. **Alerts** is everything
described here. **Sessions** is the other half: the Agent Alerts hub is itself a **session
host**, so agents you start in its context (the New session `+` while the hub is open, or the
**New session** button on the tab) run in the hub's own managed workdir
(`<userData>/alerts-agent`) and are listed there, through the same session list every other
hub uses. The **Sessions** tab carries the usual running / waiting-for-you / error count, so
you can see at a glance whether an agent is working without leaving the alert list. Picking a
session opens its chat in the main pane; the alert reading pane comes back the moment you
switch back to **Alerts**. ([AlertsSidebar.tsx](../../src/renderer/src/features/alerts/AlertsSidebar.tsx),
[alerts-session-host.ts](../../src/renderer/src/features/alerts/alerts-session-host.ts);
see [session-host-contract.md](../../.claude/memory/contracts/session-host-contract.md).)

Clicking an **active** alert opens it in the usual alert viewer (`AlertInboxViewer`, the same pane
the unified inbox uses). Clicking a **dismissed** one opens a **read-only** copy in the reading pane
([AlertsVirtualProject.tsx](../../src/renderer/src/features/alerts/AlertsVirtualProject.tsx),
through the shared `ArchivedAlertDetail`) — never the live viewer, which resolves only active alerts
and would sit on "Loading alert" forever. Its **Restore** button keeps the alert open, now as a live
one. With nothing open the pane says **"Select an alert"**, or **"No alerts"** when the list is
empty. The list and the pane are separate mounts, so which dismissed alert is open lives in a small
shared store ([alerts-hub-selection-store.ts](../../src/renderer/src/features/alerts/alerts-hub-selection-store.ts));
selecting any live alert — from the list, keyboard nav, the archive advance, the unified inbox or
a deep link — closes it, so one you moved on from can never resurface. On a
phone the list is the landing screen and a tap opens the alert full-screen.

In a **popped-out Alerts window** (Open in new window) the list and the pane come along, but no
main-window overlay exists there — so the reading pane draws the live alert itself (it asks
`isMainAppShell()`), **Go to session** opens that session in the main window, and an HTML-file
alert shows its normal file card instead of the live preview, whose `<webview>` a pop-out window
cannot host. The popped-out list stays live on its own — it starts the alert sync and the snooze
cache that only the main window's shell starts — and an alert archived elsewhere while it is open
on the Agent Alerts hub (in either window) is reconciled instead of spinning on "Loading alert".
Its buttons that lead elsewhere open in the main window: a Settings link on the exact page, **Open
channel** on the exact Team Chat channel, any other hub on that hub, and Start session plus the
actions that need the main app (Review updates, Resume them now, Finish setup, the app tour,
Restart) or pick a view inside a hub (View your records, View spend dashboard, Open this job,
Review emails) open the alert there, where they work.
Details: [pop-out-project-window.md](pop-out-project-window.md).

**Middle-click dismisses an active row on this screen too**, as everywhere else in
the inbox (the **Archive** action above). It routes through the same `archiveInboxItem` store
action, so it inherits the cursor advance, the Ctrl+Z undo and the rollback-on-failure. A row in **"Recently dismissed"** is already archived, so the gesture is a
deliberate no-op there — use **Restore** instead.

It is a pure **VIEW** window over data that already persists — archived alerts are NEVER purged
(the retention sweeps skip `inbox_alert_items`), so nothing new is stored. Backed by the read-only `listRecentlyDismissedAlerts`
([queries-inbox-alerts.ts](../../src/main/db/queries-inbox-alerts.ts) — `source_kind='agent'` so
it never poaches drip-sourced archived rows, the same project-liveness filter as
`listActiveAlerts`, `archived_at >= cutoff` bounded at 200, newest-first) over the
`ALERT_DISMISSED_LIST` read channel. The renderer keeps a **separate** `dismissedItems` store
slice (`loadDismissedItems`, epoch-guarded) refreshed only AFTER an archive/restore commits, so
the active list and its I12 reload-reconcile are untouched. A future retention purge of
`inbox_alert_items` MUST respect this 7-day window. See
[inbox-alert-contract.md](../../.claude/memory/contracts/inbox-alert-contract.md) (I24).

### Advancing to the next alert on the Alerts screen — the trap

**Dismissing an alert from the Agent Alerts screen moves you to the next alert**, the way
archiving a session moves you to the next one that needs you — so a pile can be cleared without
returning to the list between every row. On mobile the detail panel swaps its content in place
and only slides back to the list once nothing is left below, so you never land on a blank
full-screen detail.

That did not used to work, and the reason is a trap worth knowing before touching any hub.
The advance is the ordinary one — `advanceInboxCursorAfterDismiss` resolves the next row with
`getNavigableItems(view, 'attention')` + `findNextItem` — but it only advances **if it gets a
nav list at all**. The Alerts screen is a **virtual project**, so no session carries its UUID,
and its rows are alerts whose own `projectId` is the `__alerts__` sentinel (or their source
project, or the team-chat / tasks-v2 sentinel) — never this hub's UUID, which is all
`selectProjectAttentionItems` matches on. The generic project path therefore returned an
**empty** list here, and an empty nav list silently disables the advance: `findNextItem`
returns null, the selection is cleared, and on mobile the panel pops back to the list.

The fix is the seam, not the symptom:
[`getNavigableItems`](../../src/renderer/src/stores/session-nav-resolver.ts) recognises the hub
(`isAlertsHubProject`) and walks the hub's **own** queue from
[`alerts-hub-items.ts`](../../src/renderer/src/stores/alerts-hub-items.ts) — a verbatim
projection of the rows the panel renders, in the same `updated_at DESC` order, so nav order IS
rendered order. That one seam also covers keyboard nav in the hub. Deliberately **not**
`alertInboxSelectItems`: that one additionally drops snoozed rows and desktop-only nudges
because the global **inbox** hides them, and nav must walk exactly what this screen shows.
**Do not** re-derive the queue anywhere else — the single-source wall is
[nav-group-single-source.test.ts](../../tests/unit/lint/nav-group-single-source.test.ts), and
the behaviour is pinned by
[alerts-hub-nav.test.ts](../../tests/unit/stores/alerts-hub-nav.test.ts).

This is the identical bug the **Approvals** hub had — see
[approvals-hub.md](approvals-hub.md) § _Advancing to the next approval_. Any new virtual-project
hub that lists its own rows needs the same branch, or it inherits the same silent stranding.

### Reading-pane appearance (severity medallion)

Open an alert and the reading pane leads with a **tinted icon "medallion"** beside the title, and the panel carries a matching **thin left accent rail** + a **whisper-soft header wash** — all coloured by the alert's _kind_:

- **green** — an "Omniscio did a cleanup / self-heal" notice (the worktree-lock and install-orphan reapers, Auto-Tidy). An explicit allow-list — a real problem is **never** shown green. The two lowest-value self-heal FYIs — _"cloud-enforce auto-enabled"_ and _"cleared stuck worktree locks"_ — are **silenced by default** as noise (the cleanup + its audit log still run; only the FYI card is hidden). Turn on **`showSelfHealNotices`** to see them; the genuine _stuck_-reaper warning is never silenced by it.
- **amber** — an "action needed" notice (a reconnect prompt; a low-memory / low-spec / sync-drift / low-power warning; a Tasks reminder).
- **red** — an incident (an Anthropic API status incident, a broken cloud base).
- **neutral accent (indigo)** — the default for every other or unrecognised alert.

The colour + icon come from one pure classifier, [`alert-visuals.ts`](../../src/renderer/src/features/alerts/alert-visuals.ts) (`getAlertVisual`), keyed off the alert's `dedupKey`. Colours route through the themeable `status-*` / accent tokens, so a custom theme recolours the medallions automatically, and the medallion is **decorative** (screen-reader-hidden) — the title text carries the meaning, so colour is never the only signal. The accent is **alerts-only**: it rides `InboxDetailShell`'s optional, default-off `accentRailClass` / `accentWashClass`, so the approval panes and drip items render unchanged. The compact inbox **row** is unchanged (still an amber dot). See [inbox-alert-contract.md](../../.claude/memory/contracts/inbox-alert-contract.md) (I23).

**On a phone, a cut-off title is tap-to-expand.** The reading pane clamps the title to 2 lines to keep the header compact, so a long title ("Anthropic API issue: Fable 5…") shows an ellipsis instead of eating the screen. **Tap the title to reveal the whole thing; tap again to re-collapse.** A title that already fits stays a plain heading (nothing to tap), and it's fully keyboard- and screen-reader-friendly (the full title is always the button's spoken label, even while it looks clamped). Desktop is unchanged. See [inbox-alert-contract.md](../../.claude/memory/contracts/inbox-alert-contract.md) (I25).

### Start a session from an alert

Every agent-raised alert can kick off a Claude Code session about it. Open the alert and click **Start session** (the message-plus icon in the card's bottom action bar — available on desktop and mobile; **Archive** stays in the top-right corner of the header). A small dialog opens with **two text boxes**, a target-repo picker and a model/harness picker:

- a **Message** box — **empty** at open. It is your own instruction, and it owns the attachment affordances (paste / drop / paperclip) because it is the composer. Plain Enter submits from here, per your send-key setting; and
- an **Alert details** box — **collapsed by default**, and still fully editable once you expand it: a non-blank per-alert preset (`sessionPrompt`) leads, then the alert's title and content body (text / link / file), so the new session starts already briefed instead of with just the headline. Clicking the **Alert details** header reveals it. The alert text is reference material most sessions never edit and it pushed the rest of the dialog off screen, so it starts folded — the collapse changes **what you see, never what is sent**: the same text is attached to your message whether or not you open the box. Plain Enter stays a newline here, because it is a document to read and edit rather than a composer; and
- a **target repo** — the alert's saved `sessionProjectId` if it still exists and is spawnable, otherwise the project the alert is **grouped under**, falling back to the **Claude default**. Several **auto-lander** notices set `sessionProjectId` so Start session opens **in the affected repo** — the in-process supervisor already holds the repo's project id, so the _paused — uncommitted changes_ and _stuck — nothing landing_ alerts set it directly. The **paused** alert **waits out the self-heal window** before it appears — a base-dirty pause is almost always a transient the auto-lander repairs itself within ~10 min, so the inbox row is held until the folder stays dirty _past_ that window (a genuinely stuck checkout), then reads with a one-click **Open Diagnostics** button (the lander's status card) instead of the old "commit or stash" wording a non-programmer can't act on. Both **clear themselves when the repo recovers** — the paused row the moment the checkout goes clean again, the stuck row when the backlog lands or empties (mirroring the other self-clearing maintenance alerts above). A **second, longer-window** stuck escalation (_"stuck — over 15 minutes"_) fires **alongside** the ~5-minute one when a wedge persists on the **wall clock** — a backstop that still catches a lander whose supervision ticks are being silently skipped under heavy load; it sets `sessionProjectId` and clears on recovery the same way. When the blocker is a **foreign half-finished git operation** in the repo's folder (a leftover merge / rebase that never clears itself), the stuck copy names exactly that instead of the old misleading _"a land is already running"_, and both stuck alerts point you at **Start session** to clear it. (They carry no **Open Diagnostics** button — the whole auto-lander status/pileup family lost it on 2026-08-18 because it steered non-developers into a raw diagnostics screen; only the base-dirty _paused_, auto-preserved and tick-failing cards still deep-link there.) See [auto-lander-contract.md](../../.claude/memory/contracts/auto-lander-contract.md) (S13/S14/S20/S21). When the auto-lander **cannot finish** branches on its own — automatic rescues switched off, a rescue that could not even start, a session that went silent while holding the branch, or a set-aside it could not write (a branch whose rescues simply ran out is set aside quietly instead, see above) — the give-up notice (_"N branches need a hand to finish landing"_, coalesced into one card per repo) is written for a non-programmer: it reassures you the work is **safe** and points you at **Start session** to see why each branch stalled (no **Open Diagnostics** button: buttonless status/pileup family) and decide whether to land them or set them aside. It deliberately does **not** pre-fill a one-click rebase-and-land agent — the automatic retry it would re-run has already failed — so the copy no longer promises an agent can "finish landing them for you". The universal Start-session button remains, and since 2026-09-29 it hands the agent a **triage brief** instead of nothing: it points at the card's LIVE branch set, says the card collects every give-up reason so two branches on it can differ, and tells the agent to read each branch's OWN recorded reason before acting, land what can land through the normal path, and say why in one line and set aside whatever genuinely cannot — never a blind re-run of the retry that already failed. It **self-clears** a branch the moment that branch has nothing left to land: it lands, its work already reached master through **other** branches (a net-empty / superseded duplicate, which is also retired), or its ref is gone — so a branch superseded _after_ it was carded never lingers as a false "needs a hand" row (E5/E14). The **paused** (_"uncommitted changes"_) and **stuck** (_"nothing landing"_ / _"over N minutes"_) alerts carry the same one-click treatment: clicking **Start session** launches an agent already briefed to resolve that specific blocker — **safely**, since the paused case touches a checkout with uncommitted changes: the agent is told to preserve any real work (stash / recovery branch) and never run a destructive git command on changes it didn't make, while the stuck-agent aborts only a clearly-abandoned half-finished merge/rebase (E14). The same one-click treatment covers the last two per-branch notices: a branch that **moved after tagging** (new commits since its ready-to-merge tag), whose agent re-runs the checks and re-tags it, and one **blocked by a safety check** (a possible secret in the diff, or missing translations), whose agent runs the translation step for missing translations or, for a flagged secret, removes **and rotates** it safely rather than leaking it (the alert never contains the secret value) (E14). A companion **_"N branches are waiting to land"_** card coalesces every branch that has stayed ready-but-unlanded past a short threshold into **one card per repo** (each branch's exact wait time + reason live on the lander's **Diagnostics** status card); it sets `sessionProjectId` + a paused-lander preset so Start session opens **in the affected repo**, is a buttonless Start-session card (status/pileup family), and self-clears as its branches land — see [auto-lander-wait-visibility-contract.md](../../.claude/memory/contracts/auto-lander-wait-visibility-contract.md) (I3). The auto-lander's **check its folder** card (raised when a watched repo's folder looks moved, missing, or polluted) likewise **self-clears once the repo checks out healthy again** — and both its false-alarm shapes are caught before they alarm: a folder-health git probe killed under a startup load spike degrades to a quiet retry, and a "far more untracked files" overflow that is really the repo's OWN ignored deps + tracked source mis-scanned while a fleet of agents churns the checkout is **confirmed against git's committed history** (HEAD + the committed `.gitignore`, which a live scan race can't corrupt) before it alarms and otherwise degrades to a quiet retry — so neither leaves a stale "update your folder path" card (same contract, S18). Untracked clutter sitting INSIDE one of the repo's own tracked folders — an audit's saved copies, a tool's scratch — is never counted as a polluted folder at all, and a repo the lander does skip is named, with the reason, in its status line instead of reading as an idle sweep (S177). The **"main checkout out of sync"** notice arrives with no project (the merge worker can't know the project UUID), so `POST /alert` fills its `sessionProjectId` at ingest — resolving Omniscio's own checkout to its project (path-matched, spawnable-gated) — so Start session pre-selects **Omniscio**. The same ingest-time resolution covers the other projectless Omniscio-self maintenance cards keyed by their `dedupKey`: the **cloud-base-broken** card and the **worktree-cleanup** card (_"Unmerged worktrees need inspection"_ / _"Worktree triage"_, `dedupKey: unmerged-inspection-queue`, raised by a scheduled cleanup cron that likewise can't know Omniscio's project UUID). The seven **cloud-fleet self-cards** raised by the cloud-queue monitor do the same — three health verdicts (_"Cloud fleet health degraded"_, _"…failing systemically"_, _"…split-brain"_) plus four informational status cards (_"VM disk saturated"_, _"GCP capacity exhausted"_, _"spot-preemption wave"_, _"queue fall-open storm"_), each about Omniscio's own cloud test-offload fleet. Omniscio's two **self-health** alerts — _"Unusual number of errors"_ (`dedupKey: telemetry-error-spike`) and _"Sessions are failing to start"_ (`dedupKey: telemetry-spawn-failures`) — do the same, resolved **in-process** by the health monitor: best-effort, so a resolution hiccup never suppresses the alert, and off a packaged build it falls back to the Claude default. An explicit `sessionProjectId` still wins. See [inbox-alert-contract.md](../../.claude/memory/contracts/inbox-alert-contract.md) (I14).
- a **model & harness** picker — collapsed to the project's **resolved default** engine and model, so you can read what will run without touching anything. It is optional in the strong sense: an untouched picker forwards **nothing**, so a launch that ignores it is byte-identical to one made before the picker existed. Change it only to run this session on a different engine or model; it uses the same shared chips as the New Session screen and the mobile composer. The **Model** list here spans the **whole harness**, not just the engine your default resolves to: on a DeepSeek default you can still pick Opus, Sonnet, GLM, Kimi or any other brain the Claude Code harness runs, one row per model grouped by maker, and the pick carries its engine along with it. (Unlike the in-session picker, this dialog has no separate Provider control, so a provider-only list would leave everything but your default engine unreachable.)

The **two boxes compose ONE opening message**: your Message **leads**, the alert text follows, and a blank side is dropped. A left-empty Message box therefore sends exactly the alert text — what this dialog sent before the second box existed — and an emptied Alert details box sends your message alone.

The message the session actually receives also carries an **alert-context block the boxes never show**, appended at launch time by `buildAlertContextNote` (`src/shared/alert-session-prompt.ts`): the alert's **type code**, the **subsystem and repo file(s) that raise it** (resolved from the generated alert catalog, so the agent can go read the code behind the card instead of guessing from its prose), when it was **first raised** and how many times, and the `GET /alert/:id` route to read the row back. On a **folded** card — several same-title reports merged onto one row, which reads "N separate reports share this heading" — the block opens with one more line: how many reports the card lists, that starting the session dismissed all of them, and that the card's own instructions apply to every report, so the session handles each one rather than only the newest.

Edit any of them, then confirm to spawn the session (launch source `alert-start-session`). Confirming is **optimistic**: the dialog closes **immediately** and the **alert is dismissed the instant you click** — never blocking behind a spinning "Starting…" button or a ~1s pause — moving you straight to the next item that needs you while the session starts **in the background**, without pulling you into it. Your prompt and the agent's reply stream into the new session in the sidebar. (A rare failure to start **brings the alert back** with a toast so you can retry, since the dialog is already gone.) The button shows on **every** agent alert — the only exemptions the guard sanctions are a team-chat notice and a card whose bottom the reply box occupies ([inbox-alerts.md](inbox-alerts.md)) — and the agent does **not** have to attach a prompt for it to appear. The app-generated alerts listed above (stuck-task, low-spec, Low Power Mode, sync-drift, Gmail reconnect, GitHub reconnect, account re-auth, team-chat new message, release-notes, Anthropic status, …) each **also** show their own feature-specific button **alongside** it, never instead of it, with Start session leftmost in the bar so each card's own action stays rightmost. If no spawnable repo exists the button is **disabled** (never hidden) with a tooltip. The alert text is built by `buildAlertSessionPrompt` and the two boxes are joined by `composeAlertSessionPrompt` (both in `src/shared/alert-session-prompt.ts`), mirroring Drip's launch. This "every alert, apart from the two sanctioned exemptions" rule is **build-enforced**: `alert-start-session-universal.test.ts` (in the cross-cutting guard lane) fails the build if any change would let an alert ship without the button — the gate narrowed to exclude an alert, the button re-gated at its render site, or the button removed. See [inbox-alert-contract.md](../../.claude/memory/contracts/inbox-alert-contract.md) (I8) and [alert-start-session-optimistic-contract.md](../../.claude/memory/contracts/alert-start-session-optimistic-contract.md) (the optimistic close + no-double-spawn invariants).

### Auto-remediation (enrolled infra alerts auto-start a fix session)

For a **curated set of Omniscio-infra alerts** — cloud fleet degraded / failing-systemically / split-brain / offload-down, direct-SSH refused / firewall-failed, cq-monitor crashed, ops-deploy drift, Firebase degraded, the auto-lander / auto-merge daemon stalled, the worktree reconcile / trash-sweeper stuck, WAL checkpoint failing, and a degraded freeze-prevention gate — Omniscio doesn't wait for you to click **Start session**: it **auto-spawns the remediation session itself**, in the background, into the **Omniscio** project, then **silent-archives the card** (the spawned session is the only signal). This generalizes the four bespoke cloud self-healers (cloud-base / seed / chain / deploy, which fire from the `POST /alert` route) to the shared `createAlert` chokepoint, so an **in-process** infra alert is covered too — the enrollment allowlist is [`alert-auto-remediation-registry.ts`](../../src/shared/alert-auto-remediation-registry.ts) (a subset of `isAmcSelfAlertDedupKey`).

It is **cost- and freeze-safe by construction**: only a **freshly-created** enrolled alert spawns (a dedup bump never does); a **3-minute boot-grace** suppresses the startup re-detection storm; a **6-hour per-condition cooldown** (in-memory, surviving the card archive) stops a flapping alert re-spawning; and a **global burst cap** bounds a simultaneous multi-condition failure. The spawn also rides the existing daily spend-cap + disk/memory pacing in the session spawner. **Deliberately excluded** (they stay passive cards): informational / self-heal notices, anything that needs YOU (reconnect / reauth / low-spec / unsaved-changes worktree), the memory/CPU **perf/pressure** alerts (spawning under load would worsen a freeze), and the **spawn-queue** alerts (a remediation spawn could itself raise them — reentrancy).

**One fault, one fixer.** A single outage often raises several different alert types within minutes, and each used to start its own fix session. Now, before an auto-fix session — or one of the four cloud self-healers — starts, Omniscio looks for sessions that were started from an alert in the **same area** within the last two hours and are still running, and names up to five of them at the top of the new session's instructions. The new session reads what they are doing and stands down (handing its alert to that session) **only** if one is already working the same fault; otherwise it carries on and names the related session in its report. It never blocks a fix: if the lookup fails or takes more than two seconds, the session starts exactly as before. Every automatically started fix session also carries the same clickable **Spawned from** link to its alert that a card-started session has.

Turn it off at **Settings → Lab → "Auto-fix infrastructure alerts"** (`autoRemediateAlertsEnabled`, on by default) for a passive card you act on yourself — a fix session costs money, so the off switch lets you approve each one; the emergency env kill-switch is `AMC_DISABLE_ALERT_AUTO_REMEDIATION=1`. See [alert-auto-remediation-contract.md](../../.claude/memory/contracts/alert-auto-remediation-contract.md).

### Where an alert came from (provenance)

**Every alert says where it came from.** Open any alert and its detail view (`AlertInboxViewer`) names the origin right under the title, in one of two forms:

- **"Generated by &lt;session name&gt;"** — when a _session_ raised it. Captured from the `X-AMC-Source-Session-Id` header (Omniscio-spawned agents send it automatically) and persisted as the `source_session_id` snapshot column on `inbox_alert_items`. Click it to jump straight to that session.
- **"From &lt;feature&gt;"** — when a _background service_ raised it, which is the case for every built-in advisory (session load balancing, the spend monitor, the status pollers). These carry no session, so before this they showed **no origin at all**. The feature is named in plain words — never an internal id — and the name is a **link to Settings → Notifications**, so every card also carries a route to manage or turn off notifications, not just the few with their own off-switch. Resolved by `describeAlertSource` ([alert-source.ts](../../src/shared/alert-source.ts)), which falls back to "Omniscio" for a producer it doesn't recognize.

The origin is **detail-only** — it never appears on the compact inbox row. A team-chat notice is the one exception to the two forms above: its header already leads with the sender's name and avatar, which _is_ its provenance. See [session provenance](session-provenance.md) and [inbox-alert-contract.md](../../.claude/memory/contracts/inbox-alert-contract.md) (I11 / I15c).

### The daily lifecycle digest (landing and workspace notices arrive once a day)

Omniscio's landing and workspace machinery used to be by far the loudest thing in the inbox: measured over the week to 2026-09-09, it raised **41 cards a day**, spread across **60+ different kinds of notice, most of which happened less than once a day**. That spread is why fixing them one at a time never worked; none of them was big enough to matter on its own, and together they buried everything else.

They are now split by a rule:

- **A few genuinely need you.** Six kinds still arrive as their own card, the moment they happen — a branch that changes dependencies and needs your OK, a broken build on master, a merge that could not land, a conflict the auto-resolver gave up on, workspace creation failing, and _nowhere left on disk to put a workspace_.
- **Most of the rest arrive once a day.** The "landing is behind" family, cleanup pacing, "your unsaved work is kept safe", inventory counts — all gathered into **one card per area per day**: one for landing, one for workspaces. Each lists what happened, how many times, when it started and stopped, and how much of it fixed itself. That is about 31 notices a day folded into 2 cards.
- **A third group is deliberately left alone.** Around 21 kinds of notice live in the same part of the app but are not part of this split — genuine infrastructure faults that almost never fire, so there was no evidence to justify quieting them. They behave as they always did, about 4 cards a day.

  One of them is **"a project document could not be rebuilt after the last merge"** (2026-09-23). Some documents in a project are written by the machine rather than by a person — a list of every script the project ships, for instance — and Omniscio rebuilds them automatically after each merge. When a rebuild fails, the saved copy may go out of date, and if it does, the next branch that touches that area gets turned away when it tries to merge, for rows it did not cause. Nobody working on that branch can see it coming, because those documents are only checked at merge time. So the failure now gets a card of its own, naming which documents may be out of date, instead of only a line in a log nobody reads. It is one card per project however many documents are affected, it **updates itself** as documents come back, and it **disappears on its own** once the last one is rebuilt. A rebuild that failed only because the machine was momentarily out of resources is retried automatically first, so the card appears only for a problem that is really stuck. **A rebuild the machine stopped before it could finish** — it ran out of time on a busy machine, or the machine refused to start it — tells us nothing about the document, so it is simply tried again on the next pass and raises **no card**; the card appears only if that keeps happening — **three passes in a row, over at least half an hour** (2026-09-26: a single timeout had raised it over merge helpers that were all up to date).

Altogether: **about 13 cards a day instead of 41.**

The big win is what you never see at all: **91% of the gathered notices resolve on their own within a day**, so they are archived by the system before the digest is even written. A problem that fixes itself was never yours to do.

**A day where everything fixed itself sends you nothing at all** (2026-09-12). The digest is only written when at least one thing is still going on. It used to send a card on those days too — titled "everything sorted itself out", with nothing under it but a list of things that had already resolved — which is a card whose own title tells you not to bother reading it. The owner's rule: _an alert that doesn't change what you'd do isn't worth sending._

**The landing notices were skipping this until 2026-09-23.** The hold that sends a notice to the daily digest lived inside one of the app's ways of raising a card, and the auto-lander raised its own cards another way. So "Auto-lander stuck", "a ready branch is blocked by a safety check" and the other landing notices listed above went straight to the inbox instead — 216 of them between 2026-09-08 and 2026-09-23, about 90% of which fixed themselves within minutes. The hold is now applied where every card is written, so no route can skip it.

**"A branch was stopped past its time limit" joined the daily summary on 2026-09-24.** Its own text always said the branch would be retried automatically — and it was: 24 of the last 30 such branches landed on that retry, typically about half an hour later — yet each card stayed until you archived it by hand. It is now one card per repository listing the branches, a branch comes off the moment it lands, and the card is held for the daily summary, so a branch that lands on its retry never interrupts you and one that never lands is named every day until it does.

**Three changes on 2026-09-25.** "Auto-lander is not reaching your waiting branches" now waits for the digest (29 of its last 32 cleared themselves within minutes), while _unfinished work nobody is coming back for_ and _a blocked branch landed with its author's sign-off_ keep their own card. A notice still going on after a day stays in the digest, named every day until it stops, instead of arriving as its own card two days in (17 did, 2026-09-10 to 09-25). A one-off heads-up that can never "clear", such as _a landed branch had no session to notify_, counts as settled and sends no card.

**Six more kinds of notice joined the digest on 2026-09-28**, and they are the ones there was genuinely nothing for you to do about. A session that could not be resumed after a restart (it tries again next time the app restarts), idle sessions parked to protect memory (that protection stands down by itself), a test result that could not be delivered (the retry and the session's own end handle it), the test-baseline readout (a status, not a fault), cloud test-machine faults repeating (the fix is a developer's command), and guard-baseline debt growing on master (also a developer's job). Measured over the eight days before the change: 57 of these arrived, every one cleared by hand, and not one of them cleared itself.

The readout is worth knowing about: it reports the result of each test-baseline run, and the digest shows the **latest** one each day rather than each run as it finishes. It also reports on healthy runs, so its card appears most days — that is deliberate, it is the one item here you asked to keep seeing.

**On 2026-09-29 the digest took on the biggest cluster of all: the background machinery talking about itself.** A daily card called _Background jobs and housekeeping_ now carries the scheduled jobs that have not finished a run (they are still being tried on their normal schedule), the ones that keep failing (the notice clears the moment one run succeeds), a job that has stopped being attempted, a fleet status that keeps re-paging, background jobs using more of the computer than they should, a "still working" notice for a long wait, a tool with a newer version, and the "this PR body needs its real delta" / "nobody picked up that PR reply" cards — both of which tell a developer to run a command, not you. Alongside them, six finishing-touches notices from the workspace machinery ("a step is running but achieving nothing", "a step is running slow", a reclaim backlog, slow workspace creation, a git repository growing). Measured over the seven days before the change: these families sent **369 cards and every one was cleared by hand**; not one of them cleared itself, and none of them asked you anything.

What deliberately stayed a card, because the rule says so rather than because of how loud it is: everything about **your money** (including the metered-vendor spend cards, which now come with a fix — see below), every notice that carries a **button** you can press, work that is **at risk** of being lost, and the **performance** notices. Those last ones are the app telling you your own machine was slow, and slowing you down to say it would be the wrong trade — so they keep arriving when they happen.

One bug came out of this pass. The "spend today is unusually high" card for a metered vendor was coming back every 15–45 minutes **after you had already dismissed it** — six fresh cards for one provider in a single day, each one closed by hand in the seven days measured. The cause was in the card's own escape hatch: a runaway is allowed to skip the once-a-day wait so it is never missed, and that skip was also skipping your dismissal. A card you close now stays closed for the rest of that day, and a genuinely new runaway another day still arrives immediately.

Nothing is hidden by that. The self-resolved notices are not thrown away — they are held, and if something real does turn up later the same day, that day's card appears with the new item **and** the earlier self-resolved ones listed under it. A condition that keeps happening is named in every day's digest until it stops, and if the digest itself ever stops running, the individual cards come straight back — the system is built to fail toward noise, never toward silence.

### Turning a built-in notice off from the card

Omniscio's own advisory cards each carry a quiet **"Turn off these notices"** button in the card's bottom bar (pushed far left so it never out-shouts the card's main action). One click turns that _one kind_ of card off, confirms with a toast, and archives the card; Settings → Notifications has the matching toggle to turn it back on. Turning a notice off only silences the **card** — the underlying work keeps running (turning off the load-balancing notice does not stop sessions being spread across your logins, exactly as turning off the stuck-install notice does not stop the cleanup).

Cards with their own off-switch today: low-memory warnings, AI-cost alerts, stuck-install cleanup notices, screen-grab block notices, inbox-backlog nudges, watch-time recaps, input-method shortcut warnings, and **session load-balancing notices** (the "your `<login>` is carrying N active sessions" card — `loadBalancingNoticeEnabled`). Alerts that report a genuine _failure_ you need to act on — reconnect Gmail, sign in again, a dead keyboard shortcut — deliberately have **no** per-card off-switch, so a real problem can't be silently hidden.

### Relationship to Drip

Drip is a second producer of the same primitive. On release, the scanner calls the same `createAlert` service, writing rows into the shared `inbox_alert_items` table with `source_kind = 'drip'`. Those rows surface via the `drip` inbox integration (`listDripSourcedInboxItems`, filtered by `source_kind`), NOT the `alert` integration — each agent-raised row has `source_kind = 'agent'`, so the two integrations never overlap in the inbox.

A drip-sourced alert **reuses its `drip_item`'s id**, so the inbox row (alert) and Drip's detail-view row (`drip_item`) are addressable by one id: `DRIP_ITEM_ARCHIVE`/`UNARCHIVE` dual-write both tables to keep the unified inbox and DripDetailView in sync. The synthetic "drip finished" `completion` row is the one item that stays a `drip_item` only (no alert), so it remains excluded from the cross-drip inbox as before. See [inbox-alert-contract.md](../../.claude/memory/contracts/inbox-alert-contract.md) (I9, I10).

## Related

What an alert is and how you answer one is the [Inbox alerts](inbox-alerts.md) page, and how one is authored is [part 2](inbox-alerts-part-2.md). The rules that can suppress alerts automatically are on [Inbox rules](inbox-rules.md).
