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
- Drop or regenerate the stale receipt and rebuild it for the current attempt generation before finalizing.
- Verify receipt.attempt == expected_attempts before calling finalize_task_attempt_if_leased.
- 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
- Invalidate stored receipts whenever a task restart increments attempts.
- Check the lease generation from freshly rebuilt ledger state, not a cached snapshot.
- Design verifiers/supervisors to no-op when their attempt generation is no longer current.
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
- attempt finalization requires a terminal worker event
- attempt finalization status must be terminal
- attempt receipt identity does not match its terminal event
- conditional progress append does not accept terminal worker…
- conditional terminal append requires a terminal worker event
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)