Skip to content

fix(tui_gateway): close dedicated profile SessionDB handles on every path (salvage #76701) - #81071

Merged
kshitijk4poor merged 3 commits into
NousResearch:mainfrom
kshitijk4poor:salvage/76701
Aug 7, 2026
Merged

fix(tui_gateway): close dedicated profile SessionDB handles on every path (salvage #76701)#81071
kshitijk4poor merged 3 commits into
NousResearch:mainfrom
kshitijk4poor:salvage/76701

Conversation

@kshitijk4poor

Copy link
Copy Markdown
Collaborator

Summary

Dedicated profile-scoped SessionDB handles are now closed on every path — the pre-transfer early returns in session.resume, the deferred/branch/compute-host build failure paths, the reaped-mid-build discard, and agent teardown (AIAgent.close() releases a handle it owns). Fixes the fd leak class where long-lived Desktop/serve backends accumulated 2 fds + a pinned token-writer thread per abandoned handle until OSError: [Errno 24] Too many open files.

Salvage of #76701 by @Yishova — both commits cherry-picked with authorship preserved. Supersedes #80706 (strict subset: teardown half only, tests break on macOS), overlaps #78970/#72804 at other layers.

Who hits this

Anyone running the Desktop app or hermes serve against multiple profiles for days: every reconnect/tile-paint resume of a live chat (session.resume fast path — the hot one), every default cold resume, and every successfully-built-then-closed agent leaked a SQLite handle. An abandoned SessionDB is never GC'd once its background token writer starts — it pins ITSELF via atexit.register(_drain_token_queue_at_exit), which only close() unregisters. Observed live: ~490 leaked fds over ~2 days, backend wedged at the 1024-fd soft limit.

Changes

  • tui_gateway/methods_session.pysession.resume gets explicit owns_db ownership with a try/finally close covering all ten pre-transfer early returns; ownership transfers exactly at _init_session success; half-built session registration rolled back on failure (prevents a permanently-dead live-session fast path).
  • agent/agent_init.py + run_agent.py_owns_session_db lifecycle flag (mirrors _end_session_on_close); the recall fallback's self-created handle is owned; AIAgent.close() releases an owned handle after end_session(), clearing the flag first for idempotency.
  • tui_gateway/server.py + compute_host.py — deferred builder, branch handler, and compute-host paths transfer ownership via _transfer_db_to_agent() (refuses mismatched handles; closes directly when the built agent was discarded mid-build).
  • Tests: 654 lines across 2 files, platform-independent _RecordingDB doubles — owned-closed-exactly-once, shared-never-closed, idempotency, transfer refusal, reaped-mid-build, registration rollback, end_sessionclose ordering.

Note the raw diff on methods_session.py is mostly re-indentation from the try: wrap; review with git diff -w (~40 insertions / 2 deletions there).

Validation

Check Result
Ownership test files + tests/tui_gateway/ 373 passed (+1 new raising-close contract test → 22 in the ownership files)
tests/test_tui_gateway_server.py 519 passed
Mutation check (revert the 4 production files to upstream/main) 16 ownership guards fail on base, green on stack
Integration review of the re-applied commit-1 hunk every return in session.resume covered by the finally; try starts after db binds; main's churn (workspace/steer/omit_messages) all inside the try; no untracked opens in tui_gateway/
Cherry-pick conflict resolution commit 1 re-applied onto current main's session.resume (whitespace-normalized hunk set verified identical to the original)
ruff clean

Related PRs in this cluster

Based on #76701 by @Yishova — commits cherry-picked to preserve authorship.

Yishova and others added 3 commits August 7, 2026 18:48
Follow-up to the review on the session.resume ownership fix. Closing the
pre-transfer early returns left two gaps, both real.

1. The transfer had no owner on the other side. Once ownership moved to the
   agent, teardown ran AIAgent.close() (via _teardown_session on session.close
   and the orphaned-session reaper), which called session_db.end_session() —
   that finalizes the session ROW, not the connection. A successfully resumed
   profile session kept its dedicated handle, its db/-wal/-shm fds and its
   background token-writer thread for the life of the gateway.

   AIAgent now carries an explicit _owns_session_db, defaulting False so the
   SHARED launch handle — which outlives every agent and backs every other live
   session — is still never closed there. Only the dedicated-open sites set it,
   at the point ownership actually changes hands.

2. session.resume was not the only profile-scoped open with no close on its
   failure paths. Covered here with the same flag, via a _transfer_db_to_agent
   helper that refuses the transfer unless the agent really holds that handle:

   - the deferred builder (_start_agent_build), including the session-reaped-
     mid-build case, where the built agent is discarded and never torn down, so
     transferring to it would leak exactly as before;
   - session.branch's branch_db;
   - the compute host's per-profile open;
   - AIAgent's own lazy open in _get_session_db_for_recall, which no other
     object ever references and so was unconditionally abandoned.

Where a handle has already reached a registered session, the drop is
unconditional and the transfer is best-effort on top: a refused transfer leaves
the old leak, which is survivable, whereas closing under a live session is the
permanent "Cannot operate on a closed database" break the original patch exists
to avoid.

Tests: tests/tui_gateway/test_session_db_ownership_teardown.py (new, 14).
11 of the 14 fail without this change; the 3 that pass are the "must NOT close"
guards, which hold in both directions by design.
Review finding: the close block's comment promises a raising
session_db.close() is swallowed with the flag already cleared (no re-close
on a second agent.close()), but nothing pinned it. One test with a raising
_RecordingDB proves both halves.
@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/tui Terminal UI (ui-tui/ + tui_gateway/) area/sessions Session lifecycle, resume, persistence, history sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Aug 7, 2026
@kshitijk4poor
kshitijk4poor merged commit 813793d into NousResearch:main Aug 7, 2026
48 checks passed
@kshitijk4poor
kshitijk4poor deleted the salvage/76701 branch August 7, 2026 14:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/sessions Session lifecycle, resume, persistence, history comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/tui Terminal UI (ui-tui/ + tui_gateway/) P2 Medium — degraded but workaround exists sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants