Hmbown/CodeWhale · warning

Runtime turn operation is already being claimed; retry

Error message

Runtime turn operation is already being claimed; retry

What it means

Claiming an exclusive runtime turn operation uses a lock file created with create_new semantics. If the create fails with WouldBlock or Interrupted, another process or thread already holds the claim, so this caller is told to retry later. Any other io::Error is wrapped as a generic claim failure.

Solutions

  1. Wait briefly and retry the operation; the claim is transient and held by another live caller.
  2. Close the other TUI/CLI process currently using the same runtime store.
  3. Verify only one instance is pointed at the same session/store directory.
  4. If the lock is stale (owning process is gone), remove the leftover binding file and retry.

Example fix

// before: one-shot claim panics on contention
let claim = store.claim_turn_op(key)?;

// after: bounded retry loop
let claim = loop {
    match store.claim_turn_op(key) {
        Ok(c) => break c,
        Err(e) if e.to_string().contains("already being claimed") => {
            tokio::time::sleep(Duration::from_millis(50)).await;
        }
        Err(e) => return Err(e),
    }
};
Defensive patterns

Strategy: retry

Validate before calling

// No pre-call check possible; contention is detected by the claim attempt itself.
// Optionally check an existing binding before claiming:
if store.peek_turn_binding(key)?.is_some() {
    eprintln!("turn operation already claimed");
}

Try / catch

match store.claim_turn_op(key) {
    Ok(c) => c,
    Err(e) if e.to_string().contains("already being claimed") => {
        tokio::time::sleep(Duration::from_millis(100)).await;
        store.claim_turn_op(key)?
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Two processes (or two TUI instances sharing the same runtime store) call the turn-claim API concurrently; one wins the create_new, the other gets WouldBlock/Interrupted and hits this bail.

Common situations: Running multiple codewhale CLI/TUI instances against the same session directory; a stale lock held by a still-running process; concurrent tooling (scripts) poking the store while the UI runs.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/6ad617b5076cef23. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/runtime_threads.rs:1958

        let mut claim =
            fd_lock::RwLock::new(self.open_turn_operation_claim_lock(operation_key_fingerprint)?);
        let _guard = self.acquire_turn_operation_claim(&mut claim)?;
        operation()
    }

    fn acquire_turn_operation_claim<'a>(
        &self,
        claim: &'a mut fd_lock::RwLock<File>,
    ) -> Result<fd_lock::RwLockWriteGuard<'a, File>> {
        match claim.try_write() {
            Ok(guard) => Ok(guard),
            Err(error)
                if matches!(
                    error.kind(),
                    std::io::ErrorKind::WouldBlock | std::io::ErrorKind::Interrupted
                ) =>
            {
                bail!("Runtime turn operation is already being claimed; retry")
            }
            Err(error) => Err(error).context("Failed to claim Runtime turn operation"),
        }
    }

    /// Remove a binding left before its turn record by a process crash.
    ///
    /// Bindings are committed before turns, while engine submission happens
    /// only after both are durable. A binding with no turn therefore never
    /// reached the engine and is safe to discard during startup recovery.
    fn recover_incomplete_turn_operations(&self) -> Result<()> {
        let operations_dir = checked_existing_runtime_store_dir(&self.turn_operations_dir)?;
        for entry in fs::read_dir(&operations_dir)
            .with_context(|| format!("Failed to read {}", operations_dir.display()))?
        {
            let path = entry?.path();
            if path.extension().is_none_or(|extension| extension != "json") {
                continue;

View on GitHub (pinned to 73e0f67d83)