{"record":{"id":"d4a179442c2ee5f1","repo":"zeroclaw-labs/zeroclaw","slug":"chat-postmessage-lazy-draft-failed-err","errorCode":null,"errorMessage":"chat.postMessage (lazy draft) failed: {err}","messagePattern":"chat\\.postMessage \\(lazy draft\\) failed: (.+?)","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-channels/src/slack.rs","lineNumber":716,"sourceCode":"        if let Some(ts) = draft_turn.thread_ts {\n            body[\"thread_ts\"] = serde_json::json!(ts);\n        }\n\n        let resp = self\n            .http_client()\n            .post(self.slack_api_url(\"chat.postMessage\"))\n            .bearer_auth(&self.bot_token)\n            .json(&body)\n            .send()\n            .await?;\n\n        let resp_body: serde_json::Value = resp.json().await?;\n        if resp_body.get(\"ok\") != Some(&serde_json::Value::Bool(true)) {\n            let err = resp_body\n                .get(\"error\")\n                .and_then(|e| e.as_str())\n                .unwrap_or(\"unknown\");\n            anyhow::bail!(\"chat.postMessage (lazy draft) failed: {err}\");\n        }\n\n        let ts = resp_body\n            .get(\"ts\")\n            .and_then(|v| v.as_str())\n            .map(ToString::to_string);\n\n        if let Some(ref real_ts) = ts {\n            self.lazy_draft_ts\n                .lock()\n                .await\n                .insert(lazy_id.to_string(), real_ts.clone());\n        }\n\n        Ok(ts)\n    }\n\n    fn legacy_progress_event(text: &str) -> Option<ProgressEvent> {","sourceCodeStart":698,"sourceCodeEnd":734,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-channels/src/slack.rs#L698-L734","documentation":"Raised by SlackChannel::materialize_lazy_draft when chat.postMessage — used to lazily create the draft message on the first update_draft call — returns ok != true. The {err} is Slack's own error string from the response body ('channel_not_found', 'not_in_channel', 'msg_too_long', 'invalid_auth', 'account_inactive', 'rate_limited', ...), so the Slack error code is the diagnostic. The lazy-draft design means the first streamed update is what actually posts the message, so this error aborts streaming for that recipient.","triggerScenarios":"update_draft is called for a recipient that has no draft yet; materialize_lazy_draft POSTs chat.postMessage with the token, and Slack answers ok=false. Common codes: channel the bot was removed from ('not_in_channel'), wrong/invalid channel id ('channel_not_found'), message over 40k chars ('msg_too_long'), bad token ('invalid_auth'/'account_inactive'), or rate limiting ('rate_limited').","commonSituations":"Bot removed from a channel (or never invited) while drafts stream; stale workspace token after app reinstallation; very long streaming drafts exceeding Slack's message size; user/channel deleted between message receipt and reply; bursts of streaming updates tripping rate limits.","solutions":["Act on the Slack error string: 'not_in_channel'/'channel_not_found' → re-invite the bot or fix the target id","'msg_too_long' → chunk or truncate the streamed draft text before materializing","'rate_limited' → retry honoring Retry-After; pace update_draft calls","'invalid_auth'/'account_inactive' → reinstall the app / rotate the token"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"if let Err(err) = channel.update_draft(recipient, message_id, chunk).await {\n    let msg = format!(\"{err:#}\");\n    let slack_err = msg.rsplit(\": \").next().unwrap_or(\"\");\n    match slack_err {\n        \"rate_limited\" => { tokio::time::sleep(retry_after_or_3s()).await; channel.update_draft(recipient, message_id, chunk).await }\n        \"msg_too_long\" => channel.update_draft(recipient, message_id, &truncate_utf8(chunk, 39000)).await,\n        \"not_in_channel\" | \"channel_not_found\" => Ok(log_invite_needed(recipient)),\n        _ => Err(err),\n    }\n}","preventionTips":["Branch on Slack's error string — only 'rate_limited' is retryable; auth errors need token reinstall","Invite the bot to channels it must stream drafts into, and monitor membership","Chunk or truncate streamed drafts to stay under Slack's message size limit"],"tags":["slack","chat-postmessage","web-api","draft","streaming","http-status"],"backgroundTag":"slack-web-api-error","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}