# Hermes Chatto Plugin

A [Chatto](https://github.com/chattocorp/chatto) platform adapter plugin for [Hermes Agent](https://github.com/NousResearch/hermes-agent). Connects your self-hosted Chatto server to Hermes so you can interact with the agent through chat rooms and DMs.

All API interaction goes through [chattolib](https://github.com/chattocorp/chattolib): a persistent WebSocket realtime connection delivers inbound events, ConnectRPC over HTTP handles outbound messages, reactions, uploads and more. Messages are processed through the full Hermes Agent pipeline (tool use, memory, reasoning) and answered in real time.

Tested against Chatto v0.4.20 with the vendored `chattolib[realtime]` 0.4.20.post1 (realtime protocol v1). Chatto servers older than v0.4.20 are not supported.

Before setup, here's the part most people want to know: how Hermes behaves once it's in your Chatto instance.

## Hermes Agent behavior in Chatto

| Context | Behavior |
|---------|----------|
| **DMs** | Hermes responds to every message. No `@mention` needed. Each DM has its own session. |
| **Rooms** | Hermes responds to every room message by default. With `CHATTO_REQUIRE_MENTION=true`, channel-kind rooms answer only `@mentions`; group-kind rooms always respond. |
| **Threads** | If you reply in a thread, Hermes keeps the thread context isolated from the parent room. The bot auto-follows threads it participates in. |
| **Processing indicators** | Hermes adds a 👀 reaction when it starts processing a message, and replaces it with ✅ on success, ❌ on failure, or 🚫 when processing was cancelled. |
| **Editing your messages** | An edit within 5 minutes of posting counts as a correction: while Hermes is still working on the original, he restarts with the edited text (🚫 → 👀); if he had ignored or not yet answered the message, the gates are re-checked against the new text — adding a forgotten `@mention` this way works. Edits to messages Hermes has already answered change nothing. Disable with `CHATTO_EDIT_DISPATCH=false`. |
| **Typing indicators** | Hermes broadcasts persistent typing indicators while it's working, so users know the bot is active. |
| **Message splitting** | Long responses (>10000 chars) are automatically split into multiple messages. |

> **Tip:** By default Hermes answers every message in a room. Set `CHATTO_REQUIRE_MENTION=true` to have him answer only `@mentions` in channel-type rooms.

## Capability Matrix

Capabilities natively implemented by the Chatto plugin adapter:

| Capability | Supported |
|------------|--------|
| text | yes |
| markdown | yes |
| threads | yes |
| reactions | yes |
| message editing | yes |
| inbound edit processing | yes (correction / late-mention / ignored, see behavior table) |
| message deletion | yes |
| typing indicators | yes |
| processing notifications | yes (👀/✅/❌/🚫) |
| file attachments | yes (chunked upload) |
| image / video / audio / documents | yes (native attachments) |
| DM initiation | yes |
| room creation | yes |
| room join/leave over DM | yes (`/join`, `/leave`) |
| member directory | yes (cached) |
| presence broadcasting | yes (online, refreshed) |
| custom status | yes |
| read state management | yes |
| auto-reconnect | yes (with duplicate-event suppression) |

## Prerequisites

1. **A running Chatto server** — self-hosted and accessible from the Hermes host. See the [Chatto repository](https://github.com/chattocorp/chatto) for installation instructions.
2. **A Chatto user account** — the adapter logs in with a username and password (or token). Create a dedicated account for the bot (e.g., `hermes`).
3. **Room membership** — the bot account must be a member of any room where you want it to respond; the adapter picks up every room the account has joined. Membership can be granted natively in Chatto (invite the bot account), or by DMing the bot `/join <room-id or #name>` (see [Managing Rooms over DM](#managing-rooms-over-dm)). For DMs, simply start a direct message with the bot.
4. **Network access** — the Hermes host must reach the Chatto server URL over HTTPS (or HTTP) and establish a WebSocket connection to `/api/realtime`.

> **Info:** The adapter uses WebSocket protocol v1 (compatible with Chatto v0.4.20+). Ensure your Chatto server is up to date.

## Install

### Option A: Plugin Install (Recommended)

```bash
hermes plugins install https://gogs.netdome.biz/paul/hermes-chatto-plugin.git
```

Hermes clones the plugin from that Git URL into its platform-plugin directory and auto-discovers it on gateway startup — no manual registration needed.

> **Note:** During install Hermes reads the env vars declared in the plugin's `plugin.yaml` (`requires_env` / `optional_env`) and prompts you for each — connection details plus a few behaviour options. Your answers are written to `~/.hermes/.env`; secrets should still live there only (see [Configuration](#configuration)).

### Option B: Manual Installation

Copy the plugin files into the Hermes Agent plugin directory so they sit at `~/.hermes/plugins/platforms/chatto/`. Hermes auto-discovers platform plugins on gateway startup — no manual registration needed.

## Configuration

### Option A: Interactive Setup (Recommended)

Run the guided setup command:

```bash
hermes gateway setup
```

Select **Chatto** when prompted, then provide your server URL, login, and password when asked.

### Option B: Environment Variables

Add the following to your `~/.hermes/.env` file:

```bash
# Required (token instead of login/password also works)
CHATTO_BASE_URL=https://chat.example.com
CHATTO_LOGIN=hermes
CHATTO_PASSWORD=your-password

# Optional: home channel for cron/notification delivery when there is no
# conversation context to reply into — not exempt from CHATTO_RESPOND_ROOMS
# CHATTO_HOME_CHANNEL=REljMv5Pgolo6Y9

# Optional: room IDs the bot reads and answers in; other joined rooms stay
# read-only (marked as read, never seeded into context, never answered)
# CHATTO_RESPOND_ROOMS=REljMv5Pgolo6Y9

# Optional: restrict who can talk to the bot (comma-separated logins)
# CHATTO_ALLOWED_USERS=alice,bob

# Optional: allow any user to talk to the bot (default: false)
# CHATTO_ALLOW_ALL_USERS=true

# Optional: answer only @mentions in channels — group rooms and DMs always respond
# CHATTO_REQUIRE_MENTION=true

# Optional: rooms that answer without a mention even when require_mention is on
# CHATTO_FREE_RESPONSE_CHANNELS=REljMv5Pgolo6Y9

# Optional: auto-create threads for replies in rooms (default: true)
# CHATTO_AUTO_THREAD=true

# Optional: 👀/✅/❌/🚫 processing reactions (default: true)
# CHATTO_REACTIONS=true

# Optional: treat edits of chat messages as corrections (default: true).
# A message being processed restarts with the edited text; a message the bot
# ignored (e.g. missing @mention) is re-checked against its new text;
# already-answered messages stay answered.
# CHATTO_EDIT_DISPATCH=true

# Optional: how long after posting an edit may still land, in seconds
# (default: 300). Older edits are ignored.
# CHATTO_EDIT_WINDOW=300
```

Secrets (`CHATTO_TOKEN`, `CHATTO_PASSWORD`) belong in `~/.hermes/.env`, never in `config.yaml`.

### Option C: config.yaml

Or configure via `~/.hermes/config.yaml`:

```yaml
gateway:
  platforms:
    chatto:
      enabled: true
      extra:
        base_url: https://chat.example.com
        home_channel: REljMv5Pgolo6Y9
        respond_rooms: []          # empty = read and answer in every joined room
        require_mention: false     # only respond to @mentions in channels
        allowed_users: []          # empty = deny all (or set allow_all_users)
        allow_all_users: true
        edit_dispatch: true        # process message edits as corrections
        edit_window: 300           # seconds after posting an edit may land
```

> **Note:** Environment variables override `config.yaml` values.

### Environment Variables

| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `CHATTO_BASE_URL` | Yes* | ChattoHQ (`https://chat.chatto.run`) | Base URL of the Chatto server (e.g., `https://chat.example.com`) |
| `CHATTO_LOGIN` | Yes* | — | Chatto username (login) |
| `CHATTO_PASSWORD` | Yes* | — | Chatto password |
| `CHATTO_TOKEN` | No | — | Existing bearer token — alternative to login/password |
| `CHATTO_HOME_CHANNEL` | No | First joined room | Default delivery target for cron/notification output when there is no inbound conversation context to reply into. Not exempt from `CHATTO_RESPOND_ROOMS`. |
| `CHATTO_RESPOND_ROOMS` | No | _(every joined room)_ | Comma-separated room IDs the bot reads and answers in; other joined rooms stay read-only — marked as read, never seeded into context, never answered |
| `CHATTO_ALLOWED_USERS` | No | _(deny all)_ | Comma-separated Chatto logins allowed to talk to the agent |
| `CHATTO_ALLOW_ALL_USERS` | No | `false` | Allow any Chatto user to talk to the agent (`true`/`false`) |
| `CHATTO_REQUIRE_MENTION` | No | `false` | Only respond to `@mentions` in channels. DMs always get a response. |
| `CHATTO_FREE_RESPONSE_CHANNELS` | No | — | Room IDs that answer without a mention even when `require_mention` is enabled |
| `CHATTO_AUTO_THREAD` | No | `true` | Auto-create threads for replies in rooms (`true`/`false`) |
| `CHATTO_REACTIONS` | No | `true` | 👀/✅/❌/🚫 processing reactions (`true`/`false`) |
| `CHATTO_EDIT_DISPATCH` | No | `true` | Treat edits of chat messages as corrections: a message being processed restarts with the edited text, an ignored message is re-checked against its new text, already-answered messages stay answered (`true`/`false`) |
| `CHATTO_EDIT_WINDOW` | No | `300` | How long after posting an edit may still land, in seconds; older edits are ignored |

\* Either login+password or `CHATTO_TOKEN` is required.

## Start the Gateway

Once configured, start the gateway:

```bash
hermes gateway
```

The bot should connect to your Chatto server within a few seconds. Send it a message — a DM or any message in a room — to test.

If nothing happens at startup — plugin failed to load, credentials rejected, server unreachable — run `hermes logs` to see what the Chatto adapter reported.

You can run `hermes gateway` in the background or as a systemd service for persistent operation.

## Features

### Realtime Connection

The adapter maintains a persistent WebSocket connection to the Chatto server through chattolib's event streaming API. All realtime events — room messages, reactions, DM notifications — arrive pre-decoded and are dispatched to the Hermes Agent pipeline.

### ConnectRPC API (Outbound)

All outbound actions — sending messages, reactions, typing indicators, file uploads, etc. — use chattolib's high-level API methods, which handle ConnectRPC calls over HTTP with authentication and error recovery automatically.

### Markdown Support

Hermes sends messages with full Markdown formatting. Chatto renders Markdown natively, so code blocks, lists, bold/italic, links, and inline code all display correctly.

### Thread / Reply Support

When you reply to a message in Chatto (creating a thread), Hermes responds within that thread. Thread context stays isolated from the parent room — each thread has its own session namespace. With auto-threading enabled (default), fresh room replies open a thread under the incoming message, keeping busy-room timelines clean. The bot auto-follows threads it participates in.

### Reactions

Hermes uses emoji reactions for processing lifecycle notifications:

- 👀 — added when the agent starts processing your message
- ✅ — replaces 👀 when processing completes successfully
- ❌ — replaces 👀 when processing fails
- 🚫 — replaces 👀 when processing was cancelled

Disable the lifecycle reactions with `CHATTO_REACTIONS=false`.

### Message Editing and Deletion

The adapter supports editing existing messages (via chattolib's `update_message`) and deleting them (via `delete_message`). Streaming replies are delivered as incremental edits to a single message.

Inbound edits are processed too — see [Editing Your Messages](#editing-your-messages) under Usage Notes.

### Typing Indicators

While the agent works, the adapter broadcasts a persistent typing indicator per room, refreshing it until the response is sent.

### Message Splitting

Messages longer than 10000 characters are automatically split into multiple sequential messages (split threshold: 9900 chars to leave room for separators).

### Chunked File / Attachment Upload

File attachments are uploaded via chattolib's chunked upload API:

1. `create_upload` — initiates an upload session
2. `upload_chunk` — uploads the file in 256 KB chunks (SHA-256 verified per chunk)
3. `complete_upload` — finalizes the upload and attaches it to the message

Images, videos, audio and documents are posted as native Chatto attachments; several images can be bundled into a single message.

### Auto-Reconnect

If the realtime stream drops, the adapter reconnects automatically with exponential backoff (1 s → 30 s max, jittered) and honours server-provided retry hints. Events the Chatto server redelivers after a reconnect are recognised by an in-memory event-ID list and processed only once.

### Read State Management

After discovering rooms, the adapter marks joined rooms as read and dismisses pending notifications (`MarkRoomAsRead` / `DismissAllNotifications`), keeping the bot's unread indicators clean.

### DM Initiation

The adapter can proactively start direct messages with other Chatto users via the `StartDM` RPC endpoint. This is used for cron job delivery and notifications.

### Room Creation

The adapter can create new rooms via `CreateRoom`. This is available to the agent for organizing conversations or creating dedicated channels for tasks.

### Member Directory

User lookups (for mention resolution etc.) go through `GetUser` and are cached per user ID and per handle to reduce API calls.

### Presence Broadcasting

Presence is a TTL the server lets lapse, not a flag that stays set: `UpdatePresence` rejects `OFFLINE` outright ("stop refreshing to go offline"). The bot therefore announces itself online on startup and a background task re-announces it every 60 seconds until disconnect cancels that task. Going offline is exactly that cancellation — the adapter never sends an offline status.

| Presence Status | API Value |
|-----------------|-----------|
| Online | 1 |
| Away | 2 |
| Do Not Disturb | 3 |

## Usage Notes

### Respond Rooms

With `CHATTO_RESPOND_ROOMS` set to a comma-separated list of room IDs, the bot reads and answers only in those rooms. Other joined rooms stay read-only: they remain joined and are marked as read, but their history is never seeded into the agent's context and their messages never reach the agent pipeline — no answers, no processing reactions. This is the intended way to quiet server-forced rooms that every account is joined to automatically, such as announcement channels (the joined-rooms log line marks them `[universal]`).

The list gates inbound replies only. DMs always get a response so `/join` stays reachable, and context-less cron/notification delivery via `CHATTO_HOME_CHANNEL` is unaffected. When unset (default), every joined room is a respond room.

### Mention Detection

With `CHATTO_REQUIRE_MENTION=true`, Hermes answers channel-kind rooms only when the message contains an `@mention` of the bot's login or display name. In group-kind rooms and DMs every message gets a response. A channel message aimed at someone *else* is acknowledged with a 🫥 reaction instead of an answer.

### Editing Your Messages

Editing a message you sent is treated as a correction, not as new traffic. What an edit does depends on where the original message stands:

| Original message | Effect of the edit |
|------------------|--------------------|
| Being processed right now (👀) | The running turn is cancelled (🚫) and restarts immediately with the edited text (👀 again). |
| Queued behind another turn in the same conversation | The queued message is updated in place to the edited wording — no stale answer later. |
| Ignored so far (e.g. missing `@mention` in a channel) | The admission gates are re-checked against the new text; if it passes now — mention added, for example — the message is answered for real. |
| Already answered (✅/❌) | Nothing. Answered messages stay answered, so old conversations cannot be re-animated by editing them. |

Two limits apply: only edits within `CHATTO_EDIT_WINDOW` seconds of posting are processed (default 300), and edits whose text is a membership command (`/join`, `/leave`) never run those commands again. The whole behavior can be turned off with `CHATTO_EDIT_DISPATCH=false`.

### Allowed Users

By default, if neither `CHATTO_ALLOWED_USERS` nor `CHATTO_ALLOW_ALL_USERS` is set, the bot denies all users as a safety measure. Configure one of:

- `CHATTO_ALLOWED_USERS=alice,bob` — only these Chatto logins can interact with the bot
- `CHATTO_ALLOW_ALL_USERS=true` — any Chatto user can interact

Setting both at once is rejected as conflicting configuration.

> **Warning:** Setting `CHATTO_ALLOW_ALL_USERS=true` means any user on your Chatto server has full access to the agent's capabilities, including tool use and system access. Use this only on trusted, private Chatto instances.

For deployment-level hardening beyond the bot's own gates, see the security documentation of your Hermes Agent distribution.

### Home Channel

The home channel is the default outbound target for proactive messages — cron job output, reminders, and notifications when there is no inbound conversation context to reply into. Set it via:

- `CHATTO_HOME_CHANNEL` env var (room ID)
- `config.yaml` → `gateway.platforms.chatto.extra.home_channel`

If unset, the first joined room is used as the default home channel. It is not exempt from `CHATTO_RESPOND_ROOMS`: with a respond list set, the home channel still receives cron/notification posts, but the bot converses there only if the room is listed too.

### Managing Rooms over DM

The bot can manage its own room memberships when you DM it:

- `/join <room-id>` — join a room by ID
- `/join #name` — join a room by name (case-insensitive; ambiguous names return the candidate IDs)
- `/leave <room-id or #name>` — leave a room

These commands work only in direct messages with the bot and require you to pass the same user check as normal messages (`CHATTO_ALLOWED_USERS` / `CHATTO_ALLOW_ALL_USERS`). They change membership on the Chatto server itself (RoomService/JoinRoom, RoomService/LeaveRoom), so joined rooms survive gateway restarts and `/leave`d rooms stay left.

Two refusals by design: direct messages cannot be left, and leaving the configured home channel is rejected because cron/notification delivery posts there. If `CHATTO_HOME_CHANNEL` names a room the bot has not joined, the adapter logs a warning at each reconnect — invite the account natively in Chatto or DM it `/join`.

## Troubleshooting

**First step for every issue below:** look at what the gateway logged around the time things went wrong:

```bash
hermes logs
```

The Chatto adapter logs each connection attempt, room discovery (`_refresh_rooms`), rejected users, failed sends and reconnects there — most problems name their cause directly.

### Bot is not responding to messages

**Cause**: The bot account is not a member of the room, or the user is not in `CHATTO_ALLOWED_USERS`.

**Fix**: Verify the bot is a member of the room (the adapter picks up all joined rooms by default; DM the bot `/join <room-id or #name>` to add it). Check that your Chatto login is in `CHATTO_ALLOWED_USERS`, or set `CHATTO_ALLOW_ALL_USERS=true`. Restart the gateway.

### Connection refused / WebSocket fails to connect

**Cause**: The Chatto server is unreachable, or the URL is incorrect.

**Fix**: Verify `CHATTO_BASE_URL` points to your Chatto server (include `https://`, no trailing slash). Test connectivity:

```bash
curl -s -o /dev/null -w "%{http_code}" https://chat.example.com/api/connect/chatto.api.v1.ViewerService/GetViewer
```

If you use a reverse proxy (nginx, Apache), ensure WebSocket upgrade headers are configured for `/api/realtime`:

```nginx
location /api/realtime {
    proxy_pass http://chatto-backend;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_read_timeout 600s;
}
```

### Authentication failed

**Cause**: The `CHATTO_LOGIN` or `CHATTO_PASSWORD` is incorrect.

**Fix**: Verify credentials by testing the login endpoint directly:

```bash
curl -X POST https://chat.example.com/auth/login \
  -H "Content-Type: application/json" \
  -d '{"login":"hermes","password":"your-password"}'
```

If the response does not include a `token` field, the credentials are wrong. Reset the bot account password in Chatto and update `~/.hermes/.env`.

### Protocol version mismatch

**Cause**: The Chatto server is running an older version that doesn't support realtime protocol v1.

**Fix**: The adapter uses protocol v1, supported by Chatto v0.4.20 and later. Upgrade your Chatto server:

```bash
curl -s https://chat.example.com/api/version | jq .
```

If you see a `RealtimeError` with a protocol-related message in the logs, upgrading the server is the fix.

### WebSocket disconnects / reconnection loops

**Cause**: Network instability, Chatto server restarts, or firewall/proxy issues with WebSocket connections.

**Fix**: The adapter automatically reconnects with exponential backoff. Check:

1. Your server's WebSocket configuration — reverse proxies need upgrade headers (see above).
2. No firewall is blocking WebSocket connections on your Chatto server.
3. The Chatto server is running and healthy.

Run `hermes logs` for recent adapter output, or dig through the gateway log directly:

```bash
grep -i "chatto\|websocket\|realtime" ~/.hermes/logs/gateway.log | tail -30
```

### 401 Unauthorized during operation

**Cause**: The bearer token expired during a session.

**Fix**: Check that the bot account hasn't been deactivated and the password hasn't changed. Update `CHATTO_PASSWORD` (or `CHATTO_TOKEN`) in `~/.hermes/.env` and restart the gateway.

### Bot is offline

**Cause**: The Hermes gateway isn't running, or it failed to connect.

**Fix**: Check that `hermes gateway` is running. Look at the terminal output or gateway logs for error messages. Common issues: wrong URL, expired credentials, Chatto server unreachable.

## Notes

- **Self-hosted friendly**: Works with any self-hosted Chatto instance. No cloud account or subscription required.
- **Minimal dependencies**: Only requires `chattolib[realtime]>=0.4.20.post1`, which is vendored — nothing to install.
- **Resume cursor support**: On reconnect, the adapter resumes from the last event cursor, so no messages are lost or replayed during transient disconnects.

## License

MIT
