Skip to main content
ollim-bot is a single-process Python application that bridges Discord with Claude via the Agent SDK. All modules live under src/ollim_bot/, with two sub-packages (google/ and scheduling/) for domain-specific functionality.

Module map

The codebase has 28 modules organized into five layers.

Core loop

The main path from a Discord message to a streamed response.
ModuleRole
main.pyCLI entry point — dispatches to bot or subcommands
bot.pyDiscord interface — DMs, slash commands, reaction acks
agent.pyAgent SDK wrapper — sessions, MCP tools, subagents, slash routing
streamer.pyStreams text deltas to Discord — throttled edits, 2000-char overflow
prompts.pySystem prompt for the main agent and fork prompt helpers
subagent_prompts.pySubagent prompts (gmail-reader, history-reviewer, responsiveness-reviewer)

Tool system

MCP tools and the external trigger server the agent uses to interact with Discord and the outside world.
ModuleRole
agent_tools.pyMCP tools for Discord embeds, pings, forks, and chains
webhook.pyHTTP server for external triggers — auth, validation, Haiku screening
forks.pyFork state (bg + interactive), pending updates, run_agent_background
views.pyPersistent button handlers via DynamicItem — delegates to google/, forks, and streamer

Storage and state

Persistence, configuration, and cross-cutting concerns.
ModuleRole
storage.pyShared JSONL and markdown I/O, git auto-commit for ~/.ollim-bot/
sessions.pyPersists Agent SDK session ID + session history JSONL log
permissions.pyTool approval — canUseTool callback, reaction-based approval
config.pyEnv vars: OLLIM_USER_NAME, OLLIM_BOT_NAME (from .env)
embeds.pyEmbed/button types and builders shared by agent_tools and views
inquiries.pyPersists button inquiry prompts to disk (7-day TTL, survives restarts)
ping_budget.pyRefill-on-read ping budget — capacity 5, refills 1 per 90 min
formatting.pyTool-label formatting helpers shared by agent and permissions

Google integration (google/)

OAuth2-based integrations with Google services.
ModuleRole
auth.pyShared OAuth2 credentials for Tasks + Calendar + Gmail
tasks.pyGoogle Tasks CLI + API helpers (complete_task, delete_task)
calendar.pyGoogle Calendar CLI + API helpers (delete_event)
gmail.pyGmail CLI (ollim-bot gmail) — read-only access

Scheduling (scheduling/)

Proactive routines and reminders via APScheduler.
ModuleRole
scheduler.pyAPScheduler integration — polls files every 10s, registers triggers
routines.pyRoutine dataclass and markdown I/O — recurring crons in routines/*.md
reminders.pyReminder dataclass and markdown I/O — one-shot + chainable
preamble.pyBackground preamble builder — ping budget, schedule, and config
routine_cmd.pyCLI handler for ollim-bot routine (add, list, cancel)
reminder_cmd.pyCLI handler for ollim-bot reminder (add, list, cancel)

Data flow

A message from Discord travels through four stages before a response appears.
1

Discord event

bot.py receives a DM. It extracts text and image attachments, resolves reply context (including fork session resumption), and acquires the agent lock.
2

Agent processing

agent.py injects the message into the active ClaudeSDKClient session. Pending updates from background forks are prepended. The SDK streams text deltas and tool-use events back through an AsyncGenerator.
3

Streaming to Discord

streamer.py consumes the generator, buffering deltas and progressively editing a Discord message. When the message exceeds 2000 characters, it finalizes the current message and starts a new one.
4

Post-stream transitions

bot.py checks for fork transitions — if the agent called enter_fork or exit_fork during the response, the bot handles the state change (creating fork embeds, swapping clients, or discarding the fork).

Background fork execution

Scheduled routines, reminders, and webhooks run on disposable forked sessions that execute in parallel without blocking the main conversation.
The default. run_agent_background creates a client forked from the main session — the fork inherits full conversation history. Output is discarded unless the agent calls report_updates (which writes to pending_updates.json) or ping_user/discord_embed (which message the user directly, subject to ping budget).
Background forks communicate back to the main session through pending updates — summaries written to ~/.ollim-bot/state/pending_updates.json. The main session pops these updates and prepends them to the next user message. Forks peek at updates (read-only) to avoid consuming another fork’s output.
Background forks run without the agent lock. Channel references, chain context, fork state, busy state, and fork config are all scoped via contextvars so concurrent forks don’t interfere with each other or the main session.

Key architectural patterns

Session persistence

The bot maintains a single ClaudeSDKClient with a session ID persisted to ~/.ollim-bot/state/sessions.json. On restart, it resumes the existing session. All session lifecycle events (created, compacted, swapped, cleared, interactive_fork, bg_fork, isolated_bg) are logged to session_history.jsonl.

Contextvar isolation

Background forks use ContextVar instances to scope mutable state. Key variables include _in_fork_var, _busy_var, _channel_var, _chain_context_var, _bg_fork_config_var, _bg_output_flag, _bg_reported_flag, _bg_ping_count, and _msg_collector. This lets multiple forks run concurrently while the main session uses module-level globals for the same values.

File-based storage

All persistent data lives in ~/.ollim-bot/ as files:
  • Markdown with YAML frontmatter for human-editable data (routines, reminders, webhooks) — the agent reads and writes these
  • JSONL for append-only logs (session history)
  • JSON for small state files (session ID, ping budget, inquiries)
storage.py provides generic I/O with atomic writes (temp file + rename) and optional git auto-commit.

Dual state for tools

MCP tools in agent_tools.py maintain two parallel references — a module-level global for the main session and a ContextVar for background forks. Functions like set_channel / set_fork_channel set the appropriate reference based on execution context.

Persistent buttons

Discord buttons survive bot restarts through two mechanisms: DynamicItem[Button] in views.py reconstructs button handlers from custom_id patterns on startup, and inquiries.py persists agent-generated button prompts to disk with a 7-day TTL.

Next steps