Skip to content

Commit 72ea7c8

Browse files
committed
Ensure strict Matrix ordering with anchor timestamps
Introduce explicit anchor/fallback timestamps to guarantee correct Matrix event ordering. Track lastConsumedTimestamp on activeAIRun and add anchorTimestamp to assistantStreamState; compute new run anchor times via nextAssistantAnchorTimestamp and afterMatrixTimestamp (ensures at least +1ms and not in the past). Propagate timestamps through assistantEvent, assistantFinalEditWithProjection, queueAssistantRunError, queueAssistantFinal and queueConsumedUserEcho, and persist CreatedAt from the stream anchor. Add a unit test to verify fallback event ordering and adjust existing tests to supply timestamps.
1 parent f2f9593 commit 72ea7c8

2 files changed

Lines changed: 112 additions & 35 deletions

File tree

pkg/connector/client.go

Lines changed: 66 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -101,18 +101,21 @@ type activeAIRun struct {
101101
last *assistantStreamState
102102
status *bridgev2.MessageStatusEventInfo
103103
approvals map[string]*activeApproval
104+
105+
lastConsumedTimestamp time.Time
104106
}
105107

106108
type assistantStreamState struct {
107-
messageID networkid.MessageID
108-
eventID id.EventID
109-
runID string
110-
run *aistream.Run
111-
metadata *aiid.MessageMetadata
112-
entryID string
113-
tools []toolOutputEvent
114-
sources *sourceCollector
115-
publish streamPublishCursor
109+
messageID networkid.MessageID
110+
eventID id.EventID
111+
runID string
112+
run *aistream.Run
113+
metadata *aiid.MessageMetadata
114+
entryID string
115+
anchorTimestamp time.Time
116+
tools []toolOutputEvent
117+
sources *sourceCollector
118+
publish streamPublishCursor
116119
}
117120

