agent: dev launcher — own Linux env-var contract, add dev:tauri, doc sweep

The 2026-05-12 engine-slop pass removed runtime std::env::set_var mutation from src-tauri/src/lib.rs and replaced it with warn_if_x11_env_unset_on_wayland. With no launcher owning the contract, Linux Wayland dogfood was about to regress.

run.sh:
- now owns Linux launcher env defaults via case "$(uname -s)"
  LIBCLANG_PATH=/usr/lib64/llvm21/lib64 (user-set wins)
  WEBKIT_DISABLE_DMABUF_RENDERER=1 (always on Linux; user-set wins)
  GDK_BACKEND=x11, WINIT_UNIX_BACKEND=x11 (Wayland only; user-set wins)
- 60s Vite readiness timeout
- detects early Vite exit (kill -0 + wait) instead of hanging forever
- trap installed before wait so Ctrl-C cleans up
- args forwarded: ./run.sh --release etc.
- non-exec final Tauri launch preserved so cleanup trap fires

package.json:
- "dev:tauri": "./run.sh" — canonical discoverable dev command

Docs:
- README, dev-setup, architecture-map runtime + launcher pages updated with the new contract; canonical command is npm run dev:tauri (./run.sh as direct equivalent)
- dev-launcher-and-scripts.md replaces the incorrect "kills process group" claim with honest "kills the spawned npm process"
- dev-setup.md path /CORBEL-Projects/magnotia → /CORBEL-Projects/transcription-app
- gpu-tuning/plan.md gets a superseded note rather than rewriting the original rationale
- engine-slop-residuals.md gains Area F (packaged-binary launcher contract) with honest wrapper-vs-.desktop trade-off documented

Verification: bash -n run.sh; shellcheck clean; cargo check -p magnotia green; npm pkg get 'scripts.dev:tauri' returns "./run.sh"; stale-ref sweep clean across living docs.
This commit is contained in:
2026-05-12 22:05:33 +01:00
parent db654deecc
commit 792fb5ea08
11 changed files with 227 additions and 73 deletions

View File

