Conversation
A plugin-registered slash command cannot see the origin it was invoked from. `_handle_message` calls `reset_session_vars()` at handler entry so a task that inherited a concurrent sibling's ContextVars starts clean, and binds this turn's identity via `_set_session_env` only much later, on the agent path. The plugin branch sits between the two and dispatches `plugin_handler(user_args)` — a positional string, no `event`, no `source` — so every `HERMES_SESSION_*` ContextVar stays unset for the whole handler. Nothing in the product mirrors platform/chat/user/key into `os.environ` any more (the ContextVar migration removed those writes, and the gateway deliberately stopped mirroring `HERMES_SESSION_KEY`, NousResearch#24100), so `get_session_env` resolves through to the `""` default: the handler, and every Hermes internal it calls that reads the ambient context — `tools/send_message_tool.py` for delivery platform/user, `tools/cronjob_tools.py` for the origin stamped onto a job, kanban and approval — sees no origin at all. Reproducible symptom on current main: a plugin command that sends a message or schedules a cron job stamps an empty platform/chat/user. Where a host or wrapper does export those vars, the same read returns a foreign, last-writer-wins identity instead. Extract the field mapping out of `_set_session_env` into `_set_session_env_from_source(source, session_key, session_id="")` and have `_set_session_env` delegate to it (no behaviour change; it was already reading nothing but `context.source` and `context.session_key`, and the default keeps the agent path binding exactly what it bound before — `agent_init` repopulates the session id there a moment later), then bind that around plugin dispatch using the `_quick_key` already in scope, clearing it in a `finally`. `HERMES_SESSION_ID` is the one session var still mirrored process-globally, so the plugin path resolves this key's real id through the store's public lock-held `peek_session_id` accessor rather than binding `""` over it; `_peek_bound_session_id` is best-effort and never raises, so stores that predate the accessor (or fail on it) still get the rest of the origin bound. The handler signature stays `fn(raw_args: str) -> str | None`: async handlers keep working, a falsy result still suppresses the echo, and a raising handler still falls through to skill resolution as before — only now without leaving the binding behind. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VVwpfipYf3kHMKRRber2ec
7 of 8 tasks
This was referenced Sep 12, 2026
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Bind the existing
HERMES_SESSION_*ContextVars while a messaging gateway plugin slash-command handler runs.Plugin handlers currently execute before the agent path binds session context. A handler itself—or Hermes code it calls, such as message delivery, cron scheduling, kanban, or approvals—therefore sees an empty platform/chat/thread origin.
HERMES_SESSION_IDmay also retain a process-level value unrelated to the current event.Fix
finally, including when the handler raises.The handler API remains unchanged, so existing plugins benefit without opting into a new argument or registration mode.
Scope and related work
This PR covers
GatewayRunner._handle_message, the messaging gateway path where the bug was reproduced. TUI plugin dispatch uses separate host helpers and call sites and should be handled separately; the single-session CLI continues to use its process environment as the authoritative context.This is complementary to #51596 and #56782. Those PRs propose an explicit context argument for plugin handlers; they do not bind the existing ambient ContextVars read by Hermes internals and existing handlers.
Tests
git diff --check: clean.