hive-agent: surface an interrupted-turn banner in the wake prompt after /cancel

This commit is contained in:
damocles 2026-08-01 18:25:41 +02:00
commit a11f945532
5 changed files with 72 additions and 6 deletions

View file

@ -498,6 +498,11 @@ async fn serve_main<S: Surface>(socket: &Path, poll_ms: u64) -> Result<()> {
tracing::info!(state = ?initial, claude_dir = %claude_dir.display(), "harness boot");
let login_state = Arc::new(Mutex::new(initial));
let bus = Bus::new();
// Set by the web UI's `/api/cancel` on a successful SIGINT, read-and-
// cleared by `handle_turn` before building the next wake prompt — see
// `hive_sh4re::INTERRUPTED_HINT`. Shared between the web server task and
// the serve loop the same way `bus`/`todo_wake` are.
let interrupted = Arc::new(std::sync::atomic::AtomicBool::new(false));
let stats = TurnStats::open_default();
if let Some(s) = &stats {
let (ctx, cost) = s.last_usage();
@ -535,10 +540,11 @@ async fn serve_main<S: Surface>(socket: &Path, poll_ms: u64) -> Result<()> {
login_state.clone(),
bus.clone(),
socket.to_path_buf(),
interrupted.clone(),
);
tokio::spawn(async move {
let (label, port, login_state, bus, socket) = web_ui_args;
if let Err(e) = web_ui::serve(label, port, login_state, bus, socket).await {
let (label, port, login_state, bus, socket, interrupted) = web_ui_args;
if let Err(e) = web_ui::serve(label, port, login_state, bus, socket, interrupted).await {
tracing::error!(error = %e, "web_ui::serve exited with error");
}
});
@ -595,6 +601,7 @@ async fn serve_main<S: Surface>(socket: &Path, poll_ms: u64) -> Result<()> {
todo_wake,
todos_store,
reminder_rx,
interrupted,
)
.await
}
@ -619,6 +626,7 @@ async fn serve_loop<S: Surface>(
todo_wake: Arc<tokio::sync::Notify>,
todos_store: Option<Arc<todos::Todos>>,
mut reminder_rx: tokio::sync::mpsc::UnboundedReceiver<hive_sh4re::DeliveredMessage>,
interrupted: Arc<std::sync::atomic::AtomicBool>,
) -> Result<()> {
tracing::info!(socket = %socket.display(), "harness serve");
S::requeue_inflight(socket).await;
@ -725,13 +733,23 @@ async fn serve_loop<S: Surface>(
files,
&session,
graceful_stop_message(),
&interrupted,
)
.await;
S::graceful_stop_complete(socket).await;
return Ok(());
}
};
let ctrl = handle_turn::<S>(socket, &bus, stats.as_ref(), files, &session, next).await;
let ctrl = handle_turn::<S>(
socket,
&bus,
stats.as_ref(),
files,
&session,
next,
&interrupted,
)
.await;
if ctrl.auth_failed {
*login_state.lock().unwrap() = LoginState::NeedsLogin;
login::wait_for_login(
@ -756,6 +774,7 @@ async fn handle_turn<S: Surface>(
files: &turn::TurnFiles,
session: &turn::AgentSession,
first: hive_sh4re::DeliveredMessage,
interrupted: &std::sync::atomic::AtomicBool,
) -> TurnControl {
let from = first.from;
let body = first.body;
@ -773,7 +792,18 @@ async fn handle_turn<S: Surface>(
let started_at = chrono::Utc::now().timestamp();
let started_instant = std::time::Instant::now();
let model_at_start = bus.model();
let prompt = serve_common::format_wake_prompt(msg_id, &from, &body, unread, redelivered);
// Read-and-clear: this wake prompt is the one turn that gets to carry
// the "you were interrupted" banner, then the flag resets so a later
// ordinary turn doesn't repeat a stale notice.
let was_interrupted = interrupted.swap(false, std::sync::atomic::Ordering::Relaxed);
let prompt = serve_common::format_wake_prompt(
msg_id,
&from,
&body,
unread,
redelivered,
was_interrupted,
);
let outcome = turn::drive_turn(&prompt, files, bus, session).await;
turn::emit_turn_end(bus, &outcome);
bus.set_state(TurnState::Idle);