hermes - 💡(How to fix) Fix Dashboard /chat PTY WebSocket can time out before HTTP 101 response flushes

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

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…

Root Cause

Investigation / Root Cause Hypothesis

Fix Action

Fix / Workaround

Verified Workaround / Candidate Fix

Test result after patch:

Code Example

InvalidMessage('did not receive a valid HTTP response')

---

/usr/local/lib/hermes-agent/venv/bin/hermes dashboard --host 0.0.0.0 --port 9119 --no-open --tui --insecure

---

/usr/local/lib/hermes-agent/venv/bin/hermes dashboard --host 127.0.0.1 --port 9121 --no-open --tui

---

hermes dashboard --host 127.0.0.1 --port 9119 --no-open --tui

---

ws://127.0.0.1:9119/api/pty?token=<token>&channel=diag

---

import re
import urllib.request
import asyncio
import websockets

async def main():
    html = urllib.request.urlopen("http://127.0.0.1:9119/chat", timeout=10).read().decode()
    token = re.search(r'window\.__HERMES_SESSION_TOKEN__="([^"]+)"', html).group(1)
    url = f"ws://127.0.0.1:9119/api/pty?token={token}&channel=diag"

    async with websockets.connect(
        url,
        open_timeout=30,
        compression=None,
        origin="http://127.0.0.1:9119",
    ) as ws:
        print("connected")
        print(await asyncio.wait_for(ws.recv(), timeout=15))

asyncio.run(main())

---

InvalidMessage('did not receive a valid HTTP response')

---

await ws.accept()

---

argv ['/root/.local/bin/node', '/usr/local/lib/hermes-agent/ui-tui/dist/entry.js']
cwd /usr/local/lib/hermes-agent/ui-tui
spawned pid <pid>
read b"...terminal bytes..."

---

await ws.accept()
await asyncio.sleep(0)

---

connected
recv type bytes len 130 head b"\x1b[0'z..."
RAW_BUFFERClick to expand / collapse

Bug Description

The dashboard /chat tab can fail to establish the /api/pty WebSocket even though the dashboard HTTP routes and other dashboard WebSockets are working.

In my environment, clients connecting to /api/pty failed with:

InvalidMessage('did not receive a valid HTTP response')

Meanwhile:

  • GET /chat returned 200 OK
  • /api/events WebSocket connected successfully
  • /api/pub WebSocket connected successfully
  • /api/pty WebSocket failed before receiving a valid HTTP upgrade response

This made the dashboard Chat tab unusable while the rest of the dashboard worked.

Environment

  • Hermes Agent: 0.13.0 (2026.5.7)
  • Source checkout commit: bfc84bdc6 chore: add Ninso112 to AUTHOR_MAP
  • OS: Linux 6.8.0-111-generic
  • Python: 3.11.15
  • Uvicorn: 0.46.0
  • websockets: 15.0.1
  • Starlette: 1.0.0
  • FastAPI: 0.136.1
  • Dashboard command:
/usr/local/lib/hermes-agent/venv/bin/hermes dashboard --host 0.0.0.0 --port 9119 --no-open --tui --insecure

Also reproduced with a local-only test dashboard:

/usr/local/lib/hermes-agent/venv/bin/hermes dashboard --host 127.0.0.1 --port 9121 --no-open --tui

Steps to Reproduce

  1. Start the dashboard with embedded chat enabled:
hermes dashboard --host 127.0.0.1 --port 9119 --no-open --tui
  1. Load /chat and extract window.__HERMES_SESSION_TOKEN__ from the page.

  2. Connect a WebSocket client to:

ws://127.0.0.1:9119/api/pty?token=<token>&channel=diag
  1. Observe the client timing out/failing before a valid HTTP response is received.

Minimal Python repro:

import re
import urllib.request
import asyncio
import websockets

async def main():
    html = urllib.request.urlopen("http://127.0.0.1:9119/chat", timeout=10).read().decode()
    token = re.search(r'window\.__HERMES_SESSION_TOKEN__="([^"]+)"', html).group(1)
    url = f"ws://127.0.0.1:9119/api/pty?token={token}&channel=diag"

    async with websockets.connect(
        url,
        open_timeout=30,
        compression=None,
        origin="http://127.0.0.1:9119",
    ) as ws:
        print("connected")
        print(await asyncio.wait_for(ws.recv(), timeout=15))

asyncio.run(main())

Expected Behavior

The /api/pty WebSocket should complete the HTTP 101 upgrade promptly, then spawn the PTY/TUI and begin streaming terminal bytes.

Actual Behavior

The client fails during the WebSocket handshake with:

InvalidMessage('did not receive a valid HTTP response')

A raw socket test also timed out waiting for the HTTP response bytes from /api/pty, while other WebSocket endpoints accepted immediately.

Investigation / Root Cause Hypothesis

The /api/pty handler calls:

await ws.accept()

then immediately proceeds into synchronous PTY/TUI startup via _resolve_chat_argv(...) and PtyBridge.spawn(...).

On this machine, spawning the TUI behind the PTY was slow enough that the event loop did not flush the HTTP 101 upgrade response to the client before the client timed out. This only affected /api/pty; other WebSocket endpoints were fine.

Directly spawning the PTY/TUI was slow but worked:

argv ['/root/.local/bin/node', '/usr/local/lib/hermes-agent/ui-tui/dist/entry.js']
cwd /usr/local/lib/hermes-agent/ui-tui
spawned pid <pid>
read b"...terminal bytes..."

Verified Workaround / Candidate Fix

Adding a single event-loop yield immediately after await ws.accept() in hermes_cli/web_server.py's pty_ws handler fixed the issue locally:

await ws.accept()
await asyncio.sleep(0)

After restarting the dashboard, /api/pty connected successfully and returned PTY bytes.

Test result after patch:

connected
recv type bytes len 130 head b"\x1b[0'z..."

This seems to give uvicorn/websockets one loop tick to flush the HTTP 101 response before the handler enters the slower PTY/TUI startup path.

Notes

This may be timing/environment dependent, which is why it may not reproduce on faster systems. A more robust fix might be to yield after ws.accept() or move the PTY/TUI spawn off the event loop if any part of startup can block long enough to delay the handshake response.

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