March 9, 2026

Otto 2/x: Building the Thing I Actually Wanted

A personal build update on Otto: what changed since part 1, why it is built around OpenCode, what already works, and why the architecture became much more practical.

AIAgentsArchitectureSelf-hostingTelegramPersonal Infrastructure

It has been a while since part one. Back then I wrote about the boundaries I wanted: explicit permissions, a system that runs on my own machine, and an assistant I can actually trust. Since then I spent a lot of time learning where these systems get annoying. The hard parts were rarely model calls. They were sessions, transport, updates, recovery, configuration, and all the quiet glue work that decides whether a tool survives outside a demo.

The biggest change is architectural. Otto ended up built around OpenCode. Once I accepted that, the project got much better.

The Wrong Start Was Useful

My first approach was the obvious one. I started with LangChain and LangGraph because they promised flexibility and a clean way to target different models. I had already used them before. I thought I was being pragmatic.

What I was actually doing was signing up to build a lot of software I did not care about. A usable terminal chat loop is already a project. Session persistence is a project. Tool streaming is a project. Prompt layering is a project. Model support across providers is a project. I kept drifting away from the thing I wanted to work on and into the thing I was quietly rebuilding.

That was the point where the project started to feel heavy. I could see months of work ahead and a lot of it would end with something worse than tools I already liked using.

Then I came back to OpenCode. I had used it a lot for software work, but I had been thinking about it too narrowly. Once I spent more time with the server model and the SDK, it clicked for me. OpenCode already solved the parts I was about to spend a lot of time re-creating: sessions, tools, skills, MCP integration, model support, and a shell that already feels solid.

After that, the Otto question became much smaller and much clearer. Keep OpenCode for execution. Build the surrounding system around it: local state, scheduler, transport workers, release flow, control plane, and the integrations I actually want in daily life.

Otto as a Living System

The current Otto install creates a home at ~/.otto and keeps config in ~/.config/otto/config.jsonc. That workspace holds the SQLite state database, prompt files, logs, extension state, scripts, secrets, and the OpenCode runtime footprint.

At the center is one runtime process. On boot it resolves config, prepares the OpenCode side, opens the local state database, starts internal and external APIs, starts the OpenCode server, refreshes model metadata, ensures watchdog and heartbeat jobs exist, starts the scheduler, and then starts the Telegram worker when Telegram is configured. I care a lot about that shape because the useful parts keep running even when I close the machine I am chatting from.

OpenCode handles the agent execution. Otto handles the surrounding machinery. ottoctl installs releases, switches the active version, writes service files, configures Telegram, provisions local voice transcription, manages prompts, manages extensions, and controls scheduled jobs. The control plane runs as a separate web process and talks to Otto's external API. That gives me a browser surface for chat, jobs, settings, and restart controls without stuffing all of that into the runtime itself.

The extension model follows the same idea. Extensions can ship tools, skills, and MCP fragments, and Otto syncs them into the local OpenCode runtime under .opencode. That makes integrations part of the environment instead of special cases in application code. Right now the registry already includes Google Calendar, Obsidian, Brave Search, Playwright MCP, Home Assistant, AnyList, 1Password, and Google Maps tooling.

The part I am happiest about is session continuity. Telegram chats map to stable OpenCode sessions, the message flow is stored, background task context stays attached, and job results can come back into later conversations. This means we have a sort of context meshing in this system - all channels aggregate into each other, giving them context and it means we have a seemless consistency across the space.

Voice support works the same way. Otto can accept Telegram voice messages, download them, transcribe them through a local Faster-Whisper worker, and pass the transcript into the normal conversation path. If the worker is missing or not ready, Otto says so.

Otto is also a proactive system, which i think makes this thing actually great. It can do things on its own, and its own time. For this i designed a sort of tick system, that runs in the background, and can execute tasks at particular points in time - and Otto can plan these tasks himself. For example: I am a bit of a woodworking nerd, and i like to shop for machinery and tools online, particularly used. It is kind of a chore to do this manually - checking your emails for alerts, and missing a lot. Otto does that for me - He has an overview of what i have in my shop, and regularly goes on shopping lookouts throughout the internet, and notifies me when something is available that he thinks is a good price.

One other thing that was very fun for me was reserving a slot at a restaurant. Two friends of mine met in a place we seldomly spent time on - i told him that we would go there, he should look for a restaurant and give me three options (where i picked one) - and i told him to reserve a table for us. And that he did - got the confirmation email in my inbox soon later.

The Part That Became Real

The earlier idea for Otto looked broader on paper. The current version does less and does real work for me.

I use it for the kind of tasks that usually fall between tools. Calendar management is one example. Weekly briefs are another. Product tracking is a third, especially for second-hand listings where the work is mostly waiting, checking, and noticing when something changed. I also use it around woodworking projects, where I am often juggling dimensions, part lists, links, reminders, and delayed decisions over several days. Travel planning fits the same pattern. None of this is glamorous, and all of it is useful.

That is also where the architecture earns its keep. When I spawn background work, there is a real job record behind it. When I return later, the session still exists. When a Telegram conversation belongs to a specific thread, that mapping is durable. When an extension adds capability, it lands in the runtime and survives beyond one custom code path. These details matter more to me than whether the assistant can put on a good demo.

What I Actually Built

So the honest update for part two is straightforward. Otto improved once I stopped insisting on inventing every layer myself. OpenCode gave me a mature execution core. Otto became the local system around it.

That led to concrete changes: release artifacts over fragile setup, ottoctl over scattered shell scripts, a durable workspace over loose files, Telegram as a proper worker, scheduled and background execution, and an extension system that feeds the runtime directly.

This is much closer to what I wanted when I started. Otto now feels like something I can keep on a Linux box and use as part of normal life, which was always the point.

In the next parts i will dive into the details of the architecture and code a little deeper - and what i think it means to live in a world where these agentic systems are the norm (and i truly think these will become part of everyday life).

Sources

Enjoyed this article?

Check out more articles or connect with me