Skip to content

fix(gateway): bind session context for plugin slash commands - #82776

Open
dmmeteo wants to merge 1 commit into
NousResearch:mainfrom
dmmeteo:fix/plugin-command-session-context
Open

dmmeteo wants to merge 1 commit into
NousResearch:mainfrom
dmmeteo:fix/plugin-command-session-context

Conversation

@dmmeteo

@dmmeteo dmmeteo commented Aug 9, 2026

Copy link
Copy Markdown

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_ID may also retain a process-level value unrelated to the current event.

Fix

  • Resolve the current persisted session ID from the event's session key.
  • Bind the event source and session ID immediately before invoking the plugin handler.
  • Clear the binding in finally, including when the handler raises.
  • Keep the normal agent initialization path unchanged.

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

  • Added real gateway dispatch tests for platform, chat, thread, user, session key, and persisted session ID.
  • Covered sequential events, handler exceptions, no persisted session, unavailable store access, and the unchanged agent path.
  • Focused suite: 80 passed.
  • Ruff and git diff --check: clean.

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
@alt-glitch alt-glitch added type/bug Something isn't working comp/gateway Gateway runner, session dispatch, delivery area/sessions Session lifecycle, resume, persistence, history P2 Medium — degraded but workaround exists sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Aug 9, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/sessions Session lifecycle, resume, persistence, history comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants