ultraworkers/claw-code · error

team registry lock poisoned

Error message

team registry lock poisoned

What it means

This panic fires in TeamRegistry::create (team_cron_registry.rs:68), which allocates a team_{timestamp}_{counter} id and inserts a new Team into the shared Arc<Mutex<TeamInner>> (HashMap + counter). The expect fires when that mutex is poisoned — some thread previously panicked while holding it — making every subsequent team creation panic.

Source

Thrown at rust/crates/runtime/src/team_cron_registry.rs:68

#[derive(Debug, Clone, Default)]
pub struct TeamRegistry {
    inner: Arc<Mutex<TeamInner>>,
}

#[derive(Debug, Default)]
struct TeamInner {
    teams: HashMap<String, Team>,
    counter: u64,
}

impl TeamRegistry {
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    pub fn create(&self, name: &str, task_ids: Vec<String>) -> Team {
        let mut inner = self.inner.lock().expect("team registry lock poisoned");
        inner.counter += 1;
        let ts = now_secs();
        let team_id = format!("team_{:08x}_{}", ts, inner.counter);
        let team = Team {
            team_id: team_id.clone(),
            name: name.to_owned(),
            task_ids,
            status: TeamStatus::Created,
            created_at: ts,
            updated_at: ts,
        };
        inner.teams.insert(team_id, team.clone());
        team
    }

    pub fn get(&self, team_id: &str) -> Option<Team> {
        let inner = self.inner.lock().expect("team registry lock poisoned");
        inner.teams.get(team_id).cloned()

View on GitHub (pinned to 08106b0c37)

Solutions

  1. Reproduce with RUST_BACKTRACE=full and fix the first panic that held the TeamRegistry lock; create() is downstream of it.
  2. Recover the guard — TeamInner is a plain HashMap plus u64 counter: lock().unwrap_or_else(|p| p.into_inner()).
  3. Remove panicking constructs from all TeamRegistry critical sections (use Result-returning error paths).
  4. Wrap orchestration worker entry points in catch_unwind so panics cannot unwind through team mutations.

Example fix

// before
let mut inner = self.inner.lock().expect("team registry lock poisoned");

// after
let mut inner = self.inner.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
Defensive patterns

Strategy: try-catch

Validate before calling

use std::panic::{catch_unwind, AssertUnwindSafe};

// pre-flight: verify the team registry is usable before orchestration
let healthy = catch_unwind(AssertUnwindSafe(|| !registry.list().is_empty() || true)).is_ok();
if !healthy {
    eprintln!("team registry poisoned — aborting team creation");
}

Try / catch

use std::panic::{catch_unwind, AssertUnwindSafe};

let team = catch_unwind(AssertUnwindSafe(|| registry.create(name, task_ids)))
    .unwrap_or_else(|payload| {
        eprintln!("team creation failed — registry poisoned: {payload:?}");
        std::process::exit(101);
    });

Prevention

When it happens

Trigger: Calling TeamRegistry::create(name, task_ids) after any thread panicked while holding the TeamRegistry inner mutex (in create, delete, remove, or any other TeamRegistry method on the shared instance).

Common situations: Team/cron orchestration where a worker thread panicked during team mutation; afterwards every attempt to group tasks into a new team aborts the CLI process. Also seen in threaded tests sharing one TeamRegistry when a prior test panicked.

Related errors


AI-assisted analysis of ultraworkers/claw-code@08106b0c37 (2026-08-18). Data as JSON: /api/errors/1569db791faad0d0. Report an issue: GitHub.