Hmbown/CodeWhale · error

User-input request '{}' has an indeterminate terminal receip

Error message

User-input request '{}' has an indeterminate terminal receipt; inspect Runtime storage before completing turn '{turn_id}'

What it means

Thrown while a turn is being completed: a user-input request registered for this (thread_id, turn_id) has entry.indeterminate == true. That flag is set when the durable append of the user's answer failed AND the transactional rollback also failed, so the JSONL event log may or may not already contain the terminal receipt. The runtime fails closed: it refuses to complete the turn because retrying or publishing could duplicate or disclose a response whose receipt cannot be established.

Source

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

        let key = (thread_id.to_string(), input_id.to_string());
        let mut pending = self.pending_user_inputs.lock();
        if pending.get(&key).is_some_and(|entry| !entry.settling) {
            pending.remove(&key);
        }
    }

    fn claim_pending_user_inputs_for_turn(
        &self,
        thread_id: &str,
        turn_id: &str,
    ) -> Result<(Vec<PendingUserInputRequest>, Vec<watch::Receiver<u64>>)> {
        let mut pending = self.pending_user_inputs.lock();
        if let Some((_, entry)) = pending.iter().find(|((pending_thread_id, _), entry)| {
            pending_thread_id == thread_id
                && entry.request.turn_id == turn_id
                && entry.indeterminate
        }) {
            bail!(
                "User-input request '{}' has an indeterminate terminal receipt; inspect Runtime storage before completing turn '{turn_id}'",
                entry.request.id
            );
        }
        let mut claims = Vec::new();
        let mut settling = Vec::new();
        for ((pending_thread_id, _), entry) in pending.iter_mut() {
            if pending_thread_id != thread_id || entry.request.turn_id != turn_id {
                continue;
            }
            if entry.settling {
                settling.push(entry.settlement_tx.subscribe());
                continue;
            }
            entry.settling = true;
            claims.push(entry.request.clone());
        }
        Ok((claims, settling))

View on GitHub (pinned to 0c42157ee5)

Solutions

  1. Inspect the thread's JSONL event log on disk (Runtime storage) and determine whether the answer receipt line was written
  2. If the line exists, clear the tombstone by reconciling storage (restart the runtime after fixing the storage issue so state is re-derived), otherwise repair/delete the partial tail
  3. Fix the root storage cause (disk space, permissions, path) before restarting, or the flag will be set again on the next failure
  4. Do not write code that retries complete_turn against this error - the guard exists precisely to stop duplicate terminal receipts
Defensive patterns

Strategy: try-catch

Try / catch

// In the turn-completion caller: this error is terminal, never retry it.
match manager.complete_turn(thread_id, turn_id).await {
    Ok(_) => {}
    Err(e) if e.to_string().contains("indeterminate terminal receipt") => {
        // Fail closed: surface to an operator and inspect the JSONL store.
        tracing::error!(%thread_id, %turn_id, error = %e, "turn frozen: inspect Runtime storage");
        return Err(e);
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: complete_turn / turn-termination path calls claim_pending_user_inputs_for_turn after a prior submit_user_input answer hit a non-retry-safe RuntimeEventAppendError (append failed, rollback failed) and mark_pending_user_input_indeterminate ran. Any later attempt to finish that same turn re-hits the guard at runtime_threads.rs:2949-2958.

Common situations: Disk full or permission loss under the runtime store while an answer is being persisted; a crash mid-append; the process was killed between a failed fsync and rollback. The turn is permanently blocked until a human inspects the store.

Related errors


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