NousResearch/hermes-agent · Weekly · Jul 20, 2026 - Jul 26, 2026

Hermes Agent Weekly #42 — Desktop Rendering Overhaul, CLI Hard-Exit Safety, and 40+ Bug Fixes (Jul 20–26)

Commits summarized
1,515
Period
Jul 20 - Jul 26, 2026
Published
Jul 27, 2026

Hermes Agent Weekly #42

Week of Jul 20–26, 2026 · 1,515 commits in period · Highlights from 200 included summaries

If you maintain a custom MCP server, run Hermes on Windows, or use multi-pane desktop layouts, this issue has fixes you'll want to read about.

🎯 Desktop: Streaming & Rendering Overhaul

The biggest theme this week was eliminating unnecessary re-renders across the desktop app — every commit below targets the time-to-first-token (TTFT) or frame drop you might have felt during streaming sessions.

Per-token re-render fixes

  • Sidebar & tool rows no longer re-render on every stream token (#67742). ToolEntry subscriptions to $activeSessionId + $currentCwd were causing a full re-render of every mounted tool row on any session or cwd change — now read at fire time only.
  • Memoized AnsiText and CompactMarkdown (#67844) so child re-renders are skipped when their text props haven't changed.
  • total_chars estimate no longer serializes base64 images on every API iteration (#67788). The old code str()-serialized the full history — including large tool results and images — just to measure its length. Now derived from a single estimate_messages_tokens_rough call.

File tree & review pane

  • Targeted file-tree revalidation (#67824). Before, any file mutation bumped a contentless tick and the tree re-read every loaded directory. Now mutations carry their path and only the changed subtree is rescanned. Opaque mutations (terminal, unresolvable paths) still fall back to a full reconcile.
  • Review-pane diff is now virtualized (#67818). Selecting a large changed file in review used to freeze the app — a full Shiki highlight over every line. Windowing now kicks in regardless of showLineNumbers, so only visible rows mount.
  • File tree no longer goes sticky during agent edit bursts. clearProjectDirCache() was wiping gitroot + gitignore caches on every workspace tick — but listings were read fresh every time anyway, so the wipe forced a full re-read of every ancestor .gitignore for every loaded dir. Dropped.
  • Parallel child-dir reconciliation: reconcile awaited each child serially; now uses Promise.all over siblings.

Other desktop wins

  • Pane & console sash resizes are rAF-coalesced (#67838). Both drag handlers wrote to nanostores on every pointermove — now stashed and applied once per frame.
  • Eager JSON.stringify of every tool's args + result is gone (#67842). prettyJson was running for every tool row on every rebuild, even for tools like read_file where the stringified result is never read. Moved to a memoized, web_search-only computation.
  • Idle-mount boot-hidden panes (#67857). Files, preview, review, and logs were mounting their real content during first paint even though none are visible at launch. Wrapped in <IdleMount> (fires after requestIdleCallback, 2s timeout fallback) — zero UX change, instant-toggle contract intact.
  • Collapsible provider groups in the model picker (#64690). Click a provider header to collapse/expand its model list. State persists via localStorage, stale slugs are never pruned (so profile switches don't lose your collapsed set).

Model UI fixes

  • Multi-pane model UI scoped per tile (#67855). Composer model controls were keyed off primary session globals, so every tile showed the same model and a busy primary blocked switches in idle panes. Now bound to SessionView per tile, with persisted strip order so add/remove/switch stops scrambling adjacent panes.
  • Canvas/pixels driven from signal-based stores (#67569) — re-renders cut from per-frame to per-changed-property.

🔧 CLI: Oneshot Hard-Exit Safety

A significant rework of the oneshot (non-interactive) CLI teardown path to prevent SIGABRT during Python finalization:

  • Expanded cleanup to cover all process-global resources (#67768). The initial salvage only shut down MCP servers. Now also closes terminal environments, browser sessions, and interrupts async delegations — all sites that can hold native-extension-backed resources (aiohttp connectors, websocket clients) that crash during Py_FinalizeEx.
  • Guaranteed hard exit after cleanup interruption — if cleanup itself is interrupted (e.g. a stuck websocket close), the process now forces exit instead of hanging.
  • Idempotency guard (_oneshot_cleanup_done) added to _cleanup_oneshot_runtime, matching the interactive CLI's pattern.
  • Test isolation: _cleanup_oneshot_runtime is now mocked in all _run_and_exit tests so real cleanup doesn't run in the pytest worker.

🌐 Gateway & Networking

  • Don't spend a redelivery attempt when the platform is down (#67843). The delivery ledger increments attempts before the caller knows whether it can send. When the platform failed to connect this boot, the attempt was spent without ever sending — after three such boots the message was abandoned forever. Now the caller declares which platforms it can send on; rows for unreachable platforms are skipped.
  • Stop slow restart redelivery loops (#67747).
  • OAuth cookie read retry on cold-start jar race (#67769). A persist: partition's cookie store hydrates lazily, so the first cookies.get() on a fresh launch can return empty for a signed-in user. Now retries once with a ~180ms backoff before trusting the negative.
  • Spurious stdin EOF recovery (#67639). When a child process sets O_NONBLOCK on shared fd 0, the gateway's next read() can return EAGAIN → CPython sees b'' → false EOF. Now detects O_NONBLOCK, restores blocking mode, and continues. Windows guard, fd leak fix, and SO_RCVTIMEO cleanup included.

🧠 Agent Core

Compression fix: silent tool output loss

A critical bug in the compression floor: _find_tail_cut_by_tokens aligns cut_idx away from tool-call/result boundaries, but the progress floor (head_end + 1) discarded that alignment. When the floor landed inside a tool group, the parent assistant(tool_calls) fell in the summarised region while its tool results started the tail — and _sanitize_tool_pairs dropped those orphans outright. The tool output vanished.

  • Reproduced: sweeping every well-formed block layout up to length 6, 26% (5,623 of 21,840 transcripts) split a call/result pair.
  • Fix: re-align forward after applying the floor (never backward, which would reopen the no-op loop). The same sweep reports 0 violations after the change. (commit)

Model metadata preservation

  • _save_discovered_models_to_config no longer destroys per-model metadata (#67841). When custom_providers[].models uses the dict form (e.g. {context_length: ...}), the auto-save helper was replacing it with a flat list of strings. Regression introduced by PR #65652. Guard extended to the list-of-dicts form as well.
  • Qwen model context lengths updated: qwen3.7-plus added with 1M context (commit). Without this entry it fell back to 128K, causing premature compression at 64K instead of 500K.

⚙️ MCP & Config

  • Opt-out for automatic MCP reload on config change (#67642). Every config rewrite triggered a full MCP reconnect + prompt cache invalidation. New mcp.auto_reload_on_config_change toggle (default: true, backward compatible) lets users opt out. Config changes are still detected and reported; manual /reload-mcp is unaffected.
  • Config path fixed: the opt-out default was declared in auxiliary.mcp but the watcher read top-level cfg["mcp"] — the default was never observed. Moved to a new top-level mcp: section.
  • Env-var expansion in MCP config watcher (#55701). mcp_servers entries with ${VAR} templates caused false-positive reloads on every unrelated save_config_value(). Now both sides use the same expanded representation.
  • Doctor allowlist widened to all gateway-bridged top-level keys (stt_echo_transcripts, reset_triggers, always_log_local, filter_silence_narration, known_plugin_toolsets, group_sessions_per_user, thread_sessions_per_user).
  • Shipped template no longer enables session auto-reset (#67772). The example config still shipped session_reset.mode: both, overriding the code default of none. Fresh installs hit 24h-idle resets they never asked for.

🧪 Testing & DX

  • Wider Testing Library async deadline (#67849) — asyncUtilTimeout bumped to 5000ms to de-flake UI panel tests under xdist CPU contention.
  • CLI oneshot tests now mock cleanup — 3 tests that called _run_and_exit_oneshot without mocking were running real cleanup in the pytest worker.
  • Compression tests extended to cover the floor-alignment fix.

🤝 Community Contributions

This week includes salvaged commits from multiple contributors:

Contributor Area
kshitijk4poor Windows guard, config allowlist, CLI cleanup
harjoth Oneshot hard-exit safety, termux support
OYLFLMH MCP env-var expansion fix
TurgutKural MCP auto-reload opt-out
Drexuxux Gateway redelivery fix, compression fix
Dhravya Shah Supermemory self-hosted endpoint
RenoMG Supermemory routing fixes
YAMAGUCHI Seiji X Search reasoning effort
ksy Model metadata preservation
joezhang Alibaba coding plan model list
asscan Qwen 3.7-plus context length

Full commit logs available at github.com/NousResearch/hermes-agent. Next issue: Jul 27 – Aug 2.

Repository updates

Follow future updates

Get generated NousResearch/hermes-agent development summaries by email, or follow the weekly and monthly RSS feeds.

Sign in to subscribe by email. RSS feeds are public.

Sign in to subscribe