{"record":{"id":"ae2316c31b3ea563","repo":"zeroclaw-labs/zeroclaw","slug":"whatsapp-send-choice-options-exceeds-list-cap","errorCode":null,"errorMessage":"WhatsApp send_choice: {} options exceeds list cap ({}); split the prompt or call send_interactive_list directly","messagePattern":"WhatsApp send_choice: (.+?) options exceeds list cap \\((.+?)\\); split the prompt or call send_interactive_list directly","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-channels/src/whatsapp.rs","lineNumber":891,"sourceCode":"        if options.len() >= 2 && options.len() <= 3 {\n            let buttons: Vec<(String, String)> = options\n                .iter()\n                .map(|(id, label)| (id.clone(), label.clone()))\n                .collect();\n            return self\n                .send_interactive_buttons(recipient, prompt, &buttons)\n                .await;\n        }\n        if options.len() > 3 {\n            // List messages: max 10 rows per section, max 10 sections per\n            // message → up to 100 options. Chunk into sections of ≤10 rows\n            // each so we don't blow past the per-section row cap. Section\n            // titles mark the index range so list UIs render cleanly.\n            const MAX_ROWS_PER_SECTION: usize = 10;\n            const MAX_SECTIONS: usize = 10;\n            let max_total = MAX_ROWS_PER_SECTION * MAX_SECTIONS;\n            if options.len() > max_total {\n                anyhow::bail!(\n                    \"WhatsApp send_choice: {} options exceeds list cap ({}); split the prompt or call send_interactive_list directly\",\n                    options.len(),\n                    max_total\n                );\n            }\n            let sections: Vec<InteractiveListSection> = options\n                .chunks(MAX_ROWS_PER_SECTION)\n                .enumerate()\n                .map(|(chunk_idx, chunk)| {\n                    let start = chunk_idx * MAX_ROWS_PER_SECTION + 1;\n                    let end = start + chunk.len() - 1;\n                    let title = if options.len() <= MAX_ROWS_PER_SECTION {\n                        \"Options\".to_string()\n                    } else {\n                        format!(\"Options {start} to {end}\")\n                    };\n                    InteractiveListSection {\n                        title,","sourceCodeStart":873,"sourceCodeEnd":909,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-channels/src/whatsapp.rs#L873-L909","documentation":"`send_choice` renders more than 3 options as a WhatsApp interactive list, and WhatsApp lists cap at 10 sections x 10 rows = 100 options total. Passing more than 100 options bails before any send. Calling `send_interactive_list` directly (as the message suggests) does not raise the ceiling — the underlying Meta caps are the same — so splitting or trimming is the real fix.","triggerScenarios":"`send_choice(recipient, prompt, &options)` with `options.len() > 100` — e.g. an LLM emitting an unbounded tool-option list, or a search/directory flow returning 150 entries.","commonSituations":"Unbounded LLM-generated menus; product catalogs passed wholesale as choices; assuming the channel will silently truncate (it does not — it fails loudly).","solutions":["Limit choices to the top 100 or fewer and add a 'more' option that pages through the remainder.","Split into multiple prompts by category, each within the cap.","For very large structured sets, send a text summary or a link instead of an interactive list."],"exampleFix":"// before\nchannel.send_choice(to, \"Pick a doc:\", &all_150_options).await?; // > 100 -> bails\n\n// after\nlet mut top: Vec<(String, String)> = all_150_options.iter().take(99).cloned().collect();\ntop.push((\"more\".into(), \"Show more results…\".into())); // page on 'more'\nchannel.send_choice(to, \"Pick a doc:\", &top).await?;","handlingStrategy":"validation","validationCode":"const CHOICE_CAP: usize = 100; // WhatsApp list: 10 sections x 10 rows\nif options.len() > CHOICE_CAP {\n    let top: Vec<_> = options.iter().take(CHOICE_CAP - 1).cloned().collect();\n    // add a paging option and handle it in your reply handler\n    channel.send_choice(recipient, prompt, &with_more_option(top)).await\n} else {\n    channel.send_choice(recipient, prompt, &options).await\n}","typeGuard":"fn within_choice_cap(options: &[(String, String)]) -> bool {\n    options.len() <= 100\n}","tryCatchPattern":null,"preventionTips":["Bound choice generation at the source: cap LLM tool menus and search results to <= 100.","Implement paging ('more' option) instead of ever sending unbounded lists.","Test the 100/101 boundary in your choice-rendering code."],"tags":["whatsapp","interactive-list","api-limits","validation","rust"],"backgroundTag":"api-limit-exceeded","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}