Otto minding itself
The self-edit policy, the capability forge, the repetition miner, crash triage, the watchdog, canary deploys and model routing.
Live.
This is the part where Otto maintains and extends itself. It is also the part with the sharpest failure mode, so the policy came first and everything else defers to it.
The self-edit policy
What Otto is allowed to change about Otto is a function of two things: the paths being touched, and where the request came from.
| Tier | Applies to | Requires |
|---|---|---|
| unattended | trigger text, recipe parameters | nothing |
| confirmed | new recipes, prose in the instructions file | one tapped confirm |
| guarded | the server, the send path, auth scopes, the allowlist | an explicitly typed phrase |
The provenance half is what makes it real. Every request carries an origin stamped
by the channel at wake time, never claimed by the model, using the same trick the
send ledger already uses for attribution. Work originating from a fired trigger
has a hard ceiling of unattended, so "a trigger can never edit the send path" is
a property of the code rather than an instruction the model is asked to remember.
A tapped button can satisfy confirmed and can never satisfy guarded. That
closes the case where Otto, already influenced by something it read in an email,
offers you a mislabelled button.
Unrecognised paths fail closed to guarded. The rules table will eventually
fall behind a rename, and the costs of being wrong in the two directions are not
symmetrical.
The capability forge
"You should be able to do X" opens a self-extension job: draft the recipe or skill on a branch, dry-run it against a real past event drawn from the history log, and come back with a one-line summary of what it would have done, plus install, tweak and drop buttons. Every install goes through the policy above.
The loop is minutes rather than a coding session you have to sit through.
The repetition miner
The third time you ask for the same thing, Otto should offer to stop being asked.
Requests are clustered across the history log, and any intent asked three or more times with a stable shape becomes a drafted trigger with its parameters filled from the actual past instances: the cron time inferred from when you asked, the search terms lifted from what you typed.
Shared literals short-circuit token overlap, because "check the price of X" and "is X down yet" share almost nothing except the thing that matters.
Crash triage
On a crash loop, the first question is whether Otto did it to itself: if the first failure landed within an hour of a self-install, that install is reverted. Only otherwise is a bug note filed with the stack.
You get one message: down, back, cause.
The generated reproduction test fails on purpose. A stack trace does not contain enough to synthesise a real reproduction, and a generated test that passed would certify a live bug as fixed.
The watchdog
The things that break without breaking anything: expired tokens, a disconnected connector, a dead local model process, poller lag, an unbound webhook, and brief sections that have been quietly omitted for several rebuilds.
A scripted repair is attempted first, and you hear about it only when repair fails on something you would notice. The failure this prevents is the one that is invisible by construction: a dead calendar source looks exactly like an empty calendar, forever.
Changelog and rollback
Every self-install writes a capability-log entry, and a weekly line says in plain language what changed about Otto this week. Each entry carries a revert button that still resolves three weeks later.
Revert payloads store the previous file contents inline rather than a git sha, because a sha silently stops resolving after a rebase or a garbage collection, which is exactly the moment you say "undo that". Above a size limit it falls back to a sha and says so out loud.
Canary deploys
Deploying a change to the server restarts the one process you talk through, so the deploy tags a last-known-good commit, typechecks and tests, restarts, then watches for a completed channel handshake within ninety seconds and for an early error spike. Either failure hard-resets to the tag.
You hear about it only once Otto is back up, with what broke.
The staging twin
A second session against a git worktree with the channel host unset and state pointed at a scratch copy: candidate code loads and tools answer, with nothing able to reach Telegram. Credentials are scrubbed rather than copied, and the plan asserts the channel is disconnected before anything runs.
Model routing
Mechanical work does not need the main session. Scan classification, watch page reads and shadow evaluations route to a small model, escalating only when the cheap verdict says a human-grade turn is warranted.
Routing picks by complexity rather than using one fixed cheap model: Haiku for the cheapest tier, Sonnet in the middle, the main session otherwise. The routing rule is a pure function with tests, so it can be argued with.
Provenance, stamped rather than claimed
The origin of every wake is stamped by the channel when the event is injected, in its own module, and the model never gets to assert it. That is what makes the ceiling real: a request that arrived through a fired trigger cannot claim to have been typed, because nothing on the model's path writes that field.
This was the last piece to land. Until it existed, the policy above was documentation rather than enforcement.