Hmbown/CodeWhale · error

Turn lifecycle task ended before acknowledgement

Error message

Turn lifecycle task ended before acknowledgement

What it means

Raised when the oneshot channel that carries the turn's acceptance acknowledgement is dropped before a value arrives, meaning the spawned turn-lifecycle task ended (panicked, was aborted, or exited early) without ever accepting or rejecting the prepared turn. The runtime converts the RecvError into this anyhow error instead of hanging forever.

Solutions

  1. Check the application logs/stderr for a panic in the turn lifecycle task around the time of this error.
  2. Retry the turn — the store is consistent, so a fresh submission is safe.
  3. If it reproduces, ensure the Tokio runtime is not being shut down (or the task aborted) while turns are in flight.
  4. Report as a bug if reproducible: the lifecycle task should always send an acceptance or a rejection.
Defensive patterns

Strategy: retry

Try / catch

match send_turn(prompt) {
    Err(e) if e.to_string().contains("ended before acknowledgement") => {
        // check logs for lifecycle-task panic, then resubmit the turn once
    }
    r => r?,
}

Prevention

When it happens

Trigger: Awaiting `acceptance_rx` after spawning the claimed-turn monitor; the sender side is dropped because the lifecycle task panicked or was cancelled before sending the turn record through.

Common situations: A panic inside the engine turn task (bug or OOM kill); the Tokio runtime shutting down while a turn is being submitted; the lifecycle task being aborted by engine teardown during thread unload or app exit.

Related errors


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

Appendix: source

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

                policy.permission,
                configured_sandbox_mode,
            );
            engine.send_reserved_op(permit, op);
            if let Some(settlement) = initial_routed_usage_settlement.as_mut() {
                settlement.disarm();
            }
            touch_lru(&mut active.lru, thread_id);
            self.spawn_claimed_turn_monitor(
                turn.clone(),
                Some(user_item),
                engine.clone(),
                ClaimedTurnKind::Message,
            )
        };

        let turn = acceptance_rx
            .await
            .map_err(|_| anyhow!("Turn lifecycle task ended before acknowledgement"))?
            .map_err(anyhow::Error::msg)?;
        Ok((turn, false))
        })
        .await
    }

    pub async fn interrupt_turn(&self, thread_id: &str, turn_id: &str) -> Result<TurnRecord> {
        {
            let mut active = self.active.lock().await;
            let Some(active_thread) = active.engines.get_mut(thread_id) else {
                bail!("Thread is not loaded");
            };
            let Some(active_turn) = active_thread.active_turn.as_mut() else {
                bail!("No active turn on thread {thread_id}");
            };
            if active_turn.turn_id != turn_id {
                bail!("Turn {turn_id} is not active on thread {thread_id}");
            }

View on GitHub (pinned to 73e0f67d83)