Skip to content

feat(bash): background/detached shells via the background registry - #233

Merged
yogthos merged 1 commit into
mainfrom
feat/background-shell-registry
May 30, 2026
Merged

feat(bash): background/detached shells via the background registry#233
yogthos merged 1 commit into
mainfrom
feat/background-shell-registry

Conversation

@yogthos

@yogthos yogthos commented May 30, 2026

Copy link
Copy Markdown
Collaborator

Implements the deferred background-shell feature: a background mode for the bash tool so shells run detached and are tracked + counted like background subagents, with results delivered to the model.

Design

Rather than a separate registry (which would duplicate the entire delivery pipeline), I extended the existing BackgroundStore with a TaskKind { Subagent, Shell } tag. Shells reuse the proven completion round-trip — notifyprepend_pending_notifications / follow-up hook → output relay — that the model already understands, while the status bar counts each kind separately.

Changes

  • background.rsTaskKind tag on BackgroundTask; insert(id, kind); running_count_kind(kind); a per-kind shell cap (MAX_CONCURRENT_BG_SHELLS = 8). Removed the now-unused total running_count().
  • bash.rsBashTool gains an injected bg_store + with_bg_store(). call() branches on background: same permission + sandbox checks, then spawns detached, registers as Shell, returns a shell id immediately, and notify()s the captured output (incl. non-zero exit) on completion. The JSON schema exposes the new arg. Degrades to synchronous when no store is wired (headless).
  • builder.rs — injects the shared bg_store into BashTool (both the rig and loop tool paths).
  • task.rs — subagent cap + insert now use TaskKind::Subagent (so shells don't eat subagent slots and vice-versa).
  • status.rs / mod.rsrender() takes Option<&BackgroundStore> and shows agents:N (subagents) and shells:N (shells), each only when non-zero; call sites pass the store.

Available to the model

  • New background boolean in the bash schema (also flows through dynamic_tool_search).
  • Completions are delivered automatically via the background-notification channel — no polling needed; task_status also works on a shell id.
  • Session-end cleanup is free: cancel_all aborts the spawned task, and the bash PgKillGuard SIGKILLs the detached process group on drop.

Lifecycle / safety notes

  • Background shells are bounded by timeout (default 600s in background mode vs 120s sync); a long-running server is killed at the timeout — pass a large timeout for those. (True unbounded daemons would need an explicit kill-by-id surface — out of scope here.)
  • Per-kind cap (8) prevents runaway fan-out.

Tests

  • End-to-end background bash: registers a Shell, returns an id immediately, delivers echo output via the store, and the running count returns to 0.
  • Status-bar per-kind badge counts (agents:N / shells:N, hidden at zero).
  • Full feature-matrix suite green at -D warnings (2144 passed).

Adds a `background: true` arg to the bash tool so long-running commands
(builds, servers, watchers) run detached instead of blocking the turn, and
wires the result + a live status-bar count end to end.

Design: rather than a parallel registry, extend the existing BackgroundStore
with a TaskKind { Subagent, Shell } tag so shells reuse the proven completion-
delivery pipeline (notifications -> prepend/follow-up -> output relay) that the
model already understands, while the status bar counts each kind separately.

- background.rs: TaskKind tag on BackgroundTask; insert(id, kind);
  running_count_kind(kind); per-kind shell cap (MAX_CONCURRENT_BG_SHELLS=8).
  Removed the now-unused total running_count().
- bash.rs: BashTool gains an injected bg_store + with_bg_store(); call() branches
  on background: permission/sandbox check as usual, then spawn detached, register
  as Shell, return an id immediately, and notify() the captured output on
  completion. Schema exposes the new arg to the model. Degrades to synchronous
  when no store is wired (headless).
- builder.rs: inject the shared bg_store into BashTool (rig + loop paths).
- task.rs: subagent cap + insert now use TaskKind::Subagent.
- status.rs: render() takes Option<&BackgroundStore> and shows agents:N (subagents)
  and shells:N (shells); mod.rs call sites pass the store.
- Available to the model: new bash arg in the schema; completions delivered
  automatically; task_status works on a shell id too.

Tests: end-to-end background bash (registers a Shell, returns an id, delivers
output, count returns to 0); status-bar per-kind badge counts. Full
feature-matrix suite green at -D warnings (2144 passed).
@yogthos
yogthos merged commit a69ba27 into main May 30, 2026
9 checks passed
@yogthos
yogthos deleted the feat/background-shell-registry branch May 30, 2026 02:06
yogthos pushed a commit that referenced this pull request May 30, 2026
…ded + read/kill by id)

