Hmbown/CodeWhale · error

Workspace already has an active turn

Error message

Workspace already has an active turn (thread {id}); wait for it to finish before restoring files

What it means

File restoration for a workspace is refused when another active turn overlaps that workspace — either the busy thread's workspace contains the target workspace or vice versa (path prefix check in both directions). This prevents restoring files that a running turn may be reading or writing.

Solutions

  1. Wait for all turns in overlapping workspaces (thread {id} named in the message) to finish, then restore.
  2. Cancel the overlapping thread's turn if immediate restore is required.
  3. Restore into a workspace that does not nest under or contain any busy thread's workspace.
  4. Query busy threads first and skip/pause restore for overlapping paths.

Example fix

// before
api.restore_files(&workspace).await?; // may overlap a busy thread

// after
for t in api.list_threads().await? {
    if t.active_turn.is_some() && workspaces_overlap(&t.workspace, &workspace) {
        return Err(format!("wait for thread {} to finish", t.id));
    }
}
api.restore_files(&workspace).await?;
Defensive patterns

Strategy: validation

Validate before calling

for t in api.list_threads().await? {
    if t.active_turn.is_some() {
        let other = t.workspace.canonicalize()?;
        if workspace.starts_with(&other) || other.starts_with(&workspace) {
            return Err(format!("thread {} is busy in an overlapping workspace", t.id));
        }
    }
}

Try / catch

match api.restore_files(&workspace).await {
    Err(e) if e.to_string().contains("already has an active turn") => {
        // parse thread id from message, await its completion, then retry
    },
    other => other?,
}

Prevention

When it happens

Trigger: Calling the restore-files API for workspace W while any thread with an active turn has a workspace that is a path-prefix ancestor or descendant of W (after canonicalization).

Common situations: User restores a snapshot into the project root while a subagent turn runs in `project/src`; nested workspaces share a directory tree; an automation restores files on a timer that collides with running turns.

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@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/62b87faebd9dc966. Report an issue: GitHub.

Appendix: source

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

        let workspace = workspace
            .canonicalize()
            .unwrap_or_else(|_| workspace.to_path_buf());
        // Collect ids under the async lock, then do filesystem and store
        // I/O without holding it.
        let busy: Vec<String> = {
            let active = self.active.lock().await;
            active
                .engines
                .iter()
                .filter(|(_, state)| state.active_turn.is_some())
                .map(|(id, _)| id.clone())
                .collect()
        };
        for id in busy {
            let other = self.store.load_thread(&id)?.workspace;
            let other = other.canonicalize().unwrap_or(other);
            if workspace.starts_with(&other) || other.starts_with(&workspace) {
                bail!(
                    "Workspace already has an active turn (thread {id}); wait for it to finish before restoring files"
                );
            }
        }
        Ok(())
    }

    /// Test seam: mark or clear an active turn on an installed test engine so
    /// route-level tests can exercise restore admission.
    #[cfg(test)]
    pub(crate) async fn set_active_turn_for_test(
        &self,
        thread_id: &str,
        turn_id: Option<&str>,
    ) -> Result<()> {
        let mut active = self.active.lock().await;
        let state = active
            .engines

View on GitHub (pinned to 73e0f67d83)