Skip to content

fix(buzz): discover newly joined channels dynamically - #74823

Closed
cmyk wants to merge 3 commits into
NousResearch:mainfrom
Peakhunter:fix/buzz-dynamic-channels
Closed

fix(buzz): discover newly joined channels dynamically#74823
cmyk wants to merge 3 commits into
NousResearch:mainfrom
Peakhunter:fix/buzz-dynamic-channels

Conversation

@cmyk

@cmyk cmyk commented Jul 30, 2026

Copy link
Copy Markdown

What does this PR do?

Hermes currently snapshots Buzz channels at startup. When the Buzz identity joins or leaves an ordinary channel later, the running Gateway can miss the new channel indefinitely or retain a stale subscription until restart.

This refresh keeps automatic channel discovery synchronized for the lifetime of the Gateway while preserving explicitly configured channel restrictions.

When channels / BUZZ_CHANNELS is absent or empty, the adapter now:

  • seeds from an authoritative snapshot of channels the identity has joined;
  • subscribes to new joins without restart;
  • reconciles leaves against a fresh authoritative snapshot;
  • handles joins racing with removal reconciliation;
  • rejects stale join events so removed channels cannot be resurrected;
  • rebuilds joined-channel and DM state after reconnect;
  • preserves replay floors so history is not dispatched as new work;
  • allocates collision-safe WebSocket subscription IDs;
  • mutates subscription state only after successful REQ / CLOSE sends;
  • keeps bounded deduplication and polling rediscovery fallback.

When channels are explicitly configured, they remain a restrictive static watch set. Runtime membership events cannot broaden or remove them.

The Buzz user guide now documents both modes and the reconnect/replay behavior.

Related Issue

No linked issue. This replaces the stale/conflicting implementation already proposed in this PR; it is not a duplicate contribution.

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 🔒 Security fix
  • 📝 Documentation update
  • ✅ Tests (adding or improving test coverage)
  • ♻️ Refactor (no behavior change)
  • 🎯 New skill (bundled or hub)

Changes Made

  • plugins/platforms/buzz/adapter.py
    • Add authoritative joined-channel discovery and runtime reconciliation.
    • Rebuild dynamic state on reconnect and preserve replay-safe cursors.
    • Reconcile removals without losing concurrent joins.
    • Preserve strict behavior for explicit static channel configuration.
    • Make WebSocket subscription bookkeeping collision-safe and successful-send-only.
  • tests/gateway/test_buzz_adapter.py
    • Cover startup discovery, live joins, removals, stale/out-of-order events, concurrent joins, reconnects, replay floors, failed WebSocket sends, static channels, polling fallback, and bounded state.
  • website/docs/user-guide/messaging/buzz.md
    • Document dynamic joined-channel mode, restrictive static mode, reconnect rebuilding, and polling fallback.

How to Test

  1. Run:
    uv run pytest -q tests/gateway/test_buzz_adapter.py tests/gateway/test_buzz_websocket.py
    Expected: 50 passed.
  2. Run:
    uv run ruff check plugins/platforms/buzz/adapter.py tests/gateway/test_buzz_adapter.py tests/gateway/test_buzz_websocket.py
    Expected: All checks passed.
  3. Run:
    python -m py_compile plugins/platforms/buzz/adapter.py tests/gateway/test_buzz_adapter.py tests/gateway/test_buzz_websocket.py
    Expected: exit status 0.
  4. Canonical Linux CI installs the pinned test environment with:
    uv sync --locked --python 3.11 --extra all --extra dev --extra anthropic --extra mistral --extra fal --extra modal --extra daytona --extra hindsight --extra parallel-web
    and then runs files in isolated subprocesses through scripts/run_tests.sh. The two changed Buzz test files pass independently as reported above.
  5. Run:
    git diff --check upstream/main..HEAD
    Expected: no output and exit status 0.

Noncanonical monolithic-suite note

A single shared pytest tests/ process is not the repository's CI contract. It fails on pristine current upstream/main and this branch at the same first node after the same 706-test prefix:

tests/agent/test_auxiliary_main_first.py::TestResolveVisionCustomProvider::test_custom_main_forwards_runtime_endpoint

The node passes alone on both trees, proving an upstream order-dependent global-state leak. Current CI avoids this class intentionally by running each file in a fresh subprocess (.github/workflows/tests.yml, lines 120-132). The lifecycle branch does not modify provider or auxiliary-client code.

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run one monolithic pytest tests/ process and all tests pass — not checked: this is not canonical CI and current main reproduces the same order-dependent failure; changed test files pass in CI-style isolation
  • I've added tests for my changes
  • I've tested on my platform: Linux

Documentation & Housekeeping

  • I've updated relevant documentation
  • cli-config.yaml.example: N/A; no config keys were added or changed
  • CONTRIBUTING.md / AGENTS.md: N/A; no contributor workflow changed
  • Cross-platform impact considered; implementation uses existing Python, CLI, and WebSocket abstractions
  • Tool descriptions/schemas: N/A

Screenshots / Logs

50 passed
All checks passed
py_compile: passed
git diff --check: passed
monolithic shared-process baseline: same first failure on upstream/main and branch after 706 passed; node passes alone on both

@alt-glitch alt-glitch added type/bug Something isn't working P3 Low — cosmetic, nice to have comp/plugins Plugin system and bundled plugins sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages labels Jul 30, 2026
@ScaleLeanChris

Copy link
Copy Markdown
Contributor

Tested the exact PR head 8570eae454225b1b2db18e4da825c5f3f50990c5 on 2026-07-30.

