Hermes Agent
Hermes finds invocation identity leaking through shared terminal snapshots
A Hermes Agent report shows delegated-child metadata being serialized into a shared terminal snapshot and restored for later parent calls. Companion patches expose the same authority drift in desktop message state and multiplex cron delivery, where the visible profile can diverge from the state or adapter actually used.
NousResearch/hermes-agent issue #71941 is the inspected primary source: “[Bug]: Delegated child context persists through shared terminal snapshots.” The related records below were inspected as supporting context rather than independent confirmation.
The facts
- NousResearch/hermes-agent issue #71941 was created 2026-07-26T12:03:30Z and was open at inspection; its title is “[Bug]: Delegated child context persists through shared terminal snapshots.” - NousResearch/hermes-agent PR #73608 was created 2026-07-28T19:29:35Z and was open at inspection; its title is “fix(desktop): read session messages from $sessionStates, not stale global $messages.” - NousResearch/hermes-agent PR #73363 was created 2026-07-28T13:15:28Z and was open at inspection; its title is “fix(cron): deliver each profile's cron via its own adapter in multiplex.” - The repository reported 226075 stars, 28408 open issues, default branch main, and last push 2026-08-06T00:26:31Z. - The latest tagged-release baseline checked was v2026.8.3 published 2026-08-03T16:57:52Z.
What changed
NousResearch/hermes-agent issue #71941 reports: Bug Description HERMES_DELEGATED_CHILD_CONTEXT is intended to describe the currently executing subprocess lineage: terminal commands invoked by a delegate_task child should see it, while subsequent ordinary agent invocations should not. However, terminal environments are cached and shared between top-level agents and delegated children. The local terminal backend uses an export -p snapshot to preserve shell state across fresh Bash processes. When a delegated child executes a terminal command, its inherited HERMES_DELEGATED_CHILD_CONTEXT=1 value is serialized into that shared snapshot. A later non-delegated terminal call sources the same snapshot and incorrectly recovers the marker. The stale value is then written back into the snapshot, making the contamination persistent for the lifetime of the cached terminal environment. This can cause unrelated later invocations to be treated as delegated children. In particular, Kanban mutation guards that consult this marker may deny otherwise legitimate non-delegated operations. The parent/child sharing of the execution environment appears intentional. The
NousResearch/hermes-agent PR #73608 reports: Closes #68734 Problem The desktop refresh/regenerate button, restore-checkpoint action, and message-edit buttons silently fail when a session has an active runtime ID. /retry works because it's backend-executed and bypasses planReload entirely. Root cause planReload, planRestore, and planEdit in usePromptActions all read $messages.get() — the global draft atom (@/store/session). But active sessions store messages in $sessionStates[runtimeId].messages (@/store/session-states). The global atom is empty ([]) for sessions with a runtime ID, so messages.findIndex(m => m.id === parentId) returns -1 and the function returns null — silent failure. The dual-store architecture The ChatRuntimeBoundary feeds assistant-ui from the session slice (correct), but the action callbacks read from the global atom (stale). In usePromptActions, sessionId refers to the runtime session ID — the same key used to index $sessionStates. Fix Read from $sessionStates[sessionId]?.messages when a session ID exists, falling back to $messages.get() for draft sessions: Applied to 3 call sites: reloadFromMessage — refresh/regenerate button restoreToMessage —
NousResearch/hermes-agent PR #73363 reports: Summary In multiplex_profiles mode the in-process cron ticker scopes **execution** per profile (HERMES_HOME, cron store, heartbeat, recovery) but delivered **every** profile's cron output through the **default** profile's platform adapter. So a secondary profile's scheduled-job replies / failure summaries were sent by the *default* bot instead of that profile's own bot (e.g. one Telegram bot per profile). Root cause cron/scheduler_provider.py::InProcessCronScheduler._start_multiplex iterates each served profile and calls cron_tick with the single default-profile adapters set for all profiles: adapters originates from runner.adapters (the default/primary profile) in gateway/run.py, while each secondary profile's real adapters live in GatewayRunner._profile_adapters[name]. Execution was correctly scoped by a61183b, but delivery was not. Fix Thread a profile_adapters map (profile name -> adapters) from the gateway into InProcessCronScheduler.start, and select the per-profile adapter set in the multiplex tick loop, falling back to the shared adapters for the default profile (which owns them). Single-profile path is unchanged; the new parameter defaults to
Why it matters
Invocation metadata becoming durable shell state can deny legitimate parent work or misattribute later operations. The same pattern appears when desktop actions read a draft store for an active session and cron replies use the default bot for another profile. The operator test is whether persisted state, execution authority and the visible outcome describe the same event after retries, restarts or delegation.
Current
The primary record was open when captured on 2026-08-06. The release baseline was v2026.8.3 published 2026-08-03T16:57:52Z. Repository metadata, pull requests, issues, release records, Google News discovery, Hacker News discovery, Lobsters, Metamesh and the rendered ClawCharts row were checked. ClawCharts selected the subject; it did not prove the claim.
Evidence
The primary URL is https://github.com/NousResearch/hermes-agent/issues/71941. Supporting records are https://github.com/NousResearch/hermes-agent/pull/73608 and https://github.com/NousResearch/hermes-agent/pull/73363. Source bodies, timestamps and states are preserved in the daily evidence bundle. Test counts, reproductions and deployment observations remain attributed to their authors unless explicitly identified as independently rerun.
Source boundary
Open work is described as open, closed work as closed, and operator reports as reports. A pull request is evidence of proposed or reviewed direction, not proof of a shipped release. Search residue, package mirrors and historically published source spines were excluded.
Operator take
Exclude invocation-scoped variables from reusable snapshots, make active session state authoritative, and route delivery through the profile that owns execution. Preserve a before-state receipt, make the smallest reversible change, and verify the original failure independently after intervention.
Caveat
Public project records are mutable. Status, scope and evidence can change after publication. This brief records the inspected state and does not authorize changes to a reader’s deployment.
Source inspected; source state, environment and release boundary remain explicit.