Mxr
Mxr
A daemon-backed, terminal-first email client. CLI core, one length-delimited JSON protocol, several transports, multiple frontends. The architectural blueprint that I later applied to Lazydap.
Project lives at: ~/code/planetaryescape/mxr/
Repository: https://github.com/planetaryescape/mxr
Extracted crates: Mxr Extracted Crates
Subprojects: Mxr Web App, Mxr Docs Site
What mxr is, in one paragraph
A single binary mxr with subcommands. Bare mxr enters a TUI if interactive; mxr search, mxr cat, mxr compose, and the other CLI commands connect to the daemon through one length-delimited JSON protocol. Unix sockets are the default. The CLI can also use token-authenticated loopback TCP or a child command such as SSH plus mxr daemon dial-stdio. The daemon owns SQLite (canonical state), Tantivy (search index, rebuildable from SQLite), and provider adapters for Gmail, Outlook, IMAP, SMTP, and the fake demo provider. Every operation flows through one IPC contract: the TUI does not get privileged access, and neither do the agent skill or MCP server. The CLI is the canonical surface; everything else is one of N possible clients.
Philosophy
1. Local-first, daemon-backed, scriptable
Email lives on the daemon host. SQLite is the source of truth. Tantivy can be rebuilt from SQLite if it is lost. No phone-home. No telemetry. The default client path is local over an owner-only Unix socket. Explicit TCP and command transports let the CLI reach that same daemon contract without turning mxr into a hosted service.
This is the Local-First Software principle applied to communication tooling — your email shouldn't disappear when your network does, and you shouldn't have to ask Google for permission to grep your own inbox.
2. CLI-first, scriptable from any language
The CLI is the product. The TUI is one client. The agent skill is another. The MCP server is another. Anything anyone wants to build (Electron app, web dashboard, vim plugin, Slack bot, custom analytics) is just another client speaking the same JSON protocol over an allowed transport. No client gets special access.
This is the porcelain on plumbing pattern: SQLite plus Gmail, Outlook, IMAP, and SMTP are the plumbing; mxr is the porcelain. Any tool consuming mxr is a porcelain on top of mxr's porcelain. Wrappers all the way down. (See Yes It's a Wrapper thinking applied here too.)
The single rule, enforced by Cargo's dependency graph rather than convention: the TUI cannot depend on the daemon-internal crates. It cannot reach past the protocol. If you want a feature, it has to flow through the IPC contract.
3. Provider-agnostic internal model
mxr's internal types (Email Internal Model) are provider-agnostic. Gmail's labels, IMAP's folders, and Microsoft's both-of-them all collapse to mxr's Label enum with LabelKind: System | Folder | User. The provider quirks live in adapter crates (crates/provider-gmail, crates/provider-outlook, crates/provider-imap, crates/provider-smtp); the daemon talks to stable sync and send traits.
This is the adapter pattern applied to email. Same shape lazydap uses for debug adapters. Generalisable: when you wrap N backends, define a common trait, push backend quirks into adapter crates, keep the core clean.
4. JSON output is a product feature
Every command supports --format json. Auto-detected from TTY: if stdout is a terminal, default to table; if piped, default to json. Schemas are stable. Breaking the JSON shape is a major version bump. Documented in docs/blueprint/15-decision-log.md under "Non-negotiables."
This is what makes mxr agent-friendly without being agent-only. An LLM, a shell pipeline, and a web app all consume the same JSON happily.
5. Test with real systems, not mocks
mxr's testing discipline: a FakeProvider exists for fast unit tests, but the canonical integration tests run real IMAP via a Dovecot fixture and real Gmail via OAuth-mocked transport. Mocks of provider behaviour silently mask real-world bugs (label reorderings, IDLE disconnects, OAuth refresh races). Real systems catch them.
6. Destructive and bulk mutations are dry-runnable
Mailbox changes that can affect many messages support --dry-run. The selection logic that previews what would change is the same code path as the real mutation; you cannot have a preview that selects one set and an execution that changes another. Setup and authorization flows are different: mxr accounts reauth opens an OAuth session and has no --dry-run flag.
This becomes Lazydap's rule too. It came from mxr's pain: a bulk-archive bug where preview said "1,200 messages" and execution archived 12,000 because the queries diverged. After that, framework rule: same code, same selection, no exceptions.
Architecture summary
Agent skill MCP server TUI Web app (SPA) Custom script
│ │ │ │ │
│ │ │ ▼ │
│ │ │ Web bridge (HTTP+WS) │
│ │ │ │ │
└──────────────┴──────────────┴──────────────┴──────────────────┘
│
│ mxr protocol
│ (length-delimited JSON over Unix, TCP, or cmd)
▼
mxr-daemon
│
┌─────────────────┼─────────────────┐
▼ ▼ ▼
SQLite Tantivy Provider adapters
(canonical) (search, (Gmail/Outlook/IMAP/SMTP/Fake)
rebuildable)
The Mxr Web App SPA is the GUI surface. It speaks HTTP and WebSocket to the web bridge (crates/web/), which translates to the daemon protocol. The SPA dist is embedded in the daemon binary via include_dir!() and served at / - one artifact ships both. Launched via mxr web. See Embedded SPA in Daemon Binary for the distribution pattern.
There is no native desktop client in the repo today. The earlier Electron shell is gone; the GUI story is the embedded web app served by mxr web, installable as a PWA. That keeps the product to one shipped binary and one daemon/client contract instead of adding a second packaging lane around the same local runtime.
Crate dependency rule (enforced by Cargo, not convention):
core— types, traits, errors. Zero I/O.protocol— IPC contract. Depends only oncore.store— SQLite. Depends only oncore.search— Tantivy. Depends only oncore.provider-*- Gmail, Outlook, IMAP, SMTP, Fake. Depend oncore+mail-parse+outboundonly.tui,web— clients. Can depend oncore,protocol,config,compose,reader,mail-parse. Not on daemon, store, search, sync, or provider crates.daemon— orchestrates everything. Depends on all the above excepttui/web.
If a feature wants to break these rules, the architecture is wrong, not the rule. Violations are caught at build time.
Public crates extracted from mxr
Most mxr workspace crates are private implementation boundaries. Four small email contracts have been extracted and are now consumed back from crates.io:
- mail-query — Gmail-style search parser and typed AST
- mail-threading — JWZ / RFC 5256 client-side threading
- list-unsubscribe — RFC 2369 / RFC 8058 unsubscribe header parsing
- mailbox-formats — mbox variants and Maildir reader/writer
This is not a plan to publish all of mxr. The lesson is narrower: publish bounded contracts when they clear a real ecosystem gap, have their own conformance story, and make mxr's boundary sharper.
IPC contract — four buckets
Every IPC request must classify into one. New buckets require a decision-log entry.
| Bucket | Purpose | Examples |
|---|---|---|
| CoreMail | Read/write email content | Search, Cat, Thread, Compose, Send, Archive, Trash, Label |
| MxrPlatform | Per-account state | Accounts::List/Add, Rules::*, SavedSearches::*, Semantic::* |
| AdminMaintenance | Daemon health | Status, Logs, Doctor, BugReport, LocalReset |
| ClientSpecific | Pane state, focus, scroll | (TUI/web only — daemon doesn't see these) |
This bucketing inspired Lazydap's own four-bucket protocol (Session / Project / Diagnostics / ClientSpecific).
Key technical decisions (the load-bearing ones)
- SQLite as canonical state — single-writer + concurrent readers, WAL mode. Migrations compile-time-checked via sqlx.
- Tantivy for lexical search — BM25, rebuildable from SQLite at startup if dirty. Search readiness is a separate concern from sync correctness.
mail-queryfor Gmail-style search syntax — the parser/AST now lives in the publicmail-querycrate and mxr re-exports it.mxr-searchowns local execution over Tantivy/SQLite/semantic state, including caveats such as color-star variants collapsing to boolean starred and+wordpreserving intent before exact non-stemmed execution exists. See Parser Parity Is Not Execution Parity.- Extracted public email crates —
mail-threading,list-unsubscribe,mail-query, andmailbox-formatsare now standalone crates consumed from crates.io. The reusable pattern is: extract the public contract, ship conformance fixtures, keep mxr-specific policy at the boundary. See Open Source Package Extraction. - Direct Gmail API, not IMAP for Gmail — Gmail-over-IMAP is a leaky abstraction (no native labels-as-tags semantics). Direct API gives proper labels, snippet, history-based delta sync.
MailSyncProvider+MailSendProvideras separate traits — one address can read via Gmail API and send via SMTP; the split is real.- Per-account provider-operation lane — background sync, manual sync, and provider-backed mutations serialize through one account-scoped provider lane. Different accounts do not block each other, provider calls have timeouts, and post-sync fanout stays outside the lane. See Provider Lanes Need Account Scope.
- YAML frontmatter in compose — open
$EDITOR, write a markdown-ish message with YAML headers (To, Subject), save → mxr parses it. No bespoke compose UI. - Drafts edit in place - current
mxr 0.6.19exposesmxr drafts edit DRAFT_ID, and the README, generated drafts reference, compose guide, CLI source, and store tests all agree it edits the same draft id. The July receipt workaround of creating a corrected replacement draft was version-specific history, not the current workflow. - Plain-text rendering by default — HTML-to-text via reader mode. Tracking pixels are stripped separately from the remote-image toggle, which is governed by
render.html_remote_content. - Rendered body views are not source mail -
mxr cat, reader mode, and exports are useful views over cached message bodies. For audit evidence, keep the provider id or raw provider payload path available, because a cached best-effort summary can exist even when the provider still has the text/plain or text/html body. See Rendered Mail Is Not Source Mail. - Semantic search as optional layer — local embeddings, dense retrieval. Semantic work is enabled in the default config, but
lexicalremains the default search mode and semantic readiness never gates sync/read/send. - Thread summaries as visible background work — sync does not bulk-backfill summaries.
GetThreadreturns a valid cached summary when one exists;SummarizeThreadrefreshes through the daemon's LLM path. The TUI and web reader may lazily request a missing summary after the user opens a thread, but the work runs in the background and renders above the message/thread body. See Hot Path vs Warm Path, Priority Lanes Need Downstream Isolation, and Nonblocking Work Needs a Visible Home. - Demo data as a product fixture —
mxr demoseeds a separate two-account, 50k-message profile with threads, attachments, links, images, newsletters, promos, spam, suspicious inbox mail, rules, LLM canned responses, analytics, Wrapped, and active-profile semantic prewarm. It proves workflows against the daemon contract, not against hand-shaped screen data. See Demo Data Is a Product Fixture and Warm Reusable Truth, Not Screens. - Gmail OAuth is BYOC-first for v1 — users create a Google OAuth Desktop app and paste the Client ID/Secret. Release builds may include bundled Gmail OAuth credentials, but that shared client is an unverified fallback, not the production onboarding promise.
- MCP is a first-class client, not a provider shortcut —
mxr mcp serveexposes tools over stdio, but those tools call the daemon over IPC with originmcp. Account allowlists, send/destructive gates, activity logging, dry-run previews, and provider boundaries stay in one place. - No native desktop wrapper —
mxr webis the GUI client. The daemon is the native system boundary; wrapping the SPA in Electron or Tauri would add packaging and signing work without changing where the local-first guarantee lives. - Runtime identity as a state boundary — installed builds default to the
mxrinstance, debugcargo rundefaults tomxr-dev, and demo usesmxr-demo. Config, data, socket, bridge token/port files, OAuth token roots, and non-production keychain refs are derived from that identity. See Runtime Identity Is a Data Boundary. - Disk-first, lazy password credentials — IMAP/SMTP passwords live in the active profile's
0600secrets.toml; the keychain is a migration fallback, not a daemon-startup dependency. Account repair also works without a running daemon. See Credential Prompts Are Side Effects. - Account selection as product scope —
--accountis not a search-only flag. Mail-facing reads, searches, exports, drafts, replies, deliveries, invites, analytics, and mutations all accept the same selector shape where the command can sensibly target stored mail. Selectors resolve by key, email, id, or unambiguous display name. Direct ID commands also verify ownership before reading or mutating. The reusable lesson is in Building Great CLIs: scope selectors are public contract, not per-command trivia. - Account scope and sender identity are separate — a compose account key uses the account's configured email; an owned address selects that exact per-message From. The daemon rejects unowned addresses and requires
--accountwhen ownership is ambiguous. The address registry'sis_primarymarker does not replace the configured email in mxr 0.6.12. See Account scope is not sender identity. - Calendar email as mail state — mxr now parses
text/calendarand.icsinvite payloads, persists local invite rows, exposeshas:calendar, and sends iMIP RSVP replies without becoming a full calendar product. See Calendar Email Support in Mxr, Email Invites Are Mail State Before Calendar State, and Capability Slices Before Platform Expansion. - Local capability hardening — local-first did not remove the need for explicit boundaries. The daemon socket is owner-only, the web bridge auth-gates OpenAPI/Swagger like any other route, attachment destinations are allowlisted, and remote HTML assets are size-capped. See Local Capability Surfaces Need Defense in Depth and Attachment Writes Are a Trust Boundary.
- Activity logging as a local diary — activity capture lives at the daemon dispatcher, not in each client. The mapper now classifies every IPC request as logged or intentionally skipped, and skipped-request logs use coarse class/category metadata instead of dumping secret-bearing setup payloads. The product claim stays narrow: local recall for the user, not telemetry.
- Long operations stream progress; clients wait on stalls, not wall clocks —
SyncNowcan ack and run in the background, long IPC calls (seed sync, analytics, semantic reindex, Wrapped) ride the no-deadlinerequest_with_eventspath, andmxr demo/mxr sync --waitgive up only when nothing observable has moved for a stall window (StallWatch). This is what fixed issue 179, where a 120 s client deadline killed the first-run demo on machines slower than the one the number was tuned on. The 50k demo now seeds in under a minute and completes end to end in a few minutes, with an e2e test in CI. See Deadlines Bound Stalls, Not Work. - Compile-time checked SQL via sqlx — bad query = build error.
Files agents/contributors should read first
ARCHITECTURE.md— the high-leveldocs/blueprint/15-decision-log.md— every decision with rationaledocs/blueprint/01-architecture.md— full architecturedocs/web-app.md— the institutional knowledge for Mxr Web App: locked decisions, embedded SPA serving, auth model, action registry, optimistic mutations, sandboxed HTML renderingdocs/vision.md— the delight-plan maintainer notes: daemon-first, CLI-first, exactness-before-cleverness, pivots and non-goalsdocs/guides/writing-docs.md— the docs discipline; see also How I Write Software Docsdocs/guides/http-bridge.md— the HTTP+WS surface, auth model, DNS rebinding defenses; see also DNS Rebinding Hardeningdocs/reference/ai-email.md— the AI email layer: pre-send safety, citations, single-use override tokens, materialization restraint; see also Deterministic Before LLM, Citations Required, Validator Enforces, Materialize Only When Forced, Single-Use Override Tokensdocs/reference/tokio-runtime-guide.md— house style for async Rust; see also Tokiodocs/reference/email-standards.md— the RFC reference for SMTP/IMAP/MIME/authdocs/idiomatic-rust-tests.md— test quality standard; see also Tests Fail on Realistic Bugsdocs/articles/the-spec-is-the-innovation.md— the philosophy in prosedocs/articles/why-local-first-daemon-backed-email.md— why local-first matters for emailAGENTS.md/CLAUDE.md— agent guidance, dual-purposemxr.skill(a ZIP) — the agent skill manifest
Why mxr exists
Cards on the table: I wrote mxr because I wanted the email client I couldn't find. Specifically: one that lives in the terminal, doesn't require Electron, doesn't require an IDE, doesn't ship analytics, lets me grep my own data, lets agents drive it via shell, and treats SQLite as the canonical store.
The closest existing tools — mutt, notmuch, aerc, meli — are good but each has limits I kept hitting. None had agent-native CLI surface. None auto-spawned a daemon for snappy per-command response. None made gh-style JSON output a first-class feature. None imported Gmail labels with proper semantics.
mxr fills that gap, and along the way it's become the architectural pattern I now apply to other domains. Lazydap is "mxr's pattern, but for debugging." Future tools probably will be too.
What mxr is NOT
- Not a webmail replacement. Not for non-technical users.
- Not a native desktop app. The web app is served by the daemon and can be installed as a PWA; there is no
.app, DMG, Electron release lane, or desktop updater. - Not a hosted service. Local-first means local-first. Gmail setup is BYOC-first for v1; bundled OAuth, when compiled into a release build, is an unverified fallback.
- Not a unified inbox aggregator with proprietary backend. mxr is your email; there's no mxr cloud.
- Not a marketing-style email tool. No campaign sequences, no merge fields, no scheduled sends with templates.
- Not Outlook/Gmail-feature-parity-aspirant. Mxr does the 90% case fast, not the 100% case.
Connections
- Lazydap — same architecture, different domain (email → debugging)
- Spotuify — same architecture, different domain (email → music)
- Plumbing and Porcelain — the metaphor that runs through both
- The Elm Architecture — what the TUI uses internally for state management
- Headless Architecture — the broader pattern mxr is an instance of
- The Local Daemon Pattern — the specific pattern mxr/lazydap share
- Agent-Native Interfaces — what the JSON output + dry-run + skill enable
- How Email Actually Works — for the email-mechanics half (SMTP, IMAP, MIME, threading)
- How I Triage Email — how I actually use mxr day to day: my inbox-cleanup workflow and the triage preferences behind it
Vault clusters this project touches
If you're reading this in a year and want to traverse, mxr connects to these synthesis notes (each opens its own cluster):
- Email mechanics → How Email Actually Works — SMTP, IMAP, MIME, threading, OAuth, the provider-agnostic internal model. mxr is the concrete reference implementation.
- Daemon architecture → How Daemons Work — auto-spawn, PID files, signals, IPC server. mxr's daemon is the canonical instance.
- IPC plumbing → How Processes Talk to Each Other — Unix sockets + length-prefixed JSON. The wire mxr's clients ride.
- Client-agnostic cores → Client-Agnostic Cores — the architectural pattern (headless core + many clients) mxr embodies.
- TUI state management → The Elm Architecture (TEA) — what mxr's TUI uses internally.
- Docs discipline → How I Write Software Docs, Mxr Docs Site — the principles
docs/guides/writing-docs.mdcodifies (runnable per section, document every surface, recipes solve goals, Diátaxis, dry-run-first mutations). - Open-source package extraction → Open Source Package Extraction, Package Extraction Is Trust Building, Conformance Corpus as Contract, Public Boundary Before Internal Reuse — the lessons from extracting reusable email behavior into public crates.
- Demo and warm runtime truth → Demo Data Is a Product Fixture, Warm Reusable Truth, Not Screens, Generated Docs as Drift Defense — the demo seed is a realistic product fixture, then the daemon warms reusable analytics, LLM, Wrapped, and semantic artifacts for every client.
- Search language → Parser Parity Is Not Execution Parity — the split between accepting Gmail-shaped syntax and executing the closest honest local semantics.
- Calendar email → Calendar Email Support in Mxr, Email Invites Are Mail State Before Calendar State, Capability Slices Before Platform Expansion — email-derived invite handling without accepting the maintenance contract of a full calendar product.
- Web app and TUI interaction patterns → Shared Action Registry, Keyboard Parity Is a Product Contract, Optimistic Mutation Funnel, Optimistic Retries Preserve User Intent, Hot Reads Should Not Repair Cold State, Embedded SPA in Daemon Binary, Locked Decisions in Docs — client-state lessons from the web app and TUI.
- Preview vs execute → Same-Code-Path Preview — the rule that drives mxr's
--dry-rundiscipline. - AI email layer → Deterministic Before LLM, Citations Required, Validator Enforces, Materialize Only When Forced, Single-Use Override Tokens — the load-bearing patterns from
docs/reference/ai-email.md. - AI feature substrate → Substrate Audits Before AI Features — the Track 0 lesson from validating the AI email roadmap against code truth.
- HTTP bridge security → DNS Rebinding Hardening — the three-layer defense for loopback HTTP servers.
- Local capability hardening → Local Capability Surfaces Need Defense in Depth, Attachment Writes Are a Trust Boundary, Audit Evidence Is Part of the Fix — the P1/P2/P3 security audit closure lessons.
- Runtime isolation and credential recovery → Runtime Identity Is a Data Boundary, Credential Prompts Are Side Effects — the dev/prod/demo split that keeps
cargo runfrom touching installed runtime state, plus the rule that one interactive credential must not gate daemon startup or its own repair. - Async Rust → Tokio hub → Concurrent Is Not Parallel, Don't Hold a Lock Across Await, Classify Async Work Before Refactoring, Bounded Concurrency First — the rules from
docs/reference/tokio-runtime-guide.md. - Visible background work → Hot Path vs Warm Path, Priority Lanes Need Downstream Isolation, Background Work Should Be Bounded and Supersedable, Nonblocking Work Needs a Visible Home — the summary-rendering lesson from the TUI/web readers.
- Test quality → Tests Fail on Realistic Bugs — the 5-question gate from
docs/idiomatic-rust-tests.md. - Idiomatic Rust → Idiomatic Rust Rubric — mxr's 26 crates audited against it; the kept lints ratcheted into the build (Ratchet Lints Into the Build), plus the snooze daemon-panic that named expect Is for Invariants Not Input.
- Long-operation reliability → Deadlines Bound Stalls, Not Work, The Last Page Can Be Empty, Faster Code Breaks Coarse Clocks, Every Event Payload Needs a Bound, Trust Defaults Follow Evidence Provenance, A Buffered Write Hasn't Happened Yet, Measure the Thing, Not the Measurer, Serial Gates Hide the Failures Behind Them, Borrow the Compiler for Caller Audits — the issue-179 wave: the 50k first-run demo died on a client-side 120 s deadline, and fixing it end to end (v0.6.20–v0.6.25) surfaced a family of reliability lessons about deadlines, paging edges, coarse clocks, payload bounds, process identity, and measurement discipline.
The atomic notes inside each cluster cite mxr's specific implementation files as concrete grounding.
Lessons learned (worth carrying to lazydap and beyond)
- The crate dependency graph is the architecture. Not the diagram, not the docs — the
Cargo.tomlfiles. Once enforced there, violations stop at build time and discussions get short. - Test with real systems first, mocks second. Real IMAP, real Gmail OAuth, real SMTP. Mocks lie. The bugs that bite are the ones mocks paper over.
- Stable JSON is a product feature. Once one client depends on a schema, all clients do. Treat the protocol the way you'd treat a public API.
- Auto-spawning daemons remove a class of UX bugs. "Daemon not running" is never a user-facing error if the CLI handles it.
$EDITORfor compose is more powerful than custom editors. Markdown with YAML frontmatter is the format; the editor is whatever the user already loves.- Do not promise one-click OAuth until the app is verified. Bundled Gmail credentials are useful as a fallback, but an unverified shared client is not a durable v1 onboarding plan. BYOC can be the honest launch path if the docs make the Google setup boring.
--dry-runand--yesare non-negotiable for mutations. Same code path. No exceptions. See Same-Code-Path Preview.- Stop using new bucket categories. Once you have N buckets in your IPC, every new request must justify why it doesn't fit. Adding a bucket is a decision.
- Name the layer that has parity. Search taught this one. Parser parity, AST parity, execution parity, and product parity are different promises. If a local index accepts Gmail syntax but stores only boolean stars, say so. See Parser Parity Is Not Execution Parity.
- One registry beats three hardcoded lists. A web app with palette + keymap + help dialog will drift unless all three derive from a single action registry. But the registry is for durable product actions, not every page-local motion key. See Shared Action Registry and Keyboard Parity Is a Product Contract.
- Funnel all mutations through one optimistic hook. Per-mutation hooks copy-paste snapshot/restore logic and diverge. One funnel, per-action projection functions. See Optimistic Mutation Funnel.
- Embed the SPA in the daemon binary. One artifact ships server and UI; impossible to mismatch versions. See Embedded SPA in Daemon Binary.
- A client is not launched until its first run works. The removed desktop shell taught the uncomfortable version: having a working UI shell and bridge is not enough if a clean user lands on an empty mailbox, raw JSON account setup, or an OAuth flow hidden behind a headless daemon. See First Run Is the Launch Surface.
- Artifacts are product promises. mxr now ships the CLI/TUI/web binary and Homebrew formula, not a separate desktop artifact. That is healthier than publishing a fresh-looking wrapper whose signing, smoke tests, and first-run flow are not ready. See Release Artifacts Are Product Promises.
- Release gates should encode the launch contract. Deterministic build/test failures block release. Missing optional Apple signing secrets warn and ship unsigned macOS archives because that is the current distribution contract. Missing live Gmail test credentials skip the live smoke. Missing both bundled Gmail build credentials can warn because BYOC is the official v1 path; setting only one bundled value is a hard error because it creates a broken artifact. See Release Gates Encode Product Contracts.
- Lock the decisions that get relitigated. A "do not relitigate" section in the doc saves every reviewer the same conversation. See Locked Decisions in Docs.
- Runtime identity must cover credentials too. Separate data dirs and sockets are not enough. If dev and prod both read
mxr/work-imapfrom the OS keychain, the boundary leaks. See Runtime Identity Is a Data Boundary. - A demo seed is a product fixture, not decoration. The useful demo is the one that exercises real threads, repeat contacts, labels, spam, promotions, attachments, links, images, rules, analytics, semantic search, and LLM summaries through the same daemon paths users hit. See Demo Data Is a Product Fixture.
- Prewarm reusable truth, not client screens. The demo warms analytics, Wrapped, LLM-backed surfaces, and active-profile semantic vectors through IPC. It does not bake a web or TUI inbox payload. See Warm Reusable Truth, Not Screens.
- Audit the substrate before building AI features. The AI email plan only became safe once contacts, reply pairs, draft intent, safety placement, citation shape, and client surfaces were real in code. See Substrate Audits Before AI Features.
- Retry optimistic intent before reconciling it away. If a row disappeared because the user archived it, a transient pool timeout should not bring it back immediately. Keep the optimistic projection during bounded retries; reconcile only after the failure is terminal or exhausted. See Optimistic Retries Preserve User Intent.
- Local surfaces are still capability surfaces. A Unix socket, loopback bridge, API docs route, or attachment save path can all drive real authority. Make the boundary explicit in code, not just in deployment assumptions. See Local Capability Surfaces Need Defense in Depth.
- Attachment writes are not boring. Filename sanitation, destination allowlists, symlink rejection, file mode, and remote body caps all belong to the same boundary. See Attachment Writes Are a Trust Boundary.
- Audit closure needs evidence. Code fixes, tests, CI gates, docs, and tracked advisory exceptions are all different kinds of closure. See Audit Evidence Is Part of the Fix.
- Hot reads should stay hot. If sync promised bodies are eager, opening a message should be a SQLite read, not a provider repair attempt hiding behind "Loading...". Keep cache reads and repair paths separate. See Hot Reads Should Not Repair Cold State.
- Nonblocking work still needs a place to show up. Moving LLM summaries off the hot path fixed the stall, but the user still needed a visible Summary block above the message body. Background work without an obvious result surface feels broken even when the scheduler is healthy. See Nonblocking Work Needs a Visible Home.
- Extract contracts, not folders. A standalone package needs a public promise, not just reusable code. The README, conformance corpus, coverage matrix, and semver policy are part of the product. See Package Extraction Is Trust Building.
- Portable fixtures beat translated tests. If Rust and JS packages may exist, the shared JSON corpus is the contract. Tests in each language are just harnesses around that corpus. See Conformance Corpus as Contract.
- Keep app policy at the boundary.
list-unsubscribeparses standard headers; mxr addsBodyLink.mail-queryparses syntax; mxr executes local semantics. That adapter layer is healthier than exporting product-shaped internals. See Public Boundary Before Internal Reuse. - Handle embedded protocol objects in their host workflow first. Calendar invites arrive as email, so the first useful product is mail-derived invite handling: parse, show, search, dry-run, and reply over iMIP. Full calendar sync is a separate platform expansion. See Email Invites Are Mail State Before Calendar State.
- Keep always-on agent context as a router. The 2026-05-28 cleanup cut
AGENTS.mddown to the universal guardrails and moved source-work detail into anmxr-developmentskill. The general lesson is Always-On Agent Context Is a Router: root context should tell the agent where to load detail, not carry every detail itself. - Agent surfaces need product gates, not just examples. The skill teaches the CLI, and MCP gives typed tools, but both are only first-class because the daemon sees
agentandmcpas real origins with profiles, account scope, and send/destructive gates. - Launch proof should exercise public surfaces. The useful proof starts the binary, configures a fake account, syncs, reads, drafts, blocks unsafe agent/MCP actions, and runs a gated mutation path. Unit tests alone cannot prove a launch surface.
- Generated docs can drift at metadata level.
site/public/openapi.jsonlagged at0.5.58while the repo had moved to0.5.61. The surface still generated, but the published contract said "older product." Regenerate, then ask why the release PR did not own that derived state. - A local identity registry does not grant remote authority. mxr can reject an unowned From and show the exact alias in a dry-run. Zoho, Gmail, or the SMTP server still decides whether the authenticated account may send as it. See Local Identity Registration Is Not Provider Authorization.
- Account scope is not sender identity. The transport account owns authority and provider state; a registered alias chooses the visible From inside that boundary. Validate ownership in the daemon, and use the same resolution path for dry-run and send. See Account scope is not sender identity.
- Prompt-capable credentials should not gate daemon startup. Resolve a password when its account connects, keep other accounts and diagnostics alive, and make repair work without the daemon. Disk-first storage is mxr's chosen tradeoff, not a universal answer; failure isolation is the reusable rule. See Credential Prompts Are Side Effects.
- Untrusted input cannot grant authority. Mail can contain a request, a fake system message, or a plausible instruction from a known sender. It still cannot change the agent's task, recipients, permissions, or tool access. Keep mail-derived text inside an explicit data boundary and enforce authority outside it. See Untrusted input cannot grant authority.
- Rendered mail is not source mail. Body views, snippets, exported markdown, HTML, attachments, and provider payloads are separate evidence surfaces. A no-attachment result can be true while a body renderer is still stale or placeholder-only. See Rendered Mail Is Not Source Mail.
- Check the exact version surface before choosing a workflow. Draft editing changed between the July receipt workflow and
mxr 0.6.19. The right move now ismxr drafts edit, not creating a replacement draft and deleting the old one. See Version Truth Has Surfaces. - Deadlines bound stalls, not work. A client-side wall-clock timeout on a data-sized operation encodes the author's hardware. Long calls stream progress and the client bails only when nothing observable moves. Issue 179 was exactly this, on the README's front-door command. See Deadlines Bound Stalls, Not Work.
- Settle deferred work on the completion edge. Backfills can end on an empty page; anything accumulated across pages must fire on the
has_moretrue-to-false transition, not on a page that carried mail. This bit twice in one wave. See The Last Page Can Be Empty. - Faster code breaks coarse clocks. Second-granularity "changed since" checks went blind the moment the seed became sub-second, and the failure mode was a hang, not an error. See Faster Code Breaks Coarse Clocks.
- Bound every event payload. One unbounded
Vecin a daemon event crossed the 16 MiB frame cap at demo scale and tore connections down silently. See Every Event Payload Needs a Bound. - Process identity checks need provenance-matched trust defaults. The pid file the daemon wrote is ours unless disproven; a
psscan match is foreign unless proven. Applying the strict check to the wrong strand deleted live daemons' pid files. See Trust Defaults Follow Evidence Provenance. - Mail is historical evidence, not live state. The travel workflow proved that search, source reads, attachment extraction, dry-run, and compose can build a useful evidence pack. The agent must still distinguish a booked state from current provider state and retain conflicts instead of resolving them silently. Expense work adds another boundary: Mxr can collect receipts and invoices, but the bank or card ledger owns posted transactions, and several records can describe one purchase. See Consolidated records need a contradiction ledger, Reconcile economic events, not transaction rows, and Notifications are leads, not source of truth.
Local build contention — 1 October 2026
Queue heavy local commands with agent-work run -- <command> [args...], or
use the dotfiles Cargo proxy. The host uses Pueue's two-job queue with
four-worker defaults, nice priority 10 and a macOS memory-pressure gate.
~/.dotfiles/docs/agent-work.md records setup, cancellation and the limits.
The runner fix removes global Cargo process killing. It is installed locally
and prepared on fix/shared-build-queue; it has not been published to the
remote. The handover is docs/issues/local-build-contention.md. This is a
resource ownership lesson for Parallel Agent Work Needs a Closing Ledger:
a worktree is source isolation, not a separate CPU budget.