Skip to content

Readme enhancements - #1

Merged
jodeev merged 6 commits into
mainfrom
readme-enhancements
Apr 6, 2026
Merged

Readme enhancements#1
jodeev merged 6 commits into
mainfrom
readme-enhancements

Conversation

@jodeev

@jodeev jodeev commented Apr 3, 2026

Copy link
Copy Markdown
Owner

Summary

Updated the README to allow people who land on this project to decipher quickly exactly what this project does and doesn't do.

Updated the problem to be more specific. Add the key differentiators of the project. Add target audience and the Why this project is here.

Changes

REAMDE updates:

  • updated subtitle
  • corrected problem
  • Dropped the "shared intent, shared memory shared context", replaced with Alignment, alignment memory, and peer collab environment, added callout about whats delivered now and where project may go, added note about definition of "Session" for mycelium
  • New section on alternatives
  • New section on who its for and why we're building it
  • NOTE: Quick start, Architecture, Development and Built on are unchanged from the original README

@jodeev
jodeev force-pushed the readme-enhancements branch 2 times, most recently from 1873c78 to d0708b2 Compare April 3, 2026 20:57
@jodeev
jodeev force-pushed the readme-enhancements branch from d0708b2 to a04ff17 Compare April 3, 2026 21:07
Comment thread README.md Outdated
Comment thread README.md Outdated

@jodeev jodeev left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestions to replace plurals with neutral language

Comment thread README.md Outdated
Comment thread README.md Outdated
Comment thread README.md Outdated
Comment thread README.md Outdated
Comment thread README.md Outdated
Comment thread README.md Outdated
Comment thread README.md Outdated
Comment thread README.md Outdated
Comment thread README.md Outdated
Comment thread README.md Outdated
Comment thread README.md Outdated
Co-authored-by: Jodee Varney <48965776+jodeev@users.noreply.github.com>

@jodeev jodeev left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional suggested fixes

Comment thread README.md Outdated
Comment thread README.md Outdated
Comment thread README.md Outdated
Comment thread README.md Outdated
Comment thread README.md Outdated
Comment thread README.md Outdated
jodeev added 4 commits April 5, 2026 22:05
Co-authored-by: Jodee Varney <48965776+jodeev@users.noreply.github.com>
Co-authored-by: Jodee Varney <48965776+jodeev@users.noreply.github.com>
@jodeev

jodeev commented Apr 6, 2026

Copy link
Copy Markdown
Owner Author

LGTM

@jodeev
jodeev merged commit 2270b91 into main Apr 6, 2026
2 checks passed
jodeev pushed a commit that referenced this pull request Jun 17, 2026
…io#348)

* feat(hermes): integrate hermes-agent as a long-lived-gateway adapter

Mycelium can now register hermes-agent runtimes alongside openclaw,
claude_code, and cursor. Hermes runs as a single gateway process that
owns its own platform adapters, so the integration mirrors the openclaw
shape: ship a hermes-side plugin from the mycelium repo, stage it into
~/.hermes/plugins/mycelium/ at adapter-add time, and patch
~/.hermes/config.yaml so the gateway loads it on next start.

Mycelium side:

- New HermesIntegration (long_lived_gateway lifecycle) handling install,
  uninstall, manifest construction, register/destroy, and doctor checks.
  register() / destroy() append/merge room entries into
  ~/.hermes/config.yaml under platforms.mycelium-room.rooms.
- AgentManifest gains "hermes" adapter + optional hermes_profile field
  (parallel to openclaw_profile) so the same handle can address
  different gateways on one host.
- adapter CLI, doctor, daemon dispatch, and generated docs all surface
  the new family. doctor checks plugin presence, config.yaml shape,
  backend_url consistency, and the gateway pid file (JSON-encoded by
  modern hermes).
- _restart_gateway probes for a hermes-gateway systemd service before
  restarting; without one it prints a banner pointing at `hermes
  gateway run` instead of blocking forever on a foreground start.

