Skip to content

Timing and cadence

Two cheap, statistical, fully local features that read your existing reply_pairs and contacts data. Neither calls an LLM. Neither does any server-side tracking — no pixels, no open tracking, no remote calls. They surface patterns mxr already has.

Terminal window
mxr send-time alice@example.com

What you get: a table of weekday × hour buckets ranked by typical reply latency, plus a confidence label (low, medium, high) driven by sample count. low means too little data to draw a conclusion — the command returns the buckets but suppresses the recommendation.

Terminal window
# "If I send Friday at 7pm, how does that compare to her fastest slot?"
mxr send-time alice@example.com --at "fri 19:00" --format json

What you get: JSON with proposed_at, recipient_rows[] (each row has proposed_expected_reply_seconds and best_expected_reply_seconds), best_windows[], and a confidence enum. A useful note fires only when the proposed slot is at least 2× slower than the best window AND confidence is medium or high.

Terminal window
# Pick the slot that's least bad for everyone.
mxr send-time alice@example.com bob@example.com carol@example.com --format json

What you get: per-recipient rows so you can see which person dominates the recommendation. The worst meaningful delta wins; recipients with low sample count are reported but excluded from the worst-case calculation.

When you mxr send DRAFT_ID --check, the safety pipeline asks the same send-time path for a timing hint. The hint is only attached when confidence is medium/high AND the proposed slot is meaningfully worse than the best — so it doesn’t nag on every send.

Terminal window
# See the timing info attached to a real safety report:
mxr send DRAFT_ID --check --format json \
| jq '.issues[] | select(.code == "send_time_hint")'

The --at flag accepts the same forms as mxr snooze --until:

FormExample
Named dayfriday, mon, tue
Day + timefri 19:00, tomorrow 9am, monday 17:00
Relativein 2h, in 3d, in 2w
RFC33392026-06-01T15:00:00Z

Times are interpreted in the machine’s local timezone and labeled as such.

Relationships you actually maintain are a small set. mxr does not auto-watch them — you watch each one explicitly with an expected interval, and the daemon surfaces only the ones that have drifted past it.

Terminal window
# Watch Alice with a 14-day expectation:
mxr cadence watch alice@example.com --every 14d
# See the list:
mxr cadence list --format json
# See drift (positive drift_days only):
mxr cadence drift --format json

What you get from drift: rows { email, expected_days, days_since_contact, last_contact_at, drift_days } ranked drift-descending. drift_days = days_since_contact - expected_days. No rows = nothing has drifted; that’s a valid empty success state.

Terminal window
# Add a contact (interval is required — no implicit defaults).
mxr cadence watch alice@example.com --every 14d
mxr cadence watch mentor@example.com --every 30d
# Remove a row.
mxr cadence unwatch alice@example.com

The watchlist lives in relationship_watchlist, keyed by (account_id, email). Watch entries are non-destructive on unwatch — they’re removed cleanly, not soft-deleted.

mxr cadence watch refuses mailing-list addresses (anything with a List-Id history) without an explicit override:

Terminal window
# Pass --allow-list-sender when you actually mean it:
mxr cadence watch news@indie.example --every 7d --allow-list-sender

This stops the watchlist from filling up with newsletter addresses that don’t reply.

Terminal window
# For every drifted contact, open their full profile.
mxr cadence drift --format json \
| jq -r '.[].email' \
| xargs -I{} mxr sender {}

What you get: each drifted contact’s profile (volume, recent threads, open commitments) so you can decide whether the gap actually matters.

Terminal window
# Save it as a sidebar lens (TUI and web).
mxr saved add cadence-drift 'cadence:drift'
  • Monday morning planning: mxr cadence drift --format json | jq '.[0:5]' — the five most-drifted relationships you said you’d maintain. Skim, decide whether to write.
  • Choosing a send slot: before scheduling a sensitive ask, mxr send-time alice@example.com --at "thu 16:00" — switch slots if the proposed window is much slower than her best.
  • Audit of “I’ll keep in touch”: mxr cadence list --format json | jq 'length' — count how many relationships you’ve actually committed to keeping warm.
  • Newsletter denial: mxr cadence watch news@example.com --every 7d fails — confirms the screener’s list-sender classification is working.
  • All metrics are computed on demand from reply_pairs and contacts. A future recipient_reply_latency_buckets cache is documented, but there is no current table to maintain; the on-demand path is the source of truth.
  • No data leaves the machine for either feature. mxr send-time and mxr cadence drift are pure-Rust queries.
  • Watchlist entries are account-scoped: switching accounts gives you a different watchlist.
"Before scheduling any draft, check `mxr send-time <recipient> --at
<proposed_at> --format json`. If the proposed slot is at least 2x slower
than the best window AND confidence is medium or high, propose the
faster slot to me. Do not auto-reschedule."
"List my top 5 drifted relationships from `mxr cadence drift --format
json`. For each, summarize the last shared thread via `mxr summarize
<thread_id>` and suggest a one-sentence opener. Don't send."