sipeed/picoclaw · error · channels.ErrSendFailed

wecom resolve media %q: %v: %w

Error message

wecom resolve media %q: %v: %w

What it means

Raised while preparing a WeCom outbound message: resolveOutboundPart failed to turn a message part's Ref into a (localPath, filename, contentType, cleanup) tuple — the referenced attachment could not be fetched/materialized locally. The error chains the underlying cause plus channels.ErrSendFailed, so upstream retry logic treats it as a permanent send failure. Only the ref-resolution step aborts; later upload failures fall back to a placeholder instead.

Source

Thrown at pkg/channels/wecom/wecom.go:245

	route, chatType, hasTurn := c.resolveMediaRoute(msg.ChatID)
	chatID := route.ChatID
	if chatID == "" {
		chatID = msg.ChatID
	}

	for _, part := range msg.Parts {
		if strings.TrimSpace(part.Ref) == "" {
			if caption := strings.TrimSpace(part.Caption); caption != "" {
				if err := c.sendActivePush(chatID, chatType, caption); err != nil {
					return nil, err
				}
			}
			continue
		}

		localPath, filename, contentType, cleanup, err := c.resolveOutboundPart(ctx, part)
		if err != nil {
			return nil, fmt.Errorf("wecom resolve media %q: %v: %w", part.Ref, err, channels.ErrSendFailed)
		}

		func() {
			if cleanup != nil {
				defer cleanup()
			}

			uploaded, uploadErr := c.uploadOutboundMedia(ctx, localPath, filename, contentType, part)
			if uploadErr != nil {
				logger.WarnCF("wecom", "Falling back to placeholder after media upload failure", map[string]any{
					"chat_id":      chatID,
					"ref":          part.Ref,
					"filename":     filename,
					"content_type": contentType,
					"error":        uploadErr.Error(),
				})
				if hasTurn {
					if finishErr := c.sendStreamChunk(route, true, ""); finishErr != nil {

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Verify the ref exists in the attachment store before queueing the send (lookup/HEAD the ref)
  2. If the ref is a URL, check that it is still valid and reachable from the host (curl it); re-upload or re-sign if expired
  3. Ensure the media cache/storage directory the resolver uses is present and writable
  4. Drop the broken part and resend — text parts without Refs still deliver (captions are sent as active pushes)
  5. If refs routinely expire, shorten the gap between generation and send or persist the binary instead of the ref

Example fix

// before
parts := []channels.MediaPart{{Ref: oldRef}} // oldRef no longer resolvable -> error 665

// after
if _, err := store.Stat(oldRef); err != nil {
    parts = nil // drop dead ref, send text only
}
send(parts...)
Defensive patterns

Strategy: try-catch

Validate before calling

// resolve refs before building the outbound message
for _, part := range msg.Parts {
    if strings.TrimSpace(part.Ref) == "" {
        continue
    }
    if _, err := store.Stat(part.Ref); err != nil {
        // for URL refs: HEAD the URL and check 200
        return fmt.Errorf("part ref %q unresolvable: %w", part.Ref, err)
    }
}

Type guard

func isWecomResolveMedia(err error) bool {
    return err != nil && strings.HasPrefix(err.Error(), "wecom resolve media")
}

Try / catch

if _, err := ch.Send(ctx, msg); err != nil {
    if isWecomResolveMedia(err) && errors.Is(err, channels.ErrSendFailed) {
        // permanent: strip bad parts and resend text-only instead of retrying
    }
}

Prevention

When it happens

Trigger: A message part with a non-empty Ref whose backing asset cannot be resolved: unknown/expired reference in the attachment store, download of the referenced URL failing, storage backend unreachable, or the ref pointing at a file that no longer exists. The send loop at wecom.go:245 wraps any such failure with this message and stops processing remaining parts.

Common situations: The agent replies with an attachment ref produced in an earlier session that has since been evicted; the media URL is behind auth or has expired (signed URL TTL passed); local cache/tmp directory was cleaned; object store outage; a ref typo in a manually constructed message.

Related errors


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