Files
codewhale/docs/CONFIGURATION.md
T
Hunter Bown ad8064b143 chore(v0.8.8): stabilization batch — sub-agent caps, mutex contention, RLM polish, CI cleanup
Bundles the v0.8.8 stabilization fixes that were already implemented in the
working tree, plus the workflow/doc reconciliation called out in #507.

### Sub-agent runtime fixes
- **#509** Default sub-agent cap raised to 10 (configurable via
  `[subagents].max_concurrent` in `config.toml`, hard ceiling 20). The
  running-count calculation now ignores non-running, no-handle, and finished
  handles so completed agents stop counting against the cap.
- **#510** `SharedSubAgentManager` is now `Arc<RwLock<...>>`; the read paths
  that previously held a `Mutex` for inspection now take a read lock,
  eliminating the multi-agent fan-out UI freeze.
- **#511** `compact_tool_result_for_context` summarizes `agent_result` /
  `agent_wait` payloads before they are folded into the parent context.
- **#512** RLM tool cards map to `ToolFamily::Rlm` and render `rlm`, not
  `swarm`. Stale "swarm" wording cleaned in docs/comments/tests.
- **#513** (foreground stopgap only) Foreground RLM work is visible in the
  Agents sidebar projection. Full async RLM lifecycle remains v0.8.9 — the
  issue stays open with a refined scope.

### TUI / UX fixes
- **#487** Offline composer queue is now session-scoped; legacy unscoped
  queues fail closed.
- **#488** Composer Option+Backspace deletes by word; cross-platform key
  routing helpers added.
- **#443/#444** Keyboard enhancement flags pop on normal AND panic exit; the
  raw-mode startup probe is now bounded by a configurable timeout.
- **#449** Production footer reads statusline colors from `app.ui_theme`
  rather than the bespoke palette.
- **#506** `display_path_with_home` no longer mutates `HOME` in tests; the
  flake on shared-env CI is gone.

### Self-update / packaging
- **#503** `update.rs` arch mapping uses release-asset naming (`arm64`/`x64`)
  instead of the raw Rust constants. The platform-asset selector also rejects
  `.sha256` siblings as primary binaries. Tests now live alongside the source
  in `mod tests` (the `#[path]`-based integration test was removed because it
  duplicated test runs and forced a `pub(crate)` helper that no real caller
  used).
- **`Max 5 in flight` wording updated** in `agent_spawn` description,
  `prompts/base.md`, and `docs/TOOL_SURFACE.md` so the model sees the real
  default cap (10) and the configuration knob name.

### CI / release docs (#507)
- Pruned three duplicated/dead workflows: `crates-publish.yml`, `parity.yml`,
  `publish-npm.yml`. Their gates already run in `ci.yml` for every push/PR.
- `release.yml` build job now allows `parity` to be skipped (it only runs on
  tag push), unblocking `workflow_dispatch` reruns. The job still fails
  closed on a real parity failure.
- `RELEASE_RUNBOOK.md` reconciled: crate publishing is documented as the
  manual `scripts/release/publish-crates.sh` flow (no automated workflow);
  references to the deleted workflows removed.
- `CLAUDE.md` notes the `RELEASE_TAG_PAT` requirement for the auto-tag →
  release.yml chain (without it, the tag is created but `release.yml` does
  not fire) and documents the `workflow_dispatch` parity-skip behavior.

### Docs
- `docs/COMPETITIVE_ANALYSIS.md` added — capability matrix vs OpenCode and
  Codex CLI, gap analysis, and recommended implementation order.

### Verification (this branch)
- `cargo fmt --all -- --check` ✓
- `cargo check --workspace --all-targets --locked` ✓
- `cargo clippy --workspace --all-targets --all-features --locked -- -D warnings` ✓
- `cargo test --workspace --all-features --locked` ✓ (1809 + supporting)
- Parity gates ✓ (snapshot, parity_protocol, parity_state)
- `cargo build --release --locked -p deepseek-tui-cli -p deepseek-tui` ✓
- Lockfile drift guard ✓
- `deepseek doctor --json` clean
- `deepseek eval` (offline harness) success=true, 0 tool errors

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 01:57:37 -05:00

22 KiB

Configuration

