---
title: Logs & Debugging (where to find logs, export for support)
---

# Logs & Debugging (where to find logs, export for support)

## What it is

Omniscio writes four kinds of diagnostic output, each in a different place. You usually only need the first one:

1. **Log files on disk** — the authoritative record. `main.log` (current) plus up to eighteen rotated backups (`main.log.1` → `main.log.18`), each capped at 10 MB. Backups older than 30 days are auto-deleted on startup. A separate `bootstrap.log` captures the earliest startup moments with raw `fs` writes, so you still get a trace even if electron-log itself fails to initialize.

   **How far back the archive actually reaches depends on how busy the app is**, because the ring is a file count, not a time window. On a very busy machine 18 files can be under three days. Every launch measures the real span and writes it to the log — `[log-retention] archive holds NNh of history` — and warns when it drops below 72 hours, the point where an incident reported a few days later would have no log left. If you see that warning and you need a longer window, raise `MAX_BACKUPS` in `src/main/services/logging/log-rotation.ts`.

2. **Debug Log Viewer** — a live in-app console that shows a rolling window of the most recent ~2000 log lines from both the main and renderer processes, with filtering, level pills, time-period buttons, and one-click copy.
3. **Crash dumps** — native minidump files (`.dmp`/`.txt`/`.json`) written automatically when the GPU or renderer process crashes at a level regular JavaScript logs can't capture (segfaults, access violations). They live in their own folder.
4. **Extra renderer diagnostics** — optional verbose `[scroll:*]`, `[session:select]`, `[session:nav]`, `[mobile-perf]` lines that can be switched on via a `localStorage` flag when you're deep-debugging a UI bug.

## Where to find it

The log files are in the Omniscio logs folder on your machine — on Windows under `%APPDATA%/omniscio/logs/`. The in-app way to reach them, and to produce a bundle to send on, is **Settings → Diagnostics**, which also exposes the export-for-support action.

## How it behaves

### How to use it

### The easy path (for support): Settings → Diagnostics

1. Open **Settings → Diagnostics**. You'll see up to four cards: **Log Level**, **Log Files**, **Crash Dumps** (only if crashes have occurred), and **Actions**.
2. **Open Logs Folder** reveals `main.log` in Explorer so you can open it in any text editor.
3. **Export Logs** zips every log file into a dated archive (`omniscio-logs-YYYY-MM-DD.zip`) at a location you pick — this is the file to attach to a bug report. **Crash dumps are deliberately NOT in it**: a dump can embed a memory snapshot, absolute paths and prompt fragments, and this ZIP leaves the device. Attach one by hand from **Open Crash Dumps Folder** if a specific crash needs it.
4. **Log Level** switches between Error / Warning / Info (recommended default) / Verbose / Debug. Bump it up to Verbose or Debug before reproducing a bug, then switch back so files don't fill up.
5. **Open Crash Dumps Folder** (only shown when at least one dump exists) takes you to the minidumps. This button is the ONLY way to get one — they are not in the Export Logs ZIP (see above).

### Reporting a wrong or missing agent reply: attach the conversation transcript

Logs tell you what the app DID. For "the agent's answer is missing / wrong / cut off", the evidence
you actually need is what the agent **wrote** — and the screen is exactly what you cannot trust in
that situation.

So the feedback dialog (the bug icon in the header) offers a tick-box, **"Include this
conversation's transcript"**, whenever you have a conversation open. Ticking it attaches the open
conversation as a `.md` file, which then shows up as a normal removable attachment chip so you can
see what you are sending before you send it.

It is **off unless you tick it**, and the choice does not carry over to your next report. A
transcript is the most sensitive thing this app holds, so it never rides along unasked — and when
it does, it is scrubbed exactly like the message before it leaves your computer: pasted keys,
tokens, file paths and contact details are masked.

