Skip to content

feat: real-time gateway session sync (Phase 1) - #274

Closed
bergeouss wants to merge 2 commits into
nesquena:masterfrom
bergeouss:feature/gateway-session-sync
Closed

feat: real-time gateway session sync (Phase 1)#274
bergeouss wants to merge 2 commits into
nesquena:masterfrom
bergeouss:feature/gateway-session-sync

Conversation

@bergeouss

Copy link
Copy Markdown
Contributor

Summary

Implements Phase 1 from #272 — real-time bidirectional sync between Gateway sessions and WebUI.

What this does

When the "Show agent sessions" checkbox is enabled (previously "Show CLI sessions"), gateway sessions from Telegram, Discord, Slack, and other messaging platforms now appear in the sidebar and update in real-time as new messages arrive.

Backend

  • ****: Background daemon thread that polls state.db every 5 seconds for gateway session changes, using MD5 hash of session IDs + timestamps for efficient change detection
  • ****: New SSE endpoint /api/sessions/gateway/stream for pushing detected changes to the browser
  • ****: Extended get_cli_sessions() to include all non-webui sources (telegram, discord, slack, etc.) with proper source tagging

Frontend

  • Dynamic source badges: telegram (blue), discord (purple), slack (dark purple), cli (green)
  • Auto-refresh via SSE when gateway sessions change
  • Renamed "Show CLI sessions" → "Show agent sessions"

Tests

  • 10 new tests in tests/test_gateway_sync.py covering metadata, filtering, SSE endpoint, and watcher lifecycle
  • All existing tests remain green (no regressions)

Architecture

Gateway (hermes-agent)
  └── writes to state.db (WAL mode)
        └── gateway_watcher.py polls every 5s
              └── SSE /api/sessions/gateway/stream
                    └── browser sidebar updates in real-time

Zero changes to hermes-agent — the WebUI reads the shared state.db that both components already access.

Addresses

Closes #272

bergeouss and others added 2 commits April 12, 2026 02:29
- Add gateway_watcher.py: background daemon polling state.db every 5s
  for gateway session changes (telegram, discord, slack, etc.)
- Extend get_cli_sessions() to include all non-webui sources
- Add SSE endpoint /api/sessions/gateway/stream for real-time push
- Add dynamic source badges (telegram=blue, discord=purple, slack=dark purple)
- Rename 'Show CLI sessions' to 'Show agent sessions'
- Wire watcher lifecycle into server start/stop
- 10 tests covering metadata, filtering, SSE, and watcher lifecycle
- Activated via the same checkbox as CLI session import

Addresses GitHub issue nesquena#272
- Fix critical SSE bug: frontend listened for 'gateway_session_update'
  but backend sends 'sessions_changed' -- events were silently dropped
- Fix frontend field check: data.changed -> data.sessions (matches
  the actual payload structure from gateway_watcher)
- Fix TLS: ssl.TLSv1_2 -> ssl.TLSVersion.TLSv1_2 (the bare attribute
  does not exist, would crash TLS setup and silently fall back to HTTP)
- Remove PLAN.md: implementation plan should not be committed to repo

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@nesquena

Copy link
Copy Markdown
Owner

Full Review: PR #274 — real-time gateway session sync

Thanks @bergeouss! Solid architecture — polling state.db with hash-based change detection + SSE push is the right approach for real-time sync without modifying hermes-agent.

Security Audit

Clean. gateway_watcher.py uses static SQL queries with no user input (read-only SELECT from state.db). The SSE endpoint checks show_cli_sessions setting before opening and goes through the same auth path as other GET endpoints. No new external resources or injection vectors.

Bugs Found and Fixed (pushed to branch)

1. Critical: SSE event name mismatch
The backend sends events as sessions_changed but the frontend listened for gateway_session_update. Events were silently dropped — the real-time sync feature was completely non-functional. Fixed: frontend now listens for sessions_changed.

2. Frontend field check wrong
The frontend checked data.changed but the backend payload has data.sessions. Fixed: now checks data.sessions.

3. TLS attribute broken
ssl.TLSv1_2 was changed from the correct ssl.TLSVersion.TLSv1_2. The bare attribute doesn't exist — would crash TLS setup and silently fall back to HTTP. Fixed: restored ssl.TLSVersion.TLSv1_2. (This was an unrelated change sneaked into the PR.)

4. PLAN.md removed
Implementation plan file should not be committed to the repo. Removed.

Code Review

gateway_watcher.py — Well-designed:

  • GatewayWatcher class with clean subscriber pattern (subscribe/unsubscribe via queue.Queue)
  • MD5 hash of session IDs + timestamps for efficient change detection (avoids diffing full payloads)
  • Daemon thread with interruptible sleep (0.1s increments × 50 = 5s poll interval)
  • Dead subscriber cleanup on queue.Full — prevents slow consumers from blocking the watcher
  • Module-level singleton with thread-safe start/stop via _watcher_lock