DeepSeek TUI reads configuration from a TOML file plus environment variables. At process startup it also loads a workspace-local .env file when present. Use the tracked .env.example as the template; copy it to .env, then edit only the provider and safety knobs you need.

Where It Looks

Default config path:

  • ~/.deepseek/config.toml

Overrides:

  • CLI: deepseek --config /path/to/config.toml
  • Env: DEEPSEEK_CONFIG_PATH=/path/to/config.toml

If both are set, --config wins. Environment variable overrides are applied after the file is loaded.

The deepseek facade and deepseek-tui binary share the same config file for DeepSeek auth and model defaults. deepseek login --api-key ... writes the root api_key field that deepseek-tui reads directly, and deepseek --model deepseek-v4-flash is forwarded to the TUI as DEEPSEEK_MODEL.

For hosted or self-hosted DeepSeek V4 providers, set provider = "nvidia-nim", "fireworks", or "sglang" or pass deepseek --provider <name>. The facade stores provider credentials under [providers.<name>] and forwards the resolved key, base URL, provider, and model to the TUI process. Use deepseek auth set --provider nvidia-nim --api-key "YOUR_NVIDIA_API_KEY" or deepseek auth set --provider fireworks --api-key "YOUR_FIREWORKS_API_KEY" to save hosted-provider keys through the facade. SGLang is self-hosted and can run without an API key by default.

To bootstrap MCP and skills directories at their resolved paths, run deepseek-tui setup. To only scaffold MCP, run deepseek-tui mcp init.

Note: setup, doctor, mcp, features, sessions, resume/fork, exec, review, and eval are subcommands of the deepseek-tui binary. The deepseek dispatcher exposes a distinct set of commands (auth, config, model, thread, sandbox, app-server, mcp-server, completion) and forwards plain prompts to deepseek-tui.

Profiles

You can define multiple profiles in the same file:

api_key = "PERSONAL_KEY"
default_text_model = "deepseek-v4-pro"

[profiles.work]
api_key = "WORK_KEY"
base_url = "https://api.deepseek.com"

[profiles.nvidia-nim]
provider = "nvidia-nim"
api_key = "NVIDIA_KEY"
base_url = "https://integrate.api.nvidia.com/v1"
default_text_model = "deepseek-ai/deepseek-v4-pro"

[profiles.fireworks]
provider = "fireworks"
default_text_model = "accounts/fireworks/models/deepseek-v4-pro"

[profiles.sglang]
provider = "sglang"
base_url = "http://localhost:30000/v1"
default_text_model = "deepseek-ai/DeepSeek-V4-Pro"

Select a profile with:

  • CLI: deepseek --profile work
  • Env: DEEPSEEK_PROFILE=work

If a profile is selected but missing, DeepSeek TUI exits with an error listing available profiles.

Environment Variables

