sipeed/picoclaw · error · ErrTemporary

dingtalk send: %w

Error message

dingtalk send: %w

What it means

SendDirectReply's SimpleReplyMarkdown call to the session webhook failed; the error wraps channels.ErrTemporary so the manager retries with exponential backoff. Note the wrap uses only the sentinel — the underlying SDK error is discarded, so logs show 'temporary failure' without the HTTP cause.

Source

Thrown at pkg/channels/dingtalk/dingtalk.go:271

}

// SendDirectReply sends a direct reply using the session webhook
func (c *DingTalkChannel) SendDirectReply(ctx context.Context, sessionWebhook, content string) error {
	replier := chatbot.NewChatbotReplier()

	// Convert string content to []byte for the API
	contentBytes := []byte(content)
	titleBytes := []byte("PicoClaw")

	// Send markdown formatted reply
	err := replier.SimpleReplyMarkdown(
		ctx,
		sessionWebhook,
		titleBytes,
		contentBytes,
	)
	if err != nil {
		return fmt.Errorf("dingtalk send: %w", channels.ErrTemporary)
	}

	return nil
}

func stripLeadingAtMentions(content string) string {
	fields := strings.Fields(content)
	if len(fields) == 0 {
		return ""
	}

	i := 0
	for i < len(fields) && strings.HasPrefix(fields[i], "@") {
		i++
	}
	if i == 0 {
		return strings.TrimSpace(content)
	}

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Refresh the webhook: have the chat send a new inbound message (each callback stores a fresh SessionWebhook), then retry
  2. Verify network egress to oapi.dingtalk.com from the host
  3. Patch the wrap to keep the cause: fmt.Errorf("dingtalk send: %w: %w", channels.ErrTemporary, err) — ErrTemporary stays matched by errors.Is while the raw error becomes visible
  4. If it persists, log the SDK error before wrapping to diagnose expired-webhook vs network

Example fix

// before
if err != nil {
    return fmt.Errorf("dingtalk send: %w", channels.ErrTemporary)
}

// after
if err != nil {
    return fmt.Errorf("dingtalk send: %w: %w", channels.ErrTemporary, err)
}
Defensive patterns

Strategy: retry

Type guard

func isTemporary(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) {
        // manager retries with backoff; on repeated failure the webhook is likely stale —
        // prompt the chat for a new inbound message to rotate it
        return backoff.Retry(sendOp, backoff.NewExponentialBackOff())
    }
    return err
}

Prevention

When it happens

Trigger: Replying via a stale/expired session webhook (each webhook has limited validity and call count), network failure reaching the DingTalk reply endpoint, DingTalk 5xx, or a malformed markdown body rejected by the API.

Common situations: Long agent processing so the reply lands after the webhook expired, server with restricted egress, DingTalk API hiccup, replying more times than the session webhook permits.

Related errors


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