sipeed/picoclaw · error · channels.ErrSendFailed
matrix room ID is empty: %w
Error message
matrix room ID is empty: %w
What it means
Thrown by MatrixChannel.Send when the outbound message's ChatID — which for Matrix is the room ID (e.g. !abc:example.org) — is empty after trimming (pkg/channels/matrix/matrix.go:406). It wraps channels.ErrSendFailed, the permanent sentinel: the manager will NOT retry this send. The channel therefore never contacts the homeserver; the error is purely a routing/data problem upstream of Matrix.
Source
Thrown at pkg/channels/matrix/matrix.go:406
logger.InfoC("matrix", "Crypto helper initialized successfully")
return nil
}
func markdownToHTML(md string) string {
extensions := (parser.CommonExtensions | parser.NoEmptyLineBeforeBlock) &^ parser.DefinitionLists
p := parser.NewWithExtensions(extensions)
renderer := mdhtml.NewRenderer(mdhtml.RendererOptions{Flags: mdhtml.UseXHTML})
return strings.TrimSpace(string(markdown.ToHTML([]byte(md), p, renderer)))
}
func (c *MatrixChannel) Send(ctx context.Context, msg bus.OutboundMessage) ([]string, error) {
if !c.IsRunning() {
return nil, channels.ErrNotRunning
}
roomID := id.RoomID(strings.TrimSpace(msg.ChatID))
if roomID == "" {
return nil, fmt.Errorf("matrix room ID is empty: %w", channels.ErrSendFailed)
}
content := strings.TrimSpace(msg.Content)
if content == "" {
return nil, nil
}
isToolFeedback := outboundMessageIsToolFeedback(msg)
if isToolFeedback {
if msgID, handled, err := c.progress.Update(ctx, msg.ChatID, content); handled {
if err != nil {
return nil, err
}
return []string{msgID}, nil
}
}
trackedMsgID, hasTrackedMsg := c.currentToolFeedbackMessage(msg.ChatID)
if !isToolFeedback {View on GitHub (pinned to 49183d7e8d)
Solutions
- Log msg.ChatID at the call site before Send to find who produces empty room IDs
- Fix the upstream router/conversation store to always propagate the Matrix room ID from the inbound event into OutboundMessage.ChatID
- Guard sends: skip and dead-letter messages whose ChatID trims to empty instead of calling Send
- Remember no retry happens: the manager treats ErrSendFailed as terminal
Example fix
// before
_ = bus.Publish(bus.OutboundMessage{Content: reply})
// after
if strings.TrimSpace(conv.RoomID) == "" {
logger.ErrorC("matrix", "no room ID bound to conversation; dropping reply")
return
}
_ = bus.Publish(bus.OutboundMessage{ChatID: conv.RoomID, Content: reply}) Defensive patterns
Strategy: validation
Validate before calling
// before Send
if strings.TrimSpace(msg.ChatID) == "" {
return fmt.Errorf("outbound message has no chat ID; refusing to send: %w", errDeadLetter)
}
ids, err := matrixCh.Send(ctx, msg) Type guard
func hasMatrixRoomID(msg bus.OutboundMessage) bool {
id := strings.TrimSpace(msg.ChatID)
return strings.HasPrefix(id, "!") && strings.Contains(id[idIndexColon(id):], ":")
} Try / catch
ids, err := matrixCh.Send(ctx, msg)
if err != nil {
if errors.Is(err, channels.ErrSendFailed) {
deadLetter(msg) // permanent: no retry, park for inspection
return nil
}
return err // temporary: manager/outer loop retries
} Prevention
- Always propagate the inbound event's room ID into OutboundMessage.ChatID at the routing layer
- Dead-letter rather than send when ChatID is blank — ErrSendFailed is never retried
- Add a unit assertion that every outbound message carries a non-empty ChatID
When it happens
Trigger: bus.OutboundMessage built with an empty or whitespace-only ChatID; upstream conversation router failing to map the inbound event's room to the outgoing message; replying from a context where the original chat ID was never captured (e.g. scheduled/cron-initiated sends with no bound conversation).
Common situations: A new pipeline (webhook, task scheduler, cross-channel relay) emits messages without setting ChatID; refactoring that renames ChatID->RoomID fields and leaves one constructor empty; test fixtures with placeholder strings.
Related errors
- chat ID is empty: %w
- no media store available: %w
- matrix send: %w
- matrix send media: %w
- matrix room ID is empty
AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15).
Data as JSON: /api/errors/40ac9212be5bc90b.
Report an issue: GitHub.