sipeed/picoclaw · error

matrix message ID is empty

Error message

matrix message ID is empty

What it means

Thrown by MatrixChannel.EditMessage when messageID is empty after trimming (pkg/channels/matrix/matrix.go:649). The message ID must be a Matrix event ID (e.g. $abc:example.org) because it is passed to editContent.SetEdit to build the replacement event. Like the room-ID check, this is a plain error with no retry sentinel, so the manager will not retry it.

Source

Thrown at pkg/channels/matrix/matrix.go:649

	resp, err := c.client.SendMessageEvent(ctx, roomID, event.EventMessage, &event.MessageEventContent{
		MsgType: event.MsgNotice,
		Body:    text,
	})
	if err != nil {
		return "", err
	}

	return resp.EventID.String(), nil
}

// EditMessage implements channels.MessageEditor.
func (c *MatrixChannel) EditMessage(ctx context.Context, chatID string, messageID string, content string) error {
	roomID := id.RoomID(strings.TrimSpace(chatID))
	if roomID == "" {
		return fmt.Errorf("matrix room ID is empty")
	}
	if strings.TrimSpace(messageID) == "" {
		return fmt.Errorf("matrix message ID is empty")
	}

	editContent := c.messageContent(content)
	editContent.SetEdit(id.EventID(messageID))

	_, err := c.client.SendMessageEvent(ctx, roomID, event.EventMessage, editContent)
	return err
}

// DeleteMessage implements channels.MessageDeleter.
func (c *MatrixChannel) DeleteMessage(ctx context.Context, chatID string, messageID string) error {
	roomID := id.RoomID(strings.TrimSpace(chatID))
	if roomID == "" {
		return fmt.Errorf("matrix room ID is empty")
	}
	eventID := id.EventID(strings.TrimSpace(messageID))
	if eventID == "" {
		return fmt.Errorf("matrix message ID is empty")

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Store the Matrix event ID returned by Send/SendMedia (the []string result) keyed to your internal message ID, and translate before editing
  2. Reject edits targeting messages without a recorded Matrix event ID
  3. Do not feed external/foreign IDs into EditMessage — only event IDs this channel emitted

Example fix

// before
 ch.EditMessage(ctx, roomID, internalMsgID, newText) // internal ID is not an event ID

// after
 evID, ok := store.MatrixEventID(internalMsgID)
 if !ok { return fmt.Errorf("message %s has no matrix event ID; not editable", internalMsgID) }
 ch.EditMessage(ctx, roomID, evID, newText)
Defensive patterns

Strategy: validation

Validate before calling

if strings.TrimSpace(messageID) == "" {
	return errors.New("edit rejected: message has no stored matrix event ID")
}
if err := matrixCh.EditMessage(ctx, chatID, messageID, newText); err != nil { /* handle */ }

Type guard

func validMatrixEventID(messageID string) bool {
	id := strings.TrimSpace(messageID)
	return strings.HasPrefix(id, "$") && strings.Contains(id, ":")
}

Try / catch

if err := matrixCh.EditMessage(ctx, chatID, messageID, newText); err != nil {
	if strings.Contains(err.Error(), "message ID is empty") {
		deadLetterEdit(messageID) // permanent data gap, not retryable
		return nil
	}
	return err
}

Prevention

When it happens

Trigger: Editing a message whose event ID was never recorded (message sent by another device/user, message predating persistence, or an external system's ID mistaken for a Matrix event ID); passing a provider-agnostic message ID from a multi-channel abstraction without per-channel ID translation.

Common situations: Multi-channel bots storing one canonical message ID instead of per-channel IDs; edits of messages the bot did not itself send through this channel; DB rows with empty event_id after partial writes.

Related errors


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