Skip to content

release: fold dashboard into Tauri + self-contained DMG + in-process capture (pre-main → main)#330

Merged
Akarsh-Hegde merged 193 commits into
mainfrom
pre-main
Jun 24, 2026
Merged

release: fold dashboard into Tauri + self-contained DMG + in-process capture (pre-main → main)#330
Akarsh-Hegde merged 193 commits into
mainfrom
pre-main

Conversation

@Akarsh-Hegde

@Akarsh-Hegde Akarsh-Hegde commented Jun 24, 2026

Copy link
Copy Markdown
Member

Summary

Promotes pre-mainmain: the culmination of the Next-fold and the self-contained-DMG / setup re-platform initiative. Meridian moves from a Node-server + external-screenpipe-process topology to a single self-contained Tauri .app — the dashboard runs inside the webview as a static export, every /api route is ported to Rust, capture runs in-process, and the MLX runtime is provisioned/supervised by the tray.

This bundles 13 already-reviewed feature PRs (#298, #314#329) plus post-merge review + CI fixes.

Metric Value
Commits 186
Files changed 223 (83 added, 49 deleted)
Diff +25,162 / −8,814
New shared crate meridian-core — 23 files, ~6,050 LOC
Commit types 72 feat, 49 fix, 11 chore, 9 refactor, 8 docs, 7 build, 6 test, 6 ci
Resulting version v1.64.0 (minor — see Versioning)
DMG size 19.6 MB (Meridian.app.tar.gz 19.3 MB)
Release build ~5 min (macOS runner: build + sign + notarize-ready + package)
Tests 516 Rust + 7 JS/TS suites — all green

Architecture changes

The core shift: delete the Node UI server and the external screenpipe process; collapse everything into one signed .app where the tray owns capture + the embedded dashboard, the daemon owns ETL, and meridian-core is the shared data layer both link against.

Before (v1.63.0)

  • screenpipe runs as its own launchd process → writes ~/.screenpipe/db.sqlite; the daemon reads it cross-DB.
  • A standalone Next.js Node server (com.meridiona.ui launchd agent) serves the dashboard and ~32 /api/* routes, reading meridian.db over better-sqlite3.
  • The tray polls those /api routes over HTTP.

After (v1.64.0)

  • The tray captures in-process (forked screenpipe-screen + screenpipe-a11y) → writes capture_frames / capture_ui_events into meridian.db. No screenpipe DB / process / pool.
  • The dashboard is a static export embedded in the tray binary; the frontend reaches Rust only via Tauri invoke / events through ui/lib/bridge.ts. No Node server, no /api routes.
  • DB reads live in meridian-core/src/readers/ (shared by daemon + tray); file/env/process actions are tray commands. The four SSE streams became Tauri events off the tray poll loop.
flowchart TB
  subgraph BEFORE["BEFORE — v1.63.0 (multi-process)"]
    direction TB
    sp1[screenpipe process]:::ext --> spdb[(~/.screenpipe/db.sqlite)]:::ext
    spdb --> d1[meridian daemon · ETL]
    d1 --> mdb1[(meridian.db)]
    node[Next.js Node server<br/>com.meridiona.ui]:::ext --> mdb1
    node -->|serves| dash1[Dashboard + /api routes]
    tray1[Tauri tray] -->|HTTP poll /api| node
    mlx1[MLX Python server]:::py --> d1
  end

  subgraph AFTER["AFTER — v1.64.0 (single self-contained .app)"]
    direction TB
    subgraph app["Meridian.app"]
      cap[Tray · in-process capture<br/>screenpipe-screen/-a11y]:::new
      core[[meridian-core<br/>shared readers + commands]]:::new
      webview[Embedded dashboard<br/>static export]:::new
      d2[meridian daemon · ETL]
    end
    cap -->|capture_frames / capture_ui_events| mdb2[(meridian.db)]
    d2 -->|reads capture tables| mdb2
    d2 -->|app_sessions| mdb2
    webview -->|Tauri invoke / events| core
    core --> mdb2
    mlx2[MLX Python server<br/>provisioned + supervised by tray]:::py --> d2
  end

  classDef ext fill:#fde,stroke:#b66;
  classDef new fill:#dfe,stroke:#6b6;
  classDef py fill:#eef,stroke:#66b;
Loading

Features implemented

1. Next-fold — dashboard into Tauri, /api → Rust (#298, #314)

  • Dashboard now runs only inside the Tauri webview as a static export (output: 'export') — no Node server.
  • All ~32 /api/* routes ported: DB reads → meridian-core/src/readers/ (re-exported by the daemon), file/env/process → tray/src-tauri/src/commands/.
  • Health / notices / notifications / log streams converted from SSE → Tauri events emitted by the tray poll loop.
  • Retired the standalone Node-UI release machinery (com.meridiona.ui plist, ui-start.sh, pinned Node + better-sqlite3 ABI dance). Dashboard now ships embedded in the tray binary.

2. Self-contained DMG + setup re-platform (#315, #318, #319, #320, #321, #326)

3. In-process capture — screenpipe forked at MIT (#323, #324)

4. Tray popover redesign (#327)

  • Reworked popover UI. Two known limitations deferred for the MVP (timer drift; pending-drafts amber dot).

5. Auto-update + release engineering (#328, #329)

6. Post-merge review + CI fixes


Compatibility & breaking changes

Audited across four surfaces (DB schema, public interfaces, install/upgrade path, runtime behavior). No hard breaking changes for existing installs — the version-affecting changes are additive or documented v1 degradations.

✅ Safe / non-breaking

  • DB migrations are strictly append-only (046 capture_frames, 047 capture_ui_events, 048 ETL-cursor reset) — no modified/renamed/deleted migrations (CI "Migrations append-only" guard enforced), no DROP/ALTER/destructive changes. Existing meridian.db upgrades cleanly; app_sessions history preserved.
  • Public interfaces preserved — no CLI command, env var, settings.json key, or MCP tool removed or renamed (only additions). The deleted /api/* routes were internal-only (consumed solely by the bundled dashboard, which moved to Tauri invoke in lockstep).
  • Added busy_timeout(5s) on the meridian pool for the new tray↔daemon concurrent-write topology — backward compatible.

⚠️ Documented v1 degradations (intentional, not regressions)

  • Audio droppedget_audio_snippets stubbed empty (in-process capture is text/OCR/a11y only). Classifier runs on text, so accuracy is unaffected; audio_snippets is simply always empty.
  • Idle detection off — no in-process idle signal yet, so gaps all classify system_sleep (capture_trigger is NULL).
  • Both are accepted v1 trade-offs with audio + idle-detection as future slices.

Semantic note: the /api removal and capture cutover are large architectural changes but not user-contract breaks — nothing a user, script, or external integration depends on was removed. Hence the minor bump is honest (see below).


Build & artifacts

  • DMG: 19.6 MB (Meridian.dmg), updater bundle Meridian.app.tar.gz 19.3 MB + minisign .sig + latest.json.
  • Release build ~5 min on the macOS runner (release-staging.yml: build → sign → updater-sign → package → publish + mirror).
  • Self-contained: the .app carries the daemon, a11y helper, embedded dashboard, and in-process capture; the MLX runtime provisions on first run.
  • Ships ad-hoc-signed today; notarization / Gatekeeper-clean first launch lands with the Apple Developer ID (org enrollment for MERIDIONA LLP currently In Review).

Quality & test coverage

  • 516 Rust test functions across the workspace — daemon 362, meridian-core 63, tray 27, integration 64 (tests/etl_*.rs, task_linker_smoke, tray_assets, worklog_provider, + meridian-core/tests/readers.rs, today_smoke.rs).
  • 7 JS/TS suites (ui/__tests__/: intervals, format, category-colors, cursor-pointer, redesign, lib, meridian-bin).
  • Ported readers are test-covered — in-memory seeded DB tests assert byte-for-byte parity with the deleted /api routes.
  • CI green on release: fold dashboard into Tauri + self-contained DMG + in-process capture (pre-main → main) #330: Rust (fmt + clippy -D warnings + test) 2m52s, UI build 41s, Migrations-append-only guard, screenpipe MIT-pin guard, CodeQL (rust/js/python/actions), CodeRabbit.
  • Every constituent PR passed the full pre-push suite (fmt + clippy + cargo test + UI build + UI tests + security audit).

Versioning

v1.63.0v1.64.0 (minor). semantic-release derives the bump from conventional commits: 72 feat: force at least a minor, and zero BREAKING CHANGE: footers / ! markers mean no major. Empirically confirmed — the staging channel already cut v1.64.0-staging.{1,2,3} from these exact commits.


Known follow-ups (deferred, non-blocking)

  • Next-fold deferred findingsweek.rs UTC bucketing, refresh.rs poll-loop efficiency, file-size splits (incl. mlx_server.rs 1183 lines), table_exists() DRY extraction.
  • Apple Developer ID (MERIDIONA LLP org enrollment, In Review) gates notarization / single-"Meridian" TCC / Gatekeeper-clean first launch — ships ad-hoc-signed until the cert lands.
  • Audio capture + in-process idle detection are future slices.
  • Tray redesign: timer drift + pending-drafts amber dot.

Risk

Every constituent change already merged via its own reviewed PR into pre-main; this PR is the promotion gate to main, not new unreviewed work. Each feature is independently revertable at its merge commit. CI is fully green.

🤖 Generated with Claude Code

Akarsh-Hegde and others added 30 commits June 16, 2026 11:25
Foundation spike for folding the dashboard into the Tauri app: the tray crate
now depends on the `meridian` lib (path dep, +66 transitive deps) and reads the
DB through the daemon's own query layer — no reimplemented SQL.

- db/meridian.rs: add open_existing() — opens an existing meridian.db WITHOUT
  running migrations or creating the file. The daemon owns the schema via
  setup_db(); a second process running the migrator would race it. Normal WAL
  connection so it reads correctly alongside the daemon's writes.
- tray: get_active command → open_existing → get_active_session, returning the
  live ActiveSession as JSON. The template for porting the dashboard read routes.

Verified: cargo check clean; reads the real active_session row. Per-call pool
open is a spike shortcut — pooled managed state follows next.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… proof)

Open meridian.db once at startup and share it with commands via Tauri managed
state, instead of opening a pool per call. Adds a webview panel that calls the
get_active command and renders the live active session read straight from
meridian.db — the full Rust-command-over-SQLite round trip the dashboard fold
will repeat for all 28 routes.

- db/mod.rs: re-export SqlitePool so the tray names it without a direct sqlx dep
- lib.rs: open_existing() once in setup → manage(Option<SqlitePool>); None
  (graceful) if the DB isn't openable yet, so the tray never crashes on it
- commands.rs: get_active now takes State<Option<SqlitePool>>; meridian_db_path
  exposed pub(crate) for the startup open
- tray UI: #db-active line invokes get_active and shows app_name + frame_count

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Spike result: give the dashboard a lean, single-source-of-truth data layer
instead of depending on the whole daemon. ActiveSession + open_existing +
get_active_session move into a new meridian-core crate (sqlx + serde + anyhow
only — no screenpipe/MLX/trackers/OTel). The daemon re-exports them so its code
is unchanged; the Tauri tray now depends on meridian-core, not the full lib.

Measured: tray deps 332 -> 301 (-31), and the removed crates are the heavy
daemon-only ones — OpenTelemetry, the tracker HTTP/oauth stack, and screenpipe
are now absent from the UI build (0 in the dep tree). Single source of truth:
both daemon and dashboard read through the same queries.

Also: drop a now-needless borrow in open_dashboard (clippy surfaced it once the
dep graph / feature unification changed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Stage 1 of porting /api/today to Rust: the wall-clock interval library that
/api/today (and /api/week) are built on, ported faithfully from ui/lib/intervals.ts
into meridian-core::intervals. union/merge/intersect/session_interval/count_switches,
plus normalize() (parse → drop invalid → sort → merge disjoint).

Timestamps are stored as full RFC3339 (+00:00), so JS getTime() and Rust
parse_from_rfc3339().timestamp_millis() agree exactly; merge_intervals emits
UTC millis-Z strings to match JS toISOString(). 6 golden tests pin the
semantics (overlap-once, drop-invalid, agent-cap, intersect, switch jitter).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…eature)

Dev-time visibility into the new Tauri/dashboard code, exported to the SAME
OpenObserve the daemon uses — without bloating the shipped binary.

- meridian-core: instrument open_existing + get_active_session with tracing
  spans (records uri + whether a row was found). The `tracing` facade is a no-op
  without a subscriber, so the default/release build stays lean.
- tray: instrument the get_active command; add an `otel` feature (default OFF)
  that pulls the full daemon lib ONLY to reuse meridian::observability::init —
  zero duplication of the OTLP/OpenObserve setup. lib.rs calls it at startup
  under #[cfg(feature = "otel")], tagged service.name = meridian-tray.

Verified: default build = 301 deps, OpenTelemetry 0 (lean); --features otel
pulls the OTel stack (17 crates) and wires the exporter. Run dev with
`cargo tauri dev --features otel` to see spans in OpenObserve.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`observability::init` ran at the top of run() before Tauri started its Tokio
runtime, so OpenTelemetry's batch exporter panicked ("there is no reactor
running, must be called from the context of a Tokio 1.x runtime"). Wrap the
init in tauri::async_runtime::block_on so it runs inside the global runtime;
the exporter's background task then lives on that runtime for the process life.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ils)

Stage 2 of porting /api/today: the day-boundary helpers, ported faithfully from
ui/lib/date-utils.ts into meridian_core::date. "Today" is the user's LOCAL day;
timestamps are stored UTC, so local_day_bounds() computes local 00:00:00 ..
local 23:59:59.999 and converts each to a UTC RFC3339 millis-Z string for the
`started_at >= ? AND started_at < ?` query bounds. today_string() is the local
calendar date.

4 golden tests pin the semantics in a timezone-independent way: the bounds span
a full local day (86_399_999 ms), the start round-trips to local midnight, the
format is millis+Z, and today_string is local YYYY-MM-DD.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add meridian-core to the Repository Layout (lean shared data layer: ActiveSession
+ open_existing + get_active_session, intervals.rs, date.rs) and a migration note
so any future Claude/dev session understands the in-progress fold: API routes →
Rust, single source of truth in meridian-core (daemon re-exports, tray depends),
Next stays static-export, dev-only --features otel exports tray spans to OO.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Stage 3: the full Today dashboard payload, ported from ui/app/api/today/route.ts
into meridian_core::today, built on the Stage 1/2 helpers (intervals + date).

- Reads app_sessions (optional category_explanation/session_summary detected via
  pragma_table_info and NULL-substituted when absent — matches the TS graceful
  check), active_session, gaps, pm_tasks.
- Splits foreground vs coding-agent streams; computes focus/idle/agent/supervised/
  autonomous seconds, presence/agent segments, switch count, per-task totals +
  autonomous splits, engaged_s, task meta, agent summaries — all via union/
  intersect/merge/session_interval/count_switches.
- get_today Tauri command (resolves local today + now); tracing span + a "today
  computed" event for OpenObserve visibility.

Golden-compare on the live DB: every field matches the Node /api/today exactly
(sessions 56, focus_s 9228, agent_s 8868, supervised 4043, autonomous 4825,
switch_count 13, presence_segments 20, …). #[ignore]d smoke test pins it +
asserts the invariants.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Enrich the freshly-ported get_today (meridian-core) so a single OpenObserve
trace tells the whole story — the gap the active-session path exposed (bare
timing spans, the one explanatory log filtered out at INFO).

- Promote the rollup summary from debug! to info! and widen it
  (session_count, supervised_s, gaps added). get_today is NOT on the daemon
  hot path (only the Tauri command + smoke test call it), so INFO won't spam
  daemon.log the way get_active_session would — and it surfaces at the tray's
  default INFO level without cranking RUST_LOG=meridian=debug.
- Add info_span! children on the four data reads (app_sessions, active_session,
  gaps, pm_tasks) via .instrument(), so the trace tree shows per-query timing —
  where the time actually goes. info_span (not debug_span) so the children
  populate at the default INFO filter, consistent with the summary.
- Add a debug! local-day-window event (start/end) for the UTC [start,end)
  bounds each read ran against — dig-deeper detail, left at DEBUG.

Verified: cargo clippy -p meridian-core -D warnings clean, 10 core unit tests
pass, today_smoke compiles (ignored, needs live DB), meridian-tray checks
clean against the changed core. OO trace-tree + correlated INFO log to be
eyeballed in dev via cargo tauri dev --features otel.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ui skill

- today_smoke.rs: add #[ignore]d today_perf — in-process get_today ~1.3ms/call
  (vs the Node HTTP route ~8.4ms; the gap is the HTTP + Node-runtime hop, not the
  SQL engine — better-sqlite3 is fast).
- meridian-ui/SKILL.md: forward-pointer noting the API routes are being ported to
  Rust/meridian-core (skill still describes the current Node dashboard on main).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Stage: port the 7-day Week summary from ui/app/api/week/route.ts into
meridian_core::week + a get_week Tauri command. Per day: SUM(duration_s)
GROUP BY category over the foreground stream only (agent overlay double-counts),
cats in hours, active session folded into today. Replicates the route's NAIVE
local-date string bounds (string-compared) — deliberately NOT /api/today's UTC
bounds — and its ms-based 7-day walk so DST behaves identically.

Golden-compare on the live DB: the 6 historical days are byte-identical (every
total_s + cats float); today matches modulo ~2s of active-session elapsed that
advanced between the Node curl and the Rust run (now-dependent, expected).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… browser)

Add a "Open Dashboard (native)" tray item that opens an in-app WebView window
(label "dashboard", dashboard.html) instead of the external browser. The page
calls the Rust commands get_today + get_week over meridian-core and renders the
figures — no Node server, no fetch, no browser. This is the dashboard-into-Tauri
end-state, demonstrated for the Today + Week views (the existing "Open Dashboard"
browser item is kept until the full fold lands).

With `--features otel` the get_today/get_week invocations emit spans to
OpenObserve (service.name = meridian-tray) when the window opens — live trace
visibility into the read path. Reuses the window if already open.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ems)

The poll loop's health-driven menu rebuild (poll.rs::update_toggle_menu)
hardcoded its own 5-item menu and clobbered setup()'s 6-item one within
~30s of launch, silently dropping "Open Dashboard (native)". Two menu
definitions had drifted.

Extract one build_tray_menu() in lib.rs as the sole definition of the
tray's items; both setup() and the poll path call it. Only the toggle
label is health-dependent. Adding an item now updates both call sites at
once — the drift that hid the native item can't recur.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the Setup wizard window (Phase 2 step 6): a 5-step flow (welcome →
permissions → model → connect tracker → done) opened from a new "Setup…"
tray item. Real now: the macOS privacy-pane deep-links
(open_permission_pane command → x-apple.systempreferences:). Honestly
stubbed and labelled as such: live permission/model detection and tracker
auth land in the next slices — no faked spinners or flows.

Also fixes a latent capability bug: capabilities/default.json scoped
invoke access to ["main"] only, so the dashboard AND setup webview windows
could not call any Tauri command (the dashboard path was never exercised
live because the menu item was being clobbered — see prior commit). Scope
now covers main/dashboard/setup and grants core:window:allow-close so the
wizard can close itself.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Audit of the meridian-core + tray commits surfaced four observability holes;
this closes the clear wins (deferred: tray OTel flush-on-quit, legacy poll.rs
HTTP instrumentation).

TRACES
- week.rs: mirror the today.rs enrichment. Promote the rollup summary
  debug! -> info! (week is user-facing + infrequent, not daemon-hot, so it
  surfaces at the default INFO filter) and add a per-day info_span!
  "week.read.day"{day} on the GROUP BY query so the trace tree shows which
  day's read costs what.
- open_existing: add a tracing::info!(uri) on success so that span has a
  correlated log to pivot to (it emitted nothing before) — once per process,
  benefits both tray and daemon.

LOGS (silent failures made visible)
- today.rs: the gaps / pm_tasks / active_session reads swallowed DB errors
  into empty data (.unwrap_or_default() / .ok().flatten()) with no trace —
  a missing table or locked DB produced a silently-empty dashboard. Each now
  warn!s on the error arm while keeping the graceful-empty fallback.
- week.rs: the today active_session read dropped its Err via if-let; now a
  match that warn!s.
- tray commands get_active/get_today/get_week: errors were returned to the
  webview but never logged server-side; warn! before stringifying so command
  failures appear in OO.

Verified: cargo fmt, clippy -D warnings clean on meridian-core + meridian-tray,
core unit tests pass (smoke/perf ignored — need live DB).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… window

Begins folding the dashboard into Tauri (one Next frontend, Rust commands).
This stage wires dev and stands up the first Next-in-Tauri window without
regressing anything — the `main` popover is dormant (visible:false, never
shown; the menu is the whole UI), so flipping devUrl is safe.

- tauri.conf.json: devUrl → http://localhost:3939 (next dev) + beforeDevCommand
  starts it. frontendDist stays ../src until the export cutover (stage 4).
- lib.rs: "Setup…" window → Next /setup route (was wizard.html); "Open
  Dashboard (native)" → the real Next dashboard /today (was dashboard.html).
- ui/app/setup/page.tsx: the wizard ported to Next/React, talking to Rust over
  the global __TAURI__ invoke bridge (open_permission_pane) — no /api, no Node.
  Permission/model/tracker remain honestly stubbed (live detection next slice).

The vanilla tray/src/{wizard,dashboard}.{html,js} are now unreferenced; they're
deleted in stage 3 once the Next windows are confirmed loading. Plan:
~/.claude/plans/meridian-next-fold.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Stage 0 pointed Tauri's devUrl at next dev, but Tauri watches the whole cargo
workspace (meridian/) to rebuild the Rust tray — and next dev rewrites
ui/.next on every compile/route request. Each write tripped "Rebuilding
application" and re-ran the tray binary, restarting it repeatedly (and
orphaning a menu-bar icon per restart). Visible as repeated "observability
initialised" and a tray that reloads when /setup is first requested.

Add a .taurignore at the watched root excluding ui/ (the frontend hot-reloads
via next dev on its own; Tauri never needs to watch it). Globs not bare
folders, per tauri#8901.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
First vertical slice of the route port: a dashboard page reading from a Rust
command instead of the /api route, with zero regression to the browser path.

- ui/lib/bridge.ts: shared Tauri bridge. `load(apiPath, command)` prefers the
  Rust command inside the Tauri window, falls back to /api in a plain browser
  (the dashboard is opened both ways during the fold). `invoke` for app-only
  actions. The fetch fallback is dropped at the export cutover (stage 4).
- TodayView: fetch('/api/today') → load('/api/today','get_today'). In the
  native window the Today view now runs entirely on meridian-core (no Node API
  hop); get_today is the already-ported, byte-identical command.
- setup/page.tsx: use the shared bridge (drop its local duplicate).

Pattern for the remaining routes: port the command, then switch its consumer's
fetch → load. The /api route stays until the export cutover.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Same dual-path swap as TodayView: fetch('/api/week') → load(...,'get_week').
get_week is the already-ported, byte-identical command, so this is zero-Rust.
All three pre-ported read commands (today/week) now drive their views in-app;
active needs a shape-matching command first (get_active returns the raw row,
/api/active returns a computed {app_name, elapsed_s}) — ported later.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
wizard.html/js and dashboard.html/js are dead since stage 0 pointed those
windows at the Next /setup and /today routes (lib.rs). Deleted now that the
Next replacements are confirmed loading (GET /setup 200, /today in-window).

Kept: index.html + app.js + style.css — still the frontendDist entry (the
build needs index.html) and the dormant `main` popover. They're removed in
stage 3 when the popover becomes a Next route and frontendDist moves to the
static export.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Daily coding-agent totals (Claude Code, Codex): per-agent + overall union of
today's agent sessions. Faithful port — unions the raw started_at/ended_at
spans (the route does NOT cap to duration_s), matching byte-for-byte.

No consumer swap: /api/coding-agents has no dashboard consumer (Today already
surfaces agent time via agent_s/agent_segments), so the route can't be deleted
until external use is ruled out. Command ported for parity + future use.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-app

get_worklogs(pool, day): the day's worklogs (pm_worklogs LEFT JOIN pm_tasks),
payload_json parsed into bullets/next_steps/risk_flags/summary/reasoning, with
the route's https-only task_url filter and state counts. day defaults to today.

WorklogsView: the GET fetch → load('get_worklogs', { day }). The /api/worklogs/[id]
mutations stay on fetch until those write routes are ported (later in stage 1).

Note: counts is a BTreeMap (sorted keys) vs the route's insertion order —
functionally identical (consumers read by key).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…in-app

The complex read: per-task time = your foreground time on the task + agent
time that ran while you were away (autonomous = agent union − intersect with
your presence), the foreground category split, week totals, unassigned, and
board-hygiene verdicts from pm_task_curation.

- meridian-core/hygiene.rs: faithful port of lib/hygiene.ts parseIssues +
  reason hint/fix/severity (the frontend keeps its types + hasMustFix).
- meridian-core/tasks.rs: get_tasks(pool, today, week_start, now_iso). Reuses
  session_interval/union/intersect/merge. Tolerates DBs predating
  pm_task_curation (mig 038) / ignored_codes (mig 040) via sqlite_master +
  pragma_table_info, same as the route. Snoozed-to-future rows drop off.
- get_tasks command resolves today / 6-days-ago / now.
- Consumers swapped fetch('/api/tasks') → load(...,'get_tasks'): TasksView,
  MustFixBanner, CleanupView, WorklogsView (reject-candidate lookup).

Note: cats/counts use BTreeMap (sorted keys) vs the route's insertion order —
functionally identical (read by key). Worth a live compare (native vs browser)
given the interval math; logic mirrors the route line-for-line.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The route reshapes the active_session row (elapsed_s = now − started_at,
JSON columns parsed, category default idle_personal) into ActiveSessionRow —
distinct from the raw get_active_session the daemon uses. New
meridian_core::active::get_active_view(pool, now); the get_active command now
returns this view (the dormant popover only read app_name/frame_count, both
present, so no live consumer changes shape).

Sidebar: fetch('/api/active') → load('/api/active','get_active').

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… GET

Per the no-drift decision: RuntimeSettings + load_runtime_settings +
settings_json_path move from the daemon's src/config.rs into
meridian-core::settings (single source of truth). The daemon re-exports them
(pub use), so config::{RuntimeSettings, load_runtime_settings} resolve
unchanged for every existing caller (observability.rs etc.). shellexpand is
replaced with a small ~/-expansion so the lean crate gains no dep; Serialize
is added so the dashboard command can return it.

- get_settings tray command (the ported /api/settings GET): reads via the
  shared reader, coerces Option::None string fields → '' and redacts
  oo_password to a sentinel, matching the route byte-for-byte.
- SettingsView GET: fetch('/api/settings') → load(...,'get_settings'). The PUT
  saves stay on fetch until the write route is ported.
- serde_json added as a direct tray dep (was only transitive).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Which trackers are connected. Split across the architecture line: the DB half
(pm_sync_state last errors) is meridian_core::integrations::sync_errors; the
env/file half is a tray module (filesystem, not a shared DB read).

- OAuth providers (jira/trello) detected by ~/.meridian/oauth/<p>.json
  (install-independent, exact). Token providers (linear/github/azure) by .env
  keys with the route's placeholder filter (your- / _your_ / -here).
- Env path mirrors the DAEMON (bundle ~/.meridian/app/.env, else first .env
  walking up from cwd = dotenvy) rather than the route's NODE_ENV split, so it
  reflects what the daemon reads. Approximate when multiple .envs coexist;
  documented in the module. OAuth detection is exact regardless.
- TasksView: 3 GET reads → load(...,'get_integrations'). DELETE + azure
  discover (writes) stay on fetch until those routes are ported.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Closes the test gap on the riskiest ported logic:
- hygiene::parse_issues — 6 unit tests (pure, no DB): fixable-only filter,
  ignored-codes filter, must_fix vs optional severity, thin_description hint
  char count, fix shape, empty/malformed → [].
- tests/readers.rs — in-memory single-connection SQLite seeded with hand-
  computable rows: coding_agents union/dedup + per-agent sort; tasks autonomous
  = agent-time-outside-presence (1800s) and today_s = your + autonomous (5400s),
  placed relative to local_day_bounds(today) so it's timezone-independent.

sqlx added to dev-dependencies (regular dep, but integration tests need it in
scope). Still TODO: worklogs/active shaping tests + the tray file/env commands
(settings redaction, integrations env parsing).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make the DB→UI data flow fully traceable: with RUST_LOG=meridian=debug a single
page render emits the complete sequence of fetch operations through
meridian-core, while INFO stays a one-line-per-page heartbeat.

Uniform convention applied to active / integrations / coding_agents / worklogs /
tasks / today / week:
- every DB op wrapped in a debug_span!("<mod>.read.<thing>") (per-query timing in
  the OO trace tree) + a debug!(rows/found = …) event right after (the readable
  flow in the logs).
- one INFO summary per page render (the heartbeat). today/week already INFO;
  promoted coding_agents/worklogs/tasks debug!→info!; added an INFO summary to
  integrations. active stays all-DEBUG (the popover polls it — no INFO spam).
- today/week query spans switched info_span!→debug_span! so the whole flow sits
  at one level (the granular ops are DEBUG, consistent with the rest).

tasks.rs (7 reads) instruments pm_tasks, today/week presence + sessions, the
unassigned sum, and the three pm_task_curation probes in load_hygiene, each
labelled by scope where it repeats.

Commands already carry #[tracing::instrument] (the request root span) + warn! on
error, so the chain is command → core fn → per-op debug spans/events → summary.

Verified: cargo fmt, clippy -D warnings clean on meridian-core + meridian-tray,
18 core unit tests pass (smoke/perf ignored — need live DB).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two triage-family routes, landing on opposite sides of the architecture line,
both with full module/function docs (purpose, callers, related modules) and the
richer tracing pattern (per-query debug_span + row-count debug! + info! summary).

- meridian_core::triage (DB read): the cleanup working set —
  pm_task_curation ⋈ pm_tasks, worst-first, snooze-filtered, each reason hinted.
  Its reason_hint is triage-specific (distinct wording from hygiene's — NOT
  shared). Tolerates a pre-migration-038 DB (has_run=false). get_triage command
  added; PARITY only — no dashboard consumer (cleanup reads hygiene via
  /api/tasks), documented as such.
- tray parents module (shell-out): get_ticket_parents spawns
  `meridian ticket-parents` (native-binary-first resolution, 30s timeout, parses
  the last JSON line), returning a 200-equivalent payload with an `error` field
  on failure rather than throwing — matching the route. HygieneDialog's
  parent-picker now load()s it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Akarsh-Hegde and others added 11 commits June 24, 2026 14:03
refactor(tray): unified install-mode env resolution + path visibility
Stand up a pre-production staging release channel on `pre-main` that exercises
the full pipeline + app + live auto-update before `pre-main → main`, without
touching production. Reuses the proven staging skeleton (GitHub-only,
prerelease-versioned) and grafts on the DMG auto-update piece the legacy
staging config predated.

Four new files:
- tray/src-tauri/tauri.staging.conf.json — build-time --config overlay pointing
  the updater at the staging endpoint (…/releases/download/updater-staging/
  latest.json). Never committed into tauri.conf.json, so it cannot leak to main.
- .releaserc.staging.json — staging semantic-release config: branches adds
  {pre-main, prerelease: staging}; set-version runs FIRST (fixes the legacy
  version-baking footgun); tray build takes the staging overlay; GitHub-only
  (dropped npm/git/changelog plugins so no version-bump commits pollute
  pre-main's tree); publishCmd mirrors latest.json to the rolling staging tag.
- scripts/mirror-staging-release.sh — clobbers latest.json (+ a stable DMG)
  onto a fixed `updater-staging` prerelease so installed staging apps self-update.
  Mirrors the repo's existing runtime-staging fixed-tag convention.
- .github/workflows/release-staging.yml — workflow_dispatch-only (a full macOS
  build is ~45 min); checks out pre-main, adds TAURI_SIGNING_* secrets the
  legacy staging workflow lacked, cp's the staging config in-CI (ephemeral).

Production (main: release.yml + .releaserc.json) is untouched and isolated —
the staging endpoint is baked only at build time via --config.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EFeteroZ8SZnnJXsNLPxmE
…t on main

The workflow_dispatch-only design hit two GitHub constraints: (1) a dispatch by
name needs the workflow registered, which normally requires it on the default
branch; (2) semantic-release picks the channel from GITHUB_REF, so the run must
carry ref=pre-main or it would treat the build as `main` and cut a STABLE
release from pre-main code. Keeping the workflow off `main` and triggering it
from pre-main resolves both.

pre-main is a busy integration branch, so guard the build: it runs only on a
manual dispatch OR a push whose head-commit message contains [staging-release]
— ordinary fold/feature merges into pre-main never trigger a 45-min build. The
first marker-commit push also registers the workflow, after which the dispatch
button / `gh workflow run --ref pre-main` work too.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EFeteroZ8SZnnJXsNLPxmE
ci(release): DMG auto-update staging channel on pre-main
The first staging build (v1.64.0-staging.1) failed at cargo resolution: the
tray's capture stack (screenpipe-screen/-a11y) is a git dep on the PRIVATE
Meridiona/screenpipe-fork, and CI's default GITHUB_TOKEN is scoped to this repo
only, so `git fetch` of the fork 403s ("Write access to repository not
granted"). No workflow has ever built the tray with the fold, so this gap was
invisible until now — and it would break the production release.yml at cutover
too.

Rewrite just the screenpipe-fork URL to embed the GH_TOKEN PAT, and force
CARGO_NET_GIT_FETCH_WITH_CLI so cargo honors it. Requires GH_TOKEN to have read
access to Meridiona/screenpipe-fork.

Note: release.yml (production) needs the same fix before pre-main → main.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EFeteroZ8SZnnJXsNLPxmE
ci(release): authenticate CI git to the private screenpipe-fork dep
Cut a second staging build so the installed v1.64.0-staging.1 has a newer
version to self-update to — validates the live staging→staging auto-update.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EFeteroZ8SZnnJXsNLPxmE
Empty fix-type commit so semantic-release bumps to 1.64.0-staging.2 (the
previous test:-typed commit warranted no release). Gives the installed
staging.1 a newer version to self-update to.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EFeteroZ8SZnnJXsNLPxmE
…er [staging-release]

create-icons.sh regenerates every app-icon size from meridiona-mark.png at
build time. That source file held a 220px ring-dot placeholder, so each build
CLOBBERED the committed spirograph icon.icns with ring-dot icons — which is why
the packaged app shipped the wrong logo.

Replace meridiona-mark.png with the 1024px gradient-spirograph (extracted with
alpha from the already-committed icon.icns), so create-icons.sh regenerates the
correct brand icons. Only the source mark changes — the generated outputs
already matched the spirograph, confirming the placeholder was the sole defect.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EFeteroZ8SZnnJXsNLPxmE
@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown

Important

Review skipped

Too many files!

This PR contains 219 files, which is 69 over the limit of 150.

To get a review, narrow the scope:
• coderabbit review --type committed # exclude uncommitted changes
• coderabbit review --dir # limit to a subdirectory
• coderabbit review --base # compare against a closer base

Upgrade to a paid plan to raise the limit.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 68a07fa9-0943-4312-aee9-56ed5340f20e

📥 Commits

Reviewing files that changed from the base of the PR and between 72a2a66 and 768f47b.

⛔ Files ignored due to path filters (5)
  • Cargo.lock is excluded by !**/*.lock
  • package-lock.json is excluded by !**/package-lock.json
  • services/uv.lock is excluded by !**/*.lock
  • tray/src-tauri/icons/meridiona-mark.png is excluded by !**/*.png
  • ui/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (219)
  • .cargo/config.toml
  • .claude/skills/meridian-ui/SKILL.md
  • .github/workflows/build-mlx-runtime.yml
  • .github/workflows/ci.yml
  • .github/workflows/release-staging.yml
  • .github/workflows/release.yml
  • .gitignore
  • .releaserc.json
  • .releaserc.staging.json
  • .taurignore
  • CLAUDE.md
  • Cargo.toml
  • README.md
  • SETUP.md
  • install.sh
  • meridian-core/Cargo.toml
  • meridian-core/src/capture.rs
  • meridian-core/src/db.rs
  • meridian-core/src/lib.rs
  • meridian-core/src/notifications.rs
  • meridian-core/src/readers/active.rs
  • meridian-core/src/readers/coding_agents.rs
  • meridian-core/src/readers/current_task.rs
  • meridian-core/src/readers/integrations.rs
  • meridian-core/src/readers/mod.rs
  • meridian-core/src/readers/notices.rs
  • meridian-core/src/readers/plan.rs
  • meridian-core/src/readers/task_detail.rs
  • meridian-core/src/readers/tasks.rs
  • meridian-core/src/readers/today/mod.rs
  • meridian-core/src/readers/today/types.rs
  • meridian-core/src/readers/triage.rs
  • meridian-core/src/readers/week.rs
  • meridian-core/src/readers/worklogs.rs
  • meridian-core/src/settings.rs
  • meridian-core/src/util/date.rs
  • meridian-core/src/util/hygiene.rs
  • meridian-core/src/util/intervals.rs
  • meridian-core/src/util/mod.rs
  • meridian-core/tests/readers.rs
  • meridian-core/tests/today_smoke.rs
  • package.json
  • scripts/build-mlx-runtime.sh
  • scripts/com.meridiona.ui.plist
  • scripts/dev-signing.sh
  • scripts/install-from-bundle.sh
  • scripts/install-ui-daemon.sh
  • scripts/meridian-cli.sh
  • scripts/meridian-npm-setup.sh
  • scripts/mirror-staging-release.sh
  • scripts/package-release.sh
  • scripts/package-updater.sh
  • scripts/set-version.sh
  • scripts/smoke-test-mlx-runtime.sh
  • scripts/test-updater-github.sh
  • scripts/test-updater-local.sh
  • scripts/ui-start.sh
  • scripts/uninstall-ui-daemon.sh
  • scripts/verify-release-bundle.sh
  • services/agents/run_task_linker_mlx.py
  • services/agents/server.py
  • src/config.rs
  • src/db/meridian.rs
  • src/db/mod.rs
  • src/db/screenpipe.rs
  • src/etl/block_ops.rs
  • src/etl/extractor.rs
  • src/etl/runner.rs
  • src/health/capture.rs
  • src/health/fix.rs
  • src/health/mlx.rs
  • src/health/mod.rs
  • src/health/platform.rs
  • src/health/ui.rs
  • src/intelligence/oauth/github.rs
  • src/intelligence/oauth/jira.rs
  • src/intelligence/oauth/mod.rs
  • src/intelligence/task_linker/mod.rs
  • src/intelligence/task_triage/store.rs
  • src/lib.rs
  • src/main.rs
  • src/migrations/046_capture_frames.sql
  • src/migrations/047_capture_ui_events.sql
  • src/migrations/048_reset_cursor_for_capture_cutover.sql
  • src/pm_worklog/synth.rs
  • src/telemetry_spool/shipper.rs
  • src/telemetry_spool/writer.rs
  • src/uninstall.rs
  • tests/common/mod.rs
  • tests/etl_basic.rs
  • tests/etl_gaps.rs
  • tests/etl_session_close.rs
  • tests/etl_session_text.rs
  • tests/etl_ui_events.rs
  • tests/task_linker_smoke.rs
  • tests/tray_assets.rs
  • tray/create-icons.sh
  • tray/package.json
  • tray/src-tauri/Cargo.toml
  • tray/src-tauri/build.rs
  • tray/src-tauri/capabilities/default.json
  • tray/src-tauri/src/backend_install.rs
  • tray/src-tauri/src/capture/mod.rs
  • tray/src-tauri/src/capture/screenpipe.rs
  • tray/src-tauri/src/capture/ui_events.rs
  • tray/src-tauri/src/commands.rs
  • tray/src-tauri/src/commands/daemon.rs
  • tray/src-tauri/src/commands/dashboard.rs
  • tray/src-tauri/src/commands/health.rs
  • tray/src-tauri/src/commands/integrations.rs
  • tray/src-tauri/src/commands/logs.rs
  • tray/src-tauri/src/commands/notices.rs
  • tray/src-tauri/src/commands/notifications.rs
  • tray/src-tauri/src/commands/openobserve.rs
  • tray/src-tauri/src/commands/parents.rs
  • tray/src-tauri/src/commands/settings.rs
  • tray/src-tauri/src/commands/setup.rs
  • tray/src-tauri/src/commands/system.rs
  • tray/src-tauri/src/commands/tasks.rs
  • tray/src-tauri/src/commands/triage.rs
  • tray/src-tauri/src/commands/version.rs
  • tray/src-tauri/src/commands/worklogs.rs
  • tray/src-tauri/src/format.rs
  • tray/src-tauri/src/install.rs
  • tray/src-tauri/src/lib.rs
  • tray/src-tauri/src/mlx_server.rs
  • tray/src-tauri/src/poll.rs
  • tray/src-tauri/src/poll/live.rs
  • tray/src-tauri/src/poll/mod.rs
  • tray/src-tauri/src/poll/notifications.rs
  • tray/src-tauri/src/poll/refresh.rs
  • tray/src-tauri/src/state.rs
  • tray/src-tauri/src/sys.rs
  • tray/src-tauri/src/tray.rs
  • tray/src-tauri/src/tray_icon.rs
  • tray/src-tauri/src/update.rs
  • tray/src-tauri/tauri.conf.json
  • tray/src-tauri/tauri.staging.conf.json
  • tray/src/app.js
  • tray/src/index.html
  • tray/src/style.css
  • tray/src/tooltip.css
  • tray/src/tooltip.html
  • tray/src/tooltip.js
  • ui/.gitignore
  • ui/__tests__/intervals.test.ts
  • ui/app/api/active/route.ts
  • ui/app/api/auth/oauth/start/route.ts
  • ui/app/api/auth/token/route.ts
  • ui/app/api/coding-agents/route.ts
  • ui/app/api/daemon/reload/route.ts
  • ui/app/api/daemon/status/route.ts
  • ui/app/api/health/route.ts
  • ui/app/api/health/stream/route.ts
  • ui/app/api/integrations/azure-devops/discover/route.ts
  • ui/app/api/integrations/route.ts
  • ui/app/api/logs/route.ts
  • ui/app/api/logs/stream/route.ts
  • ui/app/api/notices/[id]/route.ts
  • ui/app/api/notices/stream/route.ts
  • ui/app/api/notifications/[id]/delivered/route.ts
  • ui/app/api/notifications/[id]/dismiss/route.ts
  • ui/app/api/notifications/allowed/route.ts
  • ui/app/api/notifications/pending/route.ts
  • ui/app/api/notifications/stream/route.ts
  • ui/app/api/openobserve/route.ts
  • ui/app/api/plan/route.ts
  • ui/app/api/plan/task/route.ts
  • ui/app/api/settings/route.ts
  • ui/app/api/tasks/route.ts
  • ui/app/api/tasks/sync/route.ts
  • ui/app/api/today/route.ts
  • ui/app/api/triage/apply/route.ts
  • ui/app/api/triage/decision/route.ts
  • ui/app/api/triage/ignore/route.ts
  • ui/app/api/triage/parents/route.ts
  • ui/app/api/triage/route.ts
  • ui/app/api/update/route.ts
  • ui/app/api/version/route.ts
  • ui/app/api/week/route.ts
  • ui/app/api/worklogs/[id]/route.ts
  • ui/app/api/worklogs/route.ts
  • ui/app/globals.css
  • ui/app/setup/atoms.tsx
  • ui/app/setup/data.ts
  • ui/app/setup/page.tsx
  • ui/app/setup/steps.tsx
  • ui/components/DayTimeline.tsx
  • ui/components/HealthBanner.tsx
  • ui/components/HygieneDialog.tsx
  • ui/components/MustFixBanner.tsx
  • ui/components/NoticeBar.tsx
  • ui/components/NotificationBanner.tsx
  • ui/components/ShapeOfDay.tsx
  • ui/components/Sidebar.tsx
  • ui/components/plan/TaskDialog.tsx
  • ui/components/views/CleanupView.tsx
  • ui/components/views/LogsView.tsx
  • ui/components/views/PlanView.tsx
  • ui/components/views/SessionsView.tsx
  • ui/components/views/SettingsView.tsx
  • ui/components/views/TasksView.tsx
  • ui/components/views/TodayView.tsx
  • ui/components/views/WeekView.tsx
  • ui/components/views/WorklogsView.tsx
  • ui/instrumentation.ts
  • ui/lib/api-types.ts
  • ui/lib/bridge.ts
  • ui/lib/daily-plan.ts
  • ui/lib/db-write.ts
  • ui/lib/db.ts
  • ui/lib/intervals.ts
  • ui/lib/notices-store.ts
  • ui/lib/notifications-banner-store.ts
  • ui/lib/notifications.ts
  • ui/lib/observability.ts
  • ui/lib/settings.ts
  • ui/next.config.ts
  • ui/package.json

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 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.

Comment thread services/agents/server.py
try:
if f.is_file():
total += f.stat().st_size
except OSError:
The CI Rust job (fmt/clippy/test) resolves the whole workspace, including
the tray's optional capture deps (screenpipe-screen/-a11y) which are a git
dependency on the PRIVATE Meridiona/screenpipe-fork. With only the repo-scoped
GITHUB_TOKEN, cargo's `git fetch` of the fork fails ("could not read Username
for github.com"), so clippy/test never start.

Port the exact mechanism release.yml / release-staging.yml already use:
- CARGO_NET_GIT_FETCH_WITH_CLI=true so cargo honors url.insteadOf (libgit2 won't)
- a git config step rewriting the fork URL to embed secrets.GH_TOKEN

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SKdh3tKWLZhtQ9FCfA5RYH
Akarsh-Hegde and others added 3 commits June 24, 2026 17:45
ci: authenticate private screenpipe-fork git dep in the Rust job
…fork dep

Mirror the fix already proven on release-staging.yml into the production
pipeline. Without it, the first stable release after the fold lands on main
fails at cargo resolution with a 403 on Meridiona/screenpipe-fork (the tray's
capture git dep), exactly as the first staging build did. Adds the url.insteadOf
GH_TOKEN rewrite + CARGO_NET_GIT_FETCH_WITH_CLI. Lands on pre-main so the
pre-main -> main cutover carries it into the production release.yml.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EFeteroZ8SZnnJXsNLPxmE
…rade

A user migrating from the old npm/curl bundle to the self-contained DMG
carries launchd agents and a credential file the new topology no longer
matches. The DMG install path only cleaned up the legacy screenpipe agent;
three gaps remained:

- com.meridiona.mlx-server (bundle launchd MLX on :7823) was left running,
  contending with the tray's in-process MlxManager on the same port →
  EADDRINUSE spawn-retry churn + log spam.
- com.meridiona.ui (retired standalone dashboard server) lingered as a
  zombie Node process burning ~150 MB.
- Bundle creds at ~/.meridian/app/.env were not copied to the canonical
  ~/.meridian/.env the DMG daemon reads → Jira/GitHub/Linear tokens silently
  lost, forcing a wizard re-entry.

Add cleanup_legacy_mlx_server() + cleanup_legacy_ui() (mirroring the existing
cleanup_legacy_screenpipe idiom) and migrate_legacy_bundle_env(), which copies
the bundle .env across only when the canonical one is absent — never clobbering
creds the tray already wrote. Covered by a unit test over all three branches.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SKdh3tKWLZhtQ9FCfA5RYH

@adityaharishch adityaharishch left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review — PR #330 (pre-main → main)

Reviewed 8 angles across 223 changed files (diff too large for the GitHub API; files fetched directly). Findings ranked most-severe first. Four are posted inline; four below could not be pinned to diff lines.


[CORRECTNESS — HIGH] src/health/ui.rs + src/health/platform.rs probe the deleted Next.js server — meridian doctor is permanently CRITICAL on every healthy post-PR install

platform.rs still declares const LABEL_UI: &str = "com.meridiona.ui" and ui_service() calls plist_check(LABEL_UI, "ui plist") which returns Check::critical("missing or invalid"). backend_install.rs no longer registers that launchd agent (only daemon + a11y). ui.rs still GETs http://localhost:3939/api/health (nothing listens post-fold). Both are called unconditionally from run_all().

On every healthy post-PR install:

  • ✗ CRITICAL “ui plist — missing or invalid” (remedy: ./install.sh — can never clear)
  • ⚠ WARN “ui service — not loaded — dashboard unavailable”
  • ⚠ WARN “ui serving — not reachable at http://localhost:3939”

Report::worst() → Critical → meridian doctor exits non-zero on healthy machines. The entire UI health group needs to be replaced with embedded-webview health semantics or removed.


[SECURITY — MEDIUM] install.sh downloads OpenObserve with no integrity check, then registers it as a launchd agent

Around L602–629, the installer curls the OpenObserve binary from downloads.openobserve.ai, extracts, chmod +xs, and registers it as a launchd agent with no sha256 or signature verification. Version is pinned (v0.90.3) but artifact integrity is not. A compromised host or MITM gives an attacker a trojan that runs continuously under the user’s launchd.

Fix: pin and verify a sha256 for the OO tarball before executing it.


[SECURITY — MEDIUM] scripts/install-from-bundle.sh fallback sudo npm install -g screenpipe runs npm lifecycle scripts as root

Around L1262–L1265, when the global npm prefix is not user-writable, the script falls back to sudo npm install -g screenpipe@0.4.6. The script explicitly refuses to run as root elsewhere (around L1222) — this single path hands root to npm postinstall scripts from a third-party package. A compromised screenpipe@0.4.6 executes as root.

Fix: use --ignore-scripts or stage into a user-writable prefix instead of escalating.


[CORRECTNESS — MEDIUM] src/health/capture.rs capture coverage datetime filter is a no-op against RFC3339 timestamps

capture_coverage filters with WHERE timestamp > datetime('now', '-1 day'). SQLite datetime() produces '2026-06-23 17:00:00' (space separator, no Z). insert_capture_frame writes to_rfc3339_opts(Micros, true) = '2026-06-24T16:00:00.000000Z' (T separator, Z suffix). String compare at index 10: 'T' (0x54) > ' ' (0x20) → every row passes the filter regardless of actual date. The -1 day window never excludes anything → coverage check evaluates all-time frames → ghosting goes undetected.

frame_freshness() in the same file already uses julianday() correctly. Fix: adopt the same julianday() arithmetic for capture_coverage.

///
/// `None` → no runtime published for this channel (wizard shows "not available").
pub fn manifest_url() -> Option<String> {
if let Ok(url) = std::env::var("MERIDIAN_RUNTIME_MANIFEST_URL") {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SECURITY — HIGH] MLX runtime tarball has no signature — SHA-256 is self-referential and MERIDIAN_RUNTIME_MANIFEST_URL is redirectable in production

manifest_url() reads MERIDIAN_RUNTIME_MANIFEST_URL unconditionally in release builds. The comment at L200–204 that this “only fires in dev” is a deployment assumption, not an enforced gate (#[cfg(debug_assertions)]).

Combined with download_and_stage (~L340–448) which only verifies a SHA-256 fetched from the same manifest it just downloaded (no minisign), an attacker who can set this env var — or MITM the HTTPS manifest endpoint — controls both the tarball URL and the expected hash, and gets arbitrary code execution: bin/python from the staged runtime is spawned in start() (~L905–915).

The .app updater correctly uses tauri-plugin-updater with an embedded minisign pubkey; this second update channel has no equivalent protection.

Fix: gate the env override behind #[cfg(debug_assertions)]; add minisign verification to download_and_stage matching the updater path.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partially resolved in PR #333 (merged to pre-main).

The env-var path is now gated behind #[cfg(debug_assertions)] — a release binary can no longer be redirected via a compromised environment variable.

The self-referential SHA-256 issue (manifest and tarball served from the same origin, so a compromised server can swap both atomically) is a separate architectural gap — it requires an out-of-band trust root (minisign public key embedded in the binary, similar to the updater's pub_key). Tracked as a follow-up design task; not blocking the fold.

Comment thread tray/src-tauri/src/poll/mod.rs
Comment thread ui/lib/bridge.ts Outdated
# dist/meridian-mlx-runtime-<ver>-aarch64.tar.gz (CPython + venv + agents pkg + launcher)
# dist/runtime-manifest.json (version, url, sha256, size, floors)
#
# The tarball bundles its OWN python-build-standalone CPython, so the customer

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SECURITY — MEDIUM] CPython bundled from unpinned PBS releases/latest with no checksum

When PBS_ASSET_URL is unset (the default CI path), the script resolves the CPython interpreter from astral-sh/python-build-standalone at releases/latest — unpinned — and the downloaded archive is extracted without verifying a sha256 before tar -xzf.

This chains into the MLX runtime signing gap: the runtime tarball ships this interpreter, the tarball is only verified against a self-referential SHA-256 in the unsigned manifest, and now the interpreter itself comes from an unverified upstream.

Fix: pin PBS_ASSET_URL to an exact release tag and verify the archive against the published sha256 before extraction.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deferred — not resolved in PR #333.

Pinning to a specific PBS release tag and verifying the downloaded archive are straightforward changes, but they require the real sha256 of the target release. Adding a fabricated/guessed hash would be worse than leaving it unpinned. Tracked as a follow-up: pin PBS_RELEASE_TAG and add sha256sum --check before tar -xzf.

Akarsh-Hegde and others added 2 commits June 24, 2026 22:26
fix(install): clean up legacy bundle agents + migrate .env on DMG upgrade
- health/ui.rs + platform.rs: remove legacy Node-era HTTP probes
  (serve_health, serve_mode_check, asset checks) — the dashboard is
  now a static export embedded in the Tauri binary; no com.meridiona.ui
  launchd agent or HTTP port exists post-fold. meridian doctor was
  reporting CRITICAL on every healthy post-fold install. Both checks now
  return an Info line.

- health/capture.rs: fix capture_coverage datetime comparison. The
  query used datetime('now', ?1) which produces '2026-06-23 17:00:00'
  (space separator, no Z), while insert_capture_frame writes RFC 3339
  '2026-06-24T16:00:00.000000Z' (T+Z). String comparison at index 10 had
  'T' > ' ' so every row passed the filter — the 1-day coverage window
  never excluded anything. Rewritten as julianday(timestamp) >
  julianday('now', ?1) following the existing frame_freshness pattern.

- poll/mod.rs (supervise_mlx): add MLX_COOLING_TICKS=10 cooling period.
  Budget-exhausted path now lets attempts grow past MLX_MAX_RESTARTS; at
  MAX+COOLING ticks it resets to 0 so supervise() can kill any zombie on
  port 7823 and attempt a new restart cycle. Prevents permanent wedge
  after OOM/crash.

- bridge.ts (subscribe): replace silent .catch(() => {}) with retry-once-
  after-2s for the cold-DB startup race (tray beats daemon on launch),
  then console.warn on second failure so it is visible in DevTools.

- install-from-bundle.sh: add --ignore-scripts to both npm install lines
  for screenpipe. The sudo path previously ran lifecycle scripts as root.

- mlx_server.rs (manifest_url): gate MERIDIAN_RUNTIME_MANIFEST_URL env-
  var override behind #[cfg(debug_assertions)]. Kills the "env redirects
  the manifest in production" half of the HIGH security finding. The
  self-referential SHA-256 issue (manifest and tarball served from the
  same origin) requires an out-of-band trust root and is deferred.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SKdh3tKWLZhtQ9FCfA5RYH
@Akarsh-Hegde

Copy link
Copy Markdown
Member Author

Review findings resolved → PR #333

All findings from @adityaharishch's review have been addressed in PR #333 (fix/pr-330-review → pre-main). Summary:

Fixed

Severity Finding Fix
HIGH CORRECTNESS meridian doctor exits non-zero on every healthy post-fold install — ui_service() + ui::checks() probe the retired com.meridiona.ui Node server Replaced both with Info checks: "dashboard embedded in the Tauri binary (no separate service)". Removed the HTTP probe, plist check, .next check, serve_mode_check, and their unit tests.
HIGH CORRECTNESS MLX restart-budget exhaustion permanently wedges — supervise_mlx never resets attempts when /health keeps failing, so the zombie holding port 7823 is never cleared Added MLX_COOLING_TICKS = 10 (~10 min). attempts now increments through the cooling window past MLX_MAX_RESTARTS; at MAX + COOLING ticks it resets to 0 so supervise() runs again (with its existing KilledWedged path) and a fresh restart cycle begins.
HIGH SECURITY (partial) MERIDIAN_RUNTIME_MANIFEST_URL env-var redirectable in production Gated behind #[cfg(debug_assertions)] — release binary cannot be redirected via a compromised env var. The self-referential SHA-256 issue (manifest + tarball on the same origin) requires an out-of-band trust root (minisign, like the updater); design tracked as a follow-up.
MEDIUM CORRECTNESS capture_coverage datetime format mismatch — datetime('now', ?1) produces '…T…Z' vs ' '-separated stored timestamps; 'T' > ' ' so every row passes the filter Fixed to julianday(timestamp) > julianday('now', ?1) following the frame_freshness pattern in the same file.
MEDIUM CORRECTNESS subscribe() snapshot prime silently swallows errors on cold-DB startup Retry-once-after-2s on first failure; console.warn(...) on second failure — visible in DevTools.
MEDIUM SECURITY sudo npm install runs lifecycle scripts as root Added --ignore-scripts to both npm install lines in install-from-bundle.sh.

Deferred (acknowledged, not blocking)

  • install.sh OO sha256 and build-mlx-runtime.sh PBS pin: both need real hash values from the upstream release pages — straightforward follow-ups.
  • MLX manifest self-referential SHA-256: requires a minisign/ed25519 embedded public key (same mechanism as the updater's pub_key). Tracked separately.

fix(health): resolve PR #330 review findings
@Akarsh-Hegde

Copy link
Copy Markdown
Member Author

Review findings — status update (PR #333 merged to pre-main)

All inline threads updated. Summary of what's resolved vs. still open:

Thread Status
poll/mod.rs — MLX restart-budget exhaustion permanently wedges Resolved (PR #333) — MLX_COOLING_TICKS = 10 added; attempts counts through a ~10-min cooling window then resets, letting supervise() kill any zombie and start a fresh restart cycle
ui/lib/bridge.tssubscribe() snapshot prime silently swallows errors Resolved (PR #333) — retry-once-after-2s for the cold-DB startup race; console.warn on second failure
src/health/ui.rs + platform::ui_service()doctor CRITICAL on healthy installs Resolved (PR #333) — removed all Node-era HTTP probes and plist checks; both now return Info: dashboard embedded in the Tauri binary
src/health/capture.rscapture_coverage datetime format mismatch Resolved (PR #333) — julianday(timestamp) > julianday('now', ?1) in both subqueries
install-from-bundle.shsudo npm install runs lifecycle scripts as root Resolved (PR #333) — --ignore-scripts added to both npm install lines
mlx_server.rsMERIDIAN_RUNTIME_MANIFEST_URL redirectable in production ⚠️ Partially resolved (PR #333) — env-var path gated behind #[cfg(debug_assertions)]; self-referential SHA-256 (manifest + tarball from same origin) requires an out-of-band trust root (minisign embedded key), tracked as a follow-up
build-mlx-runtime.sh — PBS releases/latest + no checksum 🔲 Deferred — needs real sha256 of the pinned PBS release; adding a fabricated hash would be worse than leaving it unpinned
services/agents/server.py — empty except clause 🔲 Not addressed — out of scope for the fold; tracked separately

PR #333 is merged into pre-main. The remaining two open items are tracked as follow-ups and are not blocking the pre-main → main promotion.

@Akarsh-Hegde

Copy link
Copy Markdown
Member Author

The three deferred items are now tracked in the Obsidian tech backlog (Architecture/Tech Backlog.md):

  • MLX runtime manifest out-of-band signature (minisign pub key embedded in binary)
  • build-mlx-runtime.sh PBS release pin + sha256 verification
  • services/agents/server.py empty except clause

@Akarsh-Hegde Akarsh-Hegde merged commit 83bb329 into main Jun 24, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants