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/
Problem
buzz-acphas no upper bound on process lifetime. Every existing bound is scoped to a turn:--idle-timeout--max-turn-duration--exit-after-inactivity--exit-after-inactivitylooks like it covers the gap, but it cannot when the harness self-prompts.last_activityis reset on every dispatch (dispatch_pendingincrates/buzz-acp/src/lib.rs), and a heartbeat is a dispatch — so--heartbeat-intervalcontinuously 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, default0(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
watchchannel observed only by the main run loop, andHarnessRelay::connectretries 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
--exit-after-inactivityto 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.timeout(1), launchd). Works, but every caller re-implements it, and an externalSIGKILLskips 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/