Skip to content

feat(mcp): Native MCP client with HTTP transport, reconnection, and security - #301

Merged
teknium1 merged 13 commits into
mainfrom
feat/mcp-support
Mar 3, 2026
Merged

feat(mcp): Native MCP client with HTTP transport, reconnection, and security#301
teknium1 merged 13 commits into
mainfrom
feat/mcp-support

Conversation

@teknium1

@teknium1 teknium1 commented Mar 3, 2026

Copy link
Copy Markdown
Contributor

MCP (Model Context Protocol) Client Support

Overview

Adds comprehensive native MCP client support to Hermes Agent. Users connect to any MCP-compatible server by adding a few lines to config.yaml — tools, resources, and prompts are automatically discovered and available to the agent.

Based on PR #291 by @0xbyt4 (merged with contributor credit preserved).

Quick Start

# ~/.hermes/config.yaml
mcp_servers:
  time:
    command: uvx
    args: ["mcp-server-time"]
  notion:
    url: https://mcp.notion.com/mcp
  github:
    command: npx
    args: ["-y", "@modelcontextprotocol/server-github"]
    env:
      GITHUB_PERSONAL_ACCESS_TOKEN: "ghp_..."

Install: pip install hermes-agent[mcp]

Features

Core:

  • Stdio transport — spawn MCP servers as subprocesses (command + args)
  • HTTP/Streamable HTTP transport — connect to remote servers (url + headers)
  • Auto-discovery — tools, resources, and prompts registered on startup
  • Resources & Promptslist_resources, read_resource, list_prompts, get_prompt tools per server
  • Parallel discovery — all servers connect simultaneously via asyncio.gather
  • Graceful degradation — works without mcp package (just skips MCP)

Robustness:

  • Reconnection — exponential backoff (1s→60s, 5 retries) on connection loss
  • Env var filtering — only safe vars + user-specified env passed to subprocesses
  • Credential stripping — tokens/keys redacted from error messages to LLM
  • Config validation — warns on conflicting url + command in same server
  • Configurable timeouts — per-server timeout and connect_timeout
  • Clean shutdown — all subprocesses terminated on exit, no orphans

UX:

  • Banner integration — MCP Servers section in CLI startup banner (transport, tool count, status)
  • /reload-mcp command — disconnect, re-read config, reconnect (CLI + gateway). Add/remove servers without restarting
  • Summary line — banner shows N tools · N skills · N MCP servers

User Prerequisites

Server Type User Needs Per-Server Install?
HTTP/remote (url) Nothing No — just a URL
npm stdio (npx) Node.js No — npx auto-downloads
Python stdio (uvx) uv No — uvx auto-downloads

Testing

  • 74 MCP-specific tests covering: config loading, schema conversion, tool/resource/prompt handlers, server lifecycle, toolset injection, graceful fallback, shutdown, env filtering, credential sanitization, HTTP config, reconnection, timeouts, utility tool schemas/registration
  • Full suite: 1186 passed, 0 failed

Files Changed (15 files, +3,705 / -27)

File Type Description
tools/mcp_tool.py New MCP client implementation (1,047 lines)
tests/tools/test_mcp_tool.py New 74 unit tests (1,491 lines)
docs/mcp.md New Full documentation (527 lines)
skills/mcp/native-mcp/SKILL.md New Native MCP skill (330 lines)
hermes_cli/banner.py Modified MCP Servers section in startup banner
cli.py Modified /reload-mcp command + shutdown hook
gateway/run.py Modified /reload-mcp command + shutdown hook
cli-config.yaml.example Modified MCP config section with examples
README.md Modified MCP feature section
TODO.md Modified Mark MCP as implemented
docs/tools.md Modified MCP in Tool Categories + section
skills/mcp/DESCRIPTION.md Modified Updated category description
model_tools.py Modified discover_mcp_tools() hook
pyproject.toml Modified mcp>=1.2.0 optional dependency
uv.lock Modified Lock file updated

Future Work

  • hermes mcp CLI subcommand (list/test/status)
  • hermes tools UI integration for MCP toolsets
  • OAuth authentication for remote servers
  • Progress notifications for long-running MCP tools
  • Tool search/filtering for very large tool sets (100+)

