sipeed/picoclaw · error · ErrSendFailed
chat ID is empty: %w
Error message
chat ID is empty: %w
What it means
IRC Send was called with an empty msg.ChatID, so there is no target (channel or nick) to PRIVMSG. The error wraps channels.ErrSendFailed, which is permanent — the outbound queue will NOT retry it; the message is dropped. The send also requires the channel to be running (ErrNotRunning guard earlier).
Source
Thrown at pkg/channels/irc/irc.go:142
c.conn.Quit()
}
if c.cancel != nil {
c.cancel()
}
logger.InfoC("irc", "IRC channel stopped")
return nil
}
// Send sends a message to an IRC channel or user.
func (c *IRCChannel) Send(ctx context.Context, msg bus.OutboundMessage) ([]string, error) {
if !c.IsRunning() {
return nil, channels.ErrNotRunning
}
target := msg.ChatID
if target == "" {
return nil, fmt.Errorf("chat ID is empty: %w", channels.ErrSendFailed)
}
if strings.TrimSpace(msg.Content) == "" {
return nil, nil
}
// Send each line separately (IRC is line-oriented)
lines := strings.Split(msg.Content, "\n")
for _, line := range lines {
line = strings.TrimRight(line, "\r")
if line == "" {
continue
}
c.conn.Privmsg(target, line)
}
logger.DebugCF("irc", "Message sent", map[string]any{
"target": target,View on GitHub (pinned to 49183d7e8d)
Solutions
- Fix the producer: never enqueue an OutboundMessage for IRC with an empty ChatID (guard at enqueue time, see exampleFix).
- Audit the inbound handler: onPrivmsg must store e.Params[0] (channel) or the sender nick for DMs into ChatID.
- If the target is genuinely unknown, route to a configured default channel instead of sending with an empty ID.
- Log the dropped message with its origin so routing gaps are visible.
Example fix
// before — message silently classified permanent-failure downstream
ch.Send(ctx, bus.OutboundMessage{ChatID: target, Content: text})
// after — guard at the producer
if strings.TrimSpace(target) == "" {
logger.Warn("dropping outbound IRC message: no target resolved")
return nil
}
ch.Send(ctx, bus.OutboundMessage{ChatID: target, Content: text}) Defensive patterns
Strategy: validation
Validate before calling
if strings.TrimSpace(msg.ChatID) == "" {
return fmt.Errorf("outbound IRC message has no target: refusing to send (permanent failure)")
}
if !c.IsRunning() {
return channels.ErrNotRunning
} Type guard
func isPermanentSendErr(err error) bool {
return errors.Is(err, channels.ErrSendFailed)
} Try / catch
if _, err := ch.Send(ctx, msg); err != nil {
if errors.Is(err, channels.ErrSendFailed) {
// dropped permanently: fix the producer's ChatID mapping; retrying will not help
logger.Warn("IRC message dropped: empty chat ID")
}
} Prevention
- Guard at enqueue time: never emit OutboundMessage with empty ChatID
- Ensure onPrivmsg stores the source channel/nick into ChatID
- Map cross-channel replies to a concrete IRC target or a configured default
- Monitor for permanent-failure drops — each one is a routing bug
When it happens
Trigger: An OutboundMessage is enqueued with an empty ChatID: the inbound PRIVMSG handler failed to capture the source nick/channel; a cross-channel reply was routed to IRC without mapping the target; a DM context was lost before the outbound message was built.
Common situations: Reply pipeline emits before the inbound event is fully parsed; notices/CTCP events that carry no usable sender; misconfigured routing rules that drop the chat-id field.
Related errors
- matrix room ID is empty: %w
- no agent available for route (agent_id=%s)
- feishu text api error (code=%d msg=%s): %w
- feishu image send: %w
- feishu image send api error (code=%d msg=%s)
AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15).
Data as JSON: /api/errors/9a747bef1659e85b.
Report an issue: GitHub.