zeroclaw-labs/zeroclaw · critical · anyhow::Error
MCP server `{server_name}` is unavailable: a prior request's
Error message
MCP server `{server_name}` is unavailable: a prior request's outcome became unknown and recovery failed; not writing on an unrecovered session What it means
When a request's outcome became unknown (timeout after the write), the client resets the transport and re-handshakes; if that recovery fails (transport reset error or rejected re-handshake in reestablish), the session is poisoned and every later call fails closed with this error instead of writing on an unrecovered session. It is deliberately terminal for this connection handle.
Source
Thrown at crates/zeroclaw-tools/src/mcp_client.rs:493
result
.as_ref()
.err()
.and_then(|error| error.downcast_ref::<McpTransportError>()),
Some(McpTransportError::RecoveryPending)
) {
continue;
}
return result;
}
}
/// Block until no recovery is pending, or fail closed if recovery has
/// permanently failed. Returns immediately when the connection is healthy.
async fn wait_recovery_ready(&self) -> Result<()> {
loop {
if self.recovery.is_poisoned() {
let server_name = self.inner.lock().await.config.name.clone();
bail!(
"MCP server `{server_name}` is unavailable: a prior request's outcome became \
unknown and recovery failed; not writing on an unrecovered session"
);
}
if !self.recovery.recovery_pending() {
return Ok(());
}
// Register for a wakeup *before* re-checking so we cannot miss a
// concurrent `finish`/`poison` pulse.
let notified = self.recovery.notify.notified();
if self.recovery.is_poisoned() {
continue;
}
if !self.recovery.recovery_pending() {
return Ok(());
}
notified.await;
}View on GitHub (pinned to 88bb9c8533)
Solutions
- Check the preceding ERROR log 'mcp_client: asynchronous recovery failed' — its error names why reset or re-handshake failed
- Verify the server process is running (or the HTTP endpoint reachable) and fix that root cause first
- Reconnect by creating a fresh connection/client for that server; the poisoned barrier is only cleared by a new successful connect
- If crashes recur, raise the tool timeout or lower request frequency so recovery is needed less often
Defensive patterns
Strategy: try-catch
Type guard
fn is_poisoned_session(err: &anyhow::Error) -> bool {
err.to_string().contains("not writing on an unrecovered session")
} Try / catch
Treat as terminal for this client handle: stop calling, log the paired recovery failure, fix/restart the server, then create a fresh connection. Retrying on the poisoned handle always fails.
Prevention
- Monitor the 'asynchronous recovery failed' ERROR events — they precede the poisoned state
- Keep MCP servers alive under load (memory limits, supervision) so recovery succeeds
- Recreate clients on long-lived services instead of assuming a connection is immortal
When it happens
Trigger: The server process died or its stdio pipe closed while recovery ran; an HTTP MCP endpoint went down; the re-handshake itself got rejected (server now in a bad state); repeated timeouts where recovery never completes within reach.
Common situations: MCP server crashing under load or OOM-killed mid-request; the server restarted out-of-band during a long tool call; flaky network links to HTTP transports.
Related errors
- MCP server `{server_name}` timed out after {timeout_secs}s b
- MCP server `{server_name}` exhausted the {timeout_secs}s bud
- MCP server `{server_name}` recovery task failed before writi
- webhook-audit: {e}
- amqp.{}: dispatch = {:?} routes to the SOP engine but no SOP
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/751b58bbab6fc33a.
Report an issue: GitHub.