Skip to content

feat: background task architecture refactoring#65

Merged
yishuiliunian merged 10 commits intomainfrom
worktree-glittery-wiggling-pine
Apr 3, 2026
Merged

feat: background task architecture refactoring#65
yishuiliunian merged 10 commits intomainfrom
worktree-glittery-wiggling-pine

Conversation

@yishuiliunian
Copy link
Copy Markdown
Contributor

Summary

  • Decouple layers: Remove backend→tool-background reverse dependency; decouple TUI from tool-background via protocol types + provider injection
  • ExecOutcome enum: Replace ToolIoError::TimedOutProcess error variant with ExecOutcome::Completed | TimedOut, making timeout-to-background a valid control flow
  • Concurrency fixes: Unify bg_stop/monitor lock order (child→status), add reader AbortHandle cleanup, ensure watch_tx always fires for blocking callers

Changes

New files (12):

  • loopal-protocol/src/bg_task.rsBgTaskSnapshot, BgTaskStatus types
  • loopal-tui/src/panel_ops.rs — unified panel focus navigation
  • loopal-tui/src/views/bg_tasks_panel.rs — background tasks panel view
  • bash/src/bg_convert.rs — timed-out/spawned process → store registration
  • bash/src/bg_monitor.rs — monitor spawning, AbortHandle cleanup
  • bash/src/bg_ops.rs — bg_output / bg_stop operations
  • bash/src/format.rs — output formatting helpers
  • 5 new test files (snapshot, streaming timeout, render guard, panel focus, panel tab)

Modified layers (bottom-up):

  • loopal-error: Remove TimedOutProcess variant; keep ProcessHandle for ExecOutcome
  • loopal-tool-api: Add ExecOutcome enum; exec_streaming returns Result<ExecOutcome>; exec_background drops desc param
  • loopal-backend: shell_stream returns ExecOutcome::TimedOut with AbortHandle; shell::exec_background returns SpawnedBackgroundData
  • tool-background: Add snapshot_running() returning protocol types
  • bash tool: Match ExecOutcome variants; register via bg_convert; unified lock order
  • loopal-tui: App.bg_provider injection; bg_tasks_panel reads &[BgTaskSnapshot]; zero-height render guard
  • bootstrap: Inject snapshot_running into TUI

Test plan

  • bazel build //... passes
  • bazel test //... — 47/48 pass (1 pre-existing file-ops failure)
  • New tests: snapshot_running (5), streaming timeout (2), render guard (2), panel focus (7), enter panel (8), panel tab (7)
  • CI passes

…cOutcome, TUI injection

Resolves 6 architectural issues in the background task system:

1. (P0) Remove backend → tool-background reverse dependency: exec_background
   now returns opaque ProcessHandle; bash tool registers in store via
   bg_convert::register_spawned.

2. (P0) Decouple TUI from tool-background: new BgTaskSnapshot/BgTaskStatus
   protocol types; App.bg_provider injection replaces direct store access;
   bootstrap injects snapshot_running at startup.

3. (P1) Replace ToolIoError::TimedOutProcess with ExecOutcome enum:
   exec_streaming returns ExecOutcome::Completed | TimedOut, making timeout
   a valid control-flow outcome rather than an error variant.

4. (P2) Unify lock order in bg_stop/monitor: consistently acquire child
   before status; monitor only updates status when still Running.

5. (P3) Add zero-height guard in render.rs: skip panel split when
   layout.agents.height == 0 to prevent small-terminal rendering issues.

6. (P3) Add reader task cleanup via AbortHandle: shell_stream stores abort
   handles in TimedOutProcessData; monitor aborts stragglers after child
   exits; watch_tx always fires so blocking bg_output callers wake up.
- Fix ~30 collapsible-if warnings across the workspace by converting
  nested if/if-let to Rust let-chain syntax (if cond && let pat = ...)
- Add snapshot_running() unit tests (5 tests)
- Add streaming timeout integration tests (2 tests)
- Add render zero-height guard tests (2 tests)
- Remove unused import in snapshot_test
The monitor was aborting reader tasks immediately after child exit,
causing a race where short-lived commands (like `echo bg_hello`) would
have their output lost before the reader could write it to the buffer.
Now waits 50ms for pipes to drain before aborting stragglers.
Also passes abort handles from register_spawned for proper cleanup.
bg_stop now always returns success with "stopped" regardless of whether
the monitor already set a terminal status. Fixes flaky CI test where
the monitor races to set Failed before bg_stop checks status.
Increase sleep before stop from 100ms to 500ms to reduce flakiness
on slow CI runners where the child process may not have started yet.
…diagnostics

- path::resolve now rejects absolute write paths outside cwd even
  without sandbox policy (defence-in-depth). This fixes the pre-existing
  file-ops path traversal test failures.
- Improve bg_stop test assertion messages for CI debugging.
All background task tests share a global LazyLock store. Without
serialization, snapshot_test::clear_store() races with other tests,
causing "Process not found" errors on CI. Add BG_STORE_LOCK to
suite.rs and acquire in all test functions.

Also fix pre-existing file-ops path traversal failures by enforcing
cwd containment for absolute write paths without sandbox policy.
…e instance

The global `LazyLock<Mutex<HashMap>>` caused test flakiness: parallel
tests sharing a single store, with `clear_store()` wiping other tests'
data. Prior workarounds (BG_STORE_LOCK, #[allow(clippy::await_holding_lock)])
masked the architectural issue.

Now:
- `BackgroundTaskStore` is an injectable Arc instance, not a global
- `BashTool::new(store)` receives the store at construction
- `register_all(registry, bg_store)` threads it through Kernel
- TUI's `App.bg_store` replaces the `bg_provider` function pointer
- Each test creates its own `BackgroundTaskStore::new()` — fully isolated
- Zero `clear_store()`, zero `BG_STORE_LOCK`, zero `allow(clippy)` hacks
@yishuiliunian yishuiliunian merged commit 3b1dd71 into main Apr 3, 2026
3 checks 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