fix(tui-gateway): claim active-session leases lazily and release them when idle (#57052) - #57059
fix(tui-gateway): claim active-session leases lazily and release them when idle (#57052)#57059lEWFkRAD wants to merge 2 commits into
Conversation
… when idle The TUI/desktop gateway claimed a max_concurrent_sessions slot for every OPEN tab (session.create/resume/branch) and only released it in _finalize_session -- tab close, WS-orphan reap, or process exit. The idle reaper requires a DEAD transport, so tabs sitting open in a connected desktop app held their slot forever: 7 idle overnight tabs pin the cap at 7/7 and every new session on any 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 (to avoid ghost "Untitled" sessions for every composer paint), yet the lease was claimed at paint time. Fix the release path instead of changing cap semantics: - Claim lazily on a tab's first turn (_ensure_turn_lease at the top of the _run_prompt_submit run body -- the chokepoint every turn entry funnels through: prompt.submit, queued drain, goal continuation, notification turns). Opening tabs and switching sessions never consumes a slot. - Keep the lease across turns (no per-turn churn, chained turns can't lose their slot mid-run), but hand it back after 30 idle minutes via the existing reaper scan (_release_idle_session_leases) -- the lease only; the session, agent, and transcript stay live. HERMES_TUI_LEASE_IDLE_S overrides the window; 0 keeps slots held until the tab closes. - The next turn transparently re-acquires. If the cap is full at that point, the turn surfaces the standard limit message as its error event (same message.start->error shape as the context-injection-refused path) and the session returns to idle. - The store is guarded against a tab finalizing between claim and store (the fresh lease is handed straight back) and against concurrent claimers (re-checked under history_lock, the same lock every turn-start path uses). - Compression re-anchor (_transfer_active_session_slot) is unchanged: a mid-turn lease sits in the same session slot and transfers as before; an idle session without a lease re-claims under the new session key on its next turn. The messaging gateway already claims per in-flight turn and the CLI keeps its process-lifetime lease; docs now spell out the per-surface semantics. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
6d9deac to
8014ea1
Compare
teknium1
left a comment
There was a problem hiding this comment.
Thanks for the focused lifecycle fix. The premise reproduces on current main: session.create claims the lease before a user prompt (tui_gateway/server.py:5261), while the idle reaper requires a dead transport (tui_gateway/server.py:860-872). The lazy first-turn claim and idle-release tests are well targeted.
Problems
- The new user-facing
HERMES_TUI_LEASE_IDLE_Ssetting (tui_gateway/server.py:930, documented atwebsite/docs/user-guide/configuration.md:1646-1648) is a non-secret behavioral configuration knob.AGENTS.md:102-106requires these settings to live inconfig.yaml, not a newHERMES_*environment variable.
Suggested changes
- Add the idle-release duration through
DEFAULT_CONFIG, resolve it from config in the TUI gateway, and update the example/docs and tests for the default and0disabled behavior.
Automated hermes-sweeper review.
|
I opened a focused stacked follow-up for the idle-release boundary: lEWFkRAD#2 It replaces sweep-only release with cancellable per-session timers at the actual full-turn idle boundary, preserves warm runtime/history, protects running/queued/pending/building work, and keeps the sweep as a recovery backstop. The branch passes 420 gateway tests plus Ruff/compile checks. One important rebase note: current |
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 #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 #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