{"record":{"id":"4e0b2a1d999938ee","repo":"sipeed/picoclaw","slug":"matrix-send-w","errorCode":null,"errorMessage":"matrix send: %w","messagePattern":"matrix send: %w","errorType":"exception","errorClass":"channels.ErrTemporary","httpStatus":null,"severity":"error","filePath":"pkg/channels/matrix/matrix.go","lineNumber":435,"sourceCode":"\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 {\n\t\tif msgIDs, handled := c.FinalizeToolFeedbackMessage(ctx, msg); handled {\n\t\t\treturn msgIDs, nil\n\t\t}\n\t}\n\tif isToolFeedback {\n\t\tcontent = channels.InitialAnimatedToolFeedbackContent(content)\n\t}\n\n\tresp, err := c.client.SendMessageEvent(ctx, roomID, event.EventMessage, c.messageContent(content))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"matrix send: %w\", channels.ErrTemporary)\n\t}\n\tmsgID := resp.EventID.String()\n\tif isToolFeedback {\n\t\tc.RecordToolFeedbackMessage(msg.ChatID, msgID, msg.Content)\n\t} else if hasTrackedMsg {\n\t\tc.dismissTrackedToolFeedbackMessage(ctx, msg.ChatID, trackedMsgID)\n\t}\n\treturn []string{msgID}, nil\n}\n\nfunc (c *MatrixChannel) messageContent(text string) *event.MessageEventContent {\n\tmc := &event.MessageEventContent{MsgType: event.MsgText, Body: text}\n\tif c.config.MessageFormat != \"plain\" {\n\t\tmc.Format = event.FormatHTML\n\t\tmc.FormattedBody = markdownToHTML(text)\n\t}\n\treturn mc\n}","sourceCodeStart":417,"sourceCodeEnd":453,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/pkg/channels/matrix/matrix.go#L417-L453","documentation":"Thrown by MatrixChannel.Send when mautrix's SendMessageEvent (PUT /_matrix/client/v3/rooms/{roomID}/send/m.room.message) fails (pkg/channels/matrix/matrix.go:435). The channel deliberately classifies the failure as channels.ErrTemporary so the manager retries with exponential backoff. Caveat for debugging: on this exact path the underlying client error is dropped — the wrap contains only ErrTemporary and nothing is logged in Send — so the true cause must be found in homeserver logs or client-level logging.","triggerScenarios":"Homeserver rate limiting (429 M_LIMIT_EXCEEDED) after burst sends or animated tool-feedback updates; transient network timeouts/5xx; but also permanent conditions misclassified as temporary: bot was kicked/banned from the room (403 M_FORBIDDEN), room not found, access token revoked mid-session — these will fail every retry.","commonSituations":"High-volume bot traffic hitting per-user rate limits; a room admin kicking the bot while conversations continue; homeserver restart/overload; expired token after long uptime. Retries eventually succeed for 429/5xx but loop uselessly on 403.","solutions":["Enable mautrix client debug logging or check homeserver access logs to recover the dropped cause","If 429: slow the send rate, batch/dedupe tool-feedback updates (the progress-update path reduces message volume)","If 403/room gone: re-invite the bot to the room, join it, and stop retrying that ChatID — check room membership with the client before further sends","If token revoked: re-login and restart the channel","For genuine transients, do nothing — the manager's exponential backoff already handles it"],"exampleFix":"// before: cause dropped\n resp, err := c.client.SendMessageEvent(ctx, roomID, event.EventMessage, c.messageContent(content))\n if err != nil {\n \treturn nil, fmt.Errorf(\"matrix send: %w\", channels.ErrTemporary)\n }\n\n// after: keep the cause while preserving retry semantics\n if err != nil {\n \tvar httpErr mautrix.HTTPError\n \tif errors.As(err, &httpErr) && httpErr.RespError != nil {\n \t\tlogger.ErrorCF(\"matrix\", \"send failed\", map[string]any{\"room\": roomID, \"err\": httpErr.RespError.Errcode})\n \t}\n \treturn nil, fmt.Errorf(\"matrix send: %w: %v\", channels.ErrTemporary, err)\n }","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"ids, err := matrixCh.Send(ctx, msg)\nif err == nil { return }\nif errors.Is(err, channels.ErrTemporary) {\n\t// manager already retries with exponential backoff; ensure your loop does not double-send on success-after-retry without dedupe\n\treturn\n}\nif errors.Is(err, channels.ErrSendFailed) {\n\tdeadLetter(msg)\n}","preventionTips":["Enable homeserver-side or mautrix client logging — this wrap drops the underlying cause","Pace sends (especially animated tool-feedback) to stay under 429 limits","Periodically verify room membership for active ChatIDs so kicked-from-room 403s stop retry loops","Deduplicate by content so backoff retries after a timeout don't double-post"],"tags":["go","matrix","send","network","rate-limit","retry","temporary-failure"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-16T03:17:38.424Z"}