mirror of
https://github.com/mudler/LocalAI.git
synced 2026-06-12 18:58:49 -04:00
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.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
78
core/services/agents/events_test.go
Normal file
78
core/services/agents/events_test.go
Normal file
@@ -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))
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user