0xbyt4 and others added 9 commits March 2, 2026 21:03
Connect to external MCP servers via stdio transport, discover their tools
at startup, and register them into the hermes-agent tool registry.

- New tools/mcp_tool.py: config loading, server connection via background
  event loop, tool handler factories, discovery, and graceful shutdown
- model_tools.py: trigger MCP discovery after built-in tool imports
- cli.py: call shutdown_mcp_servers in _run_cleanup
- pyproject.toml: add mcp>=1.2.0 as optional dependency
- 27 unit tests covering config, schema conversion, handlers, registration,
  SDK interaction, toolset injection, graceful fallback, and shutdown

Config format (in ~/.hermes/config.yaml):
  mcp_servers:
    filesystem:
      command: "npx"
      args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
Ensures MCP subprocess connections are closed when the messaging
gateway shuts down, preventing orphan processes.
Refactor MCP connections from AsyncExitStack to task-per-server
architecture. Each server now runs as a long-lived asyncio Task
with `async with stdio_client(...)`, ensuring anyio cancel-scope
cleanup happens in the same Task that opened the connection.
When discover_mcp_tools() is called multiple times (e.g. direct call
then model_tools import), return existing tool names instead of opening
new connections that would orphan the previous ones.
Patch _servers to empty dict in tests that call discover_mcp_tools()
with mocked config, preventing interference from real MCP connections
that may exist when running within the full test suite.
- Add threading.Lock protecting all shared state (_servers, _mcp_loop, _mcp_thread)
- Fix deadlock in shutdown_mcp_servers: _stop_mcp_loop was called inside
  a _lock block but also acquires _lock (non-reentrant)
- Fix race condition in _ensure_mcp_loop with concurrent callers
- Change idempotency to per-server (retry failed servers, skip connected)
- Dynamic toolset injection via startswith("hermes-") instead of hardcoded list
- Parallel shutdown via asyncio.gather instead of sequential loop
- Add tests for partial failure retry, parallel shutdown, dynamic injection
Authored by 0xbyt4. Adds MCP client with official SDK, direct tool registration,
auto-injection into hermes-* toolsets, and graceful degradation.
Upgrades the MCP client implementation from PR #291 with:

- HTTP/Streamable HTTP transport: support 'url' key in config for remote
  MCP servers (Notion, Slack, Sentry, Supabase, etc.)
- Automatic reconnection with exponential backoff (1s-60s, 5 retries)
  when a server connection drops unexpectedly
- Environment variable filtering: only pass safe vars (PATH, HOME, etc.)
  plus user-specified env to stdio subprocesses (prevents secret leaks)
- Credential stripping: sanitize error messages before returning to the
  LLM (strips GitHub PATs, OpenAI keys, Bearer tokens, etc.)
- Configurable per-server timeouts: 'timeout' and 'connect_timeout' keys
- Fix shutdown race condition in servers_snapshot variable scoping

Test coverage: 50 tests (up from 30), including new tests for env
filtering, credential sanitization, HTTP config detection, reconnection
logic, and configurable timeouts.

All 1162 tests pass (1162 passed, 3 skipped, 0 failed).
- docs/mcp.md: Full MCP documentation covering prerequisites, configuration,
  transports (stdio + HTTP), security (env filtering, credential stripping),
  reconnection, troubleshooting, popular servers, and advanced usage
- README.md: Add MCP section with quick config example and install instructions
- cli-config.yaml.example: Add commented mcp_servers section with examples
  for stdio, HTTP, and authenticated server configs
- docs/tools.md: Add MCP to Tool Categories table and MCP Tools section
- skills/mcp/native-mcp/SKILL.md: Create native MCP client skill with
  full configuration reference, transport types, security, troubleshooting
- skills/mcp/DESCRIPTION.md: Update category description to cover both
  native MCP client and mcporter bridge approaches
teknium1 added 4 commits March 2, 2026 19:02
- Discovery is now parallel (asyncio.gather) instead of sequential,
  fixing the 60s shared timeout issue with multiple servers
- Startup messages use print() so users see connection status even
  with default log levels (the 'tools' logger is set to ERROR)
- Summary line shows total tools and failed servers count
- Validate conflicting config: warn if both 'url' and 'command' are
  present (HTTP takes precedence)
- Update TODO.md: mark MCP as implemented, list remaining work
- Add test for conflicting config detection (51 tests total)

