Originally created by: Akarsh-Hegde
Starts Bucket 2 (in-process capture) — the cutover that makes the drag-to-Applications DMG a complete product (one "Meridian" Screen-Recording TCC entry, no external screenpipe). Plan: Obsidian Decisions/Bucket 2 implementation plan - in-process capture.md. Spike verdict: GO.
All behind the off-by-default capture feature — default builds, CI, and the current DMG are unchanged and pull none of the capture stack.
Created Meridiona/screenpipe-fork (private) — a clean snapshot of mediar-ai/screenpipe @ last-MIT 892199f74 + a provenance NOTICE (MIT LICENSE preserved; nothing ≥ the commercial relicense 81e412ff5).
capture/mod.rs)CapturedFrame mirrors the columns meridian's ETL reads (app/window/url/timestamp + text + text_source). Text-only — no pixels/video (privacy; no ffmpeg binary needed).CaptureEngine trait + mpsc FrameTx → backend (screenpipe-screen now, scap later) and consumer (logger now, meridian.db writer in slice 4) are independently swappable.capture/screenpipe.rs)ScreenpipeEngine via the fork: list_monitors → capture_monitor_image → perform_ocr_apple, in-process, emitting OCR text.CGRequestScreenCaptureAccess() at init (the lib only preflights + returns empty on denial — never prompts; the key spike finding).anyhow + optional screenpipe-screen git-dep pinned to the fork rev; capture = ["dep:screenpipe-screen"]..cargo/config.toml: net.git-fetch-with-cli so cargo authenticates to the private fork via the user's git creds. CI capture builds will need a deploy token (default/feature-off builds use the lockfile, don't fetch/build the fork).cargo clippy clean — fork not built, no repo access needed ✓cargo build/clippy --features capture clean — fork + native frameworks compile ✓3: window/app/url metadata + a11y-tree text + ui_events (Accessibility pane) · 4: meridian.db capture table + repoint the reader · 5: wizard grants as Meridian · 6: retire stock screenpipe from the DMG · 7: dev parallel-run parity check.
🤖 Generated with Claude Code
Implemented background MLX runtime upgrades
Refactors
Redesigned tray UI with live timer, category tiles, and task details
Bug Fixes
Originally posted by: Akarsh-Hegde
🤖 Code review — in-process capture + screenpipe cutover (overnight, high-recall)
Reviewed the full PR diff (3b/4a/3c/4b) via 8 finder angles + dedup/verify. CLAUDE.md conventions clean (headers, no
unwrap, migrations all-new, none >500 lines). Findings ranked by severity.✅ Fixed in this review
SQLITE_BUSYon the daemon ETL —setup_dbopenedmeridian.dbwith nobusy_timeoutwhile the tray now writes the same DB; daemon writes would fail immediately under contention. Fixed in75b6633(5s, matching the tray'sopen_existing).🔴 Critical — gates merge to main / shipping
capturefeature is OFF by default and no release build enables it (tray/src-tauri/Cargo.toml,tauri buildpasses no--features). The daemon now reads only the capture tables, so a packaged release would capture nothing — a silent no-op pipeline, with screenpipe already removed as a fallback. Do not merge/ship until the capture-enabled build is the shipping default (DMG path). The stock-screenpipe early-dev install is incompatible with this branch.🟠 Real bugs to fix before the cutover ships
commands/setup.rs::check_screen_recordingstill detects the grant viapgrep screenpipe+~/.screenpipe/db.sqlite(not in this diff, but the cutover invalidates it). On an in-process install there's no screenpipe process/DB, so the first-run wizard reports Screen Recording as ungranted even when granted to Meridian. Switch toCGPreflightScreenCaptureAccess()on the tray (slice 5).health/capture.rs::capture_coverageghost-detection is miscalibrated for in-process capture. It assumes ≥1 frame per focus event (true for screenpipe's push-based per-focus frames), but the new engine samples on a fixed 2s cadence decoupled from focus. An app focused repeatedly for <2s (quick alt-tabs) accumulates focus events with 0 frames → false CRITICAL "ghosted app". Recalibrate for time-driven sampling.🟡 Accepted degradations (already documented, flagging for the record)
system_sleep;idle_frame_countalways 0 —insert_capture_framenever writescapture_trigger(NULL until in-process idle detection lands), socount_frames_in_window's idle branch is unreachable. Tests stay green only because they seedcapture_triggerdirectly. (Audio is likewise dropped —get_audio_snippetsstubbed empty.)🟡 Observability regressions from the health simplification (follow-up)
AXIsProcessTrustedcheck.frame_freshnessproxy (10-min blind window). The oldscreenpipe_service()pgrep was immediate.wal_sizedeleted, not repointed —meridian.dbis now the high-write sink but nothing watches its WAL for stalled checkpoints.⚪ Minor / cleanup
capture_coveragecompares RFC3339…Ztimestamps againstdatetime('now',…)(space-separated, noZ) → the rolling window skews to midnight of the cutoff day (low-risk; both subqueries skew identically).Config.screenpipe_db/SCREENPIPE_DBunused by the daemon; the whole audio plumbing (AudioSnippet, join arm, merge, JSON column) is now a dead path fed by an empty stub;db::screenpipemodule name kept; duplicatedshare/share_pct+insert_capture_frame/insert_capture_ui_eventtails;capture_once_ocrre-enumerates monitors every 2s tick.run_etl) — noted, not a bug.Generated overnight by Claude Code. The 🔴 + 🟠 items are the ones to resolve before this branch becomes the shipping default.
Originally posted by: Akarsh-Hegde
✅ Manual end-to-end verification (2026-06-22)
Ran a
--features capturetray + the daemon against one shared throwawayMERIDIAN_DB, exercising 4 real apps (VS Code, DBeaver, Arc, Obsidian) over ~5 min. The cutover is functionally validated end-to-end and multi-app — including the link I flagged as unverified in the earlier review comment (capture frames actually flowing through the daemon ETL intoapp_sessions).E2E capture → ETL → sessions — PASS.
capture_frames: 153 frames acrossCode/DBeaver/Obsidian/Arc, alltext_source=accessibility(a11y capture; OCR fallback not exercised — every app exposed an a11y tree), clean ~2s cadence.capture_ui_events:click×53,text×21,window_focus×18,app_switch×12,clipboard×11.app_sessions+active_session. 5etl_runs, allstatus=success.Concurrent-write /
busy_timeout(75b6633) — PASS. 5 ETL runs while the tray streamed writes to the same DB → zerodatabase is locked/ SQLITE_BUSY, no failed runs. The 5sbusy_timeouton the daemon pool holds under sustained tray+daemon contention.App-switch fidelity — PASS.
app_switchevents tracked every focus change in order.Finder/UserNotificationCenterappear as app-switch events with no frames (focus <2s) — the input recorder catches transitions the 2s frame cadence misses; the two paths complement each other as designed.Gaps — PASS (expected).
gapsempty in this short run; idle detection unwired, so anything appearing classifiessystem_sleepper the accepted v1 degradation.Finding — VS Code session fragmentation: confirmed BY DESIGN, not a bug. A continuous VS Code stretch split into ~19 sessions because
vscode_project()(src/etl/runner.rs:260) re-splits on the integrated-terminal tab title (+ Claude-Code spinner glyphs✳/⠂). Clean contrast: DBeaver captured as a single 28-frame/55s block (notis_vscode_like). Product decision (Akarsh): this granularity is intentional — Meridian is a developer-efficiency tool and wants fine-grained context from dev IDEs/tools, so per-project/per-tool splitting inside an editor is a feature. Only possible future tweak is a noise-only one (strip the animated spinner glyph so the same terminal tab doesn't split frame-to-frame) — deferred, low priority.Net: the two blockers from the prior comment still stand before this becomes the shipping default (
capturefeature off by default;check_screen_recordingstill pgreps screenpipe). The pipeline itself is verified working.Originally posted by: Akarsh-Hegde
✅ Both shipping blockers resolved (2026-06-22)
The two blockers from the verification comment above are fixed and pushed (
75b6633..3119f74, full pre-push suite green):1.
capturefeature is now default-on —3119f74a01ad01tray/src-tauri/Cargo.tomlnow setsdefault = ["capture"]. Every release path (tauri build,npm run build,npm run tauri build) and a barecargo buildintray/src-tauribuilds capture-on — there's no per-call--featuresflag to forget, which is the exact footgun that created this blocker.--no-default-featuresstill builds a lean capture-free tray.cargo treeshowsscreenpipe-screenin the default dep tree and absent with--no-default-features; tray clippy clean.cargo clippy/testselect themeridianpackage only and never compile this macOS-only crate (cidre/screenpipe-screen).2.
check_screen_recordingno longer pgreps screenpipe —3119f74Replaced the
pgrep screenpipe+~/.screenpipe/db.sqliteproxy withCGPreflightScreenCaptureAccess()— a pure, prompt-free read of the tray's own Screen Recording grant (the tray is the capturing process post-cutover). Implemented as a rawextern "C"FFI mirroring the existingAXIsProcessTrustedpattern in the same file → no new dependency.Scope note (deliberately deferred to slice-5): this fixes the check. The wizard's Screen-Recording grant action (the button that surfaces the system prompt) may still assume screenpipe spawns to trigger the dialog; if so it needs
CGRequestScreenCaptureAccess(). Left out to keep this scope bounded.Also in this push:
b845647drops the vestigialdisk (screenpipe)health check (capture data lives inmeridian.dbunder~/.meridian, already covered bydisk (meridian); the old check pointed at the retired~/.screenpipe). Removed rather than relabelleddisk (capture)— a relabel would either mispoint at the dead path or duplicate the meridian check.Originally posted by: Akarsh-Hegde
Setup-wizard fixes + macOS install/signing hardening — fresh-account / Air test (2026-06-22)
End-to-end DMG test on a clean user surfaced a batch of wizard + macOS-permission issues; all fixed (commits
34edc5b..5573294).Wizard fixes
| Fix | Commit | Summary |
|---|---|---|
| 1 — manifest 404 |
34edc5b|build:stagingbakes theruntime-stagingmanifest URL;rerun-if-env-changedguard || 2 — Input Monitoring card |
33a8404| third Permissions card +check_input_monitoring(IOHIDCheckAccess) || 3 — eager spec-aware model download |
3c9756b|/prefetch_model+/prefetch_status, wizard progress, card copy || A — onboarding notice |
491479f| gatemlx.downon~/.meridian/onboarded; drop dev fix-text || B — Input Monitoring grant |
1a6dfee|request_input_monitoring(IOHIDRequestAccess) so the app registers in the pane || C-a — prefetch hardening |
100876a| check HTTP status; degrade silently on a runtime missing the endpoints || C-b — runtime republish |
8942252| bump services → 1.60.0; tagruntime-staging-v1.60.0(published) ships the endpoints |macOS permission / install hardening (from the repeated-Screen-Recording-prompt debugging)
| Change | Commit | Summary |
|---|---|---|
| preflight permissions |
b3beecb| check-before-prompt for Screen Recording / Accessibility / Input Monitoring || stale-daemon footgun |
3d3634b|build/build:stagingrebuild the daemon (build:daemon) before bundling || installer hygiene |
0c36a55|backend_installpurges a leftover pre-cutovercom.meridiona.screenpipeagent/binary on install || stable dev signing |
5573294|scripts/dev-signing.sh+APPLE_SIGNING_IDENTITYwiring → constant self-signed identity so TCC grants persist across rebuilds (no more cdhash churn / re-prompt loop) |Root cause of the prompt loop: not a code bug — TCC grant churn. Ad-hoc rebuilds change the cdhash; macOS anchors permission grants to the signing identity, so each build looked ungranted → re-prompt + dead duplicate TCC records. Fix = stable signing (Lever above) + clearing stale records. Verified on two machines: in-process
capture_framesflows (Studio 414 / Air 2331, live) with no re-prompt.Follow-up (b) — background runtime auto-upgrade (2026-06-23)
e267939rm -rf ~/.meridian/runtimeDesign — stage-and-swap, not stop-then-download.
download_runtimesplit intodownload_and_stage(download + SHA-256 verify + extract →runtime.incoming/) andcommit_staged_runtime(atomic renameruntime→runtime.old,runtime.incoming→runtime, stamp version, rollback on failure). The download/extract run with the old server still serving, so the offline window is just the instant rename + restart — the same blip anysuperviserestart already produces. A 60–120 s stop-then-download window would instead guarantee the daemon's "Classifier offline" notice (it probes/healthevery ~60 s with no debounce); a brief transient toast during the restart is still possible but self-clears next tick. No daemon changes, no cross-process marker.auto_upgrade_runtimenever auto-provisions (first ~156 MB pull stays the wizard's job); the poll loop pausessupervise_mlxwhile an upgrade runs; the in-flight flag isDrop-guard + 600 s-timeout protected so a stalled download can't latch it off; after restart it holds in-flight until the new server answers/health(bounded ~60 s) so a slow-binding server isn't killed as "wedged". Wizard path + livelive_pull_from_runtime_stagingtest behavior-unchanged (now exercises stage→commit; green againstruntime-staging1.60.0).Known follow-ups (not in this PR):
scripts/install-from-bundle.shis still the old screenpipe-based npm installer (separate cutover cleanup); shipping still needs Developer ID + notarization.Originally posted by: coderabbitai[bot]
✨ Finishing Touches
🧪 Generate unit tests (beta)
- [ ] Create PR with unit tests - [ ] Commit unit tests in branch `feat/in-process-capture`Comment
@coderabbitai helpto get the list of available commands.Originally posted by: Akarsh-Hegde
✅ Review findings addressed —
7732a82Worked through the CodeRabbit changes-requested set (34 inline) + the overnight review's 🟠 items. All fixes in one commit; full pre-push suite green (fmt · ui build · clippy · ui tests · security audit ·
cargo test323 passing). Each finding was verified against current HEAD first — a couple were already fixed by later pushes and skipped.🦀 Capture / data integrity (writer-side)
meridian-core/capture.rs— normalisetext_sourceto theocr|accessibilitycontract before persisting (binds the normalised value, not raw input); enforce thecapture_ui_eventscontract at the write boundary —text_contentnulled for every non-clipboardtype (privacy), unknownevent_typedropped with a warn. Added tests for both.capture.rsevent_type): warn-and-drop rather thanbail!— this module's contract is "a schema lag never crashes the capture loop" (it already swallows missing-table →Ok); a hard error would break that idiom.🩺 Health (
src/health/capture.rs)COALESCE(NULLIF(full_text,''), NULLIF(accessibility_text,''))so an emptyfull_textcan't mask non-empty a11y text.capture_frames(→ permission prerequisite hint) from unreadable (→ schema/migration fault, no permission remedy) via a newFramesState.capture_coverageghost-detection (overnight 🟠) — DEFERRED, with reason. The miscalibration (focus-event count vs the new time-driven 2s cadence → false CRITICAL on rapid <2s alt-tabs) is real, but the discriminating fix is gating on focus duration, which needs live tuning data to set thresholds. It's a heuristic, not in the changes-requested set, and fixing it blind risks a worse calibration. Tracking as follow-up.🛠️ Tray stability (
mlx_server/poll/update/refresh/lib)MLX_UPGRADE_TIMEOUTcould cancelauto_upgrade_runtimemid-commit_staged_runtime(afterstop_server/the rename), leaving no live runtime. Reworked so only the network download phase is time-bounded (insidemlx_server, viaRUNTIME_DOWNLOAD_TIMEOUT) and the stop→swap→restart runs to completion. The whole path stays bounded (download cap + bounded health-wait), so the in-flight flag can't latch.download_runtime(wizard first-run) now bounded by the same timeout.commit_staged_runtimesurfaces rollback failure (machine left runtime-less → loud distinct error) and warns on version-stamp write failure (else repeated reinstalls).install_updatesingle-flight guarded across all UI surfaces (INSTALLING+ drop guard), not just the tray-menu path.state.lock()in the refresh paths + the tray-enter emit (warn-and-skip / default payload instead of panic);top_titlescans all titles rather than only the first.🧩 Setup wizard (
commands/setup.rs+ui/app/setup)cfg(target_os = "macos")-gate the CoreGraphics FFI incheck/request_screen_recording(mirrors the IOKit pattern → non-mac builds compile)./1024³so a 16 GiB Mac reads as 16, not 17.set_model_preferencevalidates the id is a plausible HF repo (org/name,[A-Za-z0-9._-]) + trims. Deviation: format-check rather than an exact catalog allowlist — the supported-model list lives in the wizard UI and duplicating it in Rust would drift. Verified all 3 catalog ids pass.cancelledflag); stop the flow onset_model_preference/mark_setup_completewrite failure instead of swallowing (prevents "ready/done" lying); rail can no longer jump past a required step (gated on each prior step'scanNext);Marktint works forvar(--accent)viacolor-mix; accurate privacy copy (pixels/video never stored vs the old "frames never stored"; "stays local unless you connect tools").🎨 Misc
upd == null || state === 'unsupported', after the check settles) — not on a packaged DMG'suptodate/error. (Deviation: keyed onunsupported/null, not!== 'available'.)tooltip.cssbox-sizing: border-boxreset (mirrorsstyle.css) so the 300px window body + 16px padding don't overflow to 332px.dev-signing.shcleanuptrapfor the temp key material + dropped-A(codesign-only key access).server.py): prefetch idempotency now scoped to the requestedmodel_id(a done prefetch for model A no longer blocks model B);extra={}structured log fields in the prefetch/log paths (+run_task_linker_mlx.py).tray/package.jsonbuild:daemonuses--locked.Skipped (with reasons)
package-updater.shfail-fast on missing.sigexit 1would break legitimate non-updater releasesstyle.cssstylelintvalue-keyword-caseSFMono-Regular,Helvetica) would be a regression — the correct fix (if stylelint is ever added) is quoting, not lowercasingcapture_coveragerecalibration🤖 Generated with Claude Code
Ticket changed by: Akarsh-Hegde