Replaces the timeout-bounded, push-once background-shell design (#233) with
Claude Code's proven model: background bash runs UNBOUNDED and the model reads
its output and stops it explicitly by id.

- New BackgroundShellStore (bg_shell.rs): per-shell live output buffer (unread,
  drained on read; hard-capped so a never-read flood can't OOM), status
  (Running/Exited/Killed/Failed), and the drain JoinHandle. Process-global
  (like the subagent /kill registry) so the bash tool, the new tools, the
  status bar, and session cleanup share one instance without threading it
  through every signature; tests inject their own store.
- bash background=true: spawn_streaming_shell runs the command detached with no
  timeout (optional `timeout` = auto-kill-after-N), streaming stdout/stderr into
  the store; returns a shell id immediately. Permission + sandbox checks run
  before the spawn as before. PgKillGuard SIGKILLs the process group on abort.
- New model-facing tools (mirroring Claude Code): `bash_output` (read new output
  + status by id) and `kill_shell` (kill by id). Registered in both tool paths,
  added to BUILTIN_TOOL_NAMES, exposed in the schema.
- Reverted #233's TaskKind piggyback on BackgroundStore (push-once delivery
  doesn't fit a long-lived process): back to subagents-only + running_count().
- Status bar: agents:N from BackgroundStore.running_count(), shells:N from the
  shell store. /tasks now lists background shells with status. Session swap/end
  kills all background shells.

Tests: store unit tests (drain/cap/kill/finish-first-wins/list), an end-to-end
unbounded background bash (streams output, exits clean, count returns to 0), and
status-bar per-kind badges. Full feature-matrix suite green at -D warnings
(2150 passed).
allen-munsch pushed a commit to allen-munsch/dirge that referenced this pull request Jun 3, 2026
…ll-registry

feat(bash): background/detached shells via the background registry
allen-munsch pushed a commit to allen-munsch/dirge that referenced this pull request Jun 3, 2026
…ded + read/kill by id)

Replaces the timeout-bounded, push-once background-shell design (dirge-code#233) with
Claude Code's proven model: background bash runs UNBOUNDED and the model reads
its output and stops it explicitly by id.

- New BackgroundShellStore (bg_shell.rs): per-shell live output buffer (unread,
  drained on read; hard-capped so a never-read flood can't OOM), status
  (Running/Exited/Killed/Failed), and the drain JoinHandle. Process-global
  (like the subagent /kill registry) so the bash tool, the new tools, the
  status bar, and session cleanup share one instance without threading it
  through every signature; tests inject their own store.
- bash background=true: spawn_streaming_shell runs the command detached with no
  timeout (optional `timeout` = auto-kill-after-N), streaming stdout/stderr into
  the store; returns a shell id immediately. Permission + sandbox checks run
  before the spawn as before. PgKillGuard SIGKILLs the process group on abort.
- New model-facing tools (mirroring Claude Code): `bash_output` (read new output
  + status by id) and `kill_shell` (kill by id). Registered in both tool paths,
  added to BUILTIN_TOOL_NAMES, exposed in the schema.
- Reverted dirge-code#233's TaskKind piggyback on BackgroundStore (push-once delivery
  doesn't fit a long-lived process): back to subagents-only + running_count().
- Status bar: agents:N from BackgroundStore.running_count(), shells:N from the
  shell store. /tasks now lists background shells with status. Session swap/end
  kills all background shells.

Tests: store unit tests (drain/cap/kill/finish-first-wins/list), an end-to-end
unbounded background bash (streams output, exits clean, count returns to 0), and
status-bar per-kind badges. Full feature-matrix suite green at -D warnings
(2150 passed).
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