Architecture
Architecture
One brain, two surfaces, four memory layers. The whole system on one page.
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:
| Package | Role |
|---|---|
packages/core | The brain. Agents, context composition, librarian, feature flags. No transport — pure logic. |
packages/workers | Background jobs. Each is a function with a cron string. Hands its work to core. |
packages/desktop | Electron + React + RTK Query. Reads structured outcomes from core, renders them. |
packages/shared | Zod schemas reused across boundaries. The contract between core and surfaces. |
packages/research | Long-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:
- Anthropic — every agent call. PII-redacted on outbound.
- Whoop / Google / Gmail — only the workers, only on their schedules. OAuth refresh tokens stay in the local DB.
- 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.