openotters

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):

  1. CLI flag (--executor docker)
  2. Environment variable (OTTERSD_EXECUTOR=docker)
  3. YAML config file
  4. Built-in default

Config file locations

First match wins:

  1. --config / -c flag (any path you supply)
  2. ~/.otters/ottersd.yaml โ€” user-scoped, same directory as the rest of the daemon's state.
  3. /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: 5s

The 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 serve

Or one-shot at the CLI:

ottersd serve --executor docker

otters 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
StateMeaning
pullingImage / layers being downloaded into the local store. Slow if cold cache.
startingWorkspace materialised, model resolved, runtime subprocess / container spawned, awaiting the readiness probe.
readyRuntime answered the readiness probe. Idle, accepting RPCs.
workingAt least one chat turn or async-job RPC is in flight to this agent.
stoppedUser-initiated stop, or the runtime exited.
failedTerminal error. The companion failure_reason (pull / init / model / readiness_timeout / crashed) explains the cause; the dashboard surfaces it under the badge.
removingCleanup in progress.
removedAgent 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:

KeyDefaultEffect
max-tokens4096Max output tokens per generation.
max-iterations20Max tool-call iterations per turn.
memory-strategysummarizeCompactor mode โ€” summarize or sliding.
memory-max-messages20Compact once history crosses this length.
memory-max-tokens0 (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.