Hmbown/CodeWhale · error

turn monitor exits normally only after assigning a terminal…

Error message

turn monitor exits normally only after assigning a terminal status

What it means

Panic from `turn_status.expect(...)` at the end of the turn monitor loop. The monitor's exit paths should always assign a terminal status before breaking out of the select loop; reaching the expect with None means the loop ended without ever producing a terminal turn status, indicating an unhandled event path or premature loop exit.

Solutions

  1. Add a terminal-status assignment to every loop-exit path (including channel closure)
  2. Log unmatched select branches so silent exits become visible
  3. Replace the bare expect with an explicit error status fallback for unexpected exits

Example fix

// before
let mut turn_status = turn_status
    .expect("turn monitor exits normally only after assigning a terminal status");
// after
let turn_status = turn_status.unwrap_or_else(|| {
    tracing::error!("turn monitor exited without terminal status");
    TerminalStatus::error("turn monitor exited without terminal status")
});
Defensive patterns

Strategy: validation

Type guard

if turn_status.is_none() { tracing::error!("monitor exited without terminal status"); turn_status = Some(TerminalStatus::error("unexpected exit")); }

Try / catch

let turn_status = turn_status.unwrap_or_else(|| TerminalStatus::error("turn monitor exited without terminal status"));

Prevention

When it happens

Trigger: The turn monitor's select loop terminating via a channel close, cancellation, or an unmatched event branch without any branch setting `turn_status` to a Terminal variant.

Common situations: Upstream stream ends or errors in a way not mapped to a terminal status; a refactor removing an assignment path in the monitor's match arms.

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/7f7de60ed353f671. Report an issue: GitHub.

Appendix: source

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

                        // only when the store still has none — a concurrent
                        // PUT/DELETE is the newer revision and wins — and
                        // adopt the revision only when we actually created it,
                        // so settlement and later checkpoints stay fenced.
                        if let Some(goal_id) = self
                            .store
                            .create_goal_from_snapshot_if_absent(&thread_id, &snapshot)?
                        {
                            admitted_goal_id = Some(goal_id);
                        }
                    }
                    latest_goal_snapshot = Some(snapshot);
                }
                _ => {}
            }
        }

        let mut turn_status = turn_status
            .expect("turn monitor exits normally only after assigning a terminal status");

        if self
            .is_interrupt_requested(&thread_id, &turn_id)
            .await
            .unwrap_or(false)
        {
            turn_status = RuntimeTurnStatus::Interrupted;
        }

        if let Some(mut item) = current_message_item.take() {
            item.status = match turn_status {
                RuntimeTurnStatus::Completed => TurnItemLifecycleStatus::Completed,
                RuntimeTurnStatus::Interrupted | RuntimeTurnStatus::Canceled => {
                    TurnItemLifecycleStatus::Interrupted
                }
                RuntimeTurnStatus::Queued
                | RuntimeTurnStatus::InProgress
                | RuntimeTurnStatus::Failed => TurnItemLifecycleStatus::Failed,

View on GitHub (pinned to 73e0f67d83)