>_< arvis

01 — Architecture Overview

Every process, every port, every module and what it does.


The Two Processes

Arvis runs as two separate Node.js processes that share a SQLite database:

┌─────────────────────────────────────────────────────────────────┐
│  PROCESS 1: Arvis Core  (node src/main.ts)                      │
│                                                                  │
│  - The "brain" — handles all actual agent work                  │
│  - Connects to Discord, Telegram, Slack, etc.                   │
│  - Runs the LLM (Claude CLI or direct API)                      │
│  - Manages the job queue                                        │
│  - Runs scheduled tasks                                         │
│  - Exposes WebSocket on :5070 (for dashboard chat)              │
│  - Exposes HTTP on :5050 (for external webhooks)               │
└─────────────────────────────────────────────────────────────────┘
                        │
                        │ Both read/write same file:
                        │ data/arvis.db (SQLite, WAL mode)
                        │
┌─────────────────────────────────────────────────────────────────┐
│  PROCESS 2: Dashboard  (next dev --port 5100)                   │
│                                                                  │
│  - The "eyes" — admin UI for monitoring and configuration       │
│  - Next.js 15 app, imports @arvis/core to read DB directly      │
│  - NO separate API server — DB reads happen in API route        │
│    handlers that import from packages/core                      │
│  - Dashboard chat goes through :5070 (Process 1's WebSocket)    │
└─────────────────────────────────────────────────────────────────┘

Why WAL Mode For SQLite?

WAL (Write-Ahead Logging) allows multiple readers AND one writer simultaneously. This means both processes can read/write the same DB file without locking each other out.


Ports

Port Service Used By
5100 Dashboard (Next.js) Your browser
5070 Web Connector (WebSocket + REST) Dashboard chat, external REST calls
5050 Webhook Server GitHub, Zapier, or any HTTP caller

Full Module Map

packages/core/src/
│
├── arvis.ts                 ← MAIN ORCHESTRATOR
│   Wires everything together. The class you instantiate.
│   start() → connects all modules, starts queue, connects bots
│
├── config.ts                ← CONFIG LOADER
│   Reads .env, detects CLI accounts (HOME dirs), API keys
│   Returns ArvisConfig with all settings
│
├── logger.ts                ← STRUCTURED LOGGER
│   Pino-based JSON logger. LOG_LEVEL env var.
│   Used as: const log = createLogger('module-name')
│
├── db/
│   ├── database.ts          ← DB WRAPPER
│   │   Wraps better-sqlite3. Adds run(), get(), all(), transaction()
│   │   WAL mode, foreign keys ON, busy timeout 5s
│   ├── schema.ts            ← TS INTERFACES FOR DB ROWS
│   │   ConversationRow, MessageRow, AgentRow, etc.
│   └── migrations/
│       ├── 001-initial.ts   ← BASE SCHEMA (all core tables)
│       └── 002-multi-provider.ts ← accounts, usage_log tables
│
├── agents/
│   ├── agent.ts             ← AGENT TYPE DEFINITION
│   │   AgentConfig interface: id, slug, name, role, model,
│   │   allowedTools, systemPrompt, personality, etc.
│   ├── agent-registry.ts    ← AGENT CRUD
│   │   getAll(), getById(), getBySlug(), create(), update(), delete()
│   │   Also manages channel bindings (agent_channels table)
│   ├── conductor.ts         ← CONDUCTOR SYSTEM
│   │   CONDUCTOR_SYSTEM_PROMPT constant
│   │   ConductorParser: parses [CREATE_AGENT], [CREATE_CRON] etc.
│   │   execute() runs parsed actions against the registry
│   ├── router.ts            ← MESSAGE ROUTER
│   │   route(msg) → Agent | null
│   │   6-step priority logic (see 03-routing.md)
│   └── delegation-parser.ts ← DELEGATION TAGS
│       parseDelegations() → finds [DELEGATE:slug] blocks
│       stripDelegations() → removes them from response text
│
├── conversation/
│   ├── conversation-manager.ts ← CONVERSATION CRUD
│   │   getOrCreate() → finds or creates for (agent+platform+channel)
│   │   addMessage() → stores message, updates token estimate
│   │   compact() → two-phase: memory flush + summarize
│   │   getHistory() → with token budget trimming
│   ├── context-builder.ts   ← PROMPT ASSEMBLY
│   │   build() → assembles full LLM context (6 layers)
│   │   getCompactionThreshold() → 75% of model's context window
│   └── types.ts             ← Conversation, Message TS types
│
├── runner/
│   ├── agent-runner.ts      ← RUNNER ORCHESTRATOR
│   │   execute() → picks best account, handles failover
│   │   3-stage: preferred → fallback chain → any account
│   ├── cli-runner.ts        ← CLAUDE CLI SUBPROCESS
│   │   Spawns: claude --print --continue (in conversation CWD)
│   │   Prompt via stdin, response from stdout
│   │   180s timeout
│   ├── provider-runner.ts   ← DIRECT API RUNNER
│   │   Supports: Anthropic, OpenAI, OpenRouter, Google, Ollama, custom
│   │   Multi-turn tool loop (up to 5 tool calls per response)
│   ├── account-manager.ts   ← ACCOUNT POOL
│   │   getAvailable(), markRateLimited(), clearRateLimit()
│   │   recordUsage(), recordCost()
│   ├── classifier.ts        ← COMPLEXITY CLASSIFIER
│   │   classifyComplexity(prompt) → 'fast' | 'full'
│   │   'fast' = short/simple → use haiku/mini
│   │   'full' = complex → use sonnet/opus
│   └── types.ts             ← RunRequest, RunResult, Provider types
│
├── memory/
│   ├── memory-manager.ts    ← MEMORY SYSTEM
│   │   parseAndSave() → extracts [MEMORY:*] [STATE:*] tags from output
│   │   getFacts() → retrieves facts for agent (FTS5 search)
│   │   stripTags() → removes memory tags from response
│   └── types.ts             ← MemoryFact, KVPair types
│
├── queue/
│   ├── queue-manager.ts     ← JOB QUEUE
│   │   enqueue() → adds job, calls setImmediate(processNext) instantly
│   │   processNext() → marks running, executes, handles retry
│   │   recoverStuckJobs() → marks 5min+ running jobs as failed
│   └── types.ts             ← QueueJob, QueueStatus types
│
├── scheduler/
│   └── scheduler.ts         ← CRON + HEARTBEAT RUNNER
│       Polls every 10s. Flood guard prevents duplicate jobs.
│       Supports: "every 5m", "0 9 * * *", "*/10 * * * * *"
│
├── bus/
│   ├── message-bus.ts       ← EVENT BUS
│   │   EventEmitter. 'message', 'send', 'typing' events.
│   │   Connectors and core talk through here, never directly.
│   └── types.ts             ← IncomingMessage, OutgoingMessage types
│
├── skills/
│   ├── skill-loader.ts      ← LOADS .md SKILL FILES
│   │   Reads skills/*.md, inserts into skills DB table
│   └── skill-injector.ts    ← INJECTS RELEVANT SKILLS
│       Scores skills by keyword match to current message
│       Only injects skills with score > 0 (prevents prompt bloat)
│
├── tools/
│   └── tool-executor.ts     ← BUILT-IN TOOLS
│       web_search  → DuckDuckGo Instant Answers
│       calculate   → Safe math eval (whitelisted operations)
│       get_time    → Current datetime
│       http_fetch  → Fetches URL, strips HTML, 3000 char limit
│
├── billing/
│   └── billing-manager.ts   ← CLIENT BILLING
│       Clients, charges, balances. Used if you bill clients.
│
├── webhooks/
│   └── webhook-server.ts    ← EXTERNAL HTTP TRIGGERS
│       HTTP server on :5050
│       Validates HMAC signature, enqueues job for agent
│
└── connectors/
    └── connector-manager.ts ← BOT INSTANCE MANAGER
        Reads bot_instances table, starts/stops connectors
        Polls DB every 30s for changes (hot-reload)
        seedFromEnv() → imports env-var bots to DB on first run

Data Directories

data/
├── arvis.db          ← Main SQLite database (all state lives here)
├── arvis.db-shm      ← SQLite WAL shared memory (auto-managed)
├── arvis.db-wal      ← SQLite WAL journal (auto-managed)
├── sessions/
│   ├── 1/            ← Claude CLI working dir for conversation #1
│   ├── 2/            ← Claude CLI working dir for conversation #2
│   └── ...           ← One dir per conversation = no context bleed
└── backups/
    ├── arvis-2026-03-01.db
    └── arvis-2026-03-02.db  ← Daily backup, last 7 kept

Environment Variables

# Core
ARVIS_DATA_DIR=./data         # Where DB and sessions live

# LLM — at least one required
CLAUDE_CLI_HOME=~/.claude     # CLI subscription (Max plan)
ANTHROPIC_API_KEY=sk-ant-...  # Direct Anthropic API
OPENAI_API_KEY=sk-...
OPENROUTER_API_KEY=sk-or-...
GOOGLE_API_KEY=AIza...
OLLAMA_BASE_URL=http://localhost:11434

# Multiple accounts (add _1 _2 ... _50)
ANTHROPIC_API_KEY_1=sk-ant-...
ANTHROPIC_API_KEY_2=sk-ant-...
CLAUDE_CLI_HOME_1=~/.claude-work
CLAUDE_CLI_HOME_2=~/.claude-personal

# Platform bots
DISCORD_TOKEN=...
DISCORD_OWNER_ID=123456789
TELEGRAM_BOT_TOKEN=...
SLACK_BOT_TOKEN=xoxb-...
SLACK_APP_TOKEN=xapp-...
SLACK_SIGNING_SECRET=...
WHATSAPP_ACCESS_TOKEN=...
WHATSAPP_PHONE_NUMBER_ID=...
WHATSAPP_VERIFY_TOKEN=...
TWILIO_ACCOUNT_SID=...
TWILIO_AUTH_TOKEN=...
TWILIO_PHONE_NUMBER=+1...
EMAIL_IMAP_HOST=imap.gmail.com
EMAIL_IMAP_USER=you@gmail.com
EMAIL_IMAP_PASS=app-password

# Dashboard
DASHBOARD_PASSWORD=           # If set, login gate activates
WEB_CONNECTOR_PORT=5070
User GuideMessage Flow