From 51f4f67c470f7fe87daa0c182956b7eec759334e Mon Sep 17 00:00:00 2001 From: Aniruddh Jha <97840004+aniruddh909@users.noreply.github.com> Date: Fri, 12 Jun 2026 23:18:44 +0200 Subject: [PATCH] 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. --- core/http/react-ui/src/pages/AgentChat.jsx | 3 +- core/services/agents/events.go | 10 ++- core/services/agents/events_test.go | 78 ++++++++++++++++++++++ 3 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 core/services/agents/events_test.go diff --git a/core/http/react-ui/src/pages/AgentChat.jsx b/core/http/react-ui/src/pages/AgentChat.jsx index eb19e1e6a..ae4f5c83b 100644 --- a/core/http/react-ui/src/pages/AgentChat.jsx +++ b/core/http/react-ui/src/pages/AgentChat.jsx @@ -139,7 +139,8 @@ export default function AgentChat() { id: nextId(), sender, content: data.content || data.message || '', - timestamp: data.timestamp ? Math.floor(data.timestamp / 1e6) : Date.now(), + // Backend sends Unix milliseconds (see core/services/agents events). + timestamp: data.timestamp || Date.now(), } if (data.metadata && Object.keys(data.metadata).length > 0) { msg.metadata = data.metadata diff --git a/core/services/agents/events.go b/core/services/agents/events.go index 9a214acf6..b187b295a 100644 --- a/core/services/agents/events.go +++ b/core/services/agents/events.go @@ -27,7 +27,7 @@ type AgentEvent struct { Content string `json:"content,omitempty"` MessageID string `json:"message_id,omitempty"` Metadata string `json:"metadata,omitempty"` // JSON metadata - Timestamp int64 `json:"timestamp"` + Timestamp int64 `json:"timestamp"` // Unix milliseconds (set by PublishEvent) } // AgentCancelEvent is the NATS message payload for cancelling agent execution. @@ -61,8 +61,14 @@ func NewEventBridge(nc messaging.MessagingClient, store *AgentStore, instanceID } // PublishEvent publishes an agent event to NATS for SSE bridging. +// +// Timestamp is emitted in Unix milliseconds to match the local dispatcher's +// json_message events (see dispatcher.go) and the React UI, which feeds the +// value straight into `new Date(ts)`. Milliseconds also stay within JS's +// safe-integer range, whereas nanoseconds (~1.7e18) do not and lose precision +// when parsed as a JSON number. func (b *EventBridge) PublishEvent(agentName, userID string, evt AgentEvent) error { - evt.Timestamp = time.Now().UnixNano() + evt.Timestamp = time.Now().UnixMilli() subject := messaging.SubjectAgentEvents(agentName, userID) return b.nats.Publish(subject, evt) } diff --git a/core/services/agents/events_test.go b/core/services/agents/events_test.go new file mode 100644 index 000000000..f103f7982 --- /dev/null +++ b/core/services/agents/events_test.go @@ -0,0 +1,78 @@ +package agents + +import ( + "time" + + "github.com/mudler/LocalAI/core/services/messaging" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +// fakeMessagingClient implements messaging.MessagingClient and captures the +// last published payload so tests can assert on it. +type fakeMessagingClient struct { + lastSubject string + lastData any +} + +func (f *fakeMessagingClient) Publish(subject string, data any) error { + f.lastSubject = subject + f.lastData = data + return nil +} + +func (f *fakeMessagingClient) Subscribe(string, func([]byte)) (messaging.Subscription, error) { + return &fakeSub{}, nil +} + +func (f *fakeMessagingClient) QueueSubscribe(string, string, func([]byte)) (messaging.Subscription, error) { + return &fakeSub{}, nil +} + +func (f *fakeMessagingClient) QueueSubscribeReply(string, string, func([]byte, func([]byte))) (messaging.Subscription, error) { + return &fakeSub{}, nil +} + +func (f *fakeMessagingClient) SubscribeReply(string, func([]byte, func([]byte))) (messaging.Subscription, error) { + return &fakeSub{}, nil +} + +func (f *fakeMessagingClient) Request(string, []byte, time.Duration) ([]byte, error) { + return nil, nil +} + +func (f *fakeMessagingClient) IsConnected() bool { return true } +func (f *fakeMessagingClient) Close() {} + +type fakeSub struct{} + +func (s *fakeSub) Unsubscribe() error { return nil } + +var _ = Describe("EventBridge", func() { + Describe("PublishEvent timestamp", func() { + // Regression for #9867: agent chat messages rendered a broken + // timestamp ("Invalid Timestamp" / "12:00 AM") in the web UI because + // this path emitted Unix nanoseconds while the local dispatcher and the + // React UI both expect Unix milliseconds. Nanoseconds also overflow JS's + // safe-integer range. The timestamp must be in milliseconds. + It("emits the timestamp in Unix milliseconds", func() { + fake := &fakeMessagingClient{} + bridge := NewEventBridge(fake, nil, "instance-1") + + before := time.Now().UnixMilli() + err := bridge.PublishMessage("agent", "user", "agent", "hello", "msg-1") + after := time.Now().UnixMilli() + + Expect(err).ToNot(HaveOccurred()) + + evt, ok := fake.lastData.(AgentEvent) + Expect(ok).To(BeTrue(), "published payload should be an AgentEvent") + + // A millisecond timestamp falls within [before, after]; a nanosecond + // one (~1e6 larger) would be far outside this window. + Expect(evt.Timestamp).To(BeNumerically(">=", before)) + Expect(evt.Timestamp).To(BeNumerically("<=", after)) + }) + }) +})