chenhg5/cc-connect · error

resolve channel %s: %w

Error message

resolve channel %s: %w

What it means

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.

Source

Thrown at platform/discord/discord.go:379

	}
	return channelID
}

// resolveThreadReplyContext routes a guild message into a Discord thread for
// thread_isolation mode and returns the per-thread session key, the reply
// context, and the parent channel ID.
//
// parentChannelID is the channel the thread lives under (or, for messages
// posted directly into an existing thread, the thread's ParentID). It is
// distinct from the thread itself: it's what callers should stamp onto
// Message.ChannelKey so multi-workspace auto-bind keys by channel name
// rather than thread name. Without this distinction, threads break the
// "channel name → workspace folder" convention because Discord threads
// have their own names that rarely match a workspace directory.
func resolveThreadReplyContext(m *discordgo.MessageCreate, botID string, ops discordThreadOps) (string, replyContext, string, error) {
	ch, err := ops.ResolveChannel(m.ChannelID)
	if err != nil {
		return "", replyContext{}, "", fmt.Errorf("resolve channel %s: %w", m.ChannelID, err)
	}
	if isThreadChannelType(ch.Type) {
		// Message posted directly inside an existing thread. The parent
		// channel comes from ch.ParentID; fall back to m.ChannelID only
		// if Discord didn't populate it (defensive — discordgo always
		// sets ParentID for thread channels).
		parentChannelID := ch.ParentID
		if parentChannelID == "" {
			parentChannelID = m.ChannelID
		}
		if err := ops.JoinThread(m.ChannelID); err != nil {
			slog.Debug("discord: join existing thread failed", "thread", m.ChannelID, "error", err)
		}
		rc := replyContext{channelID: m.ChannelID, messageID: m.ID, threadID: m.ChannelID}
		return buildThreadSessionKey(m.ChannelID), rc, parentChannelID, nil
	}
	if m.Message != nil && m.Message.Thread != nil && m.Message.Thread.ID != "" {
		threadID := m.Message.Thread.ID

View on GitHub (pinned to 4000b2338a)

Solutions

  1. Inspect the wrapped cause (%w) in logs — it names the real Discord API failure.
  2. Verify the bot is still a member of the guild/channel and has ViewChannel permission.
  3. Check for Discord API rate limiting (429) or outages; add backoff/retry on ResolveChannel.
  4. Ensure the platform session is initialized (a nil session produces this same wrapper via the guard errors).

Example fix

// before
ch, err := ops.ResolveChannel(m.ChannelID)
if err != nil {
    return fmt.Errorf("resolve channel %s: %w", m.ChannelID, err)
}
// after
ch, err := ops.ResolveChannel(m.ChannelID)
if err != nil {
    slog.Warn("discord: channel resolve failed, skipping dispatch", "channel", m.ChannelID, "error", err)
    return fmt.Errorf("resolve channel %s: %w", m.ChannelID, err)
}
Defensive patterns

Strategy: try-catch

Validate before calling

if _, err := ops.ResolveChannel(m.ChannelID); err != nil {
    slog.Warn("discord: channel unavailable before dispatch", "channel", m.ChannelID, "error", err)
}

Try / catch

ch, err := ops.ResolveChannel(m.ChannelID)
if err != nil {
    var restErr *discordgo.RESTError
    if errors.As(err, &restErr) && restErr.StatusCode == 429 {
        time.Sleep(time.Until(rateLimitReset(restErr)))
        // retry once
    }
    return fmt.Errorf("resolve channel %s: %w", m.ChannelID, err)
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: 'Could not be found', 'does not exist', 'not found in database': the resource-not-found family when an ID, slug, key, or URI lookup comes back empty — this error's family across 20 libraries.

Related errors


AI-assisted analysis of chenhg5/cc-connect@4000b2338a (2026-09-06). Data as JSON: /api/errors/30c5c7bf872c0b2c. Report an issue: GitHub.