Skip to content

feat(gateway): optional per-message ISO-8601 timestamp prefix#26514

Open
Siyfion wants to merge 2 commits into
NousResearch:mainfrom
Siyfion:feat/telegram-message-timestamps
Open

feat(gateway): optional per-message ISO-8601 timestamp prefix#26514
Siyfion wants to merge 2 commits into
NousResearch:mainfrom
Siyfion:feat/telegram-message-timestamps

Conversation

@Siyfion

@Siyfion Siyfion commented May 15, 2026

Copy link
Copy Markdown

Closes #9628.

Summary

Adds an opt-in timestamp_messages flag to GatewayConfig. When enabled, every inbound user message is prepended with an ISO-8601 timestamp before it enters the agent loop:

hello                                  # default (unchanged)
[2026-05-15T19:25:00+01:00] hello      # with timestamp_messages: true

Motivation

Same problem #9628 describes: in long-running messaging-platform sessions, the only clock signal the model has is the "session start" line in the system prompt. Once a session has been alive for several hours, the model has no way to tell whether the most recent user message arrived seconds or hours ago — leading to gaffes like cheerily saying "morning!" twelve hours into a session, or losing track of "pepperami for dinner" being a 7pm thing and not a breakfast thing.

Real-world prompt for this PR: I was 12.5 hours into a Telegram session this evening and the agent kept lighting up with "morning!" because it had latched onto the original session-start timestamp and never updated its mental clock.

Differences from #9628's sketch

