{"record":{"id":"fc4e2ee115f9b9b7","repo":"jdx/mise","slug":"local-action-key-already-has-a-different-result","errorCode":null,"errorMessage":"local action key already has a different result","messagePattern":"local action key already has a different result","errorType":"exception","errorClass":"eyre::Report","httpStatus":null,"severity":"error","filePath":"crates/mise-cache-core/src/local.rs","lineNumber":224,"sourceCode":"        for digest in [\n            Some(&result.action),\n            result.metadata.as_ref(),\n            result.output_root.as_ref(),\n        ]\n        .into_iter()\n        .flatten()\n        {\n            if self.cas.find(digest)?.is_none() {\n                bail!(\"cannot publish an action result with a missing blob\");\n            }\n        }\n        let destination = self.path_for(&result.action)?;\n        let replace_invalid = match self.find(&result.action) {\n            Ok(Some(existing)) => {\n                if existing == *result {\n                    return Ok(destination);\n                }\n                bail!(\"local action key already has a different result\");\n            }\n            Ok(None) => false,\n            Err(_) => true,\n        };\n        let parent = destination\n            .parent()\n            .expect(\"action-result path has a parent\");\n        fs::create_dir_all(parent)?;\n        let mut temporary = tempfile::NamedTempFile::new_in(parent)?;\n        temporary.write_all(&canonical_json(result)?)?;\n        temporary.flush()?;\n        temporary.as_file().sync_all()?;\n        if replace_invalid {\n            temporary\n                .persist(&destination)\n                .map_err(|error| error.error)?;\n            return Ok(destination);\n        }","sourceCodeStart":206,"sourceCodeEnd":242,"githubUrl":"https://github.com/jdx/mise/blob/6f52dcdf99e282ef7a7db68c81301fa4618d0f79/crates/mise-cache-core/src/local.rs#L206-L242","documentation":"`LocalActionCache::store` is append-once per action key: if a valid result already exists under the same digest and differs from the new one, the store aborts. An identical result is an idempotent no-op, and an invalid existing entry is replaced - only a different, valid result conflicts. Since blake3 collisions are practically impossible, a conflict almost always means the action key derivation is not deterministic: two genuinely different actions hash to the same key.","triggerScenarios":"Two runs of the same action digest producing different `RemoteActionResult`s, typically because the action key was computed from a subset of the real inputs (tool version, env vars, flags, or platform not included in the hash), while metadata or output_root digests vary between runs.","commonSituations":"Non-hermetic tasks whose outputs differ; action keys hashed from display strings, paths, or timestamps; one digest reused for parameterized actions.","solutions":["Make the action key cover every input that can change the result: command line, environment, tool version, platform.","If the stored entry is stale or wrong, delete the JSON at `action-results/v1/blake3/<xx>/<hash>-<size>.json` and re-store.","Compare the existing and new results field by field (metadata/output_root digests usually diverge) to identify which input was not hashed.","Never reuse one action digest for parameterized actions."],"exampleFix":"// before: one digest for parameterized actions\nlet action = CacheDigest::blake3(cmd.as_bytes());\n\n// after: include every input that changes the result\nlet action = CacheDigest::blake3(&canonical_action_key(cmd, &env, &tool_version, platform));","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"match actions.store(&result) {\n    Ok(path) => Ok(path),\n    Err(err) if err.to_string().contains(\"already has a different result\") => {\n        // keep the existing entry; the conflict is deterministic, do not retry\n        warn!(\"action key collision: action digest does not cover all inputs\");\n        Ok(existing_path)\n    }\n    Err(err) => Err(err),\n}","preventionTips":["Hash every input that can change the result into the action key: command, env, tool version, platform.","Never reuse one action digest for parameterized actions.","Investigate the first conflict immediately - later conflicts get cached under the wrong key."],"tags":["local-cache","action-cache","determinism","cache-key","hash-collision","rust"],"backgroundTag":"cache-key-collision","analyzedSha":"6f52dcdf99e282ef7a7db68c81301fa4618d0f79","analyzedAt":"2026-08-22T10:14:23.840Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}