Skip to content

TUI session.close can leak an AIAgent that finishes building concurrently #49852

Description

@MaxFreedomPollard

Repeated session churn can retain process, sandbox, browser, child-agent, and HTTP resources until gateway restart.
Affected component: TUI gateway — session lifecycle (tui_gateway/server.py, _start_agent_build).
Platform: Independent. This is a threading race in the gateway — not tied to any OS, shell, locale, or interpreter build.

Bug description

session.create builds its AIAgent on a deferred daemon thread. If session.close removes the session while _make_agent() is still running, teardown observes session["agent"] is None and therefore has nothing to close. When _make_agent() returns, the build thread writes the new agent into the now-detached session dictionary and continues initialization.

The existing orphan handling closes a late slash-worker (_attach_worker) and unregisters a late approval callback (the _build finally block), but it never closes the agent itself.

Steps to reproduce

Deterministic, driven from a test that parks the build thread:

  1. Start session.create with _make_agent() blocked on an event.
  2. Wait until the build thread has entered _make_agent().
  3. Call session.close; it pops and finalizes the session while agent is still None.
  4. Release _make_agent() and wait for the build thread's agent_ready event.
  5. Observe that the built agent's close() method was never called.

Expected behavior

Exactly one side of the race owns the agent:

  • if the build attaches first, later teardown sees and closes it;
  • if teardown removes the session first, the build closes the new agent and aborts before creating any worker or notification callback.

Actual behavior

On upstream main (ac83365d9602d4a2d4dfd79f221432e599ef95f9), the agent that finishes building after session.close is never closed:

AssertionError: agent built after session.close was never closed — closed_agents=[]

Root cause

The build performs an unchecked assignment after the expensive constructor:

agent = _make_agent(...)
current["agent"] = agent

_close_session_by_id() correctly pops under _sessions_lock, but the later assignment neither takes that lock nor verifies that sid still maps to the same session object.

Impact

AIAgent.close() releases background processes in ProcessRegistry, terminal sandbox environments, browser daemon sessions, active delegated child agents, and OpenAI/httpx client connections. The leaked agent is no longer reachable through server._sessions, so normal session shutdown cannot recover it. Repeated fast create/close churn (e.g. fast /new) can accumulate these resources in the long-lived desktop or TUI gateway; restarting the gateway is the only general recovery.

Proposed fix

Attach the completed agent under _sessions_lock only when sid still maps to the same session object; otherwise close the just-built agent immediately and return before any worker/callback setup — the same ownership idiom already used by _attach_worker.

A fix implementing this is already open in #49756.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Medium — degraded but workaround existsarea/sessionsSession lifecycle, resume, persistence, historycomp/gatewayGateway runner, session dispatch, deliverycomp/tuiTerminal UI (ui-tui/ + tui_gateway/)type/bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions