{"record":{"id":"bd551529bca93f61","repo":"zeroclaw-labs/zeroclaw","slug":"signal-poll-requires-at-least-2-options-got","errorCode":null,"errorMessage":"Signal poll requires at least 2 options (got {}); render as text instead","messagePattern":"Signal poll requires at least 2 options \\(got (.+?)\\); render as text instead","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"warning","filePath":"crates/zeroclaw-channels/src/signal.rs","lineNumber":544,"sourceCode":"    /// Sent via signal-cli daemon's JSON-RPC `sendPollCreate` method. The\n    /// poll renders as native UI in modern Signal clients and emits a\n    /// poll-vote event (`pollAnswer` or `pollVote`, depending on signal-cli\n    /// version) back through the SSE stream when the user votes — see\n    /// `process_envelope` for how that flows back to consumers, normally as\n    /// a synthetic `[choice-index]N` `ChannelMessage`.\n    ///\n    /// `multiple_choice = false` → single-select poll (the common case\n    /// for \"pick one of N\" agent prompts). Pass `true` to allow\n    /// multi-select.\n    pub async fn send_poll(\n        &self,\n        recipient: &str,\n        question: &str,\n        options: &[String],\n        multiple_choice: bool,\n    ) -> anyhow::Result<()> {\n        if options.len() < 2 {\n            anyhow::bail!(\n                \"Signal poll requires at least 2 options (got {}); render as text instead\",\n                options.len()\n            );\n        }\n        let params = self.build_poll_params(recipient, question, options, multiple_choice);\n        self.rpc_request(\"sendPollCreate\", params).await?;\n        Ok(())\n    }\n}\n\nimpl ::zeroclaw_api::attribution::Attributable for SignalChannel {\n    fn role(&self) -> ::zeroclaw_api::attribution::Role {\n        ::zeroclaw_api::attribution::Role::Channel(::zeroclaw_api::attribution::ChannelKind::Signal)\n    }\n    fn alias(&self) -> &str {\n        &self.alias\n    }\n}","sourceCodeStart":526,"sourceCodeEnd":562,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-channels/src/signal.rs#L526-L562","documentation":"Raised by SignalChannel::send_poll when options.len() < 2 — a deliberate validation guard before build_poll_params and the sendPollCreate RPC. The message itself tells the caller what to do: 'render as text instead'. It fires from send_choice when the model/agent produced fewer than two options, since a one-option (or zero-option) poll is meaningless on Signal. It is a control-flow signal, not an infrastructure fault.","triggerScenarios":"send_choice calls send_poll with an options slice containing 0 or 1 entries — e.g. an LLM produced a single choice, a choice list was truncated, or options were parsed out of malformed output.","commonSituations":"Agent-generated choice prompts collapsing to one option; empty options after filtering/escaping; upstream prompt templates that allow a single-item list.","solutions":["Catch this error in send_choice and fall back to a plain text message rendering the question and the available options","Validate options.len() >= 2 before calling send_poll and branch to text rendering yourself","Fix the upstream choice generation so it always yields at least two options"],"exampleFix":"// before\nself.send_poll(recipient, question, options, multiple_choice).await?;\n\n// after\nif options.len() >= 2 {\n    self.send_poll(recipient, question, options, multiple_choice).await\n} else {\n    let text = format!(\"{question}\\n{}\", options.join(\"\\n\"));\n    self.send(recipient, &text).await\n}","handlingStrategy":"validation","validationCode":"fn render_choice(channel: &SignalChannel, recipient: &str, question: &str, options: &[String], multi: bool) -> BoxFuture<'_, anyhow::Result<()>> {\n    async move {\n        if options.len() >= 2 {\n            channel.send_poll(recipient, question, options, multi).await\n        } else {\n            let text = format!(\"{question}\\n{}\", options.join(\"\\n\"));\n            channel.send(recipient, &text).await\n        }\n    }\n    .boxed()\n}","typeGuard":"fn poll_options_valid(options: &[String]) -> bool {\n    options.len() >= 2 && options.iter().all(|o| !o.trim().is_empty())\n}","tryCatchPattern":"if let Err(err) = channel.send_poll(recipient, question, options, multi).await {\n    if format!(\"{err:#}\").starts_with(\"Signal poll requires at least 2 options\") {\n        let text = format!(\"{question}\\n{}\", options.join(\"\\n\"));\n        return channel.send(recipient, &text).await; // fallback to plain text\n    }\n    return Err(err);\n}","preventionTips":["Validate options.len() >= 2 before every send_poll and branch to text rendering","Fix choice-generation prompts/templates so they never emit fewer than two options","Treat this error as a signal to fall back, never as a failure to surface to the user"],"tags":["signal","poll","validation","fallback","send-choice"],"backgroundTag":"invalid-poll-options","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}