Hmbown/CodeWhale · error · anyhow

Reviewed source is unavailable; refresh the import preview

Error message

Reviewed source is unavailable; refresh the import preview

What it means

During the review-apply transaction, the candidate list is re-discovered inside the config-mutation closure and matched against the id encoded in the review token. If no candidate with that id exists anymore, the token refers to a source that vanished. Codewhale refuses to apply against stale review state instead of guessing.

Solutions

  1. Re-run `/mcp import` to generate a fresh preview and new approve/decline tokens, then use the new token.
  2. Verify the source config file still exists and is discoverable (correct home dir, plugin/workspace paths).
  3. If the source was removed intentionally, decline or skip it — do not retry the old token.

Example fix

// before
codewhale mcp import apply <stale-token>
// after
codewhale mcp import preview   # regenerate tokens
codewhale mcp import apply <fresh-token>
Defensive patterns

Strategy: retry

Validate before calling

let (candidates, _) = context.discover();
if !candidates.iter().any(|c| candidate_id(c) == id) {
    // token is stale: regenerate preview before applying
}

Try / catch

match apply_reviewed_import(...) {
    Err(e) if e.to_string().contains("Reviewed source is unavailable") => {
        // regenerate preview tokens via /mcp import and retry once
    }
    other => other?,
}

Prevention

When it happens

Trigger: Running `/mcp import approve <token>` (or `mcp_import_apply`) where the external source file that the token was generated from was deleted, renamed, or is no longer discoverable (e.g. the other MCP client was uninstalled or its config moved).

Common situations: The user generated the preview, then uninstalled/deleted the source app (e.g. removed Claude Desktop config); discovery happens on a different machine/container without the source config present.

Understand the failure class

Background: "Not found" and "does not exist" errors: why "Task not found", "No such folder", and "Can't find" fire when a lookup comes back empty — this error's family across 14 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/1347d9b632ec107f. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/mcp/external_import.rs:677

/// completed import into a false failed-write receipt.
pub fn apply_reviewed_import(
    context: &ImportContext<'_>,
    id: &str,
    hash: &str,
    revision: &str,
    decision: ImportDecision,
) -> anyhow::Result<ImportReceipt> {
    anyhow::ensure!(
        matches!(decision, ImportDecision::Approve | ImportDecision::Decline),
        "Choose approve or decline"
    );
    let (candidate, revision) = super::mutate_config(context.mcp_path, Some(revision), |config| {
        let (candidates, _) = context.discover();
        let candidate = candidates
            .into_iter()
            .find(|candidate| candidate_id(candidate) == id)
            .ok_or_else(|| {
                anyhow::anyhow!("Reviewed source is unavailable; refresh the import preview")
            })?;
        anyhow::ensure!(
            candidate.content_hash == hash,
            "Source changed; refresh the import preview"
        );
        if decision == ImportDecision::Approve {
            anyhow::ensure!(
                !source_blocked(context, &candidate),
                "Source is disabled or its workspace is untrusted"
            );
            anyhow::ensure!(
                !context.merged()?.servers.contains_key(&candidate.name)
                    && !config.servers.contains_key(&candidate.name),
                "A managed, project or plugin connector already uses this name"
            );
            let mut server = candidate.server.clone();
            server.enabled = false;
            server.disabled = true;

View on GitHub (pinned to 73e0f67d83)