Hermes-side plugin (assets/mycelium/plugin/, staged onto the host):

- Python port of openclaw's mycelium-room TS plugin — adapter.py
  registers a BasePlatformAdapter via PluginContext.register_platform,
  route.py / mentions.py mirror the TS routing + formatter, post_to_room
  / room_sse / session_sse / return_address / notify_home cover the
  rest of the wire surface.
- post_to_room sends message_type: "broadcast" so the backend's
  MessageCreate schema accepts the payload (matches openclaw).
- adapter.send() does chat_id.rpartition(":") to split the
  <room>:<agent> dispatch key back into a bare room slug and a default
  sender_handle; session sub-room keys (parent:session:<uuid>) round-
  trip cleanly through the same rpartition.

Tests:

- test_hermes_route — snapshot-style coverage for tick / consensus
  formatting and route_message dispatch, loaded through an isolated
  synthetic package so the plugin tree can be exercised without a
  running hermes.
- test_hermes_install — end-to-end YAML patching round-trip against a
  temporary $HERMES_HOME, with _restart_gateway stubbed.
- test_hermes_postback — pins the regressions from the smoke test: the
  broadcast message_type payload and the chat_id rpartition split.

Docs:

- docs/adapters.html (and the generator) document the new family with
  the same card / sidebar / section structure as the others.

Smoke-tested end-to-end against a local hermes gateway running under
user-mode systemd: an inbound mention reached the agent and the reply
landed back in the mycelium room.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(hermes): avoid daemon-pipe deadlock in _restart_gateway

Replace ``subprocess.run(..., capture_output=True, timeout=…)`` with a
temp-file backed redirect in the two ``hermes gateway …`` invocations.

``hermes gateway restart`` (on a host with the gateway installed as a
systemd service) spawns a detached planned-restart helper that inherits
the CLI's stdout/stderr fds. The CLI itself exits promptly, but the
helper keeps the pipe write-ends open. ``Popen.communicate(timeout=…)``
then blocks waiting for an EOF that never comes; when the timeout fires,
the cleanup path re-enters ``communicate()`` *without* a timeout under
the (here-false) assumption that "the child is dead so reads should EOF
immediately" — and we deadlock anyway.

Redirecting to a ``tempfile.TemporaryFile`` instead of capturing through
pipes sidesteps the whole class of issue: the helper inherits the file
fd, that closes cleanly when we do, and the existing 30s timeout
reliably fires. We still get the diagnostic output for failure
messages, so the user-facing behaviour is unchanged on the happy path.

Discovered while retesting ``mycelium adapter add hermes --reinstall``
on a host with the systemd service installed.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(hermes): pin home_channel so send_message round-trips into the room

When the LLM invokes ``send_message`` to reply (instead of just returning
plain text, which auto-routes via ``event.source.chat_id``), hermes's
tool falls back to the per-platform ``home_channel`` whenever no
``:chat_id`` suffix is supplied on the ``target``. Without a home channel
set, the tool errors with ``"No home channel set for mycelium-room …"``
and the agent's reply never reaches the room — that's the smoke-test
regression we hit after restarting the gateway.

Two parts:

1. ``_register_room`` now writes a ``home_channel: {platform, chat_id,
   name}`` block into ``platforms.mycelium-room`` at register time, with
   ``chat_id = "<room>:<handle>"``. Last-write-wins across re-registers,
   mirroring hermes's own one-``HOME_CHANNEL``-env-var-per-platform UX.
   The adapter's ``send()`` ``rpartition``s the chat_id back into a room
   slug + sender_handle, so the fallback hits the right room and posts
   as the right agent.

   The ``platform`` field is mandatory: ``PlatformConfig.from_dict``
   loads home_channel via ``HomeChannel.from_dict``, which
   unconditionally reads ``data["platform"]`` and KeyErrors without it
   (hermes-agent gateway/config.py:230, hit during gateway boot →
   crash-loop). Including ``platform: "mycelium-room"`` keeps the
   gateway happy.

