| name | meridian-etl |
| description | Debug and work with Meridian's ETL pipeline. Covers session boundary detection, cursor management, DB queries, and common failure modes. |
| allowed-tools | Bash, Read, Edit, Grep |
Meridian ETL Skill
How the ETL Works
screenpipe.db (read-only)
│
▼
runner.rs ← src/etl/runner.rs
polls every POLL_INTERVAL_SECS
│
├─ get_frames_since(cursor) ← src/db/screenpipe.rs
│ returns new frames since last processed frame_id
│
├─ detect_boundaries()
│ splits frames into blocks by focused_app change
│
├─ extract_block_context() ← src/etl/extractor.rs
│ OCR samples, window titles, audio snippets, signals
│
├─ upsert active_session ← src/db/meridian.rs
│ (current open block, updated each poll)
│
└─ insert completed app_sessions
(previous blocks, now closed)
Key Concepts
| Concept | What it is |
|---|
| cursor | last_frame_id in etl_cursor — marks where ETL left off |
| app-switch boundary | frame where focused_app differs from the previous frame |
| active_session | the currently-open, in-progress session (one row, upserted) |
| app_sessions | completed, closed sessions with final timestamps |
| category / confidence | AI-assigned activity category and confidence score on each session |
| gaps | user_idle or system_sleep periods between sessions |
Running with Verbose Logging
RUST_LOG=debug ./target/release/meridian
RUST_LOG=meridian=trace ./target/release/meridian
Useful Debug Queries
sqlite3 ~/.meridian/meridian.db
SELECT app_name, ROUND(SUM(duration_s)/60.0,1) AS minutes, COUNT(*) AS sessions
FROM app_sessions
WHERE started_at >= date('now')
GROUP BY app_name ORDER BY minutes DESC LIMIT 10;
SELECT * FROM etl_cursor;
SELECT * FROM active_session;
SELECT
a.ended_at,
b.started_at,
ROUND((julianday(b.started_at) - julianday(a.ended_at)) * 86400) AS gap_secs
FROM app_sessions a
JOIN app_sessions b ON b.rowid = a.rowid + 1
WHERE gap_secs > 120
ORDER BY gap_secs DESC LIMIT 20;
SELECT * FROM app_sessions WHERE duration_s = 0;
SELECT date(started_at) AS day, COUNT(*) AS n, ROUND(SUM(duration_s)/3600.0,2) AS hours
FROM app_sessions
GROUP BY day ORDER BY day DESC;
SELECT category, COUNT(*) AS sessions, ROUND(SUM(duration_s)/60.0,1) AS minutes
FROM app_sessions
WHERE started_at >= date('now')
GROUP BY category ORDER BY minutes DESC;
SELECT kind, COUNT(*) AS n, ROUND(SUM(duration_s)/60.0,1) AS total_min
FROM gaps GROUP BY kind;
SELECT app_name, category, confidence, ROUND(duration_s/60.0) AS min, window_titles
FROM app_sessions WHERE duration_s > 600
ORDER BY started_at DESC LIMIT 10;
Common Issues
Zero-duration sessions
Single-frame sessions returned duration_s = 0. Fixed in commit 317ceb2 (Option D).
Verify with: SELECT * FROM app_sessions WHERE duration_s = 0;
Phantom sessions spanning sleep gaps
Machine sleep between two ETL runs created a session covering the sleep period.
Fixed in commit a8f2280 (sleep gap detection at ETL run boundary).
Check: SELECT * FROM app_sessions WHERE duration_s > 3600 ORDER BY duration_s DESC;
Duplicate sessions
Cursor not advancing correctly caused re-processing. Check cursor value:
sqlite3 ~/.meridian/meridian.db "SELECT * FROM etl_cursor;"
Screenpipe DB locked
Meridian must open screenpipe DB with read-only flag. If you see SQLITE_BUSY:
lsof ~/.screenpipe/db.sqlite
Reset and Re-run ETL from Scratch
pkill meridian
rm ~/.meridian/meridian.db
./target/release/meridian
Running Integration Tests
cargo test
cargo test integration
RUST_LOG=debug cargo test -- --nocapture