mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-17 09:41:58 +00:00
feat(gateway): gate message timestamps behind opt-in (default off)
Follow-up to salvaged PR #41633: the timestamp prefix injection was unconditional. Gate the in-context render behind gateway.message_timestamps.enabled (default false) at both the live-message and history-replay sites; timestamp metadata is still captured + persisted regardless so the toggle can be flipped on later. Add DEFAULT_CONFIG entry, docs, and gate tests.
This commit is contained in:
parent
bd7fc8fdcd
commit
36ae958473
4 changed files with 118 additions and 9 deletions
|
|
@ -692,10 +692,31 @@ def _uses_telegram_observed_group_context(channel_prompt: Optional[str]) -> bool
|
|||
return bool(channel_prompt and _TELEGRAM_OBSERVED_CONTEXT_PROMPT_MARKER in channel_prompt)
|
||||
|
||||
|
||||
def _message_timestamps_enabled(user_config: Optional[dict]) -> bool:
|
||||
"""True when gateway.message_timestamps.enabled is opted in.
|
||||
|
||||
Default OFF: injecting a ``[Tue 2026-04-28 13:40:53 CEST]`` prefix onto
|
||||
every user message changes what the model sees for all gateway users, so
|
||||
it must be explicitly enabled in config.yaml under
|
||||
``gateway.message_timestamps.enabled``.
|
||||
"""
|
||||
if not isinstance(user_config, dict):
|
||||
return False
|
||||
gw = user_config.get("gateway")
|
||||
if not isinstance(gw, dict):
|
||||
return False
|
||||
mt = gw.get("message_timestamps")
|
||||
if isinstance(mt, dict):
|
||||
return bool(mt.get("enabled", False))
|
||||
# Allow a bare ``message_timestamps: true`` shorthand.
|
||||
return bool(mt)
|
||||
|
||||
|
||||
def _build_gateway_agent_history(
|
||||
history: List[Dict[str, Any]],
|
||||
*,
|
||||
channel_prompt: Optional[str] = None,
|
||||
inject_timestamps: bool = False,
|
||||
) -> tuple[List[Dict[str, Any]], Optional[str]]:
|
||||
"""Convert stored gateway transcript rows into agent replay messages.
|
||||
|
||||
|
|
@ -704,6 +725,10 @@ def _build_gateway_agent_history(
|
|||
turns. Keeping that context out of ``conversation_history`` avoids
|
||||
consecutive-user repair merging it with the live user turn and then hiding
|
||||
the current message behind ``history_offset`` during persistence.
|
||||
|
||||
When ``inject_timestamps`` is True (gateway.message_timestamps.enabled),
|
||||
each replayed user message is rendered with a single human-readable
|
||||
timestamp prefix from its stored metadata.
|
||||
"""
|
||||
|
||||
from hermes_time import get_timezone as _get_msg_tz
|
||||
|
|
@ -731,7 +756,7 @@ def _build_gateway_agent_history(
|
|||
continue
|
||||
|
||||
content = msg.get("content")
|
||||
if role == "user" and isinstance(content, str):
|
||||
if inject_timestamps and role == "user" and isinstance(content, str):
|
||||
content = _render_msg_ts(content, msg.get("timestamp"), tz=_msg_tz)
|
||||
if separate_observed_context and msg.get("observed") and role == "user" and content:
|
||||
observed_group_context.append(str(content).strip())
|
||||
|
|
@ -8912,9 +8937,12 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
|
|||
if message_text is None:
|
||||
return
|
||||
|
||||
# Inject message timestamp so the LLM sees when this message was sent.
|
||||
# Keep the persisted transcript clean: timestamps are stored as
|
||||
# metadata and rendered into context exactly once on replay.
|
||||
# Capture the platform event time as message metadata and keep the
|
||||
# persisted transcript clean (strip any leading timestamp prefix).
|
||||
# This runs regardless of the toggle so storage stays clean and the
|
||||
# send-time is preserved. Only the in-context RENDER (prepending the
|
||||
# human-readable prefix the model sees) is gated behind
|
||||
# gateway.message_timestamps.enabled — default OFF.
|
||||
try:
|
||||
from hermes_time import get_timezone as _get_evt_tz
|
||||
from gateway.message_timestamps import (
|
||||
|
|
@ -8932,11 +8960,16 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
|
|||
persist_user_timestamp = (
|
||||
_event_epoch if _event_epoch is not None else _embedded_ts
|
||||
)
|
||||
message_text = _render_msg_ts(
|
||||
_clean_message_text,
|
||||
persist_user_timestamp,
|
||||
tz=_evt_tz,
|
||||
)
|
||||
if _message_timestamps_enabled(_load_gateway_config()):
|
||||
message_text = _render_msg_ts(
|
||||
_clean_message_text,
|
||||
persist_user_timestamp,
|
||||
tz=_evt_tz,
|
||||
)
|
||||
else:
|
||||
# Toggle off: model sees the clean message; the timestamp
|
||||
# is still stored as metadata for later opt-in.
|
||||
message_text = _clean_message_text
|
||||
except Exception as _ts_err:
|
||||
logger.debug("Message timestamp injection failed (non-fatal): %s", _ts_err)
|
||||
|
||||
|
|
@ -14991,6 +15024,7 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
|
|||
agent_history, observed_group_context = _build_gateway_agent_history(
|
||||
history,
|
||||
channel_prompt=channel_prompt,
|
||||
inject_timestamps=_message_timestamps_enabled(_load_gateway_config()),
|
||||
)
|
||||
|
||||
# Collect MEDIA paths already in history so we can exclude them
|
||||
|
|
|
|||
|
|
@ -2270,6 +2270,17 @@ DEFAULT_CONFIG = {
|
|||
# Gateway settings — control how messaging platforms (Telegram, Discord,
|
||||
# Slack, etc.) deliver agent-produced files as native attachments.
|
||||
"gateway": {
|
||||
# Inject a human-readable timestamp prefix (e.g.
|
||||
# "[Tue 2026-04-28 13:40:53 CEST]") onto user messages IN THE MODEL'S
|
||||
# CONTEXT so the agent has temporal awareness of when each message was
|
||||
# sent. Off by default — when off, the model sees clean message text.
|
||||
# Persisted transcripts always stay clean (the timestamp is stored as
|
||||
# message metadata regardless of this toggle), so turning it on later
|
||||
# surfaces send-times for past messages too.
|
||||
"message_timestamps": {
|
||||
"enabled": False,
|
||||
},
|
||||
|
||||
# When false (default), any file path the agent emits is delivered
|
||||
# as a native attachment as long as it isn't under the credential /
|
||||
# system-path denylist (/etc, /proc, ~/.ssh, ~/.aws, ~/.hermes/.env,
|
||||
|
|
|
|||
|
|
@ -89,3 +89,49 @@ def test_persist_user_message_override_keeps_clean_content_and_timestamp_metadat
|
|||
"timestamp": _epoch(2026, 4, 28, 13, 40, 53),
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Opt-in gate: gateway.message_timestamps.enabled (default OFF)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_message_timestamps_enabled_defaults_off():
|
||||
from gateway.run import _message_timestamps_enabled
|
||||
|
||||
assert _message_timestamps_enabled(None) is False
|
||||
assert _message_timestamps_enabled({}) is False
|
||||
assert _message_timestamps_enabled({"gateway": {}}) is False
|
||||
assert (
|
||||
_message_timestamps_enabled({"gateway": {"message_timestamps": {}}}) is False
|
||||
)
|
||||
|
||||
|
||||
def test_message_timestamps_enabled_when_opted_in():
|
||||
from gateway.run import _message_timestamps_enabled
|
||||
|
||||
assert _message_timestamps_enabled(
|
||||
{"gateway": {"message_timestamps": {"enabled": True}}}
|
||||
) is True
|
||||
# Bare shorthand also accepted.
|
||||
assert _message_timestamps_enabled({"gateway": {"message_timestamps": True}}) is True
|
||||
|
||||
|
||||
def test_build_history_injects_only_when_enabled():
|
||||
from gateway.run import _build_gateway_agent_history
|
||||
|
||||
history = [
|
||||
{"role": "user", "content": "hello", "timestamp": _epoch(2026, 4, 28, 13, 40, 53)},
|
||||
{"role": "assistant", "content": "hi"},
|
||||
]
|
||||
|
||||
# Default (off): user content stays clean, no timestamp prefix.
|
||||
agent_history, _ = _build_gateway_agent_history(history)
|
||||
assert agent_history[0]["content"] == "hello"
|
||||
|
||||
# Enabled: user content gets exactly one timestamp prefix.
|
||||
agent_history, _ = _build_gateway_agent_history(history, inject_timestamps=True)
|
||||
assert agent_history[0]["content"].startswith("[")
|
||||
assert agent_history[0]["content"].endswith("hello")
|
||||
# Assistant message is never timestamped.
|
||||
assert agent_history[1]["content"] == "hi"
|
||||
|
|
|
|||
|
|
@ -327,6 +327,24 @@ display:
|
|||
tool_progress_grouping: accumulate # accumulate | separate
|
||||
```
|
||||
|
||||
### Message timestamps in model context
|
||||
|
||||
Off by default. When enabled, Hermes prepends a human-readable timestamp
|
||||
(e.g. `[Tue 2026-04-28 13:40:53 CEST]`) onto each **user** message *in the
|
||||
model's context* so the agent knows when messages were sent — useful for
|
||||
temporal reasoning ("you asked this morning…", noticing a long gap). It is
|
||||
**not** added to assistant messages or the system prompt.
|
||||
|
||||
```yaml
|
||||
gateway:
|
||||
message_timestamps:
|
||||
enabled: false # set true to show send-times to the model
|
||||
```
|
||||
|
||||
Persisted transcripts always stay clean — the timestamp is stored as message
|
||||
metadata regardless of this toggle, so enabling it later also surfaces
|
||||
send-times for past messages, and replay never accumulates duplicate prefixes.
|
||||
|
||||
When enabled, the bot sends status messages as it works:
|
||||
|
||||
```text
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue