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
- Wait briefly and retry the operation; the claim is transient and held by another live caller.
- Close the other TUI/CLI process currently using the same runtime store.
- Verify only one instance is pointed at the same session/store directory.
- 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
- Run only one TUI/CLI instance per runtime store directory.
- Wrap claims in bounded retry with backoff.
- Clean up stale bindings after detecting a dead owner (pid/liveness check).
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
- another Codewhale process
- Another pet recorder is using this output.
- for
- for
- Goal changed while preparing the turn; retry
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)