bug: opening a terminal whose command is not installed leaves a tab that can never be closed
What's broken
A terminal profile pointing at a binary that isn't installed (mine was named "Codex", on a machine without the codex binary) leaves a tab that cannot be closed. Three of them piled up. The close button does nothing, paseo terminal kill <id> reports success: true, and paseo terminal ls --all keeps listing them. Only paseo daemon restart clears them.
Expected: either the terminal opens, or Paseo tells me it could not start it. Got: a tab listed forever that reports success on every close and survives until the daemon restarts.
Steps to reproduce
- Configure a terminal profile whose command is not on PATH (mine:
codex, not installed). A command that exits instantly does it too, e.g.process.execPath -e "process.exit(0)". - Open it. A tab appears.
paseo terminal ls --all- the terminal is listed although its process is deadpaseo terminal kill <id>- returnssuccess: truepaseo terminal ls --all- the terminal is still therepaseo daemon restart- now it is gone
It is a race, so it does not happen on every attempt. Over 10 attempts per variant against a sandboxed daemon built from main: missing binary 7/10, instant-exit command 9/10.
Where did this happen
Desktop (Electron)
Paseo version
0.2.5
OS version
Linux (production sighting); reproduced on Linux 6.1 Debian and macOS 15
Agent provider
None
Provider configuration
No response
Logs
### Against a real daemon on main
Same create_terminal / list_terminals / kill_terminal RPCs the app uses, isolated daemon (own PASEO_HOME, OS-assigned port). Ghosts accumulate, every kill reports success:
attempt 1: terminal ls --all -> [{"id":"d6d7a042-...","name":"Codex"}]
attempt 1: terminal kill d6d7a042-... -> success: true
attempt 1: terminal ls --all (after kill) -> ["d6d7a042-..."] stillListed=true
attempt 2: terminal kill 48e37c38-... -> success: true
attempt 2: terminal ls --all (after kill) -> ["d6d7a042-...","48e37c38-..."] stillListed=true
RESULT missing-binary: ghosts=7/10
RESULT fast-exit (process.execPath -e 'process.exit(0)'): ghosts=9/10
After 20 attempts: 16 unclosable "Codex" terminals. A working command on the same daemon was still created, listed and killed normally.
### Instrumented trace
Worker side:
CREATED <id> alreadyExited=true # session already dead when watchTerminal subscribed
EXIT-CB <id> # onExit replay fired, terminalExit sent to the parent
Parent side, in received order:
RECV terminalCreated
REGISTER <id> # registered from the terminalCreated event
RECV response ok=true # createTerminal response
RECV terminalExit <id>
EXIT-EVENT <id> hasRecord=true # the exit found the record and removed it
REGISTER <id> # createTerminal's continuation re-registers the dead terminal
All three arrive in the same tick, so the awaiting createTerminal continuation runs after the exit was processed and registers the terminal a second time.
### Symptoms once a record is in that state
- `terminal ls --all --json` lists the id with `"cwd": "-"`; a live terminal shows a real path
- `terminal capture <id> -S --json` returns `{"lines": [], "totalLines": 0}`
- `terminal kill <id>` returns `success: true`, and re-listing still shows it
- `paseo daemon restart` clears it; the workspace's agent is unaffected
Production evidence, Paseo 0.2.5 Linux daemon: 3 "Codex" terminals with `cwd: "-"`, empty capture, `kill` returning `success: true` each, all gone after a daemon restart.
### What a missing command actually reports
Through `createTerminal` against a real pty (macOS 15, Node v24.13.0), full TerminalExitInfo:
1. missing binary -> {"exitCode":1,"signal":null,"lastOutputLines":[]}
2. echo hi -> {"exitCode":0,"signal":null,"lastOutputLines":["hi"]}
3. false -> {"exitCode":1,"signal":null,"lastOutputLines":[]}
4. default shell -> no exit within 1500ms (still running)
Cases 1 and 3 are identical. Paseo calls `pty.spawn` directly with no shell, so a missing binary is the node-pty spawn helper failing - no 127, no "command not found" text.
### Failing test
Replaying that message order through the existing FakeTerminalWorker in `packages/server/src/terminal/worker-terminal-manager.test.ts` reproduces it deterministically, on macOS and Linux:
x does not resurrect a terminal that exited before its create response was handled
AssertionError: expected { id: 'terminal-a', ...(21) } to be undefined
x replays the exit to a listener attached after a create that lost the race
AssertionError: expected [] to deeply equal [ { exitCode: +0 } ]Screenshots or video
No response
Source: getpaseo/paseo