---
title: Inbox alerts (how an agent gets your attention) (part 2)
---

# Inbox alerts (how an agent gets your attention) (part 2)

## What it is

This is part 2 of the [Inbox alerts](inbox-alerts.md) page. It is the authoring half: what it takes to add a new kind of alert, how an agent or a script raises one, the text handling that keeps exotic characters intact, and the cards the app raises on its own with buttons attached.

## Where to find it

There is no user-facing screen for most of this — it describes how an alert is created rather than how it is read. A reader who only wants to answer alerts should start with the [Inbox alerts](inbox-alerts.md) page instead.

## How it behaves

### Adding a new alert type (for developers)

Declare one row in `src/shared/alert-type-registry.ts` — its key, a plain-language title, a
category, and whether it may be silenced — then raise it. That row is also where you say who sees
it, whether it pages the owner, whether it opens a fix session, how often it may repeat, whether a
brand-new user is spared it, and whether it digests. Those decisions used to live in four other
modules that a contributor had to know about.

A key the app can raise with no row **fails the build**, which is what makes the registry the place
to look rather than one more list that drifts. Run `npm run alert-registry:reindex` to seed a row
for a new key; hand-written titles survive every later regeneration.

#### Picking the audience — the four levers, and the two ways to get it wrong

Ask **who can act on this card**, not who finds it interesting. Four levers exist and the registry
contract (`alert-type-registry-contract.md`, "Who a type reaches") is the source of truth:

- **`operatorOnly`** — about a SHARED system (the cloud fleet, master build/CI, a deploy). Delivered
  only on the one operator machine.
- **`developerOnly`** — about the reader's OWN checkout. Dropped on an installed build, delivered from
  source, so every developer keeps their own.
- **`defaultMuted`** — the app's own internals, with no reader action at all. Ships switched off; the
  user can turn it on.
- **the first-day hold** (`new-user-alert-hold.ts`) — a good card at the wrong moment.

Two mistakes to avoid, both silent:

- **Never set `developerOnly` and `defaultMuted` on one row.** They read alike and behave as
  opposites — restrict-to-developers versus show-nobody — and the combination simply stops reaching
  the developers it exists for.
- **A held key whose producer appends a suffix belongs in the PREFIX array, not the exact one.** An
  exact entry for `some-key:` matches nothing and holds nobody while looking identical to a working
  hold. Read the producer and match its own separator.

### How agents create alerts

```bash
# Minimal text alert
curl -X POST http://127.0.0.1:19519/alert \
  -H "Authorization: Bearer $AMC_CLI_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"title":"Build failed","contentType":"text","text":"3 tests broke in feature/auth"}'

# Alert scoped to a real project (appears in that project's Needs You section)
curl -X POST http://127.0.0.1:19519/alert \
  -H "Authorization: Bearer $AMC_CLI_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"title":"PR ready to merge","contentType":"text","text":"...","projectId":"<uuid>"}'
```

On success the server returns `{ ok: true, data: { id, created, suppressed } }` with the status code keyed to what happened: **`201 Created`** for a freshly-inserted alert, or **`200 OK`** when a `dedupKey` coalesced into an existing row (`created: false`). `suppressed: true` means the re-raise cooldown (below) ate the alert — the returned `id` is a dismissed card, not an active row. So key success on `ok: true` (or `status < 300`), not on `status === 200`. A `ALERT_CREATED` push fires; the renderer reconciles its inbox slice.

### Special characters — the intake recovers them, but post UTF-8-safely

