Robin is a single-process Node.js daemon that owns scheduling, memory, LLM dispatch, and the integration lifecycle. It exposes itself as two MCP servers that Claude Code (or any MCP client) connects to.
system/ Framework source (TypeScript, compiled to dist/)
├── kernel/ Daemon runtime, scheduler, health monitor, config, invariants
├── brain/ Memory (DB, entities, recall, beliefs), cognition (biographer,
│ dream, capture, embedder), LLM dispatcher
├── integrations/ Built-in integrations + runtime loader
├── skills/ Built-in skills + runtime loader
├── jobs/ Job runner
├── surfaces/ CLI, HTTP server, MCP servers (core + extension)
├── lib/ Shared utilities (paths, logging, telemetry, with-timeout, power)
└── tests/ Integration / architecture boundary tests
user-data/ Per-user instance data (gitignored)
├── config/ models.yaml, policies.yaml, hardware.yaml, secrets/
├── extensions/ User integrations, jobs, skills, scripts, triggers
├── content/ Knowledge files, artifacts, sources
├── state/ SQLite DB, runtime PID
└── observability/ Daemon logs, eval data
dist/ Compiled output (tsc + mirrored manifests + skills)
A single SQLite file (user-data/state/db/robin.sqlite) via better-sqlite3, with sqlite-vec for vector search. WAL mode, applied via a versioned migration runner (system/brain/memory/migrations/).
| Table | Purpose |
|---|---|
events |
Append-only event firehose — every captured session, integration tick, belief, prediction, etc. |
events_content |
Large bodies + 4096-dim embeddings (Matryoshka, via sqlite-vec). Separated from events for scan efficiency. |
events_content_fts |
FTS5 full-text index over content bodies. |
entities |
Canonical entities (person, place, tool, service, etc.) with profiles. |
relations |
Subject–predicate–object triples linking entities, sourced from biographer extraction. |
predictions |
Claims with confidence, deadline, resolution, Brier score. |
corrections |
What Robin said wrong + the correction — feeds the self-learning loop. |
refusals |
Logged refusals with reason + action policy. |
journals |
Daily narrative summaries generated by the dream loop. |
metrics_daily |
Rolled-up daily metrics (Brier calibration, entity counts, etc.). |
jobs |
Scheduler job queue (cron, manual, delayed). One row per firing; completed rows accumulate. |
biographer_progress |
In-flight chunk cursor for multi-tick biographer extraction. Deleted on session completion. |
The relations table is the graph layer — SPO triples indexed on (subject_id, predicate) and (object_id, predicate), traversed via relatedEntities(). A dedicated graph engine (Kuzu) was evaluated and removed 2026-06-11: upstream was archived in Oct 2025, and at current scale (<10k edges) SQLite traversal is nowhere near a bottleneck. If that changes, rebuild a projection from entities + relations against whatever engine has won by then.
Role-based routing defined in user-data/config/models.yaml:
roles:
embed: { provider: ollama, model: qwen3-embedding:8b }
reasoning: { provider: ollama, model: qwen3.5:35b-a3b }
summarize: { provider: ollama, model: qwen3.5:35b-a3b }The dispatcher wraps every call in withTimeout (default 5 min, overridable per-call) so a hung provider can never wedge the scheduler. Providers are registered at daemon startup; missing secrets produce a warning but don't crash (lenient: true).
The daemon's runLoop calls Scheduler.tickOnce() every second, which claims and runs one pending job. Jobs are leased (5 min TTL); a periodic reaper resets expired leases. Cron jobs self-re-arm via rescheduleCronAfterCompletion so a transient failure can't permanently silence a schedule.
Cognition jobs:
biographer.run— entity/relation extraction from captured sessions. Multi-tick: processes a bounded number of chunks per tick, persisting progress inbiographer_progress. A session resumes across cron ticks; no session can hold the scheduler past the daemon's 30-min heartbeat recovery gate.embedder.run— embedsevents_contentrows with null embeddings (deferred from the ingest hot-path, single-flight against Ollama).dream.run— nightly: resolves overdue predictions, rolls up metrics, generates a journal entry.
Invariants run every 60 seconds. The critical one is daemon.heartbeating — if the scheduler tick hasn't run in 5 minutes, the monitor logs CRITICAL. After 30 minutes sustained CRITICAL (not just a slow handler), the daemon calls process.exit(1) for launchd respawn. A boot-window gate (2 min) suppresses recovery during noisy startup.
The biographer's circuit breaker complements this: if every chunk in a tick fails because the LLM is unreachable (connection refused / timeout), the biographer aborts without advancing the cursor or writing a marker — leaving the session to retry when the LLM is back, rather than silently marking it done with empty extraction.
A Claude Code hook (~/.claude/settings.json) POSTs session transcripts to the daemon's HTTP server on session end. The daemon projects the JSONL into turns, applies skip rules (short sessions, non-Robin-folder CWD), deduplicates by content hash, and writes a session.captured event. The biographer picks these up on its cron schedule.
Each integration is a directory with an integration.yaml manifest and an index.ts exporting { tick, actions?, init?, cleanup? }. The daemon's registerIntegrations loads them from both system/integrations/builtin/ and user-data/extensions/integrations/, registers their cron schedules, and hot-reloads on file changes.
Built-in: gmail, google_calendar, github, linear, chrome, weather, finance_quote, claude_code, notify.
Skills are markdown methodology files surfaced via the skill MCP tool on robin-core. The loader scans system/skills/builtin/ and user-data/extensions/skills/, parses SKILL.md frontmatter, and merges with user-shadows-system precedence. The catalog of valid skills is embedded in the skill tool's description at MCP server startup for progressive disclosure — Robin sees what's available without a separate list call.
A typed event writer (writeTelemetry) writes structured events to the events table. An optional OTLP HTTP exporter forwards to external collectors. Telemetry is off by default; configure via policies.yaml.