diff --git a/agent/agent_init.py b/agent/agent_init.py index 3687fe55970..3a648c1b955 100644 --- a/agent/agent_init.py +++ b/agent/agent_init.py @@ -599,6 +599,7 @@ def init_agent( # (e.g. CLI voice mode adds a temporary prefix for the live call only). agent._persist_user_message_idx = None agent._persist_user_message_override = None + agent._persist_user_message_timestamp = None # Cache anthropic image-to-text fallbacks per image payload/URL so a # single tool loop does not repeatedly re-run auxiliary vision on the diff --git a/agent/conversation_loop.py b/agent/conversation_loop.py index 45722d2657f..099cefd36e1 100644 --- a/agent/conversation_loop.py +++ b/agent/conversation_loop.py @@ -474,6 +474,7 @@ def run_conversation( task_id: str = None, stream_callback: Optional[callable] = None, persist_user_message: Optional[str] = None, + persist_user_timestamp: Optional[float] = None, ) -> Dict[str, Any]: """ Run a complete conversation with tool calling until completion. @@ -489,6 +490,8 @@ def run_conversation( persist_user_message: Optional clean user message to store in transcripts/history when user_message contains API-only synthetic prefixes. + persist_user_timestamp: Optional platform event timestamp to store + as metadata on that persisted user message. or queuing follow-up prefetch work. Returns: @@ -510,6 +513,7 @@ def run_conversation( task_id, stream_callback, persist_user_message, + persist_user_timestamp, restore_or_build_system_prompt=_restore_or_build_system_prompt, install_safe_stdio=_install_safe_stdio, sanitize_surrogates=_sanitize_surrogates, diff --git a/agent/turn_context.py b/agent/turn_context.py index e94d43279ab..8041eabdb7f 100644 --- a/agent/turn_context.py +++ b/agent/turn_context.py @@ -69,6 +69,7 @@ def build_turn_context( task_id: Optional[str], stream_callback, persist_user_message: Optional[str], + persist_user_timestamp: Optional[float] = None, *, restore_or_build_system_prompt, install_safe_stdio, @@ -121,6 +122,7 @@ def build_turn_context( agent._stream_callback = stream_callback agent._persist_user_message_idx = None agent._persist_user_message_override = persist_user_message + agent._persist_user_message_timestamp = persist_user_timestamp # Generate unique task_id if not provided to isolate VMs between tasks. effective_task_id = task_id or str(uuid.uuid4()) agent._current_task_id = effective_task_id diff --git a/gateway/message_timestamps.py b/gateway/message_timestamps.py new file mode 100644 index 00000000000..8023099a385 --- /dev/null +++ b/gateway/message_timestamps.py @@ -0,0 +1,166 @@ +"""Helpers for rendering gateway message timestamps exactly once. + +Gateway messages need timestamps in the LLM context for temporal awareness, but +persisted message content should stay clean so replay does not accumulate +``[timestamp] [timestamp] ...`` prefixes across turns. +""" + +from __future__ import annotations + +import re +from datetime import datetime +from typing import Any, Optional, Tuple + + +# Current gateway format: [Tue 2026-04-28 13:40:53 CEST] +_HUMAN_TIMESTAMP_RE = re.compile( + r"^\[(?P[A-Z][a-z]{2}) " + r"(?P\d{4}-\d{2}-\d{2}) " + r"(?P