Architecture
Overview
Melian is a local-first application built on Bun. Everything runs in a single process on your machine: the HTTP/WebSocket server, cron scheduler, iMessage poller, and all integration clients. No application state lives in the cloud; only specific third-party APIs (Google Calendar, Google Vision, Govee Cloud) require outbound connections.
Tech Stack
| Layer | Technology | Notes |
|---|---|---|
| Runtime | Bun v1.3+ | TypeScript, bundler, test runner |
| Web framework | Hono | Lightweight, edge-compatible router |
| Frontend | React 19 + Tailwind CSS v4 | Bundled by Bun's HTML importer |
| Relational DB | SQLite via bun:sqlite |
Six databases, see topology below |
| Vector DB | Qdrant | Two collections, Cosine distance |
| LLM inference | OpenAI-compatible HTTP | LM Studio (local) or any compatible server |
| Embeddings | OpenAI-compatible HTTP | nomic-embed-text-v1.5 (768-dim) |
| imapflow (IMAP), nodemailer (SMTP) | Multi-account | |
| Calendar | Google Calendar REST API v3 | OAuth 2.0 device flow |
| Vision / OCR | Google Cloud Vision API | Optional; disabled if credentials absent |
| Smart home (Govee) | Govee LAN API + Govee Cloud API | LAN preferred; cloud fallback for H5179 |
| Smart home (Wiz) | WiZ UDP protocol (port 38899) | LAN only |
| Tablet sync | Remarkable Cloud API | Optional |
| Scheduler | croner | In-process cron, no daemon needed |
| Process manager | launchd (macOS) / systemd (Linux) | For unattended startup |
System Diagram
┌────────────────────────────────────────────────────────────────┐
│ Web Browser │
│ React 19 · Tailwind · Nan Elmoth UI │
│ / (Mirror) /glade (Chat) /knowledge /devices /tasks │
└────────────────────┬───────────────────────────────────────────┘
│ HTTP + WebSocket
┌────────────────────▼───────────────────────────────────────────┐
│ Bun Process (single) │
│ │
│ ┌─────────────┐ ┌─────────────────────────────────────────┐ │
│ │ Hono API │ │ Agent Core │ │
│ │ /api/* │ │ ┌──────────┐ ┌──────────────────────┐│ │
│ │ WebSocket │ │ │ Router │ │ Prompt Builder ││ │
│ └──────┬──────┘ │ │ (simple/ │ │ core memory + ││ │
│ │ │ │ complex)│ │ conversation history ││ │
│ │ │ └────┬─────┘ └──────────┬───────────┘│ │
│ │ │ │ │ │ │
│ │ │ ┌────▼─────────────────── ▼──────────┐│ │
│ │ │ │ Tool Executor ││ │
│ │ │ │ memory · email · calendar · home ││ │
│ │ │ └─────────────────────────────────────┘│ │
│ │ └─────────────────────────────────────────┘ │
│ │ │
│ ┌──────▼──────────────────────────────────────────────────┐ │
│ │ Service Layer │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌───────────┐ │ │
│ │ │ Memory │ │ Jobs / │ │ Attention│ │ Integr. │ │ │
│ │ │ System │ │ Scheduler│ │ Pipeline │ │ Clients │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ └───────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────┐ ┌──────────────────────────────┐ │
│ │ SQLite (6 DBs) │ │ Qdrant (2 collections) │ │
│ │ recall.db │ │ archival_memory │ │
│ │ jobs.db │ │ knowledge │ │
│ │ email-cache.db │ └──────────────────────────────┘ │
│ │ calendar-cache.db │ │
│ │ attention.db │ │
│ │ imessage-state.db │ │
│ └──────────────────────┘ │
└────────────────────────────────────────────────────────────────┘
│ │ │
┌────────▼───┐ ┌────────▼───┐ ┌────────▼──────┐
│ LM Studio │ │ Google │ │ Govee / Wiz │
│ (local) │ │ APIs │ │ Smart Home │
└────────────┘ └────────────┘ └───────────────┘Source Layout
The codebase is organized into four tiers under src/:
src/
├── core/ # Foundational capabilities
│ ├── agent/ # LLM agent router, tools, personality, spirit
│ ├── attention/ # Message classification pipeline
│ ├── jobs/ # Cron scheduler, job store, self-healing
│ ├── knowledge/ # Document chunking, vector search
│ ├── llm/ # LLM client, model routing, complexity scoring
│ └── memory/ # Core, archival (Qdrant), recall (SQLite)
│
├── integrations/ # External service connectors
│ ├── browser/ # Browser automation
│ ├── calendar/ # Google Calendar
│ ├── email/ # IMAP/SMTP, permissions
│ ├── govee/ # Govee smart home (LAN + cloud)
│ ├── imessage/ # macOS iMessage
│ ├── mcp/ # Model Context Protocol
│ ├── tasks/ # Obsidian + Remarkable
│ ├── weather/ # NOAA weather data
│ └── wiz/ # Wiz smart lights
│
├── surfaces/ # User-facing entry points
│ ├── cli/ # Command-line interface
│ ├── server/ # Hono API + routes
│ └── web/ # React UI (Nan Elmoth)
│
└── lib/ # Cross-cutting utilities
├── config.ts # Configuration loader
├── types.ts # Shared type definitions
├── google/ # Google OAuth helpers
├── logging/ # Error capture
├── platform/ # launchd/systemd service management
├── push/ # Web Push notifications
└── vision/ # OCR (Google Cloud Vision)Dependency Boundaries
The tiered structure enforces a strict dependency hierarchy:
| Tier | Can import from | Cannot import from |
|---|---|---|
lib/ |
Nothing (leaf tier) | core, integrations, surfaces |
core/ |
lib/ |
integrations, surfaces |
integrations/ |
lib/, core/ |
surfaces, other integrations |
surfaces/ |
Everything | (composition layer) |
This prevents circular dependencies and keeps core capabilities independent of any specific integration or UI surface.
Data Flow
A complete request cycle, from user input to streamed response:
- Input arrives via the WebSocket from the browser (or via the CLI REPL's local HTTP call)
- Thread is loaded: conversation history is fetched from
recall.db, limited tollm.maxContextTokenstokens using a token-counting truncation pass - Core memory is injected: all facts stored in the core memory table in
recall.dbare prepended as a system context block - Complexity scoring: if
llm.complexityRoutingis enabled, the query is scored using the lightweight feature extractor; the result selects which model inllm.preferredhandles the request - LLM call begins: the assembled prompt is sent to the configured OpenAI-compatible endpoint with
stream: true - Tokens stream to the client: each SSE chunk is forwarded over the WebSocket as it arrives; the frontend appends to the active bubble in real time
- Tool calls are parsed: if the stream contains a tool call, streaming pauses, the tool is executed (memory search, email read, calendar query, device command, etc.), and the result is fed back to the LLM for synthesis
- Tool results may trigger further calls: the agent loop continues until the LLM emits a final text response with no pending tool calls
- Response is persisted: the completed exchange (user message + tool calls + Melian's response) is written to
recall.db - Core memory is updated: if the response contains memory-worthy facts (detected via a lightweight heuristic on the response content), they are written to the core memory table
Database Topology
SQLite Databases
All six databases use bun:sqlite (the Bun built-in SQLite binding). Migrations are applied via a versioned migration runner on startup.
| Database | Tables | Purpose |
|---|---|---|
recall.db |
threads, messages, core_memory, facts |
Conversation history and long-term facts |
jobs.db |
jobs, runs, logs |
Job definitions, run history, output |
email-cache.db |
accounts, messages, drafts, approvals |
Email index and draft queue |
calendar-cache.db |
events, calendars, sync_tokens |
Google Calendar event cache |
attention.db |
seeds, candidates, decisions, digest_runs |
Attention pipeline state |
imessage-state.db |
contacts, last_seen, pending |
iMessage read cursor per contact |
Qdrant Collections
| Collection | Dimensions | Metric | Purpose |
|---|---|---|---|
archival_memory |
768 | Cosine | Long-form memory retrieved by semantic similarity |
knowledge |
768 | Cosine | Imported documents, notes, reference material |
Both collections are created on startup if they don't exist. Vector payloads include source, timestamp, thread_id, and text fields, enabling filtered search (e.g. "only search memories from this month").
Process Model
Melian runs as a single Bun process. There is no separate worker process, message queue, or inter-process communication. Concurrency is handled via Bun's event loop:
- Cron scheduler: croner runs inside the same process, firing job callbacks on schedule. Jobs that need to avoid blocking the event loop (long LLM calls, bulk email indexing) yield by awaiting async operations naturally.
- iMessage poller: a
setIntervalpollingchat.dbevery 3 seconds. Usesbun:sqlitein read-only mode to avoid interfering with the Messages app. - WebSocket connections: handled by
Bun.serve()'s built-in WebSocket support. Multiple simultaneous connections (browser tabs, CLI clients) are supported. - LLM streaming: uses native
fetchwith aReadableStreambody; Bun handles the async stream with no additional library.
External Dependencies
| Dependency | Protocol | Required | Fallback behavior |
|---|---|---|---|
| LM Studio (or equivalent) | HTTP (OpenAI API) | Yes | Server starts but chat is non-functional |
| Qdrant | HTTP REST | Yes | Server starts; archival memory disabled |
| Google Calendar API | HTTPS REST | No | Calendar features disabled |
| Google Cloud Vision API | HTTPS REST | No | OCR features disabled |
| Govee Cloud API | HTTPS REST | No | Cloud-only devices (H5179) show as unreachable |
| Govee LAN API | UDP | No | LAN-controllable devices show as unreachable |
| Wiz | UDP (38899) | No | Wiz cards show no devices |
| Remarkable Cloud | HTTPS REST | No | Tablet sync disabled |
| NOAA Weather API | HTTPS REST | No | Weather features disabled |
| Census Bureau Geocoder | HTTPS REST | No | Location name lookup unavailable; lat/lon still works |
Platform Integration
macOS (launchd)
On macOS, Melian is registered as a launchd user agent so it starts at login and restarts on crash:
<!-- ~/Library/LaunchAgents/com.your-username.melian.plist -->
<plist version="1.0">
<dict>
<key>Label</key> <string>com.your-username.melian</string>
<key>ProgramArguments</key>
<array>
<string>/Users/you/.bun/bin/bun</string>
<string>run</string>
<string>start</string>
</array>
<key>WorkingDirectory</key> <string>/Users/you/melian</string>
<key>RunAtLoad</key> <true/>
<key>KeepAlive</key> <true/>
<key>StandardOutPath</key> <string>/Users/you/.melian/melian.log</string>
<key>StandardErrorPath</key> <string>/Users/you/.melian/melian-error.log</string>
</dict>
</plist>Load with: launchctl load ~/Library/LaunchAgents/com.your-username.melian.plist
Linux (systemd)
On Linux, a user-level systemd unit provides equivalent behavior:
# ~/.config/systemd/user/melian.service
[Unit]
Description=Melian personal assistant
After=network.target
[Service]
ExecStart=/home/you/.bun/bin/bun run start
WorkingDirectory=/home/you/melian
Restart=on-failure
Environment=HOME=/home/you
[Install]
WantedBy=default.targetEnable with: systemctl --user enable --now melian
Self-Healing Agent
A nightly integration test suite runs at 2:00 AM via launchd/systemd. At 2:30 AM, a separate auto-fix agent reviews any failures and opens PRs against the Melian repository with proposed fixes. This ensures the system self-monitors without manual intervention. See docs/known-issues.md for current open issues surfaced by this process.