**Why it is the right evidence:** the transcript is built from the stored message text and does NOT
pass through the display logic that folds tool activity and collapses the region before an agent's
final-answer marker. It therefore shows what the agent wrote even when the screen hid part of it —
the one artifact that can settle a "your reply is not there" report. (Same builder as **Export
Conversation** in the session menu and the `GET /session/:id/export` CLI route, so all three produce
an identical file. Exporting conversation content is privacy-adjacent and is always recorded in the
audit log, this path included.)

Too big to attach (more than about 6 MB, or more than the room the report's other attachments leave) is reported as such rather than silently trimmed — a truncated
transcript is worse than none, because it reads as the whole conversation. Use **Export
Conversation** and send the file separately in that case.

### Which code is actually running: Settings -> Diagnostics -> Running build

The version number tells you which RELEASE you have. It cannot tell you which COMMIT is
running, and on a development install those two drift apart constantly - the app keeps running
whatever was compiled the last time it started, while the checkout moves on beneath it. A fix
can be merged, be sitting right there in the source, and still not be in the app you are using.

**Settings -> Diagnostics -> Running build** answers that directly. It shows three things:

| Row        | What it means                                        |
| ---------- | ---------------------------------------------------- |
| **Branch** | The branch this bundle was built from                |
| **Commit** | The short commit id - the exact code you are running |
| **Built**  | When that build was made                             |

Each row reads **Unknown** when the build carries no git identity (a release tarball, a checkout
with no `.git`, a build made outside the normal path). Unknown is a real answer, not a failure:
it means the app genuinely cannot tell you, and it is deliberately never rendered as blank or as
anything you could mistake for being up to date.

The same three values ride along in every bug report's diagnostics, so a report can be answered
without asking the reporter to go and look. That is the point: before this, establishing which
code produced a report meant reading a log file or querying the database.

**When to reach for it:** any time behaviour looks like an older version of the app - a fix that
is definitely merged but is not showing up, an instruction or prompt that reads like it predates
a change, a setting that behaves the way it used to. Compare the commit here against what you
expect; if it is behind, the app needs a rebuild and restart, not a bug report.

### The live view: Debug Logs toolbar button

1. Click **Debug Logs** (terminal icon) in the toolbar. If it's not pinned, open the toolbar overflow (**⋯**) and either pin it or launch it from there each time.
2. The viewer shows both main-process and renderer-process lines as they happen — useful for watching a bug reproduce in real time. Time buttons (30s / 1m / 5m / 15m / All) filter the window, level pills (error/warn/info/verbose/debug) toggle visibility, and the **Dedup** button collapses consecutive identical lines into a single row with a count.
3. **Copy** puts the currently visible entries on the clipboard. **Clear** wipes the viewer only — the file on disk is untouched.
4. Close with **Esc** or the **×** button.

### The nuclear option: extra scroll/session logs

The renderer has three logging modes for scroll and selection events, toggled in the DevTools console:

```
localStorage.setItem('mc:log:scroll', 'verbose')   // every call
localStorage.setItem('mc:log:scroll', 'dedup')     // default — dedups per session+tag, suppresses hidden containers
localStorage.setItem('mc:log:scroll', 'silent')    // off
```

Reload the app after setting. Only useful when debugging scroll-positioning or session-navigation issues.

### Where the files actually live

- **Log folder**: `%APPDATA%/omniscio/logs/` on Windows (or the equivalent `userData` folder on macOS/Linux). The **Log Files** card in Settings → **Diagnostics** always shows the resolved path.
- **Crash dumps**: Electron's default `crashDumps` path (a sibling of the log folder inside the same `userData` directory).
- **E2E isolation**: when `AMC_INSTANCE_ID=e2e` is set, everything lives under `omniscio-e2e` instead — lets `npm run test:e2e:prod` run alongside a live dev session with zero shared state.

### Automatic crash reporting (auto-send-every-crash)

Separately from the on-disk logs above, Omniscio **automatically sends a crash report** whenever the app dies — by email (to the developer's address via Resend) and to Sentry. You don't have to export anything or file a bug; a crash reaches the developer on its own. This is deliberately broad about what counts as a "crash":

- **Live JavaScript errors** — an unhandled exception or promise rejection while the app is running.
- **Hard native crashes** — segfaults, out-of-memory kills, or heap corruption that bypass all JavaScript error handling. Sentry receives the native minidump directly; the email channel gets a short note pointing at the local dump file (the binary itself can be megabytes, so it isn't emailed).
- **Silent deaths** — the app is force-killed (Task Manager, OS, power loss) and never gets to run another line of code. Nothing can email at the moment of death, so Omniscio detects these on the _next_ launch (see "heartbeat sentinel" below) and reports them retroactively.
- **Startup failures** — a crash while the app is still loading its own code, before any of the safety nets above exist. Omniscio writes a tiny "starting up" marker as the very first thing it does; if that marker is still there at the next launch, the previous startup died early, and the failure is reported then.
- **A blank window at startup** — the window opens but never shows the app (a display or data-loading failure). The main app process notices the window never became usable — even though the blank window itself can't report anything — and sends the report.

Also, if your machine is **offline** when a crash happens, the report can't leave right then; Omniscio saves it and sends it automatically the next time the app starts with a working connection (still capped at the daily limit below).

### Always on, independent of telemetry

Crash reporting is **not** the same setting as telemetry. The optional **telemetry** opt-in controls only the weekly usage _digest_ and Sentry _breadcrumbs_. **Crash auto-send is on by default and keeps working even with telemetry off** — the reasoning is that crashes are rare, and a silently-dropped crash is worse than a slightly noisier inbox.

### Watching crash volume: the crash-free-session-rate

The reports above tell you _that_ a crash happened; to answer _how often_ — the number the public-beta "watch crash volume" step and the 1.0 release bar ("crash rate below X% of sessions") are judged against — read the **crash-free-session-rate in Sentry Release Health**. Omniscio's Sentry SDK counts one "session" per app run and marks it healthy or crashed, so Sentry computes crash-free-session-rate for you — **no separate dashboard to build, nothing to divide by hand.** To read it: open the Sentry project → **Releases** (or Release Health), filter to **environment `production`** and the release **`omniscio@<version>`** you're evaluating; the crash-free-sessions % is the crash-rate bar. Filtering to `production` matters — dev runs report under `development` and shouldn't count toward the release bar.

This is on automatically (it rides the same always-on Sentry init as crash capture) and is **content-blind**: a session carries only a healthy/crashed status and the release/environment tags — no message, stack, or IP. Automated test/sandbox launches don't pollute it because they suppress Sentry entirely (`AMC_DISABLE_TELEMETRY` / `AMC_INSTANCE_ID`). For repo-access readers: it's the SDK's default `mainProcessSessionIntegration`, kept on (and locked by a guard test) in [/src/main/services/sentry-init.ts](/src/main/services/sentry-init.ts); the invariant + the measurement caveats are I28 in the crash-reporting contract.

### Privacy

Before any crash report leaves the machine, Omniscio scrubs it. Secrets and paths are stripped — Anthropic API keys, Bearer/JWT tokens, OAuth access/refresh fields, and your Windows/Unix home-directory path — and so is contact PII: email addresses, phone numbers, and non-loopback IP addresses are masked (`[EMAIL_REDACTED]`, `[PHONE_REDACTED]`, `[IP_REDACTED]`). The scrub runs over the error message, stack trace, and every attached log/crash file. It is deliberately targeted at those shapes so it never mangles stack traces, file paths, or error fingerprints. Your OS username is no longer included. (Your machine's hostname is intentionally kept, so reports can be told apart across machines.)

For the Node **diagnostic report** specifically — a native-crash artifact that bundles your entire process environment — Omniscio goes further than the pattern scrub: it removes the whole environment-variables block before sending, wholesale and **name-independent**, so even an oddly-named custom key can't slip past the matcher. And if that report is itself corrupted or truncated (so the block can't be cleanly removed), it's withheld entirely rather than sent through the weaker pattern scrub — the crash is still reported, just without the unparseable attachment.

### How to turn it off

Three switches, in order of bluntness:

1. **Settings toggle** — _Crash auto-send_ under Settings → System. Turning it off stops crash emails entirely (default is on).
2. **Crash hard kill-switch env var** — launch with `AMC_DISABLE_CRASH_AUTOSEND=1`. This wins over the setting and additionally prevents the Sentry SDK from loading at all. It exists for automated test suites and incident-response windows that deliberately crash the app and must not spam reports.
3. **Master telemetry kill-switch env var** — launch with `AMC_DISABLE_TELEMETRY=1`. This is the broadest off switch: one flag silences the ENTIRE phone-home stack — crash auto-send AND Sentry (main + renderer) AND the fleet-telemetry / weekly-digest / error-tail pipeline — by forcing every telemetry-gated check off. It is the `isTelemetrySuppressed()` master gate that both `getCrashAutoSendEnabled()` and `getTelemetryEnabled()` compose, so it wins over the two switches above. The E2E / cloud / CI launch fixtures set it so test boots never report; a real user MAY set it to opt out of everything at once. (A non-empty `AMC_INSTANCE_ID` — sandbox / e2e / throwaway instances — triggers the same suppression.)

There is also a built-in daily cap (15 crash emails per calendar day) that survives restarts, so an app stuck in a crash-restart loop can't flood the inbox. A matching guard protects the Sentry side: each distinct error is capped to a set number of events per rolling day (on top of a coarser per-minute limit), so one looping error can't quietly drain the shared monthly Sentry event quota and blind crash monitoring for everyone — the first several of every distinct error still send, so a genuinely new bug always gets through.

### The heartbeat sentinel (how silent deaths are caught)

While running, Omniscio rewrites a tiny `heartbeat-sentinel.json` file in the logs folder every 5 seconds and **deletes it on a clean shutdown**. If that file is still present at the next launch, the previous run must have died without shutting down cleanly — a silent death. A boot-time sweep (`sweepCrashEvidence`) finds the leftover sentinel (plus any native crash dumps and Node diagnostic reports), forwards each as a crash report, and marks them so they're never re-sent. The sentinel carries the prior process's PID, uptime, the last operation it was performing, and a short tail of health ticks — enough to answer "was it healthy right before it vanished, and what was it doing?" It also records the **launcher** that started the app (its PID, name, and start time); on the next launch the sweep compares that against the new process's launcher, so a silent death of an otherwise-healthy app now also says _who killed it_ — the same launcher respawning the app (the signature of a dev-server / `electron-vite` hot-reload restart) versus a different or absent one (an external kill). That verdict is included in the crash report. The report also says whether the app was **still killing something when it died**: Omniscio records every process tree-kill it dispatches, and clears the record once that kill finishes, so a record still on disk at the next launch means a kill was in flight when the process vanished. When the target was the app's own process ID — or the process that launched it — the report names it as a **self-kill**, because a Windows tree-kill carries `/T` and takes the target's whole subtree down with it, the app included. When nothing was in flight the report says so as evidence only, never as proof that nothing outside ended the process.

### How it works (for repo-access readers)

The send path is `reportCrash()` in [/src/main/services/telemetry/telemetry-reporter.ts](/src/main/services/telemetry/telemetry-reporter.ts), gated by `getCrashAutoSendEnabled()` in [/src/main/services/config-store/accessors-settings.ts](/src/main/services/config-store/accessors-settings.ts) (env check first, then the `crashAutoSendEnabled` setting defaulting on). Sentry init is [/src/main/services/sentry-init.ts](/src/main/services/sentry-init.ts). The boot sweep is [/src/main/services/diagnostics/crash-evidence-sweeper.ts](/src/main/services/diagnostics/crash-evidence-sweeper.ts); the sentinel is written by [/src/main/services/diagnostics/main-heartbeat.ts](/src/main/services/diagnostics/main-heartbeat.ts). The ledger behind the self-kill verdict is [/scripts/lib/process-terminations-ledger.mjs](/scripts/lib/process-terminations-ledger.mjs), written from the single tree-kill dispatch point in [/src/main/process/child-kill.ts](/src/main/process/child-kill.ts). The exhaustive invariants (the telemetry asymmetry, the sentinel-ownership race fix, idempotency, the boot wiring order) live in the feature contract: [/.claude/memory/contracts/crash-reporting-contract.md](/.claude/memory/contracts/crash-reporting-contract.md).

## For agents

### How it works

Logging is centralized in [/src/main/services/logger.ts](/src/main/services/logger.ts), which re-exports a configured [electron-log](https://github.com/megahertz/electron-log) instance. Setup happens at startup in [/src/main/index.ts](/src/main/index.ts) via `setupLogging()`: level is resolved as **env `LOG_LEVEL` > persisted `logLevel` setting > default** (verbose in dev, info in prod). Then `configureFileRotation()` caps each file at 10 MB and rotates via numbered suffixes (`.1` newest, `.5` oldest); older files are deleted in `cleanupOldLogBackups()` on the next startup (deferred via `setImmediate` to avoid blocking window creation while Windows antivirus scans the directory). A separate `writeBootstrapLog()` raw-`fs` appender writes `bootstrap.log` _before_ electron-log is ready so a very-early startup crash still leaves a trace.

`installDebugLogTransport()` pushes every file-transport log entry into a 2000-entry O(1) circular ring buffer and emits an `IPC.DEBUG_LOG` push event so the [DebugLogViewer](/src/renderer/src/components/ui/DebugLogViewer.tsx) gets real-time updates. On mount, the viewer calls `IPC.LOGS_GET_BUFFER` to replay history (de-duped against any push events already received), and also monkey-patches `console.*` methods in the renderer so those lines flow into the same viewer tagged `source=renderer`. Two guards prevent duplication: renderer console is _not_ pushed back up to the main-process file to avoid a loop, and `_debugHookInstalled` prevents hook stacking during HMR reloads.

The Settings → Diagnostics UI is [/src/renderer/src/features/settings/sections/diagnostics/DiagnosticsSettings.tsx](/src/renderer/src/features/settings/sections/diagnostics/DiagnosticsSettings.tsx), backed by the IPC handlers in [/src/main/ipc/log-handlers.ts](/src/main/ipc/log-handlers.ts): `LOGS_OPEN_FOLDER` (uses `shell.showItemInFolder`), `LOGS_EXPORT` (async `archiver` ZIP including a `crash-dumps/` subfolder), `LOGS_GET_INFO` (directory + file list + sizes), `LOGS_GET_BUFFER` (ring buffer snapshot), `CRASH_DUMPS_GET_INFO`, and `CRASH_DUMPS_OPEN_FOLDER`. Settings search indexes all four log-related setting IDs (`log-level`, `log-files`, `export-logs`, `crash-dumps`) in [/src/renderer/src/features/settings/settings-search-index.ts](/src/renderer/src/features/settings/settings-search-index.ts), so Ctrl+, then typing "log" finds them.

Full internal reference for log levels, rotation rules, error notification coverage, and renderer diagnostic prefixes: [/.claude/memory/observability.md](/.claude/memory/observability.md).

## Related

- [notifications-and-silence.md](notifications-and-silence.md) — error _notifications_ (what reaches you), not the log record (what the app writes for later)
- [session-stuck-in-needs-you.md](session-stuck-in-needs-you.md) — if one specific session is misbehaving, its needs-you state usually explains more than the log file
