{"record":{"id":"9a747bef1659e85b","repo":"sipeed/picoclaw","slug":"chat-id-is-empty-w-9a747b","errorCode":null,"errorMessage":"chat ID is empty: %w","messagePattern":"chat ID is empty: %w","errorType":"exception","errorClass":"ErrSendFailed","httpStatus":null,"severity":"error","filePath":"pkg/channels/irc/irc.go","lineNumber":142,"sourceCode":"\t\tc.conn.Quit()\n\t}\n\tif c.cancel != nil {\n\t\tc.cancel()\n\t}\n\n\tlogger.InfoC(\"irc\", \"IRC channel stopped\")\n\treturn nil\n}\n\n// Send sends a message to an IRC channel or user.\nfunc (c *IRCChannel) Send(ctx context.Context, msg bus.OutboundMessage) ([]string, error) {\n\tif !c.IsRunning() {\n\t\treturn nil, channels.ErrNotRunning\n\t}\n\n\ttarget := msg.ChatID\n\tif target == \"\" {\n\t\treturn nil, fmt.Errorf(\"chat ID is empty: %w\", channels.ErrSendFailed)\n\t}\n\n\tif strings.TrimSpace(msg.Content) == \"\" {\n\t\treturn nil, nil\n\t}\n\n\t// Send each line separately (IRC is line-oriented)\n\tlines := strings.Split(msg.Content, \"\\n\")\n\tfor _, line := range lines {\n\t\tline = strings.TrimRight(line, \"\\r\")\n\t\tif line == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tc.conn.Privmsg(target, line)\n\t}\n\n\tlogger.DebugCF(\"irc\", \"Message sent\", map[string]any{\n\t\t\"target\": target,","sourceCodeStart":124,"sourceCodeEnd":160,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/pkg/channels/irc/irc.go#L124-L160","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before — message silently classified permanent-failure downstream\nch.Send(ctx, bus.OutboundMessage{ChatID: target, Content: text})\n\n// after — guard at the producer\nif strings.TrimSpace(target) == \"\" {\n    logger.Warn(\"dropping outbound IRC message: no target resolved\")\n    return nil\n}\nch.Send(ctx, bus.OutboundMessage{ChatID: target, Content: text})","handlingStrategy":"validation","validationCode":"if strings.TrimSpace(msg.ChatID) == \"\" {\n    return fmt.Errorf(\"outbound IRC message has no target: refusing to send (permanent failure)\")\n}\nif !c.IsRunning() {\n    return channels.ErrNotRunning\n}","typeGuard":"func isPermanentSendErr(err error) bool {\n    return errors.Is(err, channels.ErrSendFailed)\n}","tryCatchPattern":"if _, err := ch.Send(ctx, msg); err != nil {\n    if errors.Is(err, channels.ErrSendFailed) {\n        // dropped permanently: fix the producer's ChatID mapping; retrying will not help\n        logger.Warn(\"IRC message dropped: empty chat ID\")\n    }\n}","preventionTips":["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"],"tags":["irc","send","go","routing","permanent-failure"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}