Skip to content

add remote-echo commands#7

Merged
KishanBagaria merged 5 commits intomainfrom
codex/pending-remote-echo
Mar 13, 2026
Merged

add remote-echo commands#7
KishanBagaria merged 5 commits intomainfrom
codex/pending-remote-echo

Conversation

@KishanBagaria
Copy link
Copy Markdown
Member

@KishanBagaria KishanBagaria commented Mar 10, 2026

To test slow or missing remote echoes, send trigger phrases in the message body:

  • remote-echo none keeps that send pending forever.
  • remote-echo fail makes that send fail immediately with an error.
  • remote-echo delay 5s delays the remote echo for the parsed Go duration.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 10, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Per-message remote-echo controls: senders can suppress, fail, or schedule delayed remote echoes using trigger phrases in message text.
  • Documentation

    • Added guidance and examples for testing suppressed, failed, and delayed remote echoes (including delay durations).
  • Tests

    • New unit tests validating parsing and behavior of remote-echo trigger phrases and delay handling.

Walkthrough

Parses per-message "remote-echo" directives, optionally marks messages Pending, schedules cancellable delayed remote-echo tasks, returns Pending for delayed echoes, and adds unit tests and README usage examples.

Changes

Cohort / File(s) Summary
Documentation
README.md
Adds documentation and test instructions for per-message remote-echo controls (remote-echo none, remote-echo fail, remote-echo delay ...).
Connector client implementation
pkg/connector/client.go
Adds parsing for remote-echo directives (delayedRemoteEchoPattern, getRemoteEchoBehavior), transaction ID derivation (getTransactionID), cloning helper (cloneMessageContent), queueRemoteEcho to schedule cancellable delayed echo tasks, and ctx/stop fields on DummyClient. Updates message handling to persist pending DB messages and return Pending when behavior.pending is true.
Unit tests
pkg/connector/client_test.go
Adds table-driven tests for getRemoteEchoBehavior covering normal, no-echo, failure, delay, and case-insensitive directive variants.

Sequence Diagram

sequenceDiagram
    participant Client as Matrix Client
    participant Handler as HandleMatrixMessage
    participant Parser as getRemoteEchoBehavior
    participant DB as Database
    participant Scheduler as queueRemoteEcho
    participant Worker as Delayed Goroutine
    participant Poster as simplevent Poster

    Client->>Handler: Send message with "remote-echo ..." directive
    Handler->>Parser: Parse message content for echo behavior
    Parser-->>Handler: Return behavior {pending, delay}
    alt pending
        Handler->>DB: Save message marked Pending (with txn ID)
        Handler->>Scheduler: Queue delayed echo (delay)
        Handler-->>Client: Return Pending response
        Scheduler->>Worker: Start cancellable goroutine
        Worker->>Worker: Wait for delay or context cancel
        alt timeout
            Worker->>Poster: Post converted/echo message
            Poster-->>DB: Update/deliver message
        else canceled
            Worker-->>DB: Abort pending echo
        end
    else immediate
        Handler->>DB: Save and deliver immediate-success message
        Handler-->>Client: Return Success response
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'add remote-echo commands' clearly and concisely describes the main change: adding remote-echo command functionality for testing slow or missing remote echoes.
Description check ✅ Passed The description directly explains the remote-echo commands feature, detailing the three trigger phrases and their behaviors, which aligns with the changeset's additions.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch codex/pending-remote-echo

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
pkg/connector/client.go (1)

279-288: Shallow copy may share pointer fields.

cloned := *content creates a shallow copy. If MessageEventContent contains pointer fields (e.g., Info, RelatesTo), they remain shared with the original. For a test bridge this is likely fine, but consider a deep copy if content could be modified after queueing.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/connector/client.go` around lines 279 - 288, The cloneMessageContent
function currently does a shallow copy via "cloned := *content", which leaves
pointer fields (e.g., Info, RelatesTo, any pointer/slice/map/struct fields on
event.MessageEventContent) shared with the original; change it to perform a deep
copy by allocating a new event.MessageEventContent and explicitly copying
primitive fields and cloning any nested pointer/slice/map/struct fields (for
example, clone content.Info, content.RelatesTo, formatted bodies, and any
maps/slices) so the returned copy does not share mutable state with the
original; update cloneMessageContent to check nils and deep-clone each nested
field accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@pkg/connector/client.go`:
- Around line 279-288: The cloneMessageContent function currently does a shallow
copy via "cloned := *content", which leaves pointer fields (e.g., Info,
RelatesTo, any pointer/slice/map/struct fields on event.MessageEventContent)
shared with the original; change it to perform a deep copy by allocating a new
event.MessageEventContent and explicitly copying primitive fields and cloning
any nested pointer/slice/map/struct fields (for example, clone content.Info,
content.RelatesTo, formatted bodies, and any maps/slices) so the returned copy
does not share mutable state with the original; update cloneMessageContent to
check nils and deep-clone each nested field accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 96edf4fe-600a-4ac0-8252-c165137e986c

📥 Commits

Reviewing files that changed from the base of the PR and between e73e945 and b22976b.

📒 Files selected for processing (3)
  • README.md
  • pkg/connector/client.go
  • pkg/connector/client_test.go
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build-docker-dummybridge
🔇 Additional comments (8)
README.md (1)

14-14: LGTM!

