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:
- Start
session.create with _make_agent() blocked on an event.
- Wait until the build thread has entered
_make_agent().
- Call
session.close; it pops and finalizes the session while agent is still None.
- Release
_make_agent() and wait for the build thread's agent_ready event.
- 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.
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.createbuilds itsAIAgenton a deferred daemon thread. Ifsession.closeremoves the session while_make_agent()is still running, teardown observessession["agent"] is Noneand 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_buildfinallyblock), but it never closes the agent itself.Steps to reproduce
Deterministic, driven from a test that parks the build thread:
session.createwith_make_agent()blocked on an event._make_agent().session.close; it pops and finalizes the session whileagentis stillNone._make_agent()and wait for the build thread'sagent_readyevent.close()method was never called.Expected behavior
Exactly one side of the race owns the agent:
Actual behavior
On upstream
main(ac83365d9602d4a2d4dfd79f221432e599ef95f9), the agent that finishes building aftersession.closeis never closed:Root cause
The build performs an unchecked assignment after the expensive constructor:
_close_session_by_id()correctly pops under_sessions_lock, but the later assignment neither takes that lock nor verifies thatsidstill maps to the same session object.Impact
AIAgent.close()releases background processes inProcessRegistry, terminal sandbox environments, browser daemon sessions, active delegated child agents, and OpenAI/httpx client connections. The leaked agent is no longer reachable throughserver._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_lockonly whensidstill 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.