Hmbown/CodeWhale · error

Steer settlement task ended before acknowledgement

Error message

Steer settlement task ended before acknowledgement

What it means

Thrown when the steer settlement task's oneshot channel closes with an error before delivering an acknowledgement — i.e. the spawned settlement task terminated (panicked, was cancelled, or its sender was dropped) without reporting either Accepted or Dropped. The caller cannot know whether the steer was delivered.

Solutions

  1. Retry the steer after confirming the thread's engine is healthy and the turn is still InProgress.
  2. Check runtime logs for a panic or cancellation in the settlement task; if it is a recurring crash, fix or report the task bug.
  3. As a safe fallback, start a new turn with the steering message rather than assuming it was delivered.

Example fix

// before
let turn = runtime.steer(thread_id, turn_id, item).await?;
// after
match runtime.steer(thread_id, turn_id, item).await {
    Ok(turn) => turn,
    Err(e) if e.to_string().contains("settlement task ended") => {
        // unknown delivery: safest is a fresh turn with the same item
        runtime.start_turn(thread_id, item).await?
    }
    Err(e) => return Err(e),
}
Defensive patterns

Strategy: try-catch

Try / catch

match runtime.steer(thread_id, turn_id, item).await {
    Err(e) if e.to_string().contains("settlement task ended") => {
        log::error("steer settlement channel closed; delivery unknown");
        runtime.start_turn(thread_id, item).await // safe fallback
    }
    other => other,
}

Prevention

When it happens

Trigger: The `settle_rx` receiver gets `Err(_)` from the oneshot channel during the STEER_SETTLE_WAIT await — the settlement task ended abnormally (panic in the task, task aborted during shutdown, sender dropped without sending).

Common situations: Runtime shutdown or engine teardown cancels the settlement task mid-await; a bug or panic inside the settlement task; thread engine replaced while settlement was pending.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


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

Appendix: source

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

            // engine and durable record agree even if the API caller drops.
            let outcome_rx = permit.send_with_outcome(prompt.clone());
            touch_lru(&mut active.lru, thread_id);
            queued_turn = turn.clone();
            self.spawn_steer_settlement(turn, item, prompt, outcome_rx)
        };

        // The settler owns the verdict; this is only an observation of it.
        // Steering a streaming turn settles in milliseconds, so the caller
        // almost always gets the truth in-band. Behind a long tool call the
        // engine will not look at its mailbox for minutes, and an API request
        // must not hang that long — so the wait is bounded and the caller
        // falls back to the honest `Queued` receipt it already has.
        match tokio::time::timeout(STEER_SETTLE_WAIT, settle_rx).await {
            Ok(Ok((SteerOutcome::Accepted, turn))) => Ok(turn),
            Ok(Ok((SteerOutcome::Dropped, _))) => bail!(
                "Turn {turn_id} moved on before the steer reached the model; {STEER_DROPPED_REASON}"
            ),
            Ok(Err(_)) => bail!("Steer settlement task ended before acknowledgement"),
            Err(_) => Ok(queued_turn),
        }
    }

    pub async fn compact_thread(
        &self,
        thread_id: &str,
        req: CompactThreadRequest,
    ) -> Result<TurnRecord> {
        // Compaction carries a concrete provider route just like a normal
        // turn. Keep the same reload/admission boundary through durable engine
        // handoff so it cannot dispatch an old credential or endpoint after a
        // successful config reload.
        let _config_admission = self.config_admission.read().await;
        self.ensure_accepting_execution()?;
        let thread = self.get_thread(thread_id).await?;
        let engine = self.ensure_engine_loaded(&thread).await?;

View on GitHub (pinned to 73e0f67d83)