{"record":{"id":"40ac9212be5bc90b","repo":"sipeed/picoclaw","slug":"matrix-room-id-is-empty-w","errorCode":null,"errorMessage":"matrix room ID is empty: %w","messagePattern":"matrix room ID is empty: %w","errorType":"validation","errorClass":"channels.ErrSendFailed","httpStatus":null,"severity":"error","filePath":"pkg/channels/matrix/matrix.go","lineNumber":406,"sourceCode":"\tlogger.InfoC(\"matrix\", \"Crypto helper initialized successfully\")\n\treturn nil\n}\n\nfunc markdownToHTML(md string) string {\n\textensions := (parser.CommonExtensions | parser.NoEmptyLineBeforeBlock) &^ parser.DefinitionLists\n\tp := parser.NewWithExtensions(extensions)\n\trenderer := mdhtml.NewRenderer(mdhtml.RendererOptions{Flags: mdhtml.UseXHTML})\n\treturn strings.TrimSpace(string(markdown.ToHTML([]byte(md), p, renderer)))\n}\n\nfunc (c *MatrixChannel) Send(ctx context.Context, msg bus.OutboundMessage) ([]string, error) {\n\tif !c.IsRunning() {\n\t\treturn nil, channels.ErrNotRunning\n\t}\n\n\troomID := id.RoomID(strings.TrimSpace(msg.ChatID))\n\tif roomID == \"\" {\n\t\treturn nil, fmt.Errorf(\"matrix room ID is empty: %w\", channels.ErrSendFailed)\n\t}\n\n\tcontent := strings.TrimSpace(msg.Content)\n\tif content == \"\" {\n\t\treturn nil, nil\n\t}\n\n\tisToolFeedback := outboundMessageIsToolFeedback(msg)\n\tif isToolFeedback {\n\t\tif msgID, handled, err := c.progress.Update(ctx, msg.ChatID, content); handled {\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\treturn []string{msgID}, nil\n\t\t}\n\t}\n\ttrackedMsgID, hasTrackedMsg := c.currentToolFeedbackMessage(msg.ChatID)\n\tif !isToolFeedback {","sourceCodeStart":388,"sourceCodeEnd":424,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/pkg/channels/matrix/matrix.go#L388-L424","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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"],"exampleFix":"// before\n _ = bus.Publish(bus.OutboundMessage{Content: reply})\n\n// after\n if strings.TrimSpace(conv.RoomID) == \"\" {\n \tlogger.ErrorC(\"matrix\", \"no room ID bound to conversation; dropping reply\")\n \treturn\n }\n _ = bus.Publish(bus.OutboundMessage{ChatID: conv.RoomID, Content: reply})","handlingStrategy":"validation","validationCode":"// before Send\nif strings.TrimSpace(msg.ChatID) == \"\" {\n\treturn fmt.Errorf(\"outbound message has no chat ID; refusing to send: %w\", errDeadLetter)\n}\nids, err := matrixCh.Send(ctx, msg)","typeGuard":"func hasMatrixRoomID(msg bus.OutboundMessage) bool {\n\tid := strings.TrimSpace(msg.ChatID)\n\treturn strings.HasPrefix(id, \"!\") && strings.Contains(id[idIndexColon(id):], \":\")\n}","tryCatchPattern":"ids, err := matrixCh.Send(ctx, msg)\nif err != nil {\n\tif errors.Is(err, channels.ErrSendFailed) {\n\t\tdeadLetter(msg) // permanent: no retry, park for inspection\n\t\treturn nil\n\t}\n\treturn err // temporary: manager/outer loop retries\n}","preventionTips":["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"],"tags":["go","matrix","validation","send","routing","permanent-failure"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}