From bd7fc8fdcd67ff892cc5bdbfd76747adc6abe1b1 Mon Sep 17 00:00:00 2001 From: Wolfram Ravenwolf Date: Mon, 18 May 2026 03:51:58 +0200 Subject: [PATCH] feat(gateway): inject stable human-readable message timestamps Consolidates these related Amy fork patches: - 429830f39 feat(gateway): inject message timestamps into user messages for LLM context - 3c3d6fac0 fix: handle both ISO string and epoch float timestamps in history replay - 2874f7725 feat: human-friendly timestamp format with weekday and timezone name - 3735f4c8b fix: render gateway message timestamps once --- agent/agent_init.py | 1 + agent/conversation_loop.py | 4 + agent/turn_context.py | 2 + gateway/message_timestamps.py | 166 +++++++++++++++++++++++ gateway/run.py | 96 ++++++++++++- gateway/session.py | 1 + hermes_state.py | 33 ++++- run_agent.py | 24 +++- tests/acp/test_session.py | 7 +- tests/gateway/test_fast_command.py | 10 +- tests/gateway/test_message_timestamps.py | 91 +++++++++++++ tests/gateway/test_session_api.py | 6 +- tests/test_hermes_state.py | 35 +++-- 13 files changed, 448 insertions(+), 28 deletions(-) create mode 100644 gateway/message_timestamps.py create mode 100644 tests/gateway/test_message_timestamps.py 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