Menu

#347 feat: v2 architecture — in-process capture, Tauri-embedded dashboard, onboarding wizard, in-process OAuth

closed
nobody
None
2026-06-26
2026-06-26
Anonymous
No

Originally created by: Akarsh-Hegde

Overview

This PR promotes pre-mainmain and represents the full v2 architecture of Meridian. It delivers four platform-level rewrites that have been built, staged, and validated on pre-main over the last several months:

  1. Dashboard → Tauri fold — all Next.js /api routes ported to Rust; dashboard ships as a static export embedded in the .app binary
  2. In-process capture (Gap-2 Bucket 2) — screenpipe process eliminated; capture runs inside the tray using forked screenpipe-screen + screenpipe-a11y crates
  3. First-run onboarding wizard — A·Rail design with MLX model provisioning (3-model registry: LLM + reranker + embedder)
  4. In-process OAuth — browser PKCE auth for all five trackers (Jira, Linear, GitHub, Notion, Asana) with in-app token connect fallback

256 commits · 340 files changed · +32 488 / −30 373 lines.


Breaking changes

What changed Impact
screenpipe process removed No screenpipe binary or launchd agent required. Existing installs: install-from-bundle.sh purges com.meridiona.screenpipe.
Node.js UI server retired com.meridiona.ui plist + ui-start.sh gone. Dashboard now runs only inside the Tauri webview. Any bookmarked localhost:3000 URL stops working.
All /api Next.js routes deleted Tray commands (Tauri invoke) replace them. Web-only consumers of the old routes must migrate.
meridian.db gains capture_frames + capture_ui_events tables Migration 025+ creates these on first launch. The old screenpipe DB path (SCREENPIPE_DB) is now vestigial.
MLX runtime re-packaged (Approach C) Old tarball layout replaced by the signed runtime-latest GitHub Release. Existing installations auto-upgrade on next launch via the stage-and-swap mechanism.

1 — Dashboard → Tauri fold (meridian-core)

