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

AcpChannel.listen is not supported (free-form ask_user await

Error message

AcpChannel.listen is not supported (free-form ask_user awaits ACP elicitation Phase 2)

What it means

AcpChannel deliberately does not implement inbound listening: ACP is a client-driven back-channel where the client pushes requests to the agent, and free-form ask_user support is deferred to elicitation Phase 2. listen() always fails fast instead of pretending to work, so orchestrators that blindly start listeners on every registered channel see the refusal immediately rather than a silent no-op.

Source

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

            .notify(
                "session/update",
                json!({
                    "sessionId": self.session_id,
                    "update": {
                        "sessionUpdate": "agent_message_chunk",
                        "content": {
                            "type": "text",
                            "text": message.content,
                        }
                    }
                }),
            )
            .await;
        Ok(())
    }

    async fn listen(&self, _tx: tokio::sync::mpsc::Sender<ChannelMessage>) -> anyhow::Result<()> {
        anyhow::bail!(
            "AcpChannel.listen is not supported (free-form ask_user awaits ACP elicitation Phase 2)"
        )
    }

    fn supports_free_form_ask(&self) -> bool {
        false
    }

    async fn add_reaction(
        &self,
        _channel_id: &str,
        _message_id: &str,
        _emoji: &str,
    ) -> anyhow::Result<()> {
        // ACP renders agent output as message chunks — there's no per-message
        // reaction primitive in the protocol, so silently no-oping (the trait
        // default) would falsely report success to the agent. Surface as Err
        // so the `reaction` tool's caller sees the truth.

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Skip listen() for ACP channels — gate on supports_free_form_ask() (which returns false for ACP) or name() == "acp" before spawning a listener task.
  2. Drive ACP inbound traffic from the RPC request handlers (elicitation, permissions) instead of a listener task.
  3. If you need user input over ACP today, use request_choice (multiple-choice) until elicitation Phase 2 ships.

Example fix

// before
for ch in channels.values() {
    ch.listen(tx.clone()).await?; // fails for the ACP channel
}

// after
for ch in channels.values() {
    if ch.name() == "acp" {
        continue; // ACP is client-driven; no listener task
    }
    ch.listen(tx.clone()).await?;
}
Defensive patterns

Strategy: validation

Validate before calling

fn needs_listener(ch: &dyn Channel) -> bool {
    // ACP channels are client-driven; they refuse listen() by design.
    ch.name() != "acp"
}

Type guard

fn is_client_driven(ch: &dyn Channel) -> bool {
    ch.name() == "acp" && !ch.supports_free_form_ask()
}

Prevention

When it happens

Trigger: A generic channel supervisor iterates the channel map and calls listen(tx) on each channel — including the per-session AcpChannel registered at session/new. Any code path that treats AcpChannel like a poll-style channel (Slack/Bluesky style) hits this.

Common situations: Adding a new channel kind to a runtime that assumes all channels are bidirectional listeners; porting a supervisor loop from another channel type without a capability check; wiring the ACP channel into the wrong registry.

Understand the failure class

Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.

Related errors


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