Validation:

  • git diff --check passed.
  • Ruff passed for the changed adapter and test files.
  • Focused Buzz adapter and WebSocket suite: 31 passed, 0 failed.
  • Full gateway suite: 4,423 passed, 3 failed. I reran the same three failures on untouched current main (b4f8c491d3452926deb7628edbdb6fe2a85ff576) and got identical results. Two fixtures use now-stale fixed runtime timestamps. The third tries to bind a Linux systemd abstract socket on macOS. These failures are not introduced by this PR.
  • I applied fix(buzz): publish presence and preserve DM reply topology #74507 and this PR, in that order, onto current main. Both applied without conflicts. The combined focused Buzz suite passed: 51 passed, 0 failed.

Live gateway test:

  1. Deployed the exact PR head to an isolated Fly.io Hermes gateway.
  2. Confirmed the running gateway initially belonged to 2 channels.
  3. Created a private test channel after gateway startup and added the Fly identity.
  4. Confirmed the member-channel count changed from 2 to 3.
  5. Sent an addressed @Fly message with the explicit p tag.
  6. Received the unique reply token from the same gateway PID, with no restart.
  7. Deleted the temporary channel and confirmed the count returned to 2.

Result: the new membership subscription discovers and services a channel joined after startup. From this test lane, the change works as designed and is compatible with #74507. New channels are found “Right Here, Right Now.”

Copy link
Copy Markdown
Contributor

Fresh integration verifier run on 2026-07-30.

Tested the exact #74823 head 8570eae after #74507 on current main acfd376. The combined source applied cleanly.

  • git diff --check origin/main..HEAD: pass
  • Ruff on the changed Buzz adapter and tests: pass
  • Canonical Buzz suites: 51 passed
  • Full gateway suite on macOS: 4,445 passed; the sole failure is the known Linux-only abstract systemd socket test, not a Buzz regression
  • Isolated Fly.io Ubuntu 24.04 gateway deployed with adapter SHA-256 75bf932e…d6b9381
  • Created a new private ordinary channel after the gateway was already running
  • Added the Fly identity as a member and confirmed it appeared in the agent's member-channel list without a restart
  • Sent an addressed exact-marker message and received the expected reply
  • Deleted the temporary channel successfully

Live dynamic-channel discovery is green. The tests covered the intended remote-gateway path.

@teknium1

Copy link
Copy Markdown
Contributor

Thanks for the focused Buzz fix. The current-main behavior matches the reported premise: startup uses unfiltered channels list at plugins/platforms/buzz/adapter.py:490, while membership events only rediscover DMs at plugins/platforms/buzz/adapter.py:801-813.

The PR's member-filtered snapshot and pre-snapshot membership cursor (plugins/platforms/buzz/adapter.py:489-496 on 8570eae) address the startup race. Its joined-channel discovery preserves explicit allowlists and escalates transient discovery failures into the existing reconnect loop (plugins/platforms/buzz/adapter.py:808-815,933-957). The focused tests cover the relevant success, allowlist, cursor, and retry paths (tests/gateway/test_buzz_adapter.py:387-509).

Automated hermes-sweeper review.

@teknium1 teknium1 added the sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform label Jul 30, 2026
@riyaazd29

Copy link
Copy Markdown

I ran two additional live canaries on this PR's behavior and stacked lifecycle hardening directly on the exact PR head rather than opening a competing upstream PR:

Peakhunter#1

The follow-up covers malformed member snapshots, stale add/remove membership events, pre-join replay floors, reconnect reconciliation, leaving/pruning channels, polling fallback discovery, and forum-event isolation.

Verification:

  • focused Buzz adapter/WebSocket suite: 43 passed
  • full gateway suite: 4,470 passed, 2 pre-existing failures reproduced unchanged on 8570eae
  • Ruff, compile, and diff checks: passed
  • live: two independent macOS gateways discovered channels created after startup and replied immediately without another restart

The branch is one commit on top of 8570eae, so Reinhold's original commit and authorship remain intact.

@riyaazd29

Copy link
Copy Markdown

Correction to my hardening follow-up: an independent review found three blocking lifecycle race/replay paths in the current stacked commit. I have marked the follow-up as blocked and am adding deterministic regressions before asking anyone to merge it. The original #74823 result and live discovery proof are unaffected.

@GottZ

GottZ commented Aug 3, 2026

Copy link
Copy Markdown

This was generated by AI during triage.

Graph note (no action implied — a maintainer has already reviewed this thread).

Our triage graph places this PR in a complex with 1 related pull request (#76823). They were checked against each other at the diff level and no consolidation is indicated — they address distinct causes.

Full neighbourhood: https://hermes-triage.gottz.de/?node=74823

This note exists so the relationship stays discoverable from the thread itself.

@reinhold-ph
reinhold-ph force-pushed the fix/buzz-dynamic-channels branch from e2e3797 to b16e7a5 Compare August 14, 2026 15:42
@alt-glitch alt-glitch added comp/gateway Gateway runner, session dispatch, delivery sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades and removed comp/gateway Gateway runner, session dispatch, delivery labels Aug 14, 2026
@cmyk

cmyk commented Aug 20, 2026

Copy link
Copy Markdown
Author

Superseded by #90802, which carries this behavior forward as part of the profile-scoped Buzz policy and Config integration on current main. Closing this earlier proposal so maintainers have one implementation to review.

@cmyk cmyk closed this Aug 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/plugins Plugin system and bundled plugins needs-decision Awaiting maintainer decision before any implementation P3 Low — cosmetic, nice to have sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants