fix(web-server): move event channel state from module globals to app.state - #37683
Merged
Conversation
…state
Module-level asyncio.Lock() binds to whatever event loop was active at
import time. When the same web_server module is reused across multiple
TestClient instances (or across uvicorn reloads), the old lock still
references a defunct loop, causing 'attached to a different loop' errors
and flaky subscriber-registration races in CI.
Replace the module-level _event_channels dict + _event_lock with:
- _lifespan() async context manager that creates both on the running
event loop during FastAPI startup (guaranteed correct loop binding)
- _get_event_state() lazy accessor that initialises on app.state when
TestClient is used without a `with` block (preserves backward compat)
All call sites (_broadcast_event, /api/pub, /api/events) now receive the
app reference and read state via _get_event_state(app) instead of the
module globals. The test polling loop is updated to check
app.state.event_channels rather than the removed module attribute.
Contributor
🔎 Lint report:
|
This was referenced Jun 3, 2026
Yuki-14544869
pushed a commit
to Yuki-14544869/hermes-agent
that referenced
this pull request
Jun 4, 2026
…state (NousResearch#37683) Module-level asyncio.Lock() binds to whatever event loop was active at import time. When the same web_server module is reused across multiple TestClient instances (or across uvicorn reloads), the old lock still references a defunct loop, causing 'attached to a different loop' errors and flaky subscriber-registration races in CI. Replace the module-level _event_channels dict + _event_lock with: - _lifespan() async context manager that creates both on the running event loop during FastAPI startup (guaranteed correct loop binding) - _get_event_state() lazy accessor that initialises on app.state when TestClient is used without a `with` block (preserves backward compat) All call sites (_broadcast_event, /api/pub, /api/events) now receive the app reference and read state via _get_event_state(app) instead of the module globals. The test polling loop is updated to check app.state.event_channels rather than the removed module attribute.
davidgut1982
pushed a commit
to davidgut1982/hermes-agent
that referenced
this pull request
Jun 5, 2026
…state (NousResearch#37683) Module-level asyncio.Lock() binds to whatever event loop was active at import time. When the same web_server module is reused across multiple TestClient instances (or across uvicorn reloads), the old lock still references a defunct loop, causing 'attached to a different loop' errors and flaky subscriber-registration races in CI. Replace the module-level _event_channels dict + _event_lock with: - _lifespan() async context manager that creates both on the running event loop during FastAPI startup (guaranteed correct loop binding) - _get_event_state() lazy accessor that initialises on app.state when TestClient is used without a `with` block (preserves backward compat) All call sites (_broadcast_event, /api/pub, /api/events) now receive the app reference and read state via _get_event_state(app) instead of the module globals. The test polling loop is updated to check app.state.event_channels rather than the removed module attribute.
changman
pushed a commit
to changman/hermes-agent
that referenced
this pull request
Jun 10, 2026
…state (NousResearch#37683) Module-level asyncio.Lock() binds to whatever event loop was active at import time. When the same web_server module is reused across multiple TestClient instances (or across uvicorn reloads), the old lock still references a defunct loop, causing 'attached to a different loop' errors and flaky subscriber-registration races in CI. Replace the module-level _event_channels dict + _event_lock with: - _lifespan() async context manager that creates both on the running event loop during FastAPI startup (guaranteed correct loop binding) - _get_event_state() lazy accessor that initialises on app.state when TestClient is used without a `with` block (preserves backward compat) All call sites (_broadcast_event, /api/pub, /api/events) now receive the app reference and read state via _get_event_state(app) instead of the module globals. The test polling loop is updated to check app.state.event_channels rather than the removed module attribute.
alt-glitch
pushed a commit
that referenced
this pull request
Jun 14, 2026
…state (#37683) Module-level asyncio.Lock() binds to whatever event loop was active at import time. When the same web_server module is reused across multiple TestClient instances (or across uvicorn reloads), the old lock still references a defunct loop, causing 'attached to a different loop' errors and flaky subscriber-registration races in CI. Replace the module-level _event_channels dict + _event_lock with: - _lifespan() async context manager that creates both on the running event loop during FastAPI startup (guaranteed correct loop binding) - _get_event_state() lazy accessor that initialises on app.state when TestClient is used without a `with` block (preserves backward compat) All call sites (_broadcast_event, /api/pub, /api/events) now receive the app reference and read state via _get_event_state(app) instead of the module globals. The test polling loop is updated to check app.state.event_channels rather than the removed module attribute.
kossteg
pushed a commit
to kossteg/hermes-agent
that referenced
this pull request
Jun 16, 2026
…state (NousResearch#37683) Module-level asyncio.Lock() binds to whatever event loop was active at import time. When the same web_server module is reused across multiple TestClient instances (or across uvicorn reloads), the old lock still references a defunct loop, causing 'attached to a different loop' errors and flaky subscriber-registration races in CI. Replace the module-level _event_channels dict + _event_lock with: - _lifespan() async context manager that creates both on the running event loop during FastAPI startup (guaranteed correct loop binding) - _get_event_state() lazy accessor that initialises on app.state when TestClient is used without a `with` block (preserves backward compat) All call sites (_broadcast_event, /api/pub, /api/events) now receive the app reference and read state via _get_event_state(app) instead of the module globals. The test polling loop is updated to check app.state.event_channels rather than the removed module attribute.
T02200059
pushed a commit
to T02200059/hermes-agent
that referenced
this pull request
Jun 18, 2026
…state (NousResearch#37683) Module-level asyncio.Lock() binds to whatever event loop was active at import time. When the same web_server module is reused across multiple TestClient instances (or across uvicorn reloads), the old lock still references a defunct loop, causing 'attached to a different loop' errors and flaky subscriber-registration races in CI. Replace the module-level _event_channels dict + _event_lock with: - _lifespan() async context manager that creates both on the running event loop during FastAPI startup (guaranteed correct loop binding) - _get_event_state() lazy accessor that initialises on app.state when TestClient is used without a `with` block (preserves backward compat) All call sites (_broadcast_event, /api/pub, /api/events) now receive the app reference and read state via _get_event_state(app) instead of the module globals. The test polling loop is updated to check app.state.event_channels rather than the removed module attribute.
xyshanren
pushed a commit
to xyshanren/hermes-agent-cn
that referenced
this pull request
Jun 25, 2026
…state (NousResearch#37683) Module-level asyncio.Lock() binds to whatever event loop was active at import time. When the same web_server module is reused across multiple TestClient instances (or across uvicorn reloads), the old lock still references a defunct loop, causing 'attached to a different loop' errors and flaky subscriber-registration races in CI. Replace the module-level _event_channels dict + _event_lock with: - _lifespan() async context manager that creates both on the running event loop during FastAPI startup (guaranteed correct loop binding) - _get_event_state() lazy accessor that initialises on app.state when TestClient is used without a `with` block (preserves backward compat) All call sites (_broadcast_event, /api/pub, /api/events) now receive the app reference and read state via _get_event_state(app) instead of the module globals. The test polling loop is updated to check app.state.event_channels rather than the removed module attribute.
waefrebeorn
pushed a commit
to waefrebeorn/slermes
that referenced
this pull request
Jul 2, 2026
…state (NousResearch#37683) Module-level asyncio.Lock() binds to whatever event loop was active at import time. When the same web_server module is reused across multiple TestClient instances (or across uvicorn reloads), the old lock still references a defunct loop, causing 'attached to a different loop' errors and flaky subscriber-registration races in CI. Replace the module-level _event_channels dict + _event_lock with: - _lifespan() async context manager that creates both on the running event loop during FastAPI startup (guaranteed correct loop binding) - _get_event_state() lazy accessor that initialises on app.state when TestClient is used without a `with` block (preserves backward compat) All call sites (_broadcast_event, /api/pub, /api/events) now receive the app reference and read state via _get_event_state(app) instead of the module globals. The test polling loop is updated to check app.state.event_channels rather than the removed module attribute.
santhreal
pushed a commit
to santhreal/hermes-agent
that referenced
this pull request
Jul 13, 2026
…state (NousResearch#37683) Module-level asyncio.Lock() binds to whatever event loop was active at import time. When the same web_server module is reused across multiple TestClient instances (or across uvicorn reloads), the old lock still references a defunct loop, causing 'attached to a different loop' errors and flaky subscriber-registration races in CI. Replace the module-level _event_channels dict + _event_lock with: - _lifespan() async context manager that creates both on the running event loop during FastAPI startup (guaranteed correct loop binding) - _get_event_state() lazy accessor that initialises on app.state when TestClient is used without a `with` block (preserves backward compat) All call sites (_broadcast_event, /api/pub, /api/events) now receive the app reference and read state via _get_event_state(app) instead of the module globals. The test polling loop is updated to check app.state.event_channels rather than the removed module attribute.
donbowman
pushed a commit
to donbowman/hermes-agent
that referenced
this pull request
Jul 13, 2026
…state (NousResearch#37683) Module-level asyncio.Lock() binds to whatever event loop was active at import time. When the same web_server module is reused across multiple TestClient instances (or across uvicorn reloads), the old lock still references a defunct loop, causing 'attached to a different loop' errors and flaky subscriber-registration races in CI. Replace the module-level _event_channels dict + _event_lock with: - _lifespan() async context manager that creates both on the running event loop during FastAPI startup (guaranteed correct loop binding) - _get_event_state() lazy accessor that initialises on app.state when TestClient is used without a `with` block (preserves backward compat) All call sites (_broadcast_event, /api/pub, /api/events) now receive the app reference and read state via _get_event_state(app) instead of the module globals. The test polling loop is updated to check app.state.event_channels rather than the removed module attribute.
Gravezzz
pushed a commit
to Gravezzz/hermes-agent
that referenced
this pull request
Jul 21, 2026
…state (NousResearch#37683) Module-level asyncio.Lock() binds to whatever event loop was active at import time. When the same web_server module is reused across multiple TestClient instances (or across uvicorn reloads), the old lock still references a defunct loop, causing 'attached to a different loop' errors and flaky subscriber-registration races in CI. Replace the module-level _event_channels dict + _event_lock with: - _lifespan() async context manager that creates both on the running event loop during FastAPI startup (guaranteed correct loop binding) - _get_event_state() lazy accessor that initialises on app.state when TestClient is used without a `with` block (preserves backward compat) All call sites (_broadcast_event, /api/pub, /api/events) now receive the app reference and read state via _get_event_state(app) instead of the module globals. The test polling loop is updated to check app.state.event_channels rather than the removed module attribute.
leewenjie
pushed a commit
to leewenjie/hermes-agent
that referenced
this pull request
Aug 7, 2026
…state (NousResearch#37683) Module-level asyncio.Lock() binds to whatever event loop was active at import time. When the same web_server module is reused across multiple TestClient instances (or across uvicorn reloads), the old lock still references a defunct loop, causing 'attached to a different loop' errors and flaky subscriber-registration races in CI. Replace the module-level _event_channels dict + _event_lock with: - _lifespan() async context manager that creates both on the running event loop during FastAPI startup (guaranteed correct loop binding) - _get_event_state() lazy accessor that initialises on app.state when TestClient is used without a `with` block (preserves backward compat) All call sites (_broadcast_event, /api/pub, /api/events) now receive the app reference and read state via _get_event_state(app) instead of the module globals. The test polling loop is updated to check app.state.event_channels rather than the removed module attribute.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Moves
_event_channelsand_event_lockout of module-level globals inhermes_cli/web_server.pyand ontoapp.state, initialised via a FastAPI lifespan context manager. This eliminates the class of test flake whereasyncio.Lock()created at import time binds to an event loop that is no longer running when a laterTestClient(or uvicorn reload) tries to use it.Root cause:
asyncio.Lock()captures the currently-running event loop at construction time. A module-level lock created during import binds to whatever loop was active then. When the module is reused in a newTestClientor after a uvicorn reload, the old lock still references a defunct loop, raisingattached to a different looperrors and causing subscriber-registration races under parallel test execution.Approach:
_lifespan()async context manager — createsevent_channelsdict +event_lockon the running loop during FastAPI startup_get_event_state(app)lazy accessor — falls back to on-demand init whenTestClientis used without awithblock, preserving backward compat for existing non-lifespan test setups_broadcast_event,/api/pub,/api/events) now receive theappreference and read state via_get_event_state(app)app.state.event_channelsinstead of the removed module-level_event_channelsRelated Issue
Fixes recurring CI flake in
test_web_server.pysubscriber-registration tests.Type of Change
Changes Made
hermes_cli/web_server.py— Replace module-level_event_channels/_event_lockwithapp.state-based state; add_lifespan()and_get_event_state(); update all consumers to passappexplicitlytests/hermes_cli/test_web_server.py— Update polling assertion fromws_mod._event_channelstows_mod.app.state.event_channelsHow to Test
python -m pytest tests/hermes_cli/test_web_server.py -q -k broadcastpython -m pytest tests/hermes_cli/test_web_server.py -qattached to a different looperrors appear across multiple sequentialTestClientusagesChecklist
Code
fix(scope):,feat(scope):, etc.)Documentation & Housekeeping