routes.py — SSE endpoint is clean:

  • Checks setting before opening stream
  • Sends initial snapshot immediately (good UX — no 5s wait for first data)
  • 30s keepalive pings
  • Proper cleanup in finally block (unsubscribe)
  • Handles connection errors (BrokenPipeError, etc.)

models.py — Minimal, correct:

  • get_cli_sessions() now includes source field from state.db
  • Dynamic title based on source (Telegram Session, Discord Session, etc.)
  • source_tag set from actual source instead of hardcoded 'cli'

server.py — Watcher lifecycle correct:

  • Start after config but before serve_forever()
  • Stop in finally block on shutdown
  • Try/except around start so watcher failure doesn't prevent server startup

Frontend (sessions.js, boot.js, panels.js):

  • startGatewaySSE()/stopGatewaySSE() lifecycle tied to the setting toggle
  • EventSource auto-reconnects on error (built-in browser behavior)
  • Source-specific CSS badges with platform colors (telegram blue, discord purple, slack dark purple)
  • data-source attribute on session items drives the CSS content: attr(data-source) badge — clean pattern

Tests — Comprehensive (10 tests):
Gateway sessions appear/hidden based on setting, metadata correctness, message count, multi-source, deduplication with WebUI sessions, SSE endpoint exists, no state.db graceful handling, CLI backward compat.

Notes

  • The WHERE s.source IS NOT NULL AND s.source != 'webui' filter in gateway_watcher.py includes CLI sessions too. This is consistent with the UI rename ("Show agent sessions" = all non-webui sources).
  • The LIMIT 200 on the gateway session query is a reasonable cap.
  • The i18n changes only update en and de labels but not zh/zh-Hant — those still say the old text. Minor, not blocking.

Tests

