Hmbown/CodeWhale · error

attempt receipt generation does not match its lease

Error message

attempt receipt generation does not match its lease

What it means

FleetLedger::finalize_task_attempt_if_leased stamps the receipt with the lease's attempt generation (expected_attempts). The guard at crates/tui/src/fleet/ledger.rs:985 rejects the call when the receipt carries an attempt number that differs from the current lease generation. Because each restart increments attempts, this blocks a late process or verifier from terminalizing or publishing evidence for an attempt that was already superseded.

Solutions

  1. Drop or regenerate the stale receipt and rebuild it for the current attempt generation before finalizing.
  2. Verify receipt.attempt == expected_attempts before calling finalize_task_attempt_if_leased.
  3. Skip finalization when the lease generation moved on — the replacement attempt owns the terminal transition now.

Example fix

// before
let receipt = saved_receipt; // attempt 1
ledger.finalize_task_attempt_if_leased(&run_id, &w, &t, current_attempts, &ts, payload, status, receipt)?;
// after
let receipt = if saved_receipt.attempt == Some(current_attempts) {
    saved_receipt
} else {
    build_receipt_for_current_attempt(&run_id, w, t, current_attempts)
};
ledger.finalize_task_attempt_if_leased(&run_id, &w, &t, current_attempts, &ts, payload, status, receipt)?;
Defensive patterns

Strategy: validation

Validate before calling

if receipt.attempt.is_some_and(|a| a != expected_attempts) {
    // regenerate receipt for the current attempt generation before finalizing
}

Type guard

fn receipt_matches_lease(receipt: &FleetReceipt, expected_attempts: u32) -> bool { receipt.attempt.map_or(true, |a| a == expected_attempts) }

Try / catch

match ledger.finalize_task_attempt_if_leased(...) {
    Ok(Some(_)) => { /* finalized */ }
    Ok(None) => { /* lease superseded */ }
    Err(e) if e.to_string().contains("generation does not match its lease") => { /* drop stale receipt */ }
    Err(e) => return Err(e.into()),
}

Prevention

When it happens

Trigger: Finalizing with a receipt whose attempt field is Some(n) where n != expected_attempts — typically a receipt persisted by attempt N being replayed after the task was restarted and is now on attempt N+1.

Common situations: A crashed worker's verifier wakes up after a supervisor restarted the task; replaying queued receipts after a manager restart; stale receipts read from a checkpoint before a restart bumped attempts.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/0e3003673e4b3c65. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/fleet/ledger.rs:985

        if final_status.is_some_and(|status| {
            !matches!(
                status,
                FleetTaskLedgerStatus::Completed
                    | FleetTaskLedgerStatus::Failed
                    | FleetTaskLedgerStatus::Cancelled
            )
        }) {
            bail!("attempt finalization status must be terminal");
        }
        if receipt.run_id != *run_id || receipt.task_id != task_id || receipt.worker_id != worker_id
        {
            bail!("attempt receipt identity does not match its terminal event");
        }
        if receipt
            .attempt
            .is_some_and(|attempt| attempt != expected_attempts)
        {
            bail!("attempt receipt generation does not match its lease");
        }
        self.with_write_lock(|| {
            let state = self.rebuild_state_unlocked()?;
            let key = task_key(&run_id.0, task_id);
            let Some(task) = state.tasks.get(&key) else {
                return Ok(None);
            };
            if task.status != FleetTaskLedgerStatus::Leased
                || task.leased_to.as_deref() != Some(worker_id)
                || task.entry.attempts != expected_attempts
            {
                return Ok(None);
            }
            let event = next_worker_event(&state, run_id, worker_id, task_id, timestamp, payload);
            receipt.attempt = Some(expected_attempts);
            receipt.terminal_seq = Some(event.seq);
            self.append_record_unlocked(&FleetLedgerRecord::TaskAttemptFinalized {
                event: event.clone(),

View on GitHub (pinned to 73e0f67d83)