Originally created by: Akarsh-Hegde
This PR promotes pre-main → main 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:
/api routes ported to Rust; dashboard ships as a static export embedded in the .app binaryscreenpipe-screen + screenpipe-a11y crates256 commits · 340 files changed · +32 488 / −30 373 lines.
| 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. |
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.ts — load(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.
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.
What: A setup Tauri window opens automatically on first launch (guarded by ~/.meridian/onboarded flag). The "A·Rail" three-step flow:
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).
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).
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:
runtime-latest / main branch release) — production usersruntime-staging / pre-main marker-commit) — pre-production validationThe 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.
NSActivationPolicy::Regular on open, reverts to Accessory on close. NSWindowCollectionBehaviorFullScreenPrimary enabled.open_dashboard Tauri command from the setup completion screen.ui/out/popover/index.html (copied into the static export at build time).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.service.name = meridian-tray (gated on --features otel in dev; controlled by otlp_enabled in settings.json at runtime).observability CI job.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.meridian.db capture_framesapp_sessions rows in the dashboardmeridian uninstall removes all installed agents and datacom.meridiona.ui and com.meridiona.screenpipe agents are purged on first launchcargo 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
Added live download progress with transfer speed and clearer status updates.
Bug Fixes
Progress tracking now reflects the full model set, preventing incomplete setup from appearing finished.
Documentation
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
Originally posted by: coderabbitai[bot]
📝 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 helpto get the list of available commands.Originally posted by: Akarsh-Hegde
Code review findings addressed
GitHub Code Quality flagged two findings on
services/agents/routes/prefetch.py:Root cause: both variables were bare module-level globals.
prefetch_model()declared themglobaland wrote to them (reset on new run) but never read them in that scope — static analysis tools flag write-onlyglobalas "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 — noglobaldeclarations needed in either function. EMA semantics and reset-on-new-run behaviour are identical; only the storage form changed.Once [#348] merges into
pre-mainthis PR's diff will be clean.Related
Tickets:
#348Originally posted by: Akarsh-Hegde
All CodeRabbit findings resolved (PR [#348])
PR [#348] now covers every open finding. Summary:
_state.pyspeedomittedspeedto the field listroutes/prefetch.pylog.errordrops traceback (TRY400)log.exceptionroutes/prefetch.pyzip()missingstrict=(B905)zip(specs, probe_results, strict=True)session_distiller.py_get_embedder(ANN202)-> tuplesession_distiller.pyimport mlx.core as mx; mx.clear_cache()steps.tsx/page.tsxcanNextstayeds.modelReadyeven when the runtime is unavailable and there's nothing to download; user could never finish setupcanNextnow also gates open whenruntime_found=false && runtime_installed=false && download_available=falsepage.tsx// Step 2/3comments after the Integrations↔MLX tab reorder// Step 2 — integrationsand// Step 3 — local intelligenceThe
_speed_stateglobals fix (commitbfc01e69) was the first commit; this second commit (af2ced2f) covers the rest. Once [#348] merges topre-main, this PR's diff is clean.Related
Tickets:
#348Ticket changed by: Akarsh-Hegde