Skip to content

TUI processes peg 100% CPU each and survive terminal close, eating all RAM #75289

Description

@wolfgang-voss

Summary

openclaw-tui processes spin at 100% CPU per process and do not exit when the controlling terminal closes or when the gateway restarts. On a 16 GB Mac mini running the gateway as a LaunchAgent, this repeatedly causes the machine to run out of RAM and swap-thrash until macOS kills processes or the machine becomes unresponsive.

The TUI processes also resist kill (SIGTERM) and require kill -9 (SIGKILL) to terminate.

Environment

  • OpenClaw: 2026.4.29 (a448042)
  • Node: v25.6.1
  • OS: macOS Darwin 25.3.0 (Mac mini, Apple Silicon, 16 GB RAM)
  • Gateway mode: local loopback LaunchAgent, port 18789
  • TUI invocation: openclaw tui --session <name>

Reproduction

  1. Start the gateway via LaunchAgent
  2. Open openclaw tui --session provisioning in a terminal tab
  3. Either: (a) close the terminal tab, or (b) gateway restarts (e.g., after brew upgrade or openclaw doctor --fix rewrites the plist)
  4. The openclaw-tui process does not exit. It continues running at 100% CPU with ~900 MB RSS
  5. Opening new terminal tabs and running openclaw tui creates additional TUI processes that also spin
  6. Within minutes, 3-5 orphaned TUI processes consume 3-5 GB RAM and 300-500% CPU

Observed behavior

PID    PROCESS          RAM      CPU
5302   openclaw-tui     1.1 GB   98%
4728   openclaw-tui     1.0 GB   99%
5696   openclaw-tui     810 MB   74%
4909   openclaw-gateway 1.1 GB   2%
  • kill <pid> (SIGTERM) does not terminate TUI processes
  • kill -9 <pid> is required
  • Machine reaches 15/16 GB RAM used, 77 MB free, then swap-thrashes
  • Gateway itself is fine (2% CPU, connectivity probe passes)
  • This has happened multiple times across sessions

Root cause analysis (partial)

Code inspection of tui-DN9rW-QU.js and pi-tui shows:

  1. No SIGHUP handler. The TUI registers handlers for SIGINT and SIGTERM (lines 4626-4627) but not SIGHUP. When a terminal tab closes, macOS sends SIGHUP. Node.js default SIGHUP behavior (exit) can be suppressed if any other code path registers a SIGHUP listener. attachChildProcessBridge in child-process-bridge-BMx3So6b.js temporarily registers one, which may leave a window where SIGHUP is swallowed.

  2. SIGTERM handler calls requestExit() but the exit path may hang. requestExit() calls client.stop() then drainAndStopTuiSafely(tui), which calls tui.stop(). If tui.stop() tries to write to a dead stdout (terminal already closed), this could block or fail silently, preventing the process from actually exiting.

  3. Loader animation keeps firing. The Loader component (pi-tui/dist/components/loader.js) runs a setInterval at 80ms that calls requestRender() on every tick. Each render writes to process.stdout. When stdout is a dead pipe (orphaned process), these writes may fail in a way that triggers immediate re-rendering rather than backing off.

  4. The gateway client reconnect loop is properly throttled (exponential backoff 1s to 30s in client-CLNzli6Y.js:706-721), so the spin is not from reconnection attempts.

The exact mechanism causing 100% CPU in orphaned processes could not be fully pinned down via static analysis. Profiling a live orphaned process with --prof or --cpu-prof would identify the hot function.

Expected behavior

  • TUI should exit when its controlling terminal closes (handle SIGHUP)
  • TUI should exit cleanly on SIGTERM without requiring SIGKILL
  • TUI should not spin at 100% CPU when disconnected from the gateway
  • TUI should not accumulate multiple zombie processes across gateway restarts

Workaround

pkill -9 openclaw-tui

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions