Skip to content

feat(bot-mode): bots on every Desktop connection can message each other - #92784

Merged
teknium1 merged 1 commit into
mainfrom
hermes/hermes-c9687084
Aug 23, 2026
Merged

teknium1 merged 1 commit into
mainfrom
hermes/hermes-c9687084

Conversation

@teknium1

Copy link
Copy Markdown
Collaborator

Summary

Bots on every Desktop connection can now message each other: message_agent reaches agents on any gateway the Desktop holds a socket to (local, remote URL, SSH, Hermes Cloud, docker) — connections ARE the peer set (Teknium ruling, Aug 2026). Also fixes a gate bug where profiles with the legacy plugin-appended protocol in SOUL.md silently lost the message_agent tool.

Root cause of the reported dead end (user screenshot: cloud bot @-mentioned, agent replies "agent messaging isn't available here"): cross-connection roster rows were visible and taggable but had no delivery route — message_agent only knew local profiles and hermes peer targets, and the injection gate keyed on protocol-section non-emptiness, which the legacy-SOUL dedupe empties.

How it works

The Desktop already holds a persistent authenticated line to every connected gateway. Two plugin loops use those lines:

  • Roster sync (60s): pushes each gateway the union roster of agents on the OTHER connections (bot_relay.roster.sync) → Bot Chat prompts list them ("Teammates on OTHER connected machines"), capability epoch refreshes eternal chats on change.
  • Envelope relay (4s): drains queued cross-connection DMs (bot_relay.outbox.drain), delivers each on the target connection's own socket (bot_relay.deliver — same one-turn Bot Chat transport local DMs use), posts the reply back (bot_relay.reply) where a waiter wakes the sender via the standard completion-notification path.

Gateways never see each other's credentials; the Desktop is the courier. Ambiguous handles disambiguate as handle@connection (the tool's error teaches the exact forms). Older backends without the RPCs are skipped per-call.

Changes

  • tools/bot_relay.py (new): roster store + validation, target resolution, atomic outbox claim, reply files, waiter command.
  • tools/bot_mode_dm.py: relay route in message_agent (peer → local → relay, incl. same-name-on-other-connection); gates now key on is_bot_mode_managed() — fixes the legacy-SOUL tool loss.
  • tools/bot_mode_probe.py: is_bot_mode_managed(); remote-teammates paragraph in the protocol section; relay roster in the capability fingerprint.
  • tui_gateway/methods_bot_relay.py (new) + server.py: 4 RPCs, all on the RPC pool (deliver runs a full agent turn).
  • apps/desktop/src/plugins/hermes-bots/plugin.js: relay worker loops (start/stop via register/onDispose, feature-detected, degrade gracefully); mention-middleware note now names the cross-connection message_agent target.
  • Docs: website/docs/user-guide/bot-mode.md — new "Messaging across connected machines (the Desktop relay)" section; @mention + cross-machine sections updated.

Validation

Before After
Cloud/SSH/remote bot via message_agent unreachable ("not a peer") delivered + reply relayed
Legacy-SOUL profile Bot Chat tool silently missing tool injected (regression test)
Bot Chat roster local + peers only + agents on every connection

Tests: 50 Python (13 relay + 5 RPC new; all bot_mode_dm/probe suites green), 368 plugin source-shape tests (5 new). E2E: real two-install round-trip — roster sync → message_agent ack → envelope claim → deliver RPC (argv contract) → reply → real waiter subprocess prints "Reply from @ops on ssh-b: … All green"; error path prints the delivery failure distinctly.

Infographic

Every Connection Is A Peer

Connections ARE the peer set: every gateway connected to the Desktop
(local, remote URL, SSH, Hermes Cloud, docker) is now message_agent-
reachable. The Desktop relays over the persistent sockets it already
holds — roster sync per connection, envelope drain/deliver/reply loops —
so cross-connection DMs work exactly like local ones, replies included.

Also fixes the legacy-SOUL gate bug: profiles whose SOUL.md carries the
old plugin-appended protocol silently lost the message_agent tool
because the injection/execution gates keyed on protocol-section
non-emptiness instead of managed-install.
@github-actions

github-actions Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

૮ >ﻌ< ა ci review

ran on a0c4b45 — feat(bot-mode): bots on every Desktop connection can message

⚠️ Warnings

CI timings · View report · View job

Wall time 9m47s vs 3m14s (+202.6%). 2 job(s) slower, 10 faster, 2 unchanged.

  • JS & TS checks / JS & TS checks: -30.0s
  • Python lints / ruff enforcement (blocking): -14.0s
  • Python lints / Windows footguns (blocking): -9.0s
  • Python tests / Run tests: +8.0s
  • OS-specific tests / Windows-only tests: -8.0s

