fix: prevent stale timestamp perception by injecting current time per-turn#15872
fix: prevent stale timestamp perception by injecting current time per-turn#15872quinnmacro wants to merge 1 commit into
Conversation
|
Likely duplicate of #10448 |
Bartok9
left a comment
There was a problem hiding this comment.
I verified against current main (59b56d44): the %I:%M %p in _build_system_prompt at L4567 is indeed evaluated at build time and baked into the system prompt, which invalidates the KV cache prefix on every _build_system_prompt re-invocation (cache-miss paths as the original issue reporter identified).
The approach here — user-message injection via _plugin_user_context — is the correct pattern. The codebase already uses this exact mechanism for plugin context (L9143), external recall, and pre_llm_call hook output, so this is consistent.
Reviewed all four changes:
- System prompt rename (
Conversation started → Session started): Clean semantic distinction. The timezone suffix is a nice touch for multi-timezone users. - Per-turn injection: Correctly prepends to
_plugin_user_context(before the main loop), so it composes with existing plugin context. Thetry/exceptfallback is appropriate sincehermes_timedepends on optional timezone configuration. - Max-iterations path: Good catch — the
_handle_max_iterationsrecovery path is a separate code path that also needs current time. Mirrors the main loop injection. - Tests: All four tests cover the right invariants (system prompt must NOT contain
Current time:, user message must contain it, max-iterations path included).
One thought: the Session started: line still contains %I:%M %p (minute precision). Since it's now clearly labeled as session start rather than current time, agents won't confuse it for 'now'. But for maximum cache stability, could the minute portion be dropped from the system prompt entirely? The per-turn injection now provides the precise time. The session start only needs date + approximate time for session context. Not blocking — just a potential follow-up optimization.
|
Thanks for reviewing @alt-glitch! This is not a duplicate of #10448 — they fix the same symptom but with fundamentally different architectures that have real production implications. Key difference: prompt cache preservation
The Hermes codebase explicitly documents this principle (in
This isn't theoretical — Anthropic's 2026 best practice also recommends per-turn context in the user message specifically for cache stability. When the system prompt changes each turn, every API call pays full token cost for the system prompt instead of leveraging the cached prefix. What #10448 does better
What this PR does better
Proposed resolutionThe ideal fix would combine:
I'm happy to add codex responses coverage to this PR if the maintainers prefer the user-message approach, or to close this PR in favor of #10448 if they accept the cache tradeoff. But they are architecturally different and shouldn't be merged as duplicates — the cache question needs a deliberate decision. |
161ffd0 to
9dc50cc
Compare
|
Thanks for the thorough review @Bartok9! Appreciate the ✅ on all four changes. Re: your suggestion about dropping the minute portion from I'll hold off on making that change for now to keep this PR focused on the core fix. Happy to follow up in a separate PR if the maintainers want that optimization. Also — I've rebased #12630 and #12987 onto the latest |
|
This PR looks like the preferred salvage path for #17459/#17476 because it separates stable session-start context from live per-turn time context rather than mutating the cached system prompt. Please align the final version with the fan-out criteria:
|
9dc50cc to
046f2de
Compare
|
Thanks for the clear alignment criteria @markojak. I've addressed all five points in the latest update (single squashed commit Alignment checklist
What changed from previous version
Not changed (by design)
|
When a user configured a timezone (HERMES_TIMEZONE env var or `timezone:` in ~/.hermes/config.yaml), `date` and TZ-honouring runtimes inside the terminal tool reported the host's wall clock instead of the user's zone, so shell output disagreed with the user's configured timezone (often UTC vs. the user's actual zone on VPS deployments). Fixed end-to-end via centralized injection points instead of touching each backend individually: - BaseEnvironment._wrap_command injects `export TZ=<name>` once. That covers local, docker, ssh, singularity, modal, daytona, and vercel_sandbox — all flow through this method. Managed Modal uses a different exec path (BaseModalExecutionEnvironment._prepare_modal_exec), so it gets the same injection there. - local.py `_make_run_env` and `_sanitize_subprocess_env` set TZ on the subprocess env so PTY/background terminals (spawned via process_registry.spawn_local) and the init_session bootstrap that runs before the snapshot exists also agree. - code_execution_tool.py `_run_local_sandbox`: previously read HERMES_TIMEZONE directly via os.getenv, silently missing config.yaml- only setups (the same bug class). Switched to hermes_time.get_timezone() so both env-var and config.yaml paths resolve consistently. The redundant inline `TZ=...` prefix on the remote-backend code path was dropped — _wrap_command already exports TZ before the script runs. - New `_resolve_configured_tz_name()` helper in tools/environments/base.py reads via hermes_time.get_timezone(), which checks both HERMES_TIMEZONE and ~/.hermes/config.yaml. CLI entry points like `hermes chat` don't bridge config.yaml -> env var (only gateway/run.py does), so an env- var-only check would silently miss config.yaml-only setups. Scope note: this PR only touches terminal TZ propagation. An earlier revision also surfaced the timezone in the agent's system prompt; that piece was removed per review feedback in NousResearch#17459/NousResearch#17476 — live time/ timezone context belongs in the ephemeral per-turn user-message path (NousResearch#15872), not the cached system prompt prefix. Terminal TZ propagation is independently useful and tested separately from prompt-cache behavior. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
046f2de to
67723f5
Compare
Rebased onto latest
|
| This PR (#15872) | #10448 | #18135 | |
|---|---|---|---|
| Injection target | User message via _plugin_user_context |
System prompt (breaks cache) | User message via prepend_current_time_context() |
| Cache-safe | ✅ | ❌ | ✅ |
| Bartok9 approved | ✅ | — | — |
| #10061 timezone alignment | ✅ IANA + UTC offset | — | %Z only |
_handle_max_iterations |
✅ | — | — |
codex_responses path |
✅ | ✅ | — |
| Multimodal content | — | — | ✅ |
| Idempotent injection | — | — | ✅ |
| Clean transcripts | — | — | ✅ |
What I would adopt from #18135
The prepend_current_time_context() helper in hermes_time.py is well-designed — especially the idempotent check and multimodal list-content support. If maintainers prefer, I can refactor this PR to use that helper (keeping _plugin_user_context as the injection channel, timezone aligned with #10061, and max_iterations + codex_responses coverage).
The transcript-cleanliness concern (_persist_user_message_override) is also valid — worth addressing in a follow-up or as part of the helper refactor.
Summary
This PR is now conflict-free, approved, and aligned with all five criteria from @markojak. Ready for merge or further iteration per maintainer preference.
Adopted
|
|
Nice work adopting the |
Approve — user-message injection is the right callThis is the correct fix. The core insight is right: injecting The two-layer split is clean:
The label rename from The max-iterations path injection ( 4 tests covering cache safety, user-message injection, and the max-iterations path. All passing. Relation to #10448: Same bug, wrong injection layer. This PR should supersede it. |
31ed790 to
1a8ab5d
Compare
1a8ab5d to
cffb977
Compare
|
Thanks for this PR — this is the correct approach. I have been manually injecting current time into the user message (same location, same reasoning: preserve prompt cache) across two versions of the codebase now, and it works exactly as expected. It solves the stale-timestamp problem without the cache penalty that #10448 would introduce. This is a ~5-line change with passing tests, and the core issue has been reported across multiple threads (#10421, #9628, #27742, #17459, #17476). It feels like the only thing missing is someone with a merge button making a call. Just wanted to add a user voice to the discussion — is there anything blocking this that the community can help with? |
cffb977 to
da5fe55
Compare
|
Rebased onto latest Changes adapted to the 3-module refactor:
All 5 tests passing, clean single commit (+95/-6). Maintainer alignment criteria (per @markojak):
Ready for merge. |
…-turn - Rename 'Conversation started' → 'Session started' in system prompt (unambiguous frozen label) - Add timezone display line to system prompt (IANA + UTC offset, aligned with NousResearch#10061) - Inject 'Current time:' per-turn into plugin_user_context (user message, not system prompt) - Inject current time into handle_max_iterations summary prompt - Add format_current_time_context() and get_timezone_display() helpers to hermes_time.py - 11 tests covering cache safety, time injection, timezone formatting Architecture: volatile time goes to user message (preserves prompt cache prefix), frozen session metadata stays in system prompt. Adapted to v2026.6.5 refactor: turn_context.py (new file) replaces the old conversation_loop.py injection point.
da5fe55 to
a38f6d0
Compare
Rebased onto v2026.6.5 (commit c94e93a)Adapted to the
Diff: +220/-1 across 5 files. Ready for merge. |
Problem
The system prompt includes a timestamp frozen at session creation time (for prompt cache stability). However, the label
"Conversation started: <time>"is ambiguous — agents interpret it as the current time, leading to incorrect time-sensitive behavior.Observed: In a session started at 5:07 AM, the agent said "good night 🌙" at 9:00 AM because the only timestamp it saw was
"Conversation started: Sunday, April 26, 2026 05:07 AM".Solution
Split the timestamp into two layers, both using user-message injection to preserve prompt cache:
Session started: <time> (timezone)Current time: <time>\nTimezone: <tz>Why user-message injection (not system prompt)?
PR #10448 addresses the same bug but injects
Current time:into the system prompt via_build_turn_scoped_system_prompt(). This changes the system prompt content every turn, which breaks prompt cache prefix.This PR injects into the user message — the same mechanism that plugins use (
pre_llm_callcontext). The Hermes codebase itself documents this principle:User-message injection also composes naturally with the existing plugin system — time context merges with plugin context in
_plugin_user_context.Changes
run_agent.py—_build_system_prompt(): Rename"Conversation started"→"Session started"and add timezone namerun_agent.py—run_conversation(): Inject current time + timezone into_plugin_user_contexton every turnrun_agent.py—_handle_max_iterations(): Inject current time into the summary request user message (recovery path)hermes_time.py: Addget_timezone_name()public APITest results
Relation to #10448
Same bug, different injection location. See my comment on #10448 for the full comparison. Key difference: this PR preserves prompt cache by using user-message injection; #10448 breaks cache by modifying the system prompt per turn.
Token impact
~50 tokens per turn (two lines: current time + timezone). This is minimal compared to typical tool schemas and memory blocks, and the information is essential for correct agent behavior.