diff --git a/hermes_cli/config_defaults.py b/hermes_cli/config_defaults.py index 0dc0776bcb..d3dbe0d585 100644 --- a/hermes_cli/config_defaults.py +++ b/hermes_cli/config_defaults.py @@ -4,551 +4,325 @@ Pure-data leaf module: DEFAULT_CONFIG and OPTIONAL_ENV_VARS, extracted verbatim hermes_cli/config.py. Must not import from hermes_cli.config. """ + +def _aux(timeout, *, reasoning_effort=True, **extra): + """Standard auxiliary-task model block (see DEFAULT_CONFIG["auxiliary"]). + + reasoning_effort=False omits that key (MoA blocks configure depth per slot); + ``extra`` keys are appended after the standard ones. + """ + d = {"provider": "auto", "model": "", "base_url": "", "api_key": "", "timeout": timeout, "extra_body": {}} + if reasoning_effort: + d["reasoning_effort"] = "" + d.update(extra) + return d + + DEFAULT_CONFIG = { "model": "", "providers": {}, "fallback_providers": [], "credential_pool_strategies": {}, "toolsets": ["hermes-cli"], - # SQLite journal mode used by every Hermes database opener. WAL is the - # normal default; set DELETE for weak-fsync/shared filesystems where WAL is - # not crash-safe (for example macOS virtiofs, NFS, or SMB). + # journal_mode: SQLite journal mode for every Hermes DB. "wal" default; use "delete" on + # weak-fsync/shared filesystems where WAL is not crash-safe (macOS virtiofs, NFS, SMB). "database": { "journal_mode": "wal", - # Optional WAL sizing pragmas, applied when set to integers. - # None = SQLite defaults (autocheckpoint 1000 pages, no size limit). + # WAL sizing pragmas (ints). None = SQLite defaults (autocheckpoint 1000 pages, no limit). "wal_autocheckpoint": None, "journal_size_limit": None, }, - # Soft file-descriptor limit for long-running Hermes server processes. - # Clamped to the OS hard limit; 0/false/null disables the adjustment. - "runtime": { - "nofile_soft_limit": 4096, - }, - # Global active chat session cap across CLI, TUI/dashboard, and messaging. - # None/0 = unbounded. + # Soft fd limit for long-running server processes; clamped to OS hard limit. 0/false/null = off. + "runtime": {"nofile_soft_limit": 4096}, + # Global active chat session cap across CLI, TUI/dashboard, and messaging. None/0 = unbounded. "max_concurrent_sessions": None, - # Soft LRU cap on in-memory TUI/desktop/dashboard sessions. When more than - # this many are live, the gateway evicts the least-recently-active DETACHED - # sessions (no live client) so accumulated agents don't pile up under memory - # pressure. Reopening one re-resumes it from disk. 0/null disables. + # Soft LRU cap on in-memory TUI/desktop/dashboard sessions. Above it the gateway evicts the + # least-recently-active DETACHED sessions (no live client); reopening re-resumes from disk. + # 0/null disables. "max_live_sessions": 16, "session": { - # Per-terminal `hermes -c`: each CLI session drops a breadcrumb file - # under $HERMES_HOME/terminal-sessions/, and a bare - # -c/--continue resumes THIS terminal's session (tmux pane, kitty - # window, wezterm pane, plain tty, ...) instead of the globally - # most-recent one. Set false to restore the old latest-session - # behavior everywhere. + # Per-terminal `hermes -c`: each CLI session writes a breadcrumb under + # $HERMES_HOME/terminal-sessions/, so bare -c/--continue resumes THIS + # terminal's session (tmux/kitty/wezterm pane, tty). false = resume globally most-recent. "terminal_continue": True, }, "agent": { - # Unlimited by default. The agent turn cap caused more problems than - # it solved (silent mid-task truncation). null = unlimited; set a - # positive integer to cap, or use "none"/"unlimited"/"inf"/0/-1 — - # all normalized by hermes_cli.config.resolve_turn_limit. + # Turn cap. null = unlimited (default; caps caused silent mid-task truncation). Positive + # int caps; "none"/"unlimited"/"inf"/0/-1 also mean unlimited (resolve_turn_limit). "max_turns": None, - # Optional wall-clock budget in seconds per conversation run. - # null/absent = feature fully off (zero behavior change). When set, - # the agent gets a one-time wrap-up notice at 80% elapsed and - # implicit provider stale timeouts are capped to the remaining - # budget. CLI one-shot equivalent: `hermes chat --run-budget N`. + # Wall-clock budget (seconds) per run. null = off. When set: one-time wrap-up notice at + # 80% elapsed; implicit provider stale timeouts capped to remaining budget. + # CLI equivalent: `hermes chat --run-budget N`. "run_budget_seconds": None, - # Inactivity timeout for gateway agent execution (seconds). - # The agent can run indefinitely as long as it's actively calling - # tools or receiving API responses. Only fires when the agent has - # been completely idle for this duration. 0 = unlimited. + # Gateway inactivity timeout (seconds). Only fires when the agent is completely idle — + # not while calling tools or receiving API responses. 0 = unlimited. "gateway_timeout": 1800, - # Maximum time an alias routing key waits for the active turn holding - # the same resolved session lease. On expiry the inbound message is - # rejected with a resend notice rather than run without serialization. - # Keep this short: Telegram dispatches updates sequentially, so an - # inline lease waiter also delays unrelated topics. Non-positive values - # fall back to the five-second safety default. + # Max seconds an alias routing key waits for the active turn holding the same session + # lease; on expiry the message is rejected with a resend notice. Keep short: Telegram + # dispatches sequentially, so a waiter delays unrelated topics. Non-positive -> 5s. "gateway_turn_lease_timeout": 5, - # Per-session AIAgent cache in the gateway. Each cached agent keeps a - # warm prompt prefix AND the session's full transcript, so the cache - # trades memory for cost: too small and every turn re-pays an uncached - # prompt, too large and tool-heavy transcripts fill the heap. + # Per-session AIAgent cache in the gateway. Each entry keeps a warm prompt prefix AND the + # full transcript: too small re-pays uncached prompts, too large fills the heap. "agent_cache": { - # LRU entry cap. - "max_size": 128, - # Evict an agent that has been idle this long (seconds). - "idle_ttl_secs": 3600, - # Anonymous-RSS budget (MB) above which the gateway starts shedding - # least-recently-used transcripts, which reload from the persisted - # session on the next turn. "auto" derives the budget from the - # cgroup memory limit the gateway runs under (or total RAM when - # uncapped); a number sets it explicitly; 0/off disables the pass - # and lets memory grow to whatever the two bounds above allow. + "max_size": 128, # LRU entry cap + "idle_ttl_secs": 3600, # evict agents idle this long + # Anonymous-RSS budget (MB) above which LRU transcripts are shed (reloaded from + # disk next turn). "auto" = derive from cgroup memory limit (or total RAM); + # number = explicit; 0/off = disable the pass. "memory_high_mb": "auto", - # Upper bound on how many sessions one pressure pass sheds, so a - # burst of teardowns cannot stall the gateway. + # Max sessions shed per pass (teardown bursts can't stall the gateway) and the number + # of most-recently-used sessions the pass never touches. "max_evictions_per_pass": 16, - # Most-recently-used sessions the pressure pass never touches — - # they are the ones actively paying for a warm prompt cache. "protect_recent": 8, }, - # Force-interrupt budget once gateway stop()/drain has begun - # (seconds). Applies to SIGTERM/external stop and to the final - # phase of in-band restart after any after-turn wait. 0 = interrupt - # immediately (the default). - # - # Keep this short and under systemd TimeoutStopSec — a long value - # here invites SIGKILL-mid-cleanup. For in-band restart - # (/restart, SIGUSR1), prefer restart_after_turn_timeout below so - # active turns finish *before* stop() begins (#77184). + # Force-interrupt budget (seconds) once gateway stop()/drain has begun (SIGTERM, and the + # final phase of in-band restart). 0 = interrupt immediately. Keep under systemd + # TimeoutStopSec or risk SIGKILL mid-cleanup; for /restart prefer + # restart_after_turn_timeout so turns finish BEFORE stop(). "restart_drain_timeout": 0, - # Cron-only floor under the stop()/drain wait (seconds). A chat turn - # interrupted by a restart is announced to the user and resumed on - # their next message; an interrupted cron run is written to jobs.json - # as a permanent failure that nobody is waiting on, so it must not - # inherit restart_drain_timeout's 0 (#82161). Clamped at runtime to - # the shutdown-watchdog leash minus teardown headroom, so raising it - # past ~50s has no effect unless TimeoutStopSec is raised too. - # 0 = opt out (cron drains on restart_drain_timeout, legacy). + # Cron-only floor under the stop()/drain wait (seconds). Interrupted chat turns resume on + # the next message, but an interrupted cron run is recorded as a permanent failure, so it + # must not inherit restart_drain_timeout's 0. Clamped to the shutdown-watchdog leash + # minus teardown headroom (~50s unless TimeoutStopSec is raised). 0 = opt out. "cron_drain_timeout": 30, - # In-band restart wait for active turns to finish before stop() - # (seconds). /restart and SIGUSR1 refuse new work, then wait up to - # this cap for in-flight agents/cron/api runs to complete naturally - # so the requesting turn is not amputated by restart_drain_timeout. - # 0 = legacy behaviour (enter stop()/drain immediately). Default - # 30 min is a safety valve for wedged agents, not a target latency — - # an interactive `hermes gateway restart` must never block for hours - # on a turn that wedged (#79133). Long unattended turns can raise - # this in config.yaml. + # In-band restart (/restart, SIGUSR1): refuse new work, then wait up to this many seconds + # for in-flight agents/cron/api runs to finish before stop(). 0 = enter stop() at once. + # 30 min is a safety valve for wedged agents, not a target; raise for long unattended turns. "restart_after_turn_timeout": 1800, - # Upper bound (seconds) a submitted prompt waits for the deferred - # agent build (MCP discovery, model metadata, skills scan) before - # failing with a visible error (#63078). The gateway's wait is - # patient — the prompt is delivered the moment the build completes - # and a progress notice is emitted past 30s — so this cap only fires - # on a genuinely hung build. Raise it for deployments with many slow - # or unreachable MCP servers. + # Max seconds a submitted prompt waits for the deferred agent build (MCP discovery, model + # metadata, skills scan) before failing visibly. The prompt is delivered as soon as the + # build completes (progress notice past 30s), so this only fires on a hung build. Raise + # for many slow/unreachable MCP servers. "build_wait_timeout": 600, - # Max app-level retry attempts for API errors (connection drops, - # provider timeouts, 5xx, etc.) before the agent surfaces the - # failure. The OpenAI SDK already does its own low-level retries - # (max_retries=2 default) for transient network errors; this is - # the Hermes-level retry loop that wraps the whole call. Lower - # this to 1 if you use fallback providers and want fast failover - # on flaky primaries; raise it if you prefer to tolerate longer - # provider hiccups on a single provider. + # Hermes-level retry attempts for API errors (connection drops, timeouts, 5xx) wrapping + # the whole call; the OpenAI SDK also retries transient errors (max_retries=2). Set 1 for + # fast failover to fallback providers; raise to tolerate longer provider hiccups. "api_max_retries": 3, - # Empty-response retry guard (NS-503). The empty-retry loop - # re-sends the full conversation input at full price on every - # attempt; these settings stop it from re-billing *deterministic* - # empties (unsignaled provider refusals with zero output tokens) - # while failing open on any ambiguous evidence (missing usage, - # any generated tokens, model/provider change mid-streak). + # Empty-response retry guard. Empty retries re-send the full input at full price; this + # stops re-billing deterministic empties (unsignaled refusals, zero output tokens) while + # failing open on ambiguous evidence (missing usage, any tokens, model/provider change). "empty_response_guard": { - # Master switch for both guards below. False restores the - # legacy fixed 3-retry behaviour unconditionally. - "enabled": True, - # When the estimated input cost of a single empty attempt - # meets or exceeds this many USD, the retry budget for the - # streak drops from 3 to 1. Unknown pricing or missing usage - # leaves the budget untouched. + "enabled": True, # False = legacy fixed 3 retries unconditionally + # When one empty attempt's estimated input cost >= this USD, the streak's retry + # budget drops from 3 to 1. Unknown pricing / missing usage leaves it untouched. "cost_threshold_usd": 0.25, }, - # Fast mode: "" / "normal" (off), "fast" (always), "auto" (first - # fast_auto_seconds of every turn), "cold" (first turn of a session only). + # Fast mode: "" / "normal" (off), "fast" (always), "auto" (first fast_auto_seconds of + # every turn), "cold" (first turn of a session only). "service_tier": "", "fast_auto_seconds": 60, - # Tool-use enforcement: injects system prompt guidance that tells the - # model to actually call tools instead of describing intended actions. - # Values: "auto" (default — applies to gpt/codex models), true/false - # (force on/off for all models), or a list of model-name substrings - # to match (e.g. ["gpt", "codex", "gemini", "qwen"]). + # System-prompt guidance telling the model to call tools instead of describing actions. + # "auto" = gpt/codex models; true/false = force for all models; or a list of model-name + # substrings (e.g. ["gpt", "codex", "gemini", "qwen"]). "tool_use_enforcement": "auto", - # Execution-discipline guidance: injects a system prompt block covering - # tool persistence, mandatory tool use for arithmetic/system facts, - # external-write read-back, count reconciliation, literal preservation - # of identifiers, and verification-gated completion. Chosen once at - # session start keyed on model name (prompt stays byte-stable). - # Values: "auto" (default — applies to gpt/codex/grok/deepseek/kimi/ - # qwen/glm/minimax/mimo/mistral models), true/false (force on/off for - # all models), or a list of model-name substrings to match. + # Execution-discipline prompt block (tool persistence, tools for arithmetic/system facts, + # read-back after external writes, count reconciliation, literal identifiers, + # verification-gated completion). Chosen once per session by model name (byte-stable). + # "auto" = gpt/codex/grok/deepseek/kimi/qwen/glm/minimax/mimo/mistral; true/false = force; + # or a list of model-name substrings. "execution_guidance": "auto", - # Intent-ack continuation: when the model opens a turn by narrating an - # action it will take ("I'll go check the logs...") but emits no tool - # call, intercept the turn-end, inject a "continue now, execute the - # tools" nudge, and loop instead of ending the turn (capped at 2 nudges - # per turn). This is the corrective sibling of tool_use_enforcement (the - # preventive prompt-side guard). Values: "auto" (default — fires only on - # the codex_responses api_mode, the historical behavior), true (all - # api_modes — fixes the Gemini/Claude "stops after stating intent" case), - # false (never), or a list of model-name substrings to match. + # When the model narrates an action ("I'll go check the logs...") but emits no tool call, + # inject a "continue now, execute the tools" nudge and loop (max 2 nudges/turn). Corrective + # sibling of tool_use_enforcement. "auto" = codex_responses api_mode only; true = all + # api_modes (fixes Gemini/Claude "stops after stating intent"); false = never; or a list + # of model-name substrings. "intent_ack_continuation": "auto", - # Runtime anti-stall guards. When True (default), two conservative - # guards run: (1) an identical-call loop breaker that appends a short - # notice to the tool result when the same tool is called 3+ consecutive - # times with identical arguments AND identical results (never blocks; - # pollers like `process` are exempt), and (2) a continue-intent - # extension of the empty-response recovery that re-prompts once when - # the model ends its turn saying it will continue but takes no action. - # Set False to disable both. + # Anti-stall guards: (1) identical-call loop breaker appends a notice when the same tool + # is called 3+ times with identical args AND results (never blocks; pollers like `process` + # exempt); (2) continue-intent extension of empty-response recovery re-prompts once when + # the model says it will continue but takes no action. False disables both. "stall_guards": True, - # Universal "finish the job" guidance — short prompt block applied to - # all models that targets two cross-family failure modes: (1) stopping - # after a stub instead of finishing the artifact, (2) fabricating - # plausible-looking output when a real path is blocked. Costs ~80 - # tokens in the cached system prompt. Set False to disable globally. + # "Finish the job" prompt block for all models: don't stop at a stub, never fabricate output + # when the real path is blocked. ~80 cached tokens. False disables. "task_completion_guidance": True, - # Universal parallel-tool-call guidance — short prompt block applied to - # all models that tells the model to batch independent tool calls - # (reads, searches, web fetches, read-only commands) into one turn - # instead of one call per turn. The runtime already runs independent - # calls concurrently, so this just steers the model to produce the - # batch — cutting round-trips and the resent-context cost that - # compounds over a long conversation. Costs ~70 tokens in the cached - # system prompt. Set False to disable globally. + # Prompt block for all models steering independent tool calls (reads, searches, fetches, + # read-only commands) into one batched turn; the runtime already runs them concurrently. + # ~70 cached tokens. False disables. "parallel_tool_call_guidance": True, - # Local-environment toolchain probe — surfaces Python/pip/uv/PEP-668 - # state in the system prompt when something non-default is detected - # (e.g. python3 has no pip module, pip→python version mismatch, PEP - # 668 enforcement without uv). Costs zero tokens when the env is - # clean (probe emits nothing). Skipped for remote terminal backends - # (docker/modal/ssh — they have their own probe). Set False to - # disable entirely. + # Toolchain probe: surfaces Python/pip/uv/PEP-668 state in the system prompt only when + # something non-default is detected (no pip module, pip/python mismatch, PEP 668 without + # uv); zero tokens when clean. Skipped for docker/modal/ssh backends (own probe). "environment_probe": True, - # Bot Mode teammate-messaging protocol section (silent unless a - # profile is managed by the desktop's Bot Mode). + # Bot Mode teammate-messaging protocol section (silent unless desktop Bot Mode manages it). "bot_mode_protocol": True, - # Embedder-supplied environment description appended to the system - # prompt's environment-hints block. Lets a host that wraps Hermes - # (sandbox runner, managed platform) explain the runtime environment - # — proxy, credential handling, mount layout — without editing the - # identity slot (SOUL.md). Empty by default. The HERMES_ENVIRONMENT_HINT - # env var overrides this (build-time/container mechanism). + # Embedder-supplied text appended to the system prompt's environment-hints block, so a + # host wrapping Hermes (sandbox runner, managed platform) can describe proxy/credential/ + # mount layout without editing SOUL.md. Env HERMES_ENVIRONMENT_HINT overrides it. "environment_hint": "", - # Coding posture — on interactive coding surfaces (CLI, TUI, desktop - # app, ACP) in a code workspace, Hermes adds a coding operating brief - # + a live git/workspace snapshot to the system prompt. See - # agent/coding_context.py. - # "auto" (default) — prompt-only posture when the surface is - # interactive AND cwd is a code workspace. - # Toolsets are never touched; messaging platforms - # unaffected. - # "focus" — auto + collapse the toolset to the lean coding - # set (+ enabled MCP servers) + demote non-coding - # skill categories to names-only in the prompt's - # skill index. Explicit opt-in. - # "on" — force the prompt posture everywhere. - # "off" — disable entirely. + # Coding posture: on interactive coding surfaces (CLI, TUI, desktop, ACP) in a code + # workspace, add a coding brief + live git/workspace snapshot to the system prompt + # (agent/coding_context.py). "auto" = prompt-only when interactive AND cwd is a code + # workspace (toolsets untouched, messaging platforms unaffected); "focus" = auto + collapse + # toolset to the lean coding set (+ enabled MCP servers) + demote non-coding skill + # categories to names-only (explicit opt-in); "on" = force everywhere; "off" = disable. "coding_context": "auto", - # Standing operator instructions for the coding posture. A string (or - # list of strings) appended to the coding brief as an extra stable - # system block — pin project-wide workflow rules here instead of editing - # the shipped brief, e.g. "For UI work, don't run tsc/lint until I - # approve. Clean the diff before you commit and push." Cache-safe: - # takes effect next session. Empty by default. + # Standing operator instructions (string or list) appended to the coding brief as an extra + # stable system block — project-wide workflow rules, e.g. "Don't run tsc/lint until I + # approve." Cache-safe: takes effect next session. "coding_instructions": "", - # When verify-on-stop finds edited code without fresh verification - # evidence, append guidance for creative UI work (avoid broad - # tsc/lint/test before visual approval) and clean-diff expectations. - # Set false to keep the evidence nudge terse. + # When verify-on-stop finds edits without fresh verification evidence, add guidance for + # creative UI work (no broad tsc/lint/test before visual approval) and clean-diff + # expectations. false = keep the evidence nudge terse. "verify_guidance": True, - # Upper bound on consecutive `pre_verify` "continue" nudges in a single - # turn, so a user/plugin hook can never trap the loop. + # Max consecutive `pre_verify` "continue" nudges per turn (hooks can't trap the loop). "max_verify_nudges": 3, - # Verification closure: after the agent edits files in a code workspace, - # do not accept a final answer until fresh verification evidence exists - # or the agent explains why it cannot run checks. The loop is bounded - # and uses the passive verification ledger. Default is False (opt-in): - # the v31/v32 config migrations already switch existing installs off - # because the verification narrative proved more noise than signal, - # and the docs tell users to treat off as the effective default — a - # fresh install must not be the one population that still gets the - # nudges. Set true to force on everywhere, or "auto" for the legacy - # surface-aware behavior (on for interactive coding surfaces — CLI, - # TUI, desktop — and programmatic callers, off for conversational - # messaging surfaces). Doc/markdown/skill-only edits never fire it. + # Verification closure: after code edits in a workspace, refuse a final answer until fresh + # verification evidence exists or the agent explains why it can't check (bounded loop, + # passive ledger). False (default) because the nudges proved more noise than signal; + # true = force on everywhere; "auto" = on for interactive coding surfaces and programmatic + # callers, off for messaging surfaces. Doc/markdown/skill-only edits never fire. "verify_on_stop": False, - # Staged inactivity warning: send a warning to the user at this - # threshold before escalating to a full timeout. The warning fires - # once per run and does not interrupt the agent. 0 = disable warning. + # Inactivity warning (seconds), once per run before gateway_timeout; no interrupt. 0 = off. "gateway_timeout_warning": 900, - # Maximum time (seconds) the gateway will block an agent waiting for - # a clarify-tool response from the user. Hit this and the agent - # unblocks with "[user did not respond within Xm]" so it can adapt - # rather than pinning the running-agent guard forever. CLI clarify - # blocks indefinitely (input() is synchronous) and ignores this. - # Default 3600 (1h): real users step away (meetings, AFK) and the - # old 600s default evicted the entry mid-think, so a later button - # tap landed on a dead entry (#32762). Tradeoff: a higher value - # holds the gateway's running-agent guard longer for a genuinely - # abandoned prompt — lower it if a single session must free up the - # guard sooner. + # Max seconds the gateway blocks an agent awaiting a clarify-tool reply; then it unblocks + # with "[user did not respond within Xm]". CLI clarify blocks indefinitely and ignores + # this. 1h because users step away and a shorter value evicted the entry mid-think so a + # later button tap hit a dead entry. Lower it to free the running-agent guard sooner. "clarify_timeout": 3600, - # Periodic "still working" notification interval (seconds). - # Sends a status message every N seconds so the user knows the - # agent hasn't died during long tasks. 0 = disable notifications. - # Lower values mean faster feedback on slow tasks but more chat - # noise; 180s is a compromise that catches spinning weak-model runs - # (60+ tool iterations with tiny output) before users assume the - # bot is dead and /restart. + # "Still working" status interval (seconds); 0 = off. Lower = faster feedback, more noise; + # 180 catches spinning weak-model runs before users /restart. "gateway_notify_interval": 180, - # Session stall watchdog (seconds). Scope (#76354): this is a - # RECOVERY notifier for an in-process AIAgent that has an - # adapter-queued follow-up (pending inbound / queued event) while its - # activity clock is stale — NOT a general gateway/session stall - # detector. It does not observe startup restoration, build sentinels, - # turn leases, debounce state, or work owned by another process; the - # scan cadence is per AIAgent instance, not globally coordinated per - # durable session. Notify-only: warns the user to try /new. Distinct - # from gateway_timeout (which kills the turn) and - # gateway_notify_interval ("still working" heartbeats). 0 = disable. + # Session stall watchdog (seconds): RECOVERY notifier for an in-process AIAgent with an + # adapter-queued follow-up while its activity clock is stale — NOT a general stall + # detector (ignores startup restore, build sentinels, leases, debounce, other processes; + # scan cadence per AIAgent). Notify-only: tells the user to try /new. Distinct from + # gateway_timeout (kills the turn) and gateway_notify_interval. 0 = disable. "session_stall_timeout": 300, - # Transcript-sanitiser repeated-heal escalation threshold (#96870). - # After this many pre-send heal passes within a 10-minute session - # window, log one ERROR (session id + heal pattern) and queue a - # ONE-TIME out-of-band user notice pointing at /debug share or - # `hermes doctor`. Delivered via the status channel only — - # conversation context / prompt caching untouched. 0 = disable - # escalation (per-window WARNINGs still fire). + # Transcript-sanitiser heal escalation: after this many pre-send heal passes within a + # 10-minute window, log one ERROR and queue a ONE-TIME out-of-band notice pointing at + # /debug share or `hermes doctor` (status channel only; prompt cache untouched). + # 0 = no escalation (per-window WARNINGs still fire). "sanitizer_heal_escalation_threshold": 3, - # Long-lived reconnect-loop escalation (seconds). A platform that has - # been continuously failing/reconnecting for this long gets - # needs_attention flagged in gateway runtime status (visible in - # `hermes status` / fleet monitoring). Retries never stop — this is a - # signal, not a circuit breaker. 0 = disable. + # Seconds of continuous reconnect failure before a platform gets needs_attention flagged + # in gateway status (`hermes status` / fleet monitoring). Retries never stop — a signal, + # not a circuit breaker. 0 = disable. "reconnect_attention_after": 7200, - # Freshness window for the gateway auto-continue note (seconds). - # After a gateway crash/restart/SIGTERM mid-run, the next user - # message gets a "[System note: your previous turn was - # interrupted — process the unfinished tool result(s) first]" - # prepended so the model picks up where it left off. That's the - # right behaviour while the interruption is fresh, but stale - # markers (transcript last touched hours or days ago) can revive - # an unrelated old task when the user's next message starts new - # work. This window is the max age of the last persisted - # transcript row for which we still inject the continue note. - # Default 3600s comfortably covers a long turn (gateway_timeout - # default is 1800s) plus runtime slack. Set to 0 to disable the - # gate and restore pre-fix behaviour (always inject). + # Freshness window (seconds) for the auto-continue note. After a crash/restart mid-run the + # next user message gets "[System note: your previous turn was interrupted...]" prepended; + # only when the last persisted transcript row is younger than this, so stale markers don't + # revive an unrelated old task. Covers gateway_timeout (1800) plus slack. 0 = always inject. "gateway_auto_continue_freshness": 3600, - # Max seconds the gateway waits for boot auto-resume turns to finish - # before it releases the startup-restore inbound gate. While startup - # restore is in progress the gateway QUEUES every inbound message - # instead of replying, so no channel gets an answer until this gate - # opens. Without a bound, one pathologically long resumed turn holds - # the gate shut and every channel's inbound piles up unanswered for as - # long as that turn runs. On timeout the gate releases and the slow - # resume turn keeps running in the background; duplicate-agent - # protection is unaffected because the resume slot is claimed - # synchronously before the gate runs. Set to 0 to disable the bound - # (historical "wait forever" behaviour). + # Max seconds the gateway waits for boot auto-resume turns before releasing the + # startup-restore inbound gate (all inbound is QUEUED while shut, so one long resumed + # turn would leave every channel unanswered). On timeout the gate opens and the resume + # keeps running in the background; duplicate-agent protection is unaffected because the + # resume slot is claimed synchronously first. 0 = wait forever. "gateway_startup_restore_drain_timeout": 30, - # Max seconds the boot turn-machinery warm-up (#99373) may hold the - # gateway's inbound gate shut. On a fresh boot the gateway warms the - # agent-side turn prerequisites (run_agent import graph, tool schemas - # + availability probes, context-file tier) BEFORE accepting inbound - # messages, so a message seconds after boot is no longer served with - # a skeleton system prompt (missing context files / tool schemas). - # On timeout the gate opens anyway and warm-up finishes in the - # background — a wedged init can't make the gateway permanently - # unavailable. Set to 0 to disable the warm-up (historical - # lazy-init behaviour). + # Max seconds the boot turn-machinery warm-up (run_agent import graph, tool schemas + + # availability probes, context-file tier) may hold the inbound gate shut, so an early + # message isn't served with a skeleton system prompt. On timeout the gate opens and + # warm-up finishes in the background. 0 = disable warm-up (lazy init). "gateway_startup_warmup_timeout": 20, - # Stale-stream ceiling for local providers (Ollama, oMLX, llama-cpp) in - # seconds. When the base stale timeout is at its default (180s) and a - # local endpoint is detected, this finite ceiling replaces the former - # infinite disable so a wedged local server eventually trips the - # detector instead of hanging forever. The env var - # ``HERMES_LOCAL_STREAM_STALE_TIMEOUT`` overrides for escape-hatch use. + # Stale-stream ceiling (seconds) for local providers (Ollama, oMLX, llama-cpp). Applied + # when the base stale timeout is at its 180s default and a local endpoint is detected, so + # a wedged local server eventually trips the detector instead of hanging forever. + # Env HERMES_LOCAL_STREAM_STALE_TIMEOUT overrides. "local_stream_stale_timeout": 900, - # How user-attached images are presented to the main model on each turn. - # "auto" — attach natively when the active model reports - # supports_vision=True AND the user hasn't explicitly - # configured auxiliary.vision.provider. Otherwise fall - # back to text (vision_analyze pre-analysis). - # "native" — always attach natively; non-vision models will either - # error at the provider or get a last-chance text fallback - # (see run_agent._prepare_messages_for_api). - # "text" — always pre-analyze with vision_analyze and prepend the - # description as text; the main model never sees pixels. - # Affects gateway platforms, the TUI, and CLI /attach. vision_analyze - # remains available as a tool regardless of this setting — the routing - # only controls how inbound user images are presented. + # How user-attached images reach the main model (gateway, TUI, CLI /attach). "auto" = + # native when the model reports supports_vision=True AND auxiliary.vision.provider is not + # explicitly set, else text; "native" = always attach (non-vision models error at the + # provider or get a last-chance text fallback); "text" = always pre-analyze with + # vision_analyze and prepend the description. vision_analyze stays a tool regardless. "image_input_mode": "auto", "disabled_toolsets": [], - - # Per-model reasoning effort overrides (spelling-tolerant). - # Dict mapping model names (any reasonable spelling) to effort levels. - # Takes precedence over agent.reasoning_effort when the current model - # matches a key in this dict. - # Edit directly in config.yaml (no CLI support due to dots in keys). + # Model name (any reasonable spelling) -> effort level; overrides agent.reasoning_effort + # when the current model matches. Edit in config.yaml (no CLI support: dots in keys). "reasoning_overrides": {}, - - # Per-provider opt-in to preserve assistant ``reasoning_content`` - # when replaying history. The built-in echo families (DeepSeek, - # Kimi/Moonshot, Xiaomi MiMo) are auto-detected by provider name - # and base-URL host. Custom providers and OpenAI-compatible - # gateways that proxy those same models (or other thinking-mode - # backends) are not covered by the host-based rules. - # - # Set ``reasoning_echo: true`` on a ``model:`` entry (primary) or a - # ``fallback_providers:`` entry (per-fallback) to preserve - # ``reasoning_content`` on replay for that provider only. Default - # ``false`` keeps the historical strict-provider behavior (Mistral, - # Groq, Cerebras reject the field with HTTP 400). + # Preserve assistant `reasoning_content` on history replay. Echo families (DeepSeek, + # Kimi/Moonshot, Xiaomi MiMo) are auto-detected by provider name/base-URL host; custom + # providers and OpenAI-compatible gateways proxying them are not. Set + # `reasoning_echo: true` on a `model:` entry or a `fallback_providers:` entry to opt in + # per provider. Default false: strict providers (Mistral, Groq, Cerebras) reject the field. "reasoning_echo": False, - # Turn liveness watchdog (#95548): a turn that shows no observable - # progress (activity-clock idle, never touched by lease renewal) for - # `timeout_s` seconds is logged loudly, force-interrupted so the UI - # can retry it, and its durable turn lease stops renewing so TTL - # expiry lets stale-turn cleanup reclaim the session even when the - # hard interrupt cannot unwind a wedged frame. `timeout_s` <= 0 - # disables the watchdog; `poll_s` is the sampling interval. Invalid - # values (typo, NaN, Inf, non-positive poll) warn and fall back to - # the default instead of crashing startup or silently disabling the - # watchdog. See agent/turn_liveness.py. - "turn_liveness": { - "timeout_s": 600.0, - "poll_s": 15.0, - }, + # Turn liveness watchdog: a turn with no observable progress for `timeout_s` seconds is + # logged, force-interrupted so the UI can retry, and its lease stops renewing so stale-turn + # cleanup can reclaim the session even if the interrupt can't unwind a wedged frame. + # timeout_s <= 0 disables; poll_s = sampling interval. Invalid values (NaN, Inf, + # non-positive poll) warn and fall back to defaults. See agent/turn_liveness.py. + "turn_liveness": {"timeout_s": 600.0, "poll_s": 15.0}, }, "terminal": { "backend": "local", "modal_mode": "auto", - # Remote-backend graceful degradation: when a connection-class - # infrastructure failure occurs (SSH host unreachable, Docker daemon - # down), "warn" (default) returns a structured degraded tool result - # with a reason + retry hint so the model can act on it; "fail" - # preserves the historical error + traceback behavior. + # Remote-backend connection-class failures (SSH host unreachable, Docker daemon down): + # "warn" = structured degraded tool result with reason + retry hint; "fail" = raise + # error + traceback. "degraded_mode": "warn", "cwd": ".", # Use current directory - # Root directory for Hermes' terminal session temp files (background - # logs/pid/exit files, code-execution sandboxes, etc.). When empty, - # Hermes uses TMPDIR/TMP/TEMP if set, otherwise a managed dir on real - # storage at HERMES_HOME/cache/terminal (auto-pruned after 72h) — NOT - # tmpfs /tmp, which is RAM-capped and small on many distros (e.g. - # Arch-based setups) and fills up under load. Set this to redirect - # session temp anywhere else; must be an absolute POSIX path that - # exists. User-set paths are never auto-pruned. + # Root for terminal session temp files (background logs/pid/exit files, code-exec + # sandboxes). Empty = TMPDIR/TMP/TEMP if set, else HERMES_HOME/cache/terminal + # (auto-pruned after 72h) — NOT tmpfs /tmp, which is RAM-capped and fills under load. + # Must be an existing absolute POSIX path; user-set paths are never auto-pruned. "temp_dir": "", - # Terminal font family for the desktop app's embedded xterm.js terminal. - # When set (e.g. "'CaskaydiaCoveNerdFont', 'JetBrains Mono', monospace"), - # the desktop terminal uses this as the CSS font-family value, with the - # built-in default ("'JetBrains Mono', 'Cascadia Code', 'SF Mono', Menlo, - # Consolas, monospace") as fallback when the field is empty or unset. - # This lets users install a Nerd Font (or any custom font) and configure - # it here without patching the built desktop app. + # CSS font-family for the desktop app's xterm.js terminal (e.g. "'CaskaydiaCoveNerdFont', + # monospace"). Empty = built-in default ("'JetBrains Mono', 'Cascadia Code', 'SF Mono', + # Menlo, Consolas, monospace"). Lets users use a Nerd Font without patching the app. "font_family": "", "timeout": 180, - # Bounded grace period (seconds) between SIGTERM and an escalated - # SIGKILL when terminating a host process tree (browser daemons, etc.). - # A daemon that stalls in its SIGTERM handler is force-killed after this - # window so it can't leak indefinitely. 0 disables escalation (SIGTERM - # only — the historical behavior). Floored internally at 0. + # Seconds between SIGTERM and escalated SIGKILL for host process trees (browser daemons). + # 0 = SIGTERM only. "daemon_term_grace_seconds": 2.0, - # Bounded linger (seconds) for one-shot CLI runs (-q/-Q/-z) that exit - # while background processes spawned with notify_on_complete=true are - # still running. The dying parent owns those children's stdout pipes, - # so exiting immediately kills the delivery a few seconds later — - # destroying Bot Mode handoff replies dispatched via message_agent / - # bot_relay from a short-lived `hermes -p chat -Q` recipient - # (#90879). The parent instead waits (up to this bound) for tracked - # notify_on_complete processes to finish before exiting. Plain - # background processes without notify_on_complete (servers, daemons) - # are never waited on. 0 disables the linger. + # Max seconds a one-shot CLI run (-q/-Q/-z) lingers for tracked notify_on_complete + # background processes to finish. The dying parent owns their stdout pipes, so exiting + # immediately kills the delivery (e.g. Bot Mode handoff replies via message_agent / + # bot_relay). Plain background processes without notify_on_complete are never waited on. + # 0 disables. "oneshot_completion_wait_seconds": 600.0, - # Environment variables to pass through to sandboxed execution - # (terminal and execute_code). Skill-declared required_environment_variables - # are passed through automatically; this list is for non-skill use cases. + # Env vars passed into sandboxed terminal/execute_code (skill-declared + # required_environment_variables pass through automatically). "env_passthrough": [], - # HOME handling for host tool subprocesses: - # auto — host keeps the real OS-user HOME; containers use - # HERMES_HOME/home for persistent state (default) - # real — force the real OS-user HOME - # profile — force HERMES_HOME/home when it exists (old strict - # per-profile CLI config isolation) + # HOME for host tool subprocesses: "auto" = host keeps the real OS-user HOME, containers + # use HERMES_HOME/home; "real" = force real HOME; "profile" = force HERMES_HOME/home when + # it exists (strict per-profile isolation). "home_mode": "auto", - # Extra files to source in the login shell when building the - # per-session environment snapshot. Use this when tools like nvm, - # pyenv, asdf, or custom PATH entries are registered by files that - # a bash login shell would skip — most commonly ``~/.bashrc`` - # (bash doesn't source bashrc in non-interactive login mode) or - # zsh-specific files like ``~/.zshrc`` / ``~/.zprofile``. - # Paths support ``~`` / ``${VAR}``. Missing files are silently - # skipped. When empty, Hermes auto-sources ``~/.profile``, - # ``~/.bash_profile``, and ``~/.bashrc`` (in that order) if the - # snapshot shell is bash (this is the ``auto_source_bashrc`` - # behaviour — disable with that key if you want strict login-only - # semantics). + # Extra files sourced in the login shell when building the per-session env snapshot — + # for nvm/pyenv/asdf/PATH entries registered by files a bash login shell skips + # (~/.bashrc, ~/.zshrc, ~/.zprofile). Supports ~ and ${VAR}; missing files skipped. + # When empty and the shell is bash, ~/.profile, ~/.bash_profile, ~/.bashrc are + # auto-sourced in that order (see auto_source_bashrc). "shell_init_files": [], - # When true (default), Hermes sources the user's shell rc files - # (``~/.profile``, ``~/.bash_profile``, ``~/.bashrc``) in the - # login shell used to build the environment snapshot. This - # captures PATH additions, shell functions, and aliases — which a - # plain ``bash -l -c`` would otherwise miss because bash skips - # bashrc in non-interactive login mode, and because a default - # Debian/Ubuntu ``~/.bashrc`` short-circuits on non-interactive - # sources. ``~/.profile`` and ``~/.bash_profile`` are tried first - # because ``n`` / ``nvm`` / ``asdf`` installers typically write - # their PATH exports there without an interactivity guard. Turn - # this off if your rc files misbehave when sourced - # non-interactively (e.g. one that hard-exits on TTY checks). + # Source ~/.profile, ~/.bash_profile, ~/.bashrc in the snapshot login shell to capture + # PATH additions, functions, and aliases that `bash -l -c` misses (bash skips bashrc when + # non-interactive; Debian/Ubuntu ~/.bashrc short-circuits). ~/.profile and ~/.bash_profile + # go first because n/nvm/asdf write PATH exports there without an interactivity guard. + # Turn off if an rc file misbehaves when sourced non-interactively (exits on TTY check). "auto_source_bashrc": True, "docker_image": "nikolaik/python-nodejs:python3.11-nodejs20", "docker_forward_env": [], - # Explicit environment variables to set inside Docker containers. - # Unlike docker_forward_env (which reads values from the host process), - # docker_env lets you specify exact key-value pairs — useful when Hermes - # runs as a systemd service without access to the user's shell environment. + # Exact key-value env pairs set inside Docker containers (unlike docker_forward_env, which + # reads host values) — useful under systemd without the user's shell env. # Example: {"SSH_AUTH_SOCK": "/run/user/1000/ssh-agent.sock"} "docker_env": {}, "singularity_image": "docker://nikolaik/python-nodejs:python3.11-nodejs20", "modal_image": "nikolaik/python-nodejs:python3.11-nodejs20", "daytona_image": "nikolaik/python-nodejs:python3.11-nodejs20", - # Vercel Sandbox runtime (vercel_sandbox backend only). - # Supported: node24, node22, python3.13. - "vercel_runtime": "node24", - # Container resource limits (docker, singularity, modal, daytona, vercel_sandbox — ignored for local/ssh) + "vercel_runtime": "node24", # vercel_sandbox backend only: node24 | node22 | python3.13 + # Container limits (docker, singularity, modal, daytona, vercel_sandbox; not local/ssh). "container_cpu": 1, "container_memory": 5120, # MB (default 5GB) "container_disk": 51200, # MB (default 50GB) "container_persistent": True, # Persist filesystem across sessions - # Docker volume mounts — share host directories with the container. - # Each entry is "host_path:container_path" (standard Docker -v syntax). - # Example: - # ["/home/user/projects:/workspace/projects", - # "/home/user/.hermes/cache/documents:/output"] - # For gateway MEDIA delivery, write inside Docker to /output/... and emit - # the host-visible path in MEDIA:, not the container path. + # Docker volume mounts, "host_path:container_path" (docker -v syntax), e.g. + # ["/home/user/.hermes/cache/documents:/output"]. For gateway MEDIA delivery, write to + # /output/... inside Docker and emit the host-visible path in MEDIA:, not the container one. "docker_volumes": [], - # Explicit opt-in: mount the host cwd into /workspace for Docker sessions. - # Default off because passing host directories into a sandbox weakens isolation. - "docker_mount_cwd_to_workspace": False, - # Opt-in egress lockdown for Docker terminal sessions. When false, - # Docker runs with --network=none so commands cannot reach the network. - "docker_network": True, + "docker_mount_cwd_to_workspace": False, # mount host cwd at /workspace (weakens isolation) + "docker_network": True, # false = --network=none, no network access from commands "docker_extra_args": [], # Extra flags passed verbatim to docker run - # /dev/shm size for the Docker sandbox. Docker's 64 MB default silently - # breaks Chromium/Playwright and PyTorch DataLoader workers; tmpfs is - # lazily allocated so the higher ceiling costs nothing until used. - # Set to "" (or "0") to omit the flag and use Docker's default. + # /dev/shm size for the Docker sandbox. Docker's 64 MB default silently breaks + # Chromium/Playwright and PyTorch DataLoader workers; tmpfs is lazily allocated so the + # higher ceiling is free until used. "" or "0" = omit the flag (Docker default). "docker_shm_size": "1g", - # Explicit opt-in: run the Docker container as the host user's uid:gid - # (via `--user`). When enabled, files written into bind-mounted dirs - # (docker_volumes, the persistent workspace, or the auto-mounted cwd) - # are owned by your host user instead of root, which avoids needing - # `sudo chown` after container runs. Default off to preserve behavior - # for images whose entrypoints expect to start as root (e.g. the - # bundled Hermes image, which drops to the `hermes` user via - # s6-setuidgid inside each supervised service). - # When on, SETUID/SETGID caps are omitted from the container since - # no privilege drop is needed. + # Run the container as the host uid:gid (`--user`) so files written to bind mounts + # (docker_volumes, persistent workspace, mounted cwd) are owned by you, not root. Off by + # default for images whose entrypoints must start as root (e.g. the bundled Hermes image, + # which drops to `hermes` via s6-setuidgid). When on, SETUID/SETGID caps are omitted. "docker_run_as_host_user": False, - # Explicit opt-in for trusted profiles to reuse the same Docker - # container identity. Empty preserves the active-profile boundary. + # Trusted profiles sharing one Docker container identity; empty = per-profile boundary. "docker_shared_container_key": "", - # Persistent shell — keep a long-lived bash shell across execute() calls - # so cwd/env vars/shell variables survive between commands. - # Enabled by default for non-local backends (SSH); local is always opt-in - # via TERMINAL_LOCAL_PERSISTENT env var. + # Keep a long-lived bash shell across execute() calls so cwd/env/shell variables survive. + # Applies to non-local backends (SSH); local is opt-in via TERMINAL_LOCAL_PERSISTENT env. "persistent_shell": True, }, @@ -556,1326 +330,669 @@ DEFAULT_CONFIG = { "backend": "", # shared fallback — applies to both search and extract "search_backend": "", # per-capability override for web_search (e.g. "searxng") "extract_backend": "", # per-capability override for web_extract (e.g. "native") - "extract_char_limit": 15000, # per-page char budget for web_extract; larger pages truncate + store full text in cache/web - # Keyless free-tier ring: with NO web backend configured or keyed, - # web_search/web_extract rotate round-robin across four vendors' - # public free tiers (exa, parallel, firecrawl, keenable), - # failing over to the next ring vendor on rate limits. Never - # pre-empts a configured or keyed backend. Set false to disable. + # per-page char budget for web_extract; larger pages truncate, full text kept in cache/web + "extract_char_limit": 15000, + # Keyless free-tier ring: with NO web backend configured or keyed, web_search/web_extract + # rotate round-robin across exa, parallel, firecrawl, keenable public free tiers, failing + # over on rate limits. Never pre-empts a configured/keyed backend. false = disable. "keyless_fallback": True, - # One-shot keyless rescue: when the chosen/keyed backend fails a - # web_search/web_extract call, THAT call retries once on the keyless - # free-tier ring — the next call attempts the chosen backend again - # (no sticky failover). Off when keyless_fallback is false. + # One-shot rescue: when the chosen/keyed backend fails a call, THAT call retries once on + # the keyless ring; the next call tries the chosen backend again (no sticky failover). + # Off when keyless_fallback is false. "keyless_rescue": True, - # Per-provider tier selection for vendors with both a keyless - # free endpoint and a keyed paid path (exa, parallel, - # firecrawl, keenable on the ring; tavily is opt-in keyless via - # `hermes tools`, not a ring member). Set by the `hermes tools` - # picker's "Free (keyless)" / "Paid (API key)" rows. - # free — always use the anonymous free endpoint (even with a key) - # paid — always use the keyed path (missing key = error; vendor - # is also excluded from the keyless ring) - # unset — auto: keyed when the API key is present, else the ring + # Per-vendor tier for vendors with both a keyless free endpoint and a keyed paid path + # (exa, parallel, firecrawl, keenable; tavily is opt-in keyless via `hermes tools`, not a + # ring member). Set by the `hermes tools` picker. "free" = always anonymous endpoint even + # with a key; "paid" = always keyed (missing key = error; vendor excluded from the ring); + # unset = keyed when the key is present, else the ring. "provider_tier": {}, - # TTL result caching for web_search + web_extract. Repeat searches - # (same query, same provider) within the TTL are served from an - # in-process memo; repeat extracts of the same URL are served from - # the cache/web full-text store. Concurrent identical searches - # (parallel subagents) coalesce into one vendor request. Only - # successful responses are cached. + # TTL caching for web_search + web_extract: repeat searches (same query + provider) within + # the TTL come from an in-process memo; repeat extracts from the cache/web store. + # Concurrent identical searches coalesce into one vendor request. Only successes cached. "cache_enabled": True, "cache_ttl_minutes": 20, - # Hosts whose pages must always be fetched live, never from the - # extract cache — sites you're actively developing but testing over - # the public internet (staging deploys, tunnel URLs, preview - # builds). Entries match exactly, as "*.wildcard", or as a domain - # suffix ("mysite.dev" also covers "preview.mysite.dev"). - # localhost/private-IP URLs are always exempt automatically. - # cache_exempt_hosts: ["mysite.vercel.app", "*.ngrok-free.app"] + # Hosts always fetched live, never from the extract cache (staging deploys, tunnel URLs, + # preview builds). Entries match exactly, as "*.wildcard", or as a domain suffix + # ("mysite.dev" also covers "preview.mysite.dev"). localhost/private IPs always exempt. "cache_exempt_hosts": [], }, "browser": { - # Browser tool implementation. - # "" — DEFAULT: Browser Use mode when the browser-use CLI - # (or uvx) is available; otherwise the built-in - # browser tools. Camofox setups always keep the - # built-in tools (no CDP surface). - # "browser-use" — force Browser Use mode: one browser_exec tool - # driving the Browser Use CLI 3.0 over any CDP - # backend (local Chrome, cloud browsers) - # "off" — force the built-in browser tools - # (browser_navigate, browser_click, …) + # "" = Browser Use mode when the browser-use CLI (or uvx) is available, else built-in tools + # (Camofox setups always keep built-in tools: no CDP surface); "browser-use" = force one + # browser_exec tool driving the Browser Use CLI over any CDP backend (local Chrome, cloud); + # "off" = force the built-in browser_navigate/browser_click/... tools. "backend": "", "inactivity_timeout": 120, - "command_timeout": 30, # Timeout for browser commands in seconds (screenshot, navigate, etc.) - "snapshot_threshold": 15000, # Max chars before snapshot truncate-and-store (min 1000) - "record_sessions": False, # Auto-record browser sessions as WebM videos - "headed": False, # Local mode: launch Chromium with a visible window (also skips per-turn cleanup so the window persists between turns; idle reaper still applies) - "allow_private_urls": False, # Allow navigating to private/internal IPs (localhost, 192.168.x.x, etc.) - # Local browser engine, for both drivers: - # Browser Use mode (default) — "lightpanda" makes Hermes spawn - # ``lightpanda serve`` per session and point browser_exec at it. - # Built-in tools (backend: off) — passed as ``--engine `` to - # agent-browser v0.25.3+ (with automatic Chrome fallback). - # "auto" — Chrome (default) - # "lightpanda" — Lightpanda (faster navigation, no screenshots) - # "chrome" — explicitly request Chrome - # Ignored while a cloud provider, Camofox, browser.cdp_url or - # browser.use_real_profile is active — `/browser status` and - # `hermes doctor` say so. Also settable via AGENT_BROWSER_ENGINE. + "command_timeout": 30, # seconds per browser command (screenshot, navigate, etc.) + "snapshot_threshold": 15000, # max chars before snapshot truncate-and-store (min 1000) + "record_sessions": False, # auto-record browser sessions as WebM videos + # headed: visible Chromium window (local); skips per-turn cleanup, idle reaper still applies + "headed": False, + "allow_private_urls": False, # allow private/internal IPs (localhost, 192.168.x.x, ...) + # Local browser engine for both drivers. "auto" = Chrome; "lightpanda" = faster navigation, + # no screenshots (Browser Use mode spawns `lightpanda serve` per session; built-in tools + # pass `--engine ` to agent-browser with Chrome fallback); "chrome" = explicit. + # Ignored while a cloud provider, Camofox, cdp_url or use_real_profile is active. + # Also settable via AGENT_BROWSER_ENGINE. "engine": "auto", - "auto_local_for_private_urls": True, # When a cloud provider is set, auto-spawn local Chromium for LAN/localhost URLs instead of sending them to the cloud - "cdp_url": "", # Optional persistent CDP endpoint for attaching to an existing Chromium/Chrome - # Consent to browse with the user's REAL logins for local browsing. - # When true, local browsing (the Browser Use CLI, or the built-in - # browser tools) runs on a Hermes-managed SNAPSHOT of the user's - # ACTIVE default-Chromium profile (Local State -> profile.last_used) — - # its cookies, logins and preferences copied in and re-synced when a - # fresh session launches — driven by Hermes' packaged Chromium. Only - # the active profile is copied. The snapshot is a non-default dir, so it - # sidesteps Chrome 136+'s block on debugging the default profile and - # never contends with the user's running browser. Turning this back off - # deletes the snapshot store (~/.hermes/browser-profile/) so copied - # credentials don't outlive consent. Only Chromium-family default - # browsers are supported (Chrome, Edge, Brave, Brave Origin, Chromium); a non-Chromium - # default (e.g. Firefox) fails closed with a clear message. Default - # false. Also gates the browser_exec ``local`` argument, which forces a - # real-profile local session even under a cloud browser backend. Toggle - # in the desktop Settings → Browser section. + # With a cloud provider, auto-spawn local Chromium for LAN/localhost URLs instead + "auto_local_for_private_urls": True, + "cdp_url": "", # persistent CDP endpoint for attaching to an existing Chromium/Chrome + # Consent to browse with the user's REAL logins locally: runs on a Hermes-managed SNAPSHOT + # of the ACTIVE default-Chromium profile (Local State -> profile.last_used; cookies, logins, + # prefs copied and re-synced per fresh session) driven by Hermes' packaged Chromium. The + # snapshot dir sidesteps Chrome 136+'s default-profile debugging block and never contends + # with the running browser. Turning off deletes ~/.hermes/browser-profile/ so credentials + # don't outlive consent. Chromium-family only (Chrome, Edge, Brave, Brave Origin, + # Chromium); Firefox etc. fails closed. Also gates the browser_exec `local` argument + # (real-profile local session even under a cloud backend). Desktop Settings -> Browser. "use_real_profile": False, - # When real-profile browsing needs the browser closed (Windows: a - # running Chrome/Edge/Brave locks its cookie DB deny-all, so it must be - # fully quit before its profile can be copied), arm the "offer to close - # it" flow. This does NOT auto-kill: when the profile is locked the - # snapshot always blocks and the agent asks the user first; only on - # approval does it run `hermes browser close-profile` (terminates the - # browser process tree bound to that profile, losing unsaved tabs) and - # retry. Still locked afterward → stays blocked, no loop, no auto-kill. - # OFF by default. No effect on macOS/Linux (copy-while-running works). + # Windows only: a running Chrome/Edge/Brave locks its cookie DB, so the profile can't be + # copied. When on, a locked profile still blocks and the agent ASKS first; on approval it + # runs `hermes browser close-profile` (kills that profile's browser tree, unsaved tabs + # lost) and retries once; still locked -> stays blocked, no auto-kill. No effect on + # macOS/Linux (copy-while-running works). "real_profile_autoclose": False, - # Pin WHICH source browser profile directory gets snapshotted for - # real-profile browsing (e.g. "Profile 2"). Unset/empty: follows the - # browser's last-used profile (Local State → profile.last_used). On a - # machine with several profiles (work + personal), last-used roulette - # can silently hand the agent the wrong identity; a pin locks it. A pin - # naming a directory that doesn't exist FAILS CLOSED with a fixable - # message rather than falling back to last-used. + # Pin WHICH source profile directory is snapshotted for real-profile browsing (e.g. + # "Profile 2"). Empty = browser's last-used profile, which on multi-profile machines can + # hand the agent the wrong identity. A pin naming a missing directory FAILS CLOSED. "real_profile_pin": "", - "allow_unsafe_evaluate": False, # Legacy override: when true, browser_console(expression=...) bypasses the restrict_evaluate denylist entirely - "restrict_evaluate": False, # Opt-in denylist blocking sensitive JS primitives (cookies/storage/clipboard/network/form values) in browser_console(expression=...) - # CDP supervisor — dialog + frame detection via a persistent WebSocket. - # Active only when a CDP-capable backend is attached (Browserbase or - # local Chrome via /browser connect). See - # website/docs/developer-guide/browser-supervisor.md. + # restrict_evaluate: opt-in denylist blocking sensitive JS primitives (cookies/storage/ + # clipboard/network/form values) in browser_console(expression=...); + # allow_unsafe_evaluate is the legacy override that bypasses that denylist entirely. + "allow_unsafe_evaluate": False, + "restrict_evaluate": False, + # CDP supervisor: dialog + frame detection over a persistent WebSocket; active only with a + # CDP-capable backend (Browserbase, or local Chrome via /browser connect). + # See website/docs/developer-guide/browser-supervisor.md. "dialog_policy": "must_respond", # must_respond | auto_dismiss | auto_accept - "dialog_timeout_s": 300, # Safety auto-dismiss after N seconds under must_respond + "dialog_timeout_s": 300, # safety auto-dismiss after N seconds under must_respond "camofox": { - # When true, Hermes sends a stable profile-scoped userId to Camofox - # so the server maps it to a persistent Firefox profile automatically. - # When false (default), each session gets a random userId (ephemeral). + # true = send a stable profile-scoped userId so Camofox maps it to a persistent + # Firefox profile; false = random ephemeral userId per session. "managed_persistence": False, - # Optional externally managed Camofox identity. Useful when another - # app owns the visible browser and Hermes should operate in it. + # Externally managed Camofox identity, for when another app owns the visible browser. "user_id": "", "session_key": "", - # Rehydrate tab_id from Camofox before creating a new tab. - "adopt_existing_tab": False, - # Docker Camofox opens page URLs from inside the container. Enable - # this to rewrite loopback page URLs (localhost/127.0.0.1/::1) to a - # host alias while leaving CAMOFOX_URL itself unchanged. + "adopt_existing_tab": False, # rehydrate tab_id from Camofox before creating a tab + # Docker Camofox opens page URLs from inside the container: rewrite loopback page URLs + # (localhost/127.0.0.1/::1) to the host alias; CAMOFOX_URL itself is unchanged. "rewrite_loopback_urls": False, "loopback_host_alias": "host.docker.internal", }, - # Authenticated browser-extension controller lane. When enabled, an - # extension that registers through the gateway can become the exact - # controller for a session's browser_* tools (fail-closed once bound). - # Local API registration additionally requires the API server bearer - # key. developer_mode gates the privileged capabilities - # (browser_cdp / browser_evaluate) — never negotiable without it. - "extension_control": { - "enabled": False, - "developer_mode": False, - }, + # Authenticated browser-extension controller lane: a registered extension can become the + # exact controller for a session's browser_* tools (fail-closed once bound). Local API + # registration also requires the API server bearer key. developer_mode gates the + # privileged browser_cdp / browser_evaluate capabilities. + "extension_control": {"enabled": False, "developer_mode": False}, }, - # Filesystem checkpoints — automatic snapshots before destructive file ops. - # When enabled, the agent takes a snapshot of the working directory once - # per conversation turn (on first write_file/patch call). Use /rollback - # to restore. - # - # Defaults changed in v2 (single shared shadow store, real pruning): - # - enabled: True -> False (opt-in; most users never use /rollback) - # - max_snapshots: 50 -> 20 (now actually enforced via ref rewrite) - # - auto_prune: False -> True (orphans/stale pruned automatically) - # Opt in via ``hermes chat --checkpoints`` or set enabled=True here. + # Filesystem checkpoints: snapshot the working directory once per turn (on the first + # write_file/patch call); restore with /rollback. Opt-in via `hermes chat --checkpoints` or + # enabled=True (most users never use /rollback). Single shared shadow store with real pruning. "checkpoints": { "enabled": False, - # Max checkpoints to keep per working directory. Pre-v2 this only - # limited the `/rollback` listing; v2 actually rewrites the ref and - # garbage-collects older commits. + # Max checkpoints per working directory; enforced by ref rewrite + GC of older commits. "max_snapshots": 20, - # Hard ceiling on total ``~/.hermes/checkpoints/`` size (MB). When - # exceeded, the oldest checkpoint per project is dropped in a - # round-robin pass until total size falls under the cap. - # 0 disables the size cap. + # Hard ceiling on total ~/.hermes/checkpoints/ size (MB); the oldest checkpoint per project + # is dropped round-robin until under the cap. 0 disables. "max_total_size_mb": 500, - # Skip any single file larger than this when staging a checkpoint. - # Prevents accidental snapshotting of datasets, model weights, and - # other large generated assets. 0 disables the filter. + # Skip files larger than this (MB) when staging (datasets, model weights). 0 = no filter. "max_file_size_mb": 10, - # Auto-maintenance: hermes sweeps the checkpoint base at startup - # (at most once per ``min_interval_hours``) and: - # * deletes project entries whose last_touch is older than - # ``retention_days`` - # * GCs the single shared store to reclaim unreachable objects - # * enforces ``max_total_size_mb`` across remaining projects - # * deletes ``legacy-*`` archives older than ``retention_days`` - # - # NOTE: this automatic sweep never deletes "orphan" entries (workdir - # no longer found on disk). A missing workdir at startup is - # ambiguous — it can mean the project was deleted, or that an - # external volume / network share / VPN is simply not mounted yet — - # and this sweep runs unattended, so it must never guess. Orphan - # cleanup is only available via the explicit - # ``hermes checkpoints prune`` command (add ``--keep-orphans`` to - # skip it), where a human is looking at the output. + # Startup sweep (at most once per min_interval_hours): deletes projects whose last_touch + # is older than retention_days, GCs the shared store, enforces max_total_size_mb, deletes + # legacy-* archives older than retention_days. It NEVER deletes orphans (workdir missing + # on disk) — a missing workdir may just be an unmounted volume/VPN, and an unattended + # sweep must not guess. Orphans: `hermes checkpoints prune` (`--keep-orphans` to skip). "auto_prune": True, "retention_days": 7, "min_interval_hours": 24, }, - # Hard cap (chars) for a single automatic context file such as SOUL.md, - # AGENTS.md, CLAUDE.md, .hermes.md, or .cursorrules before Hermes applies - # head/tail truncation. ``null`` (the default) lets the cap scale with the - # model's context window (floor 20K, ceiling 500K) so large-context models - # rarely truncate a project doc. Set a positive integer to pin a fixed cap - # and override the dynamic behavior. Separate from read_file tool limits. + # Hard cap (chars) for one auto-loaded context file (SOUL.md, AGENTS.md, CLAUDE.md, + # .hermes.md, .cursorrules) before head/tail truncation. null = scale with the model's + # context window (floor 20K, ceiling 500K); a positive int pins a fixed cap. + # Separate from read_file limits. "context_file_max_chars": None, - # Maximum characters returned by a single read_file call. Reads that - # exceed this are rejected with guidance to use offset+limit. - # 100K chars ≈ 25–35K tokens across typical tokenisers. + # Max chars per read_file call; larger reads are rejected with offset+limit guidance. + # 100K chars ≈ 25–35K tokens. "file_read_max_chars": 100_000, - # Seconds to wait at agent-build time for in-flight MCP server discovery - # to finish before the agent snapshots its tool list. MCP discovery runs - # in a background thread so a slow/dead server can't freeze startup; this - # bounds how long the first agent build blocks on it. The wait returns - # the INSTANT discovery completes, so users with no MCP servers (the common - # case) or fast servers pay ~0s regardless of this value — the bound is - # only reached when a server is genuinely still connecting. The old 0.75s - # default was a touch short for HTTP/OAuth servers on a cold connect; a - # modest bump lets more of them land in the FIRST turn's snapshot. This is - # only a turn-1 latency/UX knob: a server that misses this window is still - # picked up automatically on the next turn by the between-turns refresh - # (see agent/turn_context.py), so correctness never depends on it. Keep it - # small so a slow/dead server adds little to first-response latency. + # Seconds the first agent build waits for background MCP discovery before snapshotting + # its tool list. Returns the instant discovery completes (no MCP servers → ~0s); the + # bound only bites when a server is still connecting. Turn-1 latency knob only: a + # server that misses it is picked up by the between-turns refresh (agent/turn_context.py), + # so keep it small — a dead server adds this much to first-response latency. "mcp_discovery_timeout": 1.5, - # Single-query (``hermes -q/-z "..."``) variant of mcp_discovery_timeout. - # In one-shot mode there is only ONE turn, so the between-turns late-binding - # refresh never runs: a server that misses the small interactive bound is - # invisible to the LLM for the whole session. This larger bound gives slow - # cold-start servers (npx, uvx, remote HTTP) a chance to land in the one - # tool snapshot. ``thread.join(timeout)`` returns the instant discovery - # completes, so reachable servers only wait for their real handshake time - # while unavailable servers remain bounded. + # Same bound for single-query mode (``hermes -q/-z``). With only ONE turn there is no + # between-turns refresh, so a server that misses the window is invisible for the whole + # session; the larger bound lets slow cold-start servers (npx, uvx, remote HTTP) land. + # Reachable servers still only wait their real handshake time. "mcp_single_query_discovery_timeout": 15.0, - # MCP runtime behavior (distinct from the per-server definitions in - # mcp_servers: and from the auxiliary.mcp side-LLM task settings). + # MCP runtime behavior (distinct from mcp_servers: definitions and auxiliary.mcp). "mcp": { - # Auto-reload MCP connections when config.yaml's mcp_servers section - # changes at runtime (CLI file watcher, default on). - # Set to false to stop the automatic reload: every automatic reload - # rebuilds the agent tool surface and INVALIDATES the provider - # prompt cache (the next message re-sends the full input prefix), - # which is expensive on long-context / high-reasoning models. - # When disabled, the watcher still detects the change and prints - # guidance to apply it deliberately via /reload-mcp. + # Auto-reload MCP connections when config.yaml's mcp_servers changes (CLI watcher). + # Every reload rebuilds the tool surface and INVALIDATES the provider prompt cache + # (next message re-sends the full prefix) — costly on long-context models. When + # false the watcher still detects the change and prints /reload-mcp guidance. "auto_reload_on_config_change": True, }, - # Tool-output truncation thresholds. When terminal output or a - # single read_file page exceeds these limits, Hermes truncates the - # payload sent to the model (keeping head + tail for terminal, - # enforcing pagination for read_file). Tuning these trades context - # footprint against how much raw output the model can see in one - # shot. Ported from anomalyco/opencode PR #23770. - # - # - max_bytes: terminal_tool output cap, in chars - # (default 50_000 ≈ 12-15K tokens). - # - max_lines: read_file pagination cap — the maximum `limit` - # a single read_file call can request before - # being clamped (default 2000). - # - max_line_length: per-line cap applied when read_file emits a - # line-numbered view (default 2000 chars). - "tool_output": { - "max_bytes": 50_000, - "max_lines": 2000, - "max_line_length": 2000, - }, + # Tool-output truncation. max_bytes: terminal_tool output cap in chars (head+tail kept; + # 50_000 ≈ 12-15K tokens). max_lines: max `limit` one read_file call may request before + # clamping. max_line_length: per-line cap in read_file's line-numbered view (chars). + "tool_output": {"max_bytes": 50000, "max_lines": 2000, "max_line_length": 2000}, - # Tool loop guardrails nudge models when they repeat failed or - # non-progressing tool calls. Soft warnings are always-on by default; - # hard stops are opt-in so interactive CLI/TUI sessions keep flowing. + # Tool loop guardrails nudge models that repeat failed/non-progressing tool calls. + # Soft warnings are always on; hard stops are opt-in so interactive sessions keep flowing. "tool_loop_guardrails": { "warnings_enabled": True, "hard_stop_enabled": False, - # Unattended gateway/cron platforms get hard stops by default (nobody - # is present to /stop a model that ignores loop warnings); interactive - # cli/tui/desktop/acp stay warning-only unless hard_stop_enabled. + # Unattended gateway/cron platforms hard-stop by default (nobody can /stop a model + # that ignores warnings); interactive cli/tui/desktop/acp stay warning-only. "non_interactive_hard_stop_enabled": True, - "warn_after": { - "exact_failure": 2, - "same_tool_failure": 3, - "idempotent_no_progress": 2, - }, + "warn_after": {"exact_failure": 2, "same_tool_failure": 3, "idempotent_no_progress": 2}, "hard_stop_after": { "exact_failure": 5, "same_tool_failure": 8, "idempotent_no_progress": 5, }, - # Per-turn runaway-loop caps (inspired by Claude Code v2.1.212, - # Week 29, July 2026). Hard ceilings on how many times a runaway-prone - # tool may be called within a SINGLE agent loop (turn); the counters - # reset at the start of every turn, so a legitimate multi-turn session - # is never starved. They are always-on and fire regardless of the - # warn/hard-stop thresholds above. A single turn issuing dozens of web - # searches or spawning dozens of subagents is already pathological, so - # the defaults are low. Set either to 0 to disable that cap (unlimited). + # Per-turn hard ceilings for runaway-prone tools; counters reset every turn, always + # on regardless of the thresholds above. Dozens of searches/subagents in ONE turn + # is already pathological, hence low defaults. 0 = unlimited. "loop_caps": { - "max_web_searches": 50, # max web_search calls per turn (0 = unlimited) - "max_subagents": 50, # max subagents spawned per turn (0 = unlimited) + "max_web_searches": 50, # web_search calls per turn + "max_subagents": 50, # subagents spawned per turn }, }, "compression": { "enabled": True, - "checkpoint_required": False, # Fail closed before lossy compaction unless an - # active memory provider confirms checkpoint API - # compatibility and completes the checkpoint. - "progress_notices": False, # opt-in (#52995): when True, routine compression - # progress statuses (compacting/preflight/pre-API/ - # idle/retry) are delivered to chat gateway - # platforms instead of being suppressed by the - # gateway noise filter. Default False keeps - # routine compression silent-by-design on chat - # surfaces (server-side logging only). Failure - # notices and manual /compress feedback are - # always visible regardless of this setting. - "threshold": 0.50, # compress when context usage exceeds this ratio. - # Models with context windows below 512K are - # floored at 0.75 (raise-only) so compaction - # doesn't fire with half the window still free; - # set this above 0.75 to override the floor. - "threshold_tokens": None, # absolute token cap — when set, compression - # triggers at the lower of the ratio-based - # threshold and this token count. Clamped to - # the model's context length at apply-time. + # checkpoint_required: fail closed before lossy compaction unless an active memory + # provider confirms checkpoint API compatibility and completes the checkpoint. + "checkpoint_required": False, + # progress_notices: when True, routine compression progress statuses (compacting/ + # preflight/pre-API/idle/retry) reach chat gateways instead of being filtered as + # noise. Failure notices and manual /compress feedback are always visible. + "progress_notices": False, + # threshold: compress when context usage exceeds this ratio. Models with windows + # below 512K are floored at 0.75 (raise-only) so compaction doesn't fire with half + # the window free; set above 0.75 to override the floor. + "threshold": 0.50, + # threshold_tokens: absolute token cap — compression triggers at the lower of the + # ratio threshold and this count. Clamped to the model's context length. + "threshold_tokens": None, "target_ratio": 0.20, # fraction of threshold to preserve as recent tail - "tail_mode": "lean", # tail retention policy (#87326): - # "lean" — clamped 2.5%-of-window tail (default) - # (10K floor / 25K cap) plus chunked - # digests, a mechanical anchor index, - # verbatim user messages, and - # session_search recovery pointers in - # the summary. ~3x fewer retained - # tokens after compaction; costs a few - # extra summarizer calls at the - # compaction boundary. - # "legacy" — pre-#87326 0.20×threshold verbatim - # tail (100-240K tokens on big-window - # or raised-threshold setups). - "protect_last_n": 20, # minimum recent messages to keep uncompressed - "min_tail_user_messages": 1, # REAL (actionable) user messages guaranteed to - # survive in the uncompressed tail. 1 = existing - # single last-user anchor (default, behavior- - # preserving); raise to e.g. 3 to keep the last - # 3 real user turns verbatim when bulky tool - # outputs fill the tail token budget. - "max_attempts": 3, # compression retry rounds before a turn gives up - # with "max compression attempts reached". Raise - # (e.g. 6) for tool-schema-heavy sessions where 3 - # rounds cannot clear the request estimate. - # Validated >= 1, hard-capped at 10. - "proactive_prune_tokens": 0, # opt-in trigger (tokens) for the deterministic, - # no-LLM tool-result prune, run independently of - # `threshold` above. On large-window models - # `threshold` (≈50% of the window) rarely fires, - # so old tool output otherwise rides in history - # and is re-sent every turn; a low value like - # 48000 reclaims it early. 0 = off. Recent tail - # protected by `protect_last_n`. Built-in - # compressor only (other engines inherit a no-op). - # NOTE: each committed prune rewrites already-sent - # history, breaking the provider prompt-cache - # prefix — the min_reclaim gate below keeps those - # breaks episodic rather than per-turn. - "proactive_prune_min_result_chars": 8000, # the prune's summarize pass only - # touches tool results larger than this (chars); - # clamped to >= 200 so a generated summary can't - # itself be re-summarized. - "proactive_prune_min_reclaim_tokens": 4096, # a proactive prune only commits - # when it reclaims at least this many tokens - # (measured on the pruned output), then waits - # for a full trigger-sized token runway to - # regrow before rearming. Keeps prompt-cache - # breaks episodic. 0 = no minimum-savings gate. - "micro_compact": False, # opt-in: after each completed turn, fold the - # oldest un-absorbed exchange into a rolling - # summary, amortizing compression cost instead - # of paying it in one batch stall. Default False - # because a pass rewrites already-sent history - # and so breaks the provider prompt-cache prefix - # EVERY turn — the per-turn cache break that - # `proactive_prune_min_reclaim_tokens` above - # exists to avoid. Enable only when you have - # measured that the amortized stall is worth - # more to you than the cached-prefix discount. - # See docs/micro-compaction.md. - "micro_compact_every_n_turns": 1, # cadence: run a pass every Nth completed - # turn. Since each pass costs one prompt-cache - # break, this is the dial for how often that - # cost is paid — 1 reclaims most aggressively - # at one break per turn, 5 trades reclaim rate - # for a fifth of the breaks. Clamped to >= 1. - # Ignored unless `micro_compact` is true. - "micro_compact_defrag_threshold_tokens": 2000, # once the rolling summary - # exceeds this many tokens, the next pass - # re-summarizes the summary itself instead of - # letting it grow without bound. - "hygiene_hard_message_limit": 5000, # gateway session-hygiene force-compress threshold by message count - "hygiene_timeout_seconds": 30, # max seconds gateway waits for pre-agent hygiene compression - # WITHOUT forward progress. The summary call streams, so - # this is an inactivity budget: a slow model still - # producing tokens keeps extending the wait; only a - # silent/hung call is cut off. - "hygiene_total_ceiling_seconds": 600, # absolute cap on the hygiene compression wait even - # while tokens are still moving — bounds a degenerate - # trickle stream. Clamped to >= hygiene_timeout_seconds. - "hygiene_failure_cooldown_seconds": 300, # skip repeated failed hygiene attempts for this session - "hygiene_max_turn_hold_seconds": 10, # max seconds an ARRIVING user turn is held while a - # still-streaming hygiene summary finishes. Distinct from - # hygiene_timeout_seconds (compressor inactivity budget): - # this bounds user-visible latency once real input is - # waiting. Kept well under chat-transport idle timeouts - # (Telegram ~30s). On expiry the turn proceeds - # uncompressed — an availability boundary, not a failure. - # The detached worker keeps its commit admission when its - # commit is watermark-fenced, so the finished summary is - # adopted at the next safe boundary instead of being - # discarded (#97963 — thinking summary models). - "context_timeout_seconds": 120, # inactivity budget for in-agent compress_context - # (conversation loop, /compress, preflight, etc.). - # Same progress-aware semantics as hygiene_timeout_seconds: - # streamed summary tokens extend the wait; only a silent - # worker is cut off. 0 = disable the owned wrapper - # (callers that already pass commit_fence, e.g. gateway - # hygiene, never use this path). - "context_total_ceiling_seconds": 600, # absolute cap on the *pre-commit* - # in-agent compress_context wait (summary / - # stream phase) even while tokens are still - # moving. Clamped to >= context_timeout_seconds - # when the idle budget is > 0. Guarantee: - # the summary phase is bounded by this - # ceiling; an already-started SessionDB - # commit is never abandoned mid-flight — - # if the commit itself runs past the - # ceiling it is logged (WARNING, then - # ERROR) and surfaced to the user via the - # warning channel while the host keeps - # waiting in bounded increments for the - # commit to finish. - "protect_first_n": 3, # non-system head messages always preserved - # verbatim, in ADDITION to the system prompt - # (which is always implicitly protected). Set to - # 0 for long-running rolling-compaction sessions - # where you want nothing pinned except the - # system prompt + rolling summary + recent tail. - "abort_on_summary_failure": False, # When True, auto-compression that fails - # to generate a summary (aux LLM errored / returned - # non-JSON / timed out) aborts entirely instead of - # dropping the middle window with a static - # "summary unavailable" placeholder. Messages are - # preserved unchanged and the session "freezes" at - # its current size until the user runs /compress - # (which bypasses the failure cooldown) or /new. - # Default False matches historical behavior; set to - # True if you'd rather pause than silently lose - # context turns when your aux model is flaky. - "codex_gpt55_autoraise": True, # Historical key name kept for compatibility. - # When True, gpt-5.4 / gpt-5.5 / gpt-5.6 on the - # ChatGPT Codex OAuth route raise their compaction - # trigger to 85% (vs the global `threshold` above). - # Codex hard-caps these families at a 272K window, so - # the default 50% would compact at ~136K and waste half - # the usable context. Set to False to opt back down to - # the global threshold (e.g. 0.50) for those Codex - # sessions. Only this exact route is affected — - # gpt-5.4 / 5.5 / 5.6 on OpenAI's direct API, - # OpenRouter, and Copilot keep the global threshold - # regardless. - "codex_gpt55_autoraise_notice": True, # Display the one-time Codex gpt-5.4/5.5/5.6 - # autoraise banner. Set False to keep the - # 85% threshold autoraise but suppress the - # user-facing notice in CLI/gateway output. - "codex_app_server_auto": "native", # Codex app-server (codex CLI runtime) thread - # compaction mode. The codex agent owns the real - # thread context, so Hermes' summarizer cannot - # shrink it (#36801). native = codex decides when - # to compact its own thread (default); hermes = - # Hermes' compression threshold triggers - # thread/compact/start; off = never auto-trigger - # (codex may still compact natively). - "codex_responses_native": False, # Opt in to OpenAI's server-side compaction - # on the Responses API. Engages ONLY for - # gpt-5.6-family models on api.openai.com or - # the ChatGPT Codex backend; every other - # route/model is unaffected. Hermes' local - # compression stays armed as the fallback. - "codex_responses_compact_threshold": None, # Optional absolute server compaction - # trigger in input tokens. None follows the - # resolved local compression trigger with a - # safety margin. Explicit values only clamp - # downward so the server compacts first. - "in_place": True, # 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 #N` - # renumbering). Eliminates the session-rotation - # bug cluster (#33618 /goal loss, #14238 lost - # response, #33907 orphans, #45117 search gaps, - # #42228 null cwd) — see #38763. Non-destructive: - # the live context is compacted (lossy for what - # the model reloads), but the pre-compaction - # turns are soft-archived under the same id - # (active=0, compacted=1) — still searchable via - # session_search and recoverable, not deleted. - # Default True since 2107b86024; set False to - # restore the legacy rotating-compaction path. - "model_thresholds": {}, # Per-model threshold overrides. Keys are - # substring-matched against the model name - # (longest match wins); values replace the - # global `threshold` for that model, e.g. - # model_thresholds: - # "glm-5.2": 0.40 - # "claude-sonnet": 0.35 - # The small-context floor (0.75 for <512K - # models) still applies on top of overrides - # (raise-only: an override above the floor - # wins; one below it is raised to the floor). - "idle_compact_after_seconds": 0, # Opt-in idle compaction (0 = disabled). - # When > 0, a session that resumes after at - # least this many seconds of inactivity - # compacts its accumulated history up front, - # before the first reply — so a long-lived - # thread resumed hours later doesn't re-read - # its full stale context on every turn. - # Time-based; complements (does not replace) - # the size-based `threshold` above. Skipped - # when the context is already at/below the - # post-compression target (threshold × - # target_ratio) and it honors the same - # failure-cooldown / anti-thrash / per-session - # lock guards as every automatic compaction. - # Example: 1800 = compact after 30 min idle. + # tail_mode: "lean" = clamped 2.5%-of-window tail (10K floor / 25K cap) plus chunked + # digests, anchor index, verbatim user messages and session_search pointers in the + # summary (~3x fewer retained tokens; a few extra summarizer calls at the boundary). + # "legacy" = 0.20×threshold verbatim tail (100-240K tokens on big windows). + "tail_mode": "lean", + "protect_last_n": 20, # minimum recent messages kept uncompressed + # min_tail_user_messages: REAL (actionable) user messages guaranteed to survive in + # the tail. 1 = single last-user anchor; raise (e.g. 3) when bulky tool outputs + # fill the tail budget. + "min_tail_user_messages": 1, + # max_attempts: retry rounds before a turn gives up with "max compression attempts + # reached". Raise (e.g. 6) for tool-schema-heavy sessions. Validated >= 1, cap 10. + "max_attempts": 3, + # proactive_prune_tokens: opt-in trigger (tokens) for the deterministic no-LLM + # tool-result prune, independent of `threshold` (which rarely fires on large + # windows, so old tool output is re-sent every turn); e.g. 48000 reclaims early. + # 0 = off. Tail protected by `protect_last_n`. Built-in compressor only. Each + # committed prune rewrites sent history and breaks the prompt-cache prefix — the + # min_reclaim gate below keeps those breaks episodic. + "proactive_prune_tokens": 0, + # Prune's summarize pass only touches tool results larger than this (chars); + # clamped >= 200 so a generated summary can't be re-summarized. + "proactive_prune_min_result_chars": 8000, + # A prune only commits when it reclaims at least this many tokens, then waits for a + # trigger-sized runway to regrow before rearming. 0 = no minimum-savings gate. + "proactive_prune_min_reclaim_tokens": 4096, + # micro_compact: opt-in — after each turn fold the oldest un-absorbed exchange into a + # rolling summary, amortizing compression cost. Off by default because every pass + # rewrites sent history and breaks the prompt-cache prefix EVERY turn; enable only + # if the amortized stall beats the cached-prefix discount. See docs/micro-compaction.md. + "micro_compact": False, + # Cadence: run a pass every Nth completed turn (1 = one cache break per turn, 5 = + # a fifth of the breaks). Clamped >= 1; ignored unless micro_compact is true. + "micro_compact_every_n_turns": 1, + # Once the rolling summary exceeds this many tokens, the next pass re-summarizes it. + "micro_compact_defrag_threshold_tokens": 2000, + # Gateway session-hygiene force-compress threshold, by message count. + "hygiene_hard_message_limit": 5000, + # Max seconds the gateway waits for pre-agent hygiene compression WITHOUT forward + # progress. Inactivity budget: a slow model still streaming tokens extends the wait. + "hygiene_timeout_seconds": 30, + # Absolute cap on the hygiene wait even while tokens are moving (bounds a trickle + # stream). Clamped >= hygiene_timeout_seconds. + "hygiene_total_ceiling_seconds": 600, + "hygiene_failure_cooldown_seconds": 300, # skip repeated failed hygiene attempts + # Max seconds an ARRIVING user turn is held while a streaming hygiene summary + # finishes; bounds user-visible latency (keep under chat idle timeouts, Telegram + # ~30s). On expiry the turn proceeds uncompressed; the detached worker keeps its + # watermark-fenced commit, so the summary is adopted at the next safe boundary. + "hygiene_max_turn_hold_seconds": 10, + # Inactivity budget for in-agent compress_context (loop, /compress, preflight); + # same progress-aware semantics as hygiene_timeout_seconds. 0 = disable the owned + # wrapper (callers passing commit_fence, e.g. gateway hygiene, never use it). + "context_timeout_seconds": 120, + # Absolute cap on the *pre-commit* compress_context wait (summary/stream phase) even + # while tokens move. Clamped >= context_timeout_seconds when that is > 0. A started + # SessionDB commit is never abandoned: past the ceiling it is logged (WARNING, then + # ERROR) and surfaced on the warning channel while the host keeps waiting. + "context_total_ceiling_seconds": 600, + # Non-system head messages always kept verbatim, in ADDITION to the (always + # protected) system prompt. 0 = pin nothing but system prompt + summary + tail. + "protect_first_n": 3, + # When True, auto-compression whose summary fails (aux error / non-JSON / timeout) + # aborts instead of dropping the middle with a "summary unavailable" placeholder; + # the session freezes at its size until /compress (bypasses the cooldown) or /new. + "abort_on_summary_failure": False, + # (Historical key name.) When True, gpt-5.4/5.5/5.6 on the ChatGPT Codex OAuth route + # raise their compaction trigger to 85%: Codex hard-caps them at a 272K window, so + # the global 50% would compact at ~136K. False = global `threshold`. Only that route; + # the same models via OpenAI direct, OpenRouter or Copilot keep the global value. + "codex_gpt55_autoraise": True, + # Show the one-time autoraise banner; False keeps the autoraise, hides the notice. + "codex_gpt55_autoraise_notice": True, + # Codex app-server thread compaction mode. The codex agent owns the thread context, + # so Hermes' summarizer cannot shrink it. native = codex decides; hermes = Hermes' + # threshold triggers thread/compact/start; off = never auto-trigger. + "codex_app_server_auto": "native", + # Opt in to OpenAI server-side compaction on the Responses API. Only gpt-5.6-family + # on api.openai.com or the Codex backend; local compression stays as fallback. + "codex_responses_native": False, + # Absolute server compaction trigger (input tokens). None follows the local trigger + # with a safety margin; explicit values only clamp downward so the server goes first. + "codex_responses_compact_threshold": None, + # in_place: compaction rewrites the message list and system prompt WITHOUT rotating + # the session id (no parent_session_id chain, no `name #N` renumbering), avoiding + # the session-rotation bug cluster. Pre-compaction turns are soft-archived under the + # same id (active=0, compacted=1) — still session_search-able. False = legacy + # rotating-compaction path. + "in_place": True, + # Per-model threshold overrides: keys substring-match the model name (longest wins), + # values replace the global `threshold`, e.g. {"glm-5.2": 0.40}. The <512K floor + # (0.75) still applies raise-only on top. + "model_thresholds": {}, + # Opt-in idle compaction (0 = off): a session resuming after this many idle seconds + # compacts up front, before the first reply. Time-based complement to `threshold`; + # skipped when already at/below threshold × target_ratio; honors the same cooldown/ + # anti-thrash/lock guards. Example: 1800 = 30 min. + "idle_compact_after_seconds": 0, }, - # Anthropic prompt caching (Claude via OpenRouter or native Anthropic API). - # cache_ttl: "5m" or "1h" (Anthropic-supported tiers). Other non-falsy - # values are silently ignored. Falsy values (false, null, "off", - # "disabled", "no", "none") disable prompt caching entirely. - "prompt_caching": { - "cache_ttl": "5m", - }, + # Anthropic prompt caching (Claude via OpenRouter or native API). cache_ttl: "5m" | "1h"; + # other non-falsy values are ignored; falsy (false, null, "off", "disabled", "no", + # "none") disables caching. + "prompt_caching": {"cache_ttl": "5m"}, - # OpenRouter-specific settings. - # response_cache: enable OpenRouter response caching (X-OpenRouter-Cache header). - # When enabled, identical requests return cached responses for free (zero billing). - # This is separate from Anthropic prompt caching and works alongside it. - # See: https://openrouter.ai/docs/guides/features/response-caching - # response_cache_ttl: how long cached responses remain valid, in seconds (1-86400). - # Default 300 (5 minutes). Only used when response_cache is enabled. - # min_coding_score: knob for the openrouter/pareto-code router (0.0-1.0). - # Only applied when model.model is "openrouter/pareto-code". Higher - # values route to stronger (more expensive) coders; lower values open - # up cheaper, faster options. Default 0.65 lands on the mid-tier - # coder on the current Pareto frontier. Empty string = let OpenRouter - # pick the strongest available coder (router's documented default - # when the plugins block is omitted). - # See: https://openrouter.ai/docs/guides/routing/routers/pareto-router - "openrouter": { - "response_cache": True, - "response_cache_ttl": 300, - "min_coding_score": 0.65, - }, + # OpenRouter settings. response_cache: X-OpenRouter-Cache header — identical requests + # return cached responses at zero billing; independent of Anthropic prompt caching. + # response_cache_ttl: seconds (1-86400), only used when response_cache is on. + # min_coding_score (0.0-1.0): pareto-code router knob, applied only when model.model is + # "openrouter/pareto-code"; higher = stronger/pricier coders, 0.65 = mid-tier, "" = let + # OpenRouter pick the strongest. Docs: openrouter.ai/docs/guides/routing/routers/pareto-router + "openrouter": {"response_cache": True, "response_cache_ttl": 300, "min_coding_score": 0.65}, - # AWS Bedrock provider configuration. - # Only used when model.provider is "bedrock". + # AWS Bedrock; only used when model.provider is "bedrock". "bedrock": { - "region": "", # AWS region for Bedrock API calls (empty = AWS_REGION env var → us-east-1) + "region": "", # empty = AWS_REGION env var → us-east-1 "discovery": { - "enabled": True, # Auto-discover models via ListFoundationModels - "provider_filter": [], # Only show models from these providers (e.g. ["anthropic", "amazon"]) - "refresh_interval": 3600, # Cache discovery results for this many seconds + "enabled": True, # auto-discover models via ListFoundationModels + "provider_filter": [], # restrict to these providers, e.g. ["anthropic", "amazon"] + "refresh_interval": 3600, # cache discovery results (seconds) }, + # Bedrock Guardrails: create one in the console, then set ID and version. + # https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails.html "guardrail": { - # Amazon Bedrock Guardrails — content filtering and safety policies. - # Create a guardrail in the Bedrock console, then set the ID and version here. - # See: https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails.html "guardrail_identifier": "", # e.g. "abc123def456" "guardrail_version": "", # e.g. "1" or "DRAFT" - "stream_processing_mode": "async", # "sync" or "async" - "trace": "disabled", # "enabled", "disabled", or "enabled_full" + "stream_processing_mode": "async", # "sync" | "async" + "trace": "disabled", # "enabled" | "disabled" | "enabled_full" }, }, - # Auxiliary model config — provider:model for each side task. - # Format: provider is the provider name, model is the model slug. - # "auto" for provider = auto-detect best available provider. - # Empty model = use provider's default auxiliary model. - # All tasks fall back to openrouter:google/gemini-3-flash-preview if - # the configured provider is unavailable. - # - # extra_body: forwarded verbatim as request body fields on every aux call - # for that task. Use this to set provider-specific knobs (independent of - # main-agent settings). On OpenRouter you can set provider routing prefs - # and the Pareto Code coding-score floor here. Example: - # + # Auxiliary model config — provider/model per side task. provider "auto" = auto-detect; + # empty model = provider's default aux model; all tasks fall back to + # openrouter:google/gemini-3-flash-preview when the configured provider is unavailable. + # extra_body is forwarded verbatim as request body fields for that task, e.g. OpenRouter + # routing prefs / Pareto Code floor: # auxiliary: # compression: - # provider: openrouter - # model: openrouter/pareto-code # extra_body: - # provider: # OpenRouter provider routing - # order: [anthropic, google] - # sort: throughput # or price | latency - # plugins: # OpenRouter Pareto Code router - # - id: pareto-router - # min_coding_score: 0.5 - # - # Each aux task is independent — main-agent provider_routing and - # openrouter.min_coding_score do NOT propagate to aux calls by design. + # provider: {order: [anthropic, google], sort: throughput} # or price | latency + # plugins: [{id: pareto-router, min_coding_score: 0.5}] + # Each task is independent — main-agent provider_routing and openrouter.min_coding_score + # do NOT propagate to aux calls by design. "auxiliary": { - # Same-provider retries for a transient transport blip (connection - # reset / timeout / 5xx / 408) on ANY auxiliary call before falling - # back. Default 2 (→ 3 total attempts), clamped [0,6]. Matters most for - # pinned calls like MoA reference advisors, where provider fallback is - # not a meaningful recovery, so an unretried blip silently loses the - # call. + # Same-provider retries for a transient blip (reset/timeout/5xx/408) on ANY aux call + # before falling back; clamped [0,6]. Matters for pinned calls (MoA advisors) where + # provider fallback is not meaningful recovery. "transient_retries": 2, - # Restrict the auxiliary auto-chain's OpenRouter fallback to free - # (:free) SKUs. When true, the OpenRouter step is skipped entirely - # unless the resolved fallback model ends in ":free" — a PAID lane - # is never engaged for background auxiliary traffic (compression, - # title generation, session search, vision, web extract) even when - # OPENROUTER_API_KEY is present. Default false keeps the historical - # paid fallback for users who want it. + # When true, the auto-chain's OpenRouter step is skipped unless the fallback model + # ends in ":free" — a PAID lane is never used for background aux traffic even with + # OPENROUTER_API_KEY set. "free_only": False, - # Override the auxiliary auto-chain's OpenRouter fallback model - # (default: google/gemini-3.6-flash, a PAID model). Set e.g. - # "nvidia/nemotron-3-ultra-550b-a55b:free" together with - # free_only: true to keep auxiliary traffic free-only. A one-time - # WARNING is logged whenever a non-":free" model is engaged. + # Override the auto-chain's OpenRouter fallback model (default google/gemini-3.6-flash, + # PAID). Pair e.g. "nvidia/nemotron-3-ultra-550b-a55b:free" with free_only: true. + # A one-time WARNING is logged whenever a non-":free" model is engaged. "openrouter_model": "", - # Endpoints that reject NON-streaming chat requests outright (e.g. - # Tencent Copilot returns HTTP 400 "Non-stream chat request is - # currently not supported"). Auxiliary calls to a matching endpoint - # are sent with stream=True and aggregated client-side. Entries are - # case-insensitive substrings matched against the endpoint URL; - # copilot.tencent.com is always treated as stream-only. + # Endpoints that reject NON-streaming chat (HTTP 400): aux calls are sent with + # stream=True and aggregated. Case-insensitive URL substrings; copilot.tencent.com + # is always stream-only. "stream_only_base_urls": [], - "vision": { - "provider": "auto", # auto | openrouter | nous | codex | custom - "model": "", # e.g. "google/gemini-2.5-flash", "gpt-4o" - "base_url": "", # direct OpenAI-compatible endpoint (takes precedence over provider) - "api_key": "", # API key for base_url (falls back to OPENAI_API_KEY) - "timeout": 120, # seconds — LLM API call timeout; vision payloads need generous timeout - "extra_body": {}, # OpenAI-compatible provider-specific request fields - "reasoning_effort": "", # per-task thinking level: none|minimal|low|medium|high|xhigh|max|ultra (empty = provider default) - "download_timeout": 30, # seconds — image HTTP download timeout; increase for slow connections - }, - # Note: web_extract no longer uses an auxiliary LLM — pages are - # truncate-and-stored with a read_file pointer (no summarization), - # and browser snapshots follow the same pattern. The old - # ``auxiliary.web_extract.*`` block was removed here. Existing - # values in user config.yaml files are harmless leftovers and ignored. - "compression": { - "provider": "auto", - "model": "", - "base_url": "", - "api_key": "", - "timeout": 120, # seconds — compression summarises large contexts; increase for local models - "extra_body": {}, - "reasoning_effort": "", # per-task thinking level: none|minimal|low|medium|high|xhigh|max|ultra (empty = provider default) - # Guarded fast lane: only honored with a concrete provider/model - # and an explicit ``reasoning_effort: none`` certification. - # Zero preserves the historic uncapped compression request. - "max_output_tokens": 0, - }, - # Note: session_search no longer uses an auxiliary LLM (PR #27590 — - # single-shape tool returns DB content directly). The old - # ``auxiliary.session_search.*`` block was removed here. Existing - # values in user config.yaml files are harmless leftovers and ignored. - "skills_hub": { - "provider": "auto", - "model": "", - "base_url": "", - "api_key": "", - "timeout": 30, - "extra_body": {}, - "reasoning_effort": "", # per-task thinking level: none|minimal|low|medium|high|xhigh|max|ultra (empty = provider default) - }, - "approval": { - "provider": "auto", - "model": "", # fast/cheap model recommended (e.g. gemini-flash, haiku) - "base_url": "", - "api_key": "", - "timeout": 30, - "extra_body": {}, - "reasoning_effort": "", # per-task thinking level: none|minimal|low|medium|high|xhigh|max|ultra (empty = provider default) - }, - # /review — the independent reviewer subagent's model. Unlike other - # aux tasks this is not a single LLM call: the reviewer is a full - # subagent (all normal subagent tools) spawned on the async - # delegation rail. provider/model/base_url/api_key/api_mode are - # resolved through the same credential system as delegation.provider - # pins. Leave provider "auto" + model empty to run the reviewer on - # the main agent's model. - "review": { - "provider": "auto", # auto (= inherit main model) | openrouter | nous | anthropic | ... - "model": "", # e.g. "anthropic/claude-opus-4.6" — a strong reviewer model - "base_url": "", # direct OpenAI-compatible endpoint (takes precedence over provider) - "api_key": "", # API key for base_url / provider override - "api_mode": "", # force transport: chat_completions | anthropic_messages | codex_responses - }, - "mcp": { - "provider": "auto", - "model": "", - "base_url": "", - "api_key": "", - "timeout": 30, - "extra_body": {}, - "reasoning_effort": "", # per-task thinking level: none|minimal|low|medium|high|xhigh|max|ultra (empty = provider default) - }, + # Per-task blocks share one shape (_aux): provider "auto" = inherit the main model; + # base_url overrides provider; api_key falls back to OPENAI_API_KEY; reasoning_effort: + # none|minimal|low|medium|high|xhigh|max|ultra ("" = provider default); extra_body = + # OpenAI-compatible request fields. Vision: download_timeout = image HTTP download (s). + "vision": _aux(120, download_timeout=30), + # web_extract and session_search no longer use an aux LLM; leftover blocks in user + # config are ignored. Compression: raise timeout for local models. max_output_tokens + # is only honored with a concrete provider/model AND ``reasoning_effort: none``; + # 0 = uncapped. + "compression": _aux(120, max_output_tokens=0), + "skills_hub": _aux(30), + "approval": _aux(30), # classifier — a fast/cheap model is recommended + # /review reviewer: a full subagent on the async delegation rail, credentials + # resolved like delegation.provider pins. "auto" + "" = main agent's model. + # api_mode forces transport: chat_completions | anthropic_messages | codex_responses. + "review": {"provider": "auto", "model": "", "base_url": "", "api_key": "", "api_mode": ""}, + "mcp": _aux(30), + # prefer_fast_model opts in to the provider fast tier; auto otherwise = main model. "title_generation": { "enabled": True, "provider": "auto", "model": "", - "prefer_fast_model": False, # opt in to provider fast tier; auto otherwise uses the main model + "prefer_fast_model": False, "base_url": "", "api_key": "", "timeout": 30, "extra_body": {}, - "reasoning_effort": "", # per-task thinking level: none|minimal|low|medium|high|xhigh|max|ultra (empty = provider default) + "reasoning_effort": "", "language": "", }, - "memory_query_rewrite": { - "provider": "auto", - "model": "", - "base_url": "", - "api_key": "", - "timeout": 8, - "extra_body": {}, - }, - "tts_audio_tags": { - "provider": "auto", - "model": "", - "base_url": "", - "api_key": "", - "timeout": 30, - "extra_body": {}, - "reasoning_effort": "", # per-task thinking level: none|minimal|low|medium|high|xhigh|max|ultra (empty = provider default) - }, - # Triage specifier — flesh out a rough one-liner in the Kanban - # Triage column into a concrete spec, then promote it to ``todo``. - # Invoked by ``hermes kanban specify`` (single id or --all). Set a - # cheap, capable model here (gemini-flash works well); the main - # model is overkill for short spec expansion. - "triage_specifier": { - "provider": "auto", - "model": "", - "base_url": "", - "api_key": "", - "timeout": 120, - "extra_body": {}, - "reasoning_effort": "", # per-task thinking level: none|minimal|low|medium|high|xhigh|max|ultra (empty = provider default) - }, - # Kanban decomposer — decomposes a triage task into a graph of - # child tasks routed to specialist profiles by description. - # Invoked by ``hermes kanban decompose`` and the kanban - # auto-decompose dispatcher tick. Returns a JSON task graph; - # uses more tokens than the specifier so allow more headroom. - "kanban_decomposer": { - "provider": "auto", - "model": "", - "base_url": "", - "api_key": "", - "timeout": 180, - "extra_body": {}, - "reasoning_effort": "", # per-task thinking level: none|minimal|low|medium|high|xhigh|max|ultra (empty = provider default) - }, - # Profile describer — auto-generates a 1-2 sentence description - # of what a profile is good at. Invoked by - # ``hermes profile describe --auto`` and the dashboard's - # auto-generate button. Short, cheap call. - "profile_describer": { - "provider": "auto", - "model": "", - "base_url": "", - "api_key": "", - "timeout": 60, - "extra_body": {}, - "reasoning_effort": "", # per-task thinking level: none|minimal|low|medium|high|xhigh|max|ultra (empty = provider default) - }, - # Goal judge — evaluates whether a /goal run's latest response - # satisfies the goal/contract, and drafts goal contracts. Short - # structured-JSON calls; a fast cheap model is fine. - "goal_judge": { - "provider": "auto", - "model": "", - "base_url": "", - "api_key": "", - "timeout": 60, - "extra_body": {}, - "reasoning_effort": "", # per-task thinking level: none|minimal|low|medium|high|xhigh|max|ultra (empty = provider default) - }, - # Curator — skill-usage review fork. Timeout is generous because the - # review pass can take several minutes on reasoning models (umbrella - # building over hundreds of candidate skills). "auto" = use main chat - # model; override via `hermes model` → auxiliary → Curator to route - # to a cheaper aux model (e.g. openrouter google/gemini-3-flash-preview). - "curator": { - "provider": "auto", - "model": "", - "base_url": "", - "api_key": "", - "timeout": 600, - "extra_body": {}, - "reasoning_effort": "", # per-task thinking level: none|minimal|low|medium|high|xhigh|max|ultra (empty = provider default) - }, - # Monitor — urgency/importance classifier used by the important-mail - # monitor catalog automation (cron/scripts/classify_items.py). Scores - # candidate items 0-10 against the user's criteria so only above- - # threshold items get delivered. "auto" = main chat model; override to - # a cheap fast model (e.g. openrouter google/gemini-3-flash-preview, - # haiku) since per-item scoring is high-volume and a small model is fine. - "monitor": { - "provider": "auto", - "model": "", - "base_url": "", - "api_key": "", - "timeout": 60, - "extra_body": {}, - "reasoning_effort": "", # per-task thinking level: none|minimal|low|medium|high|xhigh|max|ultra (empty = provider default) - }, - # Background review — the post-turn self-improvement fork that decides - # whether to save a memory / patch a skill. "auto" (default) = run on - # the main chat model, replaying the full conversation, which is already - # warm in the prompt cache (cheap cache reads) — unchanged, optimal. - # Set provider/model to a cheaper model (e.g. openrouter - # google/gemini-3-flash-preview) to run the review there for ~3-5x lower - # cost. A different model can't reuse the main prompt cache anyway, so - # the fork automatically replays a compact digest instead of the full - # transcript when routed (minimises the cold-write). Same model = full - # replay; different model = digest. Quality holds (memory capture - # identical, skill near-identical in benchmarks). - "background_review": { - # Master switch for automatic post-turn memory/skill review forks. - # false = skip automatic spawns (manual /refine still works). - "enabled": True, - "provider": "auto", - "model": "", - "base_url": "", - "api_key": "", - "timeout": 120, - "extra_body": {}, - "reasoning_effort": "", # per-task thinking level: none|minimal|low|medium|high|xhigh|max|ultra (empty = provider default) - # Aggregate INPUT-token budget for one review fork (issue #93057). - # The fork's FIRST request replays the full snapshot as a warm - # prompt-cache read (compaction is deferred until the first - # provider response arrives); after that it compacts an oversized - # snapshot in memory before further provider calls. This caps the - # SUM of input tokens replayed across the whole review tool loop - # (iterations are separately capped at 16). The loop stops before - # the provider call that would cross the budget. 0 or a negative - # value = unlimited. - "max_input_tokens": 600000, - }, - "moa_reference": { - "provider": "auto", - "model": "", - "base_url": "", - "api_key": "", - "timeout": 900, - "extra_body": {}, - # NOTE: no reasoning_effort here by design — MoA reasoning depth is - # configured PER SLOT in the MoA preset (moa.presets.. - # reference_models[].reasoning_effort / aggregator.reasoning_effort), - # not at the auxiliary-task level. - }, - "moa_aggregator": { - "provider": "auto", - "model": "", - "base_url": "", - "api_key": "", - "timeout": 900, - "extra_body": {}, - # NOTE: no reasoning_effort here by design — see moa_reference above. - }, + "memory_query_rewrite": _aux(8, reasoning_effort=False), + "tts_audio_tags": _aux(30), + # Kanban: triage_specifier expands a Triage one-liner into a spec (cheap model OK); + # kanban_decomposer emits a JSON graph of child tasks (more tokens). + "triage_specifier": _aux(120), + "kanban_decomposer": _aux(180), + "profile_describer": _aux(60), # 1-2 sentence profile blurb; short, cheap + "goal_judge": _aux(60), # /goal satisfaction + contract drafting; JSON calls + # Curator skill-usage review can take minutes on reasoning models (umbrellas over + # hundreds of skills); route cheaper via `hermes model` → auxiliary → Curator. + "curator": _aux(600), + "monitor": _aux(60), # important-mail 0-10 scorer; high-volume, small model fine + # Post-turn self-improvement fork (save memory / patch skill). "auto" = main model + # replaying the full conversation (warm cache); other models replay a compact digest + # (~3-5x cheaper). enabled=false skips auto spawns (/refine still works). + # max_input_tokens caps the SUM of replayed input tokens over the review loop + # (iterations capped at 16); the loop stops before crossing it. <= 0 = unlimited. + "background_review": {"enabled": True, **_aux(120), "max_input_tokens": 600000}, + # No reasoning_effort on MoA blocks by design — configured PER SLOT in the preset + # (moa.presets..reference_models[].reasoning_effort / aggregator.reasoning_effort). + "moa_reference": _aux(900, reasoning_effort=False), + "moa_aggregator": _aux(900, reasoning_effort=False), }, - + "display": { "compact": False, "personality": "", "resume_display": "full", - # Recap tuning for /resume and startup resume. The defaults match the - # historical hardcoded values; expose them as config so power users can - # widen or tighten the snapshot to taste. + # Recap tuning for /resume and startup resume. "resume_exchanges": 10, # max user+assistant pairs to show "resume_max_user_chars": 300, # truncate user message text "resume_max_assistant_chars": 200, # truncate non-last assistant text "resume_max_assistant_lines": 3, # truncate non-last assistant lines - # When True (default), assistant entries that are *only* tool calls - # (no visible text) are skipped in the recap. This prevents the recap - # from being dominated by `[2 tool calls: terminal, read_file]` lines - # when an exchange was tool-heavy. Set False to restore the legacy - # behavior of showing tool-call summaries inline. + # Skip tool-call-only assistant entries in the recap so it isn't dominated by + # `[2 tool calls: ...]` lines; False shows them inline. "resume_skip_tool_only": True, "busy_input_mode": "interrupt", # interrupt | queue | steer - # When busy_input_mode="steer", suppress only the visible - # "Steered into current run" confirmation bubble by setting this false. - # The mid-turn steering itself still happens. + # steer mode: false hides only the "Steered into current run" bubble; steering + # itself still happens. "busy_steer_ack_enabled": True, - # Classic CLI multiline fallbacks beyond Alt+Enter. - # Default true matches Claude Code / Codex / OpenCode: Ctrl+J inserts - # a newline, a trailing backslash followed by Enter continues the draft, - # and supported terminals are asked to report Shift+Enter distinctly. - # Set false to restore the legacy c-j submit fallback on unusual POSIX - # PTYs whose plain Enter arrives as LF instead of CR. + # Classic CLI multiline beyond Alt+Enter: Ctrl+J newline, trailing backslash+Enter + # continues, Shift+Enter reported distinctly. False restores the c-j submit fallback + # for POSIX PTYs whose plain Enter arrives as LF. "cli_multiline_shortcuts": True, - # Which interface bare `hermes` (and `hermes chat`) launches by default: - # "cli" — the classic prompt_toolkit REPL (default, preserves prior behavior) - # "tui" — the modern Ink TUI (same as passing `--tui`) - # Explicit flags always win over this setting: `--cli` forces the classic - # REPL and `--tui` (or HERMES_TUI=1) forces the TUI regardless of config. + # Interface bare `hermes`/`hermes chat` launches: "cli" (prompt_toolkit REPL) | "tui" + # (Ink). Flags win: `--cli` forces the REPL, `--tui` / HERMES_TUI=1 forces the TUI. "interface": "cli", - # When true, `hermes --tui` auto-resumes the most recent human- - # facing session on launch instead of forging a fresh one. - # Mirrors `hermes -c` muscle memory. Default off so existing - # users aren't surprised. HERMES_TUI_RESUME= always wins. + # `hermes --tui` auto-resumes the most recent human-facing session (like `hermes -c`). + # HERMES_TUI_RESUME= always wins. "tui_auto_resume_recent": False, - # When true (default), the Desktop app reopens the last chat (or - # last page) on cold start. Set false to always land on a fresh - # new chat. Also a switch in Desktop Settings → Appearance. + # Desktop reopens the last chat/page on cold start (also in Settings → Appearance). "resume_last_session": True, - # When true (default), `hermes --tui` drops a one-time hint - # ("subagents working · /agents to watch live") the first time a turn - # starts delegating, nudging the user toward the live spawn-tree - # dashboard. Set false to suppress the hint. + # One-time TUI hint ("subagents working · /agents to watch live") on first delegation. "tui_agents_nudge": True, "bell_on_complete": False, - # Bell when a blocking prompt opens (clarify/approval/sudo/secret). - "bell_on_prompt": False, - # Stream the model's reasoning/thinking live before the response. - # Default ON: on thinking models the reasoning phase can run tens of - # seconds, and with this off the user stares at a spinner the whole - # time even though tokens are streaming. Set false for quiet output. + "bell_on_prompt": False, # bell when a blocking prompt opens (clarify/approval/sudo) + # Stream reasoning live before the response; otherwise thinking models show only a + # spinner for tens of seconds. "show_reasoning": True, - # When reasoning display is on, the post-response "Reasoning" recap box - # collapses long thinking to the first 10 lines. Set true to print the - # complete thinking text uncollapsed (live streaming is always full). + # Post-response "Reasoning" recap collapses to 10 lines; true prints it all + # (live streaming is always full). "reasoning_full": False, - # Background self-improvement review notifications surfaced in chat. - # "off" — no chat notification (the review still runs and writes) - # "on" — generic "💾 Memory updated" line (default) - # "verbose" — include a compact content preview of what changed - # Per-platform overrides via display.platforms..memory_notifications. + # Background self-improvement notices in chat: "off" (review still runs) | "on" + # (generic "💾 Memory updated") | "verbose" (content preview). Per-platform via + # display.platforms..memory_notifications. "memory_notifications": "on", - # Gateway notifications when a terminal(background=true) process - # finishes: - # "concise" — one-line status message; failures append a short - # output tail (default) - # "all" — running-output updates + final raw-output message - # "result" — final raw-output message only - # "error" — final raw-output message only on non-zero exit - # "off" — no watcher messages at all + # Gateway notices when a terminal(background=true) process finishes: "concise" + # (one line; failures append an output tail) | "all" (running updates + final raw + # output) | "result" (final raw only) | "error" (raw only on non-zero exit) | "off". "background_process_notifications": "concise", "streaming": False, - "timestamps": False, # Show message timestamps (CLI labels, TUI rows, desktop transcript) - "timestamp_format": "%H:%M", # strftime format for timestamps (e.g. "%b-%d %H:%M") + "timestamps": False, # message timestamps (CLI labels, TUI rows, desktop transcript) + "timestamp_format": "%H:%M", # strftime format, e.g. "%b-%d %H:%M" "final_response_markdown": "strip", # render | strip | raw - # Preserve recent classic CLI output across Ctrl+L, /redraw, and - # terminal resize full-screen clears. Disable if a terminal emulator - # behaves badly with replayed scrollback. + # Preserve recent classic-CLI output across Ctrl+L, /redraw and resize clears; + # disable if an emulator misbehaves with replayed scrollback. "persistent_output": True, "persistent_output_max_lines": 200, - # Clear terminal scrollback as well as the visible viewport when the - # classic CLI performs a full redraw/resize recovery. Disabled by - # default because some users prefer preserving terminal history; - # enable when a terminal/tmux stack stamps stale prompt chrome into - # scrollback during fullscreen/restore window transitions. + # Also clear terminal scrollback on classic-CLI full redraw/resize recovery; enable + # when a terminal/tmux stack stamps stale prompt chrome into scrollback. "cli_rebuild_scrollback_on_redraw": False, - # Print a one-line summary of resolved modal prompts (approval / - # clarify) into scrollback so the question and decision survive the - # panel repaint. Set false to keep scrollback untouched. + # Print a one-line summary of resolved modal prompts (approval/clarify) to scrollback. "persist_prompts": True, - "inline_diffs": True, # Show inline diff previews for write actions (write_file, patch, skill_manage) - # File-mutation verifier footer. When true (default), the agent - # appends a one-line advisory to its final response whenever a - # write_file / patch call failed during the turn and was never - # superseded by a successful write to the same path. This catches - # the "batch of parallel patches, half fail, model claims success" - # class of over-claim that otherwise forces users to run - # `git status` to verify edits landed. Set false to suppress. + "inline_diffs": True, # inline diff previews for write_file/patch/skill_manage + # Append a one-line advisory to the final response when a write_file/patch failed + # this turn and was never superseded by a successful write to the same path + # (catches "half the parallel patches failed, model claims success"). "file_mutation_verifier": True, - # Nous credits status-bar notices (usage bands, grant-spent, depleted / - # restored). When false, no credits notices are emitted — balance data - # is still captured and /usage keeps working. Off switch for sub + - # top-up users who find the gauge noisy. + # Nous credits status-bar notices (usage bands, grant-spent, depleted/restored). + # False mutes them; balance data and /usage keep working. "credits_notices": True, - # Turn-completion explainer. When true (default), the agent appends a - # one-line explanation to its final response whenever a turn ends - # abnormally with no usable reply — empty content after retries, a - # partial/truncated stream, a still-pending tool result, or an - # iteration/budget limit. Replaces the bare "(empty)" sentinel so the - # failure isn't silent from the UI's perspective. Set false to suppress. + # Append a one-line explanation when a turn ends with no usable reply (empty after + # retries, truncated stream, pending tool result, iteration/budget limit) instead of + # the bare "(empty)" sentinel. "turn_completion_explainer": True, - "show_cost": False, # Show $ cost in the status bar (off by default) - # Show a color-coded battery read-out as the first status-bar element in - # the CLI/TUI (off by default). No-op on machines without a battery. - "battery": False, - # Focus view (/focus): display-only reduced-output mode. When true the - # CLI/TUI pins tool_progress to "off" (reusing the existing suppression - # path), reports a per-turn hidden-line count with a recovery hint, and - # pins a "focus" segment in the status bar. focus_saved_tool_progress - # holds the mode /focus off restores. Never affects what is sent to the - # model — see hermes_cli/focus_view.py. + "show_cost": False, # $ cost in the status bar + "battery": False, # battery read-out first in status bar; no-op w/o battery + # Focus view (/focus): display-only. Pins tool_progress to "off", reports per-turn + # hidden-line count, pins a "focus" status segment. focus_saved_tool_progress holds + # the mode /focus off restores. Never affects what the model sees (focus_view.py). "focus_view": False, "focus_saved_tool_progress": "all", "skin": "default", - # UI language for static user-facing messages (approval prompts, a - # handful of gateway slash-command replies). Does NOT affect agent - # responses, log lines, tool outputs, or slash-command descriptions. - # Supported: en, zh, ja, de, es, fr, tr, uk. Unknown values fall back to en. + # UI language for static messages (approval prompts, some gateway slash replies); not + # agent responses/logs/tool outputs. en, zh, ja, de, es, fr, tr, uk; unknown → en. "language": "en", - # TUI busy indicator style: kaomoji (default), emoji, unicode (braille - # spinner), or ascii. Live-swappable via `/indicator