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
- Upgrade or restart the Runtime — this is a runtime-side pagination defect.
- Check for caching proxies in front of the Runtime API returning stale pages.
- 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
- Keep app-server and Runtime versions aligned; pagination bugs are version-skew symptoms.
- Bypass caching proxies for runtime catalog endpoints.
- Bound all pagination loops with a page cap in client-side wrappers.
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
- ${compactRuntimeError(response.status, result)}
- MCP repeated a pagination cursor
- repeated pagination cursor; aborting
- runtime API returned
- runtime API returned
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)