All 1163 tests pass.
Banner integration:
- MCP Servers section in CLI startup banner between Tools and Skills
- Shows each server with transport type, tool count, connection status
- Failed servers shown in red; section hidden when no MCP configured
- Summary line includes MCP server count
- Removed raw print() calls from discovery (banner handles display)

/reload-mcp command:
- New slash command in both CLI and gateway
- Disconnects all MCP servers, re-reads config.yaml, reconnects
- Reports what changed (added/removed/reconnected servers)
- Allows adding/removing MCP servers without restarting

Resources & Prompts support:
- 4 utility tools registered per server: list_resources, read_resource,
  list_prompts, get_prompt
- Exposes MCP Resources (data sources) and Prompts (templates) as tools
- Proper parameter schemas (uri for read_resource, name for get_prompt)
- Handles text and binary resource content
- 23 new tests covering schemas, handlers, and registration

Test coverage: 74 MCP tests total, 1186 tests pass overall.
- CLI: After reload, refreshes self.agent.tools and valid_tool_names
  so the model sees updated tools on its next API call
- Both CLI and Gateway: Appends a [SYSTEM: ...] message at the END
  of conversation history explaining what changed (added/removed/
  reconnected servers, tool count). This preserves prompt-cache for
  the system prompt and earlier messages — only the tail changes.
- Gateway already creates a new AIAgent per message so tools refresh
  naturally; the injected message provides context for the model
After /reload-mcp updates self.agent.tools, immediately call
_persist_session() so the session JSON file at ~/.hermes/sessions/
reflects the new tools list. Without this, the tools field in the
session log would only update on the next conversation turn — if
the user quit after reloading, the log would have stale tools.
@teknium1
teknium1 merged commit 68cc81a into main Mar 3, 2026
@teknium1
teknium1 deleted the feat/mcp-support branch March 3, 2026 05:32
teknium1 added a commit that referenced this pull request Mar 7, 2026
All remaining TODO items have covering issues:
- Local Browser via CDP: #374, #493
- Signal Integration: #405
- Plugin/Extension System: #359
- MCP Client Improvements: #581 (new)
- Filesystem Checkpointing: #452

Completed items (MCP core support) already shipped in PR #301.
angelburgosrosado pushed a commit to angelburgosrosado/hermes-agent that referenced this pull request Apr 27, 2026
feat(mcp): Native MCP client with HTTP transport, reconnection, and security
angelburgosrosado pushed a commit to angelburgosrosado/hermes-agent that referenced this pull request Apr 27, 2026
All remaining TODO items have covering issues:
- Local Browser via CDP: NousResearch#374, NousResearch#493
- Signal Integration: NousResearch#405
- Plugin/Extension System: NousResearch#359
- MCP Client Improvements: NousResearch#581 (new)
- Filesystem Checkpointing: NousResearch#452

Completed items (MCP core support) already shipped in PR NousResearch#301.
02356abc pushed a commit to 02356abc/hermes-agent that referenced this pull request May 14, 2026
All remaining TODO items have covering issues:
- Local Browser via CDP: NousResearch#374, NousResearch#493
- Signal Integration: NousResearch#405
- Plugin/Extension System: NousResearch#359
- MCP Client Improvements: NousResearch#581 (new)
- Filesystem Checkpointing: NousResearch#452

Completed items (MCP core support) already shipped in PR NousResearch#301.
cpmidnite pushed a commit to cpmidnite/hermes-agent that referenced this pull request Jun 3, 2026
cpmidnite pushed a commit to cpmidnite/hermes-agent that referenced this pull request Jun 3, 2026
…size storms (React NousResearch#301)

The /chat dashboard runs the Ink TUI bundle through a browser-side xterm.js
PTY. The fit-addon emits rapid resize bursts (delivered as stdout.columns
writes over the WS RESIZE escape), each invalidating the Yoga height cache
mid-convergence. The useVirtualHistory layout effect re-measures on every
height change (measuredHeightVersion is its own dep), so under the storm
heights never settle within React's 25-rerender budget → NousResearch#301.

Cap synchronous re-measure bumps (MAX_SYNC_MEASURE_BUMPS=8); past the cap,
defer the bump via setTimeout(0) so React commits the frame and the storm
settles before re-measuring. Budget resets on real layout changes (column
resize) and when heights stabilize. The prior cmux fix debounced the cols
SOURCE; this caps the convergence SINK, covering the xterm cadence that
slipped through.

Rebuilt dist/entry.js. 21/22 measurement tests pass (the 1 fail is a
pre-existing estimatedMsgHeight width assertion, unrelated).
cpmidnite pushed a commit to cpmidnite/hermes-agent that referenced this pull request Jun 3, 2026
The overlay already disables HERMES_TUI_GATEWAY_URL for browser embedded chat.
Document that the same sidecar attach also presents as Minified React NousResearch#301 in
addition to the older gateway-exited symptom.
kshitijk4poor added a commit that referenced this pull request Jun 28, 2026
…36658)

