Staying recoverable
The system is built so that no self-modification can make it unreachable. Every layer below assumes the agent will eventually write something broken.
Nothing goes live untested
A candidate must clear three things before it replaces what is running. It has to compile. It has to load as a component for its world. And it has to pass a smoke test appropriate to what it is: the agent answers a health probe, a gateway serves its index page, a tool returns a valid manifest.
The smoke test is the export the world already declares, so there is nothing extra for the agent to implement and nothing it can skip. A candidate that fails is recorded and set aside, and the running revision is untouched. The failure is evidence rather than an outage.
An infinite loop traps in seconds
Every guest call runs under a wall-clock budget and a limit on how long it may execute without yielding to a host call. Both come from [budgets]: the no-yield slice that catches infinite loops (wasm_slice_secs), and the tool and probe budgets. There is deliberately no ceiling on a turn as a whole — long work is legitimate; refusing to yield is not.
A tight loop in the agent never reaches a host call, so it burns through the no-yield slice and traps. The guest dies, the process is unharmed, and the orchestrator is still serving. Every guest call runs under the budget, not only the ones that look risky.
Repeated traps roll themselves back
One trap is an incident. A pattern of them is a bad build, and the circuit breaker treats it that way: repeated traps from one aspect roll it back to its last known-good build automatically. The window and threshold are [watchdog] settings, along with the probe interval, watch suppression, and debounce.
The incident is written into the conversation, so the user sees what happened and the agent reads it as context on the next turn. A rollback that happened silently would leave the agent confidently editing code that is no longer running.
Nothing it does to itself is final
Versioning is the conversation's git branch. Every green build lands as a
commit, and the built artifact — with its smoke-test verdict — is stored in a
content-addressed cache keyed by the source tree that produced it. Rolling back
means putting the tree back at a commit whose key is green:
reset_branch restores the whole branch to that point as a new
commit, so history is preserved and nothing is rewritten.
Source and binary cannot drift apart, because the binary is looked up by the source. The code the agent reads is always the code that is running, and the same tree builds once across every branch that holds it — a checkout whose key is already cached loads with no toolchain at all.
Trunk is the ground under all of it. Every conversation's branch starts pinned to trunk's head, its experiments stay on that branch, and trunk only ever advances when a person merges a branch into it. Whatever the agent does to itself, what every new conversation starts from is something a human chose.
The build cache lives in artifacts/; sessions and the event log
live in data/thetis.redb.
A control panel with no WebAssembly in it
The orchestrator renders /admin itself. No guest is involved in serving it, which is the entire design requirement: it keeps working when every guest and every worker is broken.
It shows trunk's recent commits, and every conversation — its branch, its worker, how far it sits ahead of or behind trunk — with the levers beside them: stop a worker (nothing is lost; branch state is on disk and in the log), merge a branch into trunk, reset trunk as the break-glass path.
The failure it exists for is the one where the agent rewrites the gateway into something that will not serve a page. The chat UI is gone, the way you would normally talk to the agent is gone, and /admin is still there with the whole branch ledger and a restore path. Whether it is on is a [server] setting.
Caching the agent cannot break
Prompt caching is on by default and is the single largest cost lever. A repeat turn in a long conversation costs about a tenth of a fresh one.
Providers differ in kind, not only in syntax, so the strategy is per vendor:
| Vendor | Behaviour | What Thetis does |
|---|---|---|
| Anthropic | Caches nothing unless the request marks where | Writes explicit cache_control breakpoints |
| OpenAI | Caches long prefixes automatically | Nothing, since marking would only bill writes |
| Implicit on recent models; explicit bills a full-price write plus storage, and only the last mark counts | Left implicit |
Anthropic's cache is a prefix cache over tools → system → messages, where a breakpoint writes one entry covering everything up to that block. The subtlety that decides whether this works at all: a later request hashes its prefix at each breakpoint and walks back at most twenty blocks looking for a match. A turn that runs a dozen tools can add more than twenty blocks at once, so a lone breakpoint at the end would sail past the previous entry and re-read the whole conversation at full price.
Thetis therefore places up to four breakpoints, the limit Anthropic allows: one on the last system message, two on anchors that sit at a fixed stride and so hold still while the conversation grows around them, and one on the final message, which writes the newest prefix for the next turn to read back. The stride is [cache] anchor_stride, next to the TTL and the list of vendors needing explicit breakpoints.
Breakpoints are applied host-side in cache.rs, after the model is resolved and regardless of what the agent sent. Caching cannot be broken by the agent rewriting its own loop, which is why it belongs on this page rather than in a chapter about cost.
Cache hits show in the transcript under each reply, because a saving you cannot see is one you cannot trust. Measured on a three-turn conversation with the full tool surface: the opening turn cost $0.0106, and each turn after it reported 99% of its prompt served from cache at $0.0010.