add turn lock to prevent /compact racing with in-flight turns

This commit is contained in:
damocles 2026-05-16 19:57:03 +02:00
parent 25508d7399
commit fca480b86e
3 changed files with 44 additions and 4 deletions

View file

@ -2,6 +2,8 @@ use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use hive_ag3nt::web_ui::TurnLock;
use anyhow::Result;
use clap::{Parser, Subcommand};
use hive_ag3nt::events::{Bus, LiveEvent, TurnState};
@ -71,6 +73,7 @@ async fn main() -> Result<()> {
let login_state = Arc::new(Mutex::new(initial));
let bus = Bus::new();
let files = turn::TurnFiles::prepare(&cli.socket, &label, mcp::Flavor::Agent).await?;
let turn_lock: TurnLock = Arc::new(tokio::sync::Mutex::new(()));
plugins::install_configured(&cli.socket, Some("manager")).await;
tokio::spawn(web_ui::serve(
label,
@ -79,6 +82,7 @@ async fn main() -> Result<()> {
bus.clone(),
cli.socket.clone(),
files.clone(),
turn_lock.clone(),
));
match initial {
LoginState::Online => {
@ -88,6 +92,7 @@ async fn main() -> Result<()> {
login_state,
bus,
&files,
turn_lock,
)
.await
}
@ -102,6 +107,7 @@ async fn main() -> Result<()> {
login_state,
bus,
&files,
turn_lock,
)
.await
}
@ -136,6 +142,7 @@ async fn serve(
state: Arc<Mutex<LoginState>>,
bus: Bus,
files: &turn::TurnFiles,
turn_lock: TurnLock,
) -> Result<()> {
tracing::info!(socket = %socket.display(), "hive-ag3nt serve");
let _ = state; // reserved for future state transitions (turn-loop -> needs-login)
@ -163,7 +170,10 @@ async fn serve(
});
bus.set_state(TurnState::Thinking);
let prompt = format_wake_prompt(&from, &body, unread);
let outcome = turn::drive_turn(&prompt, files, &bus).await;
let outcome = {
let _guard = turn_lock.lock().await;
turn::drive_turn(&prompt, files, &bus).await
};
turn::emit_turn_end(&bus, &outcome);
bus.set_state(TurnState::Idle);
// Failures are unhandled by definition — PromptTooLong is

View file

@ -6,6 +6,8 @@ use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use hive_ag3nt::web_ui::TurnLock;
use anyhow::Result;
use clap::{Parser, Subcommand};
use hive_ag3nt::events::{Bus, LiveEvent, TurnState};
@ -61,6 +63,7 @@ async fn main() -> Result<()> {
let login_state = Arc::new(Mutex::new(initial));
let bus = Bus::new();
let files = turn::TurnFiles::prepare(&cli.socket, &label, mcp::Flavor::Manager).await?;
let turn_lock: TurnLock = Arc::new(tokio::sync::Mutex::new(()));
plugins::install_configured(&cli.socket, None).await;
tokio::spawn(web_ui::serve(
label,
@ -69,14 +72,15 @@ async fn main() -> Result<()> {
bus.clone(),
cli.socket.clone(),
files.clone(),
turn_lock.clone(),
));
match initial {
LoginState::Online => {
serve(&cli.socket, Duration::from_millis(poll_ms), bus, &files).await
serve(&cli.socket, Duration::from_millis(poll_ms), bus, &files, turn_lock).await
}
LoginState::NeedsLogin => {
turn::wait_for_login(&claude_dir, login_state, poll_ms).await;
serve(&cli.socket, Duration::from_millis(poll_ms), bus, &files).await
serve(&cli.socket, Duration::from_millis(poll_ms), bus, &files, turn_lock).await
}
}
}
@ -89,6 +93,7 @@ async fn serve(
interval: Duration,
bus: Bus,
files: &turn::TurnFiles,
turn_lock: TurnLock,
) -> Result<()> {
tracing::info!(socket = %socket.display(), "hive-m1nd serve");
loop {
@ -131,7 +136,10 @@ async fn serve(
});
let prompt = format_wake_prompt(&from, &body, unread);
bus.set_state(TurnState::Thinking);
let outcome = turn::drive_turn(&prompt, files, &bus).await;
let outcome = {
let _guard = turn_lock.lock().await;
turn::drive_turn(&prompt, files, &bus).await
};
turn::emit_turn_end(&bus, &outcome);
bus.set_state(TurnState::Idle);
// Check for messages that arrived during the turn and loop