Commit acb9052
authored
🤖 feat: add background bash process execution with SSH support (#920)
### Stack:
1. #923
1. #920 (base) <- This PR
---
## Summary
Adds `run_in_background=true` option to the bash tool, enabling agents
to spawn long-running processes (dev servers, builds, file watchers)
that persist independently.
## Why This Approach
We needed background process support that works identically for both
local and SSH runtimes. A few considerations drove the design:
1. **PTY-based output doesn't fit remote use cases.** For SSH,
maintaining a persistent PTY connection just to let agents read stdout
would be fragile and complex. Agents need to search, filter, and tail
output—not consume a live stream.
2. **File-based output lets agents use standard tools.** By writing
stdout/stderr to files on the target machine, agents can use `tail -f`,
`grep`, `head`, etc. to inspect output. We don't need to reimplement
these filtering capabilities in our own tooling.
3. **A proper long-lived remote daemon is future work.** Ideally, SSH
remotes would have a persistent mux process (or agent binary) that
manages background jobs directly. The user's frontend would just connect
to it. That's a significant architectural change. This PR provides
background shell support without requiring that investment—the
file-based approach works today with no remote-side dependencies.
## Architecture
```
AI Tools (bash, bash_background_list, bash_background_terminate)
↓
BackgroundProcessManager (lifecycle, in-memory tracking)
↓
Runtime.spawnBackground() (LocalBaseRuntime / SSHRuntime)
↓
BackgroundHandle (file-based output & status)
↓
backgroundCommands.ts (shared shell builders for Local/SSH parity)
```
## Key Design Decisions
| Decision | Rationale |
|----------|-----------|
| **File-based output** | Works identically for local and SSH, agents
read via `tail`/`cat`/`grep`. |
| **set -m + nohup** | Robust process isolation, PID === PGID for clean
group termination |
| **Workspace-scoped** | Processes tied to workspace, cleaned up on
workspace removal |
| **Lazy status refresh** | No polling overhead, reads `exit_code` file
on list() |
| **Cleanup on compaction** | Background processes terminated before
compaction, as they're not guaranteed to be included (Claude Code does
this too) |
## Tools
- `bash(run_in_background=true)` — spawns process, returns
`stdout_path`/`stderr_path`
- `bash_background_list` — lists processes with status and file paths
- `bash_background_terminate` — kills process group (SIGTERM → wait →
SIGKILL)
## Output Structure
```
/tmp/mux-bashes/{workspaceId}/{bg-xxx}/
├── stdout.log # Process stdout
├── stderr.log # Process stderr
├── exit_code # Written by trap on exit
└── meta.json # Process metadata
```
## Technical Details
### Process Spawning
The spawn command uses a subshell with job control enabled:
```bash
(set -m; nohup bash -c 'WRAPPER_SCRIPT' > stdout.log 2> stderr.log < /dev/null & echo $!)
```
**Key elements:**
- `set -m` — Enables bash job control, which makes backgrounded
processes become their own process group leader (PID === PGID). This is
a bash builtin available on all platforms.
- `nohup` — Prevents SIGHUP from killing the process when the terminal
closes.
- Subshell `(...)` — Isolates the process group so the outer shell exits
immediately after echoing the PID.
- `< /dev/null` — Detaches stdin so the process doesn't block waiting
for input.
### Exit Code Detection
The wrapper script sets up a trap to capture the exit code:
```bash
trap 'echo $? > exit_code' EXIT && cd /path && export ENV=val && USER_SCRIPT
```
When the process exits (normally or via signal), the trap writes `$?` to
the `exit_code` file. Mux reads this file to determine if the process is
still running (`exit_code` doesn't exist) or has exited (file contains
the code).
### Process Group Termination
Because `set -m` ensures PID === PGID, we can kill the entire process
tree using a negative PID:
```bash
kill -15 -PID; sleep 2; if kill -0 -PID; then kill -9 -PID; echo 137 > exit_code; else echo 143 > exit_code; fi
```
**Sequence:**
1. Send SIGTERM (`-15`) to the process group (`-PID` targets the group)
2. Wait 2 seconds for graceful shutdown
3. Check if any process in the group survives (`kill -0 -PID`)
4. If still alive, send SIGKILL (`-9`) and record exit code 137
5. Otherwise, record exit code 143 (SIGTERM)
This ensures child processes spawned by the background job are also
terminated, preventing orphaned processes.
### Cross-Platform Compatibility
| Feature | Linux | macOS | Windows (MSYS2) |
|---------|-------|-------|-----------------|
| `set -m` | ✓ bash builtin | ✓ bash builtin | ✓ bash builtin |
| `kill -15/-9 -PID` | ✓ | ✓ | ✓ |
| `nohup` | ✓ | ✓ | ✓ |
| Path format | POSIX | POSIX | Converted via `cygpath` |
Using `set -m` instead of platform-specific tools like `setsid`
(Linux-only) ensures the same code works everywhere.
## Platform Support
- **Linux/macOS/Windows MSYS2**: set -m + nohup pattern (universal)
- **SSH**: Same pattern executed remotely
## Testing
- 20 unit tests in `backgroundProcessManager.test.ts` (including process
group termination)
- 7 unit tests for background execution in `bash.test.ts`
- 6 unit tests in `bash_background_list.test.ts` (including
display_name)
- 5 unit tests in `bash_background_terminate.test.ts`
- 19 unit tests in `backgroundCommands.test.ts`
- 7 runtime tests in `tests/runtime/runtime.test.ts` (Local & SSH)
- 3 end-to-end integration tests in `tests/ipc/backgroundBash.test.ts`
(real AI calls)
_Generated with mux_1 parent ff3543c commit acb9052
45 files changed
Lines changed: 3115 additions & 99 deletions
File tree
- src
- browser/components/RightSidebar/CodeReview
- cli
- common
- orpc/schemas
- types
- utils/tools
- desktop
- node
- runtime
- services
- tools
- utils
- tests
- ipc
- runtime
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 2 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
266 | 266 | | |
267 | 267 | | |
268 | 268 | | |
269 | | - | |
| 269 | + | |
| 270 | + | |
270 | 271 | | |
271 | 272 | | |
272 | 273 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
17 | 17 | | |
18 | 18 | | |
19 | 19 | | |
| 20 | + | |
20 | 21 | | |
21 | 22 | | |
22 | 23 | | |
| |||
267 | 268 | | |
268 | 269 | | |
269 | 270 | | |
270 | | - | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
271 | 279 | | |
272 | 280 | | |
273 | 281 | | |
| |||
277 | 285 | | |
278 | 286 | | |
279 | 287 | | |
| 288 | + | |
280 | 289 | | |
281 | 290 | | |
282 | 291 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
12 | 12 | | |
13 | 13 | | |
14 | 14 | | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
15 | 21 | | |
16 | 22 | | |
17 | 23 | | |
18 | 24 | | |
19 | 25 | | |
20 | 26 | | |
21 | 27 | | |
| 28 | + | |
22 | 29 | | |
23 | 30 | | |
24 | 31 | | |
25 | 32 | | |
| 33 | + | |
26 | 34 | | |
27 | 35 | | |
28 | 36 | | |
29 | 37 | | |
30 | 38 | | |
31 | 39 | | |
32 | 40 | | |
| 41 | + | |
33 | 42 | | |
34 | 43 | | |
35 | 44 | | |
| |||
40 | 49 | | |
41 | 50 | | |
42 | 51 | | |
| 52 | + | |
43 | 53 | | |
44 | 54 | | |
45 | 55 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
| 10 | + | |
| 11 | + | |
10 | 12 | | |
11 | 13 | | |
12 | 14 | | |
| |||
26 | 28 | | |
27 | 29 | | |
28 | 30 | | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
29 | 39 | | |
30 | 40 | | |
31 | 41 | | |
| |||
190 | 200 | | |
191 | 201 | | |
192 | 202 | | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
193 | 230 | | |
194 | 231 | | |
195 | 232 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
52 | 52 | | |
53 | 53 | | |
54 | 54 | | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
55 | 75 | | |
56 | 76 | | |
57 | 77 | | |
| |||
229 | 249 | | |
230 | 250 | | |
231 | 251 | | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
232 | 272 | | |
233 | 273 | | |
234 | 274 | | |
| |||
272 | 312 | | |
273 | 313 | | |
274 | 314 | | |
| 315 | + | |
| 316 | + | |
275 | 317 | | |
276 | 318 | | |
277 | 319 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
| 4 | + | |
| 5 | + | |
4 | 6 | | |
5 | 7 | | |
6 | 8 | | |
| |||
12 | 14 | | |
13 | 15 | | |
14 | 16 | | |
| 17 | + | |
15 | 18 | | |
16 | 19 | | |
17 | 20 | | |
| |||
31 | 34 | | |
32 | 35 | | |
33 | 36 | | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
34 | 41 | | |
35 | 42 | | |
36 | 43 | | |
| |||
101 | 108 | | |
102 | 109 | | |
103 | 110 | | |
| 111 | + | |
| 112 | + | |
104 | 113 | | |
105 | 114 | | |
106 | 115 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
564 | 564 | | |
565 | 565 | | |
566 | 566 | | |
| 567 | + | |
| 568 | + | |
| 569 | + | |
| 570 | + | |
| 571 | + | |
| 572 | + | |
| 573 | + | |
| 574 | + | |
| 575 | + | |
| 576 | + | |
| 577 | + | |
| 578 | + | |
| 579 | + | |
| 580 | + | |
| 581 | + | |
| 582 | + | |
| 583 | + | |
| 584 | + | |
| 585 | + | |
| 586 | + | |
| 587 | + | |
| 588 | + | |
| 589 | + | |
| 590 | + | |
567 | 591 | | |
568 | 592 | | |
569 | 593 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
0 commit comments