Briefings (AI briefing of what happened)
Briefings — the sidebar's Daily Digest — is the once-a-day AI-written recap of everything that happened across your sessions, mail, calendar, SMS, Slack, Telegram, RSS and GitHub. Covers what it includes, how to tune its hour and sources, follow-up questions, alert triage, hidden sections, the per-day usage ledger behind its token and cost figures, and excluded projects.
Shipped display name is Briefings (renamed from "Daily Digest" 2026-09-01 — it covers the weekly review too). The persisted sentinel
__daily_digest__, thedailyDigestEnabledflag, the integration iddaily-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.
The Briefings sidebar entry also hosts Weekly Summary 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
- 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 → Briefings (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 indigest-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). - 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.
- 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 —
247under 1k,34kfor thousands,1.5Mfor millions,1.8Bfor 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). - 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.
- 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 toprovider: '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). - 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.
- 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).
- 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. - Manage suppressed topics. Settings → Email & Summaries → Briefings → 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.
- 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.
- Hide sections you don't care about. Settings → Email & Summaries → Briefings → 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.
- 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.
- Snooze a digest from the Inbox. Today's digest also appears in the unified Inbox as a row under BRIEFINGS. Right-click the row to open the same context menu used in the Briefings sidebar — Archive, Snooze, or Unsnooze — or press H while the digest row is the active inbox item to open the SnoozePalette. The same
setDigestSnoozedUntilmutator 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
digestDatefalls 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
snoozedUntiltimestamp 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 — and since 2026-10-05 all five, plus Snooze, land on the next item the same way the Inbox does. Archive or snooze the open item and the panel opens the row directly below it (briefings and weekly summaries alike, in the order the sidebar draws them); on the last row it opens the one above; when it was the only row the pane closes (on a phone it slides back to the Briefings list). Archiving a row you are not reading leaves the open one alone. Every one of these goes through one shared exit, briefings-panel-exit.ts, which walks the panel with the Inbox's own next-item resolver (findNextItem) — before it, the row X and the header button never moved the selection and E used a private "next active briefing" rule that skipped weekly summaries and stranded the last row. The paths (the keyboard path also registers a 5-second Undo, whose shortcut hint reflects your live undoLastAction binding):
- Middle-click the row in the sidebar (mouse button 1 on
mousedown— Chromium eatsauxclickinside scrollable ancestors, soauxclickwon't work; this is desktop-only). - 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.
- 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.
- Tap the small X button that appears on the row at mobile widths (it's
md:hidden— never shown on desktop). - 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 advances to the next inbox item; opened from the Briefings sidebar it advances to the next Briefings item (above). 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. Until 2026-10-05 it then archived but left the pane on the archived briefing, its label flipped to Unarchive in the same spot, so a second tap undid the archive.
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.
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, locked by /tests/unit/services/daily-digest-prompt-bounds.test.ts.
The service is /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() 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. 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 with unread / archived / snoozed state and a per-digest conversation thread. The IPC surface lives in /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, invariant 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) 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.
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, called from 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 and /tests/unit/db/digest-sidebar-archived-migration.test.ts). Mutators are setDigestArchived(id, archived) and setDigestSnoozedUntil(id, iso | null) in 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; 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. Snoozing reuses the generalized 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 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 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, 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, embedded in the existing settings page at /src/renderer/src/features/settings/DailyDigestSettings.tsx. The sidebar entry is /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) 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 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 (digestDismissAlert, digestSuppressAlert, digestDismissAllAlerts) all in the daily-digest category. The dispatcher in /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 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 over every project with ≥1 session on the local calendar day (date(started_at, 'localtime')). It reads all local branches rather than just HEAD, because Omniscio's worktree-per-session model lands commits on per-session branches the main checkout can't reach. Since 2026-10-01 it counts only authored commits — git rev-list --branches --since --until --min-parents=1 --max-parents=1 --format=…, then keeps a commit only when a person wrote it, it has exactly one parent (no merge, no parentless root), and it changed something (emptiness by tree oid, not --numstat, which measured ~297 s for a week against ~0.9 s for the tree compare). The parent filter and the machine-author test are the shared declarations in scripts/lib/git-authored-history.mjs — the same ones behind the Weekly/Daily Contributor Report, so the two cannot disagree about those two questions. They are not the same number: the report additionally keeps only commits that touch code or tests, collapses rebased copies, and resolves its roster before the machine filter. 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 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 — 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, 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 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) 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; actor-tag coverage: /tests/unit/services/inbox-archive.test.ts.
Day-scoped usage ledger (2026-10-03) — the token and cost figures above are now read from a per-day ledger, session_daily_usage, instead of from whole session rows. sessions.input_tokens / output_tokens are cumulative counters with no day baseline, so the old read — sum each session over a started_at window — answered the day's question wrongly in both directions at once: it dropped every session that started earlier (which on a real day is the handful of long-running agents doing most of the work) and credited every session that did start in the window its entire lifetime. Measured on the owner's install for 2026-10-02, the briefing published 87.5 M tokens for a day whose transcripts carry 3.83 B, and $45.02 for a day that cost about $197. The ledger row is written on every turn, in the same statement as the session's cumulative counter, so the day's delta and the lifetime total can never disagree; it is keyed on the local day the work happened, so a session counts on each day it worked however long ago it started. It carries no history before it shipped — the per-day split was never recorded, so a backfill would be a guess — and an earlier day reads zero rather than a reconstructed lifetime total. Invariant: daily-digest-figures-contract.md a-day-figure-is-the-days-own-work. Coverage: /tests/integration/session-daily-usage-ledger.test.ts.
Related
Briefings and Weekly Summary 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 explains the machinery that acts for you, and Gmail integration covers the biggest single source the digest draws on.
- automations-and-auto-replies.md — digests summarize what your automations did yesterday
- gmail-integration.md — one of the most useful sources for the digest
Last verified 2026-10-04