Skip to content

fix(desktop): detach managed agents from the controlling terminal (setsid)#2321

Open
matthiasdebernardini wants to merge 2 commits into
block:mainfrom
matthiasdebernardini:fix/detach-managed-agents-from-tty
Open

fix(desktop): detach managed agents from the controlling terminal (setsid)#2321
matthiasdebernardini wants to merge 2 commits into
block:mainfrom
matthiasdebernardini:fix/detach-managed-agents-from-tty

Conversation

@matthiasdebernardini

Copy link
Copy Markdown

Problem

When the desktop app is launched from a terminal (just dev), every managed-agent process it spawns — the buzz-acp harness, the claude-agent-acp adapters, the agent engine, and its MCP servers — inherits the terminal's controlling TTY. The harness was spawned with process_group(0), which makes the subtree a background process group of that terminal session. macOS job control then intermittently stops the actively-working agent's process group with SIGTTIN-class stop signals.

Observed live: an agent turn froze mid-Bash tool call with the engine, both its MCP servers, and finally the adapter all in ps state T (stopped). The turn hangs indefinitely; the desktop shows the agent "working" forever (the typing/status indicator loops), and the reply only escapes if someone manually kill -CONTs the process group. Packaged .app launches have no controlling TTY and are unaffected — this only bites development runs, but it makes agents unusable under just dev.

Fix

Replace process_group(0) with a pre_exec setsid() in spawn_agent_child (the sole long-lived harness spawn site). setsid() puts the whole agent subtree in its own session with no controlling terminal, so background-TTY job control can never stop it. If setsid() fails (EPERM when already a group leader), fall back to setpgid(0, 0) best-effort — exactly the pre-patch behavior — and never fail the spawn.

Invariants preserved:

  • setsid() makes the child both session and process-group leader, so pid == pgid still holds and every existing kill path (sigterm_then_sigkill, orphan-sweep kill(-pid, …)) is unchanged. The orphan-cleanup comment even anticipated setsid'd children already.
  • Orphan cleanup after a desktop crash is unaffected: the BUZZ_MANAGED_AGENT marker + PID-receipt sweep was already the safety net (the harness never sat in the shell's job list even before this change).
  • Windows (Job Object) path untouched; #[cfg] gating mirrors the code it replaces.

The unsafe in pre_exec is minimal (only async-signal-safe setsid/setpgid) and follows the existing libc::kill precedent in this file. Stale comments in storage.rs / process_lifecycle.rs that cited process_group(0) as the pid==pgid mechanism are updated.

Tests

  • New #[cfg(unix)] test spawned_harness_is_session_leader_in_new_session: asserts a child spawned with the same pre_exec becomes a session leader (sid == pid) in a session distinct from the test process, with pgid == pid so the group-kill invariant holds. (It exercises the setsid pattern on a stand-in child — spawn_agent_child needs an AppHandle — see the test doc comment for the limitation.)
  • cargo test --manifest-path desktop/src-tauri/Cargo.toml managed_agents — 682 passed.
  • cargo clippy --manifest-path desktop/src-tauri/Cargo.toml --all-targets -- -D warnings — clean; cargo fmt applied.
  • Reproduced the freeze locally under just dev (agent turn stuck with adapter/engine/MCP in state T), and verified kill -CONT on the stopped group let the turn complete — confirming background-TTY stops as the mechanism this patch removes.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PCWMx2BkoFDZtFRiDny16W

…tsid)

When the desktop app is launched from a terminal (`just dev`), the managed
agent harness subtree (buzz-acp -> claude-agent-acp -> engine + MCP servers)
inherited the terminal's controlling TTY. Running as background process
groups of that session, macOS job control intermittently STOPPED the
actively-working agent's process group (SIGTTIN/SIGTTOU-class stops, observed
as `T` state in ps), freezing agent turns indefinitely. Packaged .app
launches have no controlling TTY and were unaffected.

Spawn the harness via `pre_exec(setsid())` on unix so the whole agent tree
runs in its own session with NO controlling terminal, making the freeze
impossible. This replaces the previous `process_group(0)`: setsid makes the
child both session AND process-group leader (pid == pgid), so the existing
`-pid` group-kill paths still reach the entire tree. We drop
`process_group(0)` because std may apply setpgid before pre_exec runs, which
would make setsid fail EPERM. If setsid does fail, we fall back to a
best-effort setpgid(0, 0) and never fail the spawn.

Adds a unix test asserting the spawned child is a session leader in a
session distinct from the test process.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PCWMx2BkoFDZtFRiDny16W
@matthiasdebernardini
matthiasdebernardini requested a review from a team as a code owner July 22, 2026 02:06
Resolutions: storage.rs takes main (write_agent_pid_file was replaced by
runtime receipts); runtime/tests.rs keeps both the new setsid session-leader
test and main's receipt validation tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019DHNgyFUB6276FGHfBQA4a
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.

1 participant