118121
type streamPublishCursor struct {
@@ -682,15 +685,14 @@ func (cl *Client) markConsumedFailed(ctx context.Context, pending *pendingAIMess
682685
cl.Main.Bridge.Matrix.SendMessageStatus(ctx, &status, bridgev2.StatusEventInfoFromEvent(pending.msg.Event))
683686
}
684687

685-
func (cl *Client) queueConsumedUserEcho(ctx context.Context, pending *pendingAIMessage, userEntryID string) {
688+
func (cl *Client) queueConsumedUserEcho(ctx context.Context, pending *pendingAIMessage, userEntryID string, echoTimestamp time.Time) {
686689
if pending == nil || pending.msg == nil {
687690
return
688691
}
689692
pending.metadata.SessionEntryID = userEntryID
690693
pending.metadata.Role = "user"
691694
pending.metadata.StreamStatus = "done"
692695
messageID := aiid.UserMessageID(userEntryID)
693-
echoTimestamp := matrixEventTime(pending.msg.Event)
694696
cl.UserLogin.QueueRemoteEvent(&simplevent.PreConvertedMessage{
695697
EventMeta: simplevent.EventMeta{
696698
Type: bridgev2.RemoteEventMessage,
@@ -712,7 +714,7 @@ func (cl *Client) queueConsumedUserEcho(ctx context.Context, pending *pendingAIM
712714
})
713715
}
714716

715-
func (cl *Client) queueAssistantRunError(portalKey networkid.PortalKey, messageID networkid.MessageID, providerID string, modelID string, runID string, run aistream.Run, metadata *aiid.MessageMetadata, err error) {
717+
func (cl *Client) queueAssistantRunError(portalKey networkid.PortalKey, messageID networkid.MessageID, providerID string, modelID string, runID string, run aistream.Run, metadata *aiid.MessageMetadata, timestamp time.Time, err error) {
716718
message := ai.Message{
717719
Role: "assistant",
718720
StopReason: ai.StopReasonError,
@@ -724,7 +726,7 @@ func (cl *Client) queueAssistantRunError(portalKey networkid.PortalKey, messageI
724726
metadata.StreamStatus = "error"
725727
}
726728
run = finalizedAssistantRun(run, message, 0)
727-
cl.UserLogin.QueueRemoteEvent(cl.assistantFinalEditWithProjection(portalKey, messageID, providerID, modelID, run, metadata))
729+
cl.UserLogin.QueueRemoteEvent(cl.assistantFinalEditWithProjection(portalKey, messageID, providerID, modelID, run, metadata, timestamp))
728730
}
729731

730732
func hookStreamError(err error) *ai.AssistantMessageEventStream {
@@ -902,15 +904,15 @@ func (cl *Client) defaultConversationTitle(ctx context.Context, portal *bridgev2
902904
return "New AI Chat"
903905
}
904906

905-
func (cl *Client) assistantEvent(ctx context.Context, portalKey networkid.PortalKey, messageID networkid.MessageID, providerID string, modelID string, runID string, descriptor *event.BeeperStreamInfo, run aistream.Run) (*simplevent.PreConvertedMessage, *aiid.MessageMetadata) {
907+
func (cl *Client) assistantEvent(ctx context.Context, portalKey networkid.PortalKey, messageID networkid.MessageID, providerID string, modelID string, runID string, descriptor *event.BeeperStreamInfo, run aistream.Run, timestamp time.Time) (*simplevent.PreConvertedMessage, *aiid.MessageMetadata) {
906908
metadata := &aiid.MessageMetadata{
907909
Role: "assistant",
908910
ProviderID: providerID,
909911
ModelID: modelID,
910912
RunID: runID,
911913
StreamStatus: "streaming",
912914
}
913-
msg := aibridgev2.Anchor(portalKey, aiid.AssistantUserID(), run, time.Now())
915+
msg := aibridgev2.Anchor(portalKey, aiid.AssistantUserID(), run, timestamp)
914916
if len(msg.Data.Parts) > 0 {
915917
msg.Data.Parts[0].ID = aiid.PartID("text")
916918
msg.Data.Parts[0].Content.BeeperStream = descriptor
@@ -948,9 +950,9 @@ func isZeroAGUIUsage(usage agui.Usage) bool {
948950
usage.ContextLimit == 0
949951
}
950952

951-
func (cl *Client) assistantFinalEditWithProjection(portalKey networkid.PortalKey, messageID networkid.MessageID, providerID string, modelID string, run aistream.Run, metadata *aiid.MessageMetadata) *simplevent.Message[*aistream.Run] {
953+
func (cl *Client) assistantFinalEditWithProjection(portalKey networkid.PortalKey, messageID networkid.MessageID, providerID string, modelID string, run aistream.Run, metadata *aiid.MessageMetadata, timestamp time.Time) *simplevent.Message[*aistream.Run] {
952954
initialProjection := aimatrix.ProjectFinal(run, nil)
953-
edit := aibridgev2.FinalMetadataEditWithContent(portalKey, aiid.AssistantUserID(), messageID, run, initialProjection.Content, initialProjection.Extra, time.Now())
955+
edit := aibridgev2.FinalMetadataEditWithContent(portalKey, aiid.AssistantUserID(), messageID, run, initialProjection.Content, initialProjection.Extra, timestamp)
954956
edit.ConvertEditFunc = func(ctx context.Context, portal *bridgev2.Portal, intent bridgev2.MatrixAPI, existing []*database.Message, data *aistream.Run) (*bridgev2.ConvertedEdit, error) {
955957
if len(existing) == 0 {
956958
return nil, nil
@@ -982,12 +984,12 @@ func (cl *Client) assistantFinalEditWithProjection(portalKey networkid.PortalKey
982984
return edit
983985
}
984986

985-
func (cl *Client) queueAssistantFinal(portalKey networkid.PortalKey, messageID networkid.MessageID, providerID string, model ai.Model, run aistream.Run, message ai.Message, metadata *aiid.MessageMetadata) {
987+
func (cl *Client) queueAssistantFinal(portalKey networkid.PortalKey, messageID networkid.MessageID, providerID string, model ai.Model, run aistream.Run, message ai.Message, metadata *aiid.MessageMetadata, timestamp time.Time) {
986988
if cl == nil || cl.UserLogin == nil {
987989
return
988990
}
989991
run = finalizedAssistantRun(run, message, model.ContextWindow)
990-
cl.UserLogin.QueueRemoteEvent(cl.assistantFinalEditWithProjection(portalKey, messageID, providerID, model.ID, run, metadata))
992+
cl.UserLogin.QueueRemoteEvent(cl.assistantFinalEditWithProjection(portalKey, messageID, providerID, model.ID, run, metadata, timestamp))
991993
}
992994

993995
func uploadFinalPartsRef(ctx context.Context, portal *bridgev2.Portal, intent bridgev2.MatrixAPI, run aistream.Run, message aistream.UIMessage) (*aistream.FinalPartsRef, error) {
@@ -1070,7 +1072,11 @@ func (cl *Client) assistantStreamPublisher(publisher bridgev2.BeeperStreamPublis
10701072
}
10711073
runID := session.CreateSessionID()
10721074
messageID := aiid.AssistantMessageID(runID)
1073-
run := aistream.NewRun(runID, meta.SessionID, provider.ID+"/"+model.ID, string(aiid.AssistantUserID()), "AI", time.Now())
1075+
anchorTimestamp := time.Now()
1076+
if active := cl.getActiveRun(portal.PortalKey); active != nil {
1077+
anchorTimestamp = active.nextAssistantAnchorTimestamp()
1078+
}
1079+
run := aistream.NewRun(runID, meta.SessionID, provider.ID+"/"+model.ID, string(aiid.AssistantUserID()), "AI", anchorTimestamp)
10741080
run.MessageID = string(messageID)
10751081
enrichAIRunMetadata(run, model, options)
10761082
descriptor, err := publisher.NewDescriptor(ctx, portal.MXID, aiid.StreamType)
@@ -1081,7 +1087,7 @@ func (cl *Client) assistantStreamPublisher(publisher bridgev2.BeeperStreamPublis
10811087
cl.logStreamDebug(ctx, portal.MXID, "", run, "Created AI stream descriptor", func(evt *zerolog.Event) {
10821088
evt.Str("stream_type", descriptor.Type).Str("descriptor_user_id", string(descriptor.UserID))
10831089
})
1084-
assistantEvent, metadata := cl.assistantEvent(ctx, portal.PortalKey, messageID, provider.ID, model.ID, runID, descriptor, *run)
1090+
assistantEvent, metadata := cl.assistantEvent(ctx, portal.PortalKey, messageID, provider.ID, model.ID, runID, descriptor, *run, anchorTimestamp)
10851091
eventID, err := cl.queueAssistantStreamAnchor(ctx, portal, assistantEvent, messageID)
10861092
if err != nil {
10871093
cl.logStreamError(err, portal.MXID, "", run, "Failed to queue AI stream anchor")
@@ -1090,21 +1096,22 @@ func (cl *Client) assistantStreamPublisher(publisher bridgev2.BeeperStreamPublis
10901096
cl.logStreamDebug(ctx, portal.MXID, eventID, run, "Queued AI stream anchor")
10911097
if err := publisher.Register(ctx, portal.MXID, eventID, descriptor); err != nil {
10921098
cl.logStreamError(err, portal.MXID, eventID, run, "Failed to register AI stream publisher")
1093-
cl.queueAssistantRunError(portal.PortalKey, messageID, provider.ID, model.ID, runID, *run, metadata, err)
1099+
cl.queueAssistantRunError(portal.PortalKey, messageID, provider.ID, model.ID, runID, *run, metadata, afterMatrixTimestamp(anchorTimestamp), err)
10941100
return hookStreamError(err)
10951101
}
10961102
cl.logStreamDebug(ctx, portal.MXID, eventID, run, "Registered AI stream publisher", func(evt *zerolog.Event) {
10971103
evt.Str("stream_type", descriptor.Type)
10981104
})
10991105
if active := cl.getActiveRun(portal.PortalKey); active != nil {
11001106
stream := &assistantStreamState{
1101-
messageID: messageID,
1102-
eventID: eventID,
1103-
runID: runID,
1104-
run: run,
1105-
metadata: metadata,
1106-
sources: newSourceCollector(),
1107-
publish: streamPublishCursor{nextSeq: 1},
1107+
messageID: messageID,
1108+
eventID: eventID,
1109+
runID: runID,
1110+
run: run,
1111+
metadata: metadata,
1112+
anchorTimestamp: anchorTimestamp,
1113+
sources: newSourceCollector(),
1114+
publish: streamPublishCursor{nextSeq: 1},
11081115
}
11091116
stream.publish.persist = func(ctx context.Context, run *aistream.Run) error {
11101117
return cl.persistActiveStream(ctx, portal, provider.ID, model.ID, active, stream, run)
@@ -1389,6 +1396,7 @@ func (cl *Client) persistActiveStream(ctx context.Context, portal *bridgev2.Port
13891396
Run: *run,
13901397
Metadata: metadata,
13911398
StatusInfo: statusInfo,
1399+
CreatedAt: stream.anchorTimestamp,
13921400
UpdatedAt: time.Now(),
13931401
})
13941402
}
@@ -1950,6 +1958,17 @@ func matrixEventTime(evt *event.Event) time.Time {
19501958
return time.UnixMilli(evt.Timestamp)
19511959
}
19521960

1961+
func afterMatrixTimestamp(base time.Time) time.Time {
1962+
if base.IsZero() {
1963+
return time.Now()
1964+
}
1965+
min := base.Add(time.Millisecond)
1966+
if now := time.Now(); now.After(min) {
1967+
return now
1968+
}
1969+
return min
1970+
}
1971+
19531972
func (cl *Client) setActiveHarness(key networkid.PortalKey, h *harness.AgentHarness) {
19541973
cl.activeMu.Lock()
19551974
defer cl.activeMu.Unlock()
@@ -2108,7 +2127,7 @@ func (cl *Client) finishActiveStreamRecord(ctx context.Context, record aidb.Acti
21082127
cl.queueTerminalActiveStreamFinal(ctx, record)
21092128
default:
21102129
cl.finalizeInterruptedSessionTurn(ctx, record, err)
2111-
cl.queueAssistantRunError(record.PortalKey, record.MessageID, record.ProviderID, record.ModelID, record.RunID, record.Run, &record.Metadata, err)
2130+
cl.queueAssistantRunError(record.PortalKey, record.MessageID, record.ProviderID, record.ModelID, record.RunID, record.Run, &record.Metadata, afterMatrixTimestamp(record.CreatedAt), err)
21122131
cl.sendActiveStreamRetriableStatus(ctx, record, err)
21132132
}
21142133
}
@@ -2129,7 +2148,7 @@ func (cl *Client) queueTerminalActiveStreamFinal(ctx context.Context, record aid
21292148
metadata.StopReason = string(ai.StopReasonError)
21302149
}
21312150
}
2132-
cl.UserLogin.QueueRemoteEvent(cl.assistantFinalEditWithProjection(record.PortalKey, record.MessageID, record.ProviderID, record.ModelID, record.Run, &metadata))
2151+
cl.UserLogin.QueueRemoteEvent(cl.assistantFinalEditWithProjection(record.PortalKey, record.MessageID, record.ProviderID, record.ModelID, record.Run, &metadata, afterMatrixTimestamp(record.CreatedAt)))
21332152
}
21342153

21352154
func (cl *Client) finalizeInterruptedSessionTurn(ctx context.Context, record aidb.ActiveStreamRecord, cause error) {
@@ -2353,11 +2372,18 @@ func (r *activeAIRun) markConsumed(ctx context.Context, cl *Client, entryID stri
23532372
pending := r.pending[0]
23542373
r.pending = r.pending[1:]
23552374
r.consumed = append(r.consumed, pending)
2375+
echoTimestamp := time.Now()
2376+
if pending != nil && pending.msg != nil {
2377+
echoTimestamp = matrixEventTime(pending.msg.Event)
2378+
}
2379+
if echoTimestamp.After(r.lastConsumedTimestamp) {
2380+
r.lastConsumedTimestamp = echoTimestamp
2381+
}
23562382
if pending != nil && pending.msg != nil && pending.msg.Event != nil {
23572383
r.status = bridgev2.StatusEventInfoFromEvent(pending.msg.Event)
23582384
}
23592385
r.mu.Unlock()
2360-
cl.queueConsumedUserEcho(ctx, pending, entryID)
2386+
cl.queueConsumedUserEcho(ctx, pending, entryID, echoTimestamp)
23612387
}
23622388

23632389
func (r *activeAIRun) failAll(ctx context.Context, cl *Client, err error) {
@@ -2386,6 +2412,12 @@ func (r *activeAIRun) addAssistantStream(stream *assistantStreamState) {
23862412
r.streams = append(r.streams, stream)
23872413
}
23882414

2415+
func (r *activeAIRun) nextAssistantAnchorTimestamp() time.Time {
2416+
r.mu.Lock()
2417+
defer r.mu.Unlock()
2418+
return afterMatrixTimestamp(r.lastConsumedTimestamp)
2419+
}
2420+
23892421
func (r *activeAIRun) hasAssistantRun(runID string) bool {
23902422
r.mu.Lock()
23912423
defer r.mu.Unlock()
@@ -2479,7 +2511,7 @@ func (r *activeAIRun) finalizeAssistant(ctx context.Context, cl *Client, provide
24792511

24802512
fillAssistantMetadata(stream.metadata, stream.entryID, providerID, model.ID, stream.runID, message)
24812513
appendToolOutputs(stream.run, stream.tools, message)
2482-
cl.queueAssistantFinal(r.portalKey, stream.messageID, providerID, model, *stream.run, message, stream.metadata)
2514+
cl.queueAssistantFinal(r.portalKey, stream.messageID, providerID, model, *stream.run, message, stream.metadata, afterMatrixTimestamp(stream.anchorTimestamp))
24832515
cl.deleteActiveStream(ctx, stream.runID)
24842516
cl.queueAssistantMediaMessages(r.portalKey, stream.messageID, providerID, model.ID, stream.runID, message)
24852517
}
@@ -2490,7 +2522,7 @@ func (r *activeAIRun) failOpenAssistant(ctx context.Context, cl *Client, provide
24902522
r.streams = nil
24912523
r.mu.Unlock()
24922524
for _, stream := range streams {
2493-
cl.queueAssistantRunError(r.portalKey, stream.messageID, providerID, modelID, stream.runID, *stream.run, stream.metadata, err)
2525+
cl.queueAssistantRunError(r.portalKey, stream.messageID, providerID, modelID, stream.runID, *stream.run, stream.metadata, afterMatrixTimestamp(stream.anchorTimestamp), err)
24942526
cl.deleteActiveStream(ctx, stream.runID)
24952527
}
24962528
}

pkg/connector/stream_test.go

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -915,6 +915,7 @@ func TestAssistantEventMetadataCanBeFinalizedBeforeInsert(t *testing.T) {
915915
"run",
916916
&event.BeeperStreamInfo{Type: "com.beeper.ai.response"},
917917
*run,
918+
timeNow(),
918919
)
919920
if metadata.StreamStatus != "streaming" {
920921
t.Fatalf("expected streaming metadata, got %#v", metadata)
@@ -944,7 +945,7 @@ func TestAssistantEventMetadataCanBeFinalizedBeforeInsert(t *testing.T) {
944945
Role: "assistant",
945946
StopReason: ai.StopReasonStop,
946947
}, 0)
947-
edit := client.assistantFinalEditWithProjection(aiid.PortalKey(id.RoomID("!room:example.com"), "login"), "assistant:run", "beeper", "gpt-5", finalRun, metadata)
948+
edit := client.assistantFinalEditWithProjection(aiid.PortalKey(id.RoomID("!room:example.com"), "login"), "assistant:run", "beeper", "gpt-5", finalRun, metadata, timeNow().Add(time.Millisecond))
948949
if edit.Sender.Sender != aiid.AssistantUserID() {
949950
t.Fatalf("assistant final edit used sender %q", edit.Sender.Sender)
950951
}
@@ -958,6 +959,50 @@ func TestAssistantEventMetadataCanBeFinalizedBeforeInsert(t *testing.T) {
958959
}
959960
}
960961

962+
func TestAssistantFallbackEventsUseStrictMatrixOrder(t *testing.T) {
963+
client := &Client{}
964+
userTimestamp := time.Now().Add(10 * time.Second).Truncate(time.Millisecond)
965+
active := &activeAIRun{lastConsumedTimestamp: userTimestamp}
966+
anchorTimestamp := active.nextAssistantAnchorTimestamp()
967+
if !anchorTimestamp.Equal(userTimestamp.Add(time.Millisecond)) {
968+
t.Fatalf("anchor timestamp = %s, want %s", anchorTimestamp, userTimestamp.Add(time.Millisecond))
969+
}
970+
run := aistream.NewRun("run", "thread", "beeper/gpt-5", "assistant:run", "GPT-5", anchorTimestamp)
971+
run.MessageID = "assistant:run"
972+
assistantEvent, metadata := client.assistantEvent(
973+
context.Background(),
974+
aiid.PortalKey(id.RoomID("!room:example.com"), "login"),
975+
"assistant:run",
976+
"beeper",
977+
"gpt-5",
978+
"run",
979+
&event.BeeperStreamInfo{Type: "com.beeper.ai.response"},
980+
*run,
981+
anchorTimestamp,
982+
)
983+
if !assistantEvent.Timestamp.After(userTimestamp) || assistantEvent.StreamOrder <= userTimestamp.UnixNano() {
984+
t.Fatalf("anchor event not after user echo: timestamp=%s stream_order=%d user=%s", assistantEvent.Timestamp, assistantEvent.StreamOrder, userTimestamp)
985+
}
986+
987+
finalTimestamp := afterMatrixTimestamp(anchorTimestamp)
988+
if !finalTimestamp.Equal(anchorTimestamp.Add(time.Millisecond)) {
989+
t.Fatalf("final timestamp = %s, want %s", finalTimestamp, anchorTimestamp.Add(time.Millisecond))
990+
}
991+
finalRun := finalizedAssistantRun(*run, ai.Message{Role: "assistant", StopReason: ai.StopReasonStop}, 0)
992+
edit := client.assistantFinalEditWithProjection(
993+
aiid.PortalKey(id.RoomID("!room:example.com"), "login"),
994+
"assistant:run",
995+
"beeper",
996+
"gpt-5",
997+
finalRun,
998+
metadata,
999+
finalTimestamp,
1000+
)
1001+
if !edit.Timestamp.After(assistantEvent.Timestamp) || edit.StreamOrder <= assistantEvent.StreamOrder {
1002+
t.Fatalf("final edit not after anchor: timestamp=%s stream_order=%d anchor=%s/%d", edit.Timestamp, edit.StreamOrder, assistantEvent.Timestamp, assistantEvent.StreamOrder)
1003+
}
1004+
}
1005+
9611006
func TestAssistantModelProfileUsesConfiguredModelDisplayName(t *testing.T) {
9621007
provider := aiid.ProviderConfig{
9631008
ID: "custom",

0 commit comments

Comments
 (0)