sipeed/picoclaw · error · ErrSendFailed

chat ID is empty: %w

Error message

chat ID is empty: %w

What it means

FeishuChannel.Send rejects an OutboundMessage whose ChatID is empty, wrapping the permanent ErrSendFailed sentinel. Feishu's create-message API needs a receive_id (chat_id), so an empty one can never succeed - the guard prevents a wasted API round-trip. Not retryable.

Source

Thrown at pkg/channels/feishu/feishu_64.go:161

	c.mu.Unlock()
	if c.progress != nil {
		c.progress.StopAll()
	}

	c.SetRunning(false)
	logger.InfoC("feishu", "Feishu channel stopped")
	return nil
}

// Send sends a message using Interactive Card format for markdown rendering.
// Falls back to plain text message if card sending fails (e.g., table limit exceeded).
func (c *FeishuChannel) Send(ctx context.Context, msg bus.OutboundMessage) ([]string, error) {
	if !c.IsRunning() {
		return nil, channels.ErrNotRunning
	}

	if msg.ChatID == "" {
		return nil, fmt.Errorf("chat ID is empty: %w", channels.ErrSendFailed)
	}

	isToolFeedback := outboundMessageIsToolFeedback(msg)
	if isToolFeedback {
		if msgID, handled, err := c.progress.Update(ctx, msg.ChatID, msg.Content); handled {
			if err != nil {
				// Feishu can fall back to plain text for a previous progress
				// message, and those messages cannot be patched through the card
				// edit API. Drop the stale tracker and recreate the progress
				// message so later tool feedback is not blocked.
				c.resetTrackedToolFeedbackAfterEditFailure(ctx, msg.ChatID)
			} else {
				return []string{msgID}, nil
			}
		}
	} else {
		if msgIDs, handled := c.FinalizeToolFeedbackMessage(ctx, msg); handled {
			return msgIDs, nil

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Find where the OutboundMessage was produced and ensure the inbound event's chat_id (event.message.chat_id) is copied into ChatID
  2. Add a guard in the dispatcher/router: drop or log messages with an empty ChatID before they reach the channel
  3. If it happens after adding a bot to a new chat type (p2p/group), verify the event parser handles that chat type

Example fix

// before
bus.Publish(bus.OutboundMessage{Content: reply}) // ChatID forgotten

// after
chatID := ev.Message.ChatId
if chatID == nil || *chatID == "" {
	logger.Warn("no chat_id on event; dropping reply")
	return
}
bus.Publish(bus.OutboundMessage{ChatID: *chatID, Content: reply})
Defensive patterns

Strategy: validation

Validate before calling

func outboundAddressable(msg bus.OutboundMessage) bool {
	return strings.TrimSpace(msg.ChatID) != ""
}

Type guard

null

Try / catch

if err := ch.Send(ctx, msg); err != nil {
	if errors.Is(err, channels.ErrSendFailed) && msg.ChatID == "" {
		logger.Warn("dropped unaddressed outbound message", "origin", msg.Origin)
		return nil // routing bug upstream: do not retry
	}
	return err
}

Prevention

When it happens

Trigger: The bus delivers bus.OutboundMessage{ChatID: ""} to a running Feishu channel: upstream router failed to map the inbound event to a reply chat, or a programmatically built message skipped the ChatID field.

Common situations: Webhook/event handler did not extract chat_id from the Feishu event payload; reply routing table miss; test helper constructs OutboundMessage without a chat; broadcast fan-out path with an empty target.

Related errors


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