jdx/mise · error

remote cache directory graph contains a cycle

Error message

remote cache directory graph contains a cycle

What it means

When materializing a remote cache directory tree, the walker tracks the set of digests on the current path (ancestors). If a directory blob's digest is already among its own ancestors, the remote CAS graph is cyclic and would recurse forever, so mise bails. This protects against corrupt or malicious remote data.

Solutions

  1. Delete the affected cache entry from the remote store and re-run the task to repopulate it.
  2. Verify the remote store's digests against content (each digest must be the hash of its blob) and purge mismatches.
  3. Audit any custom cache-writing tooling for digest computation or deduplication bugs.
  4. Fall back to running the task with cache disabled to unblock local work while the remote entry is fixed.
Defensive patterns

Strategy: validation

Validate before calling

function hasCycle(dir, ancestors = new Set()) {
  if (ancestors.has(dir.digest)) return true;
  ancestors.add(dir.digest);
  return dir.childDirs.some(c => hasCycle(c, new Set(ancestors)));
}

Prevention

When it happens

Trigger: A remote directory blob references a child directory whose digest equals an ancestor already on the traversal stack — only possible with a corrupted, hand-edited, or maliciously crafted remote cache entry set.

Common situations: Manual tampering with blobs in the remote object store; a buggy custom cache server that computed/reused digests incorrectly; store corruption from partial writes or blob aliasing.

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


AI-assisted analysis of jdx/mise@533346cc37 (2026-09-17). Data as JSON: /api/errors/f4d459e788c85c60. Report an issue: GitHub.

Appendix: source

Thrown at src/task/task_cache_store.rs:715

        digest: CacheDigest,
        executable: bool,
        mode: u32,
    },
    Symlink {
        mode: u32,
        target: PathBuf,
    },
}

async fn materialize_remote_tree(
    store: &HttpTaskCacheStore,
    root: &CacheDigest,
) -> Result<tempfile::NamedTempFile> {
    let mut pending = vec![(PathBuf::new(), root.clone(), BTreeSet::new())];
    let mut nodes = BTreeMap::<PathBuf, RestoredNode>::new();
    while let Some((path, digest, mut ancestors)) = pending.pop() {
        if !ancestors.insert(digest.clone()) {
            bail!("remote cache directory graph contains a cycle");
        }
        let bytes = store.client.get_blob(&digest, DIRECTORY_MEDIA_TYPE).await?;
        let directory: RemoteDirectory = serde_json::from_slice(&bytes)?;
        if canonical_json(&serde_json::to_value(&directory)?)? != bytes {
            bail!("remote cache directory is not canonical JSON");
        }
        if directory.version != 1 {
            bail!("unsupported remote cache directory version");
        }
        let mut names = BTreeSet::new();
        for directory in directory.directories {
            validate_cache_name(&directory.name)?;
            if !names.insert(directory.name.clone()) {
                bail!("remote cache directory contains duplicate names");
            }
            let child = path.join(&directory.name);
            validate_cache_path(&child)?;
            nodes.insert(

View on GitHub (pinned to 533346cc37)