buzz-acp: no bound on process lifetime — a self-prompting harness outlives its parent and bills forever
- Dominant language
- Rust
- Stars
- 32.7k
- Forks
- 4.3k
- Avg merge
- 1d 13h
- Merged PRs (30d)
- 253
Description
## Problem
`buzz-acp` has no upper bound on **process** lifetime. Every existing bound is scoped to a turn:
| Setting | Bounds |
|---|---|
| `--idle-timeout` | one silent turn |
| `--max-turn-duration` | one turn's wall clock |
| `--exit-after-inactivity` | time since the last dispatch |
`--exit-after-inactivity` looks like it covers the gap, but it cannot when the harness self-prompts. `last_activity` is reset on every dispatch (`dispatch_pending` in `crates/buzz-acp/src/lib.rs`), and a heartbeat is a dispatch — so `--heartbeat-interval` continuously resets the very clock meant to stop an unattended harness.
The consequence is that a harness with a heartbeat has no terminating condition at all. That matters because of how such harnesses are usually started: a script backgrounds `buzz-acp`, the parent exits, and the process reparents to init. Ending the session that launched it does not stop it. It keeps waking on its heartbeat and issuing model calls indefinitely against a billed provider, with nobody reading the output.
The failure is silent. Nothing errors — the turns succeed. The only external symptom is provider spend, and the process looks healthy in every log.
## Proposed solution
Add `--ttl` / `BUZZ_ACP_TTL`: a wall-clock cap on the whole process, default `0` (disabled) so existing behaviour is unchanged. On expiry the harness takes the same graceful path as SIGTERM so in-flight prompts drain.
One implementation note that is easy to get wrong: the graceful shutdown signal is a `watch` channel observed only by the main run loop, and `HarnessRelay::connect` retries with backoff *before* that loop starts. A TTL armed after the connect, or without a forced-exit backstop, is silently ignored by a harness stuck in startup. I hit exactly this while implementing it — the first version sailed past its deadline and was still running at 131s on a 60s TTL, with unit tests green. So the TTL needs a fixed absolute deadline established before the connect, plus a forced exit after a grace period.
Happy to adjust naming or the grace period if you'd prefer different ergonomics.
## Alternatives considered
- **Fix `--exit-after-inactivity` to ignore heartbeat dispatches.** Rejected: heartbeat-driven work is often legitimate, and this would change existing behaviour for anyone relying on it. A separate opt-in bound is additive.
- **Handle it externally** (supervisor, `timeout(1)`, launchd). Works, but every caller re-implements it, and an external `SIGKILL` skips the graceful drain the harness already implements.
I have a branch ready with tests if this direction is agreeable.
*Proudly Made in Nebraska. Go Big Red! 🌽 https://xkcd.com/2347/*
Contributor guide
Research direction
Start in crates/buzz-acp/src/lib.rs, tracing dispatch_pending, the main run loop, and HarnessRelay::connect. Establish the absolute TTL deadline before connection retries, preserve graceful shutdown for in-flight prompts, and provide a forced-exit backstop after the grace period. Verify the new --ttl and BUZZ_ACP_TTL behavior with tests, including startup stuck in connect retries.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- cli
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 48/100