Skip to content
Open
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
3 changes: 2 additions & 1 deletion core/http/react-ui/src/pages/AgentChat.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 8 additions & 2 deletions core/services/agents/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
}
Expand Down
78 changes: 78 additions & 0 deletions core/services/agents/events_test.go
Original file line number Diff line number Diff line change
@@ -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))
})
})
})