Skip to content

fix(gateway): defer cron scheduler and kanban init to prevent Windows event loop stall - #73605

Open
webtecnica wants to merge 5 commits into
NousResearch:mainfrom
webtecnica:fix/windows-desktop-event-loop-stall
Open

fix(gateway): defer cron scheduler and kanban init to prevent Windows event loop stall#73605
webtecnica wants to merge 5 commits into
NousResearch:mainfrom
webtecnica:fix/windows-desktop-event-loop-stall

Conversation

@webtecnica

Copy link
Copy Markdown
Contributor

Fixes #73435 (P1) — ~45s asyncio event loop stall on Windows during hermes serve startup.\n\nRoot cause: cron scheduler + kanban dispatcher initialization (YAML parsing, SQLite I/O) ran synchronously on the event loop thread, stalling WebSocket ready frame delivery.\n\nFix: Deferred heavy init to after gateway.ready signal via deferred-start pattern and on_ready callback.\n\nFiles: gateway/run.py, web_server.py, tui_gateway/ws.py

webtecnica and others added 5 commits July 27, 2026 13:14
Closes NousResearch#73175

The existing  only checked top-level
key absence. Arguments that violated , , ,
nested , , or  constraints were
dispatched blind, producing opaque downstream failures that cheap models would
loop on.

Fix uses  to catch all non-type constraint violations
before dispatch. Type mismatches are intentionally tolerated — Hermes'
downstream  already handles arbitrary type coercion, and the
validator should not be the only source of type repair for deferred tools.

The existing required-only fast-path is preserved for backward compatibility
and nicer error formatting when top-level required keys are missing.

Safety: the entire validator is wrapped in try/except, so any unexpected
schema format or exception falls through to blind dispatch (same as before).
Type errors (validator == 'type') are silently passed through.
Some OpenAI-compatible TTS backends (e.g. Speaches/Kokoro, custom
self-hosted servers) only support a subset of response_formats and
reject opus with HTTP 400/422. When _generate_openai_tts() sends
response_format=opus (derived from the .ogg output path), these
backends fail before any bytes are produced.

Fix:
- Add _is_response_format_rejection() to identify 400/422 errors
  that mention response_format.
- Add _create_speech_with_format_fallback() that retries once with
  mp3 on format rejection.
- Add _repair_mp3_in_ogg_container() that detects mp3-in-ogg by
  peeking at the file header and transcodes to real Ogg/Opus via
  ffmpeg.
- Wire the fallback into _generate_openai_tts() and always run the
  container repair for .ogg paths (it is a no-op on well-formed Ogg
  data).

Closes NousResearch#73470
… event loop stall

Defer heavy cron/kanban initialization to after gateway.ready signal,
preventing ~45s event loop stall on Windows.

Changes:
- gateway/run.py: Move resolve_cron_scheduler() and profile discovery
  into the cron daemon thread target so synchronous YAML config I/O
  never blocks the event loop during start_gateway().
- hermes_cli/web_server.py: Defer the desktop cron scheduler start from
  the lifespan handler to the first WebSocket connection's on_ready
  callback (after gateway.ready is delivered).
- tui_gateway/ws.py: Add optional on_ready callback to handle_ws(),
  fired after gateway.ready is sent, so callers can defer heavy init.

Closes NousResearch#73435
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/gateway Gateway runner, session dispatch, delivery comp/cli CLI entry point, hermes_cli/, setup wizard comp/tui Terminal UI (ui-tui/ + tui_gateway/) tool/tts Text-to-speech and transcription platform/windows Native Windows-specific behavior or breakage sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows labels Jul 28, 2026
@webtecnica

Copy link
Copy Markdown
Contributor Author

@teknium1 This PR defers cron scheduler and kanban init to prevent Windows event loop stall. P2 bug affecting Windows users. Ready for review.

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for isolating a plausible off-loop startup direction. Current main still resolves the gateway cron provider synchronously at gateway/run.py:25496-25500, but this branch needs correction before it can safely preserve scheduler lifecycle behavior.

Problems

  • gateway/run.py:24494 makes the provider local to _start_cron_in_thread, while the unchanged shutdown path still calls cron_provider.stop() at gateway/run.py:24582. That name is no longer bound, so shutdown does not call the provider's stop method.
  • The PR says kanban initialization is deferred, but the current dispatcher still performs config loading and kanban_db import before its first off-loop call (gateway/kanban_watchers.py:965-995); this PR does not alter that path.
  • The branch bundles four unrelated commits and is currently conflicting with main; the Windows startup salvage should be separated from those changes.

Suggested changes

  • Retain a shutdown-visible provider reference and test start/stop lifecycle behavior.
  • Defer the kanban bootstrap too, or reduce the scope and claim to cron/Desktop only.
  • Reconcile the focused change with the current web-server warmup path (hermes_cli/web_server.py:198-204).

Automated hermes-sweeper review.

Comment thread gateway/run.py
target=cron_provider.start,
args=(cron_stop,),
kwargs=cron_start_kwargs,
target=_start_cron_in_thread,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

_provider is local to this thread target, but the unchanged shutdown code later calls cron_provider.stop() at line 24582. Preserve a synchronized outer provider reference (including the shutdown-before-resolution case) and add a start/stop regression test; otherwise provider shutdown is skipped after this change.

Comment thread tui_gateway/ws.py
import socket
import threading
from typing import Any
from typing import Any, Awaitable, Callable, Optional, Awaitable, Callable, Optional

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Remove the duplicated Awaitable, Callable, and Optional imports.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/cli CLI entry point, hermes_cli/, setup wizard comp/gateway Gateway runner, session dispatch, delivery comp/tui Terminal UI (ui-tui/ + tui_gateway/) P2 Medium — degraded but workaround exists platform/windows Native Windows-specific behavior or breakage sweeper:blast-broad Sweeper blast radius: broad — a core path most sessions hit 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 sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows tool/tts Text-to-speech and transcription type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Desktop app (hermes desktop) fails to connect: asyncio event loop stalls ~45s on Windows startup

3 participants