{"record":{"id":"a551d53133f3b5ea","repo":"jdx/mise","slug":"cannot-record-a-hit-for-a-missing-action-result","errorCode":null,"errorMessage":"cannot record a hit for a missing action result","messagePattern":"cannot record a hit for a missing action result","errorType":"exception","errorClass":"eyre::Report","httpStatus":null,"severity":"error","filePath":"crates/mise-cache-core/src/agent.rs","lineNumber":1612,"sourceCode":"    ) -> Result<Option<RemoteActionResult>> {\n        self.stats\n            .remote_action_lookups\n            .fetch_add(1, Ordering::Relaxed);\n        let _timer = AtomicDurationTimer::start(&self.stats.remote_action_lookup_duration_ns);\n        remote.get_action_result(action).await\n    }\n\n    fn record_action_hit(\n        &self,\n        action: &CacheDigest,\n        restore: RestoreStats,\n    ) -> Result<AgentResponse> {\n        if self.actions.find(action)?.is_none() {\n            let pending = self.pending_remote_actions.lock().unwrap().remove(action);\n            if let Some(result) = pending {\n                self.actions.store(&result)?;\n            } else {\n                bail!(\"cannot record a hit for a missing action result\");\n            }\n        }\n        self.record_restore(restore);\n        self.stats.hits.fetch_add(1, Ordering::Relaxed);\n        Ok(AgentResponse::ActionHitRecorded)\n    }\n\n    fn record_restore(&self, restore: RestoreStats) {\n        self.record_materialization(restore);\n        atomic_saturating_add(&self.stats.restored_output_files, restore.output_files);\n        atomic_saturating_add(&self.stats.restored_output_bytes, restore.output_bytes);\n    }\n\n    fn record_materialization(&self, restore: RestoreStats) {\n        atomic_saturating_add(&self.stats.materialization_duration_ns, restore.duration_ns);\n    }\n\n    fn find_action_prediction(","sourceCodeStart":1594,"sourceCodeEnd":1630,"githubUrl":"https://github.com/jdx/mise/blob/6f52dcdf99e282ef7a7db68c81301fa4618d0f79/crates/mise-cache-core/src/agent.rs#L1594-L1630","documentation":"record_action_hit requires the action result to exist locally (actions.find) or to still sit in pending_remote_actions (speculative prefetch results awaiting first use). If neither holds, the client is claiming a hit for an action whose result the agent never served or prefetched, which is a protocol violation.","triggerScenarios":"A client sends RecordActionHit for a digest it never received from a lookup/prefetch response; the same digest is recorded twice after the pending entry was consumed; the local cache entry was pruned between serve and record.","commonSituations":"Hand-rolled clients speculatively reporting hits; concurrent cache pruning racing a hit record; duplicated request replay in client logic.","solutions":["Only record a hit for digests previously returned by the agent (lookup or prefetch responses)","De-duplicate RecordActionHit requests client-side — never send the same digest twice","If pruning races are expected, re-run the lookup path instead of blindly recording a hit"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// Track served digests client-side and only record hits for them\nlet mut served: HashSet<[u8; 32]> = HashSet::new();\nif let Some(hit) = agent_lookup(&agent, &digest).await? {\n    served.insert(digest);\n    // ... later ...\n    if served.contains(&digest) {\n        agent.record_action_hit(&digest, restore_stats).await?;\n    }\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Never send RecordActionHit speculatively — only for digests actually returned by the agent","De-duplicate hit records client-side (one per digest)","Treat this error as a client protocol bug, not an environment issue"],"tags":["mise-cache","protocol","cache-hit","client-error"],"backgroundTag":"cache-lookup-miss","analyzedSha":"6f52dcdf99e282ef7a7db68c81301fa4618d0f79","analyzedAt":"2026-08-22T10:14:23.840Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}