{"record":{"id":"59ab7b2673c73443","repo":"zeroclaw-labs/zeroclaw","slug":"cannot-record-approval-vote-for-run-run-id-its","errorCode":null,"errorMessage":"cannot record approval vote for run {run_id}: its parked snapshot is not yet durably persisted (retrying)","messagePattern":"cannot record 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":523,"sourceCode":"                if super::resolve::is_rejected_by_approval_mode(\n                    engine.config().approval_mode,\n                    &principal,\n                ) {\n                    return Ok(BrokerOutcome::Resolved(\n                        ResolveOutcome::RejectedSelfApproval,\n                    ));\n                }\n                // Refuse to record a quorum vote while the run's parked snapshot has\n                // not yet been durably persisted (A-core's `is_park_persist_pending`).\n                // A quorum vote is recorded BEFORE `resolve_gate` runs (only the FINAL\n                // vote that reaches quorum calls it), so `resolve_gate`'s own pending-\n                // persist guard cannot protect the first N-1 votes: recording one now\n                // would durably outlive the run if its park never manages to persist\n                // and is lost across a restart (an orphaned `gate_vote` row for a run\n                // that no longer exists). Fail closed BEFORE the vote append, matching\n                // `resolve_gate`'s own pre-claim/pre-ledger discipline.\n                if engine.is_park_persist_pending(run_id) {\n                    anyhow::bail!(\n                        \"cannot record approval vote for run {run_id}: its parked snapshot is not yet durably persisted (retrying)\"\n                    );\n                }\n                let gate_revision = engine\n                    .get_run(run_id)\n                    .map(|run| run.revision)\n                    .unwrap_or_default();\n                // Quorum > 1: durably record this vote under both the CURRENT policy\n                // and CURRENT gate presentation, so neither policy reloads nor a later\n                // visit to the same step can reuse a stale vote.\n                engine.record_gate_vote(run_id, step, policy_name, gate_revision, &principal)?;\n                // Count only votes cast under the current policy whose voter is STILL a\n                // member of the current required group - so a mid-flight policy or group\n                // change cannot let a stale vote count toward the new quorum. Propagates\n                // a gate-ledger read failure (the vote above is durably recorded, so\n                // failing here leaves the gate waiting for a retry, not a bogus quorum).\n                let have = self.count_qualified_voters(\n                    engine,","sourceCodeStart":505,"sourceCodeEnd":541,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-runtime/src/sop/approval/broker.rs#L505-L541","documentation":"Same durability guard as the checkpoint path, applied to approval votes in resolve(): before appending a vote under a named quorum policy, the broker checks is_park_persist_pending(run_id). If the parked snapshot is not yet durable, recording the first N-1 votes is refused so no gate_vote row can outlive a park lost across a restart. Transient — retry after the flush.","triggerScenarios":"broker.resolve() with ApprovalDecision::Approve under a named quorum policy (>1 required), called before the run's park snapshot has been durably persisted — the immediate post-park window, widened by slow storage or rapid scripted approvals.","commonSituations":"Bot-driven approval flows that respond instantly to a gate notice; multi-approver queues racing the persist; disk pressure delaying snapshot writes; test suites that park and vote with no yield in between.","solutions":["Retry the resolve after a short bounded backoff — the condition clears when the park persist completes.","In bots, add a small settle delay or subscribe to a park-persisted signal before approving.","If persistent, inspect engine logs for park persist errors (disk, DB) and remediate those.","Do not work around by manipulating the ledger directly — the guard prevents orphaned vote rows."],"exampleFix":"# before (approval bot)\non_gate_notice -> broker.resolve(run_id, Approve)   # races the park persist\n\n# after\non_gate_notice -> wait_until { !engine.is_park_persist_pending(run_id) } or sleep 250ms\nbroker.resolve(run_id, Approve)","handlingStrategy":"retry","validationCode":"if engine.is_park_persist_pending(run_id) {\n    return ScheduleRetry::after(Duration::from_millis(250));\n}\nbroker.resolve(run_id, ApprovalDecision::Approve).await?;","typeGuard":null,"tryCatchPattern":"let mut delay = Duration::from_millis(200);\nloop {\n    match broker.resolve(run_id, ApprovalDecision::Approve).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":["Approval bots: settle briefly (or await the park-persisted signal) after a gate notice before resolving.","Classify 'retrying' errors as a distinct retryable category in your broker client from day one.","Monitor persist latency; persistently hitting this guard signals storage problems, not logic bugs."],"tags":["sop","approval","broker","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"}