mirror of
https://github.com/block/buzz.git
synced 2026-10-27 12:40:30 +00:00
## Why PR #6330 split agent harness/runtime detection into a cheap (cache-only) path and a forced (spawning) path. Two regressions followed, both surfacing as every harness showing "(not installed)" / "CLI missing" across the agent create/edit picker, Agents > Agent defaults, and Settings > Agents — blocking agent create/edit until the user clicked Install in Settings > Agents. ## Root cause One underlying bug, two victims: - **Boot false-negative.** The resolve cache is in-memory, so it starts cold on every launch. `resolve_command_cached` (the cheap path) consulted only the Buzz-managed shim dirs plus that cold cache, and `buzz_managed_command_path`'s allowlist structurally excludes `buzz-agent`. The bundled sidecar could therefore never resolve on the cheap path until a forced pass warmed the cache, so cheap-path surfaces rendered all-missing at boot. App setup never warms the cache. - **"Check again" hang.** `run_in_login_shell` used an untimeouted `Command::output()`; a wedged login shell froze the whole forced pipeline, leaving "Check again" spinning forever. ## What - `resolve_command_cached` now also calls `resolve_workspace_command`, resolving the bundled sidecar via a filesystem stat (no spawn) — the same class of work the managed-shim check already performs. `buzz-agent` can no longer report missing, even inside the boot warm window. - New `discovery/bounded_command.rs` runs any discovery child under a hard wall-clock deadline, polling with `try_wait` rather than blocking on `wait()`. Stdout and stderr are piped to two drain threads whose buffers share an aggregate `CAPTURE_LIMIT`; a breach fails closed (kill the tree, return `None`), so a noisy or hostile probe can force neither unbounded memory nor disk fill. Tree teardown runs on every exit path — timeout, error, cap breach, *and* success — because a login-shell rc file or auth CLI can legitimately background a descendant that would otherwise outlive discovery. Ownership is deliberately asymmetric: - **Unix:** the child leads its own process group (`process_group(0)`); teardown is `SIGTERM` → bounded grace → `SIGKILL` on the group. A descendant that leaves the group (`setsid`/`setpgid`) while holding a pipe is not owned and may survive one probe, but can never hang or unbound the helper: the Unix drains read nonblocking and end on `WouldBlock` once teardown sets the stop flag, so the join returns promptly without waiting on an escaped writer's EOF. - **Windows:** the child is spawned `CREATE_SUSPENDED`, assigned to a kill-on-close Job Object while frozen, then resumed. The job owns the root before any descendant can exist and is created without breakaway, so no writer can escape — a hard whole-tree guarantee, and closing the job reaps the tree even after the root has exited. Any failure to create, assign, or resume is fail-closed: the child is terminated and reaped and the spawn returns `None` (discovery treats it as command-not-found) rather than running unowned. - Each login-shell candidate is bounded by a 10s timeout via that helper, falling through to the next candidate on timeout instead of aborting the resolve. The login-shell path cache is generation-aware: a probe that loses to a concurrent refresh or lands mid-refresh returns the authoritative cached value (or re-probes under the new generation) rather than its own rejected local result, so a losing thread can never settle the UI with a PATH-missing catalog while the cache holds a fresh success. - Warm the ACP runtime catalog once at `AppShell` mount and gate the cheap-path surfaces on that pass. A module-level boot-warm state (`idle` → `pending` → `settled`/`failed`, deduped per launch) lets `useAcpRuntimesQuery` present a cold catalog as *loading* while the first forced pass runs and as a *retryable error* (carrying the probe's real reason) if it fails, instead of blessing "every harness not installed" as authoritative. A non-empty catalog always wins, so a revalidation or later failure never blanks a good list; the gate only overlays once the warm has started, so onboarding (which renders before the warm) is unaffected. Deduping per launch also fixes the previous per-remount re-fire. ## Verification Unix teardown and the drain contract are runtime-proven by `#[ignore]`-free tests that record a backgrounded descendant's real PID and assert the helper returns promptly on both the success and timeout paths without blocking on that writer. The generation-aware login-shell cache is covered by deterministic tests through a `cfg(test)` injectable probe seam that assert the function's return value under both concurrent-refresh interleavings — the losing caller returns the peer's committed success, and a mid-probe refresh forces a re-probe to the fresh value. The Windows ownership contract has no CI lane, so `bounded_command.rs` carries two `#[ignore]`-gated tests (spawn/assign race, looped; and the timeout path) for a sanctioned run on a Windows host. The boot-warm gate is covered by unit tests for the pure overlay and the `startBootWarm` failure → retry → settle lifecycle. Origin: [Buzz thread](buzz://message?channel=5ef5d5bb-643f-4b87-bbf4-e8b64585ffeb&id=a4b1c4485de4d35cff0f914d4f4211c796f44f670e76de2ef9431f7e882c906e) Fixes #6872 Related #6662 --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>