Dashboard /chat spawns the TUI attached to the dashboard's in-memory
gateway via HERMES_TUI_GATEWAY_URL. In that attach mode the already-running
gateway replays `gateway.ready` (and `session.info`) the instant the socket
connects, so those events land in GatewayClient.bufferedEvents *before* the
consumer's mount-time subscribe effect (useMainApp.ts) calls drain().

drain() then emitted the buffered events synchronously, so the
`gateway.ready` handler's patchUiState / setHistoryItems cascade ran while
React was still inside the first commit — tripping "Too many re-renders"
(Minified React error #301) and breaking Dashboard chat after `hermes update`.
Spawn / inline / sidecar modes never hit this: their `gateway.ready` only
arrives after the Python child boots, on a later async tick.

Fix: drain() defers the replay to the next microtask AND keeps `subscribed`
false until that microtask runs. Keeping `subscribed` false in the gap means
any live event arriving before the flush keeps buffering (publish() pushes
when !subscribed) instead of emitting synchronously and jumping ahead of the
chronologically-earlier replayed events — the flush re-drains the buffer
right after flipping `subscribed`, preserving FIFO order. A drainGeneration
token (bumped in resetStartupState) makes a queued flush a no-op if the
transport was reset/killed in the meantime, avoiding use-after-teardown and
duplicate/reordered exits.

Regression tests: (1) drain() does not dispatch buffered events synchronously;
(2) a live event arriving in the post-drain / pre-microtask window still
delivers BEHIND the earlier-buffered event (FIFO). Both are red against the
old synchronous behavior, green with this fix. Same class of fix as #44528.

Closes #36658
kshitijk4poor added a commit that referenced this pull request Jun 28, 2026
…microtask

fix(tui): defer buffered gateway events to stop dashboard chat #301 (#36658)
pai-scaffolde pushed a commit to pai-scaffolde/hermes-agent that referenced this pull request Jun 28, 2026
…search#301 (NousResearch#36658)

Dashboard /chat spawns the TUI attached to the dashboard's in-memory
gateway via HERMES_TUI_GATEWAY_URL. In that attach mode the already-running
gateway replays `gateway.ready` (and `session.info`) the instant the socket
connects, so those events land in GatewayClient.bufferedEvents *before* the
consumer's mount-time subscribe effect (useMainApp.ts) calls drain().

drain() then emitted the buffered events synchronously, so the
`gateway.ready` handler's patchUiState / setHistoryItems cascade ran while
React was still inside the first commit — tripping "Too many re-renders"
(Minified React error NousResearch#301) and breaking Dashboard chat after `hermes update`.
Spawn / inline / sidecar modes never hit this: their `gateway.ready` only
arrives after the Python child boots, on a later async tick.

Fix: drain() defers the replay to the next microtask AND keeps `subscribed`
false until that microtask runs. Keeping `subscribed` false in the gap means
any live event arriving before the flush keeps buffering (publish() pushes
when !subscribed) instead of emitting synchronously and jumping ahead of the
chronologically-earlier replayed events — the flush re-drains the buffer
right after flipping `subscribed`, preserving FIFO order. A drainGeneration
token (bumped in resetStartupState) makes a queued flush a no-op if the
transport was reset/killed in the meantime, avoiding use-after-teardown and
duplicate/reordered exits.

Regression tests: (1) drain() does not dispatch buffered events synchronously;
(2) a live event arriving in the post-drain / pre-microtask window still
delivers BEHIND the earlier-buffered event (FIFO). Both are red against the
old synchronous behavior, green with this fix. Same class of fix as NousResearch#44528.

Closes NousResearch#36658
waefrebeorn pushed a commit to waefrebeorn/slermes that referenced this pull request Jul 2, 2026
feat(mcp): Native MCP client with HTTP transport, reconnection, and security
waefrebeorn pushed a commit to waefrebeorn/slermes that referenced this pull request Jul 2, 2026
All remaining TODO items have covering issues:
- Local Browser via CDP: NousResearch#374, NousResearch#493
- Signal Integration: NousResearch#405
- Plugin/Extension System: NousResearch#359
- MCP Client Improvements: NousResearch#581 (new)
- Filesystem Checkpointing: NousResearch#452

Completed items (MCP core support) already shipped in PR NousResearch#301.
waefrebeorn pushed a commit to waefrebeorn/slermes that referenced this pull request Jul 2, 2026
…search#301 (NousResearch#36658)

Dashboard /chat spawns the TUI attached to the dashboard's in-memory
gateway via HERMES_TUI_GATEWAY_URL. In that attach mode the already-running
gateway replays `gateway.ready` (and `session.info`) the instant the socket
connects, so those events land in GatewayClient.bufferedEvents *before* the
consumer's mount-time subscribe effect (useMainApp.ts) calls drain().

drain() then emitted the buffered events synchronously, so the
`gateway.ready` handler's patchUiState / setHistoryItems cascade ran while
React was still inside the first commit — tripping "Too many re-renders"
(Minified React error NousResearch#301) and breaking Dashboard chat after `hermes update`.
Spawn / inline / sidecar modes never hit this: their `gateway.ready` only
arrives after the Python child boots, on a later async tick.

Fix: drain() defers the replay to the next microtask AND keeps `subscribed`
false until that microtask runs. Keeping `subscribed` false in the gap means
any live event arriving before the flush keeps buffering (publish() pushes
when !subscribed) instead of emitting synchronously and jumping ahead of the
chronologically-earlier replayed events — the flush re-drains the buffer
right after flipping `subscribed`, preserving FIFO order. A drainGeneration
token (bumped in resetStartupState) makes a queued flush a no-op if the
transport was reset/killed in the meantime, avoiding use-after-teardown and
duplicate/reordered exits.

Regression tests: (1) drain() does not dispatch buffered events synchronously;
(2) a live event arriving in the post-drain / pre-microtask window still
delivers BEHIND the earlier-buffered event (FIFO). Both are red against the
old synchronous behavior, green with this fix. Same class of fix as NousResearch#44528.

Closes NousResearch#36658
waefrebeorn pushed a commit to waefrebeorn/slermes that referenced this pull request Jul 2, 2026
…teway-drain-microtask

fix(tui): defer buffered gateway events to stop dashboard chat NousResearch#301 (NousResearch#36658)
Jasper6439 pushed a commit to Jasper6439/hermes-agent that referenced this pull request Jul 5, 2026
…search#301 (NousResearch#36658)

Dashboard /chat spawns the TUI attached to the dashboard's in-memory
gateway via HERMES_TUI_GATEWAY_URL. In that attach mode the already-running
gateway replays `gateway.ready` (and `session.info`) the instant the socket
connects, so those events land in GatewayClient.bufferedEvents *before* the
consumer's mount-time subscribe effect (useMainApp.ts) calls drain().

drain() then emitted the buffered events synchronously, so the
`gateway.ready` handler's patchUiState / setHistoryItems cascade ran while
React was still inside the first commit — tripping "Too many re-renders"
(Minified React error NousResearch#301) and breaking Dashboard chat after `hermes update`.
Spawn / inline / sidecar modes never hit this: their `gateway.ready` only
arrives after the Python child boots, on a later async tick.

Fix: drain() defers the replay to the next microtask AND keeps `subscribed`
false until that microtask runs. Keeping `subscribed` false in the gap means
any live event arriving before the flush keeps buffering (publish() pushes
when !subscribed) instead of emitting synchronously and jumping ahead of the
chronologically-earlier replayed events — the flush re-drains the buffer
right after flipping `subscribed`, preserving FIFO order. A drainGeneration
token (bumped in resetStartupState) makes a queued flush a no-op if the
transport was reset/killed in the meantime, avoiding use-after-teardown and
duplicate/reordered exits.

Regression tests: (1) drain() does not dispatch buffered events synchronously;
(2) a live event arriving in the post-drain / pre-microtask window still
delivers BEHIND the earlier-buffered event (FIFO). Both are red against the
old synchronous behavior, green with this fix. Same class of fix as NousResearch#44528.

Closes NousResearch#36658
habarmc1223-sudo pushed a commit to habarmc1223-sudo/hermes-agent-fluxmem that referenced this pull request Jul 8, 2026
…search#301 (NousResearch#36658)

Dashboard /chat spawns the TUI attached to the dashboard's in-memory
gateway via HERMES_TUI_GATEWAY_URL. In that attach mode the already-running
gateway replays `gateway.ready` (and `session.info`) the instant the socket
connects, so those events land in GatewayClient.bufferedEvents *before* the
consumer's mount-time subscribe effect (useMainApp.ts) calls drain().

drain() then emitted the buffered events synchronously, so the
`gateway.ready` handler's patchUiState / setHistoryItems cascade ran while
React was still inside the first commit — tripping "Too many re-renders"
(Minified React error NousResearch#301) and breaking Dashboard chat after `hermes update`.
Spawn / inline / sidecar modes never hit this: their `gateway.ready` only
arrives after the Python child boots, on a later async tick.

Fix: drain() defers the replay to the next microtask AND keeps `subscribed`
false until that microtask runs. Keeping `subscribed` false in the gap means
any live event arriving before the flush keeps buffering (publish() pushes
when !subscribed) instead of emitting synchronously and jumping ahead of the
chronologically-earlier replayed events — the flush re-drains the buffer
right after flipping `subscribed`, preserving FIFO order. A drainGeneration
token (bumped in resetStartupState) makes a queued flush a no-op if the
transport was reset/killed in the meantime, avoiding use-after-teardown and
duplicate/reordered exits.

Regression tests: (1) drain() does not dispatch buffered events synchronously;
(2) a live event arriving in the post-drain / pre-microtask window still
delivers BEHIND the earlier-buffered event (FIFO). Both are red against the
old synchronous behavior, green with this fix. Same class of fix as NousResearch#44528.

Closes NousResearch#36658
habarmc1223-sudo pushed a commit to habarmc1223-sudo/hermes-agent-fluxmem that referenced this pull request Jul 8, 2026
…teway-drain-microtask

fix(tui): defer buffered gateway events to stop dashboard chat NousResearch#301 (NousResearch#36658)
santhreal pushed a commit to santhreal/hermes-agent that referenced this pull request Jul 13, 2026
…search#301 (NousResearch#36658)

Dashboard /chat spawns the TUI attached to the dashboard's in-memory
gateway via HERMES_TUI_GATEWAY_URL. In that attach mode the already-running
gateway replays `gateway.ready` (and `session.info`) the instant the socket
connects, so those events land in GatewayClient.bufferedEvents *before* the
consumer's mount-time subscribe effect (useMainApp.ts) calls drain().

drain() then emitted the buffered events synchronously, so the
`gateway.ready` handler's patchUiState / setHistoryItems cascade ran while
React was still inside the first commit — tripping "Too many re-renders"
(Minified React error NousResearch#301) and breaking Dashboard chat after `hermes update`.
Spawn / inline / sidecar modes never hit this: their `gateway.ready` only
arrives after the Python child boots, on a later async tick.

Fix: drain() defers the replay to the next microtask AND keeps `subscribed`
false until that microtask runs. Keeping `subscribed` false in the gap means
any live event arriving before the flush keeps buffering (publish() pushes
when !subscribed) instead of emitting synchronously and jumping ahead of the
chronologically-earlier replayed events — the flush re-drains the buffer
right after flipping `subscribed`, preserving FIFO order. A drainGeneration
token (bumped in resetStartupState) makes a queued flush a no-op if the
transport was reset/killed in the meantime, avoiding use-after-teardown and
duplicate/reordered exits.

Regression tests: (1) drain() does not dispatch buffered events synchronously;
(2) a live event arriving in the post-drain / pre-microtask window still
delivers BEHIND the earlier-buffered event (FIFO). Both are red against the
old synchronous behavior, green with this fix. Same class of fix as NousResearch#44528.

Closes NousResearch#36658
santhreal pushed a commit to santhreal/hermes-agent that referenced this pull request Jul 13, 2026
…teway-drain-microtask

fix(tui): defer buffered gateway events to stop dashboard chat NousResearch#301 (NousResearch#36658)
Gravezzz pushed a commit to Gravezzz/hermes-agent that referenced this pull request Jul 21, 2026
…search#301 (NousResearch#36658)

Dashboard /chat spawns the TUI attached to the dashboard's in-memory
gateway via HERMES_TUI_GATEWAY_URL. In that attach mode the already-running
gateway replays `gateway.ready` (and `session.info`) the instant the socket
connects, so those events land in GatewayClient.bufferedEvents *before* the
consumer's mount-time subscribe effect (useMainApp.ts) calls drain().

drain() then emitted the buffered events synchronously, so the
`gateway.ready` handler's patchUiState / setHistoryItems cascade ran while
React was still inside the first commit — tripping "Too many re-renders"
(Minified React error NousResearch#301) and breaking Dashboard chat after `hermes update`.
Spawn / inline / sidecar modes never hit this: their `gateway.ready` only
arrives after the Python child boots, on a later async tick.

Fix: drain() defers the replay to the next microtask AND keeps `subscribed`
false until that microtask runs. Keeping `subscribed` false in the gap means
any live event arriving before the flush keeps buffering (publish() pushes
when !subscribed) instead of emitting synchronously and jumping ahead of the
chronologically-earlier replayed events — the flush re-drains the buffer
right after flipping `subscribed`, preserving FIFO order. A drainGeneration
token (bumped in resetStartupState) makes a queued flush a no-op if the
transport was reset/killed in the meantime, avoiding use-after-teardown and
duplicate/reordered exits.

Regression tests: (1) drain() does not dispatch buffered events synchronously;
(2) a live event arriving in the post-drain / pre-microtask window still
delivers BEHIND the earlier-buffered event (FIFO). Both are red against the
old synchronous behavior, green with this fix. Same class of fix as NousResearch#44528.

Closes NousResearch#36658
Gravezzz pushed a commit to Gravezzz/hermes-agent that referenced this pull request Jul 21, 2026
…teway-drain-microtask

fix(tui): defer buffered gateway events to stop dashboard chat NousResearch#301 (NousResearch#36658)
leewenjie pushed a commit to leewenjie/hermes-agent that referenced this pull request Aug 7, 2026
…search#301 (NousResearch#36658)

Dashboard /chat spawns the TUI attached to the dashboard's in-memory
gateway via HERMES_TUI_GATEWAY_URL. In that attach mode the already-running
gateway replays `gateway.ready` (and `session.info`) the instant the socket
connects, so those events land in GatewayClient.bufferedEvents *before* the
consumer's mount-time subscribe effect (useMainApp.ts) calls drain().

drain() then emitted the buffered events synchronously, so the
`gateway.ready` handler's patchUiState / setHistoryItems cascade ran while
React was still inside the first commit — tripping "Too many re-renders"
(Minified React error NousResearch#301) and breaking Dashboard chat after `hermes update`.
Spawn / inline / sidecar modes never hit this: their `gateway.ready` only
arrives after the Python child boots, on a later async tick.

Fix: drain() defers the replay to the next microtask AND keeps `subscribed`
false until that microtask runs. Keeping `subscribed` false in the gap means
any live event arriving before the flush keeps buffering (publish() pushes
when !subscribed) instead of emitting synchronously and jumping ahead of the
chronologically-earlier replayed events — the flush re-drains the buffer
right after flipping `subscribed`, preserving FIFO order. A drainGeneration
token (bumped in resetStartupState) makes a queued flush a no-op if the
transport was reset/killed in the meantime, avoiding use-after-teardown and
duplicate/reordered exits.

Regression tests: (1) drain() does not dispatch buffered events synchronously;
(2) a live event arriving in the post-drain / pre-microtask window still
delivers BEHIND the earlier-buffered event (FIFO). Both are red against the
old synchronous behavior, green with this fix. Same class of fix as NousResearch#44528.

Closes NousResearch#36658
leewenjie pushed a commit to leewenjie/hermes-agent that referenced this pull request Aug 7, 2026
…teway-drain-microtask

fix(tui): defer buffered gateway events to stop dashboard chat NousResearch#301 (NousResearch#36658)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants