Architecture

Architecture

One brain, two surfaces, four memory layers. The whole system on one page.

Updated · 2026-05-28

The whole system fits in your head if you understand five things: core, workers, dashboard, memory, and boundaries.

The shape

                         ┌──────────────┐
   workers (BullMQ) ─────▶  packages/   ◀───── desktop (Electron + RTK Query)
                         │     core     │
   voice (whisper.cpp) ──▶              │
                         └──────┬───────┘

                ┌───────┬───────┼───────┬───────┐
                ▼       ▼       ▼       ▼       ▼
            profile  episodic graph  vector   llm
            (md)     (sqlite) (json) (embed) (anthropic)

Five packages in the monorepo:

PackageRole
packages/coreThe brain. Agents, context composition, librarian, feature flags. No transport — pure logic.
packages/workersBackground jobs. Each is a function with a cron string. Hands its work to core.
packages/desktopElectron + React + RTK Query. Reads structured outcomes from core, renders them.
packages/sharedZod schemas reused across boundaries. The contract between core and surfaces.
packages/researchLong-running research loops (Python). Talks to core via JSON over stdio.

The rule we don’t break: workers don’t render UI; the dashboard doesn’t run cron. Both speak to core. Adding a capability is one place.

packages/core

Everything that matters happens here:

  • agent/ — short-running prompted calls. Morning brief, drift report, intent classifier.
  • agent/context.ts — composes prompt context from the right memory layers. The PII redaction layer lives here.
  • librarian/ — the canonical writer across the four memory layers. Every layer has one writer to keep them consistent.
  • memory/ — readers for each layer. Profile loader, episodic queries, graph traversals, vector recall.
  • features/ — feature flag table. hasFeature('research_loop') etc. Productization seam.
  • schemas/ — re-exports the Zod schemas from @mayva/shared.

If you’re adding a capability and you can’t tell whether it goes in agent/ or librarian/: agents read and write outcomes; librarians read and write memory. Outcomes are episodic; memory is everywhere.

packages/workers

Each scheduled worker is a single file:

// packages/workers/src/scheduled/morning-brief.ts
import { runMorningBrief } from '@mayva/core/agent';

export const schedule = '0 6 * * *';      // 06:00 daily
export async function run() {
  await runMorningBrief();
}

The daemon (packages/workers/src/index.ts) discovers every file in scheduled/, registers it with BullMQ, and starts the loop. To add a worker, drop a file. To disable one, delete it.

There’s also an on-demand/ directory for jobs that fire from the dashboard (e.g. generate a focus plan now) rather than on cron.

packages/desktop

Electron renderer process is React + RTK Query. The main process is a thin shim:

  • spawns / restarts the worker daemon
  • exposes a tiny IPC for “trigger this on-demand job”
  • handles auto-updates (electron-updater)

The renderer never talks to the DB directly. It talks to core via an in-process facade (@mayva/core/desktop-api). The same facade is what powers the menubar widget and the voice overlay.

The contracts (packages/shared)

Every interface between packages is a Zod schema:

export const MorningBriefSchema = z.object({
  date: z.string(),                       // YYYY-MM-DD
  recoveryBand: z.enum(['green','yellow','red','none']),
  items: z.array(z.object({
    title: z.string(),
    priority: z.enum(['p0','p1','p2']),
    durationMin: z.number().int().positive(),
  })),
});

The worker validates its output before writing; the dashboard validates its input before rendering. If they disagree, the schema breaks the build. This is the whole “honest tone” promise: nothing renders unless the data shape says it can.

Boundaries

The only network hops, in order of how often they happen:

  1. Anthropic — every agent call. PII-redacted on outbound.
  2. Whoop / Google / Gmail — only the workers, only on their schedules. OAuth refresh tokens stay in the local DB.
  3. License server — once a day, only on hosted plans. Self-host never calls home.

Telemetry is zero. There is no /track, no analytics SDK, no error reporter that uploads stack traces.

What’s not in core

  • Auth lives in packages/desktop (local sessions) and the hosted backend (server-side).
  • Billing lives in the hosted backend only.
  • Transport — there’s no HTTP server in core. The hosted backend wraps core in a Fastify shell.

This separation is the productization seam: the server/ package in the hosted version imports core, doesn’t replace it.