604 passed, 0 regressions (1 pre-existing failure from unmerged PR #243).

Verdict

Approved with the 4 fixes pushed. The architecture is sound, the feature works correctly now that the SSE event names match, and the tests are comprehensive.

@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Merged via PR #279 which cherry-picks both commits onto current master (avoiding a stale-base regression that would have deleted the Spanish locale). Three additional fixes on top of the 4 already applied:

  • Test state.db path mismatch: test_auth_sessions.py contaminates HERMES_WEBUI_STATE_DIR at import time, causing gateway tests to write state.db to the wrong path. Fixed by having conftest.py set HERMES_WEBUI_TEST_STATE_DIR explicitly.
  • Closed-connection cleanup: conn.close() was called inside try: blocks before finally: cleanup, causing silent ProgrammingError on test failure cleanup.
  • Slow-consumer sentinel: When a subscriber queue fills, the SSE handler was left blocked forever. Now sends a None sentinel so EventSource auto-reconnects.

658/658 tests pass. Feature verified in browser (Telegram/Discord/Slack badges render correctly, SSE endpoint live). Thanks @bergeouss!

nesquena-hermes added a commit that referenced this pull request Apr 12, 2026
* feat: add real-time gateway session sync (Phase 1)

- Add gateway_watcher.py: background daemon polling state.db every 5s
  for gateway session changes (telegram, discord, slack, etc.)
- Extend get_cli_sessions() to include all non-webui sources
- Add SSE endpoint /api/sessions/gateway/stream for real-time push
- Add dynamic source badges (telegram=blue, discord=purple, slack=dark purple)
- Rename 'Show CLI sessions' to 'Show agent sessions'
- Wire watcher lifecycle into server start/stop
- 10 tests covering metadata, filtering, SSE, and watcher lifecycle
- Activated via the same checkbox as CLI session import

Addresses GitHub issue #272

* fix: SSE event name mismatch, TLS attribute, remove PLAN.md

- Fix critical SSE bug: frontend listened for 'gateway_session_update'
  but backend sends 'sessions_changed' -- events were silently dropped
- Fix frontend field check: data.changed -> data.sessions (matches
  the actual payload structure from gateway_watcher)
- Fix TLS: ssl.TLSv1_2 -> ssl.TLSVersion.TLSv1_2 (the bare attribute
  does not exist, would crash TLS setup and silently fall back to HTTP)
- Remove PLAN.md: implementation plan should not be committed to repo

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: test isolation and slow-consumer sentinel in gateway sync

tests/test_gateway_sync.py:
- Fix _get_test_state_dir() path mismatch: the function was computing
  HERMES_HOME/webui-mvp-test but conftest.py sets HERMES_HOME=TEST_STATE_DIR,
  so state.db was written to a double-nested path the server never read.
  Now uses HERMES_WEBUI_STATE_DIR first (which conftest sets directly to
  TEST_STATE_DIR), fixing the 7/10 test failures in full-suite ordering.
- Fix conn cleanup: removed conn.close() from inside try blocks so the
  connection stays valid for _remove_test_sessions() in the finally block.
  Previously the closed conn caused ProgrammingError in finally (swallowed
  by bare except), leaving ghost sessions in state.db on test failure.

api/gateway_watcher.py:
- Fix slow-consumer queue eviction: when a subscriber queue fills (>10 events)
  and is removed from _subscribers, now puts a None sentinel into it so the
  SSE handler unblocks and closes the connection, letting EventSource
  auto-reconnect. Without this the connection stayed open but received no
  further events.

* fix: test isolation — set HERMES_WEBUI_TEST_STATE_DIR in conftest

The gateway sync tests write directly to state.db and must use the same
path the test server reads from.  Previously they computed the path
independently, which broke when test_auth_sessions.py set a different
HERMES_WEBUI_STATE_DIR in the test-process environment at import time.

tests/conftest.py:
- Set HERMES_WEBUI_TEST_STATE_DIR=TEST_STATE_DIR in the test process's
  os.environ (via setdefault) so gateway tests can read it reliably.
  Using setdefault preserves any explicit override the caller may pass.

tests/test_gateway_sync.py:
- Simplify _get_test_state_dir(): check HERMES_WEBUI_TEST_STATE_DIR first
  (now reliably set by conftest), fall back to HERMES_HOME/webui-mvp-test.
  Remove the workaround that tried to snapshot HERMES_HOME at import time.

Result: 658/658 tests pass in full-suite ordering (was 651 pass / 7 fail).

---------

Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@nesquena-hermes

Copy link
Copy Markdown
Collaborator

@bergeouss Can you confirm that in the latest master this is working as expected as anticipated?

@bergeouss

Copy link
Copy Markdown
Contributor Author

Confirmed! Running the latest ghcr.io/nesquena/hermes-webui:latest (v0.48.0+) on my Hetzner CX23 — the gateway session sync is working as expected. Sessions from the gateway are syncing in real-time to the WebUI. Great job on the cherry-pick and the additional fixes in #279. Thanks for the thorough review! 🚀

@nesquena

Copy link
Copy Markdown
Owner

@bergeouss Interested in being a core contributor to this WebUI project? Reach out to me via email (on my profile) or on Twitter if so!

JKJameson pushed a commit to JKJameson/hermes-webui that referenced this pull request Apr 25, 2026
* feat: add real-time gateway session sync (Phase 1)

- Add gateway_watcher.py: background daemon polling state.db every 5s
  for gateway session changes (telegram, discord, slack, etc.)
- Extend get_cli_sessions() to include all non-webui sources
- Add SSE endpoint /api/sessions/gateway/stream for real-time push
- Add dynamic source badges (telegram=blue, discord=purple, slack=dark purple)
- Rename 'Show CLI sessions' to 'Show agent sessions'
- Wire watcher lifecycle into server start/stop
- 10 tests covering metadata, filtering, SSE, and watcher lifecycle
- Activated via the same checkbox as CLI session import

Addresses GitHub issue nesquena#272

* fix: SSE event name mismatch, TLS attribute, remove PLAN.md

- Fix critical SSE bug: frontend listened for 'gateway_session_update'
  but backend sends 'sessions_changed' -- events were silently dropped
- Fix frontend field check: data.changed -> data.sessions (matches
  the actual payload structure from gateway_watcher)
- Fix TLS: ssl.TLSv1_2 -> ssl.TLSVersion.TLSv1_2 (the bare attribute
  does not exist, would crash TLS setup and silently fall back to HTTP)
- Remove PLAN.md: implementation plan should not be committed to repo

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: test isolation and slow-consumer sentinel in gateway sync

tests/test_gateway_sync.py:
- Fix _get_test_state_dir() path mismatch: the function was computing
  HERMES_HOME/webui-mvp-test but conftest.py sets HERMES_HOME=TEST_STATE_DIR,
  so state.db was written to a double-nested path the server never read.
  Now uses HERMES_WEBUI_STATE_DIR first (which conftest sets directly to
  TEST_STATE_DIR), fixing the 7/10 test failures in full-suite ordering.
- Fix conn cleanup: removed conn.close() from inside try blocks so the
  connection stays valid for _remove_test_sessions() in the finally block.
  Previously the closed conn caused ProgrammingError in finally (swallowed
  by bare except), leaving ghost sessions in state.db on test failure.

api/gateway_watcher.py:
- Fix slow-consumer queue eviction: when a subscriber queue fills (>10 events)
  and is removed from _subscribers, now puts a None sentinel into it so the
  SSE handler unblocks and closes the connection, letting EventSource
  auto-reconnect. Without this the connection stayed open but received no
  further events.

* fix: test isolation — set HERMES_WEBUI_TEST_STATE_DIR in conftest

The gateway sync tests write directly to state.db and must use the same
path the test server reads from.  Previously they computed the path
independently, which broke when test_auth_sessions.py set a different
HERMES_WEBUI_STATE_DIR in the test-process environment at import time.

tests/conftest.py:
- Set HERMES_WEBUI_TEST_STATE_DIR=TEST_STATE_DIR in the test process's
  os.environ (via setdefault) so gateway tests can read it reliably.
  Using setdefault preserves any explicit override the caller may pass.

tests/test_gateway_sync.py:
- Simplify _get_test_state_dir(): check HERMES_WEBUI_TEST_STATE_DIR first
  (now reliably set by conftest), fall back to HERMES_HOME/webui-mvp-test.
  Remove the workaround that tried to snapshot HERMES_HOME at import time.

Result: 658/658 tests pass in full-suite ordering (was 651 pass / 7 fail).

---------

Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SysAdminDoc pushed a commit to SysAdminDoc/hermes-webui that referenced this pull request Jun 26, 2026
* feat: add real-time gateway session sync (Phase 1)

- Add gateway_watcher.py: background daemon polling state.db every 5s
  for gateway session changes (telegram, discord, slack, etc.)
- Extend get_cli_sessions() to include all non-webui sources
- Add SSE endpoint /api/sessions/gateway/stream for real-time push
- Add dynamic source badges (telegram=blue, discord=purple, slack=dark purple)
- Rename 'Show CLI sessions' to 'Show agent sessions'
- Wire watcher lifecycle into server start/stop
- 10 tests covering metadata, filtering, SSE, and watcher lifecycle
- Activated via the same checkbox as CLI session import

Addresses GitHub issue nesquena#272

* fix: SSE event name mismatch, TLS attribute, remove PLAN.md

- Fix critical SSE bug: frontend listened for 'gateway_session_update'
  but backend sends 'sessions_changed' -- events were silently dropped
- Fix frontend field check: data.changed -> data.sessions (matches
  the actual payload structure from gateway_watcher)
- Fix TLS: ssl.TLSv1_2 -> ssl.TLSVersion.TLSv1_2 (the bare attribute
  does not exist, would crash TLS setup and silently fall back to HTTP)
- Remove PLAN.md: implementation plan should not be committed to repo


* fix: test isolation and slow-consumer sentinel in gateway sync

tests/test_gateway_sync.py:
- Fix _get_test_state_dir() path mismatch: the function was computing
  HERMES_HOME/webui-mvp-test but conftest.py sets HERMES_HOME=TEST_STATE_DIR,
  so state.db was written to a double-nested path the server never read.
  Now uses HERMES_WEBUI_STATE_DIR first (which conftest sets directly to
  TEST_STATE_DIR), fixing the 7/10 test failures in full-suite ordering.
- Fix conn cleanup: removed conn.close() from inside try blocks so the
  connection stays valid for _remove_test_sessions() in the finally block.
  Previously the closed conn caused ProgrammingError in finally (swallowed
  by bare except), leaving ghost sessions in state.db on test failure.

api/gateway_watcher.py:
- Fix slow-consumer queue eviction: when a subscriber queue fills (>10 events)
  and is removed from _subscribers, now puts a None sentinel into it so the
  SSE handler unblocks and closes the connection, letting EventSource
  auto-reconnect. Without this the connection stayed open but received no
  further events.

* fix: test isolation — set HERMES_WEBUI_TEST_STATE_DIR in conftest

The gateway sync tests write directly to state.db and must use the same
path the test server reads from.  Previously they computed the path
independently, which broke when test_auth_sessions.py set a different
HERMES_WEBUI_STATE_DIR in the test-process environment at import time.

tests/conftest.py:
- Set HERMES_WEBUI_TEST_STATE_DIR=TEST_STATE_DIR in the test process's
  os.environ (via setdefault) so gateway tests can read it reliably.
  Using setdefault preserves any explicit override the caller may pass.

tests/test_gateway_sync.py:
- Simplify _get_test_state_dir(): check HERMES_WEBUI_TEST_STATE_DIR first
  (now reliably set by conftest), fall back to HERMES_HOME/webui-mvp-test.
  Remove the workaround that tried to snapshot HERMES_HOME at import time.

Result: 658/658 tests pass in full-suite ordering (was 651 pass / 7 fail).

---------

Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: Nathan Esquenazi <nesquena@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.

Real-time bidirectional sync between Gateway sessions and WebUI

3 participants