sipeed/picoclaw · error

feishu edit: card build failed: %w

Error message

feishu edit: card build failed: %w

What it means

FeishuChannel.EditMessage first converts the new content into a Feishu interactive-card JSON payload via buildMarkdownCard; this error means that conversion failed before any API request was made. The underlying build error is wrapped, so the cause (typically JSON marshaling or card-schema limits) is visible in the message. Editing cannot fall back to plain text the way Send does, because Message.Patch only accepts card content.

Source

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

			} else if hasTrackedMsg {
				c.dismissTrackedToolFeedbackMessage(ctx, msg.ChatID, trackedMsgID)
			}
			return []string{msgID}, nil
		}
		// If text also fails, return the text error
		return nil, textErr
	}

	// For other errors, return the original card error
	return nil, err
}

// EditMessage implements channels.MessageEditor.
// Uses Message.Patch to update an interactive card message.
func (c *FeishuChannel) EditMessage(ctx context.Context, chatID, messageID, content string) error {
	cardContent, err := buildMarkdownCard(content)
	if err != nil {
		return fmt.Errorf("feishu edit: card build failed: %w", err)
	}

	req := larkim.NewPatchMessageReqBuilder().
		MessageId(messageID).
		Body(larkim.NewPatchMessageReqBodyBuilder().Content(cardContent).Build()).
		Build()

	resp, err := c.client.Im.V1.Message.Patch(ctx, req)
	if err != nil {
		return fmt.Errorf("feishu edit: %w", err)
	}
	if !resp.Success() {
		c.invalidateTokenOnAuthError(resp.Code)
		return fmt.Errorf("feishu edit api error (code=%d msg=%s)", resp.Code, resp.Msg)
	}
	return nil
}

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Print the wrapped cause (%v) - it names the exact build failure (marshal error vs schema limit)
  2. Simplify the markdown before editing: split oversized tables, trim the content, or strip unsupported syntax
  3. If edits keep failing, fall back to delete + re-send as a plain text message instead of patching the card
  4. Unit-test buildMarkdownCard against your real agent output shapes

Example fix

// before
if err := ch.EditMessage(ctx, chatID, msgID, hugeMarkdown); err != nil {
	return err // stuck: card cannot be patched
}

// after
if err := ch.EditMessage(ctx, chatID, msgID, hugeMarkdown); err != nil {
	if strings.Contains(err.Error(), "card build failed") {
		_ = ch.DeleteMessage(ctx, chatID, msgID)
		_, err2 := ch.Send(ctx, bus.OutboundMessage{ChatID: chatID, Content: plainText(hugeMarkdown)})
		return err2
	}
	return err
}
Defensive patterns

Strategy: fallback

Validate before calling

// dry-run the card build before editing, with the same builder
if _, err := feishu.BuildMarkdownCard(newContent); err != nil {
	// will fail in EditMessage too: use the plain-text path now
}

Type guard

func isCardBuildFailure(err error) bool {
	return err != nil && strings.Contains(err.Error(), "card build failed")
}

Try / catch

err := ch.EditMessage(ctx, chatID, msgID, content)
if err != nil {
	if isCardBuildFailure(err) {
		// cards cannot be patched as text: replace the message instead
		_ = ch.DeleteMessage(ctx, chatID, msgID)
		_, err = ch.Send(ctx, bus.OutboundMessage{ChatID: chatID, Content: content})
	}
}
return err

Prevention

When it happens

Trigger: EditMessage is called with markdown the card builder cannot serialize: malformed nesting, unsupported constructs, or content exceeding Feishu card constraints (e.g. too many table columns/rows), causing buildMarkdownCard to return an error.

Common situations: Editing a long streaming/agent reply whose final markdown grew past card limits; markdown produced by a tool that emits exotic syntax; concurrent edit of a huge code block or table.

Related errors


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