chenhg5/cc-connect · error

start thread for message %s: %w

Error message

start thread for message %s: %w

What it means

For a guild message that is not already inside a thread, resolveThreadReplyContext starts a new thread on the message via ops.StartThread. If that Discord API call fails, the error is wrapped as "start thread for message <messageID>: <cause>" and message dispatch is aborted. Thread creation can fail for permissions, archived/locked parent threads, or API errors.

Source

Thrown at platform/discord/discord.go:415

		threadID := m.Message.Thread.ID
		if err := ops.JoinThread(threadID); err != nil {
			slog.Debug("discord: join attached thread failed", "thread", threadID, "error", err)
		}
		rc := replyContext{channelID: threadID, messageID: m.ID, threadID: threadID}
		return buildThreadSessionKey(threadID), rc, m.ChannelID, nil
	}
	if m.Flags&discordgo.MessageFlagsHasThread != 0 {
		threadID := m.ID
		if err := ops.JoinThread(threadID); err != nil {
			slog.Debug("discord: join message thread failed", "thread", threadID, "error", err)
		}
		rc := replyContext{channelID: threadID, messageID: m.ID, threadID: threadID}
		return buildThreadSessionKey(threadID), rc, m.ChannelID, nil
	}

	thread, err := ops.StartThread(m.ChannelID, m.ID, threadNameForMessage(m, botID), 1440)
	if err != nil {
		return "", replyContext{}, "", fmt.Errorf("start thread for message %s: %w", m.ID, err)
	}
	if err := ops.JoinThread(thread.ID); err != nil {
		slog.Debug("discord: join new thread failed", "thread", thread.ID, "error", err)
	}
	rc := replyContext{channelID: thread.ID, messageID: m.ID, threadID: thread.ID}
	return buildThreadSessionKey(thread.ID), rc, m.ChannelID, nil
}

func resolveCronReplyTarget(sessionKey, title string, ops discordThreadOps) (string, replyContext, error) {
	channelID, err := parseDiscordSessionKeyChannelID(sessionKey)
	if err != nil {
		return "", replyContext{}, err
	}

	ch, err := ops.ResolveChannel(channelID)
	if err != nil {
		return "", replyContext{}, fmt.Errorf("resolve channel %s: %w", channelID, err)
	}

View on GitHub (pinned to 4000b2338a)

Solutions

  1. Read the wrapped cause; grant the bot "Create Public Threads" and "Send Messages in Threads" permissions.
  2. Confirm the message is <48h old and in a guild text channel eligible for threading.
  3. Unarchive/unlock the parent thread or post to the channel directly instead of threading.
  4. Retry on 429 with backoff, or fall back to replying in the parent channel when thread creation fails.

Example fix

// before
thread, err := ops.StartThread(m.ChannelID, m.ID, threadNameForMessage(m, botID), 1440)
if err != nil {
    return "", replyContext{}, "", fmt.Errorf("start thread for message %s: %w", m.ID, err)
}
// after
thread, err := ops.StartThread(m.ChannelID, m.ID, threadNameForMessage(m, botID), 1440)
if err != nil {
    slog.Warn("discord: thread start failed, falling back to channel", "message", m.ID, "error", err)
    rc := replyContext{channelID: m.ChannelID, messageID: m.ID}
    return buildThreadSessionKey(m.ChannelID), rc, m.ChannelID, nil
}
Defensive patterns

Strategy: fallback

Validate before calling

// preflight: bot can create threads in this channel?
perm, err := session.State.UserChannelPermissions(botID, m.ChannelID)
canThread := err == nil && perm&discordgo.PermissionCreatePublicThreads != 0

Try / catch

thread, err := ops.StartThread(m.ChannelID, m.ID, name, 1440)
if err != nil {
    slog.Warn("discord: falling back to channel reply", "message", m.ID, "error", err)
    return buildThreadSessionKey(m.ChannelID), replyContext{channelID: m.ChannelID, messageID: m.ID}, m.ChannelID, nil
}

Prevention

When it happens

Trigger: ops.StartThread(m.ChannelID, m.ID, name, 1440) fails: bot lacks "Create Public Threads" permission, the message is older than 48 hours, the channel is not a guild text channel, the channel/thread is archived or locked, or Discord returns 429/5xx.

Common situations: Bots added with default permissions lacking thread management rights, messages arriving in announcement/forum channels where thread start rules differ, long-running cron sessions trying to thread an old message, rate limits during high traffic.

Understand the failure class

Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.

Related errors


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