feat: pause an agent's turn loop without stopping its container
A paused agent keeps its container, its claude session and its dashboard/todo servers up, but stops driving turns. Messages queue unacked and are drained on resume. The whole protocol is a single marker file, `<harness>/paused`. That directory is already a bind-mount shared between host and container, so both sides just stat the same path: the harness reads it to decide whether to drive a turn, hive-c0re reads it to render the badge and writes/removes it for `hivectl pause|resume`. No new wire protocol, no container round-trip, and it is sticky across restarts by construction. Not calling `recv_next` while paused *is* the queueing semantic, so there is no fencing to get wrong: reminders buffer in their unbounded channel, the todo `Notify` permit coalesces, and a `request_next_turn` that raced the pause survives because the gate sits above `self_continue.take()`. Graceful stop is handled host-side rather than in the harness: a paused agent provably has no turn in flight, so `run_signal` skips the fence entirely instead of eating the full `GRACEFUL_STOP_TIMEOUT` waiting for a checkpoint turn that will never run. `paused` is reported on `ContainerView` / `AgentStatusRow` for the dashboard, orthogonal to `running` and reported for stopped containers too. Closes: hyperhive/hyperhive issue 2271
This commit is contained in:
parent
1356a1f049
commit
31008c83df
14 changed files with 305 additions and 1 deletions
|
|
@ -40,6 +40,12 @@ const DEFAULT_SOCKET: &str = "/run/hive/mcp.sock";
|
|||
/// Default web UI port — used when `HIVE_PORT` env is unset.
|
||||
const DEFAULT_WEB_PORT: u16 = 8042;
|
||||
|
||||
/// How often the serve loop re-stats the pause marker while parked.
|
||||
/// Only paid while an agent is actually paused, and only against the
|
||||
/// local harness dir, so a tight-ish interval is cheap and keeps
|
||||
/// `hivectl resume` feeling immediate.
|
||||
const PAUSE_POLL: Duration = Duration::from_secs(5);
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
|
|
@ -659,7 +665,45 @@ async fn serve_loop<S: Surface>(
|
|||
// in-process instead of long-polling the broker. Never
|
||||
// persisted: it lives entirely in this loop's stack.
|
||||
let mut self_continue: Option<hive_sh4re::DeliveredMessage> = None;
|
||||
// Tracks the last observed pause state so the transitions get logged
|
||||
// once each instead of twelve lines a minute while parked.
|
||||
let mut was_paused = false;
|
||||
loop {
|
||||
// Pause gate. While the marker is present this loop drives no
|
||||
// turns at all — deliberately *before* the `self_continue.take()`
|
||||
// below, so a `request_next_turn` that raced the pause is still
|
||||
// waiting when the agent resumes rather than being consumed by
|
||||
// a turn that never runs.
|
||||
//
|
||||
// Nothing here touches the broker: not calling `S::recv_next` is
|
||||
// exactly the "messages queue unacked, resume drains the
|
||||
// backlog" semantic, with no fencing and nothing to requeue.
|
||||
// Reminders (unbounded channel) and todo wakes (a `Notify`
|
||||
// permit) buffer on their own. The web UI and MCP daemons run as
|
||||
// separate tasks, so the agent stays inspectable while parked.
|
||||
//
|
||||
// A `GracefulStop` can't be observed while parked, and doesn't
|
||||
// need to be: hive-c0re skips the stop-checkpoint handshake for
|
||||
// a paused agent, because this check sits at the top of the loop
|
||||
// and so a paused agent provably has no turn in flight.
|
||||
if hive_sh4re::paths::paused_marker().exists() {
|
||||
if !was_paused {
|
||||
tracing::info!("pause marker present — parking the turn loop");
|
||||
bus.emit(LiveEvent::Note {
|
||||
text: "paused: turn loop parked, messages will queue".into(),
|
||||
});
|
||||
was_paused = true;
|
||||
}
|
||||
tokio::time::sleep(PAUSE_POLL).await;
|
||||
continue;
|
||||
}
|
||||
if was_paused {
|
||||
tracing::info!("pause marker cleared — resuming the turn loop");
|
||||
bus.emit(LiveEvent::Note {
|
||||
text: "resumed: draining whatever queued while paused".into(),
|
||||
});
|
||||
was_paused = false;
|
||||
}
|
||||
let next = match self_continue.take() {
|
||||
Some(msg) => msg,
|
||||
None => match {
|
||||
|
|
|
|||
Loading…
Reference in a new issue