`title` / `text` are markdown and accept any Unicode — `×`, em-dashes, curly quotes, bullets, emoji. Two chokepoint backstops keep a dirty body from garbling the card: an invalid-UTF-8 body on `/alert` is **recovered** as Windows-1252 (so a `×` sent as the single byte `0xD7` lands as a real `×`, not `�`), and stray control characters are stripped (so a mangled `\v` / `\r` can't render as an invisible box). Genuine double-encoding (`â€"`→`—`) is repaired too. See [inbox-alert-contract.md](../../.claude/memory/contracts/inbox-alert-contract.md) I2.

**Still, post the body UTF-8-safely — especially on Windows.** A shell that builds the JSON **inline** (`curl -d "{…}"`, PowerShell `Invoke-RestMethod -Body "{…}"`) mangles non-ASCII punctuation and interprets `\n` / `\r` / `\v` escapes inside your text (this caused the 2026-08-15 cloud-audit alert to render `×` as `�` and a word with a box). Write the body to a UTF-8 **file** and post the file so the exact bytes travel untouched:

```bash
# body.json written as UTF-8 by your editor / file-write tool — NOT an inline shell string
curl -X POST http://127.0.0.1:19519/alert \
  -H "Authorization: Bearer $AMC_CLI_TOKEN" -H "Content-Type: application/json" \
  --data-binary @body.json
```

### App-generated alerts with custom actions

Most alerts come from agents via `POST /alert`, but the **main process also raises a few internally** (at startup, or in response to an app event). These reuse the same `createAlert` chokepoint + dedup, and many carry their **own** feature-specific action keyed on their `dedupKey` — shown **alongside** the generic "Start session" button (which appears on every agent alert except the two the guard sanctions — a team-chat notice, and a card whose bottom the reply box occupies — additively), never in place of it:

> **Every alert that recommends an action now offers a one-click way to take it — and this is build-enforced.** An alert that points you somewhere — _"your backup isn't finished"_, _"reconnect Telegram"_, _"your mobile access is acting up"_ — declares **where it points** as data in a small per-subsystem list ([`alert-actions/`](../../src/shared/alert-actions/)), and the alert screen shows a single button that takes you straight there — **"Open X settings"** with the exact control scrolled into view and highlighted, **"Open claude.ai"** for an external page, or a button that opens the **panel the card names** (the Browser panel, Mission Control, the Dev Pipeline panel’s worktree list). You never have to hunt for it yourself, and the button goes where the copy actually points: a card telling you to approve an automation opens the panel where you can approve it, not the settings page for that feature. A lint guard (`alert-directive-needs-action`) **fails the build** if any alert's text tells you to go do something but gives you no button to do it — and as of 2026-09-02 it also catches copy that points at an **in-app panel or tab** ("open the Browser panel", "open the Automations panel"), not just copy that says to open Settings or run a command. Widening it that far surfaced four more dead-end cards that had been shipping with no button at all; each now opens the place its copy names. The only way past the guard is to add the button, or to mark the alert as a deliberate exception with a reason (a developer-only "run this command" notice, a "sole fix is a restart" advisory, or a "this clears itself" notice). So a new _"go to Settings yourself"_ card with no button can't ship by accident. The handful of long-standing custom-action cards below predate the registry and keep their bespoke buttons (they're recognised as already-actioned):

- **KMS Quick Reference available** (`dedupKey: kms-wizard-available`) -- raised once at startup for users who already had KMS enabled before the Quick Reference Wizard was added. It says "Your knowledge base just got a guide" and shows an **Open Setup Wizards** button that navigates to **Settings > Setup Wizards** with the KMS Quick Reference card scrolled into view. One-shot via the `kmsWizardAlertSeen` system setting; gated behind `nothariEnabled` > `!nothariQuickReferenceCompleted` (users who've already completed the wizard never see it). Uses a generic registry action (the `kms` domain module), not the legacy predicate carve-out. See [kms-quick-reference-wizard.md](kms-quick-reference-wizard.md) and [kms-wizard-contract.md](../../.claude/memory/contracts/kms-wizard-contract.md).
- **Low-spec hardware warning** (`dedupKey: low-spec-warning`) — raised once when the computer is below recommended specs (under ~14 GB RAM or ≤4 CPU cores). It shows the machine's actual specs, a 16 GB recommendation, and one **Turn on Lite mode** button that switches on the whole Lite mode bundle in one click, confirms it, and archives the card (a failed save leaves the card in place). Its "what helps" list deliberately does **not** suggest running fewer sessions — `count-is-never-the-cause` / UX2 name that as a violation, and the answer to a machine that cannot keep up is Omniscio doing less, not the user (locked by `slowdown-copy-never-blames-session-count.test.ts`). One-shot via the `lowSpecWarningSeen` setting, so it never re-nags after dismissal; the 14 GB trigger sits below the 16 GB recommendation so a true 16 GB machine (which reports less after firmware reservation) is never falsely warned.
- **Low-memory warning** ("Your computer is running low on memory") — raised when this computer is _running low on memory right now_, from **either of two distinct causes**, each its own card with its own independent 24h cap:
  - **Commit-charge pressure** (`dedupKey: memory-commit-pressure-high`) — sustained commit charge ≥ 1.25× physical RAM **and** physical RAM itself genuinely filling (≥ 75% used): the Windows swap-thrash window, where the OS is forced to spill pages to the pagefile. The physical-fill check (added 2026-07-18) stops a big-RAM machine with a large _idle_ fleet — where sessions have _reserved_ far more memory than they're actually using, so commit sits above 1.25× at rest with tens of GB still free — from raising a false "low memory" card; a genuine swap-thrash still fires it. The card's text leads with how full physical RAM actually is.
  - **Physical-RAM exhaustion** (`dedupKey: memory-physical-pressure-high`) — physical RAM sustained **≥ 92% used** (3 consecutive samples) _while_ commit charge is still healthy: the file-system-cache / metafile buildup the commit signal is blind to (e.g. a 64 GB box pinned near 95% physical while commit sits at ~29%). Because the OS reports reclaimable cache as "free", this only trips on genuinely _pinned_ memory, never on healthy cache. It fires **only when commit pressure is NOT high** (`!isCommitPressureHigh`), so the two cards cover distinct failure modes and never double-fire on one storm.

  Both are detected only while Omniscio is actively spawning/recovering sessions (the sampler's poll points — which it re-checks while pacing under pressure, exactly when it matters). The low-memory warning **cannot be turned off**. Both keys are declared non-mutable in the alert type registry — the machine can crash or lose work to an out-of-memory condition, which is a named safety consequence rather than a preference — so the card shows no mute button and the Alert types list shows a disabled switch with that reason. Its `lowMemoryAlertEnabled` setting was retired for the same reason: two controls disagreeing about one alert is worse than either. The card keeps its "Start session" button and its real action (Settings → Performance). Each card is hard-capped at **at most one per 24 hours** (independently, per dedup key) — the cap 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) (I15 commit-pressure card, I16 physical-RAM sibling card).

- **Recurring-crash notice** ("Omniscio has closed unexpectedly a few times recently", `dedupKey: recurring-crash-nudge`) — raised once at startup when Omniscio has **reopened after closing unexpectedly several times** within a rolling ~2-week window (a rolling ledger in `gpu-stability.json` that survives clean restarts), _and_ the 3-consecutive-crash GPU auto-disable hasn't already stepped in — so an **intermittent** crasher (that never trips the consecutive-crash safety net) still gets surfaced instead of silently forgotten between clean runs. Its copy is deliberately **cause-agnostic**: repeated unexpected closes can be graphics drivers, low memory, or another app, so it points to **Settings → Diagnostics** (turn off GPU acceleration / Restart in Safe Mode) and to sending a **bug report** (which now carries the crash diagnostics needed to pinpoint the cause) — it never tells you to "turn off your GPU", because forcing software rendering on an unconfirmed cause can make a non-graphics crash _worse_. Unlike the cards above it keeps the **standard "Start session" button** (a session about the crashes can genuinely help — an agent can run the read-only health scan) and adds no custom action. A `crashNudgeShown` flag stops it re-nagging every boot; it re-arms only once the crash window fully clears. See [render-safe-mode-contract.md](../../.claude/memory/contracts/render-safe-mode-contract.md) (`bug-reports-capture-graphics-mode-and-crash-forensics`) and [gpu-crash-resilience-postmortem.md](../../.claude/memory/postmortems/gpu-crash-resilience-postmortem.md).
- **Stuck-install cleanup notice** ("Omniscio cleaned up stuck dependency installs", `dedupKey: install-orphan-reaper-reaped`) — raised by the [Install Orphan Reaper](../../.claude/memory/contracts/install-orphan-reaper-contract.md) after it kills wedged, orphaned `pnpm`/`npm install` processes that a stopped/restarted session left running (they otherwise wedge on the shared store and saturate the disk). Like the low-memory warning it **keeps "Start session" and ADDS a "Turn off these notices" button** (sets `installReaperAlertEnabled = false`), with a re-enable toggle at **Settings → Notifications → Stuck-install cleanup notices**. Crucially the switch gates **only the notice** — the reaper keeps cleaning up regardless (the notice runs _after_ the kill and reads no settings). A large or recurring number of these means sessions are being stopped/restarted mid-install a lot while the machine is busy. See [install-orphan-reaper-contract.md](../../.claude/memory/contracts/install-orphan-reaper-contract.md) (`the-notice-is-gated-never-the-reap`).
- **Plugin storage failure** (`dedupKey: plugin-storage-fail:<pluginId>`) — raised when a plugin's local storage couldn't be set up, so the plugin may not be saving your data (the reported Stride time-tracker symptom: clock-ins **and** backlog entries both silently vanished after an update). A plugin's storage-init is deliberately swallowed at startup so one bad plugin can't crash the app — but that left the plugin with no tables, and a plugin that swallows its own read errors then just renders empty, invisibly. Omniscio now **self-heals** it first: on the plugin's next save/read it re-provisions the missing tables and retries (idempotent — it only ever ADDS an absent table, never touches existing data), so the plugin recovers with no restart. Only when it genuinely **can't** provision does this **informational** notice appear — non-directive copy ("it's retried automatically, and this notice clears itself once it recovers"), so it carries no custom button (it's a sanctioned "self-clears" exception to the directive-needs-action guard), just the universal "Start session". It **self-clears** once the plugin's storage works again. The startup skip and the self-heal are both counted in telemetry (`plugin_storage_init_failed` / `plugin_storage_self_healed`). See [plugin-collection-reserved-columns-contract.md](../../.claude/memory/contracts/plugin-collection-reserved-columns-contract.md) (I3/I4/I5).
- **Slow cloud service** (`dedupKey: telemetry-slow-dependency`) — raised when one of Omniscio's cloud services has been timing out for a sustained ~20 min **and** it's a service you'd actually notice. It **names the specific slow service and what it's delaying** — e.g. "Omniscio's cloud service is running slow" (publishing to Shares and agent email may be delayed), or a distinct service like text messaging — resolved from the live per-host timeout signal, **never a hardcoded guess**. If the slow thing is purely background (or can't be identified), **no card is raised at all**. Directive-free and **self-clears** once responses return to normal (a sanctioned "this clears itself" exception to the directive-needs-action guard), so it carries no custom button — just the universal "Start session". See [http-host-damper-contract.md](../../.claude/memory/contracts/http-host-damper-contract.md) (INV-8).
- **Screen-grab block notice** ("An agent tried to grab your screen", `dedupKey: agent-foreground-blocked:<sessionId>`) — raised by the [agent-foreground guard](../../.claude/memory/contracts/agent-foreground-guard-contract.md) when it **blocks** an agent from pulling Omniscio to the front while you're working in another app (per session, so a looping agent bumps one row instead of stacking). Like the stuck-install notice it **keeps "Start session" and ADDS a "Turn off these notices" button** (sets `agentForegroundAlertEnabled = false`), with a re-enable toggle at **Settings → Notifications → Screen-grab block notices**. Crucially the switch gates **only the notice** — the block still happens regardless, so muting the card never lets an agent grab your screen. That makes it distinct from **Settings → CLI Control → "Let agents bring Omniscio to the front"**, which changes the _behavior_ (lets agents through); this mute only changes whether you're told. See [agent-foreground-guard-contract.md](../../.claude/memory/contracts/agent-foreground-guard-contract.md) (I7).
- **Stuck-task helper** (`dedupKey: stuck-task:*`) — the "Want a hand getting unstuck?" nudge, raised after you snooze the same session / email / PR 3+ times (in-development / Lab-gated). It shows a **Talk it through** button (starts a short get-unstuck coaching session) alongside "Start session", and — additively — a **Turn off these nudges** button: it opens a confirm and, on OK, disables the whole stuck-task-helper feature and clears the card, pointing you to **Settings → Lab** to turn it back on. See [inbox-alert-contract.md](../../.claude/memory/contracts/inbox-alert-contract.md) (I8).
- **Sync-drift catch-up** (`dedupKey: sync-drift`) — run-from-source installs only: the sync-freshness watchdog (a 6h tick) raises this when the local checkout has drifted too far behind `origin/master` or is wedged mid-update, and **straight away** when the copy has *diverged* from the remote (it has its own commits the remote does not AND is missing some of the remote's). A divergence raises without waiting out the staleness clock because nothing repairs it unattended — and because it stops the cloud fleet's worker-code deploy, which refuses to ship a checkout whose worker tree is behind the remote's. It shows a **Safe catch-up** button — which fast-forwards when only behind, otherwise backs your work up to a recovery branch first and _then_ resets (never a bare `reset --hard`), and — when the copy carries commits the remote does not, where a reset would throw them away — declines the reset and **merges** the remote in instead, keeping both sides — alongside "Start session", and auto-clears once the copy is current. The catch-up runs **in the background**: pressing it says so and hands you straight back to your inbox on a phone (the card is a notice about the checkout, not a screen to sit on while a fetch runs — the run is 37s in a typical case and is bounded at 10 minutes, and a diverged trunk pays that budget twice), with the outcome arriving as a toast, and only one catch-up ever runs at a time. The card is never dismissed by that press: it clears only when the copy is actually current, because the watchdog ticks every 6 hours and a failed catch-up must stay visible. No-op on a packaged build. Even when the watchdog **can't reach GitHub**, it still raises this if the last-known `origin/master` already shows the copy is behind and stale — it never goes silent just because a fetch failed. A separate last line of defense, the **predev launch guard** (`scripts/ensure-safe-git-state.mjs`), refuses to start `npm run dev` into a half-merged / conflicted tree (pointing you at the same safe catch-up) and sets `pull.ff=only` so a bare `git pull` can never leave that broken state. See [safe-sync-master-contract.md](../../.claude/memory/contracts/safe-sync-master-contract.md).
- **Sync auto-recovered** (`dedupKey: sync-master-auto-recover`) — run-from-source installs only: raised by the 2-hourly Sync Master job when its rebase onto `origin/master` conflicted and it healed itself by backing your local master up to an `amc-backup/pre-recover-<ts>` branch and resetting to the remote. **Nothing is deleted** — the body is written for the product owner, not a developer: it opens on "Nothing was deleted", collapses identical subjects into one line with a `(xN)` count, drops the `type(scope):` prefix off each subject, and sorts them into **Already in the team's copy** vs **Only on your machine**. A change matched to exactly one upstream commit carries a `[see the change]` link to its GitHub commit page (derived from `git remote get-url origin`; absent when there is no GitHub remote). Every sha still appears in full below a `**Technical detail**` divider, along with the backup branch and the exact one-line undo (`git reset --hard amc-backup/pre-recover-<ts>`) — that is how grouping keeps the contract's "names every parked commit" promise. The old `NOT provably upstream` verdict is **retired**: technically true, but it read to the card's only reader as "your work is gone" (owner report, 2026-08-24). It fires on **every** parking, because no cheap test reliably separates "a duplicate of work already on the remote" from "genuinely unpushed work" — but the card now consults two signals rather than one. Patch-id alone is near-useless here: on the 2026-08-24 parking it marked all 14 commits unproven, while an exact-subject lookup against the last 4000 `origin/master` commits matched 7 of the 8 distinct subjects. A subject seen exactly once upstream is a linkable match; one seen many times is a recurring automated message and is labelled a routine repeated change with no link. Both signals are **information, never a filter**. It is deliberately loud when the parked list could not be read at all, since that is the case most likely to hide real lost work; it stays silent when the catch-up was a plain fast-forward (nothing parked) or when the catch-up refused and changed nothing. One row per repeat jam (the dedup key coalesces), raised by `scripts/safe-sync-master.mjs` through the shared `POST /alert` client, so it carries the universal "Start session" button and no custom action. Distinct from the **Sync-drift catch-up** card above, which reports drift you still have to act on; this one reports an action already taken. See [safe-sync-master-contract.md](../../.claude/memory/contracts/safe-sync-master-contract.md) (`an-integration-conflict-self-heals-and-names-what-it-parked`).
- **Sync could not finish** (`dedupKey: sync-master-blocked`) — run-from-source installs only: raised by `scripts/safe-sync-master.mjs` when the launch-triggered catch-up can't complete. **One dedup key covers three distinct outcomes**, so a repeat jam updates the same row instead of stacking a card per launch, and every wording opens on "Nothing was changed and nothing was lost": (a) **a genuine merge conflict** — origin and your local master both changed the same files, so a human has to decide which version wins; the card names them, capped at 10 with an explicit "…and N more" (never a silent truncation); (b) **the working-tree carry declined** — your branch moved forward but the files on disk were left exactly as they are because one of them holds an uncommitted edit, an atomic refusal rather than a clobber; (c) since 2026-08-27, **a pre-flight skip that is costing you commits** — the sync refused to start because the tree is dirty AND that refusal has already left local master **≥50 commits behind** `origin/master`. Case (c) exists because the skip used to be silent, on the reasoning that a dirty tree is "self-clearing" — true of a developer mid-edit, false of the case that actually hurt: four never-committed regenerated catalogs skipped the sync on every single launch for 22 hours while the copy drifted 566 commits behind, with nothing ever reported. The gate is accumulated **drift**, not the skip itself, because drift that size can only build up across many refused runs — so someone legitimately mid-edit on a current checkout still hears nothing, and an unmeasurable drift never alarms. A failed reset of a generated file also stays silent **on purpose**: the card's advice ("commit or set those aside") is right for real dirt and useless for a file lock, and a card that misdirects is worse than the run output the script already writes. Carries the universal "Start session" button and no custom action. Distinct from the **Sync-drift catch-up** card above, which the 6-hourly watchdog raises about drift you still have to act on. See [safe-sync-master-contract.md](../../.claude/memory/contracts/safe-sync-master-contract.md) (`the-merge-path-moves-a-protected-branch-the-allowed-way`, `regenerated-artifacts-are-reset-never-refused-on`, `a-pre-flight-skip-costing-commits-is-never-silent`).
- **Workspace creation keeps failing** (`dedupKey: worktree-create-failing`) — raised when creating isolated agent workspaces (the git worktrees new sessions run in) has failed repeatedly over about 15 minutes, so **new coding sessions may not be starting properly**. It reaches **every** user regardless of the agent-alert toggle, because a session that never starts is not a preference. The card names the most recent problem and points at the diagnostic logs under **Settings → Diagnostics**.
- **Auto-lander keeps pausing its own git** (`dedupKey: auto-lander-git-ownership-outage`) — Windows keeps refusing the auto-lander a safe way to start and clean up the git commands it runs, so it pauses git for a moment and waits longer before each retry. **Landing keeps going, just slower**, and the card clears itself once the refusals stop — no restart needed. A recurring one is usually worth a look, so it keeps the universal "Start session" button.
- **Couldn't merge a session's work** (`dedupKey: worktree-merge-failed:<project>::<branch>`) — an isolated session's changes could not be merged back into your project. **Nothing is lost**: the work is preserved on its own branch, and the auto-lander retries on its own. One row per project and branch, so a repeated failure updates the same card instead of stacking one per attempt.
- **Test runs are being turned away** (`dedupKey: land-health:admission-shed`) — this computer is overloaded, so test and gate runs are turned away before they start rather than queued behind an unlimited backlog. **Work still gets checked, it just waits longer**, and it normally settles by itself once the load drops. The card says how many runs were turned away and why.
- **Gmail reconnect** (`dedupKey: gmail-automation-offline`, `gmail-bug-intake-reauth`, `gmail-summarizer-reauth`, `gmail-send-reauth`) — the Gmail pollers each raise a "reconnect your Google account" row when the shared Google sign-in dies (expired/revoked): the **automation poller** (`gmail-automation-offline`, which also stops email-triggered automations and new-email phone notifications), the **bug-report-intake poller** (`gmail-bug-intake-reauth`), and the **email label-summarizer poller** (`gmail-summarizer-reauth`). Because all three share one sign-in, every one of them shows the same one-click **Reconnect Gmail** button — it runs the Google sign-in, confirms it worked, then archives the row immediately — alongside "Start session". (Previously only the automation alert had the button; the other two were plain "reconnect in Settings" text — that was the bug this fixes.) The automation notice is raised **once per outage** and at most **once a day** while still down — never on every poll — so it can't spam the inbox or re-appear right after you dismiss it. Each notice clears on reconnect (or when its poller next reaches Gmail). A further producer is the **Supermail / amc-gmail send relay** (`gmail-send-reauth`): when a relayed Gmail **write** (sending a reply, modifying a message) can't authorize — a dead grant or a missing send permission — the same "can't send email — reconnect Google" card appears with the same one-click **Reconnect Gmail** button, cleared the instant a relayed send succeeds; an ordinary transient failure (a network blip, a rate-limit, a 5xx) never raises it, so "your reply couldn't send" becomes a persistent, actionable inbox surface instead of a silent failure. See [gmail-health-alert.ts](../../src/shared/alert-features/gmail-health-alert.ts).
- **GitHub reconnect** (`dedupKey: github-poll-failing`) — the GitHub notifications poller raises a "reconnect GitHub" row when its periodic poll fails because `gh` lost its sign-in or `notifications` scope (so notifications stop flowing). It shows a one-click **Reconnect GitHub** button — it reads `gh auth status`, then runs the same shared device-flow reconnect (one-time code + browser) the Settings **Grant Access** button uses, and archives the row on success — alongside "Start session". The device-code modal is mounted app-wide, so the code surfaces no matter which view you're on; the button is the user-initiated trigger the device flow requires (the adapter raises only a dismissible nudge and never auto-fires the flow itself). The notice clears on reconnect or when the poller next reaches GitHub. Mirrors the Gmail reconnect carve-out. See [github-health-alert.ts](../../src/shared/alert-features/github-health-alert.ts).
- **Account re-auth** (`dedupKey: account-reauth:<accountId>`) — raised when one of your accounts needs to be **signed in again for sure**: its login has gone genuinely dead (the saved sign-in was revoked or can no longer be renewed), so any sessions on that account will fail until you reconnect. It is **per account** — each dead login gets its own row, named after that account's email ("Sign in to you@example.com again"). It shows a one-click **Sign in again** button — which re-runs the same Google-style Anthropic sign-in the Settings → Accounts **Log In** button uses, pre-selecting the right account so you don't have to pick it — alongside "Start session". Crucially it appears **only when the account is definitely dead**, never for a passing network blip, a rate-limit, or a token that Omniscio can quietly refresh on its own — those are handled silently and never raise this row. It clears itself the moment you successfully sign that account back in. See [account-reauth-alert.ts](../../src/shared/alert-features/account-reauth-alert.ts).
- **Team-chat new message** (`dedupKey: team-chat:<workspaceKind>:<workspaceId>:<channelId>`) — raised by the Team Chat desktop notify watcher **on message arrival** (not at startup) whenever a new message warrants surfacing: the SAME rule as the OS toast (DMs + @mentions + unmuted channels, never your own), so your per-channel mutes are inherited, not re-derived. It's created **before** the Focus-Mode toast gate, so the inbox row survives Do-Not-Disturb suppression (the inbox is the durable record). **One coalescing row per channel** — its preview refreshes as messages arrive. Selecting it **embeds the REAL Team Chat conversation** in the detail pane — the SAME chat surface the Team Chat view uses (message bubbles/backgrounds, sender grouping, reactions, threads, edit/delete), so you read AND reply inline; a resolved reply clears the notice. The header is a **compact identity row** — the sender's avatar + name, with **no "Updated" timestamp** (redundant on a live conversation, where every message already shows its own time). An **Open channel** button (jumps to the full Team Chat panel — where the peripheral extras save-a-message, schedule-a-reply and custom emoji live) sits in the **header's top-right on desktop** (in the footer quiet row on a phone). A chat message is a conversation, not an actionable alert, so a team-chat notice is the **one alert with no "Start session" button** (I8). Unlike the self-clearing maintenance notices it **stays until you dismiss it**; after archive, the next message raises a fresh row (never resurrected — I3). **Two sibling targeted notices share this surface** via DISJOINT dedup prefixes: a **thread-reply** row (`team-chat-reply:…`, "replied to your message", on by default) and — opt-in, **OFF by default** (`teamChatNotifyReactions`) — a **reaction** row (`team-chat-reaction:…`, "reacted 👍 to your message", **inbox-row only**, no OS toast) raised when someone reacts to a message you wrote; each is its own coalescing row, and all three clear when you read the channel. See [team-chat-inbox-alert.ts](../../src/shared/alert-features/team-chat-inbox-alert.ts), [inbox-alert-contract.md](../../.claude/memory/contracts/inbox-alert-contract.md) (I8), [team-chat-reactions-contract.md](../../.claude/memory/contracts/team-chat-reactions-contract.md) (the reaction notice), and [team-chat-desktop-contract.md](../../.claude/memory/contracts/team-chat-desktop-contract.md) (D16, the embedded-panel mechanism).

These feature-specific buttons live in `AlertInboxViewer` and now render **alongside** the universal "Start session" button (I8) — Start session is offered on every alert **except a team-chat notice** (a conversation, not an actionable alert) and a card that carries the reply box, so none of them replace it. Several of these cards ADD a **mute** button as well: the low-memory warning, the stuck-install cleanup notice, the screen-grab block notice, and the stuck-task offer (its **Turn off these nudges** confirm). All these mutes render as a quiet grey button in the **standardized bottom action bar** — the same bar that holds Start session and the alert's own action — pushed to the far **left** (`mr-auto`) so the opt-out never out-shouts the actions beside it. Every inbox card puts its action buttons in that one bottom bar (`INBOX_DETAIL_FOOTER_BAR`); only **Archive** stays in the top-right corner of the header. See [inbox-alert-contract.md](../../.claude/memory/contracts/inbox-alert-contract.md) (I8/I15/I18).

**Brand-new users don't get the startup "nag" cards on day one.** A defined class of these startup/environment alerts — the **dead-hotkey** card, the **Ctrl+Space / IME** warning, the **wrong-clock** warning, the **no-keyring** warning, the **below-recommended-specs** card, the **KMS-wizard** nudge, and the **"didn't shut down cleanly"** notice — is **held** while the app is still inside the user's first day of use: dropped at the same `createAlert` chokepoint the operator-only alerts use, through onboarding and then until 24h after Setup completes, so a brand-new user isn't greeted by a pile of environment nags before they've even used the app. The cards appear normally once the first day passes; a legacy user with no setup timestamp is never held, the gate **fails open**, and an `AMC_DISABLE_NEW_USER_ALERT_HOLD` env kill switch turns it off. **"Still in onboarding" is not taken at face value on an install that has clearly been used before.** An install where onboarding was never recorded as finished — which on a pre-Setup-v2 copy can never flip, because the setup timestamp is the only thing that would set it — used to be held FOREVER rather than for a day, with the legacy escape hatch sitting behind the very flag that trapped it (measured 2026-09-10: a perf card raised 17 times in one hour into an empty inbox, because a dropped alert writes no row and so never spends the once-a-day cap that would have quieted the retry). The gate now also asks whether the user has **ever archived a session** — deliberately a stronger signal than the "has a session / has an account" test the first-run inbox uses, because onboarding itself creates active demo sessions and connects an account, so those would trip mid-setup for someone genuinely brand-new; an archived session is the one thing a fresh onboarding cannot fabricate. The self-healing cards re-raise on their own after the window; the two one-shot cards (low-spec, KMS-wizard) retry because they mark "seen" only when actually shown. See [inbox-alert-contract.md](../../.claude/memory/contracts/inbox-alert-contract.md) (the new-user first-day hold) and [new-user-alert-hold.ts](../../src/shared/alert-features/new-user-alert-hold.ts).

**The held class has grown well past those seven — it now also covers the developer and agent machinery.** Successive day-1 exposure sweeps (2026-09-04, 2026-09-07, 2026-09-09) added the self-diagnostics (repeated freeze, blank boot, low memory, sweeper-restarted) and then the cards about worktrees, branches, the auto-lander, gate attempts, agent lanes, orphaned helper processes and PR bodies — **123 exact keys plus 39 prefix families** today, and the counts are pinned by a test so growing the class stays a deliberate act. The 2026-09-27 sweep added 22 more and, for the first time, swept the whole day-1 surface rather than a hand-picked list — every one of the 411 alert types that carried no first-day gate was read producer by producer. Two things that sweep settled: a key whose producer appends a per-instance or per-period suffix goes in the PREFIX array even when it does not end in a colon (`release-notes-v` separates on the version's `v`, `coaching-bridge-weekly-` on a week stamp), because an exact entry for one of those matches nothing and holds nobody while looking identical to a working hold; and four producers whose latch is one-shot are gated at the producer instead (see below), which is why the sweep's 44 candidates landed as 22 holds rather than 44. Each is **held rather than made operator-only** on purpose: operator-only drops an alert on every box except the one designated fleet-operator machine, which would stop a developer being told their OWN machine is broken, whereas a brand-new user has no worktrees, branches or agent lanes for any of them to be true about. **Two things the class does NOT do:** it never holds an alert that phone-pages the owner (the gate drops before the page, so that would buzz a phone about a card not in the inbox), and it never holds a one-shot card that persists a "seen" flag at its producer — those are gated at the producer instead, or the tip would be burned and lost forever. **What is deliberately NOT held** is just as load-bearing: the **"your sign-in can no longer verify itself"** notice reaches a day-1 user on purpose, because it is the only warning given when a paid tier is quietly ageing out to free — gating it would make a fix for a silent failure fail silently. See [alert-catalog.md](../../.claude/memory/alert-catalog.md) for the full day-1 column.

## Related

What an alert looks like and how you answer one is the [Inbox alerts](inbox-alerts.md) page, and what happens over an alert's life is [part 3](inbox-alerts-part-3.md). How agents reach Omniscio in general is on the [CLI control server](cli-control.md) page.