These override config values:

  • DEEPSEEK_API_KEY
  • DEEPSEEK_BASE_URL
  • DEEPSEEK_PROVIDER (deepseek|nvidia-nim|openrouter|novita|fireworks|sglang)
  • DEEPSEEK_MODEL or DEEPSEEK_DEFAULT_TEXT_MODEL
  • NVIDIA_API_KEY or NVIDIA_NIM_API_KEY (preferred when provider is nvidia-nim; falls back to DEEPSEEK_API_KEY)
  • NVIDIA_NIM_BASE_URL, NIM_BASE_URL, or NVIDIA_BASE_URL
  • NVIDIA_NIM_MODEL
  • FIREWORKS_API_KEY
  • FIREWORKS_BASE_URL
  • SGLANG_BASE_URL
  • SGLANG_MODEL
  • SGLANG_API_KEY (optional; many localhost SGLang servers do not require auth)
  • DEEPSEEK_LOG_LEVEL or RUST_LOG (info/debug/trace enables lightweight verbose logs)
  • DEEPSEEK_SKILLS_DIR
  • DEEPSEEK_MCP_CONFIG
  • DEEPSEEK_NOTES_PATH
  • DEEPSEEK_MEMORY_PATH
  • DEEPSEEK_ALLOW_SHELL (1/true enables)
  • DEEPSEEK_APPROVAL_POLICY (on-request|untrusted|never)
  • DEEPSEEK_SANDBOX_MODE (read-only|workspace-write|danger-full-access|external-sandbox)
  • DEEPSEEK_MANAGED_CONFIG_PATH
  • DEEPSEEK_REQUIREMENTS_PATH
  • DEEPSEEK_MAX_SUBAGENTS (clamped to 1..=20)
  • DEEPSEEK_TASKS_DIR (runtime task queue/artifact storage, default ~/.deepseek/tasks)
  • DEEPSEEK_ALLOW_INSECURE_HTTP (1/true allows non-local http:// base URLs; default is reject)
  • DEEPSEEK_CAPACITY_ENABLED
  • DEEPSEEK_CAPACITY_LOW_RISK_MAX
  • DEEPSEEK_CAPACITY_MEDIUM_RISK_MAX
  • DEEPSEEK_CAPACITY_SEVERE_MIN_SLACK
  • DEEPSEEK_CAPACITY_SEVERE_VIOLATION_RATIO
  • DEEPSEEK_CAPACITY_REFRESH_COOLDOWN_TURNS
  • DEEPSEEK_CAPACITY_REPLAN_COOLDOWN_TURNS
  • DEEPSEEK_CAPACITY_MAX_REPLAY_PER_TURN
  • DEEPSEEK_CAPACITY_MIN_TURNS_BEFORE_GUARDRAIL
  • DEEPSEEK_CAPACITY_PROFILE_WINDOW
  • DEEPSEEK_CAPACITY_PRIOR_CHAT
  • DEEPSEEK_CAPACITY_PRIOR_REASONER
  • DEEPSEEK_CAPACITY_PRIOR_V4_PRO
  • DEEPSEEK_CAPACITY_PRIOR_V4_FLASH
  • DEEPSEEK_CAPACITY_PRIOR_FALLBACK

Settings File (Persistent UI Preferences)

DeepSeek TUI also stores user preferences in:

  • ~/.config/deepseek/settings.toml

Notable settings include auto_compact (default false), which opts into replacement-style summarization only near the active model limit. The default V4 path preserves the stable message prefix for cache reuse; use manual /compact or enable auto_compact only when you explicitly want automatic replacement compaction. You can inspect or update these from the TUI with /settings and /config (interactive editor).

Common settings keys:

  • theme (default, dark, light, whale)
  • auto_compact (on/off, default off)
  • paste_burst_detection (on/off, default on): fallback rapid-key paste detection for terminals that do not emit bracketed-paste events. This is independent of terminal bracketed-paste mode.
  • show_thinking (on/off)
  • show_tool_details (on/off)
  • locale (auto, en, ja, zh-Hans, pt-BR; default auto): UI chrome locale. auto checks LC_ALL, LC_MESSAGES, then LANG; unsupported or missing locales fall back to English. This does not force model output language.
  • default_mode (agent, plan, yolo; legacy normal is accepted and normalized to agent)
  • max_history (number of submitted input history entries; cleared drafts are also kept locally for composer history search)
  • default_model (model name override)

Only agent, plan, and yolo are visible modes in the UI. For compatibility, older settings files with default_mode = "normal" still load as agent, and the hidden /normal slash command switches to Agent.

Localization scope is tracked in LOCALIZATION.md. The v0.7.6 core pack covers high-visibility TUI chrome only; provider/tool schemas, personality prompts, and full documentation remain English unless explicitly translated later.

Readability semantics:

  • Selection uses a unified style across transcript, composer menus, and modals.
  • Footer hints use a dedicated semantic role (FOOTER_HINT) so hint text stays readable across themes.
  • The footer includes a compact coherence chip that describes how stable and focused the current session is right now. Possible states are healthy, crowded, refreshing, verifying, and resetting; these are derived from capacity and compaction events without exposing internal formulas in normal UI.

Token Quantities and Drivers

DeepSeek V4 prefix caching makes token labels matter. These quantities are kept separate:

Quantity Meaning Allowed to drive
Active request input estimate Conservative estimate of the next request's live system prompt and transcript payload. Header/footer context percent, hard-cycle trigger, opt-in Flash seam trigger, and emergency overflow preflight.
Reserved response headroom The requested max_tokens budget plus safety headroom. v0.7.5 keeps normal turns at 262144 output tokens and adds 1024 safety tokens for context-window checks. Hard-cycle and emergency overflow budget checks only.
Cumulative API usage Provider-reported input plus output tokens summed across completed API calls; multi-tool turns may count the same stable prefix more than once. Session usage and approximate cost telemetry only.
Prompt cache hit/miss Provider cache telemetry for the most recent call when available. Cache-hit display and cost estimation only; never compaction, seam, or cycle triggers.
Context percent Active request input estimate divided by the model context window. Display only; it mirrors the active-input basis used by context safeguards.
Cost estimate Approximate spend from provider usage and configured DeepSeek rates. Display only.

For the default V4 path, hard cycles fire when active input reaches the smaller of the configured cycle threshold (768000) and the model window minus reserved response headroom. Replacement compaction remains opt-in (auto_compact = false by default), the Flash seam manager remains opt-in ([context].enabled = false), and the capacity controller remains disabled unless configured.

Command Migration Notes

If you are upgrading from older releases:

  • Old: /deepseek New: /links (aliases: /dashboard, /api)
  • Old: /set model deepseek-reasoner New: /config and edit the model row to deepseek-v4-pro or deepseek-v4-flash
  • Old: visible Normal mode or default_mode = "normal" New: use Agent / default_mode = "agent"; legacy normal still maps to agent
  • Old: discover /set in slash UX/help New: use /config for editing and /settings for read-only inspection

Key Reference

Core keys (used by the TUI/engine)

  • provider (string, optional): deepseek (default), nvidia-nim, openrouter, novita, fireworks, or sglang. nvidia-nim targets NVIDIA's NIM-hosted DeepSeek endpoints through https://integrate.api.nvidia.com/v1; fireworks targets https://api.fireworks.ai/inference/v1; sglang targets a self-hosted OpenAI-compatible endpoint, defaulting to http://localhost:30000/v1.
  • api_key (string, required): must be non-empty (or set DEEPSEEK_API_KEY).
  • base_url (string, optional): defaults to https://api.deepseek.com for DeepSeek's OpenAI-compatible Chat Completions API, or https://integrate.api.nvidia.com/v1 for provider = "nvidia-nim". https://api.deepseek.com/v1 is also accepted for SDK compatibility; use https://api.deepseek.com/beta only for DeepSeek beta features such as strict tool mode, chat prefix completion, and FIM completion.
  • default_text_model (string, optional): defaults to deepseek-v4-pro for DeepSeek, deepseek-ai/deepseek-v4-pro for NVIDIA NIM, accounts/fireworks/models/deepseek-v4-pro for Fireworks, and deepseek-ai/DeepSeek-V4-Pro for SGLang. Current public DeepSeek IDs are deepseek-v4-pro and deepseek-v4-flash, both with 1M context windows and thinking mode enabled by default. Legacy deepseek-chat and deepseek-reasoner remain compatibility aliases for deepseek-v4-flash. Provider-specific mappings translate deepseek-v4-pro / deepseek-v4-flash to each provider's model ID where supported. Use /models or deepseek models to discover live IDs from your configured endpoint. DEEPSEEK_MODEL overrides this for a single process.
  • reasoning_effort (string, optional): off, low, medium, high, or max; defaults to the configured UI tier. DeepSeek Platform receives top-level thinking / reasoning_effort fields. NVIDIA NIM receives equivalent settings through chat_template_kwargs.
  • allow_shell (bool, optional): defaults to true (sandboxed).
  • approval_policy (string, optional): on-request, untrusted, or never. Runtime approval_mode editing in /config also accepts on-request and untrusted aliases.
  • sandbox_mode (string, optional): read-only, workspace-write, danger-full-access, external-sandbox.
  • managed_config_path (string, optional): managed config file loaded after user/env config.
  • requirements_path (string, optional): requirements file used to enforce allowed approval/sandbox values.
  • max_subagents (int, optional): defaults to 10 and is clamped to 1..=20.
  • subagents.* (optional): per-role/type model defaults for agent_spawn and related sub-agent tools. Explicit tool model values win, then role/type overrides, then the parent runtime model. Supported convenience keys are default_model, worker_model, explorer_model, awaiter_model, review_model, custom_model, and max_concurrent. The [subagents] max_concurrent value overrides top-level max_subagents and is also clamped to 1..=20. [subagents.models] accepts lower-case role or type keys such as worker, explorer, general, explore, plan, and review. Values must normalize to a supported DeepSeek model id before an agent is spawned.
  • skills_dir (string, optional): defaults to ~/.deepseek/skills (each skill is a directory containing SKILL.md). Workspace-local .agents/skills or ./skills are preferred when present.
  • mcp_config_path (string, optional): defaults to ~/.deepseek/mcp.json. It is visible in /config and can be changed from the TUI. The new path is used immediately by /mcp, but rebuilding the model-visible MCP tool pool requires restarting the TUI.
  • notes_path (string, optional): defaults to ~/.deepseek/notes.txt and is used by the note tool.
  • memory_path (string, optional): defaults to ~/.deepseek/memory.md.
  • snapshots.* (optional): side-git workspace snapshots for file rollback:
    • [snapshots].enabled (bool, default true)
    • [snapshots].max_age_days (int, default 7)
    • snapshots live under ~/.deepseek/snapshots/<project_hash>/<worktree_hash>/.git and never use the workspace's own .git directory
  • context.* (optional): append-only Flash seam manager, currently opt-in. Thresholds use the active request input estimate, not lifetime summed API usage:
    • [context].enabled (bool, default false)
    • [context].verbatim_window_turns (int, default 16)
    • [context].l1_threshold (int, default 192000)
    • [context].l2_threshold (int, default 384000)
    • [context].l3_threshold (int, default 576000)
    • [context].cycle_threshold (int, default 768000)
    • [context].seam_model (string, default deepseek-v4-flash)
  • retry.* (optional): retry/backoff settings for API requests:
    • [retry].enabled (bool, default true)
    • [retry].max_retries (int, default 3)
    • [retry].initial_delay (float seconds, default 1.0)
    • [retry].max_delay (float seconds, default 60.0)
    • [retry].exponential_base (float, default 2.0)
  • capacity.* (optional): runtime context-capacity controller. This is opt-in because its active interventions can rewrite the live transcript.
    • [capacity].enabled (bool, default false)
    • [capacity].low_risk_max (float, default 0.50)
    • [capacity].medium_risk_max (float, default 0.62)
    • [capacity].severe_min_slack (float, default -0.25)
    • [capacity].severe_violation_ratio (float, default 0.40)
    • [capacity].refresh_cooldown_turns (int, default 6)
    • [capacity].replan_cooldown_turns (int, default 5)
    • [capacity].max_replay_per_turn (int, default 1)
    • [capacity].min_turns_before_guardrail (int, default 4)
    • [capacity].profile_window (int, default 8)
    • [capacity].deepseek_v3_2_chat_prior (float, default 3.9)
    • [capacity].deepseek_v3_2_reasoner_prior (float, default 4.1)
    • [capacity].deepseek_v4_pro_prior (float, default 3.5)
    • [capacity].deepseek_v4_flash_prior (float, default 4.2)
    • [capacity].fallback_default_prior (float, default 3.8)
  • tui.alternate_screen (string, optional): auto, always, or never. auto disables the alternate screen in Zellij; --no-alt-screen forces inline mode. Set never or run with --no-alt-screen when you want real terminal scrollback.
  • tui.mouse_capture (bool, optional, default true when the alternate screen is active): enable internal mouse scrolling, transcript selection, and right-click context actions. TUI-owned drag selection copies only user/assistant transcript text. Set this to false or run with --no-mouse-capture for raw terminal selection.
  • tui.terminal_probe_timeout_ms (int, optional, default 500): startup terminal-mode probe timeout in milliseconds. Values are clamped to 100..=5000; timeout emits a warning and aborts startup instead of hanging indefinitely.
  • hooks (optional): lifecycle hooks configuration (see config.example.toml).
  • features.* (optional): feature flag overrides (see below).

Parsed but currently unused (reserved for future versions)

These keys are accepted by the config loader but not currently used by the interactive TUI or built-in tools:

  • tools_file

Feature Flags

Feature flags live under the [features] table and are merged across profiles. Defaults are enabled for built-in tooling, so you only need to set entries you want to force on or off.

[features]
shell_tool = true
subagents = true
web_search = true # enables canonical web.run plus the compatibility web_search alias
apply_patch = true
mcp = true
exec_policy = true

You can also override features for a single run:

  • deepseek-tui --enable web_search
  • deepseek-tui --disable subagents

Use deepseek-tui features list to inspect known flags and their effective state.

Local Media Attachments

Use @path/to/file in the composer to add local text file or directory context to the next message. Use /attach <path> for local image/video media paths, or Ctrl+V to attach an image from the clipboard. DeepSeek's public Chat Completions API currently accepts text message content, so media attachments are sent as explicit local path references instead of native image/video payloads. Attachment rows appear above the composer before submit; move to the start of the composer, press to select an attachment row, then press Backspace or Delete to remove it without editing the placeholder text by hand.

Managed Configuration and Requirements

DeepSeek TUI supports a policy layering model:

  1. user config + profile + env overrides
  2. managed config (if present)
  3. requirements validation (if present)

By default on Unix:

  • managed config: /etc/deepseek/managed_config.toml
  • requirements: /etc/deepseek/requirements.toml

Requirements file shape:

allowed_approval_policies = ["on-request", "untrusted", "never"]
allowed_sandbox_modes = ["read-only", "workspace-write"]

If configured values violate requirements, startup fails with a descriptive error.

See docs/capacity_controller.md for formulas, intervention behavior, and telemetry.

Notes On deepseek-tui doctor

deepseek-tui doctor follows the same config resolution rules as the rest of the TUI. That means --config / DEEPSEEK_CONFIG_PATH are respected, and MCP/skills checks use the resolved mcp_config_path / skills_dir (including env overrides).

To bootstrap missing MCP/skills paths, run deepseek-tui setup --all. You can also run deepseek-tui setup --skills --local to create a workspace-local ./skills dir.

deepseek-tui doctor --json prints a machine-readable report that skips the live API connectivity probe. Top-level keys: version, config_path, config_present, workspace, api_key.source, base_url, default_text_model, mcp, skills, tools, plugins, sandbox, platform, api_connectivity, capability. CI consumers should rely on api_key.source (env/config/missing) rather than parsing the human-readable doctor text.

The capability key contains per-provider capability info derived from static knowledge (release docs, API guides) rather than live API probes. Top-level sub-keys: resolved_provider, resolved_model, context_window, max_output, thinking_supported, cache_telemetry_supported, request_payload_mode, and deprecation. When the resolved model is a known legacy alias (e.g. deepseek-chat, deepseek-reasoner), the deprecation sub-object carries alias, replacement, and notice fields.

Use capability.context_window and capability.max_output for context-window budgeting in CI scripts. Use capability.thinking_supported to decide whether to configure reasoning effort. Use capability.deprecation to warn users about legacy model aliases.

Setup status, clean, and extension dirs

deepseek-tui setup accepts a few flags beyond the existing --mcp, --skills, --local, --all, and --force:

  • --status — print a compact one-screen status (api key, base URL, model, MCP/skills/tools/plugins counts, sandbox, .env presence). Read-only and network-free; safe to run in CI. If .env is missing and .env.example is present in the workspace, the status output points at cp .env.example .env.
  • --tools — scaffold ~/.deepseek/tools/ with a README.md describing the self-describing frontmatter convention (# name: / # description: / # usage:) and an example.sh that follows it. The directory is intentionally not auto-loaded; wire individual scripts into the agent via MCP, hooks, or skills.
  • --plugins — scaffold ~/.deepseek/plugins/ with a README.md and an example/PLUGIN.md placeholder using the same frontmatter shape as SKILL.md. Plugins are not loaded automatically either; reference them from a skill or MCP wrapper when you want them active.
  • --all now scaffolds MCP + skills + tools + plugins together.
  • --clean — list ~/.deepseek/sessions/checkpoints/latest.json and offline_queue.json if they exist. Pass --force to actually remove them. This never touches real session history or the task queue.

--status and --clean are mutually exclusive with the scaffold flags.

Why the engine strips XML/[TOOL_CALL] text

DeepSeek TUI sends and receives tool calls only over the API tool channel (structured tool_use / tool_call items). The streaming loop in crates/tui/src/core/engine.rs recognizes a fixed set of fake-wrapper start markers — [TOOL_CALL], <deepseek:tool_call, <tool_call, <invoke , <function_calls> — and scrubs them from visible assistant text without ever turning them into structured tool calls. When a wrapper is stripped, the loop emits one compact status notice per turn so the user can see why their visible text shrank. Treat any change that re-enables text-based tool execution as a regression; the protocol-recovery tests in crates/tui/tests/protocol_recovery.rs lock the contract.