Documentation clearly explains the new remote-echo testing feature with appropriate trigger phrase examples. The syntax aligns with the implementation.

Also applies to: 52-57

pkg/connector/client.go (6)

25-27: LGTM!

Good use of sync.WaitGroup for goroutine coordination and context-based cancellation for proper lifecycle management.


39-39: LGTM!

Regex pattern correctly handles case-insensitivity and anchors to the full message body. The integer-only duration constraint is a reasonable simplification for testing purposes.


77-78: LGTM!

Proper context lifecycle management with cancellation propagation and graceful shutdown via WaitGroup.

Also applies to: 107-109


177-196: LGTM!

The branching logic correctly handles pending vs immediate responses. The TransactionID properly correlates the pending message with the eventual remote echo event.


208-237: LGTM!

Clean implementation with proper nil checks, whitespace trimming, and case-insensitive matching.


239-277: LGTM!

Well-structured goroutine with proper timer cleanup and context cancellation. The early return for delay <= 0 correctly implements the "pending forever" behavior for remote-echo none.

pkg/connector/client_test.go (1)

10-33: LGTM!

Well-structured table-driven tests covering the main scenarios. Consider adding edge cases like nil content, empty body, or invalid duration formats (e.g., remote-echo delay abc) for more comprehensive coverage.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pkg/connector/client.go`:
- Around line 236-281: The goroutine closes over msg and clones msg.Content only
after the delay, so mutations to msg between scheduling and execution can change
the echoed payload; fix by snapshotting needed values before starting the
goroutine in queueRemoteEcho: read and store portalKey := msg.Portal.PortalKey
and clonedContent := cloneMessageContent(msg.Content) (and any other fields used
from msg) before calling dc.wg.Add(1)/go func(), then use portalKey and
clonedContent inside the goroutine instead of referencing msg.Portal or
msg.Content.
- Around line 177-190: The pending-path is incorrectly taken for "remote-echo
none" because getRemoteEchoBehavior can return pending=true with delay=0; update
the conditional around the pending handling in the connector to skip creating a
pending DB message and calling msg.AddPendingToSave when behavior.delay <= 0
(and when there's no portal flag if applicable): change the if that checks
behavior.pending (the block that calls getTransactionID, constructs
database.Message, calls msg.AddPendingToSave and dc.queueRemoteEcho) to require
behavior.pending && behavior.delay > 0 (and also ensure any portal-specific flag
is considered), so queueRemoteEcho is only invoked for real delayed remote-echo
cases and the zero-delay/no-echo path does not create unresolved pending
transactions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1e446cd7-bef5-4bd2-9c36-92eaa398e42b

📥 Commits

Reviewing files that changed from the base of the PR and between b22976b and befe033.

📒 Files selected for processing (1)
  • pkg/connector/client.go
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: build-docker-dummybridge
  • GitHub Check: build-docker-dummybridge

Comment on lines +177 to +190
behavior := getRemoteEchoBehavior(msg.Content)
if behavior.pending {
transactionID := getTransactionID(msg)
dbMessage := &database.Message{
ID: networkid.MessageID(transactionID),
SenderID: networkid.UserID(dc.UserLogin.ID),
Timestamp: timestamp,
}
msg.AddPendingToSave(dbMessage, transactionID, nil)
dc.queueRemoteEcho(msg, transactionID, timestamp, behavior.delay)
return &bridgev2.MatrixMessageResponse{
DB: dbMessage,
Pending: true,
}, nil
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

🧩 Analysis chain

🏁 Script executed:

cat -n pkg/connector/client.go | head -200

Repository: beeper/dummybridge

Length of output: 7293


🏁 Script executed:

# Find the getRemoteEchoBehavior and queueRemoteEcho function definitions
rg -n "func.*getRemoteEchoBehavior|func.*queueRemoteEcho" pkg/connector/client.go -A 20

Repository: beeper/dummybridge

Length of output: 1288


🏁 Script executed:

# Check lines 177-190 and 236-239 specifically
sed -n '170,200p' pkg/connector/client.go

Repository: beeper/dummybridge

Length of output: 915


🏁 Script executed:

# Also check around line 236
sed -n '230,250p' pkg/connector/client.go

Repository: beeper/dummybridge

Length of output: 520


🏁 Script executed:

# Search for any code that might resolve this pending state
rg -n "AddPendingToSave|Pending" pkg/connector/client.go

Repository: beeper/dummybridge

Length of output: 141


🏁 Script executed:

# Check if there's any context about what happens with unresolved pending transactions
rg -n "pending" pkg/connector/client.go -B 2 -A 2

Repository: beeper/dummybridge

Length of output: 661


🏁 Script executed:

# Search the broader codebase for any cleanup of pending states
rg -n "CancelPending|ResolvePending|pending.*resolve|pending.*cancel" --type go

Repository: beeper/dummybridge

Length of output: 44


Skip pending path for no-echo messages.

When remote-echo none is sent, getRemoteEchoBehavior() returns {pending: true, delay: 0}. This enters the pending block at line 178, calls msg.AddPendingToSave(), then passes delay=0 to queueRemoteEcho(). Since queueRemoteEcho() immediately returns when delay <= 0, the pending transaction is never resolved. The Pending/AddPendingToSave flow is meant for cases where a real remote event arrives later; no-echo messages have no follow-up.

Skip the pending block for zero-delay and no-portal cases by adding those conditions to the if statement:

Suggested change
-	if behavior.pending {
+	if behavior.pending && behavior.delay > 0 && msg.Portal != nil {
 		transactionID := getTransactionID(msg)
 		dbMessage := &database.Message{
 			ID:        networkid.MessageID(transactionID),
 			SenderID:  networkid.UserID(dc.UserLogin.ID),
 			Timestamp: timestamp,
 		}
 		msg.AddPendingToSave(dbMessage, transactionID, nil)
 		dc.queueRemoteEcho(msg, transactionID, timestamp, behavior.delay)
 		return &bridgev2.MatrixMessageResponse{
 			DB:      dbMessage,
 			Pending: true,
 		}, nil
 	}

Also applies to: 236-239

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/connector/client.go` around lines 177 - 190, The pending-path is
incorrectly taken for "remote-echo none" because getRemoteEchoBehavior can
return pending=true with delay=0; update the conditional around the pending
handling in the connector to skip creating a pending DB message and calling
msg.AddPendingToSave when behavior.delay <= 0 (and when there's no portal flag
if applicable): change the if that checks behavior.pending (the block that calls
getTransactionID, constructs database.Message, calls msg.AddPendingToSave and
dc.queueRemoteEcho) to require behavior.pending && behavior.delay > 0 (and also
ensure any portal-specific flag is considered), so queueRemoteEcho is only
invoked for real delayed remote-echo cases and the zero-delay/no-echo path does
not create unresolved pending transactions.

