{"record":{"id":"30c5c7bf872c0b2c","repo":"chenhg5/cc-connect","slug":"resolve-channel-s-w","errorCode":null,"errorMessage":"resolve channel %s: %w","messagePattern":"resolve channel (.+?): %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"platform/discord/discord.go","lineNumber":379,"sourceCode":"\t}\n\treturn channelID\n}\n\n// resolveThreadReplyContext routes a guild message into a Discord thread for\n// thread_isolation mode and returns the per-thread session key, the reply\n// context, and the parent channel ID.\n//\n// parentChannelID is the channel the thread lives under (or, for messages\n// posted directly into an existing thread, the thread's ParentID). It is\n// distinct from the thread itself: it's what callers should stamp onto\n// Message.ChannelKey so multi-workspace auto-bind keys by channel name\n// rather than thread name. Without this distinction, threads break the\n// \"channel name → workspace folder\" convention because Discord threads\n// have their own names that rarely match a workspace directory.\nfunc resolveThreadReplyContext(m *discordgo.MessageCreate, botID string, ops discordThreadOps) (string, replyContext, string, error) {\n\tch, err := ops.ResolveChannel(m.ChannelID)\n\tif err != nil {\n\t\treturn \"\", replyContext{}, \"\", fmt.Errorf(\"resolve channel %s: %w\", m.ChannelID, err)\n\t}\n\tif isThreadChannelType(ch.Type) {\n\t\t// Message posted directly inside an existing thread. The parent\n\t\t// channel comes from ch.ParentID; fall back to m.ChannelID only\n\t\t// if Discord didn't populate it (defensive — discordgo always\n\t\t// sets ParentID for thread channels).\n\t\tparentChannelID := ch.ParentID\n\t\tif parentChannelID == \"\" {\n\t\t\tparentChannelID = m.ChannelID\n\t\t}\n\t\tif err := ops.JoinThread(m.ChannelID); err != nil {\n\t\t\tslog.Debug(\"discord: join existing thread failed\", \"thread\", m.ChannelID, \"error\", err)\n\t\t}\n\t\trc := replyContext{channelID: m.ChannelID, messageID: m.ID, threadID: m.ChannelID}\n\t\treturn buildThreadSessionKey(m.ChannelID), rc, parentChannelID, nil\n\t}\n\tif m.Message != nil && m.Message.Thread != nil && m.Message.Thread.ID != \"\" {\n\t\tthreadID := m.Message.Thread.ID","sourceCodeStart":361,"sourceCodeEnd":397,"githubUrl":"https://github.com/chenhg5/cc-connect/blob/4000b2338aa6e850c99df54f8b0ed6ed7460b401/platform/discord/discord.go#L361-L397","documentation":"resolveThreadReplyContext must know whether an incoming message is in a thread, so it resolves the channel via ops.ResolveChannel. When that API call fails, the underlying error (rate limit, missing channel, network, permissions) is wrapped as \"resolve channel <id>: <cause>\". The message dispatch is aborted because the reply context cannot be determined.","triggerScenarios":"ops.ResolveChannel(m.ChannelID) returns an error for the channel a user message arrived in — deleted channel, bot lacks access, HTTP failure from the Discord API, or the nil-session guard error propagated through the wrapper.","commonSituations":"Messages arriving in channels deleted moments earlier, bots removed from a channel but still receiving cached events, Discord API outages or 429 rate limiting during busy threads, and missing Intents/Gateway privileges.","solutions":["Inspect the wrapped cause (%w) in logs — it names the real Discord API failure.","Verify the bot is still a member of the guild/channel and has ViewChannel permission.","Check for Discord API rate limiting (429) or outages; add backoff/retry on ResolveChannel.","Ensure the platform session is initialized (a nil session produces this same wrapper via the guard errors)."],"exampleFix":"// before\nch, err := ops.ResolveChannel(m.ChannelID)\nif err != nil {\n    return fmt.Errorf(\"resolve channel %s: %w\", m.ChannelID, err)\n}\n// after\nch, err := ops.ResolveChannel(m.ChannelID)\nif err != nil {\n    slog.Warn(\"discord: channel resolve failed, skipping dispatch\", \"channel\", m.ChannelID, \"error\", err)\n    return fmt.Errorf(\"resolve channel %s: %w\", m.ChannelID, err)\n}","handlingStrategy":"try-catch","validationCode":"if _, err := ops.ResolveChannel(m.ChannelID); err != nil {\n    slog.Warn(\"discord: channel unavailable before dispatch\", \"channel\", m.ChannelID, \"error\", err)\n}","typeGuard":null,"tryCatchPattern":"ch, err := ops.ResolveChannel(m.ChannelID)\nif err != nil {\n    var restErr *discordgo.RESTError\n    if errors.As(err, &restErr) && restErr.StatusCode == 429 {\n        time.Sleep(time.Until(rateLimitReset(restErr)))\n        // retry once\n    }\n    return fmt.Errorf(\"resolve channel %s: %w\", m.ChannelID, err)\n}","preventionTips":["Keep bot ViewChannel permissions on all channels it monitors.","Monitor logs for repeated resolve failures — usually a deleted channel or stale membership.","Add retry/backoff for 429/5xx on channel resolution."],"tags":["discord","api-error","channel-resolution"],"backgroundTag":"resource-not-found","analyzedSha":"4000b2338aa6e850c99df54f8b0ed6ed7460b401","analyzedAt":"2026-09-06T11:45:09.575Z","contentChangedAt":"2026-09-06T11:45:09.575Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}