Skip to content

Commit 51f4f67

Browse files
authored
fix(agents): emit chat event timestamps in milliseconds (#9867) (#10243)
Agent chat replies rendered a broken timestamp in the web UI ("Invalid Timestamp" / "12:00 AM", identical for every reply) because the SSE timestamp unit was inconsistent across producers. EventBridge.PublishEvent emitted Unix nanoseconds while the local dispatcher (dispatcher.go) already emitted Unix milliseconds, and the React UI fed the value straight into `new Date(ts)` after dividing by 1e6. Nanoseconds also overflow JS's safe-integer range (~1.7e18). Standardize on Unix milliseconds: switch PublishEvent to UnixMilli and drop the /1e6 conversion in AgentChat.jsx so both SSE paths agree and match the React UI's expectation. Add a regression test asserting the published timestamp is in milliseconds.
1 parent cf71e29 commit 51f4f67

3 files changed

Lines changed: 88 additions & 3 deletions

File tree

core/http/react-ui/src/pages/AgentChat.jsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,8 @@ export default function AgentChat() {
139139
id: nextId(),
140140
sender,
141141
content: data.content || data.message || '',
142-
timestamp: data.timestamp ? Math.floor(data.timestamp / 1e6) : Date.now(),
142+
// Backend sends Unix milliseconds (see core/services/agents events).
143+
timestamp: data.timestamp || Date.now(),
143144
}
144145
if (data.metadata && Object.keys(data.metadata).length > 0) {
145146
msg.metadata = data.metadata

core/services/agents/events.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ type AgentEvent struct {
2727
Content string `json:"content,omitempty"`
2828
MessageID string `json:"message_id,omitempty"`
2929
Metadata string `json:"metadata,omitempty"` // JSON metadata
30-
Timestamp int64 `json:"timestamp"`
30+
Timestamp int64 `json:"timestamp"` // Unix milliseconds (set by PublishEvent)
3131
}
3232

3333
// AgentCancelEvent is the NATS message payload for cancelling agent execution.
@@ -61,8 +61,14 @@ func NewEventBridge(nc messaging.MessagingClient, store *AgentStore, instanceID
6161
}
6262

6363
// PublishEvent publishes an agent event to NATS for SSE bridging.
64+
//
65+
// Timestamp is emitted in Unix milliseconds to match the local dispatcher's
66+
// json_message events (see dispatcher.go) and the React UI, which feeds the
67+
// value straight into `new Date(ts)`. Milliseconds also stay within JS's
68+
// safe-integer range, whereas nanoseconds (~1.7e18) do not and lose precision
69+
// when parsed as a JSON number.
6470
func (b *EventBridge) PublishEvent(agentName, userID string, evt AgentEvent) error {
65-
evt.Timestamp = time.Now().UnixNano()
71+
evt.Timestamp = time.Now().UnixMilli()
6672
subject := messaging.SubjectAgentEvents(agentName, userID)
6773
return b.nats.Publish(subject, evt)
6874
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package agents
2+
3+
import (
4+
"time"
5+
6+
"github.com/mudler/LocalAI/core/services/messaging"
7+
8+
. "github.com/onsi/ginkgo/v2"
9+
. "github.com/onsi/gomega"
10+
)
11+
12+
// fakeMessagingClient implements messaging.MessagingClient and captures the
13+
// last published payload so tests can assert on it.
14+
type fakeMessagingClient struct {
15+
lastSubject string
16+
lastData any
17+
}
18+
19+
func (f *fakeMessagingClient) Publish(subject string, data any) error {
20+
f.lastSubject = subject
21+
f.lastData = data
22+
return nil
23+
}
24+
25+
func (f *fakeMessagingClient) Subscribe(string, func([]byte)) (messaging.Subscription, error) {
26+
return &fakeSub{}, nil
27+
}
28+
29+
func (f *fakeMessagingClient) QueueSubscribe(string, string, func([]byte)) (messaging.Subscription, error) {
30+
return &fakeSub{}, nil
31+
}
32+
33+
func (f *fakeMessagingClient) QueueSubscribeReply(string, string, func([]byte, func([]byte))) (messaging.Subscription, error) {
34+
return &fakeSub{}, nil
35+
}
36+
37+
func (f *fakeMessagingClient) SubscribeReply(string, func([]byte, func([]byte))) (messaging.Subscription, error) {
38+
return &fakeSub{}, nil
39+
}
40+
41+
func (f *fakeMessagingClient) Request(string, []byte, time.Duration) ([]byte, error) {
42+
return nil, nil
43+
}
44+
45+
func (f *fakeMessagingClient) IsConnected() bool { return true }
46+
func (f *fakeMessagingClient) Close() {}
47+
48+
type fakeSub struct{}
49+
50+
func (s *fakeSub) Unsubscribe() error { return nil }
51+
52+
var _ = Describe("EventBridge", func() {
53+
Describe("PublishEvent timestamp", func() {
54+
// Regression for #9867: agent chat messages rendered a broken
55+
// timestamp ("Invalid Timestamp" / "12:00 AM") in the web UI because
56+
// this path emitted Unix nanoseconds while the local dispatcher and the
57+
// React UI both expect Unix milliseconds. Nanoseconds also overflow JS's
58+
// safe-integer range. The timestamp must be in milliseconds.
59+
It("emits the timestamp in Unix milliseconds", func() {
60+
fake := &fakeMessagingClient{}
61+
bridge := NewEventBridge(fake, nil, "instance-1")
62+
63+
before := time.Now().UnixMilli()
64+
err := bridge.PublishMessage("agent", "user", "agent", "hello", "msg-1")
65+
after := time.Now().UnixMilli()
66+
67+
Expect(err).ToNot(HaveOccurred())
68+
69+
evt, ok := fake.lastData.(AgentEvent)
70+
Expect(ok).To(BeTrue(), "published payload should be an AgentEvent")
71+
72+
// A millisecond timestamp falls within [before, after]; a nanosecond
73+
// one (~1e6 larger) would be far outside this window.
74+
Expect(evt.Timestamp).To(BeNumerically(">=", before))
75+
Expect(evt.Timestamp).To(BeNumerically("<=", after))
76+
})
77+
})
78+
})

0 commit comments

Comments
 (0)