The issue proposes [MM-dd HH:mm] in UTC, keyed under gateway.message_timestamp_prefix. This PR ships:

  • ISO 8601 with numeric offset ([2026-05-15T19:25:00+01:00]) rather than compact UTC — full ISO is unambiguous, includes the year (matters for multi-week sessions), and shows the user's local time directly without requiring the model to do tz math
  • Top-level timestamp_messages: true to match existing top-level gateway flags (group_sessions_per_user, thread_sessions_per_user, unauthorized_dm_behavior) rather than the nested form
  • Sourced from MessageEvent.timestamp (i.e. the platform's date field) rather than datetime.now() at processing time — stays accurate even if messages queue
  • Happy to flip format / config key if reviewers prefer the issue's original shape

Design

  • New config flag: timestamp_messages: false (default off — opt-in to avoid surprising consumers that grep transcript text)
  • Rendered in the gateway host's local timezone via datetime.astimezone(), with the standard TZ env var honoured
  • ISO 8601, second precision, with numeric offset
  • Applied inside _prepare_inbound_message_text so both the normal inbound path and the queued follow-up path get the prefix consistently
  • Composes cleanly with the existing shared-group sender prefix: [2026-05-15T19:25:00+01:00] [Alice] hello
  • Failures in timestamp formatting are caught and logged; never block message delivery
  • Currently exercised on Telegram (which already populates event.timestamp from message.date); other platforms with a populated MessageEvent.timestamp will work automatically

Test plan

New file: tests/gateway/test_timestamp_messages.py — 5 cases covering:

  • prefix added when enabled (with ISO regex assertion)
  • prefix skipped by default
  • numeric offset is preserved end-to-end
  • composes with the shared-group [sender] prefix
  • malformed timestamp falls back gracefully (message passes through unmodified)
$ python -m pytest tests/gateway/test_config.py \
                    tests/gateway/test_timestamp_messages.py \
                    tests/gateway/test_shared_group_sender_prefix.py \
                    tests/gateway/test_session.py \
                    tests/gateway/test_reply_to_injection.py -q
138 passed in 4.71s

Docs

User-guide entry added under configuration.md → "Per-Message Timestamps".

Notes

🤖 Drafted with Hermes Agent

Add an opt-in 'timestamp_messages' flag to GatewayConfig that prepends
each inbound user message with an ISO-8601 timestamp (rendered in the
gateway host's local timezone) before it enters the agent loop.

Motivation: in long-running messaging-platform sessions the only clock
signal the model has is the system prompt's 'session start' line. Once
a session has been alive for several hours, the model has no way to
tell whether the most recent user message arrived seconds or hours ago,
which leads to gaffes like saying 'morning!' in the evening.

The timestamp is sourced from the platform's own message timestamp
(MessageEvent.timestamp) rather than the gateway's wall clock at
processing time, so it stays accurate even if messages queue.

- New config: timestamp_messages: false (default off — opt-in to avoid
  surprising consumers that grep transcript text)
- Wired up in _prepare_inbound_message_text so both the normal inbound
  path and the queued follow-up path get the prefix consistently
- Currently exercised on Telegram (which already populates
  event.timestamp from message.date); other platforms with a populated
  MessageEvent.timestamp will work automatically
- Failures in timestamp formatting are caught and logged; never block
  message delivery
- 5 unit tests covering: on/off, numeric offset preservation,
  interaction with the shared-group sender prefix, malformed timestamp
  robustness
- User-guide docs entry under 'Per-Message Timestamps'
@alt-glitch alt-glitch added type/feature New feature or request comp/gateway Gateway runner, session dispatch, delivery P3 Low — cosmetic, nice to have labels May 15, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

Duplicate of #9784 — competing implementation of per-message timestamp prefix for gateway. Also overlaps with #15192. All implement #9628.

@Maxwelltoo

Maxwelltoo commented May 18, 2026

Copy link
Copy Markdown

I like this PR and it works well on my local machine, but for continuous conversations, it's usually not necessary to add timestamps to every message.

I think we can add a configurable threshold — timestamps are only prepended when the gap between messages exceeds message_timestamp_threshold_seconds, which avoids wasting tokens on every message in active conversations while still anchoring time in long-lived sessions.

@Siyfion

Siyfion commented May 18, 2026

Copy link
Copy Markdown
Author

Yeah, you're right... that's a better design than mine. Always-on timestamping was the simplest thing that worked for my 12+ hour Telegram sessions (model latching onto session-start time and cheerily saying "morning!" at 9pm), but during active back-and-forth it's just token waste... the model already has all the temporal context it needs from the recent message flow.

Proposed shape:

timestamp_messages: true
message_timestamp_threshold_seconds: 600  # default 10 minutes
  • Threshold defaults to 600s (10 min) when timestamp_messages is enabled... most time context doesn't expire faster than that unless it's been explicitly mentioned, so we avoid prefixing every rapid-fire message
  • Set to 0 to force prefix on every message (current behaviour)
  • Set higher (e.g. 3600) for chattier setups where you only want re-anchoring after big gaps
  • Per-session tracking of last inbound timestamp... probably belongs on GatewaySession, but happy to move it if reviewers prefer

Working on the update now. Thanks for the nudge 👍

…hold_seconds

Per @Maxwelltoo review on NousResearch#26514: prefixing every inbound message wastes
tokens during active back-and-forth where the model already has temporal
context. Add a configurable gap threshold so the prefix only re-anchors
after a meaningful pause.

- New: GatewayConfig.message_timestamp_threshold_seconds (default 600 / 10min)
- 0 preserves legacy always-on behaviour
- Tracked per-session via in-memory dict; rolling gap (anchor advances on
  every message, prefixed or not)
- First message after gateway restart re-anchors naturally (no prior ts)
- Negative gaps (out-of-order delivery) treated as 'within window' — never
  re-anchor retroactively

Adds 7 tests covering: first-message-prefix, in-window suppression,
post-gap re-anchor, threshold=0 always-on, per-session independence,
rolling anchor across suppressed messages, out-of-order graceful no-op.

Docs updated under configuration.md → Per-Message Timestamps.
@Siyfion

Siyfion commented May 18, 2026

Copy link
Copy Markdown
Author

Pushed in 38665a20e — gap-gated prefix is live on the branch.

Shape:

timestamp_messages: true
message_timestamp_threshold_seconds: 600  # default 10min; set to 0 for legacy always-on

Behaviour:

  • First message in a session (or after gateway restart) always gets a prefix... no prior timestamp = re-anchor moment
  • Within the threshold window → no prefix, just clean text
  • Gap is rolling, not anchored to last-prefixed message... so three messages 5min apart with a 10min threshold prefix once (the first), not twice
  • Per-session tracking via in-memory Dict[session_key, datetime] on the runner — gateway restart is a natural re-anchor, no persistence needed
  • Out-of-order delivery (negative gaps) treated as in-window → never re-anchors retroactively

Tests: 7 new cases covering first-message-always-prefixes, in-window suppression, post-gap re-anchor, threshold=0 always-on, per-session independence, rolling-anchor across suppressed messages, and out-of-order graceful no-op. 145/145 across tests/gateway/test_timestamp_messages.py, test_config.py, test_shared_group_sender_prefix.py, test_session.py, test_reply_to_injection.py pass.

Docs updated under configuration.md → Per-Message Timestamps with the full behavioural notes.

Ready for another look 👀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery P3 Low — cosmetic, nice to have type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: configurable message timestamp prefix to prevent temporal drift in long-lived sessions

3 participants