Harness integration
UHP and Hermes
HarnessRouter Community Edition lists Hermes as a supported harness backend and the UHP specification uses hermes as an example stable base string. That does not, by itself, establish native UHP adoption or endorsement by Nous Research / Hermes ecosystem.
Verified relationship
Section titled “Verified relationship”HarnessRouter Community Edition lists Hermes as a supported harness backend and the UHP specification uses hermes as an example stable base string.
In the UHP model, the product does not need to speak a Hermes-specific product integration. It targets a configured harness through the UHP server. The server is responsible for driving the actual backend and translating its execution into UHP tasks, sessions, events, artifacts and errors.
What UHP tries to normalize
Section titled “What UHP tries to normalize”- Task submission and terminal status.
- Progress streaming.
- Session continuation.
- Files and artifacts where supported by the conformance class.
- Cancellation behavior.
- Structured errors and capability discovery.
HarnessRouter integration notes (released through v0.9.0)
Section titled “HarnessRouter integration notes (released through v0.9.0)”The following are HarnessRouter implementation facts, re-checked 24 August 2026. They are not changes made by Nous Research, and they do not involve upstream Hermes speaking UHP.
- What broke: hermes can emit OpenAI-legal messages that aggregator translation rejects — an assistant tool-call message carrying an empty-string
contentwas forwarded by TokenRouter to Anthropic as an empty text content block and refused with HTTP 400 (“text content blocks must be non-empty”). The failure was captured intermittently by conformance check X-05 and killed the turn, because a 400 is never retried. - The fix (shipped with
v0.8.1): HarnessRouter’s openai-api path for Hermes now routes through a loopback relay that normalizes those shapes before the provider sees them — empty tool-call-message content becomes null and empty text parts are dropped; compliant requests pass through unchanged. Prevention at request time rather than retry after failure. - Credential isolation benefit: the real provider key stays inside the HarnessRouter runner process; the Hermes CLI receives only a per-turn placeholder token that the relay resolves. The key never enters the CLI’s environment or its local state database.
- Documented result: patched full-class UHP runs of 52/52 — three through TokenRouter and one through LLMTR on the patched instance, with X-05 passing on every run.
- Vision routing (
v0.8.2, released 23 Aug 2026): Hermes image questions are answered by whichever integration on the HarnessRouter instance serves a vision-capable model, independently of the writing model — a routing capability of the HarnessRouter instance, not an upstream Hermes feature. - Native skill-loader placement (
v0.9.0): HarnessRouter now writes configured Hermes skills to.harness/home/.hermes/skills, matching$HERMES_HOME/skills, where Hermes builds its own<available_skills>index. Previously Hermes received.harness/skillsplus anAGENTS.mdpointer, which bypassed the CLI’s native skill discovery. This is HarnessRouter runner behavior, not an upstream Hermes protocol change.
Upstream Hermes: stable v0.21.5 is the current release
Section titled “Upstream Hermes: stable v0.21.5 is the current release”Nous Research published v2026.9.24 / Hermes Agent v0.21.5 on 24 September 2026 at 10:09:38 UTC. Its annotated tag resolves to commit f97608f178d1ffeca59860195ab7da295f7c8e5f. Upstream describes it as a patch rollup of roughly 460 merged PRs since v0.21.4; full curated notes for the broader window are deferred to v0.22.0.
The release notes measure the v0.21.4 → v0.21.5 window at f97608f1: 1,610 non-merge commits, 4,828 changed files, 460 merged PRs, 475 closed issues and +164,132 / -149,440 lines. They deliberately summarize rather than fully curate the window, while explicitly naming the Desktop plugin-SDK wave, Connectors replacing the MCP tab, per-profile gateway controls, webhook-to-chat delivery, Bot Screen on hosted -desktop images, new model-catalog entries and broad hot-path performance work. Those figures and categories are upstream release-note evidence rather than independent validation by this guide.
GitHub ancestry checks for the source-pinned 23 September work tracked elsewhere on this site confirm that routing-owner state merge bd970b05, export-integrity merge 880b9cd4, crash-recovery merge 1136f135, Bot Screen hosted-image commit 686c34d3, memory-mutation commit d57c2a32, dynamic tool-registry commits dbe2f696 / c07501ec, and the reviewed compaction fixes through d79b8fb6 are all ancestors of the v0.21.5 tag. Those items therefore move from post-v0.21.4 current-main evidence to released behavior. The earlier v0.21.4 tag remains the historical release boundary for the 19 September work it already contained. Checked upstream main is later than f97608f1 and remains a separate development coordinate.
This maturity change does not alter UHP itself. It also does not establish native UHP adoption by Hermes: v0.21.5 is an upstream Hermes release, while UHP support remains a separate HarnessRouter adapter/reference-implementation fact unless Nous Research publishes a native UHP implementation or endorsement.
Included in v0.21.3: approval revocation, process finality and OpenRouter PKCE
Section titled “Included in v0.21.3: approval revocation, process finality and OpenRouter PKCE”The prior source cutoff for these changes was b9271bcb34e1a8b8fe0eeaef0ef4a6e1f93ba543; the stable v0.21.3 tag is downstream of that cutoff. At the 14 September cutoff, checked upstream main was 498abb677ec39ea3ae9f8f5ed60e7def6bc47e70.
- Permanent command approvals now reconcile with operator edits (
9b06d3d0, followed by9db604ce): a live Hermes process previously kept an import-time copy ofcommand_allowlistand could overwrite the file on the next[a]lwaysapproval. That could silently delete an entry the operator added by hand and, more seriously, resurrect a standing approval the operator had removed. The write path now re-reads the file, preserves current on-disk entries, adds only approvals granted by the current process since its baseline, and updates the governing in-memory set. Reloading the permanent allowlist now replaces rather than unions the governing set, including clearing stale approvals when configuration is empty. Upstream documentation is explicit about the remaining boundary: deleting a pattern fromconfig.yamlwhile a session is already running is not an instantaneous hot-path revocation; it takes effect on the next reconciliation/reload or a restart, so a safety-motivated revocation should be followed by a restart. - Process-list refresh now reports the direct child’s actual exit (
41380cce): a child could exit while one of its descendants kept the inherited capture pipe open, leaving list/status views stuck onrunninguntil the descendant closed the pipe.ProcessRegistry.list_sessions()now performs the existing local-exit reconciliation before rendering each session. The real-child regression verifies that the owning session becomesexitedwithout consuming its unread result or waiting for descendant EOF, sibling sessions remain isolated, and only one owner-stamped completion is emitted. - OpenRouter OAuth PKCE is now an explicit credential path (
0007a4c2, with invariants at the recorded cutoff):hermes auth add openrouter --type oauthuses S256 PKCE, binds an OS-assigned loopback port before constructing the redirect, exchanges the code at OpenRouter’s key endpoint, and stores the minted credential as a normalapi_keypool entry. Because OpenRouter does not echo OAuthstate, Hermes places a nonce in the callback path so a guessed loopback port with the wrong path returns 404 and never reaches code exchange; remote/SSH sessions use the documented paste-the-code path instead. The existing--api-keydefault remains unchanged.
These changes materially tighten Hermes’ authorization state, long-running process finality and provider-authentication ergonomics. They remain Hermes runtime/security/provider behavior: they do not change UHP, ACP or MCP wire semantics, do not establish native Hermes UHP adoption, and do not show that HarnessRouter’s released Hermes backend has been independently revalidated against every upstream path in v0.21.3.
Included in v0.21.3: interrupted turns now notify plugins
Section titled “Included in v0.21.3: interrupted turns now notify plugins”Hermes v0.21.3 exposes a concrete mid-turn finality signal for plugins holding resources that become useless when the agent loop is interrupted. Commit f361971e adds the observer-only agent_loop_stopped hook to the messaging gateway, and follow-up d3202bbc extends the same signal to the TUI/Desktop session.interrupt path.
- Gateway interruption finality (
f361971e): after a real running gateway agent is interrupted through/stopor the running-agent fast path of/new, Hermes dispatchesagent_loop_stoppedwithsession_key,platform,reasonandinvalidation_reason. The pending-sentinel/stoppath and already-cleared/no-running-agent case stay silent because no live loop exists to clean up. - TUI/Desktop parity (
d3202bbc):session.interruptnow emits the same hook only when a turn was genuinely running, withplatform="tui",reason="user_stop"andinvalidation_reason="session_interrupt". The plain CLI still has no equivalent interruption surface. - Observer failure cannot veto Stop: hook return values are ignored and dispatch exceptions are swallowed on both surfaces, so a misbehaving plugin cannot prevent the underlying interrupt. Upstream regressions cover the real-running, idle/pending and hook-failure cases.
The practical harness boundary is lifecycle ownership: once a turn is stopped, a plugin can immediately cancel an outbound RPC waiting on a tool result, release a per-turn credential or lock, or notify a connected realtime client instead of leaving that external resource waiting for work the loop will never consume. This is Hermes plugin/runtime lifecycle behavior. It does not change UHP cancellation or task semantics, does not alter MCP or ACP wire behavior, does not establish native Hermes UHP adoption, and does not show that HarnessRouter’s released Hermes backend exposes or independently revalidates this hook.
Included in v0.21.3: state.db permission hardening preserves live SQLite locks
Section titled “Included in v0.21.3: state.db permission hardening preserves live SQLite locks”Two commits now included in stable v0.21.3 close a P0 persistence-integrity path in Hermes’ POSIX permission-hardening code. Issues #109687 and #109786 record field failures where long-running gateway processes retained deleted state.db-wal / state.db-shm generations and subsequent persistence could fail closed or become stranded while the gateway process itself remained alive. The commits below specifically close the permission-hardening descriptor/lock-loss path referenced by those issues; they are not a blanket claim that every historical SQLite failure mode is solved.
- Existing database files are no longer opened and closed merely to tighten mode bits (
ccd360e9): Hermes documents the POSIX rule thatfcntllocks are owned per process/inode, so closing any descriptor forstate.dbcan release locks held by another SQLite connection in the same process. The prior hardening path opened the live main/WAL/SHM inode, appliedfchmod, then closed it. A sibling connection could then take the shared-memory DMS exclusively at close, checkpoint and unlink the sidecars while the long-lived gateway or Desktophermes serveprocess still held the deleted generation. Existing regular files now use path-basedchmod(2)afterlstat; symlinks and non-regular files are skipped. - The creation path cannot accidentally reopen an existing main database (
e16f6867): first-timestate.dbcreation now addsO_EXCL. A descriptor is obtained only for a genuinely new inode;FileExistsErrorroutes an existing database through the same path-basedchmod(2)path instead of open/close. - Regression evidence is ownership-focused, not a production certification: the new POSIX test opens two
SessionDBinstances in one process, runs a sibling SQLite reader, and asserts that no deleted SQLite sidecar holder appears before or after that reader. The upstream P0 issue narratives are field evidence, while the regression is source-pinned proof of the corrected lock-preservation invariant.
For harness durability, the reusable point is that security hardening around a live database must not mutate the database’s lock ownership as a side effect. Tightening permissions by briefly opening and closing the same inode can be semantically destructive even when no SQL is executed.
This remains Hermes runtime/persistence behavior included in stable v0.21.3. It does not change UHP storage/session/error semantics, MCP or ACP wire behavior, native Hermes UHP adoption, or HarnessRouter conformance evidence.
Included in v0.21.3: rotating dashboard refresh tokens are coalesced off the event loop
Section titled “Included in v0.21.3: rotating dashboard refresh tokens are coalesced off the event loop”Hermes v0.21.3 closes P1 issue #55712, a dashboard/Desktop session-liveness failure caused by concurrent refresh requests against rotating refresh tokens. A browser burst after access-token expiry could send the same stale refresh token through several requests before the first Set-Cookie response was applied. The first provider call rotated the token; a sibling replay could then trip provider reuse detection and revoke the session even though the gateway remained healthy.
- Native refresh first gained bounded single-flight ownership (
f561155a): the native refresh path coalesced concurrent calls per concrete provider rather than letting provider hints split one rotating credential’s refresh flight, and moved the synchronous provider refresh off the ASGI event loop. - Cookie and native refresh now share one replay boundary (
5dea46d1): Hermes generalizes that work intorefresh_session_coalescedand uses it from both the cookie gate and/auth/native/refresh. The final replay key is the refresh token itself, so a burst that straddles a network change still converges on one provider rotation; the middleware keeps its existingrefresh_expiredandprovider_unreachableaudit outcomes through callbacks. - Provider I/O no longer wedges unrelated dashboard requests: both refresh paths invoke the synchronous provider refresh through
run_in_threadpool. Upstream’s live E2E uses real uvicorn plus a stub rotating IdP with reuse detection. The pre-fix baseline records only 1/4 requests surviving each concurrent burst, 4 provider calls, and/api/statusdelayed about 2.7 seconds behind one slow refresh. The corrected path records 4/4, 1 provider call, and/api/statusreturning in about 10 ms during that slow refresh.
The harness-design lesson is that a rotating credential is a single-owner transition, not an idempotent read: concurrent callers presenting the same old token must share the outcome, while blocking identity-provider I/O must not execute on the server event loop. This is Hermes dashboard/gateway authentication and liveness behavior included in stable v0.21.3. It does not change UHP, MCP or ACP wire semantics, does not establish native Hermes UHP adoption, and does not show that HarnessRouter’s released Hermes backend has been independently revalidated against this auth path.
Included in v0.21.3: auxiliary capability probes no longer poison the runtime client cache
Section titled “Included in v0.21.3: auxiliary capability probes no longer poison the runtime client cache”Hermes P1 issue #87654 records a tool-availability failure in long-running processes: an auxiliary capability probe could seed _AuxProbeClientStub into the normal runtime client cache. A configured vision tool could then pass its first availability check and fail later checks even though the underlying endpoint and tool configuration had not changed.
- The probe was mutating later execution:
_store_cached_client()already refused to persist_AuxProbeClientStub, but_get_cached_client()had an inline cache-write path that bypassed that invariant whileaux_probe_mode()was active. The issue reproduces a firstcheck_vision_requirements()result ofTruefollowed byFalseresults on later probes for a slash-bearing model id; the dashboard can still show the toolset enabled because configured enablement and the runtimecheck_fnresult are separate layers. - Probe mode is now observational (
a714ff98):_get_cached_client()can resolve and return the probe client, but while the auxiliary probe context is active it does not write that object into the normal runtime cache. The guard keys on probe mode rather than only on the concrete stub type, which also covers adapter-wrapped probe clients. - Regression coverage protects wrapped and unwrapped cases (
093c58ca): the follow-up tests both the bare probe stub and aCodexAuxiliaryClient-wrapped stub, performs three consecutive availability probes and expects all three to stay resolvable, while asserting that the runtime cache key remains absent. The upstream issue also explains the observed sync/async asymmetry: a poisoned sync cache could makebrowser_visiondisappear while an async image-analysis path used a different cache key and still resolved.
For harness interoperability, the reusable invariant is that capability discovery must not poison the execution client cache. A probe may construct a constrained or synthetic client to answer “is this available?”, but that object must not become the client later runtime work inherits.
This is Hermes tool-discovery/provider-compatibility and runtime-cache behavior included in stable v0.21.3. It does not change UHP, MCP or ACP wire semantics, does not establish native Hermes UHP adoption, and does not show that HarnessRouter’s released Hermes backend has been independently revalidated against this upstream release state.
ACP agent-provider path introduced by v0.21.0
Section titled “ACP agent-provider path introduced by v0.21.0”Upstream Hermes added a materially different interoperability path on 26 August 2026: an external agent reached through an ACP client shim can act as a provider inside the Hermes runtime. That path first shipped in v2026.8.27 / Hermes Agent v0.20.6 and was consolidated in v2026.8.31 / Hermes Agent v0.21.0, published on 31 August 2026. The v0.21.0 release explicitly rolls up the v0.20.1–v0.20.6 patch windows; its annotated tag points to commit 29112bef. Stable v0.21.4 now supersedes that release without changing the ACP-vs-UHP boundary described here.
Three related implementation changes define the current boundary:
- Hermes moved its ACP/OpenAI compatibility logic into a shared
agent/acp_openai_bridge.py. ACP itself does not expose OpenAI-styletools/tool_calls, so Hermes serializes the allowed Hermes tool schemas into the ACP prompt and parses structured tool-call blocks back into the OpenAI-shaped objects used by its own loop. - Runtime handling is now keyed on the generic
acp://scheme, rather than a Copilot-specificacp://copilotspecial case. Hermes therefore treats ACP-connected CLI agents as a runtime class: their subprocess/stdin-style completion path is kept off the Responses API upgrade and off Hermes’ normal iterable streaming path. - When the ACP-backed provider is itself an autonomous agent that already executed read/edit/execute work inside its own session, Hermes can receive completed projected tool-call/result rows plus an internal tool-iteration count. Hermes appends that completed work to the parent transcript for memory/skill-review visibility instead of returning it as pending tool calls and accidentally executing it again.
This is concrete agent-as-provider / harness-composition evidence. It means ACP can sit inside Hermes as an inner runtime boundary while Hermes remains the parent agent loop. It does not establish native UHP support in upstream Hermes, does not make ACP part of UHP, and does not prove that HarnessRouter’s UHP→Hermes backend invokes this ACP path for any particular UHP task.
The v0.21.0 release also rolls up the earlier patch-window runtime work and highlights Bot Mode, bot-to-bot peer messaging, cron continuity, live subagent steering, an expanded MCP management surface and Desktop browser control. Those are Hermes product/runtime developments rather than UHP protocol changes.
v0.21.0 baseline: runtime integrity, orchestration, compaction, approvals, messaging and delegation reliability
Section titled “v0.21.0 baseline: runtime integrity, orchestration, compaction, approvals, messaging and delegation reliability”The v0.21.0 release established the twelve integrity, performance and orchestration lines this guide had previously tracked only on upstream Hermes main: revision-aware live task-state reconciliation, compaction-boundary system-prompt refresh, live convergence of compaction-routing configuration for already-open sessions, single-request lean compaction, fail-closed approval handling for unattended programmatic sessions, environment-backed credential persistence/revocation hardening, Docker sandbox secret-transport hardening, multi-profile session and scheduled-job isolation, truthful failure semantics for delegated subagents, gateway-owned hosted Group Chat execution, Buzz messaging trust/delivery hardening, and bounded off-loop MCP shutdown during gateway teardown. The release tag commit 29112bef is downstream of the previously tracked MCP-shutdown fix 11ba76c, so the earlier current-main maturity label is no longer accurate. Stable v0.21.4 now supersedes this baseline as the current Hermes release. These remain harness/runtime behaviors rather than new interoperability protocols.
Revisioned live task state
Section titled “Revisioned live task state”The task-state reconciliation path addresses a desktop/runtime integrity problem: the Tasks view could become stale when optional tool-progress projection was suppressed or missed, or after resume/reconnect, even though the canonical todo tool result in the conversation transcript was correct.
The behavior now shipped in v0.21.0 has four relevant properties:
- Monotonic revisions:
TodoStorenow maintains a monotonic in-memory revision and returns it withtodotool results. Hermes clients can reject a snapshot older than the newest state already observed for that session. - Dedicated full-snapshot event: the TUI gateway emits provider-neutral
todo.updatedevents carrying the current full task snapshot independently of optionaltool.progress/tool.completedisplay settings. This is a Hermes gateway event, not a UHP event type. - Resume/activate arbitration: session resume and activation responses can carry
todo_state; the renderer tracks revisions per session and ignores regressions. A follow-up commit derives the authoritative resume snapshot from the conversation history the resume path already loaded, rather than adding a second parallel task-state database/read path. The persisted transcript therefore remains the recovery source of truth. - Patch integrity: desktop handling for
merge: truetodo writes merges updates by task id instead of treating a one-item/status-only patch as a replacement for the whole task list.
This is useful evidence of an upstream harness maintaining ordered live plan/task state across streaming, reconnect and resume. It should not be conflated with UHP’s Task resource: Hermes todo items and todo.updated are internal Hermes runtime/UI semantics, not UHP task/session objects, not a UHP extension, and not evidence that HarnessRouter’s Hermes backend maps or exposes this internal state through UHP.
The task-state reconciliation path remains a released baseline rather than a current-main-only claim.
Compaction-boundary system-prompt refresh
Section titled “Compaction-boundary system-prompt refresh”Hermes PR #98426, merged as commit 514707f on 30 August 2026, closes a separate long-lived-session drift problem. Before this change, tools could rebuild from the live registry at a compaction commit while the stored system prompt was restored byte-for-byte from the session snapshot. A Bot Mode or gateway session could therefore keep guidance from its original session start even after memory, prompt-builder guidance, plugin sections, names or other prompt inputs had changed.
At each admitted compaction commit boundary, stable v0.21.0 now:
- runs the live system-prompt builder unconditionally rather than using the old memory-containment keep-prompt gate;
- preserves the original prompt string object when the rebuilt bytes are identical, so existing KV/prefix-cache reuse remains possible in the no-drift case;
- replaces the stored prompt and logs the drift when the rebuilt bytes differ, allowing long-lived sessions to converge on current memory/guidance without introducing a mid-turn rebuild point;
- re-renders plugin prompt sections at that boundary, with a fail-open fallback to the last good rendered bytes if a plugin render raises; restore paths between compactions retain their existing byte-stable behavior; and
- resolves the displayed “Conversation started” timestamp through the session-lineage root so compaction/rotation does not make a long-lived conversation appear newly born at the latest rotated session id.
Upstream reports a six-test compaction-prompt contract suite plus 44 system-prompt tests green; the PR explicitly describes the end-to-end forever-session confirmation as one /compress away after merge, so this guide does not upgrade that source-pinned test evidence into a broader production validation claim.
This change matters to harness durability because tools and instructions can otherwise diverge across long-lived context lifecycles. It remains an internal Hermes compaction/session semantic: it is not a UHP lifecycle rule, does not alter UHP previous_response_id, and does not establish that HarnessRouter’s UHP→Hermes adapter invokes, exposes or independently revalidates this compaction path.
Live compaction routing now follows config changes
Section titled “Live compaction routing now follows config changes”Hermes PR #98547, merged on 30 August 2026 through commits 77f5de6 and 66666f6, closes another stale-session boundary. Before this fix, changing compression.codex_responses_native or compression.codex_responses_compact_threshold after a Desktop/TUI session had already opened could update the profile while leaving that session’s cached foreground agent on its old route. A large Codex-backed session could therefore continue using local transcript summarization until a fresh session was created even though newly constructed auxiliary agents were already using native Responses compaction.
Stable v0.21.0 hot-applies the native-compaction enable flag and threshold to already-open Desktop/TUI sessions, and removal of either setting restores the documented defaults. The messaging-gateway agent-cache signature also covers those two keys plus the other construction-baked routing controls compression.in_place, compression.checkpoint_required, compression.micro_compact, compression.micro_compact_every_n_turns, and compression.micro_compact_defrag_threshold_tokens, so a config edit can no longer leave an indefinitely stale cached routing decision for those settings.
The PR describes the update as routing-only: it does not rebuild the system prompt, swap the toolset or mutate prior conversation context. Its recorded validation covers a live open-session config flip and removal-to-default behavior, with 51 hot-reload/agent-cache tests plus 52 native-compaction sibling tests passing. This guide therefore treats the behavior as source-pinned merged code/test evidence now included in stable v0.21.0, not as an independent production-runtime certification.
For harness architecture, the point is broader than one compression flag: configuration that changes execution routing must either update live runtime state or invalidate every cache that bakes that routing decision. Otherwise a long-lived foreground session and a newly spawned child can silently execute under different policy despite sharing the same current profile.
This remains internal Hermes runtime/configuration behavior. It is not a UHP lifecycle, session or capability primitive, does not establish native Hermes UHP adoption, and does not show that HarnessRouter’s released Hermes backend exposes or independently revalidates this compaction-routing path through UHP.
Lean compaction now makes one auxiliary LLM request
Section titled “Lean compaction now makes one auxiliary LLM request”Hermes PR #98628, merged as commit 4f22543 on 30 August 2026, fixes a major latency regression in the default lean-tail compaction path. The previous implementation issued the main summary request and then up to 28 sequential per-chunk digest requests. Upstream issue evidence records 7–11 minute compactions on slow auxiliary routes.
Stable v0.21.0 removes the per-chunk digest loop and defines a stricter execution contract: one auxiliary call_llm request per lean compaction attempt — the main summary call. The detailed session log is folded into that same response with an additional 4,000-token output-guidance budget. For oversized compacted regions, Hermes now samples eight proportionally spaced slices across the region, oldest to newest, with explicit elision markers and the final slice anchored at the newest end instead of relying on head+tail truncation. The LLM-free anchor index still scans the full region, and the session_search recovery footer remains unchanged.
The upstream PR includes a live A/B on the same 1,338-message, roughly 499,625-token historical transcript: auxiliary calls fell from 19 to 1, wall time from 196.5 seconds to 39.6 seconds (about 5× faster on that fast auxiliary route), and post-compaction context from 57,567 to 46,135 tokens, while the anchor index and recovery footer remained present. The PR also reports the single-call contract as sabotage-tested and its related test sets passing. These are upstream source-pinned benchmark/test results, not an independent production certification; the 7–11 minute figure describes the slower route reported in the motivating issue rather than the A/B route itself.
For harness engineering, this is both a liveness and context-efficiency correction: a default compaction boundary should not multiply auxiliary-model latency by the number of transcript chunks, and large-history retention now combines deterministic full-region anchors with uniformly sampled summary input rather than serial LLM fan-out.
This remains internal Hermes context-management behavior. It is not a UHP lifecycle or streaming primitive, does not change UHP conformance semantics, does not establish native Hermes UHP adoption, and does not show that HarnessRouter’s released Hermes backend invokes or independently revalidates this compaction implementation.
Unattended programmatic approvals now fail closed immediately
Section titled “Unattended programmatic approvals now fail closed immediately”Hermes PR #98558, merged on 30 August 2026, closes a security and liveness defect in unattended gateway surfaces. webhook, msgraph_webhook, and api_server sessions set HERMES_SESSION_PLATFORM like interactive messaging gateways, so dangerous-command guards could previously route them into the interactive approval wait even though those adapters have no send_exec_approval method and no /approve reply channel. The upstream report records waits of 60–300 seconds before the request eventually failed closed; a live webhook reproduction waited the full 300 seconds.
Stable v0.21.0 classifies those three surfaces as an explicit unattended platform set and resolves dangerous-action approval without opening an impossible human round trip:
approvals.unattended_modeis a newdeny | approvepolicy withdenyas the default.- The deny path is applied across the shared approval gate, dangerous-command guards (including Tirith parity), and
execute_codeguard handling. - Safe commands remain allowed; interactive chat gateways such as Telegram remain eligible for normal human approval flows.
- Operators can explicitly set
unattended_mode: approve, which restores automatic approval for flagged actions on those unattended surfaces and therefore deliberately widens the trust boundary.
Upstream PR validation reports 121/121 targeted approval tests passing and records both a live pre-fix pending-approval reproduction and an instant-deny verification after the fix. This guide treats that as source-pinned evidence now included in stable v0.21.0, not as an independent production certification.
The architectural point is important for autonomous harness operation: a policy requiring human approval is not a safe control if the transport has no way to obtain that approval. Hermes now makes that boundary explicit and fail-closed by default rather than waiting for a nonexistent approver. This is still Hermes-internal authorization/runtime policy — not a UHP permission primitive, not a change to UHP error or task semantics, not native Hermes UHP adoption, and not evidence that HarnessRouter exposes or revalidates this path through UHP.
Environment-backed credentials now preserve pointers and converge across stores
Section titled “Environment-backed credentials now preserve pointers and converge across stores”Stable v0.21.0 includes a five-part credential-lifecycle and routing correction: preventing plaintext spill is not sufficient unless the model keeps a usable credential reference, rotation/removal reaches every higher-precedence mirror, the live credential pool converges immediately after a Desktop save, and a model alias cannot carry one provider’s bearer secret across an endpoint-origin change.
- Model assignment preserves the pointer (issue #88990; commits
a90be562and8fd144c):POST /api/model/setpreviously read providers throughload_config(), where${VAR}references had already been expanded, then could mirror the resolved plaintext into top-levelmodel.api_key. Commita90be562stopped that unsafe resolved-secret copy. The later8fd144ccompletes the behavior: assignment and custom-endpoint activation now read the raw provider entry and carrykey_envor the raw${VAR}template into the model configuration, while literal on-disk keys keep their legacy copy behavior. The credential therefore remains resolvable at runtime without persisting the expanded secret. .envwriter/reader parity (PR #67488, commit1152d4d3):load_env()already accepted assignments such asOPENAI_API_KEY = valueby stripping whitespace around the parsed name, but the save/remove helper did not. That meant a dashboard delete could return “not found” while the credential remained live, and rotate-then-delete could expose an older spaced assignment again. The writer now recognizes the same assignment shapes as the reader; the PR records 80 passing env/credential tests and endpoint-level reproduction of the fixed rotation/deletion path.- Rotation/removal now reaches keyed provider mirrors (commit
22f9caf): the credential scrubber previously reconciledmodel,auxiliary.<task>andcustom_providers.<name>but skipped the newer keyedproviders.<id>.api_keyschema used by Desktop/custom endpoints. Because that inline key outranks the env var, an old value could shadow a freshly rotated credential or survive an explicit delete. The shipped implementation scrubs thatapi_keyon rotation/removal while deliberately preservingproviders.<id>.api, which is a base-URL alias in this schema rather than a credential. The commit reports 15 focused tests and 345 credential-lifecycle/web-server tests with one unrelated failure reproduced on clean main. - Desktop saves materialize the live credential pool (commit
82733a3):PUT /api/envpreviously updated.envwithout necessarily materializing the env-seeded provider entry inauth.json, so the runtime could keep returning 401 until a separatehermes auth add <provider> --type api-keycaused the pool to load. The save path now callsload_pool()for providers registered to that environment variable, creating the sanitized env-backed pool entry immediately so a fresh pool read can surface the just-saved token to the next request. - Direct model aliases keep credentials origin-bound (issue #83612; commit
3145986): amodel_aliasesentry could silently drop its ownapi_key, resolve a default-provider credential first, then swap only thebase_urland send that unrelated bearer token to the alias host. The shipped implementation makesDirectAliasretainapi_key/key_env, resolves the alias credential against the alias endpoint, passes the alias key through the one-shot-m <alias>path, and reuses an already-resolved session credential only when the endpoint keeps the same(scheme, host, port)origin; plaintext reuse is refused outside loopback. Cross-origin switches therefore drop the session key instead of forwarding it. Because direct aliases can now carry credentials, their process-global cache is also keyed by the active profile’s config identity so a multiplexed gateway does not reuse one profile’s alias definitions or keys in another profile. The salvaged source PR reports 19 targeted regression cases, 12 of which failed on an unpatched tree, and the final merge adds explicit scheme/port/cross-host downgrade coverage plus the missing host gate forOLLAMA_API_KEYon direct aliases.
Together, these changes tighten secret persistence, credential-pointer continuity, revocation/rotation integrity, runtime convergence and endpoint-origin isolation. They now ship in stable v0.21.0; that maturity change does not turn the upstream tests into a separate security certification.
The UHP boundary remains unchanged. These are Hermes Desktop/CLI configuration, credential-pool and model-routing behaviors, not UHP credential fields or authentication rules, and they do not establish that HarnessRouter’s released Hermes backend exposes these local configuration paths. HarnessRouter’s own relay-based provider-key isolation remains a separate implementation fact described above.
Docker sandbox secret forwarding no longer exposes values in process argv
Section titled “Docker sandbox secret forwarding no longer exposes values in process argv”Hermes PR #99637, merged on 31 August 2026 as commit d10ef89, closes a separate host-level secret-transport leak in the Docker terminal backend. The allowlist controlled which variable names could enter the sandbox, but the Docker client still received each allowed value as -e KEY=VALUE in its command line on container start, recovery/recreation, init-seeding docker exec, and every per-command runtime docker exec.
On Linux, the upstream issue and PR document /proc/<pid>/cmdline as mode 0444, so another local account could read those forwarded values through process inspection while the Docker client was alive. The corrected path keeps only the variable name in argv as -e KEY, injects the value into the spawned Docker client’s own environment, and relies on Docker’s documented valueless --env KEY behavior to propagate the current client-environment value into the container. The source therefore moves the secret from the world-readable command-line surface to the client process environment; the upstream live evidence records /proc/<pid>/environ as owner/root-only 0400 on the tested Linux host.
The fix covers all four affected construction/execution paths and preserves profile-scoped forwarded values. The PR’s sibling audit found that the separate Hermes CLI docker-exec forwarding path carries only TERM, COLORTERM, LANG and LC_ALL, while the Singularity and SSH environment backends do not use the affected -e KEY=VALUE transport. Upstream validation reports 63 Docker-environment tests plus 35 isolation/network/cgroup/privilege tests passing, and adds two regressions that fail on the old argv-value behavior.
This is a material sandbox/credential-boundary correction for multi-user hosts, but it is still Hermes Docker-backend implementation behavior. It does not define a UHP secret transport, sandbox, authentication or environment-variable primitive; it does not establish native Hermes UHP adoption; and it does not show that HarnessRouter’s released UHP→Hermes backend uses or independently revalidates this released Docker path.
Multi-profile session and scheduled-job isolation now follows the owning profile
Section titled “Multi-profile session and scheduled-job isolation now follows the owning profile”Stable v0.21.0 closes a connected profile-integrity failure class: an operation could be routed under one profile while a deeper persistence, mutation, cron-execution, delivery, or health-check layer still defaulted to another profile’s state.
The merged corrections cover the full path rather than one UI symptom:
- Session ownership is stamped at creation (commit
5cc3da6): when a managed profile-treeSessionDBcreates a row without an explicitprofile_name, it derives the owner from its ownstate.dbpath — rootstate.dbmaps todefault, andprofiles/<name>/state.dbmaps to that profile. This catches creation paths beyond the main gateway writer, including CLI/create-if-missing, foreign-session import, the ACP adapter, gateway self-heal and compression children. Explicit owners still win; stores outside the managed profile tree remain unstamped rather than guessing. - Desktop mutations target the owning database (commit
18f429a): delete now carries the owner in?profile=, while archive/pin/unread PATCHes carry it in the request body. Previouslyrequest.profilecould route Electron to the right gateway without telling the backend whichstate.dbto mutate, so a foreign-profile delete could report{ok:true, already_absent:true}against the wrong database and then reappear after refresh. Upstream records a live remote-primary verification of the corrected DELETE path plus regression coverage for all four mutations. - Cron keeps one profile secret scope through execution and delivery (commit
07f6518): scheduled work no longer drops the selected profile’s secret context between the model run and_deliver_result; the scope is reset only after the complete lifecycle, reducing both fail-closed credential misses and cross-profile fallback risk. - Multiplex delivery uses the owning profile’s adapter (commit
51e377d): the multiplex ticker now selects each profile’s own live adapter set. Before that correction, a secondary profile’s scheduled output could be delivered through the default profile’s bot even though execution itself was already scoped to the secondary profile. - Routed satellite profiles pass delivery preflight without credential duplication (commit
d5d0613): if the satellite’s own home correctly has no platform credential, preflight can recognize an enabled primary-gatewayprofile_routesroute to that profile. It reads only the primary route metadata rather than importing primary platform config/secrets, and lookup/config failures leave the block in place. - Cron health reporting is profile-scoped (commit
82d7a13): systemd process discovery now filters to the current profile by default, and a missing ticker heartbeat produces a warning instead of treating another profile’s running gateway as evidence that the current profile’s jobs will fire.
For harness engineering, the material point is ownership continuity: selecting a profile at ingress is insufficient unless persisted rows, mutations, credentials, scheduled execution, delivery adapters and health evidence all retain the same owner boundary. The fixes materially reduce false success, invisible sessions and cross-profile scheduled-message delivery in Hermes’ multiplexed profile mode.
This remains Hermes-internal profile isolation and scheduled-execution behavior. It is not a UHP tenant model, authentication rule, session primitive or cron semantic; it does not establish native Hermes UHP adoption, and it does not show that HarnessRouter’s released Hermes backend exposes or independently revalidates these profile-management paths through UHP.
Delegated subagent failures now stay failures
Section titled “Delegated subagent failures now stay failures”Stable v0.21.0 includes two connected delegation-reporting corrections merged on 31 August 2026 that prevent a child failure before useful work from being mislabeled as successful budget exhaustion.
Commit ec02d51 changes _run_single_child so structured failure fields (failed or error) take precedence over the old summary-presence heuristic. A provider-rejected child — for example an HTTP 400 invalid-model response — is now reported as status=failed, exit_reason=error, and truncated=false instead of the contradictory status=completed, exit_reason=max_iterations and “TRUNCATED” banner. Genuine iteration-budget exhaustion still retains max_iterations/truncation semantics, while interrupted children remain interrupted.
Commit c05d04f adds a separate config-level model_not_found notice to both single-task and fan-out delegation reports. The notice names the configured subagent model and provider, points to Settings → Advanced → Subagent Model / hermes config get delegation.model, and states when no fallback chain is configured. To avoid misattribution, it fires only when the rejection text names the currently configured delegation model; an error naming some other or stale model does not trigger the configuration warning.
PR #99098, merged later on 31 August, extends that same control-plane contract: failed entries now propagate a non-empty child failure_reason such as rate_limit, billing or server_error, so the parent can classify quota/provider failure without parsing the human-readable summary. The model-rejection renderer also imports the canonical pattern list from agent.error_classifier instead of maintaining a duplicate copy. The PR reports 109 passed, 1 skipped across its targeted delegation suites.
For harness engineering, this is a control-plane correctness issue rather than cosmetic output: a parent agent or operator must be able to distinguish provider/configuration failure from iteration-budget exhaustion, and now can also preserve the child’s coarse failure class. Mislabeling a dead child as completed/truncated can push retries toward task-size or budget tuning while the actual fix is correcting the configured model, quota state or failover policy.
This remains Hermes-internal delegation/runtime behavior. It does not modify UHP task terminal states or UHP error semantics, does not establish native Hermes UHP adoption, and does not show that HarnessRouter maps or revalidates these delegation fields through its released Hermes backend.
Gateway-owned hosted Group Chats can span gateways
Section titled “Gateway-owned hosted Group Chats can span gateways”Stable v0.21.0 ships a separate orchestration subsystem for hosted Bot Mode Group Chats. This is not an extension of delegate_task: the room, authority, replay log and turn scheduler are gateway-owned durable state, and selected room members can be routed to trusted peer gateways.
Four merged layers define the current boundary:
- Durable authority and replay (PR #99007): each hosted room has server-issued authority (
authority_gateway_idplus a monotonicauthority_epoch) and an ordered event log in rootstate.db. Appends are idempotent, stale writers are fenced, and replay pages carry authority lineage. That layer intentionally shipped with the driver off. - Replica/takeover primitives (PR #99047): participant gateways can store authority-stamped replicas, reject gaps/epoch regressions, explicitly promote a caught-up replica at
epoch + 1, and demote a returning stale authority. Promotion requiresconfirm: true; the storage layer does not decide when takeover is safe, so this is fenced failover machinery rather than an autonomous consensus protocol. - Same-gateway execution driver (PR #99099): the gateway owns turn scheduling through durable task identities, leases, execution/cancel generations, discussion projection and policy checkpoints, then runs local member turns through real agent sessions. The
groups.*surface gainsstop,retryandapprove, and the upstream PR reports 247 hosted-room tests passing after a deterministic disband/cancellation race was corrected. - Scoped cross-gateway transport (PR #99244): a room authority can dispatch member turns to bots on another trusted Hermes gateway using signed, TTL-bounded grants scoped to the room, member and target. Peer capability state is digest-frozen into the authorization boundary; grant refresh refuses execution-policy drift with HTTP 403
room_reauthorization_required. Remote room execution is refused while the target gateway runs YOLO/approvals-off. The transport uses authenticated signed tasks, verified results, retry-safe task identities and peer-stop acknowledgement. The PR reports 283 hosted-room/transport/grant tests passing across ten suites, plus a sabotage-verified policy-drift regression test.
Cross-gateway mode is opt-in rather than ambient discovery. An operator must configure a peer-reachable public HTTPS gateway.room_link_url (or HERMES_ROOM_LINK_URL); plain HTTP is accepted only for loopback testing. The API server key authorizes the initial invitation, while peer grants are signed with a separate installation-private secret. Disbanding a Group Chat revokes its known peer routes, but API-key rotation by itself does not revoke those scoped grants. Upstream documentation therefore also recommends limiting network reachability to trusted peer gateways.
The practical result is that laptop, homelab and VPS-hosted bots can participate in one gateway-owned room without Desktop relaying every turn. The NAT boundary is explicit: Desktop is a viewer, not a relay, so the room authority should live on a host the selected peer gateways can actually reach.
This does not establish automatic distributed failover or consensus for active room execution. PR #99047 still exposes explicit replica promotion/fencing primitives, while PR #99244 adds direct authenticated peer execution. A reachable peer route and a promoted replica are different mechanisms and should not be collapsed into a claim that Hermes automatically migrates a live room across gateways after failure.
Architecturally, this is durable multi-agent orchestration inside Hermes: authority, replay, scheduling, cancellation, policy and authenticated peer routing can outlive the UI process. It is not a UHP room/task model, does not add Group Chat semantics to UHP, does not establish native Hermes UHP adoption, and does not prove that HarnessRouter maps or revalidates this hosted-room subsystem through the released Hermes backend.
Buzz messaging now has explicit trust, thread and delivery integrity boundaries
Section titled “Buzz messaging now has explicit trust, thread and delivery integrity boundaries”Hermes PRs #99427, #99429, #99431 and #99432, merged on 31 August 2026, reconcile a large Buzz/Nostr gateway backlog into one coherent messaging boundary rather than a set of unrelated transport fixes.
- Authentication and profile isolation (#99427):
npub1…allowlist entries are normalized before comparison; NIP-OAauth_tagand Buzz credentials resolve inside the owning profile; externally managed secrets can satisfy preflight without copying them into the default environment; and multiplex credential discovery fails closed. Upstream reports 95 targeted tests and a 9/9 real-adapter synthetic-event reproduction across allowlist, per-profile auth and external-secret cases. - Stable thread ownership (#99429): replies recover the existing NIP-10 root instead of creating a nested sub-thread on every turn; the stable root becomes
source.thread_id, so turns in one thread share a session. Deployments can opt out withreply_to_mode: off/extra.reply_in_thread: false, and the same choice is honored by normal sends, media, commentary, tool progress and standalone cron delivery. The PR reports 121 targeted tests, including 17 new end-to-end regressions. - Addressing and approval liveness (#99431): forum kinds join dispatch; a NIP-10 reply to the agent’s own message can satisfy
require_mention, unblocking reply-channel flows such as/approve; group wake-up requires an explicit@mentionor signed recipientp-tag rather than a bare display-name match; and UUID targets do not silently fall through to the home channel. The PR reports 95 targeted tests, four sabotage checks and an 11/11 real-adapter synthetic-event reproduction. - Credentialed media ingress and verified egress (#99432): inbound same-relay Blossom URLs and NIP-94
imetaattachments are accepted only through constrained verified fetch paths: HTTPS exact-origin rules, count/size caps, no redirects, Content-Length/byte-count checks and SHA-256 integrity. A credentialed fetch additionally requires the authorization callback to return the literal booleanTrue;False,None, exceptions and truthy non-booleans all fail closed beforebuzz media get. Outbound image/video/voice/document sends use one native file path with a strict JSON receipt contract, path-redacted errors and verifiedmedia_deliveredreceipts. The PR reports 302 + 76 targeted tests and a 17/17 end-to-end reproduction with a real adapter import, local HTTP server and fake CLI subprocess.
For harness engineering, the important pattern is that messaging transport semantics are part of the execution trust boundary. Sender authorization must be decided before credentialed fetch side effects; a reply/thread identity must remain stable enough to preserve session context; approval messages must reach the waiting agent without weakening group-addressing rules; and outbound success should be tied to a verified receipt rather than process exit alone.
These are Hermes Buzz/Nostr adapter and gateway semantics, not UHP message, approval, attachment or session primitives. They do not establish native Hermes UHP adoption and do not show that HarnessRouter’s released Hermes backend exposes or independently revalidates the Buzz transport path.
MCP shutdown no longer blocks the gateway event loop
Section titled “MCP shutdown no longer blocks the gateway event loop”Hermes commit 11ba76c, merged on 31 August 2026, closes issue #82874 in the gateway shutdown path. Before this change, the gateway called synchronous shutdown_mcp_servers() directly from its asyncio event loop. That function can block on an MCP-loop future for up to 15 seconds; when SIGTERM tears down the MCP loop and its stdio children concurrently, the main loop could stop making progress long enough for a shorter-grace supervisor to SIGKILL the gateway before lifecycle_ledger.mark_exited() ran. The next boot could then report a routine restart as exited UNCLEANLY.
The motivating issue reproduced the failure under container-wide process-tree shutdown and specifically records s6-overlay’s 3000 ms default grace as shorter than the 15-second blocking wait. It also isolates the race: fully alive or fully dead MCP children completed quickly, while children caught mid-teardown could wedge the future wait.
Stable v0.21.0 runs shutdown_mcp_servers() on a daemon thread and waits for that thread through the existing _await_thread_exit helper with a five-second budget. If shutdown still has not completed, Hermes logs a warning and continues gateway teardown instead of freezing the event loop. The helper is used at both gateway shutdown call sites. Two new asyncio regressions pin the behavior: an intentionally wedged 30-second shutdown must leave an event-loop heartbeat responsive and return False after the bounded wait, while the normal fast path returns True.
For harness engineering, this is both a liveness and observability-integrity correction: cleanup of a child capability subsystem should not block the owning event loop long enough to convert an orderly supervisor stop into a hard kill and false lifecycle evidence.
This remains Hermes gateway/MCP lifecycle implementation behavior. It does not change MCP wire semantics, define a UHP shutdown primitive, establish native Hermes UHP adoption, or show that HarnessRouter’s released Hermes backend invokes or independently revalidates this gateway teardown path.
Historical post-v0.21.0 evidence: auxiliary timeout teardown preserves file-descriptor ownership
Section titled “Historical post-v0.21.0 evidence: auxiliary timeout teardown preserves file-descriptor ownership”Hermes main advanced beyond the 31 August release. PR #99709, merged after v0.21.0 as 8a766c3, closes a data-integrity risk in the Codex auxiliary timeout watchdog: a daemon Timer thread could call close() on a shared HTTP/TLS client while the request-owning thread was still unwinding. Upstream ties that behavior to the same file-descriptor-recycling corruption class documented in #29507, where a released TLS fd can be reused by a newly opened SQLite database before OpenSSL finishes flushing the old connection.
The reviewed correction makes the timeout thread shutdown sockets without releasing the shared client’s file descriptors, then defers the real client close() to the request-owning thread’s finally path. The PR adds focused thread-identity regressions and reports 246 sibling auxiliary tests passing; a sabotage run that restores stranger-thread close fails the stalled-stream regression. The upstream report is explicit that the actual kernel fd-recycling corruption remains probabilistic, so this guide treats the deterministic ownership tests as source-pinned evidence rather than an independent live corruption reproduction.
This is internal Hermes transport/data-integrity behavior after stable v0.21.0. It is not a UHP transport or storage primitive, does not change MCP/ACP semantics, and does not establish native Hermes UHP adoption.
Historical post-v0.21.0 evidence: live state.db replacement now fails closed
Section titled “Historical post-v0.21.0 evidence: live state.db replacement now fails closed”Hermes issue #89332 documents a more severe persistence-integrity failure class: replacing a live WAL-mode state.db out of band with cp, mv or a restore could leave the running process attached to stale database/WAL/SHM state. The reported production incident lasted 17 minutes; appends kept failing while the gateway remained alive, and turns could be lost instead of producing a clear fail-stop condition. That duration and incident narrative are upstream issue evidence, not an independent reproduction by this guide.
Commit 71256dfd makes database generation part of the persistence boundary rather than treating every SQLite corruption-shaped error as repairable in place:
- File plus generation identity:
SessionDBrecords(st_dev, st_ino)where the platform exposes useful values, while a generated token is stored instate_metaand mirrored into SQLiteapplication_id. The inode check catches rename/new-file replacement; the generation stamp also catches same-inode truncate/copy replacement that inode identity alone cannot distinguish. - Fail-stop classification: replacement raises
StateDbReplacedErrorand enters the dedicated persistence causereplaced. Before FTS repair/fail-open work, Hermes re-checks the file generation; a mismatch stops writes instead of attempting schema surgery against a different database generation. - Transcript fallback: gateway/session paths move unwritten transcript material to
sessions/<id>.jsonland the gateway pending-message spool so the remainder of the turn is not left only in RAM after SQLite writes are refused. - Reopen guard: reopen-after-close checks the recorded generation before resolving the path again, preventing a stale live
SessionDBfrom silently attaching to the replacement file.
The follow-up chain through Hermes commit 8dbf07e9 tightens the connection-lock/read context around the identity helpers and closes test database handles before cleanup. Those changes preserve the replacement guard rather than weakening it.
For harness engineering, the important principle is that a durable state store has an identity, not merely a pathname. Once a live process detects that the underlying database generation has changed externally, continuing repair/retry logic can turn a recoverable operator mistake into silent persistence loss or further corruption; stopping the write path and preserving unwritten transcript evidence is the safer boundary.
This is internal Hermes state/persistence behavior after stable v0.21.0. It is not a UHP storage, session, recovery or error primitive, does not change MCP/ACP semantics, does not establish native Hermes UHP adoption, and does not show that HarnessRouter’s released Hermes backend exposes or independently revalidates this internal state.db path.
Historical post-v0.21.0 evidence: multiplexed session storage and restart recovery follow the owner
Section titled “Historical post-v0.21.0 evidence: multiplexed session storage and restart recovery follow the owner”Hermes issue #66887, closed on 31 August 2026, identifies a profile-isolation boundary not covered by simply selecting the right profile at ingress. In a multiplexed gateway, config, model, SOUL and memory could all run under the secondary profile while background session work still resolved an ambient/default state.db. The issue records the practical consequence: secondary-profile session history, compaction/billing/FTS state and routing could land in or diverge against the default store.
Commit 5ffaed6e makes per-session persistence resolve from the profile encoded in the session key/session owner instead of ambient HERMES_HOME. That matters most for unscoped background paths such as session expiry and session-id-addressed transcript/compression operations. Upstream field evidence records two live sessions whose root/profile copies had opposite end_reason values, confirming that multiple writers had been touching different stores; once those copies disagreed, stale-routing recovery could silently drop and recreate a live conversation.
Commit 58dcc100 tightens the ownership lookup for profiles provisioned after gateway startup: only successful profile-home resolutions are memoized. A key can legitimately be observed before an enrollment bridge creates profiles/<name>/; caching that miss would otherwise pin the future profile back to the ambient store for the life of the process.
The second half, commit 8d74cb52, separates process-wide routing state from per-session state. Hermes keeps one flat routing index spanning every profile, so that index now has one deterministic gateway-owned store rather than whichever profile scope is active during a write. Per-session ended/pruning checks still resolve the owning profile’s database. This also closes the restart-recovery failure: a secondary-profile mark_turn_active() crash marker can no longer land in a store that the unscoped startup recovery pass never reads. The new regression marks a secondary-profile turn active, constructs a fresh unscoped store, runs recover_interrupted_turns(), and verifies exactly one turn is promoted to resume_pending with restart_interrupted.
For harness engineering, the durable principle is ownership must survive ambient-scope loss. Per-session state should resolve from durable session identity, while genuinely process-wide indexes need one deterministic owner; background jobs and restart recovery cannot safely depend on whichever profile context happens to be active.
These are Hermes gateway/session persistence and recovery semantics after stable v0.21.0. They are not UHP tenant, session, routing, recovery or error semantics, do not establish native Hermes UHP adoption, and do not show that HarnessRouter exposes or independently revalidates these internal profile-storage paths through UHP.
Historical post-v0.21.0 evidence: a confirmed stop cannot become crash recovery
Section titled “Historical post-v0.21.0 evidence: a confirmed stop cannot become crash recovery”Hermes closes a cancellation/recovery race where a turn that the user had explicitly stopped could later be mistaken for a crashed turn and automatically continued.
Commit e0717231 first makes Desktop /stop target the active turn through the same session.interrupt gateway path used by the composer Stop control, then retains the existing process.stop cleanup for background terminal processes. Previously the Desktop slash-command path could stop background processes without necessarily cancelling the active turn.
Commit 5c3bfb6d closes the deeper recovery-marker race after a confirmed local interrupt. The gateway previously left the turn’s crash-recovery marker on disk until the run thread’s finalizer. If the backend exited in that window, a later resume could interpret the surviving marker as crash evidence and auto-continue work the user had intentionally stopped.
The reviewed implementation records the active marker identity before the marker write, retires the original and compression-rotated marker identities as soon as session.interrupt acknowledges the Stop, and re-checks _turn_cancel_requested immediately after the disk write so a Stop racing marker creation cannot leave resumable state behind. Commit 1b472770 adds regressions for both directions of the race: Stop after the marker exists and Stop before the marker write completes.
For harness lifecycle design, the important invariant is that a confirmed user cancellation and crash-recovery evidence must not coexist for the same active turn. Once Hermes acknowledges Stop, stale recovery metadata can no longer resurrect that turn.
This is Hermes Desktop/gateway lifecycle behavior after stable v0.21.0. It does not change UHP cancellation semantics or UHP Task terminal-state rules, does not establish native Hermes UHP adoption, and does not show that HarnessRouter exposes or independently revalidates this internal Stop/recovery path through UHP.
Historical post-v0.21.0 evidence: profile exports are fenced away from source and image contexts
Section titled “Historical post-v0.21.0 evidence: profile exports are fenced away from source and image contexts”Hermes security issue #92457 documents an exposed profile archive that contained a configured inbound-webhook HMAC secret and remained reachable through repository history; the incident record explicitly establishes exposure, not exploitation. PR #100101, merged on 1 September 2026 with final commit 80146676, adds recurrence controls around the operation that creates those archives rather than relying only on cleanup after an archive reaches source control.
The reviewed boundary is layered:
- Safe automatic destination:
hermes profile export, the interactive/exportcommand and the profile-management API now shareget_profile_export_path()instead of defaulting to<name>.tar.gzin the caller’s current working directory. Automatic exports are routed to a managed profile-export store outside named profiles. - Path-anchored checkout detection: candidate safety is evaluated against the export path’s own resolved ancestry rather than
Path.cwd(), so cron/service-manager invocation from outside the repository cannot make a Hermes home inside a checkout look safe. If every automatic candidate resolves inside a Git checkout, the operation fails closed and requires an explicit external destination. - Hardened fallback ownership: the temporary fallback uses a per-user directory name and refuses a symlink or a directory owned by another user, preventing a predictable shared path from becoming a local archive-capture primitive.
- Executable publication gate: a dedicated checker rejects
*.tar.gzand*.tgzartifacts anywhere in the checkout before normal CI approval and before Docker build/publish. The checker reports filenames only and does not inspect archive contents, so the enforcement path itself does not disclose embedded secrets. - Atomic archive writing remains composed: the change keeps the existing atomic
make_targzimplementation, so automatic exports gain both an external destination boundary and temp-file/rename write integrity.
Explicit output paths remain available for intentional operator exports; the new invariant applies to automatic destinations and repository/image publication. For harness security design, the important point is that ignore files are only passive hygiene: secret-bearing generated artifacts need a producer-side safe default plus an executable publication boundary that still catches forced staging or files generated after checkout.
This is Hermes profile/export and release-pipeline security behavior after stable v0.21.0. It is not a UHP file/artifact, credential, sandbox or publication primitive, does not change MCP/ACP semantics, does not establish native Hermes UHP adoption, and does not show that HarnessRouter’s released Hermes backend invokes or independently revalidates this profile-export path.
Historical post-v0.21.0 evidence: orphan-history resets keep a bounded rescue ref
Section titled “Historical post-v0.21.0 evidence: orphan-history resets keep a bounded rescue ref”Hermes issue #87694 documents an updater-failure mode where the autostash path left HEAD on an orphan commit with no common ancestor with origin/main. The reported install then could not fast-forward on later hermes update runs; one cron sequence stopped the dashboard and stalled before restart, leaving it down for ten days. The issue is upstream field evidence rather than an independent reproduction by this guide.
PR #100155, merged on 1 September 2026 as 86b50fb, hardens the destructive fallback used for this exact orphan-divergence case:
- Detect before reset:
hermes updateprobesgit merge-base HEAD origin/<branch>before the same-branchreset --hardfallback. Ordinary divergence with a common ancestor keeps the previous behavior. - Park the pre-pull graph: when no common ancestor exists and a pre-pull SHA is available, Hermes writes
refs/hermes-update-backups/orphan-<branch>-<utc-ts>-<sha12>withgit update-refbefore resetting. The command result is checked, so the CLI does not claim a recoverable backup when the ref write failed. - Bound retained history: every orphan incident prunes rescue refs to at most 10 and expires parseable refs older than 30 days; unparseable names are not age-guessed. The user-facing recovery notice states the expiry window.
- Make disk cost explicit: a rescue ref pins every object reachable from the parked commit. The PR’s worst-case measurement with a 2 GB incompressible snapshot retained about 2.15 GB in
.gitafter aggressive GC, falling to about 0.03 MB after the ref and reflog were removed and objects reclaimed. Those numbers are upstream source-pinned measurements, not a general storage guarantee.
The correction deliberately preserves the existing reset after attempting the rescue; it creates a bounded recovery handle instead of redefining update/reconciliation semantics. The PR limits this guard to the same-branch reset arm because the custom-branch path merges instead of performing that destructive reset.
For harness operations, the durable principle is that automatic self-update may need destructive reconciliation, but destruction should be preceded by an explicit, bounded recovery anchor whose existence is reported truthfully. This is Hermes installer/updater behavior after stable v0.21.0; it is not a UHP lifecycle, persistence, checkpoint, rollback or deployment primitive, does not change MCP/ACP semantics, and does not establish native Hermes UHP adoption.
Historical post-v0.21.0 evidence: gateway state.db writers converge on one process-wide registry
Section titled “Historical post-v0.21.0 evidence: gateway state.db writers converge on one process-wide registry”Hermes P1 issue #90837 records 11 state.db corruption incidents between 2 and 20 August 2026 and an investigation that progressively eliminated external-writer and several other suspected causes. The final reported incidents occurred with a gateway-only writer set. The issue remains open, so this guide does not treat the merged mitigation below as proof that every residual corruption signature has been completely explained or eliminated.
PR #100201, merged on 1 September 2026 as db339f00, consolidates the long-lived in-process database writer topology that remained inside the gateway:
- One process-wide writer authority per path: a new refcounted registry owns a shared
SessionDBfor each resolvedstate.dbpath instead of allowing gateway call sites to independently mint writer connections. The PR records 4–6 simultaneous writers before the change and exactly one read/write descriptor after it in itslsofprobe. - Long-lived callers converge: gateway session/store and runner paths, recall, cron, mirror, channel-directory, slash-command and shutdown paths, TUI paths, plus in-process tools including session search, reactions, delegation and
mcp_serve, now acquire through the shared registry. CLI one-shots, recovery flows and read-only cross-profile opens deliberately keep directSessionDB()ownership. - Replacement remains generation-aware: an inode change retires the live registry generation so it is not handed to new callers while existing holders can drain it. A failed replacement open leaves no registry entry behind, and release is keyed to the actual shared object rather than just a pathname.
- Checkpoint/teardown ownership is narrowed: shared
close()releases one holder instead of immediately tearing down the underlying writer; final teardown/checkpoint work happens on final release or gateway shutdown and runs outside the registry lock. This removes the earlier topology where one caller’s close-time checkpoint could race another live writer’s WAL growth.
The upstream PR reports 8/8 registry lifecycle regressions passing and its targeted hermes_state, gateway, cron, session-search, state-db and run-agent suites green apart from pre-existing test_monitor_kind configuration-drift failures reproduced on clean main. These are source-pinned implementation/test results rather than an independent production certification. Issue #90837 explicitly remains open for residual signatures and the longer DELETE-vs-WAL investigation, so the site classifies this as a concrete writer-ownership and checkpoint-race mitigation, not a blanket “corruption solved” claim.
For harness durability, this composes with the earlier live-database replacement fence: state-store identity needs generation fencing, while process-local mutation needs one authoritative writer lifecycle. Path/profile ownership alone is insufficient if several long-lived connections can independently checkpoint and close the same WAL-backed state database.
This is Hermes-internal SQLite/session-persistence behavior after stable v0.21.0. It is not a UHP storage, session, recovery or concurrency primitive, does not change MCP/ACP wire semantics, does not establish native Hermes UHP adoption, and does not show that HarnessRouter’s released Hermes backend exposes or independently revalidates this internal state-store path.
Historical post-v0.21.0 evidence: scheduled turns survive managed gateway restart
Section titled “Historical post-v0.21.0 evidence: scheduled turns survive managed gateway restart”Hermes PR #101940, merged on 3 September 2026 as c3e9b28a, closes a P1 execution/delivery gap in systemd-managed Linux gateways. A forced systemctl restart or hermes gateway restart could kill an in-flight cron agent turn even when the child used start_new_session, because a new process session does not move that child out of the gateway’s systemd service cgroup.
The reviewed implementation hands a managed-gateway cron fire to a detached systemd-run --scope worker. Before model or tool side effects, that worker adopts the durable execution row through an ownership compare-and-swap from claimed plus handoff_pending to running. The parent does not re-run the job after an uncertain handoff, fencing restart recovery against duplicate side effects.
Final delivery uses a separate certainty boundary through a profile-local durable queue:
- an unclaimed
pendingrow is definitely unsent, so it stays queued across a gateway outage — including beyond the worker’s 300-second wait budget — for the next gateway to drain; - a row whose claimant dies during transport becomes
unknownand is not automatically replayed because the send outcome is uncertain; and - worker queueing is keyed to the delivering job’s own
execution_id, so a nested in-processhermes cron run <other>cannot be queued under the outer attempt and silently dropped by idempotent insertion.
The same restart-safe scope wrapper is applied to Kanban workers. Outside the managed systemd topology the wrapper is pass-through; the PR explicitly records macOS, Windows, launchd and Docker gateways, plus direct cron run CLI behavior, as unchanged.
Upstream reports an end-to-end case where the parent gateway is SIGKILLed while the worker is running and the job survives with its side effect and delivery occurring once. Its validation records 1,228 relevant tests passing, with five test_monitor_kind failures reproduced as pre-existing environment failures on main; ruff is clean and ty reports no new diagnostics. Those are upstream implementation/test results rather than independent production certification.
For harness design, the reusable invariant is that durable execution ownership must cross supervisor lifetime before side effects, and delivery recovery must preserve the difference between definitely unsent and outcome-uncertain work. Collapsing both into “failed” either loses deliverable work or makes replay risk duplication.
The v0.21.2 tag is downstream of this commit, so this behavior is now released in stable v0.21.2. It is not a UHP cron, session or cancellation primitive; it does not change MCP/ACP semantics, establish native Hermes UHP adoption, or show that HarnessRouter invokes or independently validates this internal systemd cron path.
Included in v0.21.2: relay egress authorizes destinations and preserves authorization refusals
Section titled “Included in v0.21.2: relay egress authorizes destinations and preserves authorization refusals”Hermes PR #99220, merged on 8 September 2026 as commit 866332bfb52c46e543143b2620a9aeee8bce9c77, closes an agent-side relay trust gap in send_message. The model-facing tool can name a destination as platform:chat_id; before this change the relay path authenticated the sender but did not independently establish that the named destination was one this gateway was authorized to address.
The merged boundary has two parts:
- Destination authorization: before relay-routed
sendorreact, Hermes checks the destination against provenance the gateway actually holds, including the channel directory and its own session origins. An unattested target is refused before the outbound frame is emitted. The implementation separately handles thread identity and keeps the Telegram@handlerelay-only resolution case bounded to the connector that actually owns the bot credential/resolution step rather than turning it into a generic exemption. - Refusal settlement: an authorization refusal is not treated as ordinary lane unavailability. A declined draft, edit, prompt, media, task-card or related content path must not be “recovered” by sending the same content through a different operation to the same chat. The merged PR carries structured refusal information through the relevant callers and makes those declines terminal for the affected turn-scoped fallback paths.
The PR’s final description records 511 focused tests and a mutation ledger used to verify the guard and decline-handling boundaries. It also explicitly says that the experimental mutable per-chat terminal-decline latch was removed from this PR and moved to a separate branch for redesign; the shipped control is the destination guard plus the turn/per-site refusal handling, not that deferred latch. Those are upstream source-pinned test/review claims rather than an independent security certification.
Immediately before that merge, commit fef0e16fe19b79ded929209f87c7434270b03825 also tightened relay authentication recovery. Hermes had been treating a post-handshake WebSocket close code 4401 as credential revocation even though the connector can use the same code for an expired short-lived upgrade token. The path now gives the first such failure one fresh-token redial before latching revocation, while an explicitly expired reason stays on the normal reconnect path. This prevents an expired token after scale-to-zero/resume from being misclassified as a permanent credential revocation without weakening the second-strike revocation behavior.
These Hermes relay/messaging authorization and liveness semantics are included in stable v0.21.2 and therefore also in the current v0.21.5 release line. They are not UHP authorization, messaging or transport primitives, do not change UHP conformance, do not establish native Hermes UHP adoption, and do not prove that HarnessRouter’s UHP→Hermes adapter invokes or independently revalidates these gateway relay paths.
No separate upstream Hermes UHP announcement was identified in the 24 September 2026 maintenance pass, so native-adoption status remains Not established.
What is not proven
Section titled “What is not proven”Why the distinction matters
Section titled “Why the distinction matters”A reference implementation can adapt an existing harness without that harness vendor changing its own API. Native adoption would be a stronger ecosystem signal: it would mean a vendor, tool or independent runtime exposes or consumes UHP directly, reducing dependence on one adapter implementation.
Current developer implication
Section titled “Current developer implication”If you are evaluating UHP today, test the behavior you need through the official conformance suite and your target backend. Do not assume that every backend feature maps perfectly just because the common API exists. The protocol deliberately defines common semantics, while backend-specific capabilities may continue to differ.
Related pages
Section titled “Related pages”Read harness composition, UHP vs ACP, UHP architecture, conformance, and the adoption tracker.
Primary sources
Section titled “Primary sources”- Hermes current upstream repository
- Hermes
v2026.9.24/ v0.21.5 release - Hermes v0.21.5 tagged commit —
f97608f1 - Hermes
v2026.9.21/ v0.21.4 release - Hermes v0.21.4 tagged commit —
d337b736 - Hermes
v2026.9.14/ v0.21.3 release - Hermes v0.21.3 tagged commit —
345cd2b0 - Hermes
v2026.9.11/ v0.21.2 release - Hermes v0.21.2 tagged commit —
939e45c9 - Hermes auxiliary capability-probe cache issue #87654
- Hermes probe-mode runtime-cache isolation —
a714ff98 - Hermes wrapped-probe cache regression —
093c58ca - Hermes rotating-refresh replay issue #55712
- Hermes native refresh single-flight —
f561155a - Hermes unified dashboard refresh single-flight / off-loop fix —
5dea46d1 - Hermes live
state.dbWAL-generation P0 issue #109687 - Hermes
state.dblock-loss P0 issue #109786 - Hermes path-based live-db permission hardening —
ccd360e9 - Hermes existing-main-db
O_EXCLfollow-up —e16f6867 - Hermes gateway
agent_loop_stoppedhook —f361971e - Hermes TUI/Desktop interruption parity —
d3202bbc - Hermes permanent allowlist reconciliation —
9b06d3d0 - Hermes permanent allowlist replacement reload —
9db604ce - Hermes direct-child list reconciliation —
41380cce - Hermes OpenRouter OAuth PKCE —
0007a4c2 - Hermes
v2026.9.7/ v0.21.1 release - Hermes v0.21.1 tagged commit —
2237be35 - Hermes relay destination-authorization / refusal-settlement PR #99220
- Hermes relay destination-authorization merge —
866332bf - Hermes fresh-token 4401 recovery PR #102602
- Hermes fresh-token 4401 recovery merge —
fef0e16f - Hermes
v2026.8.31/ v0.21.0 release - Hermes v0.21.0 release PR #99718
- Hermes v0.21.0 tagged commit —
29112bef - Hermes restart-safe cron worker PR #101940
- Hermes restart-safe cron merge —
c3e9b28a - Hermes post-release Codex auxiliary FD-ownership PR #99709
- Hermes post-release FD-ownership merge —
8a766c3 - Hermes live-state-database replacement issue #89332
- Hermes state-database generation/fail-stop guard —
71256dfd - Hermes state-database identity follow-up —
8dbf07e9 - Hermes recurring
state.dbcorruption tracker #90837 - Hermes shared
SessionDBwriter-registry PR #100201 - Hermes shared writer-registry merge —
db339f00 - Hermes multiplexed profile-storage issue #66887
- Hermes session-store owner resolution —
5ffaed6e - Hermes late-profile resolution follow-up —
58dcc100 - Hermes process-wide routing-index/restart-recovery fix —
8d74cb52 - Hermes Desktop
/stopactive-turn interrupt —e0717231 - Hermes Stop recovery-marker retirement —
5c3bfb6d - Hermes Stop-vs-marker recovery regressions —
1b472770 - Hermes profile-archive credential exposure issue #92457
- Hermes profile-export recurrence guard PR #100101
- Hermes profile-export final hardening —
80146676 - Hermes orphan-update/autostash issue #87694
- Hermes bounded orphan-update rescue PR #100155
- Hermes bounded orphan-update rescue merge —
86b50fb - Hermes blocking MCP-shutdown issue #82874
- Hermes bounded off-loop MCP shutdown —
11ba76c - Hermes Buzz authentication/profile isolation PR #99427
- Hermes Buzz threading/streaming PR #99429
- Hermes Buzz dispatch/addressing PR #99431
- Hermes Buzz native media pipeline PR #99432
- Hermes Docker secret-in-argv issue #96268
- Hermes Docker secret-transport PR #99637
- Hermes Docker name-only env forwarding fix —
d10ef89 - Docker
--env KEYhost-environment behavior - Hermes direct-alias cross-provider credential leak issue #83612
- Hermes direct-alias origin-bound credential fix —
3145986 - Hermes Desktop env-backed-key issue #88990
- Hermes initial env-backed provider-key mirror fix —
a90be562 - Hermes credential-pointer carry follow-up —
8fd144c - Hermes
.envwriter/reader parity PR #67488 - Hermes spaced
.envassignment lifecycle fix —1152d4d3 - Hermes keyed-provider credential scrub —
22f9caf - Hermes Desktop credential-pool materialization —
82733a3 - Hermes profile-owned session row derivation —
5cc3da6 - Hermes profile-scoped Desktop session mutations —
18f429a - Hermes cron profile secret scope through delivery —
07f6518 - Hermes per-profile multiplex cron adapters —
51e377d - Hermes satellite-profile delivery preflight routing —
d5d0613 - Hermes profile-scoped cron status and heartbeat guard —
82d7a13 - Hermes hosted-room authority/replay PR #99007
- Hermes hosted-room replication/takeover PR #99047
- Hermes same-gateway hosted-room driver PR #99099
- Hermes cross-gateway hosted-room transport PR #99244
- Hermes cross-gateway transport implementation —
e743391 - Hermes grant-refresh drift regression/current merge —
1cf3639 - Hermes delegation follow-up PR #99098
- Hermes classified delegation failure propagation —
1b6ea1a - Hermes delegation failure-label issue #97655
- Hermes delegation failure-semantics commit —
ec02d51 - Hermes delegation model-rejection visibility issue #97654
- Hermes config-level model-rejection notice —
c05d04f - Hermes delegation status follow-up/test evidence —
b4d5174 - Hermes single-request lean-compaction PR #98628
- Hermes single-request lean-compaction merge —
4f22543 - Hermes unattended-approval PR #98558
- Hermes unattended-approval final policy commit —
ef71f2c - Hermes live compaction-routing PR #98547
- Hermes native-compaction hot-apply commit —
77f5de6 - Hermes compaction-routing cache-signature follow-up —
66666f6 - Hermes compaction prompt-refresh PR #98426
- Hermes compaction prompt-refresh merge —
514707f - Hermes revisioned live task-state commit —
393af4a - Hermes resume snapshot derivation follow-up —
c0875ba - Hermes desktop todo patch merge fix —
cf69253 - Hermes shared ACP/OpenAI bridge commit —
083c592 - Hermes generic
acp://runtime handling —613164d - Hermes agent-as-provider transcript projection —
07200e9 - HarnessRouter
v0.9.0skill-loader placement fix - HarnessRouter Hermes loopback relay commit (issue #12)
- HarnessRouter Community Edition repository
- UHP architecture specification