Inbox alerts (how an agent gets your attention) (part 2)
Part 2 of the Inbox alerts page: how an alert is actually authored — adding a new alert type, the route an agent posts one through, the text handling it has to survive, and the app-generated cards that arrive with their own custom actions attached.
What it is
This is part 2 of the Inbox alerts 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 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
developerOnlyanddefaultMutedon 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
# 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 \uFFFD), 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 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 \uFFFD and a word with a box). Write the body to a UTF-8 file and post the file so the exact bytes travel untouched:
# 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/), 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 thekmsWizardAlertSeensystem setting; gated behindnothariEnabled>!nothariQuickReferenceCompleted(users who've already completed the wizard never see it). Uses a generic registry action (thekmsdomain module), not the legacy predicate carve-out. See kms-quick-reference-wizard.md and 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 byslowdown-copy-never-blames-session-count.test.ts). One-shot via thelowSpecWarningSeensetting, 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
lowMemoryAlertEnabledsetting 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 (I15 commit-pressure card, I16 physical-RAM sibling card).- Commit-charge pressure (
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 ingpu-stability.jsonthat 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. AcrashNudgeShownflag stops it re-nagging every boot; it re-arms only once the crash window fully clears. See render-safe-mode-contract.md (bug-reports-capture-graphics-mode-and-crash-forensics) and 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 after it kills wedged, orphanedpnpm/npm installprocesses 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 (setsinstallReaperAlertEnabled = 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 (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 (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 (INV-8).Screen-grab block notice ("An agent tried to grab your screen",
dedupKey: agent-foreground-blocked:<sessionId>) — raised by the agent-foreground guard 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 (setsagentForegroundAlertEnabled = 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 (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 (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 behindorigin/masteror 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 barereset --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-knownorigin/masteralready 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 startnpm run devinto a half-merged / conflicted tree (pointing you at the same safe catch-up) and setspull.ff=onlyso a baregit pullcan never leave that broken state. See 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 ontoorigin/masterconflicted and it healed itself by backing your local master up to anamc-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 thetype(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 fromgit 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 oldNOT provably upstreamverdict 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 4000origin/mastercommits 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 byscripts/safe-sync-master.mjsthrough the sharedPOST /alertclient, 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 (an-integration-conflict-self-heals-and-names-what-it-parked).Sync could not finish (
dedupKey: sync-master-blocked) — run-from-source installs only: raised byscripts/safe-sync-master.mjswhen 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 behindorigin/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 (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.GitHub reconnect (
dedupKey: github-poll-failing) — the GitHub notifications poller raises a "reconnect GitHub" row when its periodic poll fails becauseghlost its sign-in ornotificationsscope (so notifications stop flowing). It shows a one-click Reconnect GitHub button — it readsgh 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.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.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, inbox-alert-contract.md (I8), team-chat-reactions-contract.md (the reaction notice), and 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 (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 (the new-user first-day hold) and 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 for the full day-1 column.
Related
What an alert looks like and how you answer one is the Inbox alerts page, and what happens over an alert's life is part 3. How agents reach Omniscio in general is on the CLI control server page.
Last verified 2026-09-30