Hmbown/CodeWhale · error
Task store is busy; state is unavailable
Error message
Task store is busy; state is unavailable
What it means
Opening the task store requires acquiring an exclusive cross-process file lock (RuntimeProcessOwnerLock). The manager retries with exponential backoff (5ms doubling to 50ms) until a deadline; if the lock is still held at the deadline, it gives up and reports the store as busy/unavailable.
Solutions
- Close the other process using the same task store, or wait and retry the operation
- Remove the stale lock file only after confirming no live owner process exists
- Point this instance at a different task-store directory to avoid contention
Example fix
// before: parallel instances contending on the same store codewhale --task-store ~/.codewhale/tasks & codewhale --task-store ~/.codewhale/tasks & // busy // after: isolate per session codewhale --task-store /tmp/cw-tasks-$SESSION_ID
Defensive patterns
Strategy: retry
Validate before calling
if store_lock_is_held(&path)? { eprintln!("store busy; retry later"); return Ok(()); } Type guard
fn store_available(path: &Path) -> bool {
RuntimeProcessOwnerLock::try_acquire_file(path, true).map(|o| o.is_some()).unwrap_or(false)
} Try / catch
let mut attempts = 0;
loop {
match manager.load_state().await {
Err(e) if e.to_string().contains("Task store is busy") && attempts < 5 => {
attempts += 1;
tokio::time::sleep(Duration::from_millis(100 * attempts)).await;
}
other => break other?,
}
} Prevention
- Run a single instance per task-store directory
- After a crash, verify and clear stale lock files before restarting
- Keep critical sections short so locks are released quickly
When it happens
Trigger: Calling any state-loading operation on the task store while another process holds RuntimeProcessOwnerLock for the whole backoff window (deadline exceeded).
Common situations: Two TUI/CLI instances running against the same task store; a crashed process leaving a stale lock file; very long-running foreground operation in a peer process exceeding the (short) wait deadline.
Understand the failure class
Background: Request timed out: what client-side request timeouts mean across libraries (Request timed out, TIMED_OUT, APITimeoutError) — this error's family across 39 libraries.
Related errors
- for
- another Codewhale process
- Another pet recorder is using this output.
- child wall-time budget exhausted
- for
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/d7f0e8afa104811c.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/task_manager.rs:2967
/// Acquire the cross-process task-store lock.
///
/// Polls with exponential backoff (5ms → 50ms) instead of a flat 5ms
/// interval: under contention the old shape woke ~200×/s for up to its
/// whole five-second deadline (#6211 R7c). The deadline and the busy
/// error are unchanged. What this does not do: it does not add the
/// in-process mutex the issue also suggested — in-process contenders
/// just back off against the same file lock.
async fn lock_store(&self) -> Result<RuntimeProcessOwnerLock> {
let path = self.cfg.data_dir.join("task-store.lock");
let deadline = Instant::now() + Duration::from_secs(5);
let mut wait = Duration::from_millis(5);
loop {
if let Some(owner) = RuntimeProcessOwnerLock::try_acquire_file(&path, true)? {
return Ok(owner);
}
if Instant::now() >= deadline {
bail!("Task store is busy; state is unavailable");
}
sleep(wait).await;
wait = (wait * 2).min(Duration::from_millis(50));
}
}
fn refresh_locked(&self, state: &mut ManagerState) -> Result<()> {
let loaded = load_state(&self.tasks_dir, &self.queue_path)?;
state.tasks = loaded.tasks;
state.queue = loaded.queue;
for (id, events) in &state.pending_events {
let task = state
.tasks
.get_mut(id)
.context("Pending task disappeared")?;
self.require_execution_owner(task)?;
for event in events {
self.apply_event_to_task(task, event.clone())?;View on GitHub (pinned to 73e0f67d83)