Hmbown/CodeWhale · warning
telemetry privacy lock is held
Error message
telemetry privacy lock is held
What it means
read_or_create_install_id serializes its read/mint through the telemetry compaction lock via try_with_lock, which never blocks: when another process already holds the write lock it returns Ok(None), and the caller converts that into this anyhow error. It means concurrent codewhale processes raced on the same telemetry home and this one lost.
Source
Thrown at crates/telemetry/src/envelope.rs:84
let path = buffer::install_id_path(root);
let existing = std::fs::read_to_string(&path)
.ok()
.and_then(|body| serde_json::from_str::<InstallId>(&body).ok())
.filter(|record| uuid::Uuid::parse_str(record.install_id.trim()).is_ok())
.filter(|record| !is_expired(&record.rotated_at));
if let Some(record) = existing {
return Ok(record);
}
let record = InstallId {
schema_version: 1,
install_id: uuid::Uuid::new_v4().to_string(),
rotated_at: now_rfc3339(),
};
codewhale_config::persistence::atomic_write_json(&path, &record)
.with_context(|| format!("failed to write {}", path.display()))?;
Ok(record)
})?
.ok_or_else(|| anyhow::anyhow!("telemetry privacy lock is held"))
}
fn is_expired(rotated_at: &str) -> bool {
let Ok(parsed) = chrono::DateTime::parse_from_rfc3339(rotated_at) else {
// An unreadable timestamp is treated as expired: minting a fresh random
// id is always the safe direction.
return true;
};
let age = chrono::Utc::now().signed_duration_since(parsed.with_timezone(&chrono::Utc));
age.num_days() >= ROTATION_DAYS
}
/// Read `state.json`, or a default when it is missing or unreadable.
#[must_use]
pub fn read_state(root: &Path) -> TelemetryState {
std::fs::read_to_string(buffer::state_path(root))
.ok()
.and_then(|body| serde_json::from_str::<TelemetryState>(&body).ok())View on GitHub (pinned to 8880682c63)
Solutions
- Retry after a short delay: the lock is held only for the duration of one small critical section, so a second attempt usually wins
- Skip telemetry for this run if a bounded retry fails; telemetry is explicitly best-effort
- Serialize telemetry access in your own orchestration so only one process touches the home at a time
- In tests, give each test its own temp root to avoid cross-test lock contention
Example fix
// before
let id = envelope::read_or_create_install_id(&root)?;
// after
let mut attempts = 0;
let id = loop {
match envelope::read_or_create_install_id(&root) {
Ok(id) => break id,
Err(err) if err.to_string() == "telemetry privacy lock is held" && attempts < 3 => {
attempts += 1;
std::thread::sleep(std::time::Duration::from_millis(50));
}
Err(err) => return Err(err),
}
}; Defensive patterns
Strategy: retry
Try / catch
let mut attempts = 0;
let id = loop {
match envelope::read_or_create_install_id(&root) {
Ok(id) => break id,
Err(err) if err.to_string() == "telemetry privacy lock is held" && attempts < 3 => {
attempts += 1;
std::thread::sleep(std::time::Duration::from_millis(50));
}
Err(err) => return Err(err),
}
}; Prevention
- Keep exactly one process per telemetry home doing envelope work at a time
- Give each test its own temp root so suites never contend on the fd-lock
- Treat telemetry as best-effort: losing one read/mint to lock contention is acceptable
- Retry small bounded delays — the critical section is tiny, so contention windows are short
When it happens
Trigger: Two or more processes (a running TUI plus a CLI invocation, parallel scheduled drains, or concurrent tests sharing one CODEWHALE_HOME) call telemetry functions at the same moment; the loser's fd-lock try_write fails and surfaces as 'telemetry privacy lock is held'.
Common situations: Parallel test suites sharing a single home directory; a long-running TUI holding the lock during compaction while a script calls telemetry APIs; overlapping cron-style drains on the same profile.
Related errors
- unknown argument: ${arg}
- --days must be an integer from 1 through 90
- Cloudflare SQL request failed (${response.status}): ${body}
- CF_ACCOUNT_ID and CF_API_TOKEN are required
- xAI OAuth lifecycle lock was poisoned
AI-assisted analysis of Hmbown/CodeWhale@8880682c63 (2026-08-16).
Data as JSON: /api/errors/0145deb8da80a40f.
Report an issue: GitHub.