Hmbown/CodeWhale · error · anyhow::Error

Turn {turn_id} has an indeterminate dynamic-tool receipt; re

Error message

Turn {turn_id} has an indeterminate dynamic-tool receipt; refusing to publish turn completion

What it means

settle_dynamic_tools_for_terminal_turn found at least one dynamic tool call for this (thread_id, turn_id) with entry.indeterminate == true, so it refuses to publish turn completion. An indeterminate call may already have a terminal receipt in the JSONL log; completing the turn on top of that could freeze an ambiguous state into history.

Source

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

        claim: ClaimedDynamicToolSettlement,
        timeout: Duration,
    ) -> Result<()> {
        let ack = self
            .spawn_dynamic_tool_settlement(claim, DynamicToolTerminalOutcome::Timeout { timeout });
        Self::await_dynamic_tool_settlement(ack).await?;
        Ok(())
    }

    async fn settle_dynamic_tools_for_terminal_turn(
        &self,
        thread_id: &str,
        turn_id: &str,
    ) -> Result<()> {
        loop {
            let (claims, mut settling, indeterminate) =
                self.claim_or_watch_pending_dynamic_tools_for_turn(thread_id, turn_id);
            if indeterminate {
                bail!(
                    "Turn {turn_id} has an indeterminate dynamic-tool receipt; refusing to publish turn completion"
                );
            }
            if claims.is_empty() && settling.is_empty() {
                return Ok(());
            }

            let mut first_error = None;
            for claim in claims {
                let ack = self.spawn_dynamic_tool_settlement(
                    claim,
                    DynamicToolTerminalOutcome::Canceled {
                        reason: "turn_terminal",
                        terminal: true,
                    },
                );
                if let Err(error) = Self::await_dynamic_tool_settlement(ack).await
                    && first_error.is_none()

View on GitHub (pinned to 0c42157ee5)

Solutions

  1. Inspect the thread's JSONL log for terminal events of the turn's dynamic tool calls to establish what actually committed
  2. Repair the storage fault, then reconcile the turn state via runtime restart / storage reconciliation instead of forcing completion
  3. If the tombstone is provably stale (no terminal line exists), have the operator clear it - do not add a code path that bypasses the guard
  4. Keep the evidence: this error marks a genuine data-integrity incident
Defensive patterns

Strategy: try-catch

Try / catch

// Turn completion blocked by an indeterminate tool call: escalate.
match manager.complete_turn(thread_id, turn_id).await {
    Ok(_) => Ok(()),
    Err(e) if e.to_string().contains("indeterminate dynamic-tool receipt") => {
        tracing::error!(%thread_id, %turn_id, "turn frozen; inspect Runtime storage");
        Err(e)
    }
    Err(e) => Err(e),
}

Prevention

When it happens

Trigger: A turn ends (normally or via cancellation) after one of its dynamic tool calls failed settlement with a failed rollback (mark_dynamic_tool_claim_indeterminate). The loop at runtime_threads.rs:4344-4360 checks before settling or publishing.

Common situations: Storage failure during a tool-result append mid-turn, then the turn finishes; crash-recovery resuming a turn with a tombstoned call; cancellation racing a flaky disk.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@0c42157ee5 (2026-08-20). Data as JSON: /api/errors/e7d57e23ece75ad7. Report an issue: GitHub.