ultraworkers/claw-code · error

cron registry lock poisoned

Error message

cron registry lock poisoned

What it means

CronRegistry::create (rust/crates/runtime/src/team_cron_registry.rs:153) panics with .expect("cron registry lock poisoned") when the registry's inner Mutex is poisoned. The lock guards CronInner { entries, counter }; poisoning means a prior thread panicked while that lock was held, so the .expect is reporting earlier damage, not a failure of cron entry creation itself.

Source

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

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

#[derive(Debug, Default)]
struct CronInner {
    entries: HashMap<String, CronEntry>,
    counter: u64,
}

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

    pub fn create(&self, schedule: &str, prompt: &str, description: Option<&str>) -> CronEntry {
        let mut inner = self.inner.lock().expect("cron registry lock poisoned");
        inner.counter += 1;
        let ts = now_secs();
        let cron_id = format!("cron_{:08x}_{}", ts, inner.counter);
        let entry = CronEntry {
            cron_id: cron_id.clone(),
            schedule: schedule.to_owned(),
            prompt: prompt.to_owned(),
            description: description.map(str::to_owned),
            enabled: true,
            created_at: ts,
            updated_at: ts,
            last_run_at: None,
            run_count: 0,
        };
        inner.entries.insert(cron_id, entry.clone());
        entry
    }

View on GitHub (pinned to 08106b0c37)

Solutions

  1. Diagnose the original panic (the one that held the lock) from earlier log output; fixing it removes the poisoning.
  2. Restart the process: CronRegistry is in-memory only, so a restart gives a clean mutex.
  3. Change the library to .lock().unwrap_or_else(|e| e.into_inner()) so create() proceeds with a recovered guard; note counter may be mid-increment, so keep the increment inside the same critical section (it already is).
  4. Adopt parking_lot::Mutex for poison-free locking if you can change the dependency.
  5. Ensure the code running inside the registry critical sections cannot panic (no indexing, unwrap, or division in the locked region).

Example fix

// before
let mut inner = self.inner.lock().expect("cron registry lock poisoned");
inner.counter += 1;

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

Strategy: try-catch

Try / catch

let created = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
    cron_registry.create(schedule, prompt, description)
}));
match created {
    Ok(entry) => entry,
    Err(_) => panic::resume_unwind(make_original()), // or degrade the cron subsystem
}

Prevention

When it happens

Trigger: Calling CronRegistry::create(schedule, prompt, description) after any thread panicked inside a CronRegistry method (create/get/list/delete/disable/record_run/len) that held the shared lock — for example a panic between inner.counter += 1 and the entries.insert() in a racing create() call.

Common situations: Scheduler-driven workloads where cron creation happens on timer threads: one panicking tick poisons the registry and every subsequent CronCreate tool call / scheduler fire panics with this message. Also hit in tests that exercise cron lifecycle concurrently and assert-fail mid-critical-section.

Related errors


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