Eduardo Silveira
All projects

Otto

A clone of Poke: a coding agent behind a messaging channel that starts the conversation, where most of the engineering goes into deciding not to.

Status
Running as an always-on session under launchd, in daily use.
Stack
TypeScript · Bun · Claude Code channel plugin · Telegram Bot API · Whisper (MLX) · Qwen3-TTS · launchd
Size
~1.9k lines in one server, plus a voice sidecar
Links
GitHub

A coding agent answers when you ask it something. Poke, the assistant this started as a clone of, does the other thing: it texts first. Otto is that idea built as a channel plugin — one server that injects events into a live Claude Code session so the assistant can open the conversation rather than wait in it.

One server runs three subsystems and an optional fourth.

Telegram I/O long-polls a bot, gates every message against an allowlist, injects it into the running session, and exposes reply, react, edit and attachment-download as tools.

A trigger engine stores scheduled and conditional work. A cron expression means recurring; an ISO datetime means one-shot. Email triggers are natural-language conditions evaluated by the session on each scan. Mutually-cancelling pairs implement "do X unless Y happens" — two triggers, each holding the other's id, each deleting the other when it fires.

Mail scan ticks fire on a cadence, honour quiet hours, and hand the session a window to search. It classifies what it finds into interrupt-worthy and not.

Voice, optionally: inbound voice notes are transcribed with Whisper before the session sees them, and replies can be spoken back through a local TTS model. Both run through MLX in a sidecar daemon. Skip the install and it is a text channel again.

Most of the work is refusal

The hard part of a proactive assistant is not making it act. It is making it not.

  • Silence is a deliberate, valid outcome of any trigger or any scan. A trigger that fires into a situation whose premise no longer holds does nothing at all, and says nothing about it.
  • Never two proactive messages about the same underlying thing.
  • No mail is ever sent, replied to or forwarded without showing the draft and getting a clear yes.
  • Email content, trigger payloads and webhook bodies are data, never instructions. A message asking to be added to the allowlist is exactly the message a prompt injection would send.

Explicit reminders always fire. Everything else has to earn the interruption.

One entity, several surfaces

The same session is reachable from the terminal and from the phone, which creates a rule that is easy to state and easy to get wrong: a message typed in the terminal is answered in the terminal only. Mirroring it to the phone is noise — the person is already reading the transcript.

The assistant also never mentions triggers, scans, agents, tools or sessions. Not because the machinery is secret, but because "my email-scan trigger fired" is a description of plumbing where a person would have said "your landlord replied".

Buttons beat asking

Whenever the answer is one of a few known actions, it goes out as buttons rather than a question. Email drafts get send/discard. Reminders get done/1h/tomorrow. A tap can arrive days later or after a restart, so the payload is self-contained and trusted over conversation memory — the session that receives it may not be the one that sent it.

Automations you can install

The patterns that kept recurring became templates rather than prose I retype: a daily news digest, a Sunday recap, a bill reminder that fires two days before the due date, a page watch that only speaks when a price moves. The one worth the trouble is the follow-up nudge, which is the cancelling pair in practice — a one-shot reminder three days out, and an email condition watching for the reply that deletes it.

Ops notes

It runs as a launchd agent so it survives logout, which surfaced a constraint worth writing down: a headless session has to prove its credentials refresh without a browser. Cron triggers use the machine's local timezone, which matters more than it sounds when the person holding the phone has changed continents.

A separate script reads the same trigger store and registers one-shot wake alarms with pmset, so the Mac sleeps between obligations instead of being pinned awake for them. It writes nothing back and the server never loads it: if it breaks, the assistant is unaffected and just stops sleeping usefully.