OSV vulnerability scan · View job

7 known vulnerabilities found in pinned dependencies.

How to fix:

Review the findings in the Security tab. Update the affected dependencies if a patched version is available.

@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/desktop Electron desktop app (apps/desktop/*) comp/tui Terminal UI (ui-tui/ + tui_gateway/) labels Aug 23, 2026
@teknium1

Copy link
Copy Markdown
Collaborator Author

Live E2E evidence (no mocks anywhere in the chain)

Level A — relay protocol over real WebSockets. Two real hermes dashboard gateways (A :18871, B :18872, separate scratch HERMES_HOMEs, real OpenRouter key), script playing the Desktop courier over each gateway's real /api/ws:

  1. bot_relay.roster.sync on A → {count: 2} (B's default + ops pushed)
  2. Real sender turn on A (real AIAgent, Bot Chat, gemini-3.7-flash) — the model chose to call message_agent; envelope queued
  3. bot_relay.outbox.drain on A → 1 envelope, scout → ops@machine-b
  4. bot_relay.deliver on B → real one-turn hermes -p ops chat run, replied in 18.5s
  5. bot_relay.reply on A → reply file written; receiver's ops state.db carries the attributed DM + assistant reply (14 messages)

Level B — the shipped plugin loops as the courier. Real Electron desktop (dev server + CDP, worktree backend via HERMES_DESKTOP_HERMES), connections registry seeded with both gateways. No script touched the relay RPCs — the hermes-bots plugin's own 60s roster loop and 4s drain loop did all carrying:

  • Roster loop live on both sides: A's roster lists default@dash-b + ops@dash-b, B's lists default@local + scout@local, both refreshing on the 60s cadence
  • Decisive round-trip with clean dirs + unique codeword:
[sender]   envelope 94f9c35b… queued: "Message from 🤖 scout (@scout): Codeword ZEBRA-9174: what is 17*23? …"
[relay]    reply for 94f9c35b…: error='' — "Codeword ZEBRA-9174: 391"
[receiver] B ops Bot Chat (state.db): user DM + assistant "Codeword ZEBRA-9174: 391"
LEVEL B LIVE E2E (Desktop loops as courier): PASS

PASS criteria were strict: the new envelope id's reply file (a first run matched a stale reply file from Level A — invalidated, dirs swept, rerun), correct answer + codeword in the reply, and the unique DM present in the receiving profile's state.db.

Field notes from the run: desktop resolves its local backend via PATH hermes (main-repo code) unless HERMES_DESKTOP_HERMES pins the worktree — the loops silently no-op against a backend without the RPCs, exactly the graceful-degradation path older backends will take.

@teknium1
teknium1 merged commit d3e087f into main Aug 23, 2026
62 of 64 checks passed
@teknium1
teknium1 deleted the hermes/hermes-c9687084 branch August 23, 2026 09:16
teknium1 added a commit that referenced this pull request Aug 23, 2026
…tempfile

Widen the DM tempfile-leak fix (#91902/#92407) to the sibling sites
PR #92784 introduced:

- tools/bot_relay.py: expose the 6h stale sweep as
  cleanup_bot_relay_artifacts() (cleanup_*_cache contract) and wire it
  into gateway housekeeping — previously it ran only when the Desktop
  drained the outbox, so plaintext envelopes/replies queued while the
  Desktop was away could sit on disk forever.
- tui_gateway/methods_bot_relay.py: move the payload write inside the
  try/finally so a failed write no longer leaks hermes-relay-dm-*.txt.
- tools/bot_mode_dm.py: _spawn_delivery takes dm_file=None for relay
  waiter deliveries, which have no plaintext DM tempfile to reclaim.
melon-xf added a commit to melon-xf/hermes-agent that referenced this pull request Sep 3, 2026
…tempfile

Widen the DM tempfile-leak fix (NousResearch#91902/NousResearch#92407) to the sibling sites
PR NousResearch#92784 introduced:

- tools/bot_relay.py: expose the 6h stale sweep as
  cleanup_bot_relay_artifacts() (cleanup_*_cache contract) and wire it
  into gateway housekeeping — previously it ran only when the Desktop
  drained the outbox, so plaintext envelopes/replies queued while the
  Desktop was away could sit on disk forever.
- tui_gateway/methods_bot_relay.py: move the payload write inside the
  try/finally so a failed write no longer leaks hermes-relay-dm-*.txt.
- tools/bot_mode_dm.py: _spawn_delivery takes dm_file=None for relay
  waiter deliveries, which have no plaintext DM tempfile to reclaim.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/desktop Electron desktop app (apps/desktop/*) comp/tui Terminal UI (ui-tui/ + tui_gateway/) P3 Low — cosmetic, nice to have type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants