Hmbown/CodeWhale · warning

Runtime turn operation is already being claimed; retry

Error message

Runtime turn operation is already being claimed; retry

What it means

The runtime refuses to claim a turn operation because another process/thread already holds the claim. Claiming is done through an exclusive create (O_CREAT|O_EXCL-style) file binding, and when the OS reports WouldBlock or Interrupted the code converts it into this bail so callers know to retry rather than treat it as corruption. It is a transient contention signal, not a data error.

Solutions

  1. Retry the turn operation after a short backoff; the claim is transient and the competing holder usually releases it.
  2. Check for another running Codewhale instance on the same runtime store and close it.
  3. If a stale claim persists after a crash, use the store's stale-binding removal path (remove a binding left before its turn record by a process crash).
  4. Ensure only one instance uses the same runtime store directory at a time.

Example fix

// before
let claim = claim_turn_operation(&store)?; // panics/aborts on contention
// after
match claim_turn_operation(&store) {
    Err(e) if e.to_string().contains("already being claimed") => retry_with_backoff(),
    other => other?,
}
Defensive patterns

Strategy: retry

Validate before calling

// No pre-check possible; contention is dynamic.
let claim = loop {
    match claim_turn_operation(&store) {
        Ok(c) => break c,
        Err(e) if e.to_string().contains("already being claimed") => std::thread::sleep(std::time::Duration::from_millis(100)),
        Err(e) => return Err(e),
    }
};

Try / catch

match claim_turn_operation(&store) {
    Err(e) if e.to_string().contains("already being claimed") => retry_with_backoff(), // transient
    Err(e) => return Err(e.context("claim failed")),
    Ok(claim) => use_claim(claim),
}

Prevention

When it happens

Trigger: Calling the runtime claim path (claim of a Runtime turn operation binding) while the on-disk binding is locked/held by another process, or the claim syscall is interrupted (EINTR).

Common situations: Two TUI/CLI instances sharing the same runtime store directory; a crashed or hung previous run that still holds the claim; concurrent turn operations racing during startup.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


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

Appendix: source

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

        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 433685b202)