astrid-runtime/astrid · error · GatewayError::Internal
no unique compatible loaded capsule handles the registry…
Error message
no unique compatible loaded capsule handles the registry request for caller
What it means
`provider_source_ids` throws this GatewayError::Internal when the topic probe IS subscribed for the scoped key, but `subscriber_source_ids` returns an empty list. The subscription exists yet no unique compatible loaded capsule is registered as a source for it — the gateway found the topic but no provider instance that can answer with a usable source id.
Solutions
- Inspect which capsules are loaded and confirm the registry provider capsule registered its source id after subscribing; restart/reload the capsule so registration completes.
- Check for a stale probe subscription from an unloaded capsule; clear/reset the probe subscription state and retry.
- Look for races between subscription and source registration in the capsule load path and make registration atomic with subscription.
- Check kernel/capsule logs for errors during provider registration that left the subscription present but the source list empty.
Defensive patterns
Strategy: retry
Validate before calling
let ids = probe.subscriber_source_ids(&key).await;
if ids.is_empty() {
return Err(anyhow!("no capsule source registered for {key}; aborting registry request"));
} Type guard
fn has_provider_source(ids: &[Uuid]) -> bool {
!ids.is_empty()
} Try / catch
match registry_round_trip(&state, /* ... */).await {
Err(e) if e.to_string().contains("no unique compatible loaded capsule") => {
// transient: subscription present but source registration lagging — retry once, then 503
Err(StatusCode::SERVICE_UNAVAILABLE)
}
other => other?,
} Prevention
- Make capsules register their source id atomically with topic subscription
- Treat subscriptions with zero registered sources as unhealthy and re-register them
- Log and alert on the subscription-vs-registration race so stale probe keys get cleaned up
When it happens
Trigger: Calling `registry_round_trip` where the probe is subscribed to `scoped_topic_probe_key(principal, request_topic)` but `probe.subscriber_source_ids(&key).await` yields an empty Vec.
Common situations: A capsule subscribed to the probe topic but never registered its provider source id; a stale subscription left after the capsule was unloaded or crashed; a race where the probe saw the subscription before the capsule completed registration; an inconsistent registry state with zero registered provider instances.
Understand the failure class
Background: EmptyResultError / "no results found": when an API or scraper succeeds but returns zero rows — this error's family across 9 libraries.
Related errors
- an incomplete capsule authority update exists at
- cannot load capsule ' ' for unadmitted principal
- cannot remove capsule authority while an install…
- capsule ' ' failed to load
- capsule ' ' has no source directory
AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09).
Data as JSON: /api/errors/ba479cddf1f3c3a1.
Report an issue: GitHub.
Appendix: source
Thrown at crates/astrid-gateway/src/routes/models.rs:347
async fn provider_source_ids(
state: &GatewayState,
principal: &PrincipalId,
request_topic: &str,
) -> GatewayResult<Vec<Uuid>> {
let probe = state.topic_probe.as_ref().ok_or_else(|| {
GatewayError::Internal(anyhow::anyhow!(
"gateway has no live capsule provider probe"
))
})?;
let key = scoped_topic_probe_key(principal, request_topic);
if !probe.is_subscribed(&key).await && !probe.ensure_subscribed(&key).await {
return Err(GatewayError::Internal(anyhow::anyhow!(
"no loaded capsule handles the registry request for caller"
)));
}
let source_ids = probe.subscriber_source_ids(&key).await;
if source_ids.is_empty() {
return Err(GatewayError::Internal(anyhow::anyhow!(
"no unique compatible loaded capsule handles the registry request for caller"
)));
}
Ok(source_ids)
}
fn scoped_topic_probe_key(principal: &PrincipalId, topic: &str) -> String {
format!(
"{SCOPED_SERVICE_PROBE_SENTINEL}{principal}\0astrid\0registry\0{REGISTRY_INTERFACE_REQUIREMENT}\0{topic}"
)
}
/// `GET /api/models` — list the caller's available provider-entries.
#[utoipa::path(
get,
path = "/api/models",
tag = "models",
responses(View on GitHub (pinned to affd8760f4)