Skip to content

Fix worker thread leak on session close in TUI frontends - #734

Closed
JeanStory wants to merge 1 commit into
lsdefine:mainfrom
JeanStory:tuiapp-thread-leak
Closed

Fix worker thread leak on session close in TUI frontends#734
JeanStory wants to merge 1 commit into
lsdefine:mainfrom
JeanStory:tuiapp-thread-leak

Conversation

@JeanStory

Copy link
Copy Markdown

Summary

Closing a session in the TUI frontends left its background agent worker thread running forever. Over a session's lifetime, repeatedly opening and closing chats caused the process thread count to grow without bound (one leaked ga-tui-agent-* thread per closed session).

Root cause

Each session spins up a background worker thread that runs GenericAgent.run(), which blocks on self.task_queue.get() waiting for the next task:

def run(self):
    while True:
        task = self.task_queue.get()   # blocks forever
        if isinstance(task, str): break
        ...

When a session was closed, the frontend only deleted it from its session dict (del self.sessions[...]). Nothing ever unblocked task_queue.get(), so the worker thread stayed parked in get() indefinitely and was never reclaimed. abort() only sets a stop signal for the current task; it does not wake a thread that is idle-waiting for a new one.

Fix

Add a shutdown() method on GenericAgent that aborts any in-flight task and posts a string sentinel to the queue. The existing run() loop already breaks out when it receives a non-dict (string) task, so the sentinel makes the worker thread return and terminate cleanly. Frontends call shutdown() on session close and then join() the thread to reclaim it.

agentmain.py — new shutdown():

def shutdown(self):
    # Stop any running task, then send a sentinel so run()'s worker thread
    # exits its blocking task_queue.get() and terminates (prevents thread leak).
    self.abort()
    self.task_queue.put("__shutdown__")

frontends/tuiapp.py and frontends/tuiapp_v2.py — on close, pop the session, call shutdown(), and join the worker (guarded, best-effort, 5s timeout so the UI never blocks):

closed = self.sessions.pop(self.current_id)
try:
    if closed.agent is not None and hasattr(closed.agent, "shutdown"):
        closed.agent.shutdown()
    if closed.thread is not None:
        closed.thread.join(timeout=5.0)
except Exception:
    pass

Testing

  • Manual reproduction: launch the TUI, watch the process thread count in Task Manager, repeatedly /new + /close sessions. Before the fix, each close leaks one ga-tui-agent-* thread (count only grows); after the fix, the count returns to baseline as workers are reclaimed.
  • Mechanism verified in isolation: 20 workers spun up, each sent the sentinel and joined → all reclaimed (thread count 21 → 1, 0 survivors).
  • python -m py_compile passes on all three changed files.

Scope / notes

  • Touches only the TUI frontends (tuiapp.py, tuiapp_v2.py) plus the shared shutdown() primitive in agentmain.py.
  • The shutdown() primitive is generic; other frontends (desktop/ACP bridges) can adopt the same pattern in a follow-up if desired, but they are intentionally out of scope here.
  • join() uses a 5s timeout and is wrapped in a try/except so a slow or wedged worker can never block or crash the UI thread on close.

Files changed

File Change
agentmain.py Add shutdown() (abort + sentinel)
frontends/tuiapp.py _cmd_close now shuts down + joins the worker
frontends/tuiapp_v2.py _cmd_close now shuts down + joins the worker

The per-session agent worker thread was never reclaimed when a session
was closed: abort() only set the stop signal without posting a sentinel
to the task queue, so run() stayed parked in task_queue.get() forever.

- agentmain.py: add shutdown() that aborts and posts a '__shutdown__'
  sentinel; run() breaks on the sentinel so the worker exits cleanly.
- frontends/tuiapp.py: _cmd_close now shuts down and joins the worker.
- frontends/tuiapp_v2.py: _shutdown_session joins the worker on close.
@lsdefine

lsdefine commented Aug 6, 2026

Copy link
Copy Markdown
Owner

TY

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants