---
title: Briefings (AI briefing of what happened)
---

# Briefings (AI briefing of what happened)

> Shipped display name is **Briefings** (renamed from "Daily Digest" 2026-09-01 — it covers the weekly review too). The persisted sentinel `__daily_digest__`, the `dailyDigestEnabled` flag, the integration id `daily-digest`, and this file name all stay byte-stable.

## What it is

Daily Digest is a once-a-day AI-written briefing of everything that happened across your Omniscio sessions, Gmail, Calendar, SMS, Slack, Telegram, RSS, and GitHub — generated by Claude Haiku, stored as a first-class sidebar entry under **Briefings**, and **conversational**: you can ask follow-up questions in a chat panel that runs against the same summary. Good for catching up after a day off, a trip, or just a long weekend. Unread digests carry a count badge on the sidebar entry so you can tell when the overnight summary is waiting. The digest is also customizable — you can dismiss alerts you've handled, permanently suppress alert topics you don't want to see again, hide whole sections you don't care about, and exclude specific projects from being summarized at all. Keyboard hotkeys triage the alerts list at the speed of typing.

**Default-on** (owner decision 2026-08-29, launch-funnel fix): a fresh install generates the briefing without opting in, because the digest is the app's one recurring "here's what happened" reason to come back on day 2 and it previously shipped off. The flip reaches **new installs only** — an existing user's stored choice always wins, because a fresh install persists every default to disk and config load merges stored-over-default, so anyone who already had the digest off (explicitly or by never touching it) stays off. There is deliberately **no force-enable migration** here, unlike Weekly Summary's one-time `weeklySummaryDefaultOnMigrationDone` flip. Turn it off any time below; once off, it stays off.

Default-on cannot quietly bill the company. Generation stops at a **tier-aware credential gate** before any model call: a free (or signed-out / unresolved) user must bring their **own** key — the embedded key and the signed-in company gateway are paid-plan benefits — and an unresolved plan fails safe to free. With no usable credential the run raises the deduped "Couldn't generate your daily digest" card and returns, spending nothing. Details in [daily-digest-credential-contract.md](/.claude/memory/contracts/daily-digest-credential-contract.md).

The **Briefings** sidebar entry also hosts [Weekly Summary](weekly-summary.md) rows (as of 2026-05-14) — the Monday-morning ISO-week recap with the same stats-card + AI-written-paragraphs + suggestion-cards shape. Both cadences share the same Active/Snoozed/Archived buckets, the same context menu, and the same triage hotkeys. Weekly Summary has independent enable/schedule/cap controls — see its sub-section at the bottom of the Daily Digest settings panel.

## Where to find it

**Briefings** is a row in the left projects sidebar, sitting with your projects and the other built-in category rows rather than inside a panel — it is always there, so nothing has to be open for it to appear. Click it to open the date list beside the briefing viewer. The controls for it live in **Settings → Email & Summaries → Daily digest** (expand the card): the master toggle, the generation hour, the source toggles, and the **Customize digest** card that holds the section visibility, project exclusions and suppressed-alert list. The **Weekly Summary** controls sit at the bottom of that same settings panel.

## How it behaves

### How to use it

