From 4ecef97f8980af200eb81ec50a0145c1d1eafc46 Mon Sep 17 00:00:00 2001 From: Andy Braren Date: Fri, 23 Jan 2026 09:04:44 -0500 Subject: [PATCH 1/5] Enhance event handling by ensuring timestamps are set for AGUI events - Added logic to set a timestamp for AGUI events in `agui_proxy.go` and `agui.go` if not already present, ensuring accurate message tracking. - Updated `compaction.go` to preserve the timestamp from events when creating messages. - Modified frontend components to utilize the timestamp from backend messages, providing a fallback for legacy messages. This change improves the reliability of message timestamp tracking across the system. --- components/backend/websocket/agui.go | 11 ++++++++++- components/backend/websocket/agui_proxy.go | 6 +++++- components/backend/websocket/compaction.go | 17 ++++++++++++++--- .../[name]/sessions/[sessionName]/page.tsx | 3 +++ .../frontend/src/hooks/use-agui-stream.ts | 6 ++++++ 5 files changed, 38 insertions(+), 5 deletions(-) diff --git a/components/backend/websocket/agui.go b/components/backend/websocket/agui.go index 9a6681797..666fe2f78 100644 --- a/components/backend/websocket/agui.go +++ b/components/backend/websocket/agui.go @@ -123,6 +123,11 @@ func RouteAGUIEvent(sessionID string, event map[string]interface{}) { // If no active run found, check if event has a runId we should create if activeRunState == nil { + // Ensure timestamp is set before any early returns + if event["timestamp"] == nil || event["timestamp"] == "" { + event["timestamp"] = time.Now().UTC().Format(time.RFC3339Nano) + } + // Don't create lazy runs for terminal events - they should only apply to existing runs if isTerminalEventType(eventType) { go persistAGUIEventMap(sessionID, "", event) @@ -163,13 +168,17 @@ func RouteAGUIEvent(sessionID string, event map[string]interface{}) { threadID = eventThreadID } - // Fill in missing IDs only if not present + // Fill in missing IDs and timestamp if event["threadId"] == nil || event["threadId"] == "" { event["threadId"] = threadID } if event["runId"] == nil || event["runId"] == "" { event["runId"] = runID } + // Add timestamp if not present - critical for message timestamp tracking + if event["timestamp"] == nil || event["timestamp"] == "" { + event["timestamp"] = time.Now().UTC().Format(time.RFC3339Nano) + } // Broadcast to run-specific SSE subscribers activeRunState.BroadcastFull(event) diff --git a/components/backend/websocket/agui_proxy.go b/components/backend/websocket/agui_proxy.go index 076cfb3d9..106d8320b 100644 --- a/components/backend/websocket/agui_proxy.go +++ b/components/backend/websocket/agui_proxy.go @@ -274,13 +274,17 @@ func handleStreamedEvent(sessionID, runID, threadID, jsonData string, runState * eventType, _ := event["type"].(string) - // Ensure threadId and runId are set + // Ensure threadId, runId, and timestamp are set if _, ok := event["threadId"]; !ok { event["threadId"] = threadID } if _, ok := event["runId"]; !ok { event["runId"] = runID } + // Add timestamp if not present - critical for message timestamp tracking + if _, ok := event["timestamp"]; !ok { + event["timestamp"] = time.Now().UTC().Format(time.RFC3339Nano) + } // Check for terminal events switch eventType { diff --git a/components/backend/websocket/compaction.go b/components/backend/websocket/compaction.go index d03f77f7c..caa45add9 100644 --- a/components/backend/websocket/compaction.go +++ b/components/backend/websocket/compaction.go @@ -118,11 +118,14 @@ func (c *MessageCompactor) handleTextMessageStart(event map[string]interface{}) if role == "" { role = types.RoleAssistant } + // Preserve timestamp from the event + timestamp, _ := event["timestamp"].(string) c.currentMessage = &types.Message{ - ID: messageID, - Role: role, - Content: "", + ID: messageID, + Role: role, + Content: "", + Timestamp: timestamp, } } @@ -228,17 +231,25 @@ func (c *MessageCompactor) handleToolCallEnd(event map[string]interface{}) { tc.Status = "error" } + // Preserve timestamp from the event + timestamp, _ := event["timestamp"].(string) + // Add to message // Check if we need to create a new message or add to current if c.currentMessage != nil && c.currentMessage.Role == types.RoleAssistant { // Add to current message c.currentMessage.ToolCalls = append(c.currentMessage.ToolCalls, tc) + // Update timestamp if not already set + if c.currentMessage.Timestamp == "" && timestamp != "" { + c.currentMessage.Timestamp = timestamp + } } else { // Create new message for this tool call c.messages = append(c.messages, types.Message{ ID: uuid.New().String(), Role: types.RoleAssistant, ToolCalls: []types.ToolCall{tc}, + Timestamp: timestamp, }) } diff --git a/components/frontend/src/app/projects/[name]/sessions/[sessionName]/page.tsx b/components/frontend/src/app/projects/[name]/sessions/[sessionName]/page.tsx index 213a21489..ccf91987a 100644 --- a/components/frontend/src/app/projects/[name]/sessions/[sessionName]/page.tsx +++ b/components/frontend/src/app/projects/[name]/sessions/[sessionName]/page.tsx @@ -759,6 +759,8 @@ export default function ProjectSessionDetailPage({ const allToolCalls = new Map(); for (const msg of aguiState.messages) { + // Use msg.timestamp from backend, fallback to current time for legacy messages + // Note: After backend fix, new messages will have proper timestamps const timestamp = msg.timestamp || new Date().toISOString(); if (msg.toolCalls && Array.isArray(msg.toolCalls)) { @@ -852,6 +854,7 @@ export default function ProjectSessionDetailPage({ // Phase C: Process messages and build hierarchical structure for (const msg of aguiState.messages) { + // Use msg.timestamp from backend, fallback to current time for legacy messages const timestamp = msg.timestamp || new Date().toISOString(); // Handle text content by role diff --git a/components/frontend/src/hooks/use-agui-stream.ts b/components/frontend/src/hooks/use-agui-stream.ts index 9b08c45a9..84dfd3e3c 100644 --- a/components/frontend/src/hooks/use-agui-stream.ts +++ b/components/frontend/src/hooks/use-agui-stream.ts @@ -142,6 +142,7 @@ export function useAGUIStream(options: UseAGUIStreamOptions): UseAGUIStreamRetur id: newState.currentMessage.id || crypto.randomUUID(), role: newState.currentMessage.role || AGUIRole.ASSISTANT, content: newState.currentMessage.content, + timestamp: event.timestamp, } newState.messages = [...newState.messages, msg] onMessage?.(msg) @@ -214,6 +215,7 @@ export function useAGUIStream(options: UseAGUIStreamOptions): UseAGUIStreamRetur id: messageId, role: newState.currentMessage.role || AGUIRole.ASSISTANT, content: newState.currentMessage.content, + timestamp: event.timestamp, } newState.messages = [...newState.messages, msg] onMessage?.(msg) @@ -415,6 +417,7 @@ export function useAGUIStream(options: UseAGUIStreamOptions): UseAGUIStreamRetur toolCallId: toolCallId, name: toolCallName, toolCalls: [completedToolCall], + timestamp: event.timestamp, } messages.push(toolMessage) } @@ -546,6 +549,7 @@ export function useAGUIStream(options: UseAGUIStreamOptions): UseAGUIStreamRetur thinking: actualRawData.thinking as string, signature: actualRawData.signature as string, }, + timestamp: event.timestamp, } newState.messages = [...newState.messages, msg] onMessage?.(msg) @@ -562,6 +566,7 @@ export function useAGUIStream(options: UseAGUIStreamOptions): UseAGUIStreamRetur id: messageId, role: AGUIRole.USER, content: actualRawData.content as string, + timestamp: event.timestamp, } newState.messages = [...newState.messages, msg] onMessage?.(msg) @@ -575,6 +580,7 @@ export function useAGUIStream(options: UseAGUIStreamOptions): UseAGUIStreamRetur id: (actualRawData.id as string) || crypto.randomUUID(), role: actualRawData.role as AGUIMessage['role'], content: actualRawData.content as string, + timestamp: event.timestamp, } newState.messages = [...newState.messages, msg] onMessage?.(msg) From cd0df1253cdea58a094197f3a915c34f3e606ade Mon Sep 17 00:00:00 2001 From: Andy Braren Date: Fri, 30 Jan 2026 02:11:25 -0500 Subject: [PATCH 2/5] Fix port forwarding service names and don't suppress output --- Makefile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 1edcec9e4..747fd5185 100644 --- a/Makefile +++ b/Makefile @@ -596,9 +596,9 @@ kind-port-forward: check-kubectl ## Port-forward kind services (for remote Podma @echo "" @echo "$(COLOR_YELLOW)Press Ctrl+C to stop$(COLOR_RESET)" @echo "" - @trap 'echo ""; echo "$(COLOR_GREEN)✓$(COLOR_RESET) Port forwarding stopped"; exit 0' INT; \ - (kubectl port-forward -n ambient-code svc/frontend 8080:3000 >/dev/null 2>&1 &); \ - (kubectl port-forward -n ambient-code svc/backend-api 8081:8080 >/dev/null 2>&1 &); \ + @trap 'kill 0; echo ""; echo "$(COLOR_GREEN)✓$(COLOR_RESET) Port forwarding stopped"; exit 0' INT TERM; \ + kubectl port-forward -n ambient-code svc/frontend-service 8080:3000 & \ + kubectl port-forward -n ambient-code svc/backend-service 8081:8080 & \ wait ##@ E2E Testing (Portable) From beed831f07f8bb4fafb86987611cb21ca5b017b8 Mon Sep 17 00:00:00 2001 From: Andy Braren Date: Mon, 2 Feb 2026 01:25:34 -0500 Subject: [PATCH 3/5] Enhance timestamp handling in ProjectSessionDetailPage and related components - Captured timestamps from AGUI events in the useAGUIStream hook for improved message tracking. This change improves the accuracy and reliability of message timestamps across the application. --- .../components/welcome-experience.tsx | 14 +++++--------- .../[name]/sessions/[sessionName]/page.tsx | 18 ++++++++++++------ .../frontend/src/hooks/use-agui-stream.ts | 2 ++ components/frontend/src/types/agui.ts | 2 ++ 4 files changed, 21 insertions(+), 15 deletions(-) diff --git a/components/frontend/src/app/projects/[name]/sessions/[sessionName]/components/welcome-experience.tsx b/components/frontend/src/app/projects/[name]/sessions/[sessionName]/components/welcome-experience.tsx index f41e4440d..9d41a4dbe 100644 --- a/components/frontend/src/app/projects/[name]/sessions/[sessionName]/components/welcome-experience.tsx +++ b/components/frontend/src/app/projects/[name]/sessions/[sessionName]/components/welcome-experience.tsx @@ -207,8 +207,6 @@ export function WelcomeExperience({ {/* Message Content */}
- {/* Timestamp */} -
just now
{/* Content */}

@@ -400,13 +398,11 @@ export function WelcomeExperience({

- {/* Message Content */} -
- {/* Timestamp */} -
just now
-
- {/* Content */} -

+ {/* Message Content */} +

+
+ {/* Content */} +

{!isSetupTypingComplete ? ( <> {setupDisplayedText.slice(0, -3)} diff --git a/components/frontend/src/app/projects/[name]/sessions/[sessionName]/page.tsx b/components/frontend/src/app/projects/[name]/sessions/[sessionName]/page.tsx index ccf91987a..1317b92b3 100644 --- a/components/frontend/src/app/projects/[name]/sessions/[sessionName]/page.tsx +++ b/components/frontend/src/app/projects/[name]/sessions/[sessionName]/page.tsx @@ -793,7 +793,8 @@ export default function ProjectSessionDetailPage({ if (!allToolCalls.has(streamingToolId)) { allToolCalls.set(streamingToolId, { tc: streamingTC, - timestamp: new Date().toISOString() + // Use timestamp from currentToolCall if available, fallback to current time for legacy + timestamp: aguiState.pendingToolCalls?.get(streamingToolId)?.timestamp || new Date().toISOString() }); } } @@ -809,7 +810,8 @@ export default function ProjectSessionDetailPage({ if (!allToolCalls.has(tc.id)) { allToolCalls.set(tc.id, { tc: tc, - timestamp: new Date().toISOString(), + // Use timestamp from child message if available, fallback to current time + timestamp: childMsg.timestamp || new Date().toISOString(), }); } } @@ -977,7 +979,8 @@ export default function ProjectSessionDetailPage({ type: "agent_message", content: { type: "text_block", text: aguiState.currentMessage.content }, model: "claude", - timestamp: new Date().toISOString(), + // Use timestamp from currentMessage (captured from TEXT_MESSAGE_START), fallback to current time + timestamp: aguiState.currentMessage.timestamp || new Date().toISOString(), streaming: true, } as MessageObject & { streaming?: boolean }); } @@ -1007,7 +1010,8 @@ export default function ProjectSessionDetailPage({ .map(childMsg => { const childTC = childMsg.toolCalls?.[0]; if (!childTC) return null; - return createToolMessage(childTC, new Date().toISOString()); + // Use timestamp from child message if available + return createToolMessage(childTC, childMsg.timestamp || new Date().toISOString()); }) .filter((c): c is ToolUseMessages => c !== null); @@ -1017,7 +1021,8 @@ export default function ProjectSessionDetailPage({ const childInput = parseToolArgs(childTool.args || ""); children.push({ type: "tool_use_messages", - timestamp: new Date().toISOString(), + // Use timestamp from pending tool call (captured from TOOL_CALL_START) + timestamp: childTool.timestamp || new Date().toISOString(), toolUseBlock: { type: "tool_use_block", id: childId, @@ -1048,7 +1053,8 @@ export default function ProjectSessionDetailPage({ const streamingToolMessage: HierarchicalToolMessage = { type: "tool_use_messages", - timestamp: new Date().toISOString(), + // Use timestamp from pending tool call (captured from TOOL_CALL_START) + timestamp: pendingTool.timestamp || new Date().toISOString(), toolUseBlock: { type: "tool_use_block", id: toolId, diff --git a/components/frontend/src/hooks/use-agui-stream.ts b/components/frontend/src/hooks/use-agui-stream.ts index 84dfd3e3c..bd302cecf 100644 --- a/components/frontend/src/hooks/use-agui-stream.ts +++ b/components/frontend/src/hooks/use-agui-stream.ts @@ -170,6 +170,7 @@ export function useAGUIStream(options: UseAGUIStreamOptions): UseAGUIStreamRetur id: event.messageId || null, role: event.role, content: '', + timestamp: event.timestamp, // Capture timestamp from event } return newState } @@ -237,6 +238,7 @@ export function useAGUIStream(options: UseAGUIStreamOptions): UseAGUIStreamRetur name: event.toolCallName || 'unknown_tool', args: '', parentToolUseId: parentToolId, + timestamp: event.timestamp, // Capture timestamp from event }); newState.pendingToolCalls = updatedPending; diff --git a/components/frontend/src/types/agui.ts b/components/frontend/src/types/agui.ts index 4dd7f7a56..fa1dd1609 100644 --- a/components/frontend/src/types/agui.ts +++ b/components/frontend/src/types/agui.ts @@ -308,6 +308,7 @@ export type PendingToolCall = { name: string args: string parentToolUseId?: string + timestamp?: string // Timestamp from TOOL_CALL_START event } // Feedback type for messages @@ -325,6 +326,7 @@ export type AGUIClientState = { id: string | null role: AGUIRoleValue | null content: string + timestamp?: string // Timestamp from TEXT_MESSAGE_START event } | null // DEPRECATED: Use pendingToolCalls instead for parallel tool call support currentToolCall: { From c1e651a0b94bac85190ba9025918354642bca800 Mon Sep 17 00:00:00 2001 From: Andy Braren Date: Mon, 2 Feb 2026 01:34:45 -0500 Subject: [PATCH 4/5] Enhance timestamp consistency across AG-UI components - Introduced constants for AG-UI timestamp formats in `agui.go` to ensure uniformity in timestamp handling. - Updated various components (`agui_proxy.go`, `agui.go`, `legacy_translator.go`) to utilize the new timestamp constants, improving accuracy and reliability in event timestamping. This change enhances the overall consistency of timestamp formatting across the application. --- components/backend/types/agui.go | 20 +++++++++++++++++-- components/backend/websocket/agui.go | 4 ++-- components/backend/websocket/agui_proxy.go | 2 +- .../backend/websocket/legacy_translator.go | 2 +- 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/components/backend/types/agui.go b/components/backend/types/agui.go index 730b607cb..457c3d595 100644 --- a/components/backend/types/agui.go +++ b/components/backend/types/agui.go @@ -4,6 +4,22 @@ package types import "time" +// Timestamp format constants for AG-UI events and metadata. +// These ensure consistent timestamp formatting across the codebase. +const ( + // AGUITimestampFormat is used for event timestamps that require nanosecond precision. + // This preserves event ordering when multiple events occur in rapid succession. + // Format: "2006-01-02T15:04:05.999999999Z07:00" (RFC3339 with nanoseconds) + // Used in: BaseEvent.Timestamp, streamed events + AGUITimestampFormat = time.RFC3339Nano + + // AGUIMetadataTimestampFormat is used for run/session metadata timestamps. + // This is sufficient for human-readable timestamps where nanosecond precision isn't needed. + // Format: "2006-01-02T15:04:05Z07:00" (RFC3339) + // Used in: AGUIRunMetadata.StartedAt, AGUIRunMetadata.FinishedAt + AGUIMetadataTimestampFormat = time.RFC3339 +) + // AG-UI Event Types as defined in the protocol specification // See: https://docs.ag-ui.com/concepts/events const ( @@ -62,7 +78,7 @@ type BaseEvent struct { Type string `json:"type"` ThreadID string `json:"threadId"` RunID string `json:"runId"` - Timestamp string `json:"timestamp"` + Timestamp string `json:"timestamp"` // Format: AGUITimestampFormat (RFC3339Nano) // Optional fields MessageID string `json:"messageId,omitempty"` ParentRunID string `json:"parentRunId,omitempty"` @@ -293,7 +309,7 @@ func NewBaseEvent(eventType, threadID, runID string) BaseEvent { Type: eventType, ThreadID: threadID, RunID: runID, - Timestamp: time.Now().UTC().Format(time.RFC3339Nano), + Timestamp: time.Now().UTC().Format(AGUITimestampFormat), } } diff --git a/components/backend/websocket/agui.go b/components/backend/websocket/agui.go index 666fe2f78..f7ebfe97a 100644 --- a/components/backend/websocket/agui.go +++ b/components/backend/websocket/agui.go @@ -125,7 +125,7 @@ func RouteAGUIEvent(sessionID string, event map[string]interface{}) { if activeRunState == nil { // Ensure timestamp is set before any early returns if event["timestamp"] == nil || event["timestamp"] == "" { - event["timestamp"] = time.Now().UTC().Format(time.RFC3339Nano) + event["timestamp"] = time.Now().UTC().Format(types.AGUITimestampFormat) } // Don't create lazy runs for terminal events - they should only apply to existing runs @@ -177,7 +177,7 @@ func RouteAGUIEvent(sessionID string, event map[string]interface{}) { } // Add timestamp if not present - critical for message timestamp tracking if event["timestamp"] == nil || event["timestamp"] == "" { - event["timestamp"] = time.Now().UTC().Format(time.RFC3339Nano) + event["timestamp"] = time.Now().UTC().Format(types.AGUITimestampFormat) } // Broadcast to run-specific SSE subscribers diff --git a/components/backend/websocket/agui_proxy.go b/components/backend/websocket/agui_proxy.go index 106d8320b..37a878bc1 100644 --- a/components/backend/websocket/agui_proxy.go +++ b/components/backend/websocket/agui_proxy.go @@ -283,7 +283,7 @@ func handleStreamedEvent(sessionID, runID, threadID, jsonData string, runState * } // Add timestamp if not present - critical for message timestamp tracking if _, ok := event["timestamp"]; !ok { - event["timestamp"] = time.Now().UTC().Format(time.RFC3339Nano) + event["timestamp"] = time.Now().UTC().Format(types.AGUITimestampFormat) } // Check for terminal events diff --git a/components/backend/websocket/legacy_translator.go b/components/backend/websocket/legacy_translator.go index bd61d6c8f..68939729e 100644 --- a/components/backend/websocket/legacy_translator.go +++ b/components/backend/websocket/legacy_translator.go @@ -86,7 +86,7 @@ func MigrateLegacySessionToAGUI(sessionID string) error { "type": types.EventTypeMessagesSnapshot, "threadId": sessionID, "runId": "legacy-migration", - "timestamp": time.Now().UTC().Format(time.RFC3339Nano), + "timestamp": time.Now().UTC().Format(types.AGUITimestampFormat), "messages": messages, } From 23b77bd09b9302b33dcfdeb7fef1c6ea0d98f23b Mon Sep 17 00:00:00 2001 From: Andy Braren Date: Mon, 2 Feb 2026 01:35:38 -0500 Subject: [PATCH 5/5] Revert Makefile adjustment --- Makefile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 747fd5185..1edcec9e4 100644 --- a/Makefile +++ b/Makefile @@ -596,9 +596,9 @@ kind-port-forward: check-kubectl ## Port-forward kind services (for remote Podma @echo "" @echo "$(COLOR_YELLOW)Press Ctrl+C to stop$(COLOR_RESET)" @echo "" - @trap 'kill 0; echo ""; echo "$(COLOR_GREEN)✓$(COLOR_RESET) Port forwarding stopped"; exit 0' INT TERM; \ - kubectl port-forward -n ambient-code svc/frontend-service 8080:3000 & \ - kubectl port-forward -n ambient-code svc/backend-service 8081:8080 & \ + @trap 'echo ""; echo "$(COLOR_GREEN)✓$(COLOR_RESET) Port forwarding stopped"; exit 0' INT; \ + (kubectl port-forward -n ambient-code svc/frontend 8080:3000 >/dev/null 2>&1 &); \ + (kubectl port-forward -n ambient-code svc/backend-api 8081:8080 >/dev/null 2>&1 &); \ wait ##@ E2E Testing (Portable)