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

  1. Fix the producer: never enqueue an OutboundMessage for IRC with an empty ChatID (guard at enqueue time, see exampleFix).
  2. Audit the inbound handler: onPrivmsg must store e.Params[0] (channel) or the sender nick for DMs into ChatID.
  3. If the target is genuinely unknown, route to a configured default channel instead of sending with an empty ID.
  4. 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

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


AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15). Data as JSON: /api/errors/9a747bef1659e85b. Report an issue: GitHub.