libnyanpasu/clash-nyanpasu · error

materialization operation has mixed transaction families

Error message

materialization operation has mixed transaction families

What it means

converge_materialization_journals found journals for the same operation id in multiple transaction-family locations, and not all of them belong to the same family. Each journal location belongs to a transaction family; mixing families means journals from different protocol generations or transaction types coexist for one id, which cannot be safely ordered, so it aborts.

Solutions

  1. Determine which family is current for this app version and remove the foreign-family journals for this operation id, then rerun reconcile.
  2. Roll the operation back with a manual cleanup of all its journals/staged files and re-materialize with a new operation id.
  3. Avoid restoring staging directories from backups made by different app versions; only restore profiles, not journals.
  4. If this follows a version downgrade, upgrade back or clean the staging root before using the service.

Example fix

// before
// journals from two transaction families exist for one operation id
// after
for path in foreign_family_journal_paths(root, operation_id) {
    std::fs::remove_file(path)?; // keep only current family
}
client.reconcile(root).await?;
Defensive patterns

Strategy: try-catch

Validate before calling

// before resuming, ensure all journals for this op belong to one family
let families: HashSet<_> = existing_journal_families(root, op_id).into_iter().collect();
if families.len() > 1 {
    // remove journals of the obsolete family or compensate from scratch
    remove_foreign_family_journals(root, op_id, current_family)?;
}

Try / catch

match client.reconcile(root).await {
    Err(e) if e.to_string().contains("mixed transaction families") => {
        // unrecoverable automatically: purge this op and re-materialize
        purge_operation(root, op_id)?;
        let fresh = client.allocate_operation_id(root).await?;
        /* start over */
        bail!("operation reset due to mixed journal families")
    }
    r => r,
}

Prevention

When it happens

Trigger: The same operation_id has journal files under locations of different JournalLocation::ALL families — e.g. journals copied between app versions with different layouts, or an operation id reused across transaction families.

Common situations: Upgrading/downgrading the app while an operation was mid-flight; manually copying staging state between installations; restoring backups that mix journal layouts.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of libnyanpasu/clash-nyanpasu@f7dbce2997 (2026-09-08). Data as JSON: /api/errors/3dd6b6d049cff234. Report an issue: GitHub.

Appendix: source

Thrown at backend/tauri/src/service/profile_file.rs:1154

                    return Err(error)
                        .with_context(|| format!("inspect journal {}", path.display()));
                }
            }
        }
        Self::converge_materialization_journals(found)
    }

    fn converge_materialization_journals(
        mut found: Vec<(JournalLocation, MaterializationJournal, PathBuf)>,
    ) -> anyhow::Result<Option<(JournalLocation, MaterializationJournal)>> {
        let Some(family) = found.first().map(|(location, _, _)| location.family()) else {
            return Ok(None);
        };
        if found
            .iter()
            .any(|(location, _, _)| location.family() != family)
        {
            bail!("materialization operation has mixed transaction families");
        }
        found.sort_by_key(|(location, _, _)| location.rank());
        let (location, journal, _) = found
            .pop()
            .expect("non-empty materialization journal set has a preferred phase");
        for (duplicate_location, duplicate_journal, duplicate_path) in found {
            if duplicate_journal != journal {
                bail!(
                    "materialization operation has conflicting journal payloads in {duplicate_location:?}"
                );
            }
            Self::remove_private_regular(&duplicate_path)?;
        }
        Ok(Some((location, journal)))
    }

    /// The journal locations share one private root, so Unix `rename` is an
    /// atomic same-filesystem phase transition. Do not use `move_atomic`: its

View on GitHub (pinned to f7dbce2997)