{"record":{"id":"c16cb45e414f6f73","repo":"sipeed/picoclaw","slug":"channel-id-is-empty","errorCode":null,"errorMessage":"channel ID is empty","messagePattern":"channel ID is empty","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/channels/discord/discord.go","lineNumber":171,"sourceCode":"\tif c.progress != nil {\n\t\tc.progress.StopAll()\n\t}\n\n\tif err := c.session.Close(); err != nil {\n\t\treturn fmt.Errorf(\"failed to close discord session: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (c *DiscordChannel) Send(ctx context.Context, msg bus.OutboundMessage) ([]string, error) {\n\tif !c.IsRunning() {\n\t\treturn nil, channels.ErrNotRunning\n\t}\n\n\tchannelID := msg.ChatID\n\tif channelID == \"\" {\n\t\treturn nil, fmt.Errorf(\"channel ID is empty\")\n\t}\n\n\tif len([]rune(msg.Content)) == 0 {\n\t\treturn nil, nil\n\t}\n\n\tisToolFeedback := outboundMessageIsToolFeedback(msg)\n\tif isToolFeedback {\n\t\tif msgID, handled, err := c.progress.Update(ctx, channelID, msg.Content); handled {\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\treturn []string{msgID}, nil\n\t\t}\n\t}\n\ttrackedMsgID, hasTrackedMsg := c.currentToolFeedbackMessage(channelID)\n\tc.maybeStartTTS(channelID, msg.Content, isToolFeedback)\n\tif !isToolFeedback {","sourceCodeStart":153,"sourceCodeEnd":189,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/pkg/channels/discord/discord.go#L153-L189","documentation":"Send refuses to deliver an OutboundMessage whose ChatID is blank — there is no Discord channel snowflake to address, so delivery is impossible. This is a caller-side data bug in whatever produced the outbound message, not a Discord API failure.","triggerScenarios":"A producer publishes bus.OutboundMessage with an empty ChatID: agent/session wiring losing the original chat ID, scheduled or proactive sends constructed without a target, tests building messages without ChatID.","commonSituations":"New message-bus producers (cron jobs, webhooks, tools) forgetting to copy ChatID from the inbound context, refactors that rename or drop the field.","solutions":["Fix the caller to always set ChatID to the Discord channel snowflake ID from the inbound message","Add validation/logging at the message-bus publish site so empty ChatID is caught with producer context","Write a unit test asserting outbound messages never have an empty ChatID"],"exampleFix":"// before\nbus.Publish(bus.OutboundMessage{Content: reply}) // ChatID zero value\n\n// after\nbus.Publish(bus.OutboundMessage{ChatID: inbound.ChatID, Content: reply})","handlingStrategy":"validation","validationCode":"if msg.ChatID == \"\" {\n    return fmt.Errorf(\"refusing to publish outbound message with empty ChatID (source: %s)\", producer)\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Always copy ChatID from the inbound context when building outbound messages","Validate at the message-bus publish boundary with producer context in the error","Cover the empty-ChatID case in unit tests of every new producer"],"tags":["discord","send","validation","message-bus"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}