feat: soporte de threads de Matrix (m.thread)

Implementa el soporte completo de threads de Matrix:
- Listener extrae ThreadID de m.relates_to con rel_type=m.thread
- Client.SendThreadMarkdown envia mensajes como parte de un thread
  usando SetThread de mautrix con fallback m.in_reply_to
- Runner detecta ThreadID en ReplyAction y rutea a SendThreadMarkdown
- MatrixSender interfaz actualizada con SendThreadMarkdown
- runtime.go propaga ThreadID en todas las respuestas (comandos, LLM, RBAC)
- sendReply helper centraliza la logica de envio con/sin thread
- Auto-thread: si matrix.threads.auto_thread=true, crea thread nuevo
  para cada conversacion que no esta ya en un thread
- Memoria por thread: usa ThreadID como clave de window cuando el mensaje
  esta en un thread, permitiendo conversaciones paralelas independientes
- Config: matrix.threads.enabled y matrix.threads.auto_thread en ThreadsCfg

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-08 12:50:34 +00:00
parent 893ac74a16
commit 38d11a0b32
5 changed files with 86 additions and 25 deletions
+20
View File
@@ -320,6 +320,26 @@ func (c *Client) SendReplyMarkdown(ctx context.Context, roomID, inReplyTo, markd
return err
}
// SendThreadMarkdown sends a formatted message as part of a Matrix thread.
// threadRootID is the event that started the thread (always the same for all messages in a thread).
// inReplyTo is the specific event being replied to within the thread (used as fallback for non-thread clients).
// If inReplyTo is empty, it defaults to threadRootID.
func (c *Client) SendThreadMarkdown(ctx context.Context, roomID, threadRootID, inReplyTo, markdown string) error {
if inReplyTo == "" {
inReplyTo = threadRootID
}
html := mdToHTML(markdown)
content := event.MessageEventContent{
MsgType: event.MsgText,
Body: markdown,
Format: event.FormatHTML,
FormattedBody: html,
RelatesTo: (&event.RelatesTo{}).SetThread(id.EventID(threadRootID), id.EventID(inReplyTo)),
}
_, err := c.raw.SendMessageEvent(ctx, id.RoomID(roomID), event.EventMessage, content)
return err
}
// SendReaction sends a reaction to an event.
func (c *Client) SendReaction(ctx context.Context, roomID, eventID, reaction string) error {
_, err := c.raw.SendReaction(ctx, id.RoomID(roomID), id.EventID(eventID), reaction)
+11
View File
@@ -153,6 +153,17 @@ func (l *Listener) Run(ctx context.Context) error {
)
msgCtx.EventID = evt.ID.String()
// Extract thread root from m.relates_to (Matrix thread support).
if l.cfg.Threads.Enabled {
if relatesTo, ok := evt.Content.Raw["m.relates_to"].(map[string]any); ok {
if relType, _ := relatesTo["rel_type"].(string); relType == "m.thread" {
if threadRoot, _ := relatesTo["event_id"].(string); threadRoot != "" {
msgCtx.ThreadID = threadRoot
}
}
}
}
l.logger.Debug("message parsed",
"sender", msgCtx.SenderID,
"room", msgCtx.RoomID,