---
name: vps-dashboard-chat-broken
description: Diagnose a Hermes dashboard where the chat panel inside /chat shows the literal string 'Chat unavailable' (often followed by a colon and an exception message) while the rest of the dashboard renders fine. Trigger when the user says 'chat panel is broken', 'I can see the session list but can't type', 'Chat unavailable 1', or any flavour of 'web UI partially broken'. Do NOT trigger for 'gateway offline' or 'Telegram bot down' (gateway problems have their own skill).
---

# Diagnose 'Chat unavailable' in the dashboard chat panel

When the dashboard chat panel shows an error like `Chat unavailable: 1`, `Chat unavailable: the embedded terminal requires a POSIX PTY`, or `Refused: request host/origin doesn't match the dashboard`, the rest of the dashboard (session list, model picker, logs card, cron list, restart button) is usually fine. The user can browse, but cannot type into the chat. This is distinct from 'the dashboard won't load' - the page renders, only the embedded chat input is dead.

## First decision: which error string?

Get the actual error from the user's screenshot, browser DevTools console, or by reproducing locally:

```bash
python3 -c "
import asyncio, websockets
TOKEN = 'uz5WRofhGIY2Q-aFIWUXw8yeG815ZRgAEFfSQAXXY6A'
async def main():
    async with websockets.connect(
        f'ws://127.0.0.1:9119/api/pty?token={TOKEN}&attach=test',
        origin='https://<public-host>',
        additional_headers={'Host': '<public-host>'},
    ) as ws:
        await ws.send('\x1b[RESIZE:80;24]')
        msg = await asyncio.wait_for(ws.recv(), timeout=5)
        print(repr(msg)[:500])
asyncio.run(main())
"
```

Match the response to one of the layers below.

## Layer 1 - Connection refused (HTTP 4xx during WS upgrade)

- **HTTP 403 + 'host/origin doesn't match'** -> the Origin/Host guard is rejecting the browser. Dashboard is bound to loopback, nginx is proxying, browser sends public Origin, nginx can't rewrite it. Patch `_is_accepted_host` in `web_server.py` and add `HERMES_DASHBOARD_PUBLIC_HOSTS` env var to the systemd unit. See 'Patch recipe' below.
- **HTTP 401 / 'auth failed'** -> session token mismatch. The pinned token in `/etc/systemd/system/hermes-serve.service` (`HERMES_DASHBOARD_SESSION_TOKEN=...`) is being checked against a different value than the one injected into the SPA's HTML. Reload the page; if it persists, restart `hermes-serve.service` (token is pinned in the unit file, restart doesn't rotate it).
- **HTTP 400 'Invalid Host header'** -> same fix as 403: loopback bind + public Host header, patch `_is_accepted_host`.

## Layer 2 - Connected but server sends error frame

The first message from `/api/pty` after a successful WS upgrade is the error string. Decode the close code + ANSI escapes:

- **`Chat unavailable: the embedded terminal requires a POSIX PTY...`** -> only fires on native Windows Python, not relevant for a Linux VPS.
- **`Chat unavailable: 1`** (literally the string '1') -> **`SystemExit(1)` from `_make_tui_argv`** because the `ui-tui/` workspace is missing. The full path is `/root/.hermes/venv/lib/python3.12/site-packages/ui-tui/` on a venv install, or `/root/.hermes/ui-tui/` on a native checkout. **This is the most common cause on a VPS that has had a failed `hermes update`.** Fix: see 'Restore ui-tui' below.
- **`Chat unavailable: <other exception text>`** -> PTY spawned but threw during init. Check `tail -50 /root/.hermes/logs/gui.log` for the traceback.
- **`Chat failed to start: <FileNotFoundError text>`** -> node binary not found or argv path wrong. Check `_node_bin()` resolution.
- **`Console websocket error.`** from the **Console overlay** (not the chat panel) - different code path, different fix.

## Layer 3 - Server is silent

WS opens, server sends nothing within 5s, then closes. Almost always means the bundled TUI bundle failed to execute (e.g. ESM/CommonJS mismatch). Symptom: `node entry.js` standalone prints `SyntaxError: Cannot use import statement outside a module`. **Fix: drop a `package.json` next to the bundle with `{"type":"module"}`** at `/root/.hermes/venv/lib/python3.12/site-packages/hermes_cli/tui_dist/package.json`. This is a known packaging gap in 0.19.0 - see '0.19.0 ESM packaging bug' below.

## Restore ui-tui

Required when `_make_tui_argv` exits with `SystemExit(1)`. The `ui-tui/` directory is not part of the pip wheel - it must be restored from a git clone of the hermes-agent repo, then npm install + build must run.

