hermes - 💡(How to fix) Fix Dashboard chat silently breaks behind reverse proxies: _hermes_ink_bundle_stale() always returns True, triggering 15s blocking npm run build on every PTY spawn [2 comments, 3 participants]

Official PRs (…)
ON THIS PAGE

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
NousResearch/hermes-agent#25351Fetched 2026-05-14 03:47:07
View on GitHub
Comments
2
Participants
3
Timeline
9
Reactions
0
Timeline (top)
labeled ×4commented ×2cross-referenced ×1mentioned ×1

Every time a user opens the /chat tab, _make_tui_argv() in hermes_cli/main.py runs a synchronous 15-second npm run build inside FastAPI's async event handler before spawning the PTY. This blocks the entire event loop, preventing WebSocket keepalives from being processed. Reverse proxies with WebSocket idle timeouts (Cloudflare, nginx, Traefik defaults) kill the connection before the build finishes, and the user sees [session ended] with no error message.

Error Message

Every time a user opens the /chat tab, _make_tui_argv() in hermes_cli/main.py runs a synchronous 15-second npm run build inside FastAPI's async event handler before spawning the PTY. This blocks the entire event loop, preventing WebSocket keepalives from being processed. Reverse proxies with WebSocket idle timeouts (Cloudflare, nginx, Traefik defaults) kill the connection before the build finishes, and the user sees [session ended] with no error message.

  • Fails silently — no error is surfaced to the user, the PTY just never starts

Root Cause

_hermes_ink_bundle_stale() checks for packages/hermes-ink/dist/ink-bundle.js:

# hermes_cli/main.py
def _hermes_ink_bundle_stale(tui_dir: Path) -> bool:
    ink_root = tui_dir / "packages" / "hermes-ink"
    bundle = ink_root / "dist" / "ink-bundle.js"
    if not bundle.exists():
        return True   # <-- always True
    ...

But the @hermes/ink build script outputs dist/entry-exports.js, not dist/ink-bundle.js:

> @hermes/[email protected] build
> esbuild src/entry-exports.ts ... --outdir=dist

  dist/entry-exports.js  418.8kb

ink-bundle.js is never created, so _hermes_ink_bundle_stale() always returns True_tui_build_needed() always returns Truenpm run build runs on every single chat session start.

Fix Action

Workaround

Set HERMES_TUI_DIR=/opt/hermes/ui-tui in the dashboard container's environment. This activates the fast path in _make_tui_argv() which checks only for dist/entry.js existence and skips _tui_build_needed() entirely:

if not tui_dev:
    ext_dir = os.environ.get("HERMES_TUI_DIR")
    if ext_dir:
        p = Path(ext_dir)
        if (p / "dist" / "entry.js").exists() and not _tui_need_npm_install(p):
            return [node, str(p / "dist" / "entry.js")], p  # no build check

Code Example

# hermes_cli/main.py
def _hermes_ink_bundle_stale(tui_dir: Path) -> bool:
    ink_root = tui_dir / "packages" / "hermes-ink"
    bundle = ink_root / "dist" / "ink-bundle.js"
    if not bundle.exists():
        return True   # <-- always True
    ...

---

> @hermes/ink@0.0.1 build
> esbuild src/entry-exports.ts ... --outdir=dist

  dist/entry-exports.js  418.8kb

---

from pathlib import Path
from hermes_cli.main import _tui_build_needed
print(_tui_build_needed(Path("/opt/hermes/ui-tui")))  # True, always

---

if not tui_dev:
    ext_dir = os.environ.get("HERMES_TUI_DIR")
    if ext_dir:
        p = Path(ext_dir)
        if (p / "dist" / "entry.js").exists() and not _tui_need_npm_install(p):
            return [node, str(p / "dist" / "entry.js")], p  # no build check
RAW_BUFFERClick to expand / collapse

Summary

Every time a user opens the /chat tab, _make_tui_argv() in hermes_cli/main.py runs a synchronous 15-second npm run build inside FastAPI's async event handler before spawning the PTY. This blocks the entire event loop, preventing WebSocket keepalives from being processed. Reverse proxies with WebSocket idle timeouts (Cloudflare, nginx, Traefik defaults) kill the connection before the build finishes, and the user sees [session ended] with no error message.

Root Cause

_hermes_ink_bundle_stale() checks for packages/hermes-ink/dist/ink-bundle.js:

# hermes_cli/main.py
def _hermes_ink_bundle_stale(tui_dir: Path) -> bool:
    ink_root = tui_dir / "packages" / "hermes-ink"
    bundle = ink_root / "dist" / "ink-bundle.js"
    if not bundle.exists():
        return True   # <-- always True
    ...

But the @hermes/ink build script outputs dist/entry-exports.js, not dist/ink-bundle.js:

> @hermes/[email protected] build
> esbuild src/entry-exports.ts ... --outdir=dist

  dist/entry-exports.js  418.8kb

ink-bundle.js is never created, so _hermes_ink_bundle_stale() always returns True_tui_build_needed() always returns Truenpm run build runs on every single chat session start.

Impact

  • Chat is completely broken for anyone running the dashboard behind a reverse proxy with WebSocket idle timeouts shorter than ~15s (Cloudflare Tunnel default: ~100s, but practical idle timeout is lower; nginx default proxy_read_timeout: 60s)
  • Fails silently — no error is surfaced to the user, the PTY just never starts
  • The build also runs synchronously in FastAPI's async event loop (subprocess.run with capture_output=True), blocking all other WebSocket/HTTP traffic during the build

Reproduction

from pathlib import Path
from hermes_cli.main import _tui_build_needed
print(_tui_build_needed(Path("/opt/hermes/ui-tui")))  # True, always

Confirmed on a fresh container start — packages/hermes-ink/dist/ink-bundle.js does not exist in the shipped image.

Workaround

Set HERMES_TUI_DIR=/opt/hermes/ui-tui in the dashboard container's environment. This activates the fast path in _make_tui_argv() which checks only for dist/entry.js existence and skips _tui_build_needed() entirely:

if not tui_dev:
    ext_dir = os.environ.get("HERMES_TUI_DIR")
    if ext_dir:
        p = Path(ext_dir)
        if (p / "dist" / "entry.js").exists() and not _tui_need_npm_install(p):
            return [node, str(p / "dist" / "entry.js")], p  # no build check

Suggested Fix

Either:

  1. Fix _hermes_ink_bundle_stale() to check for dist/entry-exports.js instead of dist/ink-bundle.js
  2. Pre-build the ink bundle in the Docker image so the file is present on startup
  3. Run the build asynchronously (via asyncio.create_subprocess_exec) so it doesn't block the event loop

Option 3 is the most defensive regardless of the stale-check fix.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

hermes - 💡(How to fix) Fix Dashboard chat silently breaks behind reverse proxies: _hermes_ink_bundle_stale() always returns True, triggering 15s blocking npm run build on every PTY spawn [2 comments, 3 participants]