zeroclaw-labs/zeroclaw · warning · anyhow::Error
cannot record approval vote for run {run_id}: its parked sna
Error message
cannot record approval vote for run {run_id}: its parked snapshot is not yet durably persisted (retrying) What it means
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.
Source
Thrown at crates/zeroclaw-runtime/src/sop/approval/broker.rs:523
if super::resolve::is_rejected_by_approval_mode(
engine.config().approval_mode,
&principal,
) {
return Ok(BrokerOutcome::Resolved(
ResolveOutcome::RejectedSelfApproval,
));
}
// Refuse to record a quorum vote while the run's parked snapshot has
// not yet been durably persisted (A-core's `is_park_persist_pending`).
// A quorum vote is recorded BEFORE `resolve_gate` runs (only the FINAL
// vote that reaches quorum calls it), so `resolve_gate`'s own pending-
// persist guard cannot protect the first N-1 votes: recording one now
// would durably outlive the run if its park never manages to persist
// and is lost across a restart (an orphaned `gate_vote` row for a run
// that no longer exists). Fail closed BEFORE the vote append, matching
// `resolve_gate`'s own pre-claim/pre-ledger discipline.
if engine.is_park_persist_pending(run_id) {
anyhow::bail!(
"cannot record approval vote for run {run_id}: its parked snapshot is not yet durably persisted (retrying)"
);
}
let gate_revision = engine
.get_run(run_id)
.map(|run| run.revision)
.unwrap_or_default();
// Quorum > 1: durably record this vote under both the CURRENT policy
// and CURRENT gate presentation, so neither policy reloads nor a later
// visit to the same step can reuse a stale vote.
engine.record_gate_vote(run_id, step, policy_name, gate_revision, &principal)?;
// Count only votes cast under the current policy whose voter is STILL a
// member of the current required group - so a mid-flight policy or group
// change cannot let a stale vote count toward the new quorum. Propagates
// a gate-ledger read failure (the vote above is durably recorded, so
// failing here leaves the gate waiting for a retry, not a bogus quorum).
let have = self.count_qualified_voters(
engine,View on GitHub (pinned to 88bb9c8533)
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.
Example fix
# before (approval bot)
on_gate_notice -> broker.resolve(run_id, Approve) # races the park persist
# after
on_gate_notice -> wait_until { !engine.is_park_persist_pending(run_id) } or sleep 250ms
broker.resolve(run_id, Approve) Defensive patterns
Strategy: retry
Validate before calling
if engine.is_park_persist_pending(run_id) {
return ScheduleRetry::after(Duration::from_millis(250));
}
broker.resolve(run_id, ApprovalDecision::Approve).await?; Try / catch
let mut delay = Duration::from_millis(200);
loop {
match broker.resolve(run_id, ApprovalDecision::Approve).await {
Ok(out) => break out,
Err(e) if e.to_string().contains("not yet durably persisted") && delay <= Duration::from_secs(10) => {
tokio::time::sleep(delay).await;
delay *= 2;
}
Err(e) => return Err(e),
}
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- cannot record checkpoint approval vote for run {run_id}: its
- run {run_id} is parked at an approval gate; amend/revise app
- Run {} cannot resume: its parked checkpoint snapshot is not
- Gateway responded {status}: {detail}
- run {} ({}) cannot resume yet: execution slots are full; it
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/59ab7b2673c73443.
Report an issue: GitHub.