```bash
# 0. Confirm root cause first - _make_tui_argv will print the recovery steps
/root/.hermes/venv/bin/python -c "
from hermes_cli.main import PROJECT_ROOT, _make_tui_argv
_make_tui_argv(PROJECT_ROOT / 'ui-tui', tui_dev=False)
"
# Should print: 'Error: the TUI workspace is missing from this Hermes checkout.
# Expected directory: /root/.hermes/venv/lib/python3.12/site-packages/ui-tui'

# 1. Clone the hermes-agent repo at the matching version (pyproject.toml has it)
cd /root/.hermes
git clone --depth=1 https://github.com/NousResearch/hermes-agent.git hermes-agent-src

# 2. Copy the two directories pip doesn't ship
cp -r /root/.hermes/hermes-agent-src/ui-tui /root/.hermes/venv/lib/python3.12/site-packages/ui-tui
mkdir -p /root/.hermes/venv/lib/python3.12/site-packages/apps
cp -r /root/.hermes/hermes-agent-src/apps/shared /root/.hermes/venv/lib/python3.12/site-packages/apps/shared

# 3. npm cache must be redirected (default ~/.npm is on read-only /root on Hostinger VPS)
mkdir -p /root/.hermes/.npm-cache
cd /root/.hermes/venv/lib/python3.12/site-packages/ui-tui
NPM_CONFIG_CACHE=/root/.hermes/.npm-cache npm install --no-fund --no-audit

# 4. Build the TUI bundle (esbuild -> dist/entry.js inside ui-tui/)
NPM_CONFIG_CACHE=/root/.hermes/.npm-cache npm run build

# 5. Fix the 0.19.0 ESM packaging bug (see below) - write package.json next to the pip-shipped entry.js
cat > /root/.hermes/venv/lib/python3.12/site-packages/hermes_cli/tui_dist/package.json <<'JSON'
{
  'type': 'module'
}
JSON

# 6. Restart the dashboard
sudo systemctl restart hermes-serve.service
sudo systemctl is-active hermes-serve.service

# 7. Verify with the websocket probe at the top of this skill - expect the TUI welcome frame,
#    not 'Chat unavailable: 1'
```

Total time: ~5 minutes on a warm npm cache, ~15 if npm has to fetch everything.

## Patch recipe - Origin mismatch behind reverse proxy

When nginx (or any TLS terminator) fronts a loopback-bound dashboard, browsers send `Origin: https://public.host` and `Host: public.host` and nginx's `proxy_set_header Host 127.0.0.1` rewrites Host but **cannot** rewrite Origin. The dashboard's Host/Origin guard (legitimate DNS-rebind defence, GHSA-ppp5-vxwm-4cf7) then rejects every WebSocket upgrade.

Apply this venv patch + env var. Operator-pinned public hostnames only - empty default is safe, so the DNS-rebind defence still rejects arbitrary attacker hosts.

```bash
# 1. Edit /root/.hermes/venv/lib/python3.12/site-packages/hermes_cli/web_server.py
#    In _is_accepted_host(), replace the loopback-only branch with:

    # Loopback bind: accept the loopback names
    # Operator-pinned public hostnames (comma-separated). When the dashboard
    # is bound to loopback but reached via a reverse proxy (nginx, Caddy,
    # Cloudflare Tunnel) the browser sends the public Host/Origin and nginx
    # cannot rewrite either header. Pinning the public names here keeps the
    # DNS-rebinding defence for arbitrary attacker hosts while accepting the
    # proxy's published name. Empty default = no extra hosts (safe default).
    _extra = {
        h.strip().lower()
        for h in os.environ.get('HERMES_DASHBOARD_PUBLIC_HOSTS', '').split(',')
        if h.strip()
    }

    bound_lc = bound_host.lower()
    if bound_lc in _LOOPBACK_HOST_VALUES:
        return host_only in _LOOPBACK_HOST_VALUES or host_only in _extra
    # Explicit non-loopback bind: require exact host match
    return host_only == bound_lc

# 2. Add the env var to /etc/systemd/system/hermes-serve.service
Environment='HERMES_DASHBOARD_PUBLIC_HOSTS=2.25.172.164,robblake.cloud,www.robblake.cloud'

# 3. Reload systemd and restart
sudo systemctl daemon-reload
sudo systemctl restart hermes-serve.service
```

Caveat: any future `pip install --force-reinstall hermes-agent` will wipe the venv patch. To make it durable, fork the patch into a post-install hook or accept it as recurring maintenance. There's no upstream feature flag for it as of 0.19.0.

## 0.19.0 ESM packaging bug

