libnyanpasu/clash-nyanpasu · error

{error:#}

Error message

{error:#}

What it means

The bare `{error:#}` message is the top-level wrapper `legacy_mutation_partial` applies to any failure in the post-mutation reconcile phase of `run_legacy_verge_mutation`: preparing/restoring the legacy file, refreshing the legacy projection, fetching clash config, YAML conversion, building the typed patch plan, or the typed commit. The mutation itself succeeded, but rolling forward to typed state (or restoring the file after a partial failure) failed, so the result is a 'partial mutation' — committed legacy state with incomplete typed reconciliation.

Source

Thrown at backend/tauri/src/bridge/verge.rs:290

        // Reason: feat::patch_verge still executes OS effects while producing legacy state.
        // Remove when: side effects are prepared and committed by typed domain services.
        let desired = self.legacy_store.snapshot()?;
        let patch = legacy_patch_between(&previous, &desired)?;
        // Captured before `patch` is moved into `desired.patch_config(patch)`
        // below: the post-commit reconcile must build the runtime config from
        // the just-committed typed state, never from the pre-commit draft
        // (AGENTS.md section 10: commit first, then side effects).
        let reconcile_tun = patch.enable_tun_mode.is_some();
        let restore = self
            .legacy_store
            .prepare_restore(&managed.legacy_verge_path, previous)
            .map_err(|error| Self::legacy_mutation_partial(error, None))?;
        if let Err(error) = restore.commit() {
            return Err(Self::legacy_mutation_partial(error, None));
        }

        let base = self.refresh_legacy_projection().await.map_err(|error| {
            Self::legacy_mutation_partial(anyhow::anyhow!(format!("{error:#}")), Some(error))
        })?;
        let clash = managed.client.get_clash_config().await.map_err(|error| {
            Self::legacy_mutation_partial(anyhow::anyhow!(format!("{error:#}")), Some(error))
        })?;
        let legacy_clash = super::yaml_convert(&clash.overrides)
            .map_err(|error| Self::legacy_mutation_partial(error, None))?;
        let plan = Self::typed_patch_plan(base.clone(), &patch, &legacy_clash)
            .map_err(|error| Self::legacy_mutation_partial(error, None))?;
        let mut desired = base;
        desired.patch_config(patch);
        let finalize = self
            .legacy_store
            .prepare_commit(&managed.legacy_verge_path, desired)
            .map_err(|error| Self::legacy_mutation_partial(error, None))?;

        match self
            .apply_typed_config_patch_plan(plan, move || finalize.commit())
            .await

View on GitHub (pinned to f7dbce2997)

Solutions

  1. Inspect the inner `error:#` chain (and `Some(error)` cause when present) to see which reconcile step failed — projection refresh, clash fetch, yaml_convert, plan build, or commit.
  2. If the core is not running or IPC failed, restart the core/client and re-run the patch; legacy state is committed but the typed projection is stale.
  3. Fix malformed clash overrides YAML that breaks `yaml_convert`, or unsupported patch fields missing a typed mapping in `typed_patch_plan`.
  4. Check filesystem permissions on the verge config path if `prepare_restore`/`restore.commit()`/`prepare_commit` reported the failure.
  5. Report/verify degraded state to the UI: legacy config was persisted, so avoid blind re-patching with the same payload until the root cause is fixed.
Defensive patterns

Strategy: fallback

Validate before calling

// pre-flight the reconcile dependencies before mutating
async fn reconcile_ready(managed: &Managed) -> anyhow::Result<()> {
    managed.client.get_clash_config().await?; // core reachable
    let path = &managed.legacy_verge_path;
    assert!(path.parent().map(|p| p.is_ok()).unwrap_or(false), "bad config path");
    Ok(())
}

Type guard

fn has_partial_cause(err: &ClientError) -> Option<&anyhow::Error> {
    err.cause() // legacy_mutation_partial embeds the original error when present
}

Try / catch

match bridge.patch_verge_config(payload).await {
    Err(e) => {
        // legacy state may already be committed; do NOT blindly re-patch
        match e.cause() {
            Some(inner) if inner.to_string().contains("get_clash_config") => restart_core_and_resync().await?,
            Some(inner) if inner.to_string().contains("yaml") => fix_overrides_yaml().await?,
            _ => ui.report_degraded("patch applied to legacy config; typed reconcile failed"),
        }
    }
    Ok(()) => {},
}

Prevention

When it happens

Trigger: After `mutate()` succeeds: `restore.commit()` fails while unwinding a previous failure, `refresh_legacy_projection()` or `get_clash_config()` errors (core not running / IPC failure), `super::yaml_convert` fails on malformed overrides, `typed_patch_plan` rejects the diff, or `prepare_commit`/`apply_typed_config_patch_plan` fails.

Common situations: mihomo core is down so `get_clash_config` times out; clash overrides YAML is invalid so yaml_convert fails; the verge.yaml file became unwritable mid-operation; a patch field has no typed mapping so the plan builder errors.

Related errors


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