zeroclaw-labs/zeroclaw · error · anyhow::Error

ACP returned unexpected outcome: {other}

Error message

ACP returned unexpected outcome: {other}

What it means

Thrown by AcpChannel's legacy multiple-choice path (request_choice_via_permission) when the client's session/request_permission reply carries an outcome.outcome value other than "selected" or "cancelled". ZeroClaw maps each offered choice to a synthetic optionId (choice-0..n) and only understands those two outcome kinds; anything else — including a response that omits the nested outcome fields, which parses as an empty string — is treated as a client-side protocol violation. It usually indicates a buggy client, protocol revision drift, or a malformed JSON-RPC result.

Source

Thrown at crates/zeroclaw-channels/src/acp_channel.rs:121

            .and_then(|o| o.get("outcome"))
            .and_then(|s| s.as_str())
            .unwrap_or("");
        match kind {
            "selected" => {
                let option_id = outcome
                    .and_then(|o| o.get("optionId"))
                    .and_then(|s| s.as_str())
                    .unwrap_or("");
                let idx = option_id
                    .strip_prefix("choice-")
                    .and_then(|s| s.parse::<usize>().ok());
                match idx.and_then(|i| choices.get(i)) {
                    Some(text) => Ok(Some(text.clone())),
                    None => anyhow::bail!("ACP returned unknown optionId: {option_id}"),
                }
            }
            "cancelled" => Ok(None),
            other => anyhow::bail!("ACP returned unexpected outcome: {other}"),
        }
    }

    /// Form-mode elicitation path — issues `elicitation/create` with a
    /// single-select schema. Used when the client advertises
    /// `clientCapabilities.elicitation.form`.
    async fn request_choice_via_elicitation(
        &self,
        question: &str,
        choices: &[String],
        timeout: Duration,
    ) -> anyhow::Result<Option<String>> {
        let req = ElicitationRequest {
            session_id: self.session_id.clone(),
            mode: ElicitationMode::Form,
            message: question.to_string(),
            requested_schema: single_select_schema(choices),
        };

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Log the raw outcome object from the response to see exactly which string the client sent before the match arm fired.
  2. Update the ACP client to a protocol revision that replies with only selected or cancelled on session/request_permission.
  3. Have the client advertise clientCapabilities.elicitation.form so the modern elicitation/create path is used instead of the legacy overload.
  4. In the caller, degrade gracefully: log the error and treat it as a cancellation (None) rather than failing the whole agent turn.

Example fix

// before
let pick = channel.request_choice(question, &choices, timeout).await?;

// after
let pick = match channel.request_choice(question, &choices, timeout).await {
    Ok(opt) => opt,
    Err(e) if e.to_string().contains("unexpected outcome") => {
        tracing::warn!(error = %e, "unrecognized ACP outcome; treating as cancel");
        None
    }
    Err(e) => return Err(e),
};
Defensive patterns

Strategy: try-catch

Try / catch

match ch.request_choice(q, &choices, timeout).await {
    Ok(Some(text)) => { /* proceed with the selection */ }
    Ok(None) => { /* user cancelled */ }
    Err(e) => {
        tracing::warn!(error = %e, "choice prompt failed; treating as cancelled");
        // treat as cancel: never guess a default option
    }
}

Prevention

When it happens

Trigger: Calling Channel::request_choice on an AcpChannel whose client did not advertise clientCapabilities.elicitation.form (so the legacy session/request_permission overload is used), and the client replies with an unknown outcome kind, an empty outcome object, or a result shape missing outcome/outcome.

Common situations: Home-grown or early-version ACP clients (editors, orchestrators) sending values like "dismissed" or omitting the nested outcome object; protocol version drift after a client update; a client forwarding a permission response matched to the wrong request id.

Related errors


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