Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 15 additions & 19 deletions bridges/ai/chat.go
Original file line number Diff line number Diff line change
Expand Up @@ -743,16 +743,19 @@ func (oc *AIClient) initPortalForChat(ctx context.Context, opts PortalInitOpts)
}
portal.Metadata = pmeta

portal.RoomType = database.RoomTypeDM
portal.OtherUserID = modelUserID(modelID)
portal.Name = title
portal.NameSet = true
defaultAvatar := strings.TrimSpace(agents.DefaultAgentAvatarMXC)
if defaultAvatar != "" {
portal.AvatarID = networkid.AvatarID(defaultAvatar)
portal.AvatarMXC = id.ContentURIString(defaultAvatar)
}
if err := portal.Save(ctx); err != nil {
if err := agentremote.ConfigureDMPortal(ctx, agentremote.ConfigureDMPortalParams{
Portal: portal,
Title: title,
OtherUserID: modelUserID(modelID),
Save: true,
MutatePortal: func(portal *bridgev2.Portal) {
defaultAvatar := strings.TrimSpace(agents.DefaultAgentAvatarMXC)
if defaultAvatar != "" {
portal.AvatarID = networkid.AvatarID(defaultAvatar)
portal.AvatarMXC = id.ContentURIString(defaultAvatar)
}
},
}); err != nil {
return nil, nil, fmt.Errorf("failed to save portal: %w", err)
}
oc.ensureGhostDisplayName(ctx, modelID)
Expand Down Expand Up @@ -1054,17 +1057,10 @@ func (oc *AIClient) BroadcastRoomState(ctx context.Context, portal *bridgev2.Por

// sendSystemNotice sends an informational notice to the room via the bridge bot.
func (oc *AIClient) sendSystemNotice(ctx context.Context, portal *bridgev2.Portal, message string) {
if portal == nil || portal.MXID == "" || oc == nil || oc.UserLogin == nil || oc.UserLogin.Bridge == nil || oc.UserLogin.Bridge.Bot == nil {
if oc == nil {
return
}
_, err := oc.UserLogin.Bridge.Bot.SendMessage(ctx, portal.MXID, event.EventMessage, &event.Content{
Parsed: &event.MessageEventContent{
MsgType: event.MsgNotice,
Body: message,
Mentions: &event.Mentions{},
},
}, nil)
if err != nil {
if err := agentremote.SendSystemMessage(ctx, oc.UserLogin, portal, bridgev2.EventSender{}, message); err != nil {
oc.loggerForContext(ctx).Warn().Err(err).Msg("Failed to send system notice")
}
}
Expand Down
13 changes: 8 additions & 5 deletions bridges/codex/backfill.go
Original file line number Diff line number Diff line change
Expand Up @@ -234,12 +234,15 @@ func (cc *CodexClient) ensureCodexThreadPortal(ctx context.Context, existing *br
meta.Slug = codexThreadSlug(threadID)
}

portal.RoomType = database.RoomTypeDM
portal.OtherUserID = codexGhostID

if err := agentremote.ConfigureDMPortal(ctx, agentremote.ConfigureDMPortalParams{
Portal: portal,
Title: title,
OtherUserID: codexGhostID,
Save: false,
}); err != nil {
return nil, false, err
}
info := cc.composeCodexChatInfo(portal, title, true)
portal.Name = title
portal.NameSet = true
created, err = bridgesdk.EnsurePortalLifecycle(ctx, bridgesdk.PortalLifecycleOptions{
Login: cc.UserLogin,
Portal: portal,
Expand Down
40 changes: 31 additions & 9 deletions bridges/codex/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import (
"time"

"github.com/rs/zerolog"
"go.mau.fi/util/ptr"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/database"
"maunium.net/go/mautrix/bridgev2/networkid"
Expand Down Expand Up @@ -1539,18 +1538,15 @@ func (cc *CodexClient) composeCodexChatInfo(portal *bridgev2.Portal, title strin
if title == "" {
title = "Codex"
}
info := agentremote.BuildLoginDMChatInfo(agentremote.LoginDMChatInfoParams{
return agentremote.BuildLoginDMChatInfo(agentremote.LoginDMChatInfoParams{
Title: title,
Topic: cc.codexTopicForPortal(portal, portalMeta(portal)),
Login: cc.UserLogin,
HumanUserIDPrefix: cc.HumanUserIDPrefix,
BotUserID: codexGhostID,
BotDisplayName: "Codex",
CanBackfill: canBackfill,
})
if info != nil {
info.Topic = ptr.NonZero(cc.codexTopicForPortal(portal, portalMeta(portal)))
}
return info
}

func resolveCodexWorkingDirectory(raw string) (string, error) {
Expand Down Expand Up @@ -1768,11 +1764,37 @@ func (cc *CodexClient) HandleMatrixDeleteChat(ctx context.Context, msg *bridgev2
}

func (cc *CodexClient) sendSystemNotice(ctx context.Context, portal *bridgev2.Portal, message string) {
if portal == nil || portal.MXID == "" || cc.UserLogin == nil || cc.UserLogin.Bridge == nil {
if cc == nil || portal == nil || strings.TrimSpace(message) == "" {
return
}
timing := agentremote.ResolveEventTiming(time.Now(), 0)
cc.sendViaPortal(portal, agentremote.BuildSystemNotice(strings.TrimSpace(message)), "", timing.Timestamp, timing.StreamOrder)
send := func(sendCtx context.Context) error {
return agentremote.SendSystemMessage(sendCtx, cc.UserLogin, portal, cc.senderForPortal(), message)
}
if portal.MXID == "" {
go func() {
retryCtx := cc.backgroundContext(ctx)
for attempt := 0; attempt < 3; attempt++ {
if portal.MXID != "" {
if err := send(retryCtx); err != nil {
cc.log.Warn().Err(err).Msg("Failed to send system notice")
}
return
}
time.Sleep(250 * time.Millisecond)
}
if portal.MXID == "" {
cc.log.Warn().Msg("Portal MXID never became available, dropping system notice")
return
}
if err := send(retryCtx); err != nil {
cc.log.Warn().Err(err).Msg("Failed to send system notice")
}
}()
return
}
if err := send(ctx); err != nil {
cc.log.Warn().Err(err).Msg("Failed to send system notice")
}
Comment on lines 1766 to +1797
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Welcome notices are now dropped on brand-new rooms.

createWelcomeCodexChat still calls this helper immediately after creating the portal. Before the room has an MXID, SendSystemMessage returns invalid portal, so the onboarding notices are just logged and never delivered.

Suggested fix
 func (cc *CodexClient) sendSystemNotice(ctx context.Context, portal *bridgev2.Portal, message string) {
-	if cc == nil {
+	if cc == nil || portal == nil || portal.MXID == "" {
 		return
 	}
 	if err := agentremote.SendSystemMessage(ctx, cc.UserLogin, portal, bridgev2.EventSender{}, strings.TrimSpace(message)); err != nil {
 		cc.log.Warn().Err(err).Msg("Failed to send system notice")
 	}
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (cc *CodexClient) sendSystemNotice(ctx context.Context, portal *bridgev2.Portal, message string) {
if portal == nil || portal.MXID == "" || cc.UserLogin == nil || cc.UserLogin.Bridge == nil {
if cc == nil {
return
}
timing := agentremote.ResolveEventTiming(time.Now(), 0)
cc.sendViaPortal(portal, agentremote.BuildSystemNotice(strings.TrimSpace(message)), "", timing.Timestamp, timing.StreamOrder)
if err := agentremote.SendSystemMessage(ctx, cc.UserLogin, portal, bridgev2.EventSender{}, strings.TrimSpace(message)); err != nil {
cc.log.Warn().Err(err).Msg("Failed to send system notice")
}
func (cc *CodexClient) sendSystemNotice(ctx context.Context, portal *bridgev2.Portal, message string) {
if cc == nil || portal == nil || portal.MXID == "" {
return
}
if err := agentremote.SendSystemMessage(ctx, cc.UserLogin, portal, bridgev2.EventSender{}, strings.TrimSpace(message)); err != nil {
cc.log.Warn().Err(err).Msg("Failed to send system notice")
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/codex/client.go` around lines 1766 - 1772, The welcome notices are
dropped because sendSystemNotice calls agentremote.SendSystemMessage immediately
when a newly created portal has no MXID; update sendSystemNotice (and keep
createWelcomeCodexChat callers unchanged) to detect an empty portal.MXID and
retry sending: if portal.MXID == "" spawn a short-lived goroutine that retries
agentremote.SendSystemMessage with a small backoff (e.g., 3 attempts, 200–500ms
interval) until portal.MXID is populated or attempts exhausted, and preserve the
existing logging (use cc.log.Warn().Err(err).Msg on final failure); ensure the
retry logic references sendSystemNotice and agentremote.SendSystemMessage so the
message is delivered once the portal has an MXID.

}

func (cc *CodexClient) sendPendingStatus(ctx context.Context, portal *bridgev2.Portal, evt *event.Event, message string) {
Expand Down
13 changes: 8 additions & 5 deletions bridges/codex/directory_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"time"

"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/database"
"maunium.net/go/mautrix/event"

"github.com/beeper/agentremote"
Expand Down Expand Up @@ -183,10 +182,14 @@ func (cc *CodexClient) createWelcomeCodexChat(ctx context.Context) (*bridgev2.Po
meta.CodexCwd = ""
meta.AwaitingCwdSetup = true
meta.ManagedImport = false
portal.RoomType = database.RoomTypeDM
portal.OtherUserID = codexGhostID
portal.Name = meta.Title
portal.NameSet = true
if err := agentremote.ConfigureDMPortal(ctx, agentremote.ConfigureDMPortalParams{
Portal: portal,
Title: meta.Title,
OtherUserID: codexGhostID,
Save: false,
}); err != nil {
return nil, err
}
info := cc.composeCodexChatInfo(portal, meta.Title, false)
created, err := bridgesdk.EnsurePortalLifecycle(ctx, bridgesdk.PortalLifecycleOptions{
Login: cc.UserLogin,
Expand Down
18 changes: 1 addition & 17 deletions bridges/codex/portal_send.go
Original file line number Diff line number Diff line change
@@ -1,22 +1,6 @@
package codex

import (
"time"

"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/id"
)

func (cc *CodexClient) sendViaPortal(
portal *bridgev2.Portal,
converted *bridgev2.ConvertedMessage,
msgID networkid.MessageID,
timestamp time.Time,
streamOrder int64,
) (id.EventID, networkid.MessageID, error) {
return cc.ClientBase.SendViaPortalWithOptions(portal, cc.senderForPortal(), msgID, timestamp, streamOrder, converted)
}
import "maunium.net/go/mautrix/bridgev2"

func (cc *CodexClient) senderForPortal() bridgev2.EventSender {
if cc == nil || cc.UserLogin == nil {
Expand Down
29 changes: 10 additions & 19 deletions bridges/dummybridge/bridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
"github.com/rs/zerolog"
"go.mau.fi/util/ptr"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/database"
"maunium.net/go/mautrix/bridgev2/networkid"

"github.com/beeper/agentremote"
Expand Down Expand Up @@ -229,13 +228,13 @@ func (dc *DummyBridgeConnector) ensureChatForIndexLocked(ctx context.Context, lo
meta.Topic = dummyPortalTopic
meta.ChatIndex = idx

portal.RoomType = database.RoomTypeDM
portal.OtherUserID = dummyAgentUserID
portal.Name = title
portal.Topic = dummyPortalTopic
portal.NameSet = true
portal.TopicSet = true
if err := portal.Save(ctx); err != nil {
if err := agentremote.ConfigureDMPortal(ctx, agentremote.ConfigureDMPortalParams{
Portal: portal,
Title: title,
Topic: dummyPortalTopic,
OtherUserID: dummyAgentUserID,
Save: false,
}); err != nil {
return nil, fmt.Errorf("save portal: %w", err)
}

Expand All @@ -258,24 +257,16 @@ func (dc *DummyBridgeConnector) ensureChatForIndexLocked(ctx context.Context, lo
}

func (dc *DummyBridgeConnector) composeChatInfo(login *bridgev2.UserLogin, title string) *bridgev2.ChatInfo {
info := agentremote.BuildLoginDMChatInfo(agentremote.LoginDMChatInfoParams{
return agentremote.BuildLoginDMChatInfo(agentremote.LoginDMChatInfoParams{
Title: title,
Topic: dummyPortalTopic,
Login: login,
HumanUserIDPrefix: "dummybridge-user",
BotUserID: dummyAgentUserID,
BotDisplayName: dummyAgentName,
BotUserInfo: dummySDKAgent().UserInfo(),
CanBackfill: false,
})
if info == nil {
return nil
}
info.Topic = ptr.Ptr(dummyPortalTopic)
if info.Members != nil && info.Members.MemberMap != nil {
member := info.Members.MemberMap[dummyAgentUserID]
member.UserInfo = dummySDKAgent().UserInfo()
info.Members.MemberMap[dummyAgentUserID] = member
}
return info
}

func dummyPortalID(idx int) string {
Expand Down
26 changes: 8 additions & 18 deletions bridges/openclaw/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ func (oc *OpenClawClient) GetChatInfo(ctx context.Context, portal *bridgev2.Port
roomType := openClawRoomType(meta)
agentID := stringutil.TrimDefault(meta.OpenClawDMTargetAgentID, meta.OpenClawAgentID)
if roomType == database.RoomTypeDM && agentID != "" {
info := oc.syntheticDMPortalInfo(agentID, title)
info := oc.buildOpenClawDMChatInfo(agentID, title, nil)
info.Topic = ptr.NonZero(oc.topicForPortal(meta))
info.Type = ptr.Ptr(roomType)
info.CanBackfill = true
Expand Down Expand Up @@ -735,25 +735,15 @@ func (oc *OpenClawClient) senderForAgent(agentID string, fromMe bool) bridgev2.E
}
}

func (oc *OpenClawClient) sendNoticeViaPortal(ctx context.Context, portal *bridgev2.Portal, msg string, sender bridgev2.EventSender) {
if portal == nil || strings.TrimSpace(msg) == "" {
func (oc *OpenClawClient) sendSystemNotice(ctx context.Context, portal *bridgev2.Portal, sender bridgev2.EventSender, msg string) {
if oc == nil || portal == nil || strings.TrimSpace(msg) == "" {
return
}
converted := &bridgev2.ConvertedMessage{
Parts: []*bridgev2.ConvertedMessagePart{{
ID: networkid.PartID("0"),
Type: event.EventMessage,
Content: &event.MessageEventContent{MsgType: event.MsgNotice, Body: msg, Mentions: &event.Mentions{}},
}},
}
oc.UserLogin.QueueRemoteEvent(buildOpenClawRemoteMessage(
portal.PortalKey,
newOpenClawMessageID(),
sender,
time.Now(),
0,
converted,
))
if err := agentremote.SendSystemMessage(ctx, oc.UserLogin, portal, sender, msg); err != nil {
if oc.UserLogin != nil {
oc.UserLogin.Log.Warn().Err(err).Msg("Failed to send system notice")
}
}
}

func (oc *OpenClawClient) DownloadAndEncodeMedia(ctx context.Context, mediaURL string, file *event.EncryptedFileInfo, maxMB int) (string, string, error) {
Expand Down
60 changes: 6 additions & 54 deletions bridges/openclaw/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,11 @@ import (
"strings"
"time"

"github.com/google/uuid"
"github.com/rs/zerolog"
"go.mau.fi/util/ptr"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/database"
"maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/bridgev2/simplevent"
"maunium.net/go/mautrix/event"

"github.com/beeper/agentremote"
"github.com/beeper/agentremote/pkg/shared/openclawconv"
Expand Down Expand Up @@ -146,29 +143,18 @@ func getOpenClawSessionChatInfo(ctx context.Context, portal *bridgev2.Portal, cl
roomType := openClawRoomType(meta)
client.maybeRefreshPortalCapabilities(ctx, portal, &previous)
if roomType == database.RoomTypeDM {
chatInfo := agentremote.BuildLoginDMChatInfo(agentremote.LoginDMChatInfoParams{
return agentremote.BuildLoginDMChatInfo(agentremote.LoginDMChatInfoParams{
Title: title,
Topic: client.topicForPortal(meta),
Login: client.UserLogin,
HumanUserIDPrefix: "openclaw-user",
HumanSender: ptr.Ptr(client.senderForAgent(agentID, true)),
BotUserID: openClawGhostUserID(agentID),
BotDisplayName: agentName,
BotSender: ptr.Ptr(client.senderForAgent(agentID, false)),
BotUserInfo: client.userInfoForAgentProfile(profile),
CanBackfill: true,
})
if chatInfo != nil {
chatInfo.Topic = ptr.NonZero(client.topicForPortal(meta))
if chatInfo.Members != nil && chatInfo.Members.MemberMap != nil {
chatInfo.Members.MemberMap[humanUserID(client.UserLogin.ID)] = bridgev2.ChatMember{
EventSender: client.senderForAgent(agentID, true),
Membership: event.MembershipJoin,
}
chatInfo.Members.MemberMap[openClawGhostUserID(agentID)] = bridgev2.ChatMember{
EventSender: client.senderForAgent(agentID, false),
Membership: event.MembershipJoin,
UserInfo: client.userInfoForAgentProfile(profile),
}
}
}
return chatInfo, nil
}), nil
}
memberMap := bridgev2.ChatMemberMap{
humanUserID(client.UserLogin.ID): {
Expand All @@ -190,37 +176,3 @@ func getOpenClawSessionChatInfo(ctx context.Context, portal *bridgev2.Portal, cl
},
}, nil
}

func buildOpenClawRemoteMessage(
portal networkid.PortalKey,
messageID networkid.MessageID,
sender bridgev2.EventSender,
timestamp time.Time,
streamOrder int64,
preBuilt *bridgev2.ConvertedMessage,
) *simplevent.PreConvertedMessage {
if timestamp.IsZero() {
timestamp = time.Now()
}
if streamOrder == 0 {
streamOrder = timestamp.UnixMilli()
}
return &simplevent.PreConvertedMessage{
EventMeta: simplevent.EventMeta{
Type: bridgev2.RemoteEventMessage,
PortalKey: portal,
Sender: sender,
Timestamp: timestamp,
StreamOrder: streamOrder,
LogContext: func(c zerolog.Context) zerolog.Context {
return c.Str("openclaw_msg_id", string(messageID))
},
},
ID: messageID,
Data: preBuilt,
}
}

func newOpenClawMessageID() networkid.MessageID {
return networkid.MessageID("openclaw:" + uuid.NewString())
}
4 changes: 2 additions & 2 deletions bridges/openclaw/gateway_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ import (

const (
openClawProtocolVersion = 3
openClawGatewayClientID = "beeper-bridge"
openClawGatewayClientMode = "ui"
openClawGatewayClientID = "gateway-client"
openClawGatewayClientMode = "backend"
openClawGatewayDisplayName = "Beeper"
openClawGatewayWSReadLimit = 32 * 1024 * 1024
openClawGatewayPingInterval = 30 * time.Second
Expand Down
Loading
Loading