{"record":{"id":"080ed40d91712768","repo":"zeroclaw-labs/zeroclaw","slug":"cannot-record-checkpoint-approval-vote-for-run-ru","errorCode":null,"errorMessage":"cannot record checkpoint approval vote for run {run_id}: its parked snapshot is not yet durably persisted (retrying)","messagePattern":"cannot record checkpoint approval vote for run (.+?): its parked snapshot is not yet durably persisted \\(retrying\\)","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"warning","filePath":"crates/zeroclaw-runtime/src/sop/approval/broker.rs","lineNumber":400,"sourceCode":"        };\n\n        if matches!(decision, ApprovalDecision::Deny { .. }) {\n            return Ok(None);\n        }\n        let Some((decision_label, decision_identity)) = checkpoint_decision_identity(decision)\n        else {\n            return Ok(None);\n        };\n        let checkpoint_revision = engine\n            .get_run(run_id)\n            .map(|run| run.revision)\n            .unwrap_or_default();\n        let need = policy.1.quorum.max(1) as usize;\n        if need <= 1 {\n            return Ok(None);\n        }\n        if engine.is_park_persist_pending(run_id) {\n            anyhow::bail!(\n                \"cannot record checkpoint approval vote for run {run_id}: its parked snapshot is not yet durably persisted (retrying)\"\n            );\n        }\n        engine.record_checkpoint_gate_vote(\n            run_id,\n            step,\n            &policy.0,\n            checkpoint_revision,\n            decision_label,\n            &decision_identity,\n            principal,\n        )?;\n        let have = self.count_qualified_voters(\n            engine,\n            run_id,\n            step,\n            &policy.0,\n            policy.1.required_group.as_deref().filter(|g| !g.is_empty()),","sourceCodeStart":382,"sourceCodeEnd":418,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-runtime/src/sop/approval/broker.rs#L382-L418","documentation":"While recording a checkpoint approval vote, the broker checks engine.is_park_persist_pending(run_id). If the run's parked snapshot has not yet been durably written, the vote is refused: a vote recorded now could durably outlive a park that is lost on restart (an orphaned gate_vote row for a nonexistent run). This is a deliberate fail-closed, transient condition — it clears once the park flush completes.","triggerScenarios":"authorize_checkpoint (reached via resolve_via_broker) with a quorum policy needing >1 vote, invoked in the window right after the run parks but before its snapshot write reaches durable storage. More likely under slow disks, heavy IO, or immediately-scripted vote sequences.","commonSituations":"Automated multi-approver scripts that vote in rapid succession after a gate opens; CI pipelines approving a checkpoint within milliseconds of park; slow or saturated storage delaying the park persist; restart-adjacent races in tests.","solutions":["Retry the vote after a short delay (with bounded backoff) — the error is explicitly retryable and self-clears.","If it persists, check engine logs for park-persist failures (disk full, DB errors) and fix the underlying IO problem.","In automation, treat this message as a 'try again' signal, not a failure — do not re-create the run.","Verify the runs's park state settled (run shows parked durably) before firing the second and later votes."],"exampleFix":"// before\nbroker.authorize_checkpoint(run_id, step, decision).await?; // fails on first retry-window hit\n\n// after\nlet mut backoff = Duration::from_millis(200);\nloop {\n    match broker.authorize_checkpoint(run_id, step, decision.clone()).await {\n        Ok(out) => break out,\n        Err(e) if e.to_string().contains(\"not yet durably persisted\") && backoff <= Duration::from_secs(10) => {\n            tokio::time::sleep(backoff).await;\n            backoff *= 2;\n        }\n        Err(e) => return Err(e),\n    }\n}","handlingStrategy":"retry","validationCode":"if engine.is_park_persist_pending(run_id) {\n    // wait or reschedule; do not call authorize_checkpoint yet\n    return ScheduleRetry::after(Duration::from_millis(250));\n}","typeGuard":null,"tryCatchPattern":"let mut delay = Duration::from_millis(200);\nloop {\n    match broker.authorize_checkpoint(run_id, step, vote.clone()).await {\n        Ok(out) => break out,\n        Err(e) if e.to_string().contains(\"not yet durably persisted\") && delay <= Duration::from_secs(10) => {\n            tokio::time::sleep(delay).await;\n            delay *= 2;\n        }\n        Err(e) => return Err(e),\n    }\n}","preventionTips":["In approval automation, subscribe to the park-persisted event (or check is_park_persist_pending) before voting.","Treat 'not yet durably persisted (retrying)' messages as retryable by contract, never as terminal failures.","Keep bounded backoff (sub-second start, cap of a few seconds) — the flush normally completes quickly."],"tags":["sop","approval","checkpoint","quorum","transient","retry","durability"],"backgroundTag":"write-pending-retry","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}