What: Every ui/app/api/* route has been ported to a Rust command the frontend calls via Tauri invoke. A new meridian-core crate is the single source of truth for all DB-backed reads; the daemon re-exports it unchanged. The frontend is a Next.js static export (output: 'export'ui/out) bundled into the .app at build time — no Node server, no /api fetch.

Routes ported:

Route Rust command Module
GET /api/today get_today meridian-core/src/readers/today/
GET /api/week get_week meridian-core/src/readers/week.rs
GET /api/active get_active meridian-core/src/db.rs
GET /api/tasks get_tasks meridian-core/src/readers/tasks.rs
GET /api/worklogs get_worklogs meridian-core/src/readers/worklogs.rs
GET /api/coding-agents get_coding_agents meridian-core/src/readers/coding_agents.rs
GET /api/integrations get_integrations tray/commands/integrations.rs
GET /api/settings get_settings meridian-core/src/settings.rs
PUT /api/settings save_settings tray/commands/dashboard.rs
GET /api/triage get_triage meridian-core/src/readers/triage.rs
POST /api/triage/apply apply_triage_cmd tray/commands/dashboard.rs
GET /api/plan, POST /api/plan get_plan, save_plan meridian-core/src/readers/tasks.rs
GET /api/plan/task/:key get_task_detail meridian-core/src/readers/tasks.rs
GET /api/version get_app_version tray/commands/version.rs
GET /api/health, GET /api/logs get_health, get_logs tray/commands/health.rs, logs.rs
Four SSE streams (health/notices/notifications/logs) Tauri events via poll loop tray/src/poll/live.rs

Wire protocol: ui/lib/bridge.tsload(path, 'command', args) for reads, mutate(path, 'command', body) for writes, subscribe(path, null, 'event-name', cb) for live streams. All response types live in ui/lib/api-types.ts.


2 — In-process capture (Gap-2 Bucket 2)

What: The screenpipe child-process and its SQLite DB are gone. Capture runs as a set of Tokio tasks spawned inside the tray, writing directly into meridian.db's own capture_frames and capture_ui_events tables. The daemon ETL reads these same tables.

Slices landed:

Slice What
1–2 Capture boundary + screenpipe-screen engine (OCR + a11y tree)
3a Window-aware metadata — per-window app_name/window_name/browser_url
3b a11y-tree text capture with OCR fallback for Chrome where a11y tree is JS-only
3c capture_ui_events table + in-process input recorder
4a Persist frames to meridian.db
4b-1 Daemon ETL cuts over to capture_frames/capture_ui_events
4b-2 Daemon health check repointed; screenpipe plist/process retired

TCC: Single entry under com.meridiona.tray. Screen Recording, Accessibility, and Input Monitoring are requested via CGRequestScreenCaptureAccess(), AXIsProcessTrustedWithOptions(), and IOHIDRequestAccess(). The stable dev code-signing certificate (scripts/dev-signing.sh) prevents TCC cdhash churn across rebuilds.

Current v1 degradations (accepted, tracked): Audio is stubbed empty; all gaps classify system_sleep (no in-process idle detection); no input-monitoring fallback when the daemon runs without a login shell.


3 — First-run onboarding wizard

What: A setup Tauri window opens automatically on first launch (guarded by ~/.meridian/onboarded flag). The "A·Rail" three-step flow:

  1. Permissions — Accessibility, Screen Recording, Input Monitoring cards with live polling. Deep-links to System Settings panes.
  2. Integrations — ConnectTrackers component with OAuth + token flows (see §4). Rail status updates live.
  3. Local intelligence — Provisions the MLX runtime (tarball download + integrity check), starts the server, then prefetches all three pipeline models (LLM → reranker → embedder) in sequence. modelReady gate on Finish.

MLX server supervision: The poll loop auto-restarts the MLX server if it dies. Runtime auto-upgrades in the background via stage-and-swap (downloads new tarball to .meridian/mlx-server-stage/, verifies SHA-256 + semver, atomically replaces .meridian/mlx-server/ on next tray start).


4 — In-process OAuth

What: All five trackers now support browser PKCE OAuth entirely inside the app — no copy-paste, no redirect to localhost. A token-connect fallback is always available for enterprise / self-hosted setups.

Tracker OAuth method
Jira Cloud Browser PKCE → code param → server exchange
Jira Server/DC Token only (server OAuth not supported)
Linear Browser PKCE
GitHub gh auth login --web via embedded terminal
Notion Browser PKCE
Asana Browser PKCE

Each connect flow: opens a tauri::window pointing to the provider's auth URL → local callback server on a random ephemeral port captures the code → tray exchanges it, writes tokens to ~/.meridian/.env, emits integration-connected event to the wizard. Disconnect clears provider tasks from the DB.

Security: CodeQL taint path on the authorize URL is resolved (URL never written to stderr or logged). GITHUB_TOKEN env-var collision fixed (guarded .env write with trailing newline).


5 — DMG auto-update

What: Tauri updater wired to a GitHub Releases manifest. The tray poll loop checks for updates; an UpdateBanner component in the dashboard prompts the user to install. Two channels:

  • Stable (runtime-latest / main branch release) — production users
  • Staging (runtime-staging / pre-main marker-commit) — pre-production validation

The auto-update flow is fully ad-hoc signed (no Apple Dev ID required for this phase). The staging channel bakes its endpoint at build time via --config '{"plugins":{"updater":{"endpoints":[…]}}}' so the endpoint never leaks into main builds.


6 — Dashboard enhancements

  • Full-screen with dock iconNSActivationPolicy::Regular on open, reverts to Accessory on close. NSWindowCollectionBehaviorFullScreenPrimary enabled.
  • "Open Meridian" button wired to open_dashboard Tauri command from the setup completion screen.
  • Popover redesign — new compact UI served from ui/out/popover/index.html (copied into the static export at build time).
  • Live menu-bar pill — progress ring + current task key rendered in the system menu bar.
  • Rich hover tooltip — expanded card on tray icon hover showing active session, today stats, and current task.

7 — Observability

  • Per-op tracing::instrument spans on every meridian-core reader and every Tauri command that does real work.
  • debug_span! wrappers on each individual SQL query; tracing::debug!(rows = …) after every result set.
  • OpenTelemetry export to OpenObserve under service.name = meridian-tray (gated on --features otel in dev; controlled by otlp_enabled in settings.json at runtime).
  • OO dashboard definitions auto-sync on git push via the observability CI job.

Key bug fixes

  • fix(capture): fall back to OCR when browser a11y tree is chrome-only — Chrome returns JS-only a11y trees that carry no readable text; fall back to full_text (OCR) for those frames.
  • fix(etl): suppress VS Code frames when focused terminal is a coding agent — VS Code frames where window_name matches a running coding-agent terminal session no longer produce duplicate app_sessions.
  • fix(integrations): write .env with trailing newline — appended keys were concatenating onto the last line of .env when the file had no trailing newline.
  • fix(oauth): remove authorize URL from stderr — clears CodeQL CWE-918 taint path.
  • fix(obs): init OTLP inside the Tokio runtime — fixes no reactor running panic on OTLP initialisation.
  • fix(install): clean up legacy bundle agents + migrate .env on DMG upgrade — purges com.meridiona.ui and com.meridiona.screenpipe launchd agents left by pre-v2 installs.

Test plan

  • [ ] Fresh DMG install: onboarding wizard opens automatically, all three permission cards poll live, MLX download + prefetch completes, Finish marks onboarded
  • [ ] Today/Week dashboard renders without a Node server running
  • [ ] Screen Recording + Accessibility capture frames write to meridian.db capture_frames
  • [ ] ETL processes in-process frames → app_sessions rows in the dashboard
  • [ ] OAuth connect flow for at least Jira + GitHub works end-to-end
  • [ ] meridian uninstall removes all installed agents and data
  • [ ] Auto-update: staging channel detects a new release and applies it
  • [ ] Upgrade from a v1 DMG: com.meridiona.ui and com.meridiona.screenpipe agents are purged on first launch
  • [ ] cargo test passes (tests/integration_etl.rs covers ETL with in-process capture tables)
  • [ ] cargo clippy -- -D warnings passes on both src/ and tray/src-tauri/

PRs [#298], [#314], [#315], [#318], [#319], [#320], [#321], [#323], [#324], [#326], [#327], [#328], [#329], [#331], [#332], [#333], [#334], [#336], [#337], [#338], [#339], [#340], [#342], [#343] — plus standalone commits for Gap-2 Bucket 2 slices and the OAuth security hardening.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
  • Setup now automatically downloads required local models and starts the MLX runtime during onboarding.
  • Added live download progress with transfer speed and clearer status updates.

  • Bug Fixes

  • Improved handling of onboarding retries and error states so stalled model setup can be retried more reliably.
  • Progress tracking now reflects the full model set, preventing incomplete setup from appearing finished.

  • Documentation

  • Updated contributor guidance for re-running onboarding and rechecking model downloads.

Related

Tickets: #298
Tickets: #314
Tickets: #315
Tickets: #318
Tickets: #319
Tickets: #320
Tickets: #321
Tickets: #323
Tickets: #324
Tickets: #326
Tickets: #327
Tickets: #328
Tickets: #329
Tickets: #331
Tickets: #332
Tickets: #333
Tickets: #334
Tickets: #336
Tickets: #337
Tickets: #338
Tickets: #339
Tickets: #340
Tickets: #342
Tickets: #343
Tickets: #348
Tickets: #351

Discussion

  • Anonymous

    Anonymous - 2026-06-26

    Originally posted by: coderabbitai[bot]

    Review Change Stack

    📝 Walkthrough ## Walkthrough The PR replaces single-model MLX onboarding with a shared three-model registry, switches session embeddings to MLX, expands prefetching and progress reporting to track all required models, and updates the setup wizard and startup scripts to match the new flow. ## Changes **On-device model provisioning** |Layer / File(s)|Summary| |---|---| |**Registry-backed model IDs**
    `services/agents/model_registry.py`, `services/agents/mlx_classifier.py`, `services/agents/reranker.py`, `services/agents/pm_worklog_update/config.py`|A shared registry defines the LLM, reranker, and embedder checkpoint IDs, and the classifier, reranker, and MLX server config resolve defaults from it.| |**MLX embedder migration**
    `services/agents/session_distiller.py`|Session distillation loads the embedder from the registry, computes embeddings with MLX, and flushes MLX cache on eviction.| |**Startup and cache wiring**
    `CONTRIBUTING.md`, `dev-start.sh`, `install.sh`, `services/pyproject.toml`, `services/scripts/com.meridiona.mlx-server.plist`, `tray/src-tauri/src/commands/setup.rs`|Onboarding docs, startup scripts, the service plist, and the MLX dependency list now pin HuggingFace cache and Xet settings, and the install script checks cache presence across the three-model pipeline.| |**Prefetch state and backend**
    `services/agents/_state.py`, `services/agents/routes/prefetch.py`, `services/agents/server.py`|Prefetch state tracks aggregate and per-model progress, and the prefetch route now downloads all registry models, probes their sizes, and reports aggregate speed and status.| |**Tray progress transport**
    `ui/app/setup/data.ts`, `tray/src-tauri/src/mlx_server.rs`|The tray/server bridge carries speed through runtime downloads and prefetch polling, updates progress messages, and tightens 404 handling for prefetch endpoints.| |**Setup wizard flow**
    `ui/app/setup/page.tsx`, `ui/app/setup/steps.tsx`|The setup wizard auto-runs runtime install, server start, and prefetch when open, stores provisioning errors separately, adds retry handling, and reorders the local intelligence step after Integrations.| ## Sequence Diagram(s) :::mermaid sequenceDiagram participant SetupWizard participant TrayMlxServer participant PrefetchRoute participant HuggingFaceCache SetupWizard->>TrayMlxServer: POST /prefetch_model TrayMlxServer->>PrefetchRoute: initialize model prefetch PrefetchRoute->>HuggingFaceCache: snapshot_download ALL_SPECS files SetupWizard->>TrayMlxServer: GET /prefetch_status TrayMlxServer->>PrefetchRoute: poll aggregate state PrefetchRoute-->>TrayMlxServer: state, received, total, speed TrayMlxServer-->>SetupWizard: DownloadProgress ## Estimated code review effort 🎯 4 (Complex) | ⏱️ ~60 minutes ## Possibly related PRs - [[Meridiona/meridian#324](https://github.com/Meridiona/meridian/issues/324)](https://github.com/Meridiona/meridian/pull/324): Introduces the same `/prefetch_model` and `/prefetch_status` flow and tray setup wiring that this PR extends to multi-model registry-driven prefetching. - [[Meridiona/meridian#341](https://github.com/Meridiona/meridian/issues/341)](https://github.com/Meridiona/meridian/pull/341): Touches the same `prefetch_state` and prefetch-route modules that this PR reshapes for aggregate progress and per-model tracking. ## Poem > I thumped through caches under moonlit skies, > Three models hopped where the old one lies. > Speed in my whiskers, retries in paw, > Setup now sings with a gentle draw. > 🐰
    🚥 Pre-merge checks | ✅ 5
    ✅ Passed checks (5 passed) | Check name | Status | Explanation | | :------------------------: | :------- | :------------------------------------------------------------------------------------------------------------------------------------------------------- | | Title check | ✅ Passed | The title clearly summarizes the main v2 architecture changes: in-process capture, embedded dashboard, onboarding wizard, and in-process OAuth. | | Description check | ✅ Passed | The description is detailed and covers the PR purpose, breaking changes, and testing, but it omits the template's Checklist and Related issues sections. | | Docstring Coverage | ✅ Passed | Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. | | Linked Issues check | ✅ Passed | Check skipped because no linked issues were found for this pull request. | | Out of Scope Changes check | ✅ Passed | Check skipped because no linked issues were found for this pull request. |
    ✨ Finishing Touches
    📝 Generate docstrings - [ ] Create stacked PR - [ ] Commit on current branch
    🧪 Generate unit tests (beta) - [ ] Create PR with unit tests - [ ] Commit unit tests in branch `pre-main`

    Comment @coderabbitai help to get the list of available commands.

     
  • Anonymous

    Anonymous - 2026-06-26

    Originally posted by: Akarsh-Hegde

    Code review findings addressed

    GitHub Code Quality flagged two findings on services/agents/routes/prefetch.py:

    _last_recv and _last_recv_ts — unused global variable (×2)

    Root cause: both variables were bare module-level globals. prefetch_model() declared them global and wrote to them (reset on new run) but never read them in that scope — static analysis tools flag write-only global as "unused."

    Fix (PR [#348] → pre-main): replaced both globals with a single _speed_state = {"recv": 0, "ts": 0.0} dict. Dict mutation is in-place — no global declarations needed in either function. EMA semantics and reset-on-new-run behaviour are identical; only the storage form changed.

    Once [#348] merges into pre-main this PR's diff will be clean.

     

    Related

    Tickets: #348

  • Anonymous

    Anonymous - 2026-06-26

    Originally posted by: Akarsh-Hegde

    All CodeRabbit findings resolved (PR [#348])

    PR [#348] now covers every open finding. Summary:

    File Finding Fix
    _state.py Stale wire-contract comment — speed omitted Added speed to the field list
    routes/prefetch.py log.error drops traceback (TRY400) Changed to log.exception
    routes/prefetch.py zip() missing strict= (B905) zip(specs, probe_results, strict=True)
    session_distiller.py Missing return type on _get_embedder (ANN202) Added -> tuple
    session_distiller.py Two statements on one line (E702) Split import mlx.core as mx; mx.clear_cache()
    steps.tsx / page.tsx 🟠 Major: unsupported-runtime dead-endcanNext stayed s.modelReady even when the runtime is unavailable and there's nothing to download; user could never finish setup canNext now also gates open when runtime_found=false && runtime_installed=false && download_available=false
    page.tsx Stale // Step 2/3 comments after the Integrations↔MLX tab reorder Corrected to // Step 2 — integrations and // Step 3 — local intelligence

    The _speed_state globals fix (commit bfc01e69) was the first commit; this second commit (af2ced2f) covers the rest. Once [#348] merges to pre-main, this PR's diff is clean.

     

    Related

    Tickets: #348

  • Anonymous

    Anonymous - 2026-06-26

    Ticket changed by: Akarsh-Hegde

    • status: open --> closed
     

Log in to post a comment.