Skip to content

Preserve original message timestamps through fork/compress/branch#28840

Open
yitang wants to merge 2 commits into
NousResearch:mainfrom
yitang:feat/preserve-message-timestamps
Open

Preserve original message timestamps through fork/compress/branch#28840
yitang wants to merge 2 commits into
NousResearch:mainfrom
yitang:feat/preserve-message-timestamps

Conversation

@yitang

@yitang yitang commented May 19, 2026

Copy link
Copy Markdown

Preserve original message timestamps through fork/compress/branch

Summary

messages.timestamp is always time.time() at INSERT time. When messages are
copied between sessions during /branch, compression, or /retry, their
original timestamps are silently discarded. This change adds an optional
timestamp parameter to append_message() and reads msg.get("timestamp")
in replace_messages(), then forwards it from every caller that has the field
available.

All changes are backward compatible: callers that don't pass timestamp get
identical time.time() behaviour.

Changes

File Change
hermes_state.py append_message(): added optional timestamp: float = None param; uses it instead of time.time() when set. replace_messages(): reads msg.get("timestamp") per message, falls back to time.time()
run_agent.py Forwards timestamp=msg.get("timestamp") in _flush_messages_to_session_db()
gateway/run.py Forwards timestamp=msg.get("timestamp") in /branch handler
cli.py Forwards timestamp=msg.get("timestamp") in branch handler
tui_gateway/server.py Forwards timestamp=msg.get("timestamp") in branch handler
gateway/session.py Forwards timestamp=message.get("timestamp") in append_to_transcript()
gateway/mirror.py Forwards timestamp=message.get("timestamp") in _append_to_sqlite()
tests/test_hermes_state.py Added TestTimestampPreservation with 8 tests

All 8 files: each caller forwards msg.get("timestamp") to append_message()
or replace_messages(). The core fix in hermes_state.py (accept + store the
supplied timestamp) makes every caller work correctly — both the ones modified here
and existing ones using replace_messages() with timestamp-bearing dicts.

Backward compatibility

  • timestamp=None (default) → time.time() as before.
  • No signature changes for positional callers — timestamp is keyword-only
    with default.
  • replace_messages() falls back to base_ts = time.time() when a message
    dict has no timestamp key.

Testing

8 new tests in tests/test_hermes_state.py::TestTimestampPreservation:

Test What it verifies
test_append_message_with_explicit_timestamp Single explicit timestamp round-trips
test_append_message_multiple_timestamps Multiple different timestamps keep order
test_append_message_without_timestamp_defaults No timestamp → time.time() fallback
test_append_message_mixed_timestamps Explicit + default blend correctly
test_replace_messages_preserves_timestamps Dict timestamps survive replace_messages
test_replace_messages_fallback_when_no_timestamp No timestamp → auto-increment fallback
test_replace_messages_mixed_timestamps Dict timestamps + fallback blend correctly
test_fork_chain_preserves_timestamps /branch copy preserves original timestamps
python3 -m pytest tests/test_hermes_state.py -q -o addopts=
→ 220 passed in ~4.1s

Implementation notes

The root cause was narrow:

  1. append_message() never accepted a caller-supplied timestamp → 1 param + 1 conditional
  2. replace_messages() never read timestamps from dicts → 3 lines changed

The 6 callers (branch handlers, mirror, flush, transcript) all needed a 1-line
forwarding change. Existing callers using replace_messages() with
timestamp-bearing dicts (rewrite_transcript, ACP persist) worked automatically
once the core fix was in place.

messages.timestamp is always time.time() at INSERT time. When messages are
copied between sessions during /branch, compression, or /retry, their
original timestamps are silently discarded.

This change adds an optional timestamp parameter to append_message() and
reads msg.get('timestamp') in replace_messages(), then forwards it from
every caller that has the field available.

All changes are backward compatible: callers that do not pass timestamp
get identical time.time() behaviour.
@yitang yitang force-pushed the feat/preserve-message-timestamps branch from 6fed2bd to dbcdc01 Compare May 19, 2026 17:00
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/agent Core agent loop, run_agent.py, prompt builder comp/gateway Gateway runner, session dispatch, delivery comp/cli CLI entry point, hermes_cli/, setup wizard comp/tui Terminal UI (ui-tui/ + tui_gateway/) labels May 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent loop, run_agent.py, prompt builder comp/cli CLI entry point, hermes_cli/, setup wizard comp/gateway Gateway runner, session dispatch, delivery comp/tui Terminal UI (ui-tui/ + tui_gateway/) P2 Medium — degraded but workaround exists type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants