Commands, package names, and image names on this page come from the open-source project that Mibyan Desktop is built on, and can differ from the Mibyan Desktop installer. For the supported Mibyan install and update path, see Install and update.
~/.mibyan/ directory for easy access.
Directory Structure
Managing Configuration
Configuration Precedence
Settings are resolved in this order (highest priority first):- CLI arguments — e.g.,
mibyan chat --model anthropic/claude-sonnet-4(per-invocation override) ~/.mibyan/config.yaml— the primary config file for all non-secret settings~/.mibyan/.env— fallback for env vars; required for secrets (API keys, tokens, passwords)- Built-in defaults — hardcoded safe defaults when nothing else is set
Rule of ThumbSecrets (API keys, bot tokens, passwords) go in
.env. Everything else (model, terminal backend, compression settings, memory limits, toolsets) goes in config.yaml. When both are set, config.yaml wins for non-secret settings.Runtime Limits
Long-running Mibyan server surfaces (including the gateway andmibyan serve --isolated) apply the configured RLIMIT_NOFILE soft limit
during startup when the operating system supports it:
4096. Mibyan clamps the target to the operating system’s hard
limit and never lowers a process that already has a higher soft limit. Set the
value to 0, false, or null to disable the adjustment. On Windows and in
sandboxes
where the limit cannot be changed, startup continues without changing the
limit.
Database Settings
Thedatabase: section controls how Mibyan opens its SQLite state database
(state.db), which stores sessions, messages, and gateway routing:
delete — and
names database.journal_mode as the setting that makes the choice stick.
The reverse never happens automatically: a database that is already in WAL
mode is not live-downgraded when you set journal_mode: delete (a downgrade
under open connections can corrupt it). mibyan doctor warns
<db> is in WAL mode despite database.journal_mode=delete until you stop
every Mibyan process for the profile and run
mibyan sessions set-journal-mode delete (it refuses while anything still
holds the file and verifies the converted header). Under that warning it names the
processes currently holding the database (<db> is held by PID <n> (<command>))
so you know what to stop; when the holder scan is partial or unavailable it says
cannot prove the database is quiet instead of giving an all-clear.
Environment Variable Substitution
You can reference environment variables inconfig.yaml using ${VAR_NAME} syntax:
url: "${HOST}:${PORT}". If a referenced variable is not set, the placeholder is kept verbatim (${UNDEFINED_VAR} stays as-is) and a warning is logged. Bare $VAR is not expanded.
Under a multiplexed multi-profile gateway, references in a profile’s config.yaml resolve against that profile’s .env (its secret scope), not the shared process environment — a ${MATRIX_ACCESS_TOKEN} in profile B stays unresolved (kept verbatim, warning logged) unless B defines the variable itself. This holds wherever B’s config is loaded inside the multiplexer: routed gateway turns, B’s adapter startup, and B’s cron jobs. Single-profile runs are unchanged. See What is isolated per profile for the full list.
Cursor-style SecretRef syntax is also accepted: ${env:VAR_NAME} resolves exactly like ${VAR_NAME} (the env: prefix is stripped), so MCP or provider snippets copied from Cursor / Claude configs work unchanged in both config.yaml and the mcp_servers block. Other SecretRef sources (${file:...}, ${vault:...}, ${bitwarden:...}) are not resolved inline — external secret backends inject their values into the environment at startup via the secrets: block, so reference them as ${env:NAME} instead; unknown prefixes warn once and stay verbatim.
For AI provider setup (OpenRouter, Anthropic, Copilot, custom endpoints, self-hosted LLMs, fallback models, etc.), see AI Providers.
Provider Timeouts
You can setproviders.<id>.request_timeout_seconds for a provider-wide request timeout, plus providers.<id>.models.<model>.timeout_seconds for a model-specific override. Applies to the primary turn client on every transport (OpenAI-wire, native Anthropic, Anthropic-compatible), the fallback chain, rebuilds after credential rotation, and (for OpenAI-wire) the per-request timeout kwarg — so the configured value wins over the legacy mibyan_API_TIMEOUT env var.
You can also set providers.<id>.stale_timeout_seconds for the non-streaming stale-call detector, plus providers.<id>.models.<model>.stale_timeout_seconds for a model-specific override. This wins over the legacy mibyan_API_CALL_STALE_TIMEOUT env var. The same key is the streaming stale-stream deadline: an explicit value is used as-is — the implicit context-size tiers (240s above 50k tokens, 300s above 100k) and the reasoning-model floors apply only to the 180s default, so an explicit value can shorten how long a hung stream is tolerated.
Leaving these unset keeps the legacy defaults (mibyan_API_TIMEOUT=1800s, mibyan_API_CALL_STALE_TIMEOUT=90s, native Anthropic 900s). The non-streaming stale detector is auto-disabled for local endpoints when left implicit and can scale upward for very large contexts. Not currently wired for AWS Bedrock (both bedrock_converse and AnthropicBedrock SDK paths use boto3 with its own timeout configuration). See the commented example in cli-config.yaml.example.
Update Behavior
Background checks
Passive update checks (CLI banner, TUI badge, dashboard, desktop app) ask the GitHub REST API for the tip ofmain and, when it differs from your checkout,
the compare endpoint for the exact count and changelog. They never run
git fetch — in a partial (--filter=blob:none) clone they also never
download missing objects from the promisor remote (Git 2.44 or newer) — and
every install asks at most once per 24 hours (a failed check
retries after an hour). Applying an update (mibyan update, or the desktop’s
Update button) always fetches fresh and invalidates the cached answer. Explicit
checks — mibyan update --check, the desktop’s “Check for Updates…” menu item,
Settings → About → “Check now” — bypass the cache.
SSH authentication
The startup update check reads the origin URL with the same isolated Git configuration used for its network calls. Globalurl.*.insteadOf rewrites
therefore cannot hide an official SSH remote from the public HTTPS path.
Mibyan’s isolated internal Git commands default to ssh -o BatchMode=yes:
unknown host keys, passwords, and encrypted keys needing a passphrase fail
instead of opening a terminal prompt. Trusted hosts with usable keys or an
SSH agent continue to authenticate. This does not change your Git or SSH
configuration on disk, or commands you run in the terminal tool.
The internal default overrides repository core.sshCommand settings. An
explicit GIT_SSH_COMMAND environment variable still takes precedence, so
custom identity or transport commands can be retained there. Include
-o BatchMode=yes in such an override if it must remain non-interactive;
an override that permits prompting can still interrupt a background check.
mibyan update settings live under updates in config.yaml:
pre_update_backup is the single pre-update safety knob: quick (default) snapshots critical state files (pairing data, cron jobs, config, auth; files over 1 GiB are skipped) into state-snapshots/; full additionally zips all of mibyan_HOME into backups/ and can add minutes on large homes; off disables both. Legacy booleans are honored (true → full, false → off).
Point-in-time copies of config.yaml itself (taken before mibyan setup rewrites it, before mibyan migrate edits it, every time the file parses successfully, and when it fails to parse) go to backups/config/config.yaml.<reason>.<timestamp>. Identical repeats are skipped and only the newest five per reason are kept, so they never pile up beside config.yaml. If config.yaml is broken, Mibyan serves the newest good copy instead of built-in defaults and warns on every start until the YAML is fixed; the broken file is never modified.
For git installs, Mibyan auto-stashes dirty tracked files and untracked files before checking out the update branch or pulling. Interactive terminal updates prompt before restoring that stash. Non-interactive updates (desktop/chat app, gateway, or --yes) use updates.non_interactive_local_changes: stash restores local source edits after a successful pull, while discard drops the update-created stash after a successful pull. Use discard only on managed installs where local source edits are never meant to persist.
Before that stash step, Mibyan also restores tracked package-lock.json diffs left by npm install/build churn. Commit or manually stash intentional lockfile edits before updating.
Terminal Backend Configuration
Mibyan supports seven terminal backends. Each determines where the agent’s shell commands actually execute — your local machine, a Docker container, a remote server via SSH, a Modal cloud sandbox (direct or via the Nous-managed gateway), a Daytona workspace, a Vercel Sandbox, or a Singularity/Apptainer container.terminal.temp_dir controls where Mibyan puts session temp artifacts on the
local backend — background-process logs/pid/exit files, code-execution
sandboxes, and spilled tool results. When it’s empty (the default), Mibyan
honors an explicit TMPDIR/TMP/TEMP from the environment and otherwise
uses a managed directory on real storage at ~/.mibyan/cache/terminal
instead of /tmp — on many distros (Arch-based setups in particular) /tmp
is a small RAM-backed tmpfs that Mibyan session artifacts can fill under
load. The managed directory is auto-pruned: artifacts idle for 24 hours (no write
anywhere inside them) are swept hourly by gateway housekeeping and once per process
on CLI-only installs. Set temp_dir to an existing absolute path to redirect session
temp anywhere else; user-set paths are never auto-pruned.
Independently of terminal.temp_dir, every Mibyan process (CLI, TUI, gateway, Desktop
backend, cron) and every child it launches gets TMPDIR, TMP and TEMP pointed at
~/.mibyan/cache/scratch (per profile) at startup, so tempfile.mkdtemp(),
mktemp, browser profiles and probe scripts all land on real storage instead of a
RAM-backed system temp dir. The system prompt names this directory as the scratch
directory. Mibyan only sets these when they are not already set — a TMPDIR exported
by you or by the OS (macOS /var/folders, Windows %TEMP%) is left alone. Entries are
pruned at startup (at most once per hour) once they have been idle for 24 hours: an entry
stays as long as anything anywhere inside it was written in the last day, and goes a day after
the last write. Before an idle entry is deleted, Mibyan also stops any process still running
with its working directory inside that entry (or inside a scratch path that no longer exists,
such as a headless browser left behind by a test run) and drops any git worktree
registration that pointed into it. mibyan doctor reports the directory and its size, and
warns about directories over 1 GB elsewhere under cache/ that no pruner covers.
desktop.font_family sets the font for chat and the rest of the Mibyan Desktop interface (the terminal pane has its own key above). Give it one installed family name (for example, OpenDyslexic or Atkinson Hyperlegible) or a CSS font stack; Mibyan keeps the active theme’s own stack behind it so CJK and emoji glyphs still resolve, and an empty value uses the theme’s font. Edit it in Settings → Appearance → Chat Font.
terminal.font_family controls the embedded terminal in Mibyan Desktop. It accepts either one locally installed family name (for example, MesloLGS NF) or a CSS font stack. Mibyan appends its bundled JetBrains Mono stack as a fallback, and an empty value keeps the default. You can edit the same profile-scoped setting in Settings → Appearance → Terminal Font; no Google Fonts download or system-font permission is required.
For cloud sandboxes such as Modal, Daytona, and Vercel Sandbox, container_persistent: true means Mibyan will try to preserve filesystem state across sandbox recreation. It does not promise that the same live sandbox, PID space, or background processes will still be running later.
Backend Overview
Local Backend
The default. Commands run directly on your machine with no isolation. No special setup required.HOME. This lets
external CLIs such as git, ssh, gh, az, npm, Claude Code, and Codex
find the credentials and config they already use in your normal shell. Mibyan
state is still profile-scoped through mibyan_HOME; HOME is not how profiles
select config, memory, sessions, or skills.
Mibyan does not change your system-wide HOME, your shell startup files, or
the operating system account home. This setting only controls the environment
passed to subprocesses that Mibyan launches through tools such as terminal,
background terminal processes, execute_code, and ACP helper processes.
terminal.home_mode
The downside of the default is that host profiles share the same normal
user-level CLI credentials/config under
~. If you need a profile with a
separate git identity, SSH keys, GitHub CLI login, npm config, or cloud CLI
login, use home_mode: profile and initialize those tools inside that profile
home deliberately.
If you intentionally want strict per-profile tool-config isolation, set:
{mibyan_HOME}/home as HOME. Mibyan also
sets mibyan_REAL_HOME so scripts can still locate the actual user home when
they need it. Container backends keep using {mibyan_HOME}/home in auto mode
because that directory lives on the persistent Mibyan data volume.
Scripts that need to distinguish profile state from the real user home should
prefer mibyan_HOME for Mibyan data and mibyan_REAL_HOME for the account home:
Docker Backend
Runs commands inside a Docker container with security hardening (all capabilities dropped, no privilege escalation, PID limits). Single persistent container, shared across Mibyan processes. Mibyan starts ONE long-lived container on first use and routes every terminal, file, andexecute_code call through docker exec into that same container — across sessions, /new, /reset, and delegate_task subagents. Working-directory changes, installed packages, files in /workspace, and background processes all carry over from one tool call to the next, and from one Mibyan process to the next. When you close a TUI session, run /quit, or start a new mibyan invocation, the container keeps running and the next Mibyan process reuses it via a labeled lookup. See Container lifecycle below for the exact teardown rules.
Per-session isolation mode (container_persistent: false). Setting container_persistent: false on the Docker backend switches to one container per session: every chat (desktop app session, gateway conversation, TUI session) gets its own fresh sandbox, created on its first terminal/file call and removed when the session closes or goes idle past lifetime_seconds. Nothing carries over between sessions — no filesystem state, no mounts, no background processes. With docker_mount_cwd_to_workspace: true, only the workspace attached to that session is mounted at /workspace; a fresh session with no attached directory gets an empty workspace instead of inheriting the previous session’s mount. delegate_task subagents still share their parent session’s container. Use this mode when the sandbox is a security boundary between conversations; keep the default true when you want the long-lived shared container described above.
docker_env vs docker_forward_env: the former injects literal KEY=value pairs you specify in the config (the values live in your config.yaml or are passed as a JSON dict via TERMINAL_DOCKER_ENV='{"DEBUG":"1"}'). The latter forwards values from your shell or ~/.mibyan/.env, so the actual secret never appears in the config file. Use docker_forward_env for tokens and docker_env for static knobs the container needs.
terminal.docker_extra_args (also overridable via TERMINAL_DOCKER_EXTRA_ARGS='["--gpus=all"]') lets you pass arbitrary docker run flags that Mibyan doesn’t surface as first-class keys — --gpus, --network, --add-host, alternative --security-opt overrides, etc. Each entry must be a string; the list is appended last to the assembled docker run invocation so it can override Mibyan’ defaults if needed. Use sparingly — flags that conflict with the sandbox hardening (capability drops, --user, the workspace bind mount) will silently weaken isolation.
terminal.docker_network (default true; env: TERMINAL_DOCKER_NETWORK) — set to false to run the sandbox container with --network=none, cutting off all network egress from agent commands. This applies to the execution container used by terminal, execute_code, and the file tools. Because containers persist across Mibyan processes, flipping this to false while an older networked container exists will remove that container and start a fresh air-gapped one (a warning is logged); background processes running inside it are lost. Prefer this key over passing --network=none through docker_extra_args.
Requirements: Docker Desktop or Docker Engine installed and running. Mibyan probes $PATH plus common macOS install locations (/usr/local/bin/docker, /opt/homebrew/bin/docker, Docker Desktop app bundle). Podman is supported out of the box: set mibyan_DOCKER_BINARY=podman (or the full path) to force it when both are installed.
Container lifecycle
Every Mibyan-managed container is tagged with three labels so subsequent processes (and the orphan reaper) can identify it:mibyan-agent=1— marks it as Mibyan-managedmibyan-task-id=<sanitized task_id>— keys the per-task reuse probemibyan-profile=<sanitized profile name>— scopes reuse and reaping to the active Mibyan profile by default; whendocker_shared_container_keyis set, its sanitized value is used instead
docker ps --filter label=mibyan-task-id=<id> --filter label=mibyan-profile=<identity> and attaches to the existing container when it finds one. The identity is the active profile unless docker_shared_container_key explicitly opts trusted profiles into a common value. If the container is exited (e.g. after a Docker daemon restart), it’s docker start’d and reused — filesystem state and any installed packages survive, but in-container background processes do not.
When a Mibyan process exits — /quit, closing a TUI session, gateway shutdown, even SIGKILL — the cleanup path is a no-op for the container in default mode. The container keeps running. The next Mibyan process attaches to it in milliseconds via the label probe. This is the behavior the “one long-lived container shared across sessions” contract requires: it’s the only way background processes (npm watchers, dev servers, long-running pytest) survive across sessions.
The container is only torn down (stopped and docker rm -f’d) in these cases:
Edge cases worth knowing:
- OOM kill of in-container PID 1 transitions the container to
Exited. Next reuse willdocker startit; filesystem state survives, bg processes do not. - Switching profiles isolates containers from each other — a container labeled
mibyan-profile=workis invisible to a Mibyan process running undermibyan-profile=research. The orphan reaper is profile-scoped too, so cross-profile containers don’t get reaped accidentally, but they also won’t get cleaned up automatically until you start Mibyan again under their original profile. - Explicit cross-profile sharing — set the same non-empty
docker_shared_container_keyunderterminal:for profiles that intentionally collaborate in one trusted workspace. This replaces only their container identity label; task, egress, and network compatibility checks still apply. Profiles without the key remain isolated. The identity label is derived from the key with a short digest suffix, so similar-looking keys (team/workspacevsteam_workspace) never collide into one container. Important: a shared container is created once, by whichever profile starts it first — that profile’sdocker_image, volumes, shm size, and other immutable Docker settings win, and later profiles attach to it as-is; differing settings in their configs are ignored until the container is removed and recreated. Profiles sharing a key should agree on image and mounts.
delegate_task(tasks=[...]) share this one container — concurrent cd, env mutations, and writes to the same path will collide. If a subagent needs an isolated sandbox, it must register a per-task image override via register_task_env_overrides(), which RL and benchmark environments (TerminalBench2, MibyanSweEnv, etc.) do automatically for their per-task Docker images.
Security hardening:
--cap-drop ALLwith onlyDAC_OVERRIDE,CHOWN,FOWNERadded back--security-opt no-new-privileges--pids-limit 256- Size-limited tmpfs for
/tmp(512MB),/var/tmp(256MB),/run(64MB)
docker_forward_env are resolved from your shell environment first, then ~/.mibyan/.env. Skills can also declare required_environment_variables which are merged automatically.
Environment variable overrides
Every key underterminal: has an env-var override of the form TERMINAL_<KEY_UPPERCASE>. The most useful ones for the Docker backend:
SSH Backend
Runs commands on a remote server over SSH. Uses ControlMaster for connection reuse (5-minute idle keepalive). Persistent shell is enabled by default — state (cwd, env vars) survives across commands.
How it works: Connects at init time with
BatchMode=yes and StrictHostKeyChecking=accept-new. Persistent shell keeps a single bash -l process alive on the remote host, communicating via temporary files. Commands that need stdin_data or sudo automatically fall back to one-shot mode.
Skill / config env passthrough: variables a skill declares in required_environment_variables, or that you list under terminal.env_passthrough, are forwarded with OpenSSH SendEnv — the names go on the ssh command line and the values travel in the client’s environment, never in the remote command text. The remote sshd must accept them; add to /etc/ssh/sshd_config on the server and reload sshd:
AcceptEnv, the server silently drops the variables and the remote shell sees them unset. Mibyan provider credentials (OPENAI_API_KEY, …) are never forwarded even if listed. See Env Var Passthrough.
Modal Backend
Runs commands in a Modal cloud sandbox. Each task gets an isolated VM with configurable CPU, memory, and disk. Filesystem can be snapshot/restored across sessions.MODAL_TOKEN_ID + MODAL_TOKEN_SECRET environment variables, or a ~/.modal.toml config file.
Persistence: When enabled, the sandbox filesystem is snapshotted on cleanup and restored on next session. Snapshots are tracked in ~/.mibyan/modal_snapshots.json and are retained until you delete them (Mibyan opts out of the Modal SDK’s 30-day snapshot expiry). This preserves filesystem state, not live processes, PID space, or background jobs.
Credential files: Automatically mounted from ~/.mibyan/ (OAuth tokens, etc.) and synced before each command.
Daytona Backend
Runs commands in a Daytona managed workspace. Supports stop/resume for persistence.DAYTONA_API_KEY environment variable.
Persistence: When enabled, sandboxes are stopped (not deleted) on cleanup and resumed on next session. Sandbox names follow the pattern mibyan-{task_id}.
Disk limit: Daytona enforces a 10 GiB maximum. Requests above this are capped with a warning.
Vercel Sandbox Backend
Runs commands in a Vercel Sandbox cloud microVM. Mibyan uses the normal terminal and file tool surfaces; there are no Vercel-specific model-facing tools.VERCEL_TOKEN, VERCEL_PROJECT_ID, and VERCEL_TEAM_ID. This is the supported setup for deployments and normal long-running Mibyan processes on Render, Railway, Docker, and similar hosts.
For one-off local development, Mibyan also accepts short-lived Vercel OIDC tokens:
terminal.vercel_image picks the container image for fresh sandboxes: a Vercel managed image such as vercel/sandbox/universal:latest (the default: Ubuntu, Node.js 24, Python 3.14), vercel/sandbox/node:26 or vercel/sandbox/python:3.14, or a repository from your project’s Vercel Container Registry (a bare name resolves to latest; a tag or digest pins it). The older terminal.vercel_runtime presets (node24, node22, python3.13) are deprecated by Vercel; a pinned runtime still works and overrides the image, but the two cannot be combined. Snapshot restores carry their own filesystem and send neither.
Persistence: When container_persistent: true, Mibyan snapshots the sandbox filesystem during cleanup and restores a later sandbox for the same task from that snapshot. Snapshot contents can include Mibyan-synced credentials, skills, and cache files that were copied into the sandbox. This preserves filesystem state only; it does not preserve live sandbox identity, PID space, shell state, or running background processes.
Background commands: terminal(background=true) uses Mibyan’ generic non-local background process flow. You can spawn, poll, wait, view logs, and kill processes through the normal process tool while the sandbox is alive. Mibyan does not provide native Vercel detached-process recovery after cleanup or restart.
Disk sizing: Vercel Sandbox does not currently support Mibyan’ container_disk resource knob. Leave container_disk unset or at the shared default 51200; non-default values fail diagnostics and backend creation instead of being silently ignored.
Singularity/Apptainer Backend
Runs commands in a Singularity/Apptainer container. Designed for HPC clusters and shared machines where Docker isn’t available.apptainer or singularity binary in $PATH.
Image handling: Docker URLs (docker://...) are automatically converted to SIF files and cached. Existing .sif files are used directly.
Scratch directory: Resolved in order: TERMINAL_SCRATCH_DIR → TERMINAL_SANDBOX_DIR/singularity → /scratch/$USER/mibyan-agent (HPC convention) → ~/.mibyan/sandboxes/singularity.
Isolation: Uses --containall --no-home for full namespace isolation without mounting the host home directory.
Common Terminal Backend Issues
If terminal commands fail immediately or the terminal tool is reported as disabled:- Local — No special requirements. The safest default when getting started.
- Docker — Run
docker versionto verify Docker is working. If it fails, fix Docker ormibyan config set terminal.backend local. - SSH — Both
TERMINAL_SSH_HOSTandTERMINAL_SSH_USERmust be set. Mibyan logs a clear error if either is missing. - Modal — Needs
MODAL_TOKEN_IDenv var or~/.modal.toml. Runmibyan doctorto check. - Daytona — Needs
DAYTONA_API_KEY. The Daytona SDK handles server URL configuration. - Singularity — Needs
apptainerorsingularityin$PATH. Common on HPC clusters.
terminal.backend back to local and verify that commands run there first.
Remote-to-Host State Sync on Teardown
For the SSH, Modal, and Daytona backends, Mibyan pushes your~/.mibyan/ state (credential files, skills, cache) into the remote sandbox during the session, and on teardown syncs changed state files back to their original host locations. Files that differ from what was originally pushed (compared by content hash) are applied back in place; new remote files under a synced directory (e.g. a skill the agent created remotely) are mapped back to the corresponding host path. Upload-only credential files are never overwritten on the host.
- The sync-back retries up to 3 times with backoff and refuses to extract remote archives larger than 2 GiB; set
terminal.sync_back_max_bytes(bytes) inconfig.yamlto raise the cap for a larger state tree. Live sockets under the remote~/.mibyan/(e.g.gateway.sock) are skipped rather than failing the transfer. - The downloaded archive is staged under the system temp directory (
mibyan-sync-back-<pid>-*); leftovers from a hard-killed process are reclaimed on the next sync-back. - Docker and Singularity use bind mounts (live host filesystem view) and don’t need this.
- This covers Mibyan state (
~/.mibyan/), not arbitrary working-tree files inside the sandbox — have the agent copy important artifacts out explicitly (e.g.scp,modal volume put) before the sandbox is destroyed.
Docker Volume Mounts
When using the Docker backend,docker_volumes lets you share host directories with the container. Each entry uses standard Docker -v syntax: host_path:container_path[:options].
- Providing files to the agent (datasets, configs, reference code)
- Receiving files from the agent (generated code, reports, exports)
- Shared workspaces where both you and the agent access the same files
MEDIA:/..., prefer a dedicated host-visible export mount such as
/home/user/.mibyan/cache/documents:/output.
- Write files inside Docker to
/output/... - Emit the host path in
MEDIA:, for example:MEDIA:/home/user/.mibyan/cache/documents/report.txt - Do not emit
/workspace/...or/output/...unless that exact path also exists for the gateway process on the host
TERMINAL_DOCKER_VOLUMES='["/host:/container"]' (JSON array).
Docker Credential Forwarding
By default, Docker terminal sessions do not inherit arbitrary host credentials. If you need a specific token inside the container, add it toterminal.docker_forward_env.
~/.mibyan/.env if it was saved with mibyan config set.
Running the Container as Your Host User
By default Docker containers run asroot (UID 0). Files created inside /workspace or other bind-mounts end up owned by root on the host, so after a session you have to sudo chown them before you can edit them from your host editor. The terminal.docker_run_as_host_user flag fixes this:
--user $(id -u):$(id -g) to the docker run command so files written into bind-mounted directories (/workspace, /root, anything in docker_volumes) are owned by your host user, not root. The trade-off: the container can no longer apt install or write to root-owned paths like /root/.npm — use a base image whose HOME is owned by a non-root user (or add your required tooling at image build time) if you need both.
Leave this false (the default) for backwards-compatible behavior. Turn it on when your workflow is mostly “edit mounted host files” and you’re tired of sudo chown -R.
Snap-packaged Docker (AppArmor)
On hosts where Docker was installed as a snap (common on Ubuntu cloud images, e.g. Azure VMs), the snap’s AppArmor confinement rejects two of the sandbox’s hardening flags and the container dies at start:Optional: Mount the Launch Directory into /workspace
Docker sandboxes stay isolated by default. Mibyan does not pass your current host working directory into the container unless you explicitly opt in.
Enable it in config.yaml:
- if you launch Mibyan from
~/projects/my-app, that host directory is bind-mounted to/workspace - the Docker backend starts in
/workspace - file tools and terminal commands both see the same mounted project
/workspace stays sandbox-owned unless you explicitly mount something via docker_volumes.
Security tradeoff:
falsepreserves the sandbox boundarytruegives the sandbox direct access to the directory you launched Mibyan from
terminal.cwd (for example C:\Users\me\project on Windows, or a desktop/TUI session’s workspace) never becomes the container’s working directory: when it is the directory mounted at /workspace, file tools and terminal commands use /workspace; otherwise the container keeps its own working directory. If a file tool still cannot enter its working directory, the error names the invalid terminal.cwd for the active backend rather than the shell’s raw cd: line.
Persistent Shell
By default, each terminal command runs in its own subprocess — working directory, environment variables, and shell variables reset between commands. When persistent shell is enabled, a single long-lived bash process is kept alive acrossexecute() calls so that state survives between commands.
This is most useful for the SSH backend, where it also eliminates per-command connection overhead. Persistent shell is enabled by default for SSH and disabled for the local backend.
- Working directory (
cd ~/projectsticks for the next command) - Exported environment variables (
export FOO=bar) - Shell variables (
MY_VAR=hello)
Per-backend environment variables take highest precedence. If you want persistent shell on the local backend too:
Commands that require
stdin_data or sudo automatically fall back to one-shot mode, since the persistent shell’s stdin is already occupied by the IPC protocol.Skill Settings
Skills can declare their own configuration settings via their SKILL.md frontmatter. These are non-secret values (paths, preferences, domain settings) stored under theskills.config namespace in config.yaml.
mibyan config migratescans all enabled skills, finds unconfigured settings, and offers to prompt youmibyan config showdisplays all skill settings under “Skill Settings” with the skill they belong to- When a skill loads, its resolved config values are injected into the skill context automatically
Auto-loading skills every session
Pin skills so they are fully loaded at the start of every new session, on every surface:--ignore-rules / mibyan_IGNORE_RULES=1 suppresses the list. Profile-scoped. See CLI — persistent auto-load.
Guard on agent-created skill writes
When the agent usesskill_manage to create, edit, patch, or delete a skill, Mibyan can optionally scan the new/updated content for dangerous keyword patterns (credential harvesting, obvious prompt injection, exfil instructions). The scanner is off by default — real agent workflows that legitimately touch ~/.ssh/ or mention $OPENAI_API_KEY were tripping the heuristic too often. Turn it back on if you want the scanner to prompt you before the agent’s skill writes land:
skill_manage write surfaces as an approval prompt with the scanner’s rationale. Accepted writes land; denied writes return an explanatory error to the agent.
Write approval for skill writes
Independent of the content scanner above,skills.write_approval gates every agent skill write (create / edit / patch / delete / supporting files) behind your explicit approval — the same approve/deny mechanism as dangerous commands:
~/.mibyan/pending/skills/ and reviewed with /skills pending, /skills diff <id>, /skills approve <id>, /skills reject <id> — from the CLI or any messaging platform. Toggle at runtime with /skills approval on|off. Memory has the same gate (memory.write_approval, below). Full walkthrough: Gating agent skill writes.
Memory Configuration
memory.write_approval: true, memory writes need your approval before they land: interactive CLI turns prompt inline; messaging sessions and the background self-improvement review stage the write for /memory pending → /memory approve <id> / /memory reject <id> review. Toggle at runtime with /memory approval on|off. See Controlling memory writes.
Context File Truncation
Controls how much content Mibyan loads from each automatic context file before applying head/tail truncation. This applies to files injected into the system prompt such asSOUL.md, .mibyan.md, AGENTS.md, CLAUDE.md, and .cursorrules. It does not affect the read_file tool.
context_file_read_timeout (seconds, default 5.0). A file that takes longer to read — typically on a network-backed filesystem such as iCloud Drive, OneDrive or NFS — is skipped with a warning so the rest of the system prompt still loads:
File Read Safety
Controls how much content a singleread_file call can return. Reads that exceed the limit are rejected with an error telling the agent to use offset and limit for a smaller range. This prevents a single read of a minified JS bundle or large data file from flooding the context window.
Tool Output Truncation Limits
Three related caps control how much raw output a tool can return before Mibyan truncates it:max_bytes— When aterminalcommand produces more than this many characters of combined stdout/stderr, Mibyan keeps the first 40% and last 60% and inserts a[OUTPUT TRUNCATED]notice between them. Default50000(≈12-15K tokens across typical tokenisers).max_lines— Upper bound on thelimitparameter of a singleread_filecall. Requests above this are clamped so a single read can’t flood the context window. Default2000.max_line_length— Per-line cap applied whenread_fileemits the line-numbered view. Lines longer than this are truncated to this many chars followed by... [truncated]. Default2000.
Tool-Result Spillover Budget
Separately from truncation, oversized tool results are spilled to disk rather than cut: the full output is saved under$mibyan_HOME/cache/spillover/ and the in-context content is replaced by a preview plus the saved file’s path (readable with read_file using offset/limit, or processable with execute_code). The generic per-result spillover threshold is 100,000 chars, scaled down automatically for small-context models.
MCP tool results (tools named mcp_*) spill at a tighter 50,000-char default: MCP servers routinely return large un-paginated payloads (tool-discovery catalogs, batched executions) that would otherwise sit under the generic threshold and bloat context on every subsequent turn. Nothing is lost — the full result is preserved on disk. Override the threshold via:
...N more items, "has_more": true, “saved to sandbox” notes), a one-line notice is appended to the result warning that the visible data is incomplete and should be paged/fetched before treating any enumeration as complete.
Global Toolset Disable
To suppress specific toolsets across the CLI and every gateway platform in one place, list their names underagent.disabled_toolsets:
platform_toolsets written by
mibyan tools), so a toolset listed here is always removed — even if a
platform’s saved config still lists it. Use this when you want a single
switch for “turn X off everywhere” rather than editing 15+ platform rows in
the mibyan tools UI.
Leaving the list empty, or omitting the key, is a no-op.
Git Worktree Isolation
Enable isolated git worktrees for running multiple agents in parallel on the same repo:.worktrees/ with its own branch. Agents can edit files, commit, push, and create PRs without interfering with each other. Clean worktrees are removed on exit; dirty ones are kept for manual recovery.
By default the new worktree branches from the freshly-fetched remote tip (the current branch’s upstream, otherwise the remote’s default branch) so it starts current with the project rather than from the local clone’s possibly-stale HEAD. This keeps a PR’s diff scoped to the actual change instead of inheriting whatever the local clone was behind by. Set worktree_sync: false to branch from local HEAD instead — useful offline, or when you deliberately want the clone’s exact current state as the base. If the remote can’t be reached, it falls back to local HEAD automatically.
.worktreeinclude in your repo root:
Context Compression
Mibyan automatically compresses long conversations to stay within your model’s context window. The compression summarizer is a separate LLM call — you can point it at any provider or endpoint. All compression settings live inconfig.yaml (no environment variables).
Full reference
Legacy config migrationOlder configs with
compression.summary_model, compression.summary_provider, and compression.summary_base_url are automatically migrated to auxiliary.compression.* on first load (config version 17). No manual action needed.progress_notices (default false) controls whether routine compression progress statuses reach chat platforms (Telegram, Discord, Slack, etc.). By design, automatic compression is silent on chat surfaces — it runs in the background with server-side logging only. Set progress_notices: true to opt into seeing the routine lifecycle on chat platforms: the “Compacting context…” start notice, preflight/pre-API compression triggers, idle compaction, retry progress (“Compressed 30 → 12 messages, retrying…”), and the “Context compaction complete” notice. The gate is scoped to compression statuses only — unrelated operational noise (auxiliary model failures, provider rate-limit/retry chatter) stays suppressed either way. Compression failure notices and manual /compress feedback are always visible regardless of this setting. Editing this value on a running gateway takes effect on the next message.
hygiene_hard_message_limit is a gateway-only pre-compression safety valve. It exists to break a death spiral: when API calls keep disconnecting on an oversized session, the gateway never receives token-usage data, so the token-based threshold can’t fire, so the transcript keeps growing and disconnects get worse. This count-based floor fires on message count alone (always known, regardless of API failures) to force compression and recover the session. Default 5000 — far above any normal session, including large-context (1M+) models doing thousands of short turns, which compress on the token threshold long before this. Raise it further for unusual platforms, lower it to force more aggressive compression. Editing this value on a running gateway takes effect on the next message (see below).
The same limit is also a fail-closed bound on what the model is sent whenever hygiene does not land by the time a turn starts — the turn-hold budget expired, the summary timed out or failed, a failure cooldown is active, another compression is still in flight, or compression is disabled. In that case the gateway keeps the leading system/setup rows plus the newest messages, total at most hygiene_hard_message_limit, and never starts the kept tail on an orphaned tool result. Only the payload of that one turn is clipped: the transcript on disk is untouched, nothing is deleted, and a summary that lands later is adopted as-is. This is what keeps a week-long DM from feeding the model the full uncompressed history when a compression pass keeps missing.
hygiene_timeout_seconds is the gateway’s inactivity budget for this pre-agent compression pass — not a total wall-clock cap. The compression summary call streams from the model, and each arriving token counts as forward progress: a slow reasoning model that is still generating keeps extending its own deadline, so slow-but-healthy summary models are never cut off mid-generation. Only when the summary model produces no output for this many seconds (backend down, hung connection, silent provider) does the gateway warn the user, continue the incoming message without compression, and record a temporary per-session failure cooldown instead of appearing stuck.
hygiene_total_ceiling_seconds (default 600) bounds the total wait even while tokens are still moving, so a degenerate trickle stream can’t hold a turn hostage indefinitely. It is clamped to at least hygiene_timeout_seconds.
hygiene_max_turn_hold_seconds (default 10) is the gateway’s turn-hold budget — the maximum wall-clock the incoming message is held waiting on hygiene compression before the gateway stops waiting and proceeds on the uncompressed transcript. It exists because hygiene_total_ceiling_seconds alone can leave the wire silent for far longer than a chat transport’s idle-timeout: a summary model that keeps streaming tokens keeps resetting the inactivity slice, so without a turn-hold budget the wait can stretch toward the ceiling while zero bytes reach the user — Telegram (and similar transports) then drop the connection and the turn appears frozen. Capping the turn’s wait at this budget (well under the typical ~30s transport idle-timeout) guarantees the message is answered promptly. The compression is not lost when the budget expires: the worker keeps running detached and — when its commit is watermark-fenced (the normal case with a session DB) — it keeps its commit admission, so the finished summary is adopted at the next safe boundary and turns appended after the wait was abandoned survive verbatim as concurrent tail. This matters especially for thinking/reasoning summary models (DeepSeek, QwQ, etc.) whose reasoning phase alone can exceed the budget: their summaries land one turn late instead of never. If the commit cannot be safely fenced, the late result is discarded (CompressionCommitFence) and it cannot overwrite newer turns. Raise the budget if you’d rather have compression apply within the same turn and your transport tolerates the wait; lower it for snappier recovery on very slow backends.
hygiene_failure_cooldown_seconds controls that per-session cooldown after a hygiene compression timeout or abort. During the cooldown, the gateway skips repeated hygiene attempts for the same oversized session so every incoming message does not block on the same broken auxiliary backend. /compress, /reset, or a healthy later turn can still recover the session.
The value is the first rung of an escalating ladder, not a fixed interval: consecutive failures for the same session wait 1x, 3x, then 9x this value, capped at one hour. A session whose summary model is permanently broken therefore backs off instead of retrying forever on a fixed interval, and a run that actually shrinks the transcript resets it to the first rung. Escalation is per-session and process-local — a gateway restart resets it to the first rung while the cooldown deadline itself survives.
context_timeout_seconds (default 120) is the same inactivity budget for in-agent compress_context — the conversation loop, preflight compaction, and manual /compress — so a hung summary model cannot stall a session indefinitely. Streamed summary tokens extend the wait; only a silent worker is cut off. The budget is floored at the auxiliary compression request’s own timeout (auxiliary.compression.timeout, minimum 300s), so the host never gives up on a silent summariser before the request itself would — a reasoning summariser thinking before its first token, or a route that cannot stream, gets the same budget the provider call has. On timeout Mibyan retries the summary once against the first entry of auxiliary.compression.fallback_chain (using that entry’s own timeout when it declares one) — a stalled route never raises, so the auxiliary client’s own fallback handling cannot see it. If that attempt also fails, or no fallback chain is configured, what happens next depends on whether the request still fits the model’s context window: a request that fits is sent uncompressed this turn (the summary-failure cooldown stops the retry from repeating every turn); a request above the window cannot be sent at all, so Mibyan commits its deterministic fallback summary (old tool results pruned, a static handoff in place of the summarised middle) instead of ending the turn — ending the turn with the “compression timed out” recovery result (and, on the messaging gateway, the automatic session reset) is the last resort, reached only when even the deterministic pass cannot shrink the transcript. Set to 0 to disable. A preflight pass that reclaims nothing on a request still above the window ends the turn immediately with guidance to start a new session (/new) instead of sending a request the model cannot accept. Gateway session hygiene keeps its own hygiene_timeout_seconds path and is not double-wrapped.
context_total_ceiling_seconds (default 600) bounds the in-agent pre-commit wait (summary / stream phase) even while tokens are still moving. It is clamped to at least context_timeout_seconds. For a request already above the model’s context window the pre-commit wait is bounded by one context_timeout_seconds budget instead of this ceiling: such a request cannot be sent uncompressed anyway, and a summary that keeps streaming while reclaiming nothing would otherwise hold the session (and the Desktop UI) for the full ceiling on every turn — the deterministic fallback summary then carries the compaction. Raise context_timeout_seconds if your summariser legitimately needs longer. The exact guarantee: the summary phase is bounded by this ceiling; the commit phase is logged and surfaced if it exceeds it. Once the worker has entered the compression commit fence and SessionDB mutation is in flight, the commit is never abandoned mid-flight — that would risk transcript divergence — but the wait is no longer silent: if the commit runs past the ceiling, Mibyan logs the overrun (WARNING, escalating to ERROR on repeat), sends a one-shot warning through the user-visible warning channel, and keeps waiting in bounded increments until the commit completes. When the ceiling expires during the summary phase, the summary model’s stream is closed at that same instant on every auxiliary wire (chat.completions, Codex Responses, Anthropic Messages) — an abandoned summary is not billed to completion on a connection nobody is waiting for, and its session lease is freed for the next attempt.
protect_first_n controls how many non-system head messages are pinned across every compaction. Default 3 — the opening user/assistant exchange survives every summarizer pass so the original goal stays visible. On long-running rolling-compaction sessions where the opening turn is no longer relevant, set protect_first_n: 0 to pin nothing but the system prompt + summary + tail. The system prompt itself is always preserved regardless of this setting.
in_place (default true) controls what happens to the session identity when compaction fires. When true, compaction rewrites the message list and rebuilds the system prompt without rotating the session id — the conversation keeps one durable id for its whole life (no parent_session_id chain, no name #2 / #3 renumbering in session lists). Compaction is non-destructive: the live context is compacted, but the pre-compaction turns are soft-archived under the same id (marked inactive/compacted) — still searchable via session_search and recoverable, not deleted. Hooks see the mode via the in_place field on the session:compress event. Set in_place: false to restore the legacy behavior where each compaction rotates to a new session id linked to the old one.
threshold_tokens sets an optional absolute token cap for the compression trigger. When set, compression fires at the lower of the ratio-based threshold and this absolute count, so compaction never fires later than that token count regardless of which model is active. Use it when you want a fixed cost ceiling per call, for example threshold_tokens: 256000 to compact a 1M-window model at 256K instead of 500K. The cap is clamped to the model’s context length, so a value above the window is a no-op. Default null (disabled — ratio-based threshold only). The cap survives model switches and fallback activations.
idle_compact_after_seconds is an opt-in, time-based trigger that complements the size-based threshold. Default 0 (disabled). When set above 0, a session that resumes after at least that many seconds of inactivity compacts its accumulated history up front, before the first reply — so a long-lived thread (e.g. a Telegram conversation you come back to hours later) doesn’t re-read its full stale context on every subsequent turn. It never fires when the context is already at or below the post-compression target (threshold × target_ratio), and it honors the same failure-cooldown, anti-thrash, and per-session lock guards as every automatic compaction. Example: idle_compact_after_seconds: 1800 compacts after 30 minutes idle.
proactive_prune_tokens enables a deterministic, no-LLM prune of old tool-result payloads that runs independently of threshold. On large-window models the threshold compaction (≈50% of the window) rarely fires, so bulky tool outputs (terminal dumps, file reads, web extracts) ride along in history and get re-sent on every subsequent turn. When re-sent history exceeds proactive_prune_tokens (default 0 = off; try 48000 to enable), the prune dedupes identical results and summarizes older oversized tool results, protecting the most recent protect_last_n messages and never calling the model. Tool-call arguments are execution records and are never rewritten by pruning; the summary model has a separate bounded serializer for copies included in its prompt. The pressure pass likewise demotes tool-result bodies only. During full semantic compaction the carried head/tail rows keep those tool-result demotions (so an oversized tail can still compress), while tool-call arguments stay byte-exact. The opt-in proactive prune still commits eligible tool-result-body demotions, so proactive_prune_min_reclaim_tokens (default 4096) keeps those cache-breaking commits episodic; proactive_prune_min_result_chars (default 8000, clamped to ≥ 200) sets the size below which a tool result is left untouched. This runs only under the built-in compressor engine; other context engines inherit a no-op.
Common setups
Default (auto-detect) — no configuration needed:auxiliary.compression.provider: openrouter + model: google/gemini-2.5-flash) if you want compression on a cheaper model than your main chat model.
Force a specific provider (OAuth or API-key based):
nous, openrouter, codex, anthropic, main, etc.
Custom endpoint (self-hosted, Ollama, zai, DeepSeek, etc.):
OPENAI_API_KEY for auth.
How the three knobs interact
Stream progress timeout (Responses routes)
When the summary runs over a Responses stream (theopenai-codex provider, or any route the auxiliary client drives through the Responses API), two timeouts apply, and they are independent:
auxiliary.compression.timeout— the overall request budget (default 120s).auxiliary.compression.no_progress_timeout— how long the stream may go without a substantive event (a text/reasoning delta or a completed output item) before the attempt aborts withCodex auxiliary Responses stream stalled: no new output for Ns. Default 60s when unset. Keepalive and lifecycle frames (response.in_progress, pings) do not count as progress; every substantive event re-arms the window, so a slow but progressing summary is never cut off by it.
timeout alone does not widen the progress window — a request configured for 600s still aborts after a 60s gap. Set no_progress_timeout to change that gap; the effective window is capped at timeout, and the host’s hard deadline / cancellation still win. The host’s own inactivity budget is the outer cap here: in-agent compaction gives up on a silent summariser after compression.context_timeout_seconds (default 120s, floored at the effective auxiliary.compression.timeout, itself at least 300s) and gateway hygiene after compression.hygiene_timeout_seconds (default 30s), so a no_progress_timeout larger than the applicable host budget is silently cut short by it. The key is per task (auxiliary.<task>.no_progress_timeout), so widening it for compression does not change other auxiliary tasks. A value that is not a positive number is ignored with a warning in the log and the 60s default applies.
Gateway Turn Lease Timeout
The gateway serializes turns by their resolved session ID so two routing keys cannot load and write the same transcript concurrently. Configure the maximum lease wait independently of the ordinary agent inactivity timeout:Session Stall Watchdog
The gateway runs a notify-only stall watchdog (agent.session_stall_timeout, default 300 seconds, 0 = disabled). When a busy session has a pending inbound follow-up and the agent’s shared activity clock has been idle for at least this long, the gateway logs a WARNING and sends the user a one-shot notification:
- Notify-only. The watchdog never kills the turn — contrast
agent.gateway_timeout, which cancels a run after prolonged inactivity. The stall notice just tells you the agent looks wedged so you can decide (/new,/stop, or keep waiting). - One notification per stall episode. The latch clears when the pending inbound drains or activity resumes, so a session that recovers and stalls again notifies again.
- Progress comes only from the shared activity snapshot (tool calls, API stream progress, compression heartbeats). Pending inbound is a notify gate, not a progress clock.
Reconnect Attention Escalation
When a platform adapter fails to connect (network outage, revoked bot token, broken sidecar), the gateway retries it indefinitely with capped exponential backoff — retries never stop, so a transient outage always self-heals without operator action. The downside is that a permanent failure (a revoked Telegram token, missing Discord privileged intents) looks identical to a blip: “retrying”, forever. Two mechanisms make permanent failures visible:- Terminal classification. Failures whose exception type proves they can never self-heal — rejected/revoked tokens (
telegram_auth_error,discord_auth_error,email_auth_error), missing privileged intents (discord_intents_required), a Photon sidecar whose dependencies cannot install (SIDECAR_DEPS_MISSING) or whose node binary is missing (SIDECAR_NODE_MISSING) — are marked fatal instead of entering the retry queue. Classification is strictly type-based; ambiguous errors always keep retrying. - Needs-attention escalation. A platform continuously in the retry queue past
agent.reconnect_attention_after(default7200seconds = 2 hours,0disables) getsneeds_attention: trueand aretrying_sincetimestamp in gateway runtime status (mibyan status), plus a WARNING log. Retries continue unchanged — this is a signal, not a circuit breaker. The flag clears on successful reconnect.
Gateway Agent Cache
The gateway keeps one agent per session so a conversation reuses its cached prompt prefix instead of rebuilding the system prompt every turn. That cached agent also holds the session’s full transcript — tool output included, which is tens of megabytes on a session with a hundred tool calls. On a busy multi-platform gateway the cache is therefore the largest single consumer of memory in the process.max_size and idle_ttl_secs bound the cache by count and by time. Neither knows how many bytes it holds, so memory_high_mb adds a third bound: once anonymous memory crosses the budget, it sheds least-recently-used transcripts, which reload from the stored session on the next turn. Lower it if the gateway is competing for memory with other services; raise it (or set 0 to switch the pass off) if you would rather keep every prefix warm.
auto derives the budget from the memory limit the gateway actually runs under — the cgroup limit for a container or systemd unit, total RAM otherwise — so a MemoryMax/MemoryHigh on the unit is respected without a second number to keep in sync. Under such a limit the measurement is scoped the same way: the cgroup’s own anonymous charge (memory.stat anon), which includes child processes such as execute_code kernels and terminal commands that count against the unit’s limit. Uncapped, the gateway’s own anonymous RSS is measured.
Sessions that are mid-turn, the protect_recent most recently used ones, and any session whose transcript has not finished being written to disk are never shed. Eviction is logged at WARNING with the measured RSS and the sessions dropped:
Context Engine
The context engine controls how conversations are managed when approaching the model’s token limit. The built-incompressor engine uses lossy summarization (see Context Compression). Plugin engines can replace it with alternative strategies.
context.engine to the plugin name. Available engines can be browsed and selected via mibyan plugins → Provider Plugins → Context Engine.
See Memory Providers for the analogous single-select system for memory plugins.
Iteration Budget
When the agent is working on a complex task with many tool calls, it can burn through its iteration budget (default: 500 turns). Mibyan does not inject mid-task pressure warnings — earlier builds warned the model at 70%/90% budget, which caused models to abandon complex tasks prematurely and was removed in April 2026. Instead, when the budget is actually exhausted (500/500), Mibyan injects one message asking the model to wrap up and allows a single grace call so it can deliver a final response. If that grace call still doesn’t produce text, the agent is asked to summarise what it accomplished.agent.max_turns is unlimited by default — the turn cap caused more problems than it solved (silent mid-task truncation), so out of the box Mibyan runs a conversation turn to completion. To impose a cap, set a positive integer. To be explicit about “no limit”, any of these case-insensitive spellings work: "none", "null", "unlimited", "infinite", "infinity", "inf", 0, -1 (they resolve to a sys.maxsize sentinel so the loop never exits on a turn count).
agent.budget_warning_ratio is off by default for ordinary and delegated conversations. When set to a value strictly between 0 and 1 alongside a finite max_turns, Mibyan appends one model-visible checkpoint notice to the latest tool result after the threshold is reached. The notice rearms each conversation turn and uses each agent’s own iteration budget. It only appends to a current tool-result tail, never an older turn, and does not add a synthetic user/system message or change the existing exhaustion grace call. Dispatcher-owned Kanban workers receive a completion checkpoint at 90% by default (an explicit ratio changes that threshold), while their tools are still available. The checkpoint asks for verified completion or a durable progress comment, not premature success.
agent.api_max_retries controls how many times Mibyan retries a provider API call on transient errors (rate limits, connection drops, 5xx) before fallback-provider switching engages. The default is 3 — four attempts total. If you have fallback providers configured and want to fail over faster, drop this to 0 so the first transient error on your primary immediately hands off to the fallback instead of churning retries against the flaky endpoint.
agent.auto_recovery_cycles is the safety net after both the retries and the fallback chain are spent. When the failure is a transient outage (HTTP 5xx, an overloaded/529 response, a connect or read timeout) and no answer text has reached you yet, Mibyan does not end the turn with “API failed after N retries” — it waits and tries again, up to this many cycles (default 5), with a jittered 15/30/60/60/60 s schedule. A provider Retry-After header wins over the schedule (honoured up to 120 s). Every surface shows the same line while it waits — ⏳ Provider temporarily unavailable — retrying automatically in 30s (cycle 2/5); press Esc to stop on the CLI/TUI/Desktop, a status bubble on messaging platforms (send /stop to cancel), a mibyan.status SSE event on the API server, and a log line for cron jobs. Pressing Esc (or /stop) cancels the wait immediately. Fallback still comes first: with a fallback chain configured, exhaustion moves to the next provider as before, and the ladder only engages once the chain has nothing left. Authentication, billing, request-format, entitlement, content-policy and account-policy errors never enter the ladder. Set 0 to disable it.
Wall-Clock Run Budget
Separate from the iteration budget, you can give each conversation run an optional wall-clock budget. This is designed for one-shot and eval-harness invocations that run under a hard external ceiling (e.g. a 900-second per-task limit): without it, a run can time out with the work essentially done — one generation short of emitting the final answer, or stuck in a single hung provider call.- Wrap-up notice at 80%. When 80% of the budget has elapsed, Mibyan injects a one-time notice (delivered cache-safely, appended to the newest tool result like
/steermessages) telling the model to stop new discovery/verification work and produce the final deliverable from the state it already has. It fires at most once per run and mirrors the existing iteration-budget wrap-up mechanism — there are no repeated pressure warnings. - Deadline-scaled stale timeouts. Implicit non-streaming stale timeouts (the 90s default and the reasoning-model floors, e.g. 600s for DeepSeek reasoning models) are capped at
max(60, remaining_budget × 0.5)so a single silently-hung provider call can never consume the rest of the run. The cap only ever tightens the timeout — it never raises it — and an explicitly configuredstale_timeout_seconds(provider/model config ormibyan_API_CALL_STALE_TIMEOUT) always wins untouched.
run_conversation turn (it resets on each user message) and the feature is completely dormant when unset — no clock reads, no injection, no timeout changes.
Verify-on-Stop (coding verification)
When enabled, Mibyan refuses to accept a final answer on a turn where the agent edited code in a workspace but produced no fresh verification evidence (a passing test run, build, lint, etc.) — it injects a synthetic follow-up asking the agent to verify or explain why it can’t. Doc/markdown/skill-only edits never trigger it, and the loop is bounded so it can never trap the agent.verify_on_stop accepts true (on everywhere), false (off — the default), or "auto" (legacy surface-aware behavior: on for interactive coding surfaces — CLI, TUI, desktop — and programmatic callers; off for messaging surfaces like Telegram/Discord where the verification narrative reads as chat noise). Off is the default everywhere: fresh installs ship false and the config migration turned it off on existing installs, so enabling it is an explicit opt-in. The mibyan_VERIFY_ON_STOP env var overrides the config value when set.
The evidence that feeds this guard (which test/lint/build commands ran, which files were edited since) lives in ~/.mibyan/verification_evidence.db. That ledger is only written or created while the guard is enabled; with verify_on_stop: false nothing is recorded and an existing file can be deleted freely.
For a user/plugin policy gate at the same point — keep the agent going with your own checks — see the pre_verify hook.
Standing Goals (/goal)
When a standing goal is active, Mibyan judges whether each assistant response satisfies it. If not, it feeds a continuation prompt back into the same session and keeps working until the goal is done, the turn budget is exhausted, or the user pauses/clears it. The turn budget is the real backstop — judge failures fail open (continue) so a flaky judge never wedges progress.
max_turns caps how many continuation turns a goal can drive before Mibyan auto-pauses it and asks the user to /goal resume. It protects against judge false negatives (goal actually done but judge says continue) and unbounded model spend on fuzzy or unachievable goals. See Goals for the full feature.
API Timeouts
Mibyan has separate timeout layers for streaming, plus a stale detector for non-streaming calls. The stale detectors auto-adjust for local providers only when you leave them at their implicit defaults.
The socket read timeout controls how long httpx waits for the next chunk of data from the provider. Local LLMs can take minutes for prefill on large contexts before producing the first token, so Mibyan raises this to 30 minutes when it detects a local endpoint. If you explicitly set
mibyan_STREAM_READ_TIMEOUT, that value is always used regardless of endpoint detection.
The stale stream detection kills connections that receive SSE keep-alive pings but no actual content. For local providers (which don’t send keep-alive pings during prefill) the default is raised to a finite 900-second ceiling instead of the 180s base — configurable via agent.local_stream_stale_timeout or the mibyan_LOCAL_STREAM_STALE_TIMEOUT env var.
The Responses first-event watchdog (Codex / codex_responses transport, including custom providers declared with the Responses transport) aborts and reconnects a request that accepts the connection but emits no stream event within 120 seconds. A local server prefilling a large context legitimately stays silent longer than that, so on local endpoints the implicit default is raised to the same ceiling as the stale stream detector (agent.local_stream_stale_timeout / mibyan_LOCAL_STREAM_STALE_TIMEOUT, 900s). An explicit mibyan_CODEX_TTFB_TIMEOUT_SECONDS is always used as-is (0 disables the watchdog).
The stale non-stream detection kills non-streaming calls that produce no response for too long. By default Mibyan disables this on local endpoints to avoid false positives during long prefills. If you explicitly set providers.<id>.stale_timeout_seconds, providers.<id>.models.<model>.stale_timeout_seconds, or mibyan_API_CALL_STALE_TIMEOUT, that explicit value is honored even on local endpoints.
The post-terminal stream drain bounds how long a Codex/Responses stream keeps reading after its terminal response.completed frame (a courtesy so the relay finalizer can run). Some relays never close the SSE socket after the terminal frame; without a bound the turn wedged until the stale-stream watchdog fired and discarded the already-billed response, then retried. After agent.stream_drain_timeout seconds the stream is closed and the completed response is returned. Endpoints that close the connection normally finish the drain immediately and never wait this long; set 0 to skip the drain entirely.
This budget bounds every non-streaming call. A provider that accepts a request and then goes silent — connection held open, no bytes, no error — is aborted at the stale timeout and retried, rather than hanging until the much longer socket read timeout (or, for an unattended cron run, until something external kills the process).
The periodic provider-wait notice appears only after at least 60 seconds of silence. The Codex Responses waiting status describes silence, not total generation time: active stream events (including reasoning) keep it quiet. If events stop, it reports time without stream events instead of claiming no response has arrived; the notice clears when events resume. When a reconnect starts a fresh first-event watchdog phase, the waiting status follows that phase. This display behavior does not extend the separate wall-clock stale-call budget or change watchdog timeouts. The status is shown once per silence (after 60s), in neutral wording that names the wait phase (waiting for the first provider event vs provider stream active; Ns without stream events) and the watchdog that would reconnect (TTFB, stream idle, or wall-clock stale) with the seconds left before it fires; it is rewritten only when the phase changes or that deadline is near, not on every 30s liveness heartbeat. Chat-completion streams follow the same rule (waiting for the first stream chunk / stream open; Ns without stream output, stream stale watchdog) and likewise clear their silence notice promptly when chunks resume, without replacing a local model-loading status.
Cron jobs and delegated subagents stream too. They run the request inline on their own thread (the interrupt worker other sessions use wedges inside the gateway’s nested thread pools), but the wire request is still stream: true, so the stale stream detection budget above governs them — every token counts as liveness, so a reasoning model that thinks for minutes is not mistaken for a hung provider, and edge proxies that kill silent connections keep seeing bytes.
Disabling API streaming
model.streaming: false forces non-streaming requests for the whole session — parent and subagents alike. It is an escape hatch for self-hosted OpenAI-compatible servers whose streaming tool-call path is broken (for example vLLM with --tool-call-parser qwen3_xml plus a reasoning parser can leak tool-call markup into plain text and return zero tool_calls, so delegated tasks silently no-op). Default is true; leave it unless you hit that class of bug, since non-streaming calls lose the liveness properties described above. This is separate from display.streaming, which only controls token rendering in the terminal.
Mibyan also switches a session to non-streaming on its own when streaming cannot make progress: the provider reports that streaming is not supported, or an OpenAI-compatible gateway answers a streaming request with a contentless SSE frame (a bare data: / event: ping keepalive with no payload, typical of a degraded relay). The turn is retried without streaming, a warning is shown, and streaming stays off for the rest of that session.
Context Pressure Warnings
Separate from iteration budget pressure, context pressure tracks how close the conversation is to the compaction threshold — the point where context compression fires to summarize older messages. This helps both you and the agent understand when the conversation is getting long.
In the CLI, context pressure appears as a progress bar in the tool output feed:
Credential Pool Strategies
When you have multiple API keys or OAuth tokens for the same provider, configure the rotation strategy:fill_first (default), round_robin, least_used, random. See Credential Pools for full documentation.
Prompt caching
Mibyan turns on cross-session prompt caching automatically when the active provider supports it — no user config needed. For Claude on native Anthropic, OpenRouter, and Nous Portal, Mibyan attachescache_control breakpoints with the 1-hour TTL (ttl: "1h") on the system prompt and skill blocks. The first send within a fresh hour pays full input rates; subsequent sends across any session within the same hour pull from the cache at the discounted cached-read rate. This means the system prompt, loaded skill content, and the early portion of any long-context include get reused across mibyan sessions and across forked subagents for the first hour.
The Qwen Cloud (Alibaba DashScope) upstream caps cache TTL at 5 minutes, so Mibyan uses the 5-minute breakpoint TTL there instead. Other Claude-via-third-party paths (AWS Bedrock, Azure Foundry) fall back to the provider’s own caching defaults. xAI Grok uses a separate session-pinned conversation-id mechanism — see xAI prompt caching.
No knob exists to disable this — caching is always-on and saves money even on single-turn conversations because the system prompt alone is a meaningful fraction of the input token count.
The one explicit knob is the cache TTL tier Mibyan requests on Anthropic-style breakpoints:
cache_ttl selects the breakpoint TTL Mibyan attaches for Claude via the native Anthropic API, OpenRouter, and Nous Portal. The two Anthropic tiers ("5m", "1h") are sent as-is; any other value is ignored. Providers with their own caps (e.g. Qwen Cloud, which maxes at 5 minutes) still clamp to what the upstream allows.
The 1h tier writes at 2x the base input price (5m writes at 1.25x) and only pays off when your turns are more than five minutes apart — otherwise every tool result is written at the dearer rate for retention nobody uses. "auto" picks the tier per session from who paces it: 1h for sessions a person types into (CLI, TUI, Desktop, Telegram/Discord/Slack and the other messaging platforms), 5m for machine-paced ones (subagents, cron, mibyan -q one-shots, webhooks, Kanban workers, the API server, tool-invoked and batch runs). On an install where interactive sessions are parked and resumed through the day, auto cut the interactive cache-write bill by roughly 40% while leaving fan-out subagent spend untouched. Delegated subagents are always clamped to 5m, whatever the setting.
Auxiliary Models
Mibyan uses “auxiliary” models for side tasks like image analysis, browser screenshot analysis, session-title generation, and context compression. By default (auxiliary.*.provider: "auto"), Mibyan routes every auxiliary task to your main chat model — the same provider/model you picked in mibyan model. You don’t need to configure anything to get started, but be aware that on expensive reasoning models (Opus, MiniMax M2.7, etc.) auxiliary tasks add meaningful cost. If you want cheap-and-fast side tasks regardless of your main model, set auxiliary.<task>.provider and auxiliary.<task>.model explicitly (for example, Gemini Flash on OpenRouter for vision). (Web extraction is not an auxiliary task: web_extract and browser snapshots truncate long content deterministically and store the full text for read_file paging — no LLM involved.)
Why “auto” uses your main modelEarlier builds split aggregator users (OpenRouter, Nous Portal) onto a cheap provider-side default. That was surprising — users who paid for an aggregator subscription would see a different model handling their auxiliary traffic.
auto now uses the main model for everyone, and per-task overrides in config.yaml still win (see Full auxiliary config reference below).Configuring auxiliary models interactively
Instead of hand-editing YAML, runmibyan model and pick “Configure auxiliary models” from the menu. You’ll get an interactive per-task picker:
auxiliary.<task>.* in config.yaml. Same machinery as the main-model picker — no extra syntax to learn.
The Delegation entry is special: it routes the model used by delegate_task subagents and persists to the top-level delegation.* section (delegation.provider / delegation.model) rather than auxiliary.*, because subagents are full child agents, not side-LLM calls. Its auto means “inherit the parent agent’s provider, model, and credentials.”
If you do not want Mibyan to auto-generate titles after the first exchange, set
auxiliary.title_generation.enabled: false. Manual titles still work through
/title and mibyan sessions rename.
To keep the instant derived title (the first line of your opening message) but never
spend a model call upgrading it, set auxiliary.title_generation.model_upgrade_enabled: false.
No background auto-title thread starts and no automatic title-model request is sent; the
explicit repair command mibyan sessions retitle-skills still calls the model. enabled: false
still disables both stages.
On a custom main provider (llama.cpp, Ollama, vLLM, LM Studio and other self-hosted
OpenAI-compatible servers) the title model call is sent after the turn’s reply has
arrived, not concurrently with it, unless auxiliary.title_generation is pinned to another
provider or base_url. A single-slot local server that receives the json_schema title
request while decoding the reply can otherwise answer the reply with {"title": ...},
which is then stored and replayed as the assistant’s turn.
In Mibyan Desktop, a plain-text paste over 3,000 characters becomes a generated .txt
attachment. The first ~1,000 characters of that paste are handed to the title stages as a
title-only hint (the agent turn still sees only the attachment reference), so a “summarize
this” plus a large paste is named after the pasted topic. Files you attach yourself are never
read for titling.
In the local messaging gateway, text messages supply their original request to
session titling, before channel-bound skills and platform context are added.
The main model and conversation history still retain the full skill content.
Attachment-only turns retain the existing enriched-message title fallback.
This affects new title generation; it does not repair previously named sessions.
Stream-only endpoints
Some OpenAI-compatible endpoints reject non-streaming chat requests outright (e.g. Tencent Copilot returns HTTP 400"Non-stream chat request is currently not supported"). Interactive chat already streams, but auxiliary tasks (title generation, compression, vision) use non-streaming calls and would fail on every attempt. Mibyan always treats copilot.tencent.com as stream-only; for any other such endpoint, list a URL substring under auxiliary.stream_only_base_urls:
stream=True and the chunks (including tool-call deltas) are aggregated client-side — no behavior change for any other endpoint.
The universal config pattern
Every model slot in Mibyan — auxiliary tasks, compression, fallback — uses the same three knobs:
Auxiliary task blocks additionally accept a
reasoning_effort knob:
This is the per-task counterpart of the global
agent.reasoning_effort: run compression at low or vision at none to cut side-task latency and cost when your main model is an expensive reasoning model, without touching your main chat behavior. It applies to auxiliary-client tasks such as vision, compression, title_generation, and curator, across all three auxiliary wire formats (chat completions, Codex Responses, Anthropic Messages). An explicit extra_body.reasoning on the same task wins over the shorthand. A caller that turns thinking off for its own call (title generation does — a 64-token title has no room for reasoning) wins over both: the task-level effort is dropped for that request instead of being sent beside the provider’s thinking-off field.
If the endpoint rejects the reasoning field outright (a chat-only model behind an OpenAI-compatible relay answering 400 Unrecognized request argument supplied: reasoning_effort, or the reversed wording 400 reasoning_effort 'none' unsupported; use minimal|low|medium|high|xhigh), the auxiliary call is retried once with every reasoning field omitted, so the task (for example the session title) still completes with the endpoint’s default behaviour. The main conversation applies the same recovery: when a route rejects the reasoning-off request Mibyan sends for a thinking-only truncated continuation, the disable is dropped for the rest of the session and the request is retried with the route’s default.
Some models cannot turn thinking off at all (400 Reasoning is mandatory for this endpoint and cannot be disabled). For those, a thinking-off auxiliary call (title generation, or any task set to reasoning_effort: none) goes out at the lowest effort (low) instead of the disable. Mibyan knows ahead of time when the route’s model catalog marks the model mandatory (OpenRouter and Nous Portal /v1/models, cached in cache/reasoning_caps.json), or when the route already answered an earlier disable that way in the same process. So the rejected request is not sent. A fresh install with no cached catalog can still see that 400 once: the lookup fetches the catalog in the background and later calls use it.
Background review is different: a same-model review fork always inherits the parent’s reasoning effort. auxiliary.background_review.reasoning_effort is ignored on that path, including when the parent provider/model is explicitly selected. This preserves byte-identical reasoning settings, system prompt, full conversation snapshot, and tool definitions for prompt-cache parity; there is no independent-effort switch for same-model reviews. See background review reasoning. When the review is routed to a different provider/model, reasoning_effort applies to that routed fork (unset = the routed provider’s default). Mibyan prints a one-time warning when the key is set but the review runs on the main model.
MoA also uses a different configuration: reasoning depth for Mixture-of-Agents is configured per slot in the MoA preset (moa.presets.<name>.reference_models[].reasoning_effort / aggregator.reasoning_effort), not on the moa_reference/moa_aggregator auxiliary blocks — see Mixture of Agents.
base_url is set, Mibyan ignores the provider and calls that endpoint directly (using api_key or OPENAI_API_KEY for auth). When only provider is set, Mibyan uses that provider’s built-in auth and base URL.
Available providers for auxiliary tasks: auto, main, plus any provider in the provider registry — openrouter, nous, openai-codex, copilot, copilot-acp, anthropic, gemini, qwen-oauth, zai, kimi-coding, kimi-coding-cn, minimax, minimax-cn, minimax-oauth, deepseek, nvidia, xai, xai-oauth, ollama-cloud, alibaba, bedrock, huggingface, arcee, xiaomi, kilocode, opencode-zen, opencode-go, commandcode, commandcode-anthropic, ai-gateway, azure-foundry — or any named custom provider from your providers: dict (e.g. provider: "beans").
Local OpenAI-compatible servers work under their own names too: provider: ollama (also vllm, llamacpp, llama.cpp) with a base_url such as http://127.0.0.1:11434 and an empty api_key routes through the custom endpoint with a placeholder key, and a bare host:port base_url gets the /v1 suffix automatically.
provider: openai is a direct-API alias: it routes through the custom endpoint at the block’s base_url, else OPENAI_BASE_URL, else https://api.openai.com/v1, authenticated with api_key or OPENAI_API_KEY. Every auxiliary task resolves it the same way — compression/vision/title_generation as well as background_review, curator and MoA slots — so removing base_url while keeping provider: openai moves that task to the public OpenAI endpoint. A providers.openai entry in your providers: dict takes precedence and keeps its own endpoint and key.
When a routed auxiliary.<task> block cannot be resolved (unknown provider, missing endpoint or credentials), the task runs on the main model and Mibyan says so: background_review emits a one-time user-visible warning naming the provider and reason (plus a WARNING line in agent.log per review), and mibyan doctor resolves every routed auxiliary.<task> block through the same resolver and reports the ones that fail.
Full auxiliary config reference
Context compression has its own
compression: block for thresholds and an auxiliary.compression: block for model/provider settings — see Context Compression above. The primary fallback chain uses a top-level fallback_providers: list — see Fallback Providers. All three follow the same provider/model/base_url pattern.Per-task fallback chain for auxiliary tasks
Each auxiliary task can optionally define afallback_chain — a list of provider/model entries that Mibyan tries when the primary auxiliary provider fails due to rate limits, connectivity issues, or payment restrictions:
openrouter / openai/gpt-4o-mini) returns a rate-limit, connection timeout, or payment-required error, Mibyan walks the fallback_chain in order. It skips entries whose provider matches the already-failed provider, and tries each remaining entry until one succeeds or the chain is exhausted. If all fallbacks fail, Mibyan falls back to the main agent model as a final safety net.
Each entry supports the same three knobs as any auxiliary task config:
fallback_chain is available on any auxiliary task — compression, vision, approval, skills_hub, mcp, etc.
Native vision embed budgets (top-level vision:)
Separate from auxiliary.vision (which picks the describer model): when the main model is vision-capable, vision_analyze and browser screenshots embed real pixels into tool results that are re-sent every later turn. vision.embed_target_bytes (default 262144, clamped 64 KiB..4 MiB) sizes one embed; vision.max_calls_per_image caps how often the same image may be embedded per session (unset = 3 inside delegated subagents, unlimited for the main agent; 0 = unlimited). See Vision → Native embeds ride the session.
Limiting auxiliary concurrency
max_concurrency caps in-flight LLM calls for auxiliary tasks such as compression and title_generation across the whole process. auxiliary.vision.max_concurrency is excluded: it already controls only vision’s CPU-bound image encode/resize workers, not LLM requests. This is most useful when:
- Many sessions can spawn background work simultaneously (Discord/Telegram channels, multiple terminals)
- Your provider is rate-limited or going through an incident and retries would amplify the burst
2:
OpenRouter routing & Pareto Code for auxiliary tasks
When an auxiliary task resolves to OpenRouter (either explicitly or viaprovider: "main" while your main agent is on OpenRouter), the main agent’s provider_routing and openrouter.min_coding_score settings do not propagate — by design, each auxiliary task is independent. To set OpenRouter provider preferences or use the Pareto Code router for a specific aux task, set them per-task via extra_body:
extra_body verbatim, so any other OpenRouter request-body field documented at openrouter.ai/docs works the same way.
Changing the Vision Model
To use GPT-4o instead of Gemini Flash for image analysis:~/.mibyan/.env):
Provider Options
These options apply to auxiliary task configs (auxiliary:, compression:) and primary fallback entries (fallback_providers: or legacy fallback_model:), not to your main model.provider setting.
Direct API-key providers from the main provider catalog also work here when you want side tasks to bypass your default router. For example,
gmi is valid once GMI_API_KEY is configured, and fireworks is valid once FIREWORKS_API_KEY is configured:
/v1/models endpoint. Fireworks model IDs use the provider’s native slash form, for example accounts/fireworks/models/glm-5p2.
Common Setups
Using a direct custom endpoint (clearer thanprovider: "main" for local/self-hosted APIs):
base_url takes precedence over provider, so this is the most explicit way to route an auxiliary task to a specific endpoint. For direct endpoint overrides, Mibyan uses the configured api_key or falls back to OPENAI_API_KEY; it does not reuse OPENROUTER_API_KEY for that custom endpoint.
Using OpenAI API key for vision:
mibyan model and select MiniMax (OAuth) to log in and set this automatically. For the China region, the base URL will be https://api.minimaxi.com/anthropic. See the MiniMax OAuth guide for the full walkthrough.
Using a local/self-hosted model:
provider: "main" uses whatever provider Mibyan uses for normal chat — whether that’s a named custom provider (e.g. beans), a built-in provider like openrouter, or a legacy OPENAI_BASE_URL endpoint.
Environment Variables (legacy)
Auxiliary models can also be configured via environment variables. However,config.yaml is the preferred method — it’s easier to manage and supports all options including base_url and api_key.
Compression and fallback model settings are config.yaml-only. (
AUXILIARY_WEB_EXTRACT_* variables are obsolete — web extraction no longer uses an auxiliary LLM.)
Reasoning Effort
Control how much “thinking” the model does before responding:Answer length (text_verbosity)
Responses-API models (OpenAI GPT-5 family and later, direct OpenAI, ChatGPT Codex and Azure routes) also accept a separate knob for how long the final natural-language answer is, independent of reasoning depth:
text: {verbosity: ...} field only on Responses-family routes; it is never sent on chat_completions, Anthropic or xAI requests, and an empty or unknown value sends nothing. Structured-output (text.format) set through request_overrides is passed through unchanged.
Adaptive-thinking models (Claude 4.6+, Fable/Mythos-class) over OpenRouterThese models use adaptive thinking and don’t accept the usual
reasoning.effort
field — OpenRouter ignores it for them. Mibyan transparently routes your
reasoning_effort to OpenRouter’s verbosity parameter instead (which maps to
Anthropic’s output_config.effort), so the same effort knob keeps working with
the levels supported by the selected model. none (or unset) leaves the model
on its own adaptive default. The
native Anthropic provider already controls effort directly and is unaffected.OpenRouter models and supported effort levelsFor other models routed through OpenRouter, Mibyan reads the live model
catalog’s reasoning metadata (
supported_parameters + per-model
reasoning.supported_efforts) to decide whether to send reasoning controls at
all and to clamp your requested effort to the nearest level the route actually
supports (always downward — e.g. ultra becomes high on a route that stops
at high, never a silent escalation). New reasoning-capable vendors work
automatically without waiting for a Mibyan update; when the catalog is
unreachable or a model isn’t listed, Mibyan falls back to its built-in
model-family list and passes your effort through unchanged.ultra is clamped to the strongest level the route acceptsultra is a Mibyan-internal ladder step: no provider wire accepts it, so every route clamps it
to its strongest level (max on GPT-5.6 Codex and OpenAI-compatible routes, xhigh on older
Codex models). The effort pickers and /reasoning status show this as
ultra (sends max on this route) so the level you see is the level that is sent./reasoning command:
--global to save the
new level as your agent.reasoning_effort default.
Per-Model Reasoning Overrides
You can set different reasoning effort levels for different models. This is useful when you want high reasoning for complex models but medium for faster ones:claude-opus-4.5,claude-opus-4-5,claude-opus.4.5(dots and dashes are interchangeable)anthropic/claude-opus-4.5,openrouter/anthropic/claude-opus-4.5(provider prefix optional)- A key prefixed with a named custom provider (
ollama-local/qwen3.6:27b-q4_k_m) also applies when the request carries only the bare model id (qwen3.6:27b-q4_k_m), which is what fallback entries andproviders:routes send - Exact matches take precedence over variants
Custom reasoning tier names
Some OpenAI-compatible endpoints expose thinking tiers outside the standard ladder (a relay servingfast/thinking instead of low…max). A bare string outside the ladder is rejected with Unknown reasoning_effort '<value>', using default (medium) so a typo can never reach the wire. To request a provider’s own tier name, use the explicit dict form — the effort value is sent verbatim as the top-level reasoning_effort field:
enabled: false in the dict form turns thinking off, the same as reasoning_effort: none.
The dict form is set by editing config.yaml directly: the /reasoning menus, mibyan model, and the dashboard’s auxiliary-model pickers only offer the standard ladder (the TUI status and setup wizard still show the custom tier name once it is configured).
Model ids contain dots (
claude-opus-4.5, qwen3.6:27b), which mibyan config set treats as nesting separators. Escape them with a backslash to write the literal key — mibyan config set 'agent.reasoning_overrides.ollama-local/qwen3\.6:27b-q4_k_m' low — or edit the YAML directly. See Dots inside key names.OpenAI Responses (
openai-api, openai-codex)reasoning_effort: none is sent explicitly as reasoning.effort: "none" on models that accept it (GPT-5.x): omitting the field would leave the model’s default effort on (GPT-5.6 defaults to medium). An unset effort is the only state that omits the field. Chat-era models on api.openai.com (gpt-4o, gpt-4.1, their -mini variants and fine-tunes) reject any reasoning parameter, so Mibyan sends none for them regardless of the configured effort instead of failing with 400 Unsupported parameter: 'reasoning.effort'. If a model rejects none, Mibyan warns, drops the disable for the session and retries with the model’s default.Local OpenAI-compatible endpointsA custom
base_url (http://localhost:11434/v1, a vLLM, SGLang or router endpoint) receives the resolved effort — agent.reasoning_effort or the matching per-model override — as the standard top-level reasoning_effort request field, clamped to the values the OpenAI-compatible wire accepts (none, minimal, low, medium, high, xhigh, max). An unset effort is sent as medium here too, the same default the Nous Portal and OpenRouter routes apply — leaving the field off would hand the choice to the endpoint, and a hosted reasoning model’s own default can be its ceiling (kimi-k3 defaults to max: about 3x the reasoning tokens and latency of medium). The field stays off for a model the catalog or model_overrides mark supports_reasoning: false, for a local Ollama model pulled without the thinking capability, and for the rest of a session after the endpoint answered 400 to the field. The nested reasoning object is reserved for endpoints known to accept it (Nous Portal, OpenRouter reasoning-capable models, GitHub Models) because arbitrary servers reject unknown fields with HTTP 400. If your server reads its thinking budget from a different field (Ollama’s think, vLLM’s chat_template_kwargs, a router-specific key), set it under the custom provider’s extra_body, which is merged into every request routed there.- Session-scoped
/reasoning --sessionoverride (gateway only) - Per-model override from
agent.reasoning_overrides(spelling-tolerant) - Global
agent.reasoning_effort - Provider default
mibyan -p one-shots, messaging gateway, Desktop/TUI, ACP sessions, cron jobs, /model mid-session switches (including a switch issued before the first message), session resume (--resume, /resume), /new, and fallback model activation.
Fast Mode
Fast mode asks the provider for faster output at a premium price: OpenAI Priority Processing (service_tier: priority) and Ultrafast (service_tier: ultrafast) on supported OpenAI models, xAI Priority Processing on Grok 4.6, and Anthropic Fast Mode (speed: fast, Opus 4.8 / Opus 5 / Opus 5.5 only). The openai and openai-api providers use the first-party OpenAI endpoint. It is off by default.
/fast normal|fast|ultrafast|auto|cold switches the mode for the session. Add --global to persist to config.yaml. /fast alone shows the current mode.
Cost note: both providers bill fast requests at a multiplier on standard rates (Anthropic: 40 per MTok in/out on Opus 5.5, 50 on Opus 5 and Opus 4.8), stacking with prompt-cache pricing. Mibyan prices each Anthropic response from the speed the API reports in usage.speed. auto/cold bound that premium to the window only. Fast params are only sent to the first-party endpoint that supports them (api.openai.com / Codex subscription, api.anthropic.com, api.x.ai); OpenRouter, Nous Portal, Copilot, Azure, Bedrock, and custom base_url routes never receive them in any mode.
Prompt cache: only the per-request parameter changes between requests; the system prompt, tools, and messages stay byte-identical. Anthropic keeps a separate prompt cache for each speed, so on Anthropic every auto/cold window boundary re-writes the conversation prefix at the new speed. For long Anthropic sessions, fast or normal keeps a single warm cache.
Fast mode’s speedup is in output tokens per second, so long answers gain the most.
When an Anthropic organization has no fast-mode capacity for a model (the API answers a fast request with a fast-mode limit of 0), Mibyan switches that model to standard speed for the rest of the session and retries the request.
Fast tiers behind a gateway or proxy
The first-party-only rule is deliberate: a fast-tier parameter is a billing instruction, and Mibyan only sends it to the endpoint whose price list it knows. If you run an OpenAI-compatible gateway, router, or proxy that exposes its own priority tier (its ownservice_tier value, or a differently named field), request it through that provider’s extra_body instead of agent.service_tier. extra_body on a named custom provider is merged into every chat-completions request routed to that endpoint, survives gateway turns and /fast changes, and is dropped again when you /model away from the provider:
agent.service_tier: the tier is always on for that provider (no auto/cold window), /fast does not toggle it, and Mibyan does not validate the value — the gateway decides what it accepts and what it bills.
Tool-Use Enforcement
Some models occasionally describe intended actions as text instead of making tool calls (“I would run the tests…” instead of actually calling the terminal). Tool-use enforcement injects system prompt guidance that steers the model back to actually calling tools.What it injects
When enabled, two layers of guidance may be added to the system prompt:- General tool-use enforcement (all matched models) — instructs the model to make tool calls immediately instead of describing intentions, keep working until the task is complete, and never end a turn with a promise of future action.
- Google operational guidance (Gemini and Gemma models only) — conciseness, absolute paths, parallel tool calls, and verify-before-edit patterns.
"auto" excludes them.
When to turn it on
If you’re using a model not in the default auto list and notice it frequently describes what it would do instead of doing it, settool_use_enforcement: true or add the model substring to the list:
Execution-Discipline Guidance
Separately from tool-use enforcement, Mibyan injects an execution-discipline block for model families that share a set of agentic failure modes observed in eval traces: doing arithmetic in prose instead of code, skipping read-back verification after external writes, “repairing” malformed identifiers, claiming completeness despite count mismatches, and declaring “done” without verifying every acceptance criterion.
The injected block covers:
- Tool persistence — keep calling tools until the task is complete and verified; retry empty, partial, or suspiciously narrow lookup results with a broader or different query before concluding.
- Mandatory tool use — arithmetic, hashes, dates, system state, and file facts always come from a tool, never from mental computation.
- External-write read-back — after any state-changing write to an external system, read back the exact target before claiming success (internal file edits a tool already confirmed are not re-verified).
- Count reconciliation — declared totals (
total,reply_count,has_more) are hard assertions; on mismatch, re-fetch or parse programmatically. - Literal preservation — never normalize or “repair” identifiers that fail a stated format; a successful lookup does not validate a malformed source token.
- Verification-gated completion — “done” means every named acceptance criterion is verified, never a plausible subset.
tool_use_enforcement — either can be on without the other. The guidance is chosen once at session start keyed on the model name, so the system prompt stays byte-stable (and prompt-cache-friendly) for the life of the conversation. Gemini/Gemma are excluded from the auto list because they receive the more specific Google operational guidance; Claude is excluded because it doesn’t exhibit these failure modes — opt any model in with true or a substring list.
Tool-Loop Guardrails
Mibyan detects when the agent is stuck in an unproductive tool-calling loop — the same tool call failing repeatedly, the same tool failing over and over, or an idempotent call returning the same result with no progress. By default it injects a warning into the tool result so the model self-corrects. Interactive CLI, TUI, Desktop, and ACP sessions remain warning-only because a person can intervene; unattended gateway and cron sessions enable hard stops by default. The platform-aware default can be disabled for an unattended deployment, or hard stops can be explicitly enabled on every platform:hard_stop_enabled explicitly enables hard stops on every platform. When it remains false, non_interactive_hard_stop_enabled still enables them for unattended gateway/cron-style platforms while preserving warning-only behavior for CLI, TUI, Desktop, ACP, subagents, and api_server runs (supervised task loops with a live parent or client). Set non_interactive_hard_stop_enabled: false to opt an unattended deployment out. See also Docker / unattended deployments.
Hard stops are designed to catch replays — the same call, unchanged, with nothing happening in between — not legitimate iteration:
- Edit → re-run is never a loop. Any successful mutating call (
write_file,patch, a greenterminal/execute_code, a browser action, a job/message/cron mutation) marks progress for every failing call still being counted. The next identical retry (re-running a red test after a fix, re-snapshotting after a click) starts a fresh streak instead of accumulating toward a block. - Distinct red commands are diagnosis, not a loop. For tools whose non-zero exit is ordinary output (
terminal,execute_code, process pollers,browser_navigate,web_extract) thesame_tool_failurethreshold only warns and never halts. Only an exact-args replay with no intervening change, or an identical-result streak, can stop them. - A halt ends the turn, not the session. The agent replies with which guardrail fired and why; replying “continue” resumes with fresh per-turn counters.
Per-turn runaway-loop caps
Separate from the failure-based thresholds above,loop_caps sets hard ceilings on how many web_search calls and subagent spawns a single agent loop (turn) may make. The counters reset at the start of every turn, so a legitimate multi-turn session is never starved — but a single turn that spirals into an unbounded search or delegation loop is stopped. These are always on and fire regardless of hard_stop_enabled. A single turn issuing dozens of web searches or spawning dozens of subagents is already pathological, so the defaults are low. When a cap is reached, the offending tool call is blocked with an explanatory message and the turn stops cleanly instead of burning the rest of the budget. Set either value to 0 to disable that cap entirely.
A single delegate_task batch counts each task toward max_subagents (a batch of 3 spends 3), so the cap tracks real subagents spawned rather than delegate_task invocations.
This mirrors Claude Code’s per-session WebSearch and subagent caps (v2.1.212), which also default to 200 and reset on /clear.
Runtime anti-stall guards
Complementing the failure-based guardrails above,agent.stall_guards (default true) enables two conservative runtime guards against wasted turns. First, an identical-call loop breaker: when the same tool is called 3+ consecutive times with identical arguments and returns an identical result, a short one-line notice is appended to that tool result telling the model not to repeat the call — in warning-only sessions it never blocks the call, and legitimately-repeatable pollers (process_manage, *_get_result, *_poll) are exempt. When hard stops are active (explicit hard_stop_enabled, or an unattended gateway/cron platform), the same streak also becomes a hard stop once it reaches hard_stop_after.idempotent_no_progress consecutive identical calls — for any tool, not just the read-only ones the idempotent_no_progress guardrail tracks — so a model replaying the same successful terminal or skill_view call is halted instead of running out the iteration budget (identical_call_streak_halt). Second, a continue-intent recovery: when the model ends a turn with no tool calls but its short reply trails off announcing an action (“Let me now update the file…”), Mibyan re-prompts it to act via the same bounded continuation mechanism used for intent-ack recovery (max 2 re-prompts per turn). Both are cache-safe (notices are added at result construction, never retroactively) and can be disabled together:
tool_call_id, an args summary, and — if the first result was persisted to disk — its spillover path) instead of repeating the full output. The tool still executes every time, so polling semantics are preserved: a changed result always flows through whole. Results under 512 characters, error results, and multimodal results are never stubbed, and pollers are stubbed (an unchanged poll is exactly the case where the duplicate payload carries no information).
Turn liveness watchdog
agent.turn_liveness bounds how long a conversation turn may make no observable progress before Mibyan force-recovers it. The watchdog keys off the activity clock (the same signal that stamps API waits, stream tokens, and tool heartbeats — lease renewal never counts), so a turn that silently wedges mid-flight (observed as issue #95548: no tool execution, no API call, no error, but the session stays “busy” indefinitely) is surfaced loudly, interrupted so it unwinds as a retriable interrupted turn, and — when the interrupt cannot unwind the wedge — its durable turn lease stops renewing so stale-turn cleanup can reclaim the session instead of it hanging until the process is killed.
NaN, Inf, non-positive poll_s) log a warning and fall back to the defaults — they never crash startup or silently disable the watchdog. A fired abort reports the stall as it begins recovery, and publishes the definitive aborted/lease-stopped outcome only once the interrupt has actually committed.
TTS Configuration
text_to_speech tool and spoken replies in voice mode (/voice tts in the CLI or messaging gateway).
Speed fallback hierarchy: provider-specific speed (e.g. tts.edge.speed) → global tts.speed → 1.0 default. Set the global tts.speed to apply a uniform speed across all providers, or override per-provider for fine-grained control.
Display Settings
Per-turn summary and spinner token flow
display.turn_summary (default true) prints one dim accounting line after each interactive CLI turn, summarising what that turn actually did:
- Wall time is the turn’s real duration (
2m05spast the one-minute mark). - Tool calls are grouped by verb (
edited,read,ran,searched, …) with correct pluralisation; plugin/MCP tools without a curated verb collapse intocalled N tools. +X -Yline deltas appear only when the tool result already reports a diff (currentlypatch). Mibyan never shells out to git to compute them, so awrite_fileedit is counted without a delta.- Failed tool calls are not counted — a denied write never renders as a successful edit (see the file-mutation verifier for the complementary warning).
- Long turns cap at four verb segments plus a
+N moretail so the line never wraps. - A fast turn with no tool calls prints nothing at all.
display.spinner_token_flow (default true) appends the running turn’s cumulative output tokens to the CLI spinner’s live timer:
↓ 0 tok.
Both keys are display-only and CLI-only: they are suppressed in quiet mode, when display.tool_progress is off, in single-query/-Q batch runs, and in gateway/messaging surfaces (those use display.runtime_footer instead). Set either key to false to turn it off.
File-mutation verifier
Whendisplay.file_mutation_verifier is true (default), Mibyan appends a one-line advisory to the assistant’s final response whenever a write_file or patch call failed during the turn and the target file was neither written successfully afterwards (under any spelling of its path) nor otherwise changed on disk before the turn ended. This catches the “batch of parallel patches, half silently fail, model summarises success” class of over-claim without requiring you to manually run git status after every edit.
Example footer:
file_mutation_verifier: false (or mibyan_FILE_MUTATION_VERIFIER=0) to suppress the footer. The verifier only fires when real failures are outstanding at turn end — a model that retries a failed patch and succeeds within the same turn will not trigger it for that file.
Trust the verifier over the model’s summary. The footer means the listed edit calls failed and Mibyan saw no later change to those files, even if the assistant’s closing message says the task is done. It only tracks write_file/patch receipts plus a modification-time check at turn end, so run git status or read_file to confirm what actually landed. Common causes:
- Write denied — path is on the credential denylist or outside
mibyan_WRITE_SAFE_ROOT(see File write safety) - Patch mismatch —
old_stringdid not match the file on disk - Syntax gate — candidate content failed JSON/YAML/TOML validation before write
~/.mibyan/) are failing, check whether mibyan_WRITE_SAFE_ROOT is set in your environment. For cron changes, use the cronjob_manage tool or mibyan cron edit instead of patching jobs.json directly.
UI language for static messages
Thedisplay.language setting translates a small set of static user-facing messages — the CLI approval prompt, a handful of gateway slash-command replies (e.g. restart-drain notices, “approval expired”, “goal cleared”). It does not translate agent responses, log lines, tool output, error tracebacks, or slash-command descriptions — those stay in English. If you want the agent itself to reply in another language, just tell it in your prompt or system message.
Bundled values: en (default), zh (Simplified Chinese), zh-hant (Traditional Chinese), ja (Japanese), de (German), es (Spanish), fr (French), tr (Turkish), uk (Ukrainian), af (Afrikaans), ko (Korean), it (Italian), ga (Irish), pt (Portuguese), ru (Russian), hu (Hungarian), ar (Arabic).
The list is pluggable: a language pack plugin (provides_locales) or a partial <mibyan_HOME>/locales/<lang>.yaml overlay adds a language or overrides wording, and mibyan config set display.language <id> accepts any id a bundled catalog, your overlay, or an installed pack provides. Unknown ids are refused with the list of available languages; at runtime an unresolvable value falls back to English.
You can also set this per-session with the mibyan_LANGUAGE env var, which overrides the config value.
In the CLI, cycle through these modes with
/verbose. To use /verbose in messaging platforms (Telegram, Discord, Slack, etc.), set tool_progress_command: true in the display section above. The command will then cycle the mode and save to config.
Tool progress requires a gateway adapter that can display progress updates safely. Platforms without message editing support, including Signal, suppress tool-progress bubbles even if /verbose saves a non-off mode.
off hides tool-call chrome only. Application state that has its own surface in the Desktop app and TUI — the task list (todo_list), subagent progress, clarify questions, and MCP consent cards — keeps flowing regardless of this setting.
Focus view (/focus, CLI + TUI)
display.focus_view: true enables focus view — a reduced-output display mode for when you want the answer, not the play-by-play. It is a thin layer over the same tool_progress machinery rather than a second suppression path:
- turning it on pins
tool_progresstooffand stashes your previous mode indisplay.focus_saved_tool_progress; /focus offrestores that mode exactly, so a/verbose verbosesetup survives a round trip;- each completed turn ends with a dim recovery line —
⋯ 7 tool lines hidden · /focus off to show— counted against your pre-focus mode, so it never claims to have hidden lines you had already turned off; - a persistent
◉ focusbadge sits in the status bar (both the prompt_toolkit CLI and the Ink TUI) so the reduced mode is never invisible; - cycling
/verbosewhile focus is on hands the mode back to/verboseand clears the badge.
Status-bar field selection (CLI/TUI)
The interactive status bar at the bottom of the CLI/TUI shows the model, context usage, compression count, background-activity counters, timers, and mode badges.display.status_bar.fields chooses which of those are visible — useful for a minimal bar (just model + duration) or for surfacing the opt-in session token total:
model, context_detail (used/total tokens), context_pct (percent + meter), cache_hit (prompt cache hit ratio — resets on model switch and compression), latency (rolling mean API latency, last 10 calls), tps (rolling output tokens/sec, last 10 calls), compressions, bg_tasks, bg_processes, bg_subagents, goal, git_branch (⎇ current git branch of the working directory — opt-in only, never shown by default; detached HEAD shows the abbreviated commit), duration, prompt_elapsed, idle_since, focus, yolo, stash, battery, title (right-aligned session badge), and total_tokens (session Σ — opt-in only, never shown by default).
Notes:
- An empty list (the default) keeps the standard set — everything except
total_tokensandgit_branch. - The config controls visibility, not order; fields render in their built-in positions.
- Narrow terminals still drop wide-mode-only fields (
context_detail,cache_hit,latency,tps,prompt_elapsed,idle_since) regardless of config (cache_hitalso shows in the medium ≥52-col tier). latency/tpsstay hidden until API calls have been recorded (e.g. the Codex app-server backend reports no latency).batteryandtitlevisibility here compose with their own toggles (/battery,/title) — both must be on for the segment to show.- The same key also filters the Ink TUI status rule (
mibyan tui), wherecache_hit,latency, andtpsrender as width-budgeted tail segments (◎ / ◷ / ↑) on terminals ≥96/104/110 columns respectively. - Display-only: no effect on prompt caching or request payloads. Changes take effect on the next session start.
Runtime-metadata footer (gateway only)
Whendisplay.runtime_footer.enabled: true, Mibyan appends a small runtime-context footer to the final message of each gateway turn. The current footer can show the model, context-window percentage, and current working directory. Off by default; opt in per-gateway if your team wants every reply to include this provenance.
The default field set is
["model", "context_pct", "cwd"]. latency and served_model are opt-in — add them to fields to use them. served_model renders nothing when the served model is the configured one (or when the proxy sends no such header), so behind a routing proxy or an active fallback it is the field that makes the switch visible. Fields whose data is unavailable are skipped silently rather than rendering an empty slot.
The /footer slash command toggles this at runtime in any session.
Example footer appended to a Telegram/Discord/Slack reply:
Per-platform progress overrides
Different platforms have different verbosity needs. Usedisplay.platforms to set per-platform modes:
mibyan config set display.platforms.telegram.streaming false. The shorthand mibyan config set platforms.telegram.streaming false is accepted too: because per-platform display settings (streaming, show_reasoning, tool_progress, …) are only ever read from display.platforms, config set/get/unset redirect that shorthand to the canonical key and print a note. Connection keys under the top-level platforms.<name> block (token, enabled, reply_to_mode, extra) are not redirected. Writing them under the nested prefix (mibyan config set gateway.platforms.telegram.enabled true) is redirected to the top-level platforms.telegram.enabled with a note: the gateway reads both blocks, but the top-level one wins on shared keys, so a nested write would be silently shadowed by an existing top-level value.
Platforms without an override fall back to the global tool_progress value. Valid platform keys: telegram, discord, slack, signal, whatsapp, matrix, mattermost, email, sms, homeassistant, dingtalk, feishu, wecom, weixin, bluebubbles, qqbot. The legacy display.tool_progress_overrides key still loads for backward compatibility but is deprecated and migrated into display.platforms on first load.
Signal is listed as a valid platform key because the setting can be saved per platform, but the current Signal adapter cannot edit sent messages and does not render tool-progress bubbles. Keep Signal tool_progress set to off; use the CLI or an editing-capable messaging platform if you need to watch each tool call live.
interim_assistant_messages is gateway-only. When enabled, Mibyan sends completed mid-turn assistant updates as separate chat messages. This is independent from tool_progress and does not require gateway streaming.
show_commentary (default true) controls Codex Responses models’ commentary channel — the polished progress narration these models produce alongside their private reasoning. When enabled, each completed commentary message is delivered as a visible mid-turn update (on the gateway this also requires interim_assistant_messages). Set it to false if the extra narration annoys you: commentary then falls back to the reasoning channel and is only shown when show_reasoning is enabled.
Privacy
redact_pii is true, the gateway redacts personally identifiable information from the system prompt before sending it to the LLM on supported platforms:
Platform support: Redaction applies to WhatsApp, Signal, and Telegram. Discord and Slack are excluded because their mention systems (
<@user_id>) require the real ID in the LLM context.
Hashes are deterministic — the same user always maps to the same hash, so the model can still distinguish between users in group chats. Routing and delivery use the original values internally.
OpenAI Codex request identity
OpenAI requires third-party Codex harnesses to identify themselves. ChatGPT-authenticated requests to the official Codex endpoint automatically sendoriginator: mibyan-agent and User-Agent: MibyanAgent/<version>.
The existing ChatGPT account header is preserved. No additional prompt content
or telemetry request is sent.
Direct OpenAI API requests and custom proxy endpoints are unchanged.
Speech-to-Text (STT)
stt.<provider>.language → stt.language → mibyan_LOCAL_STT_LANGUAGE env var → provider auto-detect. The default is stt.language: "en" — Whisper auto-detection frequently misidentifies short or accented clips, which shows up as voice notes transcribed in the wrong language. Non-English speakers should set stt.language to their language code once (e.g. "es", "zh", "uk"); set it to "" to restore auto-detection for multilingual use.
stt.openai.timeout and stt.openai.max_retries shape the OpenAI-SDK transcription client that the openai, groq and deepinfra providers share (there are no per-provider siblings yet, and the SDK reads no environment variables for these). The defaults are 60 / 1 rather than the previous fixed 30 s / no retries because a self-hosted OpenAI-compatible endpoint can take longer than 30 s to load its model on the first request, which used to lose that voice message outright. The trade-off: an unreachable backend now holds a voice message for up to two attempts × the timeout before Mibyan gives up; set timeout: 30 and max_retries: 0 for the old shape.
Set stt.echo_transcripts: false when the gateway should transcribe voice notes for the agent but must not post the raw transcript back to the chat (for example, customer-facing WhatsApp bots).
Provider behavior:
localusesfaster-whisperrunning on your machine. Install it separately withpython -c "import pm; pm.sync_venv(['stt-whisper'], explicit=True)". Silence-hallucination hardening is on by default: a Silero VAD filter keeps silence/noise from ever reaching Whisper, cross-window conditioning is disabled, and segments the model itself flags as probably-not-speech and low-confidence are dropped. Setstt.local.vad: falseto transcribe non-speech audio (music, ambient) with the raw behavior. The model stays loaded in memory between voice messages for low-latency transcription; setstt.local.unload_after_idle_seconds(e.g.300for 5 minutes) to automatically release the model when idle. This frees GPU memory on CUDA hosts (the main win when a local LLM shares the GPU); on CPU the memory becomes reusable by the process, though the OS-visible footprint may not shrink until the process needs the space for something else. The next voice message reloads the model transparently.groquses Groq’s Whisper-compatible endpoint and readsGROQ_API_KEY. Passstt.groq.language(or the globalmibyan_LOCAL_STT_LANGUAGEenv var) to skip auto-detection and reduce latency.openaiuses the OpenAI speech API and readsVOICE_TOOLS_OPENAI_KEY.
ffmpeg is installed: long pauses in a voice note are collapsed client-side before the file uploads, keeping cloud_trim_keep_ms of each pause so natural pacing survives. Shorter audio means faster uploads, lower per-audio-minute billing, and fewer silence hallucinations from the remote model. Clips shorter than 12 seconds skip the trim entirely (savings can’t matter there, and several providers bill a per-request minimum anyway). The trim is best-effort — if ffmpeg is missing, the trim fails, the clip is mostly silence, or trimming would save less than ~10%, the original file is uploaded untouched. Set stt.cloud_trim_silence: false to always upload the original (e.g. when transcribing music or ambient audio through a cloud provider). Command-type and plugin providers never get trimmed audio.
An explicitly selected stt.provider is honored strictly — if it’s unavailable, transcription errors with guidance to run mibyan tools rather than switching providers. Only when no provider has ever been selected does Mibyan auto-detect in this order: local → groq → openai.
Groq and OpenAI model overrides are environment-driven:
Transcription prompt (vocabulary hints)
stt.prompt is an optional static hint passed to prompt-capable STT backends. Use it for proper nouns, product names, and jargon that Whisper-family models otherwise mis-hear:
pre_transcription hook mutate on top of it, last-writer-wins per field. Multiple plugins’ hints compose deterministically: plugin discovery loads plugins in sorted order by plugin id, and each plugin’s callbacks run in its own registration order, so the same set of plugins always produces the same final prompt. A hook returning an empty string for prompt clears the config prompt for that request. Hooks may also override language and model; file_path is read-only and any attempt to change it is logged and dropped. With no hook registered and no stt.prompt set, the outgoing request is identical to previous releases.
Provider support.
Length. Whisper-family models only condition on the final ~224 prompt tokens. For the whisper-family backends (
local, openai, groq, deepinfra) Mibyan enforces that cap client-side: an over-long final prompt is truncated to its tail with a logged warning — the request never errors because of prompt length. Other backends (mistral, plugin providers) receive the prompt unchanged and own their own validation. Keep hints short and specific either way.
Voice Mode (CLI)
/voice on in the CLI to enable microphone mode, record_key to start/stop recording, and /voice tts to toggle spoken replies. See Voice Mode for end-to-end setup and platform-specific behavior.
Streaming
Stream tokens to the terminal or messaging platforms as they arrive, instead of waiting for the full response.CLI Streaming
Gateway Streaming (Telegram, Discord, Slack)
display.interim_assistant_messages: true.
Overflow handling: If the streamed text exceeds the platform’s message length limit (~4096 chars), the current message is finalized and a new one starts automatically.
Fresh final (Telegram): Telegram’s editMessageText preserves the original message timestamp, so a long-running streamed reply would keep the first-token timestamp even after completion. Set fresh_final_after_seconds > 0 to opt in to delivering old previews as brand-new final messages with best-effort preview deletion. The default is 0, which always finalizes streamed replies in place and avoids the brief duplicate-message/delete sequence on clients that show both operations.
Per-platform streaming defaultsThe master
streaming.enabled switch is false by default — nothing streams until you flip it. Once enabled, streaming is decided per platform: Telegram ships with display.platforms.telegram.streaming: true (streams) and Discord with display.platforms.discord.streaming: false (does not). So after enabling streaming, Telegram streams out of the box and Discord stays on whole-message replies until you change its toggle. You can adjust these per-platform switches from the dashboard’s Channels toggles or directly in ~/.mibyan/config.yaml.Group Chat Session Isolation
Limit how many chat sessions can actively be open across CLI, TUI/dashboard, and messaging gateway:mibyan status to see the current slot usage and every holder.
This is the only cap on concurrent gateway turns: the gateway runs each turn body
on its own thread, so with the default (unset) every accepted turn starts
immediately instead of queuing behind other running turns.
The canonical key is top-level max_concurrent_sessions. Mibyan also accepts
gateway.max_concurrent_sessions as a fallback, but the top-level key wins when
both are set.
The cap is enforced with a local runtime lease file and is best-effort: Mibyan
fails open if the registry cannot be read or locked so users are not stranded.
It is intended for a single host/profile runtime, not a shared $mibyan_HOME
mounted across multiple machines. A lease whose owning process exists but whose
liveness cannot be proved (for example an unreadable /proc entry inside a
container after mibyan update restarts the backend) still counts toward the
cap and still fences its own session id, but it no longer blocks claiming or
releasing a different session.
Control whether shared chats keep one conversation per room or one conversation per participant:
trueis the default and recommended setting. In Discord channels, Telegram groups, Slack channels, and similar shared contexts, each sender gets their own session when the platform provides a user ID.falsereverts to the old shared-room behavior. That can be useful if you explicitly want Mibyan to treat a channel like one collaborative conversation, but it also means users share context, token costs, and interrupt state.- Direct messages are unaffected. Mibyan still keys DMs by chat/DM ID as usual.
- Threads stay isolated from their parent channel either way; with
true, each participant also gets their own session inside the thread.
Unauthorized DM Behavior
Control what Mibyan does when an unknown user sends a direct message:-
pairis the default for chat-style DM platforms. Mibyan denies access, but replies with a one-time pairing code in DMs. -
ignoresilently drops unauthorized DMs. -
declinesends one short, polite decline instead of a pairing code, then stays silent toward that sender for 24 hours. Override the default text:mibyan gateway setupoffers this as “Politely decline unknown senders” when you leave the allowlist empty; it writesplatforms.<platform>.unauthorized_dm_behavior: decline. -
Email defaults to
ignoreunlessplatforms.email.unauthorized_dm_behavior: pairis set, because inboxes can contain unrelated unread mail. - Platform sections override the global default, so you can keep pairing enabled broadly while making one platform quieter.
Quick Commands
Define custom commands that either run shell commands without invoking the LLM, or alias one slash command to another. Exec quick commands are zero-token and useful from messaging platforms (Telegram, Discord, etc.) for quick server checks or utility scripts./status, /disk, /update, /gpu, or /restart in the CLI or any messaging platform. exec commands run locally on the host and return the output directly — no LLM call, no tokens consumed. alias commands rewrite to the configured slash command target.
- 30-second timeout — long-running commands are killed with an error message
- Priority — quick commands are checked before skill commands, so you can override skill names
- Autocomplete — quick commands are resolved at dispatch time and are not shown in the built-in slash-command autocomplete tables
- Type — supported types are
execandalias; other types show an error - Works everywhere — CLI, Telegram, Discord, Slack, WhatsApp, Signal, Email, Home Assistant
Human Delay
Simulate human-like response pacing in messaging platforms:config.yaml is read, so multiplexed profiles keep independent pacing; there is no process-environment override. In custom mode a non-integer, negative or inverted min_ms/max_ms pair is rejected with a warning naming the key and the natural range (800–2500 ms) is used instead.
Code Execution
Configure theexecute_code tool:
mode controls the working directory and Python interpreter for scripts:
project(default) — scripts run in the session’s working directory with the active virtualenv/conda env’s python. Project deps (pandas,torch, project packages) and relative paths (.env,./data.csv) resolve naturally, matching whatterminal()sees.strict— scripts run in a temp staging directory withsys.executable(Mibyan’s own python). Maximum reproducibility, but project deps and relative paths won’t resolve.
*_API_KEY, *_TOKEN, *_SECRET, *_PASSWORD, *_CREDENTIAL, *_PASSWD, *_AUTH) and the tool whitelist apply identically in both modes — switching mode does not change the security posture.
Web Search Backends
Theweb_search and web_extract tools support five backend providers. Configure the backend in config.yaml or via mibyan tools:
Backend selection: The runtime always uses the stored
web.backend selection (set via mibyan tools; nous routes through the managed Tool Gateway). Only if no web backend has ever been selected is one auto-detected from available API keys: if only SEARXNG_URL is set, SearXNG is used; if only EXA_API_KEY is set, Exa; if only TAVILY_API_KEY is set, Tavily; if only PERPLEXITY_API_KEY is set, Perplexity; if only PARALLEL_API_KEY is set, Parallel; if only KEENABLE_API_KEY is set, Keenable. With no selection and no credentials at all, requests rotate round-robin across the keyless free-tier ring (Exa / Parallel / Firecrawl / Keenable) with automatic next-in-line failover on rate limits — see the Web Search guide for details. Once a selection exists, adding a key to .env does not change the route. Selecting Tavily, Firecrawl, or Keenable in mibyan tools also works without a key.
SearXNG is a free, self-hosted, privacy-respecting metasearch engine that queries 70+ search engines. No API key needed — just set SEARXNG_URL to your instance (e.g., http://localhost:8080). SearXNG is search-only; web_extract requires a separate extract provider (set web.extract_backend). See the Web Search setup guide for Docker setup instructions.
Self-hosted Firecrawl: Set FIRECRAWL_API_URL to point at your own instance. When a custom URL is set, the API key becomes optional (set `USE_DB_AUTHENTICATION=*** on the server to disable auth).
Parallel search modes: Set PARALLEL_SEARCH_MODE to control search behavior — fast, one-shot, or agentic (default: agentic).
Exa: Set EXA_API_KEY in ~/.mibyan/.env. Supports category filtering (company, research paper, news, people, personal site, pdf) and domain/date filters.
Browser
Configure browser automation behavior:must_respond(default) — capture the dialog, surface it inbrowser_snapshot.pending_dialogs, and wait for the agent to callbrowser_dialog(action=...). Afterdialog_timeout_sseconds with no response, the dialog is auto-dismissed to prevent the page’s JS thread from stalling forever.auto_dismiss— capture, dismiss immediately. The agent still sees the dialog record inbrowser_snapshot.recent_dialogswithclosed_by="auto_policy"after the fact.auto_accept— capture, accept immediately. Useful for pages with aggressivebeforeunloadprompts.
Timezone
Override the server-local timezone with an IANA timezone string. Affects cron scheduling and the time injected into the system prompt. It does not change log files: every line in~/.mibyan/logs/ is stamped in the machine’s local time, which is what mibyan logs --since compares against.
America/New_York, Europe/London, Asia/Kolkata, UTC). Leave empty or omit for server-local time.
mibyan doctor (and the startup config check) reports a value the runtime cannot load — a typo such as Asia/Tokio would otherwise silently put the agent clock and every cron schedule on server-local time. mibyan_TIMEZONE overrides this key when set.
The agent clock, cron schedules and time-aware tools follow this zone on every OS. Code run through execute_code also inherits it as TZ on Linux and macOS; on Windows those children keep the OS-configured zone instead (the Windows C runtime only parses POSIX-form TZ strings, and an IANA name there produces a wrong UTC offset), so set the Windows zone itself when child scripts must render local time in this zone.
Discord
Configure Discord-specific behavior for the messaging gateway:require_mention— whentrue(default), the bot only responds in server channels when mentioned with@BotName. DMs always work without mention.free_response_channels— comma-separated list of channel IDs where the bot responds to every message without requiring a mention.auto_thread— whentrue(default), mentions in channels automatically create a thread for the conversation, keeping channels clean (similar to Slack threading).free_response_auto_thread— whentrue, channels infree_response_channelsalso auto-create a thread per top-level message. Defaultfalse: free-response channels reply inline. Requiresauto_thread: true.
Security
Pre-execution security scanning and secret redaction:redact_secrets— whentrue, automatically detects and redacts patterns that look like API keys, tokens, and passwords in tool output before it enters the conversation context and logs. On by default. Set tofalseexplicitly only when you need raw credential-like strings for debugging or redactor development. Reading a secret-bearing file (.env-style files, shell rc/profile files, the Mibyanconfig.yamlundermibyan_HOMEand itsbackups/config/copies) withread_file,search_filesor a terminalcat/grepalso masks credential-shaped assignments (SOME_API_TOKEN: …) with a non-reusable«redacted-secret»marker, whatever the value looks like; ordinary source and project config files keep only the vendor-prefix patterns so fixtures such asMAX_TOKENS: 100are never mangled.tirith_enabled— whentrue, terminal commands are scanned by Tirith before execution to detect potentially dangerous operations.tirith_path— path to the tirith binary. Set this if tirith is installed in a non-standard location.tirith_timeout— maximum seconds to wait for a tirith scan. Commands proceed if the scan times out.tirith_fail_open— whentrue(default), commands are allowed to execute if tirith is unavailable or fails. Set tofalseto block commands when tirith cannot verify them.
Website Blocklist
Block specific domains from being accessed by the agent’s web and browser tools:web_search, web_extract, browser_navigate, and any tool that accesses URLs.
Domain rules support:
- Exact domains:
admin.example.com - Wildcard subdomains:
*.internal.company.com(blocks all subdomains) - TLD wildcards:
*.local
# comments are ignored). Missing or unreadable files log a warning but don’t disable other web tools.
The policy is cached for 30 seconds, so config changes take effect quickly without restart.
Smart Approvals
Control how Mibyan handles potentially dangerous commands:
Smart mode is particularly useful for reducing approval fatigue — it lets the agent work more autonomously on safe operations while still catching genuinely destructive commands.
Denial circuit breaker
approvals.denial_breaker_threshold (default 3) guards against the agent retrying variations of a command the smart-approval reviewer keeps denying — each retry burns another guardian LLM call. After that many consecutive denials in a session, the deny message escalates to a hard-stop instruction telling the agent to stop, report the blocked operation, and ask you to run it manually or /approve. Any approval resets the count; set 0 to disable:
Deny rules
approvals.deny is a list of glob patterns that block matching terminal commands unconditionally — even under --yolo, /yolo, or mode: off. It’s the user-editable counterpart to the built-in hardline blocklist:
* is a parse error). See Security — User-Defined Deny Rules for details.
Custom smart-approval policy
approvals.smart_policy lets you append your own rules to the smart-approval reviewer’s instructions. When set, the text is added to the guardian LLM’s system prompt (the trusted channel — never alongside the untrusted command text), so you can tighten or relax its judgment for your environment without editing code:
Checkpoints
Automatic filesystem snapshots before destructive file operations. See the Checkpoints & Rollback for details.Delegation
Configure subagent behavior for the delegate tool:delegation.provider and delegation.model to route subagents to a different provider:model pair — e.g., use a cheap/fast model for narrowly-scoped subtasks while your primary agent runs an expensive reasoning model.
Subagent fallback chain: Set delegation.fallback_providers to give workers their own chain (same entry shape as the top-level list). An explicitly pinned child (by provider, endpoint, or model) uses that chain only when it is declared; otherwise it fails loudly instead of borrowing the parent agent’s route. For an unpinned child, an absent or null setting preserves parent-chain inheritance. Use fallback_providers: [] under delegation: to disable child fallback entirely.
Direct endpoint override: If you want the obvious custom-endpoint path, set delegation.base_url, delegation.api_key, and delegation.model. That sends subagents directly to that OpenAI-compatible endpoint and takes precedence over delegation.provider. If delegation.api_key is omitted, Mibyan falls back to OPENAI_API_KEY only. When delegation.provider is set alongside delegation.base_url, the explicit endpoint and key still win, but that provider’s request settings (extra_body overrides and max output tokens from your custom_providers entry) are carried into the subagent.
Per-child request settings (request_overrides): delegation.request_overrides is a dict of request settings sent on every subagent API call. Top-level keys are API kwargs (e.g. service_tier); an extra_body sub-dict is merged into the request’s extra_body. It is honored on all three resolution branches — direct base_url, named provider, and pure inherit — so the key always takes effect. Precedence: explicit request_overrides values merge over any runtime- or parent-derived overrides — top-level explicit keys win, and extra_body is deep-merged one level so runtime extra_body keys (e.g. a provider’s thinking: {type: disabled} personality) survive unless your key redefines them. The canonical use case is OpenRouter routing hints for delegation children:
api_mode): Mibyan auto-detects the wire protocol from delegation.base_url (e.g. paths ending in /anthropic → anthropic_messages; Codex / native Anthropic / Kimi-coding hostnames keep their existing detection). For endpoints the heuristic can’t classify — for example Azure AI Foundry, MiniMax, Zhipu GLM, or LiteLLM proxies fronting an Anthropic-shaped backend — set delegation.api_mode explicitly to one of chat_completions, codex_responses, or anthropic_messages. Leave it empty (the default) to keep auto-detection.
The delegation provider uses the same credential resolution as CLI/gateway startup. All configured providers are supported: openrouter, nous, copilot, zai, kimi-coding, minimax, minimax-cn. When a provider is set, the system automatically resolves the correct base URL, API key, and API mode — no manual credential wiring needed.
Precedence: delegation.base_url in config → delegation.provider in config → parent provider (inherited). delegation.model in config → parent model (inherited). Setting just model without provider changes only the model name while keeping the parent’s credentials (useful for switching models within the same provider like OpenRouter).
One-shot runs: a finite mibyan chat -q / --oneshot session has no later turn to consume delegated results and no later session to learn for, so it runs a smaller footprint: skill_manage is not offered (skills are still listed and loadable with skill_view), the skills prompt asks for domain skills only rather than process skills, and oneshot_max_children caps the total subagents the run may spawn (default 2, 0 = unlimited). Past the cap delegate_task returns a tool error telling the agent to finish inline.
Width and depth: max_concurrent_children caps how many subagents run in parallel per batch (default 3, floor of 1, no ceiling). Can also be set via the DELEGATION_MAX_CONCURRENT_CHILDREN env var. When the model submits a tasks array longer than the cap, delegate_task returns a tool error explaining the limit rather than silently truncating. max_spawn_depth controls the delegation tree depth (clamped to 1-3). At the default 1, delegation is flat: children cannot spawn grandchildren, and passing role="orchestrator" silently degrades to leaf. Raise to 2 so orchestrator children can spawn leaf grandchildren; 3 for three-level trees. The agent opts into orchestration per call via role="orchestrator"; orchestrator_enabled: false forces every child back to leaf regardless. Cost scales multiplicatively — at max_spawn_depth: 3 with max_concurrent_children: 3, the tree can reach 3×3×3 = 27 concurrent leaf agents. See Subagent Delegation → Depth Limit and Nested Orchestration for usage patterns.
Child process notifications: background processes started by subagents route their completion/watch notifications to the parent conversation, but those are suppressed there by default — the child’s consolidated result is the deliverable. Set delegation.surface_child_process_notifications: true to deliver them (with subagent attribution). Delegation results themselves are never suppressed. See Subagent Delegation → Child background-process notifications.
Clarify
Configure how long Mibyan waits for a response to a clarifying question. One value covers every surface — the classic CLI modal, the TUI/Desktop card, and the messaging gateway. The canonical key isagent.clarify_timeout (default 3600 seconds; 0 or less = unlimited); a legacy top-level clarify.timeout is still honored if explicitly set:
"outcome": "timed_out" (answers the user already locked are kept) and continues on its own. A clarify prompt is never cut by the generic per-tool deadline (timeouts.tools.sequential_call); only agent.clarify_timeout bounds the wait.
Context Files (SOUL.md, AGENTS.md)
Mibyan uses two different context scopes:- SOUL.md is the agent’s primary identity. It occupies slot #1 in the system prompt, completely replacing the built-in default identity. Edit it to fully customize who the agent is.
- If SOUL.md is missing, empty, or cannot be loaded, Mibyan falls back to a built-in default identity.
- Project context files use a priority system — only ONE type is loaded (first match wins):
.mibyan.md→AGENTS.md→CLAUDE.md→.cursorrules. SOUL.md is always loaded independently. - AGENTS.md is hierarchical: if subdirectories also have AGENTS.md, all are combined.
- Mibyan automatically seeds a default
SOUL.mdif one does not already exist. - All loaded context files are capped at
context_file_max_charscharacters (default 20,000) with smart truncation.
Working Directory
Override the working directory:
MESSAGING_CWD and direct TERMINAL_CWD entries in ~/.mibyan/.env are legacy compatibility fallbacks. New configurations should use terminal.cwd.
Network
Connectivity workarounds for outbound HTTP:force_ipv4 — on servers with broken or unreachable IPv6, Python resolves AAAA records first and can hang for the full TCP timeout before falling back to IPv4. Mibyan already races IPv6 and IPv4 for every outbound connection it makes (Happy Eyeballs, RFC 8305: the IPv4 attempt starts 250 ms after IPv6 and whichever connects first wins), so an advertised-but-blackholed IPv6 route costs about a quarter second per connection instead of the full timeout. This covers the gateway’s WebSocket dials (relay connector, platform adapters) as well as HTTP. Set this to true only when you want to skip IPv6 entirely and connect over IPv4 directly. mibyan doctor runs an IPv6 route check that detects a dead IPv6 path and points at this setting.
Onboarding
First-touch onboarding hints and the structured profile-build offer:profile_build— controls the profile-build path offered on a profile’s first direct message through the gateway (never in a group chat)."ask"(default) offers to build a user profile; the offer is opt-in and consent-gated — the agent asks before any lookup and never reads connected accounts silently."off"shows a plain intro only. The offer fires at most once per profile.seen— internal state. Mibyan latches each shown hint here so it never fires again; the profile-build offer is also recorded here once shown. Don’t hand-edit it — wipe the wholeonboardingsection if you want to re-see all hints.
Dashboard
Configuration for the web dashboard — visual theme, public URL, and authentication providers. The auth providers (OAuth, basic password, drain) are documented in detail on the web-dashboard page; this is theconfig.yaml shape.
theme— dashboard visual theme.show_token_analytics— off by default. The Analytics page and token/cost figures are a local lower-bound estimate (they exclude auxiliary calls, retries, fallbacks, and cache writes), so they can read far below the provider bill. Settrueonly if you understand they’re not billing.public_url— when set, this is the complete authority (scheme + host + optional path prefix) the OAuthredirect_uriis built from. Set it for deploys behind reverse proxies that don’t reliably forwardX-Forwarded-*headers. Leave empty to use proxy-header reconstruction.trusted_proxies— IP addresses or bounded CIDR networks allowed to supplyX-Forwarded-ProtoandX-Forwarded-For. Loopback remains trusted automatically. Configure this when the TLS reverse proxy connects from another container or host. Prefer the proxy’s exact IP; use a small dedicated network only when its address is dynamic. Wildcards and/0networks are rejected.oauth/basic_auth/drain_auth— auth provider config read by the bundled dashboard-auth plugins. The drain secret itself is not set here; it’s provisioned via themibyan_DASHBOARD_DRAIN_SECRETenv var. See Web Dashboard for full auth setup.ws_ping_interval/ws_ping_timeout— WebSocket keepalive tuning for non-loopback binds (loopback connections never ping). Raise these on high-latency links (Tailscale, distant SSH tunnels) where the 20 s defaults can manufacture spurious 1006 disconnects.ssh_isolated_idle_grace_s(default900) — a Desktop-ownedmibyan serve --isolatedbackend reached over SSH is detached from the SSH session on purpose, so a laptop that sleeps mid-connection cannot tear it down; each dark-wake reconnect used to leave another backend holdingstate.db. The backend now retires itself once no client WebSocket has been connected for this long and no agent turn is running (a turn keeps it alive; an unreadable turn state keeps it alive too). Set high if you rely on a detached backend finishing long work after the laptop sleeps. Such backends also send a slow WebSocket ping (60 s, 10 min timeout) so a half-open tunnel is noticed.ws_orphan_reap_grace_s— how long a WS-detached session waits before the orphan reaper collects it. Raise alongside the keepalive values if clients reconnect slowly. Periodic session maintenance also completes cleanup for closed sockets and re-arms a missing orphan timer, so a detached chat cannot keep its ownership lease solely because its initial cleanup or timer was lost. Reconnecting cancels that timer; active delegated work and healthy running turns remain protected by the normal orphan-reaper checks. (mibyan_TUI_WS_ORPHAN_REAP_GRACE_Sremains as an internal override.)ws_orphan_activity_stale_s(default600) — how long a detached running turn’s activity clock (the same clock theagent.turn_livenesswatchdog samples: API waits, stream tokens, tool heartbeats) must be idle before the orphan reaper interrupts it. A client-absent turn that is still actively producing keeps running to completion detached — closing the laptop, backgrounding the mobile app, or a desktop update no longer cancels healthy long turns; only a genuinely wedged turn is interrupted. Set0to interrupt at the grace window regardless of activity (old behavior).startup_orphan_sweep(defaulttrue) — the WS-orphan reap timer above is in-process, so a gateway restart (update, crash, systemd) before it fires leaves the session row open forever — phantom “active” work in/resumeand dashboards. On every gateway boot — both the stdio TUI (entry.main) and the desktop/dashboard WebSocket sidecar (handle_ws) — rows with sourcetui/desktop/subagent/unknown(a row the token-accounting guard had to materialize itself) whose start time and newest message are both older than the session TTL (mibyan_TUI_SESSION_TTL_S, default 6 hours) are closed withend_reason: startup_orphan_reap. Messaging-platform sessions (Telegram, Discord, …) are never touched, live in-memory sessions (a client that already resumed) are excluded, and swept sessions remain resumable.

