Skip to content

fix(gateway): offload /model context-length resolution off the event loop - #74155

Closed
Drexuxux wants to merge 1 commit into
NousResearch:mainfrom
Drexuxux:drex/model-ctx-offload
Closed

fix(gateway): offload /model context-length resolution off the event loop#74155
Drexuxux wants to merge 1 commit into
NousResearch:mainfrom
Drexuxux:drex/model-ctx-offload

Conversation

@Drexuxux

Copy link
Copy Markdown
Contributor

What

/model on the gateway freezes the entire event loop while it resolves the context length to display.

resolve_display_context_length() runs two blocking chains — the route comparison in should_clear_context_pin() and the provider probe ladder in get_model_context_length() (blocking requests calls to Anthropic /v1/models, Copilot, Nous, Codex, GMI, Ollama, models.dev and OpenRouter).

The gateway message path already offloads both, via get_model_context_length_async() and should_clear_context_pin_async(). The /model slash-command handlers — _handle_model_command (gateway/slash_commands.py:2072) and _finish_switch (:2395) — call the sync helper bare inside async def.

While one user runs /model, no messages are processed on any platform. get_model_context_length_async()'s own docstring names the failure mode it exists to prevent: "does not freeze the asyncio event loop and cause Discord heartbeat timeouts."

Measured with a 50 ms heartbeat and a single 2 s probe standing in for the ladder:

BEFORE   max heartbeat lag : 1.997s   <-- loop frozen
AFTER    max heartbeat lag : 0.013s

Both resolve the same value in the same wall-clock time — the work simply stops blocking the loop.

Fix

Add resolve_display_context_length_async() — a thin asyncio.to_thread wrapper mirroring the two existing *_async helpers, sharing all logic with the sync version (no duplication) — and await it at both handlers.

Tests

tests/hermes_cli/test_model_switch_context_offload.py (4 tests, all fail without the fix):

  • async variant resolves the same value as the sync helper
  • the blocking chain runs on a worker thread, not the loop thread
  • a concurrent heartbeat keeps ticking while the probe ladder runs
  • the /model handlers no longer reach the sync helper
tests/hermes_cli/test_model_switch_context_offload.py ....  4 passed

Existing suites unaffected: 188 passed, 4 skipped across the model_switch / context_display / apply_model_switch selections; 283 passed across the gateway slash-command selection (the 3 failures there are present on main unchanged, in test_complete_path_at_filter, test_feishu and test_telegram_slash_confirm).

…loop

resolve_display_context_length() runs two blocking chains: the route
comparison in should_clear_context_pin() and the provider probe ladder in
get_model_context_length() (blocking requests calls to Anthropic /v1/models,
Copilot, Nous, Codex, GMI, Ollama, models.dev and OpenRouter).

The gateway message path already offloads both via
get_model_context_length_async() and should_clear_context_pin_async(), but
the /model slash-command handlers (_handle_model_command, _finish_switch)
called the sync helper directly, freezing the whole event loop for the
duration of the probe ladder - no messages processed on any platform, and
the Discord heartbeat timeouts that get_model_context_length_async() was
introduced to prevent.

Add resolve_display_context_length_async(), a thin asyncio.to_thread wrapper
mirroring the two existing *_async helpers (no logic duplication), and await
it at both handlers.
@alt-glitch alt-glitch added type/bug Something isn't working comp/gateway Gateway runner, session dispatch, delivery comp/cli CLI entry point, hermes_cli/, setup wizard P1 High — major feature broken, no workaround labels Jul 29, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Related to merged #53603 and #56212: those changes offloaded other model-switch paths, while this PR covers the two remaining synchronous display-context calls in gateway slash-command handlers.

@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 tracing the remaining display-resolution calls. The direct premise is still present on current main at gateway/slash_commands.py:2028 and :2350, and the proposed wrapper follows the established asyncio.to_thread pattern in agent/model_metadata.py:2761-2787.

Problems

  • The two handlers still synchronously call enrich_model_switch_warnings_for_gateway() before the changed display calls (gateway/slash_commands.py:1843, :2148). That helper reaches merge_preflight_compression_warning() (hermes_cli/context_switch_guard.py:194), which invokes synchronous resolve_display_context_length() at :94. Thus cached, compression-enabled sessions can still execute the probe ladder on the event loop.
  • tests/hermes_cli/test_model_switch_context_offload.py:108 uses inspect.getsource() to assert implementation text. AGENTS.md:1382-1435 explicitly prohibits source-reading tests.

Suggested changes

  • Make the preflight-warning resolver path non-blocking for both gateway callers and cover it with the same regression guarantee.
  • Replace the source assertion with a real handler-seam test using a blocking resolver plus a worker-thread or heartbeat assertion.

Automated hermes-sweeper review.


from gateway import slash_commands

source = inspect.getsource(slash_commands)

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.

inspect.getsource() is a source-shape assertion, which AGENTS.md prohibits. Replace this with a behavioral test that drives the actual handler paths and proves a deliberately blocking resolver runs off the event-loop thread.

@teknium1 teknium1 added sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 30, 2026
teknium1 added a commit that referenced this pull request Aug 1, 2026
…ioral offload tests

Follow-ups to the previous commit (#74155 by @Drexuxux):

- enrich_model_switch_warnings_for_gateway() -> merge_preflight_compression_warning()
  still called the sync resolve_display_context_length() provider probe ladder
  inline in both async /model call sites; dispatch it via asyncio.to_thread.
- Replace the inspect.getsource() test (source-reading tests are banned by
  AGENTS.md) with behavioral tests that drive the real _handle_model_command:
  assert the resolver runs off the loop thread and that the warning enrichment
  is dispatched through asyncio.to_thread.
@teknium1

teknium1 commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Merged via salvage PR #75888 (#75888) — your asyncio.to_thread offload was cherry-picked with authorship preserved, plus coverage of the enrich_model_switch_warnings_for_gateway() path that still hit the sync resolver, and the inspect.getsource() test replaced with behavioral ones (source-reading tests are banned by repo policy). Thanks!

@teknium1 teknium1 closed this Aug 1, 2026
randlee pushed a commit to randlee/hermes-agent that referenced this pull request Aug 11, 2026
…ioral offload tests

Follow-ups to the previous commit (NousResearch#74155 by @Drexuxux):

- enrich_model_switch_warnings_for_gateway() -> merge_preflight_compression_warning()
  still called the sync resolve_display_context_length() provider probe ladder
  inline in both async /model call sites; dispatch it via asyncio.to_thread.
- Replace the inspect.getsource() test (source-reading tests are banned by
  AGENTS.md) with behavioral tests that drive the real _handle_model_command:
  assert the resolver runs off the loop thread and that the warning enrichment
  is dispatched through asyncio.to_thread.
prmartinow pushed a commit to prmartinow/hermes-agent that referenced this pull request Aug 26, 2026
…ioral offload tests

Follow-ups to the previous commit (NousResearch#74155 by @Drexuxux):

- enrich_model_switch_warnings_for_gateway() -> merge_preflight_compression_warning()
  still called the sync resolve_display_context_length() provider probe ladder
  inline in both async /model call sites; dispatch it via asyncio.to_thread.
- Replace the inspect.getsource() test (source-reading tests are banned by
  AGENTS.md) with behavioral tests that drive the real _handle_model_command:
  assert the resolver runs off the loop thread and that the warning enrichment
  is dispatched through asyncio.to_thread.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cli CLI entry point, hermes_cli/, setup wizard comp/gateway Gateway runner, session dispatch, delivery P1 High — major feature broken, no workaround sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform 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.

3 participants