Hmbown/CodeWhale · error · anyhow::Error

Runtime model catalog cursor repeated

Error message

Runtime model catalog cursor repeated

What it means

The catalog pagination loop guards against a Runtime that keeps returning the same `nextCursor`, which would loop forever. Each cursor is inserted into a `seen` set; a repeat aborts with this error. It indicates a Runtime API pagination bug or a malformed catalog response.

Solutions

  1. Upgrade or restart the Runtime — this is a runtime-side pagination defect.
  2. Check for caching proxies in front of the Runtime API returning stale pages.
  3. Pin the model explicitly (avoiding catalog paging) if the endpoint is known-good for single-page results.
Defensive patterns

Strategy: fallback

Try / catch

// treat a repeated cursor as a runtime bug and degrade gracefully
match resolve_output_support(model).await {
    Err(e) if e.to_string().contains("cursor repeated") => {
        log::warn!("runtime catalog pagination broken; skipping capability check");
        Ok(()) // proceed without the capability guarantee, or disable maxOutputTokens
    }
    other => other,
}

Prevention

When it happens

Trigger: During output-token capability resolution, two successive catalog pages return an identical `nextCursor` value that was already visited.

Common situations: Buggy or mismatched Runtime version whose /v1/providers/{id}/models endpoint returns a stale cursor; a proxy caching catalog pages.

Related errors


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

Appendix: source

Thrown at crates/app-server/src/lib.rs:1689

                    .get("models")
                    .and_then(Value::as_array)
                    .and_then(|models| {
                        models
                            .iter()
                            .find(|entry| entry.get("id").and_then(Value::as_str) == Some(model))
                    })
            {
                if entry.get("output_token_limit").and_then(Value::as_str) == Some("supported") {
                    return Ok(());
                }
                bail!("The selected Runtime model does not support maxOutputTokens");
            }
            let next = catalog
                .get("nextCursor")
                .and_then(Value::as_str)
                .context("Output-limit support is unknown for the selected Runtime model")?;
            if !seen.insert(next.to_string()) {
                bail!("Runtime model catalog cursor repeated");
            }
            cursor = Some(next.to_string());
        }
    }

    /// Resolve `stdio_thread_id` to a runtime thread, minting one only when
    /// `thread_map` has no entry. The map lives on [`AppState`] and outlives
    /// this bridge, so a thread created under a previous child keeps its id
    /// here as long as the store it was persisted to is shared (#6246).
    async fn ensure_runtime_thread(
        &mut self,
        thread_map: &mut HashMap<String, String>,
        stdio_thread_id: &str,
        hint: Option<RuntimeThreadHint>,
    ) -> Result<String> {
        if let Some(runtime_thread_id) = thread_map.get(stdio_thread_id) {
            return Ok(runtime_thread_id.clone());
        }

View on GitHub (pinned to 73e0f67d83)