@@ -2,19 +2,19 @@
name: App lifecycle
type: architecture-map-page
slice: 02-tauri-runtime
last_verified: 2026/05/09
last_verified: 2026/05/12
---
# App lifecycle
> **Where you are:** [Architecture map](../README.md) → [Tauri runtime](README.md) → App lifecycle
**Plain English summary.** This is the entry point. `main.rs` calls `magnotia_lib::run()` and `lib.rs::run` does everything that has to happen before the user sees a window: sets a Linux Wayland workaround, installs the Rust panic hook, registers Tauri plugins, opens the SQLite database, prunes the error log, builds a JS preferences-injection script, configures the WebKit media-permission grant on Linux, wires close-to-tray, populates `AppState` and the per-domain managed states, emits any runtime warnings, sets up the system tray, and finally registers all 71 Tauri commands.
**Plain English summary.** This is the entry point. `main.rs` calls `magnotia_lib::run()` and `lib.rs::run` does everything that has to happen before the user sees a window: initialises tracing, checks the Linux launcher env-var contract, installs the Rust panic hook, registers Tauri plugins, opens the SQLite database, prunes the error log, builds a JS preferences-injection script, configures the WebKit media-permission grant on Linux, wires close-to-tray, populates `AppState` and the per-domain managed states, emits any runtime warnings, sets up the system tray, and finally registers all 71 Tauri commands.
## At a glance
- Path: `src-tauri/src/main.rs`, `src-tauri/src/lib.rs`.
- LOC: 5 (main) + 448 (lib).
- LOC: 5 (main) + 491 (lib).
- Tauri commands exposed directly here: `save_preferences` (string preferences -> SQLite settings table). All other commands live under `commands::*` and are registered via `tauri::generate_handler!`.
- Events emitted directly here: none (runtime warnings are emitted by `commands::models::emit_runtime_warnings`, called from setup).
- Depends on: `tauri`, `sqlx::SqlitePool`, `magnotia_core::types::EngineName`, `magnotia_llm::LlmEngine`, `magnotia_storage::{init, database_path, get_setting, set_setting, prune_error_log}`, `magnotia_transcription::LocalEngine`, plus the `commands::*` and `tray` modules.
@@ -46,16 +46,18 @@ Functions:
- `build_preferences_script(prefs_json: Option<String>) -> String` (`src-tauri/src/lib.rs:38`). Builds an IIFE that reads saved preferences (theme, zone, accessibility settings: font family, font size, letter spacing, line height, transcript size, bionic reading, reduce motion) and applies them to `<html>` before the rest of the document loads. Embeds the JSON via `serde_json::to_string` to keep it safe.
- `save_preferences(state, preferences) -> Result<(), String>` (`src-tauri/src/lib.rs:73`). The single command in `lib.rs`. Persists the preferences blob to the SQLite settings table under key `magnotia_preferences`.
- `ensure_x11_on_wayland()` (`src-tauri/src/lib.rs:99`, Linux only). Sets `WEBKIT_DISABLE_DMABUF_RENDERER=1` unconditionally on Linux (iGPU idle-cost workaround), and additionally sets `GDK_BACKEND=x11` plus `WINIT_UNIX_BACKEND=x11` when `XDG_SESSION_TYPE=wayland`. Idempotent: if a value is already set, the function leaves it alone. Must run before any threads spawn — uses `unsafe { std::env::set_var(...) }`.
- `run()` (`src-tauri/src/lib.rs:135`). The Tauri builder pipeline.
- `init_tracing()` (Linux/macOS/Windows). Initialises the process-wide tracing subscriber once, honours `RUST_LOG`, and writes structured startup/runtime logs to stderr.
- `warn_if_x11_env_unset_on_wayland()` (Linux only). Emits a `magnotia_startup` warning when the launcher has not pre-set `WEBKIT_DISABLE_DMABUF_RENDERER` (always expected on Linux), or `GDK_BACKEND=x11` / `WINIT_UNIX_BACKEND=x11` when `XDG_SESSION_TYPE=wayland`. It does not mutate the process environment; `run.sh` owns the dev-time contract and package wrappers/.desktop files must own the distribution-time contract.
- `run()`. The Tauri builder pipeline.
### `run()` step-by-step
1. **Linux env-var prelude.** Calls `ensure_x11_on_wayland()` on Linux (`src-tauri/src/lib.rs:137`).
2. **Panic hook.** Calls `commands::diagnostics::install_panic_hook()` to dump panic info to `crashes_dir()` (`src-tauri/src/lib.rs:141`).
3. **Plugin wiring (always-on).** `tauri_plugin_opener`, `tauri_plugin_dialog`, `tauri_plugin_notification` (`src-tauri/src/lib.rs:144`).
4. **Plugin wiring (desktop-only).** `tauri_plugin_global_shortcut`, `tauri_plugin_autostart` (LaunchAgent on macOS), `tauri_plugin_window_state` (`src-tauri/src/lib.rs:158`).
5. **Setup hook.** This is where the bulk of startup work lives:
1. **Tracing init.** Calls `init_tracing()` before any startup warnings/logs are emitted.
2. **Linux launcher contract check.** Calls `warn_if_x11_env_unset_on_wayland()` on Linux. Missing env vars produce warnings only; runtime env-var mutation was removed because Rust 2024 treats environment mutation in multi-threaded programs as unsafe.
3. **Panic hook.** Calls `commands::diagnostics::install_panic_hook()` to dump panic info to `crashes_dir()`.
4. **Plugin wiring (always-on).** `tauri_plugin_opener`, `tauri_plugin_dialog`, `tauri_plugin_notification` (`src-tauri/src/lib.rs:144`).
5. **Plugin wiring (desktop-only).** `tauri_plugin_global_shortcut`, `tauri_plugin_autostart` (LaunchAgent on macOS), `tauri_plugin_window_state` (`src-tauri/src/lib.rs:158`).
6. **Setup hook.** This is where the bulk of startup work lives:
- Initialise SQLite via `magnotia_storage::init(&database_path()).await` using `tauri::async_runtime::block_on` (`src-tauri/src/lib.rs:180`). The `Instant::now()` timing is logged.
- Prune `error_log` rows older than 90 days (`src-tauri/src/lib.rs:189`). Best-effort: a failure logs but does not block startup.
- Load saved preferences from the settings table; build the JS injection script (`src-tauri/src/lib.rs:204`).
@@ -66,8 +68,8 @@ Functions:
- Build the `AppState` itself: fresh `LocalEngine`s for whisper and parakeet, the open `SqlitePool`, a fresh `LlmEngine` (`src-tauri/src/lib.rs:302`).
- Emit runtime warnings (CPU baseline, Vulkan loader) via `commands::models::emit_runtime_warnings` (`src-tauri/src/lib.rs:312`).
- Setup the system tray on desktop (`src-tauri/src/lib.rs:314`).
6. **Command registration.** `tauri::generate_handler![...]` lists 71 commands (`src-tauri/src/lib.rs:321`). The order in the macro is grouped by domain (preferences, models, LLM, transcription, audio, tasks, feedback, TTS, rituals, nudges, intentions, profiles, transcripts, diagnostics, live, windows, clipboard, fs, paste, meeting, hardware, hotkey, updater).
7. **Run.** `.run(tauri::generate_context!())` blocks the main thread until the app exits. Panics are wrapped with `expect("error while running Magnotia")`.
7. **Command registration.** `tauri::generate_handler![...]` lists 71 commands (`src-tauri/src/lib.rs:321`). The order in the macro is grouped by domain (preferences, models, LLM, transcription, audio, tasks, feedback, TTS, rituals, nudges, intentions, profiles, transcripts, diagnostics, live, windows, clipboard, fs, paste, meeting, hardware, hotkey, updater).
8. **Run.** `.run(tauri::generate_context!())` blocks the main thread until the app exits. Panics are wrapped with `expect("error while running Magnotia")`.
## Data flow
@@ -79,8 +81,8 @@ Functions:
## Watch-outs
- `tauri::async_runtime::block_on` inside `setup` blocks startup. The DB init and prefs read are explicitly timed and logged so regressions show up. Adding more synchronous async work here directly pushes the time-to-first-paint up.
- The Linux media-permission wire-up is non-fatal: if `with_webview` fails the app still boots, but `getUserMedia` will be silently denied or fall back to a prompt the user cannot answer (no UI). The error path logs `[startup] failed to configure webview media permissions: ...` to stderr.
- The `unsafe std::env::set_var` calls in `ensure_x11_on_wayland` are sound only because nothing else in the binary has spawned a thread yet at that point. Do not introduce another startup-time env mutation outside this function unless the same invariant is preserved.
- The Linux media-permission wire-up is non-fatal: if `with_webview` fails the app still boots, but `getUserMedia` will be silently denied or fall back to a prompt the user cannot answer (no UI). The error path logs a `magnotia_startup` warning.
- Linux rendering env vars are a launcher contract, not a runtime mutation. In development, use `npm run dev:tauri` / `./run.sh`; packaged Linux builds need an equivalent wrapper or `.desktop` `Exec=env` policy. `WEBKIT_DISABLE_DMABUF_RENDERER=0` remains the user opt-out for the DMA-BUF workaround.
- Close-to-tray works only on desktop (the `cfg!(not(target_os = "android"))` block). On Android, closing the activity terminates the process, which is the expected platform behaviour.
- The `prewarm_default_model` call is *not* wired here. `commands::models::prewarm_default_model` exists, but `setup` does not invoke it. The frontend invokes the matching `prewarm_default_model_cmd` command after the main page mounts. If you ever want to shift pre-warm into setup, watch the spawn_blocking ordering against the engine `Arc` clones.