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

@ -36,6 +36,12 @@ use crate::turn::TurnFiles;
/// render.
pub type LoginStateCell = Arc<Mutex<LoginState>>;
/// Shared turn lock. The serve loop acquires this (as an async mutex) for the
/// duration of every `drive_turn` call. The `/api/compact` handler tries
/// `try_lock()` and rejects immediately if a turn is in flight, preventing
/// concurrent access to the claude session.
pub type TurnLock = Arc<tokio::sync::Mutex<()>>;
#[derive(Clone)]
struct AppState {
label: String,
@ -48,6 +54,8 @@ struct AppState {
/// settings claude saw on the last regular turn — keeps the
/// session shape identical across compact + normal turns.
files: TurnFiles,
/// Prevents `/api/compact` from racing with an in-flight normal turn.
turn_lock: TurnLock,
}
impl AppState {
@ -70,6 +78,7 @@ pub async fn serve(
bus: Bus,
socket: PathBuf,
files: TurnFiles,
turn_lock: TurnLock,
) -> Result<()> {
let state = AppState {
label,
@ -78,6 +87,7 @@ pub async fn serve(
bus,
socket,
files,
turn_lock,
};
let app = Router::new()
.route("/", get(serve_index))
@ -406,9 +416,21 @@ async fn post_set_model(State(state): State<AppState>, Form(form): Form<ModelFor
}
async fn post_compact(State(state): State<AppState>) -> Response {
// Clone the Arc before locking so the guard's lifetime is tied to the
// clone (which we can move into the spawn) rather than to `state`.
let lock = state.turn_lock.clone();
// Reject immediately if a normal turn is in flight — concurrent access
// to the claude session is unsafe and produces garbled output.
let guard = match lock.try_lock_owned() {
Ok(g) => g,
Err(_) => {
return error_response("turn in flight — wait for it to finish before compacting");
}
};
let bus = state.bus.clone();
let files = state.files.clone();
tokio::spawn(async move {
let _guard = guard; // keep lock alive for the duration of compaction
bus.emit(crate::events::LiveEvent::Note(
"operator: /compact — running on persistent session".into(),
));