Skip to content

[Bug]: ao start launches the Linux AppImage with stdout/stderr closed, AppImage runtime then spins at 100% CPU #6261

Description

@AlexanderMakarov

What went wrong?

On Linux with the AppImage install, every app launched by ao start leaves the AppImage runtime process (~/.ao/agent-orchestrator.AppImage --installed-via=npm-bootstrap) burning 100% of one CPU core for as long as the app runs, and after the app quits too.

Root cause: ao start starts the app with stdout and stderr closed. backend/internal/cli/process.go (at a34094e):

type processStartConfig struct {
	...
	Stdout *os.File
	Stderr *os.File
}

func startProcess(cfg processStartConfig) error {
	cmd := exec.Command(cfg.Path, cfg.Args...)
	cmd.Env = cfg.Env
	cmd.Stdout = cfg.Stdout
	cmd.Stderr = cfg.Stderr
	...

openApp leaves Stdout/Stderr unset. Assigning a nil *os.File to the io.Writer fields gives a non-nil interface holding a nil pointer, which os/exec passes as "no file". The child therefore starts with fd 1 and fd 2 closed. Stdin is untouched, so it correctly gets /dev/null. A minimal Go 1.27.1 program with the same assignment shows fd0=open fd1=CLOSED fd2=CLOSED in the child.

Why that costs a core: the AppImage type2 runtime (here AppImage/type2-runtime@dd6cebe) creates its keepalive pipe with pipe(), so with 1 and 2 free the pipe lands on fd 1/2. When the FUSE side daemonizes, it redirects 0-2 to /dev/null, which silently replaces the pipe's write end. write_pipe_thread then loops write() "until we block", and writes to /dev/null never block, so it spins in the kernel. It also never sees the EPIPE that should end it when the app exits.

Observed on the running app:

  • The runtime process has fds 0,1,2 -> /dev/null, the AppImage and /dev/fuse, and no pipe.
  • One thread spends about 75% of its time in the kernel, with 0 voluntary context switches.
  • The Electron main process has fd 2 -> /tmp/.mount_agent-*/AppRun: stderr was closed at start, and a later open() reused the number.

Minimal repro without AO:

A=~/.ao/agent-orchestrator.AppImage
setsid sh -c "exec $A --appimage-mount" </dev/null >/dev/null 2>&1 &     # busiest runtime thread: 0% CPU
setsid sh -c "exec 0<&- 1>&- 2>&-; exec $A --appimage-mount" &          # busiest runtime thread: 100% CPU

Also affected:

  • A second-instance ao start that only focuses the existing window also leaves a spinning runtime behind.
  • Killing the spinning runtime unmounts /tmp/.mount_agent-*. Running daemon, pty-host and chat-host processes execute from that mount, so they break. The only clean way out is restarting AO.
  • The .deb install isn't affected, since it has no FUSE runtime.

Suggested fix (one place, covers every caller): only set the writers when a file was given, so an unset stream falls back to os/exec's default, /dev/null:

if cfg.Stdout != nil {
	cmd.Stdout = cfg.Stdout
}
if cfg.Stderr != nil {
	cmd.Stderr = cfg.Stderr
}

Workaround: launch the AppImage yourself with /dev/null stdio instead of ao start:

setsid ~/.ao/agent-orchestrator.AppImage --installed-via=npm-bootstrap </dev/null >/dev/null 2>&1 &

Environment: AO 0.13.3 AppImage at ~/.ao/agent-orchestrator.AppImage, Ubuntu 24.04 (Linux 7.0), XFCE, libfuse2t64 2.9.9.

Activity

  1. self-assigned this
    on Oct 6, 2026
  2. added
    bugSomething isn't working
    comp/daemonGo daemon, process lifecycle, and backend control plane.
    on Oct 7, 2026
  3. added this to the Reliability milestone on Oct 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingcomp/daemonGo daemon, process lifecycle, and backend control plane.

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions