Runtime
What ottersd does, how it's configured, and where its state lives on disk.
ottersd is the long-running daemon process that hosts your agents. It
owns the agent loop, the local image store, persistent state (sessions,
chat history, captured tool output), provider credentials, and the
spawn environment that keeps host secrets out of the agent's reach.
Local layout
By default, ottersd keeps state under ~/.otters/:
~/.otters/
โโโ agents/ # per-agent runtime state (workspaces, agent.yaml)
โโโ logs/ # per-agent runtime logs (one file per agent UUID)
โโโ providers.yaml # registered LLM providers (otters provider add)
โโโ daemon.db # SQLite state store (agents, sessions, jobs, mounts)
โโโ registry/ # embedded OCI store (system executor only)The Docker executor uses Docker's image store instead of registry/;
that directory only exists under the system backend.
Configuration
ottersd serve reads YAML on start-up and merges with CLI flags and
environment variables. Precedence (highest first):
- CLI flag (
--executor docker) - Environment variable (
OTTERSD_EXECUTOR=docker) - YAML config file
- Built-in default
Config file locations
First match wins:
--config/-cflag (any path you supply)~/.otters/ottersd.yamlโ user-scoped, same directory as the rest of the daemon's state./etc/otters/ottersd.yamlโ host-scoped, for systemd-managed daemons.
Every serve flag has a matching YAML key โ kebab-case becomes nested
keys under serve:. Run ottersd serve --help for the full list.
Example: ~/.otters/ottersd.yaml
serve:
# Backend: 'system' (default) or 'docker'.
# See /docs/executors for the comparison.
executor: docker
# Pin the unix socket / TCP listener / data paths.
socket-path: /var/run/ottersd.sock
http-addr: 127.0.0.1:5500
no-ui: false
# Pool tuning โ defaults shown for context.
max-concurrent: 10
backoff-base: 1s
backoff-cap: 30s
shutdown-timeout: 5sThe same settings via env vars (e.g. for a systemd unit or a Colima post-start hook):
export OTTERSD_EXECUTOR=docker
export OTTERSD_HTTP_ADDR=127.0.0.1:5500
ottersd serveOr one-shot at the CLI:
ottersd serve --executor dockerotters info reports the active backend so you can confirm which one is
in effect after a restart.
Agent lifecycle
An agent walks through a small state machine from creation to
removal. The dashboard renders each state as a coloured badge, and
the daemon surfaces them on the wire via AgentInfo.status.
pulling โโ (cache hit) โโโ
โ โ
โผ โผ
starting โโโโโโโโโโบ failed (with failure_reason)
โ โฒ
โผ โ
ready โโโโโโโโโโโโโโโโโโโโค
โ โฒ โ
โผ โ โ
working โโโโโโโโโโโโโโโโโโ
โ
โผ
stopped โโ otters start โโโบ pulling
โ
โผ
removing โโโบ removed| State | Meaning |
|---|---|
pulling | Image / layers being downloaded into the local store. Slow if cold cache. |
starting | Workspace materialised, model resolved, runtime subprocess / container spawned, awaiting the readiness probe. |
ready | Runtime answered the readiness probe. Idle, accepting RPCs. |
working | At least one chat turn or async-job RPC is in flight to this agent. |
stopped | User-initiated stop, or the runtime exited. |
failed | Terminal error. The companion failure_reason (pull / init / model / readiness_timeout / crashed) explains the cause; the dashboard surfaces it under the badge. |
removing | Cleanup in progress. |
removed | Agent is gone. |
pulling and starting are transient โ the badge pulses amber in
the dashboard. ready and working are healthy (green / blue);
failed is red with the reason inline. Auto-restart with backoff
kicks in on failed + pull / init / model; readiness_timeout
and crashed are terminal and need operator action.
Spawn environment
Every agent runs with a locked-down env so host secrets stay out of
reach, even when the agent spawns subprocesses via sh. The Agentfile
can opt in extra OS env vars via the ENV
instruction; reserved keys are rejected at build time:
PATHโ<agent-root>/usr/bin(system) or/opt/bins(docker, flat dir of symlinks โ/opt/bin-images/<name>/<name>).HOME,XDG_*,TMPDIRโ rooted under the agent's directory.LANG,OTTERS_AGENT_ROOTโ set by the runtime.*_API_KEY,*_API_BASEโ travel through the provider-credential channel; declare a provider instead.
The lock-down rules apply to both executor backends.
Runtime tunables
This is the stock ghcr.io/openotters/runtime contract. A custom
runtime image is free to consume an entirely different set of keys โ
the CONFIG directive is a runtime-extension
point, not a fixed spec vocabulary. Always consult the runtime image
you're targeting for its authoritative list.
The stock runtime reads these keys from the configs: map in
etc/agent.yaml (which the daemon stamps from the Agentfile's
CONFIG directives, kebab-case keys mirroring the source 1:1).
Defaults apply when a key is absent:
| Key | Default | Effect |
|---|---|---|
max-tokens | 4096 | Max output tokens per generation. |
max-iterations | 20 | Max tool-call iterations per turn. |
memory-strategy | summarize | Compactor mode โ summarize or sliding. |
memory-max-messages | 20 | Compact once history crosses this length. |
memory-max-tokens | 0 (off) | Compact once estimated token count crosses this. |
These are not CLI flags. The Agentfile (or a daemon-side override
that ultimately rewrites the same configs: map) is the only input
path. Unknown keys are accepted and persisted to agent.yaml
regardless โ a custom runtime can read them via the same mechanism โ
but the stock runtime logs unknown keys at debug.
See CONFIG for the writing side and
Workspaces โ agent.yaml for the
on-disk shape.
See also
- Workspaces โ the per-agent FHS root and the context files the runtime injects into the model's system prompt.
- Sessions โ what's persisted per conversation, and how sessions are shared across surfaces.
- Authentication โ the JWT model on every listener.
- Executors โ how agents are run (system vs docker).
- Tools (BINs) โ how tools are packaged and mounted.
- Async jobs โ long-running background work.
- Surfaces โ TUI, dashboard, programmatic.