1. **It's already on.** Default-on for new installs since 2026-08-29 (see _Default-on_ below) — a fresh install gets the morning briefing without opting in. Settings → **Email & Summaries** → **Daily digest** (expand the card) to tune it, or flip the master toggle off. Pick a generation hour (0–23, defaults to 5 am local — the digest for yesterday lands in your inbox before you check email). Pick which sources contribute — toggles for Sessions, Gmail, Calendar, SMS, Slack, Telegram, RSS, GitHub. **Gmail and Calendar are opt-in** (2026-09-23, for Google's OAuth verification — their content is sent to the digest's AI model): a new install leaves both off, and even when their toggle is on the digest reads Gmail only while the Gmail integration (`gmailEnabled`) is switched on and Calendar only while the Calendar integration (`calendarEnabled`) is — the gate lives in `digest-live-fetchers.ts`, which both the main gather and the look-ahead use. Scroll to the bottom of the same panel for the **Weekly Summary** sub-section — independent toggle, independent schedule hour (default Monday 7 AM local), and an independent daily Haiku spend cap (default $0.10).
2. **Let it run.** A periodic tick (every 15 minutes) checks the clock; once your chosen hour has passed and today's digest hasn't been generated yet, it runs. First launch: Omniscio generates up to 7 days of backfill digests in one shot so you don't start with an empty list.
3. **Read it.** Click **Briefings** in the sidebar. The left pane lists digests by date — unread rows show a small **amber** dot to the left of the date (matching the amber "needs you" cue used elsewhere in the app — section header, unified inbox row, attention status). Clicking opens the digest viewer: structured sections for sessions / mail / calendar / other, with specific counts and callouts for things that probably need your attention. The stats row near the top renders four pills — **N sessions**, **N projects**, **$N total cost**, and a **tokens** chip showing the day's combined input + output token count (formatted compactly — `247` under 1k, `34k` for thousands, `1.5M` for millions, `1.8B` for billions). That count includes every token the models processed, and on a coding day almost all of it is agents re-reading their own cached conversation on each step (98.7% on one measured heavy day), so hover the tokens chip for the breakdown — new input, cached for reuse, re-read from cache, and written by agents (briefings generated before that breakdown was recorded show only the input vs. output split) — and hover the **total cost** pill for the split between what your subscription plans covered and what was billed straight to your own API keys — the total is what the day's usage would cost at pay-as-you-go API rates, so only the API-key part is money you actually paid (briefings generated before that subset was recorded show the same explanation without the two figures). Below those volume pills sits an **activity** row — three chips showing how you (vs. automation) handled the day: **N answered** (genuine replies you sent agents), **N archived** (the sessions **and** inbox items you archived), and **N automated** (auto-replies + automation rules that acted for you, plus the sessions automation self-archived and inbox items it auto-cleared) — each chip is shown only when it actually happened that day, with a hover tooltip explaining it. Beneath the chips a **Human vs. automated** line totals your side (answered + everything you archived) against automation's and shows the automated share as a percentage — it counts session archiving, which dominates a heavy day, honestly on both sides instead of as an inbox-only sliver (an earlier version counted only inbox archives, which badly understated both sides). The viewer's bottom bar carries two actions — **Share** (export the briefing) and **Start session**: the same shared "Start a new session" button the alert / drip / scheduled-message cards use (keyboard **N**), it opens a launch dialog pre-filled with the briefing summary so you can spin up a Claude session to act on the day's briefing in the repo you pick (this replaced the older blank-session project picker).
4. **Ask follow-ups.** Scroll to the bottom of the digest viewer — there's a chat input. Type "who asked me about the deploy?" or "what did I miss on the infrastructure project?" and Haiku answers from the digest content. The Q&A thread persists inside that digest.
5. **Generate or archive manually.** The **Generate now** button in the sidebar forces a digest for yesterday (useful if you changed sources or want a re-run). Generation routes through `llmProviderService.chat()` pinned to `provider: 'openrouter'`, so the embedded OpenRouter key handles the call by default — meaning the digest works for users who have never configured their own Anthropic API key, and keeps working when the user's Anthropic key is invalid or revoked. The user's Anthropic key (if any) is still passed as the circuit-breaker fallback for the rare case where OpenRouter is unavailable. If the **whole** chain fails — cheap provider down AND no working Anthropic fallback — a red toast surfaces the error message; on a 401-style key error the toast carries an **Open Settings** action that deep-links to **Settings → Accounts** so you can paste a fresh key. When generation produces nothing, the reason you are shown is the REAL one: the generator reports a named outcome, so a day with genuinely no activity says so, while no usable credential, the day's spend cap, a paused service, an in-flight run and an empty model answer each say what they are (before 2026-09-30 every one of them was reported as "No activity found", which sent anyone debugging it after a working query — Help Desk 28fd38bb). To archive a briefing you no longer want in the active list, use any of the four sidebar archive paths described in the next section — middle-click, right-click → Archive, the **E** keyboard shortcut, or the X button on mobile. Archived briefings move to a collapsed **Archived** section at the bottom of the sidebar; they are never hard-deleted and a single Undo toast (or re-archiving the same row from the Archived section) restores them. Archiving an unread briefing also instantly clears its row from the unified Inbox view (the inbox unread-count is recomputed to exclude archived rows, so the row vanishes without waiting for a full reload).
6. **Dismiss alerts you've handled.** Each alert row in the digest viewer's Alerts section has a small **×** button on the right. Click it and that alert disappears from the viewer immediately. Dismissals persist (they're stored per-digest), survive app restart, and are reversible — re-clicking the alert from another surface that exposes it brings it back. Useful for triaging the alert list down to just the things still on your plate.
7. **Dismiss every alert in one shot.** The Alerts section header has a **Dismiss all** chip. Clicking it marks every currently-visible alert in this digest as dismissed at once. The chip only appears while there's at least one un-dismissed alert. The chip's hint text reflects your live key bindings (e.g. "Press X dismiss · Shift+X suppress · Shift+D all" by default).
8. **Suppress an alert topic so it never re-appears.** Each alert row has a **⋯** overflow button next to the dismiss ×. Open the menu and pick **Dismiss + don't show again**. The alert disappears from the current digest AND its `{type, text}` is saved to a permanent suppression list — Haiku is told about that list when generating future digests, and a post-generation safety filter strips any alert whose text shares ≥ 1 distinctive token with a suppression. Useful for recurring noise you've decided is not actionable (e.g. "rate limit approaching" warnings on a project you don't manage). Suppressions are reversible from settings.
9. **Manage suppressed topics.** Settings → **Daily Digest** → **Customize digest** card has a **Suppressed alerts** subsection listing every suppression you've created, newest first, each with a small **Remove** button. Removing a suppression makes the alert eligible to re-surface in tomorrow's digest. The list collapses if you have no suppressions yet.
10. **Triage with the keyboard.** While viewing a digest, three hotkeys act on the topmost un-dismissed alert: **X** dismisses it, **Shift+X** dismisses + suppresses it (same as the overflow menu's "don't show again"), and **Shift+D** dismisses every visible alert in one stroke. The topmost target is highlighted with a brighter ring so you can see what your next keystroke will hit. Hotkeys only fire while the digest viewer is the active project — they don't collide with the same letters used in Gmail or diff-review. Bindings are user-customizable in Settings → **Keyboard Shortcuts** under the Daily Digest category.
11. **Hide sections you don't care about.** Settings → **Daily Digest** → **Customize digest** card. Eleven toggle rows let you hide entire sections of the digest viewer: greeting, stats, alerts, projects, communications, calendar, feeds, action items, feature ideas, suggestions, and today's schedule. Defaults are all-visible. Toggling a section off filters it out of the rendered viewer immediately — the section is still present in the underlying digest data, so toggling it back on is instant with no regeneration needed. If you hide every section, the viewer shows an empty state.
12. **Exclude projects from generation.** Same **Customize digest** card has a checkbox list of all your real projects (virtual projects and soft-deleted ones are not listed). Check a project to exclude it: tomorrow's digest will not gather any of its session activity, messages, or status, and previously-generated digests will hide its row from the projects-section breakdown at render time. Useful for projects that you want to keep around but don't need a daily summary for.
13. **Snooze a digest from the Inbox.** Today's digest also appears in the unified Inbox as a row under **DAILY DIGEST**. Right-click the row to open the same context menu used in the Briefings sidebar — **Archive**, **Snooze**, or **Unsnooze** — or press **S** while the digest row is the active inbox item to open the SnoozePalette. The same `setDigestSnoozedUntil` mutator drives both surfaces, so a snooze from the Inbox immediately moves the briefing into the **Snoozed** bucket of the Briefings sidebar, clears the row from the Inbox, and decrements the unread badge — with the same 5-second Undo toast.

### Sidebar sections — Active, Snoozed, Archived

The Briefings sidebar splits the date list into three buckets so old briefings don't crowd out the recent ones you actually want to revisit.

- **Active** sits flat at the top with no header — every unread or recently-generated briefing whose `digestDate` falls within the last 7 days. This is the only bucket that's always visible. Selection (arrow keys, click, the **E** shortcut) only walks rows in this bucket.
- **Snoozed (N)** appears as a collapsed header below Active when at least one briefing has a `snoozedUntil` timestamp in the future. Expand the header to see the rows; each shows a small "snoozed until …" hint. When the snooze time passes, the row re-buckets passively at the next render — there is no notification or push.
- **Archived (N)** is a collapsed header at the bottom, populated by anything you archived plus anything older than 7 days that was never archived (the 7-day cutoff folds naturally into archived). Expand to scroll the full history.

You can archive an active briefing five ways. The four sidebar paths below all advance selection to the next active row and pop a 5-second **Undo** toast (the toast shortcut hint reflects your live `undoLastAction` binding); the fifth is the open briefing's own header button:

1. **Middle-click** the row in the sidebar (mouse button 1 on `mousedown` — Chromium eats `auxclick` inside scrollable ancestors, so `auxclick` won't work; this is desktop-only).
2. **Right-click** the row and pick **Archive** from the context menu. The same menu also offers **Snooze** (opens the SnoozePalette modal) and, when right-clicking a snoozed row, **Unsnooze**.
3. Press **E** while a digest is selected and the dedicated Daily Digest sidebar has focus. The same keystroke also dispatches when the panel is open and no input is focused. **Ctrl+W** mirrors **E** for parity with the close-panel binding used elsewhere.
4. Tap the small **X** button that appears on the row at mobile widths (it's `md:hidden` — never shown on desktop).
5. Click the **Archive** button in the top-right of the open briefing itself (the shared `InboxArchiveButton`, beside Snooze). Once archived, that same button reads **Unarchive** and restores the briefing — which is also how you can tell the click landed while you are reading the briefing full-screen on mobile and cannot see the sidebar row move. Opened from the Inbox it additionally advances to the next inbox item; opened from the Briefings sidebar it simply archives. **This button marked the briefing read instead of archiving it until 2026-09-12** — and because opening a briefing already marks it read, it did nothing at all in the Briefings sidebar.

Snooze opens the same `SnoozePalette` used elsewhere in the app (sessions, emails) — pick a duration ("1 hour", "Until tomorrow morning", etc.) or a custom date/time. The briefing immediately moves into the Snoozed bucket with an Undo toast. There's no sound, no notification, and no "your snooze expired" badge — when `Date.now() > snoozedUntil`, the row simply re-appears in Active on the next sidebar render.

### How it works

**Gathering runs off the main thread (2026-07-13).** Building a digest scans a whole day of sessions, messages, and channel activity in the database. Those scans used to run synchronously on Omniscio's main window thread — and when Windows had pushed the database out of RAM under heavy disk load, they could freeze the entire window for minutes at a time, once per 15-minute retry. The scans now run in a dedicated short-lived database helper process (`amc-db-host-digest`, spawned per digest and always terminated afterward), so a slow disk can stall the digest — never your window. If the helper can't start, gathering falls back to the old in-process path; a mid-scan failure just fails that cycle and retries at the next 15-minute tick. Escape hatch: the `AMC_DISABLE_DIGEST_OFFTHREAD=1` environment variable forces the legacy path. (The same fix repaired a silent day-one bug: the "recent project history" context the AI uses for feature ideas had never actually loaded — digests generated after this change include it for the first time.) Behavior lock: [digest-gather-offthread-contract.md](/.claude/memory/contracts/digest-gather-offthread-contract.md).

**The prompt stays bounded on busy days (2026-07-16).** A single very active day can involve hundreds of sessions. The digest used to list _every_ one of them in the prompt it sends to Haiku — and once the "recent project history" fix above started actually loading, that block grew with your project count too. On the busiest days the request grew large enough that the model couldn't finish inside the call's time budget, so it timed out, and the automatic catch-up retried the _same_ oversized request every cycle — leaving those days permanently blank (this is what stranded 2026-07-12/13/14). Two caps fix it at the root: the prompt enumerates at most the **120 most-recent sessions** (the stats row still counts the true total, and a "showing the 120 most recent of N" note tells the model it saw a subset) and at most the **10 most-active projects** in the history block — mirroring the per-channel message caps every other source already had. The generation call also now uses the digest's own ~50-second budget instead of silently inheriting the provider's shorter 30-second default. Together these keep even a heavy day's request well inside the time budget, so catch-up can fill it. Caps are `MAX_SESSIONS_IN_DIGEST` / `MAX_HISTORY_PROJECTS` in [/src/main/services/daily-digest/digest-prompt-builder.ts](/src/main/services/daily-digest/digest-prompt-builder.ts), locked by [/tests/unit/services/daily-digest-prompt-bounds.test.ts](/tests/unit/services/daily-digest-prompt-bounds.test.ts).

The service is [/src/main/services/daily-digest-service.ts](/src/main/services/daily-digest-service.ts) — runs a 15-minute `createPeriodicTask` loop, checks `settings.dailyDigestGenerationHour` against local time, and if a digest for `yesterday` is missing it pulls source data via the enabled integrations (each source has its own helper: session-summary, gmail-summary, calendar-summary, …), concatenates them, and sends the whole thing through [`llmProviderService.chat()`](/src/main/services/llm-provider-service.ts) with `provider: 'openrouter'` + `responseFormat: 'json_object'`. The router uses the embedded OpenRouter key from `default-credentials.local.json` for the cheap path (so the digest works without any user-configured Anthropic account), falling back to the user's Anthropic apikey only if the cheap provider 5xxs or returns empty content. Generation skips only when there is no usable credential of either kind (no embedded OpenRouter key AND no user apikey); that genuine can't-generate case — plus an OpenRouter-only generation failure with no apikey to fall back on — raises ONE de-duplicated inbox alert ("Couldn't generate your daily digest") so the otherwise-silent stop surfaces instead of digests just disappearing. **Every generation failure is routed exactly once** (`routeDigestFailure`, 2026-09-08) into one of three surfaces, and the same verdict sets the log level so the two can't disagree: a **credential/billing** failure (rejected key, exhausted monthly allowance, no key left to fall back to, dry credit balance) raises that actionable card; a **transient** failure (a timeout/abort, or a 429/5xx) touches nothing and self-heals on the next 15-minute tick; only a genuinely unexplained failure feeds the F113 "your digest keeps failing" give-up streak. Before this, only an HTTP 401 counted as credential, so the other three classes fed the streak and its card told the user the digest was failing *"for a non-credential reason"* when it was entirely a credential problem — and transient blips, counted in a streak that only clears on a success, accumulated into the same false alarm. Invariants I7–I9 in [daily-digest-credential-contract.md](/.claude/memory/contracts/daily-digest-credential-contract.md). The follow-up Q&A path uses the same routing without `responseFormat` (returns prose). On first run it loops from `today - 7` forward so you get a backfill. Results land in SQLite via [/src/main/db/queries-daily-digest.ts](/src/main/db/queries-daily-digest.ts) with unread / archived / snoozed state and a per-digest conversation thread. The IPC surface lives in [/src/main/ipc/daily-digest-handlers.ts](/src/main/ipc/daily-digest-handlers.ts) (list / get / mark-read / archive / snooze / follow-up / generate / unread-count / toggle-alert-dismissal / dismiss-all-alerts / list-suppressions / remove-suppression); the push events `DAILY_DIGEST_GENERATED`, `DAILY_DIGEST_UNREAD_CHANGED`, and `DAILY_DIGEST_UPDATED` keep the sidebar count and the open viewer live. Because that GENERATED push is fire-and-forget (the push bus has no replay), the briefing — and the weekly summary — also **reconcile on return**: a window focus / document-visible event refetches both, so a digest generated while Omniscio sat backgrounded overnight self-heals into the inbox the moment you come back to the window, with no manual refresh/reload/restart needed ([useBriefingReconcileOnReturn](/src/renderer/src/hooks/useBriefingReconcileOnReturn.ts), invariant [briefing-reconcile-contract.md](/.claude/memory/contracts/briefing-reconcile-contract.md)). There is no delete channel — archive flips the `is_archived` flag on the digest row, so digests are never hard-deleted from the database.

**What the Alerts box contains (and what it deliberately excludes).** The digest's **Alerts** section is reserved for _non-obvious, cross-cutting_ problems you can't already see elsewhere — a broken build, a failing or wedged CI/cloud pipeline, an app crash (Sentry), or a systemic issue spanning many sessions. It does **not** re-list individual sessions that are errored / stalled / waiting / in recovery — those already appear in your inbox and the sidebar, so repeating them is noise (and a single session's transient status is usually stale by the time the morning digest is read). This is enforced two ways: the generation prompt tells the model to keep per-session attention in the _action items_ section, not alerts; and a deterministic shared filter (`filterInboxSessionAlerts` in [/src/shared/types/digest-data.ts](/src/shared/types/digest-data.ts)) drops any alert whose text echoes an internal session marker (`needs_you`, `wait_timeout`, `response_aborted`, `recovery_failed`). The filter runs at generation, at render, AND in the share export, so even digests generated before this rule clean up the moment you open them — no regeneration needed. Genuinely useful infra/crash alerts carry none of those tokens and are untouched. Invariant: [digest-alert-inbox-dedup-contract.md](/.claude/memory/contracts/digest-alert-inbox-dedup-contract.md).

**Sidebar bucketing (Active / Snoozed / Archived)** — The active/snoozed/archived split is computed by a single pure helper, `bucketDigests` in [/src/renderer/src/features/daily-digest/digest-bucket.ts](/src/renderer/src/features/daily-digest/digest-bucket.ts), called from [DigestSidebarContent.tsx](/src/renderer/src/features/daily-digest/DigestSidebarContent.tsx) on every render with `(digests, Date.now())`. Active is anything with `isArchived === false`, `snoozedUntil === null` (or in the past), and `digestDate` within the last 7 days; the same 7-day cutoff folds older un-archived rows into Archived. Snooze re-bucketing is therefore passive — the row moves to Active on the first render after `Date.now() > snoozedUntil`, no timer or push event needed. The two flags live in `daily_digests.is_archived INTEGER NOT NULL DEFAULT 0` and `daily_digests.snoozed_until TEXT NULL` (added in migration v107 — see [/src/main/db/incremental-migrations.ts](/src/main/db/incremental-migrations.ts) and [/tests/unit/db/digest-sidebar-archived-migration.test.ts](/tests/unit/db/digest-sidebar-archived-migration.test.ts)). Mutators are `setDigestArchived(id, archived)` and `setDigestSnoozedUntil(id, iso | null)` in [queries-daily-digest.ts](/src/main/db/queries-daily-digest.ts); the five archive trigger paths (middle-click, X button, right-click, **E** / **Ctrl+W**, and the open briefing's header button) all funnel into `useDailyDigestStore.archiveDigest`. The right-click context menu lives in [DigestContextMenu.tsx](/src/renderer/src/features/daily-digest/DigestContextMenu.tsx); the row component (with its mobile-only X button + `mousedown`-based middle-click handler — `auxclick` doesn't fire reliably inside scrollable ancestors) is [DigestSidebarRow.tsx](/src/renderer/src/features/daily-digest/DigestSidebarRow.tsx). Snoozing reuses the generalized [SnoozePalette.tsx](/src/renderer/src/components/ui/SnoozePalette.tsx) modal (also used for sessions and emails) via a `kind: 'digest' | 'session' | 'email'` discriminator. The same `setDigestSnoozedUntil` mutator also drives the unified Inbox row — `selectUnreadCount` and `digestInboxSelectItems` in [/src/renderer/src/stores/daily-digest-store.ts](/src/renderer/src/stores/daily-digest-store.ts) both filter out active snoozes so the inbox rows and the amber count badge mirror the sidebar's Snoozed bucketing without a separate code path. **Individual briefing rows (2026-07-17):** the inbox surfaces ONE row per unread briefing — each titled with its own date (`formatDigestDate`) and dismissed on its own — instead of a single collapsed "N new briefings" line; `digestInboxSelectItems` emits those per-briefing rows newest-first (plus a neutral-gray row for the briefing you're actively reading), and the backend `digestToInboxItems` projection mirrors it for the CLI `/inbox` view. The active-row highlight tracks the specific briefing via the digest store's `selectedDigestId` (threaded through the render surfaces as `activeInboxDigestId`) so only the briefing you're on highlights, never all of them. Right-clicking a digest in the Inbox opens the same `DigestContextMenu`; pressing **S** while the digest is the active inbox item dispatches the shared `snooze-entity` `CustomEvent` that the global `SnoozePalette` listener picks up.

Customization splits cleanly across the gather phase (which runs once per day) and the render phase (which runs every time you open a digest). **Project exclusions** are gather-phase: the service consults a `getEffectiveProjectIds` helper that subtracts the user's excluded-project list from the current real-project set before any session-summary work begins, so excluded projects never enter the prompt and never accrue Haiku tokens. **Section visibility toggles** are render-phase: the digest is generated with all sections present, and [/src/renderer/src/features/daily-digest/DigestViewer.tsx](/src/renderer/src/features/daily-digest/DigestViewer.tsx) consults the section-visibility settings at display time to filter out hidden sections. This keeps "toggle on" instant with no regeneration needed, and a previously-generated digest immediately reflects the new visibility settings. **Alert dismissals** are persisted per-digest in SQLite (a JSON dismissed-IDs array on the digest row) and applied at render time by [/src/renderer/src/features/daily-digest/AlertsSection.tsx](/src/renderer/src/features/daily-digest/AlertsSection.tsx), which also exposes the per-row × button and the section-header "Dismiss all" chip. The customization UI lives in [/src/renderer/src/features/settings/DailyDigestCustomizationCard.tsx](/src/renderer/src/features/settings/DailyDigestCustomizationCard.tsx), embedded in the existing settings page at [/src/renderer/src/features/settings/DailyDigestSettings.tsx](/src/renderer/src/features/settings/DailyDigestSettings.tsx). The sidebar entry is [/src/renderer/src/features/daily-digest/DigestSidebarContent.tsx](/src/renderer/src/features/daily-digest/DigestSidebarContent.tsx). Follow-up Q&A also uses Haiku so costs stay low even when you ask ten questions about the same digest.

**Permanent alert suppressions (Phase 2)** are a third customization track that hybridizes the gather and render phases. Each suppression is a row in `digest_alert_suppressions` (created by the toggle-alert-dismissal IPC when called with `suppress: true`) keyed by the `{alertText, alertType}` of the alert that triggered it. The generation prompt builder injects a **SUPPRESSED ALERT TOPICS** block listing every active suppression so Haiku is asked, in-prompt, not to re-surface those topics. Belt-and-braces: a post-generation safety filter (`isAlertSuppressed` + `extractKeyTokens` exported from [/src/main/services/daily-digest-service.ts](/src/main/services/daily-digest-service.ts)) walks every alert in the model's response and drops any whose text shares ≥ 1 distinctive token with an active suppression — that catches the case where Haiku rewords the same topic. The suppressed-alert list and per-row × / overflow menu / hotkey wiring all share one identity check (`{type, text}`), exported as `isAlertDismissed` from [AlertsSection.tsx](/src/renderer/src/features/daily-digest/AlertsSection.tsx) so the dispatcher and the renderer always agree on what "the topmost undismissed alert" means.

**Keyboard hotkeys (Phase 2)** are wired through the standard keybindings registry — three actions in [src/shared/keybindings.ts](/src/shared/keybindings.ts) (`digestDismissAlert`, `digestSuppressAlert`, `digestDismissAllAlerts`) all in the `daily-digest` category. The dispatcher in [/src/renderer/src/hooks/useKeyboardShortcuts.ts](/src/renderer/src/hooks/useKeyboardShortcuts.ts) gates them: each keystroke is matched against a category-scoped lookup that only fires when `activeProjectId === DAILY_DIGEST_PROJECT_ID`, so X / Shift+X never collide with the same letters used in Gmail or diff-review. Handlers are registered from [DigestViewer.tsx](/src/renderer/src/features/daily-digest/DigestViewer.tsx) via the `useShortcutAction` hook (one call per action ID); each handler computes the topmost undismissed alert via the shared identity check and calls the corresponding store action (`dismissAlert` / `dismissAlert({suppress: true})` / `dismissAllAlerts`). The Alerts section's hint chip uses the reactive `useShortcutHint()` hook (subscribed to the keybindings store) to render the live binding labels, so changing a binding in settings updates the chip text without a remount or page refresh.

**Deterministic commit tally** — `DigestStats` carries two fields, `commitsTotal` and `commitProjectCount`, that are injected into the parsed `stats` object **after** the LLM JSON is parsed — the model never authors these numbers, so they are deterministic and never hallucinated. `gatherContext` calls `gatherDayCommits`, which uses the same `gatherCommitActivity()` helper in [src/main/services/commit-activity.ts](/src/main/services/commit-activity.ts) to run `git rev-list --count --branches` (all local branches, not just `HEAD`, because Omniscio's worktree-per-session model lands commits on per-session branches the main checkout can't reach) over every project with ≥1 session on the local calendar day (`date(started_at, 'localtime')`). Virtual-project sentinels are excluded via `NOT GLOB '__*'`. `DigestViewer`'s `StatsBar` renders "N commits across M projects today" (hidden when zero).

**Deterministic activity tally (2026-08-02)** — alongside the commit tally, `DigestStats` carries three day-scoped activity counts — `inboxArchived`, `userAnswers`, and `automatedActions` — injected into the parsed `stats` object **after** the LLM JSON is parsed, so (like the commit tally) the model never authors them and they can't be hallucinated, and they add no prompt cost. `gatherDayActivity` in [/src/main/services/daily-digest/digest-context-gatherer.ts](/src/main/services/daily-digest/digest-context-gatherer.ts) counts them app-wide over the digest's local day through the same off-thread reader: **answered** = your genuine replies to agents — an operator row that is not an auto-response, **and** is not agent-to-agent (`metadata.peerMessage`), not injected over the CLI server by another session or a cron (`metadata.cliSourceSessionId` / `cliSourceCronJobId`), and not the session's first operator row (an opening prompt is not a reply, whoever typed it). The "written by you" half is the SHARED predicate in [/src/main/db/human-operator-turn-sql.ts](/src/main/db/human-operator-turn-sql.ts) — the same one behind the Statistics panel's **Your Messages** card, so the two surfaces can never disagree about who "you" is; the day-windowed query that wraps it lives in [/src/main/services/daily-digest/digest-activity-sql.ts](/src/main/services/daily-digest/digest-activity-sql.ts), which the SQL test imports rather than hand-copying. "Earlier operator row" is ordered by `(timestamp, rowid)`, not timestamp alone — on a bare `<`, two turns written in the same millisecond would each find no predecessor and BOTH be discarded as openers. **`source='operator'` alone is the channel, not the author** — peer turns and CLI injections are written there with `is_auto_response=0` deliberately, so the receiving agent treats them as a real new turn to answer; before 2026-09-09 the bare channel test made this chip report machinery as the user (measured on the dev box: 2,426 "replies by you" for 2026-09-08, of which 1,443 were peer chatter and 252 machine-spawned openers — 73% machinery, corrected to 636). The Statistics panel's `human` bucket carried the same flaw and was fixed in the same change — its per-session denormalized `message_count_*` columns are re-derived by migration `20260909100102`, which clears every `message_counts_synced_at` stamp so the read falls back to a live scan while the background sweeper refills them; **archived** = inbox items you dismissed (`inbox_alert_items.archived_by='user'` — a new nullable actor column that tells a user dismissal apart from an automatic self-clear, since both stamp the same `archived_at`; every automatic archiver defaults to `'auto'` and only the two user-facing IPC archive handlers pass `'user'`, locked by [inbox-alert-contract.md](/.claude/memory/contracts/inbox-alert-contract.md) invariant I28); **automated** = automation-rule runs that fired, succeeded, and did something (`condition_matched=1 AND action_result='success' AND action_type != 'pass'` — the same "effective automation" filter as `readChannelActions` in [queries-stats/summary.ts](/src/main/db/queries-stats/summary.ts)) **plus** away-mode auto-replies to an agent (`is_auto_response=1 AND metadata.autoRule`). `StatsBar` renders the three as an "activity" chip row (answered / archived / automated), each hidden when absent or zero so old digests and quiet days stay clean. Directly beneath that chip row, a **human-vs-automated summary line** sums the human side (answered + archived) against automation and states the % automated (e.g. "Human vs. automated: 1,093 by you · 1,931 by automation (64% automated)") — shown only when BOTH the human side and automation had activity that day, so it always frames a real comparison; a one-sided day is left to the chips alone. SQL coverage: [/tests/integration/daily-digest-sql.test.ts](/tests/integration/daily-digest-sql.test.ts); actor-tag coverage: [/tests/unit/services/inbox-archive.test.ts](/tests/unit/services/inbox-archive.test.ts).

## Related

Briefings and [Weekly Summary](weekly-summary.md) share one sidebar entry and one shape — the weekly page is the Monday-morning version of the same recap. If what you actually want to act on is a single alert rather than the whole day, [automations and auto-replies](automations-and-auto-replies.md) explains the machinery that acts for you, and [Gmail integration](gmail-integration.md) covers the biggest single source the digest draws on.

- [automations-and-auto-replies.md](automations-and-auto-replies.md) — digests summarize what your automations did yesterday
- [gmail-integration.md](gmail-integration.md) — one of the most useful sources for the digest
