BigPizzaV3/CodexPlusPlus · error

sidebar catalog row thread_id does not match snapshot

Error message

sidebar catalog row thread_id does not match snapshot

What it means

Every catalog row must carry a thread_id identical to the snapshot's thread_id being restored. This error is thrown when a row's thread_id differs, i.e. the catalog rows do not correspond to the snapshot, so restoring would attach rows to the wrong thread.

Solutions

  1. Remove or fix rows whose thread_id disagrees with the snapshot, then retry the restore
  2. Rebuild the sidebar snapshot from the current database and restore from that fresh snapshot instead of the stale one
  3. Check for concurrent Codex instances writing .codex-global-state.json during restore; close other instances and retry

Example fix

// before
{"thread_id": "aaa-old", "rows": [{"thread_id": "bbb-new"}]}
// after
{"thread_id": "bbb-new", "rows": [{"thread_id": "bbb-new"}]}
Defensive patterns

Strategy: validation

Validate before calling

fn rows_match(entry: &Value, thread_id: &str) -> bool {
    entry.get("rows").and_then(Value::as_array).map(|rows| {
        rows.iter().all(|r| r.get("thread_id").and_then(Value::as_str) == Some(thread_id))
    }).unwrap_or(false)
}

Type guard

fn row_thread<'a>(row: &'a Value, expected: &str) -> Option<&'a str> {
    row.get("thread_id").and_then(Value::as_str).filter(|t| *t == expected)
}

Try / catch

if let Err(e) = restore(&state) {
    if e.to_string().contains("does not match snapshot") {
        // discard stale snapshot and rebuild from the live database
    }
}

Prevention

When it happens

Trigger: Restoring sidebar state where any row in an entry has a 'thread_id' string that does not equal the snapshot thread id — mixed state from concurrent edits, stale rows after thread deletion/recreation, or manual JSON editing.

Common situations: Sidebar data changed between snapshot and restore; rows copied from another snapshot; UUIDs regenerated after re-login or session reset; partially written state from a crash.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


AI-assisted analysis of BigPizzaV3/CodexPlusPlus@b1ed92e5e4 (2026-09-19). Data as JSON: /api/errors/27ff68504e63edd4. Report an issue: GitHub.

Appendix: source

Thrown at crates/codex-plus-data/src/provider_sync.rs:2730

                .get("db_path")
                .and_then(Value::as_str)
                .map(PathBuf::from)
                .ok_or_else(|| anyhow::anyhow!("sidebar catalog entry is missing db_path"))?;
            let canonical = fs::canonicalize(&path)?;
            if !allowed_paths.contains(&canonical) {
                anyhow::bail!("sidebar catalog database is not an allowed Codex database");
            }
            let rows = entry
                .get("rows")
                .and_then(Value::as_array)
                .ok_or_else(|| anyhow::anyhow!("sidebar catalog entry rows must be an array"))?;
            for row in rows {
                let row_id = row
                    .get("thread_id")
                    .and_then(Value::as_str)
                    .ok_or_else(|| anyhow::anyhow!("sidebar catalog row is missing thread_id"))?;
                if row_id != thread_id {
                    anyhow::bail!("sidebar catalog row thread_id does not match snapshot");
                }
            }
        }
    }
    Ok(())
}

const SIDEBAR_CATALOG_TABLES: [&str; 3] = [
    "local_thread_catalog",
    "thread_timeline_ledger",
    "local_thread_catalog_scan_entries",
];

fn snapshot_thread_from_global_state(
    codex_home: &Path,
    thread_id: &str,
) -> anyhow::Result<Value> {
    let path = codex_home.join(".codex-global-state.json");

View on GitHub (pinned to b1ed92e5e4)