fix(tui-gateway): claim active-session leases lazily and release them when idle (#57052) - #853
Open
hashbender wants to merge 1 commit into
Open
fix(tui-gateway): claim active-session leases lazily and release them when idle (#57052)#853hashbender wants to merge 1 commit into
hashbender wants to merge 1 commit into
Conversation
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?
Stops idle desktop/TUI tabs from permanently pinning
max_concurrent_sessionsslots. Today the TUI gateway claims a cap slot for every open tab (atsession.create/resume/branch— i.e. at composer-paint time, before any user input) and only releases it in_finalize_session(tab close / WS-orphan reap / process exit). The idle reaper requires a dead transport, so a healthy connected desktop app never releases anything: N idle overnight tabs sit at N/N and every new session on every surface gets "Hermes is at the active session limit".The claim was also more eager than the gateway's own persistence policy — the DB row is deliberately created lazily on the first prompt (the in-code NOTE: avoids ghost "Untitled" sessions for every composer paint), yet the lease was claimed at paint time.
This PR fixes the claim/release timing without changing what the cap means ("recently active surfaces"):
_ensure_turn_lease, at the top of the_run_prompt_submitrun body — the chokepoint every turn entry funnels through:prompt.submit, queued-prompt drain, goal continuation, notification turns). Opening tabs / switching sessions no longer consumes a slot._release_idle_session_leases): a session with no conversational activity for 30 minutes hands back the lease only — the session, agent, and transcript stay live. Skips anything mid-turn, awaiting an input/approval prompt, holding a queued prompt, or still building its agent.HERMES_TUI_LEASE_IDLE_Soverrides the window (0= hold until tab close).message.start→errorshape as the existing context-injection-refused path, which both the Ink TUI and the desktop client already render as a failed turn.Why this approach (and not per-turn claim/release like the messaging gateway): releasing after every turn opens a window where a chained turn's re-claim loses to a concurrent surface — and the queued user prompt or notification event dispatched into that turn would be dropped. Holding across turns with an idle-window release frees idle tabs while chained turns (queued prompts, goal continuations) can never lose their slot mid-run. It also keeps the compression re-anchor (
_transfer_active_session_slot, incl. the NousResearch#49041 reserve-before-release fallback) working unchanged on the same session slot.Races covered: the claim's store is re-checked under
history_lock(the same lock every turn-start path setsrunningunder); a lease claimed for a tab that finalized in the claim window is handed straight back; the idle sweep checks running/pending/queued state under that same lock so it can't pull a lease out from under a starting turn.Related Issue
Fixes NousResearch#57052
Type of Change
Changes Made
tui_gateway/server.py_ensure_turn_lease(sid, session)— claim-or-reuse at turn start, with lost-store-race and finalized-session guards; fail-open behavior preserved._release_idle_session_leases(now)+_LEASE_IDLE_RELEASE_S(default 1800s,HERMES_TUI_LEASE_IDLE_Soverride, mirrors the existingHERMES_TUI_SESSION_TTL_Spattern); wired into_reap_idle_sessions._run_prompt_submitrun body): claim before any model work; at-cap emits the standard limit message as the turn's error.session.create, all threesession.resumebranches, andsession.branch(and their now-dead lease error-release paths).hermes_cli/active_sessions.py— module docstring updated to describe the per-surface lease semantics (no code change).website/docs/user-guide/configuration.md— documents what "active" means per surface and the idle window / env override.cli-config.yaml.example— same note on themax_concurrent_sessionskey.tests/tui_gateway/test_protocol.py— new coverage: lazy claim + reuse; at-cap limit message; no store into a finalized session; idle release + transparent re-acquire; idle-release exemptions (running / recent activity / queued prompt / pending gateway prompt);HERMES_TUI_LEASE_IDLE_S=0disable.tests/test_tui_gateway_server.py—test_session_create_rejects_at_active_session_limitreworked to pin the new contract (create claims nothing; the first turn hits the cap; close returns the slot).How to Test
max_concurrent_sessions: 2in~/.hermes/config.yaml, start the desktop app (orhermes --tuigateway).HERMES_HOME/runtime/active_sessions.jsonstays empty until a tab actually sends a message.HERMES_TUI_LEASE_IDLE_S=30, reaper scans every 300s — or call_release_idle_session_leasesdirectly as the tests do): slots free without closing the tabs; typing in a tab again re-acquires transparently.pytest tests/tui_gateway/test_protocol.py tests/hermes_cli/test_active_sessions.py tests/gateway/test_max_concurrent_sessions.py tests/hermes_cli/test_cli_active_session_limit.py -q(96 passed) andpytest tests/test_tui_gateway_server.py -q(303 passed).Checklist
Code
fix(scope):,feat(scope):, etc.)pytest tests/ -qand all tests pass — full-suite run in progress on Windows 11; all lease-related suites green (96 + 303, see How to Test). Will check this box with the result.Documentation & Housekeeping
docs/, docstrings) — or N/Acli-config.yaml.exampleif I added/changed config keys — or N/ACONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — N/A (no architecture/workflow change)active_sessions.pyimplementation (msvcrt/fcntl), unchangedScreenshots / Logs
Registry during the new lifecycle (cap=1,
HERMES_HOME/runtime/active_sessions.json):🤖 Generated with Claude Code
Mirror-of: NousResearch#57059
NousResearch#57059