Skip to content

buzz-acp: no bound on process lifetime — a self-prompting harness outlives its parent and bills forever #5636

Description

@CryptoJones

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/

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions