Hermes Fixed 4 Sneaky Bugs in One Day: Attachments, API Keys, Updates, Search

Have you ever had one of those “something’s just not right” moments? You ask Hermes to send a file to Telegram, it replies “sent” — but the attachment never shows up. Or you run multiple profiles on one machine, switch models, and realize the credentials being used don’t look like yours. Or you search your session history for a phrase you definitely said, and get nothing back. These aren’t your imagination — they’re four deep bugs in Hermes Agent that were all fixed on August 9, 2026. This post breaks down each one: what it looked like, why it happened, and how the fix works.
Bug 1: Attachments silently vanish — it said “sent”, but nothing arrived
The case: You ask Hermes on Telegram to generate a PDF report. It replies “report generated, sending now.” But your chat shows only a strange line of text: MEDIA:/path/to/report.pdf — no file, just the literal path. Worse, sometimes there’s not even that: it logs “delivery confirmed” and nothing happens at all.
Root cause: The bug lived in the queued delivery path. When a reply had to be queued (while the previous turn was still running, during subagent/compression demotions, photo bursts, or queue mode), the first turn’s response went out through a side path that bypassed MEDIA handling entirely: non-streamed replies were sent via raw adapter.send() with the literal MEDIA: tag as text and no file; already-streamed replies logged “delivery confirmed” and silently dropped the attachments. Hermes told you a file was produced — the file never arrived.
The fix: A new _deliver_queued_first_response() function now handles queued deliveries uniformly: it splits text from attachments using the existing extract_media + _deliver_media_from_response machinery (preserving path-security filtering), and routes everything through the standard attachment delivery flow. There’s also a sensible guard: if the first turn actually failed, it delivers the normalized failure text but never uploads attachments — no more disguising failures as successes.
What you should know: If you ever got a MEDIA: text line or a missing attachment on Telegram, Discord, or any gateway platform, this was it. After the fix, attachments arrive properly; before you update, you can fall back to the file browser or web_extract to inspect produced files.
Bug 2: API keys crossing profiles — switching models read someone else’s key
The case: You run multiple profiles on one machine (say, work and personal), each with its own model-provider API keys. One day you switch to your personal profile and open the model picker — the “authenticated providers” list is showing providers authenticated with your work profile’s keys. You were a click away from sending a request with the wrong key.
Root cause: Under multiplexed profiles, credential reads during model switching bypassed the per-profile secret scope. Two spots were affected: the picker’s “which providers are authenticated” listing, and switch_model’s actual key resolution — the latter raw-read the process environment (${VAR} expansion and key_env fallback) and handed the result to runtime resolution as an explicit API key. One profile could therefore see — or adopt — another profile’s API keys.
The fix: A new _scoped_key_env() helper routes both reads through agent.secret_scope.get_secret. With multiplexing off, behavior is byte-identical to os.getenv (single-profile users are unaffected). With it on, reads are strictly confined to the current profile’s secret scope. The key design decision: fail-closed. If an UnscopedSecretError is raised, it errors out rather than falling through to environment variables — keys never silently leak across profiles.
What you should know: This is the only type/security issue of the four — it’s about key isolation, so multi-profile users should update as soon as practical. Single-profile behavior is completely unchanged.
Bug 3: Orphaned processes after updates — Windows desktop updates stuck
The case: You’re on Windows, using the Hermes desktop app. You click update, but the new version won’t start — the old processes still hold the virtual environment, files are locked, and the update keeps failing. Task Manager shows a graveyard of “parentless” backend processes that refuse to die.
Root cause: During desktop updates, releaseBackendLock() sent SIGTERM to the primary backend before forceKillProcessTree() ran. On Windows that ordering is fatal: if the launcher exits before taskkill /T executes, Windows can no longer enumerate its descendants — the child processes survive, holding the venv, and become orphans.
The fix: A new stopBackendTreesForUpdate() tree-kills the live root first (no pre-signalling), then handles pool teardown. Orphan classification also became tree-aware: the scanner’s orphan roots are returned together with their descendants (including the re-exec’d uv-managed interpreter worker, whose live parent is the orphan root itself) — taskkill /T then reaps the whole subtree in one go. Backends with a genuinely live parent are left alone.
What you should know: This fix affects only the Windows desktop update flow. Linux/macOS users are untouched; if you’ve hit “update stuck / leftover processes” on Windows, this should make it dramatically better.
Bug 4: Search silently failing — everyday queries return nothing
The case: You search your session history for gateway/run.py — the file you definitely discussed — and get “no results.” You try it's, user@host, 50%. All empty. You start doubting your memory. The words were there; the search was broken.
Root cause: Session search uses SQLite FTS5 full-text search, but the query sanitizer only stripped six characters (+{}():"^). FTS5’s grammar rejects many more — apostrophes, slashes, @, commas, question marks, equals signs, semicolons, exclamation points, pipes, tildes, hashes, dollar signs, brackets, angle brackets, backslashes — and any of them reaching MATCH raw raised an OperationalError, which the query site swallowed into zero results. You weren’t searching “nothing”; the query was crashing.
The fix: The sanitizer’s strip-class was rebuilt (via re.escape, which also fixed a literal-backslash loss) to cover the full rejection set, with smart special cases: quoted exact phrases ("exact phrase") survive via placeholder extraction, dotted/hyphenated terms are untouched, and % is stripped only for non-CJK queries — because % is a reserved character for the CJK LIKE fallback, and CJK queries never hit the FTS5 error path anyway.
What you should know: After the fix, it's, gateway/run.py, user@host, and 50% all parse and match correctly; CJK queries are completely unaffected. If Hermes search ever felt flaky to you, this bug was probably why.
Summary: one merge day, four lessons
| Bug | Symptom | Severity | Affects |
|---|---|---|---|
| Vanishing attachments | Literal MEDIA: text / silently dropped files |
Functional | All gateway platforms (Telegram, Discord, …) |
| API key cross-talk | Model switching reads another profile’s key | Security | Multiplexed-profile users |
| Update orphans | Old processes lock the venv after Windows updates | Functional | Windows desktop |
| Silent search failures | Queries with special chars return empty | Functional | Everyone |
The common thread across all four: the failures were silent. Attachments dropped but reported as sent; queries crashed but displayed as “no results”; keys misread without an error. That’s exactly why they were so hard to find — the natural first reaction is to doubt yourself, not the software. All four are fixed now; if any of them bit you before, it’s worth re-trying those operations after you update.
Note: These fixes landed on Hermes Agent
mainon 2026-08-09 and are not in a released tag yet (the latest release is still v0.20.0). To use them today, run from source — or wait for the next release.