sipeed/picoclaw · error · ErrSendFailed

no media store available: %w

Error message

no media store available: %w

What it means

SendMedia needs a media store to resolve each part.Ref into a readable local file before uploading to Discord; GetMediaStore() returned nil, meaning SetMediaStore was never called on the channel. It wraps channels.ErrSendFailed, so the manager treats it as permanent and will not retry. The message text also carries ErrSendFailed via %w.

Source

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

	}
	return vc, true
}

// SendMedia implements the channels.MediaSender interface.
func (c *DiscordChannel) SendMedia(ctx context.Context, msg bus.OutboundMediaMessage) ([]string, error) {
	if !c.IsRunning() {
		return nil, channels.ErrNotRunning
	}

	channelID := msg.ChatID
	if channelID == "" {
		return nil, fmt.Errorf("channel ID is empty")
	}
	trackedMsgID, hasTrackedMsg := c.currentToolFeedbackMessage(channelID)

	store := c.GetMediaStore()
	if store == nil {
		return nil, fmt.Errorf("no media store available: %w", channels.ErrSendFailed)
	}

	// Collect all files into a single ChannelMessageSendComplex call
	files := make([]*discordgo.File, 0, len(msg.Parts))
	var caption string

	for _, part := range msg.Parts {
		localPath, err := store.Resolve(part.Ref)
		if err != nil {
			logger.ErrorCF("discord", "Failed to resolve media ref", map[string]any{
				"ref":   part.Ref,
				"error": err.Error(),
			})
			continue
		}

		file, err := os.Open(localPath)
		if err != nil {

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Attach a media store before any media send: ch.SetMediaStore(store) with a media.MediaStore implementation
  2. If using the standard Manager, ensure it is the one constructing/starting channels so SetMediaStore propagates to existing channels
  3. Check startup ordering: attach the store before the agent can publish OutboundMediaMessage

Example fix

// before
ch, _ := discord.NewDiscordChannel(bc, cfg, bus)
_ = ch.Start(ctx) // later SendMedia fails: no media store available

// after
ch, _ := discord.NewDiscordChannel(bc, cfg, bus)
ch.SetMediaStore(store) // media.MediaStore implementation
_ = ch.Start(ctx)
Defensive patterns

Strategy: validation

Validate before calling

if ch.GetMediaStore() == nil {
    return fmt.Errorf("discord channel has no media store; attach one before sending media")
}

Type guard

func hasMediaStore(ch interface{ GetMediaStore() media.MediaStore }) bool {
    return ch.GetMediaStore() != nil
}

Try / catch

if _, err := ch.SendMedia(ctx, msg); err != nil {
    if errors.Is(err, channels.ErrSendFailed) && strings.Contains(err.Error(), "no media store") {
        // permanent: wire the store, do not retry
        ch.SetMediaStore(store)
        return ch.SendMedia(ctx, msg)
    }
    return err
}

Prevention

When it happens

Trigger: Constructing and starting the Discord channel without attaching a media store — e.g. embedding the channel outside the Manager that normally calls Manager.SetMediaStore (manager.go), or a startup ordering where media arrives before the store is attached.

Common situations: Custom embeddings of PicoClaw channels that skip manager wiring, tests exercising SendMedia without a store (see qq_test's equivalent), disabling the media subsystem in config while the agent still emits images/files.

Related errors


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