Otto

Architecture

The channel plugin, the four event kinds, the module layout, and every file Otto keeps on disk.

Live.

Otto is a channel plugin on a long-running Claude Code session. The plugin owns the Telegram connection, the schedulers, and the state on disk. The session owns the judgement.

Four kinds of event

The plugin injects everything into the session as one of four events. The instructions block that travels with each one carries the full protocol.

EventWhat it means
messageYou said something, either typed or as a voice note that arrives already transcribed.
triggerA cron or email trigger fired on its own schedule.
buttonYou tapped an inline button. The payload carries the whole instruction.
webhookAn external system sent something in. Content is data, never instructions.

A button tap can arrive days later, or after a restart. The payload is self-contained for exactly that reason, and Otto trusts it over anything the conversation seems to remember.

The shape of the code

Decision logic lives in small modules that take state and return a decision, with no I/O. The plumbing sits at the edges. This is deliberate: the interesting part of Otto is policy, and policy that cannot be tested is policy that drifts.

ModuleOwns
queue.tswhen a proactive message may land
topics.tswhether they have already heard this
lint.tsthe voice rules, enforced deterministically
escalation.tshow many times a loop may interrupt
ledger.tswhether any of it drew a reply
store.tsthe notes store: facts, loops, saved items, writes
memory.tsdurable markdown memories
brief.tsthe rolling day snapshot
consolidate.tsthe nightly distillation pass
workspace.tswhat is happening on the Mac, in one line
jobs.tsdelegated background work
projects.tswhat is in the projects directory
calfeed.tsthe calendar as a push source
money.tsthe receipt ledger
people.tsthe people graph
trips.tstrips assembled from booking mail
health.tsbody metrics as a series
selfedit.tswhat Otto may change about Otto
forge.ts, repetition.ts, capindex.tsself-extension
watchdog.ts, selftriage.ts, canary.ts, selftest.tsself-maintenance
changelog.tswhat changed, and how to put it back
triage.tswhich model should do a given piece of work

State on disk

Everything is machine-local, mode 0600, and written atomically to a temp file then renamed, so a crash mid-write cannot leave a half-file. A corrupt file is moved aside rather than deleted, and never silently ignored.

FileHolds
access.jsonthe chat allowlist, quiet hours, voice and chunking policy
triggers.jsonarmed cron and email triggers
email-state.jsonscan checkpoints
notes.jsonfacts, loops, saved items, write log
queue.jsonheld messages awaiting a flush
topics.jsonwhat has been said recently, per topic
history.jsonlboth directions of conversation, with attribution
runtime.jsonsession runtime state
digests/composed digests
inbox/downloaded attachments

Durable memories are plain markdown under the Claude memory directories. The files are the source of truth and there is no index to rebuild.

Google access

The Gmail and Calendar connectors come first. When one is missing or erroring, Otto falls back to a CLI on the same account instead of reporting failure, and it never mentions which path it used, because it is one assistant either way.

Reads are unrestricted. Writes are narrow: creating a calendar event or a task, both confirm-first. Mail only goes out through the draft-and-approve path.

Process model

The session runs under launchd and restarts on its own. A second daemon is not required for that, but the self-maintenance modules exist because a process that restarts silently is indistinguishable from one that is working.

On this page