xai-org/grok-build · error

local-workspace intent mutex poisoned; refuse attach (fail c

Error message

local-workspace intent mutex poisoned; refuse attach (fail closed)

What it means

The process-global ACTIVE_LOCAL_WORKSPACE Mutex<Option<LocalWorkspaceConfig>> tracks which local workspace the session attached to. If a thread panicked while holding the lock, the Mutex is poisoned; set_active_local_workspace deliberately refuses to set the stamp ('fail closed') rather than attach to an unverified workspace.

Source

Thrown at crates/codegen/xai-grok-pager/src/app/session_startup.rs:366

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LocalWorkspaceMode {
    Own,
    Attach,
}
#[cfg(feature = "local-workspace")]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LocalWorkspaceConfig {
    pub mode: LocalWorkspaceMode,
    pub cwd: Option<std::path::PathBuf>,
    pub server_id: Option<String>,
}
#[cfg(feature = "local-workspace")]
static ACTIVE_LOCAL_WORKSPACE: std::sync::Mutex<Option<LocalWorkspaceConfig>> =
    std::sync::Mutex::new(None);
#[cfg(feature = "local-workspace")]
pub fn set_active_local_workspace(cfg: Option<LocalWorkspaceConfig>) -> anyhow::Result<()> {
    let mut guard = ACTIVE_LOCAL_WORKSPACE.lock().map_err(|_| {
        anyhow::anyhow!("local-workspace intent mutex poisoned; refuse attach (fail closed)")
    })?;
    tracing::info!(
        target: crate::views::welcome::workspace_mode::WORKSPACE_MODE_LOG,
        event = if cfg.is_some() {
            "process_stamp_set"
        } else {
            "process_stamp_cleared"
        },
        mode = cfg.as_ref().map(|c| format!("{:?}", c.mode)),
        server_id = cfg.as_ref().and_then(|c| c.server_id.as_deref()),
        cwd = cfg.as_ref().and_then(|c| c.cwd.as_ref().map(|p| p.display().to_string())),
        "local-workspace process-wide intent stamp"
    );
    *guard = cfg;
    Ok(())
}
#[cfg(feature = "local-workspace")]
pub fn active_local_workspace() -> anyhow::Result<Option<LocalWorkspaceConfig>> {

View on GitHub (pinned to bc7f02eddd)

Solutions

  1. Find and fix the panic that poisoned the mutex — check logs just before this error for a panic message
  2. Restart the process; poisoning only lasts for the process lifetime
  3. In the panicking code, avoid unwrapping/logging-panic while holding the lock, or use a recovery on PoisonError if safe
  4. Consider replacing Mutex with a poisoning-resistant pattern (lock().unwrap_or_else(|p| p.into_inner()) only if state is provably valid)

Example fix

// before: panic while holding the lock poisons it
let mut g = ACTIVE_LOCAL_WORKSPACE.lock().unwrap();
g.replace(load_config().expect("config"));
// after: no panic inside the critical section
let cfg = load_config()?;
*ACTIVE_LOCAL_WORKSPACE.lock().map_err(|_| anyhow!("poisoned"))? = cfg;
Defensive patterns

Strategy: try-catch

Validate before calling

// detect a poisoned global before setting the workspace
fn workspace_lock_healthy(m: &std::sync::Mutex<()>) -> bool {
    matches!(m.try_lock(), Ok(_) | Err(std::sync::TryLockError::WouldBlock))
}

Try / catch

if let Err(e) = session_startup::set_active_local_workspace(Some(cfg)) {
    if e.to_string().contains("mutex poisoned") {
        eprintln!("earlier panic corrupted workspace state; restart the process");
    }
    return Err(e);
}

Prevention

When it happens

Trigger: Calling set_active_local_workspace after any earlier thread panicked while holding the ACTIVE_LOCAL_WORKSPACE lock (e.g. a panic inside a closure that had locked it), leaving lock() returning Err(PoisonError).

Common situations: A panic elsewhere in the app while the workspace-intent lock was held (test teardown panics, unexpected unwrap failures), followed by a session fork/welcome flow attempting to set a local workspace.

Related errors


AI-assisted analysis of xai-org/grok-build@bc7f02eddd (2026-08-31). Data as JSON: /api/errors/d12fb5b79b33d208. Report an issue: GitHub.