zeroclaw-labs/zeroclaw · error · anyhow::Error
chat.postMessage (lazy draft) failed: {err}
Error message
chat.postMessage (lazy draft) failed: {err} What it means
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.
Source
Thrown at crates/zeroclaw-channels/src/slack.rs:716
if let Some(ts) = draft_turn.thread_ts {
body["thread_ts"] = serde_json::json!(ts);
}
let resp = self
.http_client()
.post(self.slack_api_url("chat.postMessage"))
.bearer_auth(&self.bot_token)
.json(&body)
.send()
.await?;
let resp_body: serde_json::Value = resp.json().await?;
if resp_body.get("ok") != Some(&serde_json::Value::Bool(true)) {
let err = resp_body
.get("error")
.and_then(|e| e.as_str())
.unwrap_or("unknown");
anyhow::bail!("chat.postMessage (lazy draft) failed: {err}");
}
let ts = resp_body
.get("ts")
.and_then(|v| v.as_str())
.map(ToString::to_string);
if let Some(ref real_ts) = ts {
self.lazy_draft_ts
.lock()
.await
.insert(lazy_id.to_string(), real_ts.clone());
}
Ok(ts)
}
fn legacy_progress_event(text: &str) -> Option<ProgressEvent> {View on GitHub (pinned to 88bb9c8533)
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
Defensive patterns
Strategy: try-catch
Try / catch
if let Err(err) = channel.update_draft(recipient, message_id, chunk).await {
let msg = format!("{err:#}");
let slack_err = msg.rsplit(": ").next().unwrap_or("");
match slack_err {
"rate_limited" => { tokio::time::sleep(retry_after_or_3s()).await; channel.update_draft(recipient, message_id, chunk).await }
"msg_too_long" => channel.update_draft(recipient, message_id, &truncate_utf8(chunk, 39000)).await,
"not_in_channel" | "channel_not_found" => Ok(log_invite_needed(recipient)),
_ => Err(err),
}
} Prevention
- 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
When it happens
Trigger: 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').
Common situations: 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.
Related errors
- assistant.threads.setStatus failed: {}
- Telegram sendMessage (draft) failed: {err}
- channel does not support room creation
- elicitation returned unknown choice const: {s}
- purge_namespace not supported by this memory backend
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/d4a179442c2ee5f1.
Report an issue: GitHub.