Skip to content

Instantly share code, notes, and snippets.

@nodir-t
Last active February 9, 2026 13:13
Show Gist options
  • Select an option

  • Save nodir-t/582ab9da059c8a2c5be86d3f38a41023 to your computer and use it in GitHub Desktop.

Select an option

Save nodir-t/582ab9da059c8a2c5be86d3f38a41023 to your computer and use it in GitHub Desktop.
Claudir Architecture — Part 1: The Three-Tier System

Part 1: The Three-Tier System

Three-Tier Architecture

The Three Tiers

Each tier has a distinct identity, trust level, and permission set. The bot tiers share the same Rust binary (claudir) but run with different configuration files that control their behavior.

Tier 0: Owner & Supervisor

The Owner communicates via Telegram — either in bot_xona (the shared admin group) or via DMs to Mirzo — and makes all business decisions from there. The Owner doesn't need to be at the server to approve things; Telegram is the control plane.

The Supervisor is a separate entity: a raw Claude Code process running in a terminal on the server, used only for manual intervention — debugging, emergency fixes, or direct code modifications. It is NOT the claudir binary. It's just claude (the CC CLI) with full shell access. Most of the time the Supervisor does nothing — it exists as a fallback for when the bots can't handle something themselves.

Tier 1: Mirzo (Private Assistant)

Mirzo is the owner's private assistant bot. Its configuration lives in data/prod/mirzo/ and it runs with full_permissions: true in its bot.json. This means Claude Code gets --dangerously-skip-permissions, giving it access to Bash, Edit, Write, Read, and all other tools.

The full-permissions mode is safe here because Mirzo only accepts messages from the owner and other bots (owner_dms_only: true in config). No public users can reach it. Its primary roles:

  1. CTO — manages code changes, delegates to agent teams, makes architectural decisions
  2. Monitor Nodira's health via cross-bot heartbeat checking (monitor_heartbeats: true)
  3. Act as the owner's proxy in bot_xona where the bots communicate

Mirzo uses claude-opus-4-6 — the most capable Claude model for complex reasoning and code tasks.

Tier 2: Nodira & Dilya (Public Chatbots)

Nodira is the primary public-facing bot. Dilya is a newer bot at the same tier, still learning the ropes. Both have full_permissions: false, which means Claude Code runs with --tools "WebSearch,WebFetch" only. No Bash, no Edit, no Write, no Read. This is the critical security boundary — even if a user tricks the LLM through prompt injection, it cannot execute arbitrary code on the server.

They monitor Telegram groups (listed in allowed_groups), filter spam using a two-tier classifier (regex prefilter + Claude Haiku), and participate in conversations as group members. They access tools exclusively through MCP (Model Context Protocol), where the Rust harness validates and executes every tool call server-side.

How They Communicate: Bot-to-Bot Messaging

The bots share a SQLite database at data/prod/shared/bot_messages.db. This is their "chat room." When Mirzo wants to tell Nodira something (or vice versa), it writes to this shared DB. Each bot polls for new messages every 500 milliseconds:

tokio::spawn(async move {
    loop {
        if let Some(ref chatbot) = poll_state.chatbot {
            let count = chatbot.poll_bot_messages().await;
            if count > 0 {
                info!("Received {} bot message(s)", count);
            }
        }
        tokio::time::sleep(Duration::from_millis(500)).await;
    }
});

The BotMessageDb struct handles the read/write side. Each bot writes messages tagged with its name and filters reads to only receive messages from other bots in groups it belongs to. Fast, persistent, and completely decoupled from the Telegram API.

There is also a dedicated bot_xona group — a Telegram group where all bots and the owner are members. Messages sent to this group's chat ID get routed through the shared DB, allowing the bots to have discussions visible to the owner.

Configuration: Same Binary, Different Personalities

All three bots use the same compiled binary. The differentiation happens entirely through config:

data/prod/
  mirzo/
    bot.json          # Config: full_permissions=true, owner_dms_only=true
    session_id        # Claude Code session persistence
    claudir.db        # Personal DB (messages, users, reminders, strikes)
    memories/         # Persistent memory files
      SYSTEM.md       # Bot-specific system prompt instructions
      reflections/    # Self-improvement journal
    logs/
      claudir.log     # Single persistent log file
  nodira/
    bot.json          # Config: full_permissions=false
    session_id
    claudir.db        # Personal DB
    memories/
    logs/
  shared/
    bot_messages.db   # Bot-to-bot communication
    SYSTEM.md         # Shared system prompt (both bots)
    memories/         # Shared memory files

The system prompt is assembled from three sources: a shared SYSTEM.md (immutable principles all bots follow), a bot-specific prompt file (referenced by system_prompt_path in config), and dynamically injected runtime context (available MCP tools, TTS voices, loaded skills).

The Wrapper Process: Self-Healing

Every claudir process actually runs as two processes: a wrapper and a worker (the actual bot). The wrapper's job is simple — restart the worker if it dies:

fn run_wrapper(args: &Args) -> ! {
    loop {
        let mut child = Command::new(&exe).args(&child_args).spawn()...;
        let status = child.wait()...;

        // Track restarts in sliding window to detect loops
        recent_restarts.push(now);
        recent_restarts.retain(|t| now.duration_since(*t) < WINDOW_DURATION);

        if recent_restarts.len() > MAX_RESTARTS_IN_WINDOW {
            // 10 restarts in 10 minutes = give up
            std::process::exit(1);
        }

        // Exponential backoff for rapid crashes
        if uptime < Duration::from_secs(10) {
            let backoff = Duration::from_secs(2u64.pow(restart_count.min(6)));
            std::thread::sleep(backoff);
        }
    }
}

The wrapper tracks a sliding window of recent restarts. If the worker crashes more than 10 times in 10 minutes, it stops restarting to prevent infinite loops. For rapid crashes (worker dies within 10 seconds), it applies exponential backoff: 2s, 4s, 8s, up to 64s max.

A clever detail: the wrapper re-resolves the binary path on each restart using canonicalize() on argv[0]. This means if you rebuild the binary while it is running, the next restart picks up the new version automatically — no deployment tooling needed.

Session Persistence: Surviving Restarts

Each bot maintains a session_id file that allows Claude Code to resume its conversation across restarts. When starting, the harness checks if this file exists and passes --resume <session_id> to Claude Code. This preserves the conversation history — the bot remembers what it was discussing before the restart.

Deleting the session file forces a fresh start. This is important when system prompt changes need to take effect, since CC's --resume feature loads the old conversation with the old prompt baked in.

Why This Design?

The three-tier architecture solves several real problems:

  1. Security isolation: The public bot (Nodira) cannot run code, even if compromised via prompt injection. All it can do is call pre-defined MCP tools that the Rust harness validates.
  2. Operational resilience: Each tier can restart independently. Mirzo monitors Nodira and restarts it if it dies. The wrapper process handles crash recovery automatically.
  3. Trust hierarchy: Business decisions (pricing, access control) require owner approval via Telegram. Nodira cannot relay user requests for capability changes to Mirzo and have them executed — a lesson learned from a real social engineering incident.
  4. Shared infrastructure: All bots share the same binary and a shared communication database (bot_messages.db), while each bot maintains its own personal database for messages, users, reminders, and strikes. Configuration is the only behavioral differentiator.

The result is a system where an AI chatbot runs 24/7 on a single Linux machine, self-heals from crashes, filters spam, communicates across bot instances via shared SQLite, and maintains strict security boundaries — all in pure Rust with Claude Code as the brain.


Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment