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
- Skip listen() for ACP channels — gate on supports_free_form_ask() (which returns false for ACP) or name() == "acp" before spawning a listener task.
- Drive ACP inbound traffic from the RPC request handlers (elicitation, permissions) instead of a listener task.
- 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
- Query supports_free_form_ask() and name() before wiring listener tasks
- Treat channels as capability sets, not uniform bidirectional interfaces
- Register ACP channels only in the tool-facing map, never in the listener supervisor
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
- AcpChannel does not support reactions
- ACP returned unexpected outcome: {other}
- ACP elicitation/create failed: {} ({})
- ACP elicitation/create timed out after {timeout:?}
- AcpChannel.request_choice requires at least one choice
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/3e8c2f6e031163dc.
Report an issue: GitHub.