sipeed/picoclaw · error · channels.ErrTemporary

matrix send: %w

Error message

matrix send: %w

What it means

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.

Source

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

			if err != nil {
				return nil, err
			}
			return []string{msgID}, nil
		}
	}
	trackedMsgID, hasTrackedMsg := c.currentToolFeedbackMessage(msg.ChatID)
	if !isToolFeedback {
		if msgIDs, handled := c.FinalizeToolFeedbackMessage(ctx, msg); handled {
			return msgIDs, nil
		}
	}
	if isToolFeedback {
		content = channels.InitialAnimatedToolFeedbackContent(content)
	}

	resp, err := c.client.SendMessageEvent(ctx, roomID, event.EventMessage, c.messageContent(content))
	if err != nil {
		return nil, fmt.Errorf("matrix send: %w", channels.ErrTemporary)
	}
	msgID := resp.EventID.String()
	if isToolFeedback {
		c.RecordToolFeedbackMessage(msg.ChatID, msgID, msg.Content)
	} else if hasTrackedMsg {
		c.dismissTrackedToolFeedbackMessage(ctx, msg.ChatID, trackedMsgID)
	}
	return []string{msgID}, nil
}

func (c *MatrixChannel) messageContent(text string) *event.MessageEventContent {
	mc := &event.MessageEventContent{MsgType: event.MsgText, Body: text}
	if c.config.MessageFormat != "plain" {
		mc.Format = event.FormatHTML
		mc.FormattedBody = markdownToHTML(text)
	}
	return mc
}

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Enable mautrix client debug logging or check homeserver access logs to recover the dropped cause
  2. If 429: slow the send rate, batch/dedupe tool-feedback updates (the progress-update path reduces message volume)
  3. 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
  4. If token revoked: re-login and restart the channel
  5. For genuine transients, do nothing — the manager's exponential backoff already handles it

Example fix

// before: cause dropped
 resp, err := c.client.SendMessageEvent(ctx, roomID, event.EventMessage, c.messageContent(content))
 if err != nil {
 	return nil, fmt.Errorf("matrix send: %w", channels.ErrTemporary)
 }

// after: keep the cause while preserving retry semantics
 if err != nil {
 	var httpErr mautrix.HTTPError
 	if errors.As(err, &httpErr) && httpErr.RespError != nil {
 		logger.ErrorCF("matrix", "send failed", map[string]any{"room": roomID, "err": httpErr.RespError.Errcode})
 	}
 	return nil, fmt.Errorf("matrix send: %w: %v", channels.ErrTemporary, err)
 }
Defensive patterns

Strategy: retry

Try / catch

ids, err := matrixCh.Send(ctx, msg)
if err == nil { return }
if errors.Is(err, channels.ErrTemporary) {
	// manager already retries with exponential backoff; ensure your loop does not double-send on success-after-retry without dedupe
	return
}
if errors.Is(err, channels.ErrSendFailed) {
	deadLetter(msg)
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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