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

  1. Check the preceding ERROR log 'mcp_client: asynchronous recovery failed' — its error names why reset or re-handshake failed
  2. Verify the server process is running (or the HTTP endpoint reachable) and fix that root cause first
  3. Reconnect by creating a fresh connection/client for that server; the poisoned barrier is only cleared by a new successful connect
  4. 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

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


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/751b58bbab6fc33a. Report an issue: GitHub.