Salvage of PR #67447 — the original PR fixed 3 of 7 missing keys.
gateway/config.py reads 4 more top-level keys (stt_echo_transcripts,
reset_triggers, always_log_local, filter_silence_narration) that
produced the same false 'Unknown top-level config key' warning.
Add all 4 and extend the regression test to cover them.
Hermes writes known_plugin_toolsets via tools_config and bridges
group_sessions_per_user / thread_sessions_per_user in gateway/config,
but doctor treated them as unknown top-level keys. Add them to
_EXTRA_KNOWN_ROOT_KEYS so validation matches keys Hermes itself uses.
grok-4.5 is xAI's newest release (their versioning is non-monotonic:
4.5 > 4.20) and is the model xAI's own docs use for the server-side
x_search tool. Users who explicitly pinned x_search.model keep their
choice; everyone else picks up the new default via the config
deep-merge — no _config_version bump needed.
- tools/x_search_tool.py: DEFAULT_X_SEARCH_MODEL
- hermes_cli/config.py: DEFAULT_CONFIG x_search.model + comment
- agent/reasoning_timeouts.py: 300s stale-timeout floor entry for
grok-4.5 (grok-4.20-reasoning entry kept for pinned users)
- docs: x-search.md en + zh-Hans (config sample + troubleshooting)
- tests: default-model assertion + timeout-floor positive case
Follow-up to the salvaged #56724: the runtime's _generate_xai_tts reads
voice_id, language, speed, auto_speech_tags, optimize_streaming_latency,
sample_rate, and bit_rate — but never text_normalization, and the xAI
/v1/tts payload builder has no such field. Surfacing it in the desktop
GUI would be a dead knob, so remove it from DEFAULT_CONFIG, constants.ts
(labels/descriptions/SECTIONS), and the ja/zh/zh-hant locale catalogs.
The other six xAI keys are all verified against tools/tts_tool.py.
* fix(env): recognize export-prefixed .env lines in save/remove (#40041)
load_env() parses bash-compatible 'export KEY=value' lines (#6659), so a
hand-added 'export GITHUB_TOKEN=ghp_...' shows as set (green light) in the
desktop Tools & Keys page. But save_env_value/remove_env_value only matched
plain 'KEY=' lines:
- DELETE /api/env 404'd ('not found in .env') — the token could not be
removed through the UI
- PUT /api/env appended a SECOND line; a later delete removed the new line
while the export line silently resurrected the old value
Both writers now match assignments through a shared _env_line_defines_key()
helper that understands the export prefix. Commented-out lines are still
ignored.
Regression tests drive the real dashboard endpoint handlers against a temp
HERMES_HOME with runtime-constructed classic-PAT-shaped fixtures, covering
save-does-not-500, export-line remove, export-line replace-without-duplicate,
and the plain-line path staying intact.
Fixes#40041
* fix(credentials): unify provider key delete/update across .env, auth.json, config.yaml (#51071#59761#62269)
A provider API key can live in three stores at once: ~/.hermes/.env,
auth.json credential_pool (env-seeded 'env:<VAR>' entries persisted by the
pool loader), and config.yaml mirrors (model.api_key, auxiliary.*.api_key,
custom_providers[*].api_key). The desktop/dashboard endpoints and the TUI
gateway RPCs only ever mutated .env, so the stores diverged:
- #51071/#59761: DELETE /api/env removed the key from .env but left the
credential_pool entry (the loader is additive-only and never prunes),
so the provider kept appearing in the model picker — surviving restart
via the stale pool entry + provider_models_cache.json row.
- #62269: PUT /api/env rewrote .env but left the OLD key in config.yaml
(model.api_key wins over env at client construction), producing 401s
with a key the UI no longer showed.
New hermes_cli/credential_lifecycle.py is the single choke point:
- remove_provider_env_credential(): clears the .env entry, prunes
env:<VAR> pool entries across ALL providers (a shared var like
GITHUB_TOKEN can seed several), suppresses the env source so a lingering
shell export can't re-seed it (matching 'hermes auth remove' semantics),
drops the affected providers' model-cache rows, and scrubs value-matched
config.yaml api_key mirrors. Returns 'found' spanning every store so a
stale pool-only entry is cleanable through the same delete button.
- save_provider_env_credential(): writes .env, rotates any config.yaml
mirror that held the PREVIOUS value (value-matched — an unrelated inline
key is untouched), and lifts a prior env-source suppression so re-adding
behaves like 'hermes auth add'.
OAuth preservation: only entries with source == 'env:<VAR>' are pruned.
OAuth/device-code/manual/borrowed pool entries and providers.<id> OAuth
token blocks are never touched by a key-only delete. (model.disconnect in
the TUI gateway still clears OAuth via clear_provider_auth — that surface
is a full provider disconnect, which is the documented intent there.)
Rerouted call sites: PUT/DELETE /api/env (dashboard + desktop),
tui_gateway model.save_key / model.disconnect, save_env_value_secure
(TUI/gateway secret capture), and hermes config set/unset for env-shaped
keys.
E2E tests drive the real endpoint handlers against temp-HERMES_HOME
fixtures (.env + auth.json + config.yaml with runtime-constructed fake
keys) and assert cross-store consistency after delete/update, pool-reload
survival ('restart'), OAuth preservation, models-cache invalidation, and
the suppress/unsuppress round-trip.
Fixes#51071Fixes#59761Fixes#62269
Follow-ups on the salvaged unknown-root-key warning from PR #67345:
- Add image_gen, video_gen, plugins, smart_model_routing, platform_toolsets,
session_reset, multiplex_profiles, profile_routes, platforms,
require_mention, unauthorized_dm_behavior, and signal to
_EXTRA_KNOWN_ROOT_KEYS — all are read from the raw user YAML (gateway,
registries, plugin CLI) or written by our own setup wizard, but absent
from DEFAULT_CONFIG. Without this, doctor would warn on configs Hermes
itself wrote.
- Convert tests/hermes_cli/test_config_validation.py back to LF line
endings (the PR's rewrite introduced CRLF).
Extracted from the config-validation portion of PR #67345 (the token-cost
half was not salvaged). Unknown top-level config keys now warn (naming the
key) instead of being silently ignored; known roots derive from
DEFAULT_CONFIG.keys() plus a small extras set for valid-on-disk roots
absent from defaults.
A final response generated but not confirmed-delivered was the one
artifact the gateway could lose without a trace: crash or planned
restart between finalize and platform ACK dropped it silently, and the
resume path re-ran the whole turn at full cost (#58818 P1, #41696,
#63695's gateway half).
gateway/delivery_ledger.py records each outbound final response in
state.db (same conventions as the async-delegation ledger: WAL, owner
pid + process-start-time liveness, bounded retention):
pending -> attempting -> delivered | failed
startup sweep on dead-owner rows -> redeliver | abandoned
Contract (the lessons from the closed delivery-outbox attempt #61790):
- obligation recorded BEFORE the first send attempt; cleared only on
SendResult.success (destination acceptance, #51184)
- ambiguity is labeled, never silently retried: rows that were mid-send
when the process died redeliver with a visible '♻️ Recovered reply —
may be a duplicate' prefix (honest at-least-once)
- stable ids from session_key + inbound message id + content, so
distinct threads/topics can never collide
- poison rows bounded: 3 attempts / 24h freshness -> abandoned; claim
atomically re-stamps ownership so racing sweeps can't double-claim
- redelivery clears resume_pending for the session so the resume path
never re-runs a turn whose answer the ledger already holds
- best-effort everywhere: ledger failure can never block or delay a send
- slash-command/ephemeral/empty responses are not recorded; cron and
proactive delivery stay on DeliveryRouter (separate subsystem)
Config: gateway.delivery_ledger (default on; no version bump needed).
Validation: 30 ledger+producer tests; 352 blast-radius gateway tests
green; cross-process E2E (record in process A, kill it mid-send, claim
+ marker + redeliver in a fresh process B against the same state.db).
_quote_env_value previously left internal spaces unquoted (only #/"/'
and leading/trailing whitespace triggered). Spaced macOS paths written
via hermes setup SSH / Google Chat SA path / hermes config set produced
lines that python-dotenv still parsed but shell `set -a; . file` word-split.
Extend needs_quoting with any(c.isspace()); escaping dialect unchanged.
Route recovered messages through the live Discord ingress policy, preserve dedup and completion invariants, bound and retain the recovery ledger, and expose the opt-in config with docs and backup coverage.
Replace REST-based Discord liveness probe with local WebSocket/heartbeat
state detection. REST success doesn't prove Gateway event delivery — a
half-closed WebSocket can leave Bot.start() alive while REST returns 200.
Now samples ready/open/ACK state and heartbeat latency; consecutive
unhealthy samples emit one retryable fatal code so GatewayRunner rebuilds
the adapter through the existing reconnect path.
Also fixes three lifecycle gaps in the recovery path:
1. asyncio.wait_for() can remain blocked if adapter cleanup swallows
cancellation — now uses bounded asyncio.wait() with task detachment.
2. Multiplexed secondary-profile adapters had no profile-scoped reconnect
owner — now uses one runner-owned reconnect slot per profile.
3. An in-flight turn could send its final text through the disconnected
adapter after a replacement was registered — now resolves the live
same-profile replacement for unsent final responses only (message IDs
never migrate, edits/deletes stay on the old transport).
Adds an opt-in Linux/systemd event-loop watchdog (gateway.systemd_watchdog_seconds,
default 0) for the failure mode where the whole asyncio loop stops making
progress and no in-process liveness task can run. stdlib-only sd_notify,
Type=notify/WatchdogSec generation, READY/STOPPING lifecycle.
Co-authored-by: 王鑫 <wx.xw@bytedance.com>
Follow-up to PR #66479 salvage. The three new env vars
(HERMES_LOCAL_STREAM_STALE_TIMEOUT, HERMES_GATEWAY_MAX_STARTS,
HERMES_GATEWAY_START_WINDOW_S) were introduced as bare env-var reads with no
config.yaml surface or documentation — violating the .env-is-for-secrets-only
policy (behavioral settings must live in config.yaml, bridged to env internally).
- config.yaml: add gateway.respawn_storm {max_starts, window_seconds} to
DEFAULT_CONFIG, mirroring the existing restart_loop_guard pattern.
- gateway.py: read config.yaml first, env vars override as escape-hatch.
- environment-variables.md: document all three new env vars, noting the
config.yaml alternative for the gateway ones.
- chat_completion_helpers.py: cross-reference the env-var docs from the
local stale timeout comment.
* fix(dashboard): resolve dashboard-owned assets from the process launch home
Profile-scoped chat / ?profile= requests install a context-local
HERMES_HOME override, which made custom dashboard themes AND user
dashboard-plugin extensions disappear once the embedded /chat started
under a different profile than the dashboard process.
Add get_process_hermes_home() (sharing _hermes_home_from_env() with
get_hermes_home() so the two can't drift, and splitting the profile
fallback warning into _warn_profile_fallback_once()) and use it for both
the theme YAML scan and the user dashboard-plugin scan — machine-level
assets that belong to the server's launch home and must not follow a
transient per-request override.
Genuinely profile-scoped callers (memories/backups/checkpoints/provider
config) and the paired _merged_plugins_hub classification are left
untouched so they keep following the override.
* test(dashboard): cover process-home asset discovery under profile override
- get_process_hermes_home(): env set returns that path, unset falls back
to the platform default, and an active context-local override is ignored.
- _discover_user_themes() and _discover_dashboard_plugins() keep returning
launch-home assets while a profile override scopes the request elsewhere.
Third profiling round (after #66033 / #66347), targeting the composer
model picker and dialog opens (worktree dialog etc.), measured over CDP
on real 1000+-message sessions.
Backend — model.options took 4.8s cold / 1.8s warm per call, and the
desktop model pill/picker blocks on it every open:
- agent/credential_pool: _load_config_safe uses load_config_readonly().
Every consumer only reads, and the per-call deepcopy was the dominant
cost — list_authenticated_providers calls load_pool() per provider
row, and each load_pool loaded (and deep-copied) the full config
again via get_pool_strategy.
- hermes_cli/config: memoize ensure_hermes_home() per home path. It
runs inside the config lock on EVERY load_config(), paying ~14
mkdir/chmod syscalls per call. The fast path still re-checks that the
home dir exists, so a deleted home is recreated as before; profile
switches hit the new path and re-run. Tests cover both.
- tui_gateway/server: add model.options to _LONG_HANDLERS. It measured
seconds inline on the WS reader thread — while it ran, prompt.submit
and session.interrupt sat unread (same class as #21123).
Together: model.options RPC 4825/1842ms → 426/230ms (measured on the
live desktop backend); build_models_payload in isolation 6.2s → 0.97s
cold, 0.27s warm.
Desktop — every Radix dialog/popover open forced a whole-document style
recalc (Presence reads getComputedStyle on mount), which on a
1300-message transcript cost ~650-730ms per open (CPU profile:
getAnimationName 483ms self). The worktree dialog (⌘⇧B) paid it on
every single open:
- thread/list: content-visibility:auto + contain-intrinsic-size on the
per-turn group wrappers. Off-screen turns now skip style recalc,
layout, and paint entirely; never-rendered turns hold a placeholder
height (auto: remembered real size once rendered) so scrollbar and
anchoring stay stable. Verified over CDP: worktree dialog open 656-
730ms → ~200ms on the same session; stick-to-bottom pin, scroll-to-
top rendering, and sticky human bubbles all intact.
Also: profile-session-switch harness accepts CDP_HTTP (Chrome tends to
squat on 9222).
Verification:
- scripts/run_tests.sh: config, credential-pool, inventory,
model-switch routing, tui_gateway protocol, profiles suites green
(test_profiles has one pre-existing failure on main, unrelated);
new tests for the ensure_hermes_home memo.
- apps/desktop: tsc clean, eslint/prettier clean, thread + session
suites green (326 tests).
- E2E over CDP on the live app: numbers above, plus scroll/pin sanity.
* feat(browser): store full snapshots on truncation; make eval denylist opt-in
Two harness fixes motivated by BU_Bench results where fixed-verb + lossy
observation cost Hermes heavily vs code-driven browser agents:
1. Snapshot truncation no longer loses content. When a snapshot exceeds
the 8000-char threshold, the complete accessibility tree is saved to
cache/web (same truncate-and-store pattern as web_extract) and the
truncated view / LLM summary includes the file path plus a ready-made
read_file call. Element refs beyond the cut are recoverable without
re-snapshotting. Stored copies are force-redacted and capped at 2MB;
content-hash filenames dedupe repeated snapshots of the same page.
2. The browser_console(expression=...) sensitive-primitive denylist is
now opt-in via browser.restrict_evaluate (default false). The
names-based denylist blocked legitimate DOM extraction — any selector
or expression containing 'fetch', 'cookie', 'input', etc. — which
crippled the agent's only programmatic page-inspection path. The
SSRF/private-URL egress guards in _browser_eval are independent of
this policy and remain always-on. browser.allow_unsafe_evaluate keeps
its meaning (bypass the denylist) for configs that already set it.
* test: update None-guard test for stored-snapshot pointer in _extract_relevant_content
test_normal_content_returned pinned the exact return value; the summary
now carries a pointer to the stored full snapshot. Assert the summary
passes through and the pointer is present instead.
* feat(browser): align snapshot threshold with web_extract's 15k char budget
SNAPSHOT_SUMMARIZE_THRESHOLD 8000 -> 15000, matching
web_tools.DEFAULT_EXTRACT_CHAR_LIMIT so the snapshot and web_extract
truncate-and-store paths give the model the same per-page budget.
_truncate_snapshot's default max_chars now follows the constant.
Invariant test added; docs (en+zh) and CLI tip updated.
Commentary delivery is on by default; users who find the extra mid-turn
narration noisy can set display.show_commentary: false to restore the
previous behavior (commentary routed to the reasoning channel, visible
only with show_reasoning).
- hermes_cli/config.py: display.show_commentary default true
- agent/agent_init.py: wire config -> agent.show_commentary
- run_agent.py: gate structured commentary extraction on the flag
- agent/codex_runtime.py: gate live-stream commentary callback (falls
back to legacy reasoning-channel routing when off)
- docs + 2 tests (interim path off, live stream fallback)
Also adds AUTHOR_MAP entries for davidrobertson and 100yenadmin.
Move query_rewrite from the honcho plugin to plugins/memory/ and
rename the auxiliary task key honcho_query_rewrite ->
memory_query_rewrite so any memory provider can use the same
rewrite path and model/timeout config block. No behavior change.
Add the flag-gated compute-host supervisor, delta/control protocol, PPID orphan guard, inline fail-open path, synthetic GIL-heavy turn seam, and AC-4 certify harness.
Verified on current origin/main: 343 focused tests pass; Ruff and diff checks pass; 360s AC-4 run with six heavy lanes passes at 6.11ms serving p99 with zero stalls and valid load.
Co-authored-by: Kyzcreig <9063726+Kyzcreig@users.noreply.github.com>
hermes update ran TWO separate pre-update backup mechanisms: the
config-gated full zip (updates.pre_update_backup, default off) and an
unconditional quick state snapshot added for #15733 that ignored the
user's setting entirely. On a large state.db (observed: 24 GB) the
'cheap' snapshot silently added ~60s to every update and ate 24 GB of
disk in state-snapshots/.
Now there is ONE mechanism, gated by updates.pre_update_backup with
three modes:
- quick (new default): state snapshot of critical small files (pairing
JSONs, cron jobs, config, auth, per-profile DBs). Files over 1 GiB
are skipped with a warning so a bloated state.db can never stall the
update again.
- full: the quick snapshot plus the HERMES_HOME zip (old 'true'
behavior; --backup forces it for one run).
- off: nothing runs — an explicit opt-out now disables the quick
snapshot too (--no-backup does the same per-run).
Legacy booleans are honored: true -> full, false -> off.
_run_pre_update_backup() now returns the quick-snapshot id so the
post-update cron-jobs restore safety net (#34600) keeps working; the
snapshot moved from the post-fetch site to the pre-mutation site,
which also covers the zip-fallback update path it previously missed.
* fix(auxiliary): route direct-create aux callers through call_llm (#35566)
Five callers (kanban_decompose, kanban_specify, profile_describer, and
goals.py's judge + draft-contract) built raw clients via
get_text_auxiliary_client() and passed extra_body=get_auxiliary_extra_body()
— which only returns Nous portal tags and ignores
auxiliary.<task>.extra_body from config.yaml entirely. That was the
remaining half of #35566 after the call_llm path was fixed.
Routing them through call_llm(task=...) gives each caller the full
auxiliary contract for free: task extra_body, the reasoning_effort
shorthand, transient retries, provider-profile projection, and fallback
chains. goal_judge gains a DEFAULT_CONFIG block (it had none — its
provider/model overrides silently didn't exist as documented keys).
get_auxiliary_extra_body() now has zero non-test callers; kept for
plugin back-compat.
Fixes#35566.
* test: migrate kanban dashboard + CLI specify mocks to call_llm
Two more consumers of specify_task mocked the old
get_text_auxiliary_client symbol (missed in the first sibling sweep —
they live outside tests/hermes_cli's kanban files): the dashboard
plugin's /specify endpoint tests and the /kanban slash-command E2E.
Same migration as the rest: mock call_llm at the source, no-provider
now surfaces via the LLM-error branch.
Add merge_existing to save_config (default False for full-document callers
like the dashboard YAML editor) and route partial writes through
_merge_partial_save. _persist_migration writes the full migrated dict
directly so deleted keys are not resurrected from the on-disk file.
The just-merged auxiliary.<task>.reasoning_effort shorthand applied
ensemble-wide to MoA (one value for every advisor) — wrong granularity.
Per-slot preset config supersedes it:
moa:
presets:
deep_review:
reference_models:
- {provider: ..., model: ..., reasoning_effort: low}
- {provider: ..., model: ..., reasoning_effort: xhigh}
aggregator:
{provider: ..., model: ..., reasoning_effort: high}
- Remove reasoning_effort from the moa_reference/moa_aggregator
DEFAULT_CONFIG blocks; _get_task_extra_body now warns-and-ignores the
key on MoA tasks, pointing at the preset config
- Guard tests: MoA aux blocks must not regrow the key; task-level value
is rejected with the pointer warning
- Docs: configuration.md notes the MoA exception and links the MoA page
Every auxiliary task block (vision, web_extract, compression,
title_generation, curator, background_review, moa_reference, ...) now
accepts a reasoning_effort shorthand:
auxiliary:
compression:
reasoning_effort: low
vision:
reasoning_effort: none
_get_task_extra_body() folds it into extra_body.reasoning, which every
auxiliary wire already translates: chat.completions passes it through,
the Codex Responses adapter maps it to top-level reasoning/include, and
the Anthropic auxiliary adapter now forwards it into
build_anthropic_kwargs(reasoning_config=...) (previously hardcoded None).
An explicit extra_body.reasoning on the same task wins over the
shorthand. Invalid levels are ignored with a warning. Empty string
(the shipped default) is a no-op — zero behavior change.
Config: reasoning_effort added to all 16 auxiliary task blocks in
DEFAULT_CONFIG (no version bump — deep-merge handles new keys).
Add agent.reasoning_overrides dict to config.yaml. Users can now set
a reasoning_effort per model, overriding the global agent.reasoning_effort.
Example:
agent:
reasoning_effort: "medium" # global default
reasoning_overrides:
"openrouter/anthropic/claude-opus-4.5": "xhigh"
"openai/gpt-5": "low"
"claude-sonnet-4.6": "high" # bare model name also works
The helper is spelling-tolerant: override keys match regardless of
provider prefix or dots-vs-dashes normalization, so users can write
keys in any sensible form and they'll match.
Resolution priority:
1. Session-scoped /reasoning --session override (gateway only; unchanged)
2. Per-model override from agent.reasoning_overrides (spelling-tolerant)
3. Global agent.reasoning_effort (existing)
4. Provider default (unchanged)
Wired into:
- CLI startup (cli.py)
- Messaging gateway agent construction (gateway/run.py)
- Desktop/TUI _load_reasoning_config (tui_gateway/server.py)
- Cron job scheduler (cron/scheduler.py)
- /model mid-session switch (agent/agent_runtime_helpers.py)
+ _primary_runtime now tracks reasoning_config for correct fallback recovery
- Fallback activation (agent/chat_completion_helpers.py::try_activate_fallback)
+ Re-resolves reasoning_config for the fallback model (best-effort)
Closes#21256 (per-model reasoning_effort defaults).
Note: no hermes config set agent.reasoning_overrides.<model> support;
users edit the YAML directly. _set_nested splits on "." and would
corrupt model keys containing version dots.
`UPSTAGE_API_KEY` / `UPSTAGE_BASE_URL` were wired through the provider
resolver, auth registry, and the EnvPage grouping, but never added to
`OPTIONAL_ENV_VARS` in hermes_cli/config.py. The dashboard/desktop
Providers page builds its list from that catalog (`/api/env` iterates
`OPTIONAL_ENV_VARS`), so with no entry the keys were never emitted and
"Upstage Solar" never rendered — the EnvPage prefix group stayed empty.
Add both keys under `category: "provider"` (matching gmi/minimax) so they
show up in `hermes dashboard` / `hermes desktop` under "Upstage Solar".
Adds a regression test asserting the catalog contains them, mirroring the
existing GMI coverage.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Salvage of #63935. The original fix read HERMES_CRON_SESSION_DB_TIMEOUT
from a bare env var, but AGENTS.md requires non-secret behavioral
settings to live in config.yaml with an env var bridge only for
backward compatibility.
Changes:
- Add cron.session_db_timeout_seconds to DEFAULT_CONFIG (default 10s)
- Resolution order: HERMES_CRON_SESSION_DB_TIMEOUT env override →
cron.session_db_timeout_seconds in config.yaml → 10s default
(mirrors the existing script_timeout_seconds pattern)
- 0 = unlimited (opt-in for debugging, skips the bound)
- Strengthen test: assert the warning is logged on invalid env value
(caplog was taken but never asserted)
- Add test: verify config.yaml resolution path works end-to-end
Co-authored-by: LoicHmh <26006141+LoicHmh@users.noreply.github.com>
Bundle Fireworks AI as a first-class BYOK provider across the CLI, web/TUI,
and desktop onboarding.
- New model-provider plugin with attribution headers (HTTP-Referer / X-Title)
so Fireworks can attribute Hermes traffic; PAYG-safe default aux + fallback
models (accounts/fireworks/models/...), IDs tracking fw-ai/fireconnect.
- Registered in CANONICAL_PROVIDERS so it appears in the CLI/web/TUI pickers.
- Alias wiring (fireworks-ai, fw) into both CLI resolvers.
- First-class wiring: OPTIONAL_ENV_VARS, HERMES_OVERLAYS (FIREWORKS_BASE_URL
override), doctor env hints. Live catalog + model_metadata are auto-derived.
- doctor: treat Fireworks' native slash-form IDs (accounts/fireworks/...) as
valid, not aggregator vendor prefixes, so it no longer tells Fireworks users
to switch to openrouter or drop the prefix.
- picker: plugin providers with no static curated list now lead with their
profile fallback_models, so the default is an agentic chat model instead of
whatever the live catalog returns first (Fireworks listed an image model,
flux-*, ahead of its chat models).
- Desktop onboarding: Fireworks as a RECOMMENDED hero card with the official
Fireworks logomark and a brand-purple badge, routing to the BYOK key form;
i18n in en/ja/zh/zh-hant.
- Tests: profile contract, first-class wiring (both resolvers, overlay, config,
doctor incl. the slash-form regression, aux headers, credentials), discovery
spot-check, and a live smoke test driven through the Hermes runtime.
Fire Pass (fpk_) support is coming soon; the future wiring is kept as a
commented-out scaffold in the plugin.
Sibling site of the load_cli_config fix (#58277): _deep_merge treated a
YAML-null section (terminal: with no value) as an override, replacing
the entire DEFAULT_CONFIG dict for that section with None. Every
downstream consumer expecting a mapping was a latent crash, and default
sub-keys were silently lost. A None override of a dict default is now
ignored, matching the CLI loader's behavior. Scalar-null overrides are
unchanged.
Port from openai/codex#31188: a parse failure in a policy-bearing config
file must not silently replace the effective policy with an empty/default
one. Codex's load_exec_policy_with_warning replaced the whole exec policy
with Policy::empty() when a .rules file failed to parse, silently dropping
managed prompt/forbidden rules; the fix preserves the managed policy while
still warning.
Hermes had the same bug shape in load_config(): a YAML parse error made
_load_config_impl() fall through to DEFAULT_CONFIG, dropping every user
override — including approvals.deny rules, which are documented to block
commands even under --yolo. In a long-running gateway, a user mid-editing
config.yaml into broken YAML silently disarmed their own deny rules on the
next load.
Now, when the process has a last successfully loaded config for that path
(_LAST_EXPANDED_CONFIG_BY_PATH), a parse failure keeps serving it (cached
under the corrupt file's signature so the broken file isn't re-parsed) and
the warning says edits are being ignored until the YAML is fixed. Fresh
processes with no last-known-good keep the existing DEFAULT_CONFIG
fallback and warning.
E2E-verified: deny rule 'curl*evil.com*' still blocks after mid-process
corruption; fixed file reloads normally; fresh-process fallback unchanged.
Close the remaining end-to-end gaps so the full gpt-5.6 family (sol/
terra/luna + their -pro high-effort modes, 6 slugs) works on every
surface a user can reach them through:
- agent/auxiliary_client.py: the Codex OAuth backend hard-caps context
at 272K for gpt-5.6 exactly as it does for 5.4/5.5, but the default
50% compaction trigger would summarize at ~136K and waste half the
usable window. Extend the existing _is_codex_gpt54_or_gpt55 chokepoint
(single enforced predicate feeding _compression_threshold_for_model)
to match gpt-5.6* on the openai-codex route so those sessions get the
same 0.85 auto-raise. Direct-API/OpenRouter routes (full 1.05M window)
are unaffected; the historical codex_gpt55_autoraise opt-out still
applies. The one-time notice banner is model-dynamic and already
renders the correct slug/cap.
- hermes_cli/config.py, agent/agent_init.py: refresh the autoraise
comments/notice to mention the 5.6 family.
- hermes_cli/codex_models.py: add the -pro variants to DEFAULT_CODEX_MODELS
+ forward-compat so ChatGPT-OAuth (openai-codex) Pro users see the full
family in /model, not just the base tiers.
Supersedes the earlier commit's note that 5.6 was intentionally kept out
of the codex catalog: the slugs are confirmed routable (OpenRouter live
+ codex backend), so they belong there like every other codex-capable
gpt-5.x slug.
E2E verified across all 6 slugs: direct-API ctx 1.05M, codex ctx 272K,
pricing reachable from openai + openai-api routes, codex compaction
override 0.85 (and None on direct-API + when opted out), present in
openai-api picker + codex catalog, /model gpt resolves to sol on both
native routes. Guard tests added for the compaction route matrix.
Sessions on sub-512K-context models were spending most of their wall-clock
re-summarizing: the 50% trigger left too little post-compaction headroom
(the incompressible floor — system prompt, tool schemas, protected tail,
rolling summary — ate most of the reclaimed space), so compaction re-fired
every 1-2 turns. Three compounding defects fixed:
- Threshold floor: models with context windows below 512K now trigger at
>=75% of the window (raise-only — a higher configured value or per-model
autoraise like Codex gpt-5.5's 85% always wins). Re-derived on
update_model() in both directions.
- No max_tokens on the summary call: the summary budget is prompt guidance
only ("Target ~N tokens"). The wire cap truncated summaries mid-section
on the Anthropic Messages / NVIDIA NIM paths (thinking models burn the
cap on reasoning first), yielding truncated or thinking-only summaries
and compaction loops. Summary token ceiling lowered 12K -> 10K to keep
the guidance within the intended 1K-10K envelope.
- Reasoning traces excluded end-to-end: inline <think>/<reasoning> blocks
are now stripped from assistant content before serialization to the
summarizer, and from the summarizer's own output before the summary is
stored (previously a thinking summarizer model's trace was persisted in
_previous_summary and re-fed into every iterative update, compounding
bloat). Native reasoning fields were already excluded.
Verified E2E with real imports against a temp HERMES_HOME: threshold table
across 64K-1M windows, override interactions (user 0.85 wins, spark 0.70
raised, gpt-5.5 0.85 kept), full compress() round-trip with a thinking
summarizer, and wire-kwargs capture proving no max_tokens is sent.
* feat(install): warn pip/Homebrew installs are unsupported (CLI, TUI, desktop)
pip and Homebrew are now Unsupported install methods per
website/docs/getting-started/platform-support.md. Surface a
warn-don't-block deprecation notice everywhere the install method is
already shown, pointing at the platform-support docs and noting these
installs will not receive further updates. NixOS (Tier 2) is untouched.
- hermes_cli/config.py: shared is_unsupported_install_method() /
format_unsupported_install_warning() helpers so the wording and docs
link stay consistent across every surface.
- hermes_cli/banner.py: generalize the existing pip-only banner
warning to also cover Homebrew.
- hermes_cli/main.py: hermes update and hermes update --check print
the warning before proceeding (still update; warn, don't block).
- tui_gateway/server.py: session.info gains install_warning.
- ui-tui: SessionPanel renders install_warning alongside the existing
'N commits behind' notice.
- apps/desktop: SessionRuntimeInfo/GatewayEventPayload gain
install_warning; applyRuntimeInfo + the live session.info event fire
a snoozable warning toast via a new reportInstallMethodWarning(),
mirroring the existing backend-contract-skew toast pattern. i18n
strings added for en/zh/zh-hant/ja.
- Tests: updated pip banner assertions for the new wording, added a
Homebrew banner test, and two tui_gateway session_info tests
(install_warning present for pip, absent for git).
* fix(nix): make `hermes` in developement environment actually work
install modules as editable overlay with uv
* feat: print install method when running --version
* fix: correct detect install method when running from a subtree
Opt-in discord.approval_mentions (config.yaml, bridged to
DISCORD_APPROVAL_MENTIONS) prepends <@id> mentions for numeric
allowlist entries to exec-approval prompts, with a scoped
AllowedMentions override (users only). Default off - no surprise
pings. Reapplied onto the content-mirror layout from #60245: mentions
prepend to the visible content block and its truncation budget.
Original implementation from PR #39719; commits arrived bot-authored,
re-attributed to the contributor.
The ChatGPT Codex OAuth backend caps both gpt-5.4 and gpt-5.5 at a 272K
context window, but the autoraise that lifts the compaction trigger to 85%
only matched gpt-5.5. On gpt-5.4 the global 50% threshold fired at ~136K —
half the usable window — compacting far earlier than necessary.
Rename _is_codex_gpt55 -> _is_codex_gpt54_or_gpt55 and match both families.
The one-time user notice is now model-aware (shows the actual slug). The
config key codex_gpt55_autoraise is kept as-is for backward compatibility.
Adds gpt-5.4 coverage to the autoraise tests.