sipeed/picoclaw · error · ErrTemporary

discord send: %w

Error message

discord send: %w

What it means

sendChunk's ChannelMessageSend / ChannelMessageSendComplex (used for reply references) failed inside the goroutine; wrapped as channels.ErrTemporary so the manager retries with backoff. The raw discordgo error is discarded, so the visible error never says whether it was a 403 permission, 400 bad request, or network failure.

Source

Thrown at pkg/channels/discord/discord.go:502

			err error
		)

		// If we have an ID, we send the message as "Reply"
		if replyToID != "" {
			msg, err = c.session.ChannelMessageSendComplex(channelID, &discordgo.MessageSend{
				Content: content,
				Reference: &discordgo.MessageReference{
					MessageID: replyToID,
					ChannelID: channelID,
				},
			})
		} else {
			// Otherwise, we send a normal message
			msg, err = c.session.ChannelMessageSend(channelID, content)
		}

		if err != nil {
			done <- result{err: fmt.Errorf("discord send: %w", channels.ErrTemporary)}
			return
		}
		done <- result{id: msg.ID}
	}()

	select {
	case r := <-done:
		return r.id, r.err
	case <-sendCtx.Done():
		return "", sendCtx.Err()
	}
}

// appendContent safely appends content to existing text
func appendContent(content, suffix string) string {
	if content == "" {
		return suffix
	}

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Verify the bot can send in the target channel (permissions, channel exists, not archived)
  2. Confirm content length is under 2000 runes so the splitter is doing its job
  3. Patch the wrap to preserve the cause: fmt.Errorf("discord send: %w: %w", channels.ErrTemporary, err)
  4. Log err inside the goroutine before sending to done so failures are diagnosable in logs

Example fix

// before
if err != nil {
    done <- result{err: fmt.Errorf("discord send: %w", channels.ErrTemporary)}
    return
}

// after
if err != nil {
    logger.WarnCF("discord", "send failed", map[string]any{"channel": channelID, "error": err.Error()})
    done <- result{err: fmt.Errorf("discord send: %w: %w", channels.ErrTemporary, err)}
    return
}
Defensive patterns

Strategy: retry

Validate before calling

if len([]rune(content)) > 2000 {
    content = content[:2000] // or split — channel MaxMessageLength is 2000
}

Type guard

func isTemporarySend(err error) bool { return errors.Is(err, channels.ErrTemporary) }

Try / catch

if _, err := ch.Send(ctx, msg); err != nil {
    if errors.Is(err, channels.ErrTemporary) {
        // backoff-retry once; permission/oversize failures repeat forever — inspect logs
    }
    return err
}

Prevention

When it happens

Trigger: Content exceeding Discord's 2000-char limit (the channel sets MaxMessageLength 2000 and normally splits first), missing SEND_MESSAGES permission, deleted/archived/gone channelID, replying to a deleted message via MessageReference, network drop within the 10s sendTimeout.

Common situations: Bot kicked from server or channel permissions tightened, very long messages bypassing the splitter, replies racing with message deletion, transient Discord/network issues.

Related errors


AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15). Data as JSON: /api/errors/10e17c7048b75b39. Report an issue: GitHub.