**Symptom**: After `pip install hermes-agent==0.19.0` (or any reinstall that re-extracts the wheel), `/api/pty` WebSocket opens successfully (HTTP 101) but the server sends the literal first-frame bytes:

```
\r\n\x1b[31mChat unavailable: 1\x1b[0m\r\n
```

**Wrong diagnosis**: this LOOKS like the same `SystemExit(1)` error as a missing `ui-tui/`, but `ui-tui/` is fine. The actual cause is that the pip-shipped bundle at `hermes_cli/tui_dist/entry.js` is an ES module (uses `import` syntax) and the wheel ships it without a sibling `package.json`. When Python invokes `node entry.js`, Node 18+ defaults to CommonJS, hits the `import` keyword, throws `SyntaxError: Cannot use import statement outside a module`, the Python handler catches the exception, and renders the error frame.

**Fix**: write the missing `package.json`:

```bash
cat > /root/.hermes/venv/lib/python3.12/site-packages/hermes_cli/tui_dist/package.json <<'JSON'
{
  'type': 'module'
}
JSON
```

This file gets wiped by every `pip install --force-reinstall` of hermes-agent. To make it durable: add a `postinstall` hook in a `requirements.txt` that re-creates it, or accept re-creating it manually after upgrades. There is no upstream fix as of 2026-07-27.

**Verify**: `node /root/.hermes/venv/lib/python3.12/site-packages/hermes_cli/tui_dist/entry.js` standalone should print `hermes-tui: no TTY` (the bundle loaded and is waiting for a TTY). If you still see `SyntaxError`, the package.json is missing or in the wrong place.

## How to ask the user before doing anything invasive

This skill's fixes are all invasive (git clone, pip reinstall, venv patch, systemd edit). Before running any of them:

1. **Show the user the diagnosis first** - they should see WHICH layer failed, not 'I'm about to do five things.'
2. **Ask before doing pip reinstall** - the user is running 0.18.2 for a reason (memory notes usually capture why). Confirm before bumping.
3. **Ask before systemd unit edits** - even though the change is additive, edits to `/etc/systemd/system/*.service` should be acknowledged.
4. **Show the patch diff before applying** - they have explicit preferences about reading diffs before approving edits.
5. **Venv edits**: confirm before editing files under `site-packages/`. Most users understand 'these get wiped on upgrade' but the trade-off should be named.

## Pitfalls

- **Don't trust a 'fixed' report before reloading the browser.** The chat panel React state can stick on the original error frame even after the server starts working. Always have the user hard-reload (`Ctrl+Shift+R`) and report back. If the page still shows the old error after hard reload AND a fresh incognito tab, then the server fix didn't actually take - don't claim victory.
- **Don't assume 'Chat unavailable' means the same thing in the chat panel and the Console overlay.** The chat panel (`/api/pty`) and the Console overlay (`/api/console`) have separate code paths with different close codes and different message templates. A fix to one doesn't fix the other. Match the close code to the right path:
  - `/api/pty` close codes 4401/4403/4404/4408 -> chat panel
  - `/api/console` 4401/4403/4404/4408 -> Console overlay
  - `/api/ws` and `/api/events` -> session list / live event feed, separate again
- **The Origin-mismatch venv patch gets wiped by every `pip install --force-reinstall hermes-agent`.** Either fork it into a post-install hook, or accept re-applying after every upgrade. Don't pretend it's durable.
- **`/root` is read-only on Hostinger VPS** (per memory notes for this user). npm, uv, and pip all try to write to `/root/.cache/` or `/root/.npm/` by default and silently fail. Always set `NPM_CONFIG_CACHE=/root/.hermes/.npm-cache` (or equivalent) before any npm operation. Same applies to `PIP_CACHE_DIR`, `UV_CACHE_DIR`, etc.
- **Cloning 200MB of `hermes-agent` to restore ~50KB of `ui-tui/`** is wasteful but unavoidable - there's no source tarball or partial download. The clone lives at `/root/.hermes/hermes-agent-src/` and can be deleted after restoration, but keeping it makes re-restoration fast (no network round-trip).
- **Don't reboot the VPS mid-fix.** A failed npm install + a sudo reboot = same problem but worse (systemd state lost, in-flight npm jobs killed).
- **`git restore -- ui-tui` (suggested by the error message)** does NOT work on a pip-installed hermes - there's no .git in `site-packages/`. The error message assumes a git checkout. Ignore that recovery step; use the git clone + copy recipe above.
- **Verify the chat actually works end-to-end after a fix, not just that `/api/status` returns 200.** The dashboard can be 100% up with the chat panel still broken, because `/api/pty` and `/api/status` are separate code paths. Always do the websocket probe at the top of this skill before declaring victory.
