Hmbown/CodeWhale · error
settlement turn already belongs to another thread
Error message
settlement turn {turn_id} already belongs to another thread What it means
When applying routed-usage settlement, the store loads the settlement turn if it already exists on disk. If the existing turn's `thread_id` differs from the thread the settlement is being recorded for, the operation bails rather than re-parenting a turn to another thread — this protects turn/thread ownership invariants.
Solutions
- Use the thread id that owns the existing turn when recording settlement.
- Generate a fresh turn id for the new thread instead of reusing the colliding one.
- Investigate why `unaccepted_routed_usage_turn_id` produced an id already owned by another thread (id derivation bug or stale batch).
Example fix
// before store.save_settlement_turn(&wrong_thread_id, &batch)?; // after let existing = store.load_turn(&turn_id)?; store.save_settlement_turn(&existing.thread_id, &batch)?; // record under the owning thread
Defensive patterns
Strategy: try-catch
Validate before calling
fn settlement_thread_ok(existing_thread: &str, target: &str) -> bool { existing_thread == target } Try / catch
match store.save_settlement_turn(thread_id, batch) { Err(e) if e.contains("already belongs to another thread") => record_under_owning_thread(e), Err(e) => return Err(e.into()), Ok(id) => Ok(id) } Prevention
- Derive turn ids deterministically from the thread id so collisions cannot cross threads
- Never reuse a turn id across threads
- Verify thread id before replaying settlement batches
When it happens
Trigger: Calling the settlement save with a `thread_id` different from the one the existing turn file was created under, i.e. the same turn id is claimed by two threads.
Common situations: A turn id collision or reuse across threads; a thread id changed after the turn was first written; replaying settlement batches against a re-keyed thread.
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
- A cancelled Operation cannot be edited.
- An existing Fleet artifact contains different bytes
- Archive this pet recording before accepting more telemetry.
- Automation admission belongs to a different task store; it…
- AutomationEditorConflict
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/2690a4677b6096ec.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/runtime_threads.rs:1273
///
/// The record is terminal on creation and deliberately carries no parent
/// `usage`, no parent route, and no thread-pointer update: the parent turn was
/// never accepted, so only the auxiliary call's own frozen route may be
/// charged. Because no engine `TurnComplete` will ever arrive for it, this
/// record — unlike an accepted turn — also owns the coverage gap its bounded
/// ledger could not represent. Every persisted field is re-derived from the
/// batch, so replaying the same settlement rewrites the same values.
fn settle_unaccepted_routed_usage(
store: &RuntimeThreadStore,
thread_id: &str,
batch: &crate::cost_status::RuntimeUsageBatch,
) -> Result<String> {
let turn_id = unaccepted_routed_usage_turn_id(thread_id, batch);
let _turn_mutation = store.turn_mutation.lock();
let mut turn = if store.turn_path(&turn_id)?.exists() {
let existing = store.load_turn(&turn_id)?;
if existing.thread_id != thread_id {
bail!("settlement turn {turn_id} already belongs to another thread");
}
existing
} else {
let now = Utc::now();
TurnRecord {
max_output_tokens: None,
schema_version: CURRENT_RUNTIME_SCHEMA_VERSION,
id: turn_id.clone(),
thread_id: thread_id.to_string(),
status: RuntimeTurnStatus::Failed,
input_summary: UNACCEPTED_TURN_SUMMARY.to_string(),
created_at: now,
started_at: Some(now),
ended_at: Some(now),
duration_ms: Some(0),
usage: None,
routing_settlement: true,
effective_route_usage: None,View on GitHub (pinned to 73e0f67d83)