Hmbown/CodeWhale · warning

Runtime is shutting down; recovery receipts remain pending

Error message

Runtime is shutting down; recovery receipts remain pending

What it means

`flush_recovery_receipts_for_thread` drains pending recovery receipts for a thread after acquiring the recovery flush lock. If the runtime's cancel token has fired (shutdown in progress), the flush aborts with this error rather than half-publishing receipts. Pending receipts are intentionally left in `recovery_receipts` so a future run can replay them.

Solutions

  1. Do not treat this as data loss: receipts remain pending and will be flushed on the next run.
  2. Check `cancel_token.is_cancelled()` before initiating a flush and skip gracefully.
  3. Await shutdown completion before starting new turn-completion work that triggers flushing.
  4. If a receipt must not be lost, persist it to the store before shutdown instead of relying on in-memory flush.

Example fix

// before
runtime.flush_recovery_receipts_for_thread(thread_id).await?;
// after
if !runtime.is_shutting_down() {
    runtime.flush_recovery_receipts_for_thread(thread_id).await?;
} else {
    eprintln!("shutdown in progress; receipts deferred");
}
Defensive patterns

Strategy: try-catch

Validate before calling

if cancel_token.is_cancelled() {
    eprintln!("shutdown in progress; deferring recovery receipt flush");
    return Ok(());
}

Try / catch

match runtime.flush_recovery_receipts_for_thread(thread_id).await {
    Err(e) if e.to_string().contains("shutting down") => {
        info!("receipts for {} left pending; will flush next run", thread_id);
    }
    other => other?,
}

Prevention

When it happens

Trigger: Calling flush_recovery_receipts_for_thread (or a turn-completion path that invokes it) while `cancel_token.is_cancelled()` is true — i.e., during runtime shutdown/Ctrl-C.

Common situations: User interrupts the TUI while a turn with recovery receipts is finishing; shutdown races an in-flight flush; tests tearing down the runtime while receipts are pending.

Related errors


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

Appendix: source

Thrown at crates/tui/src/runtime_threads.rs:6907

        }
        self.append_and_broadcast_event(
            &params.thread_id,
            Some(&params.turn_id),
            None,
            "tool_call.canceled",
            payload,
        )
        .await?;
        Ok(true)
    }

    async fn flush_recovery_receipts_for_thread(&self, thread_id: &str) -> Result<()> {
        if !self.recovery_receipts.lock().contains_key(thread_id) {
            return Ok(());
        }
        let _recovery_flush = self.recovery_flush.lock().await;
        if self.cancel_token.is_cancelled() {
            bail!("Runtime is shutting down; recovery receipts remain pending");
        }
        loop {
            let next = self
                .recovery_receipts
                .lock()
                .get(thread_id)
                .and_then(|receipts| receipts.first())
                .cloned();
            let Some(receipt) = next else {
                return Ok(());
            };

            // An in-process monitor failure may leave retry-safe calls in the
            // live registry. Retry their supervised cancellation before the
            // static restart-recovery receipts below. Startup recovery has no
            // live registry entries, so this is a no-op in that case.
            self.settle_dynamic_tools_for_terminal_turn(thread_id, &receipt.turn.id)
                .await?;

View on GitHub (pinned to 73e0f67d83)