2. ``post_to_room`` now wraps the request in ``asyncio.wait_for`` instead
   of passing ``timeout=aiohttp.ClientTimeout(...)`` per call. aiohttp's
   per-call ``ClientTimeout`` uses ``asyncio.timeout()`` under the hood,
   which RuntimeErrors with "Timeout context manager should be used
   inside a task" when the call enters from the hermes tool_executor
   path (the executor bridges sync → loop in a way that breaks the
   task-scope assumption). ``asyncio.wait_for`` is task-agnostic.

Live smoke test: ``mycelium room post hermes-demo … "@Julia-Agent …"``
→ the gateway agent picks up the mention, ``send_message`` resolves via
the new home_channel, and the reply lands in the room. No more "No home
channel set" errors, no more "Timeout context manager" errors.

The "model misuses send_message as a cross-channel forward tool" quirk
(messages occasionally attributed to the originating sender's handle
rather than the agent's own) is intrinsic LLM behaviour, not a plumbing
bug — left as a v2 ergonomic improvement.

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(hermes): drop hermes_profile field/plumbing

The `hermes_profile` field on AgentManifest and the matching
threading through the install/dispatch facets, doctor, and tests was
plumbing-only — no CLI flag ever surfaced it, and hermes-agent's own
profile commands don't co-operate with profile-aware boot today.
First-class multi-profile support is deferred to hermes-agent#25660
(single gateway, multiple agents), which will replace the
per-profile-deployment model entirely with handle-level routing.

Until #25660 lands, Mycelium always targets whichever profile is
active on the host via `$HERMES_HOME` (or `~/.hermes/`); multi-profile
setups install per-profile by re-exporting `HERMES_HOME` between
runs. Docs section reworked to document this and point at the PR.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(hermes): inject mycelium handle into every dispatch + roadmap docs

Hermes-side personas never learn their mycelium handle from
persona/SOUL — the SKILL.md uses `<your-handle>` placeholders and the
plugin is the only thing that knows which handle was addressed. So
every Dispatch the plugin emits now surfaces the handle:

- format_tick_instruction() folds `@<handle>` into the negotiation
  header line; the error-tick variant replaces the bare "Room: …"
  line with "You are @<handle> in room …"
- _route_consensus + _route_broadcast prepend a one-line identity
  preamble ("[mycelium-room] You are @<handle> in room <room>.")
  to each per-recipient Dispatch
- SKILL.md adds a "Hermes quirks" bullet explaining that every
  inbound message tells the agent its handle, and the agent should
  use that in any --handle CLI fragment it issues

Also documents the post-#25660 roadmap in adapters.html under a new
Roadmap subsection: the planned migration from
`platforms.mycelium-room.extra.rooms[]` fan-out to per-handle
`routes:` entries, how mycelium handle ↔ hermes agent_id maps 1:1,
why `branding.agent_name` is irrelevant (TUI cosmetic, not a routing
identity), and how Mycelium will discover agent_ids by reading the
top-level `agents:` block in `~/.hermes/config.yaml` directly.

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(hermes): clarify handle vs. identity in plugin SKILL.md

Split the handle bullet into two: the concept (handle is a Mycelium
routing label, not the agent's identity or banner name) and the
mechanic (every dispatch tells the agent what handle to use in
--handle CLI fragments). The model was occasionally treating
"@<handle>" as part of its persona; this is the bullet that tells it
otherwise.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(hermes): poll for active sessions to subscribe to session sub-rooms

The backend NOTIFY's coordination_join only on the session sub-room
channel, never on the parent room — so the parent-room SSE never sees
joins, and the prior _route_join check (`":session:" in room_name`)
was a chicken-and-egg: the only way it triggered was if we were
already subscribed to the session sub-room, which is the very thing
we needed the join to bootstrap.

Mirror the openclaw plugin's pattern: poll
/api/coordination-sessions every 5s and (re-)subscribe to every
active session sub-room. _spawn_session_sub is idempotent, so
re-polling is cheap. Sessions in terminal states are unsubscribed.

Also keep a defensive content-extract fallback in _route_join so a
future schema change (joins fanned out to the parent room channel)
would Just Work — costs nothing today.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(hermes): hub-and-spoke guide + restart-confirm + docgen hardening

Three concerns from setting up Hermes spokes on oclw3/oclw5 against
oclw4 as hub:

1. **Restart-confirm.** `mycelium adapter add hermes` and
   `mycelium agent create --adapter hermes` previously returned as soon
   as `hermes gateway restart` was *scheduled*, not when the new
   process was up — operators were left grepping `~/.hermes/logs/
   agent.log` to confirm the plugin had subscribed (the success line
   doesn't reach stderr/journalctl). `_await_gateway_subscribed` now
   anchors the log size before issuing the restart, polls for the
   post-restart `subscribed to N room(s)` line, and prints
   `✓ hermes-gateway subscribed to N room(s)` or a yellow timeout
   warning with the SIGKILL fallback. Covers all four call sites
   (`adapter add/remove`, `agent create/rm`).

2. **Hub-and-spoke (Hermes) guide.** New `hub-and-spoke-hermes.md`
   documenting the four surprises the spoke setup hit: the
   `GATEWAY_ALLOW_ALL_USERS=true` prerequisite (Hermes ships with
   user allowlists closed and silently drops hub-originated dispatch
   without it), the `model:` block prerequisite, the `mycelium init`
   no-op on pre-existing configs, and the `agent create` restart-race
   (now resolved by #1 but documented as a Troubleshooting entry for
   the timeout-warning path).

3. **Docgen hardening.** The hand-rolled markdown parser in
   `generate_docs.py` had two latent infinite-loop bugs: a bare `>`
   (blockquote paragraph break) and `####`+ headers both fell through
   to the paragraph collector, which exits without incrementing the
   line cursor and spun the outer loop forever. Hit case 1 while
   regenerating docs for this commit. Fixes: blockquote branch now
   accepts bare `>`, new H4–H6 branch, and a forward-progress guard
   that warns on stderr and skips any unhandled line so future
   markdown can't silently hang the build.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(hermes): return-address hardening, loop-suppression, config sync

return_address.py:
- Add hook-written sidecar (.mycelium-return-origin.json) as primary
  home-channel source; sessions.json scan becomes the fallback
- Add write debounce (_SIDECAR_REFRESH_S=300) so the pre_gateway_dispatch
  hook skips disk writes when the home channel hasn't changed
- Extract _parse_ts() to eliminate the wrapper-dict call pattern
- Fix redundant .strip() calls in _home_from_origin_dict
- Promote logger to module-level; bare returns in -> None functions

adapter.py:
- Fix _own_message_ids eviction: replace set(list(...)[-512:]) with a
  companion deque that preserves insertion order
- Remove redundant rstrip('/') in _poll_active_sessions (already stripped
  in __init__)

plugin.yaml:
- Declare provides_hooks: [pre_gateway_dispatch] to match __init__.py

install.py / dispatch.py / doctor.py:
- Extract _read_gateway_pid() -> int | None; handles both JSON {"pid": N}
  and legacy plain-integer pid file formats
- Replace duplicated inline PID-parsing in dispatch.py and doctor.py

tests/test_hermes_return_address.py:
- Fix ty errors: assert spec non-None, ty: ignore on dynamic ModuleType
  attribute assignments, assert await_args non-None before .kwargs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(hermes): declare hermes assets as package data in setuptools

integrations/hermes/assets/**/* was missing from
[tool.setuptools.package-data], so non-.py files (plugin.yaml,
skills/) were excluded from installed packages. On editable installs
the filesystem is live so copytree found the files, but the stale
egg-info SOURCES.txt didn't list plugin.yaml — causing it to be
silently omitted during `mycelium adapter add hermes` on non-editable
installs and making the doctor check fail with "manifest missing".

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(hermes): configured home channel wins over stale sidecar in return-address

Change return_address.py resolution order so that the gateway's
explicitly configured home channel (MATRIX_HOME_ROOM via
config.platforms.matrix.home_channel, read from the live
GatewayRunner) is always priority 1. The .mycelium-return-origin.json
sidecar falls back to priority 2, sessions.json to priority 3.

This prevents a stale sidecar from a previous infinite-loop session
from overriding the MATRIX_HOME_ROOM set for the current test run.

Also deploy the fix to both hub (oclw4) and spoke1 (oclw3) nodes via
their uv tool install + hermes adapter reinstall.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(hermes): remove cross-channel notify-home and return-address

Drops the entire return-address sidecar and notify-home delivery path
introduced in 28aca63. Hermes agents now live exclusively in mycelium
rooms — there is no cross-channel hop to bridge, so the machinery
(return_address.py, notify_home.py, home_channel config pinning,
pre_gateway_dispatch hook, NotifyHome/StashReturnAddress route actions)
is dead code. Removes ~600 lines from the plugin and ~300 from tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(hermes): housekeeping — protocol, tests, docs, SKILL.md

- protocol.py: add "hermes" to AGENT_ADAPTERS frozenset and adapter
  Literal; fix docstring adapter count (three → four)
- test_followups_e2e_fixes.py: patch reload_daemon_service (not restart)
  in both positive and negative cold-spawn tests; update module docstring
- SKILL.md: fix dangling "Channel Messaging" cross-reference (section
  was renamed to "Talking to other agents")
- hermes-setup.md: remove stale Matrix warning section; add
  require_mention: true to example config block
- hub-and-spoke-hermes.md: remove gateway_restart_notification note
  (Matrix-only, no mycelium-room equivalent)
- docs/adapters.html: regenerated

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(hermes): poll active sessions immediately on gateway startup

The session poller slept _POLL_INTERVAL_S (5s) before its first poll.
When session_create + session_join fired immediately after the gateway
restarted (as the test harness does), the CFN fanned out first-round
ticks before the plugin had subscribed to the session sub-room SSE.
Those ticks were never delivered to the agents; the CFN's 300s round
watchdog fired and the session closed broken=True.

Move the sleep to the end of the loop body (and into the error paths)
so the first poll fires immediately after the gateway connects. The
plugin discovers the already-active session sub-room within milliseconds
of starting and subscribes before any ticks are missed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs(hermes): tighten SKILL.md negotiation guidance from E2E triage

Correct counter-offer value validation docs to match client-side CLI
rejection, and add Hermes-specific rules for direct mycelium invocation,
--handle vs plan commands, and memory-full retry loops.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(openclaw): build and stage dist/ before host-native reinstall

OpenClaw 2026.5+ validates plugins.entries.mycelium against
~/.openclaw/extensions/mycelium/dist/index.js on every CLI call. The
--reinstall path copied TypeScript-only source (dist/ excluded), then
built in the package tree, so `openclaw plugins install` failed before
the post-install dist copy could run.

Build first, include dist/ in the reinstall copy, and sync dist/ to the
extension dir before plugins install. Adds a host-path regression test
mirroring the container fix from mycelium-io#347.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(daemon): make `daemon restart` a real process restart

SIGHUP reload stays automatic on agent create/rm and subscribe/unsubscribe;
only explicit restart should recycle the systemd/launchd service.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(doctor): skip CFN intent check on spoke nodes

CFN URLs are hub-only; leftover server.mas_id on spokes should not warn.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(doctor): query hub for room MAS IDs on spoke nodes

Spokes verify CFN room registration via the remote backend instead of
skipping when local CFN URLs are unset.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(doctor): report /api/rooms HTTP errors instead of unreachable

When the hub backend responds but room listing fails (e.g. migration drift),
spoke doctor should warn with the status code rather than a silent skip.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): ruff-format openclaw install helper

Unblocks CLI lint on feat/hermes after the shipped-dist install refactor.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Julia Valenti <juliarvalenti@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant