Hmbown/CodeWhale · error

Turn output allowance does not match its schema

Error message

Turn output allowance does not match its schema

What it means

A turn record may carry `max_output_tokens` only when its `schema_version` equals `OUTPUT_LIMIT_RUNTIME_SCHEMA_VERSION`; the presence/absence of the field must match the schema exactly. `validate_output_token_limit` enforces this invariant both ways (field without the version, or version without the field).

Solutions

  1. Set the record's `schema_version` to `OUTPUT_LIMIT_RUNTIME_SCHEMA_VERSION` when writing `max_output_tokens`.
  2. Set `max_output_tokens: None` for records on other schema versions.
  3. Fix the writer code so version and field are updated together.

Example fix

// before
TurnRecord { schema_version: 2, max_output_tokens: Some(4096), .. }
// after
TurnRecord { schema_version: OUTPUT_LIMIT_RUNTIME_SCHEMA_VERSION, max_output_tokens: Some(4096), .. }
Defensive patterns

Strategy: validation

Validate before calling

fn output_tokens_valid(r: &TurnRecord) -> bool { r.max_output_tokens.is_some() == (r.schema_version == OUTPUT_LIMIT_RUNTIME_SCHEMA_VERSION) }

Try / catch

match store.load_turn(&turn_id) { Err(e) if e.contains("does not match its schema") => { quarantine_record(&turn_id); continue_without(e) }, Err(e) => Err(e.into()), Ok(t) => Ok(t) }

Prevention

When it happens

Trigger: Loading or constructing a `TurnRecord` where `max_output_tokens.is_some()` disagrees with `schema_version == OUTPUT_LIMIT_RUNTIME_SCHEMA_VERSION`.

Common situations: Hand-edited or migrated turn files; code that sets `max_output_tokens` but forgets to bump the schema version (or vice versa) when writing records.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


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

Appendix: source

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

    /// idempotency bridge between a claimed mail envelope and the existing
    /// turn queue; ordinary external-user turns leave it unset.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub agent_mail_message_id: Option<String>,
}

impl TurnRecord {
    fn validate_output_token_limit(&self) -> Result<()> {
        if self.schema_version > MAX_SUPPORTED_RUNTIME_SCHEMA_VERSION {
            bail!(
                "Turn schema v{} is newer than supported v{}",
                self.schema_version,
                MAX_SUPPORTED_RUNTIME_SCHEMA_VERSION
            );
        }
        if self.max_output_tokens.is_some()
            != (self.schema_version == OUTPUT_LIMIT_RUNTIME_SCHEMA_VERSION)
        {
            bail!("Turn output allowance does not match its schema");
        }
        Ok(())
    }

    pub(crate) fn effective_provider_label(&self) -> Option<&str> {
        self.effective_provider_id
            .as_deref()
            .filter(|identity| !identity.trim().is_empty())
            .or_else(|| {
                self.effective_provider
                    .as_deref()
                    .filter(|provider| !provider.trim().is_empty())
            })
    }

    fn persist_effective_route(&mut self, route: &EffectiveRouteEnvelope) {
        let route = route.sanitized_for_persistence();
        self.effective_provider = Some(route.provider.as_str().to_string());

View on GitHub (pinned to 73e0f67d83)