Comment on lines +236 to +281
func (dc *DummyClient) queueRemoteEcho(msg *bridgev2.MatrixMessage, transactionID networkid.TransactionID, timestamp time.Time, delay time.Duration) {
if delay <= 0 || msg.Portal == nil {
return
}

dc.wg.Add(1)
go func() {
defer dc.wg.Done()

timer := time.NewTimer(delay)
defer timer.Stop()

select {
case <-dc.ctx.Done():
return
case <-timer.C:
}

dc.UserLogin.QueueRemoteEvent(&simplevent.PreConvertedMessage{
EventMeta: simplevent.EventMeta{
Type: bridgev2.RemoteEventMessage,
PortalKey: msg.Portal.PortalKey,
Sender: bridgev2.EventSender{
IsFromMe: true,
SenderLogin: dc.UserLogin.ID,
Sender: networkid.UserID(dc.UserLogin.ID),
},
Timestamp: timestamp,
StreamOrder: time.Now().UnixNano(),
},
Data: &bridgev2.ConvertedMessage{Parts: []*bridgev2.ConvertedMessagePart{{
Type: event.EventMessage,
Content: cloneMessageContent(msg.Content),
}}},
ID: randomMessageID(),
TransactionID: transactionID,
})
}()
}

func cloneMessageContent(content *event.MessageEventContent) *event.MessageEventContent {
if content == nil {
return nil
}
cloned := *content
return &cloned
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

Snapshot the echoed payload before starting the timer goroutine.

This goroutine closes over msg and only clones msg.Content after the delay expires. Any mutation to the request object after HandleMatrixMessage returns will change what gets echoed. Capture the portal key and cloned content up front, then use those immutable values inside the goroutine.

Suggested change
 func (dc *DummyClient) queueRemoteEcho(msg *bridgev2.MatrixMessage, transactionID networkid.TransactionID, timestamp time.Time, delay time.Duration) {
 	if delay <= 0 || msg.Portal == nil {
 		return
 	}
+	portalKey := msg.Portal.PortalKey
+	content := cloneMessageContent(msg.Content)
 
 	dc.wg.Add(1)
 	go func() {
 		defer dc.wg.Done()
@@
 		dc.UserLogin.QueueRemoteEvent(&simplevent.PreConvertedMessage{
 			EventMeta: simplevent.EventMeta{
 				Type:      bridgev2.RemoteEventMessage,
-				PortalKey: msg.Portal.PortalKey,
+				PortalKey: portalKey,
 				Sender: bridgev2.EventSender{
 					IsFromMe:    true,
 					SenderLogin: dc.UserLogin.ID,
 					Sender:      networkid.UserID(dc.UserLogin.ID),
 				},
@@
 			},
 			Data: &bridgev2.ConvertedMessage{Parts: []*bridgev2.ConvertedMessagePart{{
 				Type:    event.EventMessage,
-				Content: cloneMessageContent(msg.Content),
+				Content: content,
 			}}},
 			ID:            randomMessageID(),
 			TransactionID: transactionID,
 		})
 	}()
 }
📝 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 (dc *DummyClient) queueRemoteEcho(msg *bridgev2.MatrixMessage, transactionID networkid.TransactionID, timestamp time.Time, delay time.Duration) {
if delay <= 0 || msg.Portal == nil {
return
}
dc.wg.Add(1)
go func() {
defer dc.wg.Done()
timer := time.NewTimer(delay)
defer timer.Stop()
select {
case <-dc.ctx.Done():
return
case <-timer.C:
}
dc.UserLogin.QueueRemoteEvent(&simplevent.PreConvertedMessage{
EventMeta: simplevent.EventMeta{
Type: bridgev2.RemoteEventMessage,
PortalKey: msg.Portal.PortalKey,
Sender: bridgev2.EventSender{
IsFromMe: true,
SenderLogin: dc.UserLogin.ID,
Sender: networkid.UserID(dc.UserLogin.ID),
},
Timestamp: timestamp,
StreamOrder: time.Now().UnixNano(),
},
Data: &bridgev2.ConvertedMessage{Parts: []*bridgev2.ConvertedMessagePart{{
Type: event.EventMessage,
Content: cloneMessageContent(msg.Content),
}}},
ID: randomMessageID(),
TransactionID: transactionID,
})
}()
}
func cloneMessageContent(content *event.MessageEventContent) *event.MessageEventContent {
if content == nil {
return nil
}
cloned := *content
return &cloned
func (dc *DummyClient) queueRemoteEcho(msg *bridgev2.MatrixMessage, transactionID networkid.TransactionID, timestamp time.Time, delay time.Duration) {
if delay <= 0 || msg.Portal == nil {
return
}
portalKey := msg.Portal.PortalKey
content := cloneMessageContent(msg.Content)
dc.wg.Add(1)
go func() {
defer dc.wg.Done()
timer := time.NewTimer(delay)
defer timer.Stop()
select {
case <-dc.ctx.Done():
return
case <-timer.C:
}
dc.UserLogin.QueueRemoteEvent(&simplevent.PreConvertedMessage{
EventMeta: simplevent.EventMeta{
Type: bridgev2.RemoteEventMessage,
PortalKey: portalKey,
Sender: bridgev2.EventSender{
IsFromMe: true,
SenderLogin: dc.UserLogin.ID,
Sender: networkid.UserID(dc.UserLogin.ID),
},
Timestamp: timestamp,
StreamOrder: time.Now().UnixNano(),
},
Data: &bridgev2.ConvertedMessage{Parts: []*bridgev2.ConvertedMessagePart{{
Type: event.EventMessage,
Content: content,
}}},
ID: randomMessageID(),
TransactionID: transactionID,
})
}()
}
func cloneMessageContent(content *event.MessageEventContent) *event.MessageEventContent {
if content == nil {
return nil
}
cloned := *content
return &cloned
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/connector/client.go` around lines 236 - 281, The goroutine closes over
msg and clones msg.Content only after the delay, so mutations to msg between
scheduling and execution can change the echoed payload; fix by snapshotting
needed values before starting the goroutine in queueRemoteEcho: read and store
portalKey := msg.Portal.PortalKey and clonedContent :=
cloneMessageContent(msg.Content) (and any other fields used from msg) before
calling dc.wg.Add(1)/go func(), then use portalKey and clonedContent inside the
goroutine instead of referencing msg.Portal or msg.Content.

@KishanBagaria KishanBagaria marked this pull request as ready for review March 10, 2026 00:37
Copilot AI review requested due to automatic review settings March 10, 2026 00:37
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds per-message “remote echo” controls to the dummy connector so outbound Matrix sends can be intentionally left pending or remote-echoed after a delay, which is useful for testing slow/missing remote echo behavior.

Changes:

  • Introduces trigger-phrase parsing (remote-echo none / remote-echo delay <duration>) to switch a message into “pending” mode.
  • Implements delayed remote-echo scheduling via a background goroutine and connector shutdown cancellation.
  • Adds unit tests for the trigger parser and documents the feature in the README.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
pkg/connector/client.go Adds parsing + pending-send path and delayed remote echo scheduling/cancellation.
pkg/connector/client_test.go Adds unit tests for remote-echo trigger parsing.
README.md Documents how to use the new trigger phrases.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +236 to +239
func (dc *DummyClient) queueRemoteEcho(msg *bridgev2.MatrixMessage, transactionID networkid.TransactionID, timestamp time.Time, delay time.Duration) {
if delay <= 0 || msg.Portal == nil {
return
}
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

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

remote-echo delay 0s currently results in behavior.pending=true (parsed duration is 0), but queueRemoteEcho returns early for delay <= 0, so the message stays pending forever. Consider either treating a zero delay as an immediate remote echo (still queue with no timer) or rejecting/ignoring zero delays in getRemoteEchoBehavior so the send isn't marked pending.

Copilot uses AI. Check for mistakes.
Comment on lines +248 to +250
select {
case <-dc.ctx.Done():
return
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

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

queueRemoteEcho uses dc.ctx.Done() in the goroutine, but dc.ctx is only initialized in Connect. If HandleMatrixMessage (or this helper) can run before Connect, this will panic due to a nil context.Context. Consider defaulting dc.ctx to the passed-in ctx (or context.Background()) when dc.ctx is nil.

Copilot uses AI. Check for mistakes.
To test slow or missing remote echoes, send trigger phrases in the message body:

- `remote-echo none` or `remote-echo delay 0s` keeps that send pending forever.
- `remote-echo delay 5s` delays the remote echo for the parsed Go duration.
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

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

The README says remote-echo delay 5s delays the echo for the "parsed Go duration", but the implementation only accepts a restricted subset via regex ([0-9]+ + ms|s|m|h) and will reject valid time.ParseDuration inputs like 1m30s, 250ms, 1.5s, etc. Either broaden the parser/regex to match time.ParseDuration semantics or adjust the documentation to reflect the supported format.

Suggested change
- `remote-echo delay 5s` delays the remote echo for the parsed Go duration.
- `remote-echo delay 5s` delays the remote echo for the given duration, specified as an integer followed by a unit (`ms`, `s`, `m`, or `h`), for example `5s`, `250ms`, `2m`, or `1h`.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: befe033d84

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

}

func (dc *DummyClient) queueRemoteEcho(msg *bridgev2.MatrixMessage, transactionID networkid.TransactionID, timestamp time.Time, delay time.Duration) {
if delay <= 0 || msg.Portal == nil {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Complete zero-delay echoes instead of leaving them pending

When a message body is remote-echo delay 0s, HandleMatrixMessage marks it as pending and then queueRemoteEcho immediately returns because of delay <= 0, so no remote event is ever queued and the send stays pending forever. This differs from the documented “delay by duration” behavior, where a zero duration should complete immediately rather than behave like remote-echo none.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (2)
pkg/connector/client.go (2)

239-275: ⚠️ Potential issue | 🟠 Major

Snapshot the echoed payload before starting the timer goroutine.

This worker still reads msg.Portal.PortalKey and clones msg.Content only after the delay expires. If the request object is mutated after HandleMatrixMessage() returns, the remote echo can be sent to the wrong portal or with different content than the original message.

Suggested change
 func (dc *DummyClient) queueRemoteEcho(msg *bridgev2.MatrixMessage, transactionID networkid.TransactionID, timestamp time.Time, delay time.Duration) {
 	if delay <= 0 || msg.Portal == nil {
 		return
 	}
+	portalKey := msg.Portal.PortalKey
+	content := cloneMessageContent(msg.Content)
 
 	dc.wg.Add(1)
 	go func() {
 		defer dc.wg.Done()
@@
 		dc.UserLogin.QueueRemoteEvent(&simplevent.PreConvertedMessage{
 			EventMeta: simplevent.EventMeta{
 				Type:      bridgev2.RemoteEventMessage,
-				PortalKey: msg.Portal.PortalKey,
+				PortalKey: portalKey,
 				Sender: bridgev2.EventSender{
 					IsFromMe:    true,
 					SenderLogin: dc.UserLogin.ID,
 					Sender:      networkid.UserID(dc.UserLogin.ID),
 				},
@@
 			},
 			Data: &bridgev2.ConvertedMessage{Parts: []*bridgev2.ConvertedMessagePart{{
 				Type:    event.EventMessage,
-				Content: cloneMessageContent(msg.Content),
+				Content: content,
 			}}},
 			ID:            randomMessageID(),
 			TransactionID: transactionID,
 		})
 	}()
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/connector/client.go` around lines 239 - 275, The queueRemoteEcho worker
reads fields from msg after the delay, which can be mutated; to fix, snapshot
all needed values from msg before starting the goroutine in
DummyClient.queueRemoteEcho: capture portalKey := msg.Portal.PortalKey (after
the nil check), clonedContent := cloneMessageContent(msg.Content), and any other
values used later (e.g., transactionID, timestamp, ID if generated now) into
local variables, then reference those locals inside the goroutine instead of
msg.* or msg.Portal.*, ensuring the queued PreConvertedMessage uses the copied
portalKey and clonedContent.

177-190: ⚠️ Potential issue | 🟠 Major

Don't return a pending send when no delayed echo will be queued.

remote-echo none still takes this branch because getRemoteEchoBehavior() returns {pending: true, delay: 0}. That creates a pending DB message and returns Pending: true, but queueRemoteEcho() immediately exits on zero delay, so nothing ever resolves that pending state.

Suggested change
-	if behavior.pending {
+	if behavior.pending && behavior.delay > 0 && msg.Portal != nil {
 		transactionID := getTransactionID(msg)
 		dbMessage := &database.Message{
 			ID:        randomMessageID(),
 			SenderID:  networkid.UserID(dc.UserLogin.ID),
 			Timestamp: timestamp,
 		}
 		msg.AddPendingToSave(dbMessage, transactionID, nil)
 		dc.queueRemoteEcho(msg, transactionID, timestamp, behavior.delay)
 		return &bridgev2.MatrixMessageResponse{
 			DB:      dbMessage,
 			Pending: true,
 		}, nil
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/connector/client.go` around lines 177 - 190, getRemoteEchoBehavior may
set pending:true with delay:0 causing a pending DB entry that's never resolved;
only create the pending DB message, call msg.AddPendingToSave, invoke
dc.queueRemoteEcho, and return Pending:true when both behavior.pending and
behavior.delay>0. Modify the branch around getRemoteEchoBehavior(msg.Content) to
check behavior.delay>0 (in addition to behavior.pending) before calling
getTransactionID, creating dbMessage, msg.AddPendingToSave, dc.queueRemoteEcho,
and returning a bridgev2.MatrixMessageResponse with Pending:true; otherwise fall
through to the non-pending/send path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@pkg/connector/client.go`:
- Around line 239-275: The queueRemoteEcho worker reads fields from msg after
the delay, which can be mutated; to fix, snapshot all needed values from msg
before starting the goroutine in DummyClient.queueRemoteEcho: capture portalKey
:= msg.Portal.PortalKey (after the nil check), clonedContent :=
cloneMessageContent(msg.Content), and any other values used later (e.g.,
transactionID, timestamp, ID if generated now) into local variables, then
reference those locals inside the goroutine instead of msg.* or msg.Portal.*,
ensuring the queued PreConvertedMessage uses the copied portalKey and
clonedContent.
- Around line 177-190: getRemoteEchoBehavior may set pending:true with delay:0
causing a pending DB entry that's never resolved; only create the pending DB
message, call msg.AddPendingToSave, invoke dc.queueRemoteEcho, and return
Pending:true when both behavior.pending and behavior.delay>0. Modify the branch
around getRemoteEchoBehavior(msg.Content) to check behavior.delay>0 (in addition
to behavior.pending) before calling getTransactionID, creating dbMessage,
msg.AddPendingToSave, dc.queueRemoteEcho, and returning a
bridgev2.MatrixMessageResponse with Pending:true; otherwise fall through to the
non-pending/send path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8e630d4b-5360-4c6e-b6b5-2c35a52bb4ae

📥 Commits

Reviewing files that changed from the base of the PR and between c62ac2e and c630574.

📒 Files selected for processing (1)
  • pkg/connector/client.go
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: build-docker-dummybridge
  • GitHub Check: build-docker-dummybridge

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
pkg/connector/client.go (1)

245-281: ⚠️ Potential issue | 🟠 Major

Snapshot the echoed payload before starting the timer goroutine.

This still closes over msg, so any mutation after HandleMatrixMessage returns can change the delayed echo’s portal or content. Capture immutable values up front and use those inside the goroutine.

Suggested change
 func (dc *DummyClient) queueRemoteEcho(msg *bridgev2.MatrixMessage, transactionID networkid.TransactionID, timestamp time.Time, delay time.Duration) {
 	if delay <= 0 || msg.Portal == nil {
 		return
 	}
+	portalKey := msg.Portal.PortalKey
+	content := cloneMessageContent(msg.Content)
 
 	dc.wg.Add(1)
 	go func() {
 		defer dc.wg.Done()
@@
 		dc.UserLogin.QueueRemoteEvent(&simplevent.PreConvertedMessage{
 			EventMeta: simplevent.EventMeta{
 				Type:      bridgev2.RemoteEventMessage,
-				PortalKey: msg.Portal.PortalKey,
+				PortalKey: portalKey,
 				Sender: bridgev2.EventSender{
 					IsFromMe:    true,
 					SenderLogin: dc.UserLogin.ID,
 					Sender:      networkid.UserID(dc.UserLogin.ID),
@@
 			},
 			Data: &bridgev2.ConvertedMessage{Parts: []*bridgev2.ConvertedMessagePart{{
 				Type:    event.EventMessage,
-				Content: cloneMessageContent(msg.Content),
+				Content: content,
 			}}},
 			ID:            randomMessageID(),
 			TransactionID: transactionID,
 		})
 	}()
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/connector/client.go` around lines 245 - 281, In queueRemoteEcho, avoid
closing over mutable msg by snapshotting the needed immutable pieces (e.g.,
portal := msg.Portal, clonedContent := cloneMessageContent(msg.Content), and any
other fields like msg.Portal.PortalKey) before launching the goroutine, and then
have the goroutine use those local copies (referencing
DummyClient.queueRemoteEcho, msg.Portal, cloneMessageContent, and the
PreConvertedMessage construction) so mutations to msg after HandleMatrixMessage
returns cannot affect the delayed echo.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pkg/connector/client.go`:
- Line 39: The regex delayedRemoteEchoPattern currently only matches single-unit
durations and should be broadened so time.ParseDuration handles full Go duration
syntax: update the pattern declared in delayedRemoteEchoPattern to accept the
entire duration token (e.g., use a capture that matches any non-empty sequence
instead of the current single-unit pattern) and keep using time.ParseDuration
where the capture is parsed (the code path around the time.ParseDuration call
that handles remote-echo delay). Ensure the variable name
delayedRemoteEchoPattern and the existing capture group used by the downstream
parsing logic are preserved so only validation shifts to time.ParseDuration.
- Around line 285-290: cloneMessageContent currently does a shallow copy of
*event.MessageEventContent which leaves pointer and slice fields shared and can
lead to races when the original msg.Content is mutated before a delayed
goroutine runs; update cloneMessageContent to perform a deep copy: allocate a
new event.MessageEventContent, copy primitive fields, and for each pointer field
(Info, File, Mentions, NewContent, RelatesTo, MessageSendRetry,
BeeperPerMessageProfile, BeeperActionMessage, BeeperLinkPreviews) allocate and
copy the pointed-to structs, and for slice fields (Methods, BeeperGalleryImages,
BeeperLinkPreviews, Mentions if a slice) create new slices and copy each element
(deep-cloning elements as needed) so the returned copy is fully independent of
the original.

---

Duplicate comments:
In `@pkg/connector/client.go`:
- Around line 245-281: In queueRemoteEcho, avoid closing over mutable msg by
snapshotting the needed immutable pieces (e.g., portal := msg.Portal,
clonedContent := cloneMessageContent(msg.Content), and any other fields like
msg.Portal.PortalKey) before launching the goroutine, and then have the
goroutine use those local copies (referencing DummyClient.queueRemoteEcho,
msg.Portal, cloneMessageContent, and the PreConvertedMessage construction) so
mutations to msg after HandleMatrixMessage returns cannot affect the delayed
echo.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bc466abe-a93a-4564-b757-f534a3b72680

📥 Commits

Reviewing files that changed from the base of the PR and between c630574 and 9cca122.

📒 Files selected for processing (3)
  • README.md
  • pkg/connector/client.go
  • pkg/connector/client_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • pkg/connector/client_test.go
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: build-docker-dummybridge
  • GitHub Check: build-docker-dummybridge

var _ bridgev2.DeleteChatHandlingNetworkAPI = (*DummyClient)(nil)
var _ bridgev2.MessageRequestAcceptingNetworkAPI = (*DummyClient)(nil)

var delayedRemoteEchoPattern = regexp.MustCompile(`(?i)^remote-echo\s+delay\s+([0-9]+(?:ms|s|m|h))$`)
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 | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, locate and read the file around line 39 to see the pattern definition
cat -n pkg/connector/client.go | sed -n '30,50p'

Repository: beeper/dummybridge

Length of output: 1088


🏁 Script executed:

# Read lines 224-242 mentioned in "Also applies to"
cat -n pkg/connector/client.go | sed -n '220,250p'

Repository: beeper/dummybridge

Length of output: 1180


🏁 Script executed:

# Search for getRemoteEchoBehavior function to understand the parsing logic
rg -n "getRemoteEchoBehavior" pkg/connector/client.go -A 25

Repository: beeper/dummybridge

Length of output: 1907


🏁 Script executed:

# Check for README to verify what documentation says about the format
fd -name "README*" -type f

Repository: beeper/dummybridge

Length of output: 292


🏁 Script executed:

# Search for README files
find . -type f -iname "README*" | head -20

Repository: beeper/dummybridge

Length of output: 73


🏁 Script executed:

# Check if there's documentation about remote-echo format
rg -i "remote-echo" -A 3 -B 3 README* 2>/dev/null | head -50

Repository: beeper/dummybridge

Length of output: 406


🏁 Script executed:

# Verify what time.ParseDuration accepts by checking Go standard library documentation reference
# Let's also test the regex claim directly
python3 << 'EOF'
import re

pattern = re.compile(r'(?i)^remote-echo\s+delay\s+([0-9]+(?:ms|s|m|h))$')

test_cases = [
    "remote-echo delay 100ms",
    "remote-echo delay 5s",
    "remote-echo delay 1m",
    "remote-echo delay 2h",
    "remote-echo delay 1m30s",  # Should fail - two units
    "remote-echo delay 250us",    # Should fail - us not in list
    "remote-echo delay 100",      # Should fail - no unit
]

print("Testing regex pattern:")
for test in test_cases:
    match = pattern.search(test)
    print(f"  '{test}': {'MATCH' if match else 'NO MATCH'}")
    if match:
        print(f"    -> captured: '{match.group(1)}'")
EOF

Repository: beeper/dummybridge

Length of output: 418


remote-echo delay regex only accepts single-unit Go durations, not compound or nanosecond-precision formats.

The regex pattern rejects valid Go durations like 1m30s or 250us, silently falling back to immediate-success behavior. This contradicts the README claim of "parsed Go duration" support. Either tighten the README wording to document the supported subset, or change the regex to (?i)^remote-echo\s+delay\s+(.+)$ and let time.ParseDuration (line 238) handle validation.

Suggested change
-var delayedRemoteEchoPattern = regexp.MustCompile(`(?i)^remote-echo\s+delay\s+([0-9]+(?:ms|s|m|h))$`)
+var delayedRemoteEchoPattern = regexp.MustCompile(`(?i)^remote-echo\s+delay\s+(.+)$`)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/connector/client.go` at line 39, The regex delayedRemoteEchoPattern
currently only matches single-unit durations and should be broadened so
time.ParseDuration handles full Go duration syntax: update the pattern declared
in delayedRemoteEchoPattern to accept the entire duration token (e.g., use a
capture that matches any non-empty sequence instead of the current single-unit
pattern) and keep using time.ParseDuration where the capture is parsed (the code
path around the time.ParseDuration call that handles remote-echo delay). Ensure
the variable name delayedRemoteEchoPattern and the existing capture group used
by the downstream parsing logic are preserved so only validation shifts to
time.ParseDuration.

Comment on lines +285 to +290
func cloneMessageContent(content *event.MessageEventContent) *event.MessageEventContent {
if content == nil {
return nil
}
cloned := *content
return &cloned
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

🧩 Analysis chain

🏁 Script executed:

# First, locate the file and examine the context around cloneMessageContent
fd pkg/connector/client.go

Repository: beeper/dummybridge

Length of output: 44


🏁 Script executed:

# Search for the MessageEventContent definition in the event package
fd -e go | xargs grep -l "type MessageEventContent struct"

Repository: beeper/dummybridge

Length of output: 46


🏁 Script executed:

# Look at the cloneMessageContent function and its context
rg -A 20 "func cloneMessageContent" pkg/connector/client.go

Repository: beeper/dummybridge

Length of output: 922


🏁 Script executed:

# Find all usages of cloneMessageContent to understand how it's used
rg "cloneMessageContent" --type go

Repository: beeper/dummybridge

Length of output: 246


🏁 Script executed:

# Find where MessageEventContent is defined - likely in mautrix or similar dependency
fd -e go | xargs grep -l "type MessageEventContent struct"

Repository: beeper/dummybridge

Length of output: 46


🏁 Script executed:

# Get the context around the usage of cloneMessageContent
rg -B 10 -A 3 "Content: cloneMessageContent" pkg/connector/client.go

Repository: beeper/dummybridge

Length of output: 524


🏁 Script executed:

# Search for delayed echo references to understand the context
rg -i "delayed.*echo|echo.*delayed" --type go pkg/connector/

Repository: beeper/dummybridge

Length of output: 273


🏁 Script executed:

# Look for the full context of delayed remote echo handling
rg -B 5 -A 20 "delayedRemoteEchoPattern" pkg/connector/client.go

Repository: beeper/dummybridge

Length of output: 2182


🏁 Script executed:

# Check the imports in client.go to understand where event comes from
head -50 pkg/connector/client.go | grep -E "import|event"

Repository: beeper/dummybridge

Length of output: 540


🏁 Script executed:

# Search for MessageEventContent definition anywhere in go files (might be imported from mautrix)
rg "type MessageEventContent" --type go

Repository: beeper/dummybridge

Length of output: 44


🌐 Web query:

MessageEventContent mautrix go struct fields

💡 Result:

In maunium.net/go/mautrix/event, MessageEventContent is defined with these exported fields (and JSON keys): [1]

  • MsgType MessageType json:"msgtype,omitempty"
  • Body string json:"body"
  • Format Format json:"format,omitempty"
  • FormattedBody string json:"formatted_body,omitempty"
  • GeoURI string json:"geo_uri,omitempty"
  • URL id.ContentURIString json:"url,omitempty"
  • Info *FileInfo json:"info,omitempty"
  • File *EncryptedFileInfo json:"file,omitempty"
  • FileName string json:"filename,omitempty"
  • Mentions *Mentions json:"m.mentions,omitempty"
  • NewContent *MessageEventContent json:"m.new_content,omitempty"
  • RelatesTo *RelatesTo json:"m.relates_to,omitempty"
  • To id.UserID json:"to,omitempty"
  • FromDevice id.DeviceID json:"from_device,omitempty"
  • Methods []VerificationMethod json:"methods,omitempty"
  • MessageSendRetry *BeeperRetryMetadata json:"com.beeper.message_send_retry,omitempty"
  • BeeperGalleryImages []*MessageEventContent json:"com.beeper.gallery.images,omitempty"
  • BeeperGalleryCaption string json:"com.beeper.gallery.caption,omitempty"
  • BeeperGalleryCaptionHTML string json:"com.beeper.gallery.caption_html,omitempty"
  • BeeperPerMessageProfile *BeeperPerMessageProfile json:"com.beeper.per_message_profile,omitempty"
  • BeeperActionMessage *BeeperActionMessage json:"com.beeper.action_message,omitempty"
  • BeeperLinkPreviews []*BeeperLinkPreview json:"com.beeper.linkpreviews,omitempty"
  • BeeperDisappearingTimer *BeeperDisappearingTimer json:"com.beeper.disappearing_timer,omitempty"
  • MSC1767Audio *MSC1767Audio json:"org.matrix.msc1767.audio,omitempty"
  • MSC3245Voice *MSC3245Voice json:"org.matrix.msc3245.voice,omitempty"
  • MSC4391BotCommand *MSC4391BotCommandInput json:"org.matrix.msc4391.command,omitempty"

It also “contains filtered or unexported fields” (so there are additional private fields not shown in GoDocs). [1]

Source(s):
[1] (pkg.go.dev)

Citations:


🏁 Script executed:

# Get more context around queueRemoteEcho to see how the cloned content is used
rg -A 30 "func (dc \*DummyClient) queueRemoteEcho" pkg/connector/client.go

Repository: beeper/dummybridge

Length of output: 44


🏁 Script executed:

# Get the full queueRemoteEcho function
rg -A 40 "func \(dc \*DummyClient\) queueRemoteEcho" pkg/connector/client.go

Repository: beeper/dummybridge

Length of output: 1182


🏁 Script executed:

# Look at the full file around line 285 to understand the complete context
sed -n '270,320p' pkg/connector/client.go

Repository: beeper/dummybridge

Length of output: 1736


cloneMessageContent performs only a shallow copy, leaving nested reference fields shared with the original.

event.MessageEventContent contains pointer fields (Info, File, Mentions, NewContent, RelatesTo, MessageSendRetry, BeeperPerMessageProfile, BeeperActionMessage, BeeperLinkPreviews) and slice fields (Methods, BeeperGalleryImages, BeeperLinkPreviews) that are not deep-copied. When the delayed remote echo is queued, if the original msg.Content is modified after this function returns but before the delayed goroutine processes it, those mutations are visible through the cloned copy, breaking the state isolation that the delayed path requires. A deep copy is needed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/connector/client.go` around lines 285 - 290, cloneMessageContent
currently does a shallow copy of *event.MessageEventContent which leaves pointer
and slice fields shared and can lead to races when the original msg.Content is
mutated before a delayed goroutine runs; update cloneMessageContent to perform a
deep copy: allocate a new event.MessageEventContent, copy primitive fields, and
for each pointer field (Info, File, Mentions, NewContent, RelatesTo,
MessageSendRetry, BeeperPerMessageProfile, BeeperActionMessage,
BeeperLinkPreviews) allocate and copy the pointed-to structs, and for slice
fields (Methods, BeeperGalleryImages, BeeperLinkPreviews, Mentions if a slice)
create new slices and copy each element (deep-cloning elements as needed) so the
returned copy is fully independent of the original.

@KishanBagaria KishanBagaria requested a review from tulir March 11, 2026 04:06
@KishanBagaria KishanBagaria merged commit 308f5c6 into main Mar 13, 2026
7 checks passed
@KishanBagaria KishanBagaria deleted the codex/pending-remote-echo branch March 13, 2026 22:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants