libnyanpasu/clash-nyanpasu · error
replacement target remains in the candidate snapshot
Error message
replacement target remains in the candidate snapshot
What it means
This panic fires in the profiles actor when committing a profile-item replacement: the code clones the pre-commit snapshot `before`, then expects the just-resolved uid to still be present in `next.items`. The invariant is that the same snapshot in which the uid was looked up and validated is the one being mutated, so the key must exist. If it is missing, the candidate snapshot was mutated or swapped concurrently — a bug in actor state handling, not a caller mistake.
Source
Thrown at backend/tauri/src/state/profiles/actor.rs:1961
(definition.source().is_none() || old_path != canonical)
.then_some(old_path)
});
// A changed remote definition receives a durable empty
// placeholder instead of retaining stale bytes. Its next
// refresh replaces it through the file-first protocol.
let resource = if same_slot {
Ok(None)
} else {
Self::resource_for_definition(state, &definition, None).await
};
match resource {
Err(error) => Err(error),
Ok(resource) => {
let mut next = before.clone();
let item = next
.items
.get_mut(&uid)
.expect("replacement target remains in the candidate snapshot");
item.set_definition(definition);
Self::commit_state_first(
&myself,
state,
expected_version,
before,
next,
AffectsRule::Touched(uid),
resource.map(|resource| (canonical, resource)),
cleanup_path,
None,
)
.await
}
}
}
};
let _ = reply.send(result);View on GitHub (pinned to f7dbce2997)
Solutions
- Verify the `before` snapshot passed to this code is the same snapshot the uid lookup ran against; do not re-clone from a later state.
- Ensure the actor handles one commit at a time (ractor serialization) and no other code mutates `next.items` between the `get` and `get_mut` calls.
- Add a debug assert/log right before `get_mut` to dump the snapshot keys when this fires.
- If you cannot reach this code path legitimately, treat any occurrence as an actor bug and report it with the request that triggered it.
Example fix
// before
let item = next
.items
.get_mut(&uid)
.expect("replacement target remains in the candidate snapshot");
// after
let item = next.items.get_mut(&uid).ok_or_else(|| {
ProfilesError::ProfileNotFound(uid.clone())
})?; Defensive patterns
Strategy: type-guard
Validate before calling
debug_assert!(
next.items.contains_key(&uid),
"uid {uid} missing from candidate snapshot before mutation"
); Type guard
fn target_in_snapshot(snapshot: &ProfilesSnapshot, uid: &Uid) -> bool {
snapshot.items.contains_key(uid)
} Try / catch
let Some(item) = next.items.get_mut(&uid) else {
return Err(ProfilesError::ProfileNotFound(uid.clone()));
}; Prevention
- Always mutate the exact snapshot that was validated; never re-clone from a later state
- Keep uid lookup and mutation adjacent with no intervening snapshot swaps
- Cover the commit path with an actor test that replaces an existing item
When it happens
Trigger: Invoking the profile item update/replacement command on the profiles actor (actor.rs:1961) where the uid was found in `state`/`before` but the cloned `next` snapshot no longer contains the key — e.g. snapshot lifecycle broken between validation and mutation.
Common situations: Developers refactoring the actor's commit path (e.g. rebasing snapshot clones, reordering `commit_state_first`) and accidentally mutating a different snapshot than the one holding the target item; corrupted in-memory state after a failed partial commit.
Understand the failure class
Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.
Related errors
- uid-derived path is always a valid managed path
- application actor call timed out
- clash config actor reply dropped
- clash config actor call timed out
- session state actor reply dropped
AI-assisted analysis of libnyanpasu/clash-nyanpasu@f7dbce2997 (2026-09-08).
Data as JSON: /api/errors/6a53d5d5caf57d7c.
Report an issue: GitHub.