{"record":{"id":"78582c35431c87b3","repo":"jdx/mise","slug":"task-action-manifest-contains-duplicate-prediction","errorCode":null,"errorMessage":"task action manifest contains duplicate predictions","messagePattern":"task action manifest contains duplicate predictions","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/mise-cache-core/src/agent.rs","lineNumber":1831,"sourceCode":"    if prediction.payload.len() > MAX_ACTION_PREDICTION_PAYLOAD {\n        bail!(\"action prediction payload is too large\");\n    }\n    serde_json::from_str::<serde_json::Value>(&prediction.payload)?;\n    Ok(())\n}\n\nfn validate_task_manifest(manifest: &TaskActionManifest, task: &str) -> Result<()> {\n    if manifest.version != TASK_ACTION_MANIFEST_VERSION || manifest.task != task {\n        bail!(\"task action manifest has an invalid identity\");\n    }\n    if manifest.predictions.len() > MAX_TASK_ACTION_PREDICTIONS {\n        bail!(\"task action manifest contains too many predictions\");\n    }\n    let mut invocations = BTreeMap::new();\n    for prediction in &manifest.predictions {\n        validate_action_prediction(prediction)?;\n        if invocations.insert(&prediction.invocation, ()).is_some() {\n            bail!(\"task action manifest contains duplicate predictions\");\n        }\n    }\n    Ok(())\n}\n\nfn merge_task_manifests(\n    task: &str,\n    base: Option<TaskActionManifest>,\n    update: TaskActionManifest,\n) -> Result<TaskActionManifest> {\n    validate_task_manifest(&update, task)?;\n    let mut predictions = BTreeMap::new();\n    if let Some(base) = base {\n        validate_task_manifest(&base, task)?;\n        predictions.extend(\n            base.predictions\n                .into_iter()\n                .map(|prediction| (prediction.invocation.clone(), prediction)),","sourceCodeStart":1813,"sourceCodeEnd":1849,"githubUrl":"https://github.com/jdx/mise/blob/6f52dcdf99e282ef7a7db68c81301fa4618d0f79/crates/mise-cache-core/src/agent.rs#L1813-L1849","documentation":"Thrown by validate_task_manifest (crates/mise-cache-core/src/agent.rs:1831) when two predictions in the same manifest share an identical invocation value. The validator inserts each prediction.invocation into a BTreeMap and bails if the key was already present, because the cache resolves predictions by invocation — a duplicate would make the lookup ambiguous.","triggerScenarios":"Passing a manifest to merge_task_manifests (agent.rs:1836) where predictions[i].invocation == predictions[j].invocation for some i != j. Happens when a producer appends predictions for the same command line twice (e.g. merging overlapping runs without keyed dedup, or generating one entry per source file where several produce the identical invocation string).","commonSituations":"Manual manifest construction or a generator loop that emits a default/empty invocation for entries it could not classify; merging two manifests that both contain a common invocation before deduplicating; unstable serialization that collapses distinct invocations to the same string.","solutions":["Deduplicate the predictions array by invocation before persisting/merging (keep the newest payload per invocation)","Fix the producer so it never emits two predictions for one invocation — treat an existing invocation as an update, not an append","Inspect the duplicate invocations: if many are empty strings, the invocation extraction upstream is broken; fix that instead of deduping the symptom","Regenerate the manifest after the fix so the stored file no longer contains duplicates"],"exampleFix":"// before\nlet mut predictions = old_manifest.predictions;\npredictions.extend(new_manifest.predictions); // may duplicate invocations\n\n// after\nlet mut by_invocation: BTreeMap<String, ActionPrediction> = old_manifest.predictions.into_iter().map(|p| (p.invocation.clone(), p)).collect();\nby_invocation.extend(new_manifest.predictions.into_iter().map(|p| (p.invocation.clone(), p)));\nlet predictions = by_invocation.into_values().collect();","handlingStrategy":"validation","validationCode":"use std::collections::BTreeMap;\n\nfn dedupe_predictions(predictions: Vec<ActionPrediction>) -> Vec<ActionPrediction> {\n    predictions\n        .into_iter()\n        .map(|p| (p.invocation.clone(), p))\n        .collect::<BTreeMap<_, _>>()\n        .into_values()\n        .collect()\n}\n// run before persisting/merging","typeGuard":"fn has_unique_invocations(m: &TaskActionManifest) -> bool {\n    let mut seen = std::collections::BTreeSet::new();\n    m.predictions.iter().all(|p| seen.insert(p.invocation.clone()))\n}","tryCatchPattern":"match merge_task_manifests(task, base, update) {\n    Err(e) if e.to_string().contains(\"duplicate predictions\") => {\n        let deduped = dedupe_predictions(update.predictions);\n        merge_task_manifests(task, base, TaskActionManifest { predictions: deduped, ..update })\n    }\n    other => other,\n}","preventionTips":["Key predictions by invocation in your producer (map, not list) so duplicates are structurally impossible","When merging two manifests, upsert by invocation instead of concatenating","Fail loudly in the producer if an invocation repeats — it usually signals a broken extraction step"],"tags":["mise","cache","validation","duplicate-key","task-manifest"],"backgroundTag":"duplicate-key-violation","analyzedSha":"6f52dcdf99e282ef7a7db68c81301fa4618d0f79","analyzedAt":"2026-08-22T10:14:23.840Z","schemaVersion":2},"datasetVersion":"2026-08-23T13:39:53.451Z"}