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

  1. Retry the resolve after a short bounded backoff — the condition clears when the park persist completes.
  2. In bots, add a small settle delay or subscribe to a park-persisted signal before approving.
  3. If persistent, inspect engine logs for park persist errors (disk, DB) and remediate those.
  4. 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

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


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/59ab7b2673c73443. Report an issue: GitHub.