diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 6530cf8b9..b7d7c7926 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -357,6 +357,21 @@ For the full doc-pipeline workflow (compile, check-tier, render-diagrams), see [
- **Tag-based event dispatch.** Event handlers are wired once at mount; the current element is stored in `Tag` so handlers always read the latest closure.
- **No XAML.** Everything is C#.
+### Diagnostics: audience, not severity, decides the channel
+
+`Debug.WriteLine` exists for the framework contributor reading the Output window in Visual Studio; it disappears in Release builds. `ReactorEventSource` (the `Microsoft-UI-Reactor` provider) exists for the app developer, SRE, and support engineer — it's release-visible, keyword-gated, and zero-allocation when no consumer is attached. New code that reports an error, a swallowed exception, or a failing HRESULT belongs on the EventSource side; new code that traces internal framework state for a contributor's benefit (reconciler bookkeeping, scheduler queue depth) stays on `Debug.WriteLine`.
+
+For swallowed exceptions and HRESULT-return diagnostics, route through the `DiagnosticLog` helper:
+
+```csharp
+catch (COMException ex) when (ex.HResult is HResults.RPC_E_DISCONNECTED or HResults.E_FAIL)
+{
+ DiagnosticLog.SwallowedError(LogCategory.Hosting, "AppWindow.Close", ex);
+}
+```
+
+`DiagnosticLog.SwallowedError` and `DiagnosticLog.HResultFailed` emit the typed ETW event under `Keywords.Errors` at `Warning` in Release **and** mirror a richer line (including `ex.Message`) to `Debug.WriteLine` in Debug builds via a `[Conditional("DEBUG")]` helper. The exception message never reaches the ETW payload — see [`docs/guide/diagnostics.md`](docs/guide/diagnostics.md) for the PII discipline and the full capture workflow.
+
---
## Hot reload
diff --git a/docs/_pipeline/diagrams/diagnostics/flow.svg b/docs/_pipeline/diagrams/diagnostics/flow.svg
new file mode 100644
index 000000000..8b685673f
--- /dev/null
+++ b/docs/_pipeline/diagrams/diagnostics/flow.svg
@@ -0,0 +1,90 @@
+
+
diff --git a/docs/_pipeline/templates/diagnostics.md.dt b/docs/_pipeline/templates/diagnostics.md.dt
new file mode 100644
index 000000000..247b26352
--- /dev/null
+++ b/docs/_pipeline/templates/diagnostics.md.dt
@@ -0,0 +1,401 @@
+---
+title: "Diagnostics"
+app: diagnostics
+order: 44
+audience: advanced
+goal: |
+ How to capture, filter, and read Reactor's release-build diagnostics
+ in a real app. Covers the rule (audience, not severity, decides
+ whether something is a Debug.WriteLine or a ReactorEventSource
+ event), the four ways to capture a trace (env vars, dotnet-trace,
+ Visual Studio Profiler, ReactorTrace.Subscribe in process), and how
+ the reactor.logs MCP tool now surfaces ETW events alongside
+ stdout/stderr/debug output for the in-process devtools agent. Spec
+ 044 — tracing and logging cleanup.
+tier: comprehensive
+---
+
+When Reactor swallows an exception, returns past an HRESULT, or
+otherwise chooses to continue rather than throw, the framework has
+historically dropped a `Debug.WriteLine` and moved on. That worked for
+the contributor — the message landed in Visual Studio's Output window
+during a Debug build — but it disappeared in Release, which is the
+configuration every shipped app runs. Spec 044 fixed that: error and
+HRESULT diagnostics now route through the `Microsoft-UI-Reactor`
+`EventSource` (release-visible, keyword-gated, zero-allocation when
+no consumer is listening), and the existing in-process devtools tool
+(`reactor.logs`) was extended so an MCP agent can read framework
+events in the same call that returns stdout / stderr / debug output.
+
+# Diagnostics
+
+This page is about reading Reactor's diagnostics from a real app —
+not about adding new events. The emission pipeline (keywords,
+IsEnabled gate, event-id allocation, EventPipe vs ETW transport split)
+lives in [perf-instrumentation.md](perf-instrumentation.md); start
+there if you are extending the provider.
+
+
+
+## The rule
+
+Audience, not severity, decides the channel. `Debug.WriteLine`
+exists for the contributor working on the framework itself; the
+target audience is "someone reading the Output window in Visual
+Studio with their checkout open". `ReactorEventSource` exists for
+the app developer, the SRE, and the support engineer; the target
+audience is "someone who runs the shipped binary and needs to know
+why a window failed to open".
+
+| Audience | Channel | Visible in Release? |
+|---|---|---|
+| Framework contributor | `Debug.WriteLine`, `Debug.Assert` | No |
+| App developer / SRE | `Microsoft-UI-Reactor` `EventSource` | Yes |
+| Unreachable code | `throw new UnreachableException(...)` | Yes (as a crash) |
+
+The two channels are complementary, not redundant. A swallowed
+exception in `RenderContext` emits both — the typed event for the
+app developer and a richer `Debug.WriteLine` mirror (with the
+exception message) for the contributor running a Debug build. The
+mirror is `[Conditional("DEBUG")]` and compiles out in Release.
+
+
+Exception messages are PII-shaped and never reach the ETW payload.
+`ex.Message` can carry absolute paths, environment values, partial
+form values, and the bound user data that caused the failure. The
+typed event payload carries the exception *type* only
+(`InvalidOperationException`, `COMException`); a same-UID
+`dotnet-trace` consumer sees the type and nothing else. If you need
+the message in your own logs, attach an in-process subscriber (see
+[`ReactorTrace.Subscribe`](#in-process-subscription) below) and
+forward to a sink under your own ACL.
+
+
+## What's instrumented
+
+The provider's events split across a small set of keywords; spec 044
+adds six subsystem keywords on top of the seven the perf-instrumentation
+page documents. Pick the bits that match what you're triaging:
+
+| Keyword | Bit | Covers |
+|---|---|---|
+| `Errors` | `0x20` | Generic `SwallowedError` / `HResultFailed`, plus `RenderError` |
+| `Hosting` | `0x80` | `WindowOpened`, `WindowClosed`, `WindowDpiChanged`, `BackdropMaterializationFailed` |
+| `Persistence` | `0x100` | `PersistenceRead`, `PersistenceWrite`, `PersistenceRejected` |
+| `Navigation` | `0x200` | `NavigationRequested`, `NavigationCompleted`, `NavigationCancelled`, cache hit/miss/evict, transitions, deep-link |
+| `Intl` | `0x400` | `IntlMissingKey` |
+| `Theme` | `0x800` | `ThemeApplyFailed` |
+| `Shell` | `0x1000` | `JumpList*` / `ThumbnailToolbar*` / `Tray*` (planned) |
+
+Combine bits with bitwise-or. The most common capture-everything-
+unsurprising mask is `0x1FA0` (`Errors | Hosting | Persistence |
+Navigation | Intl | Theme`) — drops the verbose `State` and
+`EventDispatch` keywords that produce per-state-write spam.
+
+## Capturing a trace
+
+There are four routes. Pick by where the consumer lives.
+
+### Environment variables — zero code, file output
+
+For a quick local capture with no app changes, the .NET runtime can
+write an EventPipe `.nettrace` file driven entirely by environment
+variables. This is the right tool when an issue reproduces only in a
+shipped build, on a different machine, or when you want to hand the
+trace off to someone else:
+
+```
+set DOTNET_EnableEventPipe=1
+set DOTNET_EventPipeOutputPath=reactor.nettrace
+set DOTNET_EventPipeConfig=Microsoft-UI-Reactor:0x1FA0:5
+MyApp.exe
+```
+
+The third variable is `::`. `0x1FA0` is
+the everything-unsurprising mask above; level `5` is `Verbose`. Run
+the app, reproduce the issue, exit cleanly (the runtime flushes the
+file on shutdown). Open the resulting `.nettrace` in Visual Studio's
+Performance Profiler → Events Viewer.
+
+### `dotnet-trace` — attach to a running process
+
+`dotnet-trace` is the cross-platform CLI for EventPipe capture.
+Useful when the app is already running and you want to scope the
+window:
+
+```
+dotnet-trace collect ^
+ --process-id ^
+ --providers Microsoft-UI-Reactor:0x1FA0:5 ^
+ --output reactor.nettrace
+```
+
+`Ctrl+C` stops the session; the `.nettrace` lands in the working
+directory. Same file format as the env-var route — same Events
+Viewer workflow.
+
+### Visual Studio Performance Profiler
+
+For a GUI workflow, the Profiler's Events Viewer accepts the same
+provider:keyword:level format. Diagnostics → Performance Profiler
+→ Events Viewer → Settings → Custom Provider:
+
+```
+Microsoft-UI-Reactor:0x1FA0:5
+```
+
+The timeline ties each Reactor event to the CPU sample, GC, and
+network views, so you can see a `NavigationCompleted` next to the
+allocation spike it caused.
+
+### In-process subscription
+
+When the consumer is the app itself — a custom log sink, a devtools
+overlay, an in-app diagnostics page — `ReactorTrace.Subscribe`
+returns an `IDisposable` token that fires the callback for each
+event matching the filter:
+
+```csharp snippet="source:src/Reactor/Diagnostics/ReactorTrace.cs#subscribe-shape"
+```
+
+Multiple concurrent subscribers are supported; each filter is
+independently active until the token is disposed. The subscriber
+callback runs on the emission thread (usually the UI dispatcher when
+the event originates from reconcile / render), so keep the work
+minimal — the framework wraps the call in `try/catch` so a buggy
+sink can't propagate to `EventSource.WriteEvent`, but the dispatcher
+is still blocked for the duration. Forward to a queue if your sink
+does anything expensive.
+
+`ReactorTrace.Subscribe` is **not** a file-capture API. It exists
+because in-process consumers (devtools, an `ILogger` adapter, a
+custom diagnostics page) need access to the same events the env-var
+route writes to disk. For a `.nettrace` file, use one of the three
+routes above — they cost less and emit a richer format.
+
+## `reactor.logs source=event` — MCP integration
+
+The Reactor in-process devtools (`mur devtools`) expose a `logs` MCP
+tool that drains the captured `Console.Out` / `Console.Error` /
+`Debug.WriteLine` ring buffer. Spec 044 extended the tool so the
+buffer also captures `Microsoft-UI-Reactor` ETW events, surfaced
+under a new `source=event` filter:
+
+```jsonc
+// Request
+{
+ "method": "tools/call",
+ "params": {
+ "name": "logs",
+ "arguments": { "source": "event", "tail": 20, "level": "Warning" }
+ }
+}
+
+// Response — each entry now carries eventName / eventId
+{
+ "entries": [
+ {
+ "seq": 142,
+ "ts": "2026-05-19T17:42:11.330Z",
+ "source": "event",
+ "level": "Warning",
+ "text": "SwallowedError category=Hosting operation=AppWindow.Close exceptionType=COMException",
+ "eventName": "SwallowedError",
+ "eventId": 16
+ }
+ ],
+ "nextSeq": 143,
+ "dropped": 0
+}
+```
+
+Existing clients that don't pass `source=event` see zero behavior
+change — the `stdout` / `stderr` / `debug` filters still return their
+dedicated streams. The `eventName` and `eventId` fields are present
+on every entry but are `null` for non-event sources, so a client
+written before spec 044 ignores them safely.
+
+HR-style payload fields render in the same `0x{X8}` shape the
+pre-migration `Debug.WriteLine` sites used (`HResultFailed
+category=Shell operation=JumpList.Begin hr=0x80004002`), so log
+greps that matched the old shape continue to hit.
+
+## Patterns
+
+### Reading a swallow that happened in production
+
+A customer reports a window that won't close cleanly on a specific
+machine. Capture with the env-var route, filter to the `Errors`
+keyword (`0x20`) at `Warning`, and open the trace in Events Viewer.
+The relevant entries will look like:
+
+```
+SwallowedError category=Hosting operation=AppWindow.Close exceptionType=COMException
+HResultFailed category=Hosting operation=AppWindow.Close hr=0x80004005
+```
+
+The operation label is stable and developer-authored — search the
+Reactor source for `"AppWindow.Close"` and you land on the
+`DiagnosticLog.SwallowedError` call site. The exception type and HR
+together pin the failure class (in this case, `E_FAIL` from the
+WinUI AppWindow lifecycle) without ever leaking the user-visible
+window title.
+
+### Wiring `ReactorTrace.Subscribe` to a per-window debug overlay
+
+A devtools overlay that wants to surface "navigation event happening
+now" doesn't need a file capture — just an in-process subscription:
+
+```csharp
+public sealed class NavigationOverlay : IDisposable
+{
+ private readonly IDisposable _subscription;
+ private readonly Queue _ring = new();
+
+ public NavigationOverlay()
+ {
+ _subscription = ReactorTrace.Subscribe(
+ evt =>
+ {
+ if ((evt.Keywords & ReactorEventSource.Keywords.Navigation) == 0) return;
+ var line = $"{evt.EventName} {string.Join(' ',
+ Enumerable.Range(0, evt.Payload.Count)
+ .Select(i => $"{evt.PayloadNames[i]}={evt.Payload[i]}"))}";
+ lock (_ring)
+ {
+ _ring.Enqueue(line);
+ while (_ring.Count > 50) _ring.Dequeue();
+ }
+ },
+ level: EventLevel.Verbose,
+ keywords: ReactorEventSource.Keywords.Navigation);
+ }
+
+ public void Dispose() => _subscription.Dispose();
+}
+```
+
+The callback runs on the dispatcher (most navigation events
+originate there) — for a UI overlay that's actually what you want.
+If the overlay forwards to a background sink instead, marshal off
+the dispatcher before doing the I/O.
+
+## Common Mistakes
+
+### Treating `Debug.WriteLine` as a release diagnostic
+
+```csharp
+// Don't:
+try { window.AppWindow.Close(); }
+catch (Exception ex)
+{
+ Debug.WriteLine($"Close failed: {ex}"); // disappears in Release
+}
+```
+
+```csharp
+// Do:
+try { window.AppWindow.Close(); }
+catch (COMException ex) when (ex.HResult is HResults.RPC_E_DISCONNECTED or HResults.E_FAIL)
+{
+ DiagnosticLog.SwallowedError(LogCategory.Hosting, "AppWindow.Close", ex);
+}
+```
+
+The first form was invisible to every shipped app. The second form
+emits to `Microsoft-UI-Reactor` under `Keywords.Errors` at `Warning`
+in Release (zero allocation when no consumer is attached) *and*
+mirrors a richer line including `ex.Message` to `Debug.WriteLine`
+in Debug builds. The narrow `catch` filter is the deliberate part:
+spec 044 §6.7.2 calls for `catch (COMException) when (ex.HResult is
+HResults.X or HResults.Y)` — never a bare `catch (COMException)`,
+because the bug-class HRESULTs need to keep propagating.
+
+### Capturing without pinning the level
+
+```
+DOTNET_EventPipeConfig=Microsoft-UI-Reactor
+```
+
+Defaults to `Verbose` plus all keywords. A typical 30-second session
+on a busy app writes hundreds of megabytes — and `State` keyword
+events fire once per `UseState` write, so a state-heavy screen
+becomes the entire trace. Pin both:
+
+```
+DOTNET_EventPipeConfig=Microsoft-UI-Reactor:0x1FA0:5
+```
+
+`0x1FA0` is `Errors | Hosting | Persistence | Navigation | Intl |
+Theme` — the everything-unsurprising mask. `:5` is `Verbose`. The
+trace shrinks by an order of magnitude.
+
+### Computing the diagnostic payload outside the IsEnabled gate
+
+```csharp snippet="source:src/Reactor/Core/Diagnostics/DiagnosticLog.cs#swallowed-error-shape"
+```
+
+`DiagnosticLog.SwallowedError` does its `category.ToString()` and
+`ex.GetType().Name` work *inside* the
+`ReactorEventSource.Log.IsEnabled(...)` gate, not outside. The
+distinction is the entire point of the "zero-allocation when no
+consumer is attached" guarantee. If a future helper materializes
+the payload first and gates second, the no-allocation regression
+test (`DisabledKeyword_skips_ReactorEventSource_WriteEvent_payload_marshal`)
+catches it. The companion `HResultFailed` event has the same shape:
+
+```csharp snippet="source:src/Reactor/Core/Diagnostics/ReactorEventSource.cs#hresult-failed-event"
+```
+
+### Forwarding `ex.Message` through `ReactorTrace.Subscribe`
+
+```csharp
+// Don't — capturing the exception and forwarding its message defeats the strip:
+Exception? lastEx = null;
+try { /* ... */ }
+catch (Exception caught)
+{
+ lastEx = caught;
+ DiagnosticLog.SwallowedError(LogCategory.Hosting, "MyOp", caught);
+}
+
+ReactorTrace.Subscribe(evt =>
+{
+ // BAD: re-injects PII that the ETW payload deliberately excluded.
+ _logger.Warn(evt.EventName + ": " + string.Join(",", evt.Payload) + " " + lastEx?.Message);
+});
+```
+
+The framework already stripped `ex.Message` from the payload —
+re-adding it from a captured local is exactly the PII leak the strip
+prevented. If a sink needs the message, log it inside the `catch`
+block (where the exception is in scope and the sink's ACL applies),
+not at the `ReactorTrace.Subscribe` boundary.
+
+## Tips
+
+**`reactor.logs source=event` is the fastest read.** Inside a
+devtools session, calling the MCP tool returns the last N events
+instantly without spinning up a `dotnet-trace` capture. Use the
+env-var route when you need to hand a file to someone else; use the
+MCP tool when you're sitting in front of the running app.
+
+**The keyword mask is the audience pre-filter.** Subscribing on
+`Keywords.Errors` alone is dramatically cheaper than `(-1)` because
+the framework's hot reconcile / render paths drop their `IsEnabled`
+check immediately. A long-lived broad subscription raises the cost
+of every hot-path call site on the framework for as long as it
+lives.
+
+**HR fields are `0x{X8}`-rendered in `reactor.logs` text.** The MCP
+tool's text rendering recognizes payload field names `hr`,
+`hresult`, and `hwnd` and formats them as 8-digit uppercase hex.
+This matches the pre-migration `Debug.WriteLine` shape so existing
+log greps keep working.
+
+## Next Steps
+
+- **[Perf Instrumentation](perf-instrumentation.md)** — The emission pipeline, keyword design, and the IsEnabled gate. Read first if you're adding events.
+- **[DevTools Internals](devtools-internals.md)** — The MCP server and `logs` tool plumbing the diagnostic events flow through.
+- **[Persistence](persistence.md)** — Where `PersistenceRead` / `PersistenceWrite` / `PersistenceRejected` fire from.
+- **[Navigation](navigation.md)** — The route lifecycle that emits the Navigation-keyword events.
diff --git a/docs/_pipeline/templates/perf-instrumentation.md.dt b/docs/_pipeline/templates/perf-instrumentation.md.dt
index 81a46f9eb..828979fa7 100644
--- a/docs/_pipeline/templates/perf-instrumentation.md.dt
+++ b/docs/_pipeline/templates/perf-instrumentation.md.dt
@@ -249,6 +249,7 @@ in the band's event payload.
## Next Steps
+- **[Diagnostics](diagnostics.md)** — How to capture, filter, and read the error / HR / subsystem events Reactor emits in Release builds, plus `ReactorTrace.Subscribe` and `reactor.logs source=event`.
- **[DevTools Internals](devtools-internals.md)** — Where the overlays consume these events.
- **[Reconciliation](reconciliation.md)** — What the reconcile-stop counters describe.
- **[Hooks Internals](hooks-internals.md)** — Why `StateChange` fires from inside the slot table.
diff --git a/docs/guide/diagnostics.md b/docs/guide/diagnostics.md
new file mode 100644
index 000000000..770322b1b
--- /dev/null
+++ b/docs/guide/diagnostics.md
@@ -0,0 +1,404 @@
+
+When Reactor swallows an exception, returns past an HRESULT, or
+otherwise chooses to continue rather than throw, the framework has
+historically dropped a `Debug.WriteLine` and moved on. That worked for
+the contributor — the message landed in Visual Studio's Output window
+during a Debug build — but it disappeared in Release, which is the
+configuration every shipped app runs. Spec 044 fixed that: error and
+HRESULT diagnostics now route through the `Microsoft-UI-Reactor`
+`EventSource` (release-visible, keyword-gated, zero-allocation when
+no consumer is listening), and the existing in-process devtools tool
+(`reactor.logs`) was extended so an MCP agent can read framework
+events in the same call that returns stdout / stderr / debug output.
+
+# Diagnostics
+
+This page is about reading Reactor's diagnostics from a real app —
+not about adding new events. The emission pipeline (keywords,
+IsEnabled gate, event-id allocation, EventPipe vs ETW transport split)
+lives in [perf-instrumentation.md](perf-instrumentation.md); start
+there if you are extending the provider.
+
+
+
+## The rule
+
+Audience, not severity, decides the channel. `Debug.WriteLine`
+exists for the contributor working on the framework itself; the
+target audience is "someone reading the Output window in Visual
+Studio with their checkout open". `ReactorEventSource` exists for
+the app developer, the SRE, and the support engineer; the target
+audience is "someone who runs the shipped binary and needs to know
+why a window failed to open".
+
+| Audience | Channel | Visible in Release? |
+|---|---|---|
+| Framework contributor | `Debug.WriteLine`, `Debug.Assert` | No |
+| App developer / SRE | `Microsoft-UI-Reactor` `EventSource` | Yes |
+| Unreachable code | `throw new UnreachableException(...)` | Yes (as a crash) |
+
+The two channels are complementary, not redundant. A swallowed
+exception in `RenderContext` emits both — the typed event for the
+app developer and a richer `Debug.WriteLine` mirror (with the
+exception message) for the contributor running a Debug build. The
+mirror is `[Conditional("DEBUG")]` and compiles out in Release.
+
+> **Caveat:** Exception messages are PII-shaped and never reach the ETW payload.
+> `ex.Message` can carry absolute paths, environment values, partial
+> form values, and the bound user data that caused the failure. The
+> typed event payload carries the exception *type* only
+> (`InvalidOperationException`, `COMException`); a same-UID
+> `dotnet-trace` consumer sees the type and nothing else. If you need
+> the message in your own logs, attach an in-process subscriber (see
+> [`ReactorTrace.Subscribe`](#in-process-subscription) below) and
+> forward to a sink under your own ACL.
+
+## What's instrumented
+
+The provider's events split across a small set of keywords; spec 044
+adds six subsystem keywords on top of the seven the perf-instrumentation
+page documents. Pick the bits that match what you're triaging:
+
+| Keyword | Bit | Covers |
+|---|---|---|
+| `Errors` | `0x20` | Generic `SwallowedError` / `HResultFailed`, plus `RenderError` |
+| `Hosting` | `0x80` | `WindowOpened`, `WindowClosed`, `WindowDpiChanged`, `BackdropMaterializationFailed` |
+| `Persistence` | `0x100` | `PersistenceRead`, `PersistenceWrite`, `PersistenceRejected` |
+| `Navigation` | `0x200` | `NavigationRequested`, `NavigationCompleted`, `NavigationCancelled`, cache hit/miss/evict, transitions, deep-link |
+| `Intl` | `0x400` | `IntlMissingKey` |
+| `Theme` | `0x800` | `ThemeApplyFailed` |
+| `Shell` | `0x1000` | `JumpList*` / `ThumbnailToolbar*` / `Tray*` (planned) |
+
+Combine bits with bitwise-or. The most common capture-everything-
+unsurprising mask is `0x1FA0` (`Errors | Hosting | Persistence |
+Navigation | Intl | Theme`) — drops the verbose `State` and
+`EventDispatch` keywords that produce per-state-write spam.
+
+## Capturing a trace
+
+There are four routes. Pick by where the consumer lives.
+
+### Environment variables — zero code, file output
+
+For a quick local capture with no app changes, the .NET runtime can
+write an EventPipe `.nettrace` file driven entirely by environment
+variables. This is the right tool when an issue reproduces only in a
+shipped build, on a different machine, or when you want to hand the
+trace off to someone else:
+
+```
+set DOTNET_EnableEventPipe=1
+set DOTNET_EventPipeOutputPath=reactor.nettrace
+set DOTNET_EventPipeConfig=Microsoft-UI-Reactor:0x1FA0:5
+MyApp.exe
+```
+
+The third variable is `::`. `0x1FA0` is
+the everything-unsurprising mask above; level `5` is `Verbose`. Run
+the app, reproduce the issue, exit cleanly (the runtime flushes the
+file on shutdown). Open the resulting `.nettrace` in Visual Studio's
+Performance Profiler → Events Viewer.
+
+### `dotnet-trace` — attach to a running process
+
+`dotnet-trace` is the cross-platform CLI for EventPipe capture.
+Useful when the app is already running and you want to scope the
+window:
+
+```
+dotnet-trace collect ^
+ --process-id ^
+ --providers Microsoft-UI-Reactor:0x1FA0:5 ^
+ --output reactor.nettrace
+```
+
+`Ctrl+C` stops the session; the `.nettrace` lands in the working
+directory. Same file format as the env-var route — same Events
+Viewer workflow.
+
+### Visual Studio Performance Profiler
+
+For a GUI workflow, the Profiler's Events Viewer accepts the same
+provider:keyword:level format. Diagnostics → Performance Profiler
+→ Events Viewer → Settings → Custom Provider:
+
+```
+Microsoft-UI-Reactor:0x1FA0:5
+```
+
+The timeline ties each Reactor event to the CPU sample, GC, and
+network views, so you can see a `NavigationCompleted` next to the
+allocation spike it caused.
+
+### In-process subscription
+
+When the consumer is the app itself — a custom log sink, a devtools
+overlay, an in-app diagnostics page — `ReactorTrace.Subscribe`
+returns an `IDisposable` token that fires the callback for each
+event matching the filter:
+
+```csharp
+public static IDisposable Subscribe(
+ Action onEvent,
+ EventLevel level = EventLevel.Verbose,
+ EventKeywords keywords = (EventKeywords)(-1))
+{
+ ArgumentNullException.ThrowIfNull(onEvent);
+ return new Subscription(onEvent, level, keywords);
+}
+```
+
+Multiple concurrent subscribers are supported; each filter is
+independently active until the token is disposed. The subscriber
+callback runs on the emission thread (usually the UI dispatcher when
+the event originates from reconcile / render), so keep the work
+minimal — the framework wraps the call in `try/catch` so a buggy
+sink can't propagate to `EventSource.WriteEvent`, but the dispatcher
+is still blocked for the duration. Forward to a queue if your sink
+does anything expensive.
+
+`ReactorTrace.Subscribe` is **not** a file-capture API. It exists
+because in-process consumers (devtools, an `ILogger` adapter, a
+custom diagnostics page) need access to the same events the env-var
+route writes to disk. For a `.nettrace` file, use one of the three
+routes above — they cost less and emit a richer format.
+
+## `reactor.logs source=event` — MCP integration
+
+The Reactor in-process devtools (`mur devtools`) expose a `logs` MCP
+tool that drains the captured `Console.Out` / `Console.Error` /
+`Debug.WriteLine` ring buffer. Spec 044 extended the tool so the
+buffer also captures `Microsoft-UI-Reactor` ETW events, surfaced
+under a new `source=event` filter:
+
+```jsonc
+// Request
+{
+ "method": "tools/call",
+ "params": {
+ "name": "logs",
+ "arguments": { "source": "event", "tail": 20, "level": "Warning" }
+ }
+}
+
+// Response — each entry now carries eventName / eventId
+{
+ "entries": [
+ {
+ "seq": 142,
+ "ts": "2026-05-19T17:42:11.330Z",
+ "source": "event",
+ "level": "Warning",
+ "text": "SwallowedError category=Hosting operation=AppWindow.Close exceptionType=COMException",
+ "eventName": "SwallowedError",
+ "eventId": 16
+ }
+ ],
+ "nextSeq": 143,
+ "dropped": 0
+}
+```
+
+Existing clients that don't pass `source=event` see zero behavior
+change — the `stdout` / `stderr` / `debug` filters still return their
+dedicated streams. The `eventName` and `eventId` fields are present
+on every entry but are `null` for non-event sources, so a client
+written before spec 044 ignores them safely.
+
+HR-style payload fields render in the same `0x{X8}` shape the
+pre-migration `Debug.WriteLine` sites used (`HResultFailed
+category=Shell operation=JumpList.Begin hr=0x80004002`), so log
+greps that matched the old shape continue to hit.
+
+## Patterns
+
+### Reading a swallow that happened in production
+
+A customer reports a window that won't close cleanly on a specific
+machine. Capture with the env-var route, filter to the `Errors`
+keyword (`0x20`) at `Warning`, and open the trace in Events Viewer.
+The relevant entries will look like:
+
+```
+SwallowedError category=Hosting operation=AppWindow.Close exceptionType=COMException
+HResultFailed category=Hosting operation=AppWindow.Close hr=0x80004005
+```
+
+The operation label is stable and developer-authored — search the
+Reactor source for `"AppWindow.Close"` and you land on the
+`DiagnosticLog.SwallowedError` call site. The exception type and HR
+together pin the failure class (in this case, `E_FAIL` from the
+WinUI AppWindow lifecycle) without ever leaking the user-visible
+window title.
+
+### Wiring `ReactorTrace.Subscribe` to a per-window debug overlay
+
+A devtools overlay that wants to surface "navigation event happening
+now" doesn't need a file capture — just an in-process subscription:
+
+```csharp
+public sealed class NavigationOverlay : IDisposable
+{
+ private readonly IDisposable _subscription;
+ private readonly Queue _ring = new();
+
+ public NavigationOverlay()
+ {
+ _subscription = ReactorTrace.Subscribe(
+ evt =>
+ {
+ if ((evt.Keywords & ReactorEventSource.Keywords.Navigation) == 0) return;
+ var line = $"{evt.EventName} {string.Join(' ',
+ Enumerable.Range(0, evt.Payload.Count)
+ .Select(i => $"{evt.PayloadNames[i]}={evt.Payload[i]}"))}";
+ lock (_ring)
+ {
+ _ring.Enqueue(line);
+ while (_ring.Count > 50) _ring.Dequeue();
+ }
+ },
+ level: EventLevel.Verbose,
+ keywords: ReactorEventSource.Keywords.Navigation);
+ }
+
+ public void Dispose() => _subscription.Dispose();
+}
+```
+
+The callback runs on the dispatcher (most navigation events
+originate there) — for a UI overlay that's actually what you want.
+If the overlay forwards to a background sink instead, marshal off
+the dispatcher before doing the I/O.
+
+## Common Mistakes
+
+### Treating `Debug.WriteLine` as a release diagnostic
+
+```csharp
+// Don't:
+try { window.AppWindow.Close(); }
+catch (Exception ex)
+{
+ Debug.WriteLine($"Close failed: {ex}"); // disappears in Release
+}
+```
+
+```csharp
+// Do:
+try { window.AppWindow.Close(); }
+catch (COMException ex) when (ex.HResult is HResults.RPC_E_DISCONNECTED or HResults.E_FAIL)
+{
+ DiagnosticLog.SwallowedError(LogCategory.Hosting, "AppWindow.Close", ex);
+}
+```
+
+The first form was invisible to every shipped app. The second form
+emits to `Microsoft-UI-Reactor` under `Keywords.Errors` at `Warning`
+in Release (zero allocation when no consumer is attached) *and*
+mirrors a richer line including `ex.Message` to `Debug.WriteLine`
+in Debug builds. The narrow `catch` filter is the deliberate part:
+spec 044 §6.7.2 calls for `catch (COMException) when (ex.HResult is
+HResults.X or HResults.Y)` — never a bare `catch (COMException)`,
+because the bug-class HRESULTs need to keep propagating.
+
+### Capturing without pinning the level
+
+```
+DOTNET_EventPipeConfig=Microsoft-UI-Reactor
+```
+
+Defaults to `Verbose` plus all keywords. A typical 30-second session
+on a busy app writes hundreds of megabytes — and `State` keyword
+events fire once per `UseState` write, so a state-heavy screen
+becomes the entire trace. Pin both:
+
+```
+DOTNET_EventPipeConfig=Microsoft-UI-Reactor:0x1FA0:5
+```
+
+`0x1FA0` is `Errors | Hosting | Persistence | Navigation | Intl |
+Theme` — the everything-unsurprising mask. `:5` is `Verbose`. The
+trace shrinks by an order of magnitude.
+
+### Computing the diagnostic payload outside the IsEnabled gate
+
+```csharp
+public static void SwallowedError(LogCategory category, string operation, Exception ex)
+{
+ // Cost-of-disabled: when no consumer enables Keywords.Errors at
+ // Warning the entire branch is skipped — no enum-to-string, no
+ // type-name materialization, no WriteEvent dispatch.
+ if (ReactorEventSource.Log.IsEnabled(EventLevel.Warning, ReactorEventSource.Keywords.Errors))
+ {
+ ReactorEventSource.Log.SwallowedError(
+ category.ToString(),
+ operation ?? string.Empty,
+ ex?.GetType().Name ?? string.Empty);
+ }
+
+ DebugSwallowedError(category, operation, ex);
+}
+```
+
+`DiagnosticLog.SwallowedError` does its `category.ToString()` and
+`ex.GetType().Name` work *inside* the
+`ReactorEventSource.Log.IsEnabled(...)` gate, not outside. The
+distinction is the entire point of the "zero-allocation when no
+consumer is attached" guarantee. If a future helper materializes
+the payload first and gates second, the no-allocation regression
+test (`DisabledKeyword_skips_ReactorEventSource_WriteEvent_payload_marshal`)
+catches it. The companion `HResultFailed` event has the same shape:
+
+```csharp
+[Event(17, Level = EventLevel.Warning, Keywords = Keywords.Errors,
+ Message = "HResult failed (category={category}, op={operation}, hr=0x{hr:X8})")]
+public void HResultFailed(string category, string operation, int hr)
+{
+ if (IsEnabled(EventLevel.Warning, Keywords.Errors))
+ WriteEvent(17, category ?? string.Empty, operation ?? string.Empty, hr);
+}
+```
+
+### Forwarding `ex.Message` through `ReactorTrace.Subscribe`
+
+```csharp
+// Don't:
+ReactorTrace.Subscribe(evt =>
+{
+ _logger.Warn(evt.EventName + ": " + string.Join(",", evt.Payload) + " " + ex.Message);
+});
+```
+
+The framework already stripped `ex.Message` from the payload —
+re-adding it from a captured local is exactly the PII leak the strip
+prevented. If a sink needs the message, build it inside the `catch`
+block (where the message is in scope and the sink's ACL applies),
+not at the `ReactorTrace.Subscribe` boundary.
+
+## Tips
+
+**`reactor.logs source=event` is the fastest read.** Inside a
+devtools session, calling the MCP tool returns the last N events
+instantly without spinning up a `dotnet-trace` capture. Use the
+env-var route when you need to hand a file to someone else; use the
+MCP tool when you're sitting in front of the running app.
+
+**The keyword mask is the audience pre-filter.** Subscribing on
+`Keywords.Errors` alone is dramatically cheaper than `(-1)` because
+the framework's hot reconcile / render paths drop their `IsEnabled`
+check immediately. A long-lived broad subscription raises the cost
+of every hot-path call site on the framework for as long as it
+lives.
+
+**HR fields are `0x{X8}`-rendered in `reactor.logs` text.** The MCP
+tool's text rendering recognizes payload field names `hr`,
+`hresult`, and `hwnd` and formats them as 8-digit uppercase hex.
+This matches the pre-migration `Debug.WriteLine` shape so existing
+log greps keep working.
+
+## Next Steps
+
+- **[Perf Instrumentation](perf-instrumentation.md)** — The emission pipeline, keyword design, and the IsEnabled gate. Read first if you're adding events.
+- **[DevTools Internals](devtools-internals.md)** — The MCP server and `logs` tool plumbing the diagnostic events flow through.
+- **[Persistence](persistence.md)** — Where `PersistenceRead` / `PersistenceWrite` / `PersistenceRejected` fire from.
+- **[Navigation](navigation.md)** — The route lifecycle that emits the Navigation-keyword events.
diff --git a/docs/guide/images/diagnostics/flow.svg b/docs/guide/images/diagnostics/flow.svg
new file mode 100644
index 000000000..8b685673f
--- /dev/null
+++ b/docs/guide/images/diagnostics/flow.svg
@@ -0,0 +1,90 @@
+
+
diff --git a/docs/specs/044/swallowed-error-audit.md b/docs/specs/044/swallowed-error-audit.md
new file mode 100644
index 000000000..da3ba5180
--- /dev/null
+++ b/docs/specs/044/swallowed-error-audit.md
@@ -0,0 +1,253 @@
+# Swallowed-error audit — spec 044
+
+Companion to [`docs/specs/044-tracing-and-logging-cleanup-design.md`](../044-tracing-and-logging-cleanup-design.md)
+and the implementation task list at [`docs/specs/tasks/044-tracing-and-logging-cleanup-implementation.md`](../tasks/044-tracing-and-logging-cleanup-implementation.md).
+
+This file is the permanent record of the spec §6.7 decision Reactor
+made at each `catch (Exception ex) { Debug.WriteLine(...); }` site
+when the framework's diagnostics surfaces were migrated from
+contributor-only `Debug.WriteLine` to release-visible
+`Microsoft-UI-Reactor` ETW events. Every entry maps a site to one of
+five verdicts:
+
+- **Keep** — broad catch is correct (user-callback isolation, dispose
+ best-effort, COM-API quirk where the failure class is large and not
+ yet narrowed). Replace `Debug.WriteLine` with `DiagnosticLog.SwallowedError`
+ but leave the catch shape alone.
+- **Narrow** — the catch should filter on a specific exception type
+ and/or HRESULT range (`catch (COMException ex) when (ex.HResult is
+ HResults.X or HResults.Y)`). Anything outside the filter propagates.
+- **Propagate** — the catch should be deleted; this is a bug-class
+ failure that the caller needs to see.
+- **Replace with `TryXxx`** — the call site has a `bool TryX(out
+ result)` shape underneath; thread the return code instead of
+ catching.
+- **Promote to typed event** — the diagnostic graduates to a
+ subsystem-specific `ReactorEventSource` event (e.g.
+ `JumpListSaveFailed(int hr)`).
+
+Each entry also names the migration commit so the verdict is
+auditable against the working code.
+
+> **Scope discipline.** The spec scope (§44 task doc preamble) is
+> *the minimum change required to make Reactor's release-build
+> diagnostics visible to app developers*. The Keep migration alone
+> delivers that — every error/HR-reporting `Debug.WriteLine` in
+> `src/Reactor/` now routes through `DiagnosticLog` and lands on the
+> ETW surface. The Narrow / Propagate / Replace-with-TryXxx /
+> Promote-to-typed-event verdicts are followups that gate on a
+> subsystem subject-matter review.
+
+---
+
+## Verdict distribution (current state)
+
+| Verdict | Count | Shipped | Deferred |
+|---|---|---|---|
+| Keep (iteration sibling-independence) | 8 | 8 | — |
+| Narrow (specific exception type / HR filter) | 36 | 33 | 3 (Shell HResultFailed already narrowed; typed-event promotion deferred) |
+| Propagate (no catch — user / framework bug surfaces) | 12 | 12 | — |
+| Replace with `TryXxx` | 10 | 0 | 10 (Win32 P/Invoke reporters, Phase 4.8) |
+| Promote to typed event | 18 | 9 (Navigation 6 + Intl 1 + Persistence 2) | 9 (Shell COM-calls 5 + ConnectedAnimation 4, Phase 4.6) |
+| Deleted (dead-defensive try/catch) | 9 | 9 | — |
+
+Spec §6.7.4 worry-threshold for `Propagate` is 20; we're at 12.
+
+The dramatic shift from "56 Keep" in the first audit pass to "8 Keep + 12 Propagate + 9 Deleted + 33 Narrow" came from applying the §6.7.2 narrowing properly to ReactorWindow.cs (29 sites) and the related Hosting code. The first pass migrated `Debug.WriteLine` → `DiagnosticLog.SwallowedError` with the catch shape unchanged ("Keep"); the second pass actually applied the §6.7.2 rule that broad `catch (Exception)` is wrong almost everywhere it isn't iteration sibling-independence or genuine fail-safe-to-default behavior.
+
+---
+
+## Method
+
+Every site listed below was inspected against the template in spec
+§6.7.1. For Keep verdicts, the template is collapsed to the audit
+trail (site → migration commit) because every Keep entry shares the
+same justification: "the surrounding code is a best-effort
+operation whose semantics are 'do this if possible, otherwise
+continue' — propagation would crash the dispatcher". For Narrow /
+Promote / TryXxx verdicts, the per-site context is included.
+
+---
+
+## File-grouped sites
+
+Sites are grouped by source file in alphabetical order, matching
+the inventory in §3.3 of the task doc.
+
+### `src/Reactor/Core/Localization/IntlAccessor.cs` — Phase C.3 (commit `7312ce73`)
+
+| Site | Verdict | Notes |
+|---|---|---|
+| `ResolvePattern` missing-key (×2 collapsed into 1) | Promote to typed event | `IntlMissingKey(key, locale, fellBack)` under `Keywords.Intl`. Previous shape double-logged the no-fallback-available case; new shape emits once. PII: key is developer-authored .resw identifier. |
+| `Message` format failure | Keep + DiagnosticLog | `LogCategory.Intl` — the failure could be malformed pattern data, which is contributor-shaped not user-shaped. |
+| `RichMessage` format failure | Keep + DiagnosticLog | Same as above. |
+
+### `src/Reactor/Core/Navigation/NavigationDiagnostics.cs` — Phase C.2 (commit `e2a755b2`)
+
+| Site | Verdict | Notes |
+|---|---|---|
+| `OnNavigationRequested` | Promote to typed event | `NavigationRequested(routeTemplate)` under `Keywords.Navigation`. |
+| `OnNavigationCompleted` | Promote to typed event | `NavigationCompleted(routeTemplate, durationMs)`. |
+| `OnNavigationCancelled` | Promote to typed event | `NavigationCancelled(routeTemplate, reason)`. |
+| `OnNavigationCacheHit` | Promote to typed event | `NavigationCacheHit(routeTemplate)`. Verbose-level. |
+| `OnNavigationCacheMiss` | Promote to typed event | `NavigationCacheMiss(routeTemplate)`. Verbose-level. |
+| `OnNavigationCacheEviction` | Promote to typed event | `NavigationCacheEvict(routeTemplate, reason)`. Verbose-level. |
+| `OnTransitionStarted` | Promote to typed event | `NavigationTransitionStarted(routeTemplate)` (new event id 33). |
+| `OnTransitionCompleted` | Promote to typed event | `NavigationTransitionCompleted(routeTemplate, durationMs)` (id 34). |
+| `OnDeepLinkResolved` | Promote to typed event | `NavigationDeepLinkResolved(matched, routeCount)` (id 35). **PII (§6.2.1):** the raw `path` is attacker-controllable; the typed event emits `matched` + `routeCount` only. The `NavigationDiagnosticsEtwBridgeTests.OnDeepLinkResolved_match_emits_outcome_only_no_path` regression guard pins this. |
+
+### `src/Reactor/Core/Persistence/JsonFileStore.cs` — Phase C.5 (commit `21e22e1c`)
+
+| Site | Verdict | Notes |
+|---|---|---|
+| Round-trip read success | Promote to typed event | Emits `PersistenceRead(storeKind: "json-file", sizeBytes)`. Storekind label — never the path (§6.2.1). |
+| Round-trip write success | Promote to typed event | Emits `PersistenceWrite(...)`. Same PII discipline. |
+| Read oversize | Promote to typed event | Emits `PersistenceRejected(storeKind, reason: "oversize")`. |
+| Read narrow exceptions (`IOException`, `JsonException`, `FormatException`, `UnauthorizedAccessException`) | Narrow + DiagnosticLog | `catch (IOException) / catch (JsonException) ...` instead of `catch (Exception)`. Surprise exceptions now propagate — a `NullReferenceException` from a malformed deserializer should crash, not silently load defaults. |
+| Write narrow exceptions | Narrow + DiagnosticLog | Same shape. |
+
+### `src/Reactor/Core/Persistence/PackagedSettingsStore.cs` — Phase C.5
+
+| Site | Verdict | Notes |
+|---|---|---|
+| Read narrow exceptions (`InvalidOperationException`, `COMException`, `UnauthorizedAccessException`) | Narrow + DiagnosticLog | The WinRT call surface throws `InvalidOperationException` (HR `0x80073D54`) on every unpackaged process; that's the actual failure class here, not `IOException`/`JsonException` as the spec's draft list said. Storekind `"packaged-settings"`. |
+| Write narrow exceptions (+ `FormatException` on the base64 path) | Narrow + DiagnosticLog | Same. |
+
+### `src/Reactor/Core/Persistence/WindowPlacementCodec.cs` — Phase C.5
+
+| Site | Verdict | Notes |
+|---|---|---|
+| Win32 `GetWindowPlacement` failure | Promote to typed event | `DiagnosticLog.HResultFailed(LogCategory.Persistence, ..., GetLastError())`. |
+| `IsPlausiblePlacement` reject | Promote to typed event | `PersistenceRejected("placement", reason)` with a short reason label. The raw rect / showCmd is deliberately NOT on the payload (would fingerprint multi-monitor layouts, §6.2.1). |
+| `monitorCount` reject | Promote to typed event | Same. |
+| `EndOfStreamException` reject | Promote to typed event | Same. |
+| Outer catches | Narrow + DiagnosticLog | Narrowed to `IOException`. |
+
+### `src/Reactor/Core/Reconciler.cs` — Phase C.7b (commit `054c53ef`) + Phase C.8 (commit `21cd6ef9`)
+
+| Site | Verdict | Notes |
+|---|---|---|
+| Navigation lifecycle callback dispatch | Keep + DiagnosticLog | User-callback isolation per §6.7.3. Already shipped in C.7b. |
+| ConnectedAnimation `PrepareToAnimate` (mount path) | Keep + DiagnosticLog | LogCategory.Reactor. §6.7.4 calls for "Promote + Narrow" — deferred along with the rest of 4.6. |
+| ConnectedAnimation `PrepareToAnimate` (update path) | Keep + DiagnosticLog | Same. |
+| ConnectedAnimation `GetAnimation` | Keep + DiagnosticLog | Same. |
+| ConnectedAnimation `TryStart` | Keep + DiagnosticLog | Same. |
+| `ApplyThemeBindings` | Keep + DiagnosticLog | LogCategory.Theme — the catch wraps a XAML `Style.Load` compile. Could narrow to `XamlParseException` in a follow-up. |
+
+### `src/Reactor/Core/Reconciler.Mount.cs` — Phase C.8 (commit `21cd6ef9`)
+
+| Site | Verdict | Notes |
+|---|---|---|
+| `ContentDialog.ShowAsync + OnClosed` | Keep + DiagnosticLog | User-callback isolation per §6.7.3 — the try wraps both `ShowAsync` AND the user-supplied `OnClosed` delegate. Cannot narrow without splitting the try-catch into two; deferred. |
+
+### `src/Reactor/Core/RenderContext.cs` — Phase C.6 (commit `90d516b0`) + Phase C.9 narrowing
+
+| Site | Verdict | Notes |
+|---|---|---|
+| `UseCommand.ExecuteAsync` | **Narrow (try/finally — no catch)** | Phase C.9: fire-and-forget `Task.Run` wraps the user action with `try { await asyncAction(); } finally { guardRef.Current = false; setIsExecuting(false); }`. The framework state is restored before unwind; the user's throw faults the Task and surfaces via `Task.UnobservedTaskException` rather than being swallowed under `SwallowedError`. The earlier "Keep + DiagnosticLog" shape was hiding user bugs — apps couldn't tell their command was broken without subscribing to ETW. |
+| `UseCommand.ExecuteAsync` | **Narrow (try/finally — no catch)** | Same shape. |
+| `UseEffect` cleanup (FlushEffects phase 1) | Keep + DiagnosticLog | Iteration sibling-independence — slot i's failure must not block slots i+1…n in the same flush. The loop's invariant (forward progress through all cleanups) requires the broad catch. |
+| `UseEffect` effect (FlushEffects phase 2) | Keep + DiagnosticLog | Same. |
+| `RunCleanups.effectCleanup` | Keep + DiagnosticLog | Same. |
+| `RunCleanups.persistedSave` | Keep + DiagnosticLog | Same — persisted-slot independence. The try-catch wraps the user contact point (`IPersistedStateScope.Set`); the surrounding hook-iteration loop is outside. |
+
+### `src/Reactor/Hosting/Etw/LayoutEtwConsumer.cs` — Phase C.7a (commit `b761a7a1`)
+
+| Site | Verdict | Notes |
+|---|---|---|
+| 7 error-swallow catches (provider start, session enable, parser, etc.) | Keep + DiagnosticLog | LogCategory.LayoutCost. |
+| 5 pure-trace `Debug.WriteLine` (session started / parser output / orphan cleanup) | Keep as `Debug.WriteLine` | Framework-internal per spec §6.3 carve-out. |
+
+### `src/Reactor/Hosting/ReactorWindow.cs` — Phase C.8 (commit `21cd6ef9`) + Phase C.9 narrowing
+
+Phase C.8 migrated 29 `Debug.WriteLine` → `DiagnosticLog` with the catch
+shape unchanged. Phase C.9 applies the actual §6.7.2 narrowing per site:
+
+| Group | Sites | Verdict | After |
+|---|---|---|---|
+| Pure-advisory user callbacks | `SizeChanged`, `StateChanged`, `Closing` | **Propagate** — try/catch deleted | User throw goes to dispatcher's UnhandledException; developer sees the bug. Previous swallow silently treated thrown `Closing` handler as "didn't cancel," which was worse than crashing. |
+| User callback with framework cleanup after | `Closed?.Invoke` | **try/finally** | User throw propagates AND `RemoveOwned` / `UnregisterWindow` / `Dispose` still run. Handles the limp-along case where the app set `Application.UnhandledException.Handled = true`. |
+| WinUI AppWindow / Window API surface | `Title.set`, `Presenter.apply`, `IsShownInSwitchers.set`, `ExtendsContentIntoTitleBar.set`, `InitialResize`, `SetOwner`, `FirstDpiResize`, `Hide`, `Show`, `Close`, `SetSize`, `SetPosition`, `CenterOnScreen`, `ResolveCurrentState`, `TryApplyExeIconFallback`, `TryApplyInitialPlacement`, `ResolveOwnerDisplayArea`, all five event unsubscriptions in `Dispose` (×5) | **Narrow** — `catch (COMException ex) when (HResults.IsTeardownReentry(ex.HResult))` (the well-known `RPC_E_DISCONNECTED` / `E_HANDLE` / `RPC_E_SERVERFAULT` / `CO_E_OBJNOTCONNECTED` set) | Anything outside that HR set propagates as a genuine bug. |
+| Iteration sibling-independence | `IClosingGuard.CanClose()`, owned-window-cascade `child._window.Close()` | **Keep + DiagnosticLog (annotated)** | Closing-guard fail-safe-to-cancel is documented behavior (spec 036 §3.4 test pins it); owned-cascade sibling independence is spec 036 §9. Both have inline comments naming the contract. |
+| Framework dispose chain | `_messageMonitor.Dispose()` → `_host.Dispose()` → `_persistedScope.Dispose()` → `_thumbnailToolbar?.Dispose()` | **try/finally chain** | All four disposes run regardless of which throws; first exception propagates. No swallowing — a framework Dispose bug should surface. |
+| Dead-defensive try/catch | `QueryDpiForWindow`, `WM_GETMINMAXINFO.apply`, `GetDpiForSystemFallback`, `NativeIcon.DestroyIcon`, `MonitorEnumeration.Snapshot`, `TryRestorePersistedPlacementCore`, `TrySavePersistedPlacement` | **Try deleted** | The wrapped operations are P/Invokes on `nint` that can't throw at the marshal layer, or downstream calls that already narrow internally and return sentinel values. The outer try/catch was hiding nothing real. |
+
+LogCategory.Hosting except for the two persistence-shaped placement
+sites (LogCategory.Persistence) and the user-event sites which now
+have no catch at all.
+
+### `src/Reactor/Hosting/Shell/JumpListComInterop.cs` — Phase C.4 (commit `301593bc`)
+
+| Site | Verdict | Notes |
+|---|---|---|
+| `BeginList`, `AddUserTasks`, `AppendCategory`, `AppendKnownCategory.Recent`, `AppendKnownCategory.Frequent`, `CommitList` (6 sites) | Promote to typed event (partial) | Shipped `DiagnosticLog.HResultFailed(LogCategory.Shell, "JumpList.", hr)`. The full "Promote" verdict (subsystem-specific `JumpListSaveFailed(hr)` event) is deferred to 4.6. |
+
+### `src/Reactor/Hosting/Shell/ThumbnailToolbar.cs` — Phase C.4
+
+| Site | Verdict | Notes |
+|---|---|---|
+| `Update vs Add Buttons` | Promote to typed event (partial) | Same shape — `HResultFailed` shipped, typed `ThumbnailToolbarSetButtonsFailed` deferred. |
+
+### `src/Reactor/Hosting/Shell/TrayFlyoutHostWindow.cs` — Phase C.4
+
+| Site | Verdict | Notes |
+|---|---|---|
+| `GetDpiForMonitor` | Promote to typed event (partial) | Same. |
+
+### `src/Reactor/Markdown/Md4cParser.Block.cs` — Phase C.1 (commit `79b27be6`)
+
+| Site | Verdict | Notes |
+|---|---|---|
+| 4 `Debug.Fail("Unreachable")` sites | Propagate (as `UnreachableException`) | Release-visible crash — these are genuine state-machine impossibilities. The Reconciler.cs site the spec mentions is intentionally skipped — it's not the same pattern (see task 4.1). |
+
+### `src/Reactor/Core/Reconciler.cs:2635` (typed-ref assert) — Skipped intentionally
+
+The spec §4.3 also mentioned 1 site in `Reconciler.cs:~2635`. Audit
+note: that site is not a `Debug.Fail("Unreachable")` — its message
+is `"ElementRef<{T}> attached to a {U}. Use ElementRef or
+untyped ElementRef."` — and the containing `AssertTypedRefMatch`
+method is already `[Conditional("DEBUG")]`. Leaving as-is until a
+reviewer requests a behavior change.
+
+---
+
+## Win32 P/Invoke `TryXxx` candidates — Deferred (Phase 4.8)
+
+Spec §6.7.4 calls for ~10 sites where `bool Try* (out int hr)` is
+the right shape. Each already returns a `bool`, so the conversion is
+mechanical:
+
+- `src/Reactor/Hosting/Messaging/WindowMessageMonitor.cs` — 6 P/Invoke wrappers.
+- `src/Reactor/Hosting/Persistence/MonitorEnumeration.cs` — 2 `EnumDisplayMonitors`-shape callers.
+- `src/Reactor/Hosting/WindowIcon.cs` — 2 HICON loaders.
+
+Audit verdict on each is *Replace with `TryXxx`*; none have shipped
+yet because the GetLastError path still needs the swallowed-error
+audit trail until the conversion lands. Tracked as task 4.8.
+
+---
+
+## Shell typed-event promotion — Deferred (Phase 4.6)
+
+Spec §6.7.4 calls for ~15 sites in the Shell namespace to graduate
+from the generic `HResultFailed` event to subsystem-specific typed
+events. The Phase C.4 migration shipped the `HResultFailed` shape
+for 8 of those, which delivers the release-visibility goal. The
+typed events (`JumpListSaveFailed(hr)`,
+`ThumbnailToolbarSetButtonsFailed(hr)`, etc.) are a downstream
+ergonomic improvement — an MCP agent filtering on
+`Keywords.Shell & EventName=JumpListSaveFailed` is more discoverable
+than greppping `operation="JumpList.Begin"` strings. Tracked as task
+4.6.
+
+---
+
+## Audit completeness against §3.5
+
+- [x] Every site in the §0.3 inventory maps to exactly one entry in
+ this file or is explicitly carved out as framework-internal
+ (`Debug.Assert`, pure trace prints).
+- [x] Verdict distribution recorded at the top.
+- [ ] Per-site line-by-line review by a second pair of eyes — invited
+ via the PR that introduces this file.
+- [x] No code changes in this PR — it's the audit's permanent home.
diff --git a/docs/specs/tasks/044-tracing-and-logging-cleanup-implementation.md b/docs/specs/tasks/044-tracing-and-logging-cleanup-implementation.md
new file mode 100644
index 000000000..c45aa95ba
--- /dev/null
+++ b/docs/specs/tasks/044-tracing-and-logging-cleanup-implementation.md
@@ -0,0 +1,450 @@
+# Tracing and Logging Cleanup — Implementation Tasks
+
+Derived from: `docs/specs/044-tracing-and-logging-cleanup-design.md`
+Tracking bug: [microsoft/microsoft-ui-reactor#323](https://github.com/microsoft/microsoft-ui-reactor/issues/323)
+
+> **Scope discipline.** The point of this work is **the minimum change required to make Reactor's release-build diagnostics visible to app developers.** The headline deliverables are: the `DiagnosticLog` helper, expanded `ReactorEventSource` coverage, the swallowed-error audit + migration, an in-process `EventListener` subscription helper, a `reactor.logs` MCP wire-up, and the docs page that ties it together. Everything else in spec 044 (sibling `ILogger` package, `IDevtoolsConsole` abstraction, Roslyn analyzer CI gate) is **deferred** — captured at the bottom under "Deferred / out of scope" so the next contributor knows it exists, but it does not block closing #323.
+
+Conventions:
+- Core diagnostics types live under `src/Reactor/Core/Diagnostics/` (new folder): `DiagnosticLog.cs`, `LogCategory.cs`, `HResults.cs`. `ReactorEventSource.cs` already exists under `src/Reactor/Core/` and stays there.
+- Public subscription API (`ReactorTrace`, `ReactorEvent`) lives under `src/Reactor/Diagnostics/` (new folder) to match the public namespace `Microsoft.UI.Reactor.Diagnostics`.
+- Unit tests under `tests/Reactor.Tests/Diagnostics/` (new folder). The `ReactorTraceCollector` test harness lives there too so production code stays free of it.
+- The audit lives at `docs/specs/044/swallowed-error-audit.md` (new folder under `docs/specs/`).
+- The user guide page is generated — edit `docs/_pipeline/templates/diagnostics.md.dt`, then run `mur docs compile`. Never hand-edit `docs/guide/diagnostics.md`.
+- Public API additions need XML doc comments (no `CS1591`).
+- Code must compile under `Reactor.slnx` warnings-as-errors **and** `IsAotCompatible=true` with trim warnings promoted to errors (the core Reactor library already enforces this — see spec §12).
+
+A task is "done" only when:
+1. Code compiles clean under `Reactor.slnx` warnings-as-errors and AOT/trim-warnings-as-errors.
+2. Public API surface has XML doc comments.
+3. Tests cover both the happy path and the documented failure modes for that task.
+4. `dotnet test tests/Reactor.Tests` and `dotnet test tests/Reactor.SelfTests` are green.
+
+---
+
+## Phase 0 — Decisions captured & scaffolding
+
+### 0.1 Resolve the spec's open questions before code starts
+
+- [x] Confirm **Q2** (sibling `Microsoft.UI.Reactor.Logging.Extensions` package): **deferred** — Phase E is out of scope for this implementation. Record the deferral in the spec header.
+- [x] Confirm **Q3** (`Debug.WriteLine` Roslyn analyzer): **deferred** — Phase C-gate is out of scope. Record the deferral in the spec header.
+- [x] Confirm **Q4** (`Trace.WriteLine` listener fate): **leave as-is** in `LogCaptureInstall`. Record in spec §10.
+- [x] Confirm the public namespace for `ReactorTrace` / `ReactorEvent`: `Microsoft.UI.Reactor.Diagnostics`. Spec §6.4 already implies this; commit it in the spec header so Phase D doesn't revisit.
+
+### 0.2 New files — empty placeholders compile first, populated later
+
+- [x] Create `src/Reactor/Core/Diagnostics/` folder and `LogCategory.cs` with the enum from spec §6.1 (`Reactor, Hosting, Persistence, Navigation, Intl, Theme, Shell, LayoutCost, Devtools, Markdown`).
+- [x] Create `src/Reactor/Core/Diagnostics/HResults.cs` with the named constants from spec §6.7 (start with the ones listed; grow during audit).
+- [x] Create `src/Reactor/Core/Diagnostics/DiagnosticLog.cs` with method signatures only (`SwallowedError`, `HResultFailed`) — bodies wired in 1.1.
+- [x] Create `src/Reactor/Diagnostics/` folder and `ReactorTrace.cs` + `ReactorEvent.cs` shells with the public API surface from spec §6.4 (signatures + XML doc, body in Phase D).
+- [x] Verify `Reactor.slnx` builds clean with these placeholders.
+
+### 0.3 Audit baseline — inventory every site touched by Phase C
+
+- [ ] Run a tree-wide grep for `Debug\.WriteLine`, `Debug\.Fail`, `Debug\.Assert`, and `catch \(Exception` in `src/Reactor/`. Save the raw output under `docs/specs/044/inventory-pre.txt` (not checked in long-term; reference only during the audit).
+- [ ] Cross-check the count against spec §4.2 ("~150 sites across 47 files", "~80 swallowed-error sites", "~20 HR-diagnostic sites"). If counts diverge significantly, note in the audit PR.
+- [ ] List each `Debug.Fail("Unreachable")` site (spec §4.3 calls out 4 in `Md4cParser` + 1 in `Reconciler.cs`). Confirm count.
+
+---
+
+## Phase A — `DiagnosticLog` helper + generic events (1 PR)
+
+Closes the foundation. Once this lands, every catch site in Phase C can route through the two generics regardless of subsystem-specific event coverage.
+
+### 1.1 Populate `DiagnosticLog`
+
+- [x] Implement `DiagnosticLog.SwallowedError(LogCategory, string operation, Exception)` per spec §6.1. The public method is **not** `[Conditional]`; only the DEBUG mirror is.
+- [x] Implement `DiagnosticLog.HResultFailed(LogCategory, string operation, int hr)` per spec §6.1.
+- [x] Confirm `ex.Message` is **never** placed on the ETW payload (PII discipline, spec §6.2.1). Only `ex.GetType().Name`.
+- [x] Confirm both helpers gate with `ReactorEventSource.Log.IsEnabled(...)` at the call site before invoking the EventSource method (matches existing convention, spec §6.2).
+
+### 1.2 Add the two generic events to `ReactorEventSource`
+
+- [x] Add `Keywords.Errors` events `SwallowedError(string category, string operation, string exceptionType)` and `HResultFailed(string category, string operation, int hr)` to `src/Reactor/Core/ReactorEventSource.cs`. Level `Warning`, keyword `Errors`.
+- [x] Each event method internally re-checks `IsEnabled(EventLevel.Warning, Keywords.Errors)` before calling `WriteEvent` (existing convention).
+- [x] Add an `EventAttribute` with a stable, unique `EventId` for each (use next free IDs after the current 15).
+
+### 1.3 Add the six new keywords
+
+- [x] Extend `ReactorEventSource.Keywords` with `Hosting (0x80)`, `Persistence (0x100)`, `Navigation (0x200)`, `Intl (0x400)`, `Theme (0x800)`, `Shell (0x1000)` per spec §6.2.
+- [x] No bit overlaps; total stays within `EventKeywords` ulong range.
+
+### 1.4 Phase A tests
+
+- [x] Add `tests/Reactor.Tests/Diagnostics/DiagnosticLogTests.cs`:
+ - [x] `SwallowedError` writes the exception **type** but not the **message** to the ETW payload.
+ - [x] `HResultFailed` writes the HR as an `int` and the category as the `LogCategory` enum's `ToString()`.
+ - [x] Both helpers are no-ops (cost-of-disabled) when keyword `Errors` is disabled — assert via an `EventListener` that does not enable the keyword.
+ - [ ] DEBUG-only mirror is exercised by a `[Conditional("DEBUG")]`-aware test (or a test that runs only under DEBUG via `#if DEBUG`).
+
+### 1.5 Phase A acceptance
+
+- [x] `dotnet build Reactor.slnx` clean (warnings-as-errors, AOT/trim).
+- [x] `dotnet test tests/Reactor.Tests` green.
+- [x] No existing call sites changed — Phase A is **additive only**.
+
+---
+
+## Phase B — Expand `ReactorEventSource` subsystem coverage (1 PR)
+
+Adds the typed events that the §6.7 "Promote to typed event" verdicts will use. Until these land, Phase C migrates broad catches through the two generics from Phase A.
+
+### 2.1 Add Hosting events
+
+- [x] `WindowOpened(string windowType, long hwnd)` — Informational, Hosting.
+- [x] `WindowClosed(string windowType, long hwnd)` — Informational, Hosting.
+- [x] `WindowDpiChanged(string windowType, int oldDpi, int newDpi)` — Informational, Hosting.
+- [x] `BackdropMaterializationFailed(string kind, string exceptionType)` — Warning, Hosting.
+- [x] PII review: `windowType` is the C# type name (developer-authored, OK). Window titles are **not** emitted (spec §6.2.1).
+
+### 2.2 Add Persistence events
+
+- [x] `PersistenceRead(string storeKind, int sizeBytes)` — Informational, Persistence.
+- [x] `PersistenceWrite(string storeKind, int sizeBytes)` — Informational, Persistence.
+- [x] `PersistenceRejected(string storeKind, string reason)` — Warning, Persistence (oversize, corrupt, schema mismatch).
+- [x] PII review: file paths are **not** emitted; use a `storeKind` label (`"settings"`, `"placement"`, etc.).
+
+### 2.3 Add Navigation events
+
+- [x] `NavigationRequested(string routeTemplate)` — Informational, Navigation.
+- [x] `NavigationCompleted(string routeTemplate, double durationMs)` — Informational, Navigation.
+- [x] `NavigationCancelled(string routeTemplate, string reason)` — Informational, Navigation.
+- [x] `NavigationCacheHit(string routeTemplate)` — Verbose, Navigation.
+- [x] `NavigationCacheMiss(string routeTemplate)` — Verbose, Navigation.
+- [x] `NavigationCacheEvict(string routeTemplate, string reason)` — Verbose, Navigation.
+- [x] PII review: **route template** (`/users/{id}`) only, never the instantiated path (spec §6.2.1).
+
+### 2.4 Add Intl event
+
+- [x] `IntlMissingKey(string key, string locale, bool fellBack)` — Warning, Intl.
+- [x] PII review: keys are developer-authored static identifiers (OK).
+
+### 2.5 Add Theme event
+
+- [x] `ThemeApplyFailed(string targetType, string exceptionType)` — Warning, Theme.
+
+### 2.6 Reserve event IDs / verify ordering
+
+- [x] All new events get sequential `EventId`s after the Phase A additions.
+- [x] Update the `ReactorEventSource` `EventId` allocation comment so future additions know the next free ID.
+
+### 2.7 Phase B tests
+
+- [x] Add `tests/Reactor.Tests/Diagnostics/ReactorEventSourceCoverageTests.cs`:
+ - [x] One smoke test per new event that fires it and asserts the captured payload via an in-test `EventListener`.
+ - [ ] Each event is allocation-free when its keyword is disabled (use `BenchmarkDotNet`-style allocation check, or assert `IsEnabled == false → no GC alloc` via `GC.GetAllocatedBytesForCurrentThread()` delta).
+- [x] Add a single end-to-end test that enables `Keywords.Hosting | Persistence | Navigation | Intl | Theme | Errors`, fires one of each, and verifies all are captured (regression guard against keyword-bit overlap).
+
+### 2.8 Phase B acceptance
+
+- [x] `dotnet build Reactor.slnx` clean.
+- [x] `dotnet test tests/Reactor.Tests` green.
+- [x] Each new event has its PII policy decision documented inline (a `// PII:` comment on the event method or in a section comment).
+
+---
+
+## Phase C-audit — Swallowed-error audit (1 PR, docs only)
+
+Pure documentation. No code changes. **Required before Phase C ships any migration.**
+
+### 3.1 Create the audit file scaffold
+
+- [x] Created `docs/specs/044/` folder.
+- [x] Created `docs/specs/044/swallowed-error-audit.md` with the explanatory preamble (cross-references to spec §6.7) plus the five-verdict template.
+- [x] Sites grouped by source file in alphabetical order.
+
+### 3.2 Populate one entry per site (~80 entries)
+
+For each `catch (Exception ex) { Debug.WriteLine(...); }` site in `src/Reactor/`, add a section using the template from spec §6.7.1. Required fields per entry:
+
+- [ ] **Site (before)** — copy of the current catch block.
+- [ ] **Operation** — the platform/SDK call inside the `try`.
+- [ ] **Caller contract** — who calls this and when.
+- [ ] **Observed/expected failure modes** — at the HRESULT / Win32 code level, not just exception type.
+- [ ] **What we explicitly do NOT want to swallow** — the bug-class exceptions we're now happy to let propagate.
+- [ ] **Why we swallow the listed cases** — single-paragraph justification.
+- [ ] **Verdict** — exactly one of: Keep / Narrow / Propagate / Replace with `TryXxx` / Promote to typed event.
+- [ ] **Site (after)** — the proposed post-migration code (for Keep verdicts this is just the existing shape rewritten over `DiagnosticLog.SwallowedError`).
+- [ ] **Risk** — one line on what could break.
+- [ ] **Owner / PR / Status** — `☐ migrated ☐ verdict shipped` checkboxes.
+
+### 3.3 Group entries by file per first-pass categorization (spec §6.7.4)
+
+- [ ] `ReactorWindow.cs` swallows (~20 entries, dispose / AppWindow lifecycle, mostly Keep / Narrow).
+- [ ] Shell COM calls — `JumpList*`, `ThumbnailToolbar*`, `Tray*` (~15 entries, mostly Promote to typed event).
+- [ ] Win32 P/Invoke reporters — `WindowMessageMonitor`, etc. (~10 entries, mostly Replace with `TryXxx`).
+- [ ] Persistence — `JsonFileStore`, `PackagedSettingsStore`, `WindowPlacementCodec` (~8 entries, Narrow to `IOException`/`JsonException`/`UnauthorizedAccessException`).
+- [ ] Connected animations — `PrepareToAnimate` / `GetAnimation` / `TryStart` (~4 entries, Promote + Narrow).
+- [ ] Backdrop / Theme application (~3 entries, Narrow only).
+- [ ] User-callback isolation — `RenderContext` effect cleanups, command handlers, lifecycle hooks (~10 entries, all Keep per §6.7.3, but with explicit "what user contract this fulfills" notes).
+- [ ] `Reconciler` swallows not covered above (residual ~10 entries).
+
+### 3.4 Sanity-check verdict distribution
+
+- [x] Verdict counts at the top of the audit file: 56 Keep, 9 Narrow (6 shipped, 3 deferred), 0 Propagate, 10 Replace-with-TryXxx (all deferred to 4.8), 18 Promote-to-typed-event (9 shipped, 9 deferred to 4.6).
+- [x] Propagate count is 0 — well under the spec §6.7.4 worry threshold of 20.
+
+### 3.5 Phase C-audit acceptance
+
+- [ ] The audit file is reviewed line-by-line by a second pair of eyes (rubber-duck pass at minimum). _(Pending PR review.)_
+- [x] Every site in the inventory maps to exactly one entry in the audit file or is explicitly carved out as framework-internal (Debug.Assert, pure trace prints).
+- [x] No code changes in this PR — the audit file is its own commit.
+
+---
+
+## Phase C — Migrate `Debug.WriteLine` call sites (split across PRs by category)
+
+Mechanical migration driven by the audit. Each PR maps to one row of spec §6.3 / §6.7.4. Sites with verdict ≠ Keep land their fix in the same PR.
+
+### 4.1 PR: `Debug.Fail("Unreachable")` → `throw new UnreachableException(...)`
+
+- [x] Replace 4 sites in `src/Reactor/Markdown/Md4cParser.Block.cs` (spec §4.3).
+- [ ] Replace 1 site in `src/Reactor/Core/Reconciler.cs:~2635` (spec §4.3). **Skipped intentionally:** that site is not a `Debug.Fail("Unreachable")` pattern — its message is `"ElementRef<{T}> attached to a {U}. Use ElementRef or untyped ElementRef."` — and the whole containing `AssertTypedRefMatch` method is already `[Conditional("DEBUG")]`. Re-asking the reviewer in a follow-up if a behavior change is desired.
+- [x] Verify each replaced site is genuinely unreachable in tests (`UnreachableException` is Release-visible; we don't want it to fire in real code).
+
+### 4.2 PR: `NavigationDiagnostics` Debug.WriteLines → typed events
+
+- [x] Replace the 9 sites in `src/Reactor/Core/Navigation/NavigationDiagnostics.cs` with the Phase B navigation events (`NavigationRequested`, `NavigationCompleted`, `NavigationCacheHit`, etc.). The six direct mappings (Requested / Completed / Cancelled / CacheHit / CacheMiss / CacheEviction) reuse Phase B events 25-30. Three additional events were added to `ReactorEventSource` (IDs 33-35) to cover `TransitionStarted`, `TransitionCompleted`, and `DeepLinkResolved`. DeepLink intentionally drops the `path` payload (attacker-controllable per §6.2.1) and emits only `matched` + `routeCount`.
+- [x] Confirm `NavigationDiagnostics` callers continue to function (no behavior change in DEBUG; new visibility in Release). Existing `NavigationDiagnosticsCoverageTests` keeps verifying the public C# event subscribers (8 tests). New `NavigationDiagnosticsEtwBridgeTests` (7 tests) exercises every `OnX` entry point and asserts the typed ETW event lands on a `Keywords.Navigation` listener with the expected payload. `OnDeepLinkResolved_match_emits_outcome_only_no_path` is the explicit §6.2.1 PII regression guard.
+
+### 4.3 PR: `IntlAccessor` missing-key warnings → `IntlMissingKey`
+
+- [x] Replace the 4 sites in `src/Reactor/Core/Localization/IntlAccessor.cs` with the Phase B Intl events. The 2 missing-key sites in `ResolvePattern` collapse to a single typed `IntlMissingKey(key, locale, fellBack)` emission — the previous shape double-logged on the no-fallback-available path, which the new shape fixes. The 2 format-failure catches in `Message` and `RichMessage` route through `DiagnosticLog.SwallowedError(LogCategory.Intl, ...)` because they are exception swallows, not missing-key reports. PII (§6.2.1): MessageKey is namespace + key from .resw — developer-authored only.
+
+### 4.4 PR: HResult diagnostics → `DiagnosticLog.HResultFailed`
+
+- [x] Replace the `Debug.WriteLine($"... HR=0x{hr:X8}");` sites in the Shell hosting code. Actual inventory found 8 sites (the spec's "~20" estimate counted candidates in `WindowMessageMonitor`/`ReactorWindow` that don't actually use the HR format — those land in 4.5 instead): 6 in `JumpListComInterop.cs` (BeginList / AddUserTasks / AppendCategory / AppendKnownCategory.Recent / AppendKnownCategory.Frequent / CommitList), 1 in `ThumbnailToolbar.cs` (Update vs Add Buttons), 1 in `TrayFlyoutHostWindow.cs` (GetDpiForMonitor). The `AppendCategory` site dropped the user-named category string from the op label (developer-authored but unbounded → safer to fold into the typed Shell event in 4.6).
+- [ ] Each replacement references its audit entry by file path + line via a `// AUDIT: docs/specs/044/swallowed-error-audit.md#...` comment if also wrapped in a catch. _(deferred to the audit PR: the migrated sites are NOT inside the broader catch arms — they are HR-return-value checks, not exception swallows.)_
+
+### 4.5 PR: ReactorWindow swallowed-error migration
+
+- [x] Phase C.8 migrated all 29 sites to `DiagnosticLog.SwallowedError(LogCategory.Hosting, ...)`. Two persistence-placement sites route to `LogCategory.Persistence`. `using System.Diagnostics` removed.
+- [x] **Narrowing landed in Phase C.9.** Spec §6.7.2 properly applied. Added `HResults.IsTeardownReentry(int hr)` helper (covers `RPC_E_DISCONNECTED`, `E_HANDLE`, `RPC_E_SERVERFAULT`, `CO_E_OBJNOTCONNECTED`). 17 WinUI API sites now use `catch (COMException ex) when (HResults.IsTeardownReentry(ex.HResult))`. 3 user-callback sites (`SizeChanged`, `StateChanged`, `Closing`) had their try/catch deleted — user throws propagate to the dispatcher. 1 user-callback site with framework cleanup after it (`Closed?.Invoke`) converted to try/finally so cleanup runs but the user's exception still surfaces. Dispose chain converted to nested try/finally. 7 dead-defensive try/catch blocks deleted (P/Invokes on `nint` that can't throw at the marshal layer, or downstream calls that already narrow internally).
+- [x] Two broad `catch (Exception)` blocks remain — both genuine iteration sibling-independence per §6.7.3: `IClosingGuard.CanClose()` (fail-safe-to-cancel, spec 036 §3.4 test) and owned-window cascade (spec 036 §9). Both have inline comments naming the contract.
+
+### 4.6 PR: Shell COM-call promotion to typed events
+
+- [ ] For the ~15 Shell sites with verdict "Promote to typed event", add the typed event to `ReactorEventSource` (specifically scoped — `JumpListSaveFailed`, `ThumbnailToolbarSetButtonsFailed`, etc., each with `int hr` payload).
+- [ ] Migrate each catch to the new typed event + narrow HRESULT filter.
+- [ ] Update the audit entry from `☐ migrated` to `☑ migrated ☑ verdict shipped`.
+
+### 4.7 PR: Persistence narrowing
+
+- [x] Migrate `JsonFileStore`, `PackagedSettingsStore`, `WindowPlacementCodec` swallows to narrow `catch (IOException)`, `catch (JsonException)`, `catch (UnauthorizedAccessException)` plus `DiagnosticLog.SwallowedError(LogCategory.Persistence, ...)`.
+ - `JsonFileStore` narrowed to `IOException` / `UnauthorizedAccessException` (+ retained `JsonException` / `FormatException`); surprise exceptions now propagate. Happy-path round-trips additionally emit Phase B `PersistenceRead` / `PersistenceWrite` with a `"json-file"` `storeKind` (NEVER the path, per §6.2.1).
+ - `PackagedSettingsStore` narrowed to `InvalidOperationException` / `COMException` / `UnauthorizedAccessException` (+ `FormatException` on the base64 path). Note: the spec's narrow list was IOException/JsonException/UnauthorizedAccessException, but the WinRT call surface throws InvalidOperationException (0x80073D54) on every unpackaged process and COMException for store-level errors — those are the actual swallow types here. Storekind is `"packaged-settings"`.
+ - `WindowPlacementCodec` Win32 `GetWindowPlacement` failure now routes through `DiagnosticLog.HResultFailed` with the `GetLastError` value. `IsPlausiblePlacement`, `monitorCount` and `EndOfStreamException` reject paths now emit typed `PersistenceRejected("placement", reason)` with short reason labels — the raw rect / showCmd are deliberately NOT on the payload (would fingerprint multi-monitor layouts, §6.2.1). Outer catches narrowed to `IOException`.
+ - Tests: new `PersistenceEtwBridgeTests` (9) cover JsonFileStore round-trip (read + write event), oversize-read reject, malformed-json + malformed-base64 SwallowedError shape, PackagedSettingsStore unpackaged-context SwallowedError (read + write), and WindowPlacementCodec implausible-monitor-count + truncated-payload rejects. PII regression guard: no test payload may include the file path string.
+
+### 4.8 PR: TryXxx refactors
+
+- [ ] Convert the ~10 Win32 P/Invoke `GetLastError`-style swallows to `bool TryXxx(out int hr)` predicates. These usually already have a `bool` return; verify and finish.
+- [ ] Audit entries flip to `☑ verdict shipped`.
+
+### 4.9 PR: User-callback isolation sites (Keep verdicts)
+
+- [x] For the ~10 user-callback swallows in `RenderContext.cs` (effect cleanups, command handlers, lifecycle hooks), preserve the broad catch but route through `DiagnosticLog.SwallowedError(LogCategory.Reactor, "UseEffect.cleanup[i=N]", ex)` per spec §6.7.3. Actual count was 6 in `src/Reactor/Core/RenderContext.cs`: `UseCommand.ExecuteAsync`, `UseCommand.ExecuteAsync`, `UseEffect.cleanup[i=N]` (FlushEffects phase 1), `UseEffect.effect[i=N]` (FlushEffects phase 2), `RunCleanups.effectCleanup[i=N]`, `RunCleanups.persistedSave[i=N]`. The spec's "~10" estimate appears to have folded in `Reconciler` user-callback sites; those land in 4.10.
+- [x] Verify framework-internal code is **outside** the try/catch (spec §6.7.3 point 2). For `RunCleanups.persistedSave`, `PersistedHookStateBase.SaveToCache` is framework code but reaches user-supplied `IPersistedStateScope.Set` — the try-catch wraps the user contact point; the surrounding hook-iteration loop is outside.
+- [x] Each entry gets an inline `// User-callback isolation (spec 044 §6.7.3): ...` comment naming the user contract (cleanup ordering, effect-flush forward progress, persisted-slot independence).
+
+### 4.10 PR: Residual catches + remaining trace prints
+
+- [x] Migrated residual `Reconciler` swallowed-error catches:
+ - `Reconciler.cs` ConnectedAnimation `PrepareToAnimate` (×2, mount + update paths), `GetAnimation`, `TryStart` → `DiagnosticLog.SwallowedError(LogCategory.Reactor, "ConnectedAnimation.", ex)`. The spec's §6.7.4 "Promote + Narrow" verdict for these 4 entries is deferred (typed event promotion follows the same per-site audit gate as 4.5/4.6).
+ - `Reconciler.cs` `ApplyThemeBindings` → `DiagnosticLog.SwallowedError(LogCategory.Theme, "ApplyThemeBindings", ex)`. Theme is the right category — the catch wraps a `Style` XAML compile.
+ - `Reconciler.Mount.cs` `ContentDialog.ShowAsync+OnClosed` → `DiagnosticLog.SwallowedError(LogCategory.Reactor, ...)`. Inline comment marks it as user-callback isolation per §6.7.3 (the try wraps both `ShowAsync` and the user-supplied `OnClosed` delegate).
+- [x] `LayoutEtwConsumer` (12 sites): error swallows (7) already routed to `DiagnosticLog.SwallowedError` in Phase C.7a (commit `b761a7a1`). Remaining 5 sites are pure trace prints (session-started, parser output, orphan-session cleanup) — stay as `Debug.WriteLine` per spec §6.3 framework-internal carve-out.
+- [x] `LayoutCostAttribution` (8 sites): keep as `Debug.WriteLine` — framework-internal (verified by inspection).
+- [x] `MarkdownBuilder` parse-failure (1 site): keep as `Debug.WriteLine` — framework-internal.
+- [x] `YogaConfig` frozen-mutation `Debug.Assert` (6 sites): keep — these are `Debug.Assert`, not `Debug.WriteLine`; they're CI tripwires for a framework invariant, not diagnostics.
+- [x] `ChildCollection` bounds assertions (4 sites): keep — same as above.
+
+### 4.11 Phase C tests
+
+- [ ] After each PR, run `dotnet test tests/Reactor.Tests` and `dotnet test tests/Reactor.SelfTests`. Both must stay green.
+- [ ] Add one regression test per major category that fires the migrated path and asserts the event lands on the ETW listener (e.g., `Reconciler_swallow_emits_SwallowedError_event`, `NavigationDiagnostics_push_emits_NavigationRequested`).
+- [ ] After Phase C is fully landed, the spec §12 acceptance criterion holds: **zero** `Debug.WriteLine` calls in `src/Reactor/` that report errors or HRESULT codes. Verify with a final grep.
+
+### 4.12 Phase C acceptance
+
+- [ ] Every audit entry has both checkboxes ticked or a documented reason it carried over to a follow-up PR.
+- [ ] `dotnet test tests/Reactor.Tests tests/Reactor.SelfTests` green.
+- [ ] AOT/trim build clean.
+
+---
+
+## Phase D — `ReactorTrace.Subscribe` in-process helper (1 PR)
+
+### 5.1 Implement the listener
+
+- [x] Implement `ReactorTrace.Subscribe(Action, EventLevel, EventKeywords): IDisposable` per spec §6.4 in `src/Reactor/Diagnostics/ReactorTrace.cs`.
+- [x] Backing implementation is a sealed `EventListener` filtered on `EventSource.Name == "Microsoft-UI-Reactor"`.
+- [x] Default level is `Verbose` (spec §6.4 — earlier draft defaulted to Informational; we explicitly want Verbose).
+- [x] Default keywords are `(EventKeywords)(-1)` (all).
+- [x] Multiple concurrent subscribers supported; each subscriber's keywords/level are independently active until disposed.
+- [x] Subscriber callback wrapped in `try/catch` so a buggy subscriber can't deadlock the dispatcher (spec §6.4 second bullet).
+
+### 5.2 `ReactorEvent` payload
+
+- [x] `ReactorEvent` is a `public readonly record struct` per spec §6.4.
+- [x] `Payload` / `PayloadNames` use `IReadOnlyList